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