@iislee/opencodex 2.31.0 → 2.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/bin/ocx.mjs +99 -70
- package/gui/dist/assets/index-Be1sDpWw.css +1 -0
- package/gui/dist/assets/index-D4OKgUV4.js +114 -0
- package/gui/dist/index.html +2 -2
- package/package.json +3 -1
- package/src/adapters/anthropic.ts +104 -15
- package/src/adapters/base.ts +29 -1
- package/src/adapters/command-code.ts +42 -6
- package/src/adapters/cursor/call-id.ts +44 -0
- package/src/adapters/cursor/catalog.ts +541 -0
- package/src/adapters/cursor/checkpoint-store.ts +15 -10
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +69 -18
- package/src/adapters/cursor/effort-map.ts +79 -1
- package/src/adapters/cursor/envelope-echo.ts +290 -0
- package/src/adapters/cursor/live-models.ts +7 -2
- package/src/adapters/cursor/live-transport.ts +14 -0
- package/src/adapters/cursor/message-mapper.ts +4 -1
- package/src/adapters/cursor/native-exec-desktop.ts +23 -0
- package/src/adapters/cursor/native-exec-fs.ts +2 -2
- package/src/adapters/cursor/native-exec-network.ts +1 -1
- package/src/adapters/cursor/native-exec-shell.ts +5 -2
- package/src/adapters/cursor/native-exec.ts +20 -0
- package/src/adapters/cursor/protobuf-request.ts +68 -14
- package/src/adapters/cursor/request-builder.ts +40 -10
- package/src/adapters/cursor/tool-definitions.ts +14 -4
- package/src/adapters/cursor/tool-result-normalize.ts +28 -5
- package/src/adapters/cursor/types.ts +20 -0
- package/src/adapters/cursor.ts +209 -38
- package/src/adapters/exec-tool-result-normalize.ts +99 -0
- package/src/adapters/google-antigravity-replay.ts +173 -25
- package/src/adapters/google-antigravity-wire.ts +5 -0
- package/src/adapters/google-errors.ts +44 -12
- package/src/adapters/google.ts +136 -24
- package/src/adapters/image.ts +1 -1
- package/src/adapters/kiro-constants.ts +27 -0
- package/src/adapters/kiro-tools.ts +55 -16
- package/src/adapters/kiro.ts +182 -23
- package/src/adapters/openai-chat.ts +300 -242
- package/src/adapters/openai-responses.ts +353 -27
- package/src/adapters/run-turn-queue.ts +36 -1
- package/src/adapters/tool-catalog-nudge.ts +4 -3
- package/src/adapters/xai-tool-schema.ts +436 -0
- package/src/adapters/xai-web-search.ts +17 -16
- package/src/bridge.ts +90 -37
- package/src/chat/inbound.ts +14 -1
- package/src/chat/outbound.ts +15 -7
- package/src/claude/agents-inject.ts +8 -1
- package/src/claude/context-windows.ts +16 -9
- package/src/claude/outbound.ts +24 -11
- package/src/cli/access.ts +46 -3
- package/src/cli/account-api.ts +116 -19
- package/src/cli/account-extended.ts +262 -29
- package/src/cli/account-main.ts +12 -12
- package/src/cli/account.ts +69 -15
- package/src/cli/agent.ts +8 -1
- package/src/cli/alias.ts +66 -0
- package/src/cli/capabilities-command.ts +94 -0
- package/src/cli/capabilities.ts +496 -0
- package/src/cli/claude-desktop.ts +31 -11
- package/src/cli/claude.ts +26 -1
- package/src/cli/dispatch.ts +256 -28
- package/src/cli/doctor.ts +126 -14
- package/src/cli/help.ts +17 -6
- package/src/cli/index.ts +39 -9
- package/src/cli/init.ts +1 -0
- package/src/cli/inspect.ts +230 -0
- package/src/cli/models-runtime.ts +95 -0
- package/src/cli/models.ts +26 -10
- package/src/cli/observe.ts +31 -8
- package/src/cli/provider-runtime.ts +16 -2
- package/src/cli/provider.ts +2 -1
- package/src/cli/registry.ts +49 -10
- package/src/cli/runtime-api.ts +51 -7
- package/src/cli/status.ts +41 -1
- package/src/cli/storage.ts +234 -0
- package/src/cli/system-command.ts +16 -0
- package/src/cli/system-restart-client.ts +1 -1
- package/src/cli/usage-report.ts +184 -0
- package/src/cli/v2.ts +34 -10
- package/src/cli/version-skew.ts +46 -0
- package/src/clients/config-export.ts +85 -3
- package/src/clients/effective-status.ts +30 -2
- package/src/clients/probes/cline.ts +81 -0
- package/src/codex/account-label.ts +21 -0
- package/src/codex/account-pause.ts +2 -1
- package/src/codex/account-priority.ts +3 -2
- package/src/codex/app-server-processes.ts +83 -7
- package/src/codex/auth-api.ts +50 -10
- package/src/codex/auth-context.ts +21 -18
- package/src/codex/autostart-health.ts +16 -0
- package/src/codex/catalog/aggregation.ts +18 -0
- package/src/codex/catalog/effort.ts +18 -3
- package/src/codex/catalog/metadata.ts +27 -1
- package/src/codex/catalog/native-models.ts +5 -2
- package/src/codex/catalog/parsing.ts +72 -27
- package/src/codex/catalog/provider-fetch.ts +187 -31
- package/src/codex/catalog/sync.ts +131 -5
- package/src/codex/catalog.ts +1 -1
- package/src/codex/codex-write-lock.ts +3 -1
- package/src/codex/convergence-types.ts +1 -1
- package/src/codex/convergence.ts +27 -2
- package/src/codex/desired-state.ts +2 -2
- package/src/codex/desktop-app-restart.ts +355 -0
- package/src/codex/history-job.ts +32 -3
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +5 -5
- package/src/codex/history-provider.ts +825 -247
- package/src/codex/history-worker.ts +8 -5
- package/src/codex/inject-coordination.ts +83 -0
- package/src/codex/inject.ts +63 -22
- package/src/codex/injected-marker.ts +1 -1
- package/src/codex/internal/history-writer.ts +4 -3
- package/src/codex/log-guard/inspect.ts +22 -4
- package/src/codex/model-entitlements.ts +9 -2
- package/src/codex/native-profile-startup.ts +157 -27
- package/src/codex/native-residue.ts +26 -33
- package/src/codex/prompt-layers.ts +371 -25
- package/src/codex/prompt-text-probe.ts +238 -0
- package/src/codex/quota.ts +91 -10
- package/src/codex/routing.ts +9 -0
- package/src/codex/shim.ts +56 -3
- package/src/codex/subagent-model-fallback.ts +104 -25
- package/src/codex/transition-state.ts +116 -8
- package/src/combos/failover.ts +27 -0
- package/src/combos/types.ts +10 -0
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +121 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +309 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config/rebase-provenance.ts +68 -0
- package/src/config.ts +206 -824
- package/src/generated/compatibility-version.json +373 -213
- package/src/images/loop.ts +5 -4
- package/src/images/plan.ts +5 -4
- package/src/integrations/ownership-policy.ts +160 -0
- package/src/integrations/ownership.ts +46 -2
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/state.ts +79 -7
- package/src/integrations/writer.ts +26 -2
- package/src/lab/conformance/fixtures/protocol-v1-cases.json +1 -1
- package/src/lab/fabric/producer-child.ts +1 -1
- package/src/lib/admin-secrets.ts +24 -0
- package/src/lib/bounded-body.ts +14 -2
- package/src/lib/config-ownership.ts +20 -0
- package/src/lib/errors.ts +36 -3
- package/src/lib/package-tree-integrity.ts +101 -0
- package/src/lib/process-control.ts +2 -1
- package/src/lib/service-secrets.ts +15 -0
- package/src/lib/tool-argument-integers.ts +56 -5
- package/src/oauth/callback-server.ts +22 -2
- package/src/oauth/cline.ts +203 -0
- package/src/oauth/generic-account-failover.ts +231 -0
- package/src/oauth/health.ts +1 -1
- package/src/oauth/index.ts +65 -6
- package/src/oauth/key-providers.ts +10 -1
- package/src/oauth/kimi.ts +9 -1
- package/src/oauth/kiro.ts +45 -0
- package/src/oauth/local-token-detect.ts +2 -0
- package/src/oauth/open-browser-choice.ts +26 -0
- package/src/oauth/store.ts +14 -5
- package/src/oauth/types.ts +15 -0
- package/src/providers/auto-compact-budget.ts +65 -0
- package/src/providers/command-code-efforts.ts +38 -6
- package/src/providers/context-cap.ts +4 -3
- package/src/providers/default-aliases.ts +65 -0
- package/src/providers/derive.ts +28 -0
- package/src/providers/fastwire.ts +7 -1
- package/src/providers/label.ts +34 -1
- package/src/providers/model-presets.ts +119 -0
- package/src/providers/new-model-policy.ts +146 -0
- package/src/providers/provider-id-rewrite.ts +2 -1
- package/src/providers/quota.ts +65 -7
- package/src/providers/registry.ts +184 -69
- package/src/providers/slug-codec.ts +52 -0
- package/src/providers/xai-transport.ts +21 -0
- package/src/reasoning-effort.ts +19 -2
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/code-mode-helper-compat.ts +50 -0
- package/src/responses/custom-tool-compat.ts +157 -39
- package/src/responses/namespace-tool-compat.ts +84 -4
- package/src/responses/parser.ts +7 -2
- package/src/responses/reasoning-replay-cache.ts +81 -3
- package/src/responses/schema.ts +5 -1
- package/src/responses/state.ts +94 -4
- package/src/responses/thought-signature-replay.ts +17 -0
- package/src/responses/turn-termination.ts +107 -0
- package/src/router.ts +40 -2
- package/src/routing/analytics.ts +1 -0
- package/src/routing/quota.ts +10 -0
- package/src/server/auth-cors.ts +27 -7
- package/src/server/chat-completions.ts +24 -16
- package/src/server/chat-native-sse.ts +3 -3
- package/src/server/chat-native.ts +30 -11
- package/src/server/claude-messages.ts +1 -1
- package/src/server/index.ts +179 -32
- package/src/server/lifecycle.ts +52 -1
- package/src/server/local-management-read-client.ts +1 -1
- package/src/server/local-provider-reload-client.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +45 -23
- package/src/server/management/codex-prompt-routes.ts +570 -0
- package/src/server/management/combo-routes.ts +2 -1
- package/src/server/management/config-routes.ts +59 -12
- package/src/server/management/context.ts +10 -1
- package/src/server/management/logs-usage-routes.ts +30 -24
- package/src/server/management/model-routes.ts +274 -4
- package/src/server/management/model-rows.ts +4 -0
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/oauth-account-routes.ts +10 -4
- package/src/server/management/provider-capability-config.ts +1 -1
- package/src/server/management/provider-routes.ts +81 -11
- package/src/server/management/route-registry.ts +332 -0
- package/src/server/management/routing-profile-routes.ts +2 -2
- package/src/server/management/system-restart.ts +1 -1
- package/src/server/management-api.ts +18 -16
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/proxy-liveness.ts +29 -5
- package/src/server/relay-eager.ts +147 -20
- package/src/server/relay.ts +251 -19
- package/src/server/request-log-conversation.ts +51 -0
- package/src/server/request-log.ts +70 -16
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/collaboration.ts +37 -3
- package/src/server/responses/combo-stream-preflight.ts +178 -0
- package/src/server/responses/compact.ts +6 -21
- package/src/server/responses/core.ts +810 -208
- package/src/server/responses/empty-completion-guard.ts +63 -6
- package/src/server/responses/fetch-helpers.ts +17 -102
- package/src/server/responses/input-admission.ts +3 -1
- package/src/server/responses/passthrough-error.ts +33 -9
- package/src/server/responses/responses-field-backfill.ts +105 -13
- package/src/server/responses/ws-upstream.ts +75 -2
- package/src/server/responses-custom-tool-repair.ts +87 -6
- package/src/server/responses-terminal-repair.ts +25 -4
- package/src/server/responses-undeclared-tool-guard.ts +241 -18
- package/src/server/sse-frame-buffer.ts +31 -4
- package/src/server/ws-bridge.ts +14 -2
- package/src/service.ts +42 -4
- package/src/storage/policy-job.ts +14 -4
- package/src/storage/policy.ts +88 -23
- package/src/types/config.ts +66 -0
- package/src/types/provider.ts +69 -0
- package/src/types/request.ts +17 -4
- package/src/types/tools.ts +116 -11
- package/src/types.ts +3 -1
- package/src/update/index.ts +5 -4
- package/src/update/job.ts +3 -1
- package/src/update/transactional-install.mjs +8 -1
- package/src/usage/expected-prices.ts +43 -24
- package/src/usage/log.ts +63 -12
- package/src/usage/summary.ts +211 -8
- package/src/vision/describe.ts +18 -13
- package/src/web-search/exa-executor.ts +40 -9
- package/src/web-search/executor.ts +10 -3
- package/src/web-search/loop.ts +5 -4
- package/gui/dist/assets/index-BF38heuV.js +0 -104
- package/gui/dist/assets/index-DMiI18Kv.css +0 -1
package/src/cli/account-api.ts
CHANGED
|
@@ -24,6 +24,14 @@ export interface AccountRow {
|
|
|
24
24
|
/** Codex pool selection order, higher used earlier. Absent where ordering does not apply. */
|
|
25
25
|
priority?: number;
|
|
26
26
|
quota?: CodexQuotaDto | null;
|
|
27
|
+
/**
|
|
28
|
+
* Whether the pool is holding this account out of rotation.
|
|
29
|
+
*
|
|
30
|
+
* The server has always sent it (auth-api.ts:286 for pool accounts, :1315 for main) and the
|
|
31
|
+
* CLI dropped it, so a paused account was indistinguishable from an available one in every
|
|
32
|
+
* human listing (#2703).
|
|
33
|
+
*/
|
|
34
|
+
paused?: boolean;
|
|
27
35
|
}
|
|
28
36
|
|
|
29
37
|
export type ClassifyResult = { type: AccountType } | { error: string };
|
|
@@ -83,6 +91,12 @@ export interface ApiResult {
|
|
|
83
91
|
/** 0 = network-level failure (proxy unreachable). */
|
|
84
92
|
status: number;
|
|
85
93
|
json: Record<string, unknown>;
|
|
94
|
+
/**
|
|
95
|
+
* Message from the thrown transport error when `status` is 0. Previously the
|
|
96
|
+
* error was swallowed by a catch block with an empty body, so an unreachable proxy, a DNS
|
|
97
|
+
* failure and a TLS error were indistinguishable (#2698).
|
|
98
|
+
*/
|
|
99
|
+
transportError?: string;
|
|
86
100
|
}
|
|
87
101
|
|
|
88
102
|
export async function apiJson(
|
|
@@ -103,8 +117,14 @@ export async function apiJson(
|
|
|
103
117
|
});
|
|
104
118
|
const json = (await res.json().catch(() => ({}))) as Record<string, unknown>;
|
|
105
119
|
return { status: res.status, json };
|
|
106
|
-
} catch {
|
|
107
|
-
|
|
120
|
+
} catch (error) {
|
|
121
|
+
// status 0 stays the transport sentinel, but keep the cause: callers can now
|
|
122
|
+
// tell the operator why the request never reached the proxy (#2698).
|
|
123
|
+
return {
|
|
124
|
+
status: 0,
|
|
125
|
+
json: {},
|
|
126
|
+
transportError: error instanceof Error ? error.message : String(error),
|
|
127
|
+
};
|
|
108
128
|
}
|
|
109
129
|
}
|
|
110
130
|
|
|
@@ -115,18 +135,43 @@ export async function resolveBaseUrl(deps: AccountDeps): Promise<string | null>
|
|
|
115
135
|
return `http://${probeHostname(live.hostname)}:${live.port}`;
|
|
116
136
|
}
|
|
117
137
|
|
|
118
|
-
export function proxyUnreachable(): number {
|
|
138
|
+
export function proxyUnreachable(transportError?: string): number {
|
|
119
139
|
console.error("Proxy not reachable. Start it with 'ocx start' or 'ocx ensure'.");
|
|
140
|
+
// Naming the transport cause distinguishes "nothing is listening" from a refused
|
|
141
|
+
// or reset connection, which is what made #2696-class breakage undiagnosable.
|
|
142
|
+
if (transportError) console.error(`reason: ${transportError}`);
|
|
120
143
|
return 1;
|
|
121
144
|
}
|
|
122
145
|
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
146
|
+
function accountStringField(json: Record<string, unknown>, key: string): string | undefined {
|
|
147
|
+
const value = json[key];
|
|
148
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Report a failed management call from the account family.
|
|
153
|
+
*
|
|
154
|
+
* `reason` and `hint` are the actionable fields on a refusal — the management plane
|
|
155
|
+
* sets both on a 503, and several routes return `reason` with no `error` key at all,
|
|
156
|
+
* which used to print only the generic fallback (#2698).
|
|
157
|
+
*
|
|
158
|
+
* `status` selects the exit code so the account client speaks the same vocabulary as
|
|
159
|
+
* runtime-api.ts: 4 for not-found, 5 for conflict, 1 otherwise. Previously every
|
|
160
|
+
* failure exited 1, so a script could not distinguish a missing account from a
|
|
161
|
+
* concurrent mutation.
|
|
162
|
+
*/
|
|
163
|
+
export function apiError(json: Record<string, unknown>, fallback: string, status: number): number {
|
|
164
|
+
const primary = accountStringField(json, "error") ?? fallback;
|
|
165
|
+
const lines = [`Error: ${primary}`];
|
|
166
|
+
const reason = accountStringField(json, "reason");
|
|
167
|
+
if (reason && reason !== primary) lines.push(`reason: ${reason}`);
|
|
168
|
+
const hint = accountStringField(json, "hint");
|
|
169
|
+
if (hint && hint !== primary) lines.push(`hint: ${hint}`);
|
|
170
|
+
for (const line of lines) console.error(line);
|
|
126
171
|
if (json.cleanupRequired === true) {
|
|
127
172
|
console.error("Warning: native-login staging cleanup is still required; run 'ocx account main doctor'.");
|
|
128
173
|
}
|
|
129
|
-
return 1;
|
|
174
|
+
return status === 404 ? 4 : status === 409 ? 5 : 1;
|
|
130
175
|
}
|
|
131
176
|
|
|
132
177
|
export interface FamilyRows {
|
|
@@ -134,10 +179,12 @@ export interface FamilyRows {
|
|
|
134
179
|
activeId: string | null;
|
|
135
180
|
autoSwitchThreshold?: number;
|
|
136
181
|
/** HTTP status for a completed family read, including failures. */
|
|
137
|
-
status
|
|
182
|
+
status: number;
|
|
138
183
|
/** Set when the family endpoint returned an error. */
|
|
139
184
|
errorJson?: Record<string, unknown>;
|
|
140
185
|
networkDown?: boolean;
|
|
186
|
+
/** Transport cause when `networkDown` is set. Callers must forward this to `proxyUnreachable`. */
|
|
187
|
+
transportError?: string;
|
|
141
188
|
}
|
|
142
189
|
|
|
143
190
|
export interface CodexQuotaDto {
|
|
@@ -145,6 +192,14 @@ export interface CodexQuotaDto {
|
|
|
145
192
|
monthlyPercent?: number;
|
|
146
193
|
weeklyResetAt?: number;
|
|
147
194
|
monthlyResetAt?: number;
|
|
195
|
+
/**
|
|
196
|
+
* Five-hour window, as the per-account provider probe reports it
|
|
197
|
+
* (`/api/oauth/accounts?quota=1`). Distinct from `shortPercent`, which is the Codex pool's
|
|
198
|
+
* self-declared burst window; the two surfaces name the same idea differently and both reach
|
|
199
|
+
* this DTO.
|
|
200
|
+
*/
|
|
201
|
+
fiveHourPercent?: number;
|
|
202
|
+
fiveHourResetAt?: number;
|
|
148
203
|
/** Sub-day burst window, when upstream declares one (#1791). */
|
|
149
204
|
shortPercent?: number;
|
|
150
205
|
shortResetAt?: number;
|
|
@@ -182,12 +237,19 @@ interface CodexAccountDto {
|
|
|
182
237
|
needsReauth?: boolean;
|
|
183
238
|
priority?: number;
|
|
184
239
|
quota?: CodexQuotaDto | null;
|
|
240
|
+
paused?: boolean;
|
|
185
241
|
}
|
|
186
242
|
|
|
187
243
|
function projectQuota(quota: CodexQuotaDto | null | undefined): CodexQuotaDto | null {
|
|
188
244
|
if (!quota) return null;
|
|
189
245
|
const projected: CodexQuotaDto = {};
|
|
190
|
-
|
|
246
|
+
// `fiveHourPercent`/`fiveHourResetAt` were declared on the DTO and read by two renderers
|
|
247
|
+
// -- `quotaText`'s `quota.fiveHourPercent ?? quota.shortPercent` (account.ts:89) and
|
|
248
|
+
// `quotaParts` (account-extended.ts:275) -- but omitted from this whitelist, so the first
|
|
249
|
+
// operand was unreachable and a 5h-only account rendered as unknown (#2703). A projection
|
|
250
|
+
// that silently drops a field its own type declares is worse than one that never had it:
|
|
251
|
+
// the type checks, the renderer looks correct, and only the output is wrong.
|
|
252
|
+
for (const key of ["fiveHourPercent", "fiveHourResetAt", "weeklyPercent", "monthlyPercent", "weeklyResetAt", "monthlyResetAt", "shortPercent", "shortResetAt", "shortWindowSeconds"] as const) {
|
|
191
253
|
if (typeof quota[key] === "number" && Number.isFinite(quota[key])) projected[key] = quota[key];
|
|
192
254
|
}
|
|
193
255
|
return projected;
|
|
@@ -197,6 +259,7 @@ export async function fetchCodexRows(
|
|
|
197
259
|
deps: AccountDeps,
|
|
198
260
|
baseUrl: string,
|
|
199
261
|
forceRefresh = false,
|
|
262
|
+
includeQuota = forceRefresh,
|
|
200
263
|
): Promise<FamilyRows> {
|
|
201
264
|
const accountsPath = `/api/codex-auth/accounts${forceRefresh ? "?refresh=1" : ""}`;
|
|
202
265
|
const [accountsRes, activeRes] = await Promise.all([
|
|
@@ -210,7 +273,13 @@ export async function fetchCodexRows(
|
|
|
210
273
|
return { rows: [], activeId: null, status: activeRes.status, errorJson: activeRes.json };
|
|
211
274
|
}
|
|
212
275
|
if (accountsRes.status === 0 || activeRes.status === 0) {
|
|
213
|
-
return {
|
|
276
|
+
return {
|
|
277
|
+
rows: [],
|
|
278
|
+
activeId: null,
|
|
279
|
+
status: 0,
|
|
280
|
+
networkDown: true,
|
|
281
|
+
transportError: accountsRes.transportError ?? activeRes.transportError,
|
|
282
|
+
};
|
|
214
283
|
}
|
|
215
284
|
const activeId = typeof activeRes.json.activeCodexAccountId === "string"
|
|
216
285
|
? activeRes.json.activeCodexAccountId
|
|
@@ -229,7 +298,8 @@ export async function fetchCodexRows(
|
|
|
229
298
|
active: a.id === activeId,
|
|
230
299
|
needsReauth: a.needsReauth,
|
|
231
300
|
priority: typeof a.priority === "number" ? a.priority : 0,
|
|
232
|
-
|
|
301
|
+
paused: a.paused === true,
|
|
302
|
+
...(includeQuota ? { quota: projectQuota(a.quota) } : {}),
|
|
233
303
|
}));
|
|
234
304
|
return { rows, activeId, autoSwitchThreshold, status: 200 };
|
|
235
305
|
}
|
|
@@ -240,11 +310,25 @@ interface OAuthAccountDto {
|
|
|
240
310
|
email?: string;
|
|
241
311
|
active?: boolean;
|
|
242
312
|
needsReauth?: boolean;
|
|
313
|
+
quota?: CodexQuotaDto | null;
|
|
314
|
+
quotaUnavailable?: boolean;
|
|
243
315
|
}
|
|
244
316
|
|
|
245
|
-
async function fetchOAuthRows(
|
|
246
|
-
|
|
247
|
-
|
|
317
|
+
async function fetchOAuthRows(
|
|
318
|
+
deps: AccountDeps,
|
|
319
|
+
baseUrl: string,
|
|
320
|
+
name: string,
|
|
321
|
+
quota?: { refresh?: boolean },
|
|
322
|
+
): Promise<FamilyRows> {
|
|
323
|
+
// Quota is opt-in: the server probes the upstream once per stored credential when `quota=1`
|
|
324
|
+
// is present, so the default listing must stay a cheap local read (#2566).
|
|
325
|
+
const query = quota
|
|
326
|
+
? `?provider=${encodeURIComponent(name)}"a=1${quota.refresh ? "&refresh=1" : ""}`
|
|
327
|
+
: `?provider=${encodeURIComponent(name)}`;
|
|
328
|
+
const res = await apiJson(deps, baseUrl, "GET", `/api/oauth/accounts${query}`);
|
|
329
|
+
if (res.status === 0) {
|
|
330
|
+
return { rows: [], activeId: null, status: 0, networkDown: true, transportError: res.transportError };
|
|
331
|
+
}
|
|
248
332
|
if (res.status !== 200) return { rows: [], activeId: null, status: res.status, errorJson: res.json };
|
|
249
333
|
const activeId = typeof res.json.activeAccountId === "string" ? res.json.activeAccountId : null;
|
|
250
334
|
const accounts = Array.isArray(res.json.accounts) ? res.json.accounts as OAuthAccountDto[] : [];
|
|
@@ -256,6 +340,8 @@ async function fetchOAuthRows(deps: AccountDeps, baseUrl: string, name: string):
|
|
|
256
340
|
email: a.email,
|
|
257
341
|
active: a.active ?? a.id === activeId,
|
|
258
342
|
needsReauth: a.needsReauth,
|
|
343
|
+
...(a.quota !== undefined ? { quota: a.quota } : {}),
|
|
344
|
+
...(a.quotaUnavailable !== undefined ? { quotaUnavailable: a.quotaUnavailable } : {}),
|
|
259
345
|
}));
|
|
260
346
|
return { rows, activeId, status: 200 };
|
|
261
347
|
}
|
|
@@ -269,7 +355,9 @@ interface ApiKeyDto {
|
|
|
269
355
|
|
|
270
356
|
async function fetchKeyRows(deps: AccountDeps, baseUrl: string, name: string): Promise<FamilyRows> {
|
|
271
357
|
const res = await apiJson(deps, baseUrl, "GET", `/api/providers/keys?name=${encodeURIComponent(name)}`);
|
|
272
|
-
if (res.status === 0)
|
|
358
|
+
if (res.status === 0) {
|
|
359
|
+
return { rows: [], activeId: null, status: 0, networkDown: true, transportError: res.transportError };
|
|
360
|
+
}
|
|
273
361
|
if (res.status !== 200) return { rows: [], activeId: null, status: res.status, errorJson: res.json };
|
|
274
362
|
const activeId = typeof res.json.activeId === "string" ? res.json.activeId : null;
|
|
275
363
|
const keys = Array.isArray(res.json.keys) ? res.json.keys as ApiKeyDto[] : [];
|
|
@@ -284,9 +372,15 @@ async function fetchKeyRows(deps: AccountDeps, baseUrl: string, name: string): P
|
|
|
284
372
|
return { rows, activeId, status: 200 };
|
|
285
373
|
}
|
|
286
374
|
|
|
287
|
-
export function fetchRows(
|
|
288
|
-
|
|
289
|
-
|
|
375
|
+
export function fetchRows(
|
|
376
|
+
deps: AccountDeps,
|
|
377
|
+
baseUrl: string,
|
|
378
|
+
name: string,
|
|
379
|
+
type: AccountType,
|
|
380
|
+
quota?: { refresh?: boolean },
|
|
381
|
+
): Promise<FamilyRows> {
|
|
382
|
+
if (type === "codex") return fetchCodexRows(deps, baseUrl, Boolean(quota?.refresh), quota !== undefined);
|
|
383
|
+
if (type === "oauth") return fetchOAuthRows(deps, baseUrl, name, quota);
|
|
290
384
|
return fetchKeyRows(deps, baseUrl, name);
|
|
291
385
|
}
|
|
292
386
|
|
|
@@ -294,8 +388,11 @@ export async function fetchProviderQuotaReport(
|
|
|
294
388
|
deps: AccountDeps,
|
|
295
389
|
baseUrl: string,
|
|
296
390
|
name: string,
|
|
297
|
-
): Promise<{ status: number; report: ProviderQuotaReportDto | null; errorJson?: Record<string, unknown
|
|
391
|
+
): Promise<{ status: number; report: ProviderQuotaReportDto | null; errorJson?: Record<string, unknown>; transportError?: string }> {
|
|
298
392
|
const res = await apiJson(deps, baseUrl, "GET", "/api/provider-quotas?refresh=1");
|
|
393
|
+
if (res.status === 0) {
|
|
394
|
+
return { status: 0, report: null, errorJson: res.json, transportError: res.transportError };
|
|
395
|
+
}
|
|
299
396
|
if (res.status !== 200) return { status: res.status, report: null, errorJson: res.json };
|
|
300
397
|
const reports = Array.isArray(res.json.reports) ? res.json.reports as ProviderQuotaReportDto[] : [];
|
|
301
398
|
return { status: 200, report: reports.find(report => report?.provider === name) ?? null };
|
|
@@ -40,6 +40,11 @@ const EXTENDED_USAGE = `Usage:
|
|
|
40
40
|
ocx account auto-switch <provider> <on|off|status|threshold <0-100>> [--json]
|
|
41
41
|
ocx account alias <provider> <id|main> <display-name|-> [--json]
|
|
42
42
|
ocx account priority <provider> <id|main> [<-100..100|first|earlier|normal|later|last|reset>] [--json]
|
|
43
|
+
ocx account pause <provider> <id|main> [--json]
|
|
44
|
+
ocx account resume <provider> <id|main> [--json]
|
|
45
|
+
ocx account pause-exhausted <provider> [--json]
|
|
46
|
+
ocx account strategy <provider> [<quota|round-robin|fill-first>] [--json]
|
|
47
|
+
ocx account sticky <provider> [<1-100>] [--json]
|
|
43
48
|
ocx account remove <provider> <id|main> --yes [--json]
|
|
44
49
|
ocx account clear-cooldown <provider> <id|main> [--json]
|
|
45
50
|
ocx account add-key <provider> [--label <label>] [--json]
|
|
@@ -232,8 +237,8 @@ function configAndType(deps: AccountDeps, name: string) {
|
|
|
232
237
|
}
|
|
233
238
|
|
|
234
239
|
function familyFailure(result: FamilyRows, fallback: string): number | null {
|
|
235
|
-
if (result.networkDown) return proxyUnreachable();
|
|
236
|
-
if (result.errorJson) return apiError(result.errorJson, fallback);
|
|
240
|
+
if (result.networkDown) return proxyUnreachable(result.transportError);
|
|
241
|
+
if (result.errorJson) return apiError(result.errorJson, fallback, result.status);
|
|
237
242
|
return null;
|
|
238
243
|
}
|
|
239
244
|
|
|
@@ -249,17 +254,14 @@ function resetIso(value: number | undefined): string | null {
|
|
|
249
254
|
|
|
250
255
|
function refreshLine(row: FamilyRows["rows"][number]): string {
|
|
251
256
|
const parts = [row.id === MAIN_ID ? "main" : row.id, row.email, row.plan];
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const monthlyReset = resetIso(quota.monthlyResetAt);
|
|
261
|
-
if (monthlyReset) parts.push(`resets ${monthlyReset}`);
|
|
262
|
-
}
|
|
257
|
+
if (row.paused) parts.push("paused");
|
|
258
|
+
// Was a second quota dialect: it gated the whole block on weekly/monthly, so an account
|
|
259
|
+
// reporting only a 5h window printed `quota: unknown` while `quotaParts` five lines below
|
|
260
|
+
// rendered the same data correctly for the provider path (#2703). Two halves of one file
|
|
261
|
+
// disagreeing about how to read one DTO is the defect; delegating removes it rather than
|
|
262
|
+
// teaching the second dialect a third window.
|
|
263
|
+
const quotaText = row.quota ? quotaParts(row.quota).join(" ") : "";
|
|
264
|
+
parts.push(quotaText.length > 0 ? quotaText : "quota: unknown");
|
|
263
265
|
if (row.needsReauth) parts.push("needs-reauth");
|
|
264
266
|
return parts.filter(Boolean).join(" ");
|
|
265
267
|
}
|
|
@@ -279,7 +281,7 @@ function quotaParts(quota: ProviderQuotaDto): string[] {
|
|
|
279
281
|
return parts;
|
|
280
282
|
}
|
|
281
283
|
|
|
282
|
-
function providerQuotaLine(name: string, report: ProviderQuotaReportDto): string {
|
|
284
|
+
export function providerQuotaLine(name: string, report: ProviderQuotaReportDto): string {
|
|
283
285
|
return [name, ...quotaParts(report.quota)].join(" ");
|
|
284
286
|
}
|
|
285
287
|
|
|
@@ -325,8 +327,8 @@ export async function cmdRefresh(args: string[], deps: AccountDeps): Promise<num
|
|
|
325
327
|
if (!baseUrl) return proxyUnreachable();
|
|
326
328
|
if (classified.type !== "codex") {
|
|
327
329
|
const result = await fetchProviderQuotaReport(deps, baseUrl, name);
|
|
328
|
-
if (result.status === 0) return proxyUnreachable();
|
|
329
|
-
if (result.status !== 200) return apiError(result.errorJson ?? {}, `failed to refresh ${name}
|
|
330
|
+
if (result.status === 0) return proxyUnreachable(result.transportError);
|
|
331
|
+
if (result.status !== 200) return apiError(result.errorJson ?? {}, `failed to refresh ${name}`, result.status);
|
|
330
332
|
if (wantsJson) console.log(JSON.stringify({ provider: name, report: result.report }, null, 2));
|
|
331
333
|
else console.log(result.report ? providerQuotaLine(name, result.report) : `no quota report available for ${name}`);
|
|
332
334
|
return 0;
|
|
@@ -360,15 +362,15 @@ export async function cmdAutoSwitch(args: string[], deps: AccountDeps): Promise<
|
|
|
360
362
|
if (!baseUrl) return proxyUnreachable();
|
|
361
363
|
if (action === "status") {
|
|
362
364
|
const response = await apiJson(deps, baseUrl, "GET", "/api/codex-auth/active");
|
|
363
|
-
if (response.status === 0) return proxyUnreachable();
|
|
365
|
+
if (response.status === 0) return proxyUnreachable(response.transportError);
|
|
364
366
|
if (response.status !== 200 || typeof response.json.autoSwitchThreshold !== "number") {
|
|
365
|
-
return apiError(response.json, "failed to read auto-switch status");
|
|
367
|
+
return apiError(response.json, "failed to read auto-switch status", response.status);
|
|
366
368
|
}
|
|
367
369
|
threshold = response.json.autoSwitchThreshold;
|
|
368
370
|
} else {
|
|
369
371
|
const response = await apiJson(deps, baseUrl, "PUT", "/api/codex-auth/auto-switch", { threshold });
|
|
370
|
-
if (response.status === 0) return proxyUnreachable();
|
|
371
|
-
if (response.status !== 200) return apiError(response.json, "failed to update auto-switch");
|
|
372
|
+
if (response.status === 0) return proxyUnreachable(response.transportError);
|
|
373
|
+
if (response.status !== 200) return apiError(response.json, "failed to update auto-switch", response.status);
|
|
372
374
|
}
|
|
373
375
|
const enabled = threshold! > 0;
|
|
374
376
|
if (wantsJson) console.log(JSON.stringify({ provider: name, autoSwitchThreshold: threshold, enabled }, null, 2));
|
|
@@ -459,8 +461,8 @@ export async function cmdAddKey(args: string[], deps: AccountDeps): Promise<numb
|
|
|
459
461
|
const baseUrl = await resolveBaseUrl(deps);
|
|
460
462
|
if (!baseUrl) return proxyUnreachable();
|
|
461
463
|
const response = await apiJson(deps, baseUrl, "POST", "/api/providers/keys", { name, key, ...(label ? { label } : {}) });
|
|
462
|
-
if (response.status === 0) return proxyUnreachable();
|
|
463
|
-
if (response.status !== 201) return apiError(response.json, `failed to add a key for ${name}
|
|
464
|
+
if (response.status === 0) return proxyUnreachable(response.transportError);
|
|
465
|
+
if (response.status !== 201) return apiError(response.json, `failed to add a key for ${name}`, response.status);
|
|
464
466
|
const id = typeof response.json.id === "string" ? response.json.id : null;
|
|
465
467
|
// Redact the key inside the label BEFORE serialization — a key containing
|
|
466
468
|
// JSON-escaped characters (" or \) would otherwise survive the whole-output
|
|
@@ -536,7 +538,7 @@ export async function cmdImport(args: string[], deps: AccountDeps): Promise<numb
|
|
|
536
538
|
clearTimeout(timer);
|
|
537
539
|
}
|
|
538
540
|
if (response.status === 0) {
|
|
539
|
-
if (!timedOut) return proxyUnreachable();
|
|
541
|
+
if (!timedOut) return proxyUnreachable(response.transportError);
|
|
540
542
|
console.error(`Error: import_timeout after ${importTimeoutMs}ms`);
|
|
541
543
|
return 1;
|
|
542
544
|
}
|
|
@@ -589,8 +591,8 @@ export async function cmdClearCooldown(args: string[], deps: AccountDeps): Promi
|
|
|
589
591
|
const baseUrl = await resolveBaseUrl(deps);
|
|
590
592
|
if (!baseUrl) return proxyUnreachable();
|
|
591
593
|
const response = await apiJson(deps, baseUrl, "POST", "/api/codex-auth/accounts/clear-cooldown", { id });
|
|
592
|
-
if (response.status === 0) return proxyUnreachable();
|
|
593
|
-
if (response.status !== 200) return apiError(response.json, `failed to clear cooldown for ${requestedId}
|
|
594
|
+
if (response.status === 0) return proxyUnreachable(response.transportError);
|
|
595
|
+
if (response.status !== 200) return apiError(response.json, `failed to clear cooldown for ${requestedId}`, response.status);
|
|
594
596
|
const cleared = response.json?.cleared === true;
|
|
595
597
|
if (wantsJson) console.log(JSON.stringify({ ok: true, provider: name, id, cleared }, null, 2));
|
|
596
598
|
else if (cleared) console.log(`${name}: cooldown lifted for ${requestedId}`);
|
|
@@ -680,8 +682,8 @@ export async function cmdPriority(args: string[], deps: AccountDeps): Promise<nu
|
|
|
680
682
|
}
|
|
681
683
|
|
|
682
684
|
const response = await apiJson(deps, baseUrl, "PUT", "/api/codex-auth/accounts/priority", { id, priority });
|
|
683
|
-
if (response.status === 0) return proxyUnreachable();
|
|
684
|
-
if (response.status !== 200) return apiError(response.json, `failed to set selection order for ${requestedId}
|
|
685
|
+
if (response.status === 0) return proxyUnreachable(response.transportError);
|
|
686
|
+
if (response.status !== 200) return apiError(response.json, `failed to set selection order for ${requestedId}`, response.status);
|
|
685
687
|
const applied = typeof response.json.priority === "number" ? response.json.priority : (priority ?? 0);
|
|
686
688
|
if (wantsJson) {
|
|
687
689
|
console.log(JSON.stringify(
|
|
@@ -703,6 +705,237 @@ export async function cmdPriority(args: string[], deps: AccountDeps): Promise<nu
|
|
|
703
705
|
return 0;
|
|
704
706
|
}
|
|
705
707
|
|
|
708
|
+
/**
|
|
709
|
+
* `ocx account pause|resume <provider> <id>` (#2702).
|
|
710
|
+
*
|
|
711
|
+
* The server routes have always existed; only the CLI caller was missing, so pausing an
|
|
712
|
+
* account was dashboard-only. The issue reports these as POST; the code is PUT
|
|
713
|
+
* (`auth-api.ts:1494`), and the route is shared by both directions with a `paused` boolean
|
|
714
|
+
* rather than being two endpoints.
|
|
715
|
+
*/
|
|
716
|
+
export async function cmdPause(args: string[], deps: AccountDeps, paused: boolean): Promise<number> {
|
|
717
|
+
const wantsJson = flag(args, "--json");
|
|
718
|
+
const name = args.shift();
|
|
719
|
+
const requestedId = args.shift();
|
|
720
|
+
const verb = paused ? "pause" : "resume";
|
|
721
|
+
if (!name || !requestedId || args.length) return usage();
|
|
722
|
+
const classified = configAndType(deps, name);
|
|
723
|
+
if ("error" in classified) return usage(`Error: ${classified.error}`);
|
|
724
|
+
if (classified.type !== "codex") {
|
|
725
|
+
return usage(`Error: ${verb} applies to the openai Codex account pool`);
|
|
726
|
+
}
|
|
727
|
+
const id = requestedId === "main" ? MAIN_ID : requestedId;
|
|
728
|
+
|
|
729
|
+
const baseUrl = await resolveBaseUrl(deps);
|
|
730
|
+
if (!baseUrl) return proxyUnreachable();
|
|
731
|
+
|
|
732
|
+
const response = await apiJson(deps, baseUrl, "PUT", "/api/codex-auth/accounts/pause", { id, paused });
|
|
733
|
+
// Transport sentinel first: status 0 means the proxy never answered, and comparing it to
|
|
734
|
+
// 200 would report an unreachable proxy as a management error.
|
|
735
|
+
if (response.status === 0) return proxyUnreachable(response.transportError);
|
|
736
|
+
if (response.status !== 200) return apiError(response.json, `failed to ${verb} ${requestedId}`, response.status);
|
|
737
|
+
|
|
738
|
+
if (wantsJson) {
|
|
739
|
+
console.log(JSON.stringify({ ok: true, provider: name, id, paused }, null, 2));
|
|
740
|
+
} else {
|
|
741
|
+
console.log(`${name}: ${requestedId} ${paused ? "paused" : "resumed"}`);
|
|
742
|
+
}
|
|
743
|
+
if (paused) {
|
|
744
|
+
// Both are server-side effects of this route (auth-api.ts:1508-1510), not consequences
|
|
745
|
+
// the operator would infer from the word "pause".
|
|
746
|
+
console.error("Threads bound to this account are unbound, and a fallback account is selected if this one was active.");
|
|
747
|
+
}
|
|
748
|
+
return 0;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* `ocx account pause-exhausted [--off]` (#2702).
|
|
753
|
+
*
|
|
754
|
+
* Pauses every account whose quota is spent. The route refreshes quota for each account, so
|
|
755
|
+
* it can partially fail; the response distinguishes "checked none and some failed" from
|
|
756
|
+
* "checked some", and that distinction is reported rather than flattened into ok/not-ok.
|
|
757
|
+
*/
|
|
758
|
+
export async function cmdPauseExhausted(args: string[], deps: AccountDeps): Promise<number> {
|
|
759
|
+
const wantsJson = flag(args, "--json");
|
|
760
|
+
const name = args.shift();
|
|
761
|
+
if (!name || args.length) return usage();
|
|
762
|
+
const classified = configAndType(deps, name);
|
|
763
|
+
if ("error" in classified) return usage(`Error: ${classified.error}`);
|
|
764
|
+
if (classified.type !== "codex") {
|
|
765
|
+
return usage("Error: pause-exhausted applies to the openai Codex account pool");
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const baseUrl = await resolveBaseUrl(deps);
|
|
769
|
+
if (!baseUrl) return proxyUnreachable();
|
|
770
|
+
|
|
771
|
+
const response = await apiJson(deps, baseUrl, "PUT", "/api/codex-auth/accounts/pause-exhausted", {});
|
|
772
|
+
if (response.status === 0) return proxyUnreachable(response.transportError);
|
|
773
|
+
if (response.status !== 200) return apiError(response.json, "failed to pause exhausted accounts", response.status);
|
|
774
|
+
|
|
775
|
+
const pausedIds = Array.isArray(response.json.pausedAccountIds)
|
|
776
|
+
? (response.json.pausedAccountIds as unknown[]).filter((value): value is string => typeof value === "string")
|
|
777
|
+
: [];
|
|
778
|
+
const checked = typeof response.json.checkedAccountCount === "number" ? response.json.checkedAccountCount : null;
|
|
779
|
+
const failed = typeof response.json.failedAccountCount === "number" ? response.json.failedAccountCount : null;
|
|
780
|
+
|
|
781
|
+
const complete = failed === null || failed === 0;
|
|
782
|
+
const ok = complete;
|
|
783
|
+
if (failed !== null && failed > 0) {
|
|
784
|
+
console.error(`Quota refresh failed for ${failed} account(s); those were not evaluated.`);
|
|
785
|
+
}
|
|
786
|
+
if (wantsJson) {
|
|
787
|
+
console.log(JSON.stringify({
|
|
788
|
+
ok,
|
|
789
|
+
complete,
|
|
790
|
+
provider: name,
|
|
791
|
+
pausedAccountIds: pausedIds,
|
|
792
|
+
checkedAccountCount: checked,
|
|
793
|
+
failedAccountCount: failed,
|
|
794
|
+
}, null, 2));
|
|
795
|
+
return ok ? 0 : 1;
|
|
796
|
+
}
|
|
797
|
+
console.log(pausedIds.length > 0
|
|
798
|
+
? `${name}: paused ${pausedIds.length} exhausted account(s): ${pausedIds.join(", ")}`
|
|
799
|
+
: `${name}: no exhausted accounts to pause`);
|
|
800
|
+
return ok ? 0 : 1;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Two pools expose strategy and sticky, and they are NOT reached the same way:
|
|
805
|
+
*
|
|
806
|
+
* | | Codex pool | Anthropic pool |
|
|
807
|
+
* |---|---|---|
|
|
808
|
+
* | read | `GET /api/codex-auth/active` | `GET /api/oauth/accounts/pool?provider=` |
|
|
809
|
+
* | write | `PUT /api/codex-auth/pool-strategy` | `PUT /api/oauth/accounts/pool` |
|
|
810
|
+
* | keys | `accountPoolStrategy`/`accountPoolStickyLimit` | `strategy`/`stickyLimit` |
|
|
811
|
+
* | body | bare field | field **plus** a mandatory `provider` |
|
|
812
|
+
*
|
|
813
|
+
* Omitting `provider` from the Anthropic write body earns a 400
|
|
814
|
+
* (`oauth-account-routes.ts:344`), so the asymmetry has to be encoded somewhere. Encoding it
|
|
815
|
+
* here keeps ONE verb pair working on both pools. The alternative the plan left open -- a second
|
|
816
|
+
* `provider-strategy`/`provider-sticky` pair -- would double the surface an operator must learn
|
|
817
|
+
* to express one idea, and a CLI that can steer one pool and not the other is exactly the trap
|
|
818
|
+
* this unit exists to remove.
|
|
819
|
+
*/
|
|
820
|
+
interface PoolTransport {
|
|
821
|
+
readPath: string;
|
|
822
|
+
writePath: string;
|
|
823
|
+
/** Response key carrying the applied strategy. */
|
|
824
|
+
strategyKey: string;
|
|
825
|
+
/** Response key carrying the applied sticky limit. */
|
|
826
|
+
stickyKey: string;
|
|
827
|
+
writeBody: (field: "strategy" | "stickyLimit", value: unknown) => Record<string, unknown>;
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const CODEX_POOL_TRANSPORT: PoolTransport = {
|
|
831
|
+
readPath: "/api/codex-auth/active",
|
|
832
|
+
writePath: "/api/codex-auth/pool-strategy",
|
|
833
|
+
strategyKey: "accountPoolStrategy",
|
|
834
|
+
stickyKey: "accountPoolStickyLimit",
|
|
835
|
+
writeBody: (field, value) => ({ [field]: value }),
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
function anthropicPoolTransport(provider: string): PoolTransport {
|
|
839
|
+
return {
|
|
840
|
+
readPath: `/api/oauth/accounts/pool?provider=${encodeURIComponent(provider)}`,
|
|
841
|
+
writePath: "/api/oauth/accounts/pool",
|
|
842
|
+
strategyKey: "strategy",
|
|
843
|
+
stickyKey: "stickyLimit",
|
|
844
|
+
writeBody: (field, value) => ({ provider, [field]: value }),
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* The pool-config route supports `anthropic` only and says so with a 400. Any other OAuth
|
|
850
|
+
* provider is refused here with the same wording rather than spending a round-trip to learn it.
|
|
851
|
+
*/
|
|
852
|
+
function poolTransportFor(
|
|
853
|
+
classified: { type: "codex" | "oauth" | "api-key" },
|
|
854
|
+
name: string,
|
|
855
|
+
): PoolTransport | string {
|
|
856
|
+
if (classified.type === "codex") return CODEX_POOL_TRANSPORT;
|
|
857
|
+
if (classified.type === "oauth" && name === "anthropic") return anthropicPoolTransport(name);
|
|
858
|
+
return `pool settings apply to the openai Codex pool and the anthropic pool, not "${name}"`;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* `ocx account strategy <provider> [<name>]` and `ocx account sticky <provider> [<n>]` (#2702).
|
|
863
|
+
*
|
|
864
|
+
* One implementation rather than two near-duplicates, because strategy and sticky are two
|
|
865
|
+
* fields of one setting on every pool that has them.
|
|
866
|
+
*
|
|
867
|
+
* Values are NOT re-validated here. The server owns both contracts -- three strategy names,
|
|
868
|
+
* a 1-100 sticky bound -- and a duplicated bound is a second thing to keep in sync. Its 400
|
|
869
|
+
* is actionable now that the CLI prints `reason`.
|
|
870
|
+
*/
|
|
871
|
+
async function poolSetting(
|
|
872
|
+
args: string[],
|
|
873
|
+
deps: AccountDeps,
|
|
874
|
+
field: "strategy" | "stickyLimit",
|
|
875
|
+
): Promise<number> {
|
|
876
|
+
const wantsJson = flag(args, "--json");
|
|
877
|
+
const name = args.shift();
|
|
878
|
+
const requested = args.shift();
|
|
879
|
+
const label = field === "strategy" ? "pool strategy" : "sticky limit";
|
|
880
|
+
if (!name || args.length) return usage();
|
|
881
|
+
const classified = configAndType(deps, name);
|
|
882
|
+
if ("error" in classified) return usage(`Error: ${classified.error}`);
|
|
883
|
+
const transport = poolTransportFor(classified, name);
|
|
884
|
+
if (typeof transport === "string") return usage(`Error: ${transport}`);
|
|
885
|
+
|
|
886
|
+
const baseUrl = await resolveBaseUrl(deps);
|
|
887
|
+
if (!baseUrl) return proxyUnreachable();
|
|
888
|
+
|
|
889
|
+
// No value means show. A read must not rewrite what it is reporting.
|
|
890
|
+
if (requested === undefined) {
|
|
891
|
+
const response = await apiJson(deps, baseUrl, "GET", transport.readPath);
|
|
892
|
+
if (response.status === 0) return proxyUnreachable(response.transportError);
|
|
893
|
+
if (response.status !== 200) return apiError(response.json, `failed to read ${label}`, response.status);
|
|
894
|
+
const strategy = response.json[transport.strategyKey];
|
|
895
|
+
const sticky = response.json[transport.stickyKey];
|
|
896
|
+
if (wantsJson) {
|
|
897
|
+
// Pool-neutral key names: the two routes spell the same two settings differently, and a
|
|
898
|
+
// `--json` consumer should not have to branch on which pool answered.
|
|
899
|
+
console.log(JSON.stringify({ ok: true, provider: name, strategy, stickyLimit: sticky }, null, 2));
|
|
900
|
+
} else {
|
|
901
|
+
console.log(`${name}: ${label} is ${String(field === "strategy" ? strategy : sticky)}`);
|
|
902
|
+
}
|
|
903
|
+
return 0;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// Sent as a number when it parses as one so the server sees the type it validates;
|
|
907
|
+
// a non-numeric string still goes through and earns the server's own 400.
|
|
908
|
+
const value = field === "strategy"
|
|
909
|
+
? requested
|
|
910
|
+
: (Number.isNaN(Number(requested)) ? requested : Number(requested));
|
|
911
|
+
const response = await apiJson(deps, baseUrl, "PUT", transport.writePath, transport.writeBody(field, value));
|
|
912
|
+
if (response.status === 0) return proxyUnreachable(response.transportError);
|
|
913
|
+
if (response.status !== 200) return apiError(response.json, `failed to set ${label}`, response.status);
|
|
914
|
+
|
|
915
|
+
if (wantsJson) {
|
|
916
|
+
console.log(JSON.stringify({
|
|
917
|
+
ok: true,
|
|
918
|
+
provider: name,
|
|
919
|
+
strategy: response.json[transport.strategyKey],
|
|
920
|
+
stickyLimit: response.json[transport.stickyKey],
|
|
921
|
+
}, null, 2));
|
|
922
|
+
} else {
|
|
923
|
+
// Echo the APPLIED value from the response, not the requested one: the server normalizes,
|
|
924
|
+
// and printing the request would hide a normalization the operator should see.
|
|
925
|
+
const applied = field === "strategy" ? response.json[transport.strategyKey] : response.json[transport.stickyKey];
|
|
926
|
+
console.log(`${name}: ${label} is now ${String(applied)}`);
|
|
927
|
+
}
|
|
928
|
+
return 0;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
export function cmdStrategy(args: string[], deps: AccountDeps): Promise<number> {
|
|
932
|
+
return poolSetting(args, deps, "strategy");
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
export function cmdSticky(args: string[], deps: AccountDeps): Promise<number> {
|
|
936
|
+
return poolSetting(args, deps, "stickyLimit");
|
|
937
|
+
}
|
|
938
|
+
|
|
706
939
|
export async function cmdAlias(args: string[], deps: AccountDeps): Promise<number> {
|
|
707
940
|
const wantsJson = flag(args, "--json");
|
|
708
941
|
const name = args.shift();
|
|
@@ -728,8 +961,8 @@ export async function cmdAlias(args: string[], deps: AccountDeps): Promise<numbe
|
|
|
728
961
|
? { provider: name, accountId: id, alias }
|
|
729
962
|
: { name, id, alias };
|
|
730
963
|
const response = await apiJson(deps, baseUrl, "PUT", path, body);
|
|
731
|
-
if (response.status === 0) return proxyUnreachable();
|
|
732
|
-
if (response.status !== 200) return apiError(response.json, `failed to rename ${requestedId}
|
|
964
|
+
if (response.status === 0) return proxyUnreachable(response.transportError);
|
|
965
|
+
if (response.status !== 200) return apiError(response.json, `failed to rename ${requestedId}`, response.status);
|
|
733
966
|
const result = { ok: true, provider: name, id, alias: alias || null };
|
|
734
967
|
if (wantsJson) console.log(JSON.stringify(result, null, 2));
|
|
735
968
|
else console.log(alias ? `${name}: ${requestedId} is now “${alias}”` : `${name}: cleared alias for ${requestedId}`);
|