fix(avatar): classify image-gen failures + render actionable detail (#957) - #976
Merged
Conversation
…957) Avatar Generate dialog showed only "Failed to generate avatar" with no diagnostic info — operators couldn't tell whether the failure was a missing API key, an upstream rate limit, a safety-filter rejection, or a network timeout. Root causes: - Backend returned the raw upstream exception string as the HTTP detail. In several real failure modes (nginx 504 with HTML body, network abort) the frontend got no JSON detail at all and fell back to a hardcoded generic message. - Frontend used bare `axios` instead of the shared `@/api` client (Invariant #7), with no per-status fallback chain. Backend: - `ImageGenerationResult.error_kind` — coarse classification (`not_configured` | `invalid_input` | `safety_filter` | `rate_limited` | `upstream_error` | `timeout` | `unknown`) set on every failure path. - `_classify_exception()` maps httpx + RuntimeError exceptions to a kind. - Catch blocks now use structured logging via `extra={...}` so Vector indexes agent_name, error_kind, exception_type, etc. as fields. - `_AVATAR_ERROR_HTTP` map → kind to (HTTP status, friendly detail). `generate_avatar` and `regenerate_avatar` use the map instead of hardcoded 422 + raw exception text. Service-not-available early-exit uses the same friendly text. Frontend: - `AvatarGenerateModal.vue` switched from bare `axios` to `@/api` and bumped the per-request timeout to 180s (image gen can take >30s). - `describeAvatarError(err, verb)` falls back gracefully on 502/503/504 and no-response cases so the user gets a directional message even when the upstream strips the JSON detail. Tests: - 7 new cases in `tests/unit/test_image_generation_service.py` cover `_classify_exception` and the `error_kind` field default. Related to #957 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
vybe
approved these changes
Jun 1, 2026
vybe
left a comment
Contributor
There was a problem hiding this comment.
LGTM — verified _classify_exception substrings match the strings the service actually raises (Gemini API error {status}, no image data), and the 429-before-4xx check ordering is correct so rate-limit classification wins. Frontend axios→@/api switch fixes Invariant #7 and is required for the fallback chain. CI green, 7 new tests. Validated via /validate-pr.
5 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Avatar Generate dialog showed only "Failed to generate avatar" with no diagnostic info. Operators couldn't tell whether the failure was a missing API key, an upstream rate limit, a safety-filter rejection, or a network timeout.
Two root causes:
detailand used hardcoded422. Several real failure modes (nginx 504 with HTML body, network abort) strip the JSONdetailentirely.axios(Invariant security: Fix token logging and add HTML reports to gitignore #7 violation) with no per-status fallback chain — whendetailwas missing it dropped to a hardcoded generic message.Backend
ImageGenerationResult.error_kind— coarse classification (not_configured|invalid_input|safety_filter|rate_limited|upstream_error|timeout|unknown) set on every failure path._classify_exception()maps httpx + RuntimeError exceptions to a kind.extra={...}so Vector indexesagent_name,error_kind,exception_type, etc. as fields instead of grepping a string._AVATAR_ERROR_HTTPmap → kind to(HTTP status, friendly detail).generate_avatarandregenerate_avataruse the map instead of hardcoded422+ raw exception text. Service-not-available early-exit uses the same friendly text.Frontend
AvatarGenerateModal.vueswitched from bareaxiosto@/apiand bumped per-request timeout to 180s (image gen can take >30s).describeAvatarError(err, verb)falls back gracefully on 502/503/504/no-response cases so the user always gets a directional message.Tests
7 new cases in
tests/unit/test_image_generation_service.pycover_classify_exceptionand theerror_kindfield. All 40 unit tests in the file pass.Related to #957
Test plan
Verified live + via static-mock analysis:
not_configured—POST /api/agents/.../avatar/generateagainst emptyGEMINI_API_KEYreturns501with detail"Avatar generation isn't configured on this server. Ask an admin to set GEMINI_API_KEY."Dialog renders that text.describeAvatarErrorreturns"Avatar generate timed out — please retry."err.response) — returns"Network error while trying to generate avatar — check your connection and retry.""Image generation service is unavailable right now — please retry in a few minutes.".venv/bin/pytest tests/unit/test_image_generation_service.py— 40 passed (33 existing + 7 new).🤖 Generated with Claude Code