Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion coderd/exp_chats.go
Original file line number Diff line number Diff line change
Expand Up @@ -6781,7 +6781,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.",
},
}
}
Expand Down
30 changes: 4 additions & 26 deletions coderd/x/chatd/chatprovider/chatprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -598,21 +598,6 @@ func orderProviders(providerSet map[string]struct{}) []string {
return ordered
}

// isGatewayProvider reports whether the provider routes requests to
// multiple upstream model providers using a "<provider>/<model>" 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)) {
Expand Down Expand Up @@ -643,23 +628,16 @@ func ResolveModelWithProviderHint(modelName, providerHint string) (provider stri
return "", "", xerrors.New("model is required")
}

// Gateway providers (vercel, openrouter, openai-compat) treat the
// "<provider>/<model>" 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":
Expand Down
38 changes: 33 additions & 5 deletions coderd/x/chatd/chatprovider/chatprovider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1510,11 +1510,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",
Expand All @@ -1524,11 +1524,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",
Expand Down
5 changes: 2 additions & 3 deletions coderd/x/chatd/model_routing_aibridge.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down