Skip to content

fix(ai): stop retrying a response truncated at the length cap - #495

Merged
devops-thiago merged 3 commits into
release/v0.6.0from
fix/492-truncation-detection
Aug 10, 2026
Merged

fix(ai): stop retrying a response truncated at the length cap#495
devops-thiago merged 3 commits into
release/v0.6.0from
fix/492-truncation-detection

Conversation

@devops-thiago

@devops-thiago devops-thiago commented Aug 9, 2026

Copy link
Copy Markdown
Owner

What type of PR is this?

  • 🐛 Bug fix

Description

Fixes #492.

A model that stops on finish_reason: length returns a body cut mid-JSON. Nothing read the finish reason, so a truncation surfaced only as "Model response is not valid review JSON" — indistinguishable from a genuinely malformed response, and therefore routed into the transient-failure retry path.

It is not transient. The same prompt against the same cap is cut at the same point every time. So:

  • AiReviewService.runWithRetries retried it max-ai-retries times (default 5) with exponential backoff
  • FindingPipeline then treated the exhausted-retries failure as a batch failure and re-ran processBatch, entering that loop again

One over-cap batch therefore cost roughly ten full-price input calls, every one knowably futile, before the batch was dropped and its files disclosed as unreviewed. The only knob that bounds output spend multiplied it instead — and none of those retries counted against REVIEW_MAX_AI_CALLS, which caps only planned calls.

The change

Read finishReason off the ChatResponse and raise a distinct AiResponseTruncatedException. Both retry layers now decline it:

  • the service rethrows immediately, without retrying and without broadcasting a retry event to the dashboard (a retry that never happens should not appear in the live feed)
  • the pipeline goes straight to the existing disclosure path rather than paying for a second guaranteed truncation

The soft-fail contract is otherwise untouched: successful batches keep their findings, the truncated batch's files are still recorded as uncovered so the verdict holds and the summary discloses the gap.

Two details worth noting for review:

  • AiResponseTruncatedException extends AiReviewException deliberately. asAiReviewException returns an AiReviewException cause as-is, so the subclass survives the CompletableFuture unwrapping with its identity intact. Extending RuntimeException instead would have been silently re-wrapped and the guard would never fire.
  • isResponseTruncated walks the cause chain (the failure arrives as CompletionException over the service's exception) with a self-reference guard so a malformed chain cannot loop.

The message names max-output-tokens explicitly, so an operator reading the log can see which knob caused it and that leaving it unset falls back to the provider default.

Related Issues

Fixes #492

Companions, not included here: #493 (output-buffer-tokens subtracted from input on separate-budget models) and #494 (the startup rule built on the same shared-window assumption). This one is independent of both — it bites on any model, shared-window or not, for anyone who sets max-output-tokens at all.

How Has This Been Tested?

  • Unit tests

New fake TruncatedTokenStream emits a body cut mid-JSON with FinishReason.LENGTH — what a provider actually returns — and counts its own start() calls so a test can assert what a truncation cost.

Service layer, AiReviewServiceTest. Red phase with only the no-retry guard reverted (detection left in place), so the failure isolates the guard:

[ERROR] AiReviewServiceTest.shouldNotRetryAResponseTruncatedAtTheLengthCap:949 Unexpected exception type thrown,
expected: <...AiResponseTruncatedException> but was: <...AiReviewException>
[ERROR] AiReviewServiceTest.shouldNotBroadcastRetryEventsForATruncatedResponse:974 Unexpected exception type thrown,
expected: <...AiResponseTruncatedException> but was: <...AiReviewException>

That generic AiReviewException is the defect visible in one line: the truncation was retried to exhaustion and reported as "AI review failed after N attempts". Green with the guard restored. The first test asserts starts.get() == 1 — exactly one call — and that the parser is never reached.

Batch layer, FindingPipelineTest.multiCallDoesNotRetryABatchTruncatedAtTheModelsLengthCap. Red with the branch disabled:

org.mockito.exceptions.verification.TooManyActualInvocations:
aiReviewService.reviewBatch(
    ReviewSession<null>,
    <any>,
    1,
    <any integer>
);

The second, futile call caught directly. The test also pins the parts that must not change — the successful batch keeps its finding, plan.runtimeUncoveredFiles() still lists the truncated batch's file, and the summary still says a.java (not reviewed.

Gates on the final tree:

  • ./mvnw -B spotless:apply / spotless:check — clean
  • ./mvnw -B clean compile spotbugs:checkBugInstance size is 0, BUILD SUCCESS
  • ./mvnw -B clean testTests run: 2429, Failures: 0, Errors: 0, Skipped: 0

Checklist

  • My code follows the project's coding standards
  • I have performed a self-review of my own code
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have updated the documentation accordingly
  • My changes generate no new warnings or errors

Additional Notes

Scope, stated plainly. This makes a truncation cheap and legible instead of expensive and mute. It does not yet recover the findings — a truncated batch is still disclosed as unreviewed rather than re-run split smaller or salvaged for whatever parsed. That is a deliberate follow-up: batches are planned up front by DiffBudgetPlanner with token accounting, so re-splitting one mid-flight duplicates that logic and deserves its own change. The cost amplification was the urgent half and it is self-contained.

No behaviour changes for any deployment that does not set max-output-tokens, since no max_tokens is sent and no length stop can occur.

A model that stops on finish_reason=length returns a body cut mid-JSON.
Nothing read the finish reason, so the truncation surfaced only as a parse
failure — indistinguishable from a malformed response, and therefore routed
into the transient-failure retry path.

That path retries max-ai-retries times (default 5) with exponential backoff,
and FindingPipeline then retried the batch, entering the same loop again. So
one over-cap batch cost roughly ten full-price input calls, every one of them
knowably futile — same prompt, same cap, same cut — before the batch was
dropped and its files disclosed as unreviewed. The knob that exists to bound
output spend multiplied it instead.

Read finishReason from the ChatResponse and raise a distinct
AiResponseTruncatedException. Both retry layers now decline it: the service
rethrows without retrying or broadcasting a retry event, and the pipeline goes
straight to the existing disclosure instead of paying for a second guaranteed
truncation. The soft-fail contract is otherwise unchanged — successful batches
keep their findings and the truncated batch's files are still disclosed.

The exception extends AiReviewException so it survives asAiReviewException
with its identity intact, and the message names max-output-tokens so an
operator can see which knob caused it.

Refs #492
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

Scanned Files

None

@codecov

codecov Bot commented Aug 9, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@thrillhousebot

thrillhousebot Bot commented Aug 9, 2026

Copy link
Copy Markdown

⚠️ ThrillhouseBot review could not be completed.

The review service encountered an error. Please reply with /review or @Thrillhousebot review to retry.

…g self-cycles

The cause walk carried a `cause.getCause() == cause` guard that was both
incomplete and untested: it caught a Throwable that causes itself, but not the
ordinary two-hop cycle A caused-by B caused-by A, which would spin the walk
forever on the review thread.

Replace it with a depth bound, which terminates on any cycle regardless of
shape and is simpler to read. The bound is 16 against a real chain of 2
(CompletionException over the service exception), so no genuine truncation can
be missed for depth.

Add a test that drives a two-hop cycle through the batch path under a
preemptive timeout — the case the old guard let through — and asserts the
batch falls back to the ordinary soft-fail path.

Refs #492
@thrillhousebot

thrillhousebot Bot commented Aug 9, 2026

Copy link
Copy Markdown

🤖 ThrillhouseBot PR Summary

What this PR does

Detects responses truncated at the model's max-output-tokens cap via finish_reason=length, raises a distinct AiResponseTruncatedException, and makes both retry layers (AiReviewService and FindingPipeline) decline to re-run the deterministic failing call, disclosing the affected batch's files as not reviewed once instead of paying roughly ten futile billed calls.

Control-Flow Diagram

🔀 Show diagram
flowchart TD
  A["AiReviewService: streaming onCompleteResponse fires"] --> B{"finishReason == LENGTH?"}
  B -- "no" --> C["parse JSON and complete normally"]
  B -- "yes" --> D["completeExceptionally(AiResponseTruncatedException)"]
  D --> E["runWithRetries catches truncation; logs; rethrows, no retry, no broadcast"]
  E --> F["reviewBatch future fails with truncation in cause chain"]
  F --> G{"FindingPipeline: isResponseTruncated walks cause chain?"}
  G -- "yes" --> H["recordUncoveredFiles; skip batch retry; disclose files as not reviewed"]
  G -- "no" --> I["failedIndices; batch retry path"]
Loading

Changes Overview

  • Files changed: 6
  • Lines added: +324
  • Lines removed: -6

Changed Files

File Change Summary
src/main/java/dev/thiagogonzaga/thrillhousebot/review/FindingPipeline.java Modified Skips the batch retry for truncation-capped failures, records the batch's files as uncovered, and adds a bounded cause-chain walk (MAX_CAUSE_DEPTH=16).
src/main/java/dev/thiagogonzaga/thrillhousebot/review/ai/AiResponseTruncatedException.java Added New exception type for deterministic length-cap truncations, extending AiReviewException so it survives CompletableFuture unwrapping.
src/main/java/dev/thiagogonzaga/thrillhousebot/review/ai/AiReviewService.java Modified Detects finish_reason=length in the streaming completion handler and rethrows the new truncation exception without retrying or broadcasting retry events.
src/test/java/dev/thiagogonzaga/thrillhousebot/review/FindingPipelineTest.java Modified Adds tests: truncated batch is not retried and files are disclosed; the cyclic cause-chain test fixture is actually acyclic (see finding).
src/test/java/dev/thiagogonzaga/thrillhousebot/review/ai/AiReviewServiceTest.java Modified Adds tests: a truncated response is attempted exactly once and no retry/stream-failed events are broadcast.
src/test/java/dev/thiagogonzaga/thrillhousebot/review/ai/TruncatedTokenStream.java Added Fake TokenStream emitting a ChatResponse cut mid-JSON with finishReason LENGTH; counts start() calls so tests can assert call cost.

Risk Assessment

Risk Count
🔴 Critical 0
🟠 High 0
🟡 Medium 2
🔵 Low 1

Things to double-check

3 lower-confidence findings
  • MEDIUM: Cycle-guard test fixture is not actually cyclic (src/test/java/dev/thiagogonzaga/thrillhousebot/review/FindingPipelineTest.java:264) (low confidence — verify before acting)
  • MEDIUM: Verify OpenAI streaming populates finishReason on ChatResponse (src/main/java/dev/thiagogonzaga/thrillhousebot/review/ai/AiReviewService.java:267) (low confidence — verify before acting)
  • LOW: Verify AiResponseTruncatedException survives reviewBatch propagation (src/main/java/dev/thiagogonzaga/thrillhousebot/review/FindingPipeline.java:202) (low confidence — verify before acting)

Automated review by ThrillhouseBot. Reply with /review to re-run.

@thrillhousebot thrillhousebot Bot 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.

ThrillhouseBot noted 3 lower-confidence item(s) under Things to double-check in the PR summary (not posted as inline threads):

  • MEDIUM: Cycle-guard test fixture is not actually cyclic (src/test/java/dev/thiagogonzaga/thrillhousebot/review/FindingPipelineTest.java:264)
    multiCallSurvivesACyclicCauseChainOnAFailedBatch claims to test the MAX_CAUSE_DEPTH protection with a cycle 'A caused-by B caused-by A', but the fixture builds a linear chain. The anonymous 'outer' returns tail; the anonymous tail returns head; head is a plain new RuntimeException("head") with no cause, so head.getCause() is null. The chain outer -> tail -> head -> null terminates in 3 steps, so isResponseTruncated returns false without ever reaching the depth bound. The test would pass identically if MAX_CAUSE_DEPTH and the whole bound were deleted, so it provides no protection for the cycle guard this PR highlights as a design detail. Make the fixture genuinely cyclic (e.g. tail.getCause() returns this) so the guard is actually exercised.
  • MEDIUM: Verify OpenAI streaming populates finishReason on ChatResponse (src/main/java/dev/thiagogonzaga/thrillhousebot/review/ai/AiReviewService.java:267)
    The service-level detection depends entirely on response.finishReason() == FinishReason.LENGTH firing when the real model hits the cap. The new tests prove only the handling code: TruncatedTokenStream fabricates the trigger with ChatResponse.builder().finishReason(FinishReason.LENGTH), so the production path is exercised only against a fake. If the langchain4j/OpenAI streaming integration does not populate finishReason on the ChatResponse delivered to onCompleteResponse (e.g. it arrives null), this changed line never executes under the real trigger: the cut body still fails to parse and still lands in the transient-retry path, making the fix a silent no-op. This is a verification request: run the real OpenAiStreamingChatModel with a capped max_tokens and confirm the completion handler receives finishReason == LENGTH, or add an integration test that reproduces the provider response without fabricating it.
  • LOW: Verify AiResponseTruncatedException survives reviewBatch propagation (src/main/java/dev/thiagogonzaga/thrillhousebot/review/FindingPipeline.java:202)
    The pipeline guard only changes behavior when the CompletionException cause chain contains AiResponseTruncatedException. The batch-layer test fabricates this by stubbing aiReviewService.reviewBatch(...) with .thenThrow(new AiResponseTruncatedException(...)), so the real propagation path — service.review up through AiReviewService.reviewBatch and into the CompletableFuture/join() — is never exercised. The PR's design note covers asAiReviewException preserving an AiReviewException cause inside the service, but not whether reviewBatch itself rethrows the exception with its cause chain intact; if reviewBatch replaces it with a new exception that discards the original cause, isResponseTruncated returns false and the truncated batch still receives a futile batch-level retry. Verification request: confirm reviewBatch propagates the truncation with its cause chain intact, or add a test that goes through the real reviewBatch instead of mocking it.

The fixture built a -> b -> c where c was a plain exception, so the chain ended
at null after three hops. The walk terminated on `cause != null` and the depth
bound was never reached: the test asserted termination that would have happened
without the bound, and passed against the very defect it was written for.

Build a true two-node cycle through a holder so the chain never reaches null.
With the bound removed the test now fails as it should:

  FindingPipelineTest.multiCallSurvivesACyclicCauseChainOnAFailedBatch:252
    execution timed out after 10000 ms

Codecov found this, not review: the missed branch was `depth < MAX_CAUSE_DEPTH`
being false, which is exactly the case the fixture could not produce.

Refs #492

@thrillhousebot thrillhousebot Bot 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.

No new issues in this revision, but 2 previous finding(s) remain unresolved — fix them, or reply on their review thread (where one exists) with why they are deferred.

@sonarqubecloud

Copy link
Copy Markdown

@devops-thiago
devops-thiago merged commit 85c0812 into release/v0.6.0 Aug 10, 2026
14 checks passed
@devops-thiago
devops-thiago deleted the fix/492-truncation-detection branch August 10, 2026 00:44
devops-thiago added a commit that referenced this pull request Aug 10, 2026
…cise named model (#507)

## What type of PR is this?

- [x] ✨ Feature

> **Stacked on #506.** Base is `fix/505-audit2-drift`, not
`release/v0.6.0` — purely to keep the stack ordered; #506 is
docs/log-only and nothing here depends on it functionally. GitHub will
retarget the base to `release/v0.6.0` automatically when #506 merges —
but this repo squash-merges, so after that retarget this branch still
carries #506's original commit and needs `git rebase --onto
release/v0.6.0 a7b7fa6` before its own merge. **Review #506 first.**

## Description

Fixes #498.

`max-output-tokens` was applied once, on the model builders, so every AI
call shared one response cap. That is the wrong granularity: a **batch
review** response scales with finding count and is the call that
legitimately needs a large allowance; the **summary** returns one
fixed-shape object plus `previous_findings_status`; the **verifier**
returns a verdict per finding; a **maintainer reply** is short prose.
Sizing the shared cap for the batch call licensed every other call to
run orders of magnitude longer than it should — e.g. the shipped
`deepseek-v4-flash` entry sets `max-output-tokens=384000`, and until now
the summary call carried all of it.

### Shape: one named model, `concise`

The summary, verifier, and reply calls now bind to a `concise` named
model:

- **`PrSummarizer`** (new interface, `@RegisterAiService(modelName =
"concise")`) — `summarizeStream` moved verbatim from `PrReviewer` (same
`PrReviewPrompts.SUMMARY_*` constants, same `@V` slots), because one
`@RegisterAiService` interface is one model binding.
- **`FindingVerifier`** and **`ReplyAssistant`** — rebound with
`modelName = "concise"`; no signature change, so their callers are
untouched.
- **`quarkus.langchain4j.openai.concise.*`** aliases the **same** `AI_*`
env vars (api-key, base-url, model-name, timeout, log flags) — named
configs inherit nothing from the default block
(`OpenAiRecorder.correspondingOpenAiConfig` is `isDefault(name) ?
defaultConfig() : namedConfig().get(name)`, no fallback), so the aliases
are what keep operators configuring one provider. Note the namespace is
provider-first: a first boot attempt with
`quarkus.langchain4j.concise.openai.*` failed with `SRCFG00014: The
config property quarkus.langchain4j.openai.concise.api-key is required`.
- **`REVIEW_CONCISE_MAX_OUTPUT_TOKENS`** (default **8192**) feeds
`quarkus.langchain4j.openai.concise.chat-model.max-tokens`. 8192 is
roomy for these calls — a 50-finding review verifies in well under 4k
tokens, a summary is one object, a reply a few paragraphs — while an
order of magnitude under batch-sized caps. Empty drops the cap (provider
default).

**Deliberately NOT moved:** the review call and the command generators
(`/describe`, `/changelog`, `/add-docs`, `/improve`, `/generate-tests`,
including the describe/changelog reduce steps). Their outputs scale with
the diff — a large PR legitimately produces a long description,
changelog, docstring set, or test file — so capping them at a fixed
"concise" size would trade one wrong granularity for another. They stay
on the default model's `max-output-tokens`.

### The customizer-binding question, settled with evidence

Whether the existing `ChatModelCustomizers` would reach (and stomp) the
named model was verified against the deployed 1.12.2 jars (`javap -c` on
`ModelBuilderCustomizer` and the `OpenAiRecorder` apply-functions):

- `applyCustomizers(instance, builder, name)` selects customizer beans
**by CDI qualifier**: `Default.Literal` for the default model,
`ModelName.Literal.of(name)` for a named one. **No inheritance in either
direction.**
- The recorder runs customizers **after** the config properties,
immediately before `build()` — a customizer wins over config.
- In 1.12.2 both the blocking **and** streaming recorders apply
`ChatModelConfig.maxTokens()`, so the named block's
`chat-model.max-tokens` reaches both concise beans (the 1.11.2
blocking-only gap that motivated the customizer route does not bite
here).

Consequences implemented: the unqualified customizers **cannot** stomp
the concise cap (they never run for the named model), but the named
model would silently lose
reasoning-effort/temperature/top-p/penalties/seed. So
`ChatModelCustomizers` gains a `@ModelName("concise")` pair that applies
every shared parameter **except** `maxTokens` — applying the active
model's `max-output-tokens` there would overwrite the concise cap, since
customizers run last. `ChatModelWiringTest` pins all of it end-to-end.

### Boot-time validation and log

`StartupConfigValidator` rejects `REVIEW_CONCISE_MAX_OUTPUT_TOKENS < 1`
at boot (fail-fast, naming the env var, like the other budget keys;
empty = uncapped is allowed) and logs the concise cap next to the
per-model settings line, so the boot an operator debugs states which cap
the summary/verifier/reply calls run under.

## Related Issues

Fixes #498

Stacked on #506 (#505). Earlier in the stack: #495 (#492), #504 (#497).
Next: #499, then #500.

## How Has This Been Tested?

- [x] Unit tests

**Red/green.** All test changes were written against the structural move
only (interfaces split and rebound, named block aliased — no cap
property, no concise customizers, no validation), then the behavior was
added. Red, verbatim:

```
[ERROR] Failures:
[ERROR]   StartupConfigValidatorTest.failsFastWhenConciseResponseCapBelowOne:300->assertFailsValidation:213 Expected dev.thiagogonzaga.thrillhousebot.config.ConfigValidationException to be thrown, but nothing was thrown.
[ERROR]   ChatModelDefaultOffTest.conciseModelsCarryOnlyTheirResponseCapByDefault:75 the concise default cap must apply ==> expected: <8192> but was: <null>
[ERROR]   ChatModelWiringTest.conciseBlockingModelCarriesTheConciseCapAndTheSharedTuning:91 the concise response cap must apply, not the active model's max-output-tokens ==> expected: <8192> but was: <null>
[ERROR]   ChatModelWiringTest.conciseStreamingModelCarriesTheConciseCapAndTheSharedTuning:110 the concise response cap must apply, not the active model's max-output-tokens ==> expected: <8192> but was: <null>
[ERROR] Tests run: 72, Failures: 4, Errors: 0, Skipped: 0
```

Green with the cap property + concise customizers + validator rule in
place. The wiring tests also assert the default models still carry the
per-model `max-output-tokens` (4096 in the profile) while the concise
beans show 8192 plus the shared `reasoning_effort`/temperature/top-p —
"summary/verifier/reply carry the concise cap while batch review keeps
the model cap" pinned in one class.

**Truncation detection survives the rebinding**
(`ConciseModelTruncationTest`, new; green-only guard — detection itself
is #495/#504 behavior and was never red here): drives the real
`AiReviewService.summarize`, `FindingVerifier.verify`, and
`ReplyAssistant.reply` against `@InjectMock @ModelName("concise")` model
beans returning `finish_reason: length`, and asserts
`AiResponseTruncatedException` naming the knob. Because the mocks are
the **concise-qualified** beans, these tests double as proof the
services really bind to the named model — a silent fallback to the
default model would leave the stubs unhit and fail.

**Existing test files touched, and why** (every prior assertion
preserved; nothing disabled or deleted):

- `AiReviewServiceTest` — `AiReviewService` gained the `PrSummarizer`
dependency: new `@Mock`, the summarize test stubs/verifies
`prSummarizer` instead of `prReviewer`, and the two explicit `new
AiReviewService(...)` constructions gained the argument. Mechanical.
- `AiServicePromptRenderingTest` — reply/verify/summary rendering is now
captured off `@InjectMock @ModelName("concise")` model mocks (the
default-model mocks no longer see those calls); the capture helpers
gained a model parameter, with the old single-argument overloads
delegating for the unchanged services. Assertions untouched.
- `AiServiceUserMessagePlacementTest` — added the `PrSummarizer`
structural check (same guard every other AI service has).
- `ChatModelWiringTest` / `ChatModelDefaultOffTest` — extended with the
concise-bean assertions above (the red phase); the existing
default-model assertions are unchanged.
- `ChatModelCustomizersTest` — new cases for the concise customizer
pair: shared tuning applied, `maxTokens` never called
(`verifyNoMoreInteractions` with a per-model `max-output-tokens=384000`
present in settings).
- `StartupConfigValidatorTest` — `ConfigBuilder` passes the new
constructor argument (default `Optional.of(8192)`); new red/green case
for `< 1` rejection plus an empty-allowed case.

Gates on the final tree:

- `./mvnw -B spotless:apply` / `spotless:check` — clean
- `./mvnw -B clean compile spotbugs:check` — `BugInstance size is 0`,
BUILD SUCCESS
- `./mvnw -B clean test` — **Tests run: 2466, Failures: 0, Errors: 0,
Skipped: 0** (baseline 2454 + 12 new)
- Coverage: `jacoco.xml` intersected with this PR's added
`src/main/java` lines — **no uncovered added lines**

## Checklist

- [x] My code follows the project's coding standards
- [x] I have performed a self-review of my own code
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the documentation accordingly
- [x] My changes generate no new warnings or errors

## Additional Notes

- **Behavior change for every deployment:** the summary/verifier/reply
calls now send `max_tokens: 8192` even where nothing was configured
before (previously they were uncapped unless `max-output-tokens` was
set). That is the point of the issue — the cap exists for the runaway
case — and 8192 is far above what these calls produce; if it is ever hit
it surfaces loudly as the #492-style truncation error naming
`REVIEW_CONCISE_MAX_OUTPUT_TOKENS`, and setting the variable empty
restores the uncapped behavior.
- The issue sketched per-request parameters as one possible mechanism.
#504 already established that #497's `Result` plumbing and per-request
parameters don't fall out of one change; the named-model route gets
per-call-class caps entirely in configuration and bindings (no signature
churn through the assistants), at the cost of one extra model bean pair
— and the config block documents the no-inheritance trap it steps
around.
- The issue asked for "the per-model `max-output-tokens` as the default
when a call-specific value is not set". With a named model that exact
fallback would mean conditionally re-applying the active model's cap in
the concise customizer when the env var is unset — reintroducing the
stomping hazard for the configured case. The shipped shape instead gives
the concise cap its own always-present default (8192) and a documented
empty-to-uncap escape; the operator-visible contract (batch cap never
leaks onto the summary; no hardcoded per-call numbers in code) is
preserved, and the boot log states the effective value.
- `website/` versioned docs (0.4.0) deliberately untouched; README,
`.env.example`, and `application.properties` are the living config
surface.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working java Pull requests that update java code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant