@oh-my-pi/pi-ai 18.13.2 → 18.13.3

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,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [18.13.3] - 2026-10-28
6
+
7
+ ### Changed
8
+
9
+ - Updated Bun engine requirement to ≥1.4.4
10
+
5
11
  ## [18.13.2] - 2026-10-27
6
12
 
7
13
  ### Fixed
@@ -3,6 +3,8 @@ import type { AssistantMessage, Context, Model, ServiceTier, StreamFunction, Str
3
3
  import type { ChatCompletionMessageParam } from "./openai-chat-wire.js";
4
4
  import { type OpenAIPromptCacheOptions } from "./openai-shared.js";
5
5
  export { applyOpenRouterRoutingVariant } from "./openai-shared.js";
6
+ /** Whether a thinking block's signature is a raw LiteLLM (Anthropic) one, not a reasoning field name. */
7
+ export declare function isLiteLLMThinkingSignature(signature: string | undefined): signature is string;
6
8
  /**
7
9
  * Identify "real progress" stream chunks vs. keepalives, role-only preambles,
8
10
  * and empty `{choices:[]}` no-ops emitted by some OpenAI-compatible endpoints.
@@ -43,4 +45,15 @@ export interface OpenAICompletionsOptions extends StreamOptions {
43
45
  */
44
46
  export declare const streamOpenAICompletions: StreamFunction<"openai-completions">;
45
47
  export declare function parseChunkUsage(rawUsage: object, model: Model<"openai-completions">, premiumRequests: number | undefined, timestamp?: number, cachedTokensHeader?: number): AssistantMessage["usage"];
48
+ /**
49
+ * Whether every chat-completions request to `model` drops the thinking text of
50
+ * its own prior turns: neither the base compat nor its `whenThinking` view
51
+ * sends it. Blocks with a raw LiteLLM signature ({@link isLiteLLMThinkingSignature})
52
+ * still replay in `thinking_blocks`.
53
+ */
54
+ export declare function completionsDropsOwnThinking(model: {
55
+ api?: string;
56
+ reasoning?: boolean;
57
+ compat?: Model["compat"];
58
+ } | null | undefined): boolean;
46
59
  export declare function convertMessages(model: Model<"openai-completions">, context: Context, compat: ResolvedOpenAICompat): ChatCompletionMessageParam[];
@@ -1,3 +1,4 @@
1
+ import type { ResolvedOpenAICompat } from "@oh-my-pi/pi-catalog/types";
1
2
  import type { Api, AssistantMessage, Message, Model } from "../types.js";
2
3
  /**
3
4
  * Maximum tool-call id length the strictest replay provider accepts.
@@ -54,6 +55,14 @@ export declare function isSurvivingAssistant(msg: AssistantMessage, model?: Mode
54
55
  export declare function isAnthropicMessagesModel(model: Model): model is Model<"anthropic-messages">;
55
56
  /** Whether `turn` came from `model` itself: its own turns replay their native reasoning unchanged. */
56
57
  export declare function isSameModelTurn(turn: Pick<AssistantMessage, "provider" | "api" | "model">, model: Pick<Model, "provider" | "api" | "id">): boolean;
58
+ /**
59
+ * Whether chat completions (`convertMessages`) writes the model's own past
60
+ * thinking under `compat`: as Mistral content parts, demoted text, or a
61
+ * reasoning field. Otherwise it drops it.
62
+ */
63
+ export declare function completionsSendsOwnThinking(model: {
64
+ reasoning?: boolean;
65
+ }, compat: Pick<ResolvedOpenAICompat, "mistralReasoningContentParts" | "requiresThinkingAsText" | "requiresReasoningContentForToolCalls" | "thinkingFormat" | "replayReasoningContent">): boolean;
57
66
  /**
58
67
  * Normalize tool call ID for cross-provider compatibility.
59
68
  * OpenAI Responses API generates IDs that are 450+ chars with special characters like `|`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oh-my-pi/pi-ai",
3
- "version": "18.13.2",
3
+ "version": "18.13.3",
4
4
  "description": "Unified LLM API with automatic model discovery and provider configuration",
5
5
  "keywords": [
6
6
  "ai",
@@ -155,16 +155,16 @@
155
155
  "fmt": "oxfmt --no-error-on-unmatched-pattern 'src/**/*.{ts,tsx}' '{test,bench,examples,scripts}/**/*.ts' '*.ts'"
156
156
  },
157
157
  "dependencies": {
158
- "@oh-my-pi/omptype": "18.13.2",
159
- "@oh-my-pi/pi-catalog": "18.13.2",
160
- "@oh-my-pi/pi-natives": "18.13.2",
161
- "@oh-my-pi/pi-utils": "18.13.2",
162
- "@oh-my-pi/pi-wire": "18.13.2"
158
+ "@oh-my-pi/omptype": "18.13.3",
159
+ "@oh-my-pi/pi-catalog": "18.13.3",
160
+ "@oh-my-pi/pi-natives": "18.13.3",
161
+ "@oh-my-pi/pi-utils": "18.13.3",
162
+ "@oh-my-pi/pi-wire": "18.13.3"
163
163
  },
164
164
  "devDependencies": {
165
165
  "@types/bun": "^1.3.14"
166
166
  },
167
167
  "engines": {
168
- "bun": ">=1.3.14"
168
+ "bun": ">=1.4.4"
169
169
  }
170
170
  }
@@ -98,15 +98,7 @@ function renderStateField(key: string, value: JsonValue): string {
98
98
  if (typeof value !== "object" || value === null) {
99
99
  return `${open}${escapeXmlText(value === null ? "null" : String(value))}${close}`;
100
100
  }
101
- const yaml = YAML.stringify(value, null, 2)
102
- // Bun <=1.4.2 emits `key: ` before a nested block and puts an empty
103
- // collection on its own line; 1.4.3 emits `key:` and `key: {}`. Pin the
104
- // newer form so prompt bytes (and prompt-cache keys) do not depend on the
105
- // Bun build. Values with edge whitespace are always quoted, so trimming
106
- // line ends never alters data.
107
- .replace(/[ \t]+$/gm, "")
108
- .replace(/:\n +(\{\}|\[\])$/gm, ": $1")
109
- .trimEnd();
101
+ const yaml = YAML.stringify(value, null, 2);
110
102
  return `${open}\n${escapeXmlText(yaml)}\n${close}`;
111
103
  }
112
104
 
@@ -200,9 +200,6 @@ async function sendCoworkRequest(
200
200
  const reason = signal?.reason;
201
201
  const error = reason instanceof Error ? reason : new DOMException("The operation was aborted.", "AbortError");
202
202
  request?.destroy(error);
203
- // Bun before 1.4 emits 'close' but not 'error' for destroy(error), so the
204
- // 'error' listener below never settles a request still waiting for headers.
205
- result.reject(error);
206
203
  };
207
204
  if (signal?.aborted) {
208
205
  release();
@@ -121,7 +121,7 @@ import {
121
121
  shouldDropAutoToolChoiceForReasoning,
122
122
  shouldRetryWithoutStrictTools,
123
123
  } from "./openai-shared";
124
- import { transformMessages } from "./transform-messages";
124
+ import { completionsSendsOwnThinking, transformMessages } from "./transform-messages";
125
125
  import {
126
126
  isOpenAICompletionsVisionSupported,
127
127
  joinTextWithImagePlaceholder,
@@ -273,6 +273,13 @@ function getLiteLLMThinkingBlocksDelta(delta: object): unknown[] | undefined {
273
273
  return Array.isArray(mirrored) && mirrored.length > 0 ? mirrored : undefined;
274
274
  }
275
275
 
276
+ /** Whether a thinking block's signature is a raw LiteLLM (Anthropic) one, not a reasoning field name. */
277
+ export function isLiteLLMThinkingSignature(signature: string | undefined): signature is string {
278
+ return (
279
+ !!signature && signature !== "reasoning_content" && signature !== "reasoning" && signature !== "reasoning_text"
280
+ );
281
+ }
282
+
276
283
  /**
277
284
  * Rebuilds LiteLLM `thinking_blocks` from a stored assistant turn. Thinking
278
285
  * blocks parsed from `thinking_blocks` keep the raw Anthropic signature as
@@ -286,14 +293,7 @@ function encodeLiteLLMThinkingBlocks(content: AssistantMessage["content"]): Lite
286
293
  for (const block of content) {
287
294
  if (block.type === "thinking") {
288
295
  const signature = block.thinkingSignature;
289
- if (
290
- !signature ||
291
- signature === "reasoning_content" ||
292
- signature === "reasoning" ||
293
- signature === "reasoning_text"
294
- ) {
295
- continue;
296
- }
296
+ if (!isLiteLLMThinkingSignature(signature)) continue;
297
297
  blocks.push({ type: "thinking", thinking: block.thinking, signature });
298
298
  } else if (block.type === "redactedThinking") {
299
299
  blocks.push({ type: "redacted_thinking", data: block.data });
@@ -2366,6 +2366,25 @@ function maybeAddAnthropicCacheControl(compat: ResolvedOpenAICompat, messages: C
2366
2366
  }
2367
2367
  }
2368
2368
 
2369
+ /**
2370
+ * Whether every chat-completions request to `model` drops the thinking text of
2371
+ * its own prior turns: neither the base compat nor its `whenThinking` view
2372
+ * sends it. Blocks with a raw LiteLLM signature ({@link isLiteLLMThinkingSignature})
2373
+ * still replay in `thinking_blocks`.
2374
+ */
2375
+ export function completionsDropsOwnThinking(
2376
+ model: { api?: string; reasoning?: boolean; compat?: Model["compat"] } | null | undefined,
2377
+ ): boolean {
2378
+ if (model?.api !== "openai-completions" || !model.compat) return false;
2379
+ const compat = model.compat as ResolvedOpenAICompat;
2380
+ // A row without a resolved replay flag can't tell; counting the thinking never undercounts.
2381
+ if (!("replayReasoningContent" in compat)) return false;
2382
+ return (
2383
+ !completionsSendsOwnThinking(model, compat) &&
2384
+ (compat.whenThinking === undefined || !completionsSendsOwnThinking(model, compat.whenThinking))
2385
+ );
2386
+ }
2387
+
2369
2388
  export function convertMessages(
2370
2389
  model: Model<"openai-completions">,
2371
2390
  context: Context,
@@ -1,6 +1,7 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
2
  import { resolveCatalogPolicy } from "@oh-my-pi/pi-catalog/compat/catalog-policy";
3
3
  import { carriesReasoning } from "@oh-my-pi/pi-catalog/compat/reasoning-carry";
4
+ import type { ResolvedOpenAICompat } from "@oh-my-pi/pi-catalog/types";
4
5
  import { renderDemotedThinking } from "../dialect/demotion";
5
6
  import type {
6
7
  Api,
@@ -543,6 +544,31 @@ function targetReadsForeignThinking(model: Model, compat: Model["compat"]): bool
543
544
  return model.reasoning && compat.thinkingFormat === "zai";
544
545
  }
545
546
 
547
+ /**
548
+ * Whether chat completions (`convertMessages`) writes the model's own past
549
+ * thinking under `compat`: as Mistral content parts, demoted text, or a
550
+ * reasoning field. Otherwise it drops it.
551
+ */
552
+ export function completionsSendsOwnThinking(
553
+ model: { reasoning?: boolean },
554
+ compat: Pick<
555
+ ResolvedOpenAICompat,
556
+ | "mistralReasoningContentParts"
557
+ | "requiresThinkingAsText"
558
+ | "requiresReasoningContentForToolCalls"
559
+ | "thinkingFormat"
560
+ | "replayReasoningContent"
561
+ >,
562
+ ): boolean {
563
+ return Boolean(
564
+ compat.mistralReasoningContentParts ||
565
+ compat.requiresThinkingAsText ||
566
+ compat.requiresReasoningContentForToolCalls ||
567
+ (compat.thinkingFormat === "zai" && model.reasoning) ||
568
+ compat.replayReasoningContent,
569
+ );
570
+ }
571
+
546
572
  /**
547
573
  * Whether the target's encoder already replays the target's own plaintext
548
574
  * reasoning in a native slot. Reasoning carried from another host of the same
@@ -565,13 +591,7 @@ function targetReplaysPlaintextReasoning(model: Model, compat: Model["compat"]):
565
591
  if (!("requiresReasoningContentForToolCalls" in compat)) return false;
566
592
  switch (model.api) {
567
593
  case "openai-completions":
568
- return (
569
- !compat.requiresThinkingAsText &&
570
- (compat.mistralReasoningContentParts === true ||
571
- compat.requiresReasoningContentForToolCalls ||
572
- compat.thinkingFormat === "zai" ||
573
- compat.replayReasoningContent)
574
- );
594
+ return !compat.requiresThinkingAsText && completionsSendsOwnThinking(model, compat);
575
595
  case "openrouter":
576
596
  return !compat.filterReasoningHistory && compat.requiresReasoningContentForToolCalls;
577
597
  case "openai-responses":