@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.
- package/dist/{auth-b8dJfIDO.js → auth-BTHQn38N.js} +4 -4
- package/dist/{auth-b8dJfIDO.js.map → auth-BTHQn38N.js.map} +1 -1
- package/dist/auth-D7DnNvEO.js +1 -0
- package/dist/{config-B55PwJzQ.js → config-BRBygEXH.js} +2 -2
- package/dist/{config-B55PwJzQ.js.map → config-BRBygEXH.js.map} +1 -1
- package/dist/{config-store-DFcv4AY0.js → config-store-SwGZOmmG.js} +2 -1
- package/dist/config-store-SwGZOmmG.js.map +1 -0
- package/dist/{copilot-runtime-DNKNavLK.js → copilot-runtime-D--d2K7y.js} +3 -3
- package/dist/{copilot-runtime-DNKNavLK.js.map → copilot-runtime-D--d2K7y.js.map} +1 -1
- package/dist/{debug-1bQIZTcG.js → debug-8a37y6YE.js} +3 -3
- package/dist/{debug-1bQIZTcG.js.map → debug-8a37y6YE.js.map} +1 -1
- package/dist/main.js +4 -4
- package/dist/{provider-B8UJ434o.js → provider-CqQ4brtz.js} +2 -2
- package/dist/{provider-B8UJ434o.js.map → provider-CqQ4brtz.js.map} +1 -1
- package/dist/{provider-management-C6ZgkTJ7.js → provider-management-BDXwU7oJ.js} +2 -2
- package/dist/{provider-management-C6ZgkTJ7.js.map → provider-management-BDXwU7oJ.js.map} +1 -1
- package/dist/{server-Cw9qn5Gt.js → server-De8OQQe6.js} +1205 -1402
- package/dist/server-De8OQQe6.js.map +1 -0
- package/dist/{start-CPoEuVzr.js → start-vkJBrxcJ.js} +7 -7
- package/dist/{start-CPoEuVzr.js.map → start-vkJBrxcJ.js.map} +1 -1
- package/dist/{token-DFlvOMh2.js → token-CMboJXYu.js} +3 -3
- package/dist/{token-DFlvOMh2.js.map → token-CMboJXYu.js.map} +1 -1
- package/package.json +1 -1
- package/dist/auth-BFV7bzJo.js +0 -1
- package/dist/config-store-DFcv4AY0.js.map +0 -1
- 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-
|
|
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-
|
|
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-
|
|
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-
|
|
6
|
-
import { i as registerProcessCleanup, n as stopModelsRefreshLoop, r as EventBus, t as setupCopilotRuntime } from "./copilot-runtime-
|
|
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/
|
|
3570
|
-
const
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
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
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3617
|
-
|
|
3618
|
-
|
|
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 (
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
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 (
|
|
3805
|
-
|
|
3806
|
-
|
|
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
|
-
|
|
3811
|
-
const
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
{
|
|
3818
|
-
|
|
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
|
-
|
|
3879
|
-
const
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
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
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
}
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
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
|
|
3919
|
-
|
|
3920
|
-
|
|
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
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
}
|
|
3951
|
-
|
|
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
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
}
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
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
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
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
|
-
|
|
4013
|
-
|
|
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
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
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
|
|
4024
|
-
|
|
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
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
const
|
|
4031
|
-
|
|
4032
|
-
|
|
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
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
|
|
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
|
-
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
|
|
4087
|
-
|
|
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
|
|
4105
|
-
return
|
|
4106
|
-
|
|
4107
|
-
|
|
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
|
|
4111
|
-
|
|
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
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4130
|
-
|
|
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
|
-
*
|
|
4135
|
-
*
|
|
4136
|
-
*
|
|
4137
|
-
*
|
|
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
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
const
|
|
4143
|
-
|
|
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
|
-
|
|
4146
|
-
} catch {
|
|
4147
|
-
|
|
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
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
|
|
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
|
-
|
|
4165
|
-
|
|
4166
|
-
|
|
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
|
-
|
|
4172
|
-
|
|
4173
|
-
|
|
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
|
-
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
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
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
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
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
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
|
|
4207
|
-
if (
|
|
4208
|
-
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
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
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
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
|
|
4236
|
-
if (
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
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
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
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
|
-
|
|
4268
|
-
fields.set("images", images);
|
|
4269
|
-
return Object.fromEntries(fields);
|
|
4384
|
+
return best;
|
|
4270
4385
|
}
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
const
|
|
4278
|
-
const
|
|
4279
|
-
|
|
4280
|
-
const
|
|
4281
|
-
|
|
4282
|
-
|
|
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
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
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
|
-
|
|
4316
|
-
const
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
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
|
-
|
|
4324
|
-
|
|
4325
|
-
|
|
4326
|
-
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
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
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4352
|
-
|
|
4353
|
-
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
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
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
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
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
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
|
-
|
|
4378
|
-
const
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
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
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
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
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
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
|
-
|
|
4591
|
-
if (
|
|
4592
|
-
|
|
4515
|
+
length += value.byteLength;
|
|
4516
|
+
if (length > MAX_RESPONSE_IMAGE_BYTES) return null;
|
|
4517
|
+
chunks.push(value);
|
|
4593
4518
|
}
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
if (tail.byteLength) yield tail;
|
|
4519
|
+
} finally {
|
|
4520
|
+
if (!complete) await reader.cancel().catch(() => {});
|
|
4521
|
+
reader.releaseLock();
|
|
4522
|
+
}
|
|
4599
4523
|
}
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
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
|
-
|
|
4608
|
-
|
|
4609
|
-
duplex: "half"
|
|
4549
|
+
status: response.status,
|
|
4550
|
+
statusText: response.statusText
|
|
4610
4551
|
});
|
|
4611
4552
|
}
|
|
4612
|
-
/**
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
const
|
|
4622
|
-
|
|
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
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
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 (
|
|
4660
|
-
|
|
4661
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
}
|
|
4675
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
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
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
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
|
-
|
|
4728
|
-
model:
|
|
4729
|
-
|
|
4730
|
-
createRequest:
|
|
4731
|
-
|
|
4732
|
-
|
|
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.
|
|
5365
|
+
actualModel = parsed.model;
|
|
5350
5366
|
request = parsed.originalRequest;
|
|
5351
5367
|
}
|
|
5352
5368
|
actualModel ??= "unknown";
|
|
5353
|
-
|
|
5354
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
5460
|
+
/** Only missing models and bare gpt-image-2 select an available default. */
|
|
5472
5461
|
async function resolveImageRequestModel(requestedModel, options = {}) {
|
|
5473
|
-
|
|
5474
|
-
|
|
5475
|
-
|
|
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
|
|
5478
|
-
|
|
5479
|
-
|
|
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
|
-
*
|
|
5570
|
-
*
|
|
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(
|
|
5573
|
-
let
|
|
5491
|
+
async function routeImagesRequest(operation, parsed) {
|
|
5492
|
+
let alias;
|
|
5574
5493
|
try {
|
|
5575
|
-
|
|
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
|
-
|
|
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
|
|
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/
|
|
5607
|
-
function
|
|
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
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
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(
|
|
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",
|
|
5693
|
-
imageRoutes.post("/edits",
|
|
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
|
-
|
|
14168
|
-
|
|
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
|
-
|
|
14174
|
-
const
|
|
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
|
-
|
|
14200
|
-
|
|
14201
|
-
|
|
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-
|
|
16076
|
+
//# sourceMappingURL=server-De8OQQe6.js.map
|