@bman654/clodex 2.2.2 → 2.4.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
@@ -40,7 +40,7 @@ import {
40
40
  withProviderMutationLock,
41
41
  withRegistryWriteLock,
42
42
  withRegistryWriteLockSync
43
- } from "./chunk-W4SVDQCZ.js";
43
+ } from "./chunk-LBEJOEUY.js";
44
44
 
45
45
  // src/cli.ts
46
46
  import pc13 from "picocolors";
@@ -218,7 +218,7 @@ import { join } from "path";
218
218
  // package.json
219
219
  var package_default = {
220
220
  name: "@bman654/clodex",
221
- version: "2.2.2",
221
+ version: "2.4.0",
222
222
  publishConfig: {
223
223
  access: "public"
224
224
  },
@@ -840,6 +840,51 @@ function applyClaudeCodeThirdPartyCompat(env) {
840
840
  env["ENABLE_TOOL_SEARCH"] = "true";
841
841
  env["CLAUDE_CODE_SIMPLE_SYSTEM_PROMPT"] = "0";
842
842
  }
843
+ var LOOPBACK_NO_PROXY_ENTRIES = ["localhost", "127.0.0.1", "::1"];
844
+ function gatewayHostname(gatewayUrl) {
845
+ try {
846
+ return new URL(gatewayUrl).hostname.replace(/^\[|\]$/g, "").toLowerCase();
847
+ } catch {
848
+ return "";
849
+ }
850
+ }
851
+ function isLoopbackHost(host) {
852
+ if (host === "localhost" || host.endsWith(".localhost")) return true;
853
+ if (host === "::1" || host === "0:0:0:0:0:0:0:1") return true;
854
+ const v4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
855
+ if (!v4) return false;
856
+ const octets = v4.slice(1).map(Number);
857
+ if (octets.some((o) => o > 255)) return false;
858
+ return octets[0] === 127;
859
+ }
860
+ function effectiveNoProxyValue(env) {
861
+ return env["no_proxy"] || env["NO_PROXY"] || "";
862
+ }
863
+ function normalizedNoProxyEntries(env) {
864
+ return effectiveNoProxyValue(env).split(",").map((value) => value.trim()).filter(Boolean);
865
+ }
866
+ function addGatewayNoProxyBypass(env, gatewayUrl) {
867
+ const hasProxy = Boolean(
868
+ env["HTTPS_PROXY"]?.trim() || env["https_proxy"]?.trim() || env["HTTP_PROXY"]?.trim() || env["http_proxy"]?.trim()
869
+ );
870
+ if (!hasProxy) return;
871
+ const host = gatewayHostname(gatewayUrl);
872
+ if (!isLoopbackHost(host)) return;
873
+ if (effectiveNoProxyValue(env) === "*") return;
874
+ const existing = normalizedNoProxyEntries(env);
875
+ const additions = [...LOOPBACK_NO_PROXY_ENTRIES, host];
876
+ const seen = new Set(existing.map((entry) => entry.toLowerCase()));
877
+ const merged = [...existing];
878
+ for (const entry of additions) {
879
+ const key = entry.toLowerCase();
880
+ if (seen.has(key)) continue;
881
+ seen.add(key);
882
+ merged.push(entry);
883
+ }
884
+ const value = merged.join(",");
885
+ env["NO_PROXY"] = value;
886
+ env["no_proxy"] = value;
887
+ }
843
888
  function buildChildEnv(baseUrl, model, apiKey, proxyPort, contextWindow, enableGatewayDiscovery) {
844
889
  const env = { ...process.env };
845
890
  for (const name of CONFLICTING_ENV_VARS) {
@@ -847,6 +892,7 @@ function buildChildEnv(baseUrl, model, apiKey, proxyPort, contextWindow, enableG
847
892
  }
848
893
  env["ANTHROPIC_BASE_URL"] = proxyPort ? `http://127.0.0.1:${proxyPort}` : baseUrl;
849
894
  env["ANTHROPIC_API_KEY"] = apiKey;
895
+ addGatewayNoProxyBypass(env, env["ANTHROPIC_BASE_URL"]);
850
896
  const bareModel = stripOneMContextSuffix(model);
851
897
  env["ANTHROPIC_MODEL"] = claudeCodeClientModelId(model, contextWindow);
852
898
  env["CLAUDE_CODE_MAX_CONTEXT_TOKENS"] = String(resolveContextWindow(bareModel, contextWindow));
@@ -3414,6 +3460,7 @@ function applyPricingToRegistryProviders(registry, cache) {
3414
3460
  const index = buildPricingIndex(cache);
3415
3461
  let changed = false;
3416
3462
  for (const provider of registry.providers) {
3463
+ if (provider.preserveModelPricing) continue;
3417
3464
  if (!provider.modelsCache?.models.length) continue;
3418
3465
  const platform = TEMPLATE_TO_PRICING_PLATFORM[provider.templateId] ?? TEMPLATE_TO_PRICING_PLATFORM[provider.id];
3419
3466
  const enriched = enrichModelsWithPricing(provider.modelsCache.models, index, platform);
@@ -3716,7 +3763,9 @@ function cachedModelToLocal(cached, provider) {
3716
3763
  reasoning: cached.reasoning ?? modelsDev?.reasoning,
3717
3764
  interleavedReasoningField: cached.interleavedReasoningField ?? modelsDev?.interleaved?.field,
3718
3765
  useResponsesLite: cached.useResponsesLite,
3719
- preferWebSockets: cached.preferWebSockets
3766
+ preferWebSockets: cached.preferWebSockets,
3767
+ modalities: cached.modalities,
3768
+ compatibility: cached.compatibility
3720
3769
  };
3721
3770
  }
3722
3771
  function isAnonymousProvider(provider) {
@@ -3926,6 +3975,7 @@ function localProvidersToServerModels(localProviders) {
3926
3975
  interleavedReasoningField: model.interleavedReasoningField,
3927
3976
  useResponsesLite: model.useResponsesLite,
3928
3977
  preferWebSockets: model.preferWebSockets,
3978
+ compatibility: model.compatibility,
3929
3979
  headers: provider.headers,
3930
3980
  providerData: provider.providerData
3931
3981
  }))
@@ -4044,7 +4094,7 @@ function frameStatusCode(code, discriminator) {
4044
4094
  const numeric = Number(code);
4045
4095
  if (numeric >= 400 && numeric <= 599) return numeric;
4046
4096
  }
4047
- if (/insufficient_quota|rate_limit/.test(discriminator)) return 429;
4097
+ if (/insufficient_quota|rate_limit|usage_limit/.test(discriminator)) return 429;
4048
4098
  if (discriminator.includes("authentication")) return 401;
4049
4099
  if (discriminator.includes("permission")) return 403;
4050
4100
  if (discriminator.includes("not_found")) return 404;
@@ -4496,6 +4546,61 @@ function warnReasoningNormalizationGap(fields, log12) {
4496
4546
  } catch {
4497
4547
  }
4498
4548
  }
4549
+ function toolArgumentNormalizationGap(expected, actual, requiredProps) {
4550
+ if (conversationItemKind(expected) !== "function_call") return void 0;
4551
+ if (conversationItemKind(actual) !== "function_call") return void 0;
4552
+ const left = expected;
4553
+ const right = actual;
4554
+ const callId = left.call_id;
4555
+ if (typeof callId !== "string" || !callId || callId !== right.call_id) return void 0;
4556
+ if (typeof left.name !== "string" || left.name !== right.name) return void 0;
4557
+ if (canonicalJson(normalizeToolCallJson(left)) === canonicalJson(normalizeToolCallJson(right))) {
4558
+ return void 0;
4559
+ }
4560
+ const required = requiredProps().get(left.name);
4561
+ const stripped = (item) => {
4562
+ if (typeof item.arguments !== "string") return void 0;
4563
+ const raw = item.arguments.trim();
4564
+ try {
4565
+ const parsed = raw === "" ? {} : JSON.parse(raw);
4566
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return void 0;
4567
+ return canonicalJson({
4568
+ ...normalizeToolCallJson(item),
4569
+ arguments: canonicalJson(sanitizeToolInput(parsed, required))
4570
+ });
4571
+ } catch {
4572
+ return void 0;
4573
+ }
4574
+ };
4575
+ const leftStripped = stripped(left);
4576
+ const rightStripped = stripped(right);
4577
+ return {
4578
+ tool: left.name,
4579
+ equalAfterStrip: leftStripped !== void 0 && leftStripped === rightStripped
4580
+ };
4581
+ }
4582
+ var warnedToolArgumentGaps = /* @__PURE__ */ new Set();
4583
+ var MAX_TOOL_ARGUMENT_GAP_WARNINGS = 3;
4584
+ function warnToolArgumentNormalizationGap(gap, log12) {
4585
+ const tool3 = typeof gap.tool === "string" ? gap.tool : "unknown";
4586
+ const signature = `${tool3}:filler`;
4587
+ const message = `clodex: warning: tool call "${tool3}" failed the continuation match, but both sides are identical once clodex's filler-strip rule is applied, so the head should have matched. Prompt caching is degraded for this turn \u2014 please report it, with the adapter debug log from --trace if you can, at https://github.com/bman654/clodex/issues`;
4588
+ try {
4589
+ log12?.(`tool argument normalization gap: ${signature}`);
4590
+ } catch {
4591
+ }
4592
+ if (warnedToolArgumentGaps.has(signature)) return;
4593
+ if (warnedToolArgumentGaps.size >= MAX_TOOL_ARGUMENT_GAP_WARNINGS) return;
4594
+ warnedToolArgumentGaps.add(signature);
4595
+ try {
4596
+ process.stderr.write(`${message}
4597
+ `);
4598
+ if (warnedToolArgumentGaps.size === MAX_TOOL_ARGUMENT_GAP_WARNINGS) {
4599
+ process.stderr.write("clodex: warning: further tool-argument normalization warnings suppressed.\n");
4600
+ }
4601
+ } catch {
4602
+ }
4603
+ }
4499
4604
  function continuationMismatchDetails(entry, payload, log12, warnOnGap = false) {
4500
4605
  const full = inputArray(payload);
4501
4606
  const prefix = [...entry.requestInput ?? [], ...entry.expectedAssistant ?? []];
@@ -4511,6 +4616,34 @@ function continuationMismatchDetails(entry, payload, log12, warnOnGap = false) {
4511
4616
  const actual = mismatch < full.length ? full[mismatch] : void 0;
4512
4617
  const reasoningGap = reasoningNormalizationGap(expected, actual);
4513
4618
  if (reasoningGap && warnOnGap) warnReasoningNormalizationGap(reasoningGap, log12);
4619
+ let gapExpected = expected;
4620
+ if (conversationItemKind(expected) === "reasoning" && conversationItemKind(actual) === "function_call") {
4621
+ for (let index = mismatch; index < prefix.length; index += 1) {
4622
+ if (conversationItemKind(prefix[index]) !== "reasoning") {
4623
+ gapExpected = prefix[index];
4624
+ break;
4625
+ }
4626
+ }
4627
+ }
4628
+ let toolArgumentGap;
4629
+ try {
4630
+ toolArgumentGap = toolArgumentNormalizationGap(
4631
+ gapExpected,
4632
+ actual,
4633
+ // The head's own schema when it has one; the current turn's tools are only a
4634
+ // fallback for a head that predates the snapshot (see headRequiredToolProps).
4635
+ () => entry.headRequiredToolProps ?? requiredToolProps(payload)
4636
+ );
4637
+ } catch {
4638
+ }
4639
+ if (toolArgumentGap?.equalAfterStrip === true) {
4640
+ if (warnOnGap) warnToolArgumentNormalizationGap(toolArgumentGap, log12);
4641
+ } else if (toolArgumentGap && warnOnGap) {
4642
+ try {
4643
+ log12?.(`tool argument mismatch beyond the strip rule: ${String(toolArgumentGap.tool)}`);
4644
+ } catch {
4645
+ }
4646
+ }
4514
4647
  return {
4515
4648
  fullItems: full.length,
4516
4649
  expectedPrefixItems: prefix.length,
@@ -4528,11 +4661,12 @@ function continuationMismatchDetails(entry, payload, log12, warnOnGap = false) {
4528
4661
  entry.expectedAssistant ?? [],
4529
4662
  mismatch
4530
4663
  )
4531
- } : {}
4664
+ } : {},
4665
+ ...toolArgumentGap ? { toolArgumentNormalizationGap: toolArgumentGap } : {}
4532
4666
  };
4533
4667
  }
4534
- function continuationMismatchSummary(entry, payload, log12, mismatchDump = false) {
4535
- const details = continuationMismatchDetails(entry, payload, log12, true);
4668
+ function continuationMismatchSummary(entry, payload, log12, mismatchDump = false, precomputedDetails) {
4669
+ const details = precomputedDetails ?? continuationMismatchDetails(entry, payload, log12, true);
4536
4670
  let summary = `full_items=${details.fullItems} expected_prefix_items=${details.expectedPrefixItems} first_mismatch=${details.firstMismatch} expected=${details.expectedKind} actual=${details.actualKind}`;
4537
4671
  if (details.expectedHash || details.actualHash) {
4538
4672
  summary += ` expected_hash=${details.expectedHash ?? "none"} actual_hash=${details.actualHash ?? "none"}`;
@@ -4668,6 +4802,7 @@ function emitContextDiagnostic(entry, ctx, details) {
4668
4802
  retried: ctx.retried,
4669
4803
  frameCount: ctx.frameCount,
4670
4804
  emittedModelData: ctx.emittedModelData,
4805
+ emittedDownstreamData: ctx.emittedDownstreamData,
4671
4806
  responseIdReceived: Boolean(ctx.responseId),
4672
4807
  inFlightMs: entry.inFlightStartedAt === void 0 ? void 0 : Math.max(0, entry.options.now() - entry.inFlightStartedAt),
4673
4808
  ...details
@@ -4922,6 +5057,7 @@ function expectedAssistantItems(ctx) {
4922
5057
  }
4923
5058
  function encodeSse(ctx, event) {
4924
5059
  if (ctx.closed) return;
5060
+ ctx.emittedDownstreamData = true;
4925
5061
  ctx.controller.enqueue(ctx.encoder.encode(`data: ${JSON.stringify(event)}
4926
5062
 
4927
5063
  `));
@@ -4973,12 +5109,12 @@ function failContext(entry, ctx, message, diagnosticDetails, statusCode, retryAf
4973
5109
  closeContext(ctx);
4974
5110
  }
4975
5111
  function retryTransportFailure(entry, ctx, diagnosticDetails) {
4976
- if (ctx.closed || entry.current !== ctx || ctx.retried || ctx.frameCount !== 0 || ctx.emittedModelData) {
5112
+ if (ctx.closed || entry.current !== ctx || ctx.retried || !transportReplaySafe(ctx)) {
4977
5113
  return false;
4978
5114
  }
4979
5115
  ctx.retried = true;
4980
5116
  ctx.transportRetryPending = true;
4981
- entry.debug("transport failed before any response frame; retrying once with full context");
5117
+ entry.debug("transport failed before downstream output; retrying once with full context");
4982
5118
  emitContextDiagnostic(entry, ctx, {
4983
5119
  event: "ws_transport_retry",
4984
5120
  outcome: "started",
@@ -5012,9 +5148,9 @@ function retryTransportFailure(entry, ctx, diagnosticDetails) {
5012
5148
  function handleTransportFailure(entry, ctx, message, diagnosticDetails) {
5013
5149
  if (retryTransportFailure(entry, ctx, diagnosticDetails)) return;
5014
5150
  if (ctx.closed || entry.current !== ctx) return;
5015
- if (ctx.retried && ctx.frameCount === 0 && !ctx.emittedModelData) {
5151
+ if (ctx.retried && ctx.transportRetryPending && transportReplaySafe(ctx)) {
5016
5152
  ctx.transportRetryPending = false;
5017
- entry.debug("transport retry exhausted before any response frame");
5153
+ entry.debug("transport retry exhausted before downstream output");
5018
5154
  emitContextDiagnostic(entry, ctx, {
5019
5155
  event: "ws_transport_retry",
5020
5156
  outcome: "exhausted",
@@ -5118,6 +5254,9 @@ function resetContextForRetry(ctx) {
5118
5254
  ctx.recentUpstreamEventTypes = [];
5119
5255
  ctx.emittedProtocolAnomalies.clear();
5120
5256
  }
5257
+ function transportReplaySafe(ctx) {
5258
+ return !ctx.emittedDownstreamData && !ctx.emittedModelData && ctx.outputByIndex.size === 0;
5259
+ }
5121
5260
  function handleSocketMessage(entry, data) {
5122
5261
  const ctx = entry.current;
5123
5262
  if (!ctx || ctx.closed) return;
@@ -5178,7 +5317,8 @@ function handleSocketMessage(entry, data) {
5178
5317
  return;
5179
5318
  }
5180
5319
  const errorStatus = type === "error" && !ctx.emittedModelData ? responseErrorStatus(event) : void 0;
5181
- if (FAILURE_EVENT_TYPES.has(type ?? "") && (errorStatus === void 0 || willRetry)) {
5320
+ const emptyFailureTerminal = FAILURE_EVENT_TYPES.has(type ?? "") && errorStatus === void 0 && !willRetry && !ctx.emittedModelData;
5321
+ if (FAILURE_EVENT_TYPES.has(type ?? "") && (errorStatus === void 0 || willRetry) && !emptyFailureTerminal) {
5182
5322
  emitResponseErrorDiagnostic(entry, ctx, {
5183
5323
  source: "response_event",
5184
5324
  upstreamEventType: type,
@@ -5221,6 +5361,39 @@ function handleSocketMessage(entry, data) {
5221
5361
  );
5222
5362
  return;
5223
5363
  }
5364
+ if (emptyFailureTerminal) {
5365
+ const details = responseFailureDetails(event);
5366
+ const named = [details.errorType, details.errorCode, details.incompleteReason].filter((value) => typeof value === "string");
5367
+ const summary = "OpenAI ended the response with no output" + (named.length ? ` (${named.join(" / ")})` : ` (${type})`);
5368
+ const discriminator = [details.errorType, details.errorCode].filter((value) => typeof value === "string").join(" ").toLowerCase();
5369
+ const settledReason = details.incompleteReason === "content_filter" || details.incompleteReason === "max_output_tokens";
5370
+ const numericOrNamed = typeof details.errorCode === "string" ? details.errorCode : void 0;
5371
+ const classified = numericOrNamed !== void 0 || discriminator ? frameStatusCode(numericOrNamed, discriminator) : 500;
5372
+ const statusCode = classified !== 500 ? classified : settledReason ? 400 : 502;
5373
+ const usageLimited = statusCode === 429;
5374
+ const retryAfterSeconds = usageLimited && responseRetryAfterSeconds(event) !== void 0 ? clampRetryAfterSeconds(responseRetryAfterSeconds(event)) : void 0;
5375
+ failContext(
5376
+ entry,
5377
+ ctx,
5378
+ retryAfterSeconds === void 0 ? summary : `${summary}; retry after ${retryAfterSeconds}s`,
5379
+ {
5380
+ source: "empty_failure_terminal",
5381
+ upstreamEventType: type,
5382
+ ...details,
5383
+ // Under DISTINCT keys. `failContext` fingerprints the message it was
5384
+ // given after spreading these, so an `errorMessage*` pair here is
5385
+ // overwritten by the summary's — which would silently discard the only
5386
+ // content-free evidence of what upstream actually said, and leave two
5387
+ // failures with the same type and code indistinguishable. `errorMessage*`
5388
+ // now means "what the client was told", `upstreamMessage*` means "what
5389
+ // upstream said", and both survive.
5390
+ ...diagnosticTextFingerprint("upstreamMessage", responseErrorMessage(event))
5391
+ },
5392
+ statusCode,
5393
+ retryAfterSeconds
5394
+ );
5395
+ return;
5396
+ }
5224
5397
  ctx.pendingEvents.push(event);
5225
5398
  if (isModelDataEvent(type)) flushPending(ctx);
5226
5399
  if (TERMINAL_EVENT_TYPES.has(type ?? "") || type === "error") {
@@ -5232,6 +5405,7 @@ function handleSocketMessage(entry, data) {
5232
5405
  entry.responseId = ctx.responseId;
5233
5406
  entry.requestInput = inputArray(ctx.originalPayload);
5234
5407
  entry.expectedAssistant = expectedAssistantItems(ctx);
5408
+ entry.headRequiredToolProps = requiredToolProps(ctx.originalPayload);
5235
5409
  entry.canonicalPrefix = void 0;
5236
5410
  entry.canonicalEchoablePrefix = void 0;
5237
5411
  entry.promptFieldHashes = ctx.promptFieldHashes;
@@ -5417,6 +5591,7 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
5417
5591
  let persistent = Boolean(partitionKey);
5418
5592
  let promotedConnectionId;
5419
5593
  let decision;
5594
+ let candidateMismatchDetails;
5420
5595
  if (selected && selectedDelta) {
5421
5596
  sendPayload = { ...payload, input: selectedDelta, previous_response_id: selected.responseId };
5422
5597
  continued = true;
@@ -5439,8 +5614,23 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
5439
5614
  decision = "parallel_isolated";
5440
5615
  debug("parallel request using an isolated socket");
5441
5616
  } else if (diagnosticEntry) {
5617
+ const diagnosticMismatch = continuationMismatchDetails(diagnosticEntry, payload, debug, true);
5618
+ candidateMismatchDetails = /* @__PURE__ */ new Map([[diagnosticEntry, diagnosticMismatch]]);
5619
+ for (const candidate of candidates) {
5620
+ if (candidate === diagnosticEntry) continue;
5621
+ candidateMismatchDetails.set(
5622
+ candidate,
5623
+ continuationMismatchDetails(candidate, payload, debug, true)
5624
+ );
5625
+ }
5442
5626
  debug(
5443
- `history mismatch starting an additional chain; retained ${candidates.length} existing head(s) (${continuationMismatchSummary(diagnosticEntry, payload, debug, mismatchDump)})`
5627
+ `history mismatch starting an additional chain; retained ${candidates.length} existing head(s) (${continuationMismatchSummary(
5628
+ diagnosticEntry,
5629
+ payload,
5630
+ debug,
5631
+ mismatchDump,
5632
+ diagnosticMismatch
5633
+ )})`
5444
5634
  );
5445
5635
  decision = "history_mismatch_new_head";
5446
5636
  } else if (partitionKey) {
@@ -5500,7 +5690,7 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
5500
5690
  ttlPausedMs: entry.ttlPausedMs,
5501
5691
  idleMs: Math.max(0, now - entry.lastUsedAt),
5502
5692
  promptChanges: changedPromptFields(entry.promptFieldHashes, promptFieldHashes),
5503
- mismatch: continuationMismatchDetails(entry, payload, debug)
5693
+ mismatch: candidateMismatchDetails?.get(entry) ?? continuationMismatchDetails(entry, payload, debug)
5504
5694
  })),
5505
5695
  evictions
5506
5696
  }, diagnosticCorrelation);
@@ -5520,6 +5710,7 @@ function createResponsesWebSocketFetch(wsUrl, log12, options = {}) {
5520
5710
  frameCount: 0,
5521
5711
  pendingEvents: [],
5522
5712
  emittedModelData: false,
5713
+ emittedDownstreamData: false,
5523
5714
  transportRetryPending: false,
5524
5715
  outputByIndex: /* @__PURE__ */ new Map(),
5525
5716
  outputIndexByItemId: /* @__PURE__ */ new Map(),
@@ -5698,6 +5889,63 @@ function isCredentialBearingHeader(name) {
5698
5889
  return CREDENTIAL_BEARING_HEADER.test(name);
5699
5890
  }
5700
5891
 
5892
+ // src/model-runtime-compatibility.ts
5893
+ function remapMaxTokensField(body, field) {
5894
+ if (field === "max_tokens") {
5895
+ if (body.max_tokens === void 0 && body.max_completion_tokens !== void 0) {
5896
+ body.max_tokens = body.max_completion_tokens;
5897
+ }
5898
+ delete body.max_completion_tokens;
5899
+ return;
5900
+ }
5901
+ if (field === "max_completion_tokens") {
5902
+ if (body.max_completion_tokens === void 0 && body.max_tokens !== void 0) {
5903
+ body.max_completion_tokens = body.max_tokens;
5904
+ }
5905
+ delete body.max_tokens;
5906
+ }
5907
+ }
5908
+ function transformMessages(messages, compatibility) {
5909
+ if (!Array.isArray(messages)) return messages;
5910
+ const rewriteDeveloper = compatibility.supportsDeveloperRole === false;
5911
+ const replayReasoning = compatibility.requiresReasoningContentOnAssistantMessages === true;
5912
+ if (!rewriteDeveloper && !replayReasoning) return messages;
5913
+ let changed = false;
5914
+ const transformed = messages.map((message) => {
5915
+ if (!message || typeof message !== "object" || Array.isArray(message)) return message;
5916
+ const source = message;
5917
+ const role = source.role;
5918
+ const needsRoleRewrite = rewriteDeveloper && role === "developer";
5919
+ const needsReasoningReplay = replayReasoning && role === "assistant" && !Object.prototype.hasOwnProperty.call(source, "reasoning_content");
5920
+ if (!needsRoleRewrite && !needsReasoningReplay) return message;
5921
+ changed = true;
5922
+ return {
5923
+ ...source,
5924
+ ...needsRoleRewrite ? { role: "system" } : {},
5925
+ ...needsReasoningReplay ? { reasoning_content: "" } : {}
5926
+ };
5927
+ });
5928
+ return changed ? transformed : messages;
5929
+ }
5930
+ function transformOpenAiCompatibleRequestBody(body, compatibility) {
5931
+ const transformed = { ...body };
5932
+ if (compatibility.supportsStore === false) delete transformed.store;
5933
+ if (compatibility.supportsLongCacheRetention === false) {
5934
+ delete transformed.prompt_cache_retention;
5935
+ delete transformed.promptCacheRetention;
5936
+ }
5937
+ remapMaxTokensField(transformed, compatibility.maxTokensField);
5938
+ const messages = transformMessages(transformed.messages, compatibility);
5939
+ if (messages !== transformed.messages) transformed.messages = messages;
5940
+ const hasReasoningEffort = typeof transformed.reasoning_effort === "string" && transformed.reasoning_effort.trim().length > 0;
5941
+ if (hasReasoningEffort && compatibility.thinkingFormat === "deepseek") {
5942
+ if (transformed.thinking === void 0) transformed.thinking = { type: "enabled" };
5943
+ } else if (hasReasoningEffort && compatibility.thinkingFormat === "qwen") {
5944
+ if (transformed.enable_thinking === void 0) transformed.enable_thinking = true;
5945
+ }
5946
+ return transformed;
5947
+ }
5948
+
5701
5949
  // src/provider-factory.ts
5702
5950
  var RESPONSES_ONLY_PREFIXES = [
5703
5951
  "gpt-5-codex",
@@ -5840,7 +6088,10 @@ async function createLanguageModel(spec) {
5840
6088
  baseURL: baseURL ?? "",
5841
6089
  ...spec.authType !== "none" && apiKey.trim() ? { apiKey } : {},
5842
6090
  ...spec.authType === "none" ? { fetch: fetchWithoutCredentialHeaders } : {},
5843
- ...spec.headers ? { headers: spec.headers } : {}
6091
+ ...spec.headers ? { headers: spec.headers } : {},
6092
+ ...spec.compatibility ? {
6093
+ transformRequestBody: (body) => transformOpenAiCompatibleRequestBody(body, spec.compatibility)
6094
+ } : {}
5844
6095
  };
5845
6096
  model = createOpenAICompatible({
5846
6097
  ...options
@@ -6096,8 +6347,68 @@ function mapCodexEffortToGeminiBudget(effort) {
6096
6347
  if (!level) return void 0;
6097
6348
  return GEMINI_25_BUDGETS[level];
6098
6349
  }
6350
+ function compatibilityReasoningCapabilities(metadata) {
6351
+ const compatibility = metadata?.compatibility;
6352
+ if (!compatibility) return void 0;
6353
+ if (compatibility.supportsReasoningEffort === false) {
6354
+ return metadata?.reasoning === false ? EMPTY_REASONING : {
6355
+ ...EMPTY_REASONING,
6356
+ mode: "internal-only",
6357
+ source: "provider-metadata",
6358
+ confidence: "documented"
6359
+ };
6360
+ }
6361
+ if (compatibility.reasoningEffortMap) {
6362
+ const levels = Object.entries(compatibility.reasoningEffortMap).filter(([, mapped]) => mapped !== null).map(([level]) => level);
6363
+ if (levels.length === 0) {
6364
+ return metadata?.reasoning === false ? EMPTY_REASONING : {
6365
+ ...EMPTY_REASONING,
6366
+ mode: "internal-only",
6367
+ source: "provider-metadata",
6368
+ confidence: "documented"
6369
+ };
6370
+ }
6371
+ const preferredDefault = ["medium", "high", "max", "low"].find((level) => levels.includes(level));
6372
+ return {
6373
+ levels,
6374
+ defaultLevel: preferredDefault ?? levels[0],
6375
+ supportsSummaries: false,
6376
+ mode: "controllable",
6377
+ source: "provider-metadata",
6378
+ confidence: "documented",
6379
+ wireFormat: compatibility.thinkingFormat === "deepseek" ? { kind: "deepseek-thinking" } : { kind: "openai-reasoning-effort" }
6380
+ };
6381
+ }
6382
+ if (compatibility.supportsReasoningEffort === true || compatibility.thinkingFormat !== void 0) {
6383
+ if (metadata?.reasoning === false) return EMPTY_REASONING;
6384
+ return {
6385
+ levels: ["low", "medium", "high"],
6386
+ defaultLevel: "medium",
6387
+ supportsSummaries: false,
6388
+ mode: "controllable",
6389
+ source: "provider-metadata",
6390
+ confidence: "documented",
6391
+ wireFormat: compatibility.thinkingFormat === "deepseek" ? { kind: "deepseek-thinking" } : { kind: "openai-reasoning-effort" }
6392
+ };
6393
+ }
6394
+ return void 0;
6395
+ }
6396
+ function compatibilityReasoningEffort(effort, modelId, compatibility) {
6397
+ if (compatibility.supportsReasoningEffort === false) return void 0;
6398
+ const map = compatibility.reasoningEffortMap;
6399
+ if (map) {
6400
+ if (!Object.prototype.hasOwnProperty.call(map, effort)) return void 0;
6401
+ return map[effort] ?? void 0;
6402
+ }
6403
+ if (compatibility.supportsReasoningEffort === true || compatibility.thinkingFormat !== void 0) {
6404
+ return mapCodexEffortToOpenAI(effort, modelId);
6405
+ }
6406
+ return void 0;
6407
+ }
6099
6408
  function getReasoningCapabilities(npm, modelId, metadata) {
6100
6409
  const id = modelId.toLowerCase();
6410
+ const compatibilityCapabilities = compatibilityReasoningCapabilities(metadata);
6411
+ if (compatibilityCapabilities) return compatibilityCapabilities;
6101
6412
  if (isOpenRouterRoute(npm, metadata)) {
6102
6413
  return openRouterReasoningCapabilities(metadata);
6103
6414
  }
@@ -6243,7 +6554,10 @@ function getReasoningCapabilities(npm, modelId, metadata) {
6243
6554
  return EMPTY_REASONING;
6244
6555
  }
6245
6556
  function getPatchReasoningCapabilities(npm, modelId, metadata) {
6246
- if (metadata?.reasoning === false && !hasSupportedParameter(metadata, "reasoning_effort") && !hasSupportedParameter(metadata, "reasoning")) {
6557
+ if (metadata?.compatibility?.supportsReasoningEffort === false) {
6558
+ return compatibilityReasoningCapabilities(metadata) ?? EMPTY_REASONING;
6559
+ }
6560
+ if (metadata?.reasoning === false && !metadata?.compatibility?.reasoningEffortMap && !hasSupportedParameter(metadata, "reasoning_effort") && !hasSupportedParameter(metadata, "reasoning")) {
6247
6561
  return EMPTY_REASONING;
6248
6562
  }
6249
6563
  const capabilities = getReasoningCapabilities(npm, modelId, metadata);
@@ -6262,11 +6576,22 @@ function getPatchReasoningCapabilities(npm, modelId, metadata) {
6262
6576
  }
6263
6577
  function effortProviderOptions(npm, effort, modelId, metadata) {
6264
6578
  if (!effort) return void 0;
6579
+ if (npm === "@ai-sdk/openai-compatible" && modelId && metadata?.compatibility) {
6580
+ const reasoningEffort = compatibilityReasoningEffort(
6581
+ effort,
6582
+ modelId,
6583
+ metadata.compatibility
6584
+ );
6585
+ if (!reasoningEffort) return void 0;
6586
+ const key = metadata.providerId ? toCamelCase(metadata.providerId) : "openaiCompatible";
6587
+ return { [key]: { reasoningEffort } };
6588
+ }
6265
6589
  if (isOpenRouterRoute(npm, metadata)) {
6266
6590
  const caps = openRouterReasoningCapabilities(metadata);
6267
6591
  if (caps.mode !== "controllable") return void 0;
6268
6592
  const allowed = new Set(OPENROUTER_EFFORT_LEVELS);
6269
- const mapped = allowed.has(effort) ? effort : effort === "max" ? "xhigh" : void 0;
6593
+ const candidate = effort;
6594
+ const mapped = allowed.has(candidate) ? candidate : candidate === "max" ? "xhigh" : void 0;
6270
6595
  return mapped ? { openrouter: { reasoning: { effort: mapped, exclude: false } } } : void 0;
6271
6596
  }
6272
6597
  if (npm === "@ai-sdk/openai" || npm === "@ai-sdk/azure") {
@@ -6326,7 +6651,8 @@ function effortProviderOptions(npm, effort, modelId, metadata) {
6326
6651
  }
6327
6652
  if (hasSupportedParameter(metadata, "reasoning")) {
6328
6653
  const allowed = new Set(OPENROUTER_EFFORT_LEVELS);
6329
- const mapped = allowed.has(effort) ? effort : effort === "max" ? "xhigh" : void 0;
6654
+ const candidate = effort;
6655
+ const mapped = allowed.has(candidate) ? candidate : candidate === "max" ? "xhigh" : void 0;
6330
6656
  return mapped ? { openrouter: { reasoning: { effort: mapped, exclude: false } } } : void 0;
6331
6657
  }
6332
6658
  return void 0;
@@ -7136,6 +7462,62 @@ function parseModelList(body, npm) {
7136
7462
  }
7137
7463
  return models;
7138
7464
  }
7465
+ function materializeTemplateModel(template, model, baseUrl) {
7466
+ const npm = model.npm ?? template.npm;
7467
+ const { id, upstreamModelId: normalizedUpstream } = normalizeGoogleModelId(model.id, npm);
7468
+ const family = model.family ?? (id.split(/[-/:]/)[0] ?? id);
7469
+ const freeStatus = model.freeStatus ?? classifyFreeStatus({ model });
7470
+ return {
7471
+ ...model,
7472
+ id,
7473
+ name: normalizeGoogleDisplayName(model.name, id),
7474
+ upstreamModelId: model.upstreamModelId ?? normalizedUpstream,
7475
+ family,
7476
+ brand: model.brand ?? deriveBrand(family),
7477
+ contextWindow: model.contextWindow ?? resolveContextWindow(id),
7478
+ isFree: model.isFree ?? isFreeStatus(freeStatus),
7479
+ freeStatus,
7480
+ modelFormat: model.modelFormat ?? modelFormatForNpm(npm),
7481
+ npm,
7482
+ apiUrl: model.apiUrl ?? baseUrl
7483
+ };
7484
+ }
7485
+ function normalizeTemplateOverlay(template, model) {
7486
+ const npm = model.npm ?? template.npm;
7487
+ const { id } = normalizeGoogleModelId(model.id, npm);
7488
+ const family = model.family;
7489
+ const hasFreeMetadata = model.cost !== void 0 || model.isFree !== void 0 || model.freeStatus !== void 0;
7490
+ const freeStatus = hasFreeMetadata ? model.freeStatus ?? classifyFreeStatus({ model }) : void 0;
7491
+ return {
7492
+ ...model,
7493
+ id,
7494
+ name: normalizeGoogleDisplayName(model.name, id),
7495
+ ...model.upstreamModelId !== void 0 ? { upstreamModelId: normalizeGoogleModelId(model.upstreamModelId, npm).upstreamModelId } : {},
7496
+ ...model.npm !== void 0 ? { npm } : {},
7497
+ ...model.modelFormat !== void 0 ? { modelFormat: model.modelFormat } : model.npm !== void 0 ? { modelFormat: modelFormatForNpm(npm) } : {},
7498
+ ...family !== void 0 ? { family, brand: model.brand ?? deriveBrand(family) } : {},
7499
+ ...freeStatus !== void 0 ? {
7500
+ freeStatus,
7501
+ isFree: model.isFree ?? isFreeStatus(freeStatus)
7502
+ } : {}
7503
+ };
7504
+ }
7505
+ function applyTemplateModelMetadata(template, discovered, _baseUrl) {
7506
+ const curated = new Map(
7507
+ (template.staticModels ?? []).map((model) => normalizeTemplateOverlay(template, model)).map((model) => [model.id, model])
7508
+ );
7509
+ const visible = template.staticModelPolicy === "allowlist" ? discovered.filter((model) => curated.has(model.id)) : discovered;
7510
+ return visible.map((model) => {
7511
+ const overlay = curated.get(model.id);
7512
+ if (!overlay) return model;
7513
+ return {
7514
+ ...model,
7515
+ ...overlay,
7516
+ id: model.id,
7517
+ upstreamModelId: overlay.upstreamModelId ?? model.upstreamModelId
7518
+ };
7519
+ });
7520
+ }
7139
7521
  async function fetchTemplateModels(template, apiKey, baseUrlOverride, extraHeaders) {
7140
7522
  const trimmedOverride = baseUrlOverride?.trim();
7141
7523
  const baseUrl = (trimmedOverride || template.defaultBaseUrl)?.replace(/\/$/, "");
@@ -7147,19 +7529,7 @@ async function fetchTemplateModels(template, apiKey, baseUrlOverride, extraHeade
7147
7529
  };
7148
7530
  }
7149
7531
  if (template.modelSource === "static-seed") {
7150
- const models = (template.staticModels || []).map((sm) => {
7151
- const family = sm.id.split(/[-/:]/)[0] ?? sm.id;
7152
- return {
7153
- id: sm.id,
7154
- name: sm.name,
7155
- upstreamModelId: sm.id,
7156
- family,
7157
- brand: deriveBrand(family),
7158
- contextWindow: resolveContextWindow(sm.id),
7159
- modelFormat: modelFormatForNpm(template.npm),
7160
- npm: template.npm
7161
- };
7162
- });
7532
+ const models = (template.staticModels ?? []).map((model) => materializeTemplateModel(template, model, baseUrl));
7163
7533
  return { models, baseUrl };
7164
7534
  }
7165
7535
  const url = modelsUrl(baseUrl, template);
@@ -7229,7 +7599,7 @@ async function fetchTemplateModels(template, apiKey, baseUrlOverride, extraHeade
7229
7599
  }
7230
7600
  } catch {
7231
7601
  }
7232
- const models = parseModelList(json, template.npm);
7602
+ const models = applyTemplateModelMetadata(template, parseModelList(json, template.npm), baseUrl);
7233
7603
  if (models.length === 0) {
7234
7604
  return {
7235
7605
  models: [],
@@ -7317,11 +7687,11 @@ async function addProviderFromTemplate(template, apiKey, opts) {
7317
7687
  }
7318
7688
  const pricingCache = loadPricingCache();
7319
7689
  const platform = pricingPlatformForProvider(template.id, template.id);
7320
- const pricedModels = enrichModelsWithPricing(
7321
- usableModels.map((m) => ({ ...m, apiUrl: fetched.baseUrl })),
7322
- buildPricingIndex(pricingCache),
7323
- platform
7324
- );
7690
+ const discoveredModels = usableModels.map((m) => ({
7691
+ ...m,
7692
+ apiUrl: m.apiUrl ?? fetched.baseUrl
7693
+ }));
7694
+ const pricedModels = template.preserveModelPricing ? discoveredModels : enrichModelsWithPricing(discoveredModels, buildPricingIndex(pricingCache), platform);
7325
7695
  const account = `provider:${template.id}`;
7326
7696
  const result = await withProviderMutationLock(template.id, async () => {
7327
7697
  const currentState = await withRegistryWriteLock(() => {
@@ -7378,6 +7748,7 @@ async function addProviderFromTemplate(template, apiKey, opts) {
7378
7748
  enabled: true,
7379
7749
  authRef,
7380
7750
  authType: trimmedKey ? template.authType : "none",
7751
+ ...template.preserveModelPricing ? { preserveModelPricing: true } : {},
7381
7752
  ...!trimmedKey && template.anonymousFreeModels ? { subscriptionFilter: "free" } : {},
7382
7753
  api: {
7383
7754
  npm: template.npm,
@@ -7980,7 +8351,7 @@ async function refreshApiListProvider(provider, apiKey) {
7980
8351
  return { models: [], error: fetched2.error ?? "No models returned.", baseUrl: fetched2.baseUrl };
7981
8352
  }
7982
8353
  return {
7983
- models: fetched2.models.map((m) => ({ ...m, apiUrl: fetched2.baseUrl })),
8354
+ models: fetched2.models.map((m) => ({ ...m, apiUrl: m.apiUrl ?? fetched2.baseUrl })),
7984
8355
  baseUrl: fetched2.baseUrl
7985
8356
  };
7986
8357
  }
@@ -7999,7 +8370,7 @@ async function refreshApiListProvider(provider, apiKey) {
7999
8370
  return {
8000
8371
  models: usableModels.map((m) => ({
8001
8372
  ...m,
8002
- apiUrl: fetched.baseUrl
8373
+ apiUrl: m.apiUrl ?? fetched.baseUrl
8003
8374
  })),
8004
8375
  baseUrl: fetched.baseUrl
8005
8376
  };
@@ -8113,7 +8484,7 @@ async function refreshProviderModels(providerId, apiKey, registry) {
8113
8484
  }
8114
8485
  const pricingCache = loadPricingCache();
8115
8486
  const platform = pricingPlatformForProvider(provider.templateId, provider.id);
8116
- const enriched = enrichModelsWithPricing(models, buildPricingIndex(pricingCache), platform);
8487
+ const enriched = provider.preserveModelPricing ? models : enrichModelsWithPricing(models, buildPricingIndex(pricingCache), platform);
8117
8488
  await withRegistryWriteLock(() => {
8118
8489
  const currentRegistry = loadRegistryStrict();
8119
8490
  const currentProvider = currentRegistry.providers.find((candidate) => candidate.id === providerId);
@@ -9355,7 +9726,8 @@ function localModelToRoute(lp, model) {
9355
9726
  reasoning: model.reasoning,
9356
9727
  interleavedReasoningField: model.interleavedReasoningField,
9357
9728
  useResponsesLite: model.useResponsesLite,
9358
- preferWebSockets: model.preferWebSockets
9729
+ preferWebSockets: model.preferWebSockets,
9730
+ compatibility: model.compatibility
9359
9731
  };
9360
9732
  }
9361
9733
  function makeRouteResolver(localProviders) {
@@ -9428,7 +9800,8 @@ function buildHttpProxyRoutes(providers, favorites, modelAliases = void 0, max =
9428
9800
  unavailable.push(favorite);
9429
9801
  continue;
9430
9802
  }
9431
- if (model.modelFormat !== "openai" || !isSdkMigratedNpm(model.npm)) {
9803
+ const supported = model.modelFormat === "anthropic" ? Boolean(model.baseUrl) : isSdkMigratedNpm(model.npm);
9804
+ if (!supported) {
9432
9805
  unsupported.push(favorite);
9433
9806
  continue;
9434
9807
  }
@@ -9601,7 +9974,8 @@ function routeUnavailableMessage(modelId, reason) {
9601
9974
  }
9602
9975
 
9603
9976
  // src/upstream-forward.ts
9604
- import { Readable } from "stream";
9977
+ import { Readable, Transform } from "stream";
9978
+ import { StringDecoder } from "string_decoder";
9605
9979
 
9606
9980
  // src/server/auth.ts
9607
9981
  function sanitizeCredential(value) {
@@ -9682,6 +10056,34 @@ async function fetchWithOAuthRetry(apiKey, request3, refreshToken) {
9682
10056
  response = await request3(refreshed);
9683
10057
  return { response, apiKey: refreshed, refreshed: true };
9684
10058
  }
10059
+ function anthropicSseModelRewrite(override) {
10060
+ const decoder = new StringDecoder("utf8");
10061
+ let tail = "";
10062
+ const rewriteLine = (line) => {
10063
+ if (!line.startsWith("data:") || !line.includes('"message_start"')) return line;
10064
+ try {
10065
+ const parsed = JSON.parse(line.slice(5));
10066
+ if (parsed.type === "message_start" && parsed.message && typeof parsed.message.model === "string") {
10067
+ parsed.message.model = override;
10068
+ return "data: " + JSON.stringify(parsed);
10069
+ }
10070
+ } catch {
10071
+ }
10072
+ return line;
10073
+ };
10074
+ return new Transform({
10075
+ transform(chunk, _encoding, callback) {
10076
+ const lines = (tail + decoder.write(chunk)).split("\n");
10077
+ tail = lines.pop() ?? "";
10078
+ const rewritten = lines.map(rewriteLine);
10079
+ callback(null, rewritten.length ? rewritten.join("\n") + "\n" : "");
10080
+ },
10081
+ flush(callback) {
10082
+ const rest = tail + decoder.end();
10083
+ callback(null, rest ? rewriteLine(rest) : "");
10084
+ }
10085
+ });
10086
+ }
9685
10087
  async function relayAnthropicMessages(res, messagesUrl, body, apiKey, clientWantsStream, options = {}) {
9686
10088
  const doFetch = (key) => fetch(messagesUrl, {
9687
10089
  method: "POST",
@@ -9718,7 +10120,12 @@ async function relayAnthropicMessages(res, messagesUrl, body, apiKey, clientWant
9718
10120
  "Cache-Control": "no-cache",
9719
10121
  "Connection": "keep-alive"
9720
10122
  });
9721
- Readable.fromWeb(upstreamRes.body).on("error", () => res.destroy()).pipe(res);
10123
+ const upstream = Readable.fromWeb(upstreamRes.body).on("error", () => res.destroy());
10124
+ if (options.responseModelOverride) {
10125
+ upstream.pipe(anthropicSseModelRewrite(options.responseModelOverride)).on("error", () => res.destroy()).pipe(res);
10126
+ } else {
10127
+ upstream.pipe(res);
10128
+ }
9722
10129
  return;
9723
10130
  }
9724
10131
  if (!upstreamRes.body) {
@@ -9726,14 +10133,19 @@ async function relayAnthropicMessages(res, messagesUrl, body, apiKey, clientWant
9726
10133
  res.end(JSON.stringify({ type: "error", error: { type: "api_error", message: "Upstream returned empty response body" } }));
9727
10134
  return;
9728
10135
  }
9729
- const text4 = await upstreamRes.text();
10136
+ let text4 = await upstreamRes.text();
10137
+ let parsed;
9730
10138
  try {
9731
- JSON.parse(text4);
10139
+ parsed = JSON.parse(text4);
9732
10140
  } catch {
9733
10141
  res.writeHead(502, { "Content-Type": "application/json" });
9734
10142
  res.end(JSON.stringify({ type: "error", error: { type: "api_error", message: "Upstream response was not valid JSON" } }));
9735
10143
  return;
9736
10144
  }
10145
+ if (options.responseModelOverride && parsed && typeof parsed === "object" && !Array.isArray(parsed) && typeof parsed.model === "string") {
10146
+ parsed.model = options.responseModelOverride;
10147
+ text4 = JSON.stringify(parsed);
10148
+ }
9737
10149
  res.writeHead(200, {
9738
10150
  "Content-Type": "application/json",
9739
10151
  "Content-Length": Buffer.byteLength(text4).toString()
@@ -9882,6 +10294,41 @@ function resolveUpstreamTools(tools, messages) {
9882
10294
  return upstream;
9883
10295
  }
9884
10296
 
10297
+ // src/upstream-retry.ts
10298
+ var UPSTREAM_MAX_RETRIES_ENV = "CLODEX_UPSTREAM_MAX_RETRIES";
10299
+ var MAX_UPSTREAM_MAX_RETRIES = 5;
10300
+ var reportedValues = /* @__PURE__ */ new Set();
10301
+ function reportOnce(raw, message, warn) {
10302
+ if (reportedValues.has(raw)) return;
10303
+ reportedValues.add(raw);
10304
+ try {
10305
+ warn(message);
10306
+ } catch {
10307
+ }
10308
+ }
10309
+ function upstreamMaxRetries(env = process.env, warn = (message) => console.error(`clodex: ${message}`)) {
10310
+ const raw = env[UPSTREAM_MAX_RETRIES_ENV]?.trim();
10311
+ if (raw === void 0 || raw === "") return void 0;
10312
+ const value = Number(raw);
10313
+ if (!Number.isInteger(value) || value < 0) {
10314
+ reportOnce(
10315
+ raw,
10316
+ `ignoring ${UPSTREAM_MAX_RETRIES_ENV}=${raw} (expected a non-negative integer)`,
10317
+ warn
10318
+ );
10319
+ return void 0;
10320
+ }
10321
+ if (value > MAX_UPSTREAM_MAX_RETRIES) {
10322
+ reportOnce(
10323
+ raw,
10324
+ `clamping ${UPSTREAM_MAX_RETRIES_ENV}=${raw} to ${MAX_UPSTREAM_MAX_RETRIES} (higher values exceed the 120s streaming idle budget)`,
10325
+ warn
10326
+ );
10327
+ return MAX_UPSTREAM_MAX_RETRIES;
10328
+ }
10329
+ return value;
10330
+ }
10331
+
9885
10332
  // src/sdk-adapter.ts
9886
10333
  function sdkTranslationErrorSignature(error) {
9887
10334
  const message = error instanceof Error ? error.message : typeof error === "string" ? error : void 0;
@@ -9952,10 +10399,12 @@ function openAiCacheBreakpoint(block, enabled) {
9952
10399
  function translateTopLevelSystemForOpenAi(system) {
9953
10400
  if (!system) return [];
9954
10401
  if (typeof system === "string") {
9955
- return system.trim() ? [{ role: "system", content: system }] : [];
10402
+ const stripped = stripClaudeCodeBillingHeader(system);
10403
+ return stripped?.trim() ? [{ role: "system", content: stripped }] : [];
9956
10404
  }
9957
10405
  return system.flatMap((block) => {
9958
- const text4 = typeof block === "string" ? block : block.text ?? "";
10406
+ const raw = typeof block === "string" ? block : block.text ?? "";
10407
+ const text4 = stripClaudeCodeBillingHeader(raw) ?? "";
9959
10408
  if (!text4.trim()) return [];
9960
10409
  const cacheControl = typeof block === "string" ? void 0 : block.cache_control;
9961
10410
  return [{
@@ -10141,7 +10590,7 @@ function isClaudeCodeStructuredOutputCompactRequest(body) {
10141
10590
  function translateRequest(body, npm, options) {
10142
10591
  const messages = body.messages ?? [];
10143
10592
  annotateToolNames(messages);
10144
- const baseSystem = systemToString(body.system, options?.openAiOAuth === true);
10593
+ const baseSystem = systemToString(body.system, true);
10145
10594
  const systemText = baseSystem?.trim() || (options?.openAiOAuth ? "You are a coding assistant." : void 0);
10146
10595
  const compactRequest = isClaudeCodeStructuredOutputCompactRequest(body);
10147
10596
  let upstreamTools = resolveUpstreamTools(
@@ -10432,6 +10881,7 @@ async function streamAnthropicResponse(model, params, modelId, write, log12, obs
10432
10881
  const result = streamText({
10433
10882
  model,
10434
10883
  ...params,
10884
+ maxRetries: upstreamMaxRetries(),
10435
10885
  abortSignal,
10436
10886
  onError: () => {
10437
10887
  }
@@ -10480,6 +10930,7 @@ async function generateAnthropicResponse(model, params, modelId, options) {
10480
10930
  const r = streamText({
10481
10931
  model,
10482
10932
  ...params,
10933
+ maxRetries: upstreamMaxRetries(),
10483
10934
  abortSignal,
10484
10935
  onError: () => {
10485
10936
  }
@@ -10536,6 +10987,7 @@ async function generateAnthropicResponse(model, params, modelId, options) {
10536
10987
  const r = await generateText({
10537
10988
  model,
10538
10989
  ...params,
10990
+ maxRetries: upstreamMaxRetries(),
10539
10991
  abortSignal: generateAbort.signal
10540
10992
  });
10541
10993
  ({ text: text4, toolCalls, finishReason, usage } = r);
@@ -10955,6 +11407,10 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
10955
11407
  route.apiKey = refreshed;
10956
11408
  },
10957
11409
  signal: clientAbort.signal,
11410
+ // A route selected through a clodex: id or short alias must echo the
11411
+ // exact requested id back, or patched Claude Code misses the alias
11412
+ // context-window key and can skip auto-compaction.
11413
+ responseModelOverride: typeof originalModel === "string" && originalModel !== route.realModelId ? originalModel : void 0,
10958
11414
  onUpstreamError: inferenceLogPath ? (statusCode, errorContent) => writeInferenceResponseErrorLog(inferenceLogPath, {
10959
11415
  modelId: originalModel,
10960
11416
  provider: route.providerId ?? route.aliasId.split(":")[1] ?? "unknown",
@@ -10993,6 +11449,7 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
10993
11449
  supportedParameters: route.supportedParameters,
10994
11450
  reasoning: route.reasoning,
10995
11451
  interleavedReasoningField: route.interleavedReasoningField,
11452
+ compatibility: route.compatibility,
10996
11453
  upstreamModelId: route.realModelId
10997
11454
  }
10998
11455
  });
@@ -11011,6 +11468,7 @@ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenc
11011
11468
  headers: route.headers,
11012
11469
  useResponsesLite: route.useResponsesLite,
11013
11470
  preferWebSockets: route.preferWebSockets,
11471
+ compatibility: route.compatibility,
11014
11472
  onDebug: (msg) => plog(() => msg),
11015
11473
  onWebSocketDiagnostic: webSocketDiagnosticsLogPath ? (event) => writeWebSocketDiagnosticLog(webSocketDiagnosticsLogPath, event) : void 0
11016
11474
  });
@@ -11220,6 +11678,7 @@ function startProxy(completionsUrl, modelId, debug = false, contextWindow, sdk,
11220
11678
  interleavedReasoningField: sdk?.interleavedReasoningField,
11221
11679
  useResponsesLite: sdk?.useResponsesLite,
11222
11680
  preferWebSockets: sdk?.preferWebSockets,
11681
+ compatibility: sdk?.compatibility,
11223
11682
  headers: sdk?.headers
11224
11683
  }], clientModelId, debug);
11225
11684
  }
@@ -11490,11 +11949,20 @@ async function collectOpenAiStream(stream) {
11490
11949
  async function generateOpenAiResponse(model, params, responseModelId, options) {
11491
11950
  let result;
11492
11951
  if (options?.forceStream) {
11493
- const { stream } = streamText2({ model, ...params, onError: () => {
11494
- } });
11952
+ const { stream } = streamText2({
11953
+ model,
11954
+ ...params,
11955
+ maxRetries: upstreamMaxRetries(),
11956
+ onError: () => {
11957
+ }
11958
+ });
11495
11959
  result = await collectOpenAiStream(stream);
11496
11960
  } else {
11497
- result = await generateText2({ model, ...params });
11961
+ result = await generateText2({
11962
+ model,
11963
+ ...params,
11964
+ maxRetries: upstreamMaxRetries()
11965
+ });
11498
11966
  }
11499
11967
  const message = { role: "assistant", content: result.text || null };
11500
11968
  if (result.toolCalls?.length) {
@@ -11518,7 +11986,11 @@ async function generateOpenAiResponse(model, params, responseModelId, options) {
11518
11986
  };
11519
11987
  }
11520
11988
  async function streamOpenAiResponse(model, params, responseModelId, onChunk) {
11521
- const { stream } = streamText2({ model, ...params });
11989
+ const { stream } = streamText2({
11990
+ model,
11991
+ ...params,
11992
+ maxRetries: upstreamMaxRetries()
11993
+ });
11522
11994
  const baseData = {
11523
11995
  id: `chatcmpl-${Date.now()}`,
11524
11996
  object: "chat.completion.chunk",
@@ -11762,6 +12234,9 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
11762
12234
  onTokenRefreshed: (refreshed) => {
11763
12235
  model.apiKey = refreshed;
11764
12236
  },
12237
+ // Echo the exact requested id when it differs from the upstream id, so
12238
+ // clients that key context windows on the response model still resolve.
12239
+ responseModelOverride: typeof body.model === "string" && body.model !== upstreamModelId(model) ? body.model : void 0,
11765
12240
  onUpstreamError: options.inferenceLogPath ? (statusCode, errorContent) => writeInferenceResponseErrorLog(options.inferenceLogPath, {
11766
12241
  requestId,
11767
12242
  modelId: body.model,
@@ -11812,6 +12287,7 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
11812
12287
  supportedParameters: model.supportedParameters,
11813
12288
  reasoning: model.reasoning,
11814
12289
  interleavedReasoningField: model.interleavedReasoningField,
12290
+ compatibility: model.compatibility,
11815
12291
  upstreamModelId: upstreamModelId(model)
11816
12292
  },
11817
12293
  maxTools: npmMaxTools
@@ -12092,6 +12568,7 @@ async function getOrInitLanguageModel(modelCache, model, npm, baseURL, apiKey, w
12092
12568
  headers: model.headers,
12093
12569
  useResponsesLite: model.useResponsesLite,
12094
12570
  preferWebSockets: model.preferWebSockets,
12571
+ compatibility: model.compatibility,
12095
12572
  onWebSocketDiagnostic: webSocketDiagnosticsLogPath ? (event) => writeWebSocketDiagnosticLog(webSocketDiagnosticsLogPath, event) : void 0
12096
12573
  });
12097
12574
  cached = { apiKey, languageModel };
@@ -13435,7 +13912,8 @@ function enrichServerModelReasoning(model) {
13435
13912
  apiBaseUrl: model.apiBaseUrl,
13436
13913
  supportedParameters: model.supportedParameters,
13437
13914
  reasoning: model.reasoning,
13438
- interleavedReasoningField: model.interleavedReasoningField
13915
+ interleavedReasoningField: model.interleavedReasoningField,
13916
+ compatibility: model.compatibility
13439
13917
  });
13440
13918
  if (!caps.defaultLevel) return model;
13441
13919
  return { ...model, defaultEffort: caps.defaultLevel };
@@ -14931,6 +15409,7 @@ function buildDesiredPatchConfig() {
14931
15409
  supportedParameters: model.supportedParameters,
14932
15410
  reasoning: model.reasoning ?? modelsDev?.reasoning,
14933
15411
  interleavedReasoningField: model.interleavedReasoningField ?? modelsDev?.interleaved?.field,
15412
+ compatibility: model.compatibility,
14934
15413
  upstreamModelId: upstreamModelId2
14935
15414
  });
14936
15415
  meta.set(`${provider.id}:${model.id}`, {
@@ -16570,6 +17049,7 @@ Error: ${launchPlan.error}
16570
17049
  interleavedReasoningField: selectedModel.interleavedReasoningField,
16571
17050
  useResponsesLite: selectedModel.useResponsesLite,
16572
17051
  preferWebSockets: selectedModel.preferWebSockets,
17052
+ compatibility: selectedModel.compatibility,
16573
17053
  headers: activeProvider.headers
16574
17054
  },
16575
17055
  launchApiKey ?? ""