@oh-my-pi/pi-ai 16.4.2 → 16.4.4

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
+ ## [16.4.3] - 2026-07-11
6
+
7
+ ### Fixed
8
+
9
+ - Fixed auth database upgrades from schema v5 by creating the OAuth credential refresh-lease table before lease statements are prepared.
10
+ - Fixed an issue in the Responses API where empty tool results were incorrectly serialized with a "(see attached image)" placeholder, causing models to look for non-existent attachments.
11
+ - Fixed OpenAI Responses server non-streaming envelopes to always include the required "incomplete_details" field, using null for completed responses.
12
+ - Preserved Cloud Code Assist tool schemas when mixed-type unions carry branch-local validation descriptions.
13
+
5
14
  ## [16.4.2] - 2026-07-10
6
15
 
7
16
  ### Fixed
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "16.4.2",
4
+ "version": "16.4.4",
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.0",
41
- "@oh-my-pi/pi-catalog": "16.4.2",
42
- "@oh-my-pi/pi-utils": "16.4.2",
43
- "@oh-my-pi/pi-wire": "16.4.2",
41
+ "@oh-my-pi/pi-catalog": "16.4.4",
42
+ "@oh-my-pi/pi-utils": "16.4.4",
43
+ "@oh-my-pi/pi-wire": "16.4.4",
44
44
  "arktype": "2.2.2",
45
45
  "zod": "^4"
46
46
  },
@@ -5637,6 +5637,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
5637
5637
  } catch {
5638
5638
  // Ignore chmod failures (e.g., Windows)
5639
5639
  }
5640
+ SqliteAuthCredentialStore.#ensureAuthCredentialRefreshLeasesTable(db);
5640
5641
  return new SqliteAuthCredentialStore(db);
5641
5642
  } catch (err) {
5642
5643
  db?.close();
@@ -5655,6 +5656,18 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
5655
5656
  );
5656
5657
  }
5657
5658
 
5659
+ static #ensureAuthCredentialRefreshLeasesTable(db: Database): void {
5660
+ db.run(`
5661
+ CREATE TABLE IF NOT EXISTS auth_credential_refresh_leases (
5662
+ credential_id INTEGER PRIMARY KEY,
5663
+ owner TEXT NOT NULL,
5664
+ expires_at_ms INTEGER NOT NULL,
5665
+ updated_at INTEGER NOT NULL
5666
+ );
5667
+ CREATE INDEX IF NOT EXISTS idx_auth_credential_refresh_leases_expires ON auth_credential_refresh_leases(expires_at_ms);
5668
+ `);
5669
+ }
5670
+
5658
5671
  #initializeSchema(): void {
5659
5672
  // Install the busy handler BEFORE any lock-taking statement (incl.
5660
5673
  // `PRAGMA journal_mode=WAL`, which acquires an exclusive lock during WAL
@@ -5820,15 +5833,7 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
5820
5833
  }
5821
5834
 
5822
5835
  #createAuthCredentialRefreshLeasesTable(): void {
5823
- this.#db.run(`
5824
- CREATE TABLE IF NOT EXISTS auth_credential_refresh_leases (
5825
- credential_id INTEGER PRIMARY KEY,
5826
- owner TEXT NOT NULL,
5827
- expires_at_ms INTEGER NOT NULL,
5828
- updated_at INTEGER NOT NULL
5829
- );
5830
- CREATE INDEX IF NOT EXISTS idx_auth_credential_refresh_leases_expires ON auth_credential_refresh_leases(expires_at_ms);
5831
- `);
5836
+ SqliteAuthCredentialStore.#ensureAuthCredentialRefreshLeasesTable(this.#db);
5832
5837
  }
5833
5838
 
5834
5839
  #migrateAuthSchema(fromVersion: number): void {
@@ -568,6 +568,10 @@ function responseStatusForStopReason(message: AssistantMessage): ResponseStatus
568
568
  return "completed";
569
569
  }
570
570
 
571
+ function incompleteDetailsForStatus(status: ResponseStatus): { reason: "max_output_tokens" } | null {
572
+ return status === "incomplete" ? { reason: "max_output_tokens" } : null;
573
+ }
574
+
571
575
  function buildReasoningItem(part: ThinkingContent): ReasoningOutputItem {
572
576
  const baseId = part.itemId ?? makeReasoningId();
573
577
  if (part.thinkingSignature) {
@@ -718,7 +722,7 @@ function buildResponseEnvelope(
718
722
  model: requestedModelId,
719
723
  output: items,
720
724
  usage,
721
- ...(status === "incomplete" ? { incomplete_details: { reason: "max_output_tokens" } } : {}),
725
+ incomplete_details: incompleteDetailsForStatus(status),
722
726
  ...(status === "failed" ? { error: { message: message.errorMessage ?? "response failed" } } : {}),
723
727
  };
724
728
  }
@@ -812,6 +816,7 @@ export function encodeStream(
812
816
  model: requestedModelId,
813
817
  output,
814
818
  usage: null,
819
+ incomplete_details: incompleteDetailsForStatus(status),
815
820
  });
816
821
 
817
822
  const openMessage = (signature?: MessageSignature): OpenMessage => {
@@ -1242,7 +1247,7 @@ export function encodeStream(
1242
1247
  model: requestedModelId,
1243
1248
  output: items,
1244
1249
  usage,
1245
- ...(status === "incomplete" ? { incomplete_details: { reason: "max_output_tokens" } } : {}),
1250
+ incomplete_details: incompleteDetailsForStatus(status),
1246
1251
  ...(status === "failed"
1247
1252
  ? { error: { message: message?.errorMessage ?? "response failed" } }
1248
1253
  : {}),
@@ -1267,6 +1272,7 @@ export function encodeStream(
1267
1272
  model: requestedModelId,
1268
1273
  output: [],
1269
1274
  error: { message: err instanceof Error ? err.message : String(err) },
1275
+ incomplete_details: null,
1270
1276
  },
1271
1277
  }),
1272
1278
  ),
@@ -1720,12 +1720,19 @@ export function appendResponsesToolResultMessages<TApi extends Api>(
1720
1720
  const hasImages = toolResult.content.some((block): block is ImageContent => block.type === "image");
1721
1721
  const omittedImages = hasImages && !supportsImages;
1722
1722
  const normalized = normalizeResponsesToolCallId(toolResult.toolCallId);
1723
+ // "(see attached image)" is only truthful when the result actually carries
1724
+ // images (they ride as a separate user message on the Responses API). A
1725
+ // genuinely empty text result (empty file read, silent tool) must stay
1726
+ // empty — the placeholder sent models chasing an attachment that never
1727
+ // existed.
1723
1728
  const output = (
1724
1729
  omittedImages
1725
1730
  ? joinTextWithImagePlaceholder(textResult, true)
1726
1731
  : textResult.length > 0
1727
1732
  ? textResult
1728
- : "(see attached image)"
1733
+ : hasImages
1734
+ ? "(see attached image)"
1735
+ : ""
1729
1736
  ).toWellFormed();
1730
1737
  if (strictResponsesPairing && !knownCallIds.has(normalized.callId)) {
1731
1738
  // Strict backends (Azure, Copilot) reject unpaired outputs outright, but
@@ -547,7 +547,11 @@ function collapseMixedTypeCombinerVariants(schema: JsonObject, combiner: "anyOf"
547
547
 
548
548
  const existingValue = mergedVariantFields[key];
549
549
  if (existingValue !== undefined && !areJsonValuesEqual(existingValue, variantValue)) {
550
- return schema;
550
+ if (key !== "description") return schema;
551
+ // Descriptions are annotations, so merge branch-local spill text instead of
552
+ // treating it as a structural incompatibility.
553
+ mergedVariantFields[key] = mergeSchemaDescriptions(existingValue, variantValue);
554
+ continue;
551
555
  }
552
556
  mergedVariantFields[key] = variantValue;
553
557
  }
@@ -588,7 +592,9 @@ function collapseMixedTypeCombinerVariants(schema: JsonObject, combiner: "anyOf"
588
592
  const value = mergedVariantFields[key];
589
593
  const existingValue = nextSchema[key];
590
594
  if (existingValue !== undefined && !areJsonValuesEqual(existingValue, value)) {
591
- return schema;
595
+ if (key !== "description") return schema;
596
+ nextSchema[key] = mergeSchemaDescriptions(existingValue, value);
597
+ continue;
592
598
  }
593
599
  if (existingValue === undefined) {
594
600
  nextSchema[key] = value;
@@ -597,6 +603,13 @@ function collapseMixedTypeCombinerVariants(schema: JsonObject, combiner: "anyOf"
597
603
  return nextSchema;
598
604
  }
599
605
 
606
+ function mergeSchemaDescriptions(existing: unknown, incoming: unknown): string {
607
+ if (typeof existing !== "string") return typeof incoming === "string" ? incoming : "";
608
+ if (typeof incoming !== "string" || incoming.length === 0 || existing === incoming) return existing;
609
+ if (existing.length === 0) return incoming;
610
+ return `${existing}\n\n${incoming}`;
611
+ }
612
+
600
613
  function collapseSameTypeCombinerVariants(schema: JsonObject, combiner: "anyOf" | "oneOf"): JsonObject {
601
614
  const variantsRaw = schema[combiner];
602
615
  if (!Array.isArray(variantsRaw) || variantsRaw.length === 0) return schema;