@bman654/clodex 2.4.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-LBEJOEUY.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.4.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"] }
@@ -902,8 +1058,9 @@ function buildChildEnv(baseUrl, model, apiKey, proxyPort, contextWindow, enableG
902
1058
  applyClaudeCodeThirdPartyCompat(env);
903
1059
  return env;
904
1060
  }
905
- function buildHttpProxyChildEnv(proxyPort, caCertPath) {
906
- const env = { ...process.env };
1061
+ function buildHttpProxyChildEnv(proxyPort, caCertPath, baseEnv = process.env) {
1062
+ const baseline = networkEnvBaseline(baseEnv);
1063
+ const env = { ...baseline };
907
1064
  for (const name of CONFLICTING_ENV_VARS) {
908
1065
  if (name === "ANTHROPIC_API_KEY" || name === "ANTHROPIC_AUTH_TOKEN" || name === "ANTHROPIC_MODEL") continue;
909
1066
  delete env[name];
@@ -915,6 +1072,7 @@ function buildHttpProxyChildEnv(proxyPort, caCertPath) {
915
1072
  env["http_proxy"] = proxyUrl;
916
1073
  env["NODE_EXTRA_CA_CERTS"] = caCertPath;
917
1074
  removeAnthropicProxyBypass(env);
1075
+ recordNetworkEnvMutation(baseline, env);
918
1076
  return env;
919
1077
  }
920
1078
  function classifyKeyringError(err) {
@@ -952,7 +1110,10 @@ var KEYRING_GENERATION_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0
952
1110
  function oauthProviderIdFromAccount(account) {
953
1111
  const prefix = "oauth:provider:";
954
1112
  const baseAccount = credentialAccountBase(account);
955
- 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);
956
1117
  }
957
1118
  var oauthRefreshInflight = /* @__PURE__ */ new Map();
958
1119
  var OAUTH_CREDENTIAL_CACHE_MAX_AGE_MS = 3e4;
@@ -992,8 +1153,8 @@ function parseAuthRef(authRef) {
992
1153
  function clodexKeyEnvVar(providerId) {
993
1154
  return `CLODEX_KEY_${providerId.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
994
1155
  }
995
- function readEnvCredential(varName) {
996
- const raw = process.env[varName];
1156
+ function readEnvCredential(varName, env = process.env) {
1157
+ const raw = env[varName];
997
1158
  if (!raw?.trim()) return null;
998
1159
  return raw.trim().split(/\r?\n/)[0]?.trim() || null;
999
1160
  }
@@ -1017,6 +1178,26 @@ function usableEnvCredential(source, value, rejectedAccessToken) {
1017
1178
  }
1018
1179
  return value;
1019
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
+ }
1020
1201
  function readKeyringEntry(keyring, service, account) {
1021
1202
  const value = new keyring.Entry(service, account).getPassword();
1022
1203
  if (value !== null) return value;
@@ -2505,25 +2686,37 @@ async function deleteStoredCredential(ref, diag, blockLegacy = true) {
2505
2686
  return false;
2506
2687
  }
2507
2688
  }
2508
- async function resolveProviderCredential(providerId, authRef, diag, options = {}) {
2689
+ async function resolveProviderCredentialWithSource(providerId, authRef, diag, options = {}) {
2509
2690
  const parsed = parseAuthRef(authRef);
2510
- if (parsed?.kind === "none") return null;
2511
- const namespacedVar = clodexKeyEnvVar(providerId);
2512
- const namespaced = usableEnvCredential(
2513
- `provider:${providerId}`,
2514
- readEnvCredential(namespacedVar),
2515
- options.rejectedAccessToken
2516
- );
2517
- if (namespaced) return namespaced;
2518
- 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 };
2519
2700
  if (parsed.kind === "env") {
2520
- return usableEnvCredential(
2521
- `provider:${providerId}:env:${parsed.varName}`,
2522
- readEnvCredential(parsed.varName),
2523
- options.rejectedAccessToken
2524
- );
2701
+ return {
2702
+ credential: usableEnvCredential(
2703
+ `provider:${providerId}:env:${parsed.varName}`,
2704
+ readEnvCredential(parsed.varName),
2705
+ options.rejectedAccessToken
2706
+ )
2707
+ };
2525
2708
  }
2526
- 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;
2527
2720
  }
2528
2721
  async function resolveProviderOAuthAccountId(authRef, diag) {
2529
2722
  const parsed = parseAuthRef(authRef);
@@ -3461,12 +3654,23 @@ function applyPricingToRegistryProviders(registry, cache) {
3461
3654
  let changed = false;
3462
3655
  for (const provider of registry.providers) {
3463
3656
  if (provider.preserveModelPricing) continue;
3464
- if (!provider.modelsCache?.models.length) continue;
3465
3657
  const platform = TEMPLATE_TO_PRICING_PLATFORM[provider.templateId] ?? TEMPLATE_TO_PRICING_PLATFORM[provider.id];
3466
- const enriched = enrichModelsWithPricing(provider.modelsCache.models, index, platform);
3467
- if (JSON.stringify(enriched) !== JSON.stringify(provider.modelsCache.models)) {
3468
- 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;
3469
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
+ }
3470
3674
  }
3471
3675
  }
3472
3676
  if (changed) {
@@ -3774,6 +3978,48 @@ function isAnonymousProvider(provider) {
3774
3978
  function isLegacyAnonymousCustomEndpoint(provider, credential) {
3775
3979
  return provider.authType === void 0 && (provider.templateId === "custom-openai" || provider.templateId === "custom-anthropic") && provider.authRef === `keyring:provider:${provider.id}` && credential === "local";
3776
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
+ }
3777
4023
  function materializeOne(provider, resolveCredential, agent) {
3778
4024
  if (!provider.enabled) return null;
3779
4025
  if (!isValidProviderId(provider.id)) return null;
@@ -3821,19 +4067,37 @@ function materializeRegistry(registry, resolveCredential, opts) {
3821
4067
 
3822
4068
  // src/registry/load.ts
3823
4069
  async function loadRegistryProviders(diag, opts) {
3824
- 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 };
3825
4077
  const keys = /* @__PURE__ */ new Map();
3826
4078
  const oauthAccountIds = /* @__PURE__ */ new Map();
3827
4079
  const oauthProviderData = /* @__PURE__ */ new Map();
3828
- await Promise.all(registry.providers.map(async (provider) => {
3829
- 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;
3830
4084
  try {
3831
- const key = await resolveProviderCredential(provider.id, provider.authRef, diag);
3832
- if (key) keys.set(provider.id, key);
4085
+ resolved = await resolveProviderCredentialWithSource(provider.id, provider.authRef, diag);
3833
4086
  } catch (err) {
3834
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;
3835
4097
  }
3836
- 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) {
3837
4101
  try {
3838
4102
  const accountId = await resolveProviderOAuthAccountId(provider.authRef, diag);
3839
4103
  if (accountId) oauthAccountIds.set(provider.id, accountId);
@@ -3843,11 +4107,16 @@ async function loadRegistryProviders(diag, opts) {
3843
4107
  }
3844
4108
  }
3845
4109
  }));
3846
- 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) => ({
3847
4111
  ...provider,
3848
4112
  oauthAccountId: oauthAccountIds.get(provider.id),
3849
4113
  providerData: oauthProviderData.get(provider.id)
3850
4114
  }));
4115
+ Object.defineProperty(materialized, "blockedProviders", {
4116
+ value: blockedProviders,
4117
+ enumerable: false
4118
+ });
4119
+ return materialized;
3851
4120
  }
3852
4121
 
3853
4122
  // src/provider-templates.ts
@@ -3889,7 +4158,8 @@ function getTemplateById(id) {
3889
4158
 
3890
4159
  // src/provider-catalog.ts
3891
4160
  async function fetchProviderCatalog(opts) {
3892
- return loadRegistryProviders(void 0, opts);
4161
+ const warn = (message) => console.warn(message);
4162
+ return loadRegistryProviders(warn, { ...opts, warn });
3893
4163
  }
3894
4164
  function providersForPicker(providers) {
3895
4165
  for (const p13 of providers) {
@@ -3932,16 +4202,115 @@ function formatRegistryAuthLabel(provider) {
3932
4202
  }
3933
4203
  return provider.authRef;
3934
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
+ }
3935
4250
  async function resolveProvidersForDisplay() {
3936
4251
  const reg = loadRegistry();
3937
4252
  const entries = [];
3938
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;
3939
4298
  entries.push({
3940
4299
  id: provider.id,
3941
4300
  name: provider.name,
3942
- 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
+ })(),
3943
4312
  enabled: provider.enabled,
3944
- authLabel: formatRegistryAuthLabel(provider),
4313
+ authLabel,
3945
4314
  inRegistry: true
3946
4315
  });
3947
4316
  }
@@ -3990,6 +4359,8 @@ import { createHash as createHash3 } from "crypto";
3990
4359
  import { AsyncLocalStorage } from "async_hooks";
3991
4360
 
3992
4361
  // src/outbound-proxy.ts
4362
+ import { networkInterfaces } from "os";
4363
+ import { HttpsProxyAgent } from "https-proxy-agent";
3993
4364
  function hasOutboundProxyEnv(env = process.env) {
3994
4365
  return Boolean(
3995
4366
  env["HTTPS_PROXY"]?.trim() || env["https_proxy"]?.trim() || env["HTTP_PROXY"]?.trim() || env["http_proxy"]?.trim()
@@ -4025,6 +4396,26 @@ function outboundProxyUrlForTarget(targetUrl, env = process.env) {
4025
4396
  if (noProxyBypasses(parsed.hostname, env)) return void 0;
4026
4397
  return proxy.trim();
4027
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
+ }
4028
4419
  var dispatcherInstalled = false;
4029
4420
  async function installOutboundProxyDispatcher() {
4030
4421
  if (dispatcherInstalled) return true;
@@ -4041,11 +4432,24 @@ async function installOutboundProxyDispatcher() {
4041
4432
  return false;
4042
4433
  }
4043
4434
  }
4044
- async function outboundWsProxyAgent(wsUrl) {
4045
- const proxyUrl = outboundProxyUrlForTarget(wsUrl);
4435
+ function outboundHttpProxyAgent(targetUrl, env = process.env) {
4436
+ const proxyUrl = outboundProxyUrlForTarget(targetUrl, env);
4046
4437
  if (!proxyUrl) return void 0;
4047
- const { HttpsProxyAgent } = await import("https-proxy-agent");
4048
- 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);
4049
4453
  }
4050
4454
 
4051
4455
  // src/upstream-error.ts
@@ -4537,13 +4941,9 @@ function warnReasoningNormalizationGap(fields, log12) {
4537
4941
  if (warnedReasoningGaps.has(signature)) return;
4538
4942
  if (warnedReasoningGaps.size >= MAX_REASONING_GAP_WARNINGS) return;
4539
4943
  warnedReasoningGaps.add(signature);
4540
- try {
4541
- process.stderr.write(`${message}
4542
- `);
4543
- if (warnedReasoningGaps.size === MAX_REASONING_GAP_WARNINGS) {
4544
- process.stderr.write("clodex: warning: further reasoning-normalization warnings suppressed.\n");
4545
- }
4546
- } catch {
4944
+ emitParentNotice(message);
4945
+ if (warnedReasoningGaps.size === MAX_REASONING_GAP_WARNINGS) {
4946
+ emitParentNotice("clodex: warning: further reasoning-normalization warnings suppressed.");
4547
4947
  }
4548
4948
  }
4549
4949
  function toolArgumentNormalizationGap(expected, actual, requiredProps) {
@@ -4592,13 +4992,9 @@ function warnToolArgumentNormalizationGap(gap, log12) {
4592
4992
  if (warnedToolArgumentGaps.has(signature)) return;
4593
4993
  if (warnedToolArgumentGaps.size >= MAX_TOOL_ARGUMENT_GAP_WARNINGS) return;
4594
4994
  warnedToolArgumentGaps.add(signature);
4595
- try {
4596
- process.stderr.write(`${message}
4597
- `);
4598
- if (warnedToolArgumentGaps.size === MAX_TOOL_ARGUMENT_GAP_WARNINGS) {
4599
- process.stderr.write("clodex: warning: further tool-argument normalization warnings suppressed.\n");
4600
- }
4601
- } catch {
4995
+ emitParentNotice(message);
4996
+ if (warnedToolArgumentGaps.size === MAX_TOOL_ARGUMENT_GAP_WARNINGS) {
4997
+ emitParentNotice("clodex: warning: further tool-argument normalization warnings suppressed.");
4602
4998
  }
4603
4999
  }
4604
5000
  function continuationMismatchDetails(entry, payload, log12, warnOnGap = false) {
@@ -5036,12 +5432,12 @@ function expectedAssistantItems(ctx) {
5036
5432
  const type = accumulator.type ?? (typeof done.type === "string" ? done.type : void 0);
5037
5433
  if (type === "message") {
5038
5434
  const doneContent = Array.isArray(done.content) ? done.content : void 0;
5039
- const text4 = accumulator.text || (doneContent ? doneContent.filter((part) => part && typeof part === "object" && part.type === "output_text").map((part) => String(part.text ?? "")).join("") : "");
5040
- 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 }] });
5041
5437
  continue;
5042
5438
  }
5043
5439
  if (type === "reasoning") {
5044
- 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 : [];
5045
5441
  output.push({ ...withoutEphemeralFields(done), type: "reasoning", summary });
5046
5442
  continue;
5047
5443
  }
@@ -5260,7 +5656,7 @@ function transportReplaySafe(ctx) {
5260
5656
  function handleSocketMessage(entry, data) {
5261
5657
  const ctx = entry.current;
5262
5658
  if (!ctx || ctx.closed) return;
5263
- 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");
5264
5660
  ctx.frameCount += 1;
5265
5661
  if (ctx.transportRetryPending) {
5266
5662
  ctx.transportRetryPending = false;
@@ -5272,9 +5668,9 @@ function handleSocketMessage(entry, data) {
5272
5668
  }
5273
5669
  let event;
5274
5670
  try {
5275
- event = JSON.parse(text4);
5671
+ event = JSON.parse(text5);
5276
5672
  } catch {
5277
- ctx.pendingEvents.push(text4.replace(/\r?\n/g, " "));
5673
+ ctx.pendingEvents.push(text5.replace(/\r?\n/g, " "));
5278
5674
  flushPending(ctx);
5279
5675
  return;
5280
5676
  }
@@ -5815,8 +6211,8 @@ function buildClaudeCodeBillingSystemLine() {
5815
6211
  function systemBlockText(block) {
5816
6212
  if (typeof block === "string") return block;
5817
6213
  if (block && typeof block === "object" && "text" in block) {
5818
- const text4 = block.text;
5819
- return typeof text4 === "string" ? text4 : void 0;
6214
+ const text5 = block.text;
6215
+ return typeof text5 === "string" ? text5 : void 0;
5820
6216
  }
5821
6217
  return void 0;
5822
6218
  }
@@ -6734,7 +7130,12 @@ function isManagedCredentialAccount(account) {
6734
7130
  const base = credentialAccountBase2(account);
6735
7131
  if (!base) return false;
6736
7132
  const oauth = /^oauth:provider:(.+)$/.exec(base);
6737
- 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
+ }
6738
7139
  const provider = /^provider:([^:]+)(?::(.+))?$/.exec(base);
6739
7140
  if (!provider || !isValidProviderId(provider[1])) return false;
6740
7141
  const suffix = provider[2];
@@ -6886,7 +7287,7 @@ function appendError(errors, context, error) {
6886
7287
  errors.push(`${context}: ${errorMessage(error)}`);
6887
7288
  }
6888
7289
  function credentialIsReferenced(registry, authRef) {
6889
- 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));
6890
7291
  }
6891
7292
  async function journalCredentialWrite(authRef) {
6892
7293
  if (!await queueCredentialDelete(authRef)) {
@@ -7075,8 +7476,8 @@ function compactLogValueWithMarker(value, max) {
7075
7476
  function systemPreview(system) {
7076
7477
  if (typeof system === "string") return compactLogValue(system, REQUEST_PREVIEW_MAX) || void 0;
7077
7478
  if (!Array.isArray(system)) return void 0;
7078
- const text4 = system.map((block) => typeof block === "string" ? block : block && typeof block === "object" && typeof block.text === "string" ? block.text : "").filter(Boolean).join(" ");
7079
- 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;
7080
7481
  }
7081
7482
  function inlineSystemPreview(messages) {
7082
7483
  if (!Array.isArray(messages)) return void 0;
@@ -7102,9 +7503,9 @@ function getLatestMessagePreview(messages, system) {
7102
7503
  if (typeof content === "string") {
7103
7504
  summary = content;
7104
7505
  } else if (Array.isArray(content)) {
7105
- const text4 = content.filter((block) => Boolean(block && typeof block === "object")).filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join(" ");
7106
- if (text4.trim()) {
7107
- 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;
7108
7509
  } else {
7109
7510
  const blockTypes = [...new Set(content.filter((block) => Boolean(block && typeof block === "object")).map((block) => typeof block.type === "string" ? block.type : "unknown"))];
7110
7511
  if (blockTypes.length > 0) blockSummary = `${role}: [${blockTypes.join(", ")}]`;
@@ -7200,6 +7601,7 @@ function writeInferenceRequestLog(path, entry) {
7200
7601
  ...claudeSessionId ? { claudeSessionId } : {},
7201
7602
  modelId: compactLogValue(entry.modelId),
7202
7603
  ...entry.effort ? { effort: compactLogValue(entry.effort, 100) } : {},
7604
+ ...entry.serviceTier ? { serviceTier: compactLogValue(entry.serviceTier, 40) } : {},
7203
7605
  provider: compactLogValue(entry.provider, 200),
7204
7606
  route: entry.route,
7205
7607
  ...entry.stream !== void 0 ? { stream: entry.stream } : {},
@@ -7624,6 +8026,25 @@ async function fetchTemplateModels(template, apiKey, baseUrlOverride, extraHeade
7624
8026
  }
7625
8027
 
7626
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
+ }
7627
8048
  async function probeTemplatePackage(template) {
7628
8049
  if (!template.supported) return template.unsupportedReason ?? "Provider is not supported yet.";
7629
8050
  if (!template.npm) return "Template is missing an SDK package.";
@@ -7657,13 +8078,10 @@ async function addProviderFromTemplate(template, apiKey, opts) {
7657
8078
  const existingState = await withRegistryWriteLock(() => {
7658
8079
  const registry = loadRegistryStrict();
7659
8080
  const existing = registry.providers.find((p13) => p13.id === template.id);
7660
- if (existing && !opts?.replaceExisting) {
8081
+ const error = existingProviderError(template, existing, opts?.replaceExisting);
8082
+ if (error) {
7661
8083
  return {
7662
- error: {
7663
- added: false,
7664
- error: `${template.name} is already configured.`,
7665
- hint: `Remove it first with: clodex providers remove ${template.id}`
7666
- }
8084
+ error
7667
8085
  };
7668
8086
  }
7669
8087
  return { authRef: existing?.authRef ?? null, error: null };
@@ -7697,14 +8115,11 @@ async function addProviderFromTemplate(template, apiKey, opts) {
7697
8115
  const currentState = await withRegistryWriteLock(() => {
7698
8116
  const registry = loadRegistryStrict();
7699
8117
  const existing = registry.providers.find((p13) => p13.id === template.id);
7700
- if (existing && !opts?.replaceExisting) {
8118
+ const error = existingProviderError(template, existing, opts?.replaceExisting);
8119
+ if (error) {
7701
8120
  return {
7702
8121
  existingAuthRef: null,
7703
- error: {
7704
- added: false,
7705
- error: `${template.name} is already configured.`,
7706
- hint: `Remove it first with: clodex providers remove ${template.id}`
7707
- }
8122
+ error
7708
8123
  };
7709
8124
  }
7710
8125
  return { existingAuthRef: existing?.authRef ?? null, error: null };
@@ -7726,13 +8141,8 @@ async function addProviderFromTemplate(template, apiKey, opts) {
7726
8141
  return withRegistryWriteLock(async () => {
7727
8142
  const registry = loadRegistryStrict();
7728
8143
  const existing = registry.providers.find((p13) => p13.id === template.id);
7729
- if (existing && !opts?.replaceExisting) {
7730
- return {
7731
- added: false,
7732
- error: `${template.name} is already configured.`,
7733
- hint: `Remove it first with: clodex providers remove ${template.id}`
7734
- };
7735
- }
8144
+ const existingError = existingProviderError(template, existing, opts?.replaceExisting);
8145
+ if (existingError) return existingError;
7736
8146
  if ((existing?.authRef ?? null) !== currentState.existingAuthRef) {
7737
8147
  return {
7738
8148
  added: false,
@@ -7823,11 +8233,21 @@ async function removeProviderWithinLifecycle(id, opts) {
7823
8233
  credentialDeleted: false,
7824
8234
  error: `Provider not found: ${id}`
7825
8235
  },
7826
- authRef: null
8236
+ queuedRefs: []
7827
8237
  };
7828
8238
  }
7829
8239
  const [removedProvider] = registry.providers.splice(index, 1);
7830
- 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
+ }
7831
8251
  saveRegistry(registry);
7832
8252
  return {
7833
8253
  result: {
@@ -7836,14 +8256,14 @@ async function removeProviderWithinLifecycle(id, opts) {
7836
8256
  name: removedProvider.name,
7837
8257
  credentialDeleted: false
7838
8258
  },
7839
- authRef: cleanupQueued ? removedProvider.authRef : null
8259
+ queuedRefs
7840
8260
  };
7841
8261
  });
7842
- if (removal.authRef) {
8262
+ if (removal.queuedRefs.length > 0) {
7843
8263
  try {
7844
8264
  const cleanup = await reconcilePendingCredentialDeletes();
7845
- removal.result.credentialDeleted = cleanup.deleted.includes(removal.authRef);
7846
- 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;
7847
8267
  } catch {
7848
8268
  removal.result.credentialCleanupPending = true;
7849
8269
  }
@@ -7854,6 +8274,60 @@ async function removeProviderWithinLifecycle(id, opts) {
7854
8274
  async function removeProviderFromRegistry(id, opts) {
7855
8275
  return withProviderMutationLock(id, () => removeProviderWithinLifecycle(id, opts));
7856
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
+ }
7857
8331
  function toggleProviderEnabled(id) {
7858
8332
  return withRegistryWriteLockSync(() => {
7859
8333
  const registry = loadRegistryStrict();
@@ -8117,6 +8591,41 @@ function resolveModelSource(provider) {
8117
8591
  }
8118
8592
 
8119
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
+ }
8120
8629
  var PLACEHOLDER_KEYS = /* @__PURE__ */ new Set([
8121
8630
  "anything",
8122
8631
  "local",
@@ -8156,20 +8665,36 @@ function skipWithCachedModels(provider, reason) {
8156
8665
  reason
8157
8666
  };
8158
8667
  }
8159
- async function resolveRefreshCredential(provider, resolveKey) {
8160
- if (isAnonymousProvider(provider)) return null;
8161
- 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;
8162
8672
  try {
8163
- 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
+ };
8164
8687
  } catch {
8165
- key = null;
8688
+ resolved = { credential: null };
8166
8689
  }
8167
- if (!isLikelyPlaceholderKey(key)) return key;
8168
- 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] ?? []) {
8169
8694
  const fromEnv = process.env[envVar]?.trim();
8170
- if (fromEnv && !isLikelyPlaceholderKey(fromEnv)) return fromEnv;
8695
+ if (fromEnv && !isLikelyPlaceholderKey(fromEnv)) return { credential: fromEnv };
8171
8696
  }
8172
- return key;
8697
+ return resolved;
8173
8698
  }
8174
8699
 
8175
8700
  // src/data/openai-oauth-models.ts
@@ -8319,10 +8844,13 @@ async function refreshOpenAiOAuthModels(accessToken) {
8319
8844
  if (chatGptEntries.length > 0) {
8320
8845
  return { models: toModels(chatGptEntries), source: "live" };
8321
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));
8322
8849
  return {
8323
8850
  models: [...seedById.values()],
8324
8851
  source: "seed",
8325
- failureReason: chatGptResult.error ?? codexResult.error
8852
+ failureReason: credentialFailure ?? chatGptResult.error ?? codexResult.error,
8853
+ credentialRejected: credentialFailure !== void 0
8326
8854
  };
8327
8855
  }
8328
8856
  async function refreshApiListProvider(provider, apiKey) {
@@ -8375,32 +8903,111 @@ async function refreshApiListProvider(provider, apiKey) {
8375
8903
  baseUrl: fetched.baseUrl
8376
8904
  };
8377
8905
  }
8378
- function updateProviderCache(registry, providerId, models, baseUrl) {
8906
+ function updateProviderCache(registry, providerId, models, baseUrl, credentialSnapshot) {
8379
8907
  const idx = registry.providers.findIndex((p13) => p13.id === providerId);
8380
8908
  if (idx < 0) return;
8381
8909
  const now = (/* @__PURE__ */ new Date()).toISOString();
8382
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;
8383
8922
  registry.providers[idx] = {
8384
8923
  ...existing,
8385
- refreshedAt: now,
8386
8924
  api: baseUrl ? { ...existing.api, url: baseUrl } : existing.api,
8387
- modelsCache: {
8388
- fetchedAt: now,
8389
- models
8390
- }
8925
+ ...authAccounts ? { authAccounts } : {},
8926
+ ...!temporaryAccount ? { refreshedAt: now, modelsCache } : {}
8391
8927
  };
8392
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
+ }
8393
8944
  function providerDiscoveryInputsMatch(current, started) {
8394
- 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
+ }
8395
8975
  }
8396
- async function refreshProviderModels(providerId, apiKey, registry) {
8976
+ async function refreshProviderModels(providerId, apiKey, registry, credentialSnapshot) {
8397
8977
  const workingRegistry = registry ?? loadRegistryStrict();
8398
8978
  const provider = workingRegistry.providers.find((p13) => p13.id === providerId);
8399
8979
  if (!provider) {
8400
8980
  return { id: providerId, name: providerId, ok: false, reason: "Provider not found." };
8401
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
+ }
8402
9001
  const source = resolveModelSource(provider);
8403
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
+ }
8404
9011
  return {
8405
9012
  id: provider.id,
8406
9013
  name: provider.name,
@@ -8410,7 +9017,8 @@ async function refreshProviderModels(providerId, apiKey, registry) {
8410
9017
  };
8411
9018
  }
8412
9019
  try {
8413
- 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;
8414
9022
  let models = [];
8415
9023
  let baseUrl;
8416
9024
  let oauthFallbackReason;
@@ -8423,11 +9031,21 @@ async function refreshProviderModels(providerId, apiKey, registry) {
8423
9031
  reason: "OAuth token not available \u2014 try signing in again with clodex providers auth."
8424
9032
  };
8425
9033
  }
8426
- const oauthResult = await refreshOAuthProvider(provider, apiKey);
9034
+ const oauthResult = await refreshOAuthProvider(cacheProvider, apiKey);
8427
9035
  const failureDetail = oauthResult.failureReason ? ` (${oauthResult.failureReason})` : "";
8428
- 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) {
8429
9047
  return skipWithCachedModels(
8430
- provider,
9048
+ cacheProvider,
8431
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.`
8432
9050
  );
8433
9051
  }
@@ -8448,11 +9066,21 @@ async function refreshProviderModels(providerId, apiKey, registry) {
8448
9066
  const keyOptional = template?.apiKeyOptional === true;
8449
9067
  const effectiveKey = keyOptional && isLikelyPlaceholderKey(apiKey) ? "" : apiKey;
8450
9068
  if (!keyOptional && isLikelyPlaceholderKey(effectiveKey)) {
8451
- if (cachedModelCount(provider) > 0) {
8452
- return skipWithCachedModels(
8453
- provider,
8454
- "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."
8455
- );
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
+ };
8456
9084
  }
8457
9085
  return {
8458
9086
  id: provider.id,
@@ -8471,11 +9099,15 @@ async function refreshProviderModels(providerId, apiKey, registry) {
8471
9099
  }
8472
9100
  const fetched = await refreshApiListProvider(provider, effectiveKey ?? "");
8473
9101
  if (fetched.error) {
8474
- if ((fetched.error.includes("rejected") || fetched.error.includes("401") || fetched.error.includes("403")) && cachedModelCount(provider) > 0) {
8475
- return skipWithCachedModels(
8476
- provider,
8477
- `${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.`
8478
- );
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
+ };
8479
9111
  }
8480
9112
  return { id: provider.id, name: provider.name, ok: false, reason: fetched.error };
8481
9113
  }
@@ -8489,13 +9121,16 @@ async function refreshProviderModels(providerId, apiKey, registry) {
8489
9121
  const currentRegistry = loadRegistryStrict();
8490
9122
  const currentProvider = currentRegistry.providers.find((candidate) => candidate.id === providerId);
8491
9123
  if (!currentProvider) throw new Error("Provider was removed while models were refreshing.");
9124
+ if (credentialSnapshot) {
9125
+ assertRefreshCredentialStillCurrent(currentProvider, credentialSnapshot);
9126
+ }
8492
9127
  if (currentProvider.authRef !== provider.authRef) {
8493
9128
  throw new Error("Provider credentials changed while models were refreshing.");
8494
9129
  }
8495
9130
  if (!providerDiscoveryInputsMatch(currentProvider, provider)) {
8496
9131
  throw new Error("Provider configuration changed while models were refreshing.");
8497
9132
  }
8498
- updateProviderCache(currentRegistry, providerId, enriched, baseUrl);
9133
+ updateProviderCache(currentRegistry, providerId, enriched, baseUrl, credentialSnapshot);
8499
9134
  saveRegistry(currentRegistry);
8500
9135
  });
8501
9136
  enrichPricingAsync();
@@ -8504,7 +9139,7 @@ async function refreshProviderModels(providerId, apiKey, registry) {
8504
9139
  name: provider.name,
8505
9140
  ok: true,
8506
9141
  modelCount: enriched.length,
8507
- previousModelCount: provider.refreshedAt ? previousModelCount : void 0,
9142
+ previousModelCount: hadPreviousRefresh ? previousModelCount : void 0,
8508
9143
  reason: oauthFallbackReason
8509
9144
  };
8510
9145
  } catch (err) {
@@ -8516,13 +9151,63 @@ async function refreshProviderModels(providerId, apiKey, registry) {
8516
9151
  };
8517
9152
  }
8518
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
+ }
8519
9190
  async function refreshAllProviderModels(resolveKey) {
8520
9191
  const refreshed = [];
8521
9192
  const registry = loadRegistryStrict();
8522
9193
  const enabledProviders = registry.providers.filter((p13) => p13.enabled);
8523
9194
  for (const provider of enabledProviders) {
8524
- const key = await resolveRefreshCredential(provider, resolveKey);
8525
- 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
+ }
8526
9211
  }
8527
9212
  return { refreshed };
8528
9213
  }
@@ -8531,6 +9216,15 @@ async function refreshAllProviderModels(resolveKey) {
8531
9216
  import pc3 from "picocolors";
8532
9217
  import * as p2 from "@clack/prompts";
8533
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
+ }
8534
9228
  var OPENAI_DISPLAY = "OpenAI ChatGPT Plus/Pro";
8535
9229
  var PROVIDER_DISPLAY = {
8536
9230
  openai: OPENAI_DISPLAY,
@@ -8560,6 +9254,47 @@ async function runNativeDeviceCode(providerId) {
8560
9254
  throw err;
8561
9255
  }
8562
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
+ }
8563
9298
  function oauthDisplayName(registryId, fallbackName) {
8564
9299
  if (registryId === "openai-oauth") return "OpenAI (ChatGPT)";
8565
9300
  return fallbackName;
@@ -8571,7 +9306,7 @@ async function upsertOAuthProvider(providerId, authRef, expectedAuthRef) {
8571
9306
  const registry = loadRegistryStrict();
8572
9307
  const template = getTemplateById(templateId);
8573
9308
  let entry = registry.providers.find((pr) => pr.id === registryId);
8574
- if (entry?.authRef !== expectedAuthRef) {
9309
+ if ((entry ? providerDefaultAuthRef(entry) : void 0) !== expectedAuthRef) {
8575
9310
  throw new Error(`Provider "${registryId}" changed while its credential was being saved`);
8576
9311
  }
8577
9312
  if (!entry) {
@@ -8579,7 +9314,7 @@ async function upsertOAuthProvider(providerId, authRef, expectedAuthRef) {
8579
9314
  throw new Error(`Provider "${providerId}" is not in your registry and has no template`);
8580
9315
  }
8581
9316
  }
8582
- const previousAuthRef = entry?.authRef;
9317
+ const previousAuthRef = entry ? providerDefaultAuthRef(entry) : void 0;
8583
9318
  if (!entry) {
8584
9319
  if (!template) throw new Error(`Provider "${providerId}" has no template`);
8585
9320
  const displayName = oauthDisplayName(registryId, template.name);
@@ -8597,8 +9332,34 @@ async function upsertOAuthProvider(providerId, authRef, expectedAuthRef) {
8597
9332
  },
8598
9333
  addedAt: (/* @__PURE__ */ new Date()).toISOString()
8599
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
+ }
8600
9357
  } else {
8601
9358
  entry = { ...entry, authType: "oauth", authRef, templateId };
9359
+ delete entry.defaultAuthRef;
9360
+ delete entry.defaultModelsCache;
9361
+ delete entry.modelsCache;
9362
+ delete entry.refreshedAt;
8602
9363
  }
8603
9364
  const idx = registry.providers.findIndex((provider) => provider.id === registryId);
8604
9365
  if (idx >= 0) registry.providers[idx] = entry;
@@ -8614,9 +9375,9 @@ async function upsertOAuthProvider(providerId, authRef, expectedAuthRef) {
8614
9375
  return entry;
8615
9376
  });
8616
9377
  }
8617
- async function persistNativeOAuthCredential(providerId, cred) {
9378
+ async function persistNativeOAuthCredential(providerId, cred, accountName) {
8618
9379
  const registryId = toOAuthRegistryId(providerId);
8619
- const account = `oauth:provider:${registryId}`;
9380
+ const account = accountName ? `oauth:provider:${registryId}:account:${accountName}` : `oauth:provider:${registryId}`;
8620
9381
  const registryProvider = await withProviderMutationLock(registryId, async () => {
8621
9382
  const existingAuthRef = await withRegistryWriteLock(
8622
9383
  () => {
@@ -8626,7 +9387,17 @@ async function persistNativeOAuthCredential(providerId, cred) {
8626
9387
  if (!existing && !getTemplateById(templateId)) {
8627
9388
  throw new Error(`Provider "${providerId}" is not in your registry and has no template`);
8628
9389
  }
8629
- 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;
8630
9401
  }
8631
9402
  );
8632
9403
  const authRef = credentialInstanceAuthRef(account);
@@ -8643,7 +9414,7 @@ async function persistNativeOAuthCredential(providerId, cred) {
8643
9414
  `Could not save OAuth tokens to the credential store${diagMsg ? ` \u2014 ${diagMsg}` : " \u2014 check access and try again"}`
8644
9415
  );
8645
9416
  }
8646
- return upsertOAuthProvider(providerId, authRef, existingAuthRef);
9417
+ return accountName ? upsertOAuthAccountSlot(registryId, accountName, authRef, existingAuthRef) : upsertOAuthProvider(providerId, authRef, existingAuthRef);
8647
9418
  });
8648
9419
  });
8649
9420
  let credentialCleanupPending = true;
@@ -8658,11 +9429,25 @@ async function persistNativeOAuthCredential(providerId, cred) {
8658
9429
  credentialCleanupPending
8659
9430
  };
8660
9431
  }
8661
- async function authenticateProvider(providerId, _options = {}) {
9432
+ async function authenticateProvider(providerId, options = {}) {
8662
9433
  const registryId = toOAuthRegistryId(providerId);
9434
+ const accountName = options.account === void 0 ? void 0 : validateOAuthAccountName(options.account);
8663
9435
  if (!supportsNativeOAuth(providerId)) {
8664
9436
  throw new Error("OAuth sign-in is only available for openai (ChatGPT Plus/Pro).");
8665
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
+ }
8666
9451
  let storeDiagMsg = "";
8667
9452
  const storeReady = await probeProviderCredentialStore(oauthAuthRef(registryId), (msg) => {
8668
9453
  storeDiagMsg = msg;
@@ -8673,12 +9458,29 @@ async function authenticateProvider(providerId, _options = {}) {
8673
9458
  );
8674
9459
  }
8675
9460
  const cred = await runNativeDeviceCode(providerId);
8676
- const persisted = await persistNativeOAuthCredential(providerId, cred);
9461
+ const persisted = await persistNativeOAuthCredential(providerId, cred, accountName);
8677
9462
  const refreshSpinner = p2.spinner();
8678
9463
  refreshSpinner.start("Refreshing model list...");
8679
9464
  try {
8680
- await refreshProviderModels(registryId, cred.access);
8681
- 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
+ }
8682
9484
  } catch {
8683
9485
  refreshSpinner.stop("Could not refresh models \u2014 run clodex providers refresh-models later");
8684
9486
  }
@@ -8694,9 +9496,15 @@ function providerAuthHelpText() {
8694
9496
 
8695
9497
  ${pc3.bold("Usage:")}
8696
9498
  clodex providers auth openai
9499
+ clodex providers auth openai --account work
8697
9500
 
8698
9501
  ${pc3.bold("Device code (works on SSH/VPS):")}
8699
- 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`;
8700
9508
  }
8701
9509
 
8702
9510
  // src/prompts.ts
@@ -9045,10 +9853,19 @@ function parseProvidersArgs(args) {
9045
9853
  if (first === "auth") {
9046
9854
  if (rest.length === 0) return { subcommand: "auth", showHelp: true };
9047
9855
  let authMethod;
9856
+ let authAccount;
9048
9857
  const positional = [];
9049
- for (const arg of rest) {
9858
+ for (let i = 0; i < rest.length; i++) {
9859
+ const arg = rest[i];
9050
9860
  if (arg === "--native") authMethod = "native";
9051
- 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("-")) {
9052
9869
  return { subcommand: "auth", showHelp: false, error: `Unknown auth option: ${arg}` };
9053
9870
  } else {
9054
9871
  positional.push(arg);
@@ -9057,7 +9874,7 @@ function parseProvidersArgs(args) {
9057
9874
  if (positional.length !== 1) {
9058
9875
  return { subcommand: "auth", showHelp: false, error: "Usage: clodex providers auth <id>" };
9059
9876
  }
9060
- return { subcommand: "auth", showHelp: false, removeId: positional[0], authMethod };
9877
+ return { subcommand: "auth", showHelp: false, removeId: positional[0], authMethod, authAccount };
9061
9878
  }
9062
9879
  if (first === "remove") {
9063
9880
  if (rest.length === 0) return { subcommand: "remove", showHelp: false, error: "Usage: clodex providers remove <id>" };
@@ -9090,13 +9907,133 @@ ${pc5.bold("Subcommands:")}
9090
9907
  remove Remove a provider by id
9091
9908
  refresh-models Update cached model lists`;
9092
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
+ }
9093
10027
  function providerLabel(name, modelCount, enabled) {
9094
10028
  return `${fmtEnabledStar(enabled)} ${fmtProvider(name)} ${pc5.dim(`(${modelCount} model${modelCount === 1 ? "" : "s"})`)}`;
9095
10029
  }
9096
- async function runProvidersAuthWithCleanupState(providerId, method, cleanupState) {
10030
+ async function runProvidersAuthWithCleanupState(providerId, method, cleanupState, account) {
9097
10031
  try {
9098
- const result = await authenticateProvider(providerId, { method });
9099
- 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
+ );
9100
10037
  reportCredentialCleanup(result.credentialCleanupPending, cleanupState, true);
9101
10038
  return 0;
9102
10039
  } catch (err) {
@@ -9111,8 +10048,8 @@ async function runProvidersAuthWithCleanupState(providerId, method, cleanupState
9111
10048
  async function runProvidersAuth(providerId, method) {
9112
10049
  return runProvidersAuthWithCleanupState(providerId, method);
9113
10050
  }
9114
- async function runProvidersRefreshModels(providerId) {
9115
- 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);
9116
10053
  if (providerId) {
9117
10054
  const registry = loadRegistry();
9118
10055
  const provider = registry.providers.find((p13) => p13.id === providerId);
@@ -9122,11 +10059,25 @@ async function runProvidersRefreshModels(providerId) {
9122
10059
  }
9123
10060
  const spinner6 = p4.spinner();
9124
10061
  spinner6.start(`Refreshing ${provider.name}...`);
9125
- const key = await resolveRefreshCredential(
9126
- provider,
9127
- async (p13) => resolveProviderCredential(p13.id, p13.authRef)
9128
- );
9129
- 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
+ }
9130
10081
  spinner6.stop("");
9131
10082
  if (result.skipped) {
9132
10083
  const countNote = result.modelCount ? ` (${result.modelCount} cached models kept)` : "";
@@ -9300,8 +10251,22 @@ async function runProviderDetail(id) {
9300
10251
  const registry = loadRegistry();
9301
10252
  const provider = registry.providers.find((pr) => pr.id === id);
9302
10253
  if (!provider) return "back";
9303
- const modelCount = provider.modelsCache?.models.length ?? 0;
9304
- 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);
9305
10270
  printProviderDetailPanel(provider.name, modelCount, authLabel);
9306
10271
  const detailOptions = [];
9307
10272
  if (modelCount > 0) {
@@ -9316,11 +10281,29 @@ async function runProviderDetail(id) {
9316
10281
  label: "Refresh model list",
9317
10282
  hint: "Fetch latest models from the provider API"
9318
10283
  });
10284
+ const accountSlots = Object.keys(provider.authAccounts ?? {}).sort();
9319
10285
  if (supportsNativeOAuth(id) || provider.authType === "oauth") {
9320
10286
  detailOptions.push({
9321
10287
  value: "auth",
9322
10288
  label: "Sign in again (OAuth)",
9323
- 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)
9324
10307
  });
9325
10308
  }
9326
10309
  detailOptions.push(
@@ -9338,8 +10321,8 @@ async function runProviderDetail(id) {
9338
10321
  });
9339
10322
  if (p4.isCancel(action) || action === "back") return "back";
9340
10323
  if (action === "browse") {
9341
- const cachedModels = provider.modelsCache?.models ?? [];
9342
- 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);
9343
10326
  const localProvider = {
9344
10327
  id: provider.id,
9345
10328
  name: provider.name,
@@ -9357,6 +10340,71 @@ async function runProviderDetail(id) {
9357
10340
  await runWithCredentialCleanup((state) => runProvidersAuthWithCleanupState(id, void 0, state));
9358
10341
  return "back";
9359
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
+ }
9360
10408
  if (action === "toggle") {
9361
10409
  const result = toggleProviderEnabled(id);
9362
10410
  if (result.toggled) {
@@ -9385,6 +10433,12 @@ async function runProvidersHub() {
9385
10433
  const configuredIds = new Set(entries.map((entry) => entry.id));
9386
10434
  if (listVisibleOAuthTemplates(configuredIds).length > 0) {
9387
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
+ });
9388
10442
  }
9389
10443
  if (entries.length > 0) {
9390
10444
  options.push({ value: "refresh-all", label: "\u21BA Refresh all models", hint: "Update model lists for all providers" });
@@ -9409,6 +10463,23 @@ async function runProvidersHub() {
9409
10463
  await runWithCredentialCleanup((state) => runProvidersAuthWithCleanupState("openai", void 0, state));
9410
10464
  continue;
9411
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
+ }
9412
10483
  if (typeof choice === "string" && choice.startsWith("provider:")) {
9413
10484
  const id = choice.slice("provider:".length);
9414
10485
  const outcome = await runProviderDetail(id);
@@ -9443,7 +10514,7 @@ async function runProvidersCommand(args) {
9443
10514
  console.log(providerAuthHelpText());
9444
10515
  return 0;
9445
10516
  }
9446
- return runWithCredentialCleanup((state) => runProvidersAuthWithCleanupState(parsed.removeId, parsed.authMethod, state));
10517
+ return runWithCredentialCleanup((state) => runProvidersAuthWithCleanupState(parsed.removeId, parsed.authMethod, state, parsed.authAccount));
9447
10518
  }
9448
10519
  relayIntro("Your OpenAI providers");
9449
10520
  return runProvidersHub();
@@ -9484,7 +10555,7 @@ async function runFirstRunWizard(_trace = false) {
9484
10555
 
9485
10556
  // src/proxy.ts
9486
10557
  import { createServer } from "http";
9487
- 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";
9488
10559
 
9489
10560
  // src/http-utils.ts
9490
10561
  import * as zlib from "zlib";
@@ -10133,10 +11204,10 @@ async function relayAnthropicMessages(res, messagesUrl, body, apiKey, clientWant
10133
11204
  res.end(JSON.stringify({ type: "error", error: { type: "api_error", message: "Upstream returned empty response body" } }));
10134
11205
  return;
10135
11206
  }
10136
- let text4 = await upstreamRes.text();
11207
+ let text5 = await upstreamRes.text();
10137
11208
  let parsed;
10138
11209
  try {
10139
- parsed = JSON.parse(text4);
11210
+ parsed = JSON.parse(text5);
10140
11211
  } catch {
10141
11212
  res.writeHead(502, { "Content-Type": "application/json" });
10142
11213
  res.end(JSON.stringify({ type: "error", error: { type: "api_error", message: "Upstream response was not valid JSON" } }));
@@ -10144,13 +11215,13 @@ async function relayAnthropicMessages(res, messagesUrl, body, apiKey, clientWant
10144
11215
  }
10145
11216
  if (options.responseModelOverride && parsed && typeof parsed === "object" && !Array.isArray(parsed) && typeof parsed.model === "string") {
10146
11217
  parsed.model = options.responseModelOverride;
10147
- text4 = JSON.stringify(parsed);
11218
+ text5 = JSON.stringify(parsed);
10148
11219
  }
10149
11220
  res.writeHead(200, {
10150
11221
  "Content-Type": "application/json",
10151
- "Content-Length": Buffer.byteLength(text4).toString()
11222
+ "Content-Length": Buffer.byteLength(text5).toString()
10152
11223
  });
10153
- res.end(text4);
11224
+ res.end(text5);
10154
11225
  }
10155
11226
 
10156
11227
  // src/proxy.ts
@@ -10306,7 +11377,7 @@ function reportOnce(raw, message, warn) {
10306
11377
  } catch {
10307
11378
  }
10308
11379
  }
10309
- function upstreamMaxRetries(env = process.env, warn = (message) => console.error(`clodex: ${message}`)) {
11380
+ function upstreamMaxRetries(env = process.env, warn = (message) => emitParentNotice(`clodex: ${message}`)) {
10310
11381
  const raw = env[UPSTREAM_MAX_RETRIES_ENV]?.trim();
10311
11382
  if (raw === void 0 || raw === "") return void 0;
10312
11383
  const value = Number(raw);
@@ -10375,10 +11446,10 @@ function supportsOpenAiPromptCacheBreakpoints(modelId) {
10375
11446
  const minor = Number(match[2] ?? 0);
10376
11447
  return major > 5 || major === 5 && minor >= 6;
10377
11448
  }
10378
- function stripClaudeCodeBillingHeader(text4) {
10379
- if (!text4.startsWith(CLAUDE_CODE_BILLING_HEADER_PREFIX)) return text4;
10380
- const newline = text4.indexOf("\n");
10381
- 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);
10382
11453
  }
10383
11454
  function systemToString(system, stripAnthropicBillingHeader = false) {
10384
11455
  if (!system) return void 0;
@@ -10387,8 +11458,8 @@ function systemToString(system, stripAnthropicBillingHeader = false) {
10387
11458
  }
10388
11459
  const blocks = system.map((b) => typeof b === "string" ? b : b.text ?? "");
10389
11460
  if (!stripAnthropicBillingHeader) return blocks.join("\n");
10390
- return blocks.flatMap((text4) => {
10391
- const stripped = stripClaudeCodeBillingHeader(text4);
11461
+ return blocks.flatMap((text5) => {
11462
+ const stripped = stripClaudeCodeBillingHeader(text5);
10392
11463
  return stripped === void 0 ? [] : [stripped];
10393
11464
  }).join("\n");
10394
11465
  }
@@ -10404,12 +11475,12 @@ function translateTopLevelSystemForOpenAi(system) {
10404
11475
  }
10405
11476
  return system.flatMap((block) => {
10406
11477
  const raw = typeof block === "string" ? block : block.text ?? "";
10407
- const text4 = stripClaudeCodeBillingHeader(raw) ?? "";
10408
- if (!text4.trim()) return [];
11478
+ const text5 = stripClaudeCodeBillingHeader(raw) ?? "";
11479
+ if (!text5.trim()) return [];
10409
11480
  const cacheControl = typeof block === "string" ? void 0 : block.cache_control;
10410
11481
  return [{
10411
11482
  role: "system",
10412
- content: text4,
11483
+ content: text5,
10413
11484
  ...cacheControl ? { providerOptions: { openai: { promptCacheBreakpoint: { mode: "explicit" } } } } : {}
10414
11485
  }];
10415
11486
  });
@@ -10466,9 +11537,9 @@ function annotateToolNames(messages) {
10466
11537
  }
10467
11538
  }
10468
11539
  function thinkingToSdkPart(block, npm) {
10469
- const text4 = block.thinking ?? "";
10470
- if (npm === "@ai-sdk/openai" && !block.signature && !text4.trim()) return null;
10471
- 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 };
10472
11543
  if (block.signature) {
10473
11544
  if (npm === "@ai-sdk/google") {
10474
11545
  part.providerOptions = { google: { thoughtSignature: block.signature } };
@@ -10584,8 +11655,8 @@ function isClaudeCodeStructuredOutputCompactRequest(body) {
10584
11655
  if (!body.tools?.some((candidate) => candidate.name === "StructuredOutput")) return false;
10585
11656
  const finalMessage = body.messages.at(-1);
10586
11657
  if (!finalMessage || finalMessage.role !== "user") return false;
10587
- const text4 = typeof finalMessage.content === "string" ? finalMessage.content : finalMessage.content.filter((block) => block.type === "text").map((block) => block.text ?? "").join("\n");
10588
- 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);
10589
11660
  }
10590
11661
  function translateRequest(body, npm, options) {
10591
11662
  const messages = body.messages ?? [];
@@ -10614,10 +11685,12 @@ function translateRequest(body, npm, options) {
10614
11685
  const supportsExplicitOpenAiCaching = !options?.openAiOAuth && supportsOpenAiPromptCacheBreakpoints(upstreamModelId2);
10615
11686
  if (npm === "@ai-sdk/openai") {
10616
11687
  const claudeSessionId = extractClaudeSessionId(body, options?.claudeSessionId);
11688
+ const serviceTier = options?.openAiOAuth ? oauthServiceTier() : void 0;
10617
11689
  providerOptions = deepMergeProviderOptions(providerOptions, {
10618
11690
  openai: {
10619
11691
  promptCacheKey: claudeSessionId ? claudeSessionPromptCacheKey(claudeSessionId) : openAiPromptCacheKey(baseSystem, upstreamTools),
10620
- ...supportsExplicitOpenAiCaching ? { promptCacheOptions: { mode: "implicit", ttl: "30m" } } : {}
11692
+ ...supportsExplicitOpenAiCaching ? { promptCacheOptions: { mode: "implicit", ttl: "30m" } } : {},
11693
+ ...serviceTier ? { serviceTier } : {}
10621
11694
  }
10622
11695
  });
10623
11696
  }
@@ -10635,6 +11708,35 @@ function translateRequest(body, npm, options) {
10635
11708
  providerOptions
10636
11709
  };
10637
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
+ }
10638
11740
  function toAnthropicUsage(u) {
10639
11741
  const total = u?.inputTokens ?? 0;
10640
11742
  const cacheRead = u?.inputTokenDetails?.cacheReadTokens ?? u?.cachedInputTokens ?? 0;
@@ -10884,7 +11986,8 @@ async function streamAnthropicResponse(model, params, modelId, write, log12, obs
10884
11986
  maxRetries: upstreamMaxRetries(),
10885
11987
  abortSignal,
10886
11988
  onError: () => {
10887
- }
11989
+ },
11990
+ onStepFinish: (step) => reportUnsupportedServiceTier(params, step.warnings)
10888
11991
  });
10889
11992
  const watchedStream = (async function* () {
10890
11993
  try {
@@ -10910,10 +12013,11 @@ async function streamAnthropicResponse(model, params, modelId, write, log12, obs
10910
12013
  }
10911
12014
  }
10912
12015
  async function generateAnthropicResponse(model, params, modelId, options) {
10913
- let text4;
12016
+ let text5;
10914
12017
  let toolCalls;
10915
12018
  let finishReason;
10916
12019
  let usage;
12020
+ let warnings;
10917
12021
  if (options?.forceStream) {
10918
12022
  const forceAbort = new AbortController();
10919
12023
  const stopForwardingAbort = forwardAbortSignal(options.abortSignal, forceAbort);
@@ -10933,7 +12037,8 @@ async function generateAnthropicResponse(model, params, modelId, options) {
10933
12037
  maxRetries: upstreamMaxRetries(),
10934
12038
  abortSignal,
10935
12039
  onError: () => {
10936
- }
12040
+ },
12041
+ onStepFinish: (step) => reportUnsupportedServiceTier(params, step.warnings)
10937
12042
  });
10938
12043
  const streamedText = [];
10939
12044
  const streamedToolCalls = [];
@@ -10972,7 +12077,7 @@ async function generateAnthropicResponse(model, params, modelId, options) {
10972
12077
  clearTimeout(totalTimer);
10973
12078
  if (!forceAbort.signal.aborted) forceAbort.abort();
10974
12079
  }
10975
- text4 = streamedText.join("");
12080
+ text5 = streamedText.join("");
10976
12081
  toolCalls = streamedToolCalls;
10977
12082
  finishReason = streamedFinishReason;
10978
12083
  usage = streamedUsage;
@@ -10990,13 +12095,14 @@ async function generateAnthropicResponse(model, params, modelId, options) {
10990
12095
  maxRetries: upstreamMaxRetries(),
10991
12096
  abortSignal: generateAbort.signal
10992
12097
  });
10993
- ({ text: text4, toolCalls, finishReason, usage } = r);
12098
+ ({ text: text5, toolCalls, finishReason, usage, warnings } = r);
10994
12099
  } finally {
10995
12100
  stopForwardingAbort();
10996
12101
  clearTimeout(totalTimer);
10997
12102
  if (!generateAbort.signal.aborted) generateAbort.abort();
10998
12103
  }
10999
12104
  }
12105
+ reportUnsupportedServiceTier(params, warnings);
11000
12106
  const requiredProps = toolRequiredProps(params.tools);
11001
12107
  return {
11002
12108
  id: "msg_" + Date.now(),
@@ -11004,7 +12110,7 @@ async function generateAnthropicResponse(model, params, modelId, options) {
11004
12110
  role: "assistant",
11005
12111
  model: modelId,
11006
12112
  content: [
11007
- ...text4 ? [{ type: "text", text: text4 }] : [],
12113
+ ...text5 ? [{ type: "text", text: text5 }] : [],
11008
12114
  ...toolCalls.map((tc) => ({
11009
12115
  type: "tool_use",
11010
12116
  id: encodeToolUseId(tc.toolCallId, grabRoundTripSignature(tc)),
@@ -11165,14 +12271,14 @@ function appendSecureLog(logPath, line) {
11165
12271
  try {
11166
12272
  const fd = openSync3(logPath, "a", 384);
11167
12273
  try {
11168
- writeSync(fd, `${(/* @__PURE__ */ new Date()).toISOString()} ${redacted}
12274
+ writeSync2(fd, `${(/* @__PURE__ */ new Date()).toISOString()} ${redacted}
11169
12275
  `);
11170
12276
  } finally {
11171
12277
  closeSync3(fd);
11172
12278
  }
11173
12279
  } catch {
11174
12280
  try {
11175
- appendFileSync(logPath, `${(/* @__PURE__ */ new Date()).toISOString()} ${redacted}
12281
+ appendFileSync2(logPath, `${(/* @__PURE__ */ new Date()).toISOString()} ${redacted}
11176
12282
  `);
11177
12283
  } catch {
11178
12284
  }
@@ -11338,8 +12444,9 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
11338
12444
  }
11339
12445
  const upstreamUrl = route.upstreamUrl;
11340
12446
  const routeAuthType = route.authType ?? "api";
12447
+ const loggedTier = isOpenAiOAuthRoute(route) ? oauthServiceTier() : void 0;
11341
12448
  plog(
11342
- () => `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}` : "")
11343
12450
  );
11344
12451
  const usesSdkAdapter = isSdkMigratedNpm(route.npm);
11345
12452
  if (messagesEndpoint === "count_tokens") {
@@ -11428,7 +12535,7 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
11428
12535
  return;
11429
12536
  }
11430
12537
  if (usesSdkAdapter) {
11431
- const openAiOAuth = route.npm === "@ai-sdk/openai" && route.authType === "oauth";
12538
+ const openAiOAuth = isOpenAiOAuthRoute(route);
11432
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"];
11433
12540
  const claudeSessionId = extractClaudeSessionId(anthropicBody, claudeSessionIdHeader);
11434
12541
  const translationLifecycle = createTranslationLifecycle(
@@ -11685,7 +12792,7 @@ function startProxy(completionsUrl, modelId, debug = false, contextWindow, sdk,
11685
12792
 
11686
12793
  // src/server/index.ts
11687
12794
  import pc10 from "picocolors";
11688
- import { networkInterfaces } from "os";
12795
+ import { networkInterfaces as networkInterfaces2 } from "os";
11689
12796
  import * as p9 from "@clack/prompts";
11690
12797
 
11691
12798
  // src/target-compatibility.ts
@@ -11898,6 +13005,7 @@ function translateOpenAiRequest(body, options) {
11898
13005
  }
11899
13006
  if (options?.openAiOAuth) {
11900
13007
  const instructions = system?.trim() || "You are a coding assistant.";
13008
+ const serviceTier = oauthServiceTier();
11901
13009
  return {
11902
13010
  messages,
11903
13011
  tools,
@@ -11907,7 +13015,8 @@ function translateOpenAiRequest(body, options) {
11907
13015
  openai: {
11908
13016
  store: false,
11909
13017
  include: ["reasoning.encrypted_content"],
11910
- instructions
13018
+ instructions,
13019
+ ...serviceTier ? { serviceTier } : {}
11911
13020
  }
11912
13021
  }
11913
13022
  };
@@ -11954,7 +13063,8 @@ async function generateOpenAiResponse(model, params, responseModelId, options) {
11954
13063
  ...params,
11955
13064
  maxRetries: upstreamMaxRetries(),
11956
13065
  onError: () => {
11957
- }
13066
+ },
13067
+ onStepFinish: (step) => reportUnsupportedServiceTier(params, step.warnings)
11958
13068
  });
11959
13069
  result = await collectOpenAiStream(stream);
11960
13070
  } else {
@@ -11964,6 +13074,7 @@ async function generateOpenAiResponse(model, params, responseModelId, options) {
11964
13074
  maxRetries: upstreamMaxRetries()
11965
13075
  });
11966
13076
  }
13077
+ reportUnsupportedServiceTier(params, result.warnings);
11967
13078
  const message = { role: "assistant", content: result.text || null };
11968
13079
  if (result.toolCalls?.length) {
11969
13080
  message.tool_calls = result.toolCalls.map((tc) => ({
@@ -11989,7 +13100,8 @@ async function streamOpenAiResponse(model, params, responseModelId, onChunk) {
11989
13100
  const { stream } = streamText2({
11990
13101
  model,
11991
13102
  ...params,
11992
- maxRetries: upstreamMaxRetries()
13103
+ maxRetries: upstreamMaxRetries(),
13104
+ onStepFinish: (step) => reportUnsupportedServiceTier(params, step.warnings)
11993
13105
  });
11994
13106
  const baseData = {
11995
13107
  id: `chatcmpl-${Date.now()}`,
@@ -12267,6 +13379,9 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
12267
13379
  modelId: body.model,
12268
13380
  effort: anthropicEffortFromRequest(body) ?? model.defaultEffort,
12269
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,
12270
13385
  provider: inferenceProvider(model),
12271
13386
  route: "translated",
12272
13387
  requestPreview: getLatestMessagePreview(body.messages, body.system)
@@ -12276,7 +13391,7 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
12276
13391
  if (npmMaxTools !== void 0 && toolCount > npmMaxTools) {
12277
13392
  plog(`tools truncated: ${toolCount} \u2192 ${npmMaxTools} (provider limit)`);
12278
13393
  }
12279
- const openAiOAuth = model.npm === "@ai-sdk/openai" && model.authType === "oauth";
13394
+ const openAiOAuth = isOpenAiOAuthRoute(model);
12280
13395
  const params = translateRequest(body, model.npm, {
12281
13396
  defaultEffort: anthropicEffortFromRequest(body) ? void 0 : model.defaultEffort,
12282
13397
  openAiOAuth,
@@ -12463,12 +13578,13 @@ async function handleOpenAIChatCompletions(req, res, options, modelCache, plog)
12463
13578
  auditInference(options, {
12464
13579
  modelId: body.model,
12465
13580
  effort: openAiEffort(body),
13581
+ serviceTier: isOpenAiOAuthRoute(model) ? oauthServiceTier() : void 0,
12466
13582
  provider: inferenceProvider(model),
12467
13583
  route: "translated",
12468
13584
  requestPreview: getLatestMessagePreview(body.messages, body.system)
12469
13585
  });
12470
13586
  const baseURL = model.modelFormat === "anthropic" ? model.baseUrl : model.apiBaseUrl;
12471
- const openAiOAuth = npm === "@ai-sdk/openai" && model.authType === "oauth";
13587
+ const openAiOAuth = isOpenAiOAuthRoute(model);
12472
13588
  const params = translateOpenAiRequest(body, { openAiOAuth });
12473
13589
  const clientWantsStream = Boolean(body.stream);
12474
13590
  const responseModelId = getResponseModelId(body.model, model, options);
@@ -12987,7 +14103,7 @@ function requestHeadersWithoutProxyHeaders(req) {
12987
14103
  }
12988
14104
  return headers;
12989
14105
  }
12990
- 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) {
12991
14107
  return new Promise((resolve3) => {
12992
14108
  const startedAt = Date.now();
12993
14109
  let lastActivityAt = startedAt;
@@ -13047,7 +14163,8 @@ function forwardRawAnthropicRequest(req, res, rawBody, origin, rejectUnauthorize
13047
14163
  path: req.url,
13048
14164
  headers: requestHeadersWithoutProxyHeaders(req),
13049
14165
  servername: net.isIP(origin.hostname) ? void 0 : origin.hostname,
13050
- rejectUnauthorized
14166
+ rejectUnauthorized,
14167
+ agent
13051
14168
  }, (upstreamRes) => {
13052
14169
  headersReceived = true;
13053
14170
  statusCode = upstreamRes.statusCode ?? 502;
@@ -13388,6 +14505,8 @@ async function startHttpProxy(options) {
13388
14505
  reservedModelIds.add(normalizeRouteLookupId(modelId));
13389
14506
  }
13390
14507
  const anthropicOrigin = new URL2(options.anthropicOrigin ?? "https://api.anthropic.com");
14508
+ const anthropicProxyUrl = outboundProxyUrlForTarget(anthropicOrigin.href);
14509
+ let anthropicAgent;
13391
14510
  let adapter = options.adapterHandle ?? null;
13392
14511
  if (options.routes.length > 0) {
13393
14512
  adapter ??= await startProxyCatalog(
@@ -13470,6 +14589,9 @@ async function startHttpProxy(options) {
13470
14589
  claudeSessionId,
13471
14590
  modelId: typeof parsed?.model === "string" ? parsed.model : "unknown",
13472
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,
13473
14595
  provider,
13474
14596
  route: route ? "translated" : "passthrough",
13475
14597
  stream: Boolean(parsed?.stream),
@@ -13513,6 +14635,7 @@ async function startHttpProxy(options) {
13513
14635
  rawBody,
13514
14636
  anthropicOrigin,
13515
14637
  options.anthropicRejectUnauthorized ?? true,
14638
+ anthropicAgent,
13516
14639
  messagesEndpoint === "messages" && options.inferenceLogPath ? (statusCode, errorContent) => writeInferenceResponseErrorLog(options.inferenceLogPath, {
13517
14640
  requestId,
13518
14641
  modelId: typeof parsed?.model === "string" ? parsed.model : "unknown",
@@ -13547,7 +14670,8 @@ async function startHttpProxy(options) {
13547
14670
  res,
13548
14671
  rawBody,
13549
14672
  anthropicOrigin,
13550
- options.anthropicRejectUnauthorized ?? true
14673
+ options.anthropicRejectUnauthorized ?? true,
14674
+ anthropicAgent
13551
14675
  );
13552
14676
  });
13553
14677
  const sockets = /* @__PURE__ */ new Set();
@@ -13606,6 +14730,17 @@ async function startHttpProxy(options) {
13606
14730
  adapter?.close();
13607
14731
  throw err;
13608
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
+ }
13609
14744
  return {
13610
14745
  host: options.host ?? "127.0.0.1",
13611
14746
  port: address.port,
@@ -13621,6 +14756,7 @@ async function startHttpProxy(options) {
13621
14756
  for (const socket of sockets) socket.destroy();
13622
14757
  await new Promise((resolve3) => proxyServer.close(() => resolve3()));
13623
14758
  mitmServer.close();
14759
+ anthropicAgent?.destroy();
13624
14760
  adapter?.close();
13625
14761
  }
13626
14762
  };
@@ -13826,7 +14962,7 @@ async function runHttpProxyServerCommand(debug = false, webSocketDiagnostics = f
13826
14962
 
13827
14963
  // src/server/index.ts
13828
14964
  function getLocalIps() {
13829
- const ifaces = networkInterfaces();
14965
+ const ifaces = networkInterfaces2();
13830
14966
  const result = [];
13831
14967
  for (const [name, iface] of Object.entries(ifaces)) {
13832
14968
  for (const addr of iface ?? []) {
@@ -14839,6 +15975,10 @@ function captureBuiltInPatchProofs(source, config, results) {
14839
15975
  "PATCH 9: default effort",
14840
15976
  /\/\*ccpatch:default-effort\*\/var _cce=Object\.assign\(Object\.create\(null\),\{[^{}]*\}\)\[String\([\w$]+\|\|""\)\.trim\(\)\.toLowerCase\(\)\];if\(_cce!==void 0\)return _cce;/
14841
15977
  );
15978
+ addPattern(
15979
+ "PATCH 10: child network environment",
15980
+ /\/\*ccpatch:child-network-env\*\/let _clodexChildEnv=process\.env,[\s\S]*?catch\(_clodexError\)\{\}\}/
15981
+ );
14842
15982
  return proofs;
14843
15983
  }
14844
15984
  function builtInPatchProofsChanged(source, proofs) {
@@ -15001,7 +16141,7 @@ function collectPristineFacts(args) {
15001
16141
  }
15002
16142
 
15003
16143
  // src/patch-transforms.ts
15004
- var PATCH_TRANSFORMS_VERSION = 3;
16144
+ var PATCH_TRANSFORMS_VERSION = 5;
15005
16145
  var NATIVE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
15006
16146
  var BASE_EFFORT_LEVELS = ["low", "medium", "high"];
15007
16147
  function projectNativeEffort(effort) {
@@ -15296,6 +16436,36 @@ function applyClodexPatches(source, config) {
15296
16436
  );
15297
16437
  }
15298
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
+ }
15299
16469
  return { content: js, results: report };
15300
16470
  }
15301
16471
 
@@ -15889,7 +17059,7 @@ async function runLaunchPatchCheck(opts = {}) {
15889
17059
  }
15890
17060
 
15891
17061
  // src/cli.ts
15892
- 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"]);
15893
17063
  var CLODEX_LAUNCH_FLAGS = /* @__PURE__ */ new Set(["--provider", "--model"]);
15894
17064
  function parseClodexLaunchFlag(arg, rest, index, parsed) {
15895
17065
  if (arg === "--provider" || arg === "--model") {
@@ -16116,6 +17286,7 @@ function parseArgs(args) {
16116
17286
  }
16117
17287
  if (arg === "--dry-run") parsed.dryRun = true;
16118
17288
  if (arg === "--trace") parsed.trace = true;
17289
+ if (arg === "--fast") parsed.fast = true;
16119
17290
  consumeBridgeModeFlag(arg, parsed);
16120
17291
  if (arg === "--save-mode") parsed.saveBridgeMode = true;
16121
17292
  if (arg === "--help" || arg === "-h") parsed.showHelp = true;
@@ -16185,6 +17356,8 @@ ${pc13.bold("Options:")}
16185
17356
  --save-mode With --endpoint/--proxy: save that mode as the claude default
16186
17357
  --dry-run Run the wizard but show a preview instead of launching Claude Code
16187
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)
16188
17361
  --provider Boot provider id (skip wizard when paired with --model or in print mode)
16189
17362
  --model Boot model id (skip wizard when paired with --provider or in print mode)
16190
17363
  --help Show this command help
@@ -16365,9 +17538,9 @@ ${pc13.bold("Behavior:")}
16365
17538
  permissions. Local failures are reported but never block the built-ins.
16366
17539
  Run clodex patch again after every claude update.`;
16367
17540
  }
16368
- function printHelp(text4) {
17541
+ function printHelp(text5) {
16369
17542
  console.log(`
16370
- ${text4}
17543
+ ${text5}
16371
17544
  `);
16372
17545
  }
16373
17546
  function reportInactiveCatalogAliases(modelAliases) {
@@ -16765,6 +17938,7 @@ async function runClaudeHttpProxyCommand(parsed, claudeArgs, agentStdout) {
16765
17938
  }
16766
17939
  async function runClaudeCommand(parsed) {
16767
17940
  const { dryRun, trace, launchProvider, launchModel } = parsed;
17941
+ if (parsed.fast) process.env.CLODEX_SERVICE_TIER = "fast";
16768
17942
  const claudeArgs = normalizeClaudeAgentArgs(parsed.claudeArgs);
16769
17943
  const agentStdout = wantsCleanAgentStdout("claude", claudeArgs);
16770
17944
  setAgentStdoutMode(agentStdout);
@@ -16834,7 +18008,12 @@ Error: ${launchPlan.error}
16834
18008
  catalogSpinner.stop("");
16835
18009
  }
16836
18010
  const allProviders = providersForTarget(providersForPicker(catalog), "claude");
18011
+ const blockedLaunchReason = launchPlan.skip && launchPlan.target?.providerId ? catalog.blockedProviders.get(launchPlan.target.providerId) : void 0;
16837
18012
  if (allProviders.length === 0) {
18013
+ if (blockedLaunchReason) {
18014
+ p12.log.error(blockedLaunchReason);
18015
+ return 1;
18016
+ }
16838
18017
  p12.log.warn("No providers available.");
16839
18018
  p12.log.info(pc13.dim("Run clodex providers to get started."));
16840
18019
  return 0;
@@ -16854,7 +18033,7 @@ Error: ${launchPlan.error}
16854
18033
  const resolved = findProviderAndModel(allProviders, launchPlan.target);
16855
18034
  if (!resolved) {
16856
18035
  p12.log.error(
16857
- `Provider/model not found: ${launchPlan.target.providerId} / ${launchPlan.target.modelId}`
18036
+ blockedLaunchReason ?? `Provider/model not found: ${launchPlan.target.providerId} / ${launchPlan.target.modelId}`
16858
18037
  );
16859
18038
  return 1;
16860
18039
  }