@coseung2/opencodex 2.8.0-cs.12 → 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.
Files changed (62) hide show
  1. package/gui/dist/assets/index-MUpaVatk.js +67 -0
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +3 -3
  4. package/packages/ocx-notch/README.md +3 -2
  5. package/src/adapters/cursor/discovery.ts +6 -2
  6. package/src/adapters/cursor/effort-map.ts +3 -0
  7. package/src/adapters/google-antigravity-replay.ts +24 -0
  8. package/src/adapters/google.ts +16 -11
  9. package/src/chat/inbound.ts +5 -11
  10. package/src/cli/account-api.ts +9 -1
  11. package/src/cli/account-extended.ts +4 -1
  12. package/src/codex/account-label.ts +14 -1
  13. package/src/codex/account-lifecycle.ts +12 -1
  14. package/src/codex/account-namespaces.ts +21 -0
  15. package/src/codex/account-priority.ts +49 -0
  16. package/src/codex/account-store.ts +73 -10
  17. package/src/codex/auth-api.ts +312 -45
  18. package/src/codex/auth-context.ts +61 -16
  19. package/src/codex/catalog/metadata.ts +34 -12
  20. package/src/codex/catalog/parsing.ts +8 -1
  21. package/src/codex/catalog/provider-fetch.ts +24 -6
  22. package/src/codex/catalog.ts +1 -1
  23. package/src/codex/pool-rotation.ts +51 -4
  24. package/src/codex/quota.ts +154 -35
  25. package/src/codex/routing.ts +133 -33
  26. package/src/codex/warmup.ts +204 -82
  27. package/src/config.ts +84 -1
  28. package/src/lib/bounded-body.ts +13 -6
  29. package/src/lib/bun-stream-caps.ts +5 -6
  30. package/src/lib/redact.ts +13 -0
  31. package/src/oauth/index.ts +79 -12
  32. package/src/oauth/log.ts +3 -1
  33. package/src/oauth/store.ts +31 -8
  34. package/src/oauth/token-guardian.ts +130 -9
  35. package/src/providers/antigravity-models.ts +53 -24
  36. package/src/providers/codex-capacity.ts +303 -0
  37. package/src/providers/model-rename-migration.ts +147 -0
  38. package/src/providers/model-rename-startup.ts +29 -0
  39. package/src/providers/quota.ts +126 -16
  40. package/src/providers/registry.ts +262 -38
  41. package/src/responses/parser.ts +19 -12
  42. package/src/responses/spill-store.ts +14 -1
  43. package/src/responses/state.ts +108 -14
  44. package/src/router.ts +6 -0
  45. package/src/server/index.ts +9 -1
  46. package/src/server/management/logs-usage-routes.ts +1 -0
  47. package/src/server/management/oauth-account-routes.ts +8 -1
  48. package/src/server/relay.ts +10 -42
  49. package/src/server/request-log.ts +42 -1
  50. package/src/server/responses/compact.ts +16 -4
  51. package/src/server/responses/core.ts +217 -59
  52. package/src/server/responses/empty-completion-guard.ts +275 -0
  53. package/src/server/responses/encrypted-payload.ts +54 -39
  54. package/src/server/responses/fetch-helpers.ts +24 -3
  55. package/src/server/responses/ws-upstream.ts +318 -0
  56. package/src/server/sse-frame-buffer.ts +292 -0
  57. package/src/server/ws-bridge.ts +17 -11
  58. package/src/types.ts +13 -1
  59. package/src/usage/log.ts +24 -0
  60. package/src/usage/summary.ts +152 -2
  61. package/vendor/ocx-notch/win32-x64/ocx-notch.exe +0 -0
  62. package/gui/dist/assets/index-GgF1IpQa.js +0 -67
@@ -7,7 +7,8 @@ import {
7
7
  isCodexAccountGenerationLive,
8
8
  } from "./account-store";
9
9
  import { ConfigMutationLockError } from "../config";
10
- import { markAccountNeedsReauth } from "./account-runtime-state";
10
+ import { isAccountNeedsReauth, markAccountNeedsReauth } from "./account-runtime-state";
11
+ import { isCodexAccountPaused } from "./account-pause";
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";
@@ -38,6 +39,8 @@ export type CodexAuthContext =
38
39
  generation: number;
39
40
  accessToken: string;
40
41
  chatgptAccountId: string;
42
+ /** Exact account selector: suppresses Pool rotation and failover. */
43
+ fixedAccount?: boolean;
41
44
  /**
42
45
  * Set when this request was admitted through an active quota cooldown as
43
46
  * the account's single probe. Must be echoed into the upstream outcome so
@@ -57,6 +60,8 @@ export type CodexAuthContext =
57
60
  writerGeneration: number;
58
61
  accessToken: string;
59
62
  chatgptAccountId: string;
63
+ /** Exact account selector: suppresses Pool rotation and failover. */
64
+ fixedAccount?: boolean;
60
65
  /** See `pool.probeLeaseId`. */
61
66
  probeLeaseId?: string;
62
67
  quotaScope?: CodexQuotaScope;
@@ -100,8 +105,8 @@ export class CodexAuthContextError extends Error {
100
105
  }
101
106
 
102
107
  export class CodexPoolAuthenticationError extends Error {
103
- constructor() {
104
- super("OpenAI account pool has no usable account credential");
108
+ constructor(message = "OpenAI account pool has no usable account credential") {
109
+ super(message);
105
110
  this.name = "CodexPoolAuthenticationError";
106
111
  }
107
112
  }
@@ -154,22 +159,31 @@ export function cooldownAccountLabel(accountId: string): string {
154
159
  * as HTTP. The bare "cooling down" string left users with no route but commenting out the
155
160
  * injected `openai_base_url` in config.toml.
156
161
  */
157
- export function cooldownErrorMessage(err: CodexAccountCooldownError): string {
162
+ export function cooldownErrorMessage(err: CodexAccountCooldownError, accountSelector?: string): string {
158
163
  const until = new Date(err.cooldownUntil).toISOString();
159
164
  const scope = err.quotaScope === "spark"
160
165
  ? "Spark quota"
161
166
  : err.quotaScope === "shared"
162
167
  ? "shared native quota"
163
168
  : null;
164
- return `Selected Codex account (${cooldownAccountLabel(err.accountId)})${scope ? ` ${scope} is` : " is"} cooling down until ${until}`
165
- + ` (source: ${err.cooldownSource ?? "default"}).`
166
- + ` Run 'ocx account list openai' to find the id, then`
167
- + ` 'ocx account clear-cooldown openai <id>' to lift it, or switch accounts with 'ocx account use openai <id>'.`;
169
+ const selected = accountSelector
170
+ ? `Selected Codex account selector (${accountSelector})`
171
+ : `Selected Codex account (${cooldownAccountLabel(err.accountId)})`;
172
+ const recovery = accountSelector
173
+ ? " This request is pinned to that selector and will not switch accounts; choose another account-qualified model or retry later."
174
+ : " Run 'ocx account list openai' to find the id, then"
175
+ + " 'ocx account clear-cooldown openai <id>' to lift it, or switch accounts with 'ocx account use openai <id>'.";
176
+ return `${selected}${scope ? ` ${scope} is` : " is"} cooling down until ${until}`
177
+ + ` (source: ${err.cooldownSource ?? "default"}).${recovery}`;
168
178
  }
169
179
 
170
180
  /** HTTP form of {@link cooldownErrorMessage}, carrying Retry-After for well-behaved clients. */
171
- export function cooldownErrorResponse(err: CodexAccountCooldownError, now = Date.now()): Response {
172
- const res = formatErrorResponse(429, "rate_limit_error", cooldownErrorMessage(err));
181
+ export function cooldownErrorResponse(
182
+ err: CodexAccountCooldownError,
183
+ now = Date.now(),
184
+ accountSelector?: string,
185
+ ): Response {
186
+ const res = formatErrorResponse(429, "rate_limit_error", cooldownErrorMessage(err, accountSelector));
173
187
  const headers = new Headers(res.headers);
174
188
  headers.set("Retry-After", String(Math.max(1, Math.ceil((err.cooldownUntil - now) / 1000))));
175
189
  return new Response(res.body, { status: res.status, headers });
@@ -195,6 +209,8 @@ export function shouldMarkAccountNeedsReauthForCodexAuthFailure(cause: unknown):
195
209
 
196
210
  export interface ResolveCodexAuthContextOptions {
197
211
  excludeAccountId?: string;
212
+ /** Resolve exactly this account without consulting or mutating Pool selection. */
213
+ accountId?: string;
198
214
  /** Final native model selected for this request, used to select its quota group. */
199
215
  modelId?: string;
200
216
  }
@@ -206,14 +222,20 @@ export async function resolveCodexAuthContext(
206
222
  options: ResolveCodexAuthContextOptions = {},
207
223
  ): Promise<CodexAuthContext> {
208
224
  const writerGeneration = captureConfigGeneration();
209
- if (mode === "direct") {
225
+ const fixedAccountId = options.accountId;
226
+ if (fixedAccountId !== undefined && options.excludeAccountId !== undefined) {
227
+ throw new Error("Codex auth context cannot select and exclude an account simultaneously");
228
+ }
229
+ if (mode === "direct" && fixedAccountId === undefined) {
210
230
  if (!hasCallerCodexBearer(headers)) throw new CodexDirectAuthenticationError();
211
231
  return { kind: "main", accountId: null };
212
232
  }
213
233
  reconcileMainCodexAccountRuntimeState();
214
234
  const threadId = headers.get("x-codex-parent-thread-id");
215
235
  const quotaScope = codexQuotaScopeForModel(options.modelId);
216
- const resolution = options.excludeAccountId
236
+ const resolution = fixedAccountId !== undefined
237
+ ? { status: "selected" as const, accountId: fixedAccountId }
238
+ : options.excludeAccountId
217
239
  ? (() => {
218
240
  const accountId = pickAlternateCodexAccount(config, options.excludeAccountId!, Date.now(), quotaScope);
219
241
  return accountId
@@ -223,13 +245,28 @@ export async function resolveCodexAuthContext(
223
245
  : resolveCodexAccountForThreadDetailed(threadId, config, Date.now(), quotaScope);
224
246
  if (resolution.status === "expired") throw new CodexThreadAffinityExpiredError(resolution.accountId);
225
247
  let accountId = resolution.status === "selected" ? resolution.accountId : null;
226
- if (!accountId) throw new CodexPoolAuthenticationError();
248
+ if (!accountId) {
249
+ throw new CodexPoolAuthenticationError(
250
+ fixedAccountId !== undefined ? "Selected Codex account is unavailable" : undefined,
251
+ );
252
+ }
253
+ if (fixedAccountId !== undefined) {
254
+ if (isCodexAccountPaused(config, accountId)) {
255
+ throw new CodexPoolAuthenticationError("Selected Codex account is unavailable");
256
+ }
257
+ if (isAccountNeedsReauth(accountId)) {
258
+ throw new CodexPoolAuthenticationError("Selected Codex account needs reauthentication");
259
+ }
260
+ if (!isCodexAccountUsable(config, accountId)) {
261
+ throw new CodexPoolAuthenticationError("Selected Codex account is unavailable");
262
+ }
263
+ }
227
264
  // Lazy prime: if the selected account has no quota yet, the pool is likely
228
265
  // unprimed (dashboard never opened, or startup prime was blocked). Kick a
229
266
  // best-effort prime so the NEXT routing decision has real scores. This never
230
267
  // blocks the current request, and the helper's single-flight guard collapses
231
268
  // repeated triggers into one pass.
232
- if (!getAccountQuota(accountId)) {
269
+ if (fixedAccountId === undefined && !getAccountQuota(accountId)) {
233
270
  import("./auth-api")
234
271
  .then(({ primeCodexPoolQuotas }) => primeCodexPoolQuotas(config, "pre-route"))
235
272
  .catch(() => {});
@@ -244,6 +281,9 @@ export async function resolveCodexAuthContext(
244
281
  let probeLeaseId: string | undefined;
245
282
  let probeQuotaScope: CodexQuotaScope | undefined;
246
283
  if (cooldownUntil) {
284
+ if (fixedAccountId !== undefined) {
285
+ throw new CodexAccountCooldownError(accountId, cooldownUntil, cooldown?.cooldownSource, cooldown?.quotaScope);
286
+ }
247
287
  probeQuotaScope = cooldown?.quotaScope;
248
288
  probeLeaseId = probeQuotaScope
249
289
  ? tryAcquireCodexQuotaScopeProbeLease(accountId, probeQuotaScope) ?? undefined
@@ -260,7 +300,9 @@ export async function resolveCodexAuthContext(
260
300
  // Nothing will reach upstream, so give the probe back instead of burning it.
261
301
  if (probeLeaseId && probeQuotaScope) releaseCodexQuotaScopeProbeLease(accountId, probeQuotaScope, probeLeaseId);
262
302
  else if (probeLeaseId) releaseCodexQuotaProbeLease(accountId, probeLeaseId);
263
- throw new CodexPoolAuthenticationError();
303
+ throw new CodexPoolAuthenticationError(
304
+ fixedAccountId !== undefined ? "Selected Codex account is unavailable" : undefined,
305
+ );
264
306
  }
265
307
  return {
266
308
  kind: "main-pool",
@@ -268,6 +310,7 @@ export async function resolveCodexAuthContext(
268
310
  writerGeneration,
269
311
  accessToken: token.accessToken,
270
312
  chatgptAccountId: token.chatgptAccountId,
313
+ ...(fixedAccountId !== undefined ? { fixedAccount: true } : {}),
271
314
  ...(quotaScope ? { quotaScope } : {}),
272
315
  ...(probeLeaseId ? { probeLeaseId } : {}),
273
316
  ...(probeQuotaScope ? { probeQuotaScope } : {}),
@@ -283,6 +326,7 @@ export async function resolveCodexAuthContext(
283
326
  generation: token.generation,
284
327
  accessToken: token.accessToken,
285
328
  chatgptAccountId: token.chatgptAccountId,
329
+ ...(fixedAccountId !== undefined ? { fixedAccount: true } : {}),
286
330
  ...(quotaScope ? { quotaScope } : {}),
287
331
  ...(probeLeaseId ? { probeLeaseId } : {}),
288
332
  ...(probeQuotaScope ? { probeQuotaScope } : {}),
@@ -312,7 +356,8 @@ export function applyCodexAuthContextToProvider(
312
356
  ctx: CodexAuthContext,
313
357
  mode: CodexAccountMode | undefined,
314
358
  ): OcxRuntimeProviderConfig {
315
- if (mode !== "pool" || (ctx.kind !== "pool" && ctx.kind !== "main-pool") || provider.authMode !== "forward") return provider;
359
+ if ((ctx.kind !== "pool" && ctx.kind !== "main-pool") || provider.authMode !== "forward") return provider;
360
+ if (mode !== "pool" && ctx.fixedAccount !== true) return provider;
316
361
  return {
317
362
  ...provider,
318
363
  _codexAccountOverride: {
@@ -35,14 +35,18 @@ import { filterSupportedNativeSlugs } from "./parsing";
35
35
  import type { RawEntry } from "./parsing";
36
36
  import { readCurrentCatalogOrCache, unique } from "./bundled";
37
37
 
38
+ export const NATIVE_DAYBREAK_BLUE_MODEL = "gpt-daybreak-blue-latest";
39
+
38
40
  export const NATIVE_OPENAI_MODELS = [
39
41
  "gpt-5.5", "gpt-5.4", "gpt-5.4-mini", "gpt-5.3-codex-spark",
40
42
  "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna",
43
+ NATIVE_DAYBREAK_BLUE_MODEL,
41
44
  ];
42
45
 
43
46
  export const DOCUMENTED_NATIVE_OPENAI_ADDITIONS = [
44
47
  "gpt-5.3-codex-spark",
45
48
  "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna",
49
+ NATIVE_DAYBREAK_BLUE_MODEL,
46
50
  ];
47
51
 
48
52
  export const SUPPORTED_NATIVE_OPENAI_SLUGS = new Set(NATIVE_OPENAI_MODELS);
@@ -53,15 +57,18 @@ export function isUnsupportedOpenAiNativeSlug(slug: string): boolean {
53
57
  return /^(?:gpt|codex)-/.test(slug);
54
58
  }
55
59
 
56
- export const NATIVE_GPT56_CONTEXT_WINDOW = 372_000;
60
+ /** Measured native family contract: 922k input plus up to 128k output. */
61
+ export const NATIVE_GPT56_CONTEXT_WINDOW = 1_050_000;
62
+ export const NATIVE_GPT56_MAX_INPUT_TOKENS = 922_000;
57
63
 
58
- export const NATIVE_OPENAI_CONTEXT_OVERRIDES: Record<string, { contextWindow?: number; maxContextWindow?: number }> = {
64
+ export const NATIVE_OPENAI_CONTEXT_OVERRIDES: Record<string, { contextWindow?: number; maxContextWindow?: number; maxInputTokens?: number }> = {
59
65
  "gpt-5.5": { contextWindow: 272_000, maxContextWindow: 272_000 },
60
66
  "gpt-5.4": { contextWindow: 1_000_000, maxContextWindow: 1_000_000 },
61
67
  "gpt-5.3-codex-spark": { contextWindow: 100_000, maxContextWindow: 100_000 },
62
- "gpt-5.6-sol": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW },
63
- "gpt-5.6-terra": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW },
64
- "gpt-5.6-luna": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW },
68
+ "gpt-5.6-sol": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxInputTokens: NATIVE_GPT56_MAX_INPUT_TOKENS },
69
+ "gpt-5.6-terra": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxInputTokens: NATIVE_GPT56_MAX_INPUT_TOKENS },
70
+ "gpt-5.6-luna": { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxInputTokens: NATIVE_GPT56_MAX_INPUT_TOKENS },
71
+ [NATIVE_DAYBREAK_BLUE_MODEL]: { contextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxContextWindow: NATIVE_GPT56_CONTEXT_WINDOW, maxInputTokens: NATIVE_GPT56_MAX_INPUT_TOKENS },
65
72
  };
66
73
 
67
74
  export function nativeOpenAiContextWindow(slug: string): number | undefined {
@@ -71,6 +78,10 @@ export function nativeOpenAiContextWindow(slug: string): number | undefined {
71
78
  : undefined);
72
79
  }
73
80
 
81
+ export function nativeOpenAiMaxInputTokens(slug: string): number | undefined {
82
+ return NATIVE_OPENAI_CONTEXT_OVERRIDES[slug]?.maxInputTokens;
83
+ }
84
+
74
85
  export function nativeInputModalities(slug: string): string[] {
75
86
  const upstream = UPSTREAM_NATIVE_ENTRIES.get(slug);
76
87
  if (Array.isArray(upstream?.input_modalities) && upstream!.input_modalities!.length > 0) {
@@ -144,13 +155,24 @@ export function applyNativeVisibility(entries: RawEntry[], disabledNative: Set<s
144
155
  return entries;
145
156
  }
146
157
 
147
- export const UPSTREAM_NATIVE_ENTRIES: Map<string, RawEntry> = new Map(
148
- ((upstreamModelsSnapshot as unknown as { models?: RawEntry[] }).models ?? [])
149
- .filter(m => typeof m.slug === "string"
150
- && SUPPORTED_NATIVE_OPENAI_SLUGS.has(m.slug as string)
151
- && (m.slug as string).startsWith("gpt-5.6-"))
152
- .map(m => [m.slug as string, m]),
153
- );
158
+ export const UPSTREAM_NATIVE_ENTRIES: Map<string, RawEntry> = (() => {
159
+ const entries = new Map(
160
+ ((upstreamModelsSnapshot as unknown as { models?: RawEntry[] }).models ?? [])
161
+ .filter(m => typeof m.slug === "string"
162
+ && SUPPORTED_NATIVE_OPENAI_SLUGS.has(m.slug as string)
163
+ && (m.slug as string).startsWith("gpt-5.6-"))
164
+ .map(m => [m.slug as string, m] as const),
165
+ );
166
+ const sol = entries.get("gpt-5.6-sol");
167
+ if (sol) {
168
+ entries.set(NATIVE_DAYBREAK_BLUE_MODEL, {
169
+ ...JSON.parse(JSON.stringify(sol)) as RawEntry,
170
+ slug: NATIVE_DAYBREAK_BLUE_MODEL,
171
+ display_name: "GPT Daybreak Blue",
172
+ });
173
+ }
174
+ return entries;
175
+ })();
154
176
 
155
177
  export function upstreamNativeEntry(slug: string): RawEntry | null {
156
178
  const entry = UPSTREAM_NATIVE_ENTRIES.get(slug);
@@ -243,13 +243,20 @@ export function isNativeOpenAiEntry(entry: RawEntry): boolean {
243
243
  return typeof entry.slug === "string" && !entry.slug.includes("/");
244
244
  }
245
245
 
246
+ function nativeAutoCompactLimit(contextWindow: number, maxInputTokens?: number): number {
247
+ const ninetyPercent = Math.floor(contextWindow * 0.9);
248
+ return typeof maxInputTokens === "number" && maxInputTokens > 0
249
+ ? Math.min(ninetyPercent, maxInputTokens, contextWindow)
250
+ : ninetyPercent;
251
+ }
252
+
246
253
  export function applyNativeOpenAiContextOverride(entry: RawEntry): void {
247
254
  if (!isNativeOpenAiEntry(entry)) return;
248
255
  const override = NATIVE_OPENAI_CONTEXT_OVERRIDES[entry.slug as string];
249
256
  if (!override) return;
250
257
  if (typeof override.contextWindow === "number") {
251
258
  entry.context_window = override.contextWindow;
252
- entry.auto_compact_token_limit = Math.floor(override.contextWindow * 0.9);
259
+ entry.auto_compact_token_limit = nativeAutoCompactLimit(override.contextWindow, override.maxInputTokens);
253
260
  }
254
261
  if (typeof override.maxContextWindow === "number") {
255
262
  entry.max_context_window = override.maxContextWindow;
@@ -58,7 +58,7 @@ import { createAdmissionGate, ResourceAdmissionError, type AdmissionMetrics } fr
58
58
 
59
59
  import { JAWCODE_CATALOG_AUGMENT_PROVIDERS, catalogModelSlug, shouldExposeRoutedModel } from "./parsing";
60
60
  import type { CatalogModel } from "./parsing";
61
- import { disabledNativeSlugs, hasComboTargets, nativeInputModalities, nativeOpenAiContextWindow, nativeOpenAiSlugs, nativeParallelToolCalls, nativeReasoningEfforts } from "./metadata";
61
+ import { disabledNativeSlugs, hasComboTargets, nativeInputModalities, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, nativeOpenAiSlugs, nativeParallelToolCalls, nativeReasoningEfforts } from "./metadata";
62
62
  import { deriveComboCatalogModel, normalizedOpenAiApiSignature, openAiApiCollisionWarnings, replaceLastComboCatalogOmissions, warnUncataloguedComboOnce } from "./aggregation";
63
63
  import type { ComboCatalogOmission } from "./aggregation";
64
64
 
@@ -274,10 +274,15 @@ export function warnDroppedConfiguredIdsOnce(name: string, droppedConfiguredIds:
274
274
  }
275
275
 
276
276
  export function isGlm52ModelId(id: string): boolean {
277
- const normalized = id.toLowerCase();
277
+ const normalized = id.trim().toLowerCase();
278
278
  return normalized === "glm-5.2" || normalized === "glm-5.2[1m]";
279
279
  }
280
280
 
281
+ export function isGlm53ModelId(id: string): boolean {
282
+ const normalized = id.trim().toLowerCase();
283
+ return normalized === "glm-5.3" || normalized === "glm-5.3[1m]";
284
+ }
285
+
281
286
  function plainRecord(value: unknown): Record<string, unknown> | undefined {
282
287
  return value !== null && typeof value === "object" && !Array.isArray(value)
283
288
  ? value as Record<string, unknown>
@@ -358,6 +363,17 @@ function modelInputModalities(
358
363
  value === "text" || value === "image" || value === "audio"
359
364
  ));
360
365
  if (explicit && explicit.length > 0) return explicit;
366
+ const architecture = plainRecord(item.architecture);
367
+ const architectureModality = typeof architecture?.modality === "string"
368
+ ? normalizedMetadataString(architecture.modality, 64)
369
+ : undefined;
370
+ if (architectureModality?.includes("->")) {
371
+ const [rawInput = ""] = architectureModality.split("->");
372
+ const inferred = rawInput
373
+ .split("+")
374
+ .filter(value => value === "text" || value === "image" || value === "audio");
375
+ if (inferred.length > 0) return [...new Set(inferred)];
376
+ }
361
377
  if (capabilityRecord?.vision === false) return ["text"];
362
378
  if (capabilityRecord?.vision === true || capabilities?.some(value => value === "vision" || value === "image-input")) {
363
379
  return ["text", "image"];
@@ -385,9 +401,11 @@ export function catalogHintsFromModelsApiItem(providerName: string, item: Provid
385
401
  ? sanitizeCodexReasoningEfforts(listedReasoningEfforts)
386
402
  : typeof rawReasoningEfforts === "boolean"
387
403
  ? (rawReasoningEfforts
388
- ? ((providerName === "neuralwatt" || providerName === "zai") && isGlm52ModelId(item.id)
389
- ? ["low", "medium", "high", "xhigh", "max"]
390
- : ["low", "medium", "high", "xhigh"])
404
+ ? ((providerName === "neuralwatt" || providerName === "zai") && isGlm53ModelId(item.id)
405
+ ? ["low", "high", "max"]
406
+ : (providerName === "neuralwatt" || providerName === "zai") && isGlm52ModelId(item.id)
407
+ ? ["low", "medium", "high", "xhigh", "max"]
408
+ : ["low", "medium", "high", "xhigh"])
391
409
  : [])
392
410
  : undefined;
393
411
  const capabilities = modelCapabilities(item);
@@ -756,7 +774,7 @@ async function gatherRoutedModelsUncached(
756
774
  id: slug,
757
775
  owned_by: "openai",
758
776
  contextWindow,
759
- maxInputTokens: contextWindow,
777
+ maxInputTokens: nativeOpenAiMaxInputTokens(slug) ?? contextWindow,
760
778
  inputModalities: nativeInputModalities(slug),
761
779
  reasoningEfforts: nativeReasoningEfforts(slug),
762
780
  ...(nativeParallelToolCalls(slug) ? { parallelToolCalls: true } : {}),
@@ -2,7 +2,7 @@
2
2
  // Public surface preserved exactly; importers keep using "src/codex/catalog".
3
3
  export { isMediaGenerationModelId, shouldExposeRoutedModel, readCodexCatalogPath, readCatalog, normalizeRoutedCatalogEntry, catalogModelSlug, filterSupportedNativeSlugs, catalogModelSupportsReasoningSummaries } from "./catalog/parsing";
4
4
  export type { CatalogModel, MultiAgentMode } from "./catalog/parsing";
5
- export { NATIVE_OPENAI_MODELS, nativeOpenAiContextWindow, disabledNativeSlugs, visibleNativeSlugs, desktopVisibleNativeSlugs, nativeModelRows, applyNativeVisibility, upstreamNativeEntry, nativeOpenAiSlugs, listCatalogNativeSlugs, nativeReasoningEfforts, nativeDefaultReasoningEffort } from "./catalog/metadata";
5
+ export { NATIVE_DAYBREAK_BLUE_MODEL, NATIVE_GPT56_CONTEXT_WINDOW, NATIVE_GPT56_MAX_INPUT_TOKENS, NATIVE_OPENAI_MODELS, nativeOpenAiContextWindow, nativeOpenAiMaxInputTokens, disabledNativeSlugs, visibleNativeSlugs, desktopVisibleNativeSlugs, nativeModelRows, applyNativeVisibility, upstreamNativeEntry, nativeOpenAiSlugs, listCatalogNativeSlugs, nativeReasoningEfforts, nativeDefaultReasoningEffort } from "./catalog/metadata";
6
6
  export { isSpawnableCodexCandidate, codexExecInvocation, loadBundledCodexCatalog, materializeBundledCodexCatalog, loadCatalogTemplate } from "./catalog/bundled";
7
7
  export { nativeEffortClamp, shouldApplyNativeEffortClamp, catalogModelEfforts, codexSupportedReasoningEfforts, clampedDefaultEffort, clampEntryToCodexSupportedEfforts, clampCatalogModelsToCodexSupport } from "./catalog/effort";
8
8
  export { applyProviderConfigHints, isDatedVariantId, filterCatalogVisibleModels, gatherRoutedModels, clearGatherRoutedModelsInflight, augmentRoutedModelsWithRegistryOpenAiApiRows, augmentRoutedModelsWithJawcodeMetadata } from "./catalog/provider-fetch";
@@ -19,6 +19,10 @@ const MAX_STICKY_LIMIT = 100;
19
19
  const DEFAULT_STRATEGY: OcxAccountPoolRotationStrategy = "quota";
20
20
  const VALID_STRATEGIES = new Set<OcxAccountPoolRotationStrategy>(["quota", "round-robin", "fill-first"]);
21
21
 
22
+ export const DEFAULT_ACCOUNT_PRIORITY = 0;
23
+ export const MIN_ACCOUNT_PRIORITY = -100;
24
+ export const MAX_ACCOUNT_PRIORITY = 100;
25
+
22
26
  /** Strict parse for management APIs — returns null instead of defaulting. */
23
27
  export function parseAccountPoolStrategy(raw: unknown): OcxAccountPoolRotationStrategy | null {
24
28
  if (typeof raw === "string" && VALID_STRATEGIES.has(raw as OcxAccountPoolRotationStrategy)) {
@@ -43,6 +47,49 @@ export function normalizeAccountPoolStickyLimit(raw: unknown): number {
43
47
  return parseAccountPoolStickyLimit(raw) ?? DEFAULT_STICKY_LIMIT;
44
48
  }
45
49
 
50
+ export function parseAccountPriority(raw: unknown): number | null {
51
+ return typeof raw === "number"
52
+ && Number.isInteger(raw)
53
+ && raw >= MIN_ACCOUNT_PRIORITY
54
+ && raw <= MAX_ACCOUNT_PRIORITY
55
+ ? raw
56
+ : null;
57
+ }
58
+
59
+ export function normalizeAccountPriority(raw: unknown): number {
60
+ return parseAccountPriority(raw) ?? DEFAULT_ACCOUNT_PRIORITY;
61
+ }
62
+
63
+ /**
64
+ * Narrow eligible ids to the highest configured tier that still has quota headroom.
65
+ * Equal-priority pools and fully drained pools preserve the caller's exact list/order.
66
+ */
67
+ export function selectPriorityTier(
68
+ ids: readonly string[],
69
+ priorityOf: (id: string) => number,
70
+ hasHeadroom: (id: string) => boolean,
71
+ pinnedId?: string,
72
+ ): readonly string[] {
73
+ if (ids.length <= 1) return ids;
74
+ const priorities = ids.map(priorityOf);
75
+ const firstPriority = priorities[0]!;
76
+ if (priorities.every(priority => priority === firstPriority)) return ids;
77
+
78
+ let ceiling = Number.POSITIVE_INFINITY;
79
+ if (pinnedId !== undefined) {
80
+ const pinnedIndex = ids.indexOf(pinnedId);
81
+ if (pinnedIndex >= 0 && hasHeadroom(pinnedId)) ceiling = priorities[pinnedIndex]!;
82
+ }
83
+
84
+ const tiers = [...new Set(priorities)].sort((a, b) => b - a);
85
+ for (const tier of tiers) {
86
+ if (tier > ceiling) continue;
87
+ const members = ids.filter((_, index) => priorities[index] === tier);
88
+ if (members.some(hasHeadroom)) return members;
89
+ }
90
+ return ids;
91
+ }
92
+
46
93
  function getOrCreateState(poolKey: string): SelectionState {
47
94
  let state = selectionState.get(poolKey);
48
95
  if (!state) {
@@ -60,7 +107,7 @@ function cloneSelectionState(state: SelectionState): SelectionState {
60
107
  };
61
108
  }
62
109
 
63
- function smoothWeightedIndex(ids: string[], state: SelectionState): number {
110
+ function smoothWeightedIndex(ids: readonly string[], state: SelectionState): number {
64
111
  let best = -1;
65
112
  let bestScore = Number.NEGATIVE_INFINITY;
66
113
  let total = 0;
@@ -87,7 +134,7 @@ function smoothWeightedIndex(ids: string[], state: SelectionState): number {
87
134
  * either the live map entry or a scratch/clone for dry-run peek.
88
135
  */
89
136
  function pickRoundRobinFromState(
90
- eligibleIds: string[],
137
+ eligibleIds: readonly string[],
91
138
  stickyLimit: number,
92
139
  state: SelectionState,
93
140
  commitSticky: boolean,
@@ -118,7 +165,7 @@ function pickRoundRobinFromState(
118
165
 
119
166
  export function pickRoundRobinAccount(
120
167
  poolKey: string,
121
- eligibleIds: string[],
168
+ eligibleIds: readonly string[],
122
169
  stickyLimit: number,
123
170
  ): string | null {
124
171
  return pickRoundRobinFromState(eligibleIds, stickyLimit, getOrCreateState(poolKey), true);
@@ -130,7 +177,7 @@ export function pickRoundRobinAccount(
130
177
  */
131
178
  export function peekRoundRobinAccount(
132
179
  poolKey: string,
133
- eligibleIds: string[],
180
+ eligibleIds: readonly string[],
134
181
  stickyLimit: number,
135
182
  ): string | null {
136
183
  const live = selectionState.get(poolKey);