@oh-my-pi/pi-ai 15.9.5 → 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.5",
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.5",
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);