@xtruder/opencode-claude-max-plugin 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -2
- package/build/index.js +107 -3
- 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 +107 -3
- 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",
|
|
@@ -962,11 +1046,29 @@ class AnthropicSDKModel {
|
|
|
962
1046
|
} catch (error) {
|
|
963
1047
|
handleApiError(error);
|
|
964
1048
|
}
|
|
1049
|
+
const pausedBlocks = [];
|
|
1050
|
+
let continuations = 0;
|
|
1051
|
+
while (isPauseTurn(response.stop_reason) && continuations < MAX_PAUSE_TURN_CONTINUATIONS) {
|
|
1052
|
+
continuations++;
|
|
1053
|
+
pausedBlocks.push(...response.content);
|
|
1054
|
+
try {
|
|
1055
|
+
response = await this.client.messages.create({
|
|
1056
|
+
...continuationParams(params, pausedBlocks),
|
|
1057
|
+
stream: false
|
|
1058
|
+
}, this.buildRequestOptions(options.abortSignal, needsFallbackBetas));
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
handleApiError(error);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
if (isPauseTurn(response.stop_reason)) {
|
|
1064
|
+
throw new Error(`Anthropic paused this turn (stop_reason "pause_turn") more than ` + `${MAX_PAUSE_TURN_CONTINUATIONS} times. Retry the request.`);
|
|
1065
|
+
}
|
|
1066
|
+
const responseContent = [...pausedBlocks, ...response.content];
|
|
965
1067
|
const fallbacksSent = Array.isArray(params.fallbacks) && params.fallbacks.length > 0;
|
|
966
1068
|
if (isRefusal(response.stop_reason)) {
|
|
967
1069
|
throw refusalError(response.stop_details, fallbacksSent ? params.fallbacks[0].model : undefined);
|
|
968
1070
|
}
|
|
969
|
-
const fallbackBlock =
|
|
1071
|
+
const fallbackBlock = responseContent.find((b) => b.type === "fallback");
|
|
970
1072
|
const sticky = !fallbackBlock && fallbacksSent && response.model && response.model !== this.apiModelId;
|
|
971
1073
|
let pendingMeta;
|
|
972
1074
|
if (fallbackBlock) {
|
|
@@ -990,7 +1092,7 @@ class AnthropicSDKModel {
|
|
|
990
1092
|
return meta;
|
|
991
1093
|
};
|
|
992
1094
|
let seenFallbackBlock = false;
|
|
993
|
-
for (const block of
|
|
1095
|
+
for (const block of responseContent) {
|
|
994
1096
|
switch (block.type) {
|
|
995
1097
|
case "text": {
|
|
996
1098
|
const meta = seenFallbackBlock || sticky ? takeMeta() : undefined;
|
|
@@ -1098,8 +1200,10 @@ class AnthropicSDKModel {
|
|
|
1098
1200
|
}
|
|
1099
1201
|
handleApiError(error);
|
|
1100
1202
|
}
|
|
1203
|
+
const makeRequest = (p) => this.client.messages.create({ ...p, stream: true }, this.buildRequestOptions(options.abortSignal, needsFallbackBetas));
|
|
1204
|
+
const continued = withPauseTurnContinuation(anthropicStream, params, makeRequest);
|
|
1101
1205
|
const fallbacksSent = Array.isArray(params.fallbacks) && params.fallbacks.length > 0;
|
|
1102
|
-
const stream = convertStream(
|
|
1206
|
+
const stream = convertStream(continued, this.modelId, {
|
|
1103
1207
|
apiModelId: this.apiModelId,
|
|
1104
1208
|
fallbacksEnabled: fallbacksSent,
|
|
1105
1209
|
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;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;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",
|
|
@@ -962,11 +1046,29 @@ class AnthropicSDKModel {
|
|
|
962
1046
|
} catch (error) {
|
|
963
1047
|
handleApiError(error);
|
|
964
1048
|
}
|
|
1049
|
+
const pausedBlocks = [];
|
|
1050
|
+
let continuations = 0;
|
|
1051
|
+
while (isPauseTurn(response.stop_reason) && continuations < MAX_PAUSE_TURN_CONTINUATIONS) {
|
|
1052
|
+
continuations++;
|
|
1053
|
+
pausedBlocks.push(...response.content);
|
|
1054
|
+
try {
|
|
1055
|
+
response = await this.client.messages.create({
|
|
1056
|
+
...continuationParams(params, pausedBlocks),
|
|
1057
|
+
stream: false
|
|
1058
|
+
}, this.buildRequestOptions(options.abortSignal, needsFallbackBetas));
|
|
1059
|
+
} catch (error) {
|
|
1060
|
+
handleApiError(error);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
if (isPauseTurn(response.stop_reason)) {
|
|
1064
|
+
throw new Error(`Anthropic paused this turn (stop_reason "pause_turn") more than ` + `${MAX_PAUSE_TURN_CONTINUATIONS} times. Retry the request.`);
|
|
1065
|
+
}
|
|
1066
|
+
const responseContent = [...pausedBlocks, ...response.content];
|
|
965
1067
|
const fallbacksSent = Array.isArray(params.fallbacks) && params.fallbacks.length > 0;
|
|
966
1068
|
if (isRefusal(response.stop_reason)) {
|
|
967
1069
|
throw refusalError(response.stop_details, fallbacksSent ? params.fallbacks[0].model : undefined);
|
|
968
1070
|
}
|
|
969
|
-
const fallbackBlock =
|
|
1071
|
+
const fallbackBlock = responseContent.find((b) => b.type === "fallback");
|
|
970
1072
|
const sticky = !fallbackBlock && fallbacksSent && response.model && response.model !== this.apiModelId;
|
|
971
1073
|
let pendingMeta;
|
|
972
1074
|
if (fallbackBlock) {
|
|
@@ -990,7 +1092,7 @@ class AnthropicSDKModel {
|
|
|
990
1092
|
return meta;
|
|
991
1093
|
};
|
|
992
1094
|
let seenFallbackBlock = false;
|
|
993
|
-
for (const block of
|
|
1095
|
+
for (const block of responseContent) {
|
|
994
1096
|
switch (block.type) {
|
|
995
1097
|
case "text": {
|
|
996
1098
|
const meta = seenFallbackBlock || sticky ? takeMeta() : undefined;
|
|
@@ -1098,8 +1200,10 @@ class AnthropicSDKModel {
|
|
|
1098
1200
|
}
|
|
1099
1201
|
handleApiError(error);
|
|
1100
1202
|
}
|
|
1203
|
+
const makeRequest = (p) => this.client.messages.create({ ...p, stream: true }, this.buildRequestOptions(options.abortSignal, needsFallbackBetas));
|
|
1204
|
+
const continued = withPauseTurnContinuation(anthropicStream, params, makeRequest);
|
|
1101
1205
|
const fallbacksSent = Array.isArray(params.fallbacks) && params.fallbacks.length > 0;
|
|
1102
|
-
const stream = convertStream(
|
|
1206
|
+
const stream = convertStream(continued, this.modelId, {
|
|
1103
1207
|
apiModelId: this.apiModelId,
|
|
1104
1208
|
fallbacksEnabled: fallbacksSent,
|
|
1105
1209
|
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