Private/Invoke-AnthropicReview.ps1
|
function Invoke-AnthropicReview { <# .SYNOPSIS Invoke code review using Anthropic Claude API .DESCRIPTION Calls Claude API with retry logic and returns parsed violations. .PARAMETER ReviewContext Array of file diffs .PARAMETER Rules Review rules markdown .PARAMETER ApiKey Anthropic API key .PARAMETER Config Provider configuration .PARAMETER MaxTokens Maximum output tokens .OUTPUTS System.Array - Array of violations .NOTES Author: waldo Version: 1.0.0 #> [CmdletBinding()] param( [Parameter(Mandatory = $true)] [array]$ReviewContext, [Parameter(Mandatory = $true)] [string]$Rules, [Parameter(Mandatory = $true)] [string]$ApiKey, [Parameter(Mandatory = $true)] [object]$Config, [Parameter(Mandatory = $false)] [int]$MaxTokens = 4000, [Parameter(Mandatory = $false)] [string]$SystemPromptText ) begin { Write-Verbose "Starting $($MyInvocation.MyCommand.Name)" } process { try { $prompts = Build-ReviewPrompt -Rules $Rules -ReviewContext $ReviewContext -SystemPromptText $SystemPromptText Write-Host "Model: $($Config.model)" # Estimate tokens $estimatedInputTokens = [int](($prompts.SystemPrompt.Length + $prompts.UserPrompt.Length) / 4) Write-Host "Estimated input tokens: $estimatedInputTokens" $headers = @{ "x-api-key" = $ApiKey "anthropic-version" = "2023-06-01" "content-type" = "application/json" } $body = @{ model = $Config.model max_tokens = $MaxTokens system = $prompts.SystemPrompt messages = @( @{ role = "user" content = $prompts.UserPrompt } ) } | ConvertTo-Json -Depth 10 # Call API with retry logic $maxRetries = 3 $attempt = 0 $violations = $null while ($attempt -lt $maxRetries -and $null -eq $violations) { try { $attempt++ Write-Verbose "Calling Claude API (attempt $attempt/$maxRetries)..." $response = Invoke-RestMethod -Uri "https://api.anthropic.com/v1/messages" ` -Method Post ` -Headers $headers ` -Body $body ` -TimeoutSec 600 # Safety classifiers can decline (HTTP 200, stop_reason refusal): no review was produced, # and retrying the same request will not change that - fail the step instead of reporting Clean. if ($response.stop_reason -eq 'refusal') { $attempt = $maxRetries $category = if ($response.stop_details) { " ($($response.stop_details.category))" } else { '' } throw "Claude declined to review this diff (stop_reason refusal$category); no review produced" } if ($response.stop_reason -eq 'max_tokens') { Write-Warning "Claude's answer was cut off at max_tokens=$MaxTokens; the violations list may be incomplete" } # The response can start with a thinking block; the review is in the text block(s). $responseText = (@($response.content) | Where-Object { $_.type -eq 'text' } | ForEach-Object { $_.text }) -join "`n" Write-Verbose "Received response: $($responseText.Length) characters" # No text block at all (seen once in 104 Sonnet 5.5 replay runs): there is no review to parse. # Say what came back instead of failing on the parser's empty-string binding. if ([string]::IsNullOrWhiteSpace($responseText)) { $blockTypes = (@($response.content) | ForEach-Object { $_.type }) -join ', ' throw "Claude returned no review text (stop_reason $($response.stop_reason), content blocks: $(if ($blockTypes) { $blockTypes } else { 'none' }), output tokens: $($response.usage.output_tokens))" } $parsed = Parse-AIResponse -ResponseText $responseText $violations = $parsed.Violations $prCommentText = $parsed.PRCommentText if ($null -ne $violations) { Write-Host "✅ Successfully received $($violations.Count) violation(s)" # Log actual usage if ($response.usage) { Write-Host "Actual tokens: $($response.usage.input_tokens) input, $($response.usage.output_tokens) output" # Sonnet 5.5 list price: $2 / $10 per million input / output tokens $cost = ($response.usage.input_tokens / 1000000) * 2.0 + ($response.usage.output_tokens / 1000000) * 10.0 Write-Host "Estimated cost: `$$([math]::Round($cost, 4))" } } } catch { $errMessage = $_.Exception.Message # Try to extract the actual Anthropic error body for better diagnostics $responseBody = $null if ($_.Exception.Response) { try { $stream = $_.Exception.Response.GetResponseStream() $reader = [System.IO.StreamReader]::new($stream) $responseBody = $reader.ReadToEnd() $reader.Close() } catch {} } Write-Warning "API call failed (attempt $attempt): $errMessage" if ($responseBody) { Write-Warning "Anthropic response body: $responseBody" } if ($attempt -ge $maxRetries) { throw "Failed after $maxRetries attempts: $errMessage$(if ($responseBody) { "`nResponse: $responseBody" })" } $sleepSeconds = [Math]::Pow(2, $attempt) Write-Host "Retrying in $sleepSeconds seconds..." Start-Sleep -Seconds $sleepSeconds } } return @{ Violations = $violations PRCommentText = $prCommentText RawResponse = $responseText TokenUsage = $response.usage } } catch { Write-Error "Error in $($MyInvocation.MyCommand.Name): $_" throw } } end { Write-Verbose "Completed $($MyInvocation.MyCommand.Name)" } } |