From 2c6a177c766be78154cd2bd2d445dd6d25881edb Mon Sep 17 00:00:00 2001 From: Susana Cardoso Ferreira Date: Tue, 25 Aug 2026 12:15:37 +0000 Subject: [PATCH 1/7] test(coderd): strengthen provider warning coverage --- coderd/ai_providers_test.go | 59 +++++++++++++++++++-------- docs/ai-coder/ai-gateway/providers.md | 4 +- 2 files changed, 45 insertions(+), 18 deletions(-) diff --git a/coderd/ai_providers_test.go b/coderd/ai_providers_test.go index ed490494f7706..29be6c871666b 100644 --- a/coderd/ai_providers_test.go +++ b/coderd/ai_providers_test.go @@ -1898,6 +1898,8 @@ func TestAIProviderHostnameCollisionWarnings(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client) ctx := testutil.Context(t, testutil.WaitLong) + wantWarnings := []string{`hostname "api.openai.com" is claimed by provider "first"; not reachable via the AI Gateway Proxy, use direct routing (/api/v2/ai-gateway/second/...) instead`} + //nolint:gocritic // Owner role is the audience for this endpoint. first, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{ Type: codersdk.AIProviderTypeOpenAI, @@ -1917,16 +1919,13 @@ func TestAIProviderHostnameCollisionWarnings(t *testing.T) { }) require.NoError(t, err) require.NotNil(t, second.Status) - require.Len(t, second.Status.Warnings, 1) - require.Contains(t, second.Status.Warnings[0], `"first"`) - require.Contains(t, second.Status.Warnings[0], "AI Gateway Proxy") - require.Contains(t, second.Status.Warnings[0], "/api/v2/ai-gateway/second/...") + require.Equal(t, wantWarnings, second.Status.Warnings) //nolint:gocritic // Owner role is the audience for this endpoint. got, err := client.AIProvider(ctx, second.ID.String()) require.NoError(t, err) require.NotNil(t, got.Status) - require.Len(t, got.Status.Warnings, 1) + require.Equal(t, wantWarnings, got.Status.Warnings) //nolint:gocritic // Owner role is the audience for this endpoint. winner, err := client.AIProvider(ctx, first.ID.String()) @@ -1948,44 +1947,72 @@ func TestAIProviderHostnameCollisionWarnings(t *testing.T) { } require.Nil(t, firstListed.Status, "first provider in database order should not get a warning in list") require.NotNil(t, secondListed.Status) - require.Len(t, secondListed.Status.Warnings, 1) + require.Equal(t, wantWarnings, secondListed.Status.Warnings) }) - t.Run("UpdateReturnsWarningOnEnable", func(t *testing.T) { + t.Run("UpdateBaseURLReturnsWarning", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) _ = coderdtest.CreateFirstUser(t, client) ctx := testutil.Context(t, testutil.WaitLong) + wantWarnings := []string{`hostname "api.openai.com" is claimed by provider "first"; not reachable via the AI Gateway Proxy, use direct routing (/api/v2/ai-gateway/second/...) instead`} + //nolint:gocritic // Owner role is the audience for this endpoint. - _, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{ + first, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{ Type: codersdk.AIProviderTypeOpenAI, Name: "first", Enabled: true, BaseURL: "https://api.openai.com/v1", }) require.NoError(t, err) + require.Nil(t, first.Status) //nolint:gocritic // Owner role is the audience for this endpoint. second, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{ Type: codersdk.AIProviderTypeOpenAI, Name: "second", - Enabled: false, - BaseURL: "https://api.openai.com/v2", + Enabled: true, + BaseURL: "https://api.openai-compat.com/v1", }) require.NoError(t, err) - require.Nil(t, second.Status) + require.Nil(t, second.Status, "distinct hostnames should not collide") //nolint:gocritic // Owner role is the audience for this endpoint. updated, err := client.UpdateAIProvider(ctx, second.ID.String(), codersdk.UpdateAIProviderRequest{ - Enabled: ptr.Ref(true), + BaseURL: ptr.Ref("https://api.openai.com/v2"), }) require.NoError(t, err) require.NotNil(t, updated.Status) - require.Len(t, updated.Status.Warnings, 1) - require.Contains(t, updated.Status.Warnings[0], `"first"`) - require.Contains(t, updated.Status.Warnings[0], "AI Gateway Proxy") - require.Contains(t, updated.Status.Warnings[0], "/api/v2/ai-gateway/second/...") + require.Equal(t, wantWarnings, updated.Status.Warnings) + + //nolint:gocritic // Owner role is the audience for this endpoint. + got, err := client.AIProvider(ctx, second.ID.String()) + require.NoError(t, err) + require.NotNil(t, got.Status) + require.Equal(t, wantWarnings, got.Status.Warnings) + + //nolint:gocritic // Owner role is the audience for this endpoint. + winner, err := client.AIProvider(ctx, first.ID.String()) + require.NoError(t, err) + require.Nil(t, winner.Status, "first provider in database order should not get a warning on get") + + //nolint:gocritic // Owner role is the audience for this endpoint. + providers, err := client.AIProviders(ctx) + require.NoError(t, err) + require.Len(t, providers, 2) + var firstListed, secondListed codersdk.AIProvider + for _, p := range providers { + switch p.Name { + case "first": + firstListed = p + case "second": + secondListed = p + } + } + require.Nil(t, firstListed.Status, "first provider in database order should not get a warning in list") + require.NotNil(t, secondListed.Status) + require.Equal(t, wantWarnings, secondListed.Status.Warnings) }) t.Run("NoWarningWhenNoCollision", func(t *testing.T) { diff --git a/docs/ai-coder/ai-gateway/providers.md b/docs/ai-coder/ai-gateway/providers.md index f912a66610e0d..38523f9a0a727 100644 --- a/docs/ai-coder/ai-gateway/providers.md +++ b/docs/ai-coder/ai-gateway/providers.md @@ -279,8 +279,8 @@ an API key. ## Provider lifecycle -Every provider carries an explicit status, surfaced through the -[`provider_info`](./monitoring.md#prometheus-metrics) metric and the API: +Every provider carries an explicit status, reported by the [`provider_info`](./monitoring.md#prometheus-metrics) metrics. +`coder_ai_gateway_provider_info` reports `enabled`, `disabled`, and `error`, and `coder_ai_gateway_proxy_provider_info` also reports `proxy_excluded`: | Status | Meaning | Effect on requests | |------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------| From d26cc70a8f7b4cf329a4bbbf7a5a0761424f9fd7 Mon Sep 17 00:00:00 2001 From: Susana Cardoso Ferreira Date: Tue, 25 Aug 2026 16:16:48 +0000 Subject: [PATCH 2/7] test(coderd): preserve provider enable collision coverage --- coderd/ai_providers_test.go | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/coderd/ai_providers_test.go b/coderd/ai_providers_test.go index 29be6c871666b..05d9b2aac3463 100644 --- a/coderd/ai_providers_test.go +++ b/coderd/ai_providers_test.go @@ -1950,6 +1950,42 @@ func TestAIProviderHostnameCollisionWarnings(t *testing.T) { require.Equal(t, wantWarnings, secondListed.Status.Warnings) }) + t.Run("UpdateReturnsWarningOnEnable", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + _ = coderdtest.CreateFirstUser(t, client) + ctx := testutil.Context(t, testutil.WaitLong) + + wantWarnings := []string{`hostname "api.openai.com" is claimed by provider "first"; not reachable via the AI Gateway Proxy, use direct routing (/api/v2/ai-gateway/second/...) instead`} + + //nolint:gocritic // Owner role is the audience for this endpoint. + _, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{ + Type: codersdk.AIProviderTypeOpenAI, + Name: "first", + Enabled: true, + BaseURL: "https://api.openai.com/v1", + }) + require.NoError(t, err) + + //nolint:gocritic // Owner role is the audience for this endpoint. + second, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{ + Type: codersdk.AIProviderTypeOpenAI, + Name: "second", + Enabled: false, + BaseURL: "https://api.openai.com/v2", + }) + require.NoError(t, err) + require.Nil(t, second.Status) + + //nolint:gocritic // Owner role is the audience for this endpoint. + updated, err := client.UpdateAIProvider(ctx, second.ID.String(), codersdk.UpdateAIProviderRequest{ + Enabled: ptr.Ref(true), + }) + require.NoError(t, err) + require.NotNil(t, updated.Status) + require.Equal(t, wantWarnings, updated.Status.Warnings) + }) + t.Run("UpdateBaseURLReturnsWarning", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) From 9be80a0972edf69e295432a8fe0eae09df935bef Mon Sep 17 00:00:00 2001 From: Susana Cardoso Ferreira Date: Tue, 25 Aug 2026 16:35:56 +0000 Subject: [PATCH 3/7] test(coderd): avoid duplicate collision assertions --- coderd/ai_providers_test.go | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/coderd/ai_providers_test.go b/coderd/ai_providers_test.go index 05d9b2aac3463..5f6be30b4de89 100644 --- a/coderd/ai_providers_test.go +++ b/coderd/ai_providers_test.go @@ -2021,34 +2021,6 @@ func TestAIProviderHostnameCollisionWarnings(t *testing.T) { require.NoError(t, err) require.NotNil(t, updated.Status) require.Equal(t, wantWarnings, updated.Status.Warnings) - - //nolint:gocritic // Owner role is the audience for this endpoint. - got, err := client.AIProvider(ctx, second.ID.String()) - require.NoError(t, err) - require.NotNil(t, got.Status) - require.Equal(t, wantWarnings, got.Status.Warnings) - - //nolint:gocritic // Owner role is the audience for this endpoint. - winner, err := client.AIProvider(ctx, first.ID.String()) - require.NoError(t, err) - require.Nil(t, winner.Status, "first provider in database order should not get a warning on get") - - //nolint:gocritic // Owner role is the audience for this endpoint. - providers, err := client.AIProviders(ctx) - require.NoError(t, err) - require.Len(t, providers, 2) - var firstListed, secondListed codersdk.AIProvider - for _, p := range providers { - switch p.Name { - case "first": - firstListed = p - case "second": - secondListed = p - } - } - require.Nil(t, firstListed.Status, "first provider in database order should not get a warning in list") - require.NotNil(t, secondListed.Status) - require.Equal(t, wantWarnings, secondListed.Status.Warnings) }) t.Run("NoWarningWhenNoCollision", func(t *testing.T) { From 39c7e228549da29904c87481ecd2373b6a5b96a0 Mon Sep 17 00:00:00 2001 From: Susana Cardoso Ferreira Date: Tue, 25 Aug 2026 17:03:12 +0000 Subject: [PATCH 4/7] fix(coderd): clarify conditional proxy warning --- coderd/ai_providers.go | 2 +- coderd/ai_providers_test.go | 6 +++--- .../ProvidersPage/components/ProviderRow.stories.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/coderd/ai_providers.go b/coderd/ai_providers.go index dee812ede7dd2..eb7dccb56b9d7 100644 --- a/coderd/ai_providers.go +++ b/coderd/ai_providers.go @@ -604,7 +604,7 @@ func aiProviderHostnameWarningFromMap(provider database.AIProvider, namesByHost return nil } return &codersdk.AIProviderStatus{Warnings: []string{ - fmt.Sprintf("hostname %q is claimed by provider %q; not reachable via the AI Gateway Proxy, use direct routing (/api/v2/ai-gateway/%s/...) instead", host, names[0], provider.Name), + fmt.Sprintf("hostname %q is claimed by provider %q. If AI Gateway Proxy is enabled, this provider is excluded from proxy routing. The hostname collision does not affect direct routing (/api/v2/ai-gateway/%s/...).", host, names[0], provider.Name), }} } diff --git a/coderd/ai_providers_test.go b/coderd/ai_providers_test.go index 5f6be30b4de89..9bb5e554e8f3e 100644 --- a/coderd/ai_providers_test.go +++ b/coderd/ai_providers_test.go @@ -1898,7 +1898,7 @@ func TestAIProviderHostnameCollisionWarnings(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client) ctx := testutil.Context(t, testutil.WaitLong) - wantWarnings := []string{`hostname "api.openai.com" is claimed by provider "first"; not reachable via the AI Gateway Proxy, use direct routing (/api/v2/ai-gateway/second/...) instead`} + wantWarnings := []string{`hostname "api.openai.com" is claimed by provider "first". If AI Gateway Proxy is enabled, this provider is excluded from proxy routing. The hostname collision does not affect direct routing (/api/v2/ai-gateway/second/...).`} //nolint:gocritic // Owner role is the audience for this endpoint. first, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{ @@ -1956,7 +1956,7 @@ func TestAIProviderHostnameCollisionWarnings(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client) ctx := testutil.Context(t, testutil.WaitLong) - wantWarnings := []string{`hostname "api.openai.com" is claimed by provider "first"; not reachable via the AI Gateway Proxy, use direct routing (/api/v2/ai-gateway/second/...) instead`} + wantWarnings := []string{`hostname "api.openai.com" is claimed by provider "first". If AI Gateway Proxy is enabled, this provider is excluded from proxy routing. The hostname collision does not affect direct routing (/api/v2/ai-gateway/second/...).`} //nolint:gocritic // Owner role is the audience for this endpoint. _, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{ @@ -1992,7 +1992,7 @@ func TestAIProviderHostnameCollisionWarnings(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client) ctx := testutil.Context(t, testutil.WaitLong) - wantWarnings := []string{`hostname "api.openai.com" is claimed by provider "first"; not reachable via the AI Gateway Proxy, use direct routing (/api/v2/ai-gateway/second/...) instead`} + wantWarnings := []string{`hostname "api.openai.com" is claimed by provider "first". If AI Gateway Proxy is enabled, this provider is excluded from proxy routing. The hostname collision does not affect direct routing (/api/v2/ai-gateway/second/...).`} //nolint:gocritic // Owner role is the audience for this endpoint. first, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{ diff --git a/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderRow.stories.tsx b/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderRow.stories.tsx index 314fb2216edc7..d3bb63f1e2379 100644 --- a/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderRow.stories.tsx +++ b/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderRow.stories.tsx @@ -110,7 +110,7 @@ export const WithHostnameCollisionWarning: Story = { enabled: true, status: { warnings: [ - 'hostname "api.openai.com" is claimed by provider "first"; not reachable via the AI Gateway Proxy, use direct routing (/api/v2/ai-gateway/openai/...) instead', + 'hostname "api.openai.com" is claimed by provider "first". If AI Gateway Proxy is enabled, this provider is excluded from proxy routing. The hostname collision does not affect direct routing (/api/v2/ai-gateway/openai/...).', ], }, }, From f4d6968c32406d0ddab1460595ee771ff185b019 Mon Sep 17 00:00:00 2001 From: Susana Cardoso Ferreira Date: Wed, 26 Aug 2026 08:11:51 +0000 Subject: [PATCH 5/7] fix(coderd): scope collision warnings to enabled proxy --- coderd/ai_providers.go | 70 +++++++++---------- coderd/ai_providers_test.go | 59 +++++++++++++--- .../components/ProviderRow.stories.tsx | 2 +- 3 files changed, 87 insertions(+), 44 deletions(-) diff --git a/coderd/ai_providers.go b/coderd/ai_providers.go index eb7dccb56b9d7..4310b4a77cc45 100644 --- a/coderd/ai_providers.go +++ b/coderd/ai_providers.go @@ -80,7 +80,7 @@ func (api *API) aiProvidersList(rw http.ResponseWriter, r *http.Request) { return } - namesByHost := buildHostnameCollisionMap(rows) + namesByHost := api.proxyNamesByHost(rows) out := make([]codersdk.AIProvider, 0, len(rows)) for _, row := range rows { sdk, err := db2sdk.AIProvider(row, keysByProvider[row.ID]) @@ -92,7 +92,7 @@ func (api *API) aiProvidersList(rw http.ResponseWriter, r *http.Request) { }) return } - sdk.Status = aiProviderHostnameWarningFromMap(row, namesByHost) + sdk.Status = aiProviderStatus(row, namesByHost) out = append(out, sdk) } httpapi.Write(ctx, rw, http.StatusOK, out) @@ -134,7 +134,7 @@ func (api *API) aiProvidersGet(rw http.ResponseWriter, r *http.Request) { }) return } - sdk.Status = aiProviderHostnameWarningFromDB(ctx, api.Logger, api.Database, row) + sdk.Status = api.aiProviderStatusFromDB(ctx, row) httpapi.Write(ctx, rw, http.StatusOK, sdk) } @@ -256,7 +256,7 @@ func (api *API) aiProvidersCreate(rw http.ResponseWriter, r *http.Request) { }) return } - sdk.Status = aiProviderHostnameWarningFromDB(ctx, api.Logger, api.Database, row) + sdk.Status = api.aiProviderStatusFromDB(ctx, row) httpapi.Write(ctx, rw, http.StatusCreated, sdk) } @@ -450,7 +450,7 @@ func (api *API) aiProvidersUpdate(rw http.ResponseWriter, r *http.Request) { }) return } - sdk.Status = aiProviderHostnameWarningFromDB(ctx, api.Logger, api.Database, updated) + sdk.Status = api.aiProviderStatusFromDB(ctx, updated) httpapi.Write(ctx, rw, http.StatusOK, sdk) } @@ -582,51 +582,51 @@ func buildHostnameCollisionMap(rows []database.AIProvider) map[string][]string { return namesByHost } -// aiProviderHostnameWarningFromMap is the pure helper for the list -// handler. namesByHost is the pre-built collision map from the outer -// rows, in database order (ORDER BY name ASC). Only providers whose -// name appears after another enabled provider on the same hostname -// get a warning; the first provider in database order does not. -func aiProviderHostnameWarningFromMap(provider database.AIProvider, namesByHost map[string][]string) *codersdk.AIProviderStatus { - if !provider.Enabled || provider.Deleted { - return nil - } - host := aibridged.BaseURLHostname(provider.BaseUrl) - if host == "" { +// proxyNamesByHost returns the proxy hostname collision map when AI Gateway Proxy is enabled. +func (api *API) proxyNamesByHost(rows []database.AIProvider) map[string][]string { + if !api.DeploymentValues.AI.BridgeProxyConfig.Enabled.Value() { return nil } - names := namesByHost[host] - if len(names) < 2 { - return nil + return buildHostnameCollisionMap(rows) +} + +// aiProviderStatus collects warnings for an AI provider. +func aiProviderStatus(provider database.AIProvider, namesByHost map[string][]string) *codersdk.AIProviderStatus { + var warnings []string + if warning := proxyCollisionWarning(provider, namesByHost); warning != "" { + warnings = append(warnings, warning) } - // The first name in database order is the proxy winner. - if provider.Name == names[0] { + if len(warnings) == 0 { return nil } - return &codersdk.AIProviderStatus{Warnings: []string{ - fmt.Sprintf("hostname %q is claimed by provider %q. If AI Gateway Proxy is enabled, this provider is excluded from proxy routing. The hostname collision does not affect direct routing (/api/v2/ai-gateway/%s/...).", host, names[0], provider.Name), - }} + return &codersdk.AIProviderStatus{Warnings: warnings} } -// aiProviderHostnameWarningFromDB fetches enabled, non-deleted -// providers to determine whether the given provider is excluded from -// proxy routing by hostname collision. Used by the single-row handlers -// (Get, Create, Update) where one extra query is not N+1. -func aiProviderHostnameWarningFromDB(ctx context.Context, logger slog.Logger, store database.Store, provider database.AIProvider) *codersdk.AIProviderStatus { +// proxyCollisionWarning reports when another provider claims the proxy +// hostname first in database order. +func proxyCollisionWarning(provider database.AIProvider, namesByHost map[string][]string) string { if !provider.Enabled || provider.Deleted { - return nil + return "" } host := aibridged.BaseURLHostname(provider.BaseUrl) if host == "" { - return nil + return "" } - enabledProviders, err := store.GetAIProviders(ctx, database.GetAIProvidersParams{}) + names := namesByHost[host] + if len(names) < 2 || provider.Name == names[0] { + return "" + } + return fmt.Sprintf("hostname %q is claimed by provider %q. AI Gateway Proxy excludes this provider from proxy routing. The hostname collision does not affect direct routing (/api/v2/ai-gateway/%s/... endpoint).", host, names[0], provider.Name) +} + +// aiProviderStatusFromDB loads status data for a single provider response. +func (api *API) aiProviderStatusFromDB(ctx context.Context, provider database.AIProvider) *codersdk.AIProviderStatus { + rows, err := api.Database.GetAIProviders(ctx, database.GetAIProvidersParams{}) if err != nil { - logger.Error(ctx, "load AI providers for hostname warnings", slog.Error(err)) + api.Logger.Error(ctx, "load AI providers for status", slog.Error(err)) return nil } - namesByHost := buildHostnameCollisionMap(enabledProviders) - return aiProviderHostnameWarningFromMap(provider, namesByHost) + return aiProviderStatus(provider, api.proxyNamesByHost(rows)) } // writeAIProviderError translates an error from the AI provider diff --git a/coderd/ai_providers_test.go b/coderd/ai_providers_test.go index 9bb5e554e8f3e..e3f5403357fc2 100644 --- a/coderd/ai_providers_test.go +++ b/coderd/ai_providers_test.go @@ -18,6 +18,7 @@ import ( "github.com/coder/coder/v2/coderd/util/ptr" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/testutil" + "github.com/coder/serpent" ) // keyIDs extracts the IDs from a slice of AIProviderKey responses, in @@ -1892,13 +1893,22 @@ func TestAIProvidersBedrockExternalID(t *testing.T) { func TestAIProviderHostnameCollisionWarnings(t *testing.T) { t.Parallel() + newClient := func(t *testing.T, proxyEnabled bool) *codersdk.Client { + t.Helper() + return coderdtest.New(t, &coderdtest.Options{ + DeploymentValues: coderdtest.DeploymentValues(t, func(values *codersdk.DeploymentValues) { + values.AI.BridgeProxyConfig.Enabled = serpent.Bool(proxyEnabled) + }), + }) + } + t.Run("CreateGetListReturnsWarning", func(t *testing.T) { t.Parallel() - client := coderdtest.New(t, nil) + client := newClient(t, true) _ = coderdtest.CreateFirstUser(t, client) ctx := testutil.Context(t, testutil.WaitLong) - wantWarnings := []string{`hostname "api.openai.com" is claimed by provider "first". If AI Gateway Proxy is enabled, this provider is excluded from proxy routing. The hostname collision does not affect direct routing (/api/v2/ai-gateway/second/...).`} + wantWarnings := []string{`hostname "api.openai.com" is claimed by provider "first". AI Gateway Proxy excludes this provider from proxy routing. The hostname collision does not affect direct routing (/api/v2/ai-gateway/second/... endpoint).`} //nolint:gocritic // Owner role is the audience for this endpoint. first, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{ @@ -1952,11 +1962,11 @@ func TestAIProviderHostnameCollisionWarnings(t *testing.T) { t.Run("UpdateReturnsWarningOnEnable", func(t *testing.T) { t.Parallel() - client := coderdtest.New(t, nil) + client := newClient(t, true) _ = coderdtest.CreateFirstUser(t, client) ctx := testutil.Context(t, testutil.WaitLong) - wantWarnings := []string{`hostname "api.openai.com" is claimed by provider "first". If AI Gateway Proxy is enabled, this provider is excluded from proxy routing. The hostname collision does not affect direct routing (/api/v2/ai-gateway/second/...).`} + wantWarnings := []string{`hostname "api.openai.com" is claimed by provider "first". AI Gateway Proxy excludes this provider from proxy routing. The hostname collision does not affect direct routing (/api/v2/ai-gateway/second/... endpoint).`} //nolint:gocritic // Owner role is the audience for this endpoint. _, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{ @@ -1988,11 +1998,11 @@ func TestAIProviderHostnameCollisionWarnings(t *testing.T) { t.Run("UpdateBaseURLReturnsWarning", func(t *testing.T) { t.Parallel() - client := coderdtest.New(t, nil) + client := newClient(t, true) _ = coderdtest.CreateFirstUser(t, client) ctx := testutil.Context(t, testutil.WaitLong) - wantWarnings := []string{`hostname "api.openai.com" is claimed by provider "first". If AI Gateway Proxy is enabled, this provider is excluded from proxy routing. The hostname collision does not affect direct routing (/api/v2/ai-gateway/second/...).`} + wantWarnings := []string{`hostname "api.openai.com" is claimed by provider "first". AI Gateway Proxy excludes this provider from proxy routing. The hostname collision does not affect direct routing (/api/v2/ai-gateway/second/... endpoint).`} //nolint:gocritic // Owner role is the audience for this endpoint. first, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{ @@ -2023,9 +2033,42 @@ func TestAIProviderHostnameCollisionWarnings(t *testing.T) { require.Equal(t, wantWarnings, updated.Status.Warnings) }) + t.Run("ProxyDisabledReturnsNoWarning", func(t *testing.T) { + t.Parallel() + client := newClient(t, false) + _ = coderdtest.CreateFirstUser(t, client) + ctx := testutil.Context(t, testutil.WaitLong) + + //nolint:gocritic // Owner role is the audience for this endpoint. + _, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{ + Type: codersdk.AIProviderTypeOpenAI, + Name: "first", + Enabled: true, + BaseURL: "https://api.openai.com/v1", + }) + require.NoError(t, err) + + //nolint:gocritic // Owner role is the audience for this endpoint. + second, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{ + Type: codersdk.AIProviderTypeOpenAI, + Name: "second", + Enabled: true, + BaseURL: "https://api.openai.com/v2", + }) + require.NoError(t, err) + require.Nil(t, second.Status) + + //nolint:gocritic // Owner role is the audience for this endpoint. + providers, err := client.AIProviders(ctx) + require.NoError(t, err) + for _, provider := range providers { + require.Nil(t, provider.Status) + } + }) + t.Run("NoWarningWhenNoCollision", func(t *testing.T) { t.Parallel() - client := coderdtest.New(t, nil) + client := newClient(t, true) _ = coderdtest.CreateFirstUser(t, client) ctx := testutil.Context(t, testutil.WaitLong) @@ -2052,7 +2095,7 @@ func TestAIProviderHostnameCollisionWarnings(t *testing.T) { t.Run("UpdateSelfNoWarning", func(t *testing.T) { t.Parallel() - client := coderdtest.New(t, nil) + client := newClient(t, true) _ = coderdtest.CreateFirstUser(t, client) ctx := testutil.Context(t, testutil.WaitLong) diff --git a/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderRow.stories.tsx b/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderRow.stories.tsx index d3bb63f1e2379..030caea5592c9 100644 --- a/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderRow.stories.tsx +++ b/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderRow.stories.tsx @@ -110,7 +110,7 @@ export const WithHostnameCollisionWarning: Story = { enabled: true, status: { warnings: [ - 'hostname "api.openai.com" is claimed by provider "first". If AI Gateway Proxy is enabled, this provider is excluded from proxy routing. The hostname collision does not affect direct routing (/api/v2/ai-gateway/openai/...).', + 'hostname "api.openai.com" is claimed by provider "first". AI Gateway Proxy excludes this provider from proxy routing. The hostname collision does not affect direct routing (/api/v2/ai-gateway/openai/... endpoint).', ], }, }, From 946252399bdc09f0f51d47e3f4a0f5a6d987b4c0 Mon Sep 17 00:00:00 2001 From: Susana Cardoso Ferreira Date: Wed, 26 Aug 2026 09:17:03 +0000 Subject: [PATCH 6/7] fix(coderd): capitalize provider warning message --- coderd/ai_providers.go | 2 +- coderd/ai_providers_test.go | 6 +++--- .../ProvidersPage/components/ProviderRow.stories.tsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/coderd/ai_providers.go b/coderd/ai_providers.go index 4310b4a77cc45..7fe992a4c611e 100644 --- a/coderd/ai_providers.go +++ b/coderd/ai_providers.go @@ -616,7 +616,7 @@ func proxyCollisionWarning(provider database.AIProvider, namesByHost map[string] if len(names) < 2 || provider.Name == names[0] { return "" } - return fmt.Sprintf("hostname %q is claimed by provider %q. AI Gateway Proxy excludes this provider from proxy routing. The hostname collision does not affect direct routing (/api/v2/ai-gateway/%s/... endpoint).", host, names[0], provider.Name) + return fmt.Sprintf("Hostname %q is claimed by provider %q. AI Gateway Proxy excludes this provider from proxy routing. The hostname collision does not affect direct routing (/api/v2/ai-gateway/%s/... endpoint).", host, names[0], provider.Name) } // aiProviderStatusFromDB loads status data for a single provider response. diff --git a/coderd/ai_providers_test.go b/coderd/ai_providers_test.go index e3f5403357fc2..e97c74696eeb0 100644 --- a/coderd/ai_providers_test.go +++ b/coderd/ai_providers_test.go @@ -1908,7 +1908,7 @@ func TestAIProviderHostnameCollisionWarnings(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client) ctx := testutil.Context(t, testutil.WaitLong) - wantWarnings := []string{`hostname "api.openai.com" is claimed by provider "first". AI Gateway Proxy excludes this provider from proxy routing. The hostname collision does not affect direct routing (/api/v2/ai-gateway/second/... endpoint).`} + wantWarnings := []string{`Hostname "api.openai.com" is claimed by provider "first". AI Gateway Proxy excludes this provider from proxy routing. The hostname collision does not affect direct routing (/api/v2/ai-gateway/second/... endpoint).`} //nolint:gocritic // Owner role is the audience for this endpoint. first, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{ @@ -1966,7 +1966,7 @@ func TestAIProviderHostnameCollisionWarnings(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client) ctx := testutil.Context(t, testutil.WaitLong) - wantWarnings := []string{`hostname "api.openai.com" is claimed by provider "first". AI Gateway Proxy excludes this provider from proxy routing. The hostname collision does not affect direct routing (/api/v2/ai-gateway/second/... endpoint).`} + wantWarnings := []string{`Hostname "api.openai.com" is claimed by provider "first". AI Gateway Proxy excludes this provider from proxy routing. The hostname collision does not affect direct routing (/api/v2/ai-gateway/second/... endpoint).`} //nolint:gocritic // Owner role is the audience for this endpoint. _, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{ @@ -2002,7 +2002,7 @@ func TestAIProviderHostnameCollisionWarnings(t *testing.T) { _ = coderdtest.CreateFirstUser(t, client) ctx := testutil.Context(t, testutil.WaitLong) - wantWarnings := []string{`hostname "api.openai.com" is claimed by provider "first". AI Gateway Proxy excludes this provider from proxy routing. The hostname collision does not affect direct routing (/api/v2/ai-gateway/second/... endpoint).`} + wantWarnings := []string{`Hostname "api.openai.com" is claimed by provider "first". AI Gateway Proxy excludes this provider from proxy routing. The hostname collision does not affect direct routing (/api/v2/ai-gateway/second/... endpoint).`} //nolint:gocritic // Owner role is the audience for this endpoint. first, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{ diff --git a/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderRow.stories.tsx b/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderRow.stories.tsx index 030caea5592c9..1458666c494db 100644 --- a/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderRow.stories.tsx +++ b/site/src/pages/AISettingsPage/ProvidersPage/components/ProviderRow.stories.tsx @@ -110,7 +110,7 @@ export const WithHostnameCollisionWarning: Story = { enabled: true, status: { warnings: [ - 'hostname "api.openai.com" is claimed by provider "first". AI Gateway Proxy excludes this provider from proxy routing. The hostname collision does not affect direct routing (/api/v2/ai-gateway/openai/... endpoint).', + 'Hostname "api.openai.com" is claimed by provider "first". AI Gateway Proxy excludes this provider from proxy routing. The hostname collision does not affect direct routing (/api/v2/ai-gateway/openai/... endpoint).', ], }, }, From 4bcb98064978dac5dad108bd3f18b9366359f95f Mon Sep 17 00:00:00 2001 From: Susana Cardoso Ferreira Date: Wed, 26 Aug 2026 09:58:45 +0000 Subject: [PATCH 7/7] refactor(coderd): centralize proxy warning gating --- coderd/ai_providers.go | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/coderd/ai_providers.go b/coderd/ai_providers.go index 7fe992a4c611e..71b40597751e0 100644 --- a/coderd/ai_providers.go +++ b/coderd/ai_providers.go @@ -80,7 +80,7 @@ func (api *API) aiProvidersList(rw http.ResponseWriter, r *http.Request) { return } - namesByHost := api.proxyNamesByHost(rows) + namesByHost := buildHostnameCollisionMap(rows) out := make([]codersdk.AIProvider, 0, len(rows)) for _, row := range rows { sdk, err := db2sdk.AIProvider(row, keysByProvider[row.ID]) @@ -92,7 +92,7 @@ func (api *API) aiProvidersList(rw http.ResponseWriter, r *http.Request) { }) return } - sdk.Status = aiProviderStatus(row, namesByHost) + sdk.Status = api.aiProviderStatus(row, namesByHost) out = append(out, sdk) } httpapi.Write(ctx, rw, http.StatusOK, out) @@ -582,18 +582,10 @@ func buildHostnameCollisionMap(rows []database.AIProvider) map[string][]string { return namesByHost } -// proxyNamesByHost returns the proxy hostname collision map when AI Gateway Proxy is enabled. -func (api *API) proxyNamesByHost(rows []database.AIProvider) map[string][]string { - if !api.DeploymentValues.AI.BridgeProxyConfig.Enabled.Value() { - return nil - } - return buildHostnameCollisionMap(rows) -} - // aiProviderStatus collects warnings for an AI provider. -func aiProviderStatus(provider database.AIProvider, namesByHost map[string][]string) *codersdk.AIProviderStatus { +func (api *API) aiProviderStatus(provider database.AIProvider, namesByHost map[string][]string) *codersdk.AIProviderStatus { var warnings []string - if warning := proxyCollisionWarning(provider, namesByHost); warning != "" { + if warning := api.proxyCollisionWarning(provider, namesByHost); warning != "" { warnings = append(warnings, warning) } if len(warnings) == 0 { @@ -604,8 +596,8 @@ func aiProviderStatus(provider database.AIProvider, namesByHost map[string][]str // proxyCollisionWarning reports when another provider claims the proxy // hostname first in database order. -func proxyCollisionWarning(provider database.AIProvider, namesByHost map[string][]string) string { - if !provider.Enabled || provider.Deleted { +func (api *API) proxyCollisionWarning(provider database.AIProvider, namesByHost map[string][]string) string { + if !api.DeploymentValues.AI.BridgeProxyConfig.Enabled.Value() || !provider.Enabled || provider.Deleted { return "" } host := aibridged.BaseURLHostname(provider.BaseUrl) @@ -626,7 +618,7 @@ func (api *API) aiProviderStatusFromDB(ctx context.Context, provider database.AI api.Logger.Error(ctx, "load AI providers for status", slog.Error(err)) return nil } - return aiProviderStatus(provider, api.proxyNamesByHost(rows)) + return api.aiProviderStatus(provider, buildHostnameCollisionMap(rows)) } // writeAIProviderError translates an error from the AI provider