@bman654/clodex 2.1.3 → 2.1.5

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
@@ -4,6 +4,7 @@ import {
4
4
  ensureSecureAppHome,
5
5
  findClaudeBinary,
6
6
  getAppHome,
7
+ getClaudeVersionForBinary,
7
8
  getCredentialCleanupPath,
8
9
  getCredentialMutationLockPath,
9
10
  getCredentialStateRoot,
@@ -38,7 +39,7 @@ import {
38
39
  withProviderMutationLock,
39
40
  withRegistryWriteLock,
40
41
  withRegistryWriteLockSync
41
- } from "./chunk-OVO6OUZG.js";
42
+ } from "./chunk-VQOX3AXE.js";
42
43
 
43
44
  // src/cli.ts
44
45
  import pc13 from "picocolors";
@@ -216,7 +217,7 @@ import { join } from "path";
216
217
  // package.json
217
218
  var package_default = {
218
219
  name: "@bman654/clodex",
219
- version: "2.1.3",
220
+ version: "2.1.5",
220
221
  publishConfig: {
221
222
  access: "public"
222
223
  },
@@ -5538,6 +5539,7 @@ async function createLanguageModel(spec) {
5538
5539
  }
5539
5540
  var ANTHROPIC_EFFORT_LEVELS = ["low", "medium", "high"];
5540
5541
  var OPENAI_EFFORT_LEVELS = ["low", "medium", "high", "xhigh"];
5542
+ var GPT_56_EFFORT_LEVELS = ["none", "low", "medium", "high", "xhigh", "max"];
5541
5543
  var GEMINI_EFFORT_LEVELS = ["low", "medium", "high"];
5542
5544
  var MISTRAL_EFFORT_LEVELS = ["high", "off"];
5543
5545
  var XAI_EFFORT_LEVELS = ["none", "low", "medium", "high"];
@@ -5701,7 +5703,13 @@ function mapCodexEffortToAnthropic(effort) {
5701
5703
  return void 0;
5702
5704
  }
5703
5705
  }
5704
- function mapCodexEffortToOpenAI(effort) {
5706
+ function isGpt56Model(modelId) {
5707
+ return /^gpt-5\.6(?:-|$)/i.test(modelId);
5708
+ }
5709
+ function mapCodexEffortToOpenAI(effort, modelId) {
5710
+ if (modelId && isGpt56Model(modelId) && GPT_56_EFFORT_LEVELS.includes(effort)) {
5711
+ return effort;
5712
+ }
5705
5713
  if (effort === "xhigh") return "high";
5706
5714
  const allowed = ["low", "medium", "high"];
5707
5715
  return allowed.includes(effort) ? effort : void 0;
@@ -5782,7 +5790,7 @@ function getReasoningCapabilities(npm, modelId, metadata) {
5782
5790
  const prefersResponses = modelPrefersResponsesApi(modelId);
5783
5791
  if (prefersResponses || metadata?.reasoning) {
5784
5792
  return {
5785
- levels: [...OPENAI_EFFORT_LEVELS],
5793
+ levels: isGpt56Model(modelId) ? [...GPT_56_EFFORT_LEVELS] : [...OPENAI_EFFORT_LEVELS],
5786
5794
  defaultLevel: "medium",
5787
5795
  supportsSummaries: true,
5788
5796
  mode: "controllable",
@@ -5904,6 +5912,24 @@ function getReasoningCapabilities(npm, modelId, metadata) {
5904
5912
  }
5905
5913
  return EMPTY_REASONING;
5906
5914
  }
5915
+ function getPatchReasoningCapabilities(npm, modelId, metadata) {
5916
+ if (metadata?.reasoning === false && !hasSupportedParameter(metadata, "reasoning_effort") && !hasSupportedParameter(metadata, "reasoning")) {
5917
+ return EMPTY_REASONING;
5918
+ }
5919
+ const capabilities = getReasoningCapabilities(npm, modelId, metadata);
5920
+ const seenProviderOptions = /* @__PURE__ */ new Set();
5921
+ return {
5922
+ ...capabilities,
5923
+ levels: capabilities.levels.filter((level) => {
5924
+ const providerOptions = effortProviderOptions(npm, level, modelId, metadata);
5925
+ if (!providerOptions) return false;
5926
+ const fingerprint = JSON.stringify(providerOptions);
5927
+ if (seenProviderOptions.has(fingerprint)) return false;
5928
+ seenProviderOptions.add(fingerprint);
5929
+ return true;
5930
+ })
5931
+ };
5932
+ }
5907
5933
  function effortProviderOptions(npm, effort, modelId, metadata) {
5908
5934
  if (!effort) return void 0;
5909
5935
  if (isOpenRouterRoute(npm, metadata)) {
@@ -5915,7 +5941,7 @@ function effortProviderOptions(npm, effort, modelId, metadata) {
5915
5941
  }
5916
5942
  if (npm === "@ai-sdk/openai" || npm === "@ai-sdk/azure") {
5917
5943
  if (!modelId || !modelPrefersResponsesApi(modelId)) return void 0;
5918
- const reasoningEffort = mapCodexEffortToOpenAI(effort);
5944
+ const reasoningEffort = mapCodexEffortToOpenAI(effort, modelId);
5919
5945
  return reasoningEffort ? { openai: { reasoningEffort } } : void 0;
5920
5946
  }
5921
5947
  if (npm === "@ai-sdk/xai") {
@@ -8817,31 +8843,151 @@ function sendJson(res, status, body) {
8817
8843
  }
8818
8844
 
8819
8845
  // src/model-aliases.ts
8820
- var MODEL_ALIAS_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
8821
- function isValidModelAlias(name) {
8822
- return MODEL_ALIAS_PATTERN.test(name);
8846
+ var MODEL_ALIAS_PATTERN = /^[a-z0-9][a-z0-9._-]{0,63}$/;
8847
+ var RESERVED_MODEL_ALIASES = /* @__PURE__ */ new Set([
8848
+ "sonnet",
8849
+ "opus",
8850
+ "haiku",
8851
+ "fable",
8852
+ "best",
8853
+ "default",
8854
+ "opusplan",
8855
+ "inherit"
8856
+ ]);
8857
+ function canonicalModelAliasName(name) {
8858
+ return name.trim().toLowerCase();
8859
+ }
8860
+ function isReservedModelAlias(name) {
8861
+ return RESERVED_MODEL_ALIASES.has(
8862
+ stripOneMContextSuffix(canonicalModelAliasName(name))
8863
+ );
8864
+ }
8865
+ function isModelAliasNameSyntax(name) {
8866
+ return MODEL_ALIAS_PATTERN.test(canonicalModelAliasName(name));
8867
+ }
8868
+ function modelAliasMatchesName(value, name) {
8869
+ if (!value || typeof value !== "object" || !("name" in value)) return false;
8870
+ const candidate = value.name;
8871
+ return typeof candidate === "string" && canonicalModelAliasName(candidate) === canonicalModelAliasName(name);
8872
+ }
8873
+ function modelAliasMatchesStoredName(value, name) {
8874
+ if (!value || typeof value !== "object" || !("name" in value)) return false;
8875
+ const candidate = value.name;
8876
+ if (typeof candidate !== "string") return false;
8877
+ const requested = name.trim();
8878
+ return isModelAliasNameSyntax(requested) ? canonicalModelAliasName(candidate) === canonicalModelAliasName(requested) : candidate.trim() === requested;
8879
+ }
8880
+ function describeModelAliasRejection(reason) {
8881
+ switch (reason) {
8882
+ case "invalid-name":
8883
+ return "invalid name";
8884
+ case "reserved-name":
8885
+ return "reserved client name";
8886
+ case "invalid-target":
8887
+ return "invalid target";
8888
+ case "conflicting-targets":
8889
+ return "conflicting targets";
8890
+ case "target-not-favorite":
8891
+ return "target is not a saved favorite";
8892
+ }
8893
+ }
8894
+ function normalizeModelAliases(value) {
8895
+ if (value === void 0) {
8896
+ return { aliases: [], accepted: [], rejected: [], rejections: [] };
8897
+ }
8898
+ if (!Array.isArray(value)) {
8899
+ throw new TypeError('Saved model aliases are malformed: "modelAliases" must be an array.');
8900
+ }
8901
+ const candidates = [];
8902
+ const groups = /* @__PURE__ */ new Map();
8903
+ for (const [index, item] of value.entries()) {
8904
+ if (!item || typeof item !== "object" || typeof item.name !== "string") {
8905
+ throw new TypeError(
8906
+ `Saved model aliases are malformed: "modelAliases[${index}]" must be an object with a string "name".`
8907
+ );
8908
+ }
8909
+ const source = item;
8910
+ const candidate = { source };
8911
+ candidates.push(candidate);
8912
+ if (typeof source.providerId !== "string" || typeof source.modelId !== "string") {
8913
+ candidate.rejectionReason = "invalid-target";
8914
+ continue;
8915
+ }
8916
+ const normalized = {
8917
+ name: canonicalModelAliasName(source.name),
8918
+ providerId: source.providerId,
8919
+ modelId: source.modelId
8920
+ };
8921
+ if (!isModelAliasNameSyntax(normalized.name)) {
8922
+ candidate.rejectionReason = "invalid-name";
8923
+ continue;
8924
+ }
8925
+ if (isReservedModelAlias(normalized.name)) {
8926
+ candidate.rejectionReason = "reserved-name";
8927
+ continue;
8928
+ }
8929
+ if (!normalized.providerId.trim() || !normalized.modelId.trim()) {
8930
+ candidate.rejectionReason = "invalid-target";
8931
+ continue;
8932
+ }
8933
+ candidate.normalized = normalized;
8934
+ const group = groups.get(normalized.name) ?? [];
8935
+ group.push(candidate);
8936
+ groups.set(normalized.name, group);
8937
+ }
8938
+ for (const group of groups.values()) {
8939
+ const targets = new Set(
8940
+ group.map((candidate) => `${candidate.normalized.providerId}\0${candidate.normalized.modelId}`)
8941
+ );
8942
+ if (targets.size === 1) {
8943
+ group[0].accepted = true;
8944
+ group[0].sources = group.map((candidate) => candidate.source);
8945
+ } else {
8946
+ for (const candidate of group) candidate.rejectionReason = "conflicting-targets";
8947
+ }
8948
+ }
8949
+ const rejections = candidates.filter((candidate) => candidate.rejectionReason !== void 0).map((candidate) => ({
8950
+ alias: candidate.source,
8951
+ reason: candidate.rejectionReason
8952
+ }));
8953
+ const accepted = candidates.filter((candidate) => candidate.accepted === true && candidate.normalized !== void 0).map((candidate) => ({
8954
+ alias: candidate.normalized,
8955
+ source: candidate.source,
8956
+ sources: candidate.sources ?? [candidate.source]
8957
+ }));
8958
+ return {
8959
+ aliases: accepted.map((entry) => entry.alias),
8960
+ accepted,
8961
+ rejected: rejections.map((rejection) => rejection.alias),
8962
+ rejections
8963
+ };
8823
8964
  }
8824
8965
  function parseModelAliasAssignment(value) {
8825
8966
  const separator = value.indexOf("=");
8826
8967
  if (separator < 1 || separator === value.length - 1) {
8827
8968
  return { error: "Alias must use name=clodex:<provider-id>:<model-id>." };
8828
8969
  }
8829
- const name = value.slice(0, separator).trim();
8830
- if (!isValidModelAlias(name)) {
8970
+ const name = canonicalModelAliasName(value.slice(0, separator));
8971
+ if (!MODEL_ALIAS_PATTERN.test(name)) {
8831
8972
  return { error: "Alias names must be 1-64 letters, numbers, dots, underscores, or hyphens." };
8832
8973
  }
8974
+ if (isReservedModelAlias(name)) {
8975
+ return { error: "That alias name is reserved by the client." };
8976
+ }
8833
8977
  const rawTarget = value.slice(separator + 1).trim();
8834
8978
  const target = rawTarget.startsWith("clodex:") ? rawTarget.slice("clodex:".length) : rawTarget;
8835
8979
  const targetSeparator = target.indexOf(":");
8836
- if (targetSeparator < 1 || targetSeparator === target.length - 1) {
8980
+ const providerId = target.slice(0, targetSeparator).trim();
8981
+ const modelId = stripOneMContextSuffix(target.slice(targetSeparator + 1).trim());
8982
+ if (targetSeparator < 1 || targetSeparator === target.length - 1 || !providerId || !modelId) {
8837
8983
  return { error: "Alias target must use clodex:<provider-id>:<model-id>." };
8838
8984
  }
8839
8985
  return {
8840
8986
  name,
8841
- providerId: target.slice(0, targetSeparator),
8987
+ providerId,
8842
8988
  // `models --list` prints Claude's synthetic context suffix. It is a client
8843
8989
  // routing hint, not part of the provider catalog id stored in favorites.
8844
- modelId: stripOneMContextSuffix(target.slice(targetSeparator + 1))
8990
+ modelId
8845
8991
  };
8846
8992
  }
8847
8993
  function modelAliasTarget(alias) {
@@ -8888,11 +9034,32 @@ function makeRouteResolver(localProviders) {
8888
9034
  return provider && model ? localModelToRoute(provider, model) ?? void 0 : void 0;
8889
9035
  };
8890
9036
  }
8891
- function resolveCatalogModelAliases(modelAliases, resolveRoute) {
8892
- return modelAliases.map((alias) => ({
8893
- name: alias.name,
8894
- routeId: resolveRoute(alias.providerId, alias.modelId)?.aliasId ?? modelAliasTarget(alias)
8895
- }));
9037
+ function resolveCatalogModelAliases(modelAliases, resolveRoute, catalogRoutes = []) {
9038
+ const normalized = normalizeModelAliases(modelAliases);
9039
+ const catalogRouteIds = new Set(
9040
+ catalogRoutes.map((route) => normalizeRouteLookupId(route.aliasId))
9041
+ );
9042
+ return [
9043
+ ...normalized.accepted.map(({ alias, source, sources }) => {
9044
+ const route = resolveRoute(alias.providerId, alias.modelId);
9045
+ const collidesWithCatalog = catalogRouteIds.has(
9046
+ normalizeRouteLookupId(alias.name)
9047
+ );
9048
+ const sourceNames = [...new Set(sources.map((entry) => entry.name))];
9049
+ return {
9050
+ name: alias.name,
9051
+ ...source.name === alias.name ? {} : { savedName: source.name },
9052
+ ...sourceNames.length === 1 && sourceNames[0] === alias.name ? {} : { sourceNames },
9053
+ routeId: route?.aliasId ?? modelAliasTarget(alias),
9054
+ ...collidesWithCatalog ? { unavailableReason: "conflicts with a catalog model id" } : route ? {} : { unavailableReason: "target unavailable" }
9055
+ };
9056
+ }),
9057
+ ...normalized.rejections.map((rejection) => ({
9058
+ name: canonicalModelAliasName(rejection.alias.name),
9059
+ ...rejection.alias.name === canonicalModelAliasName(rejection.alias.name) ? {} : { savedName: rejection.alias.name },
9060
+ unavailableReason: describeModelAliasRejection(rejection.reason)
9061
+ }))
9062
+ ];
8896
9063
  }
8897
9064
  function buildCatalogRoutes(startingRoute, favorites, resolveRoute, max = MAX_MODEL_CATALOG) {
8898
9065
  const droppedFavorites = [];
@@ -8916,7 +9083,7 @@ function httpProxyModelId(providerId, modelId) {
8916
9083
  function httpProxyDisplayName(model, providerName) {
8917
9084
  return `${formatModelLabel(model)} (${providerName})`;
8918
9085
  }
8919
- function buildHttpProxyRoutes(providers, favorites, modelAliases = [], max = MAX_MODEL_CATALOG) {
9086
+ function buildHttpProxyRoutes(providers, favorites, modelAliases = void 0, max = MAX_MODEL_CATALOG) {
8920
9087
  const routes = [];
8921
9088
  const unavailable = [];
8922
9089
  const unsupported = [];
@@ -8954,16 +9121,21 @@ function buildHttpProxyRoutes(providers, favorites, modelAliases = [], max = MAX
8954
9121
  routesByFavorite.set(`${favorite.providerId}:${favorite.modelId}`, proxyRoute);
8955
9122
  }
8956
9123
  const aliases = [];
8957
- const unavailableAliases = [];
8958
- const seenAliases = /* @__PURE__ */ new Set();
8959
- for (const alias of modelAliases) {
9124
+ const normalizedAliases = normalizeModelAliases(modelAliases);
9125
+ const unavailableAliases = [...normalizedAliases.rejected];
9126
+ for (const { alias, sources } of normalizedAliases.accepted) {
8960
9127
  const route = routesByFavorite.get(`${alias.providerId}:${alias.modelId}`);
8961
- if (!isValidModelAlias(alias.name) || seenAliases.has(alias.name) || !route) {
8962
- unavailableAliases.push(alias);
9128
+ if (!route) {
9129
+ unavailableAliases.push(...sources);
8963
9130
  continue;
8964
9131
  }
8965
- seenAliases.add(alias.name);
8966
- aliases.push({ name: alias.name, routeId: route.aliasId, displayName: route.displayName });
9132
+ const sourceNames = [...new Set(sources.map((source) => source.name))];
9133
+ aliases.push({
9134
+ name: alias.name,
9135
+ routeId: route.aliasId,
9136
+ displayName: route.displayName,
9137
+ ...sourceNames.length === 1 && sourceNames[0] === alias.name ? {} : { sourceNames }
9138
+ });
8967
9139
  }
8968
9140
  return { routes, unavailable, unsupported, aliases, unavailableAliases };
8969
9141
  }
@@ -9044,7 +9216,7 @@ function createGatewayModelCatalog(models, opts, modelAliases) {
9044
9216
  const canonicalId = httpProxyModelId(gatewayProviderId(model), model.id);
9045
9217
  if (!byId.has(canonicalId)) byId.set(canonicalId, model);
9046
9218
  }
9047
- for (const alias of modelAliases ?? []) {
9219
+ for (const alias of normalizeModelAliases(modelAliases).aliases) {
9048
9220
  if (byId.has(alias.name)) continue;
9049
9221
  const target = models.find(
9050
9222
  (model) => gatewayProviderId(model) === alias.providerId && model.id === alias.modelId
@@ -9092,8 +9264,9 @@ function formatOpenAIModels(models) {
9092
9264
  }
9093
9265
 
9094
9266
  // src/route-unavailable.ts
9095
- function routeUnavailableMessage(modelId) {
9096
- return `Clodex model route '${modelId}' is unavailable. Run \`clodex models --list\` to see available routes, or \`clodex patch\` to refresh saved aliases.`;
9267
+ function routeUnavailableMessage(modelId, reason) {
9268
+ const detail = reason ? `: ${reason}` : "";
9269
+ return `Clodex model route '${modelId}' is unavailable${detail}. Run \`clodex models --list\` to inspect saved routes and aliases.`;
9097
9270
  }
9098
9271
 
9099
9272
  // src/upstream-forward.ts
@@ -9739,7 +9912,7 @@ async function writeAnthropicStream(stream, modelId, write, log12, observer, too
9739
9912
  let openToolId = null;
9740
9913
  let finishReason = "end_turn";
9741
9914
  let usage = {
9742
- input_tokens: 0,
9915
+ input_tokens: observer?.initialInputTokens ?? 0,
9743
9916
  output_tokens: 0,
9744
9917
  cache_creation_input_tokens: 0,
9745
9918
  cache_read_input_tokens: 0
@@ -9758,7 +9931,7 @@ async function writeAnthropicStream(stream, modelId, write, log12, observer, too
9758
9931
  stop_reason: null,
9759
9932
  stop_sequence: null,
9760
9933
  usage: {
9761
- input_tokens: observer?.initialInputTokens ?? 0,
9934
+ input_tokens: 0,
9762
9935
  output_tokens: 0,
9763
9936
  cache_creation_input_tokens: 0,
9764
9937
  cache_read_input_tokens: 0
@@ -9895,7 +10068,9 @@ async function writeAnthropicStream(stream, modelId, write, log12, observer, too
9895
10068
  }
9896
10069
  case "finish":
9897
10070
  if (part.totalUsage) {
9898
- usage = toAnthropicUsage(part.totalUsage);
10071
+ const finalUsage = toAnthropicUsage(part.totalUsage);
10072
+ const hasFinalInputUsage = finalUsage.input_tokens + finalUsage.cache_creation_input_tokens + finalUsage.cache_read_input_tokens > 0;
10073
+ usage = hasFinalInputUsage ? finalUsage : { ...usage, output_tokens: finalUsage.output_tokens };
9899
10074
  }
9900
10075
  if (part.finishReason === "tool-calls") finishReason = "tool_use";
9901
10076
  else if (part.finishReason === "length") finishReason = "max_tokens";
@@ -10125,6 +10300,7 @@ function anthropicPromptTooLongMessage(body, contextWindow) {
10125
10300
  // src/proxy.ts
10126
10301
  var STREAM_KEEPALIVE_INTERVAL_MS = 2e4;
10127
10302
  var STREAM_KEEPALIVE_PING = 'event: ping\ndata: {"type":"ping"}\n\n';
10303
+ var INTERNAL_ADAPTER_KEEPALIVE_TIMEOUT_MS = 6e4;
10128
10304
  function createTranslationLifecycle(logPath, requestId, claudeSessionId, modelId, provider) {
10129
10305
  if (!logPath || !requestId) return void 0;
10130
10306
  const startedAt = Date.now();
@@ -10253,6 +10429,17 @@ function aliasModelId(realId, providerId) {
10253
10429
  function lookupRoute(byAlias, id) {
10254
10430
  return byAlias.get(normalizeRouteLookupId(id));
10255
10431
  }
10432
+ function configuredAliasLookupNames(alias) {
10433
+ const sourceNames = [
10434
+ alias.name,
10435
+ ...alias.savedName === void 0 ? [] : [alias.savedName],
10436
+ ...alias.sourceNames ?? []
10437
+ ];
10438
+ return [...new Set(sourceNames.flatMap((name) => {
10439
+ const trimmed = name.trim();
10440
+ return trimmed === name ? [name] : [name, trimmed];
10441
+ }))].map(normalizeRouteLookupId);
10442
+ }
10256
10443
  async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPath, debugLogPath, webSocketDiagnosticsLogPath, modelAliases) {
10257
10444
  const proxyToken = randomUUID4();
10258
10445
  silenceSdkWarnings();
@@ -10261,9 +10448,13 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
10261
10448
  }
10262
10449
  const byAlias = new Map(routes.map((r) => [normalizeRouteLookupId(r.aliasId), r]));
10263
10450
  const configuredAliasNames = new Set(
10264
- (modelAliases ?? []).map((alias) => normalizeRouteLookupId(alias.name))
10451
+ (modelAliases ?? []).flatMap(configuredAliasLookupNames)
10452
+ );
10453
+ const unavailableAliasReasons = new Map(
10454
+ (modelAliases ?? []).filter((alias) => alias.unavailableReason !== void 0).flatMap((alias) => configuredAliasLookupNames(alias).map((name) => [name, alias.unavailableReason]))
10265
10455
  );
10266
10456
  for (const alias of modelAliases ?? []) {
10457
+ if (alias.routeId === void 0 || alias.unavailableReason !== void 0) continue;
10267
10458
  const route = lookupRoute(byAlias, alias.routeId);
10268
10459
  const aliasId = normalizeRouteLookupId(alias.name);
10269
10460
  if (route && !byAlias.has(aliasId)) byAlias.set(aliasId, route);
@@ -10338,7 +10529,14 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
10338
10529
  const resolvedRoute = typeof originalModel === "string" ? lookupRoute(byAlias, originalModel) : void 0;
10339
10530
  const configuredModelUnavailable = typeof originalModel === "string" && (normalizeRouteLookupId(originalModel).startsWith("clodex:") || configuredAliasNames.has(normalizeRouteLookupId(originalModel)));
10340
10531
  if (!resolvedRoute && configuredModelUnavailable) {
10341
- anthropicError(res, 400, routeUnavailableMessage(originalModel));
10532
+ anthropicError(
10533
+ res,
10534
+ 400,
10535
+ routeUnavailableMessage(
10536
+ originalModel,
10537
+ unavailableAliasReasons.get(normalizeRouteLookupId(originalModel))
10538
+ )
10539
+ );
10342
10540
  return;
10343
10541
  }
10344
10542
  const route = resolvedRoute ?? defaultRoute;
@@ -10656,6 +10854,7 @@ data: ${JSON.stringify({
10656
10854
  }
10657
10855
  anthropicError(res, 404, `Unknown endpoint: ${req.method} ${req.url}`);
10658
10856
  });
10857
+ server.keepAliveTimeout = INTERNAL_ADAPTER_KEEPALIVE_TIMEOUT_MS;
10659
10858
  let address;
10660
10859
  try {
10661
10860
  address = await listenTcpServer(server, 0, "127.0.0.1");
@@ -12152,7 +12351,7 @@ function forwardRawAnthropicRequest(req, res, rawBody, origin, rejectUnauthorize
12152
12351
  upstream.end(rawBody);
12153
12352
  });
12154
12353
  }
12155
- function forwardToAdapter(req, res, rawBody, adapter, adapterRequest = http.request, lifecycle, isLocalShutdown = () => false) {
12354
+ function forwardToAdapter(req, res, rawBody, adapter, adapterRequest = http.request, adapterAgent, lifecycle, isLocalShutdown = () => false) {
12156
12355
  return new Promise((resolve3) => {
12157
12356
  const startedAt = Date.now();
12158
12357
  let lastActivityAt = startedAt;
@@ -12260,6 +12459,7 @@ function forwardToAdapter(req, res, rawBody, adapter, adapterRequest = http.requ
12260
12459
  port: adapter.port,
12261
12460
  method: "POST",
12262
12461
  path: req.url,
12462
+ agent: adapterAgent,
12263
12463
  headers: {
12264
12464
  "Content-Type": "application/json",
12265
12465
  "Content-Length": String(rawBody.length),
@@ -12377,6 +12577,10 @@ async function startHttpProxy(options) {
12377
12577
  for (const alias of options.modelAliases ?? []) {
12378
12578
  const aliasId = normalizeRouteLookupId(alias.name);
12379
12579
  reservedModelIds.add(aliasId);
12580
+ for (const sourceName of alias.sourceNames ?? []) {
12581
+ reservedModelIds.add(normalizeRouteLookupId(sourceName));
12582
+ reservedModelIds.add(normalizeRouteLookupId(sourceName.trim()));
12583
+ }
12380
12584
  const route = routesById.get(normalizeRouteLookupId(alias.routeId));
12381
12585
  if (!route) continue;
12382
12586
  routesById.set(aliasId, route);
@@ -12397,6 +12601,7 @@ async function startHttpProxy(options) {
12397
12601
  options.modelAliases
12398
12602
  );
12399
12603
  }
12604
+ const adapterAgent = adapter ? new http.Agent({ keepAlive: true }) : void 0;
12400
12605
  let shuttingDown = false;
12401
12606
  const mitmServer = https.createServer({
12402
12607
  key: certificates.serverKey,
@@ -12490,6 +12695,7 @@ async function startHttpProxy(options) {
12490
12695
  adapterBody,
12491
12696
  adapter,
12492
12697
  options.adapterRequest,
12698
+ adapterAgent,
12493
12699
  messagesEndpoint === "messages" && options.inferenceLogPath ? {
12494
12700
  logPath: options.inferenceLogPath,
12495
12701
  requestId,
@@ -12597,6 +12803,7 @@ async function startHttpProxy(options) {
12597
12803
  options.host ?? "127.0.0.1"
12598
12804
  );
12599
12805
  } catch (err) {
12806
+ adapterAgent?.destroy();
12600
12807
  adapter?.close();
12601
12808
  throw err;
12602
12809
  }
@@ -12624,13 +12831,17 @@ async function startHttpProxy(options) {
12624
12831
  async function loadHttpProxyRoutes() {
12625
12832
  const prefs = loadPreferences();
12626
12833
  const favorites = prefs.favoriteModels ?? [];
12834
+ const normalizedAliases = normalizeModelAliases(prefs.modelAliases);
12627
12835
  if (favorites.length === 0) {
12628
12836
  return {
12629
12837
  routes: [],
12630
12838
  unavailable: [],
12631
12839
  unsupported: [],
12632
12840
  aliases: [],
12633
- unavailableAliases: prefs.modelAliases ?? [],
12841
+ unavailableAliases: [
12842
+ ...normalizedAliases.rejected,
12843
+ ...normalizedAliases.accepted.flatMap(({ sources }) => sources)
12844
+ ],
12634
12845
  favoriteCount: 0
12635
12846
  };
12636
12847
  }
@@ -12640,7 +12851,7 @@ async function loadHttpProxyRoutes() {
12640
12851
  apiKey: await resolveLocalProviderApiKey(provider) ?? ""
12641
12852
  })));
12642
12853
  return {
12643
- ...buildHttpProxyRoutes(catalog, favorites, prefs.modelAliases ?? []),
12854
+ ...buildHttpProxyRoutes(catalog, favorites, prefs.modelAliases),
12644
12855
  favoriteCount: favorites.length
12645
12856
  };
12646
12857
  }
@@ -12674,8 +12885,16 @@ function reportSkippedHttpProxyFavorites(loaded) {
12674
12885
  );
12675
12886
  }
12676
12887
  if (loaded.unavailableAliases.length > 0) {
12888
+ const normalizedAliases = normalizeModelAliases(loaded.unavailableAliases);
12889
+ const reasonByAlias = new Map(
12890
+ normalizedAliases.rejections.map((rejection) => [
12891
+ rejection.alias,
12892
+ describeModelAliasRejection(rejection.reason)
12893
+ ])
12894
+ );
12677
12895
  p8.log.warn(
12678
- `${loaded.unavailableAliases.length} model alias${loaded.unavailableAliases.length === 1 ? "" : "es"} skipped \u2014 its target must be an available HTTP-proxy favorite.`
12896
+ `${loaded.unavailableAliases.length} model alias${loaded.unavailableAliases.length === 1 ? "" : "es"} skipped. Saved entries were preserved.
12897
+ ` + loaded.unavailableAliases.map((alias) => ` ${JSON.stringify(alias.name)} \u2014 ${reasonByAlias.get(alias) ?? "target unavailable"}`).join("\n")
12679
12898
  );
12680
12899
  }
12681
12900
  }
@@ -12685,7 +12904,14 @@ function buildConfiguredHttpProxyOptions(loaded, port, debug = false, inferenceL
12685
12904
  port,
12686
12905
  routes: loaded.routes,
12687
12906
  modelAliases: loaded.aliases,
12688
- reservedModelIds: loaded.unavailableAliases.map((alias) => alias.name),
12907
+ reservedModelIds: [...new Set([
12908
+ ...loaded.aliases.flatMap((alias) => alias.sourceNames ?? []),
12909
+ ...loaded.unavailableAliases.map((alias) => alias.name)
12910
+ ].flatMap((name) => {
12911
+ const trimmedName = name.trim();
12912
+ const canonicalName = canonicalModelAliasName(name);
12913
+ return [name, trimmedName, canonicalName].filter(Boolean);
12914
+ }))],
12689
12915
  debug,
12690
12916
  debugLogPath,
12691
12917
  inferenceLogPath,
@@ -13096,7 +13322,28 @@ async function runServerCommand(options = {}) {
13096
13322
  return 1;
13097
13323
  }
13098
13324
  const gateway = runConfig.maskGatewayIds ? { maskGatewayIds: true } : void 0;
13099
- const modelAliases = loadPreferences().modelAliases ?? [];
13325
+ const normalizedAliases = normalizeModelAliases(loadPreferences().modelAliases);
13326
+ const baseCatalog = createGatewayModelCatalog(models, gateway);
13327
+ const unavailableAliases = normalizedAliases.accepted.filter(({ alias }) => !models.some((model) => gatewayProviderId(model) === alias.providerId && model.id === alias.modelId));
13328
+ const collidingAliases = normalizedAliases.accepted.filter(({ alias }) => baseCatalog.get(alias.name) !== void 0);
13329
+ const collidingAliasNames = new Set(collidingAliases.map(({ alias }) => alias.name));
13330
+ const targetUnavailableAliases = unavailableAliases.filter(({ alias }) => !collidingAliasNames.has(alias.name));
13331
+ const inactiveAliasNames = /* @__PURE__ */ new Set([
13332
+ ...targetUnavailableAliases.map(({ alias }) => alias.name),
13333
+ ...collidingAliasNames
13334
+ ]);
13335
+ const modelAliases = normalizedAliases.aliases.filter((alias) => !inactiveAliasNames.has(alias.name));
13336
+ const aliasWarnings = [
13337
+ ...normalizedAliases.rejections.map((rejection) => `${JSON.stringify(rejection.alias.name)} \u2014 ${describeModelAliasRejection(rejection.reason)}`),
13338
+ ...targetUnavailableAliases.flatMap(({ sources }) => sources.map((source) => `${JSON.stringify(source.name)} \u2014 target unavailable`)),
13339
+ ...collidingAliases.flatMap(({ sources }) => sources.map((source) => `${JSON.stringify(source.name)} \u2014 conflicts with a catalog model id`))
13340
+ ];
13341
+ if (aliasWarnings.length > 0) {
13342
+ p9.log.warn(
13343
+ `${aliasWarnings.length} saved model alias${aliasWarnings.length === 1 ? "" : "es"} inactive. Saved entries were preserved.
13344
+ ${aliasWarnings.join("\n ")}`
13345
+ );
13346
+ }
13100
13347
  const inferenceLogPath = getInferenceRequestLogPath();
13101
13348
  const webSocketDiagnosticsLogPath = options.wsDiagnostics ? getSessionLogPath("server-websocket-diagnostics", "jsonl") : void 0;
13102
13349
  const server = await startServer({
@@ -13408,25 +13655,192 @@ function planLaunchWizard(opts) {
13408
13655
  }
13409
13656
 
13410
13657
  // src/patcher.ts
13411
- import { createHash as createHash7 } from "crypto";
13658
+ import { createHash as createHash8 } from "crypto";
13412
13659
  import {
13413
13660
  copyFileSync,
13414
- existsSync as existsSync6,
13661
+ existsSync as existsSync7,
13662
+ mkdtempSync,
13415
13663
  mkdirSync as mkdirSync7,
13416
- readFileSync as readFileSync8,
13417
- statSync as statSync3,
13664
+ readFileSync as readFileSync9,
13665
+ renameSync as renameSync3,
13666
+ rmSync,
13667
+ statSync as statSync4,
13418
13668
  unlinkSync as unlinkSync4,
13419
13669
  writeFileSync as writeFileSync7,
13420
13670
  openSync as openSync4,
13421
13671
  closeSync as closeSync4,
13422
13672
  realpathSync
13423
13673
  } from "fs";
13424
- import { homedir as homedir2 } from "os";
13425
- import { basename, join as join7 } from "path";
13674
+ import { homedir as homedir3 } from "os";
13675
+ import { basename, dirname as dirname5, join as join8 } from "path";
13426
13676
  import pc12 from "picocolors";
13427
13677
  import * as p11 from "@clack/prompts";
13428
13678
 
13679
+ // src/patch-backup.ts
13680
+ import { createHash as createHash7 } from "crypto";
13681
+ import { existsSync as existsSync6, readFileSync as readFileSync8, readdirSync, statSync as statSync3 } from "fs";
13682
+ import { homedir as homedir2 } from "os";
13683
+ import { join as join7 } from "path";
13684
+ var BACKUP_SHA_PREFIX_LENGTH = 16;
13685
+ function backupDir() {
13686
+ return process.env["TWEAKCC_CONFIG_DIR"]?.trim() || join7(homedir2(), ".tweakcc");
13687
+ }
13688
+ function sha256File(path) {
13689
+ return createHash7("sha256").update(readFileSync8(path)).digest("hex");
13690
+ }
13691
+ function backupVersionTag(version) {
13692
+ const tag = version.trim().replace(/[^\w.-]+/g, "_");
13693
+ if (!tag) throw new Error("clodex patch: refusing to name a pristine backup for an empty claude version");
13694
+ return tag;
13695
+ }
13696
+ function contentAddressedBackupPath(version, sha256, dir = backupDir()) {
13697
+ return join7(dir, `claude-${backupVersionTag(version)}-${sha256.slice(0, BACKUP_SHA_PREFIX_LENGTH)}.orig`);
13698
+ }
13699
+ function tweakccMirrorBackupPath(dir = backupDir()) {
13700
+ return join7(dir, "native-binary.backup");
13701
+ }
13702
+ function scanPristineBackups(version, dir = backupDir()) {
13703
+ const tag = backupVersionTag(version);
13704
+ const pattern = new RegExp(`^claude-${tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:-([0-9a-f]{${BACKUP_SHA_PREFIX_LENGTH}}))?\\.orig$`);
13705
+ let entries;
13706
+ try {
13707
+ entries = readdirSync(dir);
13708
+ } catch {
13709
+ return { valid: [], corrupt: [] };
13710
+ }
13711
+ const valid = [];
13712
+ const corrupt = [];
13713
+ for (const entry of entries.sort()) {
13714
+ const match = pattern.exec(entry);
13715
+ if (!match) continue;
13716
+ const path = join7(dir, entry);
13717
+ let sha256;
13718
+ try {
13719
+ if (!statSync3(path).isFile()) continue;
13720
+ sha256 = sha256File(path);
13721
+ } catch {
13722
+ corrupt.push(path);
13723
+ continue;
13724
+ }
13725
+ const embedded = match[1];
13726
+ if (embedded) {
13727
+ if (sha256.slice(0, BACKUP_SHA_PREFIX_LENGTH) !== embedded) {
13728
+ corrupt.push(path);
13729
+ continue;
13730
+ }
13731
+ valid.push({ path, kind: "content-addressed", sha256 });
13732
+ } else {
13733
+ valid.push({ path, kind: "legacy", sha256 });
13734
+ }
13735
+ }
13736
+ return { valid, corrupt };
13737
+ }
13738
+ var CLODEX_PATCH_COMMENT_PREFIX = "/*ccpatch:";
13739
+ var LEGACY_CLODEX_PATCH_MARKERS = [
13740
+ "Additional custom models: ",
13741
+ // PATCH 4 — Agent tool model description
13742
+ "function(_i){return _i.value===_o.value}",
13743
+ // PATCH 5 — picker dedupe guard
13744
+ '"clodex:'
13745
+ // PATCH 1/3 — canonical ids as model identities
13746
+ ];
13747
+ function isPatchedClaudeSource(source) {
13748
+ return source.includes(CLODEX_PATCH_COMMENT_PREFIX);
13749
+ }
13750
+ function looksLikeLegacyClodexPatch(source) {
13751
+ return !isPatchedClaudeSource(source) && LEGACY_CLODEX_PATCH_MARKERS.some((marker) => source.includes(marker));
13752
+ }
13753
+ function noBackupMessage(facts) {
13754
+ const corrupt = facts.corruptBackups?.length ? ` (${facts.corruptBackups.length} backup file(s) for this version failed integrity checks and were ignored)` : "";
13755
+ return `claude ${facts.version} is already patched and no trustworthy pristine backup for that version exists in ${backupDir()}${corrupt}. Reinstall Claude Code to get a pristine binary, then run \`clodex patch\`.`;
13756
+ }
13757
+ function selectRestoreSource(facts) {
13758
+ const notes = [];
13759
+ const manifest = facts.manifest && facts.manifest.binaryPath === facts.binaryPath ? facts.manifest : null;
13760
+ let chosen = manifest?.pristineSha256 ? facts.backups.find((backup) => backup.sha256 === manifest.pristineSha256) : void 0;
13761
+ if (!chosen && manifest?.backupPath) {
13762
+ chosen = facts.backups.find((backup) => backup.path === manifest.backupPath);
13763
+ if (!chosen) {
13764
+ notes.push(`Recorded pristine backup ${manifest.backupPath} is missing, corrupt, or belongs to another claude version \u2014 ignoring it.`);
13765
+ }
13766
+ }
13767
+ if (!chosen) {
13768
+ const distinct = [...new Set(facts.backups.map((backup) => backup.sha256))];
13769
+ if (distinct.length > 1) {
13770
+ return {
13771
+ action: "error",
13772
+ message: `Found conflicting pristine backups for claude ${facts.version}: ${facts.backups.map((backup) => backup.path).join(", ")}. They do not hold the same bytes, so clodex cannot tell which one is pristine. Remove the wrong one (or reinstall Claude Code), then run \`clodex patch\`.`
13773
+ };
13774
+ }
13775
+ chosen = facts.backups.find((backup) => backup.kind === "content-addressed") ?? facts.backups[0];
13776
+ }
13777
+ if (!chosen) return { action: "error", message: noBackupMessage(facts) };
13778
+ return {
13779
+ action: "restore",
13780
+ backupPath: chosen.path,
13781
+ pristineSha256: chosen.sha256,
13782
+ // A legacy name carries no hash, so its bytes could be anything — including
13783
+ // another version's binary, stored under a mislabeled name by an older
13784
+ // clodex. Executing it is the only evidence available; require it.
13785
+ probeVersion: chosen.kind === "legacy",
13786
+ notes
13787
+ };
13788
+ }
13789
+ function planPristineSource(facts) {
13790
+ const identical = facts.backups.find((backup) => backup.sha256 === facts.liveSha256 && backup.kind === "content-addressed") ?? facts.backups.find((backup) => backup.sha256 === facts.liveSha256);
13791
+ if (identical) {
13792
+ return { action: "reuse", backupPath: identical.path, pristineSha256: identical.sha256, notes: [] };
13793
+ }
13794
+ const manifest = facts.manifest;
13795
+ if (manifest && manifest.binaryPath === facts.binaryPath && manifest.patchedSha256 === facts.liveSha256) {
13796
+ return selectRestoreSource(facts);
13797
+ }
13798
+ return { action: "inspect" };
13799
+ }
13800
+ function planInspectedPristineSource(facts, inspection) {
13801
+ if (inspection.patched) return selectRestoreSource(facts);
13802
+ const notes = [];
13803
+ const conflicting = facts.backups.filter((backup) => backup.sha256 !== facts.liveSha256);
13804
+ if (conflicting.length) {
13805
+ notes.push(
13806
+ `Existing backup(s) for claude ${facts.version} hold different bytes (${conflicting.map((b) => b.path).join(", ")}); the binary being patched carries no clodex patch marker, so it is being stored under its own content address. Both files are kept.`
13807
+ );
13808
+ }
13809
+ return {
13810
+ action: "snapshot",
13811
+ backupPath: contentAddressedBackupPath(facts.version, facts.liveSha256),
13812
+ pristineSha256: facts.liveSha256,
13813
+ notes
13814
+ };
13815
+ }
13816
+ function planRestoreOnly(facts) {
13817
+ return selectRestoreSource(facts);
13818
+ }
13819
+ function collectPristineFacts(args) {
13820
+ const dir = args.dir ?? backupDir();
13821
+ const scan = scanPristineBackups(args.version, dir);
13822
+ return {
13823
+ version: args.version,
13824
+ binaryPath: args.binaryPath,
13825
+ liveSha256: existsSync6(args.binaryPath) ? sha256File(args.binaryPath) : "",
13826
+ manifest: args.manifest,
13827
+ backups: scan.valid,
13828
+ corruptBackups: scan.corrupt
13829
+ };
13830
+ }
13831
+
13429
13832
  // src/patch-transforms.ts
13833
+ var PATCH_TRANSFORMS_VERSION = 2;
13834
+ var NATIVE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
13835
+ var BASE_EFFORT_LEVELS = ["low", "medium", "high"];
13836
+ function projectNativeEffort(effort) {
13837
+ if (!effort || !Array.isArray(effort.levels) || typeof effort.defaultLevel !== "string") return void 0;
13838
+ const declared = new Set(effort.levels);
13839
+ const levels = NATIVE_EFFORT_LEVELS.filter((level) => declared.has(level));
13840
+ if (!BASE_EFFORT_LEVELS.every((level) => declared.has(level))) return void 0;
13841
+ if (!levels.some((level) => level === effort.defaultLevel)) return void 0;
13842
+ return { levels, defaultLevel: "high" };
13843
+ }
13430
13844
  var PatchApplyError = class extends Error {
13431
13845
  results;
13432
13846
  constructor(message, results) {
@@ -13441,21 +13855,37 @@ function formatPatchSiteLine(result) {
13441
13855
  function applyClodexPatches(source, config) {
13442
13856
  let js = source;
13443
13857
  const MODEL_CONFIG = config;
13444
- const ALIAS_TO_ID = {};
13858
+ const ALIAS_TO_ID = /* @__PURE__ */ Object.create(null);
13445
13859
  const IDENTITIES = [];
13446
- const DISPLAY_BY_IDENTITY = {};
13447
- const CONTEXT_BY_KEY = {};
13860
+ const DISPLAY_BY_IDENTITY = /* @__PURE__ */ Object.create(null);
13861
+ const CONTEXT_BY_KEY = /* @__PURE__ */ Object.create(null);
13862
+ const CONFIGURED_CAPABILITY_KEYS = /* @__PURE__ */ new Set();
13863
+ const EFFORT_BY_KEY = /* @__PURE__ */ Object.create(null);
13448
13864
  const report = [];
13449
13865
  const fail = (message) => {
13450
13866
  throw new PatchApplyError(message, report);
13451
13867
  };
13868
+ const capabilityKeys = (value) => {
13869
+ const normalized = value.trim().toLowerCase();
13870
+ const bare = normalized.replace(/\[1m\]$/i, "");
13871
+ return [.../* @__PURE__ */ new Set([bare, `${bare}[1m]`])];
13872
+ };
13873
+ const registerCapabilityKeys = (value) => {
13874
+ for (const key of capabilityKeys(value)) {
13875
+ CONFIGURED_CAPABILITY_KEYS.add(key);
13876
+ }
13877
+ };
13452
13878
  for (const [id, value] of Object.entries(MODEL_CONFIG)) {
13453
13879
  const spec = value && typeof value === "object" ? value : { alias: value };
13454
13880
  if (spec.alias !== void 0) {
13455
- const a = String(spec.alias).trim().toLowerCase();
13881
+ const rawAlias = String(spec.alias).trim();
13882
+ const a = rawAlias.toLowerCase();
13456
13883
  if (!/^[a-z0-9][a-z0-9._-]*(\[1m\])?$/.test(a)) {
13457
13884
  fail('clodex patch: alias "' + spec.alias + '" is not a safe lowercase alias');
13458
13885
  }
13886
+ if (isReservedModelAlias(a)) {
13887
+ fail('clodex patch: reserved alias "' + a + '" cannot be reassigned');
13888
+ }
13459
13889
  ALIAS_TO_ID[a] = String(id);
13460
13890
  IDENTITIES.push(a);
13461
13891
  if (spec.display) DISPLAY_BY_IDENTITY[a] = String(spec.display);
@@ -13463,6 +13893,10 @@ function applyClodexPatches(source, config) {
13463
13893
  IDENTITIES.push(String(id));
13464
13894
  if (spec.display) DISPLAY_BY_IDENTITY[String(id)] = String(spec.display);
13465
13895
  }
13896
+ if (spec.alias !== void 0) {
13897
+ registerCapabilityKeys(String(spec.alias));
13898
+ }
13899
+ registerCapabilityKeys(String(id));
13466
13900
  if (spec.context !== void 0) {
13467
13901
  const n = Number(spec.context);
13468
13902
  if (!Number.isInteger(n) || n <= 0) {
@@ -13476,6 +13910,22 @@ function applyClodexPatches(source, config) {
13476
13910
  if (spec.alias !== void 0) CONTEXT_BY_KEY[String(spec.alias).trim().toLowerCase()] = n;
13477
13911
  CONTEXT_BY_KEY[String(id).trim().toLowerCase()] = n;
13478
13912
  }
13913
+ if (spec.effort) {
13914
+ const effort = projectNativeEffort(spec.effort);
13915
+ if (!effort) {
13916
+ fail(
13917
+ `clodex patch: effort for "${id}" must include low, medium, and high with a native default`
13918
+ );
13919
+ }
13920
+ if (spec.alias !== void 0) {
13921
+ for (const key of capabilityKeys(String(spec.alias))) {
13922
+ EFFORT_BY_KEY[key] = effort;
13923
+ }
13924
+ }
13925
+ for (const key of capabilityKeys(String(id))) {
13926
+ EFFORT_BY_KEY[key] = effort;
13927
+ }
13928
+ }
13479
13929
  }
13480
13930
  const ALIASES = Object.keys(ALIAS_TO_ID);
13481
13931
  const MODELS = Object.keys(MODEL_CONFIG);
@@ -13603,19 +14053,91 @@ function applyClodexPatches(source, config) {
13603
14053
  );
13604
14054
  }
13605
14055
  }
14056
+ function patchEffortCapability(capability, marker, name, anchor) {
14057
+ const verdicts = Object.fromEntries(
14058
+ [...CONFIGURED_CAPABILITY_KEYS].map((key) => {
14059
+ const effort = EFFORT_BY_KEY[key];
14060
+ return [
14061
+ key,
14062
+ effort !== void 0 && (capability === "effort" || effort.levels.includes(capability === "xhigh_effort" ? "xhigh" : "max"))
14063
+ ];
14064
+ })
14065
+ );
14066
+ const hasMarker = js.includes(marker);
14067
+ if (Object.keys(verdicts).length === 0 && !hasMarker) return;
14068
+ const snippet = (arg) => marker + "var _ccv=Object.assign(Object.create(null)," + JSON.stringify(verdicts) + ")[String(" + arg + '||"").trim().toLowerCase()];if(_ccv!==void 0)return _ccv;';
14069
+ if (hasMarker) {
14070
+ const markerPattern = reEsc(marker);
14071
+ applyOnce(
14072
+ name + " (refresh)",
14073
+ new RegExp(
14074
+ markerPattern + 'var _ccv=Object\\.assign\\(Object\\.create\\(null\\),\\{[^{}]*\\}\\)\\[String\\(([\\w$]+)\\|\\|""\\)\\.trim\\(\\)\\.toLowerCase\\(\\)\\];if\\(_ccv!==void 0\\)return _ccv;'
14075
+ ),
14076
+ (_m, arg) => snippet(arg),
14077
+ { required: false, noopIsSkip: true }
14078
+ );
14079
+ return;
14080
+ }
14081
+ applyOnce(
14082
+ name,
14083
+ anchor,
14084
+ (_m, head, arg, body) => head + snippet(arg) + body,
14085
+ { required: false }
14086
+ );
14087
+ }
14088
+ patchEffortCapability(
14089
+ "effort",
14090
+ "/*ccpatch:effort*/",
14091
+ "PATCH 8a: effort capability",
14092
+ /(function [\w$]+\(([\w$]+)\)\{if\([\w$]+\(\2\)\)return!1;)(let [\w$]+=[\w$]+\(\2,"effort"\);)/
14093
+ );
14094
+ patchEffortCapability(
14095
+ "xhigh_effort",
14096
+ "/*ccpatch:xhigh-effort*/",
14097
+ "PATCH 8b: xhigh effort capability",
14098
+ /(function [\w$]+\(([\w$]+)\)\{if\([\w$]+\(\2\)\)return!1;)(let [\w$]+=[\w$]+\(\2,"xhigh_effort"\);)/
14099
+ );
14100
+ patchEffortCapability(
14101
+ "max_effort",
14102
+ "/*ccpatch:max-effort*/",
14103
+ "PATCH 8c: max effort capability",
14104
+ /(function [\w$]+\(([\w$]+)\)\{if\([\w$]+\(\2\)\)return!1;)(let [\w$]+=[\w$]+\(\2,"max_effort"\);)/
14105
+ );
14106
+ const DEFAULT_EFFORT_MARKER = "/*ccpatch:default-effort*/";
14107
+ const defaults = Object.fromEntries(
14108
+ Object.entries(EFFORT_BY_KEY).map(([key, effort]) => [key, effort.defaultLevel])
14109
+ );
14110
+ if (Object.keys(defaults).length || js.includes(DEFAULT_EFFORT_MARKER)) {
14111
+ const snippet = (arg) => DEFAULT_EFFORT_MARKER + "var _cce=Object.assign(Object.create(null)," + JSON.stringify(defaults) + ")[String(" + arg + '||"").trim().toLowerCase()];if(_cce!==void 0)return _cce;';
14112
+ if (js.includes(DEFAULT_EFFORT_MARKER)) {
14113
+ applyOnce(
14114
+ "PATCH 9: default effort (refresh)",
14115
+ /\/\*ccpatch:default-effort\*\/var _cce=Object\.assign\(Object\.create\(null\),\{[^{}]*\}\)\[String\(([\w$]+)\|\|""\)\.trim\(\)\.toLowerCase\(\)\];if\(_cce!==void 0\)return _cce;/,
14116
+ (_m, arg) => snippet(arg),
14117
+ { required: false, noopIsSkip: true }
14118
+ );
14119
+ } else {
14120
+ applyOnce(
14121
+ "PATCH 9: default effort",
14122
+ /(function [\w$]+\(([\w$]+)\)\{)(return [\w$]+\([\w$]+\(\2\)\)\?\.default_effort\?\?"high"\})/,
14123
+ (_m, head, arg, body) => head + snippet(arg) + body,
14124
+ { required: false }
14125
+ );
14126
+ }
14127
+ }
13606
14128
  return { content: js, results: report };
13607
14129
  }
13608
14130
 
13609
14131
  // src/patcher.ts
13610
14132
  function getPatchManifestPath() {
13611
- return join7(getAppHome(), "patch-state.json");
14133
+ return join8(getAppHome(), "patch-state.json");
13612
14134
  }
13613
14135
  function getPatchLockPath() {
13614
- return join7(getAppHome(), "patch.lock");
14136
+ return join8(getAppHome(), "patch.lock");
13615
14137
  }
13616
14138
  function readPatchManifest(path = getPatchManifestPath()) {
13617
14139
  try {
13618
- const parsed = JSON.parse(readFileSync8(path, "utf8"));
14140
+ const parsed = JSON.parse(readFileSync9(path, "utf8"));
13619
14141
  if (parsed && typeof parsed.binaryPath === "string" && typeof parsed.configHash === "string") {
13620
14142
  return parsed;
13621
14143
  }
@@ -13631,7 +14153,20 @@ function writePatchManifest(manifest, path = getPatchManifestPath()) {
13631
14153
  function buildPatchModelConfig(favorites, aliases, modelMetaFor) {
13632
14154
  const config = {};
13633
14155
  const unknownWindows = [];
13634
- const aliasByFavorite = new Map(aliases.map((a) => [`${a.providerId}:${a.modelId}`, a.name]));
14156
+ const normalizedAliases = normalizeModelAliases(aliases);
14157
+ const favoriteTargets = new Set(
14158
+ favorites.map((favorite) => `${favorite.providerId}:${favorite.modelId}`)
14159
+ );
14160
+ const targetRejections = normalizedAliases.accepted.filter(({ alias }) => !favoriteTargets.has(`${alias.providerId}:${alias.modelId}`)).flatMap(({ sources }) => sources.map((source) => ({
14161
+ alias: source,
14162
+ reason: "target-not-favorite"
14163
+ })));
14164
+ const aliasByFavorite = new Map(
14165
+ normalizedAliases.aliases.filter((alias) => favoriteTargets.has(`${alias.providerId}:${alias.modelId}`)).map((alias) => [
14166
+ `${alias.providerId}:${alias.modelId}`,
14167
+ alias.name
14168
+ ])
14169
+ );
13635
14170
  for (const favorite of favorites) {
13636
14171
  const id = stripOneMContextSuffix(httpProxyModelId(favorite.providerId, favorite.modelId));
13637
14172
  if (config[id]) continue;
@@ -13644,29 +14179,68 @@ function buildPatchModelConfig(favorites, aliases, modelMetaFor) {
13644
14179
  else if (context !== 2e5) entry.context = context;
13645
14180
  const display = meta?.displayName?.trim();
13646
14181
  if (display) entry.display = display;
14182
+ const effort = projectNativeEffort(meta?.effort);
14183
+ if (effort) entry.effort = effort;
13647
14184
  config[id] = entry;
13648
14185
  }
13649
- return { config, unknownWindows };
14186
+ return {
14187
+ config,
14188
+ unknownWindows,
14189
+ rejectedAliases: [
14190
+ ...normalizedAliases.rejected,
14191
+ ...targetRejections.map((rejection) => rejection.alias)
14192
+ ],
14193
+ rejectedAliasRejections: [
14194
+ ...normalizedAliases.rejections,
14195
+ ...targetRejections
14196
+ ]
14197
+ };
13650
14198
  }
13651
- function computePatchConfigHash(config) {
14199
+ function reportRejectedModelAliases(rejections) {
14200
+ for (const rejection of rejections) {
14201
+ p11.log.warn(
14202
+ `Saved model alias ${JSON.stringify(rejection.alias.name)} was not patched \u2014 ${describeModelAliasRejection(rejection.reason)}. The saved entry was preserved.`
14203
+ );
14204
+ }
14205
+ }
14206
+ function computePatchConfigHash(config, transformsVersion = PATCH_TRANSFORMS_VERSION) {
13652
14207
  const canonical = Object.keys(config).sort().map((key) => {
13653
14208
  const entry = config[key];
13654
- return [key, entry.alias ?? null, entry.context ?? null, entry.display ?? null];
14209
+ return [
14210
+ key,
14211
+ entry.alias ?? null,
14212
+ entry.context ?? null,
14213
+ entry.display ?? null,
14214
+ entry.effort?.levels ?? null,
14215
+ entry.effort?.defaultLevel ?? null
14216
+ ];
13655
14217
  });
13656
- return createHash7("sha256").update(JSON.stringify(canonical)).digest("hex");
14218
+ return createHash8("sha256").update(JSON.stringify([transformsVersion, canonical])).digest("hex");
13657
14219
  }
13658
14220
  function buildDesiredPatchConfig() {
13659
14221
  const prefs = loadPreferences();
13660
14222
  const favorites = prefs.favoriteModels ?? [];
13661
- const aliases = prefs.modelAliases ?? [];
14223
+ const aliases = prefs.modelAliases;
13662
14224
  const registry = loadRegistry();
13663
14225
  const meta = /* @__PURE__ */ new Map();
13664
14226
  for (const provider of registry.providers) {
13665
14227
  for (const model of provider.modelsCache?.models ?? []) {
14228
+ const npm = model.npm ?? provider.api.npm ?? "";
14229
+ const upstreamModelId2 = model.upstreamModelId ?? model.id;
14230
+ const modelsDev = findModelsDevModel(provider.id, model.id);
14231
+ const effort = getPatchReasoningCapabilities(npm, upstreamModelId2, {
14232
+ providerId: provider.id,
14233
+ apiBaseUrl: model.apiUrl ?? provider.api.url,
14234
+ supportedParameters: model.supportedParameters,
14235
+ reasoning: model.reasoning ?? modelsDev?.reasoning,
14236
+ interleavedReasoningField: model.interleavedReasoningField ?? modelsDev?.interleaved?.field,
14237
+ upstreamModelId: upstreamModelId2
14238
+ });
13666
14239
  meta.set(`${provider.id}:${model.id}`, {
13667
14240
  contextWindow: model.contextWindow && model.contextWindow > 0 ? model.contextWindow : void 0,
13668
14241
  // Same label `clodex server` prints at startup and `models --list` shows.
13669
- displayName: httpProxyDisplayName(model, provider.name)
14242
+ displayName: httpProxyDisplayName(model, provider.name),
14243
+ effort: effort.mode === "controllable" ? { levels: effort.levels, defaultLevel: effort.defaultLevel } : void 0
13670
14244
  });
13671
14245
  }
13672
14246
  }
@@ -13696,7 +14270,7 @@ function pidIsAlive(pid) {
13696
14270
  function tryAcquirePatchLock(lockPath = getPatchLockPath(), opts = {}) {
13697
14271
  const now = opts.now ?? Date.now();
13698
14272
  const isAlive = opts.isAlive ?? pidIsAlive;
13699
- mkdirSync7(join7(lockPath, ".."), { recursive: true, mode: 448 });
14273
+ mkdirSync7(join8(lockPath, ".."), { recursive: true, mode: 448 });
13700
14274
  for (let attempt = 0; attempt < 2; attempt++) {
13701
14275
  try {
13702
14276
  const fd = openSync4(lockPath, "wx");
@@ -13712,7 +14286,7 @@ function tryAcquirePatchLock(lockPath = getPatchLockPath(), opts = {}) {
13712
14286
  } catch {
13713
14287
  let stale = false;
13714
14288
  try {
13715
- const existing = JSON.parse(readFileSync8(lockPath, "utf8"));
14289
+ const existing = JSON.parse(readFileSync9(lockPath, "utf8"));
13716
14290
  stale = !existing.pid || !isAlive(existing.pid) || typeof existing.startedAt === "number" && now - existing.startedAt > PATCH_LOCK_STALE_MS;
13717
14291
  } catch {
13718
14292
  stale = true;
@@ -13726,33 +14300,28 @@ function tryAcquirePatchLock(lockPath = getPatchLockPath(), opts = {}) {
13726
14300
  }
13727
14301
  return null;
13728
14302
  }
13729
- function sha256File(path) {
13730
- return createHash7("sha256").update(readFileSync8(path)).digest("hex");
13731
- }
13732
14303
  function resolveClaudeBinaryForPatch() {
13733
14304
  const envOverride = process.env["TWEAKCC_CC_INSTALLATION_PATH"];
13734
- const nativeSymlink = join7(homedir2(), ".local", "bin", "claude");
13735
- const source = envOverride?.trim() || (existsSync6(nativeSymlink) ? nativeSymlink : null) || findClaudeBinary();
13736
- if (!source) return null;
14305
+ const nativeSymlink = join8(homedir3(), ".local", "bin", "claude");
14306
+ const source = envOverride?.trim() || (existsSync7(nativeSymlink) ? nativeSymlink : null) || findClaudeBinary();
14307
+ if (!source) return { ok: false, reason: "binary-not-found" };
13737
14308
  let resolved;
13738
14309
  try {
13739
14310
  resolved = realpathSync(source);
13740
14311
  } catch {
13741
- return null;
14312
+ return { ok: false, reason: "binary-not-found" };
13742
14313
  }
13743
14314
  try {
13744
- if (!statSync3(resolved).isFile()) return null;
14315
+ if (!statSync4(resolved).isFile()) return { ok: false, reason: "binary-not-found" };
13745
14316
  } catch {
13746
- return null;
14317
+ return { ok: false, reason: "binary-not-found" };
13747
14318
  }
13748
- return { binaryPath: resolved, version: getInstalledClaudeVersion() };
14319
+ const version = getClaudeVersionForBinary(resolved);
14320
+ if (!version) return { ok: false, reason: "version-unknown", binaryPath: resolved };
14321
+ return { ok: true, binaryPath: resolved, version };
13749
14322
  }
13750
- function backupDir() {
13751
- return process.env["TWEAKCC_CONFIG_DIR"]?.trim() || join7(homedir2(), ".tweakcc");
13752
- }
13753
- function pristineBackupPath(version, binaryPath) {
13754
- const tag = version.replace(/[^\w.-]+/g, "_") || basename(binaryPath);
13755
- return join7(backupDir(), `claude-${tag}.orig`);
14323
+ function describePatchTargetFailure(target) {
14324
+ return target.reason === "binary-not-found" ? "claude binary not found. Install Claude Code or set TWEAKCC_CC_INSTALLATION_PATH." : `Could not determine the version of ${target.binaryPath} (\`claude --version\` failed). clodex will not patch a binary whose version it cannot read, because the version selects the pristine backup it patches from. If a previous patch left the install broken, \`clodex patch --restore\` still works \u2014 it reads the version from the patch manifest.`;
13756
14325
  }
13757
14326
  function summarizePatchResults(results) {
13758
14327
  const lines = results.map(formatPatchSiteLine);
@@ -13765,26 +14334,112 @@ function summarizePatchResults(results) {
13765
14334
  }
13766
14335
  return lines;
13767
14336
  }
14337
+ function verifyPristineSource(plan, version) {
14338
+ if (!plan.probeVersion) return { ok: true };
14339
+ const backupVersion = getClaudeVersionForBinary(plan.backupPath);
14340
+ if (backupVersion === version) return { ok: true };
14341
+ return {
14342
+ ok: false,
14343
+ message: `Refusing to use ${plan.backupPath} as the pristine source for claude ${version}: it reports ${backupVersion ? `version ${backupVersion}` : "no version at all"}. That backup predates content-addressed backup names and does not hold this version's bytes. Remove it (or reinstall Claude Code), then run \`clodex patch\`.`
14344
+ };
14345
+ }
14346
+ function publishBackupFile(from, to) {
14347
+ const temp = `${to}.tmp-${process.pid}-${Date.now().toString(36)}`;
14348
+ try {
14349
+ copyFileSync(from, temp);
14350
+ renameSync3(temp, to);
14351
+ } catch (err) {
14352
+ try {
14353
+ rmSync(temp, { force: true });
14354
+ } catch {
14355
+ }
14356
+ throw err;
14357
+ }
14358
+ }
14359
+ function describePoisonedPristineSource(backupPath, version) {
14360
+ return `Refusing to patch from ${backupPath}: those bytes already carry a clodex patch, so they are not pristine and patching them would stack a patch on a patch. That backup was recorded as pristine for claude ${version} by an older clodex, which snapshotted whatever binary was live when no backup existed. Delete ${backupPath}, reinstall Claude Code, then run \`clodex patch\`.`;
14361
+ }
14362
+ function requiredEffortPatchFailures(results) {
14363
+ return results.filter(
14364
+ (result) => result.status === "FAIL" && (result.name.startsWith("PATCH 8a:") || result.name.startsWith("PATCH 8b:") || result.name.startsWith("PATCH 8c:") || result.name.startsWith("PATCH 9:"))
14365
+ );
14366
+ }
13768
14367
  async function applyPatch(binaryPath, version, desired, configHash, opts) {
13769
- const backup = pristineBackupPath(version, binaryPath);
13770
- mkdirSync7(backupDir(), { recursive: true });
13771
- if (opts.restoreFirst) {
13772
- if (!existsSync6(backup)) {
13773
- return { ok: false, message: `Cannot re-patch: pristine backup missing at ${backup}. Reinstall claude, then run clodex patch.` };
13774
- }
13775
- copyFileSync(backup, binaryPath);
13776
- } else if (!existsSync6(backup)) {
13777
- copyFileSync(binaryPath, backup);
13778
- }
13779
- copyFileSync(backup, join7(backupDir(), "native-binary.backup"));
13780
- const { tryDetectInstallation, readContent, writeContent } = await import("tweakcc");
14368
+ let candidateDir;
13781
14369
  let results;
14370
+ let patchedSize;
14371
+ let patchedSha256;
14372
+ let backup;
14373
+ let pristineSha256;
13782
14374
  try {
13783
- const installation = await tryDetectInstallation({ path: binaryPath });
13784
- const source = await readContent(installation);
13785
- const patched = applyClodexPatches(source, desired.config);
14375
+ mkdirSync7(backupDir(), { recursive: true });
14376
+ const { tryDetectInstallation, readContent, writeContent } = await import("tweakcc");
14377
+ candidateDir = mkdtempSync(join8(dirname5(binaryPath), ".clodex-patch-"));
14378
+ const candidatePath = join8(candidateDir, basename(binaryPath));
14379
+ const seedCandidate = async (from) => {
14380
+ copyFileSync(from, candidatePath);
14381
+ const installation = await tryDetectInstallation({ path: candidatePath });
14382
+ return { installation, source: await readContent(installation) };
14383
+ };
14384
+ const facts = collectPristineFacts({
14385
+ version,
14386
+ binaryPath,
14387
+ manifest: opts.manifest
14388
+ });
14389
+ const initial = planPristineSource(facts);
14390
+ let loaded = null;
14391
+ let plan;
14392
+ if (initial.action === "inspect") {
14393
+ loaded = await seedCandidate(binaryPath);
14394
+ if (looksLikeLegacyClodexPatch(loaded.source)) {
14395
+ p11.log.warn(
14396
+ `${binaryPath} carries no clodex patch marker but does look like a patch from a clodex older than the effort patch sites. Treating it as pristine; if \`/model\` shows stale entries afterwards, reinstall Claude Code and run \`clodex patch\` again.`
14397
+ );
14398
+ }
14399
+ plan = planInspectedPristineSource(facts, { patched: isPatchedClaudeSource(loaded.source) });
14400
+ if (plan.action !== "snapshot") loaded = null;
14401
+ } else {
14402
+ plan = initial;
14403
+ }
14404
+ if (plan.action === "error") return { ok: false, message: plan.message };
14405
+ for (const note of plan.notes) p11.log.warn(note);
14406
+ if (plan.action === "restore") {
14407
+ const verified = verifyPristineSource(plan, version);
14408
+ if (!verified.ok) return { ok: false, message: verified.message };
14409
+ p11.log.info(`Binary differs from its pristine backup \u2014 building a fresh patch candidate from ${plan.backupPath}.`);
14410
+ }
14411
+ pristineSha256 = plan.pristineSha256;
14412
+ backup = plan.backupPath;
14413
+ if (!loaded) {
14414
+ loaded = await seedCandidate(backup);
14415
+ if (isPatchedClaudeSource(loaded.source)) {
14416
+ return { ok: false, message: describePoisonedPristineSource(backup, version) };
14417
+ }
14418
+ } else if (plan.action === "snapshot") {
14419
+ publishBackupFile(candidatePath, plan.backupPath);
14420
+ }
14421
+ const canonical = contentAddressedBackupPath(version, pristineSha256);
14422
+ if (canonical !== backup) {
14423
+ const alreadyStored = facts.backups.some(
14424
+ (candidate) => candidate.path === canonical && candidate.sha256 === pristineSha256
14425
+ );
14426
+ if (!alreadyStored) publishBackupFile(backup, canonical);
14427
+ backup = canonical;
14428
+ }
14429
+ publishBackupFile(backup, tweakccMirrorBackupPath());
14430
+ const patched = applyClodexPatches(loaded.source, desired.config);
13786
14431
  results = patched.results;
13787
- await writeContent(installation, patched.content);
14432
+ const failedEffortPatches = requiredEffortPatchFailures(results);
14433
+ if (failedEffortPatches.length > 0) {
14434
+ throw new PatchApplyError(
14435
+ `clodex patch: required effort patches failed: ${failedEffortPatches.map((result) => result.name).join("; ")}`,
14436
+ results
14437
+ );
14438
+ }
14439
+ await writeContent(loaded.installation, patched.content);
14440
+ patchedSize = statSync4(candidatePath).size;
14441
+ patchedSha256 = sha256File(candidatePath);
14442
+ renameSync3(candidatePath, binaryPath);
13788
14443
  } catch (err) {
13789
14444
  const detailLines = err instanceof PatchApplyError ? summarizePatchResults(err.results) : [];
13790
14445
  if (opts.trace && detailLines.length) {
@@ -13796,6 +14451,19 @@ async function applyPatch(binaryPath, version, desired, configHash, opts) {
13796
14451
  message: `Patch failed: ${err instanceof Error ? err.message : String(err)}`,
13797
14452
  detailLines
13798
14453
  };
14454
+ } finally {
14455
+ if (candidateDir !== void 0) {
14456
+ try {
14457
+ rmSync(candidateDir, { recursive: true, force: true });
14458
+ } catch (err) {
14459
+ if (opts.trace) {
14460
+ process.stderr.write(
14461
+ `clodex patch: could not remove temporary candidate directory: ${err instanceof Error ? err.message : String(err)}
14462
+ `
14463
+ );
14464
+ }
14465
+ }
14466
+ }
13799
14467
  }
13800
14468
  if (opts.trace) {
13801
14469
  process.stderr.write(`${summarizePatchResults(results).join("\n")}
@@ -13805,9 +14473,10 @@ async function applyPatch(binaryPath, version, desired, configHash, opts) {
13805
14473
  binaryPath,
13806
14474
  claudeVersion: version,
13807
14475
  configHash,
13808
- patchedSize: statSync3(binaryPath).size,
13809
- patchedSha256: sha256File(binaryPath),
14476
+ patchedSize,
14477
+ patchedSha256,
13810
14478
  backupPath: backup,
14479
+ pristineSha256,
13811
14480
  patchedAt: (/* @__PURE__ */ new Date()).toISOString()
13812
14481
  };
13813
14482
  writePatchManifest(manifest);
@@ -13820,28 +14489,54 @@ async function applyPatch(binaryPath, version, desired, configHash, opts) {
13820
14489
  detailLines: summarizePatchResults(results)
13821
14490
  };
13822
14491
  }
13823
- async function runPatchCommand(opts = {}) {
13824
- const resolved = resolveClaudeBinaryForPatch();
13825
- if (!resolved) {
13826
- p11.log.error("claude binary not found. Install Claude Code or set TWEAKCC_CC_INSTALLATION_PATH.");
14492
+ function runRestoreCommand(target) {
14493
+ if (!target.ok && target.reason === "binary-not-found") {
14494
+ p11.log.error(describePatchTargetFailure(target));
13827
14495
  return 1;
13828
14496
  }
13829
- const { binaryPath, version } = resolved;
13830
- if (opts.restore) {
13831
- const manifest2 = readPatchManifest();
13832
- const backup = manifest2?.backupPath && existsSync6(manifest2.backupPath) ? manifest2.backupPath : pristineBackupPath(version, binaryPath);
13833
- if (!existsSync6(backup)) {
13834
- p11.log.error(`No pristine backup found for claude ${version} (${backup}).`);
13835
- return 1;
13836
- }
13837
- copyFileSync(backup, binaryPath);
13838
- try {
13839
- unlinkSync4(getPatchManifestPath());
13840
- } catch {
13841
- }
13842
- p11.log.success(`Restored pristine claude ${version} from ${backup}.`);
13843
- return 0;
14497
+ const binaryPath = target.binaryPath;
14498
+ const manifest = readPatchManifest();
14499
+ let version;
14500
+ if (target.ok) {
14501
+ version = target.version;
14502
+ } else if (manifest?.binaryPath === binaryPath && manifest.claudeVersion) {
14503
+ version = manifest.claudeVersion;
14504
+ p11.log.warn(
14505
+ `Could not read the version of ${binaryPath} (\`claude --version\` failed) \u2014 using claude ${version} from the patch manifest, which recorded it when this binary was patched. This is expected when a bad patch left the install unable to run.`
14506
+ );
14507
+ } else {
14508
+ p11.log.error(
14509
+ `Could not determine the version of ${binaryPath} (\`claude --version\` failed), and no patch manifest records a pristine backup for it, so clodex cannot tell which backup belongs to this install. Restore it by hand from ${backupDir()} (or reinstall Claude Code).`
14510
+ );
14511
+ return 1;
14512
+ }
14513
+ const plan = planRestoreOnly(collectPristineFacts({ version, binaryPath, manifest }));
14514
+ if (plan.action === "error") {
14515
+ p11.log.error(plan.message);
14516
+ return 1;
14517
+ }
14518
+ for (const note of plan.notes) p11.log.warn(note);
14519
+ const verified = verifyPristineSource(plan, version);
14520
+ if (!verified.ok) {
14521
+ p11.log.error(verified.message);
14522
+ return 1;
14523
+ }
14524
+ copyFileSync(plan.backupPath, binaryPath);
14525
+ try {
14526
+ unlinkSync4(getPatchManifestPath());
14527
+ } catch {
14528
+ }
14529
+ p11.log.success(`Restored pristine claude ${version} from ${plan.backupPath}.`);
14530
+ return 0;
14531
+ }
14532
+ async function runPatchCommand(opts = {}) {
14533
+ const target = resolveClaudeBinaryForPatch();
14534
+ if (opts.restore) return runRestoreCommand(target);
14535
+ if (!target.ok) {
14536
+ p11.log.error(describePatchTargetFailure(target));
14537
+ return 1;
13844
14538
  }
14539
+ const { binaryPath, version } = target;
13845
14540
  const desired = buildDesiredPatchConfig();
13846
14541
  if (Object.keys(desired.config).length === 0) {
13847
14542
  p11.log.error("No favorite models to patch. Save favorites with `clodex models` first.");
@@ -13850,13 +14545,14 @@ async function runPatchCommand(opts = {}) {
13850
14545
  for (const id of desired.unknownWindows) {
13851
14546
  p11.log.warn(`No context window metadata for ${id} \u2014 Claude Code will assume the 200k default.`);
13852
14547
  }
14548
+ reportRejectedModelAliases(desired.rejectedAliasRejections);
13853
14549
  const configHash = computePatchConfigHash(desired.config);
13854
14550
  const manifest = readPatchManifest();
13855
14551
  const state = evaluatePatchState(manifest, {
13856
14552
  binaryPath,
13857
14553
  claudeVersion: version,
13858
14554
  configHash,
13859
- binarySize: statSync3(binaryPath).size
14555
+ binarySize: statSync4(binaryPath).size
13860
14556
  });
13861
14557
  if (state === "current") {
13862
14558
  p11.log.success(`claude ${version} is already patched with the current model config \u2014 nothing to do.`);
@@ -13868,14 +14564,9 @@ async function runPatchCommand(opts = {}) {
13868
14564
  return 1;
13869
14565
  }
13870
14566
  try {
13871
- const backup = pristineBackupPath(version, binaryPath);
13872
- const restoreFirst = existsSync6(backup) && sha256File(backup) !== sha256File(binaryPath);
13873
- if (restoreFirst) {
13874
- p11.log.info("Binary differs from its pristine backup \u2014 restoring it before patching fresh.");
13875
- }
13876
14567
  const outcome = await applyPatch(binaryPath, version, desired, configHash, {
13877
14568
  trace: opts.trace ?? false,
13878
- restoreFirst
14569
+ manifest
13879
14570
  });
13880
14571
  if (!outcome.ok) {
13881
14572
  p11.log.error(outcome.message);
@@ -13895,15 +14586,21 @@ async function runLaunchPatchCheck(opts = {}) {
13895
14586
  try {
13896
14587
  const desired = buildDesiredPatchConfig();
13897
14588
  if (Object.keys(desired.config).length === 0) return;
13898
- const resolved = resolveClaudeBinaryForPatch();
13899
- if (!resolved) return;
14589
+ const target = resolveClaudeBinaryForPatch();
14590
+ if (!target.ok) {
14591
+ if (target.reason === "version-unknown" && !opts.agentStdout) {
14592
+ console.error(pc12.dim(`clodex: ${describePatchTargetFailure(target)}`));
14593
+ }
14594
+ return;
14595
+ }
14596
+ const resolved = target;
13900
14597
  const configHash = computePatchConfigHash(desired.config);
13901
14598
  const manifest = readPatchManifest();
13902
14599
  const state = evaluatePatchState(manifest, {
13903
14600
  binaryPath: resolved.binaryPath,
13904
14601
  claudeVersion: resolved.version,
13905
14602
  configHash,
13906
- binarySize: statSync3(resolved.binaryPath).size
14603
+ binarySize: statSync4(resolved.binaryPath).size
13907
14604
  });
13908
14605
  if (state === "current") return;
13909
14606
  const interactive = !opts.dryRun && !opts.agentStdout && process.stdin.isTTY === true && process.stdout.isTTY === true;
@@ -14343,6 +15040,7 @@ ${pc13.bold("Behavior:")}
14343
15040
  proxy mode, without opening the interactive manager.
14344
15041
  --alias <name=target> saves a short name for a proxy-mode favorite. The
14345
15042
  target is clodex:<provider-id>:<model-id> (the clodex: prefix is optional).
15043
+ Alias names are stored lowercase and cannot use client-reserved model names.
14346
15044
  --unalias <name> removes a saved short name.
14347
15045
 
14348
15046
  ${pc13.bold("How it works:")}
@@ -14384,7 +15082,19 @@ function printHelp(text4) {
14384
15082
  ${text4}
14385
15083
  `);
14386
15084
  }
15085
+ function reportInactiveCatalogAliases(modelAliases) {
15086
+ const unavailableAliases = modelAliases.filter((alias) => alias.unavailableReason !== void 0);
15087
+ if (unavailableAliases.length === 0) return;
15088
+ const warningLines = unavailableAliases.flatMap((alias) => alias.sourceNames?.length ? alias.sourceNames.map((name) => ` ${JSON.stringify(name)} \u2014 ${alias.unavailableReason}`) : [
15089
+ ` ${JSON.stringify(alias.savedName ?? alias.name)} \u2014 ${alias.unavailableReason}`
15090
+ ]);
15091
+ p12.log.warn(
15092
+ `${warningLines.length} saved model alias${warningLines.length === 1 ? "" : "es"} inactive. Saved entries were preserved.
15093
+ ` + warningLines.join("\n")
15094
+ );
15095
+ }
14387
15096
  async function launchClaudeViaCatalog(catalogRoutes, startingRoute, modelAliases, contextWindow, trace, claudeArgs) {
15097
+ reportInactiveCatalogAliases(modelAliases);
14388
15098
  let proxyHandle;
14389
15099
  try {
14390
15100
  proxyHandle = await startProxyCatalog(
@@ -14444,27 +15154,36 @@ async function runModelsCommand(opts = {}) {
14444
15154
  p12.log.info("Add it with `clodex models`, then save the alias.");
14445
15155
  return 1;
14446
15156
  }
14447
- const modelAliases = (prefs2.modelAliases ?? []).filter((alias) => alias.name !== parsed.name);
15157
+ if (prefs2.modelAliases !== void 0 && !Array.isArray(prefs2.modelAliases)) {
15158
+ p12.log.error('Saved model aliases are malformed: "modelAliases" must be an array.');
15159
+ return 1;
15160
+ }
15161
+ const aliases = prefs2.modelAliases ?? [];
15162
+ const modelAliases = aliases.filter((alias) => !modelAliasMatchesName(alias, parsed.name));
14448
15163
  modelAliases.push(parsed);
14449
15164
  savePreferences({ modelAliases });
14450
15165
  p12.log.success(`Saved model alias ${parsed.name} \u2192 ${modelAliasTarget(parsed)}.`);
14451
15166
  return 0;
14452
15167
  }
14453
15168
  if (opts.unalias !== void 0) {
14454
- const name = opts.unalias.trim();
14455
- if (!isValidModelAlias(name)) {
14456
- p12.log.error("Alias names must be 1-64 letters, numbers, dots, underscores, or hyphens.");
15169
+ const requestedName = opts.unalias.trim();
15170
+ const name = canonicalModelAliasName(requestedName);
15171
+ const prefs2 = loadPreferences();
15172
+ if (prefs2.modelAliases !== void 0 && !Array.isArray(prefs2.modelAliases)) {
15173
+ p12.log.error('Saved model aliases are malformed: "modelAliases" must be an array.');
14457
15174
  return 1;
14458
15175
  }
14459
- const prefs2 = loadPreferences();
14460
15176
  const aliases = prefs2.modelAliases ?? [];
14461
- const modelAliases = aliases.filter((alias) => alias.name !== name);
14462
- if (modelAliases.length === aliases.length) {
14463
- p12.log.error(`No model alias named ${name} is saved.`);
15177
+ const modelAliases = aliases.filter((alias) => !modelAliasMatchesStoredName(alias, requestedName));
15178
+ const removedCount = aliases.length - modelAliases.length;
15179
+ if (removedCount === 0) {
15180
+ p12.log.error(`No model alias named ${JSON.stringify(requestedName)} is saved.`);
14464
15181
  return 1;
14465
15182
  }
14466
15183
  savePreferences({ modelAliases });
14467
- p12.log.success(`Removed model alias ${name}.`);
15184
+ p12.log.success(
15185
+ removedCount === 1 ? `Removed model alias ${name || JSON.stringify(requestedName)}.` : `Removed ${removedCount} model aliases named ${name || JSON.stringify(requestedName)}.`
15186
+ );
14468
15187
  return 0;
14469
15188
  }
14470
15189
  if (opts.list) {
@@ -14948,7 +15667,11 @@ Error: ${launchPlan.error}
14948
15667
  return launchClaudeViaCatalog(
14949
15668
  catalogRoutes,
14950
15669
  startingRoute,
14951
- resolveCatalogModelAliases(prefs.modelAliases ?? [], resolveRoute),
15670
+ resolveCatalogModelAliases(
15671
+ prefs.modelAliases,
15672
+ resolveRoute,
15673
+ catalogRoutes
15674
+ ),
14952
15675
  selectedModel.contextWindow,
14953
15676
  trace,
14954
15677
  claudeArgs
@@ -15195,6 +15918,7 @@ export {
15195
15918
  modelsHelpText,
15196
15919
  parseArgs,
15197
15920
  patchHelpText,
15921
+ reportInactiveCatalogAliases,
15198
15922
  rootHelpText,
15199
15923
  runClaudeCommand,
15200
15924
  runModelsCommand,