responses_language_model.go

   1package openai
   2
   3import (
   4	"context"
   5	"encoding/base64"
   6	"encoding/json"
   7	"fmt"
   8	"reflect"
   9	"strings"
  10
  11	"charm.land/fantasy"
  12	"charm.land/fantasy/object"
  13	"charm.land/fantasy/schema"
  14	"github.com/google/uuid"
  15	"github.com/openai/openai-go/v2"
  16	"github.com/openai/openai-go/v2/packages/param"
  17	"github.com/openai/openai-go/v2/responses"
  18	"github.com/openai/openai-go/v2/shared"
  19)
  20
  21const topLogprobsMax = 20
  22
  23type responsesLanguageModel struct {
  24	provider   string
  25	modelID    string
  26	client     openai.Client
  27	objectMode fantasy.ObjectMode
  28}
  29
  30// newResponsesLanguageModel implements a responses api model
  31// INFO: (kujtim) currently we do not support stored parameter we default it to false.
  32func newResponsesLanguageModel(modelID string, provider string, client openai.Client, objectMode fantasy.ObjectMode) responsesLanguageModel {
  33	return responsesLanguageModel{
  34		modelID:    modelID,
  35		provider:   provider,
  36		client:     client,
  37		objectMode: objectMode,
  38	}
  39}
  40
  41func (o responsesLanguageModel) Model() string {
  42	return o.modelID
  43}
  44
  45func (o responsesLanguageModel) Provider() string {
  46	return o.provider
  47}
  48
  49type responsesModelConfig struct {
  50	isReasoningModel           bool
  51	systemMessageMode          string
  52	requiredAutoTruncation     bool
  53	supportsFlexProcessing     bool
  54	supportsPriorityProcessing bool
  55}
  56
  57func getResponsesModelConfig(modelID string) responsesModelConfig {
  58	supportsFlexProcessing := strings.HasPrefix(modelID, "o3") ||
  59		strings.Contains(modelID, "-o3") || strings.Contains(modelID, "o4-mini") ||
  60		(strings.Contains(modelID, "gpt-5") && !strings.Contains(modelID, "gpt-5-chat"))
  61
  62	supportsPriorityProcessing := strings.Contains(modelID, "gpt-4") ||
  63		strings.Contains(modelID, "gpt-5-mini") ||
  64		(strings.Contains(modelID, "gpt-5") &&
  65			!strings.Contains(modelID, "gpt-5-nano") &&
  66			!strings.Contains(modelID, "gpt-5-chat")) ||
  67		strings.HasPrefix(modelID, "o3") ||
  68		strings.Contains(modelID, "-o3") ||
  69		strings.Contains(modelID, "o4-mini")
  70
  71	defaults := responsesModelConfig{
  72		requiredAutoTruncation:     false,
  73		systemMessageMode:          "system",
  74		supportsFlexProcessing:     supportsFlexProcessing,
  75		supportsPriorityProcessing: supportsPriorityProcessing,
  76	}
  77
  78	if strings.Contains(modelID, "gpt-5-chat") {
  79		return responsesModelConfig{
  80			isReasoningModel:           false,
  81			systemMessageMode:          defaults.systemMessageMode,
  82			requiredAutoTruncation:     defaults.requiredAutoTruncation,
  83			supportsFlexProcessing:     defaults.supportsFlexProcessing,
  84			supportsPriorityProcessing: defaults.supportsPriorityProcessing,
  85		}
  86	}
  87
  88	if strings.HasPrefix(modelID, "o1") || strings.Contains(modelID, "-o1") ||
  89		strings.HasPrefix(modelID, "o3") || strings.Contains(modelID, "-o3") ||
  90		strings.HasPrefix(modelID, "o4") || strings.Contains(modelID, "-o4") ||
  91		strings.HasPrefix(modelID, "oss") || strings.Contains(modelID, "-oss") ||
  92		strings.Contains(modelID, "gpt-5") || strings.Contains(modelID, "codex-") ||
  93		strings.Contains(modelID, "computer-use") {
  94		if strings.Contains(modelID, "o1-mini") || strings.Contains(modelID, "o1-preview") {
  95			return responsesModelConfig{
  96				isReasoningModel:           true,
  97				systemMessageMode:          "remove",
  98				requiredAutoTruncation:     defaults.requiredAutoTruncation,
  99				supportsFlexProcessing:     defaults.supportsFlexProcessing,
 100				supportsPriorityProcessing: defaults.supportsPriorityProcessing,
 101			}
 102		}
 103
 104		return responsesModelConfig{
 105			isReasoningModel:           true,
 106			systemMessageMode:          "developer",
 107			requiredAutoTruncation:     defaults.requiredAutoTruncation,
 108			supportsFlexProcessing:     defaults.supportsFlexProcessing,
 109			supportsPriorityProcessing: defaults.supportsPriorityProcessing,
 110		}
 111	}
 112
 113	return responsesModelConfig{
 114		isReasoningModel:           false,
 115		systemMessageMode:          defaults.systemMessageMode,
 116		requiredAutoTruncation:     defaults.requiredAutoTruncation,
 117		supportsFlexProcessing:     defaults.supportsFlexProcessing,
 118		supportsPriorityProcessing: defaults.supportsPriorityProcessing,
 119	}
 120}
 121
 122func (o responsesLanguageModel) prepareParams(call fantasy.Call) (*responses.ResponseNewParams, []fantasy.CallWarning) {
 123	var warnings []fantasy.CallWarning
 124	params := &responses.ResponseNewParams{
 125		Store: param.NewOpt(false),
 126	}
 127
 128	modelConfig := getResponsesModelConfig(o.modelID)
 129
 130	if call.TopK != nil {
 131		warnings = append(warnings, fantasy.CallWarning{
 132			Type:    fantasy.CallWarningTypeUnsupportedSetting,
 133			Setting: "topK",
 134		})
 135	}
 136
 137	if call.PresencePenalty != nil {
 138		warnings = append(warnings, fantasy.CallWarning{
 139			Type:    fantasy.CallWarningTypeUnsupportedSetting,
 140			Setting: "presencePenalty",
 141		})
 142	}
 143
 144	if call.FrequencyPenalty != nil {
 145		warnings = append(warnings, fantasy.CallWarning{
 146			Type:    fantasy.CallWarningTypeUnsupportedSetting,
 147			Setting: "frequencyPenalty",
 148		})
 149	}
 150
 151	var openaiOptions *ResponsesProviderOptions
 152	if opts, ok := call.ProviderOptions[Name]; ok {
 153		if typedOpts, ok := opts.(*ResponsesProviderOptions); ok {
 154			openaiOptions = typedOpts
 155		}
 156	}
 157
 158	input, inputWarnings := toResponsesPrompt(call.Prompt, modelConfig.systemMessageMode)
 159	warnings = append(warnings, inputWarnings...)
 160
 161	var include []IncludeType
 162
 163	addInclude := func(key IncludeType) {
 164		include = append(include, key)
 165	}
 166
 167	topLogprobs := 0
 168	if openaiOptions != nil && openaiOptions.Logprobs != nil {
 169		switch v := openaiOptions.Logprobs.(type) {
 170		case bool:
 171			if v {
 172				topLogprobs = topLogprobsMax
 173			}
 174		case float64:
 175			topLogprobs = int(v)
 176		case int:
 177			topLogprobs = v
 178		}
 179	}
 180
 181	if topLogprobs > 0 {
 182		addInclude(IncludeMessageOutputTextLogprobs)
 183	}
 184
 185	params.Model = o.modelID
 186	params.Input = responses.ResponseNewParamsInputUnion{
 187		OfInputItemList: input,
 188	}
 189
 190	if call.Temperature != nil {
 191		params.Temperature = param.NewOpt(*call.Temperature)
 192	}
 193	if call.TopP != nil {
 194		params.TopP = param.NewOpt(*call.TopP)
 195	}
 196	if call.MaxOutputTokens != nil {
 197		params.MaxOutputTokens = param.NewOpt(*call.MaxOutputTokens)
 198	}
 199
 200	if openaiOptions != nil {
 201		if openaiOptions.MaxToolCalls != nil {
 202			params.MaxToolCalls = param.NewOpt(*openaiOptions.MaxToolCalls)
 203		}
 204		if openaiOptions.Metadata != nil {
 205			metadata := make(shared.Metadata)
 206			for k, v := range openaiOptions.Metadata {
 207				if str, ok := v.(string); ok {
 208					metadata[k] = str
 209				}
 210			}
 211			params.Metadata = metadata
 212		}
 213		if openaiOptions.ParallelToolCalls != nil {
 214			params.ParallelToolCalls = param.NewOpt(*openaiOptions.ParallelToolCalls)
 215		}
 216		if openaiOptions.User != nil {
 217			params.User = param.NewOpt(*openaiOptions.User)
 218		}
 219		if openaiOptions.Instructions != nil {
 220			params.Instructions = param.NewOpt(*openaiOptions.Instructions)
 221		}
 222		if openaiOptions.ServiceTier != nil {
 223			params.ServiceTier = responses.ResponseNewParamsServiceTier(*openaiOptions.ServiceTier)
 224		}
 225		if openaiOptions.PromptCacheKey != nil {
 226			params.PromptCacheKey = param.NewOpt(*openaiOptions.PromptCacheKey)
 227		}
 228		if openaiOptions.SafetyIdentifier != nil {
 229			params.SafetyIdentifier = param.NewOpt(*openaiOptions.SafetyIdentifier)
 230		}
 231		if topLogprobs > 0 {
 232			params.TopLogprobs = param.NewOpt(int64(topLogprobs))
 233		}
 234
 235		if len(openaiOptions.Include) > 0 {
 236			include = append(include, openaiOptions.Include...)
 237		}
 238
 239		if modelConfig.isReasoningModel && (openaiOptions.ReasoningEffort != nil || openaiOptions.ReasoningSummary != nil) {
 240			reasoning := shared.ReasoningParam{}
 241			if openaiOptions.ReasoningEffort != nil {
 242				reasoning.Effort = shared.ReasoningEffort(*openaiOptions.ReasoningEffort)
 243			}
 244			if openaiOptions.ReasoningSummary != nil {
 245				reasoning.Summary = shared.ReasoningSummary(*openaiOptions.ReasoningSummary)
 246			}
 247			params.Reasoning = reasoning
 248		}
 249	}
 250
 251	if modelConfig.requiredAutoTruncation {
 252		params.Truncation = responses.ResponseNewParamsTruncationAuto
 253	}
 254
 255	if len(include) > 0 {
 256		includeParams := make([]responses.ResponseIncludable, len(include))
 257		for i, inc := range include {
 258			includeParams[i] = responses.ResponseIncludable(string(inc))
 259		}
 260		params.Include = includeParams
 261	}
 262
 263	if modelConfig.isReasoningModel {
 264		if call.Temperature != nil {
 265			params.Temperature = param.Opt[float64]{}
 266			warnings = append(warnings, fantasy.CallWarning{
 267				Type:    fantasy.CallWarningTypeUnsupportedSetting,
 268				Setting: "temperature",
 269				Details: "temperature is not supported for reasoning models",
 270			})
 271		}
 272
 273		if call.TopP != nil {
 274			params.TopP = param.Opt[float64]{}
 275			warnings = append(warnings, fantasy.CallWarning{
 276				Type:    fantasy.CallWarningTypeUnsupportedSetting,
 277				Setting: "topP",
 278				Details: "topP is not supported for reasoning models",
 279			})
 280		}
 281	} else {
 282		if openaiOptions != nil {
 283			if openaiOptions.ReasoningEffort != nil {
 284				warnings = append(warnings, fantasy.CallWarning{
 285					Type:    fantasy.CallWarningTypeUnsupportedSetting,
 286					Setting: "reasoningEffort",
 287					Details: "reasoningEffort is not supported for non-reasoning models",
 288				})
 289			}
 290
 291			if openaiOptions.ReasoningSummary != nil {
 292				warnings = append(warnings, fantasy.CallWarning{
 293					Type:    fantasy.CallWarningTypeUnsupportedSetting,
 294					Setting: "reasoningSummary",
 295					Details: "reasoningSummary is not supported for non-reasoning models",
 296				})
 297			}
 298		}
 299	}
 300
 301	if openaiOptions != nil && openaiOptions.ServiceTier != nil {
 302		if *openaiOptions.ServiceTier == ServiceTierFlex && !modelConfig.supportsFlexProcessing {
 303			warnings = append(warnings, fantasy.CallWarning{
 304				Type:    fantasy.CallWarningTypeUnsupportedSetting,
 305				Setting: "serviceTier",
 306				Details: "flex processing is only available for o3, o4-mini, and gpt-5 models",
 307			})
 308			params.ServiceTier = ""
 309		}
 310
 311		if *openaiOptions.ServiceTier == ServiceTierPriority && !modelConfig.supportsPriorityProcessing {
 312			warnings = append(warnings, fantasy.CallWarning{
 313				Type:    fantasy.CallWarningTypeUnsupportedSetting,
 314				Setting: "serviceTier",
 315				Details: "priority processing is only available for supported models (gpt-4, gpt-5, gpt-5-mini, o3, o4-mini) and requires Enterprise access. gpt-5-nano is not supported",
 316			})
 317			params.ServiceTier = ""
 318		}
 319	}
 320
 321	tools, toolChoice, toolWarnings := toResponsesTools(call.Tools, call.ToolChoice, openaiOptions)
 322	warnings = append(warnings, toolWarnings...)
 323
 324	if len(tools) > 0 {
 325		params.Tools = tools
 326		params.ToolChoice = toolChoice
 327	}
 328
 329	return params, warnings
 330}
 331
 332func toResponsesPrompt(prompt fantasy.Prompt, systemMessageMode string) (responses.ResponseInputParam, []fantasy.CallWarning) {
 333	var input responses.ResponseInputParam
 334	var warnings []fantasy.CallWarning
 335
 336	for _, msg := range prompt {
 337		switch msg.Role {
 338		case fantasy.MessageRoleSystem:
 339			var systemText string
 340			for _, c := range msg.Content {
 341				if c.GetType() != fantasy.ContentTypeText {
 342					warnings = append(warnings, fantasy.CallWarning{
 343						Type:    fantasy.CallWarningTypeOther,
 344						Message: "system prompt can only have text content",
 345					})
 346					continue
 347				}
 348				textPart, ok := fantasy.AsContentType[fantasy.TextPart](c)
 349				if !ok {
 350					warnings = append(warnings, fantasy.CallWarning{
 351						Type:    fantasy.CallWarningTypeOther,
 352						Message: "system prompt text part does not have the right type",
 353					})
 354					continue
 355				}
 356				if strings.TrimSpace(textPart.Text) != "" {
 357					systemText += textPart.Text
 358				}
 359			}
 360
 361			if systemText == "" {
 362				warnings = append(warnings, fantasy.CallWarning{
 363					Type:    fantasy.CallWarningTypeOther,
 364					Message: "system prompt has no text parts",
 365				})
 366				continue
 367			}
 368
 369			switch systemMessageMode {
 370			case "system":
 371				input = append(input, responses.ResponseInputItemParamOfMessage(systemText, responses.EasyInputMessageRoleSystem))
 372			case "developer":
 373				input = append(input, responses.ResponseInputItemParamOfMessage(systemText, responses.EasyInputMessageRoleDeveloper))
 374			case "remove":
 375				warnings = append(warnings, fantasy.CallWarning{
 376					Type:    fantasy.CallWarningTypeOther,
 377					Message: "system messages are removed for this model",
 378				})
 379			}
 380
 381		case fantasy.MessageRoleUser:
 382			var contentParts responses.ResponseInputMessageContentListParam
 383			for i, c := range msg.Content {
 384				switch c.GetType() {
 385				case fantasy.ContentTypeText:
 386					textPart, ok := fantasy.AsContentType[fantasy.TextPart](c)
 387					if !ok {
 388						warnings = append(warnings, fantasy.CallWarning{
 389							Type:    fantasy.CallWarningTypeOther,
 390							Message: "user message text part does not have the right type",
 391						})
 392						continue
 393					}
 394					contentParts = append(contentParts, responses.ResponseInputContentUnionParam{
 395						OfInputText: &responses.ResponseInputTextParam{
 396							Type: "input_text",
 397							Text: textPart.Text,
 398						},
 399					})
 400
 401				case fantasy.ContentTypeFile:
 402					filePart, ok := fantasy.AsContentType[fantasy.FilePart](c)
 403					if !ok {
 404						warnings = append(warnings, fantasy.CallWarning{
 405							Type:    fantasy.CallWarningTypeOther,
 406							Message: "user message file part does not have the right type",
 407						})
 408						continue
 409					}
 410
 411					if strings.HasPrefix(filePart.MediaType, "image/") {
 412						base64Encoded := base64.StdEncoding.EncodeToString(filePart.Data)
 413						imageURL := fmt.Sprintf("data:%s;base64,%s", filePart.MediaType, base64Encoded)
 414						contentParts = append(contentParts, responses.ResponseInputContentUnionParam{
 415							OfInputImage: &responses.ResponseInputImageParam{
 416								Type:     "input_image",
 417								ImageURL: param.NewOpt(imageURL),
 418							},
 419						})
 420					} else if filePart.MediaType == "application/pdf" {
 421						base64Encoded := base64.StdEncoding.EncodeToString(filePart.Data)
 422						fileData := fmt.Sprintf("data:application/pdf;base64,%s", base64Encoded)
 423						filename := filePart.Filename
 424						if filename == "" {
 425							filename = fmt.Sprintf("part-%d.pdf", i)
 426						}
 427						contentParts = append(contentParts, responses.ResponseInputContentUnionParam{
 428							OfInputFile: &responses.ResponseInputFileParam{
 429								Type:     "input_file",
 430								Filename: param.NewOpt(filename),
 431								FileData: param.NewOpt(fileData),
 432							},
 433						})
 434					} else {
 435						warnings = append(warnings, fantasy.CallWarning{
 436							Type:    fantasy.CallWarningTypeOther,
 437							Message: fmt.Sprintf("file part media type %s not supported", filePart.MediaType),
 438						})
 439					}
 440				}
 441			}
 442
 443			input = append(input, responses.ResponseInputItemParamOfMessage(contentParts, responses.EasyInputMessageRoleUser))
 444
 445		case fantasy.MessageRoleAssistant:
 446			for _, c := range msg.Content {
 447				switch c.GetType() {
 448				case fantasy.ContentTypeText:
 449					textPart, ok := fantasy.AsContentType[fantasy.TextPart](c)
 450					if !ok {
 451						warnings = append(warnings, fantasy.CallWarning{
 452							Type:    fantasy.CallWarningTypeOther,
 453							Message: "assistant message text part does not have the right type",
 454						})
 455						continue
 456					}
 457					input = append(input, responses.ResponseInputItemParamOfMessage(textPart.Text, responses.EasyInputMessageRoleAssistant))
 458
 459				case fantasy.ContentTypeToolCall:
 460					toolCallPart, ok := fantasy.AsContentType[fantasy.ToolCallPart](c)
 461					if !ok {
 462						warnings = append(warnings, fantasy.CallWarning{
 463							Type:    fantasy.CallWarningTypeOther,
 464							Message: "assistant message tool call part does not have the right type",
 465						})
 466						continue
 467					}
 468
 469					if toolCallPart.ProviderExecuted {
 470						continue
 471					}
 472
 473					inputJSON, err := json.Marshal(toolCallPart.Input)
 474					if err != nil {
 475						warnings = append(warnings, fantasy.CallWarning{
 476							Type:    fantasy.CallWarningTypeOther,
 477							Message: fmt.Sprintf("failed to marshal tool call input: %v", err),
 478						})
 479						continue
 480					}
 481
 482					input = append(input, responses.ResponseInputItemParamOfFunctionCall(string(inputJSON), toolCallPart.ToolCallID, toolCallPart.ToolName))
 483				case fantasy.ContentTypeReasoning:
 484					reasoningMetadata := GetReasoningMetadata(c.Options())
 485					if reasoningMetadata == nil || reasoningMetadata.ItemID == "" {
 486						continue
 487					}
 488					if len(reasoningMetadata.Summary) == 0 && reasoningMetadata.EncryptedContent == nil {
 489						warnings = append(warnings, fantasy.CallWarning{
 490							Type:    fantasy.CallWarningTypeOther,
 491							Message: "assistant message reasoning part does is empty",
 492						})
 493						continue
 494					}
 495					// we want to always send an empty array
 496					summary := []responses.ResponseReasoningItemSummaryParam{}
 497					for _, s := range reasoningMetadata.Summary {
 498						summary = append(summary, responses.ResponseReasoningItemSummaryParam{
 499							Type: "summary_text",
 500							Text: s,
 501						})
 502					}
 503					reasoning := &responses.ResponseReasoningItemParam{
 504						ID:      reasoningMetadata.ItemID,
 505						Summary: summary,
 506					}
 507					if reasoningMetadata.EncryptedContent != nil {
 508						reasoning.EncryptedContent = param.NewOpt(*reasoningMetadata.EncryptedContent)
 509					}
 510					input = append(input, responses.ResponseInputItemUnionParam{
 511						OfReasoning: reasoning,
 512					})
 513				}
 514			}
 515
 516		case fantasy.MessageRoleTool:
 517			for _, c := range msg.Content {
 518				if c.GetType() != fantasy.ContentTypeToolResult {
 519					warnings = append(warnings, fantasy.CallWarning{
 520						Type:    fantasy.CallWarningTypeOther,
 521						Message: "tool message can only have tool result content",
 522					})
 523					continue
 524				}
 525
 526				toolResultPart, ok := fantasy.AsContentType[fantasy.ToolResultPart](c)
 527				if !ok {
 528					warnings = append(warnings, fantasy.CallWarning{
 529						Type:    fantasy.CallWarningTypeOther,
 530						Message: "tool message result part does not have the right type",
 531					})
 532					continue
 533				}
 534
 535				var outputStr string
 536				switch toolResultPart.Output.GetType() {
 537				case fantasy.ToolResultContentTypeText:
 538					output, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentText](toolResultPart.Output)
 539					if !ok {
 540						warnings = append(warnings, fantasy.CallWarning{
 541							Type:    fantasy.CallWarningTypeOther,
 542							Message: "tool result output does not have the right type",
 543						})
 544						continue
 545					}
 546					outputStr = output.Text
 547				case fantasy.ToolResultContentTypeError:
 548					output, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentError](toolResultPart.Output)
 549					if !ok {
 550						warnings = append(warnings, fantasy.CallWarning{
 551							Type:    fantasy.CallWarningTypeOther,
 552							Message: "tool result output does not have the right type",
 553						})
 554						continue
 555					}
 556					outputStr = output.Error.Error()
 557				case fantasy.ToolResultContentTypeMedia:
 558					output, ok := fantasy.AsToolResultOutputType[fantasy.ToolResultOutputContentMedia](toolResultPart.Output)
 559					if !ok {
 560						warnings = append(warnings, fantasy.CallWarning{
 561							Type:    fantasy.CallWarningTypeOther,
 562							Message: "tool result output does not have the right type",
 563						})
 564						continue
 565					}
 566					// For media content, encode as JSON with data and media type
 567					mediaContent := map[string]string{
 568						"data":       output.Data,
 569						"media_type": output.MediaType,
 570					}
 571					jsonBytes, err := json.Marshal(mediaContent)
 572					if err != nil {
 573						warnings = append(warnings, fantasy.CallWarning{
 574							Type:    fantasy.CallWarningTypeOther,
 575							Message: fmt.Sprintf("failed to marshal tool result: %v", err),
 576						})
 577						continue
 578					}
 579					outputStr = string(jsonBytes)
 580				}
 581
 582				input = append(input, responses.ResponseInputItemParamOfFunctionCallOutput(toolResultPart.ToolCallID, outputStr))
 583			}
 584		}
 585	}
 586
 587	return input, warnings
 588}
 589
 590func toResponsesTools(tools []fantasy.Tool, toolChoice *fantasy.ToolChoice, options *ResponsesProviderOptions) ([]responses.ToolUnionParam, responses.ResponseNewParamsToolChoiceUnion, []fantasy.CallWarning) {
 591	warnings := make([]fantasy.CallWarning, 0)
 592	var openaiTools []responses.ToolUnionParam
 593
 594	if len(tools) == 0 {
 595		return nil, responses.ResponseNewParamsToolChoiceUnion{}, nil
 596	}
 597
 598	strictJSONSchema := false
 599	if options != nil && options.StrictJSONSchema != nil {
 600		strictJSONSchema = *options.StrictJSONSchema
 601	}
 602
 603	for _, tool := range tools {
 604		if tool.GetType() == fantasy.ToolTypeFunction {
 605			ft, ok := tool.(fantasy.FunctionTool)
 606			if !ok {
 607				continue
 608			}
 609			openaiTools = append(openaiTools, responses.ToolUnionParam{
 610				OfFunction: &responses.FunctionToolParam{
 611					Name:        ft.Name,
 612					Description: param.NewOpt(ft.Description),
 613					Parameters:  ft.InputSchema,
 614					Strict:      param.NewOpt(strictJSONSchema),
 615					Type:        "function",
 616				},
 617			})
 618			continue
 619		}
 620
 621		warnings = append(warnings, fantasy.CallWarning{
 622			Type:    fantasy.CallWarningTypeUnsupportedTool,
 623			Tool:    tool,
 624			Message: "tool is not supported",
 625		})
 626	}
 627
 628	if toolChoice == nil {
 629		return openaiTools, responses.ResponseNewParamsToolChoiceUnion{}, warnings
 630	}
 631
 632	var openaiToolChoice responses.ResponseNewParamsToolChoiceUnion
 633
 634	switch *toolChoice {
 635	case fantasy.ToolChoiceAuto:
 636		openaiToolChoice = responses.ResponseNewParamsToolChoiceUnion{
 637			OfToolChoiceMode: param.NewOpt(responses.ToolChoiceOptionsAuto),
 638		}
 639	case fantasy.ToolChoiceNone:
 640		openaiToolChoice = responses.ResponseNewParamsToolChoiceUnion{
 641			OfToolChoiceMode: param.NewOpt(responses.ToolChoiceOptionsNone),
 642		}
 643	case fantasy.ToolChoiceRequired:
 644		openaiToolChoice = responses.ResponseNewParamsToolChoiceUnion{
 645			OfToolChoiceMode: param.NewOpt(responses.ToolChoiceOptionsRequired),
 646		}
 647	default:
 648		openaiToolChoice = responses.ResponseNewParamsToolChoiceUnion{
 649			OfFunctionTool: &responses.ToolChoiceFunctionParam{
 650				Type: "function",
 651				Name: string(*toolChoice),
 652			},
 653		}
 654	}
 655
 656	return openaiTools, openaiToolChoice, warnings
 657}
 658
 659func (o responsesLanguageModel) Generate(ctx context.Context, call fantasy.Call) (*fantasy.Response, error) {
 660	params, warnings := o.prepareParams(call)
 661	response, err := o.client.Responses.New(ctx, *params)
 662	if err != nil {
 663		return nil, toProviderErr(err)
 664	}
 665
 666	if response.Error.Message != "" {
 667		return nil, &fantasy.Error{
 668			Title:   "provider error",
 669			Message: fmt.Sprintf("%s (code: %s)", response.Error.Message, response.Error.Code),
 670		}
 671	}
 672
 673	var content []fantasy.Content
 674	hasFunctionCall := false
 675
 676	for _, outputItem := range response.Output {
 677		switch outputItem.Type {
 678		case "message":
 679			for _, contentPart := range outputItem.Content {
 680				if contentPart.Type == "output_text" {
 681					content = append(content, fantasy.TextContent{
 682						Text: contentPart.Text,
 683					})
 684
 685					for _, annotation := range contentPart.Annotations {
 686						switch annotation.Type {
 687						case "url_citation":
 688							content = append(content, fantasy.SourceContent{
 689								SourceType: fantasy.SourceTypeURL,
 690								ID:         uuid.NewString(),
 691								URL:        annotation.URL,
 692								Title:      annotation.Title,
 693							})
 694						case "file_citation":
 695							title := "Document"
 696							if annotation.Filename != "" {
 697								title = annotation.Filename
 698							}
 699							filename := annotation.Filename
 700							if filename == "" {
 701								filename = annotation.FileID
 702							}
 703							content = append(content, fantasy.SourceContent{
 704								SourceType: fantasy.SourceTypeDocument,
 705								ID:         uuid.NewString(),
 706								MediaType:  "text/plain",
 707								Title:      title,
 708								Filename:   filename,
 709							})
 710						}
 711					}
 712				}
 713			}
 714
 715		case "function_call":
 716			hasFunctionCall = true
 717			content = append(content, fantasy.ToolCallContent{
 718				ProviderExecuted: false,
 719				ToolCallID:       outputItem.CallID,
 720				ToolName:         outputItem.Name,
 721				Input:            outputItem.Arguments,
 722			})
 723
 724		case "reasoning":
 725			metadata := &ResponsesReasoningMetadata{
 726				ItemID: outputItem.ID,
 727			}
 728			if outputItem.EncryptedContent != "" {
 729				metadata.EncryptedContent = &outputItem.EncryptedContent
 730			}
 731
 732			if len(outputItem.Summary) == 0 && metadata.EncryptedContent == nil {
 733				continue
 734			}
 735
 736			// When there are no summary parts, add an empty reasoning part
 737			summaries := outputItem.Summary
 738			if len(summaries) == 0 {
 739				summaries = []responses.ResponseReasoningItemSummary{{Type: "summary_text", Text: ""}}
 740			}
 741
 742			for _, s := range summaries {
 743				metadata.Summary = append(metadata.Summary, s.Text)
 744			}
 745
 746			content = append(content, fantasy.ReasoningContent{
 747				Text: strings.Join(metadata.Summary, "\n"),
 748				ProviderMetadata: fantasy.ProviderMetadata{
 749					Name: metadata,
 750				},
 751			})
 752		}
 753	}
 754
 755	usage := fantasy.Usage{
 756		InputTokens:  response.Usage.InputTokens,
 757		OutputTokens: response.Usage.OutputTokens,
 758		TotalTokens:  response.Usage.InputTokens + response.Usage.OutputTokens,
 759	}
 760
 761	if response.Usage.OutputTokensDetails.ReasoningTokens != 0 {
 762		usage.ReasoningTokens = response.Usage.OutputTokensDetails.ReasoningTokens
 763	}
 764	if response.Usage.InputTokensDetails.CachedTokens != 0 {
 765		usage.CacheReadTokens = response.Usage.InputTokensDetails.CachedTokens
 766	}
 767
 768	finishReason := mapResponsesFinishReason(response.IncompleteDetails.Reason, hasFunctionCall)
 769
 770	return &fantasy.Response{
 771		Content:          content,
 772		Usage:            usage,
 773		FinishReason:     finishReason,
 774		ProviderMetadata: fantasy.ProviderMetadata{},
 775		Warnings:         warnings,
 776	}, nil
 777}
 778
 779func mapResponsesFinishReason(reason string, hasFunctionCall bool) fantasy.FinishReason {
 780	if hasFunctionCall {
 781		return fantasy.FinishReasonToolCalls
 782	}
 783
 784	switch reason {
 785	case "":
 786		return fantasy.FinishReasonStop
 787	case "max_tokens", "max_output_tokens":
 788		return fantasy.FinishReasonLength
 789	case "content_filter":
 790		return fantasy.FinishReasonContentFilter
 791	default:
 792		return fantasy.FinishReasonOther
 793	}
 794}
 795
 796func (o responsesLanguageModel) Stream(ctx context.Context, call fantasy.Call) (fantasy.StreamResponse, error) {
 797	params, warnings := o.prepareParams(call)
 798
 799	stream := o.client.Responses.NewStreaming(ctx, *params)
 800
 801	finishReason := fantasy.FinishReasonUnknown
 802	var usage fantasy.Usage
 803	ongoingToolCalls := make(map[int64]*ongoingToolCall)
 804	hasFunctionCall := false
 805	activeReasoning := make(map[string]*reasoningState)
 806
 807	return func(yield func(fantasy.StreamPart) bool) {
 808		if len(warnings) > 0 {
 809			if !yield(fantasy.StreamPart{
 810				Type:     fantasy.StreamPartTypeWarnings,
 811				Warnings: warnings,
 812			}) {
 813				return
 814			}
 815		}
 816
 817		for stream.Next() {
 818			event := stream.Current()
 819
 820			switch event.Type {
 821			case "response.created":
 822				_ = event.AsResponseCreated()
 823
 824			case "response.output_item.added":
 825				added := event.AsResponseOutputItemAdded()
 826				switch added.Item.Type {
 827				case "function_call":
 828					ongoingToolCalls[added.OutputIndex] = &ongoingToolCall{
 829						toolName:   added.Item.Name,
 830						toolCallID: added.Item.CallID,
 831					}
 832					if !yield(fantasy.StreamPart{
 833						Type:         fantasy.StreamPartTypeToolInputStart,
 834						ID:           added.Item.CallID,
 835						ToolCallName: added.Item.Name,
 836					}) {
 837						return
 838					}
 839
 840				case "message":
 841					if !yield(fantasy.StreamPart{
 842						Type: fantasy.StreamPartTypeTextStart,
 843						ID:   added.Item.ID,
 844					}) {
 845						return
 846					}
 847
 848				case "reasoning":
 849					metadata := &ResponsesReasoningMetadata{
 850						ItemID:  added.Item.ID,
 851						Summary: []string{},
 852					}
 853					if added.Item.EncryptedContent != "" {
 854						metadata.EncryptedContent = &added.Item.EncryptedContent
 855					}
 856
 857					activeReasoning[added.Item.ID] = &reasoningState{
 858						metadata: metadata,
 859					}
 860					if !yield(fantasy.StreamPart{
 861						Type: fantasy.StreamPartTypeReasoningStart,
 862						ID:   added.Item.ID,
 863						ProviderMetadata: fantasy.ProviderMetadata{
 864							Name: metadata,
 865						},
 866					}) {
 867						return
 868					}
 869				}
 870
 871			case "response.output_item.done":
 872				done := event.AsResponseOutputItemDone()
 873				switch done.Item.Type {
 874				case "function_call":
 875					tc := ongoingToolCalls[done.OutputIndex]
 876					if tc != nil {
 877						delete(ongoingToolCalls, done.OutputIndex)
 878						hasFunctionCall = true
 879
 880						if !yield(fantasy.StreamPart{
 881							Type: fantasy.StreamPartTypeToolInputEnd,
 882							ID:   done.Item.CallID,
 883						}) {
 884							return
 885						}
 886						if !yield(fantasy.StreamPart{
 887							Type:          fantasy.StreamPartTypeToolCall,
 888							ID:            done.Item.CallID,
 889							ToolCallName:  done.Item.Name,
 890							ToolCallInput: done.Item.Arguments,
 891						}) {
 892							return
 893						}
 894					}
 895
 896				case "message":
 897					if !yield(fantasy.StreamPart{
 898						Type: fantasy.StreamPartTypeTextEnd,
 899						ID:   done.Item.ID,
 900					}) {
 901						return
 902					}
 903
 904				case "reasoning":
 905					state := activeReasoning[done.Item.ID]
 906					if state != nil {
 907						if !yield(fantasy.StreamPart{
 908							Type: fantasy.StreamPartTypeReasoningEnd,
 909							ID:   done.Item.ID,
 910							ProviderMetadata: fantasy.ProviderMetadata{
 911								Name: state.metadata,
 912							},
 913						}) {
 914							return
 915						}
 916						delete(activeReasoning, done.Item.ID)
 917					}
 918				}
 919
 920			case "response.function_call_arguments.delta":
 921				delta := event.AsResponseFunctionCallArgumentsDelta()
 922				tc := ongoingToolCalls[delta.OutputIndex]
 923				if tc != nil {
 924					if !yield(fantasy.StreamPart{
 925						Type:  fantasy.StreamPartTypeToolInputDelta,
 926						ID:    tc.toolCallID,
 927						Delta: delta.Delta,
 928					}) {
 929						return
 930					}
 931				}
 932
 933			case "response.output_text.delta":
 934				textDelta := event.AsResponseOutputTextDelta()
 935				if !yield(fantasy.StreamPart{
 936					Type:  fantasy.StreamPartTypeTextDelta,
 937					ID:    textDelta.ItemID,
 938					Delta: textDelta.Delta,
 939				}) {
 940					return
 941				}
 942
 943			case "response.reasoning_summary_part.added":
 944				added := event.AsResponseReasoningSummaryPartAdded()
 945				state := activeReasoning[added.ItemID]
 946				if state != nil {
 947					state.metadata.Summary = append(state.metadata.Summary, "")
 948					activeReasoning[added.ItemID] = state
 949					if !yield(fantasy.StreamPart{
 950						Type:  fantasy.StreamPartTypeReasoningDelta,
 951						ID:    added.ItemID,
 952						Delta: "\n",
 953						ProviderMetadata: fantasy.ProviderMetadata{
 954							Name: state.metadata,
 955						},
 956					}) {
 957						return
 958					}
 959				}
 960
 961			case "response.reasoning_summary_text.delta":
 962				textDelta := event.AsResponseReasoningSummaryTextDelta()
 963				state := activeReasoning[textDelta.ItemID]
 964				if state != nil {
 965					if len(state.metadata.Summary)-1 >= int(textDelta.SummaryIndex) {
 966						state.metadata.Summary[textDelta.SummaryIndex] += textDelta.Delta
 967					}
 968					activeReasoning[textDelta.ItemID] = state
 969					if !yield(fantasy.StreamPart{
 970						Type:  fantasy.StreamPartTypeReasoningDelta,
 971						ID:    textDelta.ItemID,
 972						Delta: textDelta.Delta,
 973						ProviderMetadata: fantasy.ProviderMetadata{
 974							Name: state.metadata,
 975						},
 976					}) {
 977						return
 978					}
 979				}
 980
 981			case "response.completed", "response.incomplete":
 982				completed := event.AsResponseCompleted()
 983				finishReason = mapResponsesFinishReason(completed.Response.IncompleteDetails.Reason, hasFunctionCall)
 984				usage = fantasy.Usage{
 985					InputTokens:  completed.Response.Usage.InputTokens,
 986					OutputTokens: completed.Response.Usage.OutputTokens,
 987					TotalTokens:  completed.Response.Usage.InputTokens + completed.Response.Usage.OutputTokens,
 988				}
 989				if completed.Response.Usage.OutputTokensDetails.ReasoningTokens != 0 {
 990					usage.ReasoningTokens = completed.Response.Usage.OutputTokensDetails.ReasoningTokens
 991				}
 992				if completed.Response.Usage.InputTokensDetails.CachedTokens != 0 {
 993					usage.CacheReadTokens = completed.Response.Usage.InputTokensDetails.CachedTokens
 994				}
 995
 996			case "error":
 997				errorEvent := event.AsError()
 998				if !yield(fantasy.StreamPart{
 999					Type:  fantasy.StreamPartTypeError,
1000					Error: fmt.Errorf("response error: %s (code: %s)", errorEvent.Message, errorEvent.Code),
1001				}) {
1002					return
1003				}
1004				return
1005			}
1006		}
1007
1008		err := stream.Err()
1009		if err != nil {
1010			yield(fantasy.StreamPart{
1011				Type:  fantasy.StreamPartTypeError,
1012				Error: toProviderErr(err),
1013			})
1014			return
1015		}
1016
1017		yield(fantasy.StreamPart{
1018			Type:         fantasy.StreamPartTypeFinish,
1019			Usage:        usage,
1020			FinishReason: finishReason,
1021		})
1022	}, nil
1023}
1024
1025// GetReasoningMetadata extracts reasoning metadata from provider options for responses models.
1026func GetReasoningMetadata(providerOptions fantasy.ProviderOptions) *ResponsesReasoningMetadata {
1027	if openaiResponsesOptions, ok := providerOptions[Name]; ok {
1028		if reasoning, ok := openaiResponsesOptions.(*ResponsesReasoningMetadata); ok {
1029			return reasoning
1030		}
1031	}
1032	return nil
1033}
1034
1035type ongoingToolCall struct {
1036	toolName   string
1037	toolCallID string
1038}
1039
1040type reasoningState struct {
1041	metadata *ResponsesReasoningMetadata
1042}
1043
1044// GenerateObject implements fantasy.LanguageModel.
1045func (o responsesLanguageModel) GenerateObject(ctx context.Context, call fantasy.ObjectCall) (*fantasy.ObjectResponse, error) {
1046	switch o.objectMode {
1047	case fantasy.ObjectModeText:
1048		return object.GenerateWithText(ctx, o, call)
1049	case fantasy.ObjectModeTool:
1050		return object.GenerateWithTool(ctx, o, call)
1051	default:
1052		return o.generateObjectWithJSONMode(ctx, call)
1053	}
1054}
1055
1056// StreamObject implements fantasy.LanguageModel.
1057func (o responsesLanguageModel) StreamObject(ctx context.Context, call fantasy.ObjectCall) (fantasy.ObjectStreamResponse, error) {
1058	switch o.objectMode {
1059	case fantasy.ObjectModeTool:
1060		return object.StreamWithTool(ctx, o, call)
1061	case fantasy.ObjectModeText:
1062		return object.StreamWithText(ctx, o, call)
1063	default:
1064		return o.streamObjectWithJSONMode(ctx, call)
1065	}
1066}
1067
1068func (o responsesLanguageModel) generateObjectWithJSONMode(ctx context.Context, call fantasy.ObjectCall) (*fantasy.ObjectResponse, error) {
1069	// Convert our Schema to OpenAI's JSON Schema format
1070	jsonSchemaMap := schema.ToMap(call.Schema)
1071
1072	// Add additionalProperties: false recursively for strict mode (OpenAI requirement)
1073	addAdditionalPropertiesFalse(jsonSchemaMap)
1074
1075	schemaName := call.SchemaName
1076	if schemaName == "" {
1077		schemaName = "response"
1078	}
1079
1080	// Build request using prepareParams
1081	fantasyCall := fantasy.Call{
1082		Prompt:           call.Prompt,
1083		MaxOutputTokens:  call.MaxOutputTokens,
1084		Temperature:      call.Temperature,
1085		TopP:             call.TopP,
1086		PresencePenalty:  call.PresencePenalty,
1087		FrequencyPenalty: call.FrequencyPenalty,
1088		ProviderOptions:  call.ProviderOptions,
1089	}
1090
1091	params, warnings := o.prepareParams(fantasyCall)
1092
1093	// Add structured output via Text.Format field
1094	params.Text = responses.ResponseTextConfigParam{
1095		Format: responses.ResponseFormatTextConfigParamOfJSONSchema(schemaName, jsonSchemaMap),
1096	}
1097
1098	// Make request
1099	response, err := o.client.Responses.New(ctx, *params)
1100	if err != nil {
1101		return nil, toProviderErr(err)
1102	}
1103
1104	if response.Error.Message != "" {
1105		return nil, &fantasy.Error{
1106			Title:   "provider error",
1107			Message: fmt.Sprintf("%s (code: %s)", response.Error.Message, response.Error.Code),
1108		}
1109	}
1110
1111	// Extract JSON text from response
1112	var jsonText string
1113	for _, outputItem := range response.Output {
1114		if outputItem.Type == "message" {
1115			for _, contentPart := range outputItem.Content {
1116				if contentPart.Type == "output_text" {
1117					jsonText = contentPart.Text
1118					break
1119				}
1120			}
1121		}
1122	}
1123
1124	if jsonText == "" {
1125		usage := fantasy.Usage{
1126			InputTokens:  response.Usage.InputTokens,
1127			OutputTokens: response.Usage.OutputTokens,
1128			TotalTokens:  response.Usage.InputTokens + response.Usage.OutputTokens,
1129		}
1130		finishReason := mapResponsesFinishReason(response.IncompleteDetails.Reason, false)
1131		return nil, &fantasy.NoObjectGeneratedError{
1132			RawText:      "",
1133			ParseError:   fmt.Errorf("no text content in response"),
1134			Usage:        usage,
1135			FinishReason: finishReason,
1136		}
1137	}
1138
1139	// Parse and validate
1140	var obj any
1141	if call.RepairText != nil {
1142		obj, err = schema.ParseAndValidateWithRepair(ctx, jsonText, call.Schema, call.RepairText)
1143	} else {
1144		obj, err = schema.ParseAndValidate(jsonText, call.Schema)
1145	}
1146
1147	usage := fantasy.Usage{
1148		InputTokens:  response.Usage.InputTokens,
1149		OutputTokens: response.Usage.OutputTokens,
1150		TotalTokens:  response.Usage.InputTokens + response.Usage.OutputTokens,
1151	}
1152	if response.Usage.OutputTokensDetails.ReasoningTokens != 0 {
1153		usage.ReasoningTokens = response.Usage.OutputTokensDetails.ReasoningTokens
1154	}
1155	if response.Usage.InputTokensDetails.CachedTokens != 0 {
1156		usage.CacheReadTokens = response.Usage.InputTokensDetails.CachedTokens
1157	}
1158
1159	finishReason := mapResponsesFinishReason(response.IncompleteDetails.Reason, false)
1160
1161	if err != nil {
1162		// Add usage info to error
1163		if nogErr, ok := err.(*fantasy.NoObjectGeneratedError); ok {
1164			nogErr.Usage = usage
1165			nogErr.FinishReason = finishReason
1166		}
1167		return nil, err
1168	}
1169
1170	return &fantasy.ObjectResponse{
1171		Object:       obj,
1172		RawText:      jsonText,
1173		Usage:        usage,
1174		FinishReason: finishReason,
1175		Warnings:     warnings,
1176	}, nil
1177}
1178
1179func (o responsesLanguageModel) streamObjectWithJSONMode(ctx context.Context, call fantasy.ObjectCall) (fantasy.ObjectStreamResponse, error) {
1180	// Convert our Schema to OpenAI's JSON Schema format
1181	jsonSchemaMap := schema.ToMap(call.Schema)
1182
1183	// Add additionalProperties: false recursively for strict mode (OpenAI requirement)
1184	addAdditionalPropertiesFalse(jsonSchemaMap)
1185
1186	schemaName := call.SchemaName
1187	if schemaName == "" {
1188		schemaName = "response"
1189	}
1190
1191	// Build request using prepareParams
1192	fantasyCall := fantasy.Call{
1193		Prompt:           call.Prompt,
1194		MaxOutputTokens:  call.MaxOutputTokens,
1195		Temperature:      call.Temperature,
1196		TopP:             call.TopP,
1197		PresencePenalty:  call.PresencePenalty,
1198		FrequencyPenalty: call.FrequencyPenalty,
1199		ProviderOptions:  call.ProviderOptions,
1200	}
1201
1202	params, warnings := o.prepareParams(fantasyCall)
1203
1204	// Add structured output via Text.Format field
1205	params.Text = responses.ResponseTextConfigParam{
1206		Format: responses.ResponseFormatTextConfigParamOfJSONSchema(schemaName, jsonSchemaMap),
1207	}
1208
1209	stream := o.client.Responses.NewStreaming(ctx, *params)
1210
1211	return func(yield func(fantasy.ObjectStreamPart) bool) {
1212		if len(warnings) > 0 {
1213			if !yield(fantasy.ObjectStreamPart{
1214				Type:     fantasy.ObjectStreamPartTypeObject,
1215				Warnings: warnings,
1216			}) {
1217				return
1218			}
1219		}
1220
1221		var accumulated string
1222		var lastParsedObject any
1223		var usage fantasy.Usage
1224		var finishReason fantasy.FinishReason
1225		var streamErr error
1226		hasFunctionCall := false
1227
1228		for stream.Next() {
1229			event := stream.Current()
1230
1231			switch event.Type {
1232			case "response.output_text.delta":
1233				textDelta := event.AsResponseOutputTextDelta()
1234				accumulated += textDelta.Delta
1235
1236				// Try to parse the accumulated text
1237				obj, state, parseErr := schema.ParsePartialJSON(accumulated)
1238
1239				// If we successfully parsed, validate and emit
1240				if state == schema.ParseStateSuccessful || state == schema.ParseStateRepaired {
1241					if err := schema.ValidateAgainstSchema(obj, call.Schema); err == nil {
1242						// Only emit if object is different from last
1243						if !reflect.DeepEqual(obj, lastParsedObject) {
1244							if !yield(fantasy.ObjectStreamPart{
1245								Type:   fantasy.ObjectStreamPartTypeObject,
1246								Object: obj,
1247							}) {
1248								return
1249							}
1250							lastParsedObject = obj
1251						}
1252					}
1253				}
1254
1255				// If parsing failed and we have a repair function, try it
1256				if state == schema.ParseStateFailed && call.RepairText != nil {
1257					repairedText, repairErr := call.RepairText(ctx, accumulated, parseErr)
1258					if repairErr == nil {
1259						obj2, state2, _ := schema.ParsePartialJSON(repairedText)
1260						if (state2 == schema.ParseStateSuccessful || state2 == schema.ParseStateRepaired) &&
1261							schema.ValidateAgainstSchema(obj2, call.Schema) == nil {
1262							if !reflect.DeepEqual(obj2, lastParsedObject) {
1263								if !yield(fantasy.ObjectStreamPart{
1264									Type:   fantasy.ObjectStreamPartTypeObject,
1265									Object: obj2,
1266								}) {
1267									return
1268								}
1269								lastParsedObject = obj2
1270							}
1271						}
1272					}
1273				}
1274
1275			case "response.completed", "response.incomplete":
1276				completed := event.AsResponseCompleted()
1277				finishReason = mapResponsesFinishReason(completed.Response.IncompleteDetails.Reason, hasFunctionCall)
1278				usage = fantasy.Usage{
1279					InputTokens:  completed.Response.Usage.InputTokens,
1280					OutputTokens: completed.Response.Usage.OutputTokens,
1281					TotalTokens:  completed.Response.Usage.InputTokens + completed.Response.Usage.OutputTokens,
1282				}
1283				if completed.Response.Usage.OutputTokensDetails.ReasoningTokens != 0 {
1284					usage.ReasoningTokens = completed.Response.Usage.OutputTokensDetails.ReasoningTokens
1285				}
1286				if completed.Response.Usage.InputTokensDetails.CachedTokens != 0 {
1287					usage.CacheReadTokens = completed.Response.Usage.InputTokensDetails.CachedTokens
1288				}
1289
1290			case "error":
1291				errorEvent := event.AsError()
1292				streamErr = fmt.Errorf("response error: %s (code: %s)", errorEvent.Message, errorEvent.Code)
1293				if !yield(fantasy.ObjectStreamPart{
1294					Type:  fantasy.ObjectStreamPartTypeError,
1295					Error: streamErr,
1296				}) {
1297					return
1298				}
1299				return
1300			}
1301		}
1302
1303		err := stream.Err()
1304		if err != nil {
1305			yield(fantasy.ObjectStreamPart{
1306				Type:  fantasy.ObjectStreamPartTypeError,
1307				Error: toProviderErr(err),
1308			})
1309			return
1310		}
1311
1312		// Final validation and emit
1313		if streamErr == nil && lastParsedObject != nil {
1314			yield(fantasy.ObjectStreamPart{
1315				Type:         fantasy.ObjectStreamPartTypeFinish,
1316				Usage:        usage,
1317				FinishReason: finishReason,
1318			})
1319		} else if streamErr == nil && lastParsedObject == nil {
1320			// No object was generated
1321			yield(fantasy.ObjectStreamPart{
1322				Type: fantasy.ObjectStreamPartTypeError,
1323				Error: &fantasy.NoObjectGeneratedError{
1324					RawText:      accumulated,
1325					ParseError:   fmt.Errorf("no valid object generated in stream"),
1326					Usage:        usage,
1327					FinishReason: finishReason,
1328				},
1329			})
1330		}
1331	}, nil
1332}