Skip to content

feat(opencode): add opencode_go and opencode_zen first-class providers - #37103

Open
streber42 wants to merge 1 commit into
BerriAI:litellm_internal_stagingfrom
streber42:litellm_opencode_providers
Open

feat(opencode): add opencode_go and opencode_zen first-class providers#37103
streber42 wants to merge 1 commit into
BerriAI:litellm_internal_stagingfrom
streber42:litellm_opencode_providers

Conversation

@streber42

@streber42 streber42 commented Aug 16, 2026

Copy link
Copy Markdown

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:

  • OpenCode has an API but no first-class LiteLLM provider support, so users must rely on fragile custom_llm_provider workarounds.

How it solves it:

  • Registers both surfaces as first-class providers with transformation classes, cost-map entries, wildcard model expansion, surface-aware auth key resolution, and dashboard placeholder support.

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.

  1. They send a request with model opencode_zen/grok-4.5 and no custom_llm_provider.
  2. The request fails with a model-not-found error.

After: the same request succeeds because the providers are registered.

  1. LiteLLM routes opencode_zen/grok-4.5 through the new OpenCode provider transformation class.
  2. The request is forwarded to the OpenCode gateway with the correct Bearer token resolved from the OPENCODE_ZEN_API_KEY env var.
  3. The response returns with real token counts and spend logged.

Relevant issues

N/A

Linear ticket

N/A

Pre-Submission checklist

  • I have added meaningful tests
  • My PR passes all CI/CD checks (e.g., lint, format, unit tests)
  • My PR's scope is as isolated as possible; it only solves 1 specific problem
  • I have received a Greptile Confidence Score of at least 4/5 before requesting a maintainer review (Greptile reviews automatically)

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)

  • The OPENCODE_* env vars are documented in BerriAI/litellm-docs via PR #908; the documentation_test_env_keys check goes green once that merges.
  • benchmarks job fails on CodSpeed CLI infra (not PR-caused).

Final Attestation

  • The tests check the right things, including the edge cases, and regressions in the respective real-world customer use-cases are not possible after this PR

@greptile-apps

greptile-apps Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Adds first-class OpenCode Go and Zen providers across Chat Completions, Anthropic Messages, and OpenAI Responses wire formats

  • Registers provider enums, model catalogs, pricing, endpoint capabilities, and lazy imports
  • Adds surface-specific authentication, request transformations, dispatch, and tests
  • Extends dashboard provider selection and placeholder support

Confidence Score: 3/5

This 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

Security Review

The OpenCode dispatch can select the process-wide LiteLLM API key ahead of the configured OpenCode key, sending an unrelated credential to the OpenCode gateway and failing authentication

Important Files Changed

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

Comment thread litellm/main.py Outdated
Comment on lines +3397 to +3403
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")
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security 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

Suggested change
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

@streber42 streber42 Sep 5, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +29 to +60
# 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",
}
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread litellm/main.py
Comment on lines +3366 to +3368
def _complete_opencode(
ctx: _CompletionDispatchContext,
) -> _CompletionDispatchResult:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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!

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

codecov Bot commented Aug 16, 2026

Copy link
Copy Markdown

@codspeed-hq

codspeed-hq Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

Merging this PR will not alter performance

✅ 31 untouched benchmarks


Comparing streber42:litellm_opencode_providers (860072c) with litellm_internal_staging (7672399)

Open in CodSpeed

Comment thread litellm/llms/opencode/chat/messages_transformation.py
@veria-ai

veria-ai Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

PR overview

All previously flagged issues have been addressed. No open security concerns remain on this pull request.

Security review

No open security issues remain on this pull request.

Fixed/addressed: 22 · PR risk: 0/10

@streber42
streber42 force-pushed the litellm_opencode_providers branch from 35554ea to e464d3e Compare August 23, 2026 07:22
@streber42
streber42 force-pushed the litellm_opencode_providers branch 3 times, most recently from ede15ea to a3bd79d Compare August 24, 2026 17:29
Comment thread litellm/main.py Outdated
@streber42
streber42 force-pushed the litellm_opencode_providers branch 2 times, most recently from 5a3f412 to c0bbad5 Compare September 1, 2026 21:14
@streber42
streber42 requested a review from a team September 1, 2026 21:14
@CLAassistant

CLAassistant commented Sep 1, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Comment thread litellm/llms/anthropic/chat/guardrail_translation/handler.py Outdated
Comment thread litellm/llms/anthropic/experimental_pass_through/messages/streaming_iterator.py Outdated
Comment thread litellm/integrations/prometheus.py Outdated
Comment thread litellm/proxy/management_endpoints/model_management_endpoints.py
Comment thread litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py Outdated
Comment thread litellm/proxy/policy_engine/pipeline_executor.py Outdated
Comment thread litellm/proxy/pass_through_endpoints/pass_through_endpoints.py
Comment thread tests/test_litellm/proxy/test_common_request_processing.py
Comment thread tests/test_litellm/proxy/management_endpoints/test_key_management_endpoints.py Outdated
@streber42
streber42 force-pushed the litellm_opencode_providers branch 2 times, most recently from a3fbf79 to 74b9b32 Compare September 1, 2026 22:11
@streber42

Copy link
Copy Markdown
Author

Closes: #31568

This PR implements the feature requested in issue #31568 — adding OpenCode as a first-class provider — and extends it with the opencode_zen surface variant alongside opencode_chat, opencode_messages, and opencode_go.

@streber42
streber42 force-pushed the litellm_opencode_providers branch from 74b9b32 to 36d6e61 Compare September 1, 2026 22:18
Comment thread litellm/llms/bedrock/realtime/transformation.py Outdated
Comment thread litellm/llms/gigachat/file_handler.py Outdated
Comment thread litellm/proxy/common_request_processing.py Outdated
Comment thread litellm/cost_calculator.py Outdated
Comment thread litellm/litellm_core_utils/litellm_logging.py
Comment thread litellm/proxy/guardrails/guardrail_hooks/bedrock_guardrails.py
Comment thread litellm/llms/openai/responses/guardrail_translation/handler.py Outdated
Comment thread litellm/llms/gigachat/embedding/transformation.py Outdated
@streber42
streber42 force-pushed the litellm_opencode_providers branch from 36d6e61 to 17909e2 Compare September 2, 2026 15:01
Comment thread litellm/llms/opencode/zen/responses/transformation.py Outdated
Comment thread litellm/llms/opencode/chat/messages_transformation.py Outdated
Comment thread litellm/main.py Outdated
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.
@streber42
streber42 force-pushed the litellm_opencode_providers branch from 54fa8c5 to 860072c Compare September 5, 2026 18:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants