@jeffreycao/copilot-api 2.8.0 → 2.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/main.js +1 -1
- package/dist/{server-Bz8hGP4q.js → server-C_-cCnrU.js} +643 -242
- package/dist/server-C_-cCnrU.js.map +1 -0
- package/dist/{start-BBEryu3z.js → start-Da9ynTs-.js} +2 -2
- package/dist/{start-BBEryu3z.js.map → start-Da9ynTs-.js.map} +1 -1
- package/package.json +2 -1
- package/dist/server-Bz8hGP4q.js.map +0 -1
|
@@ -6,10 +6,10 @@ import { n as isProviderAgentModelVisible, t as getProviderAgentModels } from ".
|
|
|
6
6
|
import { i as registerProcessCleanup, n as stopModelsRefreshLoop, r as EventBus, t as setupCopilotRuntime } from "./copilot-runtime-DvmA6ALD.js";
|
|
7
7
|
import consola from "consola";
|
|
8
8
|
import { createHash, randomUUID } from "node:crypto";
|
|
9
|
-
import fs, { readFileSync } from "node:fs";
|
|
10
|
-
import path from "node:path";
|
|
11
|
-
import fs$1 from "node:fs/promises";
|
|
12
|
-
import os from "node:os";
|
|
9
|
+
import fs, { createWriteStream, readFileSync, rmSync } from "node:fs";
|
|
10
|
+
import path, { join } from "node:path";
|
|
11
|
+
import fs$1, { mkdtemp, rm } from "node:fs/promises";
|
|
12
|
+
import os, { tmpdir } from "node:os";
|
|
13
13
|
import { fetch as fetch$1, getGlobalDispatcher } from "undici";
|
|
14
14
|
import { events } from "fetch-event-stream";
|
|
15
15
|
import { z } from "zod";
|
|
@@ -18,6 +18,9 @@ import { cors } from "hono/cors";
|
|
|
18
18
|
import { logger } from "hono/logger";
|
|
19
19
|
import util from "node:util";
|
|
20
20
|
import { streamSSE } from "hono/streaming";
|
|
21
|
+
import { Busboy } from "@fastify/busboy";
|
|
22
|
+
import { Readable, Transform } from "node:stream";
|
|
23
|
+
import { pipeline } from "node:stream/promises";
|
|
21
24
|
import { decompress } from "fzstd";
|
|
22
25
|
// src/lib/trace.ts
|
|
23
26
|
const traceIdMiddleware = async (c, next) => {
|
|
@@ -2360,7 +2363,7 @@ async function forwardProviderImages(providerConfig, request, operation) {
|
|
|
2360
2363
|
});
|
|
2361
2364
|
}
|
|
2362
2365
|
// src/routes/alpha-search/alpha-search-responses.ts
|
|
2363
|
-
const logger$
|
|
2366
|
+
const logger$20 = createHandlerLogger("alpha-search-responses-handler");
|
|
2364
2367
|
const SESSION_TTL_MS = 36e5;
|
|
2365
2368
|
const MAX_SESSIONS = 128;
|
|
2366
2369
|
const MAX_URL_REFERENCES = 256;
|
|
@@ -2718,14 +2721,14 @@ function createRemoteResponsesPayload(request, model, instruction) {
|
|
|
2718
2721
|
};
|
|
2719
2722
|
}
|
|
2720
2723
|
async function requestProviderSearch(c, providerConfig, payload, model, sessionId) {
|
|
2721
|
-
debugJson(logger$
|
|
2724
|
+
debugJson(logger$20, "Alpha search provider Responses request:", {
|
|
2722
2725
|
payload,
|
|
2723
2726
|
provider: providerConfig.name
|
|
2724
2727
|
});
|
|
2725
2728
|
const upstreamResponse = await forwardProviderResponses(providerConfig, payload, c.req.raw.headers, { clientSignal: c.req.raw.signal });
|
|
2726
2729
|
if (!upstreamResponse.ok) throw new HTTPError(`Failed to create ${providerConfig.name} responses for alpha search`, upstreamResponse);
|
|
2727
2730
|
const result = await upstreamResponse.json();
|
|
2728
|
-
debugJson(logger$
|
|
2731
|
+
debugJson(logger$20, "Alpha search provider Responses result:", {
|
|
2729
2732
|
provider: providerConfig.name,
|
|
2730
2733
|
result
|
|
2731
2734
|
});
|
|
@@ -2741,7 +2744,7 @@ async function requestProviderSearch(c, providerConfig, payload, model, sessionI
|
|
|
2741
2744
|
return result;
|
|
2742
2745
|
}
|
|
2743
2746
|
async function requestCopilotSearch(payload, model, requestId, sessionId, clientSignal) {
|
|
2744
|
-
debugJson(logger$
|
|
2747
|
+
debugJson(logger$20, "Alpha search Copilot Responses request:", payload);
|
|
2745
2748
|
const result = await alphaSearchResponsesDependencies.createResponses(payload, {
|
|
2746
2749
|
vision: false,
|
|
2747
2750
|
initiator: "agent",
|
|
@@ -2750,7 +2753,7 @@ async function requestCopilotSearch(payload, model, requestId, sessionId, client
|
|
|
2750
2753
|
sessionId,
|
|
2751
2754
|
clientSignal
|
|
2752
2755
|
});
|
|
2753
|
-
debugJson(logger$
|
|
2756
|
+
debugJson(logger$20, "Alpha search Copilot Responses result:", result);
|
|
2754
2757
|
alphaSearchResponsesDependencies.createUsageRecorder(model, sessionId)({
|
|
2755
2758
|
...normalizeResponsesUsage(result.usage),
|
|
2756
2759
|
total_nano_aiu: normalizeOptionalToken(result.copilot_usage?.total_nano_aiu)
|
|
@@ -2863,7 +2866,7 @@ async function forwardCodexAlphaSearch(request) {
|
|
|
2863
2866
|
});
|
|
2864
2867
|
}
|
|
2865
2868
|
// src/routes/alpha-search/route.ts
|
|
2866
|
-
const logger$
|
|
2869
|
+
const logger$19 = createHandlerLogger("alpha-search-handler");
|
|
2867
2870
|
const alphaSearchRoutes = new Hono();
|
|
2868
2871
|
const alphaSearchRouteDependencies = {
|
|
2869
2872
|
findEndpointModel,
|
|
@@ -2883,10 +2886,10 @@ function parseDebugBody(body) {
|
|
|
2883
2886
|
}
|
|
2884
2887
|
}
|
|
2885
2888
|
async function forwardCodexAlphaSearchRequest(request, model) {
|
|
2886
|
-
await debugJsonAsync(logger$
|
|
2889
|
+
await debugJsonAsync(logger$19, "alpha_search.codex.request", async () => ({ body: parseDebugBody(await request.clone().text()) }));
|
|
2887
2890
|
if (model) setPluginRequestActualModel(model);
|
|
2888
2891
|
const upstreamResponse = await alphaSearchRouteDependencies.forwardCodexAlphaSearch(request);
|
|
2889
|
-
await debugJsonAsync(logger$
|
|
2892
|
+
await debugJsonAsync(logger$19, "alpha_search.codex.response", async () => ({
|
|
2890
2893
|
body: parseDebugBody(await upstreamResponse.clone().text()),
|
|
2891
2894
|
statusCode: upstreamResponse.status
|
|
2892
2895
|
}));
|
|
@@ -2999,7 +3002,7 @@ alphaSearchRoutes.post("/", async (c) => {
|
|
|
2999
3002
|
try {
|
|
3000
3003
|
return await handleAlphaSearchRequest(c);
|
|
3001
3004
|
} catch (error) {
|
|
3002
|
-
logger$
|
|
3005
|
+
logger$19.error("alpha_search.error", { error });
|
|
3003
3006
|
return await forwardError(c, error);
|
|
3004
3007
|
}
|
|
3005
3008
|
});
|
|
@@ -3180,7 +3183,7 @@ const normalizeProviderResponsesReasoningEffort = (payload, providerConfig) => {
|
|
|
3180
3183
|
};
|
|
3181
3184
|
};
|
|
3182
3185
|
// src/routes/provider/chat-completions/handler.ts
|
|
3183
|
-
const logger$
|
|
3186
|
+
const logger$18 = createHandlerLogger("provider-chat-completions-handler");
|
|
3184
3187
|
async function handleProviderChatCompletionsForProvider(c, options) {
|
|
3185
3188
|
const { payload, provider } = options;
|
|
3186
3189
|
const configuredProvider = await resolveProviderConfig(provider);
|
|
@@ -3195,13 +3198,13 @@ async function handleProviderChatCompletionsForProvider(c, options) {
|
|
|
3195
3198
|
applyProviderStreamOptions(payload);
|
|
3196
3199
|
applyDashScopePreserveThinkingDefault(payload, providerConfig);
|
|
3197
3200
|
applyProviderContextCache(payload, modelConfig, providerConfig);
|
|
3198
|
-
debugJson(logger$
|
|
3201
|
+
debugJson(logger$18, "provider.chat_completions.request", {
|
|
3199
3202
|
payload,
|
|
3200
3203
|
provider
|
|
3201
3204
|
});
|
|
3202
3205
|
const upstreamResponse = await forwardProviderChatCompletions(providerConfig, payload, c.req.raw.headers, { clientSignal: c.req.raw.signal });
|
|
3203
3206
|
if (!upstreamResponse.ok) {
|
|
3204
|
-
logger$
|
|
3207
|
+
logger$18.error("Failed to create provider chat completions", {
|
|
3205
3208
|
provider,
|
|
3206
3209
|
statusCode: upstreamResponse.status
|
|
3207
3210
|
});
|
|
@@ -3215,7 +3218,7 @@ async function handleProviderChatCompletionsForProvider(c, options) {
|
|
|
3215
3218
|
});
|
|
3216
3219
|
const responseBody = await upstreamResponse.clone().json();
|
|
3217
3220
|
recordUsage(normalizeOpenAIUsage(responseBody.usage));
|
|
3218
|
-
debugJson(logger$
|
|
3221
|
+
debugJson(logger$18, "provider.chat_completions.response", responseBody);
|
|
3219
3222
|
return createProviderProxyResponse(upstreamResponse);
|
|
3220
3223
|
}
|
|
3221
3224
|
const applyProviderStreamOptions = (payload) => {
|
|
@@ -3237,12 +3240,12 @@ const createProviderChatCompletionsUsageRecorder = (payload, provider, modelConf
|
|
|
3237
3240
|
providerName: provider
|
|
3238
3241
|
});
|
|
3239
3242
|
const streamProviderChatCompletions = (c, upstreamResponse, options) => {
|
|
3240
|
-
logger$
|
|
3243
|
+
logger$18.debug("provider.chat_completions.streaming", { provider: options.provider });
|
|
3241
3244
|
return streamSSE(c, async (stream) => {
|
|
3242
3245
|
let usage = {};
|
|
3243
3246
|
try {
|
|
3244
3247
|
for await (const chunk of events(upstreamResponse)) {
|
|
3245
|
-
debugJson(logger$
|
|
3248
|
+
debugJson(logger$18, "provider.chat_completions.stream_chunk", chunk);
|
|
3246
3249
|
if (chunk.data && chunk.data !== "[DONE]") {
|
|
3247
3250
|
const parsedChunk = parseChatCompletionChunkData(chunk.data);
|
|
3248
3251
|
recordChatCompletionFirstToken(parsedChunk);
|
|
@@ -3301,7 +3304,7 @@ const createChatCompletions = async (payload, options) => {
|
|
|
3301
3304
|
return await response.json();
|
|
3302
3305
|
};
|
|
3303
3306
|
// src/routes/chat-completions/handler.ts
|
|
3304
|
-
const logger$
|
|
3307
|
+
const logger$17 = createHandlerLogger("chat-completions-handler");
|
|
3305
3308
|
async function handleCompletion$1(c) {
|
|
3306
3309
|
let payload = await c.req.json();
|
|
3307
3310
|
setPluginRequestModel(payload.model);
|
|
@@ -3316,7 +3319,7 @@ async function handleCompletion$1(c) {
|
|
|
3316
3319
|
provider: providerModelAlias.provider
|
|
3317
3320
|
});
|
|
3318
3321
|
}
|
|
3319
|
-
debugJson(logger$
|
|
3322
|
+
debugJson(logger$17, "Request payload:", payload);
|
|
3320
3323
|
const selectedModel = findEndpointModel(payload.model);
|
|
3321
3324
|
payload.model = selectedModel?.id ?? payload.model;
|
|
3322
3325
|
if (isNullish(payload.max_tokens) && isNullish(payload.max_completion_tokens)) {
|
|
@@ -3324,16 +3327,16 @@ async function handleCompletion$1(c) {
|
|
|
3324
3327
|
...payload,
|
|
3325
3328
|
max_tokens: selectedModel?.capabilities.limits.max_output_tokens
|
|
3326
3329
|
};
|
|
3327
|
-
debugJson(logger$
|
|
3330
|
+
debugJson(logger$17, "Set max_tokens to:", payload.max_tokens);
|
|
3328
3331
|
}
|
|
3329
3332
|
if (payload.model.includes("gpt")) {
|
|
3330
3333
|
if (isNullish(payload.max_completion_tokens)) payload.max_completion_tokens = payload.max_tokens;
|
|
3331
3334
|
delete payload.max_tokens;
|
|
3332
3335
|
}
|
|
3333
3336
|
const requestId = generateRequestIdFromPayload(payload);
|
|
3334
|
-
logger$
|
|
3337
|
+
logger$17.debug("Generated request ID:", requestId);
|
|
3335
3338
|
const sessionId = getUUID(requestId);
|
|
3336
|
-
logger$
|
|
3339
|
+
logger$17.debug("Extracted session ID:", sessionId);
|
|
3337
3340
|
const recordUsage = createCopilotTokenUsageRecorder({
|
|
3338
3341
|
endpoint: "chat_completions",
|
|
3339
3342
|
fallbackSessionId: sessionId,
|
|
@@ -3345,18 +3348,18 @@ async function handleCompletion$1(c) {
|
|
|
3345
3348
|
sessionId
|
|
3346
3349
|
});
|
|
3347
3350
|
if (isNonStreaming$1(response)) {
|
|
3348
|
-
debugJson(logger$
|
|
3351
|
+
debugJson(logger$17, "Non-streaming response:", response);
|
|
3349
3352
|
recordUsage({
|
|
3350
3353
|
...normalizeOpenAIUsage(response.usage),
|
|
3351
3354
|
total_nano_aiu: normalizeOptionalToken(response.copilot_usage?.total_nano_aiu)
|
|
3352
3355
|
});
|
|
3353
3356
|
return c.json(response);
|
|
3354
3357
|
}
|
|
3355
|
-
logger$
|
|
3358
|
+
logger$17.debug("Streaming response");
|
|
3356
3359
|
return streamSSE(c, async (stream) => {
|
|
3357
3360
|
let usage = {};
|
|
3358
3361
|
for await (const chunk of response) {
|
|
3359
|
-
debugJson(logger$
|
|
3362
|
+
debugJson(logger$17, "Streaming chunk:", chunk);
|
|
3360
3363
|
const parsedChunk = parseChatCompletionChunk(chunk);
|
|
3361
3364
|
recordChatCompletionFirstToken(parsedChunk);
|
|
3362
3365
|
if (parsedChunk?.usage || parsedChunk?.copilot_usage) usage = {
|
|
@@ -3497,209 +3500,246 @@ embeddingRoutes.post("/", async (c) => {
|
|
|
3497
3500
|
return await forwardError(c, error);
|
|
3498
3501
|
}
|
|
3499
3502
|
});
|
|
3500
|
-
// src/
|
|
3503
|
+
// src/routes/images/temp-form-data.ts
|
|
3504
|
+
const logger$16 = createHandlerLogger("images-temp-form-data");
|
|
3505
|
+
const multipartStagingDependencies = { createWriteStream };
|
|
3501
3506
|
/**
|
|
3502
|
-
*
|
|
3503
|
-
*
|
|
3507
|
+
* Matches the images forwarding timeouts: an upstream upload may take that
|
|
3508
|
+
* long, so staged files must survive at least the whole forwarding window.
|
|
3504
3509
|
*/
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3510
|
+
const STAGED_FILES_TTL_MS = 9e5;
|
|
3511
|
+
const DEFAULT_MULTIPART_STAGING_LIMITS = {
|
|
3512
|
+
maxBodySizeBytes: 134217728,
|
|
3513
|
+
maxFieldSizeBytes: 10485760,
|
|
3514
|
+
maxFields: 64,
|
|
3515
|
+
maxFileSizeBytes: 67108864,
|
|
3516
|
+
maxFiles: 16,
|
|
3517
|
+
maxParts: 80
|
|
3518
|
+
};
|
|
3519
|
+
var InvalidMultipartBodyError = class extends Error {
|
|
3520
|
+
constructor(options) {
|
|
3521
|
+
super("Invalid multipart form data body", options);
|
|
3522
|
+
this.name = "InvalidMultipartBodyError";
|
|
3523
|
+
}
|
|
3524
|
+
};
|
|
3525
|
+
var MultipartBodyTooLargeError = class extends Error {
|
|
3526
|
+
constructor() {
|
|
3527
|
+
super("Multipart form data body exceeds the configured upload limits");
|
|
3528
|
+
this.name = "MultipartBodyTooLargeError";
|
|
3529
|
+
}
|
|
3530
|
+
};
|
|
3531
|
+
function sanitizeFileName(name) {
|
|
3532
|
+
return name.split(/[\\/]/).pop()?.replaceAll("\0", "") || "file";
|
|
3522
3533
|
}
|
|
3523
|
-
function
|
|
3524
|
-
|
|
3534
|
+
async function createDiskBackedBlob(filePath, type) {
|
|
3535
|
+
if (typeof Bun !== "undefined") return Bun.file(filePath, { type });
|
|
3536
|
+
const { openAsBlob } = await import("node:fs");
|
|
3537
|
+
return await openAsBlob(filePath, { type });
|
|
3525
3538
|
}
|
|
3526
|
-
function
|
|
3527
|
-
return
|
|
3539
|
+
function toError(error) {
|
|
3540
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
3528
3541
|
}
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3542
|
+
function parseMultipartBody(body, contentType, directory, entries, limits, signal) {
|
|
3543
|
+
return new Promise((resolve, reject) => {
|
|
3544
|
+
let busboy;
|
|
3545
|
+
try {
|
|
3546
|
+
busboy = new Busboy({
|
|
3547
|
+
headers: { "content-type": contentType },
|
|
3548
|
+
limits: {
|
|
3549
|
+
fieldSize: limits.maxFieldSizeBytes,
|
|
3550
|
+
fields: limits.maxFields,
|
|
3551
|
+
fileSize: limits.maxFileSizeBytes,
|
|
3552
|
+
files: limits.maxFiles,
|
|
3553
|
+
parts: limits.maxParts
|
|
3554
|
+
}
|
|
3555
|
+
});
|
|
3556
|
+
} catch (error) {
|
|
3557
|
+
reject(new InvalidMultipartBodyError({ cause: toError(error) }));
|
|
3558
|
+
return;
|
|
3559
|
+
}
|
|
3560
|
+
const pendingWrites = [];
|
|
3561
|
+
const activeFileStreams = /* @__PURE__ */ new Set();
|
|
3562
|
+
const source = body === null ? Readable.from([]) : Readable.fromWeb(body);
|
|
3563
|
+
let bodySizeBytes = 0;
|
|
3564
|
+
const bodyLimiter = new Transform({ transform(chunk, _encoding, callback) {
|
|
3565
|
+
bodySizeBytes += chunk.byteLength;
|
|
3566
|
+
if (bodySizeBytes > limits.maxBodySizeBytes) {
|
|
3567
|
+
callback(new MultipartBodyTooLargeError());
|
|
3568
|
+
return;
|
|
3569
|
+
}
|
|
3570
|
+
callback(null, chunk);
|
|
3571
|
+
} });
|
|
3572
|
+
let fileIndex = 0;
|
|
3573
|
+
let failure = null;
|
|
3574
|
+
let inputPipeline = null;
|
|
3575
|
+
let settled = false;
|
|
3576
|
+
const settleStreams = () => Promise.allSettled([...inputPipeline ? [inputPipeline] : [], ...pendingWrites]);
|
|
3577
|
+
const fail = (error) => {
|
|
3578
|
+
if (failure || settled) return;
|
|
3579
|
+
const failureError = toError(error);
|
|
3580
|
+
failure = failureError;
|
|
3581
|
+
source.destroy(failureError);
|
|
3582
|
+
bodyLimiter.destroy(failureError);
|
|
3583
|
+
busboy.destroy(failureError);
|
|
3584
|
+
for (const stream of activeFileStreams) stream.destroy(failureError);
|
|
3585
|
+
settleStreams().then(() => {
|
|
3586
|
+
if (settled) return;
|
|
3587
|
+
settled = true;
|
|
3588
|
+
reject(failureError);
|
|
3589
|
+
});
|
|
3590
|
+
};
|
|
3591
|
+
const failLimit = () => fail(new MultipartBodyTooLargeError());
|
|
3592
|
+
busboy.on("field", (name, value, nameTruncated, valueTruncated) => {
|
|
3593
|
+
if (nameTruncated || valueTruncated) {
|
|
3594
|
+
failLimit();
|
|
3595
|
+
return;
|
|
3596
|
+
}
|
|
3597
|
+
entries.push({
|
|
3598
|
+
kind: "field",
|
|
3599
|
+
name,
|
|
3600
|
+
value
|
|
3601
|
+
});
|
|
3602
|
+
});
|
|
3603
|
+
busboy.on("file", (name, stream, fileName, _transferEncoding, mimeType) => {
|
|
3604
|
+
if (failure) {
|
|
3605
|
+
stream.resume();
|
|
3606
|
+
return;
|
|
3607
|
+
}
|
|
3608
|
+
const filePath = join(directory, `${fileIndex}.upload`);
|
|
3609
|
+
fileIndex += 1;
|
|
3610
|
+
entries.push({
|
|
3611
|
+
fileName: sanitizeFileName(fileName),
|
|
3612
|
+
filePath,
|
|
3613
|
+
kind: "file",
|
|
3614
|
+
mimeType,
|
|
3615
|
+
name
|
|
3616
|
+
});
|
|
3617
|
+
activeFileStreams.add(stream);
|
|
3618
|
+
stream.once("error", fail);
|
|
3619
|
+
stream.once("close", () => activeFileStreams.delete(stream));
|
|
3620
|
+
stream.once("limit", failLimit);
|
|
3621
|
+
try {
|
|
3622
|
+
const write = pipeline(stream, multipartStagingDependencies.createWriteStream(filePath));
|
|
3623
|
+
pendingWrites.push(write);
|
|
3624
|
+
write.catch(fail);
|
|
3625
|
+
} catch (error) {
|
|
3626
|
+
fail(error);
|
|
3627
|
+
}
|
|
3628
|
+
});
|
|
3629
|
+
busboy.on("fieldsLimit", failLimit);
|
|
3630
|
+
busboy.on("filesLimit", failLimit);
|
|
3631
|
+
busboy.on("partsLimit", failLimit);
|
|
3632
|
+
inputPipeline = pipeline(source, bodyLimiter, busboy, { signal });
|
|
3633
|
+
inputPipeline.then(() => {
|
|
3634
|
+
Promise.allSettled(pendingWrites).then((results) => {
|
|
3635
|
+
if (failure || settled) return;
|
|
3636
|
+
const failed = results.find((result) => result.status === "rejected");
|
|
3637
|
+
if (failed) fail(failed.reason);
|
|
3638
|
+
else {
|
|
3639
|
+
settled = true;
|
|
3640
|
+
resolve();
|
|
3641
|
+
}
|
|
3642
|
+
});
|
|
3643
|
+
}, (error) => {
|
|
3644
|
+
fail(error instanceof MultipartBodyTooLargeError || signal?.aborted ? error : new InvalidMultipartBodyError({ cause: toError(error) }));
|
|
3645
|
+
});
|
|
3578
3646
|
});
|
|
3579
3647
|
}
|
|
3580
|
-
|
|
3581
|
-
const
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
getImageEditModel,
|
|
3585
|
-
getImageGenerationModel,
|
|
3586
|
-
resolveMappedModel,
|
|
3587
|
-
resolveProviderConfig
|
|
3588
|
-
};
|
|
3589
|
-
function imageEditContentTypeErrorResponse(c) {
|
|
3590
|
-
if (c.req.header("content-type")?.split(";", 1)[0]?.trim().toLowerCase() === "application/json") return null;
|
|
3591
|
-
return c.json({ error: {
|
|
3592
|
-
message: "Image edits require application/json with images: [{ image_url }] or [{ file_id }]; multipart uploads are not supported",
|
|
3593
|
-
type: "invalid_request_error"
|
|
3594
|
-
} }, 415);
|
|
3595
|
-
}
|
|
3596
|
-
function getContentMetadata(headers) {
|
|
3597
|
-
return {
|
|
3598
|
-
contentType: headers.get("content-type"),
|
|
3599
|
-
contentLength: headers.get("content-length")
|
|
3648
|
+
function resolveStagingLimits(overrides) {
|
|
3649
|
+
const limits = {
|
|
3650
|
+
...DEFAULT_MULTIPART_STAGING_LIMITS,
|
|
3651
|
+
...overrides
|
|
3600
3652
|
};
|
|
3653
|
+
for (const [name, value] of Object.entries(limits)) if (!Number.isSafeInteger(value) || value <= 0) throw new RangeError(`${name} must be a positive safe integer`);
|
|
3654
|
+
return limits;
|
|
3601
3655
|
}
|
|
3602
3656
|
/**
|
|
3603
|
-
*
|
|
3657
|
+
* Streams a multipart request body through the parser, writing every file
|
|
3658
|
+
* part to a temporary directory as it arrives and rebuilding the form data
|
|
3659
|
+
* with disk-backed blobs, so forwarding streams payloads from disk instead
|
|
3660
|
+
* of pinning the whole upload in memory.
|
|
3604
3661
|
*/
|
|
3605
|
-
function
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
* srvx wraps incoming requests in a proxy class whose prototype chain
|
|
3611
|
-
* satisfies `instanceof Request` without the native internals, so
|
|
3612
|
-
* `new Request(request)` throws.
|
|
3613
|
-
*/
|
|
3614
|
-
function createForwardRequest(request, requestHeaders, body) {
|
|
3615
|
-
const init = {
|
|
3616
|
-
body,
|
|
3617
|
-
duplex: "half",
|
|
3618
|
-
headers: requestHeaders,
|
|
3619
|
-
method: request.method,
|
|
3620
|
-
signal: request.signal
|
|
3621
|
-
};
|
|
3622
|
-
return new Request(request.url, init);
|
|
3623
|
-
}
|
|
3624
|
-
function createJsonImagesRequest(request, requestHeaders, payload, model) {
|
|
3625
|
-
const headers = new Headers(requestHeaders);
|
|
3626
|
-
headers.delete("content-length");
|
|
3627
|
-
return createForwardRequest(request, headers, new TextEncoder().encode(JSON.stringify({
|
|
3628
|
-
...payload,
|
|
3629
|
-
model
|
|
3630
|
-
})));
|
|
3631
|
-
}
|
|
3632
|
-
/**
|
|
3633
|
-
* Parses an image request body as JSON, retaining the original bytes for
|
|
3634
|
-
* unchanged forwarding while allowing model mapping to rebuild the payload.
|
|
3635
|
-
* Rebuilt bodies stay binary so Node does not synthesize a text/plain content type.
|
|
3636
|
-
*/
|
|
3637
|
-
async function parseJsonImagesRequest(request) {
|
|
3638
|
-
const requestHeaders = snapshotRequestHeaders(request);
|
|
3639
|
-
const body = new Uint8Array(await request.arrayBuffer());
|
|
3640
|
-
const originalRequest = createForwardRequest(request, requestHeaders, body);
|
|
3641
|
-
let payload;
|
|
3662
|
+
async function stageMultipartBodyToDisk(body, contentType, limitOverrides = {}, signal) {
|
|
3663
|
+
signal?.throwIfAborted();
|
|
3664
|
+
const limits = resolveStagingLimits(limitOverrides);
|
|
3665
|
+
const directory = await mkdtemp(join(tmpdir(), "copilot-api-images-"));
|
|
3666
|
+
const entries = [];
|
|
3642
3667
|
try {
|
|
3643
|
-
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3668
|
+
await parseMultipartBody(body, contentType, directory, entries, limits, signal);
|
|
3669
|
+
} catch (error) {
|
|
3670
|
+
await rm(directory, {
|
|
3671
|
+
force: true,
|
|
3672
|
+
recursive: true
|
|
3673
|
+
});
|
|
3674
|
+
throw error;
|
|
3647
3675
|
}
|
|
3648
|
-
|
|
3649
|
-
return {
|
|
3650
|
-
createRequest: (mappedModel) => createJsonImagesRequest(request, requestHeaders, payload, mappedModel),
|
|
3651
|
-
model: payload.model,
|
|
3652
|
-
originalRequest
|
|
3653
|
-
};
|
|
3654
|
-
}
|
|
3655
|
-
/**
|
|
3656
|
-
* Handles Codex images proxying. Pass a resolvedProviderConfig when the
|
|
3657
|
-
* caller already resolved the codex provider to avoid a second resolve, and
|
|
3658
|
-
* pass a request when model mapping rebuilt the request body.
|
|
3659
|
-
*/
|
|
3660
|
-
async function handleCodexImages(c, operation, resolvedProviderConfig, request = c.req.raw) {
|
|
3676
|
+
const stagedFormData = new FormData();
|
|
3661
3677
|
try {
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
type: "invalid_request_error"
|
|
3665
|
-
} }, 404);
|
|
3666
|
-
if (operation === "generations") await imageRouteDependencies.debugJsonAsync(logger$15, "images.generations.codex.request", async () => ({ body: await request.clone().text() }));
|
|
3667
|
-
else debugJson(logger$15, "images.edits.codex.request", getContentMetadata(request.headers));
|
|
3668
|
-
const upstreamResponse = await forwardCodexImages(request, operation);
|
|
3669
|
-
debugJson(logger$15, `images.${operation}.codex.response`, {
|
|
3670
|
-
...getContentMetadata(upstreamResponse.headers),
|
|
3671
|
-
statusCode: upstreamResponse.status
|
|
3672
|
-
});
|
|
3673
|
-
return createProviderProxyResponse(upstreamResponse);
|
|
3678
|
+
for (const entry of entries) if (entry.kind === "field") stagedFormData.append(entry.name, entry.value);
|
|
3679
|
+
else stagedFormData.append(entry.name, await createDiskBackedBlob(entry.filePath, entry.mimeType), entry.fileName);
|
|
3674
3680
|
} catch (error) {
|
|
3675
|
-
|
|
3676
|
-
|
|
3681
|
+
await rm(directory, {
|
|
3682
|
+
force: true,
|
|
3683
|
+
recursive: true
|
|
3684
|
+
});
|
|
3685
|
+
throw error;
|
|
3677
3686
|
}
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3687
|
+
let cleanupPromise = null;
|
|
3688
|
+
let cleanupTimer = null;
|
|
3689
|
+
let unregisterProcessCleanup = null;
|
|
3690
|
+
const cleanup = () => {
|
|
3691
|
+
cleanupPromise ??= (async () => {
|
|
3692
|
+
if (cleanupTimer) {
|
|
3693
|
+
clearTimeout(cleanupTimer);
|
|
3694
|
+
cleanupTimer = null;
|
|
3695
|
+
}
|
|
3696
|
+
try {
|
|
3697
|
+
await rm(directory, {
|
|
3698
|
+
force: true,
|
|
3699
|
+
recursive: true
|
|
3700
|
+
});
|
|
3701
|
+
} catch (error) {
|
|
3702
|
+
logger$16.warn("Failed to remove staged images temp directory", {
|
|
3703
|
+
directory,
|
|
3704
|
+
error
|
|
3705
|
+
});
|
|
3706
|
+
} finally {
|
|
3707
|
+
unregisterProcessCleanup?.();
|
|
3708
|
+
unregisterProcessCleanup = null;
|
|
3709
|
+
}
|
|
3710
|
+
})();
|
|
3711
|
+
return cleanupPromise;
|
|
3712
|
+
};
|
|
3713
|
+
unregisterProcessCleanup = registerProcessCleanup(() => {
|
|
3714
|
+
if (cleanupTimer) {
|
|
3715
|
+
clearTimeout(cleanupTimer);
|
|
3716
|
+
cleanupTimer = null;
|
|
3717
|
+
}
|
|
3718
|
+
try {
|
|
3719
|
+
rmSync(directory, {
|
|
3720
|
+
force: true,
|
|
3721
|
+
recursive: true
|
|
3696
3722
|
});
|
|
3723
|
+
} catch (error) {
|
|
3724
|
+
logger$16.warn("Failed to remove staged images temp directory on exit", {
|
|
3725
|
+
directory,
|
|
3726
|
+
error
|
|
3727
|
+
});
|
|
3728
|
+
} finally {
|
|
3729
|
+
unregisterProcessCleanup?.();
|
|
3730
|
+
unregisterProcessCleanup = null;
|
|
3697
3731
|
}
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3732
|
+
});
|
|
3733
|
+
return {
|
|
3734
|
+
directory,
|
|
3735
|
+
formData: stagedFormData,
|
|
3736
|
+
cleanup,
|
|
3737
|
+
scheduleCleanup: () => {
|
|
3738
|
+
if (cleanupPromise || cleanupTimer) return;
|
|
3739
|
+
cleanupTimer = setTimeout(() => void cleanup(), STAGED_FILES_TTL_MS);
|
|
3740
|
+
cleanupTimer.unref();
|
|
3741
|
+
}
|
|
3742
|
+
};
|
|
3703
3743
|
}
|
|
3704
3744
|
// src/services/xai/images.ts
|
|
3705
3745
|
const XAI_ASPECT_RATIOS = [
|
|
@@ -4086,6 +4126,362 @@ async function ensureImageResponseB64Json(response, fetchImage = globalThis.fetc
|
|
|
4086
4126
|
data
|
|
4087
4127
|
}));
|
|
4088
4128
|
}
|
|
4129
|
+
// src/routes/images/multipart-edits.ts
|
|
4130
|
+
const multipartEditDependencies = { stageMultipartBodyToDisk };
|
|
4131
|
+
var ImageEditRequestError = class extends Error {};
|
|
4132
|
+
function imageEditRequestErrorResponse(c, error) {
|
|
4133
|
+
if (c.req.raw.signal.aborted) return null;
|
|
4134
|
+
if (!(error instanceof ImageEditRequestError) && !(error instanceof InvalidMultipartBodyError) && !(error instanceof MultipartBodyTooLargeError) && !(error instanceof XaiImagesRequestError)) return null;
|
|
4135
|
+
return c.json({ error: {
|
|
4136
|
+
message: error.message,
|
|
4137
|
+
type: "invalid_request_error"
|
|
4138
|
+
} }, error instanceof MultipartBodyTooLargeError ? 413 : 400);
|
|
4139
|
+
}
|
|
4140
|
+
function imageMimeType(bytes, declared) {
|
|
4141
|
+
if (declared.startsWith("image/")) return declared;
|
|
4142
|
+
if (bytes.subarray(0, 8).equals(Buffer.from([
|
|
4143
|
+
137,
|
|
4144
|
+
80,
|
|
4145
|
+
78,
|
|
4146
|
+
71,
|
|
4147
|
+
13,
|
|
4148
|
+
10,
|
|
4149
|
+
26,
|
|
4150
|
+
10
|
|
4151
|
+
]))) return "image/png";
|
|
4152
|
+
if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
|
|
4153
|
+
if (bytes.toString("ascii", 0, 4) === "RIFF" && bytes.toString("ascii", 8, 12) === "WEBP") return "image/webp";
|
|
4154
|
+
if (/^GIF8[79]a/u.test(bytes.toString("ascii", 0, 6))) return "image/gif";
|
|
4155
|
+
throw new ImageEditRequestError("Uploaded image must have an image MIME type");
|
|
4156
|
+
}
|
|
4157
|
+
async function imageReference(value, signal) {
|
|
4158
|
+
signal.throwIfAborted();
|
|
4159
|
+
if (typeof value === "string") {
|
|
4160
|
+
const url = value.trim();
|
|
4161
|
+
if (!/^(?:https?:\/\/|data:image\/[a-z0-9.+-]+;base64,)/iu.test(url)) throw new ImageEditRequestError("image must be a file, HTTP URL or base64 data URL");
|
|
4162
|
+
return { image_url: url };
|
|
4163
|
+
}
|
|
4164
|
+
const bytes = Buffer.from(await value.arrayBuffer());
|
|
4165
|
+
signal.throwIfAborted();
|
|
4166
|
+
if (bytes.length === 0) throw new ImageEditRequestError("image is empty");
|
|
4167
|
+
return { image_url: `data:${imageMimeType(bytes, value.type.trim().toLowerCase())};base64,${bytes.toString("base64")}` };
|
|
4168
|
+
}
|
|
4169
|
+
function fieldValue(name, value) {
|
|
4170
|
+
if ([
|
|
4171
|
+
"n",
|
|
4172
|
+
"partial_images",
|
|
4173
|
+
"output_compression"
|
|
4174
|
+
].includes(name)) {
|
|
4175
|
+
const trimmed = value.trim();
|
|
4176
|
+
const number = Number(trimmed);
|
|
4177
|
+
if (!/^-?\d+$/u.test(trimmed) || !Number.isSafeInteger(number)) throw new ImageEditRequestError(`${name} must be an integer`);
|
|
4178
|
+
return number;
|
|
4179
|
+
}
|
|
4180
|
+
if (name === "stream") {
|
|
4181
|
+
if (value.trim() === "true") return true;
|
|
4182
|
+
if (value.trim() === "false") return false;
|
|
4183
|
+
throw new ImageEditRequestError("stream must be true or false");
|
|
4184
|
+
}
|
|
4185
|
+
return value;
|
|
4186
|
+
}
|
|
4187
|
+
async function formPayload(form, signal) {
|
|
4188
|
+
const fields = /* @__PURE__ */ new Map();
|
|
4189
|
+
const images = [];
|
|
4190
|
+
for (const [name, value] of form.entries()) {
|
|
4191
|
+
if (name === "image" || name === "image[]") {
|
|
4192
|
+
images.push(await imageReference(value, signal));
|
|
4193
|
+
continue;
|
|
4194
|
+
}
|
|
4195
|
+
const key = name === "mask[]" ? "mask" : name;
|
|
4196
|
+
if (fields.has(key)) throw new ImageEditRequestError(`Duplicate image edit field: ${key}`);
|
|
4197
|
+
if (key === "mask") fields.set(key, await imageReference(value, signal));
|
|
4198
|
+
else if (typeof value === "string") fields.set(key, fieldValue(key, value));
|
|
4199
|
+
else throw new ImageEditRequestError(`Unexpected image edit file field: ${key}`);
|
|
4200
|
+
}
|
|
4201
|
+
if (images.length === 0) throw new ImageEditRequestError("image is required");
|
|
4202
|
+
fields.set("images", images);
|
|
4203
|
+
return Object.fromEntries(fields);
|
|
4204
|
+
}
|
|
4205
|
+
/** Parse multipart once, keeping uploads on disk until the target is known. */
|
|
4206
|
+
async function parseMultipartImageEdit(request) {
|
|
4207
|
+
const requestHeaders = new Headers(request.headers);
|
|
4208
|
+
const contentType = requestHeaders.get("content-type");
|
|
4209
|
+
if (contentType?.split(";", 1)[0]?.trim().toLowerCase() !== "multipart/form-data") return null;
|
|
4210
|
+
const staged = await multipartEditDependencies.stageMultipartBodyToDisk(request.body, contentType, {}, request.signal);
|
|
4211
|
+
const form = staged.formData;
|
|
4212
|
+
const model = form.get("model");
|
|
4213
|
+
let payloadPromise;
|
|
4214
|
+
const readPayload = () => {
|
|
4215
|
+
payloadPromise ??= formPayload(form, request.signal).finally(staged.cleanup);
|
|
4216
|
+
return payloadPromise;
|
|
4217
|
+
};
|
|
4218
|
+
return {
|
|
4219
|
+
...staged,
|
|
4220
|
+
model: typeof model === "string" ? model : void 0,
|
|
4221
|
+
readPayload,
|
|
4222
|
+
createRequest: async (model, provider = "codex") => {
|
|
4223
|
+
const headers = new Headers(requestHeaders);
|
|
4224
|
+
headers.delete("content-length");
|
|
4225
|
+
let body;
|
|
4226
|
+
if (provider === "codex" || provider === "xai") {
|
|
4227
|
+
const payload = {
|
|
4228
|
+
...await readPayload(),
|
|
4229
|
+
...model === void 0 ? {} : { model }
|
|
4230
|
+
};
|
|
4231
|
+
if (provider === "xai" && !model) throw new ImageEditRequestError("model is required for xAI multipart edits");
|
|
4232
|
+
headers.set("content-type", "application/json");
|
|
4233
|
+
body = new TextEncoder().encode(JSON.stringify(provider === "xai" ? translateXaiImageEditJson(payload, model) : payload));
|
|
4234
|
+
} else {
|
|
4235
|
+
if (model !== void 0) form.set("model", model);
|
|
4236
|
+
headers.delete("content-type");
|
|
4237
|
+
body = form;
|
|
4238
|
+
}
|
|
4239
|
+
request.signal.throwIfAborted();
|
|
4240
|
+
return new Request(request.url, {
|
|
4241
|
+
method: request.method,
|
|
4242
|
+
headers,
|
|
4243
|
+
body,
|
|
4244
|
+
signal: request.signal
|
|
4245
|
+
});
|
|
4246
|
+
}
|
|
4247
|
+
};
|
|
4248
|
+
}
|
|
4249
|
+
// src/lib/xai-image-model.ts
|
|
4250
|
+
/**
|
|
4251
|
+
* Resolves one image-route model setting. Empty values keep the OpenAI
|
|
4252
|
+
* protocol. Only an xai/model alias selects the xAI Imagine API.
|
|
4253
|
+
*/
|
|
4254
|
+
function resolveConfiguredXaiImageModel(configured, settingName) {
|
|
4255
|
+
if (configured === void 0 || configured === null) return { status: "disabled" };
|
|
4256
|
+
if (typeof configured !== "string") return {
|
|
4257
|
+
message: `${settingName} must be an xai/model alias`,
|
|
4258
|
+
status: "invalid"
|
|
4259
|
+
};
|
|
4260
|
+
const trimmed = configured.trim();
|
|
4261
|
+
if (!trimmed) return { status: "disabled" };
|
|
4262
|
+
const alias = parseProviderModelAlias(trimmed);
|
|
4263
|
+
if (alias?.provider.toLowerCase() !== "xai") return {
|
|
4264
|
+
message: `${settingName} only supports an xai/model alias, received '${trimmed}'`,
|
|
4265
|
+
status: "invalid"
|
|
4266
|
+
};
|
|
4267
|
+
return {
|
|
4268
|
+
model: alias.model,
|
|
4269
|
+
status: "xai"
|
|
4270
|
+
};
|
|
4271
|
+
}
|
|
4272
|
+
function getImageGenerationModel() {
|
|
4273
|
+
return resolveConfiguredXaiImageModel(getConfig().imageGenerationModel, "imageGenerationModel");
|
|
4274
|
+
}
|
|
4275
|
+
function getImageEditModel() {
|
|
4276
|
+
return resolveConfiguredXaiImageModel(getConfig().imageEditModel, "imageEditModel");
|
|
4277
|
+
}
|
|
4278
|
+
// src/routes/images/forward-provider-images.ts
|
|
4279
|
+
/**
|
|
4280
|
+
* Forwards an images request to a configured provider with debug logging.
|
|
4281
|
+
* Shared by the top-level images routes (model mapping) and the
|
|
4282
|
+
* provider-scoped images routes so both log and proxy identically.
|
|
4283
|
+
*/
|
|
4284
|
+
async function forwardProviderImagesWithLogging(providerConfig, request, operation, { logger, provider }) {
|
|
4285
|
+
if (operation === "generations") await debugJsonAsync(logger, `provider.images.${operation}.request`, async () => ({
|
|
4286
|
+
body: await request.clone().text(),
|
|
4287
|
+
provider
|
|
4288
|
+
}));
|
|
4289
|
+
else debugJson(logger, `provider.images.${operation}.request`, {
|
|
4290
|
+
contentType: request.headers.get("content-type"),
|
|
4291
|
+
contentLength: request.headers.get("content-length"),
|
|
4292
|
+
provider
|
|
4293
|
+
});
|
|
4294
|
+
const upstreamResponse = await forwardProviderImages(providerConfig, request, operation);
|
|
4295
|
+
debugJson(logger, `provider.images.${operation}.response`, {
|
|
4296
|
+
contentType: upstreamResponse.headers.get("content-type"),
|
|
4297
|
+
contentLength: upstreamResponse.headers.get("content-length"),
|
|
4298
|
+
provider,
|
|
4299
|
+
statusCode: upstreamResponse.status
|
|
4300
|
+
});
|
|
4301
|
+
return createProviderProxyResponse(upstreamResponse);
|
|
4302
|
+
}
|
|
4303
|
+
/** Forwards translated xAI requests and normalizes OpenAI-compatible output. */
|
|
4304
|
+
async function forwardXaiImagesWithLogging(providerConfig, request, operation, payload, { logger, provider }) {
|
|
4305
|
+
return await ensureImageResponseB64Json(await forwardProviderImagesWithLogging(providerConfig, new Request(request.url, {
|
|
4306
|
+
body: JSON.stringify(payload),
|
|
4307
|
+
headers: {
|
|
4308
|
+
accept: "application/json",
|
|
4309
|
+
"content-type": "application/json"
|
|
4310
|
+
},
|
|
4311
|
+
method: "POST",
|
|
4312
|
+
signal: request.signal
|
|
4313
|
+
}), operation, {
|
|
4314
|
+
logger,
|
|
4315
|
+
provider
|
|
4316
|
+
}), void 0, {
|
|
4317
|
+
clientSignal: request.signal,
|
|
4318
|
+
onImageUrl: (url) => {
|
|
4319
|
+
logger.info("images.xai.output.url", JSON.stringify({
|
|
4320
|
+
operation,
|
|
4321
|
+
url
|
|
4322
|
+
}));
|
|
4323
|
+
},
|
|
4324
|
+
responseFormat: payload.response_format === "url" ? "url" : "b64_json"
|
|
4325
|
+
});
|
|
4326
|
+
}
|
|
4327
|
+
// src/services/codex/images.ts
|
|
4328
|
+
const CODEX_IMAGES_TIMEOUT_MS = 9e5;
|
|
4329
|
+
const codexImagesDispatcher = createTimeoutDispatcher(CODEX_IMAGES_TIMEOUT_MS);
|
|
4330
|
+
function resolveCodexImagesUrl(requestUrl, operation) {
|
|
4331
|
+
const upstreamUrl = new URL(`${CODEX_API_BASE_URL}/codex/images/${operation}`);
|
|
4332
|
+
upstreamUrl.search = new URL(requestUrl, "http://localhost").search;
|
|
4333
|
+
return upstreamUrl.toString();
|
|
4334
|
+
}
|
|
4335
|
+
async function forwardCodexImages(request, operation) {
|
|
4336
|
+
const headers = buildCodexRequestHeaders(request.headers);
|
|
4337
|
+
if (!headers.has("accept")) headers.set("accept", "application/json");
|
|
4338
|
+
if (operation === "generations" && !headers.has("content-type")) headers.set("content-type", "application/json");
|
|
4339
|
+
const init = {
|
|
4340
|
+
method: "POST",
|
|
4341
|
+
headers,
|
|
4342
|
+
body: request.body,
|
|
4343
|
+
duplex: "half",
|
|
4344
|
+
signal: AbortSignal.any([request.signal, AbortSignal.timeout(CODEX_IMAGES_TIMEOUT_MS)])
|
|
4345
|
+
};
|
|
4346
|
+
const upstreamUrl = resolveCodexImagesUrl(request.url, operation);
|
|
4347
|
+
if (typeof Bun !== "undefined") return await fetch(upstreamUrl, init);
|
|
4348
|
+
return await fetch$1(upstreamUrl, {
|
|
4349
|
+
...init,
|
|
4350
|
+
dispatcher: codexImagesDispatcher
|
|
4351
|
+
});
|
|
4352
|
+
}
|
|
4353
|
+
// src/routes/images/shared.ts
|
|
4354
|
+
const logger$15 = createHandlerLogger("images-handler");
|
|
4355
|
+
const imageRouteDependencies = {
|
|
4356
|
+
debugJsonAsync,
|
|
4357
|
+
getImageEditModel,
|
|
4358
|
+
getImageGenerationModel,
|
|
4359
|
+
resolveMappedModel,
|
|
4360
|
+
resolveProviderConfig
|
|
4361
|
+
};
|
|
4362
|
+
function imageEditContentTypeErrorResponse(c) {
|
|
4363
|
+
const mediaType = c.req.header("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
4364
|
+
if (mediaType === "application/json" || mediaType === "multipart/form-data") return null;
|
|
4365
|
+
return c.json({ error: {
|
|
4366
|
+
message: "Image edits require application/json or multipart/form-data",
|
|
4367
|
+
type: "invalid_request_error"
|
|
4368
|
+
} }, 415);
|
|
4369
|
+
}
|
|
4370
|
+
function getContentMetadata(headers) {
|
|
4371
|
+
return {
|
|
4372
|
+
contentType: headers.get("content-type"),
|
|
4373
|
+
contentLength: headers.get("content-length")
|
|
4374
|
+
};
|
|
4375
|
+
}
|
|
4376
|
+
/**
|
|
4377
|
+
* Snapshots generated request headers before consuming the body.
|
|
4378
|
+
*/
|
|
4379
|
+
function snapshotRequestHeaders(request) {
|
|
4380
|
+
return new Headers(request.headers);
|
|
4381
|
+
}
|
|
4382
|
+
/**
|
|
4383
|
+
* Rebuilds a request from primitives instead of cloning: under Node runtimes
|
|
4384
|
+
* srvx wraps incoming requests in a proxy class whose prototype chain
|
|
4385
|
+
* satisfies `instanceof Request` without the native internals, so
|
|
4386
|
+
* `new Request(request)` throws.
|
|
4387
|
+
*/
|
|
4388
|
+
function createForwardRequest(request, requestHeaders, body) {
|
|
4389
|
+
const init = {
|
|
4390
|
+
body,
|
|
4391
|
+
duplex: "half",
|
|
4392
|
+
headers: requestHeaders,
|
|
4393
|
+
method: request.method,
|
|
4394
|
+
signal: request.signal
|
|
4395
|
+
};
|
|
4396
|
+
return new Request(request.url, init);
|
|
4397
|
+
}
|
|
4398
|
+
function createJsonImagesRequest(request, requestHeaders, payload, model) {
|
|
4399
|
+
const headers = new Headers(requestHeaders);
|
|
4400
|
+
headers.delete("content-length");
|
|
4401
|
+
return createForwardRequest(request, headers, new TextEncoder().encode(JSON.stringify({
|
|
4402
|
+
...payload,
|
|
4403
|
+
model
|
|
4404
|
+
})));
|
|
4405
|
+
}
|
|
4406
|
+
/**
|
|
4407
|
+
* Parses an image request body as JSON, retaining the original bytes for
|
|
4408
|
+
* unchanged forwarding while allowing model mapping to rebuild the payload.
|
|
4409
|
+
* Rebuilt bodies stay binary so Node does not synthesize a text/plain content type.
|
|
4410
|
+
*/
|
|
4411
|
+
async function parseJsonImagesRequest(request) {
|
|
4412
|
+
const requestHeaders = snapshotRequestHeaders(request);
|
|
4413
|
+
const body = new Uint8Array(await request.arrayBuffer());
|
|
4414
|
+
const originalRequest = createForwardRequest(request, requestHeaders, body);
|
|
4415
|
+
let payload;
|
|
4416
|
+
try {
|
|
4417
|
+
const bodyText = new TextDecoder("utf-8", { fatal: true }).decode(body);
|
|
4418
|
+
payload = JSON.parse(bodyText);
|
|
4419
|
+
} catch {
|
|
4420
|
+
return originalRequest;
|
|
4421
|
+
}
|
|
4422
|
+
if (payload === null || typeof payload !== "object" || Array.isArray(payload) || typeof payload.model !== "string") return originalRequest;
|
|
4423
|
+
return {
|
|
4424
|
+
createRequest: (mappedModel) => createJsonImagesRequest(request, requestHeaders, payload, mappedModel),
|
|
4425
|
+
model: payload.model,
|
|
4426
|
+
originalRequest
|
|
4427
|
+
};
|
|
4428
|
+
}
|
|
4429
|
+
/**
|
|
4430
|
+
* Handles Codex images proxying. Pass a resolvedProviderConfig when the
|
|
4431
|
+
* caller already resolved the codex provider to avoid a second resolve, and
|
|
4432
|
+
* pass a request when model mapping rebuilt the request body.
|
|
4433
|
+
*/
|
|
4434
|
+
async function handleCodexImages(c, operation, resolvedProviderConfig, request = c.req.raw) {
|
|
4435
|
+
try {
|
|
4436
|
+
if (!(resolvedProviderConfig ?? await imageRouteDependencies.resolveProviderConfig("codex"))) return c.json({ error: {
|
|
4437
|
+
message: "Provider 'codex' not found or disabled",
|
|
4438
|
+
type: "invalid_request_error"
|
|
4439
|
+
} }, 404);
|
|
4440
|
+
if (operation === "generations") await imageRouteDependencies.debugJsonAsync(logger$15, "images.generations.codex.request", async () => ({ body: await request.clone().text() }));
|
|
4441
|
+
else debugJson(logger$15, "images.edits.codex.request", getContentMetadata(request.headers));
|
|
4442
|
+
const upstreamResponse = await forwardCodexImages(request, operation);
|
|
4443
|
+
debugJson(logger$15, `images.${operation}.codex.response`, {
|
|
4444
|
+
...getContentMetadata(upstreamResponse.headers),
|
|
4445
|
+
statusCode: upstreamResponse.status
|
|
4446
|
+
});
|
|
4447
|
+
return createProviderProxyResponse(upstreamResponse);
|
|
4448
|
+
} catch (error) {
|
|
4449
|
+
logger$15.error(`images.${operation}.codex.error`, { error });
|
|
4450
|
+
return await forwardError(c, error);
|
|
4451
|
+
}
|
|
4452
|
+
}
|
|
4453
|
+
/**
|
|
4454
|
+
* Routes a parsed images request: applies the model mapping, forwards to a
|
|
4455
|
+
* mapped provider when configured, and otherwise falls back to Codex.
|
|
4456
|
+
*/
|
|
4457
|
+
async function routeImagesRequest(c, operation, parsed) {
|
|
4458
|
+
const requestedModel = parsed.model;
|
|
4459
|
+
const mappedModel = imageRouteDependencies.resolveMappedModel(requestedModel);
|
|
4460
|
+
if (mappedModel !== requestedModel) consola.debug(`Resolved model mapping: ${requestedModel} -> ${mappedModel}`);
|
|
4461
|
+
const providerModelAlias = parseProviderModelAlias(mappedModel);
|
|
4462
|
+
if (providerModelAlias) {
|
|
4463
|
+
const providerConfig = await imageRouteDependencies.resolveProviderConfig(providerModelAlias.provider);
|
|
4464
|
+
if (providerConfig) {
|
|
4465
|
+
const request = await parsed.createRequest(providerModelAlias.model, providerConfig.name);
|
|
4466
|
+
if (providerConfig.name === "codex") return await handleCodexImages(c, operation, providerConfig, request);
|
|
4467
|
+
if (providerConfig.name === "xai") {
|
|
4468
|
+
const payload = await request.json();
|
|
4469
|
+
return await forwardXaiImagesWithLogging(providerConfig, request, operation, operation === "edits" ? translateXaiImageEditJson(payload, providerModelAlias.model) : translateXaiImageGeneration(payload, providerModelAlias.model), {
|
|
4470
|
+
logger: logger$15,
|
|
4471
|
+
provider: providerModelAlias.provider
|
|
4472
|
+
});
|
|
4473
|
+
}
|
|
4474
|
+
return await forwardProviderImagesWithLogging(providerConfig, request, operation, {
|
|
4475
|
+
logger: logger$15,
|
|
4476
|
+
provider: providerModelAlias.provider
|
|
4477
|
+
});
|
|
4478
|
+
}
|
|
4479
|
+
consola.debug(`Provider '${providerModelAlias.provider}' not found or disabled; forwarding the original model to Codex`);
|
|
4480
|
+
return await handleCodexImages(c, operation, void 0, parsed.originalRequest ?? await parsed.createRequest(requestedModel));
|
|
4481
|
+
}
|
|
4482
|
+
if (parsed.originalRequest && mappedModel === requestedModel) return await handleCodexImages(c, operation, void 0, parsed.originalRequest);
|
|
4483
|
+
return await handleCodexImages(c, operation, void 0, await parsed.createRequest(mappedModel));
|
|
4484
|
+
}
|
|
4089
4485
|
// src/routes/images/xai-handler.ts
|
|
4090
4486
|
function xaiImagesRequestErrorResponse(c, error) {
|
|
4091
4487
|
if (!(error instanceof XaiImagesRequestError)) return null;
|
|
@@ -4108,36 +4504,16 @@ async function forwardTranslatedXaiImages(c, operation, payload) {
|
|
|
4108
4504
|
message: "Provider 'xai' not found or disabled",
|
|
4109
4505
|
type: "invalid_request_error"
|
|
4110
4506
|
} }, 404);
|
|
4111
|
-
return await
|
|
4112
|
-
}
|
|
4113
|
-
async function forwardXaiImages(providerConfig, request, operation, payload) {
|
|
4114
|
-
return await ensureImageResponseB64Json(await forwardProviderImagesWithLogging(providerConfig, new Request(request.url, {
|
|
4115
|
-
body: JSON.stringify(payload),
|
|
4116
|
-
headers: {
|
|
4117
|
-
accept: "application/json",
|
|
4118
|
-
"content-type": "application/json"
|
|
4119
|
-
},
|
|
4120
|
-
method: "POST",
|
|
4121
|
-
signal: request.signal
|
|
4122
|
-
}), operation, {
|
|
4507
|
+
return await forwardXaiImagesWithLogging(providerConfig, c.req.raw, operation, payload, {
|
|
4123
4508
|
logger: logger$15,
|
|
4124
4509
|
provider: "xai"
|
|
4125
|
-
}), void 0, {
|
|
4126
|
-
clientSignal: request.signal,
|
|
4127
|
-
onImageUrl: (url) => {
|
|
4128
|
-
logger$15.info("images.xai.output.url", JSON.stringify({
|
|
4129
|
-
operation,
|
|
4130
|
-
url
|
|
4131
|
-
}));
|
|
4132
|
-
},
|
|
4133
|
-
responseFormat: payload.response_format === "url" ? "url" : "b64_json"
|
|
4134
4510
|
});
|
|
4135
4511
|
}
|
|
4136
4512
|
async function handleXaiImageGeneration(c, model) {
|
|
4137
4513
|
return await forwardTranslatedXaiImages(c, "generations", translateXaiImageGeneration(await readJsonBody(c.req.raw, "Image generation body must be JSON"), model));
|
|
4138
4514
|
}
|
|
4139
|
-
async function handleXaiImageEdit(c, model) {
|
|
4140
|
-
return await forwardTranslatedXaiImages(c, "edits", translateXaiImageEditJson(await readJsonBody(c.req.raw, "Image edit body must be JSON"), model));
|
|
4515
|
+
async function handleXaiImageEdit(c, model, multipartPayload) {
|
|
4516
|
+
return await forwardTranslatedXaiImages(c, "edits", translateXaiImageEditJson(multipartPayload ?? await readJsonBody(c.req.raw, "Image edit body must be JSON"), model));
|
|
4141
4517
|
}
|
|
4142
4518
|
// src/routes/images/edits-handler.ts
|
|
4143
4519
|
async function handleImagesEdits(c) {
|
|
@@ -4149,12 +4525,26 @@ async function handleImagesEdits(c) {
|
|
|
4149
4525
|
} }, 400);
|
|
4150
4526
|
const contentTypeError = imageEditContentTypeErrorResponse(c);
|
|
4151
4527
|
if (contentTypeError) return contentTypeError;
|
|
4528
|
+
const multipart = await parseMultipartImageEdit(c.req.raw);
|
|
4529
|
+
if (multipart) try {
|
|
4530
|
+
if (configuredModel.status === "xai") return await handleXaiImageEdit(c, configuredModel.model, await multipart.readPayload());
|
|
4531
|
+
if (multipart.model === void 0) return await handleCodexImages(c, "edits", void 0, await multipart.createRequest(void 0));
|
|
4532
|
+
return await routeImagesRequest(c, "edits", {
|
|
4533
|
+
model: multipart.model,
|
|
4534
|
+
createRequest: (model, provider) => multipart.createRequest(model, provider)
|
|
4535
|
+
});
|
|
4536
|
+
} catch (error) {
|
|
4537
|
+
await multipart.cleanup();
|
|
4538
|
+
throw error;
|
|
4539
|
+
} finally {
|
|
4540
|
+
multipart.scheduleCleanup();
|
|
4541
|
+
}
|
|
4152
4542
|
if (configuredModel.status === "xai") return await handleXaiImageEdit(c, configuredModel.model);
|
|
4153
4543
|
const parsed = await parseJsonImagesRequest(c.req.raw);
|
|
4154
4544
|
if (parsed instanceof Request) return await handleCodexImages(c, "edits", void 0, parsed);
|
|
4155
4545
|
return await routeImagesRequest(c, "edits", parsed);
|
|
4156
4546
|
} catch (error) {
|
|
4157
|
-
const invalidRequest =
|
|
4547
|
+
const invalidRequest = imageEditRequestErrorResponse(c, error);
|
|
4158
4548
|
if (invalidRequest) return invalidRequest;
|
|
4159
4549
|
logger$15.error("images.edits.error", { error });
|
|
4160
4550
|
return await forwardError(c, error);
|
|
@@ -12623,18 +13013,29 @@ async function handleProviderImages(c, operation) {
|
|
|
12623
13013
|
message: `Provider '${provider}' not found or disabled`,
|
|
12624
13014
|
type: "invalid_request_error"
|
|
12625
13015
|
} }, 404);
|
|
12626
|
-
if (providerConfig.name === "codex") {
|
|
12627
|
-
|
|
12628
|
-
|
|
12629
|
-
|
|
13016
|
+
if (operation === "edits" && (providerConfig.name === "codex" || providerConfig.name === "xai")) {
|
|
13017
|
+
const contentTypeError = imageEditContentTypeErrorResponse(c);
|
|
13018
|
+
if (contentTypeError) return contentTypeError;
|
|
13019
|
+
const multipart = await parseMultipartImageEdit(c.req.raw);
|
|
13020
|
+
if (multipart) try {
|
|
13021
|
+
const request = await multipart.createRequest(multipart.model, providerConfig.name);
|
|
13022
|
+
if (providerConfig.name === "codex") return await handleCodexImages(c, operation, providerConfig, request);
|
|
13023
|
+
return await forwardProviderImagesWithLogging(providerConfig, request, operation, {
|
|
13024
|
+
logger: logger$6,
|
|
13025
|
+
provider
|
|
13026
|
+
});
|
|
13027
|
+
} finally {
|
|
13028
|
+
await multipart.cleanup();
|
|
12630
13029
|
}
|
|
12631
|
-
return await handleCodexImages(c, operation, providerConfig);
|
|
12632
13030
|
}
|
|
13031
|
+
if (providerConfig.name === "codex") return await handleCodexImages(c, operation, providerConfig);
|
|
12633
13032
|
return await forwardProviderImagesWithLogging(providerConfig, c.req.raw, operation, {
|
|
12634
13033
|
logger: logger$6,
|
|
12635
13034
|
provider
|
|
12636
13035
|
});
|
|
12637
13036
|
} catch (error) {
|
|
13037
|
+
const invalidRequest = imageEditRequestErrorResponse(c, error);
|
|
13038
|
+
if (invalidRequest) return invalidRequest;
|
|
12638
13039
|
logger$6.error(`provider.images.${operation}.error`, {
|
|
12639
13040
|
provider,
|
|
12640
13041
|
error
|
|
@@ -14623,4 +15024,4 @@ function createServer(options = {}) {
|
|
|
14623
15024
|
createServer();
|
|
14624
15025
|
export { createServer };
|
|
14625
15026
|
|
|
14626
|
-
//# sourceMappingURL=server-
|
|
15027
|
+
//# sourceMappingURL=server-C_-cCnrU.js.map
|