Skip to content

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

Closed
streber42 wants to merge 1106 commits into
litellm_internal_stagingfrom
litellm_opencode_providers
Closed

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

Conversation

@streber42

@streber42 streber42 commented Aug 13, 2026

Copy link
Copy Markdown
Owner

TLDR

Problem this solves:

  • OpenCode's two billing surfaces (Zen subscription, Go per-token) have no first-class LiteLLM provider, so users can't route to them through the proxy
  • Each surface speaks three wire formats depending on the model (OpenAI Chat Completions, Anthropic Messages, OpenAI Responses), which no single generic provider handles

How it solves it:

  • Registers two first-class providers (opencode_go, opencode_zen) that share one codebase
  • Dispatches each model to the right wire format at request time based on model classification
  • Adds the providers to the Add Model dropdown so they populate models in the Admin UI

User Flow

Before: a developer wants to send traffic to an OpenCode model through the proxy, but OpenCode isn't a selectable provider

  1. They open the Admin UI Add Model panel and search for OpenCode, but no OpenCode entry exists
  2. They try to configure an opencode_go/... or opencode_zen/... model by hand, but the proxy rejects it as an unknown provider
  3. They fall back to calling the OpenCode gateway directly, bypassing the proxy's routing, cost tracking, and key management

After: OpenCode Go and Zen are first-class providers

  1. They open the Admin UI Add Model panel and select "OpenCode Go" or "OpenCode Zen"
  2. The model dropdown lists the opencode_go/* and opencode_zen/* models
  3. They add a model (e.g. opencode_zen/gpt-5.6-sol) and send a request through the proxy
  4. The proxy routes the request to the correct OpenCode surface and wire format, and the request is tracked like any other model

Relevant issues

Linear ticket

Pre-Submission checklist

Please complete all items before asking a LiteLLM maintainer to review your PR

  • 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 once the PR is opened; only comment @greptileai to re-request a review after pushing changes)

Delays in PR merge?

If you're seeing a delay in your PR being merged, ping the LiteLLM Team on Slack (#pr-review).

Screenshots / Proof of Fix

Live verification against the OpenCode gateway (real API calls, commit abb8e2e452):

# opencode_go via chat completions surface
curl http://localhost:4000/v1/chat/completions \
  -H "Authorization: Bearer sk-1234" \
  -d '{"model": "opencode_go/glm-5", "messages": [{"role": "user", "content": "say hi"}]}'
# opencode_zen via Anthropic Messages surface
curl http://localhost:4000/v1/messages \
  -H "Authorization: Bearer sk-1234" \
  -d '{"model": "opencode_zen/gpt-5.6-sol", "max_tokens": 64, "messages": [{"role": "user", "content": "say hi"}]}'

Both return 200 with a completion, proving the provider routes to the correct OpenCode surface and wire format end to end. The Admin UI Add Model panel at http://localhost:4000/ui/models-and-endpoints/ lists opencode_go/* and opencode_zen/* models when "OpenCode Go" / "OpenCode Zen" is selected.

Type

🆕 New Feature

Caveats (if any)

  • Model classification for the Anthropic Messages arm uses the cost-map supports_anthropic_messages flag; only messages models (13 zen, 11 go) carry it
  • Responses-mode models are routed via the cost map (mode: responses); adding one only requires a cost-map entry
  • The documentation_test_env_keys and docs checks stay red until the env vars are documented in BerriAI/litellm-docs (PR #908)

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

Greptile Summary

The PR adds first-class OpenCode Go and Zen providers, selecting OpenAI Chat Completions, Anthropic Messages, or OpenAI Responses according to model metadata.

  • Registers both providers, credentials, model catalogs, and lazy configuration loading.
  • Adds transformations and tests for all three supported wire formats.
  • Exposes both providers in the dashboard’s Add Model workflow.

Confidence Score: 5/5

The PR appears safe to merge because no blocking failure remains.

No blocking failure remains.

Important Files Changed

Filename Overview
litellm/main.py Adds the central OpenCode dispatch path that selects the provider surface and wire-format handler.
litellm/llms/opencode/chat/messages_transformation.py Implements the Anthropic Messages-compatible transformation and model classification.
litellm/llms/opencode/chat/transformation.py Implements the OpenAI Chat Completions-compatible OpenCode transformation.
litellm/llms/opencode/config.py Selects the appropriate OpenCode configuration according to surface and model.
litellm/llms/opencode/go/responses/transformation.py Implements OpenCode Go’s OpenAI Responses-compatible transformation.
litellm/llms/opencode/zen/responses/transformation.py Implements OpenCode Zen’s OpenAI Responses-compatible transformation.
ui/litellm-dashboard/src/components/add_model/AddModelForm.tsx Adds both OpenCode surfaces to the dashboard’s model-creation flow.
model_prices_and_context_window.json Registers OpenCode model capabilities, pricing metadata, and response modes.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[opencode_go or opencode_zen request] --> B{Model classification}
    B -->|supports_anthropic_messages| C[Anthropic Messages transformation]
    B -->|mode: responses| D[OpenAI Responses transformation]
    B -->|default| E[OpenAI Chat Completions transformation]
    C --> F[OpenCode gateway]
    D --> F
    E --> F
Loading

Reviews (4): Last reviewed commit: "fix(anthropic): gate mid-turn system pre..." | Re-trigger Greptile

from litellm.llms.base_llm.chat.transformation import BaseLLMException
from litellm.llms.opencode.common_utils import OpenCodeException
from litellm.secret_managers.main import get_secret_str
from litellm.types.router import GenericLiteLLMParams

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Tighten provider boundary typing

The new provider modules use Any, unparameterized dict and list annotations, and an untyped configuration factory across request and configuration boundaries. This prevents static checking of substantial portions of the provider implementation; replace the coarse annotations with the existing concrete request, parameter, header, and configuration types, and remove the unnecessary explanatory comments covered by the same repository guideline.

Context Used: CLAUDE.md (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!

@streber42
streber42 force-pushed the litellm_opencode_providers branch from 58b27a6 to 35554ea Compare August 17, 2026 00:47
mateo-berri and others added 22 commits August 20, 2026 16:55
…l monitors

The monitors are separate servers with their own password, so the data node's Entra or
IAM token has no standing there. Dropping the provider only when a Sentinel password was
configured left it in place for unauthenticated monitors, where redis-py sends it as an
AUTH the monitor rejects and async Sentinel discovery fails.
* test(ci): serve /moderations from the canned OpenAI mock

The otel proxy E2E job points its `openai/*` wildcard deployment at the
canned mock, and BerriAI#37492 made `get_model_list` agree with
`get_available_deployment` on bare model names. /moderations now resolves
`omni-moderation-latest` to that wildcard deployment the way
/chat/completions already did, so the request lands on the mock, which
never implemented the route and answers a bare 404.

Add /moderations and /v1/moderations to the mock, returning an
OpenAI-shaped response with one result per input item.

* style(ci): annotate the new moderations locals as Final
The edit model is reached through the image generation path with fal's
image_urls param; /v1/images/edits is not wired for fal_ai and errors.
Point supported_endpoints at /v1/images/generations and say so in the
entry notes.
…ls that still exist (BerriAI#37733)

* test: point the live web search, groq and vertex image suites at models that still exist

Three CircleCI jobs on the staging-to-main promotion are red because the models
their live suites call have been retired by the providers, not because anything
in litellm changed.

openai/gpt-4o-search-preview now answers "has been deprecated" (its dated id
gpt-4o-search-preview-2025-03-11 carries deprecation_date 2026-07-23), so the
two web search conformance tests and the web search cost tracking test move to
gpt-5-search-api, the current search model. It keeps mode chat,
supports_web_search and a search_context_cost_per_query map, so the cost
assertion still resolves.

groq/llama-3.1-8b-instant reached its deprecation_date of 2026-08-16 and Groq
answers "does not exist or you do not have access to it". It follows
groq/llama-3.3-70b-versatile to groq/openai/gpt-oss-120b, the same replacement
PR BerriAI#37422 already picked. The proxy config that job boots routes on a */*
wildcard, so no config change is needed.

vertex_ai/imagen-3.0-fast-generate-001 404s with "was not found or your project
does not have access to it". Google retired the whole Imagen family across
Vertex and the Gemini API, so there is no Imagen id left to point at. The class
is removed rather than repointed: Vertex image generation is already covered
live by TestVertexAIGeminiImageGeneration on vertex_ai/gemini-2.5-flash-image,
and the Imagen request and response transformations keep their offline coverage
in tests/test_litellm/llms/vertex_ai/image_generation/.

Only live call sites move. Remaining references to the old ids sit in offline
cost-map and transformation tests, where the string is a lookup key and no
request leaves the process.

* chore(lint): ratchet the TQ005 ceiling down to the count this branch reached

Removing the retired TestVertexImageGeneration class cleared one TQ005
violation, so the gate demands the limit come down with it.

make lint-budget-update only lowers a limit by the delta a branch cleared, and
this ceiling already sat 2 above the base count, so the tool landed on 2834
while the gate wants the limit at or below the 2832 this branch reached. The
remaining 2 are that stale headroom, which is exactly what the gate is asking
to reclaim.
Cognition serves an OpenAI-compatible /v1/chat/completions endpoint, so it has been onboarded as
custom_llm_provider: openai. That books its traffic as OpenAI, which means OpenAI-specific cost
discounts and provider-level reporting apply to it.

Registers cognition through the JSON provider registry: a providers.json entry with
COGNITION_API_KEY and COGNITION_API_BASE, LlmProviders.COGNITION, the constants.py provider lists,
cost map entries for swe-1.6 and swe-1.7, the provider endpoints matrix, the dashboard provider
fields, and tests. JSON providers can now also be resolved from their base url alone, so an
api_base pointing at a known provider no longer falls through to an unresolved provider.
…_tokens

fix(model-costs): correct gpt-5.6 max input tokens to 922k
…RL (BerriAI#37691)

The read replica never received the operator's DB pool settings, so its
Prisma pool fell back to `num_physical_cpus * 2 + 1` and the configured cap
was not enforced. Both startup paths now pass the same params to the reader:
the CLI, and the componentized entrypoints that go through
`DatabaseURLSettings.apply_to_env`.

Only pool and timeout params are inherited, through a single allowlist both
paths share. Anything that decides which tables a query resolves against
stays on the writer, including entries smuggled in through
`database_extra_connection_params`, so a writer `search_path` cannot repoint
reader queries. Params the operator pinned on the replica URL still win.

Co-authored-by: Yassin Kortam <yassin.kortam@gmail.com>
Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
feat(fal_ai): add gpt-image-2 image generation support
…tes (BerriAI#37700)

SCIM roster writes were swallowed, so a group or user push returned 200 while the
team roster never received the membership. Surfacing the failure fixes that, but
aborting on the first failed write leaves the rest of the batch unattempted on top
of unrolled-back, which is worse than what it replaces.

Every roster write in a reconciliation is now attempted, and the ones that did not
land are reported together, naming each failed add and remove. Rollback would be the
other option and it is not safe here: the compensating write can fail too, and it can
strip a membership that pre-dated the push. SCIM reconciliation is idempotent, so a
named partial failure is what the IdP's next push needs to close the gap.

The reported status still follows the failures, so a unanimous 404 stays a 404 and
only a batch whose failures disagree falls back to 500.

Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…credential_provider

fix(redis): apply Azure AD and GCP IAM auth to every async client path
…erriAI#37744)

a369cb0 made the chat-to-responses bridge return the routing prefix on the
model it passes to responses(), so responses() re-resolving the provider is a
no-op instead of stripping a second prefix. It updated the bridge's own unit
tests but not this one, which still asserted the stripped id and has been
failing llm_translation_testing since that change landed.

The provider still receives gpt-5.4: responses() strips the openai/ prefix on
its own resolve, one layer later than this assertion used to sit. The stale
comment claiming the prefix is stripped before routing goes with it.
…ow scan (BerriAI#36497)

Every pod schedules the budget reset job, so a fleet re-read the whole due
population and wrote it back against one Postgres at the same calendar
boundary, multiplying a single sweep by its replica count. The job now takes
the shared PodLockManager lease, so one pod sweeps per tick. A deployment with
no Redis keeps its previous behavior, and a Redis that cannot answer sweeps
unguarded rather than stranding every expired budget at its cap.

The per-window scan read every row carrying budget_limits in one statement, so
its cost grew with the deployment's key count. It is now keyset-paginated and
walks to the end of the table on every sweep. A per-run cap would need a resume
position, and no pod can hold one because the lease rotates between ticks, so
the strictly advancing cursor is what terminates the walk.

Found and updated rows were also JSON-serialized into the service hook's
metadata and into debug lines on every chunk, on the event loop, whether or not
anything consumed them. The hooks now carry counts, and the debug payload is
deferred until a record is actually emitted.

Resolves LIT-4793

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…models

feat(proxy): add admin toggle to block requests for models without pricing
…tring

The passthrough tests and their coverage registry rows pointed at the internal
ticket id, which does not resolve for anyone following a link from
status.litellm.ai. Each test docstring and registry rationale now names the
GitHub issue it pins: BerriAI#36086 for the two prefix routing cases, BerriAI#36087 for the
file list cursors, BerriAI#36523 for streamed Responses cost, and BerriAI#36646 for
embeddings spend.
The swe-1.7 rates were carried over from the closed prior attempt and
match SWE-1.7 Lightning, 5x the SWE-1.7 Max and Medium rates the vendor
publishes. swe-1.6 was already on the standard tier, so the two entries
disagreed with each other. Both now read 0.5 in, 2.5 out, 0.2 cached per
million tokens.

Also drops the redundant registry comment in constants.py.
…consolidated_20260820

fix(model_prices): consolidate eleven open registry audits into one changeset
…upport

The swe-1.7 rates were briefly lowered to the standard tier. The docs page
records the API-served swe-1.7 as the Cerebras-served Lightning tier, so put
the matching rates back rather than have the cost map and the docs disagree.

Cognition also answers /v1/responses through the chat-completions bridge, the
same as every other provider in the JSON registry, so the endpoints support
matrix should say so instead of under-declaring it.
cache_read_input_tokens and cache_creation_input_tokens are pydantic extras
on Usage, not declared fields, so filling them in created keys that were not
there before rather than replacing a None. Readers that test for presence
then took the new zero as authoritative: the spend log writer skipped its
own copy from prompt_tokens_details, turning a real cache read of 500 into
0, and the prometheus provider cache counters stopped incrementing.

Carry the prompt_tokens_details counts up before defaulting to zero, so a
partial row reports the same cache numbers a complete one does. Renamed the
helper to say what it now does.
mateo-berri and others added 13 commits August 22, 2026 15:25
…ed_session_lookup

fix(responses): keep the conversation when chaining previous_response_id on the bridge
The lint job installs its dependencies from scratch on every run. That step
measures 2.8 minutes of a job whose p50 is 9.5, and lint is the slowest
required check on 9 of the last 10 merged staging PRs, so it sets the
critical path for the whole PR.

_test-unit-base.yml already caches ~/.cache/uv and .venv keyed on uv.lock.
This mirrors that block. The key carries its own `lint` namespace rather
than sharing the unit tier's: the two jobs sync different group sets
(proxy-dev + e2e-dev here, ci + proxy-dev + four extras there), so a shared
.venv entry would be pruned and rebuilt on alternating runs.
* perf(ci): fan the budget checkers out across cores

check_type_discipline.py and check_test_quality.py each walk a few thousand
files and parse every one, single-threaded. In the lint job those two steps
measure 2.3 and 1.6 minutes, second and third behind dependency install, and
lint is the slowest required check on 9 of the last 10 merged staging PRs.

check_file is already pure per-file work, so the walk fans out over a process
pool with no change to what either rule reports. Callers sort, which is what
keeps output order stable when results land out of order. Runs below
PARALLEL_MIN_PATHS stay serial rather than pay for process startup, and the
worker count is capped so a large runner does not oversubscribe.

Measured locally over the same trees, output byte-identical both times:
type-discipline 17.8s -> 3.0s over litellm/ (78,768 report lines), test-quality
14.4s -> 2.3s over tests/ (6,321 report lines), per-rule counts unchanged.

* test(ci): type the fan-out helpers and skip the comparison on one core
…rriAI#37785)

* ci: port the Postgres suites off CircleCI onto service containers

proxy_behavior_tests, proxy_security_tests and schema_migration_check were
near-identical CircleCI jobs: a Postgres sidecar, a schema seed, and one pytest
tree each. They ran nowhere else, and CircleCI holds none of the branch
ruleset's required checks, so the signal they produced gated nothing.

test-postgres.yml runs the same three trees on a Postgres service container as
one matrix, keeping each suite's own seeding rather than normalising it: the
behavior and security trees keep `prisma db push`, and the migration tree keeps
an empty database, which is what it needs to apply every committed migration
itself.

Their CircleCI definitions and workflow entries go with them, taking the config
from 47 jobs to 44. assert_ci_coverage.py stays green: dropping the new
workflow fails the census on exactly these trees, so the coverage moved rather
than went missing.

auth_ui_unit_tests is deliberately left behind. Ported, two of its
tests fail because prepare_metadata_fields refuses enterprise-only keys without
LITELLM_LICENSE, which exists as a CircleCI project variable and has no
GitHub Actions secret. Creating that secret is a human action, so the job stays
on CircleCI until it exists rather than shipping a red shard or quietly
deselecting the two tests.

* chore(ci): drop the narrative header from test-postgres.yml
…7787)

* feat(ci): gate patching of SDK internals in tests as TQ008

TQ002 catches the narrowest symptom of the suite's dominant mocking idiom,
patch X then assert only that X was called. The idiom itself is wider: tests
reach for litellm's own functions instead of faking the wire, so they pin how
the code is wired rather than what it does, and a test that patches internals
but makes weak real assertions trips nothing today.

TQ008 counts patch targets rooted at `litellm`, both the dotted string form and
the attribute chain handed to patch.object, and ratchets like every other rule.
Mocking anything outside the SDK is untouched: respx, httpx transports and
third-party clients do not trip it, which is the point, since those are the
patterns this is meant to move the suite toward.

Seeded at 9,643, in line with the ~9.4k patch sites an independent grep found
in the mirror. The burn-down horizon is long; the value here is stopping the
flow rather than clearing the stock.

Five existing rule tests patched `litellm.completion` incidentally and now
report TQ008 alongside what they were pinning. Their expected values are
updated to the accurate pair rather than loosened, so they keep failing on a
regression in either rule.

* test: add TQ008 to the shipped-budget rule canary

* fix(ci): resolve imported SDK names in TQ008

patch.object(handler.OpenAIChatCompletion, ...) after a from-import reaches the
same internal as the dotted string form, but the rule only saw the bare local
name and let it through. Import bindings are now resolved to the path they
stand for, so the aliased, renamed and from-imported forms all read alike and
the reported target is the real one.

That is 1,496 patches the ratchet could not see, so the TQ008 limit moves from
9,643 to 11,139. Third-party names and locals with no SDK import behind them
stay unflagged.
codecov.yaml has carried an `Enterprise` component scoped to `enterprise/**`
since it was written, and it has never received a line of data. Every one of
the 19 coverage invocations across the unit base, the MCP workflow and the
CircleCI config passes `--cov=./litellm` and nothing else, so 11,203 lines of
paid-customer code sat outside the measured universe while the reported number
described only the rest.

litellm-enterprise is a uv workspace member and a direct dependency, so every
job that syncs already has it installed and importable; only the measurement
was missing.

Measured on tests/test_litellm/enterprise, the shard that exercises this code:
0 enterprise files in the report before, 142 after, at `enterprise/...` paths
that match the component's existing glob. That shard alone puts enterprise at
30.8%, which nudged its own total from 24.09% to 24.20% rather than down. The
aggregate direction across every shard is not knowable until they all report,
and a drop there is the instrument working, not a regression.
The allowlist recorded eight files in tests/local_testing, 118 tests, that
every job globbing that directory then deselects: local_testing_part1 and
part2 carry `-k "... and not caching and not cache"`, and the other three keep
one unrelated keyword each. They counted as covered while running nowhere.

Five of the eight need nothing. Measured with no provider credentials and no
Redis: test_cache_preset_key, test_caching_handler, test_prompt_caching,
test_responses_stream_cache_keys and test_unit_test_caching pass, 45 tests
together, and they now run as a caching-local shard. The other three stay
allowlisted with what they actually need recorded rather than a question:
test_caching wants Redis and a provider key for 37 of its 65, disk-cache wants
OPENAI_API_KEY for 2 of 4, gcs-cache wants GCS credentials for all 4.

Taking them off the allowlist exposed a gap in the slice guard itself: it
reasoned only about CircleCI `-k` expressions, so a file every slice drops read
as unrun even when a workflow names it outright. It now credits workflow
test-paths the way the census already does, and only workflows, so a tree only
CircleCI globs is still reported.
… SQL (BerriAI#37791)

* fix(ci): make the migration DDL guard run, and stop it reading comments as SQL

TestMigrationSQLIdempotency requires guarded DDL across litellm-proxy-extras
and has never run in any job, so the convention eroded quietly. Four of its
assertions fail today, and it was allowlisted rather than wired up because
fixing the migrations is not an option: Prisma checksums an applied migration,
so editing one breaks `migrate deploy` for every existing install.

Two things were wrong with the guard itself. It scanned raw lines, so Prisma's
own `-- CREATE INDEX CONCURRENTLY ...` explanations counted as the statements
they describe, which is two of the reported migrations. And it had no way to
say "these predate the rule", so the only options were editing immutable files
or leaving the whole file unrun.

Comments are now stripped before matching, on the drop-column rule too, and the
migrations that already violate are named once in _PRE_GUARD_MIGRATIONS. The
rules bind everything after them, so a new migration with bare CREATE TABLE,
ADD COLUMN, CREATE INDEX or an unguarded ADD CONSTRAINT now fails a check
instead of landing unnoticed.

That set is 14 migrations, not the 13 previously recorded, measured after
comment-stripping. It can only shrink: a test fails if an entry names no
migration on disk, and another fails if an entry no longer violates anything.

The file now runs as a proxy-extras shard and comes off the coverage allowlist.

* fix(ci): strip block comments in the migration guard too

Prisma opens a destructive migration with a /* Warnings: You are about to
drop the column ... */ header. Nothing in the tree trips a rule on that text
today, but it is prose about a statement rather than the statement, and the
line-comment fix left the class open. Bodies are blanked rather than removed
so the reported line number still points at the real statement.
tests/enterprise is 13 files and 244 tests that only CircleCI runs, and CircleCI
gates nothing: it triggers on PR labeled events, none of its jobs are required,
and red runs get merged past. So the suite that covers the enterprise package's
guardrails, auth and management endpoints has had no say in whether a change
lands.

Measured on 2026-08-21 with every credential stripped from the environment: 240
passed, 4 skipped, nothing failed. It needs no provider key, so it can be a
required shard rather than a scheduled lane, unlike the other CircleCI suites in
this group, which each carry a live-API minority.

The CircleCI job is removed in the same commit so the suite runs once, not twice.
…erriAI#37804)

proxy-endpoints and proxy-infra are the unit tier's critical path at 358s and
325s of pytest, measured on staging 2026-08-21, and both run two xdist workers
on a four-vCPU runner. proxy-server already runs four. This is the cheaper half
of splitting them: no second job, so no second setup to pay for.
…rovider maps to (BerriAI#37807)

`exception_type` decides the class and status a caller sees for every provider
failure, across 190 raise sites, and the tests for it were written one incident
at a time. Nothing said what a plain 401 from any given provider should be, so
mutating a raise site went unnoticed: swapping the class at each of the 190 in
turn, the mapped test file caught 15.

Adds two tables asserted end to end through `exception_type`: 25 providers by
the 9 upstream statuses, and the three error shapes the router branches on
(a full context window, a content policy block, a timeout). The same 190
mutants now fail 97 of them.

The tables record today's behavior, uneven where it is uneven. cloudflare,
ollama and vllm map no status at all, so every failure reaches the caller as a
500. A full context window is recognised by 15 of the 25, and a content policy
block by 11, which bounds where `context_window_fallbacks` and the content
policy retry policy can fire.
@streber42
streber42 force-pushed the litellm_opencode_providers branch 2 times, most recently from e464d3e to 9feeaef Compare August 23, 2026 07:32
yuneng-berri and others added 7 commits August 24, 2026 10:12
…s to litellm_team (BerriAI#37918)

* fix(terraform): add soft_budget, tags, and soft_budget_alerting_emails to litellm_team

The team resource rejected soft_budget and tags at plan time and had no way
to express the list-valued metadata.soft_budget_alerting_emails the proxy
reads for soft-budget alerts, even though /team/new and /team/update accept
all three. Add the attributes, forward them in buildTeamData (alert emails
merged under metadata, where the proxy stores them), and send the full
metadata map whenever either half changes because /team/update replaces
metadata wholesale.

Read was decoding /team/info as if the team fields were top-level, but the
proxy nests them under team_info, so every attribute silently fell back to
prior state. Decode the envelope and split the proxy's metadata back into
tags / soft_budget_alerting_emails / string metadata, dropping the
server-managed team_member_budget_id.

Verified with OpenTofu plan/apply against a live proxy: the attributes are
accepted, land on the proxy, refresh into state, re-plan clean, propagate
on update, and clear when removed from HCL.

* fix(terraform): clear litellm_team.soft_budget in state when the proxy returns null

Read only wrote soft_budget when the proxy returned a value, so a soft
budget cleared outside Terraform stayed in state and never surfaced as
drift. Set it from the response unconditionally so a null clears it.
…7985)

The virtual key shown after creating a key sits in a div with a
hardcoded #f8f8f8 inline background, so in dark mode the box keeps
the light background while the key text inherits the light foreground
color, leaving the key nearly unreadable. Swap the inline styles for
the bg-muted and text-foreground tokens, which resolve per theme.
…l flow (BerriAI#37986)

The tooltip popup is an inline-flex row, so the four sibling blocks passed as a fragment laid out side by side in four columns. Wrap them in a single flex-col container instead.

The inline code samples also used bg-muted, which is defined against the page surface, not the inverted tooltip surface, so they rendered as near-white chips carrying near-white text. Tint them from the popup's own token instead.
…mited (BerriAI#37916)

* fix(ui): render team and org tpm/rpm limits of 0 as 0 instead of Unlimited

A tpm_limit or rpm_limit of 0 is a hard block on the backend (every request 429s) and only null means unlimited, but the team and organization views rendered both as "Unlimited" (and a team-member limit of 0 as "No Limit") because every display site used a falsy || fallback. The team member edit dialog also seeded its form with `tpm_limit || null`, so opening Edit Member on a member stored with 0 and clicking Save sent null to /team/member_update and silently turned the hard block into unlimited

Every limit display site in TeamInfo, organization_view, the organizations list cell and the team members table now uses a nullish check, and both member form seeding paths keep 0 for max_budget_in_team, tpm_limit and rpm_limit. Regression tests cover each site and the existing memberFormValues test that asserted 0 -> null is flipped to assert 0 survives

Resolves LIT-5760

* test(ui): assert a stored 0 member limit survives an untouched save

The EditMembership integration test named the old 0 -> null collapse as the expected payload, so the related-tests CI job went red once the form kept 0. It now asserts 0 survives and only the empty budget_duration collapses to null. The TeamMemberTab fixture is built with a map instead of mutating the nested membership
…ating (BerriAI#37968)

Regenerating a key from the key info page left the ?key= query param on the
old hash, so dismissing the dialog or reloading landed on a key that no longer
exists and the page rendered "Key not found".

Two defects had to line up. POST /key/{key}/regenerate returns the rotated
hash in token_id and leaves token null, but RegenerateKeyModal read
response.token || response.key_id, neither of which the endpoint populates, so
it always reported the old hash back to its parent. And KeyInfoView's
onKeyDataUpdate prop had no caller anywhere in the tree: VirtualKeysTable owns
the ?key= param and mounts the view but never passed it, so even a correct
hash went nowhere.

VirtualKeysTable now handles the update by pointing ?key= at the rotated hash
and refetching. KeyInfoView holds that callback until the regenerate dialog is
dismissed rather than firing it on the API response, because swapping the
selected key mid-dialog unmounts the view and tears down the one-time
plaintext key before the user can copy it.
The playground message bubble painted its fill, border and avatar circle from
inline hex values, so in dark mode both bubbles stayed near-white while the text
inherited the dark foreground: the message body was unreadable. The MCP-events
placeholder bubble in ChatUI carried the same three fills.

They move onto the tokens the rest of the sweep already uses, so the assistant
surface is bg-card over border-border and the user surface is the info tint at
the same weight the other selected-state surfaces take. Light mode keeps the
same colour family it had.

The regression test asserts the token classes and that no inline style survives
on either surface, which is the exact shape the bug took.
…tplace_commands_v2

fix(UI): correct skill install command and marketplace setup UX
@streber42
streber42 force-pushed the litellm_opencode_providers branch from 9feeaef to ede15ea Compare August 24, 2026 17:28
@greptile-apps

greptile-apps Bot commented Aug 24, 2026

Copy link
Copy Markdown

Too many files changed for review (3000 files, 100 file limit).

Add two new first-class OpenCode provider surfaces that route LLM requests
through the opencode.ai gateway:

- opencode_zen: supports chat completions, Anthropic Messages, and Responses
  arms across 36+ models including Claude, GPT-5 series, Grok, Gemini, and more
- opencode_go: similar 3-arm routing for Go-flavored models (minimax, qwen3.5+)

Each surface has its own API key resolution chain (module attr > env var >
shared fallback), cost-map entries, and model-list wildcards.

The chat arm uses Bearer token auth to /chat/completions. The messages arm
uses x-api-key to /v1/messages (Anthropic wire format). The responses arm
bridges to /v1/responses for GPT-5 models.

Includes: transformation configs, error handling, streaming handlers, cost
map entries, model wildcard registration, and test suite exercising all three
arms.
@streber42
streber42 force-pushed the litellm_opencode_providers branch from ede15ea to a3bd79d Compare August 24, 2026 17:29
… rebind-ok: CI lint and schema validation fixes
- Add supports_adaptive_thinking, prompt_cache_min_tokens, and
  supports_sampling_params to opencode_zen/claude-opus-5
- Add supports_adaptive_thinking, thinking_always_on, and
  supports_sampling_params to opencode_zen/claude-fable-5
- Add supports_sampling_params to opencode_zen/claude-opus-4-7 and
  opus-4-8
- Fix import sorting in zen/responses/transformation.py
- Regenerate model_prices_and_context_window.schema.json
@streber42 streber42 closed this Aug 25, 2026
streber42 pushed a commit that referenced this pull request Sep 1, 2026
The notice interpolates each candidate's title, and the sweep scanned the whole
comment for issue references and took the lowest. Titles are attacker-controlled,
so filing a candidate titled "... see #1" redirected the closure: any later report
matching that candidate would be closed as a duplicate of #1 instead.

The detector now emits the candidate numbers as a digits-only field inside the
marker, built from the API's number field, and the sweep reads only that. Prose is
never parsed, so nothing a reporter can type reaches the target selection.
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.