diff --git a/docs/spec/04-analysis-writing.md b/docs/spec/04-analysis-writing.md index 8ac7ac0..1680184 100644 --- a/docs/spec/04-analysis-writing.md +++ b/docs/spec/04-analysis-writing.md @@ -85,6 +85,11 @@ knowledge נגזר; G2: מקור-האמת הוא הפרוטוקול + `legal_arg היו"ר כבר מילא תאריך — לא דורסים קלט-יו"ר). - **ייצור:** קריאת-ה-LLM ההשוואתית עוברת `claude_session` (מקומי בלבד), מעוגנת `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) diff --git a/mcp-server/src/legal_mcp/services/block_writer.py b/mcp-server/src/legal_mcp/services/block_writer.py index 024a260..028a097 100644 --- a/mcp-server/src/legal_mcp/services/block_writer.py +++ b/mcp-server/src/legal_mcp/services/block_writer.py @@ -58,6 +58,18 @@ logger = logging.getLogger(__name__) # 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. 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-alef": {"index": 1, "title": "כותרת מוסדית", "gen_type": "template-fill", "model": "script"}, @@ -461,17 +473,21 @@ async def write_block( if not dir_doc.get("approved"): raise ValueError("לא ניתן לכתוב בלוק דיון ללא כיוון מאושר. הפעל brainstorm → approve_direction קודם.") - # Guard against context overflow before calling claude -p. - # Sonnet: 200K context → ~800K chars max; Opus: 200K context → same. - # In practice the CLI has crashed on prompts above ~400K chars, so use - # that as a conservative ceiling (well below the token limit). - _MAX_PROMPT_CHARS = 400_000 + # Pick the model build by prompt size (#216). Above the 200K-token wall we + # escalate to the 1M-context build (`[1m]`) instead of failing the block — + # block-yod legitimately carries the whole case as source-context. The 400K + # ceiling was an artifact of the old 200K-only build, NOT a model limit. + 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: raise RuntimeError( 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"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 @@ -483,14 +499,14 @@ async def write_block( content = await claude_session.query( prompt, timeout=timeout, - model=GENERATION_MODEL, + model=gen_model, effort=effort, tools="", # prose gen — no tool_use → no error_max_turns ) sources = await _collect_block_sources(case_id, block_id) 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 # the measured distance to the effort that actually produced the text. if result.get("effort") is not None: @@ -499,7 +515,8 @@ async def write_block( 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()) is_ai = block_cfg["model"] == "ai" return { @@ -509,8 +526,9 @@ def _build_result(block_id: str, content: str, block_cfg: dict) -> dict: "content": content, "word_count": word_count, "generation_type": block_cfg["gen_type"], - # AI blocks record the pinned model; template blocks record "script". - "model_used": GENERATION_MODEL if is_ai else block_cfg["model"], + # AI blocks record the model build actually used (200K or [1m], #216); + # 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. "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 diff --git a/mcp-server/src/legal_mcp/services/claude_session.py b/mcp-server/src/legal_mcp/services/claude_session.py index 76d9146..1e07a2a 100644 --- a/mcp-server/src/legal_mcp/services/claude_session.py +++ b/mcp-server/src/legal_mcp/services/claude_session.py @@ -146,8 +146,15 @@ async def query( """ full_prompt = f"{system}\n\n{prompt}" if system else prompt - if len(full_prompt) > 150_000: - logger.warning("Large prompt: %d chars — may hit context limits", len(full_prompt)) + # ~350K chars ≈ the 200K-token wall of the default Opus build (~2 Hebrew + # 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 = [ "claude", "-p",