// Package anthropic provides an implementation of the fantasy AI SDK for Anthropic's language models.
package anthropic

import (
	"cmp"
	"context"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"maps"
	"math"
	"strings"

	"charm.land/fantasy"
	"charm.land/fantasy/object"
	"charm.land/fantasy/providers/internal/httpheaders"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/charmbracelet/anthropic-sdk-go"
	"github.com/charmbracelet/anthropic-sdk-go/bedrock"
	"github.com/charmbracelet/anthropic-sdk-go/option"
	"github.com/charmbracelet/anthropic-sdk-go/packages/param"
	"github.com/charmbracelet/anthropic-sdk-go/vertex"
	"golang.org/x/oauth2/google"
)

// betaRequestOptions converts beta flag strings into request
// options that enable the corresponding Anthropic beta APIs.
func betaRequestOptions(flags []string) []option.RequestOption {
	if len(flags) == 0 {
		return nil
	}
	opts := []option.RequestOption{option.WithQuery("beta", "true")}
	for _, flag := range flags {
		opts = append(opts, option.WithHeaderAdd("anthropic-beta", flag))
	}
	return opts
}

// buildRequestOptions constructs the common request options shared
// by Generate and Stream: user-agent, raw tool injection, and any
// beta API flags.
func buildRequestOptions(call fantasy.Call, rawTools []json.RawMessage, betaFlags []string) []option.RequestOption {
	providerOptions := &ProviderOptions{}
	if v, ok := call.ProviderOptions[Name]; ok {
		providerOptions, _ = v.(*ProviderOptions)
	}

	reqOpts := callUARequestOptions(call)
	if len(rawTools) > 0 {
		// Tools are injected as raw JSON rather than via params.Tools
		// because the SDK doesn't model beta tool types (e.g. computer
		// use). If the SDK adds validation that reads params.Tools,
		// this will need updating.
		reqOpts = append(reqOpts, option.WithJSONSet("tools", rawTools))
	}
	for k, v := range providerOptions.ExtraBody {
		reqOpts = append(reqOpts, option.WithJSONSet(k, v))
	}
	if len(betaFlags) > 0 {
		reqOpts = append(reqOpts, betaRequestOptions(betaFlags)...)
	}
	return reqOpts
}

const (
	// Name is the name of the Anthropic provider.
	Name = "anthropic"
	// DefaultURL is the default URL for the Anthropic API.
	DefaultURL = "https://api.anthropic.com"
	// VertexAuthScope is the auth scope required for vertex auth if using a Service Account JSON file (e.g. GOOGLE_APPLICATION_CREDENTIALS).
	VertexAuthScope = "https://www.googleapis.com/auth/cloud-platform"
)

type options struct {
	baseURL   string
	apiKey    string
	name      string
	headers   map[string]string
	userAgent string
	client    option.HTTPClient

	vertexProject  string
	vertexLocation string
	skipAuth       bool

	useBedrock    bool
	bedrockRegion string

	objectMode fantasy.ObjectMode
}

type provider struct {
	options options
}

// Option defines a function that configures Anthropic provider options.
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{},
		objectMode: fantasy.ObjectModeAuto,
	}
	for _, o := range opts {
		o(&providerOptions)
	}

	if !providerOptions.useBedrock {
		providerOptions.baseURL = cmp.Or(providerOptions.baseURL, DefaultURL)
	}
	providerOptions.name = cmp.Or(providerOptions.name, Name)
	return &provider{options: providerOptions}, nil
}

// WithBaseURL sets the base URL for the Anthropic provider.
func WithBaseURL(baseURL string) Option {
	return func(o *options) {
		o.baseURL = baseURL
	}
}

// WithAPIKey sets the API key for the Anthropic provider.
func WithAPIKey(apiKey string) Option {
	return func(o *options) {
		o.apiKey = apiKey
	}
}

// WithVertex configures the Anthropic provider to use Vertex AI.
func WithVertex(project, location string) Option {
	return func(o *options) {
		o.vertexProject = project
		o.vertexLocation = location
	}
}

// WithSkipAuth configures whether to skip authentication for the Anthropic provider.
func WithSkipAuth(skip bool) Option {
	return func(o *options) {
		o.skipAuth = skip
	}
}

// WithBedrock configures the Anthropic provider to use AWS Bedrock.
func WithBedrock() Option {
	return func(o *options) {
		o.useBedrock = true
	}
}

// WithBedrockRegion sets the AWS region for the Bedrock provider.
func WithBedrockRegion(region string) Option {
	return func(o *options) {
		o.bedrockRegion = region
	}
}

// WithName sets the name for the Anthropic provider.
func WithName(name string) Option {
	return func(o *options) {
		o.name = name
	}
}

// WithHeaders sets the headers for the Anthropic provider.
func WithHeaders(headers map[string]string) Option {
	return func(o *options) {
		maps.Copy(o.headers, headers)
	}
}

// WithHTTPClient sets the HTTP client for the Anthropic provider.
func WithHTTPClient(client option.HTTPClient) Option {
	return func(o *options) {
		o.client = client
	}
}

// WithUserAgent sets an explicit User-Agent header, overriding the default and any
// value set via WithHeaders.
func WithUserAgent(ua string) Option {
	return func(o *options) {
		o.userAgent = ua
	}
}

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

	if a.options.apiKey != "" && !a.options.useBedrock {
		clientOptions = append(clientOptions, option.WithAPIKey(a.options.apiKey))
	}
	if !a.options.useBedrock && a.options.baseURL != "" {
		clientOptions = append(clientOptions, option.WithBaseURL(a.options.baseURL))
	}
	defaultUA := httpheaders.DefaultUserAgent(fantasy.Version)
	resolved := httpheaders.ResolveHeaders(a.options.headers, a.options.userAgent, defaultUA)
	for key, value := range resolved {
		clientOptions = append(clientOptions, option.WithHeader(key, value))
	}
	if a.options.client != nil {
		clientOptions = append(clientOptions, option.WithHTTPClient(a.options.client))
	}
	if a.options.vertexProject != "" && a.options.vertexLocation != "" {
		var credentials *google.Credentials
		if a.options.skipAuth {
			credentials = &google.Credentials{TokenSource: &googleDummyTokenSource{}}
		} else {
			var err error
			credentials, err = google.FindDefaultCredentials(ctx, VertexAuthScope)
			if err != nil {
				return nil, err
			}
		}

		clientOptions = append(
			clientOptions,
			vertex.WithCredentials(
				ctx,
				a.options.vertexLocation,
				a.options.vertexProject,
				credentials,
			),
		)
	}
	if a.options.useBedrock {
		if a.options.skipAuth || a.options.apiKey != "" {
			clientOptions = append(
				clientOptions,
				bedrock.WithConfig(bedrockBasicAuthConfig(a.options.apiKey, a.options.bedrockRegion)),
			)
		} else {
			if cfg, err := config.LoadDefaultConfig(ctx); err == nil {
				cfg.Region = cmp.Or(a.options.bedrockRegion, cfg.Region)
				clientOptions = append(
					clientOptions,
					bedrock.WithConfig(cfg),
				)
			}
		}
		if a.options.baseURL != "" {
			clientOptions = append(clientOptions, option.WithBaseURL(a.options.baseURL))
		}
	}
	return languageModel{
		modelID:  modelID,
		provider: a.options.name,
		options:  a.options,
		client:   anthropic.NewClient(clientOptions...),
	}, nil
}

type languageModel struct {
	provider string
	modelID  string
	client   anthropic.Client
	options  options
}

// Model implements fantasy.LanguageModel.
func (a languageModel) Model() string {
	return a.modelID
}

// Provider implements fantasy.LanguageModel.
func (a languageModel) Provider() string {
	return a.provider
}

func (a languageModel) prepareParams(call fantasy.Call) (
	params *anthropic.MessageNewParams,
	rawTools []json.RawMessage,
	warnings []fantasy.CallWarning,
	betaFlags []string,
	err error,
) {
	params = &anthropic.MessageNewParams{}
	providerOptions := &ProviderOptions{}
	if v, ok := call.ProviderOptions[Name]; ok {
		providerOptions, ok = v.(*ProviderOptions)
		if !ok {
			return nil, nil, nil, nil, &fantasy.Error{Title: "invalid argument", Message: "anthropic provider options should be *anthropic.ProviderOptions"}
		}
	}
	sendReasoning := true
	if providerOptions.SendReasoning != nil {
		sendReasoning = *providerOptions.SendReasoning
	}
	systemBlocks, messages, warnings := toPrompt(call.Prompt, sendReasoning)

	if call.FrequencyPenalty != nil {
		warnings = append(warnings, fantasy.CallWarning{
			Type:    fantasy.CallWarningTypeUnsupportedSetting,
			Setting: "FrequencyPenalty",
		})
	}
	if call.PresencePenalty != nil {
		warnings = append(warnings, fantasy.CallWarning{
			Type:    fantasy.CallWarningTypeUnsupportedSetting,
			Setting: "PresencePenalty",
		})
	}

	params.System = systemBlocks
	params.Messages = messages
	params.Model = anthropic.Model(a.modelID)
	params.MaxTokens = 4096

	if call.MaxOutputTokens != nil {
		params.MaxTokens = *call.MaxOutputTokens
	}

	if call.Temperature != nil {
		params.Temperature = param.NewOpt(*call.Temperature)
	}
	if call.TopK != nil {
		params.TopK = param.NewOpt(*call.TopK)
	}
	if call.TopP != nil {
		params.TopP = param.NewOpt(*call.TopP)
	}

	switch {
	case providerOptions.Effort != nil:
		effort := *providerOptions.Effort
		params.OutputConfig = anthropic.OutputConfigParam{
			Effort: anthropic.OutputConfigEffort(effort),
		}
		adaptive := anthropic.NewThinkingConfigAdaptiveParam()
		params.Thinking.OfAdaptive = &adaptive
	case providerOptions.Thinking != nil:
		if providerOptions.Thinking.BudgetTokens == 0 {
			return nil, nil, nil, nil, &fantasy.Error{Title: "no budget", Message: "thinking requires budget"}
		}
		params.Thinking = anthropic.ThinkingConfigParamOfEnabled(providerOptions.Thinking.BudgetTokens)
		if call.Temperature != nil {
			params.Temperature = param.Opt[float64]{}
			warnings = append(warnings, fantasy.CallWarning{
				Type:    fantasy.CallWarningTypeUnsupportedSetting,
				Setting: "temperature",
				Details: "temperature is not supported when thinking is enabled",
			})
		}
		if call.TopP != nil {
			params.TopP = param.Opt[float64]{}
			warnings = append(warnings, fantasy.CallWarning{
				Type:    fantasy.CallWarningTypeUnsupportedSetting,
				Setting: "TopP",
				Details: "TopP is not supported when thinking is enabled",
			})
		}
		if call.TopK != nil {
			params.TopK = param.Opt[int64]{}
			warnings = append(warnings, fantasy.CallWarning{
				Type:    fantasy.CallWarningTypeUnsupportedSetting,
				Setting: "TopK",
				Details: "TopK is not supported when thinking is enabled",
			})
		}
	}

	if len(call.Tools) > 0 {
		disableParallelToolUse := false
		if providerOptions.DisableParallelToolUse != nil {
			disableParallelToolUse = *providerOptions.DisableParallelToolUse
		}
		var toolChoice *anthropic.ToolChoiceUnionParam
		var toolWarnings []fantasy.CallWarning
		rawTools, toolChoice, toolWarnings, betaFlags = a.toTools(call.Tools, call.ToolChoice, disableParallelToolUse)
		if toolChoice != nil {
			params.ToolChoice = *toolChoice
		}
		warnings = append(warnings, toolWarnings...)
	}

	return params, rawTools, warnings, betaFlags, nil
}

func (a *provider) Name() string {
	return Name
}

// GetCacheControl extracts cache control settings from provider options.
func GetCacheControl(providerOptions fantasy.ProviderOptions) *CacheControl {
	if anthropicOptions, ok := providerOptions[Name]; ok {
		if options, ok := anthropicOptions.(*ProviderCacheControlOptions); ok {
			return &options.CacheControl
		}
	}
	return nil
}

// GetReasoningMetadata extracts reasoning metadata from provider options.
func GetReasoningMetadata(providerOptions fantasy.ProviderOptions) *ReasoningOptionMetadata {
	if anthropicOptions, ok := providerOptions[Name]; ok {
		if reasoning, ok := anthropicOptions.(*ReasoningOptionMetadata); ok {
			return reasoning
		}
	}
	return nil
}

func reasoningProviderMetadata(signature, redactedData string) fantasy.ProviderMetadata {
	switch {
	case signature != "":
		return fantasy.ProviderMetadata{
			Name: &ReasoningOptionMetadata{Signature: signature},
		}
	case redactedData != "":
		return fantasy.ProviderMetadata{
			Name: &ReasoningOptionMetadata{RedactedData: redactedData},
		}
	default:
		return nil
	}
}

type messageBlock struct {
	Role     fantasy.MessageRole
	Messages []fantasy.Message
}

func groupIntoBlocks(prompt fantasy.Prompt) []*messageBlock {
	var blocks []*messageBlock

	var currentBlock *messageBlock

	for _, msg := range prompt {
		switch msg.Role {
		case fantasy.MessageRoleSystem:
			if currentBlock == nil || currentBlock.Role != fantasy.MessageRoleSystem {
				currentBlock = &messageBlock{
					Role:     fantasy.MessageRoleSystem,
					Messages: []fantasy.Message{},
				}
				blocks = append(blocks, currentBlock)
			}
			currentBlock.Messages = append(currentBlock.Messages, msg)
		case fantasy.MessageRoleUser:
			if currentBlock == nil || currentBlock.Role != fantasy.MessageRoleUser {
				currentBlock = &messageBlock{
					Role:     fantasy.MessageRoleUser,
					Messages: []fantasy.Message{},
				}
				blocks = append(blocks, currentBlock)
			}
			currentBlock.Messages = append(currentBlock.Messages, msg)
		case fantasy.MessageRoleAssistant:
			if currentBlock == nil || currentBlock.Role != fantasy.MessageRoleAssistant {
				currentBlock = &messageBlock{
					Role:     fantasy.MessageRoleAssistant,
					Messages: []fantasy.Message{},
				}
				blocks = append(blocks, currentBlock)
			}
			currentBlock.Messages = append(currentBlock.Messages, msg)
		case fantasy.MessageRoleTool:
			if currentBlock == nil || currentBlock.Role != fantasy.MessageRoleUser {
				currentBlock = &messageBlock{
					Role:     fantasy.MessageRoleUser,
					Messages: []fantasy.Message{},
				}
				blocks = append(blocks, currentBlock)
			}
			currentBlock.Messages = append(currentBlock.Messages, msg)
		}
	}
	return blocks
}

func anyToStringSlice(v any) []string {
	switch typed := v.(type) {
	case []string:
		if len(typed) == 0 {
			return nil
		}
		out := make([]string, len(typed))
		copy(out, typed)
		return out
	case []any:
		if len(typed) == 0 {
			return nil
		}
		out := make([]string, 0, len(typed))
		for _, item := range typed {
			s, ok := item.(string)
			if !ok || s == "" {
				continue
			}
			out = append(out, s)
		}
		if len(out) == 0 {
			return nil
		}
		return out
	default:
		return nil
	}
}

const maxExactIntFloat64 = float64(1<<53 - 1)

// asProviderDefinedTool extracts the ProviderDefinedTool from a
// Tool, handling both ProviderDefinedTool and
// ExecutableProviderTool.
func asProviderDefinedTool(tool fantasy.Tool) (fantasy.ProviderDefinedTool, bool) {
	if pdt, ok := tool.(fantasy.ProviderDefinedTool); ok {
		return pdt, true
	}
	if ept, ok := tool.(fantasy.ExecutableProviderTool); ok {
		return ept.Definition(), true
	}
	return fantasy.ProviderDefinedTool{}, false
}

func anyToInt64(v any) (int64, bool) {
	switch typed := v.(type) {
	case int:
		return int64(typed), true
	case int8:
		return int64(typed), true
	case int16:
		return int64(typed), true
	case int32:
		return int64(typed), true
	case int64:
		return typed, true
	case uint:
		u64 := uint64(typed)
		if u64 > math.MaxInt64 {
			return 0, false
		}
		return int64(u64), true
	case uint8:
		return int64(typed), true
	case uint16:
		return int64(typed), true
	case uint32:
		return int64(typed), true
	case uint64:
		if typed > math.MaxInt64 {
			return 0, false
		}
		return int64(typed), true
	case float32:
		f := float64(typed)
		if math.Trunc(f) != f || math.IsNaN(f) || math.IsInf(f, 0) || f < -maxExactIntFloat64 || f > maxExactIntFloat64 {
			return 0, false
		}
		return int64(f), true
	case float64:
		if math.Trunc(typed) != typed || math.IsNaN(typed) || math.IsInf(typed, 0) || typed < -maxExactIntFloat64 || typed > maxExactIntFloat64 {
			return 0, false
		}
		return int64(typed), true
	case json.Number:
		parsed, err := typed.Int64()
		if err != nil {
			return 0, false
		}
		return parsed, true
	default:
		return 0, false
	}
}

func anyToUserLocation(v any) *UserLocation {
	switch typed := v.(type) {
	case *UserLocation:
		return typed
	case UserLocation:
		loc := typed
		return &loc
	case map[string]any:
		loc := &UserLocation{}
		if city, ok := typed["city"].(string); ok {
			loc.City = city
		}
		if region, ok := typed["region"].(string); ok {
			loc.Region = region
		}
		if country, ok := typed["country"].(string); ok {
			loc.Country = country
		}
		if timezone, ok := typed["timezone"].(string); ok {
			loc.Timezone = timezone
		}
		if loc.City == "" && loc.Region == "" && loc.Country == "" && loc.Timezone == "" {
			return nil
		}
		return loc
	default:
		return nil
	}
}

func (a languageModel) toTools(tools []fantasy.Tool, toolChoice *fantasy.ToolChoice, disableParallelToolCalls bool) (rawTools []json.RawMessage, anthropicToolChoice *anthropic.ToolChoiceUnionParam, warnings []fantasy.CallWarning, betaFlags []string) {
	for _, tool := range tools {
		if tool.GetType() == fantasy.ToolTypeFunction {
			ft, ok := tool.(fantasy.FunctionTool)
			if !ok {
				continue
			}
			required := []string{}
			var properties any
			if props, ok := ft.InputSchema["properties"]; ok {
				properties = props
			}
			if req, ok := ft.InputSchema["required"]; ok {
				if reqArr, ok := req.([]string); ok {
					required = reqArr
				}
			}
			cacheControl := GetCacheControl(ft.ProviderOptions)

			anthropicTool := anthropic.ToolParam{
				Name:        ft.Name,
				Description: anthropic.String(ft.Description),
				InputSchema: anthropic.ToolInputSchemaParam{
					Properties: properties,
					Required:   required,
				},
			}
			if cacheControl != nil {
				anthropicTool.CacheControl = anthropic.NewCacheControlEphemeralParam()
			}
			raw, err := json.Marshal(anthropic.ToolUnionParam{OfTool: &anthropicTool})
			if err != nil {
				warnings = append(warnings, fantasy.CallWarning{
					Type:    fantasy.CallWarningTypeOther,
					Tool:    tool,
					Message: fmt.Sprintf("failed to marshal function tool: %v", err),
				})
				continue
			}
			rawTools = append(rawTools, raw)
			continue
		}
		if tool.GetType() == fantasy.ToolTypeProviderDefined {
			pt, ok := asProviderDefinedTool(tool)
			if !ok {
				continue
			}
			switch pt.ID {
			case "web_search":
				webSearchTool := anthropic.WebSearchTool20250305Param{}
				if pt.Args != nil {
					if domains := anyToStringSlice(pt.Args["allowed_domains"]); len(domains) > 0 {
						webSearchTool.AllowedDomains = domains
					}
					if domains := anyToStringSlice(pt.Args["blocked_domains"]); len(domains) > 0 {
						webSearchTool.BlockedDomains = domains
					}
					if maxUses, ok := anyToInt64(pt.Args["max_uses"]); ok && maxUses > 0 {
						webSearchTool.MaxUses = param.NewOpt(maxUses)
					}
					if loc := anyToUserLocation(pt.Args["user_location"]); loc != nil {
						var ulp anthropic.UserLocationParam
						if loc.City != "" {
							ulp.City = param.NewOpt(loc.City)
						}
						if loc.Region != "" {
							ulp.Region = param.NewOpt(loc.Region)
						}
						if loc.Country != "" {
							ulp.Country = param.NewOpt(loc.Country)
						}
						if loc.Timezone != "" {
							ulp.Timezone = param.NewOpt(loc.Timezone)
						}
						webSearchTool.UserLocation = ulp
					}
				}
				raw, err := json.Marshal(anthropic.ToolUnionParam{
					OfWebSearchTool20250305: &webSearchTool,
				})
				if err != nil {
					warnings = append(warnings, fantasy.CallWarning{
						Type:    fantasy.CallWarningTypeOther,
						Tool:    tool,
						Message: fmt.Sprintf("failed to marshal web search tool: %v", err),
					})
					continue
				}
				rawTools = append(rawTools, raw)
				continue
			}
			if IsComputerUseTool(tool) {
				raw, err := computerUseToolJSON(pt)
				if err != nil {
					warnings = append(warnings, fantasy.CallWarning{
						Type:    fantasy.CallWarningTypeOther,
						Tool:    tool,
						Message: fmt.Sprintf("failed to build computer use tool: %v", err),
					})
					continue
				}
				version, ok := getComputerUseVersion(pt)
				if ok {
					flag, err := computerUseBetaFlag(version)
					if err != nil {
						warnings = append(warnings, fantasy.CallWarning{
							Type:    fantasy.CallWarningTypeOther,
							Tool:    tool,
							Message: fmt.Sprintf("unsupported computer use version: %v", err),
						})
						continue
					}
					betaFlags = append(betaFlags, flag)
				}
				rawTools = append(rawTools, raw)
				continue
			}
			warnings = append(warnings, fantasy.CallWarning{
				Type:    fantasy.CallWarningTypeUnsupportedTool,
				Tool:    tool,
				Message: "tool is not supported",
			})
			continue
		}
		warnings = append(warnings, fantasy.CallWarning{
			Type:    fantasy.CallWarningTypeUnsupportedTool,
			Tool:    tool,
			Message: "tool is not supported",
		})
	}

	// NOTE: Bedrock does not support this attribute.
	var disableParallelToolUse param.Opt[bool]
	if !a.options.useBedrock {
		disableParallelToolUse = param.NewOpt(disableParallelToolCalls)
	}

	if toolChoice == nil {
		if disableParallelToolCalls {
			anthropicToolChoice = &anthropic.ToolChoiceUnionParam{
				OfAuto: &anthropic.ToolChoiceAutoParam{
					Type:                   "auto",
					DisableParallelToolUse: disableParallelToolUse,
				},
			}
		}
		return rawTools, anthropicToolChoice, warnings, betaFlags
	}

	switch *toolChoice {
	case fantasy.ToolChoiceAuto:
		anthropicToolChoice = &anthropic.ToolChoiceUnionParam{
			OfAuto: &anthropic.ToolChoiceAutoParam{
				Type:                   "auto",
				DisableParallelToolUse: disableParallelToolUse,
			},
		}
	case fantasy.ToolChoiceRequired:
		anthropicToolChoice = &anthropic.ToolChoiceUnionParam{
			OfAny: &anthropic.ToolChoiceAnyParam{
				Type:                   "any",
				DisableParallelToolUse: disableParallelToolUse,
			},
		}
	case fantasy.ToolChoiceNone:
		none := anthropic.NewToolChoiceNoneParam()
		anthropicToolChoice = &anthropic.ToolChoiceUnionParam{
			OfNone: &none,
		}
	default:
		anthropicToolChoice = &anthropic.ToolChoiceUnionParam{
			OfTool: &anthropic.ToolChoiceToolParam{
				Type:                   "tool",
				Name:                   string(*toolChoice),
				DisableParallelToolUse: disableParallelToolUse,
			},
		}
	}
	return rawTools, anthropicToolChoice, warnings, betaFlags
}

func toPrompt(prompt fantasy.Prompt, sendReasoningData bool) ([]anthropic.TextBlockParam, []anthropic.MessageParam, []fantasy.CallWarning) {
	var systemBlocks []anthropic.TextBlockParam
	var messages []anthropic.MessageParam
	var warnings []fantasy.CallWarning

	blocks := groupIntoBlocks(prompt)
	finishedSystemBlock := false
	for _, block := range blocks {
		switch block.Role {
		case fantasy.MessageRoleSystem:
			if finishedSystemBlock {
				// skip multiple system messages that are separated by user/assistant messages
				// TODO: see if we need to send error here?
				continue
			}
			finishedSystemBlock = true
			for _, msg := range block.Messages {
				for i, part := range msg.Content {
					isLastPart := i == len(msg.Content)-1
					cacheControl := GetCacheControl(part.Options())
					if cacheControl == nil && isLastPart {
						cacheControl = GetCacheControl(msg.ProviderOptions)
					}
					text, ok := fantasy.AsMessagePart[fantasy.TextPart](part)
					if !ok {
						continue
					}
					textBlock := anthropic.TextBlockParam{
						Text: text.Text,
					}
					if cacheControl != nil {
						textBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
					}
					systemBlocks = append(systemBlocks, textBlock)
				}
			}

		case fantasy.MessageRoleUser:
			var anthropicContent []anthropic.ContentBlockParamUnion
			for _, msg := range block.Messages {
				if msg.Role == fantasy.MessageRoleUser {
					for i, part := range msg.Content {
						isLastPart := i == len(msg.Content)-1
						cacheControl := GetCacheControl(part.Options())
						if cacheControl == nil && isLastPart {
							cacheControl = GetCacheControl(msg.ProviderOptions)
						}
						switch part.GetType() {
						case fantasy.ContentTypeText:
							text, ok := fantasy.AsMessagePart[fantasy.TextPart](part)
							if !ok {
								continue
							}
							textBlock := &anthropic.TextBlockParam{
								Text: text.Text,
							}
							if cacheControl != nil {
								textBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
							}
							anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
								OfText: textBlock,
							})
						case fantasy.ContentTypeSource:
							// Source content from web search results is not a
							// recognized Anthropic content block type; skip it.
							continue
						case fantasy.ContentTypeFile:
							file, ok := fantasy.AsMessagePart[fantasy.FilePart](part)
							if !ok {
								continue
							}
							switch {
							case strings.HasPrefix(file.MediaType, "image/"):
								base64Encoded := base64.StdEncoding.EncodeToString(file.Data)
								imageBlock := anthropic.NewImageBlockBase64(file.MediaType, base64Encoded)
								if cacheControl != nil {
									imageBlock.OfImage.CacheControl = anthropic.NewCacheControlEphemeralParam()
								}
								anthropicContent = append(anthropicContent, imageBlock)
							case file.MediaType == "application/pdf":
								base64Encoded := base64.StdEncoding.EncodeToString(file.Data)
								docBlock := anthropic.NewDocumentBlock(anthropic.Base64PDFSourceParam{
									Data: base64Encoded,
								})
								docBlock.OfDocument.Title = anthropic.String(sanitizeAnthropicDocumentTitle(file.Filename))
								if cacheControl != nil {
									docBlock.OfDocument.CacheControl = anthropic.NewCacheControlEphemeralParam()
								}
								anthropicContent = append(anthropicContent, docBlock)
							case strings.HasPrefix(file.MediaType, "text/"):
								documentBlock := anthropic.NewDocumentBlock(anthropic.PlainTextSourceParam{
									Data: string(file.Data),
								})
								documentBlock.OfDocument.Title = anthropic.String(sanitizeAnthropicDocumentTitle(file.Filename))
								if cacheControl != nil {
									documentBlock.OfDocument.CacheControl = anthropic.NewCacheControlEphemeralParam()
								}
								anthropicContent = append(anthropicContent, documentBlock)
							default:
								warnings = append(warnings, fantasy.CallWarning{
									Type:    fantasy.CallWarningTypeOther,
									Message: fmt.Sprintf("file part media type %s not supported", file.MediaType),
								})
							}
						}
					}
				} else if msg.Role == fantasy.MessageRoleTool {
					for i, part := range msg.Content {
						isLastPart := i == len(msg.Content)-1
						cacheControl := GetCacheControl(part.Options())
						if cacheControl == nil && isLastPart {
							cacheControl = GetCacheControl(msg.ProviderOptions)
						}
						result, ok := fantasy.AsMessagePart[fantasy.ToolResultPart](part)
						if !ok {
							continue
						}
						toolResultBlock := anthropic.ToolResultBlockParam{
							ToolUseID: result.ToolCallID,
						}
						switch result.Output.GetType() {
						case fantasy.ToolResultContentTypeText:
							content, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentText](result.Output)
							if !ok {
								continue
							}
							toolResultBlock.Content = []anthropic.ToolResultBlockParamContentUnion{
								{
									OfText: &anthropic.TextBlockParam{
										Text: content.Text,
									},
								},
							}
						case fantasy.ToolResultContentTypeMedia:
							content, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentMedia](result.Output)
							if !ok {
								continue
							}
							contentBlocks := []anthropic.ToolResultBlockParamContentUnion{
								{
									OfImage: anthropic.NewImageBlockBase64(content.MediaType, content.Data).OfImage,
								},
							}
							if content.Text != "" {
								contentBlocks = append(contentBlocks, anthropic.ToolResultBlockParamContentUnion{
									OfText: &anthropic.TextBlockParam{
										Text: content.Text,
									},
								})
							}
							toolResultBlock.Content = contentBlocks
						case fantasy.ToolResultContentTypeError:
							content, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentError](result.Output)
							if !ok {
								continue
							}
							toolResultBlock.Content = []anthropic.ToolResultBlockParamContentUnion{
								{
									OfText: &anthropic.TextBlockParam{
										Text: content.Error.Error(),
									},
								},
							}
							toolResultBlock.IsError = param.NewOpt(true)
						}
						if cacheControl != nil {
							toolResultBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
						}
						anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
							OfToolResult: &toolResultBlock,
						})
					}
				}
			}
			if !hasVisibleUserContent(anthropicContent) {
				warnings = append(warnings, fantasy.CallWarning{
					Type:    fantasy.CallWarningTypeOther,
					Message: "dropping empty user message (contains neither user-facing content nor tool results)",
				})
				continue
			}
			messages = append(messages, anthropic.NewUserMessage(anthropicContent...))
		case fantasy.MessageRoleAssistant:
			var anthropicContent []anthropic.ContentBlockParamUnion
			for _, msg := range block.Messages {
				for i, part := range msg.Content {
					isLastPart := i == len(msg.Content)-1
					cacheControl := GetCacheControl(part.Options())
					if cacheControl == nil && isLastPart {
						cacheControl = GetCacheControl(msg.ProviderOptions)
					}
					switch part.GetType() {
					case fantasy.ContentTypeText:
						text, ok := fantasy.AsMessagePart[fantasy.TextPart](part)
						if !ok {
							continue
						}
						textBlock := &anthropic.TextBlockParam{
							Text: text.Text,
						}
						if cacheControl != nil {
							textBlock.CacheControl = anthropic.NewCacheControlEphemeralParam()
						}
						anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
							OfText: textBlock,
						})
					case fantasy.ContentTypeReasoning:
						reasoning, ok := fantasy.AsMessagePart[fantasy.ReasoningPart](part)
						if !ok {
							continue
						}
						if !sendReasoningData {
							warnings = append(warnings, fantasy.CallWarning{
								Type:    fantasy.CallWarningTypeOther,
								Message: "sending reasoning content is disabled for this model",
							})
							continue
						}
						reasoningMetadata := GetReasoningMetadata(part.Options())
						if reasoningMetadata == nil {
							warnings = append(warnings, fantasy.CallWarning{
								Type:    fantasy.CallWarningTypeOther,
								Message: "unsupported reasoning metadata",
							})
							continue
						}

						if reasoningMetadata.Signature != "" {
							anthropicContent = append(anthropicContent, anthropic.NewThinkingBlock(reasoningMetadata.Signature, reasoning.Text))
						} else if reasoningMetadata.RedactedData != "" {
							anthropicContent = append(anthropicContent, anthropic.NewRedactedThinkingBlock(reasoningMetadata.RedactedData))
						} else {
							warnings = append(warnings, fantasy.CallWarning{
								Type:    fantasy.CallWarningTypeOther,
								Message: "unsupported reasoning metadata",
							})
							continue
						}
					case fantasy.ContentTypeToolCall:
						toolCall, ok := fantasy.AsMessagePart[fantasy.ToolCallPart](part)
						if !ok {
							continue
						}
						if toolCall.ProviderExecuted {
							// Reconstruct server_tool_use block for
							// multi-turn round-tripping.
							inputAny, warning := decodeToolCallInputAny(toolCall)
							if warning != nil {
								warnings = append(warnings, *warning)
							}
							anthropicContent = append(anthropicContent, anthropic.ContentBlockParamUnion{
								OfServerToolUse: &anthropic.ServerToolUseBlockParam{
									ID:    toolCall.ToolCallID,
									Name:  anthropic.ServerToolUseBlockParamName(toolCall.ToolName),
									Input: inputAny,
								},
							})
							continue
						}
						inputMap, warning := decodeToolCallInputMap(toolCall)
						if warning != nil {
							warnings = append(warnings, *warning)
						}
						toolUseBlock := anthropic.NewToolUseBlock(toolCall.ToolCallID, inputMap, toolCall.ToolName)
						if cacheControl != nil {
							toolUseBlock.OfToolUse.CacheControl = anthropic.NewCacheControlEphemeralParam()
						}
						anthropicContent = append(anthropicContent, toolUseBlock)
					case fantasy.ContentTypeToolResult:
						result, ok := fantasy.AsMessagePart[fantasy.ToolResultPart](part)
						if !ok {
							continue
						}
						if result.ProviderExecuted {
							// Reconstruct web_search_tool_result blocks,
							// including encrypted content and errors, for
							// round-tripping.
							searchMeta := &WebSearchResultMetadata{}
							if webMeta, ok := result.ProviderOptions[Name]; ok {
								if typed, ok := webMeta.(*WebSearchResultMetadata); ok {
									searchMeta = typed
								}
							}
							anthropicContent = append(anthropicContent, buildWebSearchToolResultBlock(result.ToolCallID, searchMeta))
							continue
						}
					case fantasy.ContentTypeSource: // Source content from web search results is not a
						// recognized Anthropic content block type; skip it.
						continue
					}
				}
			}
			if !hasVisibleAssistantContent(anthropicContent) {
				warnings = append(warnings, fantasy.CallWarning{
					Type:    fantasy.CallWarningTypeOther,
					Message: "dropping empty assistant message (contains neither user-facing content nor tool calls)",
				})
				continue
			}
			messages = append(messages, anthropic.NewAssistantMessage(anthropicContent...))
		}
	}
	return systemBlocks, messages, warnings
}

func hasVisibleUserContent(content []anthropic.ContentBlockParamUnion) bool {
	for _, block := range content {
		if block.OfText != nil || block.OfImage != nil || block.OfDocument != nil || block.OfToolResult != nil {
			return true
		}
	}
	return false
}

func hasVisibleAssistantContent(content []anthropic.ContentBlockParamUnion) bool {
	for _, block := range content {
		if block.OfText != nil || block.OfToolUse != nil || block.OfServerToolUse != nil || block.OfWebSearchToolResult != nil {
			return true
		}
	}
	return false
}

// decodeToolCallInputMap unmarshals a ToolCallPart.Input into a map for
// reconstructing an Anthropic tool_use block. The Anthropic API rejects any
// request whose tool_result lacks a matching tool_use in the previous
// message, so this helper never drops the block: empty input becomes {},
// and malformed input falls back to {} with a CallWarning. The caller still
// emits a tool_use block with the original ToolCallID, preserving the pair.
func decodeToolCallInputMap(toolCall fantasy.ToolCallPart) (map[string]any, *fantasy.CallWarning) {
	if strings.TrimSpace(toolCall.Input) == "" {
		return map[string]any{}, nil
	}
	var inputMap map[string]any
	if err := json.Unmarshal([]byte(toolCall.Input), &inputMap); err != nil {
		return map[string]any{}, &fantasy.CallWarning{
			Type: fantasy.CallWarningTypeOther,
			Message: fmt.Sprintf(
				"tool call %q has malformed input JSON; emitting empty arguments to preserve tool_use ↔ tool_result pairing: %s",
				toolCall.ToolCallID, err,
			),
		}
	}
	if inputMap == nil {
		return map[string]any{}, nil
	}
	return inputMap, nil
}

// decodeToolCallInputAny is the server_tool_use counterpart to
// decodeToolCallInputMap. ServerToolUseBlockParam.Input has type `any` so
// nil is acceptable for the empty case.
func decodeToolCallInputAny(toolCall fantasy.ToolCallPart) (any, *fantasy.CallWarning) {
	if strings.TrimSpace(toolCall.Input) == "" {
		return nil, nil
	}
	var inputAny any
	if err := json.Unmarshal([]byte(toolCall.Input), &inputAny); err != nil {
		return nil, &fantasy.CallWarning{
			Type: fantasy.CallWarningTypeOther,
			Message: fmt.Sprintf(
				"server tool call %q has malformed input JSON; emitting empty arguments to preserve tool_use ↔ tool_result pairing: %s",
				toolCall.ToolCallID, err,
			),
		}
	}
	return inputAny, nil
}

// buildWebSearchToolResultBlock constructs an Anthropic
// web_search_tool_result content block from structured metadata.
func buildWebSearchToolResultBlock(toolCallID string, searchMeta *WebSearchResultMetadata) anthropic.ContentBlockParamUnion {
	var content anthropic.WebSearchToolResultBlockParamContentUnion
	switch {
	case searchMeta != nil && len(searchMeta.Results) > 0:
		resultBlocks := make([]anthropic.WebSearchResultBlockParam, 0, len(searchMeta.Results))
		for _, r := range searchMeta.Results {
			block := anthropic.WebSearchResultBlockParam{
				URL:              r.URL,
				Title:            r.Title,
				EncryptedContent: r.EncryptedContent,
			}
			if r.PageAge != "" {
				block.PageAge = param.NewOpt(r.PageAge)
			}
			resultBlocks = append(resultBlocks, block)
		}
		content = anthropic.WebSearchToolResultBlockParamContentUnion{
			OfWebSearchToolResultBlockItem: resultBlocks,
		}
	case searchMeta != nil && searchMeta.ErrorCode != "":
		content = anthropic.NewWebSearchToolRequestError(
			anthropic.WebSearchToolResultErrorCode(searchMeta.ErrorCode),
		)
	default:
		content = anthropic.WebSearchToolResultBlockParamContentUnion{
			OfWebSearchToolResultBlockItem: []anthropic.WebSearchResultBlockParam{},
		}
	}
	return anthropic.ContentBlockParamUnion{
		OfWebSearchToolResult: &anthropic.WebSearchToolResultBlockParam{
			ToolUseID: toolCallID,
			Content:   content,
		},
	}
}

func mapFinishReason(finishReason string) fantasy.FinishReason {
	switch finishReason {
	case "end_turn", "pause_turn", "stop_sequence":
		return fantasy.FinishReasonStop
	case "max_tokens":
		return fantasy.FinishReasonLength
	case "tool_use":
		return fantasy.FinishReasonToolCalls
	default:
		return fantasy.FinishReasonUnknown
	}
}

// Generate implements fantasy.LanguageModel.
func (a languageModel) Generate(ctx context.Context, call fantasy.Call) (*fantasy.Response, error) {
	params, rawTools, warnings, betaFlags, err := a.prepareParams(call)
	if err != nil {
		return nil, err
	}
	reqOpts := buildRequestOptions(call, rawTools, betaFlags)

	response, err := a.client.Messages.New(ctx, *params, reqOpts...)
	if err != nil {
		return nil, toProviderErr(err)
	}
	if response == nil {
		return nil, &fantasy.Error{Title: "no response", Message: "provider returned nil response"}
	}

	var content []fantasy.Content
	for _, block := range response.Content {
		switch block.Type {
		case "text":
			text, ok := block.AsAny().(anthropic.TextBlock)
			if !ok {
				continue
			}
			content = append(content, fantasy.TextContent{
				Text: text.Text,
			})
		case "thinking":
			reasoning, ok := block.AsAny().(anthropic.ThinkingBlock)
			if !ok {
				continue
			}
			content = append(content, fantasy.ReasoningContent{
				Text: reasoning.Thinking,
				ProviderMetadata: fantasy.ProviderMetadata{
					Name: &ReasoningOptionMetadata{
						Signature: reasoning.Signature,
					},
				},
			})
		case "redacted_thinking":
			reasoning, ok := block.AsAny().(anthropic.RedactedThinkingBlock)
			if !ok {
				continue
			}
			content = append(content, fantasy.ReasoningContent{
				Text: "",
				ProviderMetadata: fantasy.ProviderMetadata{
					Name: &ReasoningOptionMetadata{
						RedactedData: reasoning.Data,
					},
				},
			})
		case "tool_use":
			toolUse, ok := block.AsAny().(anthropic.ToolUseBlock)
			if !ok {
				continue
			}
			content = append(content, fantasy.ToolCallContent{
				ToolCallID:       toolUse.ID,
				ToolName:         toolUse.Name,
				Input:            string(toolUse.Input),
				ProviderExecuted: false,
			})
		case "server_tool_use":
			serverToolUse, ok := block.AsAny().(anthropic.ServerToolUseBlock)
			if !ok {
				continue
			}
			var inputStr string
			if b, err := json.Marshal(serverToolUse.Input); err == nil {
				inputStr = string(b)
			}
			content = append(content, fantasy.ToolCallContent{
				ToolCallID:       serverToolUse.ID,
				ToolName:         string(serverToolUse.Name),
				Input:            inputStr,
				ProviderExecuted: true,
			})
		case "web_search_tool_result":
			webSearchResult, ok := block.AsAny().(anthropic.WebSearchToolResultBlock)
			if !ok {
				continue
			}
			// Extract search results as sources/citations, preserving
			// encrypted_content for multi-turn round-tripping.
			toolResult := fantasy.ToolResultContent{
				ToolCallID:       webSearchResult.ToolUseID,
				ToolName:         "web_search",
				ProviderExecuted: true,
			}
			if items := webSearchResult.Content.OfWebSearchResultBlockArray; len(items) > 0 {
				var metadataResults []WebSearchResultItem
				for _, item := range items {
					content = append(content, fantasy.SourceContent{
						SourceType: fantasy.SourceTypeURL,
						ID:         item.URL,
						URL:        item.URL,
						Title:      item.Title,
					})
					metadataResults = append(metadataResults, WebSearchResultItem{
						URL:              item.URL,
						Title:            item.Title,
						EncryptedContent: item.EncryptedContent,
						PageAge:          item.PageAge,
					})
				}
				toolResult.ProviderMetadata = fantasy.ProviderMetadata{
					Name: &WebSearchResultMetadata{
						Results: metadataResults,
					},
				}
			} else if webSearchResult.Content.ErrorCode != "" {
				toolResult.ProviderMetadata = fantasy.ProviderMetadata{
					Name: &WebSearchResultMetadata{
						ErrorCode: string(webSearchResult.Content.ErrorCode),
					},
				}
			}
			content = append(content, toolResult)
		}
	}

	return &fantasy.Response{
		Content: content,
		Usage: fantasy.Usage{
			InputTokens:         response.Usage.InputTokens,
			OutputTokens:        response.Usage.OutputTokens,
			TotalTokens:         response.Usage.InputTokens + response.Usage.OutputTokens,
			CacheCreationTokens: response.Usage.CacheCreationInputTokens,
			CacheReadTokens:     response.Usage.CacheReadInputTokens,
		},
		FinishReason:     mapFinishReason(string(response.StopReason)),
		ProviderMetadata: fantasy.ProviderMetadata{},
		Warnings:         warnings,
	}, nil
}

// Stream implements fantasy.LanguageModel.
func (a languageModel) Stream(ctx context.Context, call fantasy.Call) (fantasy.StreamResponse, error) {
	params, rawTools, warnings, betaFlags, err := a.prepareParams(call)
	if err != nil {
		return nil, err
	}

	reqOpts := buildRequestOptions(call, rawTools, betaFlags)

	stream := a.client.Messages.NewStreaming(ctx, *params, reqOpts...)
	acc := anthropic.Message{}
	return func(yield func(fantasy.StreamPart) bool) {
		if len(warnings) > 0 {
			if !yield(fantasy.StreamPart{
				Type:     fantasy.StreamPartTypeWarnings,
				Warnings: warnings,
			}) {
				return
			}
		}

		sawMessageStop := false

		for stream.Next() {
			chunk := stream.Current()
			_ = acc.Accumulate(chunk)
			switch chunk.Type {
			case "content_block_start":
				contentBlockType := chunk.ContentBlock.Type
				switch contentBlockType {
				case "text":
					if !yield(fantasy.StreamPart{
						Type: fantasy.StreamPartTypeTextStart,
						ID:   fmt.Sprintf("%d", chunk.Index),
					}) {
						return
					}
				case "thinking":
					if !yield(fantasy.StreamPart{
						Type: fantasy.StreamPartTypeReasoningStart,
						ID:   fmt.Sprintf("%d", chunk.Index),
					}) {
						return
					}
				case "redacted_thinking":
					if !yield(fantasy.StreamPart{
						Type:             fantasy.StreamPartTypeReasoningStart,
						ID:               fmt.Sprintf("%d", chunk.Index),
						ProviderMetadata: reasoningProviderMetadata("", chunk.ContentBlock.Data),
					}) {
						return
					}
				case "tool_use":
					if !yield(fantasy.StreamPart{
						Type:          fantasy.StreamPartTypeToolInputStart,
						ID:            chunk.ContentBlock.ID,
						ToolCallName:  chunk.ContentBlock.Name,
						ToolCallInput: "",
					}) {
						return
					}
				case "server_tool_use":
					if !yield(fantasy.StreamPart{
						Type:             fantasy.StreamPartTypeToolInputStart,
						ID:               chunk.ContentBlock.ID,
						ToolCallName:     chunk.ContentBlock.Name,
						ToolCallInput:    "",
						ProviderExecuted: true,
					}) {
						return
					}
				}
			case "content_block_stop":
				if len(acc.Content)-1 < int(chunk.Index) {
					continue
				}
				contentBlock := acc.Content[int(chunk.Index)]
				switch contentBlock.Type {
				case "text":
					if !yield(fantasy.StreamPart{
						Type: fantasy.StreamPartTypeTextEnd,
						ID:   fmt.Sprintf("%d", chunk.Index),
					}) {
						return
					}
				case "thinking":
					if !yield(fantasy.StreamPart{
						Type:             fantasy.StreamPartTypeReasoningEnd,
						ID:               fmt.Sprintf("%d", chunk.Index),
						ProviderMetadata: reasoningProviderMetadata(contentBlock.Signature, ""),
					}) {
						return
					}
				case "redacted_thinking":
					if !yield(fantasy.StreamPart{
						Type:             fantasy.StreamPartTypeReasoningEnd,
						ID:               fmt.Sprintf("%d", chunk.Index),
						ProviderMetadata: reasoningProviderMetadata("", contentBlock.Data),
					}) {
						return
					}
				case "tool_use":
					if !yield(fantasy.StreamPart{
						Type: fantasy.StreamPartTypeToolInputEnd,
						ID:   contentBlock.ID,
					}) {
						return
					}
					if !yield(fantasy.StreamPart{
						Type:          fantasy.StreamPartTypeToolCall,
						ID:            contentBlock.ID,
						ToolCallName:  contentBlock.Name,
						ToolCallInput: string(contentBlock.Input),
					}) {
						return
					}
				case "server_tool_use":
					if !yield(fantasy.StreamPart{
						Type:             fantasy.StreamPartTypeToolInputEnd,
						ID:               contentBlock.ID,
						ProviderExecuted: true,
					}) {
						return
					}
					if !yield(fantasy.StreamPart{
						Type:             fantasy.StreamPartTypeToolCall,
						ID:               contentBlock.ID,
						ToolCallName:     contentBlock.Name,
						ToolCallInput:    string(contentBlock.Input),
						ProviderExecuted: true,
					}) {
						return
					}
				case "web_search_tool_result":
					// Read search results directly from the ContentBlockUnion
					// struct fields instead of using AsAny(). The Anthropic SDK's
					// Accumulate re-marshals the content block at content_block_stop,
					// which corrupts JSON.raw for inline union types like
					// WebSearchToolResultBlockContentUnion. The struct fields
					// themselves remain correctly populated from content_block_start.
					var metadataResults []WebSearchResultItem
					var providerMeta fantasy.ProviderMetadata
					if items := contentBlock.Content.OfWebSearchResultBlockArray; len(items) > 0 {
						for _, item := range items {
							if !yield(fantasy.StreamPart{
								Type:       fantasy.StreamPartTypeSource,
								ID:         item.URL,
								SourceType: fantasy.SourceTypeURL,
								URL:        item.URL,
								Title:      item.Title,
							}) {
								return
							}
							metadataResults = append(metadataResults, WebSearchResultItem{
								URL:              item.URL,
								Title:            item.Title,
								EncryptedContent: item.EncryptedContent,
								PageAge:          item.PageAge,
							})
						}
					}
					if len(metadataResults) > 0 {
						providerMeta = fantasy.ProviderMetadata{
							Name: &WebSearchResultMetadata{
								Results: metadataResults,
							},
						}
					} else if contentBlock.Content.ErrorCode != "" {
						providerMeta = fantasy.ProviderMetadata{
							Name: &WebSearchResultMetadata{
								ErrorCode: string(contentBlock.Content.ErrorCode),
							},
						}
					}
					if !yield(fantasy.StreamPart{
						Type:             fantasy.StreamPartTypeToolResult,
						ID:               contentBlock.ToolUseID,
						ToolCallName:     "web_search",
						ProviderExecuted: true,
						ProviderMetadata: providerMeta,
					}) {
						return
					}
				}
			case "content_block_delta":
				switch chunk.Delta.Type {
				case "text_delta":
					if !yield(fantasy.StreamPart{
						Type:  fantasy.StreamPartTypeTextDelta,
						ID:    fmt.Sprintf("%d", chunk.Index),
						Delta: chunk.Delta.Text,
					}) {
						return
					}
				case "thinking_delta":
					if !yield(fantasy.StreamPart{
						Type:  fantasy.StreamPartTypeReasoningDelta,
						ID:    fmt.Sprintf("%d", chunk.Index),
						Delta: chunk.Delta.Thinking,
					}) {
						return
					}
				case "signature_delta":
					if !yield(fantasy.StreamPart{
						Type: fantasy.StreamPartTypeReasoningDelta,
						ID:   fmt.Sprintf("%d", chunk.Index),
						ProviderMetadata: fantasy.ProviderMetadata{
							Name: &ReasoningOptionMetadata{
								Signature: chunk.Delta.Signature,
							},
						},
					}) {
						return
					}
				case "input_json_delta":
					if len(acc.Content)-1 < int(chunk.Index) {
						continue
					}
					contentBlock := acc.Content[int(chunk.Index)]
					if !yield(fantasy.StreamPart{
						Type:          fantasy.StreamPartTypeToolInputDelta,
						ID:            contentBlock.ID,
						ToolCallInput: chunk.Delta.PartialJSON,
					}) {
						return
					}
				}
			case "message_stop":
				sawMessageStop = true
			}
		}

		err := stream.Err()
		if err != nil && !errors.Is(err, io.EOF) {
			yield(fantasy.StreamPart{
				Type:  fantasy.StreamPartTypeError,
				Error: toProviderErr(err),
			})
			return
		}

		// Anthropic's SSE protocol reports the stop_reason in message_delta
		// and then terminates the message with message_stop. Require both so
		// a socket close after only one of those signals is retried.
		if !sawMessageStop || acc.StopReason == "" {
			err := ctx.Err()
			if err == nil {
				err = fantasy.NewIncompleteStreamError()
			}
			yield(fantasy.StreamPart{
				Type:  fantasy.StreamPartTypeError,
				Error: err,
			})
			return
		}

		yield(fantasy.StreamPart{
			Type:         fantasy.StreamPartTypeFinish,
			ID:           acc.ID,
			FinishReason: mapFinishReason(string(acc.StopReason)),
			Usage: fantasy.Usage{
				InputTokens:         acc.Usage.InputTokens,
				OutputTokens:        acc.Usage.OutputTokens,
				TotalTokens:         acc.Usage.InputTokens + acc.Usage.OutputTokens,
				CacheCreationTokens: acc.Usage.CacheCreationInputTokens,
				CacheReadTokens:     acc.Usage.CacheReadInputTokens,
			},
			ProviderMetadata: fantasy.ProviderMetadata{},
		})
	}, 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)
	}
}
