@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 +6 -1
- package/dist/cli.js +185 -87
- package/dist/cli.js.map +1 -1
- package/docs/credential-helpers.md +6 -4
- package/package.json +2 -1
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.
|
|
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 "
|
|
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 "
|
|
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
|
-
} :
|
|
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()] =
|
|
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 {
|
|
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
|
|
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
|
-
...
|
|
7355
|
+
...forwardedExtraHeaders,
|
|
7324
7356
|
"Content-Type": "application/json",
|
|
7325
7357
|
"anthropic-version": "2023-06-01",
|
|
7326
|
-
|
|
7327
|
-
|
|
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
|
-
|
|
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 =
|
|
8635
|
+
const isOAuth = routeAuthType === "oauth";
|
|
8530
8636
|
try {
|
|
8531
8637
|
await relayAnthropicMessages(res, targetUrl, forwardBody, apiKey, false, {
|
|
8532
8638
|
inboundBeta,
|
|
8533
|
-
authType:
|
|
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 =
|
|
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:
|
|
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
|
-
|
|
8773
|
-
|
|
8774
|
-
server
|
|
8775
|
-
|
|
8776
|
-
|
|
8777
|
-
|
|
8778
|
-
|
|
8779
|
-
|
|
8780
|
-
|
|
8781
|
-
|
|
8782
|
-
|
|
8783
|
-
|
|
8784
|
-
|
|
8785
|
-
|
|
8786
|
-
|
|
8787
|
-
|
|
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
|
|
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://${
|
|
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
|
|
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
|
|
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
|
|
10505
|
-
proxyServer
|
|
10506
|
-
|
|
10507
|
-
|
|
10508
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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(`
|
|
12960
|
+
if (!isAgentStdoutMode()) p12.log.info(`Anthropic proxy started on port ${proxyHandle.port}`);
|
|
12864
12961
|
} catch (err) {
|
|
12865
|
-
p12.log.error(`Failed to start
|
|
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" && !
|
|
13020
|
+
if (selectedModel.modelFormat === "anthropic" && !usesAnthropicProxy) {
|
|
12923
13021
|
childEnv["CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS"] = "1";
|
|
12924
13022
|
}
|
|
12925
13023
|
const debugLogPath = prepareClaudeTraceLog();
|