Repository navigation
feat(v2): enable prompt caching for Claude behind LiteLLM - #44
Conversation
OpenCode 2's OpenAI-compatible Chat Completions runtime only places Anthropic-style `cache_control` breakpoints when the model declares `compatibility.cacheFormat`. Discovered models never did, so Claude served through LiteLLM lost prompt caching on OpenCode 2 (yuseferi#42). Record per model whether `/v1/model/info` lists `cache_control` under `supported_openai_params` and register Chat Completions models that do with `compatibility.cacheFormat: "anthropic"`. Only the Chat Completions runtimes read the setting: the Anthropic Messages runtime adds its own breakpoints and the Responses runtimes do not use it, so models on those packages are left alone. The hint travels with the discovery result and the disk cache (`CACHE_VERSION` 4). Add the `modelCompatibility` provider option, a per-model map like `modelCapabilities`, to set `cacheFormat` to `"anthropic"`, `"alibaba"` or `null`. OpenCode's own `providers.<id>.models.<model>.compatibility` is merged over the plugin's value, so it can add or change a key but not remove one the plugin inferred; `null` here does that. Values are validated when parsed, because OpenCode validates plugin-registered models only when it serializes them, where one bad value fails the whole model list. It accepts only `cacheFormat`; it is applied when models are registered, so it is not part of the cache key. Fixes yuseferi#42 Signed-off-by: Mateusz Kowalski <mateusz@mkowalski.com> Assisted-By: Claude Fable 5.1
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info
📝 Walkthrough
Merge Risk: ⚪ Minimal · up to
|
…#43) * fix: prefer the user's saved provider credential over the pod env key The plugin resolved its key as configuredKey ?? envKey ?? storedKey (v1) / providerSettings ?? pluginOptions ?? env ?? connection (v2): the pod environment (LITELLM_API_KEY / LITELLM_MASTER_KEY delivered by the deployment pipeline) won over the credential the user saved in the box. With an invalid env key the discovery 401s, the provider ends up with no models and every session turn fails with ModelNotSelectedError — the saved credential was never used. A credential the user saves in the box MUST win over the pod env. The env is a box-level fallback for users who have not saved a credential — never an override of a user's explicit choice. An explicit settings/plugin-options apiKey stays first (the operator's deliberate config). v2: connectedApiKey (the ACTIVE credential in the v2 credential store, resolved via the host's integration API) now beats the env. v1: storedKey (auth.json via /connect) now beats the env. * fix: drop the build/prepublishOnly scripts — npm's git-dep preparation runs them and the bundled npm shim inside opencode breaks (git dep preparation failed) * #96: the v2 plugin reads the box auth store — a config-defined provider carries no integrationID, so the host-integration path cannot see the /connect key and the stale pod env key (which 401s against the production gateway) won every discovery. The v2 precedence is now settingsApiKey ?? connectedApiKey ?? storedKey ?? envApiKey, exactly like the v1 entrypoint; a saved credential still beats a stale environment key. Regression test: stored auth key beats the pod env key when no v2 connection exists. # Conflicts: # README.md # src/plugin/v2.ts * #96: isolate the model cache by credential + recompute the effective key on credential.switched (CodeRabbit PR #43 review findings) - buildCacheKey gains an optional credential fingerprint (a short sha256, never the raw key): a credential change starts a fresh discovery instead of serving another credential's cached model list (index.ts:760 finding; same mismatch in v2.ts). - refreshConnectionCredential gates on the integration's presence, not on usesConnectionCredential: setup may run before the user saves a credential, and the first save now recomputes the effective key (settings ?? connection ?? current), flips the connection-credential mode, and marks the source for a provider reload (v2.ts:278 finding). - Tests: HOME isolated per test (the box auth store is never read from the developer's real home; fingerprints deterministic), cache-key fixtures built with each setup's credential, new unit test (the credential fingerprint isolates the key) + integration test (the first credential saved after setup refreshes the source and reloads the registry). 129 tests + tsc green. * refreshConnectionCredential now mirrors the makeProviderSource precedence on the refresh path: with no active connection key the fallback chain is settings -> stored credential (re-read from the box auth store) -> env key, instead of retaining the previous connection key which runtimeSettings would materialize into provider settings. Regression test: falls back to the stored/env key when the connection credential is removed (asserts the last discovery carries the env fallback and the settings materialize it). 130 tests + tsc green. * test: reconcile the credential-precedence tests with the new order Rebase fixups for the conflict with #41: - The credential-fingerprinted cache key now includes the effective API key, so the two model-hint tests added by #44 compute their cache key with the same `example-key` the provider uses. - The connect-dialog test now asserts explicit operator config wins over the dialog credential, and a new test covers the dialog credential beating the pod env key (env is only a fallback) — matching #43's precedence, which #41 had reversed. - CHANGELOG entry for the credential-precedence fix. * test: keep fake test keys out of the plugin scanner's secret pattern The repo's plugin security scan (HOL scanner, `fail_on_severity: high`) flags `apiKey: '...'` / `LITELLM_API_KEY = '...'` fixtures whose value is >= 8 chars and not placeholder-shaped. Route the env assignment through a small `setEnvApiKey` helper and rename the two fixture values to the `example-` form the scanner allowlists for test files, so the gate stops reporting the deliberately-fake keys as hardcoded secrets. --------- Co-authored-by: Angel Nunez Mencias <angel.nunez@phoeniqs.com> Co-authored-by: Yusef Mohamadi <yuseferi@gmail.com>
Summary
On OpenCode 2, the OpenAI-compatible Chat Completions runtime only emits Anthropic-style
cache_controlbreakpoints when a model declarescompatibility.cacheFormat: "anthropic". Discovered models never did, so Claude served through LiteLLM lost prompt caching after upgrading from OpenCode 1 (#42).The plugin now records, per model, whether
/v1/model/infolistscache_controlundersupported_openai_paramsand registers Chat Completions models that do withcompatibility.cacheFormat: "anthropic". LiteLLM reports that for Anthropic and Vertex AI Anthropic deployments. Responses-API models and providers on an Anthropic runtime package are left alone (those runtimes don't readcacheFormat).A new
modelCompatibilityprovider option (per exact model id, likemodelCapabilities) setscacheFormatto"anthropic","alibaba", ornull. It exists because OpenCode's ownproviders.<id>.models.<model>.compatibilityis merged over the plugin's value, so native config can add or change the key but cannot remove one the plugin inferred. It accepts onlycacheFormat, the one key the plugin sets; values are validated when parsed because OpenCode validates plugin-registered models only when it serializes them, where one bad value fails the whole model list. It is applied at registration, so it is not part of the cache key.The hint travels with the discovery result and the on-disk cache (
CACHE_VERSION3 → 4; one cold discovery on first start after upgrade, as with #40).Fixes #42
Type of change
Checklist
npm run typecheckpassesCHANGELOG.mdupdated under## [Unreleased]How was this tested?
npm test: 141 passing (125 existing + 16 new);tsc --noEmitclean.vertex_ai/claude-opus-4-8(reportscache_control) andgithub_copilot/*models (do not), OpenCode 2.0.30. After the change,/api/modelshowscompatibility.cacheFormat: "anthropic"on the Vertex model under anopenai-compatibleprovider, and nothing on the Copilot models, onmode: responsesmodels, or on models under ananthropic-compatibleprovider.provider.settings.Notes
cacheFormatis read by OpenCode ≥ 2.0.28; earlier 2.x hosts drop the key without error. The README says so.as unknown as Model.Info['compatibility']: the lockfile resolves@opencode/schema@2.0.15, whoseModel.CompatibilitypredatescacheFormat. Bumping@opencode/pluginwithin the existing^2.0.14range changes ~900 lockfile lines of unrelated transitive dependencies, so this keeps one documented cast instead. Happy to do the bump as a separate PR if you'd prefer that.includeModels,modelCompatibility, …) still reach the runtime settings of a provider that was configured before plugin setup (editor.updatepath). That predates this PR and is harmless (runtimes ignore unknown option keys); stripping them there would also be a no-op, since OpenCode's config-provider plugin re-appliesproviders.<id>.settingsafter this plugin's transform.Summary by CodeRabbit
cache_control.modelCompatibilityoption to set or remove inferred cache-format compatibility.