@oh-my-pi/pi-ai 15.5.13 → 15.6.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/CHANGELOG.md CHANGED
@@ -2,6 +2,22 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [15.6.0] - 2026-05-30
6
+
7
+ ### Fixed
8
+
9
+ - Fixed Anthropic adaptive-thinking replay preserving signed thinking blocks on the latest abandoned tool-use assistant message, avoiding `thinking blocks in the latest assistant message cannot be modified` 400s. ([#1531](https://github.com/can1357/oh-my-pi/issues/1531))
10
+
11
+ ## [15.5.15] - 2026-05-30
12
+
13
+ ### Added
14
+
15
+ - Added `PI_REQ_DEBUG=1` request/response recording for provider transports. Each request writes `rr-session-N.json`; each received response writes `rr-session-N.res.log` with response headers followed by raw body bytes.
16
+
17
+ ### Fixed
18
+
19
+ - Fixed OpenCode-Go dynamic model refresh downgrading `qwen3.7-max` from Anthropic Messages to OpenAI-compatible transport, which caused `401 Model qwen3.7-max is not supported for format oa-compat` after `/v1/models` cache refreshes.
20
+
5
21
  ## [15.5.12] - 2026-05-29
6
22
  ### Removed
7
23
 
@@ -91,3 +91,10 @@ export declare function hasOpus47ApiRestrictions(modelId: string): boolean;
91
91
  * @see https://platform.claude.com/docs/en/build-with-claude/mid-conversation-system-messages
92
92
  */
93
93
  export declare function supportsMidConversationSystemMessages(modelId: string): boolean;
94
+ /**
95
+ * Claude Opus 4.8 must emit at most one tool call per turn: the Anthropic
96
+ * Messages provider sends `tool_choice.disable_parallel_tool_use = true` for
97
+ * this model. Scoped to exactly 4.8 — earlier and later Opus versions keep
98
+ * Anthropic's default parallel tool-calling.
99
+ */
100
+ export declare function disablesParallelToolUse(modelId: string): boolean;
@@ -142,8 +142,8 @@ export interface OpenCodeModelManagerConfig {
142
142
  apiKey?: string;
143
143
  baseUrl?: string;
144
144
  }
145
- export declare function opencodeZenModelManagerOptions(config?: OpenCodeModelManagerConfig): ModelManagerOptions<"openai-completions">;
146
- export declare function opencodeGoModelManagerOptions(config?: OpenCodeModelManagerConfig): ModelManagerOptions<"openai-completions">;
145
+ export declare function opencodeZenModelManagerOptions(config?: OpenCodeModelManagerConfig): ModelManagerOptions<Api>;
146
+ export declare function opencodeGoModelManagerOptions(config?: OpenCodeModelManagerConfig): ModelManagerOptions<Api>;
147
147
  export interface OllamaModelManagerConfig {
148
148
  apiKey?: string;
149
149
  baseUrl?: string;
@@ -0,0 +1,29 @@
1
+ import type { FetchImpl } from "../types";
2
+ export type RequestDebugHeaders = Headers | Record<string, string | string[] | number | undefined | null> | undefined;
3
+ export interface RequestDebugPayload {
4
+ method: string;
5
+ url: string;
6
+ headers?: RequestDebugHeaders;
7
+ body?: unknown;
8
+ bodyText?: string;
9
+ bodyBase64?: string;
10
+ bodyUnavailable?: string;
11
+ protocol?: string;
12
+ }
13
+ export interface RequestDebugResponseLog {
14
+ write(chunk: Uint8Array | string): void;
15
+ close(): Promise<void>;
16
+ }
17
+ export interface RequestDebugSession {
18
+ readonly id: number;
19
+ readonly requestPath: string;
20
+ readonly responsePath: string;
21
+ openResponseLog(statusLine: string, headers?: RequestDebugHeaders): Promise<RequestDebugResponseLog>;
22
+ wrapResponse(response: Response): Promise<Response>;
23
+ }
24
+ export declare function isRequestDebugEnabled(): boolean;
25
+ export declare function wrapFetchForRequestDebug(fetchImpl: FetchImpl): FetchImpl;
26
+ export declare function withRequestDebugFetch<T extends {
27
+ fetch?: FetchImpl;
28
+ } | undefined>(options: T): T;
29
+ export declare function createRequestDebugSession(payload: RequestDebugPayload): Promise<RequestDebugSession>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "15.5.13",
4
+ "version": "15.6.0",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -38,10 +38,10 @@
38
38
  "generate-models": "bun scripts/generate-models.ts"
39
39
  },
40
40
  "dependencies": {
41
- "@anthropic-ai/sdk": "^0.94.0",
41
+ "@anthropic-ai/sdk": "^0.99.0",
42
42
  "@bufbuild/protobuf": "^2.12.0",
43
- "@oh-my-pi/pi-utils": "15.5.13",
44
- "openai": "^6.36.0",
43
+ "@oh-my-pi/pi-utils": "15.6.0",
44
+ "openai": "^6.39.0",
45
45
  "partial-json": "^0.1.7",
46
46
  "zod": "4.4.3"
47
47
  },
@@ -281,7 +281,7 @@ export class RemoteAuthCredentialStore implements AuthCredentialStore {
281
281
 
282
282
  async prepareForRequest(credentialId: number, opts: { signal?: AbortSignal } = {}): Promise<boolean> {
283
283
  const entry = this.#snapshot.credentials.find(candidate => candidate.id === credentialId);
284
- if (!entry || entry.credential.type !== "oauth" || entry.rotatesInMs === null) return false;
284
+ if (entry?.credential.type !== "oauth" || entry.rotatesInMs === null) return false;
285
285
  const remainingMs = this.#snapshotReceivedAt + entry.rotatesInMs - Date.now();
286
286
  if (remainingMs > WAIT_THRESHOLD_MS) return false;
287
287
  return this.waitForFreshSnapshot(MAX_WAIT_MS, opts);
@@ -2984,7 +2984,7 @@ export class AuthStorage {
2984
2984
  if (!prepare) return true;
2985
2985
  const stored = this.#getStoredCredentials(provider);
2986
2986
  const selected = stored[selection.index];
2987
- if (!selected || selected.credential.type !== "oauth") return false;
2987
+ if (selected?.credential.type !== "oauth") return false;
2988
2988
 
2989
2989
  const prepared = await prepare(selected.id, { signal: options?.signal });
2990
2990
  if (!prepared) return true;
@@ -2996,7 +2996,7 @@ export class AuthStorage {
2996
2996
  const latestIndex = latestRows.findIndex(row => row.id === selected.id);
2997
2997
  if (latestIndex === -1) return false;
2998
2998
  const latest = latestRows[latestIndex];
2999
- if (!latest || latest.credential.type !== "oauth") return false;
2999
+ if (latest?.credential.type !== "oauth") return false;
3000
3000
  selection.index = latestIndex;
3001
3001
  selection.credential = latest.credential;
3002
3002
  return true;
@@ -289,20 +289,22 @@ function retainModelIds<TApi extends Api>(
289
289
  * arms calling `resolveProviderModels` with the same `staticModels` array)
290
290
  * skip the JSON+hash work after the first call.
291
291
  */
292
+ const MODEL_CACHE_FINGERPRINT_VERSION = "merge-v2";
292
293
  const kStaticFingerprint = Symbol("model-manager.staticFingerprint");
293
294
  type ModelArrayWithFingerprint = readonly Model<Api>[] & { [kStaticFingerprint]?: string };
294
295
  function fingerprintStatic<TApi extends Api>(
295
296
  models: readonly Model<TApi>[],
296
297
  dynamicModelsAuthoritative = false,
297
298
  ): string {
298
- if (models.length === 0) return "empty";
299
- if (dynamicModelsAuthoritative) return `authoritative:${fingerprintStatic(models)}`;
299
+ if (models.length === 0) return `${MODEL_CACHE_FINGERPRINT_VERSION}:empty`;
300
+ if (dynamicModelsAuthoritative)
301
+ return `${MODEL_CACHE_FINGERPRINT_VERSION}:authoritative:${fingerprintStatic(models)}`;
300
302
  const tagged = models as ModelArrayWithFingerprint;
301
303
  const cached = tagged[kStaticFingerprint];
302
304
  if (cached !== undefined) return cached;
303
305
  // `Bun.hash` returns a `bigint`; base36 keeps the string short for the
304
306
  // SQLite column without sacrificing distinguishability.
305
- const fingerprint = Bun.hash(JSON.stringify(models)).toString(36);
307
+ const fingerprint = `${MODEL_CACHE_FINGERPRINT_VERSION}:${Bun.hash(JSON.stringify(models)).toString(36)}`;
306
308
  tagged[kStaticFingerprint] = fingerprint;
307
309
  return fingerprint;
308
310
  }
@@ -379,6 +379,18 @@ export function supportsMidConversationSystemMessages(modelId: string): boolean
379
379
  return parsed.kind === "opus" && semverGte(parsed.version, "4.8");
380
380
  }
381
381
 
382
+ /**
383
+ * Claude Opus 4.8 must emit at most one tool call per turn: the Anthropic
384
+ * Messages provider sends `tool_choice.disable_parallel_tool_use = true` for
385
+ * this model. Scoped to exactly 4.8 — earlier and later Opus versions keep
386
+ * Anthropic's default parallel tool-calling.
387
+ */
388
+ export function disablesParallelToolUse(modelId: string): boolean {
389
+ const parsed = parseAnthropicModel(getCanonicalModelId(modelId));
390
+ if (!parsed) return false;
391
+ return parsed.kind === "opus" && semverEqual(parsed.version, "4.8");
392
+ }
393
+
382
394
  function anthropicModelHasRealXHighEffort<TApi extends Api>(model: ApiModel<TApi>): boolean {
383
395
  if (model.api !== "anthropic-messages") return false;
384
396
  const parsedModel = parseKnownModel(model.id);
package/src/models.json CHANGED
@@ -78417,9 +78417,9 @@
78417
78417
  "mimo-v2-flash": {
78418
78418
  "id": "mimo-v2-flash",
78419
78419
  "name": "MiMo-V2-Flash",
78420
- "api": "anthropic-messages",
78420
+ "api": "openai-completions",
78421
78421
  "provider": "xiaomi",
78422
- "baseUrl": "https://api.xiaomimimo.com/anthropic",
78422
+ "baseUrl": "https://api.xiaomimimo.com/v1",
78423
78423
  "reasoning": true,
78424
78424
  "input": [
78425
78425
  "text"
@@ -78432,18 +78432,25 @@
78432
78432
  },
78433
78433
  "contextWindow": 262144,
78434
78434
  "maxTokens": 65536,
78435
+ "compat": {
78436
+ "supportsStore": false,
78437
+ "thinkingFormat": "zai",
78438
+ "reasoningContentField": "reasoning_content",
78439
+ "requiresReasoningContentForToolCalls": true,
78440
+ "allowsSyntheticReasoningContentForToolCalls": false
78441
+ },
78435
78442
  "thinking": {
78436
- "mode": "budget",
78443
+ "mode": "effort",
78437
78444
  "minLevel": "minimal",
78438
- "maxLevel": "xhigh"
78445
+ "maxLevel": "high"
78439
78446
  }
78440
78447
  },
78441
78448
  "mimo-v2-omni": {
78442
78449
  "id": "mimo-v2-omni",
78443
78450
  "name": "MiMo-V2-Omni",
78444
- "api": "anthropic-messages",
78451
+ "api": "openai-completions",
78445
78452
  "provider": "xiaomi",
78446
- "baseUrl": "https://api.xiaomimimo.com/anthropic",
78453
+ "baseUrl": "https://api.xiaomimimo.com/v1",
78447
78454
  "reasoning": true,
78448
78455
  "input": [
78449
78456
  "text",
@@ -78457,18 +78464,25 @@
78457
78464
  },
78458
78465
  "contextWindow": 262144,
78459
78466
  "maxTokens": 131072,
78467
+ "compat": {
78468
+ "supportsStore": false,
78469
+ "thinkingFormat": "zai",
78470
+ "reasoningContentField": "reasoning_content",
78471
+ "requiresReasoningContentForToolCalls": true,
78472
+ "allowsSyntheticReasoningContentForToolCalls": false
78473
+ },
78460
78474
  "thinking": {
78461
- "mode": "budget",
78475
+ "mode": "effort",
78462
78476
  "minLevel": "minimal",
78463
- "maxLevel": "xhigh"
78477
+ "maxLevel": "high"
78464
78478
  }
78465
78479
  },
78466
78480
  "mimo-v2-pro": {
78467
78481
  "id": "mimo-v2-pro",
78468
78482
  "name": "MiMo-V2-Pro",
78469
- "api": "anthropic-messages",
78483
+ "api": "openai-completions",
78470
78484
  "provider": "xiaomi",
78471
- "baseUrl": "https://api.xiaomimimo.com/anthropic",
78485
+ "baseUrl": "https://api.xiaomimimo.com/v1",
78472
78486
  "reasoning": true,
78473
78487
  "input": [
78474
78488
  "text"
@@ -78481,18 +78495,25 @@
78481
78495
  },
78482
78496
  "contextWindow": 1048576,
78483
78497
  "maxTokens": 131072,
78498
+ "compat": {
78499
+ "supportsStore": false,
78500
+ "thinkingFormat": "zai",
78501
+ "reasoningContentField": "reasoning_content",
78502
+ "requiresReasoningContentForToolCalls": true,
78503
+ "allowsSyntheticReasoningContentForToolCalls": false
78504
+ },
78484
78505
  "thinking": {
78485
- "mode": "budget",
78506
+ "mode": "effort",
78486
78507
  "minLevel": "minimal",
78487
- "maxLevel": "xhigh"
78508
+ "maxLevel": "high"
78488
78509
  }
78489
78510
  },
78490
78511
  "mimo-v2.5": {
78491
78512
  "id": "mimo-v2.5",
78492
78513
  "name": "MiMo-V2.5",
78493
- "api": "anthropic-messages",
78514
+ "api": "openai-completions",
78494
78515
  "provider": "xiaomi",
78495
- "baseUrl": "https://api.xiaomimimo.com/anthropic",
78516
+ "baseUrl": "https://api.xiaomimimo.com/v1",
78496
78517
  "reasoning": true,
78497
78518
  "input": [
78498
78519
  "text",
@@ -78506,18 +78527,25 @@
78506
78527
  },
78507
78528
  "contextWindow": 1048576,
78508
78529
  "maxTokens": 131072,
78530
+ "compat": {
78531
+ "supportsStore": false,
78532
+ "thinkingFormat": "zai",
78533
+ "reasoningContentField": "reasoning_content",
78534
+ "requiresReasoningContentForToolCalls": true,
78535
+ "allowsSyntheticReasoningContentForToolCalls": false
78536
+ },
78509
78537
  "thinking": {
78510
- "mode": "budget",
78538
+ "mode": "effort",
78511
78539
  "minLevel": "minimal",
78512
- "maxLevel": "xhigh"
78540
+ "maxLevel": "high"
78513
78541
  }
78514
78542
  },
78515
78543
  "mimo-v2.5-pro": {
78516
78544
  "id": "mimo-v2.5-pro",
78517
78545
  "name": "MiMo-V2.5-Pro",
78518
- "api": "anthropic-messages",
78546
+ "api": "openai-completions",
78519
78547
  "provider": "xiaomi",
78520
- "baseUrl": "https://api.xiaomimimo.com/anthropic",
78548
+ "baseUrl": "https://api.xiaomimimo.com/v1",
78521
78549
  "reasoning": true,
78522
78550
  "input": [
78523
78551
  "text"
@@ -78530,10 +78558,17 @@
78530
78558
  },
78531
78559
  "contextWindow": 1048576,
78532
78560
  "maxTokens": 131072,
78561
+ "compat": {
78562
+ "supportsStore": false,
78563
+ "thinkingFormat": "zai",
78564
+ "reasoningContentField": "reasoning_content",
78565
+ "requiresReasoningContentForToolCalls": true,
78566
+ "allowsSyntheticReasoningContentForToolCalls": false
78567
+ },
78533
78568
  "thinking": {
78534
- "mode": "budget",
78569
+ "mode": "effort",
78535
78570
  "minLevel": "minimal",
78536
- "maxLevel": "xhigh"
78571
+ "maxLevel": "high"
78537
78572
  }
78538
78573
  }
78539
78574
  },
@@ -1218,37 +1218,62 @@ export interface OpenCodeModelManagerConfig {
1218
1218
  baseUrl?: string;
1219
1219
  }
1220
1220
 
1221
+ function normalizeOpenCodeBasePath(baseUrl: string | undefined, fallbackBasePath: string): string {
1222
+ const value = normalizeAnthropicBaseUrl(baseUrl, fallbackBasePath);
1223
+ return value.endsWith("/v1") ? value.slice(0, -3) : value;
1224
+ }
1225
+
1226
+ function openCodeBaseUrlForApi(api: Api, basePath: string): string {
1227
+ return api === "anthropic-messages" ? basePath : `${basePath}/v1`;
1228
+ }
1229
+
1221
1230
  function openCodeModelManagerOptions(
1222
1231
  providerId: "opencode-go" | "opencode-zen",
1223
- defaultBaseUrl: string,
1232
+ defaultBasePath: string,
1224
1233
  config?: OpenCodeModelManagerConfig,
1225
- ): ModelManagerOptions<"openai-completions"> {
1234
+ ): ModelManagerOptions<Api> {
1226
1235
  const apiKey = config?.apiKey;
1227
- const baseUrl = config?.baseUrl ?? defaultBaseUrl;
1236
+ const basePath = normalizeOpenCodeBasePath(config?.baseUrl, defaultBasePath);
1237
+ const discoveryBaseUrl = openCodeBaseUrlForApi("openai-completions", basePath);
1238
+ const references = createBundledReferenceMap<Api>(providerId);
1228
1239
  return {
1229
1240
  providerId,
1230
1241
  ...(apiKey && {
1231
1242
  fetchDynamicModels: () =>
1232
- fetchOpenAICompatibleModels({
1243
+ fetchOpenAICompatibleModels<Api>({
1233
1244
  api: "openai-completions",
1234
1245
  provider: providerId,
1235
- baseUrl,
1246
+ baseUrl: discoveryBaseUrl,
1236
1247
  apiKey,
1248
+ mapModel: (entry, defaults) => {
1249
+ const reference = references.get(defaults.id);
1250
+ const name = toModelName(entry.name, reference?.name ?? defaults.name);
1251
+ if (!reference) {
1252
+ return {
1253
+ ...defaults,
1254
+ name,
1255
+ };
1256
+ }
1257
+ return {
1258
+ ...reference,
1259
+ id: defaults.id,
1260
+ name,
1261
+ baseUrl: openCodeBaseUrlForApi(reference.api, basePath),
1262
+ contextWindow: toPositiveNumber(entry.context_length, reference.contextWindow),
1263
+ maxTokens: toPositiveNumber(entry.max_completion_tokens, reference.maxTokens),
1264
+ };
1265
+ },
1237
1266
  }),
1238
1267
  }),
1239
1268
  };
1240
1269
  }
1241
1270
 
1242
- export function opencodeZenModelManagerOptions(
1243
- config?: OpenCodeModelManagerConfig,
1244
- ): ModelManagerOptions<"openai-completions"> {
1245
- return openCodeModelManagerOptions("opencode-zen", "https://opencode.ai/zen/v1", config);
1271
+ export function opencodeZenModelManagerOptions(config?: OpenCodeModelManagerConfig): ModelManagerOptions<Api> {
1272
+ return openCodeModelManagerOptions("opencode-zen", "https://opencode.ai/zen", config);
1246
1273
  }
1247
1274
 
1248
- export function opencodeGoModelManagerOptions(
1249
- config?: OpenCodeModelManagerConfig,
1250
- ): ModelManagerOptions<"openai-completions"> {
1251
- return openCodeModelManagerOptions("opencode-go", "https://opencode.ai/zen/go/v1", config);
1275
+ export function opencodeGoModelManagerOptions(config?: OpenCodeModelManagerConfig): ModelManagerOptions<Api> {
1276
+ return openCodeModelManagerOptions("opencode-go", "https://opencode.ai/zen/go", config);
1252
1277
  }
1253
1278
 
1254
1279
  // ---------------------------------------------------------------------------
@@ -2122,25 +2147,23 @@ export function githubCopilotModelManagerOptions(config?: GithubCopilotModelMana
2122
2147
  const reference = resolveReference(defaults.id);
2123
2148
  const copilotLimits = extractCopilotLimits(entry);
2124
2149
  // Copilot exposes token limits under capabilities.limits.*.
2125
- // max_prompt_tokens is the prompt capacity (what OMP calls contextWindow).
2126
- // max_context_window_tokens is the total window (prompt + output budget)
2127
- // and must NOT be used for contextWindow it inflates the limit and
2128
- // breaks compaction thresholds, overflow detection, and promotion.
2129
- // The OpenAI-compatible root-level `context_length` field mirrors the
2130
- // total window (e.g. 400k for gpt-5.4), so Copilot's max_prompt_tokens
2131
- // (the true prompt budget) must take precedence whenever it is present.
2132
- const contextWindowFallback = toPositiveNumber(
2133
- entry.context_length,
2134
- reference?.contextWindow ?? defaults.contextWindow,
2135
- );
2150
+ // max_context_window_tokens is the model's total usable window;
2151
+ // max_prompt_tokens is Copilot's prompt/summarization budget and
2152
+ // must only be a fallback when total-window fields are absent.
2136
2153
  const contextWindow = toPositiveNumber(
2137
- copilotLimits.maxPromptTokens,
2138
- reference ? Math.min(contextWindowFallback, reference.contextWindow) : contextWindowFallback,
2154
+ copilotLimits.maxContextWindowTokens,
2155
+ toPositiveNumber(
2156
+ entry.context_length,
2157
+ toPositiveNumber(
2158
+ copilotLimits.maxPromptTokens,
2159
+ reference?.contextWindow ?? defaults.contextWindow,
2160
+ ),
2161
+ ),
2139
2162
  );
2140
2163
  const maxTokens = toPositiveNumber(
2141
- entry.max_completion_tokens,
2164
+ copilotLimits.maxOutputTokens,
2142
2165
  toPositiveNumber(
2143
- copilotLimits.maxOutputTokens,
2166
+ entry.max_completion_tokens,
2144
2167
  toPositiveNumber(
2145
2168
  copilotLimits.maxNonStreamingOutputTokens,
2146
2169
  reference?.maxTokens ?? defaults.maxTokens,
@@ -2643,9 +2666,16 @@ const MODELS_DEV_PROVIDER_DESCRIPTORS_CODING_PLANS: readonly ModelsDevProviderDe
2643
2666
  // --- zAI ---
2644
2667
  anthropicMessagesDescriptor("zai-coding-plan", "zai", "https://api.z.ai/api/anthropic"),
2645
2668
  // --- Xiaomi ---
2646
- anthropicMessagesDescriptor("xiaomi", "xiaomi", "https://api.xiaomimimo.com/anthropic", {
2669
+ openAiCompletionsDescriptor("xiaomi", "xiaomi", "https://api.xiaomimimo.com/v1", {
2647
2670
  defaultContextWindow: 262144,
2648
2671
  defaultMaxTokens: 8192,
2672
+ compat: {
2673
+ supportsStore: false,
2674
+ thinkingFormat: "zai",
2675
+ reasoningContentField: "reasoning_content",
2676
+ requiresReasoningContentForToolCalls: true,
2677
+ allowsSyntheticReasoningContentForToolCalls: false,
2678
+ },
2649
2679
  }),
2650
2680
  // --- MiniMax Coding Plan ---
2651
2681
  openAiCompletionsDescriptor("minimax-coding-plan", "minimax-code", "https://api.minimax.io/v1", {
@@ -273,6 +273,7 @@ export const streamBedrock: StreamFunction<"bedrock-converse-stream"> = (
273
273
  headers: requestHeaders,
274
274
  body,
275
275
  signal: options.signal,
276
+ fetch: options.fetch,
276
277
  });
277
278
 
278
279
  if (!response.ok) {
@@ -22,6 +22,7 @@ import {
22
22
  readSseEvents,
23
23
  } from "@oh-my-pi/pi-utils";
24
24
  import {
25
+ disablesParallelToolUse,
25
26
  hasOpus47ApiRestrictions,
26
27
  mapEffortToAnthropicAdaptiveEffort,
27
28
  supportsMidConversationSystemMessages,
@@ -654,17 +655,6 @@ function convertContentBlocks(
654
655
  return blocks;
655
656
  }
656
657
 
657
- /**
658
- * Marker phrase that Claude has been observed to hallucinate inside reasoning summaries
659
- * (e.g. "I don't see any current rewritten thinking or next thinking to process. Could
660
- * you provide..."). When this substring appears in a streamed thinking block we collapse
661
- * the entire block to {@link BROKEN_THINKING_REPLACEMENT} and drop the signature so
662
- * downstream UI/transcripts don't surface the meta-prompt and replay can't re-anchor on
663
- * the garbled chain.
664
- */
665
- const BROKEN_THINKING_MARKER = "rewritten thinking";
666
- const BROKEN_THINKING_REPLACEMENT = "Thinking...";
667
-
668
658
  export type AnthropicEffort = "low" | "medium" | "high" | "xhigh" | "max";
669
659
  export type AnthropicThinkingDisplay = "summarized" | "omitted";
670
660
 
@@ -1221,12 +1211,6 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1221
1211
  const requestTimeoutMs =
1222
1212
  firstEventTimeoutMs !== undefined && firstEventTimeoutMs > 0 ? firstEventTimeoutMs : undefined;
1223
1213
  const blocks = output.content as Block[];
1224
- // Recent Claude releases occasionally hallucinate meta-prompts asking the operator
1225
- // to supply "rewritten thinking" / "next thinking" as reasoning content. The summary
1226
- // is useless and confuses the UI, so we collapse any thinking block whose stream
1227
- // contains the marker phrase down to a plain "Thinking..." placeholder and drop the
1228
- // (now invalid) signature so subsequent turns don't replay the garbled chain.
1229
- const suppressedThinkingBlocks = new WeakSet<Block>();
1230
1214
  stream.push({ type: "start", partial: output });
1231
1215
  // Retry loop for transient errors from the stream.
1232
1216
  // Provider-level transport/rate-limit failures: only before any streamed content starts.
@@ -1381,14 +1365,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1381
1365
  const index = blocks.findIndex(b => b.index === event.index);
1382
1366
  const block = blocks[index];
1383
1367
  if (block && block.type === "thinking") {
1384
- if (suppressedThinkingBlocks.has(block)) continue;
1385
1368
  block.thinking += event.delta.thinking;
1386
- if (block.thinking.includes(BROKEN_THINKING_MARKER)) {
1387
- suppressedThinkingBlocks.add(block);
1388
- block.thinking = BROKEN_THINKING_REPLACEMENT;
1389
- block.thinkingSignature = "";
1390
- continue;
1391
- }
1392
1369
  stream.push({
1393
1370
  type: "thinking_delta",
1394
1371
  contentIndex: index,
@@ -1412,7 +1389,7 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1412
1389
  } else if (event.delta.type === "signature_delta") {
1413
1390
  const index = blocks.findIndex(b => b.index === event.index);
1414
1391
  const block = blocks[index];
1415
- if (block && block.type === "thinking" && !suppressedThinkingBlocks.has(block)) {
1392
+ if (block && block.type === "thinking") {
1416
1393
  block.thinkingSignature = block.thinkingSignature || "";
1417
1394
  block.thinkingSignature += event.delta.signature;
1418
1395
  }
@@ -1430,14 +1407,6 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
1430
1407
  partial: output,
1431
1408
  });
1432
1409
  } else if (block.type === "thinking") {
1433
- if (
1434
- !suppressedThinkingBlocks.has(block) &&
1435
- block.thinking.includes(BROKEN_THINKING_MARKER)
1436
- ) {
1437
- suppressedThinkingBlocks.add(block);
1438
- block.thinking = BROKEN_THINKING_REPLACEMENT;
1439
- block.thinkingSignature = "";
1440
- }
1441
1410
  stream.push({
1442
1411
  type: "thinking_end",
1443
1412
  contentIndex: index,
@@ -1806,7 +1775,7 @@ function disableThinkingIfToolChoiceForced(params: MessageCreateParamsStreaming)
1806
1775
 
1807
1776
  function ensureMaxTokensForThinking(params: MessageCreateParamsStreaming, model: Model<"anthropic-messages">): void {
1808
1777
  const thinking = params.thinking;
1809
- if (!thinking || thinking.type !== "enabled") return;
1778
+ if (thinking?.type !== "enabled") return;
1810
1779
 
1811
1780
  const budgetTokens = thinking.budget_tokens ?? 0;
1812
1781
  if (budgetTokens <= 0) return;
@@ -2165,6 +2134,21 @@ function buildParams(
2165
2134
  }
2166
2135
  }
2167
2136
 
2137
+ // Claude Opus 4.8 must emit at most one tool call per turn. Force
2138
+ // `disable_parallel_tool_use` onto the outgoing tool_choice (synthesizing an
2139
+ // `auto` choice when none is set). Gated on tools being present: Anthropic
2140
+ // rejects `tool_choice` without `tools`, and parallelism is moot otherwise.
2141
+ // `none` rejects the field, so leave it untouched. A fresh object is built
2142
+ // rather than mutated so the caller's `options.toolChoice` is never aliased.
2143
+ if (disablesParallelToolUse(model.id) && params.tools && params.tools.length > 0) {
2144
+ const current = params.tool_choice;
2145
+ if (!current) {
2146
+ params.tool_choice = { type: "auto", disable_parallel_tool_use: true };
2147
+ } else if (current.type !== "none") {
2148
+ params.tool_choice = { ...current, disable_parallel_tool_use: true };
2149
+ }
2150
+ }
2151
+
2168
2152
  const shouldInjectClaudeCodeInstruction = isOAuthToken && !model.id.startsWith("claude-3-5-haiku");
2169
2153
  const billingSystemPrompts = normalizeSystemPrompts(context.systemPrompt);
2170
2154
  const billingPayload = shouldInjectClaudeCodeInstruction