122 lines
4.3 KiB
Go
122 lines
4.3 KiB
Go
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")
|
||
}
|
||
}
|