Skip to content

fix: surface real cause when outfit suggestion AI response is truncated (#139)#142

Open
AnayGarodia wants to merge 1 commit into
Anyesh:mainfrom
AnayGarodia:fix/outfit-suggest-truncated-ai-response
Open

fix: surface real cause when outfit suggestion AI response is truncated (#139)#142
AnayGarodia wants to merge 1 commit into
Anyesh:mainfrom
AnayGarodia:fix/outfit-suggest-truncated-ai-response

Conversation

@AnayGarodia

Copy link
Copy Markdown

Description

Fixes #139.

The issue's attached backend + LM Studio logs pin down the actual failure
mechanism (I did not have an equivalent AI endpoint to reproduce the full
end-to-end flow, but the logs contain the exact API response shape, which
is enough to reproduce the failure precisely in a unit test — see below):

  • The model in the report is a reasoning-capable Qwen3 build. Reasoning
    models return their chain-of-thought in a separate reasoning_content
    field, distinct from content.
  • In the logs, the model spent its entire completion token budget on
    reasoning_content and never got to writing content. The response
    came back with content: "", finish_reason: "length".
  • RecommendationService._parse_ai_response then fails to parse "" as
    JSON (Could not parse AI response as JSON: — matches the log line
    exactly, note the empty string after the colon).
  • That ValueError is caught by a broad except Exception in
    generate_recommendation and replaced with a generic, misleading
    message: "AI service is not available. Please check your AI endpoint
    configuration in Settings." The AI service was available and was
    correctly configured — it just didn't produce any output content.

I also grepped the whole codebase for the literal string "An error has
occured"/"occurred" — it does not exist anywhere in the frontend. The
issue title/body is the reporter's own paraphrase of what they saw, not a
literal string bug, so there's nothing to fix there.

Fix

  • AIService.generate_text() now checks for empty/blank content right
    after extracting the API response. If found, it inspects finish_reason
    and reasoning_content to build a specific, actionable error
    (AIResponseTruncatedError), e.g. "returned an empty response: its
    reasoning/thinking output consumed the entire completion token budget
    before it produced a response... Try raising AI_MAX_TOKENS (currently
    8000) or disabling extended thinking/reasoning mode for this model."
  • This is treated like the existing retryable HTTP failures in the same
    loop — it retries (up to AI_MAX_RETRIES) before giving up, since
    whether the model finishes reasoning in time is non-deterministic.
  • RecommendationService.generate_recommendation now catches
    AIResponseTruncatedError explicitly and re-raises it with its own
    specific message, instead of letting the generic except Exception
    handler stomp it with the "AI service is not available" text.

Scope

This is deliberately scoped to the outfit-suggestion path
(ai_service.py / recommendation_service.py), which is what #139
reports. pairing_service.py has the same _parse_ai_response /
generic-error pattern and likely has the same latent issue, but that's a
separate code path (see #140, which is about pairings UI copy, not this
bug) — left out of this PR to keep it minimal and reviewable. Happy to
follow up there if maintainers want it.

I was not able to fully reproduce the end-to-end flow (would need a local
reasoning-model endpoint like the reporter's LM Studio + Qwen3 setup), but
I reproduced the exact failure mechanism with a unit test built from the
literal response payload shape captured in the issue's attached logs, and
verified the fix resolves it while leaving normal responses unaffected.

Testing

  • Added TestGenerateTextTruncatedResponse in
    backend/tests/test_ai_service.py:
    • empty content + reasoning_content + finish_reason: "length"
      raises AIResponseTruncatedError mentioning the reasoning cause and
      AI_MAX_TOKENS
    • empty content without reasoning_content → still raises
      AIResponseTruncatedError (generic truncation)
    • normal non-empty content response → unaffected, still returns
      content
  • Ran the full backend suite locally (Postgres + Redis via
    docker compose up postgres redis): 386 passed.
  • ruff check and ruff format --check pass on all changed files.

Checklist

  • I have read the CONTRIBUTING guide
  • My code follows the project's coding style
  • I have added tests that prove my fix works
  • New and existing tests pass locally
  • I have updated documentation as needed (no user-facing docs affected)
  • My changes don't introduce new warnings or errors

…ncated

Fixes Anyesh#139. The user's attached backend/LM Studio logs show the actual
failure: the model (a reasoning-capable Qwen3 build) put its whole
chain-of-thought in `reasoning_content`, exhausted the completion token
budget doing so, and returned `content: ""` with `finish_reason: "length"`.
`_parse_ai_response` then failed to parse the empty string as JSON, and
that failure was swallowed by a broad `except Exception` into the generic
"AI service is not available. Please check your AI endpoint configuration
in Settings." — which is misleading, since the endpoint responded
successfully and is correctly configured; it just never emitted any output
content.

`AIService.generate_text()` now detects this empty-content/finish_reason
case right after extracting the response and raises a new
`AIResponseTruncatedError` with the specific cause (reasoning consumed the
budget vs. some other truncation) and an actionable hint (raise
AI_MAX_TOKENS or disable extended thinking for the model). It retries
across the existing retry loop before giving up, since the failure is
non-deterministic (depends on how long the model reasons). In
`RecommendationService.generate_recommendation`, this error type is now
caught explicitly and re-raised with its original, specific message
instead of being caught by the generic fallback handler.

Reproduced with a unit test built from the exact response payload shape in
the issue's attached logs (empty content + reasoning_content + finish_reason
"length"); also confirmed the app's error text is not a literal "An error
has occured" string anywhere in the codebase, so the issue title is the
reporter's paraphrase rather than a literal typo to fix.

pairing_service.py has an analogous parsing/error-swallowing pattern but is
intentionally left untouched here — it's a separate feature (see Anyesh#140) and
out of scope for this fix.
Copilot AI review requested due to automatic review settings July 19, 2026 20:34

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Outfit Suggestion "An error has occured"

2 participants