textmachine/backend/internal/llm/provider_anthropic_test.go

122 lines
4.3 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package llm
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestAnthropicWireShapeAndUsage(t *testing.T) {
var got map[string]any
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/messages" {
t.Errorf("path = %s", r.URL.Path)
}
if r.Header.Get("x-api-key") != "k" || r.Header.Get("anthropic-version") == "" {
t.Errorf("headers: key=%q version=%q", r.Header.Get("x-api-key"), r.Header.Get("anthropic-version"))
}
if err := json.NewDecoder(r.Body).Decode(&got); err != nil {
t.Fatal(err)
}
w.Write([]byte(`{"id":"msg1","model":"claude-sonnet-5","content":[{"type":"text","text":"пере"},{"type":"text","text":"вод"}],
"stop_reason":"end_turn",
"usage":{"input_tokens":60,"output_tokens":20,"cache_creation_input_tokens":30,"cache_read_input_tokens":10}}`))
}))
defer srv.Close()
c := NewAnthropicClient(AnthropicConfig{BaseURL: srv.URL, APIKey: "k", Profile: fastProfile(), CacheTTL: "5m"}, nil)
resp, err := c.Complete(context.Background(), LLMRequest{
Model: "claude-sonnet-5",
Messages: []Message{
{Role: "system", Content: "роль"},
{Role: "system", Content: "бриф", CacheBoundary: true},
{Role: "user", Content: "чанк"},
},
MaxTokens: 512,
Temperature: 0.4,
})
if err != nil {
t.Fatal(err)
}
// Wire: ведущие system-сообщения стали system-блоками, cache_control — на
// помеченном блоке, с TTL.
sys, ok := got["system"].([]any)
if !ok || len(sys) != 2 {
t.Fatalf("system blocks = %v", got["system"])
}
first := sys[0].(map[string]any)
if _, hasCC := first["cache_control"]; hasCC {
t.Fatal("unmarked block must not carry cache_control")
}
second := sys[1].(map[string]any)
cc, ok := second["cache_control"].(map[string]any)
if !ok || cc["type"] != "ephemeral" || cc["ttl"] != "5m" {
t.Fatalf("cache_control = %v", second["cache_control"])
}
msgs, ok := got["messages"].([]any)
if !ok || len(msgs) != 1 {
t.Fatalf("messages = %v", got["messages"])
}
// Usage-инвариант: PromptTokens = input + cache_read + cache_creation.
if resp.Usage.PromptTokens != 100 || resp.Usage.CachedTokens != 10 || resp.Usage.CacheCreationTokens != 30 {
t.Fatalf("usage = %+v", resp.Usage)
}
if resp.Text != "перевод" || resp.FinishReason != FinishStop || resp.Model != "claude-sonnet-5" {
t.Fatalf("resp = %+v", resp)
}
// Sampling-параметры на этот wire не отправляются: актуальные Claude-модели
// отклоняют temperature с 400.
if _, has := got["temperature"]; has {
t.Fatal("temperature must not be sent to the Anthropic wire")
}
}
func TestAnthropicRejectsUnmappedReasoning(t *testing.T) {
c := NewAnthropicClient(AnthropicConfig{BaseURL: "http://unused", APIKey: "k", Profile: fastProfile()}, nil)
_, err := c.Complete(context.Background(), LLMRequest{
Model: "m", Messages: []Message{{Role: "user", Content: "u"}}, MaxTokens: 16, ReasoningEffort: "high",
})
if err == nil {
t.Fatal("unmapped reasoning depth must fail loud, not silently run without thinking")
}
}
func TestAnthropicStopReasonMapping(t *testing.T) {
cases := map[string]string{
"end_turn": FinishStop,
"max_tokens": FinishLength,
"refusal": FinishRefusal,
"weird": "",
}
for wire, want := range cases {
if got := mapAnthropicStopReason(wire); got != want {
t.Errorf("map(%q) = %q, want %q", wire, got, want)
}
}
}
func TestAnthropicTooManyBoundaries(t *testing.T) {
c := NewAnthropicClient(AnthropicConfig{BaseURL: "http://unused", APIKey: "k", Profile: fastProfile()}, nil)
msgs := make([]Message, 5)
for i := range msgs {
msgs[i] = Message{Role: "system", Content: "x", CacheBoundary: true}
}
_, err := c.Complete(context.Background(), LLMRequest{Model: "m", Messages: msgs, MaxTokens: 16})
if err == nil {
t.Fatal("more than 4 cache boundaries must fail loud (API limit)")
}
}
func TestAnthropicJSONOnlyUnsupported(t *testing.T) {
c := NewAnthropicClient(AnthropicConfig{BaseURL: "http://unused", APIKey: "k", Profile: fastProfile()}, nil)
_, err := c.Complete(context.Background(), LLMRequest{
Model: "m", Messages: []Message{{Role: "user", Content: "u"}}, MaxTokens: 16, JSONOnly: true,
})
if err == nil {
t.Fatal("JSONOnly must fail loud, not silently drop the constraint")
}
}