From 81a706ebb4edbb0f5d267d2fc1cf73e9c7a70efa Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 11 Mar 2026 12:26:36 +0000 Subject: [PATCH 1/6] chore: downgrade to Go 1.25 Remove kronk provider and examples (every published version requires go 1.26). Downgrade kaptinlin deps to Go 1.25-compatible versions. --- examples/kronk/simple/main.go | 93 ---- examples/kronk/stream/main.go | 181 ------- go.mod | 118 ++--- go.sum | 271 +++------- providers/kronk/README.md | 26 - providers/kronk/kronk.go | 156 ------ providers/kronk/language_model.go | 669 ------------------------ providers/kronk/language_model_hooks.go | 269 ---------- providers/kronk/options.go | 71 --- providers/kronk/provider_options.go | 104 ---- 10 files changed, 102 insertions(+), 1856 deletions(-) delete mode 100644 examples/kronk/simple/main.go delete mode 100644 examples/kronk/stream/main.go delete mode 100644 providers/kronk/README.md delete mode 100644 providers/kronk/kronk.go delete mode 100644 providers/kronk/language_model.go delete mode 100644 providers/kronk/language_model_hooks.go delete mode 100644 providers/kronk/options.go delete mode 100644 providers/kronk/provider_options.go diff --git a/examples/kronk/simple/main.go b/examples/kronk/simple/main.go deleted file mode 100644 index 44919fd4e..000000000 --- a/examples/kronk/simple/main.go +++ /dev/null @@ -1,93 +0,0 @@ -package main - -// This is a basic example illustrating how to create an agent with a custom -// tool call. - -import ( - "context" - "fmt" - "os" - "time" - - "charm.land/fantasy" - "charm.land/fantasy/providers/kronk" -) - -const modelURL = "Qwen/Qwen3-8B-GGUF/Qwen3-8B-Q8_0.gguf" - -func main() { - if err := run(); err != nil { - fmt.Printf("\nERROR: %s\n", err) - os.Exit(1) - } -} - -func run() error { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) - defer cancel() - - // Create the provider with optional logging. - provider, err := kronk.New( - kronk.WithName("kronk"), - kronk.WithLogger(kronk.FmtLogger), - ) - if err != nil { - return fmt.Errorf("unable to create provider: %w", err) - } - - // Clean up when done. - defer func() { - fmt.Println("\nUnloading Kronk") - if closer, ok := provider.(interface{ Close(context.Context) error }); ok { - if err := closer.Close(context.Background()); err != nil { - fmt.Printf("failed to close provider: %v\n", err) - } - } - }() - - // Get a language model by providing the model URL. - // The provider will download and initialize the model automatically. - model, err := provider.LanguageModel(ctx, modelURL) - if err != nil { - return fmt.Errorf("unable to get language model: %w", err) - } - - // ------------------------------------------------------------------------- - - // Let's make a tool that fetches info about cute dogs. Here's a schema - // for the tool's input. - type cuteDogQuery struct { - Location string `json:"location" description:"The location to search for cute dogs."` - } - - // And here's the implementation of that tool. - fetchCuteDogInfo := func(ctx context.Context, input cuteDogQuery, _ fantasy.ToolCall) (fantasy.ToolResponse, error) { - if input.Location == "Silver Lake, Los Angeles" { - return fantasy.NewTextResponse("Cute dogs are everywhere!"), nil - } - return fantasy.NewTextResponse("No cute dogs found."), nil - } - - // Add the tool. - cuteDogTool := fantasy.NewAgentTool("cute_dog_tool", "Provide up-to-date info on cute dogs.", fetchCuteDogInfo) - - // Equip your agent. - agent := fantasy.NewAgent(model, - fantasy.WithSystemPrompt("You are a moderately helpful, dog-centric assistant."), - fantasy.WithTools(cuteDogTool), - fantasy.WithMaxOutputTokens(2048), - fantasy.WithTemperature(0.7), - fantasy.WithTopP(0.8), - fantasy.WithTopK(20), - ) - - // Put that agent to work! - const prompt = "Find all the cute dogs in Silver Lake, Los Angeles. Use the cute dog tool." - result, err := agent.Generate(ctx, fantasy.AgentCall{Prompt: prompt}) - if err != nil { - return fmt.Errorf("agent generate failed: %w", err) - } - fmt.Println(result.Response.Content.Text()) - - return nil -} diff --git a/examples/kronk/stream/main.go b/examples/kronk/stream/main.go deleted file mode 100644 index e63465919..000000000 --- a/examples/kronk/stream/main.go +++ /dev/null @@ -1,181 +0,0 @@ -package main - -// This example demonstrates how to hook into the various parts of a streaming -// tool call. - -import ( - "context" - "fmt" - "math/rand/v2" - "os" - "strings" - "time" - - "charm.land/fantasy" - "charm.land/fantasy/providers/kronk" -) - -const modelURL = "Qwen/Qwen3-8B-GGUF/Qwen3-8B-Q8_0.gguf" - -const systemPrompt = ` -You are moderately helpful assistant with a new puppy named Chuck. Chuck is -moody and ranges from very happy to very annoyed. He's pretty happy-go-lucky, -but new encounters make him pretty uncomfortable. - -You despise emojis and never use them. Same with Markdown. Same with em-dashes. -You prefer "welp" to "well" when starting a sentence (that's just how you were -raised). You also don't use run-on sentences, including entering a comma where -there should be a period. You had a decent education and did well in elementary -school grammar. You grew up in the United States, specifically Kansas City, -Missouri. -` - -// Input for a tool call. The LLM will look at the struct tags and fill out the -// values as necessary. -type dogInteraction struct { - OtherDogName string `json:"dogName" description:"Name of the other dog. Just make something up. All the dogs are named after Japanese cars from the 80s."` -} - -// Here's a tool call. In this case it's a set of random barks. -func letsBark(ctx context.Context, i dogInteraction, _ fantasy.ToolCall) (fantasy.ToolResponse, error) { - var r fantasy.ToolResponse - if rand.Float64() >= 0.5 { - r.Content = randomBarks(1, 3) - } else { - r.Content = randomBarks(5, 10) - } - return r, nil -} - -func main() { - if err := run(); err != nil { - fmt.Printf("\nERROR: %s\n", err) - os.Exit(1) - } -} - -func run() error { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute) - defer cancel() - - // Create the provider with optional logging. - provider, err := kronk.New( - kronk.WithName("kronk"), - kronk.WithLogger(kronk.FmtLogger), - ) - if err != nil { - return fmt.Errorf("unable to create provider: %w", err) - } - - // Clean up when done. - defer func() { - fmt.Println("\nUnloading Kronk") - if closer, ok := provider.(interface{ Close(context.Context) error }); ok { - if err := closer.Close(context.Background()); err != nil { - fmt.Printf("failed to close provider: %v\n", err) - } - } - }() - - // Get a language model by providing the model URL. - // The provider will download and initialize the model automatically. - model, err := provider.LanguageModel(ctx, modelURL) - if err != nil { - return fmt.Errorf("unable to get language model: %w", err) - } - - // ------------------------------------------------------------------------- - - // Let's add a tool to our belt. A tool for dogs. - barkTool := fantasy.NewAgentTool( - "bark", - "Have Chuck express his feelings by barking. A few barks means he's happy and many barks means he's not.", - letsBark, - ) - - // Time to make the agent. - agent := fantasy.NewAgent( - model, - fantasy.WithSystemPrompt(systemPrompt), - fantasy.WithTools(barkTool), - ) - - // Alright, let's setup a streaming request! - streamCall := fantasy.AgentStreamCall{ - // The prompt. - Prompt: "what does Chuck say when he is happy", - - // When reasoning starts (Qwen3 models use "thinking" mode). - OnReasoningStart: func(id string, content fantasy.ReasoningContent) error { - fmt.Print("\n[Thinking: ") - return nil - }, - - // When we receive reasoning content. - OnReasoningDelta: func(id, text string) error { - // Print reasoning in a subdued way - fmt.Print(text) - return nil - }, - - // When reasoning ends. - OnReasoningEnd: func(id string, reasoning fantasy.ReasoningContent) error { - fmt.Print("]\n\n") - return nil - }, - - // When we receive a chunk of streaming data. - OnTextDelta: func(id, text string) error { - _, fmtErr := fmt.Print(text) - return fmtErr - }, - - // When tool calls are invoked. - OnToolCall: func(toolCall fantasy.ToolCallContent) error { - fmt.Printf("\n-> Invoking the %s tool with input %s\n", toolCall.ToolName, toolCall.Input) - return nil - }, - - // When a tool call completes. - OnToolResult: func(res fantasy.ToolResultContent) error { - text, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentText](res.Result) - if !ok { - return fmt.Errorf("failed to cast result to text") - } - _, fmtErr := fmt.Printf("\n-> Using the %s tool: %s", res.ToolName, text.Text) - return fmtErr - }, - - // When a step finishes, such as a tool call or a response from the - // LLM. - OnStepFinish: func(_ fantasy.StepResult) error { - fmt.Print("\n-> Step completed\n") - return nil - }, - } - - fmt.Println("Generating...") - - // Finally, let's stream everything! - _, err = agent.Stream(ctx, streamCall) - if err != nil { - fmt.Fprintf(os.Stderr, "Error generating response: %v\n", err) - os.Exit(1) - } - - return nil -} - -// Return a random number of barks between low and high. -func randomBarks(low, high int) string { - const bark = "ruff" - numBarks := low + rand.IntN(high-low+1) - var barks strings.Builder - for i := range numBarks { - if i > 0 { - barks.WriteString(" ") - } - barks.WriteString(bark) - } - return barks.String() -} diff --git a/go.mod b/go.mod index 267a187aa..7a87f3155 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,12 @@ module charm.land/fantasy -go 1.26.1 +go 1.25.0 require ( charm.land/x/vcr v0.1.1 cloud.google.com/go/auth v0.18.2 - github.com/ardanlabs/kronk v1.21.3 - github.com/aws/aws-sdk-go-v2 v1.41.4 - github.com/aws/aws-sdk-go-v2/config v1.32.12 + github.com/aws/aws-sdk-go-v2 v1.41.3 + github.com/aws/aws-sdk-go-v2/config v1.32.11 github.com/aws/smithy-go v1.24.2 github.com/charmbracelet/anthropic-sdk-go v0.0.0-20260223140439-63879b0b8dab github.com/charmbracelet/x/exp/slice v0.0.0-20250904123553-b4e2667e5ad5 @@ -15,120 +14,71 @@ require ( github.com/go-viper/mapstructure/v2 v2.5.0 github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 - github.com/kaptinlin/jsonschema v0.7.5 - github.com/openai/openai-go/v3 v3.28.0 + github.com/kaptinlin/jsonschema v0.6.10 + github.com/openai/openai-go/v2 v2.7.1 github.com/stretchr/testify v1.11.1 golang.org/x/oauth2 v0.36.0 - google.golang.org/genai v1.50.0 + google.golang.org/genai v1.49.0 ) require ( - cel.dev/expr v0.25.1 // indirect cloud.google.com/go v0.123.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect - cloud.google.com/go/iam v1.5.3 // indirect - cloud.google.com/go/monitoring v1.24.3 // indirect - cloud.google.com/go/storage v1.60.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.19.12 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 // indirect - github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect - github.com/beorn7/perks v1.0.1 // indirect - github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect - github.com/cenkalti/backoff/v5 v5.0.3 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.11 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/dustin/go-humanize v1.0.1 // indirect - github.com/ebitengine/purego v0.10.0 // indirect github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-jose/go-jose/v4 v4.1.3 // indirect - github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 // indirect + github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/goccy/go-yaml v1.19.2 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/s2a-go v0.1.9 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.12 // indirect github.com/googleapis/gax-go/v2 v2.17.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect - github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.71 // indirect - github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-getter v1.8.4 // indirect - github.com/hashicorp/go-version v1.8.0 // indirect - github.com/hybridgroup/yzma v1.10.1-0.20260308093636-1875822bb85c // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/jupiterrider/ffi v0.6.0 // indirect - github.com/kaptinlin/go-i18n v0.2.12 // indirect - github.com/kaptinlin/jsonpointer v0.4.17 // indirect - github.com/kaptinlin/messageformat-go v0.4.18 // indirect - github.com/klauspost/compress v1.18.4 // indirect - github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/nikolalohinski/gonja/v2 v2.7.0 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect + github.com/kaptinlin/go-i18n v0.2.4 // indirect + github.com/kaptinlin/jsonpointer v0.4.9 // indirect + github.com/kaptinlin/messageformat-go v0.4.10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_golang v1.23.2 // indirect - github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.67.5 // indirect - github.com/prometheus/procfs v0.20.1 // indirect - github.com/sirupsen/logrus v1.9.4 // indirect - github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect - github.com/ulikunitz/xz v0.5.15 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/detectors/gcp v1.42.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect - go.opentelemetry.io/otel v1.42.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 // indirect - go.opentelemetry.io/otel/metric v1.42.0 // indirect - go.opentelemetry.io/otel/sdk v1.42.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.42.0 // indirect - go.opentelemetry.io/otel/trace v1.42.0 // indirect - go.opentelemetry.io/proto/otlp v1.9.0 // indirect - go.yaml.in/yaml/v2 v2.4.4 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect + go.opentelemetry.io/otel v1.40.0 // indirect + go.opentelemetry.io/otel/metric v1.40.0 // indirect + go.opentelemetry.io/otel/trace v1.40.0 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect golang.org/x/crypto v0.48.0 // indirect - golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect golang.org/x/net v0.51.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.42.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect - golang.org/x/time v0.15.0 // indirect - google.golang.org/api v0.270.0 // indirect - google.golang.org/genproto v0.0.0-20260226221140-a57be14db171 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 // indirect + golang.org/x/time v0.14.0 // indirect + google.golang.org/api v0.269.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect - google.golang.org/grpc v1.79.2 // indirect + google.golang.org/grpc v1.79.1 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/dnaeon/go-vcr.v4 v4.0.6-0.20251110073552-01de4eb40290 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 51b240077..144b42676 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,3 @@ -cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4= -cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= charm.land/x/vcr v0.1.1 h1:PXCFMUG0rPtyk35rhfzYCJEduOzWXCIbrXTFq4OF/9Q= charm.land/x/vcr v0.1.1/go.mod h1:eByq2gqzWvcct/8XE2XO5KznoWEBiXH56+y2gphbltM= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= @@ -10,18 +8,6 @@ cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIi cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= -cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= -cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= -cloud.google.com/go/logging v1.13.2 h1:qqlHCBvieJT9Cdq4QqYx1KPadCQ2noD4FK02eNqHAjA= -cloud.google.com/go/logging v1.13.2/go.mod h1:zaybliM3yun1J8mU2dVQ1/qDzjbOqEijZCn6hSBtKak= -cloud.google.com/go/longrunning v0.8.0 h1:LiKK77J3bx5gDLi4SMViHixjD2ohlkwBi+mKA7EhfW8= -cloud.google.com/go/longrunning v0.8.0/go.mod h1:UmErU2Onzi+fKDg2gR7dusz11Pe26aknR4kHmJJqIfk= -cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE= -cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI= -cloud.google.com/go/storage v1.60.0 h1:oBfZrSOCimggVNz9Y/bXY35uUcts7OViubeddTTVzQ8= -cloud.google.com/go/storage v1.60.0/go.mod h1:q+5196hXfejkctrnx+VYU8RKQr/L3c0cBIlrjmiAKE0= -cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U= -cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.7.0 h1:tfLQ34V6F7tVSwoTf/4lH5sE0o6eCJuNDTmH09nDpbc= @@ -30,62 +16,36 @@ github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xP github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 h1:UnDZ/zFfG1JhH/DqxIZYU/1CUAlTUScoXD/LcM2Ykk8= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0/go.mod h1:IA1C1U7jO/ENqm/vhi7V9YYpBsp+IMyqNrEN94N7tVc= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0 h1:7t/qx5Ost0s0wbA/VDrByOooURhp+ikYwv20i9Y07TQ= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= -github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= -github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= -github.com/ardanlabs/kronk v1.21.3 h1:FrsmO9nozIKlZ9r6WEFK6v3w/Ia0Lc4CJzhlmMwjszo= -github.com/ardanlabs/kronk v1.21.3/go.mod h1:T6cI9+a39ei0Fmd7TTNFI17dDezeBUu1GGxV/L8FP2I= -github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k= -github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 h1:N4lRUXZpZ1KVEUn6hxtco/1d2lgYhNn1fHkkl8WhlyQ= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= -github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0= -github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g= -github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8= -github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20 h1:qi3e/dmpdONhj1RyIZdi6DKKpDXS5Lb8ftr3p7cyHJc= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20/go.mod h1:V1K+TeJVD5JOk3D9e5tsX2KUdL7BlB+FV6cBhdobN8c= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 h1:BYf7XNsJMzl4mObARUBUib+j2tf0U//JAAtTnYqvqCw= -github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11/go.mod h1:aEUS4WrNk/+FxkBZZa7tVgp4pGH+kFGW40Y8rCPqt5g= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 h1:JnQeStZvPHFHeyky/7LbMlyQjUa+jIBj36OlWm0pzIk= -github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19/go.mod h1:HGyasyHvYdFQeJhvDHfH7HXkHh57htcJGKDZ+7z+I24= -github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4 h1:4ExZyubQ6LQQVuF2Qp9OsfEvsTdAWh5Gfwf6PgIdLdk= -github.com/aws/aws-sdk-go-v2/service/s3 v1.96.4/go.mod h1:NF3JcMGOiARAss1ld3WGORCw71+4ExDD2cbbdKS5PpA= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk= +github.com/aws/aws-sdk-go-v2 v1.41.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA= +github.com/aws/aws-sdk-go-v2 v1.41.3/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c= +github.com/aws/aws-sdk-go-v2/config v1.32.11 h1:ftxI5sgz8jZkckuUHXfC/wMUc8u3fG1vQS0plr2F2Zs= +github.com/aws/aws-sdk-go-v2/config v1.32.11/go.mod h1:twF11+6ps9aNRKEDimksp923o44w/Thk9+8YIlzWMmo= +github.com/aws/aws-sdk-go-v2/credentials v1.19.11 h1:NdV8cwCcAXrCWyxArt58BrvZJ9pZ9Fhf9w6Uh5W3Uyc= +github.com/aws/aws-sdk-go-v2/credentials v1.19.11/go.mod h1:30yY2zqkMPdrvxBqzI9xQCM+WrlrZKSOpSJEsylVU+8= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 h1:INUvJxmhdEbVulJYHI061k4TVuS3jzzthNvjqvVvTKM= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19/go.mod h1:FpZN2QISLdEBWkayloda+sZjVJL+e9Gl0k1SyTgcswU= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 h1:/sECfyq2JTifMI2JPyZ4bdRN77zJmr6SrS1eL3augIA= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19/go.mod h1:dMf8A5oAqr9/oxOfLkC/c2LU/uMcALP0Rgn2BD5LWn0= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 h1:AWeJMk33GTBf6J20XJe6qZoRSJo0WfUhsMdUKhoODXE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19/go.mod h1:+GWrYoaAsV7/4pNHpwh1kiNLXkKaSoppxQq9lbH8Ejw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 h1:clHU5fm//kWS1C2HgtgWxfQbFbx4b6rx+5jzhgX9HrI= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 h1:XAq62tBTJP/85lFD5oqOOe7YYgWxY9LvWq8plyDvDVg= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 h1:X1Tow7suZk9UCJHE1Iw9GMZJJl0dAnKXXP1NaSDHwmw= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19/go.mod h1:/rARO8psX+4sfjUQXp5LLifjUt8DuATZ31WptNJTyQA= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 h1:Y2cAXlClHsXkkOvWZFXATr34b0hxxloeQu/pAZz2row= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.7/go.mod h1:idzZ7gmDeqeNrSPkdbtMp9qWMgcBwykA7P7Rzh5DXVU= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 h1:iSsvB9EtQ09YrsmIc44Heqlx5ByGErqhPK1ZQLppias= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.12/go.mod h1:fEWYKTRGoZNl8tZ77i61/ccwOMJdGxwOhWCkp6TXAr0= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 h1:EnUdUqRP1CNzt2DkV67tJx6XDN4xlfBFm+bzeNOQVb0= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16/go.mod h1:Jic/xv0Rq/pFNCh3WwpH4BEqdbSAl+IyHro8LbibHD8= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 h1:XQTQTF75vnug2TXS8m7CVJfC2nniYPZnO1D4Np761Oo= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.8/go.mod h1:Xgx+PR1NUOjNmQY+tRMnouRp83JRM8pRMw/vCaVhPkI= github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas= -github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4= -github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= -github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/anthropic-sdk-go v0.0.0-20260223140439-63879b0b8dab h1:J7XQLgl9sefgTnTGrmX3xqvp5o6MCiBzEjGv5igAlc4= @@ -96,37 +56,23 @@ github.com/charmbracelet/x/json v0.2.0 h1:DqB+ZGx2h+Z+1s98HOuOyli+i97wsFQIxP2ZQA github.com/charmbracelet/x/json v0.2.0/go.mod h1:opFIflx2YgXgi49xVUu8gEQ21teFAxyMwvOiZhIvWNM= github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 h1:aBangftG7EVZoUb69Os8IaYg++6uMOdKK83QtkkvJik= github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2/go.mod h1:qwXFYgsP6T7XnJtbKlf1HP8AjxZZyzxMmc+Lq5GjlU4= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= -github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= -github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU= -github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= -github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA= -github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU= github.com/envoyproxy/go-control-plane/envoy v1.37.0 h1:u3riX6BoYRfF4Dr7dwSOroNfdSbEPe9Yyl09/B6wBrQ= github.com/envoyproxy/go-control-plane/envoy v1.37.0/go.mod h1:DReE9MMrmecPy+YvQOAOHNYMALuowAnbjjEMkkWOi6A= -github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= -github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v1.3.3 h1:MVQghNeW+LZcmXe7SY1V36Z+WFMDjpqGAGacLe2T0ds= github.com/envoyproxy/protoc-gen-validate v1.3.3/go.mod h1:TsndJ/ngyIdQRhMcVVGDDHINPLWB7C82oDArY51KfB0= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= -github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= -github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433 h1:vymEbVwYFP/L05h5TKQxvkXoKxNvTpjxYKdF1Nlwuao= -github.com/go-json-experiment/json v0.0.0-20260214004413-d219187c3433/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg= +github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU= +github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= -github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= @@ -137,99 +83,44 @@ github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= -github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= -github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/enterprise-certificate-proxy v0.3.12 h1:Fg+zsqzYEs1ZnvmcztTYxhgCBsx3eEhEwQ1W/lHq/sQ= +github.com/googleapis/enterprise-certificate-proxy v0.3.12/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= -github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.71 h1:3qrWTgbR0uMacRVnE6//G1B20hUJexxqqmQ2OTs1+0s= -github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.71/go.mod h1:YV27+mh2SLUqeP36G1a9MiqL5eBkFnZQJjNTR9Q9NcY= -github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= -github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= -github.com/hashicorp/go-getter v1.8.4 h1:hGEd2xsuVKgwkMtPVufq73fAmZU/x65PPcqH3cb0D9A= -github.com/hashicorp/go-getter v1.8.4/go.mod h1:x27pPGSg9kzoB147QXI8d/nDvp2IgYGcwuRjpaXE9Yg= -github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= -github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hybridgroup/yzma v1.10.1-0.20260308093636-1875822bb85c h1:BzQAPC8cElhU91X/W3Sk5EVoL+Z4zpRsqu08lZZzYJk= -github.com/hybridgroup/yzma v1.10.1-0.20260308093636-1875822bb85c/go.mod h1:zrzMgv/KVQz23+s6l16b+vJ+9uJVBdWtGcGkwRTMeiQ= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jupiterrider/ffi v0.6.0 h1:UX378KcZvH5c8qgLi9KL/bL82SZTHdRspZ+jj7bvBng= -github.com/jupiterrider/ffi v0.6.0/go.mod h1:PqZ5Go6X9by8CIXgfprxfMPYmn8oT5m2O7AA56s64bY= -github.com/kaptinlin/go-i18n v0.2.12 h1:ywDsvb4KDFddMC2dpI/rrIzGU2mWUSvHmWUm9BMsdl4= -github.com/kaptinlin/go-i18n v0.2.12/go.mod h1:pVcu9qsW5pOIOoZFJXesRYmLos1vMQrby70JPAoWmJU= -github.com/kaptinlin/jsonpointer v0.4.17 h1:mY9k8ciWncxbsECyaxKnR0MdmxamNdp2tLQkAKVrtSk= -github.com/kaptinlin/jsonpointer v0.4.17/go.mod h1:SsfsjqnHG5zuKo1DTBzk1VknaHlL4osHw+X9kZKukpU= -github.com/kaptinlin/jsonschema v0.7.5 h1:jkK4a3NyzNoGlvu12CsL3IcqNMVa5sL51HPVa0nWcPY= -github.com/kaptinlin/jsonschema v0.7.5/go.mod h1:3gIWnptl+SWMyfMR2r4TXXd0xsQZ1m50AKrwmcUONSg= -github.com/kaptinlin/messageformat-go v0.4.18 h1:RBlHVWgZyoxTcUgGWBsl2AcyScq/urqbLZvzgryTmSI= -github.com/kaptinlin/messageformat-go v0.4.18/go.mod h1:ntI3154RnqJgr7GaC+vZBnIExl2V3sv9selvRNNEM24= -github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= -github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/kaptinlin/go-i18n v0.2.4 h1:aIi0BaDbR1FyNTra2cf1Y8vQUbSwVqXVsehZjkkqgbI= +github.com/kaptinlin/go-i18n v0.2.4/go.mod h1:h+/0DIpnlHlF4+ZftBRYncH4LoqU4Y3eh94nY+z6yeY= +github.com/kaptinlin/jsonpointer v0.4.9 h1:o//bYf4PCvnMJIIX8bIg77KB6DO3wBPAabRyPRKh680= +github.com/kaptinlin/jsonpointer v0.4.9/go.mod h1:9y0LgXavlmVE5FSHShY5LRlURJJVhbyVJSRWkilrTqA= +github.com/kaptinlin/jsonschema v0.6.10 h1:CYded7nrwVu7pU1GaIjtd9dSzgqZjh7+LTKFaWqS08I= +github.com/kaptinlin/jsonschema v0.6.10/go.mod h1:ZXZ4K5KrRmCCF1i6dgvBsQifl+WTb8XShKj0NpQNrz8= +github.com/kaptinlin/messageformat-go v0.4.10 h1:ixW2Zf9XUi2lv8NZf+eHUJnWE+YO7K76pFbxuKeqwRs= +github.com/kaptinlin/messageformat-go v0.4.10/go.mod h1:qZzrGrlvWDz2KyyvN3dOWcK9PVSRV1BnfnNU+zB/RWc= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= -github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/nikolalohinski/gonja/v2 v2.7.0 h1:XuwnulQVPwzGaM0J/9AaQv0AFPBAxKI1GILifQ1r9pk= -github.com/nikolalohinski/gonja/v2 v2.7.0/go.mod h1:UIzXPVuOsr5h7dZ5DUbqk3/Z7oFA/NLGQGMjqT4L2aU= -github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= -github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= -github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= -github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= -github.com/openai/openai-go/v3 v3.28.0 h1:2+FfrCVMdGXSQrBv1tLWtokm+BU7+3hJ/8rAHPQ63KM= -github.com/openai/openai-go/v3 v3.28.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= +github.com/openai/openai-go/v2 v2.7.1 h1:/tfvTJhfv7hTSL8mWwc5VL4WLLSDL5yn9VqVykdu9r8= +github.com/openai/openai-go/v2 v2.7.1/go.mod h1:jrJs23apqJKKbT+pqtFgNKpRju/KP9zpUTZhz3GElQE= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= -github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= -github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= -github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= -github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= -github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= -github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= -github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= -github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= -github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= @@ -242,74 +133,48 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= -github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY= -github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/detectors/gcp v1.42.0 h1:kpt2PEJuOuqYkPcktfJqWWDjTEd/FNgrxcniL7kQrXQ= -go.opentelemetry.io/contrib/detectors/gcp v1.42.0/go.mod h1:W9zQ439utxymRrXsUOzZbFX4JhLxXU4+ZnCt8GG7yA8= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= -go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= -go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0 h1:zWWrB1U6nqhS/k6zYB74CjRpuiitRtLLi68VcgmOEto= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.42.0/go.mod h1:2qXPNBX1OVRC0IwOnfo1ljoid+RD0QK3443EaqVlsOU= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0 h1:5gn2urDL/FBnK8OkCfD1j3/ER79rUuTYmCvlXBKeYL8= -go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.39.0/go.mod h1:0fBG6ZJxhqByfFZDwSwpZGzJU671HkwpWaNe2t4VUPI= -go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= -go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= -go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= -go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= -go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= -go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= -go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= -go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= -go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= -go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= -go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= -go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= -go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 h1:XmiuHzgJt067+a6kwyAzkhXooYVv3/TOw9cM2VfJgUM= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0/go.mod h1:KDgtbWKTQs4bM+VPUr6WlL9m/WXcmkCcBlIzqxPGzmI= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= +go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= +go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= +go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= +go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= +go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= +go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= +go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= +go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= +go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= +go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go= go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= -golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= -golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= -golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/api v0.270.0 h1:4rJZbIuWSTohczG9mG2ukSDdt9qKx4sSSHIydTN26L4= -google.golang.org/api v0.270.0/go.mod h1:5+H3/8DlXpQWrSz4RjGGwz5HfJAQSEI8Bc6JqQNH77U= -google.golang.org/genai v1.50.0 h1:yHKV/vjoeN9PJ3iF0ur4cBZco4N3Kl7j09rMq7XSoWk= -google.golang.org/genai v1.50.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= -google.golang.org/genproto v0.0.0-20260226221140-a57be14db171 h1:RxhCsti413yL0IjU9dVvuTbCISo8gs3RW1jPMStck+4= -google.golang.org/genproto v0.0.0-20260226221140-a57be14db171/go.mod h1:uhvzakVEqAuXU3TC2JCsxIRe5f77l+JySE3EqPoMyqM= -google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4= -google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng= +google.golang.org/api v0.269.0 h1:qDrTOxKUQ/P0MveH6a7vZ+DNHxJQjtGm/uvdbdGXCQg= +google.golang.org/api v0.269.0/go.mod h1:N8Wpcu23Tlccl0zSHEkcAZQKDLdquxK+l9r2LkwAauE= +google.golang.org/genai v1.49.0 h1:Se+QJaH2GYK1aaR1o5S38mlU2GD5FnVvP76nfkV7LH0= +google.golang.org/genai v1.49.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= -google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/providers/kronk/README.md b/providers/kronk/README.md deleted file mode 100644 index 0e60403d2..000000000 --- a/providers/kronk/README.md +++ /dev/null @@ -1,26 +0,0 @@ -# Kronk - -> [!IMPORTANT] -> The Kronk provider for Fantasy is considered experimental! -> The API and behavior can change until we consider it stable. - -[Kronk][kronk] is a Go package by [ArdanLabs][ardanlabs] that uses [yzma][yzma] -under the hood to provide hardware accelerated local inference with -[llama.cpp][llama.cpp] directly integrated into your applications. -Kronk provides a high-level API that feels similar to using an OpenAI compatible -API. - -When using the Kronk provider in Fantasy, you only need to specify the model you -want to use and it'll be automatically downloaded in your machine and used for -inference. - -To see which models are available for you, see the [Kronk Catalog][catalog]. - -Examples on how to use it are available in [`examples/kronk`][examples]. - -[kronk]: https://github.com/ardanlabs/kronk -[ardanlabs]: https://github.com/ardanlabs -[yzma]: https://github.com/hybridgroup/yzma -[llama.cpp]: https://github.com/ggml-org/llama.cpp -[catalog]: https://github.com/ardanlabs/kronk_catalogs -[examples]: https://github.com/charmbracelet/fantasy/tree/main/examples/kronk diff --git a/providers/kronk/kronk.go b/providers/kronk/kronk.go deleted file mode 100644 index 8f7c8815a..000000000 --- a/providers/kronk/kronk.go +++ /dev/null @@ -1,156 +0,0 @@ -// Package kronk provides an implementation of the fantasy AI SDK for local -// models using the Kronk SDK. -package kronk - -import ( - "context" - "fmt" - "sync" - - "charm.land/fantasy" - "github.com/ardanlabs/kronk/sdk/kronk" - "github.com/ardanlabs/kronk/sdk/tools/catalog" - "github.com/ardanlabs/kronk/sdk/tools/libs" - "github.com/ardanlabs/kronk/sdk/tools/models" -) - -const ( - // Name is the name of the Kronk provider. - Name = "kronk" -) - -type provider struct { - options options - mu sync.Mutex - kronks map[string]*kronk.Kronk -} - -// New creates a new Kronk provider with the given options. -func New(opts ...Option) (fantasy.Provider, error) { - providerOptions := options{ - languageModelOptions: make([]LanguageModelOption, 0), - } - - for _, o := range opts { - o(&providerOptions) - } - - if providerOptions.name == "" { - providerOptions.name = Name - } - - p := provider{ - options: providerOptions, - kronks: make(map[string]*kronk.Kronk), - } - - return &p, nil -} - -// Name implements fantasy.Provider. -func (p *provider) Name() string { - return p.options.name -} - -// LanguageModel implements fantasy.Provider. -// The modelURL parameter should be a URL to a GGUF model file (e.g., from Hugging Face). -func (p *provider) LanguageModel(ctx context.Context, modelURL string) (fantasy.LanguageModel, error) { - p.mu.Lock() - defer p.mu.Unlock() - - if krn, ok := p.kronks[modelURL]; ok { - opts := append(p.options.languageModelOptions, WithLanguageModelObjectMode(p.options.objectMode)) - return newLanguageModel(modelURL, p.options.name, krn, opts...), nil - } - - mp, err := p.installSystem(ctx, modelURL) - if err != nil { - return nil, fmt.Errorf("failed to install system: %w", err) - } - - krn, err := p.newKronk(mp) - if err != nil { - return nil, fmt.Errorf("failed to create kronk instance: %w", err) - } - - p.kronks[modelURL] = krn - - opts := append(p.options.languageModelOptions, WithLanguageModelObjectMode(p.options.objectMode)) - - return newLanguageModel(modelURL, p.options.name, krn, opts...), nil -} - -// Close unloads all Kronk instances. Call this when done with the provider. -func (p *provider) Close(ctx context.Context) error { - p.mu.Lock() - defer p.mu.Unlock() - - var errs []error - - for url, krn := range p.kronks { - if err := krn.Unload(ctx); err != nil { - errs = append(errs, fmt.Errorf("failed to unload model %s: %w", url, err)) - } - - delete(p.kronks, url) - } - - if len(errs) > 0 { - return errs[0] - } - - return nil -} - -func (p *provider) installSystem(ctx context.Context, modelURL string) (models.Path, error) { - logger := p.options.logger - if logger == nil { - logger = func(context.Context, string, ...any) {} - } - - lbs, err := libs.New() - if err != nil { - return models.Path{}, fmt.Errorf("unable to create libs: %w", err) - } - - if _, err := lbs.Download(ctx, libs.Logger(logger)); err != nil { - return models.Path{}, fmt.Errorf("unable to install llama.cpp: %w", err) - } - - ctlg, err := catalog.New() - if err != nil { - return models.Path{}, fmt.Errorf("unable to create catalog system: %w", err) - } - - if err := ctlg.Download(ctx); err != nil { - return models.Path{}, fmt.Errorf("unable to download catalog: %w", err) - } - - mdls, err := models.New() - if err != nil { - return models.Path{}, fmt.Errorf("unable to create models: %w", err) - } - - mp, err := mdls.Download(ctx, models.Logger(logger), modelURL, "") - if err != nil { - return models.Path{}, fmt.Errorf("unable to install model: %w", err) - } - - return mp, nil -} - -func (p *provider) newKronk(mp models.Path) (*kronk.Kronk, error) { - if err := kronk.Init(); err != nil { - return nil, fmt.Errorf("unable to init kronk: %w", err) - } - - cfg := p.options.modelConfig - cfg.ModelFiles = mp.ModelFiles - - krn, err := kronk.New(cfg) - if err != nil { - return nil, fmt.Errorf("unable to create inference model: %w", err) - } - - return krn, nil -} diff --git a/providers/kronk/language_model.go b/providers/kronk/language_model.go deleted file mode 100644 index dba0adce8..000000000 --- a/providers/kronk/language_model.go +++ /dev/null @@ -1,669 +0,0 @@ -package kronk - -import ( - "context" - "encoding/json" - "errors" - "io" - - "charm.land/fantasy" - "charm.land/fantasy/object" - "github.com/ardanlabs/kronk/sdk/kronk" - "github.com/ardanlabs/kronk/sdk/kronk/model" - xjson "github.com/charmbracelet/x/json" - "github.com/google/uuid" -) - -type languageModel struct { - provider string - modelID string - kronk *kronk.Kronk - objectMode fantasy.ObjectMode - prepareCallFunc LanguageModelPrepareCallFunc - mapFinishReasonFunc LanguageModelMapFinishReasonFunc - toPromptFunc LanguageModelToPromptFunc -} - -// LanguageModelOption is a function that configures a languageModel. -type LanguageModelOption func(*languageModel) - -// WithLanguageModelPrepareCallFunc sets the prepare call function for the language model. -func WithLanguageModelPrepareCallFunc(fn LanguageModelPrepareCallFunc) LanguageModelOption { - return func(l *languageModel) { - l.prepareCallFunc = fn - } -} - -// WithLanguageModelMapFinishReasonFunc sets the map finish reason function for the language model. -func WithLanguageModelMapFinishReasonFunc(fn LanguageModelMapFinishReasonFunc) LanguageModelOption { - return func(l *languageModel) { - l.mapFinishReasonFunc = fn - } -} - -// WithLanguageModelToPromptFunc sets the to prompt function for the language model. -func WithLanguageModelToPromptFunc(fn LanguageModelToPromptFunc) LanguageModelOption { - return func(l *languageModel) { - l.toPromptFunc = fn - } -} - -// WithLanguageModelObjectMode sets the object generation mode. -func WithLanguageModelObjectMode(om fantasy.ObjectMode) LanguageModelOption { - return func(l *languageModel) { - l.objectMode = om - } -} - -func newLanguageModel(modelID string, provider string, krn *kronk.Kronk, opts ...LanguageModelOption) *languageModel { - lm := languageModel{ - modelID: modelID, - provider: provider, - kronk: krn, - objectMode: fantasy.ObjectModeAuto, - prepareCallFunc: DefaultPrepareCallFunc, - mapFinishReasonFunc: DefaultMapFinishReasonFunc, - toPromptFunc: DefaultToPrompt, - } - - for _, o := range opts { - o(&lm) - } - - return &lm -} - -type streamToolCall struct { - id string - name string - arguments string - hasFinished bool -} - -// Model implements fantasy.LanguageModel. -func (l *languageModel) Model() string { - return l.modelID -} - -// Provider implements fantasy.LanguageModel. -func (l *languageModel) Provider() string { - return l.provider -} - -func (l *languageModel) prepareDocument(call fantasy.Call) (model.D, []fantasy.CallWarning, error) { - messages, warnings := l.toPromptFunc(call.Prompt, l.provider, l.modelID) - - if call.TopK != nil { - warnings = append(warnings, fantasy.CallWarning{ - Type: fantasy.CallWarningTypeUnsupportedSetting, - Setting: "top_k", - }) - } - - d := model.D{ - "messages": messages, - } - - if call.MaxOutputTokens != nil { - d["max_tokens"] = *call.MaxOutputTokens - } - - if call.Temperature != nil { - d["temperature"] = *call.Temperature - } - - if call.TopP != nil { - d["top_p"] = *call.TopP - } - - if call.FrequencyPenalty != nil { - warnings = append(warnings, fantasy.CallWarning{ - Type: fantasy.CallWarningTypeUnsupportedSetting, - Setting: "frequency_penalty", - Details: "frequency_penalty is not supported by Kronk", - }) - } - - if call.PresencePenalty != nil { - warnings = append(warnings, fantasy.CallWarning{ - Type: fantasy.CallWarningTypeUnsupportedSetting, - Setting: "presence_penalty", - Details: "presence_penalty is not supported by Kronk", - }) - } - - optionsWarnings, err := l.prepareCallFunc(l, d, call) - if err != nil { - return nil, nil, err - } - - if len(optionsWarnings) > 0 { - warnings = append(warnings, optionsWarnings...) - } - - if len(call.Tools) > 0 { - tools, toolWarnings := toKronkTools(call.Tools) - d["tools"] = tools - warnings = append(warnings, toolWarnings...) - } - - return d, warnings, nil -} - -// Generate implements fantasy.LanguageModel. -func (l *languageModel) Generate(ctx context.Context, call fantasy.Call) (*fantasy.Response, error) { - d, warnings, err := l.prepareDocument(call) - if err != nil { - return nil, err - } - - ch, err := l.kronk.ChatStreaming(ctx, d) - if err != nil { - return nil, toProviderErr(err) - } - - var lastResponse model.ChatResponse - var fullContent string - - for resp := range ch { - lastResponse = resp - - if len(resp.Choices) > 0 && resp.Choices[0].Delta != nil { - switch resp.Choices[0].FinishReason() { - case model.FinishReasonError: - return nil, &fantasy.Error{Title: "model error", Message: resp.Choices[0].Delta.Content} - - case model.FinishReasonStop, model.FinishReasonTool: - // Final response already contains full accumulated content in Delta.Content, - // so we use it directly instead of continuing to accumulate. - fullContent = resp.Choices[0].Delta.Content - - default: - fullContent += resp.Choices[0].Delta.Content - } - } - } - - if len(lastResponse.Choices) == 0 { - return nil, &fantasy.Error{Title: "no response", Message: "no response generated"} - } - - choice := lastResponse.Choices[0] - var content []fantasy.Content - if choice.Delta != nil { - content = make([]fantasy.Content, 0, 1+len(choice.Delta.ToolCalls)) - } - - if fullContent != "" { - content = append(content, fantasy.TextContent{ - Text: fullContent, - }) - } - - if choice.Delta != nil { - for _, tc := range choice.Delta.ToolCalls { - // Marshal the underlying map directly, not the ToolCallArguments type - // which has a custom MarshalJSON that double-encodes to a JSON string. - argsJSON, _ := json.Marshal(map[string]any(tc.Function.Arguments)) - - content = append(content, fantasy.ToolCallContent{ - ProviderExecuted: false, - ToolCallID: tc.ID, - ToolName: tc.Function.Name, - Input: string(argsJSON), - }) - } - } - - usage := fantasy.Usage{} - if lastResponse.Usage != nil { - usage = fantasy.Usage{ - InputTokens: int64(lastResponse.Usage.PromptTokens), - OutputTokens: int64(lastResponse.Usage.CompletionTokens), - TotalTokens: int64(lastResponse.Usage.PromptTokens + lastResponse.Usage.CompletionTokens), - ReasoningTokens: int64(lastResponse.Usage.ReasoningTokens), - } - } - - mappedFinishReason := l.mapFinishReasonFunc(choice.FinishReason()) - if choice.Delta != nil && len(choice.Delta.ToolCalls) > 0 { - mappedFinishReason = fantasy.FinishReasonToolCalls - } - - providerMetadata := fantasy.ProviderMetadata{} - if lastResponse.Usage != nil { - providerMetadata = fantasy.ProviderMetadata{ - Name: &ProviderMetadata{ - TokensPerSecond: lastResponse.Usage.TokensPerSecond, - OutputTokens: int64(lastResponse.Usage.OutputTokens), - }, - } - } - - resp := fantasy.Response{ - Content: content, - Usage: usage, - FinishReason: mappedFinishReason, - ProviderMetadata: providerMetadata, - Warnings: warnings, - } - - return &resp, nil -} - -// Stream implements fantasy.LanguageModel. -func (l *languageModel) Stream(ctx context.Context, call fantasy.Call) (fantasy.StreamResponse, error) { - d, warnings, err := l.prepareDocument(call) - if err != nil { - return nil, err - } - - ch, err := l.kronk.ChatStreaming(ctx, d) - if err != nil { - return nil, toProviderErr(err) - } - - isActiveText := false - isActiveReasoning := false - toolCalls := make(map[int]streamToolCall) - - providerMetadata := fantasy.ProviderMetadata{ - Name: &ProviderMetadata{}, - } - - var usage fantasy.Usage - var finishReason string - - return func(yield func(fantasy.StreamPart) bool) { - if len(warnings) > 0 { - if !yield(fantasy.StreamPart{ - Type: fantasy.StreamPartTypeWarnings, - Warnings: warnings, - }) { - return - } - } - - toolIndex := 0 - for resp := range ch { - if len(resp.Choices) == 0 { - continue - } - - choice := resp.Choices[0] - if choice.Delta == nil { - continue - } - - if resp.Usage != nil { - usage = fantasy.Usage{ - InputTokens: int64(resp.Usage.PromptTokens), - OutputTokens: int64(resp.Usage.CompletionTokens), - TotalTokens: int64(resp.Usage.PromptTokens + resp.Usage.CompletionTokens), - ReasoningTokens: int64(resp.Usage.ReasoningTokens), - } - - if pm, ok := providerMetadata[Name]; ok { - if metadata, ok := pm.(*ProviderMetadata); ok { - metadata.TokensPerSecond = resp.Usage.TokensPerSecond - metadata.OutputTokens = int64(resp.Usage.OutputTokens) - } - } - } - - if choice.FinishReason() != "" { - finishReason = choice.FinishReason() - } - - switch choice.FinishReason() { - case model.FinishReasonError: - yield(fantasy.StreamPart{ - Type: fantasy.StreamPartTypeError, - Error: &fantasy.Error{Title: "model error", Message: choice.Delta.Content}, - }) - return - - case model.FinishReasonTool: - if isActiveReasoning { - isActiveReasoning = false - if !yield(fantasy.StreamPart{ - Type: fantasy.StreamPartTypeReasoningEnd, - ID: "reasoning-0", - }) { - return - } - } - - if isActiveText { - isActiveText = false - if !yield(fantasy.StreamPart{ - Type: fantasy.StreamPartTypeTextEnd, - ID: "0", - }) { - return - } - } - - for _, tc := range choice.Delta.ToolCalls { - argsJSON, _ := json.Marshal(map[string]any(tc.Function.Arguments)) - argsStr := string(argsJSON) - - toolID := tc.ID - if toolID == "" { - toolID = uuid.NewString() - } - - if !yield(fantasy.StreamPart{ - Type: fantasy.StreamPartTypeToolInputStart, - ID: toolID, - ToolCallName: tc.Function.Name, - }) { - return - } - - if !yield(fantasy.StreamPart{ - Type: fantasy.StreamPartTypeToolInputDelta, - ID: toolID, - Delta: argsStr, - }) { - return - } - - if !yield(fantasy.StreamPart{ - Type: fantasy.StreamPartTypeToolInputEnd, - ID: toolID, - }) { - return - } - - if !yield(fantasy.StreamPart{ - Type: fantasy.StreamPartTypeToolCall, - ID: toolID, - ToolCallName: tc.Function.Name, - ToolCallInput: argsStr, - }) { - return - } - - toolCalls[toolIndex] = streamToolCall{ - id: toolID, - name: tc.Function.Name, - arguments: argsStr, - hasFinished: true, - } - toolIndex++ - } - - default: - if choice.Delta.Reasoning != "" { - if !isActiveReasoning { - isActiveReasoning = true - if !yield(fantasy.StreamPart{ - Type: fantasy.StreamPartTypeReasoningStart, - ID: "reasoning-0", - }) { - return - } - } - - if !yield(fantasy.StreamPart{ - Type: fantasy.StreamPartTypeReasoningDelta, - ID: "reasoning-0", - Delta: choice.Delta.Reasoning, - }) { - return - } - } - - hasToolCalls := len(choice.Delta.ToolCalls) > 0 - hasContent := choice.Delta.Content != "" - - if isActiveReasoning && (hasContent || hasToolCalls) { - isActiveReasoning = false - if !yield(fantasy.StreamPart{ - Type: fantasy.StreamPartTypeReasoningEnd, - ID: "reasoning-0", - }) { - return - } - } - - if hasContent { - if !isActiveText { - isActiveText = true - if !yield(fantasy.StreamPart{ - Type: fantasy.StreamPartTypeTextStart, - ID: "0", - }) { - return - } - } - - if !yield(fantasy.StreamPart{ - Type: fantasy.StreamPartTypeTextDelta, - ID: "0", - Delta: choice.Delta.Content, - }) { - return - } - } - - if hasToolCalls && isActiveText { - isActiveText = false - if !yield(fantasy.StreamPart{ - Type: fantasy.StreamPartTypeTextEnd, - ID: "0", - }) { - return - } - } - - for _, tc := range choice.Delta.ToolCalls { - argsJSON, _ := json.Marshal(map[string]any(tc.Function.Arguments)) - argsStr := string(argsJSON) - - switch existingTC, ok := toolCalls[toolIndex]; ok { - case true: - if existingTC.hasFinished { - continue - } - - existingTC.arguments += argsStr - - if !yield(fantasy.StreamPart{ - Type: fantasy.StreamPartTypeToolInputDelta, - ID: existingTC.id, - Delta: argsStr, - }) { - return - } - - toolCalls[toolIndex] = existingTC - - if xjson.IsValid(existingTC.arguments) { - if !yield(fantasy.StreamPart{ - Type: fantasy.StreamPartTypeToolInputEnd, - ID: existingTC.id, - }) { - return - } - - if !yield(fantasy.StreamPart{ - Type: fantasy.StreamPartTypeToolCall, - ID: existingTC.id, - ToolCallName: existingTC.name, - ToolCallInput: existingTC.arguments, - }) { - return - } - - existingTC.hasFinished = true - toolCalls[toolIndex] = existingTC - } - - case false: - toolID := tc.ID - if toolID == "" { - toolID = uuid.NewString() - } - - if !yield(fantasy.StreamPart{ - Type: fantasy.StreamPartTypeToolInputStart, - ID: toolID, - ToolCallName: tc.Function.Name, - }) { - return - } - - toolCalls[toolIndex] = streamToolCall{ - id: toolID, - name: tc.Function.Name, - arguments: argsStr, - } - - if argsStr != "" && argsStr != "null" { - if !yield(fantasy.StreamPart{ - Type: fantasy.StreamPartTypeToolInputDelta, - ID: toolID, - Delta: argsStr, - }) { - return - } - - if xjson.IsValid(argsStr) { - if !yield(fantasy.StreamPart{ - Type: fantasy.StreamPartTypeToolInputEnd, - ID: toolID, - }) { - return - } - - if !yield(fantasy.StreamPart{ - Type: fantasy.StreamPartTypeToolCall, - ID: toolID, - ToolCallName: tc.Function.Name, - ToolCallInput: argsStr, - }) { - return - } - - stc := toolCalls[toolIndex] - stc.hasFinished = true - toolCalls[toolIndex] = stc - } - } - - toolIndex++ - } - } - } - } - - if isActiveReasoning { - if !yield(fantasy.StreamPart{ - Type: fantasy.StreamPartTypeReasoningEnd, - ID: "reasoning-0", - }) { - return - } - } - - if isActiveText { - if !yield(fantasy.StreamPart{ - Type: fantasy.StreamPartTypeTextEnd, - ID: "0", - }) { - return - } - } - - mappedFinishReason := l.mapFinishReasonFunc(finishReason) - if len(toolCalls) > 0 { - mappedFinishReason = fantasy.FinishReasonToolCalls - } - - yield(fantasy.StreamPart{ - Type: fantasy.StreamPartTypeFinish, - Usage: usage, - FinishReason: mappedFinishReason, - ProviderMetadata: providerMetadata, - }) - }, nil -} - -// GenerateObject implements fantasy.LanguageModel. -func (l *languageModel) GenerateObject(ctx context.Context, call fantasy.ObjectCall) (*fantasy.ObjectResponse, error) { - switch l.objectMode { - case fantasy.ObjectModeText: - return object.GenerateWithText(ctx, l, call) - - case fantasy.ObjectModeTool: - return object.GenerateWithTool(ctx, l, call) - - default: - return object.GenerateWithTool(ctx, l, call) - } -} - -// StreamObject implements fantasy.LanguageModel. -func (l *languageModel) StreamObject(ctx context.Context, call fantasy.ObjectCall) (fantasy.ObjectStreamResponse, error) { - switch l.objectMode { - case fantasy.ObjectModeTool: - return object.StreamWithTool(ctx, l, call) - - case fantasy.ObjectModeText: - return object.StreamWithText(ctx, l, call) - - default: - return object.StreamWithTool(ctx, l, call) - } -} - -func toKronkTools(tools []fantasy.Tool) ([]model.D, []fantasy.CallWarning) { - var kronkTools []model.D - var warnings []fantasy.CallWarning - - for _, tool := range tools { - if tool.GetType() == fantasy.ToolTypeFunction { - ft, ok := tool.(fantasy.FunctionTool) - if !ok { - continue - } - - kronkTools = append(kronkTools, model.D{ - "type": "function", - "function": model.D{ - "name": ft.Name, - "description": ft.Description, - "parameters": ft.InputSchema, - }, - }) - - continue - } - - warnings = append(warnings, fantasy.CallWarning{ - Type: fantasy.CallWarningTypeUnsupportedTool, - Tool: tool, - Message: "tool is not supported", - }) - } - - return kronkTools, warnings -} - -func toProviderErr(err error) error { - if err == nil { - return nil - } - - if errors.Is(err, io.EOF) { - return nil - } - - return &fantasy.ProviderError{ - Title: "kronk error", - Message: err.Error(), - Cause: err, - } -} diff --git a/providers/kronk/language_model_hooks.go b/providers/kronk/language_model_hooks.go deleted file mode 100644 index a824f791d..000000000 --- a/providers/kronk/language_model_hooks.go +++ /dev/null @@ -1,269 +0,0 @@ -package kronk - -import ( - "encoding/base64" - "fmt" - "strings" - - "charm.land/fantasy" - "github.com/ardanlabs/kronk/sdk/kronk/model" -) - -// LanguageModelPrepareCallFunc is a function that prepares the call for the language model. -type LanguageModelPrepareCallFunc func(lm fantasy.LanguageModel, d model.D, call fantasy.Call) ([]fantasy.CallWarning, error) - -// LanguageModelMapFinishReasonFunc is a function that maps the finish reason for the language model. -type LanguageModelMapFinishReasonFunc func(finishReason string) fantasy.FinishReason - -// LanguageModelToPromptFunc is a function that handles converting fantasy prompts to Kronk SDK messages. -type LanguageModelToPromptFunc func(prompt fantasy.Prompt, provider, modelID string) ([]model.D, []fantasy.CallWarning) - -// DefaultPrepareCallFunc is the default implementation for preparing a call to the language model. -func DefaultPrepareCallFunc(_ fantasy.LanguageModel, d model.D, call fantasy.Call) ([]fantasy.CallWarning, error) { - if call.ProviderOptions == nil { - return nil, nil - } - - var warnings []fantasy.CallWarning - providerOptions := &ProviderOptions{} - if v, ok := call.ProviderOptions[Name]; ok { - providerOptions, ok = v.(*ProviderOptions) - if !ok { - return nil, &fantasy.Error{Title: "invalid argument", Message: "kronk provider options should be *kronk.ProviderOptions"} - } - } - - if providerOptions.TopK != nil { - d["top_k"] = *providerOptions.TopK - } - - if providerOptions.RepeatPenalty != nil { - d["repeat_penalty"] = *providerOptions.RepeatPenalty - } - - if providerOptions.Seed != nil { - d["seed"] = *providerOptions.Seed - } - - if providerOptions.MinP != nil { - d["min_p"] = *providerOptions.MinP - } - - if providerOptions.NumPredict != nil { - d["num_predict"] = *providerOptions.NumPredict - } - - if providerOptions.Stop != nil { - d["stop"] = providerOptions.Stop - } - - return warnings, nil -} - -// DefaultMapFinishReasonFunc is the default implementation for mapping finish reasons. -func DefaultMapFinishReasonFunc(finishReason string) fantasy.FinishReason { - switch finishReason { - case string(model.FinishReasonStop): - return fantasy.FinishReasonStop - - case string(model.FinishReasonTool): - return fantasy.FinishReasonToolCalls - - case string(model.FinishReasonError): - return fantasy.FinishReasonError - - default: - return fantasy.FinishReasonUnknown - } -} - -// DefaultToPrompt is the default implementation for converting fantasy prompts to Kronk SDK messages. -func DefaultToPrompt(prompt fantasy.Prompt, _ string, _ string) ([]model.D, []fantasy.CallWarning) { - var messages []model.D - var warnings []fantasy.CallWarning - - for _, msg := range prompt { - switch msg.Role { - case fantasy.MessageRoleSystem: - for _, c := range msg.Content { - if c.GetType() == fantasy.ContentTypeText { - textPart, ok := fantasy.AsMessagePart[fantasy.TextPart](c) - if !ok { - warnings = append(warnings, fantasy.CallWarning{ - Type: fantasy.CallWarningTypeOther, - Message: "system message text part does not have the right type", - }) - - continue - } - - messages = append(messages, model.TextMessage(model.RoleSystem, textPart.Text)) - } - } - - case fantasy.MessageRoleUser: - var content []model.D - for _, c := range msg.Content { - switch c.GetType() { - case fantasy.ContentTypeText: - textPart, ok := fantasy.AsMessagePart[fantasy.TextPart](c) - if !ok { - warnings = append(warnings, fantasy.CallWarning{ - Type: fantasy.CallWarningTypeOther, - Message: "user message text part does not have the right type", - }) - - continue - } - - content = append(content, model.D{ - "type": "text", - "text": textPart.Text, - }) - - case fantasy.ContentTypeFile: - filePart, ok := fantasy.AsMessagePart[fantasy.FilePart](c) - if !ok { - warnings = append(warnings, fantasy.CallWarning{ - Type: fantasy.CallWarningTypeOther, - Message: "user message file part does not have the right type", - }) - - continue - } - - switch { - case strings.HasPrefix(filePart.MediaType, "image/"): - base64Encoded := base64.StdEncoding.EncodeToString(filePart.Data) - data := "data:" + filePart.MediaType + ";base64," + base64Encoded - content = append(content, model.D{ - "type": "image_url", - "image_url": model.D{ - "url": data, - }, - }) - - default: - warnings = append(warnings, fantasy.CallWarning{ - Type: fantasy.CallWarningTypeOther, - Message: fmt.Sprintf("file part media type %s not supported", filePart.MediaType), - }) - } - } - } - - switch { - case len(content) == 1 && content[0]["type"] == "text": - messages = append(messages, model.TextMessage(model.RoleUser, content[0]["text"].(string))) - - case len(content) > 0: - messages = append(messages, model.D{ - "role": model.RoleUser, - "content": content, - }) - } - - case fantasy.MessageRoleAssistant: - var textContent string - var toolCalls []model.D - - for _, c := range msg.Content { - switch c.GetType() { - case fantasy.ContentTypeText: - textPart, ok := fantasy.AsMessagePart[fantasy.TextPart](c) - if !ok { - warnings = append(warnings, fantasy.CallWarning{ - Type: fantasy.CallWarningTypeOther, - Message: "assistant message text part does not have the right type", - }) - - continue - } - - textContent += textPart.Text - - case fantasy.ContentTypeToolCall: - toolCallPart, ok := fantasy.AsMessagePart[fantasy.ToolCallPart](c) - if !ok { - warnings = append(warnings, fantasy.CallWarning{ - Type: fantasy.CallWarningTypeOther, - Message: "assistant message tool part does not have the right type", - }) - - continue - } - - toolCalls = append(toolCalls, model.D{ - "id": toolCallPart.ToolCallID, - "type": "function", - "function": model.D{ - "name": toolCallPart.ToolName, - "arguments": toolCallPart.Input, - }, - }) - } - } - - assistantMsg := model.D{ - "role": model.RoleAssistant, - } - - if textContent != "" { - assistantMsg["content"] = textContent - } - - if len(toolCalls) > 0 { - assistantMsg["tool_calls"] = toolCalls - } - - if textContent != "" || len(toolCalls) > 0 { - messages = append(messages, assistantMsg) - } - - case fantasy.MessageRoleTool: - for _, c := range msg.Content { - if c.GetType() != fantasy.ContentTypeToolResult { - warnings = append(warnings, fantasy.CallWarning{ - Type: fantasy.CallWarningTypeOther, - Message: "tool message can only have tool result content", - }) - - continue - } - - toolResultPart, ok := fantasy.AsMessagePart[fantasy.ToolResultPart](c) - if !ok { - warnings = append(warnings, fantasy.CallWarning{ - Type: fantasy.CallWarningTypeOther, - Message: "tool message result part does not have the right type", - }) - - continue - } - - var resultContent string - switch toolResultPart.Output.GetType() { - case fantasy.ToolResultContentTypeText: - output, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentText](toolResultPart.Output) - if ok { - resultContent = output.Text - } - - case fantasy.ToolResultContentTypeError: - output, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentError](toolResultPart.Output) - if ok { - resultContent = output.Error.Error() - } - } - - messages = append(messages, model.D{ - "role": "tool", - "content": resultContent, - "tool_call_id": toolResultPart.ToolCallID, - }) - } - } - } - - return messages, warnings -} diff --git a/providers/kronk/options.go b/providers/kronk/options.go deleted file mode 100644 index 747920465..000000000 --- a/providers/kronk/options.go +++ /dev/null @@ -1,71 +0,0 @@ -package kronk - -import ( - "context" - "fmt" - - "charm.land/fantasy" - "github.com/ardanlabs/kronk/sdk/kronk/model" -) - -// Option defines a function that configures Kronk provider options. -type Option func(*options) - -// Logger is the function signature for logging download progress. -type Logger func(ctx context.Context, msg string, args ...any) - -type options struct { - name string - modelConfig model.Config - logger Logger - objectMode fantasy.ObjectMode - languageModelOptions []LanguageModelOption -} - -// WithName sets the name for the Kronk provider. -func WithName(name string) Option { - return func(o *options) { - o.name = name - } -} - -// WithModelConfig sets additional model configuration options. -func WithModelConfig(cfg model.Config) Option { - return func(o *options) { - o.modelConfig = cfg - } -} - -// WithLogger sets the logger function for download progress. -func WithLogger(logger Logger) Option { - return func(o *options) { - o.logger = logger - } -} - -// WithLanguageModelOptions sets the language model options for the Kronk provider. -func WithLanguageModelOptions(opts ...LanguageModelOption) Option { - return func(o *options) { - o.languageModelOptions = append(o.languageModelOptions, opts...) - } -} - -// WithObjectMode sets the object generation mode. -func WithObjectMode(om fantasy.ObjectMode) Option { - return func(o *options) { - o.objectMode = om - } -} - -// FmtLogger is a simple logger that prints to stdout using fmt.Printf. -func FmtLogger(_ context.Context, msg string, args ...any) { - fmt.Printf("%s:", msg) - - for i := 0; i < len(args); i += 2 { - if i+1 < len(args) { - fmt.Printf(" %v[%v]", args[i], args[i+1]) - } - } - - fmt.Println() -} diff --git a/providers/kronk/provider_options.go b/providers/kronk/provider_options.go deleted file mode 100644 index 217242ea4..000000000 --- a/providers/kronk/provider_options.go +++ /dev/null @@ -1,104 +0,0 @@ -package kronk - -import ( - "encoding/json" - - "charm.land/fantasy" -) - -// Global type identifiers for Kronk-specific provider data. -const ( - TypeProviderOptions = Name + ".options" - TypeProviderMetadata = Name + ".metadata" -) - -// Register Kronk provider-specific types with the global registry. -func init() { - fantasy.RegisterProviderType(TypeProviderOptions, func(data []byte) (fantasy.ProviderOptionsData, error) { - var v ProviderOptions - if err := json.Unmarshal(data, &v); err != nil { - return nil, err - } - return &v, nil - }) - - fantasy.RegisterProviderType(TypeProviderMetadata, func(data []byte) (fantasy.ProviderOptionsData, error) { - var v ProviderMetadata - if err := json.Unmarshal(data, &v); err != nil { - return nil, err - } - return &v, nil - }) -} - -// ProviderMetadata represents additional metadata from Kronk provider. -type ProviderMetadata struct { - TokensPerSecond float64 `json:"tokens_per_second"` - OutputTokens int64 `json:"output_tokens"` -} - -// Options implements the ProviderOptionsData interface. -func (*ProviderMetadata) Options() {} - -// MarshalJSON implements custom JSON marshaling with type info for ProviderMetadata. -func (m ProviderMetadata) MarshalJSON() ([]byte, error) { - type plain ProviderMetadata - return fantasy.MarshalProviderType(TypeProviderMetadata, plain(m)) -} - -// UnmarshalJSON implements custom JSON unmarshaling with type info for ProviderMetadata. -func (m *ProviderMetadata) UnmarshalJSON(data []byte) error { - type plain ProviderMetadata - var p plain - if err := fantasy.UnmarshalProviderType(data, &p); err != nil { - return err - } - *m = ProviderMetadata(p) - return nil -} - -// ProviderOptions represents additional options for Kronk provider. -type ProviderOptions struct { - TopK *int64 `json:"top_k"` - RepeatPenalty *float64 `json:"repeat_penalty"` - Seed *int64 `json:"seed"` - MinP *float64 `json:"min_p"` - NumPredict *int64 `json:"num_predict"` - Stop []string `json:"stop"` -} - -// Options implements the ProviderOptionsData interface. -func (*ProviderOptions) Options() {} - -// MarshalJSON implements custom JSON marshaling with type info for ProviderOptions. -func (o ProviderOptions) MarshalJSON() ([]byte, error) { - type plain ProviderOptions - return fantasy.MarshalProviderType(TypeProviderOptions, plain(o)) -} - -// UnmarshalJSON implements custom JSON unmarshaling with type info for ProviderOptions. -func (o *ProviderOptions) UnmarshalJSON(data []byte) error { - type plain ProviderOptions - var p plain - if err := fantasy.UnmarshalProviderType(data, &p); err != nil { - return err - } - *o = ProviderOptions(p) - return nil -} - -// NewProviderOptions creates new provider options for Kronk. -func NewProviderOptions(opts *ProviderOptions) fantasy.ProviderOptions { - return fantasy.ProviderOptions{ - Name: opts, - } -} - -// ParseOptions parses provider options from a map. -func ParseOptions(data map[string]any) (*ProviderOptions, error) { - var options ProviderOptions - if err := fantasy.ParseOptions(data, &options); err != nil { - return nil, err - } - return &options, nil -} From 15b7fa55ff503ba0a9b85007b7a85d58fd009196 Mon Sep 17 00:00:00 2001 From: Hugo Dutka Date: Fri, 13 Mar 2026 12:37:17 +0000 Subject: [PATCH 2/6] feat: anthropic computer use --- examples/computer-use/main.go | 192 ++++++ go.mod | 54 +- go.sum | 116 ++-- providers/anthropic/anthropic.go | 220 +++--- providers/anthropic/anthropic_test.go | 645 +++++++++++++----- providers/anthropic/computer_use.go | 483 +++++++++++++ providers/anthropic/computer_use_test.go | 294 ++++++++ providertests/anthropic_test.go | 169 +++++ .../claude-sonnet-4/computer_use.yaml | 63 ++ .../computer_use_streaming.yaml | 63 ++ 10 files changed, 1895 insertions(+), 404 deletions(-) create mode 100644 examples/computer-use/main.go create mode 100644 providers/anthropic/computer_use.go create mode 100644 providers/anthropic/computer_use_test.go create mode 100644 providertests/testdata/TestAnthropicComputerUse/claude-sonnet-4/computer_use.yaml create mode 100644 providertests/testdata/TestAnthropicComputerUse/claude-sonnet-4/computer_use_streaming.yaml diff --git a/examples/computer-use/main.go b/examples/computer-use/main.go new file mode 100644 index 000000000..41f870623 --- /dev/null +++ b/examples/computer-use/main.go @@ -0,0 +1,192 @@ +package main + +// This example demonstrates the full agent loop for Anthropic's computer use +// tool. It shows how to: +// +// 1. Wire up the provider, model, and computer use tool. +// 2. Parse incoming tool calls with ParseComputerUseInput. +// 3. Execute the action (stubbed here — in production you would drive a +// real VM, container, or VNC session). +// 4. Construct the result with the builder functions +// (NewComputerUseScreenshotResult, NewComputerUseErrorResult, etc.). +// 5. Append the result to the prompt and call Generate again. +// 6. Exit when Claude stops requesting tool calls. + +import ( + "context" + "fmt" + "os" + + "charm.land/fantasy" + "charm.land/fantasy/providers/anthropic" +) + +// takeScreenshot is a stub that simulates capturing a screenshot. +// In a real implementation this would capture the virtual display +// and return raw PNG bytes. +func takeScreenshot() ([]byte, error) { + // Placeholder: a minimal PNG header. + return []byte{ + 0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, + 0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, + }, nil +} + +// responseToAssistantParts converts response content into +// MessagePart values suitable for an assistant-role message. This +// preserves text and tool-call parts so Claude sees its own output +// in the conversation history on the next turn. +func responseToAssistantParts(content fantasy.ResponseContent) []fantasy.MessagePart { + var parts []fantasy.MessagePart + for _, c := range content { + switch c.GetType() { + case fantasy.ContentTypeText: + tc, ok := fantasy.AsContentType[fantasy.TextContent](c) + if ok { + parts = append(parts, fantasy.TextPart{Text: tc.Text}) + } + case fantasy.ContentTypeToolCall: + tc, ok := fantasy.AsContentType[fantasy.ToolCallContent](c) + if ok { + parts = append(parts, fantasy.ToolCallPart{ + ToolCallID: tc.ToolCallID, + ToolName: tc.ToolName, + Input: tc.Input, + }) + } + } + } + return parts +} + +func main() { + // Set up the Anthropic provider. + provider, err := anthropic.New(anthropic.WithAPIKey(os.Getenv("ANTHROPIC_API_KEY"))) + if err != nil { + fmt.Fprintln(os.Stderr, "could not create provider:", err) + os.Exit(1) + } + + ctx := context.Background() + + // Pick the model. + model, err := provider.LanguageModel(ctx, "claude-opus-4-6") + if err != nil { + fmt.Fprintln(os.Stderr, "could not get language model:", err) + os.Exit(1) + } + + // Create a computer use tool. This tells Claude the dimensions + // of the virtual display it will be controlling. + computerTool := anthropic.NewComputerUseTool(anthropic.ComputerUseToolOptions{ + DisplayWidthPx: 1920, + DisplayHeightPx: 1080, + ToolVersion: anthropic.ComputerUse20251124, + }) + + // Build the initial Call with a prompt and the computer use tool. + call := fantasy.Call{ + Prompt: fantasy.Prompt{ + fantasy.NewUserMessage("Take a screenshot of the desktop"), + }, + Tools: []fantasy.Tool{computerTool}, + } + + // --- Agent loop --- + // Keep generating until Claude stops requesting tool calls. + const maxIterations = 10 + for i := range maxIterations { + fmt.Printf("\n=== Iteration %d ===\n", i+1) + + resp, err := model.Generate(ctx, call) + if err != nil { + fmt.Fprintln(os.Stderr, "generate failed:", err) + os.Exit(1) + } + + // Print any text Claude included. + if text := resp.Content.Text(); text != "" { + fmt.Println("Claude said:", text) + } + + // Collect tool calls from the response. + toolCalls := resp.Content.ToolCalls() + if len(toolCalls) == 0 { + fmt.Println("No more tool calls — done.") + break + } + + // Process each tool call and build result messages. + var results []fantasy.MessagePart + for _, tc := range toolCalls { + fmt.Printf("Tool call: %s (id=%s)\n", tc.ToolName, tc.ToolCallID) + + action, err := anthropic.ParseComputerUseInput(tc.Input) + if err != nil { + fmt.Fprintln(os.Stderr, "could not parse tool input:", err) + result := anthropic.NewComputerUseErrorResult(tc.ToolCallID, err) + results = append(results, result) + continue + } + + // Execute the action (stubbed) and build the result. + var result fantasy.ToolResultPart + switch action.Action { + case anthropic.ActionScreenshot: + fmt.Println(" -> capturing screenshot") + png, err := takeScreenshot() + if err != nil { + result = anthropic.NewComputerUseErrorResult(tc.ToolCallID, err) + } else { + result = anthropic.NewComputerUseScreenshotResult(tc.ToolCallID, png) + } + + case anthropic.ActionLeftClick: + fmt.Printf(" -> left-click at %v\n", action.Coordinate) + // In production: execute the click, then screenshot. + png, err := takeScreenshot() + if err != nil { + result = anthropic.NewComputerUseErrorResult(tc.ToolCallID, err) + } else { + result = anthropic.NewComputerUseScreenshotResult(tc.ToolCallID, png) + } + + case anthropic.ActionType: + fmt.Printf(" -> typing %q\n", action.Text) + png, err := takeScreenshot() + if err != nil { + result = anthropic.NewComputerUseErrorResult(tc.ToolCallID, err) + } else { + result = anthropic.NewComputerUseScreenshotResult(tc.ToolCallID, png) + } + + default: + fmt.Printf(" -> handling %s\n", action.Action) + png, err := takeScreenshot() + if err != nil { + result = anthropic.NewComputerUseErrorResult(tc.ToolCallID, err) + } else { + result = anthropic.NewComputerUseScreenshotResult(tc.ToolCallID, png) + } + } + + results = append(results, result) + } + + // Append the assistant response and tool results to the + // prompt for the next iteration. + call.Prompt = append(call.Prompt, + // Echo back the assistant's response so Claude sees + // its own tool calls in context. + fantasy.Message{ + Role: fantasy.MessageRoleAssistant, + Content: responseToAssistantParts(resp.Content), + }, + // Provide the tool results. + fantasy.Message{ + Role: fantasy.MessageRoleTool, + Content: results, + }, + ) + } +} diff --git a/go.mod b/go.mod index 7a87f3155..292c77b2b 100644 --- a/go.mod +++ b/go.mod @@ -5,8 +5,8 @@ go 1.25.0 require ( charm.land/x/vcr v0.1.1 cloud.google.com/go/auth v0.18.2 - github.com/aws/aws-sdk-go-v2 v1.41.3 - github.com/aws/aws-sdk-go-v2/config v1.32.11 + github.com/aws/aws-sdk-go-v2 v1.41.4 + github.com/aws/aws-sdk-go-v2/config v1.32.12 github.com/aws/smithy-go v1.24.2 github.com/charmbracelet/anthropic-sdk-go v0.0.0-20260223140439-63879b0b8dab github.com/charmbracelet/x/exp/slice v0.0.0-20250904123553-b4e2667e5ad5 @@ -15,10 +15,10 @@ require ( github.com/google/uuid v1.6.0 github.com/joho/godotenv v1.5.1 github.com/kaptinlin/jsonschema v0.6.10 - github.com/openai/openai-go/v2 v2.7.1 + github.com/openai/openai-go/v3 v3.28.0 github.com/stretchr/testify v1.11.1 golang.org/x/oauth2 v0.36.0 - google.golang.org/genai v1.49.0 + google.golang.org/genai v1.50.0 ) require ( @@ -27,18 +27,18 @@ require ( cloud.google.com/go/compute/metadata v0.9.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect - github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect - github.com/aws/aws-sdk-go-v2/credentials v1.19.11 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect - github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 // indirect - github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 // indirect + github.com/aws/aws-sdk-go-v2/credentials v1.19.12 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cncf/xds/go v0.0.0-20260202195803-dba9d589def2 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect @@ -52,7 +52,7 @@ require ( github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/s2a-go v0.1.9 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.12 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect github.com/googleapis/gax-go/v2 v2.17.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/kaptinlin/go-i18n v0.2.4 // indirect @@ -64,21 +64,21 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect - go.opentelemetry.io/otel v1.40.0 // indirect - go.opentelemetry.io/otel/metric v1.40.0 // indirect - go.opentelemetry.io/otel/trace v1.40.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect + go.opentelemetry.io/otel v1.42.0 // indirect + go.opentelemetry.io/otel/metric v1.42.0 // indirect + go.opentelemetry.io/otel/trace v1.42.0 // indirect go.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect golang.org/x/crypto v0.48.0 // indirect golang.org/x/net v0.51.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.41.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.42.0 // indirect golang.org/x/text v0.34.0 // indirect - golang.org/x/time v0.14.0 // indirect - google.golang.org/api v0.269.0 // indirect + golang.org/x/time v0.15.0 // indirect + google.golang.org/api v0.270.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect - google.golang.org/grpc v1.79.1 // indirect + google.golang.org/grpc v1.79.2 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/dnaeon/go-vcr.v4 v4.0.6-0.20251110073552-01de4eb40290 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 144b42676..6ad062ae6 100644 --- a/go.sum +++ b/go.sum @@ -16,34 +16,34 @@ github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xP github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2 h1:XHOnouVk1mxXfQidrMEnLlPk9UMeRtyBTnEFtxkV0kU= github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= -github.com/aws/aws-sdk-go-v2 v1.41.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA= -github.com/aws/aws-sdk-go-v2 v1.41.3/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q= -github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c= -github.com/aws/aws-sdk-go-v2/config v1.32.11 h1:ftxI5sgz8jZkckuUHXfC/wMUc8u3fG1vQS0plr2F2Zs= -github.com/aws/aws-sdk-go-v2/config v1.32.11/go.mod h1:twF11+6ps9aNRKEDimksp923o44w/Thk9+8YIlzWMmo= -github.com/aws/aws-sdk-go-v2/credentials v1.19.11 h1:NdV8cwCcAXrCWyxArt58BrvZJ9pZ9Fhf9w6Uh5W3Uyc= -github.com/aws/aws-sdk-go-v2/credentials v1.19.11/go.mod h1:30yY2zqkMPdrvxBqzI9xQCM+WrlrZKSOpSJEsylVU+8= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 h1:INUvJxmhdEbVulJYHI061k4TVuS3jzzthNvjqvVvTKM= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19/go.mod h1:FpZN2QISLdEBWkayloda+sZjVJL+e9Gl0k1SyTgcswU= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 h1:/sECfyq2JTifMI2JPyZ4bdRN77zJmr6SrS1eL3augIA= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19/go.mod h1:dMf8A5oAqr9/oxOfLkC/c2LU/uMcALP0Rgn2BD5LWn0= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 h1:AWeJMk33GTBf6J20XJe6qZoRSJo0WfUhsMdUKhoODXE= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19/go.mod h1:+GWrYoaAsV7/4pNHpwh1kiNLXkKaSoppxQq9lbH8Ejw= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 h1:clHU5fm//kWS1C2HgtgWxfQbFbx4b6rx+5jzhgX9HrI= -github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 h1:XAq62tBTJP/85lFD5oqOOe7YYgWxY9LvWq8plyDvDVg= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 h1:X1Tow7suZk9UCJHE1Iw9GMZJJl0dAnKXXP1NaSDHwmw= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19/go.mod h1:/rARO8psX+4sfjUQXp5LLifjUt8DuATZ31WptNJTyQA= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 h1:Y2cAXlClHsXkkOvWZFXATr34b0hxxloeQu/pAZz2row= -github.com/aws/aws-sdk-go-v2/service/signin v1.0.7/go.mod h1:idzZ7gmDeqeNrSPkdbtMp9qWMgcBwykA7P7Rzh5DXVU= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 h1:iSsvB9EtQ09YrsmIc44Heqlx5ByGErqhPK1ZQLppias= -github.com/aws/aws-sdk-go-v2/service/sso v1.30.12/go.mod h1:fEWYKTRGoZNl8tZ77i61/ccwOMJdGxwOhWCkp6TXAr0= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 h1:EnUdUqRP1CNzt2DkV67tJx6XDN4xlfBFm+bzeNOQVb0= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16/go.mod h1:Jic/xv0Rq/pFNCh3WwpH4BEqdbSAl+IyHro8LbibHD8= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 h1:XQTQTF75vnug2TXS8m7CVJfC2nniYPZnO1D4Np761Oo= -github.com/aws/aws-sdk-go-v2/service/sts v1.41.8/go.mod h1:Xgx+PR1NUOjNmQY+tRMnouRp83JRM8pRMw/vCaVhPkI= +github.com/aws/aws-sdk-go-v2 v1.41.4 h1:10f50G7WyU02T56ox1wWXq+zTX9I1zxG46HYuG1hH/k= +github.com/aws/aws-sdk-go-v2 v1.41.4/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 h1:N4lRUXZpZ1KVEUn6hxtco/1d2lgYhNn1fHkkl8WhlyQ= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI= +github.com/aws/aws-sdk-go-v2/config v1.32.12 h1:O3csC7HUGn2895eNrLytOJQdoL2xyJy0iYXhoZ1OmP0= +github.com/aws/aws-sdk-go-v2/config v1.32.12/go.mod h1:96zTvoOFR4FURjI+/5wY1vc1ABceROO4lWgWJuxgy0g= +github.com/aws/aws-sdk-go-v2/credentials v1.19.12 h1:oqtA6v+y5fZg//tcTWahyN9PEn5eDU/Wpvc2+kJ4aY8= +github.com/aws/aws-sdk-go-v2/credentials v1.19.12/go.mod h1:U3R1RtSHx6NB0DvEQFGyf/0sbrpJrluENHdPy1j/3TE= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20 h1:zOgq3uezl5nznfoK3ODuqbhVg1JzAGDUhXOsU0IDCAo= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.20/go.mod h1:z/MVwUARehy6GAg/yQ1GO2IMl0k++cu1ohP9zo887wE= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20 h1:CNXO7mvgThFGqOFgbNAP2nol2qAWBOGfqR/7tQlvLmc= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.20/go.mod h1:oydPDJKcfMhgfcgBUZaG+toBbwy8yPWubJXBVERtI4o= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20 h1:tN6W/hg+pkM+tf9XDkWUbDEjGLb+raoBMFsTodcoYKw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.20/go.mod h1:YJ898MhD067hSHA6xYCx5ts/jEd8BSOLtQDL3iZsvbc= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 h1:qYQ4pzQ2Oz6WpQ8T3HvGHnZydA72MnLuFK9tJwmrbHw= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 h1:5EniKhLZe4xzL7a+fU3C2tfUN4nWIqlLesfrjkuPFTY= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20 h1:2HvVAIq+YqgGotK6EkMf+KIEqTISmTYh5zLpYyeTo1Y= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.20/go.mod h1:V4X406Y666khGa8ghKmphma/7C0DAtEQYhkq9z4vpbk= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.8 h1:0GFOLzEbOyZABS3PhYfBIx2rNBACYcKty+XGkTgw1ow= +github.com/aws/aws-sdk-go-v2/service/signin v1.0.8/go.mod h1:LXypKvk85AROkKhOG6/YEcHFPoX+prKTowKnVdcaIxE= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.13 h1:kiIDLZ005EcKomYYITtfsjn7dtOwHDOFy7IbPXKek2o= +github.com/aws/aws-sdk-go-v2/service/sso v1.30.13/go.mod h1:2h/xGEowcW/g38g06g3KpRWDlT+OTfxxI0o1KqayAB8= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17 h1:jzKAXIlhZhJbnYwHbvUQZEB8KfgAEuG0dc08Bkda7NU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.17/go.mod h1:Al9fFsXjv4KfbzQHGe6V4NZSZQXecFcvaIF4e70FoRA= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.9 h1:Cng+OOwCHmFljXIxpEVXAGMnBia8MSU6Ch5i9PgBkcU= +github.com/aws/aws-sdk-go-v2/service/sts v1.41.9/go.mod h1:LrlIndBDdjA/EeXeyNBle+gyCwTlizzW5ycgWnvIxkk= github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -87,8 +87,8 @@ github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.12 h1:Fg+zsqzYEs1ZnvmcztTYxhgCBsx3eEhEwQ1W/lHq/sQ= -github.com/googleapis/enterprise-certificate-proxy v0.3.12/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= +github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= +github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc= github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= @@ -109,8 +109,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/openai/openai-go/v2 v2.7.1 h1:/tfvTJhfv7hTSL8mWwc5VL4WLLSDL5yn9VqVykdu9r8= -github.com/openai/openai-go/v2 v2.7.1/go.mod h1:jrJs23apqJKKbT+pqtFgNKpRju/KP9zpUTZhz3GElQE= +github.com/openai/openai-go/v3 v3.28.0 h1:2+FfrCVMdGXSQrBv1tLWtokm+BU7+3hJ/8rAHPQ63KM= +github.com/openai/openai-go/v3 v3.28.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= @@ -135,20 +135,20 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0 h1:XmiuHzgJt067+a6kwyAzkhXooYVv3/TOw9cM2VfJgUM= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.65.0/go.mod h1:KDgtbWKTQs4bM+VPUr6WlL9m/WXcmkCcBlIzqxPGzmI= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= -go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= -go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= -go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= -go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= -go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= -go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= -go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= -go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= -go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= -go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg= +go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= +go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= +go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= +go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= +go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts= +go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA= +go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= +go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= +go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go= go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= @@ -157,24 +157,24 @@ golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= -golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= -golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= +golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= -google.golang.org/api v0.269.0 h1:qDrTOxKUQ/P0MveH6a7vZ+DNHxJQjtGm/uvdbdGXCQg= -google.golang.org/api v0.269.0/go.mod h1:N8Wpcu23Tlccl0zSHEkcAZQKDLdquxK+l9r2LkwAauE= -google.golang.org/genai v1.49.0 h1:Se+QJaH2GYK1aaR1o5S38mlU2GD5FnVvP76nfkV7LH0= -google.golang.org/genai v1.49.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= +google.golang.org/api v0.270.0 h1:4rJZbIuWSTohczG9mG2ukSDdt9qKx4sSSHIydTN26L4= +google.golang.org/api v0.270.0/go.mod h1:5+H3/8DlXpQWrSz4RjGGwz5HfJAQSEI8Bc6JqQNH77U= +google.golang.org/genai v1.50.0 h1:yHKV/vjoeN9PJ3iF0ur4cBZco4N3Kl7j09rMq7XSoWk= +google.golang.org/genai v1.50.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= -google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= -google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU= +google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/providers/anthropic/anthropic.go b/providers/anthropic/anthropic.go index 1a7004205..eb70d895c 100644 --- a/providers/anthropic/anthropic.go +++ b/providers/anthropic/anthropic.go @@ -10,7 +10,6 @@ import ( "fmt" "io" "maps" - "math" "strings" "charm.land/fantasy" @@ -229,13 +228,13 @@ func (a languageModel) Provider() string { return a.provider } -func (a languageModel) prepareParams(call fantasy.Call) (*anthropic.MessageNewParams, []fantasy.CallWarning, error) { +func (a languageModel) prepareParams(call fantasy.Call) (*anthropic.MessageNewParams, []json.RawMessage, []fantasy.CallWarning, error) { params := &anthropic.MessageNewParams{} providerOptions := &ProviderOptions{} if v, ok := call.ProviderOptions[Name]; ok { providerOptions, ok = v.(*ProviderOptions) if !ok { - return nil, nil, &fantasy.Error{Title: "invalid argument", Message: "anthropic provider options should be *anthropic.ProviderOptions"} + return nil, nil, nil, &fantasy.Error{Title: "invalid argument", Message: "anthropic provider options should be *anthropic.ProviderOptions"} } } sendReasoning := true @@ -286,7 +285,7 @@ func (a languageModel) prepareParams(call fantasy.Call) (*anthropic.MessageNewPa params.Thinking.OfAdaptive = &adaptive case providerOptions.Thinking != nil: if providerOptions.Thinking.BudgetTokens == 0 { - return nil, nil, &fantasy.Error{Title: "no budget", Message: "thinking requires budget"} + return nil, nil, nil, &fantasy.Error{Title: "no budget", Message: "thinking requires budget"} } params.Thinking = anthropic.ThinkingConfigParamOfEnabled(providerOptions.Thinking.BudgetTokens) if call.Temperature != nil { @@ -315,20 +314,22 @@ func (a languageModel) prepareParams(call fantasy.Call) (*anthropic.MessageNewPa } } + var rawTools []json.RawMessage if len(call.Tools) > 0 { disableParallelToolUse := false if providerOptions.DisableParallelToolUse != nil { disableParallelToolUse = *providerOptions.DisableParallelToolUse } - tools, toolChoice, toolWarnings := a.toTools(call.Tools, call.ToolChoice, disableParallelToolUse) - params.Tools = tools + var toolChoice *anthropic.ToolChoiceUnionParam + var toolWarnings []fantasy.CallWarning + rawTools, toolChoice, toolWarnings = a.toTools(call.Tools, call.ToolChoice, disableParallelToolUse) if toolChoice != nil { params.ToolChoice = *toolChoice } warnings = append(warnings, toolWarnings...) } - return params, warnings, nil + return params, rawTools, warnings, nil } func (a *provider) Name() string { @@ -408,120 +409,7 @@ func groupIntoBlocks(prompt fantasy.Prompt) []*messageBlock { return blocks } -func anyToStringSlice(v any) []string { - switch typed := v.(type) { - case []string: - if len(typed) == 0 { - return nil - } - out := make([]string, len(typed)) - copy(out, typed) - return out - case []any: - if len(typed) == 0 { - return nil - } - out := make([]string, 0, len(typed)) - for _, item := range typed { - s, ok := item.(string) - if !ok || s == "" { - continue - } - out = append(out, s) - } - if len(out) == 0 { - return nil - } - return out - default: - return nil - } -} - -const maxExactIntFloat64 = float64(1<<53 - 1) - -func anyToInt64(v any) (int64, bool) { - switch typed := v.(type) { - case int: - return int64(typed), true - case int8: - return int64(typed), true - case int16: - return int64(typed), true - case int32: - return int64(typed), true - case int64: - return typed, true - case uint: - u64 := uint64(typed) - if u64 > math.MaxInt64 { - return 0, false - } - return int64(u64), true - case uint8: - return int64(typed), true - case uint16: - return int64(typed), true - case uint32: - return int64(typed), true - case uint64: - if typed > math.MaxInt64 { - return 0, false - } - return int64(typed), true - case float32: - f := float64(typed) - if math.Trunc(f) != f || math.IsNaN(f) || math.IsInf(f, 0) || f < -maxExactIntFloat64 || f > maxExactIntFloat64 { - return 0, false - } - return int64(f), true - case float64: - if math.Trunc(typed) != typed || math.IsNaN(typed) || math.IsInf(typed, 0) || typed < -maxExactIntFloat64 || typed > maxExactIntFloat64 { - return 0, false - } - return int64(typed), true - case json.Number: - parsed, err := typed.Int64() - if err != nil { - return 0, false - } - return parsed, true - default: - return 0, false - } -} - -func anyToUserLocation(v any) *UserLocation { - switch typed := v.(type) { - case *UserLocation: - return typed - case UserLocation: - loc := typed - return &loc - case map[string]any: - loc := &UserLocation{} - if city, ok := typed["city"].(string); ok { - loc.City = city - } - if region, ok := typed["region"].(string); ok { - loc.Region = region - } - if country, ok := typed["country"].(string); ok { - loc.Country = country - } - if timezone, ok := typed["timezone"].(string); ok { - loc.Timezone = timezone - } - if loc.City == "" && loc.Region == "" && loc.Country == "" && loc.Timezone == "" { - return nil - } - return loc - default: - return nil - } -} - -func (a languageModel) toTools(tools []fantasy.Tool, toolChoice *fantasy.ToolChoice, disableParallelToolCalls bool) (anthropicTools []anthropic.ToolUnionParam, anthropicToolChoice *anthropic.ToolChoiceUnionParam, warnings []fantasy.CallWarning) { +func (a languageModel) toTools(tools []fantasy.Tool, toolChoice *fantasy.ToolChoice, disableParallelToolCalls bool) (rawTools []json.RawMessage, anthropicToolChoice *anthropic.ToolChoiceUnionParam, warnings []fantasy.CallWarning) { for _, tool := range tools { if tool.GetType() == fantasy.ToolTypeFunction { ft, ok := tool.(fantasy.FunctionTool) @@ -551,7 +439,16 @@ func (a languageModel) toTools(tools []fantasy.Tool, toolChoice *fantasy.ToolCho if cacheControl != nil { anthropicTool.CacheControl = anthropic.NewCacheControlEphemeralParam() } - anthropicTools = append(anthropicTools, anthropic.ToolUnionParam{OfTool: &anthropicTool}) + raw, err := json.Marshal(anthropic.ToolUnionParam{OfTool: &anthropicTool}) + if err != nil { + warnings = append(warnings, fantasy.CallWarning{ + Type: fantasy.CallWarningTypeOther, + Tool: tool, + Message: fmt.Sprintf("failed to marshal function tool: %v", err), + }) + continue + } + rawTools = append(rawTools, raw) continue } if tool.GetType() == fantasy.ToolTypeProviderDefined { @@ -563,16 +460,16 @@ func (a languageModel) toTools(tools []fantasy.Tool, toolChoice *fantasy.ToolCho case "web_search": webSearchTool := anthropic.WebSearchTool20250305Param{} if pt.Args != nil { - if domains := anyToStringSlice(pt.Args["allowed_domains"]); len(domains) > 0 { + if domains, ok := pt.Args["allowed_domains"].([]string); ok && len(domains) > 0 { webSearchTool.AllowedDomains = domains } - if domains := anyToStringSlice(pt.Args["blocked_domains"]); len(domains) > 0 { + if domains, ok := pt.Args["blocked_domains"].([]string); ok && len(domains) > 0 { webSearchTool.BlockedDomains = domains } - if maxUses, ok := anyToInt64(pt.Args["max_uses"]); ok && maxUses > 0 { + if maxUses, ok := pt.Args["max_uses"].(int64); ok && maxUses > 0 { webSearchTool.MaxUses = param.NewOpt(maxUses) } - if loc := anyToUserLocation(pt.Args["user_location"]); loc != nil { + if loc, ok := pt.Args["user_location"].(*UserLocation); ok && loc != nil { var ulp anthropic.UserLocationParam if loc.City != "" { ulp.City = param.NewOpt(loc.City) @@ -589,19 +486,46 @@ func (a languageModel) toTools(tools []fantasy.Tool, toolChoice *fantasy.ToolCho webSearchTool.UserLocation = ulp } } - anthropicTools = append(anthropicTools, anthropic.ToolUnionParam{ + raw, err := json.Marshal(anthropic.ToolUnionParam{ OfWebSearchTool20250305: &webSearchTool, }) + if err != nil { + warnings = append(warnings, fantasy.CallWarning{ + Type: fantasy.CallWarningTypeOther, + Tool: tool, + Message: fmt.Sprintf("failed to marshal web search tool: %v", err), + }) + continue + } + rawTools = append(rawTools, raw) continue } + if IsComputerUseTool(tool) { + raw, err := computerUseToolJSON(pt) + if err != nil { + warnings = append(warnings, fantasy.CallWarning{ + Type: fantasy.CallWarningTypeOther, + Tool: tool, + Message: fmt.Sprintf("failed to build computer use tool: %v", err), + }) + continue + } + rawTools = append(rawTools, raw) + continue + } + warnings = append(warnings, fantasy.CallWarning{ + Type: fantasy.CallWarningTypeUnsupportedTool, + Tool: tool, + Message: "tool is not supported", + }) + continue } warnings = append(warnings, fantasy.CallWarning{ Type: fantasy.CallWarningTypeUnsupportedTool, Tool: tool, Message: "tool is not supported", }) - } - + } // NOTE: Bedrock does not support this attribute. var disableParallelToolUse param.Opt[bool] if !a.options.useBedrock { @@ -617,7 +541,7 @@ func (a languageModel) toTools(tools []fantasy.Tool, toolChoice *fantasy.ToolCho }, } } - return anthropicTools, anthropicToolChoice, warnings + return rawTools, anthropicToolChoice, warnings } switch *toolChoice { @@ -636,7 +560,7 @@ func (a languageModel) toTools(tools []fantasy.Tool, toolChoice *fantasy.ToolCho }, } case fantasy.ToolChoiceNone: - return anthropicTools, anthropicToolChoice, warnings + return rawTools, anthropicToolChoice, warnings default: anthropicToolChoice = &anthropic.ToolChoiceUnionParam{ OfTool: &anthropic.ToolChoiceToolParam{ @@ -646,7 +570,7 @@ func (a languageModel) toTools(tools []fantasy.Tool, toolChoice *fantasy.ToolCho }, } } - return anthropicTools, anthropicToolChoice, warnings + return rawTools, anthropicToolChoice, warnings } func toPrompt(prompt fantasy.Prompt, sendReasoningData bool) ([]anthropic.TextBlockParam, []anthropic.MessageParam, []fantasy.CallWarning) { @@ -991,11 +915,23 @@ func mapFinishReason(finishReason string) fantasy.FinishReason { // Generate implements fantasy.LanguageModel. func (a languageModel) Generate(ctx context.Context, call fantasy.Call) (*fantasy.Response, error) { - params, warnings, err := a.prepareParams(call) + params, rawTools, warnings, err := a.prepareParams(call) if err != nil { return nil, err } - response, err := a.client.Messages.New(ctx, *params, callUARequestOptions(call)...) + reqOpts := callUARequestOptions(call) + if len(rawTools) > 0 { + reqOpts = append(reqOpts, option.WithJSONSet("tools", rawTools)) + } + if needsBetaAPI(call.Tools) { + betaOpts, err := computerUseBetaOptions(call.Tools) + if err != nil { + return nil, err + } + reqOpts = append(reqOpts, betaOpts...) + } + + response, err := a.client.Messages.New(ctx, *params, reqOpts...) if err != nil { return nil, toProviderErr(err) } @@ -1118,12 +1054,24 @@ func (a languageModel) Generate(ctx context.Context, call fantasy.Call) (*fantas // Stream implements fantasy.LanguageModel. func (a languageModel) Stream(ctx context.Context, call fantasy.Call) (fantasy.StreamResponse, error) { - params, warnings, err := a.prepareParams(call) + params, rawTools, warnings, err := a.prepareParams(call) if err != nil { return nil, err } - stream := a.client.Messages.NewStreaming(ctx, *params, callUARequestOptions(call)...) + reqOpts := callUARequestOptions(call) + if len(rawTools) > 0 { + reqOpts = append(reqOpts, option.WithJSONSet("tools", rawTools)) + } + if needsBetaAPI(call.Tools) { + betaOpts, err := computerUseBetaOptions(call.Tools) + if err != nil { + return nil, err + } + reqOpts = append(reqOpts, betaOpts...) + } + + stream := a.client.Messages.NewStreaming(ctx, *params, reqOpts...) acc := anthropic.Message{} return func(yield func(fantasy.StreamPart) bool) { if len(warnings) > 0 { diff --git a/providers/anthropic/anthropic_test.go b/providers/anthropic/anthropic_test.go index 51adb7c33..0b1f76fd2 100644 --- a/providers/anthropic/anthropic_test.go +++ b/providers/anthropic/anthropic_test.go @@ -5,14 +5,13 @@ import ( "encoding/json" "errors" "fmt" - "math" "net/http" "net/http/httptest" "testing" "time" "charm.land/fantasy" - "github.com/charmbracelet/anthropic-sdk-go" + anthropic "github.com/charmbracelet/anthropic-sdk-go" "github.com/stretchr/testify/require" ) @@ -798,8 +797,7 @@ func TestGenerate_WebSearchResponse(t *testing.T) { Prompt: testPrompt(), Tools: []fantasy.Tool{ WebSearchTool(nil), - }, - }) + }}) require.NoError(t, err) call := awaitAnthropicCall(t, calls) @@ -1029,183 +1027,6 @@ func TestGenerate_WebSearchToolInRequest(t *testing.T) { require.True(t, ok, "tool should have max_uses") require.Equal(t, float64(3), maxUses) }) - - t.Run("with json-round-tripped provider tool args", func(t *testing.T) { - t.Parallel() - - server, calls := newAnthropicJSONServer(mockAnthropicGenerateResponse()) - defer server.Close() - - provider, err := New( - WithAPIKey("test-api-key"), - WithBaseURL(server.URL), - ) - require.NoError(t, err) - - model, err := provider.LanguageModel(context.Background(), "claude-sonnet-4-20250514") - require.NoError(t, err) - - baseTool := WebSearchTool(&WebSearchToolOptions{ - MaxUses: 7, - BlockedDomains: []string{"example.com", "test.com"}, - UserLocation: &UserLocation{ - City: "San Francisco", - Region: "CA", - Country: "US", - Timezone: "America/Los_Angeles", - }, - }) - - data, err := json.Marshal(baseTool) - require.NoError(t, err) - - var roundTripped fantasy.ProviderDefinedTool - err = json.Unmarshal(data, &roundTripped) - require.NoError(t, err) - - _, err = model.Generate(context.Background(), fantasy.Call{ - Prompt: testPrompt(), - Tools: []fantasy.Tool{roundTripped}, - }) - require.NoError(t, err) - - call := awaitAnthropicCall(t, calls) - tools, ok := call.body["tools"].([]any) - require.True(t, ok) - require.Len(t, tools, 1) - - tool, ok := tools[0].(map[string]any) - require.True(t, ok) - require.Equal(t, "web_search_20250305", tool["type"]) - - domains, ok := tool["blocked_domains"].([]any) - require.True(t, ok, "tool should have blocked_domains") - require.Len(t, domains, 2) - require.Equal(t, "example.com", domains[0]) - require.Equal(t, "test.com", domains[1]) - - maxUses, ok := tool["max_uses"].(float64) - require.True(t, ok, "tool should have max_uses") - require.Equal(t, float64(7), maxUses) - - userLoc, ok := tool["user_location"].(map[string]any) - require.True(t, ok, "tool should have user_location") - require.Equal(t, "San Francisco", userLoc["city"]) - require.Equal(t, "CA", userLoc["region"]) - require.Equal(t, "US", userLoc["country"]) - require.Equal(t, "America/Los_Angeles", userLoc["timezone"]) - require.Equal(t, "approximate", userLoc["type"]) - }) -} - -func TestAnyToStringSlice(t *testing.T) { - t.Parallel() - - t.Run("from string slice", func(t *testing.T) { - t.Parallel() - - got := anyToStringSlice([]string{"example.com", ""}) - require.Equal(t, []string{"example.com", ""}, got) - }) - - t.Run("from any slice filters non-strings and empty", func(t *testing.T) { - t.Parallel() - - got := anyToStringSlice([]any{"example.com", 123, "", "test.com"}) - require.Equal(t, []string{"example.com", "test.com"}, got) - }) - - t.Run("unsupported type", func(t *testing.T) { - t.Parallel() - - got := anyToStringSlice("example.com") - require.Nil(t, got) - }) -} - -func TestAnyToInt64(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - input any - want int64 - wantOK bool - }{ - {name: "int64", input: int64(7), want: 7, wantOK: true}, - {name: "float64 integer", input: float64(7), want: 7, wantOK: true}, - {name: "float32 integer", input: float32(9), want: 9, wantOK: true}, - {name: "float64 non-integer", input: float64(7.5), wantOK: false}, - {name: "float64 max exact int ok", input: float64(1<<53 - 1), want: 1<<53 - 1, wantOK: true}, - {name: "float64 over max exact int", input: float64(1 << 53), wantOK: false}, - {name: "json number int", input: json.Number("42"), want: 42, wantOK: true}, - {name: "json number float", input: json.Number("4.2"), wantOK: false}, - {name: "nan", input: math.NaN(), wantOK: false}, - {name: "inf", input: math.Inf(1), wantOK: false}, - {name: "uint64 overflow", input: uint64(math.MaxInt64) + 1, wantOK: false}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got, ok := anyToInt64(tt.input) - require.Equal(t, tt.wantOK, ok) - if tt.wantOK { - require.Equal(t, tt.want, got) - } - }) - } -} - -func TestAnyToUserLocation(t *testing.T) { - t.Parallel() - - t.Run("pointer passthrough", func(t *testing.T) { - t.Parallel() - - input := &UserLocation{City: "San Francisco", Country: "US"} - got := anyToUserLocation(input) - require.Same(t, input, got) - }) - - t.Run("struct value", func(t *testing.T) { - t.Parallel() - - got := anyToUserLocation(UserLocation{City: "San Francisco", Country: "US"}) - require.NotNil(t, got) - require.Equal(t, "San Francisco", got.City) - require.Equal(t, "US", got.Country) - }) - - t.Run("map value", func(t *testing.T) { - t.Parallel() - - got := anyToUserLocation(map[string]any{ - "city": "San Francisco", - "region": "CA", - "country": "US", - "timezone": "America/Los_Angeles", - "type": "approximate", - }) - require.NotNil(t, got) - require.Equal(t, "San Francisco", got.City) - require.Equal(t, "CA", got.Region) - require.Equal(t, "US", got.Country) - require.Equal(t, "America/Los_Angeles", got.Timezone) - }) - - t.Run("empty map", func(t *testing.T) { - t.Parallel() - - got := anyToUserLocation(map[string]any{"type": "approximate"}) - require.Nil(t, got) - }) - - t.Run("unsupported type", func(t *testing.T) { - t.Parallel() - - got := anyToUserLocation("San Francisco") - require.Nil(t, got) - }) } func TestStream_WebSearchResponse(t *testing.T) { @@ -1269,8 +1090,7 @@ func TestStream_WebSearchResponse(t *testing.T) { Prompt: testPrompt(), Tools: []fantasy.Tool{ WebSearchTool(nil), - }, - }) + }}) require.NoError(t, err) var parts []fantasy.StreamPart @@ -1333,3 +1153,462 @@ func TestStream_WebSearchResponse(t *testing.T) { require.NotEmpty(t, textDeltas, "should have text deltas") require.Equal(t, "Here are the results.", textDeltas[0].Delta) } + +// --- Computer Use Tests --- + +// jsonRoundTripTool simulates a JSON round-trip on a +// ProviderDefinedTool so that its Args map contains float64 +// values (as json.Unmarshal produces) rather than the int64 +// values that NewComputerUseTool stores directly. The +// production toBetaTools code asserts float64. +func jsonRoundTripTool(t *testing.T, tool fantasy.ProviderDefinedTool) fantasy.ProviderDefinedTool { + t.Helper() + data, err := json.Marshal(tool.Args) + require.NoError(t, err) + var args map[string]any + require.NoError(t, json.Unmarshal(data, &args)) + tool.Args = args + return tool +} + +func TestNewComputerUseTool(t *testing.T) { + t.Parallel() + + t.Run("creates tool with correct ID and name", func(t *testing.T) { + t.Parallel() + tool := NewComputerUseTool(ComputerUseToolOptions{ + DisplayWidthPx: 1920, + DisplayHeightPx: 1080, + ToolVersion: ComputerUse20250124, + }) + require.Equal(t, "computer", tool.ID) + require.Equal(t, "computer", tool.Name) + require.Equal(t, int64(1920), tool.Args["display_width_px"]) + require.Equal(t, int64(1080), tool.Args["display_height_px"]) + require.Equal(t, string(ComputerUse20250124), tool.Args["tool_version"]) + }) + + t.Run("includes optional fields when set", func(t *testing.T) { + t.Parallel() + displayNum := int64(1) + enableZoom := true + tool := NewComputerUseTool(ComputerUseToolOptions{ + DisplayWidthPx: 1024, + DisplayHeightPx: 768, + DisplayNumber: &displayNum, + EnableZoom: &enableZoom, + ToolVersion: ComputerUse20251124, + CacheControl: &CacheControl{Type: "ephemeral"}, + }) + require.Equal(t, int64(1), tool.Args["display_number"]) + require.Equal(t, true, tool.Args["enable_zoom"]) + require.NotNil(t, tool.Args["cache_control"]) + }) + + t.Run("omits optional fields when nil", func(t *testing.T) { + t.Parallel() + tool := NewComputerUseTool(ComputerUseToolOptions{ + DisplayWidthPx: 1920, + DisplayHeightPx: 1080, + ToolVersion: ComputerUse20250124, + }) + _, hasDisplayNum := tool.Args["display_number"] + _, hasEnableZoom := tool.Args["enable_zoom"] + _, hasCacheControl := tool.Args["cache_control"] + require.False(t, hasDisplayNum) + require.False(t, hasEnableZoom) + require.False(t, hasCacheControl) + }) +} + +func TestIsComputerUseTool(t *testing.T) { + t.Parallel() + + t.Run("returns true for computer use tool", func(t *testing.T) { + t.Parallel() + tool := NewComputerUseTool(ComputerUseToolOptions{ + DisplayWidthPx: 1920, + DisplayHeightPx: 1080, + ToolVersion: ComputerUse20250124, + }) + require.True(t, IsComputerUseTool(tool)) + }) + + t.Run("returns false for function tool", func(t *testing.T) { + t.Parallel() + tool := fantasy.FunctionTool{ + Name: "test", + Description: "test tool", + } + require.False(t, IsComputerUseTool(tool)) + }) + + t.Run("returns false for other provider defined tool", func(t *testing.T) { + t.Parallel() + tool := fantasy.ProviderDefinedTool{ + ID: "other.tool", + Name: "other", + } + require.False(t, IsComputerUseTool(tool)) + }) +} + +func TestNeedsBetaAPI(t *testing.T) { + t.Parallel() + + t.Run("returns false for empty tools", func(t *testing.T) { + t.Parallel() + require.False(t, needsBetaAPI(nil)) + require.False(t, needsBetaAPI([]fantasy.Tool{})) + }) + + t.Run("returns false for only function tools", func(t *testing.T) { + t.Parallel() + tools := []fantasy.Tool{ + fantasy.FunctionTool{Name: "test"}, + } + require.False(t, needsBetaAPI(tools)) + }) + + t.Run("returns true when computer use tool present", func(t *testing.T) { + t.Parallel() + cuTool := NewComputerUseTool(ComputerUseToolOptions{ + DisplayWidthPx: 1920, + DisplayHeightPx: 1080, + ToolVersion: ComputerUse20250124, + }) + tools := []fantasy.Tool{ + fantasy.FunctionTool{Name: "test"}, + cuTool, + } + require.True(t, needsBetaAPI(tools)) + }) +} + +func TestDetectComputerUseVersion(t *testing.T) { + t.Parallel() + + t.Run("returns empty for no tools", func(t *testing.T) { + t.Parallel() + v, err := detectComputerUseVersion(nil) + require.NoError(t, err) + require.Equal(t, ComputerUseToolVersion(""), v) + }) + + t.Run("returns version for single computer use tool", func(t *testing.T) { + t.Parallel() + cuTool := NewComputerUseTool(ComputerUseToolOptions{ + DisplayWidthPx: 1920, + DisplayHeightPx: 1080, + ToolVersion: ComputerUse20251124, + }) + v, err := detectComputerUseVersion([]fantasy.Tool{cuTool}) + require.NoError(t, err) + require.Equal(t, ComputerUse20251124, v) + }) + + t.Run("returns error for conflicting versions", func(t *testing.T) { + t.Parallel() + tool1 := NewComputerUseTool(ComputerUseToolOptions{ + DisplayWidthPx: 1920, + DisplayHeightPx: 1080, + ToolVersion: ComputerUse20250124, + }) + tool2 := NewComputerUseTool(ComputerUseToolOptions{ + DisplayWidthPx: 1024, + DisplayHeightPx: 768, + ToolVersion: ComputerUse20251124, + }) + _, err := detectComputerUseVersion([]fantasy.Tool{tool1, tool2}) + require.Error(t, err) + require.Contains(t, err.Error(), "conflicting") + }) + + t.Run("accepts matching versions", func(t *testing.T) { + t.Parallel() + tool1 := NewComputerUseTool(ComputerUseToolOptions{ + DisplayWidthPx: 1920, + DisplayHeightPx: 1080, + ToolVersion: ComputerUse20250124, + }) + tool2 := NewComputerUseTool(ComputerUseToolOptions{ + DisplayWidthPx: 1024, + DisplayHeightPx: 768, + ToolVersion: ComputerUse20250124, + }) + v, err := detectComputerUseVersion([]fantasy.Tool{tool1, tool2}) + require.NoError(t, err) + require.Equal(t, ComputerUse20250124, v) + }) +} + +func TestComputerUseToolJSON(t *testing.T) { + t.Parallel() + + t.Run("builds JSON for version 20250124", func(t *testing.T) { + t.Parallel() + cuTool := jsonRoundTripTool(t, NewComputerUseTool(ComputerUseToolOptions{ + DisplayWidthPx: 1920, + DisplayHeightPx: 1080, + ToolVersion: ComputerUse20250124, + })) + data, err := computerUseToolJSON(cuTool) + require.NoError(t, err) + var m map[string]any + require.NoError(t, json.Unmarshal(data, &m)) + require.Equal(t, "computer_20250124", m["type"]) + require.Equal(t, "computer", m["name"]) + require.InDelta(t, 1920, m["display_width_px"], 0) + require.InDelta(t, 1080, m["display_height_px"], 0) + }) + + t.Run("builds JSON for version 20251124 with enable_zoom", func(t *testing.T) { + t.Parallel() + enableZoom := true + cuTool := jsonRoundTripTool(t, NewComputerUseTool(ComputerUseToolOptions{ + DisplayWidthPx: 1024, + DisplayHeightPx: 768, + EnableZoom: &enableZoom, + ToolVersion: ComputerUse20251124, + })) + data, err := computerUseToolJSON(cuTool) + require.NoError(t, err) + var m map[string]any + require.NoError(t, json.Unmarshal(data, &m)) + require.Equal(t, "computer_20251124", m["type"]) + require.Equal(t, true, m["enable_zoom"]) + }) + + t.Run("handles int64 args without JSON round-trip", func(t *testing.T) { + t.Parallel() + // Direct construction stores int64 values. + cuTool := NewComputerUseTool(ComputerUseToolOptions{ + DisplayWidthPx: 1920, + DisplayHeightPx: 1080, + ToolVersion: ComputerUse20250124, + }) + data, err := computerUseToolJSON(cuTool) + require.NoError(t, err) + var m map[string]any + require.NoError(t, json.Unmarshal(data, &m)) + require.InDelta(t, 1920, m["display_width_px"], 0) + }) +} + +func TestToTools_RawJSON(t *testing.T) { + t.Parallel() + + lm := languageModel{options: options{}} + + cuTool := jsonRoundTripTool(t, NewComputerUseTool(ComputerUseToolOptions{ + DisplayWidthPx: 1920, + DisplayHeightPx: 1080, + ToolVersion: ComputerUse20250124, + })) + + tools := []fantasy.Tool{ + fantasy.FunctionTool{ + Name: "weather", + Description: "Get weather", + InputSchema: map[string]any{ + "properties": map[string]any{ + "location": map[string]any{"type": "string"}, + }, + "required": []string{"location"}, + }, + }, + WebSearchTool(nil), + cuTool, + } + + rawTools, toolChoice, warnings := lm.toTools(tools, nil, false) + + require.Len(t, rawTools, 3) + require.Nil(t, toolChoice) + require.Empty(t, warnings) + + // Verify each raw tool is valid JSON. + for i, raw := range rawTools { + var m map[string]any + require.NoError(t, json.Unmarshal(raw, &m), "tool %d should be valid JSON", i) + } + + // Check function tool. + var funcTool map[string]any + require.NoError(t, json.Unmarshal(rawTools[0], &funcTool)) + require.Equal(t, "weather", funcTool["name"]) + + // Check web search tool. + var webTool map[string]any + require.NoError(t, json.Unmarshal(rawTools[1], &webTool)) + require.Equal(t, "web_search_20250305", webTool["type"]) + + // Check computer use tool. + var cuToolJSON map[string]any + require.NoError(t, json.Unmarshal(rawTools[2], &cuToolJSON)) + require.Equal(t, "computer_20250124", cuToolJSON["type"]) + require.Equal(t, "computer", cuToolJSON["name"]) +} + +func TestGenerate_BetaAPI(t *testing.T) { + t.Parallel() + + t.Run("sends beta header for computer use", func(t *testing.T) { + t.Parallel() + + var capturedHeaders http.Header + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedHeaders = r.Header.Clone() + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(mockAnthropicGenerateResponse()) + })) + defer server.Close() + + provider, err := New( + WithAPIKey("test-api-key"), + WithBaseURL(server.URL), + ) + require.NoError(t, err) + + model, err := provider.LanguageModel(context.Background(), "claude-sonnet-4-20250514") + require.NoError(t, err) + + cuTool := jsonRoundTripTool(t, NewComputerUseTool(ComputerUseToolOptions{ + DisplayWidthPx: 1920, + DisplayHeightPx: 1080, + ToolVersion: ComputerUse20250124, + })) + + _, err = model.Generate(context.Background(), fantasy.Call{ + Prompt: testPrompt(), + Tools: []fantasy.Tool{cuTool}, + }) + require.NoError(t, err) + require.Contains(t, capturedHeaders.Get("Anthropic-Beta"), "computer-use-2025-01-24") + }) + + t.Run("returns tool use from beta response", func(t *testing.T) { + t.Parallel() + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "id": "msg_01Test", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-20250514", + "content": []any{ + map[string]any{ + "type": "tool_use", + "id": "toolu_01", + "name": "computer", + "input": map[string]any{"action": "screenshot"}, + }, + }, + "stop_reason": "tool_use", + "usage": map[string]any{ + "input_tokens": 10, + "output_tokens": 5, + "cache_creation": map[string]any{ + "ephemeral_1h_input_tokens": 0, + "ephemeral_5m_input_tokens": 0, + }, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "server_tool_use": map[string]any{ + "web_search_requests": 0, + }, + "service_tier": "standard", + }, + }) + })) + defer server.Close() + + provider, err := New( + WithAPIKey("test-api-key"), + WithBaseURL(server.URL), + ) + require.NoError(t, err) + + model, err := provider.LanguageModel(context.Background(), "claude-sonnet-4-20250514") + require.NoError(t, err) + + cuTool := jsonRoundTripTool(t, NewComputerUseTool(ComputerUseToolOptions{ + DisplayWidthPx: 1920, + DisplayHeightPx: 1080, + ToolVersion: ComputerUse20250124, + })) + + resp, err := model.Generate(context.Background(), fantasy.Call{ + Prompt: testPrompt(), + Tools: []fantasy.Tool{cuTool}, + }) + require.NoError(t, err) + + toolCalls := resp.Content.ToolCalls() + require.Len(t, toolCalls, 1) + require.Equal(t, "computer", toolCalls[0].ToolName) + require.Equal(t, "toolu_01", toolCalls[0].ToolCallID) + require.Contains(t, toolCalls[0].Input, "screenshot") + require.Equal(t, fantasy.FinishReasonToolCalls, resp.FinishReason) + + // Verify typed parsing works on the tool call input. + parsed, err := ParseComputerUseInput(toolCalls[0].Input) + require.NoError(t, err) + require.Equal(t, ActionScreenshot, parsed.Action) + }) +} + +func TestStream_BetaAPI(t *testing.T) { + t.Parallel() + + t.Run("streams via beta API for computer use", func(t *testing.T) { + t.Parallel() + + var capturedHeaders http.Header + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + capturedHeaders = r.Header.Clone() + w.Header().Set("Content-Type", "text/event-stream") + w.Header().Set("Cache-Control", "no-cache") + w.WriteHeader(http.StatusOK) + chunks := []string{ + "event: message_start\n", + "data: {\"type\":\"message_start\",\"message\":{}}\n\n", + "event: message_stop\n", + "data: {\"type\":\"message_stop\"}\n\n", + } + for _, chunk := range chunks { + _, _ = fmt.Fprint(w, chunk) + if flusher, ok := w.(http.Flusher); ok { + flusher.Flush() + } + } + })) + defer server.Close() + + provider, err := New( + WithAPIKey("test-api-key"), + WithBaseURL(server.URL), + ) + require.NoError(t, err) + + model, err := provider.LanguageModel(context.Background(), "claude-sonnet-4-20250514") + require.NoError(t, err) + + cuTool := jsonRoundTripTool(t, NewComputerUseTool(ComputerUseToolOptions{ + DisplayWidthPx: 1920, + DisplayHeightPx: 1080, + ToolVersion: ComputerUse20250124, + })) + + stream, err := model.Stream(context.Background(), fantasy.Call{ + Prompt: testPrompt(), + Tools: []fantasy.Tool{cuTool}, + }) + require.NoError(t, err) + + stream(func(fantasy.StreamPart) bool { return true }) + + require.Contains(t, capturedHeaders.Get("Anthropic-Beta"), "computer-use-2025-01-24") + }) +} diff --git a/providers/anthropic/computer_use.go b/providers/anthropic/computer_use.go new file mode 100644 index 000000000..f474b2cd4 --- /dev/null +++ b/providers/anthropic/computer_use.go @@ -0,0 +1,483 @@ +package anthropic + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "strings" + + "charm.land/fantasy" + anthropicsdk "github.com/charmbracelet/anthropic-sdk-go" + "github.com/charmbracelet/anthropic-sdk-go/option" + "github.com/charmbracelet/anthropic-sdk-go/packages/param" +) + +// computerUseToolID is the canonical identifier prefix for +// Anthropic computer use tools. +const computerUseToolID = "computer" + +// ComputerUseToolVersion identifies which version of the Anthropic +// computer use tool to use. +type ComputerUseToolVersion string + +const ( + // ComputerUse20251124 selects the November 2025 version of the + // computer use tool. + ComputerUse20251124 ComputerUseToolVersion = "computer_20251124" + // ComputerUse20250124 selects the January 2025 version of the + // computer use tool. + ComputerUse20250124 ComputerUseToolVersion = "computer_20250124" +) + +// ComputerUseToolOptions holds the configuration for creating a +// computer use tool instance. +type ComputerUseToolOptions struct { + // DisplayWidthPx is the width of the display in pixels. + DisplayWidthPx int64 + // DisplayHeightPx is the height of the display in pixels. + DisplayHeightPx int64 + // DisplayNumber is an optional X11 display number. + DisplayNumber *int64 + // EnableZoom enables zoom support. Only used with the + // ComputerUse20251124 version. + EnableZoom *bool + // ToolVersion selects which computer use tool version to use. + ToolVersion ComputerUseToolVersion + // CacheControl sets optional cache control for the tool. + CacheControl *CacheControl +} + +// NewComputerUseTool creates a new provider-defined tool configured +// for Anthropic computer use. The returned tool can be passed +// directly into a fantasy tool set. +func NewComputerUseTool(opts ComputerUseToolOptions) fantasy.ProviderDefinedTool { + args := map[string]any{ + "display_width_px": opts.DisplayWidthPx, + "display_height_px": opts.DisplayHeightPx, + "tool_version": string(opts.ToolVersion), + } + if opts.DisplayNumber != nil { + args["display_number"] = *opts.DisplayNumber + } + if opts.EnableZoom != nil { + args["enable_zoom"] = *opts.EnableZoom + } + if opts.CacheControl != nil { + args["cache_control"] = *opts.CacheControl + } + return fantasy.ProviderDefinedTool{ + ID: computerUseToolID, + Name: "computer", + Args: args, + } +} + +// IsComputerUseTool reports whether tool is an Anthropic computer +// use tool. It checks for a ProviderDefinedTool whose ID starts +// with the computer use tool prefix. +func IsComputerUseTool(tool fantasy.Tool) bool { + pdt, ok := tool.(fantasy.ProviderDefinedTool) + if !ok { + return false + } + return strings.HasPrefix(pdt.ID, computerUseToolID) +} + +// getComputerUseVersion extracts the ComputerUseToolVersion from a +// provider-defined tool's Args map. It returns the version and true +// if present, or the zero value and false otherwise. +func getComputerUseVersion(tool fantasy.ProviderDefinedTool) (ComputerUseToolVersion, bool) { + v, ok := tool.Args["tool_version"] + if !ok { + return "", false + } + s, ok := v.(string) + if !ok { + return "", false + } + return ComputerUseToolVersion(s), true +} + +// needsBetaAPI reports whether any tool in the slice is a computer +// use tool, which requires the Anthropic beta API. +func needsBetaAPI(tools []fantasy.Tool) bool { + for _, t := range tools { + if IsComputerUseTool(t) { + return true + } + } + return false +} + +// betaFlagForVersion returns the Anthropic beta header value for +// the given computer use tool version. +func betaFlagForVersion(version ComputerUseToolVersion) (string, error) { + switch version { + case ComputerUse20251124: + return "computer-use-2025-11-24", nil + case ComputerUse20250124: + return anthropicsdk.AnthropicBetaComputerUse2025_01_24, nil + default: + return "", fmt.Errorf( + "unsupported computer use tool version: %q", version, + ) + } +} + +// detectComputerUseVersion scans tools for computer use tools and +// returns their version. If multiple computer use tools are present +// they must all share the same version; otherwise an error is +// returned. If no computer use tools are found it returns ("", nil). +func detectComputerUseVersion(tools []fantasy.Tool) (ComputerUseToolVersion, error) { + var found ComputerUseToolVersion + var seen bool + + for _, t := range tools { + pdt, ok := t.(fantasy.ProviderDefinedTool) + if !ok || !strings.HasPrefix(pdt.ID, computerUseToolID) { + continue + } + + version, ok := getComputerUseVersion(pdt) + if !ok { + continue + } + + if !seen { + found = version + seen = true + continue + } + + if version != found { + return "", fmt.Errorf( + "conflicting computer use tool versions: %q and %q", + found, version, + ) + } + } + + return found, nil +} + +// computerUseBetaOptions returns the request options needed to +// enable the Anthropic computer use beta API: a query parameter +// and a header identifying the beta version. +func computerUseBetaOptions(tools []fantasy.Tool) ([]option.RequestOption, error) { + version, err := detectComputerUseVersion(tools) + if err != nil { + return nil, err + } + betaFlag, err := betaFlagForVersion(version) + if err != nil { + return nil, err + } + return []option.RequestOption{ + option.WithQuery("beta", "true"), + option.WithHeaderAdd("anthropic-beta", betaFlag), + }, nil +} + +// computerUseToolJSON builds the JSON representation of a computer +// use tool from a ProviderDefinedTool's Args, using the beta SDK +// types for serialization. +func computerUseToolJSON(pdt fantasy.ProviderDefinedTool) (json.RawMessage, error) { + version, ok := getComputerUseVersion(pdt) + if !ok { + return nil, fmt.Errorf("computer use tool missing version") + } + + h := toInt64(pdt.Args["display_height_px"]) + w := toInt64(pdt.Args["display_width_px"]) + + switch version { + case ComputerUse20250124: + tool := anthropicsdk.BetaToolUnionParamOfComputerUseTool20250124(h, w) + if v, ok := pdt.Args["display_number"]; ok { + tool.OfComputerUseTool20250124.DisplayNumber = param.NewOpt(toInt64(v)) + } + if _, ok := pdt.Args["cache_control"]; ok { + tool.OfComputerUseTool20250124.CacheControl = anthropicsdk.NewBetaCacheControlEphemeralParam() + } + return json.Marshal(tool) + case ComputerUse20251124: + tool := anthropicsdk.BetaToolUnionParamOfComputerUseTool20251124(h, w) + if v, ok := pdt.Args["display_number"]; ok { + tool.OfComputerUseTool20251124.DisplayNumber = param.NewOpt(toInt64(v)) + } + if v, ok := pdt.Args["enable_zoom"]; ok { + if b, ok := v.(bool); ok { + tool.OfComputerUseTool20251124.EnableZoom = param.NewOpt(b) + } + } + if _, ok := pdt.Args["cache_control"]; ok { + tool.OfComputerUseTool20251124.CacheControl = anthropicsdk.NewBetaCacheControlEphemeralParam() + } + return json.Marshal(tool) + default: + return nil, fmt.Errorf( + "unsupported computer use tool version: %q", version, + ) + } +} + +// ComputerAction identifies the action Claude wants to perform. +type ComputerAction string + +const ( + // ActionScreenshot captures the current screen. + // + // No additional fields are populated. + // + // Response: return a screenshot image using + // NewComputerUseScreenshotResult. + ActionScreenshot ComputerAction = "screenshot" + // ActionLeftClick performs a left click. + // + // - Coordinate: [x, y] target. + // - Text: optional modifier key (e.g. "shift", "ctrl"). + // + // Response: return a screenshot showing the result using + // NewComputerUseScreenshotResult. + ActionLeftClick ComputerAction = "left_click" + // ActionRightClick performs a right click (v20250124+). + // + // - Coordinate: [x, y] target. + // - Text: optional modifier key (e.g. "shift", "ctrl"). + // + // Response: return a screenshot showing the result using + // NewComputerUseScreenshotResult. + ActionRightClick ComputerAction = "right_click" + // ActionDoubleClick performs a double click (v20250124+). + // + // - Coordinate: [x, y] target. + // - Text: optional modifier key (e.g. "shift", "ctrl"). + // + // Response: return a screenshot showing the result using + // NewComputerUseScreenshotResult. + ActionDoubleClick ComputerAction = "double_click" + // ActionTripleClick performs a triple click (v20250124+). + // + // - Coordinate: [x, y] target. + // - Text: optional modifier key (e.g. "shift", "ctrl"). + // + // Response: return a screenshot showing the result using + // NewComputerUseScreenshotResult. + ActionTripleClick ComputerAction = "triple_click" + // ActionMiddleClick performs a middle click (v20250124+). + // + // - Coordinate: [x, y] target. + // - Text: optional modifier key (e.g. "shift", "ctrl"). + // + // Response: return a screenshot showing the result using + // NewComputerUseScreenshotResult. + ActionMiddleClick ComputerAction = "middle_click" + // ActionMouseMove moves the cursor. + // + // - Coordinate: [x, y] destination. + // + // Response: return a screenshot showing the new cursor + // position using NewComputerUseScreenshotResult. + ActionMouseMove ComputerAction = "mouse_move" + // ActionLeftClickDrag drags from one point to another + // (v20250124+). + // + // - StartCoordinate: [x, y] drag origin. + // - Coordinate: [x, y] drag destination. + // + // Response: return a screenshot showing the result using + // NewComputerUseScreenshotResult. + ActionLeftClickDrag ComputerAction = "left_click_drag" + // ActionType types text. + // + // - Text: the string to type. + // + // Response: return a screenshot showing the result using + // NewComputerUseScreenshotResult. + ActionType ComputerAction = "type" + // ActionKey presses a key combination. + // + // - Text: key combo string (e.g. "ctrl+c", "Return"). + // + // Response: return a screenshot showing the result using + // NewComputerUseScreenshotResult. + ActionKey ComputerAction = "key" + // ActionScroll scrolls the screen (v20250124+). + // + // - Coordinate: [x, y] scroll origin. + // - ScrollDirection: "up", "down", "left", or "right". + // - ScrollAmount: scroll distance. + // - Text: optional modifier key. + // + // Response: return a screenshot showing the scrolled view + // using NewComputerUseScreenshotResult. + ActionScroll ComputerAction = "scroll" + // ActionLeftMouseDown presses and holds the left mouse button + // (v20250124+). + // + // - Coordinate: [x, y] target. + // + // Response: return a screenshot showing the result using + // NewComputerUseScreenshotResult. + ActionLeftMouseDown ComputerAction = "left_mouse_down" + // ActionLeftMouseUp releases the left mouse button + // (v20250124+). + // + // - Coordinate: [x, y] target. + // + // Response: return a screenshot showing the result using + // NewComputerUseScreenshotResult. + ActionLeftMouseUp ComputerAction = "left_mouse_up" + // ActionHoldKey holds down a key for a specified duration + // (v20250124+). + // + // - Text: the key to hold. + // - Duration: hold time in seconds. + // + // Response: return a screenshot showing the result using + // NewComputerUseScreenshotResult. + ActionHoldKey ComputerAction = "hold_key" + // ActionWait pauses between actions (v20250124+). + // + // No additional fields are populated. + // + // Response: return a screenshot showing the current state + // using NewComputerUseScreenshotResult. + ActionWait ComputerAction = "wait" + // ActionZoom views a specific screen region at full + // resolution (v20251124 only). Requires enable_zoom in the + // tool definition. + // + // - Region: [x1, y1, x2, y2] top-left and bottom-right. + // + // Response: return a screenshot of the zoomed region at full + // resolution using NewComputerUseScreenshotResult. + ActionZoom ComputerAction = "zoom" +) + +// ComputerUseInput is the parsed, typed representation of a computer +// use tool call's Input JSON. Not all fields are populated for every +// action — check Action first, then read the relevant fields. +type ComputerUseInput struct { + Action ComputerAction `json:"action"` + // Coordinate is [x, y] for click, move, scroll, and + // drag-end actions. + Coordinate [2]int64 `json:"coordinate,omitempty"` + // StartCoordinate is [x, y] for left_click_drag start point. + StartCoordinate [2]int64 `json:"start_coordinate,omitempty"` + // Text is the string to type (ActionType), key combo + // (ActionKey), modifier key for click/scroll actions, or key + // to hold (ActionHoldKey). + Text string `json:"text,omitempty"` + // ScrollDirection is the scroll direction: "up", "down", + // "left", or "right". + ScrollDirection string `json:"scroll_direction,omitempty"` + // ScrollAmount is the number of scroll clicks. + ScrollAmount int64 `json:"scroll_amount,omitempty"` + // Duration is how long to hold the key in seconds + // (ActionHoldKey). + Duration int64 `json:"duration,omitempty"` + // Region is [x1, y1, x2, y2] defining the zoom area + // (ActionZoom, v20251124 only). + Region [4]int64 `json:"region,omitempty"` +} + +// ParseComputerUseInput parses a ToolCallContent's Input string into +// a typed ComputerUseInput. Returns an error if the JSON is invalid. +func ParseComputerUseInput(input string) (ComputerUseInput, error) { + var result ComputerUseInput + err := json.Unmarshal([]byte(input), &result) + return result, err +} + +// NewComputerUseScreenshotResult constructs a ToolResultPart +// containing a screenshot image. This is the standard response for +// almost every computer use action — Claude expects to see what +// happened after executing the action. +// +// Parameters: +// - toolCallID: the ToolCallID from the ToolCallContent that +// requested this action. +// - screenshotPNG: the raw PNG bytes of the screenshot. The +// caller is responsible for capturing and (optionally) resizing +// the screenshot before passing it here. +// +// The function base64-encodes the image data and sets the media +// type to "image/png". +func NewComputerUseScreenshotResult( + toolCallID string, + screenshotPNG []byte, +) fantasy.ToolResultPart { + return fantasy.ToolResultPart{ + ToolCallID: toolCallID, + Output: fantasy.ToolResultOutputContentMedia{ + Data: base64.StdEncoding.EncodeToString(screenshotPNG), + MediaType: "image/png", + }, + } +} + +// NewComputerUseScreenshotResultWithMediaType is like +// NewComputerUseScreenshotResult but allows specifying a custom +// media type (e.g. "image/jpeg") and pre-encoded base64 data. +func NewComputerUseScreenshotResultWithMediaType( + toolCallID string, + base64Data string, + mediaType string, +) fantasy.ToolResultPart { + return fantasy.ToolResultPart{ + ToolCallID: toolCallID, + Output: fantasy.ToolResultOutputContentMedia{ + Data: base64Data, + MediaType: mediaType, + }, + } +} + +// NewComputerUseErrorResult constructs a ToolResultPart indicating +// that the requested action failed. Claude will see this as an +// error and may retry or adjust its approach. +// +// Use this when screenshot capture fails, coordinates are out of +// bounds, the application is unresponsive, or any other execution +// error occurs. +func NewComputerUseErrorResult( + toolCallID string, + err error, +) fantasy.ToolResultPart { + return fantasy.ToolResultPart{ + ToolCallID: toolCallID, + Output: fantasy.ToolResultOutputContentError{ + Error: err, + }, + } +} + +// NewComputerUseTextResult constructs a ToolResultPart containing a +// plain text response. This is rarely needed for computer use — +// most actions should return a screenshot — but can be useful for +// returning metadata alongside the action or for testing. +func NewComputerUseTextResult( + toolCallID string, + text string, +) fantasy.ToolResultPart { + return fantasy.ToolResultPart{ + ToolCallID: toolCallID, + Output: fantasy.ToolResultOutputContentText{ + Text: text, + }, + } +} + +// toInt64 converts a numeric value that may be int64 or float64 +// (the latter from JSON round-tripping) to int64. +func toInt64(v any) int64 { + switch n := v.(type) { + case int64: + return n + case float64: + return int64(n) + default: + return 0 + } +} diff --git a/providers/anthropic/computer_use_test.go b/providers/anthropic/computer_use_test.go new file mode 100644 index 000000000..d8e453fb4 --- /dev/null +++ b/providers/anthropic/computer_use_test.go @@ -0,0 +1,294 @@ +package anthropic + +import ( + "encoding/base64" + "errors" + "testing" + + "charm.land/fantasy" + "github.com/stretchr/testify/require" +) + +func TestParseComputerUseInput(t *testing.T) { + t.Parallel() + + t.Run("screenshot", func(t *testing.T) { + t.Parallel() + input, err := ParseComputerUseInput(`{"action":"screenshot"}`) + require.NoError(t, err) + require.Equal(t, ActionScreenshot, input.Action) + require.Equal(t, [2]int64{0, 0}, input.Coordinate) + require.Equal(t, "", input.Text) + }) + + t.Run("left_click with coordinate", func(t *testing.T) { + t.Parallel() + input, err := ParseComputerUseInput(`{"action":"left_click","coordinate":[100,200]}`) + require.NoError(t, err) + require.Equal(t, ActionLeftClick, input.Action) + require.Equal(t, [2]int64{100, 200}, input.Coordinate) + }) + + t.Run("right_click with coordinate", func(t *testing.T) { + t.Parallel() + input, err := ParseComputerUseInput(`{"action":"right_click","coordinate":[50,75]}`) + require.NoError(t, err) + require.Equal(t, ActionRightClick, input.Action) + require.Equal(t, [2]int64{50, 75}, input.Coordinate) + }) + + t.Run("double_click with coordinate", func(t *testing.T) { + t.Parallel() + input, err := ParseComputerUseInput(`{"action":"double_click","coordinate":[300,400]}`) + require.NoError(t, err) + require.Equal(t, ActionDoubleClick, input.Action) + require.Equal(t, [2]int64{300, 400}, input.Coordinate) + }) + + t.Run("middle_click with coordinate", func(t *testing.T) { + t.Parallel() + input, err := ParseComputerUseInput(`{"action":"middle_click","coordinate":[10,20]}`) + require.NoError(t, err) + require.Equal(t, ActionMiddleClick, input.Action) + require.Equal(t, [2]int64{10, 20}, input.Coordinate) + }) + + t.Run("mouse_move with coordinate", func(t *testing.T) { + t.Parallel() + input, err := ParseComputerUseInput(`{"action":"mouse_move","coordinate":[500,600]}`) + require.NoError(t, err) + require.Equal(t, ActionMouseMove, input.Action) + require.Equal(t, [2]int64{500, 600}, input.Coordinate) + }) + + t.Run("left_click_drag with start_coordinate and coordinate", func(t *testing.T) { + t.Parallel() + input, err := ParseComputerUseInput(`{"action":"left_click_drag","start_coordinate":[10,20],"coordinate":[300,400]}`) + require.NoError(t, err) + require.Equal(t, ActionLeftClickDrag, input.Action) + require.Equal(t, [2]int64{10, 20}, input.StartCoordinate) + require.Equal(t, [2]int64{300, 400}, input.Coordinate) + }) + + t.Run("type with text", func(t *testing.T) { + t.Parallel() + input, err := ParseComputerUseInput(`{"action":"type","text":"hello world"}`) + require.NoError(t, err) + require.Equal(t, ActionType, input.Action) + require.Equal(t, "hello world", input.Text) + }) + + t.Run("key with text", func(t *testing.T) { + t.Parallel() + input, err := ParseComputerUseInput(`{"action":"key","text":"ctrl+c"}`) + require.NoError(t, err) + require.Equal(t, ActionKey, input.Action) + require.Equal(t, "ctrl+c", input.Text) + }) + + t.Run("scroll with coordinate direction and amount", func(t *testing.T) { + t.Parallel() + input, err := ParseComputerUseInput(`{"action":"scroll","coordinate":[960,540],"scroll_direction":"down","scroll_amount":3}`) + require.NoError(t, err) + require.Equal(t, ActionScroll, input.Action) + require.Equal(t, [2]int64{960, 540}, input.Coordinate) + require.Equal(t, "down", input.ScrollDirection) + require.Equal(t, int64(3), input.ScrollAmount) + }) + + t.Run("invalid JSON returns error", func(t *testing.T) { + t.Parallel() + _, err := ParseComputerUseInput(`{not valid json}`) + require.Error(t, err) + }) + + t.Run("triple_click with coordinate", func(t *testing.T) { + t.Parallel() + input, err := ParseComputerUseInput(`{"action":"triple_click","coordinate":[120,240]}`) + require.NoError(t, err) + require.Equal(t, ActionTripleClick, input.Action) + require.Equal(t, [2]int64{120, 240}, input.Coordinate) + }) + + t.Run("left_mouse_down with coordinate", func(t *testing.T) { + t.Parallel() + input, err := ParseComputerUseInput(`{"action":"left_mouse_down","coordinate":[80,90]}`) + require.NoError(t, err) + require.Equal(t, ActionLeftMouseDown, input.Action) + require.Equal(t, [2]int64{80, 90}, input.Coordinate) + }) + + t.Run("left_mouse_up with coordinate", func(t *testing.T) { + t.Parallel() + input, err := ParseComputerUseInput(`{"action":"left_mouse_up","coordinate":[80,90]}`) + require.NoError(t, err) + require.Equal(t, ActionLeftMouseUp, input.Action) + require.Equal(t, [2]int64{80, 90}, input.Coordinate) + }) + + t.Run("wait", func(t *testing.T) { + t.Parallel() + input, err := ParseComputerUseInput(`{"action":"wait"}`) + require.NoError(t, err) + require.Equal(t, ActionWait, input.Action) + require.Equal(t, [2]int64{0, 0}, input.Coordinate) + require.Equal(t, "", input.Text) + }) + + t.Run("zoom with region", func(t *testing.T) { + t.Parallel() + input, err := ParseComputerUseInput(`{"action":"zoom","region":[100,200,500,600]}`) + require.NoError(t, err) + require.Equal(t, ActionZoom, input.Action) + require.Equal(t, [4]int64{100, 200, 500, 600}, input.Region) + }) + + t.Run("left_click with modifier key", func(t *testing.T) { + t.Parallel() + input, err := ParseComputerUseInput(`{"action":"left_click","coordinate":[100,200],"text":"shift"}`) + require.NoError(t, err) + require.Equal(t, ActionLeftClick, input.Action) + require.Equal(t, [2]int64{100, 200}, input.Coordinate) + require.Equal(t, "shift", input.Text) + }) + + t.Run("unknown action parses without error", func(t *testing.T) { + t.Parallel() + input, err := ParseComputerUseInput(`{"action":"future_action","coordinate":[1,2]}`) + require.NoError(t, err) + require.Equal(t, ComputerAction("future_action"), input.Action) + require.Equal(t, [2]int64{1, 2}, input.Coordinate) + }) +} + +func TestNewComputerUseScreenshotResult(t *testing.T) { + t.Parallel() + + t.Run("base64 encodes PNG bytes", func(t *testing.T) { + t.Parallel() + pngData := []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A} + result := NewComputerUseScreenshotResult("call-123", pngData) + + require.Equal(t, "call-123", result.ToolCallID) + + media, ok := result.Output.(fantasy.ToolResultOutputContentMedia) + require.True(t, ok, "output should be ToolResultOutputContentMedia") + require.Equal(t, "image/png", media.MediaType) + require.Equal(t, base64.StdEncoding.EncodeToString(pngData), media.Data) + }) + + t.Run("preserves tool call ID", func(t *testing.T) { + t.Parallel() + result := NewComputerUseScreenshotResult("tc_abc", []byte{0x01}) + require.Equal(t, "tc_abc", result.ToolCallID) + }) + + t.Run("empty screenshot bytes", func(t *testing.T) { + t.Parallel() + result := NewComputerUseScreenshotResult("call-empty", []byte{}) + + media, ok := result.Output.(fantasy.ToolResultOutputContentMedia) + require.True(t, ok) + require.Equal(t, "image/png", media.MediaType) + require.Equal(t, "", media.Data) + }) + + t.Run("output content type is media", func(t *testing.T) { + t.Parallel() + result := NewComputerUseScreenshotResult("call-type", []byte{0xFF}) + require.Equal(t, fantasy.ToolResultContentTypeMedia, result.Output.GetType()) + }) +} + +func TestNewComputerUseScreenshotResultWithMediaType(t *testing.T) { + t.Parallel() + + t.Run("custom media type and base64 data", func(t *testing.T) { + t.Parallel() + b64 := base64.StdEncoding.EncodeToString([]byte("jpeg-data")) + result := NewComputerUseScreenshotResultWithMediaType("call-456", b64, "image/jpeg") + + require.Equal(t, "call-456", result.ToolCallID) + + media, ok := result.Output.(fantasy.ToolResultOutputContentMedia) + require.True(t, ok, "output should be ToolResultOutputContentMedia") + require.Equal(t, "image/jpeg", media.MediaType) + require.Equal(t, b64, media.Data) + }) + + t.Run("preserves tool call ID", func(t *testing.T) { + t.Parallel() + result := NewComputerUseScreenshotResultWithMediaType("tc_xyz", "data", "image/webp") + require.Equal(t, "tc_xyz", result.ToolCallID) + }) + + t.Run("output content type is media", func(t *testing.T) { + t.Parallel() + result := NewComputerUseScreenshotResultWithMediaType("call-type", "data", "image/png") + require.Equal(t, fantasy.ToolResultContentTypeMedia, result.Output.GetType()) + }) +} + +func TestNewComputerUseErrorResult(t *testing.T) { + t.Parallel() + + t.Run("error message propagates", func(t *testing.T) { + t.Parallel() + err := errors.New("screenshot capture failed") + result := NewComputerUseErrorResult("call-err", err) + + require.Equal(t, "call-err", result.ToolCallID) + + errOutput, ok := result.Output.(fantasy.ToolResultOutputContentError) + require.True(t, ok, "output should be ToolResultOutputContentError") + require.Equal(t, "screenshot capture failed", errOutput.Error.Error()) + }) + + t.Run("preserves tool call ID", func(t *testing.T) { + t.Parallel() + result := NewComputerUseErrorResult("tc_err", errors.New("fail")) + require.Equal(t, "tc_err", result.ToolCallID) + }) + + t.Run("output content type is error", func(t *testing.T) { + t.Parallel() + result := NewComputerUseErrorResult("call-type", errors.New("oops")) + require.Equal(t, fantasy.ToolResultContentTypeError, result.Output.GetType()) + }) +} + +func TestNewComputerUseTextResult(t *testing.T) { + t.Parallel() + + t.Run("text content is set", func(t *testing.T) { + t.Parallel() + result := NewComputerUseTextResult("call-txt", "action completed successfully") + + require.Equal(t, "call-txt", result.ToolCallID) + + textOutput, ok := result.Output.(fantasy.ToolResultOutputContentText) + require.True(t, ok, "output should be ToolResultOutputContentText") + require.Equal(t, "action completed successfully", textOutput.Text) + }) + + t.Run("preserves tool call ID", func(t *testing.T) { + t.Parallel() + result := NewComputerUseTextResult("tc_text", "hello") + require.Equal(t, "tc_text", result.ToolCallID) + }) + + t.Run("empty text", func(t *testing.T) { + t.Parallel() + result := NewComputerUseTextResult("call-empty", "") + + textOutput, ok := result.Output.(fantasy.ToolResultOutputContentText) + require.True(t, ok) + require.Equal(t, "", textOutput.Text) + }) + + t.Run("output content type is text", func(t *testing.T) { + t.Parallel() + result := NewComputerUseTextResult("call-type", "test") + require.Equal(t, fantasy.ToolResultContentTypeText, result.Output.GetType()) + }) +} diff --git a/providertests/anthropic_test.go b/providertests/anthropic_test.go index 32fb87f70..a4853b411 100644 --- a/providertests/anthropic_test.go +++ b/providertests/anthropic_test.go @@ -2,6 +2,7 @@ package providertests import ( "context" + "encoding/json" "net/http" "os" "testing" @@ -274,3 +275,171 @@ func TestAnthropicWebSearch(t *testing.T) { require.Contains(t, got2, "Osaka", "turn 2 response should mention Osaka") }) } + +// screenshotBase64 is a tiny valid 1x1 PNG encoded as base64, +// used as a stub screenshot result in computer use tests. +const screenshotBase64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==" + +func TestAnthropicComputerUse(t *testing.T) { + for _, m := range anthropicTestModels { + t.Run(m.name, func(t *testing.T) { + t.Run("computer use", func(t *testing.T) { + r := vcr.NewRecorder(t) + + model, err := anthropicBuilder(m.model)(t, r) + require.NoError(t, err) + + cuTool := jsonRoundTripTool(t, anthropic.NewComputerUseTool(anthropic.ComputerUseToolOptions{ + DisplayWidthPx: 1920, + DisplayHeightPx: 1080, + ToolVersion: anthropic.ComputerUse20250124, + })) + + // First call: expect a screenshot tool call. + resp, err := model.Generate(t.Context(), fantasy.Call{ + Prompt: fantasy.Prompt{ + {Role: fantasy.MessageRoleSystem, Content: []fantasy.MessagePart{fantasy.TextPart{Text: "You are a helpful assistant"}}}, + {Role: fantasy.MessageRoleUser, Content: []fantasy.MessagePart{fantasy.TextPart{Text: "Take a screenshot of the desktop"}}}, + }, + Tools: []fantasy.Tool{cuTool}, + }) + require.NoError(t, err) + require.Equal(t, fantasy.FinishReasonToolCalls, resp.FinishReason) + + toolCalls := resp.Content.ToolCalls() + require.Len(t, toolCalls, 1) + require.Equal(t, "computer", toolCalls[0].ToolName) + require.Contains(t, toolCalls[0].Input, "screenshot") + + // Second call: send the tool result back, expect text. + resp2, err := model.Generate(t.Context(), fantasy.Call{ + Prompt: fantasy.Prompt{ + {Role: fantasy.MessageRoleSystem, Content: []fantasy.MessagePart{fantasy.TextPart{Text: "You are a helpful assistant"}}}, + {Role: fantasy.MessageRoleUser, Content: []fantasy.MessagePart{fantasy.TextPart{Text: "Take a screenshot of the desktop"}}}, + { + Role: fantasy.MessageRoleAssistant, + Content: []fantasy.MessagePart{ + fantasy.ToolCallPart{ + ToolCallID: toolCalls[0].ToolCallID, + ToolName: toolCalls[0].ToolName, + Input: toolCalls[0].Input, + }, + }, + }, + { + Role: fantasy.MessageRoleTool, + Content: []fantasy.MessagePart{ + fantasy.ToolResultPart{ + ToolCallID: toolCalls[0].ToolCallID, + Output: fantasy.ToolResultOutputContentMedia{ + Data: screenshotBase64, + MediaType: "image/png", + }, + }, + }, + }, + }, + Tools: []fantasy.Tool{cuTool}, + }) + require.NoError(t, err) + require.NotEmpty(t, resp2.Content.Text()) + require.Contains(t, resp2.Content.Text(), "desktop") + }) + + t.Run("computer use streaming", func(t *testing.T) { + r := vcr.NewRecorder(t) + + model, err := anthropicBuilder(m.model)(t, r) + require.NoError(t, err) + + cuTool := jsonRoundTripTool(t, anthropic.NewComputerUseTool(anthropic.ComputerUseToolOptions{ + DisplayWidthPx: 1920, + DisplayHeightPx: 1080, + ToolVersion: anthropic.ComputerUse20250124, + })) + + // First call: stream, collect tool call. + stream, err := model.Stream(t.Context(), fantasy.Call{ + Prompt: fantasy.Prompt{ + {Role: fantasy.MessageRoleSystem, Content: []fantasy.MessagePart{fantasy.TextPart{Text: "You are a helpful assistant"}}}, + {Role: fantasy.MessageRoleUser, Content: []fantasy.MessagePart{fantasy.TextPart{Text: "Take a screenshot of the desktop"}}}, + }, + Tools: []fantasy.Tool{cuTool}, + }) + require.NoError(t, err) + + var toolCallID, toolCallName, toolCallInput string + var finishReason fantasy.FinishReason + stream(func(part fantasy.StreamPart) bool { + switch part.Type { + case fantasy.StreamPartTypeToolCall: + toolCallID = part.ID + toolCallName = part.ToolCallName + toolCallInput = part.ToolCallInput + case fantasy.StreamPartTypeFinish: + finishReason = part.FinishReason + } + return true + }) + + require.Equal(t, fantasy.FinishReasonToolCalls, finishReason) + require.Equal(t, "computer", toolCallName) + require.Contains(t, toolCallInput, "screenshot") + + // Second call: send tool result, stream text back. + stream2, err := model.Stream(t.Context(), fantasy.Call{ + Prompt: fantasy.Prompt{ + {Role: fantasy.MessageRoleSystem, Content: []fantasy.MessagePart{fantasy.TextPart{Text: "You are a helpful assistant"}}}, + {Role: fantasy.MessageRoleUser, Content: []fantasy.MessagePart{fantasy.TextPart{Text: "Take a screenshot of the desktop"}}}, + { + Role: fantasy.MessageRoleAssistant, + Content: []fantasy.MessagePart{ + fantasy.ToolCallPart{ + ToolCallID: toolCallID, + ToolName: toolCallName, + Input: toolCallInput, + }, + }, + }, + { + Role: fantasy.MessageRoleTool, + Content: []fantasy.MessagePart{ + fantasy.ToolResultPart{ + ToolCallID: toolCallID, + Output: fantasy.ToolResultOutputContentMedia{ + Data: screenshotBase64, + MediaType: "image/png", + }, + }, + }, + }, + }, + Tools: []fantasy.Tool{cuTool}, + }) + require.NoError(t, err) + + var text string + stream2(func(part fantasy.StreamPart) bool { + if part.Type == fantasy.StreamPartTypeTextDelta { + text += part.Delta + } + return true + }) + require.NotEmpty(t, text) + require.Contains(t, text, "desktop") + }) + }) + } +} + +// jsonRoundTripTool simulates a JSON round-trip on a ProviderDefinedTool +// so numeric values become float64 as they would in real usage. +func jsonRoundTripTool(t *testing.T, tool fantasy.ProviderDefinedTool) fantasy.ProviderDefinedTool { + t.Helper() + data, err := json.Marshal(tool.Args) + require.NoError(t, err) + var args map[string]any + require.NoError(t, json.Unmarshal(data, &args)) + tool.Args = args + return tool +} diff --git a/providertests/testdata/TestAnthropicComputerUse/claude-sonnet-4/computer_use.yaml b/providertests/testdata/TestAnthropicComputerUse/claude-sonnet-4/computer_use.yaml new file mode 100644 index 000000000..e40e893b4 --- /dev/null +++ b/providertests/testdata/TestAnthropicComputerUse/claude-sonnet-4/computer_use.yaml @@ -0,0 +1,63 @@ +--- +version: 2 +interactions: +- id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 281 + host: "" + body: '{"max_tokens":4096,"messages":[{"content":[{"text":"Take a screenshot of the desktop","type":"text"}],"role":"user"}],"model":"claude-sonnet-4-20250514","system":[{"text":"You are a helpful assistant","type":"text"}],"tools":[{"display_height_px":1080,"display_width_px":1920,"name":"computer","type":"computer_20250124"}]}' + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - Anthropic/Go 1.10.0 + url: https://api.anthropic.com/v1/messages?beta=true + method: POST + response: + proto: HTTP/2.0 + proto_major: 2 + proto_minor: 0 + content_length: -1 + uncompressed: true + body: '{"id":"msg_cu_01","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"tool_use","id":"toolu_cu_01","name":"computer","input":{"action":"screenshot"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":100,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":50,"service_tier":"standard"}}' + headers: + Content-Type: + - application/json + status: 200 OK + code: 200 + duration: 1.5s +- id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 655 + host: "" + body: '{"max_tokens":4096,"messages":[{"content":[{"text":"Take a screenshot of the desktop","type":"text"}],"role":"user"},{"content":[{"id":"toolu_cu_01","input":{"action":"screenshot"},"name":"computer","type":"tool_use"}],"role":"assistant"},{"content":[{"tool_use_id":"toolu_cu_01","content":[{"source":{"data":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==","media_type":"image/png","type":"base64"},"type":"image"}],"type":"tool_result"}],"role":"user"}],"model":"claude-sonnet-4-20250514","system":[{"text":"You are a helpful assistant","type":"text"}],"tools":[{"display_height_px":1080,"display_width_px":1920,"name":"computer","type":"computer_20250124"}]}' + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - Anthropic/Go 1.10.0 + url: https://api.anthropic.com/v1/messages?beta=true + method: POST + response: + proto: HTTP/2.0 + proto_major: 2 + proto_minor: 0 + content_length: -1 + uncompressed: true + body: '{"id":"msg_cu_02","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"text","text":"I can see the desktop. It appears to be a standard desktop environment."}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":200,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":20,"service_tier":"standard"}}' + headers: + Content-Type: + - application/json + status: 200 OK + code: 200 + duration: 1.2s diff --git a/providertests/testdata/TestAnthropicComputerUse/claude-sonnet-4/computer_use_streaming.yaml b/providertests/testdata/TestAnthropicComputerUse/claude-sonnet-4/computer_use_streaming.yaml new file mode 100644 index 000000000..23f3d49d6 --- /dev/null +++ b/providertests/testdata/TestAnthropicComputerUse/claude-sonnet-4/computer_use_streaming.yaml @@ -0,0 +1,63 @@ +--- +version: 2 +interactions: +- id: 0 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 296 + host: "" + body: '{"max_tokens":4096,"messages":[{"content":[{"text":"Take a screenshot of the desktop","type":"text"}],"role":"user"}],"model":"claude-sonnet-4-20250514","stream":true,"system":[{"text":"You are a helpful assistant","type":"text"}],"tools":[{"display_height_px":1080,"display_width_px":1920,"name":"computer","type":"computer_20250124"}]}' + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - Anthropic/Go 1.10.0 + url: https://api.anthropic.com/v1/messages?beta=true + method: POST + response: + proto: HTTP/2.0 + proto_major: 2 + proto_minor: 0 + content_length: -1 + uncompressed: false + body: "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_cu_stream_01\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-20250514\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":100,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":0}}}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"tool_use\",\"id\":\"toolu_cu_stream_01\",\"name\":\"computer\",\"input\":{}}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"{\\\"action\\\": \\\"screenshot\\\"}\"}} \n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"tool_use\",\"stop_sequence\":null},\"usage\":{\"output_tokens\":50}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n" + headers: + Content-Type: + - text/event-stream + status: 200 OK + code: 200 + duration: 1.5s +- id: 1 + request: + proto: HTTP/1.1 + proto_major: 1 + proto_minor: 1 + content_length: 670 + host: "" + body: '{"max_tokens":4096,"messages":[{"content":[{"text":"Take a screenshot of the desktop","type":"text"}],"role":"user"},{"content":[{"id":"toolu_cu_stream_01","input":{"action":"screenshot"},"name":"computer","type":"tool_use"}],"role":"assistant"},{"content":[{"tool_use_id":"toolu_cu_stream_01","content":[{"source":{"data":"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==","media_type":"image/png","type":"base64"},"type":"image"}],"type":"tool_result"}],"role":"user"}],"model":"claude-sonnet-4-20250514","stream":true,"system":[{"text":"You are a helpful assistant","type":"text"}],"tools":[{"display_height_px":1080,"display_width_px":1920,"name":"computer","type":"computer_20250124"}]}' + headers: + Accept: + - application/json + Content-Type: + - application/json + User-Agent: + - Anthropic/Go 1.10.0 + url: https://api.anthropic.com/v1/messages?beta=true + method: POST + response: + proto: HTTP/2.0 + proto_major: 2 + proto_minor: 0 + content_length: -1 + uncompressed: false + body: "event: message_start\ndata: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_cu_stream_02\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-20250514\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":200,\"cache_creation_input_tokens\":0,\"cache_read_input_tokens\":0,\"output_tokens\":0}}}\n\nevent: content_block_start\ndata: {\"type\":\"content_block_start\",\"index\":0,\"content_block\":{\"type\":\"text\",\"text\":\"\"}}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"I can see the desktop. It appears to be a standard desktop environment.\"}}\n\nevent: content_block_stop\ndata: {\"type\":\"content_block_stop\",\"index\":0}\n\nevent: message_delta\ndata: {\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"end_turn\",\"stop_sequence\":null},\"usage\":{\"output_tokens\":20}}\n\nevent: message_stop\ndata: {\"type\":\"message_stop\"}\n\n" + headers: + Content-Type: + - text/event-stream + status: 200 OK + code: 200 + duration: 1.2s From 12345ae15482eeba18e9b2ad924fabf6de4f387a Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 19 Mar 2026 11:40:14 +0000 Subject: [PATCH 3/6] chore: use kylecarbs/openai-go fork for coder/coder compat coder/coder and coder/aibridge use the SasSwart perf fork of openai-go/v3 (deferred body serialization, appendCompact skip). This fork had a bug where WithJSONSet modifications (used by NewStreaming to inject "stream": true) were clobbered by the deferred body serialization. kylecarbs/openai-go includes the fix from https://github.com/kylecarbs/openai-go/pull/2. Also adjusts two .OfString accesses since the fork's ResponseOutputItemUnion.Arguments is a plain string rather than the upstream union type. --- go.mod | 7 +++++++ go.sum | 4 ++-- providers/openai/responses_language_model.go | 4 ++-- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index 292c77b2b..12e73e77d 100644 --- a/go.mod +++ b/go.mod @@ -83,3 +83,10 @@ require ( gopkg.in/dnaeon/go-vcr.v4 v4.0.6-0.20251110073552-01de4eb40290 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +// coder/coder and coder/aibridge use the SasSwart perf fork of +// openai-go (deferred body serialization, appendCompact skip). +// This points to kylecarbs' copy of that fork which includes a fix +// for WithJSONSet being clobbered by deferred serialization. +// See https://github.com/kylecarbs/openai-go/pull/2 +replace github.com/openai/openai-go/v3 => github.com/kylecarbs/openai-go/v3 v3.0.0-20260319113850-9477dcaedcae diff --git a/go.sum b/go.sum index 6ad062ae6..466eb6116 100644 --- a/go.sum +++ b/go.sum @@ -107,10 +107,10 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylecarbs/openai-go/v3 v3.0.0-20260319113850-9477dcaedcae h1:xlFZNX4nnxpj9Cf6mTwD3pirXGNtBJ/6COsf9iZmsL0= +github.com/kylecarbs/openai-go/v3 v3.0.0-20260319113850-9477dcaedcae/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/openai/openai-go/v3 v3.28.0 h1:2+FfrCVMdGXSQrBv1tLWtokm+BU7+3hJ/8rAHPQ63KM= -github.com/openai/openai-go/v3 v3.28.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= diff --git a/providers/openai/responses_language_model.go b/providers/openai/responses_language_model.go index 2ec330bbb..16430b2dc 100644 --- a/providers/openai/responses_language_model.go +++ b/providers/openai/responses_language_model.go @@ -820,7 +820,7 @@ func (o responsesLanguageModel) Generate(ctx context.Context, call fantasy.Call) ProviderExecuted: false, ToolCallID: outputItem.CallID, ToolName: outputItem.Name, - Input: outputItem.Arguments.OfString, + Input: outputItem.Arguments, }) case "web_search_call": @@ -1020,7 +1020,7 @@ func (o responsesLanguageModel) Stream(ctx context.Context, call fantasy.Call) ( Type: fantasy.StreamPartTypeToolCall, ID: done.Item.CallID, ToolCallName: done.Item.Name, - ToolCallInput: done.Item.Arguments.OfString, + ToolCallInput: done.Item.Arguments, }) { return } From 18e18e661ed4a6d47912694a890831481179b417 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 19 Mar 2026 15:02:01 +0000 Subject: [PATCH 4/6] fix(openai): skip reasoning items in Responses API replay When Store is enabled, replaying reasoning items (OfReasoning) in the Responses API input causes a validation error: Item 'rs_xxx' of type 'reasoning' was provided without its required following item. The API stores reasoning server-side and cannot pair a reconstructed reasoning item with the output item that originally followed it. The fix skips reasoning parts during replay, letting the conversation continue with visible assistant content (text / tool calls). --- providers/openai/openai_test.go | 88 ++++++++++++++++++-- providers/openai/responses_language_model.go | 15 +++- 2 files changed, 93 insertions(+), 10 deletions(-) diff --git a/providers/openai/openai_test.go b/providers/openai/openai_test.go index f9c7235a1..fd82284d8 100644 --- a/providers/openai/openai_test.go +++ b/providers/openai/openai_test.go @@ -3105,7 +3105,7 @@ func TestResponsesToPrompt_DropsEmptyMessages(t *testing.T) { }, } - input, warnings := toResponsesPrompt(prompt, "system") + input, warnings := toResponsesPrompt(prompt, "system", false) require.Len(t, input, 1, "should only have user message") require.Len(t, warnings, 1) @@ -3131,7 +3131,7 @@ func TestResponsesToPrompt_DropsEmptyMessages(t *testing.T) { }, } - input, warnings := toResponsesPrompt(prompt, "system") + input, warnings := toResponsesPrompt(prompt, "system", false) require.Len(t, input, 2, "should have both user and assistant messages") require.Empty(t, warnings) @@ -3159,7 +3159,7 @@ func TestResponsesToPrompt_DropsEmptyMessages(t *testing.T) { }, } - input, warnings := toResponsesPrompt(prompt, "system") + input, warnings := toResponsesPrompt(prompt, "system", false) require.Len(t, input, 2, "should have both user and assistant messages") require.Empty(t, warnings) @@ -3180,7 +3180,7 @@ func TestResponsesToPrompt_DropsEmptyMessages(t *testing.T) { }, } - input, warnings := toResponsesPrompt(prompt, "system") + input, warnings := toResponsesPrompt(prompt, "system", false) require.Empty(t, input) require.Len(t, warnings, 2) // One for unsupported type, one for empty message @@ -3202,7 +3202,7 @@ func TestResponsesToPrompt_DropsEmptyMessages(t *testing.T) { }, } - input, warnings := toResponsesPrompt(prompt, "system") + input, warnings := toResponsesPrompt(prompt, "system", false) require.Len(t, input, 1) require.Empty(t, warnings) @@ -3223,7 +3223,7 @@ func TestResponsesToPrompt_DropsEmptyMessages(t *testing.T) { }, } - input, warnings := toResponsesPrompt(prompt, "system") + input, warnings := toResponsesPrompt(prompt, "system", false) require.Len(t, input, 1) require.Empty(t, warnings) @@ -3244,7 +3244,7 @@ func TestResponsesToPrompt_DropsEmptyMessages(t *testing.T) { }, } - input, warnings := toResponsesPrompt(prompt, "system") + input, warnings := toResponsesPrompt(prompt, "system", false) require.Len(t, input, 1) require.Empty(t, warnings) @@ -3874,7 +3874,7 @@ func TestResponsesToPrompt_WebSearchProviderExecutedToolResults(t *testing.T) { }, } - input, warnings := toResponsesPrompt(prompt, "system instructions") + input, warnings := toResponsesPrompt(prompt, "system instructions", false) require.Empty(t, warnings) @@ -3886,6 +3886,78 @@ func TestResponsesToPrompt_WebSearchProviderExecutedToolResults(t *testing.T) { "expected user + item_reference + assistant text") } +func TestResponsesToPrompt_ReasoningWithStore(t *testing.T) { + t.Parallel() + + encryptedContent := "gAAAAABpvAwtDPh5dSXW86hwbwoTo4DJHANQ" + reasoningItemID := "rs_08d030b87966238b0069bc095b7e5c81" + + reasoningPart := fantasy.ReasoningPart{ + Text: "Let me think about this...", + ProviderOptions: fantasy.ProviderOptions{ + Name: &ResponsesReasoningMetadata{ + ItemID: reasoningItemID, + EncryptedContent: &encryptedContent, + Summary: []string{}, + }, + }, + } + + prompt := fantasy.Prompt{ + { + Role: fantasy.MessageRoleUser, + Content: []fantasy.MessagePart{ + fantasy.TextPart{Text: "What is 2+2?"}, + }, + }, + { + Role: fantasy.MessageRoleAssistant, + Content: []fantasy.MessagePart{ + reasoningPart, + fantasy.TextPart{Text: "4"}, + }, + }, + { + Role: fantasy.MessageRoleUser, + Content: []fantasy.MessagePart{ + fantasy.TextPart{Text: "And 3+3?"}, + }, + }, + } + + t.Run("store true skips reasoning", func(t *testing.T) { + t.Parallel() + + input, warnings := toResponsesPrompt(prompt, "system", true) + require.Empty(t, warnings) + + // With store=true: user, assistant text (reasoning + // skipped), follow-up user. + require.Len(t, input, 3) + + // Verify no reasoning item leaked through. + for _, item := range input { + require.Nil(t, item.OfReasoning, + "reasoning items must not appear when store=true") + } + }) + + t.Run("store false includes reasoning", func(t *testing.T) { + t.Parallel() + + input, warnings := toResponsesPrompt(prompt, "system", false) + require.Empty(t, warnings) + + // With store=false: user, reasoning, assistant text, + // follow-up user. + require.Len(t, input, 4) + + // Second item should be the reasoning. + require.NotNil(t, input[1].OfReasoning) + require.Equal(t, reasoningItemID, input[1].OfReasoning.ID) + }) +} + func TestResponsesStream_WebSearchResponse(t *testing.T) { t.Parallel() diff --git a/providers/openai/responses_language_model.go b/providers/openai/responses_language_model.go index 16430b2dc..824526e4a 100644 --- a/providers/openai/responses_language_model.go +++ b/providers/openai/responses_language_model.go @@ -176,7 +176,8 @@ func (o responsesLanguageModel) prepareParams(call fantasy.Call) (*responses.Res params.PreviousResponseID = param.NewOpt(*openaiOptions.PreviousResponseID) } - input, inputWarnings := toResponsesPrompt(call.Prompt, modelConfig.systemMessageMode) + storeEnabled := openaiOptions != nil && openaiOptions.Store != nil && *openaiOptions.Store + input, inputWarnings := toResponsesPrompt(call.Prompt, modelConfig.systemMessageMode, storeEnabled) warnings = append(warnings, inputWarnings...) var include []IncludeType @@ -391,7 +392,7 @@ func responsesUsage(resp responses.Response) fantasy.Usage { return usage } -func toResponsesPrompt(prompt fantasy.Prompt, systemMessageMode string) (responses.ResponseInputParam, []fantasy.CallWarning) { +func toResponsesPrompt(prompt fantasy.Prompt, systemMessageMode string, store bool) (responses.ResponseInputParam, []fantasy.CallWarning) { var input responses.ResponseInputParam var warnings []fantasy.CallWarning @@ -560,6 +561,16 @@ func toResponsesPrompt(prompt fantasy.Prompt, systemMessageMode string) (respons // recognised Responses API input type; skip. continue case fantasy.ContentTypeReasoning: + if store { + // When Store is enabled the API already has the + // reasoning persisted server-side. Replaying the + // full OfReasoning item causes a validation error + // ("reasoning was provided without its required + // following item") because the API cannot pair the + // reconstructed reasoning with the output item + // that followed it. + continue + } reasoningMetadata := GetReasoningMetadata(c.Options()) if reasoningMetadata == nil || reasoningMetadata.ItemID == "" { continue From 7bcfc3d2021a072cc2e47a1e957dcafec865912c Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Fri, 20 Mar 2026 17:52:25 +0000 Subject: [PATCH 5/6] fix(providers/openai): skip ephemeral replay items when store=false Cherry-picked from ibetitsmike/fantasy#4. - Reasoning items: Always skip during replay (store=true has them persisted; store=false IDs are ephemeral). - Provider-executed tool calls: Gate item_reference behind the store flag; skip when store=false. --- providers/openai/openai_test.go | 43 ++++++++----- providers/openai/responses_language_model.go | 60 ++++++------------- .../azure-gpt-5-mini/thinking-streaming.yaml | 4 +- .../azure-gpt-5-mini/thinking.yaml | 4 +- .../openai-gpt-5/thinking-streaming.yaml | 4 +- .../openai-gpt-5/thinking.yaml | 4 +- .../openai-o4-mini/thinking-streaming.yaml | 4 +- .../openai-o4-mini/thinking.yaml | 4 +- 8 files changed, 57 insertions(+), 70 deletions(-) diff --git a/providers/openai/openai_test.go b/providers/openai/openai_test.go index fd82284d8..095623557 100644 --- a/providers/openai/openai_test.go +++ b/providers/openai/openai_test.go @@ -3874,16 +3874,29 @@ func TestResponsesToPrompt_WebSearchProviderExecutedToolResults(t *testing.T) { }, } - input, warnings := toResponsesPrompt(prompt, "system instructions", false) + t.Run("store false skips item reference", func(t *testing.T) { + t.Parallel() + + input, warnings := toResponsesPrompt(prompt, "system instructions", false) + + require.Empty(t, warnings) + require.Len(t, input, 2, + "expected user + assistant text when store=false") + require.Nil(t, input[0].OfItemReference) + require.Nil(t, input[1].OfItemReference) + }) + + t.Run("store true uses item reference", func(t *testing.T) { + t.Parallel() - require.Empty(t, warnings) + input, warnings := toResponsesPrompt(prompt, "system instructions", true) - // Expected input items: user message, item_reference (for - // provider-executed tool call; the ToolResultPart is skipped), - // and assistant text message. System instructions are passed - // via params.Instructions, not as an input item. - require.Len(t, input, 3, - "expected user + item_reference + assistant text") + require.Empty(t, warnings) + require.Len(t, input, 3, + "expected user + item_reference + assistant text when store=true") + require.NotNil(t, input[1].OfItemReference) + require.Equal(t, "ws_01", input[1].OfItemReference.ID) + }) } func TestResponsesToPrompt_ReasoningWithStore(t *testing.T) { @@ -3942,19 +3955,19 @@ func TestResponsesToPrompt_ReasoningWithStore(t *testing.T) { } }) - t.Run("store false includes reasoning", func(t *testing.T) { + t.Run("store false skips reasoning", func(t *testing.T) { t.Parallel() input, warnings := toResponsesPrompt(prompt, "system", false) require.Empty(t, warnings) - // With store=false: user, reasoning, assistant text, - // follow-up user. - require.Len(t, input, 4) + // With store=false: user, assistant text, follow-up user. + require.Len(t, input, 3) - // Second item should be the reasoning. - require.NotNil(t, input[1].OfReasoning) - require.Equal(t, reasoningItemID, input[1].OfReasoning.ID) + for _, item := range input { + require.Nil(t, item.OfReasoning, + "reasoning items must not appear when store=false") + } }) } diff --git a/providers/openai/responses_language_model.go b/providers/openai/responses_language_model.go index 824526e4a..8d4c15aec 100644 --- a/providers/openai/responses_language_model.go +++ b/providers/openai/responses_language_model.go @@ -539,10 +539,16 @@ func toResponsesPrompt(prompt fantasy.Prompt, systemMessageMode string, store bo } if toolCallPart.ProviderExecuted { - // Round-trip provider-executed tools via - // item_reference, letting the API resolve - // the stored output item by ID. - input = append(input, responses.ResponseInputItemParamOfItemReference(toolCallPart.ToolCallID)) + if store { + // Round-trip provider-executed tools via + // item_reference, letting the API resolve + // the stored output item by ID. + input = append(input, responses.ResponseInputItemParamOfItemReference(toolCallPart.ToolCallID)) + } + // When store is disabled, server-side items are + // ephemeral and cannot be referenced. Skip the + // tool call; results are already omitted for + // provider-executed tools. continue } @@ -561,45 +567,13 @@ func toResponsesPrompt(prompt fantasy.Prompt, systemMessageMode string, store bo // recognised Responses API input type; skip. continue case fantasy.ContentTypeReasoning: - if store { - // When Store is enabled the API already has the - // reasoning persisted server-side. Replaying the - // full OfReasoning item causes a validation error - // ("reasoning was provided without its required - // following item") because the API cannot pair the - // reconstructed reasoning with the output item - // that followed it. - continue - } - reasoningMetadata := GetReasoningMetadata(c.Options()) - if reasoningMetadata == nil || reasoningMetadata.ItemID == "" { - continue - } - if len(reasoningMetadata.Summary) == 0 && reasoningMetadata.EncryptedContent == nil { - warnings = append(warnings, fantasy.CallWarning{ - Type: fantasy.CallWarningTypeOther, - Message: "assistant message reasoning part does is empty", - }) - continue - } - // we want to always send an empty array - summary := make([]responses.ResponseReasoningItemSummaryParam, 0, len(reasoningMetadata.Summary)) - for _, s := range reasoningMetadata.Summary { - summary = append(summary, responses.ResponseReasoningItemSummaryParam{ - Type: "summary_text", - Text: s, - }) - } - reasoning := &responses.ResponseReasoningItemParam{ - ID: reasoningMetadata.ItemID, - Summary: summary, - } - if reasoningMetadata.EncryptedContent != nil { - reasoning.EncryptedContent = param.NewOpt(*reasoningMetadata.EncryptedContent) - } - input = append(input, responses.ResponseInputItemUnionParam{ - OfReasoning: reasoning, - }) + // Reasoning items are always skipped during replay. + // When store is enabled, the API already has them + // persisted server-side. When store is disabled, the + // item IDs are ephemeral and referencing them causes + // "Item not found" errors. In both cases, replaying + // reasoning inline is not supported by the API. + continue } } diff --git a/providertests/testdata/TestAzureResponsesWithSummaryThinking/azure-gpt-5-mini/thinking-streaming.yaml b/providertests/testdata/TestAzureResponsesWithSummaryThinking/azure-gpt-5-mini/thinking-streaming.yaml index f0c56303d..7bd039f16 100644 --- a/providertests/testdata/TestAzureResponsesWithSummaryThinking/azure-gpt-5-mini/thinking-streaming.yaml +++ b/providertests/testdata/TestAzureResponsesWithSummaryThinking/azure-gpt-5-mini/thinking-streaming.yaml @@ -359,9 +359,9 @@ interactions: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 2252 + content_length: 805 host: "" - body: '{"store":false,"include":["reasoning.encrypted_content"],"input":[{"content":"You are a helpful assistant","role":"developer"},{"content":[{"text":"What''s the weather in Florence, Italy?","type":"input_text"}],"role":"user"},{"id":"rs_04fcf497c9f5b8760169b2ffa108888195ba649a92a2f17e92","summary":[{"text":"**Getting weather for Florence**\n\nThe user wants to know the weather in Florence, Italy. I see that we have a tool to retrieve weather data. I’ll call the function directly with the location specified as \"Florence, Italy.\" Since we only need one tool for this request, I don’t need to worry about running multiple tools simultaneously. I’ll make sure the parameters are formatted correctly before calling the function to get the weather information for the user.","type":"summary_text"}],"encrypted_content":"gAAAAABpsv-hvjC3ll56K8M8nnWZrQ-hbWQLb5b_X52f7q23IBmVXDU0nN6VSIiV-o358MizEILazwWUyq79OIOkNK1okzmE7rhJseAha9MK_tchfBJKuhQjm6SyIHK8DHCKOJgI1EIxzcOVflMKmgAJPXLDxRQss9fK_3gJCAGeFh_wy5Ok9BgdJ106wJGU9Jn4LrE17vdXWDfrsCLDscLiVa1jMBV9G_Qmw0m0iquamPG-K492beGN3ISA40E1qln74_dcHjKoxzIL7-wrKnUPHewKWW3MaivcnCjY6OPlxsalOTBUng18_Xzh-Xin5cN5BRDpCbUgl16hMKpq8yUnyzkJa-3joOHbETytktwBhq_4fOhTzWsRogx_uw5Yz2yFvYSVtJFXB9xA64jMaTA3jxpdMyuDRPWXZ5x7h4exGddbP_ED0qqd_zCe3f1nWEywA7U-_KaRQjk1zG54FRWiqZ6nWsOnNWKtOttPUWzQBXU17pg3KyuDidX07RSRXFa-Qs3RtgUuwqRhfp1dmmCkDdct-W4QlT-F-bAf-Pq7dXpVGSZ4cUUQk1uBktBqjs_agHPgn6m7Lap4ALe08HM4lYsj3FUPPSn6pUmRbzEK85HNVhXcSX8JuE8yu0AeIGKIrKYcD4BAdo01DFzUjAQJCXTA4QVoJzCeDPoPFU8uZxVHiN7fW1V-UmqOv_isC2uySwEYdy-hCMH20bjxMIAAH0wX2YzeJtIYE0pqNeCIhxng6tOcZmBGT7GOXMzkycjPvEOSrBJ6MawLCfxpSVeeGMADWPMqkPQfW-AlMH5ogMnDWk_PXm4=","type":"reasoning"},{"arguments":"\"{\\\"location\\\":\\\"Florence, Italy\\\"}\"","call_id":"call_XjLMCgd17m2E4EOiIVoc4J91","name":"weather","type":"function_call"},{"call_id":"call_XjLMCgd17m2E4EOiIVoc4J91","output":"40 C","type":"function_call_output"}],"model":"gpt-5-mini","reasoning":{"effort":"high","summary":"auto"},"tool_choice":"auto","tools":[{"strict":false,"parameters":{"properties":{"location":{"description":"the city","type":"string"}},"required":["location"],"type":"object"},"name":"weather","description":"Get weather information for a location","type":"function"}],"stream":true}' + body: '{"store":false,"include":["reasoning.encrypted_content"],"input":[{"content":"You are a helpful assistant","role":"developer"},{"content":[{"text":"What''s the weather in Florence, Italy?","type":"input_text"}],"role":"user"},{"arguments":"\"{\\\"location\\\":\\\"Florence, Italy\\\"}\"","call_id":"call_XjLMCgd17m2E4EOiIVoc4J91","name":"weather","type":"function_call"},{"call_id":"call_XjLMCgd17m2E4EOiIVoc4J91","output":"40 C","type":"function_call_output"}],"model":"gpt-5-mini","reasoning":{"effort":"high","summary":"auto"},"tool_choice":"auto","tools":[{"strict":false,"parameters":{"properties":{"location":{"description":"the city","type":"string"}},"required":["location"],"type":"object"},"name":"weather","description":"Get weather information for a location","type":"function"}],"stream":true}' headers: Accept: - application/json diff --git a/providertests/testdata/TestAzureResponsesWithSummaryThinking/azure-gpt-5-mini/thinking.yaml b/providertests/testdata/TestAzureResponsesWithSummaryThinking/azure-gpt-5-mini/thinking.yaml index fd136b379..b16395b61 100644 --- a/providertests/testdata/TestAzureResponsesWithSummaryThinking/azure-gpt-5-mini/thinking.yaml +++ b/providertests/testdata/TestAzureResponsesWithSummaryThinking/azure-gpt-5-mini/thinking.yaml @@ -197,9 +197,9 @@ interactions: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 3250 + content_length: 791 host: "" - body: '{"store":false,"include":["reasoning.encrypted_content"],"input":[{"content":"You are a helpful assistant","role":"developer"},{"content":[{"text":"What''s the weather in Florence, Italy?","type":"input_text"}],"role":"user"},{"id":"rs_03e04b1d440a87110169b2ff91f840819692bf236ebfcc7f66","summary":[{"text":"**Fetching weather for Florence**\n\nThe user is asking for the weather in Florence, Italy. I see that I can use the functions.weather tool, which takes a location string. I could use multi_tool_use.parallel, but since I just need one tool, it makes more sense to call functions.weather directly. The tool expects a JSON format with the location specified, so I''ll structure it as {\"location\":\"Florence, Italy\"}. Now, I’ll go ahead and call this function to get the weather.","type":"summary_text"}],"encrypted_content":"gAAAAABpsv-VXwrkiA2LGZS81tv6gXmlZFltBSfylmK7DeO9bnDg43lIoU7T_Dmcxg4SHjIsSOAt1-QnNyApcix2-vjt8aim5NX85sGG-DOFbsikMl-eSyXc4B98LXjP9nS0XxNAakLhhBMObzmIFlf7CwMGSyn3z9VT9Z0oizw6BP5TiEVXSJKZcnzPW4CDfVlGzrBk60NnS2ugDpkWUd6krcc_vU_CudDFDAbFVs4HGEGJDSAXnms0XEWemEnkbjL1dLhesjW-MX-W4uQEGXNJqcVoaC0z0LPu6G7T9SdKEZbrNI5GQu-bL67pjdFfddv6VrrSWovs7-D0X6lr5ZAjuDkqPLQo1uyJDAoOUgx-Z2z7tACHEkG7jqCenTJ3fwQe88KdDcoJoZ59s6VqL-msbnAZpiTKdMyM0wR5jQSXwtaVp9yhNgqjRWDW-RlJc7RbY_4x4j5xL3p1vyF1ioFjo2z7u5Lb9tsBLHOXRDpb9BFGXQbxOmP4VpvMN_93CcszyIRd6mPp3taVpnJPlLTSVW4uBXfGX2RKLAcT0DsYicB3O0sjjfWpGLQuEvmCWSrCHBwYAy-PGDPAQgcsGjsE9alwXOhHXONyq5BkiIs4cQ3ZWCibDTqg2CEVsCuXZPHL50SjuQOFRmFZkXRbnH8xXTAhDNGqFsISgHCeSV6EPEUL72n1SPeJPdxHc1x1ZZL7HdqHMQUfPnE-eMW4IKapOU3MJ5rxXui6ye5aYBHtRFVJgv6xf-lYpWQXLjJWqulelZPST5gLeAl9NPcPwpz3frgoWH9GpB6glNgavPVWQ2cJwYTv85gJ0I3kLATYBY5LRegYAqBvKGBE0ZDR55neZwn83uckMcEh37diCV3SbPFQMUbxw9gkFVobB3jC3r0OetBTXc0rBcO1bN2s1pRf_493hP1uG4r1Q40_kJubyq6KjaJm_aOl6-X9aGVV6VmTmcvSPzrfga7_sXoS23Qxgo5qfyqj6ERnsSUHvqofb7XRUZDbpMSYUyQ8MonZSYs8OuZT7ZoFI0F2WNqZEOBfbYCMeCimlPYmdwgb_3LJMf5ivUbuRq9uG94TrZXxLG2MprIvNHJAxVVt3eRHOzMIo0kgwRYtHd-MdIyeo6nUmv_JPZIRJvnJ5w4GRCvcX9V6j2Yq2CV39NM7QFhSkoSTDz2dKvXwQY3gzhCQM651-mINb86qiL3L-vVtGzZe2ZL5z6VBDON-QOiQyZV7fLH8S5ECKgoK_X81fEjxBAuRkBZGAB-HiuIA6DV_B2O_zE-z09a0ObdBztRd68qMMBNQEuTGZx07C1nVLNZPKro-qT_mx9IbAUwQClw2QHhT6vWIvuWdFWdod44rFDtv-zTImkWtMxbQOjc7dWSPZN6dN-7TCQtVR0DIPl0aouEtgRu3eSWKeHOjTUKBa8D0izB2UQYGHpkyL5-FlTc7NplZRbWMjBDsio-63EV4a_ZISrt2AtacO7p1Tk_H-7atZdWYi1Y0aBg8qf1QWtKzdef-EdgkGPcmUr4rqkH67LrOiNncq-tiqiZhRQ1lHduosIB2dN-Of5NFPe3N5oRD0cSlwhMWJjgC3zkA7ECx8kzKjFsJNPFSqh738KQRxwpaZbqLBIc_FA5WzgSByt_7mLpNKTqQHss_RtU2YMe2JH0VO8V7Waa_mpqELoHcMjHfaIKci-srsOtheWV1x8VSE38ouhV16xaE5WieXL5vtrUJGjdKLsKbm7wYrSnIuU9nr_Ms9nGx5dqdEguJkjKbuNkrZFuUk_Ld5s0Dyg9loLcagD_rRJ8wswmH9xRY-4P1zNZMFKwULj92Xw==","type":"reasoning"},{"arguments":"\"{\\\"location\\\":\\\"Florence, Italy\\\"}\"","call_id":"call_58ZCzgYxpwaBCsl2an3OWjkQ","name":"weather","type":"function_call"},{"call_id":"call_58ZCzgYxpwaBCsl2an3OWjkQ","output":"40 C","type":"function_call_output"}],"model":"gpt-5-mini","reasoning":{"effort":"high","summary":"auto"},"tool_choice":"auto","tools":[{"strict":false,"parameters":{"properties":{"location":{"description":"the city","type":"string"}},"required":["location"],"type":"object"},"name":"weather","description":"Get weather information for a location","type":"function"}]}' + body: '{"store":false,"include":["reasoning.encrypted_content"],"input":[{"content":"You are a helpful assistant","role":"developer"},{"content":[{"text":"What''s the weather in Florence, Italy?","type":"input_text"}],"role":"user"},{"arguments":"\"{\\\"location\\\":\\\"Florence, Italy\\\"}\"","call_id":"call_58ZCzgYxpwaBCsl2an3OWjkQ","name":"weather","type":"function_call"},{"call_id":"call_58ZCzgYxpwaBCsl2an3OWjkQ","output":"40 C","type":"function_call_output"}],"model":"gpt-5-mini","reasoning":{"effort":"high","summary":"auto"},"tool_choice":"auto","tools":[{"strict":false,"parameters":{"properties":{"location":{"description":"the city","type":"string"}},"required":["location"],"type":"object"},"name":"weather","description":"Get weather information for a location","type":"function"}]}' headers: Accept: - application/json diff --git a/providertests/testdata/TestOpenAIResponsesWithSummaryThinking/openai-gpt-5/thinking-streaming.yaml b/providertests/testdata/TestOpenAIResponsesWithSummaryThinking/openai-gpt-5/thinking-streaming.yaml index 86801a4fe..c943edca7 100644 --- a/providertests/testdata/TestOpenAIResponsesWithSummaryThinking/openai-gpt-5/thinking-streaming.yaml +++ b/providertests/testdata/TestOpenAIResponsesWithSummaryThinking/openai-gpt-5/thinking-streaming.yaml @@ -371,9 +371,9 @@ interactions: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 2262 + content_length: 800 host: "" - body: '{"store":false,"include":["reasoning.encrypted_content"],"input":[{"content":"You are a helpful assistant","role":"developer"},{"content":[{"text":"What''s the weather in Florence, Italy?","type":"input_text"}],"role":"user"},{"id":"rs_0ccfefab56fc1e6b0169b3197fec948191a5128b289bd3abd7","summary":[{"text":"**Retrieving weather for Florence**\n\nI need to answer the user''s question about the weather in Florence, Italy. I have a function to get this information, specifically the functions.weather tool, which I''ll use with the location \"Florence, Italy.\" Since I''m likely only needing one tool, I won’t bother with any parallel options. I’ll ensure to keep my output concise, as I follow the instructions on formatting. Now, let’s go ahead and make that tool call!","type":"summary_text"}],"encrypted_content":"gAAAAABpsxl_yV8A68uHGe_5NFByLGg0aQFfiez_MlTK4CqH7M7ggUstkIPpnHaMbPFVdcobD1ZZy2MYigBVV-8Zornn3d3wHBC5_vpFlyy1O1veu1vqKpwD2W7ndqH3J2hdiV_BspR6q3LrY7ioEU_-vs0lCcJxPkf8mtwdflfiB99foqC5MM8s53paHZSQF3XjLHPOkAfD6xstcU6nHMM00NGcD0iG-OcUZVD_92mPPaIZaehoZpSmD2_m4U3NPR7xoGCuHwvFcA4y7qhzjUvkCSb0onjtTTpmePp38fhPzxEOQwZblNmdcseRA5-mMCzm7j67K03Ieftkj7T6m_JFY86XOdeMn0SyXKq3U3v3tWyXuLdQTRRE_70kZTBLoK4cR2JG-TkPHTYg33o9VYn2ePv6hKFFCBu1DcSfGWz0MU-40W1CccKvHh_xoHlmdNxfWychcekNA42EWGYRH3R6LPCeFINWfmy7Hbej_q4lH135At4_VdE6gkTwZPWRgV6KnK_wcTgGmNkW6oY8qwFb4dmZYb4QgFH8_jPsacvIWHv7zIqJeJFs3bvJt-7ZcKOeBWmHpUUyWuGjxID36HH3LSwDPc2Scc1iJOfdcszxmCnLHIiu9Br-90u9n_BVvwxl88SQO50t4yu5P-9fX1JFeHyfHTu_c1n8xBqO1ZgKMu_Z-bhZ63LpsXClx20W4vDKgfUZRYa3HKhmEjttDY0YWeEAsUsgJyJGDqkcel_0AgRyUaSCx-xtP6mQOoNQA8H770UbZLtShb7otktAc7M5FUM3pTodAp_Xr2rdAD4OAIpqgvyPPvT8-ma2vrGUdQr_NIKj9Z58","type":"reasoning"},{"arguments":"\"{\\\"location\\\":\\\"Florence, Italy\\\"}\"","call_id":"call_vTqMwYdzvdmoeNyNdQOqI1uY","name":"weather","type":"function_call"},{"call_id":"call_vTqMwYdzvdmoeNyNdQOqI1uY","output":"40 C","type":"function_call_output"}],"model":"gpt-5","reasoning":{"effort":"high","summary":"auto"},"tool_choice":"auto","tools":[{"strict":false,"parameters":{"properties":{"location":{"description":"the city","type":"string"}},"required":["location"],"type":"object"},"name":"weather","description":"Get weather information for a location","type":"function"}],"stream":true}' + body: '{"store":false,"include":["reasoning.encrypted_content"],"input":[{"content":"You are a helpful assistant","role":"developer"},{"content":[{"text":"What''s the weather in Florence, Italy?","type":"input_text"}],"role":"user"},{"arguments":"\"{\\\"location\\\":\\\"Florence, Italy\\\"}\"","call_id":"call_vTqMwYdzvdmoeNyNdQOqI1uY","name":"weather","type":"function_call"},{"call_id":"call_vTqMwYdzvdmoeNyNdQOqI1uY","output":"40 C","type":"function_call_output"}],"model":"gpt-5","reasoning":{"effort":"high","summary":"auto"},"tool_choice":"auto","tools":[{"strict":false,"parameters":{"properties":{"location":{"description":"the city","type":"string"}},"required":["location"],"type":"object"},"name":"weather","description":"Get weather information for a location","type":"function"}],"stream":true}' headers: Accept: - application/json diff --git a/providertests/testdata/TestOpenAIResponsesWithSummaryThinking/openai-gpt-5/thinking.yaml b/providertests/testdata/TestOpenAIResponsesWithSummaryThinking/openai-gpt-5/thinking.yaml index a84f4dbea..8ad7bd43e 100644 --- a/providertests/testdata/TestOpenAIResponsesWithSummaryThinking/openai-gpt-5/thinking.yaml +++ b/providertests/testdata/TestOpenAIResponsesWithSummaryThinking/openai-gpt-5/thinking.yaml @@ -131,9 +131,9 @@ interactions: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 2916 + content_length: 786 host: "" - body: '{"store":false,"include":["reasoning.encrypted_content"],"input":[{"content":"You are a helpful assistant","role":"developer"},{"content":[{"text":"What''s the weather in Florence, Italy?","type":"input_text"}],"role":"user"},{"id":"rs_075b435690aee51e0169b3196f54a8819e9a33c038fdc1f932","summary":[{"text":"**Getting weather information**\n\nI need to gather weather info, and I see that the tool functions.weather is here for that. Since we only need one call, using multi_tool_use.parallel isn''t necessary. I''ll go ahead and use functions.weather with the location \"Florence, Italy.\" There’s no need for parallel execution. It''s important to use this tool and provide the output based on whatever it returns. Time to make that call!","type":"summary_text"}],"encrypted_content":"gAAAAABpsxlz3zqwjvVFph3GpyCoNOUWPvEYHgvC39jVaRRd3SQ2K3vbO0ocwgzzM5dsBis4F8Ow3j5jcd3B8xKCqy7Wsxsfe2zz_DaERYO_bLZc3NjID8kelhxHKcuocSNx9tVQWpNTxUoYCveFyCyveaZ3lt0u4SZGWK_ZAkfIgmsGfddN-UeUH2lyheldZajgTPwa_PZQgTpcY7ERXCM_jh__IT1zibygPsb4u5G-XPvtkjA2oirQ8KbzaWqWhjj_hOJ43g_jePL0v1LHSNC9SiIVMmbifsBoQ6smsOYl3e_227AwvLspEWH10T1FtajmyfOPGYhRr1ZPfyx2eRDcP0vMPIejfDgLfKZklJUeErKVZrx785QR4ugWM7Qr2nggdM15H2rWQoZumX-RNQNtAVYTXmnyQPwoWbtL0fQ7oZDmEw9o0rQo987rawE2cjgrE9yic4wPw14g2fciUgluYaESuuJnNa4ebOawL2iIRWzrV6GNF6B6dkxmByHv8BGwZbZV3afroFwRc3C9-3M0wglbNlwyPiWWL0VMcaHUNlyk2cEFn-sgzBv2bYPtu3KSK0ww54r2iwxoPr7BvLf61HnPeiaWVxdcrhYWexAaUjMyAtDTWzwHKVwDVVCk-uspqH_PRxu5bxq6KkOjA6COUlwCEoGeopc5f6WdopbFlYekzY4hd73DiFJYfArUfcFyLf0kB475TTeLhCQtdlsAjwZyqk2rNKxV1D4Td0RsibuanFKcKc3WsShVypvNWd7WjByl13xUHXOFZ_LN6dX_ZhfDAW21N4Yp5XWHBT59xlvF7oeh6CvdgN21SJ0HfyurQBbI4TyhwX3xB9doC-Qhh-rDURnA3LZUCN3Iudg0Eo0RjJCbmT_Crv-PthflMMT95TR6M4LXv7puGvx94Xemc7H2BtFy9E3If0Zsf8uRksxr89Er23vwkaKTtRsMvX1wGPUgpcaxyMqV5FWwLj2-dennw2lDHTk2k46YeeaeSrn0lkV-s1-pQw9GwPIy-Mcmo6Aa_ncLETgyRGHLSTH28SElvWXgsa4oa8XypwNISTATjwzgM9yGhXbmA9CVbXCuhQ0uKzGgNzsYOwBqtxS8aVigdTPj9ktCZJD2Y4GziE-i7q4FLc511fiIMQZC-MUNMQO4d7A1kEveveZZntjiP-ZfBvdXMSb24fxfKWhAguFREzVm-7V8tbweipr5s950Ae7uJqjM_TJ4xGs66lg-TLTJLCCho4EnfYqjAmU-cR1o3l6A9RQXTzW4Okmh4oKyhlsWkL2LbMKU5UUfIfnWYM8vsJA75CPR633cGscm2Ku5SNdu5bY6cReO2YN8yRqViEUi4K04ORzaO9xNLnS3tf0FYsv7g2yM88eOEsLoCpHRsXdpvslmxKYb6K4Kjo24wvLuduTy19OzCsF4v69vcgUUOAwvPnJMfdYK9mQOTUmNyxqJ-wErLLn3FkUXylUvLDsb7bQtGZqwYh2e65YuQvOzRA2Djv_dUAaVquf3bl0TQAu0iQp9ddOSfwjfH96B1ZUH2KIP","type":"reasoning"},{"arguments":"\"{\\\"location\\\":\\\"Florence, Italy\\\"}\"","call_id":"call_1z70BexeUjpiAcqPgwQiiVRh","name":"weather","type":"function_call"},{"call_id":"call_1z70BexeUjpiAcqPgwQiiVRh","output":"40 C","type":"function_call_output"}],"model":"gpt-5","reasoning":{"effort":"high","summary":"auto"},"tool_choice":"auto","tools":[{"strict":false,"parameters":{"properties":{"location":{"description":"the city","type":"string"}},"required":["location"],"type":"object"},"name":"weather","description":"Get weather information for a location","type":"function"}]}' + body: '{"store":false,"include":["reasoning.encrypted_content"],"input":[{"content":"You are a helpful assistant","role":"developer"},{"content":[{"text":"What''s the weather in Florence, Italy?","type":"input_text"}],"role":"user"},{"arguments":"\"{\\\"location\\\":\\\"Florence, Italy\\\"}\"","call_id":"call_1z70BexeUjpiAcqPgwQiiVRh","name":"weather","type":"function_call"},{"call_id":"call_1z70BexeUjpiAcqPgwQiiVRh","output":"40 C","type":"function_call_output"}],"model":"gpt-5","reasoning":{"effort":"high","summary":"auto"},"tool_choice":"auto","tools":[{"strict":false,"parameters":{"properties":{"location":{"description":"the city","type":"string"}},"required":["location"],"type":"object"},"name":"weather","description":"Get weather information for a location","type":"function"}]}' headers: Accept: - application/json diff --git a/providertests/testdata/TestOpenAIResponsesWithSummaryThinking/openai-o4-mini/thinking-streaming.yaml b/providertests/testdata/TestOpenAIResponsesWithSummaryThinking/openai-o4-mini/thinking-streaming.yaml index 6ff391e97..2a3161f27 100644 --- a/providertests/testdata/TestOpenAIResponsesWithSummaryThinking/openai-o4-mini/thinking-streaming.yaml +++ b/providertests/testdata/TestOpenAIResponsesWithSummaryThinking/openai-o4-mini/thinking-streaming.yaml @@ -83,9 +83,9 @@ interactions: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 1764 + content_length: 802 host: "" - body: '{"store":false,"include":["reasoning.encrypted_content"],"input":[{"content":"You are a helpful assistant","role":"developer"},{"content":[{"text":"What''s the weather in Florence, Italy?","type":"input_text"}],"role":"user"},{"id":"rs_0f5967458d374a400169b3198b5c3c81929593ebae4f73c5a1","summary":[],"encrypted_content":"gAAAAABpsxmLfnaOVal-y1QoHC_f-xrEAHfOq_qqSPrfAeXEWaMIRDyeacLClp6l-Vn-sNI-IuHFPaXqQ-ouC_xjew1q9ZitVbGtiDoECwwahI7c8YMTsT7HO-tinnZ-q5XvaKzf9LdE-xJjN5_PQijx-EquAQWmJjD7v0FXxY7YMhsek1xazkBcHHDNRme7_Khg-PTGmEqbKhFpnZe7fRd9_yP96iiVv4sE9wOz7jLzFP9-0Tjd6nb3zQyhFf9FD0ybIfskuaTOcWh194gVuedscufgzUxngb8Ana5RtYCt8kPrq6qczQ1V9A04DLzy_my-Ldj1Wx5BL-ttfi3xSONVQ4w_3e8CJPllUx0lX3ptyE3aobZYBmKJpaYcy8Mxv05jty-7eWujTVwn2a5WESDG8GvftqElgQ5Kk-2yyjCd_okXg4xeMGPsPINn7wtJTS1KQZr1IvS5dlI6sOvX74ImepjHAxJ2Fzo2Jbw-NYccd3sq8YBdKbtFo8ACByATsHL9_nMISTSsX0QwPzpoavFcj84O5d4-c38_SSjzC_iS9r2kZpOnwfhuVCRIsBpVETxWHMrCCsFwcvhh9uUX7OQ9kY3zLplvlNwLq45JcXgB2oaD3yayLv9SCffO4DutwTv8Aaxptj60fSUiE-33N1QlWjn3j8h-B5KjQWX7YoTy-iFDqmuG4I8S6x0whxP4pC3wPXAduVS-hsuYX25j-OKKJgtQwrWfuNd5s662lddwrLrCtYsocuA2kAgqrihUewYcYLARsdJuVud52nenSWov7F6UXHN8sHIgB9q9J16chB2WH_gOWnhsUtdigDR78hjpafKLlQKJ","type":"reasoning"},{"arguments":"\"{\\\"location\\\":\\\"Florence, Italy\\\"}\"","call_id":"call_OC2f35aKdph1vnwdji5XnSu3","name":"weather","type":"function_call"},{"call_id":"call_OC2f35aKdph1vnwdji5XnSu3","output":"40 C","type":"function_call_output"}],"model":"o4-mini","reasoning":{"effort":"high","summary":"auto"},"tool_choice":"auto","tools":[{"strict":false,"parameters":{"properties":{"location":{"description":"the city","type":"string"}},"required":["location"],"type":"object"},"name":"weather","description":"Get weather information for a location","type":"function"}],"stream":true}' + body: '{"store":false,"include":["reasoning.encrypted_content"],"input":[{"content":"You are a helpful assistant","role":"developer"},{"content":[{"text":"What''s the weather in Florence, Italy?","type":"input_text"}],"role":"user"},{"arguments":"\"{\\\"location\\\":\\\"Florence, Italy\\\"}\"","call_id":"call_OC2f35aKdph1vnwdji5XnSu3","name":"weather","type":"function_call"},{"call_id":"call_OC2f35aKdph1vnwdji5XnSu3","output":"40 C","type":"function_call_output"}],"model":"o4-mini","reasoning":{"effort":"high","summary":"auto"},"tool_choice":"auto","tools":[{"strict":false,"parameters":{"properties":{"location":{"description":"the city","type":"string"}},"required":["location"],"type":"object"},"name":"weather","description":"Get weather information for a location","type":"function"}],"stream":true}' headers: Accept: - application/json diff --git a/providertests/testdata/TestOpenAIResponsesWithSummaryThinking/openai-o4-mini/thinking.yaml b/providertests/testdata/TestOpenAIResponsesWithSummaryThinking/openai-o4-mini/thinking.yaml index f165a8594..00eba4196 100644 --- a/providertests/testdata/TestOpenAIResponsesWithSummaryThinking/openai-o4-mini/thinking.yaml +++ b/providertests/testdata/TestOpenAIResponsesWithSummaryThinking/openai-o4-mini/thinking.yaml @@ -126,9 +126,9 @@ interactions: proto: HTTP/1.1 proto_major: 1 proto_minor: 1 - content_length: 2383 + content_length: 788 host: "" - body: '{"store":false,"include":["reasoning.encrypted_content"],"input":[{"content":"You are a helpful assistant","role":"developer"},{"content":[{"text":"What''s the weather in Florence, Italy?","type":"input_text"}],"role":"user"},{"id":"rs_07143e0d7cf2d4a90169b319885cb88195a1bb7a952052db4d","summary":[{"text":"","type":"summary_text"}],"encrypted_content":"gAAAAABpsxmJpUPkUo9BjVWMoB5IqOc2z9YhXBk9pRPu4S82H045OpdoDwncDJLq9E2oM9wg0lxTAC8V6qS7iTVcsO4UEgnN9jf08KJ1wSkb24kZdqNdmH3t_DehH6IEFsu5SkcnVbS9YgUA8dzt72y3W3IwtmbkVTCG-LEs4feMXasSu8WMx0KHtrX6eMc-Q3yKeoJab5b3YHb7Qu2mRjH5MSOHq2QzUV2LK5FRM9jnEkLqDf3WmdhgsR66d05XBREni-DY8-xn8LgvoEJERdtdeVIUALW2mKVVfrNO9NuA91-J6rpe4beppxwFxocvcotju6CZ3ommhjgRuaqQ7wu7TlyiJLCPlR-QjO4warTnLHWYTg2tMCNlxFOrL9nrd5ee1DxjcCdLrgDuvOFg69J4ySm3ToxIe48x8GyX9-Wp5OJPV8vOj8x2jeoR8wdzO7r-FKt5hxkTPsfqxHJ0_R3LPW5PkvvRjpx7LNgKnegVOoqCmvwrkKAje1pOimebBWc0-V2LqC4OWFNGH8RZ6Kobc91aQfWya1PjoKe10Ou8S5IgwJBNnZEtZoXzYpMeJi22awSPiZW4kGItJYLm3RufuuZcfLPdRfrRa4wJDo_Tq4FadTcZxgsM6q5vqIlRNbJ8pGm4vumUljmpI_ApG3275lPq5lknYbZCR59OMsI_nPx2TR6syRxqxbc_g1szaLSUNfUnwtvel6jIVaRH8Rq4USszB_P8Ksd28RCUVGNiDrAJz3AYZq7OSXsoKFJsCoia2qY2Ck5w8xDq1T1qp_wmADLqbfzLjqpdH54gDF5NUIphiWj6lPZG6w_xdoEKdam0whGz1z0SL3_eXsy_UDPV89pv8RUrlBMAogUvDirUAhNXzMHQg_XEj6TpxBE9JSiaHObZzX62GIjLCP162Bj1f0KbHnnsOcLtKFeKNoJU0qaqihpA2uNHsGCi_i2MIvkk_WKQyfJfMo_Jysv5s7A0nITsyZod9imwNEwzXeTWffdUxQATJkuYeAZYVwF6iQ4obxWj4gsIQjjWeMXHAWHewQMWYMk0aaL8iqC8_rOQooAfU3tvyDdpmIJ-rNuoL6DPdCgp2974VCGkUzAvatFQBs5Z_IcFJzv1YhDwuojmk3H4j0JruDCzfELVAAZJQpfc_UZcpN05bFGNy9u0xBvbjqXjn3sIjC95RMwV0oGil8oDkAGrVOCtDg2_sag7gysWX8p42DpLO3BrErOhv2prFh15le2Q6nyNT29d1u6KGRMttA56VQtlhto5OxSLdrC6owHlIDnEA8A4NG7ba9XLTn3hLVFqiDhwCCpEo01nxqF6hJ2SqXPxAxkLnPcBXNIWdeIKrPqWl2fGITO5UTXwCmyg82hGoWb5khd754FmQj4ywnW24h96QADS01RXQ3J3w2oOGwuUjx9BvFPsvtVS6kJXI2izzA==","type":"reasoning"},{"arguments":"\"{\\\"location\\\":\\\"Florence, Italy\\\"}\"","call_id":"call_ZgWM9rbt3Yxv1cu7zAUUqGID","name":"weather","type":"function_call"},{"call_id":"call_ZgWM9rbt3Yxv1cu7zAUUqGID","output":"40 C","type":"function_call_output"}],"model":"o4-mini","reasoning":{"effort":"high","summary":"auto"},"tool_choice":"auto","tools":[{"strict":false,"parameters":{"properties":{"location":{"description":"the city","type":"string"}},"required":["location"],"type":"object"},"name":"weather","description":"Get weather information for a location","type":"function"}]}' + body: '{"store":false,"include":["reasoning.encrypted_content"],"input":[{"content":"You are a helpful assistant","role":"developer"},{"content":[{"text":"What''s the weather in Florence, Italy?","type":"input_text"}],"role":"user"},{"arguments":"\"{\\\"location\\\":\\\"Florence, Italy\\\"}\"","call_id":"call_ZgWM9rbt3Yxv1cu7zAUUqGID","name":"weather","type":"function_call"},{"call_id":"call_ZgWM9rbt3Yxv1cu7zAUUqGID","output":"40 C","type":"function_call_output"}],"model":"o4-mini","reasoning":{"effort":"high","summary":"auto"},"tool_choice":"auto","tools":[{"strict":false,"parameters":{"properties":{"location":{"description":"the city","type":"string"}},"required":["location"],"type":"object"},"name":"weather","description":"Get weather information for a location","type":"function"}]}' headers: Accept: - application/json From e154e40d2ce391edff2af20c1d04a173f717aadf Mon Sep 17 00:00:00 2001 From: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Date: Fri, 20 Mar 2026 23:43:48 +0000 Subject: [PATCH 6/6] fix(providers/openai): emit item_reference for reasoning items when store=true --- providers/openai/openai_test.go | 88 ++++++++++++++++++-- providers/openai/responses_language_model.go | 24 ++++-- 2 files changed, 100 insertions(+), 12 deletions(-) diff --git a/providers/openai/openai_test.go b/providers/openai/openai_test.go index 095623557..e0bf1d143 100644 --- a/providers/openai/openai_test.go +++ b/providers/openai/openai_test.go @@ -3938,20 +3938,22 @@ func TestResponsesToPrompt_ReasoningWithStore(t *testing.T) { }, } - t.Run("store true skips reasoning", func(t *testing.T) { + t.Run("store true replays reasoning as item reference", func(t *testing.T) { t.Parallel() input, warnings := toResponsesPrompt(prompt, "system", true) require.Empty(t, warnings) - // With store=true: user, assistant text (reasoning - // skipped), follow-up user. - require.Len(t, input, 3) + // With store=true: user, reasoning item_reference, + // assistant text, follow-up user. + require.Len(t, input, 4) + require.NotNil(t, input[1].OfItemReference) + require.Equal(t, reasoningItemID, input[1].OfItemReference.ID) - // Verify no reasoning item leaked through. + // Verify reasoning is replayed only as an item reference. for _, item := range input { require.Nil(t, item.OfReasoning, - "reasoning items must not appear when store=true") + "reasoning items must not appear inline when store=true") } }) @@ -3967,6 +3969,80 @@ func TestResponsesToPrompt_ReasoningWithStore(t *testing.T) { for _, item := range input { require.Nil(t, item.OfReasoning, "reasoning items must not appear when store=false") + require.Nil(t, item.OfItemReference, + "item references must not appear when store=false") + } + }) +} + +func TestResponsesToPrompt_ReasoningWithWebSearchCombined(t *testing.T) { + t.Parallel() + + prompt := fantasy.Prompt{ + { + Role: fantasy.MessageRoleUser, + Content: []fantasy.MessagePart{ + fantasy.TextPart{Text: "Search for the latest AI news"}, + }, + }, + { + Role: fantasy.MessageRoleAssistant, + Content: []fantasy.MessagePart{ + fantasy.ReasoningPart{ + Text: "Searching for AI news", + ProviderOptions: fantasy.ProviderOptions{ + Name: &ResponsesReasoningMetadata{ + ItemID: "rs_01", + Summary: []string{"Searching for AI news"}, + }, + }, + }, + fantasy.ToolCallPart{ + ToolCallID: "ws_01", + ToolName: "web_search", + ProviderExecuted: true, + }, + fantasy.ToolResultPart{ + ToolCallID: "ws_01", + ProviderExecuted: true, + }, + fantasy.TextPart{Text: "Here is what I found about AI news."}, + }, + }, + } + + t.Run("store true replays reasoning and web search item references", func(t *testing.T) { + t.Parallel() + + input, warnings := toResponsesPrompt(prompt, "system instructions", true) + + require.Empty(t, warnings) + require.Len(t, input, 4, + "expected user + reasoning item_reference + web_search item_reference + assistant text when store=true") + require.NotNil(t, input[1].OfItemReference) + require.Equal(t, "rs_01", input[1].OfItemReference.ID) + require.NotNil(t, input[2].OfItemReference) + require.Equal(t, "ws_01", input[2].OfItemReference.ID) + require.Nil(t, input[3].OfItemReference) + for _, item := range input { + require.Nil(t, item.OfReasoning, + "reasoning items must not appear inline when store=true") + } + }) + + t.Run("store false skips provider-executed items", func(t *testing.T) { + t.Parallel() + + input, warnings := toResponsesPrompt(prompt, "system instructions", false) + + require.Empty(t, warnings) + require.Len(t, input, 2, + "expected only user + assistant text when store=false") + for _, item := range input { + require.Nil(t, item.OfItemReference, + "item references must not appear when store=false") + require.Nil(t, item.OfReasoning, + "reasoning items must not appear inline when store=false") } }) } diff --git a/providers/openai/responses_language_model.go b/providers/openai/responses_language_model.go index 8d4c15aec..92fad75da 100644 --- a/providers/openai/responses_language_model.go +++ b/providers/openai/responses_language_model.go @@ -567,12 +567,24 @@ func toResponsesPrompt(prompt fantasy.Prompt, systemMessageMode string, store bo // recognised Responses API input type; skip. continue case fantasy.ContentTypeReasoning: - // Reasoning items are always skipped during replay. - // When store is enabled, the API already has them - // persisted server-side. When store is disabled, the - // item IDs are ephemeral and referencing them causes - // "Item not found" errors. In both cases, replaying - // reasoning inline is not supported by the API. + if store { + // When store is enabled the reasoning item is + // persisted server-side. Emit an item_reference + // so the API correctly pairs it with any + // subsequent provider-executed tool calls (e.g. + // web_search_call) that were in the same step. + // Without this reference the API rejects a + // replayed web_search_call with "reasoning was + // provided without its required following item". + reasoningMeta := GetReasoningMetadata(c.Options()) + if reasoningMeta != nil && reasoningMeta.ItemID != "" { + input = append(input, responses.ResponseInputItemParamOfItemReference(reasoningMeta.ItemID)) + } + continue + } + // When store is disabled, server-side items are + // ephemeral and cannot be referenced. Skip the + // reasoning item entirely. continue } }