Skip to content

Commit f859179

Browse files
committed
fix(llm): stop sending stage OUTPUT_RATIO as max_tokens
Each LLM stage was passing floor(input_tokens * OUTPUT_RATIO) as the chat completions max_tokens, hard-capping clean output server-side (e.g. CLEAN_OUTPUT_RATIO=0.5 truncated clean output at half input). OUTPUT_RATIO now only reserves output budget for context-window eligibility, as documented.
1 parent f91ca6d commit f859179

9 files changed

Lines changed: 145 additions & 159 deletions

File tree

.env.example

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ CLEAN_ENABLED=true
6666
CLEAN_MIN_INPUT_CHARS=500
6767
# Hard cap on clean input size. 0 means no cap.
6868
CLEAN_MAX_INPUT_CHARS=75000
69-
# Output token budget as a fraction of input tokens.
69+
# Output token budget reserved as a fraction of input tokens for context-window eligibility.
7070
CLEAN_OUTPUT_RATIO=0.5
7171
# Per-call timeout in seconds.
7272
CLEAN_TIMEOUT_SECONDS=60
@@ -82,7 +82,7 @@ SUMMARIZE_ENABLED=false
8282
SUMMARIZE_MIN_INPUT_CHARS=25000
8383
# Hard cap on summarize input size. 0 means no cap.
8484
SUMMARIZE_MAX_INPUT_CHARS=0
85-
# Output token budget as a fraction of input tokens.
85+
# Output token budget reserved as a fraction of input tokens for context-window eligibility.
8686
SUMMARIZE_OUTPUT_RATIO=0.15
8787
# Per-call timeout in seconds.
8888
SUMMARIZE_TIMEOUT_SECONDS=60

AGENTS.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,7 @@ The LLM provider should work with local OpenAI-compatible servers such as llama.
106106
"messages": [
107107
{"role": "system", "content": "..."},
108108
{"role": "user", "content": "..."}
109-
],
110-
"max_tokens": 8192
109+
]
111110
}
112111
```
113112

README.md

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -235,15 +235,13 @@ The tables below list only the **commonly tuned** keys. Open `.env.example` for
235235
| `LLM_CONCURRENCY` | `1` | Max concurrent per-URL LLM workflows. |
236236
| `LLM_EXTRA_BODY` | empty | Example: `{"temperature":0.7, ...}`. Any key wins. |
237237

238-
LLM calls intentionally use the lowest common denominator: Chat Completions, `system` and `user` messages only, and `max_tokens` derived from the stage output ratio. No sampler defaults are sent; operators set them through `LLM_EXTRA_BODY` when needed. No Responses API. No `developer` role. This keeps the provider usable with llama.cpp, vLLM, SGLang, and similar local servers.
239-
240238
### Stages
241239

242-
| Variable | Default | Notes |
243-
| ----------------------- | ------- | -------------------------------------------------------------------------------------------------- |
244-
| `CLEAN_ENABLED` | `true` | Enables the clean stage. |
245-
| `SUMMARIZE_ENABLED` | `false` | Enables the summarize stage. |
246-
| `TRUNCATE_TARGET_CHARS` | `25000` | Final body cap before footer is appended. `0` disables truncation. |
240+
| Variable | Default | Notes |
241+
| ----------------------- | ------- | ----------------------------------------------------------------------|
242+
| `CLEAN_ENABLED` | `true` | Enables the clean stage. |
243+
| `SUMMARIZE_ENABLED` | `false` | Enables the summarize stage. |
244+
| `TRUNCATE_TARGET_CHARS` | `25000` | Final body cap before footer is appended. `0` disables truncation. |
247245

248246
Each stage exposes the same knobs under a shared naming convention: `<STAGE>_ENABLED`, `<STAGE>_MIN_INPUT_CHARS`, `<STAGE>_MAX_INPUT_CHARS`, `<STAGE>_OUTPUT_RATIO`, `<STAGE>_TIMEOUT_SECONDS`, `<STAGE>_QUALITY_MIN_RATIO`, `<STAGE>_CHECK_URLS`. See `.env.example` for the full list and per-stage defaults.
249247

src/config/config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ const EnvSchema = z.object({
149149
// Units: characters. Range: integer >= 0; 0 means no limit. Default: 75000.
150150
CLEAN_MAX_INPUT_CHARS: intish(75000).pipe(z.number().int().min(0)),
151151

152-
// Output token budget ratio used for clean stage max_tokens and eligibility.
152+
// Output token budget ratio reserved for the clean stage in context-window eligibility projection.
153153
// Units: ratio. Range: number >= 0. Default: 0.5.
154154
CLEAN_OUTPUT_RATIO: numberish(0.5).pipe(z.number().min(0)),
155155

@@ -177,7 +177,7 @@ const EnvSchema = z.object({
177177
// Units: characters. Range: integer >= 0; 0 means no limit. Default: 0.
178178
SUMMARIZE_MAX_INPUT_CHARS: intish(0).pipe(z.number().int().min(0)),
179179

180-
// Output token budget ratio used for summarize-stage max_tokens and eligibility.
180+
// Output token budget ratio reserved for the summarize stage in context-window eligibility projection.
181181
// Units: ratio. Range: number >= 0. Default: 0.15.
182182
SUMMARIZE_OUTPUT_RATIO: numberish(0.15).pipe(z.number().min(0)),
183183

src/core/ports/llm-provider.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,7 @@ export interface LlmMessage {
1010
}
1111

1212
export interface LlmChatOptions {
13-
/** Max output tokens budget for the request. */
14-
maxTokens?: number;
15-
16-
/** Request timeout in milliseconds. */
13+
/** Request timeout in milliseconds. Hard `max_tokens` caps belong in `LLM_EXTRA_BODY`. */
1714
timeoutMs?: number;
1815
}
1916

src/core/use-cases/llm-stage/llm-stage.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { QualityRejectReason } from "../../cleanup/quality-gate.js";
44
import type { TemplateRenderer } from "../../cleanup/templates.js";
55
import type { LlmProvider } from "../../ports/llm-provider.js";
66

7-
import { checkEligibility, compactChars, estimateTokens } from "../../cleanup/length-policy.js";
7+
import { checkEligibility, compactChars } from "../../cleanup/length-policy.js";
88
import { assessQuality } from "../../cleanup/quality-gate.js";
99
import { reasonFromError } from "../../util/errors.js";
1010

@@ -120,9 +120,6 @@ export class LlmStage {
120120
};
121121
}
122122

123-
const inputTokens = estimateTokens(inputChars, this.deps.app.LLM_CHARS_PER_TOKEN);
124-
const maxTokens = Math.max(1, Math.floor(inputTokens * config.outputRatio));
125-
126123
let text: string;
127124
let model: string | undefined;
128125
try {
@@ -132,7 +129,6 @@ export class LlmStage {
132129
{ role: "user", content: userPrompt }
133130
],
134131
{
135-
maxTokens,
136132
timeoutMs: config.timeoutMs
137133
}
138134
);

src/providers/llm/openai-chat/openai-chat-llm.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { joinUrl } from "../../../core/util/urls.js";
1313
// Handles untrusted external content at the response parsing security boundary.
1414

1515
const DEFAULT_TIMEOUT_MS = 60_000;
16-
const DEFAULT_MAX_TOKENS = 4096;
1716

1817
/** Slugify upstream error codes before interpolating them in AppError codes. */
1918
function sanitizeUpstreamCode(value: unknown): string {
@@ -57,7 +56,6 @@ export class OpenAiChatLlmProvider implements LlmProvider {
5756
/** Execute one chat request and return normalized model text output. */
5857
async chat(messages: LlmMessage[], options: LlmChatOptions = {}): Promise<LlmChatResult> {
5958
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
60-
const maxTokens = options.maxTokens ?? DEFAULT_MAX_TOKENS;
6159

6260
const controller = new AbortController();
6361
const timeout = setTimeout(() => controller.abort(), timeoutMs);
@@ -71,7 +69,6 @@ export class OpenAiChatLlmProvider implements LlmProvider {
7169
const body = {
7270
model: this.config.LLM_MODEL,
7371
messages,
74-
max_tokens: maxTokens,
7572
...this.config.LLM_EXTRA_BODY
7673
};
7774

0 commit comments

Comments
 (0)