feat: support structured output (#66)

Kujtim Hoxha created

Change summary

agent_test.go                                                                                                  |   8 
errors.go                                                                                                      |  28 
examples/structured-outputs/go.mod                                                                             |  26 
examples/structured-outputs/go.sum                                                                             |  44 
examples/structured-outputs/main.go                                                                            |  99 
go.mod                                                                                                         |  12 
go.sum                                                                                                         |  22 
model.go                                                                                                       |   4 
object.go                                                                                                      | 233 
object/object.go                                                                                               | 616 
providers/anthropic/anthropic.go                                                                               |  37 
providers/google/google.go                                                                                     | 284 
providers/openai/language_model.go                                                                             | 301 
providers/openai/openai.go                                                                                     |  21 
providers/openai/responses_language_model.go                                                                   | 309 
providers/openaicompat/openaicompat.go                                                                         |  20 
providers/openrouter/openrouter.go                                                                             |  25 
providertests/anthropic_test.go                                                                                |   8 
providertests/google_test.go                                                                                   |  16 
providertests/object_test.go                                                                                   | 421 
providertests/openai_responses_test.go                                                                         |   8 
providertests/openai_test.go                                                                                   |   8 
providertests/openaicompat_test.go                                                                             |   8 
providertests/testdata/TestAnthropicObjectGeneration/claude-sonnet-4/complex_object.yaml                       |  33 
providertests/testdata/TestAnthropicObjectGeneration/claude-sonnet-4/complex_object_streaming.yaml             |  10 
providertests/testdata/TestAnthropicObjectGeneration/claude-sonnet-4/simple_object.yaml                        |  33 
providertests/testdata/TestAnthropicObjectGeneration/claude-sonnet-4/simple_object_streaming.yaml              |  89 
providertests/testdata/TestGoogleObjectGeneration/gemini-2.5-flash/complex_object.yaml                         |  62 
providertests/testdata/TestGoogleObjectGeneration/gemini-2.5-flash/complex_object_streaming.yaml               |  27 
providertests/testdata/TestGoogleObjectGeneration/gemini-2.5-flash/simple_object.yaml                          |  62 
providertests/testdata/TestGoogleObjectGeneration/gemini-2.5-flash/simple_object_streaming.yaml                |  34 
providertests/testdata/TestGoogleObjectGeneration/gemini-2.5-pro/complex_object.yaml                           |  62 
providertests/testdata/TestGoogleObjectGeneration/gemini-2.5-pro/complex_object_streaming.yaml                 |  27 
providertests/testdata/TestGoogleObjectGeneration/gemini-2.5-pro/simple_object.yaml                            |  62 
providertests/testdata/TestGoogleObjectGeneration/gemini-2.5-pro/simple_object_streaming.yaml                  |  34 
providertests/testdata/TestGoogleVertexObjectGeneration/vertex-claude-3-7-sonnet/complex_object.yaml           |  33 
providertests/testdata/TestGoogleVertexObjectGeneration/vertex-claude-3-7-sonnet/complex_object_streaming.yaml |  10 
providertests/testdata/TestGoogleVertexObjectGeneration/vertex-claude-3-7-sonnet/simple_object.yaml            |  33 
providertests/testdata/TestGoogleVertexObjectGeneration/vertex-claude-3-7-sonnet/simple_object_streaming.yaml  |  81 
providertests/testdata/TestGoogleVertexObjectGeneration/vertex-gemini-2-5-flash/complex_object.yaml            |  70 
providertests/testdata/TestGoogleVertexObjectGeneration/vertex-gemini-2-5-flash/complex_object_streaming.yaml  |  27 
providertests/testdata/TestGoogleVertexObjectGeneration/vertex-gemini-2-5-flash/simple_object.yaml             |  70 
providertests/testdata/TestGoogleVertexObjectGeneration/vertex-gemini-2-5-flash/simple_object_streaming.yaml   |  34 
providertests/testdata/TestGoogleVertexObjectGeneration/vertex-gemini-2-5-pro/complex_object.yaml              |  70 
providertests/testdata/TestGoogleVertexObjectGeneration/vertex-gemini-2-5-pro/complex_object_streaming.yaml    |  27 
providertests/testdata/TestGoogleVertexObjectGeneration/vertex-gemini-2-5-pro/simple_object.yaml               |  70 
providertests/testdata/TestGoogleVertexObjectGeneration/vertex-gemini-2-5-pro/simple_object_streaming.yaml     |  34 
providertests/testdata/TestOpenAICompatObjectGeneration/xai-grok-4-fast/complex_object.yaml                    |  33 
providertests/testdata/TestOpenAICompatObjectGeneration/xai-grok-4-fast/complex_object_streaming.yaml          |  10 
providertests/testdata/TestOpenAICompatObjectGeneration/xai-grok-4-fast/simple_object.yaml                     |  33 
providertests/testdata/TestOpenAICompatObjectGeneration/xai-grok-4-fast/simple_object_streaming.yaml           | 378 
providertests/testdata/TestOpenAICompatObjectGeneration/xai-grok-code-fast/complex_object.yaml                 |  33 
providertests/testdata/TestOpenAICompatObjectGeneration/xai-grok-code-fast/complex_object_streaming.yaml       |  10 
providertests/testdata/TestOpenAICompatObjectGeneration/xai-grok-code-fast/simple_object.yaml                  |  33 
providertests/testdata/TestOpenAICompatObjectGeneration/xai-grok-code-fast/simple_object_streaming.yaml        | 338 
providertests/testdata/TestOpenAICompatObjectGeneration/zai-glm-4.5/complex_object.yaml                        |  26 
providertests/testdata/TestOpenAICompatObjectGeneration/zai-glm-4.5/complex_object_streaming.yaml              |  10 
providertests/testdata/TestOpenAICompatObjectGeneration/zai-glm-4.5/simple_object.yaml                         |  33 
providertests/testdata/TestOpenAICompatObjectGeneration/zai-glm-4.5/simple_object_streaming.yaml               | 200 
providertests/testdata/TestOpenAIObjectGeneration/openai-gpt-4o-mini/complex_object.yaml                       |  69 
providertests/testdata/TestOpenAIObjectGeneration/openai-gpt-4o-mini/complex_object_streaming.yaml             |  10 
providertests/testdata/TestOpenAIObjectGeneration/openai-gpt-4o-mini/simple_object.yaml                        |  69 
providertests/testdata/TestOpenAIObjectGeneration/openai-gpt-4o-mini/simple_object_streaming.yaml              |  66 
providertests/testdata/TestOpenAIObjectGeneration/openai-gpt-4o/complex_object.yaml                            |  69 
providertests/testdata/TestOpenAIObjectGeneration/openai-gpt-4o/complex_object_streaming.yaml                  |  10 
providertests/testdata/TestOpenAIObjectGeneration/openai-gpt-4o/simple_object.yaml                             |  69 
providertests/testdata/TestOpenAIObjectGeneration/openai-gpt-4o/simple_object_streaming.yaml                   |  66 
providertests/testdata/TestOpenAIObjectGeneration/openai-gpt-5/complex_object.yaml                             |  68 
providertests/testdata/TestOpenAIObjectGeneration/openai-gpt-5/complex_object_streaming.yaml                   |  10 
providertests/testdata/TestOpenAIObjectGeneration/openai-gpt-5/simple_object.yaml                              |  68 
providertests/testdata/TestOpenAIObjectGeneration/openai-gpt-5/simple_object_streaming.yaml                    |  66 
providertests/testdata/TestOpenAIObjectGeneration/openai-o4-mini/complex_object.yaml                           |  68 
providertests/testdata/TestOpenAIObjectGeneration/openai-o4-mini/complex_object_streaming.yaml                 |  10 
providertests/testdata/TestOpenAIObjectGeneration/openai-o4-mini/simple_object.yaml                            |  68 
providertests/testdata/TestOpenAIObjectGeneration/openai-o4-mini/simple_object_streaming.yaml                  |  66 
providertests/testdata/TestOpenAIResponsesObjectGeneration/openai-gpt-4o-mini/complex_object.yaml              | 149 
providertests/testdata/TestOpenAIResponsesObjectGeneration/openai-gpt-4o-mini/complex_object_streaming.yaml    |  27 
providertests/testdata/TestOpenAIResponsesObjectGeneration/openai-gpt-4o-mini/simple_object.yaml               | 127 
providertests/testdata/TestOpenAIResponsesObjectGeneration/openai-gpt-4o-mini/simple_object_streaming.yaml     |  27 
providertests/testdata/TestOpenAIResponsesObjectGeneration/openai-gpt-4o/complex_object.yaml                   | 149 
providertests/testdata/TestOpenAIResponsesObjectGeneration/openai-gpt-4o/complex_object_streaming.yaml         |  27 
providertests/testdata/TestOpenAIResponsesObjectGeneration/openai-gpt-4o/simple_object.yaml                    | 127 
providertests/testdata/TestOpenAIResponsesObjectGeneration/openai-gpt-4o/simple_object_streaming.yaml          |  27 
providertests/testdata/TestOpenAIResponsesObjectGeneration/openai-gpt-5/complex_object.yaml                    | 154 
providertests/testdata/TestOpenAIResponsesObjectGeneration/openai-gpt-5/complex_object_streaming.yaml          |  27 
providertests/testdata/TestOpenAIResponsesObjectGeneration/openai-gpt-5/simple_object.yaml                     | 132 
providertests/testdata/TestOpenAIResponsesObjectGeneration/openai-gpt-5/simple_object_streaming.yaml           |  27 
providertests/testdata/TestOpenAIResponsesObjectGeneration/openai-o4-mini/complex_object.yaml                  | 154 
providertests/testdata/TestOpenAIResponsesObjectGeneration/openai-o4-mini/complex_object_streaming.yaml        |  27 
providertests/testdata/TestOpenAIResponsesObjectGeneration/openai-o4-mini/simple_object.yaml                   | 132 
providertests/testdata/TestOpenAIResponsesObjectGeneration/openai-o4-mini/simple_object_streaming.yaml         |  27 
schema/schema.go                                                                                               | 403 
schema/schema_test.go                                                                                          | 534 
tool.go                                                                                                        | 220 
tool_test.go                                                                                                   | 528 
95 files changed, 7,895 insertions(+), 771 deletions(-)

Detailed changes

agent_test.go šŸ”—

@@ -82,6 +82,14 @@ func (m *mockLanguageModel) Model() string {
 	return "mock-model"
 }
 
+func (m *mockLanguageModel) GenerateObject(ctx context.Context, call ObjectCall) (*ObjectResponse, error) {
+	return nil, fmt.Errorf("mock GenerateObject not implemented")
+}
+
+func (m *mockLanguageModel) StreamObject(ctx context.Context, call ObjectCall) (ObjectStreamResponse, error) {
+	return nil, fmt.Errorf("mock StreamObject not implemented")
+}
+
 // Test result.content - comprehensive content types (matches TS test)
 func TestAgent_Generate_ResultContent_AllTypes(t *testing.T) {
 	t.Parallel()

errors.go šŸ”—

@@ -1,6 +1,7 @@
 package fantasy
 
 import (
+	"errors"
 	"fmt"
 	"net/http"
 	"strings"
@@ -74,3 +75,30 @@ func (e RetryError) Unwrap() error {
 func ErrorTitleForStatusCode(statusCode int) string {
 	return strings.ToLower(http.StatusText(statusCode))
 }
+
+// NoObjectGeneratedError is returned when object generation fails
+// due to parsing errors, validation errors, or model failures.
+type NoObjectGeneratedError struct {
+	RawText         string
+	ParseError      error
+	ValidationError error
+	Usage           Usage
+	FinishReason    FinishReason
+}
+
+// Error implements the error interface.
+func (e *NoObjectGeneratedError) Error() string {
+	if e.ValidationError != nil {
+		return fmt.Sprintf("object validation failed: %v", e.ValidationError)
+	}
+	if e.ParseError != nil {
+		return fmt.Sprintf("failed to parse object: %v", e.ParseError)
+	}
+	return "failed to generate object"
+}
+
+// IsNoObjectGeneratedError checks if an error is of type NoObjectGeneratedError.
+func IsNoObjectGeneratedError(err error) bool {
+	var target *NoObjectGeneratedError
+	return errors.As(err, &target)
+}

examples/structured-outputs/go.mod šŸ”—

@@ -0,0 +1,26 @@
+module structured-outputs
+
+go 1.25.2
+
+replace charm.land/fantasy => ../..
+
+require charm.land/fantasy v0.0.0-00010101000000-000000000000
+
+require (
+	github.com/RealAlexandreAI/json-repair v0.0.14 // indirect
+	github.com/charmbracelet/x/exp/slice v0.0.0-20250904123553-b4e2667e5ad5 // indirect
+	github.com/charmbracelet/x/json v0.2.0 // indirect
+	github.com/go-json-experiment/json v0.0.0-20250910080747-cc2cfa0554c3 // indirect
+	github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
+	github.com/goccy/go-yaml v1.18.0 // indirect
+	github.com/google/uuid v1.6.0 // indirect
+	github.com/kaptinlin/go-i18n v0.2.0 // indirect
+	github.com/kaptinlin/jsonschema v0.5.2 // indirect
+	github.com/kaptinlin/messageformat-go v0.4.5 // indirect
+	github.com/openai/openai-go/v2 v2.7.1 // 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
+	golang.org/x/text v0.29.0 // indirect
+)

examples/structured-outputs/go.sum šŸ”—

@@ -0,0 +1,44 @@
+github.com/RealAlexandreAI/json-repair v0.0.14 h1:4kTqotVonDVTio5n2yweRUELVcNe2x518wl0bCsw0t0=
+github.com/RealAlexandreAI/json-repair v0.0.14/go.mod h1:GKJi5borR78O8c7HCVbgqjhoiVibZ6hJldxbc6dGrAI=
+github.com/charmbracelet/x/exp/slice v0.0.0-20250904123553-b4e2667e5ad5 h1:DTSZxdV9qQagD4iGcAt9RgaRBZtJl01bfKgdLzUzUPI=
+github.com/charmbracelet/x/exp/slice v0.0.0-20250904123553-b4e2667e5ad5/go.mod h1:vI5nDVMWi6veaYH+0Fmvpbe/+cv/iJfMntdh+N0+Tms=
+github.com/charmbracelet/x/json v0.2.0 h1:DqB+ZGx2h+Z+1s98HOuOyli+i97wsFQIxP2ZQANTPrQ=
+github.com/charmbracelet/x/json v0.2.0/go.mod h1:opFIflx2YgXgi49xVUu8gEQ21teFAxyMwvOiZhIvWNM=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-json-experiment/json v0.0.0-20250910080747-cc2cfa0554c3 h1:02WINGfSX5w0Mn+F28UyRoSt9uvMhKguwWMlOAh6U/0=
+github.com/go-json-experiment/json v0.0.0-20250910080747-cc2cfa0554c3/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
+github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
+github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
+github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
+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/kaptinlin/go-i18n v0.2.0 h1:8iwjAERQbCVF78c3HxC4MxUDxDRFvQVQlMDvlsO43hU=
+github.com/kaptinlin/go-i18n v0.2.0/go.mod h1:gRHEMrTHtQLsAFwulPbJG71TwHjXxkagn88O8FI8FuA=
+github.com/kaptinlin/jsonschema v0.5.2 h1:ipUBEv1/RnT+ErwdqXZ3Xtwkwp6uqp/Q9lFILrwhUfc=
+github.com/kaptinlin/jsonschema v0.5.2/go.mod h1:HuWb90460GwFxRe0i9Ni3Z7YXwkjpqjeccWTB9gTZZE=
+github.com/kaptinlin/messageformat-go v0.4.5 h1:Y1CTf38O6lKKXX/UZTwb2Xw7c6DPk7kjQEHPJW6qxTI=
+github.com/kaptinlin/messageformat-go v0.4.5/go.mod h1:r0PH7FsxJX8jS/n6LAYZon5w3X+yfCLUrquqYd2H7ks=
+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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+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=
+github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
+github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
+github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
+github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
+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=
+golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
+golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

examples/structured-outputs/main.go šŸ”—

@@ -0,0 +1,99 @@
+package main
+
+// This example demonstrates how to get structured, type-safe outputs from an
+// LLM. Here we're getting a recipe with validated fields that we can use
+// directly in our code.
+
+import (
+	"context"
+	"fmt"
+	"os"
+
+	"charm.land/fantasy"
+	"charm.land/fantasy/object"
+	"charm.land/fantasy/providers/openai"
+)
+
+// Here's what we want the LLM to fill out. The struct tags tell the model
+// what each field is for.
+type Recipe struct {
+	Name        string   `json:"name" description:"The name of the recipe"`
+	Ingredients []string `json:"ingredients" description:"List of ingredients needed"`
+	Steps       []string `json:"steps" description:"Step-by-step cooking instructions"`
+	PrepTime    int      `json:"prep_time" description:"Preparation time in minutes"`
+}
+
+func main() {
+	// We'll use OpenAI for this one.
+	apiKey := os.Getenv("OPENAI_API_KEY")
+	if apiKey == "" {
+		fmt.Println("Please set OPENAI_API_KEY environment variable")
+		os.Exit(1)
+	}
+
+	ctx := context.Background()
+
+	// Set up the provider.
+	provider, err := openai.New(openai.WithAPIKey(apiKey))
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Whoops: %v\n", err)
+		os.Exit(1)
+	}
+
+	// Pick the model.
+	model, err := provider.LanguageModel(ctx, "gpt-4o-mini")
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Dang: %v\n", err)
+		os.Exit(1)
+	}
+
+	fmt.Println("\nšŸŖ Generating a recipe...")
+
+	// Ask for a structured recipe. The model will return a Recipe struct
+	// that's been validated against our schema.
+	result, err := object.Generate[Recipe](ctx, model, fantasy.ObjectCall{
+		Prompt: fantasy.Prompt{
+			fantasy.NewUserMessage("Give me a recipe for chocolate chip cookies"),
+		},
+	})
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Oof: %v\n", err)
+		os.Exit(1)
+	}
+
+	// Now we have a type-safe Recipe we can use directly.
+	fmt.Printf("Recipe: %s\n", result.Object.Name)
+	fmt.Printf("Prep time: %d minutes\n", result.Object.PrepTime)
+	fmt.Printf("Ingredients: %d\n", len(result.Object.Ingredients))
+	for i, ing := range result.Object.Ingredients {
+		fmt.Printf("  %d. %s\n", i+1, ing)
+	}
+	fmt.Printf("Steps: %d\n", len(result.Object.Steps))
+	for i, step := range result.Object.Steps {
+		fmt.Printf("  %d. %s\n", i+1, step)
+	}
+	fmt.Printf("\nTokens used: %d\n\n", result.Usage.TotalTokens)
+
+	// Want to see progressive updates as the object builds? Use streaming!
+	fmt.Println("🌊 Now let's try streaming...")
+
+	stream, err := object.Stream[Recipe](ctx, model, fantasy.ObjectCall{
+		Prompt: fantasy.Prompt{
+			fantasy.NewUserMessage("Give me a recipe for banana bread"),
+		},
+	})
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Oof: %v\n", err)
+		os.Exit(1)
+	}
+
+	// Watch the recipe build in real-time!
+	updateCount := 0
+	for partial := range stream.PartialObjectStream() {
+		updateCount++
+		fmt.Printf("  Update %d: %s (%d ingredients, %d steps)\n",
+			updateCount, partial.Name, len(partial.Ingredients), len(partial.Steps))
+	}
+
+	fmt.Println()
+}

go.mod šŸ”—

@@ -1,9 +1,10 @@
 module charm.land/fantasy
 
-go 1.24.5
+go 1.25
 
 require (
 	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
 	github.com/aws/smithy-go v1.23.2
 	github.com/charmbracelet/anthropic-sdk-go v0.0.0-20251024181547-21d6f3d9a904
@@ -12,6 +13,7 @@ require (
 	github.com/go-viper/mapstructure/v2 v2.4.0
 	github.com/google/uuid v1.6.0
 	github.com/joho/godotenv v1.5.1
+	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
@@ -40,13 +42,17 @@ require (
 	github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
 	github.com/felixge/httpsnoop v1.0.4 // indirect
+	github.com/go-json-experiment/json v0.0.0-20250910080747-cc2cfa0554c3 // 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.18.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.6 // indirect
 	github.com/googleapis/gax-go/v2 v2.15.0 // indirect
 	github.com/gorilla/websocket v1.5.3 // indirect
+	github.com/kaptinlin/go-i18n v0.2.0 // indirect
+	github.com/kaptinlin/messageformat-go v0.4.5 // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/tidwall/gjson v1.18.0 // indirect
 	github.com/tidwall/match v1.1.1 // indirect
@@ -60,9 +66,9 @@ require (
 	go.opentelemetry.io/otel/trace v1.36.0 // indirect
 	golang.org/x/crypto v0.41.0 // indirect
 	golang.org/x/net v0.43.0 // indirect
-	golang.org/x/sync v0.16.0 // indirect
+	golang.org/x/sync v0.17.0 // indirect
 	golang.org/x/sys v0.35.0 // indirect
-	golang.org/x/text v0.28.0 // indirect
+	golang.org/x/text v0.29.0 // indirect
 	golang.org/x/time v0.12.0 // indirect
 	google.golang.org/api v0.239.0 // indirect
 	google.golang.org/genproto/googleapis/rpc v0.0.0-20250603155806-513f23925822 // indirect

go.sum šŸ”—

@@ -14,6 +14,8 @@ 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/RealAlexandreAI/json-repair v0.0.14 h1:4kTqotVonDVTio5n2yweRUELVcNe2x518wl0bCsw0t0=
+github.com/RealAlexandreAI/json-repair v0.0.14/go.mod h1:GKJi5borR78O8c7HCVbgqjhoiVibZ6hJldxbc6dGrAI=
 github.com/aws/aws-sdk-go-v2 v1.39.6 h1:2JrPCVgWJm7bm83BDwY5z8ietmeJUbh3O2ACnn+Xsqk=
 github.com/aws/aws-sdk-go-v2 v1.39.6/go.mod h1:c9pm7VwuW0UPxAEYGyTmyurVcNrbF6Rt/wixFqDhcjE=
 github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg=
@@ -52,6 +54,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/go-json-experiment/json v0.0.0-20250910080747-cc2cfa0554c3 h1:02WINGfSX5w0Mn+F28UyRoSt9uvMhKguwWMlOAh6U/0=
+github.com/go-json-experiment/json v0.0.0-20250910080747-cc2cfa0554c3/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=
@@ -59,6 +63,8 @@ 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-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
 github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
+github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
+github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
 github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
 github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
 github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
@@ -77,6 +83,12 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
 github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
 github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
 github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+github.com/kaptinlin/go-i18n v0.2.0 h1:8iwjAERQbCVF78c3HxC4MxUDxDRFvQVQlMDvlsO43hU=
+github.com/kaptinlin/go-i18n v0.2.0/go.mod h1:gRHEMrTHtQLsAFwulPbJG71TwHjXxkagn88O8FI8FuA=
+github.com/kaptinlin/jsonschema v0.5.2 h1:ipUBEv1/RnT+ErwdqXZ3Xtwkwp6uqp/Q9lFILrwhUfc=
+github.com/kaptinlin/jsonschema v0.5.2/go.mod h1:HuWb90460GwFxRe0i9Ni3Z7YXwkjpqjeccWTB9gTZZE=
+github.com/kaptinlin/messageformat-go v0.4.5 h1:Y1CTf38O6lKKXX/UZTwb2Xw7c6DPk7kjQEHPJW6qxTI=
+github.com/kaptinlin/messageformat-go v0.4.5/go.mod h1:r0PH7FsxJX8jS/n6LAYZon5w3X+yfCLUrquqYd2H7ks=
 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=
@@ -85,6 +97,8 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
 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/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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -127,12 +141,12 @@ golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
 golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
 golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
 golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
-golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
-golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
+golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
 golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
 golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
-golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
-golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
+golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
+golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
 golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
 golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
 google.golang.org/api v0.239.0 h1:2hZKUnFZEy81eugPs4e2XzIJ5SOwQg0G82bpXD65Puo=

model.go šŸ”—

@@ -219,7 +219,6 @@ type Call struct {
 	ProviderOptions ProviderOptions `json:"provider_options"`
 }
 
-// CallWarningType represents the type of call warning.
 // CallWarningType represents the type of call warning.
 type CallWarningType string
 
@@ -248,6 +247,9 @@ type LanguageModel interface {
 	Generate(context.Context, Call) (*Response, error)
 	Stream(context.Context, Call) (StreamResponse, error)
 
+	GenerateObject(context.Context, ObjectCall) (*ObjectResponse, error)
+	StreamObject(context.Context, ObjectCall) (ObjectStreamResponse, error)
+
 	Provider() string
 	Model() string
 }

object.go šŸ”—

@@ -0,0 +1,233 @@
+package fantasy
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"iter"
+	"reflect"
+
+	"charm.land/fantasy/schema"
+)
+
+// ObjectMode specifies how structured output should be generated.
+type ObjectMode string
+
+const (
+	// ObjectModeAuto lets the provider choose the best approach.
+	ObjectModeAuto ObjectMode = "auto"
+
+	// ObjectModeJSON forces the use of native JSON mode (if supported).
+	ObjectModeJSON ObjectMode = "json"
+
+	// ObjectModeTool forces the use of tool-based approach.
+	ObjectModeTool ObjectMode = "tool"
+
+	// ObjectModeText uses text generation with schema in prompt (fallback for models without tool/JSON support).
+	ObjectModeText ObjectMode = "text"
+)
+
+// ObjectCall represents a request to generate a structured object.
+type ObjectCall struct {
+	Prompt            Prompt
+	Schema            Schema
+	SchemaName        string
+	SchemaDescription string
+
+	MaxOutputTokens  *int64
+	Temperature      *float64
+	TopP             *float64
+	TopK             *int64
+	PresencePenalty  *float64
+	FrequencyPenalty *float64
+
+	ProviderOptions ProviderOptions
+
+	RepairText schema.ObjectRepairFunc
+}
+
+// ObjectResponse represents the response from a structured object generation.
+type ObjectResponse struct {
+	Object           any
+	RawText          string
+	Usage            Usage
+	FinishReason     FinishReason
+	Warnings         []CallWarning
+	ProviderMetadata ProviderMetadata
+}
+
+// ObjectStreamPartType indicates the type of stream part.
+type ObjectStreamPartType string
+
+const (
+	// ObjectStreamPartTypeObject is emitted when a new partial object is available.
+	ObjectStreamPartTypeObject ObjectStreamPartType = "object"
+
+	// ObjectStreamPartTypeTextDelta is emitted for text deltas (if model generates text).
+	ObjectStreamPartTypeTextDelta ObjectStreamPartType = "text-delta"
+
+	// ObjectStreamPartTypeError is emitted when an error occurs.
+	ObjectStreamPartTypeError ObjectStreamPartType = "error"
+
+	// ObjectStreamPartTypeFinish is emitted when streaming completes.
+	ObjectStreamPartTypeFinish ObjectStreamPartType = "finish"
+)
+
+// ObjectStreamPart represents a single chunk in the object stream.
+type ObjectStreamPart struct {
+	Type             ObjectStreamPartType
+	Object           any
+	Delta            string
+	Error            error
+	Usage            Usage
+	FinishReason     FinishReason
+	Warnings         []CallWarning
+	ProviderMetadata ProviderMetadata
+}
+
+// ObjectStreamResponse is an iterator over ObjectStreamPart.
+type ObjectStreamResponse = iter.Seq[ObjectStreamPart]
+
+// ObjectResult is a typed result wrapper returned by GenerateObject[T].
+type ObjectResult[T any] struct {
+	Object           T
+	RawText          string
+	Usage            Usage
+	FinishReason     FinishReason
+	Warnings         []CallWarning
+	ProviderMetadata ProviderMetadata
+}
+
+// StreamObjectResult provides typed access to a streaming object generation result.
+type StreamObjectResult[T any] struct {
+	stream ObjectStreamResponse
+	ctx    context.Context
+}
+
+// NewStreamObjectResult creates a typed stream result from an untyped stream.
+func NewStreamObjectResult[T any](ctx context.Context, stream ObjectStreamResponse) *StreamObjectResult[T] {
+	return &StreamObjectResult[T]{
+		stream: stream,
+		ctx:    ctx,
+	}
+}
+
+// PartialObjectStream returns an iterator that yields progressively more complete objects.
+// Only emits when the object actually changes (deduplication).
+func (s *StreamObjectResult[T]) PartialObjectStream() iter.Seq[T] {
+	return func(yield func(T) bool) {
+		var lastObject T
+		var hasEmitted bool
+
+		for part := range s.stream {
+			if part.Type == ObjectStreamPartTypeObject && part.Object != nil {
+				var current T
+				if err := unmarshalObject(part.Object, &current); err != nil {
+					continue
+				}
+
+				if !hasEmitted || !reflect.DeepEqual(current, lastObject) {
+					if !yield(current) {
+						return
+					}
+					lastObject = current
+					hasEmitted = true
+				}
+			}
+		}
+	}
+}
+
+// TextStream returns an iterator that yields text deltas.
+// Useful if the model generates explanatory text alongside the object.
+func (s *StreamObjectResult[T]) TextStream() iter.Seq[string] {
+	return func(yield func(string) bool) {
+		for part := range s.stream {
+			if part.Type == ObjectStreamPartTypeTextDelta && part.Delta != "" {
+				if !yield(part.Delta) {
+					return
+				}
+			}
+		}
+	}
+}
+
+// FullStream returns an iterator that yields all stream parts including errors and metadata.
+func (s *StreamObjectResult[T]) FullStream() iter.Seq[ObjectStreamPart] {
+	return s.stream
+}
+
+// Object waits for the stream to complete and returns the final object.
+// Returns an error if streaming fails or no valid object was generated.
+func (s *StreamObjectResult[T]) Object() (*ObjectResult[T], error) {
+	var finalObject T
+	var usage Usage
+	var finishReason FinishReason
+	var warnings []CallWarning
+	var providerMetadata ProviderMetadata
+	var rawText string
+	var lastError error
+	hasObject := false
+
+	for part := range s.stream {
+		switch part.Type {
+		case ObjectStreamPartTypeObject:
+			if part.Object != nil {
+				if err := unmarshalObject(part.Object, &finalObject); err == nil {
+					hasObject = true
+					if jsonBytes, err := json.Marshal(part.Object); err == nil {
+						rawText = string(jsonBytes)
+					}
+				}
+			}
+
+		case ObjectStreamPartTypeError:
+			lastError = part.Error
+
+		case ObjectStreamPartTypeFinish:
+			usage = part.Usage
+			finishReason = part.FinishReason
+			if len(part.Warnings) > 0 {
+				warnings = part.Warnings
+			}
+			if len(part.ProviderMetadata) > 0 {
+				providerMetadata = part.ProviderMetadata
+			}
+		}
+	}
+
+	if lastError != nil {
+		return nil, lastError
+	}
+
+	if !hasObject {
+		return nil, &NoObjectGeneratedError{
+			RawText:      rawText,
+			ParseError:   fmt.Errorf("no valid object generated in stream"),
+			Usage:        usage,
+			FinishReason: finishReason,
+		}
+	}
+
+	return &ObjectResult[T]{
+		Object:           finalObject,
+		RawText:          rawText,
+		Usage:            usage,
+		FinishReason:     finishReason,
+		Warnings:         warnings,
+		ProviderMetadata: providerMetadata,
+	}, nil
+}
+
+func unmarshalObject(obj any, target any) error {
+	jsonBytes, err := json.Marshal(obj)
+	if err != nil {
+		return fmt.Errorf("failed to marshal object: %w", err)
+	}
+
+	if err := json.Unmarshal(jsonBytes, target); err != nil {
+		return fmt.Errorf("failed to unmarshal into target type: %w", err)
+	}
+
+	return nil
+}

object/object.go šŸ”—

@@ -0,0 +1,616 @@
+// Package object provides utilities for generating structured objects with automatic schema generation.
+// It simplifies working with typed structured outputs by handling schema reflection and unmarshaling.
+package object
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"reflect"
+
+	"charm.land/fantasy"
+	"charm.land/fantasy/schema"
+)
+
+// Generate generates a structured object that matches the given type T.
+// The schema is automatically generated from T using reflection.
+//
+// Example:
+//
+//	type Recipe struct {
+//	    Name        string   `json:"name"`
+//	    Ingredients []string `json:"ingredients"`
+//	}
+//
+//	result, err := object.Generate[Recipe](ctx, model, fantasy.ObjectCall{
+//	    Prompt: fantasy.Prompt{fantasy.NewUserMessage("Generate a lasagna recipe")},
+//	})
+func Generate[T any](
+	ctx context.Context,
+	model fantasy.LanguageModel,
+	opts fantasy.ObjectCall,
+) (*fantasy.ObjectResult[T], error) {
+	var zero T
+	s := schema.Generate(reflect.TypeOf(zero))
+	opts.Schema = s
+
+	resp, err := model.GenerateObject(ctx, opts)
+	if err != nil {
+		return nil, err
+	}
+
+	var result T
+	if err := unmarshal(resp.Object, &result); err != nil {
+		return nil, fmt.Errorf("failed to unmarshal to %T: %w", result, err)
+	}
+
+	return &fantasy.ObjectResult[T]{
+		Object:           result,
+		RawText:          resp.RawText,
+		Usage:            resp.Usage,
+		FinishReason:     resp.FinishReason,
+		Warnings:         resp.Warnings,
+		ProviderMetadata: resp.ProviderMetadata,
+	}, nil
+}
+
+// Stream streams a structured object that matches the given type T.
+// Returns a StreamObjectResult[T] with progressive updates and deduplication.
+//
+// Example:
+//
+//	stream, err := object.Stream[Recipe](ctx, model, fantasy.ObjectCall{
+//	    Prompt: fantasy.Prompt{fantasy.NewUserMessage("Generate a lasagna recipe")},
+//	})
+//
+//	for partial := range stream.PartialObjectStream() {
+//	    fmt.Printf("Progress: %s\n", partial.Name)
+//	}
+//
+//	result, err := stream.Object()  // Wait for final result
+func Stream[T any](
+	ctx context.Context,
+	model fantasy.LanguageModel,
+	opts fantasy.ObjectCall,
+) (*fantasy.StreamObjectResult[T], error) {
+	var zero T
+	s := schema.Generate(reflect.TypeOf(zero))
+	opts.Schema = s
+
+	stream, err := model.StreamObject(ctx, opts)
+	if err != nil {
+		return nil, err
+	}
+
+	return fantasy.NewStreamObjectResult[T](ctx, stream), nil
+}
+
+// GenerateWithTool is a helper for providers without native JSON mode.
+// It converts the schema to a tool definition, forces the model to call it,
+// and extracts the tool's input as the structured output.
+func GenerateWithTool(
+	ctx context.Context,
+	model fantasy.LanguageModel,
+	call fantasy.ObjectCall,
+) (*fantasy.ObjectResponse, error) {
+	toolName := call.SchemaName
+	if toolName == "" {
+		toolName = "generate_object"
+	}
+
+	toolDescription := call.SchemaDescription
+	if toolDescription == "" {
+		toolDescription = "Generate a structured object matching the schema"
+	}
+
+	tool := fantasy.FunctionTool{
+		Name:        toolName,
+		Description: toolDescription,
+		InputSchema: schema.ToMap(call.Schema),
+	}
+
+	toolChoice := fantasy.SpecificToolChoice(tool.Name)
+	resp, err := model.Generate(ctx, fantasy.Call{
+		Prompt:           call.Prompt,
+		Tools:            []fantasy.Tool{tool},
+		ToolChoice:       &toolChoice,
+		MaxOutputTokens:  call.MaxOutputTokens,
+		Temperature:      call.Temperature,
+		TopP:             call.TopP,
+		TopK:             call.TopK,
+		PresencePenalty:  call.PresencePenalty,
+		FrequencyPenalty: call.FrequencyPenalty,
+		ProviderOptions:  call.ProviderOptions,
+	})
+	if err != nil {
+		return nil, fmt.Errorf("tool-based generation failed: %w", err)
+	}
+
+	toolCalls := resp.Content.ToolCalls()
+	if len(toolCalls) == 0 {
+		return nil, &fantasy.NoObjectGeneratedError{
+			RawText:      resp.Content.Text(),
+			ParseError:   fmt.Errorf("no tool call generated"),
+			Usage:        resp.Usage,
+			FinishReason: resp.FinishReason,
+		}
+	}
+
+	toolCall := toolCalls[0]
+
+	var obj any
+	if call.RepairText != nil {
+		obj, err = schema.ParseAndValidateWithRepair(ctx, toolCall.Input, call.Schema, call.RepairText)
+	} else {
+		obj, err = schema.ParseAndValidate(toolCall.Input, call.Schema)
+	}
+
+	if err != nil {
+		if nogErr, ok := err.(*fantasy.NoObjectGeneratedError); ok {
+			nogErr.Usage = resp.Usage
+			nogErr.FinishReason = resp.FinishReason
+		}
+		return nil, err
+	}
+
+	return &fantasy.ObjectResponse{
+		Object:           obj,
+		RawText:          toolCall.Input,
+		Usage:            resp.Usage,
+		FinishReason:     resp.FinishReason,
+		Warnings:         resp.Warnings,
+		ProviderMetadata: resp.ProviderMetadata,
+	}, nil
+}
+
+// GenerateWithText is a helper for providers without tool or JSON mode support.
+// It adds the schema to the system prompt and parses the text response as JSON.
+// This is a fallback for older models or simple providers.
+func GenerateWithText(
+	ctx context.Context,
+	model fantasy.LanguageModel,
+	call fantasy.ObjectCall,
+) (*fantasy.ObjectResponse, error) {
+	jsonSchemaBytes, err := json.Marshal(call.Schema)
+	if err != nil {
+		return nil, fmt.Errorf("failed to marshal schema: %w", err)
+	}
+
+	schemaInstruction := fmt.Sprintf(
+		"You must respond with valid JSON that matches this schema: %s\n"+
+			"Respond ONLY with the JSON object, no additional text or explanation.",
+		string(jsonSchemaBytes),
+	)
+
+	enhancedPrompt := make(fantasy.Prompt, 0, len(call.Prompt)+1)
+
+	hasSystem := false
+	for _, msg := range call.Prompt {
+		if msg.Role == fantasy.MessageRoleSystem {
+			hasSystem = true
+			existingText := ""
+			if len(msg.Content) > 0 {
+				if textPart, ok := msg.Content[0].(fantasy.TextPart); ok {
+					existingText = textPart.Text
+				}
+			}
+			enhancedPrompt = append(enhancedPrompt, fantasy.NewSystemMessage(existingText+"\n\n"+schemaInstruction))
+		} else {
+			enhancedPrompt = append(enhancedPrompt, msg)
+		}
+	}
+
+	if !hasSystem {
+		enhancedPrompt = append(fantasy.Prompt{fantasy.NewSystemMessage(schemaInstruction)}, call.Prompt...)
+	}
+
+	resp, err := model.Generate(ctx, fantasy.Call{
+		Prompt:           enhancedPrompt,
+		MaxOutputTokens:  call.MaxOutputTokens,
+		Temperature:      call.Temperature,
+		TopP:             call.TopP,
+		TopK:             call.TopK,
+		PresencePenalty:  call.PresencePenalty,
+		FrequencyPenalty: call.FrequencyPenalty,
+		ProviderOptions:  call.ProviderOptions,
+	})
+	if err != nil {
+		return nil, fmt.Errorf("text-based generation failed: %w", err)
+	}
+
+	textContent := resp.Content.Text()
+	if textContent == "" {
+		return nil, &fantasy.NoObjectGeneratedError{
+			RawText:      "",
+			ParseError:   fmt.Errorf("no text content in response"),
+			Usage:        resp.Usage,
+			FinishReason: resp.FinishReason,
+		}
+	}
+
+	var obj any
+	if call.RepairText != nil {
+		obj, err = schema.ParseAndValidateWithRepair(ctx, textContent, call.Schema, call.RepairText)
+	} else {
+		obj, err = schema.ParseAndValidate(textContent, call.Schema)
+	}
+
+	if err != nil {
+		if nogErr, ok := err.(*schema.ParseError); ok {
+			return nil, &fantasy.NoObjectGeneratedError{
+				RawText:         nogErr.RawText,
+				ParseError:      nogErr.ParseError,
+				ValidationError: nogErr.ValidationError,
+				Usage:           resp.Usage,
+				FinishReason:    resp.FinishReason,
+			}
+		}
+		return nil, err
+	}
+
+	return &fantasy.ObjectResponse{
+		Object:           obj,
+		RawText:          textContent,
+		Usage:            resp.Usage,
+		FinishReason:     resp.FinishReason,
+		Warnings:         resp.Warnings,
+		ProviderMetadata: resp.ProviderMetadata,
+	}, nil
+}
+
+// StreamWithTool is a helper for providers without native JSON streaming.
+// It uses streaming tool calls to extract and parse the structured output progressively.
+func StreamWithTool(
+	ctx context.Context,
+	model fantasy.LanguageModel,
+	call fantasy.ObjectCall,
+) (fantasy.ObjectStreamResponse, error) {
+	// Create a tool from the schema
+	toolName := call.SchemaName
+	if toolName == "" {
+		toolName = "generate_object"
+	}
+
+	toolDescription := call.SchemaDescription
+	if toolDescription == "" {
+		toolDescription = "Generate a structured object matching the schema"
+	}
+
+	tool := fantasy.FunctionTool{
+		Name:        toolName,
+		Description: toolDescription,
+		InputSchema: schema.ToMap(call.Schema),
+	}
+
+	// Make a streaming Generate call with forced tool choice
+	toolChoice := fantasy.SpecificToolChoice(tool.Name)
+	stream, err := model.Stream(ctx, fantasy.Call{
+		Prompt:           call.Prompt,
+		Tools:            []fantasy.Tool{tool},
+		ToolChoice:       &toolChoice,
+		MaxOutputTokens:  call.MaxOutputTokens,
+		Temperature:      call.Temperature,
+		TopP:             call.TopP,
+		TopK:             call.TopK,
+		PresencePenalty:  call.PresencePenalty,
+		FrequencyPenalty: call.FrequencyPenalty,
+		ProviderOptions:  call.ProviderOptions,
+	})
+	if err != nil {
+		return nil, fmt.Errorf("tool-based streaming failed: %w", err)
+	}
+
+	// Convert the text stream to object stream parts
+	return func(yield func(fantasy.ObjectStreamPart) bool) {
+		var accumulated string
+		var lastParsedObject any
+		var usage fantasy.Usage
+		var finishReason fantasy.FinishReason
+		var warnings []fantasy.CallWarning
+		var providerMetadata fantasy.ProviderMetadata
+		var streamErr error
+
+		for part := range stream {
+			switch part.Type {
+			case fantasy.StreamPartTypeTextDelta:
+				accumulated += part.Delta
+
+				obj, state, parseErr := schema.ParsePartialJSON(accumulated)
+
+				if state == schema.ParseStateSuccessful || state == schema.ParseStateRepaired {
+					if err := schema.ValidateAgainstSchema(obj, call.Schema); err == nil {
+						if !reflect.DeepEqual(obj, lastParsedObject) {
+							if !yield(fantasy.ObjectStreamPart{
+								Type:   fantasy.ObjectStreamPartTypeObject,
+								Object: obj,
+							}) {
+								return
+							}
+							lastParsedObject = obj
+						}
+					}
+				}
+
+				if state == schema.ParseStateFailed && call.RepairText != nil {
+					repairedText, repairErr := call.RepairText(ctx, accumulated, parseErr)
+					if repairErr == nil {
+						obj2, state2, _ := schema.ParsePartialJSON(repairedText)
+						if (state2 == schema.ParseStateSuccessful || state2 == schema.ParseStateRepaired) &&
+							schema.ValidateAgainstSchema(obj2, call.Schema) == nil {
+							if !reflect.DeepEqual(obj2, lastParsedObject) {
+								if !yield(fantasy.ObjectStreamPart{
+									Type:   fantasy.ObjectStreamPartTypeObject,
+									Object: obj2,
+								}) {
+									return
+								}
+								lastParsedObject = obj2
+							}
+						}
+					}
+				}
+
+			case fantasy.StreamPartTypeToolInputDelta:
+				accumulated += part.Delta
+
+				obj, state, parseErr := schema.ParsePartialJSON(accumulated)
+				if state == schema.ParseStateSuccessful || state == schema.ParseStateRepaired {
+					if err := schema.ValidateAgainstSchema(obj, call.Schema); err == nil {
+						if !reflect.DeepEqual(obj, lastParsedObject) {
+							if !yield(fantasy.ObjectStreamPart{
+								Type:   fantasy.ObjectStreamPartTypeObject,
+								Object: obj,
+							}) {
+								return
+							}
+							lastParsedObject = obj
+						}
+					}
+				}
+
+				if state == schema.ParseStateFailed && call.RepairText != nil {
+					repairedText, repairErr := call.RepairText(ctx, accumulated, parseErr)
+					if repairErr == nil {
+						obj2, state2, _ := schema.ParsePartialJSON(repairedText)
+						if (state2 == schema.ParseStateSuccessful || state2 == schema.ParseStateRepaired) &&
+							schema.ValidateAgainstSchema(obj2, call.Schema) == nil {
+							if !reflect.DeepEqual(obj2, lastParsedObject) {
+								if !yield(fantasy.ObjectStreamPart{
+									Type:   fantasy.ObjectStreamPartTypeObject,
+									Object: obj2,
+								}) {
+									return
+								}
+								lastParsedObject = obj2
+							}
+						}
+					}
+				}
+
+			case fantasy.StreamPartTypeToolCall:
+				toolInput := part.ToolCallInput
+
+				var obj any
+				var err error
+				if call.RepairText != nil {
+					obj, err = schema.ParseAndValidateWithRepair(ctx, toolInput, call.Schema, call.RepairText)
+				} else {
+					obj, err = schema.ParseAndValidate(toolInput, call.Schema)
+				}
+
+				if err == nil {
+					if !reflect.DeepEqual(obj, lastParsedObject) {
+						if !yield(fantasy.ObjectStreamPart{
+							Type:   fantasy.ObjectStreamPartTypeObject,
+							Object: obj,
+						}) {
+							return
+						}
+						lastParsedObject = obj
+					}
+				}
+
+			case fantasy.StreamPartTypeError:
+				streamErr = part.Error
+				if !yield(fantasy.ObjectStreamPart{
+					Type:  fantasy.ObjectStreamPartTypeError,
+					Error: part.Error,
+				}) {
+					return
+				}
+
+			case fantasy.StreamPartTypeFinish:
+				usage = part.Usage
+				finishReason = part.FinishReason
+
+			case fantasy.StreamPartTypeWarnings:
+				warnings = part.Warnings
+			}
+
+			if len(part.ProviderMetadata) > 0 {
+				providerMetadata = part.ProviderMetadata
+			}
+		}
+
+		if streamErr == nil && lastParsedObject != nil {
+			yield(fantasy.ObjectStreamPart{
+				Type:             fantasy.ObjectStreamPartTypeFinish,
+				Usage:            usage,
+				FinishReason:     finishReason,
+				Warnings:         warnings,
+				ProviderMetadata: providerMetadata,
+			})
+		} else if streamErr == nil && lastParsedObject == nil {
+			yield(fantasy.ObjectStreamPart{
+				Type: fantasy.ObjectStreamPartTypeError,
+				Error: &fantasy.NoObjectGeneratedError{
+					RawText:      accumulated,
+					ParseError:   fmt.Errorf("no valid object generated in stream"),
+					Usage:        usage,
+					FinishReason: finishReason,
+				},
+			})
+		}
+	}, nil
+}
+
+// StreamWithText is a helper for providers without tool or JSON streaming support.
+// It adds the schema to the system prompt and parses the streamed text as JSON progressively.
+func StreamWithText(
+	ctx context.Context,
+	model fantasy.LanguageModel,
+	call fantasy.ObjectCall,
+) (fantasy.ObjectStreamResponse, error) {
+	jsonSchemaMap := schema.ToMap(call.Schema)
+	jsonSchemaBytes, err := json.Marshal(jsonSchemaMap)
+	if err != nil {
+		return nil, fmt.Errorf("failed to marshal schema: %w", err)
+	}
+
+	schemaInstruction := fmt.Sprintf(
+		"You must respond with valid JSON that matches this schema: %s\n"+
+			"Respond ONLY with the JSON object, no additional text or explanation.",
+		string(jsonSchemaBytes),
+	)
+
+	enhancedPrompt := make(fantasy.Prompt, 0, len(call.Prompt)+1)
+
+	hasSystem := false
+	for _, msg := range call.Prompt {
+		if msg.Role == fantasy.MessageRoleSystem {
+			hasSystem = true
+			existingText := ""
+			if len(msg.Content) > 0 {
+				if textPart, ok := msg.Content[0].(fantasy.TextPart); ok {
+					existingText = textPart.Text
+				}
+			}
+			enhancedPrompt = append(enhancedPrompt, fantasy.NewSystemMessage(existingText+"\n\n"+schemaInstruction))
+		} else {
+			enhancedPrompt = append(enhancedPrompt, msg)
+		}
+	}
+
+	if !hasSystem {
+		enhancedPrompt = append(fantasy.Prompt{fantasy.NewSystemMessage(schemaInstruction)}, call.Prompt...)
+	}
+
+	stream, err := model.Stream(ctx, fantasy.Call{
+		Prompt:           enhancedPrompt,
+		MaxOutputTokens:  call.MaxOutputTokens,
+		Temperature:      call.Temperature,
+		TopP:             call.TopP,
+		TopK:             call.TopK,
+		PresencePenalty:  call.PresencePenalty,
+		FrequencyPenalty: call.FrequencyPenalty,
+		ProviderOptions:  call.ProviderOptions,
+	})
+	if err != nil {
+		return nil, fmt.Errorf("text-based streaming failed: %w", err)
+	}
+
+	return func(yield func(fantasy.ObjectStreamPart) bool) {
+		var accumulated string
+		var lastParsedObject any
+		var usage fantasy.Usage
+		var finishReason fantasy.FinishReason
+		var warnings []fantasy.CallWarning
+		var providerMetadata fantasy.ProviderMetadata
+		var streamErr error
+
+		for part := range stream {
+			switch part.Type {
+			case fantasy.StreamPartTypeTextDelta:
+				accumulated += part.Delta
+
+				obj, state, parseErr := schema.ParsePartialJSON(accumulated)
+
+				if state == schema.ParseStateSuccessful || state == schema.ParseStateRepaired {
+					if err := schema.ValidateAgainstSchema(obj, call.Schema); err == nil {
+						if !reflect.DeepEqual(obj, lastParsedObject) {
+							if !yield(fantasy.ObjectStreamPart{
+								Type:   fantasy.ObjectStreamPartTypeObject,
+								Object: obj,
+							}) {
+								return
+							}
+							lastParsedObject = obj
+						}
+					}
+				}
+
+				if state == schema.ParseStateFailed && call.RepairText != nil {
+					repairedText, repairErr := call.RepairText(ctx, accumulated, parseErr)
+					if repairErr == nil {
+						obj2, state2, _ := schema.ParsePartialJSON(repairedText)
+						if (state2 == schema.ParseStateSuccessful || state2 == schema.ParseStateRepaired) &&
+							schema.ValidateAgainstSchema(obj2, call.Schema) == nil {
+							if !reflect.DeepEqual(obj2, lastParsedObject) {
+								if !yield(fantasy.ObjectStreamPart{
+									Type:   fantasy.ObjectStreamPartTypeObject,
+									Object: obj2,
+								}) {
+									return
+								}
+								lastParsedObject = obj2
+							}
+						}
+					}
+				}
+
+			case fantasy.StreamPartTypeError:
+				streamErr = part.Error
+				if !yield(fantasy.ObjectStreamPart{
+					Type:  fantasy.ObjectStreamPartTypeError,
+					Error: part.Error,
+				}) {
+					return
+				}
+
+			case fantasy.StreamPartTypeFinish:
+				usage = part.Usage
+				finishReason = part.FinishReason
+
+			case fantasy.StreamPartTypeWarnings:
+				warnings = part.Warnings
+			}
+
+			if len(part.ProviderMetadata) > 0 {
+				providerMetadata = part.ProviderMetadata
+			}
+		}
+
+		if streamErr == nil && lastParsedObject != nil {
+			yield(fantasy.ObjectStreamPart{
+				Type:             fantasy.ObjectStreamPartTypeFinish,
+				Usage:            usage,
+				FinishReason:     finishReason,
+				Warnings:         warnings,
+				ProviderMetadata: providerMetadata,
+			})
+		} else if streamErr == nil && lastParsedObject == nil {
+			yield(fantasy.ObjectStreamPart{
+				Type: fantasy.ObjectStreamPartTypeError,
+				Error: &fantasy.NoObjectGeneratedError{
+					RawText:      accumulated,
+					ParseError:   fmt.Errorf("no valid object generated in stream"),
+					Usage:        usage,
+					FinishReason: finishReason,
+				},
+			})
+		}
+	}, nil
+}
+
+func unmarshal(obj any, target any) error {
+	jsonBytes, err := json.Marshal(obj)
+	if err != nil {
+		return fmt.Errorf("failed to marshal object: %w", err)
+	}
+
+	if err := json.Unmarshal(jsonBytes, target); err != nil {
+		return fmt.Errorf("failed to unmarshal into target type: %w", err)
+	}
+
+	return nil
+}

providers/anthropic/anthropic.go šŸ”—

@@ -13,6 +13,7 @@ import (
 	"strings"
 
 	"charm.land/fantasy"
+	"charm.land/fantasy/object"
 	"github.com/charmbracelet/anthropic-sdk-go"
 	"github.com/charmbracelet/anthropic-sdk-go/bedrock"
 	"github.com/charmbracelet/anthropic-sdk-go/option"
@@ -40,6 +41,8 @@ type options struct {
 	skipAuth       bool
 
 	useBedrock bool
+
+	objectMode fantasy.ObjectMode
 }
 
 type provider struct {
@@ -52,7 +55,8 @@ type Option = func(*options)
 // New creates a new Anthropic provider with the given options.
 func New(opts ...Option) (fantasy.Provider, error) {
 	providerOptions := options{
-		headers: map[string]string{},
+		headers:    map[string]string{},
+		objectMode: fantasy.ObjectModeAuto,
 	}
 	for _, o := range opts {
 		o(&providerOptions)
@@ -120,6 +124,17 @@ func WithHTTPClient(client option.HTTPClient) Option {
 	}
 }
 
+// WithObjectMode sets the object generation mode.
+func WithObjectMode(om fantasy.ObjectMode) Option {
+	return func(o *options) {
+		// not supported
+		if om == fantasy.ObjectModeJSON {
+			om = fantasy.ObjectModeAuto
+		}
+		o.objectMode = om
+	}
+}
+
 func (a *provider) LanguageModel(ctx context.Context, modelID string) (fantasy.LanguageModel, error) {
 	clientOptions := make([]option.RequestOption, 0, 5+len(a.options.headers))
 	clientOptions = append(clientOptions, option.WithMaxRetries(0))
@@ -952,3 +967,23 @@ func (a languageModel) Stream(ctx context.Context, call fantasy.Call) (fantasy.S
 		}
 	}, nil
 }
+
+// GenerateObject implements fantasy.LanguageModel.
+func (a languageModel) GenerateObject(ctx context.Context, call fantasy.ObjectCall) (*fantasy.ObjectResponse, error) {
+	switch a.options.objectMode {
+	case fantasy.ObjectModeText:
+		return object.GenerateWithText(ctx, a, call)
+	default:
+		return object.GenerateWithTool(ctx, a, call)
+	}
+}
+
+// StreamObject implements fantasy.LanguageModel.
+func (a languageModel) StreamObject(ctx context.Context, call fantasy.ObjectCall) (fantasy.ObjectStreamResponse, error) {
+	switch a.options.objectMode {
+	case fantasy.ObjectModeText:
+		return object.StreamWithText(ctx, a, call)
+	default:
+		return object.StreamWithTool(ctx, a, call)
+	}
+}

providers/google/google.go šŸ”—

@@ -8,10 +8,13 @@ import (
 	"fmt"
 	"maps"
 	"net/http"
+	"reflect"
 	"strings"
 
 	"charm.land/fantasy"
+	"charm.land/fantasy/object"
 	"charm.land/fantasy/providers/anthropic"
+	"charm.land/fantasy/schema"
 	"cloud.google.com/go/auth"
 	"github.com/charmbracelet/x/exp/slice"
 	"github.com/google/uuid"
@@ -39,6 +42,7 @@ type options struct {
 	location       string
 	skipAuth       bool
 	toolCallIDFunc ToolCallIDFunc
+	objectMode     fantasy.ObjectMode
 }
 
 // Option defines a function that configures Google provider options.
@@ -128,6 +132,13 @@ func WithToolCallIDFunc(f ToolCallIDFunc) Option {
 	}
 }
 
+// WithObjectMode sets the object generation mode for the Google provider.
+func WithObjectMode(om fantasy.ObjectMode) Option {
+	return func(o *options) {
+		o.objectMode = om
+	}
+}
+
 func (*provider) Name() string {
 	return Name
 }
@@ -137,6 +148,7 @@ type languageModel struct {
 	modelID         string
 	client          *genai.Client
 	providerOptions options
+	objectMode      fantasy.ObjectMode
 }
 
 // LanguageModel implements fantasy.Provider.
@@ -182,11 +194,18 @@ func (a *provider) LanguageModel(ctx context.Context, modelID string) (fantasy.L
 	if err != nil {
 		return nil, err
 	}
+
+	objectMode := a.options.objectMode
+	if objectMode == "" {
+		objectMode = fantasy.ObjectModeAuto
+	}
+
 	return &languageModel{
 		modelID:         modelID,
 		provider:        a.options.name,
 		providerOptions: a.options,
 		client:          client,
+		objectMode:      objectMode,
 	}, nil
 }
 
@@ -739,7 +758,8 @@ func (g *languageModel) Stream(ctx context.Context, call fantasy.Call) (fantasy.
 				}
 			}
 
-			if resp.UsageMetadata != nil {
+			// we need to make sure that there is actual tokendata
+			if resp.UsageMetadata != nil && resp.UsageMetadata.TotalTokenCount != 0 {
 				currentUsage := mapUsage(resp.UsageMetadata)
 				// if first usage chunk
 				if usage == nil {
@@ -789,6 +809,268 @@ func (g *languageModel) Stream(ctx context.Context, call fantasy.Call) (fantasy.
 	}, nil
 }
 
+// GenerateObject implements fantasy.LanguageModel.
+func (g *languageModel) GenerateObject(ctx context.Context, call fantasy.ObjectCall) (*fantasy.ObjectResponse, error) {
+	switch g.objectMode {
+	case fantasy.ObjectModeText:
+		return object.GenerateWithText(ctx, g, call)
+	case fantasy.ObjectModeTool:
+		return object.GenerateWithTool(ctx, g, call)
+	default:
+		return g.generateObjectWithJSONMode(ctx, call)
+	}
+}
+
+// StreamObject implements fantasy.LanguageModel.
+func (g *languageModel) StreamObject(ctx context.Context, call fantasy.ObjectCall) (fantasy.ObjectStreamResponse, error) {
+	switch g.objectMode {
+	case fantasy.ObjectModeTool:
+		return object.StreamWithTool(ctx, g, call)
+	case fantasy.ObjectModeText:
+		return object.StreamWithText(ctx, g, call)
+	default:
+		return g.streamObjectWithJSONMode(ctx, call)
+	}
+}
+
+func (g *languageModel) generateObjectWithJSONMode(ctx context.Context, call fantasy.ObjectCall) (*fantasy.ObjectResponse, error) {
+	// Convert our Schema to Google's JSON Schema format
+	jsonSchemaMap := schema.ToMap(call.Schema)
+
+	// Build request using prepareParams
+	fantasyCall := fantasy.Call{
+		Prompt:           call.Prompt,
+		MaxOutputTokens:  call.MaxOutputTokens,
+		Temperature:      call.Temperature,
+		TopP:             call.TopP,
+		TopK:             call.TopK,
+		PresencePenalty:  call.PresencePenalty,
+		FrequencyPenalty: call.FrequencyPenalty,
+		ProviderOptions:  call.ProviderOptions,
+	}
+
+	config, contents, warnings, err := g.prepareParams(fantasyCall)
+	if err != nil {
+		return nil, err
+	}
+
+	// Set ResponseMIMEType and ResponseJsonSchema for structured output
+	config.ResponseMIMEType = "application/json"
+	config.ResponseJsonSchema = jsonSchemaMap
+
+	lastMessage, history, ok := slice.Pop(contents)
+	if !ok {
+		return nil, errors.New("no messages to send")
+	}
+
+	chat, err := g.client.Chats.Create(ctx, g.modelID, config, history)
+	if err != nil {
+		return nil, err
+	}
+
+	response, err := chat.SendMessage(ctx, depointerSlice(lastMessage.Parts)...)
+	if err != nil {
+		return nil, toProviderErr(err)
+	}
+
+	mappedResponse, err := g.mapResponse(response, warnings)
+	if err != nil {
+		return nil, err
+	}
+
+	jsonText := mappedResponse.Content.Text()
+	if jsonText == "" {
+		return nil, &fantasy.NoObjectGeneratedError{
+			RawText:      "",
+			ParseError:   fmt.Errorf("no text content in response"),
+			Usage:        mappedResponse.Usage,
+			FinishReason: mappedResponse.FinishReason,
+		}
+	}
+
+	// Parse and validate
+	var obj any
+	if call.RepairText != nil {
+		obj, err = schema.ParseAndValidateWithRepair(ctx, jsonText, call.Schema, call.RepairText)
+	} else {
+		obj, err = schema.ParseAndValidate(jsonText, call.Schema)
+	}
+
+	if err != nil {
+		// Add usage info to error
+		if nogErr, ok := err.(*fantasy.NoObjectGeneratedError); ok {
+			nogErr.Usage = mappedResponse.Usage
+			nogErr.FinishReason = mappedResponse.FinishReason
+		}
+		return nil, err
+	}
+
+	return &fantasy.ObjectResponse{
+		Object:           obj,
+		RawText:          jsonText,
+		Usage:            mappedResponse.Usage,
+		FinishReason:     mappedResponse.FinishReason,
+		Warnings:         warnings,
+		ProviderMetadata: mappedResponse.ProviderMetadata,
+	}, nil
+}
+
+func (g *languageModel) streamObjectWithJSONMode(ctx context.Context, call fantasy.ObjectCall) (fantasy.ObjectStreamResponse, error) {
+	// Convert our Schema to Google's JSON Schema format
+	jsonSchemaMap := schema.ToMap(call.Schema)
+
+	// Build request using prepareParams
+	fantasyCall := fantasy.Call{
+		Prompt:           call.Prompt,
+		MaxOutputTokens:  call.MaxOutputTokens,
+		Temperature:      call.Temperature,
+		TopP:             call.TopP,
+		TopK:             call.TopK,
+		PresencePenalty:  call.PresencePenalty,
+		FrequencyPenalty: call.FrequencyPenalty,
+		ProviderOptions:  call.ProviderOptions,
+	}
+
+	config, contents, warnings, err := g.prepareParams(fantasyCall)
+	if err != nil {
+		return nil, err
+	}
+
+	// Set ResponseMIMEType and ResponseJsonSchema for structured output
+	config.ResponseMIMEType = "application/json"
+	config.ResponseJsonSchema = jsonSchemaMap
+
+	lastMessage, history, ok := slice.Pop(contents)
+	if !ok {
+		return nil, errors.New("no messages to send")
+	}
+
+	chat, err := g.client.Chats.Create(ctx, g.modelID, config, history)
+	if err != nil {
+		return nil, err
+	}
+
+	return func(yield func(fantasy.ObjectStreamPart) bool) {
+		if len(warnings) > 0 {
+			if !yield(fantasy.ObjectStreamPart{
+				Type:     fantasy.ObjectStreamPartTypeObject,
+				Warnings: warnings,
+			}) {
+				return
+			}
+		}
+
+		var accumulated string
+		var lastParsedObject any
+		var usage *fantasy.Usage
+		var lastFinishReason fantasy.FinishReason
+		var streamErr error
+
+		for resp, err := range chat.SendMessageStream(ctx, depointerSlice(lastMessage.Parts)...) {
+			if err != nil {
+				streamErr = toProviderErr(err)
+				yield(fantasy.ObjectStreamPart{
+					Type:  fantasy.ObjectStreamPartTypeError,
+					Error: streamErr,
+				})
+				return
+			}
+
+			if len(resp.Candidates) > 0 && resp.Candidates[0].Content != nil {
+				for _, part := range resp.Candidates[0].Content.Parts {
+					if part.Text != "" && !part.Thought {
+						accumulated += part.Text
+
+						// Try to parse the accumulated text
+						obj, state, parseErr := schema.ParsePartialJSON(accumulated)
+
+						// If we successfully parsed, validate and emit
+						if state == schema.ParseStateSuccessful || state == schema.ParseStateRepaired {
+							if err := schema.ValidateAgainstSchema(obj, call.Schema); err == nil {
+								// Only emit if object is different from last
+								if !reflect.DeepEqual(obj, lastParsedObject) {
+									if !yield(fantasy.ObjectStreamPart{
+										Type:   fantasy.ObjectStreamPartTypeObject,
+										Object: obj,
+									}) {
+										return
+									}
+									lastParsedObject = obj
+								}
+							}
+						}
+
+						// If parsing failed and we have a repair function, try it
+						if state == schema.ParseStateFailed && call.RepairText != nil {
+							repairedText, repairErr := call.RepairText(ctx, accumulated, parseErr)
+							if repairErr == nil {
+								obj2, state2, _ := schema.ParsePartialJSON(repairedText)
+								if (state2 == schema.ParseStateSuccessful || state2 == schema.ParseStateRepaired) &&
+									schema.ValidateAgainstSchema(obj2, call.Schema) == nil {
+									if !reflect.DeepEqual(obj2, lastParsedObject) {
+										if !yield(fantasy.ObjectStreamPart{
+											Type:   fantasy.ObjectStreamPartTypeObject,
+											Object: obj2,
+										}) {
+											return
+										}
+										lastParsedObject = obj2
+									}
+								}
+							}
+						}
+					}
+				}
+			}
+
+			// we need to make sure that there is actual tokendata
+			if resp.UsageMetadata != nil && resp.UsageMetadata.TotalTokenCount != 0 {
+				currentUsage := mapUsage(resp.UsageMetadata)
+				if usage == nil {
+					usage = &currentUsage
+				} else {
+					usage.OutputTokens += currentUsage.OutputTokens
+					usage.ReasoningTokens += currentUsage.ReasoningTokens
+					usage.CacheReadTokens += currentUsage.CacheReadTokens
+				}
+			}
+
+			if len(resp.Candidates) > 0 && resp.Candidates[0].FinishReason != "" {
+				lastFinishReason = mapFinishReason(resp.Candidates[0].FinishReason)
+			}
+		}
+
+		// Final validation and emit
+		if streamErr == nil && lastParsedObject != nil {
+			finishReason := lastFinishReason
+			if finishReason == "" {
+				finishReason = fantasy.FinishReasonStop
+			}
+
+			yield(fantasy.ObjectStreamPart{
+				Type:         fantasy.ObjectStreamPartTypeFinish,
+				Usage:        *usage,
+				FinishReason: finishReason,
+			})
+		} else if streamErr == nil && lastParsedObject == nil {
+			// No object was generated
+			finalUsage := fantasy.Usage{}
+			if usage != nil {
+				finalUsage = *usage
+			}
+			yield(fantasy.ObjectStreamPart{
+				Type: fantasy.ObjectStreamPartTypeError,
+				Error: &fantasy.NoObjectGeneratedError{
+					RawText:      accumulated,
+					ParseError:   fmt.Errorf("no valid object generated in stream"),
+					Usage:        finalUsage,
+					FinishReason: lastFinishReason,
+				},
+			})
+		}
+	}, nil
+}
+
 func toGoogleTools(tools []fantasy.Tool, toolChoice *fantasy.ToolChoice) (googleTools []*genai.FunctionDeclaration, googleToolChoice *genai.ToolConfig, warnings []fantasy.CallWarning) {
 	for _, tool := range tools {
 		if tool.GetType() == fantasy.ToolTypeFunction {

providers/openai/language_model.go šŸ”—

@@ -4,10 +4,14 @@ import (
 	"context"
 	"encoding/json"
 	"errors"
+	"fmt"
 	"io"
+	"reflect"
 	"strings"
 
 	"charm.land/fantasy"
+	"charm.land/fantasy/object"
+	"charm.land/fantasy/schema"
 	xjson "github.com/charmbracelet/x/json"
 	"github.com/google/uuid"
 	"github.com/openai/openai-go/v2"
@@ -19,6 +23,7 @@ type languageModel struct {
 	provider                   string
 	modelID                    string
 	client                     openai.Client
+	objectMode                 fantasy.ObjectMode
 	prepareCallFunc            LanguageModelPrepareCallFunc
 	mapFinishReasonFunc        LanguageModelMapFinishReasonFunc
 	extraContentFunc           LanguageModelExtraContentFunc
@@ -81,11 +86,23 @@ func WithLanguageModelToPromptFunc(fn LanguageModelToPromptFunc) LanguageModelOp
 	}
 }
 
+// WithLanguageModelObjectMode sets the object generation mode.
+func WithLanguageModelObjectMode(om fantasy.ObjectMode) LanguageModelOption {
+	return func(l *languageModel) {
+		// not supported
+		if om == fantasy.ObjectModeJSON {
+			om = fantasy.ObjectModeAuto
+		}
+		l.objectMode = om
+	}
+}
+
 func newLanguageModel(modelID string, provider string, client openai.Client, opts ...LanguageModelOption) languageModel {
 	model := languageModel{
 		modelID:                    modelID,
 		provider:                   provider,
 		client:                     client,
+		objectMode:                 fantasy.ObjectModeAuto,
 		prepareCallFunc:            DefaultPrepareCallFunc,
 		mapFinishReasonFunc:        DefaultMapFinishReasonFunc,
 		usageFunc:                  DefaultUsageFunc,
@@ -252,13 +269,12 @@ func (o languageModel) Generate(ctx context.Context, call fantasy.Call) (*fantas
 	for _, tc := range choice.Message.ToolCalls {
 		toolCallID := tc.ID
 		content = append(content, fantasy.ToolCallContent{
-			ProviderExecuted: false, // TODO: update when handling other tools
+			ProviderExecuted: false,
 			ToolCallID:       toolCallID,
 			ToolName:         tc.Function.Name,
 			Input:            tc.Function.Arguments,
 		})
 	}
-	// Handle annotations/citations
 	for _, annotation := range choice.Message.Annotations {
 		if annotation.Type == "url_citation" {
 			content = append(content, fantasy.SourceContent{
@@ -302,7 +318,6 @@ func (o languageModel) Stream(ctx context.Context, call fantasy.Call) (fantasy.S
 	isActiveText := false
 	toolCalls := make(map[int64]streamToolCall)
 
-	// Build provider metadata for streaming
 	providerMetadata := fantasy.ProviderMetadata{
 		Name: &ProviderMetadata{},
 	}
@@ -395,7 +410,6 @@ func (o languageModel) Stream(ctx context.Context, call fantasy.Call) (fantasy.S
 								toolCalls[toolCallDelta.Index] = existingToolCall
 							}
 						} else {
-							// Does not exist
 							var err error
 							if toolCallDelta.Type != "function" {
 								err = &fantasy.Error{Title: "invalid provider response", Message: "expected 'function' type."}
@@ -470,7 +484,6 @@ func (o languageModel) Stream(ctx context.Context, call fantasy.Call) (fantasy.S
 				}
 			}
 
-			// Check for annotations in the delta's raw JSON
 			for _, choice := range chunk.Choices {
 				if annotations := parseAnnotationsFromDelta(choice.Delta); len(annotations) > 0 {
 					for _, annotation := range annotations {
@@ -491,7 +504,6 @@ func (o languageModel) Stream(ctx context.Context, call fantasy.Call) (fantasy.S
 		}
 		err := stream.Err()
 		if err == nil || errors.Is(err, io.EOF) {
-			// finished
 			if isActiveText {
 				isActiveText = false
 				if !yield(fantasy.StreamPart{
@@ -504,10 +516,8 @@ func (o languageModel) Stream(ctx context.Context, call fantasy.Call) (fantasy.S
 
 			if len(acc.Choices) > 0 {
 				choice := acc.Choices[0]
-				// Add logprobs if available
 				providerMetadata = o.streamProviderMetadataFunc(choice, providerMetadata)
 
-				// Handle annotations/citations from accumulated response
 				for _, annotation := range choice.Message.Annotations {
 					if annotation.Type == "url_citation" {
 						if !yield(fantasy.StreamPart{
@@ -585,7 +595,6 @@ func toOpenAiTools(tools []fantasy.Tool, toolChoice *fantasy.ToolChoice) (openAi
 			continue
 		}
 
-		// TODO: handle provider tool calls
 		warnings = append(warnings, fantasy.CallWarning{
 			Type:    fantasy.CallWarningTypeUnsupportedTool,
 			Tool:    tool,
@@ -650,3 +659,277 @@ func parseAnnotationsFromDelta(delta openai.ChatCompletionChunkChoiceDelta) []op
 
 	return annotations
 }
+
+// GenerateObject implements fantasy.LanguageModel.
+func (o languageModel) GenerateObject(ctx context.Context, call fantasy.ObjectCall) (*fantasy.ObjectResponse, error) {
+	switch o.objectMode {
+	case fantasy.ObjectModeText:
+		return object.GenerateWithText(ctx, o, call)
+	case fantasy.ObjectModeTool:
+		return object.GenerateWithTool(ctx, o, call)
+	default:
+		return o.generateObjectWithJSONMode(ctx, call)
+	}
+}
+
+// StreamObject implements fantasy.LanguageModel.
+func (o languageModel) StreamObject(ctx context.Context, call fantasy.ObjectCall) (fantasy.ObjectStreamResponse, error) {
+	switch o.objectMode {
+	case fantasy.ObjectModeTool:
+		return object.StreamWithTool(ctx, o, call)
+	case fantasy.ObjectModeText:
+		return object.StreamWithText(ctx, o, call)
+	default:
+		return o.streamObjectWithJSONMode(ctx, call)
+	}
+}
+
+func (o languageModel) generateObjectWithJSONMode(ctx context.Context, call fantasy.ObjectCall) (*fantasy.ObjectResponse, error) {
+	jsonSchemaMap := schema.ToMap(call.Schema)
+
+	addAdditionalPropertiesFalse(jsonSchemaMap)
+
+	schemaName := call.SchemaName
+	if schemaName == "" {
+		schemaName = "response"
+	}
+
+	fantasyCall := fantasy.Call{
+		Prompt:           call.Prompt,
+		MaxOutputTokens:  call.MaxOutputTokens,
+		Temperature:      call.Temperature,
+		TopP:             call.TopP,
+		PresencePenalty:  call.PresencePenalty,
+		FrequencyPenalty: call.FrequencyPenalty,
+		ProviderOptions:  call.ProviderOptions,
+	}
+
+	params, warnings, err := o.prepareParams(fantasyCall)
+	if err != nil {
+		return nil, err
+	}
+
+	params.ResponseFormat = openai.ChatCompletionNewParamsResponseFormatUnion{
+		OfJSONSchema: &shared.ResponseFormatJSONSchemaParam{
+			JSONSchema: shared.ResponseFormatJSONSchemaJSONSchemaParam{
+				Name:        schemaName,
+				Description: param.NewOpt(call.SchemaDescription),
+				Schema:      jsonSchemaMap,
+				Strict:      param.NewOpt(true),
+			},
+		},
+	}
+
+	response, err := o.client.Chat.Completions.New(ctx, *params)
+	if err != nil {
+		return nil, toProviderErr(err)
+	}
+
+	if len(response.Choices) == 0 {
+		usage, _ := o.usageFunc(*response)
+		return nil, &fantasy.NoObjectGeneratedError{
+			RawText:      "",
+			ParseError:   fmt.Errorf("no choices in response"),
+			Usage:        usage,
+			FinishReason: fantasy.FinishReasonUnknown,
+		}
+	}
+
+	choice := response.Choices[0]
+	jsonText := choice.Message.Content
+
+	var obj any
+	if call.RepairText != nil {
+		obj, err = schema.ParseAndValidateWithRepair(ctx, jsonText, call.Schema, call.RepairText)
+	} else {
+		obj, err = schema.ParseAndValidate(jsonText, call.Schema)
+	}
+
+	usage, _ := o.usageFunc(*response)
+	finishReason := o.mapFinishReasonFunc(choice.FinishReason)
+
+	if err != nil {
+		if nogErr, ok := err.(*fantasy.NoObjectGeneratedError); ok {
+			nogErr.Usage = usage
+			nogErr.FinishReason = finishReason
+		}
+		return nil, err
+	}
+
+	return &fantasy.ObjectResponse{
+		Object:       obj,
+		RawText:      jsonText,
+		Usage:        usage,
+		FinishReason: finishReason,
+		Warnings:     warnings,
+	}, nil
+}
+
+func (o languageModel) streamObjectWithJSONMode(ctx context.Context, call fantasy.ObjectCall) (fantasy.ObjectStreamResponse, error) {
+	jsonSchemaMap := schema.ToMap(call.Schema)
+
+	addAdditionalPropertiesFalse(jsonSchemaMap)
+
+	schemaName := call.SchemaName
+	if schemaName == "" {
+		schemaName = "response"
+	}
+
+	fantasyCall := fantasy.Call{
+		Prompt:           call.Prompt,
+		MaxOutputTokens:  call.MaxOutputTokens,
+		Temperature:      call.Temperature,
+		TopP:             call.TopP,
+		PresencePenalty:  call.PresencePenalty,
+		FrequencyPenalty: call.FrequencyPenalty,
+		ProviderOptions:  call.ProviderOptions,
+	}
+
+	params, warnings, err := o.prepareParams(fantasyCall)
+	if err != nil {
+		return nil, err
+	}
+
+	params.ResponseFormat = openai.ChatCompletionNewParamsResponseFormatUnion{
+		OfJSONSchema: &shared.ResponseFormatJSONSchemaParam{
+			JSONSchema: shared.ResponseFormatJSONSchemaJSONSchemaParam{
+				Name:        schemaName,
+				Description: param.NewOpt(call.SchemaDescription),
+				Schema:      jsonSchemaMap,
+				Strict:      param.NewOpt(true),
+			},
+		},
+	}
+
+	params.StreamOptions = openai.ChatCompletionStreamOptionsParam{
+		IncludeUsage: openai.Bool(true),
+	}
+
+	stream := o.client.Chat.Completions.NewStreaming(ctx, *params)
+
+	return func(yield func(fantasy.ObjectStreamPart) bool) {
+		if len(warnings) > 0 {
+			if !yield(fantasy.ObjectStreamPart{
+				Type:     fantasy.ObjectStreamPartTypeObject,
+				Warnings: warnings,
+			}) {
+				return
+			}
+		}
+
+		var accumulated string
+		var lastParsedObject any
+		var usage fantasy.Usage
+		var finishReason fantasy.FinishReason
+		var providerMetadata fantasy.ProviderMetadata
+		var streamErr error
+
+		for stream.Next() {
+			chunk := stream.Current()
+
+			// Update usage
+			usage, providerMetadata = o.streamUsageFunc(chunk, make(map[string]any), providerMetadata)
+
+			if len(chunk.Choices) == 0 {
+				continue
+			}
+
+			choice := chunk.Choices[0]
+			if choice.FinishReason != "" {
+				finishReason = o.mapFinishReasonFunc(choice.FinishReason)
+			}
+
+			if choice.Delta.Content != "" {
+				accumulated += choice.Delta.Content
+
+				obj, state, parseErr := schema.ParsePartialJSON(accumulated)
+
+				if state == schema.ParseStateSuccessful || state == schema.ParseStateRepaired {
+					if err := schema.ValidateAgainstSchema(obj, call.Schema); err == nil {
+						if !reflect.DeepEqual(obj, lastParsedObject) {
+							if !yield(fantasy.ObjectStreamPart{
+								Type:   fantasy.ObjectStreamPartTypeObject,
+								Object: obj,
+							}) {
+								return
+							}
+							lastParsedObject = obj
+						}
+					}
+				}
+
+				if state == schema.ParseStateFailed && call.RepairText != nil {
+					repairedText, repairErr := call.RepairText(ctx, accumulated, parseErr)
+					if repairErr == nil {
+						obj2, state2, _ := schema.ParsePartialJSON(repairedText)
+						if (state2 == schema.ParseStateSuccessful || state2 == schema.ParseStateRepaired) &&
+							schema.ValidateAgainstSchema(obj2, call.Schema) == nil {
+							if !reflect.DeepEqual(obj2, lastParsedObject) {
+								if !yield(fantasy.ObjectStreamPart{
+									Type:   fantasy.ObjectStreamPartTypeObject,
+									Object: obj2,
+								}) {
+									return
+								}
+								lastParsedObject = obj2
+							}
+						}
+					}
+				}
+			}
+		}
+
+		err := stream.Err()
+		if err != nil && !errors.Is(err, io.EOF) {
+			streamErr = toProviderErr(err)
+			yield(fantasy.ObjectStreamPart{
+				Type:  fantasy.ObjectStreamPartTypeError,
+				Error: streamErr,
+			})
+			return
+		}
+
+		if lastParsedObject != nil {
+			yield(fantasy.ObjectStreamPart{
+				Type:             fantasy.ObjectStreamPartTypeFinish,
+				Usage:            usage,
+				FinishReason:     finishReason,
+				ProviderMetadata: providerMetadata,
+			})
+		} else {
+			yield(fantasy.ObjectStreamPart{
+				Type: fantasy.ObjectStreamPartTypeError,
+				Error: &fantasy.NoObjectGeneratedError{
+					RawText:      accumulated,
+					ParseError:   fmt.Errorf("no valid object generated in stream"),
+					Usage:        usage,
+					FinishReason: finishReason,
+				},
+			})
+		}
+	}, nil
+}
+
+// addAdditionalPropertiesFalse recursively adds "additionalProperties": false to all object schemas.
+// This is required by OpenAI's strict mode for structured outputs.
+func addAdditionalPropertiesFalse(schema map[string]any) {
+	if schema["type"] == "object" {
+		if _, hasAdditional := schema["additionalProperties"]; !hasAdditional {
+			schema["additionalProperties"] = false
+		}
+
+		// Recursively process nested properties
+		if properties, ok := schema["properties"].(map[string]any); ok {
+			for _, propValue := range properties {
+				if propSchema, ok := propValue.(map[string]any); ok {
+					addAdditionalPropertiesFalse(propSchema)
+				}
+			}
+		}
+	}
+
+	// Handle array items
+	if items, ok := schema["items"].(map[string]any); ok {
+		addAdditionalPropertiesFalse(items)
+	}
+}

providers/openai/openai.go šŸ”—

@@ -32,6 +32,7 @@ type options struct {
 	headers              map[string]string
 	client               option.HTTPClient
 	sdkOptions           []option.RequestOption
+	objectMode           fantasy.ObjectMode
 	languageModelOptions []LanguageModelOption
 }
 
@@ -131,6 +132,17 @@ func WithUseResponsesAPI() Option {
 	}
 }
 
+// WithObjectMode sets the object generation mode.
+func WithObjectMode(om fantasy.ObjectMode) Option {
+	return func(o *options) {
+		// not supported
+		if om == fantasy.ObjectModeJSON {
+			om = fantasy.ObjectModeAuto
+		}
+		o.objectMode = om
+	}
+}
+
 // LanguageModel implements fantasy.Provider.
 func (o *provider) LanguageModel(_ context.Context, modelID string) (fantasy.LanguageModel, error) {
 	openaiClientOptions := make([]option.RequestOption, 0, 5+len(o.options.headers)+len(o.options.sdkOptions))
@@ -156,9 +168,16 @@ func (o *provider) LanguageModel(_ context.Context, modelID string) (fantasy.Lan
 	client := openai.NewClient(openaiClientOptions...)
 
 	if o.options.useResponsesAPI && IsResponsesModel(modelID) {
-		return newResponsesLanguageModel(modelID, o.options.name, client), nil
+		// Not supported for responses API
+		objectMode := o.options.objectMode
+		if objectMode == fantasy.ObjectModeJSON {
+			objectMode = fantasy.ObjectModeAuto
+		}
+		return newResponsesLanguageModel(modelID, o.options.name, client, objectMode), nil
 	}
 
+	o.options.languageModelOptions = append(o.options.languageModelOptions, WithLanguageModelObjectMode(o.options.objectMode))
+
 	return newLanguageModel(
 		modelID,
 		o.options.name,

providers/openai/responses_language_model.go šŸ”—

@@ -5,9 +5,12 @@ import (
 	"encoding/base64"
 	"encoding/json"
 	"fmt"
+	"reflect"
 	"strings"
 
 	"charm.land/fantasy"
+	"charm.land/fantasy/object"
+	"charm.land/fantasy/schema"
 	"github.com/google/uuid"
 	"github.com/openai/openai-go/v2"
 	"github.com/openai/openai-go/v2/packages/param"
@@ -18,18 +21,20 @@ import (
 const topLogprobsMax = 20
 
 type responsesLanguageModel struct {
-	provider string
-	modelID  string
-	client   openai.Client
+	provider   string
+	modelID    string
+	client     openai.Client
+	objectMode fantasy.ObjectMode
 }
 
 // newResponsesLanguageModel implements a responses api model
 // INFO: (kujtim) currently we do not support stored parameter we default it to false.
-func newResponsesLanguageModel(modelID string, provider string, client openai.Client) responsesLanguageModel {
+func newResponsesLanguageModel(modelID string, provider string, client openai.Client, objectMode fantasy.ObjectMode) responsesLanguageModel {
 	return responsesLanguageModel{
-		modelID:  modelID,
-		provider: provider,
-		client:   client,
+		modelID:    modelID,
+		provider:   provider,
+		client:     client,
+		objectMode: objectMode,
 	}
 }
 
@@ -1032,3 +1037,293 @@ type ongoingToolCall struct {
 type reasoningState struct {
 	metadata *ResponsesReasoningMetadata
 }
+
+// GenerateObject implements fantasy.LanguageModel.
+func (o responsesLanguageModel) GenerateObject(ctx context.Context, call fantasy.ObjectCall) (*fantasy.ObjectResponse, error) {
+	switch o.objectMode {
+	case fantasy.ObjectModeText:
+		return object.GenerateWithText(ctx, o, call)
+	case fantasy.ObjectModeTool:
+		return object.GenerateWithTool(ctx, o, call)
+	default:
+		return o.generateObjectWithJSONMode(ctx, call)
+	}
+}
+
+// StreamObject implements fantasy.LanguageModel.
+func (o responsesLanguageModel) StreamObject(ctx context.Context, call fantasy.ObjectCall) (fantasy.ObjectStreamResponse, error) {
+	switch o.objectMode {
+	case fantasy.ObjectModeTool:
+		return object.StreamWithTool(ctx, o, call)
+	case fantasy.ObjectModeText:
+		return object.StreamWithText(ctx, o, call)
+	default:
+		return o.streamObjectWithJSONMode(ctx, call)
+	}
+}
+
+func (o responsesLanguageModel) generateObjectWithJSONMode(ctx context.Context, call fantasy.ObjectCall) (*fantasy.ObjectResponse, error) {
+	// Convert our Schema to OpenAI's JSON Schema format
+	jsonSchemaMap := schema.ToMap(call.Schema)
+
+	// Add additionalProperties: false recursively for strict mode (OpenAI requirement)
+	addAdditionalPropertiesFalse(jsonSchemaMap)
+
+	schemaName := call.SchemaName
+	if schemaName == "" {
+		schemaName = "response"
+	}
+
+	// Build request using prepareParams
+	fantasyCall := fantasy.Call{
+		Prompt:           call.Prompt,
+		MaxOutputTokens:  call.MaxOutputTokens,
+		Temperature:      call.Temperature,
+		TopP:             call.TopP,
+		PresencePenalty:  call.PresencePenalty,
+		FrequencyPenalty: call.FrequencyPenalty,
+		ProviderOptions:  call.ProviderOptions,
+	}
+
+	params, warnings := o.prepareParams(fantasyCall)
+
+	// Add structured output via Text.Format field
+	params.Text = responses.ResponseTextConfigParam{
+		Format: responses.ResponseFormatTextConfigParamOfJSONSchema(schemaName, jsonSchemaMap),
+	}
+
+	// Make request
+	response, err := o.client.Responses.New(ctx, *params)
+	if err != nil {
+		return nil, toProviderErr(err)
+	}
+
+	if response.Error.Message != "" {
+		return nil, &fantasy.Error{
+			Title:   "provider error",
+			Message: fmt.Sprintf("%s (code: %s)", response.Error.Message, response.Error.Code),
+		}
+	}
+
+	// Extract JSON text from response
+	var jsonText string
+	for _, outputItem := range response.Output {
+		if outputItem.Type == "message" {
+			for _, contentPart := range outputItem.Content {
+				if contentPart.Type == "output_text" {
+					jsonText = contentPart.Text
+					break
+				}
+			}
+		}
+	}
+
+	if jsonText == "" {
+		usage := fantasy.Usage{
+			InputTokens:  response.Usage.InputTokens,
+			OutputTokens: response.Usage.OutputTokens,
+			TotalTokens:  response.Usage.InputTokens + response.Usage.OutputTokens,
+		}
+		finishReason := mapResponsesFinishReason(response.IncompleteDetails.Reason, false)
+		return nil, &fantasy.NoObjectGeneratedError{
+			RawText:      "",
+			ParseError:   fmt.Errorf("no text content in response"),
+			Usage:        usage,
+			FinishReason: finishReason,
+		}
+	}
+
+	// Parse and validate
+	var obj any
+	if call.RepairText != nil {
+		obj, err = schema.ParseAndValidateWithRepair(ctx, jsonText, call.Schema, call.RepairText)
+	} else {
+		obj, err = schema.ParseAndValidate(jsonText, call.Schema)
+	}
+
+	usage := fantasy.Usage{
+		InputTokens:  response.Usage.InputTokens,
+		OutputTokens: response.Usage.OutputTokens,
+		TotalTokens:  response.Usage.InputTokens + response.Usage.OutputTokens,
+	}
+	if response.Usage.OutputTokensDetails.ReasoningTokens != 0 {
+		usage.ReasoningTokens = response.Usage.OutputTokensDetails.ReasoningTokens
+	}
+	if response.Usage.InputTokensDetails.CachedTokens != 0 {
+		usage.CacheReadTokens = response.Usage.InputTokensDetails.CachedTokens
+	}
+
+	finishReason := mapResponsesFinishReason(response.IncompleteDetails.Reason, false)
+
+	if err != nil {
+		// Add usage info to error
+		if nogErr, ok := err.(*fantasy.NoObjectGeneratedError); ok {
+			nogErr.Usage = usage
+			nogErr.FinishReason = finishReason
+		}
+		return nil, err
+	}
+
+	return &fantasy.ObjectResponse{
+		Object:       obj,
+		RawText:      jsonText,
+		Usage:        usage,
+		FinishReason: finishReason,
+		Warnings:     warnings,
+	}, nil
+}
+
+func (o responsesLanguageModel) streamObjectWithJSONMode(ctx context.Context, call fantasy.ObjectCall) (fantasy.ObjectStreamResponse, error) {
+	// Convert our Schema to OpenAI's JSON Schema format
+	jsonSchemaMap := schema.ToMap(call.Schema)
+
+	// Add additionalProperties: false recursively for strict mode (OpenAI requirement)
+	addAdditionalPropertiesFalse(jsonSchemaMap)
+
+	schemaName := call.SchemaName
+	if schemaName == "" {
+		schemaName = "response"
+	}
+
+	// Build request using prepareParams
+	fantasyCall := fantasy.Call{
+		Prompt:           call.Prompt,
+		MaxOutputTokens:  call.MaxOutputTokens,
+		Temperature:      call.Temperature,
+		TopP:             call.TopP,
+		PresencePenalty:  call.PresencePenalty,
+		FrequencyPenalty: call.FrequencyPenalty,
+		ProviderOptions:  call.ProviderOptions,
+	}
+
+	params, warnings := o.prepareParams(fantasyCall)
+
+	// Add structured output via Text.Format field
+	params.Text = responses.ResponseTextConfigParam{
+		Format: responses.ResponseFormatTextConfigParamOfJSONSchema(schemaName, jsonSchemaMap),
+	}
+
+	stream := o.client.Responses.NewStreaming(ctx, *params)
+
+	return func(yield func(fantasy.ObjectStreamPart) bool) {
+		if len(warnings) > 0 {
+			if !yield(fantasy.ObjectStreamPart{
+				Type:     fantasy.ObjectStreamPartTypeObject,
+				Warnings: warnings,
+			}) {
+				return
+			}
+		}
+
+		var accumulated string
+		var lastParsedObject any
+		var usage fantasy.Usage
+		var finishReason fantasy.FinishReason
+		var streamErr error
+		hasFunctionCall := false
+
+		for stream.Next() {
+			event := stream.Current()
+
+			switch event.Type {
+			case "response.output_text.delta":
+				textDelta := event.AsResponseOutputTextDelta()
+				accumulated += textDelta.Delta
+
+				// Try to parse the accumulated text
+				obj, state, parseErr := schema.ParsePartialJSON(accumulated)
+
+				// If we successfully parsed, validate and emit
+				if state == schema.ParseStateSuccessful || state == schema.ParseStateRepaired {
+					if err := schema.ValidateAgainstSchema(obj, call.Schema); err == nil {
+						// Only emit if object is different from last
+						if !reflect.DeepEqual(obj, lastParsedObject) {
+							if !yield(fantasy.ObjectStreamPart{
+								Type:   fantasy.ObjectStreamPartTypeObject,
+								Object: obj,
+							}) {
+								return
+							}
+							lastParsedObject = obj
+						}
+					}
+				}
+
+				// If parsing failed and we have a repair function, try it
+				if state == schema.ParseStateFailed && call.RepairText != nil {
+					repairedText, repairErr := call.RepairText(ctx, accumulated, parseErr)
+					if repairErr == nil {
+						obj2, state2, _ := schema.ParsePartialJSON(repairedText)
+						if (state2 == schema.ParseStateSuccessful || state2 == schema.ParseStateRepaired) &&
+							schema.ValidateAgainstSchema(obj2, call.Schema) == nil {
+							if !reflect.DeepEqual(obj2, lastParsedObject) {
+								if !yield(fantasy.ObjectStreamPart{
+									Type:   fantasy.ObjectStreamPartTypeObject,
+									Object: obj2,
+								}) {
+									return
+								}
+								lastParsedObject = obj2
+							}
+						}
+					}
+				}
+
+			case "response.completed", "response.incomplete":
+				completed := event.AsResponseCompleted()
+				finishReason = mapResponsesFinishReason(completed.Response.IncompleteDetails.Reason, hasFunctionCall)
+				usage = fantasy.Usage{
+					InputTokens:  completed.Response.Usage.InputTokens,
+					OutputTokens: completed.Response.Usage.OutputTokens,
+					TotalTokens:  completed.Response.Usage.InputTokens + completed.Response.Usage.OutputTokens,
+				}
+				if completed.Response.Usage.OutputTokensDetails.ReasoningTokens != 0 {
+					usage.ReasoningTokens = completed.Response.Usage.OutputTokensDetails.ReasoningTokens
+				}
+				if completed.Response.Usage.InputTokensDetails.CachedTokens != 0 {
+					usage.CacheReadTokens = completed.Response.Usage.InputTokensDetails.CachedTokens
+				}
+
+			case "error":
+				errorEvent := event.AsError()
+				streamErr = fmt.Errorf("response error: %s (code: %s)", errorEvent.Message, errorEvent.Code)
+				if !yield(fantasy.ObjectStreamPart{
+					Type:  fantasy.ObjectStreamPartTypeError,
+					Error: streamErr,
+				}) {
+					return
+				}
+				return
+			}
+		}
+
+		err := stream.Err()
+		if err != nil {
+			yield(fantasy.ObjectStreamPart{
+				Type:  fantasy.ObjectStreamPartTypeError,
+				Error: toProviderErr(err),
+			})
+			return
+		}
+
+		// Final validation and emit
+		if streamErr == nil && lastParsedObject != nil {
+			yield(fantasy.ObjectStreamPart{
+				Type:         fantasy.ObjectStreamPartTypeFinish,
+				Usage:        usage,
+				FinishReason: finishReason,
+			})
+		} else if streamErr == nil && lastParsedObject == nil {
+			// No object was generated
+			yield(fantasy.ObjectStreamPart{
+				Type: fantasy.ObjectStreamPartTypeError,
+				Error: &fantasy.NoObjectGeneratedError{
+					RawText:      accumulated,
+					ParseError:   fmt.Errorf("no valid object generated in stream"),
+					Usage:        usage,
+					FinishReason: finishReason,
+				},
+			})
+		}
+	}, nil
+}

providers/openaicompat/openaicompat.go šŸ”—

@@ -11,6 +11,7 @@ type options struct {
 	openaiOptions        []openai.Option
 	languageModelOptions []openai.LanguageModelOption
 	sdkOptions           []option.RequestOption
+	objectMode           fantasy.ObjectMode
 }
 
 const (
@@ -33,15 +34,24 @@ func New(opts ...Option) (fantasy.Provider, error) {
 			openai.WithLanguageModelExtraContentFunc(ExtraContentFunc),
 			openai.WithLanguageModelToPromptFunc(ToPromptFunc),
 		},
+		objectMode: fantasy.ObjectModeTool, // Default to tool mode for openai-compat
 	}
 	for _, o := range opts {
 		o(&providerOptions)
 	}
 
+	// Handle object mode: convert unsupported modes to tool
+	// OpenAI-compat endpoints don't support native JSON mode, so we use tool or text
+	objectMode := providerOptions.objectMode
+	if objectMode == fantasy.ObjectModeAuto || objectMode == fantasy.ObjectModeJSON {
+		objectMode = fantasy.ObjectModeTool
+	}
+
 	providerOptions.openaiOptions = append(
 		providerOptions.openaiOptions,
 		openai.WithSDKOptions(providerOptions.sdkOptions...),
 		openai.WithLanguageModelOptions(providerOptions.languageModelOptions...),
+		openai.WithObjectMode(objectMode),
 	)
 	return openai.New(providerOptions.openaiOptions...)
 }
@@ -87,3 +97,13 @@ func WithSDKOptions(opts ...option.RequestOption) Option {
 		o.sdkOptions = append(o.sdkOptions, opts...)
 	}
 }
+
+// WithObjectMode sets the object generation mode for the OpenAI-compatible provider.
+// Supported modes: ObjectModeTool, ObjectModeText.
+// ObjectModeAuto and ObjectModeJSON are automatically converted to ObjectModeTool
+// since OpenAI-compatible endpoints typically don't support native JSON mode.
+func WithObjectMode(om fantasy.ObjectMode) Option {
+	return func(o *options) {
+		o.objectMode = om
+	}
+}

providers/openrouter/openrouter.go šŸ”—

@@ -12,6 +12,7 @@ import (
 type options struct {
 	openaiOptions        []openai.Option
 	languageModelOptions []openai.LanguageModelOption
+	objectMode           fantasy.ObjectMode
 }
 
 const (
@@ -39,12 +40,24 @@ func New(opts ...Option) (fantasy.Provider, error) {
 			openai.WithLanguageModelExtraContentFunc(languageModelExtraContent),
 			openai.WithLanguageModelToPromptFunc(languageModelToPrompt),
 		},
+		objectMode: fantasy.ObjectModeTool, // Default to tool mode for openrouter
 	}
 	for _, o := range opts {
 		o(&providerOptions)
 	}
 
-	providerOptions.openaiOptions = append(providerOptions.openaiOptions, openai.WithLanguageModelOptions(providerOptions.languageModelOptions...))
+	// Handle object mode: convert unsupported modes to tool
+	// OpenRouter doesn't support native JSON mode, so we use tool or text
+	objectMode := providerOptions.objectMode
+	if objectMode == fantasy.ObjectModeAuto || objectMode == fantasy.ObjectModeJSON {
+		objectMode = fantasy.ObjectModeTool
+	}
+
+	providerOptions.openaiOptions = append(
+		providerOptions.openaiOptions,
+		openai.WithLanguageModelOptions(providerOptions.languageModelOptions...),
+		openai.WithObjectMode(objectMode),
+	)
 	return openai.New(providerOptions.openaiOptions...)
 }
 
@@ -76,6 +89,16 @@ func WithHTTPClient(client option.HTTPClient) Option {
 	}
 }
 
+// WithObjectMode sets the object generation mode for the OpenRouter provider.
+// Supported modes: ObjectModeTool, ObjectModeText.
+// ObjectModeAuto and ObjectModeJSON are automatically converted to ObjectModeTool
+// since OpenRouter doesn't support native JSON mode.
+func WithObjectMode(om fantasy.ObjectMode) Option {
+	return func(o *options) {
+		o.objectMode = om
+	}
+}
+
 func structToMapJSON(s any) (map[string]any, error) {
 	var result map[string]any
 	jsonBytes, err := json.Marshal(s)

providertests/anthropic_test.go šŸ”—

@@ -99,6 +99,14 @@ func TestAnthropicThinkingWithCacheControl(t *testing.T) {
 	testThinking(t, pairs, testAnthropicThinking)
 }
 
+func TestAnthropicObjectGeneration(t *testing.T) {
+	var pairs []builderPair
+	for _, m := range anthropicTestModels {
+		pairs = append(pairs, builderPair{m.name, anthropicBuilder(m.model), nil, nil})
+	}
+	testObjectGeneration(t, pairs)
+}
+
 func testAnthropicThinking(t *testing.T, result *fantasy.AgentResult) {
 	reasoningContentCount := 0
 	signaturesCount := 0

providertests/google_test.go šŸ”—

@@ -55,6 +55,22 @@ func TestGoogleThinking(t *testing.T) {
 	testThinking(t, pairs, testGoogleThinking)
 }
 
+func TestGoogleObjectGeneration(t *testing.T) {
+	var pairs []builderPair
+	for _, m := range geminiTestModels {
+		pairs = append(pairs, builderPair{m.name, geminiBuilder(m.model), nil, nil})
+	}
+	testObjectGeneration(t, pairs)
+}
+
+func TestGoogleVertexObjectGeneration(t *testing.T) {
+	var pairs []builderPair
+	for _, m := range vertexTestModels {
+		pairs = append(pairs, builderPair{m.name, vertexBuilder(m.model), nil, nil})
+	}
+	testObjectGeneration(t, pairs)
+}
+
 func testGoogleThinking(t *testing.T, result *fantasy.AgentResult) {
 	reasoningContentCount := 0
 	// Test if we got the signature

providertests/object_test.go šŸ”—

@@ -0,0 +1,421 @@
+package providertests
+
+import (
+	"context"
+	"strings"
+	"testing"
+
+	"charm.land/fantasy"
+	"github.com/stretchr/testify/require"
+)
+
+// Object generation tests for providers.
+//
+// These test functions can be used to test structured object generation
+// (GenerateObject and StreamObject) for any provider implementation.
+//
+// Usage example:
+//
+//	func TestMyProviderObjectGeneration(t *testing.T) {
+//		var pairs []builderPair
+//		for _, m := range myTestModels {
+//			pairs = append(pairs, builderPair{m.name, myBuilder(m.model), nil, nil})
+//		}
+//		testObjectGeneration(t, pairs)
+//	}
+//
+// The tests cover:
+// - Simple object generation (flat schema with basic types)
+// - Complex object generation (nested objects and arrays)
+// - Streaming object generation (progressive updates)
+// - Object generation with custom repair functions
+
+// testObjectGeneration tests structured object generation for a provider.
+// It includes both non-streaming (GenerateObject) and streaming (StreamObject) tests.
+func testObjectGeneration(t *testing.T, pairs []builderPair) {
+	for _, pair := range pairs {
+		t.Run(pair.name, func(t *testing.T) {
+			testSimpleObject(t, pair)
+			testComplexObject(t, pair)
+		})
+	}
+}
+
+func testSimpleObject(t *testing.T, pair builderPair) {
+	// Define a simple schema for a person object
+	schema := fantasy.Schema{
+		Type: "object",
+		Properties: map[string]*fantasy.Schema{
+			"name": {
+				Type:        "string",
+				Description: "The person's name",
+			},
+			"age": {
+				Type:        "integer",
+				Description: "The person's age",
+			},
+			"city": {
+				Type:        "string",
+				Description: "The city where the person lives",
+			},
+		},
+		Required: []string{"name", "age", "city"},
+	}
+
+	checkResult := func(t *testing.T, obj any, rawText string, usage fantasy.Usage) {
+		require.NotNil(t, obj, "object should not be nil")
+		require.NotEmpty(t, rawText, "raw text should not be empty")
+		require.Greater(t, usage.TotalTokens, int64(0), "usage should be tracked")
+
+		// Validate structure
+		objMap, ok := obj.(map[string]any)
+		require.True(t, ok, "object should be a map")
+		require.Contains(t, objMap, "name")
+		require.Contains(t, objMap, "age")
+		require.Contains(t, objMap, "city")
+
+		// Validate types
+		name, ok := objMap["name"].(string)
+		require.True(t, ok, "name should be a string")
+		require.NotEmpty(t, name, "name should not be empty")
+
+		// Age could be float64 from JSON unmarshaling
+		age, ok := objMap["age"].(float64)
+		require.True(t, ok, "age should be a number")
+		require.Greater(t, age, 0.0, "age should be greater than 0")
+
+		city, ok := objMap["city"].(string)
+		require.True(t, ok, "city should be a string")
+		require.NotEmpty(t, city, "city should not be empty")
+	}
+
+	t.Run("simple object", func(t *testing.T) {
+		r := newRecorder(t)
+
+		languageModel, err := pair.builder(t, r)
+		require.NoError(t, err, "failed to build language model")
+
+		prompt := fantasy.Prompt{
+			fantasy.NewUserMessage("Generate information about a person named Alice who is 30 years old and lives in Paris."),
+		}
+
+		response, err := languageModel.GenerateObject(t.Context(), fantasy.ObjectCall{
+			Prompt:            prompt,
+			Schema:            schema,
+			SchemaName:        "Person",
+			SchemaDescription: "A person with name, age, and city",
+			MaxOutputTokens:   fantasy.Opt(int64(4000)),
+			ProviderOptions:   pair.providerOptions,
+		})
+		require.NoError(t, err, "failed to generate object")
+		require.NotNil(t, response, "response should not be nil")
+		checkResult(t, response.Object, response.RawText, response.Usage)
+	})
+
+	t.Run("simple object streaming", func(t *testing.T) {
+		r := newRecorder(t)
+
+		languageModel, err := pair.builder(t, r)
+		require.NoError(t, err, "failed to build language model")
+
+		prompt := fantasy.Prompt{
+			fantasy.NewUserMessage("Generate information about a person named Alice who is 30 years old and lives in Paris."),
+		}
+
+		stream, err := languageModel.StreamObject(t.Context(), fantasy.ObjectCall{
+			Prompt:            prompt,
+			Schema:            schema,
+			SchemaName:        "Person",
+			SchemaDescription: "A person with name, age, and city",
+			MaxOutputTokens:   fantasy.Opt(int64(4000)),
+			ProviderOptions:   pair.providerOptions,
+		})
+		require.NoError(t, err, "failed to create object stream")
+		require.NotNil(t, stream, "stream should not be nil")
+
+		var lastObject any
+		var rawText string
+		var usage fantasy.Usage
+		var finishReason fantasy.FinishReason
+		objectCount := 0
+
+		for part := range stream {
+			switch part.Type {
+			case fantasy.ObjectStreamPartTypeObject:
+				lastObject = part.Object
+				objectCount++
+			case fantasy.ObjectStreamPartTypeTextDelta:
+				rawText += part.Delta
+			case fantasy.ObjectStreamPartTypeFinish:
+				usage = part.Usage
+				finishReason = part.FinishReason
+			case fantasy.ObjectStreamPartTypeError:
+				t.Fatalf("stream error: %v", part.Error)
+			}
+		}
+
+		require.NotNil(t, lastObject, "should have received at least one object")
+		require.Greater(t, objectCount, 0, "should have received object updates")
+		require.NotEqual(t, fantasy.FinishReasonUnknown, finishReason, "should have a finish reason")
+
+		// Validate object structure without requiring rawText (may be empty in tool-based mode)
+		require.NotNil(t, lastObject, "object should not be nil")
+		require.Greater(t, usage.TotalTokens, int64(0), "usage should be tracked")
+
+		// Validate structure
+		objMap, ok := lastObject.(map[string]any)
+		require.True(t, ok, "object should be a map")
+		require.Contains(t, objMap, "name")
+		require.Contains(t, objMap, "age")
+		require.Contains(t, objMap, "city")
+
+		// Validate types
+		name, ok := objMap["name"].(string)
+		require.True(t, ok, "name should be a string")
+		require.NotEmpty(t, name, "name should not be empty")
+
+		// Age could be float64 from JSON unmarshaling
+		age, ok := objMap["age"].(float64)
+		require.True(t, ok, "age should be a number")
+		require.Greater(t, age, 0.0, "age should be greater than 0")
+
+		city, ok := objMap["city"].(string)
+		require.True(t, ok, "city should be a string")
+		require.NotEmpty(t, city, "city should not be empty")
+	})
+}
+
+func testComplexObject(t *testing.T, pair builderPair) {
+	// Define a more complex schema with nested objects and arrays
+	schema := fantasy.Schema{
+		Type: "object",
+		Properties: map[string]*fantasy.Schema{
+			"title": {
+				Type:        "string",
+				Description: "The book title",
+			},
+			"author": {
+				Type: "object",
+				Properties: map[string]*fantasy.Schema{
+					"name": {
+						Type:        "string",
+						Description: "Author's name",
+					},
+					"nationality": {
+						Type:        "string",
+						Description: "Author's nationality",
+					},
+				},
+				Required: []string{"name", "nationality"},
+			},
+			"genres": {
+				Type: "array",
+				Items: &fantasy.Schema{
+					Type: "string",
+				},
+				Description: "List of genres",
+			},
+			"published_year": {
+				Type:        "integer",
+				Description: "Year the book was published",
+			},
+		},
+		Required: []string{"title", "author", "genres", "published_year"},
+	}
+
+	checkResult := func(t *testing.T, obj any, rawText string, usage fantasy.Usage) {
+		require.NotNil(t, obj, "object should not be nil")
+		require.NotEmpty(t, rawText, "raw text should not be empty")
+		require.Greater(t, usage.TotalTokens, int64(0), "usage should be tracked")
+
+		// Validate structure
+		objMap, ok := obj.(map[string]any)
+		require.True(t, ok, "object should be a map")
+		require.Contains(t, objMap, "title")
+		require.Contains(t, objMap, "author")
+		require.Contains(t, objMap, "genres")
+		require.Contains(t, objMap, "published_year")
+
+		// Validate title
+		title, ok := objMap["title"].(string)
+		require.True(t, ok, "title should be a string")
+		require.True(t, strings.Contains(strings.ToLower(title), "rings"), "title should contain 'rings'")
+
+		// Validate nested author object
+		author, ok := objMap["author"].(map[string]any)
+		require.True(t, ok, "author should be an object")
+		require.Contains(t, author, "name")
+		require.Contains(t, author, "nationality")
+
+		// Validate genres array
+		genres, ok := objMap["genres"].([]any)
+		require.True(t, ok, "genres should be an array")
+		require.Greater(t, len(genres), 0, "genres should have at least one item")
+		for _, genre := range genres {
+			_, ok := genre.(string)
+			require.True(t, ok, "each genre should be a string")
+		}
+
+		// Validate published_year
+		year, ok := objMap["published_year"].(float64)
+		require.True(t, ok, "published_year should be a number")
+		require.Greater(t, year, 1900.0, "published_year should be after 1900")
+	}
+
+	t.Run("complex object", func(t *testing.T) {
+		r := newRecorder(t)
+
+		languageModel, err := pair.builder(t, r)
+		require.NoError(t, err, "failed to build language model")
+
+		prompt := fantasy.Prompt{
+			fantasy.NewUserMessage("Generate information about 'The Lord of the Rings' book by J.R.R. Tolkien, including genres like fantasy and adventure, and its publication year (1954)."),
+		}
+
+		response, err := languageModel.GenerateObject(t.Context(), fantasy.ObjectCall{
+			Prompt:            prompt,
+			Schema:            schema,
+			SchemaName:        "Book",
+			SchemaDescription: "A book with title, author, genres, and publication year",
+			MaxOutputTokens:   fantasy.Opt(int64(4000)),
+			ProviderOptions:   pair.providerOptions,
+		})
+		require.NoError(t, err, "failed to generate object")
+		require.NotNil(t, response, "response should not be nil")
+		checkResult(t, response.Object, response.RawText, response.Usage)
+	})
+
+	t.Run("complex object streaming", func(t *testing.T) {
+		r := newRecorder(t)
+
+		languageModel, err := pair.builder(t, r)
+		require.NoError(t, err, "failed to build language model")
+
+		prompt := fantasy.Prompt{
+			fantasy.NewUserMessage("Generate information about 'The Lord of the Rings' book by J.R.R. Tolkien, including genres like fantasy and adventure, and its publication year (1954)."),
+		}
+
+		stream, err := languageModel.StreamObject(t.Context(), fantasy.ObjectCall{
+			Prompt:            prompt,
+			Schema:            schema,
+			SchemaName:        "Book",
+			SchemaDescription: "A book with title, author, genres, and publication year",
+			MaxOutputTokens:   fantasy.Opt(int64(4000)),
+			ProviderOptions:   pair.providerOptions,
+		})
+		require.NoError(t, err, "failed to create object stream")
+		require.NotNil(t, stream, "stream should not be nil")
+
+		var lastObject any
+		var rawText string
+		var usage fantasy.Usage
+		var finishReason fantasy.FinishReason
+		objectCount := 0
+
+		for part := range stream {
+			switch part.Type {
+			case fantasy.ObjectStreamPartTypeObject:
+				lastObject = part.Object
+				objectCount++
+			case fantasy.ObjectStreamPartTypeTextDelta:
+				rawText += part.Delta
+			case fantasy.ObjectStreamPartTypeFinish:
+				usage = part.Usage
+				finishReason = part.FinishReason
+			case fantasy.ObjectStreamPartTypeError:
+				t.Fatalf("stream error: %v", part.Error)
+			}
+		}
+
+		require.NotNil(t, lastObject, "should have received at least one object")
+		require.Greater(t, objectCount, 0, "should have received object updates")
+		require.NotEqual(t, fantasy.FinishReasonUnknown, finishReason, "should have a finish reason")
+
+		// Validate object structure without requiring rawText (may be empty in tool-based mode)
+		require.NotNil(t, lastObject, "object should not be nil")
+		require.Greater(t, usage.TotalTokens, int64(0), "usage should be tracked")
+
+		// Validate structure
+		objMap, ok := lastObject.(map[string]any)
+		require.True(t, ok, "object should be a map")
+		require.Contains(t, objMap, "title")
+		require.Contains(t, objMap, "author")
+		require.Contains(t, objMap, "genres")
+		require.Contains(t, objMap, "published_year")
+
+		// Validate title
+		title, ok := objMap["title"].(string)
+		require.True(t, ok, "title should be a string")
+		require.True(t, strings.Contains(strings.ToLower(title), "rings"), "title should contain 'rings'")
+
+		// Validate nested author object
+		author, ok := objMap["author"].(map[string]any)
+		require.True(t, ok, "author should be an object")
+		require.Contains(t, author, "name")
+		require.Contains(t, author, "nationality")
+
+		// Validate genres array
+		genres, ok := objMap["genres"].([]any)
+		require.True(t, ok, "genres should be an array")
+		require.Greater(t, len(genres), 0, "genres should have at least one item")
+		for _, genre := range genres {
+			_, ok := genre.(string)
+			require.True(t, ok, "each genre should be a string")
+		}
+
+		// Validate published_year
+		year, ok := objMap["published_year"].(float64)
+		require.True(t, ok, "published_year should be a number")
+		require.Greater(t, year, 1900.0, "published_year should be after 1900")
+	})
+}
+
+// testObjectWithRepair tests object generation with custom repair functionality.
+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)
+
+				languageModel, err := pair.builder(t, r)
+				require.NoError(t, err, "failed to build language model")
+
+				minVal := 1.0
+				schema := fantasy.Schema{
+					Type: "object",
+					Properties: map[string]*fantasy.Schema{
+						"count": {
+							Type:        "integer",
+							Description: "A count that must be positive",
+							Minimum:     &minVal,
+						},
+					},
+					Required: []string{"count"},
+				}
+
+				prompt := fantasy.Prompt{
+					fantasy.NewUserMessage("Return a count of 5"),
+				}
+
+				repairFunc := func(ctx context.Context, text string, err error) (string, error) {
+					// Simple repair: if the JSON is malformed, try to fix it
+					// This is a placeholder - real repair would be more sophisticated
+					return text, nil
+				}
+
+				response, err := languageModel.GenerateObject(t.Context(), fantasy.ObjectCall{
+					Prompt:            prompt,
+					Schema:            schema,
+					SchemaName:        "Count",
+					SchemaDescription: "A simple count object",
+					MaxOutputTokens:   fantasy.Opt(int64(4000)),
+					RepairText:        repairFunc,
+					ProviderOptions:   pair.providerOptions,
+				})
+				require.NoError(t, err, "failed to generate object")
+				require.NotNil(t, response, "response should not be nil")
+				require.NotNil(t, response.Object, "object should not be nil")
+			})
+		})
+	}
+}

providertests/openai_responses_test.go šŸ”—

@@ -53,6 +53,14 @@ func TestOpenAIResponsesWithSummaryThinking(t *testing.T) {
 	testThinking(t, pairs, testOpenAIResponsesThinkingWithSummaryThinking)
 }
 
+func TestOpenAIResponsesObjectGeneration(t *testing.T) {
+	var pairs []builderPair
+	for _, m := range openaiTestModels {
+		pairs = append(pairs, builderPair{m.name, openAIReasoningBuilder(m.model), nil, nil})
+	}
+	testObjectGeneration(t, pairs)
+}
+
 func testOpenAIResponsesThinkingWithSummaryThinking(t *testing.T, result *fantasy.AgentResult) {
 	reasoningContentCount := 0
 	encryptedData := 0

providertests/openai_test.go šŸ”—

@@ -25,6 +25,14 @@ func TestOpenAICommon(t *testing.T) {
 	testCommon(t, pairs)
 }
 
+func TestOpenAIObjectGeneration(t *testing.T) {
+	var pairs []builderPair
+	for _, m := range openaiTestModels {
+		pairs = append(pairs, builderPair{m.name, openAIBuilder(m.model), nil, nil})
+	}
+	testObjectGeneration(t, pairs)
+}
+
 func openAIBuilder(model string) builderFunc {
 	return func(t *testing.T, r *recorder.Recorder) (fantasy.LanguageModel, error) {
 		provider, err := openai.New(

providertests/openaicompat_test.go šŸ”—

@@ -23,6 +23,14 @@ func TestOpenAICompatibleCommon(t *testing.T) {
 	})
 }
 
+func TestOpenAICompatObjectGeneration(t *testing.T) {
+	testObjectGeneration(t, []builderPair{
+		{"xai-grok-4-fast", builderXAIGrok4Fast, nil, nil},
+		{"xai-grok-code-fast", builderXAIGrokCodeFast, nil, nil},
+		{"zai-glm-4.5", builderZAIGLM45, nil, nil},
+	})
+}
+
 func TestOpenAICompatibleThinking(t *testing.T) {
 	opts := fantasy.ProviderOptions{
 		openaicompat.Name: &openaicompat.ProviderOptions{

providertests/testdata/TestAnthropicObjectGeneration/claude-sonnet-4/complex_object.yaml šŸ”—

@@ -0,0 +1,33 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 973
+    host: ""
+    body: '{"max_tokens":4000,"messages":[{"content":[{"text":"Generate information about ''The Lord of the Rings'' book by J.R.R. Tolkien, including genres like fantasy and adventure, and its publication year (1954).","type":"text"}],"role":"user"}],"model":"claude-sonnet-4-20250514","tool_choice":{"name":"Book","disable_parallel_tool_use":false,"type":"tool"},"tools":[{"input_schema":{"properties":{"author":{"properties":{"name":{"description":"Author''s name","type":"string"},"nationality":{"description":"Author''s nationality","type":"string"}},"required":["name","nationality"],"type":"object"},"genres":{"description":"List of genres","items":{"type":"string"},"type":"array"},"published_year":{"description":"Year the book was published","type":"integer"},"title":{"description":"The book title","type":"string"}},"required":["title","author","genres","published_year"],"type":"object"},"name":"Book","description":"A book with title, author, genres, and publication year"}]}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - Anthropic/Go 1.14.0
+    url: https://api.anthropic.com/v1/messages
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: '{"model":"claude-sonnet-4-20250514","id":"msg_01UkSBWK7ReA1Jc9w3ZSKrNb","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01C9Z3xQvtkmqxD7fyJ4BHN5","name":"Book","input":{"title":"The Lord of the Rings","author":{"name":"J.R.R. Tolkien","nationality":"British"},"genres":["Fantasy","Adventure","Epic Fantasy","High Fantasy"],"published_year":1954}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":549,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":124,"service_tier":"standard"}}'
+    headers:
+      Content-Type:
+      - application/json
+    status: 200 OK
+    code: 200
+    duration: 3.668454583s

providertests/testdata/TestAnthropicObjectGeneration/claude-sonnet-4/simple_object.yaml šŸ”—

@@ -0,0 +1,33 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 637
+    host: ""
+    body: '{"max_tokens":4000,"messages":[{"content":[{"text":"Generate information about a person named Alice who is 30 years old and lives in Paris.","type":"text"}],"role":"user"}],"model":"claude-sonnet-4-20250514","tool_choice":{"name":"Person","disable_parallel_tool_use":false,"type":"tool"},"tools":[{"input_schema":{"properties":{"age":{"description":"The person''s age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person''s name","type":"string"}},"required":["name","age","city"],"type":"object"},"name":"Person","description":"A person with name, age, and city"}]}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - Anthropic/Go 1.14.0
+    url: https://api.anthropic.com/v1/messages
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: '{"model":"claude-sonnet-4-20250514","id":"msg_01NeodXcmcw7q1AnbSJ8ShV4","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_019sLuNYTpv7VTTJEuGWXzki","name":"Person","input":{"name":"Alice","age":30,"city":"Paris"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":454,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":67,"service_tier":"standard"}}'
+    headers:
+      Content-Type:
+      - application/json
+    status: 200 OK
+    code: 200
+    duration: 2.542812416s

providertests/testdata/TestAnthropicObjectGeneration/claude-sonnet-4/simple_object_streaming.yaml šŸ”—

@@ -0,0 +1,89 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 651
+    host: ""
+    body: '{"max_tokens":4000,"messages":[{"content":[{"text":"Generate information about a person named Alice who is 30 years old and lives in Paris.","type":"text"}],"role":"user"}],"model":"claude-sonnet-4-20250514","tool_choice":{"name":"Person","disable_parallel_tool_use":false,"type":"tool"},"tools":[{"input_schema":{"properties":{"age":{"description":"The person''s age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person''s name","type":"string"}},"required":["name","age","city"],"type":"object"},"name":"Person","description":"A person with name, age, and city"}],"stream":true}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - Anthropic/Go 1.14.0
+    url: https://api.anthropic.com/v1/messages
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    body: |+
+      event: message_start
+      data: {"type":"message_start","message":{"model":"claude-sonnet-4-20250514","id":"msg_01AihXNzoggoeqPeQuWPMYSo","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":454,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":24,"service_tier":"standard"}}           }
+
+      event: content_block_start
+      data: {"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"toolu_01Midg7qjzUz6pdkS8HcgRY7","name":"Person","input":{}}      }
+
+      event: content_block_delta
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}            }
+
+      event: ping
+      data: {"type": "ping"}
+
+      event: content_block_delta
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"{\"n"}     }
+
+      event: ping
+      data: {"type": "ping"}
+
+      event: content_block_delta
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"ame\":"}           }
+
+      event: ping
+      data: {"type": "ping"}
+
+      event: content_block_delta
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":" \"Alic"}  }
+
+      event: ping
+      data: {"type": "ping"}
+
+      event: content_block_delta
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"e\""}               }
+
+      event: content_block_delta
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":", \"age\""}             }
+
+      event: content_block_delta
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":": 30"}    }
+
+      event: content_block_delta
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":", \"c"}       }
+
+      event: content_block_delta
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"ity\": \"Pa"}  }
+
+      event: content_block_delta
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"ris\"}"}         }
+
+      event: content_block_stop
+      data: {"type":"content_block_stop","index":0         }
+
+      event: message_delta
+      data: {"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"input_tokens":454,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":67}             }
+
+      event: message_stop
+      data: {"type":"message_stop"         }
+
+    headers:
+      Content-Type:
+      - text/event-stream; charset=utf-8
+    status: 200 OK
+    code: 200
+    duration: 2.144504667s

providertests/testdata/TestGoogleObjectGeneration/gemini-2.5-flash/complex_object.yaml šŸ”—

@@ -0,0 +1,62 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 817
+    host: generativelanguage.googleapis.com
+    body: |
+      {"contents":[{"parts":[{"text":"Generate information about 'The Lord of the Rings' book by J.R.R. Tolkien, including genres like fantasy and adventure, and its publication year (1954)."}],"role":"user"}],"generationConfig":{"maxOutputTokens":4000,"responseJsonSchema":{"properties":{"author":{"properties":{"name":{"description":"Author's name","type":"string"},"nationality":{"description":"Author's nationality","type":"string"}},"required":["name","nationality"],"type":"object"},"genres":{"description":"List of genres","items":{"type":"string"},"type":"array"},"published_year":{"description":"Year the book was published","type":"integer"},"title":{"description":"The book title","type":"string"}},"required":["title","author","genres","published_year"],"type":"object"},"responseMimeType":"application/json"}}
+    headers:
+      Content-Type:
+      - application/json
+      User-Agent:
+      - google-genai-sdk/1.34.0 gl-go/go1.25.0
+    url: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: |
+      {
+        "candidates": [
+          {
+            "content": {
+              "parts": [
+                {
+                  "text": "{\"author\":{\"name\":\"J.R.R. Tolkien\",\"nationality\":\"British\"},\"genres\":[\"fantasy\",\"adventure\"],\"published_year\":1954,\"title\":\"The Lord of the Rings\"}"
+                }
+              ],
+              "role": "model"
+            },
+            "finishReason": "STOP",
+            "index": 0
+          }
+        ],
+        "usageMetadata": {
+          "promptTokenCount": 38,
+          "candidatesTokenCount": 41,
+          "totalTokenCount": 218,
+          "promptTokensDetails": [
+            {
+              "modality": "TEXT",
+              "tokenCount": 38
+            }
+          ],
+          "thoughtsTokenCount": 139
+        },
+        "modelVersion": "gemini-2.5-flash",
+        "responseId": "stsRaZqEJ9qO28oPiaHWwQ4"
+      }
+    headers:
+      Content-Type:
+      - application/json; charset=UTF-8
+    status: 200 OK
+    code: 200
+    duration: 1.554350125s

providertests/testdata/TestGoogleObjectGeneration/gemini-2.5-flash/complex_object_streaming.yaml šŸ”—

@@ -0,0 +1,34 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 817
+    host: generativelanguage.googleapis.com
+    body: |
+      {"contents":[{"parts":[{"text":"Generate information about 'The Lord of the Rings' book by J.R.R. Tolkien, including genres like fantasy and adventure, and its publication year (1954)."}],"role":"user"}],"generationConfig":{"maxOutputTokens":4000,"responseJsonSchema":{"properties":{"author":{"properties":{"name":{"description":"Author's name","type":"string"},"nationality":{"description":"Author's nationality","type":"string"}},"required":["name","nationality"],"type":"object"},"genres":{"description":"List of genres","items":{"type":"string"},"type":"array"},"published_year":{"description":"Year the book was published","type":"integer"},"title":{"description":"The book title","type":"string"}},"required":["title","author","genres","published_year"],"type":"object"},"responseMimeType":"application/json"}}
+    form:
+      alt:
+      - sse
+    headers:
+      Content-Type:
+      - application/json
+      User-Agent:
+      - google-genai-sdk/1.34.0 gl-go/go1.25.0
+    url: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:streamGenerateContent?alt=sse
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1

providertests/testdata/TestGoogleObjectGeneration/gemini-2.5-flash/simple_object.yaml šŸ”—

@@ -0,0 +1,62 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 499
+    host: generativelanguage.googleapis.com
+    body: |
+      {"contents":[{"parts":[{"text":"Generate information about a person named Alice who is 30 years old and lives in Paris."}],"role":"user"}],"generationConfig":{"maxOutputTokens":4000,"responseJsonSchema":{"properties":{"age":{"description":"The person's age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person's name","type":"string"}},"required":["name","age","city"],"type":"object"},"responseMimeType":"application/json"}}
+    headers:
+      Content-Type:
+      - application/json
+      User-Agent:
+      - google-genai-sdk/1.34.0 gl-go/go1.25.0
+    url: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: |
+      {
+        "candidates": [
+          {
+            "content": {
+              "parts": [
+                {
+                  "text": "{\"age\": 30, \"city\": \"Paris\", \"name\": \"Alice\"}"
+                }
+              ],
+              "role": "model"
+            },
+            "finishReason": "STOP",
+            "index": 0
+          }
+        ],
+        "usageMetadata": {
+          "promptTokenCount": 20,
+          "candidatesTokenCount": 19,
+          "totalTokenCount": 127,
+          "promptTokensDetails": [
+            {
+              "modality": "TEXT",
+              "tokenCount": 20
+            }
+          ],
+          "thoughtsTokenCount": 88
+        },
+        "modelVersion": "gemini-2.5-flash",
+        "responseId": "r9sRaffqJa-jvdIPwMT46A8"
+      }
+    headers:
+      Content-Type:
+      - application/json; charset=UTF-8
+    status: 200 OK
+    code: 200
+    duration: 1.129109875s

providertests/testdata/TestGoogleObjectGeneration/gemini-2.5-flash/simple_object_streaming.yaml šŸ”—

@@ -0,0 +1,34 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 499
+    host: generativelanguage.googleapis.com
+    body: |
+      {"contents":[{"parts":[{"text":"Generate information about a person named Alice who is 30 years old and lives in Paris."}],"role":"user"}],"generationConfig":{"maxOutputTokens":4000,"responseJsonSchema":{"properties":{"age":{"description":"The person's age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person's name","type":"string"}},"required":["name","age","city"],"type":"object"},"responseMimeType":"application/json"}}
+    form:
+      alt:
+      - sse
+    headers:
+      Content-Type:
+      - application/json
+      User-Agent:
+      - google-genai-sdk/1.34.0 gl-go/go1.25.0
+    url: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:streamGenerateContent?alt=sse
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    body: "data: {\"candidates\": [{\"content\": {\"parts\": [{\"text\": \"{\\\"age\\\": 30, \\\"city\\\": \\\"Paris\\\", \\\"name\\\": \\\"Alice\\\"}\"}],\"role\": \"model\"},\"finishReason\": \"STOP\",\"index\": 0}],\"usageMetadata\": {\"promptTokenCount\": 20,\"candidatesTokenCount\": 19,\"totalTokenCount\": 138,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 20}],\"thoughtsTokenCount\": 99},\"modelVersion\": \"gemini-2.5-flash\",\"responseId\": \"sNsRad21He_hxN8PlPed0Qs\"}\r\n\r\n"
+    headers:
+      Content-Type:
+      - text/event-stream
+    status: 200 OK
+    code: 200
+    duration: 1.556089083s

providertests/testdata/TestGoogleObjectGeneration/gemini-2.5-pro/complex_object.yaml šŸ”—

@@ -0,0 +1,62 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 817
+    host: generativelanguage.googleapis.com
+    body: |
+      {"contents":[{"parts":[{"text":"Generate information about 'The Lord of the Rings' book by J.R.R. Tolkien, including genres like fantasy and adventure, and its publication year (1954)."}],"role":"user"}],"generationConfig":{"maxOutputTokens":4000,"responseJsonSchema":{"properties":{"author":{"properties":{"name":{"description":"Author's name","type":"string"},"nationality":{"description":"Author's nationality","type":"string"}},"required":["name","nationality"],"type":"object"},"genres":{"description":"List of genres","items":{"type":"string"},"type":"array"},"published_year":{"description":"Year the book was published","type":"integer"},"title":{"description":"The book title","type":"string"}},"required":["title","author","genres","published_year"],"type":"object"},"responseMimeType":"application/json"}}
+    headers:
+      Content-Type:
+      - application/json
+      User-Agent:
+      - google-genai-sdk/1.34.0 gl-go/go1.25.0
+    url: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: |
+      {
+        "candidates": [
+          {
+            "content": {
+              "parts": [
+                {
+                  "text": "{\n  \"author\": {\n    \"name\": \"J.R.R. Tolkien\",\n    \"nationality\": \"British\"\n  },\n  \"genres\": [\n    \"Fantasy\",\n    \"Adventure\"\n  ],\n  \"published_year\": 1954,\n  \"title\": \"The Lord of the Rings\"\n}"
+                }
+              ],
+              "role": "model"
+            },
+            "finishReason": "STOP",
+            "index": 0
+          }
+        ],
+        "usageMetadata": {
+          "promptTokenCount": 38,
+          "candidatesTokenCount": 78,
+          "totalTokenCount": 352,
+          "promptTokensDetails": [
+            {
+              "modality": "TEXT",
+              "tokenCount": 38
+            }
+          ],
+          "thoughtsTokenCount": 236
+        },
+        "modelVersion": "gemini-2.5-pro",
+        "responseId": "v9sRad3jAc_7xs0P77zDwQE"
+      }
+    headers:
+      Content-Type:
+      - application/json; charset=UTF-8
+    status: 200 OK
+    code: 200
+    duration: 4.027611625s

providertests/testdata/TestGoogleObjectGeneration/gemini-2.5-pro/complex_object_streaming.yaml šŸ”—

@@ -0,0 +1,34 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 817
+    host: generativelanguage.googleapis.com
+    body: |
+      {"contents":[{"parts":[{"text":"Generate information about 'The Lord of the Rings' book by J.R.R. Tolkien, including genres like fantasy and adventure, and its publication year (1954)."}],"role":"user"}],"generationConfig":{"maxOutputTokens":4000,"responseJsonSchema":{"properties":{"author":{"properties":{"name":{"description":"Author's name","type":"string"},"nationality":{"description":"Author's nationality","type":"string"}},"required":["name","nationality"],"type":"object"},"genres":{"description":"List of genres","items":{"type":"string"},"type":"array"},"published_year":{"description":"Year the book was published","type":"integer"},"title":{"description":"The book title","type":"string"}},"required":["title","author","genres","published_year"],"type":"object"},"responseMimeType":"application/json"}}
+    form:
+      alt:
+      - sse
+    headers:
+      Content-Type:
+      - application/json
+      User-Agent:
+      - google-genai-sdk/1.34.0 gl-go/go1.25.0
+    url: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:streamGenerateContent?alt=sse
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1

providertests/testdata/TestGoogleObjectGeneration/gemini-2.5-pro/simple_object.yaml šŸ”—

@@ -0,0 +1,62 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 499
+    host: generativelanguage.googleapis.com
+    body: |
+      {"contents":[{"parts":[{"text":"Generate information about a person named Alice who is 30 years old and lives in Paris."}],"role":"user"}],"generationConfig":{"maxOutputTokens":4000,"responseJsonSchema":{"properties":{"age":{"description":"The person's age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person's name","type":"string"}},"required":["name","age","city"],"type":"object"},"responseMimeType":"application/json"}}
+    headers:
+      Content-Type:
+      - application/json
+      User-Agent:
+      - google-genai-sdk/1.34.0 gl-go/go1.25.0
+    url: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: |
+      {
+        "candidates": [
+          {
+            "content": {
+              "parts": [
+                {
+                  "text": "{\n  \"age\": 30,\n  \"city\": \"Paris\",\n  \"name\": \"Alice\"\n}"
+                }
+              ],
+              "role": "model"
+            },
+            "finishReason": "STOP",
+            "index": 0
+          }
+        ],
+        "usageMetadata": {
+          "promptTokenCount": 20,
+          "candidatesTokenCount": 28,
+          "totalTokenCount": 109,
+          "promptTokensDetails": [
+            {
+              "modality": "TEXT",
+              "tokenCount": 20
+            }
+          ],
+          "thoughtsTokenCount": 61
+        },
+        "modelVersion": "gemini-2.5-pro",
+        "responseId": "ttsRacXtC86PvdIP1Zjy8AE"
+      }
+    headers:
+      Content-Type:
+      - application/json; charset=UTF-8
+    status: 200 OK
+    code: 200
+    duration: 2.077859292s

providertests/testdata/TestGoogleObjectGeneration/gemini-2.5-pro/simple_object_streaming.yaml šŸ”—

@@ -0,0 +1,34 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 499
+    host: generativelanguage.googleapis.com
+    body: |
+      {"contents":[{"parts":[{"text":"Generate information about a person named Alice who is 30 years old and lives in Paris."}],"role":"user"}],"generationConfig":{"maxOutputTokens":4000,"responseJsonSchema":{"properties":{"age":{"description":"The person's age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person's name","type":"string"}},"required":["name","age","city"],"type":"object"},"responseMimeType":"application/json"}}
+    form:
+      alt:
+      - sse
+    headers:
+      Content-Type:
+      - application/json
+      User-Agent:
+      - google-genai-sdk/1.34.0 gl-go/go1.25.0
+    url: https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:streamGenerateContent?alt=sse
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    body: "data: {\"candidates\": [{\"content\": {\"parts\": [{\"text\": \"{\\n  \\\"age\\\": 30,\\n  \\\"city\\\":\"}],\"role\": \"model\"},\"index\": 0}],\"usageMetadata\": {\"promptTokenCount\": 20,\"candidatesTokenCount\": 15,\"totalTokenCount\": 189,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 20}],\"thoughtsTokenCount\": 154},\"modelVersion\": \"gemini-2.5-pro\",\"responseId\": \"udsRabusHJH_xN8PppGAiQ8\"}\r\n\r\ndata: {\"candidates\": [{\"content\": {\"parts\": [{\"text\": \" \\\"Paris\\\",\\n  \\\"name\\\": \\\"Alice\\\"\\n}\"}],\"role\": \"model\"},\"finishReason\": \"STOP\",\"index\": 0}],\"usageMetadata\": {\"promptTokenCount\": 20,\"candidatesTokenCount\": 28,\"totalTokenCount\": 202,\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 20}],\"thoughtsTokenCount\": 154},\"modelVersion\": \"gemini-2.5-pro\",\"responseId\": \"udsRabusHJH_xN8PppGAiQ8\"}\r\n\r\n"
+    headers:
+      Content-Type:
+      - text/event-stream
+    status: 200 OK
+    code: 200
+    duration: 4.791360833s

providertests/testdata/TestGoogleVertexObjectGeneration/vertex-claude-3-7-sonnet/complex_object.yaml šŸ”—

@@ -0,0 +1,33 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 978
+    host: ""
+    body: '{"max_tokens":4000,"messages":[{"content":[{"text":"Generate information about ''The Lord of the Rings'' book by J.R.R. Tolkien, including genres like fantasy and adventure, and its publication year (1954).","type":"text"}],"role":"user"}],"tool_choice":{"name":"Book","disable_parallel_tool_use":false,"type":"tool"},"tools":[{"input_schema":{"properties":{"author":{"properties":{"name":{"description":"Author''s name","type":"string"},"nationality":{"description":"Author''s nationality","type":"string"}},"required":["name","nationality"],"type":"object"},"genres":{"description":"List of genres","items":{"type":"string"},"type":"array"},"published_year":{"description":"Year the book was published","type":"integer"},"title":{"description":"The book title","type":"string"}},"required":["title","author","genres","published_year"],"type":"object"},"name":"Book","description":"A book with title, author, genres, and publication year"}],"anthropic_version":"vertex-2023-10-16"}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - Anthropic/Go 1.14.0
+    url: https://us-east5-aiplatform.googleapis.com/v1/projects/fantasy-playground-472418/locations/us-east5/publishers/anthropic/models/claude-3-7-sonnet@20250219:rawPredict
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: '{"model":"claude-3-7-sonnet-20250219","id":"msg_vrtx_01UAjss8kpXQotMqUWtjP8Vd","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_vrtx_01UqmeUWH5fBpvtZE1jx4knQ","name":"Book","input":{"title":"The Lord of the Rings","author":{"name":"J.R.R. Tolkien","nationality":"British"},"genres":["Fantasy","Adventure","Epic","High fantasy"],"published_year":1954}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":548,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":132}}'
+    headers:
+      Content-Type:
+      - application/json
+    status: 200 OK
+    code: 200
+    duration: 2.867918541s

providertests/testdata/TestGoogleVertexObjectGeneration/vertex-claude-3-7-sonnet/simple_object.yaml šŸ”—

@@ -0,0 +1,33 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 642
+    host: ""
+    body: '{"max_tokens":4000,"messages":[{"content":[{"text":"Generate information about a person named Alice who is 30 years old and lives in Paris.","type":"text"}],"role":"user"}],"tool_choice":{"name":"Person","disable_parallel_tool_use":false,"type":"tool"},"tools":[{"input_schema":{"properties":{"age":{"description":"The person''s age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person''s name","type":"string"}},"required":["name","age","city"],"type":"object"},"name":"Person","description":"A person with name, age, and city"}],"anthropic_version":"vertex-2023-10-16"}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - Anthropic/Go 1.14.0
+    url: https://us-east5-aiplatform.googleapis.com/v1/projects/fantasy-playground-472418/locations/us-east5/publishers/anthropic/models/claude-3-7-sonnet@20250219:rawPredict
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: '{"model":"claude-3-7-sonnet-20250219","id":"msg_vrtx_014VLsXeDXtuC84Nx9V4pBdW","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_vrtx_01TaBaQU6GZpxjApVgvzLfev","name":"Person","input":{"name":"Alice","age":30,"city":"Paris"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":453,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":67}}'
+    headers:
+      Content-Type:
+      - application/json
+    status: 200 OK
+    code: 200
+    duration: 1.818802583s

providertests/testdata/TestGoogleVertexObjectGeneration/vertex-claude-3-7-sonnet/simple_object_streaming.yaml šŸ”—

@@ -0,0 +1,81 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 656
+    host: ""
+    body: '{"max_tokens":4000,"messages":[{"content":[{"text":"Generate information about a person named Alice who is 30 years old and lives in Paris.","type":"text"}],"role":"user"}],"tool_choice":{"name":"Person","disable_parallel_tool_use":false,"type":"tool"},"tools":[{"input_schema":{"properties":{"age":{"description":"The person''s age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person''s name","type":"string"}},"required":["name","age","city"],"type":"object"},"name":"Person","description":"A person with name, age, and city"}],"stream":true,"anthropic_version":"vertex-2023-10-16"}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - Anthropic/Go 1.14.0
+    url: https://us-east5-aiplatform.googleapis.com/v1/projects/fantasy-playground-472418/locations/us-east5/publishers/anthropic/models/claude-3-7-sonnet@20250219:streamRawPredict
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: |+
+      event: message_start
+      data: {"type":"message_start","message":{"model":"claude-3-7-sonnet-20250219","id":"msg_vrtx_01XdrdQzn2BFw8pob1JJCRnX","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":453,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":11}}}
+
+      event: ping
+      data: {"type": "ping"}
+
+      event: content_block_start
+      data: {"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"toolu_vrtx_01Y5BDgK5DcxEvwThJWL5Pix","name":"Person","input":{}}        }
+
+      event: content_block_delta
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":""}             }
+
+      event: content_block_delta
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"{\""}        }
+
+      event: content_block_delta
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"name\":"}          }
+
+      event: content_block_delta
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":" \"Alice\""}      }
+
+      event: content_block_delta
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":", \"age\""}   }
+
+      event: content_block_delta
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":": 30"}      }
+
+      event: content_block_delta
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":", \"city\""}            }
+
+      event: content_block_delta
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":": \"P"}            }
+
+      event: content_block_delta
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"ari"}             }
+
+      event: content_block_delta
+      data: {"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"s\"}"}            }
+
+      event: content_block_stop
+      data: {"type":"content_block_stop","index":0        }
+
+      event: message_delta
+      data: {"type":"message_delta","delta":{"stop_reason":"tool_use","stop_sequence":null},"usage":{"output_tokens":67}        }
+
+      event: message_stop
+      data: {"type":"message_stop"           }
+
+    headers:
+      Content-Type:
+      - text/event-stream; charset=utf-8
+    status: 200 OK
+    code: 200
+    duration: 1.471510916s

providertests/testdata/TestGoogleVertexObjectGeneration/vertex-gemini-2-5-flash/complex_object.yaml šŸ”—

@@ -0,0 +1,70 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 817
+    host: us-east5-aiplatform.googleapis.com
+    body: |
+      {"contents":[{"parts":[{"text":"Generate information about 'The Lord of the Rings' book by J.R.R. Tolkien, including genres like fantasy and adventure, and its publication year (1954)."}],"role":"user"}],"generationConfig":{"maxOutputTokens":4000,"responseJsonSchema":{"properties":{"author":{"properties":{"name":{"description":"Author's name","type":"string"},"nationality":{"description":"Author's nationality","type":"string"}},"required":["name","nationality"],"type":"object"},"genres":{"description":"List of genres","items":{"type":"string"},"type":"array"},"published_year":{"description":"Year the book was published","type":"integer"},"title":{"description":"The book title","type":"string"}},"required":["title","author","genres","published_year"],"type":"object"},"responseMimeType":"application/json"}}
+    headers:
+      Content-Type:
+      - application/json
+      User-Agent:
+      - google-genai-sdk/1.34.0 gl-go/go1.25.0
+    url: https://us-east5-aiplatform.googleapis.com/v1beta1/projects/fantasy-playground-472418/locations/us-east5/publishers/google/models/gemini-2.5-flash:generateContent
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: |
+      {
+        "candidates": [
+          {
+            "content": {
+              "role": "model",
+              "parts": [
+                {
+                  "text": "{\n  \"title\": \"The Lord of the Rings\",\n  \"author\": {\n    \"name\": \"J.R.R. Tolkien\",\n    \"nationality\": \"British\"\n  },\n  \"genres\": [\n    \"fantasy\",\n    \"adventure\"\n  ],\n  \"published_year\": 1954\n}"
+                }
+              ]
+            },
+            "finishReason": "STOP",
+            "avgLogprobs": -0.45047834322050018
+          }
+        ],
+        "usageMetadata": {
+          "promptTokenCount": 37,
+          "candidatesTokenCount": 77,
+          "totalTokenCount": 246,
+          "trafficType": "ON_DEMAND",
+          "promptTokensDetails": [
+            {
+              "modality": "TEXT",
+              "tokenCount": 37
+            }
+          ],
+          "candidatesTokensDetails": [
+            {
+              "modality": "TEXT",
+              "tokenCount": 77
+            }
+          ],
+          "thoughtsTokenCount": 132
+        },
+        "modelVersion": "gemini-2.5-flash",
+        "createTime": "2025-11-10T12:36:25.122147Z",
+        "responseId": "SdwRaaO6B9fzptQPk5W9sQ8"
+      }
+    headers:
+      Content-Type:
+      - application/json; charset=UTF-8
+    status: 200 OK
+    code: 200
+    duration: 1.655407625s

providertests/testdata/TestGoogleVertexObjectGeneration/vertex-gemini-2-5-flash/complex_object_streaming.yaml šŸ”—

@@ -0,0 +1,34 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 817
+    host: us-east5-aiplatform.googleapis.com
+    body: |
+      {"contents":[{"parts":[{"text":"Generate information about 'The Lord of the Rings' book by J.R.R. Tolkien, including genres like fantasy and adventure, and its publication year (1954)."}],"role":"user"}],"generationConfig":{"maxOutputTokens":4000,"responseJsonSchema":{"properties":{"author":{"properties":{"name":{"description":"Author's name","type":"string"},"nationality":{"description":"Author's nationality","type":"string"}},"required":["name","nationality"],"type":"object"},"genres":{"description":"List of genres","items":{"type":"string"},"type":"array"},"published_year":{"description":"Year the book was published","type":"integer"},"title":{"description":"The book title","type":"string"}},"required":["title","author","genres","published_year"],"type":"object"},"responseMimeType":"application/json"}}
+    form:
+      alt:
+      - sse
+    headers:
+      Content-Type:
+      - application/json
+      User-Agent:
+      - google-genai-sdk/1.34.0 gl-go/go1.25.0
+    url: https://us-east5-aiplatform.googleapis.com/v1beta1/projects/fantasy-playground-472418/locations/us-east5/publishers/google/models/gemini-2.5-flash:streamGenerateContent?alt=sse
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1

providertests/testdata/TestGoogleVertexObjectGeneration/vertex-gemini-2-5-flash/simple_object.yaml šŸ”—

@@ -0,0 +1,70 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 499
+    host: us-east5-aiplatform.googleapis.com
+    body: |
+      {"contents":[{"parts":[{"text":"Generate information about a person named Alice who is 30 years old and lives in Paris."}],"role":"user"}],"generationConfig":{"maxOutputTokens":4000,"responseJsonSchema":{"properties":{"age":{"description":"The person's age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person's name","type":"string"}},"required":["name","age","city"],"type":"object"},"responseMimeType":"application/json"}}
+    headers:
+      Content-Type:
+      - application/json
+      User-Agent:
+      - google-genai-sdk/1.34.0 gl-go/go1.25.0
+    url: https://us-east5-aiplatform.googleapis.com/v1beta1/projects/fantasy-playground-472418/locations/us-east5/publishers/google/models/gemini-2.5-flash:generateContent
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: |
+      {
+        "candidates": [
+          {
+            "content": {
+              "role": "model",
+              "parts": [
+                {
+                  "text": "{\"name\": \"Alice\", \"age\": 30, \"city\": \"Paris\"}"
+                }
+              ]
+            },
+            "finishReason": "STOP",
+            "avgLogprobs": -0.57498173964651011
+          }
+        ],
+        "usageMetadata": {
+          "promptTokenCount": 19,
+          "candidatesTokenCount": 19,
+          "totalTokenCount": 83,
+          "trafficType": "ON_DEMAND",
+          "promptTokensDetails": [
+            {
+              "modality": "TEXT",
+              "tokenCount": 19
+            }
+          ],
+          "candidatesTokensDetails": [
+            {
+              "modality": "TEXT",
+              "tokenCount": 19
+            }
+          ],
+          "thoughtsTokenCount": 45
+        },
+        "modelVersion": "gemini-2.5-flash",
+        "createTime": "2025-11-10T12:36:22.649878Z",
+        "responseId": "RtwRaZbVJ-zzptQPvOrxqQ8"
+      }
+    headers:
+      Content-Type:
+      - application/json; charset=UTF-8
+    status: 200 OK
+    code: 200
+    duration: 1.886921167s

providertests/testdata/TestGoogleVertexObjectGeneration/vertex-gemini-2-5-flash/simple_object_streaming.yaml šŸ”—

@@ -0,0 +1,34 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 499
+    host: us-east5-aiplatform.googleapis.com
+    body: |
+      {"contents":[{"parts":[{"text":"Generate information about a person named Alice who is 30 years old and lives in Paris."}],"role":"user"}],"generationConfig":{"maxOutputTokens":4000,"responseJsonSchema":{"properties":{"age":{"description":"The person's age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person's name","type":"string"}},"required":["name","age","city"],"type":"object"},"responseMimeType":"application/json"}}
+    form:
+      alt:
+      - sse
+    headers:
+      Content-Type:
+      - application/json
+      User-Agent:
+      - google-genai-sdk/1.34.0 gl-go/go1.25.0
+    url: https://us-east5-aiplatform.googleapis.com/v1beta1/projects/fantasy-playground-472418/locations/us-east5/publishers/google/models/gemini-2.5-flash:streamGenerateContent?alt=sse
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    body: "data: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"{\\n  \\\"name\\\": \\\"Alice\\\",\\n  \\\"age\\\": 30,\\n  \\\"city\\\": \\\"Paris\\\"\\n}\"}]},\"finishReason\": \"STOP\"}],\"usageMetadata\": {\"promptTokenCount\": 19,\"candidatesTokenCount\": 28,\"totalTokenCount\": 124,\"trafficType\": \"ON_DEMAND\",\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 19}],\"candidatesTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 28}],\"thoughtsTokenCount\": 77},\"modelVersion\": \"gemini-2.5-flash\",\"createTime\": \"2025-11-10T12:36:23.898113Z\",\"responseId\": \"R9wRacHoNpyYw8cPz9-4-QY\"}\r\n\r\n"
+    headers:
+      Content-Type:
+      - text/event-stream
+    status: 200 OK
+    code: 200
+    duration: 1.064075083s

providertests/testdata/TestGoogleVertexObjectGeneration/vertex-gemini-2-5-pro/complex_object.yaml šŸ”—

@@ -0,0 +1,70 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 817
+    host: us-east5-aiplatform.googleapis.com
+    body: |
+      {"contents":[{"parts":[{"text":"Generate information about 'The Lord of the Rings' book by J.R.R. Tolkien, including genres like fantasy and adventure, and its publication year (1954)."}],"role":"user"}],"generationConfig":{"maxOutputTokens":4000,"responseJsonSchema":{"properties":{"author":{"properties":{"name":{"description":"Author's name","type":"string"},"nationality":{"description":"Author's nationality","type":"string"}},"required":["name","nationality"],"type":"object"},"genres":{"description":"List of genres","items":{"type":"string"},"type":"array"},"published_year":{"description":"Year the book was published","type":"integer"},"title":{"description":"The book title","type":"string"}},"required":["title","author","genres","published_year"],"type":"object"},"responseMimeType":"application/json"}}
+    headers:
+      Content-Type:
+      - application/json
+      User-Agent:
+      - google-genai-sdk/1.34.0 gl-go/go1.25.0
+    url: https://us-east5-aiplatform.googleapis.com/v1beta1/projects/fantasy-playground-472418/locations/us-east5/publishers/google/models/gemini-2.5-pro:generateContent
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: |
+      {
+        "candidates": [
+          {
+            "content": {
+              "role": "model",
+              "parts": [
+                {
+                  "text": "{\n  \"title\": \"The Lord of the Rings\",\n  \"author\": {\n    \"name\": \"J.R.R. Tolkien\",\n    \"nationality\": \"British\"\n  },\n  \"genres\": [\n    \"Fantasy\",\n    \"Adventure\"\n  ],\n  \"published_year\": 1954\n}"
+                }
+              ]
+            },
+            "finishReason": "STOP",
+            "avgLogprobs": -0.67830568784243106
+          }
+        ],
+        "usageMetadata": {
+          "promptTokenCount": 37,
+          "candidatesTokenCount": 77,
+          "totalTokenCount": 514,
+          "trafficType": "ON_DEMAND",
+          "promptTokensDetails": [
+            {
+              "modality": "TEXT",
+              "tokenCount": 37
+            }
+          ],
+          "candidatesTokensDetails": [
+            {
+              "modality": "TEXT",
+              "tokenCount": 77
+            }
+          ],
+          "thoughtsTokenCount": 400
+        },
+        "modelVersion": "gemini-2.5-pro",
+        "createTime": "2025-11-10T12:36:32.031653Z",
+        "responseId": "UNwRaaX3AZ3qptQPg8OiiQU"
+      }
+    headers:
+      Content-Type:
+      - application/json; charset=UTF-8
+    status: 200 OK
+    code: 200
+    duration: 5.257772958s

providertests/testdata/TestGoogleVertexObjectGeneration/vertex-gemini-2-5-pro/complex_object_streaming.yaml šŸ”—

@@ -0,0 +1,34 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 817
+    host: us-east5-aiplatform.googleapis.com
+    body: |
+      {"contents":[{"parts":[{"text":"Generate information about 'The Lord of the Rings' book by J.R.R. Tolkien, including genres like fantasy and adventure, and its publication year (1954)."}],"role":"user"}],"generationConfig":{"maxOutputTokens":4000,"responseJsonSchema":{"properties":{"author":{"properties":{"name":{"description":"Author's name","type":"string"},"nationality":{"description":"Author's nationality","type":"string"}},"required":["name","nationality"],"type":"object"},"genres":{"description":"List of genres","items":{"type":"string"},"type":"array"},"published_year":{"description":"Year the book was published","type":"integer"},"title":{"description":"The book title","type":"string"}},"required":["title","author","genres","published_year"],"type":"object"},"responseMimeType":"application/json"}}
+    form:
+      alt:
+      - sse
+    headers:
+      Content-Type:
+      - application/json
+      User-Agent:
+      - google-genai-sdk/1.34.0 gl-go/go1.25.0
+    url: https://us-east5-aiplatform.googleapis.com/v1beta1/projects/fantasy-playground-472418/locations/us-east5/publishers/google/models/gemini-2.5-pro:streamGenerateContent?alt=sse
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1

providertests/testdata/TestGoogleVertexObjectGeneration/vertex-gemini-2-5-pro/simple_object.yaml šŸ”—

@@ -0,0 +1,70 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 499
+    host: us-east5-aiplatform.googleapis.com
+    body: |
+      {"contents":[{"parts":[{"text":"Generate information about a person named Alice who is 30 years old and lives in Paris."}],"role":"user"}],"generationConfig":{"maxOutputTokens":4000,"responseJsonSchema":{"properties":{"age":{"description":"The person's age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person's name","type":"string"}},"required":["name","age","city"],"type":"object"},"responseMimeType":"application/json"}}
+    headers:
+      Content-Type:
+      - application/json
+      User-Agent:
+      - google-genai-sdk/1.34.0 gl-go/go1.25.0
+    url: https://us-east5-aiplatform.googleapis.com/v1beta1/projects/fantasy-playground-472418/locations/us-east5/publishers/google/models/gemini-2.5-pro:generateContent
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: |
+      {
+        "candidates": [
+          {
+            "content": {
+              "role": "model",
+              "parts": [
+                {
+                  "text": "{\n  \"name\": \"Alice\",\n  \"age\": 30,\n  \"city\": \"Paris\"\n}"
+                }
+              ]
+            },
+            "finishReason": "STOP",
+            "avgLogprobs": -0.58463580267769955
+          }
+        ],
+        "usageMetadata": {
+          "promptTokenCount": 19,
+          "candidatesTokenCount": 28,
+          "totalTokenCount": 102,
+          "trafficType": "ON_DEMAND",
+          "promptTokensDetails": [
+            {
+              "modality": "TEXT",
+              "tokenCount": 19
+            }
+          ],
+          "candidatesTokensDetails": [
+            {
+              "modality": "TEXT",
+              "tokenCount": 28
+            }
+          ],
+          "thoughtsTokenCount": 55
+        },
+        "modelVersion": "gemini-2.5-pro",
+        "createTime": "2025-11-10T12:36:28.270555Z",
+        "responseId": "TNwRadvBENLK5OMPoK7I0AI"
+      }
+    headers:
+      Content-Type:
+      - application/json; charset=UTF-8
+    status: 200 OK
+    code: 200
+    duration: 1.459651375s

providertests/testdata/TestGoogleVertexObjectGeneration/vertex-gemini-2-5-pro/simple_object_streaming.yaml šŸ”—

@@ -0,0 +1,34 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 499
+    host: us-east5-aiplatform.googleapis.com
+    body: |
+      {"contents":[{"parts":[{"text":"Generate information about a person named Alice who is 30 years old and lives in Paris."}],"role":"user"}],"generationConfig":{"maxOutputTokens":4000,"responseJsonSchema":{"properties":{"age":{"description":"The person's age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person's name","type":"string"}},"required":["name","age","city"],"type":"object"},"responseMimeType":"application/json"}}
+    form:
+      alt:
+      - sse
+    headers:
+      Content-Type:
+      - application/json
+      User-Agent:
+      - google-genai-sdk/1.34.0 gl-go/go1.25.0
+    url: https://us-east5-aiplatform.googleapis.com/v1beta1/projects/fantasy-playground-472418/locations/us-east5/publishers/google/models/gemini-2.5-pro:streamGenerateContent?alt=sse
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    body: "data: {\"candidates\": [{\"content\": {\"role\": \"model\",\"parts\": [{\"text\": \"{\\n  \\\"name\\\": \\\"Alice\\\",\\n  \\\"age\\\": 30,\\n  \\\"city\\\": \\\"Paris\\\"\\n}\"}]},\"finishReason\": \"STOP\"}],\"usageMetadata\": {\"promptTokenCount\": 19,\"candidatesTokenCount\": 28,\"totalTokenCount\": 209,\"trafficType\": \"ON_DEMAND\",\"promptTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 19}],\"candidatesTokensDetails\": [{\"modality\": \"TEXT\",\"tokenCount\": 28}],\"thoughtsTokenCount\": 162},\"modelVersion\": \"gemini-2.5-pro\",\"createTime\": \"2025-11-10T12:36:29.889263Z\",\"responseId\": \"TdwRaa-jNvWAptQPkJGMwQ8\"}\r\n\r\n"
+    headers:
+      Content-Type:
+      - text/event-stream
+    status: 200 OK
+    code: 200
+    duration: 1.9909835s

providertests/testdata/TestOpenAICompatObjectGeneration/xai-grok-4-fast/complex_object.yaml šŸ”—

@@ -0,0 +1,33 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 962
+    host: ""
+    body: '{"messages":[{"content":"Generate information about ''The Lord of the Rings'' book by J.R.R. Tolkien, including genres like fantasy and adventure, and its publication year (1954).","role":"user"}],"model":"grok-4-fast","max_tokens":4000,"tool_choice":{"function":{"name":"Book"},"type":"function"},"tools":[{"function":{"name":"Book","strict":false,"description":"A book with title, author, genres, and publication year","parameters":{"properties":{"author":{"properties":{"name":{"description":"Author''s name","type":"string"},"nationality":{"description":"Author''s nationality","type":"string"}},"required":["name","nationality"],"type":"object"},"genres":{"description":"List of genres","items":{"type":"string"},"type":"array"},"published_year":{"description":"Year the book was published","type":"integer"},"title":{"description":"The book title","type":"string"}},"required":["title","author","genres","published_year"],"type":"object"}},"type":"function"}]}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.x.ai/v1/chat/completions
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: '{"id":"c507c7e0-9670-a02e-8c70-1df65c243406","object":"chat.completion","created":1762637614,"model":"grok-4-fast-reasoning","choices":[{"index":0,"message":{"role":"assistant","content":"","tool_calls":[{"id":"call_93127190","function":{"name":"Book","arguments":"{\"author\":{\"name\":\"J.R.R. Tolkien\",\"nationality\":\"British\"},\"genres\":[\"fantasy\",\"adventure\"],\"published_year\":1954,\"title\":\"The Lord of the Rings\"}"},"type":"function"}],"refusal":null},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":468,"completion_tokens":83,"total_tokens":1122,"prompt_tokens_details":{"text_tokens":468,"audio_tokens":0,"image_tokens":0,"cached_tokens":305},"completion_tokens_details":{"reasoning_tokens":571,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0},"num_sources_used":0},"system_fingerprint":"fp_bfbe7bd0a2"}'
+    headers:
+      Content-Type:
+      - application/json
+    status: 200 OK
+    code: 200
+    duration: 3.442305458s

providertests/testdata/TestOpenAICompatObjectGeneration/xai-grok-4-fast/simple_object.yaml šŸ”—

@@ -0,0 +1,33 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 626
+    host: ""
+    body: '{"messages":[{"content":"Generate information about a person named Alice who is 30 years old and lives in Paris.","role":"user"}],"model":"grok-4-fast","max_tokens":4000,"tool_choice":{"function":{"name":"Person"},"type":"function"},"tools":[{"function":{"name":"Person","strict":false,"description":"A person with name, age, and city","parameters":{"properties":{"age":{"description":"The person''s age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person''s name","type":"string"}},"required":["name","age","city"],"type":"object"}},"type":"function"}]}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.x.ai/v1/chat/completions
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: '{"id":"c741f0c0-b3ad-605e-94e9-aa17f8ad0f1f","object":"chat.completion","created":1762637610,"model":"grok-4-fast-reasoning","choices":[{"index":0,"message":{"role":"assistant","content":"","tool_calls":[{"id":"call_85975414","function":{"name":"Person","arguments":"{\"age\":30,\"city\":\"Paris\",\"name\":\"Alice\"}"},"type":"function"}],"refusal":null},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":401,"completion_tokens":46,"total_tokens":787,"prompt_tokens_details":{"text_tokens":401,"audio_tokens":0,"image_tokens":0,"cached_tokens":305},"completion_tokens_details":{"reasoning_tokens":340,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0},"num_sources_used":0},"system_fingerprint":"fp_bfbe7bd0a2"}'
+    headers:
+      Content-Type:
+      - application/json
+    status: 200 OK
+    code: 200
+    duration: 2.228575458s

providertests/testdata/TestOpenAICompatObjectGeneration/xai-grok-4-fast/simple_object_streaming.yaml šŸ”—

@@ -0,0 +1,378 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 680
+    host: ""
+    body: '{"messages":[{"content":"Generate information about a person named Alice who is 30 years old and lives in Paris.","role":"user"}],"model":"grok-4-fast","max_tokens":4000,"stream_options":{"include_usage":true},"tool_choice":{"function":{"name":"Person"},"type":"function"},"tools":[{"function":{"name":"Person","strict":false,"description":"A person with name, age, and city","parameters":{"properties":{"age":{"description":"The person''s age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person''s name","type":"string"}},"required":["name","age","city"],"type":"object"}},"type":"function"}],"stream":true}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.x.ai/v1/chat/completions
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    body: |+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{"role":"assistant"}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637613,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637614,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{"tool_calls":[{"id":"call_85608889","function":{"name":"Person","arguments":"{\"age\":30,\"city\":\"Paris\",\"name\":\"Alice\"}"},"index":0,"type":"function"}]}}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637614,"model":"grok-4-fast-reasoning","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: {"id":"a6a4a8e9-1f95-504d-3f2c-0f106447d2d2","object":"chat.completion.chunk","created":1762637614,"model":"grok-4-fast-reasoning","choices":[],"usage":{"prompt_tokens":401,"completion_tokens":46,"total_tokens":616,"prompt_tokens_details":{"text_tokens":401,"audio_tokens":0,"image_tokens":0,"cached_tokens":400},"completion_tokens_details":{"reasoning_tokens":169,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0},"num_sources_used":0},"system_fingerprint":"fp_bfbe7bd0a2"}
+
+      data: [DONE]
+
+    headers:
+      Content-Type:
+      - text/event-stream
+    status: 200 OK
+    code: 200
+    duration: 176.963166ms

providertests/testdata/TestOpenAICompatObjectGeneration/xai-grok-code-fast/complex_object.yaml šŸ”—

@@ -0,0 +1,33 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 967
+    host: ""
+    body: '{"messages":[{"content":"Generate information about ''The Lord of the Rings'' book by J.R.R. Tolkien, including genres like fantasy and adventure, and its publication year (1954).","role":"user"}],"model":"grok-code-fast-1","max_tokens":4000,"tool_choice":{"function":{"name":"Book"},"type":"function"},"tools":[{"function":{"name":"Book","strict":false,"description":"A book with title, author, genres, and publication year","parameters":{"properties":{"author":{"properties":{"name":{"description":"Author''s name","type":"string"},"nationality":{"description":"Author''s nationality","type":"string"}},"required":["name","nationality"],"type":"object"},"genres":{"description":"List of genres","items":{"type":"string"},"type":"array"},"published_year":{"description":"Year the book was published","type":"integer"},"title":{"description":"The book title","type":"string"}},"required":["title","author","genres","published_year"],"type":"object"}},"type":"function"}]}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.x.ai/v1/chat/completions
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: '{"id":"2537f822-595e-1e8e-fe59-59c9761c15a1","object":"chat.completion","created":1762637624,"model":"grok-code-fast-1","choices":[{"index":0,"message":{"role":"assistant","content":"","tool_calls":[{"id":"call_05092142","function":{"name":"Book","arguments":"{\"author\":{\"name\":\"J.R.R. Tolkien\",\"nationality\":\"British\"},\"genres\":[\"fantasy\",\"adventure\"],\"published_year\":1954,\"title\":\"The Lord of the Rings\"}"},"type":"function"}],"refusal":null},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":496,"completion_tokens":81,"total_tokens":820,"prompt_tokens_details":{"text_tokens":496,"audio_tokens":0,"image_tokens":0,"cached_tokens":320},"completion_tokens_details":{"reasoning_tokens":243,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0},"num_sources_used":0},"system_fingerprint":"fp_10f00c862d"}'
+    headers:
+      Content-Type:
+      - application/json
+    status: 200 OK
+    code: 200
+    duration: 2.213765834s

providertests/testdata/TestOpenAICompatObjectGeneration/xai-grok-code-fast/simple_object.yaml šŸ”—

@@ -0,0 +1,33 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 631
+    host: ""
+    body: '{"messages":[{"content":"Generate information about a person named Alice who is 30 years old and lives in Paris.","role":"user"}],"model":"grok-code-fast-1","max_tokens":4000,"tool_choice":{"function":{"name":"Person"},"type":"function"},"tools":[{"function":{"name":"Person","strict":false,"description":"A person with name, age, and city","parameters":{"properties":{"age":{"description":"The person''s age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person''s name","type":"string"}},"required":["name","age","city"],"type":"object"}},"type":"function"}]}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.x.ai/v1/chat/completions
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: '{"id":"8d3d6258-cbe1-f871-55c0-32bd3487f2d7","object":"chat.completion","created":1762637621,"model":"grok-code-fast-1","choices":[{"index":0,"message":{"role":"assistant","content":"","tool_calls":[{"id":"call_38033184","function":{"name":"Person","arguments":"{\"age\":30,\"city\":\"Paris\",\"name\":\"Alice\"}"},"type":"function"}],"refusal":null},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":429,"completion_tokens":44,"total_tokens":630,"prompt_tokens_details":{"text_tokens":429,"audio_tokens":0,"image_tokens":0,"cached_tokens":320},"completion_tokens_details":{"reasoning_tokens":157,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0},"num_sources_used":0},"system_fingerprint":"fp_10f00c862d"}'
+    headers:
+      Content-Type:
+      - application/json
+    status: 200 OK
+    code: 200
+    duration: 1.615676541s

providertests/testdata/TestOpenAICompatObjectGeneration/xai-grok-code-fast/simple_object_streaming.yaml šŸ”—

@@ -0,0 +1,338 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 685
+    host: ""
+    body: '{"messages":[{"content":"Generate information about a person named Alice who is 30 years old and lives in Paris.","role":"user"}],"model":"grok-code-fast-1","max_tokens":4000,"stream_options":{"include_usage":true},"tool_choice":{"function":{"name":"Person"},"type":"function"},"tools":[{"function":{"name":"Person","strict":false,"description":"A person with name, age, and city","parameters":{"properties":{"age":{"description":"The person''s age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person''s name","type":"string"}},"required":["name","age","city"],"type":"object"}},"type":"function"}],"stream":true}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.x.ai/v1/chat/completions
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    body: |+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":"The","role":"assistant"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":" user"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":" asked"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":" to"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":" generate"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":" information"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":" about"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":" a"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":" person"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":" named"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":" Alice"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":" "}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":"30"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":" years"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":" old"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":" living"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":" in"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":" Paris"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":"."}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":" There's"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":" a"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":" tool"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":" called"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":" \""}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":"Person"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":"\""}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":" that"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":" matches"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":" this"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":" exactly"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":":"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":" it"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":" has"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":" name"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":" age"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":","}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":" and"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":" city"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":".\n"}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"tool_calls":[{"id":"call_71440968","function":{"name":"Person","arguments":"{\"age\":30,\"city\":\"Paris\",\"name\":\"Alice\"}"},"index":0,"type":"function"}]}}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[{"index":0,"delta":{"reasoning_content":"\n\n## Investigating user request  \n- The user asked for information about Alice, a 30-year-old living in Paris."},"finish_reason":"tool_calls"}],"system_fingerprint":"fp_10f00c862d"}
+
+      data: {"id":"8887124f-28ef-3f97-76c9-b348294a5bd5","object":"chat.completion.chunk","created":1762637623,"model":"grok-code-fast-1","choices":[],"usage":{"prompt_tokens":429,"completion_tokens":44,"total_tokens":622,"prompt_tokens_details":{"text_tokens":429,"audio_tokens":0,"image_tokens":0,"cached_tokens":384},"completion_tokens_details":{"reasoning_tokens":149,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0},"num_sources_used":0},"system_fingerprint":"fp_10f00c862d"}
+
+      data: [DONE]
+
+    headers:
+      Content-Type:
+      - text/event-stream
+    status: 200 OK
+    code: 200
+    duration: 244.686625ms

providertests/testdata/TestOpenAICompatObjectGeneration/zai-glm-4.5/complex_object.yaml šŸ”—

@@ -0,0 +1,33 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 958
+    host: ""
+    body: '{"messages":[{"content":"Generate information about ''The Lord of the Rings'' book by J.R.R. Tolkien, including genres like fantasy and adventure, and its publication year (1954).","role":"user"}],"model":"glm-4.5","max_tokens":4000,"tool_choice":{"function":{"name":"Book"},"type":"function"},"tools":[{"function":{"name":"Book","strict":false,"description":"A book with title, author, genres, and publication year","parameters":{"properties":{"author":{"properties":{"name":{"description":"Author''s name","type":"string"},"nationality":{"description":"Author''s nationality","type":"string"}},"required":["name","nationality"],"type":"object"},"genres":{"description":"List of genres","items":{"type":"string"},"type":"array"},"published_year":{"description":"Year the book was published","type":"integer"},"title":{"description":"The book title","type":"string"}},"required":["title","author","genres","published_year"],"type":"object"}},"type":"function"}]}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.z.ai/api/coding/paas/v4/chat/completions
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true

providertests/testdata/TestOpenAICompatObjectGeneration/zai-glm-4.5/simple_object.yaml šŸ”—

@@ -0,0 +1,33 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 622
+    host: ""
+    body: '{"messages":[{"content":"Generate information about a person named Alice who is 30 years old and lives in Paris.","role":"user"}],"model":"glm-4.5","max_tokens":4000,"tool_choice":{"function":{"name":"Person"},"type":"function"},"tools":[{"function":{"name":"Person","strict":false,"description":"A person with name, age, and city","parameters":{"properties":{"age":{"description":"The person''s age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person''s name","type":"string"}},"required":["name","age","city"],"type":"object"}},"type":"function"}]}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.z.ai/api/coding/paas/v4/chat/completions
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: '{"choices":[{"finish_reason":"tool_calls","index":0,"message":{"content":"","reasoning_content":"\nThe user is asking me to generate information about a person named Alice who is 30 years old and lives in Paris. Looking at the available function, I have a \"Person\" function that takes three required parameters:\n- name: \"Alice\" \n- age: 30\n- city: \"Paris\"\n\nAll the required parameters are provided, so I can call this function.","role":"assistant","tool_calls":[{"function":{"arguments":"{\"age\": 30, \"city\": \"Paris\", \"name\": \"Alice\"}","name":"Person"},"id":"call_-8178200539482324764","index":0,"type":"function"}]}}],"created":1762637635,"id":"2025110905335254041e69de3b448e","model":"glm-4.5","request_id":"2025110905335254041e69de3b448e","usage":{"completion_tokens":121,"prompt_tokens":230,"prompt_tokens_details":{"cached_tokens":43},"total_tokens":351}}'
+    headers:
+      Content-Type:
+      - application/json; charset=UTF-8
+    status: 200 OK
+    code: 200
+    duration: 6.174196125s

providertests/testdata/TestOpenAICompatObjectGeneration/zai-glm-4.5/simple_object_streaming.yaml šŸ”—

@@ -0,0 +1,200 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 676
+    host: ""
+    body: '{"messages":[{"content":"Generate information about a person named Alice who is 30 years old and lives in Paris.","role":"user"}],"model":"glm-4.5","max_tokens":4000,"stream_options":{"include_usage":true},"tool_choice":{"function":{"name":"Person"},"type":"function"},"tools":[{"function":{"name":"Person","strict":false,"description":"A person with name, age, and city","parameters":{"properties":{"age":{"description":"The person''s age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person''s name","type":"string"}},"required":["name","age","city"],"type":"object"}},"type":"function"}],"stream":true}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.z.ai/api/coding/paas/v4/chat/completions
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    body: |+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning_content":"\n"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"The"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" user"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" wants"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" me"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" to"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" generate"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" information"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" about"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" a"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" person"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" with"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" specific"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" details"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":":\n"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"-"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" Name"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":":"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" Alice"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning_content":"\n"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"-"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" Age"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":":"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning_content":" "}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"30"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning_content":"\n"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"-"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" City"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":":"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" Paris"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning_content":"\n\n"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"I"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" have"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" a"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" Person"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" function"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" available"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" that"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" takes"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" these"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" exact"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" parameters"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":":\n"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"-"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" name"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" ("}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"required"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"):"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" \""}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"Alice"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"\"\n"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"-"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" age"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" ("}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"required"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"):"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning_content":" "}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"30"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning_content":"\n"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"-"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" city"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" ("}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"required"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"):"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" \""}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"Paris"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"\"\n\n"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"All"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" required"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" parameters"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" are"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" provided"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":","}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" so"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" I"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" can"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" make"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" the"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" function"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":" call"}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"role":"assistant","reasoning_content":"."}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"delta":{"tool_calls":[{"id":"call_11f61396a06b43ebb9ab7992","index":0,"type":"function","function":{"name":"Person","arguments":"{\"age\":30,\"city\":\"Paris\",\"name\":\"Alice\"}"}}]}}]}
+
+      data: {"id":"2025110905335643ffc6855fee49b2","created":1762637636,"model":"glm-4.5","choices":[{"index":0,"finish_reason":"tool_calls","delta":{"role":"assistant","content":""}}],"usage":{"prompt_tokens":230,"completion_tokens":112,"total_tokens":342,"prompt_tokens_details":{"cached_tokens":43}}}
+
+      data: [DONE]
+
+    headers:
+      Content-Type:
+      - text/event-stream;charset=UTF-8
+    status: 200 OK
+    code: 200
+    duration: 1.864133208s

providertests/testdata/TestOpenAIObjectGeneration/openai-gpt-4o-mini/complex_object.yaml šŸ”—

@@ -0,0 +1,69 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 968
+    host: ""
+    body: '{"messages":[{"content":"Generate information about ''The Lord of the Rings'' book by J.R.R. Tolkien, including genres like fantasy and adventure, and its publication year (1954).","role":"user"}],"model":"gpt-4o-mini","max_tokens":4000,"response_format":{"json_schema":{"name":"Book","strict":true,"description":"A book with title, author, genres, and publication year","schema":{"additionalProperties":false,"properties":{"author":{"additionalProperties":false,"properties":{"name":{"description":"Author''s name","type":"string"},"nationality":{"description":"Author''s nationality","type":"string"}},"required":["name","nationality"],"type":"object"},"genres":{"description":"List of genres","items":{"type":"string"},"type":"array"},"published_year":{"description":"Year the book was published","type":"integer"},"title":{"description":"The book title","type":"string"}},"required":["title","author","genres","published_year"],"type":"object"}},"type":"json_schema"}}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.openai.com/v1/chat/completions
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: |
+      {
+        "id": "chatcmpl-CZkh7y0v50HiK3bHkckDQM27mAxaF",
+        "object": "chat.completion",
+        "created": 1762637009,
+        "model": "gpt-4o-mini-2024-07-18",
+        "choices": [
+          {
+            "index": 0,
+            "message": {
+              "role": "assistant",
+              "content": "{\"author\":{\"name\":\"J.R.R. Tolkien\",\"nationality\":\"British\"},\"genres\":[\"Fantasy\",\"Adventure\",\"Epic\",\"High Fantasy\"],\"published_year\":1954,\"title\":\"The Lord of the Rings\"}",
+              "refusal": null,
+              "annotations": []
+            },
+            "logprobs": null,
+            "finish_reason": "stop"
+          }
+        ],
+        "usage": {
+          "prompt_tokens": 160,
+          "completion_tokens": 43,
+          "total_tokens": 203,
+          "prompt_tokens_details": {
+            "cached_tokens": 0,
+            "audio_tokens": 0
+          },
+          "completion_tokens_details": {
+            "reasoning_tokens": 0,
+            "audio_tokens": 0,
+            "accepted_prediction_tokens": 0,
+            "rejected_prediction_tokens": 0
+          }
+        },
+        "service_tier": "default",
+        "system_fingerprint": "fp_560af6e559"
+      }
+    headers:
+      Content-Type:
+      - application/json
+    status: 200 OK
+    code: 200
+    duration: 1.814804666s

providertests/testdata/TestOpenAIObjectGeneration/openai-gpt-4o-mini/simple_object.yaml šŸ”—

@@ -0,0 +1,69 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 601
+    host: ""
+    body: '{"messages":[{"content":"Generate information about a person named Alice who is 30 years old and lives in Paris.","role":"user"}],"model":"gpt-4o-mini","max_tokens":4000,"response_format":{"json_schema":{"name":"Person","strict":true,"description":"A person with name, age, and city","schema":{"additionalProperties":false,"properties":{"age":{"description":"The person''s age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person''s name","type":"string"}},"required":["name","age","city"],"type":"object"}},"type":"json_schema"}}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.openai.com/v1/chat/completions
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: |
+      {
+        "id": "chatcmpl-CZkh6UpzjVoPIwiP6798RaJh45GaE",
+        "object": "chat.completion",
+        "created": 1762637008,
+        "model": "gpt-4o-mini-2024-07-18",
+        "choices": [
+          {
+            "index": 0,
+            "message": {
+              "role": "assistant",
+              "content": "{\"age\":30,\"city\":\"Paris\",\"name\":\"Alice\"}",
+              "refusal": null,
+              "annotations": []
+            },
+            "logprobs": null,
+            "finish_reason": "stop"
+          }
+        ],
+        "usage": {
+          "prompt_tokens": 99,
+          "completion_tokens": 13,
+          "total_tokens": 112,
+          "prompt_tokens_details": {
+            "cached_tokens": 0,
+            "audio_tokens": 0
+          },
+          "completion_tokens_details": {
+            "reasoning_tokens": 0,
+            "audio_tokens": 0,
+            "accepted_prediction_tokens": 0,
+            "rejected_prediction_tokens": 0
+          }
+        },
+        "service_tier": "default",
+        "system_fingerprint": "fp_560af6e559"
+      }
+    headers:
+      Content-Type:
+      - application/json
+    status: 200 OK
+    code: 200
+    duration: 926.519208ms

providertests/testdata/TestOpenAIObjectGeneration/openai-gpt-4o-mini/simple_object_streaming.yaml šŸ”—

@@ -0,0 +1,66 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 655
+    host: ""
+    body: '{"messages":[{"content":"Generate information about a person named Alice who is 30 years old and lives in Paris.","role":"user"}],"model":"gpt-4o-mini","max_tokens":4000,"stream_options":{"include_usage":true},"response_format":{"json_schema":{"name":"Person","strict":true,"description":"A person with name, age, and city","schema":{"additionalProperties":false,"properties":{"age":{"description":"The person''s age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person''s name","type":"string"}},"required":["name","age","city"],"type":"object"}},"type":"json_schema"},"stream":true}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.openai.com/v1/chat/completions
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    body: |+
+      data: {"id":"chatcmpl-CZkh6JIHchpBryosw9Q1zBULiOgQP","object":"chat.completion.chunk","created":1762637008,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_560af6e559","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"FopRVPvq1"}
+
+      data: {"id":"chatcmpl-CZkh6JIHchpBryosw9Q1zBULiOgQP","object":"chat.completion.chunk","created":1762637008,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_560af6e559","choices":[{"index":0,"delta":{"content":"{\""},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"vcH0mk1c"}
+
+      data: {"id":"chatcmpl-CZkh6JIHchpBryosw9Q1zBULiOgQP","object":"chat.completion.chunk","created":1762637008,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_560af6e559","choices":[{"index":0,"delta":{"content":"age"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Yp08y6zs"}
+
+      data: {"id":"chatcmpl-CZkh6JIHchpBryosw9Q1zBULiOgQP","object":"chat.completion.chunk","created":1762637008,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_560af6e559","choices":[{"index":0,"delta":{"content":"\":"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"yEwAOmw5"}
+
+      data: {"id":"chatcmpl-CZkh6JIHchpBryosw9Q1zBULiOgQP","object":"chat.completion.chunk","created":1762637008,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_560af6e559","choices":[{"index":0,"delta":{"content":"30"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"PedSD3PYu"}
+
+      data: {"id":"chatcmpl-CZkh6JIHchpBryosw9Q1zBULiOgQP","object":"chat.completion.chunk","created":1762637008,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_560af6e559","choices":[{"index":0,"delta":{"content":",\""},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"I5M18YOR"}
+
+      data: {"id":"chatcmpl-CZkh6JIHchpBryosw9Q1zBULiOgQP","object":"chat.completion.chunk","created":1762637008,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_560af6e559","choices":[{"index":0,"delta":{"content":"city"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"9ReOCb0"}
+
+      data: {"id":"chatcmpl-CZkh6JIHchpBryosw9Q1zBULiOgQP","object":"chat.completion.chunk","created":1762637008,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_560af6e559","choices":[{"index":0,"delta":{"content":"\":\""},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"dRYkzy"}
+
+      data: {"id":"chatcmpl-CZkh6JIHchpBryosw9Q1zBULiOgQP","object":"chat.completion.chunk","created":1762637008,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_560af6e559","choices":[{"index":0,"delta":{"content":"Paris"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"clnEfR"}
+
+      data: {"id":"chatcmpl-CZkh6JIHchpBryosw9Q1zBULiOgQP","object":"chat.completion.chunk","created":1762637008,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_560af6e559","choices":[{"index":0,"delta":{"content":"\",\""},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"y8uwQU"}
+
+      data: {"id":"chatcmpl-CZkh6JIHchpBryosw9Q1zBULiOgQP","object":"chat.completion.chunk","created":1762637008,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_560af6e559","choices":[{"index":0,"delta":{"content":"name"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"hoFrRnB"}
+
+      data: {"id":"chatcmpl-CZkh6JIHchpBryosw9Q1zBULiOgQP","object":"chat.completion.chunk","created":1762637008,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_560af6e559","choices":[{"index":0,"delta":{"content":"\":\""},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"f2tFLb"}
+
+      data: {"id":"chatcmpl-CZkh6JIHchpBryosw9Q1zBULiOgQP","object":"chat.completion.chunk","created":1762637008,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_560af6e559","choices":[{"index":0,"delta":{"content":"Alice"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"MdtNOk"}
+
+      data: {"id":"chatcmpl-CZkh6JIHchpBryosw9Q1zBULiOgQP","object":"chat.completion.chunk","created":1762637008,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_560af6e559","choices":[{"index":0,"delta":{"content":"\"}"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"4NFrFBQr"}
+
+      data: {"id":"chatcmpl-CZkh6JIHchpBryosw9Q1zBULiOgQP","object":"chat.completion.chunk","created":1762637008,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_560af6e559","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null,"obfuscation":"RyhJI"}
+
+      data: {"id":"chatcmpl-CZkh6JIHchpBryosw9Q1zBULiOgQP","object":"chat.completion.chunk","created":1762637008,"model":"gpt-4o-mini-2024-07-18","service_tier":"default","system_fingerprint":"fp_560af6e559","choices":[],"usage":{"prompt_tokens":99,"completion_tokens":13,"total_tokens":112,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"obfuscation":"a9TEmM6E7"}
+
+      data: [DONE]
+
+    headers:
+      Content-Type:
+      - text/event-stream; charset=utf-8
+    status: 200 OK
+    code: 200
+    duration: 611.309416ms

providertests/testdata/TestOpenAIObjectGeneration/openai-gpt-4o/complex_object.yaml šŸ”—

@@ -0,0 +1,69 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 963
+    host: ""
+    body: '{"messages":[{"content":"Generate information about ''The Lord of the Rings'' book by J.R.R. Tolkien, including genres like fantasy and adventure, and its publication year (1954).","role":"user"}],"model":"gpt-4o","max_tokens":4000,"response_format":{"json_schema":{"name":"Book","strict":true,"description":"A book with title, author, genres, and publication year","schema":{"additionalProperties":false,"properties":{"author":{"additionalProperties":false,"properties":{"name":{"description":"Author''s name","type":"string"},"nationality":{"description":"Author''s nationality","type":"string"}},"required":["name","nationality"],"type":"object"},"genres":{"description":"List of genres","items":{"type":"string"},"type":"array"},"published_year":{"description":"Year the book was published","type":"integer"},"title":{"description":"The book title","type":"string"}},"required":["title","author","genres","published_year"],"type":"object"}},"type":"json_schema"}}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.openai.com/v1/chat/completions
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: |
+      {
+        "id": "chatcmpl-CZkh4icu3jixl4IcoAjWVF1mV1oYu",
+        "object": "chat.completion",
+        "created": 1762637006,
+        "model": "gpt-4o-2024-08-06",
+        "choices": [
+          {
+            "index": 0,
+            "message": {
+              "role": "assistant",
+              "content": "{\"author\":{\"name\":\"J.R.R. Tolkien\",\"nationality\":\"British\"},\"genres\":[\"Fantasy\",\"Adventure\",\"Epic\"],\"published_year\":1954,\"title\":\"The Lord of the Rings\"}",
+              "refusal": null,
+              "annotations": []
+            },
+            "logprobs": null,
+            "finish_reason": "stop"
+          }
+        ],
+        "usage": {
+          "prompt_tokens": 160,
+          "completion_tokens": 40,
+          "total_tokens": 200,
+          "prompt_tokens_details": {
+            "cached_tokens": 0,
+            "audio_tokens": 0
+          },
+          "completion_tokens_details": {
+            "reasoning_tokens": 0,
+            "audio_tokens": 0,
+            "accepted_prediction_tokens": 0,
+            "rejected_prediction_tokens": 0
+          }
+        },
+        "service_tier": "default",
+        "system_fingerprint": "fp_b1442291a8"
+      }
+    headers:
+      Content-Type:
+      - application/json
+    status: 200 OK
+    code: 200
+    duration: 1.01740425s

providertests/testdata/TestOpenAIObjectGeneration/openai-gpt-4o/simple_object.yaml šŸ”—

@@ -0,0 +1,69 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 596
+    host: ""
+    body: '{"messages":[{"content":"Generate information about a person named Alice who is 30 years old and lives in Paris.","role":"user"}],"model":"gpt-4o","max_tokens":4000,"response_format":{"json_schema":{"name":"Person","strict":true,"description":"A person with name, age, and city","schema":{"additionalProperties":false,"properties":{"age":{"description":"The person''s age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person''s name","type":"string"}},"required":["name","age","city"],"type":"object"}},"type":"json_schema"}}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.openai.com/v1/chat/completions
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: |
+      {
+        "id": "chatcmpl-CZkh2yUHIKUTvXaI7RuJhKssQ3u8J",
+        "object": "chat.completion",
+        "created": 1762637004,
+        "model": "gpt-4o-2024-08-06",
+        "choices": [
+          {
+            "index": 0,
+            "message": {
+              "role": "assistant",
+              "content": "{\"age\":30,\"city\":\"Paris\",\"name\":\"Alice\"}",
+              "refusal": null,
+              "annotations": []
+            },
+            "logprobs": null,
+            "finish_reason": "stop"
+          }
+        ],
+        "usage": {
+          "prompt_tokens": 99,
+          "completion_tokens": 13,
+          "total_tokens": 112,
+          "prompt_tokens_details": {
+            "cached_tokens": 0,
+            "audio_tokens": 0
+          },
+          "completion_tokens_details": {
+            "reasoning_tokens": 0,
+            "audio_tokens": 0,
+            "accepted_prediction_tokens": 0,
+            "rejected_prediction_tokens": 0
+          }
+        },
+        "service_tier": "default",
+        "system_fingerprint": "fp_cbf1785567"
+      }
+    headers:
+      Content-Type:
+      - application/json
+    status: 200 OK
+    code: 200
+    duration: 866.25175ms

providertests/testdata/TestOpenAIObjectGeneration/openai-gpt-4o/simple_object_streaming.yaml šŸ”—

@@ -0,0 +1,66 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 650
+    host: ""
+    body: '{"messages":[{"content":"Generate information about a person named Alice who is 30 years old and lives in Paris.","role":"user"}],"model":"gpt-4o","max_tokens":4000,"stream_options":{"include_usage":true},"response_format":{"json_schema":{"name":"Person","strict":true,"description":"A person with name, age, and city","schema":{"additionalProperties":false,"properties":{"age":{"description":"The person''s age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person''s name","type":"string"}},"required":["name","age","city"],"type":"object"}},"type":"json_schema"},"stream":true}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.openai.com/v1/chat/completions
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    body: |+
+      data: {"id":"chatcmpl-CZkh26YobsyDr3tfAEylpsGb7mCKf","object":"chat.completion.chunk","created":1762637004,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"Q7EWId38WGszkS"}
+
+      data: {"id":"chatcmpl-CZkh26YobsyDr3tfAEylpsGb7mCKf","object":"chat.completion.chunk","created":1762637004,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"{\""},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"WDGuponLquJpz"}
+
+      data: {"id":"chatcmpl-CZkh26YobsyDr3tfAEylpsGb7mCKf","object":"chat.completion.chunk","created":1762637004,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"age"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"rCGUFNPg6GmYo"}
+
+      data: {"id":"chatcmpl-CZkh26YobsyDr3tfAEylpsGb7mCKf","object":"chat.completion.chunk","created":1762637004,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"\":"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"wEfLaS45CzBz3"}
+
+      data: {"id":"chatcmpl-CZkh26YobsyDr3tfAEylpsGb7mCKf","object":"chat.completion.chunk","created":1762637004,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"30"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"FXGR1dTIvN4Qp6"}
+
+      data: {"id":"chatcmpl-CZkh26YobsyDr3tfAEylpsGb7mCKf","object":"chat.completion.chunk","created":1762637004,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":",\""},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"KmEPUsJzFtpi5"}
+
+      data: {"id":"chatcmpl-CZkh26YobsyDr3tfAEylpsGb7mCKf","object":"chat.completion.chunk","created":1762637004,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"city"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"TxOjAiMNR8i2"}
+
+      data: {"id":"chatcmpl-CZkh26YobsyDr3tfAEylpsGb7mCKf","object":"chat.completion.chunk","created":1762637004,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"\":\""},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"ZdagyqxzdDT"}
+
+      data: {"id":"chatcmpl-CZkh26YobsyDr3tfAEylpsGb7mCKf","object":"chat.completion.chunk","created":1762637004,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"Paris"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"7AV1wjObQw9"}
+
+      data: {"id":"chatcmpl-CZkh26YobsyDr3tfAEylpsGb7mCKf","object":"chat.completion.chunk","created":1762637004,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"\",\""},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"ecbr6MtR7cR"}
+
+      data: {"id":"chatcmpl-CZkh26YobsyDr3tfAEylpsGb7mCKf","object":"chat.completion.chunk","created":1762637004,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"name"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"swinVIK7vWIt"}
+
+      data: {"id":"chatcmpl-CZkh26YobsyDr3tfAEylpsGb7mCKf","object":"chat.completion.chunk","created":1762637004,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"\":\""},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"V94lL7Iu9CQ"}
+
+      data: {"id":"chatcmpl-CZkh26YobsyDr3tfAEylpsGb7mCKf","object":"chat.completion.chunk","created":1762637004,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"Alice"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"tegZ0PohAUA"}
+
+      data: {"id":"chatcmpl-CZkh26YobsyDr3tfAEylpsGb7mCKf","object":"chat.completion.chunk","created":1762637004,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{"content":"\"}"},"logprobs":null,"finish_reason":null}],"usage":null,"obfuscation":"npvxJxBLlAEZV"}
+
+      data: {"id":"chatcmpl-CZkh26YobsyDr3tfAEylpsGb7mCKf","object":"chat.completion.chunk","created":1762637004,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}],"usage":null,"obfuscation":"gxtfhzu7ZT"}
+
+      data: {"id":"chatcmpl-CZkh26YobsyDr3tfAEylpsGb7mCKf","object":"chat.completion.chunk","created":1762637004,"model":"gpt-4o-2024-08-06","service_tier":"default","system_fingerprint":"fp_cbf1785567","choices":[],"usage":{"prompt_tokens":99,"completion_tokens":13,"total_tokens":112,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":0,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"obfuscation":"mo9vda7oGi3jDi"}
+
+      data: [DONE]
+
+    headers:
+      Content-Type:
+      - text/event-stream; charset=utf-8
+    status: 200 OK
+    code: 200
+    duration: 878.195625ms

providertests/testdata/TestOpenAIObjectGeneration/openai-gpt-5/complex_object.yaml šŸ”—

@@ -0,0 +1,68 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 973
+    host: ""
+    body: '{"messages":[{"content":"Generate information about ''The Lord of the Rings'' book by J.R.R. Tolkien, including genres like fantasy and adventure, and its publication year (1954).","role":"user"}],"model":"gpt-5","max_completion_tokens":4000,"response_format":{"json_schema":{"name":"Book","strict":true,"description":"A book with title, author, genres, and publication year","schema":{"additionalProperties":false,"properties":{"author":{"additionalProperties":false,"properties":{"name":{"description":"Author''s name","type":"string"},"nationality":{"description":"Author''s nationality","type":"string"}},"required":["name","nationality"],"type":"object"},"genres":{"description":"List of genres","items":{"type":"string"},"type":"array"},"published_year":{"description":"Year the book was published","type":"integer"},"title":{"description":"The book title","type":"string"}},"required":["title","author","genres","published_year"],"type":"object"}},"type":"json_schema"}}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.openai.com/v1/chat/completions
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: |
+      {
+        "id": "chatcmpl-CZkhdPgupBvka4xhGSEPQHw0QQKRf",
+        "object": "chat.completion",
+        "created": 1762637041,
+        "model": "gpt-5-2025-08-07",
+        "choices": [
+          {
+            "index": 0,
+            "message": {
+              "role": "assistant",
+              "content": "{\"author\":{\"name\":\"J.R.R. Tolkien\",\"nationality\":\"British\"},\"genres\":[\"Fantasy\",\"Adventure\"],\"published_year\":1954,\"title\":\"The Lord of the Rings\"}",
+              "refusal": null,
+              "annotations": []
+            },
+            "finish_reason": "stop"
+          }
+        ],
+        "usage": {
+          "prompt_tokens": 156,
+          "completion_tokens": 370,
+          "total_tokens": 526,
+          "prompt_tokens_details": {
+            "cached_tokens": 0,
+            "audio_tokens": 0
+          },
+          "completion_tokens_details": {
+            "reasoning_tokens": 320,
+            "audio_tokens": 0,
+            "accepted_prediction_tokens": 0,
+            "rejected_prediction_tokens": 0
+          }
+        },
+        "service_tier": "default",
+        "system_fingerprint": null
+      }
+    headers:
+      Content-Type:
+      - application/json
+    status: 200 OK
+    code: 200
+    duration: 6.754405583s

providertests/testdata/TestOpenAIObjectGeneration/openai-gpt-5/simple_object.yaml šŸ”—

@@ -0,0 +1,68 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 606
+    host: ""
+    body: '{"messages":[{"content":"Generate information about a person named Alice who is 30 years old and lives in Paris.","role":"user"}],"model":"gpt-5","max_completion_tokens":4000,"response_format":{"json_schema":{"name":"Person","strict":true,"description":"A person with name, age, and city","schema":{"additionalProperties":false,"properties":{"age":{"description":"The person''s age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person''s name","type":"string"}},"required":["name","age","city"],"type":"object"}},"type":"json_schema"}}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.openai.com/v1/chat/completions
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: |
+      {
+        "id": "chatcmpl-CZkhB1eWodsLQ6Gs98f4VUjNlzKV6",
+        "object": "chat.completion",
+        "created": 1762637013,
+        "model": "gpt-5-2025-08-07",
+        "choices": [
+          {
+            "index": 0,
+            "message": {
+              "role": "assistant",
+              "content": "{\"age\":30,\"city\":\"Paris\",\"name\":\"Alice\"}",
+              "refusal": null,
+              "annotations": []
+            },
+            "finish_reason": "stop"
+          }
+        ],
+        "usage": {
+          "prompt_tokens": 95,
+          "completion_tokens": 793,
+          "total_tokens": 888,
+          "prompt_tokens_details": {
+            "cached_tokens": 0,
+            "audio_tokens": 0
+          },
+          "completion_tokens_details": {
+            "reasoning_tokens": 768,
+            "audio_tokens": 0,
+            "accepted_prediction_tokens": 0,
+            "rejected_prediction_tokens": 0
+          }
+        },
+        "service_tier": "default",
+        "system_fingerprint": null
+      }
+    headers:
+      Content-Type:
+      - application/json
+    status: 200 OK
+    code: 200
+    duration: 20.674063542s

providertests/testdata/TestOpenAIObjectGeneration/openai-gpt-5/simple_object_streaming.yaml šŸ”—

@@ -0,0 +1,66 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 660
+    host: ""
+    body: '{"messages":[{"content":"Generate information about a person named Alice who is 30 years old and lives in Paris.","role":"user"}],"model":"gpt-5","max_completion_tokens":4000,"stream_options":{"include_usage":true},"response_format":{"json_schema":{"name":"Person","strict":true,"description":"A person with name, age, and city","schema":{"additionalProperties":false,"properties":{"age":{"description":"The person''s age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person''s name","type":"string"}},"required":["name","age","city"],"type":"object"}},"type":"json_schema"},"stream":true}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.openai.com/v1/chat/completions
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    body: |+
+      data: {"id":"chatcmpl-CZkhWAnMkiLSCMK7u8Hid2v2V0JKT","object":"chat.completion.chunk","created":1762637034,"model":"gpt-5-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"finish_reason":null}],"usage":null,"obfuscation":"XOkQ64PZpE"}
+
+      data: {"id":"chatcmpl-CZkhWAnMkiLSCMK7u8Hid2v2V0JKT","object":"chat.completion.chunk","created":1762637034,"model":"gpt-5-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"{\""},"finish_reason":null}],"usage":null,"obfuscation":"Ni84MpFSw"}
+
+      data: {"id":"chatcmpl-CZkhWAnMkiLSCMK7u8Hid2v2V0JKT","object":"chat.completion.chunk","created":1762637034,"model":"gpt-5-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"age"},"finish_reason":null}],"usage":null,"obfuscation":"HWAfWDi80"}
+
+      data: {"id":"chatcmpl-CZkhWAnMkiLSCMK7u8Hid2v2V0JKT","object":"chat.completion.chunk","created":1762637034,"model":"gpt-5-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"\":"},"finish_reason":null}],"usage":null,"obfuscation":"MgEKDfqen"}
+
+      data: {"id":"chatcmpl-CZkhWAnMkiLSCMK7u8Hid2v2V0JKT","object":"chat.completion.chunk","created":1762637034,"model":"gpt-5-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"30"},"finish_reason":null}],"usage":null,"obfuscation":"xSUuDmVnPA"}
+
+      data: {"id":"chatcmpl-CZkhWAnMkiLSCMK7u8Hid2v2V0JKT","object":"chat.completion.chunk","created":1762637034,"model":"gpt-5-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":",\""},"finish_reason":null}],"usage":null,"obfuscation":"rg02xRX4E"}
+
+      data: {"id":"chatcmpl-CZkhWAnMkiLSCMK7u8Hid2v2V0JKT","object":"chat.completion.chunk","created":1762637034,"model":"gpt-5-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"city"},"finish_reason":null}],"usage":null,"obfuscation":"Otwkppxs"}
+
+      data: {"id":"chatcmpl-CZkhWAnMkiLSCMK7u8Hid2v2V0JKT","object":"chat.completion.chunk","created":1762637034,"model":"gpt-5-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"\":\""},"finish_reason":null}],"usage":null,"obfuscation":"WryBuCt"}
+
+      data: {"id":"chatcmpl-CZkhWAnMkiLSCMK7u8Hid2v2V0JKT","object":"chat.completion.chunk","created":1762637034,"model":"gpt-5-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"Paris"},"finish_reason":null}],"usage":null,"obfuscation":"xHwu9qY"}
+
+      data: {"id":"chatcmpl-CZkhWAnMkiLSCMK7u8Hid2v2V0JKT","object":"chat.completion.chunk","created":1762637034,"model":"gpt-5-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"\",\""},"finish_reason":null}],"usage":null,"obfuscation":"5fVw4lG"}
+
+      data: {"id":"chatcmpl-CZkhWAnMkiLSCMK7u8Hid2v2V0JKT","object":"chat.completion.chunk","created":1762637034,"model":"gpt-5-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"name"},"finish_reason":null}],"usage":null,"obfuscation":"9Ionk7pl"}
+
+      data: {"id":"chatcmpl-CZkhWAnMkiLSCMK7u8Hid2v2V0JKT","object":"chat.completion.chunk","created":1762637034,"model":"gpt-5-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"\":\""},"finish_reason":null}],"usage":null,"obfuscation":"5vKRCJC"}
+
+      data: {"id":"chatcmpl-CZkhWAnMkiLSCMK7u8Hid2v2V0JKT","object":"chat.completion.chunk","created":1762637034,"model":"gpt-5-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"Alice"},"finish_reason":null}],"usage":null,"obfuscation":"99RRfh5"}
+
+      data: {"id":"chatcmpl-CZkhWAnMkiLSCMK7u8Hid2v2V0JKT","object":"chat.completion.chunk","created":1762637034,"model":"gpt-5-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"\"}"},"finish_reason":null}],"usage":null,"obfuscation":"mpjzsT6aj"}
+
+      data: {"id":"chatcmpl-CZkhWAnMkiLSCMK7u8Hid2v2V0JKT","object":"chat.completion.chunk","created":1762637034,"model":"gpt-5-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":null,"obfuscation":"Y02J6J"}
+
+      data: {"id":"chatcmpl-CZkhWAnMkiLSCMK7u8Hid2v2V0JKT","object":"chat.completion.chunk","created":1762637034,"model":"gpt-5-2025-08-07","service_tier":"default","system_fingerprint":null,"choices":[],"usage":{"prompt_tokens":95,"completion_tokens":345,"total_tokens":440,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":320,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"obfuscation":"oT4O7bI"}
+
+      data: [DONE]
+
+    headers:
+      Content-Type:
+      - text/event-stream; charset=utf-8
+    status: 200 OK
+    code: 200
+    duration: 6.848382125s

providertests/testdata/TestOpenAIObjectGeneration/openai-o4-mini/complex_object.yaml šŸ”—

@@ -0,0 +1,68 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 975
+    host: ""
+    body: '{"messages":[{"content":"Generate information about ''The Lord of the Rings'' book by J.R.R. Tolkien, including genres like fantasy and adventure, and its publication year (1954).","role":"user"}],"model":"o4-mini","max_completion_tokens":4000,"response_format":{"json_schema":{"name":"Book","strict":true,"description":"A book with title, author, genres, and publication year","schema":{"additionalProperties":false,"properties":{"author":{"additionalProperties":false,"properties":{"name":{"description":"Author''s name","type":"string"},"nationality":{"description":"Author''s nationality","type":"string"}},"required":["name","nationality"],"type":"object"},"genres":{"description":"List of genres","items":{"type":"string"},"type":"array"},"published_year":{"description":"Year the book was published","type":"integer"},"title":{"description":"The book title","type":"string"}},"required":["title","author","genres","published_year"],"type":"object"}},"type":"json_schema"}}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.openai.com/v1/chat/completions
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: |
+      {
+        "id": "chatcmpl-CZki5CYlesaMKCtCSbinnMUnYGvLR",
+        "object": "chat.completion",
+        "created": 1762637069,
+        "model": "o4-mini-2025-04-16",
+        "choices": [
+          {
+            "index": 0,
+            "message": {
+              "role": "assistant",
+              "content": "{\"author\":{\"name\":\"J.R.R. Tolkien\",\"nationality\":\"British\"},\"genres\":[\"Fantasy\",\"Adventure\"],\"published_year\":1954,\"title\":\"The Lord of the Rings\"}",
+              "refusal": null,
+              "annotations": []
+            },
+            "finish_reason": "stop"
+          }
+        ],
+        "usage": {
+          "prompt_tokens": 156,
+          "completion_tokens": 187,
+          "total_tokens": 343,
+          "prompt_tokens_details": {
+            "cached_tokens": 0,
+            "audio_tokens": 0
+          },
+          "completion_tokens_details": {
+            "reasoning_tokens": 128,
+            "audio_tokens": 0,
+            "accepted_prediction_tokens": 0,
+            "rejected_prediction_tokens": 0
+          }
+        },
+        "service_tier": "default",
+        "system_fingerprint": null
+      }
+    headers:
+      Content-Type:
+      - application/json
+    status: 200 OK
+    code: 200
+    duration: 3.853936875s

providertests/testdata/TestOpenAIObjectGeneration/openai-o4-mini/simple_object.yaml šŸ”—

@@ -0,0 +1,68 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 608
+    host: ""
+    body: '{"messages":[{"content":"Generate information about a person named Alice who is 30 years old and lives in Paris.","role":"user"}],"model":"o4-mini","max_completion_tokens":4000,"response_format":{"json_schema":{"name":"Person","strict":true,"description":"A person with name, age, and city","schema":{"additionalProperties":false,"properties":{"age":{"description":"The person''s age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person''s name","type":"string"}},"required":["name","age","city"],"type":"object"}},"type":"json_schema"}}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.openai.com/v1/chat/completions
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: |
+      {
+        "id": "chatcmpl-CZkhy2Semw4FSy9i59cUlONdYx0mh",
+        "object": "chat.completion",
+        "created": 1762637062,
+        "model": "o4-mini-2025-04-16",
+        "choices": [
+          {
+            "index": 0,
+            "message": {
+              "role": "assistant",
+              "content": "{\"age\":30,\"city\":\"Paris\",\"name\":\"Alice\"}",
+              "refusal": null,
+              "annotations": []
+            },
+            "finish_reason": "stop"
+          }
+        ],
+        "usage": {
+          "prompt_tokens": 95,
+          "completion_tokens": 162,
+          "total_tokens": 257,
+          "prompt_tokens_details": {
+            "cached_tokens": 0,
+            "audio_tokens": 0
+          },
+          "completion_tokens_details": {
+            "reasoning_tokens": 128,
+            "audio_tokens": 0,
+            "accepted_prediction_tokens": 0,
+            "rejected_prediction_tokens": 0
+          }
+        },
+        "service_tier": "default",
+        "system_fingerprint": null
+      }
+    headers:
+      Content-Type:
+      - application/json
+    status: 200 OK
+    code: 200
+    duration: 3.135977959s

providertests/testdata/TestOpenAIObjectGeneration/openai-o4-mini/simple_object_streaming.yaml šŸ”—

@@ -0,0 +1,66 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 662
+    host: ""
+    body: '{"messages":[{"content":"Generate information about a person named Alice who is 30 years old and lives in Paris.","role":"user"}],"model":"o4-mini","max_completion_tokens":4000,"stream_options":{"include_usage":true},"response_format":{"json_schema":{"name":"Person","strict":true,"description":"A person with name, age, and city","schema":{"additionalProperties":false,"properties":{"age":{"description":"The person''s age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person''s name","type":"string"}},"required":["name","age","city"],"type":"object"}},"type":"json_schema"},"stream":true}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.openai.com/v1/chat/completions
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    body: |+
+      data: {"id":"chatcmpl-CZki1J9qrKifHLYfd83Fec01s4z8D","object":"chat.completion.chunk","created":1762637065,"model":"o4-mini-2025-04-16","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"finish_reason":null}],"usage":null,"obfuscation":"1u4UMeKG"}
+
+      data: {"id":"chatcmpl-CZki1J9qrKifHLYfd83Fec01s4z8D","object":"chat.completion.chunk","created":1762637065,"model":"o4-mini-2025-04-16","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"{\""},"finish_reason":null}],"usage":null,"obfuscation":"Q5UxGax"}
+
+      data: {"id":"chatcmpl-CZki1J9qrKifHLYfd83Fec01s4z8D","object":"chat.completion.chunk","created":1762637065,"model":"o4-mini-2025-04-16","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"age"},"finish_reason":null}],"usage":null,"obfuscation":"PC0HaNl"}
+
+      data: {"id":"chatcmpl-CZki1J9qrKifHLYfd83Fec01s4z8D","object":"chat.completion.chunk","created":1762637065,"model":"o4-mini-2025-04-16","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"\":"},"finish_reason":null}],"usage":null,"obfuscation":"lsI4acg"}
+
+      data: {"id":"chatcmpl-CZki1J9qrKifHLYfd83Fec01s4z8D","object":"chat.completion.chunk","created":1762637065,"model":"o4-mini-2025-04-16","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"30"},"finish_reason":null}],"usage":null,"obfuscation":"tcdcmfbK"}
+
+      data: {"id":"chatcmpl-CZki1J9qrKifHLYfd83Fec01s4z8D","object":"chat.completion.chunk","created":1762637065,"model":"o4-mini-2025-04-16","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":",\""},"finish_reason":null}],"usage":null,"obfuscation":"TD1rpZS"}
+
+      data: {"id":"chatcmpl-CZki1J9qrKifHLYfd83Fec01s4z8D","object":"chat.completion.chunk","created":1762637065,"model":"o4-mini-2025-04-16","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"city"},"finish_reason":null}],"usage":null,"obfuscation":"IaOcwg"}
+
+      data: {"id":"chatcmpl-CZki1J9qrKifHLYfd83Fec01s4z8D","object":"chat.completion.chunk","created":1762637065,"model":"o4-mini-2025-04-16","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"\":\""},"finish_reason":null}],"usage":null,"obfuscation":"6cUFm"}
+
+      data: {"id":"chatcmpl-CZki1J9qrKifHLYfd83Fec01s4z8D","object":"chat.completion.chunk","created":1762637065,"model":"o4-mini-2025-04-16","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"Paris"},"finish_reason":null}],"usage":null,"obfuscation":"2yZjs"}
+
+      data: {"id":"chatcmpl-CZki1J9qrKifHLYfd83Fec01s4z8D","object":"chat.completion.chunk","created":1762637065,"model":"o4-mini-2025-04-16","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"\",\""},"finish_reason":null}],"usage":null,"obfuscation":"Q37jl"}
+
+      data: {"id":"chatcmpl-CZki1J9qrKifHLYfd83Fec01s4z8D","object":"chat.completion.chunk","created":1762637065,"model":"o4-mini-2025-04-16","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"name"},"finish_reason":null}],"usage":null,"obfuscation":"kT7Jci"}
+
+      data: {"id":"chatcmpl-CZki1J9qrKifHLYfd83Fec01s4z8D","object":"chat.completion.chunk","created":1762637065,"model":"o4-mini-2025-04-16","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"\":\""},"finish_reason":null}],"usage":null,"obfuscation":"ou5VZ"}
+
+      data: {"id":"chatcmpl-CZki1J9qrKifHLYfd83Fec01s4z8D","object":"chat.completion.chunk","created":1762637065,"model":"o4-mini-2025-04-16","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"Alice"},"finish_reason":null}],"usage":null,"obfuscation":"AMhVx"}
+
+      data: {"id":"chatcmpl-CZki1J9qrKifHLYfd83Fec01s4z8D","object":"chat.completion.chunk","created":1762637065,"model":"o4-mini-2025-04-16","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"\"}"},"finish_reason":null}],"usage":null,"obfuscation":"ZDFb7mY"}
+
+      data: {"id":"chatcmpl-CZki1J9qrKifHLYfd83Fec01s4z8D","object":"chat.completion.chunk","created":1762637065,"model":"o4-mini-2025-04-16","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":null,"obfuscation":"AQhR"}
+
+      data: {"id":"chatcmpl-CZki1J9qrKifHLYfd83Fec01s4z8D","object":"chat.completion.chunk","created":1762637065,"model":"o4-mini-2025-04-16","service_tier":"default","system_fingerprint":null,"choices":[],"usage":{"prompt_tokens":95,"completion_tokens":290,"total_tokens":385,"prompt_tokens_details":{"cached_tokens":0,"audio_tokens":0},"completion_tokens_details":{"reasoning_tokens":256,"audio_tokens":0,"accepted_prediction_tokens":0,"rejected_prediction_tokens":0}},"obfuscation":"xEKhD"}
+
+      data: [DONE]
+
+    headers:
+      Content-Type:
+      - text/event-stream; charset=utf-8
+    status: 200 OK
+    code: 200
+    duration: 3.874454375s

providertests/testdata/TestOpenAIResponsesObjectGeneration/openai-gpt-4o-mini/complex_object.yaml šŸ”—

@@ -0,0 +1,149 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 915
+    host: ""
+    body: '{"max_output_tokens":4000,"store":false,"input":[{"content":[{"text":"Generate information about ''The Lord of the Rings'' book by J.R.R. Tolkien, including genres like fantasy and adventure, and its publication year (1954).","type":"input_text"}],"role":"user"}],"model":"gpt-4o-mini","text":{"format":{"name":"Book","schema":{"additionalProperties":false,"properties":{"author":{"additionalProperties":false,"properties":{"name":{"description":"Author''s name","type":"string"},"nationality":{"description":"Author''s nationality","type":"string"}},"required":["name","nationality"],"type":"object"},"genres":{"description":"List of genres","items":{"type":"string"},"type":"array"},"published_year":{"description":"Year the book was published","type":"integer"},"title":{"description":"The book title","type":"string"}},"required":["title","author","genres","published_year"],"type":"object"},"type":"json_schema"}}}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.openai.com/v1/responses
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: |-
+      {
+        "id": "resp_00aee193a62b03b301690fb5e67a8481a0890292ac13193180",
+        "object": "response",
+        "created_at": 1762637286,
+        "status": "completed",
+        "background": false,
+        "billing": {
+          "payer": "developer"
+        },
+        "error": null,
+        "incomplete_details": null,
+        "instructions": null,
+        "max_output_tokens": 4000,
+        "max_tool_calls": null,
+        "model": "gpt-4o-mini-2024-07-18",
+        "output": [
+          {
+            "id": "msg_00aee193a62b03b301690fb5e6bf6881a0ab198f7831a11cf0",
+            "type": "message",
+            "status": "completed",
+            "content": [
+              {
+                "type": "output_text",
+                "annotations": [],
+                "logprobs": [],
+                "text": "{\"author\":{\"name\":\"J.R.R. Tolkien\",\"nationality\":\"British\"},\"genres\":[\"Fantasy\",\"Adventure\",\"Epic\"],\"published_year\":1954,\"title\":\"The Lord of the Rings\"}"
+              }
+            ],
+            "role": "assistant"
+          }
+        ],
+        "parallel_tool_calls": true,
+        "previous_response_id": null,
+        "prompt_cache_key": null,
+        "prompt_cache_retention": null,
+        "reasoning": {
+          "effort": null,
+          "summary": null
+        },
+        "safety_identifier": null,
+        "service_tier": "default",
+        "store": false,
+        "temperature": 1.0,
+        "text": {
+          "format": {
+            "type": "json_schema",
+            "description": null,
+            "name": "Book",
+            "schema": {
+              "additionalProperties": false,
+              "properties": {
+                "author": {
+                  "additionalProperties": false,
+                  "properties": {
+                    "name": {
+                      "description": "Author's name",
+                      "type": "string"
+                    },
+                    "nationality": {
+                      "description": "Author's nationality",
+                      "type": "string"
+                    }
+                  },
+                  "required": [
+                    "name",
+                    "nationality"
+                  ],
+                  "type": "object"
+                },
+                "genres": {
+                  "description": "List of genres",
+                  "items": {
+                    "type": "string"
+                  },
+                  "type": "array"
+                },
+                "published_year": {
+                  "description": "Year the book was published",
+                  "type": "integer"
+                },
+                "title": {
+                  "description": "The book title",
+                  "type": "string"
+                }
+              },
+              "required": [
+                "title",
+                "author",
+                "genres",
+                "published_year"
+              ],
+              "type": "object"
+            },
+            "strict": true
+          },
+          "verbosity": "medium"
+        },
+        "tool_choice": "auto",
+        "tools": [],
+        "top_logprobs": 0,
+        "top_p": 1.0,
+        "truncation": "disabled",
+        "usage": {
+          "input_tokens": 140,
+          "input_tokens_details": {
+            "cached_tokens": 0
+          },
+          "output_tokens": 41,
+          "output_tokens_details": {
+            "reasoning_tokens": 0
+          },
+          "total_tokens": 181
+        },
+        "user": null,
+        "metadata": {}
+      }
+    headers:
+      Content-Type:
+      - application/json
+    status: 200 OK
+    code: 200
+    duration: 1.153408417s

providertests/testdata/TestOpenAIResponsesObjectGeneration/openai-gpt-4o-mini/complex_object_streaming.yaml šŸ”—

@@ -0,0 +1,176 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 929
+    host: ""
+    body: '{"max_output_tokens":4000,"store":false,"input":[{"content":[{"text":"Generate information about ''The Lord of the Rings'' book by J.R.R. Tolkien, including genres like fantasy and adventure, and its publication year (1954).","type":"input_text"}],"role":"user"}],"model":"gpt-4o-mini","text":{"format":{"name":"Book","schema":{"additionalProperties":false,"properties":{"author":{"additionalProperties":false,"properties":{"name":{"description":"Author''s name","type":"string"},"nationality":{"description":"Author''s nationality","type":"string"}},"required":["name","nationality"],"type":"object"},"genres":{"description":"List of genres","items":{"type":"string"},"type":"array"},"published_year":{"description":"Year the book was published","type":"integer"},"title":{"description":"The book title","type":"string"}},"required":["title","author","genres","published_year"],"type":"object"},"type":"json_schema"}},"stream":true}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.openai.com/v1/responses
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    body: |+
+      event: response.created

providertests/testdata/TestOpenAIResponsesObjectGeneration/openai-gpt-4o-mini/simple_object.yaml šŸ”—

@@ -0,0 +1,127 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 570
+    host: ""
+    body: '{"max_output_tokens":4000,"store":false,"input":[{"content":[{"text":"Generate information about a person named Alice who is 30 years old and lives in Paris.","type":"input_text"}],"role":"user"}],"model":"gpt-4o-mini","text":{"format":{"name":"Person","schema":{"additionalProperties":false,"properties":{"age":{"description":"The person''s age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person''s name","type":"string"}},"required":["name","age","city"],"type":"object"},"type":"json_schema"}}}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.openai.com/v1/responses
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: |-
+      {
+        "id": "resp_0bbc50a7c5ff86f001690fb5e419a8819087b97bd3cbc4c406",
+        "object": "response",
+        "created_at": 1762637284,
+        "status": "completed",
+        "background": false,
+        "billing": {
+          "payer": "developer"
+        },
+        "error": null,
+        "incomplete_details": null,
+        "instructions": null,
+        "max_output_tokens": 4000,
+        "max_tool_calls": null,
+        "model": "gpt-4o-mini-2024-07-18",
+        "output": [
+          {
+            "id": "msg_0bbc50a7c5ff86f001690fb5e4fabc8190a9749a3e0c928bce",
+            "type": "message",
+            "status": "completed",
+            "content": [
+              {
+                "type": "output_text",
+                "annotations": [],
+                "logprobs": [],
+                "text": "{\"age\":30,\"city\":\"Paris\",\"name\":\"Alice\"}"
+              }
+            ],
+            "role": "assistant"
+          }
+        ],
+        "parallel_tool_calls": true,
+        "previous_response_id": null,
+        "prompt_cache_key": null,
+        "prompt_cache_retention": null,
+        "reasoning": {
+          "effort": null,
+          "summary": null
+        },
+        "safety_identifier": null,
+        "service_tier": "default",
+        "store": false,
+        "temperature": 1.0,
+        "text": {
+          "format": {
+            "type": "json_schema",
+            "description": null,
+            "name": "Person",
+            "schema": {
+              "additionalProperties": false,
+              "properties": {
+                "age": {
+                  "description": "The person's age",
+                  "type": "integer"
+                },
+                "city": {
+                  "description": "The city where the person lives",
+                  "type": "string"
+                },
+                "name": {
+                  "description": "The person's name",
+                  "type": "string"
+                }
+              },
+              "required": [
+                "name",
+                "age",
+                "city"
+              ],
+              "type": "object"
+            },
+            "strict": true
+          },
+          "verbosity": "medium"
+        },
+        "tool_choice": "auto",
+        "tools": [],
+        "top_logprobs": 0,
+        "top_p": 1.0,
+        "truncation": "disabled",
+        "usage": {
+          "input_tokens": 82,
+          "input_tokens_details": {
+            "cached_tokens": 0
+          },
+          "output_tokens": 14,
+          "output_tokens_details": {
+            "reasoning_tokens": 0
+          },
+          "total_tokens": 96
+        },
+        "user": null,
+        "metadata": {}
+      }
+    headers:
+      Content-Type:
+      - application/json
+    status: 200 OK
+    code: 200
+    duration: 1.465307584s

providertests/testdata/TestOpenAIResponsesObjectGeneration/openai-gpt-4o-mini/simple_object_streaming.yaml šŸ”—

@@ -0,0 +1,95 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 584
+    host: ""
+    body: '{"max_output_tokens":4000,"store":false,"input":[{"content":[{"text":"Generate information about a person named Alice who is 30 years old and lives in Paris.","type":"input_text"}],"role":"user"}],"model":"gpt-4o-mini","text":{"format":{"name":"Person","schema":{"additionalProperties":false,"properties":{"age":{"description":"The person''s age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person''s name","type":"string"}},"required":["name","age","city"],"type":"object"},"type":"json_schema"}},"stream":true}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.openai.com/v1/responses
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    body: |+
+      event: response.created

providertests/testdata/TestOpenAIResponsesObjectGeneration/openai-gpt-4o/complex_object.yaml šŸ”—

@@ -0,0 +1,149 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 910
+    host: ""
+    body: '{"max_output_tokens":4000,"store":false,"input":[{"content":[{"text":"Generate information about ''The Lord of the Rings'' book by J.R.R. Tolkien, including genres like fantasy and adventure, and its publication year (1954).","type":"input_text"}],"role":"user"}],"model":"gpt-4o","text":{"format":{"name":"Book","schema":{"additionalProperties":false,"properties":{"author":{"additionalProperties":false,"properties":{"name":{"description":"Author''s name","type":"string"},"nationality":{"description":"Author''s nationality","type":"string"}},"required":["name","nationality"],"type":"object"},"genres":{"description":"List of genres","items":{"type":"string"},"type":"array"},"published_year":{"description":"Year the book was published","type":"integer"},"title":{"description":"The book title","type":"string"}},"required":["title","author","genres","published_year"],"type":"object"},"type":"json_schema"}}}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.openai.com/v1/responses
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: |-
+      {
+        "id": "resp_00fee2bcbce77a6b01690fb5e194848191a77c3f9796902417",
+        "object": "response",
+        "created_at": 1762637281,
+        "status": "completed",
+        "background": false,
+        "billing": {
+          "payer": "developer"
+        },
+        "error": null,
+        "incomplete_details": null,
+        "instructions": null,
+        "max_output_tokens": 4000,
+        "max_tool_calls": null,
+        "model": "gpt-4o-2024-08-06",
+        "output": [
+          {
+            "id": "msg_00fee2bcbce77a6b01690fb5e25dfc8191b275e2c92a87865a",
+            "type": "message",
+            "status": "completed",
+            "content": [
+              {
+                "type": "output_text",
+                "annotations": [],
+                "logprobs": [],
+                "text": "{\"author\":{\"name\":\"J.R.R. Tolkien\",\"nationality\":\"British\"},\"genres\":[\"Fantasy\",\"Adventure\"],\"published_year\":1954,\"title\":\"The Lord of the Rings\"}"
+              }
+            ],
+            "role": "assistant"
+          }
+        ],
+        "parallel_tool_calls": true,
+        "previous_response_id": null,
+        "prompt_cache_key": null,
+        "prompt_cache_retention": null,
+        "reasoning": {
+          "effort": null,
+          "summary": null
+        },
+        "safety_identifier": null,
+        "service_tier": "default",
+        "store": false,
+        "temperature": 1.0,
+        "text": {
+          "format": {
+            "type": "json_schema",
+            "description": null,
+            "name": "Book",
+            "schema": {
+              "additionalProperties": false,
+              "properties": {
+                "author": {
+                  "additionalProperties": false,
+                  "properties": {
+                    "name": {
+                      "description": "Author's name",
+                      "type": "string"
+                    },
+                    "nationality": {
+                      "description": "Author's nationality",
+                      "type": "string"
+                    }
+                  },
+                  "required": [
+                    "name",
+                    "nationality"
+                  ],
+                  "type": "object"
+                },
+                "genres": {
+                  "description": "List of genres",
+                  "items": {
+                    "type": "string"
+                  },
+                  "type": "array"
+                },
+                "published_year": {
+                  "description": "Year the book was published",
+                  "type": "integer"
+                },
+                "title": {
+                  "description": "The book title",
+                  "type": "string"
+                }
+              },
+              "required": [
+                "title",
+                "author",
+                "genres",
+                "published_year"
+              ],
+              "type": "object"
+            },
+            "strict": true
+          },
+          "verbosity": "medium"
+        },
+        "tool_choice": "auto",
+        "tools": [],
+        "top_logprobs": 0,
+        "top_p": 1.0,
+        "truncation": "disabled",
+        "usage": {
+          "input_tokens": 140,
+          "input_tokens_details": {
+            "cached_tokens": 0
+          },
+          "output_tokens": 39,
+          "output_tokens_details": {
+            "reasoning_tokens": 0
+          },
+          "total_tokens": 179
+        },
+        "user": null,
+        "metadata": {}
+      }
+    headers:
+      Content-Type:
+      - application/json
+    status: 200 OK
+    code: 200
+    duration: 1.348413375s

providertests/testdata/TestOpenAIResponsesObjectGeneration/openai-gpt-4o/complex_object_streaming.yaml šŸ”—

@@ -0,0 +1,176 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 924
+    host: ""
+    body: '{"max_output_tokens":4000,"store":false,"input":[{"content":[{"text":"Generate information about ''The Lord of the Rings'' book by J.R.R. Tolkien, including genres like fantasy and adventure, and its publication year (1954).","type":"input_text"}],"role":"user"}],"model":"gpt-4o","text":{"format":{"name":"Book","schema":{"additionalProperties":false,"properties":{"author":{"additionalProperties":false,"properties":{"name":{"description":"Author''s name","type":"string"},"nationality":{"description":"Author''s nationality","type":"string"}},"required":["name","nationality"],"type":"object"},"genres":{"description":"List of genres","items":{"type":"string"},"type":"array"},"published_year":{"description":"Year the book was published","type":"integer"},"title":{"description":"The book title","type":"string"}},"required":["title","author","genres","published_year"],"type":"object"},"type":"json_schema"}},"stream":true}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.openai.com/v1/responses
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    body: |+
+      event: response.created

providertests/testdata/TestOpenAIResponsesObjectGeneration/openai-gpt-4o/simple_object.yaml šŸ”—

@@ -0,0 +1,127 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 565
+    host: ""
+    body: '{"max_output_tokens":4000,"store":false,"input":[{"content":[{"text":"Generate information about a person named Alice who is 30 years old and lives in Paris.","type":"input_text"}],"role":"user"}],"model":"gpt-4o","text":{"format":{"name":"Person","schema":{"additionalProperties":false,"properties":{"age":{"description":"The person''s age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person''s name","type":"string"}},"required":["name","age","city"],"type":"object"},"type":"json_schema"}}}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.openai.com/v1/responses
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: |-
+      {
+        "id": "resp_0f20344bcc1b767601690fb5e04c9c81a08af63f829b10bb55",
+        "object": "response",
+        "created_at": 1762637280,
+        "status": "completed",
+        "background": false,
+        "billing": {
+          "payer": "developer"
+        },
+        "error": null,
+        "incomplete_details": null,
+        "instructions": null,
+        "max_output_tokens": 4000,
+        "max_tool_calls": null,
+        "model": "gpt-4o-2024-08-06",
+        "output": [
+          {
+            "id": "msg_0f20344bcc1b767601690fb5e094e481a0833865218a771607",
+            "type": "message",
+            "status": "completed",
+            "content": [
+              {
+                "type": "output_text",
+                "annotations": [],
+                "logprobs": [],
+                "text": "{\"age\":30,\"city\":\"Paris\",\"name\":\"Alice\"}"
+              }
+            ],
+            "role": "assistant"
+          }
+        ],
+        "parallel_tool_calls": true,
+        "previous_response_id": null,
+        "prompt_cache_key": null,
+        "prompt_cache_retention": null,
+        "reasoning": {
+          "effort": null,
+          "summary": null
+        },
+        "safety_identifier": null,
+        "service_tier": "default",
+        "store": false,
+        "temperature": 1.0,
+        "text": {
+          "format": {
+            "type": "json_schema",
+            "description": null,
+            "name": "Person",
+            "schema": {
+              "additionalProperties": false,
+              "properties": {
+                "age": {
+                  "description": "The person's age",
+                  "type": "integer"
+                },
+                "city": {
+                  "description": "The city where the person lives",
+                  "type": "string"
+                },
+                "name": {
+                  "description": "The person's name",
+                  "type": "string"
+                }
+              },
+              "required": [
+                "name",
+                "age",
+                "city"
+              ],
+              "type": "object"
+            },
+            "strict": true
+          },
+          "verbosity": "medium"
+        },
+        "tool_choice": "auto",
+        "tools": [],
+        "top_logprobs": 0,
+        "top_p": 1.0,
+        "truncation": "disabled",
+        "usage": {
+          "input_tokens": 82,
+          "input_tokens_details": {
+            "cached_tokens": 0
+          },
+          "output_tokens": 14,
+          "output_tokens_details": {
+            "reasoning_tokens": 0
+          },
+          "total_tokens": 96
+        },
+        "user": null,
+        "metadata": {}
+      }
+    headers:
+      Content-Type:
+      - application/json
+    status: 200 OK
+    code: 200
+    duration: 816.505042ms

providertests/testdata/TestOpenAIResponsesObjectGeneration/openai-gpt-4o/simple_object_streaming.yaml šŸ”—

@@ -0,0 +1,95 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 579
+    host: ""
+    body: '{"max_output_tokens":4000,"store":false,"input":[{"content":[{"text":"Generate information about a person named Alice who is 30 years old and lives in Paris.","type":"input_text"}],"role":"user"}],"model":"gpt-4o","text":{"format":{"name":"Person","schema":{"additionalProperties":false,"properties":{"age":{"description":"The person''s age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person''s name","type":"string"}},"required":["name","age","city"],"type":"object"},"type":"json_schema"}},"stream":true}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.openai.com/v1/responses
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    body: |+
+      event: response.created

providertests/testdata/TestOpenAIResponsesObjectGeneration/openai-gpt-5/complex_object.yaml šŸ”—

@@ -0,0 +1,154 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 909
+    host: ""
+    body: '{"max_output_tokens":4000,"store":false,"input":[{"content":[{"text":"Generate information about ''The Lord of the Rings'' book by J.R.R. Tolkien, including genres like fantasy and adventure, and its publication year (1954).","type":"input_text"}],"role":"user"}],"model":"gpt-5","text":{"format":{"name":"Book","schema":{"additionalProperties":false,"properties":{"author":{"additionalProperties":false,"properties":{"name":{"description":"Author''s name","type":"string"},"nationality":{"description":"Author''s nationality","type":"string"}},"required":["name","nationality"],"type":"object"},"genres":{"description":"List of genres","items":{"type":"string"},"type":"array"},"published_year":{"description":"Year the book was published","type":"integer"},"title":{"description":"The book title","type":"string"}},"required":["title","author","genres","published_year"],"type":"object"},"type":"json_schema"}}}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.openai.com/v1/responses
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: |-
+      {
+        "id": "resp_0d7efb113168df8301690fb603c0f881a1a5ecec45f910e875",
+        "object": "response",
+        "created_at": 1762637315,
+        "status": "completed",
+        "background": false,
+        "billing": {
+          "payer": "developer"
+        },
+        "error": null,
+        "incomplete_details": null,
+        "instructions": null,
+        "max_output_tokens": 4000,
+        "max_tool_calls": null,
+        "model": "gpt-5-2025-08-07",
+        "output": [
+          {
+            "id": "rs_0d7efb113168df8301690fb6041bb881a198a6e417f00bba70",
+            "type": "reasoning",
+            "summary": []
+          },
+          {
+            "id": "msg_0d7efb113168df8301690fb610bf2881a1be28a4885c1876ac",
+            "type": "message",
+            "status": "completed",
+            "content": [
+              {
+                "type": "output_text",
+                "annotations": [],
+                "logprobs": [],
+                "text": "{\"author\":{\"name\":\"J.R.R. Tolkien\",\"nationality\":\"British\"},\"genres\":[\"Fantasy\",\"Adventure\",\"Epic fantasy\",\"High fantasy\"],\"published_year\":1954,\"title\":\"The Lord of the Rings\"}"
+              }
+            ],
+            "role": "assistant"
+          }
+        ],
+        "parallel_tool_calls": true,
+        "previous_response_id": null,
+        "prompt_cache_key": null,
+        "prompt_cache_retention": null,
+        "reasoning": {
+          "effort": "medium",
+          "summary": null
+        },
+        "safety_identifier": null,
+        "service_tier": "default",
+        "store": false,
+        "temperature": 1.0,
+        "text": {
+          "format": {
+            "type": "json_schema",
+            "description": null,
+            "name": "Book",
+            "schema": {
+              "additionalProperties": false,
+              "properties": {
+                "author": {
+                  "additionalProperties": false,
+                  "properties": {
+                    "name": {
+                      "description": "Author's name",
+                      "type": "string"
+                    },
+                    "nationality": {
+                      "description": "Author's nationality",
+                      "type": "string"
+                    }
+                  },
+                  "required": [
+                    "name",
+                    "nationality"
+                  ],
+                  "type": "object"
+                },
+                "genres": {
+                  "description": "List of genres",
+                  "items": {
+                    "type": "string"
+                  },
+                  "type": "array"
+                },
+                "published_year": {
+                  "description": "Year the book was published",
+                  "type": "integer"
+                },
+                "title": {
+                  "description": "The book title",
+                  "type": "string"
+                }
+              },
+              "required": [
+                "title",
+                "author",
+                "genres",
+                "published_year"
+              ],
+              "type": "object"
+            },
+            "strict": true
+          },
+          "verbosity": "medium"
+        },
+        "tool_choice": "auto",
+        "tools": [],
+        "top_logprobs": 0,
+        "top_p": 1.0,
+        "truncation": "disabled",
+        "usage": {
+          "input_tokens": 138,
+          "input_tokens_details": {
+            "cached_tokens": 0
+          },
+          "output_tokens": 885,
+          "output_tokens_details": {
+            "reasoning_tokens": 832
+          },
+          "total_tokens": 1023
+        },
+        "user": null,
+        "metadata": {}
+      }
+    headers:
+      Content-Type:
+      - application/json
+    status: 200 OK
+    code: 200
+    duration: 13.745367625s

providertests/testdata/TestOpenAIResponsesObjectGeneration/openai-gpt-5/complex_object_streaming.yaml šŸ”—

@@ -0,0 +1,191 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 923
+    host: ""
+    body: '{"max_output_tokens":4000,"store":false,"input":[{"content":[{"text":"Generate information about ''The Lord of the Rings'' book by J.R.R. Tolkien, including genres like fantasy and adventure, and its publication year (1954).","type":"input_text"}],"role":"user"}],"model":"gpt-5","text":{"format":{"name":"Book","schema":{"additionalProperties":false,"properties":{"author":{"additionalProperties":false,"properties":{"name":{"description":"Author''s name","type":"string"},"nationality":{"description":"Author''s nationality","type":"string"}},"required":["name","nationality"],"type":"object"},"genres":{"description":"List of genres","items":{"type":"string"},"type":"array"},"published_year":{"description":"Year the book was published","type":"integer"},"title":{"description":"The book title","type":"string"}},"required":["title","author","genres","published_year"],"type":"object"},"type":"json_schema"}},"stream":true}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.openai.com/v1/responses
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    body: |+
+      event: response.created

providertests/testdata/TestOpenAIResponsesObjectGeneration/openai-gpt-5/simple_object.yaml šŸ”—

@@ -0,0 +1,132 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 564
+    host: ""
+    body: '{"max_output_tokens":4000,"store":false,"input":[{"content":[{"text":"Generate information about a person named Alice who is 30 years old and lives in Paris.","type":"input_text"}],"role":"user"}],"model":"gpt-5","text":{"format":{"name":"Person","schema":{"additionalProperties":false,"properties":{"age":{"description":"The person''s age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person''s name","type":"string"}},"required":["name","age","city"],"type":"object"},"type":"json_schema"}}}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.openai.com/v1/responses
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: |-
+      {
+        "id": "resp_027d860ff8453c5c01690fb5e8b590819c98d4fefe82a2d697",
+        "object": "response",
+        "created_at": 1762637288,
+        "status": "completed",
+        "background": false,
+        "billing": {
+          "payer": "developer"
+        },
+        "error": null,
+        "incomplete_details": null,
+        "instructions": null,
+        "max_output_tokens": 4000,
+        "max_tool_calls": null,
+        "model": "gpt-5-2025-08-07",
+        "output": [
+          {
+            "id": "rs_027d860ff8453c5c01690fb5e90bf8819cafd2ab51156cc379",
+            "type": "reasoning",
+            "summary": []
+          },
+          {
+            "id": "msg_027d860ff8453c5c01690fb5fe2978819ca252f996006bf96e",
+            "type": "message",
+            "status": "completed",
+            "content": [
+              {
+                "type": "output_text",
+                "annotations": [],
+                "logprobs": [],
+                "text": "{\"age\":30,\"city\":\"Paris\",\"name\":\"Alice\"}"
+              }
+            ],
+            "role": "assistant"
+          }
+        ],
+        "parallel_tool_calls": true,
+        "previous_response_id": null,
+        "prompt_cache_key": null,
+        "prompt_cache_retention": null,
+        "reasoning": {
+          "effort": "medium",
+          "summary": null
+        },
+        "safety_identifier": null,
+        "service_tier": "default",
+        "store": false,
+        "temperature": 1.0,
+        "text": {
+          "format": {
+            "type": "json_schema",
+            "description": null,
+            "name": "Person",
+            "schema": {
+              "additionalProperties": false,
+              "properties": {
+                "age": {
+                  "description": "The person's age",
+                  "type": "integer"
+                },
+                "city": {
+                  "description": "The city where the person lives",
+                  "type": "string"
+                },
+                "name": {
+                  "description": "The person's name",
+                  "type": "string"
+                }
+              },
+              "required": [
+                "name",
+                "age",
+                "city"
+              ],
+              "type": "object"
+            },
+            "strict": true
+          },
+          "verbosity": "medium"
+        },
+        "tool_choice": "auto",
+        "tools": [],
+        "top_logprobs": 0,
+        "top_p": 1.0,
+        "truncation": "disabled",
+        "usage": {
+          "input_tokens": 80,
+          "input_tokens_details": {
+            "cached_tokens": 0
+          },
+          "output_tokens": 1238,
+          "output_tokens_details": {
+            "reasoning_tokens": 1216
+          },
+          "total_tokens": 1318
+        },
+        "user": null,
+        "metadata": {}
+      }
+    headers:
+      Content-Type:
+      - application/json
+    status: 200 OK
+    code: 200
+    duration: 22.08059475s

providertests/testdata/TestOpenAIResponsesObjectGeneration/openai-gpt-5/simple_object_streaming.yaml šŸ”—

@@ -0,0 +1,101 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 578
+    host: ""
+    body: '{"max_output_tokens":4000,"store":false,"input":[{"content":[{"text":"Generate information about a person named Alice who is 30 years old and lives in Paris.","type":"input_text"}],"role":"user"}],"model":"gpt-5","text":{"format":{"name":"Person","schema":{"additionalProperties":false,"properties":{"age":{"description":"The person''s age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person''s name","type":"string"}},"required":["name","age","city"],"type":"object"},"type":"json_schema"}},"stream":true}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.openai.com/v1/responses
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    body: |+
+      event: response.created

providertests/testdata/TestOpenAIResponsesObjectGeneration/openai-o4-mini/complex_object.yaml šŸ”—

@@ -0,0 +1,154 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 911
+    host: ""
+    body: '{"max_output_tokens":4000,"store":false,"input":[{"content":[{"text":"Generate information about ''The Lord of the Rings'' book by J.R.R. Tolkien, including genres like fantasy and adventure, and its publication year (1954).","type":"input_text"}],"role":"user"}],"model":"o4-mini","text":{"format":{"name":"Book","schema":{"additionalProperties":false,"properties":{"author":{"additionalProperties":false,"properties":{"name":{"description":"Author''s name","type":"string"},"nationality":{"description":"Author''s nationality","type":"string"}},"required":["name","nationality"],"type":"object"},"genres":{"description":"List of genres","items":{"type":"string"},"type":"array"},"published_year":{"description":"Year the book was published","type":"integer"},"title":{"description":"The book title","type":"string"}},"required":["title","author","genres","published_year"],"type":"object"},"type":"json_schema"}}}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.openai.com/v1/responses
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: |-
+      {
+        "id": "resp_017b2c309fa6f10301690fb622abf481a0a85dad143f87cc57",
+        "object": "response",
+        "created_at": 1762637346,
+        "status": "completed",
+        "background": false,
+        "billing": {
+          "payer": "developer"
+        },
+        "error": null,
+        "incomplete_details": null,
+        "instructions": null,
+        "max_output_tokens": 4000,
+        "max_tool_calls": null,
+        "model": "o4-mini-2025-04-16",
+        "output": [
+          {
+            "id": "rs_017b2c309fa6f10301690fb623133881a0b253929605c93041",
+            "type": "reasoning",
+            "summary": []
+          },
+          {
+            "id": "msg_017b2c309fa6f10301690fb6269d6c81a0abb3acf886a1efe0",
+            "type": "message",
+            "status": "completed",
+            "content": [
+              {
+                "type": "output_text",
+                "annotations": [],
+                "logprobs": [],
+                "text": "{\"author\":{\"name\":\"J.R.R. Tolkien\",\"nationality\":\"English\"},\"genres\":[\"Fantasy\",\"Adventure\"],\"published_year\":1954,\"title\":\"The Lord of the Rings\"}"
+              }
+            ],
+            "role": "assistant"
+          }
+        ],
+        "parallel_tool_calls": true,
+        "previous_response_id": null,
+        "prompt_cache_key": null,
+        "prompt_cache_retention": null,
+        "reasoning": {
+          "effort": "medium",
+          "summary": null
+        },
+        "safety_identifier": null,
+        "service_tier": "default",
+        "store": false,
+        "temperature": 1.0,
+        "text": {
+          "format": {
+            "type": "json_schema",
+            "description": null,
+            "name": "Book",
+            "schema": {
+              "additionalProperties": false,
+              "properties": {
+                "author": {
+                  "additionalProperties": false,
+                  "properties": {
+                    "name": {
+                      "description": "Author's name",
+                      "type": "string"
+                    },
+                    "nationality": {
+                      "description": "Author's nationality",
+                      "type": "string"
+                    }
+                  },
+                  "required": [
+                    "name",
+                    "nationality"
+                  ],
+                  "type": "object"
+                },
+                "genres": {
+                  "description": "List of genres",
+                  "items": {
+                    "type": "string"
+                  },
+                  "type": "array"
+                },
+                "published_year": {
+                  "description": "Year the book was published",
+                  "type": "integer"
+                },
+                "title": {
+                  "description": "The book title",
+                  "type": "string"
+                }
+              },
+              "required": [
+                "title",
+                "author",
+                "genres",
+                "published_year"
+              ],
+              "type": "object"
+            },
+            "strict": true
+          },
+          "verbosity": "medium"
+        },
+        "tool_choice": "auto",
+        "tools": [],
+        "top_logprobs": 0,
+        "top_p": 1.0,
+        "truncation": "disabled",
+        "usage": {
+          "input_tokens": 138,
+          "input_tokens_details": {
+            "cached_tokens": 0
+          },
+          "output_tokens": 431,
+          "output_tokens_details": {
+            "reasoning_tokens": 384
+          },
+          "total_tokens": 569
+        },
+        "user": null,
+        "metadata": {}
+      }
+    headers:
+      Content-Type:
+      - application/json
+    status: 200 OK
+    code: 200
+    duration: 4.660075375s

providertests/testdata/TestOpenAIResponsesObjectGeneration/openai-o4-mini/complex_object_streaming.yaml šŸ”—

@@ -0,0 +1,176 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 925
+    host: ""
+    body: '{"max_output_tokens":4000,"store":false,"input":[{"content":[{"text":"Generate information about ''The Lord of the Rings'' book by J.R.R. Tolkien, including genres like fantasy and adventure, and its publication year (1954).","type":"input_text"}],"role":"user"}],"model":"o4-mini","text":{"format":{"name":"Book","schema":{"additionalProperties":false,"properties":{"author":{"additionalProperties":false,"properties":{"name":{"description":"Author''s name","type":"string"},"nationality":{"description":"Author''s nationality","type":"string"}},"required":["name","nationality"],"type":"object"},"genres":{"description":"List of genres","items":{"type":"string"},"type":"array"},"published_year":{"description":"Year the book was published","type":"integer"},"title":{"description":"The book title","type":"string"}},"required":["title","author","genres","published_year"],"type":"object"},"type":"json_schema"}},"stream":true}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.openai.com/v1/responses
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    body: |+
+      event: response.created

providertests/testdata/TestOpenAIResponsesObjectGeneration/openai-o4-mini/simple_object.yaml šŸ”—

@@ -0,0 +1,132 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 566
+    host: ""
+    body: '{"max_output_tokens":4000,"store":false,"input":[{"content":[{"text":"Generate information about a person named Alice who is 30 years old and lives in Paris.","type":"input_text"}],"role":"user"}],"model":"o4-mini","text":{"format":{"name":"Person","schema":{"additionalProperties":false,"properties":{"age":{"description":"The person''s age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person''s name","type":"string"}},"required":["name","age","city"],"type":"object"},"type":"json_schema"}}}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.openai.com/v1/responses
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    uncompressed: true
+    body: |-
+      {
+        "id": "resp_098f453613ad314501690fb61bc6e0819382952362bbb4e647",
+        "object": "response",
+        "created_at": 1762637339,
+        "status": "completed",
+        "background": false,
+        "billing": {
+          "payer": "developer"
+        },
+        "error": null,
+        "incomplete_details": null,
+        "instructions": null,
+        "max_output_tokens": 4000,
+        "max_tool_calls": null,
+        "model": "o4-mini-2025-04-16",
+        "output": [
+          {
+            "id": "rs_098f453613ad314501690fb61c869c81939eea265ba983fa58",
+            "type": "reasoning",
+            "summary": []
+          },
+          {
+            "id": "msg_098f453613ad314501690fb61f8828819389d543a45e07bc0b",
+            "type": "message",
+            "status": "completed",
+            "content": [
+              {
+                "type": "output_text",
+                "annotations": [],
+                "logprobs": [],
+                "text": "{\"age\":30,\"city\":\"Paris\",\"name\":\"Alice\"}"
+              }
+            ],
+            "role": "assistant"
+          }
+        ],
+        "parallel_tool_calls": true,
+        "previous_response_id": null,
+        "prompt_cache_key": null,
+        "prompt_cache_retention": null,
+        "reasoning": {
+          "effort": "medium",
+          "summary": null
+        },
+        "safety_identifier": null,
+        "service_tier": "default",
+        "store": false,
+        "temperature": 1.0,
+        "text": {
+          "format": {
+            "type": "json_schema",
+            "description": null,
+            "name": "Person",
+            "schema": {
+              "additionalProperties": false,
+              "properties": {
+                "age": {
+                  "description": "The person's age",
+                  "type": "integer"
+                },
+                "city": {
+                  "description": "The city where the person lives",
+                  "type": "string"
+                },
+                "name": {
+                  "description": "The person's name",
+                  "type": "string"
+                }
+              },
+              "required": [
+                "name",
+                "age",
+                "city"
+              ],
+              "type": "object"
+            },
+            "strict": true
+          },
+          "verbosity": "medium"
+        },
+        "tool_choice": "auto",
+        "tools": [],
+        "top_logprobs": 0,
+        "top_p": 1.0,
+        "truncation": "disabled",
+        "usage": {
+          "input_tokens": 80,
+          "input_tokens_details": {
+            "cached_tokens": 0
+          },
+          "output_tokens": 342,
+          "output_tokens_details": {
+            "reasoning_tokens": 320
+          },
+          "total_tokens": 422
+        },
+        "user": null,
+        "metadata": {}
+      }
+    headers:
+      Content-Type:
+      - application/json
+    status: 200 OK
+    code: 200
+    duration: 4.174928125s

providertests/testdata/TestOpenAIResponsesObjectGeneration/openai-o4-mini/simple_object_streaming.yaml šŸ”—

@@ -0,0 +1,101 @@
+---
+version: 2
+interactions:
+- id: 0
+  request:
+    proto: HTTP/1.1
+    proto_major: 1
+    proto_minor: 1
+    content_length: 580
+    host: ""
+    body: '{"max_output_tokens":4000,"store":false,"input":[{"content":[{"text":"Generate information about a person named Alice who is 30 years old and lives in Paris.","type":"input_text"}],"role":"user"}],"model":"o4-mini","text":{"format":{"name":"Person","schema":{"additionalProperties":false,"properties":{"age":{"description":"The person''s age","type":"integer"},"city":{"description":"The city where the person lives","type":"string"},"name":{"description":"The person''s name","type":"string"}},"required":["name","age","city"],"type":"object"},"type":"json_schema"}},"stream":true}'
+    headers:
+      Accept:
+      - application/json
+      Content-Type:
+      - application/json
+      User-Agent:
+      - OpenAI/Go 2.7.1
+    url: https://api.openai.com/v1/responses
+    method: POST
+  response:
+    proto: HTTP/2.0
+    proto_major: 2
+    proto_minor: 0
+    content_length: -1
+    body: |+
+      event: response.created

schema/schema.go šŸ”—

@@ -0,0 +1,403 @@
+// Package schema provides JSON schema generation and validation utilities.
+// It supports automatic schema generation from Go types and validation of parsed objects.
+package schema
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"reflect"
+	"slices"
+	"strings"
+
+	jsonrepair "github.com/RealAlexandreAI/json-repair"
+	"github.com/kaptinlin/jsonschema"
+)
+
+// ObjectRepairFunc is a function that attempts to repair invalid JSON output.
+// It receives the raw text and the error encountered during parsing or validation,
+// and returns repaired text or an error if repair is not possible.
+type ObjectRepairFunc func(ctx context.Context, text string, err error) (string, error)
+
+// ParseError is returned when object generation fails
+// due to parsing errors, validation errors, or model failures.
+type ParseError struct {
+	RawText         string
+	ParseError      error
+	ValidationError error
+}
+
+// Schema represents a JSON schema for tool input validation.
+type Schema struct {
+	Type        string             `json:"type,omitempty"`
+	Properties  map[string]*Schema `json:"properties,omitempty"`
+	Required    []string           `json:"required,omitempty"`
+	Items       *Schema            `json:"items,omitempty"`
+	Description string             `json:"description,omitempty"`
+	Enum        []any              `json:"enum,omitempty"`
+	Format      string             `json:"format,omitempty"`
+	Minimum     *float64           `json:"minimum,omitempty"`
+	Maximum     *float64           `json:"maximum,omitempty"`
+	MinLength   *int               `json:"minLength,omitempty"`
+	MaxLength   *int               `json:"maxLength,omitempty"`
+}
+
+// ParseState represents the state of JSON parsing.
+type ParseState string
+
+const (
+	// ParseStateUndefined means input was undefined/empty.
+	ParseStateUndefined ParseState = "undefined"
+
+	// ParseStateSuccessful means JSON parsed without repair.
+	ParseStateSuccessful ParseState = "successful"
+
+	// ParseStateRepaired means JSON parsed after repair.
+	ParseStateRepaired ParseState = "repaired"
+
+	// ParseStateFailed means JSON could not be parsed even after repair.
+	ParseStateFailed ParseState = "failed"
+)
+
+// ToParameters converts a Schema to the parameters map format expected by ToolInfo.
+func ToParameters(s Schema) map[string]any {
+	if s.Properties == nil {
+		return make(map[string]any)
+	}
+
+	result := make(map[string]any)
+	for name, propSchema := range s.Properties {
+		result[name] = ToMap(*propSchema)
+	}
+	return result
+}
+
+// Generate generates a JSON schema from a reflect.Type.
+// It recursively processes struct fields, arrays, maps, and primitive types.
+func Generate(t reflect.Type) Schema {
+	return generateSchemaRecursive(t, make(map[reflect.Type]bool))
+}
+
+func generateSchemaRecursive(t reflect.Type, visited map[reflect.Type]bool) Schema {
+	if t.Kind() == reflect.Pointer {
+		t = t.Elem()
+	}
+
+	if visited[t] {
+		return Schema{Type: "object"}
+	}
+	visited[t] = true
+	defer delete(visited, t)
+
+	switch t.Kind() {
+	case reflect.String:
+		return Schema{Type: "string"}
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
+		reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+		return Schema{Type: "integer"}
+	case reflect.Float32, reflect.Float64:
+		return Schema{Type: "number"}
+	case reflect.Bool:
+		return Schema{Type: "boolean"}
+	case reflect.Slice, reflect.Array:
+		itemSchema := generateSchemaRecursive(t.Elem(), visited)
+		return Schema{
+			Type:  "array",
+			Items: &itemSchema,
+		}
+	case reflect.Map:
+		if t.Key().Kind() == reflect.String {
+			valueSchema := generateSchemaRecursive(t.Elem(), visited)
+			schema := Schema{
+				Type: "object",
+				Properties: map[string]*Schema{
+					"*": &valueSchema,
+				},
+			}
+			return schema
+		}
+		return Schema{Type: "object"}
+	case reflect.Struct:
+		schema := Schema{
+			Type:       "object",
+			Properties: make(map[string]*Schema),
+		}
+		for i := range t.NumField() {
+			field := t.Field(i)
+
+			if !field.IsExported() {
+				continue
+			}
+
+			jsonTag := field.Tag.Get("json")
+			if jsonTag == "-" {
+				continue
+			}
+
+			fieldName := field.Name
+			required := true
+
+			if jsonTag != "" {
+				parts := strings.Split(jsonTag, ",")
+				if parts[0] != "" {
+					fieldName = parts[0]
+				}
+
+				if slices.Contains(parts[1:], "omitempty") {
+					required = false
+				}
+			} else {
+				fieldName = toSnakeCase(fieldName)
+			}
+
+			fieldSchema := generateSchemaRecursive(field.Type, visited)
+
+			if desc := field.Tag.Get("description"); desc != "" {
+				fieldSchema.Description = desc
+			}
+
+			if enumTag := field.Tag.Get("enum"); enumTag != "" {
+				enumValues := strings.Split(enumTag, ",")
+				fieldSchema.Enum = make([]any, len(enumValues))
+				for i, v := range enumValues {
+					fieldSchema.Enum[i] = strings.TrimSpace(v)
+				}
+			}
+
+			schema.Properties[fieldName] = &fieldSchema
+
+			if required {
+				schema.Required = append(schema.Required, fieldName)
+			}
+		}
+
+		return schema
+	case reflect.Interface:
+		return Schema{Type: "object"}
+	default:
+		return Schema{Type: "object"}
+	}
+}
+
+// ToMap converts a Schema to a map representation suitable for JSON Schema.
+func ToMap(schema Schema) map[string]any {
+	result := make(map[string]any)
+
+	if schema.Type != "" {
+		result["type"] = schema.Type
+	}
+
+	if schema.Description != "" {
+		result["description"] = schema.Description
+	}
+
+	if len(schema.Enum) > 0 {
+		result["enum"] = schema.Enum
+	}
+
+	if schema.Format != "" {
+		result["format"] = schema.Format
+	}
+
+	if schema.Minimum != nil {
+		result["minimum"] = *schema.Minimum
+	}
+
+	if schema.Maximum != nil {
+		result["maximum"] = *schema.Maximum
+	}
+
+	if schema.MinLength != nil {
+		result["minLength"] = *schema.MinLength
+	}
+
+	if schema.MaxLength != nil {
+		result["maxLength"] = *schema.MaxLength
+	}
+
+	if schema.Properties != nil {
+		props := make(map[string]any)
+		for name, propSchema := range schema.Properties {
+			props[name] = ToMap(*propSchema)
+		}
+		result["properties"] = props
+	}
+
+	if len(schema.Required) > 0 {
+		result["required"] = schema.Required
+	}
+
+	if schema.Items != nil {
+		itemsMap := ToMap(*schema.Items)
+		// Ensure type is always set for items, even if it was blank for llama.cpp compatibility
+		if _, hasType := itemsMap["type"]; !hasType && schema.Items.Type == "" {
+			if len(schema.Items.Properties) > 0 {
+				itemsMap["type"] = "object"
+			}
+		}
+		result["items"] = itemsMap
+	}
+
+	return result
+}
+
+// ParsePartialJSON attempts to parse potentially incomplete JSON.
+// It first tries standard JSON parsing, then attempts repair if that fails.
+//
+// Returns:
+//   - result: The parsed JSON value (map, slice, or primitive)
+//   - state: Indicates whether parsing succeeded, needed repair, or failed
+//   - err: The error if parsing failed completely
+//
+// Example:
+//
+//	obj, state, err := ParsePartialJSON(`{"name": "John", "age": 25`)
+//	// Result: map[string]any{"name": "John", "age": 25}, ParseStateRepaired, nil
+func ParsePartialJSON(text string) (any, ParseState, error) {
+	if text == "" {
+		return nil, ParseStateUndefined, nil
+	}
+
+	var result any
+	if err := json.Unmarshal([]byte(text), &result); err == nil {
+		return result, ParseStateSuccessful, nil
+	}
+
+	repaired, err := jsonrepair.RepairJSON(text)
+	if err != nil {
+		return nil, ParseStateFailed, fmt.Errorf("json repair failed: %w", err)
+	}
+
+	if err := json.Unmarshal([]byte(repaired), &result); err != nil {
+		return nil, ParseStateFailed, fmt.Errorf("failed to parse repaired json: %w", err)
+	}
+
+	return result, ParseStateRepaired, nil
+}
+
+// Error implements the error interface.
+func (e *ParseError) Error() string {
+	if e.ValidationError != nil {
+		return fmt.Sprintf("object validation failed: %v", e.ValidationError)
+	}
+	if e.ParseError != nil {
+		return fmt.Sprintf("failed to parse object: %v", e.ParseError)
+	}
+	return "failed to generate object"
+}
+
+// ParseAndValidate combines JSON parsing and validation.
+// Returns the parsed object if both parsing and validation succeed.
+func ParseAndValidate(text string, schema Schema) (any, error) {
+	obj, state, err := ParsePartialJSON(text)
+	if state == ParseStateFailed {
+		return nil, &ParseError{
+			RawText:    text,
+			ParseError: err,
+		}
+	}
+
+	if err := validateAgainstSchema(obj, schema); err != nil {
+		return nil, &ParseError{
+			RawText:         text,
+			ValidationError: err,
+		}
+	}
+
+	return obj, nil
+}
+
+// ValidateAgainstSchema validates a parsed object against a Schema.
+func ValidateAgainstSchema(obj any, schema Schema) error {
+	return validateAgainstSchema(obj, schema)
+}
+
+func validateAgainstSchema(obj any, schema Schema) error {
+	jsonSchemaBytes, err := json.Marshal(schema)
+	if err != nil {
+		return fmt.Errorf("failed to marshal schema: %w", err)
+	}
+
+	compiler := jsonschema.NewCompiler()
+	validator, err := compiler.Compile(jsonSchemaBytes)
+	if err != nil {
+		return fmt.Errorf("invalid schema: %w", err)
+	}
+
+	result := validator.Validate(obj)
+	if !result.IsValid() {
+		var errMsgs []string
+		for field, validationErr := range result.Errors {
+			errMsgs = append(errMsgs, fmt.Sprintf("%s: %s", field, validationErr.Message))
+		}
+		return fmt.Errorf("validation failed: %s", strings.Join(errMsgs, "; "))
+	}
+
+	return nil
+}
+
+// ParseAndValidateWithRepair attempts parsing, validation, and custom repair.
+func ParseAndValidateWithRepair(
+	ctx context.Context,
+	text string,
+	schema Schema,
+	repair ObjectRepairFunc,
+) (any, error) {
+	obj, state, parseErr := ParsePartialJSON(text)
+
+	if state == ParseStateSuccessful || state == ParseStateRepaired {
+		validationErr := validateAgainstSchema(obj, schema)
+		if validationErr == nil {
+			return obj, nil
+		}
+
+		if repair != nil {
+			repairedText, repairErr := repair(ctx, text, validationErr)
+			if repairErr == nil {
+				obj2, state2, _ := ParsePartialJSON(repairedText)
+				if state2 == ParseStateSuccessful || state2 == ParseStateRepaired {
+					if err := validateAgainstSchema(obj2, schema); err == nil {
+						return obj2, nil
+					}
+				}
+			}
+		}
+
+		return nil, &ParseError{
+			RawText:         text,
+			ValidationError: validationErr,
+		}
+	}
+
+	if repair != nil {
+		repairedText, repairErr := repair(ctx, text, parseErr)
+		if repairErr == nil {
+			obj2, state2, parseErr2 := ParsePartialJSON(repairedText)
+			if state2 == ParseStateSuccessful || state2 == ParseStateRepaired {
+				if err := validateAgainstSchema(obj2, schema); err == nil {
+					return obj2, nil
+				}
+			}
+			return nil, &ParseError{
+				RawText:    repairedText,
+				ParseError: parseErr2,
+			}
+		}
+	}
+
+	return nil, &ParseError{
+		RawText:    text,
+		ParseError: parseErr,
+	}
+}
+
+func toSnakeCase(s string) string {
+	var result strings.Builder
+	for i, r := range s {
+		if i > 0 && r >= 'A' && r <= 'Z' {
+			result.WriteByte('_')
+		}
+		result.WriteRune(r)
+	}
+	return strings.ToLower(result.String())
+}

schema/schema_test.go šŸ”—

@@ -0,0 +1,534 @@
+package schema
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestEnumSupport(t *testing.T) {
+	// Test enum via struct tags
+	type WeatherInput struct {
+		Location string `json:"location" description:"City name"`
+		Units    string `json:"units" enum:"celsius,fahrenheit,kelvin" description:"Temperature units"`
+		Format   string `json:"format,omitempty" enum:"json,xml,text"`
+	}
+
+	schema := Generate(reflect.TypeOf(WeatherInput{}))
+
+	require.Equal(t, "object", schema.Type)
+
+	// Check units field has enum values
+	unitsSchema := schema.Properties["units"]
+	require.NotNil(t, unitsSchema, "Expected units property to exist")
+	require.Len(t, unitsSchema.Enum, 3)
+	expectedUnits := []string{"celsius", "fahrenheit", "kelvin"}
+	for i, expected := range expectedUnits {
+		require.Equal(t, expected, unitsSchema.Enum[i])
+	}
+
+	// Check required fields (format should not be required due to omitempty)
+	expectedRequired := []string{"location", "units"}
+	require.Len(t, schema.Required, len(expectedRequired))
+}
+
+func TestSchemaToParameters(t *testing.T) {
+	testSchema := Schema{
+		Type: "object",
+		Properties: map[string]*Schema{
+			"name": {
+				Type:        "string",
+				Description: "The name field",
+			},
+			"age": {
+				Type:    "integer",
+				Minimum: func() *float64 { v := 0.0; return &v }(),
+				Maximum: func() *float64 { v := 120.0; return &v }(),
+			},
+			"tags": {
+				Type: "array",
+				Items: &Schema{
+					Type: "string",
+				},
+			},
+			"priority": {
+				Type: "string",
+				Enum: []any{"low", "medium", "high"},
+			},
+		},
+		Required: []string{"name"},
+	}
+
+	params := ToParameters(testSchema)
+
+	// Check name parameter
+	nameParam, ok := params["name"].(map[string]any)
+	require.True(t, ok, "Expected name parameter to exist")
+	require.Equal(t, "string", nameParam["type"])
+	require.Equal(t, "The name field", nameParam["description"])
+
+	// Check age parameter with min/max
+	ageParam, ok := params["age"].(map[string]any)
+	require.True(t, ok, "Expected age parameter to exist")
+	require.Equal(t, "integer", ageParam["type"])
+	require.Equal(t, 0.0, ageParam["minimum"])
+	require.Equal(t, 120.0, ageParam["maximum"])
+
+	// Check priority parameter with enum
+	priorityParam, ok := params["priority"].(map[string]any)
+	require.True(t, ok, "Expected priority parameter to exist")
+	require.Equal(t, "string", priorityParam["type"])
+	enumValues, ok := priorityParam["enum"].([]any)
+	require.True(t, ok)
+	require.Len(t, enumValues, 3)
+}
+
+func TestGenerateSchemaBasicTypes(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name     string
+		input    any
+		expected Schema
+	}{
+		{
+			name:     "string type",
+			input:    "",
+			expected: Schema{Type: "string"},
+		},
+		{
+			name:     "int type",
+			input:    0,
+			expected: Schema{Type: "integer"},
+		},
+		{
+			name:     "int64 type",
+			input:    int64(0),
+			expected: Schema{Type: "integer"},
+		},
+		{
+			name:     "uint type",
+			input:    uint(0),
+			expected: Schema{Type: "integer"},
+		},
+		{
+			name:     "float64 type",
+			input:    0.0,
+			expected: Schema{Type: "number"},
+		},
+		{
+			name:     "float32 type",
+			input:    float32(0.0),
+			expected: Schema{Type: "number"},
+		},
+		{
+			name:     "bool type",
+			input:    false,
+			expected: Schema{Type: "boolean"},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			schema := Generate(reflect.TypeOf(tt.input))
+			require.Equal(t, tt.expected.Type, schema.Type)
+		})
+	}
+}
+
+func TestGenerateSchemaArrayTypes(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name     string
+		input    any
+		expected Schema
+	}{
+		{
+			name:  "string slice",
+			input: []string{},
+			expected: Schema{
+				Type:  "array",
+				Items: &Schema{Type: "string"},
+			},
+		},
+		{
+			name:  "int slice",
+			input: []int{},
+			expected: Schema{
+				Type:  "array",
+				Items: &Schema{Type: "integer"},
+			},
+		},
+		{
+			name:  "string array",
+			input: [3]string{},
+			expected: Schema{
+				Type:  "array",
+				Items: &Schema{Type: "string"},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			schema := Generate(reflect.TypeOf(tt.input))
+			require.Equal(t, tt.expected.Type, schema.Type)
+			require.NotNil(t, schema.Items, "Expected items schema to exist")
+			require.Equal(t, tt.expected.Items.Type, schema.Items.Type)
+		})
+	}
+}
+
+func TestGenerateSchemaMapTypes(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name     string
+		input    any
+		expected string
+	}{
+		{
+			name:     "string to string map",
+			input:    map[string]string{},
+			expected: "object",
+		},
+		{
+			name:     "string to int map",
+			input:    map[string]int{},
+			expected: "object",
+		},
+		{
+			name:     "int to string map",
+			input:    map[int]string{},
+			expected: "object",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			schema := Generate(reflect.TypeOf(tt.input))
+			require.Equal(t, tt.expected, schema.Type)
+		})
+	}
+}
+
+func TestGenerateSchemaStructTypes(t *testing.T) {
+	t.Parallel()
+
+	type SimpleStruct struct {
+		Name string `json:"name" description:"The name field"`
+		Age  int    `json:"age"`
+	}
+
+	type StructWithOmitEmpty struct {
+		Required string `json:"required"`
+		Optional string `json:"optional,omitempty"`
+	}
+
+	type StructWithJSONIgnore struct {
+		Visible string `json:"visible"`
+		Hidden  string `json:"-"`
+	}
+
+	type StructWithoutJSONTags struct {
+		FirstName string
+		LastName  string
+	}
+
+	tests := []struct {
+		name     string
+		input    any
+		validate func(t *testing.T, schema Schema)
+	}{
+		{
+			name:  "simple struct",
+			input: SimpleStruct{},
+			validate: func(t *testing.T, schema Schema) {
+				require.Equal(t, "object", schema.Type)
+				require.Len(t, schema.Properties, 2)
+				require.NotNil(t, schema.Properties["name"], "Expected name property to exist")
+				require.Equal(t, "The name field", schema.Properties["name"].Description)
+				require.Len(t, schema.Required, 2)
+			},
+		},
+		{
+			name:  "struct with omitempty",
+			input: StructWithOmitEmpty{},
+			validate: func(t *testing.T, schema Schema) {
+				require.Len(t, schema.Required, 1)
+				require.Equal(t, "required", schema.Required[0])
+			},
+		},
+		{
+			name:  "struct with json ignore",
+			input: StructWithJSONIgnore{},
+			validate: func(t *testing.T, schema Schema) {
+				require.Len(t, schema.Properties, 1)
+				require.NotNil(t, schema.Properties["visible"], "Expected visible property to exist")
+				require.Nil(t, schema.Properties["hidden"], "Expected hidden property to not exist")
+			},
+		},
+		{
+			name:  "struct without json tags",
+			input: StructWithoutJSONTags{},
+			validate: func(t *testing.T, schema Schema) {
+				require.NotNil(t, schema.Properties["first_name"], "Expected first_name property to exist")
+				require.NotNil(t, schema.Properties["last_name"], "Expected last_name property to exist")
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			schema := Generate(reflect.TypeOf(tt.input))
+			tt.validate(t, schema)
+		})
+	}
+}
+
+func TestGenerateSchemaPointerTypes(t *testing.T) {
+	t.Parallel()
+
+	type StructWithPointers struct {
+		Name *string `json:"name"`
+		Age  *int    `json:"age"`
+	}
+
+	schema := Generate(reflect.TypeOf(StructWithPointers{}))
+
+	require.Equal(t, "object", schema.Type)
+
+	require.NotNil(t, schema.Properties["name"], "Expected name property to exist")
+	require.Equal(t, "string", schema.Properties["name"].Type)
+
+	require.NotNil(t, schema.Properties["age"], "Expected age property to exist")
+	require.Equal(t, "integer", schema.Properties["age"].Type)
+}
+
+func TestGenerateSchemaNestedStructs(t *testing.T) {
+	t.Parallel()
+
+	type Address struct {
+		Street string `json:"street"`
+		City   string `json:"city"`
+	}
+
+	type Person struct {
+		Name    string  `json:"name"`
+		Address Address `json:"address"`
+	}
+
+	schema := Generate(reflect.TypeOf(Person{}))
+
+	require.Equal(t, "object", schema.Type)
+
+	require.NotNil(t, schema.Properties["address"], "Expected address property to exist")
+
+	addressSchema := schema.Properties["address"]
+	require.Equal(t, "object", addressSchema.Type)
+
+	require.NotNil(t, addressSchema.Properties["street"], "Expected street property in address to exist")
+	require.NotNil(t, addressSchema.Properties["city"], "Expected city property in address to exist")
+}
+
+func TestGenerateSchemaRecursiveStructs(t *testing.T) {
+	t.Parallel()
+
+	type Node struct {
+		Value string `json:"value"`
+		Next  *Node  `json:"next,omitempty"`
+	}
+
+	schema := Generate(reflect.TypeOf(Node{}))
+
+	require.Equal(t, "object", schema.Type)
+
+	require.NotNil(t, schema.Properties["value"], "Expected value property to exist")
+
+	require.NotNil(t, schema.Properties["next"], "Expected next property to exist")
+
+	// The recursive reference should be handled gracefully
+	nextSchema := schema.Properties["next"]
+	require.Equal(t, "object", nextSchema.Type)
+}
+
+func TestGenerateSchemaWithEnumTags(t *testing.T) {
+	t.Parallel()
+
+	type ConfigInput struct {
+		Level    string `json:"level" enum:"debug,info,warn,error" description:"Log level"`
+		Format   string `json:"format" enum:"json,text"`
+		Optional string `json:"optional,omitempty" enum:"a,b,c"`
+	}
+
+	schema := Generate(reflect.TypeOf(ConfigInput{}))
+
+	// Check level field
+	levelSchema := schema.Properties["level"]
+	require.NotNil(t, levelSchema, "Expected level property to exist")
+	require.Len(t, levelSchema.Enum, 4)
+	expectedLevels := []string{"debug", "info", "warn", "error"}
+	for i, expected := range expectedLevels {
+		require.Equal(t, expected, levelSchema.Enum[i])
+	}
+
+	// Check format field
+	formatSchema := schema.Properties["format"]
+	require.NotNil(t, formatSchema, "Expected format property to exist")
+	require.Len(t, formatSchema.Enum, 2)
+
+	// Check required fields (optional should not be required due to omitempty)
+	expectedRequired := []string{"level", "format"}
+	require.Len(t, schema.Required, len(expectedRequired))
+}
+
+func TestGenerateSchemaComplexTypes(t *testing.T) {
+	t.Parallel()
+
+	type ComplexInput struct {
+		StringSlice []string            `json:"string_slice"`
+		IntMap      map[string]int      `json:"int_map"`
+		NestedSlice []map[string]string `json:"nested_slice"`
+		Interface   any                 `json:"interface"`
+	}
+
+	schema := Generate(reflect.TypeOf(ComplexInput{}))
+
+	// Check string slice
+	stringSliceSchema := schema.Properties["string_slice"]
+	require.NotNil(t, stringSliceSchema, "Expected string_slice property to exist")
+	require.Equal(t, "array", stringSliceSchema.Type)
+	require.Equal(t, "string", stringSliceSchema.Items.Type)
+
+	// Check int map
+	intMapSchema := schema.Properties["int_map"]
+	require.NotNil(t, intMapSchema, "Expected int_map property to exist")
+	require.Equal(t, "object", intMapSchema.Type)
+
+	// Check nested slice
+	nestedSliceSchema := schema.Properties["nested_slice"]
+	require.NotNil(t, nestedSliceSchema, "Expected nested_slice property to exist")
+	require.Equal(t, "array", nestedSliceSchema.Type)
+	require.Equal(t, "object", nestedSliceSchema.Items.Type)
+
+	// Check interface
+	interfaceSchema := schema.Properties["interface"]
+	require.NotNil(t, interfaceSchema, "Expected interface property to exist")
+	require.Equal(t, "object", interfaceSchema.Type)
+}
+
+func TestToSnakeCase(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		input    string
+		expected string
+	}{
+		{"FirstName", "first_name"},
+		{"XMLHttpRequest", "x_m_l_http_request"},
+		{"ID", "i_d"},
+		{"HTTPSProxy", "h_t_t_p_s_proxy"},
+		{"simple", "simple"},
+		{"", ""},
+		{"A", "a"},
+		{"AB", "a_b"},
+		{"CamelCase", "camel_case"},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.input, func(t *testing.T) {
+			t.Parallel()
+			result := toSnakeCase(tt.input)
+			require.Equal(t, tt.expected, result, "toSnakeCase(%s)", tt.input)
+		})
+	}
+}
+
+func TestSchemaToParametersEdgeCases(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		name     string
+		schema   Schema
+		expected map[string]any
+	}{
+		{
+			name: "non-object schema",
+			schema: Schema{
+				Type: "string",
+			},
+			expected: map[string]any{},
+		},
+		{
+			name: "object with no properties",
+			schema: Schema{
+				Type:       "object",
+				Properties: nil,
+			},
+			expected: map[string]any{},
+		},
+		{
+			name: "object with empty properties",
+			schema: Schema{
+				Type:       "object",
+				Properties: map[string]*Schema{},
+			},
+			expected: map[string]any{},
+		},
+		{
+			name: "schema with all constraint types",
+			schema: Schema{
+				Type: "object",
+				Properties: map[string]*Schema{
+					"text": {
+						Type:      "string",
+						Format:    "email",
+						MinLength: func() *int { v := 5; return &v }(),
+						MaxLength: func() *int { v := 100; return &v }(),
+					},
+					"number": {
+						Type:    "number",
+						Minimum: func() *float64 { v := 0.0; return &v }(),
+						Maximum: func() *float64 { v := 100.0; return &v }(),
+					},
+				},
+			},
+			expected: map[string]any{
+				"text": map[string]any{
+					"type":      "string",
+					"format":    "email",
+					"minLength": 5,
+					"maxLength": 100,
+				},
+				"number": map[string]any{
+					"type":    "number",
+					"minimum": 0.0,
+					"maximum": 100.0,
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			t.Parallel()
+			result := ToParameters(tt.schema)
+			require.Len(t, result, len(tt.expected))
+			for key, expectedValue := range tt.expected {
+				require.NotNil(t, result[key], "Expected parameter %s to exist", key)
+				// Deep comparison would be complex, so we'll check key properties
+				resultParam := result[key].(map[string]any)
+				expectedParam := expectedValue.(map[string]any)
+				for propKey, propValue := range expectedParam {
+					require.Equal(t, propValue, resultParam[propKey], "Expected %s.%s", key, propKey)
+				}
+			}
+		})
+	}
+}

tool.go šŸ”—

@@ -5,24 +5,12 @@ import (
 	"encoding/json"
 	"fmt"
 	"reflect"
-	"slices"
-	"strings"
+
+	"charm.land/fantasy/schema"
 )
 
 // Schema represents a JSON schema for tool input validation.
-type Schema struct {
-	Type        string             `json:"type,omitempty"`
-	Properties  map[string]*Schema `json:"properties,omitempty"`
-	Required    []string           `json:"required,omitempty"`
-	Items       *Schema            `json:"items,omitempty"`
-	Description string             `json:"description,omitempty"`
-	Enum        []any              `json:"enum,omitempty"`
-	Format      string             `json:"format,omitempty"`
-	Minimum     *float64           `json:"minimum,omitempty"`
-	Maximum     *float64           `json:"maximum,omitempty"`
-	MinLength   *int               `json:"minLength,omitempty"`
-	MaxLength   *int               `json:"maxLength,omitempty"`
-}
+type Schema = schema.Schema
 
 // ToolInfo represents tool metadata, matching the existing pattern.
 type ToolInfo struct {
@@ -93,7 +81,7 @@ func NewAgentTool[TInput any](
 	fn func(ctx context.Context, input TInput, call ToolCall) (ToolResponse, error),
 ) AgentTool {
 	var input TInput
-	schema := generateSchema(reflect.TypeOf(input))
+	schema := schema.Generate(reflect.TypeOf(input))
 
 	return &funcToolWrapper[TInput]{
 		name:        name,
@@ -127,7 +115,7 @@ func (w *funcToolWrapper[TInput]) Info() ToolInfo {
 	return ToolInfo{
 		Name:        w.name,
 		Description: w.description,
-		Parameters:  schemaToParameters(w.schema),
+		Parameters:  schema.ToParameters(w.schema),
 		Required:    w.schema.Required,
 	}
 }
@@ -140,201 +128,3 @@ func (w *funcToolWrapper[TInput]) Run(ctx context.Context, params ToolCall) (Too
 
 	return w.fn(ctx, input, params)
 }
-
-// schemaToParameters converts a Schema to the parameters map format expected by ToolInfo.
-func schemaToParameters(schema Schema) map[string]any {
-	if schema.Type != "object" || schema.Properties == nil {
-		return map[string]any{}
-	}
-
-	params := make(map[string]any)
-	for name, propSchema := range schema.Properties {
-		param := map[string]any{
-			"type": propSchema.Type,
-		}
-
-		if propSchema.Description != "" {
-			param["description"] = propSchema.Description
-		}
-
-		if len(propSchema.Enum) > 0 {
-			param["enum"] = propSchema.Enum
-		}
-
-		if propSchema.Format != "" {
-			param["format"] = propSchema.Format
-		}
-
-		if propSchema.Minimum != nil {
-			param["minimum"] = *propSchema.Minimum
-		}
-
-		if propSchema.Maximum != nil {
-			param["maximum"] = *propSchema.Maximum
-		}
-
-		if propSchema.MinLength != nil {
-			param["minLength"] = *propSchema.MinLength
-		}
-
-		if propSchema.MaxLength != nil {
-			param["maxLength"] = *propSchema.MaxLength
-		}
-
-		if propSchema.Items != nil {
-			param["items"] = schemaToParameters(*propSchema.Items)
-		}
-
-		params[name] = param
-	}
-
-	return params
-}
-
-// generateSchema automatically generates a JSON schema from a Go type.
-func generateSchema(t reflect.Type) Schema {
-	return generateSchemaRecursive(t, nil, make(map[reflect.Type]bool))
-}
-
-func generateSchemaRecursive(t, parent reflect.Type, visited map[reflect.Type]bool) Schema {
-	// Handle pointers
-	if t.Kind() == reflect.Pointer {
-		t = t.Elem()
-	}
-
-	// Prevent infinite recursion
-	if visited[t] {
-		return Schema{Type: "object"}
-	}
-	visited[t] = true
-	defer delete(visited, t)
-
-	switch t.Kind() {
-	case reflect.String:
-		return Schema{Type: "string"}
-	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
-		reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
-		return Schema{Type: "integer"}
-	case reflect.Float32, reflect.Float64:
-		return Schema{Type: "number"}
-	case reflect.Bool:
-		return Schema{Type: "boolean"}
-	case reflect.Slice, reflect.Array:
-		itemSchema := generateSchemaRecursive(t.Elem(), t, visited)
-		return Schema{
-			Type:  "array",
-			Items: &itemSchema,
-		}
-	case reflect.Map:
-		if t.Key().Kind() == reflect.String {
-			valueSchema := generateSchemaRecursive(t.Elem(), t, visited)
-			schema := Schema{
-				Type: "object",
-				Properties: map[string]*Schema{
-					"*": &valueSchema,
-				},
-			}
-			if useBlankType(parent) {
-				schema.Type = ""
-			}
-			return schema
-		}
-		return Schema{Type: "object"}
-	case reflect.Struct:
-		schema := Schema{
-			Type:       "object",
-			Properties: make(map[string]*Schema),
-		}
-		if useBlankType(parent) {
-			schema.Type = ""
-		}
-
-		for i := range t.NumField() {
-			field := t.Field(i)
-
-			// Skip unexported fields
-			if !field.IsExported() {
-				continue
-			}
-
-			jsonTag := field.Tag.Get("json")
-			if jsonTag == "-" {
-				continue
-			}
-
-			fieldName := field.Name
-			required := true
-
-			// Parse JSON tag
-			if jsonTag != "" {
-				parts := strings.Split(jsonTag, ",")
-				if parts[0] != "" {
-					fieldName = parts[0]
-				}
-
-				// Check for omitempty
-				if slices.Contains(parts[1:], "omitempty") {
-					required = false
-				}
-			} else {
-				// Convert field name to snake_case for JSON
-				fieldName = toSnakeCase(fieldName)
-			}
-
-			fieldSchema := generateSchemaRecursive(field.Type, t, visited)
-
-			// Add description from struct tag if available
-			if desc := field.Tag.Get("description"); desc != "" {
-				fieldSchema.Description = desc
-			}
-
-			// Add enum values from struct tag if available
-			if enumTag := field.Tag.Get("enum"); enumTag != "" {
-				enumValues := strings.Split(enumTag, ",")
-				fieldSchema.Enum = make([]any, len(enumValues))
-				for i, v := range enumValues {
-					fieldSchema.Enum[i] = strings.TrimSpace(v)
-				}
-			}
-
-			schema.Properties[fieldName] = &fieldSchema
-
-			if required {
-				schema.Required = append(schema.Required, fieldName)
-			}
-		}
-
-		return schema
-	case reflect.Interface:
-		return Schema{Type: "object"}
-	default:
-		return Schema{Type: "object"}
-	}
-}
-
-// toSnakeCase converts PascalCase to snake_case.
-func toSnakeCase(s string) string {
-	var result strings.Builder
-	for i, r := range s {
-		if i > 0 && r >= 'A' && r <= 'Z' {
-			result.WriteByte('_')
-		}
-		result.WriteRune(r)
-	}
-	return strings.ToLower(result.String())
-}
-
-// NOTE(@andreynering): This is a hacky workaround for llama.cpp.
-// Ideally, we should always output `type: object` for objects, but
-// llama.cpp complains if we do for arrays of objects.
-func useBlankType(parent reflect.Type) bool {
-	if parent == nil {
-		return false
-	}
-	switch parent.Kind() {
-	case reflect.Slice, reflect.Array:
-		return true
-	default:
-		return false
-	}
-}

tool_test.go šŸ”—

@@ -3,7 +3,6 @@ package fantasy
 import (
 	"context"
 	"fmt"
-	"reflect"
 	"testing"
 
 	"github.com/stretchr/testify/require"
@@ -64,7 +63,6 @@ func TestEnumToolExample(t *testing.T) {
 			return NewTextResponse(fmt.Sprintf("Weather in %s: %s, sunny", input.Location, temp)), nil
 		},
 	)
-
 	// Check that the schema includes enum values
 	info := tool.Info()
 	unitsParam, ok := info.Parameters["units"].(map[string]any)
@@ -85,529 +83,3 @@ func TestEnumToolExample(t *testing.T) {
 	require.Contains(t, result.Content, "San Francisco")
 	require.Contains(t, result.Content, "72°F")
 }
-
-func TestEnumSupport(t *testing.T) {
-	// Test enum via struct tags
-	type WeatherInput struct {
-		Location string `json:"location" description:"City name"`
-		Units    string `json:"units" enum:"celsius,fahrenheit,kelvin" description:"Temperature units"`
-		Format   string `json:"format,omitempty" enum:"json,xml,text"`
-	}
-
-	schema := generateSchema(reflect.TypeOf(WeatherInput{}))
-
-	require.Equal(t, "object", schema.Type)
-
-	// Check units field has enum values
-	unitsSchema := schema.Properties["units"]
-	require.NotNil(t, unitsSchema, "Expected units property to exist")
-	require.Len(t, unitsSchema.Enum, 3)
-	expectedUnits := []string{"celsius", "fahrenheit", "kelvin"}
-	for i, expected := range expectedUnits {
-		require.Equal(t, expected, unitsSchema.Enum[i])
-	}
-
-	// Check required fields (format should not be required due to omitempty)
-	expectedRequired := []string{"location", "units"}
-	require.Len(t, schema.Required, len(expectedRequired))
-}
-
-func TestSchemaToParameters(t *testing.T) {
-	schema := Schema{
-		Type: "object",
-		Properties: map[string]*Schema{
-			"name": {
-				Type:        "string",
-				Description: "The name field",
-			},
-			"age": {
-				Type:    "integer",
-				Minimum: func() *float64 { v := 0.0; return &v }(),
-				Maximum: func() *float64 { v := 120.0; return &v }(),
-			},
-			"tags": {
-				Type: "array",
-				Items: &Schema{
-					Type: "string",
-				},
-			},
-			"priority": {
-				Type: "string",
-				Enum: []any{"low", "medium", "high"},
-			},
-		},
-		Required: []string{"name"},
-	}
-
-	params := schemaToParameters(schema)
-
-	// Check name parameter
-	nameParam, ok := params["name"].(map[string]any)
-	require.True(t, ok, "Expected name parameter to exist")
-	require.Equal(t, "string", nameParam["type"])
-	require.Equal(t, "The name field", nameParam["description"])
-
-	// Check age parameter with min/max
-	ageParam, ok := params["age"].(map[string]any)
-	require.True(t, ok, "Expected age parameter to exist")
-	require.Equal(t, "integer", ageParam["type"])
-	require.Equal(t, 0.0, ageParam["minimum"])
-	require.Equal(t, 120.0, ageParam["maximum"])
-
-	// Check priority parameter with enum
-	priorityParam, ok := params["priority"].(map[string]any)
-	require.True(t, ok, "Expected priority parameter to exist")
-	require.Equal(t, "string", priorityParam["type"])
-	enumValues, ok := priorityParam["enum"].([]any)
-	require.True(t, ok)
-	require.Len(t, enumValues, 3)
-}
-
-func TestGenerateSchemaBasicTypes(t *testing.T) {
-	t.Parallel()
-
-	tests := []struct {
-		name     string
-		input    any
-		expected Schema
-	}{
-		{
-			name:     "string type",
-			input:    "",
-			expected: Schema{Type: "string"},
-		},
-		{
-			name:     "int type",
-			input:    0,
-			expected: Schema{Type: "integer"},
-		},
-		{
-			name:     "int64 type",
-			input:    int64(0),
-			expected: Schema{Type: "integer"},
-		},
-		{
-			name:     "uint type",
-			input:    uint(0),
-			expected: Schema{Type: "integer"},
-		},
-		{
-			name:     "float64 type",
-			input:    0.0,
-			expected: Schema{Type: "number"},
-		},
-		{
-			name:     "float32 type",
-			input:    float32(0.0),
-			expected: Schema{Type: "number"},
-		},
-		{
-			name:     "bool type",
-			input:    false,
-			expected: Schema{Type: "boolean"},
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			t.Parallel()
-			schema := generateSchema(reflect.TypeOf(tt.input))
-			require.Equal(t, tt.expected.Type, schema.Type)
-		})
-	}
-}
-
-func TestGenerateSchemaArrayTypes(t *testing.T) {
-	t.Parallel()
-
-	tests := []struct {
-		name     string
-		input    any
-		expected Schema
-	}{
-		{
-			name:  "string slice",
-			input: []string{},
-			expected: Schema{
-				Type:  "array",
-				Items: &Schema{Type: "string"},
-			},
-		},
-		{
-			name:  "int slice",
-			input: []int{},
-			expected: Schema{
-				Type:  "array",
-				Items: &Schema{Type: "integer"},
-			},
-		},
-		{
-			name:  "string array",
-			input: [3]string{},
-			expected: Schema{
-				Type:  "array",
-				Items: &Schema{Type: "string"},
-			},
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			t.Parallel()
-			schema := generateSchema(reflect.TypeOf(tt.input))
-			require.Equal(t, tt.expected.Type, schema.Type)
-			require.NotNil(t, schema.Items, "Expected items schema to exist")
-			require.Equal(t, tt.expected.Items.Type, schema.Items.Type)
-		})
-	}
-}
-
-func TestGenerateSchemaMapTypes(t *testing.T) {
-	t.Parallel()
-
-	tests := []struct {
-		name     string
-		input    any
-		expected string
-	}{
-		{
-			name:     "string to string map",
-			input:    map[string]string{},
-			expected: "object",
-		},
-		{
-			name:     "string to int map",
-			input:    map[string]int{},
-			expected: "object",
-		},
-		{
-			name:     "int to string map",
-			input:    map[int]string{},
-			expected: "object",
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			t.Parallel()
-			schema := generateSchema(reflect.TypeOf(tt.input))
-			require.Equal(t, tt.expected, schema.Type)
-		})
-	}
-}
-
-func TestGenerateSchemaStructTypes(t *testing.T) {
-	t.Parallel()
-
-	type SimpleStruct struct {
-		Name string `json:"name" description:"The name field"`
-		Age  int    `json:"age"`
-	}
-
-	type StructWithOmitEmpty struct {
-		Required string `json:"required"`
-		Optional string `json:"optional,omitempty"`
-	}
-
-	type StructWithJSONIgnore struct {
-		Visible string `json:"visible"`
-		Hidden  string `json:"-"`
-	}
-
-	type StructWithoutJSONTags struct {
-		FirstName string
-		LastName  string
-	}
-
-	tests := []struct {
-		name     string
-		input    any
-		validate func(t *testing.T, schema Schema)
-	}{
-		{
-			name:  "simple struct",
-			input: SimpleStruct{},
-			validate: func(t *testing.T, schema Schema) {
-				require.Equal(t, "object", schema.Type)
-				require.Len(t, schema.Properties, 2)
-				require.NotNil(t, schema.Properties["name"], "Expected name property to exist")
-				require.Equal(t, "The name field", schema.Properties["name"].Description)
-				require.Len(t, schema.Required, 2)
-			},
-		},
-		{
-			name:  "struct with omitempty",
-			input: StructWithOmitEmpty{},
-			validate: func(t *testing.T, schema Schema) {
-				require.Len(t, schema.Required, 1)
-				require.Equal(t, "required", schema.Required[0])
-			},
-		},
-		{
-			name:  "struct with json ignore",
-			input: StructWithJSONIgnore{},
-			validate: func(t *testing.T, schema Schema) {
-				require.Len(t, schema.Properties, 1)
-				require.NotNil(t, schema.Properties["visible"], "Expected visible property to exist")
-				require.Nil(t, schema.Properties["hidden"], "Expected hidden property to not exist")
-			},
-		},
-		{
-			name:  "struct without json tags",
-			input: StructWithoutJSONTags{},
-			validate: func(t *testing.T, schema Schema) {
-				require.NotNil(t, schema.Properties["first_name"], "Expected first_name property to exist")
-				require.NotNil(t, schema.Properties["last_name"], "Expected last_name property to exist")
-			},
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			t.Parallel()
-			schema := generateSchema(reflect.TypeOf(tt.input))
-			tt.validate(t, schema)
-		})
-	}
-}
-
-func TestGenerateSchemaPointerTypes(t *testing.T) {
-	t.Parallel()
-
-	type StructWithPointers struct {
-		Name *string `json:"name"`
-		Age  *int    `json:"age"`
-	}
-
-	schema := generateSchema(reflect.TypeOf(StructWithPointers{}))
-
-	require.Equal(t, "object", schema.Type)
-
-	require.NotNil(t, schema.Properties["name"], "Expected name property to exist")
-	require.Equal(t, "string", schema.Properties["name"].Type)
-
-	require.NotNil(t, schema.Properties["age"], "Expected age property to exist")
-	require.Equal(t, "integer", schema.Properties["age"].Type)
-}
-
-func TestGenerateSchemaNestedStructs(t *testing.T) {
-	t.Parallel()
-
-	type Address struct {
-		Street string `json:"street"`
-		City   string `json:"city"`
-	}
-
-	type Person struct {
-		Name    string  `json:"name"`
-		Address Address `json:"address"`
-	}
-
-	schema := generateSchema(reflect.TypeOf(Person{}))
-
-	require.Equal(t, "object", schema.Type)
-
-	require.NotNil(t, schema.Properties["address"], "Expected address property to exist")
-
-	addressSchema := schema.Properties["address"]
-	require.Equal(t, "object", addressSchema.Type)
-
-	require.NotNil(t, addressSchema.Properties["street"], "Expected street property in address to exist")
-	require.NotNil(t, addressSchema.Properties["city"], "Expected city property in address to exist")
-}
-
-func TestGenerateSchemaRecursiveStructs(t *testing.T) {
-	t.Parallel()
-
-	type Node struct {
-		Value string `json:"value"`
-		Next  *Node  `json:"next,omitempty"`
-	}
-
-	schema := generateSchema(reflect.TypeOf(Node{}))
-
-	require.Equal(t, "object", schema.Type)
-
-	require.NotNil(t, schema.Properties["value"], "Expected value property to exist")
-
-	require.NotNil(t, schema.Properties["next"], "Expected next property to exist")
-
-	// The recursive reference should be handled gracefully
-	nextSchema := schema.Properties["next"]
-	require.Equal(t, "object", nextSchema.Type)
-}
-
-func TestGenerateSchemaWithEnumTags(t *testing.T) {
-	t.Parallel()
-
-	type ConfigInput struct {
-		Level    string `json:"level" enum:"debug,info,warn,error" description:"Log level"`
-		Format   string `json:"format" enum:"json,text"`
-		Optional string `json:"optional,omitempty" enum:"a,b,c"`
-	}
-
-	schema := generateSchema(reflect.TypeOf(ConfigInput{}))
-
-	// Check level field
-	levelSchema := schema.Properties["level"]
-	require.NotNil(t, levelSchema, "Expected level property to exist")
-	require.Len(t, levelSchema.Enum, 4)
-	expectedLevels := []string{"debug", "info", "warn", "error"}
-	for i, expected := range expectedLevels {
-		require.Equal(t, expected, levelSchema.Enum[i])
-	}
-
-	// Check format field
-	formatSchema := schema.Properties["format"]
-	require.NotNil(t, formatSchema, "Expected format property to exist")
-	require.Len(t, formatSchema.Enum, 2)
-
-	// Check required fields (optional should not be required due to omitempty)
-	expectedRequired := []string{"level", "format"}
-	require.Len(t, schema.Required, len(expectedRequired))
-}
-
-func TestGenerateSchemaComplexTypes(t *testing.T) {
-	t.Parallel()
-
-	type ComplexInput struct {
-		StringSlice []string            `json:"string_slice"`
-		IntMap      map[string]int      `json:"int_map"`
-		NestedSlice []map[string]string `json:"nested_slice"`
-		Interface   any                 `json:"interface"`
-	}
-
-	schema := generateSchema(reflect.TypeOf(ComplexInput{}))
-
-	// Check string slice
-	stringSliceSchema := schema.Properties["string_slice"]
-	require.NotNil(t, stringSliceSchema, "Expected string_slice property to exist")
-	require.Equal(t, "array", stringSliceSchema.Type)
-	require.Equal(t, "string", stringSliceSchema.Items.Type)
-
-	// Check int map
-	intMapSchema := schema.Properties["int_map"]
-	require.NotNil(t, intMapSchema, "Expected int_map property to exist")
-	require.Equal(t, "object", intMapSchema.Type)
-
-	// Check nested slice
-	nestedSliceSchema := schema.Properties["nested_slice"]
-	require.NotNil(t, nestedSliceSchema, "Expected nested_slice property to exist")
-	require.Equal(t, "array", nestedSliceSchema.Type)
-	require.Equal(t, "", nestedSliceSchema.Items.Type)
-
-	// Check interface
-	interfaceSchema := schema.Properties["interface"]
-	require.NotNil(t, interfaceSchema, "Expected interface property to exist")
-	require.Equal(t, "object", interfaceSchema.Type)
-}
-
-func TestToSnakeCase(t *testing.T) {
-	t.Parallel()
-
-	tests := []struct {
-		input    string
-		expected string
-	}{
-		{"FirstName", "first_name"},
-		{"XMLHttpRequest", "x_m_l_http_request"},
-		{"ID", "i_d"},
-		{"HTTPSProxy", "h_t_t_p_s_proxy"},
-		{"simple", "simple"},
-		{"", ""},
-		{"A", "a"},
-		{"AB", "a_b"},
-		{"CamelCase", "camel_case"},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.input, func(t *testing.T) {
-			t.Parallel()
-			result := toSnakeCase(tt.input)
-			require.Equal(t, tt.expected, result, "toSnakeCase(%s)", tt.input)
-		})
-	}
-}
-
-func TestSchemaToParametersEdgeCases(t *testing.T) {
-	t.Parallel()
-
-	tests := []struct {
-		name     string
-		schema   Schema
-		expected map[string]any
-	}{
-		{
-			name: "non-object schema",
-			schema: Schema{
-				Type: "string",
-			},
-			expected: map[string]any{},
-		},
-		{
-			name: "object with no properties",
-			schema: Schema{
-				Type:       "object",
-				Properties: nil,
-			},
-			expected: map[string]any{},
-		},
-		{
-			name: "object with empty properties",
-			schema: Schema{
-				Type:       "object",
-				Properties: map[string]*Schema{},
-			},
-			expected: map[string]any{},
-		},
-		{
-			name: "schema with all constraint types",
-			schema: Schema{
-				Type: "object",
-				Properties: map[string]*Schema{
-					"text": {
-						Type:      "string",
-						Format:    "email",
-						MinLength: func() *int { v := 5; return &v }(),
-						MaxLength: func() *int { v := 100; return &v }(),
-					},
-					"number": {
-						Type:    "number",
-						Minimum: func() *float64 { v := 0.0; return &v }(),
-						Maximum: func() *float64 { v := 100.0; return &v }(),
-					},
-				},
-			},
-			expected: map[string]any{
-				"text": map[string]any{
-					"type":      "string",
-					"format":    "email",
-					"minLength": 5,
-					"maxLength": 100,
-				},
-				"number": map[string]any{
-					"type":    "number",
-					"minimum": 0.0,
-					"maximum": 100.0,
-				},
-			},
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			t.Parallel()
-			result := schemaToParameters(tt.schema)
-			require.Len(t, result, len(tt.expected))
-			for key, expectedValue := range tt.expected {
-				require.NotNil(t, result[key], "Expected parameter %s to exist", key)
-				// Deep comparison would be complex, so we'll check key properties
-				resultParam := result[key].(map[string]any)
-				expectedParam := expectedValue.(map[string]any)
-				for propKey, propValue := range expectedParam {
-					require.Equal(t, propValue, resultParam[propKey], "Expected %s.%s", key, propKey)
-				}
-			}
-		})
-	}
-}