Public/Responses/Request-Response.ps1
function Request-Response { [CmdletBinding()] [OutputType([pscustomobject])] param ( [Parameter(Mandatory = $false, Position = 0, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [Alias('input')] [Alias('UserMessage')] [string]$Message, [Parameter()] [ValidateNotNullOrEmpty()] [Completions('user', 'system', 'developer', 'assistant')] [string][LowerCaseTransformation()]$Role = 'user', [Parameter(ValueFromPipelineByPropertyName)] [Completions( 'gpt-3.5-turbo', 'gpt-4', 'gpt-4o', 'gpt-4o-mini', 'gpt-3.5-turbo-16k', 'gpt-4-turbo', 'gpt-4.5-preview', 'o1', 'o3-mini', 'computer-use-preview' )] [string]$Model = 'gpt-4o-mini', #region System messages [Parameter()] [AllowEmptyString()] [Alias('system')] [string[]]$SystemMessage, [Parameter()] [AllowEmptyString()] [string[]]$DeveloperMessage, [Parameter()] [string]$Instructions, #endregion System messages #region Image input [Parameter()] [string[]]$Images, [Parameter()] [ValidateSet('auto', 'low', 'high')] [string][LowerCaseTransformation()]$ImageDetail = 'auto', #endregion Image input #region File input [Parameter()] [string[]]$Files, #endregion Image input #region Tools [Parameter()] [Alias('tool_choice')] [Completions('none', 'auto', 'required')] [object]$ToolChoice, [Parameter()] [Alias('parallel_tool_calls')] [bool]$ParallelToolCalls, #region Function calling [Parameter()] [ValidateNotNullOrEmpty()] [System.Collections.IDictionary[]]$Functions, [Parameter()] [ValidateSet('None', 'Auto', 'Confirm')] [string]$InvokeFunction = 'None', #endregion Function calling # Built-in tools #region File Search [Parameter()] [switch]$UseFileSearch, [Parameter(DontShow)] [string]$FileSearchType = 'file_search', # Currently, only 'file_search' is acceptable. [Parameter()] [string[]]$FileSearchVectorStoreIds, [Parameter()] [ValidateRange(1, 50)] [string[]]$FileSearchMaxNumberOfResults, [Parameter()] [string]$FileSearchFilters, [Parameter()] [Completions('auto')] [string]$FileSearchRanker = 'auto', [Parameter()] [ValidateRange(0.0, 1.0)] [double]$FileSearchScoreThreshold = 0.0, #endregion File Search #region Web Search [Parameter()] [switch]$UseWebSearch, [Parameter()] [Completions('web_search_preview')] [string]$WebSearchType = 'web_search_preview', [Parameter()] [ValidateSet('low', 'medium', 'high')] [string][LowerCaseTransformation()]$WebSearchContextSize, [Parameter(DontShow)] [string]$WebSearchUserLocationType = 'approximate', # Currently, only 'approximate' is acceptable. [Parameter()] [string]$WebSearchUserLocationCity, [Parameter()] [string]$WebSearchUserLocationCountry, [Parameter()] [string]$WebSearchUserLocationRegion, [Parameter()] [string]$WebSearchUserLocationTimeZone, #endregion Web Search #region Computer use [Parameter()] [switch]$UseComputerUse, [Parameter(DontShow)] [string]$ComputerUseType = 'computer_use_preview', # Currently, only 'computer_use_preview' is acceptable. [Parameter()] [string]$ComputerUseEnvironment, [Parameter()] [int]$ComputerUseDisplayHeight, [Parameter()] [int]$ComputerUseDisplayWidth, #endregion Computer use #endregion Tools [Parameter()] [Alias('previous_response_id')] [string]$PreviousResponseId, [Parameter()] [AllowEmptyCollection()] [string[]]$Include, [Parameter()] [Completions('auto', 'disabled')] [string]$Truncation, [Parameter()] [ValidateRange(0.0, 2.0)] [double]$Temperature, [Parameter()] [ValidateRange(0.0, 1.0)] [Alias('top_p')] [double]$TopP, [Parameter()] [switch]$Store = $false, #region Stream [Parameter()] [switch]$Stream = $false, [Parameter()] [ValidateSet('text', 'object')] [string]$StreamOutputType = 'text', #endregion Stream #region Reasoning [Parameter()] [Completions('low', 'medium', 'high')] [string]$ReasoningEffort = 'medium', [Parameter()] [Completions('concise', 'detailed')] [string]$ReasoningGenerateSummary, #endregion Reasoning [Parameter()] [System.Collections.IDictionary]$MetaData, [Parameter()] [Alias('max_output_tokens')] [int]$MaxOutputTokens, [Parameter()] [Completions('text', 'json_schema', 'json_object')] [object]$TextOutputFormat = 'text', #region Structured Outputs [Parameter()] [string]$JsonSchema, [Parameter()] [string]$JsonSchemaName, [Parameter()] [string]$JsonSchemaDescription, [Parameter()] [bool]$JsonSchemaStrict, #endregion Structured Outputs [Parameter()] [string]$User, [Parameter()] [switch]$AsBatch, [Parameter()] [string]$CustomBatchId, [Parameter()] [switch]$OutputRawResponse, [Parameter()] [int]$TimeoutSec = 0, [Parameter()] [ValidateRange(0, 100)] [int]$MaxRetryCount = 0, [Parameter()] [OpenAIApiType]$ApiType = [OpenAIApiType]::OpenAI, [Parameter()] [System.Uri]$ApiBase, [Parameter(DontShow)] [string]$ApiVersion, [Parameter()] [ValidateSet('openai', 'azure', 'azure_ad')] [string]$AuthType = 'openai', [Parameter()] [securestring][SecureStringTransformation()]$ApiKey, [Parameter()] [Alias('OrgId')] [string]$Organization, [Parameter(ValueFromPipelineByPropertyName)] [object[]]$History, [Parameter()] [System.Collections.IDictionary]$AdditionalQuery, [Parameter()] [System.Collections.IDictionary]$AdditionalHeaders, [Parameter()] [object]$AdditionalBody ) begin { # Get API context $OpenAIParameter = Get-OpenAIAPIParameter -EndpointName 'Responses' -Parameters $PSBoundParameters -Engine $Model -ErrorAction Stop } process { #region Construct parameters for API request $Response = $null $PostBody = [System.Collections.Specialized.OrderedDictionary]::new() if ($OpenAIParameter.ApiType -eq [OpenAIApiType]::OpenAI -or $AsBatch) { $PostBody.model = $Model } if ($PSBoundParameters.ContainsKey('Include')) { $PostBody.include = @($Include) } if ($PSBoundParameters.ContainsKey('Instructions')) { $PostBody.instructions = $Instructions } if ($PSBoundParameters.ContainsKey('MaxOutputTokens')) { $PostBody.max_output_tokens = $MaxOutputTokens } if ($PSBoundParameters.ContainsKey('Truncation')) { $PostBody.truncation = $Truncation } if ($PSBoundParameters.ContainsKey('MetaData')) { $PostBody.metadata = $MetaData } if ($PSBoundParameters.ContainsKey('PreviousResponseId')) { $PostBody.previous_response_id = $PreviousResponseId } if ($PSBoundParameters.ContainsKey('ToolChoice')) { $PostBody.tool_choice = $ToolChoice } if ($PSBoundParameters.ContainsKey('ParallelToolCalls')) { $PostBody.parallel_tool_calls = $ParallelToolCalls } if ($PSBoundParameters.ContainsKey('TopP')) { $PostBody.top_p = $TopP } if ($PSBoundParameters.ContainsKey('Temperature')) { $PostBody.temperature = $Temperature } if ($Store.IsPresent) { $PostBody.store = $Store.ToBool() } if ($PSBoundParameters.ContainsKey('User')) { $PostBody.user = $User } if ($Stream) { $PostBody.stream = [bool]$Stream } # Reasoning $ReasoningOptions = @{} if ($PSBoundParameters.ContainsKey('ReasoningEffort')) { $ReasoningOptions.reasoning_effort = $ReasoningEffort } if ($PSBoundParameters.ContainsKey('ReasoningGenerateSummary')) { $ReasoningOptions.generate_summary = $ReasoningGenerateSummary } if ($ReasoningOptions.Keys.Count -gt 0) { $PostBody.reasoning = $ReasoningOptions } # Text Output options $TextOutputOptions = @{} if ($PSBoundParameters.ContainsKey('TextOutputFormat')) { if ($TextOutputFormat -is [type]) { # Structured Outputs $typeSchema = ConvertTo-JsonSchema $TextOutputFormat $TextOutputOptions.format = @{ 'type' = 'json_schema' 'name' = $TextOutputFormat.Name 'strict' = $true 'schema' = $typeSchema } } elseif ($TextOutputFormat -eq 'text') { $TextOutputOptions.format = @{'type' = 'text' } } elseif ($TextOutputFormat -eq 'json_object') { $TextOutputOptions.format = @{'type' = 'json_object' } } elseif ($TextOutputFormat -eq 'json_schema') { # Structured Outputs $PostBody.response_format = @{'type' = $Format } if ($Format -eq 'json_schema') { if (-not $JsonSchema) { Write-Error -Exception ([System.ArgumentException]::new('JsonSchema must be specified.')) } else { $TextOutputOptions.format = @{ 'type' = 'json_schema' 'schema' = (ConvertFrom-Json $JsonSchema) } if ($PSBoundParameters.ContainsKey('JsonSchemaName')) { $TextOutputOptions.format.name = $JsonSchemaName } if ($PSBoundParameters.ContainsKey('JsonSchemaDescription')) { $TextOutputOptions.format.description = $JsonSchemaDescription } if ($PSBoundParameters.ContainsKey('JsonSchemaStrict')) { $TextOutputOptions.format.strict = $JsonSchemaStrict } } } } } if ($TextOutputOptions.Keys.Count -gt 0) { $PostBody.text = $TextOutputOptions } # Tools $Tools = @() if ($PSBoundParameters.ContainsKey('Functions')) { $Tools += $Functions } # File Search if ($UseFileSearch) { if ($FileSearchVectorStoreIds.Count -eq 0) { Write-Error 'VectorStore Ids must be specified.' } else { $RankingOptions = @{} $FileSearchTool = @{ type = $FileSearchType vector_store_ids = $FileSearchVectorStoreIds } if ($PSBoundParameters.ContainsKey('FileSearchMaxNumberOfResults')) { $FileSearchTool.max_num_results = $FileSearchMaxNumberOfResults } if ($PSBoundParameters.ContainsKey('FileSearchFilters')) { $FileSearchTool.filters = $FileSearchFilters } if ($PSBoundParameters.ContainsKey('FileSearchRanker')) { $RankingOptions.ranker = $FileSearchRanker } if ($PSBoundParameters.ContainsKey('FileSearchScoreThreshold')) { $RankingOptions.score_threshold = $FileSearchScoreThreshold } if ($RankingOptions.Keys.Count -gt 0) { $FileSearchTool.ranking_options = $RankingOptions } $Tools += $FileSearchTool } } # Web Search if ($UseWebSearch) { $UserLocation = @{} $WebSearchTool = @{ type = $WebSearchType } if ($PSBoundParameters.ContainsKey('WebSearchContextSize')) { $WebSearchTool.search_context_size = $WebSearchContextSize } if ($PSBoundParameters.ContainsKey('WebSearchUserLocationCity')) { $UserLocation.city = $WebSearchUserLocationCity } if ($PSBoundParameters.ContainsKey('WebSearchUserLocationCountry')) { $UserLocation.country = $WebSearchUserLocationCountry } if ($PSBoundParameters.ContainsKey('WebSearchUserLocationRegion')) { $UserLocation.region = $WebSearchUserLocationRegion } if ($PSBoundParameters.ContainsKey('WebSearchUserLocationTimeZone')) { $UserLocation.timezone = $WebSearchUserLocationTimeZone } if ($UserLocation.Keys.Count -gt 0) { $UserLocation.type = $WebSearchUserLocationType $WebSearchTool.user_location = $UserLocation } $Tools += $WebSearchTool } # Computer Use if ($UseComputerUse) { $ComputerUseTool = @{ type = $ComputerUseType } if ($PSBoundParameters.ContainsKey('ComputerUseEnvironment')) { $ComputerUseTool.environment = $ComputerUseEnvironment } else { Write-Error 'ComputerUseEnvironment must be specified.' } if ($PSBoundParameters.ContainsKey('ComputerUseDisplayHeight')) { $ComputerUseTool.display_height = $ComputerUseDisplayHeight } else { Write-Error 'ComputerUseDisplayHeight must be specified.' } if ($PSBoundParameters.ContainsKey('ComputerUseDisplayWidth')) { $ComputerUseTool.display_width = $ComputerUseDisplayWidth } else { Write-Error 'ComputerUseDisplayWidth must be specified.' } $Tools += $ComputerUseTool } #region Construct messages $Messages = [System.Collections.Generic.List[object]]::new() # Append past conversations foreach ($pastmsg in $History) { $Messages.Add($pastmsg) } # Specifies system messages (only if specified) foreach ($sysmsg in $SystemMessage) { if (-not [string]::IsNullOrWhiteSpace($sysmsg)) { $Messages.Add([ordered]@{ role = 'system' content = $sysmsg }) } } # Specifies developer messages (only if specified) foreach ($devmsg in $DeveloperMessage) { if (-not [string]::IsNullOrWhiteSpace($devmsg)) { $Messages.Add([ordered]@{ role = 'developer' content = $devmsg }) } } #region Add user messages $usermsg = [ordered]@{ role = 'user' content = @() } # Text message if (-not [string]::IsNullOrWhiteSpace($Message)) { $usermsg.content += @{type = 'text'; text = $Message } } # File input if ($PSBoundParameters.ContainsKey('Files')) { foreach ($file in $Files) { if ([string]::IsNullOrWhiteSpace($file)) { continue } $fileContent = $null if (Test-Path -LiteralPath $file -PathType Leaf) { # local file $fileItem = Get-Item -LiteralPath $file $fileContent = @{ type = 'input_file' filename = $fileItem.Name file_data = ([System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($fileItem.FullName))) } } elseif ($file -match '[\\/]') { continue } else { # file id $fileContent = @{ type = 'input_file' file_id = $file } } $usermsg.content += $fileContent } } # Image input if ($PSBoundParameters.ContainsKey('Images')) { foreach ($image in $Images) { if ([string]::IsNullOrWhiteSpace($image)) { continue } $imageContent = $null if (Test-Path -LiteralPath $image -PathType Leaf) { # local file $imageContent = @{ type = 'input_image' image_url = (Convert-ImageToDataURL $image) } } else { # file id $imageContent = @{ type = 'input_image' file_id = $file } } if ($PSBoundParameters.ContainsKey('ImageDetail')) { $imageContent.detail = $ImageDetail } $usermsg.content += $imageContent } } if ($usermsg.content.Count -ge 1) { $Messages.Add($usermsg) } #endregion # Error if message is empty. if ($Messages.Count -eq 0) { Write-Error 'No message is specified. You must specify one or more messages.' return } $PostBody.input = $Messages.ToArray() #endregion # As Batch if ($AsBatch) { if ([string]::IsNullOrEmpty($CustomBatchId)) { $CustomBatchId = 'request-{0:x4}' -f (Get-Random -Maximum 65535) } $batchInputObject = [pscustomobject]@{ 'custom_id' = $CustomBatchId 'method' = 'POST' 'url' = $OpenAIParameter.BatchEndpoint 'body' = [pscustomobject]$PostBody } $batchInputObject.PSObject.TypeNames.Insert(0, 'PSOpenAI.Batch.Input') return $batchInputObject } #region Send API Request (Stream) if ($Stream) { # Stream output $splat = @{ Method = $OpenAIParameter.Method Uri = $OpenAIParameter.Uri ContentType = $OpenAIParameter.ContentType TimeoutSec = $OpenAIParameter.TimeoutSec MaxRetryCount = $OpenAIParameter.MaxRetryCount ApiKey = $OpenAIParameter.ApiKey AuthType = $OpenAIParameter.AuthType Organization = $OpenAIParameter.Organization Body = $PostBody Stream = $Stream AdditionalQuery = $AdditionalQuery AdditionalHeaders = $AdditionalHeaders AdditionalBody = $AdditionalBody } Invoke-OpenAIAPIRequest @splat | Where-Object { -not [string]::IsNullOrEmpty($_) } | ForEach-Object { if ($OutputRawResponse) { $_ } else { try { $_ | ConvertFrom-Json -ErrorAction Stop } catch { Write-Error -Exception $_.Exception } } } | ForEach-Object -Process { if ($OutputRawResponse) { Write-Output $_ } elseif ($StreamOutputType -eq 'text') { if ($_.type -cne 'response.output_text.delta') { continue } Write-Output $_.delta } else { Write-Output $_ } } return } #endregion #region Send API Request (No Stream) else { $splat = @{ Method = $OpenAIParameter.Method Uri = $OpenAIParameter.Uri ContentType = $OpenAIParameter.ContentType TimeoutSec = $OpenAIParameter.TimeoutSec MaxRetryCount = $OpenAIParameter.MaxRetryCount ApiKey = $OpenAIParameter.ApiKey AuthType = $OpenAIParameter.AuthType Organization = $OpenAIParameter.Organization Body = $PostBody AdditionalQuery = $AdditionalQuery AdditionalHeaders = $AdditionalHeaders AdditionalBody = $AdditionalBody } $Response = Invoke-OpenAIAPIRequest @splat # error check if ($null -eq $Response) { return } # Parse response object if ($OutputRawResponse) { Write-Output $Response return } try { $Response = $Response | ConvertFrom-Json -ErrorAction Stop } catch { Write-Error -Exception $_.Exception return } } #endregion #region For history, add AI response to messages list. if (@($Response.output).Count -ge 1) { $Messages.Add(@($Response.output)[0]) } #endregion #region Function call if ($null -ne $Response.choices -and $Response.choices[0].finish_reason -eq 'tool_calls') { $ToolCallResults = @() $fCalls = @($Response.choices[0].message.tool_calls) foreach ($fCall in $fCalls) { if ($fCall.type -ne 'function') { continue } if ($fCall.function.name -notin $Tools.Where({ $_.type -eq 'function' }).function.name) { Write-Error ('"{0}" does not matches the list of functions. This command should not be executed.' -f $fCall.function.name) continue } Write-Verbose ('AI assistant preferes to call a function. (function:{0}, arguments:{1})' -f $fCall.function.name, ($fCall.function.arguments -replace '[\r\n]', '')) $fCommandResult = $null try { # Execute command $fCommandResult = Invoke-ChatCompletionFunction -Name $fCall.function.name -Arguments $fCall.function.arguments -InvokeFunctionOnCallMode $InvokeTools -ErrorAction Stop } catch { Write-Error -ErrorRecord $_ $fCommandResult = '[ERROR] ' + $_.Exception.Message } if ($null -eq $fCommandResult) { continue } $ToolCallResults += @{ role = 'tool' content = $(if ($fCommandResult -is [string]) { $fCommandResult }else { (ConvertTo-Json $fCommandResult) }) tool_call_id = $fCall.id } } # Second request if ($ToolCallResults.Count -gt 0) { Write-Verbose 'The function has been executed. The result of the execution is sent to the API.' $SecondRequestParam = $PSBoundParameters $null = $SecondRequestParam.Remove('Message') $null = $SecondRequestParam.Remove('Role') $null = $SecondRequestParam.Remove('Name') $null = $SecondRequestParam.Remove('SystemMessage') $null = $SecondRequestParam.Remove('DeveloperMessage') $Messages.AddRange($ToolCallResults) $SecondRequestParam.History = $Messages.ToArray() Request-ChatCompletion @SecondRequestParam return } } #endregion #region Output ParseChatCompletionObject $Response -Messages $Messages -OutputType $Format #endregion } end { } } |