diff --git a/coderd/exp_chats.go b/coderd/exp_chats.go index 40d1d19223381..f3bfa3d172eda 100644 --- a/coderd/exp_chats.go +++ b/coderd/exp_chats.go @@ -6853,7 +6853,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 4f116ef1b75a4..7318f52ed45a2 100644 --- a/coderd/x/chatd/chatprovider/chatprovider.go +++ b/coderd/x/chatd/chatprovider/chatprovider.go @@ -687,21 +687,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)) { @@ -732,23 +717,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 8a8904c8ef4f4..9877ef4c4e32e 100644 --- a/coderd/x/chatd/chatprovider/chatprovider_test.go +++ b/coderd/x/chatd/chatprovider/chatprovider_test.go @@ -1659,11 +1659,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", @@ -1673,11 +1673,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 832cd31a6e121..7fe9320c4251f 100644 --- a/coderd/x/chatd/model_routing_aibridge.go +++ b/coderd/x/chatd/model_routing_aibridge.go @@ -87,9 +87,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