@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 +14 -17
- package/build/index.d.ts +13 -5
- package/build/index.d.ts.map +1 -1
- package/build/index.js +207 -38
- package/build/model.d.ts +25 -0
- package/build/model.d.ts.map +1 -1
- package/build/server.js +206 -37
- 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
|
@@ -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
|
|
11
|
-
- **Prompt caching** —
|
|
12
|
-
- **All Claude models** — Opus 4.6, Sonnet 4.6, Haiku 4.5
|
|
13
|
-
- **Extended thinking** — Full reasoning support
|
|
14
|
-
- **Usage tracking** — Sidebar widget with live progress bars + `/usage` command
|
|
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
|
-
|
|
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
|
|
128
|
-
- Prompt caching
|
|
129
|
-
-
|
|
130
|
-
-
|
|
131
|
-
-
|
|
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
|
|
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
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
553
|
-
|
|
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
|
-
|
|
557
|
-
|
|
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
|
-
...
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 === "
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
1362
|
-
|
|
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
|
-
|
|
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;
|
package/build/model.d.ts.map
CHANGED
|
@@ -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;
|
|
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"}
|