@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.
|
|
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.
|
|
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:
|
|
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 =
|
|
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 =
|
|
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?.
|
|
672
|
-
?
|
|
673
|
-
:
|
|
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 {
|
|
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
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
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 ??
|
|
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
|
|
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);
|