@premai/api-sdk 1.0.46 → 1.0.48
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 +57 -7
- package/dist/anthropic/to-openai.d.ts +1 -1
- package/dist/bare.cjs +20 -5
- package/dist/bare.mjs +20 -5
- package/dist/cli-claude.mjs +419 -3004
- package/dist/cli.mjs +2774 -2206
- package/dist/core.browser.cjs +26 -6
- package/dist/core.browser.mjs +20 -5
- package/dist/core.d.ts +2 -2
- package/dist/files/index.d.ts +3 -3
- package/dist/index.cjs +459 -36
- package/dist/index.mjs +471 -37
- package/dist/launcher/proxy-subprocess.d.ts +4 -2
- package/dist/server/create-app.d.ts +1 -0
- package/dist/server/create-drain-wrapper.d.ts +5 -0
- package/dist/server/discovery.d.ts +2 -0
- package/dist/server/request-debug.d.ts +2 -0
- package/dist/server/runtime.d.ts +2 -2
- package/dist/server/shutdown-route.d.ts +6 -0
- package/dist/server/start.d.ts +7 -4
- package/dist/server.d.ts +2 -0
- package/dist/tools/index.d.ts +1 -1
- package/dist/types.d.ts +17 -1
- package/dist/utils/crypto.d.ts +1 -1
- package/dist/utils/debug.d.ts +5 -0
- package/dist/utils/dek-store.d.ts +1 -1
- package/dist/utils/poll-ready.d.ts +2 -0
- package/dist/utils/state-file.d.ts +13 -0
- package/package.json +2 -1
package/dist/cli-claude.mjs
CHANGED
|
@@ -1,1517 +1,54 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { createRequire } from "node:module";
|
|
3
|
-
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
2
|
|
|
5
3
|
// src/launcher/claude-code.ts
|
|
6
|
-
import {
|
|
7
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
8
5
|
import path from "node:path";
|
|
9
6
|
import { config } from "dotenv";
|
|
10
|
-
import
|
|
11
|
-
|
|
12
|
-
// src/
|
|
13
|
-
import
|
|
14
|
-
|
|
15
|
-
// src/
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
} else if (typeof data === "string") {
|
|
42
|
-
encodedData = new TextEncoder().encode(data);
|
|
43
|
-
} else {
|
|
44
|
-
encodedData = new TextEncoder().encode(JSON.stringify(data));
|
|
45
|
-
}
|
|
46
|
-
const encrypted = chacha.encrypt(encodedData);
|
|
47
|
-
return { encrypted, nonce };
|
|
48
|
-
}
|
|
49
|
-
function decryptPayload(encryptedData, sharedSecret, nonce) {
|
|
50
|
-
const chacha = xchacha20poly1305(sharedSecret, nonce);
|
|
51
|
-
const encrypted = hexToBytes(encryptedData);
|
|
52
|
-
const decrypted = chacha.decrypt(encrypted);
|
|
53
|
-
const str = new TextDecoder().decode(decrypted);
|
|
54
|
-
try {
|
|
55
|
-
return JSON.parse(str);
|
|
56
|
-
} catch {
|
|
57
|
-
return str;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
async function getEnclavePublicKey(timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
61
|
-
const controller = new AbortController;
|
|
62
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
63
|
-
try {
|
|
64
|
-
const response = await fetch(`${endpoints.enclave}/publicKey`, {
|
|
65
|
-
signal: controller.signal
|
|
66
|
-
});
|
|
67
|
-
if (!response.ok) {
|
|
68
|
-
throw new Error(`Failed to fetch enclave public key: ${response.status} ${response.statusText}`);
|
|
69
|
-
}
|
|
70
|
-
const data = await response.json();
|
|
71
|
-
if (!data.publicKey || typeof data.publicKey !== "string") {
|
|
72
|
-
throw new Error("Invalid public key response from enclave");
|
|
73
|
-
}
|
|
74
|
-
return data.publicKey;
|
|
75
|
-
} catch (error) {
|
|
76
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
77
|
-
throw new Error(`Enclave public key request timed out after ${timeoutMs}ms`);
|
|
78
|
-
}
|
|
79
|
-
throw new Error(`Failed to get enclave public key: ${error instanceof Error ? error.message : error}`);
|
|
80
|
-
} finally {
|
|
81
|
-
clearTimeout(timeoutId);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
async function generateEncryptionKeys(timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
85
|
-
const enclavePublicKey = await getEnclavePublicKey(timeoutMs);
|
|
86
|
-
return createMLKEMEncapsulation(enclavePublicKey);
|
|
87
|
-
}
|
|
88
|
-
function keyIdFromKEK(kek, context = "kek:v1", length = 16) {
|
|
89
|
-
const ctx = new TextEncoder().encode(context);
|
|
90
|
-
const input = new Uint8Array(kek.length + ctx.length);
|
|
91
|
-
input.set(kek, 0);
|
|
92
|
-
input.set(ctx, kek.length);
|
|
93
|
-
const digest = sha256(input);
|
|
94
|
-
return digest.slice(0, length);
|
|
95
|
-
}
|
|
96
|
-
function encryptWithDEK(dek, plaintext) {
|
|
97
|
-
const aead = managedNonce(xchacha20poly1305)(dek);
|
|
98
|
-
return aead.encrypt(plaintext);
|
|
99
|
-
}
|
|
100
|
-
function encryptMetadataWithDEK(dek, metadata) {
|
|
101
|
-
const encoded = new TextEncoder().encode(metadata);
|
|
102
|
-
const encrypted = encryptWithDEK(dek, encoded);
|
|
103
|
-
return bytesToHex(encrypted);
|
|
104
|
-
}
|
|
105
|
-
function wrapDEK(kek, dek) {
|
|
106
|
-
const kw = aeskwp(kek);
|
|
107
|
-
return kw.encrypt(dek);
|
|
108
|
-
}
|
|
109
|
-
function unwrapDEK(kek, wrappedDEK) {
|
|
110
|
-
const kw = aeskwp(kek);
|
|
111
|
-
return kw.decrypt(wrappedDEK);
|
|
112
|
-
}
|
|
113
|
-
function decryptWithDEK(dek, encryptedContent) {
|
|
114
|
-
const aead = managedNonce(xchacha20poly1305)(dek);
|
|
115
|
-
return aead.decrypt(encryptedContent);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// src/utils/dek-store.ts
|
|
119
|
-
function initializeDEKStore(clientKEK) {
|
|
120
|
-
const ragDEK = randomBytes2(32);
|
|
121
|
-
const _clientKEK = clientKEK ? hexToBytes2(clientKEK) : getClientKEK();
|
|
122
|
-
const wrappedRagDEK = wrapDEK(_clientKEK, ragDEK);
|
|
123
|
-
return {
|
|
124
|
-
fileDEKs: new Map,
|
|
125
|
-
ragDEK: wrappedRagDEK,
|
|
126
|
-
ragVersion: "2"
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
function getClientKEK() {
|
|
130
|
-
if (!process.env.CLIENT_KEK) {
|
|
131
|
-
throw new Error("CLIENT_KEK environment variable is not set.");
|
|
132
|
-
}
|
|
133
|
-
return hexToBytes2(process.env.CLIENT_KEK);
|
|
134
|
-
}
|
|
135
|
-
function getClientKID(clientKEK) {
|
|
136
|
-
if (clientKEK) {
|
|
137
|
-
return bytesToHex2(keyIdFromKEK(hexToBytes2(clientKEK)));
|
|
138
|
-
}
|
|
139
|
-
const _clientKEK = getClientKEK();
|
|
140
|
-
return bytesToHex2(keyIdFromKEK(_clientKEK));
|
|
141
|
-
}
|
|
142
|
-
function generateNewClientKEK() {
|
|
143
|
-
return bytesToHex2(randomBytes2(32));
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// src/launcher/model-picker.tsx
|
|
147
|
-
import { Box, render, Text, useApp, useInput, useWindowSize } from "ink";
|
|
148
|
-
import { useMemo, useState } from "react";
|
|
149
|
-
import { jsxDEV } from "react/jsx-dev-runtime";
|
|
150
|
-
function ModelPicker({ models, onSelect, onCancel }) {
|
|
151
|
-
const { exit } = useApp();
|
|
152
|
-
const { rows: termRows } = useWindowSize();
|
|
153
|
-
const [cursor, setCursor] = useState(0);
|
|
154
|
-
const modelLabels = useMemo(() => models.map((m) => m.display_name && m.display_name !== m.id ? `${m.id} — ${m.display_name}` : m.id), [models]);
|
|
155
|
-
const visibleCount = Math.max(1, Math.min(modelLabels.length, termRows - 4));
|
|
156
|
-
const scrollOffset = Math.max(0, Math.min(cursor - Math.floor(visibleCount / 2), modelLabels.length - visibleCount));
|
|
157
|
-
const windowedLabels = modelLabels.slice(scrollOffset, scrollOffset + visibleCount);
|
|
158
|
-
useInput((_input, key) => {
|
|
159
|
-
if (key.upArrow || _input === "k" && !key.ctrl) {
|
|
160
|
-
setCursor((c) => Math.max(0, c - 1));
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
if (key.downArrow || _input === "j" && !key.ctrl) {
|
|
164
|
-
setCursor((c) => Math.min(modelLabels.length - 1, c + 1));
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
if (key.return) {
|
|
168
|
-
onSelect(models[cursor]);
|
|
169
|
-
exit();
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
if (key.escape) {
|
|
173
|
-
onCancel?.();
|
|
174
|
-
exit();
|
|
175
|
-
}
|
|
176
|
-
});
|
|
177
|
-
return /* @__PURE__ */ jsxDEV(Box, {
|
|
178
|
-
flexDirection: "column",
|
|
179
|
-
paddingTop: 1,
|
|
180
|
-
children: [
|
|
181
|
-
/* @__PURE__ */ jsxDEV(Text, {
|
|
182
|
-
dimColor: true,
|
|
183
|
-
children: "Available models (use ↑/↓ or j/k to navigate, Enter to select):"
|
|
184
|
-
}, undefined, false, undefined, this),
|
|
185
|
-
/* @__PURE__ */ jsxDEV(Box, {
|
|
186
|
-
flexDirection: "column",
|
|
187
|
-
paddingTop: 1,
|
|
188
|
-
children: windowedLabels.map((label, i) => {
|
|
189
|
-
const globalIdx = scrollOffset + i;
|
|
190
|
-
if (globalIdx === cursor) {
|
|
191
|
-
return /* @__PURE__ */ jsxDEV(Box, {
|
|
192
|
-
paddingLeft: 2,
|
|
193
|
-
children: /* @__PURE__ */ jsxDEV(Text, {
|
|
194
|
-
inverse: true,
|
|
195
|
-
children: label.padEnd(process.stdout.columns - 4)
|
|
196
|
-
}, undefined, false, undefined, this)
|
|
197
|
-
}, label, false, undefined, this);
|
|
198
|
-
}
|
|
199
|
-
return /* @__PURE__ */ jsxDEV(Box, {
|
|
200
|
-
paddingLeft: 2,
|
|
201
|
-
children: /* @__PURE__ */ jsxDEV(Text, {
|
|
202
|
-
children: label
|
|
203
|
-
}, undefined, false, undefined, this)
|
|
204
|
-
}, label, false, undefined, this);
|
|
205
|
-
})
|
|
206
|
-
}, undefined, false, undefined, this),
|
|
207
|
-
modelLabels.length > visibleCount && /* @__PURE__ */ jsxDEV(Box, {
|
|
208
|
-
paddingLeft: 2,
|
|
209
|
-
paddingTop: 1,
|
|
210
|
-
children: /* @__PURE__ */ jsxDEV(Text, {
|
|
211
|
-
dimColor: true,
|
|
212
|
-
children: [
|
|
213
|
-
scrollOffset > 0 ? "↑ more" : "",
|
|
214
|
-
scrollOffset > 0 && scrollOffset + visibleCount < modelLabels.length ? " · " : "",
|
|
215
|
-
scrollOffset + visibleCount < modelLabels.length ? "↓ more" : ""
|
|
216
|
-
]
|
|
217
|
-
}, undefined, true, undefined, this)
|
|
218
|
-
}, undefined, false, undefined, this)
|
|
219
|
-
]
|
|
220
|
-
}, undefined, true, undefined, this);
|
|
221
|
-
}
|
|
222
|
-
function interactivePickModel(models) {
|
|
223
|
-
return new Promise((resolve, reject) => {
|
|
224
|
-
const { unmount, waitUntilExit } = render(/* @__PURE__ */ jsxDEV(ModelPicker, {
|
|
225
|
-
models,
|
|
226
|
-
onSelect: (model) => resolve(model),
|
|
227
|
-
onCancel: () => reject(new Error("Aborted by user (Escape)."))
|
|
228
|
-
}, undefined, false, undefined, this), { exitOnCtrlC: true });
|
|
229
|
-
waitUntilExit().then(() => {
|
|
230
|
-
unmount();
|
|
231
|
-
});
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// src/server/create-app.ts
|
|
236
|
-
import express from "express";
|
|
237
|
-
|
|
238
|
-
// src/anthropic/http.ts
|
|
239
|
-
import { bytesToHex as bytesToHex3, randomBytes as randomBytes3 } from "@noble/ciphers/utils.js";
|
|
240
|
-
var ANTHROPIC_VERSION_DEFAULT = "2023-06-01";
|
|
241
|
-
var ANTHROPIC_VERSION_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
242
|
-
function isAnthropicApiVersionSupported(version) {
|
|
243
|
-
if (version === ANTHROPIC_VERSION_DEFAULT) {
|
|
244
|
-
return true;
|
|
245
|
-
}
|
|
246
|
-
return ANTHROPIC_VERSION_DATE.test(version);
|
|
247
|
-
}
|
|
248
|
-
function newAnthropicRequestId() {
|
|
249
|
-
return `req_${bytesToHex3(randomBytes3(12))}`;
|
|
250
|
-
}
|
|
251
|
-
function newAnthropicMessageId() {
|
|
252
|
-
return `msg_${bytesToHex3(randomBytes3(12))}`;
|
|
253
|
-
}
|
|
254
|
-
function extractAnthropicApiKey(req) {
|
|
255
|
-
const raw = req.headers["x-api-key"];
|
|
256
|
-
if (typeof raw === "string" && raw.length > 0) {
|
|
257
|
-
return raw;
|
|
258
|
-
}
|
|
259
|
-
if (Array.isArray(raw) && raw[0]) {
|
|
260
|
-
return raw[0];
|
|
261
|
-
}
|
|
262
|
-
const authHeader = req.headers.authorization;
|
|
263
|
-
if (!authHeader) {
|
|
264
|
-
return null;
|
|
265
|
-
}
|
|
266
|
-
if (authHeader.startsWith("Bearer ")) {
|
|
267
|
-
return authHeader.slice(7);
|
|
268
|
-
}
|
|
269
|
-
return authHeader;
|
|
270
|
-
}
|
|
271
|
-
function getAnthropicVersionHeader(req) {
|
|
272
|
-
const raw = req.headers["anthropic-version"];
|
|
273
|
-
if (typeof raw === "string" && raw.length > 0) {
|
|
274
|
-
return raw;
|
|
275
|
-
}
|
|
276
|
-
if (Array.isArray(raw) && raw[0]) {
|
|
277
|
-
return raw[0];
|
|
278
|
-
}
|
|
279
|
-
return null;
|
|
280
|
-
}
|
|
281
|
-
function resolveAnthropicVersion(req) {
|
|
282
|
-
const header = getAnthropicVersionHeader(req);
|
|
283
|
-
const version = header ?? ANTHROPIC_VERSION_DEFAULT;
|
|
284
|
-
if (!isAnthropicApiVersionSupported(version)) {
|
|
285
|
-
return {
|
|
286
|
-
ok: false,
|
|
287
|
-
message: `Unsupported anthropic-version: ${version}. Expected a dated version (YYYY-MM-DD) or ${ANTHROPIC_VERSION_DEFAULT}.`
|
|
288
|
-
};
|
|
289
|
-
}
|
|
290
|
-
return { ok: true, version };
|
|
291
|
-
}
|
|
292
|
-
function sendAnthropicHttpError(res, status, errorType, message, requestId) {
|
|
293
|
-
res.setHeader("request-id", requestId);
|
|
294
|
-
res.status(status).json({
|
|
295
|
-
type: "error",
|
|
296
|
-
error: { type: errorType, message },
|
|
297
|
-
request_id: requestId
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
function httpStatusToAnthropicErrorType(status) {
|
|
301
|
-
if (status === 401) {
|
|
302
|
-
return "authentication_error";
|
|
303
|
-
}
|
|
304
|
-
if (status === 402) {
|
|
305
|
-
return "billing_error";
|
|
306
|
-
}
|
|
307
|
-
if (status === 403) {
|
|
308
|
-
return "permission_error";
|
|
309
|
-
}
|
|
310
|
-
if (status === 404) {
|
|
311
|
-
return "not_found_error";
|
|
312
|
-
}
|
|
313
|
-
if (status === 413) {
|
|
314
|
-
return "request_too_large";
|
|
315
|
-
}
|
|
316
|
-
if (status === 429) {
|
|
317
|
-
return "rate_limit_error";
|
|
318
|
-
}
|
|
319
|
-
if (status === 504) {
|
|
320
|
-
return "timeout_error";
|
|
321
|
-
}
|
|
322
|
-
if (status === 529) {
|
|
323
|
-
return "overloaded_error";
|
|
324
|
-
}
|
|
325
|
-
if (status >= 400 && status < 500) {
|
|
326
|
-
return "invalid_request_error";
|
|
327
|
-
}
|
|
328
|
-
return "api_error";
|
|
329
|
-
}
|
|
330
|
-
function extractErrorMessage(err) {
|
|
331
|
-
if (!err || typeof err !== "object") {
|
|
332
|
-
return null;
|
|
333
|
-
}
|
|
334
|
-
const o = err;
|
|
335
|
-
if (typeof o.message === "string" && o.message.length > 0) {
|
|
336
|
-
return o.message;
|
|
337
|
-
}
|
|
338
|
-
if (typeof o.error === "string" && o.error.length > 0) {
|
|
339
|
-
return o.error;
|
|
340
|
-
}
|
|
341
|
-
if (o.error && typeof o.error === "object") {
|
|
342
|
-
const nested = o.error.message;
|
|
343
|
-
if (typeof nested === "string" && nested.length > 0) {
|
|
344
|
-
return nested;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
return null;
|
|
348
|
-
}
|
|
349
|
-
function looksLikeApiErrorResponse(err) {
|
|
350
|
-
if (!err || typeof err !== "object")
|
|
351
|
-
return false;
|
|
352
|
-
const o = err;
|
|
353
|
-
if (typeof o.status !== "number")
|
|
354
|
-
return false;
|
|
355
|
-
return "error" in o || "message" in o;
|
|
356
|
-
}
|
|
357
|
-
function mapUnknownErrorToAnthropicResponse(err, res, requestId) {
|
|
358
|
-
if (looksLikeApiErrorResponse(err)) {
|
|
359
|
-
const status = err.status >= 400 && err.status < 600 ? err.status : 500;
|
|
360
|
-
const message2 = extractErrorMessage(err) ?? "Request failed";
|
|
361
|
-
const errorType = httpStatusToAnthropicErrorType(status);
|
|
362
|
-
sendAnthropicHttpError(res, status, errorType, message2, requestId);
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
const message = extractErrorMessage(err) ?? (err instanceof Error ? err.message : "Internal server error");
|
|
366
|
-
sendAnthropicHttpError(res, 500, "api_error", message, requestId);
|
|
367
|
-
}
|
|
368
|
-
function writeAnthropicSseEvent(res, event, data) {
|
|
369
|
-
res.write(`event: ${event}
|
|
370
|
-
data: ${JSON.stringify(data)}
|
|
371
|
-
|
|
372
|
-
`);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// src/anthropic/to-openai.ts
|
|
376
|
-
class AnthropicRequestValidationError extends Error {
|
|
377
|
-
status = 400;
|
|
378
|
-
anthropicType = "invalid_request_error";
|
|
379
|
-
constructor(message) {
|
|
380
|
-
super(message);
|
|
381
|
-
this.name = "AnthropicRequestValidationError";
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
function systemToOpenAiMessages(system) {
|
|
385
|
-
if (typeof system === "string") {
|
|
386
|
-
if (system.length === 0) {
|
|
387
|
-
return [];
|
|
388
|
-
}
|
|
389
|
-
return [{ role: "system", content: system }];
|
|
390
|
-
}
|
|
391
|
-
if (Array.isArray(system)) {
|
|
392
|
-
const parts = [];
|
|
393
|
-
for (const block of system) {
|
|
394
|
-
if (block && block.type === "text" && typeof block.text === "string") {
|
|
395
|
-
parts.push(block.text);
|
|
396
|
-
} else if (block && typeof block === "object") {
|
|
397
|
-
console.warn(`[proxy] system block type "${block.type}" is not supported and will be ignored.`);
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
if (parts.length === 0) {
|
|
401
|
-
return [];
|
|
402
|
-
}
|
|
403
|
-
return [{ role: "system", content: parts.join(`
|
|
404
|
-
|
|
405
|
-
`) }];
|
|
406
|
-
}
|
|
407
|
-
if (system.type === "text" && typeof system.text === "string") {
|
|
408
|
-
return [{ role: "system", content: system.text }];
|
|
409
|
-
}
|
|
410
|
-
throw new AnthropicRequestValidationError("Invalid system parameter shape.");
|
|
411
|
-
}
|
|
412
|
-
function toolResultContentToString(content) {
|
|
413
|
-
if (typeof content === "string") {
|
|
414
|
-
return content;
|
|
415
|
-
}
|
|
416
|
-
if (content === null || content === undefined) {
|
|
417
|
-
return "";
|
|
418
|
-
}
|
|
419
|
-
if (Array.isArray(content)) {
|
|
420
|
-
const parts = [];
|
|
421
|
-
for (const block of content) {
|
|
422
|
-
if (block && typeof block === "object" && "type" in block && block.type === "text" && typeof block.text === "string") {
|
|
423
|
-
parts.push(block.text);
|
|
424
|
-
} else {
|
|
425
|
-
parts.push(JSON.stringify(block));
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
return parts.join(`
|
|
429
|
-
`);
|
|
430
|
-
}
|
|
431
|
-
return JSON.stringify(content);
|
|
432
|
-
}
|
|
433
|
-
function anthropicImageBlockToOpenAIPart(part) {
|
|
434
|
-
const source = part.source;
|
|
435
|
-
if (!source || typeof source !== "object") {
|
|
436
|
-
return null;
|
|
437
|
-
}
|
|
438
|
-
const s = source;
|
|
439
|
-
if (s.type === "base64" && typeof s.data === "string" && s.data.length > 0) {
|
|
440
|
-
const mediaType = typeof s.media_type === "string" && s.media_type.length > 0 ? s.media_type : "image/png";
|
|
441
|
-
return {
|
|
442
|
-
type: "image_url",
|
|
443
|
-
image_url: { url: `data:${mediaType};base64,${s.data}` }
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
if (s.type === "url" && typeof s.url === "string" && s.url.length > 0) {
|
|
447
|
-
return { type: "image_url", image_url: { url: s.url } };
|
|
448
|
-
}
|
|
449
|
-
return null;
|
|
450
|
-
}
|
|
451
|
-
function anthropicUserContentToOpenAIMessages(content) {
|
|
452
|
-
if (typeof content === "string") {
|
|
453
|
-
return [{ role: "user", content }];
|
|
454
|
-
}
|
|
455
|
-
const out = [];
|
|
456
|
-
const partsBuf = [];
|
|
457
|
-
const flushParts = () => {
|
|
458
|
-
if (partsBuf.length === 0) {
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
461
|
-
if (partsBuf.length === 1 && partsBuf[0].type === "text") {
|
|
462
|
-
out.push({ role: "user", content: partsBuf[0].text });
|
|
463
|
-
} else {
|
|
464
|
-
out.push({ role: "user", content: [...partsBuf] });
|
|
465
|
-
}
|
|
466
|
-
partsBuf.length = 0;
|
|
467
|
-
};
|
|
468
|
-
for (const part of content) {
|
|
469
|
-
if (!part || typeof part !== "object") {
|
|
470
|
-
throw new AnthropicRequestValidationError("Invalid message content entry.");
|
|
471
|
-
}
|
|
472
|
-
if (part.type === "text" && typeof part.text === "string") {
|
|
473
|
-
partsBuf.push({
|
|
474
|
-
type: "text",
|
|
475
|
-
text: part.text
|
|
476
|
-
});
|
|
477
|
-
continue;
|
|
478
|
-
}
|
|
479
|
-
if (part.type === "image") {
|
|
480
|
-
const imgPart = anthropicImageBlockToOpenAIPart(part);
|
|
481
|
-
if (imgPart) {
|
|
482
|
-
partsBuf.push(imgPart);
|
|
483
|
-
}
|
|
484
|
-
continue;
|
|
485
|
-
}
|
|
486
|
-
if (part.type === "tool_result") {
|
|
487
|
-
flushParts();
|
|
488
|
-
const id = part.tool_use_id;
|
|
489
|
-
const rawContent = part.content;
|
|
490
|
-
if (typeof id !== "string" || id.length === 0) {
|
|
491
|
-
throw new AnthropicRequestValidationError("tool_result blocks require a non-empty tool_use_id.");
|
|
492
|
-
}
|
|
493
|
-
out.push({
|
|
494
|
-
role: "tool",
|
|
495
|
-
tool_call_id: id,
|
|
496
|
-
content: toolResultContentToString(rawContent)
|
|
497
|
-
});
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
flushParts();
|
|
501
|
-
return out;
|
|
502
|
-
}
|
|
503
|
-
function anthropicAssistantContentToOpenAI(content) {
|
|
504
|
-
if (typeof content === "string") {
|
|
505
|
-
return { role: "assistant", content };
|
|
506
|
-
}
|
|
507
|
-
const textParts = [];
|
|
508
|
-
const toolCalls = [];
|
|
509
|
-
for (const part of content) {
|
|
510
|
-
if (!part || typeof part !== "object") {
|
|
511
|
-
throw new AnthropicRequestValidationError("Invalid message content entry.");
|
|
512
|
-
}
|
|
513
|
-
if (part.type === "text" && typeof part.text === "string") {
|
|
514
|
-
textParts.push(part.text);
|
|
515
|
-
continue;
|
|
516
|
-
}
|
|
517
|
-
if (part.type === "tool_use") {
|
|
518
|
-
const p = part;
|
|
519
|
-
if (typeof p.id !== "string" || p.id.length === 0) {
|
|
520
|
-
throw new AnthropicRequestValidationError("tool_use blocks require a non-empty id.");
|
|
521
|
-
}
|
|
522
|
-
if (typeof p.name !== "string" || p.name.length === 0) {
|
|
523
|
-
throw new AnthropicRequestValidationError("tool_use blocks require a non-empty name.");
|
|
524
|
-
}
|
|
525
|
-
const args = typeof p.input === "string" ? p.input : JSON.stringify(p.input ?? {});
|
|
526
|
-
toolCalls.push({
|
|
527
|
-
id: p.id,
|
|
528
|
-
type: "function",
|
|
529
|
-
function: { name: p.name, arguments: args }
|
|
530
|
-
});
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
const msg = {
|
|
534
|
-
role: "assistant",
|
|
535
|
-
content: textParts.length > 0 ? textParts.join(`
|
|
536
|
-
`) : null
|
|
537
|
-
};
|
|
538
|
-
if (toolCalls.length > 0) {
|
|
539
|
-
msg.tool_calls = toolCalls;
|
|
540
|
-
}
|
|
541
|
-
return msg;
|
|
542
|
-
}
|
|
543
|
-
function anthropicToolsToOpenAI(tools) {
|
|
544
|
-
if (tools === undefined) {
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
if (!Array.isArray(tools)) {
|
|
548
|
-
throw new AnthropicRequestValidationError("tools must be an array.");
|
|
549
|
-
}
|
|
550
|
-
const out = [];
|
|
551
|
-
for (const t of tools) {
|
|
552
|
-
if (!t || typeof t !== "object") {
|
|
553
|
-
throw new AnthropicRequestValidationError("Invalid tool entry.");
|
|
554
|
-
}
|
|
555
|
-
const name = t.name;
|
|
556
|
-
const desc = t.description;
|
|
557
|
-
const schema = t.input_schema;
|
|
558
|
-
if (typeof name !== "string" || name.length === 0) {
|
|
559
|
-
throw new AnthropicRequestValidationError("Each tool must include a non-empty name.");
|
|
560
|
-
}
|
|
561
|
-
if (schema !== undefined && (typeof schema !== "object" || schema === null)) {
|
|
562
|
-
throw new AnthropicRequestValidationError("tool input_schema must be an object when provided.");
|
|
563
|
-
}
|
|
564
|
-
out.push({
|
|
565
|
-
type: "function",
|
|
566
|
-
function: {
|
|
567
|
-
name,
|
|
568
|
-
...typeof desc === "string" ? { description: desc } : {},
|
|
569
|
-
parameters: schema ?? {
|
|
570
|
-
type: "object",
|
|
571
|
-
properties: {}
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
});
|
|
575
|
-
}
|
|
576
|
-
return out;
|
|
577
|
-
}
|
|
578
|
-
function anthropicToolChoiceToOpenAI(toolChoice) {
|
|
579
|
-
if (toolChoice === undefined) {
|
|
580
|
-
return;
|
|
581
|
-
}
|
|
582
|
-
if (typeof toolChoice !== "object" || toolChoice === null || !("type" in toolChoice)) {
|
|
583
|
-
throw new AnthropicRequestValidationError("Invalid tool_choice shape.");
|
|
584
|
-
}
|
|
585
|
-
const tc = toolChoice;
|
|
586
|
-
switch (tc.type) {
|
|
587
|
-
case "auto":
|
|
588
|
-
return "auto";
|
|
589
|
-
case "none":
|
|
590
|
-
return "none";
|
|
591
|
-
case "any":
|
|
592
|
-
return "required";
|
|
593
|
-
case "tool": {
|
|
594
|
-
if (typeof tc.name !== "string" || tc.name.length === 0) {
|
|
595
|
-
throw new AnthropicRequestValidationError('tool_choice type "tool" requires a non-empty name.');
|
|
596
|
-
}
|
|
597
|
-
return { type: "function", function: { name: tc.name } };
|
|
598
|
-
}
|
|
599
|
-
default:
|
|
600
|
-
throw new AnthropicRequestValidationError(`Unsupported tool_choice type "${tc.type}".`);
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
function anthropicMessagesCreateToOpenAI(body) {
|
|
604
|
-
if (typeof body.model !== "string" || !body.model) {
|
|
605
|
-
throw new AnthropicRequestValidationError("model is required.");
|
|
606
|
-
}
|
|
607
|
-
if (typeof body.max_tokens !== "number" || !Number.isFinite(body.max_tokens)) {
|
|
608
|
-
throw new AnthropicRequestValidationError("max_tokens is required and must be a number.");
|
|
609
|
-
}
|
|
610
|
-
if (!Array.isArray(body.messages)) {
|
|
611
|
-
throw new AnthropicRequestValidationError("messages must be an array.");
|
|
612
|
-
}
|
|
613
|
-
const messages = [];
|
|
614
|
-
if (body.system !== undefined) {
|
|
615
|
-
messages.push(...systemToOpenAiMessages(body.system));
|
|
616
|
-
}
|
|
617
|
-
for (const m of body.messages) {
|
|
618
|
-
if (m.role !== "user" && m.role !== "assistant") {
|
|
619
|
-
throw new AnthropicRequestValidationError(`Invalid message role "${m.role}".`);
|
|
620
|
-
}
|
|
621
|
-
if (m.role === "user") {
|
|
622
|
-
messages.push(...anthropicUserContentToOpenAIMessages(m.content));
|
|
623
|
-
} else {
|
|
624
|
-
messages.push(anthropicAssistantContentToOpenAI(m.content));
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
const isStreaming = Boolean(body.stream);
|
|
628
|
-
const params = {
|
|
629
|
-
model: body.model,
|
|
630
|
-
messages,
|
|
631
|
-
max_tokens: body.max_tokens,
|
|
632
|
-
stream: isStreaming
|
|
633
|
-
};
|
|
634
|
-
if (isStreaming) {
|
|
635
|
-
params.stream_options = { include_usage: true };
|
|
636
|
-
}
|
|
637
|
-
const tools = anthropicToolsToOpenAI(body.tools);
|
|
638
|
-
if (tools !== undefined && tools.length > 0) {
|
|
639
|
-
params.tools = tools;
|
|
640
|
-
}
|
|
641
|
-
const toolChoice = anthropicToolChoiceToOpenAI(body.tool_choice);
|
|
642
|
-
if (toolChoice !== undefined) {
|
|
643
|
-
params.tool_choice = toolChoice;
|
|
644
|
-
}
|
|
645
|
-
if (body.stop_sequences !== undefined) {
|
|
646
|
-
if (!Array.isArray(body.stop_sequences) || !body.stop_sequences.every((s) => typeof s === "string")) {
|
|
647
|
-
throw new AnthropicRequestValidationError("stop_sequences must be an array of strings.");
|
|
648
|
-
}
|
|
649
|
-
params.stop = body.stop_sequences;
|
|
650
|
-
}
|
|
651
|
-
if (typeof body.temperature === "number") {
|
|
652
|
-
params.temperature = body.temperature;
|
|
653
|
-
}
|
|
654
|
-
if (typeof body.top_p === "number") {
|
|
655
|
-
params.top_p = body.top_p;
|
|
656
|
-
}
|
|
657
|
-
if (typeof body.top_k === "number") {
|
|
658
|
-
console.warn("[proxy] top_k is not supported by the OpenAI API and will be ignored.");
|
|
659
|
-
}
|
|
660
|
-
return params;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
// src/anthropic/count-tokens-route.ts
|
|
664
|
-
function extractTextCharCount(body) {
|
|
665
|
-
let len = 0;
|
|
666
|
-
if (typeof body.system === "string") {
|
|
667
|
-
len += body.system.length;
|
|
668
|
-
} else if (Array.isArray(body.system)) {
|
|
669
|
-
for (const block of body.system) {
|
|
670
|
-
if (block && block.type === "text" && typeof block.text === "string") {
|
|
671
|
-
len += block.text.length;
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
} else if (body.system && typeof body.system === "object" && body.system.type === "text") {
|
|
675
|
-
len += body.system.text.length;
|
|
676
|
-
}
|
|
677
|
-
for (const msg of body.messages) {
|
|
678
|
-
if (typeof msg.content === "string") {
|
|
679
|
-
len += msg.content.length;
|
|
680
|
-
} else if (Array.isArray(msg.content)) {
|
|
681
|
-
for (const part of msg.content) {
|
|
682
|
-
if (!part || typeof part !== "object")
|
|
683
|
-
continue;
|
|
684
|
-
if (part.type === "text" && typeof part.text === "string") {
|
|
685
|
-
len += part.text.length;
|
|
686
|
-
} else if (part.type === "tool_result") {
|
|
687
|
-
const c = part.content;
|
|
688
|
-
if (typeof c === "string") {
|
|
689
|
-
len += c.length;
|
|
690
|
-
}
|
|
691
|
-
}
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
|
-
}
|
|
695
|
-
if (Array.isArray(body.tools)) {
|
|
696
|
-
len += JSON.stringify(body.tools).length;
|
|
697
|
-
}
|
|
698
|
-
return len;
|
|
699
|
-
}
|
|
700
|
-
function registerAnthropicCountTokensRoute(router, _deps) {
|
|
701
|
-
router.post("/v1/messages/count_tokens", async (req, res) => {
|
|
702
|
-
const requestId = newAnthropicRequestId();
|
|
703
|
-
res.setHeader("request-id", requestId);
|
|
704
|
-
const versionResult = resolveAnthropicVersion(req);
|
|
705
|
-
if (!versionResult.ok) {
|
|
706
|
-
return sendAnthropicHttpError(res, 400, "invalid_request_error", versionResult.message, requestId);
|
|
707
|
-
}
|
|
708
|
-
const apiKey = extractAnthropicApiKey(req);
|
|
709
|
-
if (!apiKey) {
|
|
710
|
-
return sendAnthropicHttpError(res, 401, "authentication_error", "Missing x-api-key header (or Authorization with API key).", requestId);
|
|
711
|
-
}
|
|
712
|
-
try {
|
|
713
|
-
const raw = req.body;
|
|
714
|
-
const body = {
|
|
715
|
-
...raw,
|
|
716
|
-
max_tokens: typeof raw.max_tokens === "number" && Number.isFinite(raw.max_tokens) ? raw.max_tokens : 4096,
|
|
717
|
-
stream: false
|
|
718
|
-
};
|
|
719
|
-
anthropicMessagesCreateToOpenAI(body);
|
|
720
|
-
const input_tokens = Math.max(1, Math.ceil(extractTextCharCount(body) / 4));
|
|
721
|
-
res.json({ input_tokens });
|
|
722
|
-
} catch (err) {
|
|
723
|
-
if (err instanceof AnthropicRequestValidationError) {
|
|
724
|
-
return sendAnthropicHttpError(res, err.status, err.anthropicType, err.message, requestId);
|
|
725
|
-
}
|
|
726
|
-
mapUnknownErrorToAnthropicResponse(err, res, requestId);
|
|
727
|
-
}
|
|
728
|
-
});
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
// src/anthropic/from-openai.ts
|
|
732
|
-
function openAiFinishReasonToAnthropic(finish) {
|
|
733
|
-
if (!finish) {
|
|
734
|
-
return { stop_reason: null, stop_sequence: null };
|
|
735
|
-
}
|
|
736
|
-
switch (finish) {
|
|
737
|
-
case "stop":
|
|
738
|
-
return { stop_reason: "end_turn", stop_sequence: null };
|
|
739
|
-
case "length":
|
|
740
|
-
return { stop_reason: "max_tokens", stop_sequence: null };
|
|
741
|
-
case "tool_calls":
|
|
742
|
-
return { stop_reason: "tool_use", stop_sequence: null };
|
|
743
|
-
case "content_filter":
|
|
744
|
-
return { stop_reason: "refusal", stop_sequence: null };
|
|
745
|
-
default:
|
|
746
|
-
return { stop_reason: "end_turn", stop_sequence: null };
|
|
747
|
-
}
|
|
748
|
-
}
|
|
749
|
-
function extractTextFromAssistantContent(content) {
|
|
750
|
-
if (content == null) {
|
|
751
|
-
return "";
|
|
752
|
-
}
|
|
753
|
-
if (typeof content === "string") {
|
|
754
|
-
return content;
|
|
755
|
-
}
|
|
756
|
-
if (!Array.isArray(content)) {
|
|
757
|
-
return "";
|
|
758
|
-
}
|
|
759
|
-
const parts = [];
|
|
760
|
-
for (const p of content) {
|
|
761
|
-
if (typeof p === "string") {
|
|
762
|
-
parts.push(p);
|
|
763
|
-
continue;
|
|
764
|
-
}
|
|
765
|
-
if (p && typeof p === "object" && "type" in p && p.type === "text" && "text" in p) {
|
|
766
|
-
parts.push(String(p.text));
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
return parts.join("");
|
|
770
|
-
}
|
|
771
|
-
function openAIChatCompletionToAnthropicMessage(completion, requestModel) {
|
|
772
|
-
const choice = completion.choices[0];
|
|
773
|
-
const message = choice?.message;
|
|
774
|
-
const contentText = message ? extractTextFromAssistantContent(message.content) : "";
|
|
775
|
-
const content = [];
|
|
776
|
-
if (contentText.length > 0) {
|
|
777
|
-
content.push({ type: "text", text: contentText });
|
|
778
|
-
}
|
|
779
|
-
if (message?.tool_calls?.length) {
|
|
780
|
-
for (const tc of message.tool_calls) {
|
|
781
|
-
if (tc.type !== "function") {
|
|
782
|
-
continue;
|
|
783
|
-
}
|
|
784
|
-
let input = {};
|
|
785
|
-
try {
|
|
786
|
-
input = JSON.parse(tc.function.arguments || "{}");
|
|
787
|
-
} catch {
|
|
788
|
-
input = { _raw_arguments: tc.function.arguments ?? "" };
|
|
789
|
-
}
|
|
790
|
-
content.push({
|
|
791
|
-
type: "tool_use",
|
|
792
|
-
id: tc.id,
|
|
793
|
-
name: tc.function.name,
|
|
794
|
-
input
|
|
795
|
-
});
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
if (content.length === 0) {
|
|
799
|
-
content.push({ type: "text", text: "" });
|
|
800
|
-
}
|
|
801
|
-
const { stop_reason, stop_sequence } = openAiFinishReasonToAnthropic(choice?.finish_reason);
|
|
802
|
-
const u = completion.usage;
|
|
803
|
-
const usage = {
|
|
804
|
-
input_tokens: u?.prompt_tokens ?? 0,
|
|
805
|
-
output_tokens: u?.completion_tokens ?? 0
|
|
806
|
-
};
|
|
807
|
-
return {
|
|
808
|
-
id: newAnthropicMessageId(),
|
|
809
|
-
type: "message",
|
|
810
|
-
role: "assistant",
|
|
811
|
-
content,
|
|
812
|
-
model: requestModel,
|
|
813
|
-
stop_reason,
|
|
814
|
-
stop_sequence,
|
|
815
|
-
usage
|
|
816
|
-
};
|
|
817
|
-
}
|
|
818
|
-
function chunkFinishToAnthropic(finish) {
|
|
819
|
-
if (!finish) {
|
|
820
|
-
return null;
|
|
821
|
-
}
|
|
822
|
-
return openAiFinishReasonToAnthropic(finish).stop_reason;
|
|
823
|
-
}
|
|
824
|
-
async function pipeOpenAIChunkStreamToAnthropicSse(res, stream, options) {
|
|
825
|
-
const { anthropicModel, messageId } = options;
|
|
826
|
-
let textBlockOpen = false;
|
|
827
|
-
let inputTokens = 0;
|
|
828
|
-
let outputTokens = 0;
|
|
829
|
-
let stopReason = null;
|
|
830
|
-
const toolStates = new Map;
|
|
831
|
-
let nextAnthropicIndex = 0;
|
|
832
|
-
let textBlockIndex = null;
|
|
833
|
-
writeAnthropicSseEvent(res, "message_start", {
|
|
834
|
-
type: "message_start",
|
|
835
|
-
message: {
|
|
836
|
-
id: messageId,
|
|
837
|
-
type: "message",
|
|
838
|
-
role: "assistant",
|
|
839
|
-
content: [],
|
|
840
|
-
model: anthropicModel,
|
|
841
|
-
stop_reason: null,
|
|
842
|
-
stop_sequence: null,
|
|
843
|
-
usage: { input_tokens: inputTokens, output_tokens: outputTokens }
|
|
844
|
-
}
|
|
845
|
-
});
|
|
846
|
-
const ensureTextBlock = () => {
|
|
847
|
-
if (textBlockOpen) {
|
|
848
|
-
return;
|
|
849
|
-
}
|
|
850
|
-
textBlockIndex = nextAnthropicIndex++;
|
|
851
|
-
textBlockOpen = true;
|
|
852
|
-
writeAnthropicSseEvent(res, "content_block_start", {
|
|
853
|
-
type: "content_block_start",
|
|
854
|
-
index: textBlockIndex,
|
|
855
|
-
content_block: { type: "text", text: "" }
|
|
856
|
-
});
|
|
857
|
-
};
|
|
858
|
-
const closeTextBlockIfOpen = () => {
|
|
859
|
-
if (!textBlockOpen || textBlockIndex === null) {
|
|
860
|
-
return;
|
|
861
|
-
}
|
|
862
|
-
writeAnthropicSseEvent(res, "content_block_stop", {
|
|
863
|
-
type: "content_block_stop",
|
|
864
|
-
index: textBlockIndex
|
|
865
|
-
});
|
|
866
|
-
textBlockOpen = false;
|
|
867
|
-
};
|
|
868
|
-
const getOrCreateTool = (openAiIdx) => {
|
|
869
|
-
let st = toolStates.get(openAiIdx);
|
|
870
|
-
if (!st) {
|
|
871
|
-
st = {
|
|
872
|
-
anthropicIndex: nextAnthropicIndex++,
|
|
873
|
-
id: "",
|
|
874
|
-
name: "",
|
|
875
|
-
lastArgs: "",
|
|
876
|
-
argsEmittedLen: 0,
|
|
877
|
-
started: false,
|
|
878
|
-
stopped: false
|
|
879
|
-
};
|
|
880
|
-
toolStates.set(openAiIdx, st);
|
|
881
|
-
}
|
|
882
|
-
return st;
|
|
883
|
-
};
|
|
884
|
-
const flushToolArgs = (st) => {
|
|
885
|
-
if (!st.started || st.lastArgs.length <= st.argsEmittedLen) {
|
|
886
|
-
return;
|
|
887
|
-
}
|
|
888
|
-
const partial = st.lastArgs.slice(st.argsEmittedLen);
|
|
889
|
-
st.argsEmittedLen = st.lastArgs.length;
|
|
890
|
-
writeAnthropicSseEvent(res, "content_block_delta", {
|
|
891
|
-
type: "content_block_delta",
|
|
892
|
-
index: st.anthropicIndex,
|
|
893
|
-
delta: {
|
|
894
|
-
type: "input_json_delta",
|
|
895
|
-
partial_json: partial
|
|
896
|
-
}
|
|
897
|
-
});
|
|
898
|
-
};
|
|
899
|
-
try {
|
|
900
|
-
for await (const chunk of stream) {
|
|
901
|
-
if (chunk.usage) {
|
|
902
|
-
const u = chunk.usage;
|
|
903
|
-
inputTokens = u.prompt_tokens ?? inputTokens;
|
|
904
|
-
outputTokens = u.completion_tokens ?? outputTokens;
|
|
905
|
-
}
|
|
906
|
-
const choice = chunk.choices?.[0];
|
|
907
|
-
if (!choice) {
|
|
908
|
-
continue;
|
|
909
|
-
}
|
|
910
|
-
const delta = choice.delta;
|
|
911
|
-
if (typeof delta?.content === "string" && delta.content.length > 0) {
|
|
912
|
-
ensureTextBlock();
|
|
913
|
-
if (textBlockIndex !== null) {
|
|
914
|
-
writeAnthropicSseEvent(res, "content_block_delta", {
|
|
915
|
-
type: "content_block_delta",
|
|
916
|
-
index: textBlockIndex,
|
|
917
|
-
delta: { type: "text_delta", text: delta.content }
|
|
918
|
-
});
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
if (delta?.tool_calls?.length) {
|
|
922
|
-
closeTextBlockIfOpen();
|
|
923
|
-
for (const tc of delta.tool_calls) {
|
|
924
|
-
const idx = typeof tc.index === "number" && Number.isFinite(tc.index) ? tc.index : 0;
|
|
925
|
-
const st = getOrCreateTool(idx);
|
|
926
|
-
if (typeof tc.id === "string" && tc.id.length > 0) {
|
|
927
|
-
st.id = tc.id;
|
|
928
|
-
}
|
|
929
|
-
const fn = tc.function;
|
|
930
|
-
if (fn?.name && fn.name.length > 0) {
|
|
931
|
-
st.name = fn.name;
|
|
932
|
-
}
|
|
933
|
-
if (typeof fn?.arguments === "string") {
|
|
934
|
-
st.lastArgs += fn.arguments;
|
|
935
|
-
}
|
|
936
|
-
if (!st.started && st.id.length > 0 && st.name.length > 0) {
|
|
937
|
-
writeAnthropicSseEvent(res, "content_block_start", {
|
|
938
|
-
type: "content_block_start",
|
|
939
|
-
index: st.anthropicIndex,
|
|
940
|
-
content_block: {
|
|
941
|
-
type: "tool_use",
|
|
942
|
-
id: st.id,
|
|
943
|
-
name: st.name
|
|
944
|
-
}
|
|
945
|
-
});
|
|
946
|
-
st.started = true;
|
|
947
|
-
}
|
|
948
|
-
if (st.started) {
|
|
949
|
-
flushToolArgs(st);
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
if (choice.finish_reason) {
|
|
954
|
-
const mapped = chunkFinishToAnthropic(choice.finish_reason);
|
|
955
|
-
if (mapped) {
|
|
956
|
-
stopReason = mapped;
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
}
|
|
960
|
-
closeTextBlockIfOpen();
|
|
961
|
-
const sortedTools = [...toolStates.values()].sort((a, b) => a.anthropicIndex - b.anthropicIndex);
|
|
962
|
-
for (const st of sortedTools) {
|
|
963
|
-
if (st.started && !st.stopped) {
|
|
964
|
-
writeAnthropicSseEvent(res, "content_block_stop", {
|
|
965
|
-
type: "content_block_stop",
|
|
966
|
-
index: st.anthropicIndex
|
|
967
|
-
});
|
|
968
|
-
st.stopped = true;
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
writeAnthropicSseEvent(res, "message_delta", {
|
|
972
|
-
type: "message_delta",
|
|
973
|
-
delta: { stop_reason: stopReason, stop_sequence: null },
|
|
974
|
-
usage: {
|
|
975
|
-
input_tokens: inputTokens,
|
|
976
|
-
output_tokens: outputTokens
|
|
977
|
-
}
|
|
978
|
-
});
|
|
979
|
-
writeAnthropicSseEvent(res, "message_stop", { type: "message_stop" });
|
|
980
|
-
res.end();
|
|
981
|
-
} catch (err) {
|
|
982
|
-
const message = err instanceof Error ? err.message : "Stream error";
|
|
983
|
-
writeAnthropicSseEvent(res, "error", {
|
|
984
|
-
type: "error",
|
|
985
|
-
error: { type: "api_error", message }
|
|
986
|
-
});
|
|
987
|
-
res.end();
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
// src/anthropic/messages-route.ts
|
|
992
|
-
function registerAnthropicMessagesRoute(router, deps) {
|
|
993
|
-
router.post("/v1/messages", async (req, res) => {
|
|
994
|
-
const requestId = newAnthropicRequestId();
|
|
995
|
-
res.setHeader("request-id", requestId);
|
|
996
|
-
const versionResult = resolveAnthropicVersion(req);
|
|
997
|
-
if (!versionResult.ok) {
|
|
998
|
-
return sendAnthropicHttpError(res, 400, "invalid_request_error", versionResult.message, requestId);
|
|
999
|
-
}
|
|
1000
|
-
const apiKey = extractAnthropicApiKey(req);
|
|
1001
|
-
if (!apiKey) {
|
|
1002
|
-
return sendAnthropicHttpError(res, 401, "authentication_error", "Missing x-api-key header (or Authorization with API key).", requestId);
|
|
1003
|
-
}
|
|
1004
|
-
try {
|
|
1005
|
-
const body = req.body;
|
|
1006
|
-
const openaiParams = anthropicMessagesCreateToOpenAI(body);
|
|
1007
|
-
const client = await deps.getOrCreateClient(apiKey);
|
|
1008
|
-
const completion = await client.chat.completions.create(openaiParams);
|
|
1009
|
-
if (body.stream) {
|
|
1010
|
-
res.status(200);
|
|
1011
|
-
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
|
1012
|
-
res.setHeader("Cache-Control", "no-cache");
|
|
1013
|
-
res.setHeader("Connection", "keep-alive");
|
|
1014
|
-
if (completion && typeof completion === "object" && Symbol.asyncIterator in completion) {
|
|
1015
|
-
const messageId = newAnthropicMessageId();
|
|
1016
|
-
await pipeOpenAIChunkStreamToAnthropicSse(res, completion, {
|
|
1017
|
-
anthropicModel: body.model,
|
|
1018
|
-
messageId
|
|
1019
|
-
});
|
|
1020
|
-
} else {
|
|
1021
|
-
sendAnthropicHttpError(res, 500, "api_error", "Expected streamed completion", requestId);
|
|
1022
|
-
}
|
|
1023
|
-
return;
|
|
1024
|
-
}
|
|
1025
|
-
const message = openAIChatCompletionToAnthropicMessage(completion, body.model);
|
|
1026
|
-
res.json(message);
|
|
1027
|
-
} catch (err) {
|
|
1028
|
-
if (err instanceof AnthropicRequestValidationError) {
|
|
1029
|
-
return sendAnthropicHttpError(res, err.status, err.anthropicType, err.message, requestId);
|
|
1030
|
-
}
|
|
1031
|
-
mapUnknownErrorToAnthropicResponse(err, res, requestId);
|
|
1032
|
-
}
|
|
1033
|
-
});
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
// src/anthropic/models-route.ts
|
|
1037
|
-
function toAnthropicModel(model) {
|
|
1038
|
-
return {
|
|
1039
|
-
type: "model",
|
|
1040
|
-
id: model.model,
|
|
1041
|
-
display_name: model.name || model.model,
|
|
1042
|
-
created_at: model.created_at
|
|
1043
|
-
};
|
|
1044
|
-
}
|
|
1045
|
-
function filterEnabled(models) {
|
|
1046
|
-
return models.filter((m) => m.enabled !== 0);
|
|
1047
|
-
}
|
|
1048
|
-
function parseLimit(raw) {
|
|
1049
|
-
if (typeof raw !== "string" || raw.length === 0) {
|
|
1050
|
-
return 20;
|
|
1051
|
-
}
|
|
1052
|
-
const n = Number.parseInt(raw, 10);
|
|
1053
|
-
if (!Number.isFinite(n) || n <= 0) {
|
|
1054
|
-
return 20;
|
|
1055
|
-
}
|
|
1056
|
-
return Math.min(n, 1000);
|
|
1057
|
-
}
|
|
1058
|
-
function paginate(all, beforeId, afterId, limit) {
|
|
1059
|
-
let start = 0;
|
|
1060
|
-
let end = all.length;
|
|
1061
|
-
if (afterId) {
|
|
1062
|
-
const idx = all.findIndex((m) => m.id === afterId);
|
|
1063
|
-
if (idx >= 0) {
|
|
1064
|
-
start = idx + 1;
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
if (beforeId) {
|
|
1068
|
-
const idx = all.findIndex((m) => m.id === beforeId);
|
|
1069
|
-
if (idx >= 0) {
|
|
1070
|
-
end = idx;
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
const window = all.slice(start, end);
|
|
1074
|
-
const items = window.slice(0, limit);
|
|
1075
|
-
return { items, hasMore: window.length > items.length };
|
|
1076
|
-
}
|
|
1077
|
-
function registerAnthropicModelsRoute(router, deps) {
|
|
1078
|
-
router.get("/v1/models", async (req, res) => {
|
|
1079
|
-
const requestId = newAnthropicRequestId();
|
|
1080
|
-
res.setHeader("request-id", requestId);
|
|
1081
|
-
const versionResult = resolveAnthropicVersion(req);
|
|
1082
|
-
if (!versionResult.ok) {
|
|
1083
|
-
return sendAnthropicHttpError(res, 400, "invalid_request_error", versionResult.message, requestId);
|
|
1084
|
-
}
|
|
1085
|
-
const apiKey = extractAnthropicApiKey(req);
|
|
1086
|
-
if (!apiKey) {
|
|
1087
|
-
return sendAnthropicHttpError(res, 401, "authentication_error", "Missing x-api-key header (or Authorization with API key).", requestId);
|
|
1088
|
-
}
|
|
1089
|
-
try {
|
|
1090
|
-
const client = await deps.getOrCreateClient(apiKey);
|
|
1091
|
-
const type = typeof req.query.type === "string" ? req.query.type : undefined;
|
|
1092
|
-
const all = filterEnabled(await client.models.list({ type })).map(toAnthropicModel);
|
|
1093
|
-
const beforeId = typeof req.query.before_id === "string" ? req.query.before_id : undefined;
|
|
1094
|
-
const afterId = typeof req.query.after_id === "string" ? req.query.after_id : undefined;
|
|
1095
|
-
const limit = parseLimit(req.query.limit);
|
|
1096
|
-
const { items, hasMore } = paginate(all, beforeId, afterId, limit);
|
|
1097
|
-
res.json({
|
|
1098
|
-
data: items,
|
|
1099
|
-
first_id: items.length > 0 ? items[0].id : null,
|
|
1100
|
-
last_id: items.length > 0 ? items[items.length - 1].id : null,
|
|
1101
|
-
has_more: hasMore
|
|
1102
|
-
});
|
|
1103
|
-
} catch (err) {
|
|
1104
|
-
mapUnknownErrorToAnthropicResponse(err, res, requestId);
|
|
1105
|
-
}
|
|
1106
|
-
});
|
|
1107
|
-
router.get("/v1/models/:model_id", async (req, res) => {
|
|
1108
|
-
const requestId = newAnthropicRequestId();
|
|
1109
|
-
res.setHeader("request-id", requestId);
|
|
1110
|
-
const versionResult = resolveAnthropicVersion(req);
|
|
1111
|
-
if (!versionResult.ok) {
|
|
1112
|
-
return sendAnthropicHttpError(res, 400, "invalid_request_error", versionResult.message, requestId);
|
|
1113
|
-
}
|
|
1114
|
-
const apiKey = extractAnthropicApiKey(req);
|
|
1115
|
-
if (!apiKey) {
|
|
1116
|
-
return sendAnthropicHttpError(res, 401, "authentication_error", "Missing x-api-key header (or Authorization with API key).", requestId);
|
|
1117
|
-
}
|
|
1118
|
-
const modelId = req.params.model_id;
|
|
1119
|
-
if (!modelId) {
|
|
1120
|
-
return sendAnthropicHttpError(res, 400, "invalid_request_error", "Missing model id.", requestId);
|
|
1121
|
-
}
|
|
1122
|
-
try {
|
|
1123
|
-
const client = await deps.getOrCreateClient(apiKey);
|
|
1124
|
-
const found = filterEnabled(await client.models.list()).find((m) => m.model === modelId);
|
|
1125
|
-
if (!found) {
|
|
1126
|
-
return sendAnthropicHttpError(res, 404, "not_found_error", `Model "${modelId}" not found.`, requestId);
|
|
1127
|
-
}
|
|
1128
|
-
res.json(toAnthropicModel(found));
|
|
1129
|
-
} catch (err) {
|
|
1130
|
-
mapUnknownErrorToAnthropicResponse(err, res, requestId);
|
|
1131
|
-
}
|
|
1132
|
-
});
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
// src/server/runtime.ts
|
|
1136
|
-
import multer from "multer";
|
|
1137
|
-
|
|
1138
|
-
// src/audio/index.ts
|
|
1139
|
-
import { bytesToHex as bytesToHex4, hexToBytes as hexToBytes3 } from "@noble/ciphers/utils.js";
|
|
1140
|
-
|
|
1141
|
-
// src/utils/attestation.ts
|
|
1142
|
-
var cachedPrem;
|
|
1143
|
-
async function loadPrem() {
|
|
1144
|
-
if (cachedPrem)
|
|
1145
|
-
return cachedPrem;
|
|
1146
|
-
const isBare = typeof globalThis.Bare !== "undefined";
|
|
1147
|
-
if (isBare) {
|
|
1148
|
-
cachedPrem = await (async (s, y) => await import(s, y))("@premai/reticle", { with: { type: "script" } });
|
|
1149
|
-
return cachedPrem;
|
|
1150
|
-
}
|
|
1151
|
-
cachedPrem = await import("@premai/reticle");
|
|
1152
|
-
return cachedPrem;
|
|
1153
|
-
}
|
|
1154
|
-
function isAttestationError(err) {
|
|
1155
|
-
return err instanceof Error && err.name === "AttestationError";
|
|
1156
|
-
}
|
|
1157
|
-
var ATTEST_TTL_MS = 30000;
|
|
1158
|
-
var ATTEST_CACHE_MAX = 500;
|
|
1159
|
-
var ATTEST_MAX_ATTEMPTS = 4;
|
|
1160
|
-
var ATTEST_RETRY_BASE_MS = 250;
|
|
1161
|
-
var ATTEST_RETRY_MAX_MS = 2000;
|
|
1162
|
-
var TRANSIENT_PATTERNS = [
|
|
1163
|
-
/EOF while parsing/i,
|
|
1164
|
-
/error decoding response body/i,
|
|
1165
|
-
/connection (reset|closed|refused)/i,
|
|
1166
|
-
/socket hang up/i,
|
|
1167
|
-
/ETIMEDOUT/i
|
|
1168
|
-
];
|
|
1169
|
-
var attestCache = new Map;
|
|
1170
|
-
var attestInflight = new Map;
|
|
1171
|
-
function attestCacheKey(apiKey, model) {
|
|
1172
|
-
return `${apiKey}|${model ?? ""}`;
|
|
1173
|
-
}
|
|
1174
|
-
function pruneExpired(now) {
|
|
1175
|
-
for (const [key, entry] of attestCache) {
|
|
1176
|
-
if (entry.expires <= now) {
|
|
1177
|
-
attestCache.delete(key);
|
|
1178
|
-
} else {
|
|
1179
|
-
break;
|
|
1180
|
-
}
|
|
1181
|
-
}
|
|
1182
|
-
}
|
|
1183
|
-
function isTransientError(err) {
|
|
1184
|
-
const messages = [];
|
|
1185
|
-
if (err instanceof Error) {
|
|
1186
|
-
messages.push(err.message);
|
|
1187
|
-
}
|
|
1188
|
-
if (isAttestationError(err) && Array.isArray(err.cause)) {
|
|
1189
|
-
messages.push(...err.cause);
|
|
1190
|
-
}
|
|
1191
|
-
return messages.some((m) => TRANSIENT_PATTERNS.some((re) => re.test(m)));
|
|
1192
|
-
}
|
|
1193
|
-
function backoffDelayMs(attempt) {
|
|
1194
|
-
const exp = ATTEST_RETRY_BASE_MS * 2 ** (attempt - 1);
|
|
1195
|
-
const capped = Math.min(exp, ATTEST_RETRY_MAX_MS);
|
|
1196
|
-
const jitter = Math.floor(Math.random() * (capped / 2));
|
|
1197
|
-
return capped + jitter;
|
|
1198
|
-
}
|
|
1199
|
-
function delay(ms) {
|
|
1200
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1201
|
-
}
|
|
1202
|
-
function safeFree(obj) {
|
|
1203
|
-
if (typeof obj?.free !== "function")
|
|
1204
|
-
return;
|
|
1205
|
-
try {
|
|
1206
|
-
obj.free();
|
|
1207
|
-
} catch {}
|
|
1208
|
-
}
|
|
1209
|
-
async function attemptAttest(apiKey, options) {
|
|
1210
|
-
const prem = await loadPrem();
|
|
1211
|
-
let client;
|
|
1212
|
-
let attested;
|
|
1213
|
-
let headers;
|
|
1214
|
-
let sessionId;
|
|
1215
|
-
try {
|
|
1216
|
-
client = await new prem.ClientBuilder(endpoints.proxy ?? "").with_authorization(apiKey).build();
|
|
1217
|
-
if (options.model) {
|
|
1218
|
-
client.set_query(new prem.QueryParams().with("model", options.model));
|
|
1219
|
-
}
|
|
1220
|
-
attested = await client.attest();
|
|
1221
|
-
headers = attested.headers();
|
|
1222
|
-
sessionId = headers.cpu()?.get("x-session-id") ?? headers.gpu()?.get("x-session-id") ?? null;
|
|
1223
|
-
} finally {
|
|
1224
|
-
safeFree(headers);
|
|
1225
|
-
safeFree(attested);
|
|
1226
|
-
safeFree(client);
|
|
1227
|
-
}
|
|
1228
|
-
if (sessionId === null) {
|
|
1229
|
-
throw new Error("missing x-session-id issued by attestation");
|
|
1230
|
-
}
|
|
1231
|
-
return sessionId;
|
|
1232
|
-
}
|
|
1233
|
-
async function runAttest(apiKey, options) {
|
|
1234
|
-
let lastErr;
|
|
1235
|
-
for (let attempt = 1;attempt <= ATTEST_MAX_ATTEMPTS; attempt++) {
|
|
1236
|
-
try {
|
|
1237
|
-
return await attemptAttest(apiKey, options);
|
|
1238
|
-
} catch (err) {
|
|
1239
|
-
lastErr = err;
|
|
1240
|
-
if (attempt === ATTEST_MAX_ATTEMPTS || !isTransientError(err)) {
|
|
1241
|
-
throw err;
|
|
1242
|
-
}
|
|
1243
|
-
await delay(backoffDelayMs(attempt));
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
throw lastErr;
|
|
1247
|
-
}
|
|
1248
|
-
async function attest(apiKey, options = { enabled: true }) {
|
|
1249
|
-
if (!options.enabled)
|
|
1250
|
-
return null;
|
|
1251
|
-
const key = attestCacheKey(apiKey, options.model);
|
|
1252
|
-
const now = Date.now();
|
|
1253
|
-
const cached = attestCache.get(key);
|
|
1254
|
-
if (cached) {
|
|
1255
|
-
if (cached.expires > now)
|
|
1256
|
-
return cached.sessionId;
|
|
1257
|
-
attestCache.delete(key);
|
|
1258
|
-
}
|
|
1259
|
-
const inflight = attestInflight.get(key);
|
|
1260
|
-
if (inflight) {
|
|
1261
|
-
return inflight;
|
|
1262
|
-
}
|
|
1263
|
-
const work = runAttest(apiKey, options).then((sessionId) => {
|
|
1264
|
-
const insertTime = Date.now();
|
|
1265
|
-
pruneExpired(insertTime);
|
|
1266
|
-
attestCache.set(key, { sessionId, expires: insertTime + ATTEST_TTL_MS });
|
|
1267
|
-
if (attestCache.size > ATTEST_CACHE_MAX) {
|
|
1268
|
-
const oldest = attestCache.keys().next().value;
|
|
1269
|
-
if (oldest)
|
|
1270
|
-
attestCache.delete(oldest);
|
|
1271
|
-
}
|
|
1272
|
-
return sessionId;
|
|
1273
|
-
}).finally(() => {
|
|
1274
|
-
attestInflight.delete(key);
|
|
1275
|
-
});
|
|
1276
|
-
attestInflight.set(key, work);
|
|
1277
|
-
return work;
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
// src/utils/error.ts
|
|
1281
|
-
async function throwIfErrorResponse(response) {
|
|
1282
|
-
let raw;
|
|
1283
|
-
try {
|
|
1284
|
-
raw = await response.json();
|
|
1285
|
-
if (!raw.status)
|
|
1286
|
-
raw = { ...raw, status: response.status };
|
|
1287
|
-
} catch {
|
|
1288
|
-
raw = {
|
|
1289
|
-
status: response.status,
|
|
1290
|
-
data: null,
|
|
1291
|
-
error: response.statusText || `HTTP ${response.status}`,
|
|
1292
|
-
message: null
|
|
1293
|
-
};
|
|
1294
|
-
}
|
|
1295
|
-
throw raw;
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
// src/utils/files.ts
|
|
1299
|
-
var getFileName = (file) => {
|
|
1300
|
-
if (file instanceof File) {
|
|
1301
|
-
return file.name;
|
|
1302
|
-
}
|
|
1303
|
-
if (file instanceof Blob) {
|
|
1304
|
-
return;
|
|
1305
|
-
}
|
|
1306
|
-
const fileAny = file;
|
|
1307
|
-
if (fileAny.path) {
|
|
1308
|
-
const path = typeof fileAny.path === "string" ? fileAny.path : fileAny.path.toString();
|
|
1309
|
-
return path.split("/").pop() || path.split("\\").pop() || path;
|
|
1310
|
-
}
|
|
1311
|
-
if (file instanceof Uint8Array || file instanceof ArrayBuffer) {
|
|
1312
|
-
return;
|
|
1313
|
-
}
|
|
1314
|
-
return;
|
|
1315
|
-
};
|
|
1316
|
-
|
|
1317
|
-
// src/audio/index.ts
|
|
1318
|
-
async function readUploadableToUint8Array(file) {
|
|
1319
|
-
if (file instanceof Uint8Array) {
|
|
1320
|
-
return file;
|
|
1321
|
-
}
|
|
1322
|
-
if (file instanceof ArrayBuffer) {
|
|
1323
|
-
return new Uint8Array(file);
|
|
1324
|
-
}
|
|
1325
|
-
if (typeof file.arrayBuffer === "function") {
|
|
1326
|
-
const blob = file;
|
|
1327
|
-
const buffer = await blob.arrayBuffer();
|
|
1328
|
-
return new Uint8Array(buffer);
|
|
1329
|
-
}
|
|
1330
|
-
const fileAny = file;
|
|
1331
|
-
if (typeof fileAny.on === "function" && (typeof fileAny.read === "function" || typeof fileAny.pipe === "function")) {
|
|
1332
|
-
const chunks = [];
|
|
1333
|
-
return new Promise((resolve, reject) => {
|
|
1334
|
-
fileAny.on("data", (chunk) => {
|
|
1335
|
-
if (Buffer.isBuffer(chunk)) {
|
|
1336
|
-
chunks.push(new Uint8Array(chunk));
|
|
1337
|
-
} else if (chunk instanceof Uint8Array) {
|
|
1338
|
-
chunks.push(chunk);
|
|
1339
|
-
} else if (typeof chunk === "object" && chunk !== null) {
|
|
1340
|
-
chunks.push(new Uint8Array(Buffer.from(chunk)));
|
|
1341
|
-
}
|
|
1342
|
-
});
|
|
1343
|
-
fileAny.on("end", () => {
|
|
1344
|
-
const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
|
|
1345
|
-
const result = new Uint8Array(totalLength);
|
|
1346
|
-
let offset = 0;
|
|
1347
|
-
for (const chunk of chunks) {
|
|
1348
|
-
result.set(chunk, offset);
|
|
1349
|
-
offset += chunk.length;
|
|
1350
|
-
}
|
|
1351
|
-
resolve(result);
|
|
1352
|
-
});
|
|
1353
|
-
fileAny.on("error", (err) => reject(err));
|
|
1354
|
-
});
|
|
1355
|
-
}
|
|
1356
|
-
throw new Error("Unsupported file type for audio transcription");
|
|
1357
|
-
}
|
|
1358
|
-
async function preprocessAudioRequest(body, encryptionKeys) {
|
|
1359
|
-
const { cipherText, sharedSecret } = encryptionKeys;
|
|
1360
|
-
const audioData = await readUploadableToUint8Array(body.file);
|
|
1361
|
-
const isDeepgram = body.model.startsWith("deepgram/");
|
|
1362
|
-
const requestBody = isDeepgram ? {
|
|
1363
|
-
model: body.model,
|
|
1364
|
-
diarize: body.diarize,
|
|
1365
|
-
smart_format: body.smart_format
|
|
1366
|
-
} : {
|
|
1367
|
-
model: body.model,
|
|
1368
|
-
language: body.language,
|
|
1369
|
-
prompt: body.prompt,
|
|
1370
|
-
response_format: body.response_format,
|
|
1371
|
-
temperature: body.temperature,
|
|
1372
|
-
timestamp_granularities: body.timestamp_granularities
|
|
1373
|
-
};
|
|
1374
|
-
const cleanedBody = Object.fromEntries(Object.entries(requestBody).filter(([_, v]) => v !== undefined));
|
|
1375
|
-
const { encrypted, nonce } = encryptPayload(sharedSecret, cleanedBody);
|
|
1376
|
-
const { encrypted: encryptedFile, nonce: fileNonce } = encryptPayload(sharedSecret, audioData);
|
|
1377
|
-
const fileName = getFileName(body.file) || "audio.mp3";
|
|
1378
|
-
const { encrypted: encryptedFileName, nonce: fileNameNonce } = encryptPayload(sharedSecret, fileName);
|
|
1379
|
-
return {
|
|
1380
|
-
body: {
|
|
1381
|
-
cipherText: bytesToHex4(cipherText),
|
|
1382
|
-
encryptedInference: bytesToHex4(encrypted),
|
|
1383
|
-
nonce: bytesToHex4(nonce),
|
|
1384
|
-
fileNameNonce: bytesToHex4(fileNameNonce),
|
|
1385
|
-
encryptedFileName: bytesToHex4(encryptedFileName),
|
|
1386
|
-
fileNonce: bytesToHex4(fileNonce),
|
|
1387
|
-
encryptedFile: bytesToHex4(encryptedFile),
|
|
1388
|
-
model: body.model
|
|
1389
|
-
},
|
|
1390
|
-
sharedSecret
|
|
1391
|
-
};
|
|
1392
|
-
}
|
|
1393
|
-
async function postprocessTranscriptionResponse(response, sharedSecret) {
|
|
1394
|
-
const responseData = await response.json();
|
|
1395
|
-
const data = responseData.data;
|
|
1396
|
-
if (!data.encryptedResponse || !data.nonce) {
|
|
1397
|
-
throw new Error("Invalid transcription response: missing encryptedResponse or nonce");
|
|
1398
|
-
}
|
|
1399
|
-
const responseNonce = hexToBytes3(data.nonce);
|
|
1400
|
-
return decryptPayload(data.encryptedResponse, sharedSecret, responseNonce);
|
|
1401
|
-
}
|
|
1402
|
-
async function postprocessTranslationResponse(response, sharedSecret) {
|
|
1403
|
-
const responseData = await response.json();
|
|
1404
|
-
const data = responseData.data;
|
|
1405
|
-
if (!data.encryptedResponse || !data.nonce) {
|
|
1406
|
-
throw new Error("Invalid translation response: missing encryptedResponse or nonce");
|
|
1407
|
-
}
|
|
1408
|
-
const responseNonce = hexToBytes3(data.nonce);
|
|
1409
|
-
return decryptPayload(data.encryptedResponse, sharedSecret, responseNonce);
|
|
1410
|
-
}
|
|
1411
|
-
async function preprocessAudioTranslationRequest(body, encryptionKeys) {
|
|
1412
|
-
const { cipherText, sharedSecret } = encryptionKeys;
|
|
1413
|
-
const audioData = await readUploadableToUint8Array(body.file);
|
|
1414
|
-
const requestBody = {
|
|
1415
|
-
model: body.model,
|
|
1416
|
-
prompt: body.prompt,
|
|
1417
|
-
response_format: body.response_format,
|
|
1418
|
-
temperature: body.temperature
|
|
1419
|
-
};
|
|
1420
|
-
const cleanedBody = Object.fromEntries(Object.entries(requestBody).filter(([_, v]) => v !== undefined));
|
|
1421
|
-
const { encrypted, nonce } = encryptPayload(sharedSecret, cleanedBody);
|
|
1422
|
-
const { encrypted: encryptedFile, nonce: fileNonce } = encryptPayload(sharedSecret, audioData);
|
|
1423
|
-
const fileName = getFileName(body.file) || "audio.mp3";
|
|
1424
|
-
const { encrypted: encryptedFileName, nonce: fileNameNonce } = encryptPayload(sharedSecret, fileName);
|
|
1425
|
-
return {
|
|
1426
|
-
body: {
|
|
1427
|
-
cipherText: bytesToHex4(cipherText),
|
|
1428
|
-
encryptedInference: bytesToHex4(encrypted),
|
|
1429
|
-
nonce: bytesToHex4(nonce),
|
|
1430
|
-
fileNameNonce: bytesToHex4(fileNameNonce),
|
|
1431
|
-
encryptedFileName: bytesToHex4(encryptedFileName),
|
|
1432
|
-
fileNonce: bytesToHex4(fileNonce),
|
|
1433
|
-
encryptedFile: bytesToHex4(encryptedFile),
|
|
1434
|
-
model: body.model
|
|
1435
|
-
},
|
|
1436
|
-
sharedSecret
|
|
1437
|
-
};
|
|
1438
|
-
}
|
|
1439
|
-
function createAudioClient(apiKey, encryptionKeys, requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, attest2 = true) {
|
|
1440
|
-
async function createTranscription(body) {
|
|
1441
|
-
const controller = new AbortController;
|
|
1442
|
-
const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);
|
|
1443
|
-
try {
|
|
1444
|
-
const sessionId = await attest(apiKey, { model: body.model, enabled: attest2 });
|
|
1445
|
-
const encryptedRequest = await preprocessAudioRequest(body, encryptionKeys);
|
|
1446
|
-
const response = await fetch(`${endpoints.proxy}/rvenc/audio/transcriptions`, {
|
|
1447
|
-
method: "POST",
|
|
1448
|
-
headers: {
|
|
1449
|
-
"Content-Type": "application/json",
|
|
1450
|
-
Authorization: apiKey,
|
|
1451
|
-
...sessionId && { "X-Session-Id": sessionId }
|
|
1452
|
-
},
|
|
1453
|
-
body: JSON.stringify(encryptedRequest.body),
|
|
1454
|
-
signal: controller.signal
|
|
1455
|
-
});
|
|
1456
|
-
if (!response.ok) {
|
|
1457
|
-
await throwIfErrorResponse(response);
|
|
1458
|
-
}
|
|
1459
|
-
clearTimeout(timeoutId);
|
|
1460
|
-
return await postprocessTranscriptionResponse(response, encryptedRequest.sharedSecret);
|
|
1461
|
-
} catch (error) {
|
|
1462
|
-
clearTimeout(timeoutId);
|
|
1463
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
1464
|
-
throw new Error(`Request timed out after ${requestTimeoutMs}ms`);
|
|
1465
|
-
}
|
|
1466
|
-
throw error;
|
|
1467
|
-
}
|
|
1468
|
-
}
|
|
1469
|
-
const transcriptionsClient = {
|
|
1470
|
-
create: createTranscription
|
|
1471
|
-
};
|
|
1472
|
-
async function createTranslation(body) {
|
|
1473
|
-
const controller = new AbortController;
|
|
1474
|
-
const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);
|
|
1475
|
-
try {
|
|
1476
|
-
const sessionId = await attest(apiKey, { model: body.model, enabled: attest2 });
|
|
1477
|
-
const encryptedRequest = await preprocessAudioTranslationRequest(body, encryptionKeys);
|
|
1478
|
-
const response = await fetch(`${endpoints.proxy}/rvenc/audio/translations`, {
|
|
1479
|
-
method: "POST",
|
|
1480
|
-
headers: {
|
|
1481
|
-
"Content-Type": "application/json",
|
|
1482
|
-
Authorization: apiKey,
|
|
1483
|
-
...sessionId && { "X-Session-Id": sessionId }
|
|
1484
|
-
},
|
|
1485
|
-
body: JSON.stringify(encryptedRequest.body),
|
|
1486
|
-
signal: controller.signal
|
|
1487
|
-
});
|
|
1488
|
-
if (!response.ok) {
|
|
1489
|
-
await throwIfErrorResponse(response);
|
|
1490
|
-
}
|
|
1491
|
-
clearTimeout(timeoutId);
|
|
1492
|
-
return await postprocessTranslationResponse(response, encryptedRequest.sharedSecret);
|
|
1493
|
-
} catch (error) {
|
|
1494
|
-
clearTimeout(timeoutId);
|
|
1495
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
1496
|
-
throw new Error(`Request timed out after ${requestTimeoutMs}ms`);
|
|
1497
|
-
}
|
|
1498
|
-
throw error;
|
|
1499
|
-
}
|
|
1500
|
-
}
|
|
1501
|
-
const translationsClient = {
|
|
1502
|
-
create: createTranslation
|
|
1503
|
-
};
|
|
1504
|
-
return {
|
|
1505
|
-
transcriptions: transcriptionsClient,
|
|
1506
|
-
translations: translationsClient
|
|
1507
|
-
};
|
|
1508
|
-
}
|
|
7
|
+
import envPaths3 from "env-paths";
|
|
8
|
+
|
|
9
|
+
// src/server/runtime.ts
|
|
10
|
+
import multer from "multer";
|
|
11
|
+
|
|
12
|
+
// src/audio/index.ts
|
|
13
|
+
import { bytesToHex as bytesToHex2, hexToBytes as hexToBytes2 } from "@noble/ciphers/utils.js";
|
|
14
|
+
|
|
15
|
+
// src/config.ts
|
|
16
|
+
var endpoints = {
|
|
17
|
+
enclave: process.env.ENCLAVE_URL,
|
|
18
|
+
proxy: process.env.PROXY_URL
|
|
19
|
+
};
|
|
20
|
+
var DEFAULT_MAX_BUFFER_SIZE = 10 * 1024 * 1024;
|
|
21
|
+
|
|
22
|
+
// src/utils/attestation.ts
|
|
23
|
+
var attestCache = new Map;
|
|
24
|
+
var attestInflight = new Map;
|
|
25
|
+
|
|
26
|
+
// src/utils/crypto.ts
|
|
27
|
+
import { aeskwp } from "@noble/ciphers/aes.js";
|
|
28
|
+
import { xchacha20poly1305 } from "@noble/ciphers/chacha.js";
|
|
29
|
+
import {
|
|
30
|
+
bytesToHex,
|
|
31
|
+
hexToBytes,
|
|
32
|
+
managedNonce,
|
|
33
|
+
randomBytes
|
|
34
|
+
} from "@noble/ciphers/utils.js";
|
|
35
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
36
|
+
import { sha3_256 } from "@noble/hashes/sha3.js";
|
|
37
|
+
import { XWing } from "@noble/post-quantum/hybrid.js";
|
|
1509
38
|
|
|
1510
39
|
// src/files/index.ts
|
|
1511
|
-
import { bytesToHex as
|
|
40
|
+
import { bytesToHex as bytesToHex4, hexToBytes as hexToBytes4, randomBytes as randomBytes3 } from "@noble/ciphers/utils.js";
|
|
1512
41
|
import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
|
|
1513
42
|
import { isValid, parseISO } from "date-fns";
|
|
1514
43
|
import { z } from "zod";
|
|
44
|
+
|
|
45
|
+
// src/utils/dek-store.ts
|
|
46
|
+
import { bytesToHex as bytesToHex3, hexToBytes as hexToBytes3, randomBytes as randomBytes2 } from "@noble/ciphers/utils.js";
|
|
47
|
+
function generateNewClientKEK() {
|
|
48
|
+
return bytesToHex3(randomBytes2(32));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/files/index.ts
|
|
1515
52
|
var MAX_FILENAME_LENGTH = 255;
|
|
1516
53
|
var MIN_FILENAME_LENGTH = 1;
|
|
1517
54
|
var ALLOWED_MIME_TYPES = new Set([
|
|
@@ -1574,948 +111,27 @@ var DeleteFileOptionsSchema = z.object({
|
|
|
1574
111
|
});
|
|
1575
112
|
var IndexFileInputSchema = z.object({
|
|
1576
113
|
fileId: z.string().min(1, "File ID is required"),
|
|
1577
|
-
filePath: z.string().min(1, "File path is required"),
|
|
1578
|
-
fileDEK: z.instanceof(Uint8Array).optional()
|
|
1579
|
-
});
|
|
1580
|
-
var IndexFilesOptionsSchema = z.object({
|
|
1581
|
-
files: z.array(IndexFileInputSchema).min(1, "Files array must not be empty"),
|
|
1582
|
-
ragDEK: z.instanceof(Uint8Array).optional()
|
|
1583
|
-
});
|
|
1584
|
-
var DeleteIndexOptionsSchema = z.object({
|
|
1585
|
-
fileIds: z.array(z.string().min(1)).min(1, "File IDs array must not be empty"),
|
|
1586
|
-
ragDEK: z.instanceof(Uint8Array).optional()
|
|
1587
|
-
});
|
|
1588
|
-
function validateAPIKey(apiKey) {
|
|
1589
|
-
ApiKeySchema.parse(apiKey);
|
|
1590
|
-
}
|
|
1591
|
-
function validateDEKStore(dekStore) {
|
|
1592
|
-
DEKStoreSchema.parse(dekStore);
|
|
1593
|
-
}
|
|
1594
|
-
function validateMimeType(mimeType) {
|
|
1595
|
-
MimeTypeSchema.parse(mimeType);
|
|
1596
|
-
}
|
|
1597
|
-
function validateFileUploadOptions(options) {
|
|
1598
|
-
FileUploadOptionsSchema.parse(options);
|
|
1599
|
-
}
|
|
1600
|
-
function validateListFilesOptions(options) {
|
|
1601
|
-
ListFilesOptionsSchema.parse(options);
|
|
1602
|
-
}
|
|
1603
|
-
function validateGetFileOptions(options) {
|
|
1604
|
-
GetFileOptionsSchema.parse(options);
|
|
1605
|
-
}
|
|
1606
|
-
function guessMimeType(fileName) {
|
|
1607
|
-
const ext = fileName.toLowerCase().split(".").pop() || "";
|
|
1608
|
-
const mimeTypeMap = {
|
|
1609
|
-
jpg: "image/jpeg",
|
|
1610
|
-
jpeg: "image/jpeg",
|
|
1611
|
-
png: "image/png",
|
|
1612
|
-
gif: "image/gif",
|
|
1613
|
-
webp: "image/webp",
|
|
1614
|
-
pdf: "application/pdf",
|
|
1615
|
-
doc: "application/msword",
|
|
1616
|
-
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1617
|
-
xls: "application/vnd.ms-excel",
|
|
1618
|
-
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
1619
|
-
txt: "text/plain",
|
|
1620
|
-
csv: "text/csv",
|
|
1621
|
-
md: "text/markdown",
|
|
1622
|
-
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
1623
|
-
mp4: "video/mp4",
|
|
1624
|
-
webm: "video/webm",
|
|
1625
|
-
mov: "video/quicktime",
|
|
1626
|
-
mp3: "audio/mpeg",
|
|
1627
|
-
wav: "audio/wav",
|
|
1628
|
-
ogg: "audio/ogg",
|
|
1629
|
-
zip: "application/zip",
|
|
1630
|
-
rar: "application/x-rar-compressed",
|
|
1631
|
-
"7z": "application/x-7z-compressed"
|
|
1632
|
-
};
|
|
1633
|
-
return mimeTypeMap[ext] || "application/octet-stream";
|
|
1634
|
-
}
|
|
1635
|
-
async function saveRagDEKToBackend(apiKey, wrappedRagDEK, timeoutMs) {
|
|
1636
|
-
const controller = new AbortController;
|
|
1637
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
1638
|
-
try {
|
|
1639
|
-
const response = await fetch(`${endpoints.proxy}/users/save_rag_dek`, {
|
|
1640
|
-
method: "POST",
|
|
1641
|
-
headers: {
|
|
1642
|
-
Authorization: apiKey,
|
|
1643
|
-
"Content-Type": "application/json"
|
|
1644
|
-
},
|
|
1645
|
-
body: JSON.stringify({
|
|
1646
|
-
data: {
|
|
1647
|
-
wrappedRagDEK,
|
|
1648
|
-
confirmReplaceRagDEK: true
|
|
1649
|
-
}
|
|
1650
|
-
}),
|
|
1651
|
-
signal: controller.signal
|
|
1652
|
-
});
|
|
1653
|
-
if (!response.ok) {
|
|
1654
|
-
throw new Error(`Failed to save RAG DEK: HTTP ${response.status}`);
|
|
1655
|
-
}
|
|
1656
|
-
const result = await response.json();
|
|
1657
|
-
if (result.error) {
|
|
1658
|
-
throw new Error(result.error);
|
|
1659
|
-
}
|
|
1660
|
-
} catch (error) {
|
|
1661
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
1662
|
-
throw new Error(`Save RAG DEK request timed out after ${timeoutMs}ms`);
|
|
1663
|
-
}
|
|
1664
|
-
throw error;
|
|
1665
|
-
} finally {
|
|
1666
|
-
clearTimeout(timeoutId);
|
|
1667
|
-
}
|
|
1668
|
-
}
|
|
1669
|
-
async function prepareEncryptedPayload(dekStore, options, apiKey, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
1670
|
-
const fileBytes = options.file;
|
|
1671
|
-
const mimeType = options.mimeType || guessMimeType(options.fileName);
|
|
1672
|
-
validateMimeType(mimeType);
|
|
1673
|
-
const dek = randomBytes4(32);
|
|
1674
|
-
const encryptedFile = encryptWithDEK(dek, fileBytes);
|
|
1675
|
-
const encryptedName = encryptMetadataWithDEK(dek, options.fileName);
|
|
1676
|
-
const encryptedMimeType = encryptMetadataWithDEK(dek, mimeType);
|
|
1677
|
-
const _clientKEK = clientKEK ? hexToBytes4(clientKEK) : getClientKEK();
|
|
1678
|
-
const wrappedDEK = wrapDEK(_clientKEK, dek);
|
|
1679
|
-
const clientKID = clientKEK ? getClientKID(clientKEK) : getClientKID();
|
|
1680
|
-
const filePayload = {
|
|
1681
|
-
client_hash: bytesToHex5(sha2562(fileBytes)),
|
|
1682
|
-
encrypted_content: bytesToHex5(encryptedFile),
|
|
1683
|
-
encrypted_name: encryptedName,
|
|
1684
|
-
kid: clientKID,
|
|
1685
|
-
mime_type: encryptedMimeType,
|
|
1686
|
-
version: "2",
|
|
1687
|
-
wrapped_dek: bytesToHex5(wrappedDEK)
|
|
1688
|
-
};
|
|
1689
|
-
if (options.ragIndex) {
|
|
1690
|
-
await addRagIndexToPayload(dekStore, dek, filePayload, apiKey, clientKEK, timeoutMs);
|
|
1691
|
-
}
|
|
1692
|
-
return { dek, filePayload };
|
|
1693
|
-
}
|
|
1694
|
-
async function addRagIndexToPayload(dekStore, dek, filePayload, apiKey, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
1695
|
-
let ragDEK = dekStore.ragDEK;
|
|
1696
|
-
const _clientKEK = clientKEK ? hexToBytes4(clientKEK) : getClientKEK();
|
|
1697
|
-
if (!ragDEK) {
|
|
1698
|
-
ragDEK = randomBytes4(32);
|
|
1699
|
-
const wrappedRagDEK = wrapDEK(_clientKEK, ragDEK);
|
|
1700
|
-
dekStore.ragDEK = wrappedRagDEK;
|
|
1701
|
-
try {
|
|
1702
|
-
await saveRagDEKToBackend(apiKey, bytesToHex5(wrappedRagDEK), timeoutMs);
|
|
1703
|
-
} catch (error) {
|
|
1704
|
-
console.error("Warning: Failed to save RAG DEK to backend:", error);
|
|
1705
|
-
}
|
|
1706
|
-
} else {
|
|
1707
|
-
ragDEK = unwrapDEK(_clientKEK, ragDEK);
|
|
1708
|
-
}
|
|
1709
|
-
const enclavePublicKey = await getEnclavePublicKey(timeoutMs);
|
|
1710
|
-
const { cipherText, sharedSecret } = createMLKEMEncapsulation(enclavePublicKey);
|
|
1711
|
-
const { encrypted: encryptedFileDEK, nonce: fileNonce } = encryptPayload(sharedSecret, dek);
|
|
1712
|
-
const { encrypted: encryptedRagDEK, nonce: ragDEKNonce } = encryptPayload(sharedSecret, ragDEK);
|
|
1713
|
-
filePayload.encrypted_file_dek = bytesToHex5(encryptedFileDEK);
|
|
1714
|
-
filePayload.encrypted_rag_dek = bytesToHex5(encryptedRagDEK);
|
|
1715
|
-
filePayload.file_nonce = bytesToHex5(fileNonce);
|
|
1716
|
-
filePayload.rag_dek_nonce = bytesToHex5(ragDEKNonce);
|
|
1717
|
-
filePayload.cipher_text = bytesToHex5(cipherText);
|
|
1718
|
-
}
|
|
1719
|
-
async function performUpload(apiKey, filePayload, controller) {
|
|
1720
|
-
const uploadResponse = await fetch(`${endpoints.proxy}/files/encrypted/upload`, {
|
|
1721
|
-
method: "POST",
|
|
1722
|
-
headers: {
|
|
1723
|
-
Authorization: apiKey,
|
|
1724
|
-
"Content-Type": "application/json"
|
|
1725
|
-
},
|
|
1726
|
-
body: JSON.stringify(filePayload),
|
|
1727
|
-
signal: controller.signal
|
|
1728
|
-
});
|
|
1729
|
-
if (!uploadResponse.ok) {
|
|
1730
|
-
let errorMessage = `Upload request failed with status ${uploadResponse.status}`;
|
|
1731
|
-
try {
|
|
1732
|
-
const body = await uploadResponse.json();
|
|
1733
|
-
if (body.error) {
|
|
1734
|
-
errorMessage = body.error;
|
|
1735
|
-
}
|
|
1736
|
-
} catch {}
|
|
1737
|
-
throw new Error(errorMessage);
|
|
1738
|
-
}
|
|
1739
|
-
const uploadResult = await uploadResponse.json();
|
|
1740
|
-
if (uploadResult.status !== 200) {
|
|
1741
|
-
throw new Error(uploadResult.error || "Upload failed");
|
|
1742
|
-
}
|
|
1743
|
-
if (!uploadResult.data) {
|
|
1744
|
-
throw new Error("Upload response missing data");
|
|
1745
|
-
}
|
|
1746
|
-
return uploadResult.data;
|
|
1747
|
-
}
|
|
1748
|
-
function storeDEKForFile(dekStore, fileId, dek, clientKEK) {
|
|
1749
|
-
if (!dekStore.fileDEKs) {
|
|
1750
|
-
dekStore.fileDEKs = new Map;
|
|
1751
|
-
}
|
|
1752
|
-
const _clientKEK = clientKEK ? hexToBytes4(clientKEK) : getClientKEK();
|
|
1753
|
-
const wrappedDEK = wrapDEK(_clientKEK, dek);
|
|
1754
|
-
dekStore.fileDEKs.set(fileId, wrappedDEK);
|
|
1755
|
-
}
|
|
1756
|
-
async function uploadFile(apiKey, dekStore, options, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
1757
|
-
validateAPIKey(apiKey);
|
|
1758
|
-
validateDEKStore(dekStore);
|
|
1759
|
-
validateFileUploadOptions(options);
|
|
1760
|
-
TimeoutSchema.parse(timeoutMs);
|
|
1761
|
-
const controller = new AbortController;
|
|
1762
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
1763
|
-
try {
|
|
1764
|
-
const { dek, filePayload } = await prepareEncryptedPayload(dekStore, options, apiKey, clientKEK, timeoutMs);
|
|
1765
|
-
const uploadedFile = await performUpload(apiKey, filePayload, controller);
|
|
1766
|
-
storeDEKForFile(dekStore, uploadedFile.id, dek, clientKEK);
|
|
1767
|
-
return uploadedFile;
|
|
1768
|
-
} catch (error) {
|
|
1769
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
1770
|
-
throw new Error(`File upload timed out after ${timeoutMs}ms`);
|
|
1771
|
-
}
|
|
1772
|
-
throw error;
|
|
1773
|
-
} finally {
|
|
1774
|
-
clearTimeout(timeoutId);
|
|
1775
|
-
}
|
|
1776
|
-
}
|
|
1777
|
-
async function listFiles(apiKey, options, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
1778
|
-
validateAPIKey(apiKey);
|
|
1779
|
-
validateListFilesOptions(options);
|
|
1780
|
-
TimeoutSchema.parse(timeoutMs);
|
|
1781
|
-
const controller = new AbortController;
|
|
1782
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
1783
|
-
const queryParams = new URLSearchParams;
|
|
1784
|
-
if (options?.limit !== undefined) {
|
|
1785
|
-
queryParams.append("limit", options.limit.toString());
|
|
1786
|
-
}
|
|
1787
|
-
if (options?.offset !== undefined) {
|
|
1788
|
-
queryParams.append("offset", options.offset.toString());
|
|
1789
|
-
}
|
|
1790
|
-
if (options?.search) {
|
|
1791
|
-
queryParams.append("search", options.search);
|
|
1792
|
-
}
|
|
1793
|
-
if (options?.from) {
|
|
1794
|
-
queryParams.append("from", options.from);
|
|
1795
|
-
}
|
|
1796
|
-
if (options?.to) {
|
|
1797
|
-
queryParams.append("to", options.to);
|
|
1798
|
-
}
|
|
1799
|
-
const queryString = queryParams.toString();
|
|
1800
|
-
const url = `${endpoints.proxy}/files/encrypted${queryString ? `?${queryString}` : ""}`;
|
|
1801
|
-
try {
|
|
1802
|
-
const response = await fetch(url, {
|
|
1803
|
-
method: "GET",
|
|
1804
|
-
headers: {
|
|
1805
|
-
Authorization: apiKey,
|
|
1806
|
-
"Content-Type": "application/json"
|
|
1807
|
-
},
|
|
1808
|
-
signal: controller.signal
|
|
1809
|
-
});
|
|
1810
|
-
if (!response.ok) {
|
|
1811
|
-
throw new Error(`List files request failed with status ${response.status}`);
|
|
1812
|
-
}
|
|
1813
|
-
const result = await response.json();
|
|
1814
|
-
if (result.status !== 200) {
|
|
1815
|
-
throw new Error(result.error || "List files failed");
|
|
1816
|
-
}
|
|
1817
|
-
if (!result.data) {
|
|
1818
|
-
throw new Error("List files response missing data");
|
|
1819
|
-
}
|
|
1820
|
-
return result.data;
|
|
1821
|
-
} catch (error) {
|
|
1822
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
1823
|
-
throw new Error(`List files request timed out after ${timeoutMs}ms`);
|
|
1824
|
-
}
|
|
1825
|
-
throw error;
|
|
1826
|
-
} finally {
|
|
1827
|
-
clearTimeout(timeoutId);
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1830
|
-
async function getFile(apiKey, options, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
1831
|
-
validateAPIKey(apiKey);
|
|
1832
|
-
validateGetFileOptions(options);
|
|
1833
|
-
TimeoutSchema.parse(timeoutMs);
|
|
1834
|
-
const controller = new AbortController;
|
|
1835
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
1836
|
-
const queryParams = new URLSearchParams;
|
|
1837
|
-
if (options.url !== undefined) {
|
|
1838
|
-
queryParams.append("url", options.url ? "true" : "false");
|
|
1839
|
-
}
|
|
1840
|
-
const queryString = queryParams.toString();
|
|
1841
|
-
const url = `${endpoints.proxy}/files/encrypted/${options.id}${queryString ? `?${queryString}` : ""}`;
|
|
1842
|
-
try {
|
|
1843
|
-
const response = await fetch(url, {
|
|
1844
|
-
method: "GET",
|
|
1845
|
-
headers: {
|
|
1846
|
-
Authorization: apiKey,
|
|
1847
|
-
"Content-Type": "application/json"
|
|
1848
|
-
},
|
|
1849
|
-
signal: controller.signal
|
|
1850
|
-
});
|
|
1851
|
-
if (!response.ok) {
|
|
1852
|
-
if (response.status === 404) {
|
|
1853
|
-
throw new Error(`File not found: ${options.id}`);
|
|
1854
|
-
}
|
|
1855
|
-
throw new Error(`Get file request failed with status ${response.status}`);
|
|
1856
|
-
}
|
|
1857
|
-
const result = await response.json();
|
|
1858
|
-
if (result.status !== 200) {
|
|
1859
|
-
throw new Error(result.error || "Get file failed");
|
|
1860
|
-
}
|
|
1861
|
-
if (!result.data) {
|
|
1862
|
-
throw new Error("Get file response missing data");
|
|
1863
|
-
}
|
|
1864
|
-
return result.data;
|
|
1865
|
-
} catch (error) {
|
|
1866
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
1867
|
-
throw new Error(`Get file request timed out after ${timeoutMs}ms`);
|
|
1868
|
-
}
|
|
1869
|
-
throw error;
|
|
1870
|
-
} finally {
|
|
1871
|
-
clearTimeout(timeoutId);
|
|
1872
|
-
}
|
|
1873
|
-
}
|
|
1874
|
-
async function deleteFile(apiKey, options, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
1875
|
-
validateAPIKey(apiKey);
|
|
1876
|
-
DeleteFileOptionsSchema.parse(options);
|
|
1877
|
-
TimeoutSchema.parse(timeoutMs);
|
|
1878
|
-
const controller = new AbortController;
|
|
1879
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
1880
|
-
try {
|
|
1881
|
-
const response = await fetch(`${endpoints.proxy}/files/encrypted/${options.id}`, {
|
|
1882
|
-
method: "DELETE",
|
|
1883
|
-
headers: {
|
|
1884
|
-
Authorization: apiKey,
|
|
1885
|
-
"Content-Type": "application/json"
|
|
1886
|
-
},
|
|
1887
|
-
signal: controller.signal
|
|
1888
|
-
});
|
|
1889
|
-
if (!response.ok) {
|
|
1890
|
-
if (response.status === 404) {
|
|
1891
|
-
throw new Error(`File not found: ${options.id}`);
|
|
1892
|
-
}
|
|
1893
|
-
throw new Error(`Delete file request failed with status ${response.status}`);
|
|
1894
|
-
}
|
|
1895
|
-
await response.json();
|
|
1896
|
-
} catch (error) {
|
|
1897
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
1898
|
-
throw new Error(`Delete file request timed out after ${timeoutMs}ms`);
|
|
1899
|
-
}
|
|
1900
|
-
throw error;
|
|
1901
|
-
} finally {
|
|
1902
|
-
clearTimeout(timeoutId);
|
|
1903
|
-
}
|
|
1904
|
-
}
|
|
1905
|
-
async function indexFiles(apiKey, dekStore, options, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
1906
|
-
validateAPIKey(apiKey);
|
|
1907
|
-
validateDEKStore(dekStore);
|
|
1908
|
-
IndexFilesOptionsSchema.parse(options);
|
|
1909
|
-
TimeoutSchema.parse(timeoutMs);
|
|
1910
|
-
const wrappedRagDEK = options.ragDEK || dekStore.ragDEK;
|
|
1911
|
-
if (!wrappedRagDEK) {
|
|
1912
|
-
throw new Error("RAG DEK not found. Provide ragDEK in options or upload at least one file with ragIndex: true.");
|
|
1913
|
-
}
|
|
1914
|
-
const _clientKEK = clientKEK ? hexToBytes4(clientKEK) : getClientKEK();
|
|
1915
|
-
const ragDEK = unwrapDEK(_clientKEK, wrappedRagDEK);
|
|
1916
|
-
const enclavePublicKey = await getEnclavePublicKey(timeoutMs);
|
|
1917
|
-
const { cipherText, sharedSecret } = createMLKEMEncapsulation(enclavePublicKey);
|
|
1918
|
-
const encryptedFiles = options.files.map((file) => {
|
|
1919
|
-
const wrappedFileDEK = file.fileDEK || dekStore.fileDEKs?.get(file.fileId);
|
|
1920
|
-
if (!wrappedFileDEK) {
|
|
1921
|
-
throw new Error(`File DEK not found for file: ${file.fileId}. Provide fileDEK or ensure file was uploaded with this DEK store.`);
|
|
1922
|
-
}
|
|
1923
|
-
const fileDEK = unwrapDEK(_clientKEK, wrappedFileDEK);
|
|
1924
|
-
const { encrypted: encryptedFileDEK, nonce: fileNonce } = encryptPayload(sharedSecret, fileDEK);
|
|
1925
|
-
const { encrypted: encryptedRagDEK, nonce: ragDEKNonce } = encryptPayload(sharedSecret, ragDEK);
|
|
1926
|
-
return {
|
|
1927
|
-
file_id: file.fileId,
|
|
1928
|
-
encrypted_file_dek: bytesToHex5(encryptedFileDEK),
|
|
1929
|
-
encrypted_rag_dek: bytesToHex5(encryptedRagDEK),
|
|
1930
|
-
file_nonce: bytesToHex5(fileNonce),
|
|
1931
|
-
rag_dek_nonce: bytesToHex5(ragDEKNonce),
|
|
1932
|
-
s3_r2_path: file.filePath,
|
|
1933
|
-
cipher_text: bytesToHex5(cipherText)
|
|
1934
|
-
};
|
|
1935
|
-
});
|
|
1936
|
-
const controller = new AbortController;
|
|
1937
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
1938
|
-
try {
|
|
1939
|
-
const response = await fetch(`${endpoints.proxy}/files/encrypted/index`, {
|
|
1940
|
-
method: "POST",
|
|
1941
|
-
headers: {
|
|
1942
|
-
Authorization: apiKey,
|
|
1943
|
-
"Content-Type": "application/json"
|
|
1944
|
-
},
|
|
1945
|
-
body: JSON.stringify({ files: encryptedFiles }),
|
|
1946
|
-
signal: controller.signal
|
|
1947
|
-
});
|
|
1948
|
-
if (!response.ok) {
|
|
1949
|
-
throw new Error(`Index files request failed with status ${response.status}`);
|
|
1950
|
-
}
|
|
1951
|
-
const result = await response.json();
|
|
1952
|
-
return result;
|
|
1953
|
-
} catch (error) {
|
|
1954
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
1955
|
-
throw new Error(`Index files request timed out after ${timeoutMs}ms`);
|
|
1956
|
-
}
|
|
1957
|
-
throw error;
|
|
1958
|
-
} finally {
|
|
1959
|
-
clearTimeout(timeoutId);
|
|
1960
|
-
}
|
|
1961
|
-
}
|
|
1962
|
-
async function deleteIndex(apiKey, dekStore, options, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
1963
|
-
validateAPIKey(apiKey);
|
|
1964
|
-
validateDEKStore(dekStore);
|
|
1965
|
-
DeleteIndexOptionsSchema.parse(options);
|
|
1966
|
-
TimeoutSchema.parse(timeoutMs);
|
|
1967
|
-
const wrappedRagDEK = options.ragDEK || dekStore.ragDEK;
|
|
1968
|
-
if (!wrappedRagDEK) {
|
|
1969
|
-
throw new Error("RAG DEK not found. Provide ragDEK in options or ensure dekStore has a ragDEK.");
|
|
1970
|
-
}
|
|
1971
|
-
const _clientKEK = clientKEK ? hexToBytes4(clientKEK) : getClientKEK();
|
|
1972
|
-
const ragDEK = unwrapDEK(_clientKEK, wrappedRagDEK);
|
|
1973
|
-
const enclavePublicKey = await getEnclavePublicKey(timeoutMs);
|
|
1974
|
-
const { cipherText, sharedSecret } = createMLKEMEncapsulation(enclavePublicKey);
|
|
1975
|
-
const { encrypted: encryptedRagDEK, nonce: ragDEKNonce } = encryptPayload(sharedSecret, ragDEK);
|
|
1976
|
-
const controller = new AbortController;
|
|
1977
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
1978
|
-
try {
|
|
1979
|
-
const response = await fetch(`${endpoints.proxy}/files/encrypted/delete-index`, {
|
|
1980
|
-
method: "POST",
|
|
1981
|
-
headers: {
|
|
1982
|
-
Authorization: apiKey,
|
|
1983
|
-
"Content-Type": "application/json"
|
|
1984
|
-
},
|
|
1985
|
-
body: JSON.stringify({
|
|
1986
|
-
cipher_text: bytesToHex5(cipherText),
|
|
1987
|
-
encrypted_rag_dek: bytesToHex5(encryptedRagDEK),
|
|
1988
|
-
rag_dek_nonce: bytesToHex5(ragDEKNonce),
|
|
1989
|
-
fileIds: options.fileIds
|
|
1990
|
-
}),
|
|
1991
|
-
signal: controller.signal
|
|
1992
|
-
});
|
|
1993
|
-
if (!response.ok) {
|
|
1994
|
-
throw new Error(`Delete index request failed with status ${response.status}`);
|
|
1995
|
-
}
|
|
1996
|
-
const result = await response.json();
|
|
1997
|
-
return result;
|
|
1998
|
-
} catch (error) {
|
|
1999
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
2000
|
-
throw new Error(`Delete index request timed out after ${timeoutMs}ms`);
|
|
2001
|
-
}
|
|
2002
|
-
throw error;
|
|
2003
|
-
} finally {
|
|
2004
|
-
clearTimeout(timeoutId);
|
|
2005
|
-
}
|
|
2006
|
-
}
|
|
2007
|
-
function createFilesClient(apiKey, dekStore, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
2008
|
-
return {
|
|
2009
|
-
upload: (options) => uploadFile(apiKey, dekStore, options, clientKEK, timeoutMs),
|
|
2010
|
-
list: (options) => listFiles(apiKey, options, timeoutMs),
|
|
2011
|
-
get: (options) => getFile(apiKey, options, timeoutMs),
|
|
2012
|
-
delete: (options) => deleteFile(apiKey, options, timeoutMs),
|
|
2013
|
-
index: (options) => indexFiles(apiKey, dekStore, options, clientKEK, timeoutMs),
|
|
2014
|
-
deleteIndex: (options) => deleteIndex(apiKey, dekStore, options, clientKEK, timeoutMs)
|
|
2015
|
-
};
|
|
2016
|
-
}
|
|
2017
|
-
|
|
2018
|
-
// src/models/index.ts
|
|
2019
|
-
async function listModels(params, apiKey, timeoutMs) {
|
|
2020
|
-
const controller = new AbortController;
|
|
2021
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
2022
|
-
const queryParams = new URLSearchParams;
|
|
2023
|
-
if (params?.type !== undefined) {
|
|
2024
|
-
queryParams.append("type", params.type);
|
|
2025
|
-
}
|
|
2026
|
-
const queryString = queryParams.toString();
|
|
2027
|
-
const url = `${endpoints.proxy}/models${queryString ? `?${queryString}` : ""}`;
|
|
2028
|
-
try {
|
|
2029
|
-
const response = await fetch(url, {
|
|
2030
|
-
method: "GET",
|
|
2031
|
-
headers: {
|
|
2032
|
-
Authorization: apiKey,
|
|
2033
|
-
"Content-Type": "application/json"
|
|
2034
|
-
},
|
|
2035
|
-
signal: controller.signal
|
|
2036
|
-
});
|
|
2037
|
-
if (!response.ok) {
|
|
2038
|
-
throw new Error(`List models request failed with status ${response.status}`);
|
|
2039
|
-
}
|
|
2040
|
-
const result = await response.json();
|
|
2041
|
-
if (result.status !== 200) {
|
|
2042
|
-
throw new Error(result.error || "List models failed");
|
|
2043
|
-
}
|
|
2044
|
-
if (!result.data) {
|
|
2045
|
-
throw new Error("List models response missing data");
|
|
2046
|
-
}
|
|
2047
|
-
return result.data;
|
|
2048
|
-
} catch (error) {
|
|
2049
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
2050
|
-
throw new Error(`List models request timed out after ${timeoutMs}ms`);
|
|
2051
|
-
}
|
|
2052
|
-
throw error;
|
|
2053
|
-
} finally {
|
|
2054
|
-
clearTimeout(timeoutId);
|
|
2055
|
-
}
|
|
2056
|
-
}
|
|
2057
|
-
function createModelsClient(apiKey, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
2058
|
-
return {
|
|
2059
|
-
list: (params) => listModels(params, apiKey, timeoutMs)
|
|
2060
|
-
};
|
|
2061
|
-
}
|
|
2062
|
-
|
|
2063
|
-
// src/rvenc/index.ts
|
|
2064
|
-
import { bytesToHex as bytesToHex6, hexToBytes as hexToBytes5 } from "@noble/ciphers/utils.js";
|
|
2065
|
-
import OpenAI from "openai";
|
|
2066
|
-
function preprocessRequest(body, encryptionKeys) {
|
|
2067
|
-
const { cipherText, sharedSecret } = encryptionKeys;
|
|
2068
|
-
const { encrypted, nonce } = encryptPayload(sharedSecret, body);
|
|
2069
|
-
return {
|
|
2070
|
-
body: {
|
|
2071
|
-
cipherText: bytesToHex6(cipherText),
|
|
2072
|
-
encryptedInference: bytesToHex6(encrypted),
|
|
2073
|
-
nonce: bytesToHex6(nonce),
|
|
2074
|
-
model: body.model,
|
|
2075
|
-
stream: body.stream === true
|
|
2076
|
-
},
|
|
2077
|
-
sharedSecret,
|
|
2078
|
-
nonce
|
|
2079
|
-
};
|
|
2080
|
-
}
|
|
2081
|
-
async function postprocessStreamingResponse(response, sharedSecret, nonce, maxBufferSize) {
|
|
2082
|
-
if (!response.body) {
|
|
2083
|
-
throw new Error("Response body is null");
|
|
2084
|
-
}
|
|
2085
|
-
const reader = response.body.getReader();
|
|
2086
|
-
const generator = createDecryptedStreamGenerator(reader, sharedSecret, nonce, maxBufferSize);
|
|
2087
|
-
return {
|
|
2088
|
-
[Symbol.asyncIterator]() {
|
|
2089
|
-
return generator;
|
|
2090
|
-
}
|
|
2091
|
-
};
|
|
2092
|
-
}
|
|
2093
|
-
async function postprocessNonStreamingResponse(response, sharedSecret) {
|
|
2094
|
-
const data = await response.json();
|
|
2095
|
-
if (!data.encryptedResponse || !data.nonce) {
|
|
2096
|
-
throw new Error("Invalid non-streaming response: missing encryptedResponse or nonce");
|
|
2097
|
-
}
|
|
2098
|
-
const responseNonce = hexToBytes5(data.nonce);
|
|
2099
|
-
return decryptPayload(data.encryptedResponse, sharedSecret, responseNonce);
|
|
2100
|
-
}
|
|
2101
|
-
function createRvencChatClient(apiKey, encryptionKeys, requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, maxBufferSize = DEFAULT_MAX_BUFFER_SIZE, attest2 = true, OpenAIClientParams) {
|
|
2102
|
-
const client = new OpenAI({ apiKey: "not-used", ...OpenAIClientParams });
|
|
2103
|
-
const originalChatCreate = client.chat.completions.create.bind(client.chat.completions);
|
|
2104
|
-
client.chat.completions.create = async (body) => {
|
|
2105
|
-
const isStreaming = body.stream === true;
|
|
2106
|
-
const controller = new AbortController;
|
|
2107
|
-
const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);
|
|
2108
|
-
try {
|
|
2109
|
-
const sessionId = await attest(apiKey, { model: body.model, enabled: attest2 });
|
|
2110
|
-
const encryptedRequest = preprocessRequest(body, encryptionKeys);
|
|
2111
|
-
const response = await fetch(`${endpoints.proxy}/rvenc/chat/completions`, {
|
|
2112
|
-
method: "POST",
|
|
2113
|
-
headers: {
|
|
2114
|
-
"Content-Type": "application/json",
|
|
2115
|
-
Accept: isStreaming ? "text/event-stream" : "application/json",
|
|
2116
|
-
Authorization: apiKey,
|
|
2117
|
-
...sessionId && { "X-Session-Id": sessionId }
|
|
2118
|
-
},
|
|
2119
|
-
body: JSON.stringify(encryptedRequest.body),
|
|
2120
|
-
signal: controller.signal
|
|
2121
|
-
});
|
|
2122
|
-
if (!response.ok) {
|
|
2123
|
-
await throwIfErrorResponse(response);
|
|
2124
|
-
}
|
|
2125
|
-
clearTimeout(timeoutId);
|
|
2126
|
-
if (isStreaming) {
|
|
2127
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
2128
|
-
if (contentType.includes("text/event-stream")) {
|
|
2129
|
-
return await postprocessStreamingResponse(response, encryptedRequest.sharedSecret, encryptedRequest.nonce, maxBufferSize);
|
|
2130
|
-
}
|
|
2131
|
-
const completion = await postprocessNonStreamingResponse(response, encryptedRequest.sharedSecret);
|
|
2132
|
-
return completionToChunkStream(completion);
|
|
2133
|
-
}
|
|
2134
|
-
return await postprocessNonStreamingResponse(response, encryptedRequest.sharedSecret);
|
|
2135
|
-
} catch (error) {
|
|
2136
|
-
clearTimeout(timeoutId);
|
|
2137
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
2138
|
-
throw new Error(`Request timed out after ${requestTimeoutMs}ms`);
|
|
2139
|
-
}
|
|
2140
|
-
throw error;
|
|
2141
|
-
}
|
|
2142
|
-
};
|
|
2143
|
-
return client;
|
|
2144
|
-
}
|
|
2145
|
-
async function* completionToChunkStream(completion) {
|
|
2146
|
-
const choice = completion.choices[0];
|
|
2147
|
-
const message = choice?.message;
|
|
2148
|
-
const content = typeof message?.content === "string" ? message.content : "";
|
|
2149
|
-
const toolCalls = message?.tool_calls?.filter((tc) => tc.type === "function").map((tc, i) => ({
|
|
2150
|
-
index: i,
|
|
2151
|
-
id: tc.id,
|
|
2152
|
-
type: "function",
|
|
2153
|
-
function: {
|
|
2154
|
-
name: tc.function.name,
|
|
2155
|
-
arguments: tc.function.arguments
|
|
2156
|
-
}
|
|
2157
|
-
}));
|
|
2158
|
-
yield {
|
|
2159
|
-
id: completion.id,
|
|
2160
|
-
object: "chat.completion.chunk",
|
|
2161
|
-
created: completion.created,
|
|
2162
|
-
model: completion.model,
|
|
2163
|
-
choices: [
|
|
2164
|
-
{
|
|
2165
|
-
index: choice?.index ?? 0,
|
|
2166
|
-
delta: {
|
|
2167
|
-
role: "assistant",
|
|
2168
|
-
content,
|
|
2169
|
-
...toolCalls && toolCalls.length > 0 && { tool_calls: toolCalls }
|
|
2170
|
-
},
|
|
2171
|
-
finish_reason: choice?.finish_reason ?? "stop",
|
|
2172
|
-
logprobs: null
|
|
2173
|
-
}
|
|
2174
|
-
],
|
|
2175
|
-
usage: completion.usage ?? null
|
|
2176
|
-
};
|
|
2177
|
-
}
|
|
2178
|
-
async function* createDecryptedStreamGenerator(reader, sharedSecret, nonce, maxBufferSize) {
|
|
2179
|
-
const decoder = new TextDecoder;
|
|
2180
|
-
let buffer = "";
|
|
2181
|
-
try {
|
|
2182
|
-
while (true) {
|
|
2183
|
-
const { value, done } = await reader.read();
|
|
2184
|
-
if (done)
|
|
2185
|
-
break;
|
|
2186
|
-
buffer += decoder.decode(value, { stream: true });
|
|
2187
|
-
if (buffer.length > maxBufferSize) {
|
|
2188
|
-
throw new Error(`Stream buffer exceeded maximum size of ${maxBufferSize} bytes`);
|
|
2189
|
-
}
|
|
2190
|
-
const parts = buffer.split(`
|
|
2191
|
-
|
|
2192
|
-
`);
|
|
2193
|
-
for (let i = 0;i < parts.length - 1; i++) {
|
|
2194
|
-
const part = parts[i];
|
|
2195
|
-
const lines = part.split(`
|
|
2196
|
-
`);
|
|
2197
|
-
let event;
|
|
2198
|
-
let data;
|
|
2199
|
-
if (lines[0]) {
|
|
2200
|
-
const eventSplit = lines[0].split(": ");
|
|
2201
|
-
event = eventSplit[1];
|
|
2202
|
-
}
|
|
2203
|
-
if (lines[1]) {
|
|
2204
|
-
const dataSplit = lines[1].split(": ");
|
|
2205
|
-
data = dataSplit.slice(1).join(": ");
|
|
2206
|
-
}
|
|
2207
|
-
if (event === "done" && data === "[DONE]") {
|
|
2208
|
-
return;
|
|
2209
|
-
}
|
|
2210
|
-
if (event === "error") {
|
|
2211
|
-
const errorObj = JSON.parse(data || "{}");
|
|
2212
|
-
throw new Error(errorObj.error?.message || data || "Stream error");
|
|
2213
|
-
}
|
|
2214
|
-
if (event === "data" && data && data !== "[DONE]") {
|
|
2215
|
-
const chunk = decryptPayload(data, sharedSecret, nonce);
|
|
2216
|
-
if (chunk.error) {
|
|
2217
|
-
throw new Error(chunk.error.message || "Stream error");
|
|
2218
|
-
}
|
|
2219
|
-
yield chunk;
|
|
2220
|
-
}
|
|
2221
|
-
}
|
|
2222
|
-
buffer = parts[parts.length - 1];
|
|
2223
|
-
}
|
|
2224
|
-
} finally {
|
|
2225
|
-
reader.releaseLock();
|
|
2226
|
-
}
|
|
2227
|
-
}
|
|
2228
|
-
|
|
2229
|
-
// src/tools/index.ts
|
|
2230
|
-
import { bytesToHex as bytesToHex7, hexToBytes as hexToBytes6, randomBytes as randomBytes5 } from "@noble/ciphers/utils.js";
|
|
2231
|
-
var FILE_OUTPUT_TOOLS = ["generateImage", "audioGenerateFromText", "createFileForUser"];
|
|
2232
|
-
var FILE_INPUT_TOOLS = [
|
|
2233
|
-
"imageDescribeAndCaption",
|
|
2234
|
-
"imageDescribeAndCaptionFallback",
|
|
2235
|
-
"videoDescribeAndCaption",
|
|
2236
|
-
"getPDFContent",
|
|
2237
|
-
"getTextDocumentContent",
|
|
2238
|
-
"transcribeAudioToText",
|
|
2239
|
-
"transcribeAudioWithDiarization",
|
|
2240
|
-
"audioDiarization",
|
|
2241
|
-
"getSpreadsheetContent",
|
|
2242
|
-
"getPowerPointContent",
|
|
2243
|
-
"getDataFileContent",
|
|
2244
|
-
"getFileContentOCR"
|
|
2245
|
-
];
|
|
2246
|
-
var RAG_TOOLS = ["searchRag"];
|
|
2247
|
-
async function callToolRequest(toolName, body, apiKey, timeoutMs, attest2) {
|
|
2248
|
-
const controller = new AbortController;
|
|
2249
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
2250
|
-
try {
|
|
2251
|
-
const response = await fetch(`${endpoints.proxy}/tools/${toolName}`, {
|
|
2252
|
-
method: "POST",
|
|
2253
|
-
headers: {
|
|
2254
|
-
"Content-Type": "application/json",
|
|
2255
|
-
Authorization: apiKey
|
|
2256
|
-
},
|
|
2257
|
-
body: JSON.stringify(body),
|
|
2258
|
-
signal: controller.signal
|
|
2259
|
-
});
|
|
2260
|
-
clearTimeout(timeoutId);
|
|
2261
|
-
if (!response.ok) {
|
|
2262
|
-
await throwIfErrorResponse(response);
|
|
2263
|
-
}
|
|
2264
|
-
const data = await response.json();
|
|
2265
|
-
return data.data;
|
|
2266
|
-
} catch (error) {
|
|
2267
|
-
clearTimeout(timeoutId);
|
|
2268
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
2269
|
-
throw new Error(`Tool request timed out after ${timeoutMs}ms`);
|
|
2270
|
-
}
|
|
2271
|
-
throw new Error(`Tool request failed: ${error instanceof Error ? error.message : error}`);
|
|
2272
|
-
}
|
|
2273
|
-
}
|
|
2274
|
-
async function downloadEncryptedFile(fileId, apiKey, timeoutMs) {
|
|
2275
|
-
const controller = new AbortController;
|
|
2276
|
-
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
2277
|
-
try {
|
|
2278
|
-
const metadataResponse = await fetch(`${endpoints.proxy}/files/encrypted/${fileId}?url=true`, {
|
|
2279
|
-
headers: { Authorization: apiKey },
|
|
2280
|
-
signal: controller.signal
|
|
2281
|
-
});
|
|
2282
|
-
if (!metadataResponse.ok) {
|
|
2283
|
-
throw new Error(`Failed to get file metadata: ${metadataResponse.status}`);
|
|
2284
|
-
}
|
|
2285
|
-
const metadata = await metadataResponse.json();
|
|
2286
|
-
const downloadUrl = metadata.data?.url;
|
|
2287
|
-
if (!downloadUrl) {
|
|
2288
|
-
throw new Error("No download URL in response");
|
|
2289
|
-
}
|
|
2290
|
-
const fileResponse = await fetch(downloadUrl, { signal: controller.signal });
|
|
2291
|
-
if (!fileResponse.ok) {
|
|
2292
|
-
throw new Error(`Failed to download file: ${fileResponse.status}`);
|
|
2293
|
-
}
|
|
2294
|
-
clearTimeout(timeoutId);
|
|
2295
|
-
const arrayBuffer = await fileResponse.arrayBuffer();
|
|
2296
|
-
return new Uint8Array(arrayBuffer);
|
|
2297
|
-
} catch (error) {
|
|
2298
|
-
clearTimeout(timeoutId);
|
|
2299
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
2300
|
-
throw new Error(`File download timed out after ${timeoutMs}ms`);
|
|
2301
|
-
}
|
|
2302
|
-
throw error;
|
|
2303
|
-
}
|
|
2304
|
-
}
|
|
2305
|
-
async function downloadAndDecryptFile(response, dek, apiKey, timeoutMs) {
|
|
2306
|
-
if (!response.success || !response.fileId) {
|
|
2307
|
-
return null;
|
|
2308
|
-
}
|
|
2309
|
-
const decryptFileName = (encryptedHex) => {
|
|
2310
|
-
const encrypted = hexToBytes6(encryptedHex);
|
|
2311
|
-
const decrypted = decryptWithDEK(dek, encrypted);
|
|
2312
|
-
return new TextDecoder().decode(decrypted);
|
|
2313
|
-
};
|
|
2314
|
-
const fileName = decryptFileName(response.fileName);
|
|
2315
|
-
const mimeType = decryptFileName(response.mimeType);
|
|
2316
|
-
const encryptedFile = await downloadEncryptedFile(response.fileId, apiKey, timeoutMs);
|
|
2317
|
-
const decryptedFile = decryptWithDEK(dek, encryptedFile);
|
|
2318
|
-
return {
|
|
2319
|
-
fileId: response.fileId,
|
|
2320
|
-
fileName,
|
|
2321
|
-
mimeType,
|
|
2322
|
-
content: decryptedFile,
|
|
2323
|
-
fileSize: decryptedFile.length
|
|
2324
|
-
};
|
|
2325
|
-
}
|
|
2326
|
-
async function callSimpleTool(toolName, params, apiKey, timeoutMs, attest2) {
|
|
2327
|
-
const enclavePublicKey = await getEnclavePublicKey(timeoutMs);
|
|
2328
|
-
const { cipherText, sharedSecret } = createMLKEMEncapsulation(enclavePublicKey);
|
|
2329
|
-
const { encrypted, nonce } = encryptPayload(sharedSecret, params);
|
|
2330
|
-
const body = {
|
|
2331
|
-
cipherText: bytesToHex7(cipherText),
|
|
2332
|
-
encryptedParams: bytesToHex7(encrypted),
|
|
2333
|
-
nonce: bytesToHex7(nonce)
|
|
2334
|
-
};
|
|
2335
|
-
const response = await callToolRequest(toolName, body, apiKey, timeoutMs, attest2);
|
|
2336
|
-
return decryptPayload(response.encryptedResponse, sharedSecret, hexToBytes6(response.nonce));
|
|
2337
|
-
}
|
|
2338
|
-
async function callFileOutputTool(toolName, params, apiKey, dekStore, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, attest2 = true) {
|
|
2339
|
-
const enclavePublicKey = await getEnclavePublicKey(timeoutMs);
|
|
2340
|
-
const { cipherText, sharedSecret } = createMLKEMEncapsulation(enclavePublicKey);
|
|
2341
|
-
const { encrypted, nonce } = encryptPayload(sharedSecret, params);
|
|
2342
|
-
const dek = randomBytes5(32);
|
|
2343
|
-
const { encrypted: encryptedDEK, nonce: dekNonce } = encryptPayload(sharedSecret, dek);
|
|
2344
|
-
const _clientKEK = clientKEK ? hexToBytes6(clientKEK) : getClientKEK();
|
|
2345
|
-
const wrappedDEK = wrapDEK(_clientKEK, dek);
|
|
2346
|
-
const clientKID = clientKEK ? getClientKID(clientKEK) : getClientKID();
|
|
2347
|
-
const body = {
|
|
2348
|
-
cipherText: bytesToHex7(cipherText),
|
|
2349
|
-
encryptedParams: bytesToHex7(encrypted),
|
|
2350
|
-
nonce: bytesToHex7(nonce),
|
|
2351
|
-
encryptedDEK: bytesToHex7(encryptedDEK),
|
|
2352
|
-
dekNonce: bytesToHex7(dekNonce),
|
|
2353
|
-
kid: clientKID,
|
|
2354
|
-
wrappedDEK: bytesToHex7(wrappedDEK)
|
|
2355
|
-
};
|
|
2356
|
-
const response = await callToolRequest(toolName, body, apiKey, timeoutMs, attest2);
|
|
2357
|
-
const result = await downloadAndDecryptFile(response, dek, apiKey, timeoutMs);
|
|
2358
|
-
if (result?.fileId) {
|
|
2359
|
-
if (!dekStore.fileDEKs) {
|
|
2360
|
-
dekStore.fileDEKs = new Map;
|
|
2361
|
-
}
|
|
2362
|
-
dekStore.fileDEKs.set(result.fileId, wrappedDEK);
|
|
2363
|
-
}
|
|
2364
|
-
return result;
|
|
2365
|
-
}
|
|
2366
|
-
async function callFileInputTool(toolName, params, apiKey, dekStore, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, attest2 = true) {
|
|
2367
|
-
if (!params.fileId) {
|
|
2368
|
-
throw new Error(`Tool ${toolName} requires fileId parameter`);
|
|
2369
|
-
}
|
|
2370
|
-
const enclavePublicKey = await getEnclavePublicKey(timeoutMs);
|
|
2371
|
-
const { cipherText, sharedSecret } = createMLKEMEncapsulation(enclavePublicKey);
|
|
2372
|
-
const dek = randomBytes5(32);
|
|
2373
|
-
const { encrypted: encryptedDEK, nonce: dekNonce } = encryptPayload(sharedSecret, dek);
|
|
2374
|
-
const nonce = randomBytes5(24);
|
|
2375
|
-
if (!dekStore.fileDEKs) {
|
|
2376
|
-
dekStore.fileDEKs = new Map;
|
|
2377
|
-
}
|
|
2378
|
-
const _clientKEK = clientKEK ? hexToBytes6(clientKEK) : getClientKEK();
|
|
2379
|
-
let fileDEK = dekStore.fileDEKs.get(params.fileId);
|
|
2380
|
-
if (!fileDEK) {
|
|
2381
|
-
fileDEK = randomBytes5(32);
|
|
2382
|
-
const wrappedFileDEK = wrapDEK(_clientKEK, fileDEK);
|
|
2383
|
-
dekStore.fileDEKs.set(params.fileId, wrappedFileDEK);
|
|
2384
|
-
} else {
|
|
2385
|
-
fileDEK = unwrapDEK(_clientKEK, fileDEK);
|
|
2386
|
-
}
|
|
2387
|
-
const { encrypted: encryptedFileDEK, nonce: fileDEKNonce } = encryptPayload(sharedSecret, fileDEK);
|
|
2388
|
-
const body = {
|
|
2389
|
-
cipherText: bytesToHex7(cipherText),
|
|
2390
|
-
nonce: bytesToHex7(nonce),
|
|
2391
|
-
fileId: params.fileId,
|
|
2392
|
-
encryptedDEK: bytesToHex7(encryptedDEK),
|
|
2393
|
-
dekNonce: bytesToHex7(dekNonce),
|
|
2394
|
-
encryptedFileDEK: bytesToHex7(encryptedFileDEK),
|
|
2395
|
-
fileDEKNonce: bytesToHex7(fileDEKNonce)
|
|
2396
|
-
};
|
|
2397
|
-
const response = await callToolRequest(toolName, body, apiKey, timeoutMs, attest2);
|
|
2398
|
-
return decryptPayload(response.encryptedResponse, sharedSecret, hexToBytes6(response.nonce));
|
|
2399
|
-
}
|
|
2400
|
-
async function callRagTool(toolName, params, apiKey, dekStore, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, attest2 = true) {
|
|
2401
|
-
const enclavePublicKey = await getEnclavePublicKey(timeoutMs);
|
|
2402
|
-
const { cipherText, sharedSecret } = createMLKEMEncapsulation(enclavePublicKey);
|
|
2403
|
-
const { encrypted, nonce } = encryptPayload(sharedSecret, params);
|
|
2404
|
-
const dek = randomBytes5(32);
|
|
2405
|
-
const { encrypted: encryptedDEK, nonce: dekNonce } = encryptPayload(sharedSecret, dek);
|
|
2406
|
-
if (!dekStore.fileDEKs) {
|
|
2407
|
-
dekStore.fileDEKs = new Map;
|
|
2408
|
-
}
|
|
2409
|
-
let fileIds = [];
|
|
2410
|
-
if (dekStore.fileDEKs.size > 0) {
|
|
2411
|
-
fileIds = Array.from(dekStore.fileDEKs.keys());
|
|
2412
|
-
}
|
|
2413
|
-
const _clientKEK = clientKEK ? hexToBytes6(clientKEK) : getClientKEK();
|
|
2414
|
-
const encryptedFileDEKs = fileIds.reduce((acc, fileId) => {
|
|
2415
|
-
const fileDEK = dekStore.fileDEKs?.get(fileId);
|
|
2416
|
-
if (!fileDEK) {
|
|
2417
|
-
return acc;
|
|
2418
|
-
}
|
|
2419
|
-
const unwrappedFileDEK = unwrapDEK(_clientKEK, fileDEK);
|
|
2420
|
-
const { encrypted: encryptedFileDEK, nonce: fileDEKNonce } = encryptPayload(sharedSecret, unwrappedFileDEK);
|
|
2421
|
-
acc.push({
|
|
2422
|
-
fileId,
|
|
2423
|
-
encryptedDEK: bytesToHex7(encryptedFileDEK),
|
|
2424
|
-
nonce: bytesToHex7(fileDEKNonce)
|
|
2425
|
-
});
|
|
2426
|
-
return acc;
|
|
2427
|
-
}, []);
|
|
2428
|
-
if (!dekStore.ragDEK) {
|
|
2429
|
-
throw new Error("RAG DEK not found in dekStore. Please upload at least one file with ragIndex: true to initialize RAG.");
|
|
2430
|
-
}
|
|
2431
|
-
if (!dekStore.ragVersion) {
|
|
2432
|
-
throw new Error("RAG Version not found in dekStore. Please upload at least one file with ragIndex: true to initialize RAG.");
|
|
2433
|
-
}
|
|
2434
|
-
const ragDEK = unwrapDEK(_clientKEK, dekStore.ragDEK);
|
|
2435
|
-
const { encrypted: encryptedRagDEK, nonce: ragDEKNonce } = encryptPayload(sharedSecret, ragDEK);
|
|
2436
|
-
const { encrypted: encryptedRagVersion, nonce: ragVersionNonce } = encryptPayload(sharedSecret, dekStore.ragVersion);
|
|
2437
|
-
const body = {
|
|
2438
|
-
cipherText: bytesToHex7(cipherText),
|
|
2439
|
-
encryptedParams: bytesToHex7(encrypted),
|
|
2440
|
-
nonce: bytesToHex7(nonce),
|
|
2441
|
-
encryptedDEK: bytesToHex7(encryptedDEK),
|
|
2442
|
-
dekNonce: bytesToHex7(dekNonce),
|
|
2443
|
-
encryptedFileDEKs,
|
|
2444
|
-
encryptedRagDEK: bytesToHex7(encryptedRagDEK),
|
|
2445
|
-
ragDEKNonce: bytesToHex7(ragDEKNonce),
|
|
2446
|
-
encryptedRagVersion: bytesToHex7(encryptedRagVersion),
|
|
2447
|
-
ragVersionNonce: bytesToHex7(ragVersionNonce)
|
|
2448
|
-
};
|
|
2449
|
-
const response = await callToolRequest(toolName, body, apiKey, timeoutMs, attest2);
|
|
2450
|
-
return decryptPayload(response.encryptedResponse, sharedSecret, hexToBytes6(response.nonce));
|
|
2451
|
-
}
|
|
2452
|
-
async function callTool(toolName, params, apiKey, dekStore, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, attest2 = true) {
|
|
2453
|
-
if (FILE_OUTPUT_TOOLS.includes(toolName)) {
|
|
2454
|
-
return callFileOutputTool(toolName, params, apiKey, dekStore, clientKEK, timeoutMs, attest2);
|
|
2455
|
-
} else if (FILE_INPUT_TOOLS.includes(toolName)) {
|
|
2456
|
-
return callFileInputTool(toolName, params, apiKey, dekStore, clientKEK, timeoutMs, attest2);
|
|
2457
|
-
} else if (RAG_TOOLS.includes(toolName)) {
|
|
2458
|
-
return callRagTool(toolName, params, apiKey, dekStore, clientKEK, timeoutMs, attest2);
|
|
2459
|
-
} else {
|
|
2460
|
-
return callSimpleTool(toolName, params, apiKey, timeoutMs, attest2);
|
|
2461
|
-
}
|
|
2462
|
-
}
|
|
2463
|
-
function createToolsClient(apiKey, dekStore, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, attest2 = true) {
|
|
2464
|
-
return {
|
|
2465
|
-
generateImage: (params) => callTool("generateImage", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
2466
|
-
audioGenerateFromText: (params) => callTool("audioGenerateFromText", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
2467
|
-
createFileForUser: (params) => callTool("createFileForUser", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
2468
|
-
imageDescribeAndCaption: (params) => callTool("imageDescribeAndCaption", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
2469
|
-
imageDescribeAndCaptionFallback: (params) => callTool("imageDescribeAndCaptionFallback", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
2470
|
-
videoDescribeAndCaption: (params) => callTool("videoDescribeAndCaption", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
2471
|
-
getPDFContent: (params) => callTool("getPDFContent", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
2472
|
-
getTextDocumentContent: (params) => callTool("getTextDocumentContent", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
2473
|
-
transcribeAudioToText: (params) => callTool("transcribeAudioToText", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
2474
|
-
transcribeAudioWithDiarization: (params) => callTool("transcribeAudioWithDiarization", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
2475
|
-
audioDiarization: (params) => callTool("audioDiarization", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
2476
|
-
getFileContentOCR: (params) => callTool("getFileContentOCR", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
2477
|
-
getSpreadsheetContent: (params) => callTool("getSpreadsheetContent", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
2478
|
-
getDataFileContent: (params) => callTool("getDataFileContent", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
2479
|
-
getPowerPointContent: (params) => callTool("getPowerPointContent", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
2480
|
-
getTime: (params) => callTool("getTime", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
2481
|
-
webSearchTool: (params) => callTool("webSearchTool", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
2482
|
-
webPageScraperTool: (params) => callTool("webPageScraperTool", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
|
|
2483
|
-
searchRag: (params) => callTool("searchRag", params, apiKey, dekStore, clientKEK, timeoutMs, attest2)
|
|
2484
|
-
};
|
|
2485
|
-
}
|
|
114
|
+
filePath: z.string().min(1, "File path is required"),
|
|
115
|
+
fileDEK: z.instanceof(Uint8Array).optional()
|
|
116
|
+
});
|
|
117
|
+
var IndexFilesOptionsSchema = z.object({
|
|
118
|
+
files: z.array(IndexFileInputSchema).min(1, "Files array must not be empty"),
|
|
119
|
+
ragDEK: z.instanceof(Uint8Array).optional()
|
|
120
|
+
});
|
|
121
|
+
var DeleteIndexOptionsSchema = z.object({
|
|
122
|
+
fileIds: z.array(z.string().min(1)).min(1, "File IDs array must not be empty"),
|
|
123
|
+
ragDEK: z.instanceof(Uint8Array).optional()
|
|
124
|
+
});
|
|
2486
125
|
|
|
2487
|
-
// src/
|
|
2488
|
-
|
|
2489
|
-
|
|
2490
|
-
apiKey,
|
|
2491
|
-
clientKEK,
|
|
2492
|
-
requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS,
|
|
2493
|
-
maxBufferSize = DEFAULT_MAX_BUFFER_SIZE,
|
|
2494
|
-
attest: attest2 = true
|
|
2495
|
-
} = options;
|
|
2496
|
-
if (options.config?.endpoints !== undefined) {
|
|
2497
|
-
Object.assign(endpoints, options.config.endpoints);
|
|
2498
|
-
}
|
|
2499
|
-
let encryptionKeys;
|
|
2500
|
-
try {
|
|
2501
|
-
encryptionKeys = options.encryptionKeys ?? await generateEncryptionKeys(requestTimeoutMs);
|
|
2502
|
-
} catch (error) {
|
|
2503
|
-
throw new Error(`Failed to initialize encryption keys: ${error instanceof Error ? error.message : error}`);
|
|
2504
|
-
}
|
|
2505
|
-
const dekStore = options.dekStore ?? initializeDEKStore(clientKEK);
|
|
2506
|
-
const client = createRvencChatClient(apiKey, encryptionKeys, requestTimeoutMs, maxBufferSize, attest2, options.config?.openAIClientOptions ?? {});
|
|
2507
|
-
client.files = createFilesClient(apiKey, dekStore, clientKEK, requestTimeoutMs);
|
|
2508
|
-
client.tools = createToolsClient(apiKey, dekStore, clientKEK, requestTimeoutMs, attest2);
|
|
2509
|
-
client.audio = createAudioClient(apiKey, encryptionKeys, requestTimeoutMs, attest2);
|
|
2510
|
-
client.models = createModelsClient(apiKey, requestTimeoutMs);
|
|
2511
|
-
client.dekStore = dekStore;
|
|
2512
|
-
return client;
|
|
2513
|
-
}
|
|
2514
|
-
var core_default = createRvencClient;
|
|
126
|
+
// src/rvenc/index.ts
|
|
127
|
+
import { bytesToHex as bytesToHex5, hexToBytes as hexToBytes5 } from "@noble/ciphers/utils.js";
|
|
128
|
+
import OpenAI from "openai";
|
|
2515
129
|
|
|
130
|
+
// src/tools/index.ts
|
|
131
|
+
import { bytesToHex as bytesToHex6, hexToBytes as hexToBytes6, randomBytes as randomBytes4 } from "@noble/ciphers/utils.js";
|
|
2516
132
|
// src/server/runtime.ts
|
|
2517
|
-
var DEFAULT_HOST =
|
|
2518
|
-
var DEFAULT_PORT =
|
|
133
|
+
var DEFAULT_HOST = "127.0.0.1";
|
|
134
|
+
var DEFAULT_PORT = 8787;
|
|
2519
135
|
var CLIENT_CACHE_MAX = (() => {
|
|
2520
136
|
let cacheTTL = 256;
|
|
2521
137
|
const raw = process.env.CLIENT_CACHE_MAX;
|
|
@@ -2529,509 +145,342 @@ var CLIENT_CACHE_MAX = (() => {
|
|
|
2529
145
|
var serverProxyUrl = process.env.PROXY_URL;
|
|
2530
146
|
var serverEnclaveUrl = process.env.ENCLAVE_URL;
|
|
2531
147
|
var serverKek = process.env.CLIENT_KEK;
|
|
2532
|
-
var serverAttest = true;
|
|
2533
148
|
var clientCache = new Map;
|
|
2534
149
|
var storage = multer.memoryStorage();
|
|
2535
150
|
var audioUpload = multer({
|
|
2536
151
|
storage,
|
|
2537
152
|
limits: { fileSize: 25 * 1024 * 1024 }
|
|
2538
153
|
});
|
|
2539
|
-
function applyServerOptions(options) {
|
|
2540
|
-
const { proxyUrl, enclaveUrl, kek, attest: attest2 } = options;
|
|
2541
|
-
serverAttest = attest2 !== false;
|
|
2542
|
-
if (proxyUrl) {
|
|
2543
|
-
serverProxyUrl = proxyUrl;
|
|
2544
|
-
}
|
|
2545
|
-
if (enclaveUrl) {
|
|
2546
|
-
serverEnclaveUrl = enclaveUrl;
|
|
2547
|
-
}
|
|
2548
|
-
if (kek) {
|
|
2549
|
-
serverKek = kek;
|
|
2550
|
-
}
|
|
2551
|
-
}
|
|
2552
|
-
async function getOrCreateRvencClient(apiKey) {
|
|
2553
|
-
const existing = clientCache.get(apiKey);
|
|
2554
|
-
if (existing)
|
|
2555
|
-
return existing;
|
|
2556
|
-
const client = await core_default({
|
|
2557
|
-
apiKey,
|
|
2558
|
-
clientKEK: serverKek,
|
|
2559
|
-
attest: serverAttest,
|
|
2560
|
-
config: {
|
|
2561
|
-
endpoints: {
|
|
2562
|
-
enclave: serverEnclaveUrl,
|
|
2563
|
-
proxy: serverProxyUrl
|
|
2564
|
-
}
|
|
2565
|
-
}
|
|
2566
|
-
});
|
|
2567
|
-
clientCache.set(apiKey, client);
|
|
2568
|
-
if (clientCache.size > CLIENT_CACHE_MAX) {
|
|
2569
|
-
const oldest = clientCache.keys().next().value;
|
|
2570
|
-
if (oldest !== undefined)
|
|
2571
|
-
clientCache.delete(oldest);
|
|
2572
|
-
}
|
|
2573
|
-
return client;
|
|
2574
|
-
}
|
|
2575
154
|
|
|
2576
|
-
// src/
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
}
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
}
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
code: "invalid_api_key"
|
|
2593
|
-
}
|
|
2594
|
-
});
|
|
2595
|
-
}
|
|
2596
|
-
function sendServerError(res, error) {
|
|
2597
|
-
const err = error;
|
|
2598
|
-
res.status(err.status ?? 500).json({
|
|
2599
|
-
error: {
|
|
2600
|
-
message: err.message ?? "Internal server error",
|
|
2601
|
-
type: err.type ?? "server_error",
|
|
2602
|
-
code: err.code
|
|
155
|
+
// src/launcher/model-picker.tsx
|
|
156
|
+
import { Box, render, Text, useApp, useInput, useWindowSize } from "ink";
|
|
157
|
+
import { useMemo, useState } from "react";
|
|
158
|
+
import { jsxDEV } from "react/jsx-dev-runtime";
|
|
159
|
+
function ModelPicker({ models, onSelect, onCancel }) {
|
|
160
|
+
const { exit } = useApp();
|
|
161
|
+
const { rows: termRows } = useWindowSize();
|
|
162
|
+
const [cursor, setCursor] = useState(0);
|
|
163
|
+
const modelLabels = useMemo(() => models.map((m) => m.display_name && m.display_name !== m.id ? `${m.id} — ${m.display_name}` : m.id), [models]);
|
|
164
|
+
const visibleCount = Math.max(1, Math.min(modelLabels.length, termRows - 4));
|
|
165
|
+
const scrollOffset = Math.max(0, Math.min(cursor - Math.floor(visibleCount / 2), modelLabels.length - visibleCount));
|
|
166
|
+
const windowedLabels = modelLabels.slice(scrollOffset, scrollOffset + visibleCount);
|
|
167
|
+
useInput((_input, key) => {
|
|
168
|
+
if (key.upArrow || _input === "k" && !key.ctrl) {
|
|
169
|
+
setCursor((c) => Math.max(0, c - 1));
|
|
170
|
+
return;
|
|
2603
171
|
}
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
const slash = modelId.indexOf("/");
|
|
2608
|
-
if (slash > 0) {
|
|
2609
|
-
return modelId.slice(0, slash);
|
|
2610
|
-
}
|
|
2611
|
-
return "prem";
|
|
2612
|
-
}
|
|
2613
|
-
function isoToUnix(iso) {
|
|
2614
|
-
const t = Date.parse(iso);
|
|
2615
|
-
if (!Number.isFinite(t)) {
|
|
2616
|
-
return 0;
|
|
2617
|
-
}
|
|
2618
|
-
return Math.floor(t / 1000);
|
|
2619
|
-
}
|
|
2620
|
-
function registerOpenAICompatRoutes(router, deps) {
|
|
2621
|
-
router.get("/v1/models", async (req, res) => {
|
|
2622
|
-
try {
|
|
2623
|
-
const apiKey = extractApiKey(req);
|
|
2624
|
-
if (!apiKey) {
|
|
2625
|
-
return sendUnauthorized(res);
|
|
2626
|
-
}
|
|
2627
|
-
const client = await deps.getOrCreateClient(apiKey);
|
|
2628
|
-
const all = await client.models.list();
|
|
2629
|
-
const data = all.filter((m) => m.enabled !== 0).map((m) => ({
|
|
2630
|
-
id: m.model,
|
|
2631
|
-
object: "model",
|
|
2632
|
-
created: isoToUnix(m.created_at),
|
|
2633
|
-
owned_by: openAIOwnedBy(m.model)
|
|
2634
|
-
}));
|
|
2635
|
-
res.json({ object: "list", data });
|
|
2636
|
-
} catch (error) {
|
|
2637
|
-
sendServerError(res, error);
|
|
172
|
+
if (key.downArrow || _input === "j" && !key.ctrl) {
|
|
173
|
+
setCursor((c) => Math.min(modelLabels.length - 1, c + 1));
|
|
174
|
+
return;
|
|
2638
175
|
}
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
if (!apiKey) {
|
|
2644
|
-
return sendUnauthorized(res);
|
|
2645
|
-
}
|
|
2646
|
-
const client = await deps.getOrCreateClient(apiKey);
|
|
2647
|
-
const params = req.body;
|
|
2648
|
-
const completion = await client.chat.completions.create(params);
|
|
2649
|
-
if (params.stream) {
|
|
2650
|
-
res.setHeader("Content-Type", "text/event-stream");
|
|
2651
|
-
res.setHeader("Cache-Control", "no-cache");
|
|
2652
|
-
res.setHeader("Connection", "keep-alive");
|
|
2653
|
-
if (completion && typeof completion === "object" && Symbol.asyncIterator in completion) {
|
|
2654
|
-
try {
|
|
2655
|
-
for await (const chunk of completion) {
|
|
2656
|
-
res.write(`data: ${JSON.stringify(chunk)}
|
|
2657
|
-
|
|
2658
|
-
`);
|
|
2659
|
-
}
|
|
2660
|
-
res.write(`data: [DONE]
|
|
2661
|
-
|
|
2662
|
-
`);
|
|
2663
|
-
res.end();
|
|
2664
|
-
} catch (streamErr) {
|
|
2665
|
-
if (!res.headersSent) {
|
|
2666
|
-
sendServerError(res, streamErr);
|
|
2667
|
-
} else {
|
|
2668
|
-
res.end();
|
|
2669
|
-
}
|
|
2670
|
-
}
|
|
2671
|
-
} else {
|
|
2672
|
-
res.write(`data: ${JSON.stringify(completion)}
|
|
2673
|
-
|
|
2674
|
-
`);
|
|
2675
|
-
res.write(`data: [DONE]
|
|
2676
|
-
|
|
2677
|
-
`);
|
|
2678
|
-
res.end();
|
|
2679
|
-
}
|
|
2680
|
-
} else {
|
|
2681
|
-
res.json(completion);
|
|
2682
|
-
}
|
|
2683
|
-
} catch (error) {
|
|
2684
|
-
sendServerError(res, error);
|
|
176
|
+
if (key.return) {
|
|
177
|
+
onSelect(models[cursor]);
|
|
178
|
+
exit();
|
|
179
|
+
return;
|
|
2685
180
|
}
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
const apiKey = extractApiKey(req);
|
|
2690
|
-
if (!apiKey) {
|
|
2691
|
-
return sendUnauthorized(res);
|
|
2692
|
-
}
|
|
2693
|
-
if (!req.file) {
|
|
2694
|
-
return res.status(400).json({
|
|
2695
|
-
error: {
|
|
2696
|
-
message: "Missing required file parameter",
|
|
2697
|
-
type: "invalid_request_error"
|
|
2698
|
-
}
|
|
2699
|
-
});
|
|
2700
|
-
}
|
|
2701
|
-
const client = await deps.getOrCreateClient(apiKey);
|
|
2702
|
-
const file = new File([req.file.buffer], req.file.originalname, {
|
|
2703
|
-
type: req.file.mimetype
|
|
2704
|
-
});
|
|
2705
|
-
const params = {
|
|
2706
|
-
file,
|
|
2707
|
-
model: req.body.model
|
|
2708
|
-
};
|
|
2709
|
-
if (req.body.language) {
|
|
2710
|
-
params.language = req.body.language;
|
|
2711
|
-
}
|
|
2712
|
-
if (req.body.prompt) {
|
|
2713
|
-
params.prompt = req.body.prompt;
|
|
2714
|
-
}
|
|
2715
|
-
if (req.body.response_format) {
|
|
2716
|
-
params.response_format = req.body.response_format;
|
|
2717
|
-
}
|
|
2718
|
-
if (req.body.temperature) {
|
|
2719
|
-
params.temperature = parseFloat(req.body.temperature);
|
|
2720
|
-
}
|
|
2721
|
-
if (req.body.timestamp_granularities) {
|
|
2722
|
-
params.timestamp_granularities = Array.isArray(req.body.timestamp_granularities) ? req.body.timestamp_granularities : JSON.parse(req.body.timestamp_granularities);
|
|
2723
|
-
}
|
|
2724
|
-
const transcription = await client.audio.transcriptions.create(params);
|
|
2725
|
-
res.json(transcription);
|
|
2726
|
-
} catch (error) {
|
|
2727
|
-
sendServerError(res, error);
|
|
181
|
+
if (key.escape) {
|
|
182
|
+
onCancel?.();
|
|
183
|
+
exit();
|
|
2728
184
|
}
|
|
2729
185
|
});
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
186
|
+
return /* @__PURE__ */ jsxDEV(Box, {
|
|
187
|
+
flexDirection: "column",
|
|
188
|
+
paddingTop: 1,
|
|
189
|
+
children: [
|
|
190
|
+
/* @__PURE__ */ jsxDEV(Text, {
|
|
191
|
+
dimColor: true,
|
|
192
|
+
children: "Available models (use ↑/↓ or j/k to navigate, Enter to select):"
|
|
193
|
+
}, undefined, false, undefined, this),
|
|
194
|
+
/* @__PURE__ */ jsxDEV(Box, {
|
|
195
|
+
flexDirection: "column",
|
|
196
|
+
paddingTop: 1,
|
|
197
|
+
children: windowedLabels.map((label, i) => {
|
|
198
|
+
const globalIdx = scrollOffset + i;
|
|
199
|
+
if (globalIdx === cursor) {
|
|
200
|
+
return /* @__PURE__ */ jsxDEV(Box, {
|
|
201
|
+
paddingLeft: 2,
|
|
202
|
+
children: /* @__PURE__ */ jsxDEV(Text, {
|
|
203
|
+
inverse: true,
|
|
204
|
+
children: label.padEnd(process.stdout.columns - 4)
|
|
205
|
+
}, undefined, false, undefined, this)
|
|
206
|
+
}, label, false, undefined, this);
|
|
2741
207
|
}
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
208
|
+
return /* @__PURE__ */ jsxDEV(Box, {
|
|
209
|
+
paddingLeft: 2,
|
|
210
|
+
children: /* @__PURE__ */ jsxDEV(Text, {
|
|
211
|
+
children: label
|
|
212
|
+
}, undefined, false, undefined, this)
|
|
213
|
+
}, label, false, undefined, this);
|
|
214
|
+
})
|
|
215
|
+
}, undefined, false, undefined, this),
|
|
216
|
+
modelLabels.length > visibleCount && /* @__PURE__ */ jsxDEV(Box, {
|
|
217
|
+
paddingLeft: 2,
|
|
218
|
+
paddingTop: 1,
|
|
219
|
+
children: /* @__PURE__ */ jsxDEV(Text, {
|
|
220
|
+
dimColor: true,
|
|
221
|
+
children: [
|
|
222
|
+
scrollOffset > 0 ? "↑ more" : "",
|
|
223
|
+
scrollOffset > 0 && scrollOffset + visibleCount < modelLabels.length ? " · " : "",
|
|
224
|
+
scrollOffset + visibleCount < modelLabels.length ? "↓ more" : ""
|
|
225
|
+
]
|
|
226
|
+
}, undefined, true, undefined, this)
|
|
227
|
+
}, undefined, false, undefined, this)
|
|
228
|
+
]
|
|
229
|
+
}, undefined, true, undefined, this);
|
|
230
|
+
}
|
|
231
|
+
function interactivePickModel(models) {
|
|
232
|
+
return new Promise((resolve, reject) => {
|
|
233
|
+
const { unmount, waitUntilExit } = render(/* @__PURE__ */ jsxDEV(ModelPicker, {
|
|
234
|
+
models,
|
|
235
|
+
onSelect: (model) => resolve(model),
|
|
236
|
+
onCancel: () => reject(new Error("Aborted by user (Escape)."))
|
|
237
|
+
}, undefined, false, undefined, this), { exitOnCtrlC: true });
|
|
238
|
+
waitUntilExit().then(() => {
|
|
239
|
+
unmount();
|
|
240
|
+
});
|
|
2766
241
|
});
|
|
2767
242
|
}
|
|
2768
243
|
|
|
2769
|
-
// src/
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
}
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
return `${prefix}${s}`;
|
|
2800
|
-
}
|
|
244
|
+
// src/utils/debug.ts
|
|
245
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
246
|
+
import { dirname } from "node:path";
|
|
247
|
+
import envPaths from "env-paths";
|
|
248
|
+
import winston from "winston";
|
|
249
|
+
var defaultLogFile = `${envPaths("confidential-proxy").data}/confidential-proxy.log`;
|
|
250
|
+
var dir = dirname(defaultLogFile);
|
|
251
|
+
try {
|
|
252
|
+
if (!existsSync(dir))
|
|
253
|
+
mkdirSync(dir, { recursive: true });
|
|
254
|
+
} catch {}
|
|
255
|
+
var level = process.env.CONFIDENTIAL_PROXY_LOG_LEVEL ?? "info";
|
|
256
|
+
var fileTransport = new winston.transports.File({
|
|
257
|
+
filename: defaultLogFile,
|
|
258
|
+
level,
|
|
259
|
+
maxsize: 10 * 1024 * 1024,
|
|
260
|
+
maxFiles: 3,
|
|
261
|
+
format: winston.format.combine(winston.format.timestamp({ format: "YYYY-MM-DDTHH:mm:ss.SSSZ" }), winston.format.json())
|
|
262
|
+
});
|
|
263
|
+
var consoleTransport = new winston.transports.Console({
|
|
264
|
+
level,
|
|
265
|
+
format: winston.format.combine(winston.format.timestamp({ format: "YYYY-MM-DDTHH:mm:ss.SSSZ" }), winston.format.printf(({ timestamp, level: level2, message, ...rest }) => {
|
|
266
|
+
const meta = Object.keys(rest).length ? ` ${JSON.stringify(rest)}` : "";
|
|
267
|
+
return `[${timestamp}] [${level2}] ${message}${meta}`;
|
|
268
|
+
}))
|
|
269
|
+
});
|
|
270
|
+
var logger = winston.createLogger({
|
|
271
|
+
level,
|
|
272
|
+
transports: [fileTransport, consoleTransport]
|
|
273
|
+
});
|
|
2801
274
|
|
|
2802
275
|
// src/server/discovery.ts
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
const
|
|
2812
|
-
|
|
2813
|
-
endpoints2.chat_completions = `POST ${prefixedRoute(openaiPrefix, "/v1/chat/completions")}`;
|
|
2814
|
-
endpoints2.audio_transcriptions = `POST ${prefixedRoute(openaiPrefix, "/v1/audio/transcriptions")}`;
|
|
2815
|
-
endpoints2.audio_translations = `POST ${prefixedRoute(openaiPrefix, "/v1/audio/translations")}`;
|
|
2816
|
-
endpoints2.models = `GET ${prefixedRoute(openaiPrefix, "/v1/models")}`;
|
|
2817
|
-
}
|
|
2818
|
-
if (mountAnthropic) {
|
|
2819
|
-
endpoints2.messages = `POST ${prefixedRoute(anthropicPrefix, "/v1/messages")}`;
|
|
2820
|
-
endpoints2.messages_count_tokens = `POST ${prefixedRoute(anthropicPrefix, "/v1/messages/count_tokens")}`;
|
|
2821
|
-
endpoints2.anthropic_models = `GET ${prefixedRoute(anthropicPrefix, "/v1/models")}`;
|
|
2822
|
-
endpoints2.anthropic_model_get = `GET ${prefixedRoute(anthropicPrefix, "/v1/models/{model_id}")}`;
|
|
2823
|
-
}
|
|
2824
|
-
const labels = [];
|
|
2825
|
-
if (mountOpenAI) {
|
|
2826
|
-
labels.push("OpenAI-compatible");
|
|
2827
|
-
}
|
|
2828
|
-
if (mountAnthropic) {
|
|
2829
|
-
labels.push("Anthropic Messages-compatible");
|
|
2830
|
-
}
|
|
2831
|
-
res.json({
|
|
2832
|
-
message: `Rvenc API Server (${labels.join(" + ")})`,
|
|
2833
|
-
version: "1.0.0",
|
|
2834
|
-
compat: resolveCompatLabel(mount),
|
|
2835
|
-
route_prefixes: buildRoutePrefixesPayload(mountOpenAI, mountAnthropic, openaiPrefix, anthropicPrefix),
|
|
2836
|
-
endpoints: endpoints2
|
|
276
|
+
import { readFileSync } from "node:fs";
|
|
277
|
+
var pkg = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
|
|
278
|
+
var SERVER_MESSAGE = "Rvenc API Server";
|
|
279
|
+
var SERVER_VERSION = pkg.version;
|
|
280
|
+
|
|
281
|
+
// src/utils/poll-ready.ts
|
|
282
|
+
async function isProxyRoot(baseUrl) {
|
|
283
|
+
try {
|
|
284
|
+
const res = await fetch(`${baseUrl}/`, {
|
|
285
|
+
signal: AbortSignal.timeout(2000)
|
|
2837
286
|
});
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
}
|
|
2845
|
-
if (mountAnthropic) {
|
|
2846
|
-
out.anthropic = anthropicPrefix || "/";
|
|
2847
|
-
}
|
|
2848
|
-
if (Object.keys(out).length === 0) {
|
|
2849
|
-
return;
|
|
287
|
+
if (!res.ok)
|
|
288
|
+
return false;
|
|
289
|
+
const body = await res.json();
|
|
290
|
+
return typeof body === "object" && body !== null && typeof body.message === "string" && body.message.startsWith(SERVER_MESSAGE);
|
|
291
|
+
} catch {
|
|
292
|
+
return false;
|
|
2850
293
|
}
|
|
2851
|
-
return out;
|
|
2852
294
|
}
|
|
2853
|
-
function
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
|
|
2858
|
-
|
|
295
|
+
async function pollForReadiness(baseUrl, timeoutMs = 30000) {
|
|
296
|
+
const deadline = Date.now() + timeoutMs;
|
|
297
|
+
let backoff = 200;
|
|
298
|
+
while (Date.now() < deadline) {
|
|
299
|
+
if (await isProxyRoot(baseUrl))
|
|
300
|
+
return;
|
|
301
|
+
await new Promise((r) => setTimeout(r, backoff));
|
|
302
|
+
backoff = Math.min(backoff * 1.5, 2000);
|
|
303
|
+
}
|
|
304
|
+
throw new Error(`Proxy did not become reachable within ${timeoutMs}ms`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// src/utils/state-file.ts
|
|
308
|
+
import {
|
|
309
|
+
existsSync as existsSync2,
|
|
310
|
+
mkdirSync as mkdirSync2,
|
|
311
|
+
readFileSync as readFileSync2,
|
|
312
|
+
unlinkSync,
|
|
313
|
+
writeFileSync
|
|
314
|
+
} from "node:fs";
|
|
315
|
+
import { bytesToHex as bytesToHex7, randomBytes as randomBytes5 } from "@noble/ciphers/utils.js";
|
|
316
|
+
import envPaths2 from "env-paths";
|
|
317
|
+
var appData = envPaths2("confidential-proxy");
|
|
318
|
+
function defaultStateFile() {
|
|
319
|
+
return `${appData.data}/proxy.state.json`;
|
|
320
|
+
}
|
|
321
|
+
function readStateFile(path) {
|
|
322
|
+
try {
|
|
323
|
+
const raw = readFileSync2(path, "utf-8");
|
|
324
|
+
const parsed = JSON.parse(raw);
|
|
325
|
+
if (typeof parsed.pid !== "number" || typeof parsed.host !== "string" || typeof parsed.port !== "number" || typeof parsed.token !== "string") {
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
return parsed;
|
|
329
|
+
} catch {
|
|
330
|
+
return null;
|
|
2859
331
|
}
|
|
2860
|
-
return "openai";
|
|
2861
332
|
}
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
}
|
|
2867
|
-
function resolveJsonBodyLimit(override) {
|
|
2868
|
-
if (override != null && String(override).trim() !== "") {
|
|
2869
|
-
return String(override).trim();
|
|
2870
|
-
}
|
|
2871
|
-
const env = process.env.JSON_BODY_LIMIT;
|
|
2872
|
-
if (env != null && env !== "") {
|
|
2873
|
-
return env;
|
|
2874
|
-
}
|
|
2875
|
-
return "32mb";
|
|
333
|
+
function removeStateFile(path) {
|
|
334
|
+
try {
|
|
335
|
+
if (existsSync2(path))
|
|
336
|
+
unlinkSync(path);
|
|
337
|
+
} catch {}
|
|
2876
338
|
}
|
|
2877
|
-
function
|
|
2878
|
-
|
|
2879
|
-
const compat2 = compatOrOptions;
|
|
2880
|
-
const { openaiPrefix: openaiPrefix2, anthropicPrefix: anthropicPrefix2 } = resolvePrefixesForCompat(compat2, undefined, undefined);
|
|
2881
|
-
return {
|
|
2882
|
-
compat: compat2,
|
|
2883
|
-
openaiPrefix: openaiPrefix2,
|
|
2884
|
-
anthropicPrefix: anthropicPrefix2,
|
|
2885
|
-
jsonBodyLimit: resolveJsonBodyLimit()
|
|
2886
|
-
};
|
|
2887
|
-
}
|
|
2888
|
-
const compat = compatOrOptions.compat ?? "openai";
|
|
2889
|
-
const { openaiPrefix, anthropicPrefix } = resolvePrefixesForCompat(compat, compatOrOptions.openaiRoutePrefix, compatOrOptions.anthropicRoutePrefix);
|
|
2890
|
-
return {
|
|
2891
|
-
compat,
|
|
2892
|
-
openaiPrefix,
|
|
2893
|
-
anthropicPrefix,
|
|
2894
|
-
jsonBodyLimit: resolveJsonBodyLimit(compatOrOptions.jsonBodyLimit)
|
|
2895
|
-
};
|
|
339
|
+
function generateShutdownToken() {
|
|
340
|
+
return bytesToHex7(randomBytes5(32));
|
|
2896
341
|
}
|
|
2897
|
-
function
|
|
2898
|
-
|
|
2899
|
-
|
|
2900
|
-
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
}
|
|
342
|
+
function isProcessAlive(pid) {
|
|
343
|
+
try {
|
|
344
|
+
process.kill(pid, 0);
|
|
345
|
+
return true;
|
|
346
|
+
} catch {
|
|
347
|
+
return false;
|
|
2904
348
|
}
|
|
2905
|
-
return 500;
|
|
2906
349
|
}
|
|
2907
|
-
|
|
2908
|
-
|
|
350
|
+
|
|
351
|
+
// src/launcher/proxy-subprocess.ts
|
|
352
|
+
function resolveCliScript() {
|
|
353
|
+
return new URL(import.meta.resolve("../cli")).pathname;
|
|
2909
354
|
}
|
|
2910
|
-
function
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
});
|
|
2922
|
-
if (mountOpenAI) {
|
|
2923
|
-
const router = express.Router();
|
|
2924
|
-
registerOpenAICompatRoutes(router, rvencDeps);
|
|
2925
|
-
mountRouter(app, openaiPrefix, router);
|
|
355
|
+
async function postShutdown(host, port, token, timeoutMs = 5000) {
|
|
356
|
+
try {
|
|
357
|
+
const res = await fetch(`http://${host}:${port}/__shutdown`, {
|
|
358
|
+
method: "POST",
|
|
359
|
+
headers: { "x-shutdown-token": token },
|
|
360
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
361
|
+
});
|
|
362
|
+
return res.status === 202;
|
|
363
|
+
} catch (err) {
|
|
364
|
+
logger.debug("postShutdown failed", { error: String(err) });
|
|
365
|
+
return false;
|
|
2926
366
|
}
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
mountRouter(app, anthropicPrefix, router);
|
|
367
|
+
}
|
|
368
|
+
async function preCheckBaseUrl(baseUrl) {
|
|
369
|
+
if (await isProxyRoot(baseUrl)) {
|
|
370
|
+
logger.debug("HTTP check (pre-spawn): proxy identified");
|
|
371
|
+
return "reusable";
|
|
2933
372
|
}
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
}
|
|
2938
|
-
if (!mountOpenAI) {
|
|
2939
|
-
return true;
|
|
2940
|
-
}
|
|
2941
|
-
return req.path === anthropicPrefix || req.path.startsWith(`${anthropicPrefix}/`);
|
|
2942
|
-
};
|
|
2943
|
-
app.use((err, req, res, _next) => {
|
|
2944
|
-
const status = httpErrorStatus(err);
|
|
2945
|
-
const message = err instanceof Error ? err.message : "Internal server error";
|
|
2946
|
-
if (isAnthropicRequest(req)) {
|
|
2947
|
-
const requestId = newAnthropicRequestId();
|
|
2948
|
-
res.setHeader("request-id", requestId);
|
|
2949
|
-
res.status(status).json({
|
|
2950
|
-
type: "error",
|
|
2951
|
-
error: {
|
|
2952
|
-
type: httpStatusToAnthropicErrorType(status),
|
|
2953
|
-
message
|
|
2954
|
-
},
|
|
2955
|
-
request_id: requestId
|
|
2956
|
-
});
|
|
2957
|
-
return;
|
|
2958
|
-
}
|
|
2959
|
-
res.status(status).json({
|
|
2960
|
-
error: {
|
|
2961
|
-
message,
|
|
2962
|
-
type: "server_error"
|
|
2963
|
-
}
|
|
373
|
+
try {
|
|
374
|
+
const res = await fetch(`${baseUrl}/`, {
|
|
375
|
+
signal: AbortSignal.timeout(2000)
|
|
2964
376
|
});
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
}
|
|
377
|
+
logger.debug("HTTP check (pre-spawn)", { status: res.status, ok: res.ok });
|
|
378
|
+
return res.ok ? "occupied" : "empty";
|
|
379
|
+
} catch (err) {
|
|
380
|
+
logger.debug("HTTP pre-check failed (nothing serving yet)", {
|
|
381
|
+
error: String(err)
|
|
382
|
+
});
|
|
383
|
+
return "empty";
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
async function ensureProxyRunning(config) {
|
|
387
|
+
const baseUrl = `http://${config.host}:${config.port}`;
|
|
388
|
+
const stateFilePath = defaultStateFile();
|
|
389
|
+
logger.debug("ensureProxyRunning", { baseUrl, stateFilePath, debugLogFile: defaultLogFile });
|
|
390
|
+
const preCheck = await preCheckBaseUrl(baseUrl);
|
|
391
|
+
if (preCheck === "reusable") {
|
|
392
|
+
const state = readStateFile(stateFilePath);
|
|
393
|
+
if (!state) {
|
|
394
|
+
throw new Error(`A proxy is responding at ${baseUrl} but no state file is present at ${stateFilePath}. ` + `Cannot identify or control it safely. Stop it manually and retry.`);
|
|
395
|
+
}
|
|
396
|
+
const pid = state.pid;
|
|
397
|
+
logger.debug("reusing existing server", { pid });
|
|
398
|
+
const whenCrashed2 = new Promise((_, reject) => {
|
|
399
|
+
const interval = setInterval(() => {
|
|
400
|
+
if (!isProcessAlive(pid)) {
|
|
401
|
+
clearInterval(interval);
|
|
402
|
+
reject(new Error(`Proxy process exited unexpectedly`));
|
|
403
|
+
}
|
|
404
|
+
}, 1000);
|
|
2983
405
|
});
|
|
406
|
+
return {
|
|
407
|
+
stop: async () => {
|
|
408
|
+
await postShutdown(state.host, state.port, state.token);
|
|
409
|
+
},
|
|
410
|
+
ready: Promise.resolve(),
|
|
411
|
+
whenCrashed: whenCrashed2,
|
|
412
|
+
pid
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
if (preCheck === "occupied") {
|
|
416
|
+
throw new Error(`Port ${config.port} at ${baseUrl} is occupied by an unknown server (no proxy response). ` + `Stop it ('confidential-proxy stop') or change --port.`);
|
|
417
|
+
}
|
|
418
|
+
const token = generateShutdownToken();
|
|
419
|
+
const execPath = process.execPath;
|
|
420
|
+
const scriptPath = resolveCliScript();
|
|
421
|
+
const spawnArgs = [
|
|
422
|
+
execPath,
|
|
423
|
+
scriptPath,
|
|
424
|
+
"--host",
|
|
425
|
+
config.host,
|
|
426
|
+
"--port",
|
|
427
|
+
String(config.port),
|
|
428
|
+
"--proxy-url",
|
|
429
|
+
config.proxyUrl,
|
|
430
|
+
"--enclave-url",
|
|
431
|
+
config.enclaveUrl,
|
|
432
|
+
"--kek",
|
|
433
|
+
config.kek,
|
|
434
|
+
"--compat",
|
|
435
|
+
"anthropic",
|
|
436
|
+
"--state-file",
|
|
437
|
+
stateFilePath,
|
|
438
|
+
...config.attest === false ? ["--no-attest"] : []
|
|
439
|
+
];
|
|
440
|
+
logger.debug("spawning proxy", { spawnArgs });
|
|
441
|
+
const child = Bun.spawn(spawnArgs, {
|
|
442
|
+
stdin: "ignore",
|
|
443
|
+
stdout: defaultLogFile ? Bun.file(defaultLogFile) : "ignore",
|
|
444
|
+
stderr: defaultLogFile ? Bun.file(defaultLogFile) : "ignore",
|
|
445
|
+
env: {
|
|
446
|
+
...process.env,
|
|
447
|
+
CONFIDENTIAL_PROXY_DAEMON_CHILD: "1",
|
|
448
|
+
CONFIDENTIAL_PROXY_SHUTDOWN_TOKEN: token
|
|
449
|
+
}
|
|
2984
450
|
});
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
host,
|
|
2991
|
-
port,
|
|
2992
|
-
compat: compatOpt,
|
|
2993
|
-
openaiRoutePrefix,
|
|
2994
|
-
anthropicRoutePrefix,
|
|
2995
|
-
jsonBodyLimit
|
|
2996
|
-
} = options;
|
|
2997
|
-
const serverHost = host || DEFAULT_HOST;
|
|
2998
|
-
const serverPort = port || DEFAULT_PORT;
|
|
2999
|
-
const compat = compatOpt ?? "openai";
|
|
3000
|
-
applyServerOptions(options);
|
|
3001
|
-
resolvePrefixesForCompat(compat, openaiRoutePrefix, anthropicRoutePrefix);
|
|
3002
|
-
const app = createServerApp({
|
|
3003
|
-
compat,
|
|
3004
|
-
openaiRoutePrefix,
|
|
3005
|
-
anthropicRoutePrefix,
|
|
3006
|
-
jsonBodyLimit
|
|
451
|
+
logger.debug("proxy spawned", { pid: child.pid });
|
|
452
|
+
logger.debug("polling for readiness");
|
|
453
|
+
const startupCrash = child.exited.then((exitCode) => {
|
|
454
|
+
logger.debug("proxy exited during startup", { exitCode });
|
|
455
|
+
throw new Error(`Proxy process exited during startup with code ${exitCode}. Run with CONFIDENTIAL_PROXY_LOG_LEVEL=debug to capture logs.`);
|
|
3007
456
|
});
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
}
|
|
457
|
+
try {
|
|
458
|
+
await Promise.race([pollForReadiness(baseUrl), startupCrash]);
|
|
459
|
+
} finally {
|
|
460
|
+
child.unref();
|
|
461
|
+
startupCrash.catch(() => {});
|
|
462
|
+
}
|
|
463
|
+
logger.debug("proxy is ready");
|
|
464
|
+
const whenCrashed = new Promise((_, reject) => {
|
|
465
|
+
child.exited.then((exitCode) => {
|
|
466
|
+
logger.debug("proxy exited", { exitCode });
|
|
467
|
+
reject(new Error(`Proxy process exited unexpectedly with code ${exitCode}`));
|
|
3018
468
|
});
|
|
3019
469
|
});
|
|
3020
|
-
}
|
|
3021
|
-
// src/server.ts
|
|
3022
|
-
var server_default = createServerApp("both");
|
|
3023
|
-
|
|
3024
|
-
// src/launcher/proxy-subprocess.ts
|
|
3025
|
-
function startProxySubprocess(config) {
|
|
3026
|
-
let close;
|
|
3027
|
-
const ready = startServer({ ...config, compat: "anthropic" }).then((handle) => {
|
|
3028
|
-
close = handle.close;
|
|
3029
|
-
});
|
|
3030
|
-
const whenCrashed = new Promise(() => {});
|
|
3031
470
|
return {
|
|
3032
|
-
stop: () =>
|
|
3033
|
-
|
|
3034
|
-
|
|
471
|
+
stop: async () => {
|
|
472
|
+
const ok = await postShutdown(config.host, config.port, token);
|
|
473
|
+
if (!ok) {
|
|
474
|
+
logger.debug("shutdown HTTP call failed; leaving cleanup to OS", {
|
|
475
|
+
pid: child.pid
|
|
476
|
+
});
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
removeStateFile(stateFilePath);
|
|
480
|
+
},
|
|
481
|
+
ready: Promise.resolve(),
|
|
482
|
+
whenCrashed,
|
|
483
|
+
pid: child.pid
|
|
3035
484
|
};
|
|
3036
485
|
}
|
|
3037
486
|
|
|
@@ -3095,7 +544,7 @@ function TextInput({ label, secret, onSubmit, onCancel }) {
|
|
|
3095
544
|
showRequired && /* @__PURE__ */ jsxDEV2(Text2, {
|
|
3096
545
|
color: "red",
|
|
3097
546
|
children: [
|
|
3098
|
-
"
|
|
547
|
+
" (",
|
|
3099
548
|
label,
|
|
3100
549
|
" is required)"
|
|
3101
550
|
]
|
|
@@ -3116,15 +565,13 @@ function promptValue(label, options = {}) {
|
|
|
3116
565
|
}
|
|
3117
566
|
|
|
3118
567
|
// src/launcher/claude-code.ts
|
|
3119
|
-
var
|
|
3120
|
-
if (!
|
|
3121
|
-
|
|
3122
|
-
var envPath = path.join(
|
|
3123
|
-
if (!
|
|
3124
|
-
|
|
568
|
+
var appData2 = envPaths3("confidential-claude");
|
|
569
|
+
if (!existsSync3(appData2.config))
|
|
570
|
+
mkdirSync3(appData2.config, { recursive: true });
|
|
571
|
+
var envPath = path.join(appData2.config, ".env");
|
|
572
|
+
if (!existsSync3(envPath))
|
|
573
|
+
writeFileSync2(envPath, "");
|
|
3125
574
|
var dotenvConfig = config({ path: envPath });
|
|
3126
|
-
var DEFAULT_HOST2 = "127.0.0.1";
|
|
3127
|
-
var DEFAULT_PORT2 = 8787;
|
|
3128
575
|
var CLAUDE_FLAGS = {
|
|
3129
576
|
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1",
|
|
3130
577
|
CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK: "1",
|
|
@@ -3138,22 +585,20 @@ async function requireValue(current, label, options = {}) {
|
|
|
3138
585
|
const val = await promptValue(label, options);
|
|
3139
586
|
const updated = { ...dotenvConfig.parsed ?? {}, [label]: val };
|
|
3140
587
|
dotenvConfig.parsed = updated;
|
|
3141
|
-
|
|
588
|
+
writeFileSync2(envPath, Object.entries(updated).map(([k, v]) => `${k}=${v}`).join(`
|
|
3142
589
|
`));
|
|
3143
590
|
return val;
|
|
3144
591
|
}
|
|
3145
592
|
async function loadConfig() {
|
|
3146
|
-
const host = process.env?.HOST ??
|
|
3147
|
-
const
|
|
3148
|
-
const port = portRaw ? Number.parseInt(portRaw, 10) : DEFAULT_PORT2;
|
|
3149
|
-
if (!Number.isFinite(port) || port <= 0) {
|
|
3150
|
-
throw new Error(`Invalid PORT: ${portRaw}`);
|
|
3151
|
-
}
|
|
593
|
+
const host = process.env?.HOST ?? DEFAULT_HOST;
|
|
594
|
+
const port = DEFAULT_PORT;
|
|
3152
595
|
const enclaveUrl = await requireValue(process.env?.ENCLAVE_URL, "ENCLAVE_URL");
|
|
3153
596
|
const proxyUrl = await requireValue(process.env?.PROXY_URL, "PROXY_URL");
|
|
3154
597
|
const kek = process.env?.CLIENT_KEK ?? generateNewClientKEK();
|
|
3155
|
-
const apiKey = await requireValue(process.env?.API_KEY, "API_KEY", {
|
|
3156
|
-
|
|
598
|
+
const apiKey = await requireValue(process.env?.API_KEY, "API_KEY", {
|
|
599
|
+
secret: true
|
|
600
|
+
});
|
|
601
|
+
return { host, port, enclaveUrl, proxyUrl, kek, apiKey, attest: true };
|
|
3157
602
|
}
|
|
3158
603
|
async function fetchModels(baseUrl, apiKey, type = "CHAT") {
|
|
3159
604
|
const url = new URL(`/v1/models?type=${encodeURIComponent(type)}`, baseUrl);
|
|
@@ -3175,14 +620,18 @@ async function pickModel(models) {
|
|
|
3175
620
|
throw new Error("No models available from upstream.");
|
|
3176
621
|
}
|
|
3177
622
|
const selected = await interactivePickModel(models.map((m) => ({ id: m.id, display_name: m.display_name })));
|
|
3178
|
-
|
|
623
|
+
const found = models.find((m) => m.id === selected.id);
|
|
624
|
+
if (!found) {
|
|
625
|
+
throw new Error(`Model ${selected.id} not found in available models.`);
|
|
626
|
+
}
|
|
627
|
+
return found;
|
|
3179
628
|
}
|
|
3180
629
|
function detectCommand(command, args = ["--version"]) {
|
|
3181
|
-
const result = spawnSync(command, args, {
|
|
3182
|
-
|
|
3183
|
-
|
|
630
|
+
const result = Bun.spawnSync([command, ...args], {
|
|
631
|
+
stdout: "ignore",
|
|
632
|
+
stderr: "ignore"
|
|
3184
633
|
});
|
|
3185
|
-
return result.
|
|
634
|
+
return result.exitCode === 0;
|
|
3186
635
|
}
|
|
3187
636
|
function detectClaude() {
|
|
3188
637
|
return detectCommand("claude");
|
|
@@ -3192,106 +641,72 @@ async function ensureClaudeInstalled() {
|
|
|
3192
641
|
return;
|
|
3193
642
|
throw new Error("install claude code: https://code.claude.com/docs/en/overview");
|
|
3194
643
|
}
|
|
3195
|
-
function buildClaudeEnv(baseUrl,
|
|
644
|
+
function buildClaudeEnv(baseUrl, model, apiKey) {
|
|
3196
645
|
const env = {
|
|
3197
646
|
...process.env,
|
|
3198
647
|
ANTHROPIC_BASE_URL: baseUrl.toString(),
|
|
3199
648
|
ANTHROPIC_AUTH_TOKEN: apiKey,
|
|
3200
|
-
ANTHROPIC_MODEL:
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
ANTHROPIC_DEFAULT_SONNET_MODEL: modelId,
|
|
3204
|
-
ANTHROPIC_DEFAULT_HAIKU_MODEL: modelId,
|
|
3205
|
-
DEFAULT_MODEL: modelId,
|
|
649
|
+
ANTHROPIC_MODEL: model.id,
|
|
650
|
+
ANTHROPIC_CUSTOM_MODEL_OPTION: model.id,
|
|
651
|
+
ANTHROPIC_CUSTOM_MODEL_OPTION_NAME: model.display_name || model.id,
|
|
3206
652
|
...CLAUDE_FLAGS
|
|
3207
653
|
};
|
|
3208
654
|
delete env.ANTHROPIC_API_KEY;
|
|
3209
655
|
return env;
|
|
3210
656
|
}
|
|
3211
|
-
function spawnClaude(baseUrl,
|
|
3212
|
-
const env = buildClaudeEnv(baseUrl,
|
|
657
|
+
function spawnClaude(baseUrl, model, apiKey, forwardedArgs) {
|
|
658
|
+
const env = buildClaudeEnv(baseUrl, model, apiKey);
|
|
3213
659
|
return new Promise((resolve, reject) => {
|
|
3214
|
-
const child = spawn("claude", forwardedArgs, {
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
child.on("error", reject);
|
|
3220
|
-
child.on("exit", (code, signal) => {
|
|
3221
|
-
if (signal) {
|
|
3222
|
-
resolve(128);
|
|
3223
|
-
return;
|
|
3224
|
-
}
|
|
3225
|
-
resolve(code ?? 0);
|
|
660
|
+
const child = Bun.spawn(["claude", ...forwardedArgs], {
|
|
661
|
+
stdin: "inherit",
|
|
662
|
+
stdout: "inherit",
|
|
663
|
+
stderr: "inherit",
|
|
664
|
+
env
|
|
3226
665
|
});
|
|
666
|
+
child.exited.then((code) => {
|
|
667
|
+
resolve(code);
|
|
668
|
+
}).catch(reject);
|
|
3227
669
|
});
|
|
3228
670
|
}
|
|
3229
|
-
function installProxyLifecycleHandlers(proxy) {
|
|
3230
|
-
const onSignal = (sig) => {
|
|
3231
|
-
proxy.stop();
|
|
3232
|
-
process.exit(sig === "SIGINT" ? 130 : 143);
|
|
3233
|
-
};
|
|
3234
|
-
const sigintHandler = () => onSignal("SIGINT");
|
|
3235
|
-
const sigtermHandler = () => onSignal("SIGTERM");
|
|
3236
|
-
const exitHandler = () => proxy.stop();
|
|
3237
|
-
process.once("SIGINT", sigintHandler);
|
|
3238
|
-
process.once("SIGTERM", sigtermHandler);
|
|
3239
|
-
process.once("exit", exitHandler);
|
|
3240
|
-
return {
|
|
3241
|
-
dispose: () => {
|
|
3242
|
-
process.off("SIGINT", sigintHandler);
|
|
3243
|
-
process.off("SIGTERM", sigtermHandler);
|
|
3244
|
-
process.off("exit", exitHandler);
|
|
3245
|
-
},
|
|
3246
|
-
whenCrashed: proxy.whenCrashed
|
|
3247
|
-
};
|
|
3248
|
-
}
|
|
3249
671
|
async function runClaudeCode(forwardedArgs = []) {
|
|
3250
672
|
if (!process.stdout.isTTY || !process.stdin.isTTY) {
|
|
3251
673
|
throw new Error("TTY environment required");
|
|
3252
674
|
}
|
|
3253
675
|
const config2 = await loadConfig();
|
|
3254
676
|
const baseUrl = new URL(`http://${config2.host}:${config2.port}`);
|
|
3255
|
-
const proxy =
|
|
677
|
+
const proxy = await ensureProxyRunning({
|
|
3256
678
|
host: config2.host,
|
|
3257
679
|
port: config2.port,
|
|
3258
680
|
proxyUrl: config2.proxyUrl,
|
|
3259
681
|
enclaveUrl: config2.enclaveUrl,
|
|
3260
682
|
kek: config2.kek
|
|
3261
683
|
});
|
|
3262
|
-
|
|
684
|
+
let models;
|
|
3263
685
|
try {
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
proxy.stop();
|
|
3268
|
-
lifecycle.dispose();
|
|
3269
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
3270
|
-
throw new Error(`Failed to start proxy: ${msg}`);
|
|
3271
|
-
}
|
|
3272
|
-
let models;
|
|
3273
|
-
try {
|
|
3274
|
-
models = await Promise.race([
|
|
3275
|
-
fetchModels(baseUrl, config2.apiKey),
|
|
3276
|
-
lifecycle.whenCrashed
|
|
3277
|
-
]);
|
|
3278
|
-
} catch (err) {
|
|
3279
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
3280
|
-
throw new Error(`Failed to fetch models from proxy: ${msg}`);
|
|
3281
|
-
}
|
|
3282
|
-
const selected = await pickModel(models);
|
|
3283
|
-
await ensureClaudeInstalled();
|
|
3284
|
-
process.stdin.pause();
|
|
3285
|
-
process.stdin.removeAllListeners();
|
|
3286
|
-
const exitCode = await Promise.race([
|
|
3287
|
-
spawnClaude(baseUrl, selected.id, config2.apiKey, forwardedArgs),
|
|
3288
|
-
lifecycle.whenCrashed
|
|
686
|
+
models = await Promise.race([
|
|
687
|
+
fetchModels(baseUrl, config2.apiKey),
|
|
688
|
+
proxy.whenCrashed
|
|
3289
689
|
]);
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
proxy.stop();
|
|
690
|
+
} catch (err) {
|
|
691
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
692
|
+
throw new Error(`Failed to fetch models from proxy: ${msg}`);
|
|
3294
693
|
}
|
|
694
|
+
const selected = await pickModel(models);
|
|
695
|
+
const updated = {
|
|
696
|
+
...dotenvConfig.parsed ?? {},
|
|
697
|
+
ANTHROPIC_MODEL: selected.id
|
|
698
|
+
};
|
|
699
|
+
dotenvConfig.parsed = updated;
|
|
700
|
+
writeFileSync2(envPath, Object.entries(updated).map(([k, v]) => `${k}=${v}`).join(`
|
|
701
|
+
`));
|
|
702
|
+
await ensureClaudeInstalled();
|
|
703
|
+
process.stdin.pause();
|
|
704
|
+
process.stdin.removeAllListeners();
|
|
705
|
+
const exitCode = await Promise.race([
|
|
706
|
+
spawnClaude(baseUrl, selected, config2.apiKey, forwardedArgs),
|
|
707
|
+
proxy.whenCrashed
|
|
708
|
+
]);
|
|
709
|
+
return exitCode;
|
|
3295
710
|
}
|
|
3296
711
|
|
|
3297
712
|
// src/cli-claude.ts
|