@xtruder/opencode-claude-max-plugin 0.4.0 → 0.4.2
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 +35 -2
- package/build/index.js +119 -4
- package/build/model.d.ts.map +1 -1
- package/build/pause-turn.d.ts +36 -0
- package/build/pause-turn.d.ts.map +1 -0
- package/build/server.js +119 -4
- package/build/stream.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,8 +9,9 @@ An [OpenCode](https://opencode.ai/) plugin that enables Claude Pro/Max subscript
|
|
|
9
9
|
- **Use your Claude subscription** — Automatically reads OAuth credentials from Claude Code, no separate API key needed
|
|
10
10
|
- **Matches Claude Code 2.1.154** — Same request format and behavior as the official CLI
|
|
11
11
|
- **Prompt caching** — Multi-turn conversations cache properly, keeping costs and latency low
|
|
12
|
-
- **All Claude models** — Opus 4.8, Opus 4.7, Opus 4.6, Sonnet 4.6, Haiku 4.5
|
|
12
|
+
- **All Claude models** — Fable 5, Opus 4.8, Opus 4.7, Opus 4.6, Sonnet 4.6, Haiku 4.5
|
|
13
13
|
- **Extended / adaptive thinking** — Full reasoning support across models, including Opus 4.7+'s adaptive thinking
|
|
14
|
+
- **Safety-refusal fallback** — Fable 5 refusals are transparently answered by Opus 4.8 in the same request, with TUI notification
|
|
14
15
|
- **Usage tracking** — Sidebar widget with live progress bars + `/usage` command
|
|
15
16
|
- **Self-registering** — Models are registered automatically, no manual provider config needed
|
|
16
17
|
|
|
@@ -25,7 +26,7 @@ Add the plugin to your `opencode.json` (project-level or `~/.config/opencode/ope
|
|
|
25
26
|
}
|
|
26
27
|
```
|
|
27
28
|
|
|
28
|
-
That's it. The plugin self-registers the `anthropic-sdk` provider and its models (Haiku 4.5, Sonnet 4.6, Opus 4.6, Opus 4.7, Opus 4.8) at startup via the OpenCode config hook. No separate `provider` block is needed.
|
|
29
|
+
That's it. The plugin self-registers the `anthropic-sdk` provider and its models (Haiku 4.5, Sonnet 4.6, Opus 4.6, Opus 4.7, Opus 4.8, Fable 5) at startup via the OpenCode config hook. No separate `provider` block is needed.
|
|
29
30
|
|
|
30
31
|
Then open OpenCode and models will automatically be available under `anthropic-sdk` provider.
|
|
31
32
|
|
|
@@ -54,6 +55,7 @@ The TUI plugin provides:
|
|
|
54
55
|
- **Sidebar widget** — Compact progress bars for 5-hour session and 7-day weekly usage
|
|
55
56
|
- **`/usage` command** — Opens a dialog with full usage breakdown (per-model, extra usage)
|
|
56
57
|
- **Auto-refresh** — Polls the usage API every 60s and after each inference call
|
|
58
|
+
- **Fallback indicator** — Toast when a Fable 5 refusal falls back to Opus 4.8, plus a sidebar line showing which model served the latest turn
|
|
57
59
|
|
|
58
60
|
#### TUI Configuration
|
|
59
61
|
|
|
@@ -108,6 +110,36 @@ If you want to override model settings (e.g. thinking budgets, variants), you ca
|
|
|
108
110
|
|
|
109
111
|
Config-level settings are merged with plugin defaults — you only need to specify what you want to override.
|
|
110
112
|
|
|
113
|
+
### Claude Fable 5 and safety-refusal fallback
|
|
114
|
+
|
|
115
|
+
Claude Fable 5 (`claude-fable-5`) ships with stricter safety classifiers that can refuse a request at the API level (`stop_reason: "refusal"`) — even for benign follow-ups if the conversation contains a flagged topic. To keep sessions usable, the plugin enables Anthropic's server-side fallback by default: when Fable 5 refuses, **Opus 4.8 answers the same request in the same round trip**. Tool loops keep running, thinking chains stay verified, and prompt caching is unaffected.
|
|
116
|
+
|
|
117
|
+
When a fallback happens:
|
|
118
|
+
|
|
119
|
+
- The TUI shows a toast (`fable-5 refused — answered by opus-4-8`) on the first fallback turn
|
|
120
|
+
- The sidebar shows a `Model Fallback` line while the latest turn was served by the fallback model
|
|
121
|
+
- The served model is recorded in part metadata (`anthropic.servedBy`) — the model's own self-report will still say `claude-fable-5`, since identity comes from the prompt, not the serving model
|
|
122
|
+
|
|
123
|
+
Configure via the `refusalFallback` model option:
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"provider": {
|
|
128
|
+
"anthropic-sdk": {
|
|
129
|
+
"models": {
|
|
130
|
+
"claude-fable-5": {
|
|
131
|
+
"options": {
|
|
132
|
+
"refusalFallback": false
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Set it to another model ID to change the fallback target, or `false` to disable (refusals then surface as errors with the refusal category).
|
|
142
|
+
|
|
111
143
|
## Authentication
|
|
112
144
|
|
|
113
145
|
Credentials are resolved in order:
|
|
@@ -124,6 +156,7 @@ For Claude Code credentials, log in via `claude` CLI first (`claude auth login`)
|
|
|
124
156
|
- MCP tool name remapping (`server_tool` → `mcp__server__tool`)
|
|
125
157
|
- Extended thinking (Sonnet/Opus 4.6) and adaptive thinking (Opus 4.7+) with effort levels and multi-turn signature passthrough
|
|
126
158
|
- Prompt caching that holds across long, tool-heavy conversations
|
|
159
|
+
- Server-side safety-refusal fallback for Fable 5 (configurable, on by default)
|
|
127
160
|
- Subscription rate limit detection — fails fast with a clear message instead of hanging
|
|
128
161
|
- Long-context auto-detection for large prompts
|
|
129
162
|
- TUI sidebar with live usage bars + `/usage` slash command
|
package/build/index.js
CHANGED
|
@@ -222,6 +222,84 @@ function getCachedCredentials(path) {
|
|
|
222
222
|
import { APIError, RateLimitError } from "@anthropic-ai/sdk";
|
|
223
223
|
import { createHash, randomBytes } from "node:crypto";
|
|
224
224
|
|
|
225
|
+
// src/pause-turn.ts
|
|
226
|
+
var MAX_PAUSE_TURN_CONTINUATIONS = 5;
|
|
227
|
+
function isPauseTurn(stopReason) {
|
|
228
|
+
return stopReason === "pause_turn";
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
class BlockAccumulator {
|
|
232
|
+
blocks = [];
|
|
233
|
+
open = new Map;
|
|
234
|
+
push(event) {
|
|
235
|
+
switch (event.type) {
|
|
236
|
+
case "content_block_start": {
|
|
237
|
+
this.open.set(event.index, {
|
|
238
|
+
block: JSON.parse(JSON.stringify(event.content_block))
|
|
239
|
+
});
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
case "content_block_delta": {
|
|
243
|
+
const entry = this.open.get(event.index);
|
|
244
|
+
if (!entry)
|
|
245
|
+
break;
|
|
246
|
+
const delta = event.delta;
|
|
247
|
+
if (delta.type === "text_delta") {
|
|
248
|
+
entry.block.text = (entry.block.text ?? "") + delta.text;
|
|
249
|
+
} else if (delta.type === "thinking_delta") {
|
|
250
|
+
entry.block.thinking = (entry.block.thinking ?? "") + delta.thinking;
|
|
251
|
+
} else if (delta.type === "signature_delta") {
|
|
252
|
+
entry.block.signature = (entry.block.signature ?? "") + delta.signature;
|
|
253
|
+
} else if (delta.type === "input_json_delta") {
|
|
254
|
+
entry.argsText = (entry.argsText ?? "") + delta.partial_json;
|
|
255
|
+
}
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
case "content_block_stop": {
|
|
259
|
+
const entry = this.open.get(event.index);
|
|
260
|
+
if (!entry)
|
|
261
|
+
break;
|
|
262
|
+
this.open.delete(event.index);
|
|
263
|
+
if (entry.argsText !== undefined) {
|
|
264
|
+
try {
|
|
265
|
+
entry.block.input = JSON.parse(entry.argsText || "{}");
|
|
266
|
+
} catch {
|
|
267
|
+
entry.block.input = {};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
this.blocks.push(entry.block);
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function continuationParams(params, blocks) {
|
|
277
|
+
if (blocks.length === 0)
|
|
278
|
+
return params;
|
|
279
|
+
return {
|
|
280
|
+
...params,
|
|
281
|
+
messages: [...params.messages ?? [], { role: "assistant", content: blocks }]
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
async function* withPauseTurnContinuation(initial, params, request, maxContinuations = MAX_PAUSE_TURN_CONTINUATIONS) {
|
|
285
|
+
const acc = new BlockAccumulator;
|
|
286
|
+
let stream = initial;
|
|
287
|
+
let continuations = 0;
|
|
288
|
+
outer:
|
|
289
|
+
while (true) {
|
|
290
|
+
for await (const event of stream) {
|
|
291
|
+
if (event.type === "message_delta" && isPauseTurn(event.delta?.stop_reason) && continuations < maxContinuations) {
|
|
292
|
+
continuations++;
|
|
293
|
+
stream = await request(continuationParams(params, acc.blocks));
|
|
294
|
+
continue outer;
|
|
295
|
+
}
|
|
296
|
+
acc.push(event);
|
|
297
|
+
yield event;
|
|
298
|
+
}
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
225
303
|
// src/tool-names.ts
|
|
226
304
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
227
305
|
import { homedir as homedir2 } from "node:os";
|
|
@@ -523,6 +601,12 @@ function convertStream(anthropicStream, modelId, context) {
|
|
|
523
601
|
error: buildRefusalError(delta.delta?.stop_details, ctx.fallbacksEnabled ? ctx.fallbackModel : undefined)
|
|
524
602
|
});
|
|
525
603
|
}
|
|
604
|
+
if (delta.delta?.stop_reason === "pause_turn") {
|
|
605
|
+
controller.enqueue({
|
|
606
|
+
type: "error",
|
|
607
|
+
error: new Error(`Anthropic kept pausing this turn (stop_reason "pause_turn") ` + `after the provider's auto-continuation attempts. Retry the request.`)
|
|
608
|
+
});
|
|
609
|
+
}
|
|
526
610
|
const finishReason = mapFinishReason(delta.delta?.stop_reason);
|
|
527
611
|
controller.enqueue({
|
|
528
612
|
type: "finish",
|
|
@@ -801,6 +885,9 @@ function buildMetadata() {
|
|
|
801
885
|
function supportsContextManagement(apiModelId) {
|
|
802
886
|
return !apiModelId.includes("claude-3-");
|
|
803
887
|
}
|
|
888
|
+
function usesAdaptiveThinking(apiModelId) {
|
|
889
|
+
return apiModelId.includes("claude-opus-4-7") || apiModelId.includes("claude-opus-4-8") || apiModelId.includes("claude-fable-5");
|
|
890
|
+
}
|
|
804
891
|
function placeMessageBreakpoints(messages, cache) {
|
|
805
892
|
if (messages.length === 0)
|
|
806
893
|
return;
|
|
@@ -873,6 +960,9 @@ class AnthropicSDKModel {
|
|
|
873
960
|
const providerOpts = options.providerOptions?.["anthropic-sdk"] ?? options.providerOptions?.anthropic;
|
|
874
961
|
const effort = providerOpts?.effort ?? "medium";
|
|
875
962
|
params.output_config = { effort };
|
|
963
|
+
if (usesAdaptiveThinking(this.apiModelId)) {
|
|
964
|
+
params.thinking = { type: "adaptive", display: "summarized" };
|
|
965
|
+
}
|
|
876
966
|
}
|
|
877
967
|
} else if (system) {
|
|
878
968
|
const CACHE = { type: "ephemeral" };
|
|
@@ -911,7 +1001,12 @@ class AnthropicSDKModel {
|
|
|
911
1001
|
if (anthropicOptions.thinking) {
|
|
912
1002
|
const t = anthropicOptions.thinking;
|
|
913
1003
|
if (t.type === "adaptive") {
|
|
914
|
-
params.thinking = {
|
|
1004
|
+
params.thinking = {
|
|
1005
|
+
type: "adaptive",
|
|
1006
|
+
display: t.display ?? (usesAdaptiveThinking(this.apiModelId) ? "summarized" : undefined)
|
|
1007
|
+
};
|
|
1008
|
+
if (params.thinking.display == null)
|
|
1009
|
+
delete params.thinking.display;
|
|
915
1010
|
} else if (t.type === "enabled" && t.budgetTokens) {
|
|
916
1011
|
params.thinking = { type: "enabled", budget_tokens: t.budgetTokens };
|
|
917
1012
|
} else {
|
|
@@ -962,11 +1057,29 @@ class AnthropicSDKModel {
|
|
|
962
1057
|
} catch (error) {
|
|
963
1058
|
handleApiError(error);
|
|
964
1059
|
}
|
|
1060
|
+
const pausedBlocks = [];
|
|
1061
|
+
let continuations = 0;
|
|
1062
|
+
while (isPauseTurn(response.stop_reason) && continuations < MAX_PAUSE_TURN_CONTINUATIONS) {
|
|
1063
|
+
continuations++;
|
|
1064
|
+
pausedBlocks.push(...response.content);
|
|
1065
|
+
try {
|
|
1066
|
+
response = await this.client.messages.create({
|
|
1067
|
+
...continuationParams(params, pausedBlocks),
|
|
1068
|
+
stream: false
|
|
1069
|
+
}, this.buildRequestOptions(options.abortSignal, needsFallbackBetas));
|
|
1070
|
+
} catch (error) {
|
|
1071
|
+
handleApiError(error);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
if (isPauseTurn(response.stop_reason)) {
|
|
1075
|
+
throw new Error(`Anthropic paused this turn (stop_reason "pause_turn") more than ` + `${MAX_PAUSE_TURN_CONTINUATIONS} times. Retry the request.`);
|
|
1076
|
+
}
|
|
1077
|
+
const responseContent = [...pausedBlocks, ...response.content];
|
|
965
1078
|
const fallbacksSent = Array.isArray(params.fallbacks) && params.fallbacks.length > 0;
|
|
966
1079
|
if (isRefusal(response.stop_reason)) {
|
|
967
1080
|
throw refusalError(response.stop_details, fallbacksSent ? params.fallbacks[0].model : undefined);
|
|
968
1081
|
}
|
|
969
|
-
const fallbackBlock =
|
|
1082
|
+
const fallbackBlock = responseContent.find((b) => b.type === "fallback");
|
|
970
1083
|
const sticky = !fallbackBlock && fallbacksSent && response.model && response.model !== this.apiModelId;
|
|
971
1084
|
let pendingMeta;
|
|
972
1085
|
if (fallbackBlock) {
|
|
@@ -990,7 +1103,7 @@ class AnthropicSDKModel {
|
|
|
990
1103
|
return meta;
|
|
991
1104
|
};
|
|
992
1105
|
let seenFallbackBlock = false;
|
|
993
|
-
for (const block of
|
|
1106
|
+
for (const block of responseContent) {
|
|
994
1107
|
switch (block.type) {
|
|
995
1108
|
case "text": {
|
|
996
1109
|
const meta = seenFallbackBlock || sticky ? takeMeta() : undefined;
|
|
@@ -1098,8 +1211,10 @@ class AnthropicSDKModel {
|
|
|
1098
1211
|
}
|
|
1099
1212
|
handleApiError(error);
|
|
1100
1213
|
}
|
|
1214
|
+
const makeRequest = (p) => this.client.messages.create({ ...p, stream: true }, this.buildRequestOptions(options.abortSignal, needsFallbackBetas));
|
|
1215
|
+
const continued = withPauseTurnContinuation(anthropicStream, params, makeRequest);
|
|
1101
1216
|
const fallbacksSent = Array.isArray(params.fallbacks) && params.fallbacks.length > 0;
|
|
1102
|
-
const stream = convertStream(
|
|
1217
|
+
const stream = convertStream(continued, this.modelId, {
|
|
1103
1218
|
apiModelId: this.apiModelId,
|
|
1104
1219
|
fallbacksEnabled: fallbacksSent,
|
|
1105
1220
|
fallbackModel: fallbacksSent ? params.fallbacks[0].model : undefined
|
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;
|
|
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;AAc9C,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;AAiFrE,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;IAuNnB;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;IAUrB,UAAU,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAqL1E,QAAQ,CAAC,OAAO,EAAE,0BAA0B,GAAG,OAAO,CAAC,cAAc,CAAC;CA2F7E"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-continuation for `stop_reason: "pause_turn"`.
|
|
3
|
+
*
|
|
4
|
+
* The Messages API pauses long-running turns and expects the client to
|
|
5
|
+
* resend the conversation — with the paused turn's partial assistant
|
|
6
|
+
* content appended — to let the model continue the same turn. Observed in
|
|
7
|
+
* the wild with Claude Fable 5 when the safety-refusal fallback hop runs
|
|
8
|
+
* long: the server returns `pause_turn` after ~1 output token and no
|
|
9
|
+
* content. Without auto-continuation that surfaces as a silent empty
|
|
10
|
+
* response (finish reason "unknown").
|
|
11
|
+
*
|
|
12
|
+
* The wrapper is transport-agnostic: it consumes streaming events,
|
|
13
|
+
* reconstructs the raw wire content blocks, and re-issues the request via
|
|
14
|
+
* the provided `request` callback whenever a segment ends with
|
|
15
|
+
* `pause_turn`. The paused segment's `message_delta` and `message_stop`
|
|
16
|
+
* events are suppressed so downstream conversion sees one continuous turn.
|
|
17
|
+
*/
|
|
18
|
+
import type { MessageStreamEvent } from "@anthropic-ai/sdk/resources/messages";
|
|
19
|
+
export declare const MAX_PAUSE_TURN_CONTINUATIONS = 5;
|
|
20
|
+
type RawBlock = Record<string, any>;
|
|
21
|
+
export declare function isPauseTurn(stopReason: string | null | undefined): boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Continuation request params: the original conversation with the paused
|
|
24
|
+
* turn's partial content appended as an assistant message. With no partial
|
|
25
|
+
* content (the observed Fable 5 case) the request is resent unchanged —
|
|
26
|
+
* an empty assistant content array would be rejected by the API.
|
|
27
|
+
*/
|
|
28
|
+
export declare function continuationParams(params: Record<string, any>, blocks: RawBlock[]): Record<string, any>;
|
|
29
|
+
/**
|
|
30
|
+
* Wrap a stream of Anthropic events with pause_turn auto-continuation.
|
|
31
|
+
* After `maxContinuations` pauses the pause_turn `message_delta` is passed
|
|
32
|
+
* through so stream conversion can surface a clear error.
|
|
33
|
+
*/
|
|
34
|
+
export declare function withPauseTurnContinuation(initial: AsyncIterable<MessageStreamEvent>, params: Record<string, any>, request: (params: Record<string, any>) => Promise<AsyncIterable<MessageStreamEvent>>, maxContinuations?: number): AsyncGenerator<MessageStreamEvent>;
|
|
35
|
+
export {};
|
|
36
|
+
//# sourceMappingURL=pause-turn.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pause-turn.d.ts","sourceRoot":"","sources":["../src/pause-turn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,sCAAsC,CAAA;AAE9E,eAAO,MAAM,4BAA4B,IAAI,CAAA;AAE7C,KAAK,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;AAEnC,wBAAgB,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAE1E;AAoDD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,MAAM,EAAE,QAAQ,EAAE,GACjB,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAMrB;AAED;;;;GAIG;AACH,wBAAuB,yBAAyB,CAC9C,OAAO,EAAE,aAAa,CAAC,kBAAkB,CAAC,EAC1C,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,EAC3B,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,KAAK,OAAO,CAAC,aAAa,CAAC,kBAAkB,CAAC,CAAC,EACpF,gBAAgB,SAA+B,GAC9C,cAAc,CAAC,kBAAkB,CAAC,CAuBpC"}
|
package/build/server.js
CHANGED
|
@@ -222,6 +222,84 @@ function getCachedCredentials(path) {
|
|
|
222
222
|
import { APIError, RateLimitError } from "@anthropic-ai/sdk";
|
|
223
223
|
import { createHash, randomBytes } from "node:crypto";
|
|
224
224
|
|
|
225
|
+
// src/pause-turn.ts
|
|
226
|
+
var MAX_PAUSE_TURN_CONTINUATIONS = 5;
|
|
227
|
+
function isPauseTurn(stopReason) {
|
|
228
|
+
return stopReason === "pause_turn";
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
class BlockAccumulator {
|
|
232
|
+
blocks = [];
|
|
233
|
+
open = new Map;
|
|
234
|
+
push(event) {
|
|
235
|
+
switch (event.type) {
|
|
236
|
+
case "content_block_start": {
|
|
237
|
+
this.open.set(event.index, {
|
|
238
|
+
block: JSON.parse(JSON.stringify(event.content_block))
|
|
239
|
+
});
|
|
240
|
+
break;
|
|
241
|
+
}
|
|
242
|
+
case "content_block_delta": {
|
|
243
|
+
const entry = this.open.get(event.index);
|
|
244
|
+
if (!entry)
|
|
245
|
+
break;
|
|
246
|
+
const delta = event.delta;
|
|
247
|
+
if (delta.type === "text_delta") {
|
|
248
|
+
entry.block.text = (entry.block.text ?? "") + delta.text;
|
|
249
|
+
} else if (delta.type === "thinking_delta") {
|
|
250
|
+
entry.block.thinking = (entry.block.thinking ?? "") + delta.thinking;
|
|
251
|
+
} else if (delta.type === "signature_delta") {
|
|
252
|
+
entry.block.signature = (entry.block.signature ?? "") + delta.signature;
|
|
253
|
+
} else if (delta.type === "input_json_delta") {
|
|
254
|
+
entry.argsText = (entry.argsText ?? "") + delta.partial_json;
|
|
255
|
+
}
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
case "content_block_stop": {
|
|
259
|
+
const entry = this.open.get(event.index);
|
|
260
|
+
if (!entry)
|
|
261
|
+
break;
|
|
262
|
+
this.open.delete(event.index);
|
|
263
|
+
if (entry.argsText !== undefined) {
|
|
264
|
+
try {
|
|
265
|
+
entry.block.input = JSON.parse(entry.argsText || "{}");
|
|
266
|
+
} catch {
|
|
267
|
+
entry.block.input = {};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
this.blocks.push(entry.block);
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
function continuationParams(params, blocks) {
|
|
277
|
+
if (blocks.length === 0)
|
|
278
|
+
return params;
|
|
279
|
+
return {
|
|
280
|
+
...params,
|
|
281
|
+
messages: [...params.messages ?? [], { role: "assistant", content: blocks }]
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
async function* withPauseTurnContinuation(initial, params, request, maxContinuations = MAX_PAUSE_TURN_CONTINUATIONS) {
|
|
285
|
+
const acc = new BlockAccumulator;
|
|
286
|
+
let stream = initial;
|
|
287
|
+
let continuations = 0;
|
|
288
|
+
outer:
|
|
289
|
+
while (true) {
|
|
290
|
+
for await (const event of stream) {
|
|
291
|
+
if (event.type === "message_delta" && isPauseTurn(event.delta?.stop_reason) && continuations < maxContinuations) {
|
|
292
|
+
continuations++;
|
|
293
|
+
stream = await request(continuationParams(params, acc.blocks));
|
|
294
|
+
continue outer;
|
|
295
|
+
}
|
|
296
|
+
acc.push(event);
|
|
297
|
+
yield event;
|
|
298
|
+
}
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
225
303
|
// src/tool-names.ts
|
|
226
304
|
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
|
|
227
305
|
import { homedir as homedir2 } from "node:os";
|
|
@@ -523,6 +601,12 @@ function convertStream(anthropicStream, modelId, context) {
|
|
|
523
601
|
error: buildRefusalError(delta.delta?.stop_details, ctx.fallbacksEnabled ? ctx.fallbackModel : undefined)
|
|
524
602
|
});
|
|
525
603
|
}
|
|
604
|
+
if (delta.delta?.stop_reason === "pause_turn") {
|
|
605
|
+
controller.enqueue({
|
|
606
|
+
type: "error",
|
|
607
|
+
error: new Error(`Anthropic kept pausing this turn (stop_reason "pause_turn") ` + `after the provider's auto-continuation attempts. Retry the request.`)
|
|
608
|
+
});
|
|
609
|
+
}
|
|
526
610
|
const finishReason = mapFinishReason(delta.delta?.stop_reason);
|
|
527
611
|
controller.enqueue({
|
|
528
612
|
type: "finish",
|
|
@@ -801,6 +885,9 @@ function buildMetadata() {
|
|
|
801
885
|
function supportsContextManagement(apiModelId) {
|
|
802
886
|
return !apiModelId.includes("claude-3-");
|
|
803
887
|
}
|
|
888
|
+
function usesAdaptiveThinking(apiModelId) {
|
|
889
|
+
return apiModelId.includes("claude-opus-4-7") || apiModelId.includes("claude-opus-4-8") || apiModelId.includes("claude-fable-5");
|
|
890
|
+
}
|
|
804
891
|
function placeMessageBreakpoints(messages, cache) {
|
|
805
892
|
if (messages.length === 0)
|
|
806
893
|
return;
|
|
@@ -873,6 +960,9 @@ class AnthropicSDKModel {
|
|
|
873
960
|
const providerOpts = options.providerOptions?.["anthropic-sdk"] ?? options.providerOptions?.anthropic;
|
|
874
961
|
const effort = providerOpts?.effort ?? "medium";
|
|
875
962
|
params.output_config = { effort };
|
|
963
|
+
if (usesAdaptiveThinking(this.apiModelId)) {
|
|
964
|
+
params.thinking = { type: "adaptive", display: "summarized" };
|
|
965
|
+
}
|
|
876
966
|
}
|
|
877
967
|
} else if (system) {
|
|
878
968
|
const CACHE = { type: "ephemeral" };
|
|
@@ -911,7 +1001,12 @@ class AnthropicSDKModel {
|
|
|
911
1001
|
if (anthropicOptions.thinking) {
|
|
912
1002
|
const t = anthropicOptions.thinking;
|
|
913
1003
|
if (t.type === "adaptive") {
|
|
914
|
-
params.thinking = {
|
|
1004
|
+
params.thinking = {
|
|
1005
|
+
type: "adaptive",
|
|
1006
|
+
display: t.display ?? (usesAdaptiveThinking(this.apiModelId) ? "summarized" : undefined)
|
|
1007
|
+
};
|
|
1008
|
+
if (params.thinking.display == null)
|
|
1009
|
+
delete params.thinking.display;
|
|
915
1010
|
} else if (t.type === "enabled" && t.budgetTokens) {
|
|
916
1011
|
params.thinking = { type: "enabled", budget_tokens: t.budgetTokens };
|
|
917
1012
|
} else {
|
|
@@ -962,11 +1057,29 @@ class AnthropicSDKModel {
|
|
|
962
1057
|
} catch (error) {
|
|
963
1058
|
handleApiError(error);
|
|
964
1059
|
}
|
|
1060
|
+
const pausedBlocks = [];
|
|
1061
|
+
let continuations = 0;
|
|
1062
|
+
while (isPauseTurn(response.stop_reason) && continuations < MAX_PAUSE_TURN_CONTINUATIONS) {
|
|
1063
|
+
continuations++;
|
|
1064
|
+
pausedBlocks.push(...response.content);
|
|
1065
|
+
try {
|
|
1066
|
+
response = await this.client.messages.create({
|
|
1067
|
+
...continuationParams(params, pausedBlocks),
|
|
1068
|
+
stream: false
|
|
1069
|
+
}, this.buildRequestOptions(options.abortSignal, needsFallbackBetas));
|
|
1070
|
+
} catch (error) {
|
|
1071
|
+
handleApiError(error);
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
if (isPauseTurn(response.stop_reason)) {
|
|
1075
|
+
throw new Error(`Anthropic paused this turn (stop_reason "pause_turn") more than ` + `${MAX_PAUSE_TURN_CONTINUATIONS} times. Retry the request.`);
|
|
1076
|
+
}
|
|
1077
|
+
const responseContent = [...pausedBlocks, ...response.content];
|
|
965
1078
|
const fallbacksSent = Array.isArray(params.fallbacks) && params.fallbacks.length > 0;
|
|
966
1079
|
if (isRefusal(response.stop_reason)) {
|
|
967
1080
|
throw refusalError(response.stop_details, fallbacksSent ? params.fallbacks[0].model : undefined);
|
|
968
1081
|
}
|
|
969
|
-
const fallbackBlock =
|
|
1082
|
+
const fallbackBlock = responseContent.find((b) => b.type === "fallback");
|
|
970
1083
|
const sticky = !fallbackBlock && fallbacksSent && response.model && response.model !== this.apiModelId;
|
|
971
1084
|
let pendingMeta;
|
|
972
1085
|
if (fallbackBlock) {
|
|
@@ -990,7 +1103,7 @@ class AnthropicSDKModel {
|
|
|
990
1103
|
return meta;
|
|
991
1104
|
};
|
|
992
1105
|
let seenFallbackBlock = false;
|
|
993
|
-
for (const block of
|
|
1106
|
+
for (const block of responseContent) {
|
|
994
1107
|
switch (block.type) {
|
|
995
1108
|
case "text": {
|
|
996
1109
|
const meta = seenFallbackBlock || sticky ? takeMeta() : undefined;
|
|
@@ -1098,8 +1211,10 @@ class AnthropicSDKModel {
|
|
|
1098
1211
|
}
|
|
1099
1212
|
handleApiError(error);
|
|
1100
1213
|
}
|
|
1214
|
+
const makeRequest = (p) => this.client.messages.create({ ...p, stream: true }, this.buildRequestOptions(options.abortSignal, needsFallbackBetas));
|
|
1215
|
+
const continued = withPauseTurnContinuation(anthropicStream, params, makeRequest);
|
|
1101
1216
|
const fallbacksSent = Array.isArray(params.fallbacks) && params.fallbacks.length > 0;
|
|
1102
|
-
const stream = convertStream(
|
|
1217
|
+
const stream = convertStream(continued, this.modelId, {
|
|
1103
1218
|
apiModelId: this.apiModelId,
|
|
1104
1219
|
fallbacksEnabled: fallbacksSent,
|
|
1105
1220
|
fallbackModel: fallbacksSent ? params.fallbacks[0].model : undefined
|
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;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,
|
|
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,CAsF3C"}
|
package/package.json
CHANGED