@oh-my-pi/pi-ai 15.9.3 → 15.9.67

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,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [15.9.67] - 2026-06-06
6
+
7
+ ### Fixed
8
+
9
+ - Fixed llama.cpp/OpenAI Responses parallel tool calls losing arguments when `function_call_arguments.done` events omit `output_index` and `item_id`, by routing those identifierless final-argument events through the open function calls in item order. ([#1970](https://github.com/can1357/oh-my-pi/issues/1970))
10
+ - Fixed local Ollama (`openai-responses`) turns failing with HTTP 400 `invalid reasoning value: "minimal"` when a discovered model ran with `minimal` (or `xhigh`) thinking. Ollama's OpenAI-compatible `reasoning.effort` only accepts `high|medium|low|max|none`, so discovered reasoning-capable Ollama models now carry a `compat.reasoningEffortMap` remapping `minimal → low` and `xhigh → max`; non-reasoning models are left untouched.
11
+
5
12
  ## [15.9.2] - 2026-06-05
6
13
 
7
14
  ### Added
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.9.3",
4
+ "version": "15.9.67",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -39,7 +39,7 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@bufbuild/protobuf": "^2.12.0",
42
- "@oh-my-pi/pi-utils": "15.9.3",
42
+ "@oh-my-pi/pi-utils": "15.9.67",
43
43
  "openai": "^6.39.0",
44
44
  "partial-json": "^0.1.7",
45
45
  "zod": "4.4.3"
@@ -242,6 +242,22 @@ async function fetchOllamaNativeModels(
242
242
  const OLLAMA_FALLBACK_CONTEXT_WINDOW = 128_000;
243
243
  /** Cap max output tokens at a value that matches OMP's other openai-responses defaults. */
244
244
  const OLLAMA_DEFAULT_MAX_TOKENS = 8192;
245
+ /**
246
+ * Ollama's OpenAI-compatible `reasoning.effort` only accepts
247
+ * `high|medium|low|max|none`; passing OMP's `minimal`/`xhigh` levels verbatim
248
+ * makes the server reject the turn with HTTP 400 `invalid reasoning value`.
249
+ * Map the two unsupported levels onto the closest accepted ones (`low`/`max`).
250
+ */
251
+ const OLLAMA_REASONING_EFFORT_MAP = { minimal: "low", xhigh: "max" } as const;
252
+
253
+ /** Stamp the Ollama reasoning-effort map onto a reasoning-capable model. */
254
+ function applyOllamaReasoningCompat(model: Model<"openai-responses">): void {
255
+ if (!model.reasoning) return;
256
+ model.compat = {
257
+ ...model.compat,
258
+ reasoningEffortMap: { ...OLLAMA_REASONING_EFFORT_MAP, ...model.compat?.reasoningEffortMap },
259
+ };
260
+ }
245
261
 
246
262
  interface OllamaResolvedMetadata {
247
263
  contextWindow: number;
@@ -1357,12 +1373,14 @@ export function ollamaModelManagerOptions(config?: OllamaModelManagerConfig): Mo
1357
1373
  if (metadata.input) {
1358
1374
  model.input = metadata.input;
1359
1375
  }
1376
+ applyOllamaReasoningCompat(model);
1360
1377
  }),
1361
1378
  );
1362
1379
  return openAiCompatible;
1363
1380
  }
1364
1381
  const nativeFallback = await fetchOllamaNativeModels(baseUrl, resolveMetadata);
1365
1382
  if (nativeFallback && nativeFallback.length > 0) {
1383
+ for (const model of nativeFallback) applyOllamaReasoningCompat(model);
1366
1384
  return nativeFallback;
1367
1385
  }
1368
1386
  return openAiCompatible;
@@ -1778,16 +1778,12 @@ type SystemBlockOptions = {
1778
1778
  cacheControl?: AnthropicCacheControl;
1779
1779
  };
1780
1780
 
1781
- function withGlobalCacheScope(cacheControl: AnthropicCacheControl): AnthropicCacheControl {
1782
- return { ...cacheControl, scope: "global" };
1783
- }
1784
-
1785
1781
  function applyClaudeCodeSystemCache(
1786
1782
  blocks: AnthropicSystemBlock[],
1787
1783
  cacheControl: AnthropicCacheControl | undefined,
1788
1784
  ): number {
1789
1785
  if (!cacheControl || blocks.length <= 2) return 0;
1790
- blocks[2] = { ...blocks[2], cache_control: withGlobalCacheScope(cacheControl) };
1786
+ blocks[2] = { ...blocks[2], cache_control: cacheControl };
1791
1787
  if (blocks.length === 3) return 1;
1792
1788
  const lastIndex = blocks.length - 1;
1793
1789
  blocks[lastIndex] = { ...blocks[lastIndex], cache_control: cacheControl };
@@ -395,7 +395,7 @@ export async function processResponsesStream<TApi extends Api>(
395
395
  model: Model<TApi>,
396
396
  options?: ProcessResponsesStreamOptions,
397
397
  ): Promise<void> {
398
- type StreamingToolCallBlock = ToolCall & { partialJson: string; lastParseLen?: number };
398
+ type StreamingToolCallBlock = ToolCall & { partialJson: string; lastParseLen?: number; argumentsDone?: boolean };
399
399
  interface StreamingItem {
400
400
  item: ResponseReasoningItem | ResponseOutputMessage | ResponseFunctionToolCall | ResponseCustomToolCall;
401
401
  block: ThinkingContent | TextContent | StreamingToolCallBlock;
@@ -409,6 +409,7 @@ export async function processResponsesStream<TApi extends Api>(
409
409
  const openItemsByOutputIndex = new Map<number, StreamingItem>();
410
410
  const openItemsByItemId = new Map<string, StreamingItem>();
411
411
  let lastOpenItem: StreamingItem | null = null;
412
+ const openItemsInOrder: StreamingItem[] = [];
412
413
 
413
414
  const registerOpenItem = (
414
415
  outputIndex: number | undefined,
@@ -417,6 +418,7 @@ export async function processResponsesStream<TApi extends Api>(
417
418
  ): void => {
418
419
  if (typeof outputIndex === "number") openItemsByOutputIndex.set(outputIndex, entry);
419
420
  if (itemId) openItemsByItemId.set(itemId, entry);
421
+ openItemsInOrder.push(entry);
420
422
  lastOpenItem = entry;
421
423
  };
422
424
  const lookupOpenItem = (event: { output_index?: number; item_id?: string }): StreamingItem | undefined => {
@@ -431,6 +433,24 @@ export async function processResponsesStream<TApi extends Api>(
431
433
  // Fallback for tests / mock providers that omit identifiers on stream events.
432
434
  return lastOpenItem ?? undefined;
433
435
  };
436
+ const hasOpenItemKey = (event: { output_index?: number; item_id?: string }): boolean =>
437
+ typeof event.output_index === "number" || event.item_id !== undefined;
438
+ const lookupOpenFunctionCallItem = (event: {
439
+ output_index?: number;
440
+ item_id?: string;
441
+ }): StreamingItem | undefined => {
442
+ if (hasOpenItemKey(event)) return lookupOpenItem(event);
443
+ for (const candidate of openItemsInOrder) {
444
+ if (
445
+ candidate.item.type === "function_call" &&
446
+ candidate.block.type === "toolCall" &&
447
+ !candidate.block.argumentsDone
448
+ ) {
449
+ return candidate;
450
+ }
451
+ }
452
+ return lastOpenItem?.item.type === "function_call" ? lastOpenItem : undefined;
453
+ };
434
454
  const closeOpenItem = (
435
455
  outputIndex: number | undefined,
436
456
  itemId: string | undefined,
@@ -438,6 +458,10 @@ export async function processResponsesStream<TApi extends Api>(
438
458
  ): void => {
439
459
  if (typeof outputIndex === "number") openItemsByOutputIndex.delete(outputIndex);
440
460
  if (itemId) openItemsByItemId.delete(itemId);
461
+ if (entry) {
462
+ const index = openItemsInOrder.indexOf(entry);
463
+ if (index >= 0) openItemsInOrder.splice(index, 1);
464
+ }
441
465
  if (entry && lastOpenItem === entry) lastOpenItem = null;
442
466
  };
443
467
  const contentIndexOf = (block: ThinkingContent | TextContent | StreamingToolCallBlock): number =>
@@ -584,7 +608,7 @@ export async function processResponsesStream<TApi extends Api>(
584
608
  }
585
609
  }
586
610
  } else if (event.type === "response.function_call_arguments.delta") {
587
- const entry = lookupOpenItem(event);
611
+ const entry = lookupOpenFunctionCallItem(event);
588
612
  if (entry?.item.type === "function_call" && entry.block.type === "toolCall") {
589
613
  const block = entry.block;
590
614
  block.partialJson += event.delta;
@@ -601,11 +625,12 @@ export async function processResponsesStream<TApi extends Api>(
601
625
  });
602
626
  }
603
627
  } else if (event.type === "response.function_call_arguments.done") {
604
- const entry = lookupOpenItem(event);
628
+ const entry = lookupOpenFunctionCallItem(event);
605
629
  if (entry?.item.type === "function_call" && entry.block.type === "toolCall") {
606
630
  const block = entry.block;
607
631
  block.partialJson = event.arguments;
608
632
  block.arguments = parseStreamingJson(block.partialJson);
633
+ block.argumentsDone = true;
609
634
  delete (block as { partialJson?: string }).partialJson;
610
635
  delete (block as { lastParseLen?: number }).lastParseLen;
611
636
  }
@@ -668,9 +693,11 @@ export async function processResponsesStream<TApi extends Api>(
668
693
  closeOpenItem(event.output_index, item.id, entry);
669
694
  } else if (item.type === "function_call") {
670
695
  const block = entry?.block.type === "toolCall" ? entry.block : undefined;
671
- const args = block?.partialJson
672
- ? parseStreamingJson(block.partialJson)
673
- : parseStreamingJson(item.arguments || "{}");
696
+ const args = block?.argumentsDone
697
+ ? block.arguments
698
+ : block?.partialJson
699
+ ? parseStreamingJson(block.partialJson)
700
+ : parseStreamingJson(item.arguments || "{}");
674
701
  const toolCall: ToolCall = {
675
702
  type: "toolCall",
676
703
  id: encodeResponsesToolCallId(item.call_id, item.id),
@@ -685,6 +712,7 @@ export async function processResponsesStream<TApi extends Api>(
685
712
  block.arguments = args;
686
713
  delete (block as { partialJson?: string }).partialJson;
687
714
  delete (block as { lastParseLen?: number }).lastParseLen;
715
+ delete (block as { argumentsDone?: boolean }).argumentsDone;
688
716
  }
689
717
  const contentIndex = block ? contentIndexOf(block) : output.content.length - 1;
690
718
  closeOpenItem(event.output_index, item.id, entry);
@@ -21,7 +21,13 @@ import type {
21
21
  } from "../types";
22
22
  import { type AbortSourceTracker, createAbortSourceTracker } from "../utils/abort";
23
23
  import { AssistantMessageEventStream as EventStreamImpl } from "../utils/event-stream";
24
- import { getStreamFirstEventTimeoutMs, getStreamIdleTimeoutMs, iterateWithIdleTimeout } from "../utils/idle-iterator";
24
+ import {
25
+ getOpenAIStreamFirstEventTimeoutMs,
26
+ getOpenAIStreamIdleTimeoutMs,
27
+ getStreamFirstEventTimeoutMs,
28
+ getStreamIdleTimeoutMs,
29
+ iterateWithIdleTimeout,
30
+ } from "../utils/idle-iterator";
25
31
  import type { BedrockOptions } from "./amazon-bedrock";
26
32
  import type { AnthropicOptions } from "./anthropic";
27
33
  import type { AzureOpenAIResponsesOptions } from "./azure-openai-responses";
@@ -167,11 +173,10 @@ function hasFinalResult(
167
173
  }
168
174
 
169
175
  /**
170
- * Per-provider default overrides for the lazy stream watchdogs. These widen the
171
- * floor used when neither caller option nor env var pins a value. The env vars
172
- * (`PI_STREAM_FIRST_EVENT_TIMEOUT_MS`, `PI_STREAM_IDLE_TIMEOUT_MS`) still take
173
- * precedence; `StreamOptions.streamFirstEventTimeoutMs` / `streamIdleTimeoutMs`
174
- * still trump everything.
176
+ * floor used when neither caller option nor env var pins a value. Generic env
177
+ * vars (`PI_STREAM_FIRST_EVENT_TIMEOUT_MS`, `PI_STREAM_IDLE_TIMEOUT_MS`) still
178
+ * take precedence unless a provider opts into OpenAI-family idle flooring for
179
+ * local backends that users historically tuned with `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS`.
175
180
  */
176
181
  interface LazyStreamLimits {
177
182
  defaultFirstEventTimeoutMs?: number;
@@ -181,6 +186,12 @@ interface LazyStreamLimits {
181
186
  * stream timeouts. Keep the lazy loader from racing it with generic errors.
182
187
  */
183
188
  providerHandlesStreamTimeouts?: boolean;
189
+ /**
190
+ * Apply OpenAI-family idle timeout precedence in the lazy wrapper. Used by
191
+ * local backends whose users historically tune slow prompt-processing gaps
192
+ * with `PI_OPENAI_STREAM_IDLE_TIMEOUT_MS`.
193
+ */
194
+ openAIIdleEnvFloorsFirstEvent?: boolean;
184
195
  }
185
196
  /**
186
197
  * Cloud Code Assist (google-gemini-cli / google-antigravity) routinely takes
@@ -199,6 +210,10 @@ const PROVIDER_HANDLED_STREAM_TIMEOUTS: LazyStreamLimits = {
199
210
  providerHandlesStreamTimeouts: true,
200
211
  };
201
212
 
213
+ const OPENAI_IDLE_FLOORED_LAZY_STREAM_LIMITS: LazyStreamLimits = {
214
+ openAIIdleEnvFloorsFirstEvent: true,
215
+ };
216
+
202
217
  function forwardStream<TApi extends Api>(
203
218
  target: EventStreamImpl,
204
219
  source: AsyncIterable<AssistantMessageEvent>,
@@ -212,13 +227,19 @@ function forwardStream<TApi extends Api>(
212
227
  const providerHandlesStreamTimeouts = limits?.providerHandlesStreamTimeouts === true;
213
228
  const idleTimeoutMs = providerHandlesStreamTimeouts
214
229
  ? undefined
215
- : (options.streamIdleTimeoutMs ?? getStreamIdleTimeoutMs(limits?.defaultIdleTimeoutMs));
230
+ : (options.streamIdleTimeoutMs ??
231
+ (limits?.openAIIdleEnvFloorsFirstEvent
232
+ ? getOpenAIStreamIdleTimeoutMs(limits.defaultIdleTimeoutMs)
233
+ : getStreamIdleTimeoutMs(limits?.defaultIdleTimeoutMs)));
234
+ const firstItemTimeoutMs = providerHandlesStreamTimeouts
235
+ ? 0
236
+ : (options.streamFirstEventTimeoutMs ??
237
+ (limits?.openAIIdleEnvFloorsFirstEvent
238
+ ? getOpenAIStreamFirstEventTimeoutMs(idleTimeoutMs, limits.defaultFirstEventTimeoutMs)
239
+ : getStreamFirstEventTimeoutMs(idleTimeoutMs, limits?.defaultFirstEventTimeoutMs)));
216
240
  const watchedSource = iterateWithIdleTimeout(source, {
217
241
  idleTimeoutMs,
218
- firstItemTimeoutMs: providerHandlesStreamTimeouts
219
- ? 0
220
- : (options.streamFirstEventTimeoutMs ??
221
- getStreamFirstEventTimeoutMs(idleTimeoutMs, limits?.defaultFirstEventTimeoutMs)),
242
+ firstItemTimeoutMs,
222
243
  errorMessage: LAZY_STREAM_IDLE_TIMEOUT_ERROR,
223
244
  firstItemErrorMessage: LAZY_STREAM_FIRST_EVENT_TIMEOUT_ERROR,
224
245
  onIdle: () => abortTracker.abortLocally(new Error(LAZY_STREAM_IDLE_TIMEOUT_ERROR)),
@@ -431,6 +452,6 @@ export const streamOpenAIResponses = createLazyStream(
431
452
  PROVIDER_HANDLED_STREAM_TIMEOUTS,
432
453
  );
433
454
  export const streamCursor = createLazyStream(loadCursorProviderModule);
434
- export const streamOllama = createLazyStream(loadOllamaProviderModule);
455
+ export const streamOllama = createLazyStream(loadOllamaProviderModule, OPENAI_IDLE_FLOORED_LAZY_STREAM_LIMITS);
435
456
 
436
457
  export const streamBedrock = createLazyStream(loadBedrockProviderModule);