package pipeline import ( "context" "fmt" "path/filepath" "strings" "testing" "time" ) // maxtokens_floor_test.go pins the per-model max_tokens FLOOR (D24.3, // capabilities.min_max_tokens). The golden fixture uses only unfloored fake models, // so it does NOT exercise this behaviour — these dedicated tests do, mutation-verified: // - the floor lifts a small derived budget on the PRIMARY call (revert the stagerun // applyModelFloor line and TestMinMaxTokensFloorPrimary goes red); // - the escalation hop re-floors against ITS OWN model, not the inherited primary // budget (revert the maybeEscalate hopMaxTokens line and TestMinMaxTokensFloorHop // goes red — the exact stage-A bug where draft→deepseek-v4-pro hops inherited // 2048/3291 and returned finish=length); // - editing a floor is a loud --resnapshot (the floor is folded into the snapshot via // the resolved Capability), so TestMinMaxTokensFloorFoldedIntoSnapshot proves the // snapshotID moves when only the floor changes (invariant #2). // setupFloorProject writes a ja→ru book whose draft model carries a per-model floor and, // optionally, escalates to a floored fallback. Both models sit on the same fake provider; // the mock distinguishes them by the "model" field. A tiny source keeps the derived base // budget (EstimateTokens × ratio, floored at the small global min_max_tokens) well BELOW // the per-model floors, so a wire max_tokens at the floor can only come from the floor. func setupFloorProject(t *testing.T, providerURL string, draftFloor, hopFloor int, withEscalation bool) string { t.Helper() dir := t.TempDir() writeFile(t, filepath.Join(dir, "prompts", "translator.md"), "Переводи с {{source_lang}} на {{target_lang}}.\n---USER---\n{{text}}") writeFile(t, filepath.Join(dir, "prompts", "editor.md"), "Редактируй перевод.\n---USER---\nИсходник: {{text}}\nЧерновик перевода для редактуры: {{draft}}") draftCaps := "" if draftFloor > 0 { draftCaps = fmt.Sprintf("\n capabilities: { min_max_tokens: %d }", draftFloor) } hopCaps := "" if hopFloor > 0 { hopCaps = fmt.Sprintf("\n capabilities: { min_max_tokens: %d }", hopFloor) } writeFile(t, filepath.Join(dir, "models.yaml"), fmt.Sprintf(` prices_checked: %q default_model: fake-model providers: fake: kind: openai base_url: %q timeouts: { attempt_s: 5, max_attempts: 2, backoff_cap_s: 1 } models: fake-model: provider: fake price: { input_per_m: 1.0, cached_per_m: 0.1, cache_write_per_m: 0, output_per_m: 2.0 }%s fake-hop: provider: fake price: { input_per_m: 1.0, cached_per_m: 0.1, cache_write_per_m: 0, output_per_m: 2.0 }%s `, time.Now().UTC().Format("2006-01-02"), providerURL, draftCaps, hopCaps)) escLine, escBudget := "", "" if withEscalation { escLine = ", escalate_to: fake-hop" escBudget = "escalation: { budget_usd: 1.0 }" } // A small global min_max_tokens (512) is the floor the model-level floors must beat. writeFile(t, filepath.Join(dir, "pipeline.yaml"), fmt.Sprintf(` core: C1 version: 1 defaults: { max_output_ratio: 2.0, min_max_tokens: 512 } retries: { regenerate_before_escalate: 0 } stages: - { name: draft, role: translator, model: fake-model, prompt: prompts/translator.md, prompt_version: v-test, temperature: 0.3, reasoning: "off"%s } - { name: edit, role: editor, model: fake-model, prompt: prompts/editor.md, prompt_version: v-test, temperature: 0.4, reasoning: "off" } %s `, escLine, escBudget)) writeFile(t, filepath.Join(dir, "source.txt"), "静かな朝。") writeFile(t, filepath.Join(dir, "book.yaml"), ` book_id: test-book title: Тест source_lang: ja target_lang: ru genre: ранобэ audience: тест venuti: 0.5 honorifics: keep transcription: polivanov footnotes: minimal pipeline: pipeline.yaml models: models.yaml source_file: source.txt ceilings: { book_usd: 5.0, day_usd: 10.0 } `) return filepath.Join(dir, "book.yaml") } // maxTokensForModel returns the max_tokens of the FIRST recorded body whose "model" is // modelName (the wire proof — the floor is applied before the request_hash, so what the // provider receives is the floored value). func maxTokensForModel(t *testing.T, rec *reqRec, modelName string) int { t.Helper() for _, body := range rec.all() { if strings.Contains(body, `"model":"`+modelName+`"`) { return maxTokensOf(t, body) } } t.Fatalf("no recorded request for model %q", modelName) return 0 } // The primary call's derived budget is lifted to the model's min_max_tokens floor. func TestMinMaxTokensFloorPrimary(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, draftEdit) defer srv.Close() bookPath := setupFloorProject(t, srv.URL, 8000, 0, false) r := newRunner(t, bookPath) defer r.Close() if _, err := r.TranslateBook(context.Background()); err != nil { t.Fatal(err) } // The tiny source derives far below 8000 (and below the 512 global floor too), so a // wire max_tokens of exactly 8000 can only be the per-model floor. if got := maxTokensForModel(t, rec, "fake-model"); got != 8000 { t.Fatalf("primary draft wire max_tokens = %d, want the model floor 8000 (D24.3)", got) } } // The escalation hop is floored against ITS OWN model (fake-hop), NOT the smaller, // floor-less primary budget it inherits — the stage-A fix. The primary draft stays at the // small derived budget, proving the floor is per-call-model, not global. func TestMinMaxTokensFloorHop(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, func(body string) (string, string) { if isEditBody(body) { return "ОТРЕДАКТИРОВАННЫЙ ПЕРЕВОД", "stop" } if strings.Contains(body, `"model":"fake-hop"`) { return "Тихое утро.", "stop" // the hop translates cleanly → the re-gate passes } return "静かな図書館の朝。", "stop" // the primary echoes CJK → cjk_artifact → escalate }) defer srv.Close() // draft floor 0 (primary keeps the small derived budget), hop floor 9000. bookPath := setupFloorProject(t, srv.URL, 0, 9000, true) r := newRunner(t, bookPath) defer r.Close() res, err := r.TranslateBook(context.Background()) if err != nil { t.Fatal(err) } if !res.Chunks[0].Stages[0].Escalated { t.Fatalf("premise broken: the primary must escalate for the hop budget to be observable, got %+v", res.Chunks[0].Stages[0]) } // The hop's own floor (9000) reaches the wire... if got := maxTokensForModel(t, rec, "fake-hop"); got != 9000 { t.Fatalf("escalation hop wire max_tokens = %d, want the HOP model floor 9000 — a hop must re-floor against its own model, not inherit the primary budget (D24.3)", got) } // ...while the floor-less primary stays at the small derived budget (global floor 512), // so the hop's 9000 is not merely an inherited value. if got := maxTokensForModel(t, rec, "fake-model"); got != 512 { t.Fatalf("primary draft wire max_tokens = %d, want the small derived/global budget 512 (proves the hop floor is per-call, not shared)", got) } } // Editing a floor is a loud --resnapshot: the floor is folded into the snapshot via the // resolved Capability (snapshot.go marshals ResolveCapability for the stage AND its hop), // so a floor-only change must move the snapshotID — else a stale checkpoint false-hits a // diverged wire request (invariant #2 / D5.2). func TestMinMaxTokensFloorFoldedIntoSnapshot(t *testing.T) { rec := &reqRec{} srv := newJSONProvider(rec, draftEdit) defer srv.Close() snapFor := func(draftFloor int) string { r := newRunner(t, setupFloorProject(t, srv.URL, draftFloor, 0, false)) defer r.Close() id, _, err := r.snapshotID() if err != nil { t.Fatal(err) } return id } none, f8000, f16000 := snapFor(0), snapFor(8000), snapFor(16000) if none == f8000 { t.Fatalf("adding a min_max_tokens floor must change the snapshotID (wire-affecting input, D24.3), got %s for both", none) } if f8000 == f16000 { t.Fatalf("changing the floor value must change the snapshotID, got %s for both", f8000) } }