@oh-my-pi/pi-ai 17.2.3 → 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 +11 -0
- package/dist/types/auth-storage.d.ts +8 -0
- package/dist/types/providers/openai-codex-responses.d.ts +15 -1
- package/dist/types/providers/openai-shared.d.ts +7 -1
- package/package.json +4 -4
- package/src/auth-storage.ts +148 -14
- package/src/providers/anthropic.ts +1 -1
- package/src/providers/openai-codex-responses.ts +172 -31
- package/src/providers/openai-completions.ts +7 -1
- package/src/providers/openai-responses.ts +5 -6
- package/src/providers/openai-shared.ts +30 -5
- package/src/stream.ts +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
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
|
+
|
|
5
16
|
## [17.2.3] - 2026-08-01
|
|
6
17
|
|
|
7
18
|
### 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
|
-
|
|
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;
|
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.
|
|
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.
|
|
42
|
-
"@oh-my-pi/pi-utils": "17.2.
|
|
43
|
-
"@oh-my-pi/pi-wire": "17.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
|
},
|
package/src/auth-storage.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
5866
|
-
const
|
|
5867
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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`.
|
|
6958
|
-
|
|
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;
|
|
@@ -2002,7 +2002,7 @@ const streamAnthropicOnce = (
|
|
|
2002
2002
|
| (AnthropicServerToolContent & { [kStreamingPartialJson]?: string })
|
|
2003
2003
|
| (ToolCall & { [kStreamingPartialJson]: string; [kStreamingLastParseLen]?: number })
|
|
2004
2004
|
) & { [kStreamingBlockIndex]: number };
|
|
2005
|
-
const idleTimeoutMs = options?.streamIdleTimeoutMs ?? getStreamIdleTimeoutMs();
|
|
2005
|
+
const idleTimeoutMs = options?.streamIdleTimeoutMs ?? getStreamIdleTimeoutMs(model.compat.streamIdleTimeoutMs);
|
|
2006
2006
|
const firstEventTimeoutMs = options?.streamFirstEventTimeoutMs ?? getStreamFirstEventTimeoutMs(idleTimeoutMs);
|
|
2007
2007
|
const requestTimeoutMs =
|
|
2008
2008
|
firstEventTimeoutMs !== undefined && firstEventTimeoutMs > 0 ? firstEventTimeoutMs : undefined;
|
|
@@ -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,
|
|
@@ -165,6 +166,17 @@ export interface OpenAICodexResponsesOptions extends StreamOptions {
|
|
|
165
166
|
onModerationMetadata?: (metadata: unknown) => void;
|
|
166
167
|
}
|
|
167
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
|
+
|
|
168
180
|
/** Inputs for synthesizing Codex request identity outside the normal stream path. */
|
|
169
181
|
export interface OpenAICodexCompatibilityMetadataOptions {
|
|
170
182
|
sessionId?: string;
|
|
@@ -1360,12 +1372,16 @@ function createRequestSetup(options: OpenAICodexResponsesOptions | undefined): C
|
|
|
1360
1372
|
};
|
|
1361
1373
|
}
|
|
1362
1374
|
|
|
1363
|
-
|
|
1375
|
+
function createCodexRequestContext(
|
|
1364
1376
|
model: Model<"openai-codex-responses">,
|
|
1365
|
-
|
|
1377
|
+
transformedBody: RequestBody,
|
|
1366
1378
|
options: OpenAICodexResponsesOptions | undefined,
|
|
1367
|
-
|
|
1368
|
-
|
|
1379
|
+
contextOptions: {
|
|
1380
|
+
isolateCompactionTransport: boolean;
|
|
1381
|
+
startNewTurn?: boolean;
|
|
1382
|
+
turnStartedAtUnixMs?: number;
|
|
1383
|
+
},
|
|
1384
|
+
): CodexRequestContext {
|
|
1369
1385
|
const apiKey = options?.apiKey || getEnvApiKey(model.provider) || "";
|
|
1370
1386
|
if (!apiKey) {
|
|
1371
1387
|
throw new AIError.MissingApiKeyError(model.provider);
|
|
@@ -1374,15 +1390,13 @@ async function buildCodexRequestContext(
|
|
|
1374
1390
|
const accountId = getCodexAccountId(apiKey);
|
|
1375
1391
|
const baseUrl = model.baseUrl || CODEX_BASE_URL;
|
|
1376
1392
|
const url = resolveCodexResponsesUrl(baseUrl);
|
|
1377
|
-
|
|
1393
|
+
|
|
1378
1394
|
const transportSessionId = normalizeOpenAIPromptCacheKey(options?.sessionId);
|
|
1379
1395
|
const codexClientVersion = CODEX_CLIENT_VERSION;
|
|
1380
|
-
const transformedBody = await buildTransformedCodexRequestBody(model, context, options, promptCacheKey);
|
|
1381
|
-
|
|
1382
1396
|
const requestHeaders = { ...(model.headers ?? {}), ...(options?.headers ?? {}) };
|
|
1383
1397
|
const rawRequestDump: RawHttpRequestDump = {
|
|
1384
1398
|
provider: model.provider,
|
|
1385
|
-
api:
|
|
1399
|
+
api: model.api,
|
|
1386
1400
|
model: model.id,
|
|
1387
1401
|
method: "POST",
|
|
1388
1402
|
url,
|
|
@@ -1390,7 +1404,10 @@ async function buildCodexRequestContext(
|
|
|
1390
1404
|
};
|
|
1391
1405
|
|
|
1392
1406
|
const providerSessionState = getCodexProviderSessionState(options?.providerSessionState);
|
|
1393
|
-
const isolatedTransportState =
|
|
1407
|
+
const isolatedTransportState =
|
|
1408
|
+
contextOptions.isolateCompactionTransport && options?.codexCompaction
|
|
1409
|
+
? createCodexProviderSessionState()
|
|
1410
|
+
: undefined;
|
|
1394
1411
|
const transportProviderSessionState = isolatedTransportState ?? providerSessionState;
|
|
1395
1412
|
const responsesLite = resolveCodexResponsesLite(model, options?.responsesLite);
|
|
1396
1413
|
const sessionKey = getCodexWebSocketSessionKey(transportSessionId, model, accountId, apiKey, baseUrl, responsesLite);
|
|
@@ -1413,20 +1430,14 @@ async function buildCodexRequestContext(
|
|
|
1413
1430
|
websocketState.turnState = sharedWebsocketState.turnState;
|
|
1414
1431
|
websocketState.modelsEtag = sharedWebsocketState.modelsEtag;
|
|
1415
1432
|
}
|
|
1416
|
-
const withinTurnContinuation = isCodexWithinTurnContinuation(context);
|
|
1417
1433
|
const metadataSessionId = transportSessionId ?? crypto.randomUUID();
|
|
1418
1434
|
const metadataSession = getOrCreateCodexMetadataSessionState(metadataSessionId, providerSessionState);
|
|
1419
1435
|
const compaction = options?.codexCompaction;
|
|
1420
1436
|
const requestKind: OpenAICodexRequestKind = compaction ? "compaction" : "turn";
|
|
1421
|
-
const startNewTurn = resolveCodexStartNewTurn(
|
|
1422
|
-
metadataSession,
|
|
1423
|
-
requestKind,
|
|
1424
|
-
compaction,
|
|
1425
|
-
compaction ? undefined : !withinTurnContinuation,
|
|
1426
|
-
);
|
|
1437
|
+
const startNewTurn = resolveCodexStartNewTurn(metadataSession, requestKind, compaction, contextOptions.startNewTurn);
|
|
1427
1438
|
if (websocketState && startNewTurn) {
|
|
1428
|
-
// Codex scopes turn-state to one turn. Mid-turn compaction
|
|
1429
|
-
//
|
|
1439
|
+
// Codex scopes turn-state to one turn. Mid-turn compaction preserves it;
|
|
1440
|
+
// a pre-turn or standalone compaction starts without it.
|
|
1430
1441
|
websocketState.turnState = undefined;
|
|
1431
1442
|
}
|
|
1432
1443
|
const requestMetadata = createCodexRequestMetadata(metadataSession, requestKind, {
|
|
@@ -1435,7 +1446,7 @@ async function buildCodexRequestContext(
|
|
|
1435
1446
|
? startNewTurn || !metadataSession.turnId
|
|
1436
1447
|
? Date.now()
|
|
1437
1448
|
: undefined
|
|
1438
|
-
:
|
|
1449
|
+
: contextOptions.turnStartedAtUnixMs,
|
|
1439
1450
|
clientMetadata: transformedBody.client_metadata,
|
|
1440
1451
|
parentTurnId: options?.parentTurnId,
|
|
1441
1452
|
compaction,
|
|
@@ -1459,6 +1470,20 @@ async function buildCodexRequestContext(
|
|
|
1459
1470
|
};
|
|
1460
1471
|
}
|
|
1461
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
|
+
|
|
1462
1487
|
/** @internal Exported for tests. */
|
|
1463
1488
|
export async function buildTransformedCodexRequestBody(
|
|
1464
1489
|
model: Model<"openai-codex-responses">,
|
|
@@ -1566,6 +1591,127 @@ async function openInitialCodexEventStream(
|
|
|
1566
1591
|
}
|
|
1567
1592
|
return openCodexSseTransport(model, requestContext, requestSetup, options, websocketState, transformedBody);
|
|
1568
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
|
+
}
|
|
1569
1715
|
async function openCodexWebSocketTransport(
|
|
1570
1716
|
model: Model<"openai-codex-responses">,
|
|
1571
1717
|
options: OpenAICodexResponsesOptions | undefined,
|
|
@@ -2279,12 +2425,15 @@ class CodexStreamProcessor {
|
|
|
2279
2425
|
resetCodexWebSocketAppendState(state);
|
|
2280
2426
|
} else {
|
|
2281
2427
|
state.lastRequest = structuredCloneJSON(runtime.requestBodyForState);
|
|
2282
|
-
|
|
2428
|
+
const replayableResponseItems = sanitizeOpenAIResponsesAssistantHistoryItemsForReplay(
|
|
2429
|
+
structuredCloneJSON(runtime.nativeOutputItems),
|
|
2430
|
+
);
|
|
2431
|
+
if (responseId && replayableResponseItems) {
|
|
2283
2432
|
state.lastResponseId = responseId;
|
|
2284
|
-
state.lastResponseItems =
|
|
2433
|
+
state.lastResponseItems = replayableResponseItems;
|
|
2285
2434
|
state.canAppend = rawEvent.type === "response.done" || rawEvent.type === "response.completed";
|
|
2286
2435
|
} else {
|
|
2287
|
-
// 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.
|
|
2288
2437
|
state.canAppend = false;
|
|
2289
2438
|
}
|
|
2290
2439
|
}
|
|
@@ -2710,7 +2859,7 @@ export const streamOpenAICodexResponses: StreamFunction<"openai-codex-responses"
|
|
|
2710
2859
|
let requestContext: CodexRequestContext | undefined;
|
|
2711
2860
|
|
|
2712
2861
|
try {
|
|
2713
|
-
requestContext = await buildCodexRequestContext(model, context, options
|
|
2862
|
+
requestContext = await buildCodexRequestContext(model, context, options);
|
|
2714
2863
|
const initialTransport = await openInitialCodexEventStream(model, options, requestSetup, requestContext);
|
|
2715
2864
|
const runtime = new CodexStreamRuntime({
|
|
2716
2865
|
...initialTransport,
|
|
@@ -3005,14 +3154,6 @@ export function getOpenAICodexTransportDetails(
|
|
|
3005
3154
|
};
|
|
3006
3155
|
}
|
|
3007
3156
|
|
|
3008
|
-
function stripInputItemIds(items: Array<Record<string, unknown>>): InputItem[] {
|
|
3009
|
-
return items.map(item => {
|
|
3010
|
-
if (item.id == null) return item as InputItem;
|
|
3011
|
-
const { id: _id, ...rest } = item;
|
|
3012
|
-
return rest as InputItem;
|
|
3013
|
-
});
|
|
3014
|
-
}
|
|
3015
|
-
|
|
3016
3157
|
const codexDiagnosticsTextEncoder = new TextEncoder();
|
|
3017
3158
|
|
|
3018
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 (
|
|
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
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1197
|
-
|
|
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 {
|
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,
|
|
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
|
|
192
|
+
return getProviderInFlightRoot();
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
function providerInFlightSegment(provider: string): string {
|