From 42ef52528a36d2bc2c8c822b3557c3b3a5af15a9 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Thu, 13 Aug 2026 17:14:28 +0000 Subject: [PATCH 1/4] fix(coderd): send assigned chat model IDs verbatim --- coderd/exp_chats.go | 2 +- coderd/x/chatd/chatprovider/chatprovider.go | 30 ++------------- .../x/chatd/chatprovider/chatprovider_test.go | 38 ++++++++++++++++--- coderd/x/chatd/model_routing_aibridge.go | 5 +-- 4 files changed, 40 insertions(+), 35 deletions(-) diff --git a/coderd/exp_chats.go b/coderd/exp_chats.go index 72196d418b7c7..eed66f53e3c25 100644 --- a/coderd/exp_chats.go +++ b/coderd/exp_chats.go @@ -6441,7 +6441,7 @@ func validateChatModelConfigProviderModel(aiProvider database.AIProvider, model return &chatModelConfigProviderModelError{ Response: codersdk.Response{ Message: "OpenRouter-like provider configured as type openai does not support slash-namespaced models.", - Detail: "Change the AI provider type to openrouter or openai-compat. The openai type strips the vendor prefix from slash-namespaced model IDs, routing to the wrong upstream provider.", + Detail: "Change the AI provider type to openrouter or openai-compat. Slash-namespaced model IDs on OpenRouter-like gateways require one of those provider types.", }, } } diff --git a/coderd/x/chatd/chatprovider/chatprovider.go b/coderd/x/chatd/chatprovider/chatprovider.go index e0db1601112f3..b96180eb54a6a 100644 --- a/coderd/x/chatd/chatprovider/chatprovider.go +++ b/coderd/x/chatd/chatprovider/chatprovider.go @@ -683,21 +683,6 @@ func orderProviders(providerSet map[string]struct{}) []string { return ordered } -// isGatewayProvider reports whether the provider routes requests to -// multiple upstream model providers using a "/" model -// identifier, where the slash is part of the upstream model ID rather -// than a hint. -func isGatewayProvider(provider string) bool { - switch provider { - case fantasyvercel.Name, - fantasyopenrouter.Name, - fantasyopenaicompat.Name: - return true - default: - return false - } -} - // NormalizeProvider canonicalizes a provider name. func NormalizeProvider(provider string) string { switch strings.ToLower(strings.TrimSpace(provider)) { @@ -728,23 +713,16 @@ func ResolveModelWithProviderHint(modelName, providerHint string) (provider stri return "", "", xerrors.New("model is required") } - // Gateway providers (vercel, openrouter, openai-compat) treat the - // "/" slash as part of the upstream model ID, so - // parseCanonicalModelRef would incorrectly strip the prefix and - // route to the embedded provider name instead. Honor an explicit - // gateway hint before attempting canonical-ref parsing. - if normalized := NormalizeProvider(providerHint); normalized != "" && isGatewayProvider(normalized) { - return normalized, modelName, nil + // A valid provider hint is authoritative, so preserve the model ID + // instead of interpreting its namespace as a different provider. + if provider := NormalizeProvider(providerHint); provider != "" { + return provider, modelName, nil } if provider, modelID, ok := parseCanonicalModelRef(modelName); ok { return provider, modelID, nil } - if provider := NormalizeProvider(providerHint); provider != "" { - return provider, modelName, nil - } - normalized := strings.ToLower(modelName) switch normalized { case "claude-opus-4-6": diff --git a/coderd/x/chatd/chatprovider/chatprovider_test.go b/coderd/x/chatd/chatprovider/chatprovider_test.go index f598a01f310f0..eccaf06d1cb88 100644 --- a/coderd/x/chatd/chatprovider/chatprovider_test.go +++ b/coderd/x/chatd/chatprovider/chatprovider_test.go @@ -1729,11 +1729,11 @@ func TestResolveModelWithProviderHint(t *testing.T) { wantModel: "anthropic/claude-opus-4.6", }, { - name: "OpenAIHintStripsCanonicalPrefix", + name: "OpenAIHintPreservesCanonicalPrefix", modelName: "anthropic/claude-opus-4.6", providerHint: fantasyopenai.Name, - wantProvider: fantasyanthropic.Name, - wantModel: "claude-opus-4.6", + wantProvider: fantasyopenai.Name, + wantModel: "anthropic/claude-opus-4.6", }, { name: "OpenAIHintPreservesUnknownSlashNamespace", @@ -1743,11 +1743,39 @@ func TestResolveModelWithProviderHint(t *testing.T) { wantModel: "meta-llama/llama-3-70b", }, { - name: "AnthropicHintStripsCanonicalPrefix", + name: "AnthropicHintPreservesCanonicalPrefix", modelName: "anthropic/claude-4-5-sonnet", providerHint: fantasyanthropic.Name, wantProvider: fantasyanthropic.Name, - wantModel: "claude-4-5-sonnet", + wantModel: "anthropic/claude-4-5-sonnet", + }, + { + name: "AnthropicHintPreservesBedrockNamespace", + modelName: "bedrock/claude-opus-4-8", + providerHint: fantasyanthropic.Name, + wantProvider: fantasyanthropic.Name, + wantModel: "bedrock/claude-opus-4-8", + }, + { + name: "AnthropicHintPreservesDoubleBedrockNamespace", + modelName: "bedrock/bedrock/claude-opus-4-8", + providerHint: fantasyanthropic.Name, + wantProvider: fantasyanthropic.Name, + wantModel: "bedrock/bedrock/claude-opus-4-8", + }, + { + name: "AnthropicHintPreservesColonCanonicalRef", + modelName: "openai:gpt-5.2", + providerHint: fantasyanthropic.Name, + wantProvider: fantasyanthropic.Name, + wantModel: "openai:gpt-5.2", + }, + { + name: "BedrockHintWithBareModel", + modelName: "claude-opus-4-6", + providerHint: fantasybedrock.Name, + wantProvider: fantasybedrock.Name, + wantModel: "claude-opus-4-6", }, { name: "NoHintUsesCanonicalRef", diff --git a/coderd/x/chatd/model_routing_aibridge.go b/coderd/x/chatd/model_routing_aibridge.go index 0311c597f2002..ab03eae80df44 100644 --- a/coderd/x/chatd/model_routing_aibridge.go +++ b/coderd/x/chatd/model_routing_aibridge.go @@ -84,9 +84,8 @@ func (t *aiGatewayRoundTripper) RoundTrip(req *http.Request) (*http.Response, er return t.base.RoundTrip(cloned) } -// ValidateAIGatewayProviderModel rejects slash-namespaced models on -// OpenRouter-like providers typed as openai, where the provider type -// strips the vendor prefix. +// ValidateAIGatewayProviderModel rejects slash-namespaced models when an +// OpenRouter-like gateway is configured with the OpenAI provider type. func ValidateAIGatewayProviderModel(provider database.AIProvider, model string) error { if provider.Type != database.AIProviderTypeOpenai { return nil From 5dda809da414a03a7f58eb6e5725c9b03eb1b697 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Thu, 13 Aug 2026 17:49:13 +0000 Subject: [PATCH 2/4] fix(coderd): drop obsolete slash-namespaced model rejection Provider hints are now authoritative in model resolution, so no layer strips vendor prefixes from slash-namespaced model IDs. The save-time and request-time rejection of such models on OpenRouter-like openai-type providers guarded against the old stripping behavior and would now block working configurations. --- coderd/exp_chats.go | 43 +------ coderd/exp_chats_internal_test.go | 113 ------------------ coderd/exp_chats_test.go | 34 +++--- coderd/x/chatd/chatprovider/chatprovider.go | 25 ---- .../x/chatd/chatprovider/chatprovider_test.go | 22 ---- coderd/x/chatd/model_routing_aibridge.go | 36 ------ coderd/x/chatd/model_routing_internal_test.go | 29 +---- 7 files changed, 24 insertions(+), 278 deletions(-) diff --git a/coderd/exp_chats.go b/coderd/exp_chats.go index eed66f53e3c25..627efc63437de 100644 --- a/coderd/exp_chats.go +++ b/coderd/exp_chats.go @@ -6428,26 +6428,6 @@ func (api *API) listChatModelConfigs(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusOK, resp) } -type chatModelConfigProviderModelError struct { - Response codersdk.Response -} - -func (e *chatModelConfigProviderModelError) Error() string { - return e.Response.Message -} - -func validateChatModelConfigProviderModel(aiProvider database.AIProvider, model string) *chatModelConfigProviderModelError { - if err := chatd.ValidateAIGatewayProviderModel(aiProvider, model); err != nil { - return &chatModelConfigProviderModelError{ - Response: codersdk.Response{ - Message: "OpenRouter-like provider configured as type openai does not support slash-namespaced models.", - Detail: "Change the AI provider type to openrouter or openai-compat. Slash-namespaced model IDs on OpenRouter-like gateways require one of those provider types.", - }, - } - } - return nil -} - // inChatModelConfigWriteTx runs fn in a transaction that holds the advisory // lock serializing chat model config writes. All writes to the table must go // through this helper so concurrent writers cannot act on stale reads, e.g. @@ -6506,11 +6486,6 @@ func (api *API) createChatModelConfig(rw http.ResponseWriter, r *http.Request) { return } - if validationErr := validateChatModelConfigProviderModel(aiProvider, model); validationErr != nil { - httpapi.Write(ctx, rw, http.StatusBadRequest, validationErr.Response) - return - } - enabled := true if req.Enabled != nil { enabled = *req.Enabled @@ -6576,10 +6551,6 @@ func (api *API) createChatModelConfig(rw http.ResponseWriter, r *http.Request) { if !lockedAIProvider.Enabled { return errChatProviderNotConfigured } - if err := validateChatModelConfigProviderModel(lockedAIProvider, insertParams.Model); err != nil { - return err - } - insertAsDefault := isDefault if !insertAsDefault { _, err := tx.GetDefaultChatModelConfig(ctx) @@ -6618,11 +6589,7 @@ func (api *API) createChatModelConfig(rw http.ResponseWriter, r *http.Request) { return nil }) if err != nil { - var providerModelErr *chatModelConfigProviderModelError switch { - case errors.As(err, &providerModelErr): - httpapi.Write(ctx, rw, http.StatusBadRequest, providerModelErr.Response) - return case database.IsUniqueViolation(err): httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{ Message: "Chat model config already exists.", @@ -6770,7 +6737,8 @@ func (api *API) updateChatModelConfig(rw http.ResponseWriter, r *http.Request) { ID: existing.ID, } - // Re-derive the provider type under lock when the model or provider changes. + // Re-check the provider exists and is enabled under lock when the + // model or provider changes. revalidateProviderModel := updateParams.AIProviderID.Valid && (req.AIProviderID != nil || strings.TrimSpace(req.Model) != "") var updated database.ChatModelConfig err = api.inChatModelConfigWriteTx(ctx, func(tx database.Store) error { @@ -6786,9 +6754,6 @@ func (api *API) updateChatModelConfig(rw http.ResponseWriter, r *http.Request) { if !aiProvider.Enabled { return errChatProviderNotConfigured } - if err := validateChatModelConfigProviderModel(aiProvider, updateParams.Model); err != nil { - return err - } } setAsDefault := updateParams.IsDefault && !existing.IsDefault @@ -6831,11 +6796,7 @@ func (api *API) updateChatModelConfig(rw http.ResponseWriter, r *http.Request) { return nil }) if err != nil { - var providerModelErr *chatModelConfigProviderModelError switch { - case errors.As(err, &providerModelErr): - httpapi.Write(ctx, rw, http.StatusBadRequest, providerModelErr.Response) - return case database.IsUniqueViolation(err): httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{ Message: "Chat model config already exists.", diff --git a/coderd/exp_chats_internal_test.go b/coderd/exp_chats_internal_test.go index 85ad42dbdda0b..63e9e2f18c3e5 100644 --- a/coderd/exp_chats_internal_test.go +++ b/coderd/exp_chats_internal_test.go @@ -231,119 +231,6 @@ func TestValidateChatModelProviderOptions_AnthropicThinkingDisplay(t *testing.T) } } -func TestValidateChatModelConfigProviderModel(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - model string - provider database.AIProvider - wantErr bool - wantDetail string - }{ - { - name: "OpenRouterNameWithOpenAITypeAndSlashModel", - model: "anthropic/claude-opus-4.6", - provider: database.AIProvider{ - Name: "openrouter", - Type: database.AIProviderTypeOpenai, - }, - wantErr: true, - wantDetail: "Change the AI provider type to openrouter or openai-compat.", - }, - { - name: "OpenRouterNameWithWhitespaceAndCase", - model: "anthropic/claude-opus-4.6", - provider: database.AIProvider{ - Name: " OpenRouter ", - Type: database.AIProviderTypeOpenai, - }, - wantErr: true, - wantDetail: "Change the AI provider type to openrouter or openai-compat.", - }, - { - name: "OpenRouterHostWithOpenAITypeAndSlashModel", - model: "anthropic/claude-opus-4.6", - provider: database.AIProvider{ - Name: "private-relay", - Type: database.AIProviderTypeOpenai, - BaseUrl: "https://openrouter.ai/api/v1", - }, - wantErr: true, - wantDetail: "Change the AI provider type to openrouter or openai-compat.", - }, - { - name: "OpenRouterHostWithPort", - model: "anthropic/claude-opus-4.6", - provider: database.AIProvider{ - Name: "private-relay", - Type: database.AIProviderTypeOpenai, - BaseUrl: "https://openrouter.ai:443/api/v1", - }, - wantErr: true, - wantDetail: "Change the AI provider type to openrouter or openai-compat.", - }, - { - name: "OpenRouterSubdomainWithOpenAIType", - model: "anthropic/claude-opus-4.6", - provider: database.AIProvider{ - Name: "private-relay", - Type: database.AIProviderTypeOpenai, - BaseUrl: "https://api.openrouter.ai/v1", - }, - wantErr: true, - wantDetail: "Change the AI provider type to openrouter or openai-compat.", - }, - { - name: "OpenRouterTypeAllowsSlashModel", - model: "anthropic/claude-opus-4.6", - provider: database.AIProvider{ - Name: "openrouter", - Type: database.AIProviderTypeOpenrouter, - }, - }, - { - name: "OpenAICompatTypeAllowsSlashModel", - model: "anthropic/claude-opus-4.6", - provider: database.AIProvider{ - Name: "openrouter", - Type: database.AIProviderTypeOpenaiCompat, - }, - }, - { - name: "PrivateOpenAIProxyAllowsSlashModel", - model: "anthropic/claude-opus-4.6", - provider: database.AIProvider{ - Name: "private-relay", - Type: database.AIProviderTypeOpenai, - BaseUrl: "https://llm-relay.internal/v1", - }, - }, - { - name: "OpenRouterNameWithPlainModelAllowed", - model: "gpt-4.1", - provider: database.AIProvider{ - Name: "openrouter", - Type: database.AIProviderTypeOpenai, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - - got := validateChatModelConfigProviderModel(tt.provider, tt.model) - if tt.wantErr { - require.NotNil(t, got) - require.Contains(t, got.Response.Detail, tt.wantDetail) - return - } - require.Nil(t, got) - }) - } -} - func TestWriteChatFileErrorUnavailable(t *testing.T) { t.Parallel() diff --git a/coderd/exp_chats_test.go b/coderd/exp_chats_test.go index e32fae2a70b8e..f3f1500ea7754 100644 --- a/coderd/exp_chats_test.go +++ b/coderd/exp_chats_test.go @@ -4290,7 +4290,7 @@ func TestCreateChatModelConfig(t *testing.T) { require.Equal(t, "AI provider is disabled.", sdkErr.Message) }) - t.Run("RejectsOpenRouterMisconfiguredAsOpenAI", func(t *testing.T) { + t.Run("AllowsSlashNamespacedModelOnOpenAIType", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitLong) @@ -4307,14 +4307,13 @@ func TestCreateChatModelConfig(t *testing.T) { require.NoError(t, err) contextLimit := int64(4096) - _, err = client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + modelConfig, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ AIProviderID: &aiProvider.ID, Model: "anthropic/claude-opus-4.6", ContextLimit: &contextLimit, }) - sdkErr := requireSDKError(t, err, http.StatusBadRequest) - require.Equal(t, "OpenRouter-like provider configured as type openai does not support slash-namespaced models.", sdkErr.Message) - require.Contains(t, sdkErr.Detail, "Change the AI provider type to openrouter or openai-compat.") + require.NoError(t, err) + require.Equal(t, "anthropic/claude-opus-4.6", modelConfig.Model) }) t.Run("ForbiddenForOrganizationMember", func(t *testing.T) { @@ -4382,7 +4381,7 @@ func TestUpdateChatModelConfig(t *testing.T) { require.Equal(t, "gpt-4o-mini-updated", updated.Model) }) - t.Run("RejectsOpenRouterMisconfiguredAsOpenAI", func(t *testing.T) { + t.Run("AllowsModelChangeToSlashNamespacedOnOpenAIType", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitLong) @@ -4406,15 +4405,14 @@ func TestUpdateChatModelConfig(t *testing.T) { }) require.NoError(t, err) - _, err = client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ + updated, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ Model: "anthropic/claude-opus-4.6", }) - sdkErr := requireSDKError(t, err, http.StatusBadRequest) - require.Equal(t, "OpenRouter-like provider configured as type openai does not support slash-namespaced models.", sdkErr.Message) - require.Contains(t, sdkErr.Detail, "Change the AI provider type to openrouter or openai-compat.") + require.NoError(t, err) + require.Equal(t, "anthropic/claude-opus-4.6", updated.Model) }) - t.Run("AllowsUnrelatedEditOnExistingMisconfiguredOpenAI", func(t *testing.T) { + t.Run("UnrelatedEditPreservesSlashNamespacedModel", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitLong) @@ -4443,7 +4441,7 @@ func TestUpdateChatModelConfig(t *testing.T) { require.Equal(t, modelConfig.Model, updated.Model) }) - t.Run("RejectsProviderChangeToMisconfiguredOpenAI", func(t *testing.T) { + t.Run("AllowsProviderChangeToOpenAITypeWithSlashNamespacedModel", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitLong) @@ -4458,7 +4456,7 @@ func TestUpdateChatModelConfig(t *testing.T) { APIKeys: []string{"test-api-key"}, }) require.NoError(t, err) - misconfiguredProvider, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{ + openAITypeProvider, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{ Type: codersdk.AIProviderTypeOpenAI, Name: "openrouter", Enabled: true, @@ -4475,12 +4473,12 @@ func TestUpdateChatModelConfig(t *testing.T) { }) require.NoError(t, err) - _, err = client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ - AIProviderID: &misconfiguredProvider.ID, + updated, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ + AIProviderID: &openAITypeProvider.ID, }) - sdkErr := requireSDKError(t, err, http.StatusBadRequest) - require.Equal(t, "OpenRouter-like provider configured as type openai does not support slash-namespaced models.", sdkErr.Message) - require.Contains(t, sdkErr.Detail, "Change the AI provider type to openrouter or openai-compat.") + require.NoError(t, err) + require.Equal(t, openAITypeProvider.ID, updated.AIProviderID) + require.Equal(t, "anthropic/claude-opus-4.6", updated.Model) }) t.Run("DisablePreservesRecordAndHidesItFromNonAdmins", func(t *testing.T) { diff --git a/coderd/x/chatd/chatprovider/chatprovider.go b/coderd/x/chatd/chatprovider/chatprovider.go index b96180eb54a6a..f848e8a53f11f 100644 --- a/coderd/x/chatd/chatprovider/chatprovider.go +++ b/coderd/x/chatd/chatprovider/chatprovider.go @@ -4,7 +4,6 @@ import ( "context" "mime" "net/http" - neturl "net/url" "slices" "strings" @@ -264,30 +263,6 @@ func (k ProviderAPIKeys) Region(provider string) string { return strings.TrimSpace(k.RegionByProvider[normalized]) } -// ProviderBaseURLHostname returns the normalized hostname from a provider base URL. -func ProviderBaseURLHostname(baseURL string) string { - parsed, ok := parseProviderBaseURL(baseURL) - if !ok { - return "" - } - return strings.ToLower(parsed.Hostname()) -} - -func parseProviderBaseURL(baseURL string) (*neturl.URL, bool) { - baseURL = strings.TrimSpace(baseURL) - if baseURL == "" { - return nil, false - } - parsed, err := neturl.Parse(baseURL) - if err == nil && parsed.Hostname() == "" && !strings.Contains(baseURL, "://") { - parsed, err = neturl.Parse("https://" + baseURL) - } - if err != nil { - return nil, false - } - return parsed, true -} - // setRegion records a normalized, non-empty region for a provider. The // RegionByProvider map is allocated lazily so an unused set stays nil, which // keeps Empty() and value comparisons stable. diff --git a/coderd/x/chatd/chatprovider/chatprovider_test.go b/coderd/x/chatd/chatprovider/chatprovider_test.go index eccaf06d1cb88..90302a667ea39 100644 --- a/coderd/x/chatd/chatprovider/chatprovider_test.go +++ b/coderd/x/chatd/chatprovider/chatprovider_test.go @@ -31,28 +31,6 @@ import ( "github.com/coder/coder/v2/testutil" ) -func TestProviderBaseURLHostname(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - baseURL string - want string - }{ - {name: "URL", baseURL: "https://openrouter.ai/api/v1", want: "openrouter.ai"}, - {name: "BareHost", baseURL: "openrouter.ai", want: "openrouter.ai"}, - {name: "HostWithPort", baseURL: "https://openrouter.ai:443/api/v1", want: "openrouter.ai"}, - {name: "Empty", baseURL: "", want: ""}, - {name: "Invalid", baseURL: "://", want: ""}, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - require.Equal(t, tt.want, chatprovider.ProviderBaseURLHostname(tt.baseURL)) - }) - } -} - func TestResolveUserProviderKeys(t *testing.T) { t.Parallel() diff --git a/coderd/x/chatd/model_routing_aibridge.go b/coderd/x/chatd/model_routing_aibridge.go index ab03eae80df44..a9e8006385f84 100644 --- a/coderd/x/chatd/model_routing_aibridge.go +++ b/coderd/x/chatd/model_routing_aibridge.go @@ -84,31 +84,6 @@ func (t *aiGatewayRoundTripper) RoundTrip(req *http.Request) (*http.Response, er return t.base.RoundTrip(cloned) } -// ValidateAIGatewayProviderModel rejects slash-namespaced models when an -// OpenRouter-like gateway is configured with the OpenAI provider type. -func ValidateAIGatewayProviderModel(provider database.AIProvider, model string) error { - if provider.Type != database.AIProviderTypeOpenai { - return nil - } - if !isSlashNamespacedAIGatewayModel(model) || !isOpenRouterLikeAIGatewayProvider(provider) { - return nil - } - return xerrors.New("OpenRouter-like provider configured as type openai does not support slash-namespaced models") -} - -func isSlashNamespacedAIGatewayModel(model string) bool { - prefix, suffix, ok := strings.Cut(strings.TrimSpace(model), "/") - return ok && strings.TrimSpace(prefix) != "" && strings.TrimSpace(suffix) != "" -} - -func isOpenRouterLikeAIGatewayProvider(provider database.AIProvider) bool { - if strings.EqualFold(strings.TrimSpace(provider.Name), "openrouter") { - return true - } - host := chatprovider.ProviderBaseURLHostname(provider.BaseUrl) - return host == "openrouter.ai" || strings.HasSuffix(host, ".openrouter.ai") -} - func (p *Server) newModel( _ context.Context, req modelClientRequest, @@ -132,17 +107,6 @@ func (p *Server) newModel( ) } - if err := ValidateAIGatewayProviderModel(route.Provider, req.ModelName); err != nil { - return chatprovider.Model{}, chaterror.WithClassification( - err, - chaterror.ClassifiedError{ - Kind: codersdk.ChatErrorKindConfig, - Retryable: false, - Detail: "Ask an administrator to change the AI provider type to openrouter or openai-compat.", - }, - ) - } - factoryPtr := p.aibridgeTransportFactory if factoryPtr == nil { return chatprovider.Model{}, xerrors.New("AI Gateway transport factory is not configured") diff --git a/coderd/x/chatd/model_routing_internal_test.go b/coderd/x/chatd/model_routing_internal_test.go index 6a6d1324ee6e1..5a546343b75a3 100644 --- a/coderd/x/chatd/model_routing_internal_test.go +++ b/coderd/x/chatd/model_routing_internal_test.go @@ -498,28 +498,6 @@ func TestAIBridgeRoutingFailClosed(t *testing.T) { require.False(t, classified.Retryable) }) - t.Run("OpenRouterMisconfiguredAsOpenAI", func(t *testing.T) { - t.Parallel() - factory := &aibridgeTestFactory{rt: roundTripFunc(func(*http.Request) (*http.Response, error) { - t.Fatal("transport must not be used for invalid provider config") - return nil, xerrors.New("unreachable") - })} - server := &Server{ - aibridgeTransportFactory: aibridgeTestFactoryPointer(factory), - } - provider := aibridgeTestAIProvider(providerID, "openrouter", database.AIProviderTypeOpenai) - _, err := server.newModel( - t.Context(), - aibridgeTestRequest(chat, "anthropic/claude-opus-4.6"), - aibridgeTestRoute(provider), - modelBuildOptions{ActiveAPIKeyID: uuid.NewString()}, - ) - require.ErrorContains(t, err, "does not support slash-namespaced models") - classified := chaterror.Classify(err) - require.Equal(t, codersdk.ChatErrorKindConfig, classified.Kind) - require.False(t, classified.Retryable) - }) - t.Run("StaticModel", func(t *testing.T) { t.Parallel() server := &Server{} @@ -528,7 +506,7 @@ func TestAIBridgeRoutingFailClosed(t *testing.T) { }) } -func TestAIBridgeGatewayProviderTypesPreserveSlashModelID(t *testing.T) { +func TestAIBridgeProviderTypesPreserveSlashModelID(t *testing.T) { t.Parallel() const modelName = "anthropic/claude-opus-4.6" @@ -547,6 +525,11 @@ func TestAIBridgeGatewayProviderTypesPreserveSlashModelID(t *testing.T) { providerName: "openai-compatible-relay", providerType: database.AIProviderTypeOpenaiCompat, }, + { + name: "OpenAI", + providerName: "openrouter", + providerType: database.AIProviderTypeOpenai, + }, } for _, tt := range tests { From d475056a36eb597371547380a55dffc36c607b5d Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Thu, 13 Aug 2026 18:02:51 +0000 Subject: [PATCH 3/4] chore(coderd): drop comment restating revalidation predicate --- coderd/exp_chats.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/coderd/exp_chats.go b/coderd/exp_chats.go index 627efc63437de..4d55f3e979490 100644 --- a/coderd/exp_chats.go +++ b/coderd/exp_chats.go @@ -6737,8 +6737,6 @@ func (api *API) updateChatModelConfig(rw http.ResponseWriter, r *http.Request) { ID: existing.ID, } - // Re-check the provider exists and is enabled under lock when the - // model or provider changes. revalidateProviderModel := updateParams.AIProviderID.Valid && (req.AIProviderID != nil || strings.TrimSpace(req.Model) != "") var updated database.ChatModelConfig err = api.inChatModelConfigWriteTx(ctx, func(tx database.Store) error { From a1d636a1a2a94013155ab869f3bf7f56659d0197 Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Thu, 13 Aug 2026 18:28:34 +0000 Subject: [PATCH 4/4] fix(coderd): restore AI provider model validation --- coderd/exp_chats.go | 41 +++++++ coderd/exp_chats_internal_test.go | 113 ++++++++++++++++++ coderd/exp_chats_test.go | 34 +++--- coderd/x/chatd/chatprovider/chatprovider.go | 25 ++++ .../x/chatd/chatprovider/chatprovider_test.go | 22 ++++ coderd/x/chatd/model_routing_aibridge.go | 36 ++++++ coderd/x/chatd/model_routing_internal_test.go | 29 ++++- 7 files changed, 278 insertions(+), 22 deletions(-) diff --git a/coderd/exp_chats.go b/coderd/exp_chats.go index 4d55f3e979490..eed66f53e3c25 100644 --- a/coderd/exp_chats.go +++ b/coderd/exp_chats.go @@ -6428,6 +6428,26 @@ func (api *API) listChatModelConfigs(rw http.ResponseWriter, r *http.Request) { httpapi.Write(ctx, rw, http.StatusOK, resp) } +type chatModelConfigProviderModelError struct { + Response codersdk.Response +} + +func (e *chatModelConfigProviderModelError) Error() string { + return e.Response.Message +} + +func validateChatModelConfigProviderModel(aiProvider database.AIProvider, model string) *chatModelConfigProviderModelError { + if err := chatd.ValidateAIGatewayProviderModel(aiProvider, model); err != nil { + return &chatModelConfigProviderModelError{ + Response: codersdk.Response{ + Message: "OpenRouter-like provider configured as type openai does not support slash-namespaced models.", + Detail: "Change the AI provider type to openrouter or openai-compat. Slash-namespaced model IDs on OpenRouter-like gateways require one of those provider types.", + }, + } + } + return nil +} + // inChatModelConfigWriteTx runs fn in a transaction that holds the advisory // lock serializing chat model config writes. All writes to the table must go // through this helper so concurrent writers cannot act on stale reads, e.g. @@ -6486,6 +6506,11 @@ func (api *API) createChatModelConfig(rw http.ResponseWriter, r *http.Request) { return } + if validationErr := validateChatModelConfigProviderModel(aiProvider, model); validationErr != nil { + httpapi.Write(ctx, rw, http.StatusBadRequest, validationErr.Response) + return + } + enabled := true if req.Enabled != nil { enabled = *req.Enabled @@ -6551,6 +6576,10 @@ func (api *API) createChatModelConfig(rw http.ResponseWriter, r *http.Request) { if !lockedAIProvider.Enabled { return errChatProviderNotConfigured } + if err := validateChatModelConfigProviderModel(lockedAIProvider, insertParams.Model); err != nil { + return err + } + insertAsDefault := isDefault if !insertAsDefault { _, err := tx.GetDefaultChatModelConfig(ctx) @@ -6589,7 +6618,11 @@ func (api *API) createChatModelConfig(rw http.ResponseWriter, r *http.Request) { return nil }) if err != nil { + var providerModelErr *chatModelConfigProviderModelError switch { + case errors.As(err, &providerModelErr): + httpapi.Write(ctx, rw, http.StatusBadRequest, providerModelErr.Response) + return case database.IsUniqueViolation(err): httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{ Message: "Chat model config already exists.", @@ -6737,6 +6770,7 @@ func (api *API) updateChatModelConfig(rw http.ResponseWriter, r *http.Request) { ID: existing.ID, } + // Re-derive the provider type under lock when the model or provider changes. revalidateProviderModel := updateParams.AIProviderID.Valid && (req.AIProviderID != nil || strings.TrimSpace(req.Model) != "") var updated database.ChatModelConfig err = api.inChatModelConfigWriteTx(ctx, func(tx database.Store) error { @@ -6752,6 +6786,9 @@ func (api *API) updateChatModelConfig(rw http.ResponseWriter, r *http.Request) { if !aiProvider.Enabled { return errChatProviderNotConfigured } + if err := validateChatModelConfigProviderModel(aiProvider, updateParams.Model); err != nil { + return err + } } setAsDefault := updateParams.IsDefault && !existing.IsDefault @@ -6794,7 +6831,11 @@ func (api *API) updateChatModelConfig(rw http.ResponseWriter, r *http.Request) { return nil }) if err != nil { + var providerModelErr *chatModelConfigProviderModelError switch { + case errors.As(err, &providerModelErr): + httpapi.Write(ctx, rw, http.StatusBadRequest, providerModelErr.Response) + return case database.IsUniqueViolation(err): httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{ Message: "Chat model config already exists.", diff --git a/coderd/exp_chats_internal_test.go b/coderd/exp_chats_internal_test.go index 63e9e2f18c3e5..85ad42dbdda0b 100644 --- a/coderd/exp_chats_internal_test.go +++ b/coderd/exp_chats_internal_test.go @@ -231,6 +231,119 @@ func TestValidateChatModelProviderOptions_AnthropicThinkingDisplay(t *testing.T) } } +func TestValidateChatModelConfigProviderModel(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + model string + provider database.AIProvider + wantErr bool + wantDetail string + }{ + { + name: "OpenRouterNameWithOpenAITypeAndSlashModel", + model: "anthropic/claude-opus-4.6", + provider: database.AIProvider{ + Name: "openrouter", + Type: database.AIProviderTypeOpenai, + }, + wantErr: true, + wantDetail: "Change the AI provider type to openrouter or openai-compat.", + }, + { + name: "OpenRouterNameWithWhitespaceAndCase", + model: "anthropic/claude-opus-4.6", + provider: database.AIProvider{ + Name: " OpenRouter ", + Type: database.AIProviderTypeOpenai, + }, + wantErr: true, + wantDetail: "Change the AI provider type to openrouter or openai-compat.", + }, + { + name: "OpenRouterHostWithOpenAITypeAndSlashModel", + model: "anthropic/claude-opus-4.6", + provider: database.AIProvider{ + Name: "private-relay", + Type: database.AIProviderTypeOpenai, + BaseUrl: "https://openrouter.ai/api/v1", + }, + wantErr: true, + wantDetail: "Change the AI provider type to openrouter or openai-compat.", + }, + { + name: "OpenRouterHostWithPort", + model: "anthropic/claude-opus-4.6", + provider: database.AIProvider{ + Name: "private-relay", + Type: database.AIProviderTypeOpenai, + BaseUrl: "https://openrouter.ai:443/api/v1", + }, + wantErr: true, + wantDetail: "Change the AI provider type to openrouter or openai-compat.", + }, + { + name: "OpenRouterSubdomainWithOpenAIType", + model: "anthropic/claude-opus-4.6", + provider: database.AIProvider{ + Name: "private-relay", + Type: database.AIProviderTypeOpenai, + BaseUrl: "https://api.openrouter.ai/v1", + }, + wantErr: true, + wantDetail: "Change the AI provider type to openrouter or openai-compat.", + }, + { + name: "OpenRouterTypeAllowsSlashModel", + model: "anthropic/claude-opus-4.6", + provider: database.AIProvider{ + Name: "openrouter", + Type: database.AIProviderTypeOpenrouter, + }, + }, + { + name: "OpenAICompatTypeAllowsSlashModel", + model: "anthropic/claude-opus-4.6", + provider: database.AIProvider{ + Name: "openrouter", + Type: database.AIProviderTypeOpenaiCompat, + }, + }, + { + name: "PrivateOpenAIProxyAllowsSlashModel", + model: "anthropic/claude-opus-4.6", + provider: database.AIProvider{ + Name: "private-relay", + Type: database.AIProviderTypeOpenai, + BaseUrl: "https://llm-relay.internal/v1", + }, + }, + { + name: "OpenRouterNameWithPlainModelAllowed", + model: "gpt-4.1", + provider: database.AIProvider{ + Name: "openrouter", + Type: database.AIProviderTypeOpenai, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + got := validateChatModelConfigProviderModel(tt.provider, tt.model) + if tt.wantErr { + require.NotNil(t, got) + require.Contains(t, got.Response.Detail, tt.wantDetail) + return + } + require.Nil(t, got) + }) + } +} + func TestWriteChatFileErrorUnavailable(t *testing.T) { t.Parallel() diff --git a/coderd/exp_chats_test.go b/coderd/exp_chats_test.go index f3f1500ea7754..e32fae2a70b8e 100644 --- a/coderd/exp_chats_test.go +++ b/coderd/exp_chats_test.go @@ -4290,7 +4290,7 @@ func TestCreateChatModelConfig(t *testing.T) { require.Equal(t, "AI provider is disabled.", sdkErr.Message) }) - t.Run("AllowsSlashNamespacedModelOnOpenAIType", func(t *testing.T) { + t.Run("RejectsOpenRouterMisconfiguredAsOpenAI", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitLong) @@ -4307,13 +4307,14 @@ func TestCreateChatModelConfig(t *testing.T) { require.NoError(t, err) contextLimit := int64(4096) - modelConfig, err := client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ + _, err = client.CreateChatModelConfig(ctx, codersdk.CreateChatModelConfigRequest{ AIProviderID: &aiProvider.ID, Model: "anthropic/claude-opus-4.6", ContextLimit: &contextLimit, }) - require.NoError(t, err) - require.Equal(t, "anthropic/claude-opus-4.6", modelConfig.Model) + sdkErr := requireSDKError(t, err, http.StatusBadRequest) + require.Equal(t, "OpenRouter-like provider configured as type openai does not support slash-namespaced models.", sdkErr.Message) + require.Contains(t, sdkErr.Detail, "Change the AI provider type to openrouter or openai-compat.") }) t.Run("ForbiddenForOrganizationMember", func(t *testing.T) { @@ -4381,7 +4382,7 @@ func TestUpdateChatModelConfig(t *testing.T) { require.Equal(t, "gpt-4o-mini-updated", updated.Model) }) - t.Run("AllowsModelChangeToSlashNamespacedOnOpenAIType", func(t *testing.T) { + t.Run("RejectsOpenRouterMisconfiguredAsOpenAI", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitLong) @@ -4405,14 +4406,15 @@ func TestUpdateChatModelConfig(t *testing.T) { }) require.NoError(t, err) - updated, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ + _, err = client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ Model: "anthropic/claude-opus-4.6", }) - require.NoError(t, err) - require.Equal(t, "anthropic/claude-opus-4.6", updated.Model) + sdkErr := requireSDKError(t, err, http.StatusBadRequest) + require.Equal(t, "OpenRouter-like provider configured as type openai does not support slash-namespaced models.", sdkErr.Message) + require.Contains(t, sdkErr.Detail, "Change the AI provider type to openrouter or openai-compat.") }) - t.Run("UnrelatedEditPreservesSlashNamespacedModel", func(t *testing.T) { + t.Run("AllowsUnrelatedEditOnExistingMisconfiguredOpenAI", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitLong) @@ -4441,7 +4443,7 @@ func TestUpdateChatModelConfig(t *testing.T) { require.Equal(t, modelConfig.Model, updated.Model) }) - t.Run("AllowsProviderChangeToOpenAITypeWithSlashNamespacedModel", func(t *testing.T) { + t.Run("RejectsProviderChangeToMisconfiguredOpenAI", func(t *testing.T) { t.Parallel() ctx := testutil.Context(t, testutil.WaitLong) @@ -4456,7 +4458,7 @@ func TestUpdateChatModelConfig(t *testing.T) { APIKeys: []string{"test-api-key"}, }) require.NoError(t, err) - openAITypeProvider, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{ + misconfiguredProvider, err := client.CreateAIProvider(ctx, codersdk.CreateAIProviderRequest{ Type: codersdk.AIProviderTypeOpenAI, Name: "openrouter", Enabled: true, @@ -4473,12 +4475,12 @@ func TestUpdateChatModelConfig(t *testing.T) { }) require.NoError(t, err) - updated, err := client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ - AIProviderID: &openAITypeProvider.ID, + _, err = client.UpdateChatModelConfig(ctx, modelConfig.ID, codersdk.UpdateChatModelConfigRequest{ + AIProviderID: &misconfiguredProvider.ID, }) - require.NoError(t, err) - require.Equal(t, openAITypeProvider.ID, updated.AIProviderID) - require.Equal(t, "anthropic/claude-opus-4.6", updated.Model) + sdkErr := requireSDKError(t, err, http.StatusBadRequest) + require.Equal(t, "OpenRouter-like provider configured as type openai does not support slash-namespaced models.", sdkErr.Message) + require.Contains(t, sdkErr.Detail, "Change the AI provider type to openrouter or openai-compat.") }) t.Run("DisablePreservesRecordAndHidesItFromNonAdmins", func(t *testing.T) { diff --git a/coderd/x/chatd/chatprovider/chatprovider.go b/coderd/x/chatd/chatprovider/chatprovider.go index f848e8a53f11f..b96180eb54a6a 100644 --- a/coderd/x/chatd/chatprovider/chatprovider.go +++ b/coderd/x/chatd/chatprovider/chatprovider.go @@ -4,6 +4,7 @@ import ( "context" "mime" "net/http" + neturl "net/url" "slices" "strings" @@ -263,6 +264,30 @@ func (k ProviderAPIKeys) Region(provider string) string { return strings.TrimSpace(k.RegionByProvider[normalized]) } +// ProviderBaseURLHostname returns the normalized hostname from a provider base URL. +func ProviderBaseURLHostname(baseURL string) string { + parsed, ok := parseProviderBaseURL(baseURL) + if !ok { + return "" + } + return strings.ToLower(parsed.Hostname()) +} + +func parseProviderBaseURL(baseURL string) (*neturl.URL, bool) { + baseURL = strings.TrimSpace(baseURL) + if baseURL == "" { + return nil, false + } + parsed, err := neturl.Parse(baseURL) + if err == nil && parsed.Hostname() == "" && !strings.Contains(baseURL, "://") { + parsed, err = neturl.Parse("https://" + baseURL) + } + if err != nil { + return nil, false + } + return parsed, true +} + // setRegion records a normalized, non-empty region for a provider. The // RegionByProvider map is allocated lazily so an unused set stays nil, which // keeps Empty() and value comparisons stable. diff --git a/coderd/x/chatd/chatprovider/chatprovider_test.go b/coderd/x/chatd/chatprovider/chatprovider_test.go index 90302a667ea39..eccaf06d1cb88 100644 --- a/coderd/x/chatd/chatprovider/chatprovider_test.go +++ b/coderd/x/chatd/chatprovider/chatprovider_test.go @@ -31,6 +31,28 @@ import ( "github.com/coder/coder/v2/testutil" ) +func TestProviderBaseURLHostname(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + baseURL string + want string + }{ + {name: "URL", baseURL: "https://openrouter.ai/api/v1", want: "openrouter.ai"}, + {name: "BareHost", baseURL: "openrouter.ai", want: "openrouter.ai"}, + {name: "HostWithPort", baseURL: "https://openrouter.ai:443/api/v1", want: "openrouter.ai"}, + {name: "Empty", baseURL: "", want: ""}, + {name: "Invalid", baseURL: "://", want: ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + require.Equal(t, tt.want, chatprovider.ProviderBaseURLHostname(tt.baseURL)) + }) + } +} + func TestResolveUserProviderKeys(t *testing.T) { t.Parallel() diff --git a/coderd/x/chatd/model_routing_aibridge.go b/coderd/x/chatd/model_routing_aibridge.go index a9e8006385f84..ab03eae80df44 100644 --- a/coderd/x/chatd/model_routing_aibridge.go +++ b/coderd/x/chatd/model_routing_aibridge.go @@ -84,6 +84,31 @@ func (t *aiGatewayRoundTripper) RoundTrip(req *http.Request) (*http.Response, er return t.base.RoundTrip(cloned) } +// ValidateAIGatewayProviderModel rejects slash-namespaced models when an +// OpenRouter-like gateway is configured with the OpenAI provider type. +func ValidateAIGatewayProviderModel(provider database.AIProvider, model string) error { + if provider.Type != database.AIProviderTypeOpenai { + return nil + } + if !isSlashNamespacedAIGatewayModel(model) || !isOpenRouterLikeAIGatewayProvider(provider) { + return nil + } + return xerrors.New("OpenRouter-like provider configured as type openai does not support slash-namespaced models") +} + +func isSlashNamespacedAIGatewayModel(model string) bool { + prefix, suffix, ok := strings.Cut(strings.TrimSpace(model), "/") + return ok && strings.TrimSpace(prefix) != "" && strings.TrimSpace(suffix) != "" +} + +func isOpenRouterLikeAIGatewayProvider(provider database.AIProvider) bool { + if strings.EqualFold(strings.TrimSpace(provider.Name), "openrouter") { + return true + } + host := chatprovider.ProviderBaseURLHostname(provider.BaseUrl) + return host == "openrouter.ai" || strings.HasSuffix(host, ".openrouter.ai") +} + func (p *Server) newModel( _ context.Context, req modelClientRequest, @@ -107,6 +132,17 @@ func (p *Server) newModel( ) } + if err := ValidateAIGatewayProviderModel(route.Provider, req.ModelName); err != nil { + return chatprovider.Model{}, chaterror.WithClassification( + err, + chaterror.ClassifiedError{ + Kind: codersdk.ChatErrorKindConfig, + Retryable: false, + Detail: "Ask an administrator to change the AI provider type to openrouter or openai-compat.", + }, + ) + } + factoryPtr := p.aibridgeTransportFactory if factoryPtr == nil { return chatprovider.Model{}, xerrors.New("AI Gateway transport factory is not configured") diff --git a/coderd/x/chatd/model_routing_internal_test.go b/coderd/x/chatd/model_routing_internal_test.go index 5a546343b75a3..6a6d1324ee6e1 100644 --- a/coderd/x/chatd/model_routing_internal_test.go +++ b/coderd/x/chatd/model_routing_internal_test.go @@ -498,6 +498,28 @@ func TestAIBridgeRoutingFailClosed(t *testing.T) { require.False(t, classified.Retryable) }) + t.Run("OpenRouterMisconfiguredAsOpenAI", func(t *testing.T) { + t.Parallel() + factory := &aibridgeTestFactory{rt: roundTripFunc(func(*http.Request) (*http.Response, error) { + t.Fatal("transport must not be used for invalid provider config") + return nil, xerrors.New("unreachable") + })} + server := &Server{ + aibridgeTransportFactory: aibridgeTestFactoryPointer(factory), + } + provider := aibridgeTestAIProvider(providerID, "openrouter", database.AIProviderTypeOpenai) + _, err := server.newModel( + t.Context(), + aibridgeTestRequest(chat, "anthropic/claude-opus-4.6"), + aibridgeTestRoute(provider), + modelBuildOptions{ActiveAPIKeyID: uuid.NewString()}, + ) + require.ErrorContains(t, err, "does not support slash-namespaced models") + classified := chaterror.Classify(err) + require.Equal(t, codersdk.ChatErrorKindConfig, classified.Kind) + require.False(t, classified.Retryable) + }) + t.Run("StaticModel", func(t *testing.T) { t.Parallel() server := &Server{} @@ -506,7 +528,7 @@ func TestAIBridgeRoutingFailClosed(t *testing.T) { }) } -func TestAIBridgeProviderTypesPreserveSlashModelID(t *testing.T) { +func TestAIBridgeGatewayProviderTypesPreserveSlashModelID(t *testing.T) { t.Parallel() const modelName = "anthropic/claude-opus-4.6" @@ -525,11 +547,6 @@ func TestAIBridgeProviderTypesPreserveSlashModelID(t *testing.T) { providerName: "openai-compatible-relay", providerType: database.AIProviderTypeOpenaiCompat, }, - { - name: "OpenAI", - providerName: "openrouter", - providerType: database.AIProviderTypeOpenai, - }, } for _, tt := range tests {