项目文件夹

文件
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

154 行
5.3 KiB
Go

package anthropic
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/zzet/gortex/internal/llm"
)
// captureWithOpts runs one Complete (with the given model, request, and
// options) against a mock that accepts both freeform and structured
// shapes, and returns the decoded request body.
func captureWithOpts(t *testing.T, model string, req llm.CompletionRequest, opts ...Option) map[string]any {
t.Helper()
var got map[string]any
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
b, _ := io.ReadAll(r.Body)
_ = json.Unmarshal(b, &got)
// A reply that satisfies both freeform (text block) and
// structured (respond tool_use) extraction.
_, _ = io.WriteString(w, `{"content":[{"type":"text","text":"ok"},{"type":"tool_use","name":"respond","input":{"terms":["x"]}}]}`)
}))
defer srv.Close()
t.Setenv("ANTHROPIC_API_KEY", "k")
p, err := New(llm.RemoteConfig{Model: model, BaseURL: srv.URL}, opts...)
if err != nil {
t.Fatal(err)
}
defer p.Close()
if _, err := p.Complete(context.Background(), req); err != nil {
t.Fatal(err)
}
return got
}
func freeform(content string) llm.CompletionRequest {
return llm.CompletionRequest{Messages: []llm.Message{
{Role: llm.RoleSystem, Content: "you are terse"},
{Role: llm.RoleUser, Content: content},
}}
}
func structured() llm.CompletionRequest {
return llm.CompletionRequest{
Messages: []llm.Message{
{Role: llm.RoleSystem, Content: "you are terse"},
{Role: llm.RoleUser, Content: "auth"},
},
Shape: llm.ShapeExpandTerms,
}
}
func TestCaching_MarksSystemAndToolWhenEnabled(t *testing.T) {
got := captureWithOpts(t, "claude-sonnet-4-6", structured(), WithPromptCaching(true, "1h"))
// system must be an array of blocks, the last carrying cache_control.
sysBlocks, ok := got["system"].([]any)
if !ok || len(sysBlocks) == 0 {
t.Fatalf("system should be a content-block array when caching, got %T %v", got["system"], got["system"])
}
block := sysBlocks[0].(map[string]any)
cc, ok := block["cache_control"].(map[string]any)
if !ok {
t.Fatalf("system block missing cache_control: %v", block)
}
if cc["type"] != "ephemeral" || cc["ttl"] != "1h" {
t.Errorf("cache_control=%v want ephemeral/1h", cc)
}
// The forced tool must also carry cache_control.
tools := got["tools"].([]any)
tool := tools[0].(map[string]any)
if _, ok := tool["cache_control"].(map[string]any); !ok {
t.Errorf("tool should carry cache_control when caching, got %v", tool)
}
}
func TestCaching_OffByDefault(t *testing.T) {
got := captureWithOpts(t, "claude-sonnet-4-6", structured())
if _, isArray := got["system"].([]any); isArray {
t.Error("system must be a plain string when caching is off")
}
if _, isStr := got["system"].(string); !isStr {
t.Errorf("system=%T want string", got["system"])
}
tools := got["tools"].([]any)
tool := tools[0].(map[string]any)
if _, ok := tool["cache_control"]; ok {
t.Error("tool must not carry cache_control when caching is off")
}
}
func TestThinking_OffByDefault(t *testing.T) {
got := captureWithOpts(t, "claude-opus-4-8", freeform("hi"))
if _, ok := got["thinking"]; ok {
t.Error("thinking must be off by default")
}
}
func TestThinking_ManualFreeformBumpsMaxTokens(t *testing.T) {
got := captureWithOpts(t, "claude-opus-4-8", freeform("hi"), WithThinking("manual", 8000, ""))
think, ok := got["thinking"].(map[string]any)
if !ok {
t.Fatalf("expected a thinking object, got %v", got["thinking"])
}
if think["type"] != "enabled" || think["budget_tokens"] != float64(8000) {
t.Errorf("thinking=%v want enabled/8000", think)
}
if got["max_tokens"].(float64) < 8000 {
t.Errorf("max_tokens=%v must exceed the thinking budget", got["max_tokens"])
}
}
func TestThinking_AdaptiveOnCapableModel(t *testing.T) {
got := captureWithOpts(t, "claude-opus-4-8", freeform("hi"), WithThinking("adaptive", 0, "summarized"))
think := got["thinking"].(map[string]any)
if think["type"] != "adaptive" || think["display"] != "summarized" {
t.Errorf("thinking=%v want adaptive/summarized", think)
}
}
func TestThinking_AutoResolves(t *testing.T) {
// Capable model -> adaptive.
got := captureWithOpts(t, "claude-sonnet-4-6", freeform("hi"), WithThinking("auto", 0, ""))
if got["thinking"].(map[string]any)["type"] != "adaptive" {
t.Errorf("auto on a capable model should pick adaptive, got %v", got["thinking"])
}
// Older model -> manual.
got = captureWithOpts(t, "claude-3-5-haiku-latest", freeform("hi"), WithThinking("auto", 0, ""))
if got["thinking"].(map[string]any)["type"] != "enabled" {
t.Errorf("auto on an incapable model should fall back to manual, got %v", got["thinking"])
}
}
func TestThinking_AdaptiveOnIncapableFallsBackToManual(t *testing.T) {
got := captureWithOpts(t, "claude-3-opus-20240229", freeform("hi"), WithThinking("adaptive", 0, ""))
think := got["thinking"].(map[string]any)
if think["type"] != "enabled" {
t.Errorf("explicit adaptive on an incapable model should degrade to manual, got %v", think)
}
}
func TestThinking_SkippedForStructuredRequests(t *testing.T) {
got := captureWithOpts(t, "claude-opus-4-8", structured(), WithThinking("manual", 8000, ""))
if _, ok := got["thinking"]; ok {
t.Error("thinking is incompatible with the forced-tool structured path and must be suppressed")
}
}