@cartanova/qgrid-cli 2.10.5 → 2.10.6
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/monit/monit.frame.js +1 -1
- package/bundle/dist/application/qgrid/expired-token-reminder.js +1 -1
- package/bundle/dist/application/qgrid/oauth.js +1 -1
- package/bundle/dist/application/qgrid/qgrid-image-generation.js +7 -11
- package/bundle/dist/application/qgrid/qgrid-response-format.js +4 -4
- package/bundle/dist/application/qgrid/qgrid-run-lifecycle.js +24 -7
- package/bundle/dist/application/qgrid/qgrid.dispatcher.js +8 -3
- package/bundle/dist/application/qgrid/qgrid.frame.js +24 -10
- package/bundle/dist/application/qgrid/token-death.js +1 -1
- package/bundle/dist/application/qgrid/token-window-keepalive.js +1 -1
- package/bundle/dist/application/qgrid/tool-emulation.js +1 -1
- package/bundle/dist/application/request-log/request-log.model.js +2 -2
- package/bundle/dist/i18n/sd.generated.js +3 -3
- package/bundle/dist/sonamu.config.js +2 -2
- package/bundle/dist/utils/providers/anthropic/anthropic-dispatcher.js +1 -1
- package/bundle/dist/utils/providers/anthropic/anthropic-thinking.js +1 -1
- package/bundle/dist/utils/providers/openai/openai-backend-protocol.js +13 -2
- package/bundle/dist/utils/providers/openai/openai-direct-client.js +5 -4
- package/bundle/dist/utils/providers/openai/openai-dispatcher.js +111 -57
- package/bundle/dist/utils/providers/openai/openai-image-generation.js +25 -17
- package/bundle/dist/utils/providers/openai/openai-refresh.js +1 -1
- package/bundle/src/application/qgrid/qgrid-image-generation.test.ts +19 -8
- package/bundle/src/application/qgrid/qgrid-image-generation.ts +17 -24
- package/bundle/src/application/qgrid/qgrid-response-format.test.ts +4 -2
- package/bundle/src/application/qgrid/qgrid-response-format.ts +7 -10
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.test.ts +87 -9
- package/bundle/src/application/qgrid/qgrid-run-lifecycle.ts +45 -5
- package/bundle/src/application/qgrid/qgrid.dispatcher.test.ts +136 -2
- package/bundle/src/application/qgrid/qgrid.dispatcher.ts +11 -1
- package/bundle/src/application/qgrid/qgrid.frame.test.ts +73 -0
- package/bundle/src/application/qgrid/qgrid.frame.ts +39 -4
- package/bundle/src/application/request-log/request-log.entity.json +2 -2
- package/bundle/src/application/sonamu.generated.http +2 -2
- package/bundle/src/i18n/sd.generated.ts +2 -2
- package/bundle/src/utils/providers/openai/openai-backend-protocol.test.ts +23 -1
- package/bundle/src/utils/providers/openai/openai-backend-protocol.ts +24 -2
- package/bundle/src/utils/providers/openai/openai-direct-client.test.ts +15 -0
- package/bundle/src/utils/providers/openai/openai-direct-client.ts +14 -4
- package/bundle/src/utils/providers/openai/openai-dispatcher.test.ts +223 -32
- package/bundle/src/utils/providers/openai/openai-dispatcher.ts +174 -72
- package/bundle/src/utils/providers/openai/openai-image-generation.test.ts +89 -0
- package/bundle/src/utils/providers/openai/openai-image-generation.ts +26 -15
- package/bundle/web-dist/client/assets/cost-DH2jZbrw.js +1 -0
- package/bundle/web-dist/client/assets/{index-Dxw7DiXF.js → index-D9ryIyL-.js} +4 -4
- package/bundle/web-dist/client/assets/logs-CLf2VNfk.js +1 -0
- package/bundle/web-dist/client/assets/{monit-E8eGW1Sj.js → monit-DojxOLj6.js} +1 -1
- package/bundle/web-dist/client/assets/{routes-7spLfIGZ.js → routes-DXJhNvbI.js} +2 -2
- package/bundle/web-dist/client/assets/{services.generated-jnPHYMER.js → services.generated-DqFYgWsY.js} +1 -1
- package/bundle/web-dist/client/assets/{settings-BdosZFNC.js → settings-bopyz9pa.js} +1 -1
- package/bundle/web-dist/client/assets/{show-b2PFNI3D.js → show-DbLD3MzB.js} +1 -1
- package/bundle/web-dist/client/assets/{tokens-CBdHb2_U.js → tokens-2-AIi9sZ.js} +1 -1
- package/bundle/web-dist/client/assets/{use-oauth-login-flow-Cyn3YPqW.js → use-oauth-login-flow-CnqTWs_H.js} +1 -1
- package/bundle/web-dist/client/assets/{use-update-token-mutation-DQCwCYfR.js → use-update-token-mutation-Cxb9K0fM.js} +1 -1
- package/bundle/web-dist/client/index.html +2 -2
- package/bundle/web-dist/server/assets/{cost-59jBdx-I.js → cost-eEHeTRT5.js} +4 -5
- package/bundle/web-dist/server/assets/{lazyRouteComponent-CUR-kWzI.js → lazyRouteComponent-Bi0fYGLs.js} +2 -2
- package/bundle/web-dist/server/assets/{logs-CaaWO9-Z.js → logs-CIVFxfoi.js} +10 -9
- package/bundle/web-dist/server/assets/{monit-CBil92fJ.js → monit-DBXezg7R.js} +4 -4
- package/bundle/web-dist/server/assets/{plus-Bvdj8Pwx.js → plus-BH-t4cm_.js} +1 -1
- package/bundle/web-dist/server/assets/{react-dom-CyuShs9K.js → react-dom-CZ2eynd8.js} +1 -1
- package/bundle/web-dist/server/assets/{refresh-cw-3QAiSWBd.js → refresh-cw-CosAdcz0.js} +1 -1
- package/bundle/web-dist/server/assets/{routes-DtVf9yqo.js → routes-COWYIvA-.js} +6 -7
- package/bundle/web-dist/server/assets/{services.generated-FPwsG9p3.js → services.generated-607iwhMI.js} +2 -2
- package/bundle/web-dist/server/assets/{settings-DeHNwoMv.js → settings-C_6dpK5p.js} +3 -3
- package/bundle/web-dist/server/assets/{show-8NNdRxhM.js → show-DY396-LD.js} +8 -20
- package/bundle/web-dist/server/assets/{tokens-odgS2-9q.js → tokens-COvV7qUP.js} +4 -4
- package/bundle/web-dist/server/assets/{use-oauth-login-flow-DTQKdYBO.js → use-oauth-login-flow-VYCYf3uv.js} +1 -1
- package/bundle/web-dist/server/assets/{use-update-token-mutation-xvXbPrCI.js → use-update-token-mutation-D8XYV5of.js} +1 -1
- package/bundle/web-dist/server/entry-server.generated.js +10 -10
- package/package.json +1 -1
- package/skills/qgrid/SKILL.md +9 -8
- package/skills/qgrid/references/ai-sdk-provider-contract.md +25 -16
- package/skills/qgrid/references/decision-rationale.md +8 -8
- package/skills/qgrid/references/openai-codex-runtime.md +19 -11
- package/skills/qgrid/references/request-log-run-lifecycle.md +4 -2
- package/skills/qgrid/references/sonamu-api-web-flow.md +2 -2
- package/skills/qgrid/references/verification-and-debugging.md +23 -9
- package/bundle/web-dist/client/assets/cost-CY2BJEPC.js +0 -1
- package/bundle/web-dist/client/assets/logs-3n_U4qnI.js +0 -1
|
@@ -100,12 +100,12 @@ export async function beforeQuery(args: QueryInput): Promise<{
|
|
|
100
100
|
return { requestLogId, stepIndex };
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
async function appendGenerateStep(
|
|
104
104
|
requestLogId: number,
|
|
105
105
|
stepIndex: number,
|
|
106
106
|
args: QueryInput,
|
|
107
107
|
result: QueryOutput,
|
|
108
|
-
)
|
|
108
|
+
) {
|
|
109
109
|
// provider가 canonical model을 돌려줘도 요청 route 자체([1m] 등)는 보존한다.
|
|
110
110
|
const requestedModelName = withProviderPrefix(
|
|
111
111
|
args.model,
|
|
@@ -132,6 +132,21 @@ export async function afterQuery(
|
|
|
132
132
|
ttft_ms: result.ttftMs,
|
|
133
133
|
finish_reason: result.finishReason,
|
|
134
134
|
});
|
|
135
|
+
return { requestedModelName, servedModelName };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export async function afterQuery(
|
|
139
|
+
requestLogId: number,
|
|
140
|
+
stepIndex: number,
|
|
141
|
+
args: QueryInput,
|
|
142
|
+
result: QueryOutput,
|
|
143
|
+
): Promise<RunLifecycleResult> {
|
|
144
|
+
const { requestedModelName, servedModelName } = await appendGenerateStep(
|
|
145
|
+
requestLogId,
|
|
146
|
+
stepIndex,
|
|
147
|
+
args,
|
|
148
|
+
result,
|
|
149
|
+
);
|
|
135
150
|
|
|
136
151
|
const imageParts = getImageParts(result);
|
|
137
152
|
const imageCostMicroUsd = estimateImageGenerationCostMicroUsd(
|
|
@@ -200,12 +215,17 @@ export async function finishRunWithError(
|
|
|
200
215
|
requestLogId: number,
|
|
201
216
|
errorMessage: string,
|
|
202
217
|
args?: QueryInput,
|
|
218
|
+
completedStep?: { stepIndex: number; result: QueryOutput },
|
|
203
219
|
): Promise<void> {
|
|
204
|
-
await finishTerminalRun(requestLogId, "error", errorMessage, args);
|
|
220
|
+
await finishTerminalRun(requestLogId, "error", errorMessage, args, completedStep);
|
|
205
221
|
}
|
|
206
222
|
|
|
207
|
-
export async function finishRunAborted(
|
|
208
|
-
|
|
223
|
+
export async function finishRunAborted(
|
|
224
|
+
requestLogId: number,
|
|
225
|
+
args?: QueryInput,
|
|
226
|
+
completedStep?: { stepIndex: number; result: QueryOutput },
|
|
227
|
+
): Promise<void> {
|
|
228
|
+
await finishTerminalRun(requestLogId, "aborted", "client disconnected", args, completedStep);
|
|
209
229
|
}
|
|
210
230
|
|
|
211
231
|
async function finishTerminalRun(
|
|
@@ -213,8 +233,18 @@ async function finishTerminalRun(
|
|
|
213
233
|
status: "error" | "aborted",
|
|
214
234
|
errorMessage: string,
|
|
215
235
|
args?: QueryInput,
|
|
236
|
+
completedStep?: { stepIndex: number; result: QueryOutput },
|
|
216
237
|
): Promise<void> {
|
|
217
238
|
try {
|
|
239
|
+
if (completedStep && args) {
|
|
240
|
+
try {
|
|
241
|
+
await appendGenerateStep(requestLogId, completedStep.stepIndex, args, completedStep.result);
|
|
242
|
+
} catch (e) {
|
|
243
|
+
logger.error(
|
|
244
|
+
`appendGenerateStep failed while finishing ${status}: ${(e as Error).message}`,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
218
248
|
let aggregate: Awaited<ReturnType<typeof RequestLogModel.aggregateStepUsage>> | undefined;
|
|
219
249
|
try {
|
|
220
250
|
aggregate = await RequestLogModel.aggregateStepUsage(requestLogId);
|
|
@@ -226,6 +256,16 @@ async function finishTerminalRun(
|
|
|
226
256
|
status,
|
|
227
257
|
error_message: errorMessage,
|
|
228
258
|
history: args ? filterHistoryForStorage(args.history) : undefined,
|
|
259
|
+
...(completedStep
|
|
260
|
+
? {
|
|
261
|
+
token_name: completedStep.result.tokenName,
|
|
262
|
+
requested_model_name: withProviderPrefix(
|
|
263
|
+
args?.model,
|
|
264
|
+
args?.model ?? completedStep.result.requestedModel ?? completedStep.result.model,
|
|
265
|
+
),
|
|
266
|
+
model_name: withProviderPrefix(args?.model, completedStep.result.model),
|
|
267
|
+
}
|
|
268
|
+
: {}),
|
|
229
269
|
...(aggregate
|
|
230
270
|
? {
|
|
231
271
|
input_tokens: aggregate.input_tokens,
|
|
@@ -6,10 +6,12 @@ import {
|
|
|
6
6
|
type GenerateStreamCallbacks,
|
|
7
7
|
} from "../../utils/providers/common/provider-dispatcher";
|
|
8
8
|
import { systemHash } from "./conv-routing";
|
|
9
|
-
import { OpenAIDispatcher } from "../../utils/providers/openai/openai-dispatcher";
|
|
9
|
+
import { ImageGenerationError, OpenAIDispatcher } from "../../utils/providers/openai/openai-dispatcher";
|
|
10
10
|
import { normalizeOpenAIEvent } from "../../utils/providers/openai/openai-backend-protocol";
|
|
11
|
-
import { buildStrictOutputSchema, QgridDispatcherClass } from "./qgrid.dispatcher";
|
|
11
|
+
import { buildStrictOutputSchema, completedDriverFromImageFailure, QgridDispatcherClass } from "./qgrid.dispatcher";
|
|
12
12
|
import { QueryInput, type QueryOutput } from "./qgrid.types";
|
|
13
|
+
import { PNG } from "pngjs";
|
|
14
|
+
import { estimateImageGenerationCostMicroUsd } from "./qgrid-image-generation";
|
|
13
15
|
import { type TokenSubsetA } from "../sonamu.generated";
|
|
14
16
|
|
|
15
17
|
function providerResult(overrides: Partial<GenerateResult> = {}): GenerateResult {
|
|
@@ -36,6 +38,138 @@ function deeplyNestedOutputSchema(depth: number): string {
|
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
describe("QgridDispatcherClass", () => {
|
|
41
|
+
it("prices only the completed chat driver from an image failure", () => {
|
|
42
|
+
const error = new ImageGenerationError("incomplete", "images unavailable");
|
|
43
|
+
error.completedDriver = providerResult({
|
|
44
|
+
model: "gpt-6-luna", requestedModel: "gpt-6-astra", ttftMs: 4,
|
|
45
|
+
usage: { totalTokens: 120, inputTokens: 100, outputTokens: 20, cachedInputTokens: 10, reasoningOutputTokens: 5 },
|
|
46
|
+
});
|
|
47
|
+
const result = completedDriverFromImageFailure(error)!;
|
|
48
|
+
expect(result).toMatchObject({
|
|
49
|
+
model: "gpt-6-luna", requestedModel: "gpt-6-astra", tokenName: "provider/test",
|
|
50
|
+
usage: { input_tokens: 100, output_tokens: 20, cache_read_input_tokens: 10, reasoning_tokens: 5 },
|
|
51
|
+
costSource: "pricing_table", finishReason: "stop", durationMs: 12, ttftMs: 4,
|
|
52
|
+
content: [{ type: "text", text: "ok" }],
|
|
53
|
+
});
|
|
54
|
+
expect(result.costUsd).toBeCloseTo(0.0000191, 10);
|
|
55
|
+
expect(estimateImageGenerationCostMicroUsd(result, undefined)).toBeNull();
|
|
56
|
+
expect(result.runContext).toBeUndefined();
|
|
57
|
+
expect(completedDriverFromImageFailure(new Error("failed"))).toBeUndefined();
|
|
58
|
+
expect(completedDriverFromImageFailure(new ImageGenerationError("gate", "invalid"))).toBeUndefined();
|
|
59
|
+
});
|
|
60
|
+
it.each([
|
|
61
|
+
[false, 0, "generations"],
|
|
62
|
+
[false, 2, "edits"],
|
|
63
|
+
[true, 1, "edits"],
|
|
64
|
+
] as const)("auto chat uses Images %s/%s/%s and keeps both usages", async (transparent, referenceCount, endpoint) => {
|
|
65
|
+
const png = new PNG({ width: 2, height: 1 });
|
|
66
|
+
png.data.set([0, 0, 0, transparent ? 0 : 255, 255, 0, 0, 255]);
|
|
67
|
+
const imageUsage = {
|
|
68
|
+
input_tokens: 10, output_tokens: 100, total_tokens: 110,
|
|
69
|
+
input_tokens_details: { text_tokens: 4, image_tokens: 6 },
|
|
70
|
+
};
|
|
71
|
+
const fetchMock = vi.fn(async (url: string | URL | Request, _init?: RequestInit) => {
|
|
72
|
+
if (String(url).endsWith("/responses")) {
|
|
73
|
+
const events = [
|
|
74
|
+
{ type: "response.output_item.done", item: {
|
|
75
|
+
type: "function_call", call_id: "image-call", name: "generate_image",
|
|
76
|
+
arguments: JSON.stringify({ prompt: "A red square", transparent_background: transparent, num_last_images_to_include: referenceCount }),
|
|
77
|
+
} },
|
|
78
|
+
{ type: "response.completed", response: {
|
|
79
|
+
id: "driver", model: "gpt-6-luna",
|
|
80
|
+
usage: { input_tokens: 100, output_tokens: 20, total_tokens: 120 },
|
|
81
|
+
} },
|
|
82
|
+
];
|
|
83
|
+
return new Response(events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join(""), { headers: { "content-type": "text/event-stream" } });
|
|
84
|
+
}
|
|
85
|
+
return Response.json({
|
|
86
|
+
data: [{ b64_json: PNG.sync.write(png).toString("base64") }],
|
|
87
|
+
background: transparent ? "transparent" : "opaque", quality: "medium", usage: imageUsage,
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
const provider = new OpenAIDispatcher("https", { fetch: fetchMock });
|
|
91
|
+
await provider.onTokenAdded(1, "chat-token", {
|
|
92
|
+
accessToken: "test-access", refreshToken: "", accountId: "test-account", accessTokenExpiresAt: Date.now() + 60000,
|
|
93
|
+
});
|
|
94
|
+
const dispatcher = new QgridDispatcherClass();
|
|
95
|
+
dispatcher.openaiDispatcher = provider;
|
|
96
|
+
try {
|
|
97
|
+
const result = await dispatcher.query({
|
|
98
|
+
model: "openai/gpt-6-luna", prompt: "Draw or edit a square", imageGeneration: "auto",
|
|
99
|
+
history: JSON.stringify([{ type: "message", role: "user", content: [{ type: "input_image", image_url: "https://example.com/old.png" }] }]),
|
|
100
|
+
input: [{ type: "image", url: "https://example.com/new.png" }],
|
|
101
|
+
});
|
|
102
|
+
expect(fetchMock.mock.calls.map(([url]) => url)).toEqual([
|
|
103
|
+
"https://chatgpt.com/backend-api/codex/responses",
|
|
104
|
+
`https://chatgpt.com/backend-api/codex/images/${endpoint}`,
|
|
105
|
+
]);
|
|
106
|
+
const driverInit = fetchMock.mock.calls[0]![1]!;
|
|
107
|
+
const imageInit = fetchMock.mock.calls[1]![1]!;
|
|
108
|
+
expect(JSON.parse(String(driverInit.body)).tools).toEqual([
|
|
109
|
+
expect.objectContaining({ type: "function", name: "generate_image" }),
|
|
110
|
+
]);
|
|
111
|
+
const imageBody = JSON.parse(String(imageInit.body));
|
|
112
|
+
expect(imageBody).toMatchObject({ model: "gpt-image-2", background: transparent ? "transparent" : "opaque" });
|
|
113
|
+
expect(imageBody.prompt).toContain("A red square");
|
|
114
|
+
expect(imageBody.prompt).not.toContain("Draw or edit a square");
|
|
115
|
+
const refs = ["https://example.com/old.png", "https://example.com/new.png"].slice(-referenceCount);
|
|
116
|
+
expect(imageBody.images).toEqual(referenceCount ? refs.map((image_url) => ({ image_url })) : undefined);
|
|
117
|
+
expect(imageInit.signal).toBe(driverInit.signal);
|
|
118
|
+
expect(new Headers(imageInit.headers).get("authorization")).toBe(new Headers(driverInit.headers).get("authorization"));
|
|
119
|
+
expect(result).toMatchObject({ model: "gpt-6-luna", usage: { input_tokens: 100, output_tokens: 20 } });
|
|
120
|
+
expect(result.costUsd).toBeCloseTo(0.00002, 8);
|
|
121
|
+
expect(result.content.find((item) => item.type === "image")).toMatchObject({ generation: { route: "codex-images", usage: imageUsage } });
|
|
122
|
+
expect(estimateImageGenerationCostMicroUsd(result, undefined)).toBe(3068);
|
|
123
|
+
} finally {
|
|
124
|
+
await provider.stop();
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("routes the image model straight to subscription edits and keeps image usage separate", async () => {
|
|
129
|
+
const png = new PNG({ width: 1, height: 1 });
|
|
130
|
+
png.data.set([255, 0, 0, 255]);
|
|
131
|
+
const base64 = PNG.sync.write(png).toString("base64");
|
|
132
|
+
const usage = {
|
|
133
|
+
input_tokens: 10, output_tokens: 100, total_tokens: 110,
|
|
134
|
+
input_tokens_details: { text_tokens: 4, image_tokens: 6 },
|
|
135
|
+
};
|
|
136
|
+
const fetchMock = vi.fn(async () => new Response(JSON.stringify({
|
|
137
|
+
data: [{ b64_json: base64 }], usage, background: "opaque", quality: "medium",
|
|
138
|
+
})));
|
|
139
|
+
const provider = new OpenAIDispatcher("https", { fetch: fetchMock });
|
|
140
|
+
await provider.onTokenAdded(1, "test", {
|
|
141
|
+
accessToken: "unused", refreshToken: "", accountId: "unused",
|
|
142
|
+
accessTokenExpiresAt: Date.now() + 60000,
|
|
143
|
+
});
|
|
144
|
+
const dispatcher = new QgridDispatcherClass();
|
|
145
|
+
dispatcher.openaiDispatcher = provider;
|
|
146
|
+
try {
|
|
147
|
+
const result = await dispatcher.query({
|
|
148
|
+
model: "openai/gpt-image-2", prompt: "recolor", imageGeneration: true,
|
|
149
|
+
input: [
|
|
150
|
+
{ type: "text", text: "recolor", text_elements: [] },
|
|
151
|
+
{ type: "image", url: `data:image/png;base64,${base64}` },
|
|
152
|
+
],
|
|
153
|
+
});
|
|
154
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
155
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
156
|
+
"https://chatgpt.com/backend-api/codex/images/edits",
|
|
157
|
+
expect.objectContaining({ method: "POST", signal: expect.any(AbortSignal) }),
|
|
158
|
+
);
|
|
159
|
+
expect(result).toMatchObject({
|
|
160
|
+
model: "gpt-image-2", costUsd: 0,
|
|
161
|
+
usage: { input_tokens: 0, output_tokens: 0 },
|
|
162
|
+
content: expect.arrayContaining([expect.objectContaining({
|
|
163
|
+
type: "image", mediaType: "image/png",
|
|
164
|
+
generation: { route: "codex-images", model: "gpt-image-2", background: "opaque", size: "1x1", quality: "medium", usage },
|
|
165
|
+
})]),
|
|
166
|
+
});
|
|
167
|
+
expect(result.runContext?.threadCoord).toBeUndefined();
|
|
168
|
+
} finally {
|
|
169
|
+
await provider.stop();
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
39
173
|
it.each(["generate", "stream"])("%s yields a completed tool-call message before a speculative later answer", async (mode) => {
|
|
40
174
|
const tool = JSON.stringify({result: {action: "tool_call", answer: null, toolCalls: [{toolName: "lookupMarker", args: "{}"}]}});
|
|
41
175
|
const answer = JSON.stringify({result: {action: "answer", answer: {marker: ""}, toolCalls: null}});
|
|
@@ -17,7 +17,10 @@ import {
|
|
|
17
17
|
serializeAndValidateDispatchSchema,
|
|
18
18
|
} from "../../utils/providers/common/schema-validation";
|
|
19
19
|
import { strictify } from "../../utils/providers/common/strictifier";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
ImageGenerationError,
|
|
22
|
+
type OpenAIDispatcher,
|
|
23
|
+
} from "../../utils/providers/openai/openai-dispatcher";
|
|
21
24
|
import { type TokenSubsetA } from "../sonamu.generated";
|
|
22
25
|
import { decideConvRouting, issueConvContext } from "./conv-routing";
|
|
23
26
|
import {
|
|
@@ -325,6 +328,13 @@ function buildRawOutputSchema(
|
|
|
325
328
|
: callerOutputSchema;
|
|
326
329
|
}
|
|
327
330
|
|
|
331
|
+
export function completedDriverFromImageFailure(error: unknown): QueryOutput | undefined {
|
|
332
|
+
if (!(error instanceof ImageGenerationError) || !error.completedDriver) return undefined;
|
|
333
|
+
return applyToolCallEmulation(toEmulationResult(error.completedDriver), undefined, {
|
|
334
|
+
answerKind: "text",
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
328
338
|
// Anthropic adapter 는 cache creation 을 inputTokens 에 포함해 표준화하고, 별도 필드에도 보존한다.
|
|
329
339
|
function toEmulationResult(
|
|
330
340
|
result: GenerateResult,
|
|
@@ -5,6 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
5
5
|
|
|
6
6
|
import { QgridFrame } from "./qgrid.frame";
|
|
7
7
|
import { QueryInput } from "./qgrid.types";
|
|
8
|
+
import { ImageGenerationError } from "../../utils/providers/openai/openai-dispatcher";
|
|
8
9
|
|
|
9
10
|
const {
|
|
10
11
|
findOneMock,
|
|
@@ -213,6 +214,7 @@ describe("QgridFrame.query request logging", () => {
|
|
|
213
214
|
beforeQueryMock.mockReset().mockResolvedValue({ requestLogId: 41, stepIndex: 0 });
|
|
214
215
|
afterQueryMock.mockReset().mockResolvedValue({});
|
|
215
216
|
finishRunWithErrorMock.mockReset();
|
|
217
|
+
finishRunAbortedMock.mockReset();
|
|
216
218
|
dispatcherQueryMock.mockReset();
|
|
217
219
|
findActiveByProviderAndNameMock.mockReset();
|
|
218
220
|
});
|
|
@@ -385,6 +387,45 @@ describe("QgridFrame.query request logging", () => {
|
|
|
385
387
|
expect(finishRunWithErrorMock).toHaveBeenCalledWith(41, "provider failed", args);
|
|
386
388
|
});
|
|
387
389
|
|
|
390
|
+
it.each(["error", "abort", "logger-disabled"])("handles completed driver accounting on image %s", async (mode) => {
|
|
391
|
+
const requestRaw = Object.assign(new EventEmitter(), { aborted: false });
|
|
392
|
+
const responseRaw = Object.assign(new EventEmitter(), { destroyed: false, writableEnded: false });
|
|
393
|
+
vi.spyOn(Sonamu, "getContext").mockReturnValue({
|
|
394
|
+
transport: "http", request: { raw: requestRaw }, reply: { raw: responseRaw },
|
|
395
|
+
} as never);
|
|
396
|
+
const error = new ImageGenerationError("incomplete", "image failed");
|
|
397
|
+
error.completedDriver = {
|
|
398
|
+
text: "drawing", tokenName: "one", model: "gpt-6-luna", durationMs: 20, ttftMs: 2,
|
|
399
|
+
threadCoord: { workerId: 1, threadId: "", epoch: -1 },
|
|
400
|
+
usage: { inputTokens: 100, outputTokens: 20, cachedInputTokens: 0, reasoningOutputTokens: 0, totalTokens: 120 },
|
|
401
|
+
};
|
|
402
|
+
dispatcherQueryMock.mockImplementationOnce(async () => {
|
|
403
|
+
if (mode === "abort") { responseRaw.destroyed = true; responseRaw.emit("close"); }
|
|
404
|
+
throw error;
|
|
405
|
+
});
|
|
406
|
+
const args = {
|
|
407
|
+
prompt: "draw", model: "openai/gpt-6-luna", imageGeneration: "auto" as const,
|
|
408
|
+
...(mode === "logger-disabled" ? { logger: false } : {}),
|
|
409
|
+
};
|
|
410
|
+
await expect(QgridFrame.query(args)).rejects.toBe(error);
|
|
411
|
+
expect(afterQueryMock).not.toHaveBeenCalled();
|
|
412
|
+
if (mode === "logger-disabled") {
|
|
413
|
+
expect(beforeQueryMock).not.toHaveBeenCalled();
|
|
414
|
+
expect(finishRunWithErrorMock).not.toHaveBeenCalled();
|
|
415
|
+
expect(finishRunAbortedMock).not.toHaveBeenCalled();
|
|
416
|
+
} else {
|
|
417
|
+
const completedStep = expect.objectContaining({
|
|
418
|
+
stepIndex: 0,
|
|
419
|
+
result: expect.objectContaining({
|
|
420
|
+
model: "gpt-6-luna", tokenName: "one", costUsd: 0.00002,
|
|
421
|
+
usage: expect.objectContaining({ input_tokens: 100, output_tokens: 20 }),
|
|
422
|
+
}),
|
|
423
|
+
});
|
|
424
|
+
if (mode === "abort") expect(finishRunAbortedMock).toHaveBeenCalledWith(41, args, completedStep);
|
|
425
|
+
else expect(finishRunWithErrorMock).toHaveBeenCalledWith(41, "image failed", args, completedStep);
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
|
|
388
429
|
it("marks an incoherent structured envelope failure as a request-log error", async () => {
|
|
389
430
|
const error = new Error(
|
|
390
431
|
"tool-call emulation: structured response envelope is missing required keys: toolCalls",
|
|
@@ -775,6 +816,32 @@ describe("QgridFrame.query request logging", () => {
|
|
|
775
816
|
expect(QueryInput.safeParse({ prompt: "hi", logger: false }).success).toBe(true);
|
|
776
817
|
});
|
|
777
818
|
|
|
819
|
+
it.each([
|
|
820
|
+
{ imageGeneration: undefined },
|
|
821
|
+
{ imageGeneration: "auto" as const },
|
|
822
|
+
{ imageGeneration: true, tools: [{ name: "lookup", inputSchema: { type: "object" } }] },
|
|
823
|
+
{ imageGeneration: true, jsonSchema: '{"type":"object"}' },
|
|
824
|
+
{ imageGeneration: true, imageGenerationOptions: { outputFormat: "webp" as const } },
|
|
825
|
+
])("rejects unsupported standalone image requests before logging: %j", async (options) => {
|
|
826
|
+
await expect(QgridFrame.query({
|
|
827
|
+
prompt: "draw", model: "openai/gpt-image-2", ...options,
|
|
828
|
+
})).rejects.toMatchObject({ statusCode: 400 });
|
|
829
|
+
expect(beforeQueryMock).not.toHaveBeenCalled();
|
|
830
|
+
expect(dispatcherQueryMock).not.toHaveBeenCalled();
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
it.each([
|
|
834
|
+
{ imageGenerationOptions: { outputFormat: "webp" as const } },
|
|
835
|
+
{ tools: [{ name: "lookup", inputSchema: { type: "object" } }] },
|
|
836
|
+
{ jsonSchema: '{"type":"object"}' },
|
|
837
|
+
])("rejects unsupported auto image controls before logging: %j", async (options) => {
|
|
838
|
+
await expect(QgridFrame.query({
|
|
839
|
+
prompt: "draw", model: "openai/gpt-6-luna", imageGeneration: "auto", ...options,
|
|
840
|
+
})).rejects.toMatchObject({ statusCode: 400 });
|
|
841
|
+
expect(beforeQueryMock).not.toHaveBeenCalled();
|
|
842
|
+
expect(dispatcherQueryMock).not.toHaveBeenCalled();
|
|
843
|
+
});
|
|
844
|
+
|
|
778
845
|
it("accepts bounded positive integer timeout values only", () => {
|
|
779
846
|
expect(QueryInput.safeParse({ prompt: "hi", timeout: 360_000 }).success).toBe(true);
|
|
780
847
|
expect(QueryInput.safeParse({ prompt: "hi", timeout: 0 }).success).toBe(false);
|
|
@@ -789,6 +856,12 @@ describe("QgridFrame.prepareStream", () => {
|
|
|
789
856
|
requestLogSaveMock.mockReset();
|
|
790
857
|
});
|
|
791
858
|
|
|
859
|
+
it("rejects an image model without a streaming image flag", async () => {
|
|
860
|
+
await expect(QgridFrame.prepareStream({
|
|
861
|
+
prompt: "draw", model: "openai/gpt-image-2",
|
|
862
|
+
})).rejects.toMatchObject({ statusCode: 400 });
|
|
863
|
+
});
|
|
864
|
+
|
|
792
865
|
it.each([true, "auto"] as const)("rejects imageGeneration=%s before creating an SSE stream", async (imageGeneration) => {
|
|
793
866
|
await expect(
|
|
794
867
|
QgridFrame.prepareStream({
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
refreshAccessToken,
|
|
38
38
|
RefreshFailedError,
|
|
39
39
|
} from "./oauth";
|
|
40
|
+
import { CODEX_IMAGE_GENERATION_MODEL } from "./qgrid-image-generation";
|
|
40
41
|
import {
|
|
41
42
|
afterQuery,
|
|
42
43
|
beforeQuery,
|
|
@@ -45,6 +46,7 @@ import {
|
|
|
45
46
|
} from "./qgrid-run-lifecycle";
|
|
46
47
|
import {
|
|
47
48
|
buildAndValidateStrictOutputSchema,
|
|
49
|
+
completedDriverFromImageFailure,
|
|
48
50
|
type InternalQueryInput,
|
|
49
51
|
QgridDispatcher,
|
|
50
52
|
} from "./qgrid.dispatcher";
|
|
@@ -145,12 +147,35 @@ function parsePastedOAuthResult(input: string): { code: string; state: string }
|
|
|
145
147
|
}
|
|
146
148
|
|
|
147
149
|
function rejectImageGenerationStream(args: QueryInput): void {
|
|
148
|
-
if (!args.imageGeneration) return;
|
|
150
|
+
if (!args.imageGeneration && args.model !== `openai/${CODEX_IMAGE_GENERATION_MODEL}`) return;
|
|
149
151
|
throw new BadRequestException(
|
|
150
|
-
"qgrid: imageGeneration is not supported with streaming; use query
|
|
152
|
+
"qgrid: imageGeneration is not supported with streaming; use query or generateImage with qgrid.image() instead." as LocalizedString,
|
|
151
153
|
);
|
|
152
154
|
}
|
|
153
155
|
|
|
156
|
+
function rejectInvalidImageQuery(args: QueryInput): void {
|
|
157
|
+
const standalone = args.model === `openai/${CODEX_IMAGE_GENERATION_MODEL}`;
|
|
158
|
+
if (!standalone && args.imageGeneration !== "auto") return;
|
|
159
|
+
if (standalone && args.imageGeneration !== true) {
|
|
160
|
+
throw new BadRequestException(
|
|
161
|
+
"qgrid: the image model requires imageGeneration: true; use qgrid.image() with generateImage." as LocalizedString,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
if (args.tools?.length || args.jsonSchema !== undefined) {
|
|
165
|
+
throw new BadRequestException(
|
|
166
|
+
"qgrid: automatic and standalone image generation do not support caller tools or structured output." as LocalizedString,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
if (
|
|
170
|
+
args.imageGenerationOptions?.outputFormat &&
|
|
171
|
+
args.imageGenerationOptions.outputFormat !== "png"
|
|
172
|
+
) {
|
|
173
|
+
throw new BadRequestException(
|
|
174
|
+
"qgrid: automatic and standalone image generation only support PNG output." as LocalizedString,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
154
179
|
// anthropic 은 --json-schema 를 쓰지 않으므로(SON-532) strict 변환·argv 제한 없이 원형의
|
|
155
180
|
// 구문·복잡도만 검증된다 — provider 분기는 buildAndValidateStrictOutputSchema 가 소유한다.
|
|
156
181
|
function rejectInvalidCallerSchemas(args: QueryInput): void {
|
|
@@ -247,6 +272,7 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
247
272
|
|
|
248
273
|
@api({ httpMethod: "POST", clients: ["axios", "tanstack-mutation"] })
|
|
249
274
|
async query(args: QueryInput): Promise<QueryOutput> {
|
|
275
|
+
rejectInvalidImageQuery(args);
|
|
250
276
|
rejectInvalidCallerSchemas(args);
|
|
251
277
|
const resolvedArgs = await resolveTokenName(args);
|
|
252
278
|
const disconnect = createHttpDisconnectHandle();
|
|
@@ -260,10 +286,19 @@ class QgridFrameClass extends BaseFrameClass {
|
|
|
260
286
|
try {
|
|
261
287
|
result = await QgridDispatcher.query(resolvedArgs, disconnect.signal);
|
|
262
288
|
} catch (e) {
|
|
289
|
+
const completedDriver = completedDriverFromImageFailure(e);
|
|
290
|
+
const completedStep = completedDriver
|
|
291
|
+
? ([{ stepIndex, result: completedDriver }] as const)
|
|
292
|
+
: ([] as const);
|
|
263
293
|
if (disconnect.signal.aborted) {
|
|
264
|
-
await finishRunAborted(requestLogId, resolvedArgs);
|
|
294
|
+
await finishRunAborted(requestLogId, resolvedArgs, ...completedStep);
|
|
265
295
|
} else {
|
|
266
|
-
await finishRunWithError(
|
|
296
|
+
await finishRunWithError(
|
|
297
|
+
requestLogId,
|
|
298
|
+
(e as Error).message,
|
|
299
|
+
resolvedArgs,
|
|
300
|
+
...completedStep,
|
|
301
|
+
);
|
|
267
302
|
}
|
|
268
303
|
throw e;
|
|
269
304
|
}
|
|
@@ -134,14 +134,14 @@
|
|
|
134
134
|
"name": "image_cost_usd",
|
|
135
135
|
"type": "integer",
|
|
136
136
|
"nullable": true,
|
|
137
|
-
"desc": "이미지 생성
|
|
137
|
+
"desc": "이미지 생성 비용"
|
|
138
138
|
},
|
|
139
139
|
{
|
|
140
140
|
"name": "image_cost_method",
|
|
141
141
|
"type": "string",
|
|
142
142
|
"length": 100,
|
|
143
143
|
"nullable": true,
|
|
144
|
-
"desc": "이미지 비용
|
|
144
|
+
"desc": "이미지 비용 계산 방식"
|
|
145
145
|
},
|
|
146
146
|
{
|
|
147
147
|
"name": "effort",
|
|
@@ -320,7 +320,7 @@ Content-Type: application/json
|
|
|
320
320
|
"imageGeneration": false,
|
|
321
321
|
"imageGenerationOptions": {
|
|
322
322
|
"quality": "low",
|
|
323
|
-
"size": "
|
|
323
|
+
"size": "auto",
|
|
324
324
|
"background": "auto",
|
|
325
325
|
"outputFormat": "png"
|
|
326
326
|
}
|
|
@@ -385,7 +385,7 @@ Content-Type: application/json
|
|
|
385
385
|
"imageGeneration": false,
|
|
386
386
|
"imageGenerationOptions": {
|
|
387
387
|
"quality": "low",
|
|
388
|
-
"size": "
|
|
388
|
+
"size": "auto",
|
|
389
389
|
"background": "auto",
|
|
390
390
|
"outputFormat": "png"
|
|
391
391
|
}
|
|
@@ -230,8 +230,8 @@ const entityLabels = {
|
|
|
230
230
|
"entity.RequestLog.ttft_ms": "first gen TTFT(ms)",
|
|
231
231
|
"entity.RequestLog.cost_usd": "API 비용",
|
|
232
232
|
"entity.RequestLog.cost_source": "비용 출처(provider/pricing_table/mixed)",
|
|
233
|
-
"entity.RequestLog.image_cost_usd": "이미지 생성
|
|
234
|
-
"entity.RequestLog.image_cost_method": "이미지 비용
|
|
233
|
+
"entity.RequestLog.image_cost_usd": "이미지 생성 비용",
|
|
234
|
+
"entity.RequestLog.image_cost_method": "이미지 비용 계산 방식",
|
|
235
235
|
"entity.RequestLog.effort": "effort level",
|
|
236
236
|
"entity.RequestLog.history": "inject_items 히스토리",
|
|
237
237
|
"entity.RequestLog.tools": "요청에 장착된 tool 정의 목록",
|
|
@@ -27,7 +27,7 @@ describe("OpenAI Codex backend protocol", () => {
|
|
|
27
27
|
} })).toMatchObject({ generation: { route: "codex-responses", size: "841x1870" } });
|
|
28
28
|
});
|
|
29
29
|
it("distinguishes partial function items from completed arguments", () => {
|
|
30
|
-
const item = { type: "function_call", call_id: "call", name: "
|
|
30
|
+
const item = { type: "function_call", call_id: "call", name: "generate_image", arguments: "{}" };
|
|
31
31
|
expect(normalizeOpenAIEvent({ type: "response.output_item.added", item })).toEqual({ type: "output-item", item, completed: false });
|
|
32
32
|
expect(normalizeOpenAIEvent({ type: "response.output_item.done", item })).toEqual({ type: "output-item", item, completed: true });
|
|
33
33
|
});
|
|
@@ -149,4 +149,26 @@ describe("OpenAI Codex backend protocol", () => {
|
|
|
149
149
|
}),
|
|
150
150
|
).toEqual({ type: "image", id: "img", base64: "aGVsbG8=", mimeType: "image/png", revisedPrompt: "better", generation: { route: "codex-responses" } });
|
|
151
151
|
});
|
|
152
|
+
|
|
153
|
+
it("preserves zero image-tool usage without treating it as missing", () => {
|
|
154
|
+
const usage = { input_tokens: 0, output_tokens: 0, total_tokens: 0 };
|
|
155
|
+
expect(normalizeOpenAIEvent({ type: "response.completed", response: {
|
|
156
|
+
id: "r", tool_usage: { image_gen: usage },
|
|
157
|
+
} })).toEqual({
|
|
158
|
+
type: "completed", responseId: "r",
|
|
159
|
+
imageGeneration: { route: "codex-responses", usage },
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it.each([undefined, null, {}, { input_tokens: "4", output_tokens: 1, total_tokens: 5 }, { input_tokens: -1, output_tokens: 1, total_tokens: 0 }])(
|
|
164
|
+
"ignores missing or malformed image-tool usage: %j", (usage) => {
|
|
165
|
+
expect(normalizeOpenAIEvent({ type: "response.completed", response: {
|
|
166
|
+
id: "r", tool_usage: { image_gen: usage },
|
|
167
|
+
tools: [{ type: "image_generation", model: "gpt-image-2-codex", size: "auto", quality: "auto" }],
|
|
168
|
+
} })).toEqual({
|
|
169
|
+
type: "completed", responseId: "r",
|
|
170
|
+
imageGeneration: { route: "codex-responses", model: "gpt-image-2-codex" },
|
|
171
|
+
});
|
|
172
|
+
},
|
|
173
|
+
);
|
|
152
174
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { ImageGenerationMetadata } from "../../../application/qgrid/qgrid.types";
|
|
4
4
|
import { type OpenAIEffort } from "../common/effort";
|
|
5
5
|
|
|
6
6
|
export const CHATGPT_CODEX_RESPONSES_URL =
|
|
@@ -180,7 +180,13 @@ export type OpenAINormalizedEvent =
|
|
|
180
180
|
generation?: ImageGenerationMetadata;
|
|
181
181
|
}
|
|
182
182
|
| { type: "output-item"; item: OpenAIResponseItem; completed?: boolean }
|
|
183
|
-
| {
|
|
183
|
+
| {
|
|
184
|
+
type: "completed";
|
|
185
|
+
responseId: string;
|
|
186
|
+
usage?: OpenAIUsage;
|
|
187
|
+
model?: string;
|
|
188
|
+
imageGeneration?: ImageGenerationMetadata;
|
|
189
|
+
}
|
|
184
190
|
| { type: "error"; error: OpenAIProtocolError };
|
|
185
191
|
|
|
186
192
|
export class OpenAIProtocolError extends Error {
|
|
@@ -286,11 +292,27 @@ export function normalizeOpenAIEvent(raw: unknown): OpenAINormalizedEvent | unde
|
|
|
286
292
|
}
|
|
287
293
|
if (type === "response.completed") {
|
|
288
294
|
const response = (event.response ?? {}) as Record<string, unknown>;
|
|
295
|
+
const toolUsage = response.tool_usage as Record<string, unknown> | undefined;
|
|
296
|
+
const parsedImageUsage = ImageGenerationMetadata.shape.usage.safeParse(toolUsage?.image_gen);
|
|
297
|
+
const imageUsage = parsedImageUsage.success ? parsedImageUsage.data : undefined;
|
|
298
|
+
const imageTool = Array.isArray(response.tools)
|
|
299
|
+
? response.tools.find((tool) => tool?.type === "image_generation")
|
|
300
|
+
: undefined;
|
|
301
|
+
const imageModel = typeof imageTool?.model === "string" ? imageTool.model : undefined;
|
|
289
302
|
return {
|
|
290
303
|
type: "completed",
|
|
291
304
|
responseId: String(response.id ?? ""),
|
|
292
305
|
...(response.usage ? { usage: usageFrom(response.usage) } : {}),
|
|
293
306
|
...(typeof response.model === "string" ? { model: response.model } : {}),
|
|
307
|
+
...(imageUsage || imageModel
|
|
308
|
+
? {
|
|
309
|
+
imageGeneration: {
|
|
310
|
+
route: "codex-responses" as const,
|
|
311
|
+
...(imageModel ? { model: imageModel } : {}),
|
|
312
|
+
...(imageUsage ? { usage: imageUsage } : {}),
|
|
313
|
+
},
|
|
314
|
+
}
|
|
315
|
+
: {}),
|
|
294
316
|
};
|
|
295
317
|
}
|
|
296
318
|
if (type === "error" || type === "response.failed" || type === "response.incomplete") {
|
|
@@ -29,6 +29,21 @@ async function collect<T>(iterable: AsyncIterable<T>): Promise<T[]> {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
describe("OpenAI direct HTTPS client", () => {
|
|
32
|
+
it.each([true, { background: "opaque" }])("keeps chat-model images on Responses (%j)", async (imageGeneration) => {
|
|
33
|
+
const fetchMock = vi.fn<typeof fetch>(async () =>
|
|
34
|
+
sse({ type: "response.completed", response: { id: "hosted-image" } }),
|
|
35
|
+
);
|
|
36
|
+
const client = new OpenAIDirectClient({
|
|
37
|
+
credentials: { accessToken: "access", accountId: "acct" }, fetch: fetchMock,
|
|
38
|
+
});
|
|
39
|
+
await collect(client.responses({ model: "gpt-6-luna", history: [], imageGeneration }));
|
|
40
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
41
|
+
expect(fetchMock.mock.calls[0]?.[0]).toBe(CHATGPT_CODEX_RESPONSES_URL);
|
|
42
|
+
expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body))).toMatchObject({
|
|
43
|
+
model: "gpt-6-luna", tools: [expect.objectContaining({ type: "image_generation" })],
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
32
47
|
it("posts the pinned body and Codex identity without live calls", async () => {
|
|
33
48
|
const fetchMock = vi.fn<typeof fetch>(async () =>
|
|
34
49
|
sse(
|
|
@@ -450,8 +450,9 @@ export class OpenAIDirectClient {
|
|
|
450
450
|
|
|
451
451
|
responses(options: OpenAIResponsesOptions, signal?: AbortSignal): OpenAIEventStream {
|
|
452
452
|
if (
|
|
453
|
-
|
|
454
|
-
options.imageGeneration
|
|
453
|
+
options.model === "gpt-image-2" ||
|
|
454
|
+
(typeof options.imageGeneration === "object" &&
|
|
455
|
+
options.imageGeneration.background === "transparent")
|
|
455
456
|
) {
|
|
456
457
|
return this.standaloneImages(options, signal);
|
|
457
458
|
}
|
|
@@ -498,10 +499,19 @@ export class OpenAIDirectClient {
|
|
|
498
499
|
continue;
|
|
499
500
|
}
|
|
500
501
|
if (!response.ok) throw await responseError(response);
|
|
501
|
-
const images = await readStandaloneImageResponse(
|
|
502
|
+
const images = await readStandaloneImageResponse(
|
|
503
|
+
response,
|
|
504
|
+
typeof options.imageGeneration === "object" &&
|
|
505
|
+
options.imageGeneration.background === "transparent",
|
|
506
|
+
);
|
|
502
507
|
signal?.throwIfAborted();
|
|
503
508
|
yield* images;
|
|
504
|
-
|
|
509
|
+
const firstImage = images[0];
|
|
510
|
+
yield {
|
|
511
|
+
type: "completed",
|
|
512
|
+
responseId: requestId,
|
|
513
|
+
model: firstImage?.type === "image" ? firstImage.generation?.model : "gpt-image-2",
|
|
514
|
+
};
|
|
505
515
|
return;
|
|
506
516
|
}
|
|
507
517
|
}
|