mirror of
https://github.com/coder/coder.git
synced 2026-10-31 11:02:50 +08:00
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
58 lines
1.9 KiB
Go
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())
|
|
}
|