Skip to content
Open
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
64 changes: 64 additions & 0 deletions dev-notes/opencode-go-support.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# OpenCode Go & Zen Free Models Capabilities

| Model Name | Display Name | Base URL | API | Thinking | Default Level | Thinking Modes | API Parameter / Payload Mapped | Context Window | Max Output / Reasoning Tokens | Additional Details |
|---|---|---|---|---|---|---|---|---|---|---|
| **Go Models** | | | | | | | | | | |
| `deepseek-v4-pro` | DeepSeek V4 Pro | `https://opencode.ai/zen/go/v1` | OpenAI | Always on | `high` | `high`, `xhigh` (max) | `reasoning_effort` | 1,000,000 | 384,000 | Effort values: `"high"`, `"max"`. Inherits from `deepseek/deepseek-v4-pro`. |
| `deepseek-v4-flash` | DeepSeek V4 Flash | `https://opencode.ai/zen/go/v1` | OpenAI | Always on | `high` | `high`, `xhigh` (max) | `reasoning_effort` | 1,000,000 | 384,000 | Effort values: `"high"`, `"max"`. Inherits from `deepseek/deepseek-v4-flash`. |
| `glm-5.2` | GLM 5.2 | `https://opencode.ai/zen/go/v1` | OpenAI | Always on | `high` | `high`, `xhigh` (max) | `reasoning_effort` | 1,000,000 | 131,072 | Effort values: `"high"`, `"max"`. Inherits from `zhipuai/glm-5.2`. |
| `glm-5.1` | GLM 5.1 | `https://opencode.ai/zen/go/v1` | OpenAI | Always on | N/A | None | None | 202,752 | 32,768 | Always-on reasoning. No reasoning options defined in TOML. |
| `grok-4.5` | Grok 4.5 | `https://opencode.ai/zen/go/v1` | OpenAI | Toggleable | `medium` | `off` (none), `low`, `medium`, `high` | `reasoning_effort` | 1,000,000 | — | Tiered pricing: 200K+ tokens cost 2x. Inherits from `xai/grok-4.5`. |
| `kimi-k2.7-code` | Kimi K2.7 Code | `https://opencode.ai/zen/go/v1` | OpenAI | Always on | N/A | None | None | 262,144 | 262,144 | Always-on reasoning. Resolved from compiled models.dev catalog. |
| `kimi-k2.6` | Kimi K2.6 | `https://opencode.ai/zen/go/v1` | OpenAI | Always on | N/A | None | None | 262,144 | 65,536 | Always-on reasoning. No reasoning options defined in TOML. |
| `kimi-k3` | Kimi K3 | `https://opencode.ai/zen/go/v1` | OpenAI | Toggleable | `xhigh` | `xhigh` (max) | `reasoning_effort` | 262,144 | — | Effort value: `"max"` only. Inherits from `moonshotai/kimi-k3`. 2x usage pricing. |
| `mimo-v2.5-pro` | MiMo V2.5 Pro | `https://opencode.ai/zen/go/v1` | OpenAI | Always on | N/A | None | None | 1,048,576 | 128,000 | Always-on reasoning. No reasoning options defined in TOML. |
| `mimo-v2.5` | MiMo V2.5 | `https://opencode.ai/zen/go/v1` | OpenAI | Always on | N/A | None | None | 1,000,000 | 128,000 | Always-on reasoning. No reasoning options defined in TOML. |
| `minimax-m3` | MiniMax M3 | `https://opencode.ai/zen/go/v1` | Anthropic | Toggleable | `high` | `off`, `high` (on) | `thinking` type (`adaptive` / `disabled`) | 1,000,000 | 131,072 | Mapped toggle: `high` -> `"adaptive"`. Inherits from `minimax/MiniMax-M3`. |
| `minimax-m2.7` | MiniMax M2.7 | `https://opencode.ai/zen/go/v1` | Anthropic | Always on | N/A | None | None | 204,800 | 131,072 | Always-on reasoning. No reasoning options defined in TOML. |
| `qwen3.7-max` | Qwen 3.7 Max | `https://opencode.ai/zen/go/v1` | Anthropic | Toggleable | `high` | `off` (disabled), `low` (2048), `medium` (4096), `high` (8192), `xhigh` (16384) | `output_config.task_budget` | 1,000,000 | 65,536 | Supports `toggle` and `budget_tokens` (max 262,144 reasoning tokens). |
| `qwen3.7-plus` | Qwen 3.7 Plus | `https://opencode.ai/zen/go/v1` | Anthropic | Toggleable | `high` | `off` (disabled), `low` (2048), `medium` (4096), `high` (8192), `xhigh` (16384) | `output_config.task_budget` | 1,000,000 | 65,536 | Supports `toggle` and `budget_tokens` (max 262,144 reasoning tokens). |
| `qwen3.6-plus` | Qwen 3.6 Plus | `https://opencode.ai/zen/go/v1` | Anthropic | Toggleable | `high` | `off` (disabled), `low` (2048), `medium` (4096), `high` (8192), `xhigh` (8192/16384) | `output_config.task_budget` | 1,000,000 | 65,536 | Supports `toggle` and `budget_tokens` (max 81,920 reasoning tokens). |
| **Zen Free Models** | | | | | | | | | | |
| `big-pickle` | Big Pickle | `https://opencode.ai/zen/v1` | OpenAI | Always on | N/A | None | None | 200,000 | 32,000 | Always-on reasoning. Routed to Zen v1. |
| `deepseek-v4-flash-free` | DeepSeek V4 Flash Free | `https://opencode.ai/zen/v1` | OpenAI | Toggleable | `high` | `off`, `high`, `xhigh` (max) | `reasoning_effort` | 200,000 | 128,000 | Mapped to Zen v1. Effort values: `"high"`, `"max"`. |
| `hy3-free` | Hy3 Free | `https://opencode.ai/zen/v1` | OpenAI | Always on | N/A | None | None | 256,000 | 64,000 | Always-on reasoning. Routed to Zen v1. |
| `mimo-v2.5-free` | MiMo V2.5 Free | `https://opencode.ai/zen/v1` | OpenAI | Always on | N/A | None | None | 200,000 | 32,000 | Always-on reasoning. Routed to Zen v1. |
| `nemotron-3-ultra-free` | Nemotron 3 Ultra Free | `https://opencode.ai/zen/v1` | OpenAI | Always on | N/A | None | None | 1,000,000 | 128,000 | Always-on reasoning. Routed to Zen v1. |
| `north-mini-code-free` | North Mini Code Free | `https://opencode.ai/zen/v1` | OpenAI | Toggleable | `high` | `off` (none), `high` | `reasoning_effort` | 256,000 | 64,000 | Inherits from `cohere/north-mini-code-1-0`. Effort values: `"none"`, `"high"`. |

## Sources Used

- `anomalyco/models.dev` repository on GitHub (`dev` branch):
- [opencode-go/models/](https://github.com/anomalyco/models.dev/tree/dev/providers/opencode-go/models)
- [opencode/models/](https://github.com/anomalyco/models.dev/tree/dev/providers/opencode/models)
- Compiled [models.json](https://models.dev/models.json) (compiled models.dev catalog registry)
- `anomalyco/opencode` repository on GitHub (`dev` branch):
- [opencode.ai/zen/go/v1](https://github.com/anomalyco/opencode/tree/dev)

## Gaps & Upstream Capabilities Limitations

The following capabilities of OpenCode Go / Zen Free models are currently unsupported in upstream Tau and require local implementations:

### 1. Hybrid Protocols per Provider (Mixed OpenAI & Anthropic API models)
- **Problem**: Upstream Tau strictly assumes a provider serves models via a single client/API protocol class (either `openai-compatible` or `anthropic`/Messages API). Under a unified aggregator like `opencode-go`, some models use the OpenAI `/chat/completions` schema, while others (like Qwen and Minimax) are served via the Anthropic `/messages` API.
- **Solution**: Check model-specific metadata configuration `api = "anthropic-messages"` inside `create_model_provider` and dynamically morph the configuration class into `AnthropicProviderConfig` at runtime.

### 2. Toggle-Only Anthropic Thinking (MiniMax M3)
- **Problem**: Upstream's Anthropic provider handles thinking via either `budget` mode (which expects an integer token count) or `adaptive` mode (which always appends `output_config.effort` to the request payload). Toggle-only models like `minimax-m3` reject the `effort` config payload and require a pure `{ "thinking": { "type": "adaptive" } }` payload.
- **Solution**: Introduce the `thinking_type: Literal["adaptive", "disabled"]` runtime parameter to bypass the `output_config` payload and send a pure toggle to the endpoint.

### 3. Model-Specific Thinking Level Display Labels
- **Problem**: Provider-level thinking levels are mapped to standard canonical levels (like `high`, `xhigh`). Some models require mapping these canonical levels to custom UI display labels (e.g., mapping `high` to `"on"` or `"none"` in the CLI/TUI). Upstream does not support model-specific display label overrides.
- **Solution**: Implement `thinking_level_labels` mapping on the model catalog metadata to translate canonical levels to display strings for CLI/TUI widgets.

### 4. Always-On Reasoning Models
- **Problem**: Upstream has no native way to declare always-on reasoning models (models where reasoning cannot be toggled off or customized, but thinking parameters must NOT be sent in the API payload to prevent errors). In upstream, excluding a model from the provider's `thinking_models` list disables its reasoning display entirely (rendering it as `"unavailable"` in the UI).
- **Solution**: Set `reasoning = true` on the model metadata and exclude the model from the provider's `thinking_models` list. Our implementation detects these models by verifying that `reasoning` is `True` while supported `levels` is empty `()`. The TUI and CLI then format these models with the static `"always on"` label and disable custom level adjustments.

### 5. Anthropic-Protocol Model Routing (minimax-m3, minimax-m2.7, qwen3.7-max, qwen3.7-plus, qwen3.6-plus)
- **Problem**: These models use `@ai-sdk/anthropic` but were routed through the OpenAI-compatible endpoint because the catalog did not specify `api = "anthropic-messages"` in model_metadata.
- **Solution**: Added `api = "anthropic-messages"` and `thinking_parameter = "anthropic.thinking"` to model_metadata for each Anthropic-protocol model. The runtime now correctly morphs to `AnthropicProvider` with model-specific thinking configuration (budget tokens for Qwen, adaptive toggle for MiniMax M3, no controls for always-on models).

### 6. Model-Specific Thinking Parameter Override
- **Problem**: Provider-level `thinking_parameter` cannot describe mixed-transport providers where some models use `reasoning_effort` (OpenAI) and others use `anthropic.thinking` (Anthropic).
- **Solution**: Added `thinking_parameter` field to `ProviderModelMetadata` and `ModelCatalogMetadata`. Model-specific values override the provider-level default when constructing runtime configs.
7 changes: 6 additions & 1 deletion src/tau_ai/anthropic.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,9 +362,14 @@ def _build_messages_payload(
),
"messages": [_anthropic_message(message) for message in messages],
}
if thinking_mode == "adaptive" and thinking_effort is not None:
if thinking_mode == "disabled":
payload["thinking"] = {"type": "disabled"}
elif thinking_mode == "adaptive" and thinking_effort is not None:
payload["thinking"] = {"type": "adaptive", "display": "summarized"}
payload["output_config"] = {"effort": thinking_effort}
elif thinking_mode == "adaptive":
# Toggle-only model with thinking enabled (no effort field)
payload["thinking"] = {"type": "adaptive", "display": "summarized"}
elif thinking_budget_tokens is not None:
payload["thinking"] = {
"type": "enabled",
Expand Down
8 changes: 8 additions & 0 deletions src/tau_coding/catalog_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ class _CatalogModelMetadata(BaseModel):
headers: dict[_NonEmptyString, _NonEmptyString] = {}
compat: dict[_NonEmptyString, Any] = {}
thinking_level_map: dict[ThinkingLevel, _NonEmptyString] = {}
thinking_level_labels: dict[ThinkingLevel, _NonEmptyString] = {}
unsupported_thinking_levels: tuple[ThinkingLevel, ...] = ()
thinking_parameter: ThinkingParameter | None = None


class _CatalogProvider(BaseModel):
Expand Down Expand Up @@ -396,6 +398,8 @@ def _model_metadata_from_provider(metadata: _CatalogModelMetadata) -> ModelCatal
headers=dict(metadata.headers),
compat=_json_object(metadata.compat, "model_metadata.compat"),
thinking_level_map=thinking_level_map,
thinking_level_labels=dict(metadata.thinking_level_labels),
thinking_parameter=metadata.thinking_parameter,
)


Expand Down Expand Up @@ -523,6 +527,10 @@ def _raw_model_metadata_from_entry(metadata: ModelCatalogMetadata) -> dict[str,
raw["thinking_level_map"] = thinking_level_map
if unsupported:
raw["unsupported_thinking_levels"] = unsupported
if metadata.thinking_level_labels:
raw["thinking_level_labels"] = dict(metadata.thinking_level_labels)
if metadata.thinking_parameter is not None:
raw["thinking_parameter"] = metadata.thinking_parameter
return raw


Expand Down
13 changes: 12 additions & 1 deletion src/tau_coding/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ def context_window_tokens(self) -> int: ...
@property
def thinking_level(self) -> str: ...

@property
def thinking_level_label(self) -> str: ...

@property
def available_thinking_levels(self) -> Sequence[str]: ...

Expand Down Expand Up @@ -629,6 +632,12 @@ def _thinking_command(context: CommandContext) -> CommandResult:
return CommandResult(handled=True, message="\n".join(lines))

if not available:
always_on = getattr(session, "thinking_is_always_on", False)
if always_on:
return CommandResult(
handled=True,
message=f"Thinking mode is always on for {session.provider_name}:{session.model}",
)
message = f"Thinking controls are unavailable for {session.provider_name}:{session.model}"
reason = _thinking_unavailable_reason(session)
if reason:
Expand Down Expand Up @@ -656,7 +665,9 @@ def _thinking_command(context: CommandContext) -> CommandResult:

def _thinking_status_lines(session: CommandSession) -> list[str]:
if tuple(session.available_thinking_levels):
return [f"Thinking mode: {session.thinking_level}"]
return [f"Thinking mode: {session.thinking_level_label}"]
if getattr(session, "thinking_is_always_on", False):
return ["Thinking mode: always on"]
lines = ["Thinking mode: unavailable"]
reason = _thinking_unavailable_reason(session)
if reason:
Expand Down
165 changes: 153 additions & 12 deletions src/tau_coding/data/catalog.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6268,28 +6268,169 @@ kind = "openai-compatible"
base_url = "https://opencode.ai/zen/go/v1"
api_key_env = "OPENCODE_API_KEY"
credential_name = "opencode-go"
models = ["deepseek-v4-flash", "deepseek-v4-pro", "glm-5.1", "glm-5.2", "kimi-k2.6", "kimi-k2.7-code", "mimo-v2.5", "mimo-v2.5-pro", "minimax-m2.7", "minimax-m3", "qwen3.6-plus", "qwen3.7-max", "qwen3.7-plus"]
default_model = "kimi-k2.7-code"
models = [
"deepseek-v4-pro",
"deepseek-v4-flash",
"glm-5.2",
"glm-5.1",
"grok-4.5",
"kimi-k2.7-code",
"kimi-k2.6",
"kimi-k3",
"minimax-m3",
"minimax-m2.7",
"mimo-v2.5-pro",
"mimo-v2.5",
"qwen3.7-max",
"qwen3.7-plus",
"qwen3.6-plus",
"big-pickle",
"deepseek-v4-flash-free",
"hy3-free",
"mimo-v2.5-free",
"nemotron-3-ultra-free",
"north-mini-code-free",
]
default_model = "deepseek-v4-pro"
docs_url = "https://opencode.ai/docs/go"
api = "openai-completions"
thinking_levels = ["off", "minimal", "low", "medium", "high", "xhigh"]
thinking_levels = ["off", "low", "medium", "high", "xhigh"]
thinking_models = [
"deepseek-v4-pro",
"deepseek-v4-flash",
"glm-5.2",
"grok-4.5",
"kimi-k3",
"minimax-m3",
"qwen3.7-max",
"qwen3.7-plus",
"qwen3.6-plus",
"deepseek-v4-flash-free",
"north-mini-code-free",
]
thinking_default = "medium"
thinking_parameter = "reasoning_effort"

[providers.context_windows]
"deepseek-v4-flash" = 1000000
"deepseek-v4-pro" = 1000000
"glm-5.2" = 1000000
"glm-5.1" = 202752
"glm-5.2" = 202752
"kimi-k2.6" = 262144
"kimi-k2.7-code" = 262144
"mimo-v2.5" = 262144
"mimo-v2.5-pro" = 262144
"kimi-k2.6" = 262144
"deepseek-v4-pro" = 1000000
"deepseek-v4-flash" = 1000000
"mimo-v2.5" = 1000000
"mimo-v2.5-pro" = 1048576
"minimax-m3" = 1000000
"minimax-m2.7" = 204800
"minimax-m3" = 204800
"qwen3.6-plus" = 1000000
"qwen3.7-max" = 1000000
"qwen3.7-plus" = 1000000
"qwen3.6-plus" = 1000000
"deepseek-v4-flash-free" = 200000
"mimo-v2.5-free" = 200000
"big-pickle" = 200000
"north-mini-code-free" = 256000
"nemotron-3-ultra-free" = 1000000
"hy3-free" = 256000
"kimi-k3" = 262144
"grok-4.5" = 1000000

[providers.model_metadata."deepseek-v4-flash-free"]
base_url = "https://opencode.ai/zen/v1"
thinking_level_map = {high = "high", xhigh = "max"}
thinking_level_labels = {xhigh = "max"}
unsupported_thinking_levels = ["minimal", "low", "medium"]

[providers.model_metadata."mimo-v2.5-free"]
base_url = "https://opencode.ai/zen/v1"
reasoning = true

[providers.model_metadata."big-pickle"]
base_url = "https://opencode.ai/zen/v1"
reasoning = true

[providers.model_metadata."north-mini-code-free"]
base_url = "https://opencode.ai/zen/v1"
thinking_level_map = {off = "none", high = "high"}
unsupported_thinking_levels = ["minimal", "low", "medium", "xhigh"]

[providers.model_metadata."nemotron-3-ultra-free"]
base_url = "https://opencode.ai/zen/v1"
reasoning = true

[providers.model_metadata."hy3-free"]
base_url = "https://opencode.ai/zen/v1"
reasoning = true

[providers.model_metadata."glm-5.2"]
thinking_level_map = {high = "high", xhigh = "max"}
thinking_level_labels = {xhigh = "max"}
unsupported_thinking_levels = ["off", "minimal", "low", "medium"]

[providers.model_metadata."glm-5.1"]
reasoning = true

[providers.model_metadata."kimi-k2.7-code"]
reasoning = true

[providers.model_metadata."kimi-k2.6"]
reasoning = true

[providers.model_metadata."deepseek-v4-pro"]
thinking_level_map = {high = "high", xhigh = "max"}
thinking_level_labels = {xhigh = "max"}
unsupported_thinking_levels = ["off", "minimal", "low", "medium"]

[providers.model_metadata."deepseek-v4-flash"]
thinking_level_map = {high = "high", xhigh = "max"}
thinking_level_labels = {xhigh = "max"}
unsupported_thinking_levels = ["off", "minimal", "low", "medium"]

[providers.model_metadata."kimi-k3"]
thinking_level_map = {xhigh = "max"}
thinking_level_labels = {xhigh = "max"}
unsupported_thinking_levels = ["off", "minimal", "low", "medium", "high"]

[providers.model_metadata."grok-4.5"]
thinking_level_map = {off = "none", low = "low", medium = "medium", high = "high"}
thinking_level_labels = {}
unsupported_thinking_levels = ["minimal", "xhigh"]

[providers.model_metadata."minimax-m3"]
api = "anthropic-messages"
reasoning = true
thinking_level_map = {off = "disabled", high = "adaptive"}
thinking_level_labels = {off = "off", high = "on"}
unsupported_thinking_levels = ["minimal", "low", "medium", "xhigh"]
compat = {forceAdaptiveThinking = true}
thinking_parameter = "anthropic.thinking"

[providers.model_metadata."minimax-m2.7"]
api = "anthropic-messages"
reasoning = true
unsupported_thinking_levels = ["off", "minimal", "low", "medium", "high", "xhigh"]

[providers.model_metadata."qwen3.7-max"]
api = "anthropic-messages"
reasoning = true
thinking_level_map = {off = "disabled", low = "low", medium = "medium", high = "high", xhigh = "xhigh"}
thinking_level_labels = {}
unsupported_thinking_levels = ["minimal"]
thinking_parameter = "anthropic.thinking"

[providers.model_metadata."qwen3.7-plus"]
api = "anthropic-messages"
reasoning = true
thinking_level_map = {off = "disabled", low = "low", medium = "medium", high = "high", xhigh = "xhigh"}
thinking_level_labels = {}
unsupported_thinking_levels = ["minimal"]
thinking_parameter = "anthropic.thinking"

[providers.model_metadata."qwen3.6-plus"]
api = "anthropic-messages"
reasoning = true
thinking_level_map = {off = "disabled", low = "low", medium = "medium", high = "high", xhigh = "xhigh"}
thinking_level_labels = {}
unsupported_thinking_levels = ["minimal"]
thinking_parameter = "anthropic.thinking"

[[providers]]
name = "opencode"
Expand Down
2 changes: 2 additions & 0 deletions src/tau_coding/provider_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ class ModelCatalogMetadata:
headers: dict[str, str] = field(default_factory=dict)
compat: dict[str, JSONValue] = field(default_factory=dict)
thinking_level_map: ThinkingLevelMap = field(default_factory=dict)
thinking_level_labels: dict[ThinkingLevel, str] = field(default_factory=dict)
thinking_parameter: ThinkingParameter | None = None


def model_cost_for_input_tokens(
Expand Down
Loading