@jeffreycao/copilot-api 2.8.2 → 2.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. package/dist/auth-BFV7bzJo.js +1 -0
  2. package/dist/{auth-B9pYwLYA.js → auth-b8dJfIDO.js} +4 -4
  3. package/dist/{auth-B9pYwLYA.js.map → auth-b8dJfIDO.js.map} +1 -1
  4. package/dist/{config-BvACNGIp.js → config-B55PwJzQ.js} +44 -3
  5. package/dist/config-B55PwJzQ.js.map +1 -0
  6. package/dist/{config-store-BLr5xjym.js → config-store-DFcv4AY0.js} +2 -2
  7. package/dist/{config-store-BLr5xjym.js.map → config-store-DFcv4AY0.js.map} +1 -1
  8. package/dist/{copilot-runtime-DvmA6ALD.js → copilot-runtime-DNKNavLK.js} +4 -4
  9. package/dist/{copilot-runtime-DvmA6ALD.js.map → copilot-runtime-DNKNavLK.js.map} +1 -1
  10. package/dist/{debug-C_uR3s4n.js → debug-1bQIZTcG.js} +3 -3
  11. package/dist/{debug-C_uR3s4n.js.map → debug-1bQIZTcG.js.map} +1 -1
  12. package/dist/main.js +4 -4
  13. package/dist/{provider-_0MixRky.js → provider-B8UJ434o.js} +2 -2
  14. package/dist/{provider-_0MixRky.js.map → provider-B8UJ434o.js.map} +1 -1
  15. package/dist/{provider-management-C8AWr-Gk.js → provider-management-C6ZgkTJ7.js} +2 -2
  16. package/dist/{provider-management-C8AWr-Gk.js.map → provider-management-C6ZgkTJ7.js.map} +1 -1
  17. package/dist/{server-C_-cCnrU.js → server-Cw9qn5Gt.js} +1457 -211
  18. package/dist/server-Cw9qn5Gt.js.map +1 -0
  19. package/dist/{start-Da9ynTs-.js → start-CPoEuVzr.js} +7 -7
  20. package/dist/{start-Da9ynTs-.js.map → start-CPoEuVzr.js.map} +1 -1
  21. package/dist/{token-D3nRiw9B.js → token-DFlvOMh2.js} +12 -9
  22. package/dist/token-DFlvOMh2.js.map +1 -0
  23. package/package.json +3 -1
  24. package/dist/auth-dswtyrvJ.js +0 -1
  25. package/dist/config-BvACNGIp.js.map +0 -1
  26. package/dist/server-C_-cCnrU.js.map +0 -1
  27. package/dist/token-D3nRiw9B.js.map +0 -1
@@ -1,14 +1,14 @@
1
- import { D as state, E as isGitHubCopilotEnabled, O as HTTPError, S as PATHS, a as getAnthropicApiKey, c as getConfig, d as getUpstreamTransportConfig, g as isResponsesApiWebSocketEnabled, h as isResponsesApiWebSearchEnabled, i as getAlphaSearchModel, j as forwardError, 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-BLr5xjym.js";
2
- import { A as isOutputEncryptedContentDisabled, C as getModelResponsesApiCompactThreshold$1, D as isContextManagementEnabledForMessages, E as getSmallModelForProvider, M as setModelMappings, O as isContextManagementEnabledForResponses, S as getModelMappings, T as getSmallModel, _ as getModelsDevModelMaxOutputTokens, a as normalizeProviderBaseUrl, d as isProviderModelVisible, f as resolveProviderModelsDevId, g as getModelsDevModel, h as getBuiltinProviderModelRecords, i as listEnabledProviders, j as resolveMappedModel, k as isGpt56OrAbove, 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 getModelsDevModelPricing, w as getReasoningEffortForModel, x as getExtraPromptForModel } from "./config-BvACNGIp.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 fetchUpstreamWithLifecycle, b as parseUserIdMetadata, ct as encodePoolKeyPart, d as generateRequestIdFromPayload, dt as createWebSocketUrl, et as forwardCodexResponses, f as getPromptCacheKey, g as getUUID, h as getSessionAffinityFromHeaders, it as setPluginRequestActualModel, j as compactAutoContinuePromptStarts, k as prepareMessageProxyHeaders, lt as getResponsesStreamTerminalDisposition, m as getRootSessionIdFromHeaders, n as invalidateCodexRuntime, nt as requestContext, ot as createResponsesHttpEventStream, p as getRootSessionId, pt as stripInternalRequestHeaders, q as readGitHubToken, rt as resolveTraceId$1, s as setupCodexToken, st as createResponsesSafeStream, tt as generateTraceId, u as stopCopilotRefreshLoop, ut as createPooledWebSocketStream, v as isNullish, x as getCopilotUsage, y as isResponsesStream } from "./token-D3nRiw9B.js";
1
+ import { D as isGitHubCopilotEnabled, E as isGitHubCopilotAvailable, M as forwardError, O as state, S as PATHS, T as assertGitHubCopilotEnabled, a as getAnthropicApiKey, c as getConfig, d as getUpstreamTransportConfig, g as isResponsesApiWebSocketEnabled, h as isResponsesApiWebSearchEnabled, i as getAlphaSearchModel, k as HTTPError, l as getMessageApiWebSearchModel, m as isMessagesApiEnabled, o as getClaudeAutoModel, p as isAlphaSearchCodexPriorityEnabled, s as getClaudeTokenMultiplier, u as getOpencodeModelContextWindow, w as GITHUB_COPILOT_PROVIDER, y as reloadConfig } from "./config-store-DFcv4AY0.js";
2
+ import { A as getSmallModelForProvider, D as getModelResponsesApiCompactThreshold$1, E as getModelMappings, F as resolveMappedModel, I as setModelMappings, M as isContextManagementEnabledForResponses, N as isGpt56OrAbove, O as getReasoningEffortForModel, P as isOutputEncryptedContentDisabled, S as getModelsDevModelPricing, T as getExtraPromptForModel, _ as DEFAULT_IMAGE_MODELS, a as normalizeProviderBaseUrl, b as getModelsDevModel, d as isProviderModelVisible, f as resolveProviderModelsDevId, g as getBuiltinProviderModelRecords, h as getBuiltinProviderImageModelRecords, i as listEnabledProviders, j as isContextManagementEnabledForMessages, k as getSmallModel, l as getLocalProviderModelRecords, m as builtinProviderModelRegistry, n as getRawProviderConfig, o as resolveEffectiveProviderType, p as usesModelsDevModelCatalog, s as resolveProviderConfigForModel, t as getProviderConfig, u as getProviderModelConfig, v as IMAGE_PROVIDER_PRIORITY, x as getModelsDevModelMaxOutputTokens, y as getDefaultImageModel } from "./config-B55PwJzQ.js";
3
+ import { $ as buildCodexRequestHeaders, B as getXaiAccessToken, C as copilotHeaders, D as prepareForCompact, E as isOpencodeOauthApp, F as compactTextOnlyGuard, I as createAuthMiddleware, J as readGitHubTokenFromEnv, L as getConfiguredAdminApiKeys, M as compactMessageSections, O as prepareInteractionHeaders, P as compactSystemPromptStarts, Q as CODEX_API_BASE_URL, S as copilotBaseUrl, T as copilotWebSocketHeaders, U as readXaiCredentials, Y as XAI_API_BASE_URL, _ as isAsyncIterable, at as setPluginRequestActualModel, b as parseUserIdMetadata, ct as createResponsesSafeStream, d as generateRequestIdFromPayload, dt as createPooledWebSocketStream, et as forwardCodexResponses, f as getPromptCacheKey, ft as createWebSocketUrl, g as getUUID, h as getSessionAffinityFromHeaders, it as resolveTraceId$1, j as compactAutoContinuePromptStarts, k as prepareMessageProxyHeaders, lt as encodePoolKeyPart, m as getRootSessionIdFromHeaders, mt as stripInternalRequestHeaders, n as invalidateCodexRuntime, nt as generateTraceId, ot as fetchUpstreamWithLifecycle, p as getRootSessionId, q as readGitHubToken, rt as requestContext, s as setupCodexToken, st as createResponsesHttpEventStream, tt as formatPluginActualModel, u as stopCopilotRefreshLoop, ut as getResponsesStreamTerminalDisposition, v as isNullish, x as getCopilotUsage, y as isResponsesStream } from "./token-DFlvOMh2.js";
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-C8AWr-Gk.js";
6
- import { i as registerProcessCleanup, n as stopModelsRefreshLoop, r as EventBus, t as setupCopilotRuntime } from "./copilot-runtime-DvmA6ALD.js";
5
+ import { n as isProviderAgentModelVisible, t as getProviderAgentModels } from "./provider-management-C6ZgkTJ7.js";
6
+ import { i as registerProcessCleanup, n as stopModelsRefreshLoop, r as EventBus, t as setupCopilotRuntime } from "./copilot-runtime-DNKNavLK.js";
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";
@@ -21,6 +21,9 @@ import { streamSSE } from "hono/streaming";
21
21
  import { Busboy } from "@fastify/busboy";
22
22
  import { Readable, Transform } from "node:stream";
23
23
  import { pipeline } from "node:stream/promises";
24
+ import { none } from "stream-chain/core";
25
+ import { jsonParser } from "stream-json/core/parser.js";
26
+ import stringer from "stream-json/core/stringer.js";
24
27
  import { decompress } from "fzstd";
25
28
  // src/lib/trace.ts
26
29
  const traceIdMiddleware = async (c, next) => {
@@ -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
- `).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.first_token_ms, event.duration_ms, event.request_outcome, event.user_agent);
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
- if (c.req.method !== "POST" || !TRACKED.test(c.req.path)) return await next();
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
- plugins?.publish("request.first_token", {
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
- plugins?.publish("request.end", {
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 (plugins?.enabled) store.pluginRequest = {
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
- plugins.publish("request.model", info);
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
- plugins.publish("request.actual_model", info);
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) plugins.publish("request.session", info);
992
+ if (!finished) requestPlugins.publish("request.session", info);
980
993
  },
981
994
  recordFirstToken: timing.recordFirstToken,
982
- recordUsage: (event) => plugins.publish("usage.recorded", {
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
- plugins?.publish("request.start", info);
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() === "openrouter") {
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()),
@@ -3821,7 +3887,7 @@ var XaiImagesRequestError = class extends Error {
3821
3887
  this.name = "XaiImagesRequestError";
3822
3888
  }
3823
3889
  };
3824
- function isRecord$3(value) {
3890
+ function isRecord$5(value) {
3825
3891
  return value !== null && typeof value === "object" && !Array.isArray(value);
3826
3892
  }
3827
3893
  function readPrompt(value) {
@@ -3930,7 +3996,7 @@ function readImageReference(value) {
3930
3996
  type: "image_url",
3931
3997
  url: normalizeImageUrl(value)
3932
3998
  };
3933
- if (!isRecord$3(value)) throw new XaiImagesRequestError("image must be a URL or an image object");
3999
+ if (!isRecord$5(value)) throw new XaiImagesRequestError("image must be a URL or an image object");
3934
4000
  if (typeof value.url === "string") return {
3935
4001
  type: "image_url",
3936
4002
  url: normalizeImageUrl(value.url)
@@ -3975,7 +4041,7 @@ function buildEditPayload(model, prompt, images, source) {
3975
4041
  * request. The configured model replaces the client model.
3976
4042
  */
3977
4043
  function translateXaiImageGeneration(payload, model) {
3978
- if (!isRecord$3(payload)) throw new XaiImagesRequestError("Image generation body must be a JSON object");
4044
+ if (!isRecord$5(payload)) throw new XaiImagesRequestError("Image generation body must be a JSON object");
3979
4045
  const translated = {
3980
4046
  model,
3981
4047
  prompt: readPrompt(payload.prompt)
@@ -3987,7 +4053,7 @@ function translateXaiImageGeneration(payload, model) {
3987
4053
  * Translates an OpenAI JSON image edit body into an xAI Imagine edit request.
3988
4054
  */
3989
4055
  function translateXaiImageEditJson(payload, model) {
3990
- if (!isRecord$3(payload)) throw new XaiImagesRequestError("Image edit body must be a JSON object");
4056
+ if (!isRecord$5(payload)) throw new XaiImagesRequestError("Image edit body must be a JSON object");
3991
4057
  return buildEditPayload(model, readPrompt(payload.prompt), collectJsonImages(payload).map((image) => readImageReference(image)), payload);
3992
4058
  }
3993
4059
  const MAX_RESPONSE_IMAGE_BYTES = 33554432;
@@ -4080,15 +4146,15 @@ async function ensureImageResponseB64Json(response, fetchImage = globalThis.fetc
4080
4146
  } catch {
4081
4147
  return rebuildJsonResponse(response, body);
4082
4148
  }
4083
- if (!isRecord$3(payload) || !Array.isArray(payload.data)) return rebuildJsonResponse(response, body);
4149
+ if (!isRecord$5(payload) || !Array.isArray(payload.data)) return rebuildJsonResponse(response, body);
4084
4150
  let changed = false;
4085
4151
  const data = [];
4086
4152
  for (const item of payload.data) {
4087
- if (isRecord$3(item) && onImageUrl) {
4153
+ if (isRecord$5(item) && onImageUrl) {
4088
4154
  const outputUrl = imageOutputUrlForLog(item.url);
4089
4155
  if (outputUrl) onImageUrl(outputUrl);
4090
4156
  }
4091
- if (!isRecord$3(item) || responseFormat === "url" || responseHasB64Json(item.b64_json)) {
4157
+ if (!isRecord$5(item) || responseFormat === "url" || responseHasB64Json(item.b64_json)) {
4092
4158
  data.push(item);
4093
4159
  continue;
4094
4160
  }
@@ -4223,7 +4289,7 @@ async function parseMultipartImageEdit(request) {
4223
4289
  const headers = new Headers(requestHeaders);
4224
4290
  headers.delete("content-length");
4225
4291
  let body;
4226
- if (provider === "codex" || provider === "xai") {
4292
+ if (provider === "codex" || provider === "xai" || provider === "github-copilot") {
4227
4293
  const payload = {
4228
4294
  ...await readPayload(),
4229
4295
  ...model === void 0 ? {} : { model }
@@ -4246,6 +4312,603 @@ async function parseMultipartImageEdit(request) {
4246
4312
  }
4247
4313
  };
4248
4314
  }
4315
+ // src/lib/image-json-files.ts
4316
+ const IMAGE_JSON_MAX_BYTES = 536870912;
4317
+ function imageJsonLimitError() {
4318
+ return new HTTPError("Image JSON upload exceeds the staging limit", Response.json({ error: {
4319
+ message: "Image JSON upload exceeds the staging limit",
4320
+ type: "invalid_request_error"
4321
+ } }, { status: 413 }));
4322
+ }
4323
+ /** Owns only an mkdtemp-created directory; callers never supply deletion paths. */
4324
+ async function createImageJsonFiles(ttlMs = 9e5) {
4325
+ const directory = await mkdtemp(join(tmpdir(), "copilot-image-json-"));
4326
+ let index = 0;
4327
+ let timer;
4328
+ let cleanupPromise;
4329
+ const unregister = registerProcessCleanup(() => {
4330
+ if (timer) clearTimeout(timer);
4331
+ rmSync(directory, {
4332
+ recursive: true,
4333
+ force: true
4334
+ });
4335
+ });
4336
+ const cleanup = () => {
4337
+ cleanupPromise ??= (async () => {
4338
+ if (timer) clearTimeout(timer);
4339
+ await rm(directory, {
4340
+ recursive: true,
4341
+ force: true,
4342
+ maxRetries: 3,
4343
+ retryDelay: 20
4344
+ });
4345
+ unregister();
4346
+ })();
4347
+ return cleanupPromise;
4348
+ };
4349
+ return {
4350
+ directory,
4351
+ cleanup,
4352
+ scheduleCleanup() {
4353
+ if (timer || cleanupPromise) return;
4354
+ timer = setTimeout(() => void cleanup().catch(() => {}), ttlMs);
4355
+ timer.unref();
4356
+ },
4357
+ async create() {
4358
+ const path = join(directory, String(index++));
4359
+ return {
4360
+ path,
4361
+ handle: await open(path, "wx")
4362
+ };
4363
+ }
4364
+ };
4365
+ }
4366
+ async function* readImageJsonFile(path, signal) {
4367
+ const stream = createReadStream(path, {
4368
+ highWaterMark: 65536,
4369
+ signal
4370
+ });
4371
+ try {
4372
+ for await (const bytes of stream) yield bytes;
4373
+ } finally {
4374
+ stream.destroy();
4375
+ }
4376
+ }
4377
+ // src/lib/streaming-json.ts
4378
+ const JSON_CHUNK_BYTES = 65536;
4379
+ /** Never ask the tokenizer to accumulate untrusted strings, keys, or numbers. */
4380
+ var StreamingJsonReader = class {
4381
+ decode = new TextDecoder("utf-8", { fatal: true });
4382
+ parse = jsonParser({ packValues: false });
4383
+ depth = 0;
4384
+ scalarLength = 0;
4385
+ boundedScalar = false;
4386
+ read(bytes) {
4387
+ const text = this.decode.decode(bytes, { stream: bytes !== void 0 });
4388
+ const result = this.parse(text);
4389
+ const tokens = result === none ? [] : result.values;
4390
+ if (bytes === void 0) {
4391
+ const last = this.parse(none);
4392
+ if (last !== none) tokens.push(...last.values);
4393
+ }
4394
+ for (const token of tokens) switch (token.name) {
4395
+ case "startObject":
4396
+ case "startArray":
4397
+ if (++this.depth > 64) throw new SyntaxError("JSON nesting exceeds 64");
4398
+ break;
4399
+ case "endObject":
4400
+ case "endArray":
4401
+ this.depth--;
4402
+ break;
4403
+ case "startKey":
4404
+ case "startNumber":
4405
+ this.scalarLength = 0;
4406
+ this.boundedScalar = true;
4407
+ break;
4408
+ case "endKey":
4409
+ case "endNumber":
4410
+ this.boundedScalar = false;
4411
+ break;
4412
+ case "stringChunk":
4413
+ case "numberChunk": if (this.boundedScalar) {
4414
+ this.scalarLength += token.value.length;
4415
+ if (this.scalarLength > 65536) throw new SyntaxError("JSON key or number is too large");
4416
+ }
4417
+ }
4418
+ return tokens;
4419
+ }
4420
+ };
4421
+ /** Splitting also bounds tokenizer allocations when a runtime supplies a huge chunk. */
4422
+ async function* readJsonBytes(body, signal) {
4423
+ if (!body) return;
4424
+ const reader = body.getReader();
4425
+ const abort = () => void reader.cancel(signal?.reason).catch(() => {});
4426
+ signal?.addEventListener("abort", abort, { once: true });
4427
+ let complete = false;
4428
+ try {
4429
+ while (true) {
4430
+ signal?.throwIfAborted();
4431
+ const { done, value } = await reader.read();
4432
+ signal?.throwIfAborted();
4433
+ if (done) {
4434
+ complete = true;
4435
+ return;
4436
+ }
4437
+ for (let offset = 0; offset < value.byteLength; offset += JSON_CHUNK_BYTES) yield value.subarray(offset, offset + JSON_CHUNK_BYTES);
4438
+ }
4439
+ } finally {
4440
+ signal?.removeEventListener("abort", abort);
4441
+ if (!complete) await reader.cancel().catch(() => {});
4442
+ reader.releaseLock();
4443
+ }
4444
+ }
4445
+ function createJsonByteSource(body, signal) {
4446
+ const controller = new AbortController();
4447
+ const bytes = readJsonBytes(body, AbortSignal.any([signal, controller.signal]));
4448
+ return {
4449
+ bytes,
4450
+ async cancel(reason) {
4451
+ controller.abort(reason);
4452
+ await bytes.return(void 0).catch(() => {});
4453
+ if (body && !body.locked) await body.cancel(reason).catch(() => {});
4454
+ }
4455
+ };
4456
+ }
4457
+ /** Lazy streams allow a route to choose the original or rebuilt body without teeing. */
4458
+ function jsonByteStream(iterator, cancel) {
4459
+ return new ReadableStream({
4460
+ async pull(controller) {
4461
+ try {
4462
+ const { done, value } = await iterator.next();
4463
+ if (done) controller.close();
4464
+ else controller.enqueue(value);
4465
+ } catch (error) {
4466
+ controller.error(error);
4467
+ await iterator.return(void 0).catch(() => {});
4468
+ }
4469
+ },
4470
+ async cancel(reason) {
4471
+ await cancel?.(reason);
4472
+ await iterator.return(void 0);
4473
+ }
4474
+ }, { highWaterMark: 0 });
4475
+ }
4476
+ var DuplicateImageModelError = class extends SyntaxError {
4477
+ constructor() {
4478
+ super("Duplicate top-level image model");
4479
+ }
4480
+ };
4481
+ /** Keeps only the top-level model; all image string chunks are discarded. */
4482
+ var ImageModelReader = class {
4483
+ model;
4484
+ object = false;
4485
+ complete = false;
4486
+ invalidModel = false;
4487
+ depth = 0;
4488
+ key = "";
4489
+ inKey = false;
4490
+ readingModel = false;
4491
+ modelText = "";
4492
+ seenModel = false;
4493
+ read(tokens) {
4494
+ for (const token of tokens) {
4495
+ if (token.name === "startKey") {
4496
+ this.inKey = true;
4497
+ this.key = "";
4498
+ } else if (token.name === "endKey") {
4499
+ this.inKey = false;
4500
+ if (this.depth === 1 && this.key === "model") {
4501
+ if (this.seenModel) throw new DuplicateImageModelError();
4502
+ this.seenModel = true;
4503
+ }
4504
+ } else if (token.name === "stringChunk") {
4505
+ if (this.inKey) this.key += token.value;
4506
+ else if (this.readingModel) {
4507
+ this.modelText += token.value;
4508
+ if (this.modelText.length > 4096) {
4509
+ this.invalidModel = true;
4510
+ this.readingModel = false;
4511
+ this.modelText = "";
4512
+ }
4513
+ }
4514
+ } else if (token.name === "startString") this.readingModel = this.depth === 1 && this.key === "model";
4515
+ else if (token.name === "endString") {
4516
+ if (this.readingModel) {
4517
+ this.model = this.modelText;
4518
+ this.modelText = "";
4519
+ this.readingModel = false;
4520
+ }
4521
+ } else if (this.depth === 1 && this.key === "model" && [
4522
+ "startObject",
4523
+ "startArray",
4524
+ "startNumber",
4525
+ "nullValue",
4526
+ "trueValue",
4527
+ "falseValue"
4528
+ ].includes(token.name)) this.invalidModel = true;
4529
+ if (token.name === "startObject" || token.name === "startArray") {
4530
+ if (this.depth === 0) this.object = token.name === "startObject";
4531
+ this.depth++;
4532
+ } else if (token.name === "endObject" || token.name === "endArray") {
4533
+ if (--this.depth === 0) this.complete = true;
4534
+ }
4535
+ }
4536
+ }
4537
+ };
4538
+ /** Re-encodes bounded chunks, replacing or inserting the top-level model. */
4539
+ async function* rewriteImageModel(input, model, insertModel = false) {
4540
+ const parser = new StreamingJsonReader();
4541
+ const encode = new TextEncoder();
4542
+ const stringify = stringer();
4543
+ let depth = 0;
4544
+ let key = "";
4545
+ let inKey = false;
4546
+ let replacing = false;
4547
+ const process = (tokens) => {
4548
+ const parts = [];
4549
+ const emit = (token) => {
4550
+ const text = stringify(token);
4551
+ if (text !== none) parts.push(text);
4552
+ };
4553
+ for (const token of tokens) {
4554
+ if (insertModel && depth === 0 && token.name === "startObject") {
4555
+ emit(token);
4556
+ emit({ name: "startKey" });
4557
+ emit({
4558
+ name: "stringChunk",
4559
+ value: "model"
4560
+ });
4561
+ emit({ name: "endKey" });
4562
+ emit({ name: "startString" });
4563
+ emit({
4564
+ name: "stringChunk",
4565
+ value: model
4566
+ });
4567
+ emit({ name: "endString" });
4568
+ depth++;
4569
+ continue;
4570
+ }
4571
+ if (replacing) {
4572
+ if (token.name === "endString") replacing = false;
4573
+ continue;
4574
+ }
4575
+ if (token.name === "startKey") {
4576
+ inKey = true;
4577
+ key = "";
4578
+ } else if (token.name === "endKey") inKey = false;
4579
+ else if (token.name === "stringChunk" && inKey) key += token.value;
4580
+ if (token.name === "startString" && depth === 1 && key === "model") {
4581
+ emit(token);
4582
+ emit({
4583
+ name: "stringChunk",
4584
+ value: model
4585
+ });
4586
+ emit({ name: "endString" });
4587
+ replacing = true;
4588
+ continue;
4589
+ }
4590
+ emit(token);
4591
+ if (token.name === "startObject" || token.name === "startArray") depth++;
4592
+ else if (token.name === "endObject" || token.name === "endArray") depth--;
4593
+ }
4594
+ return encode.encode(parts.join(""));
4595
+ };
4596
+ for await (const bytes of input) yield process(parser.read(bytes));
4597
+ const tail = process(parser.read());
4598
+ if (tail.byteLength) yield tail;
4599
+ }
4600
+ // src/lib/image-json-request.ts
4601
+ function rebuild(request, body, changed = false) {
4602
+ const headers = new Headers(request.headers);
4603
+ if (changed) headers.delete("content-length");
4604
+ return new Request(request.url, {
4605
+ method: request.method,
4606
+ headers,
4607
+ body,
4608
+ signal: request.signal,
4609
+ duplex: "half"
4610
+ });
4611
+ }
4612
+ /** Read at most 64 KiB before forwarding a model-first request. Late models use disk. */
4613
+ async function prepareImageJsonRequest(request, maxStagedBytes = IMAGE_JSON_MAX_BYTES) {
4614
+ const forwardingAbort = new AbortController();
4615
+ const signal = AbortSignal.any([request.signal, forwardingAbort.signal]);
4616
+ const input = createJsonByteSource(request.body, signal);
4617
+ const bytes = input.bytes;
4618
+ const parser = new StreamingJsonReader();
4619
+ const metadata = new ImageModelReader();
4620
+ let valid = true;
4621
+ const scan = (chunk) => {
4622
+ if (!valid) return;
4623
+ try {
4624
+ metadata.read(parser.read(chunk));
4625
+ } catch (error) {
4626
+ if (error instanceof DuplicateImageModelError) throw error;
4627
+ valid = false;
4628
+ }
4629
+ };
4630
+ const prefix = [];
4631
+ let length = 0;
4632
+ let eof = false;
4633
+ let pending;
4634
+ async function* remaining() {
4635
+ if (pending) {
4636
+ const chunk = pending;
4637
+ pending = void 0;
4638
+ yield chunk;
4639
+ }
4640
+ yield* bytes;
4641
+ }
4642
+ let files;
4643
+ let stagedPath;
4644
+ try {
4645
+ while (length < JSON_CHUNK_BYTES) {
4646
+ const next = await bytes.next();
4647
+ if (next.done) {
4648
+ eof = true;
4649
+ scan();
4650
+ break;
4651
+ }
4652
+ const chunk = next.value.subarray(0, JSON_CHUNK_BYTES - length);
4653
+ pending = chunk.byteLength < next.value.byteLength ? next.value.subarray(chunk.byteLength) : void 0;
4654
+ prefix.push(chunk);
4655
+ length += chunk.byteLength;
4656
+ scan(chunk);
4657
+ if (!valid || metadata.model !== void 0 || metadata.invalidModel || metadata.complete) break;
4658
+ }
4659
+ if (valid && !eof && !metadata.complete && metadata.model === void 0 && !metadata.invalidModel) {
4660
+ files = await createImageJsonFiles();
4661
+ const file = await files.create();
4662
+ stagedPath = file.path;
4663
+ try {
4664
+ for (const chunk of prefix) {
4665
+ if (length > maxStagedBytes) throw imageJsonLimitError();
4666
+ await file.handle.writeFile(chunk);
4667
+ }
4668
+ prefix.length = 0;
4669
+ for await (const chunk of remaining()) {
4670
+ length += chunk.byteLength;
4671
+ if (length > maxStagedBytes) throw imageJsonLimitError();
4672
+ scan(chunk);
4673
+ await file.handle.writeFile(chunk);
4674
+ }
4675
+ eof = true;
4676
+ scan();
4677
+ } finally {
4678
+ await file.handle.close();
4679
+ }
4680
+ files.scheduleCleanup();
4681
+ }
4682
+ } catch (error) {
4683
+ await input.cancel(error);
4684
+ await files?.cleanup();
4685
+ if (error instanceof DuplicateImageModelError) throw new HTTPError(error.message, Response.json({ error: {
4686
+ message: error.message,
4687
+ type: "invalid_request_error"
4688
+ } }, { status: 400 }));
4689
+ throw error;
4690
+ }
4691
+ let claimed = false;
4692
+ const abort = () => {
4693
+ input.cancel().catch(() => {});
4694
+ files?.cleanup().catch(() => {});
4695
+ };
4696
+ request.signal.addEventListener("abort", abort, { once: true });
4697
+ async function* source() {
4698
+ if (claimed) throw new Error("Image request body already forwarded");
4699
+ claimed = true;
4700
+ try {
4701
+ signal.throwIfAborted();
4702
+ if (stagedPath) yield* readImageJsonFile(stagedPath, signal);
4703
+ else {
4704
+ yield* prefix;
4705
+ prefix.length = 0;
4706
+ if (!eof) {
4707
+ for await (const chunk of remaining()) {
4708
+ scan(chunk);
4709
+ yield chunk;
4710
+ }
4711
+ scan();
4712
+ }
4713
+ }
4714
+ } finally {
4715
+ request.signal.removeEventListener("abort", abort);
4716
+ await bytes.return(void 0).catch(() => {});
4717
+ await files?.cleanup();
4718
+ }
4719
+ }
4720
+ const cancel = async (reason) => {
4721
+ forwardingAbort.abort(reason);
4722
+ request.signal.removeEventListener("abort", abort);
4723
+ await input.cancel(reason);
4724
+ await files?.cleanup();
4725
+ };
4726
+ return {
4727
+ valid: valid && metadata.object && !metadata.invalidModel,
4728
+ model: metadata.model,
4729
+ originalRequest: rebuild(request, jsonByteStream(source(), cancel)),
4730
+ createRequest: (model) => rebuild(request, jsonByteStream(rewriteImageModel(source(), model, metadata.model === void 0), cancel), true),
4731
+ cleanup: cancel,
4732
+ stagedDirectory: files?.directory
4733
+ };
4734
+ }
4735
+ // src/lib/image-json-metadata.ts
4736
+ const MAX_METADATA_LENGTH = 65536;
4737
+ const OMITTED_FIELDS = /* @__PURE__ */ new Set([
4738
+ "data",
4739
+ "b64_json",
4740
+ "partial_image_b64"
4741
+ ]);
4742
+ /** Reads JSON/SSE object metadata without retaining image payloads. */
4743
+ var ImageJsonMetadataReader = class {
4744
+ depth = 0;
4745
+ inString = false;
4746
+ escaped = false;
4747
+ phase = "key";
4748
+ keyText = "";
4749
+ key = "";
4750
+ value = null;
4751
+ fields = /* @__PURE__ */ new Map();
4752
+ metadataLength = 0;
4753
+ onMetadata;
4754
+ constructor(onMetadata) {
4755
+ this.onMetadata = onMetadata;
4756
+ }
4757
+ write(text) {
4758
+ for (const char of text) this.readCharacter(char);
4759
+ }
4760
+ readCharacter(char) {
4761
+ if (this.depth === 0) {
4762
+ if (char === "{") {
4763
+ this.depth = 1;
4764
+ this.phase = "key";
4765
+ this.fields.clear();
4766
+ this.metadataLength = 0;
4767
+ }
4768
+ return;
4769
+ }
4770
+ if (!this.inString && this.depth === 1) {
4771
+ if (char === "}") {
4772
+ if (this.phase === "value") this.finishField();
4773
+ this.depth = 0;
4774
+ this.onMetadata(Object.fromEntries(this.fields));
4775
+ return;
4776
+ }
4777
+ if (this.phase === "key" && char === "\"") {
4778
+ this.keyText = "\"";
4779
+ this.phase = "colon";
4780
+ this.inString = true;
4781
+ return;
4782
+ }
4783
+ if (this.phase === "colon" && char === ":") {
4784
+ try {
4785
+ this.key = JSON.parse(this.keyText);
4786
+ } catch {
4787
+ this.key = "";
4788
+ }
4789
+ this.value = OMITTED_FIELDS.has(this.key) ? null : "";
4790
+ this.phase = "value";
4791
+ return;
4792
+ }
4793
+ if (this.phase === "value" && char === ",") {
4794
+ this.finishField();
4795
+ this.phase = "key";
4796
+ return;
4797
+ }
4798
+ }
4799
+ if (this.phase === "colon" && this.keyText.length < MAX_METADATA_LENGTH) this.keyText += char;
4800
+ else if (this.phase === "value" && this.value !== null) {
4801
+ if (this.metadataLength + this.value.length < MAX_METADATA_LENGTH) this.value += char;
4802
+ else this.value = null;
4803
+ }
4804
+ if (this.inString) {
4805
+ if (this.escaped) this.escaped = false;
4806
+ else if (char === "\\") this.escaped = true;
4807
+ else if (char === "\"") this.inString = false;
4808
+ } else if (char === "\"") this.inString = true;
4809
+ else if (char === "{" || char === "[") this.depth++;
4810
+ else if (char === "}" || char === "]") this.depth--;
4811
+ }
4812
+ finishField() {
4813
+ if (this.value === null) return;
4814
+ try {
4815
+ const value = JSON.parse(this.value);
4816
+ this.fields.set(this.key, value);
4817
+ this.metadataLength += this.key.length + this.value.length;
4818
+ } catch {}
4819
+ this.value = null;
4820
+ }
4821
+ };
4822
+ // src/lib/token-usage/images.ts
4823
+ function isRecord$4(value) {
4824
+ return value !== null && typeof value === "object" && !Array.isArray(value);
4825
+ }
4826
+ function token(value) {
4827
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? normalizeToken(value) : void 0;
4828
+ }
4829
+ function tokenDetails(value) {
4830
+ if (!isRecord$4(value)) return void 0;
4831
+ return {
4832
+ image_tokens: token(value.image_tokens),
4833
+ text_tokens: token(value.text_tokens)
4834
+ };
4835
+ }
4836
+ function normalizeImagesUsage(usage, provider) {
4837
+ if (!isRecord$4(usage)) return {};
4838
+ const inputDetails = tokenDetails(usage.input_tokens_details);
4839
+ const outputDetails = tokenDetails(usage.output_tokens_details);
4840
+ return {
4841
+ cost: provider.toLowerCase() === "xai" ? xaiUsdTicksToCost(usage.cost_in_usd_ticks) : void 0,
4842
+ input_tokens: token(usage.input_tokens) ?? (inputDetails?.image_tokens ?? 0) + (inputDetails?.text_tokens ?? 0),
4843
+ input_tokens_details: inputDetails,
4844
+ output_tokens: token(usage.output_tokens) ?? (outputDetails?.image_tokens ?? 0) + (outputDetails?.text_tokens ?? 0),
4845
+ output_tokens_details: outputDetails,
4846
+ total_tokens: token(usage.total_tokens)
4847
+ };
4848
+ }
4849
+ /** Observes bytes as the client reads them; image data is never buffered here. */
4850
+ function observeImagesResponse(response, options) {
4851
+ if (!response.body) return createProviderProxyResponse(response);
4852
+ const context = requestContext.getStore();
4853
+ const provider = options.providerConfig;
4854
+ const recordUsage = createTokenUsageRecorder({
4855
+ source: provider.name === "github-copilot" ? "copilot" : "provider",
4856
+ endpoint: options.operation === "edits" ? "images_edits" : "images_generations",
4857
+ model: options.model,
4858
+ pricing: provider.models?.[options.model]?.pricing,
4859
+ pricingCurrency: provider.pricingCurrency,
4860
+ providerName: provider.name
4861
+ });
4862
+ let usage = {};
4863
+ let finished = false;
4864
+ const decoder = new TextDecoder();
4865
+ const metadataReader = new ImageJsonMetadataReader((metadata) => {
4866
+ if (context) requestContext.run(context, () => options.onMetadata(metadata));
4867
+ else options.onMetadata(metadata);
4868
+ if (response.ok) {
4869
+ if (isRecord$4(metadata.usage)) usage = {
4870
+ ...usage,
4871
+ ...normalizeImagesUsage(metadata.usage, provider.name)
4872
+ };
4873
+ if (provider.name === "github-copilot" && isRecord$4(metadata.copilot_usage)) {
4874
+ const totalNanoAiu = token(metadata.copilot_usage.total_nano_aiu);
4875
+ if (totalNanoAiu !== void 0) usage.total_nano_aiu = totalNanoAiu;
4876
+ }
4877
+ }
4878
+ });
4879
+ const finish = () => {
4880
+ if (finished) return;
4881
+ finished = true;
4882
+ if (context) requestContext.run(context, () => recordUsage(usage));
4883
+ else recordUsage(usage);
4884
+ };
4885
+ const reader = response.body.getReader();
4886
+ return createProviderProxyResponse(response, new ReadableStream({
4887
+ async pull(controller) {
4888
+ try {
4889
+ const next = await reader.read();
4890
+ if (next.done) {
4891
+ metadataReader.write(decoder.decode());
4892
+ finish();
4893
+ reader.releaseLock();
4894
+ controller.close();
4895
+ } else {
4896
+ metadataReader.write(decoder.decode(next.value, { stream: true }));
4897
+ controller.enqueue(next.value);
4898
+ }
4899
+ } catch (error) {
4900
+ finish();
4901
+ reader.releaseLock();
4902
+ controller.error(error);
4903
+ }
4904
+ },
4905
+ async cancel(reason) {
4906
+ finish();
4907
+ await reader.cancel(reason);
4908
+ reader.releaseLock();
4909
+ }
4910
+ }));
4911
+ }
4249
4912
  // src/lib/xai-image-model.ts
4250
4913
  /**
4251
4914
  * Resolves one image-route model setting. Empty values keep the OpenAI
@@ -4275,35 +4938,459 @@ function getImageGenerationModel() {
4275
4938
  function getImageEditModel() {
4276
4939
  return resolveConfiguredXaiImageModel(getConfig().imageEditModel, "imageEditModel");
4277
4940
  }
4941
+ // src/lib/image-json-value.ts
4942
+ const STRING_LIMIT = 65536;
4943
+ const METADATA_LIMIT = 1048576;
4944
+ /** Builds bounded metadata, optionally storing large decoded strings on disk. */
4945
+ var ImageJsonValueReader = class {
4946
+ value;
4947
+ external = [];
4948
+ stack = [];
4949
+ key = "";
4950
+ inKey = false;
4951
+ number = "";
4952
+ string;
4953
+ metadataSize = 0;
4954
+ files;
4955
+ externalize;
4956
+ constructor(externalize = false) {
4957
+ this.externalize = externalize;
4958
+ }
4959
+ charge(size) {
4960
+ this.metadataSize += size;
4961
+ if (this.metadataSize > METADATA_LIMIT) throw new SyntaxError("Image JSON metadata exceeds 1 MiB");
4962
+ }
4963
+ add(value) {
4964
+ this.charge(16);
4965
+ const frame = this.stack.at(-1);
4966
+ if (!frame) this.value = value;
4967
+ else if (Array.isArray(frame.value)) frame.value.push(value);
4968
+ else frame.value[frame.key] = value;
4969
+ }
4970
+ field() {
4971
+ const frame = this.stack.at(-1);
4972
+ return frame ? Array.isArray(frame.value) ? frame.field : frame.key : "";
4973
+ }
4974
+ async append(text) {
4975
+ const value = this.string;
4976
+ if (!value.external && value.text.length + text.length <= STRING_LIMIT) {
4977
+ value.text += text;
4978
+ return;
4979
+ }
4980
+ if (!this.externalize) {
4981
+ value.text = "[large string omitted]";
4982
+ value.external ??= {
4983
+ placeholder: value.text,
4984
+ path: "",
4985
+ mode: value.mode,
4986
+ first: 0,
4987
+ end: 0,
4988
+ length: 0,
4989
+ prefix: "",
4990
+ invalidBase64: false,
4991
+ padding: 0
4992
+ };
4993
+ return;
4994
+ }
4995
+ if (!value.external) {
4996
+ if (this.external.length >= 64) throw new SyntaxError("Too many large image JSON strings");
4997
+ this.files ??= await createImageJsonFiles();
4998
+ const file = await this.files.create();
4999
+ value.external = {
5000
+ placeholder: randomBytes(24).toString("hex"),
5001
+ path: file.path,
5002
+ mode: value.mode,
5003
+ first: -1,
5004
+ end: 0,
5005
+ length: 0,
5006
+ prefix: "",
5007
+ invalidBase64: false,
5008
+ padding: 0
5009
+ };
5010
+ value.handle = file.handle;
5011
+ this.external.push(value.external);
5012
+ this.track(value.text);
5013
+ value.pending.push(value.text);
5014
+ value.text = "";
5015
+ }
5016
+ this.track(text);
5017
+ value.pending.push(text);
5018
+ }
5019
+ track(text) {
5020
+ const value = this.string.external;
5021
+ const first = text.search(/\S/u);
5022
+ if (first !== -1) {
5023
+ if (value.first === -1) value.first = value.length + first;
5024
+ value.end = value.length + text.trimEnd().length;
5025
+ }
5026
+ value.length += text.length;
5027
+ const normalized = value.mode === "compact" ? text.replaceAll(/\s+/gu, "") : value.prefix ? text : text.trimStart();
5028
+ if (value.prefix.length < 256) value.prefix = (value.prefix + normalized).slice(0, 256);
5029
+ if (value.mode === "compact") {
5030
+ if (value.padding && /[^=]/u.test(normalized)) value.invalidBase64 = true;
5031
+ if (!/^[a-z0-9+/]*={0,2}$/iu.test(normalized)) value.invalidBase64 = true;
5032
+ value.padding += normalized.length - normalized.replaceAll("=", "").length;
5033
+ if (value.padding > 2) value.invalidBase64 = true;
5034
+ }
5035
+ }
5036
+ async flushString() {
5037
+ const value = this.string;
5038
+ if (value?.handle && value.pending.length) {
5039
+ await value.handle.writeFile(Buffer.from(value.pending.join(""), "utf16le"));
5040
+ value.pending = [];
5041
+ }
5042
+ }
5043
+ async read(tokens) {
5044
+ for (const token of tokens) switch (token.name) {
5045
+ case "startObject":
5046
+ case "startArray": {
5047
+ const field = this.field();
5048
+ const value = token.name === "startArray" ? [] : Object.create(null);
5049
+ this.add(value);
5050
+ this.stack.push({
5051
+ value,
5052
+ key: "",
5053
+ field
5054
+ });
5055
+ break;
5056
+ }
5057
+ case "endObject":
5058
+ case "endArray":
5059
+ this.stack.pop();
5060
+ break;
5061
+ case "startKey":
5062
+ this.inKey = true;
5063
+ this.key = "";
5064
+ break;
5065
+ case "endKey":
5066
+ this.inKey = false;
5067
+ this.charge(this.key.length);
5068
+ this.stack.at(-1).key = this.key;
5069
+ break;
5070
+ case "startString": {
5071
+ const field = this.field();
5072
+ this.string = {
5073
+ text: "",
5074
+ pending: [],
5075
+ mode: field === "b64_json" ? "compact" : [
5076
+ "image",
5077
+ "images",
5078
+ "url",
5079
+ "image_url"
5080
+ ].includes(field) ? "trim" : "identity"
5081
+ };
5082
+ break;
5083
+ }
5084
+ case "stringChunk":
5085
+ if (this.inKey) this.key += token.value;
5086
+ else await this.append(token.value);
5087
+ break;
5088
+ case "endString": {
5089
+ await this.flushString();
5090
+ const value = this.string;
5091
+ await value.handle?.close();
5092
+ value.handle = void 0;
5093
+ if (value.external && this.externalize) {
5094
+ const external = value.external;
5095
+ external.placeholder = (/^(?:https?:\/\/|data:image\/[a-z0-9.+-]+;base64,)/iu.exec(external.prefix)?.[0] ?? "") + external.placeholder;
5096
+ this.add(external.first === -1 ? "" : external.placeholder);
5097
+ } else {
5098
+ this.charge(value.text.length);
5099
+ this.add(value.text);
5100
+ }
5101
+ this.string = void 0;
5102
+ break;
5103
+ }
5104
+ case "startNumber":
5105
+ this.number = "";
5106
+ break;
5107
+ case "numberChunk":
5108
+ this.number += token.value;
5109
+ break;
5110
+ case "endNumber":
5111
+ this.add(Number(this.number));
5112
+ break;
5113
+ case "trueValue":
5114
+ this.add(true);
5115
+ break;
5116
+ case "falseValue":
5117
+ this.add(false);
5118
+ break;
5119
+ case "nullValue": this.add(null);
5120
+ }
5121
+ await this.flushString();
5122
+ }
5123
+ async cleanup() {
5124
+ await this.string?.handle?.close();
5125
+ if (this.string) this.string.handle = void 0;
5126
+ await this.files?.cleanup();
5127
+ }
5128
+ scheduleCleanup() {
5129
+ this.files?.scheduleCleanup();
5130
+ }
5131
+ };
5132
+ /** Reads xAI input into a small skeleton, never assembling large image strings. */
5133
+ async function readExternalImageJson(request, maxBytes = IMAGE_JSON_MAX_BYTES) {
5134
+ const reader = new ImageJsonValueReader(true);
5135
+ const parser = new StreamingJsonReader();
5136
+ let length = 0;
5137
+ try {
5138
+ for await (const chunk of readJsonBytes(request.body, request.signal)) {
5139
+ length += chunk.byteLength;
5140
+ if (length > maxBytes) throw imageJsonLimitError();
5141
+ await reader.read(parser.read(chunk));
5142
+ }
5143
+ await reader.read(parser.read());
5144
+ reader.scheduleCleanup();
5145
+ return reader;
5146
+ } catch (error) {
5147
+ await reader.cleanup();
5148
+ throw error;
5149
+ }
5150
+ }
5151
+ function assertExternalImageBase64(reader, payload) {
5152
+ const text = JSON.stringify(payload);
5153
+ 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");
5154
+ }
5155
+ /** Serializes the translated skeleton, expanding only strings retained by xAI. */
5156
+ function externalImageJsonStream(reader, payload, signal) {
5157
+ const template = JSON.stringify(payload);
5158
+ const pattern = new RegExp(reader.external.map((value) => value.placeholder.replaceAll(/[.*+?^${}()|[\]\\]/gu, "\\$&")).join("|"), "gu");
5159
+ const encode = new TextEncoder();
5160
+ let activeStream;
5161
+ async function* serialize() {
5162
+ try {
5163
+ signal.throwIfAborted();
5164
+ let offset = 0;
5165
+ for (const match of reader.external.length ? template.matchAll(pattern) : []) {
5166
+ signal.throwIfAborted();
5167
+ yield encode.encode(template.slice(offset, match.index));
5168
+ const value = reader.external.find((item) => item.placeholder === match[0]);
5169
+ const trim = value.mode !== "identity";
5170
+ const stream = createReadStream(value.path, {
5171
+ encoding: "utf16le",
5172
+ highWaterMark: 65536,
5173
+ start: trim ? value.first * 2 : 0,
5174
+ end: trim ? value.end * 2 - 1 : void 0
5175
+ });
5176
+ activeStream = stream;
5177
+ try {
5178
+ for await (const chunk of stream) {
5179
+ signal.throwIfAborted();
5180
+ const text = String(chunk);
5181
+ yield encode.encode(JSON.stringify(value.mode === "compact" ? text.replaceAll(/\s+/gu, "") : text).slice(1, -1));
5182
+ }
5183
+ } finally {
5184
+ stream.destroy();
5185
+ activeStream = void 0;
5186
+ }
5187
+ offset = match.index + match[0].length;
5188
+ }
5189
+ signal.throwIfAborted();
5190
+ yield encode.encode(template.slice(offset));
5191
+ } catch (error) {
5192
+ signal.throwIfAborted();
5193
+ throw error;
5194
+ } finally {
5195
+ await reader.cleanup();
5196
+ }
5197
+ }
5198
+ const abort = () => {
5199
+ activeStream?.destroy();
5200
+ reader.cleanup().catch(() => {});
5201
+ };
5202
+ signal.addEventListener("abort", abort, { once: true });
5203
+ if (signal.aborted) abort();
5204
+ async function* body() {
5205
+ try {
5206
+ yield* serialize();
5207
+ } finally {
5208
+ signal.removeEventListener("abort", abort);
5209
+ }
5210
+ }
5211
+ return jsonByteStream(body(), async () => {
5212
+ signal.removeEventListener("abort", abort);
5213
+ activeStream?.destroy();
5214
+ await reader.cleanup();
5215
+ });
5216
+ }
5217
+ // src/routes/images/request-logging.ts
5218
+ function redactImageBase64(value, key = "") {
5219
+ if ([
5220
+ "b64_json",
5221
+ "base64",
5222
+ "image_base64"
5223
+ ].includes(key)) return "[image base64 omitted]";
5224
+ if (typeof value === "string") return /^data:image\/[^,]+;base64,/iu.test(value) ? "[image base64 omitted]" : value;
5225
+ if (Array.isArray(value)) return value.map((item) => redactImageBase64(item));
5226
+ if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([field, item]) => [field, redactImageBase64(item, field)]));
5227
+ return value;
5228
+ }
5229
+ async function editRequestBody(request) {
5230
+ try {
5231
+ const cloned = request.clone();
5232
+ if (request.headers.get("content-type")?.includes("multipart/form-data")) {
5233
+ const fields = [];
5234
+ for (const [name, value] of await cloned.formData()) fields.push({
5235
+ name,
5236
+ value: typeof value === "string" ? redactImageBase64(value, name) : {
5237
+ filename: value.name,
5238
+ mime_type: value.type,
5239
+ size: value.size
5240
+ }
5241
+ });
5242
+ return fields;
5243
+ }
5244
+ return redactImageBase64(await cloned.json());
5245
+ } catch {
5246
+ return "[unavailable JSON body]";
5247
+ }
5248
+ }
5249
+ async function logImageEditRequest(logger, label, request, extra = {}, debug = debugJsonAsync) {
5250
+ if (!state.verbose) return request;
5251
+ if (!request.headers.get("content-type")?.includes("multipart/form-data")) return logImageJsonRequest(logger, label, request, extra, debug);
5252
+ await debug(logger, label, async () => ({
5253
+ contentType: request.headers.get("content-type"),
5254
+ contentLength: request.headers.get("content-length"),
5255
+ ...extra,
5256
+ body: await editRequestBody(request)
5257
+ }));
5258
+ return request;
5259
+ }
5260
+ /** Observes the same chunks sent upstream; debug logging never tees JSON bodies. */
5261
+ function logImageJsonRequest(logger, label, request, extra, debug) {
5262
+ const value = new ImageJsonValueReader();
5263
+ const parser = new StreamingJsonReader();
5264
+ const input = createJsonByteSource(request.body, request.signal);
5265
+ let valid = true;
5266
+ async function* observe() {
5267
+ for await (const chunk of input.bytes) {
5268
+ if (valid) try {
5269
+ await value.read(parser.read(chunk));
5270
+ } catch {
5271
+ valid = false;
5272
+ }
5273
+ yield chunk;
5274
+ }
5275
+ if (valid) try {
5276
+ await value.read(parser.read());
5277
+ } catch {
5278
+ valid = false;
5279
+ }
5280
+ await debug(logger, label, () => Promise.resolve({
5281
+ contentType: request.headers.get("content-type"),
5282
+ contentLength: request.headers.get("content-length"),
5283
+ ...extra,
5284
+ body: valid ? redactImageBase64(value.value) : "[unavailable JSON body]"
5285
+ }));
5286
+ }
5287
+ return new Request(request.url, {
5288
+ method: request.method,
5289
+ headers: request.headers,
5290
+ signal: request.signal,
5291
+ body: jsonByteStream(observe(), (reason) => input.cancel(reason)),
5292
+ duplex: "half"
5293
+ });
5294
+ }
5295
+ DEFAULT_IMAGE_MODELS["github-copilot"];
5296
+ const COPILOT_CLIENT_VERSION = "copilot/1.0.103-unstable.r40031404408.gba6c003";
5297
+ const COPILOT_IMAGES_TIMEOUT_MS = 9e5;
5298
+ const copilotImagesDispatcher = createTimeoutDispatcher(COPILOT_IMAGES_TIMEOUT_MS);
5299
+ function resolveCopilotImagesUrl(requestUrl, operation) {
5300
+ const upstreamUrl = new URL(`${copilotBaseUrl(state).replace(/\/+$/u, "")}/v1/images/${operation}`);
5301
+ if (upstreamUrl.hostname === "api.githubcopilot.com") upstreamUrl.hostname = `api.${state.accountType}.githubcopilot.com`;
5302
+ upstreamUrl.search = new URL(requestUrl).search;
5303
+ return upstreamUrl.toString();
5304
+ }
5305
+ async function forwardCopilotImages(request, operation) {
5306
+ assertGitHubCopilotEnabled();
5307
+ if (!state.githubToken) throw new HTTPError("GitHub token not found", Response.json({ error: {
5308
+ message: "GitHub token not found",
5309
+ type: "authentication_error"
5310
+ } }, { status: 401 }));
5311
+ const init = {
5312
+ method: "POST",
5313
+ headers: {
5314
+ authorization: `Bearer ${state.githubToken}`,
5315
+ "content-type": "application/json",
5316
+ accept: request.headers.get("accept") ?? "application/json",
5317
+ "copilot-integration-id": "vscode-chat",
5318
+ "copilot-harness-id": "copilot-sdk",
5319
+ "editor-version": COPILOT_CLIENT_VERSION,
5320
+ "user-agent": `${COPILOT_CLIENT_VERSION} (${process.platform} unknown) term/unknown client/vscode-agent-host`,
5321
+ "openai-intent": "conversation-agent",
5322
+ "x-initiator": "agent",
5323
+ "x-github-api-version": "2026-08-01",
5324
+ "x-client-machine-id": state.vsCodeDeviceId,
5325
+ "x-interaction-id": randomUUID()
5326
+ },
5327
+ body: request.body,
5328
+ duplex: "half",
5329
+ signal: AbortSignal.any([request.signal, AbortSignal.timeout(COPILOT_IMAGES_TIMEOUT_MS)])
5330
+ };
5331
+ const upstreamUrl = resolveCopilotImagesUrl(request.url, operation);
5332
+ const response = typeof Bun !== "undefined" ? await fetch(upstreamUrl, init) : await fetch$1(upstreamUrl, {
5333
+ ...init,
5334
+ dispatcher: copilotImagesDispatcher
5335
+ });
5336
+ logCopilotRateLimits(response.headers);
5337
+ return response;
5338
+ }
4278
5339
  // src/routes/images/forward-provider-images.ts
4279
5340
  /**
4280
5341
  * Forwards an images request to a configured provider with debug logging.
4281
5342
  * Shared by the top-level images routes (model mapping) and the
4282
5343
  * provider-scoped images routes so both log and proxy identically.
4283
5344
  */
4284
- async function forwardProviderImagesWithLogging(providerConfig, request, operation, { logger, provider }) {
4285
- if (operation === "generations") await debugJsonAsync(logger, `provider.images.${operation}.request`, async () => ({
4286
- body: await request.clone().text(),
4287
- provider
4288
- }));
4289
- else debugJson(logger, `provider.images.${operation}.request`, {
4290
- contentType: request.headers.get("content-type"),
4291
- contentLength: request.headers.get("content-length"),
4292
- provider
4293
- });
4294
- const upstreamResponse = await forwardProviderImages(providerConfig, request, operation);
5345
+ async function forwardProviderImagesWithLogging(providerConfig, request, operation, { logger, provider, model }) {
5346
+ let actualModel = model;
5347
+ if (actualModel === void 0 && !request.headers.get("content-type")?.includes("multipart/form-data")) {
5348
+ const parsed = await prepareImageJsonRequest(request);
5349
+ actualModel = parsed.valid ? parsed.model : "unknown";
5350
+ request = parsed.originalRequest;
5351
+ }
5352
+ actualModel ??= "unknown";
5353
+ request = await logImageEditRequest(logger, `provider.images.${operation}.request`, request, { provider });
5354
+ let upstreamResponse;
5355
+ try {
5356
+ upstreamResponse = providerConfig.name === "github-copilot" ? await forwardCopilotImages(request, operation) : await forwardProviderImages(providerConfig, request, operation);
5357
+ } catch (error) {
5358
+ await request.body?.cancel().catch(() => {});
5359
+ throw error;
5360
+ }
4295
5361
  debugJson(logger, `provider.images.${operation}.response`, {
4296
5362
  contentType: upstreamResponse.headers.get("content-type"),
4297
5363
  contentLength: upstreamResponse.headers.get("content-length"),
4298
5364
  provider,
4299
5365
  statusCode: upstreamResponse.status
4300
5366
  });
4301
- return createProviderProxyResponse(upstreamResponse);
5367
+ return observeImagesResponse(upstreamResponse, {
5368
+ model: actualModel,
5369
+ operation,
5370
+ providerConfig,
5371
+ onMetadata: (metadata) => debugJson(logger, `provider.images.${operation}.response.body`, {
5372
+ provider,
5373
+ ...metadata
5374
+ })
5375
+ });
5376
+ }
5377
+ async function forwardCopilotImagesWithLogging(request, operation, context) {
5378
+ const config = getRawProviderConfig(GITHUB_COPILOT_PROVIDER);
5379
+ return await forwardProviderImagesWithLogging({
5380
+ name: GITHUB_COPILOT_PROVIDER,
5381
+ type: "openai-compatible",
5382
+ authType: "oauth2",
5383
+ apiKey: "",
5384
+ baseUrl: "",
5385
+ models: config?.models,
5386
+ pricingCurrency: config?.pricingCurrency
5387
+ }, request, operation, context);
4302
5388
  }
4303
5389
  /** Forwards translated xAI requests and normalizes OpenAI-compatible output. */
4304
- async function forwardXaiImagesWithLogging(providerConfig, request, operation, payload, { logger, provider }) {
5390
+ async function forwardXaiImagesWithLogging(providerConfig, request, operation, payload, { logger, provider }, body) {
4305
5391
  return await ensureImageResponseB64Json(await forwardProviderImagesWithLogging(providerConfig, new Request(request.url, {
4306
- body: JSON.stringify(payload),
5392
+ body: body ?? new TextEncoder().encode(JSON.stringify(payload)),
5393
+ duplex: "half",
4307
5394
  headers: {
4308
5395
  accept: "application/json",
4309
5396
  "content-type": "application/json"
@@ -4312,7 +5399,8 @@ async function forwardXaiImagesWithLogging(providerConfig, request, operation, p
4312
5399
  signal: request.signal
4313
5400
  }), operation, {
4314
5401
  logger,
4315
- provider
5402
+ provider,
5403
+ model: typeof payload.model === "string" ? payload.model : "unknown"
4316
5404
  }), void 0, {
4317
5405
  clientSignal: request.signal,
4318
5406
  onImageUrl: (url) => {
@@ -4325,6 +5413,7 @@ async function forwardXaiImagesWithLogging(providerConfig, request, operation, p
4325
5413
  });
4326
5414
  }
4327
5415
  // src/services/codex/images.ts
5416
+ const DEFAULT_CODEX_IMAGE_MODEL = DEFAULT_IMAGE_MODELS.codex;
4328
5417
  const CODEX_IMAGES_TIMEOUT_MS = 9e5;
4329
5418
  const codexImagesDispatcher = createTimeoutDispatcher(CODEX_IMAGES_TIMEOUT_MS);
4330
5419
  function resolveCodexImagesUrl(requestUrl, operation) {
@@ -4350,12 +5439,66 @@ async function forwardCodexImages(request, operation) {
4350
5439
  dispatcher: codexImagesDispatcher
4351
5440
  });
4352
5441
  }
5442
+ // src/routes/images/xai-json-request.ts
5443
+ /** Existing translators validate a bounded skeleton; large strings stay on disk. */
5444
+ async function translateXaiImageJsonRequest(request, operation, model) {
5445
+ let json;
5446
+ try {
5447
+ json = await readExternalImageJson(request);
5448
+ } catch (error) {
5449
+ if (error instanceof HTTPError || request.signal.aborted) throw error;
5450
+ throw new XaiImagesRequestError(operation === "edits" ? "Image edit body must be JSON" : "Image generation body must be JSON");
5451
+ }
5452
+ try {
5453
+ const payload = operation === "edits" ? translateXaiImageEditJson(json.value, model) : translateXaiImageGeneration(json.value, model);
5454
+ try {
5455
+ assertExternalImageBase64(json, payload);
5456
+ } catch {
5457
+ throw new XaiImagesRequestError("image b64_json is not valid base64");
5458
+ }
5459
+ const input = json.value;
5460
+ return {
5461
+ payload,
5462
+ body: externalImageJsonStream(json, payload, request.signal),
5463
+ requestedModel: typeof input.model === "string" ? input.model : model
5464
+ };
5465
+ } catch (error) {
5466
+ await json.cleanup();
5467
+ throw error;
5468
+ }
5469
+ }
5470
+ // src/routes/images/default-model.ts
5471
+ /** Resolve the three supported image models before sending the request. */
5472
+ async function resolveImageRequestModel(requestedModel, options = {}) {
5473
+ if (options.provider) {
5474
+ const model = getDefaultImageModel(options.provider);
5475
+ if (model) return model;
5476
+ }
5477
+ const availability = /* @__PURE__ */ new Map();
5478
+ const isAvailable = (provider) => {
5479
+ let pending = availability.get(provider);
5480
+ if (!pending) {
5481
+ pending = provider === "github-copilot" ? Promise.resolve((options.isGitHubCopilotAvailable ?? isGitHubCopilotAvailable)()) : (options.resolveProviderConfig ?? resolveProviderConfig)(provider).then((config) => config !== null);
5482
+ availability.set(provider, pending);
5483
+ }
5484
+ return pending;
5485
+ };
5486
+ const requested = requestedModel?.trim();
5487
+ const selected = IMAGE_PROVIDER_PRIORITY.find((provider) => requested === DEFAULT_IMAGE_MODELS[provider] || requested === `${provider}/${DEFAULT_IMAGE_MODELS[provider]}`);
5488
+ if (selected && await isAvailable(selected)) return `${selected}/${DEFAULT_IMAGE_MODELS[selected]}`;
5489
+ for (const provider of IMAGE_PROVIDER_PRIORITY) if (await isAvailable(provider)) return `${provider}/${DEFAULT_IMAGE_MODELS[provider]}`;
5490
+ throw new HTTPError("No enabled image provider is available", Response.json({ error: {
5491
+ message: "No enabled image provider is available",
5492
+ type: "invalid_request_error"
5493
+ } }, { status: 404 }));
5494
+ }
4353
5495
  // src/routes/images/shared.ts
4354
5496
  const logger$15 = createHandlerLogger("images-handler");
4355
5497
  const imageRouteDependencies = {
4356
5498
  debugJsonAsync,
4357
5499
  getImageEditModel,
4358
5500
  getImageGenerationModel,
5501
+ isGitHubCopilotAvailable,
4359
5502
  resolveMappedModel,
4360
5503
  resolveProviderConfig
4361
5504
  };
@@ -4374,113 +5517,91 @@ function getContentMetadata(headers) {
4374
5517
  };
4375
5518
  }
4376
5519
  /**
4377
- * Snapshots generated request headers before consuming the body.
4378
- */
4379
- function snapshotRequestHeaders(request) {
4380
- return new Headers(request.headers);
4381
- }
4382
- /**
4383
- * Rebuilds a request from primitives instead of cloning: under Node runtimes
4384
- * srvx wraps incoming requests in a proxy class whose prototype chain
4385
- * satisfies `instanceof Request` without the native internals, so
4386
- * `new Request(request)` throws.
4387
- */
4388
- function createForwardRequest(request, requestHeaders, body) {
4389
- const init = {
4390
- body,
4391
- duplex: "half",
4392
- headers: requestHeaders,
4393
- method: request.method,
4394
- signal: request.signal
4395
- };
4396
- return new Request(request.url, init);
4397
- }
4398
- function createJsonImagesRequest(request, requestHeaders, payload, model) {
4399
- const headers = new Headers(requestHeaders);
4400
- headers.delete("content-length");
4401
- return createForwardRequest(request, headers, new TextEncoder().encode(JSON.stringify({
4402
- ...payload,
4403
- model
4404
- })));
4405
- }
4406
- /**
4407
5520
  * Parses an image request body as JSON, retaining the original bytes for
4408
5521
  * unchanged forwarding while allowing model mapping to rebuild the payload.
4409
5522
  * Rebuilt bodies stay binary so Node does not synthesize a text/plain content type.
4410
5523
  */
4411
5524
  async function parseJsonImagesRequest(request) {
4412
- const requestHeaders = snapshotRequestHeaders(request);
4413
- const body = new Uint8Array(await request.arrayBuffer());
4414
- const originalRequest = createForwardRequest(request, requestHeaders, body);
4415
- let payload;
4416
- try {
4417
- const bodyText = new TextDecoder("utf-8", { fatal: true }).decode(body);
4418
- payload = JSON.parse(bodyText);
4419
- } catch {
4420
- return originalRequest;
4421
- }
4422
- if (payload === null || typeof payload !== "object" || Array.isArray(payload) || typeof payload.model !== "string") return originalRequest;
4423
- return {
4424
- createRequest: (mappedModel) => createJsonImagesRequest(request, requestHeaders, payload, mappedModel),
4425
- model: payload.model,
4426
- originalRequest
4427
- };
5525
+ const parsed = await prepareImageJsonRequest(request);
5526
+ return parsed.valid ? parsed : parsed.originalRequest;
4428
5527
  }
4429
5528
  /**
4430
5529
  * Handles Codex images proxying. Pass a resolvedProviderConfig when the
4431
5530
  * caller already resolved the codex provider to avoid a second resolve, and
4432
5531
  * pass a request when model mapping rebuilt the request body.
4433
5532
  */
4434
- async function handleCodexImages(c, operation, resolvedProviderConfig, request = c.req.raw) {
5533
+ async function handleCodexImages(c, operation, resolvedProviderConfig, request = c.req.raw, model) {
4435
5534
  try {
4436
- if (!(resolvedProviderConfig ?? await imageRouteDependencies.resolveProviderConfig("codex"))) return c.json({ error: {
4437
- message: "Provider 'codex' not found or disabled",
4438
- type: "invalid_request_error"
4439
- } }, 404);
4440
- if (operation === "generations") await imageRouteDependencies.debugJsonAsync(logger$15, "images.generations.codex.request", async () => ({ body: await request.clone().text() }));
4441
- else debugJson(logger$15, "images.edits.codex.request", getContentMetadata(request.headers));
5535
+ const codexProviderConfig = resolvedProviderConfig ?? await imageRouteDependencies.resolveProviderConfig("codex");
5536
+ if (!codexProviderConfig) {
5537
+ await request.body?.cancel().catch(() => {});
5538
+ return c.json({ error: {
5539
+ message: "Provider 'codex' not found or disabled",
5540
+ type: "invalid_request_error"
5541
+ } }, 404);
5542
+ }
5543
+ let actualModel = model;
5544
+ if (actualModel === void 0 && !request.headers.get("content-type")?.includes("multipart/form-data")) {
5545
+ const parsed = await prepareImageJsonRequest(request);
5546
+ actualModel = parsed.valid ? parsed.model ?? DEFAULT_CODEX_IMAGE_MODEL : "unknown";
5547
+ request = parsed.originalRequest;
5548
+ }
5549
+ actualModel ??= "unknown";
5550
+ request = await logImageEditRequest(logger$15, `images.${operation}.codex.request`, request, {}, imageRouteDependencies.debugJsonAsync);
4442
5551
  const upstreamResponse = await forwardCodexImages(request, operation);
4443
5552
  debugJson(logger$15, `images.${operation}.codex.response`, {
4444
5553
  ...getContentMetadata(upstreamResponse.headers),
4445
5554
  statusCode: upstreamResponse.status
4446
5555
  });
4447
- return createProviderProxyResponse(upstreamResponse);
5556
+ return observeImagesResponse(upstreamResponse, {
5557
+ model: actualModel,
5558
+ operation,
5559
+ providerConfig: codexProviderConfig,
5560
+ onMetadata: (metadata) => debugJson(logger$15, `images.${operation}.codex.response.body`, metadata)
5561
+ });
4448
5562
  } catch (error) {
5563
+ await request.body?.cancel().catch(() => {});
4449
5564
  logger$15.error(`images.${operation}.codex.error`, { error });
4450
5565
  return await forwardError(c, error);
4451
5566
  }
4452
5567
  }
4453
5568
  /**
4454
- * Routes a parsed images request: applies the model mapping, forwards to a
4455
- * mapped provider when configured, and otherwise falls back to Codex.
5569
+ * Applies model mappings and resolves unknown or absent models to an available
5570
+ * provider's image default before dispatching.
4456
5571
  */
4457
5572
  async function routeImagesRequest(c, operation, parsed) {
4458
- const requestedModel = parsed.model;
4459
- const mappedModel = imageRouteDependencies.resolveMappedModel(requestedModel);
4460
- if (mappedModel !== requestedModel) consola.debug(`Resolved model mapping: ${requestedModel} -> ${mappedModel}`);
4461
- const providerModelAlias = parseProviderModelAlias(mappedModel);
4462
- if (providerModelAlias) {
4463
- const providerConfig = await imageRouteDependencies.resolveProviderConfig(providerModelAlias.provider);
4464
- if (providerConfig) {
4465
- const request = await parsed.createRequest(providerModelAlias.model, providerConfig.name);
4466
- if (providerConfig.name === "codex") return await handleCodexImages(c, operation, providerConfig, request);
4467
- if (providerConfig.name === "xai") {
4468
- const payload = await request.json();
4469
- return await forwardXaiImagesWithLogging(providerConfig, request, operation, operation === "edits" ? translateXaiImageEditJson(payload, providerModelAlias.model) : translateXaiImageGeneration(payload, providerModelAlias.model), {
4470
- logger: logger$15,
4471
- provider: providerModelAlias.provider
4472
- });
4473
- }
4474
- return await forwardProviderImagesWithLogging(providerConfig, request, operation, {
4475
- logger: logger$15,
4476
- provider: providerModelAlias.provider
4477
- });
4478
- }
4479
- consola.debug(`Provider '${providerModelAlias.provider}' not found or disabled; forwarding the original model to Codex`);
4480
- return await handleCodexImages(c, operation, void 0, parsed.originalRequest ?? await parsed.createRequest(requestedModel));
5573
+ let resolvedModel;
5574
+ try {
5575
+ resolvedModel = await resolveImageRequestModel(parsed.model?.trim() ? imageRouteDependencies.resolveMappedModel(parsed.model.trim()) : void 0, {
5576
+ resolveProviderConfig: imageRouteDependencies.resolveProviderConfig,
5577
+ isGitHubCopilotAvailable: imageRouteDependencies.isGitHubCopilotAvailable
5578
+ });
5579
+ } catch (error) {
5580
+ await parsed.originalRequest?.body?.cancel().catch(() => {});
5581
+ throw error;
4481
5582
  }
4482
- if (parsed.originalRequest && mappedModel === requestedModel) return await handleCodexImages(c, operation, void 0, parsed.originalRequest);
4483
- return await handleCodexImages(c, operation, void 0, await parsed.createRequest(mappedModel));
5583
+ const alias = parseProviderModelAlias(resolvedModel);
5584
+ if (!alias) throw new Error("Invalid resolved image model");
5585
+ consola.debug(`Resolved image model: ${parsed.model ?? "(default)"} -> ${resolvedModel}`);
5586
+ const request = parsed.originalRequest && (alias.provider === "xai" || parsed.model === alias.model) ? parsed.originalRequest : await parsed.createRequest(alias.model, alias.provider);
5587
+ if (alias.provider === "github-copilot") return await forwardCopilotImagesWithLogging(request, operation, {
5588
+ logger: logger$15,
5589
+ ...alias
5590
+ });
5591
+ const providerConfig = await imageRouteDependencies.resolveProviderConfig(alias.provider);
5592
+ if (!providerConfig) {
5593
+ await request.body?.cancel().catch(() => {});
5594
+ throw new HTTPError(`Provider '${alias.provider}' not found or disabled`, Response.json({ error: {
5595
+ message: `Provider '${alias.provider}' not found or disabled`,
5596
+ type: "invalid_request_error"
5597
+ } }, { status: 404 }));
5598
+ }
5599
+ if (alias.provider === "codex") return await handleCodexImages(c, operation, providerConfig, request, alias.model);
5600
+ const translated = await translateXaiImageJsonRequest(request, operation, alias.model);
5601
+ return await forwardXaiImagesWithLogging(providerConfig, request, operation, translated.payload, {
5602
+ logger: logger$15,
5603
+ ...alias
5604
+ }, translated.body);
4484
5605
  }
4485
5606
  // src/routes/images/xai-handler.ts
4486
5607
  function xaiImagesRequestErrorResponse(c, error) {
@@ -4490,30 +5611,28 @@ function xaiImagesRequestErrorResponse(c, error) {
4490
5611
  type: "invalid_request_error"
4491
5612
  } }, 400);
4492
5613
  }
4493
- async function readJsonBody(request, message) {
4494
- try {
4495
- const body = new TextDecoder("utf-8", { fatal: true }).decode(new Uint8Array(await request.arrayBuffer()));
4496
- return JSON.parse(body);
4497
- } catch {
4498
- throw new XaiImagesRequestError(message);
4499
- }
4500
- }
4501
- async function forwardTranslatedXaiImages(c, operation, payload) {
5614
+ async function forwardTranslatedXaiImages(c, operation, payload, body) {
4502
5615
  const providerConfig = await imageRouteDependencies.resolveProviderConfig("xai");
4503
- if (!providerConfig) return c.json({ error: {
4504
- message: "Provider 'xai' not found or disabled",
4505
- type: "invalid_request_error"
4506
- } }, 404);
5616
+ if (!providerConfig) {
5617
+ await body?.cancel();
5618
+ return c.json({ error: {
5619
+ message: "Provider 'xai' not found or disabled",
5620
+ type: "invalid_request_error"
5621
+ } }, 404);
5622
+ }
4507
5623
  return await forwardXaiImagesWithLogging(providerConfig, c.req.raw, operation, payload, {
4508
5624
  logger: logger$15,
4509
5625
  provider: "xai"
4510
- });
5626
+ }, body);
4511
5627
  }
4512
5628
  async function handleXaiImageGeneration(c, model) {
4513
- return await forwardTranslatedXaiImages(c, "generations", translateXaiImageGeneration(await readJsonBody(c.req.raw, "Image generation body must be JSON"), model));
5629
+ const translated = await translateXaiImageJsonRequest(c.req.raw, "generations", model);
5630
+ return await forwardTranslatedXaiImages(c, "generations", translated.payload, translated.body);
4514
5631
  }
4515
5632
  async function handleXaiImageEdit(c, model, multipartPayload) {
4516
- return await forwardTranslatedXaiImages(c, "edits", translateXaiImageEditJson(multipartPayload ?? await readJsonBody(c.req.raw, "Image edit body must be JSON"), model));
5633
+ if (multipartPayload) return await forwardTranslatedXaiImages(c, "edits", translateXaiImageEditJson(multipartPayload, model));
5634
+ const translated = await translateXaiImageJsonRequest(c.req.raw, "edits", model);
5635
+ return await forwardTranslatedXaiImages(c, "edits", translated.payload, translated.body);
4517
5636
  }
4518
5637
  // src/routes/images/edits-handler.ts
4519
5638
  async function handleImagesEdits(c) {
@@ -4528,7 +5647,6 @@ async function handleImagesEdits(c) {
4528
5647
  const multipart = await parseMultipartImageEdit(c.req.raw);
4529
5648
  if (multipart) try {
4530
5649
  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
5650
  return await routeImagesRequest(c, "edits", {
4533
5651
  model: multipart.model,
4534
5652
  createRequest: (model, provider) => multipart.createRequest(model, provider)
@@ -4541,7 +5659,7 @@ async function handleImagesEdits(c) {
4541
5659
  }
4542
5660
  if (configuredModel.status === "xai") return await handleXaiImageEdit(c, configuredModel.model);
4543
5661
  const parsed = await parseJsonImagesRequest(c.req.raw);
4544
- if (parsed instanceof Request) return await handleCodexImages(c, "edits", void 0, parsed);
5662
+ if (parsed instanceof Request) return await handleCodexImages(c, "edits", void 0, parsed, "unknown");
4545
5663
  return await routeImagesRequest(c, "edits", parsed);
4546
5664
  } catch (error) {
4547
5665
  const invalidRequest = imageEditRequestErrorResponse(c, error);
@@ -4560,7 +5678,7 @@ async function handleImagesGenerations(c) {
4560
5678
  } }, 400);
4561
5679
  if (configuredModel.status === "xai") return await handleXaiImageGeneration(c, configuredModel.model);
4562
5680
  const parsed = await parseJsonImagesRequest(c.req.raw);
4563
- if (parsed instanceof Request) return await handleCodexImages(c, "generations", void 0, parsed);
5681
+ if (parsed instanceof Request) return await handleCodexImages(c, "generations", void 0, parsed, "unknown");
4564
5682
  return await routeImagesRequest(c, "generations", parsed);
4565
5683
  } catch (error) {
4566
5684
  const invalidRequest = xaiImagesRequestErrorResponse(c, error);
@@ -6716,9 +7834,9 @@ const mapResponsesUsage = (response) => {
6716
7834
  ...cacheWriteTokens !== void 0 && { cache_creation_input_tokens: cacheWriteTokens }
6717
7835
  };
6718
7836
  };
6719
- const isRecord$2 = (value) => typeof value === "object" && value !== null;
6720
- const isResponseOutputText = (block) => isRecord$2(block) && "type" in block && block.type === "output_text";
6721
- const isResponseOutputRefusal = (block) => isRecord$2(block) && "type" in block && block.type === "refusal";
7837
+ const isRecord$3 = (value) => typeof value === "object" && value !== null;
7838
+ const isResponseOutputText = (block) => isRecord$3(block) && "type" in block && block.type === "output_text";
7839
+ const isResponseOutputRefusal = (block) => isRecord$3(block) && "type" in block && block.type === "refusal";
6722
7840
  const convertToolResultContent = (content) => {
6723
7841
  if (typeof content === "string") return content;
6724
7842
  if (Array.isArray(content)) {
@@ -7471,7 +8589,7 @@ function resolveToolDescriptor(registry, alias) {
7471
8589
  };
7472
8590
  }
7473
8591
  function decodeCustomToolInput(input) {
7474
- if (isRecord$1(input) && typeof input.input === "string") return input.input;
8592
+ if (isRecord$2(input) && typeof input.input === "string") return input.input;
7475
8593
  return typeof input === "string" ? input : JSON.stringify(input ?? {});
7476
8594
  }
7477
8595
  function toResponseId(messageId) {
@@ -7552,7 +8670,7 @@ function isNamespaceTool(tool) {
7552
8670
  return getStringField$1(tool, "type") === "namespace";
7553
8671
  }
7554
8672
  function registerTool(tool, registry, strictCustomTools, namespaces = []) {
7555
- if (!isRecord$1(tool)) throw new ResponsesMessagesTranslationError("Invalid Responses tool definition");
8673
+ if (!isRecord$2(tool)) throw new ResponsesMessagesTranslationError("Invalid Responses tool definition");
7556
8674
  const type = getStringField$1(tool, "type");
7557
8675
  if (type === "namespace") {
7558
8676
  const namespaceTool = tool;
@@ -7569,7 +8687,7 @@ function registerTool(tool, registry, strictCustomTools, namespaces = []) {
7569
8687
  name,
7570
8688
  namespace: namespaces.length > 0 ? namespaces.join(".") : void 0,
7571
8689
  description: getOptionalStringField(tool, "description"),
7572
- parameters: type === "function" && isRecord$1(tool.parameters) ? tool.parameters : null
8690
+ parameters: type === "function" && isRecord$2(tool.parameters) ? tool.parameters : null
7573
8691
  }, registry, strictCustomTools);
7574
8692
  }
7575
8693
  function registerMessagesTool(registration, registry, strictCustomTools) {
@@ -7667,7 +8785,7 @@ function translateInputItems(input, messages, system, registry) {
7667
8785
  }
7668
8786
  }
7669
8787
  function translateInputMessage(item, messages, system, userMessageSeen) {
7670
- if (!isRecord$1(item)) throw new ResponsesMessagesTranslationError("Invalid Responses message item");
8788
+ if (!isRecord$2(item)) throw new ResponsesMessagesTranslationError("Invalid Responses message item");
7671
8789
  const role = getStringField$1(item, "role");
7672
8790
  if (role !== "user" && role !== "assistant" && role !== "system" && role !== "developer") throw new ResponsesMessagesTranslationError("Responses message items require a supported role");
7673
8791
  if (role === "developer" && !userMessageSeen) return;
@@ -7707,7 +8825,7 @@ function appendDeveloperPrompts(system, input) {
7707
8825
  if (type === "agent_message") break;
7708
8826
  if ((type === void 0 || type === "message") && role === "user") break;
7709
8827
  if (type !== void 0 && type !== "message" || role !== "developer") continue;
7710
- prompts.push(translateSystemContent(isRecord$1(item) ? item.content : void 0, "message.content"));
8828
+ prompts.push(translateSystemContent(isRecord$2(item) ? item.content : void 0, "message.content"));
7711
8829
  }
7712
8830
  const [firstPrompt, ...remainingPrompts] = prompts;
7713
8831
  if (firstPrompt) system.push({
@@ -7728,7 +8846,7 @@ function translateInputReasoning(item, messages) {
7728
8846
  });
7729
8847
  }
7730
8848
  function translateInputToolCall(item, kind, messages, registry) {
7731
- if (!isRecord$1(item)) throw new ResponsesMessagesTranslationError("Invalid Responses tool call item");
8849
+ if (!isRecord$2(item)) throw new ResponsesMessagesTranslationError("Invalid Responses tool call item");
7732
8850
  const name = requireStringField(item, "name", `${kind}_tool_call`);
7733
8851
  const namespace = getOptionalStringField(item, "namespace") ?? void 0;
7734
8852
  const toolIdentity = {
@@ -7759,7 +8877,7 @@ function translateAssistantContent(value, path) {
7759
8877
  }] : [];
7760
8878
  if (!Array.isArray(value)) throw new ResponsesMessagesTranslationError(`${path} must be text or an array`);
7761
8879
  return value.map((part, index) => {
7762
- if (!isRecord$1(part)) throw new ResponsesMessagesTranslationError(`${path}[${index}] must be an object`);
8880
+ if (!isRecord$2(part)) throw new ResponsesMessagesTranslationError(`${path}[${index}] must be an object`);
7763
8881
  const type = getStringField$1(part, "type");
7764
8882
  if (type !== "input_text" && type !== "output_text" && type !== "text") throw new ResponsesMessagesTranslationError(`${path}[${index}] has unsupported assistant content type '${type ?? "unknown"}'`);
7765
8883
  return {
@@ -7773,14 +8891,14 @@ function translateSystemContent(value, path) {
7773
8891
  if (typeof value === "string") return value;
7774
8892
  if (!Array.isArray(value)) throw new ResponsesMessagesTranslationError(`${path} must be text or an array`);
7775
8893
  return value.map((part, index) => {
7776
- if (!isRecord$1(part)) throw new ResponsesMessagesTranslationError(`${path}[${index}] must be an object`);
8894
+ if (!isRecord$2(part)) throw new ResponsesMessagesTranslationError(`${path}[${index}] must be an object`);
7777
8895
  const type = getStringField$1(part, "type");
7778
8896
  if (type !== "input_text" && type !== "output_text" && type !== "text") throw new ResponsesMessagesTranslationError(`${path}[${index}] has unsupported system content type '${type ?? "unknown"}'`);
7779
8897
  return requireStringField(part, "text", `${path}[${index}]`);
7780
8898
  }).join("\n\n");
7781
8899
  }
7782
8900
  function translateUserContentPart(part, path) {
7783
- if (!isRecord$1(part)) throw new ResponsesMessagesTranslationError(`${path} must be an object`);
8901
+ if (!isRecord$2(part)) throw new ResponsesMessagesTranslationError(`${path} must be an object`);
7784
8902
  const type = getStringField$1(part, "type");
7785
8903
  if (type === "input_text" || type === "output_text" || type === "text") return {
7786
8904
  type: "text",
@@ -7802,7 +8920,7 @@ function translateToolResultContent(value, path) {
7802
8920
  return blocks.length > 0 ? blocks : "";
7803
8921
  }
7804
8922
  function isEmptyTextContentPart(part) {
7805
- if (!isRecord$1(part)) return false;
8923
+ if (!isRecord$2(part)) return false;
7806
8924
  const type = part.type;
7807
8925
  return (type === "input_text" || type === "output_text" || type === "text") && part.text === "";
7808
8926
  }
@@ -7987,7 +9105,7 @@ function appendUserBlock(messages, block) {
7987
9105
  function parseFunctionArguments(value, _path) {
7988
9106
  try {
7989
9107
  const parsed = JSON.parse(value);
7990
- if (isRecord$1(parsed)) return parsed;
9108
+ if (isRecord$2(parsed)) return parsed;
7991
9109
  } catch {
7992
9110
  return {};
7993
9111
  }
@@ -8030,15 +9148,15 @@ function createStableHash(value) {
8030
9148
  return createHash("sha256").update(value).digest("hex").slice(0, 24);
8031
9149
  }
8032
9150
  function getItemType(value) {
8033
- return isRecord$1(value) && typeof value.type === "string" ? value.type : void 0;
9151
+ return isRecord$2(value) && typeof value.type === "string" ? value.type : void 0;
8034
9152
  }
8035
9153
  function getStringField$1(value, field) {
8036
- if (!isRecord$1(value)) return void 0;
9154
+ if (!isRecord$2(value)) return void 0;
8037
9155
  const fieldValue = value[field];
8038
9156
  return typeof fieldValue === "string" ? fieldValue : void 0;
8039
9157
  }
8040
9158
  function getOptionalStringField(value, field) {
8041
- if (!isRecord$1(value)) return void 0;
9159
+ if (!isRecord$2(value)) return void 0;
8042
9160
  const fieldValue = value[field];
8043
9161
  return typeof fieldValue === "string" || fieldValue === null ? fieldValue : void 0;
8044
9162
  }
@@ -8048,10 +9166,10 @@ function requireStringField(value, field, path) {
8048
9166
  return result;
8049
9167
  }
8050
9168
  function getArrayField(value, field) {
8051
- if (!isRecord$1(value)) return [];
9169
+ if (!isRecord$2(value)) return [];
8052
9170
  return Array.isArray(value[field]) ? value[field] : [];
8053
9171
  }
8054
- function isRecord$1(value) {
9172
+ function isRecord$2(value) {
8055
9173
  return typeof value === "object" && value !== null && !Array.isArray(value);
8056
9174
  }
8057
9175
  const DEFAULT_RESPONSES_COMPACT_THRESHOLD_RATIO = .85;
@@ -9208,7 +10326,7 @@ async function forwardGoogleGenerateContent(provider, model, payload, requestHea
9208
10326
  applyOpencodeSessionHeader(provider, headers, options.sessionId);
9209
10327
  headers.accept = options.stream ? "text/event-stream" : "application/json";
9210
10328
  const transport = getUpstreamTransportConfig();
9211
- setPluginRequestActualModel(model.replace(/^models\//u, ""));
10329
+ setPluginRequestActualModel(model.replace(/^models\//u, ""), provider.name);
9212
10330
  return await fetchUpstreamWithLifecycle(url, {
9213
10331
  method: "POST",
9214
10332
  headers,
@@ -10509,14 +11627,14 @@ function withModelDisplayName(model, provider) {
10509
11627
  // src/routes/models/model-discovery.ts
10510
11628
  const logger$10 = createHandlerLogger("models-handler");
10511
11629
  const EPOCH_ISO = (/* @__PURE__ */ new Date(0)).toISOString();
10512
- function isRecord(value) {
11630
+ function isRecord$1(value) {
10513
11631
  return typeof value === "object" && value !== null && !Array.isArray(value);
10514
11632
  }
10515
11633
  function firstPositiveInteger(...values) {
10516
11634
  for (const value of values) if (typeof value === "number" && Number.isFinite(value) && value >= 1) return Math.floor(value);
10517
11635
  }
10518
11636
  function normalizeModelCapabilities(model) {
10519
- if (!isRecord(model.capabilities)) return model;
11637
+ if (!isRecord$1(model.capabilities)) return model;
10520
11638
  const limits = getRecordField(model.capabilities, "limits");
10521
11639
  const supports = getRecordField(model.capabilities, "supports");
10522
11640
  return {
@@ -10547,13 +11665,21 @@ function getStringField(model, field) {
10547
11665
  }
10548
11666
  function getRecordField(model, field) {
10549
11667
  const value = model?.[field];
10550
- return isRecord(value) ? value : void 0;
11668
+ return isRecord$1(value) ? value : void 0;
10551
11669
  }
10552
11670
  function getStringList(value) {
10553
11671
  if (!Array.isArray(value)) return void 0;
10554
11672
  const strings = [...new Set(value.filter((item) => typeof item === "string"))];
10555
11673
  return strings.length > 0 ? strings : void 0;
10556
11674
  }
11675
+ function isImageOnlyModel(provider, modelId, model, config) {
11676
+ const record = isRecord$1(model) ? model : void 0;
11677
+ const catalog = getModelsDevModel(resolveProviderModelsDevId(provider, config), modelId);
11678
+ const builtin = getBuiltinProviderImageModelRecords(provider).find((image) => image.id === modelId);
11679
+ const output = getStringList(record?.output_modalities) ?? getStringList(getRecordField(record, "architecture")?.output_modalities) ?? getStringList(catalog?.modalities?.output) ?? getStringList(builtin?.output_modalities);
11680
+ if (output) return output.includes("image") && !output.includes("text");
11681
+ return record?.type === "image" || getRecordField(record, "capabilities")?.type === "image";
11682
+ }
10557
11683
  function getFallbackProviderModelRecords(provider, reason, details = {}) {
10558
11684
  const fallbackModels = getLocalProviderModelRecords(provider, getRawProviderConfig(provider)) ?? getBuiltinProviderModelRecords(provider);
10559
11685
  logger$10.warn(`models.provider.fallback_${reason}`, {
@@ -10563,10 +11689,10 @@ function getFallbackProviderModelRecords(provider, reason, details = {}) {
10563
11689
  });
10564
11690
  return fallbackModels;
10565
11691
  }
10566
- function normalizeProviderModel(provider, model, config) {
10567
- if (!isRecord(model)) return null;
11692
+ function normalizeProviderModel(provider, model, config, includeImageModels = false) {
11693
+ if (!isRecord$1(model)) return null;
10568
11694
  const rawId = getStringField(model, "id");
10569
- if (!rawId || !isProviderModelVisible(provider, rawId, config)) return null;
11695
+ if (!rawId || !isProviderModelVisible(provider, rawId, config) || !includeImageModels && isImageOnlyModel(provider, rawId, model, config)) return null;
10570
11696
  const id = `${provider}/${rawId}`;
10571
11697
  const ownedBy = getStringField(model, "owned_by") ?? getStringField(model, "vendor") ?? provider;
10572
11698
  return {
@@ -10579,8 +11705,8 @@ function normalizeProviderModel(provider, model, config) {
10579
11705
  owned_by: ownedBy
10580
11706
  };
10581
11707
  }
10582
- function normalizeProviderModels(provider, models, config) {
10583
- return models.map((model) => normalizeProviderModel(provider, model, config)).filter((model) => model !== null);
11708
+ function normalizeProviderModels(provider, models, config, includeImageModels = false) {
11709
+ return models.map((model) => normalizeProviderModel(provider, model, config, includeImageModels)).filter((model) => model !== null);
10584
11710
  }
10585
11711
  async function getProviderModelRecords(providerConfig, requestHeaders) {
10586
11712
  const localModels = getLocalProviderModelRecords(providerConfig.name, providerConfig);
@@ -10589,8 +11715,8 @@ async function getProviderModelRecords(providerConfig, requestHeaders) {
10589
11715
  const response = await forwardProviderModels(providerConfig, requestHeaders);
10590
11716
  if (!response.ok) return getFallbackProviderModelRecords(providerConfig.name, "non_ok", { statusCode: response.status });
10591
11717
  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);
11718
+ if (!isRecord$1(body) || !Array.isArray(body.data)) return getFallbackProviderModelRecords(providerConfig.name, "invalid_body");
11719
+ return body.data.filter(isRecord$1).map(normalizeModelCapabilities);
10594
11720
  } catch (error) {
10595
11721
  return getFallbackProviderModelRecords(providerConfig.name, "error", { error });
10596
11722
  }
@@ -10600,7 +11726,7 @@ function getCopilotModelRecords() {
10600
11726
  }
10601
11727
  function getModelsById(models) {
10602
11728
  return new Map(models.flatMap((model) => {
10603
- if (!isRecord(model)) return [];
11729
+ if (!isRecord$1(model)) return [];
10604
11730
  const id = getStringField(model, "id");
10605
11731
  return id ? [[id, model]] : [];
10606
11732
  }));
@@ -10614,12 +11740,22 @@ function deduplicateModels(models, getId) {
10614
11740
  return true;
10615
11741
  });
10616
11742
  }
10617
- async function getProviderModels(provider, requestHeaders, selectModels) {
11743
+ function withProviderImageModels(provider, models) {
11744
+ const imageModels = getModelsById(getBuiltinProviderImageModelRecords(provider));
11745
+ if (imageModels.size === 0) return models;
11746
+ const listedModels = getModelsById(models);
11747
+ return [...models.map((model) => isRecord$1(model) ? {
11748
+ ...imageModels.get(getStringField(model, "id") ?? ""),
11749
+ ...model
11750
+ } : model), ...[...imageModels.values()].filter((model) => !listedModels.has(getStringField(model, "id") ?? ""))];
11751
+ }
11752
+ async function getProviderModels(provider, requestHeaders, selectModels, includeImageModels = false) {
10618
11753
  try {
10619
11754
  const providerConfig = await resolveProviderConfig(provider);
10620
11755
  if (!providerConfig) return [];
10621
- const models = providerConfig.name === "codex" ? getModels().data.map(normalizeModelCapabilities) : await getProviderModelRecords(providerConfig, requestHeaders);
10622
- return normalizeProviderModels(providerConfig.name, selectModels?.(providerConfig, models) ?? models, providerConfig);
11756
+ const upstreamModels = providerConfig.name === "codex" ? getModels().data.map(normalizeModelCapabilities) : await getProviderModelRecords(providerConfig, requestHeaders);
11757
+ const models = includeImageModels ? withProviderImageModels(providerConfig.name, upstreamModels) : upstreamModels;
11758
+ return normalizeProviderModels(providerConfig.name, selectModels?.(providerConfig, models) ?? models, providerConfig, includeImageModels);
10623
11759
  } catch (error) {
10624
11760
  if (provider === "codex") {
10625
11761
  logger$10.warn("models.provider.skip_error", {
@@ -10630,14 +11766,22 @@ async function getProviderModels(provider, requestHeaders, selectModels) {
10630
11766
  }
10631
11767
  const fallbackModels = getFallbackProviderModelRecords(provider, "error", { error });
10632
11768
  const providerConfig = getRawProviderConfig(provider);
10633
- return normalizeProviderModels(provider, selectModels?.(providerConfig, fallbackModels) ?? fallbackModels, providerConfig);
11769
+ return normalizeProviderModels(provider, selectModels?.(providerConfig, fallbackModels) ?? fallbackModels, providerConfig, includeImageModels);
10634
11770
  }
10635
11771
  }
10636
11772
  async function getAggregatedModels(requestHeaders, selectors = {}) {
10637
11773
  const rawCopilotModels = getCopilotModelRecords();
10638
- const copilotModels = (selectors.selectCopilotModels?.(rawCopilotModels) ?? rawCopilotModels).map(normalizeCopilotModel);
10639
- const providerModelsByProvider = await Promise.all(listEnabledProviders().map((provider) => getProviderModels(provider, requestHeaders, selectors.selectProviderModels)));
10640
- return deduplicateModels([...copilotModels, ...providerModelsByProvider.flat()], (model) => model.id);
11774
+ const copilotConfig = getRawProviderConfig(GITHUB_COPILOT_PROVIDER);
11775
+ const isCopilotImage = (model) => isImageOnlyModel(GITHUB_COPILOT_PROVIDER, model.id, model, copilotConfig);
11776
+ const copilotModels = (selectors.selectCopilotModels?.(rawCopilotModels) ?? rawCopilotModels).filter((model) => !isCopilotImage(model)).map(normalizeCopilotModel);
11777
+ const rawCopilotImages = selectors.includeImageModels && isGitHubCopilotAvailable() ? withProviderImageModels(GITHUB_COPILOT_PROVIDER, rawCopilotModels.filter(isCopilotImage)) : [];
11778
+ 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) : [];
11779
+ const providerModelsByProvider = await Promise.all(listEnabledProviders().map((provider) => getProviderModels(provider, requestHeaders, selectors.selectProviderModels, selectors.includeImageModels)));
11780
+ return deduplicateModels([
11781
+ ...copilotModels,
11782
+ ...copilotImages,
11783
+ ...providerModelsByProvider.flat()
11784
+ ], (model) => model.id);
10641
11785
  }
10642
11786
  function selectAgentCopilotModels(models) {
10643
11787
  const selection = getProviderAgentModels(getRawProviderConfig(GITHUB_COPILOT_PROVIDER));
@@ -10650,8 +11794,9 @@ function selectAgentProviderModels(providerConfig, models) {
10650
11794
  const modelsById = getModelsById(models);
10651
11795
  return selection.map((id) => modelsById.get(id) ?? { id });
10652
11796
  }
10653
- async function getAgentModels(requestHeaders) {
11797
+ async function getAgentModels(requestHeaders, options = {}) {
10654
11798
  return getAggregatedModels(requestHeaders, {
11799
+ includeImageModels: options.includeImageModels,
10655
11800
  selectCopilotModels: selectAgentCopilotModels,
10656
11801
  selectProviderModels: selectAgentProviderModels
10657
11802
  });
@@ -10815,7 +11960,8 @@ function getCopilotCodexCandidates() {
10815
11960
  return candidates;
10816
11961
  }
10817
11962
  function isCopilotCodexCandidate(model) {
10818
- return (model.supported_endpoints ?? []).some((endpoint) => endpoint === MESSAGES_ENDPOINT || endpoint === CHAT_COMPLETIONS_ENDPOINT || RESPONSES_ENDPOINTS.has(endpoint)) && model.capabilities.supports.tool_calls !== false;
11963
+ const endpoints = model.supported_endpoints ?? [];
11964
+ 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
11965
  }
10820
11966
  function createCopilotCodexCandidate(model) {
10821
11967
  const reasoningEfforts = normalizeReasoningEfforts(model.capabilities.supports.reasoning_effort);
@@ -10845,9 +11991,11 @@ async function getProviderCodexCandidates(provider, requestHeaders) {
10845
11991
  const candidates = [];
10846
11992
  for (const modelId of modelIds) {
10847
11993
  if (!isProviderModelVisible(provider, modelId, providerConfig)) continue;
11994
+ const remoteModel = modelsById.get(modelId);
11995
+ if (isImageOnlyModel(provider, modelId, remoteModel, providerConfig)) continue;
10848
11996
  const effectiveType = resolveEffectiveProviderType(providerConfig, modelId);
10849
11997
  if (!isMessagesFallbackProviderType(effectiveType) && effectiveType !== "openai-responses") continue;
10850
- candidates.push(createProviderCodexCandidate(providerConfig, modelId, modelsById.get(modelId), effectiveType));
11998
+ candidates.push(createProviderCodexCandidate(providerConfig, modelId, remoteModel, effectiveType));
10851
11999
  }
10852
12000
  return candidates;
10853
12001
  } catch (error) {
@@ -12634,7 +13782,7 @@ async function handleCodexModelsProxy(c, resolvedProviderConfig) {
12634
13782
  const providerConfig = getRawProviderConfig("codex");
12635
13783
  return createCodexCatalogResponse(c, {
12636
13784
  ...catalog,
12637
- models: catalog.models.filter((model) => isProviderAgentModelVisible(providerConfig, model.slug))
13785
+ models: catalog.models.filter((model) => !isImageOnlyModel("codex", model.slug, model, providerConfig) && isProviderAgentModelVisible(providerConfig, model.slug))
12638
13786
  }, new Set(getProviderAgentModels(providerConfig)), FALLBACK_CODEX_MODELS[0].model_messages);
12639
13787
  }
12640
13788
  async function handleCodexModels(c) {
@@ -12651,10 +13799,10 @@ async function handleMergedCodexModels(c, candidatesRequest, options = {}) {
12651
13799
  return [];
12652
13800
  })]);
12653
13801
  const upstreamCatalog = upstreamCatalogResult?.catalog;
12654
- const upstreamModels = upstreamCatalog?.models ?? FALLBACK_CODEX_MODELS;
13802
+ const codexConfig = getRawProviderConfig("codex");
13803
+ const upstreamModels = (upstreamCatalog?.models ?? FALLBACK_CODEX_MODELS).filter((model) => !isImageOnlyModel("codex", model.slug, model, codexConfig));
12655
13804
  const template = selectTemplate(upstreamModels);
12656
13805
  const catalogModelsBySlug = new Map(upstreamModels.map((model) => [model.slug, model]));
12657
- const codexConfig = getRawProviderConfig("codex");
12658
13806
  const copilotConfig = getRawProviderConfig("github-copilot");
12659
13807
  const visibleUpstreamModels = upstreamModels.filter((model) => isProviderAgentModelVisible(codexConfig, model.slug) && (upstreamCatalog !== void 0 || codexConfig !== null || isProviderAgentModelVisible(copilotConfig, model.slug)));
12660
13808
  const explicitSlugs = /* @__PURE__ */ new Set();
@@ -12825,15 +13973,17 @@ function getCandidatePriorityBase(candidate) {
12825
13973
  return PROVIDER_PRIORITY_BASE;
12826
13974
  }
12827
13975
  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");
13976
+ if (!isRecord$1(value) || !Array.isArray(value.models)) return false;
13977
+ return value.models.every((model) => isRecord$1(model) && typeof model.slug === "string");
12830
13978
  }
12831
13979
  // src/routes/models/default-models.ts
12832
13980
  async function handleDefaultModels(c) {
12833
- return createModelListResponse(c, await (c.req.header("user-agent")?.toLowerCase().startsWith("deepseek") ? getAgentModels : getAggregatedModels)(stripInternalRequestHeaders(c.req.raw.headers)));
13981
+ const isDeepSeekClient = c.req.header("user-agent")?.toLowerCase().startsWith("deepseek") ?? false;
13982
+ return createModelListResponse(c, await (isDeepSeekClient ? getAgentModels : getAggregatedModels)(stripInternalRequestHeaders(c.req.raw.headers), { includeImageModels: !isDeepSeekClient }));
12834
13983
  }
12835
13984
  // src/routes/models/opencode-models.ts
12836
13985
  const RESPONSES_PACKAGE = "@opencode/ai/providers/openai/responses";
13986
+ const IMAGES_PACKAGE = "@opencode/ai/providers/openai/images";
12837
13987
  const CNY_PER_USD = 6.7;
12838
13988
  const COST_DECIMALS = 6;
12839
13989
  function isOpencodeUserAgent(userAgent) {
@@ -12902,10 +14052,12 @@ function toOpencodeModel(model, contextWindow) {
12902
14052
  const config = getRawProviderConfig(provider);
12903
14053
  const metadata = getModelMetadata(provider, rawId, model, config);
12904
14054
  const { modelConfig, defaults, catalog, capabilities, supports, limits } = metadata;
14055
+ const output = getStringList(model.output_modalities) ?? getStringList(catalog?.modalities?.output) ?? (model.type === "image" ? ["image"] : ["text"]);
14056
+ const isImage = output.includes("image") && !output.includes("text");
12905
14057
  const context = metadata.contextWindow ?? 256e3;
12906
14058
  const inputLimit = metadata.inputLimit ?? 0;
12907
14059
  const supportedMediaTypes = getStringList(getRecordField(limits, "vision")?.supported_media_types);
12908
- const supportPdf = resolveModelPdfSupport(provider, {
14060
+ const supportPdf = !isImage && resolveModelPdfSupport(provider, {
12909
14061
  supportPdf: modelConfig?.supportPdf,
12910
14062
  supportedMediaTypes,
12911
14063
  catalogInputModalities: catalog?.modalities?.input,
@@ -12922,15 +14074,16 @@ function toOpencodeModel(model, contextWindow) {
12922
14074
  id: model.id,
12923
14075
  modelID: model.id,
12924
14076
  providerID: "local",
14077
+ ...isImage && { type: "image" },
12925
14078
  name: getStringField(model, "display_name") ?? catalog?.name ?? model.id,
12926
- family: catalog?.family ?? getStringField(capabilities ?? {}, "family"),
12927
- package: RESPONSES_PACKAGE,
14079
+ family: catalog?.family ?? getStringField(model, "family") ?? getStringField(capabilities ?? {}, "family"),
14080
+ package: isImage ? IMAGES_PACKAGE : RESPONSES_PACKAGE,
12928
14081
  capabilities: {
12929
- tools: typeof supports?.tool_calls === "boolean" ? supports.tool_calls : catalog?.tool_call ?? true,
14082
+ tools: isImage ? false : typeof supports?.tool_calls === "boolean" ? supports.tool_calls : catalog?.tool_call ?? true,
12930
14083
  input: [...input],
12931
- output: [...catalog?.modalities?.output ?? ["text"]]
14084
+ output
12932
14085
  },
12933
- variants: efforts.map((effort) => ({
14086
+ variants: isImage ? [] : efforts.map((effort) => ({
12934
14087
  id: effort,
12935
14088
  body: { reasoning: { effort } }
12936
14089
  })),
@@ -12938,7 +14091,10 @@ function toOpencodeModel(model, contextWindow) {
12938
14091
  cost,
12939
14092
  status: catalog?.status ?? (model.preview ? "beta" : "active"),
12940
14093
  enabled: true,
12941
- limit: {
14094
+ limit: isImage ? {
14095
+ context: 0,
14096
+ output: 0
14097
+ } : {
12942
14098
  context: Math.min(context, contextWindow),
12943
14099
  ...inputLimit > 0 && { input: Math.min(inputLimit, context, contextWindow) },
12944
14100
  output: Math.min(context, metadata.maxOutputTokens ?? 32e3)
@@ -12946,7 +14102,7 @@ function toOpencodeModel(model, contextWindow) {
12946
14102
  };
12947
14103
  }
12948
14104
  async function handleOpencodeModels(c) {
12949
- const models = await getAgentModels(stripInternalRequestHeaders(c.req.raw.headers));
14105
+ const models = await getAgentModels(stripInternalRequestHeaders(c.req.raw.headers), { includeImageModels: true });
12950
14106
  const contextWindow = getOpencodeModelContextWindow();
12951
14107
  c.header("Cache-Control", "private, no-store");
12952
14108
  c.header("Vary", "User-Agent");
@@ -13008,6 +14164,7 @@ const providerImageRoutes = new Hono();
13008
14164
  async function handleProviderImages(c, operation) {
13009
14165
  const provider = c.req.param("provider") ?? "";
13010
14166
  try {
14167
+ if (provider === "github-copilot") return await handleCopilotImages(c, operation);
13011
14168
  const providerConfig = await resolveProviderConfig(provider);
13012
14169
  if (!providerConfig) return c.json({ error: {
13013
14170
  message: `Provider '${provider}' not found or disabled`,
@@ -13018,17 +14175,27 @@ async function handleProviderImages(c, operation) {
13018
14175
  if (contentTypeError) return contentTypeError;
13019
14176
  const multipart = await parseMultipartImageEdit(c.req.raw);
13020
14177
  if (multipart) try {
13021
- const request = await multipart.createRequest(multipart.model, providerConfig.name);
13022
- if (providerConfig.name === "codex") return await handleCodexImages(c, operation, providerConfig, request);
14178
+ const model = await resolveImageRequestModel(multipart.model, { provider: providerConfig.name });
14179
+ const request = await multipart.createRequest(model, providerConfig.name);
14180
+ if (providerConfig.name === "codex") return await handleCodexImages(c, operation, providerConfig, request, model);
13023
14181
  return await forwardProviderImagesWithLogging(providerConfig, request, operation, {
13024
14182
  logger: logger$6,
13025
- provider
14183
+ provider,
14184
+ model
13026
14185
  });
13027
14186
  } finally {
13028
14187
  await multipart.cleanup();
13029
14188
  }
13030
14189
  }
13031
- if (providerConfig.name === "codex") return await handleCodexImages(c, operation, providerConfig);
14190
+ if (getDefaultImageModel(providerConfig.name)) {
14191
+ const { request, model } = await prepareProviderJsonImages(c.req.raw, providerConfig.name);
14192
+ if (providerConfig.name === "codex") return await handleCodexImages(c, operation, providerConfig, request, model);
14193
+ return await forwardProviderImagesWithLogging(providerConfig, request, operation, {
14194
+ logger: logger$6,
14195
+ provider,
14196
+ model
14197
+ });
14198
+ }
13032
14199
  return await forwardProviderImagesWithLogging(providerConfig, c.req.raw, operation, {
13033
14200
  logger: logger$6,
13034
14201
  provider
@@ -13043,6 +14210,46 @@ async function handleProviderImages(c, operation) {
13043
14210
  return await forwardError(c, error);
13044
14211
  }
13045
14212
  }
14213
+ async function handleCopilotImages(c, operation) {
14214
+ if (operation === "edits") {
14215
+ const contentTypeError = imageEditContentTypeErrorResponse(c);
14216
+ if (contentTypeError) return contentTypeError;
14217
+ const multipart = await parseMultipartImageEdit(c.req.raw);
14218
+ if (multipart) try {
14219
+ const model = await resolveImageRequestModel(multipart.model, { provider: GITHUB_COPILOT_PROVIDER });
14220
+ return await forwardCopilotImagesWithLogging(await multipart.createRequest(model, GITHUB_COPILOT_PROVIDER), operation, {
14221
+ logger: logger$6,
14222
+ provider: GITHUB_COPILOT_PROVIDER,
14223
+ model
14224
+ });
14225
+ } finally {
14226
+ await multipart.cleanup();
14227
+ }
14228
+ }
14229
+ const { request, model } = await prepareProviderJsonImages(c.req.raw, GITHUB_COPILOT_PROVIDER);
14230
+ return await forwardCopilotImagesWithLogging(request, operation, {
14231
+ logger: logger$6,
14232
+ provider: GITHUB_COPILOT_PROVIDER,
14233
+ model
14234
+ });
14235
+ }
14236
+ async function prepareProviderJsonImages(request, provider) {
14237
+ const parsed = await parseJsonImagesRequest(request);
14238
+ if (parsed instanceof Request) return {
14239
+ request: parsed,
14240
+ model: "unknown"
14241
+ };
14242
+ try {
14243
+ const model = await resolveImageRequestModel(parsed.model, { provider });
14244
+ return {
14245
+ model,
14246
+ request: parsed.model === model && parsed.originalRequest ? parsed.originalRequest : await parsed.createRequest(model)
14247
+ };
14248
+ } catch (error) {
14249
+ await parsed.originalRequest?.body?.cancel().catch(() => {});
14250
+ throw error;
14251
+ }
14252
+ }
13046
14253
  providerImageRoutes.post("/generations", (c) => handleProviderImages(c, "generations"));
13047
14254
  providerImageRoutes.post("/edits", (c) => handleProviderImages(c, "edits"));
13048
14255
  // src/routes/provider/messages/route.ts
@@ -14873,6 +16080,38 @@ usageRoute.get("/", async (c) => {
14873
16080
  return c.json({ error: "Failed to fetch Copilot usage" }, 500);
14874
16081
  }
14875
16082
  });
16083
+ // src/lib/token-usage/videos.ts
16084
+ function isRecord(value) {
16085
+ return value !== null && typeof value === "object" && !Array.isArray(value);
16086
+ }
16087
+ /** Reads the finite result JSON once for usage accounting and debug output. */
16088
+ async function recordVideoResultUsage(response, options) {
16089
+ let body;
16090
+ try {
16091
+ body = await response.clone().text();
16092
+ } catch {
16093
+ return;
16094
+ }
16095
+ options.onBody(body);
16096
+ if (!response.ok) return;
16097
+ let result;
16098
+ try {
16099
+ result = JSON.parse(body);
16100
+ } catch {
16101
+ return;
16102
+ }
16103
+ if (!isRecord(result) || result.status !== "done" || !isRecord(result.usage)) return;
16104
+ const cost = xaiUsdTicksToCost(result.usage.cost_in_usd_ticks);
16105
+ if (cost === void 0) return;
16106
+ recordTokenUsageEvent({
16107
+ source: "provider",
16108
+ providerName: options.providerConfig.name,
16109
+ endpoint: "videos_generations",
16110
+ model: typeof result.model === "string" && result.model.trim() ? result.model : options.defaultModel,
16111
+ cost,
16112
+ upstreamRequestId: options.requestId
16113
+ });
16114
+ }
14876
16115
  // src/services/xai/videos.ts
14877
16116
  var XaiVideosRequestError = class extends Error {
14878
16117
  constructor(message) {
@@ -14905,6 +16144,7 @@ async function forwardXaiVideos(providerConfig, request, videoRequest) {
14905
16144
  const logger$1 = createHandlerLogger("videos-handler");
14906
16145
  const SUPPORTED_VIDEO_MODEL = "grok-imagine-video-1.5";
14907
16146
  const videoRouteDependencies = {
16147
+ debugJson,
14908
16148
  resolveMappedModel,
14909
16149
  resolveProviderConfig
14910
16150
  };
@@ -14943,6 +16183,12 @@ async function handleVideos(c, operation) {
14943
16183
  } }, 404);
14944
16184
  const upstreamResponse = await forwardXaiVideos(providerConfig, c.req.raw, videoRequest);
14945
16185
  debugJson(logger$1, `videos.${operation}.response`, { statusCode: upstreamResponse.status });
16186
+ if (videoRequest.operation === "result") await recordVideoResultUsage(upstreamResponse, {
16187
+ providerConfig,
16188
+ requestId: videoRequest.requestId,
16189
+ defaultModel: SUPPORTED_VIDEO_MODEL,
16190
+ onBody: (body) => videoRouteDependencies.debugJson(logger$1, "videos.result.response.body", { body })
16191
+ });
14946
16192
  return createProviderProxyResponse(upstreamResponse);
14947
16193
  } catch (error) {
14948
16194
  if (error instanceof XaiVideosRequestError && !c.req.raw.signal.aborted) return c.json({ error: {
@@ -15024,4 +16270,4 @@ function createServer(options = {}) {
15024
16270
  createServer();
15025
16271
  export { createServer };
15026
16272
 
15027
- //# sourceMappingURL=server-C_-cCnrU.js.map
16273
+ //# sourceMappingURL=server-Cw9qn5Gt.js.map