test: migrate tests to `charm.land/x/vcr` (#72)

Andrey Nering created

https://github.com/charmbracelet/x/pull/643

Change summary

go.mod                                 |   5 
go.sum                                 |   6 +
providertests/anthropic_test.go        |   4 
providertests/azure_responses_test.go  |   4 
providertests/azure_test.go            |   8 +-
providertests/bedrock_test.go          |  10 +-
providertests/common_test.go           |  20 ++--
providertests/google_test.go           |   6 
providertests/image_upload_test.go     |  12 +-
providertests/object_test.go           |  11 +-
providertests/openai_responses_test.go |   4 
providertests/openai_test.go           |   4 
providertests/openaicompat_test.go     |  16 ++--
providertests/openrouter_test.go       |   4 
providertests/recorder_test.go         | 104 ----------------------------
15 files changed, 59 insertions(+), 159 deletions(-)

Detailed changes

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
 )

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=

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}),

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)")),

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)")),

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")),

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")

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}),

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)

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")

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}),

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}),

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}),

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}),

providertests/recorder_test.go 🔗

@@ -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
-}