Private/Invoke-AICompletion.ps1
|
function Invoke-AICompletion { <# .SYNOPSIS Provider-agnostic LLM API caller. .DESCRIPTION Sends prompts to various LLM providers (Anthropic, OpenAI, Ollama, Custom) and returns the text response. Handles rate limiting with exponential backoff, authentication errors, and timeouts. #> [CmdletBinding()] param( [Parameter(Mandatory)] [ValidateSet('Anthropic', 'OpenAI', 'Ollama', 'Custom')] [string]$Provider, [Parameter()] [string]$ApiKey, [Parameter()] [string]$Model, [Parameter()] [string]$Endpoint, [Parameter()] [string]$SystemPrompt, [Parameter(Mandatory)] [string]$UserPrompt, [Parameter()] [int]$MaxTokens = 4096, [Parameter()] [double]$Temperature = 0.1 ) # Resolve API key from environment if not provided if (-not $ApiKey -and $Provider -in @('Anthropic', 'OpenAI', 'Custom')) { $ApiKey = $env:LIVINGDOC_API_KEY if (-not $ApiKey) { switch ($Provider) { 'Anthropic' { $ApiKey = $env:ANTHROPIC_API_KEY } 'OpenAI' { $ApiKey = $env:OPENAI_API_KEY } } } if (-not $ApiKey -and $Provider -ne 'Custom') { throw "No API key provided. Set -ApiKey parameter or `$env:LIVINGDOC_API_KEY / `$env:$($Provider.ToUpper())_API_KEY" } } # Resolve default models if (-not $Model) { $Model = switch ($Provider) { 'Anthropic' { 'claude-sonnet-4-5-20250929' } 'OpenAI' { 'gpt-4o' } 'Ollama' { 'llama3' } 'Custom' { 'default' } } } # Build request based on provider $headers = @{} $body = $null $uri = $null switch ($Provider) { 'Anthropic' { $uri = 'https://api.anthropic.com/v1/messages' $headers = @{ 'x-api-key' = $ApiKey 'anthropic-version' = '2023-06-01' 'Content-Type' = 'application/json' } $messages = @( @{ role = 'user'; content = $UserPrompt } ) $bodyObj = @{ model = $Model max_tokens = $MaxTokens temperature = $Temperature messages = $messages } if ($SystemPrompt) { $bodyObj['system'] = $SystemPrompt } $body = $bodyObj | ConvertTo-Json -Depth 10 } 'OpenAI' { $uri = 'https://api.openai.com/v1/chat/completions' $headers = @{ 'Authorization' = "Bearer $ApiKey" 'Content-Type' = 'application/json' } $messages = @() if ($SystemPrompt) { $messages += @{ role = 'system'; content = $SystemPrompt } } $messages += @{ role = 'user'; content = $UserPrompt } $bodyObj = @{ model = $Model max_tokens = $MaxTokens temperature = $Temperature messages = $messages } $body = $bodyObj | ConvertTo-Json -Depth 10 } 'Ollama' { $uri = if ($Endpoint) { "$($Endpoint.TrimEnd('/'))/api/generate" } else { 'http://localhost:11434/api/generate' } $headers = @{ 'Content-Type' = 'application/json' } $prompt = '' if ($SystemPrompt) { $prompt = "System: $SystemPrompt`n`nUser: $UserPrompt" } else { $prompt = $UserPrompt } $bodyObj = @{ model = $Model prompt = $prompt stream = $false options = @{ temperature = $Temperature num_predict = $MaxTokens } } $body = $bodyObj | ConvertTo-Json -Depth 10 } 'Custom' { if (-not $Endpoint) { throw "Custom provider requires -Endpoint parameter." } $uri = $Endpoint $headers = @{ 'Content-Type' = 'application/json' } if ($ApiKey) { $headers['Authorization'] = "Bearer $ApiKey" } $messages = @() if ($SystemPrompt) { $messages += @{ role = 'system'; content = $SystemPrompt } } $messages += @{ role = 'user'; content = $UserPrompt } $bodyObj = @{ model = $Model max_tokens = $MaxTokens temperature = $Temperature messages = $messages } $body = $bodyObj | ConvertTo-Json -Depth 10 } } # Execute request with retry logic $maxRetries = 3 $retryCount = 0 $baseDelay = 2 while ($retryCount -le $maxRetries) { try { Write-Verbose "Sending request to $Provider ($uri) with model $Model..." $splat = @{ Uri = $uri Method = 'POST' Headers = $headers Body = $body TimeoutSec = 120 UseBasicParsing = $true } # PowerShell 5.1 compatibility: handle encoding if ($PSVersionTable.PSVersion.Major -le 5) { $splat['Body'] = [System.Text.Encoding]::UTF8.GetBytes($body) } $response = Invoke-RestMethod @splat # Parse response based on provider $result = switch ($Provider) { 'Anthropic' { if ($response.content -and $response.content.Count -gt 0) { $response.content[0].text } else { throw "Unexpected Anthropic response format." } } 'OpenAI' { if ($response.choices -and $response.choices.Count -gt 0) { $response.choices[0].message.content } else { throw "Unexpected OpenAI response format." } } 'Ollama' { if ($response.response) { $response.response } else { throw "Unexpected Ollama response format." } } 'Custom' { # Try OpenAI format first, then raw if ($response.choices -and $response.choices.Count -gt 0) { $response.choices[0].message.content } elseif ($response.content -and $response.content.Count -gt 0) { $response.content[0].text } elseif ($response.response) { $response.response } else { $response | ConvertTo-Json -Depth 5 } } } Write-Verbose "Received response from $Provider ($([Math]::Min($result.Length, 100)) chars)." return $result } catch { $errorMsg = $_.Exception.Message # Check for rate limit (429) if ($errorMsg -match '429|rate.?limit|too.?many.?requests') { $retryCount++ if ($retryCount -le $maxRetries) { $delay = [Math]::Pow($baseDelay, $retryCount) Write-Warning "Rate limited by $Provider. Retrying in $delay seconds (attempt $retryCount of $maxRetries)..." Start-Sleep -Seconds $delay continue } } # Check for auth errors (401/403) if ($errorMsg -match '401|403|unauthorized|forbidden|invalid.?api.?key') { throw "Authentication failed for $Provider. Please check your API key. Error: $errorMsg" } # Check for timeout if ($errorMsg -match 'timeout|timed.?out') { $retryCount++ if ($retryCount -le $maxRetries) { $delay = [Math]::Pow($baseDelay, $retryCount) Write-Warning "Request to $Provider timed out. Retrying in $delay seconds (attempt $retryCount of $maxRetries)..." Start-Sleep -Seconds $delay continue } } # For any other error or exhausted retries, throw throw "AI API call to $Provider failed: $errorMsg" } } throw "AI API call to $Provider failed after $maxRetries retries." } |