Skip to content

fix(llm): honour configured max_tokens on AI analysis paths - #411

Open
dpage wants to merge 4 commits into
mainfrom
claude/llama-cpp-token-limit-da27aq
Open

fix(llm): honour configured max_tokens on AI analysis paths#411
dpage wants to merge 4 commits into
mainfrom
claude/llama-cpp-token-limit-da27aq

Conversation

@dpage

@dpage dpage commented Aug 11, 2026

Copy link
Copy Markdown
Member

Fixes #399

Problem

Three AI-analysis paths hardcoded a small output-token cap that ignored the operator's configured llm.max_tokens:

Path Old value
Estate summary (overview/generator.go) llmMaxTokens = 512
Server-info AI analysis (api/server_info_handlers.go) llmAnalysisMaxTokens = 512
Alerter reasoning (alerter/.../llm/reasoning.go) pgllm.Int(500)

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 pgllm.BlockText blocks 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-lib migration and the BuildClientOptions extraction; both of those were behaviour-preserving refactors that carried the values through unchanged.

Changes

  • Resolve the output 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 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.
  • Treat a response carrying no usable text as an error (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.
  • Map that condition to 502 on GET /api/v1/server-info/{id}/ai-analysis, documented in openapi.go and the regenerated openapi.json.
  • Add llm.max_tokens to the alerter, which had no equivalent setting, with a 4096 default plus the config merge and defaulting passes.

Note on the exported API

BuildClientOptions changes 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 return 200 with a null body meaning "analysis unavailable". Only the "model answered but produced nothing usable" case becomes an error. Turning a provider outage into a 502 would reach well beyond this issue.

The web client's existing .catch in ServerInfoDialog.tsx already 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_tokens value 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):

llmproxy: AnalysisMaxTokens 100.0%  BuildClientOptions 100.0%
overview: extractTextFromResponse 100.0%  generateSummaryFromPrompt 100.0%  createLLMClient 100.0%
api:      getAIAnalysis 100.0%  writeAIAnalysisResponse 100.0%  createLLMClient 100.0%
          parseDatabaseAnalysisResponse 94.1%  handleServerInfoAI 91.3%
alerter:  Classify 100.0%  newLibReasoning 100.0%  NewReasoningProvider 100.0%
          SetLLMDefaults 100.0%  NewConfig 100.0%  LoadFromFile 100.0%

The single uncovered statement in parseDatabaseAnalysisResponse is in the pre-existing numbered-list-prefix stripper, untouched here.

gofmt -l is clean. cd server && make test and cd alerter && make test both pass. go test ./internal/api/ -run OpenAPI -v passes.

make test-all could not complete locally: the collector's lint step fails because the installed golangci-lint is 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

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
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 15 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6d21975d-9273-4b7e-9836-20f4dc22e425

📥 Commits

Reviewing files that changed from the base of the PR and between 19c645d and f75d571.

📒 Files selected for processing (20)
  • .claude/golang-expert/testing-strategy.md
  • alerter/src/internal/config/config.go
  • alerter/src/internal/config/config_test.go
  • alerter/src/internal/llm/llm.go
  • alerter/src/internal/llm/reasoning.go
  • alerter/src/internal/llm/reasoning_test.go
  • docs/admin-guide/api/openapi.json
  • docs/admin-guide/api/server-info.md
  • docs/changelog.md
  • docs/getting-started/configuration/alerter.md
  • docs/getting-started/configuration/server.md
  • examples/ai-dba-alerter.yaml
  • examples/ai-dba-server.yaml
  • server/src/internal/api/openapi.go
  • server/src/internal/api/server_info_handlers.go
  • server/src/internal/api/server_info_handlers_test.go
  • server/src/internal/llmproxy/proxy.go
  • server/src/internal/llmproxy/proxy_test.go
  • server/src/internal/overview/generator.go
  • server/src/internal/overview/generator_test.go

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

@codacy-production

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 50 complexity · 2 duplication

Metric Results
Complexity 50
Duplication 2

View in Codacy

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.

claude added 3 commits August 11, 2026 21:49
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
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.

AI analysis paths hardcode a 512-token output cap, producing empty summaries with local reasoning models

2 participants