From e18e36625ec713540974a56aed82ebaf117924a3 Mon Sep 17 00:00:00 2001 From: Ed Zynda Date: Thu, 26 Mar 2026 12:44:19 +0300 Subject: [PATCH] fix: route opencode models through correct provider API Models from the opencode provider (like claude-opus-4-6 and gpt-5.3-codex) have provider overrides in the models database that specify different npm packages than the provider's default. The code was ignoring these overrides and routing all models through openaicompat, causing "bad request" errors. Changes: - Added Provider field to modelsDBModel to capture model-specific overrides - Added ProviderNPM field to ModelInfo registry struct - Updated autoRouteProvider() to check for model-specific provider overrides - Fixed URL path handling for anthropic provider (strip /v1 suffix to avoid double /v1/v1 paths when using third-party anthropic-compatible APIs) Fixes routing for: - opencode/claude-opus-4-6 -> @ai-sdk/anthropic - opencode/gpt-5.3-codex -> @ai-sdk/openai --- internal/models/modelsdb.go | 24 +++++++++++++++--------- internal/models/providers.go | 17 ++++++++++++++--- internal/models/registry.go | 6 ++++++ 3 files changed, 35 insertions(+), 12 deletions(-) diff --git a/internal/models/modelsdb.go b/internal/models/modelsdb.go index 6605a85e..0092ea9b 100644 --- a/internal/models/modelsdb.go +++ b/internal/models/modelsdb.go @@ -17,15 +17,21 @@ type modelsDBProvider struct { // modelsDBModel represents a model entry from models.dev/api.json. type modelsDBModel struct { - ID string `json:"id"` - Name string `json:"name"` - Family string `json:"family,omitempty"` - Attachment bool `json:"attachment"` - Reasoning bool `json:"reasoning"` - ToolCall bool `json:"tool_call"` - Temperature bool `json:"temperature"` - Cost modelsDBCost `json:"cost"` - Limit modelsDBLimit `json:"limit"` + ID string `json:"id"` + Name string `json:"name"` + Family string `json:"family,omitempty"` + Attachment bool `json:"attachment"` + Reasoning bool `json:"reasoning"` + ToolCall bool `json:"tool_call"` + Temperature bool `json:"temperature"` + Cost modelsDBCost `json:"cost"` + Limit modelsDBLimit `json:"limit"` + Provider *modelsDBModelProvider `json:"provider,omitempty"` // Model-specific provider override +} + +// modelsDBModelProvider represents a provider reference within a model. +type modelsDBModelProvider struct { + NPM string `json:"npm"` } // modelsDBCost represents model pricing from models.dev. diff --git a/internal/models/providers.go b/internal/models/providers.go index cda33598..fb7b2e92 100644 --- a/internal/models/providers.go +++ b/internal/models/providers.go @@ -263,14 +263,22 @@ func CreateProvider(ctx context.Context, config *ProviderConfig) (*ProviderResul // autoRouteProvider attempts to create a provider by looking up its npm package // in the models.dev database and routing through the appropriate fantasy provider. // For openai-compatible providers, it uses the api URL from models.dev. +// Models may have a provider override that specifies a different npm package than +// the provider's default (e.g., opencode's claude-opus-4-6 uses @ai-sdk/anthropic). func autoRouteProvider(ctx context.Context, config *ProviderConfig, provider, modelName string, registry *ModelsRegistry) (*ProviderResult, error) { providerInfo := registry.GetProviderInfo(provider) if providerInfo == nil { return nil, fmt.Errorf("unsupported provider: %s (not found in model database)", provider) } + // Check for model-specific provider override + npmPackage := providerInfo.NPM + if modelInfo := registry.LookupModel(provider, modelName); modelInfo != nil && modelInfo.ProviderNPM != "" { + npmPackage = modelInfo.ProviderNPM + } + // Determine the fantasy provider for this npm package - fantasyProvider := npmToFantasyProvider[providerInfo.NPM] + fantasyProvider := npmToFantasyProvider[npmPackage] if fantasyProvider == "" && providerInfo.API != "" { // Unknown npm but has API URL → route through openaicompat fantasyProvider = "openaicompat" @@ -290,7 +298,7 @@ func autoRouteProvider(ctx context.Context, config *ProviderConfig, provider, mo } return createAutoRoutedOpenAIProvider(ctx, config, modelName, providerInfo) default: - return nil, fmt.Errorf("unsupported provider: %s (npm: %s has no fantasy mapping)", provider, providerInfo.NPM) + return nil, fmt.Errorf("unsupported provider: %s (npm: %s has no fantasy mapping)", provider, npmPackage) } } @@ -348,7 +356,10 @@ func createAutoRoutedAnthropicProvider(ctx context.Context, config *ProviderConf opts = append(opts, anthropic.WithAPIKey(apiKey)) if config.ProviderURL != "" { - opts = append(opts, anthropic.WithBaseURL(config.ProviderURL)) + // The anthropic client appends "/v1/messages" to the base URL. + // If the provider URL ends with "/v1", strip it to avoid double "/v1/v1" paths. + baseURL := strings.TrimSuffix(config.ProviderURL, "/v1") + opts = append(opts, anthropic.WithBaseURL(baseURL)) } if config.TLSSkipVerify { diff --git a/internal/models/registry.go b/internal/models/registry.go index 60ac5289..bb265a1d 100644 --- a/internal/models/registry.go +++ b/internal/models/registry.go @@ -22,6 +22,7 @@ type ModelInfo struct { Temperature bool Cost Cost Limit Limit + ProviderNPM string // Model-specific provider npm override (e.g. "@ai-sdk/anthropic") } // Cost represents the pricing information for a model. @@ -78,6 +79,10 @@ func buildFromModelsDB() map[string]ProviderInfo { for providerID, dp := range dbProviders { modelsMap := make(map[string]ModelInfo, len(dp.Models)) for modelID, dm := range dp.Models { + providerNPM := "" + if dm.Provider != nil { + providerNPM = dm.Provider.NPM + } modelsMap[modelID] = ModelInfo{ ID: dm.ID, Name: dm.Name, @@ -94,6 +99,7 @@ func buildFromModelsDB() map[string]ProviderInfo { Context: dm.Limit.Context, Output: dm.Limit.Output, }, + ProviderNPM: providerNPM, } }