@cartanova/qgrid-cli 2.9.7 → 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 (66) 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 +4 -2
  5. package/bundle/dist/utils/providers/common/model-cost.js +3 -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 +5 -1
  16. package/bundle/src/utils/providers/anthropic/anthropic-constants.ts +4 -2
  17. package/bundle/src/utils/providers/common/model-cost.test.ts +1 -0
  18. package/bundle/src/utils/providers/common/model-cost.ts +3 -0
  19. package/bundle/src/utils/providers/common/provider-dispatcher.ts +1 -1
  20. package/bundle/src/utils/providers/openai/openai-backend-protocol.test.ts +5 -0
  21. package/bundle/src/utils/providers/openai/openai-backend-protocol.ts +2 -2
  22. package/bundle/src/utils/providers/openai/openai-dispatcher.test.ts +114 -0
  23. package/bundle/src/utils/providers/openai/openai-dispatcher.ts +136 -1
  24. package/bundle/web-dist/client/assets/index-D8VwLsuq.js +106 -0
  25. package/bundle/web-dist/client/assets/index-D_sagzsR.css +1 -0
  26. package/bundle/web-dist/client/assets/logs-_T8KV5ne.js +1 -0
  27. package/bundle/web-dist/client/assets/{monit-7tcWgB20.js → monit-DRCrtO2j.js} +1 -1
  28. package/bundle/web-dist/client/assets/request-model-eygfdRv7.js +1 -0
  29. package/bundle/web-dist/client/assets/{routes-CSVp9aNn.js → routes-5A6mia6C.js} +1 -1
  30. package/bundle/web-dist/client/assets/{services.generated-C6jfEwfK.js → services.generated-Bg2XkD-D.js} +1 -1
  31. package/bundle/web-dist/client/assets/settings-BpP9WVFI.js +1 -0
  32. package/bundle/web-dist/client/assets/show-D5ifZXdy.js +2 -0
  33. package/bundle/web-dist/client/assets/tokens-Cfe6WvLK.js +1 -0
  34. package/bundle/web-dist/client/assets/{use-oauth-login-flow-DTrmdduV.js → use-oauth-login-flow-BTLUGhwh.js} +1 -1
  35. package/bundle/web-dist/client/assets/{use-update-token-mutation-BIvO70-M.js → use-update-token-mutation-Dkv_HG-C.js} +1 -1
  36. package/bundle/web-dist/client/index.html +3 -3
  37. package/bundle/web-dist/server/assets/{lazyRouteComponent-C_k4R5n6.js → lazyRouteComponent-DyizoQKi.js} +2 -2
  38. package/bundle/web-dist/server/assets/{logs-lBOU5eUP.js → logs-DO8zBdBR.js} +12 -5
  39. package/bundle/web-dist/server/assets/{monit-BW2X1D23.js → monit-COH4MckI.js} +2 -2
  40. package/bundle/web-dist/server/assets/{plus-DilmHifT.js → plus-C1D8z9o0.js} +1 -1
  41. package/bundle/web-dist/server/assets/{react-dom-CHRRsKwb.js → react-dom-PfWvArTX.js} +1 -1
  42. package/bundle/web-dist/server/assets/request-model-DXclJfdP.js +13 -0
  43. package/bundle/web-dist/server/assets/{routes-B408iqh1.js → routes-Btja-741.js} +4 -4
  44. package/bundle/web-dist/server/assets/{services.generated-buX3cJzR.js → services.generated-BsKOUSXl.js} +1 -1
  45. package/bundle/web-dist/server/assets/{settings-CFoXQaAg.js → settings-DPcWGvy-.js} +3 -3
  46. package/bundle/web-dist/server/assets/{show-CcZJO6WF.js → show-CMXbb3TJ.js} +10 -4
  47. package/bundle/web-dist/server/assets/{tokens-C32lycTe.js → tokens-DAUmTrgO.js} +15 -11
  48. package/bundle/web-dist/server/assets/{use-oauth-login-flow-DCjxIa80.js → use-oauth-login-flow-GsGwqefs.js} +1 -1
  49. package/bundle/web-dist/server/assets/{use-update-token-mutation-CqK8i8Fa.js → use-update-token-mutation-RujWERvj.js} +1 -1
  50. package/bundle/web-dist/server/entry-server.generated.js +272 -51
  51. package/package.json +1 -1
  52. package/skills/qgrid/SKILL.md +10 -1
  53. package/skills/qgrid/references/anthropic-claude-code-runtime.md +3 -2
  54. package/skills/qgrid/references/decision-rationale.md +11 -2
  55. package/skills/qgrid/references/openai-codex-runtime.md +11 -1
  56. package/skills/qgrid/references/request-log-run-lifecycle.md +3 -1
  57. package/skills/qgrid/references/sonamu-api-web-flow.md +9 -1
  58. package/skills/qgrid/references/token-auth-quota-lifecycle.md +2 -0
  59. package/skills/qgrid/references/verification-and-debugging.md +4 -1
  60. package/bundle/web-dist/client/assets/index-BshX4Gd8.css +0 -1
  61. package/bundle/web-dist/client/assets/index-D4MEKFjK.js +0 -106
  62. package/bundle/web-dist/client/assets/logs-BWhH8t7Y.js +0 -1
  63. package/bundle/web-dist/client/assets/settings-CYgR0YTh.js +0 -1
  64. package/bundle/web-dist/client/assets/show-Cg8n_Mxg.js +0 -2
  65. package/bundle/web-dist/client/assets/tokens-B5yg6bP1.js +0 -1
  66. /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";
@@ -46090,8 +46258,7 @@ var MODEL_PRESET_GROUPS = [{
46090
46258
  "openai/gpt-5.6-terra",
46091
46259
  "openai/gpt-5.6-sol",
46092
46260
  "openai/gpt-5.6-luna",
46093
- "openai/gpt-5.5",
46094
- "openai/gpt-5.3-codex-spark"
46261
+ "openai/gpt-5.5"
46095
46262
  ]
46096
46263
  }, {
46097
46264
  label: "Anthropic",
@@ -46100,6 +46267,7 @@ var MODEL_PRESET_GROUPS = [{
46100
46267
  "anthropic/claude-fable-5",
46101
46268
  "anthropic/claude-opus-5-5",
46102
46269
  "anthropic/claude-opus-5",
46270
+ "anthropic/claude-sonnet-5-5",
46103
46271
  "anthropic/claude-sonnet-5",
46104
46272
  "anthropic/claude-opus-4-8",
46105
46273
  "anthropic/claude-opus-4-7",
@@ -46107,10 +46275,7 @@ var MODEL_PRESET_GROUPS = [{
46107
46275
  "anthropic/claude-opus-4-5",
46108
46276
  "anthropic/claude-sonnet-4-6",
46109
46277
  "anthropic/claude-sonnet-4-5",
46110
- "anthropic/claude-haiku-4-5",
46111
- "anthropic/claude-opus-4-1",
46112
- "anthropic/claude-opus-4",
46113
- "anthropic/claude-sonnet-4"
46278
+ "anthropic/claude-haiku-4-5"
46114
46279
  ]
46115
46280
  }];
46116
46281
  var EFFORT_PRESETS = [
@@ -46118,26 +46283,6 @@ var EFFORT_PRESETS = [
46118
46283
  "medium",
46119
46284
  "high"
46120
46285
  ];
46121
- function buildHistory(messages) {
46122
- const items = [];
46123
- for (const m of messages) if (m.role === "user") items.push({
46124
- type: "message",
46125
- role: "user",
46126
- content: [{
46127
- type: "input_text",
46128
- text: m.text
46129
- }]
46130
- });
46131
- else if (m.text) items.push({
46132
- type: "message",
46133
- role: "assistant",
46134
- content: [{
46135
- type: "output_text",
46136
- text: m.text
46137
- }]
46138
- });
46139
- return items;
46140
- }
46141
46286
  function errorMessage(e) {
46142
46287
  if (e instanceof Error) return e.message;
46143
46288
  return String(e);
@@ -46179,6 +46324,9 @@ function ChatWidget() {
46179
46324
  const [streamId, setStreamId] = (0, import_react.useState)(null);
46180
46325
  const [busy, setBusy] = (0, import_react.useState)(false);
46181
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(), []);
46182
46330
  const threadCoordRef = (0, import_react.useRef)(void 0);
46183
46331
  const lastConfigRef = (0, import_react.useRef)(void 0);
46184
46332
  const scrollRef = (0, import_react.useRef)(null);
@@ -46238,6 +46386,7 @@ function ChatWidget() {
46238
46386
  patchLastAssistant((m) => ({
46239
46387
  ...m,
46240
46388
  text: data.text || m.text,
46389
+ images: chatGeneratedImages(Array.isArray(data.content) ? data.content : [data.content]),
46241
46390
  status: "done",
46242
46391
  meta: {
46243
46392
  model: data.model,
@@ -46287,7 +46436,9 @@ function ChatWidget() {
46287
46436
  if (lastConfigRef.current && chatConfigChanged(lastConfigRef.current, currentConfig)) threadCoordRef.current = void 0;
46288
46437
  lastConfigRef.current = currentConfig;
46289
46438
  const images = supportsImages ? attachments : [];
46290
- const history = buildHistory(messages);
46439
+ const history = buildChatHistory(messages, supportsImages);
46440
+ const controller = new AbortController();
46441
+ requestAbortRef.current = controller;
46291
46442
  setMessages((prev) => [
46292
46443
  ...prev,
46293
46444
  {
@@ -46305,7 +46456,7 @@ function ChatWidget() {
46305
46456
  setAttachments([]);
46306
46457
  setBusy(true);
46307
46458
  try {
46308
- const { streamId: id } = await QgridService.prepareStream({
46459
+ const args = {
46309
46460
  prompt,
46310
46461
  model,
46311
46462
  projectName: CHAT_PROJECT_NAME,
@@ -46318,13 +46469,49 @@ function ChatWidget() {
46318
46469
  })) } : {},
46319
46470
  ...history.length > 0 ? { history: JSON.stringify(history) } : {},
46320
46471
  ...threadCoordRef.current ? { runContext: { threadCoord: threadCoordRef.current } } : {}
46321
- });
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;
46322
46505
  setStreamId(id);
46323
46506
  } catch (e) {
46507
+ if (requestAbortRef.current !== controller || controller.signal.aborted) return;
46324
46508
  failStreaming(errorMessage(e));
46509
+ requestAbortRef.current = null;
46325
46510
  }
46326
46511
  };
46327
46512
  const stop = () => {
46513
+ requestAbortRef.current?.abort();
46514
+ requestAbortRef.current = null;
46328
46515
  patchLastAssistant((m) => m.status === "streaming" ? {
46329
46516
  ...m,
46330
46517
  status: "aborted"
@@ -46345,9 +46532,12 @@ function ChatWidget() {
46345
46532
  threadCoordRef.current = void 0;
46346
46533
  lastConfigRef.current = void 0;
46347
46534
  };
46348
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
46535
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
46349
46536
  className: "fixed bottom-5 right-5 z-40",
46350
- 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", {
46351
46541
  onAnimationEnd: () => {
46352
46542
  if (panelClosing) {
46353
46543
  setOpen(false);
@@ -46496,10 +46686,16 @@ function ChatWidget() {
46496
46686
  className: "self-end max-w-[80%] flex flex-col items-end gap-1",
46497
46687
  children: [m.images && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
46498
46688
  className: "flex flex-wrap justify-end gap-1",
46499
- children: m.images.map((url, j) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", {
46500
- src: url,
46501
- alt: "",
46502
- 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
+ })
46503
46699
  }, j))
46504
46700
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
46505
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",
@@ -46510,6 +46706,26 @@ function ChatWidget() {
46510
46706
  children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
46511
46707
  className: "rounded-[18px] rounded-bl-[5px] bg-sand-100 px-3.5 py-2",
46512
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)),
46513
46729
  m.text ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
46514
46730
  className: "prose prose-sm prose-qgrid max-w-none text-[13px] leading-relaxed break-words",
46515
46731
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Markdown, {
@@ -46572,10 +46788,16 @@ function ChatWidget() {
46572
46788
  className: "flex gap-1.5 pb-2",
46573
46789
  children: attachments.map((url, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
46574
46790
  className: "relative",
46575
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("img", {
46576
- src: url,
46577
- alt: "",
46578
- 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
+ })
46579
46801
  }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
46580
46802
  type: "button",
46581
46803
  onClick: () => setAttachments((prev) => prev.filter((_, j) => j !== i)),
@@ -46647,12 +46869,12 @@ function ChatWidget() {
46647
46869
  alt: "",
46648
46870
  className: "size-full object-cover"
46649
46871
  })
46650
- })
46872
+ })]
46651
46873
  });
46652
46874
  }
46653
46875
  //#endregion
46654
46876
  //#region src/components/Sidebar.tsx
46655
- var qgridVersion = "2.9.7";
46877
+ var qgridVersion = "2.9.9";
46656
46878
  function Sidebar({ className }) {
46657
46879
  const pathname = useRouterState({ select: (s) => s.location.pathname });
46658
46880
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("aside", {
@@ -47110,7 +47332,6 @@ function isFunction$3(value) {
47110
47332
  }
47111
47333
  //#endregion
47112
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
47113
- var import_react_dom = /* @__PURE__ */ __toESM(require_react_dom(), 1);
47114
47335
  var Primitive$1 = [
47115
47336
  "a",
47116
47337
  "button",
@@ -68537,15 +68758,15 @@ function RootComponent() {
68537
68758
  }
68538
68759
  //#endregion
68539
68760
  //#region src/routes/tokens.tsx
68540
- var $$splitComponentImporter$5 = () => import("./assets/tokens-C32lycTe.js");
68761
+ var $$splitComponentImporter$5 = () => import("./assets/tokens-DAUmTrgO.js");
68541
68762
  var Route$5 = createFileRoute("/tokens")({ component: lazyRouteComponent($$splitComponentImporter$5, "component") });
68542
68763
  //#endregion
68543
68764
  //#region src/routes/settings.tsx
68544
- var $$splitComponentImporter$4 = () => import("./assets/settings-CFoXQaAg.js");
68765
+ var $$splitComponentImporter$4 = () => import("./assets/settings-DPcWGvy-.js");
68545
68766
  var Route$4 = createFileRoute("/settings")({ component: lazyRouteComponent($$splitComponentImporter$4, "component") });
68546
68767
  //#endregion
68547
68768
  //#region src/routes/monit.tsx
68548
- var $$splitComponentImporter$3 = () => import("./assets/monit-BW2X1D23.js");
68769
+ var $$splitComponentImporter$3 = () => import("./assets/monit-COH4MckI.js");
68549
68770
  var monitSearchSchema = object({ level: _enum([
68550
68771
  "all",
68551
68772
  "warn",
@@ -68557,7 +68778,7 @@ var Route$3 = createFileRoute("/monit")({
68557
68778
  });
68558
68779
  //#endregion
68559
68780
  //#region src/routes/logs.tsx
68560
- var $$splitComponentImporter$2 = () => import("./assets/logs-lBOU5eUP.js");
68781
+ var $$splitComponentImporter$2 = () => import("./assets/logs-DO8zBdBR.js");
68561
68782
  var logsSearchSchema = object({
68562
68783
  token: string$2().optional(),
68563
68784
  project: string$2().optional(),
@@ -68571,11 +68792,11 @@ var Route$2 = createFileRoute("/logs")({
68571
68792
  });
68572
68793
  //#endregion
68573
68794
  //#region src/routes/index.tsx
68574
- var $$splitComponentImporter$1 = () => import("./assets/routes-B408iqh1.js");
68795
+ var $$splitComponentImporter$1 = () => import("./assets/routes-Btja-741.js");
68575
68796
  var Route$1 = createFileRoute("/")({ component: lazyRouteComponent($$splitComponentImporter$1, "component") });
68576
68797
  //#endregion
68577
68798
  //#region src/routes/requests/show.tsx
68578
- var $$splitComponentImporter = () => import("./assets/show-CcZJO6WF.js");
68799
+ var $$splitComponentImporter = () => import("./assets/show-CMXbb3TJ.js");
68579
68800
  var showSearchSchema = object({ id: number$1() });
68580
68801
  var Route = createFileRoute("/requests/show")({
68581
68802
  validateSearch: showSearchSchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cartanova/qgrid-cli",
3
- "version": "2.9.7",
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
 
@@ -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, Opus 5 and Opus 5.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, Fable 5.1 and Opus 5.5 require always-on adaptive thinking (Fable: CLI catalog `rejects_disabled_thinking`; Opus 5.5: official spec, thinking cannot be turned off). 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
 
@@ -258,6 +258,7 @@ qgrid's exact 1M support set currently includes:
258
258
  - `claude-fable-5`
259
259
  - `claude-opus-5`
260
260
  - `claude-opus-5-5`
261
+ - `claude-sonnet-5-5`
261
262
  - `claude-sonnet-5`
262
263
  - `claude-sonnet-4-6`
263
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.
@@ -163,6 +163,16 @@ Image generation is opt-in and non-stream at the AI SDK interface. The backend s
163
163
 
164
164
  ### Transparent image route
165
165
 
166
- Only `imageGenerationOptions.background: "transparent"` switches from hosted Responses to Codex `images/generations` or `images/edits`. The same selected subscription token, cancellation deadline, in-flight tracking, and one definitive 401 refresh apply. Use JSON content negotiation; do not reuse the Responses SSE Accept header. Never replay an ambiguous generation or fall back to opaque output/IS-Net. This route runs `gpt-image-2` directly, so actual model metadata differs from the requested driver ID and ordinary driver usage stays zero. Image response usage and actual settings are preserved on `generation`; usage belongs to the first output only. Costs remain explicitly estimated from public prices.
166
+ Explicit `imageGeneration: true` with `imageGenerationOptions.background: "transparent"` switches from hosted Responses to Codex `images/generations` or `images/edits`. The same selected subscription token, cancellation deadline, in-flight tracking, and one definitive 401 refresh apply. Use JSON content negotiation; do not reuse the Responses SSE Accept header. Never replay an ambiguous generation or fall back to opaque output/IS-Net. This explicit route runs `gpt-image-2` directly, so actual model metadata differs from the requested driver ID and ordinary driver usage stays zero. Image response usage and actual settings are preserved on `generation`; usage belongs to the first output only. Costs remain explicitly estimated from public prices.
167
167
 
168
168
  The image request adapter preserves text/system context and reference images (maximum five), rejects unsupported structured/tool requests before generation, and validates PNG contents with pngjs. Both a fully transparent pixel and visible content are required. Invalid, opaque, empty, or oversized results fail. Returned dimensions come from the decoded PNG; qgrid never silently resizes. A 2026-09-08 live request returned 1254x1254/medium for requested 1024x1024/high. Do not promise exact size/quality enforcement. See `openai-image-generation.ts` and opt-in `transparent`/`transparent-edit` cases in the image smoke script.
169
+
170
+ ### Automatic dashboard chat images (2.9.8)
171
+
172
+ Raw `QueryInput.imageGeneration` accepts boolean or `"auto"`; the public AI SDK option remains boolean. OpenAI dashboard chat sends `"auto"` through one non-stream POST per turn. The dispatcher offers native `image_generation` plus a qgrid-defined Responses function, `generate_transparent_image`, reserved for transparent backgrounds. It executes that function server-side against standalone Codex Images using the same selected token and abort signal, without another GPT request. The client must not implement a `request_image_generation` decision loop.
173
+
174
+ Auto mode accepts text-only completion when no image tool was attempted. Explicit `true` still requires an image, and an attempted image tool that returns no complete image still fails. Caller tools and `outputSchema` are incompatible with auto mode. The transparent function and native image tool must not be mixed in one response; at most one transparent call is allowed.
175
+
176
+ Auto transparent turns preserve the Responses driver model and token usage; image model, settings, and usage remain separate generation metadata. Do not apply the explicit standalone route's zero-driver accounting to auto turns. Full image history is replayed on each turn without reusable provider conversation state. Global image-tool opt-in remains unchanged outside chat to preserve regular callers' tool framing and cache behavior.
177
+
178
+ The 2026-09-29 hosted-route probe returned PNG for a prompt-only WebP request and WebP for explicit `imageGenerationOptions.outputFormat: "webp"`. Verify bytes rather than model claims. The standalone transparent endpoint still returns PNG; no format conversion is added. See `output/webp-prompt-probe-2026-09-29/verification.md` and `docs/solutions/architecture-patterns/qgrid-native-auto-image-generation-2.9.8.md`.
@@ -78,7 +78,9 @@ OpenAI uses the completed Responses event's per-request usage fields.
78
78
 
79
79
  Anthropic normalizes native mutually exclusive categories by summing input + cache creation + cache read into total input. Prefer positive Claude Code `total_cost_usd`; otherwise calculate from the actual serving model and TTL split.
80
80
 
81
- For image requests, keep `cost_usd` as the Codex driver model token cost. Hosted image output costs use quality/size assumptions because image-tool usage is not exposed. Transparent standalone Images requests run no driver and report zero driver usage/cost; their image usage and actual settings are preserved as `observedGeneration` alongside `requestedOptions` in the synthetic tool log. Their separate image cost uses reported usage with an explicitly conservative public-price estimate. Treat `image_cost_usd` as a price-table estimate that may be inaccurate if Codex changes its underlying image accounting.
81
+ For image requests, keep `cost_usd` as the Codex driver model token cost. Hosted image output costs use quality/size assumptions because image-tool usage is not exposed. Explicit transparent standalone Images requests run no driver and report zero driver usage/cost; auto transparent turns preserve the Responses driver model and usage while keeping image usage/settings separately. Image usage and actual settings are preserved as `observedGeneration` alongside `requestedOptions` in the synthetic tool log. Their separate image cost uses reported usage with an explicitly conservative public-price estimate. Treat `image_cost_usd` as a price-table estimate that may be inaccurate if Codex changes its underlying image accounting.
82
+
83
+ For `imageGeneration: "auto"`, set `is_image_generation` only when returned content actually contains an image; offering image tools is not sufficient. Keep stored `model_name` as the driver for hosted and auto transparent turns. Dashboard requested-to-image labels can derive the image model from serving/image-cost metadata without changing the stored driver or adding image-generation/assumption badges.
82
84
 
83
85
  Reference images for image generation are not stored in `request_logs.user_prompt`; that field remains the text prompt. For inspection in the detail view, qgrid stores reference image metadata/base64 in the synthetic `image_generation` step's `tool_args.inputImages`. If Codex returns multiple image outputs, store `inputImages` only on the first synthetic image tool step so multi-output logs do not duplicate the same input payload on every output.
84
86