@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 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 = response.content.find((b) => b.type === "fallback");
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 response.content) {
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(anthropicStream, this.modelId, {
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
@@ -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;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"}
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 = response.content.find((b) => b.type === "fallback");
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 response.content) {
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(anthropicStream, this.modelId, {
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
@@ -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,CA0E3C"}
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xtruder/opencode-claude-max-plugin",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
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",