@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 +22 -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/dist/types/registry/aiand.d.ts +7 -0
- package/dist/types/registry/registry.d.ts +4 -0
- package/package.json +4 -4
- package/src/auth-storage.ts +148 -14
- package/src/providers/anthropic.ts +8 -13
- package/src/providers/openai-codex-responses.ts +174 -32
- 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/registry/aiand.ts +22 -0
- package/src/registry/registry.ts +2 -0
- package/src/stream.ts +2 -2
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
|
-
|
|
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.
|
|
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;
|
|
@@ -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
|
|
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
|
-
|
|
1375
|
+
function createCodexRequestContext(
|
|
1363
1376
|
model: Model<"openai-codex-responses">,
|
|
1364
|
-
|
|
1377
|
+
transformedBody: RequestBody,
|
|
1365
1378
|
options: OpenAICodexResponsesOptions | undefined,
|
|
1366
|
-
|
|
1367
|
-
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
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
|
|
1428
|
-
//
|
|
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
|
-
:
|
|
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 =
|
|
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
|
-
|
|
2428
|
+
const replayableResponseItems = sanitizeOpenAIResponsesAssistantHistoryItemsForReplay(
|
|
2429
|
+
structuredCloneJSON(runtime.nativeOutputItems),
|
|
2430
|
+
);
|
|
2431
|
+
if (responseId && replayableResponseItems) {
|
|
2282
2432
|
state.lastResponseId = responseId;
|
|
2283
|
-
state.lastResponseItems =
|
|
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
|
|
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 (
|
|
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 {
|
|
@@ -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;
|
package/src/registry/registry.ts
CHANGED
|
@@ -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,
|
|
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 {
|