Files
coder/coderd/aibridged/provider.go
T
Cian Johnston 042421694f fix: surface duplicate AI provider hostname as warning, not error (#28208)
Two live `ai_providers` rows can share the same `base_url` hostname.
This is an intended configuration (AIGOV-97): the direct routing path
(`/api/v2/ai-gateway/{name}/...`) routes by provider name and fully
supports it. Only the MITM proxy path (`aibridgeproxyd`) cannot
disambiguate, because its `nameByHost` map is 1:1.

`classifyProviderRow` marked the second provider as
`ProviderStatusError`, which surfaced as a misleading log warning and a
`status=error` Prometheus label. The provider is not actually broken; it
is fully routable via the direct path. The error was only visible in
server logs and Prometheus, not in the API or UI.

This change:

- Extracts `BaseURLHostname` into `coderd/aibridged` and eliminates
three duplicate implementations (proxy classifier, chatprovider, and the
API layer now uses it for status checks).
- Downgrades the proxy classifier from `ProviderStatusError` to a new
`ProviderStatusProxyExcluded` status. The provider is not broken; it is
just not reachable via the MITM proxy path.
- Adds `Status *AIProviderStatus` to `codersdk.AIProvider` with
`Warnings []string`, matching the `WorkspaceProxy` precedent.
- Populates warnings on create, update, GET, and list when a provider's
hostname collides with another enabled provider.
- Adds an integration test proving that a duplicate provider is routable
via the direct path but not via the proxy path.

Refs AIGOV-596

Generated by Coder Agents



https://github.com/user-attachments/assets/bf323837-5121-4d1b-ab19-593f1d32374e
2026-08-24 13:27:22 +01:00

58 lines
1.9 KiB
Go

package aibridged
import (
"net/url"
"strings"
)
// ProviderStatus is the lifecycle state of a configured AI provider.
type ProviderStatus string
const (
// ProviderStatusEnabled indicates the provider is configured and
// valid, and is included in the active pool snapshot.
ProviderStatusEnabled ProviderStatus = "enabled"
// ProviderStatusDisabled indicates the provider is configured but
// intentionally turned off by an operator.
ProviderStatusDisabled ProviderStatus = "disabled"
// ProviderStatusError indicates the provider is configured but
// cannot be constructed (missing keys, unsupported type, malformed
// settings).
ProviderStatusError ProviderStatus = "error"
// ProviderStatusProxyExcluded means another enabled provider
// already claimed its hostname. The provider is still routable via
// the direct path (/api/v2/ai-gateway/{name}/...).
ProviderStatusProxyExcluded ProviderStatus = "proxy_excluded"
)
// ProviderOutcome classifies one ai_providers row, including
// disabled rows (503 sentinel), errored rows (excluded from pool),
// and proxy-excluded rows (excluded from proxy routing).
// Err is set when Status is Error or ProxyExcluded; the build error
// is already logged at the call site.
type ProviderOutcome struct {
Name string
Type string
Status ProviderStatus
Err error
}
// BaseURLHostname returns the normalized hostname from a provider
// base URL. It is the canonical normalization used by the proxy
// classifier and the API status check. Scheme-less inputs (from
// env-config seeding) get https:// prepended.
func BaseURLHostname(baseURL string) string {
baseURL = strings.TrimSpace(baseURL)
if baseURL == "" {
return ""
}
parsed, err := url.Parse(baseURL)
if err == nil && parsed.Hostname() == "" && !strings.Contains(baseURL, "://") {
parsed, err = url.Parse("https://" + baseURL)
}
if err != nil {
return ""
}
return strings.ToLower(parsed.Hostname())
}