@cartanova/qgrid-cli 2.9.7 → 2.9.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bundle/dist/application/qgrid/qgrid-run-lifecycle.js +3 -2
- package/bundle/dist/application/qgrid/qgrid.types.js +2 -2
- package/bundle/dist/application/request-log/request-log.model.js +2 -1
- package/bundle/dist/utils/providers/anthropic/anthropic-constants.js +4 -2
- package/bundle/dist/utils/providers/common/model-cost.js +11 -2
- package/bundle/dist/utils/providers/openai/openai-backend-protocol.js +3 -2
- package/bundle/dist/utils/providers/openai/openai-dispatcher.js +86 -5
- package/bundle/src/application/qgrid/qgrid-image-generation.test.ts +7 -1
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.test.ts +13 -0
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.ts +2 -1
- package/bundle/src/application/qgrid/qgrid.frame.test.ts +2 -2
- package/bundle/src/application/qgrid/qgrid.types.ts +2 -2
- package/bundle/src/application/request-log/request-log.model.ts +2 -0
- package/bundle/src/application/sonamu.generated.http +4 -2
- package/bundle/src/utils/providers/anthropic/anthropic-constants.test.ts +5 -1
- package/bundle/src/utils/providers/anthropic/anthropic-constants.ts +4 -2
- package/bundle/src/utils/providers/common/effort.test.ts +1 -0
- package/bundle/src/utils/providers/common/model-cost.test.ts +5 -0
- package/bundle/src/utils/providers/common/model-cost.ts +15 -0
- package/bundle/src/utils/providers/common/provider-dispatcher.ts +1 -1
- package/bundle/src/utils/providers/openai/openai-backend-protocol.test.ts +5 -0
- package/bundle/src/utils/providers/openai/openai-backend-protocol.ts +2 -2
- package/bundle/src/utils/providers/openai/openai-dispatcher.test.ts +115 -0
- package/bundle/src/utils/providers/openai/openai-dispatcher.ts +136 -1
- package/bundle/web-dist/client/assets/index-DJpa8UaG.js +106 -0
- package/bundle/web-dist/client/assets/index-D_sagzsR.css +1 -0
- package/bundle/web-dist/client/assets/logs-COwHodGU.js +1 -0
- package/bundle/web-dist/client/assets/{monit-7tcWgB20.js → monit-C6ZRFTQr.js} +1 -1
- package/bundle/web-dist/client/assets/request-model-eygfdRv7.js +1 -0
- package/bundle/web-dist/client/assets/{routes-CSVp9aNn.js → routes-CrMRMQvC.js} +1 -1
- package/bundle/web-dist/client/assets/{services.generated-C6jfEwfK.js → services.generated-Bg2XkD-D.js} +1 -1
- package/bundle/web-dist/client/assets/settings-BVhU4Oul.js +1 -0
- package/bundle/web-dist/client/assets/show-DA-1k3Mt.js +2 -0
- package/bundle/web-dist/client/assets/tokens-DliSEDiF.js +1 -0
- package/bundle/web-dist/client/assets/{use-oauth-login-flow-DTrmdduV.js → use-oauth-login-flow-C0qa-nOp.js} +1 -1
- package/bundle/web-dist/client/assets/{use-update-token-mutation-BIvO70-M.js → use-update-token-mutation-Dkv_HG-C.js} +1 -1
- package/bundle/web-dist/client/index.html +3 -3
- package/bundle/web-dist/server/assets/{lazyRouteComponent-C_k4R5n6.js → lazyRouteComponent-DyizoQKi.js} +2 -2
- package/bundle/web-dist/server/assets/{logs-lBOU5eUP.js → logs-DO8zBdBR.js} +12 -5
- package/bundle/web-dist/server/assets/{monit-BW2X1D23.js → monit-COH4MckI.js} +2 -2
- package/bundle/web-dist/server/assets/{plus-DilmHifT.js → plus-C1D8z9o0.js} +1 -1
- package/bundle/web-dist/server/assets/{react-dom-CHRRsKwb.js → react-dom-PfWvArTX.js} +1 -1
- package/bundle/web-dist/server/assets/request-model-DXclJfdP.js +13 -0
- package/bundle/web-dist/server/assets/{routes-B408iqh1.js → routes-Btja-741.js} +4 -4
- package/bundle/web-dist/server/assets/{services.generated-buX3cJzR.js → services.generated-BsKOUSXl.js} +1 -1
- package/bundle/web-dist/server/assets/{settings-CFoXQaAg.js → settings-DPcWGvy-.js} +3 -3
- package/bundle/web-dist/server/assets/{show-CcZJO6WF.js → show-CMXbb3TJ.js} +10 -4
- package/bundle/web-dist/server/assets/{tokens-C32lycTe.js → tokens-DAUmTrgO.js} +15 -11
- package/bundle/web-dist/server/assets/{use-oauth-login-flow-DCjxIa80.js → use-oauth-login-flow-GsGwqefs.js} +1 -1
- package/bundle/web-dist/server/assets/{use-update-token-mutation-CqK8i8Fa.js → use-update-token-mutation-RujWERvj.js} +1 -1
- package/bundle/web-dist/server/entry-server.generated.js +273 -52
- package/package.json +1 -1
- package/skills/qgrid/SKILL.md +10 -1
- package/skills/qgrid/references/anthropic-claude-code-runtime.md +3 -2
- package/skills/qgrid/references/decision-rationale.md +11 -2
- package/skills/qgrid/references/openai-codex-runtime.md +27 -3
- package/skills/qgrid/references/request-log-run-lifecycle.md +3 -1
- package/skills/qgrid/references/sonamu-api-web-flow.md +9 -1
- package/skills/qgrid/references/token-auth-quota-lifecycle.md +2 -0
- package/skills/qgrid/references/verification-and-debugging.md +4 -1
- package/bundle/web-dist/client/assets/index-BshX4Gd8.css +0 -1
- package/bundle/web-dist/client/assets/index-D4MEKFjK.js +0 -106
- package/bundle/web-dist/client/assets/logs-BWhH8t7Y.js +0 -1
- package/bundle/web-dist/client/assets/settings-CYgR0YTh.js +0 -1
- package/bundle/web-dist/client/assets/show-Cg8n_Mxg.js +0 -2
- package/bundle/web-dist/client/assets/tokens-B5yg6bP1.js +0 -1
- /package/bundle/web-dist/client/assets/{plus-p4vtg6LN.js → plus-CknMNCP5.js} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { A as
|
|
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-
|
|
3
|
-
import { n as require_shim, t as require_react_dom } from "./assets/react-dom-
|
|
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,13 +46253,12 @@ var MODEL_PRESET_GROUPS = [{
|
|
|
46085
46253
|
label: "OpenAI",
|
|
46086
46254
|
models: [
|
|
46087
46255
|
"openai/gpt-6-astra",
|
|
46256
|
+
"openai/gpt-6.1-sol",
|
|
46088
46257
|
"openai/gpt-6-sol",
|
|
46089
46258
|
"openai/gpt-6-luna",
|
|
46090
46259
|
"openai/gpt-5.6-terra",
|
|
46091
46260
|
"openai/gpt-5.6-sol",
|
|
46092
|
-
"openai/gpt-5.6-luna"
|
|
46093
|
-
"openai/gpt-5.5",
|
|
46094
|
-
"openai/gpt-5.3-codex-spark"
|
|
46261
|
+
"openai/gpt-5.6-luna"
|
|
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 =
|
|
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
|
|
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.
|
|
46535
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
46349
46536
|
className: "fixed bottom-5 right-5 z-40",
|
|
46350
|
-
children:
|
|
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)("
|
|
46500
|
-
|
|
46501
|
-
|
|
46502
|
-
|
|
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)("
|
|
46576
|
-
|
|
46577
|
-
|
|
46578
|
-
|
|
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.
|
|
46877
|
+
var qgridVersion = "2.9.10";
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
package/skills/qgrid/SKILL.md
CHANGED
|
@@ -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.
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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.
|