@bitkyc08/opencodex 2.69.0 → 2.70.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/gui/dist/assets/App-_MrbTrBe.js +51 -0
- package/gui/dist/assets/{Tray-uqKRqfvw.js → Tray-5PuUalET.js} +1 -1
- package/gui/dist/assets/index-DKrO2eF3.js +86 -0
- package/gui/dist/assets/quota-summary-BCLivc0Q.js +1 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic-model-contract.ts +119 -0
- package/src/adapters/anthropic.ts +14 -83
- package/src/adapters/base.ts +2 -0
- package/src/adapters/command-code-project-context.ts +542 -0
- package/src/adapters/command-code-tool-text.ts +99 -67
- package/src/adapters/command-code.ts +5 -1
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/devin/cloud-direct/catalog.ts +91 -0
- package/src/adapters/devin/cloud-direct/chat.ts +78 -48
- package/src/adapters/devin/cloud-direct/tool-schema.ts +121 -0
- package/src/adapters/devin/context-overflow.ts +75 -0
- package/src/adapters/devin/live-models.ts +174 -13
- package/src/adapters/devin/reasoning-signature.ts +100 -0
- package/src/adapters/devin.ts +310 -117
- package/src/adapters/kiro/payload.ts +10 -10
- package/src/adapters/kiro-images.ts +58 -7
- package/src/adapters/openai-chat/passthrough.ts +3 -1
- package/src/adapters/openai-chat.ts +2 -1
- package/src/adapters/openai-responses/passthrough.ts +2 -1
- package/src/bridge/response-json.ts +15 -1
- package/src/bridge/sse.ts +3 -1
- package/src/claude/intercept/model-bindings.ts +1 -0
- package/src/cli/account-extended.ts +31 -1
- package/src/cli/account.ts +3 -0
- package/src/cli/capabilities.ts +13 -0
- package/src/cli/client-start-fence.ts +36 -0
- package/src/cli/dispatch.ts +3 -2
- package/src/cli/index.ts +98 -117
- package/src/cli/provider-runtime.ts +23 -2
- package/src/cli/provider.ts +3 -0
- package/src/cli/resolve.ts +18 -0
- package/src/cli/start-owner-fence.ts +30 -0
- package/src/cli/stop-approval.ts +13 -1
- package/src/cli/stop-restore.ts +73 -0
- package/src/cli/tray-proxy.ts +221 -18
- package/src/cli/version-skew.ts +25 -23
- package/src/client/runtime.ts +8 -4
- package/src/codex/auth-api/account-list.ts +11 -5
- package/src/codex/auth-api/login-flow.ts +5 -3
- package/src/codex/auth-api/main-account-probe.ts +186 -110
- package/src/codex/auth-api/pool-mode-gate.ts +61 -7
- package/src/codex/auth-api/pool-quota-probe.ts +142 -90
- package/src/codex/auth-api/reset-credit-service.ts +1 -1
- package/src/codex/catalog/gather-capture.ts +1 -0
- package/src/codex/catalog/model-hints.ts +3 -3
- package/src/codex/catalog/provider-models.ts +2 -0
- package/src/codex/low-quota-events.ts +28 -0
- package/src/codex/low-quota-observer.ts +23 -0
- package/src/codex/low-quota-protection.ts +272 -0
- package/src/codex/quota-query-backoff.ts +148 -0
- package/src/codex/quota.ts +7 -2
- package/src/config/diagnostics.ts +10 -1
- package/src/config/load-degrade.ts +17 -0
- package/src/config/mutation-lock.ts +3 -4
- package/src/config/process-state.ts +1 -1
- package/src/config/provider-validation.ts +22 -0
- package/src/config/schema/blocked-model-redirects.ts +17 -0
- package/src/config/schema/config-schema.ts +4 -1
- package/src/config/schema/leaf-validators.ts +33 -2
- package/src/config/serving-runtimes.ts +412 -0
- package/src/generated/compatibility-version.json +211 -115
- package/src/generated/model-metadata.ts +3 -3
- package/src/images/loop.ts +3 -0
- package/src/lib/errors.ts +2 -1
- package/src/lib/handled-signal-exit.ts +23 -0
- package/src/lib/request-failure-model.ts +1 -0
- package/src/lib/self-launch-argv.ts +9 -0
- package/src/lib/shadow-call.ts +5 -2
- package/src/lib/strict-semver.ts +26 -0
- package/src/lib/transient-service-path.ts +16 -0
- package/src/lib/windows-service-wrappers.ts +10 -6
- package/src/lib/winsw.ts +3 -1
- package/src/oauth/anthropic-model-routes.ts +105 -0
- package/src/oauth/anthropic-routing.ts +85 -48
- package/src/oauth/devin.ts +135 -5
- package/src/oauth/index.ts +20 -3
- package/src/oauth/kiro-terminal-failover.ts +23 -9
- package/src/oauth/pool-settings-capability.ts +10 -2
- package/src/oauth/store.ts +1 -1
- package/src/protocols/encoders/adapter-events.ts +3 -1
- package/src/providers/github-copilot-context.ts +27 -0
- package/src/providers/kiro-models.ts +4 -0
- package/src/providers/model-rename-fields.ts +3 -0
- package/src/providers/quota/account-cache.ts +8 -3
- package/src/providers/quota/devin.ts +286 -0
- package/src/providers/quota/vendor-probes-oauth.ts +6 -2
- package/src/providers/quota.ts +1 -1
- package/src/providers/registry/entries-core.ts +1 -1
- package/src/providers/registry/model-seeds.ts +5 -2
- package/src/responses/compaction-images.ts +27 -0
- package/src/router.ts +104 -16
- package/src/routing/analytics.ts +1 -0
- package/src/server/auth-cors.ts +9 -1
- package/src/server/background-lifecycle.ts +21 -10
- package/src/server/chat-completions.ts +2 -1
- package/src/server/chat-native.ts +1 -0
- package/src/server/index.ts +4 -8
- package/src/server/inference/client-encoder-delivery.ts +3 -0
- package/src/server/management/context.ts +3 -0
- package/src/server/management/low-quota-routes.ts +14 -0
- package/src/server/management/oauth-account-routes.ts +21 -1
- package/src/server/management/provider-overwrite-carry.ts +79 -6
- package/src/server/management/provider-routes.ts +41 -28
- package/src/server/management/route-registry.ts +1 -0
- package/src/server/management-api.ts +7 -0
- package/src/server/responses/adapter-continuation.ts +3 -0
- package/src/server/responses/adapter-delivery.ts +3 -0
- package/src/server/responses/adapter-dispatch.ts +20 -4
- package/src/server/responses/antigravity-validation-refusal.ts +20 -0
- package/src/server/responses/core-auth.ts +1 -0
- package/src/server/responses/core-codex-account.ts +1 -0
- package/src/server/responses/core-normalize.ts +4 -0
- package/src/server/responses/passthrough-dispatch.ts +8 -1
- package/src/server/responses/request-sidecar-auth.ts +2 -0
- package/src/server/responses/request-transport.ts +27 -10
- package/src/server/responses/response-effects.ts +4 -1
- package/src/server/responses/run-turn-execution.ts +126 -42
- package/src/server/responses/sidecar-execution.ts +4 -1
- package/src/service/diagnostics.ts +3 -1
- package/src/service/guarded-manager-target.ts +39 -12
- package/src/service/launchd.ts +14 -6
- package/src/service/repair.ts +6 -1
- package/src/service/service-child-ownership.ts +168 -0
- package/src/service/state.ts +22 -4
- package/src/service/systemd.ts +2 -2
- package/src/service/windows-guarded-manager.ts +530 -0
- package/src/service/windows-taskxml.ts +2 -2
- package/src/service.ts +2 -0
- package/src/types/config.ts +26 -0
- package/src/types/provider.ts +12 -0
- package/src/types/request.ts +2 -0
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +13 -0
- package/src/usage/telemetry-contract.ts +1 -0
- package/src/vision/anthropic-describe.ts +2 -1
- package/src/web-search/anthropic-executor.ts +2 -1
- package/src/web-search/loop.ts +1 -0
- package/gui/dist/assets/App-DDbgOTh6.js +0 -51
- package/gui/dist/assets/index-CuaBJkG4.js +0 -86
- package/gui/dist/assets/quota-summary-COppQSVq.js +0 -1
|
@@ -9,8 +9,11 @@ import type { ProviderModelDiscoverySpec } from "./types";
|
|
|
9
9
|
// 260923 Claude Opus 5.5 (`claude-opus-5-5`, released 2026-09-22): 1M context / 128K output /
|
|
10
10
|
// adaptive thinking always on / effort low..max with a medium default, per the Opus 5.5
|
|
11
11
|
// overview, effort and pricing pages (platform.claude.com).
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
// 260929 Claude Sonnet 5.5 (`claude-sonnet-5-5`, released 2026-09-28): 1M context / 128K output /
|
|
13
|
+
// adaptive thinking / effort low..max with a high default, same price as Sonnet 5, per the Sonnet 5.5
|
|
14
|
+
// overview, migration guide, effort and pricing pages (platform.claude.com).
|
|
15
|
+
export const ANTHROPIC_MODELS = ["claude-fable-5-1", "claude-fable-5", "claude-sonnet-5-5", "claude-sonnet-5", "claude-opus-5-5", "claude-opus-5", "claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4-6", "claude-haiku-4-5"];
|
|
16
|
+
export const ANTHROPIC_MODEL_CONTEXT_WINDOWS: Record<string, number> = { "claude-fable-5-1": 1_000_000, "claude-sonnet-5-5": 1_000_000, "claude-sonnet-5": 1_000_000, "claude-fable-5": 1_000_000, "claude-opus-5-5": 1_000_000, "claude-opus-5": 1_000_000, "claude-opus-4-8": 1_000_000, "claude-opus-4-7": 1_000_000, "claude-opus-4-6": 1_000_000, "claude-sonnet-4-6": 1_000_000, "claude-haiku-4-5": 200_000 };
|
|
14
17
|
// All seeded Claude models support vision: https://platform.claude.com/docs/en/models/overview
|
|
15
18
|
export const ANTHROPIC_MODEL_INPUT_MODALITIES: Record<string, string[]> = Object.fromEntries(
|
|
16
19
|
ANTHROPIC_MODELS.map(id => [id, ["text", "image"]]),
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { OcxMessage } from "../types";
|
|
2
|
+
|
|
3
|
+
export const COMPACTION_IMAGE_NOTE = "[Earlier image omitted from this compaction request. "
|
|
4
|
+
+ "Retain existing analysis and source references; reopen the original attachment if visual details are still needed.]";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Summarize earlier image-bearing turns from their text, without paying their vision cost again.
|
|
8
|
+
* A later explicit final answer is a structural boundary, not proof that every image was read.
|
|
9
|
+
* Keep unphased/commentary-only turns and pending images. Never rewrite stored/raw history.
|
|
10
|
+
*/
|
|
11
|
+
export function omitEarlierCompactionImages(messages: readonly OcxMessage[]): OcxMessage[] {
|
|
12
|
+
let finalAnswerSeen = false;
|
|
13
|
+
const result = [...messages];
|
|
14
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
15
|
+
const message = messages[index]!;
|
|
16
|
+
if (message.role === "assistant") {
|
|
17
|
+
if (message.phase === "final_answer"
|
|
18
|
+
&& message.content.some(part => part.type === "text" && part.text.trim())) finalAnswerSeen = true;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (!finalAnswerSeen || (message.role !== "user" && message.role !== "toolResult")
|
|
22
|
+
|| !Array.isArray(message.content) || !message.content.some(part => part.type === "image")) continue;
|
|
23
|
+
result[index] = { ...message, content: message.content.map(part => part.type === "image"
|
|
24
|
+
? { type: "text" as const, text: COMPACTION_IMAGE_NOTE } : part) };
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
}
|
package/src/router.ts
CHANGED
|
@@ -89,6 +89,12 @@ export interface RouteResult {
|
|
|
89
89
|
combo?: ComboPick;
|
|
90
90
|
/** Bounded route-decision trace (RI-01); never contains secrets. */
|
|
91
91
|
routeDecision?: RouteDecisionTraceV1;
|
|
92
|
+
/**
|
|
93
|
+
* Set when a blocked-model redirect moved the request to a different
|
|
94
|
+
* provider. Caller credentials addressed to the source route must not follow
|
|
95
|
+
* it, exactly as for combo and policy routes.
|
|
96
|
+
*/
|
|
97
|
+
credentialDomainRewrite?: true;
|
|
92
98
|
}
|
|
93
99
|
|
|
94
100
|
export function captureRouteStaticPolicy(
|
|
@@ -549,6 +555,44 @@ export function comboRouteDecisionTrace(
|
|
|
549
555
|
// Codex uses a small number of control-plane model ids that are not part of the public GPT/o
|
|
550
556
|
// naming families. Keep this exact: a broad `codex-*` rule could capture a third-party model.
|
|
551
557
|
const CODEX_INTERNAL_OPENAI_MODELS = new Set(["codex-auto-review"]);
|
|
558
|
+
const MAX_BLOCKED_MODEL_REDIRECT_EDGES = 5;
|
|
559
|
+
|
|
560
|
+
interface BlockedModelRedirectState {
|
|
561
|
+
visited: Set<string>;
|
|
562
|
+
edges: number;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function crossProviderRedirectTarget(config: OcxConfig, providerName: string, value: string | undefined): string | undefined {
|
|
566
|
+
if (!value) return undefined;
|
|
567
|
+
const slash = value.indexOf("/");
|
|
568
|
+
if (slash <= 0) return undefined;
|
|
569
|
+
const targetProvider = value.slice(0, slash);
|
|
570
|
+
if (targetProvider === providerName || !hasOwnProvider(config.providers, targetProvider)) return undefined;
|
|
571
|
+
const configured = config.providers[targetProvider];
|
|
572
|
+
if (configured.disabled === true) return undefined;
|
|
573
|
+
try {
|
|
574
|
+
const effective = routedProviderConfig(targetProvider, configured);
|
|
575
|
+
const registry = providerMatchesRegistryTransportWithStaticGuards(targetProvider, configured)
|
|
576
|
+
? PROVIDER_REGISTRY.find(entry => entry.id === targetProvider)
|
|
577
|
+
: undefined;
|
|
578
|
+
const authMode = effective.authMode ?? registry?.authKind ?? "key";
|
|
579
|
+
if (authMode === "forward") return undefined; // The source caller bearer is stripped on a redirect.
|
|
580
|
+
if (authMode === "oauth") {
|
|
581
|
+
if (registry?.authKind !== "oauth") return undefined;
|
|
582
|
+
const usable = peekAuthStore()[targetProvider]?.accounts.some(account =>
|
|
583
|
+
account.paused !== true && account.needsReauth !== true
|
|
584
|
+
&& (account.credential.expires > Date.now()
|
|
585
|
+
? Boolean(account.credential.access.trim())
|
|
586
|
+
: Boolean(account.credential.refresh.trim()))
|
|
587
|
+
);
|
|
588
|
+
return usable ? value : undefined;
|
|
589
|
+
}
|
|
590
|
+
return authMode === "local" || effective.keyOptional === true || Boolean(effective.apiKey?.trim())
|
|
591
|
+
? value : undefined;
|
|
592
|
+
} catch {
|
|
593
|
+
return undefined;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
552
596
|
|
|
553
597
|
function isBareOpenAiFamilyModel(modelId: string): boolean {
|
|
554
598
|
return !modelId.includes("/")
|
|
@@ -562,8 +606,32 @@ function routeResult(
|
|
|
562
606
|
modelId: string,
|
|
563
607
|
routeKind: RouteDecisionKind,
|
|
564
608
|
routeReason: string,
|
|
609
|
+
redirectState: BlockedModelRedirectState,
|
|
565
610
|
): RouteResult {
|
|
566
|
-
const
|
|
611
|
+
const qualified = resolveBlockedModelRedirect(config, `${providerName}/${modelId}`);
|
|
612
|
+
const bare = resolveBlockedModelRedirect(config, modelId);
|
|
613
|
+
const crossTarget = config && (
|
|
614
|
+
crossProviderRedirectTarget(config, providerName, qualified)
|
|
615
|
+
?? crossProviderRedirectTarget(config, providerName, bare)
|
|
616
|
+
);
|
|
617
|
+
if (crossTarget && config) {
|
|
618
|
+
if (routeKind === "explicit-account") {
|
|
619
|
+
throw new Error("Blocked model redirect cannot leave a pinned account route");
|
|
620
|
+
}
|
|
621
|
+
const source = `${providerName}/${modelId}`;
|
|
622
|
+
if (redirectState.visited.has(source)) {
|
|
623
|
+
throw new Error(`Blocked model redirect cycle detected: ${source}`);
|
|
624
|
+
}
|
|
625
|
+
if (redirectState.edges >= MAX_BLOCKED_MODEL_REDIRECT_EDGES) {
|
|
626
|
+
throw new Error(`Blocked model redirect exceeded maximum redirect depth (${MAX_BLOCKED_MODEL_REDIRECT_EDGES})`);
|
|
627
|
+
}
|
|
628
|
+
redirectState.visited.add(source);
|
|
629
|
+
redirectState.edges += 1;
|
|
630
|
+
const targetRoute = routeModelInternal(config, crossTarget, true, undefined, false, false, redirectState);
|
|
631
|
+
return { ...targetRoute, routeReason: "blocked-model-redirect", credentialDomainRewrite: true };
|
|
632
|
+
}
|
|
633
|
+
// Existing bare mappings remain one post-resolution, same-provider substitution.
|
|
634
|
+
const redirected = bare;
|
|
567
635
|
const effectiveModelId = redirected ?? modelId;
|
|
568
636
|
const effectiveRouteReason = redirected ? "blocked-model-redirect" : routeReason;
|
|
569
637
|
const codexAccountMode = providerCodexAccountMode(providerName, provider);
|
|
@@ -628,6 +696,7 @@ function routeModelInternal(
|
|
|
628
696
|
policyEvidence?: PolicyRequestEvidence,
|
|
629
697
|
allowCompactionNativeFallback = false,
|
|
630
698
|
preview = false,
|
|
699
|
+
redirectState: BlockedModelRedirectState = { visited: new Set<string>(), edges: 0 },
|
|
631
700
|
): RouteResult {
|
|
632
701
|
const slash = modelId.indexOf("/");
|
|
633
702
|
// Policy namespace is system-reserved: an explicit `policy/<id>` or a
|
|
@@ -654,12 +723,25 @@ function routeModelInternal(
|
|
|
654
723
|
}
|
|
655
724
|
const selected = evaluation.candidates[evaluation.selectedIndex]!;
|
|
656
725
|
const concrete = `${selected.provider}/${selected.model}`;
|
|
657
|
-
const routed = routeModelInternal(config, concrete, true);
|
|
726
|
+
const routed = routeModelInternal(config, concrete, true, undefined, false, preview, redirectState);
|
|
727
|
+
if (routed.routeReason === "blocked-model-redirect" && !evaluation.candidates.some(candidate =>
|
|
728
|
+
candidate.provider === routed.providerName && candidate.model === routed.modelId && candidate.eligible
|
|
729
|
+
)) {
|
|
730
|
+
throw new NoEligiblePolicyCandidateError(policyId, evaluation.trace);
|
|
731
|
+
}
|
|
658
732
|
return {
|
|
659
733
|
...routed,
|
|
660
734
|
routeKind: "policy" as const,
|
|
661
|
-
routeReason: "policy-selected",
|
|
662
|
-
routeDecision:
|
|
735
|
+
routeReason: routed.routeReason === "blocked-model-redirect" ? "blocked-model-redirect" : "policy-selected",
|
|
736
|
+
routeDecision: {
|
|
737
|
+
...evaluation.trace,
|
|
738
|
+
selected: {
|
|
739
|
+
...evaluation.trace.selected,
|
|
740
|
+
provider: routed.providerName,
|
|
741
|
+
model: routed.modelId,
|
|
742
|
+
reason: routed.routeReason === "blocked-model-redirect" ? "blocked-model-redirect" : evaluation.trace.selected.reason,
|
|
743
|
+
},
|
|
744
|
+
},
|
|
663
745
|
};
|
|
664
746
|
}
|
|
665
747
|
if (slash > 0) {
|
|
@@ -683,8 +765,12 @@ function routeModelInternal(
|
|
|
683
765
|
if (!isCanonicalOpenAiForwardProvider(providerForCanonicalCheck)) {
|
|
684
766
|
throw new NoEnabledOpenAiProviderError(nativeModelId);
|
|
685
767
|
}
|
|
768
|
+
const accountQualified = resolveBlockedModelRedirect(config, modelId);
|
|
769
|
+
if (crossProviderRedirectTarget(config, OPENAI_CODEX_PROVIDER_ID, accountQualified)) {
|
|
770
|
+
throw new Error("Blocked model redirect cannot leave a pinned account route");
|
|
771
|
+
}
|
|
686
772
|
return {
|
|
687
|
-
...routeResult(config, OPENAI_CODEX_PROVIDER_ID, provider, nativeModelId, "explicit-account", "account-namespace"),
|
|
773
|
+
...routeResult(config, OPENAI_CODEX_PROVIDER_ID, provider, nativeModelId, "explicit-account", "account-namespace", redirectState),
|
|
688
774
|
// Exact account injection uses the pool credential machinery even when the canonical
|
|
689
775
|
// provider is globally Direct. The fixed id bypasses pool selection entirely.
|
|
690
776
|
codexAccountMode: "pool",
|
|
@@ -700,8 +786,8 @@ function routeModelInternal(
|
|
|
700
786
|
const concrete = `${combo.target.provider}/${combo.target.model}`;
|
|
701
787
|
// The selected target is already a concrete provider/model reference. Resolve it without
|
|
702
788
|
// consulting combo aliases again, otherwise an alias that shadows the target can recurse.
|
|
703
|
-
const routed = routeModelInternal(config, concrete, true, undefined);
|
|
704
|
-
return { ...routed, combo, routeKind: "combo" as const, routeReason: "combo-pick" };
|
|
789
|
+
const routed = routeModelInternal(config, concrete, true, undefined, false, preview, redirectState);
|
|
790
|
+
return { ...routed, combo, routeKind: "combo" as const, routeReason: routed.routeReason === "blocked-model-redirect" ? "blocked-model-redirect" : "combo-pick" };
|
|
705
791
|
}
|
|
706
792
|
}
|
|
707
793
|
|
|
@@ -761,7 +847,7 @@ function routeModelInternal(
|
|
|
761
847
|
// itself a known model (e.g. orcarouter/auto). Route it whole instead of stripping to the
|
|
762
848
|
// remainder, which would send a bare `auto` the upstream cannot resolve.
|
|
763
849
|
if (known.includes(modelId)) {
|
|
764
|
-
return routeResult(config, provName, prov, modelId, "explicit-provider", "explicit-provider-namespace");
|
|
850
|
+
return routeResult(config, provName, prov, modelId, "explicit-provider", "explicit-provider-namespace", redirectState);
|
|
765
851
|
}
|
|
766
852
|
// Codex-facing alias ids (`provider/vendor-model`) decode back to the native
|
|
767
853
|
// slash id via an exact known-id lookup; raw full-slash selectors keep working.
|
|
@@ -777,6 +863,7 @@ function routeModelInternal(
|
|
|
777
863
|
nativeModel,
|
|
778
864
|
"explicit-provider",
|
|
779
865
|
"explicit-provider-namespace",
|
|
866
|
+
redirectState,
|
|
780
867
|
);
|
|
781
868
|
}
|
|
782
869
|
}
|
|
@@ -785,7 +872,7 @@ function routeModelInternal(
|
|
|
785
872
|
if (isBareOpenAiFamilyModel(modelId)) {
|
|
786
873
|
const provider = config.providers[OPENAI_CODEX_PROVIDER_ID];
|
|
787
874
|
if (provider && provider.disabled !== true) {
|
|
788
|
-
return routeResult(config, OPENAI_CODEX_PROVIDER_ID, provider, modelId, "native", "native-family");
|
|
875
|
+
return routeResult(config, OPENAI_CODEX_PROVIDER_ID, provider, modelId, "native", "native-family", redirectState);
|
|
789
876
|
}
|
|
790
877
|
// Codex chooses a bare native model for compaction even when the operator's
|
|
791
878
|
// ordinary route is a third-party provider. Keep the native reservation
|
|
@@ -806,6 +893,7 @@ function routeModelInternal(
|
|
|
806
893
|
modelId,
|
|
807
894
|
"default-provider",
|
|
808
895
|
"compaction-default-provider",
|
|
896
|
+
redirectState,
|
|
809
897
|
);
|
|
810
898
|
}
|
|
811
899
|
}
|
|
@@ -815,18 +903,18 @@ function routeModelInternal(
|
|
|
815
903
|
for (const [provName, prov] of activeProviderEntries(config)) {
|
|
816
904
|
if (prov.defaultModel === modelId
|
|
817
905
|
|| (typeof prov.defaultModel === "string" && encodeRoutedModelId(prov.defaultModel) === modelId)) {
|
|
818
|
-
return routeResult(config, provName, prov, prov.defaultModel as string, "explicit-provider", "configured-default-model");
|
|
906
|
+
return routeResult(config, provName, prov, prov.defaultModel as string, "explicit-provider", "configured-default-model", redirectState);
|
|
819
907
|
}
|
|
820
908
|
}
|
|
821
909
|
|
|
822
|
-
const patternRoute = routeByKnownModelPattern(config, modelId);
|
|
910
|
+
const patternRoute = routeByKnownModelPattern(config, modelId, redirectState);
|
|
823
911
|
if (patternRoute) return patternRoute;
|
|
824
912
|
|
|
825
913
|
for (const [provName, prov] of activeProviderEntries(config)) {
|
|
826
914
|
if (prov.models && Array.isArray(prov.models)) {
|
|
827
915
|
const hit = (prov.models as string[]).find(id => id === modelId || encodeRoutedModelId(id) === modelId);
|
|
828
916
|
if (hit !== undefined) {
|
|
829
|
-
return routeResult(config, provName, prov, hit, "explicit-provider", "configured-model-list");
|
|
917
|
+
return routeResult(config, provName, prov, hit, "explicit-provider", "configured-model-list", redirectState);
|
|
830
918
|
}
|
|
831
919
|
}
|
|
832
920
|
}
|
|
@@ -846,7 +934,7 @@ function routeModelInternal(
|
|
|
846
934
|
}
|
|
847
935
|
if (aliasMatches[0]) {
|
|
848
936
|
const match = aliasMatches[0];
|
|
849
|
-
return routeResult(config, match.provider, config.providers[match.provider], match.model, "explicit-provider", "model-alias");
|
|
937
|
+
return routeResult(config, match.provider, config.providers[match.provider], match.model, "explicit-provider", "model-alias", redirectState);
|
|
850
938
|
}
|
|
851
939
|
|
|
852
940
|
if (config.defaultProvider === LEGACY_CHATGPT_PROVIDER_ID) {
|
|
@@ -855,7 +943,7 @@ function routeModelInternal(
|
|
|
855
943
|
if (hasOwnProvider(config.providers, config.defaultProvider)) {
|
|
856
944
|
const defaultProv = config.providers[config.defaultProvider];
|
|
857
945
|
if (defaultProv.disabled === true) throw new Error(`Default provider is disabled: ${config.defaultProvider}`);
|
|
858
|
-
return routeResult(config, config.defaultProvider, defaultProv, modelId, "default-provider", "default-provider");
|
|
946
|
+
return routeResult(config, config.defaultProvider, defaultProv, modelId, "default-provider", "default-provider", redirectState);
|
|
859
947
|
}
|
|
860
948
|
|
|
861
949
|
throw new Error(`No provider configured for model: ${modelId}`);
|
|
@@ -921,7 +1009,7 @@ export function routeConcreteModel(config: OcxConfig, modelId: string): RouteRes
|
|
|
921
1009
|
return routeModelInternal(config, modelId, true, undefined);
|
|
922
1010
|
}
|
|
923
1011
|
|
|
924
|
-
function routeByKnownModelPattern(config: OcxConfig, modelId: string): RouteResult | undefined {
|
|
1012
|
+
function routeByKnownModelPattern(config: OcxConfig, modelId: string, redirectState: BlockedModelRedirectState): RouteResult | undefined {
|
|
925
1013
|
for (const { providerNames, prefixes } of MODEL_PROVIDER_PATTERNS) {
|
|
926
1014
|
if (prefixes.some(prefix => modelId.startsWith(prefix))) {
|
|
927
1015
|
const matchingProvider = Object.entries(config.providers).find(
|
|
@@ -929,7 +1017,7 @@ function routeByKnownModelPattern(config: OcxConfig, modelId: string): RouteResu
|
|
|
929
1017
|
);
|
|
930
1018
|
if (matchingProvider) {
|
|
931
1019
|
const [provName, prov] = matchingProvider;
|
|
932
|
-
return routeResult(config, provName, prov, modelId, "explicit-provider", "model-pattern");
|
|
1020
|
+
return routeResult(config, provName, prov, modelId, "explicit-provider", "model-pattern", redirectState);
|
|
933
1021
|
}
|
|
934
1022
|
// Deliberately no "first provider with an Anthropic adapter" fallback here. Picking by
|
|
935
1023
|
// object insertion order, without checking `models`, `selectedModels`, `disabledModels` or
|
package/src/routing/analytics.ts
CHANGED
package/src/server/auth-cors.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { providerRelativeSendPathConfigError } from "../config/provider-relative-send-path";
|
|
2
|
-
import { modelCapabilitiesConfigError } from "../config/provider-validation";
|
|
2
|
+
import { contextTierRecordConfigError, modelCapabilitiesConfigError } from "../config/provider-validation";
|
|
3
3
|
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
4
4
|
import { initialModelSelection } from "../providers/initial-model-selection";
|
|
5
5
|
import { extractAccountId } from "../oauth/chatgpt";
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
} from "../config";
|
|
17
17
|
import {
|
|
18
18
|
apiKeyTransportConfigError,
|
|
19
|
+
projectContextConfigError,
|
|
19
20
|
autoReviewModelOverridesConfigError,
|
|
20
21
|
autoReviewModelTargetConfigError,
|
|
21
22
|
booleanRecordConfigError,
|
|
@@ -693,6 +694,8 @@ export function providerManagementConfigError(
|
|
|
693
694
|
return "provider must be a plain object";
|
|
694
695
|
}
|
|
695
696
|
const raw = provider as Record<string, unknown>;
|
|
697
|
+
const contextTiersError = contextTierRecordConfigError(raw.modelContextTiers);
|
|
698
|
+
if (contextTiersError) return contextTiersError;
|
|
696
699
|
const capabilitiesError = modelCapabilitiesConfigError(raw.modelCapabilities);
|
|
697
700
|
if (capabilitiesError) return capabilitiesError;
|
|
698
701
|
const pinsError = providerReasoningPinsConfigError(raw);
|
|
@@ -828,6 +831,8 @@ export function providerManagementConfigError(
|
|
|
828
831
|
}
|
|
829
832
|
const apiKeyTransportError = apiKeyTransportConfigError(typed);
|
|
830
833
|
if (apiKeyTransportError) return `provider ${name} ${apiKeyTransportError}`;
|
|
834
|
+
const projectContextError = projectContextConfigError(typed);
|
|
835
|
+
if (projectContextError) return `provider ${JSON.stringify(redactSecretString(name))} ${projectContextError}`;
|
|
831
836
|
const maxInputError = positiveIntegerRecordConfigError(raw.modelMaxInputTokens, "modelMaxInputTokens");
|
|
832
837
|
if (maxInputError) return `provider ${name} ${maxInputError}`;
|
|
833
838
|
const autoCompactError = modelAutoCompactTokenLimitsConfigError(
|
|
@@ -967,6 +972,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
|
|
|
967
972
|
defaultAliases: "editor",
|
|
968
973
|
adapter: "editor",
|
|
969
974
|
codexToolMode: "editor",
|
|
975
|
+
projectContext: "editor",
|
|
970
976
|
requestPacing: "editor",
|
|
971
977
|
mcpMaxTools: "editor",
|
|
972
978
|
mcpMaxSchemaBytes: "editor",
|
|
@@ -1021,6 +1027,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
|
|
|
1021
1027
|
modelPreset: "editor",
|
|
1022
1028
|
contextWindow: "editor",
|
|
1023
1029
|
modelContextWindows: "editor",
|
|
1030
|
+
modelContextTiers: "editor",
|
|
1024
1031
|
modelInputModalities: "editor",
|
|
1025
1032
|
modelCapabilities: "editor",
|
|
1026
1033
|
modelMaxInputTokens: "runtime",
|
|
@@ -1084,6 +1091,7 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
|
|
|
1084
1091
|
preserveReasoningContentModels: "editor",
|
|
1085
1092
|
requiresReasoningPlaceholderModels: "editor",
|
|
1086
1093
|
showThinkingSummary: "editor",
|
|
1094
|
+
hideRawReasoning: "editor",
|
|
1087
1095
|
retryOn429: "editor",
|
|
1088
1096
|
transientRetryOn5xx: "editor",
|
|
1089
1097
|
retryOnReset: "editor",
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import type { StorageCleanupPolicy } from "../types";
|
|
1
|
+
import type { OcxConfig, StorageCleanupPolicy } from "../types";
|
|
2
|
+
import { registerCodexLowQuotaProtection, type LowQuotaRegistration } from "../codex/low-quota-protection";
|
|
3
|
+
import type { LowQuotaEvent } from "../codex/low-quota-events";
|
|
2
4
|
import { startStateStoreSweeper } from "../lib/state-store-sweeper";
|
|
3
5
|
import {
|
|
4
6
|
abortStorageCleanupPolicyJobAsync,
|
|
@@ -40,10 +42,12 @@ type LeaseOwner = {
|
|
|
40
42
|
token: symbol;
|
|
41
43
|
applyPolicy: PolicyApply;
|
|
42
44
|
resources: ServerResourceOwnerLease;
|
|
45
|
+
lowQuota: LowQuotaRegistration;
|
|
43
46
|
};
|
|
44
47
|
|
|
45
48
|
export type ServerBackgroundLifecycleLease = {
|
|
46
49
|
scheduleStartupRun(): void;
|
|
50
|
+
listLowQuotaEvents(limit?: number): LowQuotaEvent[];
|
|
47
51
|
release(): Promise<void>;
|
|
48
52
|
releaseAfterFailedStart(): void;
|
|
49
53
|
};
|
|
@@ -148,6 +152,7 @@ function removeOwner(owner: LeaseOwner): boolean {
|
|
|
148
152
|
|
|
149
153
|
function releaseOwnerSynchronously(owner: LeaseOwner): "inactive" | "shared" | "last" {
|
|
150
154
|
if (!removeOwner(owner)) return "inactive";
|
|
155
|
+
owner.lowQuota();
|
|
151
156
|
owner.resources.release();
|
|
152
157
|
const nextOwner = owners.at(-1);
|
|
153
158
|
if (nextOwner) {
|
|
@@ -168,6 +173,7 @@ function releaseOwnerSynchronously(owner: LeaseOwner): "inactive" | "shared" | "
|
|
|
168
173
|
*/
|
|
169
174
|
export function acquireServerBackgroundLifecycle(
|
|
170
175
|
applyPolicy: PolicyApply,
|
|
176
|
+
config: OcxConfig,
|
|
171
177
|
): ServerBackgroundLifecycleLease {
|
|
172
178
|
if (cleanupInProgress) {
|
|
173
179
|
throw new Error("server background lifecycle cleanup is still in progress");
|
|
@@ -177,6 +183,7 @@ export function acquireServerBackgroundLifecycle(
|
|
|
177
183
|
token: Symbol("server-background-lifecycle"),
|
|
178
184
|
applyPolicy,
|
|
179
185
|
resources: acquireServerResourceOwner(),
|
|
186
|
+
lowQuota: registerCodexLowQuotaProtection(config),
|
|
180
187
|
};
|
|
181
188
|
try {
|
|
182
189
|
if (!processLoops) {
|
|
@@ -186,12 +193,20 @@ export function acquireServerBackgroundLifecycle(
|
|
|
186
193
|
}
|
|
187
194
|
owners.push(owner);
|
|
188
195
|
} catch (error) {
|
|
196
|
+
owner.lowQuota();
|
|
189
197
|
owner.resources.release();
|
|
190
198
|
throw error;
|
|
191
199
|
}
|
|
192
200
|
|
|
193
201
|
let releaseFlight: Promise<void> | null = null;
|
|
202
|
+
function finishRelease(): Promise<void> {
|
|
203
|
+
const outcome = releaseOwnerSynchronously(owner);
|
|
204
|
+
if (outcome !== "last") return Promise.resolve();
|
|
205
|
+
cleanupInProgress = true;
|
|
206
|
+
return stopStoragePolicyWorker().finally(() => { cleanupInProgress = false; });
|
|
207
|
+
}
|
|
194
208
|
return {
|
|
209
|
+
listLowQuotaEvents(limit) { return owner.lowQuota.listEvents(limit); },
|
|
195
210
|
scheduleStartupRun() {
|
|
196
211
|
if (owners.some(candidate => candidate.token === owner.token)) {
|
|
197
212
|
scheduleStorageCleanupStartupRun();
|
|
@@ -199,19 +214,15 @@ export function acquireServerBackgroundLifecycle(
|
|
|
199
214
|
},
|
|
200
215
|
release() {
|
|
201
216
|
if (releaseFlight) return releaseFlight;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
cleanupInProgress = true;
|
|
208
|
-
releaseFlight = stopStoragePolicyWorker().finally(() => {
|
|
209
|
-
cleanupInProgress = false;
|
|
210
|
-
});
|
|
217
|
+
// Default installs have no low-quota write. Preserve synchronous owner removal;
|
|
218
|
+
// an unconditional await here extends process-global ownership into later starts.
|
|
219
|
+
if (!owner.lowQuota.hasPendingSave()) releaseFlight = finishRelease();
|
|
220
|
+
else releaseFlight = owner.lowQuota.flush().then(finishRelease, finishRelease);
|
|
211
221
|
return releaseFlight;
|
|
212
222
|
},
|
|
213
223
|
releaseAfterFailedStart() {
|
|
214
224
|
if (releaseFlight) return;
|
|
225
|
+
owner.lowQuota();
|
|
215
226
|
// Startup evaluation is scheduled only after both listeners bind, so a
|
|
216
227
|
// failed start cannot have spawned a Worker for this lease. Keep rollback
|
|
217
228
|
// synchronous so a caller may immediately retry a different port.
|
|
@@ -200,7 +200,8 @@ async function handleChatCompletionsWithBudget(
|
|
|
200
200
|
logCtx.provider = route.providerName;
|
|
201
201
|
logCtx.routeDecision = route.routeDecision;
|
|
202
202
|
settledRoute = route;
|
|
203
|
-
routeMayChangeCredentialDomain = route.combo !== undefined || route.routeKind === "policy"
|
|
203
|
+
routeMayChangeCredentialDomain = route.combo !== undefined || route.routeKind === "policy"
|
|
204
|
+
|| route.credentialDomainRewrite === true;
|
|
204
205
|
callerAuthorizationRoute = !routeMayChangeCredentialDomain
|
|
205
206
|
&& providerConsumesCallerAuthorization(route.provider);
|
|
206
207
|
if (route.provider.adapter === "cursor" || route.provider.adapter === "kiro") {
|
package/src/server/index.ts
CHANGED
|
@@ -640,6 +640,7 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
|
|
|
640
640
|
let remoteWorkspaceShutdown: (() => Promise<void>) | undefined;
|
|
641
641
|
const managementApiDeps: ManagementApiDeps = {
|
|
642
642
|
...deps.managementApi,
|
|
643
|
+
listLowQuotaEvents: limit => backgroundLifecycle?.listLowQuotaEvents(limit) ?? [],
|
|
643
644
|
remoteWorkspaceStopping: () => remoteWorkspaceStopping,
|
|
644
645
|
onRemoteWorkspaceShutdown: shutdown => { remoteWorkspaceShutdown = shutdown; }, linkSupervisor: () => optionalListeners.linkSupervisor(), linkListener: () => optionalListeners,
|
|
645
646
|
};
|
|
@@ -655,13 +656,10 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
|
|
|
655
656
|
return workspaceRuntimeFlight;
|
|
656
657
|
};
|
|
657
658
|
try {
|
|
658
|
-
backgroundLifecycle = acquireServerBackgroundLifecycle(applyPolicy);
|
|
659
|
+
backgroundLifecycle = acquireServerBackgroundLifecycle(applyPolicy, config);
|
|
659
660
|
unregisterQuotaAutoRefresh = (deps.registerCodexQuotaAutoRefreshWorker
|
|
660
661
|
?? registerCodexQuotaAutoRefreshWorker)(config);
|
|
661
|
-
//
|
|
662
|
-
// processes; poll the file so Logs/Usage display prices follow them live.
|
|
663
|
-
// Started inside the guarded startup transaction so the catch below can
|
|
664
|
-
// release the owner-scoped lease on any listener failure.
|
|
662
|
+
// Poll external pricing edits; the startup catch releases this owner-scoped lease.
|
|
665
663
|
userCostOverlayReconciler = startUserCostOverlayReconciler({ liveConfig: config });
|
|
666
664
|
const serveOptions = createServeOptions({
|
|
667
665
|
drainingResponse,
|
|
@@ -754,9 +752,7 @@ function startServerWithSpendLedgerOwner(port: number | undefined, deps: StartSe
|
|
|
754
752
|
value: async (closeActiveConnections?: boolean): Promise<void> => {
|
|
755
753
|
remoteWorkspaceStopping = true;
|
|
756
754
|
liveCallBindings.clear();
|
|
757
|
-
// Disarm the package-tree restart timer before
|
|
758
|
-
// replacement callback must not call acceptSystemRestart() after stop() has
|
|
759
|
-
// begun, or it would schedule a drain-and-restart on a stopped server.
|
|
755
|
+
// Disarm the package-tree restart timer before teardown can schedule another restart.
|
|
760
756
|
if (!packageRefreshStopped) {
|
|
761
757
|
packageRefreshStopped = true;
|
|
762
758
|
stopPackageRefresh();
|
|
@@ -87,6 +87,7 @@ export interface ClientEncodedDelivery {
|
|
|
87
87
|
fold: {
|
|
88
88
|
replayCacheScope?: OcxReasoningReplayScopeRef;
|
|
89
89
|
hideThinkingSummary?: boolean;
|
|
90
|
+
hideRawReasoning?: boolean;
|
|
90
91
|
toolNsMap?: Map<string, { namespace: string; name: string; freeform?: true }>;
|
|
91
92
|
declaredToolNames?: ReadonlySet<string>;
|
|
92
93
|
toolParameterSchemas?: ReadonlyMap<string, Record<string, unknown>>;
|
|
@@ -164,6 +165,8 @@ export async function deliverClientEncodedResponse(input: ClientEncodedDelivery)
|
|
|
164
165
|
enforceDeclaredToolNames: false,
|
|
165
166
|
translatorBudget,
|
|
166
167
|
recordBufferedDelivery: false,
|
|
168
|
+
// The client never receives this body, and its Chat/Messages wire has no envelope field.
|
|
169
|
+
omitHiddenReasoningEnvelope: true,
|
|
167
170
|
});
|
|
168
171
|
} catch {
|
|
169
172
|
return undefined;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OcxConfig } from "../../types";
|
|
2
|
+
import type { LowQuotaEvent } from "../../codex/low-quota-events";
|
|
2
3
|
import type { Channel } from "../../update/index";
|
|
3
4
|
import type { UpdateCheckResult } from "../../update/job";
|
|
4
5
|
import type { NativeProfileApiDeps } from "../../codex/native-profile-api";
|
|
@@ -44,6 +45,8 @@ export interface ManagementRequestIngress {
|
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
export interface ManagementApiDeps {
|
|
48
|
+
/** Bound to this server's lifecycle owner; absent in direct route tests. */
|
|
49
|
+
listLowQuotaEvents?: (limit?: number) => LowQuotaEvent[];
|
|
47
50
|
/** Bound Claude intercept state, injectable for isolated management-route tests. */
|
|
48
51
|
getClaudeInterceptState?: typeof import("../../claude/intercept/runtime").getClaudeInterceptState;
|
|
49
52
|
/** Reconciliation seam for field-scoped rollback tests. */
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Auth is enforced by the management dispatcher before this lazy route runs. */
|
|
2
|
+
import { jsonResponse } from "../auth-cors";
|
|
3
|
+
import type { ManagementContext } from "./context";
|
|
4
|
+
|
|
5
|
+
export async function handleLowQuotaRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
6
|
+
const { url, req, config } = ctx;
|
|
7
|
+
if (url.pathname !== "/api/codex-auth/low-quota-events" || req.method !== "GET") return null;
|
|
8
|
+
const raw = url.searchParams.get("limit");
|
|
9
|
+
if (raw !== null && (!/^[0-9]+$/.test(raw) || !Number.isSafeInteger(Number(raw)))) {
|
|
10
|
+
return jsonResponse({ error: { code: "invalid_limit", message: "limit must be a non-negative integer" } }, 400, req, config);
|
|
11
|
+
}
|
|
12
|
+
const limit = raw === null ? 20 : Math.min(100, Number(raw));
|
|
13
|
+
return jsonResponse({ events: ctx.deps.listLowQuotaEvents?.(limit) ?? [] }, 200, req, config);
|
|
14
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { parseAnthropicModelRoutes, readAnthropicModelRoutes } from "../../oauth/anthropic-model-routes";
|
|
1
2
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
2
3
|
import { readFileSync } from "node:fs";
|
|
3
4
|
import type { CatalogModel } from "../../codex/catalog";
|
|
@@ -545,7 +546,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
545
546
|
if (req.method !== "GET" && !isPlainRecord(rawBody)) {
|
|
546
547
|
return jsonResponse({ error: "body must be an object" }, 400);
|
|
547
548
|
}
|
|
548
|
-
const fields = rawBody as { provider?: unknown; enabled?: unknown; strategy?: unknown; stickyLimit?: unknown; autoSwitchThreshold?: unknown; quotaWindow?: unknown; maxConcurrentPerAccount?: unknown };
|
|
549
|
+
const fields = rawBody as { provider?: unknown; enabled?: unknown; strategy?: unknown; stickyLimit?: unknown; autoSwitchThreshold?: unknown; quotaWindow?: unknown; maxConcurrentPerAccount?: unknown; routes?: unknown };
|
|
549
550
|
const provider = req.method === "GET"
|
|
550
551
|
? (url.searchParams.get("provider") ?? "").trim().toLowerCase()
|
|
551
552
|
: (typeof fields.provider === "string" ? fields.provider.trim().toLowerCase() : "");
|
|
@@ -575,6 +576,10 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
575
576
|
if (parsed === null) return jsonResponse({ error: "autoSwitchThreshold must be an integer 0-100" }, 400);
|
|
576
577
|
autoSwitchThreshold = parsed;
|
|
577
578
|
}
|
|
579
|
+
if (Object.hasOwn(fields, "routes") && kind !== "anthropic") return jsonResponse({ error: "routes are only part of the anthropic pool contract" }, 400);
|
|
580
|
+
const parsedRoutes = Object.hasOwn(fields, "routes") && fields.routes !== null
|
|
581
|
+
? parseAnthropicModelRoutes(fields.routes) : null;
|
|
582
|
+
if (parsedRoutes && !parsedRoutes.ok) return jsonResponse({ error: parsedRoutes.error }, 400);
|
|
578
583
|
if (fields.quotaWindow !== undefined && kind !== "anthropic") {
|
|
579
584
|
return jsonResponse({ error: "quotaWindow is only part of the anthropic pool contract" }, 400);
|
|
580
585
|
}
|
|
@@ -607,6 +612,10 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
607
612
|
if (stickyLimit !== undefined) pool.stickyLimit = stickyLimit;
|
|
608
613
|
if (autoSwitchThreshold !== undefined) pool.autoSwitchThreshold = autoSwitchThreshold;
|
|
609
614
|
if (quotaWindow !== undefined) pool.quotaWindow = quotaWindow as never;
|
|
615
|
+
if (Object.hasOwn(fields, "routes")) {
|
|
616
|
+
if (fields.routes === null) delete pool.routes;
|
|
617
|
+
else if (parsedRoutes?.ok) pool.routes = parsedRoutes.routes;
|
|
618
|
+
}
|
|
610
619
|
config.anthropicAccountPool = pool;
|
|
611
620
|
} else {
|
|
612
621
|
const prov = config.providers[provider]!;
|
|
@@ -650,6 +659,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
650
659
|
strategy: normalizeAccountPoolStrategy(pool.strategy),
|
|
651
660
|
stickyLimit: normalizeAccountPoolStickyLimit(pool.stickyLimit),
|
|
652
661
|
quotaWindow: normalizeAccountPoolQuotaWindow(pool.quotaWindow),
|
|
662
|
+
...readAnthropicModelRoutes(pool.routes),
|
|
653
663
|
experimental: true,
|
|
654
664
|
});
|
|
655
665
|
}
|
|
@@ -666,6 +676,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
666
676
|
stickyLimit?: unknown;
|
|
667
677
|
quotaWindow?: unknown;
|
|
668
678
|
maxConcurrentPerAccount?: unknown;
|
|
679
|
+
routes?: unknown;
|
|
669
680
|
};
|
|
670
681
|
const provider = typeof body.provider === "string" ? body.provider.trim().toLowerCase() : "";
|
|
671
682
|
if (provider !== "anthropic") {
|
|
@@ -677,6 +688,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
677
688
|
if (!provider || !prov || poolSettingsCapability(provider, prov) !== "generic") {
|
|
678
689
|
return jsonResponse({ error: "pool config is only supported for anthropic and generic OAuth providers" }, 400);
|
|
679
690
|
}
|
|
691
|
+
if (Object.hasOwn(body, "routes")) return jsonResponse({ error: "routes are only part of the anthropic pool contract" }, 400);
|
|
680
692
|
if (body.quotaWindow !== undefined) {
|
|
681
693
|
return jsonResponse({ error: "quotaWindow is not part of the generic pool contract yet" }, 400);
|
|
682
694
|
}
|
|
@@ -762,12 +774,19 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
762
774
|
}
|
|
763
775
|
quotaWindow = parsed;
|
|
764
776
|
}
|
|
777
|
+
const parsedLegacyRoutes = Object.hasOwn(body, "routes") && body.routes !== null
|
|
778
|
+
? parseAnthropicModelRoutes(body.routes) : null;
|
|
779
|
+
if (parsedLegacyRoutes && !parsedLegacyRoutes.ok) return jsonResponse({ error: parsedLegacyRoutes.error }, 400);
|
|
780
|
+
const routes = Object.hasOwn(body, "routes")
|
|
781
|
+
? (parsedLegacyRoutes?.ok ? parsedLegacyRoutes.routes : undefined)
|
|
782
|
+
: config.anthropicAccountPool?.routes;
|
|
765
783
|
config.anthropicAccountPool = {
|
|
766
784
|
enabled,
|
|
767
785
|
autoSwitchThreshold: threshold,
|
|
768
786
|
...(strategy !== undefined ? { strategy } : {}),
|
|
769
787
|
...(stickyLimit !== undefined ? { stickyLimit } : {}),
|
|
770
788
|
...(quotaWindow !== undefined ? { quotaWindow } : {}),
|
|
789
|
+
...(routes !== undefined ? { routes } : {}),
|
|
771
790
|
};
|
|
772
791
|
saveConfigPreservingClaudeCode(config);
|
|
773
792
|
reconcileLiveStateStores();
|
|
@@ -779,6 +798,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
779
798
|
strategy: normalizeAccountPoolStrategy(strategy),
|
|
780
799
|
stickyLimit: normalizeAccountPoolStickyLimit(stickyLimit),
|
|
781
800
|
quotaWindow: normalizeAccountPoolQuotaWindow(quotaWindow),
|
|
801
|
+
routes: routes ?? null,
|
|
782
802
|
experimental: true,
|
|
783
803
|
});
|
|
784
804
|
}
|