mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
fix(coderd/x/chatd/chatprovider): keep gateway model prefix in ResolveModelWithProviderHint (#25725)
For `vercel`, `openrouter`, and `openai-compat`, the `<provider>/<model>` slash is part of the upstream model ID rather than a hint. `ResolveModelWithProviderHint` was running `parseCanonicalModelRef` before honoring `providerHint`, so a config like `(provider=vercel, model=anthropic/claude-4-5-sonnet)` resolved to `provider=anthropic, model=claude-4-5-sonnet` and the prefix-less model name was forwarded to Vercel, which returned `Model 'claude-4-5-sonnet' not found`. Honor an explicit gateway provider hint before attempting canonical-ref parsing. Non-gateway hints (anthropic, openai, etc.) keep the existing canonical-ref-first behavior so `anthropic/claude-...` still has its prefix stripped when routed directly to Anthropic. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -573,6 +573,21 @@ 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)) {
|
||||
@@ -603,6 +618,15 @@ 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
|
||||
}
|
||||
|
||||
if provider, modelID, ok := parseCanonicalModelRef(modelName); ok {
|
||||
return provider, modelID, nil
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
fantasyanthropic "charm.land/fantasy/providers/anthropic"
|
||||
fantasybedrock "charm.land/fantasy/providers/bedrock"
|
||||
fantasyopenai "charm.land/fantasy/providers/openai"
|
||||
fantasyopenaicompat "charm.land/fantasy/providers/openaicompat"
|
||||
fantasyopenrouter "charm.land/fantasy/providers/openrouter"
|
||||
fantasyvercel "charm.land/fantasy/providers/vercel"
|
||||
"github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream"
|
||||
@@ -1439,3 +1440,73 @@ func TestMergeMissingProviderOptions_OpenRouterNested(t *testing.T) {
|
||||
require.Equal(t, []string{"int8"}, options.OpenRouter.Provider.Quantizations)
|
||||
require.Equal(t, "latency", *options.OpenRouter.Provider.Sort)
|
||||
}
|
||||
|
||||
func TestResolveModelWithProviderHint(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
modelName string
|
||||
providerHint string
|
||||
wantProvider string
|
||||
wantModel string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "VercelHintPreservesPrefixedModelID",
|
||||
modelName: "anthropic/claude-4-5-sonnet",
|
||||
providerHint: fantasyvercel.Name,
|
||||
wantProvider: fantasyvercel.Name,
|
||||
wantModel: "anthropic/claude-4-5-sonnet",
|
||||
},
|
||||
{
|
||||
name: "OpenRouterHintPreservesPrefixedModelID",
|
||||
modelName: "anthropic/claude-3.5-haiku",
|
||||
providerHint: fantasyopenrouter.Name,
|
||||
wantProvider: fantasyopenrouter.Name,
|
||||
wantModel: "anthropic/claude-3.5-haiku",
|
||||
},
|
||||
{
|
||||
name: "OpenAICompatHintPreservesPrefixedModelID",
|
||||
modelName: "anthropic/claude-4-5-sonnet",
|
||||
providerHint: fantasyopenaicompat.Name,
|
||||
wantProvider: fantasyopenaicompat.Name,
|
||||
wantModel: "anthropic/claude-4-5-sonnet",
|
||||
},
|
||||
{
|
||||
name: "AnthropicHintStripsCanonicalPrefix",
|
||||
modelName: "anthropic/claude-4-5-sonnet",
|
||||
providerHint: fantasyanthropic.Name,
|
||||
wantProvider: fantasyanthropic.Name,
|
||||
wantModel: "claude-4-5-sonnet",
|
||||
},
|
||||
{
|
||||
name: "NoHintUsesCanonicalRef",
|
||||
modelName: "anthropic/claude-4-5-sonnet",
|
||||
providerHint: "",
|
||||
wantProvider: fantasyanthropic.Name,
|
||||
wantModel: "claude-4-5-sonnet",
|
||||
},
|
||||
{
|
||||
name: "VercelHintWithoutSlashPasses",
|
||||
modelName: "claude-4-5-sonnet",
|
||||
providerHint: fantasyvercel.Name,
|
||||
wantProvider: fantasyvercel.Name,
|
||||
wantModel: "claude-4-5-sonnet",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
provider, model, err := chatprovider.ResolveModelWithProviderHint(tt.modelName, tt.providerHint)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.wantProvider, provider)
|
||||
require.Equal(t, tt.wantModel, model)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user