@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.1",
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.1",
50
- "@oh-my-pi/pi-utils": "14.6.1",
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 { schema: parameters, strict: effectiveStrict } = adaptSchemaForStrict(baseParameters, strict);
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: ((value: IteratorResult<T>) => void)[] = [];
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 => this.waiting.push(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
+ }