122 lines
4.2 KiB
Go
122 lines
4.2 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: the leading system messages became system blocks, cache_control sits
|
|
// on the marked block, with a 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 invariant: 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 parameters are not sent on this wire: current Claude models
|
|
// reject temperature with a 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")
|
|
}
|
|
}
|