@oh-my-pi/pi-ai 17.2.2 → 17.2.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,28 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [17.2.4] - 2026-08-01
6
+
7
+ ### Fixed
8
+
9
+ - Fixed Codex WebSocket tool-result turns replaying full history when the preceding tool-call ID required Responses API normalization ([#7279](https://github.com/can1357/oh-my-pi/issues/7279)).
10
+ - Fixed direct Anthropic provider streams ignoring `model.compat.streamIdleTimeoutMs`. Requests dispatched through `streamAnthropic` can now widen the inter-event idle watchdog or set it to `0` to disable that watchdog; caller options and environment overrides retain precedence. Setting the compat value to `0` disables only the inter-event watchdog and leaves the first-event watchdog enabled; wider idle values continue to floor the first-event budget under the existing timeout contract.
11
+ - Fixed OpenRouter DeepSeek models failing structured subagents when the upstream returns an opaque HTTP 400 for a strict yield schema, retrying once without strict tools and remembering the fallback for the provider session ([#7264](https://github.com/can1357/oh-my-pi/issues/7264)).
12
+ - Fixed provider-native Codex compaction streams bypassing WebSocket-first transport selection and SSE transport fallback ([#7198](https://github.com/can1357/oh-my-pi/issues/7198)).
13
+ - Fixed `SqliteAuthCredentialStore.open()` running the `auth_credential_refresh_leases` DDL (`CREATE TABLE`/`CREATE INDEX`) with Bun's default `busy_timeout=0`, before the constructor's `#initializeSchema()` installed the busy handler. Under a concurrent write lock (e.g. WAL recovery on parallel omp startups) the lock-taking DDL failed immediately and, since the error wasn't BUSY-classified, bypassed `open()`'s bounded retry loop. The busy handler is now installed on the connection immediately after it opens, before any lock-taking statement, honoring the issue-#2421 invariant on every entry path. ([#7298](https://github.com/can1357/oh-my-pi/issues/7298))
14
+ - Fixed a corrupt credential store (`agent.db`) silently disabling every persisted rate-limit block. `AuthStorage` caught unrecoverable SQLite errors (`SQLITE_CORRUPT` family / `SQLITE_NOTADB`) from the persisted block read/write paths at `debug` level with no latch, so the broken store was re-queried on every credential evaluation while blocks quietly stopped applying. The first unrecoverable error is now reported once at `error` level with the store location and repair guidance, and every later persisted-block read/write short-circuits for the process lifetime; in-memory backoff still preserves availability ([#7296](https://github.com/can1357/oh-my-pi/issues/7296)).
15
+
16
+ ## [17.2.3] - 2026-08-01
17
+
18
+ ### Added
19
+
20
+ - Added the ai& (`aiand`) provider registry entry with API-key paste login validated against `https://api.aiand.com/v1/models`.
21
+
22
+ ### Fixed
23
+
24
+ - Fixed Anthropic OAuth (Claude Pro/Max subscription) requests hard-429ing (`Usage credits are required for long context requests`) on every beta-gated 1M model — e.g. `claude-sonnet-4-6`, which the default `task`/`smol`/`scout` subagent roles resolve to — regardless of prompt size, breaking all subagents. The 17.2.1 cowork request profile reintroduced the `context-1m-2025-08-07` beta for any model with a 1M catalog window, but subscription credentials have no long-context credit balance so Anthropic rejects the request outright. The beta is no longer advertised on OAuth requests; subscription accounts transparently get the standard 200k window. ([#7238](https://github.com/can1357/oh-my-pi/issues/7238))
25
+ - Fixed OpenAI Codex Responses ignoring disabled cache retention when deriving `prompt_cache_key`, while preserving transport session identity ([#7219](https://github.com/can1357/oh-my-pi/issues/7219)).
26
+
5
27
  ## [17.2.2] - 2026-07-31
6
28
 
7
29
  ### Added
@@ -1271,6 +1271,14 @@ export declare class AuthStorage {
1271
1271
  * and `SQLITE_BUSY_TIMEOUT`. All warrant the same backoff-and-retry treatment.
1272
1272
  */
1273
1273
  export declare function isSqliteBusyError(err: unknown): boolean;
1274
+ /**
1275
+ * SQLite's unrecoverable-corruption result codes — the `SQLITE_CORRUPT` family
1276
+ * (base plus extended variants like `SQLITE_CORRUPT_VTAB` / `SQLITE_CORRUPT_INDEX`)
1277
+ * and `SQLITE_NOTADB` (the file header is not a database). Unlike
1278
+ * {@link isSqliteBusyError}, these never clear by retrying: the store must be
1279
+ * repaired or replaced, so callers latch and stop touching it.
1280
+ */
1281
+ export declare function isSqliteCorruptionError(err: unknown): boolean;
1274
1282
  /**
1275
1283
  * Default SQLite-backed implementation of {@link AuthCredentialStore}.
1276
1284
  *
@@ -1,5 +1,5 @@
1
1
  import type { CodexCompactionContext, CodexCompactionRequestContext, Context, Model, ProviderSessionState, ServiceTier, StreamFunction, StreamOptions, Tool, ToolChoice } from "../types.js";
2
- import { type CodexReasoningContext, type RequestBody } from "./openai-codex/request-transformer.js";
2
+ import { type CodexLiteShapedBody, type CodexReasoningContext, type RequestBody } from "./openai-codex/request-transformer.js";
3
3
  import type { ResponseInput } from "./openai-responses-wire.js";
4
4
  export interface OpenAICodexResponsesOptions extends StreamOptions {
5
5
  reasoning?: "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
@@ -43,6 +43,15 @@ export interface OpenAICodexResponsesOptions extends StreamOptions {
43
43
  */
44
44
  onModerationMetadata?: (metadata: unknown) => void;
45
45
  }
46
+ /** Raw V2 compaction body accepted by the Codex transport selector. */
47
+ export interface OpenAICodexCompactionBody extends CodexLiteShapedBody {
48
+ model: string;
49
+ [key: string]: unknown;
50
+ }
51
+ /** Transport controls for a provider-native Codex V2 compaction stream. */
52
+ export interface OpenAICodexCompactionStreamOptions extends OpenAICodexResponsesOptions {
53
+ apiKey: string;
54
+ }
46
55
  /** Inputs for synthesizing Codex request identity outside the normal stream path. */
47
56
  export interface OpenAICodexCompatibilityMetadataOptions {
48
57
  sessionId?: string;
@@ -161,6 +170,11 @@ export declare function resetOpenAICodexHistoryAfterCompaction(options: OpenAICo
161
170
  export declare function normalizeCodexToolChoice(choice: ToolChoice | undefined, tools?: Tool[], model?: Model<"openai-codex-responses">): string | Record<string, unknown> | undefined;
162
171
  /** @internal Exported for tests. */
163
172
  export declare function buildTransformedCodexRequestBody(model: Model<"openai-codex-responses">, context: Context, options: OpenAICodexResponsesOptions | undefined, promptCacheKey?: string | undefined): Promise<RequestBody>;
173
+ /**
174
+ * Open a provider-native V2 compaction stream through Codex's WebSocket-first
175
+ * transport, replaying WebSocket transport failures over SSE.
176
+ */
177
+ export declare function openCodexCompactionEventStream(model: Model<"openai-codex-responses">, body: OpenAICodexCompactionBody, options: OpenAICodexCompactionStreamOptions): Promise<AsyncGenerator<Record<string, unknown>>>;
164
178
  export declare const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses">;
165
179
  export declare function prewarmOpenAICodexResponses(model: Model<"openai-codex-responses">, options?: Pick<OpenAICodexResponsesOptions, "apiKey" | "headers" | "sessionId" | "signal" | "preferWebsockets" | "providerSessionState" | "responsesLite">): Promise<void>;
166
180
  export interface OpenAICodexTransportDetails {
@@ -343,7 +343,13 @@ export declare function resolveOpenAIResponsesOutputClamp(model: Pick<Model, "pr
343
343
  */
344
344
  export declare function applyChatCompletionsToolStream(params: OpenAICompletionsParams, model: Model<"openai-completions">, compat: ResolvedOpenAICompat): void;
345
345
  export declare function isCompiledGrammarTooLargeStrictError(error: unknown, capturedErrorResponse: CapturedHttpErrorResponse | undefined): boolean;
346
- export declare function shouldRetryWithoutStrictTools(error: unknown, capturedErrorResponse: CapturedHttpErrorResponse | undefined, strictToolsApplied: boolean, tools: Tool[] | undefined): boolean;
346
+ interface StrictToolsRetryContext {
347
+ model: OpenAIModelIdentity;
348
+ strictToolsApplied: boolean;
349
+ tools: Tool[] | undefined;
350
+ }
351
+ /** Decide whether an OpenAI-family request should retry once with non-strict tools. */
352
+ export declare function shouldRetryWithoutStrictTools(error: unknown, capturedErrorResponse: CapturedHttpErrorResponse | undefined, context: StrictToolsRetryContext): boolean;
347
353
  export declare const OPENAI_RESPONSES_PROGRESS_EVENT_TYPES: ReadonlySet<string>;
348
354
  export declare function isOpenAIResponsesProgressEvent(event: unknown): boolean;
349
355
  export declare function encodeTextSignatureV1(id: string, phase?: TextSignatureV1["phase"]): string;
@@ -0,0 +1,7 @@
1
+ import type { OAuthLoginCallbacks } from "./oauth/types.js";
2
+ export declare const loginAiand: (options: import("./oauth/index.js").OAuthController) => Promise<string>;
3
+ export declare const aiandProvider: {
4
+ readonly id: "aiand";
5
+ readonly name: "ai&";
6
+ readonly login: (cb: OAuthLoginCallbacks) => Promise<string>;
7
+ };
@@ -7,6 +7,10 @@ import type { ProviderDefinition } from "./types.js";
7
7
  * list for the loginable providers; non-login model providers are appended.
8
8
  */
9
9
  declare const ALL: ({
10
+ readonly id: "aiand";
11
+ readonly name: "ai&";
12
+ readonly login: (cb: import("./oauth/index.js").OAuthLoginCallbacks) => Promise<string>;
13
+ } | {
10
14
  readonly id: "aimlapi";
11
15
  readonly name: "AIML API";
12
16
  } | {
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.2.2",
4
+ "version": "17.2.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.1",
41
- "@oh-my-pi/pi-catalog": "17.2.2",
42
- "@oh-my-pi/pi-utils": "17.2.2",
43
- "@oh-my-pi/pi-wire": "17.2.2",
41
+ "@oh-my-pi/pi-catalog": "17.2.4",
42
+ "@oh-my-pi/pi-utils": "17.2.4",
43
+ "@oh-my-pi/pi-wire": "17.2.4",
44
44
  "arktype": "2.2.3",
45
45
  "zod": "^4"
46
46
  },
@@ -12,7 +12,7 @@ import { createHash } from "node:crypto";
12
12
  import * as fs from "node:fs/promises";
13
13
  import * as path from "node:path";
14
14
  import { parseAlibabaTokenPlanCredential } from "@oh-my-pi/pi-catalog/wire/alibaba-token-plan";
15
- import { $env, getAgentDbPath, logger } from "@oh-my-pi/pi-utils";
15
+ import { $env, getAgentDbPath, getDbBusyTimeoutMs, logger } from "@oh-my-pi/pi-utils";
16
16
  import type { ApiKeyResolver } from "./auth-retry";
17
17
  import * as AIError from "./error";
18
18
  import { isUsageLimitOutcome } from "./error/rate-limit";
@@ -1264,6 +1264,15 @@ export class AuthStorage {
1264
1264
  #credentialBackoff: Map<string, Map<number, number>> = new Map();
1265
1265
  /** Earliest time a freshly-set in-memory block may be cleared by live usage reconciliation. */
1266
1266
  #credentialBackoffProbeAfter: Map<string, Map<number, number>> = new Map();
1267
+ /**
1268
+ * Latched true once the persistent credential-block store reports an
1269
+ * unrecoverable error (SQLite corruption / not-a-database). While set, every
1270
+ * persisted-block read and write short-circuits for the life of the process:
1271
+ * availability is preserved through {@link AuthStorage.#credentialBackoff}, but
1272
+ * cross-process persistence is abandoned rather than re-querying a broken store
1273
+ * on every credential evaluation.
1274
+ */
1275
+ #persistedBlockStoreDamaged = false;
1267
1276
  #usageProviderResolver?: (provider: Provider) => UsageProvider | undefined;
1268
1277
  #rankingStrategyResolver?: (provider: Provider) => CredentialRankingStrategy | undefined;
1269
1278
  #usageCache: UsageCache;
@@ -1312,8 +1321,10 @@ export class AuthStorage {
1312
1321
  }
1313
1322
  try {
1314
1323
  this.#store.cleanExpiredCredentialBlocks?.(Date.now());
1315
- } catch {
1316
- // Best-effort.
1324
+ } catch (err) {
1325
+ // Best-effort, but init-time corruption must latch the block store
1326
+ // immediately so the first evaluation doesn't re-query a broken DB.
1327
+ this.#handlePersistedBlockStoreError(err);
1317
1328
  }
1318
1329
  this.#usageFetch = options.usageFetch ?? fetch;
1319
1330
  this.#usageRequestTimeoutMs = options.usageRequestTimeoutMs ?? DEFAULT_USAGE_REQUEST_TIMEOUT_MS;
@@ -1483,7 +1494,16 @@ export class AuthStorage {
1483
1494
  * Reload credentials from storage.
1484
1495
  */
1485
1496
  async reload(): Promise<void> {
1486
- const records = this.#store.listAuthCredentials();
1497
+ let records: StoredAuthCredential[];
1498
+ try {
1499
+ records = this.#store.listAuthCredentials();
1500
+ } catch (err) {
1501
+ // Latch + surface repair guidance on corruption, but still fail the
1502
+ // reload: silently continuing with zero credentials would log the
1503
+ // user out of every provider without explanation.
1504
+ this.#handlePersistedBlockStoreError(err);
1505
+ throw err;
1506
+ }
1487
1507
  const grouped = new Map<string, StoredCredential[]>();
1488
1508
  for (const record of records) {
1489
1509
  const list = grouped.get(record.provider) ?? [];
@@ -1701,11 +1721,13 @@ export class AuthStorage {
1701
1721
  providerKey: string,
1702
1722
  blockScope: string | undefined,
1703
1723
  ): number | undefined {
1724
+ if (this.#persistedBlockStoreDamaged) return undefined;
1704
1725
  const getCredentialBlock = this.#store.getCredentialBlock?.bind(this.#store);
1705
1726
  if (!getCredentialBlock) return undefined;
1706
1727
  try {
1707
1728
  return getCredentialBlock(credentialId, providerKey, blockScope ?? "");
1708
1729
  } catch (err) {
1730
+ if (this.#handlePersistedBlockStoreError(err)) return undefined;
1709
1731
  logger.debug("Failed to read credential block from persistent store", {
1710
1732
  err,
1711
1733
  credentialId,
@@ -1716,6 +1738,26 @@ export class AuthStorage {
1716
1738
  }
1717
1739
  }
1718
1740
 
1741
+ #readPersistedCredentialBlockReconcileAfter(credentialId: number, providerKey: string, blockScope: string): number {
1742
+ if (this.#persistedBlockStoreDamaged) return 0;
1743
+ const getCredentialBlockReconcileAfter = this.#store.getCredentialBlockReconcileAfter?.bind(this.#store);
1744
+ if (!getCredentialBlockReconcileAfter) return 0;
1745
+ try {
1746
+ return getCredentialBlockReconcileAfter(credentialId, providerKey, blockScope) ?? 0;
1747
+ } catch (err) {
1748
+ if (this.#handlePersistedBlockStoreError(err)) return 0;
1749
+ // Advisory read: transient failures (e.g. SQLITE_BUSY) fall back to
1750
+ // the in-memory probe window, mirroring #readPersistedCredentialBlock.
1751
+ logger.debug("Failed to read credential block reconcile-after time from persistent store", {
1752
+ err,
1753
+ credentialId,
1754
+ providerKey,
1755
+ blockScope,
1756
+ });
1757
+ return 0;
1758
+ }
1759
+ }
1760
+
1719
1761
  /** Returns block expiry timestamp for a credential, checking unscoped and scoped blocks. */
1720
1762
  #getCredentialBlockedUntil(
1721
1763
  provider: string,
@@ -1792,7 +1834,7 @@ export class AuthStorage {
1792
1834
  this.#invalidateUsageReportCache(provider);
1793
1835
 
1794
1836
  const upsertCredentialBlock = this.#store.upsertCredentialBlock?.bind(this.#store);
1795
- if (!upsertCredentialBlock) return;
1837
+ if (!upsertCredentialBlock || this.#persistedBlockStoreDamaged) return;
1796
1838
  const credentialId = this.#getStoredCredentials(provider)[credentialIndex]?.id;
1797
1839
  if (credentialId === undefined) return;
1798
1840
  try {
@@ -1803,6 +1845,7 @@ export class AuthStorage {
1803
1845
  blockedUntilMs: nextBlockedUntil,
1804
1846
  });
1805
1847
  } catch (err) {
1848
+ if (this.#handlePersistedBlockStoreError(err)) return;
1806
1849
  logger.debug("Failed to persist credential block", {
1807
1850
  err,
1808
1851
  credentialId,
@@ -1814,6 +1857,36 @@ export class AuthStorage {
1814
1857
  }
1815
1858
  }
1816
1859
 
1860
+ #handlePersistedBlockStoreError(err: unknown): boolean {
1861
+ if (!isSqliteCorruptionError(err)) return false;
1862
+ this.#reportDamagedBlockStore(err);
1863
+ return true;
1864
+ }
1865
+
1866
+ #assertPersistedBlockStoreWritable(): void {
1867
+ if (!this.#persistedBlockStoreDamaged) return;
1868
+ const store = this.#sourceLabel ?? `local ${getAgentDbPath()}`;
1869
+ throw new Error(`Persistent credential block store ${store} is unavailable after SQLite corruption`);
1870
+ }
1871
+
1872
+ /**
1873
+ * Latches {@link AuthStorage.#persistedBlockStoreDamaged} on the first
1874
+ * unrecoverable persisted-block store error and surfaces it once at `error`
1875
+ * level with the store location, so an operator can repair or replace it.
1876
+ * Later reads/writes short-circuit silently — the in-memory backoff map keeps
1877
+ * rate-limit blocks applying for the life of the process; only cross-process
1878
+ * persistence is lost.
1879
+ */
1880
+ #reportDamagedBlockStore(err: unknown): void {
1881
+ if (this.#persistedBlockStoreDamaged) return;
1882
+ this.#persistedBlockStoreDamaged = true;
1883
+ const store = this.#sourceLabel ?? `local ${getAgentDbPath()}`;
1884
+ logger.error(
1885
+ "Persistent credential store is corrupt; cross-process rate-limit persistence is disabled for this process. In-memory backoff still applies. Repair the store with `sqlite3 <path> '.recover'` or delete it to recreate on next login.",
1886
+ { err, store },
1887
+ );
1888
+ }
1889
+
1817
1890
  /**
1818
1891
  * Records which credential was used for a session (for rate-limit switching).
1819
1892
  * `lastUsedAtMs` backdates the sticky (session-file pin restores on resume);
@@ -5862,9 +5935,12 @@ export class AuthStorage {
5862
5935
  const scopedBackoffKey = this.#toScopedBackoffKey(providerKey, blockScope);
5863
5936
  const globalProbeAfterMs = this.#credentialBackoffProbeAfter.get(providerKey)?.get(credentialIndex) ?? 0;
5864
5937
  const scopedProbeAfterMs = this.#credentialBackoffProbeAfter.get(scopedBackoffKey)?.get(credentialIndex) ?? 0;
5865
- const getStoreReconcileAfter = this.#store.getCredentialBlockReconcileAfter?.bind(this.#store);
5866
- const storeGlobalProbeAfterMs = getStoreReconcileAfter?.(credentialId, providerKey, "") ?? 0;
5867
- const storeScopedProbeAfterMs = getStoreReconcileAfter?.(credentialId, providerKey, blockScope ?? "") ?? 0;
5938
+ const storeGlobalProbeAfterMs = this.#readPersistedCredentialBlockReconcileAfter(credentialId, providerKey, "");
5939
+ const storeScopedProbeAfterMs = this.#readPersistedCredentialBlockReconcileAfter(
5940
+ credentialId,
5941
+ providerKey,
5942
+ blockScope ?? "",
5943
+ );
5868
5944
  if (Math.max(globalProbeAfterMs, scopedProbeAfterMs, storeGlobalProbeAfterMs, storeScopedProbeAfterMs) > nowMs) {
5869
5945
  return;
5870
5946
  }
@@ -6340,16 +6416,30 @@ export class AuthStorage {
6340
6416
  * Broker-server seam: list non-expired persisted blocks for snapshot entries.
6341
6417
  */
6342
6418
  listCredentialBlocks(credentialIds: readonly number[]): StoredCredentialBlock[] {
6343
- return this.#store.listCredentialBlocks?.(credentialIds) ?? [];
6419
+ if (this.#persistedBlockStoreDamaged) return [];
6420
+ const listCredentialBlocks = this.#store.listCredentialBlocks?.bind(this.#store);
6421
+ if (!listCredentialBlocks) return [];
6422
+ try {
6423
+ return listCredentialBlocks(credentialIds);
6424
+ } catch (err) {
6425
+ if (this.#handlePersistedBlockStoreError(err)) return [];
6426
+ throw err;
6427
+ }
6344
6428
  }
6345
6429
 
6346
6430
  /**
6347
6431
  * Broker-server seam: persist one credential block and notify snapshot waiters.
6348
6432
  */
6349
6433
  upsertCredentialBlock(block: StoredCredentialBlock): void {
6434
+ this.#assertPersistedBlockStoreWritable();
6350
6435
  const upsertCredentialBlock = this.#store.upsertCredentialBlock?.bind(this.#store);
6351
6436
  if (!upsertCredentialBlock) return;
6352
- upsertCredentialBlock(block);
6437
+ try {
6438
+ upsertCredentialBlock(block);
6439
+ } catch (err) {
6440
+ if (this.#handlePersistedBlockStoreError(err)) this.#assertPersistedBlockStoreWritable();
6441
+ throw err;
6442
+ }
6353
6443
  this.#invalidateUsageReportCacheForProviderKey(block.providerKey);
6354
6444
  this.#bumpGeneration("credential-block");
6355
6445
  }
@@ -6358,17 +6448,29 @@ export class AuthStorage {
6358
6448
  * Broker-server seam: clear all persisted blocks for one credential and notify snapshot waiters.
6359
6449
  */
6360
6450
  deleteCredentialBlock(credentialId: number, providerKey: string, blockScope: string): void {
6451
+ this.#assertPersistedBlockStoreWritable();
6361
6452
  const deleteCredentialBlock = this.#store.deleteCredentialBlock?.bind(this.#store);
6362
6453
  if (!deleteCredentialBlock) return;
6363
- deleteCredentialBlock(credentialId, providerKey, blockScope);
6454
+ try {
6455
+ deleteCredentialBlock(credentialId, providerKey, blockScope);
6456
+ } catch (err) {
6457
+ if (this.#handlePersistedBlockStoreError(err)) this.#assertPersistedBlockStoreWritable();
6458
+ throw err;
6459
+ }
6364
6460
  this.#invalidateUsageReportCacheForProviderKey(providerKey);
6365
6461
  this.#bumpGeneration("credential-block");
6366
6462
  }
6367
6463
 
6368
6464
  deleteCredentialBlocks(credentialId: number): void {
6465
+ this.#assertPersistedBlockStoreWritable();
6369
6466
  const deleteCredentialBlocks = this.#store.deleteCredentialBlocks?.bind(this.#store);
6370
6467
  if (!deleteCredentialBlocks) return;
6371
- deleteCredentialBlocks(credentialId);
6468
+ try {
6469
+ deleteCredentialBlocks(credentialId);
6470
+ } catch (err) {
6471
+ if (this.#handlePersistedBlockStoreError(err)) this.#assertPersistedBlockStoreWritable();
6472
+ throw err;
6473
+ }
6372
6474
  this.#bumpGeneration("credential-block");
6373
6475
  }
6374
6476
 
@@ -6482,6 +6584,19 @@ export function isSqliteBusyError(err: unknown): boolean {
6482
6584
  return typeof code === "string" && code.startsWith("SQLITE_BUSY");
6483
6585
  }
6484
6586
 
6587
+ /**
6588
+ * SQLite's unrecoverable-corruption result codes — the `SQLITE_CORRUPT` family
6589
+ * (base plus extended variants like `SQLITE_CORRUPT_VTAB` / `SQLITE_CORRUPT_INDEX`)
6590
+ * and `SQLITE_NOTADB` (the file header is not a database). Unlike
6591
+ * {@link isSqliteBusyError}, these never clear by retrying: the store must be
6592
+ * repaired or replaced, so callers latch and stop touching it.
6593
+ */
6594
+ export function isSqliteCorruptionError(err: unknown): boolean {
6595
+ if (err === null || typeof err !== "object" || !("code" in err)) return false;
6596
+ const code = err.code;
6597
+ return typeof code === "string" && (code.startsWith("SQLITE_CORRUPT") || code === "SQLITE_NOTADB");
6598
+ }
6599
+
6485
6600
  function normalizeStoredAccountId(accountId: string | null | undefined): string | null {
6486
6601
  const normalized = accountId?.trim();
6487
6602
  return normalized && normalized.length > 0 ? normalized : null;
@@ -6914,6 +7029,12 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
6914
7029
  let db: Database | undefined;
6915
7030
  try {
6916
7031
  db = new Database(dbPath);
7032
+ // Install the busy handler BEFORE the first lock-taking statement
7033
+ // on this connection. The leases DDL below and the constructor's
7034
+ // schema init both acquire locks during WAL recovery; without a
7035
+ // non-zero `busy_timeout` they fail immediately with SQLITE_BUSY.
7036
+ // See issue #2421.
7037
+ SqliteAuthCredentialStore.#installBusyTimeout(db);
6917
7038
  try {
6918
7039
  await fs.chmod(dbPath, 0o600);
6919
7040
  } catch {
@@ -6950,12 +7071,25 @@ export class SqliteAuthCredentialStore implements AuthCredentialStore {
6950
7071
  `);
6951
7072
  }
6952
7073
 
7074
+ /**
7075
+ * Install the per-connection busy handler so lock-taking statements wait for
7076
+ * a contended writer instead of failing immediately (Bun defaults
7077
+ * `busy_timeout` to 0). MUST run before the first lock-taking statement on
7078
+ * the connection: concurrent omp startups race WAL recovery and the leases
7079
+ * DDL. Uses the centralized timeout so headless hosts keep their bounded
7080
+ * busy wait instead of the interactive 5s value. See issues #2421, #7298.
7081
+ */
7082
+ static #installBusyTimeout(db: Database): void {
7083
+ db.run(`PRAGMA busy_timeout = ${getDbBusyTimeoutMs()}`);
7084
+ }
7085
+
6953
7086
  #initializeSchema(): void {
6954
7087
  // Install the busy handler BEFORE any lock-taking statement (incl.
6955
7088
  // `PRAGMA journal_mode=WAL`, which acquires an exclusive lock during WAL
6956
7089
  // recovery). Without this, concurrent omp startups can crash here with
6957
- // `SQLITE_BUSY` / `SQLITE_BUSY_RECOVERY`. See issue #2421.
6958
- this.#db.run("PRAGMA busy_timeout = 5000");
7090
+ // `SQLITE_BUSY` / `SQLITE_BUSY_RECOVERY`. Re-setting when opened via
7091
+ // `open()` (which already installed it) is idempotent. See issue #2421.
7092
+ SqliteAuthCredentialStore.#installBusyTimeout(this.#db);
6959
7093
  this.#db.run(`
6960
7094
  PRAGMA journal_mode=WAL;
6961
7095
  PRAGMA synchronous=NORMAL;
@@ -157,7 +157,6 @@ function mergeAnthropicBetaHeader(callerHeaders: Record<string, string>, beta: s
157
157
  const midConversationSystemBeta = "mid-conversation-system-2026-04-07";
158
158
  const contextManagementBeta = "context-management-2025-06-27";
159
159
  const structuredOutputsBeta = "structured-outputs-2025-12-15";
160
- const context1mBeta = "context-1m-2025-08-07";
161
160
  const thinkingTokenCountBeta = "thinking-token-count-2026-05-13";
162
161
  const fallbackCreditBeta = "fallback-credit-2026-06-01";
163
162
  const coworkUtilityBetaDefaults = [
@@ -187,15 +186,18 @@ const serverSideFallbackBeta = "server-side-fallback-2026-06-01";
187
186
  function buildCoworkBetas(
188
187
  agentRequest: boolean,
189
188
  thinkingRequest: boolean,
190
- longContext: boolean,
191
189
  disableStrictTools = false,
192
190
  ): readonly string[] {
191
+ // `context-1m-2025-08-07` is intentionally never advertised. OAuth
192
+ // subscription credentials have no long-context credit balance, so Anthropic
193
+ // hard-429s ("Usage credits are required for long context requests") on any
194
+ // beta-gated 1M model regardless of prompt size (#7238). Natively-1M models
195
+ // (e.g. claude-sonnet-5) serve their full window without the beta anyway.
193
196
  if (!agentRequest && !disableStrictTools) return coworkUtilityBetaDefaults;
194
197
  const betas: string[] = [];
195
198
  for (const beta of agentRequest ? coworkAgentBetaDefaults : coworkUtilityBetaDefaults) {
196
199
  if (disableStrictTools && beta === structuredOutputsBeta) continue;
197
200
  betas.push(beta);
198
- if (agentRequest && longContext && beta === "claude-code-20250219") betas.push(context1mBeta);
199
201
  }
200
202
  if (!agentRequest) return betas;
201
203
  if (thinkingRequest) betas.push(effortBeta);
@@ -245,7 +247,7 @@ export function buildAnthropicHeaders(options: AnthropicHeaderOptions): Record<s
245
247
  // Cowork's beta profile is part of the OAuth fingerprint; API-key requests
246
248
  // default to extras only, matching the streaming path.
247
249
  const betaHeader = buildBetaHeader(
248
- options.coworkBetas ?? (oauthToken ? buildCoworkBetas(true, true, false) : []),
250
+ options.coworkBetas ?? (oauthToken ? buildCoworkBetas(true, true) : []),
249
251
  extraBetas,
250
252
  );
251
253
  const acceptHeader = oauthToken ? "application/json" : stream ? "text/event-stream" : "application/json";
@@ -2000,7 +2002,7 @@ const streamAnthropicOnce = (
2000
2002
  | (AnthropicServerToolContent & { [kStreamingPartialJson]?: string })
2001
2003
  | (ToolCall & { [kStreamingPartialJson]: string; [kStreamingLastParseLen]?: number })
2002
2004
  ) & { [kStreamingBlockIndex]: number };
2003
- const idleTimeoutMs = options?.streamIdleTimeoutMs ?? getStreamIdleTimeoutMs();
2005
+ const idleTimeoutMs = options?.streamIdleTimeoutMs ?? getStreamIdleTimeoutMs(model.compat.streamIdleTimeoutMs);
2004
2006
  const firstEventTimeoutMs = options?.streamFirstEventTimeoutMs ?? getStreamFirstEventTimeoutMs(idleTimeoutMs);
2005
2007
  const requestTimeoutMs =
2006
2008
  firstEventTimeoutMs !== undefined && firstEventTimeoutMs > 0 ? firstEventTimeoutMs : undefined;
@@ -2949,14 +2951,7 @@ export function buildAnthropicClientOptions(args: AnthropicClientOptionsArgs): A
2949
2951
  isCloudflareAiGateway: model.provider === "cloudflare-ai-gateway",
2950
2952
  allowAnthropicHeaderOverrides: model.compat.allowAnthropicHeaderOverrides,
2951
2953
  claudeCodeSessionId,
2952
- coworkBetas: oauthToken
2953
- ? buildCoworkBetas(
2954
- hasTools || thinkingEnabled,
2955
- thinkingEnabled,
2956
- (model.contextWindow ?? 0) >= 1_000_000,
2957
- disableStrictTools,
2958
- )
2959
- : [],
2954
+ coworkBetas: oauthToken ? buildCoworkBetas(hasTools || thinkingEnabled, thinkingEnabled, disableStrictTools) : [],
2960
2955
  });
2961
2956
 
2962
2957
  if (model.provider === "cloudflare-ai-gateway") {
@@ -69,6 +69,7 @@ import { adaptSchemaForStrict, NO_STRICT, sanitizeSchemaForOpenAIResponses, tool
69
69
  import { notifyRawSseEvent } from "../utils/sse-debug";
70
70
  import { compactGrammarDefinition } from "./grammar";
71
71
  import {
72
+ type CodexLiteShapedBody,
72
73
  type CodexReasoningContext,
73
74
  type CodexRequestOptions,
74
75
  type InputItem,
@@ -111,6 +112,7 @@ import {
111
112
  finalizePendingResponsesToolCalls,
112
113
  finalizeReasoningThinking,
113
114
  finalizeToolCallArgumentsDone,
115
+ getOpenAIPromptCacheKey,
114
116
  hasExecutableIncompleteResponsesToolCalls,
115
117
  isOpenAIResponsesProgressEvent,
116
118
  mapOpenAIResponsesStopReason,
@@ -164,6 +166,17 @@ export interface OpenAICodexResponsesOptions extends StreamOptions {
164
166
  onModerationMetadata?: (metadata: unknown) => void;
165
167
  }
166
168
 
169
+ /** Raw V2 compaction body accepted by the Codex transport selector. */
170
+ export interface OpenAICodexCompactionBody extends CodexLiteShapedBody {
171
+ model: string;
172
+ [key: string]: unknown;
173
+ }
174
+
175
+ /** Transport controls for a provider-native Codex V2 compaction stream. */
176
+ export interface OpenAICodexCompactionStreamOptions extends OpenAICodexResponsesOptions {
177
+ apiKey: string;
178
+ }
179
+
167
180
  /** Inputs for synthesizing Codex request identity outside the normal stream path. */
168
181
  export interface OpenAICodexCompatibilityMetadataOptions {
169
182
  sessionId?: string;
@@ -1359,12 +1372,16 @@ function createRequestSetup(options: OpenAICodexResponsesOptions | undefined): C
1359
1372
  };
1360
1373
  }
1361
1374
 
1362
- async function buildCodexRequestContext(
1375
+ function createCodexRequestContext(
1363
1376
  model: Model<"openai-codex-responses">,
1364
- context: Context,
1377
+ transformedBody: RequestBody,
1365
1378
  options: OpenAICodexResponsesOptions | undefined,
1366
- output: AssistantMessage,
1367
- ): Promise<CodexRequestContext> {
1379
+ contextOptions: {
1380
+ isolateCompactionTransport: boolean;
1381
+ startNewTurn?: boolean;
1382
+ turnStartedAtUnixMs?: number;
1383
+ },
1384
+ ): CodexRequestContext {
1368
1385
  const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
1369
1386
  if (!apiKey) {
1370
1387
  throw new AIError.MissingApiKeyError(model.provider);
@@ -1373,15 +1390,13 @@ async function buildCodexRequestContext(
1373
1390
  const accountId = getCodexAccountId(apiKey);
1374
1391
  const baseUrl = model.baseUrl || CODEX_BASE_URL;
1375
1392
  const url = resolveCodexResponsesUrl(baseUrl);
1376
- const promptCacheKey = normalizeOpenAIPromptCacheKey(options?.promptCacheKey ?? options?.sessionId);
1393
+
1377
1394
  const transportSessionId = normalizeOpenAIPromptCacheKey(options?.sessionId);
1378
1395
  const codexClientVersion = CODEX_CLIENT_VERSION;
1379
- const transformedBody = await buildTransformedCodexRequestBody(model, context, options, promptCacheKey);
1380
-
1381
1396
  const requestHeaders = { ...(model.headers ?? {}), ...(options?.headers ?? {}) };
1382
1397
  const rawRequestDump: RawHttpRequestDump = {
1383
1398
  provider: model.provider,
1384
- api: output.api,
1399
+ api: model.api,
1385
1400
  model: model.id,
1386
1401
  method: "POST",
1387
1402
  url,
@@ -1389,7 +1404,10 @@ async function buildCodexRequestContext(
1389
1404
  };
1390
1405
 
1391
1406
  const providerSessionState = getCodexProviderSessionState(options?.providerSessionState);
1392
- const isolatedTransportState = options?.codexCompaction ? createCodexProviderSessionState() : undefined;
1407
+ const isolatedTransportState =
1408
+ contextOptions.isolateCompactionTransport && options?.codexCompaction
1409
+ ? createCodexProviderSessionState()
1410
+ : undefined;
1393
1411
  const transportProviderSessionState = isolatedTransportState ?? providerSessionState;
1394
1412
  const responsesLite = resolveCodexResponsesLite(model, options?.responsesLite);
1395
1413
  const sessionKey = getCodexWebSocketSessionKey(transportSessionId, model, accountId, apiKey, baseUrl, responsesLite);
@@ -1412,20 +1430,14 @@ async function buildCodexRequestContext(
1412
1430
  websocketState.turnState = sharedWebsocketState.turnState;
1413
1431
  websocketState.modelsEtag = sharedWebsocketState.modelsEtag;
1414
1432
  }
1415
- const withinTurnContinuation = isCodexWithinTurnContinuation(context);
1416
1433
  const metadataSessionId = transportSessionId ?? crypto.randomUUID();
1417
1434
  const metadataSession = getOrCreateCodexMetadataSessionState(metadataSessionId, providerSessionState);
1418
1435
  const compaction = options?.codexCompaction;
1419
1436
  const requestKind: OpenAICodexRequestKind = compaction ? "compaction" : "turn";
1420
- const startNewTurn = resolveCodexStartNewTurn(
1421
- metadataSession,
1422
- requestKind,
1423
- compaction,
1424
- compaction ? undefined : !withinTurnContinuation,
1425
- );
1437
+ const startNewTurn = resolveCodexStartNewTurn(metadataSession, requestKind, compaction, contextOptions.startNewTurn);
1426
1438
  if (websocketState && startNewTurn) {
1427
- // Codex scopes turn-state to one turn. Mid-turn compaction and tool-loop
1428
- // follow-ups preserve it; new user or compaction turns start without it.
1439
+ // Codex scopes turn-state to one turn. Mid-turn compaction preserves it;
1440
+ // a pre-turn or standalone compaction starts without it.
1429
1441
  websocketState.turnState = undefined;
1430
1442
  }
1431
1443
  const requestMetadata = createCodexRequestMetadata(metadataSession, requestKind, {
@@ -1434,7 +1446,7 @@ async function buildCodexRequestContext(
1434
1446
  ? startNewTurn || !metadataSession.turnId
1435
1447
  ? Date.now()
1436
1448
  : undefined
1437
- : getCodexTurnStartedAtUnixMs(context),
1449
+ : contextOptions.turnStartedAtUnixMs,
1438
1450
  clientMetadata: transformedBody.client_metadata,
1439
1451
  parentTurnId: options?.parentTurnId,
1440
1452
  compaction,
@@ -1458,12 +1470,26 @@ async function buildCodexRequestContext(
1458
1470
  };
1459
1471
  }
1460
1472
 
1473
+ async function buildCodexRequestContext(
1474
+ model: Model<"openai-codex-responses">,
1475
+ context: Context,
1476
+ options: OpenAICodexResponsesOptions | undefined,
1477
+ ): Promise<CodexRequestContext> {
1478
+ const promptCacheKey = getOpenAIPromptCacheKey(options);
1479
+ const transformedBody = await buildTransformedCodexRequestBody(model, context, options, promptCacheKey);
1480
+ return createCodexRequestContext(model, transformedBody, options, {
1481
+ isolateCompactionTransport: true,
1482
+ startNewTurn: options?.codexCompaction ? undefined : !isCodexWithinTurnContinuation(context),
1483
+ turnStartedAtUnixMs: options?.codexCompaction ? undefined : getCodexTurnStartedAtUnixMs(context),
1484
+ });
1485
+ }
1486
+
1461
1487
  /** @internal Exported for tests. */
1462
1488
  export async function buildTransformedCodexRequestBody(
1463
1489
  model: Model<"openai-codex-responses">,
1464
1490
  context: Context,
1465
1491
  options: OpenAICodexResponsesOptions | undefined,
1466
- promptCacheKey = normalizeOpenAIPromptCacheKey(options?.promptCacheKey ?? options?.sessionId),
1492
+ promptCacheKey = getOpenAIPromptCacheKey(options),
1467
1493
  ): Promise<RequestBody> {
1468
1494
  const params: RequestBody = {
1469
1495
  model: model.requestModelId ?? model.id,
@@ -1565,6 +1591,127 @@ async function openInitialCodexEventStream(
1565
1591
  }
1566
1592
  return openCodexSseTransport(model, requestContext, requestSetup, options, websocketState, transformedBody);
1567
1593
  }
1594
+
1595
+ function toCodexRequestBody(body: OpenAICodexCompactionBody): RequestBody {
1596
+ const request: RequestBody = { model: body.model };
1597
+ for (const key in body) {
1598
+ if (key !== "model") request[key] = body[key];
1599
+ }
1600
+ return request;
1601
+ }
1602
+
1603
+ /**
1604
+ * Open a provider-native V2 compaction stream through Codex's WebSocket-first
1605
+ * transport, replaying WebSocket transport failures over SSE.
1606
+ */
1607
+ export async function openCodexCompactionEventStream(
1608
+ model: Model<"openai-codex-responses">,
1609
+ body: OpenAICodexCompactionBody,
1610
+ options: OpenAICodexCompactionStreamOptions,
1611
+ ): Promise<AsyncGenerator<Record<string, unknown>>> {
1612
+ const requestSetup = createRequestSetup(options);
1613
+ let requestContext: CodexRequestContext;
1614
+ let initial: {
1615
+ eventStream: AsyncGenerator<Record<string, unknown>>;
1616
+ requestBodyForState: RequestBody;
1617
+ transport: CodexTransport;
1618
+ };
1619
+ try {
1620
+ requestContext = createCodexRequestContext(model, toCodexRequestBody(body), options, {
1621
+ isolateCompactionTransport: false,
1622
+ });
1623
+ initial = await openInitialCodexEventStream(model, options, requestSetup, requestContext);
1624
+ } catch (error) {
1625
+ requestSetup.requestAbortController.abort();
1626
+ throw error;
1627
+ }
1628
+
1629
+ if (requestContext.websocketState) {
1630
+ requestContext.websocketState.lastTransport = initial.transport;
1631
+ // The compaction request may use the existing append baseline, but the
1632
+ // replacement history makes that baseline stale for the next normal turn.
1633
+ resetCodexWebSocketAppendState(requestContext.websocketState);
1634
+ }
1635
+ return streamCodexCompactionEvents(model, options, requestSetup, requestContext, initial);
1636
+ }
1637
+
1638
+ async function* streamCodexCompactionEvents(
1639
+ model: Model<"openai-codex-responses">,
1640
+ options: OpenAICodexCompactionStreamOptions,
1641
+ requestSetup: CodexRequestSetup,
1642
+ requestContext: CodexRequestContext,
1643
+ initial: {
1644
+ eventStream: AsyncGenerator<Record<string, unknown>>;
1645
+ requestBodyForState: RequestBody;
1646
+ transport: CodexTransport;
1647
+ },
1648
+ ): AsyncGenerator<Record<string, unknown>> {
1649
+ let completed = false;
1650
+ const websocketState = requestContext.websocketState;
1651
+ const previousTurnState = websocketState?.turnState;
1652
+ const previousModelsEtag = websocketState?.modelsEtag;
1653
+ try {
1654
+ if (initial.transport === "websocket") {
1655
+ // Do not expose a WebSocket attempt until it finishes: an SSE replay
1656
+ // must replace, not extend, any partial compaction output.
1657
+ const bufferedEvents: Array<Record<string, unknown>> = [];
1658
+ try {
1659
+ for await (const event of initial.eventStream) bufferedEvents.push(event);
1660
+ } catch (error) {
1661
+ if (options.signal?.aborted || !(error instanceof CodexWebSocketTransportError)) {
1662
+ throw error;
1663
+ }
1664
+ const state = requestContext.websocketState;
1665
+ if (state) recordCodexWebSocketFailure(state, true);
1666
+ const fallback = await openCodexSseTransport(model, requestContext, requestSetup, options, state);
1667
+ if (state) state.lastTransport = fallback.transport;
1668
+ yield* drainCodexCompactionEvents(fallback.eventStream, requestContext.websocketState);
1669
+ completed = true;
1670
+ return;
1671
+ }
1672
+ // Apply metadata only once the WebSocket attempt succeeded: a discarded
1673
+ // attempt must not leak its `x-codex-turn-state` into the session.
1674
+ for (const event of bufferedEvents) {
1675
+ applyCodexCompactionResponseMetadata(requestContext.websocketState, event);
1676
+ yield event;
1677
+ }
1678
+ } else {
1679
+ yield* drainCodexCompactionEvents(initial.eventStream, requestContext.websocketState);
1680
+ }
1681
+ completed = true;
1682
+ } finally {
1683
+ if (!completed) {
1684
+ requestSetup.requestAbortController.abort();
1685
+ if (websocketState) {
1686
+ websocketState.turnState = previousTurnState;
1687
+ websocketState.modelsEtag = previousModelsEtag;
1688
+ }
1689
+ }
1690
+ }
1691
+ }
1692
+
1693
+ /**
1694
+ * Capture `x-codex-turn-state`/`x-models-etag` refreshes carried by a
1695
+ * `response.metadata` frame so a mid-turn compaction leaves the live session on
1696
+ * the latest turn state, matching the normal Codex stream processor.
1697
+ */
1698
+ function applyCodexCompactionResponseMetadata(
1699
+ state: CodexWebSocketSessionState | undefined,
1700
+ event: Record<string, unknown>,
1701
+ ): void {
1702
+ if (!state || event.type !== "response.metadata") return;
1703
+ updateCodexSessionMetadataFromHeaders(state, toCodexHeaders(event.headers));
1704
+ }
1705
+
1706
+ async function* drainCodexCompactionEvents(
1707
+ events: AsyncGenerator<Record<string, unknown>>,
1708
+ state: CodexWebSocketSessionState | undefined,
1709
+ ): AsyncGenerator<Record<string, unknown>> {
1710
+ for await (const event of events) {
1711
+ applyCodexCompactionResponseMetadata(state, event);
1712
+ yield event;
1713
+ }
1714
+ }
1568
1715
  async function openCodexWebSocketTransport(
1569
1716
  model: Model<"openai-codex-responses">,
1570
1717
  options: OpenAICodexResponsesOptions | undefined,
@@ -2278,12 +2425,15 @@ class CodexStreamProcessor {
2278
2425
  resetCodexWebSocketAppendState(state);
2279
2426
  } else {
2280
2427
  state.lastRequest = structuredCloneJSON(runtime.requestBodyForState);
2281
- if (responseId) {
2428
+ const replayableResponseItems = sanitizeOpenAIResponsesAssistantHistoryItemsForReplay(
2429
+ structuredCloneJSON(runtime.nativeOutputItems),
2430
+ );
2431
+ if (responseId && replayableResponseItems) {
2282
2432
  state.lastResponseId = responseId;
2283
- state.lastResponseItems = stripInputItemIds(structuredCloneJSON(runtime.nativeOutputItems));
2433
+ state.lastResponseItems = replayableResponseItems;
2284
2434
  state.canAppend = rawEvent.type === "response.done" || rawEvent.type === "response.completed";
2285
2435
  } else {
2286
- // Without a response id the append baseline cannot be trusted.
2436
+ // Without both a response id and replayable output, the append baseline cannot be trusted.
2287
2437
  state.canAppend = false;
2288
2438
  }
2289
2439
  }
@@ -2709,7 +2859,7 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"
2709
2859
  let requestContext: CodexRequestContext | undefined;
2710
2860
 
2711
2861
  try {
2712
- requestContext = await buildCodexRequestContext(model, context, options, output);
2862
+ requestContext = await buildCodexRequestContext(model, context, options);
2713
2863
  const initialTransport = await openInitialCodexEventStream(model, options, requestSetup, requestContext);
2714
2864
  const runtime = new CodexStreamRuntime({
2715
2865
  ...initialTransport,
@@ -3004,14 +3154,6 @@ export function getOpenAICodexTransportDetails(
3004
3154
  };
3005
3155
  }
3006
3156
 
3007
- function stripInputItemIds(items: Array<Record<string, unknown>>): InputItem[] {
3008
- return items.map(item => {
3009
- if (item.id == null) return item as InputItem;
3010
- const { id: _id, ...rest } = item;
3011
- return rest as InputItem;
3012
- });
3013
- }
3014
-
3015
3157
  const codexDiagnosticsTextEncoder = new TextEncoder();
3016
3158
 
3017
3159
  function jsonByteLength(value: unknown): number {
@@ -757,7 +757,13 @@ const streamOpenAICompletionsOnce = (
757
757
  disableStrictTools = true;
758
758
  openaiStream = await createCompletionsStream("none");
759
759
  } else {
760
- if (!shouldRetryWithoutStrictTools(error, capturedErrorResponse, appliedStrictTools, context.tools)) {
760
+ if (
761
+ !shouldRetryWithoutStrictTools(error, capturedErrorResponse, {
762
+ model,
763
+ strictToolsApplied: appliedStrictTools,
764
+ tools: context.tools,
765
+ })
766
+ ) {
761
767
  throw error;
762
768
  }
763
769
  // Remember the rejection for the rest of the session so every
@@ -610,12 +610,11 @@ const streamOpenAIResponsesOnce = (
610
610
  strictRetryAvailable &&
611
611
  !requestSignal.aborted &&
612
612
  (compiledGrammarTooLarge ||
613
- shouldRetryWithoutStrictTools(
614
- error,
615
- capturedErrorResponse,
616
- activeStrictToolsApplied,
617
- context.tools,
618
- ));
613
+ shouldRetryWithoutStrictTools(error, capturedErrorResponse, {
614
+ model,
615
+ strictToolsApplied: activeStrictToolsApplied,
616
+ tools: context.tools,
617
+ }));
619
618
  if (canRetryWithoutStrictTools) {
620
619
  strictRetryAvailable = false;
621
620
  forceDisableStrictTools = true;
@@ -1181,21 +1181,46 @@ export function isCompiledGrammarTooLargeStrictError(
1181
1181
  );
1182
1182
  }
1183
1183
 
1184
+ interface StrictToolsRetryContext {
1185
+ model: OpenAIModelIdentity;
1186
+ strictToolsApplied: boolean;
1187
+ tools: Tool[] | undefined;
1188
+ }
1189
+
1190
+ /** Decide whether an OpenAI-family request should retry once with non-strict tools. */
1184
1191
  export function shouldRetryWithoutStrictTools(
1185
1192
  error: unknown,
1186
1193
  capturedErrorResponse: CapturedHttpErrorResponse | undefined,
1187
- strictToolsApplied: boolean,
1188
- tools: Tool[] | undefined,
1194
+ context: StrictToolsRetryContext,
1189
1195
  ): boolean {
1196
+ const { model, strictToolsApplied, tools } = context;
1190
1197
  if (!tools || tools.length === 0 || !strictToolsApplied) return false;
1191
1198
  const status = extractHttpStatusFromError(error) ?? capturedErrorResponse?.status;
1192
1199
  if (status !== 400 && status !== 422) return false;
1200
+ const errorMessage = error instanceof Error ? error.message.trim() : "";
1193
1201
  const messageParts = [error instanceof Error ? error.message : undefined, capturedErrorResponse?.bodyText]
1194
1202
  .filter((value): value is string => typeof value === "string" && value.trim().length > 0)
1195
1203
  .join("\n");
1196
- return /wrong_api_format|mixed values for 'strict'|tool[s]?\b.*strict|\bstrict\b.*tool|tool parameters? schema|invalid schema for function|structured[_ -]?outputs?\b[^\n]*(?:not (?:supported|available|enabled)|unsupported)|(?:not support|unsupported)[^\n]*structured[_ -]?outputs?\b/i.test(
1197
- messageParts,
1198
- );
1204
+ if (
1205
+ /wrong_api_format|mixed values for 'strict'|tool[s]?\b.*strict|\bstrict\b.*tool|tool parameters? schema|invalid schema for function|structured[_ -]?outputs?\b[^\n]*(?:not (?:supported|available|enabled)|unsupported)|(?:not support|unsupported)[^\n]*structured[_ -]?outputs?\b/i.test(
1206
+ messageParts,
1207
+ )
1208
+ ) {
1209
+ return true;
1210
+ }
1211
+ if (model.provider !== "openrouter" || !/^(?:400\s+)?Provider returned error$/i.test(errorMessage)) return false;
1212
+ const body = capturedErrorResponse?.bodyJson;
1213
+ if (body && typeof body === "object" && "error" in body) {
1214
+ const errorBody = body.error;
1215
+ if (errorBody && typeof errorBody === "object" && "metadata" in errorBody) {
1216
+ const metadata = errorBody.metadata;
1217
+ if (metadata && typeof metadata === "object" && "raw" in metadata) {
1218
+ const raw = metadata.raw;
1219
+ if (typeof raw === "string" ? raw.trim().length > 0 : raw != null) return false;
1220
+ }
1221
+ }
1222
+ }
1223
+ return true;
1199
1224
  }
1200
1225
 
1201
1226
  function normalizeOpenAIStableId(value: string | undefined, maxLength: number, hashPrefix: string): string | undefined {
@@ -0,0 +1,22 @@
1
+ import { createApiKeyLogin } from "./api-key-login";
2
+ import type { OAuthLoginCallbacks } from "./oauth/types";
3
+ import type { ProviderDefinition } from "./types";
4
+
5
+ export const loginAiand = createApiKeyLogin({
6
+ providerLabel: "ai&",
7
+ authUrl: "https://console.aiand.com/api-keys",
8
+ instructions: "Copy your API key from the ai& console",
9
+ promptMessage: "Paste your ai& API key",
10
+ placeholder: "sk-...",
11
+ validation: {
12
+ kind: "models-endpoint",
13
+ provider: "ai&",
14
+ modelsUrl: "https://api.aiand.com/v1/models",
15
+ },
16
+ });
17
+
18
+ export const aiandProvider = {
19
+ id: "aiand",
20
+ name: "ai&",
21
+ login: (cb: OAuthLoginCallbacks) => loginAiand(cb),
22
+ } as const satisfies ProviderDefinition;
@@ -1,4 +1,5 @@
1
1
  import type { KnownProvider } from "@oh-my-pi/pi-catalog";
2
+ import { aiandProvider } from "./aiand";
2
3
  import { aimlApiProvider } from "./aimlapi";
3
4
  import { alibabaCodingPlanProvider } from "./alibaba-coding-plan";
4
5
  import { alibabaTokenPlanProvider } from "./alibaba-token-plan";
@@ -101,6 +102,7 @@ const ALL = [
101
102
  gitLabDuoWorkflowProvider,
102
103
  alibabaCodingPlanProvider,
103
104
  alibabaTokenPlanProvider,
105
+ aiandProvider,
104
106
  aimlApiProvider,
105
107
  zhipuCodingPlanProvider,
106
108
  umansProvider,
package/src/stream.ts CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  } from "@oh-my-pi/pi-catalog/model-thinking";
16
16
  import { CATALOG_PROVIDERS, type ProviderCatalogEntry } from "@oh-my-pi/pi-catalog/provider-models";
17
17
  import { CODEX_BASE_URL } from "@oh-my-pi/pi-catalog/wire/codex";
18
- import { $env, $pickenv, getConfigRootDir, isEnoent, logger, withExtraCaFetch } from "@oh-my-pi/pi-utils";
18
+ import { $env, $pickenv, getProviderInFlightRoot, isEnoent, logger, withExtraCaFetch } from "@oh-my-pi/pi-utils";
19
19
  import { getCustomApi } from "./api-registry";
20
20
  import { createAuthRetryKeyState, isApiKeyResolver, resolveNextAuthRetryKey } from "./auth-retry";
21
21
  import * as AIError from "./error";
@@ -189,7 +189,7 @@ function resolveProviderInFlightLimit(
189
189
 
190
190
  function providerInFlightRoot(): string {
191
191
  if (providerInFlightRootOverride) return providerInFlightRootOverride;
192
- return path.join(getConfigRootDir(), "run", "provider-inflight");
192
+ return getProviderInFlightRoot();
193
193
  }
194
194
 
195
195
  function providerInFlightSegment(provider: string): string {