responses_language_model.go

   1package openai
   2
   3import (
   4	"context"
   5	"encoding/base64"
   6	"encoding/json"
   7	"errors"
   8	"fmt"
   9	"strings"
  10
  11	"github.com/charmbracelet/fantasy/ai"
  12	"github.com/google/uuid"
  13	"github.com/openai/openai-go/v2"
  14	"github.com/openai/openai-go/v2/packages/param"
  15	"github.com/openai/openai-go/v2/responses"
  16	"github.com/openai/openai-go/v2/shared"
  17)
  18
  19const topLogprobsMax = 20
  20
  21type responsesLanguageModel struct {
  22	provider string
  23	modelID  string
  24	client   openai.Client
  25}
  26
  27// newResponsesLanguageModel implements a responses api model
  28// INFO: (kujtim) currently we do not support stored parameter we default it to false.
  29func newResponsesLanguageModel(modelID string, provider string, client openai.Client) responsesLanguageModel {
  30	return responsesLanguageModel{
  31		modelID:  modelID,
  32		provider: provider,
  33		client:   client,
  34	}
  35}
  36
  37func (o responsesLanguageModel) Model() string {
  38	return o.modelID
  39}
  40
  41func (o responsesLanguageModel) Provider() string {
  42	return o.provider
  43}
  44
  45type responsesModelConfig struct {
  46	isReasoningModel           bool
  47	systemMessageMode          string
  48	requiredAutoTruncation     bool
  49	supportsFlexProcessing     bool
  50	supportsPriorityProcessing bool
  51}
  52
  53func getResponsesModelConfig(modelID string) responsesModelConfig {
  54	supportsFlexProcessing := strings.HasPrefix(modelID, "o3") ||
  55		strings.HasPrefix(modelID, "o4-mini") ||
  56		(strings.HasPrefix(modelID, "gpt-5") && !strings.HasPrefix(modelID, "gpt-5-chat"))
  57
  58	supportsPriorityProcessing := strings.HasPrefix(modelID, "gpt-4") ||
  59		strings.HasPrefix(modelID, "gpt-5-mini") ||
  60		(strings.HasPrefix(modelID, "gpt-5") &&
  61			!strings.HasPrefix(modelID, "gpt-5-nano") &&
  62			!strings.HasPrefix(modelID, "gpt-5-chat")) ||
  63		strings.HasPrefix(modelID, "o3") ||
  64		strings.HasPrefix(modelID, "o4-mini")
  65
  66	defaults := responsesModelConfig{
  67		requiredAutoTruncation:     false,
  68		systemMessageMode:          "system",
  69		supportsFlexProcessing:     supportsFlexProcessing,
  70		supportsPriorityProcessing: supportsPriorityProcessing,
  71	}
  72
  73	if strings.HasPrefix(modelID, "gpt-5-chat") {
  74		return responsesModelConfig{
  75			isReasoningModel:           false,
  76			systemMessageMode:          defaults.systemMessageMode,
  77			requiredAutoTruncation:     defaults.requiredAutoTruncation,
  78			supportsFlexProcessing:     defaults.supportsFlexProcessing,
  79			supportsPriorityProcessing: defaults.supportsPriorityProcessing,
  80		}
  81	}
  82
  83	if strings.HasPrefix(modelID, "o") ||
  84		strings.HasPrefix(modelID, "gpt-5") ||
  85		strings.HasPrefix(modelID, "codex-") ||
  86		strings.HasPrefix(modelID, "computer-use") {
  87		if strings.HasPrefix(modelID, "o1-mini") || strings.HasPrefix(modelID, "o1-preview") {
  88			return responsesModelConfig{
  89				isReasoningModel:           true,
  90				systemMessageMode:          "remove",
  91				requiredAutoTruncation:     defaults.requiredAutoTruncation,
  92				supportsFlexProcessing:     defaults.supportsFlexProcessing,
  93				supportsPriorityProcessing: defaults.supportsPriorityProcessing,
  94			}
  95		}
  96
  97		return responsesModelConfig{
  98			isReasoningModel:           true,
  99			systemMessageMode:          "developer",
 100			requiredAutoTruncation:     defaults.requiredAutoTruncation,
 101			supportsFlexProcessing:     defaults.supportsFlexProcessing,
 102			supportsPriorityProcessing: defaults.supportsPriorityProcessing,
 103		}
 104	}
 105
 106	return responsesModelConfig{
 107		isReasoningModel:           false,
 108		systemMessageMode:          defaults.systemMessageMode,
 109		requiredAutoTruncation:     defaults.requiredAutoTruncation,
 110		supportsFlexProcessing:     defaults.supportsFlexProcessing,
 111		supportsPriorityProcessing: defaults.supportsPriorityProcessing,
 112	}
 113}
 114
 115func (o responsesLanguageModel) prepareParams(call ai.Call) (*responses.ResponseNewParams, []ai.CallWarning) {
 116	var warnings []ai.CallWarning
 117	params := &responses.ResponseNewParams{
 118		Store: param.NewOpt(false),
 119	}
 120
 121	modelConfig := getResponsesModelConfig(o.modelID)
 122
 123	if call.TopK != nil {
 124		warnings = append(warnings, ai.CallWarning{
 125			Type:    ai.CallWarningTypeUnsupportedSetting,
 126			Setting: "topK",
 127		})
 128	}
 129
 130	if call.PresencePenalty != nil {
 131		warnings = append(warnings, ai.CallWarning{
 132			Type:    ai.CallWarningTypeUnsupportedSetting,
 133			Setting: "presencePenalty",
 134		})
 135	}
 136
 137	if call.FrequencyPenalty != nil {
 138		warnings = append(warnings, ai.CallWarning{
 139			Type:    ai.CallWarningTypeUnsupportedSetting,
 140			Setting: "frequencyPenalty",
 141		})
 142	}
 143
 144	var openaiOptions *ResponsesProviderOptions
 145	if opts, ok := call.ProviderOptions[Name]; ok {
 146		if typedOpts, ok := opts.(*ResponsesProviderOptions); ok {
 147			openaiOptions = typedOpts
 148		}
 149	}
 150
 151	input, inputWarnings := toResponsesPrompt(call.Prompt, modelConfig.systemMessageMode)
 152	warnings = append(warnings, inputWarnings...)
 153
 154	var include []IncludeType
 155
 156	addInclude := func(key IncludeType) {
 157		include = append(include, key)
 158	}
 159
 160	topLogprobs := 0
 161	if openaiOptions != nil && openaiOptions.Logprobs != nil {
 162		switch v := openaiOptions.Logprobs.(type) {
 163		case bool:
 164			if v {
 165				topLogprobs = topLogprobsMax
 166			}
 167		case float64:
 168			topLogprobs = int(v)
 169		case int:
 170			topLogprobs = v
 171		}
 172	}
 173
 174	if topLogprobs > 0 {
 175		addInclude(IncludeMessageOutputTextLogprobs)
 176	}
 177
 178	params.Model = o.modelID
 179	params.Input = responses.ResponseNewParamsInputUnion{
 180		OfInputItemList: input,
 181	}
 182
 183	if call.Temperature != nil {
 184		params.Temperature = param.NewOpt(*call.Temperature)
 185	}
 186	if call.TopP != nil {
 187		params.TopP = param.NewOpt(*call.TopP)
 188	}
 189	if call.MaxOutputTokens != nil {
 190		params.MaxOutputTokens = param.NewOpt(*call.MaxOutputTokens)
 191	}
 192
 193	if openaiOptions != nil {
 194		if openaiOptions.MaxToolCalls != nil {
 195			params.MaxToolCalls = param.NewOpt(*openaiOptions.MaxToolCalls)
 196		}
 197		if openaiOptions.Metadata != nil {
 198			metadata := make(shared.Metadata)
 199			for k, v := range openaiOptions.Metadata {
 200				if str, ok := v.(string); ok {
 201					metadata[k] = str
 202				}
 203			}
 204			params.Metadata = metadata
 205		}
 206		if openaiOptions.ParallelToolCalls != nil {
 207			params.ParallelToolCalls = param.NewOpt(*openaiOptions.ParallelToolCalls)
 208		}
 209		if openaiOptions.User != nil {
 210			params.User = param.NewOpt(*openaiOptions.User)
 211		}
 212		if openaiOptions.Instructions != nil {
 213			params.Instructions = param.NewOpt(*openaiOptions.Instructions)
 214		}
 215		if openaiOptions.ServiceTier != nil {
 216			params.ServiceTier = responses.ResponseNewParamsServiceTier(*openaiOptions.ServiceTier)
 217		}
 218		if openaiOptions.PromptCacheKey != nil {
 219			params.PromptCacheKey = param.NewOpt(*openaiOptions.PromptCacheKey)
 220		}
 221		if openaiOptions.SafetyIdentifier != nil {
 222			params.SafetyIdentifier = param.NewOpt(*openaiOptions.SafetyIdentifier)
 223		}
 224		if topLogprobs > 0 {
 225			params.TopLogprobs = param.NewOpt(int64(topLogprobs))
 226		}
 227
 228		if len(openaiOptions.Include) > 0 {
 229			include = append(include, openaiOptions.Include...)
 230		}
 231
 232		if modelConfig.isReasoningModel && (openaiOptions.ReasoningEffort != nil || openaiOptions.ReasoningSummary != nil) {
 233			reasoning := shared.ReasoningParam{}
 234			if openaiOptions.ReasoningEffort != nil {
 235				reasoning.Effort = shared.ReasoningEffort(*openaiOptions.ReasoningEffort)
 236			}
 237			if openaiOptions.ReasoningSummary != nil {
 238				reasoning.Summary = shared.ReasoningSummary(*openaiOptions.ReasoningSummary)
 239			}
 240			params.Reasoning = reasoning
 241		}
 242	}
 243
 244	if modelConfig.requiredAutoTruncation {
 245		params.Truncation = responses.ResponseNewParamsTruncationAuto
 246	}
 247
 248	if len(include) > 0 {
 249		includeParams := make([]responses.ResponseIncludable, len(include))
 250		for i, inc := range include {
 251			includeParams[i] = responses.ResponseIncludable(string(inc))
 252		}
 253		params.Include = includeParams
 254	}
 255
 256	if modelConfig.isReasoningModel {
 257		if call.Temperature != nil {
 258			params.Temperature = param.Opt[float64]{}
 259			warnings = append(warnings, ai.CallWarning{
 260				Type:    ai.CallWarningTypeUnsupportedSetting,
 261				Setting: "temperature",
 262				Details: "temperature is not supported for reasoning models",
 263			})
 264		}
 265
 266		if call.TopP != nil {
 267			params.TopP = param.Opt[float64]{}
 268			warnings = append(warnings, ai.CallWarning{
 269				Type:    ai.CallWarningTypeUnsupportedSetting,
 270				Setting: "topP",
 271				Details: "topP is not supported for reasoning models",
 272			})
 273		}
 274	} else {
 275		if openaiOptions != nil {
 276			if openaiOptions.ReasoningEffort != nil {
 277				warnings = append(warnings, ai.CallWarning{
 278					Type:    ai.CallWarningTypeUnsupportedSetting,
 279					Setting: "reasoningEffort",
 280					Details: "reasoningEffort is not supported for non-reasoning models",
 281				})
 282			}
 283
 284			if openaiOptions.ReasoningSummary != nil {
 285				warnings = append(warnings, ai.CallWarning{
 286					Type:    ai.CallWarningTypeUnsupportedSetting,
 287					Setting: "reasoningSummary",
 288					Details: "reasoningSummary is not supported for non-reasoning models",
 289				})
 290			}
 291		}
 292	}
 293
 294	if openaiOptions != nil && openaiOptions.ServiceTier != nil {
 295		if *openaiOptions.ServiceTier == ServiceTierFlex && !modelConfig.supportsFlexProcessing {
 296			warnings = append(warnings, ai.CallWarning{
 297				Type:    ai.CallWarningTypeUnsupportedSetting,
 298				Setting: "serviceTier",
 299				Details: "flex processing is only available for o3, o4-mini, and gpt-5 models",
 300			})
 301			params.ServiceTier = ""
 302		}
 303
 304		if *openaiOptions.ServiceTier == ServiceTierPriority && !modelConfig.supportsPriorityProcessing {
 305			warnings = append(warnings, ai.CallWarning{
 306				Type:    ai.CallWarningTypeUnsupportedSetting,
 307				Setting: "serviceTier",
 308				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",
 309			})
 310			params.ServiceTier = ""
 311		}
 312	}
 313
 314	tools, toolChoice, toolWarnings := toResponsesTools(call.Tools, call.ToolChoice, openaiOptions)
 315	warnings = append(warnings, toolWarnings...)
 316
 317	if len(tools) > 0 {
 318		params.Tools = tools
 319		params.ToolChoice = toolChoice
 320	}
 321
 322	return params, warnings
 323}
 324
 325func toResponsesPrompt(prompt ai.Prompt, systemMessageMode string) (responses.ResponseInputParam, []ai.CallWarning) {
 326	var input responses.ResponseInputParam
 327	var warnings []ai.CallWarning
 328
 329	for _, msg := range prompt {
 330		switch msg.Role {
 331		case ai.MessageRoleSystem:
 332			var systemText string
 333			for _, c := range msg.Content {
 334				if c.GetType() != ai.ContentTypeText {
 335					warnings = append(warnings, ai.CallWarning{
 336						Type:    ai.CallWarningTypeOther,
 337						Message: "system prompt can only have text content",
 338					})
 339					continue
 340				}
 341				textPart, ok := ai.AsContentType[ai.TextPart](c)
 342				if !ok {
 343					warnings = append(warnings, ai.CallWarning{
 344						Type:    ai.CallWarningTypeOther,
 345						Message: "system prompt text part does not have the right type",
 346					})
 347					continue
 348				}
 349				if strings.TrimSpace(textPart.Text) != "" {
 350					systemText += textPart.Text
 351				}
 352			}
 353
 354			if systemText == "" {
 355				warnings = append(warnings, ai.CallWarning{
 356					Type:    ai.CallWarningTypeOther,
 357					Message: "system prompt has no text parts",
 358				})
 359				continue
 360			}
 361
 362			switch systemMessageMode {
 363			case "system":
 364				input = append(input, responses.ResponseInputItemParamOfMessage(systemText, responses.EasyInputMessageRoleSystem))
 365			case "developer":
 366				input = append(input, responses.ResponseInputItemParamOfMessage(systemText, responses.EasyInputMessageRoleDeveloper))
 367			case "remove":
 368				warnings = append(warnings, ai.CallWarning{
 369					Type:    ai.CallWarningTypeOther,
 370					Message: "system messages are removed for this model",
 371				})
 372			}
 373
 374		case ai.MessageRoleUser:
 375			var contentParts responses.ResponseInputMessageContentListParam
 376			for i, c := range msg.Content {
 377				switch c.GetType() {
 378				case ai.ContentTypeText:
 379					textPart, ok := ai.AsContentType[ai.TextPart](c)
 380					if !ok {
 381						warnings = append(warnings, ai.CallWarning{
 382							Type:    ai.CallWarningTypeOther,
 383							Message: "user message text part does not have the right type",
 384						})
 385						continue
 386					}
 387					contentParts = append(contentParts, responses.ResponseInputContentUnionParam{
 388						OfInputText: &responses.ResponseInputTextParam{
 389							Type: "input_text",
 390							Text: textPart.Text,
 391						},
 392					})
 393
 394				case ai.ContentTypeFile:
 395					filePart, ok := ai.AsContentType[ai.FilePart](c)
 396					if !ok {
 397						warnings = append(warnings, ai.CallWarning{
 398							Type:    ai.CallWarningTypeOther,
 399							Message: "user message file part does not have the right type",
 400						})
 401						continue
 402					}
 403
 404					if strings.HasPrefix(filePart.MediaType, "image/") {
 405						base64Encoded := base64.StdEncoding.EncodeToString(filePart.Data)
 406						imageURL := fmt.Sprintf("data:%s;base64,%s", filePart.MediaType, base64Encoded)
 407						contentParts = append(contentParts, responses.ResponseInputContentUnionParam{
 408							OfInputImage: &responses.ResponseInputImageParam{
 409								Type:     "input_image",
 410								ImageURL: param.NewOpt(imageURL),
 411							},
 412						})
 413					} else if filePart.MediaType == "application/pdf" {
 414						base64Encoded := base64.StdEncoding.EncodeToString(filePart.Data)
 415						fileData := fmt.Sprintf("data:application/pdf;base64,%s", base64Encoded)
 416						filename := filePart.Filename
 417						if filename == "" {
 418							filename = fmt.Sprintf("part-%d.pdf", i)
 419						}
 420						contentParts = append(contentParts, responses.ResponseInputContentUnionParam{
 421							OfInputFile: &responses.ResponseInputFileParam{
 422								Type:     "input_file",
 423								Filename: param.NewOpt(filename),
 424								FileData: param.NewOpt(fileData),
 425							},
 426						})
 427					} else {
 428						warnings = append(warnings, ai.CallWarning{
 429							Type:    ai.CallWarningTypeOther,
 430							Message: fmt.Sprintf("file part media type %s not supported", filePart.MediaType),
 431						})
 432					}
 433				}
 434			}
 435
 436			input = append(input, responses.ResponseInputItemParamOfMessage(contentParts, responses.EasyInputMessageRoleUser))
 437
 438		case ai.MessageRoleAssistant:
 439			for _, c := range msg.Content {
 440				switch c.GetType() {
 441				case ai.ContentTypeText:
 442					textPart, ok := ai.AsContentType[ai.TextPart](c)
 443					if !ok {
 444						warnings = append(warnings, ai.CallWarning{
 445							Type:    ai.CallWarningTypeOther,
 446							Message: "assistant message text part does not have the right type",
 447						})
 448						continue
 449					}
 450					input = append(input, responses.ResponseInputItemParamOfMessage(textPart.Text, responses.EasyInputMessageRoleAssistant))
 451
 452				case ai.ContentTypeToolCall:
 453					toolCallPart, ok := ai.AsContentType[ai.ToolCallPart](c)
 454					if !ok {
 455						warnings = append(warnings, ai.CallWarning{
 456							Type:    ai.CallWarningTypeOther,
 457							Message: "assistant message tool call part does not have the right type",
 458						})
 459						continue
 460					}
 461
 462					if toolCallPart.ProviderExecuted {
 463						continue
 464					}
 465
 466					inputJSON, err := json.Marshal(toolCallPart.Input)
 467					if err != nil {
 468						warnings = append(warnings, ai.CallWarning{
 469							Type:    ai.CallWarningTypeOther,
 470							Message: fmt.Sprintf("failed to marshal tool call input: %v", err),
 471						})
 472						continue
 473					}
 474
 475					input = append(input, responses.ResponseInputItemParamOfFunctionCall(string(inputJSON), toolCallPart.ToolCallID, toolCallPart.ToolName))
 476				case ai.ContentTypeReasoning:
 477					reasoningMetadata := getReasoningMetadata(c.Options())
 478					if reasoningMetadata == nil || reasoningMetadata.ItemID == "" {
 479						continue
 480					}
 481					if len(reasoningMetadata.Summary) == 0 && reasoningMetadata.EncryptedContent == nil {
 482						warnings = append(warnings, ai.CallWarning{
 483							Type:    ai.CallWarningTypeOther,
 484							Message: "assistant message reasoning part does is empty",
 485						})
 486						continue
 487					}
 488					// we want to always send an empty array
 489					summary := []responses.ResponseReasoningItemSummaryParam{}
 490					for _, s := range reasoningMetadata.Summary {
 491						summary = append(summary, responses.ResponseReasoningItemSummaryParam{
 492							Type: "summary_text",
 493							Text: s,
 494						})
 495					}
 496					reasoning := &responses.ResponseReasoningItemParam{
 497						ID:      reasoningMetadata.ItemID,
 498						Summary: summary,
 499					}
 500					if reasoningMetadata.EncryptedContent != nil {
 501						reasoning.EncryptedContent = param.NewOpt(*reasoningMetadata.EncryptedContent)
 502					}
 503					input = append(input, responses.ResponseInputItemUnionParam{
 504						OfReasoning: reasoning,
 505					})
 506				}
 507			}
 508
 509		case ai.MessageRoleTool:
 510			for _, c := range msg.Content {
 511				if c.GetType() != ai.ContentTypeToolResult {
 512					warnings = append(warnings, ai.CallWarning{
 513						Type:    ai.CallWarningTypeOther,
 514						Message: "tool message can only have tool result content",
 515					})
 516					continue
 517				}
 518
 519				toolResultPart, ok := ai.AsContentType[ai.ToolResultPart](c)
 520				if !ok {
 521					warnings = append(warnings, ai.CallWarning{
 522						Type:    ai.CallWarningTypeOther,
 523						Message: "tool message result part does not have the right type",
 524					})
 525					continue
 526				}
 527
 528				var outputStr string
 529				switch toolResultPart.Output.GetType() {
 530				case ai.ToolResultContentTypeText:
 531					output, ok := ai.AsToolResultOutputType[ai.ToolResultOutputContentText](toolResultPart.Output)
 532					if !ok {
 533						warnings = append(warnings, ai.CallWarning{
 534							Type:    ai.CallWarningTypeOther,
 535							Message: "tool result output does not have the right type",
 536						})
 537						continue
 538					}
 539					outputStr = output.Text
 540				case ai.ToolResultContentTypeError:
 541					output, ok := ai.AsToolResultOutputType[ai.ToolResultOutputContentError](toolResultPart.Output)
 542					if !ok {
 543						warnings = append(warnings, ai.CallWarning{
 544							Type:    ai.CallWarningTypeOther,
 545							Message: "tool result output does not have the right type",
 546						})
 547						continue
 548					}
 549					outputStr = output.Error.Error()
 550				case ai.ToolResultContentTypeMedia:
 551					output, ok := ai.AsToolResultOutputType[ai.ToolResultOutputContentMedia](toolResultPart.Output)
 552					if !ok {
 553						warnings = append(warnings, ai.CallWarning{
 554							Type:    ai.CallWarningTypeOther,
 555							Message: "tool result output does not have the right type",
 556						})
 557						continue
 558					}
 559					// For media content, encode as JSON with data and media type
 560					mediaContent := map[string]string{
 561						"data":       output.Data,
 562						"media_type": output.MediaType,
 563					}
 564					jsonBytes, err := json.Marshal(mediaContent)
 565					if err != nil {
 566						warnings = append(warnings, ai.CallWarning{
 567							Type:    ai.CallWarningTypeOther,
 568							Message: fmt.Sprintf("failed to marshal tool result: %v", err),
 569						})
 570						continue
 571					}
 572					outputStr = string(jsonBytes)
 573				}
 574
 575				input = append(input, responses.ResponseInputItemParamOfFunctionCallOutput(toolResultPart.ToolCallID, outputStr))
 576			}
 577		}
 578	}
 579
 580	return input, warnings
 581}
 582
 583func toResponsesTools(tools []ai.Tool, toolChoice *ai.ToolChoice, options *ResponsesProviderOptions) ([]responses.ToolUnionParam, responses.ResponseNewParamsToolChoiceUnion, []ai.CallWarning) {
 584	warnings := make([]ai.CallWarning, 0)
 585	var openaiTools []responses.ToolUnionParam
 586
 587	if len(tools) == 0 {
 588		return nil, responses.ResponseNewParamsToolChoiceUnion{}, nil
 589	}
 590
 591	strictJSONSchema := false
 592	if options != nil && options.StrictJSONSchema != nil {
 593		strictJSONSchema = *options.StrictJSONSchema
 594	}
 595
 596	for _, tool := range tools {
 597		if tool.GetType() == ai.ToolTypeFunction {
 598			ft, ok := tool.(ai.FunctionTool)
 599			if !ok {
 600				continue
 601			}
 602			openaiTools = append(openaiTools, responses.ToolUnionParam{
 603				OfFunction: &responses.FunctionToolParam{
 604					Name:        ft.Name,
 605					Description: param.NewOpt(ft.Description),
 606					Parameters:  ft.InputSchema,
 607					Strict:      param.NewOpt(strictJSONSchema),
 608					Type:        "function",
 609				},
 610			})
 611			continue
 612		}
 613
 614		warnings = append(warnings, ai.CallWarning{
 615			Type:    ai.CallWarningTypeUnsupportedTool,
 616			Tool:    tool,
 617			Message: "tool is not supported",
 618		})
 619	}
 620
 621	if toolChoice == nil {
 622		return openaiTools, responses.ResponseNewParamsToolChoiceUnion{}, warnings
 623	}
 624
 625	var openaiToolChoice responses.ResponseNewParamsToolChoiceUnion
 626
 627	switch *toolChoice {
 628	case ai.ToolChoiceAuto:
 629		openaiToolChoice = responses.ResponseNewParamsToolChoiceUnion{
 630			OfToolChoiceMode: param.NewOpt(responses.ToolChoiceOptionsAuto),
 631		}
 632	case ai.ToolChoiceNone:
 633		openaiToolChoice = responses.ResponseNewParamsToolChoiceUnion{
 634			OfToolChoiceMode: param.NewOpt(responses.ToolChoiceOptionsNone),
 635		}
 636	case ai.ToolChoiceRequired:
 637		openaiToolChoice = responses.ResponseNewParamsToolChoiceUnion{
 638			OfToolChoiceMode: param.NewOpt(responses.ToolChoiceOptionsRequired),
 639		}
 640	default:
 641		openaiToolChoice = responses.ResponseNewParamsToolChoiceUnion{
 642			OfFunctionTool: &responses.ToolChoiceFunctionParam{
 643				Type: "function",
 644				Name: string(*toolChoice),
 645			},
 646		}
 647	}
 648
 649	return openaiTools, openaiToolChoice, warnings
 650}
 651
 652func (o responsesLanguageModel) handleError(err error) error {
 653	var apiErr *openai.Error
 654	if errors.As(err, &apiErr) {
 655		requestDump := apiErr.DumpRequest(true)
 656		responseDump := apiErr.DumpResponse(true)
 657		headers := map[string]string{}
 658		for k, h := range apiErr.Response.Header {
 659			v := h[len(h)-1]
 660			headers[strings.ToLower(k)] = v
 661		}
 662		return ai.NewAPICallError(
 663			apiErr.Message,
 664			apiErr.Request.URL.String(),
 665			string(requestDump),
 666			apiErr.StatusCode,
 667			headers,
 668			string(responseDump),
 669			apiErr,
 670			false,
 671		)
 672	}
 673	return err
 674}
 675
 676func (o responsesLanguageModel) Generate(ctx context.Context, call ai.Call) (*ai.Response, error) {
 677	params, warnings := o.prepareParams(call)
 678	response, err := o.client.Responses.New(ctx, *params)
 679	if err != nil {
 680		return nil, o.handleError(err)
 681	}
 682
 683	if response.Error.Message != "" {
 684		return nil, o.handleError(fmt.Errorf("response error: %s (code: %s)", response.Error.Message, response.Error.Code))
 685	}
 686
 687	var content []ai.Content
 688	hasFunctionCall := false
 689
 690	for _, outputItem := range response.Output {
 691		switch outputItem.Type {
 692		case "message":
 693			for _, contentPart := range outputItem.Content {
 694				if contentPart.Type == "output_text" {
 695					content = append(content, ai.TextContent{
 696						Text: contentPart.Text,
 697					})
 698
 699					for _, annotation := range contentPart.Annotations {
 700						switch annotation.Type {
 701						case "url_citation":
 702							content = append(content, ai.SourceContent{
 703								SourceType: ai.SourceTypeURL,
 704								ID:         uuid.NewString(),
 705								URL:        annotation.URL,
 706								Title:      annotation.Title,
 707							})
 708						case "file_citation":
 709							title := "Document"
 710							if annotation.Filename != "" {
 711								title = annotation.Filename
 712							}
 713							filename := annotation.Filename
 714							if filename == "" {
 715								filename = annotation.FileID
 716							}
 717							content = append(content, ai.SourceContent{
 718								SourceType: ai.SourceTypeDocument,
 719								ID:         uuid.NewString(),
 720								MediaType:  "text/plain",
 721								Title:      title,
 722								Filename:   filename,
 723							})
 724						}
 725					}
 726				}
 727			}
 728
 729		case "function_call":
 730			hasFunctionCall = true
 731			content = append(content, ai.ToolCallContent{
 732				ProviderExecuted: false,
 733				ToolCallID:       outputItem.CallID,
 734				ToolName:         outputItem.Name,
 735				Input:            outputItem.Arguments,
 736			})
 737
 738		case "reasoning":
 739			metadata := &ResponsesReasoningMetadata{
 740				ItemID: outputItem.ID,
 741			}
 742			if outputItem.EncryptedContent != "" {
 743				metadata.EncryptedContent = &outputItem.EncryptedContent
 744			}
 745
 746			if len(outputItem.Summary) == 0 && metadata.EncryptedContent == nil {
 747				continue
 748			}
 749
 750			// When there are no summary parts, add an empty reasoning part
 751			summaries := outputItem.Summary
 752			if len(summaries) == 0 {
 753				summaries = []responses.ResponseReasoningItemSummary{{Type: "summary_text", Text: ""}}
 754			}
 755
 756			var summaryParts []string
 757
 758			for _, s := range summaries {
 759				metadata.Summary = append(metadata.Summary, s.Text)
 760				summaryParts = append(summaryParts, s.Text)
 761			}
 762
 763			content = append(content, ai.ReasoningContent{
 764				Text: strings.Join(summaryParts, "\n"),
 765				ProviderMetadata: ai.ProviderMetadata{
 766					Name: metadata,
 767				},
 768			})
 769		}
 770	}
 771
 772	usage := ai.Usage{
 773		InputTokens:  response.Usage.InputTokens,
 774		OutputTokens: response.Usage.OutputTokens,
 775		TotalTokens:  response.Usage.InputTokens + response.Usage.OutputTokens,
 776	}
 777
 778	if response.Usage.OutputTokensDetails.ReasoningTokens != 0 {
 779		usage.ReasoningTokens = response.Usage.OutputTokensDetails.ReasoningTokens
 780	}
 781	if response.Usage.InputTokensDetails.CachedTokens != 0 {
 782		usage.CacheReadTokens = response.Usage.InputTokensDetails.CachedTokens
 783	}
 784
 785	finishReason := mapResponsesFinishReason(response.IncompleteDetails.Reason, hasFunctionCall)
 786
 787	return &ai.Response{
 788		Content:          content,
 789		Usage:            usage,
 790		FinishReason:     finishReason,
 791		ProviderMetadata: ai.ProviderMetadata{},
 792		Warnings:         warnings,
 793	}, nil
 794}
 795
 796func mapResponsesFinishReason(reason string, hasFunctionCall bool) ai.FinishReason {
 797	if hasFunctionCall {
 798		return ai.FinishReasonToolCalls
 799	}
 800
 801	switch reason {
 802	case "":
 803		return ai.FinishReasonStop
 804	case "max_tokens", "max_output_tokens":
 805		return ai.FinishReasonLength
 806	case "content_filter":
 807		return ai.FinishReasonContentFilter
 808	default:
 809		return ai.FinishReasonOther
 810	}
 811}
 812
 813func (o responsesLanguageModel) Stream(ctx context.Context, call ai.Call) (ai.StreamResponse, error) {
 814	params, warnings := o.prepareParams(call)
 815
 816	stream := o.client.Responses.NewStreaming(ctx, *params)
 817
 818	finishReason := ai.FinishReasonUnknown
 819	var usage ai.Usage
 820	ongoingToolCalls := make(map[int64]*ongoingToolCall)
 821	hasFunctionCall := false
 822	activeReasoning := make(map[string]*reasoningState)
 823
 824	return func(yield func(ai.StreamPart) bool) {
 825		if len(warnings) > 0 {
 826			if !yield(ai.StreamPart{
 827				Type:     ai.StreamPartTypeWarnings,
 828				Warnings: warnings,
 829			}) {
 830				return
 831			}
 832		}
 833
 834		for stream.Next() {
 835			event := stream.Current()
 836
 837			switch event.Type {
 838			case "response.created":
 839				_ = event.AsResponseCreated()
 840
 841			case "response.output_item.added":
 842				added := event.AsResponseOutputItemAdded()
 843				switch added.Item.Type {
 844				case "function_call":
 845					ongoingToolCalls[added.OutputIndex] = &ongoingToolCall{
 846						toolName:   added.Item.Name,
 847						toolCallID: added.Item.CallID,
 848					}
 849					if !yield(ai.StreamPart{
 850						Type:         ai.StreamPartTypeToolInputStart,
 851						ID:           added.Item.CallID,
 852						ToolCallName: added.Item.Name,
 853					}) {
 854						return
 855					}
 856
 857				case "message":
 858					if !yield(ai.StreamPart{
 859						Type: ai.StreamPartTypeTextStart,
 860						ID:   added.Item.ID,
 861					}) {
 862						return
 863					}
 864
 865				case "reasoning":
 866					metadata := &ResponsesReasoningMetadata{
 867						ItemID:  added.Item.ID,
 868						Summary: []string{},
 869					}
 870					if added.Item.EncryptedContent != "" {
 871						metadata.EncryptedContent = &added.Item.EncryptedContent
 872					}
 873
 874					activeReasoning[added.Item.ID] = &reasoningState{
 875						metadata: metadata,
 876					}
 877					if !yield(ai.StreamPart{
 878						Type: ai.StreamPartTypeReasoningStart,
 879						ID:   added.Item.ID,
 880						ProviderMetadata: ai.ProviderMetadata{
 881							Name: metadata,
 882						},
 883					}) {
 884						return
 885					}
 886				}
 887
 888			case "response.output_item.done":
 889				done := event.AsResponseOutputItemDone()
 890				switch done.Item.Type {
 891				case "function_call":
 892					tc := ongoingToolCalls[done.OutputIndex]
 893					if tc != nil {
 894						delete(ongoingToolCalls, done.OutputIndex)
 895						hasFunctionCall = true
 896
 897						if !yield(ai.StreamPart{
 898							Type: ai.StreamPartTypeToolInputEnd,
 899							ID:   done.Item.CallID,
 900						}) {
 901							return
 902						}
 903						if !yield(ai.StreamPart{
 904							Type:          ai.StreamPartTypeToolCall,
 905							ID:            done.Item.CallID,
 906							ToolCallName:  done.Item.Name,
 907							ToolCallInput: done.Item.Arguments,
 908						}) {
 909							return
 910						}
 911					}
 912
 913				case "message":
 914					if !yield(ai.StreamPart{
 915						Type: ai.StreamPartTypeTextEnd,
 916						ID:   done.Item.ID,
 917					}) {
 918						return
 919					}
 920
 921				case "reasoning":
 922					state := activeReasoning[done.Item.ID]
 923					if state != nil {
 924						if !yield(ai.StreamPart{
 925							Type: ai.StreamPartTypeReasoningEnd,
 926							ID:   done.Item.ID,
 927							ProviderMetadata: ai.ProviderMetadata{
 928								Name: state.metadata,
 929							},
 930						}) {
 931							return
 932						}
 933						delete(activeReasoning, done.Item.ID)
 934					}
 935				}
 936
 937			case "response.function_call_arguments.delta":
 938				delta := event.AsResponseFunctionCallArgumentsDelta()
 939				tc := ongoingToolCalls[delta.OutputIndex]
 940				if tc != nil {
 941					if !yield(ai.StreamPart{
 942						Type:  ai.StreamPartTypeToolInputDelta,
 943						ID:    tc.toolCallID,
 944						Delta: delta.Delta,
 945					}) {
 946						return
 947					}
 948				}
 949
 950			case "response.output_text.delta":
 951				textDelta := event.AsResponseOutputTextDelta()
 952				if !yield(ai.StreamPart{
 953					Type:  ai.StreamPartTypeTextDelta,
 954					ID:    textDelta.ItemID,
 955					Delta: textDelta.Delta,
 956				}) {
 957					return
 958				}
 959
 960			case "response.reasoning_summary_part.added":
 961				added := event.AsResponseReasoningSummaryPartAdded()
 962				state := activeReasoning[added.ItemID]
 963				if state != nil {
 964					state.metadata.Summary = append(state.metadata.Summary, "")
 965					activeReasoning[added.ItemID] = state
 966					if !yield(ai.StreamPart{
 967						Type:  ai.StreamPartTypeReasoningDelta,
 968						ID:    added.ItemID,
 969						Delta: "\n",
 970						ProviderMetadata: ai.ProviderMetadata{
 971							Name: state.metadata,
 972						},
 973					}) {
 974						return
 975					}
 976				}
 977
 978			case "response.reasoning_summary_text.delta":
 979				textDelta := event.AsResponseReasoningSummaryTextDelta()
 980				state := activeReasoning[textDelta.ItemID]
 981				if state != nil {
 982					if len(state.metadata.Summary)-1 >= int(textDelta.SummaryIndex) {
 983						state.metadata.Summary[textDelta.SummaryIndex] += textDelta.Delta
 984					}
 985					activeReasoning[textDelta.ItemID] = state
 986					if !yield(ai.StreamPart{
 987						Type:  ai.StreamPartTypeReasoningDelta,
 988						ID:    textDelta.ItemID,
 989						Delta: textDelta.Delta,
 990						ProviderMetadata: ai.ProviderMetadata{
 991							Name: state.metadata,
 992						},
 993					}) {
 994						return
 995					}
 996				}
 997
 998			case "response.completed", "response.incomplete":
 999				completed := event.AsResponseCompleted()
1000				finishReason = mapResponsesFinishReason(completed.Response.IncompleteDetails.Reason, hasFunctionCall)
1001				usage = ai.Usage{
1002					InputTokens:  completed.Response.Usage.InputTokens,
1003					OutputTokens: completed.Response.Usage.OutputTokens,
1004					TotalTokens:  completed.Response.Usage.InputTokens + completed.Response.Usage.OutputTokens,
1005				}
1006				if completed.Response.Usage.OutputTokensDetails.ReasoningTokens != 0 {
1007					usage.ReasoningTokens = completed.Response.Usage.OutputTokensDetails.ReasoningTokens
1008				}
1009				if completed.Response.Usage.InputTokensDetails.CachedTokens != 0 {
1010					usage.CacheReadTokens = completed.Response.Usage.InputTokensDetails.CachedTokens
1011				}
1012
1013			case "error":
1014				errorEvent := event.AsError()
1015				if !yield(ai.StreamPart{
1016					Type:  ai.StreamPartTypeError,
1017					Error: fmt.Errorf("response error: %s (code: %s)", errorEvent.Message, errorEvent.Code),
1018				}) {
1019					return
1020				}
1021				return
1022			}
1023		}
1024
1025		err := stream.Err()
1026		if err != nil {
1027			yield(ai.StreamPart{
1028				Type:  ai.StreamPartTypeError,
1029				Error: o.handleError(err),
1030			})
1031			return
1032		}
1033
1034		yield(ai.StreamPart{
1035			Type:         ai.StreamPartTypeFinish,
1036			Usage:        usage,
1037			FinishReason: finishReason,
1038		})
1039	}, nil
1040}
1041
1042func getReasoningMetadata(providerOptions ai.ProviderOptions) *ResponsesReasoningMetadata {
1043	if openaiResponsesOptions, ok := providerOptions[Name]; ok {
1044		if reasoning, ok := openaiResponsesOptions.(*ResponsesReasoningMetadata); ok {
1045			return reasoning
1046		}
1047	}
1048	return nil
1049}
1050
1051type ongoingToolCall struct {
1052	toolName   string
1053	toolCallID string
1054}
1055
1056type reasoningState struct {
1057	metadata *ResponsesReasoningMetadata
1058}