providers/claude.ps1
<#
Esta função é usada como base para invocar a a API da Anthropic! #> function InvokeClaude { [CmdletBinding()] param( $endpoint ,$body ,$method = 'POST' ,$StreamCallback = $null ,$Token = $null ) $Provider = Get-AiCurrentProvider $Creds = Get-AiDefaultCredential -MigrateScript { $ApiKey = GetCurrentProviderData -Context Token; if($ApiKey){ SetCurrentProviderData Token $null; $AiCredential = NewAiCredential $AiCredential.name = "default" $AiCredential.credential = $ApiKey; return $AiCredential } } $ApiKey = $Creds.credential.credential $headers = @{ "anthropic-version" = "2023-06-01" } $headers["x-api-key"] = "$ApiKey" if($endpoint -match '^https?://'){ $url = $endpoint } else { $BaseUrl = GetCurrentProviderData BaseUrl $url = "$BaseUrl/$endpoint" } if($StreamCallback){ $body.stream = $true; } $JsonParams = @{Depth = 10} write-verbose "InvokeOpenai: Converting body to json (depth: $($JsonParams.Depth))..." $ReqBodyPrint = $body | ConvertTo-Json @JsonParams write-verbose "ReqBody:`n$($ReqBodyPrint|out-string)" $ReqBody = $body | ConvertTo-Json @JsonParams -Compress $ReqParams = @{ data = $ReqBody url = $url method = $method Headers = $headers SseCallback = $StreamCallback } write-verbose "ReqParams:`n$($ReqParams|out-string)" $RawResp = InvokeHttp @ReqParams write-verbose "RawResp: `n$($RawResp|out-string)" if($RawResp.stream){ return $RawResp; } return $RawResp.text | ConvertFrom-Json } Set-Alias Set-ClaudeToken Set-AiCredential function ConvertTo-ClaudeMessage { param($message) [object[]]$messages = @(ConvertTo-OpenaiMessage $messages) [object[]]$ContentMessages = @() $SystemMessage = @() $CallsIds = @{} :MsgLoop foreach($m in $messages){ switch($m.role){ "system" { $SystemMessage += $m.content.text; } default { $ClaudeMessage = @{ content = $m.content role = $m.role } foreach($content in $ClaudeMessage.content){ $content = $_; if($content -is [string]){ continue; } if($content.type -eq "image_url" -and $content.image_url.url -match 'data:(.+?);base64,(.+)'){ $MimeType = $matches[1]; $Base64 = $matches[2]; $content.type = "image" $content.source = @{ type = "base64" media_type = $MimeType data = $Base64 } $content.remove("image_url"); } } $ContentMessages += $ClaudeMessage } } } return [PsCustomObject]@{ SystemMessage = ($SystemMessage -Join "`n") messages = $ContentMessages } } function Get-ClaudeMessages { [CmdletBinding()] param( $messages ,$model ,$max_tokens = 200 ,$temperature = 0.5 ,$StreamCallback = $null ,$RawParams = @{} ) $Provider = Get-AiCurrentProvider #Note que reaproveitamos o convert to openai message do provider! $ClaudeMessages = @(ConvertTo-ClaudeMessage $messages) $data = HashTableMerge @{ messages = @($ClaudeMessages.messages) max_tokens = $max_tokens temperature = $temperature model = $model system = $ClaudeMessages.SystemMessage } $RawParams InvokeClaude "messages" -body $data -StreamCallback $StreamCallback } function claude_Chat { param( $prompt ,$temperature = 0.6 ,$model = $null ,$MaxTokens = 200 ,$ResponseFormat = $null ,#Function list, as acceptable by Openai #OpenAuxFunc2Tool can be used to convert from powershell to this format! $Functions = @() #Add raw params directly to api! #overwite previous ,$RawParams = @{} ,$StreamCallback = $null ) $Params = @{ messages = $prompt temperature = $temperature model = $model max_tokens = $MaxTokens RawParams = $RawParams } $StreamData = @{ #Todas as respostas enviadas! answers = @() LastOpenaiAnswer = $null CurrentEvent = $null ChunkId = [guid]::NewGuid().guid MessageMeta = @{ stop_reason = $null model = $null id = $null usage = @{ input_tokens = 0 output_tokens = 0 } } } $CurrentEvent = $null; function Convert-ClaudeToOpenaiAnswer { param($Answer, $StreamData) $Result = @{ id = [guid]::NewGuid().Guid object = "chat.completion" created = [int](((Get-Date) - (Get-Date "1970-01-01T00:00:00Z")).TotalSeconds) service_tier = $null system_fingerprint = $null usage = @{ completion_tokens = $Answer.usage.output_tokens prompt_tokens = $Answer.usage.input_tokens total_tokens = $null } logprobs = $null choices = @() model = $Answer.model } $Result | Add-Member -Force ScriptMethod GetClaudeDetails { @{ obj = $Answer } }.GetNewClosure() if($StreamData){ $Result.model = $StreamData.MessageMeta.model } $NewChoice = @{ index = $Result.choices.length logprobs = $null finish_reason = $Answer.stop_reason } $Result.choices += $NewChoice; $Message = @{ role = "assistant" refusal = $null content = $null } if($Answer.content){ $Message.content = $Answer.content[0].text } if($StreamData){ $Result.object = "chat.completion.chunk" $Result.id = $StreamData.ChunkId $Message.content = $Answer.delta.text $NewChoice.delta = $Message } else { $NewChoice.message = $Message } return $Result; } $StreamScript = $null if($StreamCallback){ $UserScriptCallback = $StreamCallback $StreamScript = { param($data) $line = @($data.line)[0]; $StreamData.lines += $line; if(!$line){ return; } if($line -match "event: (.+)"){ $StreamData.CurrentEvent = $matches[1]; return; } $CurrentEvent = $StreamData.CurrentEvent if($CurrentEvent -eq "message_stop"){ return $false; } $Answer = $null if($line -like "data: *"){ $RawJson = $line.replace("data: ",""); $Answer = $RawJson | ConvertFrom-Json; $StreamData.answers += $Answer } if($CurrentEvent -eq "message_start"){ $Message = $Answer.message; $StreamData.MessageMeta.usage.input_tokens = $Message.usage.input_tokens; $StreamData.MessageMeta.usage.output_tokens += $Message.usage.output_tokens; $StreamData.MessageMeta.model = $Message.model; return; } if($CurrentEvent -eq "content_block_delta"){ $OpenaiAnswer = Convert-ClaudeToOpenaiAnswer $Answer $StreamData $DeltaResp = $Answer.delta if($StreamData.LastOpenaiAnswer){ $StreamData.LastOpenaiAnswer.choices[0].delta.content += $OpenaiAnswer.choices[0].delta.content } else { $StreamData.LastOpenaiAnswer = $OpenaiAnswer } & $UserScriptCallback $OpenaiAnswer } if($CurrentEvent -eq "message_delta"){ $Usage = $Answer.usage; $StreamData.MessageMeta.usage.output_tokens += $Usage.output_tokens $StreamData.LastOpenaiAnswer.usage.completion_tokens = $StreamData.MessageMeta.usage.output_tokens } } } $resp = Get-ClaudeMessages @Params -StreamCallback $StreamScript if($resp.stream){ if(!$StreamData.LastOpenaiAnswer){ throw "POWERSHAI_CLAUDE_CHAT_NOOPENAI: No openai message was returned. Probably a bug in claude for powershai!" } $StreamData.LastOpenaiAnswer.stream = @{ RawResp = $resp answers = $StreamData.answers } $StreamData.LastOpenaiAnswer.choices | %{ $_.message = $_.delta $_.remove('delta'); } $OpenaiAnswer = $StreamData.LastOpenaiAnswer } else { $OpenaiAnswer = Convert-ClaudeToOpenaiAnswer $resp } $Choices = @(); foreach($choice in $OpenaiAnswer.choices){ #$tools = @() #foreach($call in $_.message.tool_calls){ # $tools += New-AiChatResultToolCall -FunctionName $call.function.name -FunctionArgs $call.function.arguments -CallId $call.id #} # $FinishReason = "stop"; switch($choice.finish_reason){ "end_turn" { $FinishReason = "stop" } "max_tokens" { $FinishReason = "length" } "stop_sequence" { $FinishReason = "stop" } "tool_use" { $FinishReason = "tool_calls" } default { verbose "Unkown claude finish reason: $($choice.finish_reason). Using default!"; } } $Choices += New-AiChatResultChoice -FinishReason $FinishReason -role $choice.message.role -Content $choice.message.content #-tools $tools } $params = @{ id = $OpenaiAnswer.id model = $OpenaiAnswer.model choices = $Choices SystemFingerprint = $OpenaiAnswer.system_fingerprint PromptTokens = $OpenaiAnswer.usage.prompt_tokens CompletionTokens = $OpenaiAnswer.usage.completion_tokens TotalTokens = $OpenaiAnswer.usage.total_tokens } $ChatResult = New-AiChatResult @params return $ChatResult; } function claude_GetModels() { return @( @{ name = 'claude-3-5-sonnet-latest' } @{ name = 'claude-3-5-sonnet-20241022' } @{ name = 'claude-3-5-haiku-20241022' } @{ name = 'claude-3-5-haiku-latest' } @{ name = 'claude-3-opus-20240229' } @{ name = 'claude-3-opus-latest' } @{ name = 'claude-3-sonnet-20240229' } @{ name = 'claude-3-haiku-20240307' } ) | %{ [PsCustomObject]$_ } } return @{ RequireToken = $true BaseUrl = "https://api.anthropic.com/v1" DefaultModel = "claude-3-5-haiku-latest" CredentialEnvName = "CLAUDE_API_KEY" info = @{ desc = "Anthropic Claude" url = "https://docs.anthropic.com/" } } |