From dda40d1f685a125bc4a95c5392ed1e530fa15081 Mon Sep 17 00:00:00 2001 From: Alec Chen Date: Mon, 24 Aug 2026 23:00:26 +0800 Subject: [PATCH 1/2] fix(models): classify Responses and Gemini endpoints by prefix The Zen classifier hard-coded individual model IDs for the Responses and Gemini endpoints, so newer models (grok-*, muse-spark-*, gemini-3.7-flash, gemini-3.6-flash, gemini-3.5-flash-lite, gpt-5.6-*) fell through to the default Chat Completions endpoint and the proxy could not route them. Switch IsResponsesModel and IsGeminiModel to prefix matching so all current and future models in each family route to the correct endpoint. Correct the grok-build-0.1 test expectation (it uses the Responses endpoint) and fix the howto-add-model.md claim that adding a model requires zero code changes. Co-Authored-By: Claude Opus 4.8 --- docs/howto-add-model.md | 2 +- internal/client/opencode_test.go | 29 +++++++++++++++++++---------- internal/models/classifier.go | 22 ++++++---------------- 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/docs/howto-add-model.md b/docs/howto-add-model.md index ada87f05..3bb45cc3 100644 --- a/docs/howto-add-model.md +++ b/docs/howto-add-model.md @@ -1,6 +1,6 @@ # How to Add a New Model -Adding a new model requires zero code changes. Everything is config-driven. +Adding most new models requires only config changes. New model families that use non-default endpoints (Responses, Gemini, Messages) may require updating `internal/models/classifier.go`. ## Step 1: Identify the Provider and Endpoint 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-") } From 149d3a065155e61eb3cd63e5a9cddf1dccd58f1d Mon Sep 17 00:00:00 2001 From: TUYIZERE Samuel Date: Tue, 25 Aug 2026 06:16:58 +0200 Subject: [PATCH 2/2] docs(models): clarify endpoint family classification --- CLAUDE.md | 8 ++-- docs/howto-add-model.md | 69 +++++++++++++++---------------- internal/provider/opencode_zen.go | 3 +- 3 files changed, 39 insertions(+), 41 deletions(-) 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 3bb45cc3..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 most new models requires only config changes. New model families that use non-default endpoints (Responses, Gemini, Messages) may require updating `internal/models/classifier.go`. +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/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 }