@jeffreycao/copilot-api 2.8.0 → 2.8.1

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.
@@ -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$19 = createHandlerLogger("alpha-search-responses-handler");
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$19, "Alpha search provider Responses request:", {
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$19, "Alpha search provider Responses result:", {
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$19, "Alpha search Copilot Responses request:", payload);
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$19, "Alpha search Copilot Responses result:", result);
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$18 = createHandlerLogger("alpha-search-handler");
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$18, "alpha_search.codex.request", async () => ({ body: parseDebugBody(await request.clone().text()) }));
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$18, "alpha_search.codex.response", async () => ({
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$18.error("alpha_search.error", { error });
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$17 = createHandlerLogger("provider-chat-completions-handler");
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$17, "provider.chat_completions.request", {
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$17.error("Failed to create provider chat completions", {
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$17, "provider.chat_completions.response", responseBody);
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$17.debug("provider.chat_completions.streaming", { provider: options.provider });
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$17, "provider.chat_completions.stream_chunk", chunk);
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$16 = createHandlerLogger("chat-completions-handler");
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$16, "Request payload:", payload);
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$16, "Set max_tokens to:", payload.max_tokens);
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$16.debug("Generated request ID:", requestId);
3337
+ logger$17.debug("Generated request ID:", requestId);
3335
3338
  const sessionId = getUUID(requestId);
3336
- logger$16.debug("Extracted session ID:", sessionId);
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$16, "Non-streaming response:", response);
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$16.debug("Streaming response");
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$16, "Streaming chunk:", chunk);
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/lib/xai-image-model.ts
3503
+ // src/routes/images/temp-form-data.ts
3504
+ const logger$16 = createHandlerLogger("images-temp-form-data");
3505
+ const multipartStagingDependencies = { createWriteStream };
3501
3506
  /**
3502
- * Resolves one image-route model setting. Empty values keep the OpenAI
3503
- * protocol. Only an xai/model alias selects the xAI Imagine API.
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
- function resolveConfiguredXaiImageModel(configured, settingName) {
3506
- if (configured === void 0 || configured === null) return { status: "disabled" };
3507
- if (typeof configured !== "string") return {
3508
- message: `${settingName} must be an xai/model alias`,
3509
- status: "invalid"
3510
- };
3511
- const trimmed = configured.trim();
3512
- if (!trimmed) return { status: "disabled" };
3513
- const alias = parseProviderModelAlias(trimmed);
3514
- if (alias?.provider.toLowerCase() !== "xai") return {
3515
- message: `${settingName} only supports an xai/model alias, received '${trimmed}'`,
3516
- status: "invalid"
3517
- };
3518
- return {
3519
- model: alias.model,
3520
- status: "xai"
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 getImageGenerationModel() {
3524
- return resolveConfiguredXaiImageModel(getConfig().imageGenerationModel, "imageGenerationModel");
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 getImageEditModel() {
3527
- return resolveConfiguredXaiImageModel(getConfig().imageEditModel, "imageEditModel");
3539
+ function toError(error) {
3540
+ return error instanceof Error ? error : new Error(String(error));
3528
3541
  }
3529
- // src/routes/images/forward-provider-images.ts
3530
- /**
3531
- * Forwards an images request to a configured provider with debug logging.
3532
- * Shared by the top-level images routes (model mapping) and the
3533
- * provider-scoped images routes so both log and proxy identically.
3534
- */
3535
- async function forwardProviderImagesWithLogging(providerConfig, request, operation, { logger, provider }) {
3536
- if (operation === "generations") await debugJsonAsync(logger, `provider.images.${operation}.request`, async () => ({
3537
- body: await request.clone().text(),
3538
- provider
3539
- }));
3540
- else debugJson(logger, `provider.images.${operation}.request`, {
3541
- contentType: request.headers.get("content-type"),
3542
- contentLength: request.headers.get("content-length"),
3543
- provider
3544
- });
3545
- const upstreamResponse = await forwardProviderImages(providerConfig, request, operation);
3546
- debugJson(logger, `provider.images.${operation}.response`, {
3547
- contentType: upstreamResponse.headers.get("content-type"),
3548
- contentLength: upstreamResponse.headers.get("content-length"),
3549
- provider,
3550
- statusCode: upstreamResponse.status
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
+ });
3551
3646
  });
3552
- return createProviderProxyResponse(upstreamResponse);
3553
- }
3554
- // src/services/codex/images.ts
3555
- const CODEX_IMAGES_TIMEOUT_MS = 9e5;
3556
- const codexImagesDispatcher = createTimeoutDispatcher(CODEX_IMAGES_TIMEOUT_MS);
3557
- function resolveCodexImagesUrl(requestUrl, operation) {
3558
- const upstreamUrl = new URL(`${CODEX_API_BASE_URL}/codex/images/${operation}`);
3559
- upstreamUrl.search = new URL(requestUrl, "http://localhost").search;
3560
- return upstreamUrl.toString();
3561
3647
  }
3562
- async function forwardCodexImages(request, operation) {
3563
- const headers = buildCodexRequestHeaders(request.headers);
3564
- if (!headers.has("accept")) headers.set("accept", "application/json");
3565
- if (operation === "generations" && !headers.has("content-type")) headers.set("content-type", "application/json");
3566
- const init = {
3567
- method: "POST",
3568
- headers,
3569
- body: request.body,
3570
- duplex: "half",
3571
- signal: AbortSignal.timeout(CODEX_IMAGES_TIMEOUT_MS)
3572
- };
3573
- const upstreamUrl = resolveCodexImagesUrl(request.url, operation);
3574
- if (typeof Bun !== "undefined") return await fetch(upstreamUrl, init);
3575
- return await fetch$1(upstreamUrl, {
3576
- ...init,
3577
- dispatcher: codexImagesDispatcher
3578
- });
3579
- }
3580
- // src/routes/images/shared.ts
3581
- const logger$15 = createHandlerLogger("images-handler");
3582
- const imageRouteDependencies = {
3583
- debugJsonAsync,
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
- * Snapshots generated request headers before consuming the body.
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 snapshotRequestHeaders(request) {
3606
- return new Headers(request.headers);
3607
- }
3608
- /**
3609
- * Rebuilds a request from primitives instead of cloning: under Node runtimes
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
- const bodyText = new TextDecoder("utf-8", { fatal: true }).decode(body);
3644
- payload = JSON.parse(bodyText);
3645
- } catch {
3646
- return originalRequest;
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
- if (payload === null || typeof payload !== "object" || Array.isArray(payload) || typeof payload.model !== "string") return originalRequest;
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
- if (!(resolvedProviderConfig ?? await imageRouteDependencies.resolveProviderConfig("codex"))) return c.json({ error: {
3663
- message: "Provider 'codex' not found or disabled",
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
- logger$15.error(`images.${operation}.codex.error`, { error });
3676
- return await forwardError(c, error);
3681
+ await rm(directory, {
3682
+ force: true,
3683
+ recursive: true
3684
+ });
3685
+ throw error;
3677
3686
  }
3678
- }
3679
- /**
3680
- * Routes a parsed images request: applies the model mapping, forwards to a
3681
- * mapped provider when configured, and otherwise falls back to Codex.
3682
- */
3683
- async function routeImagesRequest(c, operation, parsed) {
3684
- const requestedModel = parsed.model;
3685
- const mappedModel = imageRouteDependencies.resolveMappedModel(requestedModel);
3686
- if (mappedModel !== requestedModel) consola.debug(`Resolved model mapping: ${requestedModel} -> ${mappedModel}`);
3687
- const providerModelAlias = parseProviderModelAlias(mappedModel);
3688
- if (providerModelAlias) {
3689
- const providerConfig = await imageRouteDependencies.resolveProviderConfig(providerModelAlias.provider);
3690
- if (providerConfig) {
3691
- const request = parsed.createRequest(providerModelAlias.model);
3692
- if (providerConfig.name === "codex") return await handleCodexImages(c, operation, providerConfig, request);
3693
- return await forwardProviderImagesWithLogging(providerConfig, request, operation, {
3694
- logger: logger$15,
3695
- provider: providerModelAlias.provider
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
- consola.debug(`Provider '${providerModelAlias.provider}' not found or disabled; forwarding the original model to Codex`);
3699
- return await handleCodexImages(c, operation, void 0, parsed.originalRequest ?? parsed.createRequest(requestedModel));
3700
- }
3701
- if (parsed.originalRequest && mappedModel === requestedModel) return await handleCodexImages(c, operation, void 0, parsed.originalRequest);
3702
- return await handleCodexImages(c, operation, void 0, parsed.createRequest(mappedModel));
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,331 @@ 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
+ // src/services/codex/images.ts
4304
+ const CODEX_IMAGES_TIMEOUT_MS = 9e5;
4305
+ const codexImagesDispatcher = createTimeoutDispatcher(CODEX_IMAGES_TIMEOUT_MS);
4306
+ function resolveCodexImagesUrl(requestUrl, operation) {
4307
+ const upstreamUrl = new URL(`${CODEX_API_BASE_URL}/codex/images/${operation}`);
4308
+ upstreamUrl.search = new URL(requestUrl, "http://localhost").search;
4309
+ return upstreamUrl.toString();
4310
+ }
4311
+ async function forwardCodexImages(request, operation) {
4312
+ const headers = buildCodexRequestHeaders(request.headers);
4313
+ if (!headers.has("accept")) headers.set("accept", "application/json");
4314
+ if (operation === "generations" && !headers.has("content-type")) headers.set("content-type", "application/json");
4315
+ const init = {
4316
+ method: "POST",
4317
+ headers,
4318
+ body: request.body,
4319
+ duplex: "half",
4320
+ signal: AbortSignal.any([request.signal, AbortSignal.timeout(CODEX_IMAGES_TIMEOUT_MS)])
4321
+ };
4322
+ const upstreamUrl = resolveCodexImagesUrl(request.url, operation);
4323
+ if (typeof Bun !== "undefined") return await fetch(upstreamUrl, init);
4324
+ return await fetch$1(upstreamUrl, {
4325
+ ...init,
4326
+ dispatcher: codexImagesDispatcher
4327
+ });
4328
+ }
4329
+ // src/routes/images/shared.ts
4330
+ const logger$15 = createHandlerLogger("images-handler");
4331
+ const imageRouteDependencies = {
4332
+ debugJsonAsync,
4333
+ getImageEditModel,
4334
+ getImageGenerationModel,
4335
+ resolveMappedModel,
4336
+ resolveProviderConfig
4337
+ };
4338
+ function imageEditContentTypeErrorResponse(c) {
4339
+ const mediaType = c.req.header("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
4340
+ if (mediaType === "application/json" || mediaType === "multipart/form-data") return null;
4341
+ return c.json({ error: {
4342
+ message: "Image edits require application/json or multipart/form-data",
4343
+ type: "invalid_request_error"
4344
+ } }, 415);
4345
+ }
4346
+ function getContentMetadata(headers) {
4347
+ return {
4348
+ contentType: headers.get("content-type"),
4349
+ contentLength: headers.get("content-length")
4350
+ };
4351
+ }
4352
+ /**
4353
+ * Snapshots generated request headers before consuming the body.
4354
+ */
4355
+ function snapshotRequestHeaders(request) {
4356
+ return new Headers(request.headers);
4357
+ }
4358
+ /**
4359
+ * Rebuilds a request from primitives instead of cloning: under Node runtimes
4360
+ * srvx wraps incoming requests in a proxy class whose prototype chain
4361
+ * satisfies `instanceof Request` without the native internals, so
4362
+ * `new Request(request)` throws.
4363
+ */
4364
+ function createForwardRequest(request, requestHeaders, body) {
4365
+ const init = {
4366
+ body,
4367
+ duplex: "half",
4368
+ headers: requestHeaders,
4369
+ method: request.method,
4370
+ signal: request.signal
4371
+ };
4372
+ return new Request(request.url, init);
4373
+ }
4374
+ function createJsonImagesRequest(request, requestHeaders, payload, model) {
4375
+ const headers = new Headers(requestHeaders);
4376
+ headers.delete("content-length");
4377
+ return createForwardRequest(request, headers, new TextEncoder().encode(JSON.stringify({
4378
+ ...payload,
4379
+ model
4380
+ })));
4381
+ }
4382
+ /**
4383
+ * Parses an image request body as JSON, retaining the original bytes for
4384
+ * unchanged forwarding while allowing model mapping to rebuild the payload.
4385
+ * Rebuilt bodies stay binary so Node does not synthesize a text/plain content type.
4386
+ */
4387
+ async function parseJsonImagesRequest(request) {
4388
+ const requestHeaders = snapshotRequestHeaders(request);
4389
+ const body = new Uint8Array(await request.arrayBuffer());
4390
+ const originalRequest = createForwardRequest(request, requestHeaders, body);
4391
+ let payload;
4392
+ try {
4393
+ const bodyText = new TextDecoder("utf-8", { fatal: true }).decode(body);
4394
+ payload = JSON.parse(bodyText);
4395
+ } catch {
4396
+ return originalRequest;
4397
+ }
4398
+ if (payload === null || typeof payload !== "object" || Array.isArray(payload) || typeof payload.model !== "string") return originalRequest;
4399
+ return {
4400
+ createRequest: (mappedModel) => createJsonImagesRequest(request, requestHeaders, payload, mappedModel),
4401
+ model: payload.model,
4402
+ originalRequest
4403
+ };
4404
+ }
4405
+ /**
4406
+ * Handles Codex images proxying. Pass a resolvedProviderConfig when the
4407
+ * caller already resolved the codex provider to avoid a second resolve, and
4408
+ * pass a request when model mapping rebuilt the request body.
4409
+ */
4410
+ async function handleCodexImages(c, operation, resolvedProviderConfig, request = c.req.raw) {
4411
+ try {
4412
+ if (!(resolvedProviderConfig ?? await imageRouteDependencies.resolveProviderConfig("codex"))) return c.json({ error: {
4413
+ message: "Provider 'codex' not found or disabled",
4414
+ type: "invalid_request_error"
4415
+ } }, 404);
4416
+ if (operation === "generations") await imageRouteDependencies.debugJsonAsync(logger$15, "images.generations.codex.request", async () => ({ body: await request.clone().text() }));
4417
+ else debugJson(logger$15, "images.edits.codex.request", getContentMetadata(request.headers));
4418
+ const upstreamResponse = await forwardCodexImages(request, operation);
4419
+ debugJson(logger$15, `images.${operation}.codex.response`, {
4420
+ ...getContentMetadata(upstreamResponse.headers),
4421
+ statusCode: upstreamResponse.status
4422
+ });
4423
+ return createProviderProxyResponse(upstreamResponse);
4424
+ } catch (error) {
4425
+ logger$15.error(`images.${operation}.codex.error`, { error });
4426
+ return await forwardError(c, error);
4427
+ }
4428
+ }
4429
+ /**
4430
+ * Routes a parsed images request: applies the model mapping, forwards to a
4431
+ * mapped provider when configured, and otherwise falls back to Codex.
4432
+ */
4433
+ async function routeImagesRequest(c, operation, parsed) {
4434
+ const requestedModel = parsed.model;
4435
+ const mappedModel = imageRouteDependencies.resolveMappedModel(requestedModel);
4436
+ if (mappedModel !== requestedModel) consola.debug(`Resolved model mapping: ${requestedModel} -> ${mappedModel}`);
4437
+ const providerModelAlias = parseProviderModelAlias(mappedModel);
4438
+ if (providerModelAlias) {
4439
+ const providerConfig = await imageRouteDependencies.resolveProviderConfig(providerModelAlias.provider);
4440
+ if (providerConfig) {
4441
+ const request = await parsed.createRequest(providerModelAlias.model, providerConfig.name);
4442
+ if (providerConfig.name === "codex") return await handleCodexImages(c, operation, providerConfig, request);
4443
+ return await forwardProviderImagesWithLogging(providerConfig, request, operation, {
4444
+ logger: logger$15,
4445
+ provider: providerModelAlias.provider
4446
+ });
4447
+ }
4448
+ consola.debug(`Provider '${providerModelAlias.provider}' not found or disabled; forwarding the original model to Codex`);
4449
+ return await handleCodexImages(c, operation, void 0, parsed.originalRequest ?? await parsed.createRequest(requestedModel));
4450
+ }
4451
+ if (parsed.originalRequest && mappedModel === requestedModel) return await handleCodexImages(c, operation, void 0, parsed.originalRequest);
4452
+ return await handleCodexImages(c, operation, void 0, await parsed.createRequest(mappedModel));
4453
+ }
4089
4454
  // src/routes/images/xai-handler.ts
4090
4455
  function xaiImagesRequestErrorResponse(c, error) {
4091
4456
  if (!(error instanceof XaiImagesRequestError)) return null;
@@ -4136,8 +4501,8 @@ async function forwardXaiImages(providerConfig, request, operation, payload) {
4136
4501
  async function handleXaiImageGeneration(c, model) {
4137
4502
  return await forwardTranslatedXaiImages(c, "generations", translateXaiImageGeneration(await readJsonBody(c.req.raw, "Image generation body must be JSON"), model));
4138
4503
  }
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));
4504
+ async function handleXaiImageEdit(c, model, multipartPayload) {
4505
+ return await forwardTranslatedXaiImages(c, "edits", translateXaiImageEditJson(multipartPayload ?? await readJsonBody(c.req.raw, "Image edit body must be JSON"), model));
4141
4506
  }
4142
4507
  // src/routes/images/edits-handler.ts
4143
4508
  async function handleImagesEdits(c) {
@@ -4149,12 +4514,26 @@ async function handleImagesEdits(c) {
4149
4514
  } }, 400);
4150
4515
  const contentTypeError = imageEditContentTypeErrorResponse(c);
4151
4516
  if (contentTypeError) return contentTypeError;
4517
+ const multipart = await parseMultipartImageEdit(c.req.raw);
4518
+ if (multipart) try {
4519
+ if (configuredModel.status === "xai") return await handleXaiImageEdit(c, configuredModel.model, await multipart.readPayload());
4520
+ if (multipart.model === void 0) return await handleCodexImages(c, "edits", void 0, await multipart.createRequest(void 0));
4521
+ return await routeImagesRequest(c, "edits", {
4522
+ model: multipart.model,
4523
+ createRequest: (model, provider) => multipart.createRequest(model, provider)
4524
+ });
4525
+ } catch (error) {
4526
+ await multipart.cleanup();
4527
+ throw error;
4528
+ } finally {
4529
+ multipart.scheduleCleanup();
4530
+ }
4152
4531
  if (configuredModel.status === "xai") return await handleXaiImageEdit(c, configuredModel.model);
4153
4532
  const parsed = await parseJsonImagesRequest(c.req.raw);
4154
4533
  if (parsed instanceof Request) return await handleCodexImages(c, "edits", void 0, parsed);
4155
4534
  return await routeImagesRequest(c, "edits", parsed);
4156
4535
  } catch (error) {
4157
- const invalidRequest = xaiImagesRequestErrorResponse(c, error);
4536
+ const invalidRequest = imageEditRequestErrorResponse(c, error);
4158
4537
  if (invalidRequest) return invalidRequest;
4159
4538
  logger$15.error("images.edits.error", { error });
4160
4539
  return await forwardError(c, error);
@@ -12623,18 +13002,29 @@ async function handleProviderImages(c, operation) {
12623
13002
  message: `Provider '${provider}' not found or disabled`,
12624
13003
  type: "invalid_request_error"
12625
13004
  } }, 404);
12626
- if (providerConfig.name === "codex") {
12627
- if (operation === "edits") {
12628
- const contentTypeError = imageEditContentTypeErrorResponse(c);
12629
- if (contentTypeError) return contentTypeError;
13005
+ if (operation === "edits" && (providerConfig.name === "codex" || providerConfig.name === "xai")) {
13006
+ const contentTypeError = imageEditContentTypeErrorResponse(c);
13007
+ if (contentTypeError) return contentTypeError;
13008
+ const multipart = await parseMultipartImageEdit(c.req.raw);
13009
+ if (multipart) try {
13010
+ const request = await multipart.createRequest(multipart.model, providerConfig.name);
13011
+ if (providerConfig.name === "codex") return await handleCodexImages(c, operation, providerConfig, request);
13012
+ return await forwardProviderImagesWithLogging(providerConfig, request, operation, {
13013
+ logger: logger$6,
13014
+ provider
13015
+ });
13016
+ } finally {
13017
+ await multipart.cleanup();
12630
13018
  }
12631
- return await handleCodexImages(c, operation, providerConfig);
12632
13019
  }
13020
+ if (providerConfig.name === "codex") return await handleCodexImages(c, operation, providerConfig);
12633
13021
  return await forwardProviderImagesWithLogging(providerConfig, c.req.raw, operation, {
12634
13022
  logger: logger$6,
12635
13023
  provider
12636
13024
  });
12637
13025
  } catch (error) {
13026
+ const invalidRequest = imageEditRequestErrorResponse(c, error);
13027
+ if (invalidRequest) return invalidRequest;
12638
13028
  logger$6.error(`provider.images.${operation}.error`, {
12639
13029
  provider,
12640
13030
  error
@@ -14623,4 +15013,4 @@ function createServer(options = {}) {
14623
15013
  createServer();
14624
15014
  export { createServer };
14625
15015
 
14626
- //# sourceMappingURL=server-Bz8hGP4q.js.map
15016
+ //# sourceMappingURL=server-CwSW3Oz8.js.map