@bitkyc08/opencodex 2.10.0 → 2.10.1
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 +77 -0
- package/README.md +4 -10
- package/bin/ocx.mjs +71 -18
- package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
- package/gui/dist/assets/index-ChZQsmBY.js +70 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/alibaba-color.svg +1 -1
- package/gui/dist/provider-icons/antigravity-color.svg +1 -1
- package/gui/dist/provider-icons/claude-color.svg +1 -1
- package/gui/dist/provider-icons/cline-color.svg +16 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/copilot-color.svg +1 -1
- package/gui/dist/provider-icons/cursor-color.svg +1 -1
- package/gui/dist/provider-icons/deepseek-color.svg +1 -1
- package/gui/dist/provider-icons/firepass-color.svg +1 -1
- package/gui/dist/provider-icons/fireworks-color.svg +1 -1
- package/gui/dist/provider-icons/gemini-color.svg +1 -1
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
- package/gui/dist/provider-icons/grok.svg +1 -1
- package/gui/dist/provider-icons/groq-color.svg +1 -1
- package/gui/dist/provider-icons/huggingface-color.svg +1 -1
- package/gui/dist/provider-icons/kimi-color.svg +1 -1
- package/gui/dist/provider-icons/kiro-color.svg +2 -2
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
- package/gui/dist/provider-icons/mistral-color.svg +1 -1
- package/gui/dist/provider-icons/moonshot-color.svg +1 -1
- package/gui/dist/provider-icons/nvidia-color.svg +1 -1
- package/gui/dist/provider-icons/ollama-color.svg +1 -1
- package/gui/dist/provider-icons/openai.svg +1 -1
- package/gui/dist/provider-icons/opencode.svg +2 -1
- package/gui/dist/provider-icons/openrouter-color.svg +1 -1
- package/gui/dist/provider-icons/pi.svg +2 -2
- package/gui/dist/provider-icons/qianfan-color.svg +1 -1
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
- package/gui/dist/provider-icons/vllm-color.svg +1 -1
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
- package/package.json +8 -4
- package/src/adapters/anthropic.ts +208 -14
- package/src/adapters/base.ts +16 -5
- package/src/adapters/cursor/effort-map.ts +3 -2
- package/src/adapters/cursor/framing.ts +39 -0
- package/src/adapters/cursor/live-transport.ts +105 -95
- package/src/adapters/cursor/native-exec.ts +32 -6
- package/src/adapters/cursor/protobuf-request.ts +20 -15
- package/src/adapters/cursor/request-builder.ts +21 -7
- package/src/adapters/cursor/types.ts +7 -0
- package/src/adapters/google-antigravity-replay.ts +237 -21
- package/src/adapters/google-truncation.ts +11 -0
- package/src/adapters/google.ts +50 -9
- package/src/adapters/identity.ts +39 -6
- package/src/adapters/kiro-errors.ts +11 -0
- package/src/adapters/kiro-events.ts +19 -1
- package/src/adapters/kiro-thinking.ts +10 -2
- package/src/adapters/kiro-tools.ts +10 -1
- package/src/adapters/kiro.ts +37 -11
- package/src/adapters/openai-chat.ts +284 -83
- package/src/adapters/openai-responses.ts +182 -24
- package/src/bridge.ts +177 -7
- package/src/chat/outbound.ts +78 -23
- package/src/claude/agents-inject.ts +27 -5
- package/src/claude/inbound.ts +11 -1
- package/src/claude/model-info.ts +13 -10
- package/src/claude/outbound.ts +17 -0
- package/src/cli/account-api.ts +24 -0
- package/src/cli/account-auth.ts +31 -6
- package/src/cli/account-main.ts +317 -0
- package/src/cli/account.ts +5 -0
- package/src/cli/claude.ts +2 -1
- package/src/cli/doctor.ts +93 -22
- package/src/cli/export-command.ts +26 -12
- package/src/cli/help.ts +8 -6
- package/src/cli/index.ts +56 -22
- package/src/cli/integrations.ts +84 -1
- package/src/cli/observe.ts +54 -1
- package/src/cli/opencode.ts +2 -1
- package/src/cli/provider-runtime.ts +18 -1
- package/src/cli/route-policy.ts +92 -0
- package/src/cli/runtime-api.ts +6 -3
- package/src/cli/star-prompt.ts +71 -15
- package/src/cli/status.ts +1 -1
- package/src/cli/v2.ts +36 -9
- package/src/clients/config-export.ts +687 -10
- package/src/codex/account-lifecycle.ts +30 -5
- package/src/codex/account-usability.ts +22 -2
- package/src/codex/admission.ts +255 -0
- package/src/codex/auth-api.ts +427 -140
- package/src/codex/auth-context.ts +155 -30
- package/src/codex/autostart-health.ts +8 -1
- package/src/codex/catalog/account-models.ts +62 -0
- package/src/codex/catalog/aggregation.ts +14 -1
- package/src/codex/catalog/bundled.ts +282 -32
- package/src/codex/catalog/filesystem-evidence.ts +302 -0
- package/src/codex/catalog/metadata.ts +51 -6
- package/src/codex/catalog/parsing.ts +6 -3
- package/src/codex/catalog/provider-fetch.ts +576 -41
- package/src/codex/catalog/sync.ts +505 -66
- package/src/codex/catalog-admission.ts +197 -0
- package/src/codex/catalog-write-serialization.ts +241 -0
- package/src/codex/catalog.ts +2 -1
- package/src/codex/codex-write-lock.ts +372 -0
- package/src/codex/convergence-types.ts +593 -0
- package/src/codex/convergence.ts +441 -0
- package/src/codex/desired-state.ts +177 -0
- package/src/codex/features.ts +52 -8
- package/src/codex/generation.ts +202 -0
- package/src/codex/history-job.ts +257 -0
- package/src/codex/history-lock.ts +241 -0
- package/src/codex/history-migration-guardian.ts +18 -5
- package/src/codex/history-provider.ts +9 -2
- package/src/codex/history-transition.ts +105 -0
- package/src/codex/history-worker.ts +176 -0
- package/src/codex/inject-coordination.ts +245 -0
- package/src/codex/inject.ts +605 -124
- package/src/codex/integration-record.ts +266 -0
- package/src/codex/internal/catalog-writer.ts +203 -0
- package/src/codex/internal/history-writer.ts +80 -0
- package/src/codex/journal.ts +10 -1
- package/src/codex/main-account-cache.ts +24 -0
- package/src/codex/management-convergence.ts +114 -0
- package/src/codex/native-main-admission.ts +47 -0
- package/src/codex/native-main-auth-temp.ts +187 -0
- package/src/codex/native-main-claim.ts +167 -0
- package/src/codex/native-main-lock-file.ts +158 -0
- package/src/codex/native-main-owner.ts +315 -0
- package/src/codex/native-profile-api.ts +247 -0
- package/src/codex/native-profile-manager.ts +1512 -0
- package/src/codex/native-profile-processes.ts +121 -0
- package/src/codex/native-profile-recovery.ts +99 -0
- package/src/codex/native-profile-stage-store.ts +387 -0
- package/src/codex/native-profile-startup.ts +340 -0
- package/src/codex/native-profile-store.ts +855 -0
- package/src/codex/native-profile-types.ts +120 -0
- package/src/codex/native-residue.ts +557 -0
- package/src/codex/project-config-warnings.ts +18 -4
- package/src/codex/prompt-journal.ts +311 -0
- package/src/codex/prompt-layers.ts +967 -0
- package/src/codex/prompt-lock.ts +143 -0
- package/src/codex/quota-rejection.ts +224 -0
- package/src/codex/quota.ts +86 -3
- package/src/codex/routing.ts +299 -62
- package/src/codex/runtime.ts +159 -38
- package/src/codex/shim.ts +39 -13
- package/src/codex/subagent-model-fallback.ts +73 -12
- package/src/codex/transition-state.ts +604 -0
- package/src/codex/upstream-host-health.ts +70 -0
- package/src/codex/user-identity.ts +266 -0
- package/src/codex/write-coordination.ts +114 -0
- package/src/config.ts +562 -26
- package/src/generated/jawcode-model-metadata.ts +2 -2
- package/src/grok/inject.ts +15 -4
- package/src/grok/inspect.ts +45 -0
- package/src/images/loop.ts +113 -20
- package/src/integrations/config-io.ts +151 -0
- package/src/integrations/journal.ts +315 -0
- package/src/integrations/merge.ts +135 -0
- package/src/integrations/native/ownership-preflight.ts +165 -0
- package/src/integrations/ownership.ts +111 -0
- package/src/integrations/registry.ts +101 -0
- package/src/integrations/serialize.ts +235 -0
- package/src/integrations/state.ts +290 -0
- package/src/integrations/store.ts +103 -0
- package/src/integrations/writer.ts +492 -0
- package/src/lib/bounded-body.ts +46 -8
- package/src/lib/bun-runtime.ts +110 -1
- package/src/lib/bun-stream-caps.ts +2 -1
- package/src/lib/redact.ts +407 -2
- package/src/lib/shadow-call.ts +24 -0
- package/src/lib/translator-budget.ts +10 -0
- package/src/lib/upstream-reachability.ts +91 -0
- package/src/lib/upstream-retry.ts +154 -2
- package/src/lib/windows-secret-acl.ts +212 -11
- package/src/lib/winsw.ts +9 -3
- package/src/oauth/index.ts +61 -3
- package/src/oauth/key-providers.ts +4 -0
- package/src/oauth/kiro.ts +50 -6
- package/src/oauth/store.ts +31 -6
- package/src/oauth/token-guardian.ts +9 -3
- package/src/providers/codex-capacity.ts +288 -0
- package/src/providers/derive.ts +33 -1
- package/src/providers/free-directory.ts +3 -1
- package/src/providers/key-failover.ts +71 -3
- package/src/providers/openai-sidecar.ts +64 -4
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota.ts +334 -26
- package/src/providers/registry.ts +284 -16
- package/src/providers/xai-transport.ts +11 -4
- package/src/responses/compaction.ts +8 -1
- package/src/responses/hosted-tool-policy.ts +9 -0
- package/src/responses/parser.ts +44 -2
- package/src/responses/reasoning-envelope.ts +9 -1
- package/src/responses/reasoning-replay-cache.ts +105 -0
- package/src/responses/spill-store.ts +45 -8
- package/src/responses/state.ts +161 -17
- package/src/router.ts +302 -16
- package/src/routing/analytics.ts +377 -0
- package/src/routing/capability.ts +204 -0
- package/src/routing/cost.ts +77 -0
- package/src/routing/evaluator.ts +444 -0
- package/src/routing/health.ts +401 -0
- package/src/routing/history/cursor.ts +43 -0
- package/src/routing/history/indexer.ts +590 -0
- package/src/routing/history/schema.ts +72 -0
- package/src/routing/profile.ts +423 -0
- package/src/routing/quota.ts +124 -0
- package/src/routing/request-evidence.ts +45 -0
- package/src/routing/trace.ts +686 -0
- package/src/server/auth-cors.ts +46 -6
- package/src/server/chat-completions.ts +28 -13
- package/src/server/claude-messages.ts +23 -15
- package/src/server/gui-static.ts +39 -10
- package/src/server/images.ts +10 -1
- package/src/server/index.ts +238 -52
- package/src/server/lifecycle.ts +228 -9
- package/src/server/live.ts +14 -3
- package/src/server/management/agent-settings-routes.ts +64 -14
- package/src/server/management/combo-routes.ts +5 -5
- package/src/server/management/config-routes.ts +1 -1
- package/src/server/management/context.ts +42 -2
- package/src/server/management/integration-routes.ts +538 -0
- package/src/server/management/logs-usage-routes.ts +1 -1
- package/src/server/management/model-routes.ts +32 -113
- package/src/server/management/model-rows.ts +117 -0
- package/src/server/management/native-integration-routes.ts +587 -0
- package/src/server/management/oauth-account-routes.ts +1 -1
- package/src/server/management/provider-routes.ts +218 -117
- package/src/server/management/request-history-routes.ts +191 -0
- package/src/server/management/routing-analytics-routes.ts +74 -0
- package/src/server/management/routing-profile-routes.ts +177 -0
- package/src/server/management/shared.ts +2 -2
- package/src/server/management/sidebar-routes.ts +47 -31
- package/src/server/management/sync-response.ts +69 -0
- package/src/server/management/system-restart.ts +276 -24
- package/src/server/management/system-routes.ts +4 -0
- package/src/server/management-api.ts +84 -9
- package/src/server/management-auth.ts +43 -5
- package/src/server/relay-eager.ts +82 -42
- package/src/server/relay.ts +120 -6
- package/src/server/request-log.ts +26 -6
- package/src/server/responses/collaboration.ts +63 -8
- package/src/server/responses/compact.ts +272 -41
- package/src/server/responses/core.ts +730 -132
- package/src/server/responses/fetch-helpers.ts +15 -1
- package/src/server/responses-item-id-repair.ts +32 -3
- package/src/server/responses-json-events.ts +52 -0
- package/src/server/responses-snapshot-repair.ts +621 -0
- package/src/server/search.ts +51 -6
- package/src/server/sse-payload-rewrite.ts +89 -12
- package/src/server/startup-health-cache.ts +7 -1
- package/src/server/ws-bridge.ts +11 -17
- package/src/service-manager-probe.ts +297 -0
- package/src/service.ts +222 -32
- package/src/tray/windows-tray.ps1 +9 -0
- package/src/tray/windows.ts +15 -7
- package/src/types.ts +194 -14
- package/src/update/index.ts +13 -13
- package/src/update/job.ts +24 -21
- package/src/update/notify.ts +7 -3
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +129 -10
- package/src/usage/log.ts +50 -15
- package/src/usage/summary.ts +4 -4
- package/src/vision/index.ts +6 -1
- package/src/web-search/loop.ts +161 -34
- package/gui/dist/assets/index-OY43ubAq.css +0 -1
- package/gui/dist/assets/index-YwNnKZcL.js +0 -67
- package/gui/dist/provider-icons/antigravity.svg +0 -1
- package/gui/dist/provider-icons/claude.svg +0 -1
- package/gui/dist/provider-icons/copilot.svg +0 -1
- package/gui/dist/provider-icons/cursor.svg +0 -2
- package/gui/dist/provider-icons/gemini.svg +0 -1
- package/gui/dist/provider-icons/grok-color.svg +0 -1
- package/gui/dist/provider-icons/kiro.svg +0 -14
|
@@ -45,6 +45,8 @@ export interface FreeDirectoryProvider {
|
|
|
45
45
|
keyOptional?: boolean;
|
|
46
46
|
models?: string[];
|
|
47
47
|
liveModels: boolean;
|
|
48
|
+
/** Anthropic-compatible gateways that may close streams before terminal frames. */
|
|
49
|
+
anthropicEofTolerance?: boolean;
|
|
48
50
|
note?: string;
|
|
49
51
|
googleMode?: "ai-studio" | "vertex";
|
|
50
52
|
}
|
|
@@ -116,7 +118,7 @@ const CONNECTABLE: Record<string, ConnectableOverride> = {
|
|
|
116
118
|
// `unverified` and drops the shared verification date rather than borrowing it.
|
|
117
119
|
bytez: openAi("https://api.bytez.com/models/v2/openai/v1", "https://bytez.com", { verification: "unverified", lastVerified: undefined, documentationUrl: "https://docs.bytez.com/", discovery: "static", liveModels: false, models: ["meta-llama/Llama-3.3-70B-Instruct", "mistralai/Mistral-7B-Instruct-v0.3", "Qwen/Qwen2.5-72B-Instruct"], note: "The recurring-credit classification is retained from the requested catalog, but the current reset terms could not be independently verified." }),
|
|
118
120
|
"nous-research": openAi("https://inference-api.nousresearch.com/v1", "https://portal.nousresearch.com", { discovery: "static", liveModels: false, models: ["Hermes-4-405B", "Hermes-4-70B"] }),
|
|
119
|
-
agentrouter: { baseUrl: "https://agentrouter.org", dashboardUrl: "https://agentrouter.org", adapter: "anthropic", authKind: "key", supportLevel: "experimental", verification: "primary", modelsUrl: "https://agentrouter.org/v1/models", lastVerified: LAST_VERIFIED, discovery: "live", liveModels: true },
|
|
121
|
+
agentrouter: { baseUrl: "https://agentrouter.org", dashboardUrl: "https://agentrouter.org", adapter: "anthropic", authKind: "key", supportLevel: "experimental", verification: "primary", modelsUrl: "https://agentrouter.org/v1/models", lastVerified: LAST_VERIFIED, discovery: "live", liveModels: true, anthropicEofTolerance: true },
|
|
120
122
|
ai21: openAi("https://api.ai21.com/studio/v1", "https://studio.ai21.com/account/api-key", { supportLevel: "supported", verification: "official", documentationUrl: "https://docs.ai21.com/reference/models" }),
|
|
121
123
|
baichuan: openAi("https://api.baichuan-ai.com/v1", "https://platform.baichuan-ai.com/console/apikey", { verification: "official" }),
|
|
122
124
|
// Verified end-to-end 2026-07-30: /v1/models returns the OpenAI-shaped live catalog (13 models),
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Modelled after src/codex/routing.ts cooldown logic but scoped to plain API-key pools.
|
|
10
10
|
*/
|
|
11
11
|
import { saveConfigPreservingClaudeCode } from "../config";
|
|
12
|
-
import type { OcxConfig, OcxProviderConfig } from "../types";
|
|
12
|
+
import type { OcxConfig, OcxProviderConfig, RateLimitRetryPolicy } from "../types";
|
|
13
13
|
import { resolveProviderTransport, type OcxProviderTransport } from "./xai-transport";
|
|
14
14
|
import { sweepExpiredOnWrite } from "../lib/state-store-sweeper";
|
|
15
15
|
|
|
@@ -22,6 +22,18 @@ interface KeyCooldown {
|
|
|
22
22
|
const DEFAULT_COOLDOWN_MS = 60_000;
|
|
23
23
|
const MAX_COOLDOWN_MS = 10 * 60_000; // cap at 10 min for api-key rotation
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Default same-target 429 retry policy used when a provider opts in via a bare
|
|
27
|
+
* `retryOn429: {}` (presence = opt-in with these defaults).
|
|
28
|
+
*/
|
|
29
|
+
const DEFAULT_RATE_LIMIT_RETRY = {
|
|
30
|
+
enabled: true,
|
|
31
|
+
attempts: 3,
|
|
32
|
+
intervalMs: 5_000,
|
|
33
|
+
maxIntervalMs: 60_000,
|
|
34
|
+
respectRetryAfter: true,
|
|
35
|
+
} as const satisfies Required<RateLimitRetryPolicy>;
|
|
36
|
+
|
|
25
37
|
/** Map<`${providerName}\0${keyId}`, KeyCooldown> */
|
|
26
38
|
const keyCooldowns = new Map<string, KeyCooldown>();
|
|
27
39
|
|
|
@@ -29,21 +41,32 @@ function cooldownKey(providerName: string, keyId: string): string {
|
|
|
29
41
|
return `${providerName}\0${keyId}`;
|
|
30
42
|
}
|
|
31
43
|
|
|
44
|
+
/**
|
|
45
|
+
* Parse an upstream `Retry-After` header: numeric seconds (including `0`) or an HTTP-date.
|
|
46
|
+
* Returns a bounded delay in ms (1..MAX_COOLDOWN_MS), or undefined when the value is
|
|
47
|
+
* malformed. An HTTP-date already in the past yields an immediate (1 ms) retry.
|
|
48
|
+
*/
|
|
32
49
|
function parseRetryAfterMs(value: string | null | undefined, now = Date.now()): number | undefined {
|
|
33
50
|
const text = value?.trim();
|
|
34
51
|
if (!text) return undefined;
|
|
35
52
|
if (/^\d+(?:\.\d+)?$/.test(text)) {
|
|
36
53
|
const seconds = Number(text);
|
|
37
|
-
if (Number.isFinite(seconds) && seconds
|
|
54
|
+
if (Number.isFinite(seconds) && seconds >= 0) {
|
|
38
55
|
return Math.min(Math.max(Math.ceil(seconds * 1000), 1), MAX_COOLDOWN_MS);
|
|
39
56
|
}
|
|
40
57
|
}
|
|
41
58
|
const timestamp = Date.parse(text);
|
|
42
59
|
if (!Number.isFinite(timestamp)) return undefined;
|
|
43
60
|
const delay = timestamp - now;
|
|
44
|
-
|
|
61
|
+
// A valid HTTP-date whose retry time has already passed is an immediate retry, exactly like
|
|
62
|
+
// numeric `Retry-After: 0` — never a malformed-header fallback to the fixed interval.
|
|
63
|
+
return Math.min(Math.max(delay, 1), MAX_COOLDOWN_MS);
|
|
45
64
|
}
|
|
46
65
|
|
|
66
|
+
/**
|
|
67
|
+
* True while the given key is inside its 429 cooldown window (lazily evicting the entry once the
|
|
68
|
+
* window expires). Used to skip keys that the upstream just rate-limited during failover.
|
|
69
|
+
*/
|
|
47
70
|
function isKeyInCooldown(providerName: string, keyId: string, now = Date.now()): boolean {
|
|
48
71
|
const entry = keyCooldowns.get(cooldownKey(providerName, keyId));
|
|
49
72
|
if (!entry) return false;
|
|
@@ -65,6 +88,51 @@ export function hasKeyPoolFailover(provider: OcxProviderConfig): boolean {
|
|
|
65
88
|
return (provider.apiKeyPool?.length ?? 0) >= 2;
|
|
66
89
|
}
|
|
67
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Normalize a provider's `retryOn429` policy, or return null when the knob is absent,
|
|
93
|
+
* explicitly disabled, or the provider is not key-auth (OAuth/forward credentials must not be
|
|
94
|
+
* replayed on the same token, forward passthrough never reaches the recovery loop anyway, and
|
|
95
|
+
* local runtimes have no remote key to preserve). The returned policy is fully defaulted so
|
|
96
|
+
* callers never re-check fields.
|
|
97
|
+
*/
|
|
98
|
+
export function rateLimitRetryPolicyFor(
|
|
99
|
+
provider: Pick<OcxProviderConfig, "retryOn429" | "authMode">,
|
|
100
|
+
): Required<RateLimitRetryPolicy> | null {
|
|
101
|
+
const policy = provider.retryOn429;
|
|
102
|
+
if (!policy || policy.enabled === false) return null;
|
|
103
|
+
// Fail closed: only explicit key auth or the documented omitted-default (undefined == key for
|
|
104
|
+
// custom API-key providers) may use same-key replays. OAuth/forward are never replayed on the
|
|
105
|
+
// same token, local runtimes have no remote key to preserve, and unknown/custom values are
|
|
106
|
+
// rejected rather than guessed at.
|
|
107
|
+
if (provider.authMode !== undefined && provider.authMode !== "key") return null;
|
|
108
|
+
return {
|
|
109
|
+
enabled: policy.enabled ?? DEFAULT_RATE_LIMIT_RETRY.enabled,
|
|
110
|
+
attempts: policy.attempts ?? DEFAULT_RATE_LIMIT_RETRY.attempts,
|
|
111
|
+
intervalMs: policy.intervalMs ?? DEFAULT_RATE_LIMIT_RETRY.intervalMs,
|
|
112
|
+
maxIntervalMs: policy.maxIntervalMs ?? DEFAULT_RATE_LIMIT_RETRY.maxIntervalMs,
|
|
113
|
+
respectRetryAfter: policy.respectRetryAfter ?? DEFAULT_RATE_LIMIT_RETRY.respectRetryAfter,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Wait before the next same-target replay: upstream Retry-After (seconds or HTTP-date) when
|
|
119
|
+
* `respectRetryAfter` is on and the header parses, capped at `maxIntervalMs`; otherwise the
|
|
120
|
+
* fixed `intervalMs`, also capped at `maxIntervalMs` (a single wait never exceeds the cap).
|
|
121
|
+
* Malformed headers fall back to the fixed interval.
|
|
122
|
+
*/
|
|
123
|
+
export function rateLimitRetryDelayMs(
|
|
124
|
+
policy: Required<RateLimitRetryPolicy>,
|
|
125
|
+
retryAfterHeader: string | null | undefined,
|
|
126
|
+
now = Date.now(),
|
|
127
|
+
): number {
|
|
128
|
+
const raw = retryAfterHeader?.trim();
|
|
129
|
+
if (policy.respectRetryAfter && raw) {
|
|
130
|
+
const parsed = parseRetryAfterMs(raw, now);
|
|
131
|
+
if (parsed !== undefined) return Math.min(parsed, policy.maxIntervalMs);
|
|
132
|
+
}
|
|
133
|
+
return Math.min(policy.intervalMs, policy.maxIntervalMs);
|
|
134
|
+
}
|
|
135
|
+
|
|
68
136
|
/**
|
|
69
137
|
* Record a 429 for the current key and attempt to switch to the next available one.
|
|
70
138
|
*
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { resolveEnvValue } from "../config";
|
|
2
2
|
import {
|
|
3
|
+
CodexPoolAuthenticationError,
|
|
3
4
|
headersForCodexAuthContext,
|
|
4
5
|
hasCallerCodexBearer,
|
|
5
6
|
isCodexAuthContextUsable,
|
|
6
7
|
resolveCodexAuthContext,
|
|
8
|
+
type CodexAccountSelectionAdmission,
|
|
7
9
|
type CodexAuthContext,
|
|
8
10
|
} from "../codex/auth-context";
|
|
9
11
|
import { recordCodexUpstreamOutcome, type CodexUpstreamOutcome } from "../codex/routing";
|
|
@@ -29,6 +31,17 @@ export interface ResolvedOpenAiForwardSidecar extends OpenAiForwardSidecarCandid
|
|
|
29
31
|
recordOutcome?: (outcome: CodexUpstreamOutcome) => void;
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Server-resolved exact account selection for a ChatGPT sidecar call.
|
|
36
|
+
* `accountId` must come from a validated account-qualified route; request headers
|
|
37
|
+
* are never trusted as account ids. `modelId` is the sidecar's actual upstream
|
|
38
|
+
* model so cooldown admission and outcome recording use the correct quota scope.
|
|
39
|
+
*/
|
|
40
|
+
export interface ExactOpenAiSidecarAccount {
|
|
41
|
+
accountId: string;
|
|
42
|
+
modelId: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
32
45
|
export interface OpenAiImagesProviderSelection {
|
|
33
46
|
forwardCandidates: OpenAiForwardSidecarCandidate[];
|
|
34
47
|
keyed?: {
|
|
@@ -41,11 +54,18 @@ export interface OpenAiImagesProviderSelection {
|
|
|
41
54
|
|
|
42
55
|
export function listOpenAiForwardSidecarCandidates(config: OcxConfig): OpenAiForwardSidecarCandidate[] {
|
|
43
56
|
const provider = config.providers[OPENAI_CODEX_PROVIDER_ID];
|
|
44
|
-
if (!provider || provider.disabled === true
|
|
57
|
+
if (!provider || provider.disabled === true) return [];
|
|
58
|
+
// The built-in registry defaults an omitted authMode to forward. Normalize only that
|
|
59
|
+
// missing field before the strict adapter/destination check; explicit key mode and
|
|
60
|
+
// noncanonical destinations remain ineligible for ChatGPT credential injection.
|
|
61
|
+
const canonicalProvider = provider.authMode === undefined
|
|
62
|
+
? { ...provider, authMode: "forward" as const }
|
|
63
|
+
: provider;
|
|
64
|
+
if (!isCanonicalOpenAiForwardProvider(canonicalProvider)) return [];
|
|
45
65
|
return [{
|
|
46
66
|
providerName: OPENAI_CODEX_PROVIDER_ID,
|
|
47
|
-
provider,
|
|
48
|
-
accountMode: providerCodexAccountMode(OPENAI_CODEX_PROVIDER_ID,
|
|
67
|
+
provider: canonicalProvider,
|
|
68
|
+
accountMode: providerCodexAccountMode(OPENAI_CODEX_PROVIDER_ID, canonicalProvider) ?? "pool",
|
|
49
69
|
}];
|
|
50
70
|
}
|
|
51
71
|
|
|
@@ -70,7 +90,12 @@ export async function resolveFirstUsableOpenAiSidecar(
|
|
|
70
90
|
candidates: readonly OpenAiForwardSidecarCandidate[],
|
|
71
91
|
incomingHeaders: Headers,
|
|
72
92
|
config: OcxConfig,
|
|
93
|
+
options: {
|
|
94
|
+
exactAccount?: ExactOpenAiSidecarAccount;
|
|
95
|
+
beginCodexAccountSelection?: () => CodexAccountSelectionAdmission | undefined;
|
|
96
|
+
} = {},
|
|
73
97
|
): Promise<ResolvedOpenAiForwardSidecar | undefined> {
|
|
98
|
+
const { exactAccount } = options;
|
|
74
99
|
let callerBearerMayBeForwarded = true;
|
|
75
100
|
try {
|
|
76
101
|
validateForwardAdmissionCredential(incomingHeaders, config);
|
|
@@ -79,6 +104,39 @@ export async function resolveFirstUsableOpenAiSidecar(
|
|
|
79
104
|
callerBearerMayBeForwarded = false;
|
|
80
105
|
}
|
|
81
106
|
for (const candidate of candidates) {
|
|
107
|
+
if (exactAccount) {
|
|
108
|
+
// An account-qualified model is an explicit user choice. Resolve the stored
|
|
109
|
+
// credential directly even when the provider is globally Direct, and never
|
|
110
|
+
// consult Pool active state, affinity, probes, or alternates.
|
|
111
|
+
const authContext = await resolveCodexAuthContext(incomingHeaders, config, "pool", {
|
|
112
|
+
accountId: exactAccount.accountId,
|
|
113
|
+
modelId: exactAccount.modelId,
|
|
114
|
+
beginCodexAccountSelection: options.beginCodexAccountSelection,
|
|
115
|
+
});
|
|
116
|
+
if ((authContext.kind !== "pool" && authContext.kind !== "main-pool")
|
|
117
|
+
|| !isCodexAuthContextUsable(authContext, config)) {
|
|
118
|
+
// Exact selection is fail-closed. A generation/runtime-state race must not fall through
|
|
119
|
+
// to the caller-bearer error or let a later candidate select another account.
|
|
120
|
+
throw new CodexPoolAuthenticationError("Selected Codex account is unavailable");
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
...candidate,
|
|
124
|
+
authContext,
|
|
125
|
+
headers: headersForCodexAuthContext(incomingHeaders, authContext),
|
|
126
|
+
recordOutcome: (outcome: CodexUpstreamOutcome) => recordCodexUpstreamOutcome(
|
|
127
|
+
config,
|
|
128
|
+
authContext.accountId,
|
|
129
|
+
outcome,
|
|
130
|
+
{
|
|
131
|
+
modelId: exactAccount.modelId,
|
|
132
|
+
fixedAccount: true,
|
|
133
|
+
probeLeaseId: authContext.probeLeaseId,
|
|
134
|
+
probeQuotaScope: authContext.probeQuotaScope,
|
|
135
|
+
writerGeneration: authContext.writerGeneration,
|
|
136
|
+
},
|
|
137
|
+
),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
82
140
|
if (candidate.accountMode === "direct") {
|
|
83
141
|
if (!callerBearerMayBeForwarded || !hasCallerCodexBearer(incomingHeaders)) continue;
|
|
84
142
|
const headers = directSidecarHeaders(incomingHeaders);
|
|
@@ -89,7 +147,9 @@ export async function resolveFirstUsableOpenAiSidecar(
|
|
|
89
147
|
headers,
|
|
90
148
|
};
|
|
91
149
|
}
|
|
92
|
-
const authContext = await resolveCodexAuthContext(incomingHeaders, config, candidate.accountMode
|
|
150
|
+
const authContext = await resolveCodexAuthContext(incomingHeaders, config, candidate.accountMode, {
|
|
151
|
+
beginCodexAccountSelection: options.beginCodexAccountSelection,
|
|
152
|
+
});
|
|
93
153
|
if (!isCodexAuthContextUsable(authContext, config)) continue;
|
|
94
154
|
return {
|
|
95
155
|
...candidate,
|
|
@@ -62,6 +62,7 @@ export function applyOpenAiVirtualModel(
|
|
|
62
62
|
logCtx.resolvedModel = resolution.wireModelId;
|
|
63
63
|
route.modelId = resolution.wireModelId;
|
|
64
64
|
parsed.modelId = resolution.wireModelId;
|
|
65
|
+
parsed._openAiVirtualSelectedModelId = resolution.selectedModelId;
|
|
65
66
|
|
|
66
67
|
if (parsed._rawBody && typeof parsed._rawBody === "object" && !Array.isArray(parsed._rawBody)) {
|
|
67
68
|
const raw = parsed._rawBody as Record<string, unknown>;
|