@xtruder/opencode-claude-max-plugin 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- 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
|
@@ -27,7 +27,7 @@ Add the plugin to your `opencode.json` (project-level or `~/.config/opencode/ope
|
|
|
27
27
|
|
|
28
28
|
That's it. The plugin self-registers the `anthropic-sdk` provider and its models (Haiku 4.5, Sonnet 4.6, Opus 4.6, Opus 4.7, Opus 4.8) at startup via the OpenCode config hook. No separate `provider` block is needed.
|
|
29
29
|
|
|
30
|
-
Then open OpenCode and
|
|
30
|
+
Then open OpenCode and models will automatically be available under `anthropic-sdk` provider.
|
|
31
31
|
|
|
32
32
|
### TUI Plugin (sidebar + /usage command)
|
|
33
33
|
|
package/build/index.d.ts
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
import type { LanguageModelV3 } from "@ai-sdk/provider";
|
|
2
2
|
import type { Plugin } from "@opencode-ai/plugin";
|
|
3
3
|
export declare const CLAUDE_CODE_SYSTEM_PROMPT: string;
|
|
4
|
-
export declare const
|
|
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"}
|
package/build/server.js
CHANGED
|
@@ -48,8 +48,8 @@ function hasCchPlaceholder(body) {
|
|
|
48
48
|
return block?.text?.includes("cch=00000") ?? false;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
// src/claudecode-system-
|
|
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) => {
|
package/build/stream.d.ts
CHANGED
|
@@ -1,6 +1,36 @@
|
|
|
1
1
|
import type { LanguageModelV3StreamPart } from "@ai-sdk/provider";
|
|
2
2
|
import type Anthropic from "@anthropic-ai/sdk";
|
|
3
3
|
type MessageStreamEvent = Anthropic.MessageStreamEvent;
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Options and mutable state for one streamed response.
|
|
6
|
+
*
|
|
7
|
+
* Server-side fallback (Fable 5 safety refusals → Opus 4.8) surfaces in two
|
|
8
|
+
* ways that we translate into part metadata:
|
|
9
|
+
*
|
|
10
|
+
* - A `fallback` content block (content_block_start/stop, no deltas) marks
|
|
11
|
+
* the hop where the declining model's output gives way to the fallback
|
|
12
|
+
* model. We don't emit a part for it; instead we carry two keys on the
|
|
13
|
+
* NEXT block's part metadata (OpenCode persists part metadata and syncs
|
|
14
|
+
* it to the TUI — the same round trip thinking signatures use):
|
|
15
|
+
* anthropic.fallback — {from, to}; prompt.ts re-inserts the block into
|
|
16
|
+
* history verbatim. Position is load-bearing for thinking
|
|
17
|
+
* verification, so it must precede exactly this block.
|
|
18
|
+
* anthropic.servedBy — {from, to, kind} display-only notice for the
|
|
19
|
+
* TUI plugin (toast + sidebar). Never echoed back.
|
|
20
|
+
* - Sticky-routed follow-up turns carry no block; message_start announcing
|
|
21
|
+
* a different model than requested is the signal. servedBy (kind
|
|
22
|
+
* "sticky") attaches to the first content block.
|
|
23
|
+
*/
|
|
24
|
+
interface StreamContext {
|
|
25
|
+
/** Model ID requested by the caller (without -1m suffix). */
|
|
26
|
+
apiModelId: string;
|
|
27
|
+
/** Whether the request carried a `fallbacks` chain. */
|
|
28
|
+
fallbacksEnabled: boolean;
|
|
29
|
+
/** First fallback model — used for error wording when the chain refuses. */
|
|
30
|
+
fallbackModel?: string;
|
|
31
|
+
/** Metadata waiting to be attached to the next content block. */
|
|
32
|
+
pendingMeta?: Record<string, any>;
|
|
33
|
+
}
|
|
34
|
+
export declare function convertStream(anthropicStream: AsyncIterable<MessageStreamEvent>, modelId: string, context?: Partial<StreamContext>): ReadableStream<LanguageModelV3StreamPart>;
|
|
5
35
|
export {};
|
|
6
36
|
//# sourceMappingURL=stream.d.ts.map
|
package/build/stream.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA+B,yBAAyB,EAAE,MAAM,kBAAkB,CAAA;AAC9F,OAAO,KAAK,SAAS,MAAM,mBAAmB,CAAA;AAG9C,KAAK,kBAAkB,GAAG,SAAS,CAAC,kBAAkB,CAAA;
|
|
1
|
+
{"version":3,"file":"stream.d.ts","sourceRoot":"","sources":["../src/stream.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA+B,yBAAyB,EAAE,MAAM,kBAAkB,CAAA;AAC9F,OAAO,KAAK,SAAS,MAAM,mBAAmB,CAAA;AAG9C,KAAK,kBAAkB,GAAG,SAAS,CAAC,kBAAkB,CAAA;AAetD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,UAAU,aAAa;IACrB,6DAA6D;IAC7D,UAAU,EAAE,MAAM,CAAA;IAClB,uDAAuD;IACvD,gBAAgB,EAAE,OAAO,CAAA;IACzB,4EAA4E;IAC5E,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;CAClC;AA0CD,wBAAgB,aAAa,CAC3B,eAAe,EAAE,aAAa,CAAC,kBAAkB,CAAC,EAClD,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,GAC/B,cAAc,CAAC,yBAAyB,CAAC,CA0E3C"}
|
package/build/tui.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tui.d.ts","sourceRoot":"","sources":["../src/tui.tsx"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,OAAO,KAAK,EAGV,eAAe,EAEhB,MAAM,yBAAyB,CAAA;
|
|
1
|
+
{"version":3,"file":"tui.d.ts","sourceRoot":"","sources":["../src/tui.tsx"],"names":[],"mappings":"AAAA,sCAAsC;AACtC,OAAO,KAAK,EAGV,eAAe,EAEhB,MAAM,yBAAyB,CAAA;AAujBhC,QAAA,MAAM,MAAM,EAAE,eAAe,GAAG;IAAE,EAAE,EAAE,MAAM,CAAA;CAG3C,CAAA;AAED,eAAe,MAAM,CAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xtruder/opencode-claude-max-plugin",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "OpenCode plugin for Claude Max/Pro subscription via @anthropic-ai/sdk - uses Claude Code OAuth credentials",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-sdk",
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
"prepare": "simple-git-hooks"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@anthropic-ai/sdk": "0.
|
|
52
|
+
"@anthropic-ai/sdk": "0.104.1",
|
|
53
53
|
"xxhash-wasm": "^1.1.0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
package/src/tui.tsx
CHANGED
|
@@ -132,6 +132,144 @@ function createUsageStore(pollInterval: number) {
|
|
|
132
132
|
}
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
// ─── Fallback store ──────────────────────────────────────────────────
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Watches for server-side model fallback (Fable 5 safety refusal → Opus 4.8).
|
|
139
|
+
*
|
|
140
|
+
* The provider attaches a display-only `anthropic.servedBy` object to part
|
|
141
|
+
* metadata when a response was served by a fallback model (see stream.ts).
|
|
142
|
+
* OpenCode persists part metadata and forwards it here via
|
|
143
|
+
* `message.part.updated` — no side-channel store is needed.
|
|
144
|
+
*
|
|
145
|
+
* Sticky routing is keyed on the conversation prefix server-side, so the
|
|
146
|
+
* fallback state is PER SESSION — a refusal in one session says nothing
|
|
147
|
+
* about another.
|
|
148
|
+
*
|
|
149
|
+
* The sidebar state is DERIVED from the session's persisted messages (via
|
|
150
|
+
* api.state, OpenCode's synced replica) rather than accumulated from live
|
|
151
|
+
* events — so it is correct after a TUI restart or when opening an old
|
|
152
|
+
* session. The event subscription only drives the live toast, deduped with
|
|
153
|
+
* an in-memory Set (part events never replay across restarts).
|
|
154
|
+
*/
|
|
155
|
+
type FallbackInfo = {
|
|
156
|
+
from: string
|
|
157
|
+
to: string
|
|
158
|
+
kind: "fallback" | "sticky"
|
|
159
|
+
at: number
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const shortModel = (model: string) => model.replace(/^claude-/, "")
|
|
163
|
+
|
|
164
|
+
// Absolute time — stable without periodic re-rendering ("ago" phrasing
|
|
165
|
+
// would freeze between reactive updates).
|
|
166
|
+
const atTime = (at: number) =>
|
|
167
|
+
new Date(at).toLocaleTimeString(undefined, { hour: "numeric", minute: "2-digit" })
|
|
168
|
+
|
|
169
|
+
function createFallbackStore(api: Api) {
|
|
170
|
+
// Dedupe the repeated part-update events that fire as a part streams.
|
|
171
|
+
const seenParts = new Set<string>()
|
|
172
|
+
|
|
173
|
+
/** Extract the servedBy marker from one assistant message's parts. */
|
|
174
|
+
const markerOf = (msg: any): FallbackInfo | null => {
|
|
175
|
+
let latest: FallbackInfo | null = null
|
|
176
|
+
for (const part of api.state.part(msg.id)) {
|
|
177
|
+
const servedBy = (part as any)?.metadata?.anthropic?.servedBy
|
|
178
|
+
if (!servedBy?.to) continue
|
|
179
|
+
const time = (part as any)?.time
|
|
180
|
+
const at = time?.end ?? time?.start ?? msg.time?.created ?? 0
|
|
181
|
+
latest = {
|
|
182
|
+
from: String(servedBy.from ?? "unknown"),
|
|
183
|
+
to: String(servedBy.to),
|
|
184
|
+
kind: servedBy.kind === "sticky" ? "sticky" : "fallback",
|
|
185
|
+
at,
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return latest
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Walk a session's messages newest-first, yielding real assistant turns
|
|
193
|
+
* (skipping title-generation / compaction summaries, which never carry
|
|
194
|
+
* routing markers and would mask the real last turn).
|
|
195
|
+
*/
|
|
196
|
+
function* assistantTurns(sessionID: string, beforeMessageID?: string) {
|
|
197
|
+
const messages = api.state.session.messages(sessionID)
|
|
198
|
+
let skipping = beforeMessageID !== undefined
|
|
199
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
200
|
+
const msg = messages[i] as any
|
|
201
|
+
if (skipping) {
|
|
202
|
+
if (msg?.id === beforeMessageID) skipping = false
|
|
203
|
+
continue
|
|
204
|
+
}
|
|
205
|
+
if (msg?.role !== "assistant") continue
|
|
206
|
+
if (msg.summary) continue
|
|
207
|
+
yield msg
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Live toast — only on the TRANSITION into fallback. A conversation that
|
|
212
|
+
// trips the classifier every turn would otherwise toast on every message;
|
|
213
|
+
// the sidebar line covers the continuing state. Sticky follow-ups never
|
|
214
|
+
// toast for the same reason.
|
|
215
|
+
const off = api.event.on("message.part.updated", (event) => {
|
|
216
|
+
const part = (event as any).properties?.part
|
|
217
|
+
const sessionID = (event as any).properties?.sessionID ?? part?.sessionID
|
|
218
|
+
const servedBy = part?.metadata?.anthropic?.servedBy
|
|
219
|
+
if (!servedBy?.to || !part?.id || !part?.messageID || !sessionID) return
|
|
220
|
+
if (seenParts.has(part.id)) return
|
|
221
|
+
seenParts.add(part.id)
|
|
222
|
+
if (servedBy.kind === "sticky") return
|
|
223
|
+
|
|
224
|
+
// Was the previous completed assistant turn already fallback-served?
|
|
225
|
+
for (const msg of assistantTurns(sessionID, part.messageID)) {
|
|
226
|
+
if (markerOf(msg)) return // continuation, not a transition
|
|
227
|
+
if (msg.time?.completed) break // last known state was the primary model
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
api.ui.toast({
|
|
231
|
+
variant: "warning",
|
|
232
|
+
title: "Model fallback",
|
|
233
|
+
message: `${shortModel(String(servedBy.from ?? "unknown"))} refused — answered by ${shortModel(String(servedBy.to))}`,
|
|
234
|
+
duration: 6000,
|
|
235
|
+
})
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Fallback state for one session, derived from its message parts — a pure
|
|
240
|
+
* reactive derivation that re-evaluates only when session state changes.
|
|
241
|
+
*
|
|
242
|
+
* Only the MOST RECENT *settled* assistant turn is consulted: sticky
|
|
243
|
+
* routing re-emits a servedBy marker on every turn while active, and a
|
|
244
|
+
* turn that routed back to the primary model carries no marker. Looking
|
|
245
|
+
* further back would resurrect a stale fallback after the session has
|
|
246
|
+
* already returned to the requested model.
|
|
247
|
+
*
|
|
248
|
+
* A still-streaming turn without a marker is "unknown", not "no
|
|
249
|
+
* fallback" — the previous completed turn's state keeps showing until
|
|
250
|
+
* the new turn settles (otherwise the line flickers off on every send
|
|
251
|
+
* and back on when the refusal hits mid-stream).
|
|
252
|
+
*
|
|
253
|
+
* No time window: "the latest turn was served by X" is a fact that stays
|
|
254
|
+
* true until the next turn says otherwise. Whether the server's sticky
|
|
255
|
+
* routing is still active is unobservable — we don't guess.
|
|
256
|
+
*/
|
|
257
|
+
const active = (sessionID: string | undefined): FallbackInfo | null => {
|
|
258
|
+
if (!sessionID) return null
|
|
259
|
+
for (const msg of assistantTurns(sessionID)) {
|
|
260
|
+
const marker = markerOf(msg)
|
|
261
|
+
if (marker) return marker
|
|
262
|
+
// No marker on a finished turn → currently served by the requested
|
|
263
|
+
// model. No marker on a streaming turn → not known yet, keep looking
|
|
264
|
+
// at the previous turn.
|
|
265
|
+
if (msg.time?.completed) return null
|
|
266
|
+
}
|
|
267
|
+
return null
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return { active, dispose: off }
|
|
271
|
+
}
|
|
272
|
+
|
|
135
273
|
// ─── Helpers ─────────────────────────────────────────────────────────
|
|
136
274
|
|
|
137
275
|
const clamp01 = (v: number) => Math.max(0, Math.min(1, v))
|
|
@@ -242,6 +380,34 @@ function UsageSidebar(props: {
|
|
|
242
380
|
)
|
|
243
381
|
}
|
|
244
382
|
|
|
383
|
+
// ─── Fallback Sidebar Component ──────────────────────────────────────
|
|
384
|
+
|
|
385
|
+
function FallbackSidebar(props: {
|
|
386
|
+
theme: TuiThemeCurrent
|
|
387
|
+
sessionID: string | undefined
|
|
388
|
+
active: (sessionID: string | undefined) => FallbackInfo | null
|
|
389
|
+
}) {
|
|
390
|
+
return (
|
|
391
|
+
<Show when={props.active(props.sessionID)}>
|
|
392
|
+
{(info: Accessor<FallbackInfo>) => (
|
|
393
|
+
<box width="100%" flexDirection="column">
|
|
394
|
+
<text fg={props.theme.warning}>
|
|
395
|
+
<b>Model Fallback</b>
|
|
396
|
+
</text>
|
|
397
|
+
<text fg={props.theme.text}>
|
|
398
|
+
{shortModel(info().from)} → {shortModel(info().to)}
|
|
399
|
+
</text>
|
|
400
|
+
<text fg={props.theme.textMuted}>
|
|
401
|
+
{info().kind === "sticky"
|
|
402
|
+
? `sticky-routed at ${atTime(info().at)}`
|
|
403
|
+
: `refused at ${atTime(info().at)}`}
|
|
404
|
+
</text>
|
|
405
|
+
</box>
|
|
406
|
+
)}
|
|
407
|
+
</Show>
|
|
408
|
+
)
|
|
409
|
+
}
|
|
410
|
+
|
|
245
411
|
// ─── Dialog Component ────────────────────────────────────────────────
|
|
246
412
|
|
|
247
413
|
function UsageDialog(props: {
|
|
@@ -353,6 +519,10 @@ const tui: TuiPlugin = async (api, options) => {
|
|
|
353
519
|
const store = createUsageStore(config.poll_interval)
|
|
354
520
|
api.lifecycle.onDispose(store.dispose)
|
|
355
521
|
|
|
522
|
+
// Model fallback notifications (toast) + sidebar status line
|
|
523
|
+
const fallback = createFallbackStore(api)
|
|
524
|
+
api.lifecycle.onDispose(fallback.dispose)
|
|
525
|
+
|
|
356
526
|
// Refresh when session goes idle (inference completed — server just wrote new headers)
|
|
357
527
|
const offEvent = api.event.on("session.idle", () => {
|
|
358
528
|
setTimeout(store.refresh, 1000)
|
|
@@ -360,31 +530,40 @@ const tui: TuiPlugin = async (api, options) => {
|
|
|
360
530
|
api.lifecycle.onDispose(offEvent)
|
|
361
531
|
|
|
362
532
|
// Register /usage command
|
|
363
|
-
api.
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
533
|
+
const offCommand = api.keymap.registerLayer({
|
|
534
|
+
commands: [
|
|
535
|
+
{
|
|
536
|
+
namespace: "palette",
|
|
537
|
+
name: "anthropic-sdk.usage",
|
|
538
|
+
title: "Claude subscription usage",
|
|
539
|
+
category: "Claude",
|
|
540
|
+
slashName: "usage",
|
|
541
|
+
run() {
|
|
542
|
+
store.refresh()
|
|
543
|
+
api.ui.dialog.replace(() => (
|
|
544
|
+
<UsageDialog api={api} usage={store.usage} status={store.status} />
|
|
545
|
+
))
|
|
546
|
+
},
|
|
376
547
|
},
|
|
377
|
-
|
|
378
|
-
|
|
548
|
+
],
|
|
549
|
+
})
|
|
550
|
+
api.lifecycle.onDispose(offCommand)
|
|
379
551
|
|
|
380
552
|
// Register sidebar slot
|
|
381
553
|
if (config.sidebar) {
|
|
382
554
|
api.slots.register({
|
|
383
555
|
order: 110,
|
|
384
556
|
slots: {
|
|
385
|
-
sidebar_content(ctx) {
|
|
557
|
+
sidebar_content(ctx, props) {
|
|
386
558
|
return (
|
|
387
|
-
|
|
559
|
+
<>
|
|
560
|
+
<UsageSidebar theme={ctx.theme.current} usage={store.usage} status={store.status} />
|
|
561
|
+
<FallbackSidebar
|
|
562
|
+
theme={ctx.theme.current}
|
|
563
|
+
sessionID={(props as { session_id?: string } | undefined)?.session_id}
|
|
564
|
+
active={fallback.active}
|
|
565
|
+
/>
|
|
566
|
+
</>
|
|
388
567
|
)
|
|
389
568
|
},
|
|
390
569
|
},
|