@oh-my-pi/pi-ai 17.1.7 → 17.1.8

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,14 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.1.8] - 2026-07-28
6
+
7
+ ### Fixed
8
+
9
+ - Fixed an HTTP 400 error when resuming or replaying OpenAI history after an interrupted native Computer Use turn.
10
+ - Fixed connection 404 errors when using Google Vertex AI in multi-region locations (eu and us) by correctly resolving regional endpoint (REP) hosts.
11
+ - Fixed a resource leak in SqliteAuthCredentialStore.close() where unclosed prepared statements kept the SQLite connection alive, preventing database file cleanup (especially on Windows where files remained locked).
12
+
5
13
  ## [17.1.7] - 2026-07-27
6
14
 
7
15
  ### Changed
@@ -15,8 +15,16 @@ export declare function normalizeResponsesToolCallId(id: string, itemPrefix?: Re
15
15
  export declare function truncateResponseItemId(id: string, prefix: string): string;
16
16
  interface OpenAIResponsesReplaySanitizeOptions {
17
17
  supportsImageDetailOriginal?: boolean;
18
+ supportsComputerUse?: boolean;
18
19
  }
19
20
  export declare function sanitizeOpenAIResponsesHistoryItemsForReplay(items: Array<Record<string, unknown>>, options?: OpenAIResponsesReplaySanitizeOptions): ResponseInput;
21
+ /** Strip reasoning IDs whose only linked native output is a computer call that will be demoted. */
22
+ export declare function stripOpenAIResponsesComputerLinkedReasoningIdsForReplay(items: ResponseInput): ResponseInput;
23
+ /**
24
+ * Finalize provisional native-computer reasoning IDs after the complete
25
+ * Responses input has been rebuilt, model-adapted, and orphan-repaired.
26
+ */
27
+ export declare function stripUnpairedOpenAIResponsesComputerReasoningIdsForReplay(items: ResponseInput): ResponseInput;
20
28
  /**
21
29
  * Sanitize assistant-native Responses history for replay.
22
30
  *
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "17.1.7",
4
+ "version": "17.1.8",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -38,9 +38,9 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@bufbuild/protobuf": "^2.12.1",
41
- "@oh-my-pi/pi-catalog": "17.1.7",
42
- "@oh-my-pi/pi-utils": "17.1.7",
43
- "@oh-my-pi/pi-wire": "17.1.7",
41
+ "@oh-my-pi/pi-catalog": "17.1.8",
42
+ "@oh-my-pi/pi-utils": "17.1.8",
43
+ "@oh-my-pi/pi-wire": "17.1.8",
44
44
  "arktype": "2.2.3",
45
45
  "zod": "^4"
46
46
  },
@@ -7923,6 +7923,14 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
7923
7923
  this.#updateUsageHistoryStmt.finalize();
7924
7924
  this.#insertUsageCostStmt.finalize();
7925
7925
  this.#listUsageCostsStmt.finalize();
7926
+ this.#updateIfMatchesStmt.finalize();
7927
+ this.#updateIfMatchesWithLeaseStmt.finalize();
7928
+ this.#deleteIfMatchesWithLeaseStmt.finalize();
7929
+ this.#deleteCachePrefixStmt.finalize();
7930
+ this.#acquireCredentialRefreshLeaseStmt.finalize();
7931
+ this.#getCredentialRefreshLeaseStmt.finalize();
7932
+ this.#renewCredentialRefreshLeaseStmt.finalize();
7933
+ this.#releaseCredentialRefreshLeaseStmt.finalize();
7926
7934
  this.#db.close();
7927
7935
  }
7928
7936
  }
@@ -1,3 +1,4 @@
1
+ import { resolveVertexEndpointHost } from "@oh-my-pi/pi-catalog/hosts";
1
2
  import { $env } from "@oh-my-pi/pi-utils";
2
3
  import * as AIError from "../error";
3
4
  import type { Context, Model, StreamFunction } from "../types";
@@ -69,7 +70,7 @@ export const streamGoogleVertex: StreamFunction<"google-vertex"> = (
69
70
  // global-only request.
70
71
  const explicitLocation = options?.location;
71
72
  const location = explicitLocation ?? resolveAmbientLocation() ?? "global";
72
- const host = resolveEndpointHost(location);
73
+ const host = resolveVertexEndpointHost(location);
73
74
  const path = `${API_VERSION}/publishers/google/models/${model.id}:streamGenerateContent?alt=sse`;
74
75
  const useGlobalFallback = !explicitLocation && host !== "aiplatform.googleapis.com";
75
76
  return {
@@ -87,7 +88,7 @@ export const streamGoogleVertex: StreamFunction<"google-vertex"> = (
87
88
  const project = resolveProject(options);
88
89
  const location = resolveLocation(options);
89
90
  const accessToken = await getVertexAccessToken({ signal: options?.signal, fetch: options?.fetch });
90
- const host = resolveEndpointHost(location);
91
+ const host = resolveVertexEndpointHost(location);
91
92
  const url = `https://${host}/${API_VERSION}/projects/${project}/locations/${location}/publishers/google/models/${model.id}:streamGenerateContent?alt=sse`;
92
93
  return {
93
94
  params,
@@ -117,9 +118,6 @@ function resolveProject(options?: GoogleVertexOptions): string {
117
118
  return project;
118
119
  }
119
120
 
120
- function resolveEndpointHost(location: string): string {
121
- return location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`;
122
- }
123
121
  function resolveAmbientLocation(): string | undefined {
124
122
  return $env.GOOGLE_VERTEX_LOCATION || $env.GOOGLE_CLOUD_LOCATION || $env.VERTEX_LOCATION || undefined;
125
123
  }
@@ -51,6 +51,7 @@ import {
51
51
  normalizeSystemPrompts,
52
52
  sanitizeOpenAIResponsesAssistantFallbackItemsForReplay,
53
53
  sanitizeOpenAIResponsesAssistantHistoryItemsForReplay,
54
+ stripOpenAIResponsesComputerLinkedReasoningIdsForReplay,
54
55
  } from "../utils";
55
56
  import { clearStreamingPartialJson, kStreamingLastParseLen, kStreamingPartialJson } from "../utils/block-symbols";
56
57
  import { AssistantMessageEventStream } from "../utils/event-stream";
@@ -1172,8 +1173,9 @@ export function normalizeCodexToolChoice(
1172
1173
  return undefined;
1173
1174
  }
1174
1175
  function unrollCodexComputerItems(items: ResponseInput, supportsImageDetailOriginal: boolean): ResponseInput {
1176
+ const replayItems = stripOpenAIResponsesComputerLinkedReasoningIdsForReplay(items);
1175
1177
  const unrolled: ResponseInput = [];
1176
- for (const item of items) {
1178
+ for (const item of replayItems) {
1177
1179
  if (item.type === "computer_call") {
1178
1180
  const actions = item.actions ?? (item.action ? [item.action] : []);
1179
1181
  unrolled.push({
@@ -69,6 +69,7 @@ import {
69
69
  sanitizeOpenAIResponsesAssistantFallbackItemsForReplay,
70
70
  sanitizeOpenAIResponsesAssistantHistoryItemsForReplay,
71
71
  sanitizeOpenAIResponsesHistoryItemsForReplay,
72
+ stripUnpairedOpenAIResponsesComputerReasoningIdsForReplay,
72
73
  } from "../utils";
73
74
  import {
74
75
  clearStreamingPartialJson,
@@ -1646,6 +1647,7 @@ export function buildResponsesInput<TApi extends Api>(options: BuildResponsesInp
1646
1647
  if (historyItems && shouldReplayPayloadItems) {
1647
1648
  const sanitizedItems = sanitizeOpenAIResponsesHistoryItemsForReplay(filterReasoning(historyItems), {
1648
1649
  supportsImageDetailOriginal,
1650
+ supportsComputerUse: options.model.supportsComputerUse === true,
1649
1651
  });
1650
1652
  messages.push(
1651
1653
  ...adaptResponsesReplayItemsForModel(
@@ -1694,7 +1696,10 @@ export function buildResponsesInput<TApi extends Api>(options: BuildResponsesInp
1694
1696
  if (historyItems) {
1695
1697
  const rawSanitizedHistoryItems = sanitizeOpenAIResponsesAssistantHistoryItemsForReplay(
1696
1698
  filterReasoning(historyItems),
1697
- { supportsImageDetailOriginal },
1699
+ {
1700
+ supportsImageDetailOriginal,
1701
+ supportsComputerUse: options.model.supportsComputerUse === true,
1702
+ },
1698
1703
  );
1699
1704
  const sanitizedHistoryItems = rawSanitizedHistoryItems
1700
1705
  ? adaptResponsesReplayItemsForModel(
@@ -1755,7 +1760,8 @@ export function buildResponsesInput<TApi extends Api>(options: BuildResponsesInp
1755
1760
  }
1756
1761
 
1757
1762
  const withRepairedOutputs = options.repairOrphanOutputs ? repairOrphanResponsesToolOutputs(messages) : messages;
1758
- return repairOrphanResponsesToolCalls(withRepairedOutputs);
1763
+ const withRepairedCalls = repairOrphanResponsesToolCalls(withRepairedOutputs);
1764
+ return stripUnpairedOpenAIResponsesComputerReasoningIdsForReplay(withRepairedCalls);
1759
1765
  }
1760
1766
 
1761
1767
  type ResponsesReplayAssistantMessage = Omit<ResponseOutputMessage, "id"> & { id?: string };
package/src/stream.ts CHANGED
@@ -5,7 +5,7 @@ import * as path from "node:path";
5
5
  import { scheduler } from "node:timers/promises";
6
6
  import { isOfficialAnthropicApiUrl } from "@oh-my-pi/pi-catalog/compat/anthropic";
7
7
  import type { Effort } from "@oh-my-pi/pi-catalog/effort";
8
- import { isVertexExpressOpenAIUrl, isVertexRawPredictUrl } from "@oh-my-pi/pi-catalog/hosts";
8
+ import { isVertexExpressOpenAIUrl, isVertexRawPredictUrl, resolveVertexEndpointHost } from "@oh-my-pi/pi-catalog/hosts";
9
9
  import {
10
10
  mapEffortToAnthropicAdaptiveEffort,
11
11
  mapEffortToGoogleThinkingLevel,
@@ -662,7 +662,7 @@ function resolveVertexRequest(input: string | URL | Request): string | URL | Req
662
662
  url.includes("{location}") ||
663
663
  url.includes("%7Bproject%7D") ||
664
664
  url.includes("%7Blocation%7D");
665
- const host = location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`;
665
+ const host = resolveVertexEndpointHost(location);
666
666
  const rewritten = hasPlaceholder
667
667
  ? url
668
668
  .replace("https://{location}-aiplatform.googleapis.com", `https://${host}`)
package/src/utils.ts CHANGED
@@ -70,6 +70,7 @@ export function truncateResponseItemId(id: string, prefix: string): string {
70
70
 
71
71
  interface OpenAIResponsesReplaySanitizeOptions {
72
72
  supportsImageDetailOriginal?: boolean;
73
+ supportsComputerUse?: boolean;
73
74
  }
74
75
 
75
76
  /**
@@ -100,22 +101,185 @@ function clampReplayItemImageDetail(
100
101
  return changed ? { ...item, content } : item;
101
102
  }
102
103
 
104
+ function isOpenAIResponsesClientInputBoundary(item: Record<string, unknown>): boolean {
105
+ if (item.type === "message") return item.role !== "assistant";
106
+ if (item.type === undefined && typeof item.role === "string") return item.role !== "assistant";
107
+
108
+ switch (item.type) {
109
+ case "input_text":
110
+ case "input_image":
111
+ case "input_file":
112
+ case "input_audio":
113
+ case "function_call_output":
114
+ case "custom_tool_call_output":
115
+ case "computer_call_output":
116
+ case "local_shell_call_output":
117
+ case "shell_call_output":
118
+ case "apply_patch_call_output":
119
+ case "mcp_approval_response":
120
+ case "compaction":
121
+ case "compaction_summary":
122
+ case "compaction_trigger":
123
+ case "item_reference":
124
+ return true;
125
+ case "additional_tools":
126
+ return item.role !== "assistant";
127
+ case "tool_search_output":
128
+ return item.execution !== "server";
129
+ default:
130
+ return false;
131
+ }
132
+ }
133
+
134
+ function collectOpenAIResponsesComputerLinkedReasoningItems(
135
+ items: Array<Record<string, unknown>>,
136
+ requireLaterOutput: boolean,
137
+ ): Set<Record<string, unknown>> {
138
+ let computerCallsWithLaterOutputs: Set<Record<string, unknown>> | undefined;
139
+ if (requireLaterOutput) {
140
+ computerCallsWithLaterOutputs = new Set();
141
+ const laterComputerOutputCallIds = new Set<string>();
142
+ for (let index = items.length - 1; index >= 0; index--) {
143
+ const item = items[index]!;
144
+ if (item.type === "computer_call_output" && typeof item.call_id === "string") {
145
+ laterComputerOutputCallIds.add(item.call_id);
146
+ } else if (
147
+ item.type === "computer_call" &&
148
+ typeof item.id === "string" &&
149
+ typeof item.call_id === "string" &&
150
+ laterComputerOutputCallIds.has(item.call_id)
151
+ ) {
152
+ computerCallsWithLaterOutputs.add(item);
153
+ }
154
+ }
155
+ }
156
+
157
+ const computerLinkedReasoningItems = new Set<Record<string, unknown>>();
158
+ const responseReasoningItems: Array<Record<string, unknown>> = [];
159
+ for (const item of items) {
160
+ if (isOpenAIResponsesClientInputBoundary(item)) {
161
+ responseReasoningItems.length = 0;
162
+ } else if (item.type === "reasoning") {
163
+ responseReasoningItems.push(item);
164
+ } else if (
165
+ item.type === "computer_call" &&
166
+ typeof item.id === "string" &&
167
+ (!computerCallsWithLaterOutputs || computerCallsWithLaterOutputs.has(item))
168
+ ) {
169
+ for (const reasoningItem of responseReasoningItems) computerLinkedReasoningItems.add(reasoningItem);
170
+ }
171
+ }
172
+ return computerLinkedReasoningItems;
173
+ }
174
+
175
+ const provisionalOpenAIResponsesComputerReasoningItems = new WeakSet<object>();
176
+
103
177
  export function sanitizeOpenAIResponsesHistoryItemsForReplay(
104
178
  items: Array<Record<string, unknown>>,
105
179
  options: OpenAIResponsesReplaySanitizeOptions = {},
106
180
  ): ResponseInput {
107
181
  const normalizedCallIds = new Map<string, string>();
108
182
  const supportsImageDetailOriginal = options.supportsImageDetailOriginal !== false;
183
+ const computerLinkedReasoningItems =
184
+ options.supportsComputerUse === false
185
+ ? undefined
186
+ : collectOpenAIResponsesComputerLinkedReasoningItems(items, false);
109
187
  return items.flatMap(item => {
188
+ const preserveForComputer = computerLinkedReasoningItems?.has(item) === true;
110
189
  const sanitized = sanitizeOpenAIResponsesHistoryItemForReplay(
111
190
  item,
112
191
  normalizedCallIds,
113
192
  supportsImageDetailOriginal,
193
+ preserveForComputer,
114
194
  );
195
+ if (preserveForComputer && sanitized?.type === "reasoning") {
196
+ provisionalOpenAIResponsesComputerReasoningItems.add(sanitized);
197
+ }
115
198
  return sanitized ? [sanitized] : [];
116
199
  });
117
200
  }
118
201
 
202
+ function collectOpenAIResponsesReasoningItemsWithSurvivingOutputIds(
203
+ items: Array<Record<string, unknown>>,
204
+ ): Set<Record<string, unknown>> {
205
+ const retainedReasoningItems = new Set<Record<string, unknown>>();
206
+ let responseReasoningItems: Array<Record<string, unknown>> = [];
207
+ let hasSurvivingOutputId = false;
208
+ const finishResponse = (): void => {
209
+ if (hasSurvivingOutputId) {
210
+ for (const reasoningItem of responseReasoningItems) retainedReasoningItems.add(reasoningItem);
211
+ }
212
+ responseReasoningItems = [];
213
+ hasSurvivingOutputId = false;
214
+ };
215
+
216
+ for (const item of items) {
217
+ if (isOpenAIResponsesClientInputBoundary(item)) {
218
+ finishResponse();
219
+ } else if (item.type === "reasoning") {
220
+ responseReasoningItems.push(item);
221
+ } else if (item.type !== "computer_call" && typeof item.id === "string") {
222
+ hasSurvivingOutputId = true;
223
+ }
224
+ }
225
+ finishResponse();
226
+ return retainedReasoningItems;
227
+ }
228
+
229
+ /** Strip reasoning IDs whose only linked native output is a computer call that will be demoted. */
230
+ export function stripOpenAIResponsesComputerLinkedReasoningIdsForReplay(items: ResponseInput): ResponseInput {
231
+ const records = items as unknown as Array<Record<string, unknown>>;
232
+ const linkedReasoningItems = collectOpenAIResponsesComputerLinkedReasoningItems(records, false);
233
+ const retainedReasoningItems = collectOpenAIResponsesReasoningItemsWithSurvivingOutputIds(records);
234
+ let sanitized: ResponseInput | undefined;
235
+
236
+ for (let index = 0; index < items.length; index++) {
237
+ const item = items[index]!;
238
+ const record = records[index]!;
239
+ if (
240
+ item.type !== "reasoning" ||
241
+ typeof record.id !== "string" ||
242
+ !linkedReasoningItems.has(record) ||
243
+ retainedReasoningItems.has(record)
244
+ ) {
245
+ sanitized?.push(item);
246
+ continue;
247
+ }
248
+ if (!sanitized) sanitized = items.slice(0, index);
249
+ const { id: _id, ...withoutId } = record;
250
+ sanitized.push(withoutId as unknown as ResponseInput[number]);
251
+ }
252
+ return sanitized ?? items;
253
+ }
254
+
255
+ /**
256
+ * Finalize provisional native-computer reasoning IDs after the complete
257
+ * Responses input has been rebuilt, model-adapted, and orphan-repaired.
258
+ */
259
+ export function stripUnpairedOpenAIResponsesComputerReasoningIdsForReplay(items: ResponseInput): ResponseInput {
260
+ const records = items as unknown as Array<Record<string, unknown>>;
261
+ const linkedReasoningItems = collectOpenAIResponsesComputerLinkedReasoningItems(records, true);
262
+ let sanitized: ResponseInput | undefined;
263
+
264
+ for (let index = 0; index < items.length; index++) {
265
+ const item = items[index]!;
266
+ const record = records[index]!;
267
+ if (
268
+ item.type !== "reasoning" ||
269
+ !provisionalOpenAIResponsesComputerReasoningItems.has(item) ||
270
+ typeof record.id !== "string" ||
271
+ linkedReasoningItems.has(record)
272
+ ) {
273
+ sanitized?.push(item);
274
+ continue;
275
+ }
276
+ if (!sanitized) sanitized = items.slice(0, index);
277
+ const { id: _id, ...withoutId } = record;
278
+ sanitized.push(withoutId as unknown as ResponseInput[number]);
279
+ }
280
+ return sanitized ?? items;
281
+ }
282
+
119
283
  /**
120
284
  * Sanitize assistant-native Responses history for replay.
121
285
  *
@@ -198,11 +362,13 @@ function sanitizeOpenAIResponsesHistoryItemForReplay(
198
362
  item: Record<string, unknown>,
199
363
  normalizedCallIds: Map<string, string>,
200
364
  supportsImageDetailOriginal: boolean,
365
+ preserveReasoningItemIds: boolean,
201
366
  ): OpenAIResponsesReplayItem | undefined {
202
367
  if (item.type === "item_reference") return undefined;
203
368
  if (item.type === "image_generation_call") return sanitizeOpenAIResponsesImageGenerationCallForReplay(item);
204
- if (item.type === "reasoning") return sanitizeOpenAIResponsesReasoningItemForReplay(item);
205
-
369
+ if (item.type === "reasoning") {
370
+ return sanitizeOpenAIResponsesReasoningItemForReplay(item, preserveReasoningItemIds);
371
+ }
206
372
  // Strip status only from item types whose replay input rejects output
207
373
  // lifecycle metadata. Hosted built-in tool items require status for replay.
208
374
  const { id: _id, ...sanitizedItem } = item;
@@ -220,8 +386,12 @@ function sanitizeOpenAIResponsesHistoryItemForReplay(
220
386
  ) as unknown as OpenAIResponsesReplayItem;
221
387
  }
222
388
 
223
- function sanitizeOpenAIResponsesReasoningItemForReplay(item: Record<string, unknown>): OpenAIResponsesReplayItem {
389
+ function sanitizeOpenAIResponsesReasoningItemForReplay(
390
+ item: Record<string, unknown>,
391
+ preserveItemId: boolean,
392
+ ): OpenAIResponsesReplayItem {
224
393
  const sanitizedItem: Record<string, unknown> = { type: "reasoning" };
394
+ if (preserveItemId && typeof item.id === "string") sanitizedItem.id = item.id;
225
395
  if (Array.isArray(item.summary)) sanitizedItem.summary = item.summary;
226
396
  if (Array.isArray(item.content)) sanitizedItem.content = item.content;
227
397
  if (typeof item.encrypted_content === "string" || item.encrypted_content === null) {