@oh-my-pi/pi-ai 16.2.4 → 16.2.6

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,18 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [16.2.6] - 2026-06-29
6
+
7
+ ### Fixed
8
+
9
+ - Fixed Antigravity usage reporting to correctly infer daily and weekly quota windows from unlabeled reset-only rows, preventing Cloud Code Assist payloads from collapsing these counters into the default category.
10
+
11
+ ## [16.2.5] - 2026-06-28
12
+
13
+ ### Fixed
14
+
15
+ - Fixed Google and Cloud Code Assist streams that end without a finish reason (dropped connections or truncated responses) being treated as fatal; they are now classified as transient so the coding agent automatically retries.
16
+
5
17
  ## [16.2.4] - 2026-06-28
6
18
 
7
19
  ### 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": "16.2.4",
4
+ "version": "16.2.6",
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.2.4",
42
- "@oh-my-pi/pi-utils": "16.2.4",
43
- "@oh-my-pi/pi-wire": "16.2.4",
41
+ "@oh-my-pi/pi-catalog": "16.2.6",
42
+ "@oh-my-pi/pi-utils": "16.2.6",
43
+ "@oh-my-pi/pi-wire": "16.2.6",
44
44
  "arktype": "^2.2.0",
45
45
  "zod": "^4"
46
46
  },
@@ -36,7 +36,13 @@ export class ProviderResponseError extends Error {
36
36
  this.name = "ProviderResponseError";
37
37
  this.provider = options.provider;
38
38
  this.kind = options.kind ?? "output";
39
+ // A safety filter block is a terminal provider finish, not a transient fault.
39
40
  if (this.kind === "content-blocked") attach(this, create(Flag.ProviderFinishError));
41
+ // An incomplete stream (connection dropped / truncated before any terminal
42
+ // event) never produced a finish reason — the request didn't complete, so it
43
+ // is safe to retry. The retry layer's replay-unsafe guard still blocks a
44
+ // retry when partial tool output was already emitted.
45
+ else if (this.kind === "incomplete-stream") attach(this, create(Flag.Transient));
40
46
  }
41
47
  }
42
48
 
@@ -2151,14 +2151,29 @@ function recordCodexWebSocketFailure(state: CodexWebSocketSessionState, activate
2151
2151
  }
2152
2152
  }
2153
2153
 
2154
+ function getCodexWebSocketEnvValue(): boolean | undefined {
2155
+ const envVal = $env.PI_CODEX_WEBSOCKET;
2156
+ if (envVal !== undefined) {
2157
+ return $flag("PI_CODEX_WEBSOCKET");
2158
+ }
2159
+ return undefined;
2160
+ }
2161
+
2154
2162
  function shouldUseCodexWebSocket(
2155
2163
  model: Model<"openai-codex-responses">,
2156
2164
  state: CodexWebSocketSessionState | undefined,
2157
2165
  preferWebsockets?: boolean,
2158
2166
  ): boolean {
2167
+ // Explicitly disabled by the model or session state.
2168
+ if (model.preferWebsockets === false) return false;
2169
+ // Explicitly disabled by the session state.
2159
2170
  if (!state || state.disableWebsocket) return false;
2171
+ // Env val > Preference
2172
+ const envVal = getCodexWebSocketEnvValue();
2173
+ if (envVal !== undefined) return envVal;
2174
+ // Negative preference overrides model preference; otherwise use the model's preference.
2160
2175
  if (preferWebsockets === false) return false;
2161
- return $flag("PI_CODEX_WEBSOCKET") || preferWebsockets === true || model.preferWebsockets === true;
2176
+ return true;
2162
2177
  }
2163
2178
 
2164
2179
  export interface OpenAICodexTransportDetails {
@@ -2214,10 +2229,13 @@ export function getOpenAICodexTransportDetails(
2214
2229
  providerSessionState?: Map<string, ProviderSessionState>;
2215
2230
  },
2216
2231
  ): OpenAICodexTransportDetails {
2232
+ const envVal = getCodexWebSocketEnvValue();
2217
2233
  const websocketPreferred =
2218
- options?.preferWebsockets === false
2219
- ? false
2220
- : $flag("PI_CODEX_WEBSOCKET") || options?.preferWebsockets === true || model.preferWebsockets === true;
2234
+ envVal !== undefined
2235
+ ? envVal
2236
+ : options?.preferWebsockets === false
2237
+ ? false
2238
+ : options?.preferWebsockets === true || model.preferWebsockets === true;
2221
2239
  const state = getCodexWebSocketStateForPublicSession(model, options);
2222
2240
 
2223
2241
  return {
package/src/stream.ts CHANGED
@@ -367,14 +367,17 @@ async function tryAcquireProviderInFlightLease(
367
367
  }
368
368
  }
369
369
 
370
- async function signalProviderInFlightWaiters(provider: string): Promise<void> {
370
+ async function signalProviderInFlightWaitersInDir(dir: string): Promise<void> {
371
371
  try {
372
- const dir = providerInFlightDir(provider);
373
372
  await fs.mkdir(dir, { recursive: true });
374
- await Bun.write(providerInFlightSignalPath(provider), String(Date.now()));
373
+ await Bun.write(path.join(dir, ".wakeup"), String(Date.now()));
375
374
  } catch {}
376
375
  }
377
376
 
377
+ async function signalProviderInFlightWaiters(provider: string): Promise<void> {
378
+ await signalProviderInFlightWaitersInDir(providerInFlightDir(provider));
379
+ }
380
+
378
381
  function waitForProviderInFlightSignal(provider: string, signal?: AbortSignal): Promise<void> {
379
382
  if (signal?.aborted)
380
383
  return Promise.reject(signal.reason ?? new AIError.AbortError("Provider request aborted before dispatch"));
@@ -434,11 +437,15 @@ async function removeProviderInFlightLeaseDir(leasePath: string): Promise<void>
434
437
  }
435
438
  }
436
439
 
437
- async function releaseProviderInFlightLease(provider: string, lease: ProviderInFlightLease): Promise<void> {
440
+ // Signal into the lease's OWN provider directory (derived from `lease.path`)
441
+ // rather than recomputing it from the current root. A release that lands after
442
+ // the in-flight root has been repointed (only the test seam does that) must not
443
+ // write `.wakeup` into an unrelated provider directory.
444
+ async function releaseProviderInFlightLease(lease: ProviderInFlightLease): Promise<void> {
438
445
  clearInterval(lease.heartbeat);
439
446
  await lease.flushHeartbeat();
440
447
  await removeProviderInFlightLeaseDir(lease.path);
441
- await signalProviderInFlightWaiters(provider);
448
+ await signalProviderInFlightWaitersInDir(path.dirname(lease.path));
442
449
  }
443
450
 
444
451
  async function acquireProviderInFlightSlot(
@@ -451,7 +458,7 @@ async function acquireProviderInFlightSlot(
451
458
  while (true) {
452
459
  if (signal?.aborted) throw signal.reason ?? new AIError.AbortError("Provider request aborted before dispatch");
453
460
  const lease = await tryAcquireProviderInFlightLease(provider, limit, signal);
454
- if (lease) return () => releaseProviderInFlightLease(provider, lease);
461
+ if (lease) return () => releaseProviderInFlightLease(lease);
455
462
  if (!loggedWait) {
456
463
  loggedWait = true;
457
464
  logger.debug("Provider in-flight limit blocked request", { provider, limit });
@@ -68,6 +68,57 @@ function classifyWindow(id: string | undefined, label: string | undefined): Anti
68
68
  return undefined;
69
69
  }
70
70
 
71
+ function parseResetTime(info: AntigravityQuotaInfo): number | undefined {
72
+ const resetAt = info.resetTime ? Date.parse(info.resetTime) : undefined;
73
+ return resetAt !== undefined && Number.isFinite(resetAt) ? resetAt : undefined;
74
+ }
75
+
76
+ function inferWindowFromReset(resetAt: number | undefined, nowMs: number): AntigravityWindowDescriptor {
77
+ if (resetAt !== undefined && resetAt - nowMs > ONE_DAY_MS) {
78
+ return { id: "weekly", label: "Weekly", durationMs: ONE_WEEK_MS };
79
+ }
80
+ return { id: "daily", label: "Daily", durationMs: ONE_DAY_MS };
81
+ }
82
+
83
+ function quotaInferenceKey(info: AntigravityQuotaInfo): string {
84
+ return [info.modelProvider ?? "", info.apiProvider ?? "", info.tier ?? ""].join("|");
85
+ }
86
+
87
+ function inferWindowDescriptors(
88
+ quotaInfos: AntigravityQuotaInfo[],
89
+ nowMs: number,
90
+ ): WeakMap<AntigravityQuotaInfo, AntigravityWindowDescriptor> {
91
+ const descriptors = new WeakMap<AntigravityQuotaInfo, AntigravityWindowDescriptor>();
92
+ const groups = new Map<string, { info: AntigravityQuotaInfo; resetAt: number | undefined }[]>();
93
+
94
+ for (const info of quotaInfos) {
95
+ const explicitDescriptor = classifyWindow(info.windowId, info.windowLabel);
96
+ if (explicitDescriptor) {
97
+ descriptors.set(info, explicitDescriptor);
98
+ continue;
99
+ }
100
+ const group = groups.get(quotaInferenceKey(info)) ?? [];
101
+ group.push({ info, resetAt: parseResetTime(info) });
102
+ groups.set(quotaInferenceKey(info), group);
103
+ }
104
+
105
+ for (const group of groups.values()) {
106
+ const resetTimes = [...new Set(group.map(entry => entry.resetAt).filter(resetAt => resetAt !== undefined))].sort(
107
+ (a, b) => a - b,
108
+ );
109
+ const latestReset = resetTimes.length > 1 ? resetTimes.at(-1) : undefined;
110
+ for (const entry of group) {
111
+ const descriptor =
112
+ latestReset !== undefined && entry.resetAt === latestReset
113
+ ? { id: "weekly", label: "Weekly", durationMs: ONE_WEEK_MS }
114
+ : inferWindowFromReset(entry.resetAt, nowMs);
115
+ descriptors.set(entry.info, descriptor);
116
+ }
117
+ }
118
+
119
+ return descriptors;
120
+ }
121
+
71
122
  function withWindowDescriptor(
72
123
  info: AntigravityQuotaInfo,
73
124
  descriptor: AntigravityWindowDescriptor | undefined,
@@ -94,10 +145,12 @@ function getUsageStatus(remainingFraction: number | undefined): UsageStatus | un
94
145
  return "ok";
95
146
  }
96
147
 
97
- function parseWindow(info: AntigravityQuotaInfo): UsageWindow | undefined {
98
- const descriptor = classifyWindow(info.windowId, info.windowLabel);
99
- const resetAt = info.resetTime ? Date.parse(info.resetTime) : undefined;
100
- const hasResetAt = resetAt !== undefined && Number.isFinite(resetAt);
148
+ function parseWindow(
149
+ info: AntigravityQuotaInfo,
150
+ descriptor: AntigravityWindowDescriptor | undefined,
151
+ ): UsageWindow | undefined {
152
+ const resetAt = parseResetTime(info);
153
+ const hasResetAt = resetAt !== undefined;
101
154
  if (!descriptor && !hasResetAt) return undefined;
102
155
  return {
103
156
  id: descriptor?.id ?? info.windowId ?? "default",
@@ -276,9 +329,10 @@ async function fetchAntigravityUsage(params: UsageFetchParams, ctx: UsageFetchCo
276
329
 
277
330
  for (const [_modelId, modelInfo] of Object.entries(data.models ?? {})) {
278
331
  const quotaInfos = normalizeQuotaInfos(modelInfo);
332
+ const inferredDescriptors = inferWindowDescriptors(quotaInfos, nowMs);
279
333
  for (const quotaInfo of quotaInfos) {
280
334
  const amount = buildAmount(quotaInfo);
281
- const window = parseWindow(quotaInfo);
335
+ const window = parseWindow(quotaInfo, inferredDescriptors.get(quotaInfo));
282
336
  if (window?.resetsAt) {
283
337
  earliestReset = earliestReset ? Math.min(earliestReset, window.resetsAt) : window.resetsAt;
284
338
  }