Private/Invoke-OpenAIAPIRequest.ps1
using namespace System.Collections using namespace System.Text using namespace System.Net using namespace System.Net.Http using namespace System.Runtime.InteropServices using namespace System.Management.Automation using namespace Microsoft.PowerShell.Commands $script:HttpClientHandler = @{ WebSession = $null Expires = $null } function Invoke-OpenAIAPIRequest { [CmdletBinding()] param ( [Parameter()] [string]$Method = 'Post', [Parameter(Mandatory)] [System.Uri]$Uri, [Parameter()] [string]$ContentType = 'application/json', [Parameter(Mandatory)] [ValidateNotNull()] [securestring]$ApiKey, [Parameter()] [IDictionary]$AdditionalQuery, [Parameter()] [AllowEmptyString()] [string]$Organization, [Parameter()] [object]$Body, [Parameter()] [object]$AdditionalBody, [Parameter()] [IDictionary]$Headers, [Parameter()] [IDictionary]$AdditionalHeaders, [Parameter()] [int]$TimeoutSec = 0, [Parameter()] [int]$MaxRetryCount = 0, [Parameter()] [int]$RetryCount = 0, [Parameter()] [bool]$Stream = $false, [Parameter()] # [ValidateSet('openai', 'azure', 'azure_ad')] [string]$AuthType = 'openai', [Parameter()] [bool]$ReturnRawResponse = $false ) #region Set variables $IsDebug = Test-Debug $ServiceName = switch -Wildcard ($AuthType) { 'openai*' { 'OpenAI' } 'azure*' { 'Azure OpenAI' } } #endregion #region Assert selected model is discontinued if ($null -ne $Body -and $null -ne $Body.model) { Assert-UnsupportedModels -Model $Body.model } #endregion # Query string if ($PSBoundParameters.ContainsKey('AdditionalQuery') -and $null -ne $AdditionalQuery) { $UriBuilder = [System.UriBuilder]::new($Uri) $QueryParam = [System.Web.HttpUtility]::ParseQueryString($UriBuilder.Query) foreach ($s in $AdditionalQuery.GetEnumerator()) { $QueryParam.Add($s.Key, $s.Value) } $UriBuilder.Query = $QueryParam.ToString() $Uri = $UriBuilder.Uri } # Headers dictionary $RequestHeaders = @{} if ($PSBoundParameters.ContainsKey('Headers') -and $null -ne $Headers) { $RequestHeaders = Merge-Dictionary $Headers $RequestHeaders } if ($PSBoundParameters.ContainsKey('AdditionalHeaders') -and $null -ne $AdditionalHeaders) { $RequestHeaders = Merge-Dictionary $RequestHeaders $AdditionalHeaders } # Set debug header if ($IsDebug) { $RequestHeaders['OpenAI-Debug'] = 'true' } # Body object if ($null -ne $Body) { if ($ContentType -match 'multipart/form-data') { $Boundary = New-MultipartFormBoundary $Body = New-MultipartFormContent -FormData $Body -Boundary $Boundary $ContentType = ('multipart/form-data; boundary="{0}"' -f $Boundary) } elseif ($ContentType -match 'application/json') { if ($Body -is [pscustomobject]) { $Body = ObjectToHashTable $Body } if ($PSBoundParameters.ContainsKey('AdditionalBody') -and $null -ne $AdditionalBody) { if ($AdditionalBody -is [string]) { try { $AdditionalBody = ConvertFrom-Json $AdditionalBody -Depth 100 } catch { Write-Error -Exception ([System.InvalidOperationException]::new('Failed to parse AdditionalBody as JSON.')) } } if ($AdditionalBody -is [pscustomobject]) { $AdditionalBody = ObjectToHashTable $AdditionalBody } $Body = Merge-Dictionary $Body $AdditionalBody } } } #region Server-Sent-Events if ($Stream) { $params = @{ Method = $Method Uri = $Uri ContentType = $ContentType ApiKey = $ApiKey Organization = $Organization AuthType = $AuthType Body = $Body Headers = $RequestHeaders TimeoutSec = $TimeoutSec MaxRetryCount = $MaxRetryCount } Invoke-OpenAIAPIRequestSSE @params return } #endregion #region API request # Construct parameter for Invoke-WebRequest $PlainToken = DecryptSecureString $ApiKey $IwrParam = @{ Method = $Method Uri = $Uri ContentType = $ContentType TimeoutSec = $TimeoutSec UseBasicParsing = $true } # Use HTTP/2 (if possible) if ($null -ne (Get-Command 'Microsoft.PowerShell.Utility\Invoke-WebRequest').Parameters.HttpVersion) { $IwrParam.HttpVersion = [version]::new(2, 0) } # Reuse WebSession (HttpClient) # Note: This method is only available on PS7.4+ if ($PSVersionTable.PSVersion -ge 7.4) { if ($null -eq $script:HttpClientHandler.WebSession -or $script:HttpClientHandler.Expires -lt [datetime]::Now) { # Reset Session $script:HttpClientHandler.WebSession = $null $script:HttpClientHandler.Expires = $null $IwrParam.SessionVariable = 'WebSession' } else { # Reuse Session $IwrParam.WebSession = $script:HttpClientHandler.WebSession } } switch ($AuthType) { 'openai' { $UseBearer = $true # Set Organization-ID if (-not [string]::IsNullOrWhiteSpace($Organization)) { $RequestHeaders['OpenAI-Organization'] = $Organization.Trim() } } 'azure' { $UseBearer = $false $RequestHeaders['api-key'] = $PlainToken } 'azure_ad' { $UseBearer = $true } default { # covers null $UseBearer = $true } } # Absorb differences in PowerShell version if ($PSVersionTable.PSVersion.Major -ge 6) { $PlainToken = $null $IwrParam.AllowUnencryptedAuthentication = $true # Use Bearer Token Auth if ($UseBearer) { $IwrParam.Authentication = 'Bearer' $IwrParam.Token = $ApiKey } } else { # Use Bearer Token Auth if ($UseBearer) { $RequestHeaders['Authorization'] = "Bearer $PlainToken" } } if ($null -ne $Body) { if ($ContentType -match 'application/json') { try { $Body = ($Body | ConvertTo-Json -Compress -Depth 100) }catch { Write-Error -Exception $_.Exception } $IwrParam.Body = ([System.Text.Encoding]::UTF8.GetBytes($Body)) } else { $IwrParam.Body = $Body } } # Set http request headers if ($null -ne $RequestHeaders -and $RequestHeaders.Count -ne 0) { $IwrParam.Headers = $RequestHeaders } # Verbose / Debug output Write-Verbose -Message "Request to $ServiceName API" Write-Verbose -Message "Method = $Method, Path = $Uri" if ($IsDebug) { $startIdx = $lastIdx = 2 if ($AuthType -eq 'openai') { $startIdx += 4 } # 'org-' $params1 = @{ Source = 'Request parameters: ' + (([pscustomobject]$IwrParam) | Format-List Method, Uri, ContentType, Headers, Authentication | Out-String).TrimEnd() Target = ($ApiKey, $Organization) First = $startIdx Last = $lastIdx MaxNumberOfAsterisks = 45 } $maskedString1 = Get-MaskedString @params1 Write-Debug -Message $maskedString1 $params2 = @{ Source = 'Post body: ' + $Body Target = ($ApiKey, $Organization) First = $startIdx Last = $lastIdx MaxNumberOfAsterisks = 45 } $maskedString2 = Get-MaskedString @params2 Write-Debug -Message $maskedString2 } #region Send API Request try { $Response = Microsoft.PowerShell.Utility\Invoke-WebRequest @IwrParam } catch [HttpRequestException], [WebException] { # Trash last error from cmdlet if ($global:Error[0].FullyQualifiedErrorId.StartsWith('WebCmdletWebResponseException', [StringComparison]::Ordinal)) { $global:Error.RemoveAt(0) } # Parse error details $ErrorObject = Parse-WebExceptionResponse -ErrorRecord $_ -ServiceName $ServiceName if (-not $ErrorObject) { $PSCmdlet.ThrowTerminatingError($_) } # Retry if (Should-Retry -ErrorCode $ErrorObject.StatusCode -ErrorMessage $ErrorObject.Message -Headers $ErrorObject.Response.Headers -RetryCount $RetryCount -MaxRetryCount $MaxRetryCount) { $Delay = Get-RetryDelay -RetryCount $RetryCount -ResponseHeaders $ErrorObject.Response.Headers Write-Warning $ErrorObject.Message Write-Warning ('Retry the request after waiting {0} ms (retry count: {1})' -f $Delay, $RetryCount) Start-Sleep -Milliseconds $Delay $PSBoundParameters.RetryCount = (++$RetryCount) Invoke-OpenAIAPIRequest @PSBoundParameters return } $er = [ErrorRecord]::new( $ErrorObject, ('PSOpenAI.APIRequest.{0}' -f $ErrorObject.GetType().Name), [ErrorCategory]::InvalidOperation, $null ) $er.ErrorDetails = $ErrorObject.Message $PSCmdlet.ThrowTerminatingError($er) } catch { $PSCmdlet.ThrowTerminatingError($_) } finally { $PlainToken = $null } #endregion # Fix content charset from ISO-8859-1 to UTF-8 (only JSON with PowerShell 5) if (($PSVersionTable.PSVersion.Major -le 5) -and ` ($Response.Headers.'Content-Type' -match 'application/json')) { $Content = [Encoding]::UTF8.GetString([Encoding]::GetEncoding('ISO-8859-1').GetBytes($Response.Content)) } else { $Content = $Response.Content } # Verbose / Debug output $verboseMessage = "$ServiceName API response: " + ($Response | Format-List StatusCode, @{ name = 'processing_ms' expression = { $_.Headers['openai-processing-ms'] } }, @{ name = 'request_id' expression = { $_.Headers['X-Request-Id'] } } | Out-String).TrimEnd() Write-Verbose -Message $verboseMessage # Don't read the whole stream for debug logging unless necessary. if ($IsDebug) { $startIdx = $lastIdx = 2 if ($AuthType -eq 'openai') { $startIdx += 4 } # 'org-' $params1 = @{ Source = 'API response header: ' + ($Response.Headers | Format-Table -HideTableHeaders | Out-String).TrimEnd() Target = ($ApiKey, $Organization) First = $startIdx Last = $lastIdx MaxNumberOfAsterisks = 45 } $maskedString1 = Get-MaskedString @params1 Write-Debug -Message $maskedString1 $params2 = @{ Source = 'API response body: ' + ($Response.Content | Out-String).TrimEnd() Target = ($ApiKey, $Organization) First = $startIdx Last = $lastIdx MaxNumberOfAsterisks = 45 } $maskedString2 = Get-MaskedString @params2 Write-Debug -Message $maskedString2 } # Save WebSession if ($WebSession) { $script:HttpClientHandler.WebSession = $WebSession $script:HttpClientHandler.Expires = [datetime]::Now.AddMinutes(5) } # Output if ($ReturnRawResponse) { Write-Output $Response } else { Write-Output $Content } #endregion } |