@yansigit/opencodex 2.33.0 → 2.35.0
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/README.md +3 -3
- package/gui/dist/assets/index-BjCaHxdz.js +112 -0
- package/gui/dist/assets/index-DLkXOXLC.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +79 -2
- package/src/adapters/command-code.ts +141 -23
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/discovery.ts +60 -2
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +162 -0
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +17 -1
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-fs.ts +13 -12
- package/src/adapters/cursor/native-exec-network.ts +3 -5
- package/src/adapters/cursor/native-exec-policy.ts +47 -0
- package/src/adapters/cursor/native-exec-shell.ts +116 -31
- package/src/adapters/cursor/native-exec.ts +38 -10
- package/src/adapters/cursor/protobuf-events.ts +28 -2
- package/src/adapters/cursor/protobuf-request.ts +93 -41
- package/src/adapters/cursor/request-builder.ts +39 -10
- package/src/adapters/cursor/tool-definitions.ts +27 -3
- package/src/adapters/cursor/tool-result-normalize.ts +51 -6
- package/src/adapters/cursor/types.ts +23 -4
- package/src/adapters/cursor.ts +170 -29
- package/src/adapters/google-aistudio-parser.ts +49 -0
- package/src/adapters/google-antigravity-replay.ts +105 -25
- package/src/adapters/google-antigravity-wire.ts +5 -0
- package/src/adapters/google-errors.ts +41 -12
- package/src/adapters/google-http.ts +12 -11
- package/src/adapters/google.ts +219 -36
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +15 -0
- package/src/adapters/kiro-tools.ts +43 -15
- package/src/adapters/kiro.ts +54 -9
- package/src/adapters/openai-chat.ts +286 -242
- package/src/adapters/openai-responses.ts +335 -24
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-catalog-nudge.ts +2 -2
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/bridge.ts +67 -26
- package/src/chat/inbound.ts +29 -1
- package/src/chat/outbound.ts +15 -7
- package/src/claude/agents-inject.ts +8 -1
- package/src/claude/outbound.ts +10 -8
- package/src/cli/account-api.ts +27 -7
- package/src/cli/account-extended.ts +10 -3
- package/src/cli/account.ts +29 -5
- package/src/cli/alias.ts +66 -0
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +13 -1
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +6 -1
- package/src/cli/init.ts +1 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +13 -7
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/registry.ts +6 -1
- package/src/cli/telemetry-commands.ts +25 -0
- package/src/cli/v2.ts +34 -10
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +80 -6
- package/src/codex/auth-api.ts +48 -8
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/catalog/aggregation.ts +6 -0
- package/src/codex/catalog/model-metadata.ts +13 -1
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +16 -0
- package/src/codex/catalog/provider-fetch.ts +20 -3
- package/src/codex/catalog/sync.ts +127 -2
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-write-lock.ts +3 -1
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +22 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +18 -5
- package/src/codex/inject-coordination.ts +83 -0
- package/src/codex/inject.ts +14 -1
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/prompt-layers.ts +371 -25
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota.ts +123 -18
- package/src/codex/routing.ts +9 -0
- package/src/codex/subagent-model-fallback.ts +198 -27
- package/src/codex/transition-state.ts +107 -8
- package/src/combos/types.ts +10 -0
- package/src/compatibility/openai-responses.ts +33 -1
- package/src/config/autonomous-remediation.ts +21 -0
- package/src/config/provider-validation.ts +14 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +191 -17
- package/src/generated/compatibility-version.json +279 -159
- package/src/generated/model-metadata.ts +3 -0
- package/src/images/loop.ts +5 -4
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +11 -2
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/oauth/aistudio-credentials.ts +65 -0
- package/src/oauth/aistudio-native-daemon.ts +116 -0
- package/src/oauth/aistudio-session-sync.ts +95 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/google-aistudio-auth.ts +98 -0
- package/src/oauth/index.ts +57 -5
- package/src/oauth/key-providers.ts +18 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/login-cli.ts +65 -1
- package/src/oauth/types.ts +15 -0
- package/src/providers/codex-capacity.ts +5 -2
- package/src/providers/command-code-efforts.ts +38 -6
- package/src/providers/context-cap.ts +4 -3
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +29 -1
- package/src/providers/fastwire.ts +7 -1
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/provider-id-rewrite.ts +2 -1
- package/src/providers/quota.ts +157 -46
- package/src/providers/registry.ts +184 -71
- package/src/providers/slug-codec.ts +52 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +34 -10
- package/src/responses/parser.ts +4 -0
- package/src/responses/schema.ts +5 -1
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/router.ts +43 -2
- package/src/routing/account-pool/cooldown.ts +8 -0
- package/src/routing/account-pool/index.ts +1 -0
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +24 -0
- package/src/server/chat-completions.ts +26 -16
- package/src/server/chat-native-sse.ts +3 -3
- package/src/server/chat-native.ts +30 -11
- package/src/server/claude-messages.ts +1 -1
- package/src/server/effort-policy.ts +16 -0
- package/src/server/index.ts +180 -14
- package/src/server/lifecycle.ts +52 -1
- package/src/server/management/agent-settings-routes.ts +31 -15
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +2 -1
- package/src/server/management/config-routes.ts +27 -9
- package/src/server/management/context.ts +9 -0
- package/src/server/management/logs-usage-routes.ts +11 -5
- package/src/server/management/model-routes.ts +266 -0
- package/src/server/management/oauth-account-routes.ts +13 -3
- package/src/server/management/provider-routes.ts +137 -3
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management-api.ts +2 -0
- package/src/server/port-reclaim.ts +19 -1
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +33 -0
- package/src/server/request-log.ts +48 -21
- package/src/server/responses/collaboration.ts +42 -5
- package/src/server/responses/combo-stream-preflight.ts +10 -3
- package/src/server/responses/core.ts +575 -140
- package/src/server/responses/empty-completion-guard.ts +35 -0
- package/src/server/responses/fetch-helpers.ts +14 -6
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/policy-fallback.ts +1 -1
- package/src/server/responses/responses-field-backfill.ts +105 -13
- package/src/server/responses/ws-upstream.ts +35 -5
- package/src/server/responses-custom-tool-repair.ts +52 -7
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +14 -2
- package/src/smoke/fingerprint-cache.ts +133 -0
- package/src/smoke/live-scenarios.ts +33 -0
- package/src/smoke/runner.ts +119 -0
- package/src/telemetry/dispatcher.ts +44 -0
- package/src/telemetry/fingerprint.ts +24 -0
- package/src/telemetry/hook.ts +43 -0
- package/src/telemetry/ledger.ts +54 -0
- package/src/telemetry/types.ts +23 -0
- package/src/types/config.ts +66 -14
- package/src/types/provider.ts +79 -1
- package/src/types/request.ts +18 -10
- package/src/types/tools.ts +30 -11
- package/src/types.ts +1 -0
- package/src/usage/command-code-manifest.ts +116 -0
- package/src/usage/cost.ts +2 -2
- package/src/usage/expected-prices.ts +126 -24
- package/src/usage/log.ts +18 -8
- package/src/usage/summary.ts +34 -12
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/index.ts +16 -8
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-DKLr4LTE.js +0 -102
- package/gui/dist/assets/index-DrSQdTRd.css +0 -1
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
9
9
|
import { join } from "node:path";
|
|
10
|
-
import { hasOwnProvider } from "../config";
|
|
10
|
+
import { hasOwnProvider, resolveSubagentCandidates } from "../config";
|
|
11
11
|
import { isRateLimitOrQuotaFailureMessage } from "../lib/errors";
|
|
12
12
|
import type { OcxParsedRequest, OcxConfig } from "../types";
|
|
13
13
|
import { slugsEquivalent } from "../providers/slug-codec";
|
|
@@ -15,12 +15,12 @@ import { CODEX_HOME, getCodexHome } from "./paths";
|
|
|
15
15
|
import { CODEX_UNKNOWN_USAGE_SCORE, getAccountQuota } from "./quota";
|
|
16
16
|
import {
|
|
17
17
|
canAcquireCodexQuotaProbeLease,
|
|
18
|
+
canAcquireCodexQuotaScopeProbeLease,
|
|
18
19
|
codexQuotaScopeForModel,
|
|
19
20
|
computeCodexUsageScore,
|
|
20
21
|
getCodexQuotaHealthSnapshot,
|
|
21
22
|
getEffectiveActiveCodexAccountId,
|
|
22
23
|
getPoolAccountPlan,
|
|
23
|
-
isCodexAccountInCooldown,
|
|
24
24
|
} from "./routing";
|
|
25
25
|
import {
|
|
26
26
|
isCodexAccountUsable,
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
import { routeModel, type RouteResult } from "../router";
|
|
39
39
|
import { sweepExpiredOnWrite } from "../lib/state-store-sweeper";
|
|
40
40
|
import { codexAccountNamespaceForModel } from "./account-namespace-match";
|
|
41
|
+
import { ACCOUNT_GATED_NATIVE_OPENAI_MODELS } from "./catalog/native-models";
|
|
41
42
|
import {
|
|
42
43
|
getUpstreamHostHealth,
|
|
43
44
|
normalizeUpstreamHostCircuitThreshold,
|
|
@@ -48,6 +49,15 @@ export const DEFAULT_SUBAGENT_MODEL_FALLBACK_POLL_MS = 60_000;
|
|
|
48
49
|
const CODEX_FORWARD_ORIGIN = new URL(CODEX_FORWARD_BASE_URL).origin.toLowerCase();
|
|
49
50
|
|
|
50
51
|
type SubagentQuotaPrimeFn = (config: OcxConfig, reason: string) => Promise<void>;
|
|
52
|
+
/** Side-effect-free Pool account preview for one resolved fallback candidate. */
|
|
53
|
+
export type SubagentPoolAccountPreview = (
|
|
54
|
+
modelId: string | undefined,
|
|
55
|
+
now: number,
|
|
56
|
+
modelEligibleAccountIds?: ReadonlySet<string>,
|
|
57
|
+
) => string | null;
|
|
58
|
+
export type SubagentModelEligibleAccountIds = (
|
|
59
|
+
modelId: string | undefined,
|
|
60
|
+
) => ReadonlySet<string> | undefined;
|
|
51
61
|
let subagentQuotaPrimeForTests: SubagentQuotaPrimeFn | null = null;
|
|
52
62
|
let quotaPrimeInFlight: Promise<void> | null = null;
|
|
53
63
|
|
|
@@ -177,8 +187,15 @@ function resolveRouteFallbackAccountId(
|
|
|
177
187
|
route: RouteResult | null,
|
|
178
188
|
config: OcxConfig,
|
|
179
189
|
accountId?: string | null,
|
|
190
|
+
now = Date.now(),
|
|
191
|
+
poolAccountPreview?: SubagentPoolAccountPreview,
|
|
192
|
+
modelEligibleAccountIds?: ReadonlySet<string>,
|
|
180
193
|
): string | null {
|
|
181
|
-
|
|
194
|
+
if (route?.codexAccountId !== undefined) return route.codexAccountId;
|
|
195
|
+
if (route && isPoolCodexRoute(route) && poolAccountPreview) {
|
|
196
|
+
return poolAccountPreview(route.modelId, now, modelEligibleAccountIds);
|
|
197
|
+
}
|
|
198
|
+
return resolvePoolFallbackAccountId(config, accountId);
|
|
182
199
|
}
|
|
183
200
|
|
|
184
201
|
function isRoutableFallbackModel(model: string, config: OcxConfig): boolean {
|
|
@@ -237,33 +254,53 @@ export function isSubagentModelUnavailable(
|
|
|
237
254
|
accountId?: string | null,
|
|
238
255
|
now = Date.now(),
|
|
239
256
|
accountUsabilityOptions?: CodexAccountUsabilityOptions,
|
|
257
|
+
poolAccountPreview?: SubagentPoolAccountPreview,
|
|
258
|
+
modelEligibleAccountIdsForModel?: SubagentModelEligibleAccountIds,
|
|
240
259
|
): boolean {
|
|
241
260
|
if (isDisabledFallbackModel(model, config)) return true;
|
|
242
261
|
if (!isRoutableFallbackModel(model, config)) return true;
|
|
243
262
|
const route = tryRouteFallbackModel(config, model);
|
|
244
263
|
if (!route || route.provider.disabled === true) return true;
|
|
245
|
-
|
|
264
|
+
const modelEligibleAccountIds = modelEligibleAccountIdsForModel?.(route.modelId);
|
|
265
|
+
const candidateAccountUsabilityOptions = modelEligibleAccountIds !== undefined
|
|
266
|
+
? {
|
|
267
|
+
...accountUsabilityOptions,
|
|
268
|
+
modelEligibleAccountIds,
|
|
269
|
+
}
|
|
270
|
+
: accountUsabilityOptions;
|
|
271
|
+
const resolvedAccountId = resolveRouteFallbackAccountId(
|
|
272
|
+
route,
|
|
273
|
+
config,
|
|
274
|
+
accountId,
|
|
275
|
+
now,
|
|
276
|
+
poolAccountPreview,
|
|
277
|
+
candidateAccountUsabilityOptions?.modelEligibleAccountIds,
|
|
278
|
+
);
|
|
279
|
+
if (isModelHealthBlocked(model, config, resolvedAccountId, now)) return true;
|
|
246
280
|
if (!isPoolCodexRoute(route)) return false;
|
|
247
281
|
|
|
248
282
|
// Pool candidates need a usable account. Derive requirement from the resolved
|
|
249
283
|
// route (canonical openai defaults to pool even when codexAccountMode is omitted).
|
|
250
|
-
const resolvedAccountId = resolveRouteFallbackAccountId(route, config, accountId);
|
|
251
284
|
if (!resolvedAccountId) return true;
|
|
252
285
|
if (isCodexAccountPaused(config, resolvedAccountId)) return true;
|
|
253
|
-
if (!isCodexAccountUsable(config, resolvedAccountId,
|
|
286
|
+
if (!isCodexAccountUsable(config, resolvedAccountId, candidateAccountUsabilityOptions)) return true;
|
|
254
287
|
if (route.codexAccountId !== undefined) {
|
|
255
288
|
// An account-qualified route is pinned and cannot consume Pool's recovery-probe
|
|
256
289
|
// escape hatch. Honor both account-wide and model-scoped cooldowns so fallback
|
|
257
290
|
// advances instead of selecting a candidate that exact auth will reject.
|
|
258
291
|
const quotaScope = codexQuotaScopeForModel(route.modelId);
|
|
259
292
|
if (getCodexQuotaHealthSnapshot(resolvedAccountId, quotaScope, now) !== null) return true;
|
|
260
|
-
} else
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
293
|
+
} else {
|
|
294
|
+
const quotaScope = codexQuotaScopeForModel(route.modelId);
|
|
295
|
+
const cooldown = getCodexQuotaHealthSnapshot(resolvedAccountId, quotaScope, now);
|
|
296
|
+
if (cooldown !== null) {
|
|
297
|
+
const probeAvailable = cooldown.quotaScope
|
|
298
|
+
? canAcquireCodexQuotaScopeProbeLease(resolvedAccountId, cooldown.quotaScope, now)
|
|
299
|
+
: canAcquireCodexQuotaProbeLease(resolvedAccountId, now);
|
|
300
|
+
if (!probeAvailable) return true;
|
|
301
|
+
}
|
|
265
302
|
}
|
|
266
|
-
return isNativeModelQuotaExhausted(model, config,
|
|
303
|
+
return isNativeModelQuotaExhausted(model, config, resolvedAccountId, now);
|
|
267
304
|
}
|
|
268
305
|
|
|
269
306
|
export function selectAvailableSubagentModel(
|
|
@@ -275,8 +312,11 @@ export function selectAvailableSubagentModel(
|
|
|
275
312
|
nativeFallbackOnly = false,
|
|
276
313
|
accountUsabilityOptions?: CodexAccountUsabilityOptions,
|
|
277
314
|
trailingFallback: readonly string[] = [],
|
|
315
|
+
poolAccountPreview?: SubagentPoolAccountPreview,
|
|
316
|
+
modelEligibleAccountIdsForModel?: SubagentModelEligibleAccountIds,
|
|
317
|
+
resolvedChain?: readonly string[],
|
|
278
318
|
): { model: string; rewritten: boolean; skipped: string[] } {
|
|
279
|
-
const chain = normalizedChain(primary, config, extraFallback, trailingFallback);
|
|
319
|
+
const chain = resolvedChain ?? normalizedChain(primary, config, extraFallback, trailingFallback);
|
|
280
320
|
const skipped: string[] = [];
|
|
281
321
|
for (const candidate of chain) {
|
|
282
322
|
if (nativeFallbackOnly) {
|
|
@@ -286,7 +326,15 @@ export function selectAvailableSubagentModel(
|
|
|
286
326
|
continue;
|
|
287
327
|
}
|
|
288
328
|
}
|
|
289
|
-
if (isSubagentModelUnavailable(
|
|
329
|
+
if (isSubagentModelUnavailable(
|
|
330
|
+
candidate,
|
|
331
|
+
config,
|
|
332
|
+
accountId,
|
|
333
|
+
now,
|
|
334
|
+
accountUsabilityOptions,
|
|
335
|
+
poolAccountPreview,
|
|
336
|
+
modelEligibleAccountIdsForModel,
|
|
337
|
+
)) {
|
|
290
338
|
skipped.push(candidate);
|
|
291
339
|
continue;
|
|
292
340
|
}
|
|
@@ -295,6 +343,71 @@ export function selectAvailableSubagentModel(
|
|
|
295
343
|
return { model: primary, rewritten: false, skipped };
|
|
296
344
|
}
|
|
297
345
|
|
|
346
|
+
function isSubagentCandidateFailureMessage(message: string): boolean {
|
|
347
|
+
const normalized = String(message ?? "").trim();
|
|
348
|
+
if (!normalized) return false;
|
|
349
|
+
const lower = normalized.toLowerCase();
|
|
350
|
+
// Explicit client/transport errors must win over broad 5xx/network matches.
|
|
351
|
+
if (lower.includes("connection refused")) return false;
|
|
352
|
+
if (
|
|
353
|
+
lower.includes("invalid_request_error")
|
|
354
|
+
|| lower.includes("invalid request")
|
|
355
|
+
|| lower.includes("missing field")
|
|
356
|
+
) return false;
|
|
357
|
+
const numericStatus = Number(normalized);
|
|
358
|
+
if (Number.isInteger(numericStatus) && numericStatus >= 500 && numericStatus < 600) return true;
|
|
359
|
+
if (isRateLimitOrQuotaFailureMessage(normalized)) return true;
|
|
360
|
+
if (lower.includes("stream closed before response.completed")) return true;
|
|
361
|
+
if (lower.includes("upstream json response stalled before completing")) return true;
|
|
362
|
+
if (lower.includes("etimedout") || lower.includes("timed out") || lower.includes("timeout")) return true;
|
|
363
|
+
if (lower.includes("fetch failed") || lower.includes("network error")) return true;
|
|
364
|
+
if (lower.includes("provider error 503") || lower.includes("service unavailable")) return true;
|
|
365
|
+
if (lower.includes("internal server error")) return true;
|
|
366
|
+
// Connection-refused is a legacy transport signal and is intentionally not a cooldown.
|
|
367
|
+
if (/provider error 5\d\d/.test(lower)) return true;
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function resolveSubagentSpawnRoleFromHeaders(headers: Headers): string | undefined {
|
|
372
|
+
const headerRole = headers.get("x-codex-agent-role")?.trim();
|
|
373
|
+
if (headerRole) return headerRole;
|
|
374
|
+
const turnMeta = headers.get("x-codex-turn-metadata");
|
|
375
|
+
if (!turnMeta) return undefined;
|
|
376
|
+
try {
|
|
377
|
+
const parsed = JSON.parse(turnMeta) as { agent_role?: unknown };
|
|
378
|
+
const role = parsed.agent_role;
|
|
379
|
+
return typeof role === "string" && role.trim() !== "" ? role.trim() : undefined;
|
|
380
|
+
} catch {
|
|
381
|
+
return undefined;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function selectAvailableSubagentCandidate(
|
|
386
|
+
candidates: readonly string[],
|
|
387
|
+
config: OcxConfig,
|
|
388
|
+
accountId?: string | null,
|
|
389
|
+
now = Date.now(),
|
|
390
|
+
nativeFallbackOnly = false,
|
|
391
|
+
accountUsabilityOptions?: CodexAccountUsabilityOptions,
|
|
392
|
+
): { model: string | null; skipped: string[] } {
|
|
393
|
+
const skipped: string[] = [];
|
|
394
|
+
for (const candidate of candidates) {
|
|
395
|
+
if (nativeFallbackOnly) {
|
|
396
|
+
const route = tryRouteFallbackModel(config, candidate);
|
|
397
|
+
if (!route || !isCanonicalOpenAiForwardProvider(route.provider)) {
|
|
398
|
+
skipped.push(candidate);
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
if (isSubagentModelUnavailable(candidate, config, accountId, now, accountUsabilityOptions)) {
|
|
403
|
+
skipped.push(candidate);
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
return { model: candidate, skipped };
|
|
407
|
+
}
|
|
408
|
+
return { model: null, skipped };
|
|
409
|
+
}
|
|
410
|
+
|
|
298
411
|
export function noteSubagentModelFailure(
|
|
299
412
|
model: string,
|
|
300
413
|
message: string,
|
|
@@ -304,7 +417,7 @@ export function noteSubagentModelFailure(
|
|
|
304
417
|
ttlMs?: number,
|
|
305
418
|
): void {
|
|
306
419
|
const interval = ttlMs ?? DEFAULT_SUBAGENT_MODEL_FALLBACK_POLL_MS;
|
|
307
|
-
if (!
|
|
420
|
+
if (!isSubagentCandidateFailureMessage(message)) return;
|
|
308
421
|
const route = tryRouteFallbackModel(config, model);
|
|
309
422
|
const poolScoped = !!route && isPoolCodexRoute(route);
|
|
310
423
|
modelHealth.set(
|
|
@@ -507,6 +620,8 @@ export function recordSubagentQuotaFailureForThreadSpawn(
|
|
|
507
620
|
noteSubagentModelFailure(model, String(message), config, accountId, now, pollIntervalMs(config));
|
|
508
621
|
}
|
|
509
622
|
|
|
623
|
+
export const recordSubagentFailureForThreadSpawn = recordSubagentQuotaFailureForThreadSpawn;
|
|
624
|
+
|
|
510
625
|
export function applySubagentModelFallback(
|
|
511
626
|
parsed: OcxParsedRequest,
|
|
512
627
|
headers: Headers,
|
|
@@ -515,27 +630,52 @@ export function applySubagentModelFallback(
|
|
|
515
630
|
now = Date.now(),
|
|
516
631
|
nativeFallbackOnly = false,
|
|
517
632
|
accountUsabilityOptions?: CodexAccountUsabilityOptions,
|
|
633
|
+
poolAccountPreview?: SubagentPoolAccountPreview,
|
|
634
|
+
modelEligibleAccountIdsForModel?: SubagentModelEligibleAccountIds,
|
|
635
|
+
resolvedFallbackChain?: readonly string[] | null,
|
|
518
636
|
): { from?: string; to?: string; skipped?: string[] } | null {
|
|
519
637
|
if (!isThreadSpawnRequest(headers)) return null;
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
config.
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
638
|
+
|
|
639
|
+
if (config.subagentCandidates !== undefined) {
|
|
640
|
+
const role = resolveSubagentSpawnRoleFromHeaders(headers);
|
|
641
|
+
const candidates = resolveSubagentCandidates(config, role ?? parsed.modelId);
|
|
642
|
+
if (candidates.length > 0) {
|
|
643
|
+
const requested = parsed.modelId;
|
|
644
|
+
const selection = selectAvailableSubagentCandidate(
|
|
645
|
+
candidates,
|
|
646
|
+
config,
|
|
647
|
+
accountId,
|
|
648
|
+
now,
|
|
649
|
+
nativeFallbackOnly,
|
|
650
|
+
accountUsabilityOptions,
|
|
651
|
+
);
|
|
652
|
+
if (selection.model && !slugsEquivalent(selection.model, requested)) {
|
|
653
|
+
rewriteParsedModel(parsed, selection.model);
|
|
654
|
+
return { from: requested, to: selection.model, skipped: selection.skipped };
|
|
655
|
+
}
|
|
656
|
+
if (selection.skipped.length > 0) {
|
|
657
|
+
return { from: requested, to: requested, skipped: selection.skipped };
|
|
658
|
+
}
|
|
659
|
+
return null;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const fallbackChain = resolvedFallbackChain === undefined
|
|
664
|
+
? resolveSubagentFallbackChain(parsed, config)
|
|
665
|
+
: resolvedFallbackChain;
|
|
666
|
+
if (!fallbackChain) return null;
|
|
530
667
|
const selection = selectAvailableSubagentModel(
|
|
531
668
|
parsed.modelId,
|
|
532
669
|
config,
|
|
533
|
-
|
|
670
|
+
[],
|
|
534
671
|
accountId,
|
|
535
672
|
now,
|
|
536
673
|
nativeFallbackOnly,
|
|
537
674
|
accountUsabilityOptions,
|
|
538
|
-
|
|
675
|
+
[],
|
|
676
|
+
poolAccountPreview,
|
|
677
|
+
modelEligibleAccountIdsForModel,
|
|
678
|
+
fallbackChain,
|
|
539
679
|
);
|
|
540
680
|
if (!selection.rewritten) return selection.skipped.length > 0
|
|
541
681
|
? { from: parsed.modelId, to: parsed.modelId, skipped: selection.skipped }
|
|
@@ -545,6 +685,37 @@ export function applySubagentModelFallback(
|
|
|
545
685
|
return { from, to: selection.model, skipped: selection.skipped };
|
|
546
686
|
}
|
|
547
687
|
|
|
688
|
+
/** Resolve the effective fallback chain once for one logical spawn request. */
|
|
689
|
+
export function resolveSubagentFallbackChain(
|
|
690
|
+
parsed: OcxParsedRequest,
|
|
691
|
+
config: OcxConfig,
|
|
692
|
+
): readonly string[] | null {
|
|
693
|
+
const tomlRoleFallback = resolveAgentModelFallbackForPrimary(
|
|
694
|
+
parsed.modelId,
|
|
695
|
+
getCodexHome(),
|
|
696
|
+
config.codexAccountNamespaces,
|
|
697
|
+
);
|
|
698
|
+
// Config-keyed chains are the supported per-role home (#1190); TOML `model_fallback`
|
|
699
|
+
// stays readable for backwards compatibility with homes written before Codex 0.146.
|
|
700
|
+
const configuredFallback = resolveConfiguredModelFallbackForPrimary(parsed.modelId, config);
|
|
701
|
+
const globalFallback = config.subagentModelFallback ?? [];
|
|
702
|
+
if (globalFallback.length === 0 && configuredFallback.length === 0 && tomlRoleFallback.length === 0) return null;
|
|
703
|
+
return normalizedChain(parsed.modelId, config, configuredFallback, tomlRoleFallback);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/** Whether the effective fallback chain crosses an account-gated native Pool model. */
|
|
707
|
+
export function subagentFallbackNeedsModelEntitlements(
|
|
708
|
+
fallbackChain: readonly string[] | null,
|
|
709
|
+
config: OcxConfig,
|
|
710
|
+
): boolean {
|
|
711
|
+
return fallbackChain?.some((candidate) => {
|
|
712
|
+
const route = tryRouteFallbackModel(config, candidate);
|
|
713
|
+
return !!route
|
|
714
|
+
&& isPoolCodexRoute(route)
|
|
715
|
+
&& ACCOUNT_GATED_NATIVE_OPENAI_MODELS.has(route.modelId);
|
|
716
|
+
}) === true;
|
|
717
|
+
}
|
|
718
|
+
|
|
548
719
|
export function subagentFallbackGuidanceText(config: OcxConfig): string {
|
|
549
720
|
const chain = config.subagentModelFallback ?? [];
|
|
550
721
|
if (chain.length === 0) return "";
|
|
@@ -10,7 +10,17 @@
|
|
|
10
10
|
* Design record: devlog/_fin/260804_codex_write_substrate/005_contract.md §1.
|
|
11
11
|
*/
|
|
12
12
|
import { randomUUID } from "node:crypto";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
chmodSync,
|
|
15
|
+
closeSync,
|
|
16
|
+
fsyncSync,
|
|
17
|
+
linkSync,
|
|
18
|
+
lstatSync,
|
|
19
|
+
openSync,
|
|
20
|
+
realpathSync,
|
|
21
|
+
unlinkSync,
|
|
22
|
+
} from "node:fs";
|
|
23
|
+
import { basename, dirname, join } from "node:path";
|
|
14
24
|
|
|
15
25
|
import { Database } from "bun:sqlite";
|
|
16
26
|
|
|
@@ -38,7 +48,7 @@ import {
|
|
|
38
48
|
} from "./user-identity";
|
|
39
49
|
|
|
40
50
|
export const CODEX_COORDINATOR_SCHEMA_VERSION = 1;
|
|
41
|
-
const DURABLE_HISTORY_STATUSES = new Set(["converged", "pending", "running", "blocked", "unknown"]);
|
|
51
|
+
const DURABLE_HISTORY_STATUSES = new Set(["adoption-pending", "converged", "pending", "running", "blocked", "unknown"]);
|
|
42
52
|
const DURABLE_HISTORY_REASONS = new Set([
|
|
43
53
|
"db-busy",
|
|
44
54
|
"permission",
|
|
@@ -66,7 +76,7 @@ const CREATE_TRANSITION_TABLE = `
|
|
|
66
76
|
history_pending_rows INTEGER,
|
|
67
77
|
history_backup_entries INTEGER,
|
|
68
78
|
updated_at TEXT NOT NULL,
|
|
69
|
-
CHECK (history_status IN ('converged', 'pending', 'running', 'blocked', 'unknown')),
|
|
79
|
+
CHECK (history_status IN ('adoption-pending', 'converged', 'pending', 'running', 'blocked', 'unknown')),
|
|
70
80
|
CHECK (history_reason IS NULL OR history_reason IN
|
|
71
81
|
('db-busy', 'permission', 'unreadable', 'schema', 'timeout',
|
|
72
82
|
'shutdown-cancelled', 'worker-died', 'overtaken', 'record-write-failed')),
|
|
@@ -75,14 +85,20 @@ const CREATE_TRANSITION_TABLE = `
|
|
|
75
85
|
CHECK ((native_generation = 0 AND current_tx_id IS NULL)
|
|
76
86
|
OR (native_generation > 0 AND length(trim(current_tx_id)) > 0)),
|
|
77
87
|
CHECK ((native_generation = 0
|
|
88
|
+
AND history_status = 'unknown'
|
|
78
89
|
AND history_tx_id IS NULL
|
|
79
90
|
AND history_direction IS NULL
|
|
80
91
|
AND history_authority_snapshot_id IS NULL)
|
|
92
|
+
OR (native_generation = 0
|
|
93
|
+
AND history_status = 'adoption-pending'
|
|
94
|
+
AND length(trim(history_tx_id)) > 0
|
|
95
|
+
AND history_direction IS NOT NULL
|
|
96
|
+
AND length(trim(history_authority_snapshot_id)) > 0)
|
|
81
97
|
OR (native_generation > 0
|
|
82
98
|
AND history_tx_id = current_tx_id
|
|
83
99
|
AND history_direction IS NOT NULL
|
|
84
100
|
AND length(trim(history_authority_snapshot_id)) > 0)),
|
|
85
|
-
CHECK (native_generation > 0 OR
|
|
101
|
+
CHECK (native_generation > 0 OR history_status = 'adoption-pending' OR
|
|
86
102
|
(history_status = 'unknown'
|
|
87
103
|
AND history_reason IS NULL
|
|
88
104
|
AND history_attempts = 0
|
|
@@ -100,6 +116,15 @@ const INITIALIZE_TRANSITION_ROW = `
|
|
|
100
116
|
history_backup_entries, updated_at
|
|
101
117
|
) VALUES (1, 0, NULL, 'unknown', NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, ?)`;
|
|
102
118
|
|
|
119
|
+
const INITIALIZE_ADOPTION_ROW = `
|
|
120
|
+
INSERT INTO codex_transition_state (
|
|
121
|
+
singleton, native_generation, current_tx_id,
|
|
122
|
+
history_status, history_reason, history_attempts,
|
|
123
|
+
history_next_retry_at, history_tx_id, history_direction,
|
|
124
|
+
history_authority_snapshot_id, history_pending_rows,
|
|
125
|
+
history_backup_entries, updated_at
|
|
126
|
+
) VALUES (1, 0, NULL, 'adoption-pending', NULL, 0, NULL, ?, ?, ?, NULL, NULL, ?)`;
|
|
127
|
+
|
|
103
128
|
const SELECT_TRANSITION_ROW = `
|
|
104
129
|
SELECT native_generation, current_tx_id,
|
|
105
130
|
history_status, history_reason, history_attempts,
|
|
@@ -210,8 +235,15 @@ function rowToState(row: TransitionRow | null): CodexTransitionState {
|
|
|
210
235
|
|
|
211
236
|
const generation = row.native_generation;
|
|
212
237
|
if (generation === 0) {
|
|
213
|
-
|
|
214
|
-
|
|
238
|
+
const ordinaryInitial = row.history_status === "unknown"
|
|
239
|
+
&& row.history_tx_id === null
|
|
240
|
+
&& row.history_direction === null
|
|
241
|
+
&& row.history_authority_snapshot_id === null;
|
|
242
|
+
const adoptionInitial = row.history_status === "adoption-pending"
|
|
243
|
+
&& Boolean(row.history_tx_id?.trim())
|
|
244
|
+
&& (row.history_direction === "apply" || row.history_direction === "remove")
|
|
245
|
+
&& Boolean(row.history_authority_snapshot_id?.trim());
|
|
246
|
+
if (row.current_tx_id !== null || (!ordinaryInitial && !adoptionInitial)) {
|
|
215
247
|
throw new CodexCoordinatorTransactionError("The initial coordinator row contains transition metadata.");
|
|
216
248
|
}
|
|
217
249
|
} else if (!row.current_tx_id?.trim()
|
|
@@ -234,13 +266,73 @@ function rowToState(row: TransitionRow | null): CodexTransitionState {
|
|
|
234
266
|
nativeGeneration: generation,
|
|
235
267
|
currentTxId: row.current_tx_id,
|
|
236
268
|
history,
|
|
237
|
-
historySchedule: generation === 0 ? null : {
|
|
269
|
+
historySchedule: generation === 0 && row.history_status !== "adoption-pending" ? null : {
|
|
238
270
|
direction: row.history_direction as "apply" | "remove",
|
|
239
271
|
authoritySnapshotId: row.history_authority_snapshot_id as string,
|
|
240
272
|
},
|
|
241
273
|
};
|
|
242
274
|
}
|
|
243
275
|
|
|
276
|
+
export type CodexCoordinatorAdoptionCheckpoint = "temp-created" | "temp-committed" | "published";
|
|
277
|
+
|
|
278
|
+
export interface CodexCoordinatorAdoptionOptions {
|
|
279
|
+
readonly direction: "apply" | "remove";
|
|
280
|
+
readonly onCheckpoint?: (checkpoint: CodexCoordinatorAdoptionCheckpoint) => void;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function fsyncPath(path: string): void {
|
|
284
|
+
const fd = openSync(path, "r");
|
|
285
|
+
try { fsyncSync(fd); } finally { closeSync(fd); }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function publishAdoptionDatabase(
|
|
289
|
+
finalDatabasePath: string,
|
|
290
|
+
options: CodexCoordinatorAdoptionOptions,
|
|
291
|
+
): void {
|
|
292
|
+
const parent = dirname(finalDatabasePath);
|
|
293
|
+
const tempPath = join(parent, `.${basename(finalDatabasePath)}.adopt-${process.pid}-${randomUUID()}.tmp`);
|
|
294
|
+
let tempPresent = false;
|
|
295
|
+
try {
|
|
296
|
+
const fd = openSync(tempPath, "wx", 0o600);
|
|
297
|
+
closeSync(fd);
|
|
298
|
+
tempPresent = true;
|
|
299
|
+
options.onCheckpoint?.("temp-created");
|
|
300
|
+
|
|
301
|
+
const temp = new Database(tempPath, { readwrite: true, create: false });
|
|
302
|
+
try {
|
|
303
|
+
temp.exec("PRAGMA journal_mode = DELETE; BEGIN IMMEDIATE");
|
|
304
|
+
temp.exec(CREATE_TRANSITION_TABLE);
|
|
305
|
+
temp.query(INITIALIZE_ADOPTION_ROW).run(
|
|
306
|
+
randomUUID(),
|
|
307
|
+
options.direction,
|
|
308
|
+
randomUUID(),
|
|
309
|
+
new Date().toISOString(),
|
|
310
|
+
);
|
|
311
|
+
temp.exec(`PRAGMA user_version = ${CODEX_COORDINATOR_SCHEMA_VERSION}; COMMIT`);
|
|
312
|
+
readCodexCoordinatorState(temp);
|
|
313
|
+
} catch (error) {
|
|
314
|
+
try { temp.exec("ROLLBACK"); } catch { /* commit may already have completed */ }
|
|
315
|
+
throw error;
|
|
316
|
+
} finally {
|
|
317
|
+
temp.close();
|
|
318
|
+
}
|
|
319
|
+
fsyncPath(tempPath);
|
|
320
|
+
options.onCheckpoint?.("temp-committed");
|
|
321
|
+
|
|
322
|
+
linkSync(tempPath, finalDatabasePath);
|
|
323
|
+
options.onCheckpoint?.("published");
|
|
324
|
+
if (process.platform !== "win32") fsyncPath(parent);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
if (errorCode(error) !== "EEXIST") throw error;
|
|
327
|
+
} finally {
|
|
328
|
+
if (tempPresent) {
|
|
329
|
+
try { unlinkSync(tempPath); } catch (error) {
|
|
330
|
+
if (errorCode(error) !== "ENOENT") throw error;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
244
336
|
export function readCodexCoordinatorState(database: Database): CodexTransitionState {
|
|
245
337
|
const row = database.query<TransitionRow, []>(SELECT_TRANSITION_ROW).get();
|
|
246
338
|
return rowToState(row);
|
|
@@ -346,7 +438,10 @@ function createCapability(
|
|
|
346
438
|
};
|
|
347
439
|
}
|
|
348
440
|
|
|
349
|
-
export function openCodexCoordinatorTransaction(
|
|
441
|
+
export function openCodexCoordinatorTransaction(
|
|
442
|
+
finalDatabasePath: string,
|
|
443
|
+
adoption?: CodexCoordinatorAdoptionOptions,
|
|
444
|
+
): CodexCoordinatorTransactionController {
|
|
350
445
|
let database: Database | undefined;
|
|
351
446
|
let transactionOpen = false;
|
|
352
447
|
let closed = false;
|
|
@@ -389,6 +484,10 @@ export function openCodexCoordinatorTransaction(finalDatabasePath: string): Code
|
|
|
389
484
|
if (errorCode(cause) !== "ENOENT") throw cause;
|
|
390
485
|
databaseWasAbsent = true;
|
|
391
486
|
}
|
|
487
|
+
if (databaseWasAbsent && adoption) {
|
|
488
|
+
publishAdoptionDatabase(finalDatabasePath, adoption);
|
|
489
|
+
databaseWasAbsent = false;
|
|
490
|
+
}
|
|
392
491
|
database = new Database(finalDatabasePath, { create: true });
|
|
393
492
|
if (databaseWasAbsent) {
|
|
394
493
|
try { chmodSync(finalDatabasePath, 0o600); } catch { /* Windows applies ACLs in WP11. */ }
|
package/src/combos/types.ts
CHANGED
|
@@ -203,6 +203,16 @@ export function comboConfigIssues(
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
const body = raw as Record<string, unknown>;
|
|
206
|
+
if (typeof body.alias === "string") {
|
|
207
|
+
const alias = body.alias.toLowerCase();
|
|
208
|
+
for (const [providerName, provider] of Object.entries(providers)) {
|
|
209
|
+
if (provider.alias?.toLowerCase() === alias) {
|
|
210
|
+
issues.push({ path: ["alias"], message: `alias "${body.alias}" is already used by provider "${providerName}"` });
|
|
211
|
+
}
|
|
212
|
+
const model = Object.entries(provider.modelAliases ?? {}).find(([, value]) => value.toLowerCase() === alias);
|
|
213
|
+
if (model) issues.push({ path: ["alias"], message: `alias "${body.alias}" is already used by model "${providerName}/${model[0]}"` });
|
|
214
|
+
}
|
|
215
|
+
}
|
|
206
216
|
if (body.strategy !== undefined
|
|
207
217
|
&& body.strategy !== "failover"
|
|
208
218
|
&& body.strategy !== "round-robin") {
|
|
@@ -9,7 +9,7 @@ const FIXTURE_ID = "openai-codex-forward-gpt56-sol-v1";
|
|
|
9
9
|
export const OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST = defineCompatibilityManifest({
|
|
10
10
|
schemaVersion: 1,
|
|
11
11
|
id: "openai.codex-forward.gpt-5-6-sol.responses",
|
|
12
|
-
version: "1.
|
|
12
|
+
version: "1.2.0",
|
|
13
13
|
subject: {
|
|
14
14
|
providerId: "openai",
|
|
15
15
|
baseUrl: "https://chatgpt.com/backend-api/codex",
|
|
@@ -69,6 +69,38 @@ export const OPENAI_CODEX_FORWARD_GPT56_SOL_MANIFEST = defineCompatibilityManife
|
|
|
69
69
|
limitation: "The field is removed before dispatch.",
|
|
70
70
|
evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["metadata-removed"] }],
|
|
71
71
|
},
|
|
72
|
+
{
|
|
73
|
+
id: "system-input-messages",
|
|
74
|
+
feature: "request.input.system_messages",
|
|
75
|
+
disposition: "translated",
|
|
76
|
+
summary: "Text-only input system messages are folded into top-level instructions.",
|
|
77
|
+
limitation: "The destination does not accept system-role input items; multimodal system messages stay unchanged rather than being dropped.",
|
|
78
|
+
evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["system-message-folded", "system-message-removed"] }],
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
id: "truncation",
|
|
82
|
+
feature: "request.truncation",
|
|
83
|
+
disposition: "unsupported",
|
|
84
|
+
summary: "The ChatGPT Codex forward route does not receive truncation.",
|
|
85
|
+
limitation: "The field is removed only for the canonical forward destination; public and custom Responses providers keep it.",
|
|
86
|
+
evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["truncation-removed"] }],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
id: "prompt-cache-breakpoint",
|
|
90
|
+
feature: "request.prompt_cache_breakpoint",
|
|
91
|
+
disposition: "unsupported",
|
|
92
|
+
summary: "Client-only prompt cache breakpoint markers do not reach the forward backend.",
|
|
93
|
+
limitation: "Markers are removed recursively from input within bounded depth and node budgets.",
|
|
94
|
+
evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["prompt-cache-breakpoint-removed"] }],
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
id: "unstored-item-reference",
|
|
98
|
+
feature: "continuation.item_reference",
|
|
99
|
+
disposition: "degraded",
|
|
100
|
+
summary: "Unpersisted item references are omitted from store-false continuations.",
|
|
101
|
+
limitation: "The referenced item cannot exist at the backend when store is false; tool call_id pairs remain unchanged.",
|
|
102
|
+
evidence: [{ kind: "fixture", id: FIXTURE_ID, assertionIds: ["unstored-item-reference-removed"] }],
|
|
103
|
+
},
|
|
72
104
|
{
|
|
73
105
|
id: "prompt-cache-retention",
|
|
74
106
|
feature: "request.prompt_cache_retention",
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface AutonomousRemediationConfig {
|
|
2
|
+
enabled: boolean;
|
|
3
|
+
instanceId?: string;
|
|
4
|
+
threshold: number;
|
|
5
|
+
rollingWindowMs: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const DEFAULT_THRESHOLD = 3;
|
|
9
|
+
const DEFAULT_WINDOW_MS = 86_400_000;
|
|
10
|
+
|
|
11
|
+
export function resolveAutonomousRemediationConfig(value: unknown): AutonomousRemediationConfig {
|
|
12
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
13
|
+
return { enabled: false, instanceId: undefined, threshold: DEFAULT_THRESHOLD, rollingWindowMs: DEFAULT_WINDOW_MS };
|
|
14
|
+
}
|
|
15
|
+
const input = value as Record<string, unknown>;
|
|
16
|
+
const enabled = input.enabled === true;
|
|
17
|
+
const instanceId = typeof input.instanceId === "string" && input.instanceId.trim() ? input.instanceId.trim() : undefined;
|
|
18
|
+
const threshold = typeof input.threshold === "number" && Number.isInteger(input.threshold) && input.threshold > 0 ? input.threshold : DEFAULT_THRESHOLD;
|
|
19
|
+
const rollingWindowMs = typeof input.rollingWindowMs === "number" && Number.isInteger(input.rollingWindowMs) && input.rollingWindowMs > 0 ? input.rollingWindowMs : DEFAULT_WINDOW_MS;
|
|
20
|
+
return { enabled, instanceId, threshold, rollingWindowMs };
|
|
21
|
+
}
|
|
@@ -212,3 +212,17 @@ export function modelAdapterRecordConfigError(
|
|
|
212
212
|
}
|
|
213
213
|
return null;
|
|
214
214
|
}
|
|
215
|
+
|
|
216
|
+
export function wsUpstreamConfigError(value: unknown): string | null {
|
|
217
|
+
if (value === undefined || value === null) return null;
|
|
218
|
+
if (typeof value !== "boolean") return "wsUpstream must be a boolean";
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function maxWsFrameBytesConfigError(value: unknown): string | null {
|
|
223
|
+
if (value === undefined || value === null) return null;
|
|
224
|
+
if (typeof value !== "number" || !Number.isFinite(value) || !Number.isInteger(value) || value <= 0) {
|
|
225
|
+
return "maxWsFrameBytes must be a positive finite integer";
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
}
|