@cartanova/qgrid-cli 2.9.6 → 2.9.9

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 (69) hide show
  1. package/bundle/dist/application/qgrid/qgrid-run-lifecycle.js +3 -2
  2. package/bundle/dist/application/qgrid/qgrid.types.js +2 -2
  3. package/bundle/dist/application/request-log/request-log.model.js +2 -1
  4. package/bundle/dist/utils/providers/anthropic/anthropic-constants.js +6 -2
  5. package/bundle/dist/utils/providers/common/model-cost.js +20 -2
  6. package/bundle/dist/utils/providers/openai/openai-backend-protocol.js +3 -2
  7. package/bundle/dist/utils/providers/openai/openai-dispatcher.js +86 -5
  8. package/bundle/src/application/qgrid/qgrid-image-generation.test.ts +7 -1
  9. package/bundle/src/application/qgrid/qgrid-run-lifecycle.test.ts +13 -0
  10. package/bundle/src/application/qgrid/qgrid-run-lifecycle.ts +2 -1
  11. package/bundle/src/application/qgrid/qgrid.frame.test.ts +2 -2
  12. package/bundle/src/application/qgrid/qgrid.types.ts +2 -2
  13. package/bundle/src/application/request-log/request-log.model.ts +2 -0
  14. package/bundle/src/application/sonamu.generated.http +4 -2
  15. package/bundle/src/utils/providers/anthropic/anthropic-constants.test.ts +9 -1
  16. package/bundle/src/utils/providers/anthropic/anthropic-constants.ts +11 -2
  17. package/bundle/src/utils/providers/common/effort.test.ts +3 -0
  18. package/bundle/src/utils/providers/common/model-cost.test.ts +23 -0
  19. package/bundle/src/utils/providers/common/model-cost.ts +28 -2
  20. package/bundle/src/utils/providers/common/provider-dispatcher.ts +1 -1
  21. package/bundle/src/utils/providers/openai/openai-backend-protocol.test.ts +5 -0
  22. package/bundle/src/utils/providers/openai/openai-backend-protocol.ts +2 -2
  23. package/bundle/src/utils/providers/openai/openai-dispatcher.test.ts +117 -0
  24. package/bundle/src/utils/providers/openai/openai-dispatcher.ts +136 -1
  25. package/bundle/web-dist/client/assets/index-D8VwLsuq.js +106 -0
  26. package/bundle/web-dist/client/assets/index-D_sagzsR.css +1 -0
  27. package/bundle/web-dist/client/assets/logs-_T8KV5ne.js +1 -0
  28. package/bundle/web-dist/client/assets/{monit-BkGkzl6m.js → monit-DRCrtO2j.js} +1 -1
  29. package/bundle/web-dist/client/assets/request-model-eygfdRv7.js +1 -0
  30. package/bundle/web-dist/client/assets/{routes-DqY8SXcB.js → routes-5A6mia6C.js} +1 -1
  31. package/bundle/web-dist/client/assets/{services.generated-C6jfEwfK.js → services.generated-Bg2XkD-D.js} +1 -1
  32. package/bundle/web-dist/client/assets/settings-BpP9WVFI.js +1 -0
  33. package/bundle/web-dist/client/assets/show-D5ifZXdy.js +2 -0
  34. package/bundle/web-dist/client/assets/tokens-Cfe6WvLK.js +1 -0
  35. package/bundle/web-dist/client/assets/{use-oauth-login-flow-Cjdnj_8D.js → use-oauth-login-flow-BTLUGhwh.js} +1 -1
  36. package/bundle/web-dist/client/assets/{use-update-token-mutation-BIvO70-M.js → use-update-token-mutation-Dkv_HG-C.js} +1 -1
  37. package/bundle/web-dist/client/index.html +3 -3
  38. package/bundle/web-dist/server/assets/{lazyRouteComponent-C_k4R5n6.js → lazyRouteComponent-DyizoQKi.js} +2 -2
  39. package/bundle/web-dist/server/assets/{logs-lBOU5eUP.js → logs-DO8zBdBR.js} +12 -5
  40. package/bundle/web-dist/server/assets/{monit-BW2X1D23.js → monit-COH4MckI.js} +2 -2
  41. package/bundle/web-dist/server/assets/{plus-DilmHifT.js → plus-C1D8z9o0.js} +1 -1
  42. package/bundle/web-dist/server/assets/{react-dom-CHRRsKwb.js → react-dom-PfWvArTX.js} +1 -1
  43. package/bundle/web-dist/server/assets/request-model-DXclJfdP.js +13 -0
  44. package/bundle/web-dist/server/assets/{routes-B408iqh1.js → routes-Btja-741.js} +4 -4
  45. package/bundle/web-dist/server/assets/{services.generated-buX3cJzR.js → services.generated-BsKOUSXl.js} +1 -1
  46. package/bundle/web-dist/server/assets/{settings-CFoXQaAg.js → settings-DPcWGvy-.js} +3 -3
  47. package/bundle/web-dist/server/assets/{show-CcZJO6WF.js → show-CMXbb3TJ.js} +10 -4
  48. package/bundle/web-dist/server/assets/{tokens-C32lycTe.js → tokens-DAUmTrgO.js} +15 -11
  49. package/bundle/web-dist/server/assets/{use-oauth-login-flow-DCjxIa80.js → use-oauth-login-flow-GsGwqefs.js} +1 -1
  50. package/bundle/web-dist/server/assets/{use-update-token-mutation-CqK8i8Fa.js → use-update-token-mutation-RujWERvj.js} +1 -1
  51. package/bundle/web-dist/server/entry-server.generated.js +275 -51
  52. package/package.json +1 -1
  53. package/skills/qgrid/SKILL.md +10 -1
  54. package/skills/qgrid/references/ai-sdk-provider-contract.md +1 -1
  55. package/skills/qgrid/references/anthropic-claude-code-runtime.md +4 -2
  56. package/skills/qgrid/references/decision-rationale.md +11 -2
  57. package/skills/qgrid/references/openai-codex-runtime.md +30 -5
  58. package/skills/qgrid/references/provider-runtime-differences.md +1 -1
  59. package/skills/qgrid/references/request-log-run-lifecycle.md +3 -1
  60. package/skills/qgrid/references/sonamu-api-web-flow.md +9 -1
  61. package/skills/qgrid/references/token-auth-quota-lifecycle.md +2 -0
  62. package/skills/qgrid/references/verification-and-debugging.md +4 -1
  63. package/bundle/web-dist/client/assets/index-BshX4Gd8.css +0 -1
  64. package/bundle/web-dist/client/assets/index-BtVmANcA.js +0 -106
  65. package/bundle/web-dist/client/assets/logs-BCuaLfPL.js +0 -1
  66. package/bundle/web-dist/client/assets/settings-DbkDAJfh.js +0 -1
  67. package/bundle/web-dist/client/assets/show-Dx5JFzwy.js +0 -2
  68. package/bundle/web-dist/client/assets/tokens-DHahzDQJ.js +0 -1
  69. /package/bundle/web-dist/client/assets/{plus-p4vtg6LN.js → plus-CknMNCP5.js} +0 -0
@@ -1,6 +1,6 @@
1
- import { A as matchMutation, B as __require, C as infiniteQueryBehavior, D as functionalUpdate, E as notifyManager, F as skipToken, I as focusManager, L as Subscribable, M as noop$1, N as partialMatchKey, O as hashKey, P as resolveStaleTime, R as __commonJSMin, S as Mutation, T as onlineManager, V as __toESM, b as require_jsx_runtime, c as SD, d as _enum, f as number$1, j as matchQuery, k as hashQueryKeyByOptions, l as SUPPORTED_LOCALES, m as string$2, n as QgridService, o as TokenService, p as object, u as setLocale, v as QueryClientProvider, w as Query, x as require_react, z as __exportAll } from "./assets/services.generated-buX3cJzR.js";
2
- import { A as decodePath, C as findRouteMatch, D as createLRUCache, E as processRouteTree, F as last, I as replaceEqualDeep, M as findLast, N as functionalUpdate$1, O as invariant, P as isPromise, S as findFlatMatch, T as processRouteMasks, _ as interpolatePath, a as Link$1, b as trimPath, c as usePrevious$1, d as useRouter, f as getRouterContext, g as cleanPath, h as isNotFound, i as createRootRouteWithContext, j as deepEqual$2, k as createControlledPromise, l as matchContext, m as rootRouteId, n as createFileRoute, p as warning, r as createLazyFileRoute, s as useLayoutEffect$3, t as lazyRouteComponent, u as useRouterState, v as joinPaths, w as findSingleMatch, x as trimPathRight, y as resolvePath } from "./assets/lazyRouteComponent-C_k4R5n6.js";
3
- import { n as require_shim, t as require_react_dom } from "./assets/react-dom-CHRRsKwb.js";
1
+ import { A as hashQueryKeyByOptions, B as __exportAll, C as Mutation, D as notifyManager, E as onlineManager, F as resolveStaleTime, H as __toESM, I as skipToken, L as focusManager, M as matchQuery, N as noop$1, O as functionalUpdate, P as partialMatchKey, R as Subscribable, S as require_react, T as Query, V as __require, d as setLocale, f as _enum, h as string$2, j as matchMutation, k as hashKey, l as SD, m as object, n as QgridService, o as TokenService, p as number$1, s as fetch$1, u as SUPPORTED_LOCALES, w as infiniteQueryBehavior, x as require_jsx_runtime, y as QueryClientProvider, z as __commonJSMin } from "./assets/services.generated-BsKOUSXl.js";
2
+ import { A as decodePath, C as findRouteMatch, D as createLRUCache, E as processRouteTree, F as last, I as replaceEqualDeep, M as findLast, N as functionalUpdate$1, O as invariant, P as isPromise, S as findFlatMatch, T as processRouteMasks, _ as interpolatePath, a as Link$1, b as trimPath, c as usePrevious$1, d as useRouter, f as getRouterContext, g as cleanPath, h as isNotFound, i as createRootRouteWithContext, j as deepEqual$2, k as createControlledPromise, l as matchContext, m as rootRouteId, n as createFileRoute, p as warning, r as createLazyFileRoute, s as useLayoutEffect$3, t as lazyRouteComponent, u as useRouterState, v as joinPaths, w as findSingleMatch, x as trimPathRight, y as resolvePath } from "./assets/lazyRouteComponent-DyizoQKi.js";
3
+ import { n as require_shim, t as require_react_dom } from "./assets/react-dom-PfWvArTX.js";
4
4
  import minpath from "node:path";
5
5
  import minproc from "node:process";
6
6
  import { fileURLToPath as urlToPath } from "node:url";
@@ -46060,6 +46060,45 @@ var lucideX = (props) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", {
46060
46060
  //#region src/assets/chat-icon.png
46061
46061
  var chat_icon_default = "/assets/chat-icon-JYfue7fB.png";
46062
46062
  //#endregion
46063
+ //#region src/components/qgrid/chat-image-generation.ts
46064
+ function buildChatHistory(messages, includeImages) {
46065
+ return messages.flatMap((message) => {
46066
+ const images = includeImages ? message.images ?? [] : [];
46067
+ if (message.role === "user") return [{
46068
+ type: "message",
46069
+ role: "user",
46070
+ content: [{
46071
+ type: "input_text",
46072
+ text: message.text
46073
+ }, ...images.map((url) => ({
46074
+ type: "input_image",
46075
+ image_url: url
46076
+ }))]
46077
+ }];
46078
+ return [...message.text ? [{
46079
+ type: "message",
46080
+ role: "assistant",
46081
+ content: [{
46082
+ type: "output_text",
46083
+ text: message.text
46084
+ }]
46085
+ }] : [], ...images.length ? [{
46086
+ type: "message",
46087
+ role: "user",
46088
+ content: [{
46089
+ type: "input_text",
46090
+ text: "Previously generated assistant images (reference for follow-up edits):"
46091
+ }, ...images.map((url) => ({
46092
+ type: "input_image",
46093
+ image_url: url
46094
+ }))]
46095
+ }] : []];
46096
+ });
46097
+ }
46098
+ function chatGeneratedImages(content) {
46099
+ return content.flatMap((part) => part.type === "image" ? [`data:${part.mediaType ?? "image/png"};base64,${part.data}`] : []);
46100
+ }
46101
+ //#endregion
46063
46102
  //#region src/components/qgrid/chat-token-selection.ts
46064
46103
  function chatTokenOptions(tokens, provider) {
46065
46104
  return tokens.filter((token) => token.active && token.provider === provider).toSorted((a, b) => a.ord - b.ord || a.name.localeCompare(b.name));
@@ -46078,6 +46117,135 @@ function tokenTargetPayload(tokenName) {
46078
46117
  return tokenName ? { tokenName } : {};
46079
46118
  }
46080
46119
  //#endregion
46120
+ //#region src/components/qgrid/chat-image-info.ts
46121
+ var import_react_dom = /* @__PURE__ */ __toESM(require_react_dom(), 1);
46122
+ function chatImageInfo(url) {
46123
+ const comma = url.indexOf(",");
46124
+ const mime = url.slice(0, comma).match(/^data:image\/(png|jpeg|webp);base64$/)?.[1];
46125
+ if (!mime) return {
46126
+ extension: "—",
46127
+ bytes: 0
46128
+ };
46129
+ const padding = url.endsWith("==") ? 2 : url.endsWith("=") ? 1 : 0;
46130
+ return {
46131
+ extension: mime === "jpeg" ? "jpg" : mime,
46132
+ bytes: Math.max(0, Math.floor((url.length - comma - 1) * 3 / 4) - padding)
46133
+ };
46134
+ }
46135
+ function formatImageBytes(bytes) {
46136
+ if (bytes < 1024) return `${bytes} B`;
46137
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
46138
+ return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
46139
+ }
46140
+ //#endregion
46141
+ //#region src/components/qgrid/ChatImagePreview.tsx
46142
+ function ChatImagePreview({ url, onClose }) {
46143
+ const dialogRef = (0, import_react.useRef)(null);
46144
+ const [dimensions, setDimensions] = (0, import_react.useState)(null);
46145
+ const [originalSize, setOriginalSize] = (0, import_react.useState)(false);
46146
+ const [failed, setFailed] = (0, import_react.useState)(false);
46147
+ const info = chatImageInfo(url);
46148
+ (0, import_react.useEffect)(() => {
46149
+ const dialog = dialogRef.current;
46150
+ dialog?.showModal();
46151
+ return () => dialog?.close();
46152
+ }, []);
46153
+ return (0, import_react_dom.createPortal)(/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("dialog", {
46154
+ ref: dialogRef,
46155
+ "aria-labelledby": "chat-image-preview-title",
46156
+ onCancel: (event) => {
46157
+ event.preventDefault();
46158
+ onClose();
46159
+ },
46160
+ onClick: (event) => {
46161
+ if (event.target !== event.currentTarget) return;
46162
+ const rect = event.currentTarget.getBoundingClientRect();
46163
+ if (event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom) onClose();
46164
+ },
46165
+ className: "m-auto w-[min(1100px,calc(100vw-2rem))] max-w-none max-h-[calc(100dvh-2rem)] overflow-hidden rounded-2xl bg-white p-0 text-sand-900 shadow-xl backdrop:bg-black/65",
46166
+ children: [
46167
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
46168
+ className: "flex items-center justify-between gap-3 border-b border-sand-100 px-4 py-3",
46169
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", {
46170
+ id: "chat-image-preview-title",
46171
+ className: "text-sm font-semibold",
46172
+ children: "이미지 미리보기"
46173
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
46174
+ type: "button",
46175
+ onClick: onClose,
46176
+ "aria-label": "이미지 미리보기 닫기",
46177
+ className: "grid size-8 place-items-center rounded-full hover:bg-sand-100",
46178
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(lucideX, { className: "size-4" })
46179
+ })]
46180
+ }),
46181
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
46182
+ className: "max-h-[calc(100dvh-13rem)] overflow-auto bg-sand-100 p-3",
46183
+ style: {
46184
+ backgroundImage: "conic-gradient(#e8e4df 25%, #fff 0 50%, #e8e4df 0 75%, #fff 0)",
46185
+ backgroundSize: "20px 20px"
46186
+ },
46187
+ children: failed ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
46188
+ role: "alert",
46189
+ className: "p-8 text-center text-danger-500",
46190
+ children: "이미지를 불러올 수 없습니다."
46191
+ }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", {
46192
+ src: url,
46193
+ alt: "확대된 채팅 이미지",
46194
+ onLoad: (event) => setDimensions({
46195
+ width: event.currentTarget.naturalWidth,
46196
+ height: event.currentTarget.naturalHeight
46197
+ }),
46198
+ onError: () => setFailed(true),
46199
+ className: originalSize ? "block max-w-none" : "mx-auto block max-h-[calc(100dvh-15rem)] max-w-full object-contain"
46200
+ })
46201
+ }),
46202
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
46203
+ className: "flex flex-wrap items-center justify-between gap-3 border-t border-sand-100 px-4 py-3",
46204
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("dl", {
46205
+ className: "flex flex-wrap gap-x-5 gap-y-1 text-xs tabular-nums",
46206
+ children: [
46207
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", {
46208
+ className: "text-sand-400",
46209
+ children: "확장자"
46210
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", {
46211
+ className: "mt-1 uppercase",
46212
+ children: info.extension
46213
+ })] }),
46214
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", {
46215
+ className: "text-sand-400",
46216
+ children: "파일 용량"
46217
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", {
46218
+ className: "mt-1",
46219
+ children: formatImageBytes(info.bytes)
46220
+ })] }),
46221
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", {
46222
+ className: "text-sand-400",
46223
+ children: "해상도"
46224
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", {
46225
+ className: "mt-1",
46226
+ children: dimensions ? `${dimensions.width} × ${dimensions.height} px` : "—"
46227
+ })] })
46228
+ ]
46229
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
46230
+ className: "flex items-center gap-3 text-xs",
46231
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
46232
+ type: "button",
46233
+ disabled: !dimensions || failed,
46234
+ onClick: () => setOriginalSize((value) => !value),
46235
+ className: "text-sand-600 hover:underline disabled:opacity-40",
46236
+ children: originalSize ? "화면에 맞추기" : "원본 크기"
46237
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", {
46238
+ href: url,
46239
+ download: `qgrid-image.${info.extension}`,
46240
+ className: "text-sienna-500 hover:underline",
46241
+ children: "다운로드"
46242
+ })]
46243
+ })]
46244
+ })
46245
+ ]
46246
+ }), document.body);
46247
+ }
46248
+ //#endregion
46081
46249
  //#region src/components/qgrid/ChatWidget.tsx
46082
46250
  var CHAT_PROJECT_NAME = "qgrid_chat";
46083
46251
  var DEFAULT_MODEL = "anthropic/claude-fable-5-1";
@@ -46085,18 +46253,21 @@ var MODEL_PRESET_GROUPS = [{
46085
46253
  label: "OpenAI",
46086
46254
  models: [
46087
46255
  "openai/gpt-6-astra",
46256
+ "openai/gpt-6-sol",
46257
+ "openai/gpt-6-luna",
46088
46258
  "openai/gpt-5.6-terra",
46089
46259
  "openai/gpt-5.6-sol",
46090
46260
  "openai/gpt-5.6-luna",
46091
- "openai/gpt-5.5",
46092
- "openai/gpt-5.3-codex-spark"
46261
+ "openai/gpt-5.5"
46093
46262
  ]
46094
46263
  }, {
46095
46264
  label: "Anthropic",
46096
46265
  models: [
46097
46266
  "anthropic/claude-fable-5-1",
46098
46267
  "anthropic/claude-fable-5",
46268
+ "anthropic/claude-opus-5-5",
46099
46269
  "anthropic/claude-opus-5",
46270
+ "anthropic/claude-sonnet-5-5",
46100
46271
  "anthropic/claude-sonnet-5",
46101
46272
  "anthropic/claude-opus-4-8",
46102
46273
  "anthropic/claude-opus-4-7",
@@ -46104,10 +46275,7 @@ var MODEL_PRESET_GROUPS = [{
46104
46275
  "anthropic/claude-opus-4-5",
46105
46276
  "anthropic/claude-sonnet-4-6",
46106
46277
  "anthropic/claude-sonnet-4-5",
46107
- "anthropic/claude-haiku-4-5",
46108
- "anthropic/claude-opus-4-1",
46109
- "anthropic/claude-opus-4",
46110
- "anthropic/claude-sonnet-4"
46278
+ "anthropic/claude-haiku-4-5"
46111
46279
  ]
46112
46280
  }];
46113
46281
  var EFFORT_PRESETS = [
@@ -46115,26 +46283,6 @@ var EFFORT_PRESETS = [
46115
46283
  "medium",
46116
46284
  "high"
46117
46285
  ];
46118
- function buildHistory(messages) {
46119
- const items = [];
46120
- for (const m of messages) if (m.role === "user") items.push({
46121
- type: "message",
46122
- role: "user",
46123
- content: [{
46124
- type: "input_text",
46125
- text: m.text
46126
- }]
46127
- });
46128
- else if (m.text) items.push({
46129
- type: "message",
46130
- role: "assistant",
46131
- content: [{
46132
- type: "output_text",
46133
- text: m.text
46134
- }]
46135
- });
46136
- return items;
46137
- }
46138
46286
  function errorMessage(e) {
46139
46287
  if (e instanceof Error) return e.message;
46140
46288
  return String(e);
@@ -46176,6 +46324,9 @@ function ChatWidget() {
46176
46324
  const [streamId, setStreamId] = (0, import_react.useState)(null);
46177
46325
  const [busy, setBusy] = (0, import_react.useState)(false);
46178
46326
  const [attachments, setAttachments] = (0, import_react.useState)([]);
46327
+ const [previewImage, setPreviewImage] = (0, import_react.useState)(null);
46328
+ const requestAbortRef = (0, import_react.useRef)(null);
46329
+ (0, import_react.useEffect)(() => () => requestAbortRef.current?.abort(), []);
46179
46330
  const threadCoordRef = (0, import_react.useRef)(void 0);
46180
46331
  const lastConfigRef = (0, import_react.useRef)(void 0);
46181
46332
  const scrollRef = (0, import_react.useRef)(null);
@@ -46235,6 +46386,7 @@ function ChatWidget() {
46235
46386
  patchLastAssistant((m) => ({
46236
46387
  ...m,
46237
46388
  text: data.text || m.text,
46389
+ images: chatGeneratedImages(Array.isArray(data.content) ? data.content : [data.content]),
46238
46390
  status: "done",
46239
46391
  meta: {
46240
46392
  model: data.model,
@@ -46284,7 +46436,9 @@ function ChatWidget() {
46284
46436
  if (lastConfigRef.current && chatConfigChanged(lastConfigRef.current, currentConfig)) threadCoordRef.current = void 0;
46285
46437
  lastConfigRef.current = currentConfig;
46286
46438
  const images = supportsImages ? attachments : [];
46287
- const history = buildHistory(messages);
46439
+ const history = buildChatHistory(messages, supportsImages);
46440
+ const controller = new AbortController();
46441
+ requestAbortRef.current = controller;
46288
46442
  setMessages((prev) => [
46289
46443
  ...prev,
46290
46444
  {
@@ -46302,7 +46456,7 @@ function ChatWidget() {
46302
46456
  setAttachments([]);
46303
46457
  setBusy(true);
46304
46458
  try {
46305
- const { streamId: id } = await QgridService.prepareStream({
46459
+ const args = {
46306
46460
  prompt,
46307
46461
  model,
46308
46462
  projectName: CHAT_PROJECT_NAME,
@@ -46315,13 +46469,49 @@ function ChatWidget() {
46315
46469
  })) } : {},
46316
46470
  ...history.length > 0 ? { history: JSON.stringify(history) } : {},
46317
46471
  ...threadCoordRef.current ? { runContext: { threadCoord: threadCoordRef.current } } : {}
46318
- });
46472
+ };
46473
+ if (supportsImages) {
46474
+ const result = await fetch$1({
46475
+ method: "POST",
46476
+ url: "/api/qgrid/query",
46477
+ data: { args: {
46478
+ ...args,
46479
+ imageGeneration: "auto"
46480
+ } },
46481
+ signal: controller.signal
46482
+ });
46483
+ if (requestAbortRef.current !== controller || controller.signal.aborted) return;
46484
+ threadCoordRef.current = result.runContext?.threadCoord;
46485
+ patchLastAssistant((m) => ({
46486
+ ...m,
46487
+ text: result.text,
46488
+ images: chatGeneratedImages(result.content),
46489
+ status: "done",
46490
+ meta: {
46491
+ model: result.model,
46492
+ tokenName: result.tokenName,
46493
+ durationMs: result.durationMs,
46494
+ costUsd: result.costUsd,
46495
+ requestLogId: result.runContext?.requestLogId,
46496
+ fallback: (result.modelFallbacks?.length ?? 0) > 0
46497
+ }
46498
+ }));
46499
+ setBusy(false);
46500
+ requestAbortRef.current = null;
46501
+ return;
46502
+ }
46503
+ const { streamId: id } = await QgridService.prepareStream(args);
46504
+ if (requestAbortRef.current !== controller || controller.signal.aborted) return;
46319
46505
  setStreamId(id);
46320
46506
  } catch (e) {
46507
+ if (requestAbortRef.current !== controller || controller.signal.aborted) return;
46321
46508
  failStreaming(errorMessage(e));
46509
+ requestAbortRef.current = null;
46322
46510
  }
46323
46511
  };
46324
46512
  const stop = () => {
46513
+ requestAbortRef.current?.abort();
46514
+ requestAbortRef.current = null;
46325
46515
  patchLastAssistant((m) => m.status === "streaming" ? {
46326
46516
  ...m,
46327
46517
  status: "aborted"
@@ -46342,9 +46532,12 @@ function ChatWidget() {
46342
46532
  threadCoordRef.current = void 0;
46343
46533
  lastConfigRef.current = void 0;
46344
46534
  };
46345
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
46535
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
46346
46536
  className: "fixed bottom-5 right-5 z-40",
46347
- children: open ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
46537
+ children: [previewImage && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ChatImagePreview, {
46538
+ url: previewImage,
46539
+ onClose: () => setPreviewImage(null)
46540
+ }, previewImage), open ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
46348
46541
  onAnimationEnd: () => {
46349
46542
  if (panelClosing) {
46350
46543
  setOpen(false);
@@ -46493,10 +46686,16 @@ function ChatWidget() {
46493
46686
  className: "self-end max-w-[80%] flex flex-col items-end gap-1",
46494
46687
  children: [m.images && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
46495
46688
  className: "flex flex-wrap justify-end gap-1",
46496
- children: m.images.map((url, j) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", {
46497
- src: url,
46498
- alt: "",
46499
- className: "size-24 rounded-xl object-cover"
46689
+ children: m.images.map((url, j) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
46690
+ type: "button",
46691
+ onClick: () => setPreviewImage(url),
46692
+ "aria-label": `첨부 이미지 ${j + 1} 크게 보기`,
46693
+ className: "cursor-zoom-in rounded-xl",
46694
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", {
46695
+ src: url,
46696
+ alt: "",
46697
+ className: "size-24 rounded-xl object-cover"
46698
+ })
46500
46699
  }, j))
46501
46700
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
46502
46701
  className: "rounded-[18px] rounded-br-[5px] bg-sienna-600 px-3.5 py-2 text-[13px] leading-relaxed text-white whitespace-pre-wrap break-words",
@@ -46507,6 +46706,26 @@ function ChatWidget() {
46507
46706
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
46508
46707
  className: "rounded-[18px] rounded-bl-[5px] bg-sand-100 px-3.5 py-2",
46509
46708
  children: [
46709
+ m.images?.map((url, imageIndex) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
46710
+ className: "my-1.5",
46711
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
46712
+ type: "button",
46713
+ onClick: () => setPreviewImage(url),
46714
+ "aria-label": `생성된 이미지 ${imageIndex + 1} 크게 보기`,
46715
+ className: "block w-full cursor-zoom-in rounded-xl",
46716
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", {
46717
+ src: url,
46718
+ alt: `생성된 이미지 ${imageIndex + 1}`,
46719
+ onLoad: () => scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight }),
46720
+ className: "w-full rounded-xl object-contain bg-white"
46721
+ })
46722
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("a", {
46723
+ href: url,
46724
+ download: `qgrid-image-${imageIndex + 1}.${url.startsWith("data:image/jpeg;") ? "jpg" : url.startsWith("data:image/webp;") ? "webp" : "png"}`,
46725
+ className: "mt-1 inline-block text-[11px] text-sienna-500 hover:underline",
46726
+ children: "이미지 다운로드"
46727
+ })]
46728
+ }, imageIndex)),
46510
46729
  m.text ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
46511
46730
  className: "prose prose-sm prose-qgrid max-w-none text-[13px] leading-relaxed break-words",
46512
46731
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Markdown, {
@@ -46569,10 +46788,16 @@ function ChatWidget() {
46569
46788
  className: "flex gap-1.5 pb-2",
46570
46789
  children: attachments.map((url, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
46571
46790
  className: "relative",
46572
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", {
46573
- src: url,
46574
- alt: "",
46575
- className: "size-12 rounded-xl object-cover"
46791
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
46792
+ type: "button",
46793
+ onClick: () => setPreviewImage(url),
46794
+ "aria-label": `첨부할 이미지 ${i + 1} 크게 보기`,
46795
+ className: "cursor-zoom-in rounded-xl",
46796
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", {
46797
+ src: url,
46798
+ alt: "",
46799
+ className: "size-12 rounded-xl object-cover"
46800
+ })
46576
46801
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
46577
46802
  type: "button",
46578
46803
  onClick: () => setAttachments((prev) => prev.filter((_, j) => j !== i)),
@@ -46644,12 +46869,12 @@ function ChatWidget() {
46644
46869
  alt: "",
46645
46870
  className: "size-full object-cover"
46646
46871
  })
46647
- })
46872
+ })]
46648
46873
  });
46649
46874
  }
46650
46875
  //#endregion
46651
46876
  //#region src/components/Sidebar.tsx
46652
- var qgridVersion = "2.9.6";
46877
+ var qgridVersion = "2.9.9";
46653
46878
  function Sidebar({ className }) {
46654
46879
  const pathname = useRouterState({ select: (s) => s.location.pathname });
46655
46880
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("aside", {
@@ -47107,7 +47332,6 @@ function isFunction$3(value) {
47107
47332
  }
47108
47333
  //#endregion
47109
47334
  //#region ../../node_modules/.pnpm/@radix-ui+react-primitive@2.1.3_@types+react-dom@19.2.3_@types+react@19.2.14__@types+re_1181ea5061ec9212248424669240e4ec/node_modules/@radix-ui/react-primitive/dist/index.mjs
47110
- var import_react_dom = /* @__PURE__ */ __toESM(require_react_dom(), 1);
47111
47335
  var Primitive$1 = [
47112
47336
  "a",
47113
47337
  "button",
@@ -68534,15 +68758,15 @@ function RootComponent() {
68534
68758
  }
68535
68759
  //#endregion
68536
68760
  //#region src/routes/tokens.tsx
68537
- var $$splitComponentImporter$5 = () => import("./assets/tokens-C32lycTe.js");
68761
+ var $$splitComponentImporter$5 = () => import("./assets/tokens-DAUmTrgO.js");
68538
68762
  var Route$5 = createFileRoute("/tokens")({ component: lazyRouteComponent($$splitComponentImporter$5, "component") });
68539
68763
  //#endregion
68540
68764
  //#region src/routes/settings.tsx
68541
- var $$splitComponentImporter$4 = () => import("./assets/settings-CFoXQaAg.js");
68765
+ var $$splitComponentImporter$4 = () => import("./assets/settings-DPcWGvy-.js");
68542
68766
  var Route$4 = createFileRoute("/settings")({ component: lazyRouteComponent($$splitComponentImporter$4, "component") });
68543
68767
  //#endregion
68544
68768
  //#region src/routes/monit.tsx
68545
- var $$splitComponentImporter$3 = () => import("./assets/monit-BW2X1D23.js");
68769
+ var $$splitComponentImporter$3 = () => import("./assets/monit-COH4MckI.js");
68546
68770
  var monitSearchSchema = object({ level: _enum([
68547
68771
  "all",
68548
68772
  "warn",
@@ -68554,7 +68778,7 @@ var Route$3 = createFileRoute("/monit")({
68554
68778
  });
68555
68779
  //#endregion
68556
68780
  //#region src/routes/logs.tsx
68557
- var $$splitComponentImporter$2 = () => import("./assets/logs-lBOU5eUP.js");
68781
+ var $$splitComponentImporter$2 = () => import("./assets/logs-DO8zBdBR.js");
68558
68782
  var logsSearchSchema = object({
68559
68783
  token: string$2().optional(),
68560
68784
  project: string$2().optional(),
@@ -68568,11 +68792,11 @@ var Route$2 = createFileRoute("/logs")({
68568
68792
  });
68569
68793
  //#endregion
68570
68794
  //#region src/routes/index.tsx
68571
- var $$splitComponentImporter$1 = () => import("./assets/routes-B408iqh1.js");
68795
+ var $$splitComponentImporter$1 = () => import("./assets/routes-Btja-741.js");
68572
68796
  var Route$1 = createFileRoute("/")({ component: lazyRouteComponent($$splitComponentImporter$1, "component") });
68573
68797
  //#endregion
68574
68798
  //#region src/routes/requests/show.tsx
68575
- var $$splitComponentImporter = () => import("./assets/show-CcZJO6WF.js");
68799
+ var $$splitComponentImporter = () => import("./assets/show-CMXbb3TJ.js");
68576
68800
  var showSearchSchema = object({ id: number$1() });
68577
68801
  var Route = createFileRoute("/requests/show")({
68578
68802
  validateSearch: showSearchSchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cartanova/qgrid-cli",
3
- "version": "2.9.6",
3
+ "version": "2.9.9",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/cartanova-ai/qgrid"
@@ -53,7 +53,15 @@ Request logging is enabled by default. Use `providerOptions.qgrid.logger: false`
53
53
  - Prefer `getPuri()` for Sonamu model queries. Treat `getDB()` as a legacy escape hatch only when Puri cannot express the required query. Inside `@transactional` methods, all participating queries must use `getPuri()`; `getDB()` does not reuse Sonamu's ambient transaction connection.
54
54
  - Never hand-author migration files for Sonamu-managed schema. In the qgrid source repository, change the entity definition, inspect `sonamu migrate status`, and create migration files with `mise exec -- pnpm --dir packages/api sonamu migrate generate`. Inspect the generated files before applying them. If Sonamu cannot express a required schema change, stop and ask rather than silently replacing its workflow with a custom migration.
55
55
  - Keep OpenAI Codex built-in tools, apps, plugins, skills, web search, shell, and environment instruction blocks disabled unless the user explicitly asks for agentic Codex behavior.
56
- - Treat OpenAI image generation as opt-in. Transparent background requests use Codex standalone Images endpoints with subscription credentials; other image requests use the hosted Responses image tool. Inspect current code before modifying either route; image costs remain API-price estimates.
56
+ - Treat OpenAI image generation as opt-in outside dashboard chat. In 2.9.8, OpenAI `qgrid_chat` opts into raw API `imageGeneration: "auto"` on every turn; the public AI SDK option remains boolean. Ordinary images use the hosted Responses image tool; a server-executed transparent-image function calls Codex standalone Images directly with subscription credentials. Do not reintroduce a client decision tool followed by another GPT generation request. Image costs remain API-price estimates.
57
+
58
+ ## Automatic chat images (2.9.8+)
59
+
60
+ - Raw `imageGeneration: "auto"` permits text-only replies; explicit `true` still requires an image. Both use non-stream requests. Auto mode rejects caller tools and output schemas.
61
+ - Dashboard chat sends complete text and image history, including prior generated images as user visual references. Do not assume provider conversation state retains image bytes or send assistant `input_image` blocks.
62
+ - The model chooses ordinary `image_generation` or `generate_transparent_image`; the server runs the latter directly with the selected token and cancellation signal. Preserve driver usage separately from image usage.
63
+ - Request-log model labels show requested model to image model using serving/image-cost metadata. Do not hardcode `gpt-image-2.5` or restore image-generation/assumption badges. In auto mode, mark image generation only when an image was produced.
64
+ - Read `references/decision-rationale.md` and `references/openai-codex-runtime.md` before changing this routing or its cache boundary.
57
65
 
58
66
  ## Image output formats (2.9.6+)
59
67
 
@@ -61,6 +69,7 @@ Request logging is enabled by default. Use `providerOptions.qgrid.logger: false`
61
69
  - Preserve the actual returned MIME type in qgrid content, AI SDK files, and request-log image URLs; do not relabel bytes as the requested format.
62
70
  - Transparent generation keeps the standalone Codex Images route and accepts PNG only. Reject transparent JPEG/WebP requests before generation.
63
71
  - Keep the AI SDK standard `GeneratedFile.mediaType: string` contract. Do not add a qgrid-specific image wrapper or conditional return type.
72
+ - A 2026-09-29 live probe returned PNG for a prompt-only WebP request and WebP when `outputFormat: "webp"` was supplied. Model prose is not format evidence; inspect file bytes. Chat does not infer output format from prose, and no post-generation conversion is performed.
64
73
 
65
74
  ## Verification
66
75
 
@@ -63,7 +63,7 @@ Do not write `providerOptions: { ... } satisfies QgridProviderOptions`: the expo
63
63
  rejected by the SDK before transport; omission keeps weighted round-robin routing.
64
64
  - `logger`: request-log switch. Omitted or `true` is the default and enables qgrid logging; `false` guarantees that generation creates zero qgrid request-log rows.
65
65
  - `sessionKey`: source for model-scoped opaque OpenAI prompt-cache affinity. Disabled for Anthropic models.
66
- - `effort`: provider-specific vocabulary. OpenAI (ChatGPT-subscription Codex route): `low | medium | high | xhigh | max | ultra`, where GPT-6 Astra, GPT-5.6 Sol, and Terra support `ultra`, and Luna supports through `max`; the public OpenAI API's `none`/`minimal` do not exist on this route. Anthropic (Claude Code `--effort`): `low | medium | high | xhigh | max`. The server resolves the value per provider and model (`providers/common/effort.ts`): anything outside the provider set, or above the Codex model's supported maximum, is silently dropped and treated as unspecified — Anthropic falls back to qgrid's default `low`, OpenAI omits `reasoning` so the backend default applies. No error, no metadata; a debug log line only. Use `QgridOpenAIProviderOptions` / `QgridAnthropicProviderOptions` for provider-typed `satisfies`; `QgridProviderOptions` is their union.
66
+ - `effort`: provider-specific vocabulary. OpenAI (ChatGPT-subscription Codex route): `low | medium | high | xhigh | max | ultra`, where GPT-6 Astra, GPT-6 Sol, GPT-5.6 Sol, and Terra support `ultra`, and GPT-6 Luna and GPT-5.6 Luna support through `max`; the public OpenAI API's `none`/`minimal` do not exist on this route. Anthropic (Claude Code `--effort`): `low | medium | high | xhigh | max`. The server resolves the value per provider and model (`providers/common/effort.ts`): anything outside the provider set, or above the Codex model's supported maximum, is silently dropped and treated as unspecified — Anthropic falls back to qgrid's default `low`, OpenAI omits `reasoning` so the backend default applies. No error, no metadata; a debug log line only. Use `QgridOpenAIProviderOptions` / `QgridAnthropicProviderOptions` for provider-typed `satisfies`; `QgridProviderOptions` is their union.
67
67
  - `verbosity`: OpenAI/Codex route only.
68
68
  - `reasoningSummary`: OpenAI/Codex route only.
69
69
  - `serviceTier`: OpenAI/Codex route only.
@@ -77,7 +77,7 @@ claude -p
77
77
  --model <canonical-model-or-1m-suffix>
78
78
  --system-prompt <text> # small system prompt
79
79
  --system-prompt-file <path> # large system prompt
80
- --thinking disabled # omitted for Fable 5/5.1 and Opus 5
80
+ --thinking disabled # omitted for Fable 5/5.1, Opus 5, Opus 5.5 and Sonnet 5.5
81
81
  --effort <effort-or-low>
82
82
  --disable-slash-commands
83
83
  --session-id <uuid>
@@ -97,7 +97,7 @@ Important details:
97
97
  - Large system prompts over 64 KiB are written to a temporary file to avoid argv `E2BIG` — this
98
98
  same branch absorbs large injected schema contracts, so the old 64 KiB schema argv limit no
99
99
  longer applies (only the global 512 KiB caller-schema complexity limit remains).
100
- - `--thinking disabled`, `MAX_THINKING_TOKENS=0`, and adaptive thinking env suppression keep thinking off for existing models. Fable 5 and Fable 5.1 require always-on adaptive thinking (CLI catalog `rejects_disabled_thinking`). Opus 5 defaults to adaptive thinking and rejects disabled thinking at `xhigh`/`max` effort. qgrid omits all three suppressors for these models (`usesAdaptiveThinking`) and uses `effort` to control depth.
100
+ - `--thinking disabled`, `MAX_THINKING_TOKENS=0`, and adaptive thinking env suppression keep thinking off for existing models. Fable 5, Fable 5.1, Opus 5.5 and Sonnet 5.5 require always-on adaptive thinking (Fable: CLI catalog `rejects_disabled_thinking`; Opus 5.5: official spec, thinking cannot be turned off; Sonnet 5.5: API `{type: "disabled"}` returns 400 and the CLI catalog marks `rejects_disabled_thinking`). Opus 5 defaults to adaptive thinking and rejects disabled thinking at `xhigh`/`max` effort. qgrid omits all three suppressors for these models (`usesAdaptiveThinking`) and uses `effort` to control depth.
101
101
 
102
102
  ## Spawn env
103
103
 
@@ -257,6 +257,8 @@ qgrid's exact 1M support set currently includes:
257
257
  - `claude-fable-5-1`
258
258
  - `claude-fable-5`
259
259
  - `claude-opus-5`
260
+ - `claude-opus-5-5`
261
+ - `claude-sonnet-5-5`
260
262
  - `claude-sonnet-5`
261
263
  - `claude-sonnet-4-6`
262
264
  - `claude-opus-4-6`
@@ -218,15 +218,24 @@ Sources:
218
218
  - `docs/plans/2026-07-05-001-feat-codex-image-generation-plan.md`
219
219
  - `docs/brainstorms/2026-07-06-qgrid-image-persistence-requirements.md`
220
220
  - `docs/brainstorms/2026-07-06-codex-image-cost-estimate.md`
221
+ - `docs/solutions/architecture-patterns/qgrid-native-auto-image-generation-2.9.8.md`
221
222
 
222
223
  Key decisions:
223
224
 
224
- - Image generation is OpenAI/Codex-only and request-level opt-in. Most requests use the Codex-hosted `image_generation` tool. Explicit transparent backgrounds use Codex standalone Images because the hosted route rejected them in live probes. Always-on image tools would change every turn's tool configuration and threaten prompt-cache stability.
225
+ - Image generation is OpenAI/Codex-only and request-level opt-in outside dashboard chat. In 2.9.8, chat explicitly opts into raw `imageGeneration: "auto"` each turn. Most requests use the Codex-hosted `image_generation` tool. Transparent backgrounds use Codex standalone Images because the hosted route rejected them in live probes. Do not globally enable image tools for other callers: that would change their tool framing and cache behavior.
225
226
  - It is non-stream only. Codex returns completed base64 image payloads, not useful image deltas.
226
227
  - Image turns send full input directly and retain no provider conversation state. This prevents base64 payloads from entering reusable state.
227
- - qgrid performs preflight capability/model gates and postflight "image count must be greater than zero" checks. Codex can otherwise silently return text when image generation is unavailable or unused.
228
+ - qgrid performs preflight capability/model gates. Explicit `imageGeneration: true` requires an image; `"auto"` allows normal text replies but still rejects attempted image generation without a completed image. The public AI SDK retains its boolean option and image-required contract.
228
229
  - Returned images are inline base64 content parts for consumers and AI SDK `file` parts. Reference images are accepted through AI SDK multimodal message parts only on the `imageGeneration` path, and are transported as JSON data URLs with SDK-side size guarding.
229
230
  - `imageGenerationOptions` supports quality, size, and background. Hosted-route pricing assumptions use `gpt-image-2`, `medium`, and `1536x1024` when omitted; these do not establish actual output settings. Transparent-route image usage and observed settings are recorded separately, and no background-removal fallback is allowed.
230
231
  - qgrid does not manage generated or reference images as durable assets through a `generated_images` table or object-storage layer. Current request logs can still contain inline image data URLs for inspection and synthetic `image_generation` tool-call steps; reference input images live in the first synthetic step's `tool_args.inputImages`.
231
232
  - Image cost is stored separately in `request_logs.image_cost_usd`; `request_logs.cost_usd` remains the Codex driver model token cost. Because Codex does not expose exact image tool usage, `image_cost_usd` is a price-table estimate and may be inaccurate.
232
233
  - Inspect current plans, tests, and implementation before changing behavior; the Codex tool surface can change underneath qgrid.
234
+
235
+ The 2.9.8 chat design removes the discarded client `request_image_generation` decision step, which asked GPT to choose a tool and then called GPT again for ordinary generation. Native `image_generation` handles ordinary generation directly. A separate qgrid-defined `generate_transparent_image` function is necessary to reach the standalone transparent endpoint, but the server executes it with the same token and cancellation signal without another GPT request. Routing belongs to the server; the chat only sends context and displays results.
236
+
237
+ Image edits need complete prior image bytes, not just text history or an assumption that the backend remembers earlier outputs. Chat replays uploaded and generated images, representing generated images as user visual references because assistant `input_image` blocks are invalid. Image previews report actual extension, decoded byte size, and natural dimensions. Uploaded images describe the existing resized JPEG sent to the model; generated images retain their returned bytes.
238
+
239
+ Model labels show requested model to image model using serving metadata or the image model encoded in the logged cost method. The cost-method fallback remains an accounting assumption, not new evidence of the upstream model version. The UI has no image-generation or assumption badge and never fixes the name to `gpt-image-2.5`. Hosted and auto transparent logs retain driver identity and token costs separately from image metadata/cost.
240
+
241
+ Prompt wording alone is not a reliable format control: a 2026-09-29 probe returned PNG for a WebP prompt and WebP with explicit `outputFormat: "webp"`. Model prose contradicted the output and must not be used as evidence. Transparent generation remains PNG-only, and this release does not infer format from chat prose or introduce post-generation conversion.