@jeffreycao/copilot-api 2.8.2 → 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-B9pYwLYA.js → auth-BTHQn38N.js} +4 -4
- package/dist/{auth-B9pYwLYA.js.map → auth-BTHQn38N.js.map} +1 -1
- package/dist/auth-D7DnNvEO.js +1 -0
- package/dist/{config-BvACNGIp.js → config-BRBygEXH.js} +44 -3
- package/dist/config-BRBygEXH.js.map +1 -0
- package/dist/{config-store-BLr5xjym.js → config-store-SwGZOmmG.js} +3 -2
- package/dist/config-store-SwGZOmmG.js.map +1 -0
- package/dist/{copilot-runtime-DvmA6ALD.js → copilot-runtime-D--d2K7y.js} +4 -4
- package/dist/{copilot-runtime-DvmA6ALD.js.map → copilot-runtime-D--d2K7y.js.map} +1 -1
- package/dist/{debug-C_uR3s4n.js → debug-8a37y6YE.js} +3 -3
- package/dist/{debug-C_uR3s4n.js.map → debug-8a37y6YE.js.map} +1 -1
- package/dist/main.js +4 -4
- package/dist/{provider-_0MixRky.js → provider-CqQ4brtz.js} +2 -2
- package/dist/{provider-_0MixRky.js.map → provider-CqQ4brtz.js.map} +1 -1
- package/dist/{provider-management-C8AWr-Gk.js → provider-management-BDXwU7oJ.js} +2 -2
- package/dist/{provider-management-C8AWr-Gk.js.map → provider-management-BDXwU7oJ.js.map} +1 -1
- package/dist/{server-C_-cCnrU.js → server-De8OQQe6.js} +1419 -370
- package/dist/server-De8OQQe6.js.map +1 -0
- package/dist/{start-Da9ynTs-.js → start-vkJBrxcJ.js} +7 -7
- package/dist/{start-Da9ynTs-.js.map → start-vkJBrxcJ.js.map} +1 -1
- package/dist/{token-D3nRiw9B.js → token-CMboJXYu.js} +12 -9
- package/dist/token-CMboJXYu.js.map +1 -0
- package/package.json +3 -1
- package/dist/auth-dswtyrvJ.js +0 -1
- package/dist/config-BvACNGIp.js.map +0 -1
- package/dist/config-store-BLr5xjym.js.map +0 -1
- package/dist/server-C_-cCnrU.js.map +0 -1
- package/dist/token-D3nRiw9B.js.map +0 -1
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { D as
|
|
2
|
-
import { A as
|
|
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
|
|
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
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
9
|
-
import fs, { createWriteStream, readFileSync, rmSync } from "node:fs";
|
|
8
|
+
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
9
|
+
import fs, { createReadStream, createWriteStream, readFileSync, rmSync } from "node:fs";
|
|
10
10
|
import path, { join } from "node:path";
|
|
11
|
-
import fs$1, { mkdtemp, rm } from "node:fs/promises";
|
|
11
|
+
import fs$1, { mkdtemp, open, rm } from "node:fs/promises";
|
|
12
12
|
import os, { tmpdir } from "node:os";
|
|
13
13
|
import { fetch as fetch$1, getGlobalDispatcher } from "undici";
|
|
14
14
|
import { events } from "fetch-event-stream";
|
|
@@ -18,6 +18,9 @@ import { cors } from "hono/cors";
|
|
|
18
18
|
import { logger } from "hono/logger";
|
|
19
19
|
import util from "node:util";
|
|
20
20
|
import { streamSSE } from "hono/streaming";
|
|
21
|
+
import { none } from "stream-chain/core";
|
|
22
|
+
import { jsonParser } from "stream-json/core/parser.js";
|
|
23
|
+
import stringer from "stream-json/core/stringer.js";
|
|
21
24
|
import { Busboy } from "@fastify/busboy";
|
|
22
25
|
import { Readable, Transform } from "node:stream";
|
|
23
26
|
import { pipeline } from "node:stream/promises";
|
|
@@ -175,10 +178,15 @@ function initializeTokenUsageDb(db) {
|
|
|
175
178
|
ensureColumn(db, "total_cost_nanos", "INTEGER");
|
|
176
179
|
ensureColumn(db, "cost_source", "TEXT");
|
|
177
180
|
ensureColumn(db, "request_id", "TEXT");
|
|
181
|
+
ensureColumn(db, "upstream_request_id", "TEXT");
|
|
178
182
|
ensureColumn(db, "first_token_ms", "REAL");
|
|
179
183
|
ensureColumn(db, "duration_ms", "REAL");
|
|
180
184
|
ensureColumn(db, "request_outcome", "TEXT");
|
|
181
185
|
db.exec(`
|
|
186
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_token_usage_events_upstream_request
|
|
187
|
+
ON token_usage_events(source, provider_name, endpoint, upstream_request_id)
|
|
188
|
+
`);
|
|
189
|
+
db.exec(`
|
|
182
190
|
CREATE INDEX IF NOT EXISTS idx_token_usage_events_request_id
|
|
183
191
|
ON token_usage_events(request_id)
|
|
184
192
|
`);
|
|
@@ -243,12 +251,14 @@ async function writeTokenUsageEvent(event) {
|
|
|
243
251
|
total_cost_nanos,
|
|
244
252
|
cost_source,
|
|
245
253
|
request_id,
|
|
254
|
+
upstream_request_id,
|
|
246
255
|
first_token_ms,
|
|
247
256
|
duration_ms,
|
|
248
257
|
request_outcome,
|
|
249
258
|
user_agent
|
|
250
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
251
|
-
|
|
259
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
260
|
+
ON CONFLICT(source, provider_name, endpoint, upstream_request_id) DO NOTHING
|
|
261
|
+
`).run(event.created_at_ms, event.created_at_utc, event.trace_id, event.session_id, event.user_id, event.source, event.endpoint, event.provider_name, event.model, event.input_tokens, event.output_tokens, event.cache_read_input_tokens, event.cache_creation_input_tokens, event.total_tokens, event.total_nano_aiu, event.cost_currency, event.total_cost_nanos, event.cost_source, event.request_id, event.upstream_request_id ?? null, event.first_token_ms, event.duration_ms, event.request_outcome, event.user_agent);
|
|
252
262
|
}
|
|
253
263
|
function enqueueTokenUsageWrite(event) {
|
|
254
264
|
if (!isTokenUsageStorageEnabled()) return;
|
|
@@ -906,14 +916,17 @@ async function closeUsageStore() {
|
|
|
906
916
|
registerProcessCleanup(closeUsageStore);
|
|
907
917
|
// src/lib/request-events.ts
|
|
908
918
|
const TRACKED = /\/(chat\/completions|responses|messages|alpha\/search)$/;
|
|
919
|
+
const IMAGE_REQUEST = /\/images\/(generations|edits)$/u;
|
|
909
920
|
function setPluginRequestModel(model) {
|
|
910
921
|
requestContext.getStore()?.pluginRequest?.setModel(model);
|
|
911
922
|
}
|
|
912
923
|
function requestEventsMiddleware(plugins, port) {
|
|
913
924
|
return async (c, next) => {
|
|
914
|
-
|
|
925
|
+
const isImageRequest = IMAGE_REQUEST.test(c.req.path);
|
|
926
|
+
if (c.req.method !== "POST" || !TRACKED.test(c.req.path) && !isImageRequest) return await next();
|
|
915
927
|
const store = requestContext.getStore();
|
|
916
928
|
if (!store) return await next();
|
|
929
|
+
const requestPlugins = isImageRequest ? void 0 : plugins;
|
|
917
930
|
const started = performance.now();
|
|
918
931
|
const info = {
|
|
919
932
|
id: randomUUID(),
|
|
@@ -937,7 +950,7 @@ function requestEventsMiddleware(plugins, port) {
|
|
|
937
950
|
recordFirstToken: () => {
|
|
938
951
|
if (finished || timing.firstTokenMs !== null) return;
|
|
939
952
|
timing.firstTokenMs = performance.now() - started;
|
|
940
|
-
|
|
953
|
+
requestPlugins?.publish("request.first_token", {
|
|
941
954
|
...info,
|
|
942
955
|
firstTokenMs: timing.firstTokenMs
|
|
943
956
|
});
|
|
@@ -951,7 +964,7 @@ function requestEventsMiddleware(plugins, port) {
|
|
|
951
964
|
timing.durationMs = performance.now() - started;
|
|
952
965
|
timing.outcome = outcome;
|
|
953
966
|
if (timing.hasUsage) enqueueTokenUsageTiming(timing);
|
|
954
|
-
|
|
967
|
+
requestPlugins?.publish("request.end", {
|
|
955
968
|
...info,
|
|
956
969
|
durationMs: timing.durationMs,
|
|
957
970
|
firstTokenMs: timing.firstTokenMs,
|
|
@@ -961,33 +974,33 @@ function requestEventsMiddleware(plugins, port) {
|
|
|
961
974
|
signal.removeEventListener("abort", abort);
|
|
962
975
|
};
|
|
963
976
|
const abort = () => finish("cancelled");
|
|
964
|
-
if (
|
|
977
|
+
if (requestPlugins?.enabled) store.pluginRequest = {
|
|
965
978
|
id: info.id,
|
|
966
979
|
setModel: (model) => {
|
|
967
980
|
if (finished || typeof model !== "string" || !model || info.model === model) return;
|
|
968
981
|
info.model = model;
|
|
969
|
-
|
|
982
|
+
requestPlugins.publish("request.model", info);
|
|
970
983
|
},
|
|
971
984
|
setActualModel: (model) => {
|
|
972
985
|
if (finished || typeof model !== "string" || !model || info.actualModel === model) return;
|
|
973
986
|
info.actualModel = model;
|
|
974
|
-
|
|
987
|
+
requestPlugins.publish("request.actual_model", info);
|
|
975
988
|
},
|
|
976
989
|
setSessionId: (sessionId) => {
|
|
977
990
|
if (!sessionId || info.sessionId === sessionId) return;
|
|
978
991
|
info.sessionId = sessionId;
|
|
979
|
-
if (!finished)
|
|
992
|
+
if (!finished) requestPlugins.publish("request.session", info);
|
|
980
993
|
},
|
|
981
994
|
recordFirstToken: timing.recordFirstToken,
|
|
982
|
-
recordUsage: (event) =>
|
|
995
|
+
recordUsage: (event) => requestPlugins.publish("usage.recorded", {
|
|
983
996
|
...event,
|
|
984
997
|
requestId: info.id,
|
|
985
998
|
userAgent: info.userAgent,
|
|
986
999
|
sessionId: event.session_id || null,
|
|
987
|
-
actualModel: event.model
|
|
1000
|
+
actualModel: formatPluginActualModel(event.model, event.provider_name)
|
|
988
1001
|
})
|
|
989
1002
|
};
|
|
990
|
-
|
|
1003
|
+
requestPlugins?.publish("request.start", info);
|
|
991
1004
|
signal.addEventListener("abort", abort, { once: true });
|
|
992
1005
|
if (signal.aborted) abort();
|
|
993
1006
|
try {
|
|
@@ -1389,6 +1402,54 @@ async function ensureConfiguredProviderModelAlias(alias, resolveConfig = resolve
|
|
|
1389
1402
|
async function resolveConfiguredProviderModelAlias(model, resolveConfig = resolveProviderConfig) {
|
|
1390
1403
|
return ensureConfiguredProviderModelAlias(parseProviderModelAlias(model), resolveConfig);
|
|
1391
1404
|
}
|
|
1405
|
+
// src/lib/token-usage/image-pricing.ts
|
|
1406
|
+
const IMAGE_PRICES = {
|
|
1407
|
+
textInput: 5,
|
|
1408
|
+
imageInput: 8,
|
|
1409
|
+
imageOutput: 30
|
|
1410
|
+
};
|
|
1411
|
+
const IMAGE_MODELS = /* @__PURE__ */ new Set([
|
|
1412
|
+
"gpt-image-2",
|
|
1413
|
+
"gpt-image-2.5",
|
|
1414
|
+
"gpt-image-2.5-sunburst",
|
|
1415
|
+
"gpt-image-2.5-flare"
|
|
1416
|
+
]);
|
|
1417
|
+
function splitTokens(total, details, defaultModality) {
|
|
1418
|
+
const tokens = normalizeToken(total);
|
|
1419
|
+
if (details?.image_tokens !== void 0) {
|
|
1420
|
+
const image = Math.min(tokens, normalizeToken(details.image_tokens));
|
|
1421
|
+
return {
|
|
1422
|
+
image,
|
|
1423
|
+
text: tokens - image
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
if (details?.text_tokens !== void 0) {
|
|
1427
|
+
const text = Math.min(tokens, normalizeToken(details.text_tokens));
|
|
1428
|
+
return {
|
|
1429
|
+
image: tokens - text,
|
|
1430
|
+
text
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
return defaultModality === "image" ? {
|
|
1434
|
+
image: tokens,
|
|
1435
|
+
text: 0
|
|
1436
|
+
} : {
|
|
1437
|
+
image: 0,
|
|
1438
|
+
text: tokens
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
function resolveBuiltinImageCost(input) {
|
|
1442
|
+
const model = input.model.trim().toLowerCase().replace(/-\d{4}-\d{2}-\d{2}$/u, "");
|
|
1443
|
+
if (!IMAGE_MODELS.has(model)) return null;
|
|
1444
|
+
const prompt = splitTokens(input.input_tokens, input.input_tokens_details, "text");
|
|
1445
|
+
const output = splitTokens(input.output_tokens, input.output_tokens_details, "image");
|
|
1446
|
+
const totalCostNanos = Math.round((prompt.text * IMAGE_PRICES.textInput + prompt.image * IMAGE_PRICES.imageInput + output.image * IMAGE_PRICES.imageOutput) * 1e3);
|
|
1447
|
+
return totalCostNanos > 0 ? {
|
|
1448
|
+
currency: "USD",
|
|
1449
|
+
source: "builtin",
|
|
1450
|
+
total_cost_nanos: totalCostNanos
|
|
1451
|
+
} : null;
|
|
1452
|
+
}
|
|
1392
1453
|
// src/lib/token-usage/pricing.ts
|
|
1393
1454
|
const COST_NANOS_PER_UNIT = 1e9;
|
|
1394
1455
|
const COST_NANOS_PER_TOKEN_AT_ONE_PER_MILLION = 1e3;
|
|
@@ -1401,13 +1462,14 @@ const BUILTIN_PROVIDER_CURRENCIES = {
|
|
|
1401
1462
|
"opencode-go": "USD"
|
|
1402
1463
|
};
|
|
1403
1464
|
function resolveTokenUsageCost(input) {
|
|
1404
|
-
if (input.source === "provider" && input.providerName?.trim().toLowerCase()
|
|
1465
|
+
if (input.source === "provider" && ["openrouter", "xai"].includes(input.providerName?.trim().toLowerCase() ?? "")) {
|
|
1405
1466
|
const reportedCost = resolveReportedProviderCost(input);
|
|
1406
1467
|
if (reportedCost) return reportedCost;
|
|
1407
1468
|
}
|
|
1408
1469
|
if (input.source === "copilot") return resolveCopilotCost(input);
|
|
1409
1470
|
const providerName = input.providerName?.trim();
|
|
1410
1471
|
if (!providerName) return null;
|
|
1472
|
+
if (providerName.toLowerCase() === "codex" && !input.pricing && (input.endpoint === "images_generations" || input.endpoint === "images_edits")) return resolveBuiltinImageCost(input);
|
|
1411
1473
|
const resolvedPricing = resolveProviderPricing(providerName, input.model, input.pricing);
|
|
1412
1474
|
if (!resolvedPricing) return null;
|
|
1413
1475
|
const pricing = resolvePricingTier(resolvedPricing.pricing, getInputTokenTotal(input), resolvePricingTime(input.at));
|
|
@@ -1446,6 +1508,9 @@ function resolveReportedProviderCost(input) {
|
|
|
1446
1508
|
total_cost_nanos: totalCostNanos
|
|
1447
1509
|
};
|
|
1448
1510
|
}
|
|
1511
|
+
function xaiUsdTicksToCost(value) {
|
|
1512
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value / 1e10 : void 0;
|
|
1513
|
+
}
|
|
1449
1514
|
function resolveCopilotCost(input) {
|
|
1450
1515
|
const totalNanoAiu = normalizeToken(input.total_nano_aiu);
|
|
1451
1516
|
if (totalNanoAiu <= 0) return null;
|
|
@@ -1551,13 +1616,13 @@ function resolveUserId(input) {
|
|
|
1551
1616
|
return state.userName?.trim() || "";
|
|
1552
1617
|
}
|
|
1553
1618
|
function toPersistedEvent(input) {
|
|
1554
|
-
if (!hasAnyToken(input)) return null;
|
|
1555
1619
|
const now = /* @__PURE__ */ new Date();
|
|
1556
|
-
const timing = requestContext.getStore()?.requestTiming;
|
|
1557
1620
|
const cost = resolveTokenUsageCost({
|
|
1558
1621
|
...input,
|
|
1559
1622
|
at: now
|
|
1560
1623
|
});
|
|
1624
|
+
if (!hasAnyToken(input) && !cost) return null;
|
|
1625
|
+
const timing = requestContext.getStore()?.requestTiming;
|
|
1561
1626
|
return {
|
|
1562
1627
|
request_id: timing?.id ?? null,
|
|
1563
1628
|
first_token_ms: timing?.firstTokenMs ?? null,
|
|
@@ -1581,7 +1646,8 @@ function toPersistedEvent(input) {
|
|
|
1581
1646
|
total_tokens: resolveTotalTokens(input),
|
|
1582
1647
|
trace_id: resolveTraceId(input.traceId),
|
|
1583
1648
|
user_agent: (requestContext.getStore()?.userAgent ?? "").slice(0, USER_AGENT_MAX_LENGTH),
|
|
1584
|
-
user_id: resolveUserId(input)
|
|
1649
|
+
user_id: resolveUserId(input),
|
|
1650
|
+
upstream_request_id: input.upstreamRequestId?.trim() || null
|
|
1585
1651
|
};
|
|
1586
1652
|
}
|
|
1587
1653
|
tokenUsageEventBus.subscribe("token_usage.recorded", enqueueTokenUsageWrite);
|
|
@@ -2230,7 +2296,7 @@ async function forwardProviderMessages(providerConfig, payload, requestHeaders,
|
|
|
2230
2296
|
const headers = buildProviderUpstreamHeaders(providerConfig, requestHeaders);
|
|
2231
2297
|
applyOpencodeSessionHeader(providerConfig, headers, resolveOpencodeMessagesSession(payload));
|
|
2232
2298
|
const transportConfig = getUpstreamTransportConfig();
|
|
2233
|
-
setPluginRequestActualModel(payload.model);
|
|
2299
|
+
setPluginRequestActualModel(payload.model, providerConfig.name);
|
|
2234
2300
|
return await fetchUpstreamWithLifecycle(resolveProviderEndpointUrl(providerConfig, "messages"), {
|
|
2235
2301
|
method: "POST",
|
|
2236
2302
|
headers,
|
|
@@ -2246,7 +2312,7 @@ async function forwardProviderChatCompletions(providerConfig, payload, requestHe
|
|
|
2246
2312
|
const headers = buildProviderUpstreamHeaders(providerConfig, requestHeaders);
|
|
2247
2313
|
applyOpencodeSessionHeader(providerConfig, headers, payload.prompt_cache_key?.trim() || void 0);
|
|
2248
2314
|
const transportConfig = getUpstreamTransportConfig();
|
|
2249
|
-
setPluginRequestActualModel(payload.model);
|
|
2315
|
+
setPluginRequestActualModel(payload.model, providerConfig.name);
|
|
2250
2316
|
return await fetchUpstreamWithLifecycle(resolveProviderEndpointUrl(providerConfig, "chat/completions"), {
|
|
2251
2317
|
method: "POST",
|
|
2252
2318
|
headers,
|
|
@@ -2263,7 +2329,7 @@ async function forwardProviderResponses(providerConfig, payload, requestHeaders,
|
|
|
2263
2329
|
const headers = buildProviderUpstreamHeaders(providerConfig, requestHeaders);
|
|
2264
2330
|
applyOpencodeSessionHeader(providerConfig, headers, payload.prompt_cache_key?.trim() || void 0);
|
|
2265
2331
|
await applyXaiGrokBuildHeaders(providerConfig, headers, payload);
|
|
2266
|
-
setPluginRequestActualModel(payload.model);
|
|
2332
|
+
setPluginRequestActualModel(payload.model, providerConfig.name);
|
|
2267
2333
|
return await fetchUpstreamWithLifecycle(resolveProviderEndpointUrl(providerConfig, "responses"), {
|
|
2268
2334
|
method: "POST",
|
|
2269
2335
|
headers,
|
|
@@ -2887,7 +2953,7 @@ function parseDebugBody(body) {
|
|
|
2887
2953
|
}
|
|
2888
2954
|
async function forwardCodexAlphaSearchRequest(request, model) {
|
|
2889
2955
|
await debugJsonAsync(logger$19, "alpha_search.codex.request", async () => ({ body: parseDebugBody(await request.clone().text()) }));
|
|
2890
|
-
if (model) setPluginRequestActualModel(model);
|
|
2956
|
+
if (model) setPluginRequestActualModel(model, "codex");
|
|
2891
2957
|
const upstreamResponse = await alphaSearchRouteDependencies.forwardCodexAlphaSearch(request);
|
|
2892
2958
|
await debugJsonAsync(logger$19, "alpha_search.codex.response", async () => ({
|
|
2893
2959
|
body: parseDebugBody(await upstreamResponse.clone().text()),
|
|
@@ -3500,6 +3566,426 @@ embeddingRoutes.post("/", async (c) => {
|
|
|
3500
3566
|
return await forwardError(c, error);
|
|
3501
3567
|
}
|
|
3502
3568
|
});
|
|
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 }));
|
|
3576
|
+
}
|
|
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
|
|
3588
|
+
});
|
|
3589
|
+
});
|
|
3590
|
+
const cleanup = () => {
|
|
3591
|
+
cleanupPromise ??= (async () => {
|
|
3592
|
+
if (timer) clearTimeout(timer);
|
|
3593
|
+
await rm(directory, {
|
|
3594
|
+
recursive: true,
|
|
3595
|
+
force: true,
|
|
3596
|
+
maxRetries: 3,
|
|
3597
|
+
retryDelay: 20
|
|
3598
|
+
});
|
|
3599
|
+
unregister();
|
|
3600
|
+
})();
|
|
3601
|
+
return cleanupPromise;
|
|
3602
|
+
};
|
|
3603
|
+
return {
|
|
3604
|
+
directory,
|
|
3605
|
+
cleanup,
|
|
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
|
+
};
|
|
3617
|
+
}
|
|
3618
|
+
};
|
|
3619
|
+
}
|
|
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();
|
|
3629
|
+
}
|
|
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;
|
|
3673
|
+
}
|
|
3674
|
+
};
|
|
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();
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
function createJsonByteSource(body, signal) {
|
|
3700
|
+
const controller = new AbortController();
|
|
3701
|
+
const bytes = readJsonBytes(body, AbortSignal.any([signal, controller.signal]));
|
|
3702
|
+
return {
|
|
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(() => {});
|
|
3708
|
+
}
|
|
3709
|
+
};
|
|
3710
|
+
}
|
|
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 });
|
|
3729
|
+
}
|
|
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(""));
|
|
3849
|
+
};
|
|
3850
|
+
for await (const bytes of input) yield process(parser.read(bytes));
|
|
3851
|
+
const tail = process(parser.read());
|
|
3852
|
+
if (tail.byteLength) yield tail;
|
|
3853
|
+
}
|
|
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 }));
|
|
3860
|
+
}
|
|
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
|
+
});
|
|
3871
|
+
}
|
|
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();
|
|
3980
|
+
};
|
|
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
|
|
3987
|
+
};
|
|
3988
|
+
}
|
|
3503
3989
|
// src/routes/images/temp-form-data.ts
|
|
3504
3990
|
const logger$16 = createHandlerLogger("images-temp-form-data");
|
|
3505
3991
|
const multipartStagingDependencies = { createWriteStream };
|
|
@@ -3821,7 +4307,7 @@ var XaiImagesRequestError = class extends Error {
|
|
|
3821
4307
|
this.name = "XaiImagesRequestError";
|
|
3822
4308
|
}
|
|
3823
4309
|
};
|
|
3824
|
-
function isRecord$
|
|
4310
|
+
function isRecord$5(value) {
|
|
3825
4311
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
3826
4312
|
}
|
|
3827
4313
|
function readPrompt(value) {
|
|
@@ -3930,7 +4416,7 @@ function readImageReference(value) {
|
|
|
3930
4416
|
type: "image_url",
|
|
3931
4417
|
url: normalizeImageUrl(value)
|
|
3932
4418
|
};
|
|
3933
|
-
if (!isRecord$
|
|
4419
|
+
if (!isRecord$5(value)) throw new XaiImagesRequestError("image must be a URL or an image object");
|
|
3934
4420
|
if (typeof value.url === "string") return {
|
|
3935
4421
|
type: "image_url",
|
|
3936
4422
|
url: normalizeImageUrl(value.url)
|
|
@@ -3975,7 +4461,7 @@ function buildEditPayload(model, prompt, images, source) {
|
|
|
3975
4461
|
* request. The configured model replaces the client model.
|
|
3976
4462
|
*/
|
|
3977
4463
|
function translateXaiImageGeneration(payload, model) {
|
|
3978
|
-
if (!isRecord$
|
|
4464
|
+
if (!isRecord$5(payload)) throw new XaiImagesRequestError("Image generation body must be a JSON object");
|
|
3979
4465
|
const translated = {
|
|
3980
4466
|
model,
|
|
3981
4467
|
prompt: readPrompt(payload.prompt)
|
|
@@ -3987,7 +4473,7 @@ function translateXaiImageGeneration(payload, model) {
|
|
|
3987
4473
|
* Translates an OpenAI JSON image edit body into an xAI Imagine edit request.
|
|
3988
4474
|
*/
|
|
3989
4475
|
function translateXaiImageEditJson(payload, model) {
|
|
3990
|
-
if (!isRecord$
|
|
4476
|
+
if (!isRecord$5(payload)) throw new XaiImagesRequestError("Image edit body must be a JSON object");
|
|
3991
4477
|
return buildEditPayload(model, readPrompt(payload.prompt), collectJsonImages(payload).map((image) => readImageReference(image)), payload);
|
|
3992
4478
|
}
|
|
3993
4479
|
const MAX_RESPONSE_IMAGE_BYTES = 33554432;
|
|
@@ -4080,15 +4566,15 @@ async function ensureImageResponseB64Json(response, fetchImage = globalThis.fetc
|
|
|
4080
4566
|
} catch {
|
|
4081
4567
|
return rebuildJsonResponse(response, body);
|
|
4082
4568
|
}
|
|
4083
|
-
if (!isRecord$
|
|
4569
|
+
if (!isRecord$5(payload) || !Array.isArray(payload.data)) return rebuildJsonResponse(response, body);
|
|
4084
4570
|
let changed = false;
|
|
4085
4571
|
const data = [];
|
|
4086
4572
|
for (const item of payload.data) {
|
|
4087
|
-
if (isRecord$
|
|
4573
|
+
if (isRecord$5(item) && onImageUrl) {
|
|
4088
4574
|
const outputUrl = imageOutputUrlForLog(item.url);
|
|
4089
4575
|
if (outputUrl) onImageUrl(outputUrl);
|
|
4090
4576
|
}
|
|
4091
|
-
if (!isRecord$
|
|
4577
|
+
if (!isRecord$5(item) || responseFormat === "url" || responseHasB64Json(item.b64_json)) {
|
|
4092
4578
|
data.push(item);
|
|
4093
4579
|
continue;
|
|
4094
4580
|
}
|
|
@@ -4129,6 +4615,24 @@ async function ensureImageResponseB64Json(response, fetchImage = globalThis.fetc
|
|
|
4129
4615
|
// src/routes/images/multipart-edits.ts
|
|
4130
4616
|
const multipartEditDependencies = { stageMultipartBodyToDisk };
|
|
4131
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);
|
|
4629
|
+
} catch (error) {
|
|
4630
|
+
await multipart?.cleanup();
|
|
4631
|
+
throw error;
|
|
4632
|
+
} finally {
|
|
4633
|
+
multipart?.scheduleCleanup();
|
|
4634
|
+
}
|
|
4635
|
+
}
|
|
4132
4636
|
function imageEditRequestErrorResponse(c, error) {
|
|
4133
4637
|
if (c.req.raw.signal.aborted) return null;
|
|
4134
4638
|
if (!(error instanceof ImageEditRequestError) && !(error instanceof InvalidMultipartBodyError) && !(error instanceof MultipartBodyTooLargeError) && !(error instanceof XaiImagesRequestError)) return null;
|
|
@@ -4223,7 +4727,7 @@ async function parseMultipartImageEdit(request) {
|
|
|
4223
4727
|
const headers = new Headers(requestHeaders);
|
|
4224
4728
|
headers.delete("content-length");
|
|
4225
4729
|
let body;
|
|
4226
|
-
if (provider === "codex" || provider === "xai") {
|
|
4730
|
+
if (provider === "codex" || provider === "xai" || provider === "github-copilot") {
|
|
4227
4731
|
const payload = {
|
|
4228
4732
|
...await readPayload(),
|
|
4229
4733
|
...model === void 0 ? {} : { model }
|
|
@@ -4246,85 +4750,538 @@ async function parseMultipartImageEdit(request) {
|
|
|
4246
4750
|
}
|
|
4247
4751
|
};
|
|
4248
4752
|
}
|
|
4249
|
-
// src/lib/
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4753
|
+
// src/lib/image-json-metadata.ts
|
|
4754
|
+
const MAX_METADATA_LENGTH = 65536;
|
|
4755
|
+
const OMITTED_FIELDS = /* @__PURE__ */ new Set([
|
|
4756
|
+
"data",
|
|
4757
|
+
"b64_json",
|
|
4758
|
+
"partial_image_b64"
|
|
4759
|
+
]);
|
|
4760
|
+
/** Reads JSON/SSE object metadata without retaining image payloads. */
|
|
4761
|
+
var ImageJsonMetadataReader = class {
|
|
4762
|
+
depth = 0;
|
|
4763
|
+
inString = false;
|
|
4764
|
+
escaped = false;
|
|
4765
|
+
phase = "key";
|
|
4766
|
+
keyText = "";
|
|
4767
|
+
key = "";
|
|
4768
|
+
value = null;
|
|
4769
|
+
fields = /* @__PURE__ */ new Map();
|
|
4770
|
+
metadataLength = 0;
|
|
4771
|
+
onMetadata;
|
|
4772
|
+
constructor(onMetadata) {
|
|
4773
|
+
this.onMetadata = onMetadata;
|
|
4774
|
+
}
|
|
4775
|
+
write(text) {
|
|
4776
|
+
for (const char of text) this.readCharacter(char);
|
|
4777
|
+
}
|
|
4778
|
+
readCharacter(char) {
|
|
4779
|
+
if (this.depth === 0) {
|
|
4780
|
+
if (char === "{") {
|
|
4781
|
+
this.depth = 1;
|
|
4782
|
+
this.phase = "key";
|
|
4783
|
+
this.fields.clear();
|
|
4784
|
+
this.metadataLength = 0;
|
|
4785
|
+
}
|
|
4786
|
+
return;
|
|
4787
|
+
}
|
|
4788
|
+
if (!this.inString && this.depth === 1) {
|
|
4789
|
+
if (char === "}") {
|
|
4790
|
+
if (this.phase === "value") this.finishField();
|
|
4791
|
+
this.depth = 0;
|
|
4792
|
+
this.onMetadata(Object.fromEntries(this.fields));
|
|
4793
|
+
return;
|
|
4794
|
+
}
|
|
4795
|
+
if (this.phase === "key" && char === "\"") {
|
|
4796
|
+
this.keyText = "\"";
|
|
4797
|
+
this.phase = "colon";
|
|
4798
|
+
this.inString = true;
|
|
4799
|
+
return;
|
|
4800
|
+
}
|
|
4801
|
+
if (this.phase === "colon" && char === ":") {
|
|
4802
|
+
try {
|
|
4803
|
+
this.key = JSON.parse(this.keyText);
|
|
4804
|
+
} catch {
|
|
4805
|
+
this.key = "";
|
|
4806
|
+
}
|
|
4807
|
+
this.value = OMITTED_FIELDS.has(this.key) ? null : "";
|
|
4808
|
+
this.phase = "value";
|
|
4809
|
+
return;
|
|
4810
|
+
}
|
|
4811
|
+
if (this.phase === "value" && char === ",") {
|
|
4812
|
+
this.finishField();
|
|
4813
|
+
this.phase = "key";
|
|
4814
|
+
return;
|
|
4815
|
+
}
|
|
4816
|
+
}
|
|
4817
|
+
if (this.phase === "colon" && this.keyText.length < MAX_METADATA_LENGTH) this.keyText += char;
|
|
4818
|
+
else if (this.phase === "value" && this.value !== null) {
|
|
4819
|
+
if (this.metadataLength + this.value.length < MAX_METADATA_LENGTH) this.value += char;
|
|
4820
|
+
else this.value = null;
|
|
4821
|
+
}
|
|
4822
|
+
if (this.inString) {
|
|
4823
|
+
if (this.escaped) this.escaped = false;
|
|
4824
|
+
else if (char === "\\") this.escaped = true;
|
|
4825
|
+
else if (char === "\"") this.inString = false;
|
|
4826
|
+
} else if (char === "\"") this.inString = true;
|
|
4827
|
+
else if (char === "{" || char === "[") this.depth++;
|
|
4828
|
+
else if (char === "}" || char === "]") this.depth--;
|
|
4829
|
+
}
|
|
4830
|
+
finishField() {
|
|
4831
|
+
if (this.value === null) return;
|
|
4832
|
+
try {
|
|
4833
|
+
const value = JSON.parse(this.value);
|
|
4834
|
+
this.fields.set(this.key, value);
|
|
4835
|
+
this.metadataLength += this.key.length + this.value.length;
|
|
4836
|
+
} catch {}
|
|
4837
|
+
this.value = null;
|
|
4838
|
+
}
|
|
4839
|
+
};
|
|
4840
|
+
// src/lib/token-usage/images.ts
|
|
4841
|
+
function isRecord$4(value) {
|
|
4842
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
4843
|
+
}
|
|
4844
|
+
function token(value) {
|
|
4845
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? normalizeToken(value) : void 0;
|
|
4846
|
+
}
|
|
4847
|
+
function tokenDetails(value) {
|
|
4848
|
+
if (!isRecord$4(value)) return void 0;
|
|
4849
|
+
return {
|
|
4850
|
+
image_tokens: token(value.image_tokens),
|
|
4851
|
+
text_tokens: token(value.text_tokens)
|
|
4266
4852
|
};
|
|
4853
|
+
}
|
|
4854
|
+
function normalizeImagesUsage(usage, provider) {
|
|
4855
|
+
if (!isRecord$4(usage)) return {};
|
|
4856
|
+
const inputDetails = tokenDetails(usage.input_tokens_details);
|
|
4857
|
+
const outputDetails = tokenDetails(usage.output_tokens_details);
|
|
4267
4858
|
return {
|
|
4268
|
-
|
|
4269
|
-
|
|
4859
|
+
cost: provider.toLowerCase() === "xai" ? xaiUsdTicksToCost(usage.cost_in_usd_ticks) : void 0,
|
|
4860
|
+
input_tokens: token(usage.input_tokens) ?? (inputDetails?.image_tokens ?? 0) + (inputDetails?.text_tokens ?? 0),
|
|
4861
|
+
input_tokens_details: inputDetails,
|
|
4862
|
+
output_tokens: token(usage.output_tokens) ?? (outputDetails?.image_tokens ?? 0) + (outputDetails?.text_tokens ?? 0),
|
|
4863
|
+
output_tokens_details: outputDetails,
|
|
4864
|
+
total_tokens: token(usage.total_tokens)
|
|
4865
|
+
};
|
|
4866
|
+
}
|
|
4867
|
+
/** Observes bytes as the client reads them; image data is never buffered here. */
|
|
4868
|
+
function observeImagesResponse(response, options) {
|
|
4869
|
+
if (!response.body) return createProviderProxyResponse(response);
|
|
4870
|
+
const context = requestContext.getStore();
|
|
4871
|
+
const provider = options.providerConfig;
|
|
4872
|
+
const recordUsage = createTokenUsageRecorder({
|
|
4873
|
+
source: provider.name === "github-copilot" ? "copilot" : "provider",
|
|
4874
|
+
endpoint: options.operation === "edits" ? "images_edits" : "images_generations",
|
|
4875
|
+
model: options.model,
|
|
4876
|
+
pricing: provider.models?.[options.model]?.pricing,
|
|
4877
|
+
pricingCurrency: provider.pricingCurrency,
|
|
4878
|
+
providerName: provider.name
|
|
4879
|
+
});
|
|
4880
|
+
let usage = {};
|
|
4881
|
+
let finished = false;
|
|
4882
|
+
const decoder = new TextDecoder();
|
|
4883
|
+
const metadataReader = new ImageJsonMetadataReader((metadata) => {
|
|
4884
|
+
if (context) requestContext.run(context, () => options.onMetadata(metadata));
|
|
4885
|
+
else options.onMetadata(metadata);
|
|
4886
|
+
if (response.ok) {
|
|
4887
|
+
if (isRecord$4(metadata.usage)) usage = {
|
|
4888
|
+
...usage,
|
|
4889
|
+
...normalizeImagesUsage(metadata.usage, provider.name)
|
|
4890
|
+
};
|
|
4891
|
+
if (provider.name === "github-copilot" && isRecord$4(metadata.copilot_usage)) {
|
|
4892
|
+
const totalNanoAiu = token(metadata.copilot_usage.total_nano_aiu);
|
|
4893
|
+
if (totalNanoAiu !== void 0) usage.total_nano_aiu = totalNanoAiu;
|
|
4894
|
+
}
|
|
4895
|
+
}
|
|
4896
|
+
});
|
|
4897
|
+
const finish = () => {
|
|
4898
|
+
if (finished) return;
|
|
4899
|
+
finished = true;
|
|
4900
|
+
if (context) requestContext.run(context, () => recordUsage(usage));
|
|
4901
|
+
else recordUsage(usage);
|
|
4270
4902
|
};
|
|
4903
|
+
const reader = response.body.getReader();
|
|
4904
|
+
return createProviderProxyResponse(response, new ReadableStream({
|
|
4905
|
+
async pull(controller) {
|
|
4906
|
+
try {
|
|
4907
|
+
const next = await reader.read();
|
|
4908
|
+
if (next.done) {
|
|
4909
|
+
metadataReader.write(decoder.decode());
|
|
4910
|
+
finish();
|
|
4911
|
+
reader.releaseLock();
|
|
4912
|
+
controller.close();
|
|
4913
|
+
} else {
|
|
4914
|
+
metadataReader.write(decoder.decode(next.value, { stream: true }));
|
|
4915
|
+
controller.enqueue(next.value);
|
|
4916
|
+
}
|
|
4917
|
+
} catch (error) {
|
|
4918
|
+
finish();
|
|
4919
|
+
reader.releaseLock();
|
|
4920
|
+
controller.error(error);
|
|
4921
|
+
}
|
|
4922
|
+
},
|
|
4923
|
+
async cancel(reason) {
|
|
4924
|
+
finish();
|
|
4925
|
+
await reader.cancel(reason);
|
|
4926
|
+
reader.releaseLock();
|
|
4927
|
+
}
|
|
4928
|
+
}));
|
|
4271
4929
|
}
|
|
4272
|
-
|
|
4273
|
-
|
|
4930
|
+
// src/lib/image-json-value.ts
|
|
4931
|
+
const STRING_LIMIT = 65536;
|
|
4932
|
+
const METADATA_LIMIT = 1048576;
|
|
4933
|
+
/** Builds bounded metadata, optionally storing large decoded strings on disk. */
|
|
4934
|
+
var ImageJsonValueReader = class {
|
|
4935
|
+
value;
|
|
4936
|
+
external = [];
|
|
4937
|
+
stack = [];
|
|
4938
|
+
key = "";
|
|
4939
|
+
inKey = false;
|
|
4940
|
+
number = "";
|
|
4941
|
+
string;
|
|
4942
|
+
metadataSize = 0;
|
|
4943
|
+
files;
|
|
4944
|
+
externalize;
|
|
4945
|
+
constructor(externalize = false) {
|
|
4946
|
+
this.externalize = externalize;
|
|
4947
|
+
}
|
|
4948
|
+
charge(size) {
|
|
4949
|
+
this.metadataSize += size;
|
|
4950
|
+
if (this.metadataSize > METADATA_LIMIT) throw new SyntaxError("Image JSON metadata exceeds 1 MiB");
|
|
4951
|
+
}
|
|
4952
|
+
add(value) {
|
|
4953
|
+
this.charge(16);
|
|
4954
|
+
const frame = this.stack.at(-1);
|
|
4955
|
+
if (!frame) this.value = value;
|
|
4956
|
+
else if (Array.isArray(frame.value)) frame.value.push(value);
|
|
4957
|
+
else frame.value[frame.key] = value;
|
|
4958
|
+
}
|
|
4959
|
+
field() {
|
|
4960
|
+
const frame = this.stack.at(-1);
|
|
4961
|
+
return frame ? Array.isArray(frame.value) ? frame.field : frame.key : "";
|
|
4962
|
+
}
|
|
4963
|
+
async append(text) {
|
|
4964
|
+
const value = this.string;
|
|
4965
|
+
if (!value.external && value.text.length + text.length <= STRING_LIMIT) {
|
|
4966
|
+
value.text += text;
|
|
4967
|
+
return;
|
|
4968
|
+
}
|
|
4969
|
+
if (!this.externalize) {
|
|
4970
|
+
value.text = "[large string omitted]";
|
|
4971
|
+
value.external ??= {
|
|
4972
|
+
placeholder: value.text,
|
|
4973
|
+
path: "",
|
|
4974
|
+
mode: value.mode,
|
|
4975
|
+
first: 0,
|
|
4976
|
+
end: 0,
|
|
4977
|
+
length: 0,
|
|
4978
|
+
prefix: "",
|
|
4979
|
+
invalidBase64: false,
|
|
4980
|
+
padding: 0
|
|
4981
|
+
};
|
|
4982
|
+
return;
|
|
4983
|
+
}
|
|
4984
|
+
if (!value.external) {
|
|
4985
|
+
if (this.external.length >= 64) throw new SyntaxError("Too many large image JSON strings");
|
|
4986
|
+
this.files ??= await createImageJsonFiles();
|
|
4987
|
+
const file = await this.files.create();
|
|
4988
|
+
value.external = {
|
|
4989
|
+
placeholder: randomBytes(24).toString("hex"),
|
|
4990
|
+
path: file.path,
|
|
4991
|
+
mode: value.mode,
|
|
4992
|
+
first: -1,
|
|
4993
|
+
end: 0,
|
|
4994
|
+
length: 0,
|
|
4995
|
+
prefix: "",
|
|
4996
|
+
invalidBase64: false,
|
|
4997
|
+
padding: 0
|
|
4998
|
+
};
|
|
4999
|
+
value.handle = file.handle;
|
|
5000
|
+
this.external.push(value.external);
|
|
5001
|
+
this.track(value.text);
|
|
5002
|
+
value.pending.push(value.text);
|
|
5003
|
+
value.text = "";
|
|
5004
|
+
}
|
|
5005
|
+
this.track(text);
|
|
5006
|
+
value.pending.push(text);
|
|
5007
|
+
}
|
|
5008
|
+
track(text) {
|
|
5009
|
+
const value = this.string.external;
|
|
5010
|
+
const first = text.search(/\S/u);
|
|
5011
|
+
if (first !== -1) {
|
|
5012
|
+
if (value.first === -1) value.first = value.length + first;
|
|
5013
|
+
value.end = value.length + text.trimEnd().length;
|
|
5014
|
+
}
|
|
5015
|
+
value.length += text.length;
|
|
5016
|
+
const normalized = value.mode === "compact" ? text.replaceAll(/\s+/gu, "") : value.prefix ? text : text.trimStart();
|
|
5017
|
+
if (value.prefix.length < 256) value.prefix = (value.prefix + normalized).slice(0, 256);
|
|
5018
|
+
if (value.mode === "compact") {
|
|
5019
|
+
if (value.padding && /[^=]/u.test(normalized)) value.invalidBase64 = true;
|
|
5020
|
+
if (!/^[a-z0-9+/]*={0,2}$/iu.test(normalized)) value.invalidBase64 = true;
|
|
5021
|
+
value.padding += normalized.length - normalized.replaceAll("=", "").length;
|
|
5022
|
+
if (value.padding > 2) value.invalidBase64 = true;
|
|
5023
|
+
}
|
|
5024
|
+
}
|
|
5025
|
+
async flushString() {
|
|
5026
|
+
const value = this.string;
|
|
5027
|
+
if (value?.handle && value.pending.length) {
|
|
5028
|
+
await value.handle.writeFile(Buffer.from(value.pending.join(""), "utf16le"));
|
|
5029
|
+
value.pending = [];
|
|
5030
|
+
}
|
|
5031
|
+
}
|
|
5032
|
+
async read(tokens) {
|
|
5033
|
+
for (const token of tokens) switch (token.name) {
|
|
5034
|
+
case "startObject":
|
|
5035
|
+
case "startArray": {
|
|
5036
|
+
const field = this.field();
|
|
5037
|
+
const value = token.name === "startArray" ? [] : Object.create(null);
|
|
5038
|
+
this.add(value);
|
|
5039
|
+
this.stack.push({
|
|
5040
|
+
value,
|
|
5041
|
+
key: "",
|
|
5042
|
+
field
|
|
5043
|
+
});
|
|
5044
|
+
break;
|
|
5045
|
+
}
|
|
5046
|
+
case "endObject":
|
|
5047
|
+
case "endArray":
|
|
5048
|
+
this.stack.pop();
|
|
5049
|
+
break;
|
|
5050
|
+
case "startKey":
|
|
5051
|
+
this.inKey = true;
|
|
5052
|
+
this.key = "";
|
|
5053
|
+
break;
|
|
5054
|
+
case "endKey":
|
|
5055
|
+
this.inKey = false;
|
|
5056
|
+
this.charge(this.key.length);
|
|
5057
|
+
this.stack.at(-1).key = this.key;
|
|
5058
|
+
break;
|
|
5059
|
+
case "startString": {
|
|
5060
|
+
const field = this.field();
|
|
5061
|
+
this.string = {
|
|
5062
|
+
text: "",
|
|
5063
|
+
pending: [],
|
|
5064
|
+
mode: field === "b64_json" ? "compact" : [
|
|
5065
|
+
"image",
|
|
5066
|
+
"images",
|
|
5067
|
+
"url",
|
|
5068
|
+
"image_url"
|
|
5069
|
+
].includes(field) ? "trim" : "identity"
|
|
5070
|
+
};
|
|
5071
|
+
break;
|
|
5072
|
+
}
|
|
5073
|
+
case "stringChunk":
|
|
5074
|
+
if (this.inKey) this.key += token.value;
|
|
5075
|
+
else await this.append(token.value);
|
|
5076
|
+
break;
|
|
5077
|
+
case "endString": {
|
|
5078
|
+
await this.flushString();
|
|
5079
|
+
const value = this.string;
|
|
5080
|
+
await value.handle?.close();
|
|
5081
|
+
value.handle = void 0;
|
|
5082
|
+
if (value.external && this.externalize) {
|
|
5083
|
+
const external = value.external;
|
|
5084
|
+
external.placeholder = (/^(?:https?:\/\/|data:image\/[a-z0-9.+-]+;base64,)/iu.exec(external.prefix)?.[0] ?? "") + external.placeholder;
|
|
5085
|
+
this.add(external.first === -1 ? "" : external.placeholder);
|
|
5086
|
+
} else {
|
|
5087
|
+
this.charge(value.text.length);
|
|
5088
|
+
this.add(value.text);
|
|
5089
|
+
}
|
|
5090
|
+
this.string = void 0;
|
|
5091
|
+
break;
|
|
5092
|
+
}
|
|
5093
|
+
case "startNumber":
|
|
5094
|
+
this.number = "";
|
|
5095
|
+
break;
|
|
5096
|
+
case "numberChunk":
|
|
5097
|
+
this.number += token.value;
|
|
5098
|
+
break;
|
|
5099
|
+
case "endNumber":
|
|
5100
|
+
this.add(Number(this.number));
|
|
5101
|
+
break;
|
|
5102
|
+
case "trueValue":
|
|
5103
|
+
this.add(true);
|
|
5104
|
+
break;
|
|
5105
|
+
case "falseValue":
|
|
5106
|
+
this.add(false);
|
|
5107
|
+
break;
|
|
5108
|
+
case "nullValue": this.add(null);
|
|
5109
|
+
}
|
|
5110
|
+
await this.flushString();
|
|
5111
|
+
}
|
|
5112
|
+
async cleanup() {
|
|
5113
|
+
await this.string?.handle?.close();
|
|
5114
|
+
if (this.string) this.string.handle = void 0;
|
|
5115
|
+
await this.files?.cleanup();
|
|
5116
|
+
}
|
|
5117
|
+
scheduleCleanup() {
|
|
5118
|
+
this.files?.scheduleCleanup();
|
|
5119
|
+
}
|
|
5120
|
+
};
|
|
5121
|
+
/** Reads xAI input into a small skeleton, never assembling large image strings. */
|
|
5122
|
+
async function readExternalImageJson(request, maxBytes = IMAGE_JSON_MAX_BYTES) {
|
|
5123
|
+
const reader = new ImageJsonValueReader(true);
|
|
5124
|
+
const parser = new StreamingJsonReader();
|
|
5125
|
+
let length = 0;
|
|
5126
|
+
try {
|
|
5127
|
+
for await (const chunk of readJsonBytes(request.body, request.signal)) {
|
|
5128
|
+
length += chunk.byteLength;
|
|
5129
|
+
if (length > maxBytes) throw imageJsonLimitError();
|
|
5130
|
+
await reader.read(parser.read(chunk));
|
|
5131
|
+
}
|
|
5132
|
+
await reader.read(parser.read());
|
|
5133
|
+
reader.scheduleCleanup();
|
|
5134
|
+
return reader;
|
|
5135
|
+
} catch (error) {
|
|
5136
|
+
await reader.cleanup();
|
|
5137
|
+
throw error;
|
|
5138
|
+
}
|
|
4274
5139
|
}
|
|
4275
|
-
function
|
|
4276
|
-
|
|
5140
|
+
function assertExternalImageBase64(reader, payload) {
|
|
5141
|
+
const text = JSON.stringify(payload);
|
|
5142
|
+
for (const value of reader.external) if (text.includes(value.placeholder) && value.mode === "compact" && !value.prefix.startsWith("data:image/") && value.invalidBase64) throw new SyntaxError("image b64_json is not valid base64");
|
|
4277
5143
|
}
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
async function
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
5144
|
+
/** Serializes the translated skeleton, expanding only strings retained by xAI. */
|
|
5145
|
+
function externalImageJsonStream(reader, payload, signal) {
|
|
5146
|
+
const template = JSON.stringify(payload);
|
|
5147
|
+
const pattern = new RegExp(reader.external.map((value) => value.placeholder.replaceAll(/[.*+?^${}()|[\]\\]/gu, "\\$&")).join("|"), "gu");
|
|
5148
|
+
const encode = new TextEncoder();
|
|
5149
|
+
let activeStream;
|
|
5150
|
+
async function* serialize() {
|
|
5151
|
+
try {
|
|
5152
|
+
signal.throwIfAborted();
|
|
5153
|
+
let offset = 0;
|
|
5154
|
+
for (const match of reader.external.length ? template.matchAll(pattern) : []) {
|
|
5155
|
+
signal.throwIfAborted();
|
|
5156
|
+
yield encode.encode(template.slice(offset, match.index));
|
|
5157
|
+
const value = reader.external.find((item) => item.placeholder === match[0]);
|
|
5158
|
+
const trim = value.mode !== "identity";
|
|
5159
|
+
const stream = createReadStream(value.path, {
|
|
5160
|
+
encoding: "utf16le",
|
|
5161
|
+
highWaterMark: 65536,
|
|
5162
|
+
start: trim ? value.first * 2 : 0,
|
|
5163
|
+
end: trim ? value.end * 2 - 1 : void 0
|
|
5164
|
+
});
|
|
5165
|
+
activeStream = stream;
|
|
5166
|
+
try {
|
|
5167
|
+
for await (const chunk of stream) {
|
|
5168
|
+
signal.throwIfAborted();
|
|
5169
|
+
const text = String(chunk);
|
|
5170
|
+
yield encode.encode(JSON.stringify(value.mode === "compact" ? text.replaceAll(/\s+/gu, "") : text).slice(1, -1));
|
|
5171
|
+
}
|
|
5172
|
+
} finally {
|
|
5173
|
+
stream.destroy();
|
|
5174
|
+
activeStream = void 0;
|
|
5175
|
+
}
|
|
5176
|
+
offset = match.index + match[0].length;
|
|
5177
|
+
}
|
|
5178
|
+
signal.throwIfAborted();
|
|
5179
|
+
yield encode.encode(template.slice(offset));
|
|
5180
|
+
} catch (error) {
|
|
5181
|
+
signal.throwIfAborted();
|
|
5182
|
+
throw error;
|
|
5183
|
+
} finally {
|
|
5184
|
+
await reader.cleanup();
|
|
5185
|
+
}
|
|
5186
|
+
}
|
|
5187
|
+
const abort = () => {
|
|
5188
|
+
activeStream?.destroy();
|
|
5189
|
+
reader.cleanup().catch(() => {});
|
|
5190
|
+
};
|
|
5191
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
5192
|
+
if (signal.aborted) abort();
|
|
5193
|
+
async function* body() {
|
|
5194
|
+
try {
|
|
5195
|
+
yield* serialize();
|
|
5196
|
+
} finally {
|
|
5197
|
+
signal.removeEventListener("abort", abort);
|
|
5198
|
+
}
|
|
5199
|
+
}
|
|
5200
|
+
return jsonByteStream(body(), async () => {
|
|
5201
|
+
signal.removeEventListener("abort", abort);
|
|
5202
|
+
activeStream?.destroy();
|
|
5203
|
+
await reader.cleanup();
|
|
4300
5204
|
});
|
|
4301
|
-
return createProviderProxyResponse(upstreamResponse);
|
|
4302
5205
|
}
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4322
|
-
|
|
4323
|
-
|
|
4324
|
-
|
|
5206
|
+
// src/routes/images/request-logging.ts
|
|
5207
|
+
function redactImageBase64(value, key = "") {
|
|
5208
|
+
if ([
|
|
5209
|
+
"b64_json",
|
|
5210
|
+
"base64",
|
|
5211
|
+
"image_base64"
|
|
5212
|
+
].includes(key)) return "[image base64 omitted]";
|
|
5213
|
+
if (typeof value === "string") return /^data:image\/[^,]+;base64,/iu.test(value) ? "[image base64 omitted]" : value;
|
|
5214
|
+
if (Array.isArray(value)) return value.map((item) => redactImageBase64(item));
|
|
5215
|
+
if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([field, item]) => [field, redactImageBase64(item, field)]));
|
|
5216
|
+
return value;
|
|
5217
|
+
}
|
|
5218
|
+
async function editRequestBody(request) {
|
|
5219
|
+
try {
|
|
5220
|
+
const cloned = request.clone();
|
|
5221
|
+
if (request.headers.get("content-type")?.includes("multipart/form-data")) {
|
|
5222
|
+
const fields = [];
|
|
5223
|
+
for (const [name, value] of await cloned.formData()) fields.push({
|
|
5224
|
+
name,
|
|
5225
|
+
value: typeof value === "string" ? redactImageBase64(value, name) : {
|
|
5226
|
+
filename: value.name,
|
|
5227
|
+
mime_type: value.type,
|
|
5228
|
+
size: value.size
|
|
5229
|
+
}
|
|
5230
|
+
});
|
|
5231
|
+
return fields;
|
|
5232
|
+
}
|
|
5233
|
+
return redactImageBase64(await cloned.json());
|
|
5234
|
+
} catch {
|
|
5235
|
+
return "[unavailable JSON body]";
|
|
5236
|
+
}
|
|
5237
|
+
}
|
|
5238
|
+
async function logImageEditRequest(logger, label, request, extra = {}, debug = debugJsonAsync) {
|
|
5239
|
+
if (!state.verbose) return request;
|
|
5240
|
+
if (!request.headers.get("content-type")?.includes("multipart/form-data")) return logImageJsonRequest(logger, label, request, extra, debug);
|
|
5241
|
+
await debug(logger, label, async () => ({
|
|
5242
|
+
contentType: request.headers.get("content-type"),
|
|
5243
|
+
contentLength: request.headers.get("content-length"),
|
|
5244
|
+
...extra,
|
|
5245
|
+
body: await editRequestBody(request)
|
|
5246
|
+
}));
|
|
5247
|
+
return request;
|
|
5248
|
+
}
|
|
5249
|
+
/** Observes the same chunks sent upstream; debug logging never tees JSON bodies. */
|
|
5250
|
+
function logImageJsonRequest(logger, label, request, extra, debug) {
|
|
5251
|
+
const value = new ImageJsonValueReader();
|
|
5252
|
+
const parser = new StreamingJsonReader();
|
|
5253
|
+
const input = createJsonByteSource(request.body, request.signal);
|
|
5254
|
+
let valid = true;
|
|
5255
|
+
async function* observe() {
|
|
5256
|
+
for await (const chunk of input.bytes) {
|
|
5257
|
+
if (valid) try {
|
|
5258
|
+
await value.read(parser.read(chunk));
|
|
5259
|
+
} catch {
|
|
5260
|
+
valid = false;
|
|
5261
|
+
}
|
|
5262
|
+
yield chunk;
|
|
5263
|
+
}
|
|
5264
|
+
if (valid) try {
|
|
5265
|
+
await value.read(parser.read());
|
|
5266
|
+
} catch {
|
|
5267
|
+
valid = false;
|
|
5268
|
+
}
|
|
5269
|
+
await debug(logger, label, () => Promise.resolve({
|
|
5270
|
+
contentType: request.headers.get("content-type"),
|
|
5271
|
+
contentLength: request.headers.get("content-length"),
|
|
5272
|
+
...extra,
|
|
5273
|
+
body: valid ? redactImageBase64(value.value) : "[unavailable JSON body]"
|
|
5274
|
+
}));
|
|
5275
|
+
}
|
|
5276
|
+
return new Request(request.url, {
|
|
5277
|
+
method: request.method,
|
|
5278
|
+
headers: request.headers,
|
|
5279
|
+
signal: request.signal,
|
|
5280
|
+
body: jsonByteStream(observe(), (reason) => input.cancel(reason)),
|
|
5281
|
+
duplex: "half"
|
|
4325
5282
|
});
|
|
4326
5283
|
}
|
|
4327
|
-
|
|
5284
|
+
DEFAULT_IMAGE_MODELS.codex;
|
|
4328
5285
|
const CODEX_IMAGES_TIMEOUT_MS = 9e5;
|
|
4329
5286
|
const codexImagesDispatcher = createTimeoutDispatcher(CODEX_IMAGES_TIMEOUT_MS);
|
|
4330
5287
|
function resolveCodexImagesUrl(requestUrl, operation) {
|
|
@@ -4350,229 +5307,239 @@ async function forwardCodexImages(request, operation) {
|
|
|
4350
5307
|
dispatcher: codexImagesDispatcher
|
|
4351
5308
|
});
|
|
4352
5309
|
}
|
|
4353
|
-
|
|
4354
|
-
const
|
|
4355
|
-
const
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
4362
|
-
function imageEditContentTypeErrorResponse(c) {
|
|
4363
|
-
const mediaType = c.req.header("content-type")?.split(";", 1)[0]?.trim().toLowerCase();
|
|
4364
|
-
if (mediaType === "application/json" || mediaType === "multipart/form-data") return null;
|
|
4365
|
-
return c.json({ error: {
|
|
4366
|
-
message: "Image edits require application/json or multipart/form-data",
|
|
4367
|
-
type: "invalid_request_error"
|
|
4368
|
-
} }, 415);
|
|
5310
|
+
DEFAULT_IMAGE_MODELS["github-copilot"];
|
|
5311
|
+
const COPILOT_CLIENT_VERSION = "copilot/1.0.103-unstable.r40031404408.gba6c003";
|
|
5312
|
+
const COPILOT_IMAGES_TIMEOUT_MS = 9e5;
|
|
5313
|
+
const copilotImagesDispatcher = createTimeoutDispatcher(COPILOT_IMAGES_TIMEOUT_MS);
|
|
5314
|
+
function resolveCopilotImagesUrl(requestUrl, operation) {
|
|
5315
|
+
const upstreamUrl = new URL(`${copilotBaseUrl(state).replace(/\/+$/u, "")}/v1/images/${operation}`);
|
|
5316
|
+
if (upstreamUrl.hostname === "api.githubcopilot.com") upstreamUrl.hostname = `api.${state.accountType}.githubcopilot.com`;
|
|
5317
|
+
upstreamUrl.search = new URL(requestUrl).search;
|
|
5318
|
+
return upstreamUrl.toString();
|
|
4369
5319
|
}
|
|
4370
|
-
function
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
5320
|
+
async function forwardCopilotImages(request, operation) {
|
|
5321
|
+
assertGitHubCopilotEnabled();
|
|
5322
|
+
if (!state.githubToken) throw new HTTPError("GitHub token not found", Response.json({ error: {
|
|
5323
|
+
message: "GitHub token not found",
|
|
5324
|
+
type: "authentication_error"
|
|
5325
|
+
} }, { status: 401 }));
|
|
5326
|
+
const init = {
|
|
5327
|
+
method: "POST",
|
|
5328
|
+
headers: {
|
|
5329
|
+
authorization: `Bearer ${state.githubToken}`,
|
|
5330
|
+
"content-type": "application/json",
|
|
5331
|
+
accept: request.headers.get("accept") ?? "application/json",
|
|
5332
|
+
"copilot-integration-id": "vscode-chat",
|
|
5333
|
+
"copilot-harness-id": "copilot-sdk",
|
|
5334
|
+
"editor-version": COPILOT_CLIENT_VERSION,
|
|
5335
|
+
"user-agent": `${COPILOT_CLIENT_VERSION} (${process.platform} unknown) term/unknown client/vscode-agent-host`,
|
|
5336
|
+
"openai-intent": "conversation-agent",
|
|
5337
|
+
"x-initiator": "agent",
|
|
5338
|
+
"x-github-api-version": "2026-08-01",
|
|
5339
|
+
"x-client-machine-id": state.vsCodeDeviceId,
|
|
5340
|
+
"x-interaction-id": randomUUID()
|
|
5341
|
+
},
|
|
5342
|
+
body: request.body,
|
|
5343
|
+
duplex: "half",
|
|
5344
|
+
signal: AbortSignal.any([request.signal, AbortSignal.timeout(COPILOT_IMAGES_TIMEOUT_MS)])
|
|
4374
5345
|
};
|
|
5346
|
+
const upstreamUrl = resolveCopilotImagesUrl(request.url, operation);
|
|
5347
|
+
const response = typeof Bun !== "undefined" ? await fetch(upstreamUrl, init) : await fetch$1(upstreamUrl, {
|
|
5348
|
+
...init,
|
|
5349
|
+
dispatcher: copilotImagesDispatcher
|
|
5350
|
+
});
|
|
5351
|
+
logCopilotRateLimits(response.headers);
|
|
5352
|
+
return response;
|
|
4375
5353
|
}
|
|
5354
|
+
// src/routes/images/forward-provider-images.ts
|
|
5355
|
+
const imageForwardDependencies = { debugJsonAsync };
|
|
4376
5356
|
/**
|
|
4377
|
-
*
|
|
5357
|
+
* Forwards an images request to a configured provider with debug logging.
|
|
5358
|
+
* Shared by the top-level images routes (model mapping) and the
|
|
5359
|
+
* provider-scoped images routes so both log and proxy identically.
|
|
4378
5360
|
*/
|
|
4379
|
-
function
|
|
4380
|
-
|
|
5361
|
+
async function forwardProviderImagesWithLogging(providerConfig, request, operation, { logger, provider, model }) {
|
|
5362
|
+
let actualModel = model;
|
|
5363
|
+
if (actualModel === void 0 && !request.headers.get("content-type")?.includes("multipart/form-data")) {
|
|
5364
|
+
const parsed = await prepareImageJsonRequest(request);
|
|
5365
|
+
actualModel = parsed.model;
|
|
5366
|
+
request = parsed.originalRequest;
|
|
5367
|
+
}
|
|
5368
|
+
actualModel ??= "unknown";
|
|
5369
|
+
const isCodex = providerConfig.name === "codex";
|
|
5370
|
+
const label = isCodex ? `images.${operation}.codex` : `provider.images.${operation}`;
|
|
5371
|
+
const extra = isCodex ? {} : { provider };
|
|
5372
|
+
try {
|
|
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
|
+
});
|
|
5390
|
+
} catch (error) {
|
|
5391
|
+
await request.body?.cancel().catch(() => {});
|
|
5392
|
+
throw error;
|
|
5393
|
+
}
|
|
4381
5394
|
}
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
5395
|
+
async function forwardCopilotImagesWithLogging(request, operation, context) {
|
|
5396
|
+
const config = getRawProviderConfig(GITHUB_COPILOT_PROVIDER);
|
|
5397
|
+
return await forwardProviderImagesWithLogging({
|
|
5398
|
+
name: GITHUB_COPILOT_PROVIDER,
|
|
5399
|
+
type: "openai-compatible",
|
|
5400
|
+
authType: "oauth2",
|
|
5401
|
+
apiKey: "",
|
|
5402
|
+
baseUrl: "",
|
|
5403
|
+
models: config?.models,
|
|
5404
|
+
pricingCurrency: config?.pricingCurrency
|
|
5405
|
+
}, request, operation, context);
|
|
5406
|
+
}
|
|
5407
|
+
/** Forwards translated xAI requests and normalizes OpenAI-compatible output. */
|
|
5408
|
+
async function forwardXaiImagesWithLogging(providerConfig, request, operation, payload, { logger, provider }, body) {
|
|
5409
|
+
return await ensureImageResponseB64Json(await forwardProviderImagesWithLogging(providerConfig, new Request(request.url, {
|
|
4390
5410
|
body,
|
|
4391
5411
|
duplex: "half",
|
|
4392
|
-
headers:
|
|
4393
|
-
|
|
5412
|
+
headers: {
|
|
5413
|
+
accept: "application/json",
|
|
5414
|
+
"content-type": "application/json"
|
|
5415
|
+
},
|
|
5416
|
+
method: "POST",
|
|
4394
5417
|
signal: request.signal
|
|
4395
|
-
}
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
5418
|
+
}), operation, {
|
|
5419
|
+
logger,
|
|
5420
|
+
provider,
|
|
5421
|
+
model: typeof payload.model === "string" ? payload.model : "unknown"
|
|
5422
|
+
}), void 0, {
|
|
5423
|
+
clientSignal: request.signal,
|
|
5424
|
+
onImageUrl: (url) => {
|
|
5425
|
+
logger.info("images.xai.output.url", JSON.stringify({
|
|
5426
|
+
operation,
|
|
5427
|
+
url
|
|
5428
|
+
}));
|
|
5429
|
+
},
|
|
5430
|
+
responseFormat: payload.response_format === "url" ? "url" : "b64_json"
|
|
5431
|
+
});
|
|
4405
5432
|
}
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
*/
|
|
4411
|
-
async function parseJsonImagesRequest(request) {
|
|
4412
|
-
const requestHeaders = snapshotRequestHeaders(request);
|
|
4413
|
-
const body = new Uint8Array(await request.arrayBuffer());
|
|
4414
|
-
const originalRequest = createForwardRequest(request, requestHeaders, body);
|
|
4415
|
-
let payload;
|
|
5433
|
+
// src/routes/images/xai-json-request.ts
|
|
5434
|
+
/** Existing translators validate a bounded skeleton; large strings stay on disk. */
|
|
5435
|
+
async function translateXaiImageJsonRequest(request, operation, model) {
|
|
5436
|
+
let json;
|
|
4416
5437
|
try {
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
5438
|
+
json = await readExternalImageJson(request);
|
|
5439
|
+
} catch (error) {
|
|
5440
|
+
if (error instanceof HTTPError || request.signal.aborted) throw error;
|
|
5441
|
+
throw new XaiImagesRequestError(operation === "edits" ? "Image edit body must be JSON" : "Image generation body must be JSON");
|
|
4421
5442
|
}
|
|
4422
|
-
if (payload === null || typeof payload !== "object" || Array.isArray(payload) || typeof payload.model !== "string") return originalRequest;
|
|
4423
|
-
return {
|
|
4424
|
-
createRequest: (mappedModel) => createJsonImagesRequest(request, requestHeaders, payload, mappedModel),
|
|
4425
|
-
model: payload.model,
|
|
4426
|
-
originalRequest
|
|
4427
|
-
};
|
|
4428
|
-
}
|
|
4429
|
-
/**
|
|
4430
|
-
* Handles Codex images proxying. Pass a resolvedProviderConfig when the
|
|
4431
|
-
* caller already resolved the codex provider to avoid a second resolve, and
|
|
4432
|
-
* pass a request when model mapping rebuilt the request body.
|
|
4433
|
-
*/
|
|
4434
|
-
async function handleCodexImages(c, operation, resolvedProviderConfig, request = c.req.raw) {
|
|
4435
5443
|
try {
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
|
|
4439
|
-
}
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
});
|
|
4447
|
-
return createProviderProxyResponse(upstreamResponse);
|
|
5444
|
+
const payload = operation === "edits" ? translateXaiImageEditJson(json.value, model) : translateXaiImageGeneration(json.value, model);
|
|
5445
|
+
try {
|
|
5446
|
+
assertExternalImageBase64(json, payload);
|
|
5447
|
+
} catch {
|
|
5448
|
+
throw new XaiImagesRequestError("image b64_json is not valid base64");
|
|
5449
|
+
}
|
|
5450
|
+
return {
|
|
5451
|
+
payload,
|
|
5452
|
+
body: externalImageJsonStream(json, payload, request.signal)
|
|
5453
|
+
};
|
|
4448
5454
|
} catch (error) {
|
|
4449
|
-
|
|
4450
|
-
|
|
5455
|
+
await json.cleanup();
|
|
5456
|
+
throw error;
|
|
4451
5457
|
}
|
|
4452
5458
|
}
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
if (providerConfig) {
|
|
4465
|
-
const request = await parsed.createRequest(providerModelAlias.model, providerConfig.name);
|
|
4466
|
-
if (providerConfig.name === "codex") return await handleCodexImages(c, operation, providerConfig, request);
|
|
4467
|
-
if (providerConfig.name === "xai") {
|
|
4468
|
-
const payload = await request.json();
|
|
4469
|
-
return await forwardXaiImagesWithLogging(providerConfig, request, operation, operation === "edits" ? translateXaiImageEditJson(payload, providerModelAlias.model) : translateXaiImageGeneration(payload, providerModelAlias.model), {
|
|
4470
|
-
logger: logger$15,
|
|
4471
|
-
provider: providerModelAlias.provider
|
|
4472
|
-
});
|
|
4473
|
-
}
|
|
4474
|
-
return await forwardProviderImagesWithLogging(providerConfig, request, operation, {
|
|
4475
|
-
logger: logger$15,
|
|
4476
|
-
provider: providerModelAlias.provider
|
|
4477
|
-
});
|
|
4478
|
-
}
|
|
4479
|
-
consola.debug(`Provider '${providerModelAlias.provider}' not found or disabled; forwarding the original model to Codex`);
|
|
4480
|
-
return await handleCodexImages(c, operation, void 0, parsed.originalRequest ?? await parsed.createRequest(requestedModel));
|
|
5459
|
+
// src/routes/images/default-model.ts
|
|
5460
|
+
/** Only missing models and bare gpt-image-2 select an available default. */
|
|
5461
|
+
async function resolveImageRequestModel(requestedModel, options = {}) {
|
|
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
|
+
};
|
|
4481
5470
|
}
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
return c.json({ error: {
|
|
4489
|
-
message: error.message,
|
|
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]
|
|
5474
|
+
};
|
|
5475
|
+
throw new HTTPError("No enabled image provider is available", Response.json({ error: {
|
|
5476
|
+
message: "No enabled image provider is available",
|
|
4490
5477
|
type: "invalid_request_error"
|
|
4491
|
-
} },
|
|
5478
|
+
} }, { status: 404 }));
|
|
4492
5479
|
}
|
|
4493
|
-
|
|
5480
|
+
// src/routes/images/shared.ts
|
|
5481
|
+
const logger$15 = createHandlerLogger("images-handler");
|
|
5482
|
+
const imageRouteDependencies = {
|
|
5483
|
+
isGitHubCopilotAvailable,
|
|
5484
|
+
resolveMappedModel,
|
|
5485
|
+
resolveProviderConfig
|
|
5486
|
+
};
|
|
5487
|
+
/**
|
|
5488
|
+
* Apply model mappings and resolve automatic defaults before dispatching.
|
|
5489
|
+
* Explicit targets retain their model and never fall back to another provider.
|
|
5490
|
+
*/
|
|
5491
|
+
async function routeImagesRequest(operation, parsed) {
|
|
5492
|
+
let alias;
|
|
4494
5493
|
try {
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
5494
|
+
alias = await resolveImageRequestModel(parsed.model?.trim() ? imageRouteDependencies.resolveMappedModel(parsed.model.trim()) : void 0, {
|
|
5495
|
+
resolveProviderConfig: imageRouteDependencies.resolveProviderConfig,
|
|
5496
|
+
isGitHubCopilotAvailable: imageRouteDependencies.isGitHubCopilotAvailable
|
|
5497
|
+
});
|
|
5498
|
+
} catch (error) {
|
|
5499
|
+
await parsed.originalRequest?.body?.cancel().catch(() => {});
|
|
5500
|
+
throw error;
|
|
4499
5501
|
}
|
|
4500
|
-
}
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
if (!providerConfig) return c.json({ error: {
|
|
4504
|
-
message: "Provider 'xai' not found or disabled",
|
|
4505
|
-
type: "invalid_request_error"
|
|
4506
|
-
} }, 404);
|
|
4507
|
-
return await forwardXaiImagesWithLogging(providerConfig, c.req.raw, operation, payload, {
|
|
5502
|
+
consola.debug(`Resolved image model: ${parsed.model ?? "(default)"} -> ${alias.provider}/${alias.model}`);
|
|
5503
|
+
const request = parsed.originalRequest && (alias.provider === "xai" || parsed.model === alias.model) ? parsed.originalRequest : await parsed.createRequest(alias.model, alias.provider);
|
|
5504
|
+
if (alias.provider === "github-copilot") return await forwardCopilotImagesWithLogging(request, operation, {
|
|
4508
5505
|
logger: logger$15,
|
|
4509
|
-
|
|
5506
|
+
...alias
|
|
4510
5507
|
});
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
}
|
|
4515
|
-
|
|
4516
|
-
return await forwardTranslatedXaiImages(c, "edits", translateXaiImageEditJson(multipartPayload ?? await readJsonBody(c.req.raw, "Image edit body must be JSON"), model));
|
|
4517
|
-
}
|
|
4518
|
-
// src/routes/images/edits-handler.ts
|
|
4519
|
-
async function handleImagesEdits(c) {
|
|
4520
|
-
try {
|
|
4521
|
-
const configuredModel = imageRouteDependencies.getImageEditModel();
|
|
4522
|
-
if (configuredModel.status === "invalid") return c.json({ error: {
|
|
4523
|
-
message: configuredModel.message,
|
|
5508
|
+
const providerConfig = await imageRouteDependencies.resolveProviderConfig(alias.provider);
|
|
5509
|
+
if (!providerConfig) {
|
|
5510
|
+
await request.body?.cancel().catch(() => {});
|
|
5511
|
+
throw new HTTPError(`Provider '${alias.provider}' not found or disabled`, Response.json({ error: {
|
|
5512
|
+
message: `Provider '${alias.provider}' not found or disabled`,
|
|
4524
5513
|
type: "invalid_request_error"
|
|
4525
|
-
} },
|
|
4526
|
-
const contentTypeError = imageEditContentTypeErrorResponse(c);
|
|
4527
|
-
if (contentTypeError) return contentTypeError;
|
|
4528
|
-
const multipart = await parseMultipartImageEdit(c.req.raw);
|
|
4529
|
-
if (multipart) try {
|
|
4530
|
-
if (configuredModel.status === "xai") return await handleXaiImageEdit(c, configuredModel.model, await multipart.readPayload());
|
|
4531
|
-
if (multipart.model === void 0) return await handleCodexImages(c, "edits", void 0, await multipart.createRequest(void 0));
|
|
4532
|
-
return await routeImagesRequest(c, "edits", {
|
|
4533
|
-
model: multipart.model,
|
|
4534
|
-
createRequest: (model, provider) => multipart.createRequest(model, provider)
|
|
4535
|
-
});
|
|
4536
|
-
} catch (error) {
|
|
4537
|
-
await multipart.cleanup();
|
|
4538
|
-
throw error;
|
|
4539
|
-
} finally {
|
|
4540
|
-
multipart.scheduleCleanup();
|
|
4541
|
-
}
|
|
4542
|
-
if (configuredModel.status === "xai") return await handleXaiImageEdit(c, configuredModel.model);
|
|
4543
|
-
const parsed = await parseJsonImagesRequest(c.req.raw);
|
|
4544
|
-
if (parsed instanceof Request) return await handleCodexImages(c, "edits", void 0, parsed);
|
|
4545
|
-
return await routeImagesRequest(c, "edits", parsed);
|
|
4546
|
-
} catch (error) {
|
|
4547
|
-
const invalidRequest = imageEditRequestErrorResponse(c, error);
|
|
4548
|
-
if (invalidRequest) return invalidRequest;
|
|
4549
|
-
logger$15.error("images.edits.error", { error });
|
|
4550
|
-
return await forwardError(c, error);
|
|
5514
|
+
} }, { status: 404 }));
|
|
4551
5515
|
}
|
|
5516
|
+
if (alias.provider !== "xai") return await forwardProviderImagesWithLogging(providerConfig, request, operation, {
|
|
5517
|
+
logger: logger$15,
|
|
5518
|
+
...alias
|
|
5519
|
+
});
|
|
5520
|
+
const translated = await translateXaiImageJsonRequest(request, operation, alias.model);
|
|
5521
|
+
return await forwardXaiImagesWithLogging(providerConfig, request, operation, translated.payload, {
|
|
5522
|
+
logger: logger$15,
|
|
5523
|
+
...alias
|
|
5524
|
+
}, translated.body);
|
|
4552
5525
|
}
|
|
4553
|
-
// src/routes/images/
|
|
4554
|
-
async function
|
|
5526
|
+
// src/routes/images/handler.ts
|
|
5527
|
+
async function handleImages(c, operation) {
|
|
4555
5528
|
try {
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
type: "invalid_request_error"
|
|
4560
|
-
} }, 400);
|
|
4561
|
-
if (configuredModel.status === "xai") return await handleXaiImageGeneration(c, configuredModel.model);
|
|
4562
|
-
const parsed = await parseJsonImagesRequest(c.req.raw);
|
|
4563
|
-
if (parsed instanceof Request) return await handleCodexImages(c, "generations", void 0, parsed);
|
|
4564
|
-
return await routeImagesRequest(c, "generations", parsed);
|
|
5529
|
+
return await withImageEditRequest(c, operation, async (multipart) => {
|
|
5530
|
+
return await routeImagesRequest(operation, multipart ?? await prepareImageJsonRequest(c.req.raw));
|
|
5531
|
+
});
|
|
4565
5532
|
} catch (error) {
|
|
4566
|
-
const invalidRequest =
|
|
5533
|
+
const invalidRequest = imageEditRequestErrorResponse(c, error);
|
|
4567
5534
|
if (invalidRequest) return invalidRequest;
|
|
4568
|
-
logger$15.error(
|
|
5535
|
+
logger$15.error(`images.${operation}.error`, { error });
|
|
4569
5536
|
return await forwardError(c, error);
|
|
4570
5537
|
}
|
|
4571
5538
|
}
|
|
4572
5539
|
// src/routes/images/route.ts
|
|
4573
5540
|
const imageRoutes = new Hono();
|
|
4574
|
-
imageRoutes.post("/generations",
|
|
4575
|
-
imageRoutes.post("/edits",
|
|
5541
|
+
imageRoutes.post("/generations", (c) => handleImages(c, "generations"));
|
|
5542
|
+
imageRoutes.post("/edits", (c) => handleImages(c, "edits"));
|
|
4576
5543
|
// src/lib/model-pdf-support.ts
|
|
4577
5544
|
function resolveModelPdfSupport(provider, options) {
|
|
4578
5545
|
if (options.supportPdf !== void 0) return options.supportPdf;
|
|
@@ -6716,9 +7683,9 @@ const mapResponsesUsage = (response) => {
|
|
|
6716
7683
|
...cacheWriteTokens !== void 0 && { cache_creation_input_tokens: cacheWriteTokens }
|
|
6717
7684
|
};
|
|
6718
7685
|
};
|
|
6719
|
-
const isRecord$
|
|
6720
|
-
const isResponseOutputText = (block) => isRecord$
|
|
6721
|
-
const isResponseOutputRefusal = (block) => isRecord$
|
|
7686
|
+
const isRecord$3 = (value) => typeof value === "object" && value !== null;
|
|
7687
|
+
const isResponseOutputText = (block) => isRecord$3(block) && "type" in block && block.type === "output_text";
|
|
7688
|
+
const isResponseOutputRefusal = (block) => isRecord$3(block) && "type" in block && block.type === "refusal";
|
|
6722
7689
|
const convertToolResultContent = (content) => {
|
|
6723
7690
|
if (typeof content === "string") return content;
|
|
6724
7691
|
if (Array.isArray(content)) {
|
|
@@ -7471,7 +8438,7 @@ function resolveToolDescriptor(registry, alias) {
|
|
|
7471
8438
|
};
|
|
7472
8439
|
}
|
|
7473
8440
|
function decodeCustomToolInput(input) {
|
|
7474
|
-
if (isRecord$
|
|
8441
|
+
if (isRecord$2(input) && typeof input.input === "string") return input.input;
|
|
7475
8442
|
return typeof input === "string" ? input : JSON.stringify(input ?? {});
|
|
7476
8443
|
}
|
|
7477
8444
|
function toResponseId(messageId) {
|
|
@@ -7552,7 +8519,7 @@ function isNamespaceTool(tool) {
|
|
|
7552
8519
|
return getStringField$1(tool, "type") === "namespace";
|
|
7553
8520
|
}
|
|
7554
8521
|
function registerTool(tool, registry, strictCustomTools, namespaces = []) {
|
|
7555
|
-
if (!isRecord$
|
|
8522
|
+
if (!isRecord$2(tool)) throw new ResponsesMessagesTranslationError("Invalid Responses tool definition");
|
|
7556
8523
|
const type = getStringField$1(tool, "type");
|
|
7557
8524
|
if (type === "namespace") {
|
|
7558
8525
|
const namespaceTool = tool;
|
|
@@ -7569,7 +8536,7 @@ function registerTool(tool, registry, strictCustomTools, namespaces = []) {
|
|
|
7569
8536
|
name,
|
|
7570
8537
|
namespace: namespaces.length > 0 ? namespaces.join(".") : void 0,
|
|
7571
8538
|
description: getOptionalStringField(tool, "description"),
|
|
7572
|
-
parameters: type === "function" && isRecord$
|
|
8539
|
+
parameters: type === "function" && isRecord$2(tool.parameters) ? tool.parameters : null
|
|
7573
8540
|
}, registry, strictCustomTools);
|
|
7574
8541
|
}
|
|
7575
8542
|
function registerMessagesTool(registration, registry, strictCustomTools) {
|
|
@@ -7667,7 +8634,7 @@ function translateInputItems(input, messages, system, registry) {
|
|
|
7667
8634
|
}
|
|
7668
8635
|
}
|
|
7669
8636
|
function translateInputMessage(item, messages, system, userMessageSeen) {
|
|
7670
|
-
if (!isRecord$
|
|
8637
|
+
if (!isRecord$2(item)) throw new ResponsesMessagesTranslationError("Invalid Responses message item");
|
|
7671
8638
|
const role = getStringField$1(item, "role");
|
|
7672
8639
|
if (role !== "user" && role !== "assistant" && role !== "system" && role !== "developer") throw new ResponsesMessagesTranslationError("Responses message items require a supported role");
|
|
7673
8640
|
if (role === "developer" && !userMessageSeen) return;
|
|
@@ -7707,7 +8674,7 @@ function appendDeveloperPrompts(system, input) {
|
|
|
7707
8674
|
if (type === "agent_message") break;
|
|
7708
8675
|
if ((type === void 0 || type === "message") && role === "user") break;
|
|
7709
8676
|
if (type !== void 0 && type !== "message" || role !== "developer") continue;
|
|
7710
|
-
prompts.push(translateSystemContent(isRecord$
|
|
8677
|
+
prompts.push(translateSystemContent(isRecord$2(item) ? item.content : void 0, "message.content"));
|
|
7711
8678
|
}
|
|
7712
8679
|
const [firstPrompt, ...remainingPrompts] = prompts;
|
|
7713
8680
|
if (firstPrompt) system.push({
|
|
@@ -7728,7 +8695,7 @@ function translateInputReasoning(item, messages) {
|
|
|
7728
8695
|
});
|
|
7729
8696
|
}
|
|
7730
8697
|
function translateInputToolCall(item, kind, messages, registry) {
|
|
7731
|
-
if (!isRecord$
|
|
8698
|
+
if (!isRecord$2(item)) throw new ResponsesMessagesTranslationError("Invalid Responses tool call item");
|
|
7732
8699
|
const name = requireStringField(item, "name", `${kind}_tool_call`);
|
|
7733
8700
|
const namespace = getOptionalStringField(item, "namespace") ?? void 0;
|
|
7734
8701
|
const toolIdentity = {
|
|
@@ -7759,7 +8726,7 @@ function translateAssistantContent(value, path) {
|
|
|
7759
8726
|
}] : [];
|
|
7760
8727
|
if (!Array.isArray(value)) throw new ResponsesMessagesTranslationError(`${path} must be text or an array`);
|
|
7761
8728
|
return value.map((part, index) => {
|
|
7762
|
-
if (!isRecord$
|
|
8729
|
+
if (!isRecord$2(part)) throw new ResponsesMessagesTranslationError(`${path}[${index}] must be an object`);
|
|
7763
8730
|
const type = getStringField$1(part, "type");
|
|
7764
8731
|
if (type !== "input_text" && type !== "output_text" && type !== "text") throw new ResponsesMessagesTranslationError(`${path}[${index}] has unsupported assistant content type '${type ?? "unknown"}'`);
|
|
7765
8732
|
return {
|
|
@@ -7773,14 +8740,14 @@ function translateSystemContent(value, path) {
|
|
|
7773
8740
|
if (typeof value === "string") return value;
|
|
7774
8741
|
if (!Array.isArray(value)) throw new ResponsesMessagesTranslationError(`${path} must be text or an array`);
|
|
7775
8742
|
return value.map((part, index) => {
|
|
7776
|
-
if (!isRecord$
|
|
8743
|
+
if (!isRecord$2(part)) throw new ResponsesMessagesTranslationError(`${path}[${index}] must be an object`);
|
|
7777
8744
|
const type = getStringField$1(part, "type");
|
|
7778
8745
|
if (type !== "input_text" && type !== "output_text" && type !== "text") throw new ResponsesMessagesTranslationError(`${path}[${index}] has unsupported system content type '${type ?? "unknown"}'`);
|
|
7779
8746
|
return requireStringField(part, "text", `${path}[${index}]`);
|
|
7780
8747
|
}).join("\n\n");
|
|
7781
8748
|
}
|
|
7782
8749
|
function translateUserContentPart(part, path) {
|
|
7783
|
-
if (!isRecord$
|
|
8750
|
+
if (!isRecord$2(part)) throw new ResponsesMessagesTranslationError(`${path} must be an object`);
|
|
7784
8751
|
const type = getStringField$1(part, "type");
|
|
7785
8752
|
if (type === "input_text" || type === "output_text" || type === "text") return {
|
|
7786
8753
|
type: "text",
|
|
@@ -7802,7 +8769,7 @@ function translateToolResultContent(value, path) {
|
|
|
7802
8769
|
return blocks.length > 0 ? blocks : "";
|
|
7803
8770
|
}
|
|
7804
8771
|
function isEmptyTextContentPart(part) {
|
|
7805
|
-
if (!isRecord$
|
|
8772
|
+
if (!isRecord$2(part)) return false;
|
|
7806
8773
|
const type = part.type;
|
|
7807
8774
|
return (type === "input_text" || type === "output_text" || type === "text") && part.text === "";
|
|
7808
8775
|
}
|
|
@@ -7987,7 +8954,7 @@ function appendUserBlock(messages, block) {
|
|
|
7987
8954
|
function parseFunctionArguments(value, _path) {
|
|
7988
8955
|
try {
|
|
7989
8956
|
const parsed = JSON.parse(value);
|
|
7990
|
-
if (isRecord$
|
|
8957
|
+
if (isRecord$2(parsed)) return parsed;
|
|
7991
8958
|
} catch {
|
|
7992
8959
|
return {};
|
|
7993
8960
|
}
|
|
@@ -8030,15 +8997,15 @@ function createStableHash(value) {
|
|
|
8030
8997
|
return createHash("sha256").update(value).digest("hex").slice(0, 24);
|
|
8031
8998
|
}
|
|
8032
8999
|
function getItemType(value) {
|
|
8033
|
-
return isRecord$
|
|
9000
|
+
return isRecord$2(value) && typeof value.type === "string" ? value.type : void 0;
|
|
8034
9001
|
}
|
|
8035
9002
|
function getStringField$1(value, field) {
|
|
8036
|
-
if (!isRecord$
|
|
9003
|
+
if (!isRecord$2(value)) return void 0;
|
|
8037
9004
|
const fieldValue = value[field];
|
|
8038
9005
|
return typeof fieldValue === "string" ? fieldValue : void 0;
|
|
8039
9006
|
}
|
|
8040
9007
|
function getOptionalStringField(value, field) {
|
|
8041
|
-
if (!isRecord$
|
|
9008
|
+
if (!isRecord$2(value)) return void 0;
|
|
8042
9009
|
const fieldValue = value[field];
|
|
8043
9010
|
return typeof fieldValue === "string" || fieldValue === null ? fieldValue : void 0;
|
|
8044
9011
|
}
|
|
@@ -8048,10 +9015,10 @@ function requireStringField(value, field, path) {
|
|
|
8048
9015
|
return result;
|
|
8049
9016
|
}
|
|
8050
9017
|
function getArrayField(value, field) {
|
|
8051
|
-
if (!isRecord$
|
|
9018
|
+
if (!isRecord$2(value)) return [];
|
|
8052
9019
|
return Array.isArray(value[field]) ? value[field] : [];
|
|
8053
9020
|
}
|
|
8054
|
-
function isRecord$
|
|
9021
|
+
function isRecord$2(value) {
|
|
8055
9022
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
8056
9023
|
}
|
|
8057
9024
|
const DEFAULT_RESPONSES_COMPACT_THRESHOLD_RATIO = .85;
|
|
@@ -9208,7 +10175,7 @@ async function forwardGoogleGenerateContent(provider, model, payload, requestHea
|
|
|
9208
10175
|
applyOpencodeSessionHeader(provider, headers, options.sessionId);
|
|
9209
10176
|
headers.accept = options.stream ? "text/event-stream" : "application/json";
|
|
9210
10177
|
const transport = getUpstreamTransportConfig();
|
|
9211
|
-
setPluginRequestActualModel(model.replace(/^models\//u, ""));
|
|
10178
|
+
setPluginRequestActualModel(model.replace(/^models\//u, ""), provider.name);
|
|
9212
10179
|
return await fetchUpstreamWithLifecycle(url, {
|
|
9213
10180
|
method: "POST",
|
|
9214
10181
|
headers,
|
|
@@ -10509,14 +11476,14 @@ function withModelDisplayName(model, provider) {
|
|
|
10509
11476
|
// src/routes/models/model-discovery.ts
|
|
10510
11477
|
const logger$10 = createHandlerLogger("models-handler");
|
|
10511
11478
|
const EPOCH_ISO = (/* @__PURE__ */ new Date(0)).toISOString();
|
|
10512
|
-
function isRecord(value) {
|
|
11479
|
+
function isRecord$1(value) {
|
|
10513
11480
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10514
11481
|
}
|
|
10515
11482
|
function firstPositiveInteger(...values) {
|
|
10516
11483
|
for (const value of values) if (typeof value === "number" && Number.isFinite(value) && value >= 1) return Math.floor(value);
|
|
10517
11484
|
}
|
|
10518
11485
|
function normalizeModelCapabilities(model) {
|
|
10519
|
-
if (!isRecord(model.capabilities)) return model;
|
|
11486
|
+
if (!isRecord$1(model.capabilities)) return model;
|
|
10520
11487
|
const limits = getRecordField(model.capabilities, "limits");
|
|
10521
11488
|
const supports = getRecordField(model.capabilities, "supports");
|
|
10522
11489
|
return {
|
|
@@ -10547,13 +11514,21 @@ function getStringField(model, field) {
|
|
|
10547
11514
|
}
|
|
10548
11515
|
function getRecordField(model, field) {
|
|
10549
11516
|
const value = model?.[field];
|
|
10550
|
-
return isRecord(value) ? value : void 0;
|
|
11517
|
+
return isRecord$1(value) ? value : void 0;
|
|
10551
11518
|
}
|
|
10552
11519
|
function getStringList(value) {
|
|
10553
11520
|
if (!Array.isArray(value)) return void 0;
|
|
10554
11521
|
const strings = [...new Set(value.filter((item) => typeof item === "string"))];
|
|
10555
11522
|
return strings.length > 0 ? strings : void 0;
|
|
10556
11523
|
}
|
|
11524
|
+
function isImageOnlyModel(provider, modelId, model, config) {
|
|
11525
|
+
const record = isRecord$1(model) ? model : void 0;
|
|
11526
|
+
const catalog = getModelsDevModel(resolveProviderModelsDevId(provider, config), modelId);
|
|
11527
|
+
const builtin = getBuiltinProviderImageModelRecords(provider).find((image) => image.id === modelId);
|
|
11528
|
+
const output = getStringList(record?.output_modalities) ?? getStringList(getRecordField(record, "architecture")?.output_modalities) ?? getStringList(catalog?.modalities?.output) ?? getStringList(builtin?.output_modalities);
|
|
11529
|
+
if (output) return output.includes("image") && !output.includes("text");
|
|
11530
|
+
return record?.type === "image" || getRecordField(record, "capabilities")?.type === "image";
|
|
11531
|
+
}
|
|
10557
11532
|
function getFallbackProviderModelRecords(provider, reason, details = {}) {
|
|
10558
11533
|
const fallbackModels = getLocalProviderModelRecords(provider, getRawProviderConfig(provider)) ?? getBuiltinProviderModelRecords(provider);
|
|
10559
11534
|
logger$10.warn(`models.provider.fallback_${reason}`, {
|
|
@@ -10563,10 +11538,10 @@ function getFallbackProviderModelRecords(provider, reason, details = {}) {
|
|
|
10563
11538
|
});
|
|
10564
11539
|
return fallbackModels;
|
|
10565
11540
|
}
|
|
10566
|
-
function normalizeProviderModel(provider, model, config) {
|
|
10567
|
-
if (!isRecord(model)) return null;
|
|
11541
|
+
function normalizeProviderModel(provider, model, config, includeImageModels = false) {
|
|
11542
|
+
if (!isRecord$1(model)) return null;
|
|
10568
11543
|
const rawId = getStringField(model, "id");
|
|
10569
|
-
if (!rawId || !isProviderModelVisible(provider, rawId, config)) return null;
|
|
11544
|
+
if (!rawId || !isProviderModelVisible(provider, rawId, config) || !includeImageModels && isImageOnlyModel(provider, rawId, model, config)) return null;
|
|
10570
11545
|
const id = `${provider}/${rawId}`;
|
|
10571
11546
|
const ownedBy = getStringField(model, "owned_by") ?? getStringField(model, "vendor") ?? provider;
|
|
10572
11547
|
return {
|
|
@@ -10579,8 +11554,8 @@ function normalizeProviderModel(provider, model, config) {
|
|
|
10579
11554
|
owned_by: ownedBy
|
|
10580
11555
|
};
|
|
10581
11556
|
}
|
|
10582
|
-
function normalizeProviderModels(provider, models, config) {
|
|
10583
|
-
return models.map((model) => normalizeProviderModel(provider, model, config)).filter((model) => model !== null);
|
|
11557
|
+
function normalizeProviderModels(provider, models, config, includeImageModels = false) {
|
|
11558
|
+
return models.map((model) => normalizeProviderModel(provider, model, config, includeImageModels)).filter((model) => model !== null);
|
|
10584
11559
|
}
|
|
10585
11560
|
async function getProviderModelRecords(providerConfig, requestHeaders) {
|
|
10586
11561
|
const localModels = getLocalProviderModelRecords(providerConfig.name, providerConfig);
|
|
@@ -10589,8 +11564,8 @@ async function getProviderModelRecords(providerConfig, requestHeaders) {
|
|
|
10589
11564
|
const response = await forwardProviderModels(providerConfig, requestHeaders);
|
|
10590
11565
|
if (!response.ok) return getFallbackProviderModelRecords(providerConfig.name, "non_ok", { statusCode: response.status });
|
|
10591
11566
|
const body = await response.json();
|
|
10592
|
-
if (!isRecord(body) || !Array.isArray(body.data)) return getFallbackProviderModelRecords(providerConfig.name, "invalid_body");
|
|
10593
|
-
return body.data.filter(isRecord).map(normalizeModelCapabilities);
|
|
11567
|
+
if (!isRecord$1(body) || !Array.isArray(body.data)) return getFallbackProviderModelRecords(providerConfig.name, "invalid_body");
|
|
11568
|
+
return body.data.filter(isRecord$1).map(normalizeModelCapabilities);
|
|
10594
11569
|
} catch (error) {
|
|
10595
11570
|
return getFallbackProviderModelRecords(providerConfig.name, "error", { error });
|
|
10596
11571
|
}
|
|
@@ -10600,7 +11575,7 @@ function getCopilotModelRecords() {
|
|
|
10600
11575
|
}
|
|
10601
11576
|
function getModelsById(models) {
|
|
10602
11577
|
return new Map(models.flatMap((model) => {
|
|
10603
|
-
if (!isRecord(model)) return [];
|
|
11578
|
+
if (!isRecord$1(model)) return [];
|
|
10604
11579
|
const id = getStringField(model, "id");
|
|
10605
11580
|
return id ? [[id, model]] : [];
|
|
10606
11581
|
}));
|
|
@@ -10614,12 +11589,22 @@ function deduplicateModels(models, getId) {
|
|
|
10614
11589
|
return true;
|
|
10615
11590
|
});
|
|
10616
11591
|
}
|
|
10617
|
-
|
|
11592
|
+
function withProviderImageModels(provider, models) {
|
|
11593
|
+
const imageModels = getModelsById(getBuiltinProviderImageModelRecords(provider));
|
|
11594
|
+
if (imageModels.size === 0) return models;
|
|
11595
|
+
const listedModels = getModelsById(models);
|
|
11596
|
+
return [...models.map((model) => isRecord$1(model) ? {
|
|
11597
|
+
...imageModels.get(getStringField(model, "id") ?? ""),
|
|
11598
|
+
...model
|
|
11599
|
+
} : model), ...[...imageModels.values()].filter((model) => !listedModels.has(getStringField(model, "id") ?? ""))];
|
|
11600
|
+
}
|
|
11601
|
+
async function getProviderModels(provider, requestHeaders, selectModels, includeImageModels = false) {
|
|
10618
11602
|
try {
|
|
10619
11603
|
const providerConfig = await resolveProviderConfig(provider);
|
|
10620
11604
|
if (!providerConfig) return [];
|
|
10621
|
-
const
|
|
10622
|
-
|
|
11605
|
+
const upstreamModels = providerConfig.name === "codex" ? getModels().data.map(normalizeModelCapabilities) : await getProviderModelRecords(providerConfig, requestHeaders);
|
|
11606
|
+
const models = includeImageModels ? withProviderImageModels(providerConfig.name, upstreamModels) : upstreamModels;
|
|
11607
|
+
return normalizeProviderModels(providerConfig.name, selectModels?.(providerConfig, models) ?? models, providerConfig, includeImageModels);
|
|
10623
11608
|
} catch (error) {
|
|
10624
11609
|
if (provider === "codex") {
|
|
10625
11610
|
logger$10.warn("models.provider.skip_error", {
|
|
@@ -10630,14 +11615,22 @@ async function getProviderModels(provider, requestHeaders, selectModels) {
|
|
|
10630
11615
|
}
|
|
10631
11616
|
const fallbackModels = getFallbackProviderModelRecords(provider, "error", { error });
|
|
10632
11617
|
const providerConfig = getRawProviderConfig(provider);
|
|
10633
|
-
return normalizeProviderModels(provider, selectModels?.(providerConfig, fallbackModels) ?? fallbackModels, providerConfig);
|
|
11618
|
+
return normalizeProviderModels(provider, selectModels?.(providerConfig, fallbackModels) ?? fallbackModels, providerConfig, includeImageModels);
|
|
10634
11619
|
}
|
|
10635
11620
|
}
|
|
10636
11621
|
async function getAggregatedModels(requestHeaders, selectors = {}) {
|
|
10637
11622
|
const rawCopilotModels = getCopilotModelRecords();
|
|
10638
|
-
const
|
|
10639
|
-
const
|
|
10640
|
-
|
|
11623
|
+
const copilotConfig = getRawProviderConfig(GITHUB_COPILOT_PROVIDER);
|
|
11624
|
+
const isCopilotImage = (model) => isImageOnlyModel(GITHUB_COPILOT_PROVIDER, model.id, model, copilotConfig);
|
|
11625
|
+
const copilotModels = (selectors.selectCopilotModels?.(rawCopilotModels) ?? rawCopilotModels).filter((model) => !isCopilotImage(model)).map(normalizeCopilotModel);
|
|
11626
|
+
const rawCopilotImages = selectors.includeImageModels && isGitHubCopilotAvailable() ? withProviderImageModels(GITHUB_COPILOT_PROVIDER, rawCopilotModels.filter(isCopilotImage)) : [];
|
|
11627
|
+
const copilotImages = selectors.includeImageModels && isGitHubCopilotAvailable() ? normalizeProviderModels(GITHUB_COPILOT_PROVIDER, (selectors.selectProviderModels?.(copilotConfig, rawCopilotImages) ?? rawCopilotImages).filter((model) => isRecord$1(model) && isImageOnlyModel("github-copilot", getStringField(model, "id") ?? "", model, copilotConfig)), copilotConfig, true) : [];
|
|
11628
|
+
const providerModelsByProvider = await Promise.all(listEnabledProviders().map((provider) => getProviderModels(provider, requestHeaders, selectors.selectProviderModels, selectors.includeImageModels)));
|
|
11629
|
+
return deduplicateModels([
|
|
11630
|
+
...copilotModels,
|
|
11631
|
+
...copilotImages,
|
|
11632
|
+
...providerModelsByProvider.flat()
|
|
11633
|
+
], (model) => model.id);
|
|
10641
11634
|
}
|
|
10642
11635
|
function selectAgentCopilotModels(models) {
|
|
10643
11636
|
const selection = getProviderAgentModels(getRawProviderConfig(GITHUB_COPILOT_PROVIDER));
|
|
@@ -10650,8 +11643,9 @@ function selectAgentProviderModels(providerConfig, models) {
|
|
|
10650
11643
|
const modelsById = getModelsById(models);
|
|
10651
11644
|
return selection.map((id) => modelsById.get(id) ?? { id });
|
|
10652
11645
|
}
|
|
10653
|
-
async function getAgentModels(requestHeaders) {
|
|
11646
|
+
async function getAgentModels(requestHeaders, options = {}) {
|
|
10654
11647
|
return getAggregatedModels(requestHeaders, {
|
|
11648
|
+
includeImageModels: options.includeImageModels,
|
|
10655
11649
|
selectCopilotModels: selectAgentCopilotModels,
|
|
10656
11650
|
selectProviderModels: selectAgentProviderModels
|
|
10657
11651
|
});
|
|
@@ -10815,7 +11809,8 @@ function getCopilotCodexCandidates() {
|
|
|
10815
11809
|
return candidates;
|
|
10816
11810
|
}
|
|
10817
11811
|
function isCopilotCodexCandidate(model) {
|
|
10818
|
-
|
|
11812
|
+
const endpoints = model.supported_endpoints ?? [];
|
|
11813
|
+
return !isImageOnlyModel("github-copilot", model.id, model) && endpoints.some((endpoint) => endpoint === MESSAGES_ENDPOINT || endpoint === CHAT_COMPLETIONS_ENDPOINT || RESPONSES_ENDPOINTS.has(endpoint)) && model.capabilities.supports.tool_calls !== false;
|
|
10819
11814
|
}
|
|
10820
11815
|
function createCopilotCodexCandidate(model) {
|
|
10821
11816
|
const reasoningEfforts = normalizeReasoningEfforts(model.capabilities.supports.reasoning_effort);
|
|
@@ -10845,9 +11840,11 @@ async function getProviderCodexCandidates(provider, requestHeaders) {
|
|
|
10845
11840
|
const candidates = [];
|
|
10846
11841
|
for (const modelId of modelIds) {
|
|
10847
11842
|
if (!isProviderModelVisible(provider, modelId, providerConfig)) continue;
|
|
11843
|
+
const remoteModel = modelsById.get(modelId);
|
|
11844
|
+
if (isImageOnlyModel(provider, modelId, remoteModel, providerConfig)) continue;
|
|
10848
11845
|
const effectiveType = resolveEffectiveProviderType(providerConfig, modelId);
|
|
10849
11846
|
if (!isMessagesFallbackProviderType(effectiveType) && effectiveType !== "openai-responses") continue;
|
|
10850
|
-
candidates.push(createProviderCodexCandidate(providerConfig, modelId,
|
|
11847
|
+
candidates.push(createProviderCodexCandidate(providerConfig, modelId, remoteModel, effectiveType));
|
|
10851
11848
|
}
|
|
10852
11849
|
return candidates;
|
|
10853
11850
|
} catch (error) {
|
|
@@ -12634,7 +13631,7 @@ async function handleCodexModelsProxy(c, resolvedProviderConfig) {
|
|
|
12634
13631
|
const providerConfig = getRawProviderConfig("codex");
|
|
12635
13632
|
return createCodexCatalogResponse(c, {
|
|
12636
13633
|
...catalog,
|
|
12637
|
-
models: catalog.models.filter((model) => isProviderAgentModelVisible(providerConfig, model.slug))
|
|
13634
|
+
models: catalog.models.filter((model) => !isImageOnlyModel("codex", model.slug, model, providerConfig) && isProviderAgentModelVisible(providerConfig, model.slug))
|
|
12638
13635
|
}, new Set(getProviderAgentModels(providerConfig)), FALLBACK_CODEX_MODELS[0].model_messages);
|
|
12639
13636
|
}
|
|
12640
13637
|
async function handleCodexModels(c) {
|
|
@@ -12651,10 +13648,10 @@ async function handleMergedCodexModels(c, candidatesRequest, options = {}) {
|
|
|
12651
13648
|
return [];
|
|
12652
13649
|
})]);
|
|
12653
13650
|
const upstreamCatalog = upstreamCatalogResult?.catalog;
|
|
12654
|
-
const
|
|
13651
|
+
const codexConfig = getRawProviderConfig("codex");
|
|
13652
|
+
const upstreamModels = (upstreamCatalog?.models ?? FALLBACK_CODEX_MODELS).filter((model) => !isImageOnlyModel("codex", model.slug, model, codexConfig));
|
|
12655
13653
|
const template = selectTemplate(upstreamModels);
|
|
12656
13654
|
const catalogModelsBySlug = new Map(upstreamModels.map((model) => [model.slug, model]));
|
|
12657
|
-
const codexConfig = getRawProviderConfig("codex");
|
|
12658
13655
|
const copilotConfig = getRawProviderConfig("github-copilot");
|
|
12659
13656
|
const visibleUpstreamModels = upstreamModels.filter((model) => isProviderAgentModelVisible(codexConfig, model.slug) && (upstreamCatalog !== void 0 || codexConfig !== null || isProviderAgentModelVisible(copilotConfig, model.slug)));
|
|
12660
13657
|
const explicitSlugs = /* @__PURE__ */ new Set();
|
|
@@ -12825,15 +13822,17 @@ function getCandidatePriorityBase(candidate) {
|
|
|
12825
13822
|
return PROVIDER_PRIORITY_BASE;
|
|
12826
13823
|
}
|
|
12827
13824
|
function isCodexModelsResponse(value) {
|
|
12828
|
-
if (!isRecord(value) || !Array.isArray(value.models)) return false;
|
|
12829
|
-
return value.models.every((model) => isRecord(model) && typeof model.slug === "string");
|
|
13825
|
+
if (!isRecord$1(value) || !Array.isArray(value.models)) return false;
|
|
13826
|
+
return value.models.every((model) => isRecord$1(model) && typeof model.slug === "string");
|
|
12830
13827
|
}
|
|
12831
13828
|
// src/routes/models/default-models.ts
|
|
12832
13829
|
async function handleDefaultModels(c) {
|
|
12833
|
-
|
|
13830
|
+
const isDeepSeekClient = c.req.header("user-agent")?.toLowerCase().startsWith("deepseek") ?? false;
|
|
13831
|
+
return createModelListResponse(c, await (isDeepSeekClient ? getAgentModels : getAggregatedModels)(stripInternalRequestHeaders(c.req.raw.headers), { includeImageModels: !isDeepSeekClient }));
|
|
12834
13832
|
}
|
|
12835
13833
|
// src/routes/models/opencode-models.ts
|
|
12836
13834
|
const RESPONSES_PACKAGE = "@opencode/ai/providers/openai/responses";
|
|
13835
|
+
const IMAGES_PACKAGE = "@opencode/ai/providers/openai/images";
|
|
12837
13836
|
const CNY_PER_USD = 6.7;
|
|
12838
13837
|
const COST_DECIMALS = 6;
|
|
12839
13838
|
function isOpencodeUserAgent(userAgent) {
|
|
@@ -12902,10 +13901,12 @@ function toOpencodeModel(model, contextWindow) {
|
|
|
12902
13901
|
const config = getRawProviderConfig(provider);
|
|
12903
13902
|
const metadata = getModelMetadata(provider, rawId, model, config);
|
|
12904
13903
|
const { modelConfig, defaults, catalog, capabilities, supports, limits } = metadata;
|
|
13904
|
+
const output = getStringList(model.output_modalities) ?? getStringList(catalog?.modalities?.output) ?? (model.type === "image" ? ["image"] : ["text"]);
|
|
13905
|
+
const isImage = output.includes("image") && !output.includes("text");
|
|
12905
13906
|
const context = metadata.contextWindow ?? 256e3;
|
|
12906
13907
|
const inputLimit = metadata.inputLimit ?? 0;
|
|
12907
13908
|
const supportedMediaTypes = getStringList(getRecordField(limits, "vision")?.supported_media_types);
|
|
12908
|
-
const supportPdf = resolveModelPdfSupport(provider, {
|
|
13909
|
+
const supportPdf = !isImage && resolveModelPdfSupport(provider, {
|
|
12909
13910
|
supportPdf: modelConfig?.supportPdf,
|
|
12910
13911
|
supportedMediaTypes,
|
|
12911
13912
|
catalogInputModalities: catalog?.modalities?.input,
|
|
@@ -12922,15 +13923,16 @@ function toOpencodeModel(model, contextWindow) {
|
|
|
12922
13923
|
id: model.id,
|
|
12923
13924
|
modelID: model.id,
|
|
12924
13925
|
providerID: "local",
|
|
13926
|
+
...isImage && { type: "image" },
|
|
12925
13927
|
name: getStringField(model, "display_name") ?? catalog?.name ?? model.id,
|
|
12926
|
-
family: catalog?.family ?? getStringField(capabilities ?? {}, "family"),
|
|
12927
|
-
package: RESPONSES_PACKAGE,
|
|
13928
|
+
family: catalog?.family ?? getStringField(model, "family") ?? getStringField(capabilities ?? {}, "family"),
|
|
13929
|
+
package: isImage ? IMAGES_PACKAGE : RESPONSES_PACKAGE,
|
|
12928
13930
|
capabilities: {
|
|
12929
|
-
tools: typeof supports?.tool_calls === "boolean" ? supports.tool_calls : catalog?.tool_call ?? true,
|
|
13931
|
+
tools: isImage ? false : typeof supports?.tool_calls === "boolean" ? supports.tool_calls : catalog?.tool_call ?? true,
|
|
12930
13932
|
input: [...input],
|
|
12931
|
-
output
|
|
13933
|
+
output
|
|
12932
13934
|
},
|
|
12933
|
-
variants: efforts.map((effort) => ({
|
|
13935
|
+
variants: isImage ? [] : efforts.map((effort) => ({
|
|
12934
13936
|
id: effort,
|
|
12935
13937
|
body: { reasoning: { effort } }
|
|
12936
13938
|
})),
|
|
@@ -12938,7 +13940,10 @@ function toOpencodeModel(model, contextWindow) {
|
|
|
12938
13940
|
cost,
|
|
12939
13941
|
status: catalog?.status ?? (model.preview ? "beta" : "active"),
|
|
12940
13942
|
enabled: true,
|
|
12941
|
-
limit: {
|
|
13943
|
+
limit: isImage ? {
|
|
13944
|
+
context: 0,
|
|
13945
|
+
output: 0
|
|
13946
|
+
} : {
|
|
12942
13947
|
context: Math.min(context, contextWindow),
|
|
12943
13948
|
...inputLimit > 0 && { input: Math.min(inputLimit, context, contextWindow) },
|
|
12944
13949
|
output: Math.min(context, metadata.maxOutputTokens ?? 32e3)
|
|
@@ -12946,7 +13951,7 @@ function toOpencodeModel(model, contextWindow) {
|
|
|
12946
13951
|
};
|
|
12947
13952
|
}
|
|
12948
13953
|
async function handleOpencodeModels(c) {
|
|
12949
|
-
const models = await getAgentModels(stripInternalRequestHeaders(c.req.raw.headers));
|
|
13954
|
+
const models = await getAgentModels(stripInternalRequestHeaders(c.req.raw.headers), { includeImageModels: true });
|
|
12950
13955
|
const contextWindow = getOpencodeModelContextWindow();
|
|
12951
13956
|
c.header("Cache-Control", "private, no-store");
|
|
12952
13957
|
c.header("Vary", "User-Agent");
|
|
@@ -13008,30 +14013,35 @@ const providerImageRoutes = new Hono();
|
|
|
13008
14013
|
async function handleProviderImages(c, operation) {
|
|
13009
14014
|
const provider = c.req.param("provider") ?? "";
|
|
13010
14015
|
try {
|
|
13011
|
-
const providerConfig = await resolveProviderConfig(provider);
|
|
13012
|
-
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: {
|
|
13013
14018
|
message: `Provider '${provider}' not found or disabled`,
|
|
13014
14019
|
type: "invalid_request_error"
|
|
13015
14020
|
} }, 404);
|
|
13016
|
-
|
|
13017
|
-
const
|
|
13018
|
-
|
|
13019
|
-
|
|
13020
|
-
|
|
13021
|
-
|
|
13022
|
-
|
|
13023
|
-
|
|
13024
|
-
|
|
13025
|
-
|
|
13026
|
-
|
|
13027
|
-
|
|
13028
|
-
|
|
14021
|
+
const forward = async (request, model) => {
|
|
14022
|
+
const context = {
|
|
14023
|
+
logger: logger$6,
|
|
14024
|
+
provider,
|
|
14025
|
+
model
|
|
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;
|
|
13029
14044
|
}
|
|
13030
|
-
}
|
|
13031
|
-
if (providerConfig.name === "codex") return await handleCodexImages(c, operation, providerConfig);
|
|
13032
|
-
return await forwardProviderImagesWithLogging(providerConfig, c.req.raw, operation, {
|
|
13033
|
-
logger: logger$6,
|
|
13034
|
-
provider
|
|
13035
14045
|
});
|
|
13036
14046
|
} catch (error) {
|
|
13037
14047
|
const invalidRequest = imageEditRequestErrorResponse(c, error);
|
|
@@ -14873,6 +15883,38 @@ usageRoute.get("/", async (c) => {
|
|
|
14873
15883
|
return c.json({ error: "Failed to fetch Copilot usage" }, 500);
|
|
14874
15884
|
}
|
|
14875
15885
|
});
|
|
15886
|
+
// src/lib/token-usage/videos.ts
|
|
15887
|
+
function isRecord(value) {
|
|
15888
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
15889
|
+
}
|
|
15890
|
+
/** Reads the finite result JSON once for usage accounting and debug output. */
|
|
15891
|
+
async function recordVideoResultUsage(response, options) {
|
|
15892
|
+
let body;
|
|
15893
|
+
try {
|
|
15894
|
+
body = await response.clone().text();
|
|
15895
|
+
} catch {
|
|
15896
|
+
return;
|
|
15897
|
+
}
|
|
15898
|
+
options.onBody(body);
|
|
15899
|
+
if (!response.ok) return;
|
|
15900
|
+
let result;
|
|
15901
|
+
try {
|
|
15902
|
+
result = JSON.parse(body);
|
|
15903
|
+
} catch {
|
|
15904
|
+
return;
|
|
15905
|
+
}
|
|
15906
|
+
if (!isRecord(result) || result.status !== "done" || !isRecord(result.usage)) return;
|
|
15907
|
+
const cost = xaiUsdTicksToCost(result.usage.cost_in_usd_ticks);
|
|
15908
|
+
if (cost === void 0) return;
|
|
15909
|
+
recordTokenUsageEvent({
|
|
15910
|
+
source: "provider",
|
|
15911
|
+
providerName: options.providerConfig.name,
|
|
15912
|
+
endpoint: "videos_generations",
|
|
15913
|
+
model: typeof result.model === "string" && result.model.trim() ? result.model : options.defaultModel,
|
|
15914
|
+
cost,
|
|
15915
|
+
upstreamRequestId: options.requestId
|
|
15916
|
+
});
|
|
15917
|
+
}
|
|
14876
15918
|
// src/services/xai/videos.ts
|
|
14877
15919
|
var XaiVideosRequestError = class extends Error {
|
|
14878
15920
|
constructor(message) {
|
|
@@ -14905,6 +15947,7 @@ async function forwardXaiVideos(providerConfig, request, videoRequest) {
|
|
|
14905
15947
|
const logger$1 = createHandlerLogger("videos-handler");
|
|
14906
15948
|
const SUPPORTED_VIDEO_MODEL = "grok-imagine-video-1.5";
|
|
14907
15949
|
const videoRouteDependencies = {
|
|
15950
|
+
debugJson,
|
|
14908
15951
|
resolveMappedModel,
|
|
14909
15952
|
resolveProviderConfig
|
|
14910
15953
|
};
|
|
@@ -14943,6 +15986,12 @@ async function handleVideos(c, operation) {
|
|
|
14943
15986
|
} }, 404);
|
|
14944
15987
|
const upstreamResponse = await forwardXaiVideos(providerConfig, c.req.raw, videoRequest);
|
|
14945
15988
|
debugJson(logger$1, `videos.${operation}.response`, { statusCode: upstreamResponse.status });
|
|
15989
|
+
if (videoRequest.operation === "result") await recordVideoResultUsage(upstreamResponse, {
|
|
15990
|
+
providerConfig,
|
|
15991
|
+
requestId: videoRequest.requestId,
|
|
15992
|
+
defaultModel: SUPPORTED_VIDEO_MODEL,
|
|
15993
|
+
onBody: (body) => videoRouteDependencies.debugJson(logger$1, "videos.result.response.body", { body })
|
|
15994
|
+
});
|
|
14946
15995
|
return createProviderProxyResponse(upstreamResponse);
|
|
14947
15996
|
} catch (error) {
|
|
14948
15997
|
if (error instanceof XaiVideosRequestError && !c.req.raw.signal.aborted) return c.json({ error: {
|
|
@@ -15024,4 +16073,4 @@ function createServer(options = {}) {
|
|
|
15024
16073
|
createServer();
|
|
15025
16074
|
export { createServer };
|
|
15026
16075
|
|
|
15027
|
-
//# sourceMappingURL=server-
|
|
16076
|
+
//# sourceMappingURL=server-De8OQQe6.js.map
|