@khalilgharbaoui/opencode-claude-code-plugin 0.30.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -69,7 +69,7 @@ That package spec is the whole install. Do **not** `npm install` the package you
69
69
 
70
70
  Quit opencode fully and relaunch it: plugins are loaded once, at process start, so a reload is not enough.
71
71
 
72
- In the model picker you should now see a provider called **Claude Code (Default)** holding entries such as `Claude Haiku 4.5 (1×)`, `Claude Sonnet 5 (3×)` and `Claude Opus 5 (5×)`. The `(N×)` suffix is each model's list price relative to Haiku; see [Models](#models). Pick one and send a message.
72
+ In the model picker you should now see a provider called **Claude Code (Default)** holding entries such as `Claude Haiku 4.5 (1×)`, `Claude Sonnet 5.5 (2×)` and `Claude Opus 5 (5×)`. The `(N×)` suffix is each model's list price relative to Haiku; see [Models](#models). Pick one and send a message.
73
73
 
74
74
  If the provider does not appear, if the models are there but a message fails, or if a version you just upgraded to is missing, go to [Troubleshooting](#troubleshooting). It is keyed on the first thing you see and names one check per symptom.
75
75
 
@@ -122,14 +122,15 @@ Verified live on opencode **2.0.11** with Claude Code 2.1.280: chat turns, proxi
122
122
 
123
123
  ## Models
124
124
 
125
- The plugin auto-registers the following, and they appear in the model picker with no extra config: Haiku 4.5, Sonnet 4.5/4.6/5, Opus 4.5/4.6/4.7/4.8/5/5.5 (plus three fast-mode Opus entries), Fable 5/5.1 and Mythos 5/5.1, each except Haiku carrying `low` / `medium` / `high` / `xhigh` / `max` reasoning variants.
125
+ The plugin auto-registers the following, and they appear in the model picker with no extra config: Haiku 4.5, Sonnet 4.5/4.6/5/5.5, Opus 4.5/4.6/4.7/4.8/5/5.5 (plus three fast-mode Opus entries), Fable 5/5.1 and Mythos 5/5.1, each except Haiku carrying `low` / `medium` / `high` / `xhigh` / `max` reasoning variants.
126
126
 
127
127
  | ID | Display name | Context | Output | Reasoning variants | Price × |
128
128
  |---|---|---|---|---|---|
129
129
  | `claude-haiku-4-5` | Claude Haiku 4.5 | 200k | 64,000 | – | 1× |
130
130
  | `claude-sonnet-4-5` | Claude Sonnet 4.5 | 200k | 64,000 | low/medium/high/xhigh/max | 3× |
131
131
  | `claude-sonnet-4-6` | Claude Sonnet 4.6 | 1M | 128,000 | low/medium/high/xhigh/max | 3× |
132
- | `claude-sonnet-5` | Claude Sonnet 5 | 1M | 128,000 | low/medium/high/xhigh/max | 3× |
132
+ | `claude-sonnet-5` | Claude Sonnet 5 | 1M | 128,000 | low/medium/high/xhigh/max | 2× |
133
+ | `claude-sonnet-5-5` | Claude Sonnet 5.5 | 1M | 128,000 | low/medium/high/xhigh/max | 2× |
133
134
  | `claude-opus-4-5` | Claude Opus 4.5 | 200k | 64,000 | low/medium/high/xhigh/max | 5× |
134
135
  | `claude-opus-4-6` | Claude Opus 4.6 | 1M | 128,000 | low/medium/high/xhigh/max | 5× |
135
136
  | `claude-opus-4-7` | Claude Opus 4.7 | 1M | 128,000 | low/medium/high/xhigh/max | 5× |
@@ -148,10 +149,12 @@ The plugin auto-registers the following, and they appear in the model picker wit
148
149
 
149
150
  Capabilities for every model: text + image input, text output, tool use, attachments. No temperature control, no PDF/audio/video, no interleaved streaming.
150
151
 
151
- **Price ×** is each model's per-token list price relative to Haiku, the cheapest model. It's derived exactly from Anthropic's published pricing (input and output ratios both come out the same: Haiku $1/$5 = 1×, Sonnet $3/$15 = 3×, Opus 5.5 $4/$20 = 4×, Opus $5/$25 = 5×, Opus 5.5 fast mode $8/$40 = 8×, Fable/Mythos 5 and 5.1 / Opus 5 and 4.8 fast mode $10/$50 = 10×). So **Fable/Mythos 5 and 5.1, and fast-mode Opus 5 and 4.8, all cost 2× standard Opus 5**, and fast mode is 2× the standard price on every Opus that offers it. The same multiplier is shown as a `(N×)` suffix on the display name in opencode's model picker, since opencode has no dedicated multiplier field. On a flat Max/Pro subscription it doubles as a rough guide to how fast each model drains your usage limit.
152
+ **Price ×** is each model's per-token list price relative to Haiku, the cheapest model. It's derived exactly from Anthropic's published pricing (input and output ratios both come out the same: Haiku $1/$5 = 1×, Sonnet 5 and 5.5 $2/$10 = 2×, Sonnet 4.5/4.6 $3/$15 = 3×, Opus 5.5 $4/$20 = 4×, Opus $5/$25 = 5×, Opus 5.5 fast mode $8/$40 = 8×, Fable/Mythos 5 and 5.1 / Opus 5 and 4.8 fast mode $10/$50 = 10×). So **Fable/Mythos 5 and 5.1, and fast-mode Opus 5 and 4.8, all cost 2× standard Opus 5**, and fast mode is 2× the standard price on every Opus that offers it. The same multiplier is shown as a `(N×)` suffix on the display name in opencode's model picker, since opencode has no dedicated multiplier field. On a flat Max/Pro subscription it doubles as a rough guide to how fast each model drains your usage limit.
152
153
 
153
154
  Fable 5.1 and Mythos 5.1 keep the same $10/M input and $50/M output rates as 5.0, but cache reads cost $0.25/M instead of $1/M. Their cache-write rate remains $12.50/M.
154
155
 
156
+ Sonnet 5 and Sonnet 5.5 are $2/M input and $10/M output, with cache writes at $2.50/M and cache reads at $0.20/M. Sonnet 5's price was announced as introductory until 2026-08-31, but Anthropic cancelled the increase to $3/$15, so $2/$10 is its standard price. Sonnet 5.5 runs on any recent Claude Code, but **2.1.284 is the first release that knows it**. An older CLI still serves it, on fallback limits (a 200k context window instead of 1M, and an estimated cost). The plugin warns once when the CLI reports that, and `claude update` fixes it.
157
+
155
158
  Opus 5.5 is priced below the Opus line at $4/M input and $20/M output, with cache writes at $5/M and cache reads at $0.20/M (0.05× input rather than the usual 0.1×). It needs **Claude Code 2.1.280 or newer**: the API rejects it from an older CLI with a 400 naming that floor, which shows up as a failed turn.
156
159
 
157
160
  The model ID is passed straight through to `claude --model`, so anything Claude Code accepts works. The three `-fast` IDs are the one exception, described below.
@@ -909,7 +912,7 @@ One line at the end of a finished turn, from the numbers the CLI already reports
909
912
 
910
913
  - Never on a `/compact` turn (the footer would be appended to what opencode stores as the summary) and never on a turn that ended in error, where the error is the thing to read.
911
914
  - It is its own text part led by `▌ **stats:**`, and the plugin strips it again if the conversation is ever replayed into a fresh Claude Code process. The model never reads its own accounting.
912
- - Token counts are the turn's totals, which is what matches the cost. They are deliberately not the same numbers opencode's context gauge shows, which use the last tool-use iteration so the window is not inflated.
915
+ - Token counts are the turn's totals, which is what matches the cost. They are deliberately not what opencode records for the message: opencode reads that as context occupancy (its context gauge and its auto-compaction), so it gets the input and cache counts of the turn's last API call, plus the whole turn's output. opencode's own cost figure for a multi-call turn therefore counts only the last call's input and cache; the CLI's real cost is this line and `providerMetadata["claude-code"].costUsd`.
913
916
  - The cost is what the CLI reported for the turn, not a billing guarantee.
914
917
 
915
918
  The same numbers are logged at INFO whatever this option is set to, and `total_cost_usd`, `duration_ms`, `duration_api_ms`, `num_turns`, `usage`, `modelUsage` and `permission_denials` always reach `providerMetadata` (denials by tool name and id only, never their inputs).
@@ -1248,7 +1251,7 @@ The plugin respects the standard Claude Code thinking env vars. If you set them
1248
1251
  - **Smart incomplete-turn continuation.** By default, the plugin keeps the current opencode stream open and feeds Claude CLI a small internal continuation message when Claude emits a `result` after reasoning/tool activity without a useful visible answer. It still stops normally on final-looking answers, questions, blockers, errors, aborts, or internal safety-budget exhaustion. It also resumes an answer the model was cut off mid-sentence: a `max_tokens` stop means truncation rather than completion, so the turn continues instead of ending on half a sentence, capped at 8 attempts and 10 minutes. Every other stop reason is taken at face value. Disable with `"autoContinueIncompleteTurns": false`.
1249
1252
  - **`AskUserQuestion`** from the CLI is converted into plain text content rather than forwarded as a tool call — unless `"Question"` is in `proxyTools`, in which case it is routed through opencode's native `question` tool (see [AskUserQuestion](#askuserquestion)).
1250
1253
  - **Wire-inactivity watchdog.** Once the CLI has produced any content, the stream closes gracefully if stdout goes silent for 60 seconds without a `result` message arriving. Resets on every line received, so long mid-turn pauses (Sonnet between text-end and the next tool_use, for example) are tolerated. On a user-initiated abort, the watchdog shortens to 5 seconds.
1251
- - **Per-iteration usage.** When the CLI internally retries with tools, the plugin only counts the last iteration's usage so opencode's context accounting stays accurate.
1254
+ - **Context usage, not turn totals.** The CLI's `result` adds up every API call in a turn, and opencode reads a message's usage as how full the context is, so a tool-heavy turn looked several times its real size and triggered auto-compaction far below the window. The plugin reports the last API call's input and cache counts plus the turn's output instead. See [Per-turn stats](#per-turn-stats) for what that does to opencode's cost figure.
1252
1255
  - **Lazy `cwd`.** The working directory is re-resolved at every request, so opencode's project-aware behavior works without restarting the plugin.
1253
1256
  - **Variants survive merge.** opencode recalculates variant lists after the plugin loads; the plugin re-injects defaults into runtime config so your variants don't disappear.
1254
1257
 
@@ -1573,7 +1576,7 @@ This plugin absorbs work from its forks directly, cherry-picked with the origina
1573
1576
  | [@galvani](https://github.com/galvani) (Jan Kozak) | Per-session working directory for `opencode serve`, so one server spawns each project's `claude` in the right place. Also found the stale `toolCallMap` re-emission three months before it was fixed here. | `9e02ce4`, `2238ed0` |
1574
1577
  | [@HeikoAtGitHub](https://github.com/HeikoAtGitHub) | Stopped sending `AGENTS.md` to the model twice (opencode already forwards it). Independently diagnosed the 5-minute proxy wall. | `25260a4`, `42f426d` |
1575
1578
  | [@bernardofortes](https://github.com/bernardofortes) (Bernardo Fortes) | `idleProcessTimeoutMs`, idle eviction of retained `claude` workers. | `a5f723a` |
1576
- | [@broskees](https://github.com/broskees) (Joseph Roberts) | Task proxy default-on (PR #18), the abort `interrupt` so Esc really stops the CLI, the skill bridge, `task_batch` for concurrent subagents (and the measurement that the CLI serialises MCP calls), the undici 300 s diagnosis of the proxy wall, the lifecycle release of proxied calls that made the `task` deadline unnecessary (PR #36), and Claude Opus 5.5 with its fast-mode entry (PR #43). | PR #18, `68ed142`, PR #36, PR #43 |
1579
+ | [@broskees](https://github.com/broskees) (Joseph Roberts) | Task proxy default-on (PR #18), the abort `interrupt` so Esc really stops the CLI, the skill bridge, `task_batch` for concurrent subagents (and the measurement that the CLI serialises MCP calls), the undici 300 s diagnosis of the proxy wall, the lifecycle release of proxied calls that made the `task` deadline unnecessary (PR #36), Claude Opus 5.5 with its fast-mode entry (PR #43), and the fix for turn-summed usage that made opencode auto-compact far below the window (PR #63). | PR #18, `68ed142`, PR #36, PR #43, PR #63 |
1577
1580
  | [@jknlsn](https://github.com/jknlsn) (Jake Nelson) | Per-tool proxy timeouts, subagent dispatch steering, the question proxy, the start watchdog respawn. | `84f3db9`, `94980a6`, `47501d0`, `ffefc24` |
1578
1581
  | [@CollieIsCute](https://github.com/CollieIsCute) (Collie Tsai) | The plan-mode approval bridge. | `8c5b583` |
1579
1582
  | [@flupkede](https://github.com/flupkede) | The compress proxy tool design and the AI-SDK v4 image-part fix. | `4ac319f`, `60a6e9a` |
@@ -1581,6 +1584,7 @@ This plugin absorbs work from its forks directly, cherry-picked with the origina
1581
1584
  | [@willmcginnis](https://github.com/willmcginnis) | The proxy endpoint authentication (PR #28, GHSA-3mxm-w7gf-3c5x). | PR #28 |
1582
1585
  | [@nic-lan](https://github.com/nic-lan) | The issue #29 diagnosis of subagent output lost across the CLI resume boundary, and the fix for unattended output replaying as one text block per delta (PR #35). | #29, PR #35 |
1583
1586
  | [@acastro2](https://github.com/acastro2) (Alexandre Castro) | Found and fixed CLI tool results being emitted under a different name than their call, which made opencode 2 abort every turn that used a Claude-side MCP server (PR #46). | PR #46 |
1587
+ | [@bangnh1](https://github.com/bangnh1) | Independently found and diagnosed the turn-summed usage that tripped auto-compaction after a single prompt, measured on opencode 2 (PR #62; the fix landed as PR #63). | PR #62 |
1584
1588
  | [@JWebCoder](https://github.com/JWebCoder) (joao moura) | Diagnosed that auto-continue never fires on current CLIs (PR #15). | PR #15 |
1585
1589
 
1586
1590
  Commit hashes are on the contributors' forks where the work was cherry-picked; `git log --author` on this repo shows the preserved authorship.
package/dist/index.d.ts CHANGED
@@ -937,6 +937,8 @@ interface ClaudeStreamMessage {
937
937
  /** On a `tool_result` block: the CLI-executed tool failed. */
938
938
  is_error?: boolean;
939
939
  }>;
940
+ /** On an `assistant` frame: that one API call's usage, not the turn's. */
941
+ usage?: ClaudeStreamMessage["usage"];
940
942
  };
941
943
  apiKeySource?: string;
942
944
  permissionMode?: string;
package/dist/index.js CHANGED
@@ -736,6 +736,38 @@ function formatSilentTurnNote(hadReasoning) {
736
736
  ${SILENT_TURN_MARKER} ${what}, so there is no reply above. Nothing failed and nothing is pending: send the message again, or rephrase it.
737
737
  `;
738
738
  }
739
+ var UNRECOGNIZED_MODEL_STDERR_MARKER = "[claude-code:unrecognized_model]";
740
+ var MODEL_CLI_FLOORS = {
741
+ // "Added Claude Sonnet 5.5 (`claude-sonnet-5-5`)" (CHANGELOG, 2.1.284).
742
+ "claude-sonnet-5-5": "2.1.284"
743
+ };
744
+ function parseUnrecognizedModel(stderr) {
745
+ const at = stderr.indexOf(UNRECOGNIZED_MODEL_STDERR_MARKER);
746
+ if (at === -1) return null;
747
+ const rest = stderr.slice(at + UNRECOGNIZED_MODEL_STDERR_MARKER.length);
748
+ const line = rest.split("\n", 1)[0].trim();
749
+ try {
750
+ const payload = JSON.parse(line);
751
+ return { model: isRecord(payload) ? str(payload.model) : void 0 };
752
+ } catch {
753
+ return { model: void 0 };
754
+ }
755
+ }
756
+ var warnedUnrecognizedModels = /* @__PURE__ */ new Set();
757
+ function reportUnrecognizedModel(stderr) {
758
+ const parsed = parseUnrecognizedModel(stderr);
759
+ if (!parsed) return;
760
+ const model = parsed.model ?? "unknown";
761
+ const floor = parsed.model ? MODEL_CLI_FLOORS[parsed.model] : void 0;
762
+ const fix = floor ? `It needs Claude Code ${floor} or newer: run \`claude update\`.` : "Update Claude Code (`claude update`) to a release that knows it.";
763
+ const message = `Claude Code does not recognise the model "${model}". It still runs it, but on fallback limits (measured on 2.1.280: a 200k context window instead of 1M, and an estimated cost), so its own compaction may start early and turnStats' cost is approximate. ${fix}`;
764
+ if (warnedUnrecognizedModels.has(model)) {
765
+ log.debug(message, { model });
766
+ return;
767
+ }
768
+ warnedUnrecognizedModels.add(model);
769
+ log.warn(message, { model, ...floor ? { cliFloor: floor } : {} });
770
+ }
739
771
 
740
772
  // src/plan-mode-question.ts
741
773
  var QUESTION_TOOL_NAME = "question";
@@ -3507,6 +3539,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
3507
3539
  const stderr = data.toString();
3508
3540
  log.debug("stderr", { data: stderr.slice(0, 200) });
3509
3541
  retainStderr(ap, stderr);
3542
+ reportUnrecognizedModel(stderr);
3510
3543
  if (stderr.includes("No conversation found") || stderr.includes("Session ID") && (stderr.includes("already in use") || stderr.includes("not found") || stderr.includes("invalid"))) {
3511
3544
  if (activeProcesses.get(sessionKey2) === ap) {
3512
3545
  log.warn("claude session ID error, clearing session", {
@@ -5022,6 +5055,7 @@ function passthroughModel(id) {
5022
5055
  }
5023
5056
  var haikuCost = { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 };
5024
5057
  var sonnetCost = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 };
5058
+ var sonnet5Cost = { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 };
5025
5059
  var opusCost = { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 };
5026
5060
  var fableCost = { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 };
5027
5061
  var fable51Cost = { input: 10, output: 50, cacheRead: 0.25, cacheWrite: 12.5 };
@@ -5101,10 +5135,26 @@ var defaultModels = {
5101
5135
  reasoning: true,
5102
5136
  context: 1e6,
5103
5137
  output: 128e3,
5104
- cost: sonnetCost,
5105
- multiplier: 3,
5138
+ cost: sonnet5Cost,
5139
+ multiplier: 2,
5106
5140
  releaseDate: "2026-06-30"
5107
5141
  }),
5142
+ // Sonnet 5.5 runs on any recent Claude Code, but 2.1.284 is the first
5143
+ // release that knows it: an older CLI serves it on fallback limits (200k
5144
+ // context, an estimated cost) and says so on stderr, which
5145
+ // `reportUnrecognizedModel` turns into a warning. No `-fast` entry: the CLI
5146
+ // gates fast mode on Opus names.
5147
+ "claude-sonnet-5-5": defineModel({
5148
+ id: "claude-sonnet-5-5",
5149
+ name: "Claude Sonnet 5.5",
5150
+ family: "sonnet",
5151
+ reasoning: true,
5152
+ context: 1e6,
5153
+ output: 128e3,
5154
+ cost: sonnet5Cost,
5155
+ multiplier: 2,
5156
+ releaseDate: "2026-09-28"
5157
+ }),
5108
5158
  "claude-opus-4-5": defineModel({
5109
5159
  id: "claude-opus-4-5",
5110
5160
  name: "Claude Opus 4.5",
@@ -7764,6 +7814,17 @@ function toUsage(rawUsage) {
7764
7814
  raw: rawUsage
7765
7815
  };
7766
7816
  }
7817
+ function lastCallContextUsage(lastCallUsage, turnTotalUsage) {
7818
+ if (!lastCallUsage || !turnTotalUsage) return turnTotalUsage;
7819
+ const iterations = lastCallUsage.iterations;
7820
+ const context = iterations?.length ? iterations[iterations.length - 1] : lastCallUsage;
7821
+ return {
7822
+ input_tokens: context.input_tokens,
7823
+ cache_read_input_tokens: context.cache_read_input_tokens,
7824
+ cache_creation_input_tokens: context.cache_creation_input_tokens,
7825
+ output_tokens: turnTotalUsage.output_tokens
7826
+ };
7827
+ }
7767
7828
  function toFinishReason(reason = "stop") {
7768
7829
  return {
7769
7830
  unified: reason,
@@ -7855,6 +7916,7 @@ function createTurnState(init) {
7855
7916
  skipResultForIds: /* @__PURE__ */ new Set(),
7856
7917
  toolCallsById: /* @__PURE__ */ new Map(),
7857
7918
  resultMeta: {},
7919
+ lastCallUsage: void 0,
7858
7920
  resultFailure: void 0,
7859
7921
  accountLimitHit: null,
7860
7922
  accountBlock: null,
@@ -8264,7 +8326,8 @@ function finishWithToolCalls(state, calls) {
8264
8326
  state.controller.enqueue({
8265
8327
  type: "finish",
8266
8328
  finishReason: state.toFinishReason("tool-calls"),
8267
- usage: state.toUsage(state.resultMeta.usage),
8329
+ // No result yet (the usual mid-turn boundary) still reports nothing.
8330
+ usage: state.toUsage(lastCallContextUsage(state.lastCallUsage, state.resultMeta.usage)),
8268
8331
  providerMetadata: {
8269
8332
  "claude-code": state.resultMeta
8270
8333
  }
@@ -8295,7 +8358,7 @@ function finishWithQuestionCall(state, call) {
8295
8358
  state.controller.enqueue({
8296
8359
  type: "finish",
8297
8360
  finishReason: state.toFinishReason("tool-calls"),
8298
- usage: state.toUsage(state.resultMeta.usage),
8361
+ usage: state.toUsage(lastCallContextUsage(state.lastCallUsage, state.resultMeta.usage)),
8299
8362
  providerMetadata: {
8300
8363
  "claude-code": state.resultMeta
8301
8364
  }
@@ -8743,6 +8806,10 @@ ${plan}
8743
8806
  if (msg.type === "assistant" && msg.message && typeof msg.message.stop_reason === "string") {
8744
8807
  state.lastStopReason = msg.message.stop_reason;
8745
8808
  }
8809
+ const callUsage = msg.type === "assistant" ? msg.message?.usage : void 0;
8810
+ if (callUsage && (callUsage.input_tokens ?? 0) + (callUsage.cache_read_input_tokens ?? 0) + (callUsage.cache_creation_input_tokens ?? 0) > 0) {
8811
+ state.lastCallUsage = callUsage;
8812
+ }
8746
8813
  if (msg.type === "assistant" && msg.message?.content && state.gotPartialEvents) {
8747
8814
  const thinkingBlocks = msg.message.content.filter(
8748
8815
  (b) => b.type === "thinking"
@@ -10226,7 +10293,7 @@ var ClaudeCodeLanguageModel = class {
10226
10293
  controller.enqueue({
10227
10294
  type: "finish",
10228
10295
  finishReason: { unified: "error", raw: refusal.kind },
10229
- usage: toUsage2(msg.usage),
10296
+ usage: toUsage2(lastCallContextUsage(state.lastCallUsage, msg.usage)),
10230
10297
  providerMetadata: {
10231
10298
  "claude-code": { ...state.resultMeta, path: "model-fallback" }
10232
10299
  }
@@ -10274,19 +10341,23 @@ var ClaudeCodeLanguageModel = class {
10274
10341
  });
10275
10342
  state.endTextBlock();
10276
10343
  }
10344
+ const usage = toUsage2(lastCallContextUsage(state.lastCallUsage, msg.usage));
10277
10345
  controller.enqueue({
10278
10346
  type: "finish",
10279
10347
  finishReason: state.resultFailure ? { unified: "error", raw: state.resultFailure } : toFinishReason2("stop"),
10280
- usage: toUsage2(msg.usage),
10348
+ usage,
10281
10349
  providerMetadata: {
10282
10350
  "claude-code": {
10283
10351
  ...state.resultMeta,
10284
10352
  ...state.resultFailure ? { resultSubtype: state.resultFailure } : {},
10285
10353
  ...compactionMode ? { compactionModel: effectiveModelId } : {}
10286
10354
  },
10355
+ // opencode falls back to this when the usage has no cache
10356
+ // write (0 is sent as none), so it must match the usage, never
10357
+ // the turn total.
10287
10358
  ...typeof msg.usage?.cache_creation_input_tokens === "number" ? {
10288
10359
  anthropic: {
10289
- cacheCreationInputTokens: msg.usage.cache_creation_input_tokens
10360
+ cacheCreationInputTokens: usage.inputTokens.cacheWrite ?? 0
10290
10361
  }
10291
10362
  } : {}
10292
10363
  }