@bman654/clodex 2.14.0 → 2.14.1

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
@@ -344,7 +344,7 @@ import { join } from "path";
344
344
  // package.json
345
345
  var package_default = {
346
346
  name: "@bman654/clodex",
347
- version: "2.14.0",
347
+ version: "2.14.1",
348
348
  publishConfig: {
349
349
  access: "public"
350
350
  },
@@ -3432,7 +3432,7 @@ function savedStopsAfter(current, assignments) {
3432
3432
  }
3433
3433
 
3434
3434
  // src/patch-transforms.ts
3435
- var PATCH_TRANSFORMS_VERSION = 12;
3435
+ var PATCH_TRANSFORMS_VERSION = 13;
3436
3436
  var NATIVE_EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
3437
3437
  var BASE_EFFORT_LEVELS = ["low", "medium", "high"];
3438
3438
  function projectNativeEffort(effort) {
@@ -3589,14 +3589,22 @@ function applyClodexPatches(source, config) {
3589
3589
  { required: true, noopIsSkip: true }
3590
3590
  );
3591
3591
  {
3592
- const missing = ALIASES.filter((a) => !new RegExp("case" + reEsc(q(a)) + ":return").test(js));
3592
+ const RESOLVER_ANCHOR = /(case"best":\{[^{}]*\})/;
3593
+ const RESOLVER_BUDGET = 2e3 + ALIASES.reduce((n, a) => n + 2 * a.length + 17, 0);
3594
+ const RESOLVER_SWITCH = new RegExp(
3595
+ 'case"best":\\{[^{}]*\\}[\\s\\S]{0,' + RESOLVER_BUDGET + "}?default:return"
3596
+ );
3597
+ const resolver = js.match(RESOLVER_SWITCH)?.[0] ?? "";
3598
+ const missing = ALIASES.filter(
3599
+ (a) => !new RegExp("case" + reEsc(q(a)) + ":return").test(resolver)
3600
+ );
3593
3601
  const cases = missing.map((a) => "case" + q(a) + ":return " + q(a) + ";").join("");
3594
3602
  if (ALIASES.length === 0) {
3595
3603
  log12("SKIP", "PATCH 6: alias resolver switch", "no aliases configured");
3596
3604
  } else {
3597
3605
  applyOnce(
3598
3606
  "PATCH 6: alias resolver switch",
3599
- /(case"best":\{[^{}]*\})/,
3607
+ RESOLVER_ANCHOR,
3600
3608
  (m) => m + cases,
3601
3609
  { required: true, noopIsSkip: true }
3602
3610
  );
@@ -11641,6 +11649,7 @@ function routeUnavailableMessage(modelId, reason) {
11641
11649
  }
11642
11650
 
11643
11651
  // src/upstream-forward.ts
11652
+ import { randomUUID as randomUUID4 } from "crypto";
11644
11653
  import { Readable, Transform } from "stream";
11645
11654
  import { StringDecoder } from "string_decoder";
11646
11655
 
@@ -11729,18 +11738,50 @@ async function fetchWithOAuthRetry(apiKey, request3, refreshToken) {
11729
11738
  response = await request3(refreshed);
11730
11739
  return { response, apiKey: refreshed, refreshed: true };
11731
11740
  }
11741
+ function upstreamHoldsThreads(url) {
11742
+ try {
11743
+ return new URL(url).hostname.toLowerCase() === "api.anthropic.com";
11744
+ } catch {
11745
+ return false;
11746
+ }
11747
+ }
11748
+ function anchorSafeMessageId(id) {
11749
+ return typeof id === "string" && id.startsWith("msg_") ? "clodex_" + randomUUID4().replace(/-/g, "") : id;
11750
+ }
11751
+ function isThreadContinuation(body) {
11752
+ const thread = body.thread;
11753
+ return typeof thread === "object" && thread !== null && thread.type === "continue";
11754
+ }
11755
+ var THREAD_UNSUPPORTED_BODY = JSON.stringify({
11756
+ type: "error",
11757
+ error: {
11758
+ type: "invalid_request_error",
11759
+ message: "thread continuation is not supported on this route; resend the full conversation",
11760
+ details: { error_code: "thread_unsupported_request" }
11761
+ }
11762
+ });
11732
11763
  var SSE_LINE_SPLIT = /(\r\n|\r|\n)/;
11733
- function anthropicSseModelRewrite(override) {
11764
+ function anthropicSseMessageStartRewrite(rewrite) {
11734
11765
  const decoder = new StringDecoder("utf8");
11735
11766
  let tail = "";
11736
11767
  const rewriteLine = (line) => {
11737
11768
  if (!line.startsWith("data:") || !line.includes('"message_start"')) return line;
11738
11769
  try {
11739
11770
  const parsed = JSON.parse(line.slice(5));
11740
- if (parsed.type === "message_start" && parsed.message && typeof parsed.message.model === "string") {
11741
- parsed.message.model = override;
11742
- return "data: " + JSON.stringify(parsed);
11771
+ if (parsed.type !== "message_start" || !parsed.message) return line;
11772
+ let changed = false;
11773
+ if (rewrite.model !== void 0 && typeof parsed.message.model === "string") {
11774
+ parsed.message.model = rewrite.model;
11775
+ changed = true;
11776
+ }
11777
+ if (rewrite.anchorSafeId) {
11778
+ const id = anchorSafeMessageId(parsed.message.id);
11779
+ if (id !== parsed.message.id) {
11780
+ parsed.message.id = id;
11781
+ changed = true;
11782
+ }
11743
11783
  }
11784
+ if (changed) return "data: " + JSON.stringify(parsed);
11744
11785
  } catch {
11745
11786
  }
11746
11787
  return line;
@@ -11806,8 +11847,11 @@ async function relayAnthropicMessages(res, messagesUrl, body, apiKey, clientWant
11806
11847
  "Connection": "keep-alive"
11807
11848
  });
11808
11849
  const upstream = Readable.fromWeb(upstreamRes.body).on("error", () => res.destroy());
11809
- if (options.responseModelOverride) {
11810
- upstream.pipe(anthropicSseModelRewrite(options.responseModelOverride)).on("error", () => res.destroy()).pipe(res);
11850
+ if (options.responseModelOverride || options.anchorSafeMessageIds) {
11851
+ upstream.pipe(anthropicSseMessageStartRewrite({
11852
+ model: options.responseModelOverride,
11853
+ anchorSafeId: options.anchorSafeMessageIds
11854
+ })).on("error", () => res.destroy()).pipe(res);
11811
11855
  } else {
11812
11856
  upstream.pipe(res);
11813
11857
  }
@@ -11827,9 +11871,21 @@ async function relayAnthropicMessages(res, messagesUrl, body, apiKey, clientWant
11827
11871
  res.end(JSON.stringify({ type: "error", error: { type: "api_error", message: "Upstream response was not valid JSON" } }));
11828
11872
  return;
11829
11873
  }
11830
- if (options.responseModelOverride && parsed && typeof parsed === "object" && !Array.isArray(parsed) && parsed.type === "message" && typeof parsed.model === "string") {
11831
- parsed.model = options.responseModelOverride;
11832
- text5 = JSON.stringify(parsed);
11874
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed) && parsed.type === "message") {
11875
+ const message = parsed;
11876
+ let changed = false;
11877
+ if (options.responseModelOverride && typeof message.model === "string") {
11878
+ message.model = options.responseModelOverride;
11879
+ changed = true;
11880
+ }
11881
+ if (options.anchorSafeMessageIds) {
11882
+ const id = anchorSafeMessageId(message.id);
11883
+ if (id !== message.id) {
11884
+ message.id = id;
11885
+ changed = true;
11886
+ }
11887
+ }
11888
+ if (changed) text5 = JSON.stringify(message);
11833
11889
  }
11834
11890
  res.writeHead(200, {
11835
11891
  "Content-Type": "application/json",
@@ -11839,10 +11895,10 @@ async function relayAnthropicMessages(res, messagesUrl, body, apiKey, clientWant
11839
11895
  }
11840
11896
 
11841
11897
  // src/proxy.ts
11842
- import { randomUUID as randomUUID4 } from "crypto";
11898
+ import { randomUUID as randomUUID6 } from "crypto";
11843
11899
 
11844
11900
  // src/sdk-adapter.ts
11845
- import { createHash as createHash7 } from "crypto";
11901
+ import { createHash as createHash7, randomUUID as randomUUID5 } from "crypto";
11846
11902
  import { streamText, generateText, tool, jsonSchema } from "ai";
11847
11903
 
11848
11904
  // src/proxy-shared.ts
@@ -12690,8 +12746,11 @@ function forwardAbortSignal(source, target) {
12690
12746
  source.addEventListener("abort", forward, { once: true });
12691
12747
  return () => source.removeEventListener("abort", forward);
12692
12748
  }
12749
+ function translatedMessageId() {
12750
+ return "clodex_" + randomUUID5().replace(/-/g, "");
12751
+ }
12693
12752
  async function writeAnthropicStream(stream, modelId, write, log12, observer, tools) {
12694
- const messageId = "msg_" + Date.now();
12753
+ const messageId = translatedMessageId();
12695
12754
  const requiredProps = toolRequiredProps(tools);
12696
12755
  let blockIndex = -1;
12697
12756
  let started = false;
@@ -13049,7 +13108,7 @@ async function generateAnthropicResponse(model, params, modelId, options) {
13049
13108
  reportPromptTokens({ onPromptTokens: options?.onPromptTokens }, usage);
13050
13109
  const requiredProps = toolRequiredProps(params.tools);
13051
13110
  return {
13052
- id: "msg_" + Date.now(),
13111
+ id: translatedMessageId(),
13053
13112
  type: "message",
13054
13113
  role: "assistant",
13055
13114
  model: modelId,
@@ -13265,7 +13324,7 @@ function configuredAliasLookupNames(alias) {
13265
13324
  }))].map(normalizeRouteLookupId);
13266
13325
  }
13267
13326
  async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPath, debugLogPath, webSocketDiagnosticsLogPath, modelAliases) {
13268
- const proxyToken = randomUUID4();
13327
+ const proxyToken = randomUUID6();
13269
13328
  silenceSdkWarnings();
13270
13329
  if (routes.length === 0) {
13271
13330
  throw new Error("Proxy catalog requires at least one route");
@@ -13417,6 +13476,13 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
13417
13476
  }
13418
13477
  return;
13419
13478
  }
13479
+ const holdsThreads = route.modelFormat === "anthropic" && upstreamHoldsThreads(upstreamUrl);
13480
+ if (!holdsThreads && isThreadContinuation(anthropicBody)) {
13481
+ plog(() => `thread continuation refused: route=${route.realModelId} upstream does not hold threads`);
13482
+ res.writeHead(400, { "Content-Type": "application/json" });
13483
+ res.end(THREAD_UNSUPPORTED_BODY);
13484
+ return;
13485
+ }
13420
13486
  if (!apiKey && routeAuthType !== "none" && !usesSdkAdapter) {
13421
13487
  anthropicError(res, 401, "Missing API key");
13422
13488
  return;
@@ -13470,6 +13536,9 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
13470
13536
  // the client would believe it reached a model it never reached, and
13471
13537
  // would key its context window off that phantom id.
13472
13538
  responseModelOverride: resolvedRoute && typeof originalModel === "string" && originalModel !== route.realModelId ? originalModel : void 0,
13539
+ // An upstream that holds no threads must not hand Claude Code an id
13540
+ // it would anchor the next request on.
13541
+ anchorSafeMessageIds: !holdsThreads,
13473
13542
  onUpstreamError: inferenceLogPath ? (statusCode, errorContent) => writeInferenceResponseErrorLog(inferenceLogPath, {
13474
13543
  modelId: originalModel,
13475
13544
  provider: route.providerId ?? route.aliasId.split(":")[1] ?? "unknown",
@@ -13678,7 +13747,7 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
13678
13747
  res,
13679
13748
  upstreamStatus === 500 ? 502 : upstreamStatus,
13680
13749
  clientMessage,
13681
- contextLengthExceeded ? relayRequestId ?? randomUUID4() : void 0
13750
+ contextLengthExceeded ? relayRequestId ?? randomUUID6() : void 0
13682
13751
  );
13683
13752
  } else {
13684
13753
  const errorType = anthropicErrorType(upstreamStatus, details?.transportCode);
@@ -13686,7 +13755,7 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
13686
13755
  data: ${JSON.stringify({
13687
13756
  type: "error",
13688
13757
  error: { type: errorType, message: clientMessage },
13689
- ...contextLengthExceeded ? { request_id: relayRequestId ?? randomUUID4() } : {}
13758
+ ...contextLengthExceeded ? { request_id: relayRequestId ?? randomUUID6() } : {}
13690
13759
  })}
13691
13760
 
13692
13761
  `);
@@ -15009,7 +15078,7 @@ function localProvidersToServerModels(localProviders) {
15009
15078
  }
15010
15079
 
15011
15080
  // src/registry/credential-cleanup-journal.ts
15012
- import { randomUUID as randomUUID5 } from "crypto";
15081
+ import { randomUUID as randomUUID7 } from "crypto";
15013
15082
  import {
15014
15083
  closeSync as closeSync9,
15015
15084
  existsSync as existsSync5,
@@ -15149,7 +15218,7 @@ function writeJournalUnlocked(journal, path) {
15149
15218
  assertRegistryWriteOwnership(path);
15150
15219
  ensureSecureAppHome();
15151
15220
  mkdirSync6(dirname6(path), { recursive: true, mode: DIR_MODE2 });
15152
- const tmp = `${path}.${process.pid}.${randomUUID5()}.tmp`;
15221
+ const tmp = `${path}.${process.pid}.${randomUUID7()}.tmp`;
15153
15222
  let fd;
15154
15223
  try {
15155
15224
  fd = openSync9(tmp, "wx", FILE_MODE4);
@@ -18042,7 +18111,7 @@ async function askSaveServerPassword() {
18042
18111
 
18043
18112
  // src/server/router.ts
18044
18113
  import { createServer as createServer2 } from "http";
18045
- import { randomUUID as randomUUID6 } from "crypto";
18114
+ import { randomUUID as randomUUID8 } from "crypto";
18046
18115
 
18047
18116
  // src/openai-adapter.ts
18048
18117
  import { tool as tool2, jsonSchema as jsonSchema2, streamText as streamText2, generateText as generateText2 } from "ai";
@@ -18458,7 +18527,7 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
18458
18527
  plog(`model not found: ${body.model}`);
18459
18528
  return;
18460
18529
  }
18461
- const requestId = randomUUID6();
18530
+ const requestId = randomUUID8();
18462
18531
  const claudeSessionIdHeader = Array.isArray(req.headers["x-claude-code-session-id"]) ? req.headers["x-claude-code-session-id"][0] : req.headers["x-claude-code-session-id"];
18463
18532
  const claudeSessionId = extractClaudeSessionId(body, claudeSessionIdHeader);
18464
18533
  const claudeAgentIds = extractClaudeAgentIds(req.headers);
@@ -19132,7 +19201,7 @@ import * as http2 from "http";
19132
19201
  import * as https from "https";
19133
19202
  import * as net from "net";
19134
19203
  import { PassThrough } from "stream";
19135
- import { randomUUID as randomUUID7 } from "crypto";
19204
+ import { randomUUID as randomUUID9 } from "crypto";
19136
19205
  import { URL as URL2 } from "url";
19137
19206
  import { createBrotliDecompress, createGunzip, createInflate } from "zlib";
19138
19207
 
@@ -19990,7 +20059,7 @@ async function startHttpProxy(options) {
19990
20059
  }
19991
20060
  const messagesEndpoint = anthropicMessagesEndpoint(req.url);
19992
20061
  if (req.method === "POST" && messagesEndpoint) {
19993
- const requestId = randomUUID7();
20062
+ const requestId = randomUUID9();
19994
20063
  let parsed = null;
19995
20064
  let route;
19996
20065
  let adapterBody = rawBody;