@bman654/clodex 1.2.1 → 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.1",
205
+ version: "1.2.2",
206
206
  publishConfig: {
207
207
  access: "public"
208
208
  },
@@ -2731,13 +2731,12 @@ function providersForPicker(providers) {
2731
2731
  return providers.sort((a, b) => a.name.localeCompare(b.name, void 0, { sensitivity: "base", numeric: true }));
2732
2732
  }
2733
2733
  async function resolveLocalProviderApiKey(provider) {
2734
- if (provider.authRef === "none:anonymous") return "anonymous";
2734
+ if (provider.authRef === "none:anonymous" || provider.authType === "none") return "";
2735
2735
  const direct = provider.apiKey?.trim();
2736
2736
  if (direct) return direct;
2737
- if (provider.authType === "none") return "anonymous";
2738
2737
  const template = getTemplateById(provider.id);
2739
2738
  if (template?.apiKeyOptional || template?.anonymousFreeModels) {
2740
- return "anonymous";
2739
+ return "";
2741
2740
  }
2742
2741
  const reg = loadRegistry().providers.find((p13) => p13.id === provider.id);
2743
2742
  const authRef = provider.authRef ?? reg?.authRef ?? oauthAuthRef(provider.id);
@@ -4080,6 +4079,12 @@ function injectClaudeIdentity(body, providerData, seed) {
4080
4079
  return { sessionId, userId };
4081
4080
  }
4082
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
+
4083
4088
  // src/provider-factory.ts
4084
4089
  var RESPONSES_ONLY_PREFIXES = [
4085
4090
  "gpt-5-codex",
@@ -4089,6 +4094,15 @@ var RESPONSES_ONLY_PREFIXES = [
4089
4094
  "o4"
4090
4095
  ];
4091
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
+ };
4092
4106
  function modelPrefersResponsesApi(modelId) {
4093
4107
  const lower = modelId.toLowerCase();
4094
4108
  if (RESPONSES_ONLY_PREFIXES.some((prefix) => lower === prefix || lower.startsWith(`${prefix}-`))) {
@@ -4153,6 +4167,7 @@ async function createLanguageModel(spec) {
4153
4167
  apiKey,
4154
4168
  baseURL: "https://chatgpt.com/backend-api/codex",
4155
4169
  headers: {
4170
+ ...spec.headers,
4156
4171
  ...accountId ? { "ChatGPT-Account-Id": accountId } : {},
4157
4172
  originator: "clodex",
4158
4173
  // Responses-Lite models (backend prefer_websockets/use_responses_lite,
@@ -4170,7 +4185,11 @@ async function createLanguageModel(spec) {
4170
4185
  onDiagnostic: spec.onWebSocketDiagnostic
4171
4186
  })
4172
4187
  } : {}
4173
- } : { apiKey };
4188
+ } : spec.authType === "none" ? {
4189
+ apiKey: "",
4190
+ ...spec.headers ? { headers: spec.headers } : {},
4191
+ fetch: fetchWithoutCredentialHeaders
4192
+ } : { apiKey, ...spec.headers ? { headers: spec.headers } : {} };
4174
4193
  const openai = createOpenAI(oauthOptions);
4175
4194
  return useResponsesEndpoint ? openai.responses(modelId) : openai.chat(modelId);
4176
4195
  }
@@ -4190,7 +4209,7 @@ async function createLanguageModel(spec) {
4190
4209
  ).sessionId
4191
4210
  }
4192
4211
  } : {}
4193
- } : { apiKey };
4212
+ } : spec.authType === "none" ? { apiKey: "", fetch: fetchWithoutCredentialHeaders } : { apiKey };
4194
4213
  if (spec.headers) {
4195
4214
  anthropicOptions.headers = { ...anthropicOptions.headers, ...spec.headers };
4196
4215
  }
@@ -4206,7 +4225,8 @@ async function createLanguageModel(spec) {
4206
4225
  const options = {
4207
4226
  name: spec.providerId ?? "openai-compatible",
4208
4227
  baseURL: baseURL ?? "",
4209
- ...apiKey.trim() ? { apiKey } : {},
4228
+ ...spec.authType !== "none" && apiKey.trim() ? { apiKey } : {},
4229
+ ...spec.authType === "none" ? { fetch: fetchWithoutCredentialHeaders } : {},
4210
4230
  ...spec.headers ? { headers: spec.headers } : {}
4211
4231
  };
4212
4232
  model = createOpenAICompatible({
@@ -4215,7 +4235,8 @@ async function createLanguageModel(spec) {
4215
4235
  } else {
4216
4236
  const create = await loadSdkProviderFactory(npm);
4217
4237
  const provider = create({
4218
- apiKey,
4238
+ apiKey: spec.authType === "none" ? "" : apiKey,
4239
+ ...spec.authType === "none" ? { fetch: fetchWithoutCredentialHeaders } : {},
4219
4240
  ...baseURL ? { baseURL } : {},
4220
4241
  ...spec.headers ? { headers: spec.headers } : {}
4221
4242
  });
@@ -4822,7 +4843,6 @@ function getLatestMessagePreview(messages, system) {
4822
4843
  return compactLogValue(preview, REQUEST_PREVIEW_MAX + 20);
4823
4844
  }
4824
4845
  var REDACTED_DIAGNOSTIC_HEADER = "[REDACTED]";
4825
- var SENSITIVE_DIAGNOSTIC_HEADER = /(?:^|[-_])(?:authorization|api[-_]?key|cookie|token|secret|credential)(?:$|[-_])/i;
4826
4846
  var CONVERSATION_BODY_FIELDS = /* @__PURE__ */ new Set(["system", "messages", "tools"]);
4827
4847
  function canonicalDiagnosticValue(value) {
4828
4848
  if (Array.isArray(value)) return value.map(canonicalDiagnosticValue);
@@ -4841,7 +4861,7 @@ function sanitizeDiagnosticHeaders(headers) {
4841
4861
  const out = {};
4842
4862
  for (const [name, value] of Object.entries(headers).sort(([left], [right]) => left.localeCompare(right))) {
4843
4863
  if (value === void 0) continue;
4844
- out[name.toLowerCase()] = SENSITIVE_DIAGNOSTIC_HEADER.test(name) ? REDACTED_DIAGNOSTIC_HEADER : value;
4864
+ out[name.toLowerCase()] = isCredentialBearingHeader(name) ? REDACTED_DIAGNOSTIC_HEADER : value;
4845
4865
  }
4846
4866
  return out;
4847
4867
  }
@@ -5397,6 +5417,9 @@ async function addProviderFromTemplate(template, apiKey, opts) {
5397
5417
  function credentialStillReferenced(authRef, remaining) {
5398
5418
  return remaining.some((p13) => p13.authRef === authRef);
5399
5419
  }
5420
+ function isStoredCredentialRef(authRef) {
5421
+ return authRef.startsWith("keyring:") || authRef.startsWith("helper:");
5422
+ }
5400
5423
  async function removeProviderFromRegistry(id, opts) {
5401
5424
  const removal = await withRegistryWriteLock(() => {
5402
5425
  const registry = loadRegistry();
@@ -5421,7 +5444,7 @@ async function removeProviderFromRegistry(id, opts) {
5421
5444
  name: removedProvider.name,
5422
5445
  credentialDeleted: false
5423
5446
  },
5424
- 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
5425
5448
  };
5426
5449
  });
5427
5450
  const authRefToDelete = removal.authRefToDelete;
@@ -6215,16 +6238,18 @@ async function authenticateProvider(providerId, _options = {}) {
6215
6238
  nativeDiagMsg = msg;
6216
6239
  }
6217
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
+ }
6218
6246
  const registryProvider2 = await upsertOAuthProvider(
6219
6247
  providerId,
6220
6248
  cred,
6221
6249
  authRef
6222
6250
  );
6223
- return { saved, nativeDiagMsg, registryProvider: registryProvider2 };
6251
+ return { registryProvider: registryProvider2 };
6224
6252
  });
6225
- if (!persisted.saved) {
6226
- p2.log.warn(`Could not save OAuth tokens to the credential store \u2014 ${persisted.nativeDiagMsg || "session may not persist."}`);
6227
- }
6228
6253
  const { registryProvider } = persisted;
6229
6254
  const refreshSpinner = p2.spinner();
6230
6255
  refreshSpinner.start("Refreshing model list...");
@@ -7138,7 +7163,7 @@ function buildHttpProxyRoutes(providers, favorites, modelAliases = [], max = MAX
7138
7163
  continue;
7139
7164
  }
7140
7165
  const route = localModelToRoute(provider, model);
7141
- if (!route || !route.apiKey.trim()) {
7166
+ if (!route || !route.apiKey.trim() && route.authType !== "none") {
7142
7167
  unavailable.push(favorite);
7143
7168
  continue;
7144
7169
  }
@@ -7319,13 +7344,21 @@ function extractBearerToken(value) {
7319
7344
  // src/upstream-forward.ts
7320
7345
  function anthropicUpstreamHeaders(apiKey, stream = false, inboundBeta, authType, claudeCodeSessionId, extraHeaders) {
7321
7346
  const key = sanitizeCredential(apiKey) ?? apiKey.trim();
7322
- 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;
7323
7354
  const headers = {
7324
- ...extraHeaders,
7355
+ ...forwardedExtraHeaders,
7325
7356
  "Content-Type": "application/json",
7326
7357
  "anthropic-version": "2023-06-01",
7327
- Authorization: `Bearer ${key}`,
7328
- ...isOAuth ? {} : { "x-api-key": key },
7358
+ ...resolvedAuthType === "none" ? {} : {
7359
+ Authorization: `Bearer ${key}`,
7360
+ ...isOAuth ? {} : { "x-api-key": key }
7361
+ },
7329
7362
  ...isOAuth ? { "User-Agent": CLAUDE_CODE_USER_AGENT, "x-app": "cli" } : {},
7330
7363
  ...isOAuth && claudeCodeSessionId ? { "X-Claude-Code-Session-Id": claudeCodeSessionId } : {},
7331
7364
  ...stream ? { Accept: "text/event-stream" } : {}
@@ -8297,6 +8330,77 @@ function anthropicPromptTooLongMessage(body, contextWindow) {
8297
8330
  return `prompt is too long: ${promptTokens} tokens > ${maximum} maximum`;
8298
8331
  }
8299
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
+
8300
8404
  // src/proxy.ts
8301
8405
  var STREAM_KEEPALIVE_INTERVAL_MS = 2e4;
8302
8406
  var STREAM_KEEPALIVE_PING = 'event: ping\ndata: {"type":"ping"}\n\n';
@@ -8426,11 +8530,11 @@ function lookupRoute(byAlias, id) {
8426
8530
  }
8427
8531
  return void 0;
8428
8532
  }
8429
- function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPath, debugLogPath, webSocketDiagnosticsLogPath, modelAliases) {
8533
+ async function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPath, debugLogPath, webSocketDiagnosticsLogPath, modelAliases) {
8430
8534
  const proxyToken = randomUUID6();
8431
8535
  silenceSdkWarnings();
8432
8536
  if (routes.length === 0) {
8433
- return Promise.reject(new Error("Proxy catalog requires at least one route"));
8537
+ throw new Error("Proxy catalog requires at least one route");
8434
8538
  }
8435
8539
  const byAlias = new Map(routes.map((r) => [r.aliasId, r]));
8436
8540
  for (const alias of modelAliases ?? []) {
@@ -8507,8 +8611,9 @@ function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPa
8507
8611
  const route = lookupRoute(byAlias, originalModel) ?? defaultRoute;
8508
8612
  const apiKey = route.apiKey;
8509
8613
  const upstreamUrl = route.upstreamUrl;
8614
+ const routeAuthType = route.authType ?? "api";
8510
8615
  plog(
8511
- () => `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"}`
8512
8617
  );
8513
8618
  const usesSdkAdapter = isSdkMigratedNpm(route.npm);
8514
8619
  if (messagesEndpoint === "count_tokens") {
@@ -8519,7 +8624,7 @@ function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPa
8519
8624
  sendJson(res, 200, { input_tokens: inputTokens });
8520
8625
  return;
8521
8626
  }
8522
- if (!apiKey) {
8627
+ if (!apiKey && routeAuthType !== "none") {
8523
8628
  anthropicError(res, 401, "Missing API key");
8524
8629
  return;
8525
8630
  }
@@ -8527,11 +8632,11 @@ function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPa
8527
8632
  const inboundBeta = Array.isArray(betaHeaderRaw) ? betaHeaderRaw.join(",") : betaHeaderRaw;
8528
8633
  const forwardBody = { ...anthropicBody, model: route.realModelId };
8529
8634
  const targetUrl = `${upstreamUrl}/v1/messages/count_tokens`;
8530
- const isOAuth = route.authType === "oauth";
8635
+ const isOAuth = routeAuthType === "oauth";
8531
8636
  try {
8532
8637
  await relayAnthropicMessages(res, targetUrl, forwardBody, apiKey, false, {
8533
8638
  inboundBeta,
8534
- authType: isOAuth ? "oauth" : "api",
8639
+ authType: routeAuthType,
8535
8640
  log: (message) => plog(message),
8536
8641
  extraHeaders: route.headers,
8537
8642
  refreshToken: route.refreshToken,
@@ -8548,7 +8653,7 @@ function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPa
8548
8653
  }
8549
8654
  return;
8550
8655
  }
8551
- if (!apiKey && !usesSdkAdapter) {
8656
+ if (!apiKey && routeAuthType !== "none" && !usesSdkAdapter) {
8552
8657
  anthropicError(res, 401, "Missing API key");
8553
8658
  return;
8554
8659
  }
@@ -8557,7 +8662,7 @@ function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPa
8557
8662
  const inboundBeta = Array.isArray(betaHeaderRaw) ? betaHeaderRaw.join(",") : betaHeaderRaw;
8558
8663
  const forwardBody = { ...anthropicBody, model: route.realModelId };
8559
8664
  const targetUrl = `${upstreamUrl}/v1/messages`;
8560
- const isOAuth = route.authType === "oauth";
8665
+ const isOAuth = routeAuthType === "oauth";
8561
8666
  let effectiveBeta = inboundBeta;
8562
8667
  let claudeCodeSessionId;
8563
8668
  if (isOAuth) {
@@ -8574,7 +8679,7 @@ function startProxyCatalog(routes, defaultAliasId, debug = false, inferenceLogPa
8574
8679
  try {
8575
8680
  await relayAnthropicMessages(res, targetUrl, forwardBody, apiKey, clientWantsStream, {
8576
8681
  inboundBeta: effectiveBeta,
8577
- authType: isOAuth ? "oauth" : "api",
8682
+ authType: routeAuthType,
8578
8683
  log: (message) => plog(message),
8579
8684
  claudeCodeSessionId,
8580
8685
  extraHeaders: route.headers,
@@ -8770,26 +8875,26 @@ data: ${JSON.stringify({
8770
8875
  }
8771
8876
  anthropicError(res, 404, `Unknown endpoint: ${req.method} ${req.url}`);
8772
8877
  });
8773
- return new Promise((resolve2, reject) => {
8774
- server.on("error", reject);
8775
- server.listen(0, "127.0.0.1", () => {
8776
- const addr = server.address();
8777
- if (!addr || typeof addr === "string") {
8778
- reject(new Error("Failed to bind proxy"));
8779
- return;
8780
- }
8781
- plog(() => `started on port ${addr.port}, catalog=${routes.length} model(s), default=${defaultRoute.aliasId}`);
8782
- resolve2({
8783
- port: addr.port,
8784
- token: proxyToken,
8785
- close: () => {
8786
- process.off("unhandledRejection", onRejection);
8787
- process.off("uncaughtException", onException);
8788
- server.close();
8789
- }
8790
- });
8791
- });
8792
- });
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
+ };
8793
8898
  }
8794
8899
  function startProxy(completionsUrl, modelId, debug = false, contextWindow, sdk, apiKey) {
8795
8900
  const bareModelId = stripOneMContextSuffix(modelId);
@@ -8812,7 +8917,8 @@ function startProxy(completionsUrl, modelId, debug = false, contextWindow, sdk,
8812
8917
  reasoning: sdk?.reasoning,
8813
8918
  interleavedReasoningField: sdk?.interleavedReasoningField,
8814
8919
  useResponsesLite: sdk?.useResponsesLite,
8815
- preferWebSockets: sdk?.preferWebSockets
8920
+ preferWebSockets: sdk?.preferWebSockets,
8921
+ headers: sdk?.headers
8816
8922
  }], clientModelId, debug);
8817
8923
  }
8818
8924
 
@@ -9188,21 +9294,11 @@ async function startServer(options) {
9188
9294
  const server = createServer2((req, res) => {
9189
9295
  void routeRequest(req, res, options, languageModelCache, plog);
9190
9296
  });
9191
- await new Promise((resolve2, reject) => {
9192
- server.once("error", reject);
9193
- server.listen(options.port, options.host, () => {
9194
- server.off("error", reject);
9195
- resolve2();
9196
- });
9197
- });
9198
- const address = server.address();
9199
- if (!address || typeof address === "string") {
9200
- throw new Error("Server did not bind to a TCP port");
9201
- }
9297
+ const address = await listenTcpServer(server, options.port, options.host);
9202
9298
  return {
9203
9299
  host: options.host,
9204
9300
  port: address.port,
9205
- url: `http://${options.host}:${address.port}`,
9301
+ url: `http://${tcpListenerUrlHost(address.address)}:${address.port}`,
9206
9302
  server,
9207
9303
  inferenceLogPath: options.inferenceLogPath,
9208
9304
  close: () => new Promise((resolve2, reject) => {
@@ -9287,7 +9383,8 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
9287
9383
  const inboundBeta = Array.isArray(betaHeaderRaw) ? betaHeaderRaw.join(",") : betaHeaderRaw;
9288
9384
  const clientWantsStream = Boolean(body.stream);
9289
9385
  const forwardBody = { ...body, model: upstreamModelId(model) };
9290
- const isOAuth = model.authType === "oauth";
9386
+ const authType = model.authType ?? "api";
9387
+ const isOAuth = authType === "oauth";
9291
9388
  auditInference(options, {
9292
9389
  requestId,
9293
9390
  modelId: body.model,
@@ -9310,7 +9407,7 @@ async function handleAnthropicMessages(req, res, options, modelCache, plog) {
9310
9407
  plog(() => `anthropic-passthrough \u2192 ${messagesUrl} oauth=${isOAuth} stream=${clientWantsStream}`);
9311
9408
  await relayAnthropicMessages(res, messagesUrl, forwardBody, apiKey, clientWantsStream, {
9312
9409
  inboundBeta: effectiveBeta,
9313
- authType: isOAuth ? "oauth" : "api",
9410
+ authType,
9314
9411
  log: (message) => plog(message),
9315
9412
  claudeCodeSessionId,
9316
9413
  extraHeaders: model.headers,
@@ -9466,6 +9563,8 @@ async function handleOpenAIChatCompletions(req, res, options, modelCache, plog)
9466
9563
  requestPreview: getLatestMessagePreview(body.messages, body.system)
9467
9564
  });
9468
9565
  await relayAnthropicMessages(res, completionsUrl, forwardBody, apiKey2, Boolean(body.stream), {
9566
+ authType: model.authType ?? "api",
9567
+ extraHeaders: model.headers,
9469
9568
  onUpstreamError: options.inferenceLogPath ? (statusCode, errorContent) => writeInferenceResponseErrorLog(options.inferenceLogPath, {
9470
9569
  modelId: body.model,
9471
9570
  provider: inferenceProvider(model),
@@ -10501,23 +10600,17 @@ async function startHttpProxy(options) {
10501
10600
  clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n", () => clientSocket.destroy());
10502
10601
  });
10503
10602
  });
10603
+ let address;
10504
10604
  try {
10505
- await new Promise((resolve2, reject) => {
10506
- proxyServer.once("error", reject);
10507
- proxyServer.listen(options.port ?? 0, options.host ?? "127.0.0.1", () => {
10508
- proxyServer.off("error", reject);
10509
- resolve2();
10510
- });
10511
- });
10605
+ address = await listenTcpServer(
10606
+ proxyServer,
10607
+ options.port ?? 0,
10608
+ options.host ?? "127.0.0.1"
10609
+ );
10512
10610
  } catch (err) {
10513
10611
  adapter?.close();
10514
10612
  throw err;
10515
10613
  }
10516
- const address = proxyServer.address();
10517
- if (!address || typeof address === "string") {
10518
- adapter?.close();
10519
- throw new Error("HTTP proxy did not bind to a TCP port");
10520
- }
10521
10614
  return {
10522
10615
  host: options.host ?? "127.0.0.1",
10523
10616
  port: address.port,
@@ -12836,7 +12929,8 @@ Error: ${launchPlan.error}
12836
12929
  return 0;
12837
12930
  }
12838
12931
  const launchApiKey = await resolveLocalProviderApiKey(activeProvider);
12839
- if (!launchApiKey?.trim()) {
12932
+ const anonymousProvider = activeProvider.authType === "none";
12933
+ if (!anonymousProvider && !launchApiKey?.trim()) {
12840
12934
  p12.log.error(
12841
12935
  `No credential found for ${activeProvider.name}. Add a key or sign in with clodex providers.`
12842
12936
  );
@@ -12845,7 +12939,8 @@ Error: ${launchPlan.error}
12845
12939
  let proxyHandle = null;
12846
12940
  let childEnv;
12847
12941
  const isOAuthAnthropic = selectedModel.modelFormat === "anthropic" && activeProvider.authType === "oauth";
12848
- if (isOAuthAnthropic) {
12942
+ const usesAnthropicProxy = selectedModel.modelFormat === "anthropic" && (isOAuthAnthropic || anonymousProvider);
12943
+ if (usesAnthropicProxy) {
12849
12944
  try {
12850
12945
  proxyHandle = await startProxy(
12851
12946
  selectedModel.baseUrl ?? "https://api.anthropic.com",
@@ -12854,16 +12949,17 @@ Error: ${launchPlan.error}
12854
12949
  selectedModel.contextWindow,
12855
12950
  {
12856
12951
  providerId: activeProvider.id,
12857
- authType: "oauth",
12952
+ authType: activeProvider.authType,
12858
12953
  oauthAccountId: activeProvider.oauthAccountId,
12859
12954
  providerData: activeProvider.providerData,
12860
- modelFormat: "anthropic"
12955
+ modelFormat: "anthropic",
12956
+ headers: activeProvider.headers
12861
12957
  },
12862
- launchApiKey
12958
+ launchApiKey ?? ""
12863
12959
  );
12864
- 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}`);
12865
12961
  } catch (err) {
12866
- 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)}`);
12867
12963
  return 1;
12868
12964
  }
12869
12965
  childEnv = buildChildEnv(
@@ -12877,7 +12973,7 @@ Error: ${launchPlan.error}
12877
12973
  childEnv = buildChildEnv(
12878
12974
  selectedModel.baseUrl,
12879
12975
  selectedModel.id,
12880
- launchApiKey,
12976
+ launchApiKey ?? "",
12881
12977
  void 0,
12882
12978
  selectedModel.contextWindow
12883
12979
  );
@@ -12899,9 +12995,10 @@ Error: ${launchPlan.error}
12899
12995
  reasoning: selectedModel.reasoning,
12900
12996
  interleavedReasoningField: selectedModel.interleavedReasoningField,
12901
12997
  useResponsesLite: selectedModel.useResponsesLite,
12902
- preferWebSockets: selectedModel.preferWebSockets
12998
+ preferWebSockets: selectedModel.preferWebSockets,
12999
+ headers: activeProvider.headers
12903
13000
  },
12904
- launchApiKey
13001
+ launchApiKey ?? ""
12905
13002
  );
12906
13003
  if (!isAgentStdoutMode()) {
12907
13004
  p12.log.info(
@@ -12920,7 +13017,7 @@ Error: ${launchPlan.error}
12920
13017
  selectedModel.contextWindow
12921
13018
  );
12922
13019
  }
12923
- if (selectedModel.modelFormat === "anthropic" && !isOAuthAnthropic) {
13020
+ if (selectedModel.modelFormat === "anthropic" && !usesAnthropicProxy) {
12924
13021
  childEnv["CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS"] = "1";
12925
13022
  }
12926
13023
  const debugLogPath = prepareClaudeTraceLog();