@jeffreycao/copilot-api 2.8.3 → 2.8.4

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.
Files changed (26) hide show
  1. package/dist/{auth-b8dJfIDO.js → auth-BTHQn38N.js} +4 -4
  2. package/dist/{auth-b8dJfIDO.js.map → auth-BTHQn38N.js.map} +1 -1
  3. package/dist/auth-D7DnNvEO.js +1 -0
  4. package/dist/{config-B55PwJzQ.js → config-BRBygEXH.js} +2 -2
  5. package/dist/{config-B55PwJzQ.js.map → config-BRBygEXH.js.map} +1 -1
  6. package/dist/{config-store-DFcv4AY0.js → config-store-SwGZOmmG.js} +2 -1
  7. package/dist/config-store-SwGZOmmG.js.map +1 -0
  8. package/dist/{copilot-runtime-DNKNavLK.js → copilot-runtime-D--d2K7y.js} +3 -3
  9. package/dist/{copilot-runtime-DNKNavLK.js.map → copilot-runtime-D--d2K7y.js.map} +1 -1
  10. package/dist/{debug-1bQIZTcG.js → debug-8a37y6YE.js} +3 -3
  11. package/dist/{debug-1bQIZTcG.js.map → debug-8a37y6YE.js.map} +1 -1
  12. package/dist/main.js +4 -4
  13. package/dist/{provider-B8UJ434o.js → provider-CqQ4brtz.js} +2 -2
  14. package/dist/{provider-B8UJ434o.js.map → provider-CqQ4brtz.js.map} +1 -1
  15. package/dist/{provider-management-C6ZgkTJ7.js → provider-management-BDXwU7oJ.js} +2 -2
  16. package/dist/{provider-management-C6ZgkTJ7.js.map → provider-management-BDXwU7oJ.js.map} +1 -1
  17. package/dist/{server-Cw9qn5Gt.js → server-De8OQQe6.js} +1205 -1402
  18. package/dist/server-De8OQQe6.js.map +1 -0
  19. package/dist/{start-CPoEuVzr.js → start-vkJBrxcJ.js} +7 -7
  20. package/dist/{start-CPoEuVzr.js.map → start-vkJBrxcJ.js.map} +1 -1
  21. package/dist/{token-DFlvOMh2.js → token-CMboJXYu.js} +3 -3
  22. package/dist/{token-DFlvOMh2.js.map → token-CMboJXYu.js.map} +1 -1
  23. package/package.json +1 -1
  24. package/dist/auth-BFV7bzJo.js +0 -1
  25. package/dist/config-store-DFcv4AY0.js.map +0 -1
  26. package/dist/server-Cw9qn5Gt.js.map +0 -1
@@ -1,9 +1,9 @@
1
- import { D as isGitHubCopilotEnabled, E as isGitHubCopilotAvailable, M as forwardError, O as state, S as PATHS, T as assertGitHubCopilotEnabled, a as getAnthropicApiKey, c as getConfig, d as getUpstreamTransportConfig, g as isResponsesApiWebSocketEnabled, h as isResponsesApiWebSearchEnabled, i as getAlphaSearchModel, k as HTTPError, l as getMessageApiWebSearchModel, m as isMessagesApiEnabled, o as getClaudeAutoModel, p as isAlphaSearchCodexPriorityEnabled, s as getClaudeTokenMultiplier, u as getOpencodeModelContextWindow, w as GITHUB_COPILOT_PROVIDER, y as reloadConfig } from "./config-store-DFcv4AY0.js";
2
- import { A as getSmallModelForProvider, D as getModelResponsesApiCompactThreshold$1, E as getModelMappings, F as resolveMappedModel, I as setModelMappings, M as isContextManagementEnabledForResponses, N as isGpt56OrAbove, O as getReasoningEffortForModel, P as isOutputEncryptedContentDisabled, S as getModelsDevModelPricing, T as getExtraPromptForModel, _ as DEFAULT_IMAGE_MODELS, a as normalizeProviderBaseUrl, b as getModelsDevModel, d as isProviderModelVisible, f as resolveProviderModelsDevId, g as getBuiltinProviderModelRecords, h as getBuiltinProviderImageModelRecords, i as listEnabledProviders, j as isContextManagementEnabledForMessages, k as getSmallModel, l as getLocalProviderModelRecords, m as builtinProviderModelRegistry, n as getRawProviderConfig, o as resolveEffectiveProviderType, p as usesModelsDevModelCatalog, s as resolveProviderConfigForModel, t as getProviderConfig, u as getProviderModelConfig, v as IMAGE_PROVIDER_PRIORITY, x as getModelsDevModelMaxOutputTokens, y as getDefaultImageModel } from "./config-B55PwJzQ.js";
3
- import { $ as buildCodexRequestHeaders, B as getXaiAccessToken, C as copilotHeaders, D as prepareForCompact, E as isOpencodeOauthApp, F as compactTextOnlyGuard, I as createAuthMiddleware, J as readGitHubTokenFromEnv, L as getConfiguredAdminApiKeys, M as compactMessageSections, O as prepareInteractionHeaders, P as compactSystemPromptStarts, Q as CODEX_API_BASE_URL, S as copilotBaseUrl, T as copilotWebSocketHeaders, U as readXaiCredentials, Y as XAI_API_BASE_URL, _ as isAsyncIterable, at as setPluginRequestActualModel, b as parseUserIdMetadata, ct as createResponsesSafeStream, d as generateRequestIdFromPayload, dt as createPooledWebSocketStream, et as forwardCodexResponses, f as getPromptCacheKey, ft as createWebSocketUrl, g as getUUID, h as getSessionAffinityFromHeaders, it as resolveTraceId$1, j as compactAutoContinuePromptStarts, k as prepareMessageProxyHeaders, lt as encodePoolKeyPart, m as getRootSessionIdFromHeaders, mt as stripInternalRequestHeaders, n as invalidateCodexRuntime, nt as generateTraceId, ot as fetchUpstreamWithLifecycle, p as getRootSessionId, q as readGitHubToken, rt as requestContext, s as setupCodexToken, st as createResponsesHttpEventStream, tt as formatPluginActualModel, u as stopCopilotRefreshLoop, ut as getResponsesStreamTerminalDisposition, v as isNullish, x as getCopilotUsage, y as isResponsesStream } from "./token-DFlvOMh2.js";
1
+ import { D as isGitHubCopilotEnabled, E as isGitHubCopilotAvailable, M as forwardError, O as state, S as PATHS, T as assertGitHubCopilotEnabled, a as getAnthropicApiKey, c as getConfig, d as getUpstreamTransportConfig, g as isResponsesApiWebSocketEnabled, h as isResponsesApiWebSearchEnabled, i as getAlphaSearchModel, k as HTTPError, l as getMessageApiWebSearchModel, m as isMessagesApiEnabled, o as getClaudeAutoModel, p as isAlphaSearchCodexPriorityEnabled, s as getClaudeTokenMultiplier, u as getOpencodeModelContextWindow, w as GITHUB_COPILOT_PROVIDER, y as reloadConfig } from "./config-store-SwGZOmmG.js";
2
+ import { A as getSmallModelForProvider, D as getModelResponsesApiCompactThreshold$1, E as getModelMappings, F as resolveMappedModel, I as setModelMappings, M as isContextManagementEnabledForResponses, N as isGpt56OrAbove, O as getReasoningEffortForModel, P as isOutputEncryptedContentDisabled, S as getModelsDevModelPricing, T as getExtraPromptForModel, _ as DEFAULT_IMAGE_MODELS, a as normalizeProviderBaseUrl, b as getModelsDevModel, d as isProviderModelVisible, f as resolveProviderModelsDevId, g as getBuiltinProviderModelRecords, h as getBuiltinProviderImageModelRecords, i as listEnabledProviders, j as isContextManagementEnabledForMessages, k as getSmallModel, l as getLocalProviderModelRecords, m as builtinProviderModelRegistry, n as getRawProviderConfig, o as resolveEffectiveProviderType, p as usesModelsDevModelCatalog, s as resolveProviderConfigForModel, t as getProviderConfig, u as getProviderModelConfig, v as IMAGE_PROVIDER_PRIORITY, x as getModelsDevModelMaxOutputTokens, y as getDefaultImageModel } from "./config-BRBygEXH.js";
3
+ import { $ as buildCodexRequestHeaders, B as getXaiAccessToken, C as copilotHeaders, D as prepareForCompact, E as isOpencodeOauthApp, F as compactTextOnlyGuard, I as createAuthMiddleware, J as readGitHubTokenFromEnv, L as getConfiguredAdminApiKeys, M as compactMessageSections, O as prepareInteractionHeaders, P as compactSystemPromptStarts, Q as CODEX_API_BASE_URL, S as copilotBaseUrl, T as copilotWebSocketHeaders, U as readXaiCredentials, Y as XAI_API_BASE_URL, _ as isAsyncIterable, at as setPluginRequestActualModel, b as parseUserIdMetadata, ct as createResponsesSafeStream, d as generateRequestIdFromPayload, dt as createPooledWebSocketStream, et as forwardCodexResponses, f as getPromptCacheKey, ft as createWebSocketUrl, g as getUUID, h as getSessionAffinityFromHeaders, it as resolveTraceId$1, j as compactAutoContinuePromptStarts, k as prepareMessageProxyHeaders, lt as encodePoolKeyPart, m as getRootSessionIdFromHeaders, mt as stripInternalRequestHeaders, n as invalidateCodexRuntime, nt as generateTraceId, ot as fetchUpstreamWithLifecycle, p as getRootSessionId, q as readGitHubToken, rt as requestContext, s as setupCodexToken, st as createResponsesHttpEventStream, tt as formatPluginActualModel, u as stopCopilotRefreshLoop, ut as getResponsesStreamTerminalDisposition, v as isNullish, x as getCopilotUsage, y as isResponsesStream } from "./token-CMboJXYu.js";
4
4
  import { a as isDeferredToolName, c as parseMcpToolSearchSentinel, d as shouldEnableResponsesToolSearch, i as isBridgeToolSearchName, l as resolveBridgeToolSearchName, o as listDeferredToolNames, r as formatToolSearchBridgeArguments, s as normalizeToolSearchBridgeArguments, t as BRIDGE_TOOL_SEARCH_NAME, u as selectDeferredToolsByNames } from "./tool-search-DzipwtCX.js";
5
- import { n as isProviderAgentModelVisible, t as getProviderAgentModels } from "./provider-management-C6ZgkTJ7.js";
6
- import { i as registerProcessCleanup, n as stopModelsRefreshLoop, r as EventBus, t as setupCopilotRuntime } from "./copilot-runtime-DNKNavLK.js";
5
+ import { n as isProviderAgentModelVisible, t as getProviderAgentModels } from "./provider-management-BDXwU7oJ.js";
6
+ import { i as registerProcessCleanup, n as stopModelsRefreshLoop, r as EventBus, t as setupCopilotRuntime } from "./copilot-runtime-D--d2K7y.js";
7
7
  import consola from "consola";
8
8
  import { createHash, randomBytes, randomUUID } from "node:crypto";
9
9
  import fs, { createReadStream, createWriteStream, readFileSync, rmSync } from "node:fs";
@@ -18,12 +18,12 @@ 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";
24
21
  import { none } from "stream-chain/core";
25
22
  import { jsonParser } from "stream-json/core/parser.js";
26
23
  import stringer from "stream-json/core/stringer.js";
24
+ import { Busboy } from "@fastify/busboy";
25
+ import { Readable, Transform } from "node:stream";
26
+ import { pipeline } from "node:stream/promises";
27
27
  import { decompress } from "fzstd";
28
28
  // src/lib/trace.ts
29
29
  const traceIdMiddleware = async (c, next) => {
@@ -3566,1170 +3566,1188 @@ embeddingRoutes.post("/", async (c) => {
3566
3566
  return await forwardError(c, error);
3567
3567
  }
3568
3568
  });
3569
- // src/routes/images/temp-form-data.ts
3570
- const logger$16 = createHandlerLogger("images-temp-form-data");
3571
- const multipartStagingDependencies = { createWriteStream };
3572
- /**
3573
- * Matches the images forwarding timeouts: an upstream upload may take that
3574
- * long, so staged files must survive at least the whole forwarding window.
3575
- */
3576
- const STAGED_FILES_TTL_MS = 9e5;
3577
- const DEFAULT_MULTIPART_STAGING_LIMITS = {
3578
- maxBodySizeBytes: 134217728,
3579
- maxFieldSizeBytes: 10485760,
3580
- maxFields: 64,
3581
- maxFileSizeBytes: 67108864,
3582
- maxFiles: 16,
3583
- maxParts: 80
3584
- };
3585
- var InvalidMultipartBodyError = class extends Error {
3586
- constructor(options) {
3587
- super("Invalid multipart form data body", options);
3588
- this.name = "InvalidMultipartBodyError";
3589
- }
3590
- };
3591
- var MultipartBodyTooLargeError = class extends Error {
3592
- constructor() {
3593
- super("Multipart form data body exceeds the configured upload limits");
3594
- this.name = "MultipartBodyTooLargeError";
3595
- }
3596
- };
3597
- function sanitizeFileName(name) {
3598
- return name.split(/[\\/]/).pop()?.replaceAll("\0", "") || "file";
3599
- }
3600
- async function createDiskBackedBlob(filePath, type) {
3601
- if (typeof Bun !== "undefined") return Bun.file(filePath, { type });
3602
- const { openAsBlob } = await import("node:fs");
3603
- return await openAsBlob(filePath, { type });
3604
- }
3605
- function toError(error) {
3606
- return error instanceof Error ? error : new Error(String(error));
3569
+ // src/lib/image-json-files.ts
3570
+ const IMAGE_JSON_MAX_BYTES = 536870912;
3571
+ function imageJsonLimitError() {
3572
+ return new HTTPError("Image JSON upload exceeds the staging limit", Response.json({ error: {
3573
+ message: "Image JSON upload exceeds the staging limit",
3574
+ type: "invalid_request_error"
3575
+ } }, { status: 413 }));
3607
3576
  }
3608
- function parseMultipartBody(body, contentType, directory, entries, limits, signal) {
3609
- return new Promise((resolve, reject) => {
3610
- let busboy;
3611
- try {
3612
- busboy = new Busboy({
3613
- headers: { "content-type": contentType },
3614
- limits: {
3615
- fieldSize: limits.maxFieldSizeBytes,
3616
- fields: limits.maxFields,
3617
- fileSize: limits.maxFileSizeBytes,
3618
- files: limits.maxFiles,
3619
- parts: limits.maxParts
3620
- }
3621
- });
3622
- } catch (error) {
3623
- reject(new InvalidMultipartBodyError({ cause: toError(error) }));
3624
- return;
3625
- }
3626
- const pendingWrites = [];
3627
- const activeFileStreams = /* @__PURE__ */ new Set();
3628
- const source = body === null ? Readable.from([]) : Readable.fromWeb(body);
3629
- let bodySizeBytes = 0;
3630
- const bodyLimiter = new Transform({ transform(chunk, _encoding, callback) {
3631
- bodySizeBytes += chunk.byteLength;
3632
- if (bodySizeBytes > limits.maxBodySizeBytes) {
3633
- callback(new MultipartBodyTooLargeError());
3634
- return;
3635
- }
3636
- callback(null, chunk);
3637
- } });
3638
- let fileIndex = 0;
3639
- let failure = null;
3640
- let inputPipeline = null;
3641
- let settled = false;
3642
- const settleStreams = () => Promise.allSettled([...inputPipeline ? [inputPipeline] : [], ...pendingWrites]);
3643
- const fail = (error) => {
3644
- if (failure || settled) return;
3645
- const failureError = toError(error);
3646
- failure = failureError;
3647
- source.destroy(failureError);
3648
- bodyLimiter.destroy(failureError);
3649
- busboy.destroy(failureError);
3650
- for (const stream of activeFileStreams) stream.destroy(failureError);
3651
- settleStreams().then(() => {
3652
- if (settled) return;
3653
- settled = true;
3654
- reject(failureError);
3655
- });
3656
- };
3657
- const failLimit = () => fail(new MultipartBodyTooLargeError());
3658
- busboy.on("field", (name, value, nameTruncated, valueTruncated) => {
3659
- if (nameTruncated || valueTruncated) {
3660
- failLimit();
3661
- return;
3662
- }
3663
- entries.push({
3664
- kind: "field",
3665
- name,
3666
- value
3667
- });
3668
- });
3669
- busboy.on("file", (name, stream, fileName, _transferEncoding, mimeType) => {
3670
- if (failure) {
3671
- stream.resume();
3672
- return;
3673
- }
3674
- const filePath = join(directory, `${fileIndex}.upload`);
3675
- fileIndex += 1;
3676
- entries.push({
3677
- fileName: sanitizeFileName(fileName),
3678
- filePath,
3679
- kind: "file",
3680
- mimeType,
3681
- name
3682
- });
3683
- activeFileStreams.add(stream);
3684
- stream.once("error", fail);
3685
- stream.once("close", () => activeFileStreams.delete(stream));
3686
- stream.once("limit", failLimit);
3687
- try {
3688
- const write = pipeline(stream, multipartStagingDependencies.createWriteStream(filePath));
3689
- pendingWrites.push(write);
3690
- write.catch(fail);
3691
- } catch (error) {
3692
- fail(error);
3693
- }
3694
- });
3695
- busboy.on("fieldsLimit", failLimit);
3696
- busboy.on("filesLimit", failLimit);
3697
- busboy.on("partsLimit", failLimit);
3698
- inputPipeline = pipeline(source, bodyLimiter, busboy, { signal });
3699
- inputPipeline.then(() => {
3700
- Promise.allSettled(pendingWrites).then((results) => {
3701
- if (failure || settled) return;
3702
- const failed = results.find((result) => result.status === "rejected");
3703
- if (failed) fail(failed.reason);
3704
- else {
3705
- settled = true;
3706
- resolve();
3707
- }
3708
- });
3709
- }, (error) => {
3710
- fail(error instanceof MultipartBodyTooLargeError || signal?.aborted ? error : new InvalidMultipartBodyError({ cause: toError(error) }));
3577
+ /** Owns only an mkdtemp-created directory; callers never supply deletion paths. */
3578
+ async function createImageJsonFiles(ttlMs = 9e5) {
3579
+ const directory = await mkdtemp(join(tmpdir(), "copilot-image-json-"));
3580
+ let index = 0;
3581
+ let timer;
3582
+ let cleanupPromise;
3583
+ const unregister = registerProcessCleanup(() => {
3584
+ if (timer) clearTimeout(timer);
3585
+ rmSync(directory, {
3586
+ recursive: true,
3587
+ force: true
3711
3588
  });
3712
3589
  });
3713
- }
3714
- function resolveStagingLimits(overrides) {
3715
- const limits = {
3716
- ...DEFAULT_MULTIPART_STAGING_LIMITS,
3717
- ...overrides
3718
- };
3719
- for (const [name, value] of Object.entries(limits)) if (!Number.isSafeInteger(value) || value <= 0) throw new RangeError(`${name} must be a positive safe integer`);
3720
- return limits;
3721
- }
3722
- /**
3723
- * Streams a multipart request body through the parser, writing every file
3724
- * part to a temporary directory as it arrives and rebuilding the form data
3725
- * with disk-backed blobs, so forwarding streams payloads from disk instead
3726
- * of pinning the whole upload in memory.
3727
- */
3728
- async function stageMultipartBodyToDisk(body, contentType, limitOverrides = {}, signal) {
3729
- signal?.throwIfAborted();
3730
- const limits = resolveStagingLimits(limitOverrides);
3731
- const directory = await mkdtemp(join(tmpdir(), "copilot-api-images-"));
3732
- const entries = [];
3733
- try {
3734
- await parseMultipartBody(body, contentType, directory, entries, limits, signal);
3735
- } catch (error) {
3736
- await rm(directory, {
3737
- force: true,
3738
- recursive: true
3739
- });
3740
- throw error;
3741
- }
3742
- const stagedFormData = new FormData();
3743
- try {
3744
- for (const entry of entries) if (entry.kind === "field") stagedFormData.append(entry.name, entry.value);
3745
- else stagedFormData.append(entry.name, await createDiskBackedBlob(entry.filePath, entry.mimeType), entry.fileName);
3746
- } catch (error) {
3747
- await rm(directory, {
3748
- force: true,
3749
- recursive: true
3750
- });
3751
- throw error;
3752
- }
3753
- let cleanupPromise = null;
3754
- let cleanupTimer = null;
3755
- let unregisterProcessCleanup = null;
3756
3590
  const cleanup = () => {
3757
3591
  cleanupPromise ??= (async () => {
3758
- if (cleanupTimer) {
3759
- clearTimeout(cleanupTimer);
3760
- cleanupTimer = null;
3761
- }
3762
- try {
3763
- await rm(directory, {
3764
- force: true,
3765
- recursive: true
3766
- });
3767
- } catch (error) {
3768
- logger$16.warn("Failed to remove staged images temp directory", {
3769
- directory,
3770
- error
3771
- });
3772
- } finally {
3773
- unregisterProcessCleanup?.();
3774
- unregisterProcessCleanup = null;
3775
- }
3592
+ if (timer) clearTimeout(timer);
3593
+ await rm(directory, {
3594
+ recursive: true,
3595
+ force: true,
3596
+ maxRetries: 3,
3597
+ retryDelay: 20
3598
+ });
3599
+ unregister();
3776
3600
  })();
3777
3601
  return cleanupPromise;
3778
3602
  };
3779
- unregisterProcessCleanup = registerProcessCleanup(() => {
3780
- if (cleanupTimer) {
3781
- clearTimeout(cleanupTimer);
3782
- cleanupTimer = null;
3783
- }
3784
- try {
3785
- rmSync(directory, {
3786
- force: true,
3787
- recursive: true
3788
- });
3789
- } catch (error) {
3790
- logger$16.warn("Failed to remove staged images temp directory on exit", {
3791
- directory,
3792
- error
3793
- });
3794
- } finally {
3795
- unregisterProcessCleanup?.();
3796
- unregisterProcessCleanup = null;
3797
- }
3798
- });
3799
3603
  return {
3800
3604
  directory,
3801
- formData: stagedFormData,
3802
3605
  cleanup,
3803
- scheduleCleanup: () => {
3804
- if (cleanupPromise || cleanupTimer) return;
3805
- cleanupTimer = setTimeout(() => void cleanup(), STAGED_FILES_TTL_MS);
3806
- cleanupTimer.unref();
3606
+ scheduleCleanup() {
3607
+ if (timer || cleanupPromise) return;
3608
+ timer = setTimeout(() => void cleanup().catch(() => {}), ttlMs);
3609
+ timer.unref();
3610
+ },
3611
+ async create() {
3612
+ const path = join(directory, String(index++));
3613
+ return {
3614
+ path,
3615
+ handle: await open(path, "wx")
3616
+ };
3807
3617
  }
3808
3618
  };
3809
3619
  }
3810
- // src/services/xai/images.ts
3811
- const XAI_ASPECT_RATIOS = [
3812
- {
3813
- label: "1:1",
3814
- width: 1,
3815
- height: 1
3816
- },
3817
- {
3818
- label: "16:9",
3819
- width: 16,
3820
- height: 9
3821
- },
3822
- {
3823
- label: "9:16",
3824
- width: 9,
3825
- height: 16
3826
- },
3827
- {
3828
- label: "4:3",
3829
- width: 4,
3830
- height: 3
3831
- },
3832
- {
3833
- label: "3:4",
3834
- width: 3,
3835
- height: 4
3836
- },
3837
- {
3838
- label: "3:2",
3839
- width: 3,
3840
- height: 2
3841
- },
3842
- {
3843
- label: "2:3",
3844
- width: 2,
3845
- height: 3
3846
- },
3847
- {
3848
- label: "2:1",
3849
- width: 2,
3850
- height: 1
3851
- },
3852
- {
3853
- label: "1:2",
3854
- width: 1,
3855
- height: 2
3856
- },
3857
- {
3858
- label: "19.5:9",
3859
- width: 19.5,
3860
- height: 9
3861
- },
3862
- {
3863
- label: "9:19.5",
3864
- width: 9,
3865
- height: 19.5
3866
- },
3867
- {
3868
- label: "20:9",
3869
- width: 20,
3870
- height: 9
3871
- },
3872
- {
3873
- label: "9:20",
3874
- width: 9,
3875
- height: 20
3620
+ async function* readImageJsonFile(path, signal) {
3621
+ const stream = createReadStream(path, {
3622
+ highWaterMark: 65536,
3623
+ signal
3624
+ });
3625
+ try {
3626
+ for await (const bytes of stream) yield bytes;
3627
+ } finally {
3628
+ stream.destroy();
3876
3629
  }
3877
- ];
3878
- const XAI_ASPECT_RATIO_LABELS = /* @__PURE__ */ new Set([...XAI_ASPECT_RATIOS.map((ratio) => ratio.label), "auto"]);
3879
- const XAI_RESPONSE_FORMATS = /* @__PURE__ */ new Set(["url", "b64_json"]);
3880
- const XAI_RESOLUTIONS = /* @__PURE__ */ new Set(["1k", "2k"]);
3881
- const MAX_EDIT_IMAGES = 5;
3882
- const MAX_IMAGE_COUNT = 10;
3883
- const HIGH_RESOLUTION_EDGE = 1536;
3884
- var XaiImagesRequestError = class extends Error {
3885
- constructor(message) {
3886
- super(message);
3887
- this.name = "XaiImagesRequestError";
3630
+ }
3631
+ // src/lib/streaming-json.ts
3632
+ const JSON_CHUNK_BYTES = 65536;
3633
+ /** Never ask the tokenizer to accumulate untrusted strings, keys, or numbers. */
3634
+ var StreamingJsonReader = class {
3635
+ decode = new TextDecoder("utf-8", { fatal: true });
3636
+ parse = jsonParser({ packValues: false });
3637
+ depth = 0;
3638
+ scalarLength = 0;
3639
+ boundedScalar = false;
3640
+ read(bytes) {
3641
+ const text = this.decode.decode(bytes, { stream: bytes !== void 0 });
3642
+ const result = this.parse(text);
3643
+ const tokens = result === none ? [] : result.values;
3644
+ if (bytes === void 0) {
3645
+ const last = this.parse(none);
3646
+ if (last !== none) tokens.push(...last.values);
3647
+ }
3648
+ for (const token of tokens) switch (token.name) {
3649
+ case "startObject":
3650
+ case "startArray":
3651
+ if (++this.depth > 64) throw new SyntaxError("JSON nesting exceeds 64");
3652
+ break;
3653
+ case "endObject":
3654
+ case "endArray":
3655
+ this.depth--;
3656
+ break;
3657
+ case "startKey":
3658
+ case "startNumber":
3659
+ this.scalarLength = 0;
3660
+ this.boundedScalar = true;
3661
+ break;
3662
+ case "endKey":
3663
+ case "endNumber":
3664
+ this.boundedScalar = false;
3665
+ break;
3666
+ case "stringChunk":
3667
+ case "numberChunk": if (this.boundedScalar) {
3668
+ this.scalarLength += token.value.length;
3669
+ if (this.scalarLength > 65536) throw new SyntaxError("JSON key or number is too large");
3670
+ }
3671
+ }
3672
+ return tokens;
3888
3673
  }
3889
3674
  };
3890
- function isRecord$5(value) {
3891
- return value !== null && typeof value === "object" && !Array.isArray(value);
3892
- }
3893
- function readPrompt(value) {
3894
- if (typeof value !== "string" || value.trim() === "") throw new XaiImagesRequestError("prompt is required");
3895
- return value;
3896
- }
3897
- function assertNotStreaming(value) {
3898
- if (value === true || value === "true") throw new XaiImagesRequestError("xAI image requests do not support streaming");
3899
- }
3900
- function readCount(value) {
3901
- if (value === void 0 || value === null) return void 0;
3902
- const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value.trim()) : NaN;
3903
- if (!Number.isInteger(parsed) || parsed < 1 || parsed > MAX_IMAGE_COUNT) throw new XaiImagesRequestError(`n must be an integer from 1 to ${MAX_IMAGE_COUNT}`);
3904
- return parsed;
3905
- }
3906
- function readQuality(value) {
3907
- if (typeof value !== "string") return void 0;
3908
- switch (value.trim().toLowerCase()) {
3909
- case "low": return "low";
3910
- case "medium":
3911
- case "standard":
3912
- case "high":
3913
- case "hd": return "medium";
3914
- case "auto": return "auto";
3915
- default: return;
3675
+ /** Splitting also bounds tokenizer allocations when a runtime supplies a huge chunk. */
3676
+ async function* readJsonBytes(body, signal) {
3677
+ if (!body) return;
3678
+ const reader = body.getReader();
3679
+ const abort = () => void reader.cancel(signal?.reason).catch(() => {});
3680
+ signal?.addEventListener("abort", abort, { once: true });
3681
+ let complete = false;
3682
+ try {
3683
+ while (true) {
3684
+ signal?.throwIfAborted();
3685
+ const { done, value } = await reader.read();
3686
+ signal?.throwIfAborted();
3687
+ if (done) {
3688
+ complete = true;
3689
+ return;
3690
+ }
3691
+ for (let offset = 0; offset < value.byteLength; offset += JSON_CHUNK_BYTES) yield value.subarray(offset, offset + JSON_CHUNK_BYTES);
3692
+ }
3693
+ } finally {
3694
+ signal?.removeEventListener("abort", abort);
3695
+ if (!complete) await reader.cancel().catch(() => {});
3696
+ reader.releaseLock();
3916
3697
  }
3917
3698
  }
3918
- function readChoice(value, allowed, name, normalize = (item) => item) {
3919
- if (value === void 0 || value === null) return void 0;
3920
- if (typeof value !== "string") throw new XaiImagesRequestError(`${name} is invalid`);
3921
- const trimmed = value.trim();
3922
- if (!trimmed) return void 0;
3923
- const normalized = normalize(trimmed);
3924
- if (!allowed.has(normalized)) throw new XaiImagesRequestError(`${name} must be one of ${Array.from(allowed).join(", ")}`);
3925
- return normalized;
3926
- }
3927
- function readAspectRatio(value) {
3928
- if (value === void 0) return void 0;
3929
- if (typeof value !== "string") throw new XaiImagesRequestError("aspect_ratio is invalid");
3930
- const trimmed = value.trim();
3931
- if (!trimmed) return void 0;
3932
- const normalized = trimmed.toLowerCase() === "auto" ? "auto" : trimmed;
3933
- if (!XAI_ASPECT_RATIO_LABELS.has(normalized)) throw new XaiImagesRequestError("aspect_ratio must be a supported xAI ratio such as 1:1, 16:9, 9:16, or auto");
3934
- return normalized;
3935
- }
3936
- function readSize(value) {
3937
- if (typeof value !== "string") return void 0;
3938
- const trimmed = value.trim().toLowerCase();
3939
- if (!trimmed) return void 0;
3940
- if (trimmed === "auto") return "auto";
3941
- const match = /^(\d+)x(\d+)$/u.exec(trimmed);
3942
- if (!match?.[1] || !match[2]) return void 0;
3943
- const width = Number(match[1]);
3944
- const height = Number(match[2]);
3945
- if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height) || width <= 0 || height <= 0) return;
3699
+ function createJsonByteSource(body, signal) {
3700
+ const controller = new AbortController();
3701
+ const bytes = readJsonBytes(body, AbortSignal.any([signal, controller.signal]));
3946
3702
  return {
3947
- height,
3948
- width
3949
- };
3950
- }
3951
- function aspectRatioFromSize(size) {
3952
- if (!size) return void 0;
3953
- if (size === "auto") return "auto";
3954
- let best = XAI_ASPECT_RATIOS[0].label;
3955
- let bestDistance = Number.POSITIVE_INFINITY;
3956
- const ratio = Math.log(size.width / size.height);
3957
- for (const candidate of XAI_ASPECT_RATIOS) {
3958
- const distance = Math.abs(ratio - Math.log(candidate.width / candidate.height));
3959
- if (distance < bestDistance) {
3960
- best = candidate.label;
3961
- bestDistance = distance;
3703
+ bytes,
3704
+ async cancel(reason) {
3705
+ controller.abort(reason);
3706
+ await bytes.return(void 0).catch(() => {});
3707
+ if (body && !body.locked) await body.cancel(reason).catch(() => {});
3962
3708
  }
3963
- }
3964
- return best;
3965
- }
3966
- function resolutionFromSize(size) {
3967
- if (!size || size === "auto") return void 0;
3968
- return Math.max(size.width, size.height) >= HIGH_RESOLUTION_EDGE ? "2k" : "1k";
3709
+ };
3969
3710
  }
3970
- function applyCommonImageFields(target, source) {
3971
- assertNotStreaming(source.stream);
3972
- const count = readCount(source.n);
3973
- const quality = readQuality(source.quality);
3974
- const responseFormat = readChoice(source.response_format, XAI_RESPONSE_FORMATS, "response_format");
3975
- const aspectRatio = readAspectRatio(source.aspect_ratio) ?? aspectRatioFromSize(readSize(source.size));
3976
- const resolution = readChoice(source.resolution, XAI_RESOLUTIONS, "resolution", (value) => value.toLowerCase()) ?? resolutionFromSize(readSize(source.size));
3977
- if (count !== void 0) target.n = count;
3978
- if (quality && target.model === "grok-imagine-image-2.0") target.quality = quality;
3979
- target.response_format = responseFormat ?? "b64_json";
3980
- if (aspectRatio) target.aspect_ratio = aspectRatio;
3981
- if (resolution) target.resolution = resolution;
3982
- }
3983
- function normalizeImageUrl(value) {
3984
- const trimmed = value.trim();
3985
- if (/^https?:\/\//iu.test(trimmed) || /^data:image\/[a-z0-9.+-]+;base64,/iu.test(trimmed)) return trimmed;
3986
- throw new XaiImagesRequestError("image must be an http(s) URL or a base64 data URL");
3987
- }
3988
- function base64ToDataUrl(value, mime) {
3989
- const compact = value.trim().replaceAll(/\s+/gu, "");
3990
- if (compact.startsWith("data:image/")) return normalizeImageUrl(compact);
3991
- if (!/^[a-z0-9+/]+={0,2}$/iu.test(compact)) throw new XaiImagesRequestError("image b64_json is not valid base64");
3992
- return `data:${typeof mime === "string" && mime.trim().toLowerCase().startsWith("image/") ? mime.trim().toLowerCase() : "image/png"};base64,${compact}`;
3711
+ /** Lazy streams allow a route to choose the original or rebuilt body without teeing. */
3712
+ function jsonByteStream(iterator, cancel) {
3713
+ return new ReadableStream({
3714
+ async pull(controller) {
3715
+ try {
3716
+ const { done, value } = await iterator.next();
3717
+ if (done) controller.close();
3718
+ else controller.enqueue(value);
3719
+ } catch (error) {
3720
+ controller.error(error);
3721
+ await iterator.return(void 0).catch(() => {});
3722
+ }
3723
+ },
3724
+ async cancel(reason) {
3725
+ await cancel?.(reason);
3726
+ await iterator.return(void 0);
3727
+ }
3728
+ }, { highWaterMark: 0 });
3993
3729
  }
3994
- function readImageReference(value) {
3995
- if (typeof value === "string") return {
3996
- type: "image_url",
3997
- url: normalizeImageUrl(value)
3998
- };
3999
- if (!isRecord$5(value)) throw new XaiImagesRequestError("image must be a URL or an image object");
4000
- if (typeof value.url === "string") return {
4001
- type: "image_url",
4002
- url: normalizeImageUrl(value.url)
4003
- };
4004
- if (typeof value.image_url === "string") return {
4005
- type: "image_url",
4006
- url: normalizeImageUrl(value.image_url)
4007
- };
4008
- if (typeof value.b64_json === "string") return {
4009
- type: "image_url",
4010
- url: base64ToDataUrl(value.b64_json, value.mime_type ?? value.media_type)
3730
+ var DuplicateImageModelError = class extends SyntaxError {
3731
+ constructor() {
3732
+ super("Duplicate top-level image model");
3733
+ }
3734
+ };
3735
+ /** Keeps only the top-level model; all image string chunks are discarded. */
3736
+ var ImageModelReader = class {
3737
+ model;
3738
+ object = false;
3739
+ complete = false;
3740
+ invalidModel = false;
3741
+ depth = 0;
3742
+ key = "";
3743
+ inKey = false;
3744
+ readingModel = false;
3745
+ modelText = "";
3746
+ seenModel = false;
3747
+ read(tokens) {
3748
+ for (const token of tokens) {
3749
+ if (token.name === "startKey") {
3750
+ this.inKey = true;
3751
+ this.key = "";
3752
+ } else if (token.name === "endKey") {
3753
+ this.inKey = false;
3754
+ if (this.depth === 1 && this.key === "model") {
3755
+ if (this.seenModel) throw new DuplicateImageModelError();
3756
+ this.seenModel = true;
3757
+ }
3758
+ } else if (token.name === "stringChunk") {
3759
+ if (this.inKey) this.key += token.value;
3760
+ else if (this.readingModel) {
3761
+ this.modelText += token.value;
3762
+ if (this.modelText.length > 4096) {
3763
+ this.invalidModel = true;
3764
+ this.readingModel = false;
3765
+ this.modelText = "";
3766
+ }
3767
+ }
3768
+ } else if (token.name === "startString") this.readingModel = this.depth === 1 && this.key === "model";
3769
+ else if (token.name === "endString") {
3770
+ if (this.readingModel) {
3771
+ this.model = this.modelText;
3772
+ this.modelText = "";
3773
+ this.readingModel = false;
3774
+ }
3775
+ } else if (this.depth === 1 && this.key === "model" && [
3776
+ "startObject",
3777
+ "startArray",
3778
+ "startNumber",
3779
+ "nullValue",
3780
+ "trueValue",
3781
+ "falseValue"
3782
+ ].includes(token.name)) this.invalidModel = true;
3783
+ if (token.name === "startObject" || token.name === "startArray") {
3784
+ if (this.depth === 0) this.object = token.name === "startObject";
3785
+ this.depth++;
3786
+ } else if (token.name === "endObject" || token.name === "endArray") {
3787
+ if (--this.depth === 0) this.complete = true;
3788
+ }
3789
+ }
3790
+ }
3791
+ };
3792
+ /** Re-encodes bounded chunks, replacing or inserting the top-level model. */
3793
+ async function* rewriteImageModel(input, model, insertModel = false) {
3794
+ const parser = new StreamingJsonReader();
3795
+ const encode = new TextEncoder();
3796
+ const stringify = stringer();
3797
+ let depth = 0;
3798
+ let key = "";
3799
+ let inKey = false;
3800
+ let replacing = false;
3801
+ const process = (tokens) => {
3802
+ const parts = [];
3803
+ const emit = (token) => {
3804
+ const text = stringify(token);
3805
+ if (text !== none) parts.push(text);
3806
+ };
3807
+ for (const token of tokens) {
3808
+ if (insertModel && depth === 0 && token.name === "startObject") {
3809
+ emit(token);
3810
+ emit({ name: "startKey" });
3811
+ emit({
3812
+ name: "stringChunk",
3813
+ value: "model"
3814
+ });
3815
+ emit({ name: "endKey" });
3816
+ emit({ name: "startString" });
3817
+ emit({
3818
+ name: "stringChunk",
3819
+ value: model
3820
+ });
3821
+ emit({ name: "endString" });
3822
+ depth++;
3823
+ continue;
3824
+ }
3825
+ if (replacing) {
3826
+ if (token.name === "endString") replacing = false;
3827
+ continue;
3828
+ }
3829
+ if (token.name === "startKey") {
3830
+ inKey = true;
3831
+ key = "";
3832
+ } else if (token.name === "endKey") inKey = false;
3833
+ else if (token.name === "stringChunk" && inKey) key += token.value;
3834
+ if (token.name === "startString" && depth === 1 && key === "model") {
3835
+ emit(token);
3836
+ emit({
3837
+ name: "stringChunk",
3838
+ value: model
3839
+ });
3840
+ emit({ name: "endString" });
3841
+ replacing = true;
3842
+ continue;
3843
+ }
3844
+ emit(token);
3845
+ if (token.name === "startObject" || token.name === "startArray") depth++;
3846
+ else if (token.name === "endObject" || token.name === "endArray") depth--;
3847
+ }
3848
+ return encode.encode(parts.join(""));
4011
3849
  };
4012
- if (value.file_id !== void 0) throw new XaiImagesRequestError("file_id is not supported for xAI image edits; provide an image_url instead");
4013
- throw new XaiImagesRequestError("image must include a url, image_url, or b64_json");
3850
+ for await (const bytes of input) yield process(parser.read(bytes));
3851
+ const tail = process(parser.read());
3852
+ if (tail.byteLength) yield tail;
4014
3853
  }
4015
- function collectJsonImages(payload) {
4016
- const values = [];
4017
- if (isUnknownArray(payload.image)) values.push(...payload.image);
4018
- else if (payload.image !== void 0) values.push(payload.image);
4019
- if (isUnknownArray(payload.images)) values.push(...payload.images);
4020
- if (payload.image_url !== void 0) values.push(payload.image_url);
4021
- return values;
3854
+ // src/lib/image-json-request.ts
3855
+ function invalidImageJson(message = "Image request body must be a JSON object") {
3856
+ return new HTTPError(message, Response.json({ error: {
3857
+ message,
3858
+ type: "invalid_request_error"
3859
+ } }, { status: 400 }));
4022
3860
  }
4023
- function isUnknownArray(value) {
4024
- return Array.isArray(value);
3861
+ function rebuild(request, body, changed = false) {
3862
+ const headers = new Headers(request.headers);
3863
+ if (changed) headers.delete("content-length");
3864
+ return new Request(request.url, {
3865
+ method: request.method,
3866
+ headers,
3867
+ body,
3868
+ signal: request.signal,
3869
+ duplex: "half"
3870
+ });
4025
3871
  }
4026
- function buildEditPayload(model, prompt, images, source) {
4027
- if (source.mask !== void 0) throw new XaiImagesRequestError("xAI image edits do not support mask");
4028
- if (images.length === 0) throw new XaiImagesRequestError("image is required");
4029
- if (images.length > MAX_EDIT_IMAGES) throw new XaiImagesRequestError(`xAI image edits accept at most ${MAX_EDIT_IMAGES} images`);
4030
- const payload = {
4031
- model,
4032
- prompt
3872
+ /** Read at most 64 KiB before forwarding a model-first request. Late models use disk. */
3873
+ async function prepareImageJsonRequest(request, maxStagedBytes = IMAGE_JSON_MAX_BYTES) {
3874
+ const forwardingAbort = new AbortController();
3875
+ const signal = AbortSignal.any([request.signal, forwardingAbort.signal]);
3876
+ const input = createJsonByteSource(request.body, signal);
3877
+ const bytes = input.bytes;
3878
+ const parser = new StreamingJsonReader();
3879
+ const metadata = new ImageModelReader();
3880
+ const scan = (chunk) => {
3881
+ try {
3882
+ metadata.read(parser.read(chunk));
3883
+ } catch (error) {
3884
+ throw invalidImageJson(error instanceof DuplicateImageModelError ? error.message : void 0);
3885
+ }
3886
+ if (metadata.invalidModel) throw invalidImageJson("Image model must be a string of at most 4096 characters");
3887
+ };
3888
+ const prefix = [];
3889
+ let length = 0;
3890
+ let eof = false;
3891
+ let pending;
3892
+ async function* remaining() {
3893
+ if (pending) {
3894
+ const chunk = pending;
3895
+ pending = void 0;
3896
+ yield chunk;
3897
+ }
3898
+ yield* bytes;
3899
+ }
3900
+ let files;
3901
+ let stagedPath;
3902
+ try {
3903
+ while (length < JSON_CHUNK_BYTES) {
3904
+ const next = await bytes.next();
3905
+ if (next.done) {
3906
+ eof = true;
3907
+ scan();
3908
+ break;
3909
+ }
3910
+ const chunk = next.value.subarray(0, JSON_CHUNK_BYTES - length);
3911
+ pending = chunk.byteLength < next.value.byteLength ? next.value.subarray(chunk.byteLength) : void 0;
3912
+ prefix.push(chunk);
3913
+ length += chunk.byteLength;
3914
+ scan(chunk);
3915
+ if (metadata.model !== void 0 || metadata.complete) break;
3916
+ }
3917
+ if (!eof && !metadata.complete && metadata.model === void 0) {
3918
+ files = await createImageJsonFiles();
3919
+ const file = await files.create();
3920
+ stagedPath = file.path;
3921
+ try {
3922
+ for (const chunk of prefix) {
3923
+ if (length > maxStagedBytes) throw imageJsonLimitError();
3924
+ await file.handle.writeFile(chunk);
3925
+ }
3926
+ prefix.length = 0;
3927
+ for await (const chunk of remaining()) {
3928
+ length += chunk.byteLength;
3929
+ if (length > maxStagedBytes) throw imageJsonLimitError();
3930
+ scan(chunk);
3931
+ await file.handle.writeFile(chunk);
3932
+ }
3933
+ eof = true;
3934
+ scan();
3935
+ } finally {
3936
+ await file.handle.close();
3937
+ }
3938
+ files.scheduleCleanup();
3939
+ }
3940
+ if (!metadata.object) throw invalidImageJson();
3941
+ } catch (error) {
3942
+ await input.cancel(error);
3943
+ await files?.cleanup();
3944
+ throw error;
3945
+ }
3946
+ let claimed = false;
3947
+ const abort = () => {
3948
+ input.cancel().catch(() => {});
3949
+ files?.cleanup().catch(() => {});
3950
+ };
3951
+ request.signal.addEventListener("abort", abort, { once: true });
3952
+ async function* source() {
3953
+ if (claimed) throw new Error("Image request body already forwarded");
3954
+ claimed = true;
3955
+ try {
3956
+ signal.throwIfAborted();
3957
+ if (stagedPath) yield* readImageJsonFile(stagedPath, signal);
3958
+ else {
3959
+ yield* prefix;
3960
+ prefix.length = 0;
3961
+ if (!eof) {
3962
+ for await (const chunk of remaining()) {
3963
+ scan(chunk);
3964
+ yield chunk;
3965
+ }
3966
+ scan();
3967
+ }
3968
+ }
3969
+ } finally {
3970
+ request.signal.removeEventListener("abort", abort);
3971
+ await bytes.return(void 0).catch(() => {});
3972
+ await files?.cleanup();
3973
+ }
3974
+ }
3975
+ const cancel = async (reason) => {
3976
+ forwardingAbort.abort(reason);
3977
+ request.signal.removeEventListener("abort", abort);
3978
+ await input.cancel(reason);
3979
+ await files?.cleanup();
4033
3980
  };
4034
- applyCommonImageFields(payload, source);
4035
- if (images.length === 1) payload.image = images[0];
4036
- else payload.images = images;
4037
- return payload;
4038
- }
4039
- /**
4040
- * Translates an OpenAI image generation body into an xAI Imagine generation
4041
- * request. The configured model replaces the client model.
4042
- */
4043
- function translateXaiImageGeneration(payload, model) {
4044
- if (!isRecord$5(payload)) throw new XaiImagesRequestError("Image generation body must be a JSON object");
4045
- const translated = {
4046
- model,
4047
- prompt: readPrompt(payload.prompt)
3981
+ return {
3982
+ model: metadata.model,
3983
+ originalRequest: rebuild(request, jsonByteStream(source(), cancel)),
3984
+ createRequest: (model) => rebuild(request, jsonByteStream(rewriteImageModel(source(), model, metadata.model === void 0), cancel), true),
3985
+ cleanup: cancel,
3986
+ stagedDirectory: files?.directory
4048
3987
  };
4049
- applyCommonImageFields(translated, payload);
4050
- return translated;
4051
3988
  }
3989
+ // src/routes/images/temp-form-data.ts
3990
+ const logger$16 = createHandlerLogger("images-temp-form-data");
3991
+ const multipartStagingDependencies = { createWriteStream };
4052
3992
  /**
4053
- * Translates an OpenAI JSON image edit body into an xAI Imagine edit request.
3993
+ * Matches the images forwarding timeouts: an upstream upload may take that
3994
+ * long, so staged files must survive at least the whole forwarding window.
4054
3995
  */
4055
- function translateXaiImageEditJson(payload, model) {
4056
- if (!isRecord$5(payload)) throw new XaiImagesRequestError("Image edit body must be a JSON object");
4057
- return buildEditPayload(model, readPrompt(payload.prompt), collectJsonImages(payload).map((image) => readImageReference(image)), payload);
4058
- }
4059
- const MAX_RESPONSE_IMAGE_BYTES = 33554432;
4060
- function responseHasB64Json(value) {
4061
- return typeof value === "string" && value.trim() !== "";
4062
- }
4063
- function imageCreatedTimestamp(value) {
4064
- if (typeof value === "number" && Number.isFinite(value) && value > 0) return Math.trunc(value);
4065
- return Math.floor(Date.now() / 1e3);
4066
- }
4067
- function trustedImageUrl(value) {
4068
- if (typeof value !== "string" || !value.trim()) return null;
4069
- try {
4070
- const url = new URL(value.trim());
4071
- if (url.protocol !== "https:" || url.hostname !== "imgen.x.ai" || url.port || url.username || url.password) return null;
4072
- return url.toString();
4073
- } catch {
4074
- return null;
4075
- }
4076
- }
4077
- async function readImageBytes(response) {
4078
- const declaredLength = Number(response.headers.get("content-length"));
4079
- if (Number.isFinite(declaredLength) && declaredLength > MAX_RESPONSE_IMAGE_BYTES) {
4080
- await response.body?.cancel().catch(() => {});
4081
- return null;
3996
+ const STAGED_FILES_TTL_MS = 9e5;
3997
+ const DEFAULT_MULTIPART_STAGING_LIMITS = {
3998
+ maxBodySizeBytes: 134217728,
3999
+ maxFieldSizeBytes: 10485760,
4000
+ maxFields: 64,
4001
+ maxFileSizeBytes: 67108864,
4002
+ maxFiles: 16,
4003
+ maxParts: 80
4004
+ };
4005
+ var InvalidMultipartBodyError = class extends Error {
4006
+ constructor(options) {
4007
+ super("Invalid multipart form data body", options);
4008
+ this.name = "InvalidMultipartBodyError";
4082
4009
  }
4083
- if (!response.body) return null;
4084
- const reader = response.body.getReader();
4085
- const chunks = [];
4086
- let length = 0;
4087
- let complete = false;
4088
- try {
4089
- while (true) {
4090
- const { done, value } = await reader.read();
4091
- if (done) {
4092
- complete = true;
4093
- return length > 0 ? Buffer.concat(chunks, length) : null;
4094
- }
4095
- length += value.byteLength;
4096
- if (length > MAX_RESPONSE_IMAGE_BYTES) return null;
4097
- chunks.push(value);
4098
- }
4099
- } finally {
4100
- if (!complete) await reader.cancel().catch(() => {});
4101
- reader.releaseLock();
4010
+ };
4011
+ var MultipartBodyTooLargeError = class extends Error {
4012
+ constructor() {
4013
+ super("Multipart form data body exceeds the configured upload limits");
4014
+ this.name = "MultipartBodyTooLargeError";
4102
4015
  }
4016
+ };
4017
+ function sanitizeFileName(name) {
4018
+ return name.split(/[\\/]/).pop()?.replaceAll("\0", "") || "file";
4103
4019
  }
4104
- function imageDownloadErrorResponse() {
4105
- return Response.json({ error: {
4106
- message: "Failed to retrieve xAI image output as b64_json",
4107
- type: "upstream_error"
4108
- } }, { status: 502 });
4020
+ async function createDiskBackedBlob(filePath, type) {
4021
+ if (typeof Bun !== "undefined") return Bun.file(filePath, { type });
4022
+ const { openAsBlob } = await import("node:fs");
4023
+ return await openAsBlob(filePath, { type });
4109
4024
  }
4110
- function imageOutputUrlForLog(value) {
4111
- if (typeof value !== "string" || !value.trim()) return null;
4112
- try {
4113
- const url = new URL(value.trim());
4114
- if (url.protocol !== "https:" && url.protocol !== "http:") return null;
4115
- url.username = "";
4116
- url.password = "";
4117
- for (const key of url.searchParams.keys()) if (/token|secret|signature|credential|password|authorization|api[_-]?key|^key$|^sig$/iu.test(key)) url.searchParams.set(key, "[REDACTED]");
4118
- return url.toString();
4119
- } catch {
4120
- return null;
4121
- }
4025
+ function toError(error) {
4026
+ return error instanceof Error ? error : new Error(String(error));
4122
4027
  }
4123
- function rebuildJsonResponse(response, body) {
4124
- const headers = new Headers(response.headers);
4125
- headers.delete("content-length");
4126
- if (!headers.get("content-type")) headers.set("content-type", "application/json");
4127
- return new Response(body, {
4128
- headers,
4129
- status: response.status,
4130
- statusText: response.statusText
4028
+ function parseMultipartBody(body, contentType, directory, entries, limits, signal) {
4029
+ return new Promise((resolve, reject) => {
4030
+ let busboy;
4031
+ try {
4032
+ busboy = new Busboy({
4033
+ headers: { "content-type": contentType },
4034
+ limits: {
4035
+ fieldSize: limits.maxFieldSizeBytes,
4036
+ fields: limits.maxFields,
4037
+ fileSize: limits.maxFileSizeBytes,
4038
+ files: limits.maxFiles,
4039
+ parts: limits.maxParts
4040
+ }
4041
+ });
4042
+ } catch (error) {
4043
+ reject(new InvalidMultipartBodyError({ cause: toError(error) }));
4044
+ return;
4045
+ }
4046
+ const pendingWrites = [];
4047
+ const activeFileStreams = /* @__PURE__ */ new Set();
4048
+ const source = body === null ? Readable.from([]) : Readable.fromWeb(body);
4049
+ let bodySizeBytes = 0;
4050
+ const bodyLimiter = new Transform({ transform(chunk, _encoding, callback) {
4051
+ bodySizeBytes += chunk.byteLength;
4052
+ if (bodySizeBytes > limits.maxBodySizeBytes) {
4053
+ callback(new MultipartBodyTooLargeError());
4054
+ return;
4055
+ }
4056
+ callback(null, chunk);
4057
+ } });
4058
+ let fileIndex = 0;
4059
+ let failure = null;
4060
+ let inputPipeline = null;
4061
+ let settled = false;
4062
+ const settleStreams = () => Promise.allSettled([...inputPipeline ? [inputPipeline] : [], ...pendingWrites]);
4063
+ const fail = (error) => {
4064
+ if (failure || settled) return;
4065
+ const failureError = toError(error);
4066
+ failure = failureError;
4067
+ source.destroy(failureError);
4068
+ bodyLimiter.destroy(failureError);
4069
+ busboy.destroy(failureError);
4070
+ for (const stream of activeFileStreams) stream.destroy(failureError);
4071
+ settleStreams().then(() => {
4072
+ if (settled) return;
4073
+ settled = true;
4074
+ reject(failureError);
4075
+ });
4076
+ };
4077
+ const failLimit = () => fail(new MultipartBodyTooLargeError());
4078
+ busboy.on("field", (name, value, nameTruncated, valueTruncated) => {
4079
+ if (nameTruncated || valueTruncated) {
4080
+ failLimit();
4081
+ return;
4082
+ }
4083
+ entries.push({
4084
+ kind: "field",
4085
+ name,
4086
+ value
4087
+ });
4088
+ });
4089
+ busboy.on("file", (name, stream, fileName, _transferEncoding, mimeType) => {
4090
+ if (failure) {
4091
+ stream.resume();
4092
+ return;
4093
+ }
4094
+ const filePath = join(directory, `${fileIndex}.upload`);
4095
+ fileIndex += 1;
4096
+ entries.push({
4097
+ fileName: sanitizeFileName(fileName),
4098
+ filePath,
4099
+ kind: "file",
4100
+ mimeType,
4101
+ name
4102
+ });
4103
+ activeFileStreams.add(stream);
4104
+ stream.once("error", fail);
4105
+ stream.once("close", () => activeFileStreams.delete(stream));
4106
+ stream.once("limit", failLimit);
4107
+ try {
4108
+ const write = pipeline(stream, multipartStagingDependencies.createWriteStream(filePath));
4109
+ pendingWrites.push(write);
4110
+ write.catch(fail);
4111
+ } catch (error) {
4112
+ fail(error);
4113
+ }
4114
+ });
4115
+ busboy.on("fieldsLimit", failLimit);
4116
+ busboy.on("filesLimit", failLimit);
4117
+ busboy.on("partsLimit", failLimit);
4118
+ inputPipeline = pipeline(source, bodyLimiter, busboy, { signal });
4119
+ inputPipeline.then(() => {
4120
+ Promise.allSettled(pendingWrites).then((results) => {
4121
+ if (failure || settled) return;
4122
+ const failed = results.find((result) => result.status === "rejected");
4123
+ if (failed) fail(failed.reason);
4124
+ else {
4125
+ settled = true;
4126
+ resolve();
4127
+ }
4128
+ });
4129
+ }, (error) => {
4130
+ fail(error instanceof MultipartBodyTooLargeError || signal?.aborted ? error : new InvalidMultipartBodyError({ cause: toError(error) }));
4131
+ });
4131
4132
  });
4132
4133
  }
4134
+ function resolveStagingLimits(overrides) {
4135
+ const limits = {
4136
+ ...DEFAULT_MULTIPART_STAGING_LIMITS,
4137
+ ...overrides
4138
+ };
4139
+ for (const [name, value] of Object.entries(limits)) if (!Number.isSafeInteger(value) || value <= 0) throw new RangeError(`${name} must be a positive safe integer`);
4140
+ return limits;
4141
+ }
4133
4142
  /**
4134
- * Adapts an xAI image body to the OpenAI Images response.
4135
- * Generation returns created: null, and edits omit created. OpenAI clients
4136
- * require a unix timestamp. When b64_json is requested, a URL-only result
4137
- * is downloaded from the xAI CDN with redirects disabled and a byte limit.
4143
+ * Streams a multipart request body through the parser, writing every file
4144
+ * part to a temporary directory as it arrives and rebuilding the form data
4145
+ * with disk-backed blobs, so forwarding streams payloads from disk instead
4146
+ * of pinning the whole upload in memory.
4138
4147
  */
4139
- async function ensureImageResponseB64Json(response, fetchImage = globalThis.fetch, { clientSignal, onImageUrl, responseFormat = "b64_json" } = {}) {
4140
- if (!response.ok) return response;
4141
- clientSignal?.throwIfAborted();
4142
- const body = await response.text();
4143
- let payload;
4148
+ async function stageMultipartBodyToDisk(body, contentType, limitOverrides = {}, signal) {
4149
+ signal?.throwIfAborted();
4150
+ const limits = resolveStagingLimits(limitOverrides);
4151
+ const directory = await mkdtemp(join(tmpdir(), "copilot-api-images-"));
4152
+ const entries = [];
4144
4153
  try {
4145
- payload = JSON.parse(body);
4146
- } catch {
4147
- return rebuildJsonResponse(response, body);
4154
+ await parseMultipartBody(body, contentType, directory, entries, limits, signal);
4155
+ } catch (error) {
4156
+ await rm(directory, {
4157
+ force: true,
4158
+ recursive: true
4159
+ });
4160
+ throw error;
4148
4161
  }
4149
- if (!isRecord$5(payload) || !Array.isArray(payload.data)) return rebuildJsonResponse(response, body);
4150
- let changed = false;
4151
- const data = [];
4152
- for (const item of payload.data) {
4153
- if (isRecord$5(item) && onImageUrl) {
4154
- const outputUrl = imageOutputUrlForLog(item.url);
4155
- if (outputUrl) onImageUrl(outputUrl);
4156
- }
4157
- if (!isRecord$5(item) || responseFormat === "url" || responseHasB64Json(item.b64_json)) {
4158
- data.push(item);
4159
- continue;
4162
+ const stagedFormData = new FormData();
4163
+ try {
4164
+ for (const entry of entries) if (entry.kind === "field") stagedFormData.append(entry.name, entry.value);
4165
+ else stagedFormData.append(entry.name, await createDiskBackedBlob(entry.filePath, entry.mimeType), entry.fileName);
4166
+ } catch (error) {
4167
+ await rm(directory, {
4168
+ force: true,
4169
+ recursive: true
4170
+ });
4171
+ throw error;
4172
+ }
4173
+ let cleanupPromise = null;
4174
+ let cleanupTimer = null;
4175
+ let unregisterProcessCleanup = null;
4176
+ const cleanup = () => {
4177
+ cleanupPromise ??= (async () => {
4178
+ if (cleanupTimer) {
4179
+ clearTimeout(cleanupTimer);
4180
+ cleanupTimer = null;
4181
+ }
4182
+ try {
4183
+ await rm(directory, {
4184
+ force: true,
4185
+ recursive: true
4186
+ });
4187
+ } catch (error) {
4188
+ logger$16.warn("Failed to remove staged images temp directory", {
4189
+ directory,
4190
+ error
4191
+ });
4192
+ } finally {
4193
+ unregisterProcessCleanup?.();
4194
+ unregisterProcessCleanup = null;
4195
+ }
4196
+ })();
4197
+ return cleanupPromise;
4198
+ };
4199
+ unregisterProcessCleanup = registerProcessCleanup(() => {
4200
+ if (cleanupTimer) {
4201
+ clearTimeout(cleanupTimer);
4202
+ cleanupTimer = null;
4160
4203
  }
4161
- const url = trustedImageUrl(item.url);
4162
- if (!url) return imageDownloadErrorResponse();
4163
4204
  try {
4164
- clientSignal?.throwIfAborted();
4165
- const timeoutSignal = AbortSignal.timeout(6e4);
4166
- const imageResponse = await fetchImage(url, {
4167
- method: "GET",
4168
- redirect: "error",
4169
- signal: clientSignal ? AbortSignal.any([clientSignal, timeoutSignal]) : timeoutSignal
4205
+ rmSync(directory, {
4206
+ force: true,
4207
+ recursive: true
4170
4208
  });
4171
- if (!imageResponse.ok) {
4172
- await imageResponse.body?.cancel().catch(() => {});
4173
- return imageDownloadErrorResponse();
4174
- }
4175
- const bytes = await readImageBytes(imageResponse);
4176
- if (!bytes) return imageDownloadErrorResponse();
4177
- data.push({
4178
- ...item,
4179
- b64_json: bytes.toString("base64")
4209
+ } catch (error) {
4210
+ logger$16.warn("Failed to remove staged images temp directory on exit", {
4211
+ directory,
4212
+ error
4180
4213
  });
4181
- changed = true;
4182
- } catch {
4183
- clientSignal?.throwIfAborted();
4184
- return imageDownloadErrorResponse();
4214
+ } finally {
4215
+ unregisterProcessCleanup?.();
4216
+ unregisterProcessCleanup = null;
4217
+ }
4218
+ });
4219
+ return {
4220
+ directory,
4221
+ formData: stagedFormData,
4222
+ cleanup,
4223
+ scheduleCleanup: () => {
4224
+ if (cleanupPromise || cleanupTimer) return;
4225
+ cleanupTimer = setTimeout(() => void cleanup(), STAGED_FILES_TTL_MS);
4226
+ cleanupTimer.unref();
4185
4227
  }
4228
+ };
4229
+ }
4230
+ // src/services/xai/images.ts
4231
+ const XAI_ASPECT_RATIOS = [
4232
+ {
4233
+ label: "1:1",
4234
+ width: 1,
4235
+ height: 1
4236
+ },
4237
+ {
4238
+ label: "16:9",
4239
+ width: 16,
4240
+ height: 9
4241
+ },
4242
+ {
4243
+ label: "9:16",
4244
+ width: 9,
4245
+ height: 16
4246
+ },
4247
+ {
4248
+ label: "4:3",
4249
+ width: 4,
4250
+ height: 3
4251
+ },
4252
+ {
4253
+ label: "3:4",
4254
+ width: 3,
4255
+ height: 4
4256
+ },
4257
+ {
4258
+ label: "3:2",
4259
+ width: 3,
4260
+ height: 2
4261
+ },
4262
+ {
4263
+ label: "2:3",
4264
+ width: 2,
4265
+ height: 3
4266
+ },
4267
+ {
4268
+ label: "2:1",
4269
+ width: 2,
4270
+ height: 1
4271
+ },
4272
+ {
4273
+ label: "1:2",
4274
+ width: 1,
4275
+ height: 2
4276
+ },
4277
+ {
4278
+ label: "19.5:9",
4279
+ width: 19.5,
4280
+ height: 9
4281
+ },
4282
+ {
4283
+ label: "9:19.5",
4284
+ width: 9,
4285
+ height: 19.5
4286
+ },
4287
+ {
4288
+ label: "20:9",
4289
+ width: 20,
4290
+ height: 9
4291
+ },
4292
+ {
4293
+ label: "9:20",
4294
+ width: 9,
4295
+ height: 20
4186
4296
  }
4187
- const created = imageCreatedTimestamp(payload.created);
4188
- if (!changed && payload.created === created) return rebuildJsonResponse(response, body);
4189
- return rebuildJsonResponse(response, JSON.stringify({
4190
- ...payload,
4191
- created,
4192
- data
4193
- }));
4297
+ ];
4298
+ const XAI_ASPECT_RATIO_LABELS = /* @__PURE__ */ new Set([...XAI_ASPECT_RATIOS.map((ratio) => ratio.label), "auto"]);
4299
+ const XAI_RESPONSE_FORMATS = /* @__PURE__ */ new Set(["url", "b64_json"]);
4300
+ const XAI_RESOLUTIONS = /* @__PURE__ */ new Set(["1k", "2k"]);
4301
+ const MAX_EDIT_IMAGES = 5;
4302
+ const MAX_IMAGE_COUNT = 10;
4303
+ const HIGH_RESOLUTION_EDGE = 1536;
4304
+ var XaiImagesRequestError = class extends Error {
4305
+ constructor(message) {
4306
+ super(message);
4307
+ this.name = "XaiImagesRequestError";
4308
+ }
4309
+ };
4310
+ function isRecord$5(value) {
4311
+ return value !== null && typeof value === "object" && !Array.isArray(value);
4194
4312
  }
4195
- // src/routes/images/multipart-edits.ts
4196
- const multipartEditDependencies = { stageMultipartBodyToDisk };
4197
- var ImageEditRequestError = class extends Error {};
4198
- function imageEditRequestErrorResponse(c, error) {
4199
- if (c.req.raw.signal.aborted) return null;
4200
- if (!(error instanceof ImageEditRequestError) && !(error instanceof InvalidMultipartBodyError) && !(error instanceof MultipartBodyTooLargeError) && !(error instanceof XaiImagesRequestError)) return null;
4201
- return c.json({ error: {
4202
- message: error.message,
4203
- type: "invalid_request_error"
4204
- } }, error instanceof MultipartBodyTooLargeError ? 413 : 400);
4313
+ function readPrompt(value) {
4314
+ if (typeof value !== "string" || value.trim() === "") throw new XaiImagesRequestError("prompt is required");
4315
+ return value;
4205
4316
  }
4206
- function imageMimeType(bytes, declared) {
4207
- if (declared.startsWith("image/")) return declared;
4208
- if (bytes.subarray(0, 8).equals(Buffer.from([
4209
- 137,
4210
- 80,
4211
- 78,
4212
- 71,
4213
- 13,
4214
- 10,
4215
- 26,
4216
- 10
4217
- ]))) return "image/png";
4218
- if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
4219
- if (bytes.toString("ascii", 0, 4) === "RIFF" && bytes.toString("ascii", 8, 12) === "WEBP") return "image/webp";
4220
- if (/^GIF8[79]a/u.test(bytes.toString("ascii", 0, 6))) return "image/gif";
4221
- throw new ImageEditRequestError("Uploaded image must have an image MIME type");
4317
+ function assertNotStreaming(value) {
4318
+ if (value === true || value === "true") throw new XaiImagesRequestError("xAI image requests do not support streaming");
4319
+ }
4320
+ function readCount(value) {
4321
+ if (value === void 0 || value === null) return void 0;
4322
+ const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value.trim()) : NaN;
4323
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > MAX_IMAGE_COUNT) throw new XaiImagesRequestError(`n must be an integer from 1 to ${MAX_IMAGE_COUNT}`);
4324
+ return parsed;
4222
4325
  }
4223
- async function imageReference(value, signal) {
4224
- signal.throwIfAborted();
4225
- if (typeof value === "string") {
4226
- const url = value.trim();
4227
- 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");
4228
- return { image_url: url };
4326
+ function readQuality(value) {
4327
+ if (typeof value !== "string") return void 0;
4328
+ switch (value.trim().toLowerCase()) {
4329
+ case "low": return "low";
4330
+ case "medium":
4331
+ case "standard":
4332
+ case "high":
4333
+ case "hd": return "medium";
4334
+ case "auto": return "auto";
4335
+ default: return;
4229
4336
  }
4230
- const bytes = Buffer.from(await value.arrayBuffer());
4231
- signal.throwIfAborted();
4232
- if (bytes.length === 0) throw new ImageEditRequestError("image is empty");
4233
- return { image_url: `data:${imageMimeType(bytes, value.type.trim().toLowerCase())};base64,${bytes.toString("base64")}` };
4234
4337
  }
4235
- function fieldValue(name, value) {
4236
- if ([
4237
- "n",
4238
- "partial_images",
4239
- "output_compression"
4240
- ].includes(name)) {
4241
- const trimmed = value.trim();
4242
- const number = Number(trimmed);
4243
- if (!/^-?\d+$/u.test(trimmed) || !Number.isSafeInteger(number)) throw new ImageEditRequestError(`${name} must be an integer`);
4244
- return number;
4245
- }
4246
- if (name === "stream") {
4247
- if (value.trim() === "true") return true;
4248
- if (value.trim() === "false") return false;
4249
- throw new ImageEditRequestError("stream must be true or false");
4250
- }
4251
- return value;
4338
+ function readChoice(value, allowed, name, normalize = (item) => item) {
4339
+ if (value === void 0 || value === null) return void 0;
4340
+ if (typeof value !== "string") throw new XaiImagesRequestError(`${name} is invalid`);
4341
+ const trimmed = value.trim();
4342
+ if (!trimmed) return void 0;
4343
+ const normalized = normalize(trimmed);
4344
+ if (!allowed.has(normalized)) throw new XaiImagesRequestError(`${name} must be one of ${Array.from(allowed).join(", ")}`);
4345
+ return normalized;
4252
4346
  }
4253
- async function formPayload(form, signal) {
4254
- const fields = /* @__PURE__ */ new Map();
4255
- const images = [];
4256
- for (const [name, value] of form.entries()) {
4257
- if (name === "image" || name === "image[]") {
4258
- images.push(await imageReference(value, signal));
4259
- continue;
4347
+ function readAspectRatio(value) {
4348
+ if (value === void 0) return void 0;
4349
+ if (typeof value !== "string") throw new XaiImagesRequestError("aspect_ratio is invalid");
4350
+ const trimmed = value.trim();
4351
+ if (!trimmed) return void 0;
4352
+ const normalized = trimmed.toLowerCase() === "auto" ? "auto" : trimmed;
4353
+ if (!XAI_ASPECT_RATIO_LABELS.has(normalized)) throw new XaiImagesRequestError("aspect_ratio must be a supported xAI ratio such as 1:1, 16:9, 9:16, or auto");
4354
+ return normalized;
4355
+ }
4356
+ function readSize(value) {
4357
+ if (typeof value !== "string") return void 0;
4358
+ const trimmed = value.trim().toLowerCase();
4359
+ if (!trimmed) return void 0;
4360
+ if (trimmed === "auto") return "auto";
4361
+ const match = /^(\d+)x(\d+)$/u.exec(trimmed);
4362
+ if (!match?.[1] || !match[2]) return void 0;
4363
+ const width = Number(match[1]);
4364
+ const height = Number(match[2]);
4365
+ if (!Number.isSafeInteger(width) || !Number.isSafeInteger(height) || width <= 0 || height <= 0) return;
4366
+ return {
4367
+ height,
4368
+ width
4369
+ };
4370
+ }
4371
+ function aspectRatioFromSize(size) {
4372
+ if (!size) return void 0;
4373
+ if (size === "auto") return "auto";
4374
+ let best = XAI_ASPECT_RATIOS[0].label;
4375
+ let bestDistance = Number.POSITIVE_INFINITY;
4376
+ const ratio = Math.log(size.width / size.height);
4377
+ for (const candidate of XAI_ASPECT_RATIOS) {
4378
+ const distance = Math.abs(ratio - Math.log(candidate.width / candidate.height));
4379
+ if (distance < bestDistance) {
4380
+ best = candidate.label;
4381
+ bestDistance = distance;
4260
4382
  }
4261
- const key = name === "mask[]" ? "mask" : name;
4262
- if (fields.has(key)) throw new ImageEditRequestError(`Duplicate image edit field: ${key}`);
4263
- if (key === "mask") fields.set(key, await imageReference(value, signal));
4264
- else if (typeof value === "string") fields.set(key, fieldValue(key, value));
4265
- else throw new ImageEditRequestError(`Unexpected image edit file field: ${key}`);
4266
4383
  }
4267
- if (images.length === 0) throw new ImageEditRequestError("image is required");
4268
- fields.set("images", images);
4269
- return Object.fromEntries(fields);
4384
+ return best;
4270
4385
  }
4271
- /** Parse multipart once, keeping uploads on disk until the target is known. */
4272
- async function parseMultipartImageEdit(request) {
4273
- const requestHeaders = new Headers(request.headers);
4274
- const contentType = requestHeaders.get("content-type");
4275
- if (contentType?.split(";", 1)[0]?.trim().toLowerCase() !== "multipart/form-data") return null;
4276
- const staged = await multipartEditDependencies.stageMultipartBodyToDisk(request.body, contentType, {}, request.signal);
4277
- const form = staged.formData;
4278
- const model = form.get("model");
4279
- let payloadPromise;
4280
- const readPayload = () => {
4281
- payloadPromise ??= formPayload(form, request.signal).finally(staged.cleanup);
4282
- return payloadPromise;
4386
+ function resolutionFromSize(size) {
4387
+ if (!size || size === "auto") return void 0;
4388
+ return Math.max(size.width, size.height) >= HIGH_RESOLUTION_EDGE ? "2k" : "1k";
4389
+ }
4390
+ function applyCommonImageFields(target, source) {
4391
+ assertNotStreaming(source.stream);
4392
+ const count = readCount(source.n);
4393
+ const quality = readQuality(source.quality);
4394
+ const responseFormat = readChoice(source.response_format, XAI_RESPONSE_FORMATS, "response_format");
4395
+ const aspectRatio = readAspectRatio(source.aspect_ratio) ?? aspectRatioFromSize(readSize(source.size));
4396
+ const resolution = readChoice(source.resolution, XAI_RESOLUTIONS, "resolution", (value) => value.toLowerCase()) ?? resolutionFromSize(readSize(source.size));
4397
+ if (count !== void 0) target.n = count;
4398
+ if (quality && target.model === "grok-imagine-image-2.0") target.quality = quality;
4399
+ target.response_format = responseFormat ?? "b64_json";
4400
+ if (aspectRatio) target.aspect_ratio = aspectRatio;
4401
+ if (resolution) target.resolution = resolution;
4402
+ }
4403
+ function normalizeImageUrl(value) {
4404
+ const trimmed = value.trim();
4405
+ if (/^https?:\/\//iu.test(trimmed) || /^data:image\/[a-z0-9.+-]+;base64,/iu.test(trimmed)) return trimmed;
4406
+ throw new XaiImagesRequestError("image must be an http(s) URL or a base64 data URL");
4407
+ }
4408
+ function base64ToDataUrl(value, mime) {
4409
+ const compact = value.trim().replaceAll(/\s+/gu, "");
4410
+ if (compact.startsWith("data:image/")) return normalizeImageUrl(compact);
4411
+ if (!/^[a-z0-9+/]+={0,2}$/iu.test(compact)) throw new XaiImagesRequestError("image b64_json is not valid base64");
4412
+ return `data:${typeof mime === "string" && mime.trim().toLowerCase().startsWith("image/") ? mime.trim().toLowerCase() : "image/png"};base64,${compact}`;
4413
+ }
4414
+ function readImageReference(value) {
4415
+ if (typeof value === "string") return {
4416
+ type: "image_url",
4417
+ url: normalizeImageUrl(value)
4283
4418
  };
4284
- return {
4285
- ...staged,
4286
- model: typeof model === "string" ? model : void 0,
4287
- readPayload,
4288
- createRequest: async (model, provider = "codex") => {
4289
- const headers = new Headers(requestHeaders);
4290
- headers.delete("content-length");
4291
- let body;
4292
- if (provider === "codex" || provider === "xai" || provider === "github-copilot") {
4293
- const payload = {
4294
- ...await readPayload(),
4295
- ...model === void 0 ? {} : { model }
4296
- };
4297
- if (provider === "xai" && !model) throw new ImageEditRequestError("model is required for xAI multipart edits");
4298
- headers.set("content-type", "application/json");
4299
- body = new TextEncoder().encode(JSON.stringify(provider === "xai" ? translateXaiImageEditJson(payload, model) : payload));
4300
- } else {
4301
- if (model !== void 0) form.set("model", model);
4302
- headers.delete("content-type");
4303
- body = form;
4304
- }
4305
- request.signal.throwIfAborted();
4306
- return new Request(request.url, {
4307
- method: request.method,
4308
- headers,
4309
- body,
4310
- signal: request.signal
4311
- });
4312
- }
4419
+ if (!isRecord$5(value)) throw new XaiImagesRequestError("image must be a URL or an image object");
4420
+ if (typeof value.url === "string") return {
4421
+ type: "image_url",
4422
+ url: normalizeImageUrl(value.url)
4423
+ };
4424
+ if (typeof value.image_url === "string") return {
4425
+ type: "image_url",
4426
+ url: normalizeImageUrl(value.image_url)
4427
+ };
4428
+ if (typeof value.b64_json === "string") return {
4429
+ type: "image_url",
4430
+ url: base64ToDataUrl(value.b64_json, value.mime_type ?? value.media_type)
4313
4431
  };
4432
+ if (value.file_id !== void 0) throw new XaiImagesRequestError("file_id is not supported for xAI image edits; provide an image_url instead");
4433
+ throw new XaiImagesRequestError("image must include a url, image_url, or b64_json");
4314
4434
  }
4315
- // src/lib/image-json-files.ts
4316
- const IMAGE_JSON_MAX_BYTES = 536870912;
4317
- function imageJsonLimitError() {
4318
- return new HTTPError("Image JSON upload exceeds the staging limit", Response.json({ error: {
4319
- message: "Image JSON upload exceeds the staging limit",
4320
- type: "invalid_request_error"
4321
- } }, { status: 413 }));
4435
+ function collectJsonImages(payload) {
4436
+ const values = [];
4437
+ if (isUnknownArray(payload.image)) values.push(...payload.image);
4438
+ else if (payload.image !== void 0) values.push(payload.image);
4439
+ if (isUnknownArray(payload.images)) values.push(...payload.images);
4440
+ if (payload.image_url !== void 0) values.push(payload.image_url);
4441
+ return values;
4322
4442
  }
4323
- /** Owns only an mkdtemp-created directory; callers never supply deletion paths. */
4324
- async function createImageJsonFiles(ttlMs = 9e5) {
4325
- const directory = await mkdtemp(join(tmpdir(), "copilot-image-json-"));
4326
- let index = 0;
4327
- let timer;
4328
- let cleanupPromise;
4329
- const unregister = registerProcessCleanup(() => {
4330
- if (timer) clearTimeout(timer);
4331
- rmSync(directory, {
4332
- recursive: true,
4333
- force: true
4334
- });
4335
- });
4336
- const cleanup = () => {
4337
- cleanupPromise ??= (async () => {
4338
- if (timer) clearTimeout(timer);
4339
- await rm(directory, {
4340
- recursive: true,
4341
- force: true,
4342
- maxRetries: 3,
4343
- retryDelay: 20
4344
- });
4345
- unregister();
4346
- })();
4347
- return cleanupPromise;
4443
+ function isUnknownArray(value) {
4444
+ return Array.isArray(value);
4445
+ }
4446
+ function buildEditPayload(model, prompt, images, source) {
4447
+ if (source.mask !== void 0) throw new XaiImagesRequestError("xAI image edits do not support mask");
4448
+ if (images.length === 0) throw new XaiImagesRequestError("image is required");
4449
+ if (images.length > MAX_EDIT_IMAGES) throw new XaiImagesRequestError(`xAI image edits accept at most ${MAX_EDIT_IMAGES} images`);
4450
+ const payload = {
4451
+ model,
4452
+ prompt
4348
4453
  };
4349
- return {
4350
- directory,
4351
- cleanup,
4352
- scheduleCleanup() {
4353
- if (timer || cleanupPromise) return;
4354
- timer = setTimeout(() => void cleanup().catch(() => {}), ttlMs);
4355
- timer.unref();
4356
- },
4357
- async create() {
4358
- const path = join(directory, String(index++));
4359
- return {
4360
- path,
4361
- handle: await open(path, "wx")
4362
- };
4363
- }
4454
+ applyCommonImageFields(payload, source);
4455
+ if (images.length === 1) payload.image = images[0];
4456
+ else payload.images = images;
4457
+ return payload;
4458
+ }
4459
+ /**
4460
+ * Translates an OpenAI image generation body into an xAI Imagine generation
4461
+ * request. The configured model replaces the client model.
4462
+ */
4463
+ function translateXaiImageGeneration(payload, model) {
4464
+ if (!isRecord$5(payload)) throw new XaiImagesRequestError("Image generation body must be a JSON object");
4465
+ const translated = {
4466
+ model,
4467
+ prompt: readPrompt(payload.prompt)
4364
4468
  };
4469
+ applyCommonImageFields(translated, payload);
4470
+ return translated;
4365
4471
  }
4366
- async function* readImageJsonFile(path, signal) {
4367
- const stream = createReadStream(path, {
4368
- highWaterMark: 65536,
4369
- signal
4370
- });
4472
+ /**
4473
+ * Translates an OpenAI JSON image edit body into an xAI Imagine edit request.
4474
+ */
4475
+ function translateXaiImageEditJson(payload, model) {
4476
+ if (!isRecord$5(payload)) throw new XaiImagesRequestError("Image edit body must be a JSON object");
4477
+ return buildEditPayload(model, readPrompt(payload.prompt), collectJsonImages(payload).map((image) => readImageReference(image)), payload);
4478
+ }
4479
+ const MAX_RESPONSE_IMAGE_BYTES = 33554432;
4480
+ function responseHasB64Json(value) {
4481
+ return typeof value === "string" && value.trim() !== "";
4482
+ }
4483
+ function imageCreatedTimestamp(value) {
4484
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) return Math.trunc(value);
4485
+ return Math.floor(Date.now() / 1e3);
4486
+ }
4487
+ function trustedImageUrl(value) {
4488
+ if (typeof value !== "string" || !value.trim()) return null;
4371
4489
  try {
4372
- for await (const bytes of stream) yield bytes;
4373
- } finally {
4374
- stream.destroy();
4490
+ const url = new URL(value.trim());
4491
+ if (url.protocol !== "https:" || url.hostname !== "imgen.x.ai" || url.port || url.username || url.password) return null;
4492
+ return url.toString();
4493
+ } catch {
4494
+ return null;
4375
4495
  }
4376
4496
  }
4377
- // src/lib/streaming-json.ts
4378
- const JSON_CHUNK_BYTES = 65536;
4379
- /** Never ask the tokenizer to accumulate untrusted strings, keys, or numbers. */
4380
- var StreamingJsonReader = class {
4381
- decode = new TextDecoder("utf-8", { fatal: true });
4382
- parse = jsonParser({ packValues: false });
4383
- depth = 0;
4384
- scalarLength = 0;
4385
- boundedScalar = false;
4386
- read(bytes) {
4387
- const text = this.decode.decode(bytes, { stream: bytes !== void 0 });
4388
- const result = this.parse(text);
4389
- const tokens = result === none ? [] : result.values;
4390
- if (bytes === void 0) {
4391
- const last = this.parse(none);
4392
- if (last !== none) tokens.push(...last.values);
4393
- }
4394
- for (const token of tokens) switch (token.name) {
4395
- case "startObject":
4396
- case "startArray":
4397
- if (++this.depth > 64) throw new SyntaxError("JSON nesting exceeds 64");
4398
- break;
4399
- case "endObject":
4400
- case "endArray":
4401
- this.depth--;
4402
- break;
4403
- case "startKey":
4404
- case "startNumber":
4405
- this.scalarLength = 0;
4406
- this.boundedScalar = true;
4407
- break;
4408
- case "endKey":
4409
- case "endNumber":
4410
- this.boundedScalar = false;
4411
- break;
4412
- case "stringChunk":
4413
- case "numberChunk": if (this.boundedScalar) {
4414
- this.scalarLength += token.value.length;
4415
- if (this.scalarLength > 65536) throw new SyntaxError("JSON key or number is too large");
4416
- }
4417
- }
4418
- return tokens;
4497
+ async function readImageBytes(response) {
4498
+ const declaredLength = Number(response.headers.get("content-length"));
4499
+ if (Number.isFinite(declaredLength) && declaredLength > MAX_RESPONSE_IMAGE_BYTES) {
4500
+ await response.body?.cancel().catch(() => {});
4501
+ return null;
4419
4502
  }
4420
- };
4421
- /** Splitting also bounds tokenizer allocations when a runtime supplies a huge chunk. */
4422
- async function* readJsonBytes(body, signal) {
4423
- if (!body) return;
4424
- const reader = body.getReader();
4425
- const abort = () => void reader.cancel(signal?.reason).catch(() => {});
4426
- signal?.addEventListener("abort", abort, { once: true });
4503
+ if (!response.body) return null;
4504
+ const reader = response.body.getReader();
4505
+ const chunks = [];
4506
+ let length = 0;
4427
4507
  let complete = false;
4428
4508
  try {
4429
4509
  while (true) {
4430
- signal?.throwIfAborted();
4431
- const { done, value } = await reader.read();
4432
- signal?.throwIfAborted();
4433
- if (done) {
4434
- complete = true;
4435
- return;
4436
- }
4437
- for (let offset = 0; offset < value.byteLength; offset += JSON_CHUNK_BYTES) yield value.subarray(offset, offset + JSON_CHUNK_BYTES);
4438
- }
4439
- } finally {
4440
- signal?.removeEventListener("abort", abort);
4441
- if (!complete) await reader.cancel().catch(() => {});
4442
- reader.releaseLock();
4443
- }
4444
- }
4445
- function createJsonByteSource(body, signal) {
4446
- const controller = new AbortController();
4447
- const bytes = readJsonBytes(body, AbortSignal.any([signal, controller.signal]));
4448
- return {
4449
- bytes,
4450
- async cancel(reason) {
4451
- controller.abort(reason);
4452
- await bytes.return(void 0).catch(() => {});
4453
- if (body && !body.locked) await body.cancel(reason).catch(() => {});
4454
- }
4455
- };
4456
- }
4457
- /** Lazy streams allow a route to choose the original or rebuilt body without teeing. */
4458
- function jsonByteStream(iterator, cancel) {
4459
- return new ReadableStream({
4460
- async pull(controller) {
4461
- try {
4462
- const { done, value } = await iterator.next();
4463
- if (done) controller.close();
4464
- else controller.enqueue(value);
4465
- } catch (error) {
4466
- controller.error(error);
4467
- await iterator.return(void 0).catch(() => {});
4468
- }
4469
- },
4470
- async cancel(reason) {
4471
- await cancel?.(reason);
4472
- await iterator.return(void 0);
4473
- }
4474
- }, { highWaterMark: 0 });
4475
- }
4476
- var DuplicateImageModelError = class extends SyntaxError {
4477
- constructor() {
4478
- super("Duplicate top-level image model");
4479
- }
4480
- };
4481
- /** Keeps only the top-level model; all image string chunks are discarded. */
4482
- var ImageModelReader = class {
4483
- model;
4484
- object = false;
4485
- complete = false;
4486
- invalidModel = false;
4487
- depth = 0;
4488
- key = "";
4489
- inKey = false;
4490
- readingModel = false;
4491
- modelText = "";
4492
- seenModel = false;
4493
- read(tokens) {
4494
- for (const token of tokens) {
4495
- if (token.name === "startKey") {
4496
- this.inKey = true;
4497
- this.key = "";
4498
- } else if (token.name === "endKey") {
4499
- this.inKey = false;
4500
- if (this.depth === 1 && this.key === "model") {
4501
- if (this.seenModel) throw new DuplicateImageModelError();
4502
- this.seenModel = true;
4503
- }
4504
- } else if (token.name === "stringChunk") {
4505
- if (this.inKey) this.key += token.value;
4506
- else if (this.readingModel) {
4507
- this.modelText += token.value;
4508
- if (this.modelText.length > 4096) {
4509
- this.invalidModel = true;
4510
- this.readingModel = false;
4511
- this.modelText = "";
4512
- }
4513
- }
4514
- } else if (token.name === "startString") this.readingModel = this.depth === 1 && this.key === "model";
4515
- else if (token.name === "endString") {
4516
- if (this.readingModel) {
4517
- this.model = this.modelText;
4518
- this.modelText = "";
4519
- this.readingModel = false;
4520
- }
4521
- } else if (this.depth === 1 && this.key === "model" && [
4522
- "startObject",
4523
- "startArray",
4524
- "startNumber",
4525
- "nullValue",
4526
- "trueValue",
4527
- "falseValue"
4528
- ].includes(token.name)) this.invalidModel = true;
4529
- if (token.name === "startObject" || token.name === "startArray") {
4530
- if (this.depth === 0) this.object = token.name === "startObject";
4531
- this.depth++;
4532
- } else if (token.name === "endObject" || token.name === "endArray") {
4533
- if (--this.depth === 0) this.complete = true;
4534
- }
4535
- }
4536
- }
4537
- };
4538
- /** Re-encodes bounded chunks, replacing or inserting the top-level model. */
4539
- async function* rewriteImageModel(input, model, insertModel = false) {
4540
- const parser = new StreamingJsonReader();
4541
- const encode = new TextEncoder();
4542
- const stringify = stringer();
4543
- let depth = 0;
4544
- let key = "";
4545
- let inKey = false;
4546
- let replacing = false;
4547
- const process = (tokens) => {
4548
- const parts = [];
4549
- const emit = (token) => {
4550
- const text = stringify(token);
4551
- if (text !== none) parts.push(text);
4552
- };
4553
- for (const token of tokens) {
4554
- if (insertModel && depth === 0 && token.name === "startObject") {
4555
- emit(token);
4556
- emit({ name: "startKey" });
4557
- emit({
4558
- name: "stringChunk",
4559
- value: "model"
4560
- });
4561
- emit({ name: "endKey" });
4562
- emit({ name: "startString" });
4563
- emit({
4564
- name: "stringChunk",
4565
- value: model
4566
- });
4567
- emit({ name: "endString" });
4568
- depth++;
4569
- continue;
4570
- }
4571
- if (replacing) {
4572
- if (token.name === "endString") replacing = false;
4573
- continue;
4574
- }
4575
- if (token.name === "startKey") {
4576
- inKey = true;
4577
- key = "";
4578
- } else if (token.name === "endKey") inKey = false;
4579
- else if (token.name === "stringChunk" && inKey) key += token.value;
4580
- if (token.name === "startString" && depth === 1 && key === "model") {
4581
- emit(token);
4582
- emit({
4583
- name: "stringChunk",
4584
- value: model
4585
- });
4586
- emit({ name: "endString" });
4587
- replacing = true;
4588
- continue;
4510
+ const { done, value } = await reader.read();
4511
+ if (done) {
4512
+ complete = true;
4513
+ return length > 0 ? Buffer.concat(chunks, length) : null;
4589
4514
  }
4590
- emit(token);
4591
- if (token.name === "startObject" || token.name === "startArray") depth++;
4592
- else if (token.name === "endObject" || token.name === "endArray") depth--;
4515
+ length += value.byteLength;
4516
+ if (length > MAX_RESPONSE_IMAGE_BYTES) return null;
4517
+ chunks.push(value);
4593
4518
  }
4594
- return encode.encode(parts.join(""));
4595
- };
4596
- for await (const bytes of input) yield process(parser.read(bytes));
4597
- const tail = process(parser.read());
4598
- if (tail.byteLength) yield tail;
4519
+ } finally {
4520
+ if (!complete) await reader.cancel().catch(() => {});
4521
+ reader.releaseLock();
4522
+ }
4599
4523
  }
4600
- // src/lib/image-json-request.ts
4601
- function rebuild(request, body, changed = false) {
4602
- const headers = new Headers(request.headers);
4603
- if (changed) headers.delete("content-length");
4604
- return new Request(request.url, {
4605
- method: request.method,
4524
+ function imageDownloadErrorResponse() {
4525
+ return Response.json({ error: {
4526
+ message: "Failed to retrieve xAI image output as b64_json",
4527
+ type: "upstream_error"
4528
+ } }, { status: 502 });
4529
+ }
4530
+ function imageOutputUrlForLog(value) {
4531
+ if (typeof value !== "string" || !value.trim()) return null;
4532
+ try {
4533
+ const url = new URL(value.trim());
4534
+ if (url.protocol !== "https:" && url.protocol !== "http:") return null;
4535
+ url.username = "";
4536
+ url.password = "";
4537
+ for (const key of url.searchParams.keys()) if (/token|secret|signature|credential|password|authorization|api[_-]?key|^key$|^sig$/iu.test(key)) url.searchParams.set(key, "[REDACTED]");
4538
+ return url.toString();
4539
+ } catch {
4540
+ return null;
4541
+ }
4542
+ }
4543
+ function rebuildJsonResponse(response, body) {
4544
+ const headers = new Headers(response.headers);
4545
+ headers.delete("content-length");
4546
+ if (!headers.get("content-type")) headers.set("content-type", "application/json");
4547
+ return new Response(body, {
4606
4548
  headers,
4607
- body,
4608
- signal: request.signal,
4609
- duplex: "half"
4549
+ status: response.status,
4550
+ statusText: response.statusText
4610
4551
  });
4611
4552
  }
4612
- /** Read at most 64 KiB before forwarding a model-first request. Late models use disk. */
4613
- async function prepareImageJsonRequest(request, maxStagedBytes = IMAGE_JSON_MAX_BYTES) {
4614
- const forwardingAbort = new AbortController();
4615
- const signal = AbortSignal.any([request.signal, forwardingAbort.signal]);
4616
- const input = createJsonByteSource(request.body, signal);
4617
- const bytes = input.bytes;
4618
- const parser = new StreamingJsonReader();
4619
- const metadata = new ImageModelReader();
4620
- let valid = true;
4621
- const scan = (chunk) => {
4622
- if (!valid) return;
4623
- try {
4624
- metadata.read(parser.read(chunk));
4625
- } catch (error) {
4626
- if (error instanceof DuplicateImageModelError) throw error;
4627
- valid = false;
4628
- }
4629
- };
4630
- const prefix = [];
4631
- let length = 0;
4632
- let eof = false;
4633
- let pending;
4634
- async function* remaining() {
4635
- if (pending) {
4636
- const chunk = pending;
4637
- pending = void 0;
4638
- yield chunk;
4639
- }
4640
- yield* bytes;
4641
- }
4642
- let files;
4643
- let stagedPath;
4553
+ /**
4554
+ * Adapts an xAI image body to the OpenAI Images response.
4555
+ * Generation returns created: null, and edits omit created. OpenAI clients
4556
+ * require a unix timestamp. When b64_json is requested, a URL-only result
4557
+ * is downloaded from the xAI CDN with redirects disabled and a byte limit.
4558
+ */
4559
+ async function ensureImageResponseB64Json(response, fetchImage = globalThis.fetch, { clientSignal, onImageUrl, responseFormat = "b64_json" } = {}) {
4560
+ if (!response.ok) return response;
4561
+ clientSignal?.throwIfAborted();
4562
+ const body = await response.text();
4563
+ let payload;
4644
4564
  try {
4645
- while (length < JSON_CHUNK_BYTES) {
4646
- const next = await bytes.next();
4647
- if (next.done) {
4648
- eof = true;
4649
- scan();
4650
- break;
4651
- }
4652
- const chunk = next.value.subarray(0, JSON_CHUNK_BYTES - length);
4653
- pending = chunk.byteLength < next.value.byteLength ? next.value.subarray(chunk.byteLength) : void 0;
4654
- prefix.push(chunk);
4655
- length += chunk.byteLength;
4656
- scan(chunk);
4657
- if (!valid || metadata.model !== void 0 || metadata.invalidModel || metadata.complete) break;
4565
+ payload = JSON.parse(body);
4566
+ } catch {
4567
+ return rebuildJsonResponse(response, body);
4568
+ }
4569
+ if (!isRecord$5(payload) || !Array.isArray(payload.data)) return rebuildJsonResponse(response, body);
4570
+ let changed = false;
4571
+ const data = [];
4572
+ for (const item of payload.data) {
4573
+ if (isRecord$5(item) && onImageUrl) {
4574
+ const outputUrl = imageOutputUrlForLog(item.url);
4575
+ if (outputUrl) onImageUrl(outputUrl);
4658
4576
  }
4659
- if (valid && !eof && !metadata.complete && metadata.model === void 0 && !metadata.invalidModel) {
4660
- files = await createImageJsonFiles();
4661
- const file = await files.create();
4662
- stagedPath = file.path;
4663
- try {
4664
- for (const chunk of prefix) {
4665
- if (length > maxStagedBytes) throw imageJsonLimitError();
4666
- await file.handle.writeFile(chunk);
4667
- }
4668
- prefix.length = 0;
4669
- for await (const chunk of remaining()) {
4670
- length += chunk.byteLength;
4671
- if (length > maxStagedBytes) throw imageJsonLimitError();
4672
- scan(chunk);
4673
- await file.handle.writeFile(chunk);
4674
- }
4675
- eof = true;
4676
- scan();
4677
- } finally {
4678
- await file.handle.close();
4577
+ if (!isRecord$5(item) || responseFormat === "url" || responseHasB64Json(item.b64_json)) {
4578
+ data.push(item);
4579
+ continue;
4580
+ }
4581
+ const url = trustedImageUrl(item.url);
4582
+ if (!url) return imageDownloadErrorResponse();
4583
+ try {
4584
+ clientSignal?.throwIfAborted();
4585
+ const timeoutSignal = AbortSignal.timeout(6e4);
4586
+ const imageResponse = await fetchImage(url, {
4587
+ method: "GET",
4588
+ redirect: "error",
4589
+ signal: clientSignal ? AbortSignal.any([clientSignal, timeoutSignal]) : timeoutSignal
4590
+ });
4591
+ if (!imageResponse.ok) {
4592
+ await imageResponse.body?.cancel().catch(() => {});
4593
+ return imageDownloadErrorResponse();
4679
4594
  }
4680
- files.scheduleCleanup();
4595
+ const bytes = await readImageBytes(imageResponse);
4596
+ if (!bytes) return imageDownloadErrorResponse();
4597
+ data.push({
4598
+ ...item,
4599
+ b64_json: bytes.toString("base64")
4600
+ });
4601
+ changed = true;
4602
+ } catch {
4603
+ clientSignal?.throwIfAborted();
4604
+ return imageDownloadErrorResponse();
4681
4605
  }
4606
+ }
4607
+ const created = imageCreatedTimestamp(payload.created);
4608
+ if (!changed && payload.created === created) return rebuildJsonResponse(response, body);
4609
+ return rebuildJsonResponse(response, JSON.stringify({
4610
+ ...payload,
4611
+ created,
4612
+ data
4613
+ }));
4614
+ }
4615
+ // src/routes/images/multipart-edits.ts
4616
+ const multipartEditDependencies = { stageMultipartBodyToDisk };
4617
+ var ImageEditRequestError = class extends Error {};
4618
+ /** Share edit validation and staged-upload cleanup across both route families. */
4619
+ async function withImageEditRequest(c, operation, handle) {
4620
+ if (operation !== "edits") return await handle(null);
4621
+ const mediaType = c.req.header("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
4622
+ if (mediaType !== "application/json" && mediaType !== "multipart/form-data") return c.json({ error: {
4623
+ message: "Image edits require application/json or multipart/form-data",
4624
+ type: "invalid_request_error"
4625
+ } }, 415);
4626
+ const multipart = await parseMultipartImageEdit(c.req.raw);
4627
+ try {
4628
+ return await handle(multipart);
4682
4629
  } catch (error) {
4683
- await input.cancel(error);
4684
- await files?.cleanup();
4685
- if (error instanceof DuplicateImageModelError) throw new HTTPError(error.message, Response.json({ error: {
4686
- message: error.message,
4687
- type: "invalid_request_error"
4688
- } }, { status: 400 }));
4630
+ await multipart?.cleanup();
4689
4631
  throw error;
4632
+ } finally {
4633
+ multipart?.scheduleCleanup();
4690
4634
  }
4691
- let claimed = false;
4692
- const abort = () => {
4693
- input.cancel().catch(() => {});
4694
- files?.cleanup().catch(() => {});
4695
- };
4696
- request.signal.addEventListener("abort", abort, { once: true });
4697
- async function* source() {
4698
- if (claimed) throw new Error("Image request body already forwarded");
4699
- claimed = true;
4700
- try {
4701
- signal.throwIfAborted();
4702
- if (stagedPath) yield* readImageJsonFile(stagedPath, signal);
4703
- else {
4704
- yield* prefix;
4705
- prefix.length = 0;
4706
- if (!eof) {
4707
- for await (const chunk of remaining()) {
4708
- scan(chunk);
4709
- yield chunk;
4710
- }
4711
- scan();
4712
- }
4713
- }
4714
- } finally {
4715
- request.signal.removeEventListener("abort", abort);
4716
- await bytes.return(void 0).catch(() => {});
4717
- await files?.cleanup();
4635
+ }
4636
+ function imageEditRequestErrorResponse(c, error) {
4637
+ if (c.req.raw.signal.aborted) return null;
4638
+ if (!(error instanceof ImageEditRequestError) && !(error instanceof InvalidMultipartBodyError) && !(error instanceof MultipartBodyTooLargeError) && !(error instanceof XaiImagesRequestError)) return null;
4639
+ return c.json({ error: {
4640
+ message: error.message,
4641
+ type: "invalid_request_error"
4642
+ } }, error instanceof MultipartBodyTooLargeError ? 413 : 400);
4643
+ }
4644
+ function imageMimeType(bytes, declared) {
4645
+ if (declared.startsWith("image/")) return declared;
4646
+ if (bytes.subarray(0, 8).equals(Buffer.from([
4647
+ 137,
4648
+ 80,
4649
+ 78,
4650
+ 71,
4651
+ 13,
4652
+ 10,
4653
+ 26,
4654
+ 10
4655
+ ]))) return "image/png";
4656
+ if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpeg";
4657
+ if (bytes.toString("ascii", 0, 4) === "RIFF" && bytes.toString("ascii", 8, 12) === "WEBP") return "image/webp";
4658
+ if (/^GIF8[79]a/u.test(bytes.toString("ascii", 0, 6))) return "image/gif";
4659
+ throw new ImageEditRequestError("Uploaded image must have an image MIME type");
4660
+ }
4661
+ async function imageReference(value, signal) {
4662
+ signal.throwIfAborted();
4663
+ if (typeof value === "string") {
4664
+ const url = value.trim();
4665
+ 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");
4666
+ return { image_url: url };
4667
+ }
4668
+ const bytes = Buffer.from(await value.arrayBuffer());
4669
+ signal.throwIfAborted();
4670
+ if (bytes.length === 0) throw new ImageEditRequestError("image is empty");
4671
+ return { image_url: `data:${imageMimeType(bytes, value.type.trim().toLowerCase())};base64,${bytes.toString("base64")}` };
4672
+ }
4673
+ function fieldValue(name, value) {
4674
+ if ([
4675
+ "n",
4676
+ "partial_images",
4677
+ "output_compression"
4678
+ ].includes(name)) {
4679
+ const trimmed = value.trim();
4680
+ const number = Number(trimmed);
4681
+ if (!/^-?\d+$/u.test(trimmed) || !Number.isSafeInteger(number)) throw new ImageEditRequestError(`${name} must be an integer`);
4682
+ return number;
4683
+ }
4684
+ if (name === "stream") {
4685
+ if (value.trim() === "true") return true;
4686
+ if (value.trim() === "false") return false;
4687
+ throw new ImageEditRequestError("stream must be true or false");
4688
+ }
4689
+ return value;
4690
+ }
4691
+ async function formPayload(form, signal) {
4692
+ const fields = /* @__PURE__ */ new Map();
4693
+ const images = [];
4694
+ for (const [name, value] of form.entries()) {
4695
+ if (name === "image" || name === "image[]") {
4696
+ images.push(await imageReference(value, signal));
4697
+ continue;
4718
4698
  }
4699
+ const key = name === "mask[]" ? "mask" : name;
4700
+ if (fields.has(key)) throw new ImageEditRequestError(`Duplicate image edit field: ${key}`);
4701
+ if (key === "mask") fields.set(key, await imageReference(value, signal));
4702
+ else if (typeof value === "string") fields.set(key, fieldValue(key, value));
4703
+ else throw new ImageEditRequestError(`Unexpected image edit file field: ${key}`);
4719
4704
  }
4720
- const cancel = async (reason) => {
4721
- forwardingAbort.abort(reason);
4722
- request.signal.removeEventListener("abort", abort);
4723
- await input.cancel(reason);
4724
- await files?.cleanup();
4705
+ if (images.length === 0) throw new ImageEditRequestError("image is required");
4706
+ fields.set("images", images);
4707
+ return Object.fromEntries(fields);
4708
+ }
4709
+ /** Parse multipart once, keeping uploads on disk until the target is known. */
4710
+ async function parseMultipartImageEdit(request) {
4711
+ const requestHeaders = new Headers(request.headers);
4712
+ const contentType = requestHeaders.get("content-type");
4713
+ if (contentType?.split(";", 1)[0]?.trim().toLowerCase() !== "multipart/form-data") return null;
4714
+ const staged = await multipartEditDependencies.stageMultipartBodyToDisk(request.body, contentType, {}, request.signal);
4715
+ const form = staged.formData;
4716
+ const model = form.get("model");
4717
+ let payloadPromise;
4718
+ const readPayload = () => {
4719
+ payloadPromise ??= formPayload(form, request.signal).finally(staged.cleanup);
4720
+ return payloadPromise;
4725
4721
  };
4726
4722
  return {
4727
- valid: valid && metadata.object && !metadata.invalidModel,
4728
- model: metadata.model,
4729
- originalRequest: rebuild(request, jsonByteStream(source(), cancel)),
4730
- createRequest: (model) => rebuild(request, jsonByteStream(rewriteImageModel(source(), model, metadata.model === void 0), cancel), true),
4731
- cleanup: cancel,
4732
- stagedDirectory: files?.directory
4723
+ ...staged,
4724
+ model: typeof model === "string" ? model : void 0,
4725
+ readPayload,
4726
+ createRequest: async (model, provider = "codex") => {
4727
+ const headers = new Headers(requestHeaders);
4728
+ headers.delete("content-length");
4729
+ let body;
4730
+ if (provider === "codex" || provider === "xai" || provider === "github-copilot") {
4731
+ const payload = {
4732
+ ...await readPayload(),
4733
+ ...model === void 0 ? {} : { model }
4734
+ };
4735
+ if (provider === "xai" && !model) throw new ImageEditRequestError("model is required for xAI multipart edits");
4736
+ headers.set("content-type", "application/json");
4737
+ body = new TextEncoder().encode(JSON.stringify(provider === "xai" ? translateXaiImageEditJson(payload, model) : payload));
4738
+ } else {
4739
+ if (model !== void 0) form.set("model", model);
4740
+ headers.delete("content-type");
4741
+ body = form;
4742
+ }
4743
+ request.signal.throwIfAborted();
4744
+ return new Request(request.url, {
4745
+ method: request.method,
4746
+ headers,
4747
+ body,
4748
+ signal: request.signal
4749
+ });
4750
+ }
4733
4751
  };
4734
4752
  }
4735
4753
  // src/lib/image-json-metadata.ts
@@ -4909,35 +4927,6 @@ function observeImagesResponse(response, options) {
4909
4927
  }
4910
4928
  }));
4911
4929
  }
4912
- // src/lib/xai-image-model.ts
4913
- /**
4914
- * Resolves one image-route model setting. Empty values keep the OpenAI
4915
- * protocol. Only an xai/model alias selects the xAI Imagine API.
4916
- */
4917
- function resolveConfiguredXaiImageModel(configured, settingName) {
4918
- if (configured === void 0 || configured === null) return { status: "disabled" };
4919
- if (typeof configured !== "string") return {
4920
- message: `${settingName} must be an xai/model alias`,
4921
- status: "invalid"
4922
- };
4923
- const trimmed = configured.trim();
4924
- if (!trimmed) return { status: "disabled" };
4925
- const alias = parseProviderModelAlias(trimmed);
4926
- if (alias?.provider.toLowerCase() !== "xai") return {
4927
- message: `${settingName} only supports an xai/model alias, received '${trimmed}'`,
4928
- status: "invalid"
4929
- };
4930
- return {
4931
- model: alias.model,
4932
- status: "xai"
4933
- };
4934
- }
4935
- function getImageGenerationModel() {
4936
- return resolveConfiguredXaiImageModel(getConfig().imageGenerationModel, "imageGenerationModel");
4937
- }
4938
- function getImageEditModel() {
4939
- return resolveConfiguredXaiImageModel(getConfig().imageEditModel, "imageEditModel");
4940
- }
4941
4930
  // src/lib/image-json-value.ts
4942
4931
  const STRING_LIMIT = 65536;
4943
4932
  const METADATA_LIMIT = 1048576;
@@ -5292,6 +5281,32 @@ function logImageJsonRequest(logger, label, request, extra, debug) {
5292
5281
  duplex: "half"
5293
5282
  });
5294
5283
  }
5284
+ DEFAULT_IMAGE_MODELS.codex;
5285
+ const CODEX_IMAGES_TIMEOUT_MS = 9e5;
5286
+ const codexImagesDispatcher = createTimeoutDispatcher(CODEX_IMAGES_TIMEOUT_MS);
5287
+ function resolveCodexImagesUrl(requestUrl, operation) {
5288
+ const upstreamUrl = new URL(`${CODEX_API_BASE_URL}/codex/images/${operation}`);
5289
+ upstreamUrl.search = new URL(requestUrl, "http://localhost").search;
5290
+ return upstreamUrl.toString();
5291
+ }
5292
+ async function forwardCodexImages(request, operation) {
5293
+ const headers = buildCodexRequestHeaders(request.headers);
5294
+ if (!headers.has("accept")) headers.set("accept", "application/json");
5295
+ if (operation === "generations" && !headers.has("content-type")) headers.set("content-type", "application/json");
5296
+ const init = {
5297
+ method: "POST",
5298
+ headers,
5299
+ body: request.body,
5300
+ duplex: "half",
5301
+ signal: AbortSignal.any([request.signal, AbortSignal.timeout(CODEX_IMAGES_TIMEOUT_MS)])
5302
+ };
5303
+ const upstreamUrl = resolveCodexImagesUrl(request.url, operation);
5304
+ if (typeof Bun !== "undefined") return await fetch(upstreamUrl, init);
5305
+ return await fetch$1(upstreamUrl, {
5306
+ ...init,
5307
+ dispatcher: codexImagesDispatcher
5308
+ });
5309
+ }
5295
5310
  DEFAULT_IMAGE_MODELS["github-copilot"];
5296
5311
  const COPILOT_CLIENT_VERSION = "copilot/1.0.103-unstable.r40031404408.gba6c003";
5297
5312
  const COPILOT_IMAGES_TIMEOUT_MS = 9e5;
@@ -5337,6 +5352,7 @@ async function forwardCopilotImages(request, operation) {
5337
5352
  return response;
5338
5353
  }
5339
5354
  // src/routes/images/forward-provider-images.ts
5355
+ const imageForwardDependencies = { debugJsonAsync };
5340
5356
  /**
5341
5357
  * Forwards an images request to a configured provider with debug logging.
5342
5358
  * Shared by the top-level images routes (model mapping) and the
@@ -5346,33 +5362,35 @@ async function forwardProviderImagesWithLogging(providerConfig, request, operati
5346
5362
  let actualModel = model;
5347
5363
  if (actualModel === void 0 && !request.headers.get("content-type")?.includes("multipart/form-data")) {
5348
5364
  const parsed = await prepareImageJsonRequest(request);
5349
- actualModel = parsed.valid ? parsed.model : "unknown";
5365
+ actualModel = parsed.model;
5350
5366
  request = parsed.originalRequest;
5351
5367
  }
5352
5368
  actualModel ??= "unknown";
5353
- request = await logImageEditRequest(logger, `provider.images.${operation}.request`, request, { provider });
5354
- let upstreamResponse;
5369
+ const isCodex = providerConfig.name === "codex";
5370
+ const label = isCodex ? `images.${operation}.codex` : `provider.images.${operation}`;
5371
+ const extra = isCodex ? {} : { provider };
5355
5372
  try {
5356
- upstreamResponse = providerConfig.name === "github-copilot" ? await forwardCopilotImages(request, operation) : await forwardProviderImages(providerConfig, request, operation);
5373
+ request = await logImageEditRequest(logger, `${label}.request`, request, extra, imageForwardDependencies.debugJsonAsync);
5374
+ const upstreamResponse = isCodex ? await forwardCodexImages(request, operation) : providerConfig.name === "github-copilot" ? await forwardCopilotImages(request, operation) : await forwardProviderImages(providerConfig, request, operation);
5375
+ debugJson(logger, `${label}.response`, {
5376
+ contentType: upstreamResponse.headers.get("content-type"),
5377
+ contentLength: upstreamResponse.headers.get("content-length"),
5378
+ ...extra,
5379
+ statusCode: upstreamResponse.status
5380
+ });
5381
+ return observeImagesResponse(upstreamResponse, {
5382
+ model: actualModel,
5383
+ operation,
5384
+ providerConfig,
5385
+ onMetadata: (metadata) => debugJson(logger, `${label}.response.body`, {
5386
+ ...extra,
5387
+ ...metadata
5388
+ })
5389
+ });
5357
5390
  } catch (error) {
5358
5391
  await request.body?.cancel().catch(() => {});
5359
5392
  throw error;
5360
5393
  }
5361
- debugJson(logger, `provider.images.${operation}.response`, {
5362
- contentType: upstreamResponse.headers.get("content-type"),
5363
- contentLength: upstreamResponse.headers.get("content-length"),
5364
- provider,
5365
- statusCode: upstreamResponse.status
5366
- });
5367
- return observeImagesResponse(upstreamResponse, {
5368
- model: actualModel,
5369
- operation,
5370
- providerConfig,
5371
- onMetadata: (metadata) => debugJson(logger, `provider.images.${operation}.response.body`, {
5372
- provider,
5373
- ...metadata
5374
- })
5375
- });
5376
5394
  }
5377
5395
  async function forwardCopilotImagesWithLogging(request, operation, context) {
5378
5396
  const config = getRawProviderConfig(GITHUB_COPILOT_PROVIDER);
@@ -5389,7 +5407,7 @@ async function forwardCopilotImagesWithLogging(request, operation, context) {
5389
5407
  /** Forwards translated xAI requests and normalizes OpenAI-compatible output. */
5390
5408
  async function forwardXaiImagesWithLogging(providerConfig, request, operation, payload, { logger, provider }, body) {
5391
5409
  return await ensureImageResponseB64Json(await forwardProviderImagesWithLogging(providerConfig, new Request(request.url, {
5392
- body: body ?? new TextEncoder().encode(JSON.stringify(payload)),
5410
+ body,
5393
5411
  duplex: "half",
5394
5412
  headers: {
5395
5413
  accept: "application/json",
@@ -5412,33 +5430,6 @@ async function forwardXaiImagesWithLogging(providerConfig, request, operation, p
5412
5430
  responseFormat: payload.response_format === "url" ? "url" : "b64_json"
5413
5431
  });
5414
5432
  }
5415
- // src/services/codex/images.ts
5416
- const DEFAULT_CODEX_IMAGE_MODEL = DEFAULT_IMAGE_MODELS.codex;
5417
- const CODEX_IMAGES_TIMEOUT_MS = 9e5;
5418
- const codexImagesDispatcher = createTimeoutDispatcher(CODEX_IMAGES_TIMEOUT_MS);
5419
- function resolveCodexImagesUrl(requestUrl, operation) {
5420
- const upstreamUrl = new URL(`${CODEX_API_BASE_URL}/codex/images/${operation}`);
5421
- upstreamUrl.search = new URL(requestUrl, "http://localhost").search;
5422
- return upstreamUrl.toString();
5423
- }
5424
- async function forwardCodexImages(request, operation) {
5425
- const headers = buildCodexRequestHeaders(request.headers);
5426
- if (!headers.has("accept")) headers.set("accept", "application/json");
5427
- if (operation === "generations" && !headers.has("content-type")) headers.set("content-type", "application/json");
5428
- const init = {
5429
- method: "POST",
5430
- headers,
5431
- body: request.body,
5432
- duplex: "half",
5433
- signal: AbortSignal.any([request.signal, AbortSignal.timeout(CODEX_IMAGES_TIMEOUT_MS)])
5434
- };
5435
- const upstreamUrl = resolveCodexImagesUrl(request.url, operation);
5436
- if (typeof Bun !== "undefined") return await fetch(upstreamUrl, init);
5437
- return await fetch$1(upstreamUrl, {
5438
- ...init,
5439
- dispatcher: codexImagesDispatcher
5440
- });
5441
- }
5442
5433
  // src/routes/images/xai-json-request.ts
5443
5434
  /** Existing translators validate a bounded skeleton; large strings stay on disk. */
5444
5435
  async function translateXaiImageJsonRequest(request, operation, model) {
@@ -5456,11 +5447,9 @@ async function translateXaiImageJsonRequest(request, operation, model) {
5456
5447
  } catch {
5457
5448
  throw new XaiImagesRequestError("image b64_json is not valid base64");
5458
5449
  }
5459
- const input = json.value;
5460
5450
  return {
5461
5451
  payload,
5462
- body: externalImageJsonStream(json, payload, request.signal),
5463
- requestedModel: typeof input.model === "string" ? input.model : model
5452
+ body: externalImageJsonStream(json, payload, request.signal)
5464
5453
  };
5465
5454
  } catch (error) {
5466
5455
  await json.cleanup();
@@ -5468,25 +5457,21 @@ async function translateXaiImageJsonRequest(request, operation, model) {
5468
5457
  }
5469
5458
  }
5470
5459
  // src/routes/images/default-model.ts
5471
- /** Resolve the three supported image models before sending the request. */
5460
+ /** Only missing models and bare gpt-image-2 select an available default. */
5472
5461
  async function resolveImageRequestModel(requestedModel, options = {}) {
5473
- if (options.provider) {
5474
- const model = getDefaultImageModel(options.provider);
5475
- if (model) return model;
5462
+ const requested = requestedModel?.trim();
5463
+ if (requested && requested !== DEFAULT_IMAGE_MODELS.codex) {
5464
+ const alias = parseProviderModelAlias(requested);
5465
+ if (alias) return alias;
5466
+ return {
5467
+ provider: "codex",
5468
+ model: requested
5469
+ };
5476
5470
  }
5477
- const availability = /* @__PURE__ */ new Map();
5478
- const isAvailable = (provider) => {
5479
- let pending = availability.get(provider);
5480
- if (!pending) {
5481
- pending = provider === "github-copilot" ? Promise.resolve((options.isGitHubCopilotAvailable ?? isGitHubCopilotAvailable)()) : (options.resolveProviderConfig ?? resolveProviderConfig)(provider).then((config) => config !== null);
5482
- availability.set(provider, pending);
5483
- }
5484
- return pending;
5471
+ for (const provider of IMAGE_PROVIDER_PRIORITY) if (provider === "github-copilot" ? (options.isGitHubCopilotAvailable ?? isGitHubCopilotAvailable)() : await (options.resolveProviderConfig ?? resolveProviderConfig)(provider) !== null) return {
5472
+ provider,
5473
+ model: DEFAULT_IMAGE_MODELS[provider]
5485
5474
  };
5486
- const requested = requestedModel?.trim();
5487
- const selected = IMAGE_PROVIDER_PRIORITY.find((provider) => requested === DEFAULT_IMAGE_MODELS[provider] || requested === `${provider}/${DEFAULT_IMAGE_MODELS[provider]}`);
5488
- if (selected && await isAvailable(selected)) return `${selected}/${DEFAULT_IMAGE_MODELS[selected]}`;
5489
- for (const provider of IMAGE_PROVIDER_PRIORITY) if (await isAvailable(provider)) return `${provider}/${DEFAULT_IMAGE_MODELS[provider]}`;
5490
5475
  throw new HTTPError("No enabled image provider is available", Response.json({ error: {
5491
5476
  message: "No enabled image provider is available",
5492
5477
  type: "invalid_request_error"
@@ -5495,84 +5480,18 @@ async function resolveImageRequestModel(requestedModel, options = {}) {
5495
5480
  // src/routes/images/shared.ts
5496
5481
  const logger$15 = createHandlerLogger("images-handler");
5497
5482
  const imageRouteDependencies = {
5498
- debugJsonAsync,
5499
- getImageEditModel,
5500
- getImageGenerationModel,
5501
5483
  isGitHubCopilotAvailable,
5502
5484
  resolveMappedModel,
5503
5485
  resolveProviderConfig
5504
5486
  };
5505
- function imageEditContentTypeErrorResponse(c) {
5506
- const mediaType = c.req.header("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
5507
- if (mediaType === "application/json" || mediaType === "multipart/form-data") return null;
5508
- return c.json({ error: {
5509
- message: "Image edits require application/json or multipart/form-data",
5510
- type: "invalid_request_error"
5511
- } }, 415);
5512
- }
5513
- function getContentMetadata(headers) {
5514
- return {
5515
- contentType: headers.get("content-type"),
5516
- contentLength: headers.get("content-length")
5517
- };
5518
- }
5519
- /**
5520
- * Parses an image request body as JSON, retaining the original bytes for
5521
- * unchanged forwarding while allowing model mapping to rebuild the payload.
5522
- * Rebuilt bodies stay binary so Node does not synthesize a text/plain content type.
5523
- */
5524
- async function parseJsonImagesRequest(request) {
5525
- const parsed = await prepareImageJsonRequest(request);
5526
- return parsed.valid ? parsed : parsed.originalRequest;
5527
- }
5528
- /**
5529
- * Handles Codex images proxying. Pass a resolvedProviderConfig when the
5530
- * caller already resolved the codex provider to avoid a second resolve, and
5531
- * pass a request when model mapping rebuilt the request body.
5532
- */
5533
- async function handleCodexImages(c, operation, resolvedProviderConfig, request = c.req.raw, model) {
5534
- try {
5535
- const codexProviderConfig = resolvedProviderConfig ?? await imageRouteDependencies.resolveProviderConfig("codex");
5536
- if (!codexProviderConfig) {
5537
- await request.body?.cancel().catch(() => {});
5538
- return c.json({ error: {
5539
- message: "Provider 'codex' not found or disabled",
5540
- type: "invalid_request_error"
5541
- } }, 404);
5542
- }
5543
- let actualModel = model;
5544
- if (actualModel === void 0 && !request.headers.get("content-type")?.includes("multipart/form-data")) {
5545
- const parsed = await prepareImageJsonRequest(request);
5546
- actualModel = parsed.valid ? parsed.model ?? DEFAULT_CODEX_IMAGE_MODEL : "unknown";
5547
- request = parsed.originalRequest;
5548
- }
5549
- actualModel ??= "unknown";
5550
- request = await logImageEditRequest(logger$15, `images.${operation}.codex.request`, request, {}, imageRouteDependencies.debugJsonAsync);
5551
- const upstreamResponse = await forwardCodexImages(request, operation);
5552
- debugJson(logger$15, `images.${operation}.codex.response`, {
5553
- ...getContentMetadata(upstreamResponse.headers),
5554
- statusCode: upstreamResponse.status
5555
- });
5556
- return observeImagesResponse(upstreamResponse, {
5557
- model: actualModel,
5558
- operation,
5559
- providerConfig: codexProviderConfig,
5560
- onMetadata: (metadata) => debugJson(logger$15, `images.${operation}.codex.response.body`, metadata)
5561
- });
5562
- } catch (error) {
5563
- await request.body?.cancel().catch(() => {});
5564
- logger$15.error(`images.${operation}.codex.error`, { error });
5565
- return await forwardError(c, error);
5566
- }
5567
- }
5568
5487
  /**
5569
- * Applies model mappings and resolves unknown or absent models to an available
5570
- * provider's image default before dispatching.
5488
+ * Apply model mappings and resolve automatic defaults before dispatching.
5489
+ * Explicit targets retain their model and never fall back to another provider.
5571
5490
  */
5572
- async function routeImagesRequest(c, operation, parsed) {
5573
- let resolvedModel;
5491
+ async function routeImagesRequest(operation, parsed) {
5492
+ let alias;
5574
5493
  try {
5575
- resolvedModel = await resolveImageRequestModel(parsed.model?.trim() ? imageRouteDependencies.resolveMappedModel(parsed.model.trim()) : void 0, {
5494
+ alias = await resolveImageRequestModel(parsed.model?.trim() ? imageRouteDependencies.resolveMappedModel(parsed.model.trim()) : void 0, {
5576
5495
  resolveProviderConfig: imageRouteDependencies.resolveProviderConfig,
5577
5496
  isGitHubCopilotAvailable: imageRouteDependencies.isGitHubCopilotAvailable
5578
5497
  });
@@ -5580,9 +5499,7 @@ async function routeImagesRequest(c, operation, parsed) {
5580
5499
  await parsed.originalRequest?.body?.cancel().catch(() => {});
5581
5500
  throw error;
5582
5501
  }
5583
- const alias = parseProviderModelAlias(resolvedModel);
5584
- if (!alias) throw new Error("Invalid resolved image model");
5585
- consola.debug(`Resolved image model: ${parsed.model ?? "(default)"} -> ${resolvedModel}`);
5502
+ consola.debug(`Resolved image model: ${parsed.model ?? "(default)"} -> ${alias.provider}/${alias.model}`);
5586
5503
  const request = parsed.originalRequest && (alias.provider === "xai" || parsed.model === alias.model) ? parsed.originalRequest : await parsed.createRequest(alias.model, alias.provider);
5587
5504
  if (alias.provider === "github-copilot") return await forwardCopilotImagesWithLogging(request, operation, {
5588
5505
  logger: logger$15,
@@ -5596,101 +5513,33 @@ async function routeImagesRequest(c, operation, parsed) {
5596
5513
  type: "invalid_request_error"
5597
5514
  } }, { status: 404 }));
5598
5515
  }
5599
- if (alias.provider === "codex") return await handleCodexImages(c, operation, providerConfig, request, alias.model);
5516
+ if (alias.provider !== "xai") return await forwardProviderImagesWithLogging(providerConfig, request, operation, {
5517
+ logger: logger$15,
5518
+ ...alias
5519
+ });
5600
5520
  const translated = await translateXaiImageJsonRequest(request, operation, alias.model);
5601
5521
  return await forwardXaiImagesWithLogging(providerConfig, request, operation, translated.payload, {
5602
5522
  logger: logger$15,
5603
5523
  ...alias
5604
5524
  }, translated.body);
5605
5525
  }
5606
- // src/routes/images/xai-handler.ts
5607
- function xaiImagesRequestErrorResponse(c, error) {
5608
- if (!(error instanceof XaiImagesRequestError)) return null;
5609
- return c.json({ error: {
5610
- message: error.message,
5611
- type: "invalid_request_error"
5612
- } }, 400);
5613
- }
5614
- async function forwardTranslatedXaiImages(c, operation, payload, body) {
5615
- const providerConfig = await imageRouteDependencies.resolveProviderConfig("xai");
5616
- if (!providerConfig) {
5617
- await body?.cancel();
5618
- return c.json({ error: {
5619
- message: "Provider 'xai' not found or disabled",
5620
- type: "invalid_request_error"
5621
- } }, 404);
5622
- }
5623
- return await forwardXaiImagesWithLogging(providerConfig, c.req.raw, operation, payload, {
5624
- logger: logger$15,
5625
- provider: "xai"
5626
- }, body);
5627
- }
5628
- async function handleXaiImageGeneration(c, model) {
5629
- const translated = await translateXaiImageJsonRequest(c.req.raw, "generations", model);
5630
- return await forwardTranslatedXaiImages(c, "generations", translated.payload, translated.body);
5631
- }
5632
- async function handleXaiImageEdit(c, model, multipartPayload) {
5633
- if (multipartPayload) return await forwardTranslatedXaiImages(c, "edits", translateXaiImageEditJson(multipartPayload, model));
5634
- const translated = await translateXaiImageJsonRequest(c.req.raw, "edits", model);
5635
- return await forwardTranslatedXaiImages(c, "edits", translated.payload, translated.body);
5636
- }
5637
- // src/routes/images/edits-handler.ts
5638
- async function handleImagesEdits(c) {
5526
+ // src/routes/images/handler.ts
5527
+ async function handleImages(c, operation) {
5639
5528
  try {
5640
- const configuredModel = imageRouteDependencies.getImageEditModel();
5641
- if (configuredModel.status === "invalid") return c.json({ error: {
5642
- message: configuredModel.message,
5643
- type: "invalid_request_error"
5644
- } }, 400);
5645
- const contentTypeError = imageEditContentTypeErrorResponse(c);
5646
- if (contentTypeError) return contentTypeError;
5647
- const multipart = await parseMultipartImageEdit(c.req.raw);
5648
- if (multipart) try {
5649
- if (configuredModel.status === "xai") return await handleXaiImageEdit(c, configuredModel.model, await multipart.readPayload());
5650
- return await routeImagesRequest(c, "edits", {
5651
- model: multipart.model,
5652
- createRequest: (model, provider) => multipart.createRequest(model, provider)
5653
- });
5654
- } catch (error) {
5655
- await multipart.cleanup();
5656
- throw error;
5657
- } finally {
5658
- multipart.scheduleCleanup();
5659
- }
5660
- if (configuredModel.status === "xai") return await handleXaiImageEdit(c, configuredModel.model);
5661
- const parsed = await parseJsonImagesRequest(c.req.raw);
5662
- if (parsed instanceof Request) return await handleCodexImages(c, "edits", void 0, parsed, "unknown");
5663
- return await routeImagesRequest(c, "edits", parsed);
5529
+ return await withImageEditRequest(c, operation, async (multipart) => {
5530
+ return await routeImagesRequest(operation, multipart ?? await prepareImageJsonRequest(c.req.raw));
5531
+ });
5664
5532
  } catch (error) {
5665
5533
  const invalidRequest = imageEditRequestErrorResponse(c, error);
5666
5534
  if (invalidRequest) return invalidRequest;
5667
- logger$15.error("images.edits.error", { error });
5668
- return await forwardError(c, error);
5669
- }
5670
- }
5671
- // src/routes/images/generations-handler.ts
5672
- async function handleImagesGenerations(c) {
5673
- try {
5674
- const configuredModel = imageRouteDependencies.getImageGenerationModel();
5675
- if (configuredModel.status === "invalid") return c.json({ error: {
5676
- message: configuredModel.message,
5677
- type: "invalid_request_error"
5678
- } }, 400);
5679
- if (configuredModel.status === "xai") return await handleXaiImageGeneration(c, configuredModel.model);
5680
- const parsed = await parseJsonImagesRequest(c.req.raw);
5681
- if (parsed instanceof Request) return await handleCodexImages(c, "generations", void 0, parsed, "unknown");
5682
- return await routeImagesRequest(c, "generations", parsed);
5683
- } catch (error) {
5684
- const invalidRequest = xaiImagesRequestErrorResponse(c, error);
5685
- if (invalidRequest) return invalidRequest;
5686
- logger$15.error("images.generations.error", { error });
5535
+ logger$15.error(`images.${operation}.error`, { error });
5687
5536
  return await forwardError(c, error);
5688
5537
  }
5689
5538
  }
5690
5539
  // src/routes/images/route.ts
5691
5540
  const imageRoutes = new Hono();
5692
- imageRoutes.post("/generations", handleImagesGenerations);
5693
- imageRoutes.post("/edits", handleImagesEdits);
5541
+ imageRoutes.post("/generations", (c) => handleImages(c, "generations"));
5542
+ imageRoutes.post("/edits", (c) => handleImages(c, "edits"));
5694
5543
  // src/lib/model-pdf-support.ts
5695
5544
  function resolveModelPdfSupport(provider, options) {
5696
5545
  if (options.supportPdf !== void 0) return options.supportPdf;
@@ -14164,41 +14013,35 @@ const providerImageRoutes = new Hono();
14164
14013
  async function handleProviderImages(c, operation) {
14165
14014
  const provider = c.req.param("provider") ?? "";
14166
14015
  try {
14167
- if (provider === "github-copilot") return await handleCopilotImages(c, operation);
14168
- const providerConfig = await resolveProviderConfig(provider);
14169
- if (!providerConfig) return c.json({ error: {
14016
+ const providerConfig = provider === "github-copilot" ? null : await resolveProviderConfig(provider);
14017
+ if (!providerConfig && provider !== "github-copilot") return c.json({ error: {
14170
14018
  message: `Provider '${provider}' not found or disabled`,
14171
14019
  type: "invalid_request_error"
14172
14020
  } }, 404);
14173
- if (operation === "edits" && (providerConfig.name === "codex" || providerConfig.name === "xai")) {
14174
- const contentTypeError = imageEditContentTypeErrorResponse(c);
14175
- if (contentTypeError) return contentTypeError;
14176
- const multipart = await parseMultipartImageEdit(c.req.raw);
14177
- if (multipart) try {
14178
- const model = await resolveImageRequestModel(multipart.model, { provider: providerConfig.name });
14179
- const request = await multipart.createRequest(model, providerConfig.name);
14180
- if (providerConfig.name === "codex") return await handleCodexImages(c, operation, providerConfig, request, model);
14181
- return await forwardProviderImagesWithLogging(providerConfig, request, operation, {
14182
- logger: logger$6,
14183
- provider,
14184
- model
14185
- });
14186
- } finally {
14187
- await multipart.cleanup();
14188
- }
14189
- }
14190
- if (getDefaultImageModel(providerConfig.name)) {
14191
- const { request, model } = await prepareProviderJsonImages(c.req.raw, providerConfig.name);
14192
- if (providerConfig.name === "codex") return await handleCodexImages(c, operation, providerConfig, request, model);
14193
- return await forwardProviderImagesWithLogging(providerConfig, request, operation, {
14021
+ const forward = async (request, model) => {
14022
+ const context = {
14194
14023
  logger: logger$6,
14195
14024
  provider,
14196
14025
  model
14197
- });
14198
- }
14199
- return await forwardProviderImagesWithLogging(providerConfig, c.req.raw, operation, {
14200
- logger: logger$6,
14201
- provider
14026
+ };
14027
+ if (!providerConfig) return await forwardCopilotImagesWithLogging(request, operation, context);
14028
+ return await forwardProviderImagesWithLogging(providerConfig, request, operation, context);
14029
+ };
14030
+ const target = providerConfig?.name ?? provider;
14031
+ const defaultModel = getDefaultImageModel(target);
14032
+ if (!defaultModel) return await forward(c.req.raw);
14033
+ return await withImageEditRequest(c, operation, async (multipart) => {
14034
+ const parsed = multipart ?? await prepareImageJsonRequest(c.req.raw);
14035
+ try {
14036
+ const requested = parsed.model?.trim();
14037
+ const alias = requested ? parseProviderModelAlias(requested) : null;
14038
+ const model = (alias?.provider === target ? alias.model : requested) || defaultModel;
14039
+ const request = parsed.model === model && parsed.originalRequest ? parsed.originalRequest : await parsed.createRequest(model, target);
14040
+ return await forward(request, model);
14041
+ } catch (error) {
14042
+ await parsed.originalRequest?.body?.cancel().catch(() => {});
14043
+ throw error;
14044
+ }
14202
14045
  });
14203
14046
  } catch (error) {
14204
14047
  const invalidRequest = imageEditRequestErrorResponse(c, error);
@@ -14210,46 +14053,6 @@ async function handleProviderImages(c, operation) {
14210
14053
  return await forwardError(c, error);
14211
14054
  }
14212
14055
  }
14213
- async function handleCopilotImages(c, operation) {
14214
- if (operation === "edits") {
14215
- const contentTypeError = imageEditContentTypeErrorResponse(c);
14216
- if (contentTypeError) return contentTypeError;
14217
- const multipart = await parseMultipartImageEdit(c.req.raw);
14218
- if (multipart) try {
14219
- const model = await resolveImageRequestModel(multipart.model, { provider: GITHUB_COPILOT_PROVIDER });
14220
- return await forwardCopilotImagesWithLogging(await multipart.createRequest(model, GITHUB_COPILOT_PROVIDER), operation, {
14221
- logger: logger$6,
14222
- provider: GITHUB_COPILOT_PROVIDER,
14223
- model
14224
- });
14225
- } finally {
14226
- await multipart.cleanup();
14227
- }
14228
- }
14229
- const { request, model } = await prepareProviderJsonImages(c.req.raw, GITHUB_COPILOT_PROVIDER);
14230
- return await forwardCopilotImagesWithLogging(request, operation, {
14231
- logger: logger$6,
14232
- provider: GITHUB_COPILOT_PROVIDER,
14233
- model
14234
- });
14235
- }
14236
- async function prepareProviderJsonImages(request, provider) {
14237
- const parsed = await parseJsonImagesRequest(request);
14238
- if (parsed instanceof Request) return {
14239
- request: parsed,
14240
- model: "unknown"
14241
- };
14242
- try {
14243
- const model = await resolveImageRequestModel(parsed.model, { provider });
14244
- return {
14245
- model,
14246
- request: parsed.model === model && parsed.originalRequest ? parsed.originalRequest : await parsed.createRequest(model)
14247
- };
14248
- } catch (error) {
14249
- await parsed.originalRequest?.body?.cancel().catch(() => {});
14250
- throw error;
14251
- }
14252
- }
14253
14056
  providerImageRoutes.post("/generations", (c) => handleProviderImages(c, "generations"));
14254
14057
  providerImageRoutes.post("/edits", (c) => handleProviderImages(c, "edits"));
14255
14058
  // src/routes/provider/messages/route.ts
@@ -16270,4 +16073,4 @@ function createServer(options = {}) {
16270
16073
  createServer();
16271
16074
  export { createServer };
16272
16075
 
16273
- //# sourceMappingURL=server-Cw9qn5Gt.js.map
16076
+ //# sourceMappingURL=server-De8OQQe6.js.map