@bman654/clodex 2.3.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,6 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ CHILD_NETWORK_ENV_VARS,
4
+ NETWORK_ENV_CONTRACT_VAR,
5
+ OAUTH_ACCOUNT_ENV,
6
+ OAUTH_ACCOUNT_NAME_RE,
7
+ REGISTRY_SCHEMA_VERSION_WITH_MATERIALIZED_ACTIVE_ACCOUNT,
3
8
  assertRegistryWriteOwnership,
9
+ clearActiveOAuthAccount,
4
10
  ensureSecureAppHome,
5
11
  findClaudeBinary,
6
12
  getAppHome,
@@ -11,6 +17,7 @@ import {
11
17
  getInstalledClaudeVersion,
12
18
  getLocalPatchesPath,
13
19
  getLogsPath,
20
+ getOAuthAccountSlot,
14
21
  getSavedServerPassword,
15
22
  getServerExposedProviders,
16
23
  getServerFavoritesOnly,
@@ -18,12 +25,15 @@ import {
18
25
  getServerMaskGatewayIds,
19
26
  isDiscoveryDisabled,
20
27
  isValidProviderId,
21
- launchClaude,
22
28
  listenTcpServer,
23
29
  loadPreferences,
24
30
  loadRegistry,
25
31
  loadRegistryStrict,
32
+ networkEnvBaseline,
33
+ providerDefaultAuthRef,
34
+ readLiveServerRuntimeStates,
26
35
  recordLaunchSelection,
36
+ recordNetworkEnvMutation,
27
37
  registerServerRuntimeState,
28
38
  removeAnthropicProxyBypass,
29
39
  resolveBridgeMode,
@@ -34,13 +44,14 @@ import {
34
44
  setServerFavoritesOnly,
35
45
  setServerListenMode,
36
46
  setServerMaskGatewayIds,
47
+ storeActiveOAuthAccount,
37
48
  tcpListenerUrlHost,
38
49
  unregisterServerRuntimeState,
39
50
  withCredentialMutationLock,
40
51
  withProviderMutationLock,
41
52
  withRegistryWriteLock,
42
53
  withRegistryWriteLockSync
43
- } from "./chunk-W4SVDQCZ.js";
54
+ } from "./chunk-MRO3KE3P.js";
44
55
 
45
56
  // src/cli.ts
46
57
  import pc13 from "picocolors";
@@ -109,8 +120,8 @@ function fmtProvider(name) {
109
120
  }
110
121
  function fmtProviderBracket(providerId, providerName, isFree) {
111
122
  const color = providerTagColor(providerId);
112
- const text4 = isFree ? `${providerName} \xB7 free` : providerName;
113
- return color(pc.bold(`(${text4})`));
123
+ const text5 = isFree ? `${providerName} \xB7 free` : providerName;
124
+ return color(pc.bold(`(${text5})`));
114
125
  }
115
126
  function providerTagColor(providerId) {
116
127
  switch (providerId) {
@@ -211,6 +222,151 @@ import * as p12 from "@clack/prompts";
211
222
  import { realpathSync as realpathSync2 } from "fs";
212
223
  import { fileURLToPath } from "url";
213
224
 
225
+ // src/launch.ts
226
+ import { spawn } from "child_process";
227
+ import { appendFileSync } from "fs";
228
+
229
+ // src/parent-notice.ts
230
+ import { writeSync } from "fs";
231
+ var MAX_NOTICE_CHARS = 2e3;
232
+ var CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f]/g;
233
+ var activeSink = null;
234
+ var stderrErrorGuardInstalled = false;
235
+ function toNoticeLine(message) {
236
+ const flattened = message.replace(CONTROL_CHARS, " ").trimEnd();
237
+ const bounded = flattened.length > MAX_NOTICE_CHARS ? `${flattened.slice(0, MAX_NOTICE_CHARS)}...` : flattened;
238
+ return `${bounded}
239
+ `;
240
+ }
241
+ function guardStderrErrors() {
242
+ if (stderrErrorGuardInstalled) return;
243
+ stderrErrorGuardInstalled = true;
244
+ try {
245
+ process.stderr.on("error", () => {
246
+ });
247
+ } catch {
248
+ }
249
+ }
250
+ function writeParentNoticeLines(lines) {
251
+ if (lines.length === 0) return;
252
+ guardStderrErrors();
253
+ try {
254
+ process.stderr.write(lines.join(""), () => {
255
+ });
256
+ } catch {
257
+ }
258
+ }
259
+ function writeParentNoticeLinesSync(lines) {
260
+ if (lines.length === 0) return;
261
+ try {
262
+ writeSync(2, lines.join(""));
263
+ } catch {
264
+ }
265
+ }
266
+ function emitParentNotice(message) {
267
+ const line = toNoticeLine(message);
268
+ try {
269
+ if (activeSink) activeSink(line);
270
+ else writeParentNoticeLines([line]);
271
+ } catch {
272
+ }
273
+ }
274
+ function installParentNoticeSink(sink) {
275
+ const previous = activeSink;
276
+ activeSink = sink;
277
+ let released = false;
278
+ return () => {
279
+ if (released) return;
280
+ released = true;
281
+ if (activeSink === sink) activeSink = previous;
282
+ };
283
+ }
284
+
285
+ // src/launch.ts
286
+ var isWindows = process.platform === "win32";
287
+ var MAX_QUEUED_NOTICES = 50;
288
+ function buildClaudeArgs(model, extraArgs) {
289
+ return model ? ["--model", model, ...extraArgs] : [...extraArgs];
290
+ }
291
+ function launchClaude(env, model, extraArgs) {
292
+ return new Promise((resolve3) => {
293
+ const claudePath = findClaudeBinary();
294
+ const args = buildClaudeArgs(model, extraArgs);
295
+ const debugFileIdx = extraArgs.indexOf("--debug-file");
296
+ const debugLogPath = debugFileIdx !== -1 && extraArgs[debugFileIdx + 1] ? extraArgs[debugFileIdx + 1] : void 0;
297
+ const originalStdoutWrite = process.stdout.write;
298
+ const originalStderrWrite = process.stderr.write;
299
+ const muteWrite = (chunk, encoding, callback) => {
300
+ if (typeof encoding === "function") {
301
+ callback = encoding;
302
+ }
303
+ if (debugLogPath) {
304
+ try {
305
+ const str = typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk);
306
+ appendFileSync(debugLogPath, `[parent] ${str}`);
307
+ } catch {
308
+ }
309
+ }
310
+ if (callback) callback();
311
+ return true;
312
+ };
313
+ process.stdout.write = muteWrite;
314
+ process.stderr.write = muteWrite;
315
+ const queuedNotices = [];
316
+ let droppedNotices = 0;
317
+ const releaseNoticeSink = installParentNoticeSink((line) => {
318
+ if (debugLogPath) {
319
+ try {
320
+ appendFileSync(debugLogPath, `[parent] ${line}`);
321
+ } catch {
322
+ }
323
+ }
324
+ if (queuedNotices.length < MAX_QUEUED_NOTICES) queuedNotices.push(line);
325
+ else droppedNotices += 1;
326
+ });
327
+ const takeQueuedNotices = () => {
328
+ const lines = queuedNotices.splice(0, queuedNotices.length);
329
+ if (droppedNotices > 0) {
330
+ lines.push(
331
+ `clodex: warning: and ${droppedNotices} further notice${droppedNotices === 1 ? "" : "s"} suppressed while Claude Code held the terminal.
332
+ `
333
+ );
334
+ droppedNotices = 0;
335
+ }
336
+ return lines;
337
+ };
338
+ const flushNoticesOnExit = () => {
339
+ writeParentNoticeLinesSync(takeQueuedNotices());
340
+ };
341
+ process.once("exit", flushNoticesOnExit);
342
+ const restore = () => {
343
+ releaseNoticeSink();
344
+ process.removeListener("exit", flushNoticesOnExit);
345
+ process.stdout.write = originalStdoutWrite;
346
+ process.stderr.write = originalStderrWrite;
347
+ writeParentNoticeLines(takeQueuedNotices());
348
+ };
349
+ const child = spawn(claudePath, args, {
350
+ stdio: "inherit",
351
+ env,
352
+ shell: isWindows
353
+ });
354
+ const forward = (signal) => {
355
+ child.kill(signal);
356
+ };
357
+ process.once("SIGINT", () => forward("SIGINT"));
358
+ process.once("SIGTERM", () => forward("SIGTERM"));
359
+ child.on("exit", (code) => {
360
+ restore();
361
+ resolve3(code ?? 0);
362
+ });
363
+ child.on("error", (err) => {
364
+ restore();
365
+ resolve3(1);
366
+ });
367
+ });
368
+ }
369
+
214
370
  // src/constants.ts
215
371
  import { homedir } from "os";
216
372
  import { join } from "path";
@@ -218,7 +374,7 @@ import { join } from "path";
218
374
  // package.json
219
375
  var package_default = {
220
376
  name: "@bman654/clodex",
221
- version: "2.3.0",
377
+ version: "2.5.0",
222
378
  publishConfig: {
223
379
  access: "public"
224
380
  },
@@ -351,7 +507,7 @@ import {
351
507
  import { join as join2 } from "path";
352
508
 
353
509
  // src/credential-helper.ts
354
- import { spawn } from "child_process";
510
+ import { spawn as spawn2 } from "child_process";
355
511
  import { createHash } from "crypto";
356
512
  import { accessSync, constants, statSync } from "fs";
357
513
  import { isAbsolute, normalize, resolve } from "path";
@@ -418,7 +574,7 @@ async function runCredentialHelper(operation, account, input, expectedHelperId)
418
574
  );
419
575
  }
420
576
  return new Promise((resolve3, reject) => {
421
- const child = spawn(
577
+ const child = spawn2(
422
578
  helper.path,
423
579
  [operation, CREDENTIAL_HELPER_SERVICE, account],
424
580
  { shell: false, stdio: ["pipe", "pipe", "pipe"] }
@@ -840,6 +996,51 @@ function applyClaudeCodeThirdPartyCompat(env) {
840
996
  env["ENABLE_TOOL_SEARCH"] = "true";
841
997
  env["CLAUDE_CODE_SIMPLE_SYSTEM_PROMPT"] = "0";
842
998
  }
999
+ var LOOPBACK_NO_PROXY_ENTRIES = ["localhost", "127.0.0.1", "::1"];
1000
+ function gatewayHostname(gatewayUrl) {
1001
+ try {
1002
+ return new URL(gatewayUrl).hostname.replace(/^\[|\]$/g, "").toLowerCase();
1003
+ } catch {
1004
+ return "";
1005
+ }
1006
+ }
1007
+ function isLoopbackHost(host) {
1008
+ if (host === "localhost" || host.endsWith(".localhost")) return true;
1009
+ if (host === "::1" || host === "0:0:0:0:0:0:0:1") return true;
1010
+ const v4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
1011
+ if (!v4) return false;
1012
+ const octets = v4.slice(1).map(Number);
1013
+ if (octets.some((o) => o > 255)) return false;
1014
+ return octets[0] === 127;
1015
+ }
1016
+ function effectiveNoProxyValue(env) {
1017
+ return env["no_proxy"] || env["NO_PROXY"] || "";
1018
+ }
1019
+ function normalizedNoProxyEntries(env) {
1020
+ return effectiveNoProxyValue(env).split(",").map((value) => value.trim()).filter(Boolean);
1021
+ }
1022
+ function addGatewayNoProxyBypass(env, gatewayUrl) {
1023
+ const hasProxy = Boolean(
1024
+ env["HTTPS_PROXY"]?.trim() || env["https_proxy"]?.trim() || env["HTTP_PROXY"]?.trim() || env["http_proxy"]?.trim()
1025
+ );
1026
+ if (!hasProxy) return;
1027
+ const host = gatewayHostname(gatewayUrl);
1028
+ if (!isLoopbackHost(host)) return;
1029
+ if (effectiveNoProxyValue(env) === "*") return;
1030
+ const existing = normalizedNoProxyEntries(env);
1031
+ const additions = [...LOOPBACK_NO_PROXY_ENTRIES, host];
1032
+ const seen = new Set(existing.map((entry) => entry.toLowerCase()));
1033
+ const merged = [...existing];
1034
+ for (const entry of additions) {
1035
+ const key = entry.toLowerCase();
1036
+ if (seen.has(key)) continue;
1037
+ seen.add(key);
1038
+ merged.push(entry);
1039
+ }
1040
+ const value = merged.join(",");
1041
+ env["NO_PROXY"] = value;
1042
+ env["no_proxy"] = value;
1043
+ }
843
1044
  function buildChildEnv(baseUrl, model, apiKey, proxyPort, contextWindow, enableGatewayDiscovery) {
844
1045
  const env = { ...process.env };
845
1046
  for (const name of CONFLICTING_ENV_VARS) {
@@ -847,6 +1048,7 @@ function buildChildEnv(baseUrl, model, apiKey, proxyPort, contextWindow, enableG
847
1048
  }
848
1049
  env["ANTHROPIC_BASE_URL"] = proxyPort ? `http://127.0.0.1:${proxyPort}` : baseUrl;
849
1050
  env["ANTHROPIC_API_KEY"] = apiKey;
1051
+ addGatewayNoProxyBypass(env, env["ANTHROPIC_BASE_URL"]);
850
1052
  const bareModel = stripOneMContextSuffix(model);
851
1053
  env["ANTHROPIC_MODEL"] = claudeCodeClientModelId(model, contextWindow);
852
1054
  env["CLAUDE_CODE_MAX_CONTEXT_TOKENS"] = String(resolveContextWindow(bareModel, contextWindow));
@@ -856,8 +1058,9 @@ function buildChildEnv(baseUrl, model, apiKey, proxyPort, contextWindow, enableG
856
1058
  applyClaudeCodeThirdPartyCompat(env);
857
1059
  return env;
858
1060
  }
859
- function buildHttpProxyChildEnv(proxyPort, caCertPath) {
860
- const env = { ...process.env };
1061
+ function buildHttpProxyChildEnv(proxyPort, caCertPath, baseEnv = process.env) {
1062
+ const baseline = networkEnvBaseline(baseEnv);
1063
+ const env = { ...baseline };
861
1064
  for (const name of CONFLICTING_ENV_VARS) {
862
1065
  if (name === "ANTHROPIC_API_KEY" || name === "ANTHROPIC_AUTH_TOKEN" || name === "ANTHROPIC_MODEL") continue;
863
1066
  delete env[name];
@@ -869,6 +1072,7 @@ function buildHttpProxyChildEnv(proxyPort, caCertPath) {
869
1072
  env["http_proxy"] = proxyUrl;
870
1073
  env["NODE_EXTRA_CA_CERTS"] = caCertPath;
871
1074
  removeAnthropicProxyBypass(env);
1075
+ recordNetworkEnvMutation(baseline, env);
872
1076
  return env;
873
1077
  }
874
1078
  function classifyKeyringError(err) {
@@ -906,7 +1110,10 @@ var KEYRING_GENERATION_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0
906
1110
  function oauthProviderIdFromAccount(account) {
907
1111
  const prefix = "oauth:provider:";
908
1112
  const baseAccount = credentialAccountBase(account);
909
- return baseAccount.startsWith(prefix) ? baseAccount.slice(prefix.length) : null;
1113
+ if (!baseAccount.startsWith(prefix)) return null;
1114
+ const id = baseAccount.slice(prefix.length);
1115
+ const slot = id.indexOf(":account:");
1116
+ return slot === -1 ? id : id.slice(0, slot);
910
1117
  }
911
1118
  var oauthRefreshInflight = /* @__PURE__ */ new Map();
912
1119
  var OAUTH_CREDENTIAL_CACHE_MAX_AGE_MS = 3e4;
@@ -946,8 +1153,8 @@ function parseAuthRef(authRef) {
946
1153
  function clodexKeyEnvVar(providerId) {
947
1154
  return `CLODEX_KEY_${providerId.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
948
1155
  }
949
- function readEnvCredential(varName) {
950
- const raw = process.env[varName];
1156
+ function readEnvCredential(varName, env = process.env) {
1157
+ const raw = env[varName];
951
1158
  if (!raw?.trim()) return null;
952
1159
  return raw.trim().split(/\r?\n/)[0]?.trim() || null;
953
1160
  }
@@ -971,6 +1178,26 @@ function usableEnvCredential(source, value, rejectedAccessToken) {
971
1178
  }
972
1179
  return value;
973
1180
  }
1181
+ function readProviderCredentialOverride(providerId, env, options) {
1182
+ if (options.ignoreProviderOverride) return null;
1183
+ const variable = clodexKeyEnvVar(providerId);
1184
+ const credential = usableEnvCredential(
1185
+ `provider:${providerId}`,
1186
+ readEnvCredential(variable, env),
1187
+ options.rejectedAccessToken
1188
+ );
1189
+ if (!credential) return null;
1190
+ return {
1191
+ credential,
1192
+ state: {
1193
+ variable,
1194
+ fingerprint: credentialFingerprint(credential)
1195
+ }
1196
+ };
1197
+ }
1198
+ function resolveProviderCredentialOverrideState(providerId, env = process.env, options = {}) {
1199
+ return readProviderCredentialOverride(providerId, env, options)?.state ?? null;
1200
+ }
974
1201
  function readKeyringEntry(keyring, service, account) {
975
1202
  const value = new keyring.Entry(service, account).getPassword();
976
1203
  if (value !== null) return value;
@@ -2459,25 +2686,37 @@ async function deleteStoredCredential(ref, diag, blockLegacy = true) {
2459
2686
  return false;
2460
2687
  }
2461
2688
  }
2462
- async function resolveProviderCredential(providerId, authRef, diag, options = {}) {
2689
+ async function resolveProviderCredentialWithSource(providerId, authRef, diag, options = {}) {
2463
2690
  const parsed = parseAuthRef(authRef);
2464
- if (parsed?.kind === "none") return null;
2465
- const namespacedVar = clodexKeyEnvVar(providerId);
2466
- const namespaced = usableEnvCredential(
2467
- `provider:${providerId}`,
2468
- readEnvCredential(namespacedVar),
2469
- options.rejectedAccessToken
2470
- );
2471
- if (namespaced) return namespaced;
2472
- if (!parsed) return null;
2691
+ if (parsed?.kind === "none") return { credential: null };
2692
+ const namespaced = readProviderCredentialOverride(providerId, process.env, options);
2693
+ if (namespaced) {
2694
+ return {
2695
+ credential: namespaced.credential,
2696
+ credentialOverride: namespaced.state
2697
+ };
2698
+ }
2699
+ if (!parsed) return { credential: null };
2473
2700
  if (parsed.kind === "env") {
2474
- return usableEnvCredential(
2475
- `provider:${providerId}:env:${parsed.varName}`,
2476
- readEnvCredential(parsed.varName),
2477
- options.rejectedAccessToken
2478
- );
2701
+ return {
2702
+ credential: usableEnvCredential(
2703
+ `provider:${providerId}:env:${parsed.varName}`,
2704
+ readEnvCredential(parsed.varName),
2705
+ options.rejectedAccessToken
2706
+ )
2707
+ };
2479
2708
  }
2480
- return readProviderSecret(parsed, diag, options.rejectedAccessToken);
2709
+ return {
2710
+ credential: await readProviderSecret(parsed, diag, options.rejectedAccessToken)
2711
+ };
2712
+ }
2713
+ async function resolveProviderCredential(providerId, authRef, diag, options = {}) {
2714
+ return (await resolveProviderCredentialWithSource(
2715
+ providerId,
2716
+ authRef,
2717
+ diag,
2718
+ options
2719
+ )).credential;
2481
2720
  }
2482
2721
  async function resolveProviderOAuthAccountId(authRef, diag) {
2483
2722
  const parsed = parseAuthRef(authRef);
@@ -3414,12 +3653,24 @@ function applyPricingToRegistryProviders(registry, cache) {
3414
3653
  const index = buildPricingIndex(cache);
3415
3654
  let changed = false;
3416
3655
  for (const provider of registry.providers) {
3417
- if (!provider.modelsCache?.models.length) continue;
3656
+ if (provider.preserveModelPricing) continue;
3418
3657
  const platform = TEMPLATE_TO_PRICING_PLATFORM[provider.templateId] ?? TEMPLATE_TO_PRICING_PLATFORM[provider.id];
3419
- const enriched = enrichModelsWithPricing(provider.modelsCache.models, index, platform);
3420
- if (JSON.stringify(enriched) !== JSON.stringify(provider.modelsCache.models)) {
3421
- provider.modelsCache = { ...provider.modelsCache, models: enriched };
3658
+ const enrichCache = (modelsCache) => {
3659
+ if (!modelsCache?.models.length) return modelsCache;
3660
+ const enriched = enrichModelsWithPricing(modelsCache.models, index, platform);
3661
+ if (JSON.stringify(enriched) === JSON.stringify(modelsCache.models)) return modelsCache;
3422
3662
  changed = true;
3663
+ return { ...modelsCache, models: enriched };
3664
+ };
3665
+ const topLevelCache = enrichCache(provider.modelsCache);
3666
+ if (topLevelCache !== provider.modelsCache) provider.modelsCache = topLevelCache;
3667
+ const defaultCache = enrichCache(provider.defaultModelsCache);
3668
+ if (defaultCache !== provider.defaultModelsCache) provider.defaultModelsCache = defaultCache;
3669
+ for (const [name, account] of Object.entries(provider.authAccounts ?? {})) {
3670
+ const accountCache = enrichCache(account.modelsCache);
3671
+ if (accountCache !== account.modelsCache) {
3672
+ provider.authAccounts[name] = { ...account, modelsCache: accountCache };
3673
+ }
3423
3674
  }
3424
3675
  }
3425
3676
  if (changed) {
@@ -3716,7 +3967,9 @@ function cachedModelToLocal(cached, provider) {
3716
3967
  reasoning: cached.reasoning ?? modelsDev?.reasoning,
3717
3968
  interleavedReasoningField: cached.interleavedReasoningField ?? modelsDev?.interleaved?.field,
3718
3969
  useResponsesLite: cached.useResponsesLite,
3719
- preferWebSockets: cached.preferWebSockets
3970
+ preferWebSockets: cached.preferWebSockets,
3971
+ modalities: cached.modalities,
3972
+ compatibility: cached.compatibility
3720
3973
  };
3721
3974
  }
3722
3975
  function isAnonymousProvider(provider) {
@@ -3725,6 +3978,48 @@ function isAnonymousProvider(provider) {
3725
3978
  function isLegacyAnonymousCustomEndpoint(provider, credential) {
3726
3979
  return provider.authType === void 0 && (provider.templateId === "custom-openai" || provider.templateId === "custom-anthropic") && provider.authRef === `keyring:provider:${provider.id}` && credential === "local";
3727
3980
  }
3981
+ function applySelectedOAuthAccount(provider, selected = process.env[OAUTH_ACCOUNT_ENV], warn) {
3982
+ const requested = selected?.trim().toLowerCase();
3983
+ const fromEnvironment = Boolean(requested);
3984
+ const name = requested || provider.activeAuthAccount?.trim();
3985
+ if (!name) return provider;
3986
+ if (!provider.enabled) return provider;
3987
+ if (provider.authType !== "oauth") return provider;
3988
+ const slots = provider.authAccounts;
3989
+ const stored = provider.activeAuthAccount?.trim();
3990
+ if (!slots || Object.keys(slots).length === 0) {
3991
+ if (stored === void 0 || stored === "") {
3992
+ if (fromEnvironment) {
3993
+ warn?.(`${OAUTH_ACCOUNT_ENV}=${requested} ignored for provider "${provider.id}" because it has no named account slots.`);
3994
+ }
3995
+ return provider;
3996
+ }
3997
+ throw new Error(
3998
+ `Provider "${provider.id}" is set to use account "${stored}", but it has no named accounts. Re-add the account with: clodex providers auth openai --account ` + stored + ", or clear the selection with: clodex providers"
3999
+ );
4000
+ }
4001
+ if (!Object.prototype.hasOwnProperty.call(slots, name)) {
4002
+ const available = Object.keys(slots).sort().join(", ");
4003
+ throw new Error(
4004
+ fromEnvironment ? `CLODEX_OAUTH_ACCOUNT=${name}: provider "${provider.id}" has no account named "${name}" (available: ${available}). Add it with: clodex providers auth openai --account ` + name : `Provider "${provider.id}" is set to use account "${name}", which no longer exists (available: ${available}). Choose another with: clodex providers`
4005
+ );
4006
+ }
4007
+ const account = slots[name];
4008
+ const projected = { ...provider, authRef: account.authRef };
4009
+ if (fromEnvironment && name !== stored) {
4010
+ if (account.modelsCache) projected.modelsCache = account.modelsCache;
4011
+ else delete projected.modelsCache;
4012
+ } else if (!projected.modelsCache && account.modelsCache) {
4013
+ projected.modelsCache = account.modelsCache;
4014
+ }
4015
+ return projected;
4016
+ }
4017
+ function projectSelectedOAuthAccount(provider, selected = process.env[OAUTH_ACCOUNT_ENV]) {
4018
+ const dormantOAuth = provider.authType === "oauth" && !provider.enabled;
4019
+ const candidate = dormantOAuth ? { ...provider, enabled: true } : provider;
4020
+ const projected = applySelectedOAuthAccount(candidate, selected);
4021
+ return dormantOAuth ? { ...projected, enabled: false } : projected;
4022
+ }
3728
4023
  function materializeOne(provider, resolveCredential, agent) {
3729
4024
  if (!provider.enabled) return null;
3730
4025
  if (!isValidProviderId(provider.id)) return null;
@@ -3772,19 +4067,37 @@ function materializeRegistry(registry, resolveCredential, opts) {
3772
4067
 
3773
4068
  // src/registry/load.ts
3774
4069
  async function loadRegistryProviders(diag, opts) {
3775
- const registry = loadRegistry();
4070
+ const registry = loadRegistry(void 0, diag);
4071
+ const providers = registry.providers.map((provider) => applySelectedOAuthAccount(
4072
+ provider,
4073
+ void 0,
4074
+ opts?.warn
4075
+ ));
4076
+ const selectedRegistry = { ...registry, providers };
3776
4077
  const keys = /* @__PURE__ */ new Map();
3777
4078
  const oauthAccountIds = /* @__PURE__ */ new Map();
3778
4079
  const oauthProviderData = /* @__PURE__ */ new Map();
3779
- await Promise.all(registry.providers.map(async (provider) => {
3780
- if (isAnonymousProvider(provider)) return;
4080
+ const blockedProviders = /* @__PURE__ */ new Map();
4081
+ await Promise.all(providers.map(async (provider) => {
4082
+ if (isAnonymousProvider(provider) || provider.authType === "none" || provider.authRef === "none:anonymous") return;
4083
+ let resolved;
3781
4084
  try {
3782
- const key = await resolveProviderCredential(provider.id, provider.authRef, diag);
3783
- if (key) keys.set(provider.id, key);
4085
+ resolved = await resolveProviderCredentialWithSource(provider.id, provider.authRef, diag);
3784
4086
  } catch (err) {
3785
4087
  diag?.(`${provider.id}: credential unavailable \u2014 ${err instanceof Error ? err.message : String(err)}`);
4088
+ return;
4089
+ }
4090
+ const credentialOverride = resolved.credentialOverride !== void 0;
4091
+ if (provider.enabled && resolved.credentialOverride) {
4092
+ blockedProviders.set(
4093
+ provider.id,
4094
+ `${resolved.credentialOverride.variable} is a process-scoped credential with no isolated model catalog for provider "${provider.id}". Save that credential as a provider or account and refresh its models, or unset the variable.`
4095
+ );
4096
+ return;
3786
4097
  }
3787
- if (provider.authType === "oauth") {
4098
+ const credentialAvailable = Boolean(resolved.credential);
4099
+ if (resolved.credential) keys.set(provider.id, resolved.credential);
4100
+ if (provider.authType === "oauth" && credentialAvailable && !credentialOverride) {
3788
4101
  try {
3789
4102
  const accountId = await resolveProviderOAuthAccountId(provider.authRef, diag);
3790
4103
  if (accountId) oauthAccountIds.set(provider.id, accountId);
@@ -3794,11 +4107,16 @@ async function loadRegistryProviders(diag, opts) {
3794
4107
  }
3795
4108
  }
3796
4109
  }));
3797
- return materializeRegistry(registry, (provider) => keys.get(provider.id) ?? null, opts).map((provider) => ({
4110
+ const materialized = materializeRegistry(selectedRegistry, (provider) => keys.get(provider.id) ?? null, opts).map((provider) => ({
3798
4111
  ...provider,
3799
4112
  oauthAccountId: oauthAccountIds.get(provider.id),
3800
4113
  providerData: oauthProviderData.get(provider.id)
3801
4114
  }));
4115
+ Object.defineProperty(materialized, "blockedProviders", {
4116
+ value: blockedProviders,
4117
+ enumerable: false
4118
+ });
4119
+ return materialized;
3802
4120
  }
3803
4121
 
3804
4122
  // src/provider-templates.ts
@@ -3840,7 +4158,8 @@ function getTemplateById(id) {
3840
4158
 
3841
4159
  // src/provider-catalog.ts
3842
4160
  async function fetchProviderCatalog(opts) {
3843
- return loadRegistryProviders(void 0, opts);
4161
+ const warn = (message) => console.warn(message);
4162
+ return loadRegistryProviders(warn, { ...opts, warn });
3844
4163
  }
3845
4164
  function providersForPicker(providers) {
3846
4165
  for (const p13 of providers) {
@@ -3883,16 +4202,115 @@ function formatRegistryAuthLabel(provider) {
3883
4202
  }
3884
4203
  return provider.authRef;
3885
4204
  }
4205
+ var PROVIDER_DEFAULT_ACCOUNT_LABEL = "(provider default)";
4206
+ function resolveActiveAccount(provider, env = process.env) {
4207
+ const slots = provider.authAccounts ?? {};
4208
+ const has = (name) => Object.prototype.hasOwnProperty.call(slots, name);
4209
+ const stored = provider.activeAuthAccount?.trim();
4210
+ const override = env[OAUTH_ACCOUNT_ENV]?.trim().toLowerCase();
4211
+ const projectOAuthSelection = (environmentSelection) => {
4212
+ const latentOrphan = stored && !has(stored) && stored !== environmentSelection ? { latentOrphan: stored } : {};
4213
+ if (environmentSelection) {
4214
+ if (has(environmentSelection)) {
4215
+ return {
4216
+ kind: "slot",
4217
+ name: environmentSelection,
4218
+ fromEnvironment: true,
4219
+ ...latentOrphan
4220
+ };
4221
+ }
4222
+ if (Object.keys(slots).length > 0) {
4223
+ return {
4224
+ kind: "broken",
4225
+ name: environmentSelection,
4226
+ fromEnvironment: true,
4227
+ ...latentOrphan
4228
+ };
4229
+ }
4230
+ }
4231
+ if (!stored) return { kind: "default" };
4232
+ if (has(stored)) return { kind: "slot", name: stored, fromEnvironment: false };
4233
+ return { kind: "broken", name: stored, fromEnvironment: false };
4234
+ };
4235
+ const selected = provider.authType !== "oauth" ? { ...projectOAuthSelection(void 0), inactiveReason: "non-oauth" } : provider.enabled ? projectOAuthSelection(override) : { ...projectOAuthSelection(override), inactiveReason: "disabled" };
4236
+ const effectiveAuthRef = provider.authType === "oauth" && selected.kind === "slot" ? slots[selected.name]?.authRef : provider.authRef;
4237
+ const credentialOverride = provider.authType !== "none" && effectiveAuthRef !== "none:anonymous" ? resolveProviderCredentialOverrideState(provider.id, env) : null;
4238
+ if (!credentialOverride) return selected;
4239
+ if (provider.authType === "oauth" && selected.kind === "broken") {
4240
+ return { ...selected, credentialOverride };
4241
+ }
4242
+ return {
4243
+ kind: "credential-override",
4244
+ credentialOverride,
4245
+ selection: selected,
4246
+ ...selected.latentOrphan ? { latentOrphan: selected.latentOrphan } : {},
4247
+ ...selected.inactiveReason ? { inactiveReason: selected.inactiveReason } : {}
4248
+ };
4249
+ }
3886
4250
  async function resolveProvidersForDisplay() {
3887
4251
  const reg = loadRegistry();
3888
4252
  const entries = [];
3889
4253
  for (const provider of reg.providers) {
4254
+ const accountNames = Object.keys(provider.authAccounts ?? {}).sort();
4255
+ const effective = resolveActiveAccount(provider);
4256
+ const selection = effective.kind === "credential-override" ? effective.selection : effective;
4257
+ const credentialOverride = effective.credentialOverride;
4258
+ const credentialOverrideWins = effective.kind === "credential-override";
4259
+ const active = !credentialOverrideWins && selection.kind === "slot" && !selection.inactiveReason ? selection.name : void 0;
4260
+ const projected = !credentialOverrideWins && selection.kind === "slot" && selection.inactiveReason === "disabled" ? selection.name : void 0;
4261
+ const storedButInapplicable = selection.kind === "slot" && selection.inactiveReason === "non-oauth" ? selection.name : void 0;
4262
+ const accountOverrideApplies = selection.kind !== "default" && selection.fromEnvironment;
4263
+ const broken = selection.kind === "broken" ? selection : void 0;
4264
+ const latent = selection.latentOrphan;
4265
+ const label = (name) => {
4266
+ if (name === active) {
4267
+ return accountOverrideApplies ? `${name} (active, from ${OAUTH_ACCOUNT_ENV})` : `${name} (active)`;
4268
+ }
4269
+ if (credentialOverrideWins && selection.kind === "slot" && name === selection.name) {
4270
+ if (selection.inactiveReason === "non-oauth") {
4271
+ return `${name} (stored; provider is not OAuth)`;
4272
+ }
4273
+ const selectedFrom = accountOverrideApplies ? `, from ${OAUTH_ACCOUNT_ENV}` : "";
4274
+ return selection.inactiveReason === "disabled" ? `${name} (selected${selectedFrom}; provider disabled; ${credentialOverride.variable} has no isolated model catalog)` : `${name} (selected${selectedFrom}; ${credentialOverride.variable} configured; launch blocked \u2014 no isolated model catalog)`;
4275
+ }
4276
+ if (name === projected) {
4277
+ return accountOverrideApplies ? `${name} (selected, from ${OAUTH_ACCOUNT_ENV}; provider disabled)` : `${name} (selected; provider disabled)`;
4278
+ }
4279
+ if (name === storedButInapplicable) {
4280
+ return `${name} (stored; provider is not OAuth)`;
4281
+ }
4282
+ return name;
4283
+ };
4284
+ const defaultLabel = selection.kind !== "default" ? PROVIDER_DEFAULT_ACCOUNT_LABEL : credentialOverrideWins ? selection.inactiveReason === "disabled" ? `${PROVIDER_DEFAULT_ACCOUNT_LABEL} (selected; provider disabled; ${credentialOverride.variable} has no isolated model catalog)` : selection.inactiveReason === "non-oauth" ? `${PROVIDER_DEFAULT_ACCOUNT_LABEL} (OAuth selection inactive; provider is not OAuth)` : `${PROVIDER_DEFAULT_ACCOUNT_LABEL} (selected; ${credentialOverride.variable} configured; launch blocked \u2014 no isolated model catalog)` : selection.inactiveReason === "disabled" ? `${PROVIDER_DEFAULT_ACCOUNT_LABEL} (selected; provider disabled)` : selection.inactiveReason === "non-oauth" ? `${PROVIDER_DEFAULT_ACCOUNT_LABEL} (OAuth selection inactive; provider is not OAuth)` : `${PROVIDER_DEFAULT_ACCOUNT_LABEL} (active)`;
4285
+ const brokenConsequence = selection.inactiveReason === "disabled" ? "will fail if this provider is enabled" : selection.inactiveReason === "non-oauth" ? "ignored because this provider is not OAuth" : "every launch fails";
4286
+ const accountList = [
4287
+ defaultLabel,
4288
+ ...accountNames.map(label),
4289
+ // An inactive account selection gets saved-state wording, not a current
4290
+ // OAuth outcome. A disabled provider is excluded from materialization;
4291
+ // a non-OAuth provider may launch, but never applies these selectors.
4292
+ ...broken ? [`${broken.name} (${selection.inactiveReason === "non-oauth" ? "stored" : "selected"}${broken.fromEnvironment ? ` via ${OAUTH_ACCOUNT_ENV}` : ""}, MISSING \u2014 ${brokenConsequence})`] : [],
4293
+ ...latent ? [`${latent} (stored, MISSING \u2014 masked by ${OAUTH_ACCOUNT_ENV}; ${selection.inactiveReason === "disabled" ? "will fail if enabled without it" : "launches fail without it"})`] : []
4294
+ ].join(", ");
4295
+ const storedAuthLabel = formatRegistryAuthLabel(provider);
4296
+ const effectiveAuthLabel = !credentialOverride ? storedAuthLabel : credentialOverrideWins ? provider.enabled ? `${credentialOverride.variable} (configured provider override; launch blocked \u2014 no isolated model catalog; stored auth: ${storedAuthLabel})` : `${credentialOverride.variable} (configured provider override; no isolated model catalog; provider disabled; stored auth: ${storedAuthLabel})` : `${storedAuthLabel}; ${credentialOverride.variable} is configured but blocked by the invalid OAuth account selection`;
4297
+ const authLabel = accountNames.length || broken || latent ? `${effectiveAuthLabel}; accounts: ${accountList}` : effectiveAuthLabel;
3890
4298
  entries.push({
3891
4299
  id: provider.id,
3892
4300
  name: provider.name,
3893
- modelCount: provider.modelsCache?.models.length ?? 0,
4301
+ // Model counts describe the identity this process would launch, not the
4302
+ // persisted account hidden behind CLODEX_OAUTH_ACCOUNT. A broken
4303
+ // selection has no safe catalog to advertise.
4304
+ modelCount: (() => {
4305
+ if (credentialOverrideWins) return 0;
4306
+ try {
4307
+ return projectSelectedOAuthAccount(provider).modelsCache?.models.length ?? 0;
4308
+ } catch {
4309
+ return 0;
4310
+ }
4311
+ })(),
3894
4312
  enabled: provider.enabled,
3895
- authLabel: formatRegistryAuthLabel(provider),
4313
+ authLabel,
3896
4314
  inRegistry: true
3897
4315
  });
3898
4316
  }
@@ -3926,6 +4344,7 @@ function localProvidersToServerModels(localProviders) {
3926
4344
  interleavedReasoningField: model.interleavedReasoningField,
3927
4345
  useResponsesLite: model.useResponsesLite,
3928
4346
  preferWebSockets: model.preferWebSockets,
4347
+ compatibility: model.compatibility,
3929
4348
  headers: provider.headers,
3930
4349
  providerData: provider.providerData
3931
4350
  }))
@@ -3940,6 +4359,8 @@ import { createHash as createHash3 } from "crypto";
3940
4359
  import { AsyncLocalStorage } from "async_hooks";
3941
4360
 
3942
4361
  // src/outbound-proxy.ts
4362
+ import { networkInterfaces } from "os";
4363
+ import { HttpsProxyAgent } from "https-proxy-agent";
3943
4364
  function hasOutboundProxyEnv(env = process.env) {
3944
4365
  return Boolean(
3945
4366
  env["HTTPS_PROXY"]?.trim() || env["https_proxy"]?.trim() || env["HTTP_PROXY"]?.trim() || env["http_proxy"]?.trim()
@@ -3975,6 +4396,26 @@ function outboundProxyUrlForTarget(targetUrl, env = process.env) {
3975
4396
  if (noProxyBypasses(parsed.hostname, env)) return void 0;
3976
4397
  return proxy.trim();
3977
4398
  }
4399
+ function proxyUrlTargetsListener(proxyUrl, listenerHost, listenerPort, localAddresses = new Set(
4400
+ Object.values(networkInterfaces()).flatMap((entries) => (entries ?? []).map((entry) => entry.address.toLowerCase()))
4401
+ )) {
4402
+ let parsed;
4403
+ try {
4404
+ parsed = new URL(proxyUrl);
4405
+ } catch {
4406
+ return false;
4407
+ }
4408
+ const proxyPort = parsed.port ? Number(parsed.port) : parsed.protocol === "https:" ? 443 : parsed.protocol === "http:" ? 80 : void 0;
4409
+ if (proxyPort !== listenerPort) return false;
4410
+ const normalizeHost = (host) => host.toLowerCase().replace(/^\[|\]$/g, "");
4411
+ const proxyHost = normalizeHost(parsed.hostname);
4412
+ const boundHost = normalizeHost(listenerHost);
4413
+ if (proxyHost === boundHost) return true;
4414
+ const isLoopback = (host) => host === "localhost" || host === "::1" || /^127(?:\.\d{1,3}){3}$/.test(host);
4415
+ const isWildcard = (host) => host === "0.0.0.0" || host === "::";
4416
+ if (isLoopback(proxyHost) && (isLoopback(boundHost) || isWildcard(boundHost))) return true;
4417
+ return isWildcard(boundHost) && (isWildcard(proxyHost) || localAddresses.has(proxyHost));
4418
+ }
3978
4419
  var dispatcherInstalled = false;
3979
4420
  async function installOutboundProxyDispatcher() {
3980
4421
  if (dispatcherInstalled) return true;
@@ -3991,11 +4432,24 @@ async function installOutboundProxyDispatcher() {
3991
4432
  return false;
3992
4433
  }
3993
4434
  }
3994
- async function outboundWsProxyAgent(wsUrl) {
3995
- const proxyUrl = outboundProxyUrlForTarget(wsUrl);
4435
+ function outboundHttpProxyAgent(targetUrl, env = process.env) {
4436
+ const proxyUrl = outboundProxyUrlForTarget(targetUrl, env);
3996
4437
  if (!proxyUrl) return void 0;
3997
- const { HttpsProxyAgent } = await import("https-proxy-agent");
3998
- return new HttpsProxyAgent(proxyUrl);
4438
+ try {
4439
+ const parsedProxy = new URL(proxyUrl);
4440
+ if (!parsedProxy.hostname || !["http:", "https:"].includes(parsedProxy.protocol)) {
4441
+ throw new TypeError("Invalid proxy URL");
4442
+ }
4443
+ return new HttpsProxyAgent(parsedProxy, { keepAlive: true });
4444
+ } catch (err) {
4445
+ console.error(
4446
+ `clodex: HTTP(S)_PROXY cannot be used for a CONNECT tunnel; using a direct connection (${err instanceof Error ? err.message : String(err)})`
4447
+ );
4448
+ return void 0;
4449
+ }
4450
+ }
4451
+ function outboundWsProxyAgent(wsUrl, env = process.env) {
4452
+ return outboundHttpProxyAgent(wsUrl, env);
3999
4453
  }
4000
4454
 
4001
4455
  // src/upstream-error.ts
@@ -4044,7 +4498,7 @@ function frameStatusCode(code, discriminator) {
4044
4498
  const numeric = Number(code);
4045
4499
  if (numeric >= 400 && numeric <= 599) return numeric;
4046
4500
  }
4047
- if (/insufficient_quota|rate_limit/.test(discriminator)) return 429;
4501
+ if (/insufficient_quota|rate_limit|usage_limit/.test(discriminator)) return 429;
4048
4502
  if (discriminator.includes("authentication")) return 401;
4049
4503
  if (discriminator.includes("permission")) return 403;
4050
4504
  if (discriminator.includes("not_found")) return 404;
@@ -4487,13 +4941,9 @@ function warnReasoningNormalizationGap(fields, log12) {
4487
4941
  if (warnedReasoningGaps.has(signature)) return;
4488
4942
  if (warnedReasoningGaps.size >= MAX_REASONING_GAP_WARNINGS) return;
4489
4943
  warnedReasoningGaps.add(signature);
4490
- try {
4491
- process.stderr.write(`${message}
4492
- `);
4493
- if (warnedReasoningGaps.size === MAX_REASONING_GAP_WARNINGS) {
4494
- process.stderr.write("clodex: warning: further reasoning-normalization warnings suppressed.\n");
4495
- }
4496
- } catch {
4944
+ emitParentNotice(message);
4945
+ if (warnedReasoningGaps.size === MAX_REASONING_GAP_WARNINGS) {
4946
+ emitParentNotice("clodex: warning: further reasoning-normalization warnings suppressed.");
4497
4947
  }
4498
4948
  }
4499
4949
  function toolArgumentNormalizationGap(expected, actual, requiredProps) {
@@ -4542,13 +4992,9 @@ function warnToolArgumentNormalizationGap(gap, log12) {
4542
4992
  if (warnedToolArgumentGaps.has(signature)) return;
4543
4993
  if (warnedToolArgumentGaps.size >= MAX_TOOL_ARGUMENT_GAP_WARNINGS) return;
4544
4994
  warnedToolArgumentGaps.add(signature);
4545
- try {
4546
- process.stderr.write(`${message}
4547
- `);
4548
- if (warnedToolArgumentGaps.size === MAX_TOOL_ARGUMENT_GAP_WARNINGS) {
4549
- process.stderr.write("clodex: warning: further tool-argument normalization warnings suppressed.\n");
4550
- }
4551
- } catch {
4995
+ emitParentNotice(message);
4996
+ if (warnedToolArgumentGaps.size === MAX_TOOL_ARGUMENT_GAP_WARNINGS) {
4997
+ emitParentNotice("clodex: warning: further tool-argument normalization warnings suppressed.");
4552
4998
  }
4553
4999
  }
4554
5000
  function continuationMismatchDetails(entry, payload, log12, warnOnGap = false) {
@@ -4986,12 +5432,12 @@ function expectedAssistantItems(ctx) {
4986
5432
  const type = accumulator.type ?? (typeof done.type === "string" ? done.type : void 0);
4987
5433
  if (type === "message") {
4988
5434
  const doneContent = Array.isArray(done.content) ? done.content : void 0;
4989
- const text4 = accumulator.text || (doneContent ? doneContent.filter((part) => part && typeof part === "object" && part.type === "output_text").map((part) => String(part.text ?? "")).join("") : "");
4990
- output.push({ role: "assistant", content: [{ type: "output_text", text: text4 }] });
5435
+ const text5 = accumulator.text || (doneContent ? doneContent.filter((part) => part && typeof part === "object" && part.type === "output_text").map((part) => String(part.text ?? "")).join("") : "");
5436
+ output.push({ role: "assistant", content: [{ type: "output_text", text: text5 }] });
4991
5437
  continue;
4992
5438
  }
4993
5439
  if (type === "reasoning") {
4994
- const summary = accumulator.summaries.size ? [...accumulator.summaries.entries()].sort(([a], [b]) => a - b).map(([, text4]) => ({ type: "summary_text", text: text4 })) : Array.isArray(done.summary) ? done.summary : [];
5440
+ const summary = accumulator.summaries.size ? [...accumulator.summaries.entries()].sort(([a], [b]) => a - b).map(([, text5]) => ({ type: "summary_text", text: text5 })) : Array.isArray(done.summary) ? done.summary : [];
4995
5441
  output.push({ ...withoutEphemeralFields(done), type: "reasoning", summary });
4996
5442
  continue;
4997
5443
  }
@@ -5210,7 +5656,7 @@ function transportReplaySafe(ctx) {
5210
5656
  function handleSocketMessage(entry, data) {
5211
5657
  const ctx = entry.current;
5212
5658
  if (!ctx || ctx.closed) return;
5213
- const text4 = Array.isArray(data) ? Buffer.concat(data).toString("utf8") : data.toString("utf8");
5659
+ const text5 = Array.isArray(data) ? Buffer.concat(data).toString("utf8") : data.toString("utf8");
5214
5660
  ctx.frameCount += 1;
5215
5661
  if (ctx.transportRetryPending) {
5216
5662
  ctx.transportRetryPending = false;
@@ -5222,9 +5668,9 @@ function handleSocketMessage(entry, data) {
5222
5668
  }
5223
5669
  let event;
5224
5670
  try {
5225
- event = JSON.parse(text4);
5671
+ event = JSON.parse(text5);
5226
5672
  } catch {
5227
- ctx.pendingEvents.push(text4.replace(/\r?\n/g, " "));
5673
+ ctx.pendingEvents.push(text5.replace(/\r?\n/g, " "));
5228
5674
  flushPending(ctx);
5229
5675
  return;
5230
5676
  }
@@ -5267,7 +5713,8 @@ function handleSocketMessage(entry, data) {
5267
5713
  return;
5268
5714
  }
5269
5715
  const errorStatus = type === "error" && !ctx.emittedModelData ? responseErrorStatus(event) : void 0;
5270
- if (FAILURE_EVENT_TYPES.has(type ?? "") && (errorStatus === void 0 || willRetry)) {
5716
+ const emptyFailureTerminal = FAILURE_EVENT_TYPES.has(type ?? "") && errorStatus === void 0 && !willRetry && !ctx.emittedModelData;
5717
+ if (FAILURE_EVENT_TYPES.has(type ?? "") && (errorStatus === void 0 || willRetry) && !emptyFailureTerminal) {
5271
5718
  emitResponseErrorDiagnostic(entry, ctx, {
5272
5719
  source: "response_event",
5273
5720
  upstreamEventType: type,
@@ -5310,6 +5757,39 @@ function handleSocketMessage(entry, data) {
5310
5757
  );
5311
5758
  return;
5312
5759
  }
5760
+ if (emptyFailureTerminal) {
5761
+ const details = responseFailureDetails(event);
5762
+ const named = [details.errorType, details.errorCode, details.incompleteReason].filter((value) => typeof value === "string");
5763
+ const summary = "OpenAI ended the response with no output" + (named.length ? ` (${named.join(" / ")})` : ` (${type})`);
5764
+ const discriminator = [details.errorType, details.errorCode].filter((value) => typeof value === "string").join(" ").toLowerCase();
5765
+ const settledReason = details.incompleteReason === "content_filter" || details.incompleteReason === "max_output_tokens";
5766
+ const numericOrNamed = typeof details.errorCode === "string" ? details.errorCode : void 0;
5767
+ const classified = numericOrNamed !== void 0 || discriminator ? frameStatusCode(numericOrNamed, discriminator) : 500;
5768
+ const statusCode = classified !== 500 ? classified : settledReason ? 400 : 502;
5769
+ const usageLimited = statusCode === 429;
5770
+ const retryAfterSeconds = usageLimited && responseRetryAfterSeconds(event) !== void 0 ? clampRetryAfterSeconds(responseRetryAfterSeconds(event)) : void 0;
5771
+ failContext(
5772
+ entry,
5773
+ ctx,
5774
+ retryAfterSeconds === void 0 ? summary : `${summary}; retry after ${retryAfterSeconds}s`,
5775
+ {
5776
+ source: "empty_failure_terminal",
5777
+ upstreamEventType: type,
5778
+ ...details,
5779
+ // Under DISTINCT keys. `failContext` fingerprints the message it was
5780
+ // given after spreading these, so an `errorMessage*` pair here is
5781
+ // overwritten by the summary's — which would silently discard the only
5782
+ // content-free evidence of what upstream actually said, and leave two
5783
+ // failures with the same type and code indistinguishable. `errorMessage*`
5784
+ // now means "what the client was told", `upstreamMessage*` means "what
5785
+ // upstream said", and both survive.
5786
+ ...diagnosticTextFingerprint("upstreamMessage", responseErrorMessage(event))
5787
+ },
5788
+ statusCode,
5789
+ retryAfterSeconds
5790
+ );
5791
+ return;
5792
+ }
5313
5793
  ctx.pendingEvents.push(event);
5314
5794
  if (isModelDataEvent(type)) flushPending(ctx);
5315
5795
  if (TERMINAL_EVENT_TYPES.has(type ?? "") || type === "error") {
@@ -5731,8 +6211,8 @@ function buildClaudeCodeBillingSystemLine() {
5731
6211
  function systemBlockText(block) {
5732
6212
  if (typeof block === "string") return block;
5733
6213
  if (block && typeof block === "object" && "text" in block) {
5734
- const text4 = block.text;
5735
- return typeof text4 === "string" ? text4 : void 0;
6214
+ const text5 = block.text;
6215
+ return typeof text5 === "string" ? text5 : void 0;
5736
6216
  }
5737
6217
  return void 0;
5738
6218
  }
@@ -5805,6 +6285,63 @@ function isCredentialBearingHeader(name) {
5805
6285
  return CREDENTIAL_BEARING_HEADER.test(name);
5806
6286
  }
5807
6287
 
6288
+ // src/model-runtime-compatibility.ts
6289
+ function remapMaxTokensField(body, field) {
6290
+ if (field === "max_tokens") {
6291
+ if (body.max_tokens === void 0 && body.max_completion_tokens !== void 0) {
6292
+ body.max_tokens = body.max_completion_tokens;
6293
+ }
6294
+ delete body.max_completion_tokens;
6295
+ return;
6296
+ }
6297
+ if (field === "max_completion_tokens") {
6298
+ if (body.max_completion_tokens === void 0 && body.max_tokens !== void 0) {
6299
+ body.max_completion_tokens = body.max_tokens;
6300
+ }
6301
+ delete body.max_tokens;
6302
+ }
6303
+ }
6304
+ function transformMessages(messages, compatibility) {
6305
+ if (!Array.isArray(messages)) return messages;
6306
+ const rewriteDeveloper = compatibility.supportsDeveloperRole === false;
6307
+ const replayReasoning = compatibility.requiresReasoningContentOnAssistantMessages === true;
6308
+ if (!rewriteDeveloper && !replayReasoning) return messages;
6309
+ let changed = false;
6310
+ const transformed = messages.map((message) => {
6311
+ if (!message || typeof message !== "object" || Array.isArray(message)) return message;
6312
+ const source = message;
6313
+ const role = source.role;
6314
+ const needsRoleRewrite = rewriteDeveloper && role === "developer";
6315
+ const needsReasoningReplay = replayReasoning && role === "assistant" && !Object.prototype.hasOwnProperty.call(source, "reasoning_content");
6316
+ if (!needsRoleRewrite && !needsReasoningReplay) return message;
6317
+ changed = true;
6318
+ return {
6319
+ ...source,
6320
+ ...needsRoleRewrite ? { role: "system" } : {},
6321
+ ...needsReasoningReplay ? { reasoning_content: "" } : {}
6322
+ };
6323
+ });
6324
+ return changed ? transformed : messages;
6325
+ }
6326
+ function transformOpenAiCompatibleRequestBody(body, compatibility) {
6327
+ const transformed = { ...body };
6328
+ if (compatibility.supportsStore === false) delete transformed.store;
6329
+ if (compatibility.supportsLongCacheRetention === false) {
6330
+ delete transformed.prompt_cache_retention;
6331
+ delete transformed.promptCacheRetention;
6332
+ }
6333
+ remapMaxTokensField(transformed, compatibility.maxTokensField);
6334
+ const messages = transformMessages(transformed.messages, compatibility);
6335
+ if (messages !== transformed.messages) transformed.messages = messages;
6336
+ const hasReasoningEffort = typeof transformed.reasoning_effort === "string" && transformed.reasoning_effort.trim().length > 0;
6337
+ if (hasReasoningEffort && compatibility.thinkingFormat === "deepseek") {
6338
+ if (transformed.thinking === void 0) transformed.thinking = { type: "enabled" };
6339
+ } else if (hasReasoningEffort && compatibility.thinkingFormat === "qwen") {
6340
+ if (transformed.enable_thinking === void 0) transformed.enable_thinking = true;
6341
+ }
6342
+ return transformed;
6343
+ }
6344
+
5808
6345
  // src/provider-factory.ts
5809
6346
  var RESPONSES_ONLY_PREFIXES = [
5810
6347
  "gpt-5-codex",
@@ -5947,7 +6484,10 @@ async function createLanguageModel(spec) {
5947
6484
  baseURL: baseURL ?? "",
5948
6485
  ...spec.authType !== "none" && apiKey.trim() ? { apiKey } : {},
5949
6486
  ...spec.authType === "none" ? { fetch: fetchWithoutCredentialHeaders } : {},
5950
- ...spec.headers ? { headers: spec.headers } : {}
6487
+ ...spec.headers ? { headers: spec.headers } : {},
6488
+ ...spec.compatibility ? {
6489
+ transformRequestBody: (body) => transformOpenAiCompatibleRequestBody(body, spec.compatibility)
6490
+ } : {}
5951
6491
  };
5952
6492
  model = createOpenAICompatible({
5953
6493
  ...options
@@ -6203,8 +6743,68 @@ function mapCodexEffortToGeminiBudget(effort) {
6203
6743
  if (!level) return void 0;
6204
6744
  return GEMINI_25_BUDGETS[level];
6205
6745
  }
6746
+ function compatibilityReasoningCapabilities(metadata) {
6747
+ const compatibility = metadata?.compatibility;
6748
+ if (!compatibility) return void 0;
6749
+ if (compatibility.supportsReasoningEffort === false) {
6750
+ return metadata?.reasoning === false ? EMPTY_REASONING : {
6751
+ ...EMPTY_REASONING,
6752
+ mode: "internal-only",
6753
+ source: "provider-metadata",
6754
+ confidence: "documented"
6755
+ };
6756
+ }
6757
+ if (compatibility.reasoningEffortMap) {
6758
+ const levels = Object.entries(compatibility.reasoningEffortMap).filter(([, mapped]) => mapped !== null).map(([level]) => level);
6759
+ if (levels.length === 0) {
6760
+ return metadata?.reasoning === false ? EMPTY_REASONING : {
6761
+ ...EMPTY_REASONING,
6762
+ mode: "internal-only",
6763
+ source: "provider-metadata",
6764
+ confidence: "documented"
6765
+ };
6766
+ }
6767
+ const preferredDefault = ["medium", "high", "max", "low"].find((level) => levels.includes(level));
6768
+ return {
6769
+ levels,
6770
+ defaultLevel: preferredDefault ?? levels[0],
6771
+ supportsSummaries: false,
6772
+ mode: "controllable",
6773
+ source: "provider-metadata",
6774
+ confidence: "documented",
6775
+ wireFormat: compatibility.thinkingFormat === "deepseek" ? { kind: "deepseek-thinking" } : { kind: "openai-reasoning-effort" }
6776
+ };
6777
+ }
6778
+ if (compatibility.supportsReasoningEffort === true || compatibility.thinkingFormat !== void 0) {
6779
+ if (metadata?.reasoning === false) return EMPTY_REASONING;
6780
+ return {
6781
+ levels: ["low", "medium", "high"],
6782
+ defaultLevel: "medium",
6783
+ supportsSummaries: false,
6784
+ mode: "controllable",
6785
+ source: "provider-metadata",
6786
+ confidence: "documented",
6787
+ wireFormat: compatibility.thinkingFormat === "deepseek" ? { kind: "deepseek-thinking" } : { kind: "openai-reasoning-effort" }
6788
+ };
6789
+ }
6790
+ return void 0;
6791
+ }
6792
+ function compatibilityReasoningEffort(effort, modelId, compatibility) {
6793
+ if (compatibility.supportsReasoningEffort === false) return void 0;
6794
+ const map = compatibility.reasoningEffortMap;
6795
+ if (map) {
6796
+ if (!Object.prototype.hasOwnProperty.call(map, effort)) return void 0;
6797
+ return map[effort] ?? void 0;
6798
+ }
6799
+ if (compatibility.supportsReasoningEffort === true || compatibility.thinkingFormat !== void 0) {
6800
+ return mapCodexEffortToOpenAI(effort, modelId);
6801
+ }
6802
+ return void 0;
6803
+ }
6206
6804
  function getReasoningCapabilities(npm, modelId, metadata) {
6207
6805
  const id = modelId.toLowerCase();
6806
+ const compatibilityCapabilities = compatibilityReasoningCapabilities(metadata);
6807
+ if (compatibilityCapabilities) return compatibilityCapabilities;
6208
6808
  if (isOpenRouterRoute(npm, metadata)) {
6209
6809
  return openRouterReasoningCapabilities(metadata);
6210
6810
  }
@@ -6350,7 +6950,10 @@ function getReasoningCapabilities(npm, modelId, metadata) {
6350
6950
  return EMPTY_REASONING;
6351
6951
  }
6352
6952
  function getPatchReasoningCapabilities(npm, modelId, metadata) {
6353
- if (metadata?.reasoning === false && !hasSupportedParameter(metadata, "reasoning_effort") && !hasSupportedParameter(metadata, "reasoning")) {
6953
+ if (metadata?.compatibility?.supportsReasoningEffort === false) {
6954
+ return compatibilityReasoningCapabilities(metadata) ?? EMPTY_REASONING;
6955
+ }
6956
+ if (metadata?.reasoning === false && !metadata?.compatibility?.reasoningEffortMap && !hasSupportedParameter(metadata, "reasoning_effort") && !hasSupportedParameter(metadata, "reasoning")) {
6354
6957
  return EMPTY_REASONING;
6355
6958
  }
6356
6959
  const capabilities = getReasoningCapabilities(npm, modelId, metadata);
@@ -6369,11 +6972,22 @@ function getPatchReasoningCapabilities(npm, modelId, metadata) {
6369
6972
  }
6370
6973
  function effortProviderOptions(npm, effort, modelId, metadata) {
6371
6974
  if (!effort) return void 0;
6975
+ if (npm === "@ai-sdk/openai-compatible" && modelId && metadata?.compatibility) {
6976
+ const reasoningEffort = compatibilityReasoningEffort(
6977
+ effort,
6978
+ modelId,
6979
+ metadata.compatibility
6980
+ );
6981
+ if (!reasoningEffort) return void 0;
6982
+ const key = metadata.providerId ? toCamelCase(metadata.providerId) : "openaiCompatible";
6983
+ return { [key]: { reasoningEffort } };
6984
+ }
6372
6985
  if (isOpenRouterRoute(npm, metadata)) {
6373
6986
  const caps = openRouterReasoningCapabilities(metadata);
6374
6987
  if (caps.mode !== "controllable") return void 0;
6375
6988
  const allowed = new Set(OPENROUTER_EFFORT_LEVELS);
6376
- const mapped = allowed.has(effort) ? effort : effort === "max" ? "xhigh" : void 0;
6989
+ const candidate = effort;
6990
+ const mapped = allowed.has(candidate) ? candidate : candidate === "max" ? "xhigh" : void 0;
6377
6991
  return mapped ? { openrouter: { reasoning: { effort: mapped, exclude: false } } } : void 0;
6378
6992
  }
6379
6993
  if (npm === "@ai-sdk/openai" || npm === "@ai-sdk/azure") {
@@ -6433,7 +7047,8 @@ function effortProviderOptions(npm, effort, modelId, metadata) {
6433
7047
  }
6434
7048
  if (hasSupportedParameter(metadata, "reasoning")) {
6435
7049
  const allowed = new Set(OPENROUTER_EFFORT_LEVELS);
6436
- const mapped = allowed.has(effort) ? effort : effort === "max" ? "xhigh" : void 0;
7050
+ const candidate = effort;
7051
+ const mapped = allowed.has(candidate) ? candidate : candidate === "max" ? "xhigh" : void 0;
6437
7052
  return mapped ? { openrouter: { reasoning: { effort: mapped, exclude: false } } } : void 0;
6438
7053
  }
6439
7054
  return void 0;
@@ -6515,7 +7130,12 @@ function isManagedCredentialAccount(account) {
6515
7130
  const base = credentialAccountBase2(account);
6516
7131
  if (!base) return false;
6517
7132
  const oauth = /^oauth:provider:(.+)$/.exec(base);
6518
- if (oauth) return isValidProviderId(oauth[1]);
7133
+ if (oauth) {
7134
+ const id = oauth[1];
7135
+ const slot = id.indexOf(":account:");
7136
+ if (slot === -1) return isValidProviderId(id);
7137
+ return isValidProviderId(id.slice(0, slot)) && OAUTH_ACCOUNT_NAME_RE.test(id.slice(slot + ":account:".length));
7138
+ }
6519
7139
  const provider = /^provider:([^:]+)(?::(.+))?$/.exec(base);
6520
7140
  if (!provider || !isValidProviderId(provider[1])) return false;
6521
7141
  const suffix = provider[2];
@@ -6667,7 +7287,7 @@ function appendError(errors, context, error) {
6667
7287
  errors.push(`${context}: ${errorMessage(error)}`);
6668
7288
  }
6669
7289
  function credentialIsReferenced(registry, authRef) {
6670
- return registry.providers.some((provider) => provider.authRef === authRef);
7290
+ return registry.providers.some((provider) => provider.authRef === authRef || provider.defaultAuthRef === authRef || Object.values(provider.authAccounts ?? {}).some((slot) => slot.authRef === authRef));
6671
7291
  }
6672
7292
  async function journalCredentialWrite(authRef) {
6673
7293
  if (!await queueCredentialDelete(authRef)) {
@@ -6856,8 +7476,8 @@ function compactLogValueWithMarker(value, max) {
6856
7476
  function systemPreview(system) {
6857
7477
  if (typeof system === "string") return compactLogValue(system, REQUEST_PREVIEW_MAX) || void 0;
6858
7478
  if (!Array.isArray(system)) return void 0;
6859
- const text4 = system.map((block) => typeof block === "string" ? block : block && typeof block === "object" && typeof block.text === "string" ? block.text : "").filter(Boolean).join(" ");
6860
- return compactLogValue(text4, REQUEST_PREVIEW_MAX) || void 0;
7479
+ const text5 = system.map((block) => typeof block === "string" ? block : block && typeof block === "object" && typeof block.text === "string" ? block.text : "").filter(Boolean).join(" ");
7480
+ return compactLogValue(text5, REQUEST_PREVIEW_MAX) || void 0;
6861
7481
  }
6862
7482
  function inlineSystemPreview(messages) {
6863
7483
  if (!Array.isArray(messages)) return void 0;
@@ -6883,9 +7503,9 @@ function getLatestMessagePreview(messages, system) {
6883
7503
  if (typeof content === "string") {
6884
7504
  summary = content;
6885
7505
  } else if (Array.isArray(content)) {
6886
- const text4 = content.filter((block) => Boolean(block && typeof block === "object")).filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join(" ");
6887
- if (text4.trim()) {
6888
- summary = text4;
7506
+ const text5 = content.filter((block) => Boolean(block && typeof block === "object")).filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join(" ");
7507
+ if (text5.trim()) {
7508
+ summary = text5;
6889
7509
  } else {
6890
7510
  const blockTypes = [...new Set(content.filter((block) => Boolean(block && typeof block === "object")).map((block) => typeof block.type === "string" ? block.type : "unknown"))];
6891
7511
  if (blockTypes.length > 0) blockSummary = `${role}: [${blockTypes.join(", ")}]`;
@@ -6981,6 +7601,7 @@ function writeInferenceRequestLog(path, entry) {
6981
7601
  ...claudeSessionId ? { claudeSessionId } : {},
6982
7602
  modelId: compactLogValue(entry.modelId),
6983
7603
  ...entry.effort ? { effort: compactLogValue(entry.effort, 100) } : {},
7604
+ ...entry.serviceTier ? { serviceTier: compactLogValue(entry.serviceTier, 40) } : {},
6984
7605
  provider: compactLogValue(entry.provider, 200),
6985
7606
  route: entry.route,
6986
7607
  ...entry.stream !== void 0 ? { stream: entry.stream } : {},
@@ -7243,6 +7864,62 @@ function parseModelList(body, npm) {
7243
7864
  }
7244
7865
  return models;
7245
7866
  }
7867
+ function materializeTemplateModel(template, model, baseUrl) {
7868
+ const npm = model.npm ?? template.npm;
7869
+ const { id, upstreamModelId: normalizedUpstream } = normalizeGoogleModelId(model.id, npm);
7870
+ const family = model.family ?? (id.split(/[-/:]/)[0] ?? id);
7871
+ const freeStatus = model.freeStatus ?? classifyFreeStatus({ model });
7872
+ return {
7873
+ ...model,
7874
+ id,
7875
+ name: normalizeGoogleDisplayName(model.name, id),
7876
+ upstreamModelId: model.upstreamModelId ?? normalizedUpstream,
7877
+ family,
7878
+ brand: model.brand ?? deriveBrand(family),
7879
+ contextWindow: model.contextWindow ?? resolveContextWindow(id),
7880
+ isFree: model.isFree ?? isFreeStatus(freeStatus),
7881
+ freeStatus,
7882
+ modelFormat: model.modelFormat ?? modelFormatForNpm(npm),
7883
+ npm,
7884
+ apiUrl: model.apiUrl ?? baseUrl
7885
+ };
7886
+ }
7887
+ function normalizeTemplateOverlay(template, model) {
7888
+ const npm = model.npm ?? template.npm;
7889
+ const { id } = normalizeGoogleModelId(model.id, npm);
7890
+ const family = model.family;
7891
+ const hasFreeMetadata = model.cost !== void 0 || model.isFree !== void 0 || model.freeStatus !== void 0;
7892
+ const freeStatus = hasFreeMetadata ? model.freeStatus ?? classifyFreeStatus({ model }) : void 0;
7893
+ return {
7894
+ ...model,
7895
+ id,
7896
+ name: normalizeGoogleDisplayName(model.name, id),
7897
+ ...model.upstreamModelId !== void 0 ? { upstreamModelId: normalizeGoogleModelId(model.upstreamModelId, npm).upstreamModelId } : {},
7898
+ ...model.npm !== void 0 ? { npm } : {},
7899
+ ...model.modelFormat !== void 0 ? { modelFormat: model.modelFormat } : model.npm !== void 0 ? { modelFormat: modelFormatForNpm(npm) } : {},
7900
+ ...family !== void 0 ? { family, brand: model.brand ?? deriveBrand(family) } : {},
7901
+ ...freeStatus !== void 0 ? {
7902
+ freeStatus,
7903
+ isFree: model.isFree ?? isFreeStatus(freeStatus)
7904
+ } : {}
7905
+ };
7906
+ }
7907
+ function applyTemplateModelMetadata(template, discovered, _baseUrl) {
7908
+ const curated = new Map(
7909
+ (template.staticModels ?? []).map((model) => normalizeTemplateOverlay(template, model)).map((model) => [model.id, model])
7910
+ );
7911
+ const visible = template.staticModelPolicy === "allowlist" ? discovered.filter((model) => curated.has(model.id)) : discovered;
7912
+ return visible.map((model) => {
7913
+ const overlay = curated.get(model.id);
7914
+ if (!overlay) return model;
7915
+ return {
7916
+ ...model,
7917
+ ...overlay,
7918
+ id: model.id,
7919
+ upstreamModelId: overlay.upstreamModelId ?? model.upstreamModelId
7920
+ };
7921
+ });
7922
+ }
7246
7923
  async function fetchTemplateModels(template, apiKey, baseUrlOverride, extraHeaders) {
7247
7924
  const trimmedOverride = baseUrlOverride?.trim();
7248
7925
  const baseUrl = (trimmedOverride || template.defaultBaseUrl)?.replace(/\/$/, "");
@@ -7254,19 +7931,7 @@ async function fetchTemplateModels(template, apiKey, baseUrlOverride, extraHeade
7254
7931
  };
7255
7932
  }
7256
7933
  if (template.modelSource === "static-seed") {
7257
- const models = (template.staticModels || []).map((sm) => {
7258
- const family = sm.id.split(/[-/:]/)[0] ?? sm.id;
7259
- return {
7260
- id: sm.id,
7261
- name: sm.name,
7262
- upstreamModelId: sm.id,
7263
- family,
7264
- brand: deriveBrand(family),
7265
- contextWindow: resolveContextWindow(sm.id),
7266
- modelFormat: modelFormatForNpm(template.npm),
7267
- npm: template.npm
7268
- };
7269
- });
7934
+ const models = (template.staticModels ?? []).map((model) => materializeTemplateModel(template, model, baseUrl));
7270
7935
  return { models, baseUrl };
7271
7936
  }
7272
7937
  const url = modelsUrl(baseUrl, template);
@@ -7336,7 +8001,7 @@ async function fetchTemplateModels(template, apiKey, baseUrlOverride, extraHeade
7336
8001
  }
7337
8002
  } catch {
7338
8003
  }
7339
- const models = parseModelList(json, template.npm);
8004
+ const models = applyTemplateModelMetadata(template, parseModelList(json, template.npm), baseUrl);
7340
8005
  if (models.length === 0) {
7341
8006
  return {
7342
8007
  models: [],
@@ -7361,6 +8026,25 @@ async function fetchTemplateModels(template, apiKey, baseUrlOverride, extraHeade
7361
8026
  }
7362
8027
 
7363
8028
  // src/registry/add-template.ts
8029
+ function existingProviderError(template, existing, replaceExisting) {
8030
+ if (!existing) return null;
8031
+ const removeFirst = `Remove it first with: clodex providers remove ${template.id}`;
8032
+ if (!replaceExisting) {
8033
+ return {
8034
+ added: false,
8035
+ error: `${template.name} is already configured.`,
8036
+ hint: removeFirst
8037
+ };
8038
+ }
8039
+ if (existing.defaultAuthRef !== void 0 || existing.activeAuthAccount !== void 0 || Object.keys(existing.authAccounts ?? {}).length > 0) {
8040
+ return {
8041
+ added: false,
8042
+ error: `${template.name} has OAuth account state and cannot be replaced in place.`,
8043
+ hint: removeFirst
8044
+ };
8045
+ }
8046
+ return null;
8047
+ }
7364
8048
  async function probeTemplatePackage(template) {
7365
8049
  if (!template.supported) return template.unsupportedReason ?? "Provider is not supported yet.";
7366
8050
  if (!template.npm) return "Template is missing an SDK package.";
@@ -7394,13 +8078,10 @@ async function addProviderFromTemplate(template, apiKey, opts) {
7394
8078
  const existingState = await withRegistryWriteLock(() => {
7395
8079
  const registry = loadRegistryStrict();
7396
8080
  const existing = registry.providers.find((p13) => p13.id === template.id);
7397
- if (existing && !opts?.replaceExisting) {
8081
+ const error = existingProviderError(template, existing, opts?.replaceExisting);
8082
+ if (error) {
7398
8083
  return {
7399
- error: {
7400
- added: false,
7401
- error: `${template.name} is already configured.`,
7402
- hint: `Remove it first with: clodex providers remove ${template.id}`
7403
- }
8084
+ error
7404
8085
  };
7405
8086
  }
7406
8087
  return { authRef: existing?.authRef ?? null, error: null };
@@ -7424,24 +8105,21 @@ async function addProviderFromTemplate(template, apiKey, opts) {
7424
8105
  }
7425
8106
  const pricingCache = loadPricingCache();
7426
8107
  const platform = pricingPlatformForProvider(template.id, template.id);
7427
- const pricedModels = enrichModelsWithPricing(
7428
- usableModels.map((m) => ({ ...m, apiUrl: fetched.baseUrl })),
7429
- buildPricingIndex(pricingCache),
7430
- platform
7431
- );
8108
+ const discoveredModels = usableModels.map((m) => ({
8109
+ ...m,
8110
+ apiUrl: m.apiUrl ?? fetched.baseUrl
8111
+ }));
8112
+ const pricedModels = template.preserveModelPricing ? discoveredModels : enrichModelsWithPricing(discoveredModels, buildPricingIndex(pricingCache), platform);
7432
8113
  const account = `provider:${template.id}`;
7433
8114
  const result = await withProviderMutationLock(template.id, async () => {
7434
8115
  const currentState = await withRegistryWriteLock(() => {
7435
8116
  const registry = loadRegistryStrict();
7436
8117
  const existing = registry.providers.find((p13) => p13.id === template.id);
7437
- if (existing && !opts?.replaceExisting) {
8118
+ const error = existingProviderError(template, existing, opts?.replaceExisting);
8119
+ if (error) {
7438
8120
  return {
7439
8121
  existingAuthRef: null,
7440
- error: {
7441
- added: false,
7442
- error: `${template.name} is already configured.`,
7443
- hint: `Remove it first with: clodex providers remove ${template.id}`
7444
- }
8122
+ error
7445
8123
  };
7446
8124
  }
7447
8125
  return { existingAuthRef: existing?.authRef ?? null, error: null };
@@ -7463,13 +8141,8 @@ async function addProviderFromTemplate(template, apiKey, opts) {
7463
8141
  return withRegistryWriteLock(async () => {
7464
8142
  const registry = loadRegistryStrict();
7465
8143
  const existing = registry.providers.find((p13) => p13.id === template.id);
7466
- if (existing && !opts?.replaceExisting) {
7467
- return {
7468
- added: false,
7469
- error: `${template.name} is already configured.`,
7470
- hint: `Remove it first with: clodex providers remove ${template.id}`
7471
- };
7472
- }
8144
+ const existingError = existingProviderError(template, existing, opts?.replaceExisting);
8145
+ if (existingError) return existingError;
7473
8146
  if ((existing?.authRef ?? null) !== currentState.existingAuthRef) {
7474
8147
  return {
7475
8148
  added: false,
@@ -7485,6 +8158,7 @@ async function addProviderFromTemplate(template, apiKey, opts) {
7485
8158
  enabled: true,
7486
8159
  authRef,
7487
8160
  authType: trimmedKey ? template.authType : "none",
8161
+ ...template.preserveModelPricing ? { preserveModelPricing: true } : {},
7488
8162
  ...!trimmedKey && template.anonymousFreeModels ? { subscriptionFilter: "free" } : {},
7489
8163
  api: {
7490
8164
  npm: template.npm,
@@ -7559,11 +8233,21 @@ async function removeProviderWithinLifecycle(id, opts) {
7559
8233
  credentialDeleted: false,
7560
8234
  error: `Provider not found: ${id}`
7561
8235
  },
7562
- authRef: null
8236
+ queuedRefs: []
7563
8237
  };
7564
8238
  }
7565
8239
  const [removedProvider] = registry.providers.splice(index, 1);
7566
- const cleanupQueued = opts?.deleteCredential !== false ? await queueCredentialDelete(removedProvider.authRef) : false;
8240
+ const queuedRefs = [];
8241
+ if (opts?.deleteCredential !== false) {
8242
+ const credentialRefs = /* @__PURE__ */ new Set([
8243
+ removedProvider.authRef,
8244
+ ...removedProvider.defaultAuthRef ? [removedProvider.defaultAuthRef] : [],
8245
+ ...Object.values(removedProvider.authAccounts ?? {}).map((slot) => slot.authRef)
8246
+ ]);
8247
+ for (const authRef of credentialRefs) {
8248
+ if (await queueCredentialDelete(authRef)) queuedRefs.push(authRef);
8249
+ }
8250
+ }
7567
8251
  saveRegistry(registry);
7568
8252
  return {
7569
8253
  result: {
@@ -7572,14 +8256,14 @@ async function removeProviderWithinLifecycle(id, opts) {
7572
8256
  name: removedProvider.name,
7573
8257
  credentialDeleted: false
7574
8258
  },
7575
- authRef: cleanupQueued ? removedProvider.authRef : null
8259
+ queuedRefs
7576
8260
  };
7577
8261
  });
7578
- if (removal.authRef) {
8262
+ if (removal.queuedRefs.length > 0) {
7579
8263
  try {
7580
8264
  const cleanup = await reconcilePendingCredentialDeletes();
7581
- removal.result.credentialDeleted = cleanup.deleted.includes(removal.authRef);
7582
- removal.result.credentialCleanupPending = cleanup.pending.includes(removal.authRef) || cleanup.persistenceError !== void 0;
8265
+ removal.result.credentialDeleted = removal.queuedRefs.every((ref) => cleanup.deleted.includes(ref));
8266
+ removal.result.credentialCleanupPending = removal.queuedRefs.some((ref) => cleanup.pending.includes(ref)) || cleanup.persistenceError !== void 0;
7583
8267
  } catch {
7584
8268
  removal.result.credentialCleanupPending = true;
7585
8269
  }
@@ -7590,6 +8274,60 @@ async function removeProviderWithinLifecycle(id, opts) {
7590
8274
  async function removeProviderFromRegistry(id, opts) {
7591
8275
  return withProviderMutationLock(id, () => removeProviderWithinLifecycle(id, opts));
7592
8276
  }
8277
+ async function setActiveOAuthAccount(id, account) {
8278
+ return withProviderMutationLock(id, () => withRegistryWriteLock(() => {
8279
+ const registry = loadRegistryStrict();
8280
+ const provider = registry.providers.find((p13) => p13.id === id);
8281
+ if (!provider) return { updated: false, error: `Provider not found: ${id}` };
8282
+ const name = account?.trim().toLowerCase();
8283
+ const previous = provider.activeAuthAccount?.trim() || void 0;
8284
+ const selectedSlot = name ? getOAuthAccountSlot(provider, name) : void 0;
8285
+ let selectionChanged = previous !== name;
8286
+ let storageChanged = false;
8287
+ if (name && !selectedSlot) {
8288
+ const slots = provider.authAccounts ?? {};
8289
+ const available = Object.keys(slots).sort().join(", ") || "none";
8290
+ return {
8291
+ updated: false,
8292
+ error: `${provider.name} has no account named "${name}" (available: ${available}).`
8293
+ };
8294
+ }
8295
+ if (name && provider.authType === "oauth") {
8296
+ storageChanged = storeActiveOAuthAccount(provider, name, selectedSlot.authRef);
8297
+ }
8298
+ if (selectionChanged && provider.authType === "oauth") {
8299
+ const previousSlot = previous ? getOAuthAccountSlot(provider, previous) : void 0;
8300
+ if (previous && previousSlot && provider.modelsCache && provider.authAccounts) {
8301
+ provider.authAccounts[previous] = {
8302
+ ...previousSlot,
8303
+ modelsCache: provider.modelsCache
8304
+ };
8305
+ }
8306
+ const selectedCache = selectedSlot?.modelsCache;
8307
+ if (selectedCache) {
8308
+ provider.modelsCache = selectedCache;
8309
+ provider.refreshedAt = selectedCache.fetchedAt;
8310
+ } else {
8311
+ delete provider.modelsCache;
8312
+ delete provider.refreshedAt;
8313
+ }
8314
+ }
8315
+ if (name && provider.authType !== "oauth") {
8316
+ storageChanged = clearActiveOAuthAccount(provider) || storageChanged;
8317
+ if (provider.activeAuthAccount !== name) {
8318
+ provider.activeAuthAccount = name;
8319
+ storageChanged = true;
8320
+ }
8321
+ } else if (!name && (previous !== void 0 || provider.defaultAuthRef !== void 0)) {
8322
+ storageChanged = clearActiveOAuthAccount(provider);
8323
+ } else if (!name) {
8324
+ selectionChanged = false;
8325
+ }
8326
+ const migrationNeedsPersistence = registry.schemaVersion < REGISTRY_SCHEMA_VERSION_WITH_MATERIALIZED_ACTIVE_ACCOUNT && provider.defaultAuthRef !== void 0;
8327
+ if (selectionChanged || storageChanged || migrationNeedsPersistence) saveRegistry(registry);
8328
+ return { updated: true, changed: selectionChanged, ...name ? { account: name } : {}, provider };
8329
+ }));
8330
+ }
7593
8331
  function toggleProviderEnabled(id) {
7594
8332
  return withRegistryWriteLockSync(() => {
7595
8333
  const registry = loadRegistryStrict();
@@ -7853,6 +8591,41 @@ function resolveModelSource(provider) {
7853
8591
  }
7854
8592
 
7855
8593
  // src/registry/refresh-credentials.ts
8594
+ function refreshCredentialSnapshot(provider, selected = process.env[OAUTH_ACCOUNT_ENV], options = {}) {
8595
+ const environmentAccount = selected === null ? void 0 : selected?.trim().toLowerCase() || void 0;
8596
+ const effective = projectSelectedOAuthAccount(provider, environmentAccount ?? "");
8597
+ const activeAuthAccount = provider.activeAuthAccount?.trim() || void 0;
8598
+ const selectedName = environmentAccount || activeAuthAccount;
8599
+ const selectedAccount = provider.authType === "oauth" && selectedName ? getOAuthAccountSlot(provider, selectedName) : void 0;
8600
+ const credentialOverride = effective.authType !== "none" && effective.authRef !== "none:anonymous" ? resolveProviderCredentialOverrideState(effective.id, process.env, {
8601
+ ignoreProviderOverride: options.ignoreProviderOverride
8602
+ }) : null;
8603
+ return {
8604
+ provider: {
8605
+ id: provider.id,
8606
+ addedAt: provider.addedAt,
8607
+ enabled: provider.enabled,
8608
+ authType: provider.authType,
8609
+ templateId: provider.templateId,
8610
+ api: {
8611
+ ...provider.api,
8612
+ ...provider.api.headers ? { headers: { ...provider.api.headers } } : {}
8613
+ }
8614
+ },
8615
+ authRef: effective.authRef,
8616
+ ...activeAuthAccount ? { activeAuthAccount } : {},
8617
+ ...environmentAccount ? { environmentAccount } : {},
8618
+ ...selectedName && selectedAccount ? {
8619
+ selectedAccount: {
8620
+ name: selectedName,
8621
+ authRef: selectedAccount.authRef,
8622
+ addedAt: selectedAccount.addedAt
8623
+ }
8624
+ } : {},
8625
+ ...credentialOverride ? { credentialOverride } : {},
8626
+ ...options.ignoreProviderOverride ? { ignoreProviderOverride: true } : {}
8627
+ };
8628
+ }
7856
8629
  var PLACEHOLDER_KEYS = /* @__PURE__ */ new Set([
7857
8630
  "anything",
7858
8631
  "local",
@@ -7892,20 +8665,36 @@ function skipWithCachedModels(provider, reason) {
7892
8665
  reason
7893
8666
  };
7894
8667
  }
7895
- async function resolveRefreshCredential(provider, resolveKey) {
7896
- if (isAnonymousProvider(provider)) return null;
7897
- let key;
8668
+ async function resolveRefreshCredentialWithSource(provider, resolveKey, selected = process.env[OAUTH_ACCOUNT_ENV], options = {}) {
8669
+ const effectiveProvider = projectSelectedOAuthAccount(provider, selected ?? "");
8670
+ if (isAnonymousProvider(effectiveProvider) || effectiveProvider.authType === "none" || effectiveProvider.authRef === "none:anonymous") return { credential: null };
8671
+ let resolved;
7898
8672
  try {
7899
- key = await resolveKey(provider);
8673
+ const result = await resolveKey(effectiveProvider);
8674
+ resolved = typeof result === "object" && result !== null ? result : {
8675
+ credential: result,
8676
+ // Backwards-compatible resolvers return only the key. Attribute the
8677
+ // current usable override for them; production resolvers return the
8678
+ // source atomically with the credential and close this race fully.
8679
+ ...result ? {
8680
+ credentialOverride: resolveProviderCredentialOverrideState(
8681
+ effectiveProvider.id,
8682
+ process.env,
8683
+ { ignoreProviderOverride: options.ignoreProviderOverride }
8684
+ ) ?? void 0
8685
+ } : {}
8686
+ };
7900
8687
  } catch {
7901
- key = null;
8688
+ resolved = { credential: null };
7902
8689
  }
7903
- if (!isLikelyPlaceholderKey(key)) return key;
7904
- for (const envVar of ENV_FALLBACK_BY_PROVIDER[provider.id] ?? []) {
8690
+ if (resolved.credentialOverride || !isLikelyPlaceholderKey(resolved.credential)) {
8691
+ return resolved;
8692
+ }
8693
+ for (const envVar of ENV_FALLBACK_BY_PROVIDER[effectiveProvider.id] ?? []) {
7905
8694
  const fromEnv = process.env[envVar]?.trim();
7906
- if (fromEnv && !isLikelyPlaceholderKey(fromEnv)) return fromEnv;
8695
+ if (fromEnv && !isLikelyPlaceholderKey(fromEnv)) return { credential: fromEnv };
7907
8696
  }
7908
- return key;
8697
+ return resolved;
7909
8698
  }
7910
8699
 
7911
8700
  // src/data/openai-oauth-models.ts
@@ -8055,10 +8844,13 @@ async function refreshOpenAiOAuthModels(accessToken) {
8055
8844
  if (chatGptEntries.length > 0) {
8056
8845
  return { models: toModels(chatGptEntries), source: "live" };
8057
8846
  }
8847
+ const failures = [codexResult.error, chatGptResult.error].filter((error) => error !== void 0);
8848
+ const credentialFailure = failures.find((error) => /(?:\brejected\b|\b401\b|\b403\b)/i.test(error));
8058
8849
  return {
8059
8850
  models: [...seedById.values()],
8060
8851
  source: "seed",
8061
- failureReason: chatGptResult.error ?? codexResult.error
8852
+ failureReason: credentialFailure ?? chatGptResult.error ?? codexResult.error,
8853
+ credentialRejected: credentialFailure !== void 0
8062
8854
  };
8063
8855
  }
8064
8856
  async function refreshApiListProvider(provider, apiKey) {
@@ -8087,7 +8879,7 @@ async function refreshApiListProvider(provider, apiKey) {
8087
8879
  return { models: [], error: fetched2.error ?? "No models returned.", baseUrl: fetched2.baseUrl };
8088
8880
  }
8089
8881
  return {
8090
- models: fetched2.models.map((m) => ({ ...m, apiUrl: fetched2.baseUrl })),
8882
+ models: fetched2.models.map((m) => ({ ...m, apiUrl: m.apiUrl ?? fetched2.baseUrl })),
8091
8883
  baseUrl: fetched2.baseUrl
8092
8884
  };
8093
8885
  }
@@ -8106,37 +8898,116 @@ async function refreshApiListProvider(provider, apiKey) {
8106
8898
  return {
8107
8899
  models: usableModels.map((m) => ({
8108
8900
  ...m,
8109
- apiUrl: fetched.baseUrl
8901
+ apiUrl: m.apiUrl ?? fetched.baseUrl
8110
8902
  })),
8111
8903
  baseUrl: fetched.baseUrl
8112
8904
  };
8113
8905
  }
8114
- function updateProviderCache(registry, providerId, models, baseUrl) {
8906
+ function updateProviderCache(registry, providerId, models, baseUrl, credentialSnapshot) {
8115
8907
  const idx = registry.providers.findIndex((p13) => p13.id === providerId);
8116
8908
  if (idx < 0) return;
8117
8909
  const now = (/* @__PURE__ */ new Date()).toISOString();
8118
8910
  const existing = registry.providers[idx];
8911
+ const modelsCache = { fetchedAt: now, models };
8912
+ const selectedAccount = credentialSnapshot?.selectedAccount;
8913
+ const temporaryAccount = isTemporaryAccountSelection(credentialSnapshot);
8914
+ const selectedSlot = selectedAccount ? getOAuthAccountSlot(existing, selectedAccount.name) : void 0;
8915
+ const authAccounts = selectedAccount && selectedSlot ? {
8916
+ ...existing.authAccounts,
8917
+ [selectedAccount.name]: {
8918
+ ...selectedSlot,
8919
+ modelsCache
8920
+ }
8921
+ } : existing.authAccounts;
8119
8922
  registry.providers[idx] = {
8120
8923
  ...existing,
8121
- refreshedAt: now,
8122
8924
  api: baseUrl ? { ...existing.api, url: baseUrl } : existing.api,
8123
- modelsCache: {
8124
- fetchedAt: now,
8125
- models
8126
- }
8925
+ ...authAccounts ? { authAccounts } : {},
8926
+ ...!temporaryAccount ? { refreshedAt: now, modelsCache } : {}
8127
8927
  };
8128
8928
  }
8929
+ function isTemporaryAccountSelection(snapshot) {
8930
+ return Boolean(
8931
+ snapshot?.environmentAccount && snapshot.selectedAccount && snapshot.environmentAccount !== snapshot.activeAuthAccount
8932
+ );
8933
+ }
8934
+ function providerWithRefreshCache(provider, snapshot) {
8935
+ const selected = snapshot?.selectedAccount;
8936
+ const temporary = isTemporaryAccountSelection(snapshot);
8937
+ if (!temporary || !selected) return provider;
8938
+ const projected = { ...provider };
8939
+ const cache = getOAuthAccountSlot(provider, selected.name)?.modelsCache;
8940
+ if (cache) projected.modelsCache = cache;
8941
+ else delete projected.modelsCache;
8942
+ return projected;
8943
+ }
8129
8944
  function providerDiscoveryInputsMatch(current, started) {
8130
- return current.authRef === started.authRef && current.authType === started.authType && current.templateId === started.templateId && isDeepStrictEqual(current.api, started.api);
8945
+ return current.authRef === started.authRef && current.enabled === started.enabled && current.authType === started.authType && current.templateId === started.templateId && isDeepStrictEqual(current.api, started.api);
8946
+ }
8947
+ function assertRefreshCredentialStillCurrent(current, snapshot) {
8948
+ const routing = snapshot.provider;
8949
+ if (current.id !== routing.id || current.addedAt !== routing.addedAt || current.enabled !== routing.enabled || current.authType !== routing.authType || current.templateId !== routing.templateId || !isDeepStrictEqual(current.api, routing.api)) {
8950
+ throw new Error("Provider configuration changed while credentials were resolving.");
8951
+ }
8952
+ const activeAuthAccount = current.activeAuthAccount?.trim() || void 0;
8953
+ if (activeAuthAccount !== snapshot.activeAuthAccount) {
8954
+ throw new Error("Provider account selection changed while models were refreshing.");
8955
+ }
8956
+ let currentSnapshot;
8957
+ try {
8958
+ currentSnapshot = refreshCredentialSnapshot(
8959
+ current,
8960
+ snapshot.environmentAccount ?? null,
8961
+ { ignoreProviderOverride: snapshot.ignoreProviderOverride }
8962
+ );
8963
+ } catch {
8964
+ throw new Error("Provider account selection changed while models were refreshing.");
8965
+ }
8966
+ if (currentSnapshot.authRef !== snapshot.authRef) {
8967
+ throw new Error("Provider credentials changed while models were refreshing.");
8968
+ }
8969
+ if (!isDeepStrictEqual(currentSnapshot.selectedAccount, snapshot.selectedAccount)) {
8970
+ throw new Error("Provider account credentials changed while models were refreshing.");
8971
+ }
8972
+ if (!isDeepStrictEqual(currentSnapshot.credentialOverride, snapshot.credentialOverride)) {
8973
+ throw new Error("Provider credential override changed while models were refreshing.");
8974
+ }
8131
8975
  }
8132
- async function refreshProviderModels(providerId, apiKey, registry) {
8976
+ async function refreshProviderModels(providerId, apiKey, registry, credentialSnapshot) {
8133
8977
  const workingRegistry = registry ?? loadRegistryStrict();
8134
8978
  const provider = workingRegistry.providers.find((p13) => p13.id === providerId);
8135
8979
  if (!provider) {
8136
8980
  return { id: providerId, name: providerId, ok: false, reason: "Provider not found." };
8137
8981
  }
8982
+ if (credentialSnapshot) {
8983
+ try {
8984
+ assertRefreshCredentialStillCurrent(provider, credentialSnapshot);
8985
+ } catch (err) {
8986
+ return {
8987
+ id: provider.id,
8988
+ name: provider.name,
8989
+ ok: false,
8990
+ reason: err instanceof Error ? err.message : String(err)
8991
+ };
8992
+ }
8993
+ }
8994
+ const cacheProvider = providerWithRefreshCache(provider, credentialSnapshot);
8995
+ if (credentialSnapshot?.credentialOverride) {
8996
+ return skipWithCachedModels(
8997
+ cacheProvider,
8998
+ `${credentialSnapshot.credentialOverride.variable} is a process-scoped provider credential override \u2014 skipped the persistent model refresh so another shell cannot inherit this credential's catalog.`
8999
+ );
9000
+ }
8138
9001
  const source = resolveModelSource(provider);
8139
9002
  if (source === "manual-only") {
9003
+ if (provider.authType !== "none" && !apiKey) {
9004
+ return {
9005
+ id: provider.id,
9006
+ name: provider.name,
9007
+ ok: false,
9008
+ reason: provider.authType === "oauth" ? "OAuth token not available \u2014 try signing in again with clodex providers auth." : "API key not available \u2014 cannot verify the saved model catalog."
9009
+ };
9010
+ }
8140
9011
  return {
8141
9012
  id: provider.id,
8142
9013
  name: provider.name,
@@ -8146,7 +9017,8 @@ async function refreshProviderModels(providerId, apiKey, registry) {
8146
9017
  };
8147
9018
  }
8148
9019
  try {
8149
- const previousModelCount = provider.modelsCache?.models.length ?? 0;
9020
+ const previousModelCount = cacheProvider.modelsCache?.models.length ?? 0;
9021
+ const hadPreviousRefresh = isTemporaryAccountSelection(credentialSnapshot) ? cacheProvider.modelsCache !== void 0 : provider.refreshedAt !== void 0;
8150
9022
  let models = [];
8151
9023
  let baseUrl;
8152
9024
  let oauthFallbackReason;
@@ -8159,11 +9031,21 @@ async function refreshProviderModels(providerId, apiKey, registry) {
8159
9031
  reason: "OAuth token not available \u2014 try signing in again with clodex providers auth."
8160
9032
  };
8161
9033
  }
8162
- const oauthResult = await refreshOAuthProvider(provider, apiKey);
9034
+ const oauthResult = await refreshOAuthProvider(cacheProvider, apiKey);
8163
9035
  const failureDetail = oauthResult.failureReason ? ` (${oauthResult.failureReason})` : "";
8164
- if (oauthResult.source === "seed" && cachedModelCount(provider) > 0) {
9036
+ if (oauthResult.source === "seed" && oauthResult.credentialRejected) {
9037
+ const count = cachedModelCount(cacheProvider);
9038
+ return {
9039
+ id: provider.id,
9040
+ name: provider.name,
9041
+ ok: false,
9042
+ ...count > 0 ? { modelCount: count } : {},
9043
+ reason: `OAuth credential was rejected${failureDetail}. ` + (count > 0 ? `Kept ${count} cached model${count === 1 ? "" : "s"}, but sign in again before launching.` : "Sign in again before refreshing or launching.")
9044
+ };
9045
+ }
9046
+ if (oauthResult.source === "seed" && cachedModelCount(cacheProvider) > 0) {
8165
9047
  return skipWithCachedModels(
8166
- provider,
9048
+ cacheProvider,
8167
9049
  `Live model discovery failed${failureDetail} \u2014 kept your existing cached model list instead of overwriting it with clodex's built-in fallback list. Try refreshing again later.`
8168
9050
  );
8169
9051
  }
@@ -8184,11 +9066,21 @@ async function refreshProviderModels(providerId, apiKey, registry) {
8184
9066
  const keyOptional = template?.apiKeyOptional === true;
8185
9067
  const effectiveKey = keyOptional && isLikelyPlaceholderKey(apiKey) ? "" : apiKey;
8186
9068
  if (!keyOptional && isLikelyPlaceholderKey(effectiveKey)) {
8187
- if (cachedModelCount(provider) > 0) {
8188
- return skipWithCachedModels(
8189
- provider,
8190
- "A placeholder API key is configured \u2014 kept cached model list. Add this provider again via clodex providers add with a real key to refresh live."
8191
- );
9069
+ if (cachedModelCount(cacheProvider) > 0) {
9070
+ if (isLegacyAnonymousCustomEndpoint(provider, effectiveKey)) {
9071
+ return skipWithCachedModels(
9072
+ cacheProvider,
9073
+ "Legacy anonymous custom endpoint \u2014 kept cached model list."
9074
+ );
9075
+ }
9076
+ const count = cachedModelCount(cacheProvider);
9077
+ return {
9078
+ id: provider.id,
9079
+ name: provider.name,
9080
+ ok: false,
9081
+ modelCount: count,
9082
+ reason: `A placeholder API key is configured \u2014 kept ${count} cached model${count === 1 ? "" : "s"}, but add this provider again with a real key before launching.`
9083
+ };
8192
9084
  }
8193
9085
  return {
8194
9086
  id: provider.id,
@@ -8207,11 +9099,15 @@ async function refreshProviderModels(providerId, apiKey, registry) {
8207
9099
  }
8208
9100
  const fetched = await refreshApiListProvider(provider, effectiveKey ?? "");
8209
9101
  if (fetched.error) {
8210
- if ((fetched.error.includes("rejected") || fetched.error.includes("401") || fetched.error.includes("403")) && cachedModelCount(provider) > 0) {
8211
- return skipWithCachedModels(
8212
- provider,
8213
- `${fetched.error} Kept ${cachedModelCount(provider)} cached model${cachedModelCount(provider) === 1 ? "" : "s"} from import. Update your API key via clodex providers add if you need a live refresh.`
8214
- );
9102
+ if ((fetched.error.includes("rejected") || fetched.error.includes("401") || fetched.error.includes("403")) && cachedModelCount(cacheProvider) > 0) {
9103
+ const count = cachedModelCount(cacheProvider);
9104
+ return {
9105
+ id: provider.id,
9106
+ name: provider.name,
9107
+ ok: false,
9108
+ modelCount: count,
9109
+ reason: `${fetched.error} Kept ${count} cached model${count === 1 ? "" : "s"} from import, but update the API key before launching.`
9110
+ };
8215
9111
  }
8216
9112
  return { id: provider.id, name: provider.name, ok: false, reason: fetched.error };
8217
9113
  }
@@ -8220,18 +9116,21 @@ async function refreshProviderModels(providerId, apiKey, registry) {
8220
9116
  }
8221
9117
  const pricingCache = loadPricingCache();
8222
9118
  const platform = pricingPlatformForProvider(provider.templateId, provider.id);
8223
- const enriched = enrichModelsWithPricing(models, buildPricingIndex(pricingCache), platform);
9119
+ const enriched = provider.preserveModelPricing ? models : enrichModelsWithPricing(models, buildPricingIndex(pricingCache), platform);
8224
9120
  await withRegistryWriteLock(() => {
8225
9121
  const currentRegistry = loadRegistryStrict();
8226
9122
  const currentProvider = currentRegistry.providers.find((candidate) => candidate.id === providerId);
8227
9123
  if (!currentProvider) throw new Error("Provider was removed while models were refreshing.");
9124
+ if (credentialSnapshot) {
9125
+ assertRefreshCredentialStillCurrent(currentProvider, credentialSnapshot);
9126
+ }
8228
9127
  if (currentProvider.authRef !== provider.authRef) {
8229
9128
  throw new Error("Provider credentials changed while models were refreshing.");
8230
9129
  }
8231
9130
  if (!providerDiscoveryInputsMatch(currentProvider, provider)) {
8232
9131
  throw new Error("Provider configuration changed while models were refreshing.");
8233
9132
  }
8234
- updateProviderCache(currentRegistry, providerId, enriched, baseUrl);
9133
+ updateProviderCache(currentRegistry, providerId, enriched, baseUrl, credentialSnapshot);
8235
9134
  saveRegistry(currentRegistry);
8236
9135
  });
8237
9136
  enrichPricingAsync();
@@ -8240,7 +9139,7 @@ async function refreshProviderModels(providerId, apiKey, registry) {
8240
9139
  name: provider.name,
8241
9140
  ok: true,
8242
9141
  modelCount: enriched.length,
8243
- previousModelCount: provider.refreshedAt ? previousModelCount : void 0,
9142
+ previousModelCount: hadPreviousRefresh ? previousModelCount : void 0,
8244
9143
  reason: oauthFallbackReason
8245
9144
  };
8246
9145
  } catch (err) {
@@ -8252,13 +9151,63 @@ async function refreshProviderModels(providerId, apiKey, registry) {
8252
9151
  };
8253
9152
  }
8254
9153
  }
9154
+ async function refreshProviderModelsWithCredential(providerId, resolveKey, selected = process.env[OAUTH_ACCOUNT_ENV], options = {}) {
9155
+ return withProviderMutationLock(providerId, async () => {
9156
+ const provider = loadRegistryStrict().providers.find((candidate) => candidate.id === providerId);
9157
+ if (!provider) {
9158
+ return { id: providerId, name: providerId, ok: false, reason: "Provider not found." };
9159
+ }
9160
+ if (options.requireEnabled && !provider.enabled) {
9161
+ return {
9162
+ id: provider.id,
9163
+ name: provider.name,
9164
+ ok: true,
9165
+ skipped: true,
9166
+ reason: "Provider was disabled before its model refresh began."
9167
+ };
9168
+ }
9169
+ const accountOverride = selected === null ? null : selected ?? process.env[OAUTH_ACCOUNT_ENV] ?? null;
9170
+ const snapshot = refreshCredentialSnapshot(provider, accountOverride, {
9171
+ ignoreProviderOverride: options.ignoreProviderOverride
9172
+ });
9173
+ const resolved = await resolveRefreshCredentialWithSource(
9174
+ provider,
9175
+ resolveKey,
9176
+ accountOverride,
9177
+ { ignoreProviderOverride: options.ignoreProviderOverride }
9178
+ );
9179
+ if (!isDeepStrictEqual(resolved.credentialOverride, snapshot.credentialOverride)) {
9180
+ return {
9181
+ id: provider.id,
9182
+ name: provider.name,
9183
+ ok: false,
9184
+ reason: "Provider credential override changed while models were refreshing."
9185
+ };
9186
+ }
9187
+ return refreshProviderModels(provider.id, resolved.credential, void 0, snapshot);
9188
+ });
9189
+ }
8255
9190
  async function refreshAllProviderModels(resolveKey) {
8256
9191
  const refreshed = [];
8257
9192
  const registry = loadRegistryStrict();
8258
9193
  const enabledProviders = registry.providers.filter((p13) => p13.enabled);
8259
9194
  for (const provider of enabledProviders) {
8260
- const key = await resolveRefreshCredential(provider, resolveKey);
8261
- refreshed.push(await refreshProviderModels(provider.id, key));
9195
+ const accountOverride = process.env[OAUTH_ACCOUNT_ENV] ?? null;
9196
+ try {
9197
+ refreshed.push(await refreshProviderModelsWithCredential(
9198
+ provider.id,
9199
+ resolveKey,
9200
+ accountOverride,
9201
+ { requireEnabled: true }
9202
+ ));
9203
+ } catch (err) {
9204
+ refreshed.push({
9205
+ id: provider.id,
9206
+ name: provider.name,
9207
+ ok: false,
9208
+ reason: err instanceof Error ? err.message : String(err)
9209
+ });
9210
+ }
8262
9211
  }
8263
9212
  return { refreshed };
8264
9213
  }
@@ -8267,6 +9216,15 @@ async function refreshAllProviderModels(resolveKey) {
8267
9216
  import pc3 from "picocolors";
8268
9217
  import * as p2 from "@clack/prompts";
8269
9218
  import open from "open";
9219
+ function validateOAuthAccountName(name) {
9220
+ const trimmed = name.trim().toLowerCase();
9221
+ if (!OAUTH_ACCOUNT_NAME_RE.test(trimmed)) {
9222
+ throw new Error(
9223
+ `Invalid account name "${name}" \u2014 use 1-32 characters: lowercase letters, digits, "-" or "_", starting with a letter or digit.`
9224
+ );
9225
+ }
9226
+ return trimmed;
9227
+ }
8270
9228
  var OPENAI_DISPLAY = "OpenAI ChatGPT Plus/Pro";
8271
9229
  var PROVIDER_DISPLAY = {
8272
9230
  openai: OPENAI_DISPLAY,
@@ -8296,6 +9254,47 @@ async function runNativeDeviceCode(providerId) {
8296
9254
  throw err;
8297
9255
  }
8298
9256
  }
9257
+ async function upsertOAuthAccountSlot(registryId, account, authRef, expectedAuthRef) {
9258
+ return withRegistryWriteLock(async () => {
9259
+ const registry = loadRegistryStrict();
9260
+ const entry = registry.providers.find((pr) => pr.id === registryId);
9261
+ if (!entry) {
9262
+ throw new Error(
9263
+ `Provider "${registryId}" is not configured yet \u2014 run the default sign-in first: clodex providers auth openai`
9264
+ );
9265
+ }
9266
+ const previousAuthRef = getOAuthAccountSlot(entry, account)?.authRef;
9267
+ if (previousAuthRef !== expectedAuthRef) {
9268
+ throw new Error(`Account "${account}" of "${registryId}" changed while its credential was being saved`);
9269
+ }
9270
+ const updated = {
9271
+ ...entry,
9272
+ authAccounts: {
9273
+ ...entry.authAccounts,
9274
+ [account]: {
9275
+ authRef,
9276
+ addedAt: (/* @__PURE__ */ new Date()).toISOString()
9277
+ }
9278
+ }
9279
+ };
9280
+ if (entry.activeAuthAccount === account) {
9281
+ storeActiveOAuthAccount(updated, account, authRef);
9282
+ delete updated.modelsCache;
9283
+ delete updated.refreshedAt;
9284
+ }
9285
+ const idx = registry.providers.findIndex((provider) => provider.id === registryId);
9286
+ registry.providers[idx] = updated;
9287
+ if (previousAuthRef && previousAuthRef !== authRef) {
9288
+ await queueCredentialDelete(previousAuthRef);
9289
+ }
9290
+ saveRegistry(registry);
9291
+ try {
9292
+ await cancelCredentialDelete(authRef);
9293
+ } catch {
9294
+ }
9295
+ return updated;
9296
+ });
9297
+ }
8299
9298
  function oauthDisplayName(registryId, fallbackName) {
8300
9299
  if (registryId === "openai-oauth") return "OpenAI (ChatGPT)";
8301
9300
  return fallbackName;
@@ -8307,7 +9306,7 @@ async function upsertOAuthProvider(providerId, authRef, expectedAuthRef) {
8307
9306
  const registry = loadRegistryStrict();
8308
9307
  const template = getTemplateById(templateId);
8309
9308
  let entry = registry.providers.find((pr) => pr.id === registryId);
8310
- if (entry?.authRef !== expectedAuthRef) {
9309
+ if ((entry ? providerDefaultAuthRef(entry) : void 0) !== expectedAuthRef) {
8311
9310
  throw new Error(`Provider "${registryId}" changed while its credential was being saved`);
8312
9311
  }
8313
9312
  if (!entry) {
@@ -8315,7 +9314,7 @@ async function upsertOAuthProvider(providerId, authRef, expectedAuthRef) {
8315
9314
  throw new Error(`Provider "${providerId}" is not in your registry and has no template`);
8316
9315
  }
8317
9316
  }
8318
- const previousAuthRef = entry?.authRef;
9317
+ const previousAuthRef = entry ? providerDefaultAuthRef(entry) : void 0;
8319
9318
  if (!entry) {
8320
9319
  if (!template) throw new Error(`Provider "${providerId}" has no template`);
8321
9320
  const displayName = oauthDisplayName(registryId, template.name);
@@ -8333,8 +9332,34 @@ async function upsertOAuthProvider(providerId, authRef, expectedAuthRef) {
8333
9332
  },
8334
9333
  addedAt: (/* @__PURE__ */ new Date()).toISOString()
8335
9334
  };
9335
+ } else if (entry.activeAuthAccount) {
9336
+ const selected = getOAuthAccountSlot(entry, entry.activeAuthAccount);
9337
+ if (!selected) {
9338
+ throw new Error(
9339
+ `Provider "${registryId}" is set to use account "${entry.activeAuthAccount}", which no longer exists`
9340
+ );
9341
+ }
9342
+ entry = {
9343
+ ...entry,
9344
+ authType: "oauth",
9345
+ authRef: selected.authRef,
9346
+ defaultAuthRef: authRef,
9347
+ templateId
9348
+ };
9349
+ delete entry.defaultModelsCache;
9350
+ if (selected.modelsCache) {
9351
+ entry.modelsCache = selected.modelsCache;
9352
+ entry.refreshedAt = selected.modelsCache.fetchedAt;
9353
+ } else {
9354
+ delete entry.modelsCache;
9355
+ delete entry.refreshedAt;
9356
+ }
8336
9357
  } else {
8337
9358
  entry = { ...entry, authType: "oauth", authRef, templateId };
9359
+ delete entry.defaultAuthRef;
9360
+ delete entry.defaultModelsCache;
9361
+ delete entry.modelsCache;
9362
+ delete entry.refreshedAt;
8338
9363
  }
8339
9364
  const idx = registry.providers.findIndex((provider) => provider.id === registryId);
8340
9365
  if (idx >= 0) registry.providers[idx] = entry;
@@ -8350,9 +9375,9 @@ async function upsertOAuthProvider(providerId, authRef, expectedAuthRef) {
8350
9375
  return entry;
8351
9376
  });
8352
9377
  }
8353
- async function persistNativeOAuthCredential(providerId, cred) {
9378
+ async function persistNativeOAuthCredential(providerId, cred, accountName) {
8354
9379
  const registryId = toOAuthRegistryId(providerId);
8355
- const account = `oauth:provider:${registryId}`;
9380
+ const account = accountName ? `oauth:provider:${registryId}:account:${accountName}` : `oauth:provider:${registryId}`;
8356
9381
  const registryProvider = await withProviderMutationLock(registryId, async () => {
8357
9382
  const existingAuthRef = await withRegistryWriteLock(
8358
9383
  () => {
@@ -8362,7 +9387,17 @@ async function persistNativeOAuthCredential(providerId, cred) {
8362
9387
  if (!existing && !getTemplateById(templateId)) {
8363
9388
  throw new Error(`Provider "${providerId}" is not in your registry and has no template`);
8364
9389
  }
8365
- return existing?.authRef;
9390
+ if (accountName && !existing) {
9391
+ throw new Error(
9392
+ `Provider "${registryId}" is not configured yet \u2014 run the default sign-in first: clodex providers auth openai`
9393
+ );
9394
+ }
9395
+ if (accountName && existing?.authType !== "oauth") {
9396
+ throw new Error(
9397
+ `Provider "${registryId}" does not currently have a default OAuth sign-in \u2014 run clodex providers auth openai without --account first.`
9398
+ );
9399
+ }
9400
+ return accountName ? existing ? getOAuthAccountSlot(existing, accountName)?.authRef : void 0 : existing ? providerDefaultAuthRef(existing) : void 0;
8366
9401
  }
8367
9402
  );
8368
9403
  const authRef = credentialInstanceAuthRef(account);
@@ -8379,7 +9414,7 @@ async function persistNativeOAuthCredential(providerId, cred) {
8379
9414
  `Could not save OAuth tokens to the credential store${diagMsg ? ` \u2014 ${diagMsg}` : " \u2014 check access and try again"}`
8380
9415
  );
8381
9416
  }
8382
- return upsertOAuthProvider(providerId, authRef, existingAuthRef);
9417
+ return accountName ? upsertOAuthAccountSlot(registryId, accountName, authRef, existingAuthRef) : upsertOAuthProvider(providerId, authRef, existingAuthRef);
8383
9418
  });
8384
9419
  });
8385
9420
  let credentialCleanupPending = true;
@@ -8394,11 +9429,25 @@ async function persistNativeOAuthCredential(providerId, cred) {
8394
9429
  credentialCleanupPending
8395
9430
  };
8396
9431
  }
8397
- async function authenticateProvider(providerId, _options = {}) {
9432
+ async function authenticateProvider(providerId, options = {}) {
8398
9433
  const registryId = toOAuthRegistryId(providerId);
9434
+ const accountName = options.account === void 0 ? void 0 : validateOAuthAccountName(options.account);
8399
9435
  if (!supportsNativeOAuth(providerId)) {
8400
9436
  throw new Error("OAuth sign-in is only available for openai (ChatGPT Plus/Pro).");
8401
9437
  }
9438
+ if (accountName) {
9439
+ const existing = loadRegistryStrict().providers.find((provider) => provider.id === registryId);
9440
+ if (!existing) {
9441
+ throw new Error(
9442
+ `Provider "${registryId}" is not configured yet \u2014 run the default sign-in first: clodex providers auth openai`
9443
+ );
9444
+ }
9445
+ if (existing.authType !== "oauth") {
9446
+ throw new Error(
9447
+ `Provider "${registryId}" does not currently have a default OAuth sign-in \u2014 run clodex providers auth openai without --account first.`
9448
+ );
9449
+ }
9450
+ }
8402
9451
  let storeDiagMsg = "";
8403
9452
  const storeReady = await probeProviderCredentialStore(oauthAuthRef(registryId), (msg) => {
8404
9453
  storeDiagMsg = msg;
@@ -8409,12 +9458,29 @@ async function authenticateProvider(providerId, _options = {}) {
8409
9458
  );
8410
9459
  }
8411
9460
  const cred = await runNativeDeviceCode(providerId);
8412
- const persisted = await persistNativeOAuthCredential(providerId, cred);
9461
+ const persisted = await persistNativeOAuthCredential(providerId, cred, accountName);
8413
9462
  const refreshSpinner = p2.spinner();
8414
9463
  refreshSpinner.start("Refreshing model list...");
8415
9464
  try {
8416
- await refreshProviderModels(registryId, cred.access);
8417
- refreshSpinner.stop("Models refreshed");
9465
+ const accountOverride = accountName ?? (persisted.registryProvider.activeAuthAccount === void 0 ? null : process.env[OAUTH_ACCOUNT_ENV] ?? null);
9466
+ const refreshResult = await refreshProviderModelsWithCredential(
9467
+ registryId,
9468
+ async (provider) => resolveProviderCredentialWithSource(
9469
+ provider.id,
9470
+ provider.authRef,
9471
+ void 0,
9472
+ { ignoreProviderOverride: true }
9473
+ ),
9474
+ accountOverride,
9475
+ { ignoreProviderOverride: true }
9476
+ );
9477
+ if (refreshResult.skipped) {
9478
+ refreshSpinner.stop(`Models not refreshed${refreshResult.reason ? ` \u2014 ${refreshResult.reason}` : ""}`);
9479
+ } else if (refreshResult.ok) {
9480
+ refreshSpinner.stop("Models refreshed");
9481
+ } else {
9482
+ refreshSpinner.stop(`Could not refresh models${refreshResult.reason ? ` \u2014 ${refreshResult.reason}` : ""}`);
9483
+ }
8418
9484
  } catch {
8419
9485
  refreshSpinner.stop("Could not refresh models \u2014 run clodex providers refresh-models later");
8420
9486
  }
@@ -8430,9 +9496,15 @@ function providerAuthHelpText() {
8430
9496
 
8431
9497
  ${pc3.bold("Usage:")}
8432
9498
  clodex providers auth openai
9499
+ clodex providers auth openai --account work
8433
9500
 
8434
9501
  ${pc3.bold("Device code (works on SSH/VPS):")}
8435
- openai ChatGPT Plus/Pro (device code at auth.openai.com/codex/device)`;
9502
+ openai ChatGPT Plus/Pro (device code at auth.openai.com/codex/device)
9503
+
9504
+ ${pc3.bold("Named accounts:")}
9505
+ --account <name> store an additional ChatGPT account under a named slot
9506
+ (the default sign-in is untouched). Select one at launch:
9507
+ CLODEX_OAUTH_ACCOUNT=work clodex claude`;
8436
9508
  }
8437
9509
 
8438
9510
  // src/prompts.ts
@@ -8781,10 +9853,19 @@ function parseProvidersArgs(args) {
8781
9853
  if (first === "auth") {
8782
9854
  if (rest.length === 0) return { subcommand: "auth", showHelp: true };
8783
9855
  let authMethod;
9856
+ let authAccount;
8784
9857
  const positional = [];
8785
- for (const arg of rest) {
9858
+ for (let i = 0; i < rest.length; i++) {
9859
+ const arg = rest[i];
8786
9860
  if (arg === "--native") authMethod = "native";
8787
- else if (arg.startsWith("-")) {
9861
+ else if (arg === "--account") {
9862
+ const value = rest[i + 1];
9863
+ if (!value || value.startsWith("-")) {
9864
+ return { subcommand: "auth", showHelp: false, error: "Usage: clodex providers auth <id> --account <name>" };
9865
+ }
9866
+ authAccount = value;
9867
+ i++;
9868
+ } else if (arg.startsWith("-")) {
8788
9869
  return { subcommand: "auth", showHelp: false, error: `Unknown auth option: ${arg}` };
8789
9870
  } else {
8790
9871
  positional.push(arg);
@@ -8793,7 +9874,7 @@ function parseProvidersArgs(args) {
8793
9874
  if (positional.length !== 1) {
8794
9875
  return { subcommand: "auth", showHelp: false, error: "Usage: clodex providers auth <id>" };
8795
9876
  }
8796
- return { subcommand: "auth", showHelp: false, removeId: positional[0], authMethod };
9877
+ return { subcommand: "auth", showHelp: false, removeId: positional[0], authMethod, authAccount };
8797
9878
  }
8798
9879
  if (first === "remove") {
8799
9880
  if (rest.length === 0) return { subcommand: "remove", showHelp: false, error: "Usage: clodex providers remove <id>" };
@@ -8826,13 +9907,133 @@ ${pc5.bold("Subcommands:")}
8826
9907
  remove Remove a provider by id
8827
9908
  refresh-models Update cached model lists`;
8828
9909
  }
9910
+ function accountSwitchOutcome(providerName, saved, effective) {
9911
+ const savedLabel = saved ?? PROVIDER_DEFAULT_ACCOUNT_LABEL;
9912
+ if (effective.kind === "credential-override") {
9913
+ const variable = effective.credentialOverride.variable;
9914
+ if (effective.inactiveReason === "non-oauth") {
9915
+ return {
9916
+ ok: false,
9917
+ message: `Saved ${savedLabel} for ${providerName}, but this provider is not configured for OAuth account selection; ${variable} is configured and blocks launch because it has no isolated model catalog. Save that credential as a provider or unset the variable.`
9918
+ };
9919
+ }
9920
+ if (effective.inactiveReason === "disabled") {
9921
+ return {
9922
+ ok: false,
9923
+ message: `Saved ${savedLabel} for ${providerName} (provider disabled); ${variable} has no isolated model catalog, so enabling the provider in this shell will fail until that credential is saved and refreshed or the variable is unset.`
9924
+ };
9925
+ }
9926
+ return {
9927
+ ok: false,
9928
+ message: `Saved ${savedLabel} for ${providerName}, but ${variable} has no isolated model catalog, so launches are blocked. Save that credential as a provider or account and refresh its models, or unset the variable.`
9929
+ };
9930
+ }
9931
+ if (effective.inactiveReason === "non-oauth") {
9932
+ return {
9933
+ ok: true,
9934
+ message: `Saved ${savedLabel} for ${providerName}, but this provider is not configured for OAuth account selection.`
9935
+ };
9936
+ }
9937
+ if (effective.inactiveReason === "disabled") {
9938
+ if (effective.kind === "broken") {
9939
+ const blockedOverride = effective.credentialOverride ? ` ${effective.credentialOverride.variable} is configured, but OAuth account selection is validated before credential resolution.` : "";
9940
+ return {
9941
+ ok: false,
9942
+ message: effective.fromEnvironment ? `Saved ${savedLabel} for ${providerName} (provider disabled), but ${OAUTH_ACCOUNT_ENV}=${effective.name} names no such account \u2014 enabling it in this shell will fail until the variable is unset or corrected.${blockedOverride}` : `Saved ${savedLabel} for ${providerName} (provider disabled), but that account no longer exists \u2014 enabling the provider will fail.${blockedOverride}`
9943
+ };
9944
+ }
9945
+ if (effective.kind === "slot" && effective.fromEnvironment && effective.name !== saved) {
9946
+ return {
9947
+ ok: true,
9948
+ message: `Saved ${savedLabel} for ${providerName} (provider disabled); if enabled in this shell, ${OAUTH_ACCOUNT_ENV}=${effective.name} will override it.`
9949
+ };
9950
+ }
9951
+ return {
9952
+ ok: true,
9953
+ message: `Saved ${savedLabel} for ${providerName} (provider disabled).`
9954
+ };
9955
+ }
9956
+ if (effective.kind === "broken") {
9957
+ const blockedOverride = effective.credentialOverride ? ` ${effective.credentialOverride.variable} is configured, but OAuth account selection is validated before credential resolution.` : "";
9958
+ return {
9959
+ ok: false,
9960
+ message: effective.fromEnvironment ? `Saved ${savedLabel} for ${providerName}, but ${OAUTH_ACCOUNT_ENV}=${effective.name} names no such account \u2014 every launch fails until it is unset or corrected.${blockedOverride}` : `Saved ${savedLabel} for ${providerName}, but it names no existing account \u2014 every launch fails.${blockedOverride}`
9961
+ };
9962
+ }
9963
+ if (effective.kind === "slot" && effective.fromEnvironment && effective.name !== saved) {
9964
+ return {
9965
+ ok: true,
9966
+ message: `Saved ${savedLabel} for ${providerName}, but ${OAUTH_ACCOUNT_ENV}=${effective.name} overrides it in this shell.`
9967
+ };
9968
+ }
9969
+ return {
9970
+ ok: true,
9971
+ message: `${providerName} will launch as ${savedLabel}.`,
9972
+ confirmsLaunch: true
9973
+ };
9974
+ }
9975
+ function accountSwitchServerRestartWarning(liveServerCount, selectionChanged = true) {
9976
+ if (!selectionChanged || !Number.isInteger(liveServerCount) || liveServerCount <= 0) return null;
9977
+ return `Restart ${liveServerCount} running standalone clodex server${liveServerCount === 1 ? "" : "s"} ${liveServerCount === 1 ? "because it retains" : "because they retain"} the previous provider and credential snapshot.`;
9978
+ }
9979
+ function accountSwitchHint(provider, effective) {
9980
+ if (effective.kind === "credential-override") {
9981
+ const variable = effective.credentialOverride.variable;
9982
+ const selected = effective.selection;
9983
+ const account = selected.kind === "slot" ? `account ${selected.name}` : selected.kind === "default" ? PROVIDER_DEFAULT_ACCOUNT_LABEL : `missing stored OAuth account "${selected.name}"`;
9984
+ const masked2 = selected.latentOrphan ? `; stored "${selected.latentOrphan}" is missing and will fail without ${OAUTH_ACCOUNT_ENV}` : "";
9985
+ if (effective.inactiveReason === "non-oauth") {
9986
+ return `${variable} is configured but launches are blocked because it has no isolated model catalog; OAuth selection (${account}) is stored but inactive because this provider is not configured for OAuth${masked2}`;
9987
+ }
9988
+ if (effective.inactiveReason === "disabled") {
9989
+ return `${variable} is configured for ${account} but has no isolated model catalog; enabling this provider will fail until that credential is saved and refreshed or the variable is unset${masked2}`;
9990
+ }
9991
+ return `${variable} is configured for ${account}, but launches are blocked because it has no isolated model catalog; save and refresh that credential or unset the variable${masked2}`;
9992
+ }
9993
+ if (effective.inactiveReason === "non-oauth") {
9994
+ if (effective.kind === "broken") {
9995
+ return `Stored OAuth account "${effective.name}" no longer exists \u2014 provider is not configured for OAuth selection`;
9996
+ }
9997
+ if (effective.kind === "slot") {
9998
+ return `Stored OAuth account: ${effective.name} (provider is not configured for OAuth selection)`;
9999
+ }
10000
+ return "OAuth account selection inactive (provider is not configured for OAuth)";
10001
+ }
10002
+ if (effective.inactiveReason === "disabled") {
10003
+ const masked2 = effective.latentOrphan ? `; stored "${effective.latentOrphan}" is missing and will fail if enabled without the override` : "";
10004
+ if (effective.kind === "broken") {
10005
+ const blockedOverride = effective.credentialOverride ? `; ${effective.credentialOverride.variable} cannot bypass account selection` : "";
10006
+ return effective.fromEnvironment ? `${OAUTH_ACCOUNT_ENV}=${effective.name} names no such account \u2014 enabling this provider will fail${masked2}${blockedOverride}` : `Selected account "${effective.name}" no longer exists \u2014 enabling this provider will fail${blockedOverride}`;
10007
+ }
10008
+ if (effective.kind === "slot") {
10009
+ return effective.fromEnvironment ? `If enabled, ${OAUTH_ACCOUNT_ENV}=${effective.name} overrides the stored ${provider.activeAuthAccount ?? PROVIDER_DEFAULT_ACCOUNT_LABEL}${masked2}` : `Saved account: ${effective.name} (provider disabled)`;
10010
+ }
10011
+ return `Saved account: ${PROVIDER_DEFAULT_ACCOUNT_LABEL} (provider disabled)`;
10012
+ }
10013
+ if (effective.kind === "broken") {
10014
+ const also = effective.latentOrphan ? ` (and stored "${effective.latentOrphan}" is missing too)` : "";
10015
+ const blockedOverride = effective.credentialOverride ? `; ${effective.credentialOverride.variable} cannot bypass account selection` : "";
10016
+ return effective.fromEnvironment ? `${OAUTH_ACCOUNT_ENV}=${effective.name} names no such account \u2014 every launch fails${also}` + blockedOverride : `Selected account "${effective.name}" no longer exists \u2014 every launch fails; clear it here${blockedOverride}`;
10017
+ }
10018
+ const masked = effective.latentOrphan ? ` \u2014 stored "${effective.latentOrphan}" no longer exists and will fail without it` : "";
10019
+ if (effective.kind === "default") {
10020
+ return `Every launch currently uses ${PROVIDER_DEFAULT_ACCOUNT_LABEL}${masked}`;
10021
+ }
10022
+ return effective.fromEnvironment ? `${OAUTH_ACCOUNT_ENV}=${effective.name} overrides the stored ${provider.activeAuthAccount ?? PROVIDER_DEFAULT_ACCOUNT_LABEL}${masked}` : `Every launch currently uses ${effective.name}${masked}`;
10023
+ }
10024
+ function shouldOfferAccountSwitch(provider) {
10025
+ return Object.keys(provider.authAccounts ?? {}).length > 0 || provider.activeAuthAccount !== void 0;
10026
+ }
8829
10027
  function providerLabel(name, modelCount, enabled) {
8830
10028
  return `${fmtEnabledStar(enabled)} ${fmtProvider(name)} ${pc5.dim(`(${modelCount} model${modelCount === 1 ? "" : "s"})`)}`;
8831
10029
  }
8832
- async function runProvidersAuthWithCleanupState(providerId, method, cleanupState) {
10030
+ async function runProvidersAuthWithCleanupState(providerId, method, cleanupState, account) {
8833
10031
  try {
8834
- const result = await authenticateProvider(providerId, { method });
8835
- p4.log.success(`Signed in to ${result.registryProvider.name} \u2014 credential saved to the credential store.`);
10032
+ const result = await authenticateProvider(providerId, { method, account });
10033
+ const slot = account === void 0 ? void 0 : validateOAuthAccountName(account);
10034
+ p4.log.success(
10035
+ slot ? `Signed in to ${result.registryProvider.name} (account "${slot}") \u2014 make it the account every launch uses with: clodex providers` : `Signed in to ${result.registryProvider.name} \u2014 credential saved to the credential store.`
10036
+ );
8836
10037
  reportCredentialCleanup(result.credentialCleanupPending, cleanupState, true);
8837
10038
  return 0;
8838
10039
  } catch (err) {
@@ -8847,8 +10048,8 @@ async function runProvidersAuthWithCleanupState(providerId, method, cleanupState
8847
10048
  async function runProvidersAuth(providerId, method) {
8848
10049
  return runProvidersAuthWithCleanupState(providerId, method);
8849
10050
  }
8850
- async function runProvidersRefreshModels(providerId) {
8851
- const resolveKey = async (provider) => resolveProviderCredential(provider.id, provider.authRef);
10051
+ async function runProvidersRefreshModels(providerId, options = {}) {
10052
+ const resolveKey = async (provider) => resolveProviderCredentialWithSource(provider.id, provider.authRef);
8852
10053
  if (providerId) {
8853
10054
  const registry = loadRegistry();
8854
10055
  const provider = registry.providers.find((p13) => p13.id === providerId);
@@ -8858,11 +10059,25 @@ async function runProvidersRefreshModels(providerId) {
8858
10059
  }
8859
10060
  const spinner6 = p4.spinner();
8860
10061
  spinner6.start(`Refreshing ${provider.name}...`);
8861
- const key = await resolveRefreshCredential(
8862
- provider,
8863
- async (p13) => resolveProviderCredential(p13.id, p13.authRef)
8864
- );
8865
- const result = await refreshProviderModels(providerId, key);
10062
+ const accountOverride = options.accountOverride === void 0 ? process.env[OAUTH_ACCOUNT_ENV] ?? null : options.accountOverride;
10063
+ let result;
10064
+ try {
10065
+ result = await refreshProviderModelsWithCredential(
10066
+ providerId,
10067
+ async (candidate) => resolveProviderCredentialWithSource(
10068
+ candidate.id,
10069
+ candidate.authRef,
10070
+ void 0,
10071
+ { ignoreProviderOverride: options.ignoreProviderCredentialOverride }
10072
+ ),
10073
+ accountOverride,
10074
+ { ignoreProviderOverride: options.ignoreProviderCredentialOverride }
10075
+ );
10076
+ } catch (err) {
10077
+ spinner6.stop("");
10078
+ p4.log.error(err instanceof Error ? err.message : String(err));
10079
+ return 1;
10080
+ }
8866
10081
  spinner6.stop("");
8867
10082
  if (result.skipped) {
8868
10083
  const countNote = result.modelCount ? ` (${result.modelCount} cached models kept)` : "";
@@ -9036,8 +10251,22 @@ async function runProviderDetail(id) {
9036
10251
  const registry = loadRegistry();
9037
10252
  const provider = registry.providers.find((pr) => pr.id === id);
9038
10253
  if (!provider) return "back";
9039
- const modelCount = provider.modelsCache?.models.length ?? 0;
9040
- const authLabel = formatRegistryAuthLabel(provider);
10254
+ const effective = resolveActiveAccount(provider);
10255
+ let modelProvider;
10256
+ try {
10257
+ modelProvider = projectSelectedOAuthAccount(provider);
10258
+ if (effective.kind === "credential-override") {
10259
+ modelProvider = { ...modelProvider };
10260
+ delete modelProvider.modelsCache;
10261
+ delete modelProvider.refreshedAt;
10262
+ }
10263
+ } catch {
10264
+ modelProvider = { ...provider };
10265
+ delete modelProvider.modelsCache;
10266
+ delete modelProvider.refreshedAt;
10267
+ }
10268
+ const modelCount = modelProvider.modelsCache?.models.length ?? 0;
10269
+ const authLabel = (await resolveProvidersForDisplay()).find((entry) => entry.id === id)?.authLabel ?? formatRegistryAuthLabel(provider);
9041
10270
  printProviderDetailPanel(provider.name, modelCount, authLabel);
9042
10271
  const detailOptions = [];
9043
10272
  if (modelCount > 0) {
@@ -9052,11 +10281,29 @@ async function runProviderDetail(id) {
9052
10281
  label: "Refresh model list",
9053
10282
  hint: "Fetch latest models from the provider API"
9054
10283
  });
10284
+ const accountSlots = Object.keys(provider.authAccounts ?? {}).sort();
9055
10285
  if (supportsNativeOAuth(id) || provider.authType === "oauth") {
9056
10286
  detailOptions.push({
9057
10287
  value: "auth",
9058
10288
  label: "Sign in again (OAuth)",
9059
- hint: "Refresh OAuth tokens or switch accounts"
10289
+ // Says what the action DOES. It calls the auth flow with no account
10290
+ // name, so it re-authenticates the provider's own credential and cannot
10291
+ // create or refresh a named slot — the previous wording advertised
10292
+ // exactly the thing it does not do, which is worst when the account
10293
+ // needing reauthentication is a named one that this would leave broken
10294
+ // while overwriting the default.
10295
+ hint: accountSlots.length > 0 ? `Re-authenticate ${PROVIDER_DEFAULT_ACCOUNT_LABEL} only \u2014 for a named account: clodex providers auth ${id} --account <name>` : `Re-authenticate ${PROVIDER_DEFAULT_ACCOUNT_LABEL}`
10296
+ });
10297
+ }
10298
+ if (shouldOfferAccountSwitch(provider)) {
10299
+ detailOptions.push({
10300
+ value: "account",
10301
+ label: "Switch account",
10302
+ // Same resolver the list view uses, so this screen cannot contradict it
10303
+ // about which identity is live — including when the answer is "none of
10304
+ // them, the launch fails", which this hint previously reported as a
10305
+ // working account.
10306
+ hint: accountSwitchHint(provider, effective)
9060
10307
  });
9061
10308
  }
9062
10309
  detailOptions.push(
@@ -9074,8 +10321,8 @@ async function runProviderDetail(id) {
9074
10321
  });
9075
10322
  if (p4.isCancel(action) || action === "back") return "back";
9076
10323
  if (action === "browse") {
9077
- const cachedModels = provider.modelsCache?.models ?? [];
9078
- const localModels = cachedModels.map((m) => cachedModelToLocal(m, provider)).filter((m) => m !== null);
10324
+ const cachedModels = modelProvider.modelsCache?.models ?? [];
10325
+ const localModels = cachedModels.map((m) => cachedModelToLocal(m, modelProvider)).filter((m) => m !== null);
9079
10326
  const localProvider = {
9080
10327
  id: provider.id,
9081
10328
  name: provider.name,
@@ -9093,6 +10340,71 @@ async function runProviderDetail(id) {
9093
10340
  await runWithCredentialCleanup((state) => runProvidersAuthWithCleanupState(id, void 0, state));
9094
10341
  return "back";
9095
10342
  }
10343
+ if (action === "account") {
10344
+ const providerDefault = "<default>";
10345
+ const stored = provider.activeAuthAccount;
10346
+ const current = stored !== void 0 && accountSlots.includes(stored) ? stored : providerDefault;
10347
+ const chosen = await p4.select({
10348
+ message: "Which account should every launch use?",
10349
+ initialValue: current,
10350
+ options: [
10351
+ {
10352
+ value: providerDefault,
10353
+ label: PROVIDER_DEFAULT_ACCOUNT_LABEL,
10354
+ hint: "the provider's original sign-in"
10355
+ },
10356
+ ...accountSlots.map((name) => ({
10357
+ value: name,
10358
+ label: name,
10359
+ hint: effective.kind === "slot" && effective.name === name ? effective.fromEnvironment ? `active via ${OAUTH_ACCOUNT_ENV}` : "current" : name === provider.activeAuthAccount ? "stored" : ""
10360
+ }))
10361
+ ]
10362
+ });
10363
+ if (p4.isCancel(chosen)) return "back";
10364
+ return withProviderMutationLock(id, async () => {
10365
+ const result = await setActiveOAuthAccount(id, chosen === providerDefault ? void 0 : chosen);
10366
+ if (!result.updated) {
10367
+ p4.log.error(result.error ?? "Could not switch account.");
10368
+ return "back";
10369
+ }
10370
+ if (!result.provider) {
10371
+ p4.log.error("Account selection was saved, but the resulting provider state could not be read.");
10372
+ return "back";
10373
+ }
10374
+ const refreshExitCode = await runProvidersRefreshModels(id, {
10375
+ accountOverride: null,
10376
+ ignoreProviderCredentialOverride: true
10377
+ });
10378
+ const currentProvider = loadRegistry().providers.find((candidate) => candidate.id === id);
10379
+ if (!currentProvider) {
10380
+ p4.log.error("Account selection was saved, but the resulting provider state could not be read.");
10381
+ return "back";
10382
+ }
10383
+ const outcome = accountSwitchOutcome(
10384
+ currentProvider.name,
10385
+ result.account,
10386
+ resolveActiveAccount(currentProvider)
10387
+ );
10388
+ const selectedCatalogReady = Boolean(currentProvider.modelsCache?.models.length);
10389
+ if (outcome.ok && currentProvider.enabled && currentProvider.authType === "oauth" && (refreshExitCode !== 0 || !selectedCatalogReady)) {
10390
+ const savedLabel = result.account ?? PROVIDER_DEFAULT_ACCOUNT_LABEL;
10391
+ const savedContext = outcome.confirmsLaunch ? `Saved ${savedLabel} for ${currentProvider.name}.` : outcome.message;
10392
+ p4.log.warn(
10393
+ `${savedContext} Automatic model refresh for the saved selection did not produce a usable catalog; choose Switch account again to retry it before relying on that selection for launches.`
10394
+ );
10395
+ } else if (outcome.ok) {
10396
+ p4.log.success(outcome.message);
10397
+ } else {
10398
+ p4.log.warn(outcome.message);
10399
+ }
10400
+ const restartWarning = accountSwitchServerRestartWarning(
10401
+ readLiveServerRuntimeStates().length,
10402
+ result.changed
10403
+ );
10404
+ if (restartWarning) p4.log.warn(restartWarning);
10405
+ return "back";
10406
+ });
10407
+ }
9096
10408
  if (action === "toggle") {
9097
10409
  const result = toggleProviderEnabled(id);
9098
10410
  if (result.toggled) {
@@ -9121,6 +10433,12 @@ async function runProvidersHub() {
9121
10433
  const configuredIds = new Set(entries.map((entry) => entry.id));
9122
10434
  if (listVisibleOAuthTemplates(configuredIds).length > 0) {
9123
10435
  options.push({ value: "auth-menu", label: "\u2192 Sign in with ChatGPT (OAuth)", hint: "device code" });
10436
+ } else if (configuredIds.has("openai-oauth")) {
10437
+ options.push({
10438
+ value: "auth-account",
10439
+ label: "\u2192 Add another ChatGPT account",
10440
+ hint: "named slot; pick which one launches via Switch account"
10441
+ });
9124
10442
  }
9125
10443
  if (entries.length > 0) {
9126
10444
  options.push({ value: "refresh-all", label: "\u21BA Refresh all models", hint: "Update model lists for all providers" });
@@ -9145,6 +10463,23 @@ async function runProvidersHub() {
9145
10463
  await runWithCredentialCleanup((state) => runProvidersAuthWithCleanupState("openai", void 0, state));
9146
10464
  continue;
9147
10465
  }
10466
+ if (choice === "auth-account") {
10467
+ const name = await p4.text({
10468
+ message: "Name for this account (choose which one launches with: clodex providers)",
10469
+ placeholder: "work",
10470
+ validate: (value) => {
10471
+ try {
10472
+ validateOAuthAccountName(String(value ?? ""));
10473
+ return void 0;
10474
+ } catch (err) {
10475
+ return err instanceof Error ? err.message : String(err);
10476
+ }
10477
+ }
10478
+ });
10479
+ if (p4.isCancel(name)) continue;
10480
+ await runWithCredentialCleanup((state) => runProvidersAuthWithCleanupState("openai", void 0, state, String(name)));
10481
+ continue;
10482
+ }
9148
10483
  if (typeof choice === "string" && choice.startsWith("provider:")) {
9149
10484
  const id = choice.slice("provider:".length);
9150
10485
  const outcome = await runProviderDetail(id);
@@ -9179,7 +10514,7 @@ async function runProvidersCommand(args) {
9179
10514
  console.log(providerAuthHelpText());
9180
10515
  return 0;
9181
10516
  }
9182
- return runWithCredentialCleanup((state) => runProvidersAuthWithCleanupState(parsed.removeId, parsed.authMethod, state));
10517
+ return runWithCredentialCleanup((state) => runProvidersAuthWithCleanupState(parsed.removeId, parsed.authMethod, state, parsed.authAccount));
9183
10518
  }
9184
10519
  relayIntro("Your OpenAI providers");
9185
10520
  return runProvidersHub();
@@ -9220,7 +10555,7 @@ async function runFirstRunWizard(_trace = false) {
9220
10555
 
9221
10556
  // src/proxy.ts
9222
10557
  import { createServer } from "http";
9223
- import { appendFileSync, openSync as openSync3, writeSync, closeSync as closeSync3 } from "fs";
10558
+ import { appendFileSync as appendFileSync2, openSync as openSync3, writeSync as writeSync2, closeSync as closeSync3 } from "fs";
9224
10559
 
9225
10560
  // src/http-utils.ts
9226
10561
  import * as zlib from "zlib";
@@ -9462,7 +10797,8 @@ function localModelToRoute(lp, model) {
9462
10797
  reasoning: model.reasoning,
9463
10798
  interleavedReasoningField: model.interleavedReasoningField,
9464
10799
  useResponsesLite: model.useResponsesLite,
9465
- preferWebSockets: model.preferWebSockets
10800
+ preferWebSockets: model.preferWebSockets,
10801
+ compatibility: model.compatibility
9466
10802
  };
9467
10803
  }
9468
10804
  function makeRouteResolver(localProviders) {
@@ -9535,7 +10871,8 @@ function buildHttpProxyRoutes(providers, favorites, modelAliases = void 0, max =
9535
10871
  unavailable.push(favorite);
9536
10872
  continue;
9537
10873
  }
9538
- if (model.modelFormat !== "openai" || !isSdkMigratedNpm(model.npm)) {
10874
+ const supported = model.modelFormat === "anthropic" ? Boolean(model.baseUrl) : isSdkMigratedNpm(model.npm);
10875
+ if (!supported) {
9539
10876
  unsupported.push(favorite);
9540
10877
  continue;
9541
10878
  }
@@ -9708,7 +11045,8 @@ function routeUnavailableMessage(modelId, reason) {
9708
11045
  }
9709
11046
 
9710
11047
  // src/upstream-forward.ts
9711
- import { Readable } from "stream";
11048
+ import { Readable, Transform } from "stream";
11049
+ import { StringDecoder } from "string_decoder";
9712
11050
 
9713
11051
  // src/server/auth.ts
9714
11052
  function sanitizeCredential(value) {
@@ -9789,6 +11127,34 @@ async function fetchWithOAuthRetry(apiKey, request3, refreshToken) {
9789
11127
  response = await request3(refreshed);
9790
11128
  return { response, apiKey: refreshed, refreshed: true };
9791
11129
  }
11130
+ function anthropicSseModelRewrite(override) {
11131
+ const decoder = new StringDecoder("utf8");
11132
+ let tail = "";
11133
+ const rewriteLine = (line) => {
11134
+ if (!line.startsWith("data:") || !line.includes('"message_start"')) return line;
11135
+ try {
11136
+ const parsed = JSON.parse(line.slice(5));
11137
+ if (parsed.type === "message_start" && parsed.message && typeof parsed.message.model === "string") {
11138
+ parsed.message.model = override;
11139
+ return "data: " + JSON.stringify(parsed);
11140
+ }
11141
+ } catch {
11142
+ }
11143
+ return line;
11144
+ };
11145
+ return new Transform({
11146
+ transform(chunk, _encoding, callback) {
11147
+ const lines = (tail + decoder.write(chunk)).split("\n");
11148
+ tail = lines.pop() ?? "";
11149
+ const rewritten = lines.map(rewriteLine);
11150
+ callback(null, rewritten.length ? rewritten.join("\n") + "\n" : "");
11151
+ },
11152
+ flush(callback) {
11153
+ const rest = tail + decoder.end();
11154
+ callback(null, rest ? rewriteLine(rest) : "");
11155
+ }
11156
+ });
11157
+ }
9792
11158
  async function relayAnthropicMessages(res, messagesUrl, body, apiKey, clientWantsStream, options = {}) {
9793
11159
  const doFetch = (key) => fetch(messagesUrl, {
9794
11160
  method: "POST",
@@ -9825,7 +11191,12 @@ async function relayAnthropicMessages(res, messagesUrl, body, apiKey, clientWant
9825
11191
  "Cache-Control": "no-cache",
9826
11192
  "Connection": "keep-alive"
9827
11193
  });
9828
- Readable.fromWeb(upstreamRes.body).on("error", () => res.destroy()).pipe(res);
11194
+ const upstream = Readable.fromWeb(upstreamRes.body).on("error", () => res.destroy());
11195
+ if (options.responseModelOverride) {
11196
+ upstream.pipe(anthropicSseModelRewrite(options.responseModelOverride)).on("error", () => res.destroy()).pipe(res);
11197
+ } else {
11198
+ upstream.pipe(res);
11199
+ }
9829
11200
  return;
9830
11201
  }
9831
11202
  if (!upstreamRes.body) {
@@ -9833,19 +11204,24 @@ async function relayAnthropicMessages(res, messagesUrl, body, apiKey, clientWant
9833
11204
  res.end(JSON.stringify({ type: "error", error: { type: "api_error", message: "Upstream returned empty response body" } }));
9834
11205
  return;
9835
11206
  }
9836
- const text4 = await upstreamRes.text();
11207
+ let text5 = await upstreamRes.text();
11208
+ let parsed;
9837
11209
  try {
9838
- JSON.parse(text4);
11210
+ parsed = JSON.parse(text5);
9839
11211
  } catch {
9840
11212
  res.writeHead(502, { "Content-Type": "application/json" });
9841
11213
  res.end(JSON.stringify({ type: "error", error: { type: "api_error", message: "Upstream response was not valid JSON" } }));
9842
11214
  return;
9843
11215
  }
11216
+ if (options.responseModelOverride && parsed && typeof parsed === "object" && !Array.isArray(parsed) && typeof parsed.model === "string") {
11217
+ parsed.model = options.responseModelOverride;
11218
+ text5 = JSON.stringify(parsed);
11219
+ }
9844
11220
  res.writeHead(200, {
9845
11221
  "Content-Type": "application/json",
9846
- "Content-Length": Buffer.byteLength(text4).toString()
11222
+ "Content-Length": Buffer.byteLength(text5).toString()
9847
11223
  });
9848
- res.end(text4);
11224
+ res.end(text5);
9849
11225
  }
9850
11226
 
9851
11227
  // src/proxy.ts
@@ -10001,7 +11377,7 @@ function reportOnce(raw, message, warn) {
10001
11377
  } catch {
10002
11378
  }
10003
11379
  }
10004
- function upstreamMaxRetries(env = process.env, warn = (message) => console.error(`clodex: ${message}`)) {
11380
+ function upstreamMaxRetries(env = process.env, warn = (message) => emitParentNotice(`clodex: ${message}`)) {
10005
11381
  const raw = env[UPSTREAM_MAX_RETRIES_ENV]?.trim();
10006
11382
  if (raw === void 0 || raw === "") return void 0;
10007
11383
  const value = Number(raw);
@@ -10070,10 +11446,10 @@ function supportsOpenAiPromptCacheBreakpoints(modelId) {
10070
11446
  const minor = Number(match[2] ?? 0);
10071
11447
  return major > 5 || major === 5 && minor >= 6;
10072
11448
  }
10073
- function stripClaudeCodeBillingHeader(text4) {
10074
- if (!text4.startsWith(CLAUDE_CODE_BILLING_HEADER_PREFIX)) return text4;
10075
- const newline = text4.indexOf("\n");
10076
- return newline === -1 ? void 0 : text4.slice(newline + 1);
11449
+ function stripClaudeCodeBillingHeader(text5) {
11450
+ if (!text5.startsWith(CLAUDE_CODE_BILLING_HEADER_PREFIX)) return text5;
11451
+ const newline = text5.indexOf("\n");
11452
+ return newline === -1 ? void 0 : text5.slice(newline + 1);
10077
11453
  }
10078
11454
  function systemToString(system, stripAnthropicBillingHeader = false) {
10079
11455
  if (!system) return void 0;
@@ -10082,8 +11458,8 @@ function systemToString(system, stripAnthropicBillingHeader = false) {
10082
11458
  }
10083
11459
  const blocks = system.map((b) => typeof b === "string" ? b : b.text ?? "");
10084
11460
  if (!stripAnthropicBillingHeader) return blocks.join("\n");
10085
- return blocks.flatMap((text4) => {
10086
- const stripped = stripClaudeCodeBillingHeader(text4);
11461
+ return blocks.flatMap((text5) => {
11462
+ const stripped = stripClaudeCodeBillingHeader(text5);
10087
11463
  return stripped === void 0 ? [] : [stripped];
10088
11464
  }).join("\n");
10089
11465
  }
@@ -10099,12 +11475,12 @@ function translateTopLevelSystemForOpenAi(system) {
10099
11475
  }
10100
11476
  return system.flatMap((block) => {
10101
11477
  const raw = typeof block === "string" ? block : block.text ?? "";
10102
- const text4 = stripClaudeCodeBillingHeader(raw) ?? "";
10103
- if (!text4.trim()) return [];
11478
+ const text5 = stripClaudeCodeBillingHeader(raw) ?? "";
11479
+ if (!text5.trim()) return [];
10104
11480
  const cacheControl = typeof block === "string" ? void 0 : block.cache_control;
10105
11481
  return [{
10106
11482
  role: "system",
10107
- content: text4,
11483
+ content: text5,
10108
11484
  ...cacheControl ? { providerOptions: { openai: { promptCacheBreakpoint: { mode: "explicit" } } } } : {}
10109
11485
  }];
10110
11486
  });
@@ -10161,9 +11537,9 @@ function annotateToolNames(messages) {
10161
11537
  }
10162
11538
  }
10163
11539
  function thinkingToSdkPart(block, npm) {
10164
- const text4 = block.thinking ?? "";
10165
- if (npm === "@ai-sdk/openai" && !block.signature && !text4.trim()) return null;
10166
- const part = { type: "reasoning", text: text4 };
11540
+ const text5 = block.thinking ?? "";
11541
+ if (npm === "@ai-sdk/openai" && !block.signature && !text5.trim()) return null;
11542
+ const part = { type: "reasoning", text: text5 };
10167
11543
  if (block.signature) {
10168
11544
  if (npm === "@ai-sdk/google") {
10169
11545
  part.providerOptions = { google: { thoughtSignature: block.signature } };
@@ -10279,8 +11655,8 @@ function isClaudeCodeStructuredOutputCompactRequest(body) {
10279
11655
  if (!body.tools?.some((candidate) => candidate.name === "StructuredOutput")) return false;
10280
11656
  const finalMessage = body.messages.at(-1);
10281
11657
  if (!finalMessage || finalMessage.role !== "user") return false;
10282
- const text4 = typeof finalMessage.content === "string" ? finalMessage.content : finalMessage.content.filter((block) => block.type === "text").map((block) => block.text ?? "").join("\n");
10283
- return text4.includes(COMPACT_TEXT_ONLY_START) && text4.includes(COMPACT_TEXT_ONLY_END);
11658
+ const text5 = typeof finalMessage.content === "string" ? finalMessage.content : finalMessage.content.filter((block) => block.type === "text").map((block) => block.text ?? "").join("\n");
11659
+ return text5.includes(COMPACT_TEXT_ONLY_START) && text5.includes(COMPACT_TEXT_ONLY_END);
10284
11660
  }
10285
11661
  function translateRequest(body, npm, options) {
10286
11662
  const messages = body.messages ?? [];
@@ -10309,10 +11685,12 @@ function translateRequest(body, npm, options) {
10309
11685
  const supportsExplicitOpenAiCaching = !options?.openAiOAuth && supportsOpenAiPromptCacheBreakpoints(upstreamModelId2);
10310
11686
  if (npm === "@ai-sdk/openai") {
10311
11687
  const claudeSessionId = extractClaudeSessionId(body, options?.claudeSessionId);
11688
+ const serviceTier = options?.openAiOAuth ? oauthServiceTier() : void 0;
10312
11689
  providerOptions = deepMergeProviderOptions(providerOptions, {
10313
11690
  openai: {
10314
11691
  promptCacheKey: claudeSessionId ? claudeSessionPromptCacheKey(claudeSessionId) : openAiPromptCacheKey(baseSystem, upstreamTools),
10315
- ...supportsExplicitOpenAiCaching ? { promptCacheOptions: { mode: "implicit", ttl: "30m" } } : {}
11692
+ ...supportsExplicitOpenAiCaching ? { promptCacheOptions: { mode: "implicit", ttl: "30m" } } : {},
11693
+ ...serviceTier ? { serviceTier } : {}
10316
11694
  }
10317
11695
  });
10318
11696
  }
@@ -10330,6 +11708,35 @@ function translateRequest(body, npm, options) {
10330
11708
  providerOptions
10331
11709
  };
10332
11710
  }
11711
+ function isOpenAiOAuthRoute(route) {
11712
+ return route?.npm === "@ai-sdk/openai" && route.authType === "oauth";
11713
+ }
11714
+ var SERVICE_TIERS = /* @__PURE__ */ new Set(["auto", "default", "flex", "priority"]);
11715
+ var warnedInvalidServiceTier = false;
11716
+ var warnedUnsupportedServiceTier = false;
11717
+ function oauthServiceTier() {
11718
+ const raw = process.env.CLODEX_SERVICE_TIER;
11719
+ if (raw === void 0 || raw.trim() === "") return void 0;
11720
+ const normalized = raw.trim().toLowerCase() === "fast" ? "priority" : raw.trim().toLowerCase();
11721
+ if (!SERVICE_TIERS.has(normalized)) {
11722
+ if (!warnedInvalidServiceTier) {
11723
+ warnedInvalidServiceTier = true;
11724
+ emitParentNotice("clodex: ignoring CLODEX_SERVICE_TIER (expected auto, default, flex, priority, or fast)");
11725
+ }
11726
+ return void 0;
11727
+ }
11728
+ return normalized;
11729
+ }
11730
+ function reportUnsupportedServiceTier(params, warnings) {
11731
+ if (warnedUnsupportedServiceTier || !params.providerOptions?.openai?.serviceTier) return;
11732
+ if (!Array.isArray(warnings) || !warnings.some((warning) => {
11733
+ if (!warning || typeof warning !== "object") return false;
11734
+ const candidate = warning;
11735
+ return candidate.type === "unsupported" && candidate.feature === "serviceTier";
11736
+ })) return;
11737
+ warnedUnsupportedServiceTier = true;
11738
+ emitParentNotice("clodex: requested service tier was not sent for this model; the backend default will be used");
11739
+ }
10333
11740
  function toAnthropicUsage(u) {
10334
11741
  const total = u?.inputTokens ?? 0;
10335
11742
  const cacheRead = u?.inputTokenDetails?.cacheReadTokens ?? u?.cachedInputTokens ?? 0;
@@ -10579,7 +11986,8 @@ async function streamAnthropicResponse(model, params, modelId, write, log12, obs
10579
11986
  maxRetries: upstreamMaxRetries(),
10580
11987
  abortSignal,
10581
11988
  onError: () => {
10582
- }
11989
+ },
11990
+ onStepFinish: (step) => reportUnsupportedServiceTier(params, step.warnings)
10583
11991
  });
10584
11992
  const watchedStream = (async function* () {
10585
11993
  try {
@@ -10605,10 +12013,11 @@ async function streamAnthropicResponse(model, params, modelId, write, log12, obs
10605
12013
  }
10606
12014
  }
10607
12015
  async function generateAnthropicResponse(model, params, modelId, options) {
10608
- let text4;
12016
+ let text5;
10609
12017
  let toolCalls;
10610
12018
  let finishReason;
10611
12019
  let usage;
12020
+ let warnings;
10612
12021
  if (options?.forceStream) {
10613
12022
  const forceAbort = new AbortController();
10614
12023
  const stopForwardingAbort = forwardAbortSignal(options.abortSignal, forceAbort);
@@ -10628,7 +12037,8 @@ async function generateAnthropicResponse(model, params, modelId, options) {
10628
12037
  maxRetries: upstreamMaxRetries(),
10629
12038
  abortSignal,
10630
12039
  onError: () => {
10631
- }
12040
+ },
12041
+ onStepFinish: (step) => reportUnsupportedServiceTier(params, step.warnings)
10632
12042
  });
10633
12043
  const streamedText = [];
10634
12044
  const streamedToolCalls = [];
@@ -10667,7 +12077,7 @@ async function generateAnthropicResponse(model, params, modelId, options) {
10667
12077
  clearTimeout(totalTimer);
10668
12078
  if (!forceAbort.signal.aborted) forceAbort.abort();
10669
12079
  }
10670
- text4 = streamedText.join("");
12080
+ text5 = streamedText.join("");
10671
12081
  toolCalls = streamedToolCalls;
10672
12082
  finishReason = streamedFinishReason;
10673
12083
  usage = streamedUsage;
@@ -10685,13 +12095,14 @@ async function generateAnthropicResponse(model, params, modelId, options) {
10685
12095
  maxRetries: upstreamMaxRetries(),
10686
12096
  abortSignal: generateAbort.signal
10687
12097
  });
10688
- ({ text: text4, toolCalls, finishReason, usage } = r);
12098
+ ({ text: text5, toolCalls, finishReason, usage, warnings } = r);
10689
12099
  } finally {
10690
12100
  stopForwardingAbort();
10691
12101
  clearTimeout(totalTimer);
10692
12102
  if (!generateAbort.signal.aborted) generateAbort.abort();
10693
12103
  }
10694
12104
  }
12105
+ reportUnsupportedServiceTier(params, warnings);
10695
12106
  const requiredProps = toolRequiredProps(params.tools);
10696
12107
  return {
10697
12108
  id: "msg_" + Date.now(),
@@ -10699,7 +12110,7 @@ async function generateAnthropicResponse(model, params, modelId, options) {
10699
12110
  role: "assistant",
10700
12111
  model: modelId,
10701
12112
  content: [
10702
- ...text4 ? [{ type: "text", text: text4 }] : [],
12113
+ ...text5 ? [{ type: "text", text: text5 }] : [],
10703
12114
  ...toolCalls.map((tc) => ({
10704
12115
  type: "tool_use",
10705
12116
  id: encodeToolUseId(tc.toolCallId, grabRoundTripSignature(tc)),
@@ -10860,14 +12271,14 @@ function appendSecureLog(logPath, line) {
10860
12271
  try {
10861
12272
  const fd = openSync3(logPath, "a", 384);
10862
12273
  try {
10863
- writeSync(fd, `${(/* @__PURE__ */ new Date()).toISOString()} ${redacted}
12274
+ writeSync2(fd, `${(/* @__PURE__ */ new Date()).toISOString()} ${redacted}
10864
12275
  `);
10865
12276
  } finally {
10866
12277
  closeSync3(fd);
10867
12278
  }
10868
12279
  } catch {
10869
12280
  try {
10870
- appendFileSync(logPath, `${(/* @__PURE__ */ new Date()).toISOString()} ${redacted}
12281
+ appendFileSync2(logPath, `${(/* @__PURE__ */ new Date()).toISOString()} ${redacted}
10871
12282
  `);
10872
12283
  } catch {
10873
12284
  }
@@ -11033,8 +12444,9 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
11033
12444
  }
11034
12445
  const upstreamUrl = route.upstreamUrl;
11035
12446
  const routeAuthType = route.authType ?? "api";
12447
+ const loggedTier = isOpenAiOAuthRoute(route) ? oauthServiceTier() : void 0;
11036
12448
  plog(
11037
- () => `POST /v1/messages - alias=${originalModel} route=${route.realModelId} format=${route.modelFormat} key=${routeAuthType === "none" ? "none" : apiKey ? `len:${apiKey.length}` : "MISSING"}`
12449
+ () => `POST /v1/messages - alias=${originalModel} route=${route.realModelId} format=${route.modelFormat} key=${routeAuthType === "none" ? "none" : apiKey ? `len:${apiKey.length}` : "MISSING"}` + (loggedTier ? ` tier=${loggedTier}` : "")
11038
12450
  );
11039
12451
  const usesSdkAdapter = isSdkMigratedNpm(route.npm);
11040
12452
  if (messagesEndpoint === "count_tokens") {
@@ -11102,6 +12514,10 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
11102
12514
  route.apiKey = refreshed;
11103
12515
  },
11104
12516
  signal: clientAbort.signal,
12517
+ // A route selected through a clodex: id or short alias must echo the
12518
+ // exact requested id back, or patched Claude Code misses the alias
12519
+ // context-window key and can skip auto-compaction.
12520
+ responseModelOverride: typeof originalModel === "string" && originalModel !== route.realModelId ? originalModel : void 0,
11105
12521
  onUpstreamError: inferenceLogPath ? (statusCode, errorContent) => writeInferenceResponseErrorLog(inferenceLogPath, {
11106
12522
  modelId: originalModel,
11107
12523
  provider: route.providerId ?? route.aliasId.split(":")[1] ?? "unknown",
@@ -11119,7 +12535,7 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
11119
12535
  return;
11120
12536
  }
11121
12537
  if (usesSdkAdapter) {
11122
- const openAiOAuth = route.npm === "@ai-sdk/openai" && route.authType === "oauth";
12538
+ const openAiOAuth = isOpenAiOAuthRoute(route);
11123
12539
  const claudeSessionIdHeader = Array.isArray(req.headers["x-claude-code-session-id"]) ? req.headers["x-claude-code-session-id"][0] : req.headers["x-claude-code-session-id"];
11124
12540
  const claudeSessionId = extractClaudeSessionId(anthropicBody, claudeSessionIdHeader);
11125
12541
  const translationLifecycle = createTranslationLifecycle(
@@ -11140,6 +12556,7 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
11140
12556
  supportedParameters: route.supportedParameters,
11141
12557
  reasoning: route.reasoning,
11142
12558
  interleavedReasoningField: route.interleavedReasoningField,
12559
+ compatibility: route.compatibility,
11143
12560
  upstreamModelId: route.realModelId
11144
12561
  }
11145
12562
  });
@@ -11158,6 +12575,7 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
11158
12575
  headers: route.headers,
11159
12576
  useResponsesLite: route.useResponsesLite,
11160
12577
  preferWebSockets: route.preferWebSockets,
12578
+ compatibility: route.compatibility,
11161
12579
  onDebug: (msg) => plog(() => msg),
11162
12580
  onWebSocketDiagnostic: webSocketDiagnosticsLogPath ? (event) => writeWebSocketDiagnosticLog(webSocketDiagnosticsLogPath, event) : void 0
11163
12581
  });
@@ -11367,13 +12785,14 @@ function startProxy(completionsUrl, modelId, debug = false, contextWindow, sdk,
11367
12785
  interleavedReasoningField: sdk?.interleavedReasoningField,
11368
12786
  useResponsesLite: sdk?.useResponsesLite,
11369
12787
  preferWebSockets: sdk?.preferWebSockets,
12788
+ compatibility: sdk?.compatibility,
11370
12789
  headers: sdk?.headers
11371
12790
  }], clientModelId, debug);
11372
12791
  }
11373
12792
 
11374
12793
  // src/server/index.ts
11375
12794
  import pc10 from "picocolors";
11376
- import { networkInterfaces } from "os";
12795
+ import { networkInterfaces as networkInterfaces2 } from "os";
11377
12796
  import * as p9 from "@clack/prompts";
11378
12797
 
11379
12798
  // src/target-compatibility.ts
@@ -11586,6 +13005,7 @@ function translateOpenAiRequest(body, options) {
11586
13005
  }
11587
13006
  if (options?.openAiOAuth) {
11588
13007
  const instructions = system?.trim() || "You are a coding assistant.";
13008
+ const serviceTier = oauthServiceTier();
11589
13009
  return {
11590
13010
  messages,
11591
13011
  tools,
@@ -11595,7 +13015,8 @@ function translateOpenAiRequest(body, options) {
11595
13015
  openai: {
11596
13016
  store: false,
11597
13017
  include: ["reasoning.encrypted_content"],
11598
- instructions
13018
+ instructions,
13019
+ ...serviceTier ? { serviceTier } : {}
11599
13020
  }
11600
13021
  }
11601
13022
  };
@@ -11642,7 +13063,8 @@ async function generateOpenAiResponse(model, params, responseModelId, options) {
11642
13063
  ...params,
11643
13064
  maxRetries: upstreamMaxRetries(),
11644
13065
  onError: () => {
11645
- }
13066
+ },
13067
+ onStepFinish: (step) => reportUnsupportedServiceTier(params, step.warnings)
11646
13068
  });
11647
13069
  result = await collectOpenAiStream(stream);
11648
13070
  } else {
@@ -11652,6 +13074,7 @@ async function generateOpenAiResponse(model, params, responseModelId, options) {
11652
13074
  maxRetries: upstreamMaxRetries()
11653
13075
  });
11654
13076
  }
13077
+ reportUnsupportedServiceTier(params, result.warnings);
11655
13078
  const message = { role: "assistant", content: result.text || null };
11656
13079
  if (result.toolCalls?.length) {
11657
13080
  message.tool_calls = result.toolCalls.map((tc) => ({
@@ -11677,7 +13100,8 @@ async function streamOpenAiResponse(model, params, responseModelId, onChunk) {
11677
13100
  const { stream } = streamText2({
11678
13101
  model,
11679
13102
  ...params,
11680
- maxRetries: upstreamMaxRetries()
13103
+ maxRetries: upstreamMaxRetries(),
13104
+ onStepFinish: (step) => reportUnsupportedServiceTier(params, step.warnings)
11681
13105
  });
11682
13106
  const baseData = {
11683
13107
  id: `chatcmpl-${Date.now()}`,
@@ -11922,6 +13346,9 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
11922
13346
  onTokenRefreshed: (refreshed) => {
11923
13347
  model.apiKey = refreshed;
11924
13348
  },
13349
+ // Echo the exact requested id when it differs from the upstream id, so
13350
+ // clients that key context windows on the response model still resolve.
13351
+ responseModelOverride: typeof body.model === "string" && body.model !== upstreamModelId(model) ? body.model : void 0,
11925
13352
  onUpstreamError: options.inferenceLogPath ? (statusCode, errorContent) => writeInferenceResponseErrorLog(options.inferenceLogPath, {
11926
13353
  requestId,
11927
13354
  modelId: body.model,
@@ -11952,6 +13379,9 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
11952
13379
  modelId: body.model,
11953
13380
  effort: anthropicEffortFromRequest(body) ?? model.defaultEffort,
11954
13381
  claudeSessionId,
13382
+ // Use the adapter's route predicate and resolver so this records the same
13383
+ // pre-dispatch request intent. It does not prove SDK serialization.
13384
+ serviceTier: isOpenAiOAuthRoute(model) ? oauthServiceTier() : void 0,
11955
13385
  provider: inferenceProvider(model),
11956
13386
  route: "translated",
11957
13387
  requestPreview: getLatestMessagePreview(body.messages, body.system)
@@ -11961,7 +13391,7 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
11961
13391
  if (npmMaxTools !== void 0 && toolCount > npmMaxTools) {
11962
13392
  plog(`tools truncated: ${toolCount} \u2192 ${npmMaxTools} (provider limit)`);
11963
13393
  }
11964
- const openAiOAuth = model.npm === "@ai-sdk/openai" && model.authType === "oauth";
13394
+ const openAiOAuth = isOpenAiOAuthRoute(model);
11965
13395
  const params = translateRequest(body, model.npm, {
11966
13396
  defaultEffort: anthropicEffortFromRequest(body) ? void 0 : model.defaultEffort,
11967
13397
  openAiOAuth,
@@ -11972,6 +13402,7 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
11972
13402
  supportedParameters: model.supportedParameters,
11973
13403
  reasoning: model.reasoning,
11974
13404
  interleavedReasoningField: model.interleavedReasoningField,
13405
+ compatibility: model.compatibility,
11975
13406
  upstreamModelId: upstreamModelId(model)
11976
13407
  },
11977
13408
  maxTools: npmMaxTools
@@ -12147,12 +13578,13 @@ async function handleOpenAIChatCompletions(req, res, options, modelCache, plog)
12147
13578
  auditInference(options, {
12148
13579
  modelId: body.model,
12149
13580
  effort: openAiEffort(body),
13581
+ serviceTier: isOpenAiOAuthRoute(model) ? oauthServiceTier() : void 0,
12150
13582
  provider: inferenceProvider(model),
12151
13583
  route: "translated",
12152
13584
  requestPreview: getLatestMessagePreview(body.messages, body.system)
12153
13585
  });
12154
13586
  const baseURL = model.modelFormat === "anthropic" ? model.baseUrl : model.apiBaseUrl;
12155
- const openAiOAuth = npm === "@ai-sdk/openai" && model.authType === "oauth";
13587
+ const openAiOAuth = isOpenAiOAuthRoute(model);
12156
13588
  const params = translateOpenAiRequest(body, { openAiOAuth });
12157
13589
  const clientWantsStream = Boolean(body.stream);
12158
13590
  const responseModelId = getResponseModelId(body.model, model, options);
@@ -12252,6 +13684,7 @@ async function getOrInitLanguageModel(modelCache, model, npm, baseURL, apiKey, w
12252
13684
  headers: model.headers,
12253
13685
  useResponsesLite: model.useResponsesLite,
12254
13686
  preferWebSockets: model.preferWebSockets,
13687
+ compatibility: model.compatibility,
12255
13688
  onWebSocketDiagnostic: webSocketDiagnosticsLogPath ? (event) => writeWebSocketDiagnosticLog(webSocketDiagnosticsLogPath, event) : void 0
12256
13689
  });
12257
13690
  cached = { apiKey, languageModel };
@@ -12670,7 +14103,7 @@ function requestHeadersWithoutProxyHeaders(req) {
12670
14103
  }
12671
14104
  return headers;
12672
14105
  }
12673
- function forwardRawAnthropicRequest(req, res, rawBody, origin, rejectUnauthorized, onErrorResponse, onResponseUsage, lifecycle, isLocalShutdown = () => false) {
14106
+ function forwardRawAnthropicRequest(req, res, rawBody, origin, rejectUnauthorized, agent, onErrorResponse, onResponseUsage, lifecycle, isLocalShutdown = () => false) {
12674
14107
  return new Promise((resolve3) => {
12675
14108
  const startedAt = Date.now();
12676
14109
  let lastActivityAt = startedAt;
@@ -12730,7 +14163,8 @@ function forwardRawAnthropicRequest(req, res, rawBody, origin, rejectUnauthorize
12730
14163
  path: req.url,
12731
14164
  headers: requestHeadersWithoutProxyHeaders(req),
12732
14165
  servername: net.isIP(origin.hostname) ? void 0 : origin.hostname,
12733
- rejectUnauthorized
14166
+ rejectUnauthorized,
14167
+ agent
12734
14168
  }, (upstreamRes) => {
12735
14169
  headersReceived = true;
12736
14170
  statusCode = upstreamRes.statusCode ?? 502;
@@ -13071,6 +14505,8 @@ async function startHttpProxy(options) {
13071
14505
  reservedModelIds.add(normalizeRouteLookupId(modelId));
13072
14506
  }
13073
14507
  const anthropicOrigin = new URL2(options.anthropicOrigin ?? "https://api.anthropic.com");
14508
+ const anthropicProxyUrl = outboundProxyUrlForTarget(anthropicOrigin.href);
14509
+ let anthropicAgent;
13074
14510
  let adapter = options.adapterHandle ?? null;
13075
14511
  if (options.routes.length > 0) {
13076
14512
  adapter ??= await startProxyCatalog(
@@ -13153,6 +14589,9 @@ async function startHttpProxy(options) {
13153
14589
  claudeSessionId,
13154
14590
  modelId: typeof parsed?.model === "string" ? parsed.model : "unknown",
13155
14591
  effort: parsed ? anthropicEffortFromRequest(parsed) : void 0,
14592
+ // Only for the route that actually carries one, using the same
14593
+ // predicate and the same resolver the adapter applies.
14594
+ serviceTier: isOpenAiOAuthRoute(route) ? oauthServiceTier() : void 0,
13156
14595
  provider,
13157
14596
  route: route ? "translated" : "passthrough",
13158
14597
  stream: Boolean(parsed?.stream),
@@ -13196,6 +14635,7 @@ async function startHttpProxy(options) {
13196
14635
  rawBody,
13197
14636
  anthropicOrigin,
13198
14637
  options.anthropicRejectUnauthorized ?? true,
14638
+ anthropicAgent,
13199
14639
  messagesEndpoint === "messages" && options.inferenceLogPath ? (statusCode, errorContent) => writeInferenceResponseErrorLog(options.inferenceLogPath, {
13200
14640
  requestId,
13201
14641
  modelId: typeof parsed?.model === "string" ? parsed.model : "unknown",
@@ -13230,7 +14670,8 @@ async function startHttpProxy(options) {
13230
14670
  res,
13231
14671
  rawBody,
13232
14672
  anthropicOrigin,
13233
- options.anthropicRejectUnauthorized ?? true
14673
+ options.anthropicRejectUnauthorized ?? true,
14674
+ anthropicAgent
13234
14675
  );
13235
14676
  });
13236
14677
  const sockets = /* @__PURE__ */ new Set();
@@ -13289,6 +14730,17 @@ async function startHttpProxy(options) {
13289
14730
  adapter?.close();
13290
14731
  throw err;
13291
14732
  }
14733
+ if (anthropicProxyUrl && proxyUrlTargetsListener(
14734
+ anthropicProxyUrl,
14735
+ address.address,
14736
+ address.port
14737
+ )) {
14738
+ console.error(
14739
+ "clodex: HTTP(S)_PROXY points at this proxy; sending Anthropic passthrough direct"
14740
+ );
14741
+ } else {
14742
+ anthropicAgent = outboundHttpProxyAgent(anthropicOrigin.href);
14743
+ }
13292
14744
  return {
13293
14745
  host: options.host ?? "127.0.0.1",
13294
14746
  port: address.port,
@@ -13304,6 +14756,7 @@ async function startHttpProxy(options) {
13304
14756
  for (const socket of sockets) socket.destroy();
13305
14757
  await new Promise((resolve3) => proxyServer.close(() => resolve3()));
13306
14758
  mitmServer.close();
14759
+ anthropicAgent?.destroy();
13307
14760
  adapter?.close();
13308
14761
  }
13309
14762
  };
@@ -13509,7 +14962,7 @@ async function runHttpProxyServerCommand(debug = false, webSocketDiagnostics = f
13509
14962
 
13510
14963
  // src/server/index.ts
13511
14964
  function getLocalIps() {
13512
- const ifaces = networkInterfaces();
14965
+ const ifaces = networkInterfaces2();
13513
14966
  const result = [];
13514
14967
  for (const [name, iface] of Object.entries(ifaces)) {
13515
14968
  for (const addr of iface ?? []) {
@@ -13595,7 +15048,8 @@ function enrichServerModelReasoning(model) {
13595
15048
  apiBaseUrl: model.apiBaseUrl,
13596
15049
  supportedParameters: model.supportedParameters,
13597
15050
  reasoning: model.reasoning,
13598
- interleavedReasoningField: model.interleavedReasoningField
15051
+ interleavedReasoningField: model.interleavedReasoningField,
15052
+ compatibility: model.compatibility
13599
15053
  });
13600
15054
  if (!caps.defaultLevel) return model;
13601
15055
  return { ...model, defaultEffort: caps.defaultLevel };
@@ -14521,6 +15975,10 @@ function captureBuiltInPatchProofs(source, config, results) {
14521
15975
  "PATCH 9: default effort",
14522
15976
  /\/\*ccpatch:default-effort\*\/var _cce=Object\.assign\(Object\.create\(null\),\{[^{}]*\}\)\[String\([\w$]+\|\|""\)\.trim\(\)\.toLowerCase\(\)\];if\(_cce!==void 0\)return _cce;/
14523
15977
  );
15978
+ addPattern(
15979
+ "PATCH 10: child network environment",
15980
+ /\/\*ccpatch:child-network-env\*\/let _clodexChildEnv=process\.env,[\s\S]*?catch\(_clodexError\)\{\}\}/
15981
+ );
14524
15982
  return proofs;
14525
15983
  }
14526
15984
  function builtInPatchProofsChanged(source, proofs) {
@@ -14683,7 +16141,7 @@ function collectPristineFacts(args) {
14683
16141
  }
14684
16142
 
14685
16143
  // src/patch-transforms.ts
14686
- var PATCH_TRANSFORMS_VERSION = 3;
16144
+ var PATCH_TRANSFORMS_VERSION = 5;
14687
16145
  var NATIVE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
14688
16146
  var BASE_EFFORT_LEVELS = ["low", "medium", "high"];
14689
16147
  function projectNativeEffort(effort) {
@@ -14978,6 +16436,36 @@ function applyClodexPatches(source, config) {
14978
16436
  );
14979
16437
  }
14980
16438
  }
16439
+ {
16440
+ const patchName = "PATCH 10: child network environment";
16441
+ const marker = "/*ccpatch:child-network-env*/";
16442
+ const contractVar = q(NETWORK_ENV_CONTRACT_VAR);
16443
+ const networkVars = JSON.stringify(CHILD_NETWORK_ENV_VARS);
16444
+ const requiredBodyLiterals = [
16445
+ "{...process.env",
16446
+ "CLAUDE_CODE_REMOTE",
16447
+ "CLAUDE_CODE_OAUTH_TOKEN",
16448
+ "CLAUDE_CODE_SUBSCRIPTION_TYPE",
16449
+ "CLAUDE_BG_PTY_AUTH",
16450
+ '"OTEL_"',
16451
+ "CLAUDE_CODE_OTEL_DIAG_STDERR"
16452
+ ];
16453
+ applyOnce(
16454
+ patchName,
16455
+ /(function [\w$]+\(\)\{)(let [\w$]+=[\w$]+\(\),[\w$]+=Object\.keys\([\w$]+\)\.length>0,[\w$]+=Object\.keys\([\w$]+\)\.length>0,[\w$]+=[\w$]+\(process\.env\.CLAUDE_CODE_REMOTE\)\?(?:(?!\}\s*function )[\s\S])*?for\(let [\w$]+ of [\w$]+\)delete [\w$]+\[[\w$]+\],delete [\w$]+\[`INPUT_\$\{[\w$]+\}`\];return [\w$]+)(\})/,
16456
+ (_match, head, body, tail) => {
16457
+ const targetIsValid = requiredBodyLiterals.every((literal) => body.includes(literal)) && !/\bfunction\s*[\w$]*\(/.test(body);
16458
+ if (!targetIsValid) {
16459
+ log12("FAIL", patchName, "target validation failed");
16460
+ fail("clodex patch: child network environment target validation failed");
16461
+ }
16462
+ const restoredBody = body.replace(/process\.env/g, "_clodexChildEnv");
16463
+ const restore = marker + "let _clodexChildEnv=process.env,_clodexNetworkRaw=_clodexChildEnv[" + contractVar + "];if(_clodexNetworkRaw!==void 0){_clodexChildEnv={..._clodexChildEnv};delete _clodexChildEnv[" + contractVar + '];try{let _clodexNetwork=JSON.parse(_clodexNetworkRaw);if(_clodexNetwork&&typeof _clodexNetwork==="object"&&!Array.isArray(_clodexNetwork)&&_clodexNetwork.version===1&&_clodexNetwork.original&&typeof _clodexNetwork.original==="object"&&!Array.isArray(_clodexNetwork.original)&&_clodexNetwork.injected&&typeof _clodexNetwork.injected==="object"&&!Array.isArray(_clodexNetwork.injected)&&Object.keys(_clodexNetwork.original).every(_clodexKey=>' + networkVars + '.includes(_clodexKey)&&(typeof _clodexNetwork.original[_clodexKey]==="string"||_clodexNetwork.original[_clodexKey]===null)&&Object.prototype.hasOwnProperty.call(_clodexNetwork.injected,_clodexKey))&&Object.keys(_clodexNetwork.injected).every(_clodexKey=>' + networkVars + '.includes(_clodexKey)&&(typeof _clodexNetwork.injected[_clodexKey]==="string"||_clodexNetwork.injected[_clodexKey]===null)&&Object.prototype.hasOwnProperty.call(_clodexNetwork.original,_clodexKey)))for(let _clodexKey of ' + networkVars + '){if(Object.prototype.hasOwnProperty.call(_clodexNetwork.original,_clodexKey)&&Object.prototype.hasOwnProperty.call(_clodexNetwork.injected,_clodexKey)){let _clodexOriginal=_clodexNetwork.original[_clodexKey],_clodexInjected=_clodexNetwork.injected[_clodexKey],_clodexCurrent=_clodexChildEnv[_clodexKey]===void 0?null:_clodexChildEnv[_clodexKey];if((typeof _clodexOriginal==="string"||_clodexOriginal===null)&&(typeof _clodexInjected==="string"||_clodexInjected===null)&&_clodexCurrent===_clodexInjected){if(_clodexOriginal===null)delete _clodexChildEnv[_clodexKey];else _clodexChildEnv[_clodexKey]=_clodexOriginal}}}}catch(_clodexError){}}';
16464
+ return head + restore + restoredBody + tail;
16465
+ },
16466
+ { marker, required: true }
16467
+ );
16468
+ }
14981
16469
  return { content: js, results: report };
14982
16470
  }
14983
16471
 
@@ -15091,6 +16579,7 @@ function buildDesiredPatchConfig() {
15091
16579
  supportedParameters: model.supportedParameters,
15092
16580
  reasoning: model.reasoning ?? modelsDev?.reasoning,
15093
16581
  interleavedReasoningField: model.interleavedReasoningField ?? modelsDev?.interleaved?.field,
16582
+ compatibility: model.compatibility,
15094
16583
  upstreamModelId: upstreamModelId2
15095
16584
  });
15096
16585
  meta.set(`${provider.id}:${model.id}`, {
@@ -15570,7 +17059,7 @@ async function runLaunchPatchCheck(opts = {}) {
15570
17059
  }
15571
17060
 
15572
17061
  // src/cli.ts
15573
- var STARTER_CLAUDE_FLAGS = /* @__PURE__ */ new Set(["--dry-run", "--trace", "--endpoint", "--proxy", "--save-mode", "--help", "-h", "--version", "-v"]);
17062
+ var STARTER_CLAUDE_FLAGS = /* @__PURE__ */ new Set(["--dry-run", "--trace", "--fast", "--endpoint", "--proxy", "--save-mode", "--help", "-h", "--version", "-v"]);
15574
17063
  var CLODEX_LAUNCH_FLAGS = /* @__PURE__ */ new Set(["--provider", "--model"]);
15575
17064
  function parseClodexLaunchFlag(arg, rest, index, parsed) {
15576
17065
  if (arg === "--provider" || arg === "--model") {
@@ -15797,6 +17286,7 @@ function parseArgs(args) {
15797
17286
  }
15798
17287
  if (arg === "--dry-run") parsed.dryRun = true;
15799
17288
  if (arg === "--trace") parsed.trace = true;
17289
+ if (arg === "--fast") parsed.fast = true;
15800
17290
  consumeBridgeModeFlag(arg, parsed);
15801
17291
  if (arg === "--save-mode") parsed.saveBridgeMode = true;
15802
17292
  if (arg === "--help" || arg === "-h") parsed.showHelp = true;
@@ -15866,6 +17356,8 @@ ${pc13.bold("Options:")}
15866
17356
  --save-mode With --endpoint/--proxy: save that mode as the claude default
15867
17357
  --dry-run Run the wizard but show a preview instead of launching Claude Code
15868
17358
  --trace Write debug logs to ~/.clodex/logs/ and show errors on exit
17359
+ --fast Request Codex fast mode (service_tier=priority) on ChatGPT-OAuth models
17360
+ (equivalent to CLODEX_SERVICE_TIER=fast; warns if the SDK omits it)
15869
17361
  --provider Boot provider id (skip wizard when paired with --model or in print mode)
15870
17362
  --model Boot model id (skip wizard when paired with --provider or in print mode)
15871
17363
  --help Show this command help
@@ -16046,9 +17538,9 @@ ${pc13.bold("Behavior:")}
16046
17538
  permissions. Local failures are reported but never block the built-ins.
16047
17539
  Run clodex patch again after every claude update.`;
16048
17540
  }
16049
- function printHelp(text4) {
17541
+ function printHelp(text5) {
16050
17542
  console.log(`
16051
- ${text4}
17543
+ ${text5}
16052
17544
  `);
16053
17545
  }
16054
17546
  function reportInactiveCatalogAliases(modelAliases) {
@@ -16446,6 +17938,7 @@ async function runClaudeHttpProxyCommand(parsed, claudeArgs, agentStdout) {
16446
17938
  }
16447
17939
  async function runClaudeCommand(parsed) {
16448
17940
  const { dryRun, trace, launchProvider, launchModel } = parsed;
17941
+ if (parsed.fast) process.env.CLODEX_SERVICE_TIER = "fast";
16449
17942
  const claudeArgs = normalizeClaudeAgentArgs(parsed.claudeArgs);
16450
17943
  const agentStdout = wantsCleanAgentStdout("claude", claudeArgs);
16451
17944
  setAgentStdoutMode(agentStdout);
@@ -16515,7 +18008,12 @@ Error: ${launchPlan.error}
16515
18008
  catalogSpinner.stop("");
16516
18009
  }
16517
18010
  const allProviders = providersForTarget(providersForPicker(catalog), "claude");
18011
+ const blockedLaunchReason = launchPlan.skip && launchPlan.target?.providerId ? catalog.blockedProviders.get(launchPlan.target.providerId) : void 0;
16518
18012
  if (allProviders.length === 0) {
18013
+ if (blockedLaunchReason) {
18014
+ p12.log.error(blockedLaunchReason);
18015
+ return 1;
18016
+ }
16519
18017
  p12.log.warn("No providers available.");
16520
18018
  p12.log.info(pc13.dim("Run clodex providers to get started."));
16521
18019
  return 0;
@@ -16535,7 +18033,7 @@ Error: ${launchPlan.error}
16535
18033
  const resolved = findProviderAndModel(allProviders, launchPlan.target);
16536
18034
  if (!resolved) {
16537
18035
  p12.log.error(
16538
- `Provider/model not found: ${launchPlan.target.providerId} / ${launchPlan.target.modelId}`
18036
+ blockedLaunchReason ?? `Provider/model not found: ${launchPlan.target.providerId} / ${launchPlan.target.modelId}`
16539
18037
  );
16540
18038
  return 1;
16541
18039
  }
@@ -16730,6 +18228,7 @@ Error: ${launchPlan.error}
16730
18228
  interleavedReasoningField: selectedModel.interleavedReasoningField,
16731
18229
  useResponsesLite: selectedModel.useResponsesLite,
16732
18230
  preferWebSockets: selectedModel.preferWebSockets,
18231
+ compatibility: selectedModel.compatibility,
16733
18232
  headers: activeProvider.headers
16734
18233
  },
16735
18234
  launchApiKey ?? ""