Skip to content

Commit 7900e4e

Browse files
Arnd Kohrsclaude
authored andcommitted
summarize: raise output-token budget to 16k for reasoning models
Reasoning models (MiniMax M-series, Claude with thinking) emit a variable <think> block before the JSON, overrunning the ~4000-token provider default and truncating the structured output. Forward an explicit MAX_OUTPUT_TOKENS=16000 ceiling (override via PODMIND_MAX_OUTPUT_TOKENS); non-thinking models like the DeepSeek default still stop at ~1.3k, so cost is unchanged for them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent d3fa60f commit 7900e4e

2 files changed

Lines changed: 58 additions & 1 deletion

File tree

bin/summarize.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import argparse
99
import asyncio
1010
import json
11+
import os
1112
import sys
1213
from pathlib import Path
1314

@@ -22,6 +23,15 @@
2223
RESULTS = Path("/tmp/podmind-results")
2324
RESULTS.mkdir(exist_ok=True)
2425

26+
# Output-token ceiling for the summary call. The structured JSON is only ~1-2k
27+
# tokens, but reasoning models (MiniMax M-series, Claude with thinking) spend a
28+
# large, variable amount of the budget on a <think> block first — a 4000 cap
29+
# truncated the JSON for ~25-50% of episodes on MiniMax-M3 (some needed >9000).
30+
# This is a ceiling, not a target: non-thinking models (DeepSeek) still stop at
31+
# ~1.3k, so the higher cap doesn't change their cost. Override per-vault if
32+
# needed.
33+
MAX_OUTPUT_TOKENS = int(os.environ.get("PODMIND_MAX_OUTPUT_TOKENS", "16000"))
34+
2535
PROMPT = """You are extracting a structured summary of a podcast episode for a knowledge wiki.
2636
2737
Read the meta and transcript below. Produce a single JSON object with these exact fields. Do not wrap in markdown fences. Do not include any text before or after the JSON.
@@ -104,7 +114,7 @@ async def process_one(client: httpx.AsyncClient, provider: llm.LLMProvider, item
104114
async with sem:
105115
prompt = build_prompt(item)
106116
try:
107-
content, usage = await provider.achat(client, prompt)
117+
content, usage = await provider.achat(client, prompt, max_tokens=MAX_OUTPUT_TOKENS)
108118
except llm.LLMError as e:
109119
print(f" [error] {item['raw_dir']}: {e}", file=sys.stderr)
110120
return item["idx"], False, llm.Usage()

tests/test_review_fixes.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,53 @@ def test_placeholder_in_meta_not_substituted(self, tmp_path: Path):
7979
assert "raw_dir: test-show/2026-01-01-test-ep" in prompt
8080

8181

82+
# ---------- summarize output-token budget (reasoning-model fix) ----------
83+
84+
class TestMaxOutputTokens:
85+
def test_default_budget_is_16000(self):
86+
import summarize
87+
assert summarize.MAX_OUTPUT_TOKENS == 16000
88+
89+
def test_env_override(self, monkeypatch):
90+
import importlib
91+
import summarize
92+
monkeypatch.setenv("PODMIND_MAX_OUTPUT_TOKENS", "5000")
93+
importlib.reload(summarize)
94+
try:
95+
assert summarize.MAX_OUTPUT_TOKENS == 5000
96+
finally:
97+
monkeypatch.delenv("PODMIND_MAX_OUTPUT_TOKENS", raising=False)
98+
importlib.reload(summarize) # restore default for other tests
99+
100+
def test_process_one_forwards_budget_to_achat(self, tmp_path, monkeypatch):
101+
import asyncio
102+
103+
import summarize
104+
from podmind import llm, paths
105+
106+
d = paths.EPISODES_DIR / "test-show" / "2026-01-01-budget-ep"
107+
d.mkdir(parents=True, exist_ok=True)
108+
(d / "meta.json").write_text(json.dumps({"title": "T", "show": "S", "duration_sec": 60}))
109+
(d / "transcript.md").write_text("hello world transcript")
110+
monkeypatch.setattr(summarize, "RESULTS", tmp_path) # don't touch /tmp/podmind-results
111+
item = {"raw_dir": "test-show/2026-01-01-budget-ep", "idx": "01",
112+
"show_slug": "test-show", "date": "2026-01-01"}
113+
114+
seen = {}
115+
116+
class FakeProvider:
117+
async def achat(self, client, prompt, **kwargs):
118+
seen["max_tokens"] = kwargs.get("max_tokens")
119+
return '{"hook": "x", "takeaways": []}', llm.Usage(10, 0, 5)
120+
121+
async def go():
122+
return await summarize.process_one(None, FakeProvider(), item, asyncio.Semaphore(1))
123+
124+
idx, ok, _usage = asyncio.run(go())
125+
assert ok is True
126+
assert seen["max_tokens"] == summarize.MAX_OUTPUT_TOKENS == 16000
127+
128+
82129
# ---------- tick_finalize.to_episode resilience ----------
83130

84131
class TestToEpisodeResilience:

0 commit comments

Comments
 (0)