@bitkyc08/opencodex 2.73.0 → 2.74.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.
Files changed (58) hide show
  1. package/gui/dist/assets/{App-mHGa7dE1.js → App-B00Yq11u.js} +15 -15
  2. package/gui/dist/assets/App-KLz1TAXi.css +1 -0
  3. package/gui/dist/assets/{Tray-BxY_RYZg.js → Tray-D3Ij1Cvf.js} +1 -1
  4. package/gui/dist/assets/{index-ur_BPgK1.js → index-BYQQfBGK.js} +12 -12
  5. package/gui/dist/assets/index-DBT3Q31Y.css +1 -0
  6. package/gui/dist/assets/{quota-summary-D1Vq7N9H.js → quota-summary-yPtcEhGI.js} +1 -1
  7. package/gui/dist/index.html +2 -2
  8. package/package.json +1 -1
  9. package/src/adapters/kiro/stream.ts +49 -28
  10. package/src/adapters/openai-chat.ts +3 -5
  11. package/src/adapters/openai-responses/passthrough.ts +2 -5
  12. package/src/codex/auth-api/account-list.ts +14 -2
  13. package/src/codex/auth-api/main-account-probe.ts +2 -0
  14. package/src/codex/auth-api/pool-quota-probe.ts +4 -0
  15. package/src/codex/autostart-health.ts +15 -3
  16. package/src/codex/catalog/discovered-natives.ts +166 -0
  17. package/src/codex/catalog/metadata.ts +25 -6
  18. package/src/codex/catalog/model-hints.ts +8 -0
  19. package/src/codex/catalog/model-visibility.ts +4 -0
  20. package/src/codex/catalog/native-models.ts +46 -19
  21. package/src/codex/catalog-auto-refresh-sources.ts +71 -0
  22. package/src/codex/catalog-auto-refresh.ts +42 -6
  23. package/src/codex/catalog-refresh-status.ts +4 -0
  24. package/src/codex/credits.ts +86 -0
  25. package/src/codex/main-account-hard-lock.ts +2 -2
  26. package/src/codex/model-entitlements.ts +122 -1
  27. package/src/codex/quota-types.ts +1 -0
  28. package/src/codex/quota.ts +4 -3
  29. package/src/codex/shim-probe.ts +5 -1
  30. package/src/codex/shim-templates.ts +10 -4
  31. package/src/config/derived-registries.ts +4 -2
  32. package/src/config/diagnostics.ts +9 -0
  33. package/src/config/feature-flags.ts +6 -4
  34. package/src/config/schema/config-schema.ts +1 -0
  35. package/src/config/schema/leaf-validators.ts +1 -0
  36. package/src/generated/compatibility-version.json +67 -43
  37. package/src/generated/model-metadata.ts +1 -1
  38. package/src/lib/test-home-guard.ts +47 -3
  39. package/src/providers/fastwire.ts +22 -0
  40. package/src/providers/model-rename-startup.ts +7 -5
  41. package/src/providers/registry/entries-core.ts +13 -6
  42. package/src/providers/registry/entries-extended.ts +14 -8
  43. package/src/providers/registry/model-seeds.ts +14 -1
  44. package/src/providers/stale-model-roster-migration.ts +101 -0
  45. package/src/providers/xai-fast-model.ts +85 -0
  46. package/src/server/auth-cors.ts +1 -0
  47. package/src/server/background-lifecycle.ts +2 -2
  48. package/src/server/management/config-routes.ts +13 -1
  49. package/src/server/management/oauth-account-routes.ts +8 -2
  50. package/src/server/responses/core-normalize.ts +3 -0
  51. package/src/server/startup-health-cache.ts +7 -2
  52. package/src/service/desktop-startup.ts +93 -0
  53. package/src/types/config.ts +12 -11
  54. package/src/types/provider.ts +3 -1
  55. package/src/types/request.ts +2 -0
  56. package/src/usage/log.ts +3 -1
  57. package/gui/dist/assets/App-DCBismRi.css +0 -1
  58. package/gui/dist/assets/index-pWYJt_vL.css +0 -1
@@ -37,6 +37,10 @@ import {
37
37
  MINIMAX_MODEL_CONTEXT_WINDOWS,
38
38
  MINIMAX_M3_REASONING_EFFORTS,
39
39
  MINIMAX_M3_REASONING_EFFORT_MAP,
40
+ MINIMAX_M31_FLASH_PREVIEW,
41
+ MINIMAX_M31_REASONING_EFFORTS,
42
+ MINIMAX_M31_DEFAULT_REASONING_EFFORT,
43
+ MINIMAX_REASONING_SPLIT_MODELS,
40
44
  THINKING_TOGGLE_EFFORTS,
41
45
  THINKING_TOGGLE_MAP,
42
46
  ZHIPU_BIGMODEL_MODELS,
@@ -1020,22 +1024,24 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [
1020
1024
  id: "minimax", label: "MiniMax — Coding Plan", baseUrl: "https://api.minimax.io/v1", adapter: "openai-chat", authKind: "key",
1021
1025
  dashboardUrl: "https://platform.minimax.io", defaultModel: "MiniMax-M3", models: MINIMAX_MODELS,
1022
1026
  modelContextWindows: MINIMAX_MODEL_CONTEXT_WINDOWS,
1023
- modelReasoningEfforts: { "MiniMax-M3": MINIMAX_M3_REASONING_EFFORTS },
1024
- modelDefaultReasoningEfforts: { "MiniMax-M3": "medium" },
1027
+ modelReasoningEfforts: { "MiniMax-M3": MINIMAX_M3_REASONING_EFFORTS, [MINIMAX_M31_FLASH_PREVIEW]: MINIMAX_M31_REASONING_EFFORTS },
1028
+ modelDefaultReasoningEfforts: { "MiniMax-M3": "medium", [MINIMAX_M31_FLASH_PREVIEW]: MINIMAX_M31_DEFAULT_REASONING_EFFORT },
1025
1029
  modelReasoningEffortMap: { "MiniMax-M3": MINIMAX_M3_REASONING_EFFORT_MAP },
1026
1030
  preserveReasoningContentModels: MINIMAX_MODELS,
1027
1031
  // MiniMax-M3 low effort maps to thinking disabled, so a legitimate tool
1028
1032
  // round can carry no reasoning at all; only replay real recorded text,
1029
1033
  // never a fabricated placeholder (chatgpt-codex-connector P2 on #1205).
1030
1034
  requiresReasoningPlaceholderModels: [],
1031
- reasoningSplitModels: MINIMAX_MODELS,
1035
+ // M3.1-Flash-Preview ignores reasoning_split and always answers with reasoning_content,
1036
+ // so it stays off the split/details lists and replays as a reasoning_content string.
1037
+ reasoningSplitModels: MINIMAX_REASONING_SPLIT_MODELS,
1032
1038
  // With reasoning_split the upstream returns thinking as a structured
1033
1039
  // reasoning_details array (cumulative text snapshots per stream chunk) and
1034
1040
  // requires that array back verbatim on the next turn — a reasoning_content
1035
1041
  // string replay is the native-format pass-back the docs say is unsupported.
1036
1042
  // Evidence: platform.minimax.io/docs/guides/text-m3-function-call and
1037
1043
  // /docs/api-reference/text-openai-api (verified 2026-09-01).
1038
- reasoningDetailsModels: MINIMAX_MODELS,
1044
+ reasoningDetailsModels: MINIMAX_REASONING_SPLIT_MODELS,
1039
1045
  thinkingToggleModels: ["MiniMax-M3"],
1040
1046
  jawcodeBundle: "minimax", metadataModelIdNormalize: "case-insensitive", note: "Subscription Key or API Key",
1041
1047
  },
@@ -1043,13 +1049,13 @@ export const PROVIDER_REGISTRY_EXTENDED: readonly ProviderRegistryEntry[] = [
1043
1049
  id: "minimax-cn", label: "MiniMax — Coding Plan (CN)", baseUrl: "https://api.minimaxi.com/v1", adapter: "openai-chat", authKind: "key",
1044
1050
  dashboardUrl: "https://platform.minimaxi.com", defaultModel: "MiniMax-M3", models: MINIMAX_MODELS,
1045
1051
  modelContextWindows: MINIMAX_MODEL_CONTEXT_WINDOWS,
1046
- modelReasoningEfforts: { "MiniMax-M3": MINIMAX_M3_REASONING_EFFORTS },
1047
- modelDefaultReasoningEfforts: { "MiniMax-M3": "medium" },
1052
+ modelReasoningEfforts: { "MiniMax-M3": MINIMAX_M3_REASONING_EFFORTS, [MINIMAX_M31_FLASH_PREVIEW]: MINIMAX_M31_REASONING_EFFORTS },
1053
+ modelDefaultReasoningEfforts: { "MiniMax-M3": "medium", [MINIMAX_M31_FLASH_PREVIEW]: MINIMAX_M31_DEFAULT_REASONING_EFFORT },
1048
1054
  modelReasoningEffortMap: { "MiniMax-M3": MINIMAX_M3_REASONING_EFFORT_MAP },
1049
1055
  preserveReasoningContentModels: MINIMAX_MODELS,
1050
1056
  requiresReasoningPlaceholderModels: [],
1051
- reasoningSplitModels: MINIMAX_MODELS,
1052
- reasoningDetailsModels: MINIMAX_MODELS,
1057
+ reasoningSplitModels: MINIMAX_REASONING_SPLIT_MODELS,
1058
+ reasoningDetailsModels: MINIMAX_REASONING_SPLIT_MODELS,
1053
1059
  thinkingToggleModels: ["MiniMax-M3"],
1054
1060
  jawcodeBundle: "minimax", metadataModelIdNormalize: "case-insensitive", note: "中国区 Subscription Key",
1055
1061
  },
@@ -123,17 +123,30 @@ export const ZAI_GLM_5X_REASONING_EFFORTS: Record<string, string[]> = {
123
123
  };
124
124
  // 260710 MiniMax models and context windows: Tier-2 evidence in
125
125
  // devlog/_plan/260710_provider_hardening/002_research_cn.md.
126
+ // 260930 MiniMax-M3.1-Flash-Preview: Token Plan / MiniMax Code only, 1M context, thinking
127
+ // always on (effort none or thinking disabled answers 400 code 2013), omitted effort = max.
128
+ // It returns thinking as reasoning_content and ignores reasoning_split. The live /v1/models
129
+ // roster does not list it yet. Evidence: devlog/_plan/260930_minimax_m31_flash_preview/.
130
+ export const MINIMAX_M31_FLASH_PREVIEW = "MiniMax-M3.1-Flash-Preview";
126
131
  export const MINIMAX_MODELS = [
132
+ MINIMAX_M31_FLASH_PREVIEW,
127
133
  "MiniMax-M3",
128
134
  "MiniMax-M2.7", "MiniMax-M2.7-highspeed",
129
135
  "MiniMax-M2.5", "MiniMax-M2.5-highspeed",
130
136
  "MiniMax-M2.1", "MiniMax-M2.1-highspeed",
131
137
  "MiniMax-M2",
132
138
  ];
139
+ /** The eight-id roster every MiniMax preset seeded from 2026-07-10 until the preview landed. */
140
+ export const MINIMAX_MODELS_BEFORE_M31 = MINIMAX_MODELS.filter(id => id !== MINIMAX_M31_FLASH_PREVIEW);
141
+ /** Models that honour reasoning_split and answer with structured reasoning_details. */
142
+ export const MINIMAX_REASONING_SPLIT_MODELS = MINIMAX_MODELS_BEFORE_M31;
133
143
  export const MINIMAX_MODEL_CONTEXT_WINDOWS: Record<string, number> = Object.fromEntries(
134
- MINIMAX_MODELS.map(id => [id, id === "MiniMax-M3" ? 1_000_000 : 204_800]),
144
+ MINIMAX_MODELS.map(id => [id, id === "MiniMax-M3" || id === MINIMAX_M31_FLASH_PREVIEW ? 1_000_000 : 204_800]),
135
145
  );
136
146
  export const MINIMAX_M3_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
147
+ /** Identity efforts on the wire; no map, so none omits the field instead of disabling thinking. */
148
+ export const MINIMAX_M31_REASONING_EFFORTS = ["low", "medium", "high", "xhigh", "max"];
149
+ export const MINIMAX_M31_DEFAULT_REASONING_EFFORT = "max";
137
150
  export const MINIMAX_M3_REASONING_EFFORT_MAP: Record<string, string> = {
138
151
  none: "disabled",
139
152
  minimal: "disabled",
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Add registry models to a saved roster that is still the previous registry seed.
3
+ *
4
+ * `enrichProviderFromRegistry` copies the registry's `models` only when a row has none, so
5
+ * every config saved while an older roster was current keeps that roster forever. That is
6
+ * the right posture for a hand-edited list and the wrong one for an untouched seed: a model
7
+ * the vendor added never reaches the install, and when the vendor's live `/models` roster
8
+ * does not list it either, nothing else can surface it.
9
+ *
10
+ * This rewrites one thing: a saved `models` list that is still byte-for-byte the roster
11
+ * this file names as `from`, on a provider that still carries the registry adapter, and
12
+ * only while the registry currently seeds `to`. A list the user changed does not match and
13
+ * is left alone. For each added id it also fills a per-model context window and default
14
+ * effort, but only inside a container the row already has: enrichment fills those records
15
+ * all-or-nothing, so creating one here would hide the rest of the registry seed.
16
+ * Same shape and restraint as `stale-context-window-migration`.
17
+ */
18
+ import { PROVIDER_REGISTRY } from "./registry";
19
+ import {
20
+ MINIMAX_M31_DEFAULT_REASONING_EFFORT,
21
+ MINIMAX_M31_FLASH_PREVIEW,
22
+ MINIMAX_MODELS,
23
+ MINIMAX_MODELS_BEFORE_M31,
24
+ MINIMAX_MODEL_CONTEXT_WINDOWS,
25
+ } from "./registry/model-seeds";
26
+ import type { OcxConfig } from "../types";
27
+
28
+ export interface StaleModelRoster {
29
+ /** Registry provider id whose saved rows may carry the old roster. */
30
+ provider: string;
31
+ /** The exact saved roster this migration may replace, and nothing else. */
32
+ from: readonly string[];
33
+ to: readonly string[];
34
+ /** Per-model seed values for the added ids, written only into containers that exist. */
35
+ contextWindows?: Readonly<Record<string, number>>;
36
+ defaultReasoningEfforts?: Readonly<Record<string, string>>;
37
+ }
38
+
39
+ export interface StaleModelRosterProjection {
40
+ config: OcxConfig;
41
+ changed: boolean;
42
+ warnings: string[];
43
+ }
44
+
45
+ /**
46
+ * MiniMax-M3.1-Flash-Preview (2026-09-30). MiniMax's /v1/models roster does not list the
47
+ * preview, so live discovery cannot add it; the catalog keeps it only when it is configured.
48
+ */
49
+ export const STALE_MODEL_ROSTERS: readonly StaleModelRoster[] = ["minimax", "minimax-cn"].map(provider => ({
50
+ provider,
51
+ from: MINIMAX_MODELS_BEFORE_M31,
52
+ to: MINIMAX_MODELS,
53
+ contextWindows: { [MINIMAX_M31_FLASH_PREVIEW]: MINIMAX_MODEL_CONTEXT_WINDOWS[MINIMAX_M31_FLASH_PREVIEW]! },
54
+ defaultReasoningEfforts: { [MINIMAX_M31_FLASH_PREVIEW]: MINIMAX_M31_DEFAULT_REASONING_EFFORT },
55
+ }));
56
+
57
+ function sameList(value: unknown, expected: readonly string[]): boolean {
58
+ return Array.isArray(value)
59
+ && value.length === expected.length
60
+ && value.every((item, index) => item === expected[index]);
61
+ }
62
+
63
+ function registrySeeds(provider: string, adapter: unknown, roster: readonly string[]): boolean {
64
+ const entry = PROVIDER_REGISTRY.find(row => row.id === provider);
65
+ return entry !== undefined && entry.adapter === adapter && sameList(entry.models, roster);
66
+ }
67
+
68
+ function fillExisting<T>(container: Record<string, T> | undefined, values: Readonly<Record<string, T>> | undefined, ids: readonly string[]): void {
69
+ if (!container || !values) return;
70
+ for (const id of ids) {
71
+ const value = values[id];
72
+ if (value !== undefined && !Object.prototype.hasOwnProperty.call(container, id)) container[id] = value;
73
+ }
74
+ }
75
+
76
+ /** Pure projection. The caller decides whether to persist. */
77
+ export function projectStaleModelRosters(
78
+ config: OcxConfig,
79
+ entries: readonly StaleModelRoster[] = STALE_MODEL_ROSTERS,
80
+ ): StaleModelRosterProjection {
81
+ const warnings: string[] = [];
82
+ let changed = false;
83
+
84
+ for (const entry of entries) {
85
+ const prov = config.providers?.[entry.provider];
86
+ if (!prov) continue;
87
+ if (!registrySeeds(entry.provider, prov.adapter, entry.to)) continue;
88
+ if (!sameList(prov.models, entry.from)) continue;
89
+ const added = entry.to.filter(id => !entry.from.includes(id));
90
+ prov.models = [...entry.to];
91
+ fillExisting(prov.modelContextWindows, entry.contextWindows, added);
92
+ fillExisting(prov.modelDefaultReasoningEfforts, entry.defaultReasoningEfforts, added);
93
+ changed = true;
94
+ warnings.push(
95
+ `added ${added.join(", ")} to the saved "${entry.provider}" model list, which was still `
96
+ + "the previous registry seed.",
97
+ );
98
+ }
99
+
100
+ return { config, changed, warnings };
101
+ }
@@ -0,0 +1,85 @@
1
+ import type { FastWire, OcxParsedRequest, OcxProviderConfig } from "../types";
2
+
3
+ /**
4
+ * Grok OAuth Fast is a serving lane, not a tier.
5
+ *
6
+ * The Grok OAuth gateway lists `grok-4.7` and `grok-4.7-build-fast` as two models, but they are one model
7
+ * on two lanes (same effort ladder, image input, 500k limit and advertised defaults). Measured live on
8
+ * 2026-09-30: build-fast is 1.5-1.7x faster end to end, while `service_tier: "priority"` on grok-4.7 bought
9
+ * no measurable speed and cost ~5.9x the ticks per output token (build-fast costs ~2x).
10
+ * Evidence: devlog/_plan/260930_grok47_build_unify/010_probe-evidence.md.
11
+ *
12
+ * So on the OAuth lane only, a Fast grok-4.7 request is SERIALIZED as the build-fast id and sends no tier.
13
+ * The logical id stays grok-4.7 everywhere else (parsed.modelId, route, policy, usage attempt), so effort,
14
+ * sampling strips and operator overrides keep resolving against grok-4.7. Key auth never reaches this
15
+ * (build-fast is not on the public API) and keeps priority processing.
16
+ */
17
+ export const XAI_OAUTH_FAST_MODELS: Readonly<Record<string, string>> = Object.freeze({
18
+ "grok-4.7": "grok-4.7-build-fast",
19
+ });
20
+
21
+ /** Serving-lane ids that must not be published as rows of their own. */
22
+ export const XAI_OAUTH_FAST_VARIANT_IDS: ReadonlySet<string> = new Set(Object.values(XAI_OAUTH_FAST_MODELS));
23
+
24
+ export function xaiOauthFastModel(
25
+ providerName: string,
26
+ provider: Pick<OcxProviderConfig, "authMode">,
27
+ modelId: string,
28
+ ): string | undefined {
29
+ // Same predicate the transport uses to select the Grok CLI gateway (xai-transport.ts).
30
+ if (providerName !== "xai" || provider.authMode !== "oauth") return undefined;
31
+ return Object.hasOwn(XAI_OAUTH_FAST_MODELS, modelId) ? XAI_OAUTH_FAST_MODELS[modelId] : undefined;
32
+ }
33
+
34
+ function modelVariantFastWire(variant: string): FastWire {
35
+ return { kind: "model-variant", canonicalToWire: { priority: variant }, foreignCallerTiers: "drop" };
36
+ }
37
+
38
+ function isRecord(value: unknown): value is Record<string, unknown> {
39
+ return !!value && typeof value === "object" && !Array.isArray(value);
40
+ }
41
+
42
+ /**
43
+ * Settle the serialized model for the final route. Runs after decideTier on every (re)normalization, so it
44
+ * also undoes an override a previous route in the same request installed: core-normalize only rewrites
45
+ * `raw.model` when the route id differs from parsed.modelId, and a same-id fallback (xai/grok-4.7 on key
46
+ * auth) would otherwise inherit the variant.
47
+ *
48
+ * The caller's Fast intent (options.serviceTier, raw.service_tier) is left in place for such re-runs; the
49
+ * `drop` decision is what keeps the tier off this wire (canonical-forward.ts, openai-chat.ts).
50
+ */
51
+ export function applyXaiOauthFastModel(
52
+ parsed: OcxParsedRequest,
53
+ route: { providerName: string; provider: Pick<OcxProviderConfig, "authMode" | "fastWire">; modelId: string },
54
+ logCtx?: { wireModel?: string },
55
+ ): void {
56
+ const raw = isRecord(parsed._rawBody) ? parsed._rawBody : undefined;
57
+ const previous = parsed._wireModelOverride;
58
+ // The lane switch replaces the registry's service-tier Fast only. The xai registry entry declares no
59
+ // FastWire, so a provider-level `fastWire` is always the operator's own (service-tier.ts reads it
60
+ // first): that decision carries a wire value they verified, and it is sent unchanged.
61
+ const variant = parsed.options.tierDecision?.kind === "set" && route.provider.fastWire === undefined
62
+ ? xaiOauthFastModel(route.providerName, route.provider, route.modelId)
63
+ : undefined;
64
+ if (!variant) {
65
+ if (previous === undefined) return;
66
+ delete parsed._wireModelOverride;
67
+ if (raw && raw.model === previous) raw.model = route.modelId;
68
+ if (logCtx?.wireModel === previous) delete logCtx.wireModel;
69
+ return;
70
+ }
71
+ const observation = parsed.options.tierObservation;
72
+ if (observation) {
73
+ // The lane switch is the Fast wire. A service_tier echo says nothing about it, so it can neither
74
+ // confirm nor deny Fast here (and never unlocks priority pricing, which needs confirmation).
75
+ parsed.options.tierObservation = {
76
+ ...observation,
77
+ fastWire: modelVariantFastWire(variant),
78
+ responseTierAuthoritative: false,
79
+ };
80
+ }
81
+ parsed.options.tierDecision = { kind: "drop" };
82
+ parsed._wireModelOverride = variant;
83
+ if (raw) raw.model = variant;
84
+ if (logCtx) logCtx.wireModel = variant;
85
+ }
@@ -1258,6 +1258,7 @@ export function safeConfigDTO(config: OcxConfig): unknown {
1258
1258
  // The GUI's browser-open toggle reads and writes this; absent means the
1259
1259
  // historical auto-open behavior.
1260
1260
  oauthOpenBrowser: config.oauthOpenBrowser !== false,
1261
+ showCodexCredits: config.showCodexCredits === true,
1261
1262
  providers,
1262
1263
  };
1263
1264
  }
@@ -79,8 +79,8 @@ function startProcessLoops(applyPolicy: PolicyApply): ProcessLoops {
79
79
  .catch(() => {
80
80
  // The next tick adopts it.
81
81
  });
82
- // Opt-in: the tick is a no-op unless catalogAutoRefresh.enabled is true, and the
83
- // interval is unref'd, so a default install pays one dormant timer. The scheduler
82
+ // Default-on: explicit false or a zero cadence makes ticks dormant. Both startup
83
+ // and interval timers are unref'd. The scheduler
84
84
  // module keeps every heavy import inside its tick, so naming it statically here
85
85
  // costs a module record and nothing else.
86
86
  startCatalogAutoRefresh();
@@ -367,6 +367,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
367
367
  // Absent means the historical auto-open, so the GUI can render the toggle
368
368
  // without having to know that `undefined` and `true` mean the same thing.
369
369
  oauthOpenBrowser: config.oauthOpenBrowser !== false,
370
+ showCodexCredits: config.showCodexCredits === true,
370
371
  // Absent means off (today's Design B injection), so the GUI/CLI render a plain switch.
371
372
  codexDesktopAuthless: config.codexDesktopAuthless === true,
372
373
  // Absent keeps Design B remote compaction; true selects the dedicated provider identity.
@@ -462,6 +463,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
462
463
  codexAccountPickerEnabled?: unknown;
463
464
  codexQuotaAutoRefresh?: unknown;
464
465
  oauthOpenBrowser?: unknown;
466
+ showCodexCredits?: unknown;
465
467
  ultraFastTier?: unknown;
466
468
  fastRows?: unknown;
467
469
  codexMainAccountHardLock?: unknown;
@@ -477,6 +479,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
477
479
  && body.codexAccountPickerEnabled === undefined
478
480
  && body.codexQuotaAutoRefresh === undefined
479
481
  && body.oauthOpenBrowser === undefined
482
+ && body.showCodexCredits === undefined
480
483
  && body.ultraFastTier === undefined
481
484
  && body.fastRows === undefined
482
485
  && body.codexMainAccountHardLock === undefined
@@ -485,11 +488,14 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
485
488
  && body.compactionRouting === undefined
486
489
  && body.compactionRecovery === undefined
487
490
  && body.memoryModels === undefined) {
488
- return jsonResponse({ error: "provide codexAutoStart, streamMode, appOwnedMemoryBudgetMb, codexAccountPickerEnabled, codexQuotaAutoRefresh, oauthOpenBrowser, ultraFastTier, fastRows, codexMainAccountHardLock, codexDesktopAuthless, codexClientCompaction, compactionRouting, compactionRecovery, or memoryModels" }, 400);
491
+ return jsonResponse({ error: "provide codexAutoStart, streamMode, appOwnedMemoryBudgetMb, codexAccountPickerEnabled, codexQuotaAutoRefresh, oauthOpenBrowser, showCodexCredits, ultraFastTier, fastRows, codexMainAccountHardLock, codexDesktopAuthless, codexClientCompaction, compactionRouting, compactionRecovery, or memoryModels" }, 400);
489
492
  }
490
493
  if (body.codexAutoStart !== undefined && typeof body.codexAutoStart !== "boolean") {
491
494
  return jsonResponse({ error: "codexAutoStart boolean is required" }, 400);
492
495
  }
496
+ if (body.showCodexCredits !== undefined && typeof body.showCodexCredits !== "boolean") {
497
+ return jsonResponse({ error: "showCodexCredits boolean is required" }, 400);
498
+ }
493
499
  if (body.oauthOpenBrowser !== undefined && typeof body.oauthOpenBrowser !== "boolean") {
494
500
  return jsonResponse({ error: "oauthOpenBrowser boolean is required" }, 400);
495
501
  }
@@ -574,6 +580,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
574
580
  hasCodexQuotaAutoRefresh: Object.hasOwn(config, "codexQuotaAutoRefresh"),
575
581
  oauthOpenBrowser: config.oauthOpenBrowser,
576
582
  hasOauthOpenBrowser: Object.hasOwn(config, "oauthOpenBrowser"),
583
+ showCodexCredits: config.showCodexCredits,
584
+ hasShowCodexCredits: Object.hasOwn(config, "showCodexCredits"),
577
585
  ultraFastTier: config.ultraFastTier,
578
586
  hasUltraFastTier: Object.hasOwn(config, "ultraFastTier"),
579
587
  fastRows: config.fastRows,
@@ -610,6 +618,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
610
618
  } else if (body.codexAccountPickerEnabled === false) {
611
619
  config.codexAccountPickerEnabled = false;
612
620
  }
621
+ if (typeof body.showCodexCredits === "boolean") config.showCodexCredits = body.showCodexCredits;
613
622
  if (typeof body.oauthOpenBrowser === "boolean") {
614
623
  config.oauthOpenBrowser = body.oauthOpenBrowser;
615
624
  }
@@ -664,6 +673,8 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
664
673
  if (previousSettings.hasCodexQuotaAutoRefresh) {
665
674
  config.codexQuotaAutoRefresh = previousSettings.codexQuotaAutoRefresh;
666
675
  } else deleteConfigTopLevelKey(config, "codexQuotaAutoRefresh");
676
+ if (previousSettings.hasShowCodexCredits) config.showCodexCredits = previousSettings.showCodexCredits;
677
+ else deleteConfigTopLevelKey(config, "showCodexCredits");
667
678
  if (previousSettings.hasOauthOpenBrowser) {
668
679
  config.oauthOpenBrowser = previousSettings.oauthOpenBrowser;
669
680
  } else deleteConfigTopLevelKey(config, "oauthOpenBrowser");
@@ -729,6 +740,7 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
729
740
  codexAccountPickerEnabled: pickerIsEnabled,
730
741
  codexQuotaAutoRefresh: quotaAutoRefreshSettings(config),
731
742
  oauthOpenBrowser: config.oauthOpenBrowser !== false,
743
+ showCodexCredits: config.showCodexCredits === true,
732
744
  catalogRefreshPending,
733
745
  fastRows: config.fastRows !== false,
734
746
  codexDesktopAuthless: authlessIsEnabled,
@@ -292,12 +292,18 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
292
292
  // browser profile other than the OS default, or on a different machine
293
293
  // than the proxy. Declining changes nothing else: the URL is still
294
294
  // returned below and every login surface renders it with a copy button.
295
+ //
296
+ // The launch outcome is returned rather than discarded, the same contract the Codex
297
+ // account login already keeps: a login whose browser never opened otherwise reads as one
298
+ // that did, and the dashboard can only say so if it is told. `openUrl` answers within its
299
+ // short settle window and never rejects.
295
300
  const { shouldOpenBrowserForLogin } = await import("../../oauth/open-browser-choice");
301
+ let browserLaunch: "started" | "failed" | "skipped" = "skipped";
296
302
  if (authUrl && !deviceCode && shouldOpenBrowserForLogin(body.openBrowser, config)) {
297
303
  const { openUrl } = await import("../../lib/open-url");
298
- void openUrl(authUrl);
304
+ browserLaunch = (await openUrl(authUrl)).status === "started" ? "started" : "failed";
299
305
  }
300
- return jsonResponse({ url: authUrl, instructions, deviceCode });
306
+ return jsonResponse({ url: authUrl, instructions, deviceCode, browserLaunch });
301
307
  } catch (err) {
302
308
  if (err instanceof OAuthMutationBusyError) throw err;
303
309
  const message = err instanceof Error ? err.message : String(err);
@@ -38,6 +38,7 @@ import { isInjectionDebugEnabled } from "../../lib/debug-settings";
38
38
  import { injectionDebugLog } from "../../lib/injection-debug-log";
39
39
  import { recordAttemptRequestedEffort } from "../request-log";
40
40
  import type { ResolvedFastPolicy } from "../../providers/fastwire";
41
+ import { applyXaiOauthFastModel } from "../../providers/xai-fast-model";
41
42
 
42
43
  export const MAX_FAST_WIRE_CAPABILITY_WARNINGS = 256;
43
44
 
@@ -246,6 +247,8 @@ export async function applyFinalRouteRequestNormalization(args: {
246
247
  inboundWire,
247
248
  fastPolicy,
248
249
  );
250
+ // xAI OAuth Fast is a serving-lane switch: serialize the variant id, keep the logical id for policy.
251
+ applyXaiOauthFastModel(parsed, route, logCtx);
249
252
  if (modelServiceTierSupport === false) {
250
253
  logCtx.requestedServiceTier = undefined;
251
254
  logCtx.requestedSpeedLabel = undefined;
@@ -5,6 +5,8 @@ import { deriveStartupHealth, type StartupHealth } from "../codex/autostart-heal
5
5
  import { getCodexRoutingKind } from "../codex/inject";
6
6
  import { diagnoseCodexShim } from "../codex/shim";
7
7
  import { durableBunPath } from "../lib/bun-runtime";
8
+ import { selfLaunchArgv } from "../lib/self-launch-argv";
9
+ import { desktopStartupOwnership } from "../service/desktop-startup";
8
10
  import type { OcxConfig } from "../types";
9
11
  import { truncateRetainedUtf8 } from "../lib/admission";
10
12
 
@@ -90,7 +92,9 @@ export function markStartupHealthDiagnosticStale(value: StartupHealth): StartupH
90
92
  // Mirror deriveStartupHealth's choice: an already-registered service is refreshed in
91
93
  // place. Hardcoding installService here silently undid that for every stale-cache
92
94
  // read, which is the path the dashboard hits while a probe is revalidating.
93
- recommendedCommand: value.routingKind === "custom-local" || value.routingKind === "unknown"
95
+ recommendedCommand: value.routingKind === "opencodex-local" && value.desktop?.owned
96
+ ? null
97
+ : value.routingKind === "custom-local" || value.routingKind === "unknown"
94
98
  ? value.commands.restoreNative
95
99
  : value.serviceInstalled && !value.serviceConflict
96
100
  ? value.commands.repairService
@@ -101,6 +105,7 @@ export function markStartupHealthDiagnosticStale(value: StartupHealth): StartupH
101
105
  function conservativeFallback(config: Pick<OcxConfig, "codexAutoStart">): StartupHealth {
102
106
  const shim = diagnoseCodexShim();
103
107
  return deriveStartupHealth({
108
+ desktop: desktopStartupOwnership(),
104
109
  routingKind: getCodexRoutingKind(),
105
110
  autostartEnabled: codexAutoStartEnabled(config),
106
111
  serviceInstalled: false,
@@ -121,7 +126,7 @@ function runProbe(config: Pick<OcxConfig, "codexAutoStart">): Promise<StartupHea
121
126
  const bun = durableBunPath();
122
127
  const cli = join(import.meta.dir, "..", "cli", "index.ts");
123
128
  return new Promise(resolve => {
124
- execFile(bun, [cli, "__startup-health"], {
129
+ execFile(bun, selfLaunchArgv(["__startup-health"], { sourceEntrypoint: cli }), {
125
130
  encoding: "utf8",
126
131
  env: process.env,
127
132
  timeout: PROBE_TIMEOUT_MS,
@@ -0,0 +1,93 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { accessSync, constants, readFileSync, realpathSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { readPid } from "../config/process-state";
6
+ import { resolveServiceOwnership, sameServiceOwnershipSubject, type ServiceOwnershipResolution } from "./state";
7
+
8
+ export interface DesktopStartupDiagnostic {
9
+ /** Durable desktop claim; failed supervision does not release ownership. */
10
+ owned: boolean;
11
+ loginEnabled: boolean;
12
+ running: boolean;
13
+ viable: boolean;
14
+ }
15
+
16
+ /** A login item alone is insufficient: the same app must own and supervise this proxy. */
17
+ export function deriveDesktopStartup(facts: Omit<DesktopStartupDiagnostic, "viable">): DesktopStartupDiagnostic {
18
+ return { ...facts, viable: facts.owned && facts.loginEnabled && facts.running };
19
+ }
20
+
21
+ function run(command: string, args: string[]): string {
22
+ return execFileSync(command, args, { encoding: "utf8", timeout: 750, maxBuffer: 128 * 1024, stdio: ["ignore", "pipe", "pipe"] }).trim();
23
+ }
24
+
25
+ interface DesktopStartupDeps {
26
+ platform?: NodeJS.Platform;
27
+ home?: string;
28
+ uid?: number;
29
+ ownership?: () => ServiceOwnershipResolution;
30
+ readPid?: () => number | null;
31
+ run?: typeof run;
32
+ }
33
+
34
+ /** Cheap ownership-only snapshot: no launchd/process probes on the server request path. */
35
+ export function desktopStartupOwnership(deps: DesktopStartupDeps = {}): DesktopStartupDiagnostic | undefined {
36
+ if ((deps.platform ?? process.platform) !== "darwin") return undefined;
37
+ const owner = (deps.ownership ?? resolveServiceOwnership)();
38
+ return owner.kind === "owned" && owner.ownership.owner === "desktop"
39
+ ? deriveDesktopStartup({ owned: true, loginEnabled: false, running: false }) : undefined;
40
+ }
41
+
42
+ function processIdentity(pid: number, execute: typeof run): { parent: number; executable: string } | null {
43
+ const row = /^(\d+)\s+(.+)$/.exec(execute("/bin/ps", ["-p", String(pid), "-o", "ppid=,comm="]));
44
+ return row ? { parent: Number(row[1]), executable: realpathSync(row[2]!) } : null;
45
+ }
46
+
47
+ /** Read-only macOS desktop ownership, login registration and live parent/child checks. */
48
+ export function diagnoseMacDesktopStartup(deps: DesktopStartupDeps = {}): DesktopStartupDiagnostic | undefined {
49
+ if ((deps.platform ?? process.platform) !== "darwin") return undefined;
50
+ const ownership = deps.ownership ?? resolveServiceOwnership;
51
+ const owner = ownership();
52
+ if (owner.kind !== "owned" || owner.ownership.owner !== "desktop") return undefined;
53
+ const facts = { owned: true, loginEnabled: false, running: false };
54
+ const execute = deps.run ?? run;
55
+ const pidReader = deps.readPid ?? readPid;
56
+ try {
57
+ const home = deps.home ?? homedir();
58
+ const id = readFileSync(join(home, "Library", "Application Support", "com.opencodex.desktop", "install-id"), "utf8").trim();
59
+ if (id !== owner.ownership.installId) return deriveDesktopStartup(facts);
60
+ const path = join(home, "Library", "LaunchAgents", "OpenCodex.plist");
61
+ const plist = JSON.parse(execute("/usr/bin/plutil", ["-convert", "json", "-o", "-", path]));
62
+ const args = plist.ProgramArguments;
63
+ if (plist.Label !== "OpenCodex" || plist.RunAtLoad !== true || !Array.isArray(args)
64
+ || args.length !== 2 || args[1] !== "--autostart" || typeof args[0] !== "string"
65
+ || !args[0].endsWith("/Contents/MacOS/opencodex-desktop")
66
+ || (plist.Program !== undefined && plist.Program !== args[0])) return deriveDesktopStartup(facts);
67
+ const app = realpathSync(args[0]);
68
+ const proxy = realpathSync(join(dirname(app), "ocx"));
69
+ accessSync(app, constants.X_OK);
70
+ accessSync(proxy, constants.X_OK);
71
+ const domain = `gui/${deps.uid ?? process.getuid!()}`;
72
+ const disabled = execute("/bin/launchctl", ["print-disabled", domain]);
73
+ const loaded = execute("/bin/launchctl", ["print", `${domain}/OpenCodex`]);
74
+ const program = /^\s*program = (.+)$/m.exec(loaded)?.[1];
75
+ const loadedPath = /^\s*path = (.+)$/m.exec(loaded)?.[1];
76
+ facts.loginEnabled = /^\s*disabled services = \{[\s\S]*\}\s*$/.test(disabled)
77
+ && !/"OpenCodex"\s*=>\s*(?:disabled|true)/.test(disabled)
78
+ && program !== undefined && realpathSync(program) === app
79
+ && loadedPath !== undefined && realpathSync(loadedPath) === realpathSync(path);
80
+ const pid = pidReader();
81
+ if (pid !== null) {
82
+ const child = processIdentity(pid, execute);
83
+ const parent = child && child.parent > 1 ? processIdentity(child.parent, execute) : null;
84
+ facts.running = child?.executable === proxy && parent?.executable === app && pidReader() === pid;
85
+ }
86
+ const currentOwner = ownership();
87
+ if (currentOwner.kind === "unknown" || !sameServiceOwnershipSubject(owner, currentOwner)) facts.running = false;
88
+ return deriveDesktopStartup(facts);
89
+ } catch {
90
+ // Unreadable launchd/process evidence never grants protection or releases the claim.
91
+ return deriveDesktopStartup({ ...facts, running: false });
92
+ }
93
+ }
@@ -527,6 +527,8 @@ export interface OcxConfig {
527
527
  * the guess is wrong.
528
528
  */
529
529
  oauthOpenBrowser?: boolean;
530
+ /** Display Codex credits on account cards; display only, default off. */
531
+ showCodexCredits?: boolean;
530
532
  /**
531
533
  * @deprecated Compatibility-only limit for bounded legacy usage readers.
532
534
  * `GET /api/usage` always aggregates the complete ledger.
@@ -853,13 +855,14 @@ export interface OcxConfig {
853
855
  */
854
856
  quotaResetNotify?: OcxQuotaResetNotifyConfig;
855
857
  /**
856
- * Periodic provider model-catalog refresh (issue #3630). Absent means off: no timer, no
857
- * refresh pass, no outcome record.
858
+ * Periodic provider model-catalog refresh (issue #3630). Absent means on at the hourly
859
+ * default; `enabled: false` or `intervalMinutes: 0` turns it off.
858
860
  *
859
- * Off by default for the same reason every optional subsystem here is: a refresh spends a
860
- * live /models call against every enabled provider, and this repository's rule is that a
861
- * default install runs no detection code and starts no live timer work. Not in
862
- * `getDefaultConfig()` — absence is the only default state this feature has.
861
+ * It started opt-in, and that is why GPT-6.1 Sol never reached an install without a
862
+ * release: nobody had turned the section on, so the authenticated Codex roster that
863
+ * announces a new model was never re-read. One hourly pass costs a live /models call per
864
+ * enabled provider, which is the price of new models appearing on their own. Not in
865
+ * `getDefaultConfig()` — absence is the default state.
863
866
  */
864
867
  catalogAutoRefresh?: OcxCatalogAutoRefreshConfig;
865
868
  /** Active provider context limits; native long windows remain within their supported ceilings. */
@@ -1645,14 +1648,12 @@ export interface OcxQuotaResetNotifyConfig {
1645
1648
  /**
1646
1649
  * Periodic model-catalog auto-refresh settings (issue #3630).
1647
1650
  *
1648
- * Every field is optional and the whole section defaults to off. Each tick converges the
1651
+ * Every field is optional and an absent section runs hourly. Each tick converges the
1649
1652
  * served catalog the same way `ocx sync` does, which costs a live /models call against
1650
- * every enabled provider — so an install that never asked for this must run no refresh
1651
- * code and start no timer, matching the optional-subsystem rule the rest of this file
1652
- * follows.
1653
+ * every enabled provider; set `enabled: false` to keep the catalog to explicit syncs.
1653
1654
  */
1654
1655
  export interface OcxCatalogAutoRefreshConfig {
1655
- /** Master switch. Default false — no scheduler, no tick, no upstream calls. */
1656
+ /** Master switch. Default true; false leaves the timer dormant with no upstream calls. */
1656
1657
  enabled?: boolean;
1657
1658
  /**
1658
1659
  * Minutes between refresh ticks. Default 60, floor 15, and 0 keeps the timer dormant
@@ -197,8 +197,10 @@ export interface FastWire {
197
197
  * `service_tier` request field; `cursor-variant` is a MODEL-VARIANT switch, because
198
198
  * Cursor has no tier field — its fast product is a different model id
199
199
  * (`claude-opus-5-thinking-high-fast`) or a `{id:"fast"}` request parameter for Grok.
200
+ * `model-variant` is internal only (config validation rejects it): the xAI OAuth Fast lane switch in
201
+ * src/providers/xai-fast-model.ts, whose only wire value is the serialized model id.
200
202
  */
201
- kind: "service-tier" | "anthropic-speed" | "cursor-variant";
203
+ kind: "service-tier" | "anthropic-speed" | "cursor-variant" | "model-variant";
202
204
  /** Canonical tier name to upstream wire spelling. */
203
205
  canonicalToWire: Readonly<Record<string, string>>;
204
206
  /** Policy for non-canonical caller-provided tier values. */
@@ -50,6 +50,8 @@ export interface OcxParsedRequest {
50
50
  _responseModelId?: string;
51
51
  /** Selected OpenAI API virtual-model id retained after it rewrites the upstream wire model. */
52
52
  _openAiVirtualSelectedModelId?: string;
53
+ /** Serialized-only model id (xAI OAuth Fast lane); policy keeps reading `modelId`. */
54
+ _wireModelOverride?: string;
53
55
  previousResponseId?: string;
54
56
  context: OcxContext;
55
57
  stream: boolean;
package/src/usage/log.ts CHANGED
@@ -645,7 +645,8 @@ function normalizeAttemptTierOutcome(raw: unknown): AttemptTierOutcome | null {
645
645
  && outcome.wireKind !== null
646
646
  && outcome.wireKind !== "service-tier"
647
647
  && outcome.wireKind !== "anthropic-speed"
648
- && outcome.wireKind !== "cursor-variant") return null;
648
+ && outcome.wireKind !== "cursor-variant"
649
+ && outcome.wireKind !== "model-variant") return null;
649
650
  if ("wireValue" in outcome && outcome.wireValue !== null && typeof outcome.wireValue !== "string") return null;
650
651
  if ("fastDowngradeReason" in outcome
651
652
  && (typeof outcome.fastDowngradeReason !== "string"
@@ -665,6 +666,7 @@ function normalizeAttemptTierOutcome(raw: unknown): AttemptTierOutcome | null {
665
666
  || outcome.wireKind === "service-tier"
666
667
  || outcome.wireKind === "anthropic-speed"
667
668
  || outcome.wireKind === "cursor-variant"
669
+ || outcome.wireKind === "model-variant"
668
670
  ? { wireKind: outcome.wireKind }
669
671
  : {}),
670
672
  ...(outcome.wireValue === null