@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.
Files changed (60) 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 +2 -1
  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 +2 -1
  17. package/src/codex/auth-api.ts +108 -17
  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 +193 -85
  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/providers/antigravity-models.ts +53 -24
  35. package/src/providers/codex-capacity.ts +303 -0
  36. package/src/providers/model-rename-migration.ts +147 -0
  37. package/src/providers/model-rename-startup.ts +29 -0
  38. package/src/providers/quota.ts +126 -16
  39. package/src/providers/registry.ts +258 -38
  40. package/src/responses/parser.ts +19 -12
  41. package/src/responses/spill-store.ts +14 -1
  42. package/src/responses/state.ts +108 -14
  43. package/src/server/index.ts +9 -1
  44. package/src/server/management/logs-usage-routes.ts +1 -0
  45. package/src/server/management/oauth-account-routes.ts +8 -1
  46. package/src/server/relay.ts +10 -42
  47. package/src/server/request-log.ts +42 -1
  48. package/src/server/responses/compact.ts +16 -4
  49. package/src/server/responses/core.ts +217 -59
  50. package/src/server/responses/empty-completion-guard.ts +275 -0
  51. package/src/server/responses/encrypted-payload.ts +54 -39
  52. package/src/server/responses/fetch-helpers.ts +24 -3
  53. package/src/server/responses/ws-upstream.ts +318 -0
  54. package/src/server/sse-frame-buffer.ts +292 -0
  55. package/src/server/ws-bridge.ts +17 -11
  56. package/src/types.ts +8 -0
  57. package/src/usage/log.ts +24 -0
  58. package/src/usage/summary.ts +152 -2
  59. package/vendor/ocx-notch/win32-x64/ocx-notch.exe +0 -0
  60. package/gui/dist/assets/index-BucjyD4I.js +0 -67
@@ -0,0 +1,303 @@
1
+ export const CODEX_CONFIGURED_CAPACITY_WEIGHTS = {
2
+ plus: 1,
3
+ team: 1,
4
+ business: 1,
5
+ prolite: 5,
6
+ pro: 20,
7
+ } as const;
8
+
9
+ export const CODEX_CAPACITY_MAX_QUOTA_AGE_MS = 30 * 60_000;
10
+
11
+ export type CodexCapacityQuota = {
12
+ fiveHourPercent?: number;
13
+ fiveHourResetAt?: number;
14
+ weeklyPercent?: number;
15
+ weeklyResetAt?: number;
16
+ monthlyPercent?: number;
17
+ monthlyResetAt?: number;
18
+ customWindows?: Array<{ label: string; percent: number; resetAt?: number }>;
19
+ updatedAt: number;
20
+ };
21
+
22
+ export interface CodexCapacityAccount {
23
+ isMain: boolean;
24
+ active?: boolean;
25
+ plan?: unknown;
26
+ paused: boolean;
27
+ needsReauth?: boolean;
28
+ quota: CodexCapacityQuota | null;
29
+ }
30
+
31
+ export interface CodexCapacityWindowAggregation {
32
+ usedPercent: number;
33
+ includedAccounts: number;
34
+ excludedAccounts: number;
35
+ incomplete: boolean;
36
+ /** Internal calculation evidence; management projections should omit it. */
37
+ totalWeight?: number;
38
+ consumedWeight?: number;
39
+ remainingWeight?: number;
40
+ updatedAt: number;
41
+ nextRecoveryAt?: number;
42
+ nextRecoveryPercent?: number;
43
+ }
44
+
45
+ export interface CodexCapacityAggregation {
46
+ kind: "capacity-weighted-v1";
47
+ scope: "routable-known";
48
+ includedAccounts: number;
49
+ excludedAccounts: number;
50
+ unknownPlanAccounts: number;
51
+ missingQuotaAccounts: number;
52
+ pausedAccounts: number;
53
+ reauthAccounts: number;
54
+ staleQuotaAccounts: number;
55
+ partialWindowAccounts: number;
56
+ incomplete: boolean;
57
+ presentation?: "aggregate" | "effective-account-fallback" | "coverage-only";
58
+ fiveHour?: CodexCapacityWindowAggregation;
59
+ weekly?: CodexCapacityWindowAggregation;
60
+ monthly?: CodexCapacityWindowAggregation;
61
+ customWindows?: Array<CodexCapacityWindowAggregation & { label: string }>;
62
+ currentAccount?: {
63
+ isMain: boolean;
64
+ plan?: string | null;
65
+ quota: CodexCapacityQuota | null;
66
+ };
67
+ }
68
+
69
+ export interface CodexCapacityResult {
70
+ quota: CodexCapacityQuota | null;
71
+ aggregation: CodexCapacityAggregation | null;
72
+ currentAccount?: CodexCapacityAggregation["currentAccount"];
73
+ }
74
+
75
+ type MutableWindow = {
76
+ totalWeight: number;
77
+ consumedWeight: number;
78
+ includedAccounts: number;
79
+ recoveries: Map<number, number>;
80
+ oldestUpdatedAt: number;
81
+ };
82
+
83
+ function planValue(plan: unknown): string | undefined {
84
+ return typeof plan === "string" && plan.trim() ? plan.trim() : undefined;
85
+ }
86
+
87
+ function normalizedPlan(plan: unknown): string | undefined {
88
+ return planValue(plan)?.toLowerCase();
89
+ }
90
+
91
+ function configuredWeight(plan: unknown): number | undefined {
92
+ const normalized = normalizedPlan(plan);
93
+ return normalized && Object.hasOwn(CODEX_CONFIGURED_CAPACITY_WEIGHTS, normalized)
94
+ ? CODEX_CONFIGURED_CAPACITY_WEIGHTS[normalized as keyof typeof CODEX_CONFIGURED_CAPACITY_WEIGHTS]
95
+ : undefined;
96
+ }
97
+
98
+ function normalizedPercent(value: unknown): number | undefined {
99
+ return typeof value === "number" && Number.isFinite(value)
100
+ ? Math.max(0, Math.min(100, value))
101
+ : undefined;
102
+ }
103
+
104
+ function futureResetMs(value: unknown, now: number): number | undefined {
105
+ if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
106
+ const milliseconds = value > 10_000_000_000 ? value : value * 1000;
107
+ return milliseconds > now ? milliseconds : undefined;
108
+ }
109
+
110
+ function hasKnownQuotaWindow(quota: CodexCapacityQuota | null): quota is CodexCapacityQuota {
111
+ if (!quota) return false;
112
+ return normalizedPercent(quota.fiveHourPercent) !== undefined
113
+ || normalizedPercent(quota.weeklyPercent) !== undefined
114
+ || normalizedPercent(quota.monthlyPercent) !== undefined
115
+ || !!quota.customWindows?.some(window => normalizedPercent(window.percent) !== undefined);
116
+ }
117
+
118
+ function currentQuotaForDisplay(account: CodexCapacityAccount, now: number): CodexCapacityQuota | null {
119
+ const quota = account.quota;
120
+ const fresh = !!quota
121
+ && Number.isFinite(quota.updatedAt)
122
+ && now - quota.updatedAt <= CODEX_CAPACITY_MAX_QUOTA_AGE_MS;
123
+ return !account.paused && !account.needsReauth && fresh && hasKnownQuotaWindow(quota) ? quota : null;
124
+ }
125
+
126
+ function addWindow(
127
+ windows: Map<string, MutableWindow>,
128
+ key: string,
129
+ weight: number,
130
+ percent: number,
131
+ resetAt: number | undefined,
132
+ updatedAt: number,
133
+ ): void {
134
+ const window = windows.get(key) ?? {
135
+ totalWeight: 0,
136
+ consumedWeight: 0,
137
+ includedAccounts: 0,
138
+ recoveries: new Map<number, number>(),
139
+ oldestUpdatedAt: updatedAt,
140
+ };
141
+ const consumed = weight * percent / 100;
142
+ window.totalWeight += weight;
143
+ window.consumedWeight += consumed;
144
+ window.includedAccounts += 1;
145
+ window.oldestUpdatedAt = Math.min(window.oldestUpdatedAt, updatedAt);
146
+ if (resetAt !== undefined && consumed > 0) {
147
+ window.recoveries.set(resetAt, (window.recoveries.get(resetAt) ?? 0) + consumed);
148
+ }
149
+ windows.set(key, window);
150
+ }
151
+
152
+ function finalizeWindow(window: MutableWindow, totalAccounts: number): CodexCapacityWindowAggregation {
153
+ const nextRecoveryAt = [...window.recoveries.keys()].sort((a, b) => a - b)[0];
154
+ const recovered = nextRecoveryAt === undefined ? undefined : window.recoveries.get(nextRecoveryAt);
155
+ return {
156
+ usedPercent: window.consumedWeight / window.totalWeight * 100,
157
+ includedAccounts: window.includedAccounts,
158
+ excludedAccounts: totalAccounts - window.includedAccounts,
159
+ incomplete: window.includedAccounts < totalAccounts,
160
+ totalWeight: window.totalWeight,
161
+ consumedWeight: window.consumedWeight,
162
+ remainingWeight: window.totalWeight - window.consumedWeight,
163
+ updatedAt: window.oldestUpdatedAt,
164
+ ...(nextRecoveryAt !== undefined ? { nextRecoveryAt } : {}),
165
+ ...(recovered !== undefined ? { nextRecoveryPercent: recovered / window.totalWeight * 100 } : {}),
166
+ };
167
+ }
168
+
169
+ /** Display-only configured-weight estimate; never used by routing or quota admission. */
170
+ export function aggregateCodexPoolCapacity(
171
+ accounts: readonly CodexCapacityAccount[],
172
+ now = Date.now(),
173
+ ): CodexCapacityResult {
174
+ const current = accounts.find(account => account.active)
175
+ ?? accounts.find(account => account.isMain)
176
+ ?? accounts[0];
177
+ const currentPlan = planValue(current?.plan);
178
+ const currentAccount = current ? {
179
+ isMain: current.isMain,
180
+ ...(currentPlan !== undefined ? { plan: currentPlan } : {}),
181
+ quota: currentQuotaForDisplay(current, now),
182
+ } : undefined;
183
+ const windows = new Map<string, MutableWindow>();
184
+ const included = new Set<CodexCapacityAccount>();
185
+ const contributions = new Map<CodexCapacityAccount, Set<string>>();
186
+ let unknownPlanAccounts = 0;
187
+ let missingQuotaAccounts = 0;
188
+ let pausedAccounts = 0;
189
+ let reauthAccounts = 0;
190
+ let staleQuotaAccounts = 0;
191
+
192
+ for (const account of accounts) {
193
+ const weight = configuredWeight(account.plan);
194
+ if (weight === undefined) unknownPlanAccounts += 1;
195
+ if (account.paused) pausedAccounts += 1;
196
+ if (account.needsReauth) reauthAccounts += 1;
197
+ const quota = account.quota;
198
+ const quotaFresh = !!quota
199
+ && Number.isFinite(quota.updatedAt)
200
+ && now - quota.updatedAt <= CODEX_CAPACITY_MAX_QUOTA_AGE_MS;
201
+ if (quota && !quotaFresh) staleQuotaAccounts += 1;
202
+ const standard = quota ? [
203
+ ["fiveHour", quota.fiveHourPercent, quota.fiveHourResetAt],
204
+ ["weekly", quota.weeklyPercent, quota.weeklyResetAt],
205
+ ["monthly", quota.monthlyPercent, quota.monthlyResetAt],
206
+ ] as const : [];
207
+ const custom = quota?.customWindows ?? [];
208
+ const hasQuota = hasKnownQuotaWindow(quota);
209
+ if (!hasQuota) missingQuotaAccounts += 1;
210
+ if (account.paused || account.needsReauth || weight === undefined || !quota || !hasQuota || !quotaFresh) continue;
211
+
212
+ const contributionKeys = new Set<string>();
213
+ for (const [key, rawPercent, rawReset] of standard) {
214
+ const percent = normalizedPercent(rawPercent);
215
+ if (percent === undefined) continue;
216
+ addWindow(windows, key, weight, percent, futureResetMs(rawReset, now), quota.updatedAt);
217
+ contributionKeys.add(key);
218
+ }
219
+ for (const customWindow of custom) {
220
+ const percent = normalizedPercent(customWindow.percent);
221
+ if (percent === undefined) continue;
222
+ const key = `custom:${customWindow.label}`;
223
+ addWindow(windows, key, weight, percent, futureResetMs(customWindow.resetAt, now), quota.updatedAt);
224
+ contributionKeys.add(key);
225
+ }
226
+ if (contributionKeys.size > 0) {
227
+ included.add(account);
228
+ contributions.set(account, contributionKeys);
229
+ }
230
+ }
231
+
232
+ if (windows.size === 0) {
233
+ if (accounts.length === 0) return { quota: null, aggregation: null };
234
+ const aggregation: CodexCapacityAggregation = {
235
+ kind: "capacity-weighted-v1",
236
+ scope: "routable-known",
237
+ includedAccounts: 0,
238
+ excludedAccounts: accounts.length,
239
+ unknownPlanAccounts,
240
+ missingQuotaAccounts,
241
+ pausedAccounts,
242
+ reauthAccounts,
243
+ staleQuotaAccounts,
244
+ partialWindowAccounts: 0,
245
+ incomplete: true,
246
+ ...(currentAccount ? { currentAccount } : {}),
247
+ };
248
+ return { quota: null, aggregation, ...(currentAccount ? { currentAccount } : {}) };
249
+ }
250
+
251
+ const fiveHour = windows.get("fiveHour") ? finalizeWindow(windows.get("fiveHour")!, accounts.length) : undefined;
252
+ const weekly = windows.get("weekly") ? finalizeWindow(windows.get("weekly")!, accounts.length) : undefined;
253
+ const monthly = windows.get("monthly") ? finalizeWindow(windows.get("monthly")!, accounts.length) : undefined;
254
+ const customWindows = [...windows.entries()].flatMap(([key, window]) => key.startsWith("custom:")
255
+ ? [{ label: key.slice("custom:".length), ...finalizeWindow(window, accounts.length) }]
256
+ : []);
257
+ const quota: CodexCapacityQuota = {
258
+ ...(fiveHour ? { fiveHourPercent: fiveHour.usedPercent } : {}),
259
+ ...(fiveHour && currentAccount?.quota?.fiveHourResetAt !== undefined
260
+ ? { fiveHourResetAt: currentAccount.quota.fiveHourResetAt }
261
+ : {}),
262
+ ...(weekly ? { weeklyPercent: weekly.usedPercent } : {}),
263
+ ...(weekly && currentAccount?.quota?.weeklyResetAt !== undefined
264
+ ? { weeklyResetAt: currentAccount.quota.weeklyResetAt }
265
+ : {}),
266
+ ...(monthly ? { monthlyPercent: monthly.usedPercent } : {}),
267
+ ...(monthly && currentAccount?.quota?.monthlyResetAt !== undefined
268
+ ? { monthlyResetAt: currentAccount.quota.monthlyResetAt }
269
+ : {}),
270
+ ...(customWindows.length > 0 ? {
271
+ customWindows: customWindows.map(window => ({ label: window.label, percent: window.usedPercent })),
272
+ } : {}),
273
+ updatedAt: Math.min(
274
+ ...[fiveHour, weekly, monthly, ...customWindows]
275
+ .flatMap(window => window ? [window.updatedAt] : []),
276
+ ),
277
+ };
278
+ const visibleWindowKeys = [...windows.keys()];
279
+ const partialWindowAccounts = accounts.filter(account => {
280
+ const keys = contributions.get(account);
281
+ return !!keys && visibleWindowKeys.some(key => !keys.has(key));
282
+ }).length;
283
+ const excludedAccounts = accounts.length - included.size;
284
+ const aggregation: CodexCapacityAggregation = {
285
+ kind: "capacity-weighted-v1",
286
+ scope: "routable-known",
287
+ includedAccounts: included.size,
288
+ excludedAccounts,
289
+ unknownPlanAccounts,
290
+ missingQuotaAccounts,
291
+ pausedAccounts,
292
+ reauthAccounts,
293
+ staleQuotaAccounts,
294
+ partialWindowAccounts,
295
+ incomplete: excludedAccounts > 0 || partialWindowAccounts > 0,
296
+ ...(fiveHour ? { fiveHour } : {}),
297
+ ...(weekly ? { weekly } : {}),
298
+ ...(monthly ? { monthly } : {}),
299
+ ...(customWindows.length > 0 ? { customWindows } : {}),
300
+ ...(currentAccount ? { currentAccount } : {}),
301
+ };
302
+ return { quota, aggregation, ...(currentAccount ? { currentAccount } : {}) };
303
+ }
@@ -0,0 +1,147 @@
1
+ import type { OcxConfig, OcxProviderConfig } from "../types";
2
+ import { PROVIDER_REGISTRY } from "./registry";
3
+
4
+ export interface ModelRename {
5
+ provider: string;
6
+ from: string;
7
+ to: string;
8
+ reason: string;
9
+ dropReasoningEffortMap?: boolean;
10
+ }
11
+
12
+ const RETIRED_ANTIGRAVITY_FLASH_MODELS = [
13
+ "gemini-3.6-flash",
14
+ "gemini-3.6-flash-low",
15
+ "gemini-3.6-flash-medium",
16
+ "gemini-3.6-flash-high",
17
+ "gemini-3.5-flash-extra-low",
18
+ "gemini-3.5-flash-low",
19
+ "gemini-3.5-flash-mid",
20
+ "gemini-3.5-flash-high",
21
+ "gemini-3-flash-agent",
22
+ ] as const;
23
+
24
+ export const MODEL_RENAMES: readonly ModelRename[] = RETIRED_ANTIGRAVITY_FLASH_MODELS.map(from => ({
25
+ provider: "google-antigravity",
26
+ from,
27
+ to: "gemini-3.7-flash",
28
+ reason: "Google retires the previous Antigravity Flash generation from Cloud Code Assist when its successor ships",
29
+ dropReasoningEffortMap: true,
30
+ }));
31
+
32
+ const MODEL_KEYED_RECORDS = [
33
+ "modelContextWindows",
34
+ "modelMaxOutputTokens",
35
+ "modelInputModalities",
36
+ "modelReasoningEfforts",
37
+ "modelDefaultReasoningEfforts",
38
+ "modelReasoningEffortMap",
39
+ ] as const;
40
+
41
+ const MODEL_ID_LISTS = [
42
+ "models",
43
+ "selectedModels",
44
+ "noVisionModels",
45
+ "noReasoningModels",
46
+ "noTemperatureModels",
47
+ "noTopPModels",
48
+ "noPenaltyModels",
49
+ "autoToolChoiceOnlyModels",
50
+ "preserveReasoningContentModels",
51
+ "thinkingBudgetModels",
52
+ ] as const;
53
+
54
+ function renameInList(value: unknown, from: string, to: string): string[] | null {
55
+ if (!Array.isArray(value) || !value.includes(from)) return null;
56
+ const next: string[] = [];
57
+ const seen = new Set<string>();
58
+ for (const entry of value) {
59
+ if (typeof entry !== "string") continue;
60
+ const mapped = entry === from ? to : entry;
61
+ if (seen.has(mapped)) continue;
62
+ seen.add(mapped);
63
+ next.push(mapped);
64
+ }
65
+ return next;
66
+ }
67
+
68
+ function renameInRecord(value: unknown, from: string, to: string): Record<string, unknown> | null {
69
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
70
+ const record = value as Record<string, unknown>;
71
+ if (!(from in record)) return null;
72
+ const next: Record<string, unknown> = {};
73
+ for (const [key, entry] of Object.entries(record)) {
74
+ const mapped = key === from ? to : key;
75
+ if (mapped in next) continue;
76
+ next[mapped] = key === from && to in record ? record[to] : entry;
77
+ }
78
+ return next;
79
+ }
80
+
81
+ function dropFromRecord(value: unknown, from: string): Record<string, unknown> | null {
82
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
83
+ const record = value as Record<string, unknown>;
84
+ if (!(from in record)) return null;
85
+ return Object.fromEntries(Object.entries(record).filter(([key]) => key !== from));
86
+ }
87
+
88
+ function providerStillMatchesRegistry(name: string, provider: OcxProviderConfig): boolean {
89
+ const entry = PROVIDER_REGISTRY.find(row => row.id === name);
90
+ if (!entry) return false;
91
+ if (!provider.baseUrl || !entry.baseUrl) return true;
92
+ const known = [entry.baseUrl, ...(entry.baseUrlChoices?.map(choice => choice.baseUrl).filter(Boolean) ?? [])]
93
+ .map(url => url!.replace(/\/+$/, ""));
94
+ return known.includes(provider.baseUrl.replace(/\/+$/, ""));
95
+ }
96
+
97
+ export interface ModelRenameProjection {
98
+ config: OcxConfig;
99
+ changed: boolean;
100
+ warnings: string[];
101
+ }
102
+
103
+ export function projectModelRenames(
104
+ config: OcxConfig,
105
+ renames: readonly ModelRename[] = MODEL_RENAMES,
106
+ ): ModelRenameProjection {
107
+ let changed = false;
108
+ const warnings: string[] = [];
109
+
110
+ for (const rename of renames) {
111
+ const provider = config.providers?.[rename.provider];
112
+ const registry = PROVIDER_REGISTRY.find(row => row.id === rename.provider);
113
+ if (!provider || !registry?.models?.includes(rename.to) || !providerStillMatchesRegistry(rename.provider, provider)) continue;
114
+
115
+ const row = provider as unknown as Record<string, unknown>;
116
+ let touched = false;
117
+ for (const field of MODEL_ID_LISTS) {
118
+ const next = renameInList(row[field], rename.from, rename.to);
119
+ if (!next) continue;
120
+ row[field] = next;
121
+ touched = true;
122
+ }
123
+ for (const field of MODEL_KEYED_RECORDS) {
124
+ const next = rename.dropReasoningEffortMap && field === "modelReasoningEffortMap"
125
+ ? dropFromRecord(row[field], rename.from)
126
+ : renameInRecord(row[field], rename.from, rename.to);
127
+ if (!next) continue;
128
+ row[field] = next;
129
+ touched = true;
130
+ }
131
+ if (provider.defaultModel === rename.from) {
132
+ provider.defaultModel = rename.to;
133
+ touched = true;
134
+ }
135
+ const disabled = `${rename.provider}/${rename.from}`;
136
+ if (config.disabledModels?.includes(disabled)) {
137
+ config.disabledModels = config.disabledModels.filter(model => model !== disabled);
138
+ touched = true;
139
+ }
140
+ if (touched) {
141
+ changed = true;
142
+ warnings.push(`renamed "${rename.provider}/${rename.from}" to "${rename.to}": ${rename.reason}.`);
143
+ }
144
+ }
145
+
146
+ return { config, changed, warnings };
147
+ }
@@ -0,0 +1,29 @@
1
+
2
+ import { saveConfig } from "../config";
3
+ import { projectModelRenames } from "./model-rename-migration";
4
+ import type { OcxConfig } from "../types";
5
+
6
+ export interface ModelRenameStartupDeps {
7
+ project: typeof projectModelRenames;
8
+ save: (config: OcxConfig) => void;
9
+ }
10
+
11
+ /**
12
+ * Apply registry model renames to the saved config at startup (issue #1610).
13
+ *
14
+ * No backup is taken, unlike the OpenAI tier and Alibaba region migrations: those
15
+ * rewrite credentials and provider identity, where a bad projection is not
16
+ * recoverable from the config alone. This one only rewrites model ids that the
17
+ * registry itself no longer seeds, and the pre-migration value is a string this
18
+ * file still names, so the change is reversible by hand.
19
+ */
20
+ export function runModelRenameStartupMigration(
21
+ config: OcxConfig,
22
+ deps: ModelRenameStartupDeps = { project: projectModelRenames, save: saveConfig },
23
+ ): OcxConfig {
24
+ const projection = deps.project(config);
25
+ for (const warning of projection.warnings) console.warn(`[model-rename-migration] ${warning}`);
26
+ if (!projection.changed) return projection.config;
27
+ deps.save(projection.config);
28
+ return projection.config;
29
+ }