Files
Agent-Native/runtime/llm/providers_test.go
T
weichenandCursor 694b08cdbe
ci / vet-and-test (pull_request) Failing after 2s
ci / docker-checkout-hostgateway (pull_request) Successful in 2s
Pair skipped tool calls and close the stdio and fetch gaps
Why: a tool-budget stop left tool calls without results, host callbacks raced, a canceled MCP read stole the next response, and fetch re-resolved after the SSRF check.
Evidence: GOWORK=off go vet ./... && go test -count=1 ./... && go test -race ./runtime/... ./adapters/native/... passed, including TestLoopBudgetToolCallCap, TestParallelHostCallbacksAreSerial, TestStdioSlowCancelThenEcho, TestPresetsAndClientForChatCompletions, TestFetchResolveChangesToLoopbackRejected, and TestFetchHostCheckRedirectStillRejectsPrivate.
Impact: over-budget calls get an error result and no tool event; one Run serializes host callbacks; MCP dispatches by id; anthropic and google presets use OpenAI-compatible chat endpoints; fetch dials the address it just checked. deps/agent-intent is 3130190; error-code-system and logging-system are a3ee67b and 49b5d8b.
Refs: N1, N2, N3, N4, N13, N14
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-03 01:10:29 +00:00

162 lines
5.0 KiB
Go

package llm
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
)
func TestParseModelRefAndThinking(t *testing.T) {
ref, err := ParseModelRef("anthropic/claude-sonnet:high")
if err != nil {
t.Fatal(err)
}
if ref.Provider != "anthropic" || ref.Model != "claude-sonnet" || ref.Thinking != "high" {
t.Fatalf("%+v", ref)
}
if _, err := ParseModelRef("openai/gpt:nope"); err == nil {
t.Fatal("unknown thinking level must fail")
}
if AnthropicThinkingBudget("high") != 8192 || OpenAIReasoningEffort("off") != "" {
t.Fatal("thinking map")
}
p, ok := Lookup("groq", nil)
if !ok || !strings.Contains(p.BaseURL, "groq") || p.Protocol != ProtocolOpenAIChat {
t.Fatalf("preset: %+v", p)
}
}
func TestModelsJSONAndBodies(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "models.json")
raw := []byte(`{"providers":[{"name":"local","protocol":"openai-chat","baseURL":"http://127.0.0.1:9","apiKeyEnv":"UNUSED_KEY"}]}`)
if err := os.WriteFile(path, raw, 0o644); err != nil {
t.Fatal(err)
}
got, err := LoadModelsJSON(path)
if err != nil || len(got) != 1 || got[0].Name != "local" {
t.Fatalf("%+v %v", got, err)
}
msgs := []Message{{
Role: RoleUser, Content: "see",
Images: []Image{{MIME: "image/png", Data: "aaaa"}},
}}
body, err := BuildBody(ProtocolAnthropic, "claude", append([]Message{{Role: RoleSystem, Content: "sys"}}, msgs...), nil, StreamOptions{ThinkingLevel: "low"})
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(body), "budget_tokens") || !strings.Contains(string(body), "base64") {
t.Fatalf("anthropic body: %s", body)
}
gbody, err := BuildBody(ProtocolGoogle, "gemini", msgs, nil, StreamOptions{ThinkingLevel: "max"})
if err != nil || !strings.Contains(string(gbody), "inlineData") || !strings.Contains(string(gbody), "thinkingBudget") {
t.Fatalf("google: %s %v", gbody, err)
}
rbody, err := BuildBody(ProtocolOpenAIResponses, "gpt", msgs, nil, StreamOptions{ThinkingLevel: "medium", MaxTokens: 20})
if err != nil || !strings.Contains(string(rbody), "max_output_tokens") || !strings.Contains(string(rbody), "image_url") {
t.Fatalf("responses: %s %v", rbody, err)
}
var decoded map[string]any
if err := json.Unmarshal(rbody, &decoded); err != nil {
t.Fatal(err)
}
}
func TestPresetsAndClientForChatCompletions(t *testing.T) {
want := map[string]string{
"anthropic": "https://api.anthropic.com/v1",
"google": "https://generativelanguage.googleapis.com/v1beta/openai",
}
for _, p := range Presets() {
base, ok := want[p.Name]
if !ok {
continue
}
if p.Protocol != ProtocolOpenAIChat || p.BaseURL != base {
t.Fatalf("%s preset: %+v", p.Name, p)
}
}
for _, name := range []string{"openai-responses", "azure"} {
p, ok := Lookup(name, nil)
if !ok {
t.Fatal(name)
}
if _, err := ClientFor(p); err == nil {
t.Fatalf("%s should fail closed", name)
}
}
var path string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path = r.URL.Path
w.Header().Set("Content-Type", "text/event-stream")
_, _ = w.Write([]byte("data: [DONE]\n\n"))
}))
defer srv.Close()
client, err := ClientFor(Provider{Protocol: ProtocolOpenAIChat, BaseURL: srv.URL, APIKey: "k"})
if err != nil {
t.Fatal(err)
}
for ev := range client.Stream(context.Background(), "m", []Message{{Role: RoleUser, Content: "hi"}}, nil, StreamOptions{}) {
if ev.Kind == KindError {
t.Fatal(ev.Err)
}
}
if path != "/chat/completions" {
t.Fatalf("path %q", path)
}
}
func TestRetryThenSuccess(t *testing.T) {
n := 0
base := StreamFunc(func(ctx context.Context, model string, messages []Message, tools []ToolDecl, opts StreamOptions) <-chan Event {
out := make(chan Event, 2)
n++
if n == 1 {
out <- Event{Kind: KindError, Retryable: true, ErrorMessage: "429"}
} else {
out <- Event{Kind: KindTextDelta, Text: "ok"}
out <- Event{Kind: KindMessageEnd, Message: Message{Role: RoleAssistant, Content: "ok"}}
}
close(out)
return out
})
var events []string
wrapped := WithRetry(base, 2, func(kind, detail string) { events = append(events, kind) })
var texts []string
for ev := range wrapped(context.Background(), "m", nil, nil, StreamOptions{}) {
if ev.Kind == KindTextDelta {
texts = append(texts, ev.Text)
}
}
if len(texts) != 1 || texts[0] != "ok" {
t.Fatalf("texts %v calls %d", texts, n)
}
if len(events) == 0 || events[0] != "auto_retry_start" {
t.Fatalf("events %v", events)
}
}
func TestContextOverflowIsNotRetried(t *testing.T) {
if !ContextOverflow("maximum context length exceeded") {
t.Fatal("expected overflow")
}
n := 0
base := StreamFunc(func(ctx context.Context, model string, messages []Message, tools []ToolDecl, opts StreamOptions) <-chan Event {
out := make(chan Event, 1)
n++
out <- Event{Kind: KindError, Retryable: true, ErrorMessage: "context length overflow"}
close(out)
return out
})
for range WithRetry(base, 3, nil)(context.Background(), "m", nil, nil, StreamOptions{}) {
}
if n != 1 {
t.Fatalf("overflow must not retry, calls=%d", n)
}
}