@coseung2/opencodex 2.8.0-cs.13 → 2.8.0-cs.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/index-MUpaVatk.js +67 -0
- package/gui/dist/index.html +1 -1
- package/package.json +3 -3
- package/packages/ocx-notch/README.md +2 -1
- package/src/adapters/cursor/discovery.ts +6 -2
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/google-antigravity-replay.ts +24 -0
- package/src/adapters/google.ts +16 -11
- package/src/chat/inbound.ts +5 -11
- package/src/cli/account-api.ts +9 -1
- package/src/cli/account-extended.ts +4 -1
- package/src/codex/account-label.ts +14 -1
- package/src/codex/account-lifecycle.ts +12 -1
- package/src/codex/account-namespaces.ts +21 -0
- package/src/codex/account-priority.ts +49 -0
- package/src/codex/account-store.ts +2 -1
- package/src/codex/auth-api.ts +108 -17
- package/src/codex/auth-context.ts +61 -16
- package/src/codex/catalog/metadata.ts +34 -12
- package/src/codex/catalog/parsing.ts +8 -1
- package/src/codex/catalog/provider-fetch.ts +24 -6
- package/src/codex/catalog.ts +1 -1
- package/src/codex/pool-rotation.ts +51 -4
- package/src/codex/quota.ts +154 -35
- package/src/codex/routing.ts +133 -33
- package/src/codex/warmup.ts +193 -85
- package/src/config.ts +84 -1
- package/src/lib/bounded-body.ts +13 -6
- package/src/lib/bun-stream-caps.ts +5 -6
- package/src/lib/redact.ts +13 -0
- package/src/oauth/index.ts +79 -12
- package/src/oauth/log.ts +3 -1
- package/src/oauth/store.ts +31 -8
- package/src/providers/antigravity-models.ts +53 -24
- package/src/providers/codex-capacity.ts +303 -0
- package/src/providers/model-rename-migration.ts +147 -0
- package/src/providers/model-rename-startup.ts +29 -0
- package/src/providers/quota.ts +126 -16
- package/src/providers/registry.ts +258 -38
- package/src/responses/parser.ts +19 -12
- package/src/responses/spill-store.ts +14 -1
- package/src/responses/state.ts +108 -14
- package/src/server/index.ts +9 -1
- package/src/server/management/logs-usage-routes.ts +1 -0
- package/src/server/management/oauth-account-routes.ts +8 -1
- package/src/server/relay.ts +10 -42
- package/src/server/request-log.ts +42 -1
- package/src/server/responses/compact.ts +16 -4
- package/src/server/responses/core.ts +217 -59
- package/src/server/responses/empty-completion-guard.ts +275 -0
- package/src/server/responses/encrypted-payload.ts +54 -39
- package/src/server/responses/fetch-helpers.ts +24 -3
- package/src/server/responses/ws-upstream.ts +318 -0
- package/src/server/sse-frame-buffer.ts +292 -0
- package/src/server/ws-bridge.ts +17 -11
- package/src/types.ts +8 -0
- package/src/usage/log.ts +24 -0
- package/src/usage/summary.ts +152 -2
- package/vendor/ocx-notch/win32-x64/ocx-notch.exe +0 -0
- package/gui/dist/assets/index-BucjyD4I.js +0 -67
package/gui/dist/index.html
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
} catch (e) {}
|
|
17
17
|
})();
|
|
18
18
|
</script>
|
|
19
|
-
<script type="module" crossorigin src="/assets/index-
|
|
19
|
+
<script type="module" crossorigin src="/assets/index-MUpaVatk.js"></script>
|
|
20
20
|
<link rel="stylesheet" crossorigin href="/assets/index-CxisOo-q.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@coseung2/opencodex",
|
|
3
|
-
"version": "2.8.0-cs.
|
|
3
|
+
"version": "2.8.0-cs.14",
|
|
4
4
|
"description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./bin/package-main.mjs",
|
|
@@ -62,11 +62,11 @@
|
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"@bufbuild/protobuf": "^2.12.0",
|
|
64
64
|
"@modelcontextprotocol/sdk": "^1",
|
|
65
|
-
"bun": "1.
|
|
65
|
+
"bun": "1.4.0",
|
|
66
66
|
"zod": "4.4.3"
|
|
67
67
|
},
|
|
68
68
|
"devDependencies": {
|
|
69
|
-
"@types/bun": "1.
|
|
69
|
+
"@types/bun": "1.4.0",
|
|
70
70
|
"typescript": "5.9.3"
|
|
71
71
|
},
|
|
72
72
|
"overrides": {
|
|
@@ -51,9 +51,10 @@ Startup failures and Rust panics are written to the bounded diagnostic log `%LOC
|
|
|
51
51
|
- Drag anywhere on the notch to move it; its chosen position is preserved while it expands, collapses, or refreshes.
|
|
52
52
|
- Click a provider with multiple accounts to expand or collapse its account rows.
|
|
53
53
|
- Click the pause icon beside an OpenAI account to exclude it from the rotation pool. A paused account shows a play icon that includes it again. The icon updates immediately and rolls back if OCX rejects the request.
|
|
54
|
+
- OpenAI accounts with reset credits show the same compact ticket badge used by the dashboard beside the pause/play control. Click it to open an in-app modal with every ticket's grant and expiry dates; OCX identifies the FIFO next ticket, confirms consumption in the modal, and refreshes the displayed count after a successful reset.
|
|
54
55
|
- Accounts that require renewed credentials expose an inline **재인증** action on the same row. OpenAI's main account is intentionally excluded; eligible OpenAI pool and generic OAuth accounts can start, cancel, and retry browser authentication.
|
|
55
56
|
- Providers with quota data are shown first. Configured providers without quota data remain visible with their usage and account controls.
|
|
56
57
|
- Press **Esc** to collapse.
|
|
57
58
|
- Right-click to add a provider, set the real OCX account rotation threshold, fine-tune it by 1%, **Refresh**, or **Exit**. The opaque provider modal groups supported presets into Account, Free, and Paid tabs. Canonical OpenAI adds another Codex account, OAuth presets use browser/device authorization, and required-key presets use masked API-key entry. Fixed-endpoint key-optional presets (including OpenCode Free and MiMo Free) accept an empty key; entering a key instead adds a distinct account slot to OCX's existing key pool, which can be switched from the provider row. Ollama, vLLM, and LM Studio use local auth without a key. Cloudflare Workers AI also appears in Free and asks for its Account ID before creating the provider. Endpoint-choice and other unresolved placeholder-URL presets remain omitted because this compact modal cannot preserve those setup contracts safely. `Off` writes threshold `0`.
|
|
58
59
|
|
|
59
|
-
Quota percentages are shown as used percentages with
|
|
60
|
+
Quota percentages are shown as used percentages with 5-hour/weekly/monthly/custom-window columns in one compact row, reset countdowns, 5px progress bars, green fill, and the green-to-amber threshold warning used by the OCX dashboard. Columns are derived from whichever quota windows the management API returns, so provider-specific windows appear without a plan-name list in the notch. Provider usage is merged by exact provider name, limited to the newest day, and formatted with Korean `만/억/조` units. OpenAI account rows show their own 5-hour/weekly/monthly quotas; OAuth and key-pool rows show their masked identity and active/health state, while provider-level quota remains associated with the active account. The native window uses a subtle 238/255 global alpha.
|
|
@@ -189,10 +189,14 @@ export const CURSOR_STATIC_MODELS: readonly CursorModelInfo[] = normalizeCursorM
|
|
|
189
189
|
{ id: "claude-4.6-opus", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
190
190
|
{ id: "claude-4.6-sonnet", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
191
191
|
{ id: "claude-opus-4-7", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
192
|
-
//
|
|
193
|
-
|
|
192
|
+
// Opus Fast families are exposed only as effort-suffixed wire ids. The
|
|
193
|
+
// request mapper therefore always resolves a tier and never sends a bare
|
|
194
|
+
// `-fast` id.
|
|
195
|
+
{ id: "claude-opus-4-7-fast", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
196
|
+
{ id: "claude-opus-4-8-fast", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
194
197
|
{ id: "claude-opus-4-8", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
195
198
|
{ id: "claude-opus-5", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
199
|
+
{ id: "claude-opus-5-fast", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
196
200
|
{ id: "claude-fable-5", contextWindow: CONTEXT_200K, supportsReasoningEffort: true },
|
|
197
201
|
|
|
198
202
|
{ id: "composer-1", contextWindow: CONTEXT_200K },
|
|
@@ -24,8 +24,11 @@ const CURSOR_MODEL_EFFORT_TIERS: Record<string, readonly string[]> = {
|
|
|
24
24
|
// against Anthropic's effort ladder docs and Cursor's live model lineup.
|
|
25
25
|
"claude-fable-5": ["low", "medium", "high", "xhigh", "max"],
|
|
26
26
|
"claude-opus-4-7": ["low", "medium", "high", "xhigh", "max"],
|
|
27
|
+
"claude-opus-4-7-fast": ["low", "medium", "high", "xhigh", "max"],
|
|
27
28
|
"claude-opus-4-8": ["low", "medium", "high", "xhigh", "max"],
|
|
29
|
+
"claude-opus-4-8-fast": ["low", "medium", "high", "xhigh", "max"],
|
|
28
30
|
"claude-opus-5": ["low", "medium", "high", "xhigh", "max"],
|
|
31
|
+
"claude-opus-5-fast": ["low", "medium", "high"],
|
|
29
32
|
"claude-sonnet-5": ["low", "medium", "high", "xhigh", "max"],
|
|
30
33
|
"glm-5.2": ["high", "max"],
|
|
31
34
|
// GetUsableModels (2026-07-28) lists kimi-k3 only as effort-suffixed kimi-k3-{low,high,max};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createHmac, randomBytes } from "node:crypto";
|
|
1
2
|
import { enforceAppOwnedMemoryBudget } from "../lib/app-owned-memory";
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -48,6 +49,7 @@ const DEFAULT_REPLAY_LIMITS: ReplayLimits = {
|
|
|
48
49
|
};
|
|
49
50
|
|
|
50
51
|
const replayCache = new Map<string, ReplayEntry>();
|
|
52
|
+
const replayIsolationKey = randomBytes(32);
|
|
51
53
|
const utf8 = new TextEncoder();
|
|
52
54
|
let replayLimits = { ...DEFAULT_REPLAY_LIMITS };
|
|
53
55
|
let replayBytes = 0;
|
|
@@ -58,6 +60,28 @@ function replayKey(model: string, sessionId: string): string {
|
|
|
58
60
|
return `${model}::session:${sessionId}`;
|
|
59
61
|
}
|
|
60
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Process-local replay namespace isolated by both destination and credential.
|
|
65
|
+
* Raw credentials never become cache keys; the HMAC output is non-reversible and
|
|
66
|
+
* intentionally changes after restart because this replay cache is memory-only.
|
|
67
|
+
*/
|
|
68
|
+
export function scopedAntigravityReplaySessionId(
|
|
69
|
+
sessionId: string,
|
|
70
|
+
baseUrl: string | undefined,
|
|
71
|
+
credential: string | undefined,
|
|
72
|
+
headers?: Record<string, string>,
|
|
73
|
+
): string | undefined {
|
|
74
|
+
const session = sessionId.trim();
|
|
75
|
+
const destination = baseUrl?.trim().replace(/\/+$/, "");
|
|
76
|
+
const secret = credential?.trim();
|
|
77
|
+
if (!session || !destination || !secret) return undefined;
|
|
78
|
+
const headerEntries = Object.entries(headers ?? {}).sort(([a], [b]) => a.localeCompare(b));
|
|
79
|
+
const scope = createHmac("sha256", replayIsolationKey)
|
|
80
|
+
.update(JSON.stringify([destination, secret, headerEntries]))
|
|
81
|
+
.digest("hex");
|
|
82
|
+
return `${scope}:${session}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
61
85
|
/** Recursively canonicalize a JSON value: object keys sorted, arrays preserved. */
|
|
62
86
|
function canonicalJson(value: unknown): string {
|
|
63
87
|
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
package/src/adapters/google.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { isVertexTruncationReason, vertexTruncationErrorMessage } from "./google
|
|
|
21
21
|
import { ANTIGRAVITY_REQUEST_UA, antigravitySessionId, isLikelyRealThoughtSignature, sanitizeAntigravityClaudeSignatures } from "./google-antigravity-wire";
|
|
22
22
|
import { compileGoogleWireBody } from "./google-wire-compiler";
|
|
23
23
|
import { neutralizeIdentity } from "./identity";
|
|
24
|
-
import { antigravityUsesReplayCache, applyAntigravityReplay, clearAntigravityReplay, observeAntigravityReplay } from "./google-antigravity-replay";
|
|
24
|
+
import { antigravityUsesReplayCache, applyAntigravityReplay, clearAntigravityReplay, observeAntigravityReplay, scopedAntigravityReplaySessionId } from "./google-antigravity-replay";
|
|
25
25
|
import { resolveAntigravityEffortWireModel } from "../providers/antigravity-models";
|
|
26
26
|
import {
|
|
27
27
|
isTranslatorBudgetExceededError,
|
|
@@ -275,7 +275,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
275
275
|
// Per-request closure: resolveAdapter builds a fresh adapter per request (server.ts), so buildRequest
|
|
276
276
|
// can stash the CCA model/session for parseStream's reasoning-replay observation.
|
|
277
277
|
let antigravityModel: string | undefined;
|
|
278
|
-
let
|
|
278
|
+
let antigravityReplaySession: string | undefined;
|
|
279
279
|
let restoreGoogleToolName = (name: string): string => name;
|
|
280
280
|
return {
|
|
281
281
|
name: "google",
|
|
@@ -333,7 +333,12 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
333
333
|
const mappedEffort = mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning);
|
|
334
334
|
const { wireModelId, thinkingLevel } = resolveAntigravityEffortWireModel(parsed.modelId, mappedEffort);
|
|
335
335
|
antigravityModel = wireModelId;
|
|
336
|
-
|
|
336
|
+
antigravityReplaySession = scopedAntigravityReplaySessionId(
|
|
337
|
+
sessionId,
|
|
338
|
+
provider.baseUrl,
|
|
339
|
+
provider.apiKey,
|
|
340
|
+
provider.headers,
|
|
341
|
+
);
|
|
337
342
|
// Effort → thinkingConfig for CCA (CLIProxyAPI proven: request.generationConfig.thinkingConfig).
|
|
338
343
|
// Suffix/compat IDs return thinkingLevel=undefined — the suffix IS the effort, no contradiction.
|
|
339
344
|
if (thinkingLevel) {
|
|
@@ -359,8 +364,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
359
364
|
// Compile names before replay: signatures are keyed by the exact provider-visible name.
|
|
360
365
|
if (Array.isArray((request as { contents?: unknown[] }).contents)) {
|
|
361
366
|
const contents = (request as { contents: unknown[] }).contents;
|
|
362
|
-
if (antigravityUsesReplayCache(wireModelId)) {
|
|
363
|
-
applyAntigravityReplay(wireModelId,
|
|
367
|
+
if (antigravityUsesReplayCache(wireModelId) && antigravityReplaySession) {
|
|
368
|
+
applyAntigravityReplay(wireModelId, antigravityReplaySession, contents);
|
|
364
369
|
} else {
|
|
365
370
|
sanitizeAntigravityClaudeSignatures(contents);
|
|
366
371
|
}
|
|
@@ -457,9 +462,9 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
457
462
|
const err = chunk.error as { message?: string } | undefined;
|
|
458
463
|
// Clear-on-invalid: a signature rejection means our replayed thoughtSignatures are stale.
|
|
459
464
|
// Drop the cache entry so the next turn starts clean instead of re-injecting a bad sig.
|
|
460
|
-
if (provider.googleMode === "cloud-code-assist" && antigravityModel &&
|
|
465
|
+
if (provider.googleMode === "cloud-code-assist" && antigravityModel && antigravityReplaySession
|
|
461
466
|
&& /signature|invalid_argument|invalid argument/i.test(err?.message ?? "")) {
|
|
462
|
-
clearAntigravityReplay(antigravityModel,
|
|
467
|
+
clearAntigravityReplay(antigravityModel, antigravityReplaySession);
|
|
463
468
|
}
|
|
464
469
|
yield { type: "error", message: err?.message ?? "upstream error" };
|
|
465
470
|
return "terminate";
|
|
@@ -494,8 +499,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
494
499
|
|
|
495
500
|
const parts = candidates[0].content?.parts as { text?: string; functionCall?: { name: string; args: unknown } }[] | undefined;
|
|
496
501
|
// Antigravity reasoning-replay: record thoughtSignatures from the model parts for the next turn.
|
|
497
|
-
if (provider.googleMode === "cloud-code-assist" && parts && antigravityModel &&
|
|
498
|
-
observeAntigravityReplay(antigravityModel,
|
|
502
|
+
if (provider.googleMode === "cloud-code-assist" && parts && antigravityModel && antigravityReplaySession) {
|
|
503
|
+
observeAntigravityReplay(antigravityModel, antigravityReplaySession, parts as unknown[]);
|
|
499
504
|
}
|
|
500
505
|
if (parts) {
|
|
501
506
|
for (const part of parts) {
|
|
@@ -711,8 +716,8 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
|
|
|
711
716
|
const imageBudget = createImageBudget();
|
|
712
717
|
if (candidates?.[0]?.content?.parts) {
|
|
713
718
|
// Non-streaming CCA: observe thoughtSignatures for the next turn, same as the stream path.
|
|
714
|
-
if (provider.googleMode === "cloud-code-assist" && antigravityModel &&
|
|
715
|
-
observeAntigravityReplay(antigravityModel,
|
|
719
|
+
if (provider.googleMode === "cloud-code-assist" && antigravityModel && antigravityReplaySession) {
|
|
720
|
+
observeAntigravityReplay(antigravityModel, antigravityReplaySession, candidates[0].content.parts as unknown[]);
|
|
716
721
|
}
|
|
717
722
|
for (const part of candidates[0].content.parts) {
|
|
718
723
|
if (part.text) events.push({ type: "text_delta", text: part.text });
|
package/src/chat/inbound.ts
CHANGED
|
@@ -86,17 +86,8 @@ function pushSystemText(parts: string[], content: unknown): void {
|
|
|
86
86
|
if (text) parts.push(text);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
function toolCallsToItems(toolCalls: unknown, input: Rec[]): void {
|
|
89
|
+
function toolCallsToItems(toolCalls: unknown, input: Rec[], knownNameByCallId: Map<string, string>): void {
|
|
90
90
|
if (!Array.isArray(toolCalls)) return;
|
|
91
|
-
// Recover names from earlier function_call items in the same transcript when a client
|
|
92
|
-
// re-sends tool_calls with only id/arguments (replace-style merge lost function.name).
|
|
93
|
-
const knownNameByCallId = new Map<string, string>();
|
|
94
|
-
for (const item of input) {
|
|
95
|
-
if (!isRec(item) || item.type !== "function_call") continue;
|
|
96
|
-
if (typeof item.call_id === "string" && typeof item.name === "string" && item.name.length > 0) {
|
|
97
|
-
knownNameByCallId.set(item.call_id, item.name);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
91
|
for (const raw of toolCalls) {
|
|
101
92
|
if (!isRec(raw)) continue;
|
|
102
93
|
const fn = isRec(raw.function) ? raw.function : null;
|
|
@@ -220,6 +211,9 @@ export function chatCompletionsToResponsesBody(raw: unknown): Rec {
|
|
|
220
211
|
|
|
221
212
|
const systemParts: string[] = [];
|
|
222
213
|
const input: Rec[] = [];
|
|
214
|
+
// Recover replace-style tool calls incrementally instead of rebuilding the
|
|
215
|
+
// call-id index from the entire translated transcript for every message.
|
|
216
|
+
const knownNameByCallId = new Map<string, string>();
|
|
223
217
|
|
|
224
218
|
for (const msg of raw.messages) {
|
|
225
219
|
if (!isRec(msg)) continue;
|
|
@@ -237,7 +231,7 @@ export function chatCompletionsToResponsesBody(raw: unknown): Rec {
|
|
|
237
231
|
case "assistant": {
|
|
238
232
|
const blocks = assistantContentToBlocks(msg.content);
|
|
239
233
|
if (blocks.length > 0) input.push({ type: "message", role: "assistant", content: blocks });
|
|
240
|
-
if (msg.tool_calls !== undefined) toolCallsToItems(msg.tool_calls, input);
|
|
234
|
+
if (msg.tool_calls !== undefined) toolCallsToItems(msg.tool_calls, input, knownNameByCallId);
|
|
241
235
|
break;
|
|
242
236
|
}
|
|
243
237
|
case "tool": {
|
package/src/cli/account-api.ts
CHANGED
|
@@ -113,8 +113,10 @@ export interface FamilyRows {
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
export interface CodexQuotaDto {
|
|
116
|
+
fiveHourPercent?: number;
|
|
116
117
|
weeklyPercent?: number;
|
|
117
118
|
monthlyPercent?: number;
|
|
119
|
+
fiveHourResetAt?: number;
|
|
118
120
|
weeklyResetAt?: number;
|
|
119
121
|
monthlyResetAt?: number;
|
|
120
122
|
}
|
|
@@ -123,6 +125,12 @@ export interface ProviderQuotaWindowDto {
|
|
|
123
125
|
label: string;
|
|
124
126
|
percent: number;
|
|
125
127
|
resetAt?: number;
|
|
128
|
+
valueLabel?: string;
|
|
129
|
+
segments?: Array<{
|
|
130
|
+
label: string;
|
|
131
|
+
percent: number;
|
|
132
|
+
resetAt?: number;
|
|
133
|
+
}>;
|
|
126
134
|
}
|
|
127
135
|
|
|
128
136
|
export interface ProviderQuotaDto extends CodexQuotaDto {
|
|
@@ -154,7 +162,7 @@ interface CodexAccountDto {
|
|
|
154
162
|
function projectQuota(quota: CodexQuotaDto | null | undefined): CodexQuotaDto | null {
|
|
155
163
|
if (!quota) return null;
|
|
156
164
|
const projected: CodexQuotaDto = {};
|
|
157
|
-
for (const key of ["weeklyPercent", "monthlyPercent", "weeklyResetAt", "monthlyResetAt"] as const) {
|
|
165
|
+
for (const key of ["fiveHourPercent", "weeklyPercent", "monthlyPercent", "fiveHourResetAt", "weeklyResetAt", "monthlyResetAt"] as const) {
|
|
158
166
|
if (typeof quota[key] === "number" && Number.isFinite(quota[key])) projected[key] = quota[key];
|
|
159
167
|
}
|
|
160
168
|
return projected;
|
|
@@ -70,9 +70,12 @@ function resetIso(value: number | undefined): string | null {
|
|
|
70
70
|
function refreshLine(row: FamilyRows["rows"][number]): string {
|
|
71
71
|
const parts = [row.id === MAIN_ID ? "main" : row.id, row.email, row.plan];
|
|
72
72
|
const quota = row.quota;
|
|
73
|
-
if (!quota || (quota.weeklyPercent === undefined && quota.monthlyPercent === undefined)) {
|
|
73
|
+
if (!quota || (quota.fiveHourPercent === undefined && quota.weeklyPercent === undefined && quota.monthlyPercent === undefined)) {
|
|
74
74
|
parts.push("quota: unknown");
|
|
75
75
|
} else {
|
|
76
|
+
if (quota.fiveHourPercent !== undefined) parts.push(`5h ${quota.fiveHourPercent}%`);
|
|
77
|
+
const fiveHourReset = resetIso(quota.fiveHourResetAt);
|
|
78
|
+
if (fiveHourReset) parts.push(`resets ${fiveHourReset}`);
|
|
76
79
|
if (quota.weeklyPercent !== undefined) parts.push(`weekly ${quota.weeklyPercent}%`);
|
|
77
80
|
const weeklyReset = resetIso(quota.weeklyResetAt);
|
|
78
81
|
if (weeklyReset) parts.push(`resets ${weeklyReset}`);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
-
import type { CodexAccount } from "../types";
|
|
2
|
+
import type { CodexAccount, OcxConfig } from "../types";
|
|
3
|
+
import type { CodexAuthContext } from "./auth-context";
|
|
4
|
+
import { MAIN_CODEX_ACCOUNT_ID } from "./main-account";
|
|
3
5
|
|
|
4
6
|
export const CODEX_ACCOUNT_LOG_LABEL_RE = /^p[a-f0-9]{6}$/;
|
|
5
7
|
|
|
@@ -22,6 +24,17 @@ export function codexAccountLogLabel(account: CodexAccount): string {
|
|
|
22
24
|
: fallbackCodexAccountLogLabel(account.id);
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
/** Effective privacy-safe label for the Codex Pool account selected to serve a request. */
|
|
28
|
+
export function codexAuthContextLogLabel(
|
|
29
|
+
authCtx: CodexAuthContext,
|
|
30
|
+
config: Pick<OcxConfig, "codexAccounts">,
|
|
31
|
+
): "main" | `p${string}` | undefined {
|
|
32
|
+
if (authCtx.kind !== "pool" && authCtx.kind !== "main-pool") return undefined;
|
|
33
|
+
if (authCtx.accountId === MAIN_CODEX_ACCOUNT_ID) return "main";
|
|
34
|
+
const account = (config.codexAccounts ?? []).find(candidate => candidate.id === authCtx.accountId);
|
|
35
|
+
return account ? codexAccountLogLabel(account) as `p${string}` : undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
25
38
|
export function withCodexAccountLogLabel(
|
|
26
39
|
account: Omit<CodexAccount, "logLabel"> & Partial<Pick<CodexAccount, "logLabel">>,
|
|
27
40
|
existingAccounts: readonly CodexAccount[],
|
|
@@ -7,6 +7,8 @@ import { clearCodexUpstreamHealthForAccount, clearThreadAccountMapForAccount } f
|
|
|
7
7
|
import { invalidateCodexWebSocketsForAccount } from "./websocket-registry";
|
|
8
8
|
import { clearMainAccountInfoCache } from "./main-account-cache";
|
|
9
9
|
import { forgetCodexAccountPause } from "./account-pause";
|
|
10
|
+
import { clearCodexAccountPin, forgetCodexAccountPriority } from "./account-priority";
|
|
11
|
+
import { codexAccountNamespaceEntries, codexAccountPickerEnabled } from "./account-namespaces";
|
|
10
12
|
import type { OcxConfig } from "../types";
|
|
11
13
|
|
|
12
14
|
let observedMainChatgptAccountId: string | undefined;
|
|
@@ -44,12 +46,21 @@ export function resetMainCodexAccountIdentityTrackingForTests(): void {
|
|
|
44
46
|
observedMainChatgptAccountId = undefined;
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
|
|
49
|
+
/** Delete credentials and runtime state while retaining the account's stable selector binding. */
|
|
50
|
+
export function deleteCodexAccount(runtimeConfig: OcxConfig, accountId: string): boolean {
|
|
51
|
+
const hadStoredAccount = (runtimeConfig.codexAccounts ?? [])
|
|
52
|
+
.some(account => !account.isMain && account.id === accountId);
|
|
53
|
+
const hadVisiblePickerBinding = hadStoredAccount
|
|
54
|
+
&& codexAccountPickerEnabled(runtimeConfig)
|
|
55
|
+
&& codexAccountNamespaceEntries(runtimeConfig).some(([, target]) => target === accountId);
|
|
48
56
|
removeCodexAccountCredential(accountId);
|
|
49
57
|
runtimeConfig.codexAccounts = (runtimeConfig.codexAccounts ?? [])
|
|
50
58
|
.filter(account => account.isMain || account.id !== accountId);
|
|
51
59
|
forgetCodexAccountPause(runtimeConfig, accountId);
|
|
60
|
+
forgetCodexAccountPriority(runtimeConfig, accountId);
|
|
61
|
+
clearCodexAccountPin(runtimeConfig, accountId);
|
|
52
62
|
if (runtimeConfig.activeCodexAccountId === accountId) runtimeConfig.activeCodexAccountId = undefined;
|
|
53
63
|
purgeCodexAccountRuntimeState(accountId);
|
|
54
64
|
invalidateCodexWebSocketsForAccount(accountId);
|
|
65
|
+
return hadVisiblePickerBinding;
|
|
55
66
|
}
|
|
@@ -99,6 +99,19 @@ export function defaultCodexAccountNamespaces(
|
|
|
99
99
|
return namespaces;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/** Initialize generated selectors only for an explicit opt-in with no existing bindings. */
|
|
103
|
+
export function initializeDefaultCodexAccountNamespaces(
|
|
104
|
+
config: Pick<
|
|
105
|
+
OcxConfig,
|
|
106
|
+
"codexAccountPickerEnabled" | "codexAccountNamespaces" | "codexAccounts" | "combos" | "providers"
|
|
107
|
+
>,
|
|
108
|
+
): boolean {
|
|
109
|
+
if (config.codexAccountPickerEnabled !== true
|
|
110
|
+
|| Object.keys(config.codexAccountNamespaces ?? {}).length > 0) return false;
|
|
111
|
+
config.codexAccountNamespaces = defaultCodexAccountNamespaces(config);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
|
|
102
115
|
/**
|
|
103
116
|
* Add one account to a generated map without renaming or replacing explicit existing entries.
|
|
104
117
|
* The account-creation layer must reject a new id that already equals an existing selector key.
|
|
@@ -147,3 +160,11 @@ export function codexAccountNamespaceEntries(
|
|
|
147
160
|
return Object.entries(config.codexAccountNamespaces ?? {})
|
|
148
161
|
.map(([namespace, accountId]) => [namespace, normalizeCodexAccountNamespaceTarget(accountId)]);
|
|
149
162
|
}
|
|
163
|
+
|
|
164
|
+
/** Existing hand-written maps remain enabled unless explicitly hidden. */
|
|
165
|
+
export function codexAccountPickerEnabled(
|
|
166
|
+
config: Pick<OcxConfig, "codexAccountNamespaces" | "codexAccountPickerEnabled">,
|
|
167
|
+
): boolean {
|
|
168
|
+
return (config.codexAccountPickerEnabled === undefined || config.codexAccountPickerEnabled === true)
|
|
169
|
+
&& Object.keys(config.codexAccountNamespaces ?? {}).length > 0;
|
|
170
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { OcxConfig } from "../types";
|
|
2
|
+
import { isValidCodexAccountId, MAIN_CODEX_ACCOUNT_ID } from "./account-id";
|
|
3
|
+
import { DEFAULT_ACCOUNT_PRIORITY, normalizeAccountPriority } from "./pool-rotation";
|
|
4
|
+
|
|
5
|
+
/** Account ids that may carry persisted selection order. */
|
|
6
|
+
export function isCodexAccountPriorityKey(key: unknown): key is string {
|
|
7
|
+
return key === MAIN_CODEX_ACCOUNT_ID || isValidCodexAccountId(key);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function getCodexAccountPriority(config: OcxConfig, accountId: string): number {
|
|
11
|
+
return codexAccountPriorityLookup(config)(accountId);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Store default priority as absence so unconfigured pools retain their old behavior. */
|
|
15
|
+
export function setCodexAccountPriority(config: OcxConfig, accountId: string, priority: number): void {
|
|
16
|
+
const entries = new Map(Object.entries(config.codexAccountPriorities ?? {}));
|
|
17
|
+
if (priority === DEFAULT_ACCOUNT_PRIORITY) entries.delete(accountId);
|
|
18
|
+
else entries.set(accountId, priority);
|
|
19
|
+
if (entries.size > 0) config.codexAccountPriorities = Object.fromEntries(entries);
|
|
20
|
+
else delete config.codexAccountPriorities;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function forgetCodexAccountPriority(config: OcxConfig, accountId: string): void {
|
|
24
|
+
setCodexAccountPriority(config, accountId, DEFAULT_ACCOUNT_PRIORITY);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Stable lookup for one routing pass. */
|
|
28
|
+
export function codexAccountPriorityLookup(config: OcxConfig): (accountId: string) => number {
|
|
29
|
+
const priorities = config.codexAccountPriorities;
|
|
30
|
+
if (!priorities) return () => DEFAULT_ACCOUNT_PRIORITY;
|
|
31
|
+
return accountId => Object.hasOwn(priorities, accountId)
|
|
32
|
+
? normalizeAccountPriority(priorities[accountId])
|
|
33
|
+
: DEFAULT_ACCOUNT_PRIORITY;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function pinnedCodexAccountId(config: OcxConfig): string | undefined {
|
|
37
|
+
return config.activeCodexAccountPinned;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function setCodexAccountPin(config: OcxConfig, accountId: string): void {
|
|
41
|
+
config.activeCodexAccountPinned = accountId;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Release the pin, optionally only when it belongs to one account. */
|
|
45
|
+
export function clearCodexAccountPin(config: OcxConfig, accountId?: string): void {
|
|
46
|
+
if (accountId === undefined || config.activeCodexAccountPinned === accountId) {
|
|
47
|
+
delete config.activeCodexAccountPinned;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -179,6 +179,7 @@ export function markCodexAccountQuotaValidationPending(
|
|
|
179
179
|
id: string,
|
|
180
180
|
reason: string,
|
|
181
181
|
retryAtMs: number | undefined,
|
|
182
|
+
pauseOwned = true,
|
|
182
183
|
): void {
|
|
183
184
|
withCredentialMutationLockSync(() => {
|
|
184
185
|
const store = loadCodexAccountRecordStore();
|
|
@@ -189,7 +190,7 @@ export function markCodexAccountQuotaValidationPending(
|
|
|
189
190
|
lastCodexValidationStatus: "quota_pending",
|
|
190
191
|
lastCodexValidationError: reason,
|
|
191
192
|
...(retryAtMs !== undefined ? { codexQuotaRetryAt: retryAtMs } : {}),
|
|
192
|
-
codexQuotaPauseOwned: true,
|
|
193
|
+
codexQuotaPauseOwned: pauseOwned ? true : undefined,
|
|
193
194
|
};
|
|
194
195
|
persist(store);
|
|
195
196
|
});
|