@oh-my-pi/pi-ai 14.6.1 → 14.6.2
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,15 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [14.6.2] - 2026-05-03
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Added `EventStream.fail(err)` method to terminate the async iterator with an error, enabling consumers to catch stream-level failures via `for await` without hanging
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Fixed OpenAI Responses tool schema conversion to rewrite non-strict `oneOf` unions to `anyOf` before sending tools to the Responses API ([#920](https://github.com/can1357/oh-my-pi/issues/920))
|
|
13
|
+
|
|
5
14
|
## [14.6.0] - 2026-05-02
|
|
6
15
|
|
|
7
16
|
### 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": "14.6.
|
|
4
|
+
"version": "14.6.2",
|
|
5
5
|
"description": "Unified LLM API with automatic model discovery and provider configuration",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"@aws-sdk/credential-provider-node": "^3.972.36",
|
|
47
47
|
"@bufbuild/protobuf": "^2.12.0",
|
|
48
48
|
"@google/genai": "^1.50.1",
|
|
49
|
-
"@oh-my-pi/pi-natives": "14.6.
|
|
50
|
-
"@oh-my-pi/pi-utils": "14.6.
|
|
49
|
+
"@oh-my-pi/pi-natives": "14.6.2",
|
|
50
|
+
"@oh-my-pi/pi-utils": "14.6.2",
|
|
51
51
|
"@sinclair/typebox": "^0.34.49",
|
|
52
52
|
"@smithy/node-http-handler": "^4.6.1",
|
|
53
53
|
"ajv": "^8.20.0",
|
|
@@ -40,7 +40,7 @@ import {
|
|
|
40
40
|
import { parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
|
|
41
41
|
import { notifyProviderResponse } from "../utils/provider-response";
|
|
42
42
|
import { callWithCopilotModelRetry } from "../utils/retry";
|
|
43
|
-
import { adaptSchemaForStrict, NO_STRICT } from "../utils/schema";
|
|
43
|
+
import { adaptSchemaForStrict, NO_STRICT, sanitizeSchemaForOpenAIResponses } from "../utils/schema";
|
|
44
44
|
import { mapToOpenAIResponsesToolChoice, type OpenAIResponsesToolChoice } from "../utils/tool-choice";
|
|
45
45
|
import {
|
|
46
46
|
buildCopilotDynamicHeaders,
|
|
@@ -592,7 +592,8 @@ export function convertTools(tools: Tool[], strictMode: boolean, model: Model<"o
|
|
|
592
592
|
}
|
|
593
593
|
const strict = !NO_STRICT && strictMode && tool.strict !== false;
|
|
594
594
|
const baseParameters = tool.parameters as unknown as Record<string, unknown>;
|
|
595
|
-
const
|
|
595
|
+
const responseParameters = sanitizeSchemaForOpenAIResponses(baseParameters);
|
|
596
|
+
const { schema: parameters, strict: effectiveStrict } = adaptSchemaForStrict(responseParameters, strict);
|
|
596
597
|
return {
|
|
597
598
|
type: "function",
|
|
598
599
|
name: tool.name,
|
|
@@ -3,17 +3,24 @@ import type { AssistantMessage, AssistantMessageEvent } from "../types";
|
|
|
3
3
|
// Generic event stream class for async iteration
|
|
4
4
|
export class EventStream<T, R = T> implements AsyncIterable<T> {
|
|
5
5
|
queue: T[] = [];
|
|
6
|
-
waiting: (
|
|
6
|
+
waiting: Array<{ resolve: (value: IteratorResult<T>) => void; reject: (err: unknown) => void }> = [];
|
|
7
7
|
done = false;
|
|
8
|
+
#failed = false;
|
|
9
|
+
#error: unknown = undefined;
|
|
8
10
|
finalResultPromise: Promise<R>;
|
|
9
11
|
resolveFinalResult!: (result: R) => void;
|
|
12
|
+
rejectFinalResult!: (err: unknown) => void;
|
|
10
13
|
isComplete: (event: T) => boolean;
|
|
11
14
|
extractResult: (event: T) => R;
|
|
12
15
|
|
|
13
16
|
constructor(isComplete: (event: T) => boolean, extractResult: (event: T) => R) {
|
|
14
|
-
const { promise, resolve } = Promise.withResolvers<R>();
|
|
17
|
+
const { promise, resolve, reject } = Promise.withResolvers<R>();
|
|
18
|
+
// Prevent an unhandled rejection when fail() is called but nobody awaits result().
|
|
19
|
+
// Callers who do await result() still receive the rejection normally.
|
|
20
|
+
promise.catch(() => {});
|
|
15
21
|
this.finalResultPromise = promise;
|
|
16
22
|
this.resolveFinalResult = resolve;
|
|
23
|
+
this.rejectFinalResult = reject;
|
|
17
24
|
this.isComplete = isComplete;
|
|
18
25
|
this.extractResult = extractResult;
|
|
19
26
|
}
|
|
@@ -29,7 +36,7 @@ export class EventStream<T, R = T> implements AsyncIterable<T> {
|
|
|
29
36
|
// Deliver to waiting consumer or queue it
|
|
30
37
|
const waiter = this.waiting.shift();
|
|
31
38
|
if (waiter) {
|
|
32
|
-
waiter({ value: event, done: false });
|
|
39
|
+
waiter.resolve({ value: event, done: false });
|
|
33
40
|
} else {
|
|
34
41
|
this.queue.push(event);
|
|
35
42
|
}
|
|
@@ -38,7 +45,7 @@ export class EventStream<T, R = T> implements AsyncIterable<T> {
|
|
|
38
45
|
deliver(event: T): void {
|
|
39
46
|
const waiter = this.waiting.shift();
|
|
40
47
|
if (waiter) {
|
|
41
|
-
waiter({ value: event, done: false });
|
|
48
|
+
waiter.resolve({ value: event, done: false });
|
|
42
49
|
} else {
|
|
43
50
|
this.queue.push(event);
|
|
44
51
|
}
|
|
@@ -52,14 +59,26 @@ export class EventStream<T, R = T> implements AsyncIterable<T> {
|
|
|
52
59
|
// Notify all waiting consumers that we're done
|
|
53
60
|
while (this.waiting.length > 0) {
|
|
54
61
|
const waiter = this.waiting.shift()!;
|
|
55
|
-
waiter({ value: undefined as any, done: true });
|
|
62
|
+
waiter.resolve({ value: undefined as any, done: true });
|
|
56
63
|
}
|
|
57
64
|
}
|
|
58
65
|
|
|
59
66
|
endWaiting(): void {
|
|
60
67
|
while (this.waiting.length > 0) {
|
|
61
68
|
const waiter = this.waiting.shift()!;
|
|
62
|
-
waiter({ value: undefined as any, done: true });
|
|
69
|
+
waiter.resolve({ value: undefined as any, done: true });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fail(err: unknown): void {
|
|
74
|
+
if (this.done) return;
|
|
75
|
+
this.done = true;
|
|
76
|
+
this.#failed = true;
|
|
77
|
+
this.#error = err;
|
|
78
|
+
this.rejectFinalResult(err);
|
|
79
|
+
while (this.waiting.length > 0) {
|
|
80
|
+
const waiter = this.waiting.shift()!;
|
|
81
|
+
waiter.reject(err);
|
|
63
82
|
}
|
|
64
83
|
}
|
|
65
84
|
|
|
@@ -67,10 +86,14 @@ export class EventStream<T, R = T> implements AsyncIterable<T> {
|
|
|
67
86
|
while (true) {
|
|
68
87
|
if (this.queue.length > 0) {
|
|
69
88
|
yield this.queue.shift()!;
|
|
89
|
+
} else if (this.#failed) {
|
|
90
|
+
throw this.#error;
|
|
70
91
|
} else if (this.done) {
|
|
71
92
|
return;
|
|
72
93
|
} else {
|
|
73
|
-
const result = await new Promise<IteratorResult<T>>(resolve =>
|
|
94
|
+
const result = await new Promise<IteratorResult<T>>((resolve, reject) =>
|
|
95
|
+
this.waiting.push({ resolve, reject }),
|
|
96
|
+
);
|
|
74
97
|
if (result.done) return;
|
|
75
98
|
yield result.value;
|
|
76
99
|
}
|
|
@@ -144,6 +167,15 @@ export class AssistantMessageEventStream extends EventStream<AssistantMessageEve
|
|
|
144
167
|
this.endWaiting();
|
|
145
168
|
}
|
|
146
169
|
|
|
170
|
+
override fail(err: unknown): void {
|
|
171
|
+
if (this.#flushTimer) {
|
|
172
|
+
clearTimeout(this.#flushTimer);
|
|
173
|
+
this.#flushTimer = undefined;
|
|
174
|
+
}
|
|
175
|
+
this.#deltaBuffer = [];
|
|
176
|
+
super.fail(err);
|
|
177
|
+
}
|
|
178
|
+
|
|
147
179
|
#scheduleFlush(): void {
|
|
148
180
|
if (this.#flushTimer) return; // Already scheduled
|
|
149
181
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { tryEnforceStrictSchema } from "./strict-mode";
|
|
2
|
+
import type { JsonObject } from "./types";
|
|
2
3
|
/**
|
|
3
4
|
* Consolidated helper for OpenAI-style strict schema enforcement.
|
|
4
5
|
*
|
|
@@ -18,3 +19,51 @@ export function adaptSchemaForStrict(
|
|
|
18
19
|
|
|
19
20
|
return tryEnforceStrictSchema(schema);
|
|
20
21
|
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* OpenAI Responses rejects `oneOf` in tool schemas even when strict mode is
|
|
25
|
+
* disabled. Non-strict schemas can still use `anyOf`, so preserve the union
|
|
26
|
+
* shape by recursively rewriting `oneOf` branches to `anyOf`.
|
|
27
|
+
*/
|
|
28
|
+
export function sanitizeSchemaForOpenAIResponses(schema: JsonObject): JsonObject {
|
|
29
|
+
return rewriteOneOfToAnyOf(schema) as JsonObject;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function rewriteOneOfToAnyOf(value: unknown): unknown {
|
|
33
|
+
if (Array.isArray(value)) {
|
|
34
|
+
let changed = false;
|
|
35
|
+
const rewritten = value.map(item => {
|
|
36
|
+
const next = rewriteOneOfToAnyOf(item);
|
|
37
|
+
if (next !== item) changed = true;
|
|
38
|
+
return next;
|
|
39
|
+
});
|
|
40
|
+
return changed ? rewritten : value;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!value || typeof value !== "object") {
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const input = value as Record<string, unknown>;
|
|
48
|
+
let changed = false;
|
|
49
|
+
const output: Record<string, unknown> = {};
|
|
50
|
+
for (const [key, child] of Object.entries(input)) {
|
|
51
|
+
if (key === "oneOf") {
|
|
52
|
+
changed = true;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const next = rewriteOneOfToAnyOf(child);
|
|
56
|
+
if (next !== child) changed = true;
|
|
57
|
+
output[key] = next;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (Array.isArray(input.oneOf)) {
|
|
61
|
+
const rewrittenOneOf = rewriteOneOfToAnyOf(input.oneOf);
|
|
62
|
+
const existingAnyOf = output.anyOf;
|
|
63
|
+
output.anyOf = Array.isArray(existingAnyOf)
|
|
64
|
+
? [...existingAnyOf, ...(rewrittenOneOf as unknown[])]
|
|
65
|
+
: rewrittenOneOf;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return changed ? output : value;
|
|
69
|
+
}
|