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

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.
@@ -887,6 +890,8 @@ It carries the startup-diagnostics fields (plugin version, opencode version, `cl
887
890
 
888
891
  When Claude Code refused an entry in an `--mcp-config` it was handed, an **MCP config entries Claude Code skipped** section names each one with the CLI's own category and sentence. That section only appears when there is something in it. It matters because a skipped server is absent from the CLI's server list entirely rather than listed as broken, so the model silently does not have those tools; if the skipped name is `opencode_proxy` the report says so plainly, because then it is the plugin's own server and every proxied tool call in the session fails. The same thing is a warning in your terminal when it happens.
889
892
 
893
+ A **Plugins Claude Code did not load** section works the same way for Claude plugins: one the CLI demoted at load time (for example, a dependency that is not installed) is absent from its plugin list, so its skills, commands and MCP servers are silently missing. The skill bridge is such a plugin (`opencode-skills`), and a failure there is reported as the plugin's own bug rather than your config. A plugin warning only counts when its content did not load; advisory feedback about a plugin that did load stays in the log at INFO.
894
+
890
895
  ```text
891
896
  /claude-code-doctor usage
892
897
  ```
@@ -909,7 +914,7 @@ One line at the end of a finished turn, from the numbers the CLI already reports
909
914
 
910
915
  - 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
916
  - 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.
917
+ - 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
918
  - The cost is what the CLI reported for the turn, not a billing guarantee.
914
919
 
915
920
  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 +1253,7 @@ The plugin respects the standard Claude Code thinking env vars. If you set them
1248
1253
  - **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
1254
  - **`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
1255
  - **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.
1256
+ - **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
1257
  - **Lazy `cwd`.** The working directory is re-resolved at every request, so opencode's project-aware behavior works without restarting the plugin.
1253
1258
  - **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
1259
 
@@ -1573,7 +1578,7 @@ This plugin absorbs work from its forks directly, cherry-picked with the origina
1573
1578
  | [@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
1579
  | [@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
1580
  | [@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 |
1581
+ | [@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
1582
  | [@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
1583
  | [@CollieIsCute](https://github.com/CollieIsCute) (Collie Tsai) | The plan-mode approval bridge. | `8c5b583` |
1579
1584
  | [@flupkede](https://github.com/flupkede) | The compress proxy tool design and the AI-SDK v4 image-part fix. | `4ac319f`, `60a6e9a` |
@@ -1581,6 +1586,7 @@ This plugin absorbs work from its forks directly, cherry-picked with the origina
1581
1586
  | [@willmcginnis](https://github.com/willmcginnis) | The proxy endpoint authentication (PR #28, GHSA-3mxm-w7gf-3c5x). | PR #28 |
1582
1587
  | [@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
1588
  | [@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 |
1589
+ | [@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
1590
  | [@JWebCoder](https://github.com/JWebCoder) (joao moura) | Diagnosed that auto-continue never fires on current CLIs (PR #15). | PR #15 |
1585
1591
 
1586
1592
  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
@@ -415,6 +415,7 @@ var DEFAULT_PROXY_TOOL_NAMES = [
415
415
  "Task"
416
416
  ];
417
417
  var PROXY_MCP_SERVER_NAME = "opencode_proxy";
418
+ var SKILL_PLUGIN_NAME = "opencode-skills";
418
419
 
419
420
  // src/cli-events.ts
420
421
  function isRecord(value) {
@@ -595,9 +596,44 @@ function parseSystemInit(msg) {
595
596
  cliVersion: str(raw.claude_code_version),
596
597
  toolCount: Array.isArray(raw.tools) ? raw.tools.length : 0,
597
598
  mcpServers: servers,
598
- mcpServerErrors: parseMcpServerErrors(msg)
599
+ mcpServerErrors: parseMcpServerErrors(msg),
600
+ pluginErrors: parsePluginDiagnostics(raw.plugin_errors),
601
+ pluginWarnings: parsePluginDiagnostics(raw.plugin_warnings),
602
+ loadedPlugins: parseLoadedPlugins(raw.plugins)
599
603
  };
600
604
  }
605
+ function parsePluginDiagnostics(raw) {
606
+ if (!Array.isArray(raw)) return [];
607
+ const diagnostics = [];
608
+ for (const entry of raw) {
609
+ if (!isRecord(entry)) continue;
610
+ diagnostics.push({
611
+ plugin: str(entry.plugin) ?? "unknown",
612
+ type: str(entry.type) ?? "unknown",
613
+ message: str(entry.message) ?? ""
614
+ });
615
+ }
616
+ return diagnostics;
617
+ }
618
+ function parseLoadedPlugins(raw) {
619
+ if (!Array.isArray(raw)) return [];
620
+ const loaded = [];
621
+ for (const entry of raw) {
622
+ if (!isRecord(entry)) continue;
623
+ for (const id of [str(entry.source), str(entry.name)]) {
624
+ if (id && !loaded.includes(id)) loaded.push(id);
625
+ }
626
+ }
627
+ return loaded;
628
+ }
629
+ function describePluginLoadFailure(diagnostic) {
630
+ const detail = diagnostic.message ? ` Claude Code said: ${diagnostic.message}` : "";
631
+ const name = diagnostic.plugin.split("@", 1)[0];
632
+ if (name === SKILL_PLUGIN_NAME) {
633
+ return `Claude Code did not load the plugin's own skill bridge "${diagnostic.plugin}" (${diagnostic.type}), so the opencode skills it bridges are missing this session. This is a plugin bug or a corrupted scratch directory rather than your config: report it with this line.${detail}`;
634
+ }
635
+ return `Claude Code did not load plugin "${diagnostic.plugin}" (${diagnostic.type}), so its skills, commands and MCP servers are missing this session.${detail}`;
636
+ }
601
637
  function apiKeySourceWarning(apiKeySource, ignoreAnthropicApiKey) {
602
638
  if (!apiKeySource || !API_KEY_SOURCES.has(apiKeySource)) return null;
603
639
  const base = `Claude Code authenticated with an API key (apiKeySource: ${apiKeySource}), so these turns bill as pay-as-you-go API usage instead of your subscription.`;
@@ -607,9 +643,32 @@ var warnedApiKeySources = /* @__PURE__ */ new Set();
607
643
  var warnedMcpFailures = /* @__PURE__ */ new Set();
608
644
  var warnedMcpSkips = /* @__PURE__ */ new Set();
609
645
  var lastMcpServerErrors = /* @__PURE__ */ new Map();
646
+ var warnedPluginDiagnostics = /* @__PURE__ */ new Set();
647
+ var lastPluginLoadFailures = /* @__PURE__ */ new Map();
610
648
  function snapshotMcpServerErrors() {
611
649
  return [...lastMcpServerErrors.values()];
612
650
  }
651
+ function snapshotPluginLoadFailures() {
652
+ return [...lastPluginLoadFailures.values()];
653
+ }
654
+ function reportPluginDiagnostics(info) {
655
+ const report = (kind, diagnostic) => {
656
+ const loaded = kind === "warning" && info.loadedPlugins.includes(diagnostic.plugin);
657
+ const message = loaded ? `Claude Code has advisory feedback for plugin "${diagnostic.plugin}" (${diagnostic.type}): ${diagnostic.message}` : describePluginLoadFailure(diagnostic);
658
+ if (!loaded) lastPluginLoadFailures.set(diagnostic.plugin, { kind, ...diagnostic });
659
+ const key = `${kind}:${diagnostic.plugin}:${diagnostic.type}`;
660
+ const data = { plugin: diagnostic.plugin, type: diagnostic.type, kind };
661
+ if (warnedPluginDiagnostics.has(key)) {
662
+ log.debug(message, data);
663
+ return;
664
+ }
665
+ warnedPluginDiagnostics.add(key);
666
+ if (loaded) log.info(message, data);
667
+ else log.warn(message, data);
668
+ };
669
+ for (const diagnostic of info.pluginErrors) report("error", diagnostic);
670
+ for (const diagnostic of info.pluginWarnings) report("warning", diagnostic);
671
+ }
613
672
  function reportSystemInit(msg, options = {}) {
614
673
  const info = parseSystemInit(msg);
615
674
  if (!info) return;
@@ -620,8 +679,11 @@ function reportSystemInit(msg, options = {}) {
620
679
  cliVersion: info.cliVersion ?? null,
621
680
  tools: info.toolCount,
622
681
  mcpServers: info.mcpServers,
623
- mcpServerErrors: info.mcpServerErrors
682
+ mcpServerErrors: info.mcpServerErrors,
683
+ pluginErrors: info.pluginErrors,
684
+ pluginWarnings: info.pluginWarnings
624
685
  });
686
+ reportPluginDiagnostics(info);
625
687
  for (const error of info.mcpServerErrors) {
626
688
  lastMcpServerErrors.set(error.name, error);
627
689
  const key2 = `${error.name}:${error.type}`;
@@ -736,6 +798,38 @@ function formatSilentTurnNote(hadReasoning) {
736
798
  ${SILENT_TURN_MARKER} ${what}, so there is no reply above. Nothing failed and nothing is pending: send the message again, or rephrase it.
737
799
  `;
738
800
  }
801
+ var UNRECOGNIZED_MODEL_STDERR_MARKER = "[claude-code:unrecognized_model]";
802
+ var MODEL_CLI_FLOORS = {
803
+ // "Added Claude Sonnet 5.5 (`claude-sonnet-5-5`)" (CHANGELOG, 2.1.284).
804
+ "claude-sonnet-5-5": "2.1.284"
805
+ };
806
+ function parseUnrecognizedModel(stderr) {
807
+ const at = stderr.indexOf(UNRECOGNIZED_MODEL_STDERR_MARKER);
808
+ if (at === -1) return null;
809
+ const rest = stderr.slice(at + UNRECOGNIZED_MODEL_STDERR_MARKER.length);
810
+ const line = rest.split("\n", 1)[0].trim();
811
+ try {
812
+ const payload = JSON.parse(line);
813
+ return { model: isRecord(payload) ? str(payload.model) : void 0 };
814
+ } catch {
815
+ return { model: void 0 };
816
+ }
817
+ }
818
+ var warnedUnrecognizedModels = /* @__PURE__ */ new Set();
819
+ function reportUnrecognizedModel(stderr) {
820
+ const parsed = parseUnrecognizedModel(stderr);
821
+ if (!parsed) return;
822
+ const model = parsed.model ?? "unknown";
823
+ const floor = parsed.model ? MODEL_CLI_FLOORS[parsed.model] : void 0;
824
+ const fix = floor ? `It needs Claude Code ${floor} or newer: run \`claude update\`.` : "Update Claude Code (`claude update`) to a release that knows it.";
825
+ 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}`;
826
+ if (warnedUnrecognizedModels.has(model)) {
827
+ log.debug(message, { model });
828
+ return;
829
+ }
830
+ warnedUnrecognizedModels.add(model);
831
+ log.warn(message, { model, ...floor ? { cliFloor: floor } : {} });
832
+ }
739
833
 
740
834
  // src/plan-mode-question.ts
741
835
  var QUESTION_TOOL_NAME = "question";
@@ -3507,6 +3601,7 @@ function spawnClaudeProcess(cliPath, cliArgs, cwd, sessionKey2, proxyServer, mcp
3507
3601
  const stderr = data.toString();
3508
3602
  log.debug("stderr", { data: stderr.slice(0, 200) });
3509
3603
  retainStderr(ap, stderr);
3604
+ reportUnrecognizedModel(stderr);
3510
3605
  if (stderr.includes("No conversation found") || stderr.includes("Session ID") && (stderr.includes("already in use") || stderr.includes("not found") || stderr.includes("invalid"))) {
3511
3606
  if (activeProcesses.get(sessionKey2) === ap) {
3512
3607
  log.warn("claude session ID error, clearing session", {
@@ -4836,6 +4931,18 @@ function formatDoctorReport(report) {
4836
4931
  lines.push("");
4837
4932
  lines.push("A skipped server is missing from the model's tools with no other sign of it.");
4838
4933
  }
4934
+ if (report.pluginLoadFailures.length > 0) {
4935
+ lines.push("");
4936
+ lines.push("**Plugins Claude Code did not load**");
4937
+ lines.push("");
4938
+ lines.push("| plugin | kind | category | Claude Code said |");
4939
+ lines.push("|---|---|---|---|");
4940
+ for (const failure of report.pluginLoadFailures) {
4941
+ lines.push(
4942
+ `| ${failure.plugin} | ${failure.kind} | \`${failure.type}\` | ${failure.message || "no detail"} |`
4943
+ );
4944
+ }
4945
+ }
4839
4946
  lines.push("");
4840
4947
  lines.push("**Plan usage**");
4841
4948
  lines.push("");
@@ -4934,6 +5041,7 @@ async function gatherDoctorReport(options) {
4934
5041
  pendingCalls: snapshotPendingProxyCalls(),
4935
5042
  proxyServers,
4936
5043
  mcpServerErrors: snapshotMcpServerErrors(),
5044
+ pluginLoadFailures: snapshotPluginLoadFailures(),
4937
5045
  planUsage
4938
5046
  };
4939
5047
  }
@@ -4948,6 +5056,7 @@ async function buildDoctorReport(options) {
4948
5056
  pendingCalls: report.pendingCalls.length,
4949
5057
  proxyServers: report.proxyServers.map((server2) => server2.auth.status),
4950
5058
  mcpServerErrors: report.mcpServerErrors.length,
5059
+ pluginLoadFailures: report.pluginLoadFailures.length,
4951
5060
  planUsage: report.planUsage.status
4952
5061
  });
4953
5062
  return formatDoctorReport(report);
@@ -5022,6 +5131,7 @@ function passthroughModel(id) {
5022
5131
  }
5023
5132
  var haikuCost = { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 };
5024
5133
  var sonnetCost = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 };
5134
+ var sonnet5Cost = { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 };
5025
5135
  var opusCost = { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 };
5026
5136
  var fableCost = { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 };
5027
5137
  var fable51Cost = { input: 10, output: 50, cacheRead: 0.25, cacheWrite: 12.5 };
@@ -5101,10 +5211,26 @@ var defaultModels = {
5101
5211
  reasoning: true,
5102
5212
  context: 1e6,
5103
5213
  output: 128e3,
5104
- cost: sonnetCost,
5105
- multiplier: 3,
5214
+ cost: sonnet5Cost,
5215
+ multiplier: 2,
5106
5216
  releaseDate: "2026-06-30"
5107
5217
  }),
5218
+ // Sonnet 5.5 runs on any recent Claude Code, but 2.1.284 is the first
5219
+ // release that knows it: an older CLI serves it on fallback limits (200k
5220
+ // context, an estimated cost) and says so on stderr, which
5221
+ // `reportUnrecognizedModel` turns into a warning. No `-fast` entry: the CLI
5222
+ // gates fast mode on Opus names.
5223
+ "claude-sonnet-5-5": defineModel({
5224
+ id: "claude-sonnet-5-5",
5225
+ name: "Claude Sonnet 5.5",
5226
+ family: "sonnet",
5227
+ reasoning: true,
5228
+ context: 1e6,
5229
+ output: 128e3,
5230
+ cost: sonnet5Cost,
5231
+ multiplier: 2,
5232
+ releaseDate: "2026-09-28"
5233
+ }),
5108
5234
  "claude-opus-4-5": defineModel({
5109
5235
  id: "claude-opus-4-5",
5110
5236
  name: "Claude Opus 4.5",
@@ -6028,7 +6154,6 @@ import * as fs5 from "fs";
6028
6154
  import * as os3 from "os";
6029
6155
  import * as path7 from "path";
6030
6156
  import { fileURLToPath as fileURLToPath2 } from "url";
6031
- var SKILL_PLUGIN_NAME = "opencode-skills";
6032
6157
  function bundledSkillsDir() {
6033
6158
  try {
6034
6159
  const here = fileURLToPath2(import.meta.url);
@@ -7764,6 +7889,17 @@ function toUsage(rawUsage) {
7764
7889
  raw: rawUsage
7765
7890
  };
7766
7891
  }
7892
+ function lastCallContextUsage(lastCallUsage, turnTotalUsage) {
7893
+ if (!lastCallUsage || !turnTotalUsage) return turnTotalUsage;
7894
+ const iterations = lastCallUsage.iterations;
7895
+ const context = iterations?.length ? iterations[iterations.length - 1] : lastCallUsage;
7896
+ return {
7897
+ input_tokens: context.input_tokens,
7898
+ cache_read_input_tokens: context.cache_read_input_tokens,
7899
+ cache_creation_input_tokens: context.cache_creation_input_tokens,
7900
+ output_tokens: turnTotalUsage.output_tokens
7901
+ };
7902
+ }
7767
7903
  function toFinishReason(reason = "stop") {
7768
7904
  return {
7769
7905
  unified: reason,
@@ -7855,6 +7991,7 @@ function createTurnState(init) {
7855
7991
  skipResultForIds: /* @__PURE__ */ new Set(),
7856
7992
  toolCallsById: /* @__PURE__ */ new Map(),
7857
7993
  resultMeta: {},
7994
+ lastCallUsage: void 0,
7858
7995
  resultFailure: void 0,
7859
7996
  accountLimitHit: null,
7860
7997
  accountBlock: null,
@@ -8264,7 +8401,8 @@ function finishWithToolCalls(state, calls) {
8264
8401
  state.controller.enqueue({
8265
8402
  type: "finish",
8266
8403
  finishReason: state.toFinishReason("tool-calls"),
8267
- usage: state.toUsage(state.resultMeta.usage),
8404
+ // No result yet (the usual mid-turn boundary) still reports nothing.
8405
+ usage: state.toUsage(lastCallContextUsage(state.lastCallUsage, state.resultMeta.usage)),
8268
8406
  providerMetadata: {
8269
8407
  "claude-code": state.resultMeta
8270
8408
  }
@@ -8295,7 +8433,7 @@ function finishWithQuestionCall(state, call) {
8295
8433
  state.controller.enqueue({
8296
8434
  type: "finish",
8297
8435
  finishReason: state.toFinishReason("tool-calls"),
8298
- usage: state.toUsage(state.resultMeta.usage),
8436
+ usage: state.toUsage(lastCallContextUsage(state.lastCallUsage, state.resultMeta.usage)),
8299
8437
  providerMetadata: {
8300
8438
  "claude-code": state.resultMeta
8301
8439
  }
@@ -8743,6 +8881,10 @@ ${plan}
8743
8881
  if (msg.type === "assistant" && msg.message && typeof msg.message.stop_reason === "string") {
8744
8882
  state.lastStopReason = msg.message.stop_reason;
8745
8883
  }
8884
+ const callUsage = msg.type === "assistant" ? msg.message?.usage : void 0;
8885
+ if (callUsage && (callUsage.input_tokens ?? 0) + (callUsage.cache_read_input_tokens ?? 0) + (callUsage.cache_creation_input_tokens ?? 0) > 0) {
8886
+ state.lastCallUsage = callUsage;
8887
+ }
8746
8888
  if (msg.type === "assistant" && msg.message?.content && state.gotPartialEvents) {
8747
8889
  const thinkingBlocks = msg.message.content.filter(
8748
8890
  (b) => b.type === "thinking"
@@ -10226,7 +10368,7 @@ var ClaudeCodeLanguageModel = class {
10226
10368
  controller.enqueue({
10227
10369
  type: "finish",
10228
10370
  finishReason: { unified: "error", raw: refusal.kind },
10229
- usage: toUsage2(msg.usage),
10371
+ usage: toUsage2(lastCallContextUsage(state.lastCallUsage, msg.usage)),
10230
10372
  providerMetadata: {
10231
10373
  "claude-code": { ...state.resultMeta, path: "model-fallback" }
10232
10374
  }
@@ -10274,19 +10416,23 @@ var ClaudeCodeLanguageModel = class {
10274
10416
  });
10275
10417
  state.endTextBlock();
10276
10418
  }
10419
+ const usage = toUsage2(lastCallContextUsage(state.lastCallUsage, msg.usage));
10277
10420
  controller.enqueue({
10278
10421
  type: "finish",
10279
10422
  finishReason: state.resultFailure ? { unified: "error", raw: state.resultFailure } : toFinishReason2("stop"),
10280
- usage: toUsage2(msg.usage),
10423
+ usage,
10281
10424
  providerMetadata: {
10282
10425
  "claude-code": {
10283
10426
  ...state.resultMeta,
10284
10427
  ...state.resultFailure ? { resultSubtype: state.resultFailure } : {},
10285
10428
  ...compactionMode ? { compactionModel: effectiveModelId } : {}
10286
10429
  },
10430
+ // opencode falls back to this when the usage has no cache
10431
+ // write (0 is sent as none), so it must match the usage, never
10432
+ // the turn total.
10287
10433
  ...typeof msg.usage?.cache_creation_input_tokens === "number" ? {
10288
10434
  anthropic: {
10289
- cacheCreationInputTokens: msg.usage.cache_creation_input_tokens
10435
+ cacheCreationInputTokens: usage.inputTokens.cacheWrite ?? 0
10290
10436
  }
10291
10437
  } : {}
10292
10438
  }