diff --git a/CLAUDE.md b/CLAUDE.md index e7326b02..5c5e2ed0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,7 +35,7 @@ make dist # Cross-compile for all platforms **Purpose:** routatic-proxy is a proxy server that sits between Claude Code and OpenCode Go. It intercepts Anthropic API requests, transforms them to OpenAI Chat Completions format, forwards them to OpenCode Go, and transforms responses back to Anthropic SSE. -**Model routing is config-driven, not code-driven.** All models are defined in `~/.config/routatic-proxy/config.json` — adding a new model requires no code changes. Go provider models are transformed to OpenAI Chat Completions format automatically. Zen models use endpoint classification via `ClassifyEndpoint()`. The router in `internal/router/` selects models by matching request content against scenario patterns defined in `scenarios.go`. +**Model routing is config-driven for existing model families.** All models are defined in `~/.config/routatic-proxy/config.json`. Adding a Go-provider model or a Zen model whose ID matches a recognized family prefix requires only config changes. A new Zen family that uses a non-default endpoint requires updating `ClassifyEndpoint()`. Go-provider wire-format differences remain configurable through `wire_format`. The router in `internal/router/` selects models by matching request content against scenario patterns defined in `scenarios.go`. If a model's upstream doesn't support Anthropic tool format (`type: "custom"` server-tool shorthands), set `"anthropic_tools_disabled": true` in the model config to force it through the Chat Completions transform path instead of the raw Anthropic endpoint. @@ -43,8 +43,8 @@ If a model's upstream doesn't support Anthropic tool format (`type: "custom"` se - `EndpointChatCompletions` — OpenAI-compatible `/v1/chat/completions`. The default, and what most models use. - `EndpointAnthropic` — Anthropic `/v1/messages`. -- `EndpointResponses` — OpenAI native `/v1/responses`. Used by the GPT-5.x families (`IsResponsesModel`). -- `EndpointGemini` — Google `/v1/models/{id}`. Used by `gemini-3.5-flash`, `gemini-3.1-pro`, `gemini-3-flash` (`IsGeminiModel`). +- `EndpointResponses` — OpenAI native `/v1/responses`. Used by `gpt-*`, `grok-*`, and `muse-spark-*` models (`IsResponsesModel`). +- `EndpointGemini` — Google `/v1/models/{id}`. Used by `gemini-*` models (`IsGeminiModel`). Which models take the Anthropic endpoint depends on the provider: @@ -81,7 +81,7 @@ Which models take the Anthropic endpoint depends on the provider: | `qwen3.6-plus` | Go | 1M | 8192 | yes | Streaming fallback | | `qwen3.5-plus` | Go | 1M | 8192 | yes | Simple read-only ops | -The "typical provider" column reflects how the shipped config wires each model; the registry itself is provider-agnostic, so any model can be pointed at any provider in `config.json`. Zen additionally exposes many models that are not in the registry (Claude, Gemini, GPT-5.x, other free-tier models) — those get their capabilities from the catalog rather than `modelMetadata`. +The "typical provider" column reflects how the shipped config wires each model; the registry itself is provider-agnostic, so any model can be pointed at any provider in `config.json`. Zen additionally exposes many models that are not in the registry (Claude, Gemini, GPT, Grok, Muse Spark, and other free-tier models) — those get their capabilities from the catalog rather than `modelMetadata`. `internal/client/opencode.go` routes Go provider models to Chat Completions; Zen models are classified by `models.ClassifyEndpoint()` in `internal/models/classifier.go`. If a model's upstream doesn't support Anthropic tool format, set `anthropic_tools_disabled: true` in config. diff --git a/docs/howto-add-model.md b/docs/howto-add-model.md index ada87f05..2bbd550d 100644 --- a/docs/howto-add-model.md +++ b/docs/howto-add-model.md @@ -1,6 +1,8 @@ # How to Add a New Model -Adding a new model requires zero code changes. Everything is config-driven. +Adding a model from an existing family requires only config changes. A new Zen +model family that uses a non-default endpoint (Responses, Gemini, or Messages) +may require updating `internal/models/classifier.go`. ## Step 1: Identify the Provider and Endpoint @@ -12,56 +14,51 @@ Determine which upstream provider the model uses and which endpoint format it ac | `opencode-go` | `/v1/messages` | Anthropic Messages (MiniMax, Qwen) | | `opencode-zen` | `/v1/chat/completions` | OpenAI Chat Completions | | `opencode-zen` | `/v1/messages` | Anthropic Messages (Claude, Qwen) | -| `opencode-zen` | `/v1/responses` | OpenAI Responses (GPT models) | +| `opencode-zen` | `/v1/responses` | OpenAI Responses (GPT, Grok, Muse Spark) | | `opencode-zen` | `/v1/models/{id}` | Gemini | | `aws-bedrock` | `/v1/chat/completions` | OpenAI Chat Completions (Bedrock Mantle) | | `aws-bedrock` | `/v1/messages` | Anthropic Messages (Bedrock Mantle, requires `wire_format: "anthropic"`) | -## Step 2: Add Endpoint Classification (Zen only) +## Step 2: Check Endpoint Classification -If the model uses Zen, add it to the appropriate classifier in `internal/models/classifier.go`: +Zen endpoint classification is prefix-based. Models in these existing families +need no classifier change: -```go -// For Anthropic endpoint: -func IsZenAnthropicModel(modelID string) bool { - // ... - if modelID == "my-new-model" { - return true - } - // ... -} +| Endpoint | Recognized model prefixes | Classifier | +|----------|---------------------------|------------| +| Anthropic Messages | `claude-*`, `qwen*` | `IsZenAnthropicModel` | +| OpenAI Responses | `gpt-*`, `grok-*`, `muse-spark-*` | `IsResponsesModel` | +| Gemini | `gemini-*` | `IsGeminiModel` | -// For Responses endpoint: -func IsResponsesModel(modelID string) bool { - // ... - case "gpt-5.5", "gpt-5.5-pro", "my-new-model": - return true - // ... -} +Update `internal/models/classifier.go` only when Zen introduces a new model +family that uses a non-default endpoint. Add the family prefix to the +appropriate classifier and add unit-test coverage. For example, a new +Responses family would be added alongside the existing prefixes: -// For Gemini endpoint: -func IsGeminiModel(modelID string) bool { - // ... - case "gemini-3.5-flash", "my-new-model": - return true - // ... +```go +func IsResponsesModel(modelID string) bool { + return strings.HasPrefix(modelID, "gpt-") || + strings.HasPrefix(modelID, "grok-") || + strings.HasPrefix(modelID, "muse-spark-") || + strings.HasPrefix(modelID, "my-responses-family-") } ``` -If the model uses Go provider and requires the Anthropic endpoint (not Chat Completions), add it to `IsAnthropicModel`: +The Go provider is config-driven. If a Go model requires a non-default wire +format, set `wire_format` on its model configuration instead of changing the +Zen classifier: -```go -func IsAnthropicModel(modelID string) bool { - switch modelID { - // ... - case "minimax-m2.5", "my-new-model": - return true - // ... - } +```json +{ + "provider": "opencode-go", + "model_id": "my-new-model", + "wire_format": "anthropic" } ``` -**Note:** These classification functions are shared between `internal/client` and `internal/provider` packages to ensure consistent routing. +Supported Go-provider overrides are `openai`, `anthropic`, and `responses`. +Zen classification functions are shared between `internal/client` and +`internal/provider` so both paths route models consistently. ## Step 3: Add to Config diff --git a/internal/client/opencode_test.go b/internal/client/opencode_test.go index 77d1f132..57e94d01 100644 --- a/internal/client/opencode_test.go +++ b/internal/client/opencode_test.go @@ -320,9 +320,9 @@ func TestClassifyEndpoint(t *testing.T) { expected: EndpointChatCompletions, }, { - name: "grok-build-0.1 uses chat completions endpoint", + name: "grok-build-0.1 uses responses endpoint", modelID: "grok-build-0.1", - expected: EndpointChatCompletions, + expected: EndpointResponses, }, { name: "big-pickle uses chat completions endpoint", @@ -375,10 +375,13 @@ func TestIsGeminiModel(t *testing.T) { modelID string want bool }{ - // Gemini models + // Gemini models (prefix check) {"gemini-3.5-flash", true}, {"gemini-3.1-pro", true}, {"gemini-3-flash", true}, + {"gemini-3.7-flash", true}, + {"gemini-3.6-flash", true}, + {"gemini-3.5-flash-lite", true}, // Non-Gemini models {"kimi-k2.6", false}, {"kimi-k2.7-code", false}, @@ -407,32 +410,38 @@ func TestIsResponsesModel(t *testing.T) { modelID string want bool }{ - // GPT 5.5 series + // GPT models (prefix check) {"gpt-5.5", true}, {"gpt-5.5-pro", true}, {"gpt-5.5-mini", true}, {"gpt-5.5-nano", true}, - // GPT 5.4 series {"gpt-5.4", true}, {"gpt-5.4-pro", true}, {"gpt-5.4-mini", true}, {"gpt-5.4-nano", true}, - // GPT 5.3 series {"gpt-5.3-codex", true}, {"gpt-5.3-codex-spark", true}, - // GPT 5.2 series {"gpt-5.2", true}, {"gpt-5.2-codex", true}, - // GPT 5.1 series {"gpt-5.1", true}, {"gpt-5.1-codex", true}, {"gpt-5.1-codex-max", true}, {"gpt-5.1-codex-mini", true}, - // GPT 5 series {"gpt-5", true}, {"gpt-5-codex", true}, {"gpt-5-nano", true}, - // Non-GPT models + {"gpt-5.6-sol", true}, + {"gpt-5.6-terra", true}, + {"gpt-5.6-luna", true}, + // Grok models (prefix check) + {"grok-4.5", true}, + {"grok-4.6", true}, + {"grok-build-0.1", true}, + // Muse Spark models (prefix check) + {"muse-spark-1.2", true}, + {"muse-spark-1.2-contributor", true}, + {"muse-spark-1.2-contributor-free", true}, + // Non-Responses models {"kimi-k2.6", false}, {"kimi-k2.7-code", false}, {"glm-5.1", false}, diff --git a/internal/models/classifier.go b/internal/models/classifier.go index 3f2d276c..3c0cca80 100644 --- a/internal/models/classifier.go +++ b/internal/models/classifier.go @@ -66,25 +66,15 @@ func IsZenAnthropicModel(modelID string) bool { } // IsGeminiModel returns true for models using the Gemini endpoint. +// Uses prefix check to cover all current and future Gemini models. func IsGeminiModel(modelID string) bool { - switch modelID { - case "gemini-3.5-flash", "gemini-3.1-pro", "gemini-3-flash": - return true - default: - return false - } + return strings.HasPrefix(modelID, "gemini-") } // IsResponsesModel returns true for models using the OpenAI Responses endpoint. +// Uses prefix checks to cover all current and future model families. func IsResponsesModel(modelID string) bool { - switch modelID { - case "gpt-5.5", "gpt-5.5-pro", "gpt-5.5-mini", "gpt-5.5-nano", - "gpt-5.4", "gpt-5.4-pro", "gpt-5.4-mini", "gpt-5.4-nano", - "gpt-5.3-codex", "gpt-5.3-codex-spark", "gpt-5.2", "gpt-5.2-codex", - "gpt-5.1", "gpt-5.1-codex", "gpt-5.1-codex-max", "gpt-5.1-codex-mini", - "gpt-5", "gpt-5-codex", "gpt-5-nano": - return true - default: - return false - } + return strings.HasPrefix(modelID, "gpt-") || + strings.HasPrefix(modelID, "grok-") || + strings.HasPrefix(modelID, "muse-spark-") } diff --git a/internal/provider/opencode_zen.go b/internal/provider/opencode_zen.go index 4c2caee9..357ee427 100644 --- a/internal/provider/opencode_zen.go +++ b/internal/provider/opencode_zen.go @@ -24,7 +24,8 @@ const upstreamUserAgent = "opencode/routatic-proxy" // OpenCodeZenProvider implements core.Provider for the OpenCode Zen backend. // Zen supports four wire formats determined by model ID: Anthropic (Claude, -// Qwen), Responses (GPT), Gemini, and Chat Completions (everything else). +// Qwen), Responses (GPT, Grok, Muse Spark), Gemini, and Chat Completions +// (everything else). type OpenCodeZenProvider struct { baseProvider }