@xtruder/opencode-claude-max-plugin 0.3.0 → 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
@@ -7,11 +7,11 @@ An [OpenCode](https://opencode.ai/) plugin that enables Claude Pro/Max subscript
7
7
  ## Why?
8
8
 
9
9
  - **Use your Claude subscription** — Automatically reads OAuth credentials from Claude Code, no separate API key needed
10
- - **Matches Claude Code exactly** — Same headers, billing, tool names, and request format as Claude Code CLI
11
- - **Prompt caching** — 98% of input tokens served from cache (system prompt + tools cached globally)
12
- - **All Claude models** — Opus 4.6, Sonnet 4.6, Haiku 4.5
13
- - **Extended thinking** — Full reasoning support with thinking variants (high/max)
14
- - **Usage tracking** — Sidebar widget with live progress bars + `/usage` command with full details
10
+ - **Matches Claude Code 2.1.154** — Same request format and behavior as the official CLI
11
+ - **Prompt caching** — Multi-turn conversations cache properly, keeping costs and latency low
12
+ - **All Claude models** — Opus 4.8, Opus 4.7, Opus 4.6, Sonnet 4.6, Haiku 4.5
13
+ - **Extended / adaptive thinking** — Full reasoning support across models, including Opus 4.7+'s adaptive thinking
14
+ - **Usage tracking** — Sidebar widget with live progress bars + `/usage` command
15
15
  - **Self-registering** — Models are registered automatically, no manual provider config needed
16
16
 
17
17
  ## Installation
@@ -25,11 +25,9 @@ Add the plugin to your `opencode.json` (project-level or `~/.config/opencode/ope
25
25
  }
26
26
  ```
27
27
 
28
- That's it. The plugin self-registers the `anthropic-sdk` provider and its models (Haiku 4.5, Sonnet 4.6, Opus 4.6) at startup via the OpenCode config hook. No separate `provider` block is needed.
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
- In OpenCode, the plugin also replaces the default base system prompt with a Claude-compatible one via `experimental.chat.system.transform`. This is required for Claude Code OAuth requests to be treated as Claude-native rather than third-party app requests.
31
-
32
- 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.
33
31
 
34
32
  ### TUI Plugin (sidebar + /usage command)
35
33
 
@@ -122,14 +120,13 @@ For Claude Code credentials, log in via `claude` CLI first (`claude auth login`)
122
120
  ## Features
123
121
 
124
122
  - Streaming and non-streaming completions
125
- - Tool/function calling with Claude Code tool name mapping (`task` → `Agent`, etc.)
123
+ - Tool/function calling with Claude Code tool name mapping (`task` → `Agent`, `webfetch` → `WebFetch`, etc.)
126
124
  - MCP tool name remapping (`server_tool` → `mcp__server__tool`)
127
- - Extended thinking with signature passthrough for multi-turn conversations
128
- - Prompt caching (98% cache hit rate with full OpenCode tool set)
129
- - CCH request signing — computes xxHash64 body integrity hash to unlock features like fast mode
130
- - Subscription rate limit detection — fails fast with clear message instead of hanging
131
- - Long context auto-detection — adds `context-1m` beta header when request body is large
132
- - TUI sidebar with live usage bars + `/usage` slash command with full subscription details
125
+ - Extended thinking (Sonnet/Opus 4.6) and adaptive thinking (Opus 4.7+) with effort levels and multi-turn signature passthrough
126
+ - Prompt caching that holds across long, tool-heavy conversations
127
+ - Subscription rate limit detection — fails fast with a clear message instead of hanging
128
+ - Long-context auto-detection for large prompts
129
+ - TUI sidebar with live usage bars + `/usage` slash command
133
130
 
134
131
  ## With Vercel AI SDK
135
132
 
@@ -160,7 +157,7 @@ When using Claude Code OAuth credentials outside OpenCode, you must pass a Claud
160
157
  ```bash
161
158
  bun install
162
159
  bun run build
163
- bun run test # integration tests (requires ANTHROPIC_API_KEY or Claude Code credentials)
160
+ bun test src/*.test.ts # unit + integration tests (model tests require ANTHROPIC_API_KEY or Claude Code credentials)
164
161
  ```
165
162
 
166
163
  See [RESEARCH.md](RESEARCH.md) for detailed reverse-engineering findings on how we matched Claude Code's request format.
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"}