@xtruder/opencode-claude-max-plugin 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -3
- package/build/index.d.ts +13 -5
- package/build/index.d.ts.map +1 -1
- package/build/index.js +312 -39
- package/build/model.d.ts +25 -0
- package/build/model.d.ts.map +1 -1
- package/build/pause-turn.d.ts +36 -0
- package/build/pause-turn.d.ts.map +1 -0
- package/build/server.js +311 -38
- package/build/stream.d.ts +31 -1
- package/build/stream.d.ts.map +1 -1
- package/build/tui.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/tui.tsx +196 -17
package/README.md
CHANGED
|
@@ -9,8 +9,9 @@ An [OpenCode](https://opencode.ai/) plugin that enables Claude Pro/Max subscript
|
|
|
9
9
|
- **Use your Claude subscription** — Automatically reads OAuth credentials from Claude Code, no separate API key needed
|
|
10
10
|
- **Matches Claude Code 2.1.154** — Same request format and behavior as the official CLI
|
|
11
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
|
|
12
|
+
- **All Claude models** — Fable 5, Opus 4.8, Opus 4.7, Opus 4.6, Sonnet 4.6, Haiku 4.5
|
|
13
13
|
- **Extended / adaptive thinking** — Full reasoning support across models, including Opus 4.7+'s adaptive thinking
|
|
14
|
+
- **Safety-refusal fallback** — Fable 5 refusals are transparently answered by Opus 4.8 in the same request, with TUI notification
|
|
14
15
|
- **Usage tracking** — Sidebar widget with live progress bars + `/usage` command
|
|
15
16
|
- **Self-registering** — Models are registered automatically, no manual provider config needed
|
|
16
17
|
|
|
@@ -25,9 +26,9 @@ Add the plugin to your `opencode.json` (project-level or `~/.config/opencode/ope
|
|
|
25
26
|
}
|
|
26
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
|
+
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, Fable 5) at startup via the OpenCode config hook. No separate `provider` block is needed.
|
|
29
30
|
|
|
30
|
-
Then open OpenCode and
|
|
31
|
+
Then open OpenCode and models will automatically be available under `anthropic-sdk` provider.
|
|
31
32
|
|
|
32
33
|
### TUI Plugin (sidebar + /usage command)
|
|
33
34
|
|
|
@@ -54,6 +55,7 @@ The TUI plugin provides:
|
|
|
54
55
|
- **Sidebar widget** — Compact progress bars for 5-hour session and 7-day weekly usage
|
|
55
56
|
- **`/usage` command** — Opens a dialog with full usage breakdown (per-model, extra usage)
|
|
56
57
|
- **Auto-refresh** — Polls the usage API every 60s and after each inference call
|
|
58
|
+
- **Fallback indicator** — Toast when a Fable 5 refusal falls back to Opus 4.8, plus a sidebar line showing which model served the latest turn
|
|
57
59
|
|
|
58
60
|
#### TUI Configuration
|
|
59
61
|
|
|
@@ -108,6 +110,36 @@ If you want to override model settings (e.g. thinking budgets, variants), you ca
|
|
|
108
110
|
|
|
109
111
|
Config-level settings are merged with plugin defaults — you only need to specify what you want to override.
|
|
110
112
|
|
|
113
|
+
### Claude Fable 5 and safety-refusal fallback
|
|
114
|
+
|
|
115
|
+
Claude Fable 5 (`claude-fable-5`) ships with stricter safety classifiers that can refuse a request at the API level (`stop_reason: "refusal"`) — even for benign follow-ups if the conversation contains a flagged topic. To keep sessions usable, the plugin enables Anthropic's server-side fallback by default: when Fable 5 refuses, **Opus 4.8 answers the same request in the same round trip**. Tool loops keep running, thinking chains stay verified, and prompt caching is unaffected.
|
|
116
|
+
|
|
117
|
+
When a fallback happens:
|
|
118
|
+
|
|
119
|
+
- The TUI shows a toast (`fable-5 refused — answered by opus-4-8`) on the first fallback turn
|
|
120
|
+
- The sidebar shows a `Model Fallback` line while the latest turn was served by the fallback model
|
|
121
|
+
- The served model is recorded in part metadata (`anthropic.servedBy`) — the model's own self-report will still say `claude-fable-5`, since identity comes from the prompt, not the serving model
|
|
122
|
+
|
|
123
|
+
Configure via the `refusalFallback` model option:
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"provider": {
|
|
128
|
+
"anthropic-sdk": {
|
|
129
|
+
"models": {
|
|
130
|
+
"claude-fable-5": {
|
|
131
|
+
"options": {
|
|
132
|
+
"refusalFallback": false
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Set it to another model ID to change the fallback target, or `false` to disable (refusals then surface as errors with the refusal category).
|
|
142
|
+
|
|
111
143
|
## Authentication
|
|
112
144
|
|
|
113
145
|
Credentials are resolved in order:
|
|
@@ -124,6 +156,7 @@ For Claude Code credentials, log in via `claude` CLI first (`claude auth login`)
|
|
|
124
156
|
- MCP tool name remapping (`server_tool` → `mcp__server__tool`)
|
|
125
157
|
- Extended thinking (Sonnet/Opus 4.6) and adaptive thinking (Opus 4.7+) with effort levels and multi-turn signature passthrough
|
|
126
158
|
- Prompt caching that holds across long, tool-heavy conversations
|
|
159
|
+
- Server-side safety-refusal fallback for Fable 5 (configurable, on by default)
|
|
127
160
|
- Subscription rate limit detection — fails fast with a clear message instead of hanging
|
|
128
161
|
- Long-context auto-detection for large prompts
|
|
129
162
|
- TUI sidebar with live usage bars + `/usage` slash command
|
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
|
|
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
|
|
9
|
-
*
|
|
10
|
-
* (Opus 4.7, Sonnet 4.6, Haiku 4.5, Opus 4.6)
|
|
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 {
|
package/build/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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-
|
|
52
|
-
var
|
|
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
|
|
@@ -222,6 +222,84 @@ function getCachedCredentials(path) {
|
|
|
222
222
|
import { APIError, RateLimitError } from "@anthropic-ai/sdk";
|
|
223
223
|
import { createHash, randomBytes } from "node:crypto";
|
|
224
224
|
|
|
225
|
+
// src/pause-turn.ts
|
|
226
|
+
var MAX_PAUSE_TURN_CONTINUATIONS = 5;
|
|
227
|
+
function isPauseTurn(stopReason) {
|
|
228
|
+
return stopReason === "pause_turn";
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
class BlockAccumulator {
|
|
232
|
+
blocks = [];
|
|
233
|
+
open = new Map;
|
|
234
|
+
push(event) {
|
|
235
|
+
switch (event.type) {
|
|
236
|
+
case "content_block_start": {
|
|
237
|
+
this.open.set(event.index, {
|
|
238
|
+
block: JSON.parse(JSON.stringify(event.content_block))
|
|
239
|
+
});
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
case "content_block_delta": {
|
|
243
|
+
const entry = this.open.get(event.index);
|
|
244
|
+
if (!entry)
|
|
245
|
+
break;
|
|
246
|
+
const delta = event.delta;
|
|
247
|
+
if (delta.type === "text_delta") {
|
|
248
|
+
entry.block.text = (entry.block.text ?? "") + delta.text;
|
|
249
|
+
} else if (delta.type === "thinking_delta") {
|
|
250
|
+
entry.block.thinking = (entry.block.thinking ?? "") + delta.thinking;
|
|
251
|
+
} else if (delta.type === "signature_delta") {
|
|
252
|
+
entry.block.signature = (entry.block.signature ?? "") + delta.signature;
|
|
253
|
+
} else if (delta.type === "input_json_delta") {
|
|
254
|
+
entry.argsText = (entry.argsText ?? "") + delta.partial_json;
|
|
255
|
+
}
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
case "content_block_stop": {
|
|
259
|
+
const entry = this.open.get(event.index);
|
|
260
|
+
if (!entry)
|
|
261
|
+
break;
|
|
262
|
+
this.open.delete(event.index);
|
|
263
|
+
if (entry.argsText !== undefined) {
|
|
264
|
+
try {
|
|
265
|
+
entry.block.input = JSON.parse(entry.argsText || "{}");
|
|
266
|
+
} catch {
|
|
267
|
+
entry.block.input = {};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
this.blocks.push(entry.block);
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function continuationParams(params, blocks) {
|
|
277
|
+
if (blocks.length === 0)
|
|
278
|
+
return params;
|
|
279
|
+
return {
|
|
280
|
+
...params,
|
|
281
|
+
messages: [...params.messages ?? [], { role: "assistant", content: blocks }]
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
async function* withPauseTurnContinuation(initial, params, request, maxContinuations = MAX_PAUSE_TURN_CONTINUATIONS) {
|
|
285
|
+
const acc = new BlockAccumulator;
|
|
286
|
+
let stream = initial;
|
|
287
|
+
let continuations = 0;
|
|
288
|
+
outer:
|
|
289
|
+
while (true) {
|
|
290
|
+
for await (const event of stream) {
|
|
291
|
+
if (event.type === "message_delta" && isPauseTurn(event.delta?.stop_reason) && continuations < maxContinuations) {
|
|
292
|
+
continuations++;
|
|
293
|
+
stream = await request(continuationParams(params, acc.blocks));
|
|
294
|
+
continue outer;
|
|
295
|
+
}
|
|
296
|
+
acc.push(event);
|
|
297
|
+
yield event;
|
|
298
|
+
}
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
225
303
|
// src/tool-names.ts
|
|
226
304
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
227
305
|
import { homedir as homedir2 } from "node:os";
|
|
@@ -382,6 +460,14 @@ function convertUserMessage(message) {
|
|
|
382
460
|
function convertAssistantMessage(message) {
|
|
383
461
|
const content = [];
|
|
384
462
|
for (const part of message.content) {
|
|
463
|
+
const fallback = part.providerMetadata?.anthropic?.fallback ?? part.providerOptions?.anthropic?.fallback;
|
|
464
|
+
if (fallback?.from?.model && fallback?.to?.model) {
|
|
465
|
+
content.push({
|
|
466
|
+
type: "fallback",
|
|
467
|
+
from: { model: fallback.from.model },
|
|
468
|
+
to: { model: fallback.to.model }
|
|
469
|
+
});
|
|
470
|
+
}
|
|
385
471
|
switch (part.type) {
|
|
386
472
|
case "text":
|
|
387
473
|
if (part.text.length > 0) {
|
|
@@ -460,6 +546,14 @@ var idCounter = 0;
|
|
|
460
546
|
function generateId() {
|
|
461
547
|
return `block-${Date.now()}-${idCounter++}`;
|
|
462
548
|
}
|
|
549
|
+
function buildRefusalError(stopDetails, fallbackModel) {
|
|
550
|
+
const category = stopDetails?.category ?? null;
|
|
551
|
+
const explanation = stopDetails?.explanation ?? null;
|
|
552
|
+
const categoryNote = category ? ` (category: ${category})` : "";
|
|
553
|
+
const detail = explanation ? explanation : "Claude Fable 5's safety classifiers declined this request.";
|
|
554
|
+
const hint = fallbackModel ? `The configured fallback model (${fallbackModel}) also refused.` : `Retry with a fallback model such as anthropic-sdk/claude-opus-4-8.`;
|
|
555
|
+
return new Error(`Claude Fable 5 refused this request${categoryNote}. ${detail} ${hint}`);
|
|
556
|
+
}
|
|
463
557
|
function mapFinishReason(stopReason) {
|
|
464
558
|
const unified = (() => {
|
|
465
559
|
switch (stopReason) {
|
|
@@ -476,8 +570,13 @@ function mapFinishReason(stopReason) {
|
|
|
476
570
|
})();
|
|
477
571
|
return { unified, raw: stopReason ?? undefined };
|
|
478
572
|
}
|
|
479
|
-
function convertStream(anthropicStream, modelId) {
|
|
573
|
+
function convertStream(anthropicStream, modelId, context) {
|
|
480
574
|
const blockStates = new Map;
|
|
575
|
+
const ctx = {
|
|
576
|
+
apiModelId: context?.apiModelId ?? modelId,
|
|
577
|
+
fallbacksEnabled: context?.fallbacksEnabled ?? false,
|
|
578
|
+
fallbackModel: context?.fallbackModel
|
|
579
|
+
};
|
|
481
580
|
let inputTokens;
|
|
482
581
|
let outputTokens;
|
|
483
582
|
let cachedInputTokens;
|
|
@@ -486,7 +585,7 @@ function convertStream(anthropicStream, modelId) {
|
|
|
486
585
|
async start(controller) {
|
|
487
586
|
try {
|
|
488
587
|
for await (const event of anthropicStream) {
|
|
489
|
-
const parts = processEvent(event, blockStates, modelId);
|
|
588
|
+
const parts = processEvent(event, blockStates, modelId, ctx);
|
|
490
589
|
if (event.type === "message_start" && event.message.usage) {
|
|
491
590
|
const u = event.message.usage;
|
|
492
591
|
inputTokens = u.input_tokens;
|
|
@@ -496,6 +595,18 @@ function convertStream(anthropicStream, modelId) {
|
|
|
496
595
|
if (event.type === "message_delta") {
|
|
497
596
|
const delta = event;
|
|
498
597
|
outputTokens = delta.usage?.output_tokens;
|
|
598
|
+
if (delta.delta?.stop_reason === "refusal") {
|
|
599
|
+
controller.enqueue({
|
|
600
|
+
type: "error",
|
|
601
|
+
error: buildRefusalError(delta.delta?.stop_details, ctx.fallbacksEnabled ? ctx.fallbackModel : undefined)
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
if (delta.delta?.stop_reason === "pause_turn") {
|
|
605
|
+
controller.enqueue({
|
|
606
|
+
type: "error",
|
|
607
|
+
error: new Error(`Anthropic kept pausing this turn (stop_reason "pause_turn") ` + `after the provider's auto-continuation attempts. Retry the request.`)
|
|
608
|
+
});
|
|
609
|
+
}
|
|
499
610
|
const finishReason = mapFinishReason(delta.delta?.stop_reason);
|
|
500
611
|
controller.enqueue({
|
|
501
612
|
type: "finish",
|
|
@@ -532,10 +643,16 @@ function convertStream(anthropicStream, modelId) {
|
|
|
532
643
|
}
|
|
533
644
|
});
|
|
534
645
|
}
|
|
535
|
-
function processEvent(event, blockStates, modelId) {
|
|
646
|
+
function processEvent(event, blockStates, modelId, ctx) {
|
|
536
647
|
const parts = [];
|
|
537
648
|
switch (event.type) {
|
|
538
649
|
case "message_start": {
|
|
650
|
+
const servedModel = event.message.model;
|
|
651
|
+
if (ctx.fallbacksEnabled && servedModel && servedModel !== ctx.apiModelId) {
|
|
652
|
+
ctx.pendingMeta = {
|
|
653
|
+
servedBy: { from: ctx.apiModelId, to: servedModel, kind: "sticky" }
|
|
654
|
+
};
|
|
655
|
+
}
|
|
539
656
|
parts.push({
|
|
540
657
|
type: "response-metadata",
|
|
541
658
|
id: event.message.id,
|
|
@@ -547,14 +664,38 @@ function processEvent(event, blockStates, modelId) {
|
|
|
547
664
|
case "content_block_start": {
|
|
548
665
|
const index = event.index;
|
|
549
666
|
const block = event.content_block;
|
|
550
|
-
|
|
667
|
+
const takeMeta = () => {
|
|
668
|
+
const meta = ctx.pendingMeta;
|
|
669
|
+
ctx.pendingMeta = undefined;
|
|
670
|
+
return meta;
|
|
671
|
+
};
|
|
672
|
+
if (block.type === "fallback") {
|
|
673
|
+
ctx.pendingMeta = {
|
|
674
|
+
fallback: { from: block.from, to: block.to },
|
|
675
|
+
servedBy: {
|
|
676
|
+
from: block.from?.model ?? ctx.apiModelId,
|
|
677
|
+
to: block.to?.model ?? "unknown",
|
|
678
|
+
kind: "fallback"
|
|
679
|
+
}
|
|
680
|
+
};
|
|
681
|
+
} else if (block.type === "text") {
|
|
551
682
|
const id = generateId();
|
|
552
|
-
|
|
553
|
-
|
|
683
|
+
const meta = takeMeta();
|
|
684
|
+
blockStates.set(index, { type: "text", id, fallbackMeta: meta });
|
|
685
|
+
parts.push({
|
|
686
|
+
type: "text-start",
|
|
687
|
+
id,
|
|
688
|
+
...meta ? { providerMetadata: { anthropic: meta } } : {}
|
|
689
|
+
});
|
|
554
690
|
} else if (block.type === "thinking") {
|
|
555
691
|
const id = generateId();
|
|
556
|
-
|
|
557
|
-
|
|
692
|
+
const meta = takeMeta();
|
|
693
|
+
blockStates.set(index, { type: "thinking", id, fallbackMeta: meta });
|
|
694
|
+
parts.push({
|
|
695
|
+
type: "reasoning-start",
|
|
696
|
+
id,
|
|
697
|
+
...meta ? { providerMetadata: { anthropic: meta } } : {}
|
|
698
|
+
});
|
|
558
699
|
} else if (block.type === "tool_use") {
|
|
559
700
|
const toolId = block.id;
|
|
560
701
|
const toolName = toOpencodeToolName(block.name);
|
|
@@ -563,7 +704,8 @@ function processEvent(event, blockStates, modelId) {
|
|
|
563
704
|
id: toolId,
|
|
564
705
|
toolUseId: toolId,
|
|
565
706
|
toolName,
|
|
566
|
-
argsText: ""
|
|
707
|
+
argsText: "",
|
|
708
|
+
fallbackMeta: takeMeta()
|
|
567
709
|
});
|
|
568
710
|
parts.push({
|
|
569
711
|
type: "tool-input-start",
|
|
@@ -611,12 +753,14 @@ function processEvent(event, blockStates, modelId) {
|
|
|
611
753
|
if (state.type === "text") {
|
|
612
754
|
parts.push({ type: "text-end", id: state.id });
|
|
613
755
|
} else if (state.type === "thinking") {
|
|
756
|
+
const meta = {
|
|
757
|
+
...state.signature ? { signature: state.signature } : {},
|
|
758
|
+
...state.fallbackMeta
|
|
759
|
+
};
|
|
614
760
|
parts.push({
|
|
615
761
|
type: "reasoning-end",
|
|
616
762
|
id: state.id,
|
|
617
|
-
...
|
|
618
|
-
providerMetadata: { anthropic: { signature: state.signature } }
|
|
619
|
-
} : {}
|
|
763
|
+
...Object.keys(meta).length > 0 ? { providerMetadata: { anthropic: meta } } : {}
|
|
620
764
|
});
|
|
621
765
|
} else if (state.type === "tool_use") {
|
|
622
766
|
parts.push({ type: "tool-input-end", id: state.id });
|
|
@@ -624,7 +768,8 @@ function processEvent(event, blockStates, modelId) {
|
|
|
624
768
|
type: "tool-call",
|
|
625
769
|
toolCallId: state.toolUseId,
|
|
626
770
|
toolName: state.toolName,
|
|
627
|
-
input: state.argsText || "{}"
|
|
771
|
+
input: state.argsText || "{}",
|
|
772
|
+
...state.fallbackMeta ? { providerMetadata: { anthropic: state.fallbackMeta } } : {}
|
|
628
773
|
});
|
|
629
774
|
}
|
|
630
775
|
blockStates.delete(index);
|
|
@@ -695,6 +840,17 @@ function handleApiError(error) {
|
|
|
695
840
|
}
|
|
696
841
|
throw error;
|
|
697
842
|
}
|
|
843
|
+
function isRefusal(stopReason) {
|
|
844
|
+
return stopReason === "refusal";
|
|
845
|
+
}
|
|
846
|
+
function refusalError(stopDetails, fallbackModel) {
|
|
847
|
+
const category = stopDetails?.category ?? null;
|
|
848
|
+
const explanation = stopDetails?.explanation ?? null;
|
|
849
|
+
const categoryNote = category ? ` (category: ${category})` : "";
|
|
850
|
+
const detail = explanation ? explanation : "Claude Fable 5's safety classifiers declined this request.";
|
|
851
|
+
const hint = fallbackModel ? `The configured fallback model (${fallbackModel}) also refused.` : `Retry with a fallback model such as anthropic-sdk/claude-opus-4-8.`;
|
|
852
|
+
return new Error(`Claude Fable 5 refused this request${categoryNote}. ${detail} ${hint}`);
|
|
853
|
+
}
|
|
698
854
|
function mapFinishReason2(stopReason) {
|
|
699
855
|
const unified = (() => {
|
|
700
856
|
switch (stopReason) {
|
|
@@ -711,9 +867,10 @@ function mapFinishReason2(stopReason) {
|
|
|
711
867
|
})();
|
|
712
868
|
return { unified, raw: stopReason ?? undefined };
|
|
713
869
|
}
|
|
870
|
+
var FALLBACK_BETAS_HEADER = "x-anthropic-sdk-fallback-betas";
|
|
714
871
|
var BILLING_SYSTEM_BLOCK = {
|
|
715
872
|
type: "text",
|
|
716
|
-
text: "x-anthropic-billing-header: cc_version=2.1.
|
|
873
|
+
text: "x-anthropic-billing-header: cc_version=2.1.173.d11; cc_entrypoint=sdk-cli; cch=00000;"
|
|
717
874
|
};
|
|
718
875
|
var IDENTITY_SYSTEM_BLOCK = {
|
|
719
876
|
type: "text",
|
|
@@ -847,6 +1004,9 @@ class AnthropicSDKModel {
|
|
|
847
1004
|
}
|
|
848
1005
|
if (anthropicOptions.metadata)
|
|
849
1006
|
params.metadata = anthropicOptions.metadata;
|
|
1007
|
+
if (typeof anthropicOptions.refusalFallback === "string" && anthropicOptions.refusalFallback.length > 0) {
|
|
1008
|
+
params.fallbacks = [{ model: anthropicOptions.refusalFallback }];
|
|
1009
|
+
}
|
|
850
1010
|
}
|
|
851
1011
|
if (this.isOAuth && supportsContextManagement(this.apiModelId)) {
|
|
852
1012
|
const edits = [];
|
|
@@ -861,16 +1021,19 @@ class AnthropicSDKModel {
|
|
|
861
1021
|
params.context_management = { edits };
|
|
862
1022
|
}
|
|
863
1023
|
}
|
|
864
|
-
|
|
1024
|
+
const needsFallbackBetas = params.fallbacks != null || messages.some((msg) => Array.isArray(msg.content) && msg.content.some((block) => block?.type === "fallback"));
|
|
1025
|
+
return { params, warnings, needsFallbackBetas };
|
|
865
1026
|
}
|
|
866
|
-
buildRequestOptions(signal) {
|
|
1027
|
+
buildRequestOptions(signal, needsFallbackBetas) {
|
|
867
1028
|
const opts = {};
|
|
868
1029
|
if (signal)
|
|
869
1030
|
opts.signal = signal;
|
|
1031
|
+
if (needsFallbackBetas)
|
|
1032
|
+
opts.headers = { [FALLBACK_BETAS_HEADER]: "1" };
|
|
870
1033
|
return opts;
|
|
871
1034
|
}
|
|
872
1035
|
async doGenerate(options) {
|
|
873
|
-
const { params, warnings } = this.buildParams(options);
|
|
1036
|
+
const { params, warnings, needsFallbackBetas } = this.buildParams(options);
|
|
874
1037
|
if (!options.maxOutputTokens && params.max_tokens > 4096) {
|
|
875
1038
|
params.max_tokens = 4096;
|
|
876
1039
|
}
|
|
@@ -879,31 +1042,92 @@ class AnthropicSDKModel {
|
|
|
879
1042
|
response = await this.client.messages.create({
|
|
880
1043
|
...params,
|
|
881
1044
|
stream: false
|
|
882
|
-
}, this.buildRequestOptions(options.abortSignal));
|
|
1045
|
+
}, this.buildRequestOptions(options.abortSignal, needsFallbackBetas));
|
|
883
1046
|
} catch (error) {
|
|
884
1047
|
handleApiError(error);
|
|
885
1048
|
}
|
|
1049
|
+
const pausedBlocks = [];
|
|
1050
|
+
let continuations = 0;
|
|
1051
|
+
while (isPauseTurn(response.stop_reason) && continuations < MAX_PAUSE_TURN_CONTINUATIONS) {
|
|
1052
|
+
continuations++;
|
|
1053
|
+
pausedBlocks.push(...response.content);
|
|
1054
|
+
try {
|
|
1055
|
+
response = await this.client.messages.create({
|
|
1056
|
+
...continuationParams(params, pausedBlocks),
|
|
1057
|
+
stream: false
|
|
1058
|
+
}, this.buildRequestOptions(options.abortSignal, needsFallbackBetas));
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
handleApiError(error);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
if (isPauseTurn(response.stop_reason)) {
|
|
1064
|
+
throw new Error(`Anthropic paused this turn (stop_reason "pause_turn") more than ` + `${MAX_PAUSE_TURN_CONTINUATIONS} times. Retry the request.`);
|
|
1065
|
+
}
|
|
1066
|
+
const responseContent = [...pausedBlocks, ...response.content];
|
|
1067
|
+
const fallbacksSent = Array.isArray(params.fallbacks) && params.fallbacks.length > 0;
|
|
1068
|
+
if (isRefusal(response.stop_reason)) {
|
|
1069
|
+
throw refusalError(response.stop_details, fallbacksSent ? params.fallbacks[0].model : undefined);
|
|
1070
|
+
}
|
|
1071
|
+
const fallbackBlock = responseContent.find((b) => b.type === "fallback");
|
|
1072
|
+
const sticky = !fallbackBlock && fallbacksSent && response.model && response.model !== this.apiModelId;
|
|
1073
|
+
let pendingMeta;
|
|
1074
|
+
if (fallbackBlock) {
|
|
1075
|
+
pendingMeta = {
|
|
1076
|
+
fallback: { from: fallbackBlock.from, to: fallbackBlock.to },
|
|
1077
|
+
servedBy: {
|
|
1078
|
+
from: fallbackBlock.from?.model ?? this.apiModelId,
|
|
1079
|
+
to: fallbackBlock.to?.model ?? "unknown",
|
|
1080
|
+
kind: "fallback"
|
|
1081
|
+
}
|
|
1082
|
+
};
|
|
1083
|
+
} else if (sticky) {
|
|
1084
|
+
pendingMeta = {
|
|
1085
|
+
servedBy: { from: this.apiModelId, to: response.model, kind: "sticky" }
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
886
1088
|
const content = [];
|
|
887
|
-
|
|
1089
|
+
const takeMeta = () => {
|
|
1090
|
+
const meta = pendingMeta;
|
|
1091
|
+
pendingMeta = undefined;
|
|
1092
|
+
return meta;
|
|
1093
|
+
};
|
|
1094
|
+
let seenFallbackBlock = false;
|
|
1095
|
+
for (const block of responseContent) {
|
|
888
1096
|
switch (block.type) {
|
|
889
|
-
case "text":
|
|
890
|
-
|
|
1097
|
+
case "text": {
|
|
1098
|
+
const meta = seenFallbackBlock || sticky ? takeMeta() : undefined;
|
|
1099
|
+
content.push({
|
|
1100
|
+
type: "text",
|
|
1101
|
+
text: block.text,
|
|
1102
|
+
...meta ? { providerMetadata: { anthropic: meta } } : {}
|
|
1103
|
+
});
|
|
891
1104
|
break;
|
|
892
|
-
|
|
1105
|
+
}
|
|
1106
|
+
case "tool_use": {
|
|
1107
|
+
const meta = seenFallbackBlock || sticky ? takeMeta() : undefined;
|
|
893
1108
|
content.push({
|
|
894
1109
|
type: "tool-call",
|
|
895
1110
|
toolCallId: block.id,
|
|
896
1111
|
toolName: toOpencodeToolName(block.name),
|
|
897
|
-
input: JSON.stringify(block.input)
|
|
1112
|
+
input: JSON.stringify(block.input),
|
|
1113
|
+
...meta ? { providerMetadata: { anthropic: meta } } : {}
|
|
898
1114
|
});
|
|
899
1115
|
break;
|
|
1116
|
+
}
|
|
900
1117
|
default: {
|
|
901
1118
|
const anyBlock = block;
|
|
902
|
-
if (anyBlock.type === "
|
|
1119
|
+
if (anyBlock.type === "fallback") {
|
|
1120
|
+
seenFallbackBlock = true;
|
|
1121
|
+
} else if (anyBlock.type === "thinking" && anyBlock.thinking) {
|
|
1122
|
+
const extra = seenFallbackBlock || sticky ? takeMeta() : undefined;
|
|
1123
|
+
const meta = {
|
|
1124
|
+
...anyBlock.signature ? { signature: anyBlock.signature } : {},
|
|
1125
|
+
...extra
|
|
1126
|
+
};
|
|
903
1127
|
content.push({
|
|
904
1128
|
type: "reasoning",
|
|
905
1129
|
text: anyBlock.thinking,
|
|
906
|
-
providerMetadata:
|
|
1130
|
+
providerMetadata: Object.keys(meta).length > 0 ? { anthropic: meta } : undefined
|
|
907
1131
|
});
|
|
908
1132
|
}
|
|
909
1133
|
break;
|
|
@@ -942,13 +1166,13 @@ class AnthropicSDKModel {
|
|
|
942
1166
|
};
|
|
943
1167
|
}
|
|
944
1168
|
async doStream(options) {
|
|
945
|
-
const { params, warnings } = this.buildParams(options);
|
|
1169
|
+
const { params, warnings, needsFallbackBetas } = this.buildParams(options);
|
|
946
1170
|
let anthropicStream;
|
|
947
1171
|
try {
|
|
948
1172
|
anthropicStream = await this.client.messages.create({
|
|
949
1173
|
...params,
|
|
950
1174
|
stream: true
|
|
951
|
-
}, this.buildRequestOptions(options.abortSignal));
|
|
1175
|
+
}, this.buildRequestOptions(options.abortSignal, needsFallbackBetas));
|
|
952
1176
|
} catch (error) {
|
|
953
1177
|
if (error instanceof APIError && error.status === 400 && error.message?.includes("must end with a user message")) {
|
|
954
1178
|
return {
|
|
@@ -976,7 +1200,14 @@ class AnthropicSDKModel {
|
|
|
976
1200
|
}
|
|
977
1201
|
handleApiError(error);
|
|
978
1202
|
}
|
|
979
|
-
const
|
|
1203
|
+
const makeRequest = (p) => this.client.messages.create({ ...p, stream: true }, this.buildRequestOptions(options.abortSignal, needsFallbackBetas));
|
|
1204
|
+
const continued = withPauseTurnContinuation(anthropicStream, params, makeRequest);
|
|
1205
|
+
const fallbacksSent = Array.isArray(params.fallbacks) && params.fallbacks.length > 0;
|
|
1206
|
+
const stream = convertStream(continued, this.modelId, {
|
|
1207
|
+
apiModelId: this.apiModelId,
|
|
1208
|
+
fallbacksEnabled: fallbacksSent,
|
|
1209
|
+
fallbackModel: fallbacksSent ? params.fallbacks[0].model : undefined
|
|
1210
|
+
});
|
|
980
1211
|
const wrappedStream = new ReadableStream({
|
|
981
1212
|
async start(controller) {
|
|
982
1213
|
controller.enqueue({
|
|
@@ -1099,13 +1330,14 @@ var API_TTL_MS = 5 * 60000;
|
|
|
1099
1330
|
|
|
1100
1331
|
// src/index.ts
|
|
1101
1332
|
var CLAUDE_CODE_SYSTEM_PROMPT = claudecode_system_default;
|
|
1102
|
-
var
|
|
1333
|
+
var CLAUDE_CODE_NEW_SYSTEM_PROMPT = claudecode_system_new_default;
|
|
1103
1334
|
function selectClaudePromptForModel(modelId) {
|
|
1104
|
-
if (modelId.includes("opus-4-8"))
|
|
1105
|
-
return
|
|
1335
|
+
if (modelId.includes("opus-4-8") || modelId.includes("fable-5")) {
|
|
1336
|
+
return claudecode_system_new_default;
|
|
1337
|
+
}
|
|
1106
1338
|
return claudecode_system_default;
|
|
1107
1339
|
}
|
|
1108
|
-
var CLAUDE_CODE_VERSION = "2.1.
|
|
1340
|
+
var CLAUDE_CODE_VERSION = "2.1.173";
|
|
1109
1341
|
var OAUTH_BETAS = [
|
|
1110
1342
|
"claude-code-20250219",
|
|
1111
1343
|
"oauth-2025-04-20",
|
|
@@ -1122,7 +1354,8 @@ var PROVIDER_ID = "anthropic-sdk";
|
|
|
1122
1354
|
var API_KEY_COSTS = {
|
|
1123
1355
|
haiku: { input: 1, output: 5, cache_read: 0.1, cache_write: 1.25 },
|
|
1124
1356
|
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 }
|
|
1357
|
+
opus: { input: 5, output: 25, cache_read: 0.5, cache_write: 6.25 },
|
|
1358
|
+
fable: { input: 10, output: 50, cache_read: 1, cache_write: 12.5 }
|
|
1126
1359
|
};
|
|
1127
1360
|
var ZERO_COST = { input: 0, output: 0, cache_read: 0, cache_write: 0 };
|
|
1128
1361
|
function buildPluginModels(isOAuth) {
|
|
@@ -1236,6 +1469,21 @@ function buildPluginModels(isOAuth) {
|
|
|
1236
1469
|
},
|
|
1237
1470
|
options: { effort: "medium" },
|
|
1238
1471
|
...opus47Variants
|
|
1472
|
+
},
|
|
1473
|
+
"claude-fable-5": {
|
|
1474
|
+
name: "Claude Fable 5",
|
|
1475
|
+
reasoning: true,
|
|
1476
|
+
tool_call: true,
|
|
1477
|
+
attachment: true,
|
|
1478
|
+
temperature: true,
|
|
1479
|
+
limit: { context: 1e6, output: 128000 },
|
|
1480
|
+
cost: cost("fable"),
|
|
1481
|
+
modalities: {
|
|
1482
|
+
input: ["text", "image", "pdf"],
|
|
1483
|
+
output: ["text"]
|
|
1484
|
+
},
|
|
1485
|
+
options: { effort: "medium", refusalFallback: "claude-opus-4-8" },
|
|
1486
|
+
...opus47Variants
|
|
1239
1487
|
}
|
|
1240
1488
|
};
|
|
1241
1489
|
}
|
|
@@ -1300,7 +1548,7 @@ function createAnthropicSDK(options = {}) {
|
|
|
1300
1548
|
if (!betas.includes("effort-2025-11-24"))
|
|
1301
1549
|
betas.push("effort-2025-11-24");
|
|
1302
1550
|
}
|
|
1303
|
-
if (model.includes("opus-4-8")) {
|
|
1551
|
+
if (model.includes("opus-4-8") || model.includes("fable-5")) {
|
|
1304
1552
|
if (!betas.includes("mid-conversation-system-2026-04-07")) {
|
|
1305
1553
|
betas.push("mid-conversation-system-2026-04-07");
|
|
1306
1554
|
}
|
|
@@ -1308,6 +1556,19 @@ function createAnthropicSDK(options = {}) {
|
|
|
1308
1556
|
reqHeaders.set("anthropic-beta", betas.join(","));
|
|
1309
1557
|
init = { ...init, headers: reqHeaders };
|
|
1310
1558
|
}
|
|
1559
|
+
{
|
|
1560
|
+
const reqHeaders = new Headers(init?.headers);
|
|
1561
|
+
if (reqHeaders.has(FALLBACK_BETAS_HEADER)) {
|
|
1562
|
+
reqHeaders.delete(FALLBACK_BETAS_HEADER);
|
|
1563
|
+
const betas = (reqHeaders.get("anthropic-beta") ?? "").split(",").map((beta) => beta.trim()).filter(Boolean);
|
|
1564
|
+
for (const beta of ["server-side-fallback-2026-06-01", "fallback-credit-2026-06-01"]) {
|
|
1565
|
+
if (!betas.includes(beta))
|
|
1566
|
+
betas.push(beta);
|
|
1567
|
+
}
|
|
1568
|
+
reqHeaders.set("anthropic-beta", betas.join(","));
|
|
1569
|
+
init = { ...init, headers: reqHeaders };
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1311
1572
|
const resp = await baseFetch(url, init);
|
|
1312
1573
|
const h5 = resp.headers.get("anthropic-ratelimit-unified-5h-utilization");
|
|
1313
1574
|
if (h5 != null) {
|
|
@@ -1356,10 +1617,22 @@ var anthropicSDKPlugin = async () => {
|
|
|
1356
1617
|
if (!cfg.provider) {
|
|
1357
1618
|
cfg.provider = {};
|
|
1358
1619
|
}
|
|
1620
|
+
const userProvider = cfg.provider[PROVIDER_ID] ?? {};
|
|
1621
|
+
const defaultModels = buildPluginModels(isOAuth);
|
|
1622
|
+
const models = { ...defaultModels };
|
|
1623
|
+
for (const [modelID, userModel] of Object.entries(userProvider.models ?? {})) {
|
|
1624
|
+
const base = defaultModels[modelID];
|
|
1625
|
+
models[modelID] = base ? {
|
|
1626
|
+
...base,
|
|
1627
|
+
...userModel,
|
|
1628
|
+
options: { ...base.options, ...userModel?.options },
|
|
1629
|
+
variants: { ...base.variants, ...userModel?.variants }
|
|
1630
|
+
} : userModel;
|
|
1631
|
+
}
|
|
1359
1632
|
cfg.provider[PROVIDER_ID] = {
|
|
1360
1633
|
npm: PACKAGE_NAME,
|
|
1361
|
-
|
|
1362
|
-
|
|
1634
|
+
...userProvider,
|
|
1635
|
+
models
|
|
1363
1636
|
};
|
|
1364
1637
|
},
|
|
1365
1638
|
"experimental.chat.system.transform": async (input, output) => {
|
|
@@ -1391,5 +1664,5 @@ export {
|
|
|
1391
1664
|
createAnthropicSDK,
|
|
1392
1665
|
anthropicSDKPlugin,
|
|
1393
1666
|
CLAUDE_CODE_SYSTEM_PROMPT,
|
|
1394
|
-
|
|
1667
|
+
CLAUDE_CODE_NEW_SYSTEM_PROMPT
|
|
1395
1668
|
};
|