@cartanova/qgrid-cli 2.9.6 → 2.9.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +6 -2
- package/bundle/dist/utils/providers/common/model-cost.js +20 -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 +9 -1
- package/bundle/src/utils/providers/anthropic/anthropic-constants.ts +11 -2
- package/bundle/src/utils/providers/common/effort.test.ts +3 -0
- package/bundle/src/utils/providers/common/model-cost.test.ts +23 -0
- package/bundle/src/utils/providers/common/model-cost.ts +28 -2
- 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 +117 -0
- package/bundle/src/utils/providers/openai/openai-dispatcher.ts +136 -1
- package/bundle/web-dist/client/assets/index-D8VwLsuq.js +106 -0
- package/bundle/web-dist/client/assets/index-D_sagzsR.css +1 -0
- package/bundle/web-dist/client/assets/logs-_T8KV5ne.js +1 -0
- package/bundle/web-dist/client/assets/{monit-BkGkzl6m.js → monit-DRCrtO2j.js} +1 -1
- package/bundle/web-dist/client/assets/request-model-eygfdRv7.js +1 -0
- package/bundle/web-dist/client/assets/{routes-DqY8SXcB.js → routes-5A6mia6C.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-BpP9WVFI.js +1 -0
- package/bundle/web-dist/client/assets/show-D5ifZXdy.js +2 -0
- package/bundle/web-dist/client/assets/tokens-Cfe6WvLK.js +1 -0
- package/bundle/web-dist/client/assets/{use-oauth-login-flow-Cjdnj_8D.js → use-oauth-login-flow-BTLUGhwh.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 +275 -51
- package/package.json +1 -1
- package/skills/qgrid/SKILL.md +10 -1
- package/skills/qgrid/references/ai-sdk-provider-contract.md +1 -1
- package/skills/qgrid/references/anthropic-claude-code-runtime.md +4 -2
- package/skills/qgrid/references/decision-rationale.md +11 -2
- package/skills/qgrid/references/openai-codex-runtime.md +30 -5
- package/skills/qgrid/references/provider-runtime-differences.md +1 -1
- 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-BtVmANcA.js +0 -106
- package/bundle/web-dist/client/assets/logs-BCuaLfPL.js +0 -1
- package/bundle/web-dist/client/assets/settings-DbkDAJfh.js +0 -1
- package/bundle/web-dist/client/assets/show-Dx5JFzwy.js +0 -2
- package/bundle/web-dist/client/assets/tokens-DHahzDQJ.js +0 -1
- /package/bundle/web-dist/client/assets/{plus-p4vtg6LN.js → plus-CknMNCP5.js} +0 -0
|
@@ -327,7 +327,8 @@ Content-Type: application/json
|
|
|
327
327
|
"imageGenerationOptions": {
|
|
328
328
|
"quality": "low",
|
|
329
329
|
"size": "1024x1024",
|
|
330
|
-
"background": "auto"
|
|
330
|
+
"background": "auto",
|
|
331
|
+
"outputFormat": "png"
|
|
331
332
|
}
|
|
332
333
|
}
|
|
333
334
|
}
|
|
@@ -390,7 +391,8 @@ Content-Type: application/json
|
|
|
390
391
|
"imageGenerationOptions": {
|
|
391
392
|
"quality": "low",
|
|
392
393
|
"size": "1024x1024",
|
|
393
|
-
"background": "auto"
|
|
394
|
+
"background": "auto",
|
|
395
|
+
"outputFormat": "png"
|
|
394
396
|
}
|
|
395
397
|
}
|
|
396
398
|
}
|
|
@@ -33,7 +33,9 @@ describe("Anthropic 1M context policy", () => {
|
|
|
33
33
|
expect(supports1MContext("claude-fable-5-1")).toBe(true);
|
|
34
34
|
expect(supports1MContext("claude-fable-5")).toBe(true);
|
|
35
35
|
expect(supports1MContext("claude-opus-5")).toBe(true);
|
|
36
|
+
expect(supports1MContext("claude-opus-5-5")).toBe(true);
|
|
36
37
|
expect(supports1MContext("claude-sonnet-5")).toBe(true);
|
|
38
|
+
expect(supports1MContext("claude-sonnet-5-5")).toBe(true);
|
|
37
39
|
expect(supports1MContext("claude-sonnet-4-6")).toBe(true);
|
|
38
40
|
expect(supports1MContext("claude-opus-4-6")).toBe(true);
|
|
39
41
|
expect(supports1MContext("claude-opus-4-8")).toBe(true);
|
|
@@ -48,7 +50,9 @@ describe("Anthropic 1M context policy", () => {
|
|
|
48
50
|
expect(needsCli1mSuffix("claude-fable-5-1")).toBe(false);
|
|
49
51
|
expect(needsCli1mSuffix("claude-fable-5")).toBe(false);
|
|
50
52
|
expect(needsCli1mSuffix("claude-opus-5")).toBe(false);
|
|
53
|
+
expect(needsCli1mSuffix("claude-opus-5-5")).toBe(false);
|
|
51
54
|
expect(needsCli1mSuffix("claude-sonnet-5")).toBe(false);
|
|
55
|
+
expect(needsCli1mSuffix("claude-sonnet-5-5")).toBe(false);
|
|
52
56
|
expect(needsCli1mSuffix("claude-sonnet-4-6")).toBe(true);
|
|
53
57
|
expect(needsCli1mSuffix("claude-opus-4-6")).toBe(true);
|
|
54
58
|
expect(needsCli1mSuffix("claude-opus-4-8")).toBe(false);
|
|
@@ -78,13 +82,17 @@ describe("Anthropic 1M context policy", () => {
|
|
|
78
82
|
});
|
|
79
83
|
|
|
80
84
|
describe("Anthropic thinking policy", () => {
|
|
81
|
-
it("Fable 5/5.1
|
|
85
|
+
it("Fable 5/5.1, Opus 5, Opus 5.5, Sonnet 5.5 는 adaptive thinking 을 보존한다", () => {
|
|
82
86
|
expect(usesAdaptiveThinking("claude-fable-5-1")).toBe(true);
|
|
83
87
|
expect(usesAdaptiveThinking("anthropic/claude-fable-5-1")).toBe(true);
|
|
84
88
|
expect(usesAdaptiveThinking("claude-fable-5")).toBe(true);
|
|
85
89
|
expect(usesAdaptiveThinking("anthropic/claude-fable-5")).toBe(true);
|
|
86
90
|
expect(usesAdaptiveThinking("claude-opus-5")).toBe(true);
|
|
87
91
|
expect(usesAdaptiveThinking("anthropic/claude-opus-5")).toBe(true);
|
|
92
|
+
expect(usesAdaptiveThinking("claude-opus-5-5")).toBe(true);
|
|
93
|
+
expect(usesAdaptiveThinking("anthropic/claude-opus-5-5[1m]")).toBe(true);
|
|
94
|
+
expect(usesAdaptiveThinking("claude-sonnet-5-5")).toBe(true);
|
|
95
|
+
expect(usesAdaptiveThinking("anthropic/claude-sonnet-5-5[1m]")).toBe(true);
|
|
88
96
|
expect(usesAdaptiveThinking("claude-sonnet-5")).toBe(false);
|
|
89
97
|
expect(usesAdaptiveThinking("claude-opus-4-8")).toBe(false);
|
|
90
98
|
});
|
|
@@ -48,6 +48,8 @@ const ONE_MILLION_CONTEXT_MODELS = new Set([
|
|
|
48
48
|
"claude-fable-5-1",
|
|
49
49
|
"claude-fable-5",
|
|
50
50
|
"claude-opus-5",
|
|
51
|
+
"claude-opus-5-5",
|
|
52
|
+
"claude-sonnet-5-5",
|
|
51
53
|
"claude-sonnet-5",
|
|
52
54
|
"claude-sonnet-4-6",
|
|
53
55
|
"claude-opus-4-6",
|
|
@@ -55,11 +57,18 @@ const ONE_MILLION_CONTEXT_MODELS = new Set([
|
|
|
55
57
|
]);
|
|
56
58
|
|
|
57
59
|
const CLI_ONE_MILLION_SUFFIX_MODELS = new Set(["claude-sonnet-4-6", "claude-opus-4-6"]);
|
|
58
|
-
// Fable 5/5.1
|
|
60
|
+
// Fable 5/5.1, Opus 5.5, Sonnet 5.5 는 thinking disabled 를 거부한다(Fable: CLI 카탈로그 `rejects_disabled_thinking`,
|
|
61
|
+
// Opus 5.5: 공식 사양 "can't be turned off", 2026-09-22 출시. Sonnet 5.5: API `{type: "disabled"}` 400, CLI 카탈로그 `rejects_disabled_thinking`, 2026-09-29 출시). Opus 5 는
|
|
59
62
|
// disabled 를 high 이하에서 지원하지만, 공식 기본값·권장 동작인 adaptive thinking 을 유지한다. qgrid
|
|
60
63
|
// 기본 effort=low 가 비용/지연 제어를 맡으며, 이 정책은 disabled+xhigh/max 400 오류와 disabled 시
|
|
61
64
|
// tool/XML 출력 오염도 피한다.
|
|
62
|
-
const ADAPTIVE_THINKING_MODELS = new Set([
|
|
65
|
+
const ADAPTIVE_THINKING_MODELS = new Set([
|
|
66
|
+
"claude-fable-5-1",
|
|
67
|
+
"claude-fable-5",
|
|
68
|
+
"claude-opus-5",
|
|
69
|
+
"claude-opus-5-5",
|
|
70
|
+
"claude-sonnet-5-5",
|
|
71
|
+
]);
|
|
63
72
|
|
|
64
73
|
// Fable 계열: 구독 플랜에서 usage credits 가 필요한 최상위 티어. 구독 usage API 의
|
|
65
74
|
// `seven_day_overage_included` 버킷은 이 계열에만 적용되므로 quota threshold 판정이 참조한다.
|
|
@@ -23,6 +23,8 @@ describe("resolveOpenAIEffort", () => {
|
|
|
23
23
|
expect(resolveOpenAIEffort("gpt-5.6-terra", "max")).toBe("max");
|
|
24
24
|
expect(resolveOpenAIEffort("gpt-5.6-terra", "ultra")).toBe("ultra");
|
|
25
25
|
expect(resolveOpenAIEffort("gpt-5.6-luna", "max")).toBe("max");
|
|
26
|
+
expect(resolveOpenAIEffort("gpt-6-sol", "ultra")).toBe("ultra");
|
|
27
|
+
expect(resolveOpenAIEffort("gpt-6-luna", "max")).toBe("max");
|
|
26
28
|
});
|
|
27
29
|
|
|
28
30
|
it("공개 API 어휘(none/minimal)와 다른 provider 어휘, 오타는 조용히 미지정으로 바꾼다", () => {
|
|
@@ -36,6 +38,7 @@ describe("resolveOpenAIEffort", () => {
|
|
|
36
38
|
expect(resolveOpenAIEffort("gpt-5.5", "max")).toBeUndefined();
|
|
37
39
|
expect(resolveOpenAIEffort("gpt-5.4", "ultra")).toBeUndefined();
|
|
38
40
|
expect(resolveOpenAIEffort("gpt-5.6-luna", "ultra")).toBeUndefined();
|
|
41
|
+
expect(resolveOpenAIEffort("gpt-6-luna", "ultra")).toBeUndefined();
|
|
39
42
|
expect(resolveOpenAIEffort("gpt-5.3-codex-spark", "xhigh")).toBe("xhigh");
|
|
40
43
|
});
|
|
41
44
|
|
|
@@ -13,6 +13,8 @@ describe("calculateCostUsd", () => {
|
|
|
13
13
|
});
|
|
14
14
|
it.each([
|
|
15
15
|
["gpt-6-astra", 10, 50, 1, 12.5],
|
|
16
|
+
["gpt-6-sol", 2, 10, 0.2, 2.5],
|
|
17
|
+
["gpt-6-luna", 0.1, 0.5, 0.01, 0.125],
|
|
16
18
|
["gpt-5.6-sol", 4, 20, 0.4, 5],
|
|
17
19
|
["gpt-5.6-terra", 2, 12, 0.2, 2.5],
|
|
18
20
|
["gpt-5.6-luna", 0.2, 1.2, 0.02, 0.25],
|
|
@@ -54,7 +56,10 @@ describe("calculateCostUsd", () => {
|
|
|
54
56
|
["claude-opus-4-7", 5, 25, 0.5, 10],
|
|
55
57
|
["claude-opus-4-8", 5, 25, 0.5, 10],
|
|
56
58
|
["claude-opus-5", 5, 25, 0.5, 10],
|
|
59
|
+
// Opus 5.5 는 cache read 만 0.05x 특례($0.20), cache write 배율은 표준(5m $5 / 1h $8).
|
|
60
|
+
["claude-opus-5-5", 4, 20, 0.2, 8],
|
|
57
61
|
["claude-sonnet-5", 2, 10, 0.2, 4],
|
|
62
|
+
["claude-sonnet-5-5", 2, 10, 0.2, 4],
|
|
58
63
|
])(
|
|
59
64
|
"%s official Anthropic rates for 5m/1h cache writes",
|
|
60
65
|
(model, inputTokens, outputTokens, cachedInputTokens, cacheCreationInputTokens) => {
|
|
@@ -92,6 +97,8 @@ describe("calculateCostUsd", () => {
|
|
|
92
97
|
|
|
93
98
|
it.each([
|
|
94
99
|
["gpt-6-astra", 10, 50, 1, 12.5, 5.6125],
|
|
100
|
+
["gpt-6-sol", 2, 10, 0.2, 2.5, 1.1225],
|
|
101
|
+
["gpt-6-luna", 0.1, 0.5, 0.01, 0.125, 0.056125],
|
|
95
102
|
["gpt-5.6-sol", 4, 20, 0.4, 5, 2.245],
|
|
96
103
|
["gpt-5.6-terra", 2, 12, 0.2, 2.5, 1.3225],
|
|
97
104
|
["gpt-5.6-luna", 0.2, 1.2, 0.02, 0.25, 0.13225],
|
|
@@ -118,6 +125,8 @@ describe("calculateCostUsd", () => {
|
|
|
118
125
|
|
|
119
126
|
it.each([
|
|
120
127
|
["gpt-6-astra", 2.475],
|
|
128
|
+
["gpt-6-sol", 0.495],
|
|
129
|
+
["gpt-6-luna", 0.02475],
|
|
121
130
|
["gpt-5.6-sol", 0.99],
|
|
122
131
|
["gpt-5.6-terra", 0.498],
|
|
123
132
|
["gpt-5.6-luna", 0.0498],
|
|
@@ -133,6 +142,8 @@ describe("calculateCostUsd", () => {
|
|
|
133
142
|
|
|
134
143
|
it.each([
|
|
135
144
|
["gpt-6-astra", 2.725],
|
|
145
|
+
["gpt-6-sol", 0.545],
|
|
146
|
+
["gpt-6-luna", 0.02725],
|
|
136
147
|
["gpt-5.6-sol", 1.09],
|
|
137
148
|
["gpt-5.6-terra", 0.548],
|
|
138
149
|
["gpt-5.6-luna", 0.0548],
|
|
@@ -204,6 +215,18 @@ describe("calculateCostUsd", () => {
|
|
|
204
215
|
expect(calculateCostUsd("claude-fable-5-1", usage)).toBeCloseTo(5.7625, 10);
|
|
205
216
|
});
|
|
206
217
|
|
|
218
|
+
it("Opus 5.5 는 $4/$20 에 cache read 0.05x($0.20), 1h cache write $8 을 적용한다", () => {
|
|
219
|
+
// uncached 30K*$4 + output 10K*$20 + cache read 50K*$0.20 + 1h write 20K*$8 = 0.12+0.2+0.01+0.16
|
|
220
|
+
expect(
|
|
221
|
+
calculateCostUsd("anthropic/claude-opus-5-5", {
|
|
222
|
+
inputTokens: 100_000,
|
|
223
|
+
outputTokens: 10_000,
|
|
224
|
+
cachedInputTokens: 50_000,
|
|
225
|
+
cacheCreationInputTokens1h: 20_000,
|
|
226
|
+
}),
|
|
227
|
+
).toBeCloseTo(0.49, 10);
|
|
228
|
+
});
|
|
229
|
+
|
|
207
230
|
it("Fable 의 5분/1시간 cache write 단가도 구분한다", () => {
|
|
208
231
|
expect(
|
|
209
232
|
calculateCostUsd("claude-fable-5", {
|
|
@@ -34,7 +34,7 @@ export interface ModelCosts {
|
|
|
34
34
|
|
|
35
35
|
// ── OpenAI ─────────────────────────────────────────────────────────
|
|
36
36
|
//
|
|
37
|
-
// Standard pricing verified 2026-09-
|
|
37
|
+
// Standard pricing verified 2026-09-23 (기존 모델 단가 변동 없음): https://developers.openai.com/api/docs/pricing
|
|
38
38
|
// 신모델 출시마다 단가가 바뀌므로(5.2→5.4→5.5) 모델 추가 시 반드시 공식 페이지 재확인해야함
|
|
39
39
|
|
|
40
40
|
// GPT-5.4에서 처음 도입된 long-context 할증 (5.2/5.3-codex는 해당 없음, 5.4-mini/nano는 공식 표에서 long-context 단가 없음)
|
|
@@ -65,6 +65,26 @@ const OPENAI_COSTS: Record<string, OpenAIModelSpec> = {
|
|
|
65
65
|
cacheCreationInputTokens: 12.5,
|
|
66
66
|
longContext: LONG_CONTEXT_272K,
|
|
67
67
|
},
|
|
68
|
+
// GPT-6 Sol, Luna (2026-09-22 출시): Standard API pricing verified 2026-09-23. GPT-5.6 Sol/Luna 의 절반 단가.
|
|
69
|
+
// Codex 카탈로그(2026-09-23)는 Sol 에 ultra, Luna 에 max 까지 허용하고 기본 effort 는 둘 다 medium 이다.
|
|
70
|
+
// @see https://developers.openai.com/api/docs/models/gpt-6-sol
|
|
71
|
+
// @see https://developers.openai.com/api/docs/models/gpt-6-luna
|
|
72
|
+
"gpt-6-sol": {
|
|
73
|
+
maxEffort: "ultra",
|
|
74
|
+
inputTokens: 2,
|
|
75
|
+
outputTokens: 10,
|
|
76
|
+
cachedInputTokens: 0.2,
|
|
77
|
+
cacheCreationInputTokens: 2.5,
|
|
78
|
+
longContext: LONG_CONTEXT_272K,
|
|
79
|
+
},
|
|
80
|
+
"gpt-6-luna": {
|
|
81
|
+
maxEffort: "max",
|
|
82
|
+
inputTokens: 0.1,
|
|
83
|
+
outputTokens: 0.5,
|
|
84
|
+
cachedInputTokens: 0.01,
|
|
85
|
+
cacheCreationInputTokens: 0.125,
|
|
86
|
+
longContext: LONG_CONTEXT_272K,
|
|
87
|
+
},
|
|
68
88
|
// GPT-5.6 Sol, Terra, Luna. cache write 단가는 외부 logger/manual usage 입력을 위해
|
|
69
89
|
// 유지한다. 현재 OpenAI 응답 usage 에 cache write 토큰 필드가 없으면 비용은 0으로 계산된다.
|
|
70
90
|
// @see https://developers.openai.com/api/docs/models/gpt-5.6-sol
|
|
@@ -140,7 +160,7 @@ function anthropicCosts(
|
|
|
140
160
|
};
|
|
141
161
|
}
|
|
142
162
|
|
|
143
|
-
// 가격 출처: https://platform.claude.com/docs/en/about-claude/pricing (2026-09-
|
|
163
|
+
// 가격 출처: https://platform.claude.com/docs/en/about-claude/pricing (2026-09-23 확인, 기존 모델 단가 변동 없음)
|
|
144
164
|
const ANTHROPIC_COSTS: Record<string, ModelCosts> = {
|
|
145
165
|
// Fable 5.1 (2026-09-01 출시): input/output 은 Fable 5 와 같고 cache read 만 0.025x($0.25) 특례.
|
|
146
166
|
// @see https://platform.claude.com/docs/en/models/fable-5-1/overview
|
|
@@ -162,9 +182,15 @@ const ANTHROPIC_COSTS: Record<string, ModelCosts> = {
|
|
|
162
182
|
"claude-opus-4-7": anthropicCosts(5, 25),
|
|
163
183
|
"claude-opus-4-8": anthropicCosts(5, 25),
|
|
164
184
|
"claude-opus-5": anthropicCosts(5, 25),
|
|
185
|
+
// Opus 5.5 (2026-09-22 출시): $4/$20. cache read 는 0.05x($0.20) 특례, cache write 는 표준 배율($5/$8).
|
|
186
|
+
// @see https://platform.claude.com/docs/en/models/opus-5-5/overview
|
|
187
|
+
"claude-opus-5-5": anthropicCosts(4, 20, { cachedInputTokens: 0.2 }),
|
|
165
188
|
// Sonnet 5 의 introductory $2/$10 이 정식 단가로 확정됐다. 2026-09-01 에 예정됐던 $3/$15 인상은
|
|
166
189
|
// 취소됐으므로 날짜 분기 없이 고정 단가로 계산한다.
|
|
167
190
|
"claude-sonnet-5": anthropicCosts(2, 10),
|
|
191
|
+
// Sonnet 5.5 (2026-09-29 출시): Sonnet 5 와 같은 $2/$10, cache read 는 표준 0.1x($0.20).
|
|
192
|
+
// @see https://platform.claude.com/docs/en/about-claude/pricing
|
|
193
|
+
"claude-sonnet-5-5": anthropicCosts(2, 10),
|
|
168
194
|
};
|
|
169
195
|
|
|
170
196
|
// gpt-5.3-codex-spark 는 research preview 로 공식 token 단가가 아직 final 이 아니다.
|
|
@@ -79,7 +79,7 @@ export interface GenerateRequest {
|
|
|
79
79
|
reuseInput?: Array<UserInput>;
|
|
80
80
|
// OpenAI image_generation tool 을 켠다(OpenAI 경로 전용, opt-in).
|
|
81
81
|
// 이 플래그가 있으면 dispatcher 는 항상 cold thread 로 실행하고 재사용 라우팅을 건너뛴다.
|
|
82
|
-
imageGeneration?: boolean;
|
|
82
|
+
imageGeneration?: boolean | "auto";
|
|
83
83
|
imageGenerationOptions?: ImageGenerationOptions;
|
|
84
84
|
}
|
|
85
85
|
|
|
@@ -8,6 +8,11 @@ import {
|
|
|
8
8
|
} from "./openai-backend-protocol";
|
|
9
9
|
|
|
10
10
|
describe("OpenAI Codex backend protocol", () => {
|
|
11
|
+
it("distinguishes partial function items from completed arguments", () => {
|
|
12
|
+
const item = { type: "function_call", call_id: "call", name: "generate_transparent_image", arguments: "{}" };
|
|
13
|
+
expect(normalizeOpenAIEvent({ type: "response.output_item.added", item })).toEqual({ type: "output-item", item, completed: false });
|
|
14
|
+
expect(normalizeOpenAIEvent({ type: "response.output_item.done", item })).toEqual({ type: "output-item", item, completed: true });
|
|
15
|
+
});
|
|
11
16
|
it("uses returned bytes rather than a mismatched format label", () => {
|
|
12
17
|
expect(normalizeOpenAIEvent({ type: "response.output_item.done", item: {
|
|
13
18
|
type: "image_generation_call", result: "UklGRgAAAABXRUJQ", output_format: "png",
|
|
@@ -179,7 +179,7 @@ export type OpenAINormalizedEvent =
|
|
|
179
179
|
revisedPrompt?: string;
|
|
180
180
|
generation?: ImageGenerationMetadata;
|
|
181
181
|
}
|
|
182
|
-
| { type: "output-item"; item: OpenAIResponseItem }
|
|
182
|
+
| { type: "output-item"; item: OpenAIResponseItem; completed?: boolean }
|
|
183
183
|
| { type: "completed"; responseId: string; usage?: OpenAIUsage; model?: string }
|
|
184
184
|
| { type: "error"; error: OpenAIProtocolError };
|
|
185
185
|
|
|
@@ -256,7 +256,7 @@ export function normalizeOpenAIEvent(raw: unknown): OpenAINormalizedEvent | unde
|
|
|
256
256
|
: {}),
|
|
257
257
|
};
|
|
258
258
|
}
|
|
259
|
-
return { type: "output-item", item };
|
|
259
|
+
return { type: "output-item", item, completed: type === "response.output_item.done" };
|
|
260
260
|
}
|
|
261
261
|
if (type === "response.completed") {
|
|
262
262
|
const response = (event.response ?? {}) as Record<string, unknown>;
|
|
@@ -295,6 +295,120 @@ describe("OpenAIDispatcher direct runtime", () => {
|
|
|
295
295
|
expect(complete).toHaveBeenCalledWith(expect.objectContaining({ text: "ab" }));
|
|
296
296
|
});
|
|
297
297
|
|
|
298
|
+
it("returns multiple hosted images with one driver request", async () => {
|
|
299
|
+
const run = vi.fn(() => events(
|
|
300
|
+
{ type: "image", id: "first", base64: "first", mimeType: "image/png" },
|
|
301
|
+
{ type: "image", id: "second", base64: "second", mimeType: "image/webp" },
|
|
302
|
+
{ type: "completed", responseId: "driver" },
|
|
303
|
+
));
|
|
304
|
+
const d = dispatcher(run);
|
|
305
|
+
await d.onTokenAdded(1, "one", credentials);
|
|
306
|
+
const result = await d.generate(request({ imageGeneration: "auto" }));
|
|
307
|
+
expect(result.images).toMatchObject([{ data: "first", mediaType: "image/png" }, { data: "second", mediaType: "image/webp" }]);
|
|
308
|
+
expect(run).toHaveBeenCalledTimes(1);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("lets automatic mode return text or a hosted image with one driver request", async () => {
|
|
312
|
+
for (const image of [false, true]) {
|
|
313
|
+
const run = vi.fn((options: OpenAIResponsesOptions) => {
|
|
314
|
+
const body = buildOpenAIResponsesRequest(options);
|
|
315
|
+
expect(body.parallel_tool_calls).toBe(false);
|
|
316
|
+
expect(body.tools).toEqual(expect.arrayContaining([{ type: "image_generation" }, expect.objectContaining({ name: "generate_transparent_image" })]));
|
|
317
|
+
return events(...(image ? [{ type: "image" as const, base64: "png", mimeType: "image/png" as const }] : [{ type: "text-delta" as const, text: "hello" }]), { type: "completed", responseId: "r" });
|
|
318
|
+
});
|
|
319
|
+
const d = dispatcher(run);
|
|
320
|
+
await d.onTokenAdded(1, "one", credentials);
|
|
321
|
+
const result = await d.generate(request({ imageGeneration: "auto" }));
|
|
322
|
+
expect(run).toHaveBeenCalledTimes(1);
|
|
323
|
+
expect(image ? result.images?.length : result.text).toBe(image ? 1 : "hello");
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("runs one standalone transparent call after completed arguments and preserves driver accounting", async () => {
|
|
328
|
+
const calls: OpenAIResponsesOptions[] = [];
|
|
329
|
+
const signals: Array<AbortSignal | undefined> = [];
|
|
330
|
+
const item = { type: "function_call", call_id: "call", name: "generate_transparent_image", arguments: JSON.stringify({ prompt: "remove background" }) };
|
|
331
|
+
const d = dispatcher((options, signal) => {
|
|
332
|
+
calls.push(options); signals.push(signal);
|
|
333
|
+
if (calls.length === 1) return events(
|
|
334
|
+
{ type: "output-item", item: { ...item, arguments: "" }, completed: false },
|
|
335
|
+
{ type: "output-item", item, completed: true },
|
|
336
|
+
{ type: "output-item", item, completed: true },
|
|
337
|
+
{ type: "completed", responseId: "driver", model: "actual-driver", usage: { inputTokens: 10, cachedInputTokens: 2, outputTokens: 3, reasoningTokens: 1, totalTokens: 13 } },
|
|
338
|
+
);
|
|
339
|
+
return events({ type: "image", base64: "png", mimeType: "image/png", generation: { route: "codex-images", model: "gpt-image-2" } }, { type: "completed", responseId: "image", model: "gpt-image-2", usage: { inputTokens: 999, outputTokens: 999, cachedInputTokens: 0, reasoningTokens: 0, totalTokens: 1998 } });
|
|
340
|
+
});
|
|
341
|
+
await d.onTokenAdded(1, "one", credentials);
|
|
342
|
+
const history = [{ type: "message", role: "user", content: [{ type: "input_image", image_url: "https://example.com/ref.png" }] }];
|
|
343
|
+
const result = await d.generate(request({ imageGeneration: "auto", systemPrompt: "system", coldHistory: history, imageGenerationOptions: { outputFormat: "webp" } }));
|
|
344
|
+
expect(calls).toHaveLength(2);
|
|
345
|
+
expect(calls[1]).toMatchObject({ instructions: "system", imageGeneration: { background: "transparent", output_format: "png" } });
|
|
346
|
+
expect(calls[1]!.history[0]).toEqual(history[0]);
|
|
347
|
+
expect(calls[1]!.tools).toBeUndefined();
|
|
348
|
+
expect(calls[1]!.outputSchema).toBeUndefined();
|
|
349
|
+
expect(signals[0]).toBe(signals[1]);
|
|
350
|
+
expect(result).toMatchObject({ model: "actual-driver", usage: { inputTokens: 10, outputTokens: 3, totalTokens: 13 }, images: [{ generation: { model: "gpt-image-2" } }] });
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it("rejects incomplete, invalid, multiple and mixed automatic image calls before standalone execution", async () => {
|
|
354
|
+
const item = { type: "function_call", call_id: "call", name: "generate_transparent_image", arguments: JSON.stringify({ prompt: "remove background" }) };
|
|
355
|
+
const done = { type: "output-item" as const, item, completed: true };
|
|
356
|
+
const badEvents: OpenAINormalizedEvent[][] = [
|
|
357
|
+
[{ ...done, completed: false }],
|
|
358
|
+
[{ ...done, item: { ...item, arguments: '{"prompt":"x","extra":true}' } }],
|
|
359
|
+
[done, { ...done, item: { ...item, call_id: "second" } }],
|
|
360
|
+
[done, { type: "image", base64: "png", mimeType: "image/png" }],
|
|
361
|
+
[{ type: "output-item", item: { type: "image_generation_call" }, completed: false }],
|
|
362
|
+
];
|
|
363
|
+
for (const stream of badEvents) {
|
|
364
|
+
const run = vi.fn(() => events(...stream, { type: "completed", responseId: "driver" }));
|
|
365
|
+
const d = dispatcher(run);
|
|
366
|
+
await d.onTokenAdded(1, "one", credentials);
|
|
367
|
+
await expect(d.generate(request({ imageGeneration: "auto" }))).rejects.toMatchObject({ kind: "incomplete" });
|
|
368
|
+
expect(run).toHaveBeenCalledTimes(1);
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it("rejects structured output in auto mode before transport", async () => {
|
|
373
|
+
const run = vi.fn(() => events());
|
|
374
|
+
const d = dispatcher(run);
|
|
375
|
+
await d.onTokenAdded(1, "one", credentials);
|
|
376
|
+
await expect(d.generate(request({ imageGeneration: "auto", outputSchema: { type: "object" } }))).rejects.toMatchObject({ kind: "gate" });
|
|
377
|
+
expect(run).not.toHaveBeenCalled();
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it.each(["missing-image", "missing-completion", "cancel"])("rejects standalone %s", async (failure) => {
|
|
381
|
+
const controller = new AbortController();
|
|
382
|
+
let count = 0;
|
|
383
|
+
const d = dispatcher(async function* (_options, signal) {
|
|
384
|
+
count++;
|
|
385
|
+
if (count === 1) {
|
|
386
|
+
yield { type: "output-item", completed: true, item: { type: "function_call", call_id: "call", name: "generate_transparent_image", arguments: '{"prompt":"transparent"}' } };
|
|
387
|
+
yield { type: "completed", responseId: "driver" };
|
|
388
|
+
} else {
|
|
389
|
+
if (failure === "cancel") { controller.abort(); expect(signal?.aborted).toBe(true); throw signal!.reason; }
|
|
390
|
+
if (failure === "missing-image") yield { type: "completed", responseId: "image" };
|
|
391
|
+
else yield { type: "image", base64: "png", mimeType: "image/png" };
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
await d.onTokenAdded(1, "one", credentials);
|
|
395
|
+
await expect(d.generate(request({ imageGeneration: "auto", abortSignal: controller.signal }))).rejects.toMatchObject(failure === "cancel" ? { name: "AbortError" } : { kind: "incomplete" });
|
|
396
|
+
expect(count).toBe(2);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it("does not start standalone generation after cancellation", async () => {
|
|
400
|
+
const controller = new AbortController();
|
|
401
|
+
const run = vi.fn(async function* () {
|
|
402
|
+
yield { type: "output-item" as const, completed: true, item: { type: "function_call", call_id: "call", name: "generate_transparent_image", arguments: '{"prompt":"transparent"}' } };
|
|
403
|
+
yield { type: "completed" as const, responseId: "driver" };
|
|
404
|
+
controller.abort();
|
|
405
|
+
});
|
|
406
|
+
const d = dispatcher(run);
|
|
407
|
+
await d.onTokenAdded(1, "one", credentials);
|
|
408
|
+
await expect(d.generate(request({ imageGeneration: "auto", abortSignal: controller.signal }))).rejects.toMatchObject({ name: "AbortError" });
|
|
409
|
+
expect(run).toHaveBeenCalledTimes(1);
|
|
410
|
+
});
|
|
411
|
+
|
|
298
412
|
it("maps images and preserves image failure classifications", async () => {
|
|
299
413
|
const success = dispatcher(() =>
|
|
300
414
|
events(
|
|
@@ -584,6 +698,9 @@ describe("effort 어휘 해석 (Codex 카탈로그 기준)", () => {
|
|
|
584
698
|
|
|
585
699
|
it("모델이 지원하는 값은 그대로 보내고, 상한 초과·공개 API 어휘는 reasoning 에서 빼 백엔드 기본값을 쓴다", async () => {
|
|
586
700
|
await expect(mappedEffortFor("gpt-6-astra", "ultra")).resolves.toBe("ultra");
|
|
701
|
+
await expect(mappedEffortFor("gpt-6-sol", "ultra")).resolves.toBe("ultra");
|
|
702
|
+
await expect(mappedEffortFor("gpt-6-luna", "max")).resolves.toBe("max");
|
|
703
|
+
await expect(mappedEffortFor("gpt-6-luna", "ultra")).resolves.toBeUndefined();
|
|
587
704
|
await expect(mappedEffortFor("gpt-5.6-terra", "ultra")).resolves.toBe("ultra");
|
|
588
705
|
await expect(mappedEffortFor("gpt-5.5", "xhigh")).resolves.toBe("xhigh");
|
|
589
706
|
await expect(mappedEffortFor("gpt-5.5", "max")).resolves.toBeUndefined();
|
|
@@ -26,6 +26,7 @@ const logger = getLogger(["qgrid", "openai-dispatcher"]);
|
|
|
26
26
|
// 교체된 codex worker 가 강제하던 watchdog. 호출자가 timeoutMs 를 생략해도 요청이
|
|
27
27
|
// 영원히 매달리지 않도록 direct transport 도 유한한 기본 상한을 유지한다.
|
|
28
28
|
const DEFAULT_REQUEST_TIMEOUT_MS = 600_000;
|
|
29
|
+
const TRANSPARENT_IMAGE_TOOL = "generate_transparent_image";
|
|
29
30
|
|
|
30
31
|
export type ImageFailureKind = "gate" | "not_called" | "incomplete";
|
|
31
32
|
|
|
@@ -113,9 +114,48 @@ function asReasoning(req: GenerateRequest): OpenAIResponsesOptions["reasoning"]
|
|
|
113
114
|
|
|
114
115
|
function requestOptions(req: GenerateRequest): OpenAIResponsesOptions {
|
|
115
116
|
const { outputFormat, ...imageControls } = req.imageGenerationOptions ?? {};
|
|
117
|
+
const auto = req.imageGeneration === "auto";
|
|
118
|
+
if (auto && req.outputSchema) {
|
|
119
|
+
throw new ImageGenerationError(
|
|
120
|
+
"gate",
|
|
121
|
+
"Automatic image generation does not support structured output or external tools",
|
|
122
|
+
);
|
|
123
|
+
}
|
|
116
124
|
return {
|
|
117
125
|
model: req.model ?? "",
|
|
118
126
|
...(req.systemPrompt ? { instructions: req.systemPrompt } : {}),
|
|
127
|
+
...(auto
|
|
128
|
+
? {
|
|
129
|
+
instructions: [
|
|
130
|
+
req.systemPrompt,
|
|
131
|
+
"Use image_generation for ordinary image generation or editing. Use generate_transparent_image only when a transparent background is requested. Text replies are allowed when no image is needed. Use only one kind of image tool per response, and at most one generate_transparent_image call.",
|
|
132
|
+
]
|
|
133
|
+
.filter(Boolean)
|
|
134
|
+
.join("\n\n"),
|
|
135
|
+
parallelToolCalls: false,
|
|
136
|
+
tools: [
|
|
137
|
+
{
|
|
138
|
+
type: "function",
|
|
139
|
+
name: TRANSPARENT_IMAGE_TOOL,
|
|
140
|
+
description:
|
|
141
|
+
"Generate or edit an image with a transparent background using the conversation's reference images.",
|
|
142
|
+
strict: true,
|
|
143
|
+
parameters: {
|
|
144
|
+
type: "object",
|
|
145
|
+
properties: {
|
|
146
|
+
prompt: {
|
|
147
|
+
type: "string",
|
|
148
|
+
description:
|
|
149
|
+
"Complete instructions for the transparent image generation or edit.",
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
required: ["prompt"],
|
|
153
|
+
additionalProperties: false,
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
}
|
|
158
|
+
: {}),
|
|
119
159
|
history: inputItems(req),
|
|
120
160
|
...(asReasoning(req) ? { reasoning: asReasoning(req) } : {}),
|
|
121
161
|
...(req.verbosity ? { verbosity: req.verbosity as OpenAIResponsesOptions["verbosity"] } : {}),
|
|
@@ -129,6 +169,7 @@ function requestOptions(req: GenerateRequest): OpenAIResponsesOptions {
|
|
|
129
169
|
imageGeneration: req.imageGenerationOptions
|
|
130
170
|
? {
|
|
131
171
|
...imageControls,
|
|
172
|
+
...(auto ? { background: "auto" } : {}),
|
|
132
173
|
...(outputFormat ? { output_format: outputFormat } : {}),
|
|
133
174
|
}
|
|
134
175
|
: true,
|
|
@@ -312,6 +353,11 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
312
353
|
};
|
|
313
354
|
const images: GeneratedImage[] = [];
|
|
314
355
|
let imageAttempted = false;
|
|
356
|
+
const auto = req.imageGeneration === "auto";
|
|
357
|
+
const transparentCalls = new Map<string, string>();
|
|
358
|
+
const transparentCallIds = new Set<unknown>();
|
|
359
|
+
let transparentAttempted = false;
|
|
360
|
+
let driverCompleted = false;
|
|
315
361
|
let servingModel = req.model ?? "";
|
|
316
362
|
const metadata = selection.metadata;
|
|
317
363
|
const clientEntry = this.acquireClient(selection);
|
|
@@ -334,7 +380,42 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
334
380
|
});
|
|
335
381
|
} else if (event.type === "output-item" && event.item.type === "image_generation_call") {
|
|
336
382
|
imageAttempted = true;
|
|
383
|
+
} else if (auto && event.type === "output-item" && event.item.type === "function_call") {
|
|
384
|
+
if (event.item.name !== TRANSPARENT_IMAGE_TOOL) {
|
|
385
|
+
throw new ImageGenerationError("incomplete", "Unexpected automatic image tool call");
|
|
386
|
+
}
|
|
387
|
+
transparentAttempted = true;
|
|
388
|
+
transparentCallIds.add(event.item.call_id);
|
|
389
|
+
if (!event.completed) continue;
|
|
390
|
+
const id = event.item.call_id;
|
|
391
|
+
let args: unknown;
|
|
392
|
+
try {
|
|
393
|
+
args = JSON.parse(String(event.item.arguments));
|
|
394
|
+
} catch {
|
|
395
|
+
throw new ImageGenerationError("incomplete", "Invalid transparent image arguments");
|
|
396
|
+
}
|
|
397
|
+
if (
|
|
398
|
+
typeof id !== "string" ||
|
|
399
|
+
!id ||
|
|
400
|
+
!args ||
|
|
401
|
+
typeof args !== "object" ||
|
|
402
|
+
Array.isArray(args) ||
|
|
403
|
+
Object.keys(args).length !== 1 ||
|
|
404
|
+
!("prompt" in args) ||
|
|
405
|
+
typeof args.prompt !== "string" ||
|
|
406
|
+
!args.prompt.trim()
|
|
407
|
+
) {
|
|
408
|
+
throw new ImageGenerationError("incomplete", "Invalid transparent image arguments");
|
|
409
|
+
}
|
|
410
|
+
if (transparentCalls.has(id) && transparentCalls.get(id) !== args.prompt) {
|
|
411
|
+
throw new ImageGenerationError(
|
|
412
|
+
"incomplete",
|
|
413
|
+
"Conflicting transparent image tool calls",
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
transparentCalls.set(id, args.prompt);
|
|
337
417
|
} else if (event.type === "completed" && event.usage) {
|
|
418
|
+
driverCompleted = true;
|
|
338
419
|
servingModel = event.model ?? servingModel;
|
|
339
420
|
usage = {
|
|
340
421
|
totalTokens: event.usage.totalTokens,
|
|
@@ -344,6 +425,7 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
344
425
|
reasoningOutputTokens: event.usage.reasoningTokens,
|
|
345
426
|
};
|
|
346
427
|
} else if (event.type === "completed") {
|
|
428
|
+
driverCompleted = true;
|
|
347
429
|
servingModel = event.model ?? servingModel;
|
|
348
430
|
} else if (event.type === "error") {
|
|
349
431
|
if (req.imageGeneration)
|
|
@@ -351,10 +433,63 @@ export class OpenAIDispatcher implements ProviderDispatcher {
|
|
|
351
433
|
throw event.error;
|
|
352
434
|
}
|
|
353
435
|
}
|
|
436
|
+
if (auto && transparentAttempted) {
|
|
437
|
+
if (
|
|
438
|
+
!driverCompleted ||
|
|
439
|
+
transparentCalls.size !== 1 ||
|
|
440
|
+
transparentCallIds.size !== 1 ||
|
|
441
|
+
imageAttempted
|
|
442
|
+
) {
|
|
443
|
+
throw new ImageGenerationError(
|
|
444
|
+
"incomplete",
|
|
445
|
+
"Expected one completed transparent image call without another image tool",
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
if (req.abortSignal?.aborted) throw abortError(req.abortSignal);
|
|
449
|
+
const prompt = [...transparentCalls.values()][0]!;
|
|
450
|
+
let imageCompleted = false;
|
|
451
|
+
for await (const event of clientEntry.client.responses(
|
|
452
|
+
{
|
|
453
|
+
model: req.model ?? "",
|
|
454
|
+
...(req.systemPrompt ? { instructions: req.systemPrompt } : {}),
|
|
455
|
+
history: [
|
|
456
|
+
...inputItems(req),
|
|
457
|
+
{ type: "message", role: "user", content: [{ type: "input_text", text: prompt }] },
|
|
458
|
+
],
|
|
459
|
+
imageGeneration: {
|
|
460
|
+
quality: req.imageGenerationOptions?.quality,
|
|
461
|
+
size: req.imageGenerationOptions?.size,
|
|
462
|
+
output_format: "png",
|
|
463
|
+
background: "transparent",
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
req.abortSignal,
|
|
467
|
+
)) {
|
|
468
|
+
if (event.type === "image")
|
|
469
|
+
images.push({
|
|
470
|
+
data: event.base64,
|
|
471
|
+
mediaType: event.mimeType,
|
|
472
|
+
revisedPrompt: event.revisedPrompt ?? null,
|
|
473
|
+
...(event.generation ? { generation: event.generation } : {}),
|
|
474
|
+
});
|
|
475
|
+
else if (event.type === "completed") imageCompleted = true;
|
|
476
|
+
else if (event.type === "error")
|
|
477
|
+
throw new ImageGenerationError("incomplete", event.error.message);
|
|
478
|
+
}
|
|
479
|
+
imageAttempted = true;
|
|
480
|
+
if (!imageCompleted)
|
|
481
|
+
throw new ImageGenerationError(
|
|
482
|
+
"incomplete",
|
|
483
|
+
"Transparent image generation did not complete",
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
if (auto && imageAttempted && !driverCompleted)
|
|
487
|
+
throw new ImageGenerationError("incomplete", "Image generation response did not complete");
|
|
488
|
+
if (req.abortSignal?.aborted) throw abortError(req.abortSignal);
|
|
354
489
|
} finally {
|
|
355
490
|
this.releaseClient(clientEntry);
|
|
356
491
|
}
|
|
357
|
-
if (req.imageGeneration && images.length === 0) {
|
|
492
|
+
if ((req.imageGeneration === true || (auto && imageAttempted)) && images.length === 0) {
|
|
358
493
|
throw new ImageGenerationError(
|
|
359
494
|
imageAttempted ? "incomplete" : "not_called",
|
|
360
495
|
imageAttempted
|