@bman654/clodex 1.2.0 → 1.2.2

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/README.md CHANGED
@@ -219,8 +219,13 @@ clodex --version # version
219
219
  FAT, exFAT, or a network mount that rejects hard links. An abrupt process kill
220
220
  during lock publication can leave a `providers.json.lock.*.tmp` file; it does
221
221
  not block later lock acquisition and can be removed when no Clodex process is
222
- running.
222
+ running. A canonical `providers.json.lock` whose recorded PID is no longer
223
+ running is reclaimed automatically on the next lock acquisition. If it remains
224
+ while that PID is active, stop every Clodex process and verify the recorded PID
225
+ before removing the lock manually. Never remove the canonical lock while a
226
+ Clodex process is active.
223
227
  - Credentials live in the OS credential store (Keychain / Windows Credential Manager / Secret Service) under the `clodex` service. Set `CLODEX_CREDENTIAL_HELPER` to an absolute executable path to use an external secure store instead; see [credential helpers](docs/credential-helpers.md).
228
+ - Proxied routes forward configured provider headers for API-key and OAuth authentication. Anonymous routes preserve non-credential headers while removing authorization, API-key, cookie, token, secret, and credential-bearing header names before dispatch.
224
229
  - `CLODEX_CLAUDE_PATH` overrides Claude Code binary discovery.
225
230
  - **Outbound proxy:** when `HTTP_PROXY`/`HTTPS_PROXY` (and optionally `NO_PROXY`) are set in clodex's environment, all clodex-originated network calls honor them — OAuth sign-in and token refresh, model-list and models.dev refreshes, upstream OpenAI API calls, and the ChatGPT/Codex OAuth WebSocket transport (tunneled via HTTP CONNECT).
226
231
 
package/dist/cli.js CHANGED
@@ -202,7 +202,7 @@ import { join } from "path";
202
202
  // package.json
203
203
  var package_default = {
204
204
  name: "@bman654/clodex",
205
- version: "1.2.0",
205
+ version: "1.2.2",
206
206
  publishConfig: {
207
207
  access: "public"
208
208
  },
@@ -254,6 +254,7 @@ var package_default = {
254
254
  "https-proxy-agent": "9.1.0",
255
255
  "ipaddr.js": "2.4.0",
256
256
  "node-forge": "1.4.0",
257
+ "node-gyp-build": "4.8.4",
257
258
  open: "11.0.0",
258
259
  picocolors: "1.1.1",
259
260
  tweakcc: "4.3.0",
@@ -2730,13 +2731,12 @@ function providersForPicker(providers) {
2730
2731
  return providers.sort((a, b) => a.name.localeCompare(b.name, void 0, { sensitivity: "base", numeric: true }));
2731
2732
  }
2732
2733
  async function resolveLocalProviderApiKey(provider) {
2733
- if (provider.authRef === "none:anonymous") return "anonymous";
2734
+ if (provider.authRef === "none:anonymous" || provider.authType === "none") return "";
2734
2735
  const direct = provider.apiKey?.trim();
2735
2736
  if (direct) return direct;
2736
- if (provider.authType === "none") return "anonymous";
2737
2737
  const template = getTemplateById(provider.id);
2738
2738
  if (template?.apiKeyOptional || template?.anonymousFreeModels) {
2739
- return "anonymous";
2739
+ return "";
2740
2740
  }
2741
2741
  const reg = loadRegistry().providers.find((p13) => p13.id === provider.id);
2742
2742
  const authRef = provider.authRef ?? reg?.authRef ?? oauthAuthRef(provider.id);
@@ -4079,6 +4079,12 @@ function injectClaudeIdentity(body, providerData, seed) {
4079
4079
  return { sessionId, userId };
4080
4080
  }
4081
4081
 
4082
+ // src/credential-headers.ts
4083
+ var CREDENTIAL_BEARING_HEADER = /(?:^|[-_])(?:authorization|api[-_]?key|cookie|token|secret|credential)(?:$|[-_])/i;
4084
+ function isCredentialBearingHeader(name) {
4085
+ return CREDENTIAL_BEARING_HEADER.test(name);
4086
+ }
4087
+
4082
4088
  // src/provider-factory.ts
4083
4089
  var RESPONSES_ONLY_PREFIXES = [
4084
4090
  "gpt-5-codex",
@@ -4088,6 +4094,15 @@ var RESPONSES_ONLY_PREFIXES = [
4088
4094
  "o4"
4089
4095
  ];
4090
4096
  var factoryCache = /* @__PURE__ */ new Map();
4097
+ var fetchWithoutCredentialHeaders = (input, init) => {
4098
+ const headers = new Headers(
4099
+ init?.headers ?? (input instanceof Request ? input.headers : void 0)
4100
+ );
4101
+ for (const name of [...headers.keys()]) {
4102
+ if (isCredentialBearingHeader(name)) headers.delete(name);
4103
+ }
4104
+ return fetch(input, { ...init, headers });
4105
+ };
4091
4106
  function modelPrefersResponsesApi(modelId) {
4092
4107
  const lower = modelId.toLowerCase();
4093
4108
  if (RESPONSES_ONLY_PREFIXES.some((prefix) => lower === prefix || lower.startsWith(`${prefix}-`))) {
@@ -4152,6 +4167,7 @@ async function createLanguageModel(spec) {
4152
4167
  apiKey,
4153
4168
  baseURL: "https://chatgpt.com/backend-api/codex",
4154
4169
  headers: {
4170
+ ...spec.headers,
4155
4171
  ...accountId ? { "ChatGPT-Account-Id": accountId } : {},
4156
4172
  originator: "clodex",
4157
4173
  // Responses-Lite models (backend prefer_websockets/use_responses_lite,
@@ -4169,7 +4185,11 @@ async function createLanguageModel(spec) {
4169
4185
  onDiagnostic: spec.onWebSocketDiagnostic
4170
4186
  })
4171
4187
  } : {}
4172
- } : { apiKey };
4188
+ } : spec.authType === "none" ? {
4189
+ apiKey: "",
4190
+ ...spec.headers ? { headers: spec.headers } : {},
4191
+ fetch: fetchWithoutCredentialHeaders
4192
+ } : { apiKey, ...spec.headers ? { headers: spec.headers } : {} };
4173
4193
  const openai = createOpenAI(oauthOptions);
4174
4194
  return useResponsesEndpoint ? openai.responses(modelId) : openai.chat(modelId);
4175
4195
  }
@@ -4189,7 +4209,7 @@ async function createLanguageModel(spec) {
4189
4209
  ).sessionId
4190
4210
  }
4191
4211
  } : {}
4192
- } : { apiKey };
4212
+ } : spec.authType === "none" ? { apiKey: "", fetch: fetchWithoutCredentialHeaders } : { apiKey };
4193
4213
  if (spec.headers) {
4194
4214
  anthropicOptions.headers = { ...anthropicOptions.headers, ...spec.headers };
4195
4215
  }
@@ -4205,7 +4225,8 @@ async function createLanguageModel(spec) {
4205
4225
  const options = {
4206
4226
  name: spec.providerId ?? "openai-compatible",
4207
4227
  baseURL: baseURL ?? "",
4208
- ...apiKey.trim() ? { apiKey } : {},
4228
+ ...spec.authType !== "none" && apiKey.trim() ? { apiKey } : {},
4229
+ ...spec.authType === "none" ? { fetch: fetchWithoutCredentialHeaders } : {},
4209
4230
  ...spec.headers ? { headers: spec.headers } : {}
4210
4231
  };
4211
4232
  model = createOpenAICompatible({
@@ -4214,7 +4235,8 @@ async function createLanguageModel(spec) {
4214
4235
  } else {
4215
4236
  const create = await loadSdkProviderFactory(npm);
4216
4237
  const provider = create({
4217
- apiKey,
4238
+ apiKey: spec.authType === "none" ? "" : apiKey,
4239
+ ...spec.authType === "none" ? { fetch: fetchWithoutCredentialHeaders } : {},
4218
4240
  ...baseURL ? { baseURL } : {},
4219
4241
  ...spec.headers ? { headers: spec.headers } : {}
4220
4242
  });
@@ -4821,7 +4843,6 @@ function getLatestMessagePreview(messages, system) {
4821
4843
  return compactLogValue(preview, REQUEST_PREVIEW_MAX + 20);
4822
4844
  }
4823
4845
  var REDACTED_DIAGNOSTIC_HEADER = "[REDACTED]";
4824
- var SENSITIVE_DIAGNOSTIC_HEADER = /(?:^|[-_])(?:authorization|api[-_]?key|cookie|token|secret|credential)(?:$|[-_])/i;
4825
4846
  var CONVERSATION_BODY_FIELDS = /* @__PURE__ */ new Set(["system", "messages", "tools"]);
4826
4847
  function canonicalDiagnosticValue(value) {
4827
4848
  if (Array.isArray(value)) return value.map(canonicalDiagnosticValue);
@@ -4840,7 +4861,7 @@ function sanitizeDiagnosticHeaders(headers) {
4840
4861
  const out = {};
4841
4862
  for (const [name, value] of Object.entries(headers).sort(([left], [right]) => left.localeCompare(right))) {
4842
4863
  if (value === void 0) continue;
4843
- out[name.toLowerCase()] = SENSITIVE_DIAGNOSTIC_HEADER.test(name) ? REDACTED_DIAGNOSTIC_HEADER : value;
4864
+ out[name.toLowerCase()] = isCredentialBearingHeader(name) ? REDACTED_DIAGNOSTIC_HEADER : value;
4844
4865
  }
4845
4866
  return out;
4846
4867
  }
@@ -5396,6 +5417,9 @@ async function addProviderFromTemplate(template, apiKey, opts) {
5396
5417
  function credentialStillReferenced(authRef, remaining) {
5397
5418
  return remaining.some((p13) => p13.authRef === authRef);
5398
5419
  }
5420
+ function isStoredCredentialRef(authRef) {
5421
+ return authRef.startsWith("keyring:") || authRef.startsWith("helper:");
5422
+ }
5399
5423
  async function removeProviderFromRegistry(id, opts) {
5400
5424
  const removal = await withRegistryWriteLock(() => {
5401
5425
  const registry = loadRegistry();
@@ -5420,7 +5444,7 @@ async function removeProviderFromRegistry(id, opts) {
5420
5444
  name: removedProvider.name,
5421
5445
  credentialDeleted: false
5422
5446
  },
5423
- authRefToDelete: opts?.deleteCredential !== false && !credentialStillReferenced(removedProvider.authRef, registry.providers) ? removedProvider.authRef : null
5447
+ authRefToDelete: opts?.deleteCredential !== false && isStoredCredentialRef(removedProvider.authRef) && !credentialStillReferenced(removedProvider.authRef, registry.providers) ? removedProvider.authRef : null
5424
5448
  };
5425
5449
  });
5426
5450
  const authRefToDelete = removal.authRefToDelete;
@@ -6214,16 +6238,18 @@ async function authenticateProvider(providerId, _options = {}) {
6214
6238
  nativeDiagMsg = msg;
6215
6239
  }
6216
6240
  );
6241
+ if (!saved) {
6242
+ throw new Error(
6243
+ `Could not save OAuth tokens to the credential store${nativeDiagMsg ? ` \u2014 ${nativeDiagMsg}` : " \u2014 check access and try again"}`
6244
+ );
6245
+ }
6217
6246
  const registryProvider2 = await upsertOAuthProvider(
6218
6247
  providerId,
6219
6248
  cred,
6220
6249
  authRef
6221
6250
  );
6222
- return { saved, nativeDiagMsg, registryProvider: registryProvider2 };
6251
+ return { registryProvider: registryProvider2 };
6223
6252
  });
6224
- if (!persisted.saved) {
6225
- p2.log.warn(`Could not save OAuth tokens to the credential store \u2014 ${persisted.nativeDiagMsg || "session may not persist."}`);
6226
- }
6227
6253
  const { registryProvider } = persisted;
6228
6254
  const refreshSpinner = p2.spinner();
6229
6255
  refreshSpinner.start("Refreshing model list...");
@@ -7137,7 +7163,7 @@ function buildHttpProxyRoutes(providers, favorites, modelAliases = [], max = MAX
7137
7163
  continue;
7138
7164
  }
7139
7165
  const route = localModelToRoute(provider, model);
7140
- if (!route || !route.apiKey.trim()) {
7166
+ if (!route || !route.apiKey.trim() && route.authType !== "none") {
7141
7167
  unavailable.push(favorite);
7142
7168
  continue;
7143
7169
  }
@@ -7318,13 +7344,21 @@ function extractBearerToken(value) {
7318
7344
  // src/upstream-forward.ts
7319
7345
  function anthropicUpstreamHeaders(apiKey, stream = false, inboundBeta, authType, claudeCodeSessionId, extraHeaders) {
7320
7346
  const key = sanitizeCredential(apiKey) ?? apiKey.trim();
7321
- const isOAuth = authType === "oauth";
7347
+ const resolvedAuthType = authType ?? "api";
7348
+ const isOAuth = resolvedAuthType === "oauth";
7349
+ const forwardedExtraHeaders = resolvedAuthType === "none" ? Object.fromEntries(
7350
+ Object.entries(extraHeaders ?? {}).filter(
7351
+ ([name]) => !isCredentialBearingHeader(name)
7352
+ )
7353
+ ) : extraHeaders;
7322
7354
  const headers = {
7323
- ...extraHeaders,
7355
+ ...forwardedExtraHeaders,
7324
7356
  "Content-Type": "application/json",
7325
7357
  "anthropic-version": "2023-06-01",
7326
- Authorization: `Bearer ${key}`,
7327
- ...isOAuth ? {} : { "x-api-key": key },
7358
+ ...resolvedAuthType === "none" ? {} : {
7359
+ Authorization: `Bearer ${key}`,
7360
+ ...isOAuth ? {} : { "x-api-key": key }
7361
+ },
7328
7362
  ...isOAuth ? { "User-Agent": CLAUDE_CODE_USER_AGENT, "x-app": "cli" } : {},
7329
7363
  ...isOAuth && claudeCodeSessionId ? { "X-Claude-Code-Session-Id": claudeCodeSessionId } : {},
7330
7364
  ...stream ? { Accept: "text/event-stream" } : {}
@@ -8296,6 +8330,77 @@ function anthropicPromptTooLongMessage(body, contextWindow) {
8296
8330
  return `prompt is too long: ${promptTokens} tokens > ${maximum} maximum`;
8297
8331
  }
8298
8332
 
8333
+ // src/listener-ready.ts
8334
+ import { connect } from "net";
8335
+ import { setTimeout as delay } from "timers/promises";
8336
+ var LISTENER_READY_TIMEOUT_MS = 1e3;
8337
+ var LISTENER_READY_RETRY_MS = 5;
8338
+ function connectHost(address) {
8339
+ if (address === "0.0.0.0") return "127.0.0.1";
8340
+ if (address === "::") return "::1";
8341
+ return address;
8342
+ }
8343
+ function tcpListenerUrlHost(address) {
8344
+ const host = connectHost(address);
8345
+ return host.includes(":") ? `[${host}]` : host;
8346
+ }
8347
+ function probeTcpListener(host, port, timeoutMs) {
8348
+ return new Promise((resolve2) => {
8349
+ const socket = connect({ host, port });
8350
+ let settled = false;
8351
+ const finish = (ready) => {
8352
+ if (settled) return;
8353
+ settled = true;
8354
+ socket.destroy();
8355
+ resolve2(ready);
8356
+ };
8357
+ socket.once("connect", () => finish(true));
8358
+ socket.once("error", () => finish(false));
8359
+ socket.setTimeout(timeoutMs, () => finish(false));
8360
+ });
8361
+ }
8362
+ async function closeAfterReadinessFailure(server) {
8363
+ if (!server.listening) return;
8364
+ await new Promise((resolve2) => server.close(() => resolve2()));
8365
+ }
8366
+ async function listenTcpServer(server, port, host) {
8367
+ await new Promise((resolve2, reject) => {
8368
+ const cleanup = () => server.off("error", onError);
8369
+ const onError = (error) => {
8370
+ cleanup();
8371
+ reject(error);
8372
+ };
8373
+ server.once("error", onError);
8374
+ try {
8375
+ server.listen(port, host, () => {
8376
+ cleanup();
8377
+ resolve2();
8378
+ });
8379
+ } catch (error) {
8380
+ cleanup();
8381
+ reject(error);
8382
+ }
8383
+ });
8384
+ const address = server.address();
8385
+ if (!address || typeof address === "string") {
8386
+ await closeAfterReadinessFailure(server);
8387
+ throw new Error("TCP server did not bind to a network address");
8388
+ }
8389
+ const probeHost = connectHost(address.address);
8390
+ const deadline = Date.now() + LISTENER_READY_TIMEOUT_MS;
8391
+ while (Date.now() < deadline) {
8392
+ const remaining = deadline - Date.now();
8393
+ if (await probeTcpListener(probeHost, address.port, Math.min(remaining, 50))) {
8394
+ return address;
8395
+ }
8396
+ await delay(Math.min(LISTENER_READY_RETRY_MS, Math.max(1, deadline - Date.now())));
8397
+ }
8398
+ await closeAfterReadinessFailure(server);
8399
+ throw new Error(
8400
+ `TCP listener did not become reachable within ${LISTENER_READY_TIMEOUT_MS}ms: ${probeHost}:${address.port}`
8401
+ );
8402
+ }
8403
+
8299
8404
  // src/proxy.ts
8300
8405
  var STREAM_KEEPALIVE_INTERVAL_MS = 2e4;
8301
8406
  var STREAM_KEEPALIVE_PING = 'event: ping\ndata: {"type":"ping"}\n\n';
@@ -8425,11 +8530,11 @@ function lookupRoute(byAlias, id) {
8425
8530
  }
8426
8531
  return void 0;
8427
8532
  }
8428
- function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPath, debugLogPath, webSocketDiagnosticsLogPath, modelAliases) {
8533
+ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPath, debugLogPath, webSocketDiagnosticsLogPath, modelAliases) {
8429
8534
  const proxyToken = randomUUID6();
8430
8535
  silenceSdkWarnings();
8431
8536
  if (routes.length === 0) {
8432
- return Promise.reject(new Error("Proxy catalog requires at least one route"));
8537
+ throw new Error("Proxy catalog requires at least one route");
8433
8538
  }
8434
8539
  const byAlias = new Map(routes.map((r) => [r.aliasId, r]));
8435
8540
  for (const alias of modelAliases ?? []) {
@@ -8506,8 +8611,9 @@ function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPa
8506
8611
  const route = lookupRoute(byAlias, originalModel) ?? defaultRoute;
8507
8612
  const apiKey = route.apiKey;
8508
8613
  const upstreamUrl = route.upstreamUrl;
8614
+ const routeAuthType = route.authType ?? "api";
8509
8615
  plog(
8510
- () => `POST /v1/messages - alias=${originalModel} route=${route.realModelId} format=${route.modelFormat} key=${apiKey ? `len:${apiKey.length}` : "MISSING"}`
8616
+ () => `POST /v1/messages - alias=${originalModel} route=${route.realModelId} format=${route.modelFormat} key=${routeAuthType === "none" ? "none" : apiKey ? `len:${apiKey.length}` : "MISSING"}`
8511
8617
  );
8512
8618
  const usesSdkAdapter = isSdkMigratedNpm(route.npm);
8513
8619
  if (messagesEndpoint === "count_tokens") {
@@ -8518,7 +8624,7 @@ function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPa
8518
8624
  sendJson(res, 200, { input_tokens: inputTokens });
8519
8625
  return;
8520
8626
  }
8521
- if (!apiKey) {
8627
+ if (!apiKey && routeAuthType !== "none") {
8522
8628
  anthropicError(res, 401, "Missing API key");
8523
8629
  return;
8524
8630
  }
@@ -8526,11 +8632,11 @@ function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPa
8526
8632
  const inboundBeta = Array.isArray(betaHeaderRaw) ? betaHeaderRaw.join(",") : betaHeaderRaw;
8527
8633
  const forwardBody = { ...anthropicBody, model: route.realModelId };
8528
8634
  const targetUrl = `${upstreamUrl}/v1/messages/count_tokens`;
8529
- const isOAuth = route.authType === "oauth";
8635
+ const isOAuth = routeAuthType === "oauth";
8530
8636
  try {
8531
8637
  await relayAnthropicMessages(res, targetUrl, forwardBody, apiKey, false, {
8532
8638
  inboundBeta,
8533
- authType: isOAuth ? "oauth" : "api",
8639
+ authType: routeAuthType,
8534
8640
  log: (message) => plog(message),
8535
8641
  extraHeaders: route.headers,
8536
8642
  refreshToken: route.refreshToken,
@@ -8547,7 +8653,7 @@ function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPa
8547
8653
  }
8548
8654
  return;
8549
8655
  }
8550
- if (!apiKey && !usesSdkAdapter) {
8656
+ if (!apiKey && routeAuthType !== "none" && !usesSdkAdapter) {
8551
8657
  anthropicError(res, 401, "Missing API key");
8552
8658
  return;
8553
8659
  }
@@ -8556,7 +8662,7 @@ function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPa
8556
8662
  const inboundBeta = Array.isArray(betaHeaderRaw) ? betaHeaderRaw.join(",") : betaHeaderRaw;
8557
8663
  const forwardBody = { ...anthropicBody, model: route.realModelId };
8558
8664
  const targetUrl = `${upstreamUrl}/v1/messages`;
8559
- const isOAuth = route.authType === "oauth";
8665
+ const isOAuth = routeAuthType === "oauth";
8560
8666
  let effectiveBeta = inboundBeta;
8561
8667
  let claudeCodeSessionId;
8562
8668
  if (isOAuth) {
@@ -8573,7 +8679,7 @@ function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPa
8573
8679
  try {
8574
8680
  await relayAnthropicMessages(res, targetUrl, forwardBody, apiKey, clientWantsStream, {
8575
8681
  inboundBeta: effectiveBeta,
8576
- authType: isOAuth ? "oauth" : "api",
8682
+ authType: routeAuthType,
8577
8683
  log: (message) => plog(message),
8578
8684
  claudeCodeSessionId,
8579
8685
  extraHeaders: route.headers,
@@ -8769,26 +8875,26 @@ data: ${JSON.stringify({
8769
8875
  }
8770
8876
  anthropicError(res, 404, `Unknown endpoint: ${req.method} ${req.url}`);
8771
8877
  });
8772
- return new Promise((resolve2, reject) => {
8773
- server.on("error", reject);
8774
- server.listen(0, "127.0.0.1", () => {
8775
- const addr = server.address();
8776
- if (!addr || typeof addr === "string") {
8777
- reject(new Error("Failed to bind proxy"));
8778
- return;
8779
- }
8780
- plog(() => `started on port ${addr.port}, catalog=${routes.length} model(s), default=${defaultRoute.aliasId}`);
8781
- resolve2({
8782
- port: addr.port,
8783
- token: proxyToken,
8784
- close: () => {
8785
- process.off("unhandledRejection", onRejection);
8786
- process.off("uncaughtException", onException);
8787
- server.close();
8788
- }
8789
- });
8790
- });
8791
- });
8878
+ let address;
8879
+ try {
8880
+ address = await listenTcpServer(server, 0, "127.0.0.1");
8881
+ } catch (error) {
8882
+ process.off("unhandledRejection", onRejection);
8883
+ process.off("uncaughtException", onException);
8884
+ throw error;
8885
+ }
8886
+ plog(
8887
+ () => `started on port ${address.port}, catalog=${routes.length} model(s), default=${defaultRoute.aliasId}`
8888
+ );
8889
+ return {
8890
+ port: address.port,
8891
+ token: proxyToken,
8892
+ close: () => {
8893
+ process.off("unhandledRejection", onRejection);
8894
+ process.off("uncaughtException", onException);
8895
+ server.close();
8896
+ }
8897
+ };
8792
8898
  }
8793
8899
  function startProxy(completionsUrl, modelId, debug = false, contextWindow, sdk, apiKey) {
8794
8900
  const bareModelId = stripOneMContextSuffix(modelId);
@@ -8811,7 +8917,8 @@ function startProxy(completionsUrl, modelId, debug = false, contextWindow, sdk,
8811
8917
  reasoning: sdk?.reasoning,
8812
8918
  interleavedReasoningField: sdk?.interleavedReasoningField,
8813
8919
  useResponsesLite: sdk?.useResponsesLite,
8814
- preferWebSockets: sdk?.preferWebSockets
8920
+ preferWebSockets: sdk?.preferWebSockets,
8921
+ headers: sdk?.headers
8815
8922
  }], clientModelId, debug);
8816
8923
  }
8817
8924
 
@@ -9187,21 +9294,11 @@ async function startServer(options) {
9187
9294
  const server = createServer2((req, res) => {
9188
9295
  void routeRequest(req, res, options, languageModelCache, plog);
9189
9296
  });
9190
- await new Promise((resolve2, reject) => {
9191
- server.once("error", reject);
9192
- server.listen(options.port, options.host, () => {
9193
- server.off("error", reject);
9194
- resolve2();
9195
- });
9196
- });
9197
- const address = server.address();
9198
- if (!address || typeof address === "string") {
9199
- throw new Error("Server did not bind to a TCP port");
9200
- }
9297
+ const address = await listenTcpServer(server, options.port, options.host);
9201
9298
  return {
9202
9299
  host: options.host,
9203
9300
  port: address.port,
9204
- url: `http://${options.host}:${address.port}`,
9301
+ url: `http://${tcpListenerUrlHost(address.address)}:${address.port}`,
9205
9302
  server,
9206
9303
  inferenceLogPath: options.inferenceLogPath,
9207
9304
  close: () => new Promise((resolve2, reject) => {
@@ -9286,7 +9383,8 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
9286
9383
  const inboundBeta = Array.isArray(betaHeaderRaw) ? betaHeaderRaw.join(",") : betaHeaderRaw;
9287
9384
  const clientWantsStream = Boolean(body.stream);
9288
9385
  const forwardBody = { ...body, model: upstreamModelId(model) };
9289
- const isOAuth = model.authType === "oauth";
9386
+ const authType = model.authType ?? "api";
9387
+ const isOAuth = authType === "oauth";
9290
9388
  auditInference(options, {
9291
9389
  requestId,
9292
9390
  modelId: body.model,
@@ -9309,7 +9407,7 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
9309
9407
  plog(() => `anthropic-passthrough \u2192 ${messagesUrl} oauth=${isOAuth} stream=${clientWantsStream}`);
9310
9408
  await relayAnthropicMessages(res, messagesUrl, forwardBody, apiKey, clientWantsStream, {
9311
9409
  inboundBeta: effectiveBeta,
9312
- authType: isOAuth ? "oauth" : "api",
9410
+ authType,
9313
9411
  log: (message) => plog(message),
9314
9412
  claudeCodeSessionId,
9315
9413
  extraHeaders: model.headers,
@@ -9465,6 +9563,8 @@ async function handleOpenAIChatCompletions(req, res, options, modelCache, plog)
9465
9563
  requestPreview: getLatestMessagePreview(body.messages, body.system)
9466
9564
  });
9467
9565
  await relayAnthropicMessages(res, completionsUrl, forwardBody, apiKey2, Boolean(body.stream), {
9566
+ authType: model.authType ?? "api",
9567
+ extraHeaders: model.headers,
9468
9568
  onUpstreamError: options.inferenceLogPath ? (statusCode, errorContent) => writeInferenceResponseErrorLog(options.inferenceLogPath, {
9469
9569
  modelId: body.model,
9470
9570
  provider: inferenceProvider(model),
@@ -10500,23 +10600,17 @@ async function startHttpProxy(options) {
10500
10600
  clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n", () => clientSocket.destroy());
10501
10601
  });
10502
10602
  });
10603
+ let address;
10503
10604
  try {
10504
- await new Promise((resolve2, reject) => {
10505
- proxyServer.once("error", reject);
10506
- proxyServer.listen(options.port ?? 0, options.host ?? "127.0.0.1", () => {
10507
- proxyServer.off("error", reject);
10508
- resolve2();
10509
- });
10510
- });
10605
+ address = await listenTcpServer(
10606
+ proxyServer,
10607
+ options.port ?? 0,
10608
+ options.host ?? "127.0.0.1"
10609
+ );
10511
10610
  } catch (err) {
10512
10611
  adapter?.close();
10513
10612
  throw err;
10514
10613
  }
10515
- const address = proxyServer.address();
10516
- if (!address || typeof address === "string") {
10517
- adapter?.close();
10518
- throw new Error("HTTP proxy did not bind to a TCP port");
10519
- }
10520
10614
  return {
10521
10615
  host: options.host ?? "127.0.0.1",
10522
10616
  port: address.port,
@@ -12835,7 +12929,8 @@ Error: ${launchPlan.error}
12835
12929
  return 0;
12836
12930
  }
12837
12931
  const launchApiKey = await resolveLocalProviderApiKey(activeProvider);
12838
- if (!launchApiKey?.trim()) {
12932
+ const anonymousProvider = activeProvider.authType === "none";
12933
+ if (!anonymousProvider && !launchApiKey?.trim()) {
12839
12934
  p12.log.error(
12840
12935
  `No credential found for ${activeProvider.name}. Add a key or sign in with clodex providers.`
12841
12936
  );
@@ -12844,7 +12939,8 @@ Error: ${launchPlan.error}
12844
12939
  let proxyHandle = null;
12845
12940
  let childEnv;
12846
12941
  const isOAuthAnthropic = selectedModel.modelFormat === "anthropic" && activeProvider.authType === "oauth";
12847
- if (isOAuthAnthropic) {
12942
+ const usesAnthropicProxy = selectedModel.modelFormat === "anthropic" && (isOAuthAnthropic || anonymousProvider);
12943
+ if (usesAnthropicProxy) {
12848
12944
  try {
12849
12945
  proxyHandle = await startProxy(
12850
12946
  selectedModel.baseUrl ?? "https://api.anthropic.com",
@@ -12853,16 +12949,17 @@ Error: ${launchPlan.error}
12853
12949
  selectedModel.contextWindow,
12854
12950
  {
12855
12951
  providerId: activeProvider.id,
12856
- authType: "oauth",
12952
+ authType: activeProvider.authType,
12857
12953
  oauthAccountId: activeProvider.oauthAccountId,
12858
12954
  providerData: activeProvider.providerData,
12859
- modelFormat: "anthropic"
12955
+ modelFormat: "anthropic",
12956
+ headers: activeProvider.headers
12860
12957
  },
12861
- launchApiKey
12958
+ launchApiKey ?? ""
12862
12959
  );
12863
- if (!isAgentStdoutMode()) p12.log.info(`OAuth proxy started on port ${proxyHandle.port}`);
12960
+ if (!isAgentStdoutMode()) p12.log.info(`Anthropic proxy started on port ${proxyHandle.port}`);
12864
12961
  } catch (err) {
12865
- p12.log.error(`Failed to start OAuth proxy: ${err instanceof Error ? err.message : String(err)}`);
12962
+ p12.log.error(`Failed to start Anthropic proxy: ${err instanceof Error ? err.message : String(err)}`);
12866
12963
  return 1;
12867
12964
  }
12868
12965
  childEnv = buildChildEnv(
@@ -12876,7 +12973,7 @@ Error: ${launchPlan.error}
12876
12973
  childEnv = buildChildEnv(
12877
12974
  selectedModel.baseUrl,
12878
12975
  selectedModel.id,
12879
- launchApiKey,
12976
+ launchApiKey ?? "",
12880
12977
  void 0,
12881
12978
  selectedModel.contextWindow
12882
12979
  );
@@ -12898,9 +12995,10 @@ Error: ${launchPlan.error}
12898
12995
  reasoning: selectedModel.reasoning,
12899
12996
  interleavedReasoningField: selectedModel.interleavedReasoningField,
12900
12997
  useResponsesLite: selectedModel.useResponsesLite,
12901
- preferWebSockets: selectedModel.preferWebSockets
12998
+ preferWebSockets: selectedModel.preferWebSockets,
12999
+ headers: activeProvider.headers
12902
13000
  },
12903
- launchApiKey
13001
+ launchApiKey ?? ""
12904
13002
  );
12905
13003
  if (!isAgentStdoutMode()) {
12906
13004
  p12.log.info(
@@ -12919,7 +13017,7 @@ Error: ${launchPlan.error}
12919
13017
  selectedModel.contextWindow
12920
13018
  );
12921
13019
  }
12922
- if (selectedModel.modelFormat === "anthropic" && !isOAuthAnthropic) {
13020
+ if (selectedModel.modelFormat === "anthropic" && !usesAnthropicProxy) {
12923
13021
  childEnv["CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS"] = "1";
12924
13022
  }
12925
13023
  const debugLogPath = prepareClaudeTraceLog();