diff --git a/go.mod b/go.mod index 0148b9e9e8bc6e4cb6baf85bc338675da5c36ba4..95af7849ecab4aa12f7ee093da282deb0b3782de 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module charm.land/fantasy go 1.25 require ( + charm.land/x/vcr v0.1.0 cloud.google.com/go/auth v0.17.0 github.com/RealAlexandreAI/json-repair v0.0.14 github.com/aws/aws-sdk-go-v2 v1.39.6 @@ -16,10 +17,8 @@ require ( github.com/kaptinlin/jsonschema v0.5.2 github.com/openai/openai-go/v2 v2.7.1 github.com/stretchr/testify v1.11.1 - go.yaml.in/yaml/v4 v4.0.0-rc.3 golang.org/x/oauth2 v0.33.0 google.golang.org/genai v1.34.0 - gopkg.in/dnaeon/go-vcr.v4 v4.0.6-0.20250923044825-7b4892dd3117 ) require ( @@ -64,6 +63,7 @@ require ( go.opentelemetry.io/otel v1.36.0 // indirect go.opentelemetry.io/otel/metric v1.36.0 // indirect go.opentelemetry.io/otel/trace v1.36.0 // indirect + go.yaml.in/yaml/v4 v4.0.0-rc.3 // indirect golang.org/x/crypto v0.41.0 // indirect golang.org/x/net v0.43.0 // indirect golang.org/x/sync v0.17.0 // indirect @@ -74,5 +74,6 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect google.golang.org/grpc v1.74.2 // indirect google.golang.org/protobuf v1.36.7 // 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 566d18a5d83c2b118cb73b4dccb5fe699ab5011b..291a585bae7b36a4911dce0421225565d48e0630 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +charm.land/x/vcr v0.1.0 h1:XhCUVij6Ss6+xJuAb2n4mNRGSS/SrnNoUmEwJziy+Dg= +charm.land/x/vcr v0.1.0/go.mod h1:eByq2gqzWvcct/8XE2XO5KznoWEBiXH56+y2gphbltM= cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= @@ -162,7 +164,7 @@ google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/dnaeon/go-vcr.v4 v4.0.6-0.20250923044825-7b4892dd3117 h1:fbE/sTnBb9UNfE8cJsOzrYYPqVWVHb7jWH4SI1W//cM= -gopkg.in/dnaeon/go-vcr.v4 v4.0.6-0.20250923044825-7b4892dd3117/go.mod h1:YuVT9NPq7t3oT2WpUemB0DbNL7djIjgajZycxoDLnqs= +gopkg.in/dnaeon/go-vcr.v4 v4.0.6-0.20251110073552-01de4eb40290 h1:g3ah7zaWmw41EtOgBNXpx8zk4HYuH3OMwB+qh1Dt834= +gopkg.in/dnaeon/go-vcr.v4 v4.0.6-0.20251110073552-01de4eb40290/go.mod h1:sbq5oMEcM4PXngbcNbHhzfCP9OdZodLhrbRYoyg09HY= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/providertests/anthropic_test.go b/providertests/anthropic_test.go index 8cd98a7c70068cc7973bed41ccd28af140a5c35f..3987ea03f5ed9d728d0530091dc0a89451272c95 100644 --- a/providertests/anthropic_test.go +++ b/providertests/anthropic_test.go @@ -8,8 +8,8 @@ import ( "charm.land/fantasy" "charm.land/fantasy/providers/anthropic" + "charm.land/x/vcr" "github.com/stretchr/testify/require" - "gopkg.in/dnaeon/go-vcr.v4/pkg/recorder" ) var anthropicTestModels = []testModel{ @@ -144,7 +144,7 @@ func testAnthropicThinking(t *testing.T, result *fantasy.AgentResult) { } func anthropicBuilder(model string) builderFunc { - return func(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) { + return func(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) { provider, err := anthropic.New( anthropic.WithAPIKey(os.Getenv("FANTASY_ANTHROPIC_API_KEY")), anthropic.WithHTTPClient(&http.Client{Transport: r}), diff --git a/providertests/azure_responses_test.go b/providertests/azure_responses_test.go index 150426ee269d61cfa5b11c3c63992cf013d16632..8b3945ba4c5e702ff55ebc5608645150e23d61db 100644 --- a/providertests/azure_responses_test.go +++ b/providertests/azure_responses_test.go @@ -9,8 +9,8 @@ import ( "charm.land/fantasy" "charm.land/fantasy/providers/azure" "charm.land/fantasy/providers/openai" + "charm.land/x/vcr" "github.com/stretchr/testify/require" - "gopkg.in/dnaeon/go-vcr.v4/pkg/recorder" ) func TestAzureResponsesCommon(t *testing.T) { @@ -26,7 +26,7 @@ func TestAzureResponsesCommon(t *testing.T) { } func azureReasoningBuilder(model string) builderFunc { - return func(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) { + return func(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) { provider, err := azure.New( azure.WithBaseURL(cmp.Or(os.Getenv("FANTASY_AZURE_BASE_URL"), defaultBaseURL)), azure.WithAPIKey(cmp.Or(os.Getenv("FANTASY_AZURE_API_KEY"), "(missing)")), diff --git a/providertests/azure_test.go b/providertests/azure_test.go index 89903b72467b9af0626b472c08fed356d3481095..198b8dc1c22ff74ac38acf06a46ca034f58b7528 100644 --- a/providertests/azure_test.go +++ b/providertests/azure_test.go @@ -9,8 +9,8 @@ import ( "charm.land/fantasy" "charm.land/fantasy/providers/azure" "charm.land/fantasy/providers/openai" + "charm.land/x/vcr" "github.com/stretchr/testify/require" - "gopkg.in/dnaeon/go-vcr.v4/pkg/recorder" ) const defaultBaseURL = "https://fantasy-playground-resource.openai.azure.com" @@ -39,7 +39,7 @@ func testAzureThinking(t *testing.T, result *fantasy.AgentResult) { require.Greater(t, result.Response.Usage.ReasoningTokens, int64(0), "expected reasoning tokens, got none") } -func builderAzureO4Mini(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) { +func builderAzureO4Mini(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) { provider, err := azure.New( azure.WithBaseURL(cmp.Or(os.Getenv("FANTASY_AZURE_BASE_URL"), defaultBaseURL)), azure.WithAPIKey(cmp.Or(os.Getenv("FANTASY_AZURE_API_KEY"), "(missing)")), @@ -51,7 +51,7 @@ func builderAzureO4Mini(t *testing.T, r *recorder.Recorder) (fantasy.LanguageMod return provider.LanguageModel(t.Context(), "o4-mini") } -func builderAzureGpt5Mini(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) { +func builderAzureGpt5Mini(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) { provider, err := azure.New( azure.WithBaseURL(cmp.Or(os.Getenv("FANTASY_AZURE_BASE_URL"), defaultBaseURL)), azure.WithAPIKey(cmp.Or(os.Getenv("FANTASY_AZURE_API_KEY"), "(missing)")), @@ -63,7 +63,7 @@ func builderAzureGpt5Mini(t *testing.T, r *recorder.Recorder) (fantasy.LanguageM return provider.LanguageModel(t.Context(), "gpt-5-mini") } -func builderAzureGrok3Mini(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) { +func builderAzureGrok3Mini(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) { provider, err := azure.New( azure.WithBaseURL(cmp.Or(os.Getenv("FANTASY_AZURE_BASE_URL"), defaultBaseURL)), azure.WithAPIKey(cmp.Or(os.Getenv("FANTASY_AZURE_API_KEY"), "(missing)")), diff --git a/providertests/bedrock_test.go b/providertests/bedrock_test.go index e377f3568d393a30b53d13604a8dcf1c9970d322..da591ade1a9b9e17317d261490b5bb0e108a04d5 100644 --- a/providertests/bedrock_test.go +++ b/providertests/bedrock_test.go @@ -7,7 +7,7 @@ import ( "charm.land/fantasy" "charm.land/fantasy/providers/bedrock" - "gopkg.in/dnaeon/go-vcr.v4/pkg/recorder" + "charm.land/x/vcr" ) func TestBedrockCommon(t *testing.T) { @@ -22,7 +22,7 @@ func TestBedrockBasicAuth(t *testing.T) { testSimple(t, builderPair{"bedrock-anthropic-claude-3-sonnet", buildersBedrockBasicAuth, nil, nil}) } -func builderBedrockClaude3Sonnet(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) { +func builderBedrockClaude3Sonnet(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) { provider, err := bedrock.New( bedrock.WithHTTPClient(&http.Client{Transport: r}), bedrock.WithSkipAuth(!r.IsRecording()), @@ -33,7 +33,7 @@ func builderBedrockClaude3Sonnet(t *testing.T, r *recorder.Recorder) (fantasy.La return provider.LanguageModel(t.Context(), "anthropic.claude-3-sonnet-20240229-v1:0") } -func builderBedrockClaude3Opus(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) { +func builderBedrockClaude3Opus(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) { provider, err := bedrock.New( bedrock.WithHTTPClient(&http.Client{Transport: r}), bedrock.WithSkipAuth(!r.IsRecording()), @@ -44,7 +44,7 @@ func builderBedrockClaude3Opus(t *testing.T, r *recorder.Recorder) (fantasy.Lang return provider.LanguageModel(t.Context(), "anthropic.claude-3-opus-20240229-v1:0") } -func builderBedrockClaude3Haiku(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) { +func builderBedrockClaude3Haiku(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) { provider, err := bedrock.New( bedrock.WithHTTPClient(&http.Client{Transport: r}), bedrock.WithSkipAuth(!r.IsRecording()), @@ -55,7 +55,7 @@ func builderBedrockClaude3Haiku(t *testing.T, r *recorder.Recorder) (fantasy.Lan return provider.LanguageModel(t.Context(), "anthropic.claude-3-haiku-20240307-v1:0") } -func buildersBedrockBasicAuth(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) { +func buildersBedrockBasicAuth(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) { provider, err := bedrock.New( bedrock.WithHTTPClient(&http.Client{Transport: r}), bedrock.WithAPIKey(os.Getenv("FANTASY_BEDROCK_API_KEY")), diff --git a/providertests/common_test.go b/providertests/common_test.go index 05df5a4a31d3b77e859f11316132dfc09920b85f..e5f9c1c0e29b19ecd3cd8ac8a4e1f61e5182fba4 100644 --- a/providertests/common_test.go +++ b/providertests/common_test.go @@ -8,9 +8,9 @@ import ( "testing" "charm.land/fantasy" + "charm.land/x/vcr" "github.com/joho/godotenv" "github.com/stretchr/testify/require" - "gopkg.in/dnaeon/go-vcr.v4/pkg/recorder" ) func init() { @@ -27,7 +27,7 @@ type testModel struct { reasoning bool } -type builderFunc func(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) +type builderFunc func(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) type builderPair struct { name string @@ -54,7 +54,7 @@ func testSimple(t *testing.T, pair builderPair) { } t.Run("simple", func(t *testing.T) { - r := newRecorder(t) + r := vcr.NewRecorder(t) languageModel, err := pair.builder(t, r) require.NoError(t, err, "failed to build language model") @@ -73,7 +73,7 @@ func testSimple(t *testing.T, pair builderPair) { checkResult(t, result) }) t.Run("simple streaming", func(t *testing.T) { - r := newRecorder(t) + r := vcr.NewRecorder(t) languageModel, err := pair.builder(t, r) require.NoError(t, err, "failed to build language model") @@ -127,7 +127,7 @@ func testTool(t *testing.T, pair builderPair) { } t.Run("tool", func(t *testing.T) { - r := newRecorder(t) + r := vcr.NewRecorder(t) languageModel, err := pair.builder(t, r) require.NoError(t, err, "failed to build language model") @@ -147,7 +147,7 @@ func testTool(t *testing.T, pair builderPair) { checkResult(t, result) }) t.Run("tool streaming", func(t *testing.T) { - r := newRecorder(t) + r := vcr.NewRecorder(t) languageModel, err := pair.builder(t, r) require.NoError(t, err, "failed to build language model") @@ -227,7 +227,7 @@ func testMultiTool(t *testing.T, pair builderPair) { } t.Run("multi tool", func(t *testing.T) { - r := newRecorder(t) + r := vcr.NewRecorder(t) languageModel, err := pair.builder(t, r) require.NoError(t, err, "failed to build language model") @@ -248,7 +248,7 @@ func testMultiTool(t *testing.T, pair builderPair) { checkResult(t, result) }) t.Run("multi tool streaming", func(t *testing.T) { - r := newRecorder(t) + r := vcr.NewRecorder(t) languageModel, err := pair.builder(t, r) require.NoError(t, err, "failed to build language model") @@ -274,7 +274,7 @@ func testThinking(t *testing.T, pairs []builderPair, thinkChecks func(*testing.T for _, pair := range pairs { t.Run(pair.name, func(t *testing.T) { t.Run("thinking", func(t *testing.T) { - r := newRecorder(t) + r := vcr.NewRecorder(t) languageModel, err := pair.builder(t, r) require.NoError(t, err, "failed to build language model") @@ -311,7 +311,7 @@ func testThinking(t *testing.T, pairs []builderPair, thinkChecks func(*testing.T thinkChecks(t, result) }) t.Run("thinking-streaming", func(t *testing.T) { - r := newRecorder(t) + r := vcr.NewRecorder(t) languageModel, err := pair.builder(t, r) require.NoError(t, err, "failed to build language model") diff --git a/providertests/google_test.go b/providertests/google_test.go index a3ed753612fc1411e1f2f30d28e28433ee5c60fe..8f6b1d3c5106e0d099ac4b3c41302446a002a626 100644 --- a/providertests/google_test.go +++ b/providertests/google_test.go @@ -9,8 +9,8 @@ import ( "charm.land/fantasy" "charm.land/fantasy/providers/google" + "charm.land/x/vcr" "github.com/stretchr/testify/require" - "gopkg.in/dnaeon/go-vcr.v4/pkg/recorder" ) var geminiTestModels = []testModel{ @@ -95,7 +95,7 @@ func generateIDMock() google.ToolCallIDFunc { } func geminiBuilder(model string) builderFunc { - return func(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) { + return func(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) { provider, err := google.New( google.WithGeminiAPIKey(cmp.Or(os.Getenv("FANTASY_GEMINI_API_KEY"), "(missing)")), google.WithHTTPClient(&http.Client{Transport: r}), @@ -109,7 +109,7 @@ func geminiBuilder(model string) builderFunc { } func vertexBuilder(model string) builderFunc { - return func(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) { + return func(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) { provider, err := google.New( google.WithVertex(os.Getenv("FANTASY_VERTEX_PROJECT"), os.Getenv("FANTASY_VERTEX_LOCATION")), google.WithHTTPClient(&http.Client{Transport: r}), diff --git a/providertests/image_upload_test.go b/providertests/image_upload_test.go index 15abc1eef7bc311a64cc329c80ebb9813ad7be5a..cf7fad7cace821f9f8a2e8a5d9badbf1062160df 100644 --- a/providertests/image_upload_test.go +++ b/providertests/image_upload_test.go @@ -10,12 +10,12 @@ import ( "charm.land/fantasy/providers/anthropic" "charm.land/fantasy/providers/google" "charm.land/fantasy/providers/openai" + "charm.land/x/vcr" "github.com/stretchr/testify/require" - "gopkg.in/dnaeon/go-vcr.v4/pkg/recorder" ) func anthropicImageBuilder(model string) builderFunc { - return func(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) { + return func(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) { provider, err := anthropic.New( anthropic.WithAPIKey(cmp.Or(os.Getenv("FANTASY_ANTHROPIC_API_KEY"), "(missing)")), anthropic.WithHTTPClient(&http.Client{Transport: r}), @@ -28,7 +28,7 @@ func anthropicImageBuilder(model string) builderFunc { } func openAIImageBuilder(model string) builderFunc { - return func(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) { + return func(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) { provider, err := openai.New( openai.WithAPIKey(cmp.Or(os.Getenv("FANTASY_OPENAI_API_KEY"), "(missing)")), openai.WithHTTPClient(&http.Client{Transport: r}), @@ -41,7 +41,7 @@ func openAIImageBuilder(model string) builderFunc { } func geminiImageBuilder(model string) builderFunc { - return func(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) { + return func(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) { provider, err := google.New( google.WithGeminiAPIKey(cmp.Or(os.Getenv("FANTASY_GEMINI_API_KEY"), "(missing)")), google.WithHTTPClient(&http.Client{Transport: r}), @@ -76,7 +76,7 @@ func TestImageUploadAgent(t *testing.T) { for _, pair := range pairs { t.Run(pair.name, func(t *testing.T) { - r := newRecorder(t) + r := vcr.NewRecorder(t) lm, err := pair.builder(t, r) require.NoError(t, err) @@ -122,7 +122,7 @@ func TestImageUploadAgentStreaming(t *testing.T) { for _, pair := range pairs { t.Run(pair.name+"-stream", func(t *testing.T) { - r := newRecorder(t) + r := vcr.NewRecorder(t) lm, err := pair.builder(t, r) require.NoError(t, err) diff --git a/providertests/object_test.go b/providertests/object_test.go index b23986ee7aa547556ca7dda447a61bfc8617f9dd..9c91e33187799da35259915bd93a596abc23688a 100644 --- a/providertests/object_test.go +++ b/providertests/object_test.go @@ -6,6 +6,7 @@ import ( "testing" "charm.land/fantasy" + "charm.land/x/vcr" "github.com/stretchr/testify/require" ) @@ -90,7 +91,7 @@ func testSimpleObject(t *testing.T, pair builderPair) { } t.Run("simple object", func(t *testing.T) { - r := newRecorder(t) + r := vcr.NewRecorder(t) languageModel, err := pair.builder(t, r) require.NoError(t, err, "failed to build language model") @@ -113,7 +114,7 @@ func testSimpleObject(t *testing.T, pair builderPair) { }) t.Run("simple object streaming", func(t *testing.T) { - r := newRecorder(t) + r := vcr.NewRecorder(t) languageModel, err := pair.builder(t, r) require.NoError(t, err, "failed to build language model") @@ -263,7 +264,7 @@ func testComplexObject(t *testing.T, pair builderPair) { } t.Run("complex object", func(t *testing.T) { - r := newRecorder(t) + r := vcr.NewRecorder(t) languageModel, err := pair.builder(t, r) require.NoError(t, err, "failed to build language model") @@ -286,7 +287,7 @@ func testComplexObject(t *testing.T, pair builderPair) { }) t.Run("complex object streaming", func(t *testing.T) { - r := newRecorder(t) + r := vcr.NewRecorder(t) languageModel, err := pair.builder(t, r) require.NoError(t, err, "failed to build language model") @@ -375,7 +376,7 @@ func testObjectWithRepair(t *testing.T, pairs []builderPair) { for _, pair := range pairs { t.Run(pair.name, func(t *testing.T) { t.Run("object with repair", func(t *testing.T) { - r := newRecorder(t) + r := vcr.NewRecorder(t) languageModel, err := pair.builder(t, r) require.NoError(t, err, "failed to build language model") diff --git a/providertests/openai_responses_test.go b/providertests/openai_responses_test.go index d036fe607908c7082641026c969579494859000b..db85f73d4963c893dc7e884773d07dd5ceb85454 100644 --- a/providertests/openai_responses_test.go +++ b/providertests/openai_responses_test.go @@ -7,8 +7,8 @@ import ( "charm.land/fantasy" "charm.land/fantasy/providers/openai" + "charm.land/x/vcr" "github.com/stretchr/testify/require" - "gopkg.in/dnaeon/go-vcr.v4/pkg/recorder" ) func TestOpenAIResponsesCommon(t *testing.T) { @@ -20,7 +20,7 @@ func TestOpenAIResponsesCommon(t *testing.T) { } func openAIReasoningBuilder(model string) builderFunc { - return func(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) { + return func(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) { provider, err := openai.New( openai.WithAPIKey(os.Getenv("FANTASY_OPENAI_API_KEY")), openai.WithHTTPClient(&http.Client{Transport: r}), diff --git a/providertests/openai_test.go b/providertests/openai_test.go index 82a8d77e83d9af6c5b8540e763f65aec688c7351..e232ed99926a708d052ce964cca573e55ecc56ff 100644 --- a/providertests/openai_test.go +++ b/providertests/openai_test.go @@ -7,7 +7,7 @@ import ( "charm.land/fantasy" "charm.land/fantasy/providers/openai" - "gopkg.in/dnaeon/go-vcr.v4/pkg/recorder" + "charm.land/x/vcr" ) var openaiTestModels = []testModel{ @@ -34,7 +34,7 @@ func TestOpenAIObjectGeneration(t *testing.T) { } func openAIBuilder(model string) builderFunc { - return func(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) { + return func(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) { provider, err := openai.New( openai.WithAPIKey(os.Getenv("FANTASY_OPENAI_API_KEY")), openai.WithHTTPClient(&http.Client{Transport: r}), diff --git a/providertests/openaicompat_test.go b/providertests/openaicompat_test.go index e7191d724582c08fc0940d56a856d339a5310a46..c19891345cb26b8d7ec7b762d41e151b41aefa29 100644 --- a/providertests/openaicompat_test.go +++ b/providertests/openaicompat_test.go @@ -8,8 +8,8 @@ import ( "charm.land/fantasy" "charm.land/fantasy/providers/openai" "charm.land/fantasy/providers/openaicompat" + "charm.land/x/vcr" "github.com/stretchr/testify/require" - "gopkg.in/dnaeon/go-vcr.v4/pkg/recorder" ) func TestOpenAICompatibleCommon(t *testing.T) { @@ -58,7 +58,7 @@ func testOpenAICompatThinking(t *testing.T, result *fantasy.AgentResult) { require.Greater(t, reasoningContentCount, 0, "expected reasoning content, got none") } -func builderXAIGrokCodeFast(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) { +func builderXAIGrokCodeFast(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) { provider, err := openaicompat.New( openaicompat.WithBaseURL("https://api.x.ai/v1"), openaicompat.WithAPIKey(os.Getenv("FANTASY_XAI_API_KEY")), @@ -70,7 +70,7 @@ func builderXAIGrokCodeFast(t *testing.T, r *recorder.Recorder) (fantasy.Languag return provider.LanguageModel(t.Context(), "grok-code-fast-1") } -func builderXAIGrok4Fast(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) { +func builderXAIGrok4Fast(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) { provider, err := openaicompat.New( openaicompat.WithBaseURL("https://api.x.ai/v1"), openaicompat.WithAPIKey(os.Getenv("FANTASY_XAI_API_KEY")), @@ -82,7 +82,7 @@ func builderXAIGrok4Fast(t *testing.T, r *recorder.Recorder) (fantasy.LanguageMo return provider.LanguageModel(t.Context(), "grok-4-fast") } -func builderXAIGrok3Mini(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) { +func builderXAIGrok3Mini(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) { provider, err := openaicompat.New( openaicompat.WithBaseURL("https://api.x.ai/v1"), openaicompat.WithAPIKey(os.Getenv("FANTASY_XAI_API_KEY")), @@ -94,7 +94,7 @@ func builderXAIGrok3Mini(t *testing.T, r *recorder.Recorder) (fantasy.LanguageMo return provider.LanguageModel(t.Context(), "grok-3-mini") } -func builderZAIGLM45(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) { +func builderZAIGLM45(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) { provider, err := openaicompat.New( openaicompat.WithBaseURL("https://api.z.ai/api/coding/paas/v4"), openaicompat.WithAPIKey(os.Getenv("FANTASY_ZAI_API_KEY")), @@ -106,7 +106,7 @@ func builderZAIGLM45(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, return provider.LanguageModel(t.Context(), "glm-4.5") } -func builderGroq(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) { +func builderGroq(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) { provider, err := openaicompat.New( openaicompat.WithBaseURL("https://api.groq.com/openai/v1"), openaicompat.WithAPIKey(os.Getenv("FANTASY_GROQ_API_KEY")), @@ -118,7 +118,7 @@ func builderGroq(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, err return provider.LanguageModel(t.Context(), "moonshotai/kimi-k2-instruct-0905") } -func builderHuggingFace(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) { +func builderHuggingFace(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) { provider, err := openaicompat.New( openaicompat.WithBaseURL("https://router.huggingface.co/v1"), openaicompat.WithAPIKey(os.Getenv("FANTASY_HUGGINGFACE_API_KEY")), @@ -130,7 +130,7 @@ func builderHuggingFace(t *testing.T, r *recorder.Recorder) (fantasy.LanguageMod return provider.LanguageModel(t.Context(), "zai-org/GLM-4.6:cerebras") } -func builderLlamaCppGptOss(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) { +func builderLlamaCppGptOss(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) { provider, err := openaicompat.New( openaicompat.WithBaseURL("http://localhost:8080/v1"), openaicompat.WithHTTPClient(&http.Client{Transport: r}), diff --git a/providertests/openrouter_test.go b/providertests/openrouter_test.go index 026e25f09c6439e56b9041adf0955158123fad33..003e8f10cc15f9efec3a7bb54974a6f5c90fd673 100644 --- a/providertests/openrouter_test.go +++ b/providertests/openrouter_test.go @@ -8,8 +8,8 @@ import ( "charm.land/fantasy" "charm.land/fantasy/providers/anthropic" "charm.land/fantasy/providers/openrouter" + "charm.land/x/vcr" "github.com/stretchr/testify/require" - "gopkg.in/dnaeon/go-vcr.v4/pkg/recorder" ) var openrouterTestModels = []testModel{ @@ -115,7 +115,7 @@ func testOpenrouterThinking(t *testing.T, result *fantasy.AgentResult) { } func openrouterBuilder(model string) builderFunc { - return func(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) { + return func(t *testing.T, r *vcr.Recorder) (fantasy.LanguageModel, error) { provider, err := openrouter.New( openrouter.WithAPIKey(os.Getenv("FANTASY_OPENROUTER_API_KEY")), openrouter.WithHTTPClient(&http.Client{Transport: r}), diff --git a/providertests/recorder_test.go b/providertests/recorder_test.go deleted file mode 100644 index 459f6732a4e03e594707d5a8baa39e13128e0f64..0000000000000000000000000000000000000000 --- a/providertests/recorder_test.go +++ /dev/null @@ -1,104 +0,0 @@ -package providertests - -import ( - "bytes" - "encoding/json" - "io" - "net/http" - "path/filepath" - "reflect" - "strings" - "testing" - - "go.yaml.in/yaml/v4" - "gopkg.in/dnaeon/go-vcr.v4/pkg/cassette" - "gopkg.in/dnaeon/go-vcr.v4/pkg/recorder" -) - -func newRecorder(t *testing.T) *recorder.Recorder { - cassetteName := filepath.Join("testdata", t.Name()) - - r, err := recorder.New( - cassetteName, - recorder.WithMode(recorder.ModeRecordOnce), - recorder.WithMatcher(customMatcher(t)), - recorder.WithMarshalFunc(marshalFunc), - recorder.WithSkipRequestLatency(true), // disable sleep to simulate response time, makes tests faster - recorder.WithHook(hookRemoveHeaders, recorder.AfterCaptureHook), - ) - if err != nil { - t.Fatalf("recorder: failed to create recorder: %v", err) - } - - t.Cleanup(func() { - if err := r.Stop(); err != nil { - t.Errorf("recorder: failed to stop recorder: %v", err) - } - }) - - return r -} - -func customMatcher(t *testing.T) recorder.MatcherFunc { - return func(r *http.Request, i cassette.Request) bool { - if r.Body == nil || r.Body == http.NoBody { - return cassette.DefaultMatcher(r, i) - } - if r.Method != i.Method || r.URL.String() != i.URL { - return false - } - - reqBody, err := io.ReadAll(r.Body) - if err != nil { - t.Fatalf("recorder: failed to read request body") - } - r.Body.Close() - r.Body = io.NopCloser(bytes.NewBuffer(reqBody)) - - // Some providers can sometimes generate JSON requests with keys in - // a different order, which means a direct string comparison will fail. - // Falling back to deserializing the content if we don't have a match. - if string(reqBody) == i.Body { // hot path - return true - } - var content1, content2 any - if err := json.Unmarshal(reqBody, &content1); err != nil { - return false - } - if err := json.Unmarshal([]byte(i.Body), &content2); err != nil { - return false - } - return reflect.DeepEqual(content1, content2) - } -} - -func marshalFunc(in any) ([]byte, error) { - var buff bytes.Buffer - enc := yaml.NewEncoder(&buff) - enc.SetIndent(2) - enc.CompactSeqIndent() - if err := enc.Encode(in); err != nil { - return nil, err - } - return buff.Bytes(), nil -} - -var headersToKeep = map[string]struct{}{ - "accept": {}, - "content-type": {}, - "user-agent": {}, -} - -func hookRemoveHeaders(i *cassette.Interaction) error { - for k := range i.Request.Headers { - if _, ok := headersToKeep[strings.ToLower(k)]; !ok { - delete(i.Request.Headers, k) - } - } - for k := range i.Response.Headers { - if _, ok := headersToKeep[strings.ToLower(k)]; !ok { - delete(i.Response.Headers, k) - } - } - return nil -}