Merge pull request 'fix(block-writer): רתימת חלון-1M של Opus 4.8 לבלוק-י (#216)' (#389) from worktree-blockyod-1m into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m34s
G12 Leak-Guard / leak-guard (push) Successful in 4s
Lint — undefined names / undefined-names (push) Successful in 11s

This commit was merged in pull request #389.
This commit is contained in:
2026-06-30 20:40:48 +00:00
3 changed files with 44 additions and 14 deletions

View File

@@ -85,6 +85,11 @@ knowledge נגזר; G2: מקור-האמת הוא הפרוטוקול + `legal_arg
היו"ר כבר מילא תאריך — לא דורסים קלט-יו"ר). היו"ר כבר מילא תאריך — לא דורסים קלט-יו"ר).
- **ייצור:** קריאת-ה-LLM ההשוואתית עוברת `claude_session` (מקומי בלבד), מעוגנת - **ייצור:** קריאת-ה-LLM ההשוואתית עוברת `claude_session` (מקומי בלבד), מעוגנת
`model="claude-opus-4-8"` + `effort="high"` (ראה `reference_claude_generation_path`). `model="claude-opus-4-8"` + `effort="high"` (ראה `reference_claude_generation_path`).
**חלון-הקשר (#216):** הבנייה הסטנדרטית רצה 200K-טוקן (~400K תווים עברית). פרומפט גדול
(בעיקר בלוק-י, שנושא את התיק המלא כ-source-context) מוסלם אוטומטית לבניית-ה-1M של *אותו*
מודל — `claude-opus-4-8[1m]` (1M-טוקן, מחיר רגיל; `claude -p` חושף אותה ישירות כ-model-id,
בלי beta-header). **עידון של אותו pin, לא מסלול-מודל מקביל (G2).** הסף ב-`block_writer`:
פרומפט > ~350K תווים → `[1m]`; אחרת הבנייה הסטנדרטית. תקרת-קשיחה: 1.5M תווים.
### 1.4 סיכום-מנהלים של טענות הצדדים (מסמך-הכנה לדיון, WS3/#202) ### 1.4 סיכום-מנהלים של טענות הצדדים (מסמך-הכנה לדיון, WS3/#202)

View File

@@ -58,6 +58,18 @@ logger = logging.getLogger(__name__)
# Output token note (Anthropic): Opus 4.8 supports large outputs; streaming is # Output token note (Anthropic): Opus 4.8 supports large outputs; streaming is
# handled by the CLI. `max_tokens` is advisory context for callers, not sent. # handled by the CLI. `max_tokens` is advisory context for callers, not sent.
GENERATION_MODEL = "claude-opus-4-8" # single pinned model for every AI block (#204) GENERATION_MODEL = "claude-opus-4-8" # single pinned model for every AI block (#204)
# 1M-context build of the SAME pinned model (#216). The default build runs a
# 200K-token context (~400K Hebrew chars at ~2 chars/token); large prompts —
# notably block-yod, which carries the full case as source-context — overflow it.
# Opus 4.8 offers a 1M-token window at standard pricing; `claude -p` exposes it
# via the `[1m]` model id directly (no beta header — verified CLI 2.1.196). We
# escalate to it only when the prompt is large, so small blocks stay on the
# cheaper/faster 200K build. NOT a parallel model (G2) — same model, wider window.
GENERATION_MODEL_1M = "claude-opus-4-8[1m]"
# Escalate to the 1M build above this prompt size. Set below the 200K-token wall
# (~400K chars) with headroom for output tokens (max_tokens up to 16K) + tokenizer
# variance, so we switch before the standard build can overflow.
_CTX_1M_THRESHOLD_CHARS = 350_000
BLOCK_CONFIG = { BLOCK_CONFIG = {
"block-alef": {"index": 1, "title": "כותרת מוסדית", "gen_type": "template-fill", "model": "script"}, "block-alef": {"index": 1, "title": "כותרת מוסדית", "gen_type": "template-fill", "model": "script"},
@@ -461,17 +473,21 @@ async def write_block(
if not dir_doc.get("approved"): if not dir_doc.get("approved"):
raise ValueError("לא ניתן לכתוב בלוק דיון ללא כיוון מאושר. הפעל brainstorm → approve_direction קודם.") raise ValueError("לא ניתן לכתוב בלוק דיון ללא כיוון מאושר. הפעל brainstorm → approve_direction קודם.")
# Guard against context overflow before calling claude -p. # Pick the model build by prompt size (#216). Above the 200K-token wall we
# Sonnet: 200K context → ~800K chars max; Opus: 200K context → same. # escalate to the 1M-context build (`[1m]`) instead of failing the block —
# In practice the CLI has crashed on prompts above ~400K chars, so use # block-yod legitimately carries the whole case as source-context. The 400K
# that as a conservative ceiling (well below the token limit). # ceiling was an artifact of the old 200K-only build, NOT a model limit.
_MAX_PROMPT_CHARS = 400_000 gen_model = GENERATION_MODEL_1M if len(prompt) > _CTX_1M_THRESHOLD_CHARS else GENERATION_MODEL
# Final guard: even the 1M build is finite (~2M Hebrew chars of input). Cap at
# 1.5M chars (~750K tokens) to leave room for output + a safety margin under 1M.
_MAX_PROMPT_CHARS = 1_500_000
if len(prompt) > _MAX_PROMPT_CHARS: if len(prompt) > _MAX_PROMPT_CHARS:
raise RuntimeError( raise RuntimeError(
f"Prompt too large for {block_id}: {len(prompt):,} chars " f"Prompt too large for {block_id}: {len(prompt):,} chars "
f"(limit {_MAX_PROMPT_CHARS:,}). " f"(limit {_MAX_PROMPT_CHARS:,}, even on the 1M-context build). "
f"source_context: {len(source_context):,} chars. " f"source_context: {len(source_context):,} chars. "
f"Reduce documents or call extract_appraiser_facts first." f"Reduce source-context (summaries / appraiser_facts / focused RAG)."
) )
# Call Claude via Claude Code session (no API). #204: pin the model + per-block # Call Claude via Claude Code session (no API). #204: pin the model + per-block
@@ -483,14 +499,14 @@ async def write_block(
content = await claude_session.query( content = await claude_session.query(
prompt, prompt,
timeout=timeout, timeout=timeout,
model=GENERATION_MODEL, model=gen_model,
effort=effort, effort=effort,
tools="", # prose gen — no tool_use → no error_max_turns tools="", # prose gen — no tool_use → no error_max_turns
) )
sources = await _collect_block_sources(case_id, block_id) sources = await _collect_block_sources(case_id, block_id)
sources["case_law_ids"] = _precedent_case_law_ids sources["case_law_ids"] = _precedent_case_law_ids
result = _build_result(block_id, content, block_cfg) result = _build_result(block_id, content, block_cfg, model_used=gen_model)
# Record the EFFECTIVE effort (override wins) so the harness can attribute # Record the EFFECTIVE effort (override wins) so the harness can attribute
# the measured distance to the effort that actually produced the text. # the measured distance to the effort that actually produced the text.
if result.get("effort") is not None: if result.get("effort") is not None:
@@ -499,7 +515,8 @@ async def write_block(
return result return result
def _build_result(block_id: str, content: str, block_cfg: dict) -> dict: def _build_result(block_id: str, content: str, block_cfg: dict,
model_used: str = GENERATION_MODEL) -> dict:
word_count = len(content.split()) word_count = len(content.split())
is_ai = block_cfg["model"] == "ai" is_ai = block_cfg["model"] == "ai"
return { return {
@@ -509,8 +526,9 @@ def _build_result(block_id: str, content: str, block_cfg: dict) -> dict:
"content": content, "content": content,
"word_count": word_count, "word_count": word_count,
"generation_type": block_cfg["gen_type"], "generation_type": block_cfg["gen_type"],
# AI blocks record the pinned model; template blocks record "script". # AI blocks record the model build actually used (200K or [1m], #216);
"model_used": GENERATION_MODEL if is_ai else block_cfg["model"], # template blocks record "script".
"model_used": model_used if is_ai else block_cfg["model"],
# The real generation knob (#204). None for template/script blocks. # The real generation knob (#204). None for template/script blocks.
"effort": block_cfg.get("effort", DEFAULT_EFFORT) if is_ai else None, "effort": block_cfg.get("effort", DEFAULT_EFFORT) if is_ai else None,
# DEPRECATED: temperature is not a real knob on Opus 4.7/4.8 (sending it # DEPRECATED: temperature is not a real knob on Opus 4.7/4.8 (sending it

View File

@@ -146,8 +146,15 @@ async def query(
""" """
full_prompt = f"{system}\n\n{prompt}" if system else prompt full_prompt = f"{system}\n\n{prompt}" if system else prompt
if len(full_prompt) > 150_000: # ~350K chars ≈ the 200K-token wall of the default Opus build (~2 Hebrew
logger.warning("Large prompt: %d chars — may hit context limits", len(full_prompt)) # chars/token). Above it callers should pass the 1M-context model id
# (`claude-opus-4-8[1m]`) — block_writer escalates automatically (#216).
if len(full_prompt) > 350_000 and "[1m]" not in (model or ""):
logger.warning(
"Large prompt: %d chars on a non-[1m] model (%s) — may overflow the "
"200K-token context. Pass claude-opus-4-8[1m] for the 1M window.",
len(full_prompt), model or "CLI-default",
)
cmd = [ cmd = [
"claude", "-p", "claude", "-p",