@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
|
@@ -6,11 +6,13 @@ import {
|
|
|
6
6
|
getValidCodexToken,
|
|
7
7
|
isCodexAccountGenerationLive,
|
|
8
8
|
} from "./account-store";
|
|
9
|
+
import { isAccountNeedsReauth, markAccountNeedsReauth } from "./account-runtime-state";
|
|
10
|
+
import { isCodexAccountPaused } from "./account-pause";
|
|
9
11
|
import { ConfigMutationLockError } from "../config";
|
|
10
|
-
import { markAccountNeedsReauth } from "./account-runtime-state";
|
|
11
12
|
import { isCodexAccountUsable } from "./account-usability";
|
|
12
13
|
import { reconcileMainCodexAccountRuntimeState } from "./account-lifecycle";
|
|
13
14
|
import { MAIN_CODEX_ACCOUNT_ID, getMainAccountToken } from "./main-account";
|
|
15
|
+
import { isNativeMainTrafficBlocked } from "./native-profile-startup";
|
|
14
16
|
import {
|
|
15
17
|
codexQuotaScopeForModel,
|
|
16
18
|
getCodexQuotaHealthSnapshot,
|
|
@@ -38,6 +40,8 @@ export type CodexAuthContext =
|
|
|
38
40
|
generation: number;
|
|
39
41
|
accessToken: string;
|
|
40
42
|
chatgptAccountId: string;
|
|
43
|
+
/** Bypass Pool selection and suppress quota/transient failover for an exact selector. */
|
|
44
|
+
fixedAccount?: boolean;
|
|
41
45
|
/**
|
|
42
46
|
* Set when this request was admitted through an active quota cooldown as
|
|
43
47
|
* the account's single probe. Must be echoed into the upstream outcome so
|
|
@@ -57,6 +61,8 @@ export type CodexAuthContext =
|
|
|
57
61
|
writerGeneration: number;
|
|
58
62
|
accessToken: string;
|
|
59
63
|
chatgptAccountId: string;
|
|
64
|
+
/** Bypass Pool selection and suppress quota/transient failover for an exact selector. */
|
|
65
|
+
fixedAccount?: boolean;
|
|
60
66
|
/** See `pool.probeLeaseId`. */
|
|
61
67
|
probeLeaseId?: string;
|
|
62
68
|
quotaScope?: CodexQuotaScope;
|
|
@@ -100,12 +106,26 @@ export class CodexAuthContextError extends Error {
|
|
|
100
106
|
}
|
|
101
107
|
|
|
102
108
|
export class CodexPoolAuthenticationError extends Error {
|
|
103
|
-
constructor() {
|
|
104
|
-
super(
|
|
109
|
+
constructor(message = "OpenAI account pool has no usable account credential") {
|
|
110
|
+
super(message);
|
|
105
111
|
this.name = "CodexPoolAuthenticationError";
|
|
106
112
|
}
|
|
107
113
|
}
|
|
108
114
|
|
|
115
|
+
export class CodexMainProfileDrainingError extends Error {
|
|
116
|
+
constructor() {
|
|
117
|
+
super("Native Codex main profile is switching; retry this request");
|
|
118
|
+
this.name = "CodexMainProfileDrainingError";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function codexMainProfileDrainingResponse(): Response {
|
|
123
|
+
const response = formatErrorResponse(503, "server_busy", "Native Codex main profile is switching; retry this request");
|
|
124
|
+
const headers = new Headers(response.headers);
|
|
125
|
+
headers.set("Retry-After", "1");
|
|
126
|
+
return new Response(response.body, { status: response.status, headers });
|
|
127
|
+
}
|
|
128
|
+
|
|
109
129
|
export class CodexDirectAuthenticationError extends Error {
|
|
110
130
|
constructor() {
|
|
111
131
|
super("Codex Direct requires a caller Authorization bearer token");
|
|
@@ -154,22 +174,31 @@ export function cooldownAccountLabel(accountId: string): string {
|
|
|
154
174
|
* as HTTP. The bare "cooling down" string left users with no route but commenting out the
|
|
155
175
|
* injected `openai_base_url` in config.toml.
|
|
156
176
|
*/
|
|
157
|
-
export function cooldownErrorMessage(err: CodexAccountCooldownError): string {
|
|
177
|
+
export function cooldownErrorMessage(err: CodexAccountCooldownError, accountSelector?: string): string {
|
|
158
178
|
const until = new Date(err.cooldownUntil).toISOString();
|
|
159
179
|
const scope = err.quotaScope === "spark"
|
|
160
180
|
? "Spark quota"
|
|
161
181
|
: err.quotaScope === "shared"
|
|
162
182
|
? "shared native quota"
|
|
163
183
|
: null;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
184
|
+
const selected = accountSelector
|
|
185
|
+
? `Selected Codex account selector (${accountSelector})`
|
|
186
|
+
: `Selected Codex account (${cooldownAccountLabel(err.accountId)})`;
|
|
187
|
+
const recovery = accountSelector
|
|
188
|
+
? " This request is pinned to that selector and will not switch accounts; choose another account-qualified model or retry later."
|
|
189
|
+
: " Run 'ocx account list openai' to find the id, then"
|
|
190
|
+
+ " 'ocx account clear-cooldown openai <id>' to lift it, or switch accounts with 'ocx account use openai <id>'.";
|
|
191
|
+
return `${selected}${scope ? ` ${scope} is` : " is"} cooling down until ${until}`
|
|
192
|
+
+ ` (source: ${err.cooldownSource ?? "default"}).${recovery}`;
|
|
168
193
|
}
|
|
169
194
|
|
|
170
195
|
/** HTTP form of {@link cooldownErrorMessage}, carrying Retry-After for well-behaved clients. */
|
|
171
|
-
export function cooldownErrorResponse(
|
|
172
|
-
|
|
196
|
+
export function cooldownErrorResponse(
|
|
197
|
+
err: CodexAccountCooldownError,
|
|
198
|
+
now = Date.now(),
|
|
199
|
+
accountSelector?: string,
|
|
200
|
+
): Response {
|
|
201
|
+
const res = formatErrorResponse(429, "rate_limit_error", cooldownErrorMessage(err, accountSelector));
|
|
173
202
|
const headers = new Headers(res.headers);
|
|
174
203
|
headers.set("Retry-After", String(Math.max(1, Math.ceil((err.cooldownUntil - now) / 1000))));
|
|
175
204
|
return new Response(res.body, { status: res.status, headers });
|
|
@@ -195,8 +224,22 @@ export function shouldMarkAccountNeedsReauthForCodexAuthFailure(cause: unknown):
|
|
|
195
224
|
|
|
196
225
|
export interface ResolveCodexAuthContextOptions {
|
|
197
226
|
excludeAccountId?: string;
|
|
227
|
+
/** Resolve exactly this account without consulting or mutating Pool selection. */
|
|
228
|
+
accountId?: string;
|
|
198
229
|
/** Final native model selected for this request, used to select its quota group. */
|
|
199
230
|
modelId?: string;
|
|
231
|
+
/** Short reservation converted to turn ownership before native `__main__` token materialization. */
|
|
232
|
+
beginCodexAccountSelection?: () => CodexAccountSelectionAdmission | undefined;
|
|
233
|
+
/** Test-only native credential read seams. */
|
|
234
|
+
isMainAccountTokenLive?: () => boolean;
|
|
235
|
+
getMainAccountToken?: typeof getMainAccountToken;
|
|
236
|
+
primeCodexPoolQuotas?: (config: OcxConfig, reason: string) => Promise<void>;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export interface CodexAccountSelectionAdmission {
|
|
240
|
+
readonly mainProfileDraining: boolean;
|
|
241
|
+
claimMainProfile(): boolean;
|
|
242
|
+
release(): void;
|
|
200
243
|
}
|
|
201
244
|
|
|
202
245
|
export async function resolveCodexAuthContext(
|
|
@@ -206,33 +249,106 @@ export async function resolveCodexAuthContext(
|
|
|
206
249
|
options: ResolveCodexAuthContextOptions = {},
|
|
207
250
|
): Promise<CodexAuthContext> {
|
|
208
251
|
const writerGeneration = captureConfigGeneration();
|
|
209
|
-
|
|
252
|
+
const fixedAccountId = options.accountId;
|
|
253
|
+
if (fixedAccountId !== undefined && options.excludeAccountId !== undefined) {
|
|
254
|
+
throw new Error("Codex auth context cannot select and exclude an account simultaneously");
|
|
255
|
+
}
|
|
256
|
+
// An explicit namespace binding is stronger than the provider's default mode. It must use the
|
|
257
|
+
// selected stored credential even while the canonical OpenAI provider is globally Direct.
|
|
258
|
+
if (mode === "direct" && fixedAccountId === undefined) {
|
|
210
259
|
if (!hasCallerCodexBearer(headers)) throw new CodexDirectAuthenticationError();
|
|
211
260
|
return { kind: "main", accountId: null };
|
|
212
261
|
}
|
|
213
|
-
|
|
214
|
-
|
|
262
|
+
// Retained startup recovery makes the physical main identity ineligible. Routing
|
|
263
|
+
// can still preserve service by selecting a healthy configured pool account.
|
|
264
|
+
const nativeMainTrafficBlocked = isNativeMainTrafficBlocked();
|
|
265
|
+
const selectionAdmission = options.beginCodexAccountSelection?.();
|
|
266
|
+
const nativeMainReadsForbidden = nativeMainTrafficBlocked || selectionAdmission?.mainProfileDraining === true;
|
|
267
|
+
const selectionOptions = {
|
|
268
|
+
// Temporary switch drain keeps the candidate until the atomic claim rejects
|
|
269
|
+
// it. Retained recovery makes main wholly ineligible so pool routing continues.
|
|
270
|
+
nativeMainSelectionOnly: !nativeMainTrafficBlocked
|
|
271
|
+
&& selectionAdmission?.mainProfileDraining === true,
|
|
272
|
+
isMainAccountTokenLive: options.isMainAccountTokenLive,
|
|
273
|
+
};
|
|
274
|
+
let accountId: string;
|
|
215
275
|
const quotaScope = codexQuotaScopeForModel(options.modelId);
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
276
|
+
try {
|
|
277
|
+
// A pre-drain selector reserves the native identity while reconciliation and
|
|
278
|
+
// routing inspect it. Selectors arriving after the fence skip reconciliation
|
|
279
|
+
// and may still route to non-main pool accounts without touching switch state.
|
|
280
|
+
if (!nativeMainReadsForbidden) reconcileMainCodexAccountRuntimeState();
|
|
281
|
+
const threadId = headers.get("x-codex-parent-thread-id");
|
|
282
|
+
const resolution = fixedAccountId !== undefined
|
|
283
|
+
? { status: "selected" as const, accountId: fixedAccountId }
|
|
284
|
+
: options.excludeAccountId
|
|
285
|
+
? (() => {
|
|
286
|
+
const selected = pickAlternateCodexAccount(
|
|
287
|
+
config,
|
|
288
|
+
options.excludeAccountId!,
|
|
289
|
+
Date.now(),
|
|
290
|
+
quotaScope,
|
|
291
|
+
selectionOptions,
|
|
292
|
+
);
|
|
293
|
+
return selected
|
|
294
|
+
? { status: "selected" as const, accountId: selected }
|
|
295
|
+
: { status: "none" as const };
|
|
296
|
+
})()
|
|
297
|
+
: resolveCodexAccountForThreadDetailed(threadId, config, Date.now(), quotaScope, selectionOptions);
|
|
298
|
+
if (resolution.status === "expired") throw new CodexThreadAffinityExpiredError(resolution.accountId);
|
|
299
|
+
const selected = resolution.status === "selected" ? resolution.accountId : null;
|
|
300
|
+
if (!selected) {
|
|
301
|
+
if (fixedAccountId !== undefined) {
|
|
302
|
+
throw new CodexPoolAuthenticationError("Selected Codex account is unavailable");
|
|
303
|
+
}
|
|
304
|
+
// Recovery deliberately makes physical main ineligible. If no healthy
|
|
305
|
+
// pool route is configured and main is the intended route, report the
|
|
306
|
+
// temporary fence rather than misclassifying that credential as invalid.
|
|
307
|
+
// A configured pool retry/exclusion that finds no alternate preserves its
|
|
308
|
+
// ordinary pool-auth failure instead of being mislabeled as a main fence.
|
|
309
|
+
if (nativeMainTrafficBlocked && !options.excludeAccountId) {
|
|
310
|
+
throw new CodexMainProfileDrainingError();
|
|
311
|
+
}
|
|
312
|
+
throw new CodexPoolAuthenticationError();
|
|
313
|
+
}
|
|
314
|
+
accountId = selected;
|
|
315
|
+
if (accountId === MAIN_CODEX_ACCOUNT_ID && nativeMainTrafficBlocked) {
|
|
316
|
+
throw new CodexMainProfileDrainingError();
|
|
317
|
+
}
|
|
318
|
+
if (
|
|
319
|
+
accountId === MAIN_CODEX_ACCOUNT_ID
|
|
320
|
+
&& selectionAdmission
|
|
321
|
+
&& !selectionAdmission.claimMainProfile()
|
|
322
|
+
) {
|
|
323
|
+
throw new CodexMainProfileDrainingError();
|
|
324
|
+
}
|
|
325
|
+
if (fixedAccountId !== undefined) {
|
|
326
|
+
if (isCodexAccountPaused(config, accountId)) {
|
|
327
|
+
throw new CodexPoolAuthenticationError("Selected Codex account is unavailable");
|
|
328
|
+
}
|
|
329
|
+
if (isAccountNeedsReauth(accountId)) {
|
|
330
|
+
throw new CodexPoolAuthenticationError("Selected Codex account needs reauthentication");
|
|
331
|
+
}
|
|
332
|
+
if (!isCodexAccountUsable(config, accountId, selectionOptions)) {
|
|
333
|
+
throw new CodexPoolAuthenticationError("Selected Codex account is unavailable");
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
} finally {
|
|
337
|
+
selectionAdmission?.release();
|
|
338
|
+
}
|
|
227
339
|
// Lazy prime: if the selected account has no quota yet, the pool is likely
|
|
228
340
|
// unprimed (dashboard never opened, or startup prime was blocked). Kick a
|
|
229
341
|
// best-effort prime so the NEXT routing decision has real scores. This never
|
|
230
342
|
// blocks the current request, and the helper's single-flight guard collapses
|
|
231
343
|
// repeated triggers into one pass.
|
|
232
|
-
if (!getAccountQuota(accountId)) {
|
|
233
|
-
|
|
234
|
-
.
|
|
235
|
-
|
|
344
|
+
if (fixedAccountId === undefined && !nativeMainReadsForbidden && !getAccountQuota(accountId)) {
|
|
345
|
+
if (options.primeCodexPoolQuotas) {
|
|
346
|
+
void options.primeCodexPoolQuotas(config, "pre-route").catch(() => {});
|
|
347
|
+
} else {
|
|
348
|
+
import("./auth-api")
|
|
349
|
+
.then(({ primeCodexPoolQuotas }) => primeCodexPoolQuotas(config, "pre-route"))
|
|
350
|
+
.catch(() => {});
|
|
351
|
+
}
|
|
236
352
|
}
|
|
237
353
|
// Snapshot (not just the deadline) so a refused request can report WHY it is cooled:
|
|
238
354
|
// a literal Retry-After reads very differently to a user than a reset-derived guess.
|
|
@@ -244,6 +360,11 @@ export async function resolveCodexAuthContext(
|
|
|
244
360
|
let probeLeaseId: string | undefined;
|
|
245
361
|
let probeQuotaScope: CodexQuotaScope | undefined;
|
|
246
362
|
if (cooldownUntil) {
|
|
363
|
+
// Exact bindings are not Pool recovery traffic. Fail closed instead of consuming the Pool's
|
|
364
|
+
// one probe lease or selecting another account.
|
|
365
|
+
if (fixedAccountId !== undefined) {
|
|
366
|
+
throw new CodexAccountCooldownError(accountId, cooldownUntil, cooldown?.cooldownSource, cooldown?.quotaScope);
|
|
367
|
+
}
|
|
247
368
|
probeQuotaScope = cooldown?.quotaScope;
|
|
248
369
|
probeLeaseId = probeQuotaScope
|
|
249
370
|
? tryAcquireCodexQuotaScopeProbeLease(accountId, probeQuotaScope) ?? undefined
|
|
@@ -255,12 +376,14 @@ export async function resolveCodexAuthContext(
|
|
|
255
376
|
|
|
256
377
|
if (accountId === MAIN_CODEX_ACCOUNT_ID) {
|
|
257
378
|
// Main account in rotation: inject the read-only auth.json token and fail closed if it vanished.
|
|
258
|
-
const token = getMainAccountToken();
|
|
379
|
+
const token = (options.getMainAccountToken ?? getMainAccountToken)();
|
|
259
380
|
if (!token) {
|
|
260
381
|
// Nothing will reach upstream, so give the probe back instead of burning it.
|
|
261
382
|
if (probeLeaseId && probeQuotaScope) releaseCodexQuotaScopeProbeLease(accountId, probeQuotaScope, probeLeaseId);
|
|
262
383
|
else if (probeLeaseId) releaseCodexQuotaProbeLease(accountId, probeLeaseId);
|
|
263
|
-
throw new CodexPoolAuthenticationError(
|
|
384
|
+
throw new CodexPoolAuthenticationError(
|
|
385
|
+
fixedAccountId !== undefined ? "Selected Codex account is unavailable" : undefined,
|
|
386
|
+
);
|
|
264
387
|
}
|
|
265
388
|
return {
|
|
266
389
|
kind: "main-pool",
|
|
@@ -268,6 +391,7 @@ export async function resolveCodexAuthContext(
|
|
|
268
391
|
writerGeneration,
|
|
269
392
|
accessToken: token.accessToken,
|
|
270
393
|
chatgptAccountId: token.chatgptAccountId,
|
|
394
|
+
...(fixedAccountId !== undefined ? { fixedAccount: true } : {}),
|
|
271
395
|
...(quotaScope ? { quotaScope } : {}),
|
|
272
396
|
...(probeLeaseId ? { probeLeaseId } : {}),
|
|
273
397
|
...(probeQuotaScope ? { probeQuotaScope } : {}),
|
|
@@ -283,6 +407,7 @@ export async function resolveCodexAuthContext(
|
|
|
283
407
|
generation: token.generation,
|
|
284
408
|
accessToken: token.accessToken,
|
|
285
409
|
chatgptAccountId: token.chatgptAccountId,
|
|
410
|
+
...(fixedAccountId !== undefined ? { fixedAccount: true } : {}),
|
|
286
411
|
...(quotaScope ? { quotaScope } : {}),
|
|
287
412
|
...(probeLeaseId ? { probeLeaseId } : {}),
|
|
288
413
|
...(probeQuotaScope ? { probeQuotaScope } : {}),
|
|
@@ -47,6 +47,7 @@ export interface StartupHealth {
|
|
|
47
47
|
recommendedCommand: string | null;
|
|
48
48
|
commands: {
|
|
49
49
|
installService: string;
|
|
50
|
+
repairService: string;
|
|
50
51
|
installShim: string;
|
|
51
52
|
restoreNative: string;
|
|
52
53
|
};
|
|
@@ -54,6 +55,7 @@ export interface StartupHealth {
|
|
|
54
55
|
|
|
55
56
|
const COMMANDS = {
|
|
56
57
|
installService: "ocx service install",
|
|
58
|
+
repairService: "ocx service repair",
|
|
57
59
|
installShim: "ocx codex-shim install",
|
|
58
60
|
restoreNative: "ocx restore",
|
|
59
61
|
} as const;
|
|
@@ -88,7 +90,12 @@ export function deriveStartupHealth(inputs: StartupHealthInputs): StartupHealth
|
|
|
88
90
|
: inputs.routingKind === "custom-local" || inputs.routingKind === "unknown"
|
|
89
91
|
? COMMANDS.restoreNative
|
|
90
92
|
: inputs.serviceSupported
|
|
91
|
-
|
|
93
|
+
// An already-registered service is refreshed in place: `repair` rewrites its assets
|
|
94
|
+
// and restarts it without re-registering, so it needs no elevation on Windows and
|
|
95
|
+
// cannot switch a WinSW install to Task Scheduler the way `install` would. Only a
|
|
96
|
+
// genuinely absent (or conflicting, which needs uninstall-then-install) service
|
|
97
|
+
// gets the registering command.
|
|
98
|
+
? (inputs.serviceInstalled && !inputs.serviceConflict ? COMMANDS.repairService : COMMANDS.installService)
|
|
92
99
|
: COMMANDS.restoreNative;
|
|
93
100
|
return {
|
|
94
101
|
...inputs,
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { OcxConfig } from "../../types";
|
|
2
|
+
import { isMainCodexAccountTarget } from "../account-namespaces";
|
|
3
|
+
import type { RawEntry } from "./parsing";
|
|
4
|
+
|
|
5
|
+
/** Stable nonsemantic marker used to distinguish generated rows from provider-owned rows. */
|
|
6
|
+
export const CODEX_ACCOUNT_BOUND_CATALOG_KIND = "account-selector-v1";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Public selectors whose configured account row still exists.
|
|
10
|
+
*
|
|
11
|
+
* Stale mappings stay in config so exact routing keeps failing closed, but they must not advertise
|
|
12
|
+
* a deleted account. Credential health, pause, and reauthentication state intentionally do not
|
|
13
|
+
* churn catalog identity. Selector keys have already passed config's nonempty, single-segment
|
|
14
|
+
* namespace validation. Only those public keys leave this boundary; private account ids do not.
|
|
15
|
+
*/
|
|
16
|
+
export function visibleCodexAccountSelectors(
|
|
17
|
+
config: Pick<OcxConfig, "codexAccounts" | "codexAccountNamespaces">,
|
|
18
|
+
): string[] {
|
|
19
|
+
const storedPoolAccounts = new Set(
|
|
20
|
+
(config.codexAccounts ?? [])
|
|
21
|
+
.filter(account => !account.isMain)
|
|
22
|
+
.map(account => account.id),
|
|
23
|
+
);
|
|
24
|
+
return Object.entries(config.codexAccountNamespaces ?? {})
|
|
25
|
+
.filter(([, accountId]) =>
|
|
26
|
+
isMainCodexAccountTarget(accountId) || storedPoolAccounts.has(accountId)
|
|
27
|
+
)
|
|
28
|
+
.map(([selector]) => selector);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function accountBoundNativeDisplayName(selector: string, native: RawEntry): string {
|
|
32
|
+
const rawModel = typeof native.display_name === "string"
|
|
33
|
+
? native.display_name
|
|
34
|
+
: String(native.slug ?? "");
|
|
35
|
+
const model = rawModel
|
|
36
|
+
.replace(/^gpt-/i, "")
|
|
37
|
+
.split("-")
|
|
38
|
+
.filter(Boolean)
|
|
39
|
+
.map(part => /^[a-z]/.test(part) ? part.charAt(0).toUpperCase() + part.slice(1) : part)
|
|
40
|
+
.join(" ");
|
|
41
|
+
return `${selector} / ${model}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Identify current generated rows without changing Codex's semantic model fields. */
|
|
45
|
+
export function trustedAccountBoundNativeCatalogSlug(entry: RawEntry): string | undefined {
|
|
46
|
+
if (entry.opencodex_catalog_kind !== CODEX_ACCOUNT_BOUND_CATALOG_KIND
|
|
47
|
+
|| typeof entry.slug !== "string") return undefined;
|
|
48
|
+
const slash = entry.slug.indexOf("/");
|
|
49
|
+
if (slash <= 0 || slash !== entry.slug.lastIndexOf("/") || slash === entry.slug.length - 1) {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
return entry.slug.slice(slash + 1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function accountBoundNativeModelSlugs(
|
|
56
|
+
config: Pick<OcxConfig, "codexAccounts" | "codexAccountNamespaces">,
|
|
57
|
+
nativeSlugs: Iterable<string>,
|
|
58
|
+
): string[] {
|
|
59
|
+
const natives = [...nativeSlugs];
|
|
60
|
+
return visibleCodexAccountSelectors(config)
|
|
61
|
+
.flatMap(selector => natives.map(slug => `${selector}/${slug}`));
|
|
62
|
+
}
|
|
@@ -281,6 +281,8 @@ export function resetOpenAiApiCatalogWarningStateForTests(): void {
|
|
|
281
281
|
export const slugAliasCollisionWarnings = new Set<string>();
|
|
282
282
|
|
|
283
283
|
export const comboMasqueradeCollisionWarnings = new Set<string>();
|
|
284
|
+
|
|
285
|
+
export const accountSelectorShadowCollisionWarnings = new Set<string>();
|
|
284
286
|
let lastWarningReconciledGeneration = 0;
|
|
285
287
|
|
|
286
288
|
export function reconcileCatalogWarningMemos(generation: number): number {
|
|
@@ -288,11 +290,13 @@ export function reconcileCatalogWarningMemos(generation: number): number {
|
|
|
288
290
|
const removed = openAiApiCollisionWarnings.size
|
|
289
291
|
+ comboCatalogWarningSignatures.size
|
|
290
292
|
+ slugAliasCollisionWarnings.size
|
|
291
|
-
+ comboMasqueradeCollisionWarnings.size
|
|
293
|
+
+ comboMasqueradeCollisionWarnings.size
|
|
294
|
+
+ accountSelectorShadowCollisionWarnings.size;
|
|
292
295
|
openAiApiCollisionWarnings.clear();
|
|
293
296
|
comboCatalogWarningSignatures.clear();
|
|
294
297
|
slugAliasCollisionWarnings.clear();
|
|
295
298
|
comboMasqueradeCollisionWarnings.clear();
|
|
299
|
+
accountSelectorShadowCollisionWarnings.clear();
|
|
296
300
|
lastWarningReconciledGeneration = generation;
|
|
297
301
|
return removed;
|
|
298
302
|
}
|
|
@@ -305,6 +309,15 @@ export function warnComboMasqueradeCollisionOnce(slug: string): void {
|
|
|
305
309
|
);
|
|
306
310
|
}
|
|
307
311
|
|
|
312
|
+
/** Warn once when a live provider row loses its reserved slug to an account selector. */
|
|
313
|
+
export function warnAccountSelectorShadowedProviderOnce(slug: string): void {
|
|
314
|
+
if (accountSelectorShadowCollisionWarnings.has(slug)) return;
|
|
315
|
+
accountSelectorShadowCollisionWarnings.add(slug);
|
|
316
|
+
console.warn(
|
|
317
|
+
`[opencodex] account selector collision on "${safeCatalogWarningLabel(slug)}": the account-bound native model wins and the shadowed provider model is omitted from the catalog. Rename the provider or account selector.`,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
308
321
|
export function resolveSlugAliasCollisions(goModels: CatalogModel[]): Set<CatalogModel> {
|
|
309
322
|
const skipped = new Set<CatalogModel>();
|
|
310
323
|
const winnerByAlias = new Map<string, CatalogModel>();
|