feat(opencode): add opencode_go and opencode_zen first-class providers - #37103
feat(opencode): add opencode_go and opencode_zen first-class providers#37103streber42 wants to merge 1 commit into
Conversation
Greptile SummaryAdds first-class OpenCode Go and Zen providers across Chat Completions, Anthropic Messages, and OpenAI Responses wire formats
Confidence Score: 3/5This PR is not safe to merge until OpenCode-specific credentials take precedence over the process-wide API key Mixed-provider processes can send an unrelated global credential to the OpenCode gateway because the new dispatch resolves litellm.api_key before every OpenCode-specific key source Files Needing Attention: litellm/main.py, litellm/llms/opencode/chat/messages_transformation.py, docs/adr/0003-opencode-polyglot-provider.md
|
| Filename | Overview |
|---|---|
| litellm/main.py | Adds OpenCode polyglot dispatch, but key precedence can expose an unrelated global credential and provider-specific logic is placed in the shared entrypoint |
| litellm/llms/opencode/chat/messages_transformation.py | Implements Anthropic Messages routing and authentication, but hard-codes model routing classifications that belong in model metadata |
| litellm/llms/opencode/chat/transformation.py | Adds the shared-handler OpenAI Chat Completions transformation with surface-aware URLs and credentials |
| litellm/llms/opencode/go/responses/transformation.py | Adds the Go Responses API configuration and provider-specific credential resolution |
| litellm/llms/opencode/zen/responses/transformation.py | Adds the Zen Responses API configuration and provider-specific credential resolution |
| litellm/llms/base_llm/anthropic_messages/transformation.py | Extends the shared Anthropic Messages config interface with developer-role and OpenAI-parameter mapping behavior |
| model_prices_and_context_window.json | Registers the OpenCode model catalogs, pricing, capabilities, and Responses mode metadata |
| docs/adr/0003-opencode-polyglot-provider.md | Documents the polyglot provider design in this repository despite the requirement to keep documentation in litellm-docs |
| ui/litellm-dashboard/src/components/add_model/AddModelForm.tsx | Adds dashboard credential placeholders and provider handling for both OpenCode surfaces |
Reviews (1): Last reviewed commit: "feat(opencode): add opencode_go and open..." | Re-trigger Greptile
| api_key = ( # rebind-ok: resolve key from module/env fallbacks | ||
| api_key | ||
| or litellm.api_key | ||
| or getattr(litellm, f"opencode_{surface}_api_key", None) | ||
| or get_secret_str(f"OPENCODE_{surface_upper}_API_KEY") | ||
| or get_secret_str("OPENCODE_API_KEY") | ||
| ) |
There was a problem hiding this comment.
Global key overrides provider key
If litellm.api_key and an OpenCode-specific key are configured, the global key is sent to OpenCode, exposing it and failing authentication.
How this was verified: The selected key is inserted into the outbound Bearer header before the shared HTTP handler is called
| api_key = ( # rebind-ok: resolve key from module/env fallbacks | |
| api_key | |
| or litellm.api_key | |
| or getattr(litellm, f"opencode_{surface}_api_key", None) | |
| or get_secret_str(f"OPENCODE_{surface_upper}_API_KEY") | |
| or get_secret_str("OPENCODE_API_KEY") | |
| ) | |
| api_key = ( # rebind-ok: resolve key from module/env fallbacks | |
| api_key | |
| or getattr(litellm, f"opencode_{surface}_api_key", None) | |
| or get_secret_str(f"OPENCODE_{surface_upper}_API_KEY") | |
| or get_secret_str("OPENCODE_API_KEY") | |
| or litellm.api_key | |
| ) |
Knowledge Base Used: LLM Provider Adapters
There was a problem hiding this comment.
Checked against the current head and could not reproduce. resolve_opencode_api_key (litellm/llms/opencode/common_utils.py:138) already resolves most-specific-first, with the process-wide litellm.api_key last: explicit arg, litellm.opencode_{surface}_api_key, OPENCODE_{SURFACE}_API_KEY, litellm.opencode_api_key, OPENCODE_API_KEY, then litellm.api_key. The responses arm uses the same ordering (litellm/llms/opencode/zen/responses/transformation.py:56-61), so the suggested patch matches existing behaviour minus the surface module-var step.
Verified end-to-end rather than by reading: with litellm.api_key set to an unrelated credential and litellm.opencode_zen_api_key configured, the chat arm sends Authorization: Bearer sk-opencode-specific and the messages arm sends x-api-key: sk-opencode-specific — the global never reaches opencode.ai.
Since no test pinned that ordering, 43f9f1d adds three regression guards for exactly this scenario: one at config level, plus end-to-end header assertions on the chat (main.py Bearer construction) and messages arms.
| # Source: models.dev ``npm == @ai-sdk/anthropic`` classification. | ||
|
|
||
| OPENCODE_ZEN_MESSAGES_MODELS: Final = frozenset( | ||
| { | ||
| "claude-fable-5", | ||
| "claude-haiku-4-5", | ||
| "claude-opus-4-5", | ||
| "claude-opus-4-6", | ||
| "claude-opus-4-7", | ||
| "claude-opus-4-8", | ||
| "claude-opus-5", | ||
| "claude-sonnet-4", | ||
| "claude-sonnet-4-5", | ||
| "claude-sonnet-4-6", | ||
| "claude-sonnet-5", | ||
| "qwen3.5-plus", | ||
| "qwen3.6-plus", | ||
| } | ||
| ) | ||
|
|
||
| OPENCODE_GO_MESSAGES_MODELS: Final = frozenset( | ||
| { | ||
| "minimax-m2.5", | ||
| "minimax-m2.7", | ||
| "minimax-m3", | ||
| "qwen3.5-plus", | ||
| "qwen3.6-plus", | ||
| "qwen3.7-max", | ||
| "qwen3.7-plus", | ||
| "qwen3.8-max", | ||
| } | ||
| ) |
There was a problem hiding this comment.
Hard-coded wire-format classification
Model allowlists and name parsing require code releases for catalog changes, while stale entries route OpenCode models to the wrong wire endpoint
Rule Used: What: Do not hardcode model-specific flags in the ... (source)
Knowledge Base Used: LLM Provider Adapters
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
This is deliberate, and the reasoning is in the module comment at messages_transformation.py:36-42.
Routing cannot key off the cost map's mode because litellm.model_cost is fetched from the published remote map at import: on any install whose map predates this provider, every model below would resolve to the chat arm — the wrong wire format — and the failure is silent. Live verification made the stakes concrete: the Go gateway's chat arm is a translating facade that answers any model on either wire with a 200, so a 200 from the chat endpoint is not evidence the model is chat-native. Only a messages-arm request returning an Anthropic-shaped body proves the arm. A cost-map-driven classifier would therefore be both stale-prone and unverifiable at runtime.
The forward-looking grid you're seeing (qwen3.{5..8}-{tier}) exists so a gateway-side model addition keeps routing correctly without waiting on a LiteLLM release; names with no published price simply stay unpriced rather than getting a guessed one.
Happy to discuss moving the sets to provider metadata if there's a mechanism that's guaranteed present before first request, but I'd argue against the cost map specifically.
There was a problem hiding this comment.
That context addresses the concern. I agree that the cost map is not a reliable routing source here: it can be stale at import time, and the chat facade's successful response cannot prove that the model belongs on the chat wire format. Since the messages/chat choice must be made before the request and the gateway-side additions are covered by the forward-looking model sets, keeping this classification in provider metadata is the safer approach. I’m withdrawing the finding; no change needed.
| @@ -0,0 +1,103 @@ | |||
| # 0003 — OpenCode Go / Zen: one provider prefix, three wire formats | |||
There was a problem hiding this comment.
Documentation added in wrong repository
This new architecture document splits provider documentation across repositories and bypasses the required litellm-docs ownership and publishing workflow
Rule Used: Prevent documentation from being added - needs to ... (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Intentional split, not an oversight. This repo carries the ADR because it records an implementation decision about code that lives here (dispatch shape, why classification is in code) — the same category as ARCHITECTURE.md in the root. User-facing provider documentation does follow the litellm-docs workflow: the five OPENCODE_* environment variables are documented in BerriAI/litellm-docs#908, which is also what the currently-failing documentation_test_env_keys check is waiting on.
No provider docs pages can land in this checkout regardless — it has no docs/my-website tree at all (the documentation_test_env_keys failure exists for exactly that reason). If maintainers would rather the ADR content live somewhere specific, happy to move it.
There was a problem hiding this comment.
That distinction is valid. This ADR documents an in-repository implementation decision, not user-facing provider documentation, and the absence of a docs/my-website tree means the customer-facing documentation belongs in litellm-docs as described. I’m withdrawing the wrong-repository finding for docs/adr/0003-opencode-polyglot-provider.md.
Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.
| def _complete_opencode( | ||
| ctx: _CompletionDispatchContext, | ||
| ) -> _CompletionDispatchResult: |
There was a problem hiding this comment.
Provider logic leaks into dispatch
OpenCode authentication, URL normalization, and format selection belong under litellm/llms/; placing them here couples provider changes to the shared request funnel
Rule Used: What: Avoid writing provider-specific code outside... (source)
Knowledge Base Used: LLM Provider Adapters
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
This matches the house pattern rather than introducing new coupling: every provider reaches the dispatch chain the same way (_complete_groq, _complete_xai, _complete_minimax, ... each resolve their own key/base inline in main.py). What the opencode branch adds beyond that baseline is a three-line call into litellm/llms/opencode/common_utils.py plus the arm selection itself — the URL normalization and format-selection logic all live under litellm/llms/opencode/ (resolve_opencode_api_base, is_messages_model, the three transformation modules). The dispatch branch is the selection point, which is what the branch is for.
Consolidating further would mean changing the dispatch contract for every provider, which feels out of scope here.
There was a problem hiding this comment.
That’s fair. Re-evaluating this against the existing dispatch helpers, _complete_opencode follows the established provider pattern: the dispatch function selects the provider/surface and invokes the shared handler contract, while URL normalization and wire-format behavior live under litellm/llms/opencode/. Refactoring that contract across all providers would be out of scope here. I’m withdrawing this comment; no change is requested.
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
6e00597 to
58b27a6
Compare
58b27a6 to
35554ea
Compare
PR overviewAll previously flagged issues have been addressed. No open security concerns remain on this pull request. Security reviewNo open security issues remain on this pull request. Fixed/addressed: 22 · PR risk: 0/10 |
35554ea to
e464d3e
Compare
ede15ea to
a3bd79d
Compare
5a3f412 to
c0bbad5
Compare
a3fbf79 to
74b9b32
Compare
74b9b32 to
36d6e61
Compare
36d6e61 to
17909e2
Compare
0e6f3dc to
56805c3
Compare
43f9f1d to
54fa8c5
Compare
Adds OpenCode Zen and OpenCode Go as first-class LiteLLM providers, each serving three wire formats: Chat Completions, Anthropic Messages, and OpenAI Responses. Routing between the three arms is decided in code, not from the runtime cost map, because a published map predating this provider would send every Messages-native model down the wrong wire. The model sets carry the full forward-looking grid of model names so a gateway-side addition routes correctly without a release; names without a bundled price simply stay unpriced until a real one is published. Cost resolution falls back to pricing bundled with the package when the runtime cost map carries no usable entry -- the Router registers a bare placeholder for every deployment at startup, so the guard asks for pricing the cost calculator can actually use, not for the key's presence. The cost-map JSON schema gains a `messages` mode so the new entries validate, and the provider tests set module-level configuration through monkeypatch rather than writing process-wide globals directly.
54fa8c5 to
860072c
Compare
TLDR
Add OpenCode as a first-class LiteLLM provider with two surface variants, opencode_go and opencode_zen, supporting three wire formats: Chat Completions, Anthropic Messages, and OpenAI Responses.
Problem this solves:
How it solves it:
User Flow
Before: a user cannot reach the OpenCode models through LiteLLM because the providers are not wired into the dispatch mapping or the cost map.
After: the same request succeeds because the providers are registered.
Relevant issues
N/A
Linear ticket
N/A
Pre-Submission checklist
Screenshots / Proof of Fix
The fork PR (streber42#1) runs this exact commit through the full CI matrix with a correct base after syncing the fork's litellm_internal_staging to the current upstream tip. The type-discipline lint gate, ruff strict budget, basedpyright budget, and model-prices JSON validation all pass against that base. See https://github.com/streber42/litellm/pull/1/checks.
Two checks on the fork PR remain external: documentation and code-quality both fail only on the documentation_test_env_keys step until BerriAI/litellm-docs PR #908 (which documents the five OPENCODE_* env vars) is merged, and benchmarks is CodSpeed CLI infra. None of these are caused by this change.
Type
New Feature
Caveats (if any)
Final Attestation