@xtruder/opencode-claude-max-plugin 0.3.1 → 0.4.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
@@ -27,7 +27,7 @@ Add the plugin to your `opencode.json` (project-level or `~/.config/opencode/ope
27
27
 
28
28
  That's it. The plugin self-registers the `anthropic-sdk` provider and its models (Haiku 4.5, Sonnet 4.6, Opus 4.6, Opus 4.7, Opus 4.8) at startup via the OpenCode config hook. No separate `provider` block is needed.
29
29
 
30
- Then open OpenCode and run `/connect` → Other → `anthropic-sdk`. If Claude Code is installed and you're logged in, credentials are read automatically from `~/.claude/.credentials.json` — no API key needed.
30
+ Then open OpenCode and models will automatically be available under `anthropic-sdk` provider.
31
31
 
32
32
  ### TUI Plugin (sidebar + /usage command)
33
33
 
package/build/index.d.ts CHANGED
@@ -1,14 +1,22 @@
1
1
  import type { LanguageModelV3 } from "@ai-sdk/provider";
2
2
  import type { Plugin } from "@opencode-ai/plugin";
3
3
  export declare const CLAUDE_CODE_SYSTEM_PROMPT: string;
4
- export declare const CLAUDE_CODE_OPUS48_SYSTEM_PROMPT: string;
4
+ export declare const CLAUDE_CODE_NEW_SYSTEM_PROMPT: string;
5
5
  /**
6
6
  * Select the Claude Code system prompt that matches the given model.
7
7
  *
8
- * Claude Code 2.1.154 ships per-model prompts: Opus 4.8 gets a condensed
9
- * ~6KB "Harness" prompt that trusts the model's defaults; everything else
10
- * (Opus 4.7, Sonnet 4.6, Haiku 4.5, Opus 4.6) gets the long-form ~11KB
11
- * prompt with explicit behavior rules.
8
+ * Claude Code ships per-model prompts: the newer flagships (Opus 4.8,
9
+ * Fable 5) get a condensed "Harness" prompt that trusts the model's
10
+ * defaults; everything else (Opus 4.7, Sonnet 4.6, Haiku 4.5, Opus 4.6)
11
+ * gets the long-form prompt with explicit behavior rules.
12
+ *
13
+ * Opus 4.8 and Fable 5 share one condensed prompt here. Claude Code 2.1.173
14
+ * sends Fable 5 a longer variant (security steering, a verbose
15
+ * "Communicating" section, a Fable 5 identity blurb, and CLI-only session
16
+ * items), but the safety classifiers are server-side rather than
17
+ * prompt-driven and the CLI-specific guidance does not apply to OpenCode —
18
+ * so we distill it down to the shared base. The verbatim capture lives in
19
+ * src/fixtures/claudecode-system-fable5-original.txt.
12
20
  */
13
21
  export declare function selectClaudePromptForModel(modelId: string): string;
14
22
  export interface AnthropicSDKProviderOptions {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AACvD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AASjD,eAAO,MAAM,yBAAyB,QAA4B,CAAA;AAClE,eAAO,MAAM,gCAAgC,QAA8B,CAAA;AAE3E;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAGlE;AAqND,MAAM,WAAW,2BAA2B;IAC1C;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAEhC;;OAEG;IACH,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;IAE/B;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IAEb;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IAExB;;OAEG;IACH,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAED,MAAM,WAAW,oBAAoB;IACnC;;OAEG;IACH,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,CAAA;CAChD;AA4BD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,GAAE,2BAAgC,GACxC,oBAAoB,CAqJtB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,kBAAkB,EAAE,MAoDhC,CAAA;AAID,eAAe,kBAAkB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AACvD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAA;AASjD,eAAO,MAAM,yBAAyB,QAA4B,CAAA;AAClE,eAAO,MAAM,6BAA6B,QAA2B,CAAA;AAErE;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAKlE;AAwPD,MAAM,WAAW,2BAA2B;IAC1C;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;IAEf;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAA;IAEhB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAEhC;;OAEG;IACH,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAA;IAE/B;;OAEG;IACH,IAAI,CAAC,EAAE,MAAM,CAAA;IAEb;;;OAGG;IACH,eAAe,CAAC,EAAE,MAAM,CAAA;IAExB;;OAEG;IACH,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;CACvB;AAED,MAAM,WAAW,oBAAoB;IACnC;;OAEG;IACH,aAAa,CAAC,OAAO,EAAE,MAAM,GAAG,eAAe,CAAA;CAChD;AA4BD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,OAAO,GAAE,2BAAgC,GACxC,oBAAoB,CA6KtB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,kBAAkB,EAAE,MAwEhC,CAAA;AAID,eAAe,kBAAkB,CAAA"}
package/build/index.js CHANGED
@@ -48,8 +48,8 @@ function hasCchPlaceholder(body) {
48
48
  return block?.text?.includes("cch=00000") ?? false;
49
49
  }
50
50
 
51
- // src/claudecode-system-opus48.txt
52
- var claudecode_system_opus48_default = `
51
+ // src/claudecode-system-new.txt
52
+ var claudecode_system_new_default = `
53
53
  You are an interactive agent that helps users with software engineering tasks.
54
54
 
55
55
  # Harness
@@ -382,6 +382,14 @@ function convertUserMessage(message) {
382
382
  function convertAssistantMessage(message) {
383
383
  const content = [];
384
384
  for (const part of message.content) {
385
+ const fallback = part.providerMetadata?.anthropic?.fallback ?? part.providerOptions?.anthropic?.fallback;
386
+ if (fallback?.from?.model && fallback?.to?.model) {
387
+ content.push({
388
+ type: "fallback",
389
+ from: { model: fallback.from.model },
390
+ to: { model: fallback.to.model }
391
+ });
392
+ }
385
393
  switch (part.type) {
386
394
  case "text":
387
395
  if (part.text.length > 0) {
@@ -460,6 +468,14 @@ var idCounter = 0;
460
468
  function generateId() {
461
469
  return `block-${Date.now()}-${idCounter++}`;
462
470
  }
471
+ function buildRefusalError(stopDetails, fallbackModel) {
472
+ const category = stopDetails?.category ?? null;
473
+ const explanation = stopDetails?.explanation ?? null;
474
+ const categoryNote = category ? ` (category: ${category})` : "";
475
+ const detail = explanation ? explanation : "Claude Fable 5's safety classifiers declined this request.";
476
+ const hint = fallbackModel ? `The configured fallback model (${fallbackModel}) also refused.` : `Retry with a fallback model such as anthropic-sdk/claude-opus-4-8.`;
477
+ return new Error(`Claude Fable 5 refused this request${categoryNote}. ${detail} ${hint}`);
478
+ }
463
479
  function mapFinishReason(stopReason) {
464
480
  const unified = (() => {
465
481
  switch (stopReason) {
@@ -476,8 +492,13 @@ function mapFinishReason(stopReason) {
476
492
  })();
477
493
  return { unified, raw: stopReason ?? undefined };
478
494
  }
479
- function convertStream(anthropicStream, modelId) {
495
+ function convertStream(anthropicStream, modelId, context) {
480
496
  const blockStates = new Map;
497
+ const ctx = {
498
+ apiModelId: context?.apiModelId ?? modelId,
499
+ fallbacksEnabled: context?.fallbacksEnabled ?? false,
500
+ fallbackModel: context?.fallbackModel
501
+ };
481
502
  let inputTokens;
482
503
  let outputTokens;
483
504
  let cachedInputTokens;
@@ -486,7 +507,7 @@ function convertStream(anthropicStream, modelId) {
486
507
  async start(controller) {
487
508
  try {
488
509
  for await (const event of anthropicStream) {
489
- const parts = processEvent(event, blockStates, modelId);
510
+ const parts = processEvent(event, blockStates, modelId, ctx);
490
511
  if (event.type === "message_start" && event.message.usage) {
491
512
  const u = event.message.usage;
492
513
  inputTokens = u.input_tokens;
@@ -496,6 +517,12 @@ function convertStream(anthropicStream, modelId) {
496
517
  if (event.type === "message_delta") {
497
518
  const delta = event;
498
519
  outputTokens = delta.usage?.output_tokens;
520
+ if (delta.delta?.stop_reason === "refusal") {
521
+ controller.enqueue({
522
+ type: "error",
523
+ error: buildRefusalError(delta.delta?.stop_details, ctx.fallbacksEnabled ? ctx.fallbackModel : undefined)
524
+ });
525
+ }
499
526
  const finishReason = mapFinishReason(delta.delta?.stop_reason);
500
527
  controller.enqueue({
501
528
  type: "finish",
@@ -532,10 +559,16 @@ function convertStream(anthropicStream, modelId) {
532
559
  }
533
560
  });
534
561
  }
535
- function processEvent(event, blockStates, modelId) {
562
+ function processEvent(event, blockStates, modelId, ctx) {
536
563
  const parts = [];
537
564
  switch (event.type) {
538
565
  case "message_start": {
566
+ const servedModel = event.message.model;
567
+ if (ctx.fallbacksEnabled && servedModel && servedModel !== ctx.apiModelId) {
568
+ ctx.pendingMeta = {
569
+ servedBy: { from: ctx.apiModelId, to: servedModel, kind: "sticky" }
570
+ };
571
+ }
539
572
  parts.push({
540
573
  type: "response-metadata",
541
574
  id: event.message.id,
@@ -547,14 +580,38 @@ function processEvent(event, blockStates, modelId) {
547
580
  case "content_block_start": {
548
581
  const index = event.index;
549
582
  const block = event.content_block;
550
- if (block.type === "text") {
583
+ const takeMeta = () => {
584
+ const meta = ctx.pendingMeta;
585
+ ctx.pendingMeta = undefined;
586
+ return meta;
587
+ };
588
+ if (block.type === "fallback") {
589
+ ctx.pendingMeta = {
590
+ fallback: { from: block.from, to: block.to },
591
+ servedBy: {
592
+ from: block.from?.model ?? ctx.apiModelId,
593
+ to: block.to?.model ?? "unknown",
594
+ kind: "fallback"
595
+ }
596
+ };
597
+ } else if (block.type === "text") {
551
598
  const id = generateId();
552
- blockStates.set(index, { type: "text", id });
553
- parts.push({ type: "text-start", id });
599
+ const meta = takeMeta();
600
+ blockStates.set(index, { type: "text", id, fallbackMeta: meta });
601
+ parts.push({
602
+ type: "text-start",
603
+ id,
604
+ ...meta ? { providerMetadata: { anthropic: meta } } : {}
605
+ });
554
606
  } else if (block.type === "thinking") {
555
607
  const id = generateId();
556
- blockStates.set(index, { type: "thinking", id });
557
- parts.push({ type: "reasoning-start", id });
608
+ const meta = takeMeta();
609
+ blockStates.set(index, { type: "thinking", id, fallbackMeta: meta });
610
+ parts.push({
611
+ type: "reasoning-start",
612
+ id,
613
+ ...meta ? { providerMetadata: { anthropic: meta } } : {}
614
+ });
558
615
  } else if (block.type === "tool_use") {
559
616
  const toolId = block.id;
560
617
  const toolName = toOpencodeToolName(block.name);
@@ -563,7 +620,8 @@ function processEvent(event, blockStates, modelId) {
563
620
  id: toolId,
564
621
  toolUseId: toolId,
565
622
  toolName,
566
- argsText: ""
623
+ argsText: "",
624
+ fallbackMeta: takeMeta()
567
625
  });
568
626
  parts.push({
569
627
  type: "tool-input-start",
@@ -611,12 +669,14 @@ function processEvent(event, blockStates, modelId) {
611
669
  if (state.type === "text") {
612
670
  parts.push({ type: "text-end", id: state.id });
613
671
  } else if (state.type === "thinking") {
672
+ const meta = {
673
+ ...state.signature ? { signature: state.signature } : {},
674
+ ...state.fallbackMeta
675
+ };
614
676
  parts.push({
615
677
  type: "reasoning-end",
616
678
  id: state.id,
617
- ...state.signature ? {
618
- providerMetadata: { anthropic: { signature: state.signature } }
619
- } : {}
679
+ ...Object.keys(meta).length > 0 ? { providerMetadata: { anthropic: meta } } : {}
620
680
  });
621
681
  } else if (state.type === "tool_use") {
622
682
  parts.push({ type: "tool-input-end", id: state.id });
@@ -624,7 +684,8 @@ function processEvent(event, blockStates, modelId) {
624
684
  type: "tool-call",
625
685
  toolCallId: state.toolUseId,
626
686
  toolName: state.toolName,
627
- input: state.argsText || "{}"
687
+ input: state.argsText || "{}",
688
+ ...state.fallbackMeta ? { providerMetadata: { anthropic: state.fallbackMeta } } : {}
628
689
  });
629
690
  }
630
691
  blockStates.delete(index);
@@ -695,6 +756,17 @@ function handleApiError(error) {
695
756
  }
696
757
  throw error;
697
758
  }
759
+ function isRefusal(stopReason) {
760
+ return stopReason === "refusal";
761
+ }
762
+ function refusalError(stopDetails, fallbackModel) {
763
+ const category = stopDetails?.category ?? null;
764
+ const explanation = stopDetails?.explanation ?? null;
765
+ const categoryNote = category ? ` (category: ${category})` : "";
766
+ const detail = explanation ? explanation : "Claude Fable 5's safety classifiers declined this request.";
767
+ const hint = fallbackModel ? `The configured fallback model (${fallbackModel}) also refused.` : `Retry with a fallback model such as anthropic-sdk/claude-opus-4-8.`;
768
+ return new Error(`Claude Fable 5 refused this request${categoryNote}. ${detail} ${hint}`);
769
+ }
698
770
  function mapFinishReason2(stopReason) {
699
771
  const unified = (() => {
700
772
  switch (stopReason) {
@@ -711,9 +783,10 @@ function mapFinishReason2(stopReason) {
711
783
  })();
712
784
  return { unified, raw: stopReason ?? undefined };
713
785
  }
786
+ var FALLBACK_BETAS_HEADER = "x-anthropic-sdk-fallback-betas";
714
787
  var BILLING_SYSTEM_BLOCK = {
715
788
  type: "text",
716
- text: "x-anthropic-billing-header: cc_version=2.1.154.cea; cc_entrypoint=sdk-cli; cch=00000;"
789
+ text: "x-anthropic-billing-header: cc_version=2.1.173.d11; cc_entrypoint=sdk-cli; cch=00000;"
717
790
  };
718
791
  var IDENTITY_SYSTEM_BLOCK = {
719
792
  type: "text",
@@ -847,6 +920,9 @@ class AnthropicSDKModel {
847
920
  }
848
921
  if (anthropicOptions.metadata)
849
922
  params.metadata = anthropicOptions.metadata;
923
+ if (typeof anthropicOptions.refusalFallback === "string" && anthropicOptions.refusalFallback.length > 0) {
924
+ params.fallbacks = [{ model: anthropicOptions.refusalFallback }];
925
+ }
850
926
  }
851
927
  if (this.isOAuth && supportsContextManagement(this.apiModelId)) {
852
928
  const edits = [];
@@ -861,16 +937,19 @@ class AnthropicSDKModel {
861
937
  params.context_management = { edits };
862
938
  }
863
939
  }
864
- return { params, warnings };
940
+ const needsFallbackBetas = params.fallbacks != null || messages.some((msg) => Array.isArray(msg.content) && msg.content.some((block) => block?.type === "fallback"));
941
+ return { params, warnings, needsFallbackBetas };
865
942
  }
866
- buildRequestOptions(signal) {
943
+ buildRequestOptions(signal, needsFallbackBetas) {
867
944
  const opts = {};
868
945
  if (signal)
869
946
  opts.signal = signal;
947
+ if (needsFallbackBetas)
948
+ opts.headers = { [FALLBACK_BETAS_HEADER]: "1" };
870
949
  return opts;
871
950
  }
872
951
  async doGenerate(options) {
873
- const { params, warnings } = this.buildParams(options);
952
+ const { params, warnings, needsFallbackBetas } = this.buildParams(options);
874
953
  if (!options.maxOutputTokens && params.max_tokens > 4096) {
875
954
  params.max_tokens = 4096;
876
955
  }
@@ -879,31 +958,74 @@ class AnthropicSDKModel {
879
958
  response = await this.client.messages.create({
880
959
  ...params,
881
960
  stream: false
882
- }, this.buildRequestOptions(options.abortSignal));
961
+ }, this.buildRequestOptions(options.abortSignal, needsFallbackBetas));
883
962
  } catch (error) {
884
963
  handleApiError(error);
885
964
  }
965
+ const fallbacksSent = Array.isArray(params.fallbacks) && params.fallbacks.length > 0;
966
+ if (isRefusal(response.stop_reason)) {
967
+ throw refusalError(response.stop_details, fallbacksSent ? params.fallbacks[0].model : undefined);
968
+ }
969
+ const fallbackBlock = response.content.find((b) => b.type === "fallback");
970
+ const sticky = !fallbackBlock && fallbacksSent && response.model && response.model !== this.apiModelId;
971
+ let pendingMeta;
972
+ if (fallbackBlock) {
973
+ pendingMeta = {
974
+ fallback: { from: fallbackBlock.from, to: fallbackBlock.to },
975
+ servedBy: {
976
+ from: fallbackBlock.from?.model ?? this.apiModelId,
977
+ to: fallbackBlock.to?.model ?? "unknown",
978
+ kind: "fallback"
979
+ }
980
+ };
981
+ } else if (sticky) {
982
+ pendingMeta = {
983
+ servedBy: { from: this.apiModelId, to: response.model, kind: "sticky" }
984
+ };
985
+ }
886
986
  const content = [];
987
+ const takeMeta = () => {
988
+ const meta = pendingMeta;
989
+ pendingMeta = undefined;
990
+ return meta;
991
+ };
992
+ let seenFallbackBlock = false;
887
993
  for (const block of response.content) {
888
994
  switch (block.type) {
889
- case "text":
890
- content.push({ type: "text", text: block.text });
995
+ case "text": {
996
+ const meta = seenFallbackBlock || sticky ? takeMeta() : undefined;
997
+ content.push({
998
+ type: "text",
999
+ text: block.text,
1000
+ ...meta ? { providerMetadata: { anthropic: meta } } : {}
1001
+ });
891
1002
  break;
892
- case "tool_use":
1003
+ }
1004
+ case "tool_use": {
1005
+ const meta = seenFallbackBlock || sticky ? takeMeta() : undefined;
893
1006
  content.push({
894
1007
  type: "tool-call",
895
1008
  toolCallId: block.id,
896
1009
  toolName: toOpencodeToolName(block.name),
897
- input: JSON.stringify(block.input)
1010
+ input: JSON.stringify(block.input),
1011
+ ...meta ? { providerMetadata: { anthropic: meta } } : {}
898
1012
  });
899
1013
  break;
1014
+ }
900
1015
  default: {
901
1016
  const anyBlock = block;
902
- if (anyBlock.type === "thinking" && anyBlock.thinking) {
1017
+ if (anyBlock.type === "fallback") {
1018
+ seenFallbackBlock = true;
1019
+ } else if (anyBlock.type === "thinking" && anyBlock.thinking) {
1020
+ const extra = seenFallbackBlock || sticky ? takeMeta() : undefined;
1021
+ const meta = {
1022
+ ...anyBlock.signature ? { signature: anyBlock.signature } : {},
1023
+ ...extra
1024
+ };
903
1025
  content.push({
904
1026
  type: "reasoning",
905
1027
  text: anyBlock.thinking,
906
- providerMetadata: anyBlock.signature ? { anthropic: { signature: anyBlock.signature } } : undefined
1028
+ providerMetadata: Object.keys(meta).length > 0 ? { anthropic: meta } : undefined
907
1029
  });
908
1030
  }
909
1031
  break;
@@ -942,13 +1064,13 @@ class AnthropicSDKModel {
942
1064
  };
943
1065
  }
944
1066
  async doStream(options) {
945
- const { params, warnings } = this.buildParams(options);
1067
+ const { params, warnings, needsFallbackBetas } = this.buildParams(options);
946
1068
  let anthropicStream;
947
1069
  try {
948
1070
  anthropicStream = await this.client.messages.create({
949
1071
  ...params,
950
1072
  stream: true
951
- }, this.buildRequestOptions(options.abortSignal));
1073
+ }, this.buildRequestOptions(options.abortSignal, needsFallbackBetas));
952
1074
  } catch (error) {
953
1075
  if (error instanceof APIError && error.status === 400 && error.message?.includes("must end with a user message")) {
954
1076
  return {
@@ -976,7 +1098,12 @@ class AnthropicSDKModel {
976
1098
  }
977
1099
  handleApiError(error);
978
1100
  }
979
- const stream = convertStream(anthropicStream, this.modelId);
1101
+ const fallbacksSent = Array.isArray(params.fallbacks) && params.fallbacks.length > 0;
1102
+ const stream = convertStream(anthropicStream, this.modelId, {
1103
+ apiModelId: this.apiModelId,
1104
+ fallbacksEnabled: fallbacksSent,
1105
+ fallbackModel: fallbacksSent ? params.fallbacks[0].model : undefined
1106
+ });
980
1107
  const wrappedStream = new ReadableStream({
981
1108
  async start(controller) {
982
1109
  controller.enqueue({
@@ -1099,13 +1226,14 @@ var API_TTL_MS = 5 * 60000;
1099
1226
 
1100
1227
  // src/index.ts
1101
1228
  var CLAUDE_CODE_SYSTEM_PROMPT = claudecode_system_default;
1102
- var CLAUDE_CODE_OPUS48_SYSTEM_PROMPT = claudecode_system_opus48_default;
1229
+ var CLAUDE_CODE_NEW_SYSTEM_PROMPT = claudecode_system_new_default;
1103
1230
  function selectClaudePromptForModel(modelId) {
1104
- if (modelId.includes("opus-4-8"))
1105
- return claudecode_system_opus48_default;
1231
+ if (modelId.includes("opus-4-8") || modelId.includes("fable-5")) {
1232
+ return claudecode_system_new_default;
1233
+ }
1106
1234
  return claudecode_system_default;
1107
1235
  }
1108
- var CLAUDE_CODE_VERSION = "2.1.154";
1236
+ var CLAUDE_CODE_VERSION = "2.1.173";
1109
1237
  var OAUTH_BETAS = [
1110
1238
  "claude-code-20250219",
1111
1239
  "oauth-2025-04-20",
@@ -1122,7 +1250,8 @@ var PROVIDER_ID = "anthropic-sdk";
1122
1250
  var API_KEY_COSTS = {
1123
1251
  haiku: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 },
1124
1252
  sonnet: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 },
1125
- opus: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }
1253
+ opus: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 },
1254
+ fable: { input: 10, output: 50, cache_read: 1, cache_write: 12.5 }
1126
1255
  };
1127
1256
  var ZERO_COST = { input: 0, output: 0, cache_read: 0, cache_write: 0 };
1128
1257
  function buildPluginModels(isOAuth) {
@@ -1236,6 +1365,21 @@ function buildPluginModels(isOAuth) {
1236
1365
  },
1237
1366
  options: { effort: "medium" },
1238
1367
  ...opus47Variants
1368
+ },
1369
+ "claude-fable-5": {
1370
+ name: "Claude Fable 5",
1371
+ reasoning: true,
1372
+ tool_call: true,
1373
+ attachment: true,
1374
+ temperature: true,
1375
+ limit: { context: 1e6, output: 128000 },
1376
+ cost: cost("fable"),
1377
+ modalities: {
1378
+ input: ["text", "image", "pdf"],
1379
+ output: ["text"]
1380
+ },
1381
+ options: { effort: "medium", refusalFallback: "claude-opus-4-8" },
1382
+ ...opus47Variants
1239
1383
  }
1240
1384
  };
1241
1385
  }
@@ -1300,7 +1444,7 @@ function createAnthropicSDK(options = {}) {
1300
1444
  if (!betas.includes("effort-2025-11-24"))
1301
1445
  betas.push("effort-2025-11-24");
1302
1446
  }
1303
- if (model.includes("opus-4-8")) {
1447
+ if (model.includes("opus-4-8") || model.includes("fable-5")) {
1304
1448
  if (!betas.includes("mid-conversation-system-2026-04-07")) {
1305
1449
  betas.push("mid-conversation-system-2026-04-07");
1306
1450
  }
@@ -1308,6 +1452,19 @@ function createAnthropicSDK(options = {}) {
1308
1452
  reqHeaders.set("anthropic-beta", betas.join(","));
1309
1453
  init = { ...init, headers: reqHeaders };
1310
1454
  }
1455
+ {
1456
+ const reqHeaders = new Headers(init?.headers);
1457
+ if (reqHeaders.has(FALLBACK_BETAS_HEADER)) {
1458
+ reqHeaders.delete(FALLBACK_BETAS_HEADER);
1459
+ const betas = (reqHeaders.get("anthropic-beta") ?? "").split(",").map((beta) => beta.trim()).filter(Boolean);
1460
+ for (const beta of ["server-side-fallback-2026-06-01", "fallback-credit-2026-06-01"]) {
1461
+ if (!betas.includes(beta))
1462
+ betas.push(beta);
1463
+ }
1464
+ reqHeaders.set("anthropic-beta", betas.join(","));
1465
+ init = { ...init, headers: reqHeaders };
1466
+ }
1467
+ }
1311
1468
  const resp = await baseFetch(url, init);
1312
1469
  const h5 = resp.headers.get("anthropic-ratelimit-unified-5h-utilization");
1313
1470
  if (h5 != null) {
@@ -1356,10 +1513,22 @@ var anthropicSDKPlugin = async () => {
1356
1513
  if (!cfg.provider) {
1357
1514
  cfg.provider = {};
1358
1515
  }
1516
+ const userProvider = cfg.provider[PROVIDER_ID] ?? {};
1517
+ const defaultModels = buildPluginModels(isOAuth);
1518
+ const models = { ...defaultModels };
1519
+ for (const [modelID, userModel] of Object.entries(userProvider.models ?? {})) {
1520
+ const base = defaultModels[modelID];
1521
+ models[modelID] = base ? {
1522
+ ...base,
1523
+ ...userModel,
1524
+ options: { ...base.options, ...userModel?.options },
1525
+ variants: { ...base.variants, ...userModel?.variants }
1526
+ } : userModel;
1527
+ }
1359
1528
  cfg.provider[PROVIDER_ID] = {
1360
1529
  npm: PACKAGE_NAME,
1361
- models: buildPluginModels(isOAuth),
1362
- ...cfg.provider[PROVIDER_ID]
1530
+ ...userProvider,
1531
+ models
1363
1532
  };
1364
1533
  },
1365
1534
  "experimental.chat.system.transform": async (input, output) => {
@@ -1391,5 +1560,5 @@ export {
1391
1560
  createAnthropicSDK,
1392
1561
  anthropicSDKPlugin,
1393
1562
  CLAUDE_CODE_SYSTEM_PROMPT,
1394
- CLAUDE_CODE_OPUS48_SYSTEM_PROMPT
1563
+ CLAUDE_CODE_NEW_SYSTEM_PROMPT
1395
1564
  };
package/build/model.d.ts CHANGED
@@ -2,6 +2,31 @@ import type { LanguageModelV3, LanguageModelV3CallOptions, LanguageModelV3Genera
2
2
  import type Anthropic from "@anthropic-ai/sdk";
3
3
  type DoGenerateResult = LanguageModelV3GenerateResult;
4
4
  type DoStreamResult = LanguageModelV3StreamResult;
5
+ /**
6
+ * Build a clear error for a Fable 5 / Mythos-class safety refusal.
7
+ *
8
+ * Fable 5's safety classifiers can decline a request — the Messages API
9
+ * returns this as a *successful* HTTP 200 with `stop_reason: "refusal"`,
10
+ * empty `content`, and a `stop_details` object naming the policy area.
11
+ * Categories: cyber, bio, frontier_llm, reasoning_extraction (may be null).
12
+ *
13
+ * For now we surface this as an error so the user gets a clear message
14
+ * instead of an empty/confusing response. A future enhancement is to
15
+ * automatically retry on a fallback model (Claude Opus 4.8), per Anthropic's
16
+ * recommended pattern. See RESEARCH.md.
17
+ */
18
+ export declare function isRefusal(stopReason: string | null | undefined): boolean;
19
+ export declare function refusalError(stopDetails: any, fallbackModel?: string): Error;
20
+ /**
21
+ * Internal marker header: set on requests that need the server-side-fallback
22
+ * betas (request carries a `fallbacks` chain, or the conversation history
23
+ * echoes a `fallback` block from a previous fallback response). The fetch
24
+ * wrapper in index.ts translates it into the real `anthropic-beta` flags and
25
+ * strips it before the request leaves the process. This keeps the decision
26
+ * here — where the request is built — instead of sniffing the serialized
27
+ * body in the fetch wrapper (cf. RESEARCH.md Discovery #24/#25).
28
+ */
29
+ export declare const FALLBACK_BETAS_HEADER = "x-anthropic-sdk-fallback-betas";
5
30
  export declare class AnthropicSDKModel implements LanguageModelV3 {
6
31
  private client;
7
32
  private providerName;
@@ -1 +1 @@
1
- {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,0BAA0B,EAG1B,6BAA6B,EAE7B,2BAA2B,EAE5B,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,SAAS,MAAM,mBAAmB,CAAA;AAQ9C,KAAK,gBAAgB,GAAG,6BAA6B,CAAA;AACrD,KAAK,cAAc,GAAG,2BAA2B,CAAA;AA8HjD,qBAAa,iBAAkB,YAAW,eAAe;IAarD,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,YAAY;IACpB,OAAO,CAAC,OAAO;IAdjB,QAAQ,CAAC,oBAAoB,EAAG,IAAI,CAAS;IAC7C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAK;IAErD,mEAAmE;IACnE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAQ;IACnC,+FAA+F;IAC/F,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;gBAGnC,OAAO,EAAE,MAAM,EACP,MAAM,EAAE,SAAS,EACjB,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,OAAe;IASlC,OAAO,CAAC,WAAW;IA6KnB;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;IAMrB,UAAU,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAsF1E,QAAQ,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,cAAc,CAAC;CA4E7E"}
1
+ {"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../src/model.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,eAAe,EACf,0BAA0B,EAG1B,6BAA6B,EAE7B,2BAA2B,EAE5B,MAAM,kBAAkB,CAAA;AACzB,OAAO,KAAK,SAAS,MAAM,mBAAmB,CAAA;AAQ9C,KAAK,gBAAgB,GAAG,6BAA6B,CAAA;AACrD,KAAK,cAAc,GAAG,2BAA2B,CAAA;AAkDjD;;;;;;;;;;;;GAYG;AACH,wBAAgB,SAAS,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAExE;AAED,wBAAgB,YAAY,CAAC,WAAW,EAAE,GAAG,EAAE,aAAa,CAAC,EAAE,MAAM,GAAG,KAAK,CAW5E;AAmBD;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,mCAAmC,CAAA;AA6DrE,qBAAa,iBAAkB,YAAW,eAAe;IAarD,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,YAAY;IACpB,OAAO,CAAC,OAAO;IAdjB,QAAQ,CAAC,oBAAoB,EAAG,IAAI,CAAS;IAC7C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAK;IAErD,mEAAmE;IACnE,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAQ;IACnC,+FAA+F;IAC/F,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;gBAGnC,OAAO,EAAE,MAAM,EACP,MAAM,EAAE,SAAS,EACjB,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,OAAe;IASlC,OAAO,CAAC,WAAW;IAuMnB;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;IAUrB,UAAU,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAyJ1E,QAAQ,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,cAAc,CAAC;CAiF7E"}
package/build/server.js CHANGED
@@ -48,8 +48,8 @@ function hasCchPlaceholder(body) {
48
48
  return block?.text?.includes("cch=00000") ?? false;
49
49
  }
50
50
 
51
- // src/claudecode-system-opus48.txt
52
- var claudecode_system_opus48_default = `
51
+ // src/claudecode-system-new.txt
52
+ var claudecode_system_new_default = `
53
53
  You are an interactive agent that helps users with software engineering tasks.
54
54
 
55
55
  # Harness
@@ -382,6 +382,14 @@ function convertUserMessage(message) {
382
382
  function convertAssistantMessage(message) {
383
383
  const content = [];
384
384
  for (const part of message.content) {
385
+ const fallback = part.providerMetadata?.anthropic?.fallback ?? part.providerOptions?.anthropic?.fallback;
386
+ if (fallback?.from?.model && fallback?.to?.model) {
387
+ content.push({
388
+ type: "fallback",
389
+ from: { model: fallback.from.model },
390
+ to: { model: fallback.to.model }
391
+ });
392
+ }
385
393
  switch (part.type) {
386
394
  case "text":
387
395
  if (part.text.length > 0) {
@@ -460,6 +468,14 @@ var idCounter = 0;
460
468
  function generateId() {
461
469
  return `block-${Date.now()}-${idCounter++}`;
462
470
  }
471
+ function buildRefusalError(stopDetails, fallbackModel) {
472
+ const category = stopDetails?.category ?? null;
473
+ const explanation = stopDetails?.explanation ?? null;
474
+ const categoryNote = category ? ` (category: ${category})` : "";
475
+ const detail = explanation ? explanation : "Claude Fable 5's safety classifiers declined this request.";
476
+ const hint = fallbackModel ? `The configured fallback model (${fallbackModel}) also refused.` : `Retry with a fallback model such as anthropic-sdk/claude-opus-4-8.`;
477
+ return new Error(`Claude Fable 5 refused this request${categoryNote}. ${detail} ${hint}`);
478
+ }
463
479
  function mapFinishReason(stopReason) {
464
480
  const unified = (() => {
465
481
  switch (stopReason) {
@@ -476,8 +492,13 @@ function mapFinishReason(stopReason) {
476
492
  })();
477
493
  return { unified, raw: stopReason ?? undefined };
478
494
  }
479
- function convertStream(anthropicStream, modelId) {
495
+ function convertStream(anthropicStream, modelId, context) {
480
496
  const blockStates = new Map;
497
+ const ctx = {
498
+ apiModelId: context?.apiModelId ?? modelId,
499
+ fallbacksEnabled: context?.fallbacksEnabled ?? false,
500
+ fallbackModel: context?.fallbackModel
501
+ };
481
502
  let inputTokens;
482
503
  let outputTokens;
483
504
  let cachedInputTokens;
@@ -486,7 +507,7 @@ function convertStream(anthropicStream, modelId) {
486
507
  async start(controller) {
487
508
  try {
488
509
  for await (const event of anthropicStream) {
489
- const parts = processEvent(event, blockStates, modelId);
510
+ const parts = processEvent(event, blockStates, modelId, ctx);
490
511
  if (event.type === "message_start" && event.message.usage) {
491
512
  const u = event.message.usage;
492
513
  inputTokens = u.input_tokens;
@@ -496,6 +517,12 @@ function convertStream(anthropicStream, modelId) {
496
517
  if (event.type === "message_delta") {
497
518
  const delta = event;
498
519
  outputTokens = delta.usage?.output_tokens;
520
+ if (delta.delta?.stop_reason === "refusal") {
521
+ controller.enqueue({
522
+ type: "error",
523
+ error: buildRefusalError(delta.delta?.stop_details, ctx.fallbacksEnabled ? ctx.fallbackModel : undefined)
524
+ });
525
+ }
499
526
  const finishReason = mapFinishReason(delta.delta?.stop_reason);
500
527
  controller.enqueue({
501
528
  type: "finish",
@@ -532,10 +559,16 @@ function convertStream(anthropicStream, modelId) {
532
559
  }
533
560
  });
534
561
  }
535
- function processEvent(event, blockStates, modelId) {
562
+ function processEvent(event, blockStates, modelId, ctx) {
536
563
  const parts = [];
537
564
  switch (event.type) {
538
565
  case "message_start": {
566
+ const servedModel = event.message.model;
567
+ if (ctx.fallbacksEnabled && servedModel && servedModel !== ctx.apiModelId) {
568
+ ctx.pendingMeta = {
569
+ servedBy: { from: ctx.apiModelId, to: servedModel, kind: "sticky" }
570
+ };
571
+ }
539
572
  parts.push({
540
573
  type: "response-metadata",
541
574
  id: event.message.id,
@@ -547,14 +580,38 @@ function processEvent(event, blockStates, modelId) {
547
580
  case "content_block_start": {
548
581
  const index = event.index;
549
582
  const block = event.content_block;
550
- if (block.type === "text") {
583
+ const takeMeta = () => {
584
+ const meta = ctx.pendingMeta;
585
+ ctx.pendingMeta = undefined;
586
+ return meta;
587
+ };
588
+ if (block.type === "fallback") {
589
+ ctx.pendingMeta = {
590
+ fallback: { from: block.from, to: block.to },
591
+ servedBy: {
592
+ from: block.from?.model ?? ctx.apiModelId,
593
+ to: block.to?.model ?? "unknown",
594
+ kind: "fallback"
595
+ }
596
+ };
597
+ } else if (block.type === "text") {
551
598
  const id = generateId();
552
- blockStates.set(index, { type: "text", id });
553
- parts.push({ type: "text-start", id });
599
+ const meta = takeMeta();
600
+ blockStates.set(index, { type: "text", id, fallbackMeta: meta });
601
+ parts.push({
602
+ type: "text-start",
603
+ id,
604
+ ...meta ? { providerMetadata: { anthropic: meta } } : {}
605
+ });
554
606
  } else if (block.type === "thinking") {
555
607
  const id = generateId();
556
- blockStates.set(index, { type: "thinking", id });
557
- parts.push({ type: "reasoning-start", id });
608
+ const meta = takeMeta();
609
+ blockStates.set(index, { type: "thinking", id, fallbackMeta: meta });
610
+ parts.push({
611
+ type: "reasoning-start",
612
+ id,
613
+ ...meta ? { providerMetadata: { anthropic: meta } } : {}
614
+ });
558
615
  } else if (block.type === "tool_use") {
559
616
  const toolId = block.id;
560
617
  const toolName = toOpencodeToolName(block.name);
@@ -563,7 +620,8 @@ function processEvent(event, blockStates, modelId) {
563
620
  id: toolId,
564
621
  toolUseId: toolId,
565
622
  toolName,
566
- argsText: ""
623
+ argsText: "",
624
+ fallbackMeta: takeMeta()
567
625
  });
568
626
  parts.push({
569
627
  type: "tool-input-start",
@@ -611,12 +669,14 @@ function processEvent(event, blockStates, modelId) {
611
669
  if (state.type === "text") {
612
670
  parts.push({ type: "text-end", id: state.id });
613
671
  } else if (state.type === "thinking") {
672
+ const meta = {
673
+ ...state.signature ? { signature: state.signature } : {},
674
+ ...state.fallbackMeta
675
+ };
614
676
  parts.push({
615
677
  type: "reasoning-end",
616
678
  id: state.id,
617
- ...state.signature ? {
618
- providerMetadata: { anthropic: { signature: state.signature } }
619
- } : {}
679
+ ...Object.keys(meta).length > 0 ? { providerMetadata: { anthropic: meta } } : {}
620
680
  });
621
681
  } else if (state.type === "tool_use") {
622
682
  parts.push({ type: "tool-input-end", id: state.id });
@@ -624,7 +684,8 @@ function processEvent(event, blockStates, modelId) {
624
684
  type: "tool-call",
625
685
  toolCallId: state.toolUseId,
626
686
  toolName: state.toolName,
627
- input: state.argsText || "{}"
687
+ input: state.argsText || "{}",
688
+ ...state.fallbackMeta ? { providerMetadata: { anthropic: state.fallbackMeta } } : {}
628
689
  });
629
690
  }
630
691
  blockStates.delete(index);
@@ -695,6 +756,17 @@ function handleApiError(error) {
695
756
  }
696
757
  throw error;
697
758
  }
759
+ function isRefusal(stopReason) {
760
+ return stopReason === "refusal";
761
+ }
762
+ function refusalError(stopDetails, fallbackModel) {
763
+ const category = stopDetails?.category ?? null;
764
+ const explanation = stopDetails?.explanation ?? null;
765
+ const categoryNote = category ? ` (category: ${category})` : "";
766
+ const detail = explanation ? explanation : "Claude Fable 5's safety classifiers declined this request.";
767
+ const hint = fallbackModel ? `The configured fallback model (${fallbackModel}) also refused.` : `Retry with a fallback model such as anthropic-sdk/claude-opus-4-8.`;
768
+ return new Error(`Claude Fable 5 refused this request${categoryNote}. ${detail} ${hint}`);
769
+ }
698
770
  function mapFinishReason2(stopReason) {
699
771
  const unified = (() => {
700
772
  switch (stopReason) {
@@ -711,9 +783,10 @@ function mapFinishReason2(stopReason) {
711
783
  })();
712
784
  return { unified, raw: stopReason ?? undefined };
713
785
  }
786
+ var FALLBACK_BETAS_HEADER = "x-anthropic-sdk-fallback-betas";
714
787
  var BILLING_SYSTEM_BLOCK = {
715
788
  type: "text",
716
- text: "x-anthropic-billing-header: cc_version=2.1.154.cea; cc_entrypoint=sdk-cli; cch=00000;"
789
+ text: "x-anthropic-billing-header: cc_version=2.1.173.d11; cc_entrypoint=sdk-cli; cch=00000;"
717
790
  };
718
791
  var IDENTITY_SYSTEM_BLOCK = {
719
792
  type: "text",
@@ -847,6 +920,9 @@ class AnthropicSDKModel {
847
920
  }
848
921
  if (anthropicOptions.metadata)
849
922
  params.metadata = anthropicOptions.metadata;
923
+ if (typeof anthropicOptions.refusalFallback === "string" && anthropicOptions.refusalFallback.length > 0) {
924
+ params.fallbacks = [{ model: anthropicOptions.refusalFallback }];
925
+ }
850
926
  }
851
927
  if (this.isOAuth && supportsContextManagement(this.apiModelId)) {
852
928
  const edits = [];
@@ -861,16 +937,19 @@ class AnthropicSDKModel {
861
937
  params.context_management = { edits };
862
938
  }
863
939
  }
864
- return { params, warnings };
940
+ const needsFallbackBetas = params.fallbacks != null || messages.some((msg) => Array.isArray(msg.content) && msg.content.some((block) => block?.type === "fallback"));
941
+ return { params, warnings, needsFallbackBetas };
865
942
  }
866
- buildRequestOptions(signal) {
943
+ buildRequestOptions(signal, needsFallbackBetas) {
867
944
  const opts = {};
868
945
  if (signal)
869
946
  opts.signal = signal;
947
+ if (needsFallbackBetas)
948
+ opts.headers = { [FALLBACK_BETAS_HEADER]: "1" };
870
949
  return opts;
871
950
  }
872
951
  async doGenerate(options) {
873
- const { params, warnings } = this.buildParams(options);
952
+ const { params, warnings, needsFallbackBetas } = this.buildParams(options);
874
953
  if (!options.maxOutputTokens && params.max_tokens > 4096) {
875
954
  params.max_tokens = 4096;
876
955
  }
@@ -879,31 +958,74 @@ class AnthropicSDKModel {
879
958
  response = await this.client.messages.create({
880
959
  ...params,
881
960
  stream: false
882
- }, this.buildRequestOptions(options.abortSignal));
961
+ }, this.buildRequestOptions(options.abortSignal, needsFallbackBetas));
883
962
  } catch (error) {
884
963
  handleApiError(error);
885
964
  }
965
+ const fallbacksSent = Array.isArray(params.fallbacks) && params.fallbacks.length > 0;
966
+ if (isRefusal(response.stop_reason)) {
967
+ throw refusalError(response.stop_details, fallbacksSent ? params.fallbacks[0].model : undefined);
968
+ }
969
+ const fallbackBlock = response.content.find((b) => b.type === "fallback");
970
+ const sticky = !fallbackBlock && fallbacksSent && response.model && response.model !== this.apiModelId;
971
+ let pendingMeta;
972
+ if (fallbackBlock) {
973
+ pendingMeta = {
974
+ fallback: { from: fallbackBlock.from, to: fallbackBlock.to },
975
+ servedBy: {
976
+ from: fallbackBlock.from?.model ?? this.apiModelId,
977
+ to: fallbackBlock.to?.model ?? "unknown",
978
+ kind: "fallback"
979
+ }
980
+ };
981
+ } else if (sticky) {
982
+ pendingMeta = {
983
+ servedBy: { from: this.apiModelId, to: response.model, kind: "sticky" }
984
+ };
985
+ }
886
986
  const content = [];
987
+ const takeMeta = () => {
988
+ const meta = pendingMeta;
989
+ pendingMeta = undefined;
990
+ return meta;
991
+ };
992
+ let seenFallbackBlock = false;
887
993
  for (const block of response.content) {
888
994
  switch (block.type) {
889
- case "text":
890
- content.push({ type: "text", text: block.text });
995
+ case "text": {
996
+ const meta = seenFallbackBlock || sticky ? takeMeta() : undefined;
997
+ content.push({
998
+ type: "text",
999
+ text: block.text,
1000
+ ...meta ? { providerMetadata: { anthropic: meta } } : {}
1001
+ });
891
1002
  break;
892
- case "tool_use":
1003
+ }
1004
+ case "tool_use": {
1005
+ const meta = seenFallbackBlock || sticky ? takeMeta() : undefined;
893
1006
  content.push({
894
1007
  type: "tool-call",
895
1008
  toolCallId: block.id,
896
1009
  toolName: toOpencodeToolName(block.name),
897
- input: JSON.stringify(block.input)
1010
+ input: JSON.stringify(block.input),
1011
+ ...meta ? { providerMetadata: { anthropic: meta } } : {}
898
1012
  });
899
1013
  break;
1014
+ }
900
1015
  default: {
901
1016
  const anyBlock = block;
902
- if (anyBlock.type === "thinking" && anyBlock.thinking) {
1017
+ if (anyBlock.type === "fallback") {
1018
+ seenFallbackBlock = true;
1019
+ } else if (anyBlock.type === "thinking" && anyBlock.thinking) {
1020
+ const extra = seenFallbackBlock || sticky ? takeMeta() : undefined;
1021
+ const meta = {
1022
+ ...anyBlock.signature ? { signature: anyBlock.signature } : {},
1023
+ ...extra
1024
+ };
903
1025
  content.push({
904
1026
  type: "reasoning",
905
1027
  text: anyBlock.thinking,
906
- providerMetadata: anyBlock.signature ? { anthropic: { signature: anyBlock.signature } } : undefined
1028
+ providerMetadata: Object.keys(meta).length > 0 ? { anthropic: meta } : undefined
907
1029
  });
908
1030
  }
909
1031
  break;
@@ -942,13 +1064,13 @@ class AnthropicSDKModel {
942
1064
  };
943
1065
  }
944
1066
  async doStream(options) {
945
- const { params, warnings } = this.buildParams(options);
1067
+ const { params, warnings, needsFallbackBetas } = this.buildParams(options);
946
1068
  let anthropicStream;
947
1069
  try {
948
1070
  anthropicStream = await this.client.messages.create({
949
1071
  ...params,
950
1072
  stream: true
951
- }, this.buildRequestOptions(options.abortSignal));
1073
+ }, this.buildRequestOptions(options.abortSignal, needsFallbackBetas));
952
1074
  } catch (error) {
953
1075
  if (error instanceof APIError && error.status === 400 && error.message?.includes("must end with a user message")) {
954
1076
  return {
@@ -976,7 +1098,12 @@ class AnthropicSDKModel {
976
1098
  }
977
1099
  handleApiError(error);
978
1100
  }
979
- const stream = convertStream(anthropicStream, this.modelId);
1101
+ const fallbacksSent = Array.isArray(params.fallbacks) && params.fallbacks.length > 0;
1102
+ const stream = convertStream(anthropicStream, this.modelId, {
1103
+ apiModelId: this.apiModelId,
1104
+ fallbacksEnabled: fallbacksSent,
1105
+ fallbackModel: fallbacksSent ? params.fallbacks[0].model : undefined
1106
+ });
980
1107
  const wrappedStream = new ReadableStream({
981
1108
  async start(controller) {
982
1109
  controller.enqueue({
@@ -1099,13 +1226,14 @@ var API_TTL_MS = 5 * 60000;
1099
1226
 
1100
1227
  // src/index.ts
1101
1228
  var CLAUDE_CODE_SYSTEM_PROMPT = claudecode_system_default;
1102
- var CLAUDE_CODE_OPUS48_SYSTEM_PROMPT = claudecode_system_opus48_default;
1229
+ var CLAUDE_CODE_NEW_SYSTEM_PROMPT = claudecode_system_new_default;
1103
1230
  function selectClaudePromptForModel(modelId) {
1104
- if (modelId.includes("opus-4-8"))
1105
- return claudecode_system_opus48_default;
1231
+ if (modelId.includes("opus-4-8") || modelId.includes("fable-5")) {
1232
+ return claudecode_system_new_default;
1233
+ }
1106
1234
  return claudecode_system_default;
1107
1235
  }
1108
- var CLAUDE_CODE_VERSION = "2.1.154";
1236
+ var CLAUDE_CODE_VERSION = "2.1.173";
1109
1237
  var OAUTH_BETAS = [
1110
1238
  "claude-code-20250219",
1111
1239
  "oauth-2025-04-20",
@@ -1122,7 +1250,8 @@ var PROVIDER_ID = "anthropic-sdk";
1122
1250
  var API_KEY_COSTS = {
1123
1251
  haiku: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 },
1124
1252
  sonnet: { input: 3, output: 15, cache_read: 0.3, cache_write: 3.75 },
1125
- opus: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 }
1253
+ opus: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 },
1254
+ fable: { input: 10, output: 50, cache_read: 1, cache_write: 12.5 }
1126
1255
  };
1127
1256
  var ZERO_COST = { input: 0, output: 0, cache_read: 0, cache_write: 0 };
1128
1257
  function buildPluginModels(isOAuth) {
@@ -1236,6 +1365,21 @@ function buildPluginModels(isOAuth) {
1236
1365
  },
1237
1366
  options: { effort: "medium" },
1238
1367
  ...opus47Variants
1368
+ },
1369
+ "claude-fable-5": {
1370
+ name: "Claude Fable 5",
1371
+ reasoning: true,
1372
+ tool_call: true,
1373
+ attachment: true,
1374
+ temperature: true,
1375
+ limit: { context: 1e6, output: 128000 },
1376
+ cost: cost("fable"),
1377
+ modalities: {
1378
+ input: ["text", "image", "pdf"],
1379
+ output: ["text"]
1380
+ },
1381
+ options: { effort: "medium", refusalFallback: "claude-opus-4-8" },
1382
+ ...opus47Variants
1239
1383
  }
1240
1384
  };
1241
1385
  }
@@ -1300,7 +1444,7 @@ function createAnthropicSDK(options = {}) {
1300
1444
  if (!betas.includes("effort-2025-11-24"))
1301
1445
  betas.push("effort-2025-11-24");
1302
1446
  }
1303
- if (model.includes("opus-4-8")) {
1447
+ if (model.includes("opus-4-8") || model.includes("fable-5")) {
1304
1448
  if (!betas.includes("mid-conversation-system-2026-04-07")) {
1305
1449
  betas.push("mid-conversation-system-2026-04-07");
1306
1450
  }
@@ -1308,6 +1452,19 @@ function createAnthropicSDK(options = {}) {
1308
1452
  reqHeaders.set("anthropic-beta", betas.join(","));
1309
1453
  init = { ...init, headers: reqHeaders };
1310
1454
  }
1455
+ {
1456
+ const reqHeaders = new Headers(init?.headers);
1457
+ if (reqHeaders.has(FALLBACK_BETAS_HEADER)) {
1458
+ reqHeaders.delete(FALLBACK_BETAS_HEADER);
1459
+ const betas = (reqHeaders.get("anthropic-beta") ?? "").split(",").map((beta) => beta.trim()).filter(Boolean);
1460
+ for (const beta of ["server-side-fallback-2026-06-01", "fallback-credit-2026-06-01"]) {
1461
+ if (!betas.includes(beta))
1462
+ betas.push(beta);
1463
+ }
1464
+ reqHeaders.set("anthropic-beta", betas.join(","));
1465
+ init = { ...init, headers: reqHeaders };
1466
+ }
1467
+ }
1311
1468
  const resp = await baseFetch(url, init);
1312
1469
  const h5 = resp.headers.get("anthropic-ratelimit-unified-5h-utilization");
1313
1470
  if (h5 != null) {
@@ -1356,10 +1513,22 @@ var anthropicSDKPlugin = async () => {
1356
1513
  if (!cfg.provider) {
1357
1514
  cfg.provider = {};
1358
1515
  }
1516
+ const userProvider = cfg.provider[PROVIDER_ID] ?? {};
1517
+ const defaultModels = buildPluginModels(isOAuth);
1518
+ const models = { ...defaultModels };
1519
+ for (const [modelID, userModel] of Object.entries(userProvider.models ?? {})) {
1520
+ const base = defaultModels[modelID];
1521
+ models[modelID] = base ? {
1522
+ ...base,
1523
+ ...userModel,
1524
+ options: { ...base.options, ...userModel?.options },
1525
+ variants: { ...base.variants, ...userModel?.variants }
1526
+ } : userModel;
1527
+ }
1359
1528
  cfg.provider[PROVIDER_ID] = {
1360
1529
  npm: PACKAGE_NAME,
1361
- models: buildPluginModels(isOAuth),
1362
- ...cfg.provider[PROVIDER_ID]
1530
+ ...userProvider,
1531
+ models
1363
1532
  };
1364
1533
  },
1365
1534
  "experimental.chat.system.transform": async (input, output) => {
package/build/stream.d.ts CHANGED
@@ -1,6 +1,36 @@
1
1
  import type { LanguageModelV3StreamPart } from "@ai-sdk/provider";
2
2
  import type Anthropic from "@anthropic-ai/sdk";
3
3
  type MessageStreamEvent = Anthropic.MessageStreamEvent;
4
- export declare function convertStream(anthropicStream: AsyncIterable<MessageStreamEvent>, modelId: string): ReadableStream<LanguageModelV3StreamPart>;
4
+ /**
5
+ * Options and mutable state for one streamed response.
6
+ *
7
+ * Server-side fallback (Fable 5 safety refusals → Opus 4.8) surfaces in two
8
+ * ways that we translate into part metadata:
9
+ *
10
+ * - A `fallback` content block (content_block_start/stop, no deltas) marks
11
+ * the hop where the declining model's output gives way to the fallback
12
+ * model. We don't emit a part for it; instead we carry two keys on the
13
+ * NEXT block's part metadata (OpenCode persists part metadata and syncs
14
+ * it to the TUI — the same round trip thinking signatures use):
15
+ * anthropic.fallback — {from, to}; prompt.ts re-inserts the block into
16
+ * history verbatim. Position is load-bearing for thinking
17
+ * verification, so it must precede exactly this block.
18
+ * anthropic.servedBy — {from, to, kind} display-only notice for the
19
+ * TUI plugin (toast + sidebar). Never echoed back.
20
+ * - Sticky-routed follow-up turns carry no block; message_start announcing
21
+ * a different model than requested is the signal. servedBy (kind
22
+ * "sticky") attaches to the first content block.
23
+ */
24
+ interface StreamContext {
25
+ /** Model ID requested by the caller (without -1m suffix). */
26
+ apiModelId: string;
27
+ /** Whether the request carried a `fallbacks` chain. */
28
+ fallbacksEnabled: boolean;
29
+ /** First fallback model — used for error wording when the chain refuses. */
30
+ fallbackModel?: string;
31
+ /** Metadata waiting to be attached to the next content block. */
32
+ pendingMeta?: Record<string, any>;
33
+ }
34
+ export declare function convertStream(anthropicStream: AsyncIterable<MessageStreamEvent>, modelId: string, context?: Partial<StreamContext>): ReadableStream<LanguageModelV3StreamPart>;
5
35
  export {};
6
36
  //# sourceMappingURL=stream.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA+B,yBAAyB,EAAE,MAAM,kBAAkB,CAAA;AAC9F,OAAO,KAAK,SAAS,MAAM,mBAAmB,CAAA;AAG9C,KAAK,kBAAkB,GAAG,SAAS,CAAC,kBAAkB,CAAA;AAmCtD,wBAAgB,aAAa,CAC3B,eAAe,EAAE,aAAa,CAAC,kBAAkB,CAAC,EAClD,OAAO,EAAE,MAAM,GACd,cAAc,CAAC,yBAAyB,CAAC,CAwD3C"}
1
+ {"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA+B,yBAAyB,EAAE,MAAM,kBAAkB,CAAA;AAC9F,OAAO,KAAK,SAAS,MAAM,mBAAmB,CAAA;AAG9C,KAAK,kBAAkB,GAAG,SAAS,CAAC,kBAAkB,CAAA;AAetD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,UAAU,aAAa;IACrB,6DAA6D;IAC7D,UAAU,EAAE,MAAM,CAAA;IAClB,uDAAuD;IACvD,gBAAgB,EAAE,OAAO,CAAA;IACzB,4EAA4E;IAC5E,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CAClC;AA0CD,wBAAgB,aAAa,CAC3B,eAAe,EAAE,aAAa,CAAC,kBAAkB,CAAC,EAClD,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,GAC/B,cAAc,CAAC,yBAAyB,CAAC,CA0E3C"}
@@ -1 +1 @@
1
- {"version":3,"file":"tui.d.ts","sourceRoot":"","sources":["../src/tui.tsx"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,OAAO,KAAK,EAGV,eAAe,EAEhB,MAAM,yBAAyB,CAAA;AAoYhC,QAAA,MAAM,MAAM,EAAE,eAAe,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAG3C,CAAA;AAED,eAAe,MAAM,CAAA"}
1
+ {"version":3,"file":"tui.d.ts","sourceRoot":"","sources":["../src/tui.tsx"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,OAAO,KAAK,EAGV,eAAe,EAEhB,MAAM,yBAAyB,CAAA;AAujBhC,QAAA,MAAM,MAAM,EAAE,eAAe,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAG3C,CAAA;AAED,eAAe,MAAM,CAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtruder/opencode-claude-max-plugin",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "OpenCode plugin for Claude Max/Pro subscription via @anthropic-ai/sdk - uses Claude Code OAuth credentials",
5
5
  "keywords": [
6
6
  "ai-sdk",
@@ -49,7 +49,7 @@
49
49
  "prepare": "simple-git-hooks"
50
50
  },
51
51
  "dependencies": {
52
- "@anthropic-ai/sdk": "0.94.0",
52
+ "@anthropic-ai/sdk": "0.104.1",
53
53
  "xxhash-wasm": "^1.1.0"
54
54
  },
55
55
  "devDependencies": {
package/src/tui.tsx CHANGED
@@ -132,6 +132,144 @@ function createUsageStore(pollInterval: number) {
132
132
  }
133
133
  }
134
134
 
135
+ // ─── Fallback store ──────────────────────────────────────────────────
136
+
137
+ /**
138
+ * Watches for server-side model fallback (Fable 5 safety refusal → Opus 4.8).
139
+ *
140
+ * The provider attaches a display-only `anthropic.servedBy` object to part
141
+ * metadata when a response was served by a fallback model (see stream.ts).
142
+ * OpenCode persists part metadata and forwards it here via
143
+ * `message.part.updated` — no side-channel store is needed.
144
+ *
145
+ * Sticky routing is keyed on the conversation prefix server-side, so the
146
+ * fallback state is PER SESSION — a refusal in one session says nothing
147
+ * about another.
148
+ *
149
+ * The sidebar state is DERIVED from the session's persisted messages (via
150
+ * api.state, OpenCode's synced replica) rather than accumulated from live
151
+ * events — so it is correct after a TUI restart or when opening an old
152
+ * session. The event subscription only drives the live toast, deduped with
153
+ * an in-memory Set (part events never replay across restarts).
154
+ */
155
+ type FallbackInfo = {
156
+ from: string
157
+ to: string
158
+ kind: "fallback" | "sticky"
159
+ at: number
160
+ }
161
+
162
+ const shortModel = (model: string) => model.replace(/^claude-/, "")
163
+
164
+ // Absolute time — stable without periodic re-rendering ("ago" phrasing
165
+ // would freeze between reactive updates).
166
+ const atTime = (at: number) =>
167
+ new Date(at).toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" })
168
+
169
+ function createFallbackStore(api: Api) {
170
+ // Dedupe the repeated part-update events that fire as a part streams.
171
+ const seenParts = new Set<string>()
172
+
173
+ /** Extract the servedBy marker from one assistant message's parts. */
174
+ const markerOf = (msg: any): FallbackInfo | null => {
175
+ let latest: FallbackInfo | null = null
176
+ for (const part of api.state.part(msg.id)) {
177
+ const servedBy = (part as any)?.metadata?.anthropic?.servedBy
178
+ if (!servedBy?.to) continue
179
+ const time = (part as any)?.time
180
+ const at = time?.end ?? time?.start ?? msg.time?.created ?? 0
181
+ latest = {
182
+ from: String(servedBy.from ?? "unknown"),
183
+ to: String(servedBy.to),
184
+ kind: servedBy.kind === "sticky" ? "sticky" : "fallback",
185
+ at,
186
+ }
187
+ }
188
+ return latest
189
+ }
190
+
191
+ /**
192
+ * Walk a session's messages newest-first, yielding real assistant turns
193
+ * (skipping title-generation / compaction summaries, which never carry
194
+ * routing markers and would mask the real last turn).
195
+ */
196
+ function* assistantTurns(sessionID: string, beforeMessageID?: string) {
197
+ const messages = api.state.session.messages(sessionID)
198
+ let skipping = beforeMessageID !== undefined
199
+ for (let i = messages.length - 1; i >= 0; i--) {
200
+ const msg = messages[i] as any
201
+ if (skipping) {
202
+ if (msg?.id === beforeMessageID) skipping = false
203
+ continue
204
+ }
205
+ if (msg?.role !== "assistant") continue
206
+ if (msg.summary) continue
207
+ yield msg
208
+ }
209
+ }
210
+
211
+ // Live toast — only on the TRANSITION into fallback. A conversation that
212
+ // trips the classifier every turn would otherwise toast on every message;
213
+ // the sidebar line covers the continuing state. Sticky follow-ups never
214
+ // toast for the same reason.
215
+ const off = api.event.on("message.part.updated", (event) => {
216
+ const part = (event as any).properties?.part
217
+ const sessionID = (event as any).properties?.sessionID ?? part?.sessionID
218
+ const servedBy = part?.metadata?.anthropic?.servedBy
219
+ if (!servedBy?.to || !part?.id || !part?.messageID || !sessionID) return
220
+ if (seenParts.has(part.id)) return
221
+ seenParts.add(part.id)
222
+ if (servedBy.kind === "sticky") return
223
+
224
+ // Was the previous completed assistant turn already fallback-served?
225
+ for (const msg of assistantTurns(sessionID, part.messageID)) {
226
+ if (markerOf(msg)) return // continuation, not a transition
227
+ if (msg.time?.completed) break // last known state was the primary model
228
+ }
229
+
230
+ api.ui.toast({
231
+ variant: "warning",
232
+ title: "Model fallback",
233
+ message: `${shortModel(String(servedBy.from ?? "unknown"))} refused — answered by ${shortModel(String(servedBy.to))}`,
234
+ duration: 6000,
235
+ })
236
+ })
237
+
238
+ /**
239
+ * Fallback state for one session, derived from its message parts — a pure
240
+ * reactive derivation that re-evaluates only when session state changes.
241
+ *
242
+ * Only the MOST RECENT *settled* assistant turn is consulted: sticky
243
+ * routing re-emits a servedBy marker on every turn while active, and a
244
+ * turn that routed back to the primary model carries no marker. Looking
245
+ * further back would resurrect a stale fallback after the session has
246
+ * already returned to the requested model.
247
+ *
248
+ * A still-streaming turn without a marker is "unknown", not "no
249
+ * fallback" — the previous completed turn's state keeps showing until
250
+ * the new turn settles (otherwise the line flickers off on every send
251
+ * and back on when the refusal hits mid-stream).
252
+ *
253
+ * No time window: "the latest turn was served by X" is a fact that stays
254
+ * true until the next turn says otherwise. Whether the server's sticky
255
+ * routing is still active is unobservable — we don't guess.
256
+ */
257
+ const active = (sessionID: string | undefined): FallbackInfo | null => {
258
+ if (!sessionID) return null
259
+ for (const msg of assistantTurns(sessionID)) {
260
+ const marker = markerOf(msg)
261
+ if (marker) return marker
262
+ // No marker on a finished turn → currently served by the requested
263
+ // model. No marker on a streaming turn → not known yet, keep looking
264
+ // at the previous turn.
265
+ if (msg.time?.completed) return null
266
+ }
267
+ return null
268
+ }
269
+
270
+ return { active, dispose: off }
271
+ }
272
+
135
273
  // ─── Helpers ─────────────────────────────────────────────────────────
136
274
 
137
275
  const clamp01 = (v: number) => Math.max(0, Math.min(1, v))
@@ -242,6 +380,34 @@ function UsageSidebar(props: {
242
380
  )
243
381
  }
244
382
 
383
+ // ─── Fallback Sidebar Component ──────────────────────────────────────
384
+
385
+ function FallbackSidebar(props: {
386
+ theme: TuiThemeCurrent
387
+ sessionID: string | undefined
388
+ active: (sessionID: string | undefined) => FallbackInfo | null
389
+ }) {
390
+ return (
391
+ <Show when={props.active(props.sessionID)}>
392
+ {(info: Accessor<FallbackInfo>) => (
393
+ <box width="100%" flexDirection="column">
394
+ <text fg={props.theme.warning}>
395
+ <b>Model Fallback</b>
396
+ </text>
397
+ <text fg={props.theme.text}>
398
+ {shortModel(info().from)} → {shortModel(info().to)}
399
+ </text>
400
+ <text fg={props.theme.textMuted}>
401
+ {info().kind === "sticky"
402
+ ? `sticky-routed at ${atTime(info().at)}`
403
+ : `refused at ${atTime(info().at)}`}
404
+ </text>
405
+ </box>
406
+ )}
407
+ </Show>
408
+ )
409
+ }
410
+
245
411
  // ─── Dialog Component ────────────────────────────────────────────────
246
412
 
247
413
  function UsageDialog(props: {
@@ -353,6 +519,10 @@ const tui: TuiPlugin = async (api, options) => {
353
519
  const store = createUsageStore(config.poll_interval)
354
520
  api.lifecycle.onDispose(store.dispose)
355
521
 
522
+ // Model fallback notifications (toast) + sidebar status line
523
+ const fallback = createFallbackStore(api)
524
+ api.lifecycle.onDispose(fallback.dispose)
525
+
356
526
  // Refresh when session goes idle (inference completed — server just wrote new headers)
357
527
  const offEvent = api.event.on("session.idle", () => {
358
528
  setTimeout(store.refresh, 1000)
@@ -360,31 +530,40 @@ const tui: TuiPlugin = async (api, options) => {
360
530
  api.lifecycle.onDispose(offEvent)
361
531
 
362
532
  // Register /usage command
363
- api.command?.register(() => [
364
- {
365
- title: "Claude subscription usage",
366
- value: "anthropic-sdk.usage",
367
- category: "Claude",
368
- slash: {
369
- name: "usage",
370
- },
371
- onSelect() {
372
- store.refresh()
373
- api.ui.dialog.replace(() => (
374
- <UsageDialog api={api} usage={store.usage} status={store.status} />
375
- ))
533
+ const offCommand = api.keymap.registerLayer({
534
+ commands: [
535
+ {
536
+ namespace: "palette",
537
+ name: "anthropic-sdk.usage",
538
+ title: "Claude subscription usage",
539
+ category: "Claude",
540
+ slashName: "usage",
541
+ run() {
542
+ store.refresh()
543
+ api.ui.dialog.replace(() => (
544
+ <UsageDialog api={api} usage={store.usage} status={store.status} />
545
+ ))
546
+ },
376
547
  },
377
- },
378
- ])
548
+ ],
549
+ })
550
+ api.lifecycle.onDispose(offCommand)
379
551
 
380
552
  // Register sidebar slot
381
553
  if (config.sidebar) {
382
554
  api.slots.register({
383
555
  order: 110,
384
556
  slots: {
385
- sidebar_content(ctx) {
557
+ sidebar_content(ctx, props) {
386
558
  return (
387
- <UsageSidebar theme={ctx.theme.current} usage={store.usage} status={store.status} />
559
+ <>
560
+ <UsageSidebar theme={ctx.theme.current} usage={store.usage} status={store.status} />
561
+ <FallbackSidebar
562
+ theme={ctx.theme.current}
563
+ sessionID={(props as { session_id?: string } | undefined)?.session_id}
564
+ active={fallback.active}
565
+ />
566
+ </>
388
567
  )
389
568
  },
390
569
  },