package config import ( "fmt" "os" "path/filepath" "strings" "testing" "time" ) // additive_reasoning_test.go is the D13.6 fail-fast gate: a stage that THINKS // (reasoning low|medium|high) on an ADDITIVE-billing provider (xAI — reasoning bills on top // of completion) must declare reasoning_max_tokens, or LoadPipeline fails loud — the same // structural discipline as the echo mine. Reverting the check in LoadPipeline makes the // "armed" cases below stop failing. func TestReasoningBufferFailFast(t *testing.T) { today := time.Now().UTC().Format("2006-01-02") dir := t.TempDir() // A prompt template LoadPipeline can stat + split. promptPath := filepath.Join(dir, "p.md") if err := os.WriteFile(promptPath, []byte("sys\n---USER---\n{{text}}"), 0o644); err != nil { t.Fatal(err) } modelsPath := filepath.Join(dir, "models.yaml") if err := os.WriteFile(modelsPath, []byte(fmt.Sprintf(` prices_checked: %q default_model: additive-model providers: xai: kind: openai base_url: http://x reasoning: additive ds: kind: openai base_url: http://y reasoning: subset models: additive-model: { provider: xai, price: { input_per_m: 1, output_per_m: 2 } } subset-model: { provider: ds, price: { input_per_m: 1, output_per_m: 2 } } `, today)), 0o644); err != nil { t.Fatal(err) } models, err := LoadModels(modelsPath) if err != nil { t.Fatalf("models load: %v", err) } load := func(stage string) error { pp := filepath.Join(dir, "pipe.yaml") body := fmt.Sprintf("core: C1\nversion: 1\ndefaults: { max_output_ratio: 2, min_max_tokens: 512 }\nstages:\n%s", stage) if err := os.WriteFile(pp, []byte(body), 0o644); err != nil { t.Fatal(err) } _, e := LoadPipeline(pp, models) return e } stage := func(model, reasoning string, buf int, escalateTo string) string { s := fmt.Sprintf(" - { name: s, role: translator, model: %s, prompt: %s, prompt_version: v, reasoning: %q", model, promptPath, reasoning) if buf > 0 { s += fmt.Sprintf(", reasoning_max_tokens: %d", buf) } if escalateTo != "" { s += fmt.Sprintf(", escalate_to: %s", escalateTo) } return s + " }" } cases := []struct { name string stage string wantErr bool }{ {"think-on additive no buffer → fail", stage("additive-model", "high", 0, ""), true}, {"think-on additive with buffer → ok", stage("additive-model", "medium", 4000, ""), false}, {"think-on subset no buffer → ok", stage("subset-model", "high", 0, ""), false}, {"off additive no buffer → ok", stage("additive-model", "off", 0, ""), false}, // finding #4: "" on an additive provider falls to the grok default (thinks) → MUST fail. {"empty-reasoning additive no buffer → fail", stage("additive-model", "", 0, ""), true}, {"empty-reasoning additive with buffer → ok", stage("additive-model", "", 4000, ""), false}, {"empty-reasoning subset no buffer → ok", stage("subset-model", "", 0, ""), false}, {"escalate_to additive think-on no buffer → fail", stage("subset-model", "high", 0, "additive-model"), true}, {"escalate_to additive empty no buffer → fail", stage("subset-model", "", 0, "additive-model"), true}, {"escalate_to additive think-on with buffer → ok", stage("subset-model", "high", 4000, "additive-model"), false}, } for _, c := range cases { err := load(c.stage) if c.wantErr { if err == nil || !strings.Contains(err.Error(), "reasoning_max_tokens") { t.Errorf("%s: want an error mentioning reasoning_max_tokens, got %v", c.name, err) } } else if err != nil { t.Errorf("%s: want ok, got %v", c.name, err) } } }