@bitkyc08/opencodex 2.42.0 → 2.43.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/AGENTS_INSTALL.md +2 -2
- package/README.md +31 -0
- package/bin/ocx.mjs +10 -1
- package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
- package/gui/dist/assets/index-Djowl68T.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-codec.ts +304 -0
- package/src/adapters/anthropic-image-normalize.ts +8 -298
- package/src/adapters/anthropic.ts +30 -7
- package/src/adapters/command-code.ts +7 -5
- package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
- package/src/adapters/cursor/images.ts +36 -6
- package/src/adapters/cursor/live-transport.ts +7 -2
- package/src/adapters/cursor/native-exec-desktop.ts +2 -15
- package/src/adapters/cursor/protobuf-request.ts +13 -3
- package/src/adapters/cursor/tool-definitions.ts +5 -670
- package/src/adapters/cursor/tool-guidance.ts +236 -0
- package/src/adapters/cursor/tool-naming.ts +252 -0
- package/src/adapters/cursor/tool-schemas.ts +195 -0
- package/src/adapters/cursor/types.ts +6 -3
- package/src/adapters/exec-tool-result-normalize.ts +1 -1
- package/src/adapters/google-errors.ts +9 -1
- package/src/adapters/google.ts +1 -0
- package/src/adapters/kiro-calibration.ts +181 -0
- package/src/adapters/kiro.ts +135 -3
- package/src/adapters/openai-responses.ts +216 -26
- package/src/adapters/responses-code-mode.ts +59 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-schema-analysis.ts +86 -0
- package/src/adapters/xai-tool-schema.ts +2 -87
- package/src/adapters/xai-web-search.ts +1 -1
- package/src/bridge.ts +22 -10
- package/src/chat/inbound.ts +11 -3
- package/src/claude/inbound-content-options.ts +60 -0
- package/src/claude/inbound-model-options.ts +142 -0
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +10 -202
- package/src/claude/model-info.ts +45 -0
- package/src/cli/account-auth.ts +21 -6
- package/src/cli/capabilities.ts +13 -2
- package/src/cli/claude.ts +232 -39
- package/src/cli/config-command.ts +9 -1
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/effort.ts +372 -0
- package/src/cli/export-command.ts +3 -9
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/init.ts +4 -0
- package/src/cli/model-selection-guidance.ts +30 -0
- package/src/cli/models-runtime.ts +3 -2
- package/src/cli/models.ts +8 -3
- package/src/cli/opencode.ts +4 -1
- package/src/cli/provider-runtime.ts +65 -0
- package/src/cli/provider.ts +8 -0
- package/src/cli/registry.ts +16 -2
- package/src/cli/runtime-api.ts +3 -1
- package/src/cli/star-prompt.ts +22 -6
- package/src/cli/status-probes.ts +168 -0
- package/src/cli/status.ts +5 -168
- package/src/clients/config-export/constants.ts +69 -0
- package/src/clients/config-export/contracts.ts +154 -0
- package/src/clients/config-export/dsh.ts +132 -0
- package/src/clients/config-export/fast-models.ts +29 -0
- package/src/clients/config-export/mcode.ts +83 -0
- package/src/clients/config-export/model-metadata.ts +108 -0
- package/src/clients/config-export/omp.ts +104 -0
- package/src/clients/config-export/zcode.ts +92 -0
- package/src/clients/config-export.ts +18 -710
- package/src/codex/account-lifecycle.ts +20 -3
- package/src/codex/account-usability.ts +2 -0
- package/src/codex/auth-api.ts +170 -23
- package/src/codex/auth-context.ts +328 -24
- package/src/codex/catalog/effort.ts +30 -4
- package/src/codex/catalog/metadata.ts +39 -9
- package/src/codex/catalog/native-models.ts +53 -12
- package/src/codex/catalog/parsing.ts +119 -5
- package/src/codex/catalog/provider-fetch.ts +150 -20
- package/src/codex/catalog/reserve.ts +52 -0
- package/src/codex/catalog/sync.ts +89 -16
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/convergence.ts +2 -0
- package/src/codex/data/upstream-models.json +1 -1
- package/src/codex/forward-transport-headers.ts +25 -0
- package/src/codex/inject.ts +3 -28
- package/src/codex/legacy-config-keys.ts +68 -0
- package/src/codex/log-guard/inspect-schema.ts +137 -0
- package/src/codex/log-guard/inspect.ts +2 -134
- package/src/codex/loopback-target.ts +54 -0
- package/src/codex/main-account-cache.ts +63 -1
- package/src/codex/main-account-hard-lock.ts +52 -0
- package/src/codex/main-account.ts +3 -1
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/model-entitlements.ts +54 -4
- package/src/codex/project-config-warnings.ts +92 -2
- package/src/codex/prompt-layers/encoding.ts +80 -0
- package/src/codex/prompt-layers/paths.ts +54 -0
- package/src/codex/prompt-layers/revision.ts +55 -0
- package/src/codex/prompt-layers/toml-edit.ts +163 -0
- package/src/codex/prompt-layers/toml-read.ts +181 -0
- package/src/codex/prompt-layers.ts +14 -520
- package/src/codex/quota-auto-refresh-state.ts +16 -0
- package/src/codex/quota-auto-refresh.ts +219 -0
- package/src/codex/quota-types.ts +51 -0
- package/src/codex/quota.ts +252 -93
- package/src/codex/reserve-availability.ts +177 -0
- package/src/codex/routing.ts +28 -9
- package/src/codex/shim.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +23 -3
- package/src/combos/failover.ts +125 -7
- package/src/combos/identifiers.ts +89 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/resolve.ts +80 -9
- package/src/combos/types.ts +20 -93
- package/src/config/subagent-models.ts +24 -0
- package/src/config.ts +156 -13
- package/src/generated/compatibility-version.json +454 -166
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/journal.ts +65 -4
- package/src/integrations/store.ts +5 -0
- package/src/lab/events/limits.ts +4 -0
- package/src/lib/destination-policy.ts +31 -2
- package/src/lib/errors.ts +39 -0
- package/src/lib/provider-outbound.ts +69 -3
- package/src/lib/proxy-env.ts +22 -0
- package/src/lib/redact-folding.ts +176 -0
- package/src/lib/redact.ts +2 -175
- package/src/lib/state-store-sweeper.ts +20 -6
- package/src/lib/token-estimate.ts +94 -27
- package/src/lib/windows-user-principal.ts +53 -5
- package/src/oauth/anthropic-routing.ts +99 -3
- package/src/oauth/generic-account-failover.ts +36 -13
- package/src/oauth/index.ts +127 -26
- package/src/oauth/login-cli.ts +5 -0
- package/src/oauth/meta-muse.ts +117 -15
- package/src/oauth/pool-settings-capability.ts +15 -4
- package/src/providers/api-keys.ts +8 -10
- package/src/providers/default-aliases.ts +39 -0
- package/src/providers/derive.ts +10 -2
- package/src/providers/fastwire.ts +36 -7
- package/src/providers/initial-model-selection-runtime.ts +90 -0
- package/src/providers/initial-model-selection.ts +120 -0
- package/src/providers/key-failover.ts +134 -54
- package/src/providers/key-store.ts +11 -1
- package/src/providers/label.ts +1 -1
- package/src/providers/model-discovery.ts +76 -0
- package/src/providers/model-rename-startup.ts +72 -8
- package/src/providers/openai-sidecar.ts +17 -5
- package/src/providers/openai-tiers-destination.ts +102 -0
- package/src/providers/openai-tiers.ts +2 -99
- package/src/providers/opencode-go-transport.ts +41 -0
- package/src/providers/quota-key-accounts.ts +141 -0
- package/src/providers/quota-types.ts +9 -0
- package/src/providers/quota.ts +535 -104
- package/src/providers/registry.ts +60 -17
- package/src/providers/xai-responses-opt-in.ts +31 -5
- package/src/quota/reset-activation.ts +81 -0
- package/src/quota/reset-detector.ts +305 -0
- package/src/quota/reset-notify-config.ts +162 -0
- package/src/quota/reset-observer.ts +125 -0
- package/src/quota/reset-poller.ts +160 -0
- package/src/quota/reset-seen-store.ts +385 -0
- package/src/quota/reset-sinks.ts +199 -0
- package/src/quota/window-mapping.ts +106 -0
- package/src/responses/apply-patch-envelope.ts +46 -0
- package/src/responses/code-mode-helper-compat.ts +39 -1
- package/src/responses/custom-tool-compat.ts +10 -4
- package/src/responses/hosted-tool-policy.ts +12 -4
- package/src/responses/parser-content.ts +133 -0
- package/src/responses/parser-text-format.ts +24 -0
- package/src/responses/parser-tools.ts +188 -0
- package/src/responses/parser.ts +3 -326
- package/src/responses/state.ts +124 -28
- package/src/router.ts +48 -13
- package/src/routing/analytics.ts +1 -0
- package/src/routing/capability.ts +17 -4
- package/src/server/auth-cors.ts +7 -1
- package/src/server/background-lifecycle.ts +23 -1
- package/src/server/chat-completions.ts +25 -3
- package/src/server/claude-messages.ts +62 -5
- package/src/server/effort-row.ts +1 -1
- package/src/server/fast-row.ts +295 -0
- package/src/server/gui-static.ts +30 -4
- package/src/server/index.ts +107 -21
- package/src/server/management/agent-settings-routes.ts +2 -2
- package/src/server/management/combo-routes.ts +37 -9
- package/src/server/management/config-routes.ts +93 -2
- package/src/server/management/integration-routes.ts +108 -0
- package/src/server/management/model-routes.ts +13 -3
- package/src/server/management/model-rows.ts +20 -1
- package/src/server/management/native-integration-routes.ts +4 -1
- package/src/server/management/oauth-account-routes.ts +38 -10
- package/src/server/management/provider-routes.ts +34 -3
- package/src/server/management/quota-reset-routes.ts +57 -0
- package/src/server/management/route-registry.ts +7 -4
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/system-routes.ts +3 -2
- package/src/server/management-api.ts +14 -2
- package/src/server/ports.ts +12 -2
- package/src/server/relay-eager.ts +38 -23
- package/src/server/relay.ts +4 -0
- package/src/server/request-log.ts +6 -0
- package/src/server/responses/codex-ws-correlation.ts +65 -0
- package/src/server/responses/codex-ws-exchange.ts +261 -0
- package/src/server/responses/codex-ws-metadata.ts +134 -0
- package/src/server/responses/codex-ws-pool.ts +162 -0
- package/src/server/responses/codex-ws-request.ts +87 -0
- package/src/server/responses/codex-ws-session.ts +93 -0
- package/src/server/responses/codex-ws-wire.ts +144 -0
- package/src/server/responses/collaboration.ts +41 -1
- package/src/server/responses/compact.ts +105 -12
- package/src/server/responses/core.ts +476 -57
- package/src/server/responses/fetch-helpers.ts +10 -3
- package/src/server/responses/input-admission.ts +16 -9
- package/src/server/responses/responses-field-backfill.ts +1 -1
- package/src/server/responses/ws-upstream.ts +34 -318
- package/src/server/responses-custom-tool-repair.ts +20 -4
- package/src/server/responses-undeclared-tool-guard.ts +100 -8
- package/src/server/safe-response-headers.ts +23 -0
- package/src/server/search.ts +9 -0
- package/src/server/subagent-models-startup.ts +27 -0
- package/src/server/system-env-shell.ts +238 -0
- package/src/server/system-env.ts +7 -234
- package/src/server/ws-bridge.ts +3 -25
- package/src/server/xai-responses-startup.ts +21 -0
- package/src/service-manager-probe.ts +1 -1
- package/src/service.ts +55 -16
- package/src/types/config.ts +108 -12
- package/src/types/provider.ts +36 -7
- package/src/types/tools.ts +26 -1
- package/src/types.ts +2 -0
- package/src/update/notify.ts +8 -2
- package/src/usage/cost.ts +38 -28
- package/src/usage/expected-prices.ts +34 -15
- package/src/usage/log.ts +2 -0
- package/src/usage/model-identity.ts +26 -0
- package/src/usage/summary.ts +15 -1
- package/src/vision/describe.ts +6 -0
- package/src/vision/image-rewrite.ts +108 -0
- package/src/vision/index.ts +19 -306
- package/src/vision/plan.ts +205 -0
- package/src/web-search/executor.ts +6 -0
- package/src/web-search/index.ts +8 -1
- package/gui/dist/assets/index-BU1tE0sr.js +0 -112
- package/gui/dist/assets/index-DL9-iS6J.css +0 -1
package/src/combos/failover.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseResetCooldownMs } from "../codex/routing";
|
|
1
2
|
import { classifyError, isCyberPolicyCode } from "../lib/errors";
|
|
2
3
|
import type { OcxComboTarget } from "../types";
|
|
3
4
|
import { targetKey } from "./types";
|
|
@@ -197,6 +198,7 @@ export function coolComboTarget(
|
|
|
197
198
|
target: Pick<OcxComboTarget, "provider" | "model">,
|
|
198
199
|
options?: {
|
|
199
200
|
retryAfter?: string | null;
|
|
201
|
+
resetAt?: unknown | unknown[];
|
|
200
202
|
now?: number;
|
|
201
203
|
cooldownMs?: number;
|
|
202
204
|
writerGeneration?: number;
|
|
@@ -209,8 +211,12 @@ export function coolComboTarget(
|
|
|
209
211
|
const writerGeneration = options?.writerGeneration ?? captureConfigGeneration();
|
|
210
212
|
const ownerKey = `${comboId}::${targetKey(target)}`;
|
|
211
213
|
if (writerGeneration < lastReconciledGeneration && !liveComboTargets.has(ownerKey)) return;
|
|
212
|
-
|
|
213
|
-
|
|
214
|
+
// A server-provided Retry-After is authoritative, including an immediate `0` directive.
|
|
215
|
+
// A quota reset is the next-most-specific signal (#3256); configured and default cooldowns
|
|
216
|
+
// are only fallbacks when upstream supplied neither usable value.
|
|
217
|
+
const cooldownMs = parseRetryAfterMs(options?.retryAfter, now, { preserveImmediate: true })
|
|
218
|
+
?? parseResetCooldownMs(options?.resetAt, now)
|
|
219
|
+
?? options?.cooldownMs
|
|
214
220
|
?? (isTransientRequestRateLimit({
|
|
215
221
|
status: options?.status,
|
|
216
222
|
code: options?.code,
|
|
@@ -222,6 +228,32 @@ export function coolComboTarget(
|
|
|
222
228
|
sweepExpiredOnWrite(now);
|
|
223
229
|
}
|
|
224
230
|
|
|
231
|
+
export function earliestComboCooldown(
|
|
232
|
+
comboId: string,
|
|
233
|
+
targets: Iterable<Pick<OcxComboTarget, "provider" | "model">>,
|
|
234
|
+
now = Date.now(),
|
|
235
|
+
): { expiry: number; target: Pick<OcxComboTarget, "provider" | "model"> } | undefined {
|
|
236
|
+
let earliest: { expiry: number; target: Pick<OcxComboTarget, "provider" | "model"> } | undefined;
|
|
237
|
+
for (const target of targets) {
|
|
238
|
+
const key = cooldownMapKey(comboId, target);
|
|
239
|
+
const entry = targetCooldowns.get(key);
|
|
240
|
+
if (!entry || entry.cooldownUntil <= now) continue;
|
|
241
|
+
if (earliest === undefined || entry.cooldownUntil < earliest.expiry) {
|
|
242
|
+
earliest = { expiry: entry.cooldownUntil, target };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return earliest;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** Public convenience wrapper returning only the earliest cooldown expiry. */
|
|
249
|
+
export function earliestComboCooldownExpiry(
|
|
250
|
+
comboId: string,
|
|
251
|
+
targets: Iterable<Pick<OcxComboTarget, "provider" | "model">>,
|
|
252
|
+
now = Date.now(),
|
|
253
|
+
): number | undefined {
|
|
254
|
+
return earliestComboCooldown(comboId, targets, now)?.expiry;
|
|
255
|
+
}
|
|
256
|
+
|
|
225
257
|
export function reconcileComboTargetCooldowns(context: GenerationContext): number {
|
|
226
258
|
if (context.generation <= lastReconciledGeneration) return 0;
|
|
227
259
|
liveComboTargets = new Set(context.comboTargets);
|
|
@@ -253,7 +285,7 @@ export function clearComboTargetCooldowns(comboId?: string): void {
|
|
|
253
285
|
}
|
|
254
286
|
|
|
255
287
|
export type ComboFailureDecision = "hop" | "stop";
|
|
256
|
-
export type ComboFailureCooldownScope = "target" | "provider";
|
|
288
|
+
export type ComboFailureCooldownScope = "none" | "target" | "provider";
|
|
257
289
|
|
|
258
290
|
function normalizedFailureCode(code?: string | null): string {
|
|
259
291
|
return code?.trim().toLowerCase().replaceAll("-", "_") ?? "";
|
|
@@ -272,17 +304,71 @@ function isProviderScopedQuotaCap(
|
|
|
272
304
|
) {
|
|
273
305
|
return true;
|
|
274
306
|
}
|
|
275
|
-
return
|
|
276
|
-
|| text.includes("err_free_prompt_cap")
|
|
307
|
+
return text.includes("err_free_prompt_cap")
|
|
277
308
|
|| (text.includes("free tier") && text.includes("single request"));
|
|
278
309
|
}
|
|
279
310
|
|
|
311
|
+
/**
|
|
312
|
+
* A free-tier cap the upstream evaluates PER REQUEST rather than per account window. These
|
|
313
|
+
* needles used to reach only `isProviderScopedQuotaCap`, so a single oversized free-tier prompt
|
|
314
|
+
* cooled the whole provider for every other combo — including the shorter requests that same
|
|
315
|
+
* provider would still have served. `free_rate_limited` also left the provider-scoped predicate
|
|
316
|
+
* for the same reason; it stays a hop signal, but stops recording provider-wide evidence.
|
|
317
|
+
*/
|
|
318
|
+
function isRequestLocalFreePromptCap(
|
|
319
|
+
status: number | undefined,
|
|
320
|
+
message: string,
|
|
321
|
+
code?: string | null,
|
|
322
|
+
): boolean {
|
|
323
|
+
if (status !== 400) return false;
|
|
324
|
+
const text = message.toLowerCase();
|
|
325
|
+
if (normalizedFailureCode(code) === "free_rate_limited") return true;
|
|
326
|
+
if (text.includes("err_free_prompt_cap")) return true;
|
|
327
|
+
return text.includes("free tier") && (text.includes("single request") || text.includes("prompt"));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Failures that describe the SHAPE of this request rather than the health of the target.
|
|
332
|
+
* Cooling anything for these is wrong twice over: the target is fine, and the next request
|
|
333
|
+
* (shorter prompt, smaller tool catalog) would have succeeded against it.
|
|
334
|
+
*/
|
|
335
|
+
const REQUEST_SHAPE_FAILURE_CODES = new Set([
|
|
336
|
+
"input_admission_refused",
|
|
337
|
+
"context_length_exceeded",
|
|
338
|
+
"tool_catalog_too_large",
|
|
339
|
+
"cursor_root_envelope_limit",
|
|
340
|
+
"target_incompatible",
|
|
341
|
+
]);
|
|
342
|
+
|
|
343
|
+
/** Credential/billing failures that every target sharing the provider inherits. */
|
|
344
|
+
const PROVIDER_SCOPED_FAILURE_CODES = new Set([
|
|
345
|
+
"invalid_api_key",
|
|
346
|
+
"insufficient_quota",
|
|
347
|
+
"subscription_required",
|
|
348
|
+
"payment_required",
|
|
349
|
+
"billing_error",
|
|
350
|
+
"insufficient_balance",
|
|
351
|
+
]);
|
|
352
|
+
|
|
280
353
|
export function comboFailureCooldownScope(
|
|
281
354
|
status: number,
|
|
282
355
|
message: string,
|
|
283
356
|
options?: { code?: string | null },
|
|
284
357
|
): ComboFailureCooldownScope {
|
|
285
|
-
|
|
358
|
+
const code = normalizedFailureCode(options?.code);
|
|
359
|
+
// Request-shape refusals first: an oversized request must not cool a healthy target.
|
|
360
|
+
if (
|
|
361
|
+
status === 413
|
|
362
|
+
|| REQUEST_SHAPE_FAILURE_CODES.has(code)
|
|
363
|
+
|| isRequestLocalFreePromptCap(status, message, options?.code)
|
|
364
|
+
|| isProviderTargetContextOverflow(status, message, options?.code)
|
|
365
|
+
) return "none";
|
|
366
|
+
if (isProviderScopedQuotaCap(status, message, options?.code)) return "provider";
|
|
367
|
+
// A rejected or unpaid credential is provider-wide evidence: every target that routes
|
|
368
|
+
// through the same provider row carries the same key and will fail identically.
|
|
369
|
+
if (status === 401 || status === 402 || status === 403) return "provider";
|
|
370
|
+
if (PROVIDER_SCOPED_FAILURE_CODES.has(code)) return "provider";
|
|
371
|
+
return "target";
|
|
286
372
|
}
|
|
287
373
|
|
|
288
374
|
function isModelLifecycleGone(
|
|
@@ -307,6 +393,19 @@ function isModelLifecycleGone(
|
|
|
307
393
|
);
|
|
308
394
|
}
|
|
309
395
|
|
|
396
|
+
function isProviderTargetContextOverflow(
|
|
397
|
+
status: number,
|
|
398
|
+
message: string,
|
|
399
|
+
code?: string | null,
|
|
400
|
+
): boolean {
|
|
401
|
+
if (status !== 400) return false;
|
|
402
|
+
const normalizedCode = normalizedFailureCode(code);
|
|
403
|
+
const text = message.toLowerCase();
|
|
404
|
+
if (text.includes("invalid_request_prompt_too_long")) return true;
|
|
405
|
+
return normalizedCode === "5059"
|
|
406
|
+
&& /\bprompt\s+\d+\s*>\s*\d+\s+maximum context length\b/i.test(message);
|
|
407
|
+
}
|
|
408
|
+
|
|
310
409
|
export function comboFailureDecision(
|
|
311
410
|
status: number,
|
|
312
411
|
message: string,
|
|
@@ -324,6 +423,10 @@ export function comboFailureDecision(
|
|
|
324
423
|
if (isModelLifecycleGone(status, message, options?.code)) return "hop";
|
|
325
424
|
const error = classifyError(status, "upstream_error", message);
|
|
326
425
|
if (isCyberPolicyCode(error.code)) return "stop";
|
|
426
|
+
// A provider can expose its own target hard cap with a non-semantic vendor code
|
|
427
|
+
// (for example 5059 + invalid_request_prompt_too_long). That is evidence that this
|
|
428
|
+
// target is too small, not that every later combo target is incapable of serving it.
|
|
429
|
+
if (isProviderTargetContextOverflow(status, message, options?.code)) return "hop";
|
|
327
430
|
// A local input-admission refusal (#1524) says "this candidate cannot fit the request",
|
|
328
431
|
// not "the request is impossible": the next candidate may have a larger context window.
|
|
329
432
|
//
|
|
@@ -347,15 +450,30 @@ export function comboFailureDecision(
|
|
|
347
450
|
if (isProviderScopedQuotaCap(status, message, options?.code || error.code)) {
|
|
348
451
|
return "hop";
|
|
349
452
|
}
|
|
453
|
+
// A model-scoped rejection is target-local: this provider does not serve THIS model, which
|
|
454
|
+
// says nothing about the next combo target. Structured code only, plus the explicit prose
|
|
455
|
+
// form upstreams emit when they carry no code, so an unrelated 400 stays terminal.
|
|
456
|
+
const failureCode = normalizedFailureCode(options?.code || error.code);
|
|
457
|
+
if (["model_not_found", "model_unavailable", "unsupported_model"].includes(failureCode)) {
|
|
458
|
+
return "hop";
|
|
459
|
+
}
|
|
460
|
+
// `free_rate_limited` no longer routes through `isProviderScopedQuotaCap` (it is a
|
|
461
|
+
// per-request cap, not provider-wide evidence), so keep its hop verdict explicit here.
|
|
462
|
+
if (failureCode === "free_rate_limited") return "hop";
|
|
350
463
|
if (["origin_rejected", "context_length_exceeded", "invalid_request_error"].includes(error.code ?? "")) {
|
|
351
464
|
return "stop";
|
|
352
465
|
}
|
|
353
|
-
|
|
466
|
+
// 402 (payment required) and 425 (too early) are provider-state signals, not verdicts about
|
|
467
|
+
// the request: another combo target can still serve it.
|
|
468
|
+
if ([401, 402, 403, 404, 408, 425, 429].includes(status) || status >= 500) return "hop";
|
|
354
469
|
if ([
|
|
355
470
|
"permission_denied",
|
|
356
471
|
"subscription_required",
|
|
357
472
|
"invalid_api_key",
|
|
358
473
|
"insufficient_quota",
|
|
474
|
+
"payment_required",
|
|
475
|
+
"billing_error",
|
|
476
|
+
"insufficient_balance",
|
|
359
477
|
"rate_limit_exceeded",
|
|
360
478
|
"server_is_overloaded",
|
|
361
479
|
"upstream_server_error",
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { SUPPORTED_NATIVE_OPENAI_SLUGS } from "../codex/catalog/native-models";
|
|
2
|
+
import type { OcxComboConfig, OcxComboTarget, OcxConfig } from "../types";
|
|
3
|
+
|
|
4
|
+
export const COMBO_NAMESPACE = "combo";
|
|
5
|
+
|
|
6
|
+
export function preservesPhysicalComboProvider(
|
|
7
|
+
config: Pick<OcxConfig, "providers" | "combos">,
|
|
8
|
+
): boolean {
|
|
9
|
+
return Object.hasOwn(config.providers, COMBO_NAMESPACE)
|
|
10
|
+
&& Object.keys(config.combos ?? {}).length === 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const COMBO_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
14
|
+
|
|
15
|
+
/** True only for an explicitly opted-in bare native-family alias. */
|
|
16
|
+
export function isNativeAliasCombo(
|
|
17
|
+
combo: { alias?: string | null; nativeAlias?: boolean },
|
|
18
|
+
): boolean {
|
|
19
|
+
const alias = typeof combo.alias === "string" ? combo.alias.trim() : "";
|
|
20
|
+
return combo.nativeAlias === true
|
|
21
|
+
&& SUPPORTED_NATIVE_OPENAI_SLUGS.has(alias);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function targetKey(target: Pick<OcxComboTarget, "provider" | "model">): string {
|
|
25
|
+
return `${target.provider}/${target.model}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function parseComboModelId(modelId: string): string | null {
|
|
29
|
+
const slash = modelId.indexOf("/");
|
|
30
|
+
if (slash <= 0 || modelId.slice(0, slash) !== COMBO_NAMESPACE) return null;
|
|
31
|
+
const id = modelId.slice(slash + 1);
|
|
32
|
+
return id.length > 0 ? id : null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function comboModelId(id: string): string {
|
|
36
|
+
return `${COMBO_NAMESPACE}/${id}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Public model id clients request: the alias when set, else the default `combo/<id>`. */
|
|
40
|
+
export function comboPublicModelId(id: string, combo: { alias?: string | null }): string {
|
|
41
|
+
const alias = typeof combo.alias === "string" ? combo.alias.trim() : "";
|
|
42
|
+
return alias || comboModelId(id);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Persisted selector that hides a combo from discovery. Native aliases keep the canonical
|
|
47
|
+
* `combo/<id>` selector because their bare public id remains the native OpenAI disable key.
|
|
48
|
+
*/
|
|
49
|
+
export function comboDisabledModelId(
|
|
50
|
+
id: string,
|
|
51
|
+
combo: { alias?: string | null; nativeAlias?: boolean },
|
|
52
|
+
): string {
|
|
53
|
+
return isNativeAliasCombo(combo) ? comboModelId(id) : comboPublicModelId(id, combo);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Every persisted selector that can refer to this combo in `disabledModels`. */
|
|
57
|
+
export function comboDisabledModelSelectors(
|
|
58
|
+
id: string,
|
|
59
|
+
combo: { alias?: string | null; nativeAlias?: boolean },
|
|
60
|
+
): string[] {
|
|
61
|
+
const canonical = comboModelId(id);
|
|
62
|
+
const preferred = comboDisabledModelId(id, combo);
|
|
63
|
+
return preferred === canonical ? [canonical] : [canonical, preferred];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolve a client-requested model id to a combo config key. The canonical `combo/<id>`
|
|
68
|
+
* form wins first (back-compat); otherwise an exact alias match across configured combos.
|
|
69
|
+
*/
|
|
70
|
+
export function resolveComboId(
|
|
71
|
+
config: { combos?: Record<string, OcxComboConfig> },
|
|
72
|
+
modelId: string,
|
|
73
|
+
): string | null {
|
|
74
|
+
const direct = parseComboModelId(modelId);
|
|
75
|
+
if (direct) return direct;
|
|
76
|
+
const combos = config.combos;
|
|
77
|
+
if (!combos) return null;
|
|
78
|
+
for (const [id, raw] of Object.entries(combos)) {
|
|
79
|
+
if (!raw || typeof raw !== "object") continue;
|
|
80
|
+
const alias = typeof raw.alias === "string" ? raw.alias.trim() : "";
|
|
81
|
+
if (alias && alias === modelId) return id;
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
export function isValidComboId(id: string): boolean {
|
|
88
|
+
return COMBO_ID_PATTERN.test(id);
|
|
89
|
+
}
|
package/src/combos/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export {
|
|
2
|
+
COMBO_DEFAULT_WAIT_FOR_COOLDOWN_MS,
|
|
2
3
|
COMBO_NAMESPACE,
|
|
3
4
|
comboAliasIssues,
|
|
4
5
|
comboConfigError,
|
|
@@ -25,6 +26,7 @@ export {
|
|
|
25
26
|
noteComboFailure,
|
|
26
27
|
noteComboSuccess,
|
|
27
28
|
pickComboTarget,
|
|
29
|
+
pickComboTargetWithWait,
|
|
28
30
|
tryPickComboModel,
|
|
29
31
|
UnknownComboError,
|
|
30
32
|
type ComboPick,
|
|
@@ -34,6 +36,8 @@ export {
|
|
|
34
36
|
comboCooldownRetryAfterSeconds,
|
|
35
37
|
COMBO_REQUEST_RATE_COOLDOWN_MS,
|
|
36
38
|
coolComboTarget,
|
|
39
|
+
earliestComboCooldown,
|
|
40
|
+
earliestComboCooldownExpiry,
|
|
37
41
|
isComboTargetInCooldown,
|
|
38
42
|
isTransientRequestRateLimit,
|
|
39
43
|
parseRetryAfterMs,
|
package/src/combos/resolve.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import type { OcxComboTarget, OcxConfig } from "../types";
|
|
2
2
|
import { getCachedProviderQuota } from "../providers/quota-routing-cache";
|
|
3
3
|
import type { ProviderQuota } from "../providers/quota-types";
|
|
4
|
-
import {
|
|
4
|
+
import { sleepWithAbort } from "../lib/upstream-retry";
|
|
5
|
+
import {
|
|
6
|
+
coolComboTarget,
|
|
7
|
+
earliestComboCooldown,
|
|
8
|
+
isComboTargetInCooldown,
|
|
9
|
+
type ComboFailureCooldownScope,
|
|
10
|
+
} from "./failover";
|
|
5
11
|
import { quotaResetRemainingMs } from "./reset-window";
|
|
6
12
|
import { getCombo, resolveComboId, targetKey } from "./types";
|
|
7
13
|
import type { NormalizedComboConfig } from "./types";
|
|
@@ -155,6 +161,7 @@ export function pickComboTarget(
|
|
|
155
161
|
const eligible = (target: Required<OcxComboTarget>): boolean =>
|
|
156
162
|
targetProviderIsUsable(config, target)
|
|
157
163
|
&& !cachedProviderQuotaIsExhausted(getCachedProviderQuota(target.provider, now), now)
|
|
164
|
+
&& !isComboTargetInCooldown(comboId, target, now)
|
|
158
165
|
&& !excluded.has(targetKey(target))
|
|
159
166
|
&& (options.eligible?.(target) ?? true);
|
|
160
167
|
|
|
@@ -274,7 +281,9 @@ export function advanceComboAfterFailure(
|
|
|
274
281
|
pick: ComboPick,
|
|
275
282
|
options: {
|
|
276
283
|
retryAfter?: string | null;
|
|
284
|
+
resetAt?: unknown | unknown[];
|
|
277
285
|
now?: number;
|
|
286
|
+
cooldownMs?: number;
|
|
278
287
|
eligible?: (target: Required<OcxComboTarget>) => boolean;
|
|
279
288
|
cooldownScope?: ComboFailureCooldownScope;
|
|
280
289
|
status?: number;
|
|
@@ -284,14 +293,19 @@ export function advanceComboAfterFailure(
|
|
|
284
293
|
): ComboPick | null {
|
|
285
294
|
noteComboFailure(pick.comboId, pick.target, pick.writerGeneration);
|
|
286
295
|
const combo = getCombo(config, pick.comboId);
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
296
|
+
// "none" records no cooldown at all: the failure described the request, not the target, so
|
|
297
|
+
// the target must stay immediately selectable for the next (differently shaped) request.
|
|
298
|
+
if (options.cooldownScope !== "none") {
|
|
299
|
+
const cooldownTargets = options.cooldownScope === "provider" && combo
|
|
300
|
+
? combo.targets.filter(target => target.provider === pick.target.provider)
|
|
301
|
+
: [pick.target];
|
|
302
|
+
for (const target of cooldownTargets) {
|
|
303
|
+
coolComboTarget(pick.comboId, target, {
|
|
304
|
+
...options,
|
|
305
|
+
cooldownMs: options.cooldownMs ?? combo?.cooldownMs,
|
|
306
|
+
writerGeneration: pick.writerGeneration,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
295
309
|
}
|
|
296
310
|
return pickComboTarget(config, pick.comboId, {
|
|
297
311
|
exclude: pick.attempted,
|
|
@@ -301,6 +315,63 @@ export function advanceComboAfterFailure(
|
|
|
301
315
|
});
|
|
302
316
|
}
|
|
303
317
|
|
|
318
|
+
export async function pickComboTargetWithWait(
|
|
319
|
+
config: OcxConfig,
|
|
320
|
+
comboId: string,
|
|
321
|
+
options: {
|
|
322
|
+
exclude?: Iterable<string>;
|
|
323
|
+
eligible?: (target: Required<OcxComboTarget>) => boolean;
|
|
324
|
+
waitForCooldownMs: number;
|
|
325
|
+
abortSignal?: AbortSignal;
|
|
326
|
+
now?: number;
|
|
327
|
+
sleep?: (ms: number, signal?: AbortSignal) => Promise<void>;
|
|
328
|
+
},
|
|
329
|
+
): Promise<ComboPick | null> {
|
|
330
|
+
const now = options.now ?? Date.now();
|
|
331
|
+
const excluded = new Set(options.exclude ?? []);
|
|
332
|
+
const customEligible = options.eligible;
|
|
333
|
+
const eligible = (target: Required<OcxComboTarget>): boolean =>
|
|
334
|
+
!isComboTargetInCooldown(comboId, target, now)
|
|
335
|
+
&& (customEligible?.(target) ?? true);
|
|
336
|
+
const pick = pickComboTarget(config, comboId, { exclude: excluded, eligible, now });
|
|
337
|
+
if (pick || options.waitForCooldownMs <= 0 || options.abortSignal?.aborted) return pick;
|
|
338
|
+
const combo = getCombo(config, comboId);
|
|
339
|
+
if (!combo) throw new UnknownComboError(comboId);
|
|
340
|
+
const waitingTargets = combo.targets.filter(target =>
|
|
341
|
+
targetProviderIsUsable(config, target)
|
|
342
|
+
&& !cachedProviderQuotaIsExhausted(getCachedProviderQuota(target.provider, now), now)
|
|
343
|
+
&& !excluded.has(targetKey(target))
|
|
344
|
+
&& isComboTargetInCooldown(comboId, target, now)
|
|
345
|
+
&& (customEligible?.(target) ?? true),
|
|
346
|
+
);
|
|
347
|
+
const earliest = earliestComboCooldown(comboId, waitingTargets, now);
|
|
348
|
+
if (earliest === undefined) return null;
|
|
349
|
+
const delay = earliest.expiry - now;
|
|
350
|
+
if (delay > options.waitForCooldownMs) return null;
|
|
351
|
+
// The expiry computation above is the single source of truth for the wait budget.
|
|
352
|
+
// Its target preserves configured order for ties.
|
|
353
|
+
const target = earliest.target;
|
|
354
|
+
console.warn(
|
|
355
|
+
`[combo] ${comboId}: all targets cooling, waiting ${delay}ms for ${targetKey(target)}`,
|
|
356
|
+
);
|
|
357
|
+
try {
|
|
358
|
+
await (options.sleep ?? sleepWithAbort)(delay, options.abortSignal);
|
|
359
|
+
} catch (error) {
|
|
360
|
+
if (options.abortSignal?.aborted) return null;
|
|
361
|
+
throw error;
|
|
362
|
+
}
|
|
363
|
+
if (options.abortSignal?.aborted) return null;
|
|
364
|
+
// Management updates can delete or rename the combo while this request sleeps.
|
|
365
|
+
if (!getCombo(config, comboId)) return null;
|
|
366
|
+
return pickComboTarget(config, comboId, {
|
|
367
|
+
exclude: excluded,
|
|
368
|
+
now: now + delay,
|
|
369
|
+
eligible: targetCandidate =>
|
|
370
|
+
!isComboTargetInCooldown(comboId, targetCandidate, now + delay)
|
|
371
|
+
&& (customEligible?.(targetCandidate) ?? true),
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
304
375
|
export function reconcileComboRotationState(context: GenerationContext): number {
|
|
305
376
|
if (context.generation <= lastReconciledGeneration) return 0;
|
|
306
377
|
let removed = 0;
|
package/src/combos/types.ts
CHANGED
|
@@ -1,25 +1,11 @@
|
|
|
1
1
|
import { isCodexReasoningEffort } from "../reasoning-effort";
|
|
2
2
|
import { SUPPORTED_NATIVE_OPENAI_SLUGS } from "../codex/catalog/native-models";
|
|
3
|
-
import type {
|
|
4
|
-
|
|
5
|
-
OcxComboDefaultEffort,
|
|
6
|
-
OcxComboReasoningEffortMode,
|
|
7
|
-
OcxComboStrategy,
|
|
8
|
-
OcxComboTarget,
|
|
9
|
-
OcxConfig,
|
|
10
|
-
OcxProviderConfig,
|
|
11
|
-
} from "../types";
|
|
3
|
+
import type { OcxComboConfig, OcxComboDefaultEffort, OcxComboReasoningEffortMode, OcxComboStrategy, OcxComboTarget, OcxProviderConfig } from "../types";
|
|
4
|
+
import { COMBO_NAMESPACE, isValidComboId, targetKey } from "./identifiers";
|
|
12
5
|
|
|
13
|
-
export const
|
|
6
|
+
export const COMBO_DEFAULT_WAIT_FOR_COOLDOWN_MS = 0;
|
|
7
|
+
export { COMBO_NAMESPACE, preservesPhysicalComboProvider, isNativeAliasCombo, targetKey, parseComboModelId, comboModelId, comboPublicModelId, comboDisabledModelId, comboDisabledModelSelectors, resolveComboId, isValidComboId } from "./identifiers";
|
|
14
8
|
|
|
15
|
-
export function preservesPhysicalComboProvider(
|
|
16
|
-
config: Pick<OcxConfig, "providers" | "combos">,
|
|
17
|
-
): boolean {
|
|
18
|
-
return Object.hasOwn(config.providers, COMBO_NAMESPACE)
|
|
19
|
-
&& Object.keys(config.combos ?? {}).length === 0;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const COMBO_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
23
9
|
/**
|
|
24
10
|
* Public alias shape: one optional "/" segment, each segment id-shaped. Bare aliases
|
|
25
11
|
* (no "/") are the masquerade case — the combo answers to a mandated model id with no
|
|
@@ -37,6 +23,8 @@ export interface ComboValidationIssue {
|
|
|
37
23
|
export interface NormalizedComboConfig {
|
|
38
24
|
strategy: OcxComboStrategy;
|
|
39
25
|
stickyLimit: number;
|
|
26
|
+
cooldownMs?: number;
|
|
27
|
+
waitForCooldownMs: number;
|
|
40
28
|
defaultEffort: OcxComboDefaultEffort | null;
|
|
41
29
|
/** Picker-ladder derivation policy; `strict` preserves the legacy intersection rule. */
|
|
42
30
|
reasoningEffortMode: OcxComboReasoningEffortMode;
|
|
@@ -51,77 +39,6 @@ export interface NormalizedComboConfig {
|
|
|
51
39
|
targets: Array<Required<OcxComboTarget>>;
|
|
52
40
|
}
|
|
53
41
|
|
|
54
|
-
/** True only for an explicitly opted-in bare native-family alias. */
|
|
55
|
-
export function isNativeAliasCombo(
|
|
56
|
-
combo: { alias?: string | null; nativeAlias?: boolean },
|
|
57
|
-
): boolean {
|
|
58
|
-
const alias = typeof combo.alias === "string" ? combo.alias.trim() : "";
|
|
59
|
-
return combo.nativeAlias === true
|
|
60
|
-
&& SUPPORTED_NATIVE_OPENAI_SLUGS.has(alias);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function targetKey(target: Pick<OcxComboTarget, "provider" | "model">): string {
|
|
64
|
-
return `${target.provider}/${target.model}`;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function parseComboModelId(modelId: string): string | null {
|
|
68
|
-
const slash = modelId.indexOf("/");
|
|
69
|
-
if (slash <= 0 || modelId.slice(0, slash) !== COMBO_NAMESPACE) return null;
|
|
70
|
-
const id = modelId.slice(slash + 1);
|
|
71
|
-
return id.length > 0 ? id : null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function comboModelId(id: string): string {
|
|
75
|
-
return `${COMBO_NAMESPACE}/${id}`;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/** Public model id clients request: the alias when set, else the default `combo/<id>`. */
|
|
79
|
-
export function comboPublicModelId(id: string, combo: { alias?: string | null }): string {
|
|
80
|
-
const alias = typeof combo.alias === "string" ? combo.alias.trim() : "";
|
|
81
|
-
return alias || comboModelId(id);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
* Persisted selector that hides a combo from discovery. Native aliases keep the canonical
|
|
86
|
-
* `combo/<id>` selector because their bare public id remains the native OpenAI disable key.
|
|
87
|
-
*/
|
|
88
|
-
export function comboDisabledModelId(
|
|
89
|
-
id: string,
|
|
90
|
-
combo: { alias?: string | null; nativeAlias?: boolean },
|
|
91
|
-
): string {
|
|
92
|
-
return isNativeAliasCombo(combo) ? comboModelId(id) : comboPublicModelId(id, combo);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/** Every persisted selector that can refer to this combo in `disabledModels`. */
|
|
96
|
-
export function comboDisabledModelSelectors(
|
|
97
|
-
id: string,
|
|
98
|
-
combo: { alias?: string | null; nativeAlias?: boolean },
|
|
99
|
-
): string[] {
|
|
100
|
-
const canonical = comboModelId(id);
|
|
101
|
-
const preferred = comboDisabledModelId(id, combo);
|
|
102
|
-
return preferred === canonical ? [canonical] : [canonical, preferred];
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Resolve a client-requested model id to a combo config key. The canonical `combo/<id>`
|
|
107
|
-
* form wins first (back-compat); otherwise an exact alias match across configured combos.
|
|
108
|
-
*/
|
|
109
|
-
export function resolveComboId(
|
|
110
|
-
config: { combos?: Record<string, OcxComboConfig> },
|
|
111
|
-
modelId: string,
|
|
112
|
-
): string | null {
|
|
113
|
-
const direct = parseComboModelId(modelId);
|
|
114
|
-
if (direct) return direct;
|
|
115
|
-
const combos = config.combos;
|
|
116
|
-
if (!combos) return null;
|
|
117
|
-
for (const [id, raw] of Object.entries(combos)) {
|
|
118
|
-
if (!raw || typeof raw !== "object") continue;
|
|
119
|
-
const alias = typeof raw.alias === "string" ? raw.alias.trim() : "";
|
|
120
|
-
if (alias && alias === modelId) return id;
|
|
121
|
-
}
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
42
|
/**
|
|
126
43
|
* Cross-combo alias checks that need the full combos map (uniqueness). Kept separate
|
|
127
44
|
* from `comboConfigIssues` so config-file validation and the management API share it.
|
|
@@ -230,6 +147,18 @@ export function comboConfigIssues(
|
|
|
230
147
|
|| body.stickyLimit > 100)) {
|
|
231
148
|
issues.push({ path: ["stickyLimit"], message: "stickyLimit must be an integer from 1 to 100" });
|
|
232
149
|
}
|
|
150
|
+
if (body.cooldownMs !== undefined
|
|
151
|
+
&& (typeof body.cooldownMs !== "number" || !Number.isInteger(body.cooldownMs)
|
|
152
|
+
|| body.cooldownMs < 1
|
|
153
|
+
|| body.cooldownMs > 600_000)) {
|
|
154
|
+
issues.push({ path: ["cooldownMs"], message: "cooldownMs must be an integer from 1 to 600000" });
|
|
155
|
+
}
|
|
156
|
+
if (body.waitForCooldownMs !== undefined
|
|
157
|
+
&& (typeof body.waitForCooldownMs !== "number" || !Number.isInteger(body.waitForCooldownMs)
|
|
158
|
+
|| body.waitForCooldownMs < 0
|
|
159
|
+
|| body.waitForCooldownMs > 600_000)) {
|
|
160
|
+
issues.push({ path: ["waitForCooldownMs"], message: "waitForCooldownMs must be an integer from 0 to 600000" });
|
|
161
|
+
}
|
|
233
162
|
if (body.defaultEffort !== undefined
|
|
234
163
|
&& body.defaultEffort !== null
|
|
235
164
|
&& (typeof body.defaultEffort !== "string" || !isCodexReasoningEffort(body.defaultEffort))) {
|
|
@@ -367,6 +296,8 @@ export function normalizeComboConfig(raw: OcxComboConfig): NormalizedComboConfig
|
|
|
367
296
|
return {
|
|
368
297
|
strategy: raw.strategy ?? "failover",
|
|
369
298
|
stickyLimit: raw.stickyLimit ?? 1,
|
|
299
|
+
cooldownMs: raw.cooldownMs,
|
|
300
|
+
waitForCooldownMs: raw.waitForCooldownMs ?? COMBO_DEFAULT_WAIT_FOR_COOLDOWN_MS,
|
|
370
301
|
defaultEffort: raw.defaultEffort ?? null,
|
|
371
302
|
reasoningEffortMode: raw.reasoningEffortMode === "adaptive" ? "adaptive" : "strict",
|
|
372
303
|
imageInput: raw.imageInput === "disabled" ? "disabled" : "auto",
|
|
@@ -393,10 +324,6 @@ export function comboDefaultEffort(
|
|
|
393
324
|
: null;
|
|
394
325
|
}
|
|
395
326
|
|
|
396
|
-
export function isValidComboId(id: string): boolean {
|
|
397
|
-
return COMBO_ID_PATTERN.test(id);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
327
|
export function listComboIds(config: { combos?: Record<string, OcxComboConfig> }): string[] {
|
|
401
328
|
return Object.keys(config.combos ?? {}).sort((a, b) => a.localeCompare(b));
|
|
402
329
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { OcxConfig } from "../types";
|
|
2
|
+
import { NATIVE_GPT6_ASTRA_MODEL } from "../codex/catalog/native-models";
|
|
3
|
+
|
|
4
|
+
export const SUBAGENT_MODELS_VERSION = 1;
|
|
5
|
+
|
|
6
|
+
/** Native featured defaults; Codex advertises at most five picker-visible rows. */
|
|
7
|
+
export const DEFAULT_SUBAGENT_MODELS = [
|
|
8
|
+
NATIVE_GPT6_ASTRA_MODEL, "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5",
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
/** One-time upgrade; later user edits (including removing Astra) remain authoritative. */
|
|
12
|
+
export function migrateSubagentModels(config: OcxConfig): boolean {
|
|
13
|
+
if ((config.subagentModelsVersion ?? 0) >= SUBAGENT_MODELS_VERSION) return false;
|
|
14
|
+
if (config.subagentModels === undefined) {
|
|
15
|
+
config.subagentModels = [...DEFAULT_SUBAGENT_MODELS];
|
|
16
|
+
} else {
|
|
17
|
+
const retained = [...new Set([NATIVE_GPT6_ASTRA_MODEL, ...config.subagentModels])].slice(0, 5);
|
|
18
|
+
// Cap first: do not rescue a fifth old choice. Retained 5.5 belongs at the bottom.
|
|
19
|
+
config.subagentModels = retained.filter(model => model !== "gpt-5.5");
|
|
20
|
+
if (retained.includes("gpt-5.5")) config.subagentModels.push("gpt-5.5");
|
|
21
|
+
}
|
|
22
|
+
config.subagentModelsVersion = SUBAGENT_MODELS_VERSION;
|
|
23
|
+
return true;
|
|
24
|
+
}
|