Skip to content

feat(chat): reasoning-effort override scoped to chat generation, actionable 422 gateway errors, and touch-device copy action - #159

Merged
WilliamAGH merged 4 commits into
mainfrom
dev
Jul 31, 2026
Merged

feat(chat): reasoning-effort override scoped to chat generation, actionable 422 gateway errors, and touch-device copy action#159
WilliamAGH merged 4 commits into
mainfrom
dev

Conversation

@WilliamAGH

Copy link
Copy Markdown
Owner

Summary

The app.llm.reasoning-effort override now applies only to chat generation so reranker/enrichment JSON completions can no longer be starved by reasoning tokens, and a gateway rejection of an unsatisfiable effort now surfaces an actionable non-retryable error instead of a generic failure. Also ships a touch-device fix for the assistant copy action and full test-failure stack traces for developers.

Changes

Features

  • Reasoning-effort override scoped to chat: JSON-object completions (reranker, enrichment) now inherit the gateway/model default reasoning effort instead of the configured override, whose reasoning tokens could consume their small output budgets and leave no room for the visible JSON answer (OpenAiRequestFactory.prepareRequest)
  • Actionable 422 reasoning errors: when the gateway deterministically rejects a reasoning effort it cannot preserve (HTTP 422 unpreservable_reasoning_intent), the stream now ends with a non-retryable error naming the configured effort and the owning app.llm.reasoning-effort setting, instead of the generic "something went wrong" message that invited futile retries (ChatController.isUnpreservableReasoningIntent, ChatController.unpreservableReasoningIntentMessage)

Bug Fixes

  • Copy action hidden on touch devices: the assistant message copy action, previously revealed only on hover, is now discoverable on touch and hybrid input devices while desktop hover behavior is preserved; the action slot is reserved during streaming (MessageBubble.svelte)

Developer Experience

  • Full test-failure stack traces: Gradle test output now logs full exception chains with stack traces, and -XX:-OmitStackTraceInFastThrow stops the JVM from dropping frames on repeatedly thrown hot exceptions, restoring root-cause visibility when diagnosing streaming failures (build.gradle.kts)
  • Streaming failure test under PMD threshold: ChatControllerStreamingFailureTest exceeded the CouplingBetweenObjects threshold (37 > 35) and blocked the pmdTest pre-push gate; the 422 gateway failure is now simulated with a stubbed InternalServerException wrapped in ReportedTerminalStreamingFailure, matching the production path and removing the excess type references (ChatControllerStreamingFailureTest.unprocessableEntityFailure)

Documentation

  • Configuration guide: documents that the reasoning-effort override is chat-only and explains the deterministic 422 meaning and remedy (docs/configuration.md)

Breaking Changes

None

Test Plan

  • ./gradlew test --tests OpenAiRequestFactoryTest --tests ChatControllerStreamingFailureTest — green
  • ./gradlew pmdTest — green (previously failing at 37 > 35)
  • Pre-push gate (make build-with-lock: frontend build + gradle build -x test + lint) — green

Keep assistant copy actions discoverable across touch and hybrid input while preserving desktop hover behavior.
- Reserve the action slot during streaming and verify visibility, focus, and disabled state
JSON-object completions (reranker, enrichment) own small output budgets whose visible answers reasoning tokens would starve, so they now inherit the gateway/model default. A gateway HTTP 422 unpreservable_reasoning_intent is deterministic and retrying unchanged can never succeed.
- OpenAiRequestFactory applies the configured reasoning effort only to non-JSON requests
- ChatController maps HTTP 422 unpreservable_reasoning_intent to a non-retryable error naming the configured effort and app.llm.reasoning-effort
- docs/configuration.md documents the chat-only scope and the 422 meaning
Gradle default test logging truncates exception chains, and the JVM OmitStackTraceInFastThrow optimization drops frames from repeatedly thrown hot exceptions; both hid the root cause while diagnosing streaming failure regressions.
- Log FULL exception format with stack traces for all Test tasks
- Pass -XX:-OmitStackTraceInFastThrow to test JVMs
ChatControllerStreamingFailureTest exceeded the CouplingBetweenObjects threshold (37 > 35) after the unpreservable_reasoning_intent coverage added Headers, ErrorObject, and UnprocessableEntityException references, failing the pmdTest pre-push gate. The straightforward mock replacement also crashed the test container: a bare mocked Throwable reaches Reactor and logging machinery that dereferences surfaces a mock leaves null.
- Simulate the gateway 422 with a mocked InternalServerException (an OpenAIServiceException) stubbing only statusCode and code
- Wrap it in ReportedTerminalStreamingFailure, matching the existing gateway-timeout test and the production path where the service reports terminal failures before the controller classifies them
@coderabbitai

coderabbitaiBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes

    • Copy actions now remain accessible on touch and narrow screens.
    • Copy controls are hidden and disabled while responses are streaming, while preserving the message layout.
    • Added clearer guidance when configured reasoning settings are unsupported, including the setting that needs adjustment.
    • JSON responses now follow the appropriate default reasoning behavior.
  • Documentation

    • Updated gateway configuration guidance for provider backoff, reasoning overrides, JSON completions, and related error handling.
  • Tests

    • Expanded coverage for copy controls, streaming behavior, reasoning settings, and streaming failures.

Walkthrough

The change updates LLM reasoning behavior for chat and JSON completions, adds targeted handling for reasoning-related streaming failures, adjusts message copy-action visibility during streaming and across pointer types, and improves test diagnostics.

Changes

LLM reasoning and failure handling

Layer / File(s)Summary
LLM request reasoning policy
src/main/java/.../OpenAiRequestFactory.java, src/test/java/.../OpenAiRequestFactoryTest.java, docs/configuration.md, build.gradle.kts
Reasoning effort is forwarded for chat requests, omitted for JSON-object completions, documented accordingly, and test logging now preserves full stack traces.
Streaming reasoning error handling
src/main/java/.../ChatController.java, src/test/java/.../ChatControllerStreamingFailureTest.java
HTTP 422 unpreservable_reasoning_intent failures report the configured reasoning effort; other failures retain generic non-retryable messaging.
Message action interaction states
frontend/src/lib/components/MessageBubble.svelte, frontend/src/lib/components/MessageBubble.test.ts
Copy actions remain available on touch and narrow layouts, use desktop hover/focus behavior, and are hidden and disabled while streaming while preserving layout space.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
participant ChatController
participant OpenAiRequestFactory
participant OpenAIService
ChatController->>OpenAiRequestFactory: Build chat request with configured reasoning
OpenAiRequestFactory->>OpenAIService: Send streaming or JSON-object request
OpenAIService-->>ChatController: Return HTTP 422 reasoning-intent failure
ChatController-->>ChatController: Create effort-specific failure message
Loading

Possibly related PRs

Suggested labels:enhancement, bug, java, javascript

Poem

Reasoning winds through chat’s bright stream,
JSON keeps a quieter dream.
Copy buttons wait, then softly hide,
Stack traces shine with nothing denied.
A 422 points the way—
Lower the effort, save the day!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 11.76% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Title check✅ PassedThe title clearly summarizes the PR's main reasoning-effort, gateway-error, and touch-device copy-action changes.
Description check✅ PassedThe description is detailed and directly explains the reasoning, error-handling, UI, testing, documentation, and developer-experience changes.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitaicoderabbitaiBot added bug Something isn't working enhancement New feature or request java Pull requests that update java code javascript Pull requests that update javascript code labels Jul 31, 2026

@coderabbitaicoderabbitaiBot 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@src/test/java/com/williamcallahan/javachat/web/ChatControllerStreamingFailureTest.java`:
- Around line 216-219: Replace the hardcoded "error" event names in the stream
filtering assertions around streamEvents with the canonical EVENT_ERROR constant
from SseConstants, including the occurrence near the later assertion. Preserve
the existing filtering and assertion behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 20c6ae5a-727c-4d92-9d92-1d008f84e168

📥 Commits

Reviewing files that changed from the base of the PR and between 92c4e11 and 739ff66.

📒 Files selected for processing (8)
  • build.gradle.kts
  • docs/configuration.md
  • frontend/src/lib/components/MessageBubble.svelte
  • frontend/src/lib/components/MessageBubble.test.ts
  • src/main/java/com/williamcallahan/javachat/service/OpenAiRequestFactory.java
  • src/main/java/com/williamcallahan/javachat/web/ChatController.java
  • src/test/java/com/williamcallahan/javachat/service/OpenAiRequestFactoryTest.java
  • src/test/java/com/williamcallahan/javachat/web/ChatControllerStreamingFailureTest.java

Comment on lines +216 to +219
ServerSentEvent<String> errorEvent = streamEvents.stream()
.filter(streamEvent -> "error".equals(streamEvent.event()))
.findFirst()
.orElseThrow();

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.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use the canonical SSE error-event constant.

Lines 217 and 235 duplicate "error" instead of using EVENT_ERROR, which this test already uses elsewhere. Tiny consistency win.

Proposed fix
- .filter(streamEvent -> "error".equals(streamEvent.event()))+ .filter(streamEvent -> EVENT_ERROR.equals(streamEvent.event()))

Based on learnings, reference event type strings from SseConstants.java instead of hardcoding values like "error".

Also applies to: 234-237

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@src/test/java/com/williamcallahan/javachat/web/ChatControllerStreamingFailureTest.java`
around lines 216 - 219, Replace the hardcoded "error" event names in the stream
filtering assertions around streamEvents with the canonical EVENT_ERROR constant
from SseConstants, including the occurrence near the later assertion. Preserve
the existing filtering and assertion behavior.

Source: Learnings

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit:739ff6652b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +436 to +437
if (isUnpreservableReasoningIntent(error)) {
return unpreservableReasoningIntentMessage();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Handle reasoning 422s in guided chat streams

When the same configured reasoning effort is used through /api/guided/stream, GuidedLearningController.guidedStreamError() still maps unpreservable_reasoning_intent to the generic "Streaming error" response whose details tell the user to try again. Because this new classification is local to ChatController, guided-chat users receive no instruction to change the deterministic failing setting; apply the classification to that streaming boundary as well.

Useful? React with 👍 / 👎.

@WilliamAGH
WilliamAGH merged commit f5f190e into mainJul 31, 2026
7 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugSomething isn't workingenhancementNew feature or requestjavaPull requests that update java codejavascriptPull requests that update javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@WilliamAGH