Skip to content

feat(v2): enable prompt caching for Claude behind LiteLLM - #44

Merged
yuseferi merged 1 commit into
yuseferi:mainfrom
mkowalski:fix/anthropic-cache-format
Oct 20, 2026
Merged

yuseferi merged 1 commit into
yuseferi:mainfrom
mkowalski:fix/anthropic-cache-format

Conversation

@mkowalski

@mkowalski mkowalski commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary

On OpenCode 2, the OpenAI-compatible Chat Completions runtime only emits Anthropic-style cache_control breakpoints when a model declares compatibility.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/info lists cache_control under supported_openai_params and registers Chat Completions models that do with compatibility.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 read cacheFormat).

A new modelCompatibility provider option (per exact model id, like modelCapabilities) sets cacheFormat to "anthropic", "alibaba", or null. It exists because OpenCode's own providers.<id>.models.<model>.compatibility is merged over the plugin's value, so native config can add or change the key but cannot remove one the plugin inferred. It accepts only cacheFormat, 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_VERSION 3 → 4; one cold discovery on first start after upgrade, as with #40).

Fixes #42

Type of change

  • 🐛 Bug fix (non-breaking)
  • ✨ New feature (non-breaking)
  • 💥 Breaking change
  • 📝 Documentation only
  • 🔧 Internal / refactor

Checklist

  • npm run typecheck passes
  • No new runtime dependencies (or justified in this PR description)
  • README updated if public API or behavior changed
  • CHANGELOG.md updated under ## [Unreleased]
  • Commit messages follow Conventional Commits

How was this tested?

  • npm test: 141 passing (125 existing + 16 new); tsc --noEmit clean.
  • Live: LiteLLM 1.104.0 proxy serving vertex_ai/claude-opus-4-8 (reports cache_control) and github_copilot/* models (do not), OpenCode 2.0.30. After the change, /api/model shows compatibility.cacheFormat: "anthropic" on the Vertex model under an openai-compatible provider, and nothing on the Copilot models, on mode: responses models, or on models under an anthropic-compatible provider.
  • New tests cover cold start, warm cache (no network), background refresh with changed and unchanged inventories, a provider registered after setup, the V1 → V2 shared cache, and the override from both plugin options and provider.settings.

Notes

  • cacheFormat is 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, whose Model.Compatibility predates cacheFormat. Bumping @opencode/plugin within the existing ^2.0.14 range 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.
  • Discovery-only option keys (includeModels, modelCompatibility, …) still reach the runtime settings of a provider that was configured before plugin setup (editor.update path). 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-applies providers.<id>.settings after this plugin's transform.

This change was developed with help from an AI coding agent (Claude, running in OpenCode); I reviewed and tested it before opening the PR.

Summary by CodeRabbit

  • New Features
    • OpenCode 2 models can now use Anthropic prompt caching when the proxy reports support for cache_control.
    • Added a per-model modelCompatibility option to set or remove inferred cache-format compatibility.
  • Bug Fixes
    • Restored reasoning-effort variants and their inference constraints.
  • Documentation
    • Added guidance on prompt caching compatibility, overrides, and differences between OpenCode versions.

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
@coderabbitai

coderabbitai Bot commented Oct 19, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 4eb389dd-722c-46a1-8907-253b4f7b3edc
📥 Commits

Reviewing files that changed from the base of the PR and between 0d65275 and 9e6529d.

📒 Files selected for processing: 10
  • CHANGELOG.md
  • README.md
  • src/plugin/index.ts
  • src/plugin/v2.ts
  • src/utils/model-cache.ts
  • src/utils/model-compatibility.ts
  • test/model-cache.test.ts
  • test/model-compatibility.test.ts
  • test/plugin-v1.test.ts
  • test/plugin-v2.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

📝 Walkthrough

The plugin records per-model cache-control support during discovery and stores it in the model cache. OpenCode 2 provider construction uses the hints to infer Anthropic cache compatibility for eligible Chat Completions models. Per-model modelCompatibility settings can replace or remove the inferred format.

Changes: Model cache compatibility

Layer / File(s) Summary
Discover and persist model hints
src/utils/model-cache.ts, src/plugin/index.ts, test/model-cache.test.ts, test/plugin-v1.test.ts
Discovery records whether each model advertises cache_control. Cache version 4 stores and normalizes the hints. Tests cover hint persistence, malformed values, and cache refresh.
Resolve compatibility during provider construction
src/utils/model-compatibility.ts, src/plugin/v2.ts, test/model-compatibility.test.ts, test/plugin-v2.test.ts
Compatibility inference is limited to supported Chat Completions runtimes and models with cache-control hints. Per-model overrides can replace or remove the inferred format. Tests cover discovery, cached hints, configured providers, and refreshes.
Document cache compatibility
README.md, CHANGELOG.md
The README describes inference conditions, accepted override values, and the OpenCode 1 limitation. The changelog records the compatibility changes and modelCompatibility option.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Bug fix · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant LiteLLM
  participant discoverModels
  participant ModelCache
  participant toProviderModels
  LiteLLM->>discoverModels: model info with supported_openai_params
  discoverModels->>ModelCache: models and cache-control hints
  ModelCache->>toProviderModels: cached models and hints
  toProviderModels->>toProviderModels: infer compatibility and apply modelCompatibility override
Loading

Suggested reviewers: temeteke, alexanderwillner

Merge Risk: ⚪ Minimal · up to 9e652

The cache-compatibility change is ready to merge after normal checks; no actionable issue remains identified.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 18 functions across 8 files. (2 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly summarizes the main change: enabling prompt-caching compatibility for Claude models behind LiteLLM in the v2 plugin.
Linked Issues check Passed Issue #42 requires discovered Claude models behind LiteLLM to receive compatibility.cacheFormat: "anthropic", with an override for deployments without proxy metadata. The implementation records `sup…
Out of Scope Changes check Passed The changes stay within issue #42 scope. Discovery metadata, cache propagation and invalidation, runtime filtering, provider registration, documentation, and tests directly support reliable `cacheForm…
Full details: Linked Issues check

Explanation

Issue #42 requires discovered Claude models behind LiteLLM to receive compatibility.cacheFormat: "anthropic", with an override for deployments without proxy metadata. The implementation records supported_openai_params.cache_control in src/plugin/index.ts, carries the hint through cache version 4, and applies cacheFormat: "anthropic" in src/plugin/v2.ts for Chat Completions models. modelCompatibility supports exact model IDs with "anthropic", "alibaba", or null, and parsing validates these values. Responses models and non-Chat-Completions runtime packages are excluded. The added tests cover inference, cache reuse and refresh, runtime restrictions, and overrides.

Full details: Out of Scope Changes check

Explanation

The changes stay within issue #42 scope. Discovery metadata, cache propagation and invalidation, runtime filtering, provider registration, documentation, and tests directly support reliable cacheFormat registration and its override. No unrelated product behavior is demonstrated by the reviewed changes.

Full details: Docstring Coverage

Explanation

Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 18 functions across 8 files. (2 skipped: 2 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

  • Fix all pre-merge checks with AI
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@yuseferi
yuseferi merged commit d6f6e11 into yuseferi:main Oct 20, 2026
5 checks passed
github-actions Bot pushed a commit that referenced this pull request Oct 20, 2026
# [1.6.0](v1.5.0...v1.6.0) (2026-10-20)

### Features

* **v2:** enable prompt caching for Claude behind LiteLLM ([#44](#44)) ([d6f6e11](d6f6e11)), closes [#42](#42) [#42](#42)
yuseferi added a commit that referenced this pull request Oct 20, 2026
…#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>
github-actions Bot pushed a commit that referenced this pull request Oct 20, 2026
## [1.7.1](v1.7.0...v1.7.1) (2026-10-20)

### Bug Fixes

* prefer the user's saved provider credential over the pod env key ([#43](#43)) ([cab808f](cab808f)), closes [#44](#44) [#41](#41)
@mkowalski
mkowalski deleted the fix/anthropic-cache-format branch October 20, 2026 19:13
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.

OpenCode 2: Claude models via LiteLLM get no prompt caching (missing compatibility.cacheFormat)

2 participants