fix(llm): honour configured max_tokens on AI analysis paths - #411
Conversation
The estate summary, the server-info AI database analysis, and the alerter's reasoning path each hardcoded a small output-token cap that ignored the operator's configured llm.max_tokens: 512, 512, and 500 respectively. The values survived the move to pgedge-go-llm-lib and the BuildClientOptions extraction, both of which were behaviour-preserving. Reasoning models served through an OpenAI-compatible endpoint charge their thinking tokens against the same max_tokens budget as the answer. A 512-token budget is routinely consumed in full by the thinking block, so the model never emits a text block. extractTextFromResponse collected only text blocks and returned an empty string, which callers treated as a successful generation, so the estate summary rendered blank with no error. This reproduces against a local llama.cpp server. Resolve the budget from configuration on all three paths, falling back to 4096 when the setting is unset, zero, or negative. BuildClientOptions now resolves max-tokens internally via the new nil-safe AnalysisMaxTokens rather than accepting it as a parameter; a parameter is what let the two analysis call sites drift into hardcoding their own constants, and removing it makes divergence impossible. Treat a response carrying no usable text as an error rather than an empty summary, so the condition surfaces instead of silently caching a blank panel. The server-info endpoint maps it to 502, which openapi.go now documents. The alerter had no max-tokens setting, so add llm.max_tokens with a 4096 default alongside the config merge and defaulting passes. Fixes #399 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Piu2z26Jjg1fuPrkSJ5aFq
|
Warning Review limit reachedYou’ve reached a temporary PR review limit under our Fair Usage Limits Policy. Next review available in: 15 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (20)
Comment |
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Complexity | 50 |
| Duplication | 2 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
Record that the server's llm.max_tokens now governs the AI overview and the server-info database analysis, and document the alerter's new llm.max_tokens setting with its default and its effect. Both sections explain that a reasoning model counts its thinking tokens against the same budget, which is the failure mode behind issue #399. Refs #399 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Piu2z26Jjg1fuPrkSJ5aFq
The golangci-lint misspell check enforces US spelling in Go source, and the new tests introduced British spellings in table-driven case labels, doc comments, and three test function names. CI failed the lint step on both the server and the alerter. Rename the affected identifiers and correct the strings. The change is confined to Go source; the markdown documentation keeps the British spelling its style guide prescribes. Refs #399 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Piu2z26Jjg1fuPrkSJ5aFq
Add the changelog entries for the alerter's new llm.max_tokens setting and for the blank-summary fix. Document the 502 response that the server-info AI analysis endpoint now returns when a model produces no usable text. Correct two inaccuracies found while writing: the server-info error table omitted 502, and it described a null response as covering only the AI-disabled and no-databases cases when a failed provider call also yields null. Widen the server example's llm banner, which described the section as the web client chat proxy although the provider and model settings have always driven the AI overview as well. Refs #399 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Piu2z26Jjg1fuPrkSJ5aFq
Fixes #399
Problem
Three AI-analysis paths hardcoded a small output-token cap that ignored the operator's configured
llm.max_tokens:overview/generator.go)llmMaxTokens = 512api/server_info_handlers.go)llmAnalysisMaxTokens = 512alerter/.../llm/reasoning.go)pgllm.Int(500)Reasoning models served through an OpenAI-compatible endpoint charge their thinking tokens against the same
max_tokensbudget as the answer. A 512-token budget is routinely consumed in full by the thinking block, so the model never emits a text block.extractTextFromResponsecollected onlypgllm.BlockTextblocks and returned an empty string, which callers treated as a successful generation. The estate summary therefore rendered blank with no error. This reproduces against a local llama.cpp server and was originally hit while building an all-in-one Docker stack.The constants predate the
pgedge-go-llm-libmigration and theBuildClientOptionsextraction; both of those were behaviour-preserving refactors that carried the values through unchanged.Changes
4096when the setting is unset, zero, or negative.BuildClientOptionsnow resolves max-tokens internally via the new nil-safeAnalysisMaxTokens()rather than accepting it as a parameter. A max-tokens parameter is exactly what let the two analysis call sites drift into hardcoding their own constants; removing it makes divergence impossible. Temperature stays a parameter because the two paths legitimately tune it.ErrNoTextContent) rather than an empty summary, so the condition surfaces instead of silently caching a blank panel. The error message names the likely cause and the setting to change.502onGET /api/v1/server-info/{id}/ai-analysis, documented inopenapi.goand the regeneratedopenapi.json.llm.max_tokensto the alerter, which had no equivalent setting, with a4096default plus the config merge and defaulting passes.Note on the exported API
BuildClientOptionschanges signature from(maxTokens int, temperature float64)to(temperature float64). Both call sites are internal to this repository and are updated here.Behaviour deliberately left unchanged
The transient failure paths in
getAIAnalysis(nil LLM config, no databases, client construction failure, chat error) still return200with anullbody meaning "analysis unavailable". Only the "model answered but produced nothing usable" case becomes an error. Turning a provider outage into a502would reach well beyond this issue.The web client's existing
.catchinServerInfoDialog.tsxalready logs and leaves the panel empty, so no client change is needed.Testing
Table-driven tests cover each path for: configured value used; fallback on unset, zero, and negative; a small configured value still honoured; empty, whitespace-only, reasoning-block-only, and nil responses all yielding an error; non-empty responses passing through unchanged; and the
max_tokensvalue actually observed on the wire, decoded from a stub server's request body.Per-function line coverage on every touched function (verified with
go tool cover -func):The single uncovered statement in
parseDatabaseAnalysisResponseis in the pre-existing numbered-list-prefix stripper, untouched here.gofmt -lis clean.cd server && make testandcd alerter && make testboth pass.go test ./internal/api/ -run OpenAPI -vpasses.make test-allcould not complete locally: the collector's lint step fails because the installedgolangci-lintis built with Go 1.25 while the config targets Go 1.26.2. This reproduces on a pristine tree with all changes stashed, so it is pre-existing and unrelated to this change.go vet ./...is clean on both modules as a substitute; CI is the real gate.🤖 Generated with Claude Code
https://claude.ai/code/session_01Piu2z26Jjg1fuPrkSJ5aFq
Generated by Claude Code