@premai/api-sdk 1.0.40 → 1.0.42
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 +165 -39
- package/dist/anthropic/count-tokens-route.d.ts +3 -0
- package/dist/anthropic/from-openai.d.ts +33 -0
- package/dist/anthropic/http.d.ts +19 -0
- package/dist/anthropic/messages-route.d.ts +3 -0
- package/dist/anthropic/models-route.d.ts +3 -0
- package/dist/anthropic/to-openai.d.ts +35 -0
- package/dist/audio/index.d.ts +1 -0
- package/dist/cli-claude.d.ts +2 -0
- package/dist/cli-claude.mjs +3304 -0
- package/dist/cli.mjs +2959 -0
- package/dist/core.browser.cjs +161 -21
- package/dist/core.browser.mjs +161 -21
- package/dist/index.cjs +1508 -238
- package/dist/index.d.ts +5 -3
- package/dist/index.mjs +1508 -240
- package/dist/launcher/claude-code.d.ts +1 -0
- package/dist/launcher/model-picker.d.ts +7 -0
- package/dist/launcher/proxy-subprocess.d.ts +13 -0
- package/dist/launcher/text-input.d.ts +3 -0
- package/dist/openai/routes.d.ts +3 -0
- package/dist/server/create-app.d.ts +8 -0
- package/dist/server/discovery.d.ts +7 -0
- package/dist/server/route-prefix.d.ts +9 -0
- package/dist/server/runtime.d.ts +8 -0
- package/dist/server/start.d.ts +4 -0
- package/dist/server.d.ts +7 -5
- package/dist/types.d.ts +9 -3
- package/package.json +8 -3
- package/dist/cli.cjs +0 -1698
package/dist/index.cjs
CHANGED
|
@@ -66,6 +66,8 @@ __export(exports_src, {
|
|
|
66
66
|
startServer: () => startServer,
|
|
67
67
|
serverApp: () => server_default,
|
|
68
68
|
serializeDEKStore: () => serializeDEKStore,
|
|
69
|
+
resolvePrefixesForCompat: () => resolvePrefixesForCompat,
|
|
70
|
+
normalizeRoutePrefix: () => normalizeRoutePrefix,
|
|
69
71
|
loadPrem: () => loadPrem,
|
|
70
72
|
isGatewayError: () => isGatewayError,
|
|
71
73
|
isAttestationError: () => isAttestationError,
|
|
@@ -77,15 +79,12 @@ __export(exports_src, {
|
|
|
77
79
|
generateEncryptionKeys: () => generateEncryptionKeys,
|
|
78
80
|
deserializeDEKStore: () => deserializeDEKStore,
|
|
79
81
|
default: () => core_default,
|
|
80
|
-
|
|
82
|
+
createServerApp: () => createServerApp,
|
|
83
|
+
createRvencClient: () => createRvencClient,
|
|
84
|
+
DEFAULT_OPENAI_ROUTE_PREFIX_BOTH: () => DEFAULT_OPENAI_ROUTE_PREFIX_BOTH,
|
|
85
|
+
DEFAULT_ANTHROPIC_ROUTE_PREFIX_BOTH: () => DEFAULT_ANTHROPIC_ROUTE_PREFIX_BOTH
|
|
81
86
|
});
|
|
82
87
|
module.exports = __toCommonJS(exports_src);
|
|
83
|
-
var import_dotenv2 = __toESM(require("dotenv"));
|
|
84
|
-
|
|
85
|
-
// src/server.ts
|
|
86
|
-
var import_dotenv = __toESM(require("dotenv"));
|
|
87
|
-
var import_express = __toESM(require("express"));
|
|
88
|
-
var import_multer = __toESM(require("multer"));
|
|
89
88
|
|
|
90
89
|
// src/audio/index.ts
|
|
91
90
|
var import_utils2 = require("@noble/ciphers/utils.js");
|
|
@@ -126,25 +125,127 @@ function getGatewayErrorMessage(err) {
|
|
|
126
125
|
return err.kind.message;
|
|
127
126
|
return null;
|
|
128
127
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
128
|
+
var ATTEST_TTL_MS = 30000;
|
|
129
|
+
var ATTEST_CACHE_MAX = 500;
|
|
130
|
+
var ATTEST_MAX_ATTEMPTS = 4;
|
|
131
|
+
var ATTEST_RETRY_BASE_MS = 250;
|
|
132
|
+
var ATTEST_RETRY_MAX_MS = 2000;
|
|
133
|
+
var TRANSIENT_PATTERNS = [
|
|
134
|
+
/EOF while parsing/i,
|
|
135
|
+
/error decoding response body/i,
|
|
136
|
+
/connection (reset|closed|refused)/i,
|
|
137
|
+
/socket hang up/i,
|
|
138
|
+
/ETIMEDOUT/i
|
|
139
|
+
];
|
|
140
|
+
var attestCache = new Map;
|
|
141
|
+
var attestInflight = new Map;
|
|
142
|
+
function attestCacheKey(apiKey, model) {
|
|
143
|
+
return `${apiKey}|${model ?? ""}`;
|
|
144
|
+
}
|
|
145
|
+
function pruneExpired(now) {
|
|
146
|
+
for (const [key, entry] of attestCache) {
|
|
147
|
+
if (entry.expires <= now) {
|
|
148
|
+
attestCache.delete(key);
|
|
149
|
+
} else {
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function isTransientError(err) {
|
|
155
|
+
const messages = [];
|
|
156
|
+
if (err instanceof Error) {
|
|
157
|
+
messages.push(err.message);
|
|
158
|
+
}
|
|
159
|
+
if (isAttestationError(err) && Array.isArray(err.cause)) {
|
|
160
|
+
messages.push(...err.cause);
|
|
161
|
+
}
|
|
162
|
+
return messages.some((m) => TRANSIENT_PATTERNS.some((re) => re.test(m)));
|
|
163
|
+
}
|
|
164
|
+
function backoffDelayMs(attempt) {
|
|
165
|
+
const exp = ATTEST_RETRY_BASE_MS * 2 ** (attempt - 1);
|
|
166
|
+
const capped = Math.min(exp, ATTEST_RETRY_MAX_MS);
|
|
167
|
+
const jitter = Math.floor(Math.random() * (capped / 2));
|
|
168
|
+
return capped + jitter;
|
|
169
|
+
}
|
|
170
|
+
function delay(ms) {
|
|
171
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
172
|
+
}
|
|
173
|
+
function safeFree(obj) {
|
|
174
|
+
if (typeof obj?.free !== "function")
|
|
175
|
+
return;
|
|
176
|
+
try {
|
|
177
|
+
obj.free();
|
|
178
|
+
} catch {}
|
|
179
|
+
}
|
|
180
|
+
async function attemptAttest(apiKey, options) {
|
|
132
181
|
const prem = await loadPrem();
|
|
133
|
-
|
|
134
|
-
let
|
|
135
|
-
|
|
136
|
-
|
|
182
|
+
let client;
|
|
183
|
+
let attested;
|
|
184
|
+
let headers;
|
|
185
|
+
let sessionId;
|
|
137
186
|
try {
|
|
138
|
-
client.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
attested.
|
|
144
|
-
|
|
187
|
+
client = await new prem.ClientBuilder(endpoints.proxy ?? "").with_authorization(apiKey).build();
|
|
188
|
+
if (options.model) {
|
|
189
|
+
client.set_query(new prem.QueryParams().with("model", options.model));
|
|
190
|
+
}
|
|
191
|
+
attested = await client.attest();
|
|
192
|
+
headers = attested.headers();
|
|
193
|
+
sessionId = headers.cpu()?.get("x-session-id") ?? headers.gpu()?.get("x-session-id") ?? null;
|
|
145
194
|
} finally {
|
|
146
|
-
|
|
195
|
+
safeFree(headers);
|
|
196
|
+
safeFree(attested);
|
|
197
|
+
safeFree(client);
|
|
198
|
+
}
|
|
199
|
+
if (sessionId === null) {
|
|
200
|
+
throw new Error("missing x-session-id issued by attestation");
|
|
147
201
|
}
|
|
202
|
+
return sessionId;
|
|
203
|
+
}
|
|
204
|
+
async function runAttest(apiKey, options) {
|
|
205
|
+
let lastErr;
|
|
206
|
+
for (let attempt = 1;attempt <= ATTEST_MAX_ATTEMPTS; attempt++) {
|
|
207
|
+
try {
|
|
208
|
+
return await attemptAttest(apiKey, options);
|
|
209
|
+
} catch (err) {
|
|
210
|
+
lastErr = err;
|
|
211
|
+
if (attempt === ATTEST_MAX_ATTEMPTS || !isTransientError(err)) {
|
|
212
|
+
throw err;
|
|
213
|
+
}
|
|
214
|
+
await delay(backoffDelayMs(attempt));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
throw lastErr;
|
|
218
|
+
}
|
|
219
|
+
async function attest(apiKey, options = { enabled: true }) {
|
|
220
|
+
if (!options.enabled)
|
|
221
|
+
return null;
|
|
222
|
+
const key = attestCacheKey(apiKey, options.model);
|
|
223
|
+
const now = Date.now();
|
|
224
|
+
const cached = attestCache.get(key);
|
|
225
|
+
if (cached) {
|
|
226
|
+
if (cached.expires > now)
|
|
227
|
+
return cached.sessionId;
|
|
228
|
+
attestCache.delete(key);
|
|
229
|
+
}
|
|
230
|
+
const inflight = attestInflight.get(key);
|
|
231
|
+
if (inflight) {
|
|
232
|
+
return inflight;
|
|
233
|
+
}
|
|
234
|
+
const work = runAttest(apiKey, options).then((sessionId) => {
|
|
235
|
+
const insertTime = Date.now();
|
|
236
|
+
pruneExpired(insertTime);
|
|
237
|
+
attestCache.set(key, { sessionId, expires: insertTime + ATTEST_TTL_MS });
|
|
238
|
+
if (attestCache.size > ATTEST_CACHE_MAX) {
|
|
239
|
+
const oldest = attestCache.keys().next().value;
|
|
240
|
+
if (oldest)
|
|
241
|
+
attestCache.delete(oldest);
|
|
242
|
+
}
|
|
243
|
+
return sessionId;
|
|
244
|
+
}).finally(() => {
|
|
245
|
+
attestInflight.delete(key);
|
|
246
|
+
});
|
|
247
|
+
attestInflight.set(key, work);
|
|
248
|
+
return work;
|
|
148
249
|
}
|
|
149
250
|
|
|
150
251
|
// src/utils/crypto.ts
|
|
@@ -324,7 +425,8 @@ async function preprocessAudioRequest(body, encryptionKeys) {
|
|
|
324
425
|
const isDeepgram = body.model.startsWith("deepgram/");
|
|
325
426
|
const requestBody = isDeepgram ? {
|
|
326
427
|
model: body.model,
|
|
327
|
-
diarize: body.diarize
|
|
428
|
+
diarize: body.diarize,
|
|
429
|
+
smart_format: body.smart_format
|
|
328
430
|
} : {
|
|
329
431
|
model: body.model,
|
|
330
432
|
language: body.language,
|
|
@@ -1144,10 +1246,14 @@ function createRvencChatClient(apiKey, encryptionKeys, requestTimeoutMs = DEFAUL
|
|
|
1144
1246
|
}
|
|
1145
1247
|
clearTimeout(timeoutId);
|
|
1146
1248
|
if (isStreaming) {
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1249
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
1250
|
+
if (contentType.includes("text/event-stream")) {
|
|
1251
|
+
return await postprocessStreamingResponse(response, encryptedRequest.sharedSecret, encryptedRequest.nonce, maxBufferSize);
|
|
1252
|
+
}
|
|
1253
|
+
const completion = await postprocessNonStreamingResponse(response, encryptedRequest.sharedSecret);
|
|
1254
|
+
return completionToChunkStream(completion);
|
|
1150
1255
|
}
|
|
1256
|
+
return await postprocessNonStreamingResponse(response, encryptedRequest.sharedSecret);
|
|
1151
1257
|
} catch (error) {
|
|
1152
1258
|
clearTimeout(timeoutId);
|
|
1153
1259
|
if (error instanceof Error && error.name === "AbortError") {
|
|
@@ -1158,6 +1264,39 @@ function createRvencChatClient(apiKey, encryptionKeys, requestTimeoutMs = DEFAUL
|
|
|
1158
1264
|
};
|
|
1159
1265
|
return client;
|
|
1160
1266
|
}
|
|
1267
|
+
async function* completionToChunkStream(completion) {
|
|
1268
|
+
const choice = completion.choices[0];
|
|
1269
|
+
const message = choice?.message;
|
|
1270
|
+
const content = typeof message?.content === "string" ? message.content : "";
|
|
1271
|
+
const toolCalls = message?.tool_calls?.filter((tc) => tc.type === "function").map((tc, i) => ({
|
|
1272
|
+
index: i,
|
|
1273
|
+
id: tc.id,
|
|
1274
|
+
type: "function",
|
|
1275
|
+
function: {
|
|
1276
|
+
name: tc.function.name,
|
|
1277
|
+
arguments: tc.function.arguments
|
|
1278
|
+
}
|
|
1279
|
+
}));
|
|
1280
|
+
yield {
|
|
1281
|
+
id: completion.id,
|
|
1282
|
+
object: "chat.completion.chunk",
|
|
1283
|
+
created: completion.created,
|
|
1284
|
+
model: completion.model,
|
|
1285
|
+
choices: [
|
|
1286
|
+
{
|
|
1287
|
+
index: choice?.index ?? 0,
|
|
1288
|
+
delta: {
|
|
1289
|
+
role: "assistant",
|
|
1290
|
+
content,
|
|
1291
|
+
...toolCalls && toolCalls.length > 0 && { tool_calls: toolCalls }
|
|
1292
|
+
},
|
|
1293
|
+
finish_reason: choice?.finish_reason ?? "stop",
|
|
1294
|
+
logprobs: null
|
|
1295
|
+
}
|
|
1296
|
+
],
|
|
1297
|
+
usage: completion.usage ?? null
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1161
1300
|
async function* createDecryptedStreamGenerator(reader, sharedSecret, nonce, maxBufferSize) {
|
|
1162
1301
|
const decoder = new TextDecoder;
|
|
1163
1302
|
let buffer = "";
|
|
@@ -1338,7 +1477,7 @@ async function callFileOutputTool(toolName, params, apiKey, dekStore, clientKEK,
|
|
|
1338
1477
|
};
|
|
1339
1478
|
const response = await callToolRequest(toolName, body, apiKey, timeoutMs, attest2);
|
|
1340
1479
|
const result = await downloadAndDecryptFile(response, dek, apiKey, timeoutMs);
|
|
1341
|
-
if (result
|
|
1480
|
+
if (result?.fileId) {
|
|
1342
1481
|
if (!dekStore.fileDEKs) {
|
|
1343
1482
|
dekStore.fileDEKs = new Map;
|
|
1344
1483
|
}
|
|
@@ -1395,7 +1534,7 @@ async function callRagTool(toolName, params, apiKey, dekStore, clientKEK, timeou
|
|
|
1395
1534
|
}
|
|
1396
1535
|
const _clientKEK = clientKEK ? import_utils6.hexToBytes(clientKEK) : getClientKEK();
|
|
1397
1536
|
const encryptedFileDEKs = fileIds.reduce((acc, fileId) => {
|
|
1398
|
-
const fileDEK = dekStore.fileDEKs
|
|
1537
|
+
const fileDEK = dekStore.fileDEKs?.get(fileId);
|
|
1399
1538
|
if (!fileDEK) {
|
|
1400
1539
|
return acc;
|
|
1401
1540
|
}
|
|
@@ -1495,39 +1634,947 @@ async function createRvencClient(options) {
|
|
|
1495
1634
|
return client;
|
|
1496
1635
|
}
|
|
1497
1636
|
var core_default = createRvencClient;
|
|
1637
|
+
// src/server/create-app.ts
|
|
1638
|
+
var import_express = __toESM(require("express"));
|
|
1498
1639
|
|
|
1499
|
-
// src/
|
|
1500
|
-
|
|
1501
|
-
var
|
|
1502
|
-
var
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1640
|
+
// src/anthropic/http.ts
|
|
1641
|
+
var import_node_crypto = require("node:crypto");
|
|
1642
|
+
var ANTHROPIC_VERSION_DEFAULT = "2023-06-01";
|
|
1643
|
+
var ANTHROPIC_VERSION_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
1644
|
+
function isAnthropicApiVersionSupported(version) {
|
|
1645
|
+
if (version === ANTHROPIC_VERSION_DEFAULT) {
|
|
1646
|
+
return true;
|
|
1647
|
+
}
|
|
1648
|
+
return ANTHROPIC_VERSION_DATE.test(version);
|
|
1649
|
+
}
|
|
1650
|
+
function newAnthropicRequestId() {
|
|
1651
|
+
return `req_${import_node_crypto.randomBytes(12).toString("hex")}`;
|
|
1652
|
+
}
|
|
1653
|
+
function newAnthropicMessageId() {
|
|
1654
|
+
return `msg_${import_node_crypto.randomBytes(12).toString("hex")}`;
|
|
1655
|
+
}
|
|
1656
|
+
function extractAnthropicApiKey(req) {
|
|
1657
|
+
const raw = req.headers["x-api-key"];
|
|
1658
|
+
if (typeof raw === "string" && raw.length > 0) {
|
|
1659
|
+
return raw;
|
|
1660
|
+
}
|
|
1661
|
+
if (Array.isArray(raw) && raw[0]) {
|
|
1662
|
+
return raw[0];
|
|
1514
1663
|
}
|
|
1515
|
-
});
|
|
1516
|
-
var clientCache = new Map;
|
|
1517
|
-
function extractApiKey(req) {
|
|
1518
1664
|
const authHeader = req.headers.authorization;
|
|
1519
1665
|
if (!authHeader) {
|
|
1520
1666
|
return null;
|
|
1521
1667
|
}
|
|
1522
1668
|
if (authHeader.startsWith("Bearer ")) {
|
|
1523
|
-
return authHeader.
|
|
1669
|
+
return authHeader.slice(7);
|
|
1524
1670
|
}
|
|
1525
1671
|
return authHeader;
|
|
1526
1672
|
}
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1673
|
+
function getAnthropicVersionHeader(req) {
|
|
1674
|
+
const raw = req.headers["anthropic-version"];
|
|
1675
|
+
if (typeof raw === "string" && raw.length > 0) {
|
|
1676
|
+
return raw;
|
|
1677
|
+
}
|
|
1678
|
+
if (Array.isArray(raw) && raw[0]) {
|
|
1679
|
+
return raw[0];
|
|
1680
|
+
}
|
|
1681
|
+
return null;
|
|
1682
|
+
}
|
|
1683
|
+
function resolveAnthropicVersion(req) {
|
|
1684
|
+
const header = getAnthropicVersionHeader(req);
|
|
1685
|
+
const version = header ?? ANTHROPIC_VERSION_DEFAULT;
|
|
1686
|
+
if (!isAnthropicApiVersionSupported(version)) {
|
|
1687
|
+
return {
|
|
1688
|
+
ok: false,
|
|
1689
|
+
message: `Unsupported anthropic-version: ${version}. Expected a dated version (YYYY-MM-DD) or ${ANTHROPIC_VERSION_DEFAULT}.`
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
return { ok: true, version };
|
|
1693
|
+
}
|
|
1694
|
+
function sendAnthropicHttpError(res, status, errorType, message, requestId) {
|
|
1695
|
+
res.setHeader("request-id", requestId);
|
|
1696
|
+
res.status(status).json({
|
|
1697
|
+
type: "error",
|
|
1698
|
+
error: { type: errorType, message },
|
|
1699
|
+
request_id: requestId
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
function httpStatusToAnthropicErrorType(status) {
|
|
1703
|
+
if (status === 401) {
|
|
1704
|
+
return "authentication_error";
|
|
1705
|
+
}
|
|
1706
|
+
if (status === 402) {
|
|
1707
|
+
return "billing_error";
|
|
1708
|
+
}
|
|
1709
|
+
if (status === 403) {
|
|
1710
|
+
return "permission_error";
|
|
1711
|
+
}
|
|
1712
|
+
if (status === 404) {
|
|
1713
|
+
return "not_found_error";
|
|
1714
|
+
}
|
|
1715
|
+
if (status === 413) {
|
|
1716
|
+
return "request_too_large";
|
|
1717
|
+
}
|
|
1718
|
+
if (status === 429) {
|
|
1719
|
+
return "rate_limit_error";
|
|
1720
|
+
}
|
|
1721
|
+
if (status === 504) {
|
|
1722
|
+
return "timeout_error";
|
|
1723
|
+
}
|
|
1724
|
+
if (status === 529) {
|
|
1725
|
+
return "overloaded_error";
|
|
1726
|
+
}
|
|
1727
|
+
if (status >= 400 && status < 500) {
|
|
1728
|
+
return "invalid_request_error";
|
|
1729
|
+
}
|
|
1730
|
+
return "api_error";
|
|
1731
|
+
}
|
|
1732
|
+
function extractErrorMessage(err) {
|
|
1733
|
+
if (!err || typeof err !== "object") {
|
|
1734
|
+
return null;
|
|
1735
|
+
}
|
|
1736
|
+
const o = err;
|
|
1737
|
+
if (typeof o.message === "string" && o.message.length > 0) {
|
|
1738
|
+
return o.message;
|
|
1739
|
+
}
|
|
1740
|
+
if (typeof o.error === "string" && o.error.length > 0) {
|
|
1741
|
+
return o.error;
|
|
1742
|
+
}
|
|
1743
|
+
if (o.error && typeof o.error === "object") {
|
|
1744
|
+
const nested = o.error.message;
|
|
1745
|
+
if (typeof nested === "string" && nested.length > 0) {
|
|
1746
|
+
return nested;
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
return null;
|
|
1750
|
+
}
|
|
1751
|
+
function looksLikeApiErrorResponse(err) {
|
|
1752
|
+
if (!err || typeof err !== "object")
|
|
1753
|
+
return false;
|
|
1754
|
+
const o = err;
|
|
1755
|
+
if (typeof o.status !== "number")
|
|
1756
|
+
return false;
|
|
1757
|
+
return "error" in o || "message" in o;
|
|
1758
|
+
}
|
|
1759
|
+
function mapUnknownErrorToAnthropicResponse(err, res, requestId) {
|
|
1760
|
+
if (looksLikeApiErrorResponse(err)) {
|
|
1761
|
+
const status = err.status >= 400 && err.status < 600 ? err.status : 500;
|
|
1762
|
+
const message2 = extractErrorMessage(err) ?? "Request failed";
|
|
1763
|
+
const errorType = httpStatusToAnthropicErrorType(status);
|
|
1764
|
+
sendAnthropicHttpError(res, status, errorType, message2, requestId);
|
|
1765
|
+
return;
|
|
1766
|
+
}
|
|
1767
|
+
const message = extractErrorMessage(err) ?? (err instanceof Error ? err.message : "Internal server error");
|
|
1768
|
+
sendAnthropicHttpError(res, 500, "api_error", message, requestId);
|
|
1769
|
+
}
|
|
1770
|
+
function writeAnthropicSseEvent(res, event, data) {
|
|
1771
|
+
res.write(`event: ${event}
|
|
1772
|
+
data: ${JSON.stringify(data)}
|
|
1773
|
+
|
|
1774
|
+
`);
|
|
1775
|
+
}
|
|
1776
|
+
|
|
1777
|
+
// src/anthropic/to-openai.ts
|
|
1778
|
+
class AnthropicRequestValidationError extends Error {
|
|
1779
|
+
status = 400;
|
|
1780
|
+
anthropicType = "invalid_request_error";
|
|
1781
|
+
constructor(message) {
|
|
1782
|
+
super(message);
|
|
1783
|
+
this.name = "AnthropicRequestValidationError";
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
function systemToOpenAiMessages(system) {
|
|
1787
|
+
if (typeof system === "string") {
|
|
1788
|
+
if (system.length === 0) {
|
|
1789
|
+
return [];
|
|
1790
|
+
}
|
|
1791
|
+
return [{ role: "system", content: system }];
|
|
1792
|
+
}
|
|
1793
|
+
if (Array.isArray(system)) {
|
|
1794
|
+
const parts = [];
|
|
1795
|
+
for (const block of system) {
|
|
1796
|
+
if (block && block.type === "text" && typeof block.text === "string") {
|
|
1797
|
+
parts.push(block.text);
|
|
1798
|
+
} else if (block && typeof block === "object") {
|
|
1799
|
+
console.warn(`[proxy] system block type "${block.type}" is not supported and will be ignored.`);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
if (parts.length === 0) {
|
|
1803
|
+
return [];
|
|
1804
|
+
}
|
|
1805
|
+
return [{ role: "system", content: parts.join(`
|
|
1806
|
+
|
|
1807
|
+
`) }];
|
|
1808
|
+
}
|
|
1809
|
+
if (system.type === "text" && typeof system.text === "string") {
|
|
1810
|
+
return [{ role: "system", content: system.text }];
|
|
1811
|
+
}
|
|
1812
|
+
throw new AnthropicRequestValidationError("Invalid system parameter shape.");
|
|
1813
|
+
}
|
|
1814
|
+
function toolResultContentToString(content) {
|
|
1815
|
+
if (typeof content === "string") {
|
|
1816
|
+
return content;
|
|
1817
|
+
}
|
|
1818
|
+
if (content === null || content === undefined) {
|
|
1819
|
+
return "";
|
|
1820
|
+
}
|
|
1821
|
+
if (Array.isArray(content)) {
|
|
1822
|
+
const parts = [];
|
|
1823
|
+
for (const block of content) {
|
|
1824
|
+
if (block && typeof block === "object" && "type" in block && block.type === "text" && typeof block.text === "string") {
|
|
1825
|
+
parts.push(block.text);
|
|
1826
|
+
} else {
|
|
1827
|
+
parts.push(JSON.stringify(block));
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
return parts.join(`
|
|
1831
|
+
`);
|
|
1832
|
+
}
|
|
1833
|
+
return JSON.stringify(content);
|
|
1834
|
+
}
|
|
1835
|
+
function anthropicImageBlockToOpenAIPart(part) {
|
|
1836
|
+
const source = part.source;
|
|
1837
|
+
if (!source || typeof source !== "object") {
|
|
1838
|
+
return null;
|
|
1839
|
+
}
|
|
1840
|
+
const s = source;
|
|
1841
|
+
if (s.type === "base64" && typeof s.data === "string" && s.data.length > 0) {
|
|
1842
|
+
const mediaType = typeof s.media_type === "string" && s.media_type.length > 0 ? s.media_type : "image/png";
|
|
1843
|
+
return {
|
|
1844
|
+
type: "image_url",
|
|
1845
|
+
image_url: { url: `data:${mediaType};base64,${s.data}` }
|
|
1846
|
+
};
|
|
1847
|
+
}
|
|
1848
|
+
if (s.type === "url" && typeof s.url === "string" && s.url.length > 0) {
|
|
1849
|
+
return { type: "image_url", image_url: { url: s.url } };
|
|
1850
|
+
}
|
|
1851
|
+
return null;
|
|
1852
|
+
}
|
|
1853
|
+
function anthropicUserContentToOpenAIMessages(content) {
|
|
1854
|
+
if (typeof content === "string") {
|
|
1855
|
+
return [{ role: "user", content }];
|
|
1856
|
+
}
|
|
1857
|
+
const out = [];
|
|
1858
|
+
const partsBuf = [];
|
|
1859
|
+
const flushParts = () => {
|
|
1860
|
+
if (partsBuf.length === 0) {
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1863
|
+
if (partsBuf.length === 1 && partsBuf[0].type === "text") {
|
|
1864
|
+
out.push({ role: "user", content: partsBuf[0].text });
|
|
1865
|
+
} else {
|
|
1866
|
+
out.push({ role: "user", content: [...partsBuf] });
|
|
1867
|
+
}
|
|
1868
|
+
partsBuf.length = 0;
|
|
1869
|
+
};
|
|
1870
|
+
for (const part of content) {
|
|
1871
|
+
if (!part || typeof part !== "object") {
|
|
1872
|
+
throw new AnthropicRequestValidationError("Invalid message content entry.");
|
|
1873
|
+
}
|
|
1874
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
1875
|
+
partsBuf.push({
|
|
1876
|
+
type: "text",
|
|
1877
|
+
text: part.text
|
|
1878
|
+
});
|
|
1879
|
+
continue;
|
|
1880
|
+
}
|
|
1881
|
+
if (part.type === "image") {
|
|
1882
|
+
const imgPart = anthropicImageBlockToOpenAIPart(part);
|
|
1883
|
+
if (imgPart) {
|
|
1884
|
+
partsBuf.push(imgPart);
|
|
1885
|
+
}
|
|
1886
|
+
continue;
|
|
1887
|
+
}
|
|
1888
|
+
if (part.type === "tool_result") {
|
|
1889
|
+
flushParts();
|
|
1890
|
+
const id = part.tool_use_id;
|
|
1891
|
+
const rawContent = part.content;
|
|
1892
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
1893
|
+
throw new AnthropicRequestValidationError("tool_result blocks require a non-empty tool_use_id.");
|
|
1894
|
+
}
|
|
1895
|
+
out.push({
|
|
1896
|
+
role: "tool",
|
|
1897
|
+
tool_call_id: id,
|
|
1898
|
+
content: toolResultContentToString(rawContent)
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
flushParts();
|
|
1903
|
+
return out;
|
|
1904
|
+
}
|
|
1905
|
+
function anthropicAssistantContentToOpenAI(content) {
|
|
1906
|
+
if (typeof content === "string") {
|
|
1907
|
+
return { role: "assistant", content };
|
|
1908
|
+
}
|
|
1909
|
+
const textParts = [];
|
|
1910
|
+
const toolCalls = [];
|
|
1911
|
+
for (const part of content) {
|
|
1912
|
+
if (!part || typeof part !== "object") {
|
|
1913
|
+
throw new AnthropicRequestValidationError("Invalid message content entry.");
|
|
1914
|
+
}
|
|
1915
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
1916
|
+
textParts.push(part.text);
|
|
1917
|
+
continue;
|
|
1918
|
+
}
|
|
1919
|
+
if (part.type === "tool_use") {
|
|
1920
|
+
const p = part;
|
|
1921
|
+
if (typeof p.id !== "string" || p.id.length === 0) {
|
|
1922
|
+
throw new AnthropicRequestValidationError("tool_use blocks require a non-empty id.");
|
|
1923
|
+
}
|
|
1924
|
+
if (typeof p.name !== "string" || p.name.length === 0) {
|
|
1925
|
+
throw new AnthropicRequestValidationError("tool_use blocks require a non-empty name.");
|
|
1926
|
+
}
|
|
1927
|
+
const args = typeof p.input === "string" ? p.input : JSON.stringify(p.input ?? {});
|
|
1928
|
+
toolCalls.push({
|
|
1929
|
+
id: p.id,
|
|
1930
|
+
type: "function",
|
|
1931
|
+
function: { name: p.name, arguments: args }
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
const msg = {
|
|
1936
|
+
role: "assistant",
|
|
1937
|
+
content: textParts.length > 0 ? textParts.join(`
|
|
1938
|
+
`) : null
|
|
1939
|
+
};
|
|
1940
|
+
if (toolCalls.length > 0) {
|
|
1941
|
+
msg.tool_calls = toolCalls;
|
|
1942
|
+
}
|
|
1943
|
+
return msg;
|
|
1944
|
+
}
|
|
1945
|
+
function anthropicToolsToOpenAI(tools) {
|
|
1946
|
+
if (tools === undefined) {
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
if (!Array.isArray(tools)) {
|
|
1950
|
+
throw new AnthropicRequestValidationError("tools must be an array.");
|
|
1951
|
+
}
|
|
1952
|
+
const out = [];
|
|
1953
|
+
for (const t of tools) {
|
|
1954
|
+
if (!t || typeof t !== "object") {
|
|
1955
|
+
throw new AnthropicRequestValidationError("Invalid tool entry.");
|
|
1956
|
+
}
|
|
1957
|
+
const name = t.name;
|
|
1958
|
+
const desc = t.description;
|
|
1959
|
+
const schema = t.input_schema;
|
|
1960
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
1961
|
+
throw new AnthropicRequestValidationError("Each tool must include a non-empty name.");
|
|
1962
|
+
}
|
|
1963
|
+
if (schema !== undefined && (typeof schema !== "object" || schema === null)) {
|
|
1964
|
+
throw new AnthropicRequestValidationError("tool input_schema must be an object when provided.");
|
|
1965
|
+
}
|
|
1966
|
+
out.push({
|
|
1967
|
+
type: "function",
|
|
1968
|
+
function: {
|
|
1969
|
+
name,
|
|
1970
|
+
...typeof desc === "string" ? { description: desc } : {},
|
|
1971
|
+
parameters: schema ?? {
|
|
1972
|
+
type: "object",
|
|
1973
|
+
properties: {}
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
return out;
|
|
1979
|
+
}
|
|
1980
|
+
function anthropicToolChoiceToOpenAI(toolChoice) {
|
|
1981
|
+
if (toolChoice === undefined) {
|
|
1982
|
+
return;
|
|
1983
|
+
}
|
|
1984
|
+
if (typeof toolChoice !== "object" || toolChoice === null || !("type" in toolChoice)) {
|
|
1985
|
+
throw new AnthropicRequestValidationError("Invalid tool_choice shape.");
|
|
1986
|
+
}
|
|
1987
|
+
const tc = toolChoice;
|
|
1988
|
+
switch (tc.type) {
|
|
1989
|
+
case "auto":
|
|
1990
|
+
return "auto";
|
|
1991
|
+
case "none":
|
|
1992
|
+
return "none";
|
|
1993
|
+
case "any":
|
|
1994
|
+
return "required";
|
|
1995
|
+
case "tool": {
|
|
1996
|
+
if (typeof tc.name !== "string" || tc.name.length === 0) {
|
|
1997
|
+
throw new AnthropicRequestValidationError('tool_choice type "tool" requires a non-empty name.');
|
|
1998
|
+
}
|
|
1999
|
+
return { type: "function", function: { name: tc.name } };
|
|
2000
|
+
}
|
|
2001
|
+
default:
|
|
2002
|
+
throw new AnthropicRequestValidationError(`Unsupported tool_choice type "${tc.type}".`);
|
|
2003
|
+
}
|
|
2004
|
+
}
|
|
2005
|
+
function anthropicMessagesCreateToOpenAI(body) {
|
|
2006
|
+
if (typeof body.model !== "string" || !body.model) {
|
|
2007
|
+
throw new AnthropicRequestValidationError("model is required.");
|
|
2008
|
+
}
|
|
2009
|
+
if (typeof body.max_tokens !== "number" || !Number.isFinite(body.max_tokens)) {
|
|
2010
|
+
throw new AnthropicRequestValidationError("max_tokens is required and must be a number.");
|
|
2011
|
+
}
|
|
2012
|
+
if (!Array.isArray(body.messages)) {
|
|
2013
|
+
throw new AnthropicRequestValidationError("messages must be an array.");
|
|
2014
|
+
}
|
|
2015
|
+
const messages = [];
|
|
2016
|
+
if (body.system !== undefined) {
|
|
2017
|
+
messages.push(...systemToOpenAiMessages(body.system));
|
|
2018
|
+
}
|
|
2019
|
+
for (const m of body.messages) {
|
|
2020
|
+
if (m.role !== "user" && m.role !== "assistant") {
|
|
2021
|
+
throw new AnthropicRequestValidationError(`Invalid message role "${m.role}".`);
|
|
2022
|
+
}
|
|
2023
|
+
if (m.role === "user") {
|
|
2024
|
+
messages.push(...anthropicUserContentToOpenAIMessages(m.content));
|
|
2025
|
+
} else {
|
|
2026
|
+
messages.push(anthropicAssistantContentToOpenAI(m.content));
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
const isStreaming = Boolean(body.stream);
|
|
2030
|
+
const params = {
|
|
2031
|
+
model: body.model,
|
|
2032
|
+
messages,
|
|
2033
|
+
max_tokens: body.max_tokens,
|
|
2034
|
+
stream: isStreaming
|
|
2035
|
+
};
|
|
2036
|
+
if (isStreaming) {
|
|
2037
|
+
params.stream_options = { include_usage: true };
|
|
2038
|
+
}
|
|
2039
|
+
const tools = anthropicToolsToOpenAI(body.tools);
|
|
2040
|
+
if (tools !== undefined && tools.length > 0) {
|
|
2041
|
+
params.tools = tools;
|
|
2042
|
+
}
|
|
2043
|
+
const toolChoice = anthropicToolChoiceToOpenAI(body.tool_choice);
|
|
2044
|
+
if (toolChoice !== undefined) {
|
|
2045
|
+
params.tool_choice = toolChoice;
|
|
2046
|
+
}
|
|
2047
|
+
if (body.stop_sequences !== undefined) {
|
|
2048
|
+
if (!Array.isArray(body.stop_sequences) || !body.stop_sequences.every((s) => typeof s === "string")) {
|
|
2049
|
+
throw new AnthropicRequestValidationError("stop_sequences must be an array of strings.");
|
|
2050
|
+
}
|
|
2051
|
+
params.stop = body.stop_sequences;
|
|
2052
|
+
}
|
|
2053
|
+
if (typeof body.temperature === "number") {
|
|
2054
|
+
params.temperature = body.temperature;
|
|
2055
|
+
}
|
|
2056
|
+
if (typeof body.top_p === "number") {
|
|
2057
|
+
params.top_p = body.top_p;
|
|
2058
|
+
}
|
|
2059
|
+
if (typeof body.top_k === "number") {
|
|
2060
|
+
console.warn("[proxy] top_k is not supported by the OpenAI API and will be ignored.");
|
|
2061
|
+
}
|
|
2062
|
+
return params;
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
// src/anthropic/count-tokens-route.ts
|
|
2066
|
+
function extractTextCharCount(body) {
|
|
2067
|
+
let len = 0;
|
|
2068
|
+
if (typeof body.system === "string") {
|
|
2069
|
+
len += body.system.length;
|
|
2070
|
+
} else if (Array.isArray(body.system)) {
|
|
2071
|
+
for (const block of body.system) {
|
|
2072
|
+
if (block && block.type === "text" && typeof block.text === "string") {
|
|
2073
|
+
len += block.text.length;
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
} else if (body.system && typeof body.system === "object" && body.system.type === "text") {
|
|
2077
|
+
len += body.system.text.length;
|
|
2078
|
+
}
|
|
2079
|
+
for (const msg of body.messages) {
|
|
2080
|
+
if (typeof msg.content === "string") {
|
|
2081
|
+
len += msg.content.length;
|
|
2082
|
+
} else if (Array.isArray(msg.content)) {
|
|
2083
|
+
for (const part of msg.content) {
|
|
2084
|
+
if (!part || typeof part !== "object")
|
|
2085
|
+
continue;
|
|
2086
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
2087
|
+
len += part.text.length;
|
|
2088
|
+
} else if (part.type === "tool_result") {
|
|
2089
|
+
const c = part.content;
|
|
2090
|
+
if (typeof c === "string") {
|
|
2091
|
+
len += c.length;
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
if (Array.isArray(body.tools)) {
|
|
2098
|
+
len += JSON.stringify(body.tools).length;
|
|
2099
|
+
}
|
|
2100
|
+
return len;
|
|
2101
|
+
}
|
|
2102
|
+
function registerAnthropicCountTokensRoute(router, _deps) {
|
|
2103
|
+
router.post("/v1/messages/count_tokens", async (req, res) => {
|
|
2104
|
+
const requestId = newAnthropicRequestId();
|
|
2105
|
+
res.setHeader("request-id", requestId);
|
|
2106
|
+
const versionResult = resolveAnthropicVersion(req);
|
|
2107
|
+
if (!versionResult.ok) {
|
|
2108
|
+
return sendAnthropicHttpError(res, 400, "invalid_request_error", versionResult.message, requestId);
|
|
2109
|
+
}
|
|
2110
|
+
const apiKey = extractAnthropicApiKey(req);
|
|
2111
|
+
if (!apiKey) {
|
|
2112
|
+
return sendAnthropicHttpError(res, 401, "authentication_error", "Missing x-api-key header (or Authorization with API key).", requestId);
|
|
2113
|
+
}
|
|
2114
|
+
try {
|
|
2115
|
+
const raw = req.body;
|
|
2116
|
+
const body = {
|
|
2117
|
+
...raw,
|
|
2118
|
+
max_tokens: typeof raw.max_tokens === "number" && Number.isFinite(raw.max_tokens) ? raw.max_tokens : 4096,
|
|
2119
|
+
stream: false
|
|
2120
|
+
};
|
|
2121
|
+
anthropicMessagesCreateToOpenAI(body);
|
|
2122
|
+
const input_tokens = Math.max(1, Math.ceil(extractTextCharCount(body) / 4));
|
|
2123
|
+
res.json({ input_tokens });
|
|
2124
|
+
} catch (err) {
|
|
2125
|
+
if (err instanceof AnthropicRequestValidationError) {
|
|
2126
|
+
return sendAnthropicHttpError(res, err.status, err.anthropicType, err.message, requestId);
|
|
2127
|
+
}
|
|
2128
|
+
mapUnknownErrorToAnthropicResponse(err, res, requestId);
|
|
2129
|
+
}
|
|
2130
|
+
});
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2133
|
+
// src/anthropic/from-openai.ts
|
|
2134
|
+
function openAiFinishReasonToAnthropic(finish) {
|
|
2135
|
+
if (!finish) {
|
|
2136
|
+
return { stop_reason: null, stop_sequence: null };
|
|
2137
|
+
}
|
|
2138
|
+
switch (finish) {
|
|
2139
|
+
case "stop":
|
|
2140
|
+
return { stop_reason: "end_turn", stop_sequence: null };
|
|
2141
|
+
case "length":
|
|
2142
|
+
return { stop_reason: "max_tokens", stop_sequence: null };
|
|
2143
|
+
case "tool_calls":
|
|
2144
|
+
return { stop_reason: "tool_use", stop_sequence: null };
|
|
2145
|
+
case "content_filter":
|
|
2146
|
+
return { stop_reason: "refusal", stop_sequence: null };
|
|
2147
|
+
default:
|
|
2148
|
+
return { stop_reason: "end_turn", stop_sequence: null };
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
function extractTextFromAssistantContent(content) {
|
|
2152
|
+
if (content == null) {
|
|
2153
|
+
return "";
|
|
2154
|
+
}
|
|
2155
|
+
if (typeof content === "string") {
|
|
2156
|
+
return content;
|
|
2157
|
+
}
|
|
2158
|
+
if (!Array.isArray(content)) {
|
|
2159
|
+
return "";
|
|
2160
|
+
}
|
|
2161
|
+
const parts = [];
|
|
2162
|
+
for (const p of content) {
|
|
2163
|
+
if (typeof p === "string") {
|
|
2164
|
+
parts.push(p);
|
|
2165
|
+
continue;
|
|
2166
|
+
}
|
|
2167
|
+
if (p && typeof p === "object" && "type" in p && p.type === "text" && "text" in p) {
|
|
2168
|
+
parts.push(String(p.text));
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
return parts.join("");
|
|
2172
|
+
}
|
|
2173
|
+
function openAIChatCompletionToAnthropicMessage(completion, requestModel) {
|
|
2174
|
+
const choice = completion.choices[0];
|
|
2175
|
+
const message = choice?.message;
|
|
2176
|
+
const contentText = message ? extractTextFromAssistantContent(message.content) : "";
|
|
2177
|
+
const content = [];
|
|
2178
|
+
if (contentText.length > 0) {
|
|
2179
|
+
content.push({ type: "text", text: contentText });
|
|
2180
|
+
}
|
|
2181
|
+
if (message?.tool_calls?.length) {
|
|
2182
|
+
for (const tc of message.tool_calls) {
|
|
2183
|
+
if (tc.type !== "function") {
|
|
2184
|
+
continue;
|
|
2185
|
+
}
|
|
2186
|
+
let input = {};
|
|
2187
|
+
try {
|
|
2188
|
+
input = JSON.parse(tc.function.arguments || "{}");
|
|
2189
|
+
} catch {
|
|
2190
|
+
input = { _raw_arguments: tc.function.arguments ?? "" };
|
|
2191
|
+
}
|
|
2192
|
+
content.push({
|
|
2193
|
+
type: "tool_use",
|
|
2194
|
+
id: tc.id,
|
|
2195
|
+
name: tc.function.name,
|
|
2196
|
+
input
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
}
|
|
2200
|
+
if (content.length === 0) {
|
|
2201
|
+
content.push({ type: "text", text: "" });
|
|
2202
|
+
}
|
|
2203
|
+
const { stop_reason, stop_sequence } = openAiFinishReasonToAnthropic(choice?.finish_reason);
|
|
2204
|
+
const u = completion.usage;
|
|
2205
|
+
const usage = {
|
|
2206
|
+
input_tokens: u?.prompt_tokens ?? 0,
|
|
2207
|
+
output_tokens: u?.completion_tokens ?? 0
|
|
2208
|
+
};
|
|
2209
|
+
return {
|
|
2210
|
+
id: newAnthropicMessageId(),
|
|
2211
|
+
type: "message",
|
|
2212
|
+
role: "assistant",
|
|
2213
|
+
content,
|
|
2214
|
+
model: requestModel,
|
|
2215
|
+
stop_reason,
|
|
2216
|
+
stop_sequence,
|
|
2217
|
+
usage
|
|
2218
|
+
};
|
|
2219
|
+
}
|
|
2220
|
+
function chunkFinishToAnthropic(finish) {
|
|
2221
|
+
if (!finish) {
|
|
2222
|
+
return null;
|
|
2223
|
+
}
|
|
2224
|
+
return openAiFinishReasonToAnthropic(finish).stop_reason;
|
|
2225
|
+
}
|
|
2226
|
+
async function pipeOpenAIChunkStreamToAnthropicSse(res, stream, options) {
|
|
2227
|
+
const { anthropicModel, messageId } = options;
|
|
2228
|
+
let textBlockOpen = false;
|
|
2229
|
+
let inputTokens = 0;
|
|
2230
|
+
let outputTokens = 0;
|
|
2231
|
+
let stopReason = null;
|
|
2232
|
+
const toolStates = new Map;
|
|
2233
|
+
let nextAnthropicIndex = 0;
|
|
2234
|
+
let textBlockIndex = null;
|
|
2235
|
+
writeAnthropicSseEvent(res, "message_start", {
|
|
2236
|
+
type: "message_start",
|
|
2237
|
+
message: {
|
|
2238
|
+
id: messageId,
|
|
2239
|
+
type: "message",
|
|
2240
|
+
role: "assistant",
|
|
2241
|
+
content: [],
|
|
2242
|
+
model: anthropicModel,
|
|
2243
|
+
stop_reason: null,
|
|
2244
|
+
stop_sequence: null,
|
|
2245
|
+
usage: { input_tokens: inputTokens, output_tokens: outputTokens }
|
|
2246
|
+
}
|
|
2247
|
+
});
|
|
2248
|
+
const ensureTextBlock = () => {
|
|
2249
|
+
if (textBlockOpen) {
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
textBlockIndex = nextAnthropicIndex++;
|
|
2253
|
+
textBlockOpen = true;
|
|
2254
|
+
writeAnthropicSseEvent(res, "content_block_start", {
|
|
2255
|
+
type: "content_block_start",
|
|
2256
|
+
index: textBlockIndex,
|
|
2257
|
+
content_block: { type: "text", text: "" }
|
|
2258
|
+
});
|
|
2259
|
+
};
|
|
2260
|
+
const closeTextBlockIfOpen = () => {
|
|
2261
|
+
if (!textBlockOpen || textBlockIndex === null) {
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
writeAnthropicSseEvent(res, "content_block_stop", {
|
|
2265
|
+
type: "content_block_stop",
|
|
2266
|
+
index: textBlockIndex
|
|
2267
|
+
});
|
|
2268
|
+
textBlockOpen = false;
|
|
2269
|
+
};
|
|
2270
|
+
const getOrCreateTool = (openAiIdx) => {
|
|
2271
|
+
let st = toolStates.get(openAiIdx);
|
|
2272
|
+
if (!st) {
|
|
2273
|
+
st = {
|
|
2274
|
+
anthropicIndex: nextAnthropicIndex++,
|
|
2275
|
+
id: "",
|
|
2276
|
+
name: "",
|
|
2277
|
+
lastArgs: "",
|
|
2278
|
+
argsEmittedLen: 0,
|
|
2279
|
+
started: false,
|
|
2280
|
+
stopped: false
|
|
2281
|
+
};
|
|
2282
|
+
toolStates.set(openAiIdx, st);
|
|
2283
|
+
}
|
|
2284
|
+
return st;
|
|
2285
|
+
};
|
|
2286
|
+
const flushToolArgs = (st) => {
|
|
2287
|
+
if (!st.started || st.lastArgs.length <= st.argsEmittedLen) {
|
|
2288
|
+
return;
|
|
2289
|
+
}
|
|
2290
|
+
const partial = st.lastArgs.slice(st.argsEmittedLen);
|
|
2291
|
+
st.argsEmittedLen = st.lastArgs.length;
|
|
2292
|
+
writeAnthropicSseEvent(res, "content_block_delta", {
|
|
2293
|
+
type: "content_block_delta",
|
|
2294
|
+
index: st.anthropicIndex,
|
|
2295
|
+
delta: {
|
|
2296
|
+
type: "input_json_delta",
|
|
2297
|
+
partial_json: partial
|
|
2298
|
+
}
|
|
2299
|
+
});
|
|
2300
|
+
};
|
|
2301
|
+
try {
|
|
2302
|
+
for await (const chunk of stream) {
|
|
2303
|
+
if (chunk.usage) {
|
|
2304
|
+
const u = chunk.usage;
|
|
2305
|
+
inputTokens = u.prompt_tokens ?? inputTokens;
|
|
2306
|
+
outputTokens = u.completion_tokens ?? outputTokens;
|
|
2307
|
+
}
|
|
2308
|
+
const choice = chunk.choices?.[0];
|
|
2309
|
+
if (!choice) {
|
|
2310
|
+
continue;
|
|
2311
|
+
}
|
|
2312
|
+
const delta = choice.delta;
|
|
2313
|
+
if (typeof delta?.content === "string" && delta.content.length > 0) {
|
|
2314
|
+
ensureTextBlock();
|
|
2315
|
+
if (textBlockIndex !== null) {
|
|
2316
|
+
writeAnthropicSseEvent(res, "content_block_delta", {
|
|
2317
|
+
type: "content_block_delta",
|
|
2318
|
+
index: textBlockIndex,
|
|
2319
|
+
delta: { type: "text_delta", text: delta.content }
|
|
2320
|
+
});
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
if (delta?.tool_calls?.length) {
|
|
2324
|
+
closeTextBlockIfOpen();
|
|
2325
|
+
for (const tc of delta.tool_calls) {
|
|
2326
|
+
const idx = typeof tc.index === "number" && Number.isFinite(tc.index) ? tc.index : 0;
|
|
2327
|
+
const st = getOrCreateTool(idx);
|
|
2328
|
+
if (typeof tc.id === "string" && tc.id.length > 0) {
|
|
2329
|
+
st.id = tc.id;
|
|
2330
|
+
}
|
|
2331
|
+
const fn = tc.function;
|
|
2332
|
+
if (fn?.name && fn.name.length > 0) {
|
|
2333
|
+
st.name = fn.name;
|
|
2334
|
+
}
|
|
2335
|
+
if (typeof fn?.arguments === "string") {
|
|
2336
|
+
st.lastArgs += fn.arguments;
|
|
2337
|
+
}
|
|
2338
|
+
if (!st.started && st.id.length > 0 && st.name.length > 0) {
|
|
2339
|
+
writeAnthropicSseEvent(res, "content_block_start", {
|
|
2340
|
+
type: "content_block_start",
|
|
2341
|
+
index: st.anthropicIndex,
|
|
2342
|
+
content_block: {
|
|
2343
|
+
type: "tool_use",
|
|
2344
|
+
id: st.id,
|
|
2345
|
+
name: st.name
|
|
2346
|
+
}
|
|
2347
|
+
});
|
|
2348
|
+
st.started = true;
|
|
2349
|
+
}
|
|
2350
|
+
if (st.started) {
|
|
2351
|
+
flushToolArgs(st);
|
|
2352
|
+
}
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
if (choice.finish_reason) {
|
|
2356
|
+
const mapped = chunkFinishToAnthropic(choice.finish_reason);
|
|
2357
|
+
if (mapped) {
|
|
2358
|
+
stopReason = mapped;
|
|
2359
|
+
}
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
closeTextBlockIfOpen();
|
|
2363
|
+
const sortedTools = [...toolStates.values()].sort((a, b) => a.anthropicIndex - b.anthropicIndex);
|
|
2364
|
+
for (const st of sortedTools) {
|
|
2365
|
+
if (st.started && !st.stopped) {
|
|
2366
|
+
writeAnthropicSseEvent(res, "content_block_stop", {
|
|
2367
|
+
type: "content_block_stop",
|
|
2368
|
+
index: st.anthropicIndex
|
|
2369
|
+
});
|
|
2370
|
+
st.stopped = true;
|
|
2371
|
+
}
|
|
2372
|
+
}
|
|
2373
|
+
writeAnthropicSseEvent(res, "message_delta", {
|
|
2374
|
+
type: "message_delta",
|
|
2375
|
+
delta: { stop_reason: stopReason, stop_sequence: null },
|
|
2376
|
+
usage: {
|
|
2377
|
+
input_tokens: inputTokens,
|
|
2378
|
+
output_tokens: outputTokens
|
|
2379
|
+
}
|
|
2380
|
+
});
|
|
2381
|
+
writeAnthropicSseEvent(res, "message_stop", { type: "message_stop" });
|
|
2382
|
+
res.end();
|
|
2383
|
+
} catch (err) {
|
|
2384
|
+
const message = err instanceof Error ? err.message : "Stream error";
|
|
2385
|
+
writeAnthropicSseEvent(res, "error", {
|
|
2386
|
+
type: "error",
|
|
2387
|
+
error: { type: "api_error", message }
|
|
2388
|
+
});
|
|
2389
|
+
res.end();
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
// src/anthropic/messages-route.ts
|
|
2394
|
+
function registerAnthropicMessagesRoute(router, deps) {
|
|
2395
|
+
router.post("/v1/messages", async (req, res) => {
|
|
2396
|
+
const requestId = newAnthropicRequestId();
|
|
2397
|
+
res.setHeader("request-id", requestId);
|
|
2398
|
+
const versionResult = resolveAnthropicVersion(req);
|
|
2399
|
+
if (!versionResult.ok) {
|
|
2400
|
+
return sendAnthropicHttpError(res, 400, "invalid_request_error", versionResult.message, requestId);
|
|
2401
|
+
}
|
|
2402
|
+
const apiKey = extractAnthropicApiKey(req);
|
|
2403
|
+
if (!apiKey) {
|
|
2404
|
+
return sendAnthropicHttpError(res, 401, "authentication_error", "Missing x-api-key header (or Authorization with API key).", requestId);
|
|
2405
|
+
}
|
|
2406
|
+
try {
|
|
2407
|
+
const body = req.body;
|
|
2408
|
+
const openaiParams = anthropicMessagesCreateToOpenAI(body);
|
|
2409
|
+
const client = await deps.getOrCreateClient(apiKey);
|
|
2410
|
+
const completion = await client.chat.completions.create(openaiParams);
|
|
2411
|
+
if (body.stream) {
|
|
2412
|
+
res.status(200);
|
|
2413
|
+
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
|
2414
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
2415
|
+
res.setHeader("Connection", "keep-alive");
|
|
2416
|
+
if (completion && typeof completion === "object" && Symbol.asyncIterator in completion) {
|
|
2417
|
+
const messageId = newAnthropicMessageId();
|
|
2418
|
+
await pipeOpenAIChunkStreamToAnthropicSse(res, completion, {
|
|
2419
|
+
anthropicModel: body.model,
|
|
2420
|
+
messageId
|
|
2421
|
+
});
|
|
2422
|
+
} else {
|
|
2423
|
+
sendAnthropicHttpError(res, 500, "api_error", "Expected streamed completion", requestId);
|
|
2424
|
+
}
|
|
2425
|
+
return;
|
|
2426
|
+
}
|
|
2427
|
+
const message = openAIChatCompletionToAnthropicMessage(completion, body.model);
|
|
2428
|
+
res.json(message);
|
|
2429
|
+
} catch (err) {
|
|
2430
|
+
if (err instanceof AnthropicRequestValidationError) {
|
|
2431
|
+
return sendAnthropicHttpError(res, err.status, err.anthropicType, err.message, requestId);
|
|
2432
|
+
}
|
|
2433
|
+
mapUnknownErrorToAnthropicResponse(err, res, requestId);
|
|
2434
|
+
}
|
|
2435
|
+
});
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
// src/anthropic/models-route.ts
|
|
2439
|
+
function toAnthropicModel(model) {
|
|
2440
|
+
return {
|
|
2441
|
+
type: "model",
|
|
2442
|
+
id: model.model,
|
|
2443
|
+
display_name: model.name || model.model,
|
|
2444
|
+
created_at: model.created_at
|
|
2445
|
+
};
|
|
2446
|
+
}
|
|
2447
|
+
function filterEnabled(models) {
|
|
2448
|
+
return models.filter((m) => m.enabled !== 0);
|
|
2449
|
+
}
|
|
2450
|
+
function parseLimit(raw) {
|
|
2451
|
+
if (typeof raw !== "string" || raw.length === 0) {
|
|
2452
|
+
return 20;
|
|
2453
|
+
}
|
|
2454
|
+
const n = Number.parseInt(raw, 10);
|
|
2455
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
2456
|
+
return 20;
|
|
2457
|
+
}
|
|
2458
|
+
return Math.min(n, 1000);
|
|
2459
|
+
}
|
|
2460
|
+
function paginate(all, beforeId, afterId, limit) {
|
|
2461
|
+
let start = 0;
|
|
2462
|
+
let end = all.length;
|
|
2463
|
+
if (afterId) {
|
|
2464
|
+
const idx = all.findIndex((m) => m.id === afterId);
|
|
2465
|
+
if (idx >= 0) {
|
|
2466
|
+
start = idx + 1;
|
|
2467
|
+
}
|
|
1530
2468
|
}
|
|
2469
|
+
if (beforeId) {
|
|
2470
|
+
const idx = all.findIndex((m) => m.id === beforeId);
|
|
2471
|
+
if (idx >= 0) {
|
|
2472
|
+
end = idx;
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
const window = all.slice(start, end);
|
|
2476
|
+
const items = window.slice(0, limit);
|
|
2477
|
+
return { items, hasMore: window.length > items.length };
|
|
2478
|
+
}
|
|
2479
|
+
function registerAnthropicModelsRoute(router, deps) {
|
|
2480
|
+
router.get("/v1/models", async (req, res) => {
|
|
2481
|
+
const requestId = newAnthropicRequestId();
|
|
2482
|
+
res.setHeader("request-id", requestId);
|
|
2483
|
+
const versionResult = resolveAnthropicVersion(req);
|
|
2484
|
+
if (!versionResult.ok) {
|
|
2485
|
+
return sendAnthropicHttpError(res, 400, "invalid_request_error", versionResult.message, requestId);
|
|
2486
|
+
}
|
|
2487
|
+
const apiKey = extractAnthropicApiKey(req);
|
|
2488
|
+
if (!apiKey) {
|
|
2489
|
+
return sendAnthropicHttpError(res, 401, "authentication_error", "Missing x-api-key header (or Authorization with API key).", requestId);
|
|
2490
|
+
}
|
|
2491
|
+
try {
|
|
2492
|
+
const client = await deps.getOrCreateClient(apiKey);
|
|
2493
|
+
const type = typeof req.query.type === "string" ? req.query.type : undefined;
|
|
2494
|
+
const all = filterEnabled(await client.models.list({ type })).map(toAnthropicModel);
|
|
2495
|
+
const beforeId = typeof req.query.before_id === "string" ? req.query.before_id : undefined;
|
|
2496
|
+
const afterId = typeof req.query.after_id === "string" ? req.query.after_id : undefined;
|
|
2497
|
+
const limit = parseLimit(req.query.limit);
|
|
2498
|
+
const { items, hasMore } = paginate(all, beforeId, afterId, limit);
|
|
2499
|
+
res.json({
|
|
2500
|
+
data: items,
|
|
2501
|
+
first_id: items.length > 0 ? items[0].id : null,
|
|
2502
|
+
last_id: items.length > 0 ? items[items.length - 1].id : null,
|
|
2503
|
+
has_more: hasMore
|
|
2504
|
+
});
|
|
2505
|
+
} catch (err) {
|
|
2506
|
+
mapUnknownErrorToAnthropicResponse(err, res, requestId);
|
|
2507
|
+
}
|
|
2508
|
+
});
|
|
2509
|
+
router.get("/v1/models/:model_id", async (req, res) => {
|
|
2510
|
+
const requestId = newAnthropicRequestId();
|
|
2511
|
+
res.setHeader("request-id", requestId);
|
|
2512
|
+
const versionResult = resolveAnthropicVersion(req);
|
|
2513
|
+
if (!versionResult.ok) {
|
|
2514
|
+
return sendAnthropicHttpError(res, 400, "invalid_request_error", versionResult.message, requestId);
|
|
2515
|
+
}
|
|
2516
|
+
const apiKey = extractAnthropicApiKey(req);
|
|
2517
|
+
if (!apiKey) {
|
|
2518
|
+
return sendAnthropicHttpError(res, 401, "authentication_error", "Missing x-api-key header (or Authorization with API key).", requestId);
|
|
2519
|
+
}
|
|
2520
|
+
const modelId = req.params.model_id;
|
|
2521
|
+
if (!modelId) {
|
|
2522
|
+
return sendAnthropicHttpError(res, 400, "invalid_request_error", "Missing model id.", requestId);
|
|
2523
|
+
}
|
|
2524
|
+
try {
|
|
2525
|
+
const client = await deps.getOrCreateClient(apiKey);
|
|
2526
|
+
const found = filterEnabled(await client.models.list()).find((m) => m.model === modelId);
|
|
2527
|
+
if (!found) {
|
|
2528
|
+
return sendAnthropicHttpError(res, 404, "not_found_error", `Model "${modelId}" not found.`, requestId);
|
|
2529
|
+
}
|
|
2530
|
+
res.json(toAnthropicModel(found));
|
|
2531
|
+
} catch (err) {
|
|
2532
|
+
mapUnknownErrorToAnthropicResponse(err, res, requestId);
|
|
2533
|
+
}
|
|
2534
|
+
});
|
|
2535
|
+
}
|
|
2536
|
+
|
|
2537
|
+
// src/server/runtime.ts
|
|
2538
|
+
var import_multer = __toESM(require("multer"));
|
|
2539
|
+
var DEFAULT_HOST = process.env.HOST ?? "127.0.0.1";
|
|
2540
|
+
var DEFAULT_PORT = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 8000;
|
|
2541
|
+
var CLIENT_CACHE_MAX = (() => {
|
|
2542
|
+
let cacheTTL = 256;
|
|
2543
|
+
const raw = process.env.CLIENT_CACHE_MAX;
|
|
2544
|
+
if (raw) {
|
|
2545
|
+
const _cacheTTL = Number.parseInt(raw, 10);
|
|
2546
|
+
if (Number.isSafeInteger(_cacheTTL) && _cacheTTL > 0)
|
|
2547
|
+
cacheTTL = _cacheTTL;
|
|
2548
|
+
}
|
|
2549
|
+
return cacheTTL;
|
|
2550
|
+
})();
|
|
2551
|
+
var serverProxyUrl = process.env.PROXY_URL;
|
|
2552
|
+
var serverEnclaveUrl = process.env.ENCLAVE_URL;
|
|
2553
|
+
var serverKek = process.env.CLIENT_KEK;
|
|
2554
|
+
var serverAttest = true;
|
|
2555
|
+
var clientCache = new Map;
|
|
2556
|
+
var storage = import_multer.default.memoryStorage();
|
|
2557
|
+
var audioUpload = import_multer.default({
|
|
2558
|
+
storage,
|
|
2559
|
+
limits: { fileSize: 25 * 1024 * 1024 }
|
|
2560
|
+
});
|
|
2561
|
+
function applyServerOptions(options) {
|
|
2562
|
+
const { proxyUrl, enclaveUrl, kek, attest: attest2 } = options;
|
|
2563
|
+
serverAttest = attest2 !== false;
|
|
2564
|
+
if (proxyUrl) {
|
|
2565
|
+
serverProxyUrl = proxyUrl;
|
|
2566
|
+
}
|
|
2567
|
+
if (enclaveUrl) {
|
|
2568
|
+
serverEnclaveUrl = enclaveUrl;
|
|
2569
|
+
}
|
|
2570
|
+
if (kek) {
|
|
2571
|
+
serverKek = kek;
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
async function getOrCreateRvencClient(apiKey) {
|
|
2575
|
+
const existing = clientCache.get(apiKey);
|
|
2576
|
+
if (existing)
|
|
2577
|
+
return existing;
|
|
1531
2578
|
const client = await core_default({
|
|
1532
2579
|
apiKey,
|
|
1533
2580
|
clientKEK: serverKek,
|
|
@@ -1540,235 +2587,458 @@ async function getOrCreateClient(apiKey) {
|
|
|
1540
2587
|
}
|
|
1541
2588
|
});
|
|
1542
2589
|
clientCache.set(apiKey, client);
|
|
2590
|
+
if (clientCache.size > CLIENT_CACHE_MAX) {
|
|
2591
|
+
const oldest = clientCache.keys().next().value;
|
|
2592
|
+
if (oldest !== undefined)
|
|
2593
|
+
clientCache.delete(oldest);
|
|
2594
|
+
}
|
|
1543
2595
|
return client;
|
|
1544
2596
|
}
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
2597
|
+
|
|
2598
|
+
// src/openai/routes.ts
|
|
2599
|
+
function extractApiKey(req) {
|
|
2600
|
+
const authHeader = req.headers.authorization;
|
|
2601
|
+
if (!authHeader) {
|
|
2602
|
+
return null;
|
|
2603
|
+
}
|
|
2604
|
+
if (authHeader.startsWith("Bearer ")) {
|
|
2605
|
+
return authHeader.slice(7);
|
|
2606
|
+
}
|
|
2607
|
+
return authHeader;
|
|
2608
|
+
}
|
|
2609
|
+
function sendUnauthorized(res) {
|
|
2610
|
+
res.status(401).json({
|
|
2611
|
+
error: {
|
|
2612
|
+
message: 'Missing Authorization header. Expected format: "Bearer <api-key>" or "<api-key>"',
|
|
2613
|
+
type: "invalid_request_error",
|
|
2614
|
+
code: "invalid_api_key"
|
|
1551
2615
|
}
|
|
1552
2616
|
});
|
|
1553
|
-
}
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
type: "invalid_request_error",
|
|
1562
|
-
code: "invalid_api_key"
|
|
1563
|
-
}
|
|
1564
|
-
});
|
|
2617
|
+
}
|
|
2618
|
+
function sendServerError(res, error) {
|
|
2619
|
+
const err = error;
|
|
2620
|
+
res.status(err.status ?? 500).json({
|
|
2621
|
+
error: {
|
|
2622
|
+
message: err.message ?? "Internal server error",
|
|
2623
|
+
type: err.type ?? "server_error",
|
|
2624
|
+
code: err.code
|
|
1565
2625
|
}
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
2626
|
+
});
|
|
2627
|
+
}
|
|
2628
|
+
function openAIOwnedBy(modelId) {
|
|
2629
|
+
const slash = modelId.indexOf("/");
|
|
2630
|
+
if (slash > 0) {
|
|
2631
|
+
return modelId.slice(0, slash);
|
|
2632
|
+
}
|
|
2633
|
+
return "prem";
|
|
2634
|
+
}
|
|
2635
|
+
function isoToUnix(iso) {
|
|
2636
|
+
const t = Date.parse(iso);
|
|
2637
|
+
if (!Number.isFinite(t)) {
|
|
2638
|
+
return 0;
|
|
2639
|
+
}
|
|
2640
|
+
return Math.floor(t / 1000);
|
|
2641
|
+
}
|
|
2642
|
+
function registerOpenAICompatRoutes(router, deps) {
|
|
2643
|
+
router.get("/v1/models", async (req, res) => {
|
|
2644
|
+
try {
|
|
2645
|
+
const apiKey = extractApiKey(req);
|
|
2646
|
+
if (!apiKey) {
|
|
2647
|
+
return sendUnauthorized(res);
|
|
2648
|
+
}
|
|
2649
|
+
const client = await deps.getOrCreateClient(apiKey);
|
|
2650
|
+
const all = await client.models.list();
|
|
2651
|
+
const data = all.filter((m) => m.enabled !== 0).map((m) => ({
|
|
2652
|
+
id: m.model,
|
|
2653
|
+
object: "model",
|
|
2654
|
+
created: isoToUnix(m.created_at),
|
|
2655
|
+
owned_by: openAIOwnedBy(m.model)
|
|
2656
|
+
}));
|
|
2657
|
+
res.json({ object: "list", data });
|
|
2658
|
+
} catch (error) {
|
|
2659
|
+
sendServerError(res, error);
|
|
2660
|
+
}
|
|
2661
|
+
});
|
|
2662
|
+
router.post("/v1/chat/completions", async (req, res) => {
|
|
2663
|
+
try {
|
|
2664
|
+
const apiKey = extractApiKey(req);
|
|
2665
|
+
if (!apiKey) {
|
|
2666
|
+
return sendUnauthorized(res);
|
|
2667
|
+
}
|
|
2668
|
+
const client = await deps.getOrCreateClient(apiKey);
|
|
2669
|
+
const params = req.body;
|
|
2670
|
+
const completion = await client.chat.completions.create(params);
|
|
2671
|
+
if (params.stream) {
|
|
2672
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
2673
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
2674
|
+
res.setHeader("Connection", "keep-alive");
|
|
2675
|
+
if (completion && typeof completion === "object" && Symbol.asyncIterator in completion) {
|
|
2676
|
+
try {
|
|
2677
|
+
for await (const chunk of completion) {
|
|
2678
|
+
res.write(`data: ${JSON.stringify(chunk)}
|
|
1577
2679
|
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
res.write(`data: [DONE]
|
|
2680
|
+
`);
|
|
2681
|
+
}
|
|
2682
|
+
res.write(`data: [DONE]
|
|
1582
2683
|
|
|
1583
2684
|
`);
|
|
1584
|
-
res.end();
|
|
1585
|
-
} catch (error) {
|
|
1586
|
-
console.error("Streaming error:", error);
|
|
1587
|
-
if (!res.headersSent) {
|
|
1588
|
-
res.status(500).json({
|
|
1589
|
-
error: {
|
|
1590
|
-
message: error.message || "Streaming error",
|
|
1591
|
-
type: "server_error"
|
|
1592
|
-
}
|
|
1593
|
-
});
|
|
1594
|
-
} else {
|
|
1595
2685
|
res.end();
|
|
2686
|
+
} catch (streamErr) {
|
|
2687
|
+
if (!res.headersSent) {
|
|
2688
|
+
sendServerError(res, streamErr);
|
|
2689
|
+
} else {
|
|
2690
|
+
res.end();
|
|
2691
|
+
}
|
|
1596
2692
|
}
|
|
1597
|
-
}
|
|
1598
|
-
|
|
1599
|
-
res.write(`data: ${JSON.stringify(completion)}
|
|
2693
|
+
} else {
|
|
2694
|
+
res.write(`data: ${JSON.stringify(completion)}
|
|
1600
2695
|
|
|
1601
2696
|
`);
|
|
1602
|
-
|
|
2697
|
+
res.write(`data: [DONE]
|
|
1603
2698
|
|
|
1604
2699
|
`);
|
|
1605
|
-
|
|
2700
|
+
res.end();
|
|
2701
|
+
}
|
|
2702
|
+
} else {
|
|
2703
|
+
res.json(completion);
|
|
1606
2704
|
}
|
|
1607
|
-
}
|
|
1608
|
-
res
|
|
2705
|
+
} catch (error) {
|
|
2706
|
+
sendServerError(res, error);
|
|
1609
2707
|
}
|
|
1610
|
-
}
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
type: error.type || "server_error",
|
|
1617
|
-
code: error.code
|
|
2708
|
+
});
|
|
2709
|
+
router.post("/v1/audio/transcriptions", audioUpload.single("file"), async (req, res) => {
|
|
2710
|
+
try {
|
|
2711
|
+
const apiKey = extractApiKey(req);
|
|
2712
|
+
if (!apiKey) {
|
|
2713
|
+
return sendUnauthorized(res);
|
|
1618
2714
|
}
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
code: "invalid_api_key"
|
|
1631
|
-
}
|
|
2715
|
+
if (!req.file) {
|
|
2716
|
+
return res.status(400).json({
|
|
2717
|
+
error: {
|
|
2718
|
+
message: "Missing required file parameter",
|
|
2719
|
+
type: "invalid_request_error"
|
|
2720
|
+
}
|
|
2721
|
+
});
|
|
2722
|
+
}
|
|
2723
|
+
const client = await deps.getOrCreateClient(apiKey);
|
|
2724
|
+
const file = new File([req.file.buffer], req.file.originalname, {
|
|
2725
|
+
type: req.file.mimetype
|
|
1632
2726
|
});
|
|
2727
|
+
const params = {
|
|
2728
|
+
file,
|
|
2729
|
+
model: req.body.model
|
|
2730
|
+
};
|
|
2731
|
+
if (req.body.language) {
|
|
2732
|
+
params.language = req.body.language;
|
|
2733
|
+
}
|
|
2734
|
+
if (req.body.prompt) {
|
|
2735
|
+
params.prompt = req.body.prompt;
|
|
2736
|
+
}
|
|
2737
|
+
if (req.body.response_format) {
|
|
2738
|
+
params.response_format = req.body.response_format;
|
|
2739
|
+
}
|
|
2740
|
+
if (req.body.temperature) {
|
|
2741
|
+
params.temperature = parseFloat(req.body.temperature);
|
|
2742
|
+
}
|
|
2743
|
+
if (req.body.timestamp_granularities) {
|
|
2744
|
+
params.timestamp_granularities = Array.isArray(req.body.timestamp_granularities) ? req.body.timestamp_granularities : JSON.parse(req.body.timestamp_granularities);
|
|
2745
|
+
}
|
|
2746
|
+
const transcription = await client.audio.transcriptions.create(params);
|
|
2747
|
+
res.json(transcription);
|
|
2748
|
+
} catch (error) {
|
|
2749
|
+
sendServerError(res, error);
|
|
1633
2750
|
}
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
2751
|
+
});
|
|
2752
|
+
router.post("/v1/audio/translations", audioUpload.single("file"), async (req, res) => {
|
|
2753
|
+
try {
|
|
2754
|
+
const apiKey = extractApiKey(req);
|
|
2755
|
+
if (!apiKey) {
|
|
2756
|
+
return sendUnauthorized(res);
|
|
2757
|
+
}
|
|
2758
|
+
if (!req.file) {
|
|
2759
|
+
return res.status(400).json({
|
|
2760
|
+
error: {
|
|
2761
|
+
message: "Missing required file parameter",
|
|
2762
|
+
type: "invalid_request_error"
|
|
2763
|
+
}
|
|
2764
|
+
});
|
|
2765
|
+
}
|
|
2766
|
+
const client = await deps.getOrCreateClient(apiKey);
|
|
2767
|
+
const file = new File([req.file.buffer], req.file.originalname, {
|
|
2768
|
+
type: req.file.mimetype
|
|
1640
2769
|
});
|
|
2770
|
+
const params = {
|
|
2771
|
+
file,
|
|
2772
|
+
model: req.body.model
|
|
2773
|
+
};
|
|
2774
|
+
if (req.body.prompt) {
|
|
2775
|
+
params.prompt = req.body.prompt;
|
|
2776
|
+
}
|
|
2777
|
+
if (req.body.response_format) {
|
|
2778
|
+
params.response_format = req.body.response_format;
|
|
2779
|
+
}
|
|
2780
|
+
if (req.body.temperature) {
|
|
2781
|
+
params.temperature = parseFloat(req.body.temperature);
|
|
2782
|
+
}
|
|
2783
|
+
const translation = await client.audio.translations.create(params);
|
|
2784
|
+
res.json(translation);
|
|
2785
|
+
} catch (error) {
|
|
2786
|
+
sendServerError(res, error);
|
|
2787
|
+
}
|
|
2788
|
+
});
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
// src/server/route-prefix.ts
|
|
2792
|
+
function normalizeRoutePrefix(raw) {
|
|
2793
|
+
if (raw == null) {
|
|
2794
|
+
return "";
|
|
2795
|
+
}
|
|
2796
|
+
return new URL(String(raw).trim(), "http://localhost").pathname || "";
|
|
2797
|
+
}
|
|
2798
|
+
var DEFAULT_OPENAI_ROUTE_PREFIX_BOTH = "/openai";
|
|
2799
|
+
var DEFAULT_ANTHROPIC_ROUTE_PREFIX_BOTH = "/anthropic";
|
|
2800
|
+
function resolvePrefixesForCompat(compat, openaiRaw, anthropicRaw) {
|
|
2801
|
+
const oNorm = normalizeRoutePrefix(openaiRaw);
|
|
2802
|
+
const aNorm = normalizeRoutePrefix(anthropicRaw);
|
|
2803
|
+
if (compat === "both") {
|
|
2804
|
+
const openaiPrefix = oNorm || DEFAULT_OPENAI_ROUTE_PREFIX_BOTH;
|
|
2805
|
+
const anthropicPrefix = aNorm || DEFAULT_ANTHROPIC_ROUTE_PREFIX_BOTH;
|
|
2806
|
+
if (openaiPrefix === anthropicPrefix) {
|
|
2807
|
+
throw new Error(`When compat is "both", openaiRoutePrefix and anthropicRoutePrefix must differ (both resolved to "${openaiPrefix}").`);
|
|
2808
|
+
}
|
|
2809
|
+
return { openaiPrefix, anthropicPrefix };
|
|
2810
|
+
}
|
|
2811
|
+
if (compat === "openai") {
|
|
2812
|
+
return { openaiPrefix: oNorm, anthropicPrefix: "" };
|
|
2813
|
+
}
|
|
2814
|
+
return { openaiPrefix: "", anthropicPrefix: aNorm };
|
|
2815
|
+
}
|
|
2816
|
+
function prefixedRoute(prefix, path) {
|
|
2817
|
+
const s = path.startsWith("/") ? path : `/${path}`;
|
|
2818
|
+
if (!prefix) {
|
|
2819
|
+
return s;
|
|
2820
|
+
}
|
|
2821
|
+
return `${prefix}${s}`;
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
// src/server/discovery.ts
|
|
2825
|
+
function registerApiDiscoveryRoute(app, mount) {
|
|
2826
|
+
const {
|
|
2827
|
+
openai: mountOpenAI,
|
|
2828
|
+
anthropic: mountAnthropic,
|
|
2829
|
+
openaiPrefix,
|
|
2830
|
+
anthropicPrefix
|
|
2831
|
+
} = mount;
|
|
2832
|
+
app.get("/", (_, res) => {
|
|
2833
|
+
const endpoints2 = {};
|
|
2834
|
+
if (mountOpenAI) {
|
|
2835
|
+
endpoints2.chat_completions = `POST ${prefixedRoute(openaiPrefix, "/v1/chat/completions")}`;
|
|
2836
|
+
endpoints2.audio_transcriptions = `POST ${prefixedRoute(openaiPrefix, "/v1/audio/transcriptions")}`;
|
|
2837
|
+
endpoints2.audio_translations = `POST ${prefixedRoute(openaiPrefix, "/v1/audio/translations")}`;
|
|
2838
|
+
endpoints2.models = `GET ${prefixedRoute(openaiPrefix, "/v1/models")}`;
|
|
1641
2839
|
}
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
2840
|
+
if (mountAnthropic) {
|
|
2841
|
+
endpoints2.messages = `POST ${prefixedRoute(anthropicPrefix, "/v1/messages")}`;
|
|
2842
|
+
endpoints2.messages_count_tokens = `POST ${prefixedRoute(anthropicPrefix, "/v1/messages/count_tokens")}`;
|
|
2843
|
+
endpoints2.anthropic_models = `GET ${prefixedRoute(anthropicPrefix, "/v1/models")}`;
|
|
2844
|
+
endpoints2.anthropic_model_get = `GET ${prefixedRoute(anthropicPrefix, "/v1/models/{model_id}")}`;
|
|
2845
|
+
}
|
|
2846
|
+
const labels = [];
|
|
2847
|
+
if (mountOpenAI) {
|
|
2848
|
+
labels.push("OpenAI-compatible");
|
|
2849
|
+
}
|
|
2850
|
+
if (mountAnthropic) {
|
|
2851
|
+
labels.push("Anthropic Messages-compatible");
|
|
2852
|
+
}
|
|
2853
|
+
res.json({
|
|
2854
|
+
message: `Rvenc API Server (${labels.join(" + ")})`,
|
|
2855
|
+
version: "1.0.0",
|
|
2856
|
+
compat: resolveCompatLabel(mount),
|
|
2857
|
+
route_prefixes: buildRoutePrefixesPayload(mountOpenAI, mountAnthropic, openaiPrefix, anthropicPrefix),
|
|
2858
|
+
endpoints: endpoints2
|
|
1645
2859
|
});
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
2860
|
+
});
|
|
2861
|
+
}
|
|
2862
|
+
function buildRoutePrefixesPayload(mountOpenAI, mountAnthropic, openaiPrefix, anthropicPrefix) {
|
|
2863
|
+
const out = {};
|
|
2864
|
+
if (mountOpenAI) {
|
|
2865
|
+
out.openai = openaiPrefix || "/";
|
|
2866
|
+
}
|
|
2867
|
+
if (mountAnthropic) {
|
|
2868
|
+
out.anthropic = anthropicPrefix || "/";
|
|
2869
|
+
}
|
|
2870
|
+
if (Object.keys(out).length === 0) {
|
|
2871
|
+
return;
|
|
2872
|
+
}
|
|
2873
|
+
return out;
|
|
2874
|
+
}
|
|
2875
|
+
function resolveCompatLabel(mount) {
|
|
2876
|
+
if (mount.openai && mount.anthropic) {
|
|
2877
|
+
return "both";
|
|
2878
|
+
}
|
|
2879
|
+
if (mount.anthropic) {
|
|
2880
|
+
return "anthropic";
|
|
2881
|
+
}
|
|
2882
|
+
return "openai";
|
|
2883
|
+
}
|
|
2884
|
+
|
|
2885
|
+
// src/server/create-app.ts
|
|
2886
|
+
var rvencDeps = {
|
|
2887
|
+
getOrCreateClient: getOrCreateRvencClient
|
|
2888
|
+
};
|
|
2889
|
+
function resolveJsonBodyLimit(override) {
|
|
2890
|
+
if (override != null && String(override).trim() !== "") {
|
|
2891
|
+
return String(override).trim();
|
|
2892
|
+
}
|
|
2893
|
+
const env = process.env.JSON_BODY_LIMIT;
|
|
2894
|
+
if (env != null && env !== "") {
|
|
2895
|
+
return env;
|
|
2896
|
+
}
|
|
2897
|
+
return "32mb";
|
|
2898
|
+
}
|
|
2899
|
+
function resolveCreateServerInput(compatOrOptions) {
|
|
2900
|
+
if (typeof compatOrOptions === "string") {
|
|
2901
|
+
const compat2 = compatOrOptions;
|
|
2902
|
+
const { openaiPrefix: openaiPrefix2, anthropicPrefix: anthropicPrefix2 } = resolvePrefixesForCompat(compat2, undefined, undefined);
|
|
2903
|
+
return {
|
|
2904
|
+
compat: compat2,
|
|
2905
|
+
openaiPrefix: openaiPrefix2,
|
|
2906
|
+
anthropicPrefix: anthropicPrefix2,
|
|
2907
|
+
jsonBodyLimit: resolveJsonBodyLimit()
|
|
1649
2908
|
};
|
|
1650
|
-
if (req.body.language)
|
|
1651
|
-
params.language = req.body.language;
|
|
1652
|
-
if (req.body.prompt)
|
|
1653
|
-
params.prompt = req.body.prompt;
|
|
1654
|
-
if (req.body.response_format)
|
|
1655
|
-
params.response_format = req.body.response_format;
|
|
1656
|
-
if (req.body.temperature)
|
|
1657
|
-
params.temperature = parseFloat(req.body.temperature);
|
|
1658
|
-
if (req.body.timestamp_granularities) {
|
|
1659
|
-
params.timestamp_granularities = Array.isArray(req.body.timestamp_granularities) ? req.body.timestamp_granularities : JSON.parse(req.body.timestamp_granularities);
|
|
1660
|
-
}
|
|
1661
|
-
const transcription = await client.audio.transcriptions.create(params);
|
|
1662
|
-
res.json(transcription);
|
|
1663
|
-
} catch (error) {
|
|
1664
|
-
console.error("Audio transcription error:", error);
|
|
1665
|
-
const statusCode = error.status || 500;
|
|
1666
|
-
res.status(statusCode).json({
|
|
1667
|
-
error: {
|
|
1668
|
-
message: error.message || "Internal server error",
|
|
1669
|
-
type: error.type || "server_error",
|
|
1670
|
-
code: error.code
|
|
1671
|
-
}
|
|
1672
|
-
});
|
|
1673
2909
|
}
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
2910
|
+
const compat = compatOrOptions.compat ?? "openai";
|
|
2911
|
+
const { openaiPrefix, anthropicPrefix } = resolvePrefixesForCompat(compat, compatOrOptions.openaiRoutePrefix, compatOrOptions.anthropicRoutePrefix);
|
|
2912
|
+
return {
|
|
2913
|
+
compat,
|
|
2914
|
+
openaiPrefix,
|
|
2915
|
+
anthropicPrefix,
|
|
2916
|
+
jsonBodyLimit: resolveJsonBodyLimit(compatOrOptions.jsonBodyLimit)
|
|
2917
|
+
};
|
|
2918
|
+
}
|
|
2919
|
+
function httpErrorStatus(err) {
|
|
2920
|
+
if (err && typeof err === "object") {
|
|
2921
|
+
const o = err;
|
|
2922
|
+
const s = o.status ?? o.statusCode;
|
|
2923
|
+
if (typeof s === "number" && s >= 400 && s < 600) {
|
|
2924
|
+
return s;
|
|
1686
2925
|
}
|
|
1687
|
-
|
|
1688
|
-
|
|
2926
|
+
}
|
|
2927
|
+
return 500;
|
|
2928
|
+
}
|
|
2929
|
+
function mountRouter(app, prefix, router) {
|
|
2930
|
+
app.use(prefix || "/", router);
|
|
2931
|
+
}
|
|
2932
|
+
function createServerApp(compatOrOptions = "openai") {
|
|
2933
|
+
const { compat, openaiPrefix, anthropicPrefix, jsonBodyLimit } = resolveCreateServerInput(compatOrOptions);
|
|
2934
|
+
const mountOpenAI = compat === "openai" || compat === "both";
|
|
2935
|
+
const mountAnthropic = compat === "anthropic" || compat === "both";
|
|
2936
|
+
const app = import_express.default();
|
|
2937
|
+
app.use(import_express.default.json({ limit: jsonBodyLimit }));
|
|
2938
|
+
registerApiDiscoveryRoute(app, {
|
|
2939
|
+
openai: mountOpenAI,
|
|
2940
|
+
anthropic: mountAnthropic,
|
|
2941
|
+
openaiPrefix,
|
|
2942
|
+
anthropicPrefix
|
|
2943
|
+
});
|
|
2944
|
+
if (mountOpenAI) {
|
|
2945
|
+
const router = import_express.default.Router();
|
|
2946
|
+
registerOpenAICompatRoutes(router, rvencDeps);
|
|
2947
|
+
mountRouter(app, openaiPrefix, router);
|
|
2948
|
+
}
|
|
2949
|
+
if (mountAnthropic) {
|
|
2950
|
+
const router = import_express.default.Router();
|
|
2951
|
+
registerAnthropicMessagesRoute(router, rvencDeps);
|
|
2952
|
+
registerAnthropicCountTokensRoute(router, rvencDeps);
|
|
2953
|
+
registerAnthropicModelsRoute(router, rvencDeps);
|
|
2954
|
+
mountRouter(app, anthropicPrefix, router);
|
|
2955
|
+
}
|
|
2956
|
+
const isAnthropicRequest = (req) => {
|
|
2957
|
+
if (!mountAnthropic) {
|
|
2958
|
+
return false;
|
|
2959
|
+
}
|
|
2960
|
+
if (!mountOpenAI) {
|
|
2961
|
+
return true;
|
|
2962
|
+
}
|
|
2963
|
+
return req.path === anthropicPrefix || req.path.startsWith(`${anthropicPrefix}/`);
|
|
2964
|
+
};
|
|
2965
|
+
app.use((err, req, res, _next) => {
|
|
2966
|
+
const status = httpErrorStatus(err);
|
|
2967
|
+
const message = err instanceof Error ? err.message : "Internal server error";
|
|
2968
|
+
if (isAnthropicRequest(req)) {
|
|
2969
|
+
const requestId = newAnthropicRequestId();
|
|
2970
|
+
res.setHeader("request-id", requestId);
|
|
2971
|
+
res.status(status).json({
|
|
2972
|
+
type: "error",
|
|
1689
2973
|
error: {
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
}
|
|
2974
|
+
type: httpStatusToAnthropicErrorType(status),
|
|
2975
|
+
message
|
|
2976
|
+
},
|
|
2977
|
+
request_id: requestId
|
|
1693
2978
|
});
|
|
2979
|
+
return;
|
|
1694
2980
|
}
|
|
1695
|
-
|
|
1696
|
-
const file = new File([req.file.buffer], req.file.originalname, {
|
|
1697
|
-
type: req.file.mimetype
|
|
1698
|
-
});
|
|
1699
|
-
const params = {
|
|
1700
|
-
file,
|
|
1701
|
-
model: req.body.model
|
|
1702
|
-
};
|
|
1703
|
-
if (req.body.prompt)
|
|
1704
|
-
params.prompt = req.body.prompt;
|
|
1705
|
-
if (req.body.response_format)
|
|
1706
|
-
params.response_format = req.body.response_format;
|
|
1707
|
-
if (req.body.temperature)
|
|
1708
|
-
params.temperature = parseFloat(req.body.temperature);
|
|
1709
|
-
const translation = await client.audio.translations.create(params);
|
|
1710
|
-
res.json(translation);
|
|
1711
|
-
} catch (error) {
|
|
1712
|
-
console.error("Audio translation error:", error);
|
|
1713
|
-
const statusCode = error.status || 500;
|
|
1714
|
-
res.status(statusCode).json({
|
|
2981
|
+
res.status(status).json({
|
|
1715
2982
|
error: {
|
|
1716
|
-
message
|
|
1717
|
-
type:
|
|
1718
|
-
code: error.code
|
|
2983
|
+
message,
|
|
2984
|
+
type: "server_error"
|
|
1719
2985
|
}
|
|
1720
2986
|
});
|
|
1721
|
-
}
|
|
1722
|
-
});
|
|
1723
|
-
app.use((err, _req, res, _next) => {
|
|
1724
|
-
console.error(`Unhandled error: ${err}`);
|
|
1725
|
-
res.status(500).json({
|
|
1726
|
-
error: {
|
|
1727
|
-
message: err.message || "Internal server error",
|
|
1728
|
-
type: "server_error"
|
|
1729
|
-
}
|
|
1730
2987
|
});
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
2988
|
+
app.use((req, res) => {
|
|
2989
|
+
const message = `Route ${req.method} ${req.path} not found`;
|
|
2990
|
+
if (isAnthropicRequest(req)) {
|
|
2991
|
+
const requestId = newAnthropicRequestId();
|
|
2992
|
+
res.setHeader("request-id", requestId);
|
|
2993
|
+
res.status(404).json({
|
|
2994
|
+
type: "error",
|
|
2995
|
+
error: { type: "not_found_error", message },
|
|
2996
|
+
request_id: requestId
|
|
2997
|
+
});
|
|
2998
|
+
return;
|
|
1737
2999
|
}
|
|
3000
|
+
res.status(404).json({
|
|
3001
|
+
error: {
|
|
3002
|
+
message,
|
|
3003
|
+
type: "invalid_request_error"
|
|
3004
|
+
}
|
|
3005
|
+
});
|
|
1738
3006
|
});
|
|
1739
|
-
|
|
3007
|
+
return app;
|
|
3008
|
+
}
|
|
3009
|
+
// src/server/start.ts
|
|
1740
3010
|
async function startServer(options = {}) {
|
|
1741
|
-
const {
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
3011
|
+
const {
|
|
3012
|
+
host,
|
|
3013
|
+
port,
|
|
3014
|
+
compat: compatOpt,
|
|
3015
|
+
openaiRoutePrefix,
|
|
3016
|
+
anthropicRoutePrefix,
|
|
3017
|
+
jsonBodyLimit
|
|
3018
|
+
} = options;
|
|
3019
|
+
const serverHost = host || DEFAULT_HOST;
|
|
3020
|
+
const serverPort = port || DEFAULT_PORT;
|
|
3021
|
+
const compat = compatOpt ?? "openai";
|
|
3022
|
+
applyServerOptions(options);
|
|
3023
|
+
resolvePrefixesForCompat(compat, openaiRoutePrefix, anthropicRoutePrefix);
|
|
3024
|
+
const app = createServerApp({
|
|
3025
|
+
compat,
|
|
3026
|
+
openaiRoutePrefix,
|
|
3027
|
+
anthropicRoutePrefix,
|
|
3028
|
+
jsonBodyLimit
|
|
3029
|
+
});
|
|
1754
3030
|
return new Promise((resolve, reject) => {
|
|
1755
3031
|
const server = app.listen(serverPort, serverHost, () => {
|
|
1756
|
-
|
|
1757
|
-
Rvenc Server running on http://${serverHost}:${serverPort}`);
|
|
1758
|
-
resolve();
|
|
3032
|
+
resolve({ close: () => server.close() });
|
|
1759
3033
|
});
|
|
1760
3034
|
server.on("error", (error) => {
|
|
1761
|
-
if (error.code === "EADDRINUSE") {
|
|
1762
|
-
|
|
3035
|
+
if (error && typeof error === "object" && "code" in error && error.code === "EADDRINUSE") {
|
|
3036
|
+
reject(new Error(`Port ${serverPort} is already in use`));
|
|
1763
3037
|
} else {
|
|
1764
|
-
|
|
3038
|
+
reject(error);
|
|
1765
3039
|
}
|
|
1766
|
-
reject(error);
|
|
1767
3040
|
});
|
|
1768
3041
|
});
|
|
1769
3042
|
}
|
|
1770
|
-
|
|
1771
|
-
var server_default =
|
|
1772
|
-
|
|
1773
|
-
// src/index.ts
|
|
1774
|
-
import_dotenv2.default.config();
|
|
3043
|
+
// src/server.ts
|
|
3044
|
+
var server_default = createServerApp("both");
|