ima2-gen 1.1.13 → 1.1.14
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/README.md +10 -1
- package/bin/commands/doctor.js +195 -0
- package/bin/commands/doctor.ts +202 -0
- package/bin/ima2.js +3 -105
- package/bin/ima2.ts +3 -109
- package/config.js +1 -0
- package/config.ts +5 -0
- package/docs/CLI.md +36 -0
- package/docs/FAQ.ko.md +82 -2
- package/docs/FAQ.md +85 -2
- package/docs/PROMPT_STUDIO.ko.md +111 -0
- package/docs/PROMPT_STUDIO.md +115 -0
- package/docs/README.ko.md +8 -1
- package/docs/migration/runtime-test-inventory.md +6 -1
- package/lib/agentRuntime.js +9 -2
- package/lib/agentRuntime.ts +8 -2
- package/lib/errorClassify.js +1 -1
- package/lib/errorClassify.ts +1 -1
- package/lib/generationErrors.js +121 -23
- package/lib/generationErrors.ts +100 -13
- package/lib/responsesDoctor.js +386 -0
- package/lib/responsesDoctor.ts +456 -0
- package/lib/responsesErrors.js +57 -0
- package/lib/responsesErrors.ts +83 -0
- package/lib/responsesFallback.js +72 -0
- package/lib/responsesFallback.ts +114 -0
- package/lib/responsesImageAdapter.js +121 -174
- package/lib/responsesImageAdapter.ts +136 -211
- package/lib/responsesParse.js +324 -0
- package/lib/responsesParse.ts +452 -0
- package/lib/responsesTools.js +15 -0
- package/lib/responsesTools.ts +28 -0
- package/package.json +1 -1
- package/routes/edit.js +26 -1
- package/routes/edit.ts +26 -1
- package/routes/generate.js +40 -0
- package/routes/generate.ts +47 -0
- package/ui/dist/.vite/manifest.json +12 -12
- package/ui/dist/assets/{AgentWorkspace-BJe9yxPA.js → AgentWorkspace-B6YNOZHi.js} +1 -1
- package/ui/dist/assets/{CardNewsWorkspace-BBLdwzYU.js → CardNewsWorkspace-EFVeg4l_.js} +1 -1
- package/ui/dist/assets/{NodeCanvas-BSZ527J4.js → NodeCanvas-iM6yjHvO.js} +1 -1
- package/ui/dist/assets/{PromptBuilderPanel-Y2VygFc0.js → PromptBuilderPanel-C3GdLDCl.js} +1 -1
- package/ui/dist/assets/{PromptImportDialog-C6lFV-LL.js → PromptImportDialog-DS9vrc_w.js} +2 -2
- package/ui/dist/assets/{PromptImportDiscoverySection-D8YJFhND.js → PromptImportDiscoverySection-DHFEt_FA.js} +1 -1
- package/ui/dist/assets/{PromptImportFolderSection-ywfcQolW.js → PromptImportFolderSection-BQxb1zs5.js} +1 -1
- package/ui/dist/assets/{PromptLibraryPanel-fk4KmrGy.js → PromptLibraryPanel-NhMKVGfU.js} +2 -2
- package/ui/dist/assets/{SettingsWorkspace-DL5vhAHQ.js → SettingsWorkspace-FjKjaDqj.js} +1 -1
- package/ui/dist/assets/index-BAN6lKgf.js +28 -0
- package/ui/dist/assets/{index-BLx55BOg.js → index-BbFZyM92.js} +1 -1
- package/ui/dist/assets/index-DK1faG9Z.css +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/index-ByViUJfx.css +0 -1
- package/ui/dist/assets/index-Ci36vcFD.js +0 -28
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Prompt Studio Manual
|
|
2
|
+
|
|
3
|
+
Prompt Studio is the Classic workspace profile for repeated image iteration. It
|
|
4
|
+
keeps the image viewer in the center, recent generations on the side, and the
|
|
5
|
+
composer plus generation controls close to the current image.
|
|
6
|
+
|
|
7
|
+
Use this page when you are not sure what a Prompt Studio control does or when
|
|
8
|
+
you want a reproducible way to report a workspace issue.
|
|
9
|
+
|
|
10
|
+
## Feature Map
|
|
11
|
+
|
|
12
|
+
| Area | What it does | Notes |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| Composer | Holds the prompt for the next request. | Selecting an existing image is view-only. It should not overwrite the composer. |
|
|
15
|
+
| Multimode | Starts several separate image requests from the current prompt. | Each slot is a candidate output, not a collage panel or a guaranteed scene sequence. |
|
|
16
|
+
| 1:1 Direct | Sends the prompt through with less rewriting by the app. | Use it for exact wording, strict prompt experiments, or provider-side prompt syntax. |
|
|
17
|
+
| Model quick menu | Changes the image model and reasoning effort from the sidebar header. | The full Settings workspace remains the detailed configuration page. |
|
|
18
|
+
| Recent generations | Shows the visible Prompt Studio history domain. | Arrow keys move inside the same visible recent domain instead of hidden older rows. |
|
|
19
|
+
| Gallery | Browses saved local images, All/Favorites tabs, and folders. | Favorite toggles should preserve the gallery viewport you were browsing. |
|
|
20
|
+
| Prompt library | Imports saved prompt text into the composer intentionally. | Library insert/continue actions are explicit prompt imports; passive image selection is not. |
|
|
21
|
+
|
|
22
|
+
## Multimode Prompting
|
|
23
|
+
|
|
24
|
+
Multimode repeats one generation request shape across several candidate slots.
|
|
25
|
+
It is useful when you want alternatives, not when you need one combined
|
|
26
|
+
multi-panel image.
|
|
27
|
+
|
|
28
|
+
For related candidates, put the shared subject first and the variation rule
|
|
29
|
+
second:
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
Same character design in every image: a silver-haired courier in a red raincoat.
|
|
33
|
+
Make 4 alternatives that vary only camera angle, lighting, and background street.
|
|
34
|
+
Keep face, outfit, age, and color palette consistent.
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
For unrelated candidates, say so directly:
|
|
38
|
+
|
|
39
|
+
```text
|
|
40
|
+
Create 4 unrelated sticker ideas for a local image generation app.
|
|
41
|
+
Each image should use a different mascot, color palette, and composition.
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
If you need a true two-panel comic, contact sheet, before/after comparison, or
|
|
45
|
+
collage, ask for that in a single normal image request instead of relying on
|
|
46
|
+
multimode slots:
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
Create one 2-panel comparison image. Left panel: rough sketch UI. Right panel:
|
|
50
|
+
polished Prompt Studio UI. Add small labels inside each panel.
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Direct Mode
|
|
54
|
+
|
|
55
|
+
Use **1:1 Direct** when the exact prompt text matters. It is helpful for:
|
|
56
|
+
|
|
57
|
+
- comparing prompt wording changes,
|
|
58
|
+
- preserving a structured prompt template,
|
|
59
|
+
- using provider-specific instruction style,
|
|
60
|
+
- avoiding app-side phrasing changes during troubleshooting.
|
|
61
|
+
|
|
62
|
+
Direct mode can be used together with Multimode. In that case, each multimode
|
|
63
|
+
slot receives the same direct prompt request shape.
|
|
64
|
+
|
|
65
|
+
## Reasoning Effort
|
|
66
|
+
|
|
67
|
+
Reasoning effort controls how much planning the selected model may spend before
|
|
68
|
+
or during generation. Start with the default for everyday work. Raise it when
|
|
69
|
+
the prompt has many constraints, references, or composition requirements.
|
|
70
|
+
|
|
71
|
+
The sidebar model label opens quick settings for both model and reasoning. The
|
|
72
|
+
Settings workspace still shows the full model configuration and explanatory
|
|
73
|
+
copy.
|
|
74
|
+
|
|
75
|
+
## Gallery And Prompt Safety
|
|
76
|
+
|
|
77
|
+
Prompt Studio separates browsing from composing:
|
|
78
|
+
|
|
79
|
+
- Passive image selection is view-only.
|
|
80
|
+
- Clicking a history or gallery image focuses the image for viewing.
|
|
81
|
+
- Favorite on/off changes the saved image metadata and should not jump your
|
|
82
|
+
gallery browsing position.
|
|
83
|
+
- The All and Favorites tabs are browsing filters. Switching between them does
|
|
84
|
+
not intentionally import a prompt.
|
|
85
|
+
- Prompt Library insert, "continue from this image", and explicit reuse actions
|
|
86
|
+
are the actions that intentionally change the composer.
|
|
87
|
+
|
|
88
|
+
Before generating, glance at the composer if you were using explicit prompt
|
|
89
|
+
import actions. Passive image selection should leave your draft alone.
|
|
90
|
+
|
|
91
|
+
## Issue #75 Closeout Notes
|
|
92
|
+
|
|
93
|
+
The v1.1.13 Prompt Studio fixes tightened these user-visible contracts:
|
|
94
|
+
|
|
95
|
+
- concurrent multimode completions keep their slot/request identity,
|
|
96
|
+
- keyboard navigation follows the visible recent history domain,
|
|
97
|
+
- the gallery button remains reachable beside recent history,
|
|
98
|
+
- long prompts no longer starve the default image viewer,
|
|
99
|
+
- Direct and Multimode states can be seen at the same time,
|
|
100
|
+
- gallery favorite toggles and tab changes preserve the browsing viewport,
|
|
101
|
+
- passive image selection does not refill the composer.
|
|
102
|
+
|
|
103
|
+
## Reporting A Prompt Studio Problem
|
|
104
|
+
|
|
105
|
+
When opening an issue, include:
|
|
106
|
+
|
|
107
|
+
- `ima2-gen` version and operating system,
|
|
108
|
+
- browser and viewport size if layout is involved,
|
|
109
|
+
- workspace profile, mode toggles, model, reasoning effort, and image count,
|
|
110
|
+
- numbered steps from a fresh `ima2 serve` session,
|
|
111
|
+
- whether the problem happens in All, Favorites, or recent history,
|
|
112
|
+
- safe screenshots or screen recordings if they do not reveal private prompts.
|
|
113
|
+
|
|
114
|
+
Do not share ChatGPT cookies, OAuth token files, API keys, raw upstream
|
|
115
|
+
responses, private prompt history, or generated base64 data.
|
package/docs/README.ko.md
CHANGED
|
@@ -82,6 +82,10 @@ API 키가 env/config에 있으면 생성 엔드포인트에서 `provider: "api"
|
|
|
82
82
|
4. 한 장을 만들거나, multimode를 켜서 같은 프롬프트에서 여러 후보 슬롯을 만듭니다.
|
|
83
83
|
5. 생성 후 복사, 다운로드, 이어서 작업, Canvas Mode 정리를 선택합니다.
|
|
84
84
|
|
|
85
|
+
Prompt Studio의 각 컨트롤, 멀티모드 작성법, 1:1 Direct, 추론 강도, 갤러리
|
|
86
|
+
즐겨찾기 동작은 [Prompt Studio 사용 설명서](PROMPT_STUDIO.ko.md)에 정리되어
|
|
87
|
+
있습니다.
|
|
88
|
+
|
|
85
89
|

|
|
86
90
|
|
|
87
91
|
### Node mode
|
|
@@ -186,7 +190,10 @@ environment variables > ~/.ima2/config.json > built-in defaults
|
|
|
186
190
|
|
|
187
191
|
엔드포인트 목록은 [API Reference](API.md)로 분리했습니다.
|
|
188
192
|
|
|
189
|
-
자주 묻는 질문은 [FAQ](FAQ.ko.md)에 정리했습니다.
|
|
193
|
+
자주 묻는 질문은 [FAQ](FAQ.ko.md)에 정리했습니다. Prompt Studio 기능은
|
|
194
|
+
[Prompt Studio 사용 설명서](PROMPT_STUDIO.ko.md)를 확인하세요. 업데이트 후
|
|
195
|
+
예전 이미지가 안 보이면 [예전 이미지 복구 안내](RECOVER_OLD_IMAGES.md)를
|
|
196
|
+
먼저 확인하세요.
|
|
190
197
|
|
|
191
198
|
## 문제 해결
|
|
192
199
|
|
|
@@ -4,7 +4,7 @@ Generated by `npm run test:inventory` (script: `scripts/classify-tests.mjs`).
|
|
|
4
4
|
|
|
5
5
|
_Tests considered "runtime-importing" if they import from `../lib/`, `../routes/`, `../bin/`, `../server`, or `../config`._
|
|
6
6
|
|
|
7
|
-
Total:
|
|
7
|
+
Total: 161 (runtime: 49, contract: 112)
|
|
8
8
|
|
|
9
9
|
## Runtime-importing tests
|
|
10
10
|
- `tests/agent-mode-auto-planner-contract.test.ts`
|
|
@@ -47,6 +47,9 @@ Total: 156 (runtime: 46, contract: 110)
|
|
|
47
47
|
- `tests/reference-image-compress.test.ts`
|
|
48
48
|
- `tests/refs-size.test.ts`
|
|
49
49
|
- `tests/request-logging.test.ts`
|
|
50
|
+
- `tests/responses-adapter-safety.test.ts`
|
|
51
|
+
- `tests/responses-empty-taxonomy.test.ts`
|
|
52
|
+
- `tests/responses-parse-diagnostics.test.ts`
|
|
50
53
|
- `tests/runtime-context-normalize.test.ts`
|
|
51
54
|
- `tests/runtime-ports.test.ts`
|
|
52
55
|
- `tests/serve-ui-build-contract.test.ts`
|
|
@@ -123,6 +126,7 @@ Total: 156 (runtime: 46, contract: 110)
|
|
|
123
126
|
- `tests/inflight-list-tooltip-contract.test.js`
|
|
124
127
|
- `tests/inflight-reload-race.test.js`
|
|
125
128
|
- `tests/inflight-reload-reconcile-contract.test.js`
|
|
129
|
+
- `tests/issue75-prompt-studio-state-contract.test.js`
|
|
126
130
|
- `tests/mobile-generate-entry-contract.test.js`
|
|
127
131
|
- `tests/multimode-backend-contract.test.js`
|
|
128
132
|
- `tests/multimode-concurrent-store-contract.test.js`
|
|
@@ -150,6 +154,7 @@ Total: 156 (runtime: 46, contract: 110)
|
|
|
150
154
|
- `tests/prompt-import-folder-ui-contract.test.js`
|
|
151
155
|
- `tests/prompt-import-search-ux-contract.test.js`
|
|
152
156
|
- `tests/prompt-library-ui-contract.test.js`
|
|
157
|
+
- `tests/prompt-studio-docs-contract.test.js`
|
|
153
158
|
- `tests/prompt-studio-ui-contract.test.js`
|
|
154
159
|
- `tests/server-fallback-contract.test.js`
|
|
155
160
|
- `tests/server.test.js`
|
package/lib/agentRuntime.js
CHANGED
|
@@ -187,7 +187,9 @@ async function generateAgentImageWithRetry(ctx, sessionId, prompt, manifest, web
|
|
|
187
187
|
}
|
|
188
188
|
catch (error) {
|
|
189
189
|
lastError = error;
|
|
190
|
-
if (!isTextOnlyResult(error)
|
|
190
|
+
if (!isTextOnlyResult(error))
|
|
191
|
+
throw error;
|
|
192
|
+
if (attempt === 1)
|
|
191
193
|
break;
|
|
192
194
|
appendAgentTurn({
|
|
193
195
|
sessionId,
|
|
@@ -264,7 +266,12 @@ function forceImagePrompt(prompt) {
|
|
|
264
266
|
}
|
|
265
267
|
function isTextOnlyResult(error) {
|
|
266
268
|
const err = errInfo(error);
|
|
267
|
-
return
|
|
269
|
+
return [
|
|
270
|
+
"EMPTY_RESPONSE",
|
|
271
|
+
"IMAGE_TOOL_NOT_CALLED",
|
|
272
|
+
"WEB_SEARCH_ONLY_RESPONSE",
|
|
273
|
+
"IMAGE_TOOL_COMPLETED_WITHOUT_RESULT",
|
|
274
|
+
].includes(err.code || "") || err.message.includes("No image data");
|
|
268
275
|
}
|
|
269
276
|
function textOnlyError(cause) {
|
|
270
277
|
const err = new Error("Agent result did not include an image artifact.");
|
package/lib/agentRuntime.ts
CHANGED
|
@@ -252,7 +252,8 @@ async function generateAgentImageWithRetry(
|
|
|
252
252
|
if (result.image) return result;
|
|
253
253
|
} catch (error) {
|
|
254
254
|
lastError = error;
|
|
255
|
-
if (!isTextOnlyResult(error)
|
|
255
|
+
if (!isTextOnlyResult(error)) throw error;
|
|
256
|
+
if (attempt === 1) break;
|
|
256
257
|
appendAgentTurn({
|
|
257
258
|
sessionId,
|
|
258
259
|
role: "tool",
|
|
@@ -357,7 +358,12 @@ function forceImagePrompt(prompt: string) {
|
|
|
357
358
|
|
|
358
359
|
function isTextOnlyResult(error: unknown) {
|
|
359
360
|
const err = errInfo(error);
|
|
360
|
-
return
|
|
361
|
+
return [
|
|
362
|
+
"EMPTY_RESPONSE",
|
|
363
|
+
"IMAGE_TOOL_NOT_CALLED",
|
|
364
|
+
"WEB_SEARCH_ONLY_RESPONSE",
|
|
365
|
+
"IMAGE_TOOL_COMPLETED_WITHOUT_RESULT",
|
|
366
|
+
].includes(err.code || "") || err.message.includes("No image data");
|
|
361
367
|
}
|
|
362
368
|
|
|
363
369
|
function textOnlyError(cause: unknown) {
|
package/lib/errorClassify.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// 0.09.8 — upstream error classifier.
|
|
2
2
|
// Pattern-match upstream OpenAI / OAuth / network errors into stable ImaErrorCode
|
|
3
3
|
// values so the UI can surface localized, actionable messages with CTAs.
|
|
4
|
-
/** @typedef {"REF_TOO_LARGE"|"REF_NOT_BASE64"|"REF_EMPTY"|"REF_TOO_MANY"|"MODERATION_REFUSED"|"UPSTREAM_5XX"|"AUTH_CHATGPT_EXPIRED"|"AUTH_API_KEY_INVALID"|"NETWORK_FAILED"|"OAUTH_UNAVAILABLE"|"INVALID_REQUEST"|"INVALID_MODERATION"|"APIKEY_DISABLED"|"SAFETY_REFUSAL"|"EMPTY_RESPONSE"|"OAUTH_UPSTREAM_ERROR"|"DB_ERROR"|"UNKNOWN"} ImaErrorCode */
|
|
4
|
+
/** @typedef {"REF_TOO_LARGE"|"REF_NOT_BASE64"|"REF_EMPTY"|"REF_TOO_MANY"|"MODERATION_REFUSED"|"UPSTREAM_5XX"|"AUTH_CHATGPT_EXPIRED"|"AUTH_API_KEY_INVALID"|"NETWORK_FAILED"|"OAUTH_UNAVAILABLE"|"INVALID_REQUEST"|"INVALID_MODERATION"|"APIKEY_DISABLED"|"SAFETY_REFUSAL"|"EMPTY_RESPONSE"|"STREAM_PARSE_FAILED"|"IMAGE_TOOL_NOT_CALLED"|"WEB_SEARCH_ONLY_RESPONSE"|"IMAGE_TOOL_FAILED"|"IMAGE_TOOL_COMPLETED_WITHOUT_RESULT"|"OAUTH_IMAGE_CAPABILITY_UNAVAILABLE"|"RESPONSES_STREAM_ERROR"|"OAUTH_UPSTREAM_ERROR"|"DB_ERROR"|"UNKNOWN"} ImaErrorCode */
|
|
5
5
|
const INVALID_REQUEST_CODES = new Set([
|
|
6
6
|
"bad_request",
|
|
7
7
|
"invalid_request",
|
package/lib/errorClassify.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Pattern-match upstream OpenAI / OAuth / network errors into stable ImaErrorCode
|
|
3
3
|
// values so the UI can surface localized, actionable messages with CTAs.
|
|
4
4
|
|
|
5
|
-
/** @typedef {"REF_TOO_LARGE"|"REF_NOT_BASE64"|"REF_EMPTY"|"REF_TOO_MANY"|"MODERATION_REFUSED"|"UPSTREAM_5XX"|"AUTH_CHATGPT_EXPIRED"|"AUTH_API_KEY_INVALID"|"NETWORK_FAILED"|"OAUTH_UNAVAILABLE"|"INVALID_REQUEST"|"INVALID_MODERATION"|"APIKEY_DISABLED"|"SAFETY_REFUSAL"|"EMPTY_RESPONSE"|"OAUTH_UPSTREAM_ERROR"|"DB_ERROR"|"UNKNOWN"} ImaErrorCode */
|
|
5
|
+
/** @typedef {"REF_TOO_LARGE"|"REF_NOT_BASE64"|"REF_EMPTY"|"REF_TOO_MANY"|"MODERATION_REFUSED"|"UPSTREAM_5XX"|"AUTH_CHATGPT_EXPIRED"|"AUTH_API_KEY_INVALID"|"NETWORK_FAILED"|"OAUTH_UNAVAILABLE"|"INVALID_REQUEST"|"INVALID_MODERATION"|"APIKEY_DISABLED"|"SAFETY_REFUSAL"|"EMPTY_RESPONSE"|"STREAM_PARSE_FAILED"|"IMAGE_TOOL_NOT_CALLED"|"WEB_SEARCH_ONLY_RESPONSE"|"IMAGE_TOOL_FAILED"|"IMAGE_TOOL_COMPLETED_WITHOUT_RESULT"|"OAUTH_IMAGE_CAPABILITY_UNAVAILABLE"|"RESPONSES_STREAM_ERROR"|"OAUTH_UPSTREAM_ERROR"|"DB_ERROR"|"UNKNOWN"} ImaErrorCode */
|
|
6
6
|
|
|
7
7
|
const INVALID_REQUEST_CODES = new Set([
|
|
8
8
|
"bad_request",
|
package/lib/generationErrors.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { classifyUpstreamError, classifyUpstreamErrorCode } from "./errorClassify.js";
|
|
2
|
+
import { safeDiagnosticLabel } from "./responsesParse.js";
|
|
3
|
+
import { RESPONSE_DIAGNOSTIC_CODES } from "./responsesErrors.js";
|
|
2
4
|
const PASSTHROUGH_CODES = new Set([
|
|
3
5
|
"OAUTH_UNAVAILABLE",
|
|
4
6
|
"NETWORK_FAILED",
|
|
@@ -22,13 +24,50 @@ function diagnosticReasonFrom(err) {
|
|
|
22
24
|
return err.diagnosticReason;
|
|
23
25
|
if (Number(err?.referenceMismatchCount) > 0)
|
|
24
26
|
return "reference_mime_mismatch_candidate";
|
|
27
|
+
const responseDiagnosticReason = responseDiagnosticReasonFrom(err);
|
|
28
|
+
if (responseDiagnosticReason)
|
|
29
|
+
return responseDiagnosticReason;
|
|
25
30
|
if (has4kSize(err?.size))
|
|
26
31
|
return "experimental_4k_empty_response";
|
|
27
32
|
return null;
|
|
28
33
|
}
|
|
34
|
+
function responseDiagnosticReasonFrom(err) {
|
|
35
|
+
if (!err?.responseDiagnostics || typeof err.responseDiagnostics !== "object")
|
|
36
|
+
return null;
|
|
37
|
+
const diagnostics = err.responseDiagnostics;
|
|
38
|
+
const bytesRead = Number(diagnostics.streamStats?.bytesRead);
|
|
39
|
+
if (Number.isFinite(bytesRead) && bytesRead > 0 && Number(err.eventCount) === 0)
|
|
40
|
+
return "stream_parse_failed";
|
|
41
|
+
if (diagnostics.imageCallFailed === true)
|
|
42
|
+
return "image_tool_failed";
|
|
43
|
+
if (diagnostics.imageCallCompleted === true && Number(diagnostics.imageResultCount) === 0)
|
|
44
|
+
return "image_tool_completed_without_result";
|
|
45
|
+
if (diagnostics.imageCallSeen !== true && Number(err.webSearchCalls) > 0)
|
|
46
|
+
return "web_search_only_response";
|
|
47
|
+
if (diagnostics.imageCallSeen !== true && diagnostics.messageOutputSeen === true)
|
|
48
|
+
return "image_tool_not_called";
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
function responseDiagnosticCodeFrom(err) {
|
|
52
|
+
const reason = responseDiagnosticReasonFrom(err);
|
|
53
|
+
if (reason === "stream_parse_failed")
|
|
54
|
+
return "STREAM_PARSE_FAILED";
|
|
55
|
+
if (reason === "web_search_only_response")
|
|
56
|
+
return "WEB_SEARCH_ONLY_RESPONSE";
|
|
57
|
+
if (reason === "image_tool_not_called")
|
|
58
|
+
return "IMAGE_TOOL_NOT_CALLED";
|
|
59
|
+
if (reason === "image_tool_failed")
|
|
60
|
+
return "IMAGE_TOOL_FAILED";
|
|
61
|
+
if (reason === "image_tool_completed_without_result")
|
|
62
|
+
return "IMAGE_TOOL_COMPLETED_WITHOUT_RESULT";
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
29
65
|
export function errorCodeFrom(err) {
|
|
30
66
|
if (!err)
|
|
31
67
|
return "UNKNOWN";
|
|
68
|
+
const appCode = err.code;
|
|
69
|
+
if (RESPONSE_DIAGNOSTIC_CODES.has(appCode) || appCode === "EMPTY_RESPONSE")
|
|
70
|
+
return appCode;
|
|
32
71
|
const upstreamCode = classifyUpstreamErrorCode(err.upstreamCode);
|
|
33
72
|
if (upstreamCode !== "UNKNOWN")
|
|
34
73
|
return upstreamCode;
|
|
@@ -36,14 +75,17 @@ export function errorCodeFrom(err) {
|
|
|
36
75
|
if (upstreamType !== "UNKNOWN")
|
|
37
76
|
return upstreamType;
|
|
38
77
|
// Known app-level codes pass through directly (before message heuristic)
|
|
39
|
-
if (PASSTHROUGH_CODES.has(
|
|
40
|
-
return
|
|
78
|
+
if (PASSTHROUGH_CODES.has(appCode) || SAFETY_CODES.has(appCode))
|
|
79
|
+
return appCode;
|
|
41
80
|
const rawCode = classifyUpstreamErrorCode(err.code);
|
|
42
81
|
if (rawCode !== "UNKNOWN")
|
|
43
82
|
return rawCode;
|
|
44
83
|
const direct = classifyUpstreamError(err.message);
|
|
45
84
|
if (direct !== "UNKNOWN")
|
|
46
85
|
return direct;
|
|
86
|
+
const responseDiagnosticCode = responseDiagnosticCodeFrom(err);
|
|
87
|
+
if (responseDiagnosticCode)
|
|
88
|
+
return responseDiagnosticCode;
|
|
47
89
|
const status = Number(err.status);
|
|
48
90
|
if (Number.isFinite(status) && status >= 400 && status < 500 && !SAFETY_CODES.has(err.code)) {
|
|
49
91
|
return "INVALID_REQUEST";
|
|
@@ -70,14 +112,66 @@ export function statusForErrorCode(code, fallback = 500) {
|
|
|
70
112
|
return 401;
|
|
71
113
|
if (code === "UPSTREAM_5XX")
|
|
72
114
|
return 502;
|
|
115
|
+
if (code === "RESPONSES_STREAM_ERROR")
|
|
116
|
+
return 502;
|
|
73
117
|
if (code === "OAUTH_IMAGE_TIMEOUT")
|
|
74
118
|
return 504;
|
|
75
119
|
if (code === "INVALID_REQUEST")
|
|
76
120
|
return 400;
|
|
121
|
+
if (RESPONSE_DIAGNOSTIC_CODES.has(code))
|
|
122
|
+
return 422;
|
|
77
123
|
if (code === "SAFETY_REFUSAL" || code === "MODERATION_REFUSED" || code === "moderation_blocked")
|
|
78
124
|
return 422;
|
|
79
125
|
return fallback;
|
|
80
126
|
}
|
|
127
|
+
function copyEmptyResponseMetadata(target, source) {
|
|
128
|
+
if (!source)
|
|
129
|
+
return;
|
|
130
|
+
if (typeof source.eventCount === "number")
|
|
131
|
+
target.eventCount = source.eventCount;
|
|
132
|
+
if (source.eventTypes)
|
|
133
|
+
target.eventTypes = source.eventTypes;
|
|
134
|
+
if (typeof source.webSearchCalls === "number")
|
|
135
|
+
target.webSearchCalls = source.webSearchCalls;
|
|
136
|
+
if (source.responseDiagnostics)
|
|
137
|
+
target.responseDiagnostics = source.responseDiagnostics;
|
|
138
|
+
if (typeof source.webSearchEnabled === "boolean")
|
|
139
|
+
target.webSearchEnabled = source.webSearchEnabled;
|
|
140
|
+
if (Array.isArray(source.toolTypes))
|
|
141
|
+
target.toolTypes = source.toolTypes;
|
|
142
|
+
if (source.toolChoiceKind)
|
|
143
|
+
target.toolChoiceKind = source.toolChoiceKind;
|
|
144
|
+
if (typeof source.promptChars === "number")
|
|
145
|
+
target.promptChars = source.promptChars;
|
|
146
|
+
if (typeof source.refsCount === "number")
|
|
147
|
+
target.refsCount = source.refsCount;
|
|
148
|
+
if (typeof source.inputImageCount === "number")
|
|
149
|
+
target.inputImageCount = source.inputImageCount;
|
|
150
|
+
if (Array.isArray(source.referenceDiagnostics))
|
|
151
|
+
target.referenceDiagnostics = source.referenceDiagnostics;
|
|
152
|
+
if (typeof source.referenceMismatchCount === "number")
|
|
153
|
+
target.referenceMismatchCount = source.referenceMismatchCount;
|
|
154
|
+
if (source.retryKind)
|
|
155
|
+
target.retryKind = source.retryKind;
|
|
156
|
+
if (typeof source.initialEventCount === "number")
|
|
157
|
+
target.initialEventCount = source.initialEventCount;
|
|
158
|
+
if (source.initialEventTypes)
|
|
159
|
+
target.initialEventTypes = source.initialEventTypes;
|
|
160
|
+
if (typeof source.referencesDroppedOnRetry === "boolean")
|
|
161
|
+
target.referencesDroppedOnRetry = source.referencesDroppedOnRetry;
|
|
162
|
+
if (typeof source.developerPromptDroppedOnRetry === "boolean")
|
|
163
|
+
target.developerPromptDroppedOnRetry = source.developerPromptDroppedOnRetry;
|
|
164
|
+
if (typeof source.webSearchDroppedOnRetry === "boolean")
|
|
165
|
+
target.webSearchDroppedOnRetry = source.webSearchDroppedOnRetry;
|
|
166
|
+
if (typeof source.fallbackEventCount === "number")
|
|
167
|
+
target.fallbackEventCount = source.fallbackEventCount;
|
|
168
|
+
if (source.fallbackEventTypes)
|
|
169
|
+
target.fallbackEventTypes = source.fallbackEventTypes;
|
|
170
|
+
if (typeof source.fallbackImageCallSeen === "boolean")
|
|
171
|
+
target.fallbackImageCallSeen = source.fallbackImageCallSeen;
|
|
172
|
+
if (typeof source.fallbackImageResultCount === "number")
|
|
173
|
+
target.fallbackImageResultCount = source.fallbackImageResultCount;
|
|
174
|
+
}
|
|
81
175
|
export function normalizeGenerationFailure(lastErr, options = {}) {
|
|
82
176
|
const code = errorCodeFrom(lastErr);
|
|
83
177
|
if (PASSTHROUGH_CODES.has(code)) {
|
|
@@ -86,11 +180,11 @@ export function normalizeGenerationFailure(lastErr, options = {}) {
|
|
|
86
180
|
err.status = lastErr?.status || statusForErrorCode(code);
|
|
87
181
|
err.cause = lastErr;
|
|
88
182
|
if (lastErr?.upstreamCode)
|
|
89
|
-
err.upstreamCode = lastErr.upstreamCode;
|
|
183
|
+
err.upstreamCode = safeDiagnosticLabel(lastErr.upstreamCode);
|
|
90
184
|
if (lastErr?.upstreamType)
|
|
91
|
-
err.upstreamType = lastErr.upstreamType;
|
|
185
|
+
err.upstreamType = safeDiagnosticLabel(lastErr.upstreamType);
|
|
92
186
|
if (lastErr?.upstreamParam)
|
|
93
|
-
err.upstreamParam = lastErr.upstreamParam;
|
|
187
|
+
err.upstreamParam = safeDiagnosticLabel(lastErr.upstreamParam);
|
|
94
188
|
if (lastErr?.eventType)
|
|
95
189
|
err.eventType = lastErr.eventType;
|
|
96
190
|
if (typeof lastErr?.eventCount === "number")
|
|
@@ -104,6 +198,23 @@ export function normalizeGenerationFailure(lastErr, options = {}) {
|
|
|
104
198
|
err.cause = lastErr;
|
|
105
199
|
return err;
|
|
106
200
|
}
|
|
201
|
+
if (RESPONSE_DIAGNOSTIC_CODES.has(code)) {
|
|
202
|
+
const err = new Error(lastErr?.message || "Image generation did not return image data");
|
|
203
|
+
err.code = code;
|
|
204
|
+
err.status = lastErr?.status || statusForErrorCode(code, 422);
|
|
205
|
+
err.cause = lastErr;
|
|
206
|
+
if (lastErr?.upstreamCode)
|
|
207
|
+
err.upstreamCode = safeDiagnosticLabel(lastErr.upstreamCode);
|
|
208
|
+
if (lastErr?.upstreamType)
|
|
209
|
+
err.upstreamType = safeDiagnosticLabel(lastErr.upstreamType);
|
|
210
|
+
if (lastErr?.upstreamParam)
|
|
211
|
+
err.upstreamParam = safeDiagnosticLabel(lastErr.upstreamParam);
|
|
212
|
+
if (lastErr?.eventType)
|
|
213
|
+
err.eventType = lastErr.eventType;
|
|
214
|
+
copyEmptyResponseMetadata(err, lastErr);
|
|
215
|
+
err.diagnosticReason = diagnosticReasonFrom(lastErr) || code.toLowerCase();
|
|
216
|
+
return err;
|
|
217
|
+
}
|
|
107
218
|
// Empty response with metadata → likely a technical limitation (unsupported size/quality/model)
|
|
108
219
|
if (typeof lastErr?.eventCount === "number") {
|
|
109
220
|
const meta = [];
|
|
@@ -126,24 +237,11 @@ export function normalizeGenerationFailure(lastErr, options = {}) {
|
|
|
126
237
|
err.quality = lastErr.quality;
|
|
127
238
|
if (lastErr.model)
|
|
128
239
|
err.model = lastErr.model;
|
|
129
|
-
if (
|
|
130
|
-
err.
|
|
131
|
-
if (lastErr.
|
|
132
|
-
err.
|
|
133
|
-
|
|
134
|
-
err.refsCount = lastErr.refsCount;
|
|
135
|
-
if (typeof lastErr.inputImageCount === "number")
|
|
136
|
-
err.inputImageCount = lastErr.inputImageCount;
|
|
137
|
-
if (Array.isArray(lastErr.referenceDiagnostics))
|
|
138
|
-
err.referenceDiagnostics = lastErr.referenceDiagnostics;
|
|
139
|
-
if (typeof lastErr.referenceMismatchCount === "number")
|
|
140
|
-
err.referenceMismatchCount = lastErr.referenceMismatchCount;
|
|
141
|
-
if (lastErr.retryKind)
|
|
142
|
-
err.retryKind = lastErr.retryKind;
|
|
143
|
-
if (typeof lastErr.referencesDroppedOnRetry === "boolean")
|
|
144
|
-
err.referencesDroppedOnRetry = lastErr.referencesDroppedOnRetry;
|
|
145
|
-
if (typeof lastErr.developerPromptDroppedOnRetry === "boolean")
|
|
146
|
-
err.developerPromptDroppedOnRetry = lastErr.developerPromptDroppedOnRetry;
|
|
240
|
+
if (lastErr.provider)
|
|
241
|
+
err.provider = lastErr.provider;
|
|
242
|
+
if (lastErr.moderation)
|
|
243
|
+
err.moderation = lastErr.moderation;
|
|
244
|
+
copyEmptyResponseMetadata(err, lastErr);
|
|
147
245
|
const diagnosticReason = diagnosticReasonFrom(lastErr);
|
|
148
246
|
if (diagnosticReason)
|
|
149
247
|
err.diagnosticReason = diagnosticReason;
|
package/lib/generationErrors.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { classifyUpstreamError, classifyUpstreamErrorCode } from "./errorClassify.js";
|
|
2
|
+
import { safeDiagnosticLabel } from "./responsesParse.js";
|
|
3
|
+
import { RESPONSE_DIAGNOSTIC_CODES } from "./responsesErrors.js";
|
|
2
4
|
|
|
3
5
|
const PASSTHROUGH_CODES = new Set([
|
|
4
6
|
"OAUTH_UNAVAILABLE",
|
|
@@ -34,14 +36,29 @@ export interface UpstreamErr {
|
|
|
34
36
|
eventType?: string;
|
|
35
37
|
eventCount?: number;
|
|
36
38
|
eventTypes?: unknown;
|
|
39
|
+
webSearchCalls?: number;
|
|
40
|
+
responseDiagnostics?: unknown;
|
|
41
|
+
webSearchEnabled?: boolean;
|
|
42
|
+
toolTypes?: unknown;
|
|
43
|
+
toolChoiceKind?: string;
|
|
44
|
+
promptChars?: number;
|
|
37
45
|
quality?: string;
|
|
46
|
+
moderation?: string;
|
|
38
47
|
model?: string;
|
|
48
|
+
provider?: string;
|
|
39
49
|
refsCount?: number;
|
|
40
50
|
inputImageCount?: number;
|
|
41
51
|
referenceDiagnostics?: unknown;
|
|
42
52
|
retryKind?: string;
|
|
53
|
+
initialEventCount?: number;
|
|
54
|
+
initialEventTypes?: unknown;
|
|
43
55
|
referencesDroppedOnRetry?: boolean;
|
|
44
56
|
developerPromptDroppedOnRetry?: boolean;
|
|
57
|
+
webSearchDroppedOnRetry?: boolean;
|
|
58
|
+
fallbackEventCount?: number;
|
|
59
|
+
fallbackEventTypes?: unknown;
|
|
60
|
+
fallbackImageCallSeen?: boolean;
|
|
61
|
+
fallbackImageResultCount?: number;
|
|
45
62
|
name?: string;
|
|
46
63
|
stack?: string;
|
|
47
64
|
}
|
|
@@ -49,22 +66,57 @@ export interface UpstreamErr {
|
|
|
49
66
|
function diagnosticReasonFrom(err: UpstreamErr | null | undefined) {
|
|
50
67
|
if (typeof err?.diagnosticReason === "string" && err.diagnosticReason) return err.diagnosticReason;
|
|
51
68
|
if (Number(err?.referenceMismatchCount) > 0) return "reference_mime_mismatch_candidate";
|
|
69
|
+
const responseDiagnosticReason = responseDiagnosticReasonFrom(err);
|
|
70
|
+
if (responseDiagnosticReason) return responseDiagnosticReason;
|
|
52
71
|
if (has4kSize(err?.size)) return "experimental_4k_empty_response";
|
|
53
72
|
return null;
|
|
54
73
|
}
|
|
55
74
|
|
|
75
|
+
function responseDiagnosticReasonFrom(err: UpstreamErr | null | undefined) {
|
|
76
|
+
if (!err?.responseDiagnostics || typeof err.responseDiagnostics !== "object") return null;
|
|
77
|
+
const diagnostics = err.responseDiagnostics as {
|
|
78
|
+
imageCallSeen?: unknown;
|
|
79
|
+
imageCallCompleted?: unknown;
|
|
80
|
+
imageCallFailed?: unknown;
|
|
81
|
+
imageResultCount?: unknown;
|
|
82
|
+
messageOutputSeen?: unknown;
|
|
83
|
+
streamStats?: { bytesRead?: unknown };
|
|
84
|
+
};
|
|
85
|
+
const bytesRead = Number(diagnostics.streamStats?.bytesRead);
|
|
86
|
+
if (Number.isFinite(bytesRead) && bytesRead > 0 && Number(err.eventCount) === 0) return "stream_parse_failed";
|
|
87
|
+
if (diagnostics.imageCallFailed === true) return "image_tool_failed";
|
|
88
|
+
if (diagnostics.imageCallCompleted === true && Number(diagnostics.imageResultCount) === 0) return "image_tool_completed_without_result";
|
|
89
|
+
if (diagnostics.imageCallSeen !== true && Number(err.webSearchCalls) > 0) return "web_search_only_response";
|
|
90
|
+
if (diagnostics.imageCallSeen !== true && diagnostics.messageOutputSeen === true) return "image_tool_not_called";
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function responseDiagnosticCodeFrom(err: UpstreamErr | null | undefined) {
|
|
95
|
+
const reason = responseDiagnosticReasonFrom(err);
|
|
96
|
+
if (reason === "stream_parse_failed") return "STREAM_PARSE_FAILED";
|
|
97
|
+
if (reason === "web_search_only_response") return "WEB_SEARCH_ONLY_RESPONSE";
|
|
98
|
+
if (reason === "image_tool_not_called") return "IMAGE_TOOL_NOT_CALLED";
|
|
99
|
+
if (reason === "image_tool_failed") return "IMAGE_TOOL_FAILED";
|
|
100
|
+
if (reason === "image_tool_completed_without_result") return "IMAGE_TOOL_COMPLETED_WITHOUT_RESULT";
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
56
104
|
export function errorCodeFrom(err: UpstreamErr | null | undefined): string {
|
|
57
105
|
if (!err) return "UNKNOWN";
|
|
106
|
+
const appCode = err.code as string;
|
|
107
|
+
if (RESPONSE_DIAGNOSTIC_CODES.has(appCode) || appCode === "EMPTY_RESPONSE") return appCode;
|
|
58
108
|
const upstreamCode = classifyUpstreamErrorCode(err.upstreamCode);
|
|
59
109
|
if (upstreamCode !== "UNKNOWN") return upstreamCode;
|
|
60
110
|
const upstreamType = classifyUpstreamErrorCode(err.upstreamType);
|
|
61
111
|
if (upstreamType !== "UNKNOWN") return upstreamType;
|
|
62
112
|
// Known app-level codes pass through directly (before message heuristic)
|
|
63
|
-
if (PASSTHROUGH_CODES.has(
|
|
113
|
+
if (PASSTHROUGH_CODES.has(appCode) || SAFETY_CODES.has(appCode)) return appCode;
|
|
64
114
|
const rawCode = classifyUpstreamErrorCode(err.code);
|
|
65
115
|
if (rawCode !== "UNKNOWN") return rawCode;
|
|
66
116
|
const direct = classifyUpstreamError(err.message);
|
|
67
117
|
if (direct !== "UNKNOWN") return direct;
|
|
118
|
+
const responseDiagnosticCode = responseDiagnosticCodeFrom(err);
|
|
119
|
+
if (responseDiagnosticCode) return responseDiagnosticCode;
|
|
68
120
|
const status = Number(err.status);
|
|
69
121
|
if (Number.isFinite(status) && status >= 400 && status < 500 && !SAFETY_CODES.has(err.code as string)) {
|
|
70
122
|
return "INVALID_REQUEST";
|
|
@@ -86,12 +138,40 @@ export function statusForErrorCode(code: string, fallback = 500) {
|
|
|
86
138
|
if (code === "AUTH_CHATGPT_EXPIRED" || code === "AUTH_API_KEY_INVALID") return 401;
|
|
87
139
|
if (code === "API_KEY_REQUIRED") return 401;
|
|
88
140
|
if (code === "UPSTREAM_5XX") return 502;
|
|
141
|
+
if (code === "RESPONSES_STREAM_ERROR") return 502;
|
|
89
142
|
if (code === "OAUTH_IMAGE_TIMEOUT") return 504;
|
|
90
143
|
if (code === "INVALID_REQUEST") return 400;
|
|
144
|
+
if (RESPONSE_DIAGNOSTIC_CODES.has(code)) return 422;
|
|
91
145
|
if (code === "SAFETY_REFUSAL" || code === "MODERATION_REFUSED" || code === "moderation_blocked") return 422;
|
|
92
146
|
return fallback;
|
|
93
147
|
}
|
|
94
148
|
|
|
149
|
+
function copyEmptyResponseMetadata(target: any, source: UpstreamErr | null | undefined) {
|
|
150
|
+
if (!source) return;
|
|
151
|
+
if (typeof source.eventCount === "number") target.eventCount = source.eventCount;
|
|
152
|
+
if (source.eventTypes) target.eventTypes = source.eventTypes;
|
|
153
|
+
if (typeof source.webSearchCalls === "number") target.webSearchCalls = source.webSearchCalls;
|
|
154
|
+
if (source.responseDiagnostics) target.responseDiagnostics = source.responseDiagnostics;
|
|
155
|
+
if (typeof source.webSearchEnabled === "boolean") target.webSearchEnabled = source.webSearchEnabled;
|
|
156
|
+
if (Array.isArray(source.toolTypes)) target.toolTypes = source.toolTypes;
|
|
157
|
+
if (source.toolChoiceKind) target.toolChoiceKind = source.toolChoiceKind;
|
|
158
|
+
if (typeof source.promptChars === "number") target.promptChars = source.promptChars;
|
|
159
|
+
if (typeof source.refsCount === "number") target.refsCount = source.refsCount;
|
|
160
|
+
if (typeof source.inputImageCount === "number") target.inputImageCount = source.inputImageCount;
|
|
161
|
+
if (Array.isArray(source.referenceDiagnostics)) target.referenceDiagnostics = source.referenceDiagnostics;
|
|
162
|
+
if (typeof source.referenceMismatchCount === "number") target.referenceMismatchCount = source.referenceMismatchCount;
|
|
163
|
+
if (source.retryKind) target.retryKind = source.retryKind;
|
|
164
|
+
if (typeof source.initialEventCount === "number") target.initialEventCount = source.initialEventCount;
|
|
165
|
+
if (source.initialEventTypes) target.initialEventTypes = source.initialEventTypes;
|
|
166
|
+
if (typeof source.referencesDroppedOnRetry === "boolean") target.referencesDroppedOnRetry = source.referencesDroppedOnRetry;
|
|
167
|
+
if (typeof source.developerPromptDroppedOnRetry === "boolean") target.developerPromptDroppedOnRetry = source.developerPromptDroppedOnRetry;
|
|
168
|
+
if (typeof source.webSearchDroppedOnRetry === "boolean") target.webSearchDroppedOnRetry = source.webSearchDroppedOnRetry;
|
|
169
|
+
if (typeof source.fallbackEventCount === "number") target.fallbackEventCount = source.fallbackEventCount;
|
|
170
|
+
if (source.fallbackEventTypes) target.fallbackEventTypes = source.fallbackEventTypes;
|
|
171
|
+
if (typeof source.fallbackImageCallSeen === "boolean") target.fallbackImageCallSeen = source.fallbackImageCallSeen;
|
|
172
|
+
if (typeof source.fallbackImageResultCount === "number") target.fallbackImageResultCount = source.fallbackImageResultCount;
|
|
173
|
+
}
|
|
174
|
+
|
|
95
175
|
export function normalizeGenerationFailure(lastErr: UpstreamErr | null | undefined, options: any = {}) {
|
|
96
176
|
const code = errorCodeFrom(lastErr);
|
|
97
177
|
if (PASSTHROUGH_CODES.has(code)) {
|
|
@@ -99,9 +179,9 @@ export function normalizeGenerationFailure(lastErr: UpstreamErr | null | undefin
|
|
|
99
179
|
err.code = code;
|
|
100
180
|
err.status = lastErr?.status || statusForErrorCode(code);
|
|
101
181
|
err.cause = lastErr;
|
|
102
|
-
if (lastErr?.upstreamCode) err.upstreamCode = lastErr.upstreamCode;
|
|
103
|
-
if (lastErr?.upstreamType) err.upstreamType = lastErr.upstreamType;
|
|
104
|
-
if (lastErr?.upstreamParam) err.upstreamParam = lastErr.upstreamParam;
|
|
182
|
+
if (lastErr?.upstreamCode) err.upstreamCode = safeDiagnosticLabel(lastErr.upstreamCode);
|
|
183
|
+
if (lastErr?.upstreamType) err.upstreamType = safeDiagnosticLabel(lastErr.upstreamType);
|
|
184
|
+
if (lastErr?.upstreamParam) err.upstreamParam = safeDiagnosticLabel(lastErr.upstreamParam);
|
|
105
185
|
if (lastErr?.eventType) err.eventType = lastErr.eventType;
|
|
106
186
|
if (typeof lastErr?.eventCount === "number") err.eventCount = lastErr.eventCount;
|
|
107
187
|
return err;
|
|
@@ -113,6 +193,19 @@ export function normalizeGenerationFailure(lastErr: UpstreamErr | null | undefin
|
|
|
113
193
|
err.cause = lastErr;
|
|
114
194
|
return err;
|
|
115
195
|
}
|
|
196
|
+
if (RESPONSE_DIAGNOSTIC_CODES.has(code)) {
|
|
197
|
+
const err: any = new Error(lastErr?.message || "Image generation did not return image data");
|
|
198
|
+
err.code = code;
|
|
199
|
+
err.status = lastErr?.status || statusForErrorCode(code, 422);
|
|
200
|
+
err.cause = lastErr;
|
|
201
|
+
if (lastErr?.upstreamCode) err.upstreamCode = safeDiagnosticLabel(lastErr.upstreamCode);
|
|
202
|
+
if (lastErr?.upstreamType) err.upstreamType = safeDiagnosticLabel(lastErr.upstreamType);
|
|
203
|
+
if (lastErr?.upstreamParam) err.upstreamParam = safeDiagnosticLabel(lastErr.upstreamParam);
|
|
204
|
+
if (lastErr?.eventType) err.eventType = lastErr.eventType;
|
|
205
|
+
copyEmptyResponseMetadata(err, lastErr);
|
|
206
|
+
err.diagnosticReason = diagnosticReasonFrom(lastErr) || code.toLowerCase();
|
|
207
|
+
return err;
|
|
208
|
+
}
|
|
116
209
|
// Empty response with metadata → likely a technical limitation (unsupported size/quality/model)
|
|
117
210
|
if (typeof lastErr?.eventCount === "number") {
|
|
118
211
|
const meta: string[] = [];
|
|
@@ -129,15 +222,9 @@ export function normalizeGenerationFailure(lastErr: UpstreamErr | null | undefin
|
|
|
129
222
|
if (lastErr.size) err.size = lastErr.size;
|
|
130
223
|
if (lastErr.quality) err.quality = lastErr.quality;
|
|
131
224
|
if (lastErr.model) err.model = lastErr.model;
|
|
132
|
-
if (
|
|
133
|
-
if (lastErr.
|
|
134
|
-
|
|
135
|
-
if (typeof lastErr.inputImageCount === "number") err.inputImageCount = lastErr.inputImageCount;
|
|
136
|
-
if (Array.isArray(lastErr.referenceDiagnostics)) err.referenceDiagnostics = lastErr.referenceDiagnostics;
|
|
137
|
-
if (typeof lastErr.referenceMismatchCount === "number") err.referenceMismatchCount = lastErr.referenceMismatchCount;
|
|
138
|
-
if (lastErr.retryKind) err.retryKind = lastErr.retryKind;
|
|
139
|
-
if (typeof lastErr.referencesDroppedOnRetry === "boolean") err.referencesDroppedOnRetry = lastErr.referencesDroppedOnRetry;
|
|
140
|
-
if (typeof lastErr.developerPromptDroppedOnRetry === "boolean") err.developerPromptDroppedOnRetry = lastErr.developerPromptDroppedOnRetry;
|
|
225
|
+
if (lastErr.provider) err.provider = lastErr.provider;
|
|
226
|
+
if (lastErr.moderation) err.moderation = lastErr.moderation;
|
|
227
|
+
copyEmptyResponseMetadata(err, lastErr);
|
|
141
228
|
const diagnosticReason = diagnosticReasonFrom(lastErr);
|
|
142
229
|
if (diagnosticReason) err.diagnosticReason = diagnosticReason;
|
|
143
230
|
return err;
|