@bman654/clodex 0.1.0 → 1.0.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
@@ -11,10 +11,11 @@ import {
11
11
  getServerFavoritesOnly,
12
12
  getServerListenMode,
13
13
  getServerMaskGatewayIds,
14
+ isDiscoveryDisabled,
14
15
  launchClaude,
15
16
  loadPreferences,
16
17
  recordLaunchSelection,
17
- removeServerRuntimeState,
18
+ registerServerRuntimeState,
18
19
  resolveBridgeMode,
19
20
  savePreferences,
20
21
  setSavedServerPassword,
@@ -22,8 +23,8 @@ import {
22
23
  setServerFavoritesOnly,
23
24
  setServerListenMode,
24
25
  setServerMaskGatewayIds,
25
- writeServerRuntimeState
26
- } from "./chunk-C7N2JI7Z.js";
26
+ unregisterServerRuntimeState
27
+ } from "./chunk-3XM6UZWP.js";
27
28
 
28
29
  // src/cli.ts
29
30
  import pc13 from "picocolors";
@@ -201,7 +202,7 @@ import { join } from "path";
201
202
  // package.json
202
203
  var package_default = {
203
204
  name: "@bman654/clodex",
204
- version: "0.1.0",
205
+ version: "1.0.0",
205
206
  publishConfig: {
206
207
  access: "public"
207
208
  },
@@ -6259,6 +6260,146 @@ function sendJson(res, status, body) {
6259
6260
  res.end(json);
6260
6261
  }
6261
6262
 
6263
+ // src/catalog.ts
6264
+ function localModelToRoute(lp, model) {
6265
+ if (model.modelFormat === "anthropic" && !model.baseUrl) return null;
6266
+ if (model.modelFormat === "openai" && !isSdkMigratedNpm(model.npm) && !model.completionsUrl) return null;
6267
+ const upstreamUrl = model.modelFormat === "anthropic" ? model.baseUrl : model.completionsUrl;
6268
+ return {
6269
+ aliasId: claudeCodeClientModelId(aliasModelId(model.id, lp.id), model.contextWindow),
6270
+ realModelId: model.upstreamModelId,
6271
+ displayName: `${model.name || model.id} (${lp.name})`,
6272
+ upstreamUrl: upstreamUrl ?? "",
6273
+ apiKey: lp.apiKey,
6274
+ modelFormat: model.modelFormat,
6275
+ contextWindow: model.contextWindow,
6276
+ npm: model.npm,
6277
+ baseURL: model.apiBaseUrl,
6278
+ providerId: lp.id,
6279
+ authType: lp.authType,
6280
+ oauthAccountId: lp.oauthAccountId,
6281
+ providerData: lp.providerData,
6282
+ headers: lp.headers,
6283
+ supportedParameters: model.supportedParameters,
6284
+ reasoning: model.reasoning,
6285
+ interleavedReasoningField: model.interleavedReasoningField,
6286
+ useResponsesLite: model.useResponsesLite,
6287
+ preferWebSockets: model.preferWebSockets
6288
+ };
6289
+ }
6290
+ function makeRouteResolver(localProviders) {
6291
+ return (providerId, modelId) => {
6292
+ const provider = localProviders?.find((lp) => lp.id === providerId);
6293
+ const model = provider?.models.find((m) => m.id === modelId);
6294
+ return provider && model ? localModelToRoute(provider, model) ?? void 0 : void 0;
6295
+ };
6296
+ }
6297
+ function buildCatalogRoutes(startingRoute, favorites, resolveRoute, max = MAX_MODEL_CATALOG) {
6298
+ const droppedFavorites = [];
6299
+ const tail = favorites.map((fav) => {
6300
+ const route = resolveRoute(fav.providerId, fav.modelId);
6301
+ if (!route) droppedFavorites.push(fav);
6302
+ return route;
6303
+ }).filter((route) => route !== void 0);
6304
+ const routes = [
6305
+ startingRoute,
6306
+ ...tail.filter((route) => route.aliasId !== startingRoute.aliasId)
6307
+ ].slice(0, max);
6308
+ return { routes, droppedFavorites };
6309
+ }
6310
+
6311
+ // src/model-aliases.ts
6312
+ var MODEL_ALIAS_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
6313
+ function isValidModelAlias(name) {
6314
+ return MODEL_ALIAS_PATTERN.test(name);
6315
+ }
6316
+ function parseModelAliasAssignment(value) {
6317
+ const separator = value.indexOf("=");
6318
+ if (separator < 1 || separator === value.length - 1) {
6319
+ return { error: "Alias must use name=clodex:<provider-id>:<model-id>." };
6320
+ }
6321
+ const name = value.slice(0, separator).trim();
6322
+ if (!isValidModelAlias(name)) {
6323
+ return { error: "Alias names must be 1-64 letters, numbers, dots, underscores, or hyphens." };
6324
+ }
6325
+ const rawTarget = value.slice(separator + 1).trim();
6326
+ const target = rawTarget.startsWith("clodex:") ? rawTarget.slice("clodex:".length) : rawTarget;
6327
+ const targetSeparator = target.indexOf(":");
6328
+ if (targetSeparator < 1 || targetSeparator === target.length - 1) {
6329
+ return { error: "Alias target must use clodex:<provider-id>:<model-id>." };
6330
+ }
6331
+ return {
6332
+ name,
6333
+ providerId: target.slice(0, targetSeparator),
6334
+ // `models --list` prints Claude's synthetic context suffix. It is a client
6335
+ // routing hint, not part of the provider catalog id stored in favorites.
6336
+ modelId: stripOneMContextSuffix(target.slice(targetSeparator + 1))
6337
+ };
6338
+ }
6339
+ function modelAliasTarget(alias) {
6340
+ return `clodex:${alias.providerId}:${alias.modelId}`;
6341
+ }
6342
+
6343
+ // src/http-proxy/routes.ts
6344
+ var HTTP_PROXY_MODEL_PREFIX = "clodex:";
6345
+ function httpProxyModelId(providerId, modelId) {
6346
+ return `${HTTP_PROXY_MODEL_PREFIX}${providerId}:${modelId}`;
6347
+ }
6348
+ function httpProxyDisplayName(model, providerName) {
6349
+ return `${formatModelLabel(model)} (${providerName})`;
6350
+ }
6351
+ function buildHttpProxyRoutes(providers, favorites, modelAliases = [], max = MAX_MODEL_CATALOG) {
6352
+ const routes = [];
6353
+ const unavailable = [];
6354
+ const unsupported = [];
6355
+ const seen = /* @__PURE__ */ new Set();
6356
+ const routesByFavorite = /* @__PURE__ */ new Map();
6357
+ for (const favorite of favorites) {
6358
+ if (routes.length >= max) break;
6359
+ const provider = providers.find((item) => item.id === favorite.providerId);
6360
+ const model = provider?.models.find((item) => item.id === favorite.modelId);
6361
+ if (!provider || !model) {
6362
+ unavailable.push(favorite);
6363
+ continue;
6364
+ }
6365
+ if (model.modelFormat !== "openai" || !isSdkMigratedNpm(model.npm)) {
6366
+ unsupported.push(favorite);
6367
+ continue;
6368
+ }
6369
+ const route = localModelToRoute(provider, model);
6370
+ if (!route || !route.apiKey.trim()) {
6371
+ unavailable.push(favorite);
6372
+ continue;
6373
+ }
6374
+ const aliasId = claudeCodeClientModelId(
6375
+ httpProxyModelId(provider.id, model.id),
6376
+ model.contextWindow
6377
+ );
6378
+ if (seen.has(aliasId)) continue;
6379
+ seen.add(aliasId);
6380
+ const proxyRoute = {
6381
+ ...route,
6382
+ aliasId,
6383
+ displayName: httpProxyDisplayName(model, provider.name)
6384
+ };
6385
+ routes.push(proxyRoute);
6386
+ routesByFavorite.set(`${favorite.providerId}:${favorite.modelId}`, proxyRoute);
6387
+ }
6388
+ const aliases = [];
6389
+ const unavailableAliases = [];
6390
+ const seenAliases = /* @__PURE__ */ new Set();
6391
+ for (const alias of modelAliases) {
6392
+ const route = routesByFavorite.get(`${alias.providerId}:${alias.modelId}`);
6393
+ if (!isValidModelAlias(alias.name) || seenAliases.has(alias.name) || !route) {
6394
+ unavailableAliases.push(alias);
6395
+ continue;
6396
+ }
6397
+ seenAliases.add(alias.name);
6398
+ aliases.push({ name: alias.name, routeId: route.aliasId, displayName: route.displayName });
6399
+ }
6400
+ return { routes, unavailable, unsupported, aliases, unavailableAliases };
6401
+ }
6402
+
6262
6403
  // src/server/vendor-mask.ts
6263
6404
  function reverseSegment(value) {
6264
6405
  return [...value].reverse().join("");
@@ -6320,7 +6461,7 @@ function formatGatewayAnthropicModels(models, opts) {
6320
6461
  }))
6321
6462
  );
6322
6463
  }
6323
- function createGatewayModelCatalog(models, opts) {
6464
+ function createGatewayModelCatalog(models, opts, modelAliases) {
6324
6465
  const byId = /* @__PURE__ */ new Map();
6325
6466
  for (const model of models) {
6326
6467
  byId.set(model.id, model);
@@ -6331,6 +6472,17 @@ function createGatewayModelCatalog(models, opts) {
6331
6472
  if (rawAlias !== alias) byId.set(rawAlias, model);
6332
6473
  }
6333
6474
  }
6475
+ for (const model of models) {
6476
+ const canonicalId = httpProxyModelId(gatewayProviderId(model), model.id);
6477
+ if (!byId.has(canonicalId)) byId.set(canonicalId, model);
6478
+ }
6479
+ for (const alias of modelAliases ?? []) {
6480
+ if (byId.has(alias.name)) continue;
6481
+ const target = models.find(
6482
+ (model) => gatewayProviderId(model) === alias.providerId && model.id === alias.modelId
6483
+ );
6484
+ if (target) byId.set(alias.name, target);
6485
+ }
6334
6486
  return {
6335
6487
  get: (id) => byId.get(id),
6336
6488
  list: () => [...models]
@@ -7898,54 +8050,6 @@ function startProxy(completionsUrl, modelId, debug = false, contextWindow, sdk,
7898
8050
  }], clientModelId, debug);
7899
8051
  }
7900
8052
 
7901
- // src/catalog.ts
7902
- function localModelToRoute(lp, model) {
7903
- if (model.modelFormat === "anthropic" && !model.baseUrl) return null;
7904
- if (model.modelFormat === "openai" && !isSdkMigratedNpm(model.npm) && !model.completionsUrl) return null;
7905
- const upstreamUrl = model.modelFormat === "anthropic" ? model.baseUrl : model.completionsUrl;
7906
- return {
7907
- aliasId: claudeCodeClientModelId(aliasModelId(model.id, lp.id), model.contextWindow),
7908
- realModelId: model.upstreamModelId,
7909
- displayName: `${model.name || model.id} (${lp.name})`,
7910
- upstreamUrl: upstreamUrl ?? "",
7911
- apiKey: lp.apiKey,
7912
- modelFormat: model.modelFormat,
7913
- contextWindow: model.contextWindow,
7914
- npm: model.npm,
7915
- baseURL: model.apiBaseUrl,
7916
- providerId: lp.id,
7917
- authType: lp.authType,
7918
- oauthAccountId: lp.oauthAccountId,
7919
- providerData: lp.providerData,
7920
- headers: lp.headers,
7921
- supportedParameters: model.supportedParameters,
7922
- reasoning: model.reasoning,
7923
- interleavedReasoningField: model.interleavedReasoningField,
7924
- useResponsesLite: model.useResponsesLite,
7925
- preferWebSockets: model.preferWebSockets
7926
- };
7927
- }
7928
- function makeRouteResolver(localProviders) {
7929
- return (providerId, modelId) => {
7930
- const provider = localProviders?.find((lp) => lp.id === providerId);
7931
- const model = provider?.models.find((m) => m.id === modelId);
7932
- return provider && model ? localModelToRoute(provider, model) ?? void 0 : void 0;
7933
- };
7934
- }
7935
- function buildCatalogRoutes(startingRoute, favorites, resolveRoute, max = MAX_MODEL_CATALOG) {
7936
- const droppedFavorites = [];
7937
- const tail = favorites.map((fav) => {
7938
- const route = resolveRoute(fav.providerId, fav.modelId);
7939
- if (!route) droppedFavorites.push(fav);
7940
- return route;
7941
- }).filter((route) => route !== void 0);
7942
- const routes = [
7943
- startingRoute,
7944
- ...tail.filter((route) => route.aliasId !== startingRoute.aliasId)
7945
- ].slice(0, max);
7946
- return { routes, droppedFavorites };
7947
- }
7948
-
7949
8053
  // src/server/index.ts
7950
8054
  import pc10 from "picocolors";
7951
8055
  import { networkInterfaces } from "os";
@@ -8587,6 +8691,7 @@ async function handleOpenAIChatCompletions(req, res, options, modelCache, plog)
8587
8691
  }
8588
8692
  const completionsUrl = model.completionsUrl;
8589
8693
  const apiKey2 = model.apiKey ?? options.apiKey;
8694
+ const forwardBody = body.model === upstreamModelId(model) ? body : { ...body, model: upstreamModelId(model) };
8590
8695
  auditInference(options, {
8591
8696
  modelId: body.model,
8592
8697
  effort: openAiEffort(body),
@@ -8594,7 +8699,7 @@ async function handleOpenAIChatCompletions(req, res, options, modelCache, plog)
8594
8699
  route: "passthrough",
8595
8700
  requestPreview: getLatestMessagePreview(body.messages, body.system)
8596
8701
  });
8597
- await relayAnthropicMessages(res, completionsUrl, body, apiKey2, Boolean(body.stream), {
8702
+ await relayAnthropicMessages(res, completionsUrl, forwardBody, apiKey2, Boolean(body.stream), {
8598
8703
  onUpstreamError: options.inferenceLogPath ? (statusCode, errorContent) => writeInferenceResponseErrorLog(options.inferenceLogPath, {
8599
8704
  modelId: body.model,
8600
8705
  provider: inferenceProvider(model),
@@ -8698,6 +8803,7 @@ async function getOrInitLanguageModel(modelCache, model, npm, baseURL, apiKey, w
8698
8803
  return languageModel;
8699
8804
  }
8700
8805
  function getResponseModelId(bodyModel, model, options) {
8806
+ if (typeof bodyModel === "string" && options.aliasNames?.has(bodyModel)) return bodyModel;
8701
8807
  return options.gateway?.maskGatewayIds ? gatewayDisplayName(model, options.gateway) : typeof bodyModel === "string" ? bodyModel : model.id;
8702
8808
  }
8703
8809
  async function readJson(req) {
@@ -8833,95 +8939,6 @@ async function selectServerProviders(available, initial) {
8833
8939
  import pc9 from "picocolors";
8834
8940
  import * as p8 from "@clack/prompts";
8835
8941
 
8836
- // src/model-aliases.ts
8837
- var MODEL_ALIAS_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
8838
- function isValidModelAlias(name) {
8839
- return MODEL_ALIAS_PATTERN.test(name);
8840
- }
8841
- function parseModelAliasAssignment(value) {
8842
- const separator = value.indexOf("=");
8843
- if (separator < 1 || separator === value.length - 1) {
8844
- return { error: "Alias must use name=clodex:<provider-id>:<model-id>." };
8845
- }
8846
- const name = value.slice(0, separator).trim();
8847
- if (!isValidModelAlias(name)) {
8848
- return { error: "Alias names must be 1-64 letters, numbers, dots, underscores, or hyphens." };
8849
- }
8850
- const rawTarget = value.slice(separator + 1).trim();
8851
- const target = rawTarget.startsWith("clodex:") ? rawTarget.slice("clodex:".length) : rawTarget;
8852
- const targetSeparator = target.indexOf(":");
8853
- if (targetSeparator < 1 || targetSeparator === target.length - 1) {
8854
- return { error: "Alias target must use clodex:<provider-id>:<model-id>." };
8855
- }
8856
- return {
8857
- name,
8858
- providerId: target.slice(0, targetSeparator),
8859
- // `models --list` prints Claude's synthetic context suffix. It is a client
8860
- // routing hint, not part of the provider catalog id stored in favorites.
8861
- modelId: stripOneMContextSuffix(target.slice(targetSeparator + 1))
8862
- };
8863
- }
8864
- function modelAliasTarget(alias) {
8865
- return `clodex:${alias.providerId}:${alias.modelId}`;
8866
- }
8867
-
8868
- // src/http-proxy/routes.ts
8869
- var HTTP_PROXY_MODEL_PREFIX = "clodex:";
8870
- function httpProxyModelId(providerId, modelId) {
8871
- return `${HTTP_PROXY_MODEL_PREFIX}${providerId}:${modelId}`;
8872
- }
8873
- function buildHttpProxyRoutes(providers, favorites, modelAliases = [], max = MAX_MODEL_CATALOG) {
8874
- const routes = [];
8875
- const unavailable = [];
8876
- const unsupported = [];
8877
- const seen = /* @__PURE__ */ new Set();
8878
- const routesByFavorite = /* @__PURE__ */ new Map();
8879
- for (const favorite of favorites) {
8880
- if (routes.length >= max) break;
8881
- const provider = providers.find((item) => item.id === favorite.providerId);
8882
- const model = provider?.models.find((item) => item.id === favorite.modelId);
8883
- if (!provider || !model) {
8884
- unavailable.push(favorite);
8885
- continue;
8886
- }
8887
- if (model.modelFormat !== "openai" || !isSdkMigratedNpm(model.npm)) {
8888
- unsupported.push(favorite);
8889
- continue;
8890
- }
8891
- const route = localModelToRoute(provider, model);
8892
- if (!route || !route.apiKey.trim()) {
8893
- unavailable.push(favorite);
8894
- continue;
8895
- }
8896
- const aliasId = claudeCodeClientModelId(
8897
- httpProxyModelId(provider.id, model.id),
8898
- model.contextWindow
8899
- );
8900
- if (seen.has(aliasId)) continue;
8901
- seen.add(aliasId);
8902
- const proxyRoute = {
8903
- ...route,
8904
- aliasId,
8905
- displayName: `${model.name || model.id} (${provider.name})`
8906
- };
8907
- routes.push(proxyRoute);
8908
- routesByFavorite.set(`${favorite.providerId}:${favorite.modelId}`, proxyRoute);
8909
- }
8910
- const aliases = [];
8911
- const unavailableAliases = [];
8912
- const seenAliases = /* @__PURE__ */ new Set();
8913
- for (const alias of modelAliases) {
8914
- const route = routesByFavorite.get(`${alias.providerId}:${alias.modelId}`);
8915
- if (!isValidModelAlias(alias.name) || seenAliases.has(alias.name) || !route) {
8916
- unavailableAliases.push(alias);
8917
- continue;
8918
- }
8919
- seenAliases.add(alias.name);
8920
- aliases.push({ name: alias.name, routeId: route.aliasId, displayName: route.displayName });
8921
- }
8922
- return { routes, unavailable, unsupported, aliases, unavailableAliases };
8923
- }
8924
-
8925
8942
  // src/http-proxy/server.ts
8926
8943
  import * as http from "http";
8927
8944
  import * as https from "https";
@@ -9842,7 +9859,7 @@ function waitForShutdown() {
9842
9859
  process.once("SIGTERM", done);
9843
9860
  });
9844
9861
  }
9845
- async function runHttpProxyServerCommand(debug = false, webSocketDiagnostics = false, port) {
9862
+ async function runHttpProxyServerCommand(debug = false, webSocketDiagnostics = false, port, noDiscovery = false) {
9846
9863
  const webSocketDiagnosticsLogPath = webSocketDiagnostics ? getSessionLogPath("server-websocket-diagnostics", "jsonl") : void 0;
9847
9864
  let started;
9848
9865
  try {
@@ -9875,15 +9892,17 @@ async function runHttpProxyServerCommand(debug = false, webSocketDiagnostics = f
9875
9892
  console.log(pc9.dim("Anthropic requests keep Claude Code auth and pass through unchanged."));
9876
9893
  console.log(pc9.dim("Use `/model <listed-name>` for a favorite or saved alias."));
9877
9894
  console.log(pc9.dim("Press Ctrl+C to stop."));
9878
- writeServerRuntimeState({
9879
- mode: "proxy",
9880
- port: handle.port,
9881
- pid: process.pid,
9882
- caPath: handle.caCertPath,
9883
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
9884
- });
9895
+ if (!noDiscovery) {
9896
+ registerServerRuntimeState({
9897
+ mode: "proxy",
9898
+ port: handle.port,
9899
+ pid: process.pid,
9900
+ caPath: handle.caCertPath,
9901
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
9902
+ });
9903
+ }
9885
9904
  await waitForShutdown();
9886
- removeServerRuntimeState();
9905
+ if (!noDiscovery) unregisterServerRuntimeState();
9887
9906
  await handle.close();
9888
9907
  return 0;
9889
9908
  }
@@ -10113,13 +10132,14 @@ async function resolveServerUpstreamApiKey() {
10113
10132
  return null;
10114
10133
  }
10115
10134
  async function runServerCommand(options = {}) {
10135
+ const noDiscovery = isDiscoveryDisabled(options.noDiscovery);
10116
10136
  if (options.httpProxy) {
10117
10137
  const hasGatewayOptions = options.quick || options.listenMode !== void 0 || options.providersMode !== void 0 || options.maskGatewayIds !== void 0 || options.password !== void 0;
10118
10138
  if (hasGatewayOptions) {
10119
10139
  p9.log.error("--proxy is a local-only server mode and cannot be combined with endpoint-mode server options.");
10120
10140
  return 1;
10121
10141
  }
10122
- return runHttpProxyServerCommand(false, options.wsDiagnostics, options.port);
10142
+ return runHttpProxyServerCommand(false, options.wsDiagnostics, options.port, noDiscovery);
10123
10143
  }
10124
10144
  const apiKey = await resolveServerUpstreamApiKey();
10125
10145
  if (!apiKey) {
@@ -10184,6 +10204,7 @@ async function runServerCommand(options = {}) {
10184
10204
  return 1;
10185
10205
  }
10186
10206
  const gateway = runConfig.maskGatewayIds ? { maskGatewayIds: true } : void 0;
10207
+ const modelAliases = loadPreferences().modelAliases ?? [];
10187
10208
  const inferenceLogPath = getInferenceRequestLogPath();
10188
10209
  const webSocketDiagnosticsLogPath = options.wsDiagnostics ? getSessionLogPath("server-websocket-diagnostics", "jsonl") : void 0;
10189
10210
  const server = await startServer({
@@ -10191,8 +10212,9 @@ async function runServerCommand(options = {}) {
10191
10212
  port: options.port ?? DEFAULT_SERVER_PORT,
10192
10213
  apiKey,
10193
10214
  serverPassword,
10194
- catalog: createGatewayModelCatalog(models, gateway),
10215
+ catalog: createGatewayModelCatalog(models, gateway, modelAliases),
10195
10216
  gateway,
10217
+ aliasNames: new Set(modelAliases.map((alias) => alias.name)),
10196
10218
  inferenceLogPath,
10197
10219
  webSocketDiagnosticsLogPath
10198
10220
  });
@@ -10231,14 +10253,16 @@ async function runServerCommand(options = {}) {
10231
10253
  console.log("");
10232
10254
  printModelCatalog(models, gateway);
10233
10255
  console.log(pc10.dim("Press Ctrl+C to stop."));
10234
- writeServerRuntimeState({
10235
- mode: "endpoint",
10236
- port: server.port,
10237
- pid: process.pid,
10238
- startedAt: (/* @__PURE__ */ new Date()).toISOString()
10239
- });
10256
+ if (!noDiscovery) {
10257
+ registerServerRuntimeState({
10258
+ mode: "endpoint",
10259
+ port: server.port,
10260
+ pid: process.pid,
10261
+ startedAt: (/* @__PURE__ */ new Date()).toISOString()
10262
+ });
10263
+ }
10240
10264
  await waitForShutdown2();
10241
- removeServerRuntimeState();
10265
+ if (!noDiscovery) unregisterServerRuntimeState();
10242
10266
  await server.close();
10243
10267
  return 0;
10244
10268
  }
@@ -10527,6 +10551,14 @@ const MODEL_CONFIG = {};
10527
10551
  // ---- derive helpers --------------------------------------------------------
10528
10552
  // alias -> model id (only for entries that define an alias)
10529
10553
  const ALIAS_TO_ID = {};
10554
+ // The name Claude Code knows a model by: its alias when it has one, else its
10555
+ // canonical id. This single value is used for the Agent-tool enum, the
10556
+ // known-alias validator, the /model picker value, and the context-window table,
10557
+ // so the name the binary validates == the name it sends upstream == the name
10558
+ // the proxy echoes back == the key its context window is stored under.
10559
+ const IDENTITIES = [];
10560
+ // identity -> human label for the /model picker (falls back at use site)
10561
+ const DISPLAY_BY_IDENTITY = {};
10530
10562
  // lowercased alias AND id -> context-window tokens (only for models that set it)
10531
10563
  const CONTEXT_BY_KEY = {};
10532
10564
  for (const [id, value] of Object.entries(MODEL_CONFIG)) {
@@ -10537,6 +10569,11 @@ for (const [id, value] of Object.entries(MODEL_CONFIG)) {
10537
10569
  throw new Error('clodex patch: alias "' + spec.alias + '" is not a safe lowercase alias');
10538
10570
  }
10539
10571
  ALIAS_TO_ID[a] = String(id);
10572
+ IDENTITIES.push(a);
10573
+ if (spec.display) DISPLAY_BY_IDENTITY[a] = String(spec.display);
10574
+ } else {
10575
+ IDENTITIES.push(String(id));
10576
+ if (spec.display) DISPLAY_BY_IDENTITY[String(id)] = String(spec.display);
10540
10577
  }
10541
10578
 
10542
10579
  if (spec.context !== undefined) {
@@ -10560,6 +10597,11 @@ const ALIASES = Object.keys(ALIAS_TO_ID);
10560
10597
  const MODELS = Object.keys(MODEL_CONFIG);
10561
10598
  if (MODELS.length === 0) throw new Error("clodex patch: MODEL_CONFIG is empty");
10562
10599
 
10600
+ /** Picker/description label for an identity; falls back to the old wording. */
10601
+ function displayFor(identity, fallbackId) {
10602
+ return DISPLAY_BY_IDENTITY[identity] || "Custom model (" + fallbackId + ")";
10603
+ }
10604
+
10563
10605
  const reEsc = (s) => s.replace(/[.*+?^$\{\}()|[\]\\]/g, "\\$&");
10564
10606
  const q = (s) => JSON.stringify(s); // safe JS string literal
10565
10607
 
@@ -10608,22 +10650,24 @@ function applyOnce(name, regex, fn, { marker, required, noopIsSkip } = {}) {
10608
10650
  log("OK", name);
10609
10651
  }
10610
10652
 
10611
- /** Insert missing ids just before the closing bracket of a JS array literal string. */
10653
+ /** Insert missing identities just before the closing bracket of a JS array literal string. */
10612
10654
  function extendAliasArray(arrLiteral) {
10613
- const toAdd = MODELS.filter((a) => !new RegExp('"' + reEsc(a) + '"').test(arrLiteral));
10655
+ const toAdd = IDENTITIES.filter((a) => !new RegExp('"' + reEsc(a) + '"').test(arrLiteral));
10614
10656
  if (toAdd.length === 0) return arrLiteral; // idempotent
10615
10657
  return arrLiteral.replace(/\]\s*$/, "," + toAdd.map(q).join(",") + "]");
10616
10658
  }
10617
10659
 
10618
- console.error("clodex patch: injecting models [" + MODELS.join(", ") + "]"
10619
- + (ALIASES.length ? " aliases [" + ALIASES.join(", ") + "]" : ""));
10660
+ console.error("clodex patch: injecting model names [" + IDENTITIES.join(", ") + "]");
10620
10661
 
10621
10662
  // ---------------------------------------------------------------------------
10622
10663
  // PATCH 1 — Agent/subagent tool 'model' zod enum.
10623
10664
  // Anchor: .enum([ "sonnet",...,"fable" ]).optional().describe( — the array
10624
10665
  // begins with the built-in aliases and is immediately followed by
10625
- // .optional().describe(. We append our ids inside the enum so the tool
10626
- // accepts them. (This same .describe( is patched by PATCH 4 below.)
10666
+ // .optional().describe(. We append our identities (alias when defined, else
10667
+ // the canonical id) inside the enum so the tool accepts them — this is the same
10668
+ // enum subagent/skill 'model:' frontmatter is validated against, which is why
10669
+ // the short alias has to be the value that lands here.
10670
+ // (This same .describe( is patched by PATCH 4 below.)
10627
10671
  // ---------------------------------------------------------------------------
10628
10672
  applyOnce(
10629
10673
  "PATCH 1: Agent tool model enum",
@@ -10636,7 +10680,7 @@ applyOnce(
10636
10680
  // PATCH 3 — known-alias validator list (drives "is this a known alias?").
10637
10681
  // Anchor: the master list literal, matched loosely as
10638
10682
  // ["sonnet","opus","haiku","fable", ...anything... ,"opusplan"] so it
10639
- // tolerates new built-ins being added in the middle. Appending our ids
10683
+ // tolerates new built-ins being added in the middle. Appending our identities
10640
10684
  // makes them recognized as first-class aliases everywhere the gate runs.
10641
10685
  // ---------------------------------------------------------------------------
10642
10686
  applyOnce(
@@ -10647,16 +10691,26 @@ applyOnce(
10647
10691
  );
10648
10692
 
10649
10693
  // ---------------------------------------------------------------------------
10650
- // PATCH 6 — alias -> model-id resolver switch.
10694
+ // PATCH 6 — alias resolver switch (IDENTITY mapping).
10651
10695
  // Anchor: case"best":{ ... } (the case"best":{ is unique). We inject
10652
- // case"<alias>":return"<model-id>"; right after it (before the switch's
10653
- // default:return null) so each alias resolves to its concrete model id.
10696
+ // case"<alias>":return"<alias>"; right after it (before the switch's
10697
+ // default:return null).
10698
+ //
10699
+ // The mapping is deliberately an identity, NOT alias -> canonical id: the alias
10700
+ // IS the model's identity everywhere else in the patched binary (enum,
10701
+ // validator, picker, context table), and the MITM proxy resolves short alias
10702
+ // names as request model ids and echoes request bodies unrewritten. Resolving
10703
+ // to the canonical id here would make Claude Code send one name and look its
10704
+ // context window up under another — the exact mismatch that stopped auto-compact
10705
+ // from firing and killed agents with "Prompt is too long". The case still has to
10706
+ // EXIST (rather than be skipped) so the resolver returns the name instead of
10707
+ // falling through to default:return null.
10654
10708
  // Only aliases not already present are inserted, so a rerun (or a config
10655
10709
  // edit) tops up cleanly rather than duplicating cases.
10656
10710
  // ---------------------------------------------------------------------------
10657
10711
  {
10658
10712
  const missing = ALIASES.filter((a) => !new RegExp("case" + reEsc(q(a)) + ":return").test(js));
10659
- const cases = missing.map((a) => "case" + q(a) + ":return " + q(ALIAS_TO_ID[a]) + ";").join("");
10713
+ const cases = missing.map((a) => "case" + q(a) + ":return " + q(a) + ";").join("");
10660
10714
  if (ALIASES.length === 0) {
10661
10715
  log("SKIP", "PATCH 6: alias resolver switch", "no aliases configured");
10662
10716
  } else {
@@ -10683,7 +10737,9 @@ applyOnce(
10683
10737
  .map(
10684
10738
  // ASCII only: tweakcc's script->JSON->repack path double-encodes any
10685
10739
  // non-ASCII byte.
10686
- (a) => "{value:" + q(a) + ",label:" + q(a.charAt(0).toUpperCase() + a.slice(1)) + ",description:" + q("Custom model (" + ALIAS_TO_ID[a] + ")") + "}"
10740
+ // value = the alias (the name the user types and the binary sends);
10741
+ // description = the real model label, e.g. "GPT-5.6 Sol (OpenAI (ChatGPT))".
10742
+ (a) => "{value:" + q(a) + ",label:" + q(a.charAt(0).toUpperCase() + a.slice(1)) + ",description:" + q(displayFor(a, ALIAS_TO_ID[a])) + "}"
10687
10743
  )
10688
10744
  .join(",");
10689
10745
  const inject = missing.length
@@ -10703,18 +10759,24 @@ applyOnce(
10703
10759
 
10704
10760
  // ---------------------------------------------------------------------------
10705
10761
  // PATCH 4 — Agent tool 'model' parameter description text.
10706
- // Append the available model names before the closing backtick so the model
10707
- // knows the aliases exist and can request them. Best-effort (cosmetic).
10762
+ // Append the available model names (with their real labels) before the closing
10763
+ // backtick so the model knows which extra names it may request and what they
10764
+ // actually are. Best-effort (cosmetic). The text is spliced into a backtick
10765
+ // template literal, so backticks and interpolation openers are stripped.
10708
10766
  // ---------------------------------------------------------------------------
10709
10767
  {
10710
- const listing = MODELS.join(", ");
10768
+ const safe = (s) => String(s).replace(/\`/g, "'").replace(/\$\{/g, "(");
10769
+ const listing = IDENTITIES.map(function (i) {
10770
+ const d = DISPLAY_BY_IDENTITY[i];
10771
+ return d ? safe(i) + " = " + safe(d) : safe(i);
10772
+ }).join("; ");
10711
10773
  applyOnce(
10712
10774
  "PATCH 4: Agent tool model description",
10713
10775
  /(describe\(\`Optional model override for this agent[^\`]*?)(\`\))/,
10714
10776
  (_m, body, close) =>
10715
- body.includes("custom aliases")
10777
+ body.includes("Additional custom models")
10716
10778
  ? body + close
10717
- : body + " Additional custom aliases: " + listing + "." + close,
10779
+ : body + " Additional custom models: " + listing + "." + close,
10718
10780
  { required: false, noopIsSkip: true }
10719
10781
  );
10720
10782
  }
@@ -10800,28 +10862,30 @@ function writePatchManifest(manifest, path = getPatchManifestPath()) {
10800
10862
  writeFileSync5(path, `${JSON.stringify(manifest, null, 2)}
10801
10863
  `, { encoding: "utf8", mode: 384 });
10802
10864
  }
10803
- function buildPatchModelConfig(favorites, aliases, contextWindowFor) {
10865
+ function buildPatchModelConfig(favorites, aliases, modelMetaFor) {
10804
10866
  const config = {};
10805
10867
  const unknownWindows = [];
10806
10868
  const aliasByFavorite = new Map(aliases.map((a) => [`${a.providerId}:${a.modelId}`, a.name]));
10807
10869
  for (const favorite of favorites) {
10808
10870
  const id = stripOneMContextSuffix(httpProxyModelId(favorite.providerId, favorite.modelId));
10809
10871
  if (config[id]) continue;
10810
- const context = contextWindowFor(favorite.providerId, favorite.modelId);
10872
+ const meta = modelMetaFor(favorite.providerId, favorite.modelId);
10873
+ const context = meta?.contextWindow;
10811
10874
  const alias = aliasByFavorite.get(`${favorite.providerId}:${favorite.modelId}`);
10812
- if (context === void 0 || context <= 0 || context === 2e5) {
10813
- if (context === void 0 || context <= 0) unknownWindows.push(id);
10814
- config[id] = alias ? { alias } : {};
10815
- } else {
10816
- config[id] = alias ? { alias, context } : { context };
10817
- }
10875
+ const entry = {};
10876
+ if (alias) entry.alias = alias;
10877
+ if (context === void 0 || context <= 0) unknownWindows.push(id);
10878
+ else if (context !== 2e5) entry.context = context;
10879
+ const display = meta?.displayName?.trim();
10880
+ if (display) entry.display = display;
10881
+ config[id] = entry;
10818
10882
  }
10819
10883
  return { config, unknownWindows };
10820
10884
  }
10821
10885
  function computePatchConfigHash(config) {
10822
10886
  const canonical = Object.keys(config).sort().map((key) => {
10823
10887
  const entry = config[key];
10824
- return [key, entry.alias ?? null, entry.context ?? null];
10888
+ return [key, entry.alias ?? null, entry.context ?? null, entry.display ?? null];
10825
10889
  });
10826
10890
  return createHash5("sha256").update(JSON.stringify(canonical)).digest("hex");
10827
10891
  }
@@ -10830,18 +10894,20 @@ function buildDesiredPatchConfig() {
10830
10894
  const favorites = prefs.favoriteModels ?? [];
10831
10895
  const aliases = prefs.modelAliases ?? [];
10832
10896
  const registry = loadRegistry();
10833
- const windows = /* @__PURE__ */ new Map();
10897
+ const meta = /* @__PURE__ */ new Map();
10834
10898
  for (const provider of registry.providers) {
10835
10899
  for (const model of provider.modelsCache?.models ?? []) {
10836
- if (model.contextWindow && model.contextWindow > 0) {
10837
- windows.set(`${provider.id}:${model.id}`, model.contextWindow);
10838
- }
10900
+ meta.set(`${provider.id}:${model.id}`, {
10901
+ contextWindow: model.contextWindow && model.contextWindow > 0 ? model.contextWindow : void 0,
10902
+ // Same label `clodex server` prints at startup and `models --list` shows.
10903
+ displayName: httpProxyDisplayName(model, provider.name)
10904
+ });
10839
10905
  }
10840
10906
  }
10841
10907
  return buildPatchModelConfig(
10842
10908
  favorites,
10843
10909
  aliases,
10844
- (providerId, modelId) => windows.get(`${providerId}:${modelId}`)
10910
+ (providerId, modelId) => meta.get(`${providerId}:${modelId}`)
10845
10911
  );
10846
10912
  }
10847
10913
  function evaluatePatchState(manifest, current) {
@@ -11206,6 +11272,7 @@ function parseArgs(args) {
11206
11272
  else if (consumeBridgeModeFlag(arg, parsed2)) continue;
11207
11273
  else if (arg === "--save-mode") parsed2.saveBridgeMode = true;
11208
11274
  else if (arg === "--ws-diagnostics") parsed2.serverWsDiagnostics = true;
11275
+ else if (arg === "--no-discovery") parsed2.serverNoDiscovery = true;
11209
11276
  else if (arg === "--quick" || arg === "--saved") parsed2.serverQuick = true;
11210
11277
  else if (arg === "--mask-gateway-ids") parsed2.serverMaskGatewayIds = true;
11211
11278
  else if (arg === "--no-mask-gateway-ids") parsed2.serverMaskGatewayIds = false;
@@ -11438,6 +11505,10 @@ ${pc13.bold("Common options (both modes):")}
11438
11505
  --save-mode With --endpoint/--proxy: save that mode as the
11439
11506
  server default
11440
11507
  --port <1-65535> Listen port (default 17645)
11508
+ --no-discovery Do not advertise this server in
11509
+ ~/.clodex/server-runtime.json, so the
11510
+ clodex-claude wrapper never bridges to it
11511
+ (CLODEX_NO_DISCOVERY=1 works too)
11441
11512
  --ws-diagnostics Log sanitized request envelopes and WebSocket
11442
11513
  head decisions
11443
11514
  --help, --version Help / version
@@ -12272,7 +12343,8 @@ Error: ${parsed.error}
12272
12343
  maskGatewayIds: parsed.serverMaskGatewayIds,
12273
12344
  password: parsed.serverPassword,
12274
12345
  wsDiagnostics: parsed.serverWsDiagnostics,
12275
- port: parsed.serverPort
12346
+ port: parsed.serverPort,
12347
+ noDiscovery: parsed.serverNoDiscovery
12276
12348
  });
12277
12349
  }
12278
12350
  if (parsed.command === "models") {