Skip to content
Merged
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
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ mcpServers:
--model, -m Model to use (provider/model format)
--provider-api-key API key for the provider
--provider-url Base URL for provider API
--provider-wire Wire protocol for auto-routed providers (openai, openai-compat, anthropic, google)
--tls-skip-verify Skip TLS certificate verification

# Session management
Expand Down Expand Up @@ -1163,6 +1164,30 @@ kit --model opencode/gpt-5 "Hello" # → OpenAI wire
kit --model opencode/gemini-3.5-flash "Hello" # → Google wire
```

### Provider Overrides

Declare or patch providers in your config file's `providers` section — fix a
wire protocol the database gets wrong, or define an internal gateway the
database doesn't know about:

```yaml
# ~/.kit.yml
providers:
minimax: # patch a known provider
wire: anthropic
corp-llm: # declare a new one
wire: anthropic
baseUrl: https://llm.internal.corp/api
apiKeyEnv: [CORP_LLM_KEY]
headers:
X-Team: platform
```

Accepted `wire` values: `openai` (Responses API), `openai-compat` (chat
completions), `anthropic`, `google`. Unset fields inherit database values.
For one-off use, `--provider-wire` with `--provider-url` routes any
`provider/model` through the declared wire without a config entry.

### Model String Format

```bash
Expand Down
12 changes: 12 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ var (
modelFlag string
providerURL string
providerAPIKey string
providerWire string
debugMode bool
positionalPrompt string // set by processPositionalArgs from CLI positional args
positionalFiles []ui.FilePart // binary @file parts from processPositionalArgs
Expand Down Expand Up @@ -314,6 +315,7 @@ func init() {
flags := rootCmd.PersistentFlags()
flags.StringVar(&providerURL, "provider-url", "", "base URL for the provider API (applies to OpenAI, Anthropic, Ollama, and Google)")
flags.StringVar(&providerAPIKey, "provider-api-key", "", "API key for the provider (applies to OpenAI, Anthropic, and Google)")
flags.StringVar(&providerWire, "provider-wire", "", "wire protocol for auto-routed providers: openai, openai-compat, anthropic, google (overrides the model database)")
flags.BoolVar(&tlsSkipVerify, "tls-skip-verify", false, "skip TLS certificate verification (WARNING: insecure, use only for self-signed certificates)")

// Prompt template flags
Expand Down Expand Up @@ -345,6 +347,7 @@ func init() {

_ = viper.BindPFlag("provider-url", rootCmd.PersistentFlags().Lookup("provider-url"))
_ = viper.BindPFlag("provider-api-key", rootCmd.PersistentFlags().Lookup("provider-api-key"))
_ = viper.BindPFlag("provider-wire", rootCmd.PersistentFlags().Lookup("provider-wire"))
_ = viper.BindPFlag("max-tokens", rootCmd.PersistentFlags().Lookup("max-tokens"))
_ = viper.BindPFlag("temperature", rootCmd.PersistentFlags().Lookup("temperature"))
_ = viper.BindPFlag("top-p", rootCmd.PersistentFlags().Lookup("top-p"))
Expand Down Expand Up @@ -764,6 +767,15 @@ func applyProviderURLRouting() {
return
}

// When --provider-wire is set alongside --provider-url the user is being
// explicit about the wire protocol; the custom/ rewrite below would force
// the OpenAI-compatible wire, so skip it and let the model's provider
// prefix route through the auto-router (which honors --provider-wire and
// synthesizes unknown providers when both flags are present).
if viper.GetString("provider-wire") != "" {
return
}

// When --provider-url is set but no explicit --model was provided,
// default to "custom/custom" so the user doesn't need to remember a
// provider/model pair for custom OpenAI-compatible endpoints.
Expand Down
1 change: 1 addition & 0 deletions internal/kitsetup/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ func BuildProviderConfig(v *viper.Viper) (*models.ProviderConfig, string, error)
SystemPrompt: systemPrompt,
ProviderAPIKey: v.GetString("provider-api-key"),
ProviderURL: v.GetString("provider-url"),
ProviderWire: v.GetString("provider-wire"),
MaxTokens: v.GetInt("max-tokens"),
StopSequences: v.GetStringSlice("stop-sequences"),
NumGPU: &numGPU,
Expand Down
26 changes: 13 additions & 13 deletions internal/models/autoroute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,24 @@ import (
func TestNpmToWireProtocol(t *testing.T) {
want := map[string]wireProtocol{
"@ai-sdk/openai": wireOpenAI,
"@ai-sdk/openai-compatible": wireOpenAI,
"@ai-sdk/openai-compatible": wireOpenAICompat,
"@ai-sdk/anthropic": wireAnthropic,
"@ai-sdk/google": wireGoogle,

// Thin OpenAI-compatible wrappers — routed via openaicompat using
// the SDK's hard-coded default base URL (sdkDefaultBaseURL).
"@ai-sdk/groq": wireOpenAI,
"@ai-sdk/cerebras": wireOpenAI,
"@ai-sdk/perplexity": wireOpenAI,
"@ai-sdk/togetherai": wireOpenAI,
"@ai-sdk/xai": wireOpenAI,
"@ai-sdk/deepinfra": wireOpenAI,
"@ai-sdk/mistral": wireOpenAI,
"@ai-sdk/cohere": wireOpenAI,
"@ai-sdk/vercel": wireOpenAI,
"@aihubmix/ai-sdk-provider": wireOpenAI,
"venice-ai-sdk-provider": wireOpenAI,
"merge-gateway-ai-sdk-provider": wireOpenAI,
"@ai-sdk/groq": wireOpenAICompat,
"@ai-sdk/cerebras": wireOpenAICompat,
"@ai-sdk/perplexity": wireOpenAICompat,
"@ai-sdk/togetherai": wireOpenAICompat,
"@ai-sdk/xai": wireOpenAICompat,
"@ai-sdk/deepinfra": wireOpenAICompat,
"@ai-sdk/mistral": wireOpenAICompat,
"@ai-sdk/cohere": wireOpenAICompat,
"@ai-sdk/vercel": wireOpenAICompat,
"@aihubmix/ai-sdk-provider": wireOpenAICompat,
"venice-ai-sdk-provider": wireOpenAICompat,
"merge-gateway-ai-sdk-provider": wireOpenAICompat,
}
for npm, wire := range want {
if got := npmToWireProtocol[npm]; got != wire {
Expand Down
73 changes: 56 additions & 17 deletions internal/models/modelsdb.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package models

import "strings"

// ModelsDBProviders is the top-level type for models.dev/api.json data:
// a map of provider ID → provider object.
type ModelsDBProviders = map[string]modelsDBProvider
Expand Down Expand Up @@ -48,19 +50,56 @@ type modelsDBLimit struct {
Output int `json:"output"`
}

// wireProtocol identifies which LLM API protocol an npm package speaks.
// wireProtocol identifies which LLM API protocol a provider speaks.
// Fantasy implements three native protocols (openai, anthropic, google);
// everything else in its providers/ tree is a thin wrapper around one of
// them with a pre-baked default URL or auth scheme.
// them with a pre-baked default URL or auth scheme. The OpenAI protocol is
// split into two wires: wireOpenAI (the Responses API, spoken only by
// api.openai.com and true mirrors of it) and wireOpenAICompat (the
// chat-completions wire spoken by virtually every OpenAI-compatible
// provider and proxy).
type wireProtocol int

const (
wireUnknown wireProtocol = iota
wireOpenAI
wireUnknown wireProtocol = iota
wireOpenAI // OpenAI Responses API
wireOpenAICompat // OpenAI chat-completions API
wireAnthropic
wireGoogle
)

// Wire protocol names accepted in provider overrides (config `providers`
// section) and the --provider-wire flag. These are the user-facing,
// dependency-agnostic names for the wire protocols.
const (
WireNameOpenAI = "openai"
WireNameOpenAICompat = "openai-compat"
WireNameAnthropic = "anthropic"
WireNameGoogle = "google"
)

// parseWire maps a user-facing wire protocol name to its wireProtocol value.
// Accepts a few common aliases. Returns (wireUnknown, false) for empty or
// unrecognized names.
func parseWire(name string) (wireProtocol, bool) {
switch strings.ToLower(strings.TrimSpace(name)) {
case WireNameOpenAI, "openai-responses":
return wireOpenAI, true
case WireNameOpenAICompat, "openai-compatible", "openai-chat":
return wireOpenAICompat, true
case WireNameAnthropic:
return wireAnthropic, true
case WireNameGoogle, "gemini":
return wireGoogle, true
}
return wireUnknown, false
}

// wireNames lists the accepted canonical wire names for error messages.
func wireNames() string {
return strings.Join([]string{WireNameOpenAI, WireNameOpenAICompat, WireNameAnthropic, WireNameGoogle}, ", ")
}

// npmToWireProtocol maps npm package names from models.dev to the wire
// protocol they speak. Provider-specific bundles that need bespoke auth or
// URL templating (azure, bedrock, openrouter, google-vertex, google-vertex-
Expand All @@ -75,26 +114,26 @@ const (
var npmToWireProtocol = map[string]wireProtocol{
// Native wires.
"@ai-sdk/openai": wireOpenAI,
"@ai-sdk/openai-compatible": wireOpenAI,
"@ai-sdk/openai-compatible": wireOpenAICompat,
"@ai-sdk/anthropic": wireAnthropic,
"@ai-sdk/google": wireGoogle,

// Thin OpenAI-compatible wrappers. Each ships with a hard-coded base URL
// in its JS SDK (see sdkDefaultBaseURL) but speaks the plain OpenAI chat
// completions wire — so we can route them all through fantasy's
// openaicompat provider once we supply the URL.
"@ai-sdk/groq": wireOpenAI,
"@ai-sdk/cerebras": wireOpenAI,
"@ai-sdk/perplexity": wireOpenAI,
"@ai-sdk/togetherai": wireOpenAI,
"@ai-sdk/xai": wireOpenAI,
"@ai-sdk/deepinfra": wireOpenAI,
"@ai-sdk/mistral": wireOpenAI,
"@ai-sdk/cohere": wireOpenAI,
"@ai-sdk/vercel": wireOpenAI, // v0 API (api.v0.dev), distinct from @ai-sdk/gateway
"@aihubmix/ai-sdk-provider": wireOpenAI,
"venice-ai-sdk-provider": wireOpenAI,
"merge-gateway-ai-sdk-provider": wireOpenAI,
"@ai-sdk/groq": wireOpenAICompat,
"@ai-sdk/cerebras": wireOpenAICompat,
"@ai-sdk/perplexity": wireOpenAICompat,
"@ai-sdk/togetherai": wireOpenAICompat,
"@ai-sdk/xai": wireOpenAICompat,
"@ai-sdk/deepinfra": wireOpenAICompat,
"@ai-sdk/mistral": wireOpenAICompat,
"@ai-sdk/cohere": wireOpenAICompat,
"@ai-sdk/vercel": wireOpenAICompat, // v0 API (api.v0.dev), distinct from @ai-sdk/gateway
"@aihubmix/ai-sdk-provider": wireOpenAICompat,
"venice-ai-sdk-provider": wireOpenAICompat,
"merge-gateway-ai-sdk-provider": wireOpenAICompat,
}

// sdkDefaultBaseURL maps an npm package name to the base URL its JavaScript
Expand Down
113 changes: 113 additions & 0 deletions internal/models/provider_overrides.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package models

import (
"fmt"
"os"

"github.com/spf13/viper"
)

// ProviderOverrideConfig declares or patches a provider in the model registry
// via the `providers` config section. All fields are optional; non-empty
// fields override the corresponding registry values (which come from the
// models.dev database). A provider ID that is not in the database is
// registered fresh, so users can declare entirely new providers (e.g.
// internal LLM gateways) without touching the database:
//
// providers:
// # Patch the wire protocol of a known provider
// minimax:
// wire: anthropic
//
// # Declare a brand-new provider
// corp-llm:
// name: "Corp LLM Gateway"
// wire: anthropic
// baseUrl: https://llm.internal.corp/api
// apiKeyEnv: [CORP_LLM_KEY, LLM_GATEWAY_KEY]
// headers:
// X-Team: platform
type ProviderOverrideConfig struct {
// Name is the human-readable provider name.
Name string `json:"name,omitempty" yaml:"name,omitempty"`
// Wire is the wire protocol this provider speaks: "openai" (Responses
// API), "openai-compat" (chat completions), "anthropic", or "google".
Wire string `json:"wire,omitempty" yaml:"wire,omitempty"`
// BaseURL is the provider's API base URL.
BaseURL string `json:"baseUrl,omitempty" yaml:"baseUrl,omitempty"`
// APIKeyEnv lists environment variable names to try (in order) when
// resolving the API key.
APIKeyEnv []string `json:"apiKeyEnv,omitempty" yaml:"apiKeyEnv,omitempty"`
// Headers are default HTTP headers added to every request.
Headers map[string]string `json:"headers,omitempty" yaml:"headers,omitempty"`
}

// loadProviderOverridesFromConfig loads the `providers` config section from
// the process-global viper store (the model registry is a process-global
// singleton, mirroring loadCustomModelsFromConfig).
func loadProviderOverridesFromConfig() map[string]ProviderOverrideConfig {
return loadProviderOverridesFrom(viper.GetViper())
}

// loadProviderOverridesFrom loads provider overrides from the supplied store.
// When v is nil the process-global store is used. Returns nil when the
// `providers` key is absent or malformed.
func loadProviderOverridesFrom(v *viper.Viper) map[string]ProviderOverrideConfig {
if v == nil {
v = viper.GetViper()
}
if !v.IsSet("providers") {
return nil
}

var overrides map[string]ProviderOverrideConfig
if err := v.UnmarshalKey("providers", &overrides); err != nil {
fmt.Fprintf(os.Stderr, "Warning: failed to parse providers config: %v\n", err)
return nil
}
return overrides
}

// applyProviderOverrides merges provider overrides into the registry provider
// map. Non-empty override fields replace the registry values; unset fields
// inherit whatever the database (or special-case injection) provided. IDs not
// already present are registered as new providers with an empty model map,
// like ollama — model lookups are advisory for auto-routed providers.
func applyProviderOverrides(providers map[string]ProviderInfo, overrides map[string]ProviderOverrideConfig) {
for id, o := range overrides {
if o.Wire != "" {
if _, ok := parseWire(o.Wire); !ok {
fmt.Fprintf(os.Stderr, "Warning: provider %q has unknown wire %q (expected one of: %s); ignoring override\n",
id, o.Wire, wireNames())
continue
}
}

info, exists := providers[id]
if !exists {
info = ProviderInfo{
ID: id,
Name: id,
Models: make(map[string]ModelInfo),
}
}

if o.Name != "" {
info.Name = o.Name
}
if o.Wire != "" {
info.Wire = o.Wire
}
if o.BaseURL != "" {
info.API = o.BaseURL
}
if len(o.APIKeyEnv) > 0 {
info.Env = o.APIKeyEnv
}
if len(o.Headers) > 0 {
info.Headers = o.Headers
}

providers[id] = info
}
}
Loading
Loading