@premai/api-sdk 1.0.45 → 1.0.47

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/dist/cli.mjs CHANGED
@@ -3,2419 +3,2424 @@ import { createRequire } from "node:module";
3
3
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
4
 
5
5
  // src/cli.ts
6
- import { parseArgs } from "node:util";
6
+ import { Command } from "commander";
7
7
 
8
- // src/server/create-app.ts
9
- import express from "express";
8
+ // src/audio/index.ts
9
+ import { bytesToHex as bytesToHex2, hexToBytes as hexToBytes2 } from "@noble/ciphers/utils.js";
10
10
 
11
- // src/anthropic/http.ts
12
- import { bytesToHex, randomBytes } from "@noble/ciphers/utils.js";
13
- var ANTHROPIC_VERSION_DEFAULT = "2023-06-01";
14
- var ANTHROPIC_VERSION_DATE = /^\d{4}-\d{2}-\d{2}$/;
15
- function isAnthropicApiVersionSupported(version) {
16
- if (version === ANTHROPIC_VERSION_DEFAULT) {
17
- return true;
11
+ // src/config.ts
12
+ var endpoints = {
13
+ enclave: process.env.ENCLAVE_URL,
14
+ proxy: process.env.PROXY_URL
15
+ };
16
+ var DEFAULT_REQUEST_TIMEOUT_MS = 600000;
17
+ var DEFAULT_MAX_BUFFER_SIZE = 10 * 1024 * 1024;
18
+
19
+ // src/utils/attestation.ts
20
+ var cachedPrem;
21
+ async function loadPrem() {
22
+ if (cachedPrem)
23
+ return cachedPrem;
24
+ const isBare = typeof globalThis.Bare !== "undefined";
25
+ if (isBare) {
26
+ cachedPrem = await (async (s, y) => await import(s, y))("@premai/reticle", { with: { type: "script" } });
27
+ return cachedPrem;
18
28
  }
19
- return ANTHROPIC_VERSION_DATE.test(version);
29
+ cachedPrem = await import("@premai/reticle");
30
+ return cachedPrem;
20
31
  }
21
- function newAnthropicRequestId() {
22
- return `req_${bytesToHex(randomBytes(12))}`;
32
+ function isAttestationError(err) {
33
+ return err instanceof Error && err.name === "AttestationError";
23
34
  }
24
- function newAnthropicMessageId() {
25
- return `msg_${bytesToHex(randomBytes(12))}`;
35
+ var ATTEST_TTL_MS = 30000;
36
+ var ATTEST_CACHE_MAX = 500;
37
+ var ATTEST_MAX_ATTEMPTS = 4;
38
+ var ATTEST_RETRY_BASE_MS = 250;
39
+ var ATTEST_RETRY_MAX_MS = 2000;
40
+ var TRANSIENT_PATTERNS = [
41
+ /EOF while parsing/i,
42
+ /error decoding response body/i,
43
+ /connection (reset|closed|refused)/i,
44
+ /socket hang up/i,
45
+ /ETIMEDOUT/i
46
+ ];
47
+ var attestCache = new Map;
48
+ var attestInflight = new Map;
49
+ function attestCacheKey(apiKey, model) {
50
+ return `${apiKey}|${model ?? ""}`;
26
51
  }
27
- function extractAnthropicApiKey(req) {
28
- const raw = req.headers["x-api-key"];
29
- if (typeof raw === "string" && raw.length > 0) {
30
- return raw;
31
- }
32
- if (Array.isArray(raw) && raw[0]) {
33
- return raw[0];
34
- }
35
- const authHeader = req.headers.authorization;
36
- if (!authHeader) {
37
- return null;
38
- }
39
- if (authHeader.startsWith("Bearer ")) {
40
- return authHeader.slice(7);
52
+ function pruneExpired(now) {
53
+ for (const [key, entry] of attestCache) {
54
+ if (entry.expires <= now) {
55
+ attestCache.delete(key);
56
+ } else {
57
+ break;
58
+ }
41
59
  }
42
- return authHeader;
43
60
  }
44
- function getAnthropicVersionHeader(req) {
45
- const raw = req.headers["anthropic-version"];
46
- if (typeof raw === "string" && raw.length > 0) {
47
- return raw;
61
+ function isTransientError(err) {
62
+ const messages = [];
63
+ if (err instanceof Error) {
64
+ messages.push(err.message);
48
65
  }
49
- if (Array.isArray(raw) && raw[0]) {
50
- return raw[0];
66
+ if (isAttestationError(err) && Array.isArray(err.cause)) {
67
+ messages.push(...err.cause);
51
68
  }
52
- return null;
69
+ return messages.some((m) => TRANSIENT_PATTERNS.some((re) => re.test(m)));
53
70
  }
54
- function resolveAnthropicVersion(req) {
55
- const header = getAnthropicVersionHeader(req);
56
- const version = header ?? ANTHROPIC_VERSION_DEFAULT;
57
- if (!isAnthropicApiVersionSupported(version)) {
58
- return {
59
- ok: false,
60
- message: `Unsupported anthropic-version: ${version}. Expected a dated version (YYYY-MM-DD) or ${ANTHROPIC_VERSION_DEFAULT}.`
61
- };
62
- }
63
- return { ok: true, version };
71
+ function backoffDelayMs(attempt) {
72
+ const exp = ATTEST_RETRY_BASE_MS * 2 ** (attempt - 1);
73
+ const capped = Math.min(exp, ATTEST_RETRY_MAX_MS);
74
+ const jitter = Math.floor(Math.random() * (capped / 2));
75
+ return capped + jitter;
64
76
  }
65
- function sendAnthropicHttpError(res, status, errorType, message, requestId) {
66
- res.setHeader("request-id", requestId);
67
- res.status(status).json({
68
- type: "error",
69
- error: { type: errorType, message },
70
- request_id: requestId
71
- });
77
+ function delay(ms) {
78
+ return new Promise((resolve) => setTimeout(resolve, ms));
72
79
  }
73
- function httpStatusToAnthropicErrorType(status) {
74
- if (status === 401) {
75
- return "authentication_error";
76
- }
77
- if (status === 402) {
78
- return "billing_error";
79
- }
80
- if (status === 403) {
81
- return "permission_error";
82
- }
83
- if (status === 404) {
84
- return "not_found_error";
85
- }
86
- if (status === 413) {
87
- return "request_too_large";
88
- }
89
- if (status === 429) {
90
- return "rate_limit_error";
91
- }
92
- if (status === 504) {
93
- return "timeout_error";
80
+ function safeFree(obj) {
81
+ if (typeof obj?.free !== "function")
82
+ return;
83
+ try {
84
+ obj.free();
85
+ } catch {}
86
+ }
87
+ async function attemptAttest(apiKey, options) {
88
+ const prem = await loadPrem();
89
+ let client;
90
+ let attested;
91
+ let headers;
92
+ let sessionId;
93
+ try {
94
+ client = await new prem.ClientBuilder(endpoints.proxy ?? "").with_authorization(apiKey).build();
95
+ if (options.model) {
96
+ client.set_query(new prem.QueryParams().with("model", options.model));
97
+ }
98
+ attested = await client.attest();
99
+ headers = attested.headers();
100
+ sessionId = headers.cpu()?.get("x-session-id") ?? headers.gpu()?.get("x-session-id") ?? null;
101
+ } finally {
102
+ safeFree(headers);
103
+ safeFree(attested);
104
+ safeFree(client);
94
105
  }
95
- if (status === 529) {
96
- return "overloaded_error";
106
+ if (sessionId === null) {
107
+ throw new Error("missing x-session-id issued by attestation");
97
108
  }
98
- if (status >= 400 && status < 500) {
99
- return "invalid_request_error";
109
+ return sessionId;
110
+ }
111
+ async function runAttest(apiKey, options) {
112
+ let lastErr;
113
+ for (let attempt = 1;attempt <= ATTEST_MAX_ATTEMPTS; attempt++) {
114
+ try {
115
+ return await attemptAttest(apiKey, options);
116
+ } catch (err) {
117
+ lastErr = err;
118
+ if (attempt === ATTEST_MAX_ATTEMPTS || !isTransientError(err)) {
119
+ throw err;
120
+ }
121
+ await delay(backoffDelayMs(attempt));
122
+ }
100
123
  }
101
- return "api_error";
124
+ throw lastErr;
102
125
  }
103
- function extractErrorMessage(err) {
104
- if (!err || typeof err !== "object") {
126
+ async function attest(apiKey, options = { enabled: true }) {
127
+ if (!options.enabled)
105
128
  return null;
129
+ const key = attestCacheKey(apiKey, options.model);
130
+ const now = Date.now();
131
+ const cached = attestCache.get(key);
132
+ if (cached) {
133
+ if (cached.expires > now)
134
+ return cached.sessionId;
135
+ attestCache.delete(key);
106
136
  }
107
- const o = err;
108
- if (typeof o.message === "string" && o.message.length > 0) {
109
- return o.message;
110
- }
111
- if (typeof o.error === "string" && o.error.length > 0) {
112
- return o.error;
137
+ const inflight = attestInflight.get(key);
138
+ if (inflight) {
139
+ return inflight;
113
140
  }
114
- if (o.error && typeof o.error === "object") {
115
- const nested = o.error.message;
116
- if (typeof nested === "string" && nested.length > 0) {
117
- return nested;
141
+ const work = runAttest(apiKey, options).then((sessionId) => {
142
+ const insertTime = Date.now();
143
+ pruneExpired(insertTime);
144
+ attestCache.set(key, { sessionId, expires: insertTime + ATTEST_TTL_MS });
145
+ if (attestCache.size > ATTEST_CACHE_MAX) {
146
+ const oldest = attestCache.keys().next().value;
147
+ if (oldest)
148
+ attestCache.delete(oldest);
118
149
  }
119
- }
120
- return null;
150
+ return sessionId;
151
+ }).finally(() => {
152
+ attestInflight.delete(key);
153
+ });
154
+ attestInflight.set(key, work);
155
+ return work;
121
156
  }
122
- function looksLikeApiErrorResponse(err) {
123
- if (!err || typeof err !== "object")
124
- return false;
125
- const o = err;
126
- if (typeof o.status !== "number")
127
- return false;
128
- return "error" in o || "message" in o;
157
+
158
+ // src/utils/crypto.ts
159
+ import { aeskwp } from "@noble/ciphers/aes.js";
160
+ import { xchacha20poly1305 } from "@noble/ciphers/chacha.js";
161
+ import { bytesToHex, hexToBytes, managedNonce, randomBytes } from "@noble/ciphers/utils.js";
162
+ import { sha256 } from "@noble/hashes/sha2.js";
163
+ import { sha3_256 } from "@noble/hashes/sha3.js";
164
+ import { XWing } from "@noble/post-quantum/hybrid.js";
165
+ function createMLKEMEncapsulation(publicKeyHex) {
166
+ return XWing.encapsulate(hexToBytes(publicKeyHex));
129
167
  }
130
- function mapUnknownErrorToAnthropicResponse(err, res, requestId) {
131
- if (looksLikeApiErrorResponse(err)) {
132
- const status = err.status >= 400 && err.status < 600 ? err.status : 500;
133
- const message2 = extractErrorMessage(err) ?? "Request failed";
134
- const errorType = httpStatusToAnthropicErrorType(status);
135
- sendAnthropicHttpError(res, status, errorType, message2, requestId);
136
- return;
168
+ function encryptPayload(sharedSecret, data) {
169
+ const nonce = randomBytes(24);
170
+ const chacha = xchacha20poly1305(sharedSecret, nonce);
171
+ let encodedData;
172
+ if (data instanceof Uint8Array) {
173
+ encodedData = data;
174
+ } else if (typeof data === "string") {
175
+ encodedData = new TextEncoder().encode(data);
176
+ } else {
177
+ encodedData = new TextEncoder().encode(JSON.stringify(data));
137
178
  }
138
- const message = extractErrorMessage(err) ?? (err instanceof Error ? err.message : "Internal server error");
139
- sendAnthropicHttpError(res, 500, "api_error", message, requestId);
179
+ const encrypted = chacha.encrypt(encodedData);
180
+ return { encrypted, nonce };
140
181
  }
141
- function writeAnthropicSseEvent(res, event, data) {
142
- res.write(`event: ${event}
143
- data: ${JSON.stringify(data)}
144
-
145
- `);
146
- }
147
-
148
- // src/anthropic/to-openai.ts
149
- class AnthropicRequestValidationError extends Error {
150
- status = 400;
151
- anthropicType = "invalid_request_error";
152
- constructor(message) {
153
- super(message);
154
- this.name = "AnthropicRequestValidationError";
182
+ function decryptPayload(encryptedData, sharedSecret, nonce) {
183
+ const chacha = xchacha20poly1305(sharedSecret, nonce);
184
+ const encrypted = hexToBytes(encryptedData);
185
+ const decrypted = chacha.decrypt(encrypted);
186
+ const str = new TextDecoder().decode(decrypted);
187
+ try {
188
+ return JSON.parse(str);
189
+ } catch {
190
+ return str;
155
191
  }
156
192
  }
157
- function systemToOpenAiMessages(system) {
158
- if (typeof system === "string") {
159
- if (system.length === 0) {
160
- return [];
193
+ async function getEnclavePublicKey(timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
194
+ const controller = new AbortController;
195
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
196
+ try {
197
+ const response = await fetch(`${endpoints.enclave}/publicKey`, {
198
+ signal: controller.signal
199
+ });
200
+ if (!response.ok) {
201
+ throw new Error(`Failed to fetch enclave public key: ${response.status} ${response.statusText}`);
161
202
  }
162
- return [{ role: "system", content: system }];
163
- }
164
- if (Array.isArray(system)) {
165
- const parts = [];
166
- for (const block of system) {
167
- if (block && block.type === "text" && typeof block.text === "string") {
168
- parts.push(block.text);
169
- } else if (block && typeof block === "object") {
170
- console.warn(`[proxy] system block type "${block.type}" is not supported and will be ignored.`);
171
- }
203
+ const data = await response.json();
204
+ if (!data.publicKey || typeof data.publicKey !== "string") {
205
+ throw new Error("Invalid public key response from enclave");
172
206
  }
173
- if (parts.length === 0) {
174
- return [];
207
+ return data.publicKey;
208
+ } catch (error) {
209
+ if (error instanceof Error && error.name === "AbortError") {
210
+ throw new Error(`Enclave public key request timed out after ${timeoutMs}ms`);
175
211
  }
176
- return [{ role: "system", content: parts.join(`
177
-
178
- `) }];
212
+ throw new Error(`Failed to get enclave public key: ${error instanceof Error ? error.message : error}`);
213
+ } finally {
214
+ clearTimeout(timeoutId);
179
215
  }
180
- if (system.type === "text" && typeof system.text === "string") {
181
- return [{ role: "system", content: system.text }];
216
+ }
217
+ async function generateEncryptionKeys(timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
218
+ const enclavePublicKey = await getEnclavePublicKey(timeoutMs);
219
+ return createMLKEMEncapsulation(enclavePublicKey);
220
+ }
221
+ function keyIdFromKEK(kek, context = "kek:v1", length = 16) {
222
+ const ctx = new TextEncoder().encode(context);
223
+ const input = new Uint8Array(kek.length + ctx.length);
224
+ input.set(kek, 0);
225
+ input.set(ctx, kek.length);
226
+ const digest = sha256(input);
227
+ return digest.slice(0, length);
228
+ }
229
+ function encryptWithDEK(dek, plaintext) {
230
+ const aead = managedNonce(xchacha20poly1305)(dek);
231
+ return aead.encrypt(plaintext);
232
+ }
233
+ function encryptMetadataWithDEK(dek, metadata) {
234
+ const encoded = new TextEncoder().encode(metadata);
235
+ const encrypted = encryptWithDEK(dek, encoded);
236
+ return bytesToHex(encrypted);
237
+ }
238
+ function wrapDEK(kek, dek) {
239
+ const kw = aeskwp(kek);
240
+ return kw.encrypt(dek);
241
+ }
242
+ function unwrapDEK(kek, wrappedDEK) {
243
+ const kw = aeskwp(kek);
244
+ return kw.decrypt(wrappedDEK);
245
+ }
246
+ function decryptWithDEK(dek, encryptedContent) {
247
+ const aead = managedNonce(xchacha20poly1305)(dek);
248
+ return aead.decrypt(encryptedContent);
249
+ }
250
+
251
+ // src/utils/error.ts
252
+ async function throwIfErrorResponse(response) {
253
+ let raw;
254
+ try {
255
+ raw = await response.json();
256
+ if (!raw.status)
257
+ raw = { ...raw, status: response.status };
258
+ } catch {
259
+ raw = {
260
+ status: response.status,
261
+ data: null,
262
+ error: response.statusText || `HTTP ${response.status}`,
263
+ message: null
264
+ };
182
265
  }
183
- throw new AnthropicRequestValidationError("Invalid system parameter shape.");
266
+ throw raw;
184
267
  }
185
- function toolResultContentToString(content) {
186
- if (typeof content === "string") {
187
- return content;
268
+
269
+ // src/utils/files.ts
270
+ var getFileName = (file) => {
271
+ if (file instanceof File) {
272
+ return file.name;
188
273
  }
189
- if (content === null || content === undefined) {
190
- return "";
274
+ if (file instanceof Blob) {
275
+ return;
191
276
  }
192
- if (Array.isArray(content)) {
193
- const parts = [];
194
- for (const block of content) {
195
- if (block && typeof block === "object" && "type" in block && block.type === "text" && typeof block.text === "string") {
196
- parts.push(block.text);
197
- } else {
198
- parts.push(JSON.stringify(block));
199
- }
200
- }
201
- return parts.join(`
202
- `);
277
+ const fileAny = file;
278
+ if (fileAny.path) {
279
+ const path = typeof fileAny.path === "string" ? fileAny.path : fileAny.path.toString();
280
+ return path.split("/").pop() || path.split("\\").pop() || path;
203
281
  }
204
- return JSON.stringify(content);
205
- }
206
- function anthropicImageBlockToOpenAIPart(part) {
207
- const source = part.source;
208
- if (!source || typeof source !== "object") {
209
- return null;
282
+ if (file instanceof Uint8Array || file instanceof ArrayBuffer) {
283
+ return;
210
284
  }
211
- const s = source;
212
- if (s.type === "base64" && typeof s.data === "string" && s.data.length > 0) {
213
- const mediaType = typeof s.media_type === "string" && s.media_type.length > 0 ? s.media_type : "image/png";
214
- return {
215
- type: "image_url",
216
- image_url: { url: `data:${mediaType};base64,${s.data}` }
217
- };
285
+ return;
286
+ };
287
+
288
+ // src/audio/index.ts
289
+ async function readUploadableToUint8Array(file) {
290
+ if (file instanceof Uint8Array) {
291
+ return file;
218
292
  }
219
- if (s.type === "url" && typeof s.url === "string" && s.url.length > 0) {
220
- return { type: "image_url", image_url: { url: s.url } };
293
+ if (file instanceof ArrayBuffer) {
294
+ return new Uint8Array(file);
221
295
  }
222
- return null;
223
- }
224
- function anthropicUserContentToOpenAIMessages(content) {
225
- if (typeof content === "string") {
226
- return [{ role: "user", content }];
296
+ if (typeof file.arrayBuffer === "function") {
297
+ const blob = file;
298
+ const buffer = await blob.arrayBuffer();
299
+ return new Uint8Array(buffer);
227
300
  }
228
- const out = [];
229
- const partsBuf = [];
230
- const flushParts = () => {
231
- if (partsBuf.length === 0) {
232
- return;
233
- }
234
- if (partsBuf.length === 1 && partsBuf[0].type === "text") {
235
- out.push({ role: "user", content: partsBuf[0].text });
236
- } else {
237
- out.push({ role: "user", content: [...partsBuf] });
238
- }
239
- partsBuf.length = 0;
240
- };
241
- for (const part of content) {
242
- if (!part || typeof part !== "object") {
243
- throw new AnthropicRequestValidationError("Invalid message content entry.");
244
- }
245
- if (part.type === "text" && typeof part.text === "string") {
246
- partsBuf.push({
247
- type: "text",
248
- text: part.text
301
+ const fileAny = file;
302
+ if (typeof fileAny.on === "function" && (typeof fileAny.read === "function" || typeof fileAny.pipe === "function")) {
303
+ const chunks = [];
304
+ return new Promise((resolve, reject) => {
305
+ fileAny.on("data", (chunk) => {
306
+ if (Buffer.isBuffer(chunk)) {
307
+ chunks.push(new Uint8Array(chunk));
308
+ } else if (chunk instanceof Uint8Array) {
309
+ chunks.push(chunk);
310
+ } else if (typeof chunk === "object" && chunk !== null) {
311
+ chunks.push(new Uint8Array(Buffer.from(chunk)));
312
+ }
249
313
  });
250
- continue;
251
- }
252
- if (part.type === "image") {
253
- const imgPart = anthropicImageBlockToOpenAIPart(part);
254
- if (imgPart) {
255
- partsBuf.push(imgPart);
256
- }
257
- continue;
258
- }
259
- if (part.type === "tool_result") {
260
- flushParts();
261
- const id = part.tool_use_id;
262
- const rawContent = part.content;
263
- if (typeof id !== "string" || id.length === 0) {
264
- throw new AnthropicRequestValidationError("tool_result blocks require a non-empty tool_use_id.");
265
- }
266
- out.push({
267
- role: "tool",
268
- tool_call_id: id,
269
- content: toolResultContentToString(rawContent)
314
+ fileAny.on("end", () => {
315
+ const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
316
+ const result = new Uint8Array(totalLength);
317
+ let offset = 0;
318
+ for (const chunk of chunks) {
319
+ result.set(chunk, offset);
320
+ offset += chunk.length;
321
+ }
322
+ resolve(result);
270
323
  });
271
- }
324
+ fileAny.on("error", (err) => reject(err));
325
+ });
272
326
  }
273
- flushParts();
274
- return out;
327
+ throw new Error("Unsupported file type for audio transcription");
275
328
  }
276
- function anthropicAssistantContentToOpenAI(content) {
277
- if (typeof content === "string") {
278
- return { role: "assistant", content };
279
- }
280
- const textParts = [];
281
- const toolCalls = [];
282
- for (const part of content) {
283
- if (!part || typeof part !== "object") {
284
- throw new AnthropicRequestValidationError("Invalid message content entry.");
285
- }
286
- if (part.type === "text" && typeof part.text === "string") {
287
- textParts.push(part.text);
288
- continue;
289
- }
290
- if (part.type === "tool_use") {
291
- const p = part;
292
- if (typeof p.id !== "string" || p.id.length === 0) {
293
- throw new AnthropicRequestValidationError("tool_use blocks require a non-empty id.");
294
- }
295
- if (typeof p.name !== "string" || p.name.length === 0) {
296
- throw new AnthropicRequestValidationError("tool_use blocks require a non-empty name.");
297
- }
298
- const args = typeof p.input === "string" ? p.input : JSON.stringify(p.input ?? {});
299
- toolCalls.push({
300
- id: p.id,
301
- type: "function",
302
- function: { name: p.name, arguments: args }
303
- });
304
- }
305
- }
306
- const msg = {
307
- role: "assistant",
308
- content: textParts.length > 0 ? textParts.join(`
309
- `) : null
329
+ async function preprocessAudioRequest(body, encryptionKeys) {
330
+ const { cipherText, sharedSecret } = encryptionKeys;
331
+ const audioData = await readUploadableToUint8Array(body.file);
332
+ const isDeepgram = body.model.startsWith("deepgram/");
333
+ const requestBody = isDeepgram ? {
334
+ model: body.model,
335
+ diarize: body.diarize,
336
+ smart_format: body.smart_format
337
+ } : {
338
+ model: body.model,
339
+ language: body.language,
340
+ prompt: body.prompt,
341
+ response_format: body.response_format,
342
+ temperature: body.temperature,
343
+ timestamp_granularities: body.timestamp_granularities
344
+ };
345
+ const cleanedBody = Object.fromEntries(Object.entries(requestBody).filter(([_, v]) => v !== undefined));
346
+ const { encrypted, nonce } = encryptPayload(sharedSecret, cleanedBody);
347
+ const { encrypted: encryptedFile, nonce: fileNonce } = encryptPayload(sharedSecret, audioData);
348
+ const fileName = getFileName(body.file) || "audio.mp3";
349
+ const { encrypted: encryptedFileName, nonce: fileNameNonce } = encryptPayload(sharedSecret, fileName);
350
+ return {
351
+ body: {
352
+ cipherText: bytesToHex2(cipherText),
353
+ encryptedInference: bytesToHex2(encrypted),
354
+ nonce: bytesToHex2(nonce),
355
+ fileNameNonce: bytesToHex2(fileNameNonce),
356
+ encryptedFileName: bytesToHex2(encryptedFileName),
357
+ fileNonce: bytesToHex2(fileNonce),
358
+ encryptedFile: bytesToHex2(encryptedFile),
359
+ model: body.model
360
+ },
361
+ sharedSecret
310
362
  };
311
- if (toolCalls.length > 0) {
312
- msg.tool_calls = toolCalls;
313
- }
314
- return msg;
315
363
  }
316
- function anthropicToolsToOpenAI(tools) {
317
- if (tools === undefined) {
318
- return;
319
- }
320
- if (!Array.isArray(tools)) {
321
- throw new AnthropicRequestValidationError("tools must be an array.");
322
- }
323
- const out = [];
324
- for (const t of tools) {
325
- if (!t || typeof t !== "object") {
326
- throw new AnthropicRequestValidationError("Invalid tool entry.");
327
- }
328
- const name = t.name;
329
- const desc = t.description;
330
- const schema = t.input_schema;
331
- if (typeof name !== "string" || name.length === 0) {
332
- throw new AnthropicRequestValidationError("Each tool must include a non-empty name.");
333
- }
334
- if (schema !== undefined && (typeof schema !== "object" || schema === null)) {
335
- throw new AnthropicRequestValidationError("tool input_schema must be an object when provided.");
336
- }
337
- out.push({
338
- type: "function",
339
- function: {
340
- name,
341
- ...typeof desc === "string" ? { description: desc } : {},
342
- parameters: schema ?? {
343
- type: "object",
344
- properties: {}
345
- }
346
- }
347
- });
364
+ async function postprocessTranscriptionResponse(response, sharedSecret) {
365
+ const responseData = await response.json();
366
+ const data = responseData.data;
367
+ if (!data.encryptedResponse || !data.nonce) {
368
+ throw new Error("Invalid transcription response: missing encryptedResponse or nonce");
348
369
  }
349
- return out;
370
+ const responseNonce = hexToBytes2(data.nonce);
371
+ return decryptPayload(data.encryptedResponse, sharedSecret, responseNonce);
350
372
  }
351
- function anthropicToolChoiceToOpenAI(toolChoice) {
352
- if (toolChoice === undefined) {
353
- return;
354
- }
355
- if (typeof toolChoice !== "object" || toolChoice === null || !("type" in toolChoice)) {
356
- throw new AnthropicRequestValidationError("Invalid tool_choice shape.");
357
- }
358
- const tc = toolChoice;
359
- switch (tc.type) {
360
- case "auto":
361
- return "auto";
362
- case "none":
363
- return "none";
364
- case "any":
365
- return "required";
366
- case "tool": {
367
- if (typeof tc.name !== "string" || tc.name.length === 0) {
368
- throw new AnthropicRequestValidationError('tool_choice type "tool" requires a non-empty name.');
369
- }
370
- return { type: "function", function: { name: tc.name } };
371
- }
372
- default:
373
- throw new AnthropicRequestValidationError(`Unsupported tool_choice type "${tc.type}".`);
373
+ async function postprocessTranslationResponse(response, sharedSecret) {
374
+ const responseData = await response.json();
375
+ const data = responseData.data;
376
+ if (!data.encryptedResponse || !data.nonce) {
377
+ throw new Error("Invalid translation response: missing encryptedResponse or nonce");
374
378
  }
379
+ const responseNonce = hexToBytes2(data.nonce);
380
+ return decryptPayload(data.encryptedResponse, sharedSecret, responseNonce);
375
381
  }
376
- function anthropicMessagesCreateToOpenAI(body) {
377
- if (typeof body.model !== "string" || !body.model) {
378
- throw new AnthropicRequestValidationError("model is required.");
379
- }
380
- if (typeof body.max_tokens !== "number" || !Number.isFinite(body.max_tokens)) {
381
- throw new AnthropicRequestValidationError("max_tokens is required and must be a number.");
382
- }
383
- if (!Array.isArray(body.messages)) {
384
- throw new AnthropicRequestValidationError("messages must be an array.");
385
- }
386
- const messages = [];
387
- if (body.system !== undefined) {
388
- messages.push(...systemToOpenAiMessages(body.system));
389
- }
390
- for (const m of body.messages) {
391
- if (m.role !== "user" && m.role !== "assistant") {
392
- throw new AnthropicRequestValidationError(`Invalid message role "${m.role}".`);
393
- }
394
- if (m.role === "user") {
395
- messages.push(...anthropicUserContentToOpenAIMessages(m.content));
396
- } else {
397
- messages.push(anthropicAssistantContentToOpenAI(m.content));
398
- }
399
- }
400
- const isStreaming = Boolean(body.stream);
401
- const params = {
382
+ async function preprocessAudioTranslationRequest(body, encryptionKeys) {
383
+ const { cipherText, sharedSecret } = encryptionKeys;
384
+ const audioData = await readUploadableToUint8Array(body.file);
385
+ const requestBody = {
402
386
  model: body.model,
403
- messages,
404
- max_tokens: body.max_tokens,
405
- stream: isStreaming
387
+ prompt: body.prompt,
388
+ response_format: body.response_format,
389
+ temperature: body.temperature
390
+ };
391
+ const cleanedBody = Object.fromEntries(Object.entries(requestBody).filter(([_, v]) => v !== undefined));
392
+ const { encrypted, nonce } = encryptPayload(sharedSecret, cleanedBody);
393
+ const { encrypted: encryptedFile, nonce: fileNonce } = encryptPayload(sharedSecret, audioData);
394
+ const fileName = getFileName(body.file) || "audio.mp3";
395
+ const { encrypted: encryptedFileName, nonce: fileNameNonce } = encryptPayload(sharedSecret, fileName);
396
+ return {
397
+ body: {
398
+ cipherText: bytesToHex2(cipherText),
399
+ encryptedInference: bytesToHex2(encrypted),
400
+ nonce: bytesToHex2(nonce),
401
+ fileNameNonce: bytesToHex2(fileNameNonce),
402
+ encryptedFileName: bytesToHex2(encryptedFileName),
403
+ fileNonce: bytesToHex2(fileNonce),
404
+ encryptedFile: bytesToHex2(encryptedFile),
405
+ model: body.model
406
+ },
407
+ sharedSecret
406
408
  };
407
- if (isStreaming) {
408
- params.stream_options = { include_usage: true };
409
- }
410
- const tools = anthropicToolsToOpenAI(body.tools);
411
- if (tools !== undefined && tools.length > 0) {
412
- params.tools = tools;
413
- }
414
- const toolChoice = anthropicToolChoiceToOpenAI(body.tool_choice);
415
- if (toolChoice !== undefined) {
416
- params.tool_choice = toolChoice;
417
- }
418
- if (body.stop_sequences !== undefined) {
419
- if (!Array.isArray(body.stop_sequences) || !body.stop_sequences.every((s) => typeof s === "string")) {
420
- throw new AnthropicRequestValidationError("stop_sequences must be an array of strings.");
421
- }
422
- params.stop = body.stop_sequences;
423
- }
424
- if (typeof body.temperature === "number") {
425
- params.temperature = body.temperature;
426
- }
427
- if (typeof body.top_p === "number") {
428
- params.top_p = body.top_p;
429
- }
430
- if (typeof body.top_k === "number") {
431
- console.warn("[proxy] top_k is not supported by the OpenAI API and will be ignored.");
432
- }
433
- return params;
434
409
  }
435
-
436
- // src/anthropic/count-tokens-route.ts
437
- function extractTextCharCount(body) {
438
- let len = 0;
439
- if (typeof body.system === "string") {
440
- len += body.system.length;
441
- } else if (Array.isArray(body.system)) {
442
- for (const block of body.system) {
443
- if (block && block.type === "text" && typeof block.text === "string") {
444
- len += block.text.length;
410
+ function createAudioClient(apiKey, encryptionKeys, requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, attest2 = true) {
411
+ async function createTranscription(body) {
412
+ const controller = new AbortController;
413
+ const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);
414
+ try {
415
+ const sessionId = await attest(apiKey, { model: body.model, enabled: attest2 });
416
+ const encryptedRequest = await preprocessAudioRequest(body, encryptionKeys);
417
+ const response = await fetch(`${endpoints.proxy}/rvenc/audio/transcriptions`, {
418
+ method: "POST",
419
+ headers: {
420
+ "Content-Type": "application/json",
421
+ Authorization: apiKey,
422
+ ...sessionId && { "X-Session-Id": sessionId }
423
+ },
424
+ body: JSON.stringify(encryptedRequest.body),
425
+ signal: controller.signal
426
+ });
427
+ if (!response.ok) {
428
+ await throwIfErrorResponse(response);
445
429
  }
446
- }
447
- } else if (body.system && typeof body.system === "object" && body.system.type === "text") {
448
- len += body.system.text.length;
449
- }
450
- for (const msg of body.messages) {
451
- if (typeof msg.content === "string") {
452
- len += msg.content.length;
453
- } else if (Array.isArray(msg.content)) {
454
- for (const part of msg.content) {
455
- if (!part || typeof part !== "object")
456
- continue;
457
- if (part.type === "text" && typeof part.text === "string") {
458
- len += part.text.length;
459
- } else if (part.type === "tool_result") {
460
- const c = part.content;
461
- if (typeof c === "string") {
462
- len += c.length;
463
- }
464
- }
430
+ clearTimeout(timeoutId);
431
+ return await postprocessTranscriptionResponse(response, encryptedRequest.sharedSecret);
432
+ } catch (error) {
433
+ clearTimeout(timeoutId);
434
+ if (error instanceof Error && error.name === "AbortError") {
435
+ throw new Error(`Request timed out after ${requestTimeoutMs}ms`);
465
436
  }
437
+ throw error;
466
438
  }
467
439
  }
468
- if (Array.isArray(body.tools)) {
469
- len += JSON.stringify(body.tools).length;
470
- }
471
- return len;
472
- }
473
- function registerAnthropicCountTokensRoute(router, _deps) {
474
- router.post("/v1/messages/count_tokens", async (req, res) => {
475
- const requestId = newAnthropicRequestId();
476
- res.setHeader("request-id", requestId);
477
- const versionResult = resolveAnthropicVersion(req);
478
- if (!versionResult.ok) {
479
- return sendAnthropicHttpError(res, 400, "invalid_request_error", versionResult.message, requestId);
480
- }
481
- const apiKey = extractAnthropicApiKey(req);
482
- if (!apiKey) {
483
- return sendAnthropicHttpError(res, 401, "authentication_error", "Missing x-api-key header (or Authorization with API key).", requestId);
484
- }
485
- try {
486
- const raw = req.body;
487
- const body = {
488
- ...raw,
489
- max_tokens: typeof raw.max_tokens === "number" && Number.isFinite(raw.max_tokens) ? raw.max_tokens : 4096,
490
- stream: false
491
- };
492
- anthropicMessagesCreateToOpenAI(body);
493
- const input_tokens = Math.max(1, Math.ceil(extractTextCharCount(body) / 4));
494
- res.json({ input_tokens });
495
- } catch (err) {
496
- if (err instanceof AnthropicRequestValidationError) {
497
- return sendAnthropicHttpError(res, err.status, err.anthropicType, err.message, requestId);
498
- }
499
- mapUnknownErrorToAnthropicResponse(err, res, requestId);
500
- }
501
- });
502
- }
503
-
504
- // src/anthropic/from-openai.ts
505
- function openAiFinishReasonToAnthropic(finish) {
506
- if (!finish) {
507
- return { stop_reason: null, stop_sequence: null };
508
- }
509
- switch (finish) {
510
- case "stop":
511
- return { stop_reason: "end_turn", stop_sequence: null };
512
- case "length":
513
- return { stop_reason: "max_tokens", stop_sequence: null };
514
- case "tool_calls":
515
- return { stop_reason: "tool_use", stop_sequence: null };
516
- case "content_filter":
517
- return { stop_reason: "refusal", stop_sequence: null };
518
- default:
519
- return { stop_reason: "end_turn", stop_sequence: null };
520
- }
521
- }
522
- function extractTextFromAssistantContent(content) {
523
- if (content == null) {
524
- return "";
525
- }
526
- if (typeof content === "string") {
527
- return content;
528
- }
529
- if (!Array.isArray(content)) {
530
- return "";
531
- }
532
- const parts = [];
533
- for (const p of content) {
534
- if (typeof p === "string") {
535
- parts.push(p);
536
- continue;
537
- }
538
- if (p && typeof p === "object" && "type" in p && p.type === "text" && "text" in p) {
539
- parts.push(String(p.text));
540
- }
541
- }
542
- return parts.join("");
543
- }
544
- function openAIChatCompletionToAnthropicMessage(completion, requestModel) {
545
- const choice = completion.choices[0];
546
- const message = choice?.message;
547
- const contentText = message ? extractTextFromAssistantContent(message.content) : "";
548
- const content = [];
549
- if (contentText.length > 0) {
550
- content.push({ type: "text", text: contentText });
551
- }
552
- if (message?.tool_calls?.length) {
553
- for (const tc of message.tool_calls) {
554
- if (tc.type !== "function") {
555
- continue;
556
- }
557
- let input = {};
558
- try {
559
- input = JSON.parse(tc.function.arguments || "{}");
560
- } catch {
561
- input = { _raw_arguments: tc.function.arguments ?? "" };
562
- }
563
- content.push({
564
- type: "tool_use",
565
- id: tc.id,
566
- name: tc.function.name,
567
- input
568
- });
569
- }
570
- }
571
- if (content.length === 0) {
572
- content.push({ type: "text", text: "" });
573
- }
574
- const { stop_reason, stop_sequence } = openAiFinishReasonToAnthropic(choice?.finish_reason);
575
- const u = completion.usage;
576
- const usage = {
577
- input_tokens: u?.prompt_tokens ?? 0,
578
- output_tokens: u?.completion_tokens ?? 0
579
- };
580
- return {
581
- id: newAnthropicMessageId(),
582
- type: "message",
583
- role: "assistant",
584
- content,
585
- model: requestModel,
586
- stop_reason,
587
- stop_sequence,
588
- usage
589
- };
590
- }
591
- function chunkFinishToAnthropic(finish) {
592
- if (!finish) {
593
- return null;
594
- }
595
- return openAiFinishReasonToAnthropic(finish).stop_reason;
596
- }
597
- async function pipeOpenAIChunkStreamToAnthropicSse(res, stream, options) {
598
- const { anthropicModel, messageId } = options;
599
- let textBlockOpen = false;
600
- let inputTokens = 0;
601
- let outputTokens = 0;
602
- let stopReason = null;
603
- const toolStates = new Map;
604
- let nextAnthropicIndex = 0;
605
- let textBlockIndex = null;
606
- writeAnthropicSseEvent(res, "message_start", {
607
- type: "message_start",
608
- message: {
609
- id: messageId,
610
- type: "message",
611
- role: "assistant",
612
- content: [],
613
- model: anthropicModel,
614
- stop_reason: null,
615
- stop_sequence: null,
616
- usage: { input_tokens: inputTokens, output_tokens: outputTokens }
617
- }
618
- });
619
- const ensureTextBlock = () => {
620
- if (textBlockOpen) {
621
- return;
622
- }
623
- textBlockIndex = nextAnthropicIndex++;
624
- textBlockOpen = true;
625
- writeAnthropicSseEvent(res, "content_block_start", {
626
- type: "content_block_start",
627
- index: textBlockIndex,
628
- content_block: { type: "text", text: "" }
629
- });
630
- };
631
- const closeTextBlockIfOpen = () => {
632
- if (!textBlockOpen || textBlockIndex === null) {
633
- return;
634
- }
635
- writeAnthropicSseEvent(res, "content_block_stop", {
636
- type: "content_block_stop",
637
- index: textBlockIndex
638
- });
639
- textBlockOpen = false;
640
- };
641
- const getOrCreateTool = (openAiIdx) => {
642
- let st = toolStates.get(openAiIdx);
643
- if (!st) {
644
- st = {
645
- anthropicIndex: nextAnthropicIndex++,
646
- id: "",
647
- name: "",
648
- lastArgs: "",
649
- argsEmittedLen: 0,
650
- started: false,
651
- stopped: false
652
- };
653
- toolStates.set(openAiIdx, st);
654
- }
655
- return st;
656
- };
657
- const flushToolArgs = (st) => {
658
- if (!st.started || st.lastArgs.length <= st.argsEmittedLen) {
659
- return;
660
- }
661
- const partial = st.lastArgs.slice(st.argsEmittedLen);
662
- st.argsEmittedLen = st.lastArgs.length;
663
- writeAnthropicSseEvent(res, "content_block_delta", {
664
- type: "content_block_delta",
665
- index: st.anthropicIndex,
666
- delta: {
667
- type: "input_json_delta",
668
- partial_json: partial
669
- }
670
- });
440
+ const transcriptionsClient = {
441
+ create: createTranscription
671
442
  };
672
- try {
673
- for await (const chunk of stream) {
674
- if (chunk.usage) {
675
- const u = chunk.usage;
676
- inputTokens = u.prompt_tokens ?? inputTokens;
677
- outputTokens = u.completion_tokens ?? outputTokens;
678
- }
679
- const choice = chunk.choices?.[0];
680
- if (!choice) {
681
- continue;
682
- }
683
- const delta = choice.delta;
684
- if (typeof delta?.content === "string" && delta.content.length > 0) {
685
- ensureTextBlock();
686
- if (textBlockIndex !== null) {
687
- writeAnthropicSseEvent(res, "content_block_delta", {
688
- type: "content_block_delta",
689
- index: textBlockIndex,
690
- delta: { type: "text_delta", text: delta.content }
691
- });
692
- }
693
- }
694
- if (delta?.tool_calls?.length) {
695
- closeTextBlockIfOpen();
696
- for (const tc of delta.tool_calls) {
697
- const idx = typeof tc.index === "number" && Number.isFinite(tc.index) ? tc.index : 0;
698
- const st = getOrCreateTool(idx);
699
- if (typeof tc.id === "string" && tc.id.length > 0) {
700
- st.id = tc.id;
701
- }
702
- const fn = tc.function;
703
- if (fn?.name && fn.name.length > 0) {
704
- st.name = fn.name;
705
- }
706
- if (typeof fn?.arguments === "string") {
707
- st.lastArgs += fn.arguments;
708
- }
709
- if (!st.started && st.id.length > 0 && st.name.length > 0) {
710
- writeAnthropicSseEvent(res, "content_block_start", {
711
- type: "content_block_start",
712
- index: st.anthropicIndex,
713
- content_block: {
714
- type: "tool_use",
715
- id: st.id,
716
- name: st.name
717
- }
718
- });
719
- st.started = true;
720
- }
721
- if (st.started) {
722
- flushToolArgs(st);
723
- }
724
- }
725
- }
726
- if (choice.finish_reason) {
727
- const mapped = chunkFinishToAnthropic(choice.finish_reason);
728
- if (mapped) {
729
- stopReason = mapped;
730
- }
731
- }
732
- }
733
- closeTextBlockIfOpen();
734
- const sortedTools = [...toolStates.values()].sort((a, b) => a.anthropicIndex - b.anthropicIndex);
735
- for (const st of sortedTools) {
736
- if (st.started && !st.stopped) {
737
- writeAnthropicSseEvent(res, "content_block_stop", {
738
- type: "content_block_stop",
739
- index: st.anthropicIndex
740
- });
741
- st.stopped = true;
742
- }
743
- }
744
- writeAnthropicSseEvent(res, "message_delta", {
745
- type: "message_delta",
746
- delta: { stop_reason: stopReason, stop_sequence: null },
747
- usage: {
748
- input_tokens: inputTokens,
749
- output_tokens: outputTokens
750
- }
751
- });
752
- writeAnthropicSseEvent(res, "message_stop", { type: "message_stop" });
753
- res.end();
754
- } catch (err) {
755
- const message = err instanceof Error ? err.message : "Stream error";
756
- writeAnthropicSseEvent(res, "error", {
757
- type: "error",
758
- error: { type: "api_error", message }
759
- });
760
- res.end();
761
- }
762
- }
763
-
764
- // src/anthropic/messages-route.ts
765
- function registerAnthropicMessagesRoute(router, deps) {
766
- router.post("/v1/messages", async (req, res) => {
767
- const requestId = newAnthropicRequestId();
768
- res.setHeader("request-id", requestId);
769
- const versionResult = resolveAnthropicVersion(req);
770
- if (!versionResult.ok) {
771
- return sendAnthropicHttpError(res, 400, "invalid_request_error", versionResult.message, requestId);
772
- }
773
- const apiKey = extractAnthropicApiKey(req);
774
- if (!apiKey) {
775
- return sendAnthropicHttpError(res, 401, "authentication_error", "Missing x-api-key header (or Authorization with API key).", requestId);
776
- }
777
- try {
778
- const body = req.body;
779
- const openaiParams = anthropicMessagesCreateToOpenAI(body);
780
- const client = await deps.getOrCreateClient(apiKey);
781
- const completion = await client.chat.completions.create(openaiParams);
782
- if (body.stream) {
783
- res.status(200);
784
- res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
785
- res.setHeader("Cache-Control", "no-cache");
786
- res.setHeader("Connection", "keep-alive");
787
- if (completion && typeof completion === "object" && Symbol.asyncIterator in completion) {
788
- const messageId = newAnthropicMessageId();
789
- await pipeOpenAIChunkStreamToAnthropicSse(res, completion, {
790
- anthropicModel: body.model,
791
- messageId
792
- });
793
- } else {
794
- sendAnthropicHttpError(res, 500, "api_error", "Expected streamed completion", requestId);
795
- }
796
- return;
443
+ async function createTranslation(body) {
444
+ const controller = new AbortController;
445
+ const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);
446
+ try {
447
+ const sessionId = await attest(apiKey, { model: body.model, enabled: attest2 });
448
+ const encryptedRequest = await preprocessAudioTranslationRequest(body, encryptionKeys);
449
+ const response = await fetch(`${endpoints.proxy}/rvenc/audio/translations`, {
450
+ method: "POST",
451
+ headers: {
452
+ "Content-Type": "application/json",
453
+ Authorization: apiKey,
454
+ ...sessionId && { "X-Session-Id": sessionId }
455
+ },
456
+ body: JSON.stringify(encryptedRequest.body),
457
+ signal: controller.signal
458
+ });
459
+ if (!response.ok) {
460
+ await throwIfErrorResponse(response);
797
461
  }
798
- const message = openAIChatCompletionToAnthropicMessage(completion, body.model);
799
- res.json(message);
800
- } catch (err) {
801
- if (err instanceof AnthropicRequestValidationError) {
802
- return sendAnthropicHttpError(res, err.status, err.anthropicType, err.message, requestId);
462
+ clearTimeout(timeoutId);
463
+ return await postprocessTranslationResponse(response, encryptedRequest.sharedSecret);
464
+ } catch (error) {
465
+ clearTimeout(timeoutId);
466
+ if (error instanceof Error && error.name === "AbortError") {
467
+ throw new Error(`Request timed out after ${requestTimeoutMs}ms`);
803
468
  }
804
- mapUnknownErrorToAnthropicResponse(err, res, requestId);
469
+ throw error;
805
470
  }
806
- });
471
+ }
472
+ const translationsClient = {
473
+ create: createTranslation
474
+ };
475
+ return {
476
+ transcriptions: transcriptionsClient,
477
+ translations: translationsClient
478
+ };
807
479
  }
808
480
 
809
- // src/anthropic/models-route.ts
810
- function toAnthropicModel(model) {
481
+ // src/files/index.ts
482
+ import { bytesToHex as bytesToHex4, hexToBytes as hexToBytes4, randomBytes as randomBytes3 } from "@noble/ciphers/utils.js";
483
+ import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
484
+ import { isValid, parseISO } from "date-fns";
485
+ import { z } from "zod";
486
+
487
+ // src/utils/dek-store.ts
488
+ import { bytesToHex as bytesToHex3, hexToBytes as hexToBytes3, randomBytes as randomBytes2 } from "@noble/ciphers/utils.js";
489
+ function initializeDEKStore(clientKEK) {
490
+ const ragDEK = randomBytes2(32);
491
+ const _clientKEK = clientKEK ? hexToBytes3(clientKEK) : getClientKEK();
492
+ const wrappedRagDEK = wrapDEK(_clientKEK, ragDEK);
811
493
  return {
812
- type: "model",
813
- id: model.model,
814
- display_name: model.name || model.model,
815
- created_at: model.created_at
494
+ fileDEKs: new Map,
495
+ ragDEK: wrappedRagDEK,
496
+ ragVersion: "2"
816
497
  };
817
498
  }
818
- function filterEnabled(models) {
819
- return models.filter((m) => m.enabled !== 0);
820
- }
821
- function parseLimit(raw) {
822
- if (typeof raw !== "string" || raw.length === 0) {
823
- return 20;
499
+ function getClientKEK() {
500
+ if (!process.env.CLIENT_KEK) {
501
+ throw new Error("CLIENT_KEK environment variable is not set.");
824
502
  }
825
- const n = Number.parseInt(raw, 10);
826
- if (!Number.isFinite(n) || n <= 0) {
827
- return 20;
503
+ return hexToBytes3(process.env.CLIENT_KEK);
504
+ }
505
+ function getClientKID(clientKEK) {
506
+ if (clientKEK) {
507
+ return bytesToHex3(keyIdFromKEK(hexToBytes3(clientKEK)));
828
508
  }
829
- return Math.min(n, 1000);
509
+ const _clientKEK = getClientKEK();
510
+ return bytesToHex3(keyIdFromKEK(_clientKEK));
830
511
  }
831
- function paginate(all, beforeId, afterId, limit) {
832
- let start = 0;
833
- let end = all.length;
834
- if (afterId) {
835
- const idx = all.findIndex((m) => m.id === afterId);
836
- if (idx >= 0) {
837
- start = idx + 1;
512
+ function generateNewClientKEK() {
513
+ return bytesToHex3(randomBytes2(32));
514
+ }
515
+
516
+ // src/files/index.ts
517
+ var MAX_FILENAME_LENGTH = 255;
518
+ var MIN_FILENAME_LENGTH = 1;
519
+ var ALLOWED_MIME_TYPES = new Set([
520
+ "image/jpeg",
521
+ "image/png",
522
+ "image/gif",
523
+ "image/webp",
524
+ "application/pdf",
525
+ "application/msword",
526
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
527
+ "application/vnd.ms-excel",
528
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
529
+ "text/plain",
530
+ "text/csv",
531
+ "text/markdown",
532
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
533
+ "video/mp4",
534
+ "video/webm",
535
+ "video/quicktime",
536
+ "audio/mpeg",
537
+ "audio/wav",
538
+ "audio/ogg",
539
+ "application/zip",
540
+ "application/x-rar-compressed",
541
+ "application/x-7z-compressed",
542
+ "application/octet-stream"
543
+ ]);
544
+ var ApiKeySchema = z.string().trim().min(1, "API key cannot be empty");
545
+ var TimeoutSchema = z.number().int().min(1, "Timeout must be at least 1ms").max(600000, "Timeout must not exceed 600000ms (10 minutes)");
546
+ var DEKStoreSchema = z.object({
547
+ ragDEK: z.instanceof(Uint8Array).optional(),
548
+ fileDEKs: z.instanceof(Map).optional()
549
+ });
550
+ var ISO8601DateSchema = z.string().refine((val) => {
551
+ const date = parseISO(val);
552
+ return isValid(date);
553
+ }, { message: "Must be a valid ISO8601 date" });
554
+ var MimeTypeSchema = z.string().refine((val) => ALLOWED_MIME_TYPES.has(val), {
555
+ message: "MIME type is not allowed"
556
+ });
557
+ var FileUploadOptionsSchema = z.object({
558
+ file: z.instanceof(Uint8Array),
559
+ fileName: z.string().min(MIN_FILENAME_LENGTH, "File name cannot be empty").max(MAX_FILENAME_LENGTH, `File name cannot exceed ${MAX_FILENAME_LENGTH} characters`).refine((name) => !/[<>:"|?*\x00-\x1F]/.test(name), "Invalid characters").refine((name) => !name.includes("..") && !name.includes("/") && !name.includes("\\"), "No path separators allowed"),
560
+ mimeType: z.string().optional(),
561
+ ragIndex: z.boolean().optional()
562
+ });
563
+ var ListFilesOptionsSchema = z.object({
564
+ limit: z.number().int().positive("Limit must be a positive integer").optional(),
565
+ offset: z.number().int().nonnegative("Offset must be a non-negative integer").optional(),
566
+ search: z.string().optional(),
567
+ from: ISO8601DateSchema.optional(),
568
+ to: ISO8601DateSchema.optional()
569
+ }).optional();
570
+ var GetFileOptionsSchema = z.object({
571
+ id: z.string().trim().min(1, "File ID cannot be empty"),
572
+ url: z.boolean().optional()
573
+ });
574
+ var DeleteFileOptionsSchema = z.object({
575
+ id: z.string().min(1, "File ID is required")
576
+ });
577
+ var IndexFileInputSchema = z.object({
578
+ fileId: z.string().min(1, "File ID is required"),
579
+ filePath: z.string().min(1, "File path is required"),
580
+ fileDEK: z.instanceof(Uint8Array).optional()
581
+ });
582
+ var IndexFilesOptionsSchema = z.object({
583
+ files: z.array(IndexFileInputSchema).min(1, "Files array must not be empty"),
584
+ ragDEK: z.instanceof(Uint8Array).optional()
585
+ });
586
+ var DeleteIndexOptionsSchema = z.object({
587
+ fileIds: z.array(z.string().min(1)).min(1, "File IDs array must not be empty"),
588
+ ragDEK: z.instanceof(Uint8Array).optional()
589
+ });
590
+ function validateAPIKey(apiKey) {
591
+ ApiKeySchema.parse(apiKey);
592
+ }
593
+ function validateDEKStore(dekStore) {
594
+ DEKStoreSchema.parse(dekStore);
595
+ }
596
+ function validateMimeType(mimeType) {
597
+ MimeTypeSchema.parse(mimeType);
598
+ }
599
+ function validateFileUploadOptions(options) {
600
+ FileUploadOptionsSchema.parse(options);
601
+ }
602
+ function validateListFilesOptions(options) {
603
+ ListFilesOptionsSchema.parse(options);
604
+ }
605
+ function validateGetFileOptions(options) {
606
+ GetFileOptionsSchema.parse(options);
607
+ }
608
+ function guessMimeType(fileName) {
609
+ const ext = fileName.toLowerCase().split(".").pop() || "";
610
+ const mimeTypeMap = {
611
+ jpg: "image/jpeg",
612
+ jpeg: "image/jpeg",
613
+ png: "image/png",
614
+ gif: "image/gif",
615
+ webp: "image/webp",
616
+ pdf: "application/pdf",
617
+ doc: "application/msword",
618
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
619
+ xls: "application/vnd.ms-excel",
620
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
621
+ txt: "text/plain",
622
+ csv: "text/csv",
623
+ md: "text/markdown",
624
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
625
+ mp4: "video/mp4",
626
+ webm: "video/webm",
627
+ mov: "video/quicktime",
628
+ mp3: "audio/mpeg",
629
+ wav: "audio/wav",
630
+ ogg: "audio/ogg",
631
+ zip: "application/zip",
632
+ rar: "application/x-rar-compressed",
633
+ "7z": "application/x-7z-compressed"
634
+ };
635
+ return mimeTypeMap[ext] || "application/octet-stream";
636
+ }
637
+ async function saveRagDEKToBackend(apiKey, wrappedRagDEK, timeoutMs) {
638
+ const controller = new AbortController;
639
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
640
+ try {
641
+ const response = await fetch(`${endpoints.proxy}/users/save_rag_dek`, {
642
+ method: "POST",
643
+ headers: {
644
+ Authorization: apiKey,
645
+ "Content-Type": "application/json"
646
+ },
647
+ body: JSON.stringify({
648
+ data: {
649
+ wrappedRagDEK,
650
+ confirmReplaceRagDEK: true
651
+ }
652
+ }),
653
+ signal: controller.signal
654
+ });
655
+ if (!response.ok) {
656
+ throw new Error(`Failed to save RAG DEK: HTTP ${response.status}`);
838
657
  }
839
- }
840
- if (beforeId) {
841
- const idx = all.findIndex((m) => m.id === beforeId);
842
- if (idx >= 0) {
843
- end = idx;
658
+ const result = await response.json();
659
+ if (result.error) {
660
+ throw new Error(result.error);
661
+ }
662
+ } catch (error) {
663
+ if (error instanceof Error && error.name === "AbortError") {
664
+ throw new Error(`Save RAG DEK request timed out after ${timeoutMs}ms`);
844
665
  }
666
+ throw error;
667
+ } finally {
668
+ clearTimeout(timeoutId);
669
+ }
670
+ }
671
+ async function prepareEncryptedPayload(dekStore, options, apiKey, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
672
+ const fileBytes = options.file;
673
+ const mimeType = options.mimeType || guessMimeType(options.fileName);
674
+ validateMimeType(mimeType);
675
+ const dek = randomBytes3(32);
676
+ const encryptedFile = encryptWithDEK(dek, fileBytes);
677
+ const encryptedName = encryptMetadataWithDEK(dek, options.fileName);
678
+ const encryptedMimeType = encryptMetadataWithDEK(dek, mimeType);
679
+ const _clientKEK = clientKEK ? hexToBytes4(clientKEK) : getClientKEK();
680
+ const wrappedDEK = wrapDEK(_clientKEK, dek);
681
+ const clientKID = clientKEK ? getClientKID(clientKEK) : getClientKID();
682
+ const filePayload = {
683
+ client_hash: bytesToHex4(sha2562(fileBytes)),
684
+ encrypted_content: bytesToHex4(encryptedFile),
685
+ encrypted_name: encryptedName,
686
+ kid: clientKID,
687
+ mime_type: encryptedMimeType,
688
+ version: "2",
689
+ wrapped_dek: bytesToHex4(wrappedDEK)
690
+ };
691
+ if (options.ragIndex) {
692
+ await addRagIndexToPayload(dekStore, dek, filePayload, apiKey, clientKEK, timeoutMs);
845
693
  }
846
- const window = all.slice(start, end);
847
- const items = window.slice(0, limit);
848
- return { items, hasMore: window.length > items.length };
694
+ return { dek, filePayload };
849
695
  }
850
- function registerAnthropicModelsRoute(router, deps) {
851
- router.get("/v1/models", async (req, res) => {
852
- const requestId = newAnthropicRequestId();
853
- res.setHeader("request-id", requestId);
854
- const versionResult = resolveAnthropicVersion(req);
855
- if (!versionResult.ok) {
856
- return sendAnthropicHttpError(res, 400, "invalid_request_error", versionResult.message, requestId);
857
- }
858
- const apiKey = extractAnthropicApiKey(req);
859
- if (!apiKey) {
860
- return sendAnthropicHttpError(res, 401, "authentication_error", "Missing x-api-key header (or Authorization with API key).", requestId);
861
- }
696
+ async function addRagIndexToPayload(dekStore, dek, filePayload, apiKey, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
697
+ let ragDEK = dekStore.ragDEK;
698
+ const _clientKEK = clientKEK ? hexToBytes4(clientKEK) : getClientKEK();
699
+ if (!ragDEK) {
700
+ ragDEK = randomBytes3(32);
701
+ const wrappedRagDEK = wrapDEK(_clientKEK, ragDEK);
702
+ dekStore.ragDEK = wrappedRagDEK;
862
703
  try {
863
- const client = await deps.getOrCreateClient(apiKey);
864
- const type = typeof req.query.type === "string" ? req.query.type : undefined;
865
- const all = filterEnabled(await client.models.list({ type })).map(toAnthropicModel);
866
- const beforeId = typeof req.query.before_id === "string" ? req.query.before_id : undefined;
867
- const afterId = typeof req.query.after_id === "string" ? req.query.after_id : undefined;
868
- const limit = parseLimit(req.query.limit);
869
- const { items, hasMore } = paginate(all, beforeId, afterId, limit);
870
- res.json({
871
- data: items,
872
- first_id: items.length > 0 ? items[0].id : null,
873
- last_id: items.length > 0 ? items[items.length - 1].id : null,
874
- has_more: hasMore
875
- });
876
- } catch (err) {
877
- mapUnknownErrorToAnthropicResponse(err, res, requestId);
704
+ await saveRagDEKToBackend(apiKey, bytesToHex4(wrappedRagDEK), timeoutMs);
705
+ } catch (error) {
706
+ console.error("Warning: Failed to save RAG DEK to backend:", error);
878
707
  }
708
+ } else {
709
+ ragDEK = unwrapDEK(_clientKEK, ragDEK);
710
+ }
711
+ const enclavePublicKey = await getEnclavePublicKey(timeoutMs);
712
+ const { cipherText, sharedSecret } = createMLKEMEncapsulation(enclavePublicKey);
713
+ const { encrypted: encryptedFileDEK, nonce: fileNonce } = encryptPayload(sharedSecret, dek);
714
+ const { encrypted: encryptedRagDEK, nonce: ragDEKNonce } = encryptPayload(sharedSecret, ragDEK);
715
+ filePayload.encrypted_file_dek = bytesToHex4(encryptedFileDEK);
716
+ filePayload.encrypted_rag_dek = bytesToHex4(encryptedRagDEK);
717
+ filePayload.file_nonce = bytesToHex4(fileNonce);
718
+ filePayload.rag_dek_nonce = bytesToHex4(ragDEKNonce);
719
+ filePayload.cipher_text = bytesToHex4(cipherText);
720
+ }
721
+ async function performUpload(apiKey, filePayload, controller) {
722
+ const uploadResponse = await fetch(`${endpoints.proxy}/files/encrypted/upload`, {
723
+ method: "POST",
724
+ headers: {
725
+ Authorization: apiKey,
726
+ "Content-Type": "application/json"
727
+ },
728
+ body: JSON.stringify(filePayload),
729
+ signal: controller.signal
879
730
  });
880
- router.get("/v1/models/:model_id", async (req, res) => {
881
- const requestId = newAnthropicRequestId();
882
- res.setHeader("request-id", requestId);
883
- const versionResult = resolveAnthropicVersion(req);
884
- if (!versionResult.ok) {
885
- return sendAnthropicHttpError(res, 400, "invalid_request_error", versionResult.message, requestId);
886
- }
887
- const apiKey = extractAnthropicApiKey(req);
888
- if (!apiKey) {
889
- return sendAnthropicHttpError(res, 401, "authentication_error", "Missing x-api-key header (or Authorization with API key).", requestId);
890
- }
891
- const modelId = req.params.model_id;
892
- if (!modelId) {
893
- return sendAnthropicHttpError(res, 400, "invalid_request_error", "Missing model id.", requestId);
894
- }
731
+ if (!uploadResponse.ok) {
732
+ let errorMessage = `Upload request failed with status ${uploadResponse.status}`;
895
733
  try {
896
- const client = await deps.getOrCreateClient(apiKey);
897
- const found = filterEnabled(await client.models.list()).find((m) => m.model === modelId);
898
- if (!found) {
899
- return sendAnthropicHttpError(res, 404, "not_found_error", `Model "${modelId}" not found.`, requestId);
734
+ const body = await uploadResponse.json();
735
+ if (body.error) {
736
+ errorMessage = body.error;
900
737
  }
901
- res.json(toAnthropicModel(found));
902
- } catch (err) {
903
- mapUnknownErrorToAnthropicResponse(err, res, requestId);
904
- }
905
- });
906
- }
907
-
908
- // src/server/runtime.ts
909
- import multer from "multer";
910
-
911
- // src/audio/index.ts
912
- import { bytesToHex as bytesToHex3, hexToBytes as hexToBytes2 } from "@noble/ciphers/utils.js";
913
-
914
- // src/config.ts
915
- var endpoints = {
916
- enclave: process.env.ENCLAVE_URL,
917
- proxy: process.env.PROXY_URL
918
- };
919
- var DEFAULT_REQUEST_TIMEOUT_MS = 600000;
920
- var DEFAULT_MAX_BUFFER_SIZE = 10 * 1024 * 1024;
921
-
922
- // src/utils/attestation.ts
923
- var cachedPrem;
924
- async function loadPrem() {
925
- if (cachedPrem)
926
- return cachedPrem;
927
- const isBare = typeof globalThis.Bare !== "undefined";
928
- if (isBare) {
929
- cachedPrem = await (async (s, y) => await import(s, y))("@premai/reticle", { with: { type: "script" } });
930
- return cachedPrem;
738
+ } catch {}
739
+ throw new Error(errorMessage);
931
740
  }
932
- cachedPrem = await import("@premai/reticle");
933
- return cachedPrem;
934
- }
935
- function isAttestationError(err) {
936
- return err instanceof Error && err.name === "AttestationError";
741
+ const uploadResult = await uploadResponse.json();
742
+ if (uploadResult.status !== 200) {
743
+ throw new Error(uploadResult.error || "Upload failed");
744
+ }
745
+ if (!uploadResult.data) {
746
+ throw new Error("Upload response missing data");
747
+ }
748
+ return uploadResult.data;
937
749
  }
938
- var ATTEST_TTL_MS = 30000;
939
- var ATTEST_CACHE_MAX = 500;
940
- var ATTEST_MAX_ATTEMPTS = 4;
941
- var ATTEST_RETRY_BASE_MS = 250;
942
- var ATTEST_RETRY_MAX_MS = 2000;
943
- var TRANSIENT_PATTERNS = [
944
- /EOF while parsing/i,
945
- /error decoding response body/i,
946
- /connection (reset|closed|refused)/i,
947
- /socket hang up/i,
948
- /ETIMEDOUT/i
949
- ];
950
- var attestCache = new Map;
951
- var attestInflight = new Map;
952
- function attestCacheKey(apiKey, model) {
953
- return `${apiKey}|${model ?? ""}`;
750
+ function storeDEKForFile(dekStore, fileId, dek, clientKEK) {
751
+ if (!dekStore.fileDEKs) {
752
+ dekStore.fileDEKs = new Map;
753
+ }
754
+ const _clientKEK = clientKEK ? hexToBytes4(clientKEK) : getClientKEK();
755
+ const wrappedDEK = wrapDEK(_clientKEK, dek);
756
+ dekStore.fileDEKs.set(fileId, wrappedDEK);
954
757
  }
955
- function pruneExpired(now) {
956
- for (const [key, entry] of attestCache) {
957
- if (entry.expires <= now) {
958
- attestCache.delete(key);
959
- } else {
960
- break;
758
+ async function uploadFile(apiKey, dekStore, options, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
759
+ validateAPIKey(apiKey);
760
+ validateDEKStore(dekStore);
761
+ validateFileUploadOptions(options);
762
+ TimeoutSchema.parse(timeoutMs);
763
+ const controller = new AbortController;
764
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
765
+ try {
766
+ const { dek, filePayload } = await prepareEncryptedPayload(dekStore, options, apiKey, clientKEK, timeoutMs);
767
+ const uploadedFile = await performUpload(apiKey, filePayload, controller);
768
+ storeDEKForFile(dekStore, uploadedFile.id, dek, clientKEK);
769
+ return uploadedFile;
770
+ } catch (error) {
771
+ if (error instanceof Error && error.name === "AbortError") {
772
+ throw new Error(`File upload timed out after ${timeoutMs}ms`);
961
773
  }
774
+ throw error;
775
+ } finally {
776
+ clearTimeout(timeoutId);
962
777
  }
963
778
  }
964
- function isTransientError(err) {
965
- const messages = [];
966
- if (err instanceof Error) {
967
- messages.push(err.message);
779
+ async function listFiles(apiKey, options, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
780
+ validateAPIKey(apiKey);
781
+ validateListFilesOptions(options);
782
+ TimeoutSchema.parse(timeoutMs);
783
+ const controller = new AbortController;
784
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
785
+ const queryParams = new URLSearchParams;
786
+ if (options?.limit !== undefined) {
787
+ queryParams.append("limit", options.limit.toString());
968
788
  }
969
- if (isAttestationError(err) && Array.isArray(err.cause)) {
970
- messages.push(...err.cause);
789
+ if (options?.offset !== undefined) {
790
+ queryParams.append("offset", options.offset.toString());
971
791
  }
972
- return messages.some((m) => TRANSIENT_PATTERNS.some((re) => re.test(m)));
973
- }
974
- function backoffDelayMs(attempt) {
975
- const exp = ATTEST_RETRY_BASE_MS * 2 ** (attempt - 1);
976
- const capped = Math.min(exp, ATTEST_RETRY_MAX_MS);
977
- const jitter = Math.floor(Math.random() * (capped / 2));
978
- return capped + jitter;
979
- }
980
- function delay(ms) {
981
- return new Promise((resolve) => setTimeout(resolve, ms));
982
- }
983
- function safeFree(obj) {
984
- if (typeof obj?.free !== "function")
985
- return;
792
+ if (options?.search) {
793
+ queryParams.append("search", options.search);
794
+ }
795
+ if (options?.from) {
796
+ queryParams.append("from", options.from);
797
+ }
798
+ if (options?.to) {
799
+ queryParams.append("to", options.to);
800
+ }
801
+ const queryString = queryParams.toString();
802
+ const url = `${endpoints.proxy}/files/encrypted${queryString ? `?${queryString}` : ""}`;
986
803
  try {
987
- obj.free();
988
- } catch {}
804
+ const response = await fetch(url, {
805
+ method: "GET",
806
+ headers: {
807
+ Authorization: apiKey,
808
+ "Content-Type": "application/json"
809
+ },
810
+ signal: controller.signal
811
+ });
812
+ if (!response.ok) {
813
+ throw new Error(`List files request failed with status ${response.status}`);
814
+ }
815
+ const result = await response.json();
816
+ if (result.status !== 200) {
817
+ throw new Error(result.error || "List files failed");
818
+ }
819
+ if (!result.data) {
820
+ throw new Error("List files response missing data");
821
+ }
822
+ return result.data;
823
+ } catch (error) {
824
+ if (error instanceof Error && error.name === "AbortError") {
825
+ throw new Error(`List files request timed out after ${timeoutMs}ms`);
826
+ }
827
+ throw error;
828
+ } finally {
829
+ clearTimeout(timeoutId);
830
+ }
989
831
  }
990
- async function attemptAttest(apiKey, options) {
991
- const prem = await loadPrem();
992
- let client;
993
- let attested;
994
- let headers;
995
- let sessionId;
832
+ async function getFile(apiKey, options, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
833
+ validateAPIKey(apiKey);
834
+ validateGetFileOptions(options);
835
+ TimeoutSchema.parse(timeoutMs);
836
+ const controller = new AbortController;
837
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
838
+ const queryParams = new URLSearchParams;
839
+ if (options.url !== undefined) {
840
+ queryParams.append("url", options.url ? "true" : "false");
841
+ }
842
+ const queryString = queryParams.toString();
843
+ const url = `${endpoints.proxy}/files/encrypted/${options.id}${queryString ? `?${queryString}` : ""}`;
996
844
  try {
997
- client = await new prem.ClientBuilder(endpoints.proxy ?? "").with_authorization(apiKey).build();
998
- if (options.model) {
999
- client.set_query(new prem.QueryParams().with("model", options.model));
845
+ const response = await fetch(url, {
846
+ method: "GET",
847
+ headers: {
848
+ Authorization: apiKey,
849
+ "Content-Type": "application/json"
850
+ },
851
+ signal: controller.signal
852
+ });
853
+ if (!response.ok) {
854
+ if (response.status === 404) {
855
+ throw new Error(`File not found: ${options.id}`);
856
+ }
857
+ throw new Error(`Get file request failed with status ${response.status}`);
1000
858
  }
1001
- attested = await client.attest();
1002
- headers = attested.headers();
1003
- sessionId = headers.cpu()?.get("x-session-id") ?? headers.gpu()?.get("x-session-id") ?? null;
859
+ const result = await response.json();
860
+ if (result.status !== 200) {
861
+ throw new Error(result.error || "Get file failed");
862
+ }
863
+ if (!result.data) {
864
+ throw new Error("Get file response missing data");
865
+ }
866
+ return result.data;
867
+ } catch (error) {
868
+ if (error instanceof Error && error.name === "AbortError") {
869
+ throw new Error(`Get file request timed out after ${timeoutMs}ms`);
870
+ }
871
+ throw error;
1004
872
  } finally {
1005
- safeFree(headers);
1006
- safeFree(attested);
1007
- safeFree(client);
1008
- }
1009
- if (sessionId === null) {
1010
- throw new Error("missing x-session-id issued by attestation");
873
+ clearTimeout(timeoutId);
1011
874
  }
1012
- return sessionId;
1013
875
  }
1014
- async function runAttest(apiKey, options) {
1015
- let lastErr;
1016
- for (let attempt = 1;attempt <= ATTEST_MAX_ATTEMPTS; attempt++) {
1017
- try {
1018
- return await attemptAttest(apiKey, options);
1019
- } catch (err) {
1020
- lastErr = err;
1021
- if (attempt === ATTEST_MAX_ATTEMPTS || !isTransientError(err)) {
1022
- throw err;
876
+ async function deleteFile(apiKey, options, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
877
+ validateAPIKey(apiKey);
878
+ DeleteFileOptionsSchema.parse(options);
879
+ TimeoutSchema.parse(timeoutMs);
880
+ const controller = new AbortController;
881
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
882
+ try {
883
+ const response = await fetch(`${endpoints.proxy}/files/encrypted/${options.id}`, {
884
+ method: "DELETE",
885
+ headers: {
886
+ Authorization: apiKey,
887
+ "Content-Type": "application/json"
888
+ },
889
+ signal: controller.signal
890
+ });
891
+ if (!response.ok) {
892
+ if (response.status === 404) {
893
+ throw new Error(`File not found: ${options.id}`);
1023
894
  }
1024
- await delay(backoffDelayMs(attempt));
895
+ throw new Error(`Delete file request failed with status ${response.status}`);
896
+ }
897
+ await response.json();
898
+ } catch (error) {
899
+ if (error instanceof Error && error.name === "AbortError") {
900
+ throw new Error(`Delete file request timed out after ${timeoutMs}ms`);
1025
901
  }
902
+ throw error;
903
+ } finally {
904
+ clearTimeout(timeoutId);
1026
905
  }
1027
- throw lastErr;
1028
906
  }
1029
- async function attest(apiKey, options = { enabled: true }) {
1030
- if (!options.enabled)
1031
- return null;
1032
- const key = attestCacheKey(apiKey, options.model);
1033
- const now = Date.now();
1034
- const cached = attestCache.get(key);
1035
- if (cached) {
1036
- if (cached.expires > now)
1037
- return cached.sessionId;
1038
- attestCache.delete(key);
1039
- }
1040
- const inflight = attestInflight.get(key);
1041
- if (inflight) {
1042
- return inflight;
907
+ async function indexFiles(apiKey, dekStore, options, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
908
+ validateAPIKey(apiKey);
909
+ validateDEKStore(dekStore);
910
+ IndexFilesOptionsSchema.parse(options);
911
+ TimeoutSchema.parse(timeoutMs);
912
+ const wrappedRagDEK = options.ragDEK || dekStore.ragDEK;
913
+ if (!wrappedRagDEK) {
914
+ throw new Error("RAG DEK not found. Provide ragDEK in options or upload at least one file with ragIndex: true.");
1043
915
  }
1044
- const work = runAttest(apiKey, options).then((sessionId) => {
1045
- const insertTime = Date.now();
1046
- pruneExpired(insertTime);
1047
- attestCache.set(key, { sessionId, expires: insertTime + ATTEST_TTL_MS });
1048
- if (attestCache.size > ATTEST_CACHE_MAX) {
1049
- const oldest = attestCache.keys().next().value;
1050
- if (oldest)
1051
- attestCache.delete(oldest);
916
+ const _clientKEK = clientKEK ? hexToBytes4(clientKEK) : getClientKEK();
917
+ const ragDEK = unwrapDEK(_clientKEK, wrappedRagDEK);
918
+ const enclavePublicKey = await getEnclavePublicKey(timeoutMs);
919
+ const { cipherText, sharedSecret } = createMLKEMEncapsulation(enclavePublicKey);
920
+ const encryptedFiles = options.files.map((file) => {
921
+ const wrappedFileDEK = file.fileDEK || dekStore.fileDEKs?.get(file.fileId);
922
+ if (!wrappedFileDEK) {
923
+ throw new Error(`File DEK not found for file: ${file.fileId}. Provide fileDEK or ensure file was uploaded with this DEK store.`);
1052
924
  }
1053
- return sessionId;
1054
- }).finally(() => {
1055
- attestInflight.delete(key);
925
+ const fileDEK = unwrapDEK(_clientKEK, wrappedFileDEK);
926
+ const { encrypted: encryptedFileDEK, nonce: fileNonce } = encryptPayload(sharedSecret, fileDEK);
927
+ const { encrypted: encryptedRagDEK, nonce: ragDEKNonce } = encryptPayload(sharedSecret, ragDEK);
928
+ return {
929
+ file_id: file.fileId,
930
+ encrypted_file_dek: bytesToHex4(encryptedFileDEK),
931
+ encrypted_rag_dek: bytesToHex4(encryptedRagDEK),
932
+ file_nonce: bytesToHex4(fileNonce),
933
+ rag_dek_nonce: bytesToHex4(ragDEKNonce),
934
+ s3_r2_path: file.filePath,
935
+ cipher_text: bytesToHex4(cipherText)
936
+ };
1056
937
  });
1057
- attestInflight.set(key, work);
1058
- return work;
1059
- }
1060
-
1061
- // src/utils/crypto.ts
1062
- import { aeskwp } from "@noble/ciphers/aes.js";
1063
- import { xchacha20poly1305 } from "@noble/ciphers/chacha.js";
1064
- import { bytesToHex as bytesToHex2, hexToBytes, managedNonce, randomBytes as randomBytes2 } from "@noble/ciphers/utils.js";
1065
- import { sha256 } from "@noble/hashes/sha2.js";
1066
- import { sha3_256 } from "@noble/hashes/sha3.js";
1067
- import { XWing } from "@noble/post-quantum/hybrid.js";
1068
- function createMLKEMEncapsulation(publicKeyHex) {
1069
- return XWing.encapsulate(hexToBytes(publicKeyHex));
1070
- }
1071
- function encryptPayload(sharedSecret, data) {
1072
- const nonce = randomBytes2(24);
1073
- const chacha = xchacha20poly1305(sharedSecret, nonce);
1074
- let encodedData;
1075
- if (data instanceof Uint8Array) {
1076
- encodedData = data;
1077
- } else if (typeof data === "string") {
1078
- encodedData = new TextEncoder().encode(data);
1079
- } else {
1080
- encodedData = new TextEncoder().encode(JSON.stringify(data));
938
+ const controller = new AbortController;
939
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
940
+ try {
941
+ const response = await fetch(`${endpoints.proxy}/files/encrypted/index`, {
942
+ method: "POST",
943
+ headers: {
944
+ Authorization: apiKey,
945
+ "Content-Type": "application/json"
946
+ },
947
+ body: JSON.stringify({ files: encryptedFiles }),
948
+ signal: controller.signal
949
+ });
950
+ if (!response.ok) {
951
+ throw new Error(`Index files request failed with status ${response.status}`);
952
+ }
953
+ const result = await response.json();
954
+ return result;
955
+ } catch (error) {
956
+ if (error instanceof Error && error.name === "AbortError") {
957
+ throw new Error(`Index files request timed out after ${timeoutMs}ms`);
958
+ }
959
+ throw error;
960
+ } finally {
961
+ clearTimeout(timeoutId);
1081
962
  }
1082
- const encrypted = chacha.encrypt(encodedData);
1083
- return { encrypted, nonce };
1084
963
  }
1085
- function decryptPayload(encryptedData, sharedSecret, nonce) {
1086
- const chacha = xchacha20poly1305(sharedSecret, nonce);
1087
- const encrypted = hexToBytes(encryptedData);
1088
- const decrypted = chacha.decrypt(encrypted);
1089
- const str = new TextDecoder().decode(decrypted);
964
+ async function deleteIndex(apiKey, dekStore, options, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
965
+ validateAPIKey(apiKey);
966
+ validateDEKStore(dekStore);
967
+ DeleteIndexOptionsSchema.parse(options);
968
+ TimeoutSchema.parse(timeoutMs);
969
+ const wrappedRagDEK = options.ragDEK || dekStore.ragDEK;
970
+ if (!wrappedRagDEK) {
971
+ throw new Error("RAG DEK not found. Provide ragDEK in options or ensure dekStore has a ragDEK.");
972
+ }
973
+ const _clientKEK = clientKEK ? hexToBytes4(clientKEK) : getClientKEK();
974
+ const ragDEK = unwrapDEK(_clientKEK, wrappedRagDEK);
975
+ const enclavePublicKey = await getEnclavePublicKey(timeoutMs);
976
+ const { cipherText, sharedSecret } = createMLKEMEncapsulation(enclavePublicKey);
977
+ const { encrypted: encryptedRagDEK, nonce: ragDEKNonce } = encryptPayload(sharedSecret, ragDEK);
978
+ const controller = new AbortController;
979
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
1090
980
  try {
1091
- return JSON.parse(str);
1092
- } catch {
1093
- return str;
981
+ const response = await fetch(`${endpoints.proxy}/files/encrypted/delete-index`, {
982
+ method: "POST",
983
+ headers: {
984
+ Authorization: apiKey,
985
+ "Content-Type": "application/json"
986
+ },
987
+ body: JSON.stringify({
988
+ cipher_text: bytesToHex4(cipherText),
989
+ encrypted_rag_dek: bytesToHex4(encryptedRagDEK),
990
+ rag_dek_nonce: bytesToHex4(ragDEKNonce),
991
+ fileIds: options.fileIds
992
+ }),
993
+ signal: controller.signal
994
+ });
995
+ if (!response.ok) {
996
+ throw new Error(`Delete index request failed with status ${response.status}`);
997
+ }
998
+ const result = await response.json();
999
+ return result;
1000
+ } catch (error) {
1001
+ if (error instanceof Error && error.name === "AbortError") {
1002
+ throw new Error(`Delete index request timed out after ${timeoutMs}ms`);
1003
+ }
1004
+ throw error;
1005
+ } finally {
1006
+ clearTimeout(timeoutId);
1094
1007
  }
1095
1008
  }
1096
- async function getEnclavePublicKey(timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
1009
+ function createFilesClient(apiKey, dekStore, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
1010
+ return {
1011
+ upload: (options) => uploadFile(apiKey, dekStore, options, clientKEK, timeoutMs),
1012
+ list: (options) => listFiles(apiKey, options, timeoutMs),
1013
+ get: (options) => getFile(apiKey, options, timeoutMs),
1014
+ delete: (options) => deleteFile(apiKey, options, timeoutMs),
1015
+ index: (options) => indexFiles(apiKey, dekStore, options, clientKEK, timeoutMs),
1016
+ deleteIndex: (options) => deleteIndex(apiKey, dekStore, options, clientKEK, timeoutMs)
1017
+ };
1018
+ }
1019
+
1020
+ // src/models/index.ts
1021
+ async function listModels(params, apiKey, timeoutMs) {
1097
1022
  const controller = new AbortController;
1098
1023
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
1024
+ const queryParams = new URLSearchParams;
1025
+ if (params?.type !== undefined) {
1026
+ queryParams.append("type", params.type);
1027
+ }
1028
+ const queryString = queryParams.toString();
1029
+ const url = `${endpoints.proxy}/models${queryString ? `?${queryString}` : ""}`;
1099
1030
  try {
1100
- const response = await fetch(`${endpoints.enclave}/publicKey`, {
1031
+ const response = await fetch(url, {
1032
+ method: "GET",
1033
+ headers: {
1034
+ Authorization: apiKey,
1035
+ "Content-Type": "application/json"
1036
+ },
1101
1037
  signal: controller.signal
1102
1038
  });
1103
1039
  if (!response.ok) {
1104
- throw new Error(`Failed to fetch enclave public key: ${response.status} ${response.statusText}`);
1040
+ throw new Error(`List models request failed with status ${response.status}`);
1105
1041
  }
1106
- const data = await response.json();
1107
- if (!data.publicKey || typeof data.publicKey !== "string") {
1108
- throw new Error("Invalid public key response from enclave");
1042
+ const result = await response.json();
1043
+ if (result.status !== 200) {
1044
+ throw new Error(result.error || "List models failed");
1109
1045
  }
1110
- return data.publicKey;
1046
+ if (!result.data) {
1047
+ throw new Error("List models response missing data");
1048
+ }
1049
+ return result.data;
1111
1050
  } catch (error) {
1112
1051
  if (error instanceof Error && error.name === "AbortError") {
1113
- throw new Error(`Enclave public key request timed out after ${timeoutMs}ms`);
1052
+ throw new Error(`List models request timed out after ${timeoutMs}ms`);
1114
1053
  }
1115
- throw new Error(`Failed to get enclave public key: ${error instanceof Error ? error.message : error}`);
1054
+ throw error;
1116
1055
  } finally {
1117
1056
  clearTimeout(timeoutId);
1118
1057
  }
1119
1058
  }
1120
- async function generateEncryptionKeys(timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
1121
- const enclavePublicKey = await getEnclavePublicKey(timeoutMs);
1122
- return createMLKEMEncapsulation(enclavePublicKey);
1059
+ function createModelsClient(apiKey, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
1060
+ return {
1061
+ list: (params) => listModels(params, apiKey, timeoutMs)
1062
+ };
1123
1063
  }
1124
- function keyIdFromKEK(kek, context = "kek:v1", length = 16) {
1125
- const ctx = new TextEncoder().encode(context);
1126
- const input = new Uint8Array(kek.length + ctx.length);
1127
- input.set(kek, 0);
1128
- input.set(ctx, kek.length);
1129
- const digest = sha256(input);
1130
- return digest.slice(0, length);
1064
+
1065
+ // src/rvenc/index.ts
1066
+ import { bytesToHex as bytesToHex5, hexToBytes as hexToBytes5 } from "@noble/ciphers/utils.js";
1067
+ import OpenAI from "openai";
1068
+ function preprocessRequest(body, encryptionKeys) {
1069
+ const { cipherText, sharedSecret } = encryptionKeys;
1070
+ const { encrypted, nonce } = encryptPayload(sharedSecret, body);
1071
+ return {
1072
+ body: {
1073
+ cipherText: bytesToHex5(cipherText),
1074
+ encryptedInference: bytesToHex5(encrypted),
1075
+ nonce: bytesToHex5(nonce),
1076
+ model: body.model,
1077
+ stream: body.stream === true
1078
+ },
1079
+ sharedSecret,
1080
+ nonce
1081
+ };
1131
1082
  }
1132
- function encryptWithDEK(dek, plaintext) {
1133
- const aead = managedNonce(xchacha20poly1305)(dek);
1134
- return aead.encrypt(plaintext);
1083
+ async function postprocessStreamingResponse(response, sharedSecret, nonce, maxBufferSize) {
1084
+ if (!response.body) {
1085
+ throw new Error("Response body is null");
1086
+ }
1087
+ const reader = response.body.getReader();
1088
+ const generator = createDecryptedStreamGenerator(reader, sharedSecret, nonce, maxBufferSize);
1089
+ return {
1090
+ [Symbol.asyncIterator]() {
1091
+ return generator;
1092
+ }
1093
+ };
1135
1094
  }
1136
- function encryptMetadataWithDEK(dek, metadata) {
1137
- const encoded = new TextEncoder().encode(metadata);
1138
- const encrypted = encryptWithDEK(dek, encoded);
1139
- return bytesToHex2(encrypted);
1095
+ async function postprocessNonStreamingResponse(response, sharedSecret) {
1096
+ const data = await response.json();
1097
+ if (!data.encryptedResponse || !data.nonce) {
1098
+ throw new Error("Invalid non-streaming response: missing encryptedResponse or nonce");
1099
+ }
1100
+ const responseNonce = hexToBytes5(data.nonce);
1101
+ return decryptPayload(data.encryptedResponse, sharedSecret, responseNonce);
1140
1102
  }
1141
- function wrapDEK(kek, dek) {
1142
- const kw = aeskwp(kek);
1143
- return kw.encrypt(dek);
1103
+ function createRvencChatClient(apiKey, encryptionKeys, requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, maxBufferSize = DEFAULT_MAX_BUFFER_SIZE, attest2 = true, OpenAIClientParams) {
1104
+ const client = new OpenAI({ apiKey: "not-used", ...OpenAIClientParams });
1105
+ const originalChatCreate = client.chat.completions.create.bind(client.chat.completions);
1106
+ client.chat.completions.create = async (body) => {
1107
+ const isStreaming = body.stream === true;
1108
+ const controller = new AbortController;
1109
+ const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);
1110
+ try {
1111
+ const sessionId = await attest(apiKey, { model: body.model, enabled: attest2 });
1112
+ const encryptedRequest = preprocessRequest(body, encryptionKeys);
1113
+ const response = await fetch(`${endpoints.proxy}/rvenc/chat/completions`, {
1114
+ method: "POST",
1115
+ headers: {
1116
+ "Content-Type": "application/json",
1117
+ Accept: isStreaming ? "text/event-stream" : "application/json",
1118
+ Authorization: apiKey,
1119
+ ...sessionId && { "X-Session-Id": sessionId }
1120
+ },
1121
+ body: JSON.stringify(encryptedRequest.body),
1122
+ signal: controller.signal
1123
+ });
1124
+ if (!response.ok) {
1125
+ await throwIfErrorResponse(response);
1126
+ }
1127
+ clearTimeout(timeoutId);
1128
+ if (isStreaming) {
1129
+ const contentType = response.headers.get("content-type") ?? "";
1130
+ if (contentType.includes("text/event-stream")) {
1131
+ return await postprocessStreamingResponse(response, encryptedRequest.sharedSecret, encryptedRequest.nonce, maxBufferSize);
1132
+ }
1133
+ const completion = await postprocessNonStreamingResponse(response, encryptedRequest.sharedSecret);
1134
+ return completionToChunkStream(completion);
1135
+ }
1136
+ return await postprocessNonStreamingResponse(response, encryptedRequest.sharedSecret);
1137
+ } catch (error) {
1138
+ clearTimeout(timeoutId);
1139
+ if (error instanceof Error && error.name === "AbortError") {
1140
+ throw new Error(`Request timed out after ${requestTimeoutMs}ms`);
1141
+ }
1142
+ throw error;
1143
+ }
1144
+ };
1145
+ return client;
1144
1146
  }
1145
- function unwrapDEK(kek, wrappedDEK) {
1146
- const kw = aeskwp(kek);
1147
- return kw.decrypt(wrappedDEK);
1147
+ async function* completionToChunkStream(completion) {
1148
+ const choice = completion.choices[0];
1149
+ const message = choice?.message;
1150
+ const content = typeof message?.content === "string" ? message.content : "";
1151
+ const toolCalls = message?.tool_calls?.filter((tc) => tc.type === "function").map((tc, i) => ({
1152
+ index: i,
1153
+ id: tc.id,
1154
+ type: "function",
1155
+ function: {
1156
+ name: tc.function.name,
1157
+ arguments: tc.function.arguments
1158
+ }
1159
+ }));
1160
+ yield {
1161
+ id: completion.id,
1162
+ object: "chat.completion.chunk",
1163
+ created: completion.created,
1164
+ model: completion.model,
1165
+ choices: [
1166
+ {
1167
+ index: choice?.index ?? 0,
1168
+ delta: {
1169
+ role: "assistant",
1170
+ content,
1171
+ ...toolCalls && toolCalls.length > 0 && { tool_calls: toolCalls }
1172
+ },
1173
+ finish_reason: choice?.finish_reason ?? "stop",
1174
+ logprobs: null
1175
+ }
1176
+ ],
1177
+ usage: completion.usage ?? null
1178
+ };
1148
1179
  }
1149
- function decryptWithDEK(dek, encryptedContent) {
1150
- const aead = managedNonce(xchacha20poly1305)(dek);
1151
- return aead.decrypt(encryptedContent);
1180
+ async function* createDecryptedStreamGenerator(reader, sharedSecret, nonce, maxBufferSize) {
1181
+ const decoder = new TextDecoder;
1182
+ let buffer = "";
1183
+ try {
1184
+ while (true) {
1185
+ const { value, done } = await reader.read();
1186
+ if (done)
1187
+ break;
1188
+ buffer += decoder.decode(value, { stream: true });
1189
+ if (buffer.length > maxBufferSize) {
1190
+ throw new Error(`Stream buffer exceeded maximum size of ${maxBufferSize} bytes`);
1191
+ }
1192
+ const parts = buffer.split(`
1193
+
1194
+ `);
1195
+ for (let i = 0;i < parts.length - 1; i++) {
1196
+ const part = parts[i];
1197
+ const lines = part.split(`
1198
+ `);
1199
+ let event;
1200
+ let data;
1201
+ if (lines[0]) {
1202
+ const eventSplit = lines[0].split(": ");
1203
+ event = eventSplit[1];
1204
+ }
1205
+ if (lines[1]) {
1206
+ const dataSplit = lines[1].split(": ");
1207
+ data = dataSplit.slice(1).join(": ");
1208
+ }
1209
+ if (event === "done" && data === "[DONE]") {
1210
+ return;
1211
+ }
1212
+ if (event === "error") {
1213
+ const errorObj = JSON.parse(data || "{}");
1214
+ throw new Error(errorObj.error?.message || data || "Stream error");
1215
+ }
1216
+ if (event === "data" && data && data !== "[DONE]") {
1217
+ const chunk = decryptPayload(data, sharedSecret, nonce);
1218
+ if (chunk.error) {
1219
+ throw new Error(chunk.error.message || "Stream error");
1220
+ }
1221
+ yield chunk;
1222
+ }
1223
+ }
1224
+ buffer = parts[parts.length - 1];
1225
+ }
1226
+ } finally {
1227
+ reader.releaseLock();
1228
+ }
1152
1229
  }
1153
1230
 
1154
- // src/utils/error.ts
1155
- async function throwIfErrorResponse(response) {
1156
- let raw;
1231
+ // src/tools/index.ts
1232
+ import { bytesToHex as bytesToHex6, hexToBytes as hexToBytes6, randomBytes as randomBytes4 } from "@noble/ciphers/utils.js";
1233
+ var FILE_OUTPUT_TOOLS = ["generateImage", "audioGenerateFromText", "createFileForUser"];
1234
+ var FILE_INPUT_TOOLS = [
1235
+ "imageDescribeAndCaption",
1236
+ "imageDescribeAndCaptionFallback",
1237
+ "videoDescribeAndCaption",
1238
+ "getPDFContent",
1239
+ "getTextDocumentContent",
1240
+ "transcribeAudioToText",
1241
+ "transcribeAudioWithDiarization",
1242
+ "audioDiarization",
1243
+ "getSpreadsheetContent",
1244
+ "getPowerPointContent",
1245
+ "getDataFileContent",
1246
+ "getFileContentOCR"
1247
+ ];
1248
+ var RAG_TOOLS = ["searchRag"];
1249
+ async function callToolRequest(toolName, body, apiKey, timeoutMs, attest2) {
1250
+ const controller = new AbortController;
1251
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
1157
1252
  try {
1158
- raw = await response.json();
1159
- if (!raw.status)
1160
- raw = { ...raw, status: response.status };
1161
- } catch {
1162
- raw = {
1163
- status: response.status,
1164
- data: null,
1165
- error: response.statusText || `HTTP ${response.status}`,
1166
- message: null
1167
- };
1253
+ const response = await fetch(`${endpoints.proxy}/tools/${toolName}`, {
1254
+ method: "POST",
1255
+ headers: {
1256
+ "Content-Type": "application/json",
1257
+ Authorization: apiKey
1258
+ },
1259
+ body: JSON.stringify(body),
1260
+ signal: controller.signal
1261
+ });
1262
+ clearTimeout(timeoutId);
1263
+ if (!response.ok) {
1264
+ await throwIfErrorResponse(response);
1265
+ }
1266
+ const data = await response.json();
1267
+ return data.data;
1268
+ } catch (error) {
1269
+ clearTimeout(timeoutId);
1270
+ if (error instanceof Error && error.name === "AbortError") {
1271
+ throw new Error(`Tool request timed out after ${timeoutMs}ms`);
1272
+ }
1273
+ throw new Error(`Tool request failed: ${error instanceof Error ? error.message : error}`);
1168
1274
  }
1169
- throw raw;
1170
1275
  }
1171
-
1172
- // src/utils/files.ts
1173
- var getFileName = (file) => {
1174
- if (file instanceof File) {
1175
- return file.name;
1176
- }
1177
- if (file instanceof Blob) {
1178
- return;
1179
- }
1180
- const fileAny = file;
1181
- if (fileAny.path) {
1182
- const path = typeof fileAny.path === "string" ? fileAny.path : fileAny.path.toString();
1183
- return path.split("/").pop() || path.split("\\").pop() || path;
1184
- }
1185
- if (file instanceof Uint8Array || file instanceof ArrayBuffer) {
1186
- return;
1187
- }
1188
- return;
1189
- };
1190
-
1191
- // src/audio/index.ts
1192
- async function readUploadableToUint8Array(file) {
1193
- if (file instanceof Uint8Array) {
1194
- return file;
1195
- }
1196
- if (file instanceof ArrayBuffer) {
1197
- return new Uint8Array(file);
1198
- }
1199
- if (typeof file.arrayBuffer === "function") {
1200
- const blob = file;
1201
- const buffer = await blob.arrayBuffer();
1202
- return new Uint8Array(buffer);
1203
- }
1204
- const fileAny = file;
1205
- if (typeof fileAny.on === "function" && (typeof fileAny.read === "function" || typeof fileAny.pipe === "function")) {
1206
- const chunks = [];
1207
- return new Promise((resolve, reject) => {
1208
- fileAny.on("data", (chunk) => {
1209
- if (Buffer.isBuffer(chunk)) {
1210
- chunks.push(new Uint8Array(chunk));
1211
- } else if (chunk instanceof Uint8Array) {
1212
- chunks.push(chunk);
1213
- } else if (typeof chunk === "object" && chunk !== null) {
1214
- chunks.push(new Uint8Array(Buffer.from(chunk)));
1215
- }
1216
- });
1217
- fileAny.on("end", () => {
1218
- const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
1219
- const result = new Uint8Array(totalLength);
1220
- let offset = 0;
1221
- for (const chunk of chunks) {
1222
- result.set(chunk, offset);
1223
- offset += chunk.length;
1224
- }
1225
- resolve(result);
1226
- });
1227
- fileAny.on("error", (err) => reject(err));
1276
+ async function downloadEncryptedFile(fileId, apiKey, timeoutMs) {
1277
+ const controller = new AbortController;
1278
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
1279
+ try {
1280
+ const metadataResponse = await fetch(`${endpoints.proxy}/files/encrypted/${fileId}?url=true`, {
1281
+ headers: { Authorization: apiKey },
1282
+ signal: controller.signal
1228
1283
  });
1284
+ if (!metadataResponse.ok) {
1285
+ throw new Error(`Failed to get file metadata: ${metadataResponse.status}`);
1286
+ }
1287
+ const metadata = await metadataResponse.json();
1288
+ const downloadUrl = metadata.data?.url;
1289
+ if (!downloadUrl) {
1290
+ throw new Error("No download URL in response");
1291
+ }
1292
+ const fileResponse = await fetch(downloadUrl, { signal: controller.signal });
1293
+ if (!fileResponse.ok) {
1294
+ throw new Error(`Failed to download file: ${fileResponse.status}`);
1295
+ }
1296
+ clearTimeout(timeoutId);
1297
+ const arrayBuffer = await fileResponse.arrayBuffer();
1298
+ return new Uint8Array(arrayBuffer);
1299
+ } catch (error) {
1300
+ clearTimeout(timeoutId);
1301
+ if (error instanceof Error && error.name === "AbortError") {
1302
+ throw new Error(`File download timed out after ${timeoutMs}ms`);
1303
+ }
1304
+ throw error;
1229
1305
  }
1230
- throw new Error("Unsupported file type for audio transcription");
1231
1306
  }
1232
- async function preprocessAudioRequest(body, encryptionKeys) {
1233
- const { cipherText, sharedSecret } = encryptionKeys;
1234
- const audioData = await readUploadableToUint8Array(body.file);
1235
- const isDeepgram = body.model.startsWith("deepgram/");
1236
- const requestBody = isDeepgram ? {
1237
- model: body.model,
1238
- diarize: body.diarize,
1239
- smart_format: body.smart_format
1240
- } : {
1241
- model: body.model,
1242
- language: body.language,
1243
- prompt: body.prompt,
1244
- response_format: body.response_format,
1245
- temperature: body.temperature,
1246
- timestamp_granularities: body.timestamp_granularities
1307
+ async function downloadAndDecryptFile(response, dek, apiKey, timeoutMs) {
1308
+ if (!response.success || !response.fileId) {
1309
+ return null;
1310
+ }
1311
+ const decryptFileName = (encryptedHex) => {
1312
+ const encrypted = hexToBytes6(encryptedHex);
1313
+ const decrypted = decryptWithDEK(dek, encrypted);
1314
+ return new TextDecoder().decode(decrypted);
1247
1315
  };
1248
- const cleanedBody = Object.fromEntries(Object.entries(requestBody).filter(([_, v]) => v !== undefined));
1249
- const { encrypted, nonce } = encryptPayload(sharedSecret, cleanedBody);
1250
- const { encrypted: encryptedFile, nonce: fileNonce } = encryptPayload(sharedSecret, audioData);
1251
- const fileName = getFileName(body.file) || "audio.mp3";
1252
- const { encrypted: encryptedFileName, nonce: fileNameNonce } = encryptPayload(sharedSecret, fileName);
1316
+ const fileName = decryptFileName(response.fileName);
1317
+ const mimeType = decryptFileName(response.mimeType);
1318
+ const encryptedFile = await downloadEncryptedFile(response.fileId, apiKey, timeoutMs);
1319
+ const decryptedFile = decryptWithDEK(dek, encryptedFile);
1253
1320
  return {
1254
- body: {
1255
- cipherText: bytesToHex3(cipherText),
1256
- encryptedInference: bytesToHex3(encrypted),
1257
- nonce: bytesToHex3(nonce),
1258
- fileNameNonce: bytesToHex3(fileNameNonce),
1259
- encryptedFileName: bytesToHex3(encryptedFileName),
1260
- fileNonce: bytesToHex3(fileNonce),
1261
- encryptedFile: bytesToHex3(encryptedFile),
1262
- model: body.model
1263
- },
1264
- sharedSecret
1321
+ fileId: response.fileId,
1322
+ fileName,
1323
+ mimeType,
1324
+ content: decryptedFile,
1325
+ fileSize: decryptedFile.length
1265
1326
  };
1266
1327
  }
1267
- async function postprocessTranscriptionResponse(response, sharedSecret) {
1268
- const responseData = await response.json();
1269
- const data = responseData.data;
1270
- if (!data.encryptedResponse || !data.nonce) {
1271
- throw new Error("Invalid transcription response: missing encryptedResponse or nonce");
1272
- }
1273
- const responseNonce = hexToBytes2(data.nonce);
1274
- return decryptPayload(data.encryptedResponse, sharedSecret, responseNonce);
1328
+ async function callSimpleTool(toolName, params, apiKey, timeoutMs, attest2) {
1329
+ const enclavePublicKey = await getEnclavePublicKey(timeoutMs);
1330
+ const { cipherText, sharedSecret } = createMLKEMEncapsulation(enclavePublicKey);
1331
+ const { encrypted, nonce } = encryptPayload(sharedSecret, params);
1332
+ const body = {
1333
+ cipherText: bytesToHex6(cipherText),
1334
+ encryptedParams: bytesToHex6(encrypted),
1335
+ nonce: bytesToHex6(nonce)
1336
+ };
1337
+ const response = await callToolRequest(toolName, body, apiKey, timeoutMs, attest2);
1338
+ return decryptPayload(response.encryptedResponse, sharedSecret, hexToBytes6(response.nonce));
1275
1339
  }
1276
- async function postprocessTranslationResponse(response, sharedSecret) {
1277
- const responseData = await response.json();
1278
- const data = responseData.data;
1279
- if (!data.encryptedResponse || !data.nonce) {
1280
- throw new Error("Invalid translation response: missing encryptedResponse or nonce");
1340
+ async function callFileOutputTool(toolName, params, apiKey, dekStore, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, attest2 = true) {
1341
+ const enclavePublicKey = await getEnclavePublicKey(timeoutMs);
1342
+ const { cipherText, sharedSecret } = createMLKEMEncapsulation(enclavePublicKey);
1343
+ const { encrypted, nonce } = encryptPayload(sharedSecret, params);
1344
+ const dek = randomBytes4(32);
1345
+ const { encrypted: encryptedDEK, nonce: dekNonce } = encryptPayload(sharedSecret, dek);
1346
+ const _clientKEK = clientKEK ? hexToBytes6(clientKEK) : getClientKEK();
1347
+ const wrappedDEK = wrapDEK(_clientKEK, dek);
1348
+ const clientKID = clientKEK ? getClientKID(clientKEK) : getClientKID();
1349
+ const body = {
1350
+ cipherText: bytesToHex6(cipherText),
1351
+ encryptedParams: bytesToHex6(encrypted),
1352
+ nonce: bytesToHex6(nonce),
1353
+ encryptedDEK: bytesToHex6(encryptedDEK),
1354
+ dekNonce: bytesToHex6(dekNonce),
1355
+ kid: clientKID,
1356
+ wrappedDEK: bytesToHex6(wrappedDEK)
1357
+ };
1358
+ const response = await callToolRequest(toolName, body, apiKey, timeoutMs, attest2);
1359
+ const result = await downloadAndDecryptFile(response, dek, apiKey, timeoutMs);
1360
+ if (result?.fileId) {
1361
+ if (!dekStore.fileDEKs) {
1362
+ dekStore.fileDEKs = new Map;
1363
+ }
1364
+ dekStore.fileDEKs.set(result.fileId, wrappedDEK);
1281
1365
  }
1282
- const responseNonce = hexToBytes2(data.nonce);
1283
- return decryptPayload(data.encryptedResponse, sharedSecret, responseNonce);
1366
+ return result;
1284
1367
  }
1285
- async function preprocessAudioTranslationRequest(body, encryptionKeys) {
1286
- const { cipherText, sharedSecret } = encryptionKeys;
1287
- const audioData = await readUploadableToUint8Array(body.file);
1288
- const requestBody = {
1289
- model: body.model,
1290
- prompt: body.prompt,
1291
- response_format: body.response_format,
1292
- temperature: body.temperature
1293
- };
1294
- const cleanedBody = Object.fromEntries(Object.entries(requestBody).filter(([_, v]) => v !== undefined));
1295
- const { encrypted, nonce } = encryptPayload(sharedSecret, cleanedBody);
1296
- const { encrypted: encryptedFile, nonce: fileNonce } = encryptPayload(sharedSecret, audioData);
1297
- const fileName = getFileName(body.file) || "audio.mp3";
1298
- const { encrypted: encryptedFileName, nonce: fileNameNonce } = encryptPayload(sharedSecret, fileName);
1299
- return {
1300
- body: {
1301
- cipherText: bytesToHex3(cipherText),
1302
- encryptedInference: bytesToHex3(encrypted),
1303
- nonce: bytesToHex3(nonce),
1304
- fileNameNonce: bytesToHex3(fileNameNonce),
1305
- encryptedFileName: bytesToHex3(encryptedFileName),
1306
- fileNonce: bytesToHex3(fileNonce),
1307
- encryptedFile: bytesToHex3(encryptedFile),
1308
- model: body.model
1309
- },
1310
- sharedSecret
1368
+ async function callFileInputTool(toolName, params, apiKey, dekStore, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, attest2 = true) {
1369
+ if (!params.fileId) {
1370
+ throw new Error(`Tool ${toolName} requires fileId parameter`);
1371
+ }
1372
+ const enclavePublicKey = await getEnclavePublicKey(timeoutMs);
1373
+ const { cipherText, sharedSecret } = createMLKEMEncapsulation(enclavePublicKey);
1374
+ const dek = randomBytes4(32);
1375
+ const { encrypted: encryptedDEK, nonce: dekNonce } = encryptPayload(sharedSecret, dek);
1376
+ const nonce = randomBytes4(24);
1377
+ if (!dekStore.fileDEKs) {
1378
+ dekStore.fileDEKs = new Map;
1379
+ }
1380
+ const _clientKEK = clientKEK ? hexToBytes6(clientKEK) : getClientKEK();
1381
+ let fileDEK = dekStore.fileDEKs.get(params.fileId);
1382
+ if (!fileDEK) {
1383
+ fileDEK = randomBytes4(32);
1384
+ const wrappedFileDEK = wrapDEK(_clientKEK, fileDEK);
1385
+ dekStore.fileDEKs.set(params.fileId, wrappedFileDEK);
1386
+ } else {
1387
+ fileDEK = unwrapDEK(_clientKEK, fileDEK);
1388
+ }
1389
+ const { encrypted: encryptedFileDEK, nonce: fileDEKNonce } = encryptPayload(sharedSecret, fileDEK);
1390
+ const body = {
1391
+ cipherText: bytesToHex6(cipherText),
1392
+ nonce: bytesToHex6(nonce),
1393
+ fileId: params.fileId,
1394
+ encryptedDEK: bytesToHex6(encryptedDEK),
1395
+ dekNonce: bytesToHex6(dekNonce),
1396
+ encryptedFileDEK: bytesToHex6(encryptedFileDEK),
1397
+ fileDEKNonce: bytesToHex6(fileDEKNonce)
1311
1398
  };
1399
+ const response = await callToolRequest(toolName, body, apiKey, timeoutMs, attest2);
1400
+ return decryptPayload(response.encryptedResponse, sharedSecret, hexToBytes6(response.nonce));
1312
1401
  }
1313
- function createAudioClient(apiKey, encryptionKeys, requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, attest2 = true) {
1314
- async function createTranscription(body) {
1315
- const controller = new AbortController;
1316
- const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);
1317
- try {
1318
- const sessionId = await attest(apiKey, { model: body.model, enabled: attest2 });
1319
- const encryptedRequest = await preprocessAudioRequest(body, encryptionKeys);
1320
- const response = await fetch(`${endpoints.proxy}/rvenc/audio/transcriptions`, {
1321
- method: "POST",
1322
- headers: {
1323
- "Content-Type": "application/json",
1324
- Authorization: apiKey,
1325
- ...sessionId && { "X-Session-Id": sessionId }
1326
- },
1327
- body: JSON.stringify(encryptedRequest.body),
1328
- signal: controller.signal
1329
- });
1330
- if (!response.ok) {
1331
- await throwIfErrorResponse(response);
1332
- }
1333
- clearTimeout(timeoutId);
1334
- return await postprocessTranscriptionResponse(response, encryptedRequest.sharedSecret);
1335
- } catch (error) {
1336
- clearTimeout(timeoutId);
1337
- if (error instanceof Error && error.name === "AbortError") {
1338
- throw new Error(`Request timed out after ${requestTimeoutMs}ms`);
1339
- }
1340
- throw error;
1341
- }
1402
+ async function callRagTool(toolName, params, apiKey, dekStore, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, attest2 = true) {
1403
+ const enclavePublicKey = await getEnclavePublicKey(timeoutMs);
1404
+ const { cipherText, sharedSecret } = createMLKEMEncapsulation(enclavePublicKey);
1405
+ const { encrypted, nonce } = encryptPayload(sharedSecret, params);
1406
+ const dek = randomBytes4(32);
1407
+ const { encrypted: encryptedDEK, nonce: dekNonce } = encryptPayload(sharedSecret, dek);
1408
+ if (!dekStore.fileDEKs) {
1409
+ dekStore.fileDEKs = new Map;
1410
+ }
1411
+ let fileIds = [];
1412
+ if (dekStore.fileDEKs.size > 0) {
1413
+ fileIds = Array.from(dekStore.fileDEKs.keys());
1342
1414
  }
1343
- const transcriptionsClient = {
1344
- create: createTranscription
1345
- };
1346
- async function createTranslation(body) {
1347
- const controller = new AbortController;
1348
- const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);
1349
- try {
1350
- const sessionId = await attest(apiKey, { model: body.model, enabled: attest2 });
1351
- const encryptedRequest = await preprocessAudioTranslationRequest(body, encryptionKeys);
1352
- const response = await fetch(`${endpoints.proxy}/rvenc/audio/translations`, {
1353
- method: "POST",
1354
- headers: {
1355
- "Content-Type": "application/json",
1356
- Authorization: apiKey,
1357
- ...sessionId && { "X-Session-Id": sessionId }
1358
- },
1359
- body: JSON.stringify(encryptedRequest.body),
1360
- signal: controller.signal
1361
- });
1362
- if (!response.ok) {
1363
- await throwIfErrorResponse(response);
1364
- }
1365
- clearTimeout(timeoutId);
1366
- return await postprocessTranslationResponse(response, encryptedRequest.sharedSecret);
1367
- } catch (error) {
1368
- clearTimeout(timeoutId);
1369
- if (error instanceof Error && error.name === "AbortError") {
1370
- throw new Error(`Request timed out after ${requestTimeoutMs}ms`);
1371
- }
1372
- throw error;
1415
+ const _clientKEK = clientKEK ? hexToBytes6(clientKEK) : getClientKEK();
1416
+ const encryptedFileDEKs = fileIds.reduce((acc, fileId) => {
1417
+ const fileDEK = dekStore.fileDEKs?.get(fileId);
1418
+ if (!fileDEK) {
1419
+ return acc;
1373
1420
  }
1421
+ const unwrappedFileDEK = unwrapDEK(_clientKEK, fileDEK);
1422
+ const { encrypted: encryptedFileDEK, nonce: fileDEKNonce } = encryptPayload(sharedSecret, unwrappedFileDEK);
1423
+ acc.push({
1424
+ fileId,
1425
+ encryptedDEK: bytesToHex6(encryptedFileDEK),
1426
+ nonce: bytesToHex6(fileDEKNonce)
1427
+ });
1428
+ return acc;
1429
+ }, []);
1430
+ if (!dekStore.ragDEK) {
1431
+ throw new Error("RAG DEK not found in dekStore. Please upload at least one file with ragIndex: true to initialize RAG.");
1374
1432
  }
1375
- const translationsClient = {
1376
- create: createTranslation
1433
+ if (!dekStore.ragVersion) {
1434
+ throw new Error("RAG Version not found in dekStore. Please upload at least one file with ragIndex: true to initialize RAG.");
1435
+ }
1436
+ const ragDEK = unwrapDEK(_clientKEK, dekStore.ragDEK);
1437
+ const { encrypted: encryptedRagDEK, nonce: ragDEKNonce } = encryptPayload(sharedSecret, ragDEK);
1438
+ const { encrypted: encryptedRagVersion, nonce: ragVersionNonce } = encryptPayload(sharedSecret, dekStore.ragVersion);
1439
+ const body = {
1440
+ cipherText: bytesToHex6(cipherText),
1441
+ encryptedParams: bytesToHex6(encrypted),
1442
+ nonce: bytesToHex6(nonce),
1443
+ encryptedDEK: bytesToHex6(encryptedDEK),
1444
+ dekNonce: bytesToHex6(dekNonce),
1445
+ encryptedFileDEKs,
1446
+ encryptedRagDEK: bytesToHex6(encryptedRagDEK),
1447
+ ragDEKNonce: bytesToHex6(ragDEKNonce),
1448
+ encryptedRagVersion: bytesToHex6(encryptedRagVersion),
1449
+ ragVersionNonce: bytesToHex6(ragVersionNonce)
1377
1450
  };
1451
+ const response = await callToolRequest(toolName, body, apiKey, timeoutMs, attest2);
1452
+ return decryptPayload(response.encryptedResponse, sharedSecret, hexToBytes6(response.nonce));
1453
+ }
1454
+ async function callTool(toolName, params, apiKey, dekStore, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, attest2 = true) {
1455
+ if (FILE_OUTPUT_TOOLS.includes(toolName)) {
1456
+ return callFileOutputTool(toolName, params, apiKey, dekStore, clientKEK, timeoutMs, attest2);
1457
+ } else if (FILE_INPUT_TOOLS.includes(toolName)) {
1458
+ return callFileInputTool(toolName, params, apiKey, dekStore, clientKEK, timeoutMs, attest2);
1459
+ } else if (RAG_TOOLS.includes(toolName)) {
1460
+ return callRagTool(toolName, params, apiKey, dekStore, clientKEK, timeoutMs, attest2);
1461
+ } else {
1462
+ return callSimpleTool(toolName, params, apiKey, timeoutMs, attest2);
1463
+ }
1464
+ }
1465
+ function createToolsClient(apiKey, dekStore, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, attest2 = true) {
1378
1466
  return {
1379
- transcriptions: transcriptionsClient,
1380
- translations: translationsClient
1467
+ generateImage: (params) => callTool("generateImage", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
1468
+ audioGenerateFromText: (params) => callTool("audioGenerateFromText", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
1469
+ createFileForUser: (params) => callTool("createFileForUser", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
1470
+ imageDescribeAndCaption: (params) => callTool("imageDescribeAndCaption", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
1471
+ imageDescribeAndCaptionFallback: (params) => callTool("imageDescribeAndCaptionFallback", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
1472
+ videoDescribeAndCaption: (params) => callTool("videoDescribeAndCaption", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
1473
+ getPDFContent: (params) => callTool("getPDFContent", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
1474
+ getTextDocumentContent: (params) => callTool("getTextDocumentContent", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
1475
+ transcribeAudioToText: (params) => callTool("transcribeAudioToText", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
1476
+ transcribeAudioWithDiarization: (params) => callTool("transcribeAudioWithDiarization", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
1477
+ audioDiarization: (params) => callTool("audioDiarization", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
1478
+ getFileContentOCR: (params) => callTool("getFileContentOCR", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
1479
+ getSpreadsheetContent: (params) => callTool("getSpreadsheetContent", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
1480
+ getDataFileContent: (params) => callTool("getDataFileContent", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
1481
+ getPowerPointContent: (params) => callTool("getPowerPointContent", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
1482
+ getTime: (params) => callTool("getTime", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
1483
+ webSearchTool: (params) => callTool("webSearchTool", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
1484
+ webPageScraperTool: (params) => callTool("webPageScraperTool", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
1485
+ searchRag: (params) => callTool("searchRag", params, apiKey, dekStore, clientKEK, timeoutMs, attest2)
1381
1486
  };
1382
1487
  }
1383
1488
 
1384
- // src/files/index.ts
1385
- import { bytesToHex as bytesToHex5, hexToBytes as hexToBytes4, randomBytes as randomBytes4 } from "@noble/ciphers/utils.js";
1386
- import { sha256 as sha2562 } from "@noble/hashes/sha2.js";
1387
- import { isValid, parseISO } from "date-fns";
1388
- import { z } from "zod";
1489
+ // src/core.ts
1490
+ async function createRvencClient(options) {
1491
+ const {
1492
+ apiKey,
1493
+ clientKEK,
1494
+ requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS,
1495
+ maxBufferSize = DEFAULT_MAX_BUFFER_SIZE,
1496
+ attest: attest2 = true
1497
+ } = options;
1498
+ if (options.config?.endpoints !== undefined) {
1499
+ Object.assign(endpoints, options.config.endpoints);
1500
+ }
1501
+ let encryptionKeys;
1502
+ try {
1503
+ encryptionKeys = options.encryptionKeys ?? await generateEncryptionKeys(requestTimeoutMs);
1504
+ } catch (error) {
1505
+ throw new Error(`Failed to initialize encryption keys: ${error instanceof Error ? error.message : error}`);
1506
+ }
1507
+ const dekStore = options.dekStore ?? initializeDEKStore(clientKEK);
1508
+ const client = createRvencChatClient(apiKey, encryptionKeys, requestTimeoutMs, maxBufferSize, attest2, options.config?.openAIClientOptions ?? {});
1509
+ client.files = createFilesClient(apiKey, dekStore, clientKEK, requestTimeoutMs);
1510
+ client.tools = createToolsClient(apiKey, dekStore, clientKEK, requestTimeoutMs, attest2);
1511
+ client.audio = createAudioClient(apiKey, encryptionKeys, requestTimeoutMs, attest2);
1512
+ client.models = createModelsClient(apiKey, requestTimeoutMs);
1513
+ client.dekStore = dekStore;
1514
+ return client;
1515
+ }
1516
+ var core_default = createRvencClient;
1389
1517
 
1390
- // src/utils/dek-store.ts
1391
- import { bytesToHex as bytesToHex4, hexToBytes as hexToBytes3, randomBytes as randomBytes3 } from "@noble/ciphers/utils.js";
1392
- function initializeDEKStore(clientKEK) {
1393
- const ragDEK = randomBytes3(32);
1394
- const _clientKEK = clientKEK ? hexToBytes3(clientKEK) : getClientKEK();
1395
- const wrappedRagDEK = wrapDEK(_clientKEK, ragDEK);
1396
- return {
1397
- fileDEKs: new Map,
1398
- ragDEK: wrappedRagDEK,
1399
- ragVersion: "2"
1400
- };
1518
+ // src/server/create-app.ts
1519
+ import express from "express";
1520
+
1521
+ // src/anthropic/http.ts
1522
+ import { bytesToHex as bytesToHex7, randomBytes as randomBytes5 } from "@noble/ciphers/utils.js";
1523
+ var ANTHROPIC_VERSION_DEFAULT = "2023-06-01";
1524
+ var ANTHROPIC_VERSION_DATE = /^\d{4}-\d{2}-\d{2}$/;
1525
+ function isAnthropicApiVersionSupported(version) {
1526
+ if (version === ANTHROPIC_VERSION_DEFAULT) {
1527
+ return true;
1528
+ }
1529
+ return ANTHROPIC_VERSION_DATE.test(version);
1401
1530
  }
1402
- function getClientKEK() {
1403
- if (!process.env.CLIENT_KEK) {
1404
- throw new Error("CLIENT_KEK environment variable is not set.");
1531
+ function newAnthropicRequestId() {
1532
+ return `req_${bytesToHex7(randomBytes5(12))}`;
1533
+ }
1534
+ function newAnthropicMessageId() {
1535
+ return `msg_${bytesToHex7(randomBytes5(12))}`;
1536
+ }
1537
+ function extractAnthropicApiKey(req) {
1538
+ const raw = req.headers["x-api-key"];
1539
+ if (typeof raw === "string" && raw.length > 0) {
1540
+ return raw;
1405
1541
  }
1406
- return hexToBytes3(process.env.CLIENT_KEK);
1542
+ if (Array.isArray(raw) && raw[0]) {
1543
+ return raw[0];
1544
+ }
1545
+ const authHeader = req.headers.authorization;
1546
+ if (!authHeader) {
1547
+ return null;
1548
+ }
1549
+ if (authHeader.startsWith("Bearer ")) {
1550
+ return authHeader.slice(7);
1551
+ }
1552
+ return authHeader;
1407
1553
  }
1408
- function getClientKID(clientKEK) {
1409
- if (clientKEK) {
1410
- return bytesToHex4(keyIdFromKEK(hexToBytes3(clientKEK)));
1554
+ function getAnthropicVersionHeader(req) {
1555
+ const raw = req.headers["anthropic-version"];
1556
+ if (typeof raw === "string" && raw.length > 0) {
1557
+ return raw;
1411
1558
  }
1412
- const _clientKEK = getClientKEK();
1413
- return bytesToHex4(keyIdFromKEK(_clientKEK));
1559
+ if (Array.isArray(raw) && raw[0]) {
1560
+ return raw[0];
1561
+ }
1562
+ return null;
1414
1563
  }
1415
-
1416
- // src/files/index.ts
1417
- var MAX_FILENAME_LENGTH = 255;
1418
- var MIN_FILENAME_LENGTH = 1;
1419
- var ALLOWED_MIME_TYPES = new Set([
1420
- "image/jpeg",
1421
- "image/png",
1422
- "image/gif",
1423
- "image/webp",
1424
- "application/pdf",
1425
- "application/msword",
1426
- "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1427
- "application/vnd.ms-excel",
1428
- "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1429
- "text/plain",
1430
- "text/csv",
1431
- "text/markdown",
1432
- "application/vnd.openxmlformats-officedocument.presentationml.presentation",
1433
- "video/mp4",
1434
- "video/webm",
1435
- "video/quicktime",
1436
- "audio/mpeg",
1437
- "audio/wav",
1438
- "audio/ogg",
1439
- "application/zip",
1440
- "application/x-rar-compressed",
1441
- "application/x-7z-compressed",
1442
- "application/octet-stream"
1443
- ]);
1444
- var ApiKeySchema = z.string().trim().min(1, "API key cannot be empty");
1445
- var TimeoutSchema = z.number().int().min(1, "Timeout must be at least 1ms").max(600000, "Timeout must not exceed 600000ms (10 minutes)");
1446
- var DEKStoreSchema = z.object({
1447
- ragDEK: z.instanceof(Uint8Array).optional(),
1448
- fileDEKs: z.instanceof(Map).optional()
1449
- });
1450
- var ISO8601DateSchema = z.string().refine((val) => {
1451
- const date = parseISO(val);
1452
- return isValid(date);
1453
- }, { message: "Must be a valid ISO8601 date" });
1454
- var MimeTypeSchema = z.string().refine((val) => ALLOWED_MIME_TYPES.has(val), {
1455
- message: "MIME type is not allowed"
1456
- });
1457
- var FileUploadOptionsSchema = z.object({
1458
- file: z.instanceof(Uint8Array),
1459
- fileName: z.string().min(MIN_FILENAME_LENGTH, "File name cannot be empty").max(MAX_FILENAME_LENGTH, `File name cannot exceed ${MAX_FILENAME_LENGTH} characters`).refine((name) => !/[<>:"|?*\x00-\x1F]/.test(name), "Invalid characters").refine((name) => !name.includes("..") && !name.includes("/") && !name.includes("\\"), "No path separators allowed"),
1460
- mimeType: z.string().optional(),
1461
- ragIndex: z.boolean().optional()
1462
- });
1463
- var ListFilesOptionsSchema = z.object({
1464
- limit: z.number().int().positive("Limit must be a positive integer").optional(),
1465
- offset: z.number().int().nonnegative("Offset must be a non-negative integer").optional(),
1466
- search: z.string().optional(),
1467
- from: ISO8601DateSchema.optional(),
1468
- to: ISO8601DateSchema.optional()
1469
- }).optional();
1470
- var GetFileOptionsSchema = z.object({
1471
- id: z.string().trim().min(1, "File ID cannot be empty"),
1472
- url: z.boolean().optional()
1473
- });
1474
- var DeleteFileOptionsSchema = z.object({
1475
- id: z.string().min(1, "File ID is required")
1476
- });
1477
- var IndexFileInputSchema = z.object({
1478
- fileId: z.string().min(1, "File ID is required"),
1479
- filePath: z.string().min(1, "File path is required"),
1480
- fileDEK: z.instanceof(Uint8Array).optional()
1481
- });
1482
- var IndexFilesOptionsSchema = z.object({
1483
- files: z.array(IndexFileInputSchema).min(1, "Files array must not be empty"),
1484
- ragDEK: z.instanceof(Uint8Array).optional()
1485
- });
1486
- var DeleteIndexOptionsSchema = z.object({
1487
- fileIds: z.array(z.string().min(1)).min(1, "File IDs array must not be empty"),
1488
- ragDEK: z.instanceof(Uint8Array).optional()
1489
- });
1490
- function validateAPIKey(apiKey) {
1491
- ApiKeySchema.parse(apiKey);
1564
+ function resolveAnthropicVersion(req) {
1565
+ const header = getAnthropicVersionHeader(req);
1566
+ const version = header ?? ANTHROPIC_VERSION_DEFAULT;
1567
+ if (!isAnthropicApiVersionSupported(version)) {
1568
+ return {
1569
+ ok: false,
1570
+ message: `Unsupported anthropic-version: ${version}. Expected a dated version (YYYY-MM-DD) or ${ANTHROPIC_VERSION_DEFAULT}.`
1571
+ };
1572
+ }
1573
+ return { ok: true, version };
1492
1574
  }
1493
- function validateDEKStore(dekStore) {
1494
- DEKStoreSchema.parse(dekStore);
1575
+ function sendAnthropicHttpError(res, status, errorType, message, requestId) {
1576
+ res.setHeader("request-id", requestId);
1577
+ res.status(status).json({
1578
+ type: "error",
1579
+ error: { type: errorType, message },
1580
+ request_id: requestId
1581
+ });
1495
1582
  }
1496
- function validateMimeType(mimeType) {
1497
- MimeTypeSchema.parse(mimeType);
1583
+ function httpStatusToAnthropicErrorType(status) {
1584
+ if (status === 401) {
1585
+ return "authentication_error";
1586
+ }
1587
+ if (status === 402) {
1588
+ return "billing_error";
1589
+ }
1590
+ if (status === 403) {
1591
+ return "permission_error";
1592
+ }
1593
+ if (status === 404) {
1594
+ return "not_found_error";
1595
+ }
1596
+ if (status === 413) {
1597
+ return "request_too_large";
1598
+ }
1599
+ if (status === 429) {
1600
+ return "rate_limit_error";
1601
+ }
1602
+ if (status === 504) {
1603
+ return "timeout_error";
1604
+ }
1605
+ if (status === 529) {
1606
+ return "overloaded_error";
1607
+ }
1608
+ if (status >= 400 && status < 500) {
1609
+ return "invalid_request_error";
1610
+ }
1611
+ return "api_error";
1498
1612
  }
1499
- function validateFileUploadOptions(options) {
1500
- FileUploadOptionsSchema.parse(options);
1613
+ function extractErrorMessage(err) {
1614
+ if (!err || typeof err !== "object") {
1615
+ return null;
1616
+ }
1617
+ const o = err;
1618
+ if (typeof o.message === "string" && o.message.length > 0) {
1619
+ return o.message;
1620
+ }
1621
+ if (typeof o.error === "string" && o.error.length > 0) {
1622
+ return o.error;
1623
+ }
1624
+ if (o.error && typeof o.error === "object") {
1625
+ const nested = o.error.message;
1626
+ if (typeof nested === "string" && nested.length > 0) {
1627
+ return nested;
1628
+ }
1629
+ }
1630
+ return null;
1501
1631
  }
1502
- function validateListFilesOptions(options) {
1503
- ListFilesOptionsSchema.parse(options);
1632
+ function looksLikeApiErrorResponse(err) {
1633
+ if (!err || typeof err !== "object")
1634
+ return false;
1635
+ const o = err;
1636
+ if (typeof o.status !== "number")
1637
+ return false;
1638
+ return "error" in o || "message" in o;
1504
1639
  }
1505
- function validateGetFileOptions(options) {
1506
- GetFileOptionsSchema.parse(options);
1640
+ function mapUnknownErrorToAnthropicResponse(err, res, requestId) {
1641
+ if (looksLikeApiErrorResponse(err)) {
1642
+ const status = err.status >= 400 && err.status < 600 ? err.status : 500;
1643
+ const message2 = extractErrorMessage(err) ?? "Request failed";
1644
+ const errorType = httpStatusToAnthropicErrorType(status);
1645
+ sendAnthropicHttpError(res, status, errorType, message2, requestId);
1646
+ return;
1647
+ }
1648
+ const message = extractErrorMessage(err) ?? (err instanceof Error ? err.message : "Internal server error");
1649
+ sendAnthropicHttpError(res, 500, "api_error", message, requestId);
1507
1650
  }
1508
- function guessMimeType(fileName) {
1509
- const ext = fileName.toLowerCase().split(".").pop() || "";
1510
- const mimeTypeMap = {
1511
- jpg: "image/jpeg",
1512
- jpeg: "image/jpeg",
1513
- png: "image/png",
1514
- gif: "image/gif",
1515
- webp: "image/webp",
1516
- pdf: "application/pdf",
1517
- doc: "application/msword",
1518
- docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1519
- xls: "application/vnd.ms-excel",
1520
- xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1521
- txt: "text/plain",
1522
- csv: "text/csv",
1523
- md: "text/markdown",
1524
- pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
1525
- mp4: "video/mp4",
1526
- webm: "video/webm",
1527
- mov: "video/quicktime",
1528
- mp3: "audio/mpeg",
1529
- wav: "audio/wav",
1530
- ogg: "audio/ogg",
1531
- zip: "application/zip",
1532
- rar: "application/x-rar-compressed",
1533
- "7z": "application/x-7z-compressed"
1534
- };
1535
- return mimeTypeMap[ext] || "application/octet-stream";
1651
+ function writeAnthropicSseEvent(res, event, data) {
1652
+ res.write(`event: ${event}
1653
+ data: ${JSON.stringify(data)}
1654
+
1655
+ `);
1536
1656
  }
1537
- async function saveRagDEKToBackend(apiKey, wrappedRagDEK, timeoutMs) {
1538
- const controller = new AbortController;
1539
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
1540
- try {
1541
- const response = await fetch(`${endpoints.proxy}/users/save_rag_dek`, {
1542
- method: "POST",
1543
- headers: {
1544
- Authorization: apiKey,
1545
- "Content-Type": "application/json"
1546
- },
1547
- body: JSON.stringify({
1548
- data: {
1549
- wrappedRagDEK,
1550
- confirmReplaceRagDEK: true
1551
- }
1552
- }),
1553
- signal: controller.signal
1554
- });
1555
- if (!response.ok) {
1556
- throw new Error(`Failed to save RAG DEK: HTTP ${response.status}`);
1657
+
1658
+ // src/anthropic/to-openai.ts
1659
+ class AnthropicRequestValidationError extends Error {
1660
+ status = 400;
1661
+ anthropicType = "invalid_request_error";
1662
+ constructor(message) {
1663
+ super(message);
1664
+ this.name = "AnthropicRequestValidationError";
1665
+ }
1666
+ }
1667
+ function systemToOpenAiMessages(system) {
1668
+ if (typeof system === "string") {
1669
+ if (system.length === 0) {
1670
+ return [];
1557
1671
  }
1558
- const result = await response.json();
1559
- if (result.error) {
1560
- throw new Error(result.error);
1672
+ return [{ role: "system", content: system }];
1673
+ }
1674
+ if (Array.isArray(system)) {
1675
+ const parts = [];
1676
+ for (const block of system) {
1677
+ if (block && block.type === "text" && typeof block.text === "string") {
1678
+ parts.push(block.text);
1679
+ } else if (block && typeof block === "object") {
1680
+ console.warn(`[proxy] system block type "${block.type}" is not supported and will be ignored.`);
1681
+ }
1561
1682
  }
1562
- } catch (error) {
1563
- if (error instanceof Error && error.name === "AbortError") {
1564
- throw new Error(`Save RAG DEK request timed out after ${timeoutMs}ms`);
1683
+ if (parts.length === 0) {
1684
+ return [];
1565
1685
  }
1566
- throw error;
1567
- } finally {
1568
- clearTimeout(timeoutId);
1686
+ return [{ role: "system", content: parts.join(`
1687
+
1688
+ `) }];
1569
1689
  }
1570
- }
1571
- async function prepareEncryptedPayload(dekStore, options, apiKey, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
1572
- const fileBytes = options.file;
1573
- const mimeType = options.mimeType || guessMimeType(options.fileName);
1574
- validateMimeType(mimeType);
1575
- const dek = randomBytes4(32);
1576
- const encryptedFile = encryptWithDEK(dek, fileBytes);
1577
- const encryptedName = encryptMetadataWithDEK(dek, options.fileName);
1578
- const encryptedMimeType = encryptMetadataWithDEK(dek, mimeType);
1579
- const _clientKEK = clientKEK ? hexToBytes4(clientKEK) : getClientKEK();
1580
- const wrappedDEK = wrapDEK(_clientKEK, dek);
1581
- const clientKID = clientKEK ? getClientKID(clientKEK) : getClientKID();
1582
- const filePayload = {
1583
- client_hash: bytesToHex5(sha2562(fileBytes)),
1584
- encrypted_content: bytesToHex5(encryptedFile),
1585
- encrypted_name: encryptedName,
1586
- kid: clientKID,
1587
- mime_type: encryptedMimeType,
1588
- version: "2",
1589
- wrapped_dek: bytesToHex5(wrappedDEK)
1590
- };
1591
- if (options.ragIndex) {
1592
- await addRagIndexToPayload(dekStore, dek, filePayload, apiKey, clientKEK, timeoutMs);
1690
+ if (system.type === "text" && typeof system.text === "string") {
1691
+ return [{ role: "system", content: system.text }];
1593
1692
  }
1594
- return { dek, filePayload };
1693
+ throw new AnthropicRequestValidationError("Invalid system parameter shape.");
1595
1694
  }
1596
- async function addRagIndexToPayload(dekStore, dek, filePayload, apiKey, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
1597
- let ragDEK = dekStore.ragDEK;
1598
- const _clientKEK = clientKEK ? hexToBytes4(clientKEK) : getClientKEK();
1599
- if (!ragDEK) {
1600
- ragDEK = randomBytes4(32);
1601
- const wrappedRagDEK = wrapDEK(_clientKEK, ragDEK);
1602
- dekStore.ragDEK = wrappedRagDEK;
1603
- try {
1604
- await saveRagDEKToBackend(apiKey, bytesToHex5(wrappedRagDEK), timeoutMs);
1605
- } catch (error) {
1606
- console.error("Warning: Failed to save RAG DEK to backend:", error);
1695
+ function toolResultContentToString(content) {
1696
+ if (typeof content === "string") {
1697
+ return content;
1698
+ }
1699
+ if (content === null || content === undefined) {
1700
+ return "";
1701
+ }
1702
+ if (Array.isArray(content)) {
1703
+ const parts = [];
1704
+ for (const block of content) {
1705
+ if (block && typeof block === "object" && "type" in block && block.type === "text" && typeof block.text === "string") {
1706
+ parts.push(block.text);
1707
+ } else {
1708
+ parts.push(JSON.stringify(block));
1709
+ }
1607
1710
  }
1608
- } else {
1609
- ragDEK = unwrapDEK(_clientKEK, ragDEK);
1711
+ return parts.join(`
1712
+ `);
1610
1713
  }
1611
- const enclavePublicKey = await getEnclavePublicKey(timeoutMs);
1612
- const { cipherText, sharedSecret } = createMLKEMEncapsulation(enclavePublicKey);
1613
- const { encrypted: encryptedFileDEK, nonce: fileNonce } = encryptPayload(sharedSecret, dek);
1614
- const { encrypted: encryptedRagDEK, nonce: ragDEKNonce } = encryptPayload(sharedSecret, ragDEK);
1615
- filePayload.encrypted_file_dek = bytesToHex5(encryptedFileDEK);
1616
- filePayload.encrypted_rag_dek = bytesToHex5(encryptedRagDEK);
1617
- filePayload.file_nonce = bytesToHex5(fileNonce);
1618
- filePayload.rag_dek_nonce = bytesToHex5(ragDEKNonce);
1619
- filePayload.cipher_text = bytesToHex5(cipherText);
1714
+ return JSON.stringify(content);
1620
1715
  }
1621
- async function performUpload(apiKey, filePayload, controller) {
1622
- const uploadResponse = await fetch(`${endpoints.proxy}/files/encrypted/upload`, {
1623
- method: "POST",
1624
- headers: {
1625
- Authorization: apiKey,
1626
- "Content-Type": "application/json"
1627
- },
1628
- body: JSON.stringify(filePayload),
1629
- signal: controller.signal
1630
- });
1631
- if (!uploadResponse.ok) {
1632
- let errorMessage = `Upload request failed with status ${uploadResponse.status}`;
1633
- try {
1634
- const body = await uploadResponse.json();
1635
- if (body.error) {
1636
- errorMessage = body.error;
1637
- }
1638
- } catch {}
1639
- throw new Error(errorMessage);
1716
+ function anthropicImageBlockToOpenAIPart(part) {
1717
+ const source = part.source;
1718
+ if (!source || typeof source !== "object") {
1719
+ return null;
1640
1720
  }
1641
- const uploadResult = await uploadResponse.json();
1642
- if (uploadResult.status !== 200) {
1643
- throw new Error(uploadResult.error || "Upload failed");
1721
+ const s = source;
1722
+ if (s.type === "base64" && typeof s.data === "string" && s.data.length > 0) {
1723
+ const mediaType = typeof s.media_type === "string" && s.media_type.length > 0 ? s.media_type : "image/png";
1724
+ return {
1725
+ type: "image_url",
1726
+ image_url: { url: `data:${mediaType};base64,${s.data}` }
1727
+ };
1644
1728
  }
1645
- if (!uploadResult.data) {
1646
- throw new Error("Upload response missing data");
1729
+ if (s.type === "url" && typeof s.url === "string" && s.url.length > 0) {
1730
+ return { type: "image_url", image_url: { url: s.url } };
1647
1731
  }
1648
- return uploadResult.data;
1732
+ return null;
1649
1733
  }
1650
- function storeDEKForFile(dekStore, fileId, dek, clientKEK) {
1651
- if (!dekStore.fileDEKs) {
1652
- dekStore.fileDEKs = new Map;
1734
+ function anthropicUserContentToOpenAIMessages(content) {
1735
+ if (typeof content === "string") {
1736
+ return [{ role: "user", content }];
1653
1737
  }
1654
- const _clientKEK = clientKEK ? hexToBytes4(clientKEK) : getClientKEK();
1655
- const wrappedDEK = wrapDEK(_clientKEK, dek);
1656
- dekStore.fileDEKs.set(fileId, wrappedDEK);
1657
- }
1658
- async function uploadFile(apiKey, dekStore, options, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
1659
- validateAPIKey(apiKey);
1660
- validateDEKStore(dekStore);
1661
- validateFileUploadOptions(options);
1662
- TimeoutSchema.parse(timeoutMs);
1663
- const controller = new AbortController;
1664
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
1665
- try {
1666
- const { dek, filePayload } = await prepareEncryptedPayload(dekStore, options, apiKey, clientKEK, timeoutMs);
1667
- const uploadedFile = await performUpload(apiKey, filePayload, controller);
1668
- storeDEKForFile(dekStore, uploadedFile.id, dek, clientKEK);
1669
- return uploadedFile;
1670
- } catch (error) {
1671
- if (error instanceof Error && error.name === "AbortError") {
1672
- throw new Error(`File upload timed out after ${timeoutMs}ms`);
1738
+ const out = [];
1739
+ const partsBuf = [];
1740
+ const flushParts = () => {
1741
+ if (partsBuf.length === 0) {
1742
+ return;
1743
+ }
1744
+ if (partsBuf.length === 1 && partsBuf[0].type === "text") {
1745
+ out.push({ role: "user", content: partsBuf[0].text });
1746
+ } else {
1747
+ out.push({ role: "user", content: [...partsBuf] });
1748
+ }
1749
+ partsBuf.length = 0;
1750
+ };
1751
+ for (const part of content) {
1752
+ if (!part || typeof part !== "object") {
1753
+ throw new AnthropicRequestValidationError("Invalid message content entry.");
1754
+ }
1755
+ if (part.type === "text" && typeof part.text === "string") {
1756
+ partsBuf.push({
1757
+ type: "text",
1758
+ text: part.text
1759
+ });
1760
+ continue;
1761
+ }
1762
+ if (part.type === "image") {
1763
+ const imgPart = anthropicImageBlockToOpenAIPart(part);
1764
+ if (imgPart) {
1765
+ partsBuf.push(imgPart);
1766
+ }
1767
+ continue;
1768
+ }
1769
+ if (part.type === "tool_result") {
1770
+ flushParts();
1771
+ const id = part.tool_use_id;
1772
+ const rawContent = part.content;
1773
+ if (typeof id !== "string" || id.length === 0) {
1774
+ throw new AnthropicRequestValidationError("tool_result blocks require a non-empty tool_use_id.");
1775
+ }
1776
+ out.push({
1777
+ role: "tool",
1778
+ tool_call_id: id,
1779
+ content: toolResultContentToString(rawContent)
1780
+ });
1673
1781
  }
1674
- throw error;
1675
- } finally {
1676
- clearTimeout(timeoutId);
1677
1782
  }
1783
+ flushParts();
1784
+ return out;
1678
1785
  }
1679
- async function listFiles(apiKey, options, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
1680
- validateAPIKey(apiKey);
1681
- validateListFilesOptions(options);
1682
- TimeoutSchema.parse(timeoutMs);
1683
- const controller = new AbortController;
1684
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
1685
- const queryParams = new URLSearchParams;
1686
- if (options?.limit !== undefined) {
1687
- queryParams.append("limit", options.limit.toString());
1786
+ function anthropicAssistantContentToOpenAI(content) {
1787
+ if (typeof content === "string") {
1788
+ return { role: "assistant", content };
1688
1789
  }
1689
- if (options?.offset !== undefined) {
1690
- queryParams.append("offset", options.offset.toString());
1790
+ const textParts = [];
1791
+ const toolCalls = [];
1792
+ for (const part of content) {
1793
+ if (!part || typeof part !== "object") {
1794
+ throw new AnthropicRequestValidationError("Invalid message content entry.");
1795
+ }
1796
+ if (part.type === "text" && typeof part.text === "string") {
1797
+ textParts.push(part.text);
1798
+ continue;
1799
+ }
1800
+ if (part.type === "tool_use") {
1801
+ const p = part;
1802
+ if (typeof p.id !== "string" || p.id.length === 0) {
1803
+ throw new AnthropicRequestValidationError("tool_use blocks require a non-empty id.");
1804
+ }
1805
+ if (typeof p.name !== "string" || p.name.length === 0) {
1806
+ throw new AnthropicRequestValidationError("tool_use blocks require a non-empty name.");
1807
+ }
1808
+ const args = typeof p.input === "string" ? p.input : JSON.stringify(p.input ?? {});
1809
+ toolCalls.push({
1810
+ id: p.id,
1811
+ type: "function",
1812
+ function: { name: p.name, arguments: args }
1813
+ });
1814
+ }
1691
1815
  }
1692
- if (options?.search) {
1693
- queryParams.append("search", options.search);
1816
+ const msg = {
1817
+ role: "assistant",
1818
+ content: textParts.length > 0 ? textParts.join(`
1819
+ `) : null
1820
+ };
1821
+ if (toolCalls.length > 0) {
1822
+ msg.tool_calls = toolCalls;
1694
1823
  }
1695
- if (options?.from) {
1696
- queryParams.append("from", options.from);
1824
+ return msg;
1825
+ }
1826
+ function anthropicToolsToOpenAI(tools) {
1827
+ if (tools === undefined) {
1828
+ return;
1697
1829
  }
1698
- if (options?.to) {
1699
- queryParams.append("to", options.to);
1830
+ if (!Array.isArray(tools)) {
1831
+ throw new AnthropicRequestValidationError("tools must be an array.");
1700
1832
  }
1701
- const queryString = queryParams.toString();
1702
- const url = `${endpoints.proxy}/files/encrypted${queryString ? `?${queryString}` : ""}`;
1703
- try {
1704
- const response = await fetch(url, {
1705
- method: "GET",
1706
- headers: {
1707
- Authorization: apiKey,
1708
- "Content-Type": "application/json"
1709
- },
1710
- signal: controller.signal
1711
- });
1712
- if (!response.ok) {
1713
- throw new Error(`List files request failed with status ${response.status}`);
1714
- }
1715
- const result = await response.json();
1716
- if (result.status !== 200) {
1717
- throw new Error(result.error || "List files failed");
1833
+ const out = [];
1834
+ for (const t of tools) {
1835
+ if (!t || typeof t !== "object") {
1836
+ throw new AnthropicRequestValidationError("Invalid tool entry.");
1718
1837
  }
1719
- if (!result.data) {
1720
- throw new Error("List files response missing data");
1838
+ const name = t.name;
1839
+ const desc = t.description;
1840
+ const schema = t.input_schema;
1841
+ if (typeof name !== "string" || name.length === 0) {
1842
+ throw new AnthropicRequestValidationError("Each tool must include a non-empty name.");
1721
1843
  }
1722
- return result.data;
1723
- } catch (error) {
1724
- if (error instanceof Error && error.name === "AbortError") {
1725
- throw new Error(`List files request timed out after ${timeoutMs}ms`);
1844
+ if (schema !== undefined && (typeof schema !== "object" || schema === null)) {
1845
+ throw new AnthropicRequestValidationError("tool input_schema must be an object when provided.");
1726
1846
  }
1727
- throw error;
1728
- } finally {
1729
- clearTimeout(timeoutId);
1847
+ out.push({
1848
+ type: "function",
1849
+ function: {
1850
+ name,
1851
+ ...typeof desc === "string" ? { description: desc } : {},
1852
+ parameters: schema ?? {
1853
+ type: "object",
1854
+ properties: {}
1855
+ }
1856
+ }
1857
+ });
1730
1858
  }
1859
+ return out;
1731
1860
  }
1732
- async function getFile(apiKey, options, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
1733
- validateAPIKey(apiKey);
1734
- validateGetFileOptions(options);
1735
- TimeoutSchema.parse(timeoutMs);
1736
- const controller = new AbortController;
1737
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
1738
- const queryParams = new URLSearchParams;
1739
- if (options.url !== undefined) {
1740
- queryParams.append("url", options.url ? "true" : "false");
1861
+ function anthropicToolChoiceToOpenAI(toolChoice) {
1862
+ if (toolChoice === undefined) {
1863
+ return;
1741
1864
  }
1742
- const queryString = queryParams.toString();
1743
- const url = `${endpoints.proxy}/files/encrypted/${options.id}${queryString ? `?${queryString}` : ""}`;
1744
- try {
1745
- const response = await fetch(url, {
1746
- method: "GET",
1747
- headers: {
1748
- Authorization: apiKey,
1749
- "Content-Type": "application/json"
1750
- },
1751
- signal: controller.signal
1752
- });
1753
- if (!response.ok) {
1754
- if (response.status === 404) {
1755
- throw new Error(`File not found: ${options.id}`);
1865
+ if (typeof toolChoice !== "object" || toolChoice === null || !("type" in toolChoice)) {
1866
+ throw new AnthropicRequestValidationError("Invalid tool_choice shape.");
1867
+ }
1868
+ const tc = toolChoice;
1869
+ switch (tc.type) {
1870
+ case "auto":
1871
+ return "auto";
1872
+ case "none":
1873
+ return "none";
1874
+ case "any":
1875
+ return "required";
1876
+ case "tool": {
1877
+ if (typeof tc.name !== "string" || tc.name.length === 0) {
1878
+ throw new AnthropicRequestValidationError('tool_choice type "tool" requires a non-empty name.');
1756
1879
  }
1757
- throw new Error(`Get file request failed with status ${response.status}`);
1880
+ return { type: "function", function: { name: tc.name } };
1881
+ }
1882
+ default:
1883
+ throw new AnthropicRequestValidationError(`Unsupported tool_choice type "${tc.type}".`);
1884
+ }
1885
+ }
1886
+ function anthropicMessagesCreateToOpenAI(body) {
1887
+ if (typeof body.model !== "string" || !body.model) {
1888
+ throw new AnthropicRequestValidationError("model is required.");
1889
+ }
1890
+ if (typeof body.max_tokens !== "number" || !Number.isFinite(body.max_tokens)) {
1891
+ throw new AnthropicRequestValidationError("max_tokens is required and must be a number.");
1892
+ }
1893
+ if (!Array.isArray(body.messages)) {
1894
+ throw new AnthropicRequestValidationError("messages must be an array.");
1895
+ }
1896
+ const messages = [];
1897
+ if (body.system !== undefined) {
1898
+ messages.push(...systemToOpenAiMessages(body.system));
1899
+ }
1900
+ for (const m of body.messages) {
1901
+ if (m.role === "system") {
1902
+ messages.push(...systemToOpenAiMessages(m.content));
1903
+ continue;
1758
1904
  }
1759
- const result = await response.json();
1760
- if (result.status !== 200) {
1761
- throw new Error(result.error || "Get file failed");
1905
+ if (m.role !== "user" && m.role !== "assistant") {
1906
+ throw new AnthropicRequestValidationError(`Invalid message role "${m.role}".`);
1762
1907
  }
1763
- if (!result.data) {
1764
- throw new Error("Get file response missing data");
1908
+ if (m.role === "user") {
1909
+ messages.push(...anthropicUserContentToOpenAIMessages(m.content));
1910
+ } else {
1911
+ messages.push(anthropicAssistantContentToOpenAI(m.content));
1765
1912
  }
1766
- return result.data;
1767
- } catch (error) {
1768
- if (error instanceof Error && error.name === "AbortError") {
1769
- throw new Error(`Get file request timed out after ${timeoutMs}ms`);
1913
+ }
1914
+ const isStreaming = Boolean(body.stream);
1915
+ const params = {
1916
+ model: body.model,
1917
+ messages,
1918
+ max_tokens: body.max_tokens,
1919
+ stream: isStreaming
1920
+ };
1921
+ if (isStreaming) {
1922
+ params.stream_options = { include_usage: true };
1923
+ }
1924
+ const tools = anthropicToolsToOpenAI(body.tools);
1925
+ if (tools !== undefined && tools.length > 0) {
1926
+ params.tools = tools;
1927
+ }
1928
+ const toolChoice = anthropicToolChoiceToOpenAI(body.tool_choice);
1929
+ if (toolChoice !== undefined) {
1930
+ params.tool_choice = toolChoice;
1931
+ }
1932
+ if (body.stop_sequences !== undefined) {
1933
+ if (!Array.isArray(body.stop_sequences) || !body.stop_sequences.every((s) => typeof s === "string")) {
1934
+ throw new AnthropicRequestValidationError("stop_sequences must be an array of strings.");
1770
1935
  }
1771
- throw error;
1772
- } finally {
1773
- clearTimeout(timeoutId);
1936
+ params.stop = body.stop_sequences;
1774
1937
  }
1938
+ if (typeof body.temperature === "number") {
1939
+ params.temperature = body.temperature;
1940
+ }
1941
+ if (typeof body.top_p === "number") {
1942
+ params.top_p = body.top_p;
1943
+ }
1944
+ if (typeof body.top_k === "number") {
1945
+ console.warn("[proxy] top_k is not supported by the OpenAI API and will be ignored.");
1946
+ }
1947
+ return params;
1775
1948
  }
1776
- async function deleteFile(apiKey, options, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
1777
- validateAPIKey(apiKey);
1778
- DeleteFileOptionsSchema.parse(options);
1779
- TimeoutSchema.parse(timeoutMs);
1780
- const controller = new AbortController;
1781
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
1782
- try {
1783
- const response = await fetch(`${endpoints.proxy}/files/encrypted/${options.id}`, {
1784
- method: "DELETE",
1785
- headers: {
1786
- Authorization: apiKey,
1787
- "Content-Type": "application/json"
1788
- },
1789
- signal: controller.signal
1790
- });
1791
- if (!response.ok) {
1792
- if (response.status === 404) {
1793
- throw new Error(`File not found: ${options.id}`);
1949
+
1950
+ // src/anthropic/count-tokens-route.ts
1951
+ function extractTextCharCount(body) {
1952
+ let len = 0;
1953
+ if (typeof body.system === "string") {
1954
+ len += body.system.length;
1955
+ } else if (Array.isArray(body.system)) {
1956
+ for (const block of body.system) {
1957
+ if (block && block.type === "text" && typeof block.text === "string") {
1958
+ len += block.text.length;
1794
1959
  }
1795
- throw new Error(`Delete file request failed with status ${response.status}`);
1796
1960
  }
1797
- await response.json();
1798
- } catch (error) {
1799
- if (error instanceof Error && error.name === "AbortError") {
1800
- throw new Error(`Delete file request timed out after ${timeoutMs}ms`);
1961
+ } else if (body.system && typeof body.system === "object" && body.system.type === "text") {
1962
+ len += body.system.text.length;
1963
+ }
1964
+ for (const msg of body.messages) {
1965
+ if (typeof msg.content === "string") {
1966
+ len += msg.content.length;
1967
+ } else if (Array.isArray(msg.content)) {
1968
+ for (const part of msg.content) {
1969
+ if (!part || typeof part !== "object")
1970
+ continue;
1971
+ if (part.type === "text" && typeof part.text === "string") {
1972
+ len += part.text.length;
1973
+ } else if (part.type === "tool_result") {
1974
+ const c = part.content;
1975
+ if (typeof c === "string") {
1976
+ len += c.length;
1977
+ }
1978
+ }
1979
+ }
1801
1980
  }
1802
- throw error;
1803
- } finally {
1804
- clearTimeout(timeoutId);
1805
1981
  }
1806
- }
1807
- async function indexFiles(apiKey, dekStore, options, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
1808
- validateAPIKey(apiKey);
1809
- validateDEKStore(dekStore);
1810
- IndexFilesOptionsSchema.parse(options);
1811
- TimeoutSchema.parse(timeoutMs);
1812
- const wrappedRagDEK = options.ragDEK || dekStore.ragDEK;
1813
- if (!wrappedRagDEK) {
1814
- throw new Error("RAG DEK not found. Provide ragDEK in options or upload at least one file with ragIndex: true.");
1982
+ if (Array.isArray(body.tools)) {
1983
+ len += JSON.stringify(body.tools).length;
1815
1984
  }
1816
- const _clientKEK = clientKEK ? hexToBytes4(clientKEK) : getClientKEK();
1817
- const ragDEK = unwrapDEK(_clientKEK, wrappedRagDEK);
1818
- const enclavePublicKey = await getEnclavePublicKey(timeoutMs);
1819
- const { cipherText, sharedSecret } = createMLKEMEncapsulation(enclavePublicKey);
1820
- const encryptedFiles = options.files.map((file) => {
1821
- const wrappedFileDEK = file.fileDEK || dekStore.fileDEKs?.get(file.fileId);
1822
- if (!wrappedFileDEK) {
1823
- throw new Error(`File DEK not found for file: ${file.fileId}. Provide fileDEK or ensure file was uploaded with this DEK store.`);
1985
+ return len;
1986
+ }
1987
+ function registerAnthropicCountTokensRoute(router, _deps) {
1988
+ router.post("/v1/messages/count_tokens", async (req, res) => {
1989
+ const requestId = newAnthropicRequestId();
1990
+ res.setHeader("request-id", requestId);
1991
+ const versionResult = resolveAnthropicVersion(req);
1992
+ if (!versionResult.ok) {
1993
+ return sendAnthropicHttpError(res, 400, "invalid_request_error", versionResult.message, requestId);
1824
1994
  }
1825
- const fileDEK = unwrapDEK(_clientKEK, wrappedFileDEK);
1826
- const { encrypted: encryptedFileDEK, nonce: fileNonce } = encryptPayload(sharedSecret, fileDEK);
1827
- const { encrypted: encryptedRagDEK, nonce: ragDEKNonce } = encryptPayload(sharedSecret, ragDEK);
1828
- return {
1829
- file_id: file.fileId,
1830
- encrypted_file_dek: bytesToHex5(encryptedFileDEK),
1831
- encrypted_rag_dek: bytesToHex5(encryptedRagDEK),
1832
- file_nonce: bytesToHex5(fileNonce),
1833
- rag_dek_nonce: bytesToHex5(ragDEKNonce),
1834
- s3_r2_path: file.filePath,
1835
- cipher_text: bytesToHex5(cipherText)
1836
- };
1837
- });
1838
- const controller = new AbortController;
1839
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
1840
- try {
1841
- const response = await fetch(`${endpoints.proxy}/files/encrypted/index`, {
1842
- method: "POST",
1843
- headers: {
1844
- Authorization: apiKey,
1845
- "Content-Type": "application/json"
1846
- },
1847
- body: JSON.stringify({ files: encryptedFiles }),
1848
- signal: controller.signal
1849
- });
1850
- if (!response.ok) {
1851
- throw new Error(`Index files request failed with status ${response.status}`);
1995
+ const apiKey = extractAnthropicApiKey(req);
1996
+ if (!apiKey) {
1997
+ return sendAnthropicHttpError(res, 401, "authentication_error", "Missing x-api-key header (or Authorization with API key).", requestId);
1852
1998
  }
1853
- const result = await response.json();
1854
- return result;
1855
- } catch (error) {
1856
- if (error instanceof Error && error.name === "AbortError") {
1857
- throw new Error(`Index files request timed out after ${timeoutMs}ms`);
1999
+ try {
2000
+ const raw = req.body;
2001
+ const body = {
2002
+ ...raw,
2003
+ max_tokens: typeof raw.max_tokens === "number" && Number.isFinite(raw.max_tokens) ? raw.max_tokens : 4096,
2004
+ stream: false
2005
+ };
2006
+ anthropicMessagesCreateToOpenAI(body);
2007
+ const input_tokens = Math.max(1, Math.ceil(extractTextCharCount(body) / 4));
2008
+ res.json({ input_tokens });
2009
+ } catch (err) {
2010
+ if (err instanceof AnthropicRequestValidationError) {
2011
+ return sendAnthropicHttpError(res, err.status, err.anthropicType, err.message, requestId);
2012
+ }
2013
+ mapUnknownErrorToAnthropicResponse(err, res, requestId);
1858
2014
  }
1859
- throw error;
1860
- } finally {
1861
- clearTimeout(timeoutId);
1862
- }
2015
+ });
1863
2016
  }
1864
- async function deleteIndex(apiKey, dekStore, options, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
1865
- validateAPIKey(apiKey);
1866
- validateDEKStore(dekStore);
1867
- DeleteIndexOptionsSchema.parse(options);
1868
- TimeoutSchema.parse(timeoutMs);
1869
- const wrappedRagDEK = options.ragDEK || dekStore.ragDEK;
1870
- if (!wrappedRagDEK) {
1871
- throw new Error("RAG DEK not found. Provide ragDEK in options or ensure dekStore has a ragDEK.");
2017
+
2018
+ // src/anthropic/from-openai.ts
2019
+ function openAiFinishReasonToAnthropic(finish) {
2020
+ if (!finish) {
2021
+ return { stop_reason: null, stop_sequence: null };
1872
2022
  }
1873
- const _clientKEK = clientKEK ? hexToBytes4(clientKEK) : getClientKEK();
1874
- const ragDEK = unwrapDEK(_clientKEK, wrappedRagDEK);
1875
- const enclavePublicKey = await getEnclavePublicKey(timeoutMs);
1876
- const { cipherText, sharedSecret } = createMLKEMEncapsulation(enclavePublicKey);
1877
- const { encrypted: encryptedRagDEK, nonce: ragDEKNonce } = encryptPayload(sharedSecret, ragDEK);
1878
- const controller = new AbortController;
1879
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
1880
- try {
1881
- const response = await fetch(`${endpoints.proxy}/files/encrypted/delete-index`, {
1882
- method: "POST",
1883
- headers: {
1884
- Authorization: apiKey,
1885
- "Content-Type": "application/json"
1886
- },
1887
- body: JSON.stringify({
1888
- cipher_text: bytesToHex5(cipherText),
1889
- encrypted_rag_dek: bytesToHex5(encryptedRagDEK),
1890
- rag_dek_nonce: bytesToHex5(ragDEKNonce),
1891
- fileIds: options.fileIds
1892
- }),
1893
- signal: controller.signal
1894
- });
1895
- if (!response.ok) {
1896
- throw new Error(`Delete index request failed with status ${response.status}`);
1897
- }
1898
- const result = await response.json();
1899
- return result;
1900
- } catch (error) {
1901
- if (error instanceof Error && error.name === "AbortError") {
1902
- throw new Error(`Delete index request timed out after ${timeoutMs}ms`);
1903
- }
1904
- throw error;
1905
- } finally {
1906
- clearTimeout(timeoutId);
2023
+ switch (finish) {
2024
+ case "stop":
2025
+ return { stop_reason: "end_turn", stop_sequence: null };
2026
+ case "length":
2027
+ return { stop_reason: "max_tokens", stop_sequence: null };
2028
+ case "tool_calls":
2029
+ return { stop_reason: "tool_use", stop_sequence: null };
2030
+ case "content_filter":
2031
+ return { stop_reason: "refusal", stop_sequence: null };
2032
+ default:
2033
+ return { stop_reason: "end_turn", stop_sequence: null };
1907
2034
  }
1908
2035
  }
1909
- function createFilesClient(apiKey, dekStore, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
1910
- return {
1911
- upload: (options) => uploadFile(apiKey, dekStore, options, clientKEK, timeoutMs),
1912
- list: (options) => listFiles(apiKey, options, timeoutMs),
1913
- get: (options) => getFile(apiKey, options, timeoutMs),
1914
- delete: (options) => deleteFile(apiKey, options, timeoutMs),
1915
- index: (options) => indexFiles(apiKey, dekStore, options, clientKEK, timeoutMs),
1916
- deleteIndex: (options) => deleteIndex(apiKey, dekStore, options, clientKEK, timeoutMs)
1917
- };
1918
- }
1919
-
1920
- // src/models/index.ts
1921
- async function listModels(params, apiKey, timeoutMs) {
1922
- const controller = new AbortController;
1923
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
1924
- const queryParams = new URLSearchParams;
1925
- if (params?.type !== undefined) {
1926
- queryParams.append("type", params.type);
2036
+ function extractTextFromAssistantContent(content) {
2037
+ if (content == null) {
2038
+ return "";
1927
2039
  }
1928
- const queryString = queryParams.toString();
1929
- const url = `${endpoints.proxy}/models${queryString ? `?${queryString}` : ""}`;
1930
- try {
1931
- const response = await fetch(url, {
1932
- method: "GET",
1933
- headers: {
1934
- Authorization: apiKey,
1935
- "Content-Type": "application/json"
1936
- },
1937
- signal: controller.signal
1938
- });
1939
- if (!response.ok) {
1940
- throw new Error(`List models request failed with status ${response.status}`);
1941
- }
1942
- const result = await response.json();
1943
- if (result.status !== 200) {
1944
- throw new Error(result.error || "List models failed");
1945
- }
1946
- if (!result.data) {
1947
- throw new Error("List models response missing data");
2040
+ if (typeof content === "string") {
2041
+ return content;
2042
+ }
2043
+ if (!Array.isArray(content)) {
2044
+ return "";
2045
+ }
2046
+ const parts = [];
2047
+ for (const p of content) {
2048
+ if (typeof p === "string") {
2049
+ parts.push(p);
2050
+ continue;
1948
2051
  }
1949
- return result.data;
1950
- } catch (error) {
1951
- if (error instanceof Error && error.name === "AbortError") {
1952
- throw new Error(`List models request timed out after ${timeoutMs}ms`);
2052
+ if (p && typeof p === "object" && "type" in p && p.type === "text" && "text" in p) {
2053
+ parts.push(String(p.text));
1953
2054
  }
1954
- throw error;
1955
- } finally {
1956
- clearTimeout(timeoutId);
1957
2055
  }
2056
+ return parts.join("");
1958
2057
  }
1959
- function createModelsClient(apiKey, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
1960
- return {
1961
- list: (params) => listModels(params, apiKey, timeoutMs)
2058
+ function openAIChatCompletionToAnthropicMessage(completion, requestModel) {
2059
+ const choice = completion.choices[0];
2060
+ const message = choice?.message;
2061
+ const contentText = message ? extractTextFromAssistantContent(message.content) : "";
2062
+ const content = [];
2063
+ if (contentText.length > 0) {
2064
+ content.push({ type: "text", text: contentText });
2065
+ }
2066
+ if (message?.tool_calls?.length) {
2067
+ for (const tc of message.tool_calls) {
2068
+ if (tc.type !== "function") {
2069
+ continue;
2070
+ }
2071
+ let input = {};
2072
+ try {
2073
+ input = JSON.parse(tc.function.arguments || "{}");
2074
+ } catch {
2075
+ input = { _raw_arguments: tc.function.arguments ?? "" };
2076
+ }
2077
+ content.push({
2078
+ type: "tool_use",
2079
+ id: tc.id,
2080
+ name: tc.function.name,
2081
+ input
2082
+ });
2083
+ }
2084
+ }
2085
+ if (content.length === 0) {
2086
+ content.push({ type: "text", text: "" });
2087
+ }
2088
+ const { stop_reason, stop_sequence } = openAiFinishReasonToAnthropic(choice?.finish_reason);
2089
+ const u = completion.usage;
2090
+ const usage = {
2091
+ input_tokens: u?.prompt_tokens ?? 0,
2092
+ output_tokens: u?.completion_tokens ?? 0
1962
2093
  };
1963
- }
1964
-
1965
- // src/rvenc/index.ts
1966
- import { bytesToHex as bytesToHex6, hexToBytes as hexToBytes5 } from "@noble/ciphers/utils.js";
1967
- import OpenAI from "openai";
1968
- function preprocessRequest(body, encryptionKeys) {
1969
- const { cipherText, sharedSecret } = encryptionKeys;
1970
- const { encrypted, nonce } = encryptPayload(sharedSecret, body);
1971
2094
  return {
1972
- body: {
1973
- cipherText: bytesToHex6(cipherText),
1974
- encryptedInference: bytesToHex6(encrypted),
1975
- nonce: bytesToHex6(nonce),
1976
- model: body.model,
1977
- stream: body.stream === true
1978
- },
1979
- sharedSecret,
1980
- nonce
2095
+ id: newAnthropicMessageId(),
2096
+ type: "message",
2097
+ role: "assistant",
2098
+ content,
2099
+ model: requestModel,
2100
+ stop_reason,
2101
+ stop_sequence,
2102
+ usage
1981
2103
  };
1982
2104
  }
1983
- async function postprocessStreamingResponse(response, sharedSecret, nonce, maxBufferSize) {
1984
- if (!response.body) {
1985
- throw new Error("Response body is null");
2105
+ function chunkFinishToAnthropic(finish) {
2106
+ if (!finish) {
2107
+ return null;
1986
2108
  }
1987
- const reader = response.body.getReader();
1988
- const generator = createDecryptedStreamGenerator(reader, sharedSecret, nonce, maxBufferSize);
1989
- return {
1990
- [Symbol.asyncIterator]() {
1991
- return generator;
2109
+ return openAiFinishReasonToAnthropic(finish).stop_reason;
2110
+ }
2111
+ async function pipeOpenAIChunkStreamToAnthropicSse(res, stream, options) {
2112
+ const { anthropicModel, messageId } = options;
2113
+ let textBlockOpen = false;
2114
+ let inputTokens = 0;
2115
+ let outputTokens = 0;
2116
+ let stopReason = null;
2117
+ const toolStates = new Map;
2118
+ let nextAnthropicIndex = 0;
2119
+ let textBlockIndex = null;
2120
+ writeAnthropicSseEvent(res, "message_start", {
2121
+ type: "message_start",
2122
+ message: {
2123
+ id: messageId,
2124
+ type: "message",
2125
+ role: "assistant",
2126
+ content: [],
2127
+ model: anthropicModel,
2128
+ stop_reason: null,
2129
+ stop_sequence: null,
2130
+ usage: { input_tokens: inputTokens, output_tokens: outputTokens }
1992
2131
  }
2132
+ });
2133
+ const ensureTextBlock = () => {
2134
+ if (textBlockOpen) {
2135
+ return;
2136
+ }
2137
+ textBlockIndex = nextAnthropicIndex++;
2138
+ textBlockOpen = true;
2139
+ writeAnthropicSseEvent(res, "content_block_start", {
2140
+ type: "content_block_start",
2141
+ index: textBlockIndex,
2142
+ content_block: { type: "text", text: "" }
2143
+ });
1993
2144
  };
1994
- }
1995
- async function postprocessNonStreamingResponse(response, sharedSecret) {
1996
- const data = await response.json();
1997
- if (!data.encryptedResponse || !data.nonce) {
1998
- throw new Error("Invalid non-streaming response: missing encryptedResponse or nonce");
1999
- }
2000
- const responseNonce = hexToBytes5(data.nonce);
2001
- return decryptPayload(data.encryptedResponse, sharedSecret, responseNonce);
2002
- }
2003
- function createRvencChatClient(apiKey, encryptionKeys, requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, maxBufferSize = DEFAULT_MAX_BUFFER_SIZE, attest2 = true, OpenAIClientParams) {
2004
- const client = new OpenAI({ apiKey: "not-used", ...OpenAIClientParams });
2005
- const originalChatCreate = client.chat.completions.create.bind(client.chat.completions);
2006
- client.chat.completions.create = async (body) => {
2007
- const isStreaming = body.stream === true;
2008
- const controller = new AbortController;
2009
- const timeoutId = setTimeout(() => controller.abort(), requestTimeoutMs);
2010
- try {
2011
- const sessionId = await attest(apiKey, { model: body.model, enabled: attest2 });
2012
- const encryptedRequest = preprocessRequest(body, encryptionKeys);
2013
- const response = await fetch(`${endpoints.proxy}/rvenc/chat/completions`, {
2014
- method: "POST",
2015
- headers: {
2016
- "Content-Type": "application/json",
2017
- Accept: isStreaming ? "text/event-stream" : "application/json",
2018
- Authorization: apiKey,
2019
- ...sessionId && { "X-Session-Id": sessionId }
2020
- },
2021
- body: JSON.stringify(encryptedRequest.body),
2022
- signal: controller.signal
2023
- });
2024
- if (!response.ok) {
2025
- await throwIfErrorResponse(response);
2026
- }
2027
- clearTimeout(timeoutId);
2028
- if (isStreaming) {
2029
- const contentType = response.headers.get("content-type") ?? "";
2030
- if (contentType.includes("text/event-stream")) {
2031
- return await postprocessStreamingResponse(response, encryptedRequest.sharedSecret, encryptedRequest.nonce, maxBufferSize);
2032
- }
2033
- const completion = await postprocessNonStreamingResponse(response, encryptedRequest.sharedSecret);
2034
- return completionToChunkStream(completion);
2035
- }
2036
- return await postprocessNonStreamingResponse(response, encryptedRequest.sharedSecret);
2037
- } catch (error) {
2038
- clearTimeout(timeoutId);
2039
- if (error instanceof Error && error.name === "AbortError") {
2040
- throw new Error(`Request timed out after ${requestTimeoutMs}ms`);
2041
- }
2042
- throw error;
2145
+ const closeTextBlockIfOpen = () => {
2146
+ if (!textBlockOpen || textBlockIndex === null) {
2147
+ return;
2043
2148
  }
2149
+ writeAnthropicSseEvent(res, "content_block_stop", {
2150
+ type: "content_block_stop",
2151
+ index: textBlockIndex
2152
+ });
2153
+ textBlockOpen = false;
2044
2154
  };
2045
- return client;
2046
- }
2047
- async function* completionToChunkStream(completion) {
2048
- const choice = completion.choices[0];
2049
- const message = choice?.message;
2050
- const content = typeof message?.content === "string" ? message.content : "";
2051
- const toolCalls = message?.tool_calls?.filter((tc) => tc.type === "function").map((tc, i) => ({
2052
- index: i,
2053
- id: tc.id,
2054
- type: "function",
2055
- function: {
2056
- name: tc.function.name,
2057
- arguments: tc.function.arguments
2155
+ const getOrCreateTool = (openAiIdx) => {
2156
+ let st = toolStates.get(openAiIdx);
2157
+ if (!st) {
2158
+ st = {
2159
+ anthropicIndex: nextAnthropicIndex++,
2160
+ id: "",
2161
+ name: "",
2162
+ lastArgs: "",
2163
+ argsEmittedLen: 0,
2164
+ started: false,
2165
+ stopped: false
2166
+ };
2167
+ toolStates.set(openAiIdx, st);
2058
2168
  }
2059
- }));
2060
- yield {
2061
- id: completion.id,
2062
- object: "chat.completion.chunk",
2063
- created: completion.created,
2064
- model: completion.model,
2065
- choices: [
2066
- {
2067
- index: choice?.index ?? 0,
2068
- delta: {
2069
- role: "assistant",
2070
- content,
2071
- ...toolCalls && toolCalls.length > 0 && { tool_calls: toolCalls }
2072
- },
2073
- finish_reason: choice?.finish_reason ?? "stop",
2074
- logprobs: null
2169
+ return st;
2170
+ };
2171
+ const flushToolArgs = (st) => {
2172
+ if (!st.started || st.lastArgs.length <= st.argsEmittedLen) {
2173
+ return;
2174
+ }
2175
+ const partial = st.lastArgs.slice(st.argsEmittedLen);
2176
+ st.argsEmittedLen = st.lastArgs.length;
2177
+ writeAnthropicSseEvent(res, "content_block_delta", {
2178
+ type: "content_block_delta",
2179
+ index: st.anthropicIndex,
2180
+ delta: {
2181
+ type: "input_json_delta",
2182
+ partial_json: partial
2075
2183
  }
2076
- ],
2077
- usage: completion.usage ?? null
2184
+ });
2078
2185
  };
2079
- }
2080
- async function* createDecryptedStreamGenerator(reader, sharedSecret, nonce, maxBufferSize) {
2081
- const decoder = new TextDecoder;
2082
- let buffer = "";
2083
2186
  try {
2084
- while (true) {
2085
- const { value, done } = await reader.read();
2086
- if (done)
2087
- break;
2088
- buffer += decoder.decode(value, { stream: true });
2089
- if (buffer.length > maxBufferSize) {
2090
- throw new Error(`Stream buffer exceeded maximum size of ${maxBufferSize} bytes`);
2187
+ for await (const chunk of stream) {
2188
+ if (chunk.usage) {
2189
+ const u = chunk.usage;
2190
+ inputTokens = u.prompt_tokens ?? inputTokens;
2191
+ outputTokens = u.completion_tokens ?? outputTokens;
2091
2192
  }
2092
- const parts = buffer.split(`
2093
-
2094
- `);
2095
- for (let i = 0;i < parts.length - 1; i++) {
2096
- const part = parts[i];
2097
- const lines = part.split(`
2098
- `);
2099
- let event;
2100
- let data;
2101
- if (lines[0]) {
2102
- const eventSplit = lines[0].split(": ");
2103
- event = eventSplit[1];
2104
- }
2105
- if (lines[1]) {
2106
- const dataSplit = lines[1].split(": ");
2107
- data = dataSplit.slice(1).join(": ");
2108
- }
2109
- if (event === "done" && data === "[DONE]") {
2110
- return;
2111
- }
2112
- if (event === "error") {
2113
- const errorObj = JSON.parse(data || "{}");
2114
- throw new Error(errorObj.error?.message || data || "Stream error");
2193
+ const choice = chunk.choices?.[0];
2194
+ if (!choice) {
2195
+ continue;
2196
+ }
2197
+ const delta = choice.delta;
2198
+ if (typeof delta?.content === "string" && delta.content.length > 0) {
2199
+ ensureTextBlock();
2200
+ if (textBlockIndex !== null) {
2201
+ writeAnthropicSseEvent(res, "content_block_delta", {
2202
+ type: "content_block_delta",
2203
+ index: textBlockIndex,
2204
+ delta: { type: "text_delta", text: delta.content }
2205
+ });
2115
2206
  }
2116
- if (event === "data" && data && data !== "[DONE]") {
2117
- const chunk = decryptPayload(data, sharedSecret, nonce);
2118
- if (chunk.error) {
2119
- throw new Error(chunk.error.message || "Stream error");
2207
+ }
2208
+ if (delta?.tool_calls?.length) {
2209
+ closeTextBlockIfOpen();
2210
+ for (const tc of delta.tool_calls) {
2211
+ const idx = typeof tc.index === "number" && Number.isFinite(tc.index) ? tc.index : 0;
2212
+ const st = getOrCreateTool(idx);
2213
+ if (typeof tc.id === "string" && tc.id.length > 0) {
2214
+ st.id = tc.id;
2215
+ }
2216
+ const fn = tc.function;
2217
+ if (fn?.name && fn.name.length > 0) {
2218
+ st.name = fn.name;
2219
+ }
2220
+ if (typeof fn?.arguments === "string") {
2221
+ st.lastArgs += fn.arguments;
2222
+ }
2223
+ if (!st.started && st.id.length > 0 && st.name.length > 0) {
2224
+ writeAnthropicSseEvent(res, "content_block_start", {
2225
+ type: "content_block_start",
2226
+ index: st.anthropicIndex,
2227
+ content_block: {
2228
+ type: "tool_use",
2229
+ id: st.id,
2230
+ name: st.name
2231
+ }
2232
+ });
2233
+ st.started = true;
2234
+ }
2235
+ if (st.started) {
2236
+ flushToolArgs(st);
2120
2237
  }
2121
- yield chunk;
2122
2238
  }
2123
2239
  }
2124
- buffer = parts[parts.length - 1];
2125
- }
2126
- } finally {
2127
- reader.releaseLock();
2128
- }
2129
- }
2130
-
2131
- // src/tools/index.ts
2132
- import { bytesToHex as bytesToHex7, hexToBytes as hexToBytes6, randomBytes as randomBytes5 } from "@noble/ciphers/utils.js";
2133
- var FILE_OUTPUT_TOOLS = ["generateImage", "audioGenerateFromText", "createFileForUser"];
2134
- var FILE_INPUT_TOOLS = [
2135
- "imageDescribeAndCaption",
2136
- "imageDescribeAndCaptionFallback",
2137
- "videoDescribeAndCaption",
2138
- "getPDFContent",
2139
- "getTextDocumentContent",
2140
- "transcribeAudioToText",
2141
- "transcribeAudioWithDiarization",
2142
- "audioDiarization",
2143
- "getSpreadsheetContent",
2144
- "getPowerPointContent",
2145
- "getDataFileContent",
2146
- "getFileContentOCR"
2147
- ];
2148
- var RAG_TOOLS = ["searchRag"];
2149
- async function callToolRequest(toolName, body, apiKey, timeoutMs, attest2) {
2150
- const controller = new AbortController;
2151
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
2152
- try {
2153
- const response = await fetch(`${endpoints.proxy}/tools/${toolName}`, {
2154
- method: "POST",
2155
- headers: {
2156
- "Content-Type": "application/json",
2157
- Authorization: apiKey
2158
- },
2159
- body: JSON.stringify(body),
2160
- signal: controller.signal
2161
- });
2162
- clearTimeout(timeoutId);
2163
- if (!response.ok) {
2164
- await throwIfErrorResponse(response);
2240
+ if (choice.finish_reason) {
2241
+ const mapped = chunkFinishToAnthropic(choice.finish_reason);
2242
+ if (mapped) {
2243
+ stopReason = mapped;
2244
+ }
2245
+ }
2165
2246
  }
2166
- const data = await response.json();
2167
- return data.data;
2168
- } catch (error) {
2169
- clearTimeout(timeoutId);
2170
- if (error instanceof Error && error.name === "AbortError") {
2171
- throw new Error(`Tool request timed out after ${timeoutMs}ms`);
2247
+ closeTextBlockIfOpen();
2248
+ const sortedTools = [...toolStates.values()].sort((a, b) => a.anthropicIndex - b.anthropicIndex);
2249
+ for (const st of sortedTools) {
2250
+ if (st.started && !st.stopped) {
2251
+ writeAnthropicSseEvent(res, "content_block_stop", {
2252
+ type: "content_block_stop",
2253
+ index: st.anthropicIndex
2254
+ });
2255
+ st.stopped = true;
2256
+ }
2172
2257
  }
2173
- throw new Error(`Tool request failed: ${error instanceof Error ? error.message : error}`);
2258
+ writeAnthropicSseEvent(res, "message_delta", {
2259
+ type: "message_delta",
2260
+ delta: { stop_reason: stopReason, stop_sequence: null },
2261
+ usage: {
2262
+ input_tokens: inputTokens,
2263
+ output_tokens: outputTokens
2264
+ }
2265
+ });
2266
+ writeAnthropicSseEvent(res, "message_stop", { type: "message_stop" });
2267
+ res.end();
2268
+ } catch (err) {
2269
+ const message = err instanceof Error ? err.message : "Stream error";
2270
+ writeAnthropicSseEvent(res, "error", {
2271
+ type: "error",
2272
+ error: { type: "api_error", message }
2273
+ });
2274
+ res.end();
2174
2275
  }
2175
2276
  }
2176
- async function downloadEncryptedFile(fileId, apiKey, timeoutMs) {
2177
- const controller = new AbortController;
2178
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
2179
- try {
2180
- const metadataResponse = await fetch(`${endpoints.proxy}/files/encrypted/${fileId}?url=true`, {
2181
- headers: { Authorization: apiKey },
2182
- signal: controller.signal
2183
- });
2184
- if (!metadataResponse.ok) {
2185
- throw new Error(`Failed to get file metadata: ${metadataResponse.status}`);
2186
- }
2187
- const metadata = await metadataResponse.json();
2188
- const downloadUrl = metadata.data?.url;
2189
- if (!downloadUrl) {
2190
- throw new Error("No download URL in response");
2191
- }
2192
- const fileResponse = await fetch(downloadUrl, { signal: controller.signal });
2193
- if (!fileResponse.ok) {
2194
- throw new Error(`Failed to download file: ${fileResponse.status}`);
2277
+
2278
+ // src/anthropic/messages-route.ts
2279
+ function registerAnthropicMessagesRoute(router, deps) {
2280
+ router.post("/v1/messages", async (req, res) => {
2281
+ const requestId = newAnthropicRequestId();
2282
+ res.setHeader("request-id", requestId);
2283
+ const versionResult = resolveAnthropicVersion(req);
2284
+ if (!versionResult.ok) {
2285
+ return sendAnthropicHttpError(res, 400, "invalid_request_error", versionResult.message, requestId);
2195
2286
  }
2196
- clearTimeout(timeoutId);
2197
- const arrayBuffer = await fileResponse.arrayBuffer();
2198
- return new Uint8Array(arrayBuffer);
2199
- } catch (error) {
2200
- clearTimeout(timeoutId);
2201
- if (error instanceof Error && error.name === "AbortError") {
2202
- throw new Error(`File download timed out after ${timeoutMs}ms`);
2287
+ const apiKey = extractAnthropicApiKey(req);
2288
+ if (!apiKey) {
2289
+ return sendAnthropicHttpError(res, 401, "authentication_error", "Missing x-api-key header (or Authorization with API key).", requestId);
2203
2290
  }
2204
- throw error;
2205
- }
2206
- }
2207
- async function downloadAndDecryptFile(response, dek, apiKey, timeoutMs) {
2208
- if (!response.success || !response.fileId) {
2209
- return null;
2210
- }
2211
- const decryptFileName = (encryptedHex) => {
2212
- const encrypted = hexToBytes6(encryptedHex);
2213
- const decrypted = decryptWithDEK(dek, encrypted);
2214
- return new TextDecoder().decode(decrypted);
2215
- };
2216
- const fileName = decryptFileName(response.fileName);
2217
- const mimeType = decryptFileName(response.mimeType);
2218
- const encryptedFile = await downloadEncryptedFile(response.fileId, apiKey, timeoutMs);
2219
- const decryptedFile = decryptWithDEK(dek, encryptedFile);
2220
- return {
2221
- fileId: response.fileId,
2222
- fileName,
2223
- mimeType,
2224
- content: decryptedFile,
2225
- fileSize: decryptedFile.length
2226
- };
2291
+ try {
2292
+ const body = req.body;
2293
+ const openaiParams = anthropicMessagesCreateToOpenAI(body);
2294
+ const client = await deps.getOrCreateClient(apiKey);
2295
+ const completion = await client.chat.completions.create(openaiParams);
2296
+ if (body.stream) {
2297
+ res.status(200);
2298
+ res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
2299
+ res.setHeader("Cache-Control", "no-cache");
2300
+ res.setHeader("Connection", "keep-alive");
2301
+ if (completion && typeof completion === "object" && Symbol.asyncIterator in completion) {
2302
+ const messageId = newAnthropicMessageId();
2303
+ await pipeOpenAIChunkStreamToAnthropicSse(res, completion, {
2304
+ anthropicModel: body.model,
2305
+ messageId
2306
+ });
2307
+ } else {
2308
+ sendAnthropicHttpError(res, 500, "api_error", "Expected streamed completion", requestId);
2309
+ }
2310
+ return;
2311
+ }
2312
+ const message = openAIChatCompletionToAnthropicMessage(completion, body.model);
2313
+ res.json(message);
2314
+ } catch (err) {
2315
+ if (err instanceof AnthropicRequestValidationError) {
2316
+ return sendAnthropicHttpError(res, err.status, err.anthropicType, err.message, requestId);
2317
+ }
2318
+ mapUnknownErrorToAnthropicResponse(err, res, requestId);
2319
+ }
2320
+ });
2227
2321
  }
2228
- async function callSimpleTool(toolName, params, apiKey, timeoutMs, attest2) {
2229
- const enclavePublicKey = await getEnclavePublicKey(timeoutMs);
2230
- const { cipherText, sharedSecret } = createMLKEMEncapsulation(enclavePublicKey);
2231
- const { encrypted, nonce } = encryptPayload(sharedSecret, params);
2232
- const body = {
2233
- cipherText: bytesToHex7(cipherText),
2234
- encryptedParams: bytesToHex7(encrypted),
2235
- nonce: bytesToHex7(nonce)
2322
+
2323
+ // src/anthropic/models-route.ts
2324
+ function toAnthropicModel(model) {
2325
+ return {
2326
+ type: "model",
2327
+ id: model.model,
2328
+ display_name: model.name || model.model,
2329
+ created_at: model.created_at
2236
2330
  };
2237
- const response = await callToolRequest(toolName, body, apiKey, timeoutMs, attest2);
2238
- return decryptPayload(response.encryptedResponse, sharedSecret, hexToBytes6(response.nonce));
2239
2331
  }
2240
- async function callFileOutputTool(toolName, params, apiKey, dekStore, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, attest2 = true) {
2241
- const enclavePublicKey = await getEnclavePublicKey(timeoutMs);
2242
- const { cipherText, sharedSecret } = createMLKEMEncapsulation(enclavePublicKey);
2243
- const { encrypted, nonce } = encryptPayload(sharedSecret, params);
2244
- const dek = randomBytes5(32);
2245
- const { encrypted: encryptedDEK, nonce: dekNonce } = encryptPayload(sharedSecret, dek);
2246
- const _clientKEK = clientKEK ? hexToBytes6(clientKEK) : getClientKEK();
2247
- const wrappedDEK = wrapDEK(_clientKEK, dek);
2248
- const clientKID = clientKEK ? getClientKID(clientKEK) : getClientKID();
2249
- const body = {
2250
- cipherText: bytesToHex7(cipherText),
2251
- encryptedParams: bytesToHex7(encrypted),
2252
- nonce: bytesToHex7(nonce),
2253
- encryptedDEK: bytesToHex7(encryptedDEK),
2254
- dekNonce: bytesToHex7(dekNonce),
2255
- kid: clientKID,
2256
- wrappedDEK: bytesToHex7(wrappedDEK)
2257
- };
2258
- const response = await callToolRequest(toolName, body, apiKey, timeoutMs, attest2);
2259
- const result = await downloadAndDecryptFile(response, dek, apiKey, timeoutMs);
2260
- if (result?.fileId) {
2261
- if (!dekStore.fileDEKs) {
2262
- dekStore.fileDEKs = new Map;
2263
- }
2264
- dekStore.fileDEKs.set(result.fileId, wrappedDEK);
2265
- }
2266
- return result;
2332
+ function filterEnabled(models) {
2333
+ return models.filter((m) => m.enabled !== 0);
2267
2334
  }
2268
- async function callFileInputTool(toolName, params, apiKey, dekStore, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, attest2 = true) {
2269
- if (!params.fileId) {
2270
- throw new Error(`Tool ${toolName} requires fileId parameter`);
2271
- }
2272
- const enclavePublicKey = await getEnclavePublicKey(timeoutMs);
2273
- const { cipherText, sharedSecret } = createMLKEMEncapsulation(enclavePublicKey);
2274
- const dek = randomBytes5(32);
2275
- const { encrypted: encryptedDEK, nonce: dekNonce } = encryptPayload(sharedSecret, dek);
2276
- const nonce = randomBytes5(24);
2277
- if (!dekStore.fileDEKs) {
2278
- dekStore.fileDEKs = new Map;
2335
+ function parseLimit(raw) {
2336
+ if (typeof raw !== "string" || raw.length === 0) {
2337
+ return 20;
2279
2338
  }
2280
- const _clientKEK = clientKEK ? hexToBytes6(clientKEK) : getClientKEK();
2281
- let fileDEK = dekStore.fileDEKs.get(params.fileId);
2282
- if (!fileDEK) {
2283
- fileDEK = randomBytes5(32);
2284
- const wrappedFileDEK = wrapDEK(_clientKEK, fileDEK);
2285
- dekStore.fileDEKs.set(params.fileId, wrappedFileDEK);
2286
- } else {
2287
- fileDEK = unwrapDEK(_clientKEK, fileDEK);
2339
+ const n = Number.parseInt(raw, 10);
2340
+ if (!Number.isFinite(n) || n <= 0) {
2341
+ return 20;
2288
2342
  }
2289
- const { encrypted: encryptedFileDEK, nonce: fileDEKNonce } = encryptPayload(sharedSecret, fileDEK);
2290
- const body = {
2291
- cipherText: bytesToHex7(cipherText),
2292
- nonce: bytesToHex7(nonce),
2293
- fileId: params.fileId,
2294
- encryptedDEK: bytesToHex7(encryptedDEK),
2295
- dekNonce: bytesToHex7(dekNonce),
2296
- encryptedFileDEK: bytesToHex7(encryptedFileDEK),
2297
- fileDEKNonce: bytesToHex7(fileDEKNonce)
2298
- };
2299
- const response = await callToolRequest(toolName, body, apiKey, timeoutMs, attest2);
2300
- return decryptPayload(response.encryptedResponse, sharedSecret, hexToBytes6(response.nonce));
2343
+ return Math.min(n, 1000);
2301
2344
  }
2302
- async function callRagTool(toolName, params, apiKey, dekStore, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, attest2 = true) {
2303
- const enclavePublicKey = await getEnclavePublicKey(timeoutMs);
2304
- const { cipherText, sharedSecret } = createMLKEMEncapsulation(enclavePublicKey);
2305
- const { encrypted, nonce } = encryptPayload(sharedSecret, params);
2306
- const dek = randomBytes5(32);
2307
- const { encrypted: encryptedDEK, nonce: dekNonce } = encryptPayload(sharedSecret, dek);
2308
- if (!dekStore.fileDEKs) {
2309
- dekStore.fileDEKs = new Map;
2310
- }
2311
- let fileIds = [];
2312
- if (dekStore.fileDEKs.size > 0) {
2313
- fileIds = Array.from(dekStore.fileDEKs.keys());
2314
- }
2315
- const _clientKEK = clientKEK ? hexToBytes6(clientKEK) : getClientKEK();
2316
- const encryptedFileDEKs = fileIds.reduce((acc, fileId) => {
2317
- const fileDEK = dekStore.fileDEKs?.get(fileId);
2318
- if (!fileDEK) {
2319
- return acc;
2345
+ function paginate(all, beforeId, afterId, limit) {
2346
+ let start = 0;
2347
+ let end = all.length;
2348
+ if (afterId) {
2349
+ const idx = all.findIndex((m) => m.id === afterId);
2350
+ if (idx >= 0) {
2351
+ start = idx + 1;
2320
2352
  }
2321
- const unwrappedFileDEK = unwrapDEK(_clientKEK, fileDEK);
2322
- const { encrypted: encryptedFileDEK, nonce: fileDEKNonce } = encryptPayload(sharedSecret, unwrappedFileDEK);
2323
- acc.push({
2324
- fileId,
2325
- encryptedDEK: bytesToHex7(encryptedFileDEK),
2326
- nonce: bytesToHex7(fileDEKNonce)
2327
- });
2328
- return acc;
2329
- }, []);
2330
- if (!dekStore.ragDEK) {
2331
- throw new Error("RAG DEK not found in dekStore. Please upload at least one file with ragIndex: true to initialize RAG.");
2332
- }
2333
- if (!dekStore.ragVersion) {
2334
- throw new Error("RAG Version not found in dekStore. Please upload at least one file with ragIndex: true to initialize RAG.");
2335
2353
  }
2336
- const ragDEK = unwrapDEK(_clientKEK, dekStore.ragDEK);
2337
- const { encrypted: encryptedRagDEK, nonce: ragDEKNonce } = encryptPayload(sharedSecret, ragDEK);
2338
- const { encrypted: encryptedRagVersion, nonce: ragVersionNonce } = encryptPayload(sharedSecret, dekStore.ragVersion);
2339
- const body = {
2340
- cipherText: bytesToHex7(cipherText),
2341
- encryptedParams: bytesToHex7(encrypted),
2342
- nonce: bytesToHex7(nonce),
2343
- encryptedDEK: bytesToHex7(encryptedDEK),
2344
- dekNonce: bytesToHex7(dekNonce),
2345
- encryptedFileDEKs,
2346
- encryptedRagDEK: bytesToHex7(encryptedRagDEK),
2347
- ragDEKNonce: bytesToHex7(ragDEKNonce),
2348
- encryptedRagVersion: bytesToHex7(encryptedRagVersion),
2349
- ragVersionNonce: bytesToHex7(ragVersionNonce)
2350
- };
2351
- const response = await callToolRequest(toolName, body, apiKey, timeoutMs, attest2);
2352
- return decryptPayload(response.encryptedResponse, sharedSecret, hexToBytes6(response.nonce));
2353
- }
2354
- async function callTool(toolName, params, apiKey, dekStore, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, attest2 = true) {
2355
- if (FILE_OUTPUT_TOOLS.includes(toolName)) {
2356
- return callFileOutputTool(toolName, params, apiKey, dekStore, clientKEK, timeoutMs, attest2);
2357
- } else if (FILE_INPUT_TOOLS.includes(toolName)) {
2358
- return callFileInputTool(toolName, params, apiKey, dekStore, clientKEK, timeoutMs, attest2);
2359
- } else if (RAG_TOOLS.includes(toolName)) {
2360
- return callRagTool(toolName, params, apiKey, dekStore, clientKEK, timeoutMs, attest2);
2361
- } else {
2362
- return callSimpleTool(toolName, params, apiKey, timeoutMs, attest2);
2354
+ if (beforeId) {
2355
+ const idx = all.findIndex((m) => m.id === beforeId);
2356
+ if (idx >= 0) {
2357
+ end = idx;
2358
+ }
2363
2359
  }
2360
+ const window = all.slice(start, end);
2361
+ const items = window.slice(0, limit);
2362
+ return { items, hasMore: window.length > items.length };
2364
2363
  }
2365
- function createToolsClient(apiKey, dekStore, clientKEK, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS, attest2 = true) {
2366
- return {
2367
- generateImage: (params) => callTool("generateImage", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
2368
- audioGenerateFromText: (params) => callTool("audioGenerateFromText", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
2369
- createFileForUser: (params) => callTool("createFileForUser", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
2370
- imageDescribeAndCaption: (params) => callTool("imageDescribeAndCaption", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
2371
- imageDescribeAndCaptionFallback: (params) => callTool("imageDescribeAndCaptionFallback", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
2372
- videoDescribeAndCaption: (params) => callTool("videoDescribeAndCaption", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
2373
- getPDFContent: (params) => callTool("getPDFContent", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
2374
- getTextDocumentContent: (params) => callTool("getTextDocumentContent", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
2375
- transcribeAudioToText: (params) => callTool("transcribeAudioToText", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
2376
- transcribeAudioWithDiarization: (params) => callTool("transcribeAudioWithDiarization", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
2377
- audioDiarization: (params) => callTool("audioDiarization", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
2378
- getFileContentOCR: (params) => callTool("getFileContentOCR", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
2379
- getSpreadsheetContent: (params) => callTool("getSpreadsheetContent", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
2380
- getDataFileContent: (params) => callTool("getDataFileContent", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
2381
- getPowerPointContent: (params) => callTool("getPowerPointContent", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
2382
- getTime: (params) => callTool("getTime", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
2383
- webSearchTool: (params) => callTool("webSearchTool", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
2384
- webPageScraperTool: (params) => callTool("webPageScraperTool", params, apiKey, dekStore, clientKEK, timeoutMs, attest2),
2385
- searchRag: (params) => callTool("searchRag", params, apiKey, dekStore, clientKEK, timeoutMs, attest2)
2386
- };
2387
- }
2388
-
2389
- // src/core.ts
2390
- async function createRvencClient(options) {
2391
- const {
2392
- apiKey,
2393
- clientKEK,
2394
- requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS,
2395
- maxBufferSize = DEFAULT_MAX_BUFFER_SIZE,
2396
- attest: attest2 = true
2397
- } = options;
2398
- if (options.config?.endpoints !== undefined) {
2399
- Object.assign(endpoints, options.config.endpoints);
2400
- }
2401
- let encryptionKeys;
2402
- try {
2403
- encryptionKeys = options.encryptionKeys ?? await generateEncryptionKeys(requestTimeoutMs);
2404
- } catch (error) {
2405
- throw new Error(`Failed to initialize encryption keys: ${error instanceof Error ? error.message : error}`);
2406
- }
2407
- const dekStore = options.dekStore ?? initializeDEKStore(clientKEK);
2408
- const client = createRvencChatClient(apiKey, encryptionKeys, requestTimeoutMs, maxBufferSize, attest2, options.config?.openAIClientOptions ?? {});
2409
- client.files = createFilesClient(apiKey, dekStore, clientKEK, requestTimeoutMs);
2410
- client.tools = createToolsClient(apiKey, dekStore, clientKEK, requestTimeoutMs, attest2);
2411
- client.audio = createAudioClient(apiKey, encryptionKeys, requestTimeoutMs, attest2);
2412
- client.models = createModelsClient(apiKey, requestTimeoutMs);
2413
- client.dekStore = dekStore;
2414
- return client;
2364
+ function registerAnthropicModelsRoute(router, deps) {
2365
+ router.get("/v1/models", async (req, res) => {
2366
+ const requestId = newAnthropicRequestId();
2367
+ res.setHeader("request-id", requestId);
2368
+ const versionResult = resolveAnthropicVersion(req);
2369
+ if (!versionResult.ok) {
2370
+ return sendAnthropicHttpError(res, 400, "invalid_request_error", versionResult.message, requestId);
2371
+ }
2372
+ const apiKey = extractAnthropicApiKey(req);
2373
+ if (!apiKey) {
2374
+ return sendAnthropicHttpError(res, 401, "authentication_error", "Missing x-api-key header (or Authorization with API key).", requestId);
2375
+ }
2376
+ try {
2377
+ const client = await deps.getOrCreateClient(apiKey);
2378
+ const type = typeof req.query.type === "string" ? req.query.type : undefined;
2379
+ const all = filterEnabled(await client.models.list({ type })).map(toAnthropicModel);
2380
+ const beforeId = typeof req.query.before_id === "string" ? req.query.before_id : undefined;
2381
+ const afterId = typeof req.query.after_id === "string" ? req.query.after_id : undefined;
2382
+ const limit = parseLimit(req.query.limit);
2383
+ const { items, hasMore } = paginate(all, beforeId, afterId, limit);
2384
+ res.json({
2385
+ data: items,
2386
+ first_id: items.length > 0 ? items[0].id : null,
2387
+ last_id: items.length > 0 ? items[items.length - 1].id : null,
2388
+ has_more: hasMore
2389
+ });
2390
+ } catch (err) {
2391
+ mapUnknownErrorToAnthropicResponse(err, res, requestId);
2392
+ }
2393
+ });
2394
+ router.get("/v1/models/:model_id", async (req, res) => {
2395
+ const requestId = newAnthropicRequestId();
2396
+ res.setHeader("request-id", requestId);
2397
+ const versionResult = resolveAnthropicVersion(req);
2398
+ if (!versionResult.ok) {
2399
+ return sendAnthropicHttpError(res, 400, "invalid_request_error", versionResult.message, requestId);
2400
+ }
2401
+ const apiKey = extractAnthropicApiKey(req);
2402
+ if (!apiKey) {
2403
+ return sendAnthropicHttpError(res, 401, "authentication_error", "Missing x-api-key header (or Authorization with API key).", requestId);
2404
+ }
2405
+ const modelId = req.params.model_id;
2406
+ if (!modelId) {
2407
+ return sendAnthropicHttpError(res, 400, "invalid_request_error", "Missing model id.", requestId);
2408
+ }
2409
+ try {
2410
+ const client = await deps.getOrCreateClient(apiKey);
2411
+ const found = filterEnabled(await client.models.list()).find((m) => m.model === modelId);
2412
+ if (!found) {
2413
+ return sendAnthropicHttpError(res, 404, "not_found_error", `Model "${modelId}" not found.`, requestId);
2414
+ }
2415
+ res.json(toAnthropicModel(found));
2416
+ } catch (err) {
2417
+ mapUnknownErrorToAnthropicResponse(err, res, requestId);
2418
+ }
2419
+ });
2415
2420
  }
2416
- var core_default = createRvencClient;
2417
2421
 
2418
2422
  // src/server/runtime.ts
2423
+ import multer from "multer";
2419
2424
  var DEFAULT_HOST = process.env.HOST ?? "127.0.0.1";
2420
2425
  var DEFAULT_PORT = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 8000;
2421
2426
  var CLIENT_CACHE_MAX = (() => {
@@ -2924,36 +2929,23 @@ async function startServer(options = {}) {
2924
2929
  var server_default = createServerApp("both");
2925
2930
 
2926
2931
  // src/cli.ts
2927
- var { values } = parseArgs({
2928
- args: process.argv.slice(2),
2929
- options: {
2930
- host: { type: "string", default: "127.0.0.1" },
2931
- "proxy-url": { type: "string" },
2932
- "enclave-url": { type: "string" },
2933
- kek: { type: "string" },
2934
- port: { type: "string", default: "8000" },
2935
- "no-attest": { type: "boolean", default: false },
2936
- compat: { type: "string", default: "openai" },
2937
- "openai-prefix": { type: "string" },
2938
- "anthropic-prefix": { type: "string" },
2939
- "json-body-limit": { type: "string" }
2940
- },
2941
- strict: true
2942
- });
2943
- var compat = values.compat;
2944
- if (compat !== "openai" && compat !== "anthropic" && compat !== "both") {
2945
- console.error(`Invalid --compat "${compat}". Use openai, anthropic, or both.`);
2946
- process.exit(1);
2947
- }
2932
+ var COMPAT_MODES = ["openai", "anthropic", "both"];
2933
+ var program = new Command("confidential-proxy").description("end-to-end encrypted OpenAI/Anthropic-compatible proxy").option("--host <host>", "Host to listen on", "127.0.0.1").option("--port <port>", "Port to listen on", (v) => parseInt(v, 10), 8000).option("--proxy-url <url>", "Upstream proxy URL").option("--enclave-url <url>", "Enclave URL").option("--kek <key>", "Client key encryption key", generateNewClientKEK()).option("--no-attest", "Disable enclave attestation").option("--compat <mode>", `Compatibility mode: ${COMPAT_MODES.join(", ")}`, (v) => {
2934
+ if (!COMPAT_MODES.includes(v)) {
2935
+ throw new Error(`Invalid --compat "${v}". Use: ${COMPAT_MODES.join(", ")}.`);
2936
+ }
2937
+ return v;
2938
+ }, "openai").option("--openai-prefix <prefix>", "Route prefix for the OpenAI API (used with --compat both)").option("--anthropic-prefix <prefix>", "Route prefix for the Anthropic API (used with --compat both)").option("--json-body-limit <size>", "JSON body size limit (e.g. 64mb)").parse();
2939
+ var opts = program.opts();
2948
2940
  startServer({
2949
- host: values.host,
2950
- proxyUrl: values["proxy-url"],
2951
- enclaveUrl: values["enclave-url"],
2952
- kek: values.kek,
2953
- port: Number(values.port),
2954
- attest: !values["no-attest"],
2955
- compat,
2956
- openaiRoutePrefix: values["openai-prefix"],
2957
- anthropicRoutePrefix: values["anthropic-prefix"],
2958
- jsonBodyLimit: values["json-body-limit"]
2941
+ host: opts.host,
2942
+ port: opts.port,
2943
+ proxyUrl: opts.proxyUrl,
2944
+ enclaveUrl: opts.enclaveUrl,
2945
+ kek: opts.kek,
2946
+ attest: opts.attest,
2947
+ compat: opts.compat,
2948
+ openaiRoutePrefix: opts.openaiPrefix,
2949
+ anthropicRoutePrefix: opts.anthropicPrefix,
2950
+ jsonBodyLimit: opts.jsonBodyLimit
2959
2951
  });