@premai/api-sdk 1.0.44 → 1.0.46
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -0
- package/dist/cli.mjs +2179 -2191
- package/package.json +3 -2
package/dist/cli.mjs
CHANGED
|
@@ -3,2419 +3,2420 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
4
|
|
|
5
5
|
// src/cli.ts
|
|
6
|
-
import {
|
|
6
|
+
import { Command } from "commander";
|
|
7
7
|
|
|
8
|
-
// src/
|
|
9
|
-
import
|
|
8
|
+
// src/audio/index.ts
|
|
9
|
+
import { bytesToHex as bytesToHex2, hexToBytes as hexToBytes2 } from "@noble/ciphers/utils.js";
|
|
10
10
|
|
|
11
|
-
// src/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
29
|
+
cachedPrem = await import("@premai/reticle");
|
|
30
|
+
return cachedPrem;
|
|
20
31
|
}
|
|
21
|
-
function
|
|
22
|
-
return
|
|
32
|
+
function isAttestationError(err) {
|
|
33
|
+
return err instanceof Error && err.name === "AttestationError";
|
|
23
34
|
}
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
45
|
-
const
|
|
46
|
-
if (
|
|
47
|
-
|
|
61
|
+
function isTransientError(err) {
|
|
62
|
+
const messages = [];
|
|
63
|
+
if (err instanceof Error) {
|
|
64
|
+
messages.push(err.message);
|
|
48
65
|
}
|
|
49
|
-
if (
|
|
50
|
-
|
|
66
|
+
if (isAttestationError(err) && Array.isArray(err.cause)) {
|
|
67
|
+
messages.push(...err.cause);
|
|
51
68
|
}
|
|
52
|
-
return
|
|
69
|
+
return messages.some((m) => TRANSIENT_PATTERNS.some((re) => re.test(m)));
|
|
53
70
|
}
|
|
54
|
-
function
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
66
|
-
|
|
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
|
|
74
|
-
if (
|
|
75
|
-
return
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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 (
|
|
96
|
-
|
|
106
|
+
if (sessionId === null) {
|
|
107
|
+
throw new Error("missing x-session-id issued by attestation");
|
|
97
108
|
}
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
124
|
+
throw lastErr;
|
|
102
125
|
}
|
|
103
|
-
function
|
|
104
|
-
if (!
|
|
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
|
|
108
|
-
if (
|
|
109
|
-
return
|
|
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
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
150
|
+
return sessionId;
|
|
151
|
+
}).finally(() => {
|
|
152
|
+
attestInflight.delete(key);
|
|
153
|
+
});
|
|
154
|
+
attestInflight.set(key, work);
|
|
155
|
+
return work;
|
|
121
156
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
139
|
-
|
|
179
|
+
const encrypted = chacha.encrypt(encodedData);
|
|
180
|
+
return { encrypted, nonce };
|
|
140
181
|
}
|
|
141
|
-
function
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
181
|
-
|
|
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
|
|
266
|
+
throw raw;
|
|
184
267
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
268
|
+
|
|
269
|
+
// src/utils/files.ts
|
|
270
|
+
var getFileName = (file) => {
|
|
271
|
+
if (file instanceof File) {
|
|
272
|
+
return file.name;
|
|
188
273
|
}
|
|
189
|
-
if (
|
|
190
|
-
return
|
|
274
|
+
if (file instanceof Blob) {
|
|
275
|
+
return;
|
|
191
276
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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 (
|
|
220
|
-
return
|
|
293
|
+
if (file instanceof ArrayBuffer) {
|
|
294
|
+
return new Uint8Array(file);
|
|
221
295
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
274
|
-
return out;
|
|
327
|
+
throw new Error("Unsupported file type for audio transcription");
|
|
275
328
|
}
|
|
276
|
-
function
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
370
|
+
const responseNonce = hexToBytes2(data.nonce);
|
|
371
|
+
return decryptPayload(data.encryptedResponse, sharedSecret, responseNonce);
|
|
350
372
|
}
|
|
351
|
-
function
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
|
377
|
-
|
|
378
|
-
|
|
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
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
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
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
469
|
-
|
|
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
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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
|
-
|
|
799
|
-
|
|
800
|
-
} catch (
|
|
801
|
-
|
|
802
|
-
|
|
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
|
-
|
|
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/
|
|
810
|
-
|
|
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
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
created_at: model.created_at
|
|
494
|
+
fileDEKs: new Map,
|
|
495
|
+
ragDEK: wrappedRagDEK,
|
|
496
|
+
ragVersion: "2"
|
|
816
497
|
};
|
|
817
498
|
}
|
|
818
|
-
function
|
|
819
|
-
|
|
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
|
-
|
|
826
|
-
|
|
827
|
-
|
|
503
|
+
return hexToBytes3(process.env.CLIENT_KEK);
|
|
504
|
+
}
|
|
505
|
+
function getClientKID(clientKEK) {
|
|
506
|
+
if (clientKEK) {
|
|
507
|
+
return bytesToHex3(keyIdFromKEK(hexToBytes3(clientKEK)));
|
|
828
508
|
}
|
|
829
|
-
|
|
509
|
+
const _clientKEK = getClientKEK();
|
|
510
|
+
return bytesToHex3(keyIdFromKEK(_clientKEK));
|
|
830
511
|
}
|
|
831
|
-
function
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
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
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
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
|
-
|
|
847
|
-
const items = window.slice(0, limit);
|
|
848
|
-
return { items, hasMore: window.length > items.length };
|
|
694
|
+
return { dek, filePayload };
|
|
849
695
|
}
|
|
850
|
-
function
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
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
|
-
|
|
864
|
-
|
|
865
|
-
|
|
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
|
-
|
|
881
|
-
|
|
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
|
|
897
|
-
|
|
898
|
-
|
|
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
|
-
|
|
902
|
-
|
|
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
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
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
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
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
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
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
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
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 (
|
|
970
|
-
|
|
789
|
+
if (options?.offset !== undefined) {
|
|
790
|
+
queryParams.append("offset", options.offset.toString());
|
|
971
791
|
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
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
|
-
|
|
988
|
-
|
|
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
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
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
|
-
|
|
998
|
-
|
|
999
|
-
|
|
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
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
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
|
-
|
|
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
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
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
|
-
|
|
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
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
const
|
|
1035
|
-
if (
|
|
1036
|
-
|
|
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
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
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
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
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
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
}
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
}
|
|
1080
|
-
|
|
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
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
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
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
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
|
-
|
|
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(
|
|
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(`
|
|
1040
|
+
throw new Error(`List models request failed with status ${response.status}`);
|
|
1105
1041
|
}
|
|
1106
|
-
const
|
|
1107
|
-
if (
|
|
1108
|
-
throw new Error(
|
|
1042
|
+
const result = await response.json();
|
|
1043
|
+
if (result.status !== 200) {
|
|
1044
|
+
throw new Error(result.error || "List models failed");
|
|
1109
1045
|
}
|
|
1110
|
-
|
|
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(`
|
|
1052
|
+
throw new Error(`List models request timed out after ${timeoutMs}ms`);
|
|
1114
1053
|
}
|
|
1115
|
-
throw
|
|
1054
|
+
throw error;
|
|
1116
1055
|
} finally {
|
|
1117
1056
|
clearTimeout(timeoutId);
|
|
1118
1057
|
}
|
|
1119
1058
|
}
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1059
|
+
function createModelsClient(apiKey, timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
1060
|
+
return {
|
|
1061
|
+
list: (params) => listModels(params, apiKey, timeoutMs)
|
|
1062
|
+
};
|
|
1123
1063
|
}
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
const
|
|
1130
|
-
|
|
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
|
|
1133
|
-
|
|
1134
|
-
|
|
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
|
|
1137
|
-
const
|
|
1138
|
-
|
|
1139
|
-
|
|
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
|
|
1142
|
-
const
|
|
1143
|
-
|
|
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
|
|
1146
|
-
const
|
|
1147
|
-
|
|
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
|
|
1150
|
-
const
|
|
1151
|
-
|
|
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/
|
|
1155
|
-
|
|
1156
|
-
|
|
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
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
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
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
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
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
const
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
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
|
|
1249
|
-
const
|
|
1250
|
-
const
|
|
1251
|
-
const
|
|
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
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
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
|
|
1268
|
-
const
|
|
1269
|
-
const
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
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
|
|
1277
|
-
const
|
|
1278
|
-
const
|
|
1279
|
-
|
|
1280
|
-
|
|
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
|
-
|
|
1283
|
-
return decryptPayload(data.encryptedResponse, sharedSecret, responseNonce);
|
|
1366
|
+
return result;
|
|
1284
1367
|
}
|
|
1285
|
-
async function
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
const
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
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
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
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
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
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
|
-
|
|
1376
|
-
|
|
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
|
-
|
|
1380
|
-
|
|
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/
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
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/
|
|
1391
|
-
import
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
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
|
|
1403
|
-
|
|
1404
|
-
|
|
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
|
-
|
|
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
|
|
1409
|
-
|
|
1410
|
-
|
|
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
|
-
|
|
1413
|
-
|
|
1559
|
+
if (Array.isArray(raw) && raw[0]) {
|
|
1560
|
+
return raw[0];
|
|
1561
|
+
}
|
|
1562
|
+
return null;
|
|
1414
1563
|
}
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
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
|
|
1494
|
-
|
|
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
|
|
1497
|
-
|
|
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
|
|
1500
|
-
|
|
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
|
|
1503
|
-
|
|
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
|
|
1506
|
-
|
|
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
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
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
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
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
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
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
|
-
|
|
1563
|
-
|
|
1564
|
-
throw new Error(`Save RAG DEK request timed out after ${timeoutMs}ms`);
|
|
1683
|
+
if (parts.length === 0) {
|
|
1684
|
+
return [];
|
|
1565
1685
|
}
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1686
|
+
return [{ role: "system", content: parts.join(`
|
|
1687
|
+
|
|
1688
|
+
`) }];
|
|
1569
1689
|
}
|
|
1570
|
-
|
|
1571
|
-
|
|
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
|
-
|
|
1693
|
+
throw new AnthropicRequestValidationError("Invalid system parameter shape.");
|
|
1595
1694
|
}
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
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);
|
|
1607
|
-
}
|
|
1608
|
-
} else {
|
|
1609
|
-
ragDEK = unwrapDEK(_clientKEK, ragDEK);
|
|
1695
|
+
function toolResultContentToString(content) {
|
|
1696
|
+
if (typeof content === "string") {
|
|
1697
|
+
return content;
|
|
1610
1698
|
}
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
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;
|
|
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));
|
|
1637
1709
|
}
|
|
1638
|
-
}
|
|
1639
|
-
|
|
1710
|
+
}
|
|
1711
|
+
return parts.join(`
|
|
1712
|
+
`);
|
|
1713
|
+
}
|
|
1714
|
+
return JSON.stringify(content);
|
|
1715
|
+
}
|
|
1716
|
+
function anthropicImageBlockToOpenAIPart(part) {
|
|
1717
|
+
const source = part.source;
|
|
1718
|
+
if (!source || typeof source !== "object") {
|
|
1719
|
+
return null;
|
|
1640
1720
|
}
|
|
1641
|
-
const
|
|
1642
|
-
if (
|
|
1643
|
-
|
|
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 (
|
|
1646
|
-
|
|
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
|
|
1732
|
+
return null;
|
|
1649
1733
|
}
|
|
1650
|
-
function
|
|
1651
|
-
if (
|
|
1652
|
-
|
|
1734
|
+
function anthropicUserContentToOpenAIMessages(content) {
|
|
1735
|
+
if (typeof content === "string") {
|
|
1736
|
+
return [{ role: "user", content }];
|
|
1653
1737
|
}
|
|
1654
|
-
const
|
|
1655
|
-
const
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
if (
|
|
1672
|
-
|
|
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
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
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
|
-
|
|
1690
|
-
|
|
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
|
-
|
|
1693
|
-
|
|
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
|
-
|
|
1696
|
-
|
|
1824
|
+
return msg;
|
|
1825
|
+
}
|
|
1826
|
+
function anthropicToolsToOpenAI(tools) {
|
|
1827
|
+
if (tools === undefined) {
|
|
1828
|
+
return;
|
|
1697
1829
|
}
|
|
1698
|
-
if (
|
|
1699
|
-
|
|
1830
|
+
if (!Array.isArray(tools)) {
|
|
1831
|
+
throw new AnthropicRequestValidationError("tools must be an array.");
|
|
1700
1832
|
}
|
|
1701
|
-
const
|
|
1702
|
-
const
|
|
1703
|
-
|
|
1704
|
-
|
|
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
|
-
|
|
1720
|
-
|
|
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
|
-
|
|
1723
|
-
|
|
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
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
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
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
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
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
if (
|
|
1755
|
-
throw new
|
|
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
|
-
|
|
1880
|
+
return { type: "function", function: { name: tc.name } };
|
|
1758
1881
|
}
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
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 !== "user" && m.role !== "assistant") {
|
|
1902
|
+
throw new AnthropicRequestValidationError(`Invalid message role "${m.role}".`);
|
|
1762
1903
|
}
|
|
1763
|
-
if (
|
|
1764
|
-
|
|
1904
|
+
if (m.role === "user") {
|
|
1905
|
+
messages.push(...anthropicUserContentToOpenAIMessages(m.content));
|
|
1906
|
+
} else {
|
|
1907
|
+
messages.push(anthropicAssistantContentToOpenAI(m.content));
|
|
1765
1908
|
}
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1909
|
+
}
|
|
1910
|
+
const isStreaming = Boolean(body.stream);
|
|
1911
|
+
const params = {
|
|
1912
|
+
model: body.model,
|
|
1913
|
+
messages,
|
|
1914
|
+
max_tokens: body.max_tokens,
|
|
1915
|
+
stream: isStreaming
|
|
1916
|
+
};
|
|
1917
|
+
if (isStreaming) {
|
|
1918
|
+
params.stream_options = { include_usage: true };
|
|
1919
|
+
}
|
|
1920
|
+
const tools = anthropicToolsToOpenAI(body.tools);
|
|
1921
|
+
if (tools !== undefined && tools.length > 0) {
|
|
1922
|
+
params.tools = tools;
|
|
1923
|
+
}
|
|
1924
|
+
const toolChoice = anthropicToolChoiceToOpenAI(body.tool_choice);
|
|
1925
|
+
if (toolChoice !== undefined) {
|
|
1926
|
+
params.tool_choice = toolChoice;
|
|
1927
|
+
}
|
|
1928
|
+
if (body.stop_sequences !== undefined) {
|
|
1929
|
+
if (!Array.isArray(body.stop_sequences) || !body.stop_sequences.every((s) => typeof s === "string")) {
|
|
1930
|
+
throw new AnthropicRequestValidationError("stop_sequences must be an array of strings.");
|
|
1770
1931
|
}
|
|
1771
|
-
|
|
1772
|
-
} finally {
|
|
1773
|
-
clearTimeout(timeoutId);
|
|
1932
|
+
params.stop = body.stop_sequences;
|
|
1774
1933
|
}
|
|
1934
|
+
if (typeof body.temperature === "number") {
|
|
1935
|
+
params.temperature = body.temperature;
|
|
1936
|
+
}
|
|
1937
|
+
if (typeof body.top_p === "number") {
|
|
1938
|
+
params.top_p = body.top_p;
|
|
1939
|
+
}
|
|
1940
|
+
if (typeof body.top_k === "number") {
|
|
1941
|
+
console.warn("[proxy] top_k is not supported by the OpenAI API and will be ignored.");
|
|
1942
|
+
}
|
|
1943
|
+
return params;
|
|
1775
1944
|
}
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
const
|
|
1784
|
-
|
|
1785
|
-
|
|
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}`);
|
|
1945
|
+
|
|
1946
|
+
// src/anthropic/count-tokens-route.ts
|
|
1947
|
+
function extractTextCharCount(body) {
|
|
1948
|
+
let len = 0;
|
|
1949
|
+
if (typeof body.system === "string") {
|
|
1950
|
+
len += body.system.length;
|
|
1951
|
+
} else if (Array.isArray(body.system)) {
|
|
1952
|
+
for (const block of body.system) {
|
|
1953
|
+
if (block && block.type === "text" && typeof block.text === "string") {
|
|
1954
|
+
len += block.text.length;
|
|
1794
1955
|
}
|
|
1795
|
-
throw new Error(`Delete file request failed with status ${response.status}`);
|
|
1796
1956
|
}
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1957
|
+
} else if (body.system && typeof body.system === "object" && body.system.type === "text") {
|
|
1958
|
+
len += body.system.text.length;
|
|
1959
|
+
}
|
|
1960
|
+
for (const msg of body.messages) {
|
|
1961
|
+
if (typeof msg.content === "string") {
|
|
1962
|
+
len += msg.content.length;
|
|
1963
|
+
} else if (Array.isArray(msg.content)) {
|
|
1964
|
+
for (const part of msg.content) {
|
|
1965
|
+
if (!part || typeof part !== "object")
|
|
1966
|
+
continue;
|
|
1967
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
1968
|
+
len += part.text.length;
|
|
1969
|
+
} else if (part.type === "tool_result") {
|
|
1970
|
+
const c = part.content;
|
|
1971
|
+
if (typeof c === "string") {
|
|
1972
|
+
len += c.length;
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1801
1976
|
}
|
|
1802
|
-
throw error;
|
|
1803
|
-
} finally {
|
|
1804
|
-
clearTimeout(timeoutId);
|
|
1805
1977
|
}
|
|
1806
|
-
|
|
1807
|
-
|
|
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.");
|
|
1978
|
+
if (Array.isArray(body.tools)) {
|
|
1979
|
+
len += JSON.stringify(body.tools).length;
|
|
1815
1980
|
}
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1981
|
+
return len;
|
|
1982
|
+
}
|
|
1983
|
+
function registerAnthropicCountTokensRoute(router, _deps) {
|
|
1984
|
+
router.post("/v1/messages/count_tokens", async (req, res) => {
|
|
1985
|
+
const requestId = newAnthropicRequestId();
|
|
1986
|
+
res.setHeader("request-id", requestId);
|
|
1987
|
+
const versionResult = resolveAnthropicVersion(req);
|
|
1988
|
+
if (!versionResult.ok) {
|
|
1989
|
+
return sendAnthropicHttpError(res, 400, "invalid_request_error", versionResult.message, requestId);
|
|
1824
1990
|
}
|
|
1825
|
-
const
|
|
1826
|
-
|
|
1827
|
-
|
|
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}`);
|
|
1991
|
+
const apiKey = extractAnthropicApiKey(req);
|
|
1992
|
+
if (!apiKey) {
|
|
1993
|
+
return sendAnthropicHttpError(res, 401, "authentication_error", "Missing x-api-key header (or Authorization with API key).", requestId);
|
|
1852
1994
|
}
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1995
|
+
try {
|
|
1996
|
+
const raw = req.body;
|
|
1997
|
+
const body = {
|
|
1998
|
+
...raw,
|
|
1999
|
+
max_tokens: typeof raw.max_tokens === "number" && Number.isFinite(raw.max_tokens) ? raw.max_tokens : 4096,
|
|
2000
|
+
stream: false
|
|
2001
|
+
};
|
|
2002
|
+
anthropicMessagesCreateToOpenAI(body);
|
|
2003
|
+
const input_tokens = Math.max(1, Math.ceil(extractTextCharCount(body) / 4));
|
|
2004
|
+
res.json({ input_tokens });
|
|
2005
|
+
} catch (err) {
|
|
2006
|
+
if (err instanceof AnthropicRequestValidationError) {
|
|
2007
|
+
return sendAnthropicHttpError(res, err.status, err.anthropicType, err.message, requestId);
|
|
2008
|
+
}
|
|
2009
|
+
mapUnknownErrorToAnthropicResponse(err, res, requestId);
|
|
1858
2010
|
}
|
|
1859
|
-
|
|
1860
|
-
} finally {
|
|
1861
|
-
clearTimeout(timeoutId);
|
|
1862
|
-
}
|
|
2011
|
+
});
|
|
1863
2012
|
}
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
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.");
|
|
2013
|
+
|
|
2014
|
+
// src/anthropic/from-openai.ts
|
|
2015
|
+
function openAiFinishReasonToAnthropic(finish) {
|
|
2016
|
+
if (!finish) {
|
|
2017
|
+
return { stop_reason: null, stop_sequence: null };
|
|
1872
2018
|
}
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
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);
|
|
2019
|
+
switch (finish) {
|
|
2020
|
+
case "stop":
|
|
2021
|
+
return { stop_reason: "end_turn", stop_sequence: null };
|
|
2022
|
+
case "length":
|
|
2023
|
+
return { stop_reason: "max_tokens", stop_sequence: null };
|
|
2024
|
+
case "tool_calls":
|
|
2025
|
+
return { stop_reason: "tool_use", stop_sequence: null };
|
|
2026
|
+
case "content_filter":
|
|
2027
|
+
return { stop_reason: "refusal", stop_sequence: null };
|
|
2028
|
+
default:
|
|
2029
|
+
return { stop_reason: "end_turn", stop_sequence: null };
|
|
1907
2030
|
}
|
|
1908
2031
|
}
|
|
1909
|
-
function
|
|
1910
|
-
|
|
1911
|
-
|
|
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);
|
|
2032
|
+
function extractTextFromAssistantContent(content) {
|
|
2033
|
+
if (content == null) {
|
|
2034
|
+
return "";
|
|
1927
2035
|
}
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
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");
|
|
2036
|
+
if (typeof content === "string") {
|
|
2037
|
+
return content;
|
|
2038
|
+
}
|
|
2039
|
+
if (!Array.isArray(content)) {
|
|
2040
|
+
return "";
|
|
2041
|
+
}
|
|
2042
|
+
const parts = [];
|
|
2043
|
+
for (const p of content) {
|
|
2044
|
+
if (typeof p === "string") {
|
|
2045
|
+
parts.push(p);
|
|
2046
|
+
continue;
|
|
1948
2047
|
}
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
if (error instanceof Error && error.name === "AbortError") {
|
|
1952
|
-
throw new Error(`List models request timed out after ${timeoutMs}ms`);
|
|
2048
|
+
if (p && typeof p === "object" && "type" in p && p.type === "text" && "text" in p) {
|
|
2049
|
+
parts.push(String(p.text));
|
|
1953
2050
|
}
|
|
1954
|
-
throw error;
|
|
1955
|
-
} finally {
|
|
1956
|
-
clearTimeout(timeoutId);
|
|
1957
2051
|
}
|
|
2052
|
+
return parts.join("");
|
|
1958
2053
|
}
|
|
1959
|
-
function
|
|
1960
|
-
|
|
1961
|
-
|
|
2054
|
+
function openAIChatCompletionToAnthropicMessage(completion, requestModel) {
|
|
2055
|
+
const choice = completion.choices[0];
|
|
2056
|
+
const message = choice?.message;
|
|
2057
|
+
const contentText = message ? extractTextFromAssistantContent(message.content) : "";
|
|
2058
|
+
const content = [];
|
|
2059
|
+
if (contentText.length > 0) {
|
|
2060
|
+
content.push({ type: "text", text: contentText });
|
|
2061
|
+
}
|
|
2062
|
+
if (message?.tool_calls?.length) {
|
|
2063
|
+
for (const tc of message.tool_calls) {
|
|
2064
|
+
if (tc.type !== "function") {
|
|
2065
|
+
continue;
|
|
2066
|
+
}
|
|
2067
|
+
let input = {};
|
|
2068
|
+
try {
|
|
2069
|
+
input = JSON.parse(tc.function.arguments || "{}");
|
|
2070
|
+
} catch {
|
|
2071
|
+
input = { _raw_arguments: tc.function.arguments ?? "" };
|
|
2072
|
+
}
|
|
2073
|
+
content.push({
|
|
2074
|
+
type: "tool_use",
|
|
2075
|
+
id: tc.id,
|
|
2076
|
+
name: tc.function.name,
|
|
2077
|
+
input
|
|
2078
|
+
});
|
|
2079
|
+
}
|
|
2080
|
+
}
|
|
2081
|
+
if (content.length === 0) {
|
|
2082
|
+
content.push({ type: "text", text: "" });
|
|
2083
|
+
}
|
|
2084
|
+
const { stop_reason, stop_sequence } = openAiFinishReasonToAnthropic(choice?.finish_reason);
|
|
2085
|
+
const u = completion.usage;
|
|
2086
|
+
const usage = {
|
|
2087
|
+
input_tokens: u?.prompt_tokens ?? 0,
|
|
2088
|
+
output_tokens: u?.completion_tokens ?? 0
|
|
1962
2089
|
};
|
|
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
2090
|
return {
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
nonce
|
|
2091
|
+
id: newAnthropicMessageId(),
|
|
2092
|
+
type: "message",
|
|
2093
|
+
role: "assistant",
|
|
2094
|
+
content,
|
|
2095
|
+
model: requestModel,
|
|
2096
|
+
stop_reason,
|
|
2097
|
+
stop_sequence,
|
|
2098
|
+
usage
|
|
1981
2099
|
};
|
|
1982
2100
|
}
|
|
1983
|
-
|
|
1984
|
-
if (!
|
|
1985
|
-
|
|
2101
|
+
function chunkFinishToAnthropic(finish) {
|
|
2102
|
+
if (!finish) {
|
|
2103
|
+
return null;
|
|
1986
2104
|
}
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
2105
|
+
return openAiFinishReasonToAnthropic(finish).stop_reason;
|
|
2106
|
+
}
|
|
2107
|
+
async function pipeOpenAIChunkStreamToAnthropicSse(res, stream, options) {
|
|
2108
|
+
const { anthropicModel, messageId } = options;
|
|
2109
|
+
let textBlockOpen = false;
|
|
2110
|
+
let inputTokens = 0;
|
|
2111
|
+
let outputTokens = 0;
|
|
2112
|
+
let stopReason = null;
|
|
2113
|
+
const toolStates = new Map;
|
|
2114
|
+
let nextAnthropicIndex = 0;
|
|
2115
|
+
let textBlockIndex = null;
|
|
2116
|
+
writeAnthropicSseEvent(res, "message_start", {
|
|
2117
|
+
type: "message_start",
|
|
2118
|
+
message: {
|
|
2119
|
+
id: messageId,
|
|
2120
|
+
type: "message",
|
|
2121
|
+
role: "assistant",
|
|
2122
|
+
content: [],
|
|
2123
|
+
model: anthropicModel,
|
|
2124
|
+
stop_reason: null,
|
|
2125
|
+
stop_sequence: null,
|
|
2126
|
+
usage: { input_tokens: inputTokens, output_tokens: outputTokens }
|
|
1992
2127
|
}
|
|
2128
|
+
});
|
|
2129
|
+
const ensureTextBlock = () => {
|
|
2130
|
+
if (textBlockOpen) {
|
|
2131
|
+
return;
|
|
2132
|
+
}
|
|
2133
|
+
textBlockIndex = nextAnthropicIndex++;
|
|
2134
|
+
textBlockOpen = true;
|
|
2135
|
+
writeAnthropicSseEvent(res, "content_block_start", {
|
|
2136
|
+
type: "content_block_start",
|
|
2137
|
+
index: textBlockIndex,
|
|
2138
|
+
content_block: { type: "text", text: "" }
|
|
2139
|
+
});
|
|
1993
2140
|
};
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
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;
|
|
2141
|
+
const closeTextBlockIfOpen = () => {
|
|
2142
|
+
if (!textBlockOpen || textBlockIndex === null) {
|
|
2143
|
+
return;
|
|
2043
2144
|
}
|
|
2145
|
+
writeAnthropicSseEvent(res, "content_block_stop", {
|
|
2146
|
+
type: "content_block_stop",
|
|
2147
|
+
index: textBlockIndex
|
|
2148
|
+
});
|
|
2149
|
+
textBlockOpen = false;
|
|
2044
2150
|
};
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2151
|
+
const getOrCreateTool = (openAiIdx) => {
|
|
2152
|
+
let st = toolStates.get(openAiIdx);
|
|
2153
|
+
if (!st) {
|
|
2154
|
+
st = {
|
|
2155
|
+
anthropicIndex: nextAnthropicIndex++,
|
|
2156
|
+
id: "",
|
|
2157
|
+
name: "",
|
|
2158
|
+
lastArgs: "",
|
|
2159
|
+
argsEmittedLen: 0,
|
|
2160
|
+
started: false,
|
|
2161
|
+
stopped: false
|
|
2162
|
+
};
|
|
2163
|
+
toolStates.set(openAiIdx, st);
|
|
2058
2164
|
}
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
finish_reason: choice?.finish_reason ?? "stop",
|
|
2074
|
-
logprobs: null
|
|
2165
|
+
return st;
|
|
2166
|
+
};
|
|
2167
|
+
const flushToolArgs = (st) => {
|
|
2168
|
+
if (!st.started || st.lastArgs.length <= st.argsEmittedLen) {
|
|
2169
|
+
return;
|
|
2170
|
+
}
|
|
2171
|
+
const partial = st.lastArgs.slice(st.argsEmittedLen);
|
|
2172
|
+
st.argsEmittedLen = st.lastArgs.length;
|
|
2173
|
+
writeAnthropicSseEvent(res, "content_block_delta", {
|
|
2174
|
+
type: "content_block_delta",
|
|
2175
|
+
index: st.anthropicIndex,
|
|
2176
|
+
delta: {
|
|
2177
|
+
type: "input_json_delta",
|
|
2178
|
+
partial_json: partial
|
|
2075
2179
|
}
|
|
2076
|
-
|
|
2077
|
-
usage: completion.usage ?? null
|
|
2180
|
+
});
|
|
2078
2181
|
};
|
|
2079
|
-
}
|
|
2080
|
-
async function* createDecryptedStreamGenerator(reader, sharedSecret, nonce, maxBufferSize) {
|
|
2081
|
-
const decoder = new TextDecoder;
|
|
2082
|
-
let buffer = "";
|
|
2083
2182
|
try {
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
if (buffer.length > maxBufferSize) {
|
|
2090
|
-
throw new Error(`Stream buffer exceeded maximum size of ${maxBufferSize} bytes`);
|
|
2183
|
+
for await (const chunk of stream) {
|
|
2184
|
+
if (chunk.usage) {
|
|
2185
|
+
const u = chunk.usage;
|
|
2186
|
+
inputTokens = u.prompt_tokens ?? inputTokens;
|
|
2187
|
+
outputTokens = u.completion_tokens ?? outputTokens;
|
|
2091
2188
|
}
|
|
2092
|
-
const
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
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");
|
|
2189
|
+
const choice = chunk.choices?.[0];
|
|
2190
|
+
if (!choice) {
|
|
2191
|
+
continue;
|
|
2192
|
+
}
|
|
2193
|
+
const delta = choice.delta;
|
|
2194
|
+
if (typeof delta?.content === "string" && delta.content.length > 0) {
|
|
2195
|
+
ensureTextBlock();
|
|
2196
|
+
if (textBlockIndex !== null) {
|
|
2197
|
+
writeAnthropicSseEvent(res, "content_block_delta", {
|
|
2198
|
+
type: "content_block_delta",
|
|
2199
|
+
index: textBlockIndex,
|
|
2200
|
+
delta: { type: "text_delta", text: delta.content }
|
|
2201
|
+
});
|
|
2115
2202
|
}
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2203
|
+
}
|
|
2204
|
+
if (delta?.tool_calls?.length) {
|
|
2205
|
+
closeTextBlockIfOpen();
|
|
2206
|
+
for (const tc of delta.tool_calls) {
|
|
2207
|
+
const idx = typeof tc.index === "number" && Number.isFinite(tc.index) ? tc.index : 0;
|
|
2208
|
+
const st = getOrCreateTool(idx);
|
|
2209
|
+
if (typeof tc.id === "string" && tc.id.length > 0) {
|
|
2210
|
+
st.id = tc.id;
|
|
2211
|
+
}
|
|
2212
|
+
const fn = tc.function;
|
|
2213
|
+
if (fn?.name && fn.name.length > 0) {
|
|
2214
|
+
st.name = fn.name;
|
|
2215
|
+
}
|
|
2216
|
+
if (typeof fn?.arguments === "string") {
|
|
2217
|
+
st.lastArgs += fn.arguments;
|
|
2218
|
+
}
|
|
2219
|
+
if (!st.started && st.id.length > 0 && st.name.length > 0) {
|
|
2220
|
+
writeAnthropicSseEvent(res, "content_block_start", {
|
|
2221
|
+
type: "content_block_start",
|
|
2222
|
+
index: st.anthropicIndex,
|
|
2223
|
+
content_block: {
|
|
2224
|
+
type: "tool_use",
|
|
2225
|
+
id: st.id,
|
|
2226
|
+
name: st.name
|
|
2227
|
+
}
|
|
2228
|
+
});
|
|
2229
|
+
st.started = true;
|
|
2230
|
+
}
|
|
2231
|
+
if (st.started) {
|
|
2232
|
+
flushToolArgs(st);
|
|
2120
2233
|
}
|
|
2121
|
-
yield chunk;
|
|
2122
2234
|
}
|
|
2123
2235
|
}
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
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);
|
|
2236
|
+
if (choice.finish_reason) {
|
|
2237
|
+
const mapped = chunkFinishToAnthropic(choice.finish_reason);
|
|
2238
|
+
if (mapped) {
|
|
2239
|
+
stopReason = mapped;
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2165
2242
|
}
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2243
|
+
closeTextBlockIfOpen();
|
|
2244
|
+
const sortedTools = [...toolStates.values()].sort((a, b) => a.anthropicIndex - b.anthropicIndex);
|
|
2245
|
+
for (const st of sortedTools) {
|
|
2246
|
+
if (st.started && !st.stopped) {
|
|
2247
|
+
writeAnthropicSseEvent(res, "content_block_stop", {
|
|
2248
|
+
type: "content_block_stop",
|
|
2249
|
+
index: st.anthropicIndex
|
|
2250
|
+
});
|
|
2251
|
+
st.stopped = true;
|
|
2252
|
+
}
|
|
2172
2253
|
}
|
|
2173
|
-
|
|
2254
|
+
writeAnthropicSseEvent(res, "message_delta", {
|
|
2255
|
+
type: "message_delta",
|
|
2256
|
+
delta: { stop_reason: stopReason, stop_sequence: null },
|
|
2257
|
+
usage: {
|
|
2258
|
+
input_tokens: inputTokens,
|
|
2259
|
+
output_tokens: outputTokens
|
|
2260
|
+
}
|
|
2261
|
+
});
|
|
2262
|
+
writeAnthropicSseEvent(res, "message_stop", { type: "message_stop" });
|
|
2263
|
+
res.end();
|
|
2264
|
+
} catch (err) {
|
|
2265
|
+
const message = err instanceof Error ? err.message : "Stream error";
|
|
2266
|
+
writeAnthropicSseEvent(res, "error", {
|
|
2267
|
+
type: "error",
|
|
2268
|
+
error: { type: "api_error", message }
|
|
2269
|
+
});
|
|
2270
|
+
res.end();
|
|
2174
2271
|
}
|
|
2175
2272
|
}
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
const
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
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}`);
|
|
2273
|
+
|
|
2274
|
+
// src/anthropic/messages-route.ts
|
|
2275
|
+
function registerAnthropicMessagesRoute(router, deps) {
|
|
2276
|
+
router.post("/v1/messages", async (req, res) => {
|
|
2277
|
+
const requestId = newAnthropicRequestId();
|
|
2278
|
+
res.setHeader("request-id", requestId);
|
|
2279
|
+
const versionResult = resolveAnthropicVersion(req);
|
|
2280
|
+
if (!versionResult.ok) {
|
|
2281
|
+
return sendAnthropicHttpError(res, 400, "invalid_request_error", versionResult.message, requestId);
|
|
2195
2282
|
}
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
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`);
|
|
2283
|
+
const apiKey = extractAnthropicApiKey(req);
|
|
2284
|
+
if (!apiKey) {
|
|
2285
|
+
return sendAnthropicHttpError(res, 401, "authentication_error", "Missing x-api-key header (or Authorization with API key).", requestId);
|
|
2203
2286
|
}
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2287
|
+
try {
|
|
2288
|
+
const body = req.body;
|
|
2289
|
+
const openaiParams = anthropicMessagesCreateToOpenAI(body);
|
|
2290
|
+
const client = await deps.getOrCreateClient(apiKey);
|
|
2291
|
+
const completion = await client.chat.completions.create(openaiParams);
|
|
2292
|
+
if (body.stream) {
|
|
2293
|
+
res.status(200);
|
|
2294
|
+
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
|
2295
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
2296
|
+
res.setHeader("Connection", "keep-alive");
|
|
2297
|
+
if (completion && typeof completion === "object" && Symbol.asyncIterator in completion) {
|
|
2298
|
+
const messageId = newAnthropicMessageId();
|
|
2299
|
+
await pipeOpenAIChunkStreamToAnthropicSse(res, completion, {
|
|
2300
|
+
anthropicModel: body.model,
|
|
2301
|
+
messageId
|
|
2302
|
+
});
|
|
2303
|
+
} else {
|
|
2304
|
+
sendAnthropicHttpError(res, 500, "api_error", "Expected streamed completion", requestId);
|
|
2305
|
+
}
|
|
2306
|
+
return;
|
|
2307
|
+
}
|
|
2308
|
+
const message = openAIChatCompletionToAnthropicMessage(completion, body.model);
|
|
2309
|
+
res.json(message);
|
|
2310
|
+
} catch (err) {
|
|
2311
|
+
if (err instanceof AnthropicRequestValidationError) {
|
|
2312
|
+
return sendAnthropicHttpError(res, err.status, err.anthropicType, err.message, requestId);
|
|
2313
|
+
}
|
|
2314
|
+
mapUnknownErrorToAnthropicResponse(err, res, requestId);
|
|
2315
|
+
}
|
|
2316
|
+
});
|
|
2227
2317
|
}
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2318
|
+
|
|
2319
|
+
// src/anthropic/models-route.ts
|
|
2320
|
+
function toAnthropicModel(model) {
|
|
2321
|
+
return {
|
|
2322
|
+
type: "model",
|
|
2323
|
+
id: model.model,
|
|
2324
|
+
display_name: model.name || model.model,
|
|
2325
|
+
created_at: model.created_at
|
|
2236
2326
|
};
|
|
2237
|
-
const response = await callToolRequest(toolName, body, apiKey, timeoutMs, attest2);
|
|
2238
|
-
return decryptPayload(response.encryptedResponse, sharedSecret, hexToBytes6(response.nonce));
|
|
2239
2327
|
}
|
|
2240
|
-
|
|
2241
|
-
|
|
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;
|
|
2328
|
+
function filterEnabled(models) {
|
|
2329
|
+
return models.filter((m) => m.enabled !== 0);
|
|
2267
2330
|
}
|
|
2268
|
-
|
|
2269
|
-
if (
|
|
2270
|
-
|
|
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;
|
|
2331
|
+
function parseLimit(raw) {
|
|
2332
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
2333
|
+
return 20;
|
|
2279
2334
|
}
|
|
2280
|
-
const
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
fileDEK = randomBytes5(32);
|
|
2284
|
-
const wrappedFileDEK = wrapDEK(_clientKEK, fileDEK);
|
|
2285
|
-
dekStore.fileDEKs.set(params.fileId, wrappedFileDEK);
|
|
2286
|
-
} else {
|
|
2287
|
-
fileDEK = unwrapDEK(_clientKEK, fileDEK);
|
|
2335
|
+
const n = Number.parseInt(raw, 10);
|
|
2336
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
2337
|
+
return 20;
|
|
2288
2338
|
}
|
|
2289
|
-
|
|
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));
|
|
2339
|
+
return Math.min(n, 1000);
|
|
2301
2340
|
}
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
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;
|
|
2341
|
+
function paginate(all, beforeId, afterId, limit) {
|
|
2342
|
+
let start = 0;
|
|
2343
|
+
let end = all.length;
|
|
2344
|
+
if (afterId) {
|
|
2345
|
+
const idx = all.findIndex((m) => m.id === afterId);
|
|
2346
|
+
if (idx >= 0) {
|
|
2347
|
+
start = idx + 1;
|
|
2320
2348
|
}
|
|
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
2349
|
}
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
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);
|
|
2350
|
+
if (beforeId) {
|
|
2351
|
+
const idx = all.findIndex((m) => m.id === beforeId);
|
|
2352
|
+
if (idx >= 0) {
|
|
2353
|
+
end = idx;
|
|
2354
|
+
}
|
|
2363
2355
|
}
|
|
2356
|
+
const window = all.slice(start, end);
|
|
2357
|
+
const items = window.slice(0, limit);
|
|
2358
|
+
return { items, hasMore: window.length > items.length };
|
|
2364
2359
|
}
|
|
2365
|
-
function
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2360
|
+
function registerAnthropicModelsRoute(router, deps) {
|
|
2361
|
+
router.get("/v1/models", async (req, res) => {
|
|
2362
|
+
const requestId = newAnthropicRequestId();
|
|
2363
|
+
res.setHeader("request-id", requestId);
|
|
2364
|
+
const versionResult = resolveAnthropicVersion(req);
|
|
2365
|
+
if (!versionResult.ok) {
|
|
2366
|
+
return sendAnthropicHttpError(res, 400, "invalid_request_error", versionResult.message, requestId);
|
|
2367
|
+
}
|
|
2368
|
+
const apiKey = extractAnthropicApiKey(req);
|
|
2369
|
+
if (!apiKey) {
|
|
2370
|
+
return sendAnthropicHttpError(res, 401, "authentication_error", "Missing x-api-key header (or Authorization with API key).", requestId);
|
|
2371
|
+
}
|
|
2372
|
+
try {
|
|
2373
|
+
const client = await deps.getOrCreateClient(apiKey);
|
|
2374
|
+
const type = typeof req.query.type === "string" ? req.query.type : undefined;
|
|
2375
|
+
const all = filterEnabled(await client.models.list({ type })).map(toAnthropicModel);
|
|
2376
|
+
const beforeId = typeof req.query.before_id === "string" ? req.query.before_id : undefined;
|
|
2377
|
+
const afterId = typeof req.query.after_id === "string" ? req.query.after_id : undefined;
|
|
2378
|
+
const limit = parseLimit(req.query.limit);
|
|
2379
|
+
const { items, hasMore } = paginate(all, beforeId, afterId, limit);
|
|
2380
|
+
res.json({
|
|
2381
|
+
data: items,
|
|
2382
|
+
first_id: items.length > 0 ? items[0].id : null,
|
|
2383
|
+
last_id: items.length > 0 ? items[items.length - 1].id : null,
|
|
2384
|
+
has_more: hasMore
|
|
2385
|
+
});
|
|
2386
|
+
} catch (err) {
|
|
2387
|
+
mapUnknownErrorToAnthropicResponse(err, res, requestId);
|
|
2388
|
+
}
|
|
2389
|
+
});
|
|
2390
|
+
router.get("/v1/models/:model_id", async (req, res) => {
|
|
2391
|
+
const requestId = newAnthropicRequestId();
|
|
2392
|
+
res.setHeader("request-id", requestId);
|
|
2393
|
+
const versionResult = resolveAnthropicVersion(req);
|
|
2394
|
+
if (!versionResult.ok) {
|
|
2395
|
+
return sendAnthropicHttpError(res, 400, "invalid_request_error", versionResult.message, requestId);
|
|
2396
|
+
}
|
|
2397
|
+
const apiKey = extractAnthropicApiKey(req);
|
|
2398
|
+
if (!apiKey) {
|
|
2399
|
+
return sendAnthropicHttpError(res, 401, "authentication_error", "Missing x-api-key header (or Authorization with API key).", requestId);
|
|
2400
|
+
}
|
|
2401
|
+
const modelId = req.params.model_id;
|
|
2402
|
+
if (!modelId) {
|
|
2403
|
+
return sendAnthropicHttpError(res, 400, "invalid_request_error", "Missing model id.", requestId);
|
|
2404
|
+
}
|
|
2405
|
+
try {
|
|
2406
|
+
const client = await deps.getOrCreateClient(apiKey);
|
|
2407
|
+
const found = filterEnabled(await client.models.list()).find((m) => m.model === modelId);
|
|
2408
|
+
if (!found) {
|
|
2409
|
+
return sendAnthropicHttpError(res, 404, "not_found_error", `Model "${modelId}" not found.`, requestId);
|
|
2410
|
+
}
|
|
2411
|
+
res.json(toAnthropicModel(found));
|
|
2412
|
+
} catch (err) {
|
|
2413
|
+
mapUnknownErrorToAnthropicResponse(err, res, requestId);
|
|
2414
|
+
}
|
|
2415
|
+
});
|
|
2415
2416
|
}
|
|
2416
|
-
var core_default = createRvencClient;
|
|
2417
2417
|
|
|
2418
2418
|
// src/server/runtime.ts
|
|
2419
|
+
import multer from "multer";
|
|
2419
2420
|
var DEFAULT_HOST = process.env.HOST ?? "127.0.0.1";
|
|
2420
2421
|
var DEFAULT_PORT = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 8000;
|
|
2421
2422
|
var CLIENT_CACHE_MAX = (() => {
|
|
@@ -2924,36 +2925,23 @@ async function startServer(options = {}) {
|
|
|
2924
2925
|
var server_default = createServerApp("both");
|
|
2925
2926
|
|
|
2926
2927
|
// src/cli.ts
|
|
2927
|
-
var
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
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
|
-
}
|
|
2928
|
+
var COMPAT_MODES = ["openai", "anthropic", "both"];
|
|
2929
|
+
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) => {
|
|
2930
|
+
if (!COMPAT_MODES.includes(v)) {
|
|
2931
|
+
throw new Error(`Invalid --compat "${v}". Use: ${COMPAT_MODES.join(", ")}.`);
|
|
2932
|
+
}
|
|
2933
|
+
return v;
|
|
2934
|
+
}, "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();
|
|
2935
|
+
var opts = program.opts();
|
|
2948
2936
|
startServer({
|
|
2949
|
-
host:
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
attest:
|
|
2955
|
-
compat,
|
|
2956
|
-
openaiRoutePrefix:
|
|
2957
|
-
anthropicRoutePrefix:
|
|
2958
|
-
jsonBodyLimit:
|
|
2937
|
+
host: opts.host,
|
|
2938
|
+
port: opts.port,
|
|
2939
|
+
proxyUrl: opts.proxyUrl,
|
|
2940
|
+
enclaveUrl: opts.enclaveUrl,
|
|
2941
|
+
kek: opts.kek,
|
|
2942
|
+
attest: opts.attest,
|
|
2943
|
+
compat: opts.compat,
|
|
2944
|
+
openaiRoutePrefix: opts.openaiPrefix,
|
|
2945
|
+
anthropicRoutePrefix: opts.anthropicPrefix,
|
|
2946
|
+
jsonBodyLimit: opts.jsonBodyLimit
|
|
2959
2947
|
});
|