From 97f072263b5b8b741d2d3c8057a3a4bd81bebc1a Mon Sep 17 00:00:00 2001 From: Doug Finke Date: Sat, 7 Feb 2026 06:56:58 -0500 Subject: [PATCH 01/16] Add OpenAI provider support --- Public/Invoke-PSClaudeCode.ps1 | 197 ++++++++++++++++++++++++++------- README.md | 22 ++-- 2 files changed, 170 insertions(+), 49 deletions(-) diff --git a/Public/Invoke-PSClaudeCode.ps1 b/Public/Invoke-PSClaudeCode.ps1 index fa2c454..9f5aed3 100644 --- a/Public/Invoke-PSClaudeCode.ps1 +++ b/Public/Invoke-PSClaudeCode.ps1 @@ -3,9 +3,9 @@ Invokes Claude Code, an AI-powered PowerShell agent that can perform tasks using structured tools. .DESCRIPTION - Invoke-PSClaudeCode uses Anthropic's Claude AI model to execute tasks by leveraging various tools including - file reading/writing, command execution, and sub-agent delegation. The function supports pipeline input - and provides safety checks for potentially dangerous operations. + Invoke-PSClaudeCode uses configurable LLM providers (Anthropic or OpenAI) to execute tasks by leveraging + various tools including file reading/writing, command execution, and sub-agent delegation. The function + supports pipeline input and provides safety checks for potentially dangerous operations. .PARAMETER Task The task description for the AI agent to perform. If not provided and pipeline input exists, the piped content becomes the task. @@ -14,7 +14,10 @@ Accepts pipeline input that can be used as part of the task description. .PARAMETER Model - The Claude model to use. Defaults to "claude-sonnet-4-5-20250929". + The model to use. Defaults to "claude-sonnet-4-5-20250929" for Anthropic. + +.PARAMETER Provider + The LLM provider to use. Defaults to "Anthropic". Use "OpenAI" for OpenAI-compatible models. .PARAMETER dangerouslySkipPermissions Switch to skip permission prompts for potentially dangerous operations. Use with caution. @@ -35,7 +38,7 @@ This example runs a command without permission prompts. .NOTES - Requires ANTHROPIC_API_KEY environment variable to be set. + Requires ANTHROPIC_API_KEY or OPENAI_API_KEY environment variable to be set, depending on Provider. The function includes safety checks for file operations and command execution. #> function Invoke-PSClaudeCode { @@ -46,6 +49,8 @@ function Invoke-PSClaudeCode { [Parameter(ValueFromPipeline = $true)] [object]$InputObject, [string]$Model = "claude-sonnet-4-5-20250929", + [ValidateSet("Anthropic", "OpenAI")] + [string]$Provider = "Anthropic", [switch]$dangerouslySkipPermissions ) @@ -69,10 +74,139 @@ function Invoke-PSClaudeCode { # proceed with the rest of the function using the possibly-updated $Task - $apiKey = $env:ANTHROPIC_API_KEY - if (-not $apiKey) { Write-Host "Set ANTHROPIC_API_KEY"; exit } + function Get-ApiKey { + param([string]$SelectedProvider) + + switch ($SelectedProvider) { + "Anthropic" { return $env:ANTHROPIC_API_KEY } + "OpenAI" { return $env:OPENAI_API_KEY } + default { return $null } + } + } + + function Convert-ToolsForProvider { + param([string]$SelectedProvider, $ToolDefinitions) + + if ($SelectedProvider -eq "OpenAI") { + return $ToolDefinitions | ForEach-Object { + @{ + type = "function" + function = @{ + name = $_.name + description = $_.description + parameters = $_.input_schema + } + } + } + } + + return $ToolDefinitions + } + + function Normalize-Response { + param([string]$SelectedProvider, $Response) + + if ($SelectedProvider -eq "OpenAI") { + $message = $Response.choices[0].message + $contentItems = @() + + if ($message.tool_calls) { + foreach ($toolCall in $message.tool_calls) { + $arguments = $toolCall.function.arguments + $inputObject = if ([string]::IsNullOrWhiteSpace($arguments)) { @{} } else { $arguments | ConvertFrom-Json } + $contentItems += @{ + type = "tool_use" + id = $toolCall.id + name = $toolCall.function.name + input = $inputObject + } + } + } + + if ($message.content) { + $contentItems += @{ + type = "text" + text = $message.content + } + } + + return @{ content = $contentItems } + } + + return $Response + } + + function Invoke-ModelRequest { + param( + [string]$SelectedProvider, + [string]$SelectedModel, + $MessageHistory, + $ToolDefinitions, + [string]$Key + ) + + if ($SelectedProvider -eq "OpenAI") { + $body = @{ + model = $SelectedModel + messages = $MessageHistory + max_tokens = 4096 + tools = $ToolDefinitions + tool_choice = "auto" + } | ConvertTo-Json -Depth 10 + + $response = Invoke-RestMethod -Uri "https://api.openai.com/v1/chat/completions" -Method Post -Headers @{ + "Authorization" = "Bearer $Key" + "Content-Type" = "application/json" + } -Body $body + + return Normalize-Response -SelectedProvider $SelectedProvider -Response $response + } + + $body = @{ + model = $SelectedModel + messages = $MessageHistory + max_tokens = 4096 + tools = $ToolDefinitions + } | ConvertTo-Json -Depth 10 + + $response = Invoke-RestMethod -Uri "https://api.anthropic.com/v1/messages" -Method Post -Headers @{ + "x-api-key" = $Key + "anthropic-version" = "2023-06-01" + "Content-Type" = "application/json" + } -Body $body + + return $response + } + + function Append-ToolResults { + param( + [string]$SelectedProvider, + $MessageHistory, + $ToolResults + ) + + if ($SelectedProvider -eq "OpenAI") { + foreach ($toolResult in $ToolResults) { + $MessageHistory += @{ + role = "tool" + tool_call_id = $toolResult.id + content = $toolResult.content + } + } + return $MessageHistory + } + + $MessageHistory += @{ role = "user"; content = $ToolResults } + return $MessageHistory + } + + $apiKey = Get-ApiKey -SelectedProvider $Provider + if (-not $apiKey) { + Write-Host "Set ANTHROPIC_API_KEY or OPENAI_API_KEY for provider $Provider" + exit + } - Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] 🤖 Processing request..." + Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] 🤖 Processing request with $Provider..." $tools = @( @{ @@ -169,22 +303,12 @@ function Invoke-PSClaudeCode { Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] 🤖 Starting sub-agent for: $SubTask" $subMessages = @(@{ role = "user"; content = $SubTask }) $turns = 0 + $providerTools = Convert-ToolsForProvider -SelectedProvider $Provider -ToolDefinitions $tools while ($turns -lt $MaxTurns) { $turns++ - Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] 🤖 Consulting Claude..." - $body = @{ - model = $Model - messages = $subMessages - max_tokens = 4096 - tools = $tools - } | ConvertTo-Json -Depth 10 - - $response = Invoke-RestMethod -Uri "https://api.anthropic.com/v1/messages" -Method Post -Headers @{ - "x-api-key" = $apiKey - "anthropic-version" = "2023-06-01" - "Content-Type" = "application/json" - } -Body $body + Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] 🤖 Consulting model..." + $response = Invoke-ModelRequest -SelectedProvider $Provider -SelectedModel $Model -MessageHistory $subMessages -ToolDefinitions $providerTools -Key $apiKey Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] 🤖 Response received, analyzing..." @@ -211,13 +335,13 @@ function Invoke-PSClaudeCode { } $toolResults += @{ + id = $toolUse.id + content = $result type = "tool_result" tool_use_id = $toolUse.id - content = $result } } - $userMessage = @{ role = "user"; content = $toolResults } - $subMessages += $userMessage + $subMessages = Append-ToolResults -SelectedProvider $Provider -MessageHistory $subMessages -ToolResults $toolResults } else { $textContent = ($response.content | Where-Object { $_.type -eq "text" } | ForEach-Object { $_.text }) -join "" @@ -251,24 +375,15 @@ function Invoke-PSClaudeCode { $messages = @(@{ role = "user"; content = $Task }) + $providerTools = Convert-ToolsForProvider -SelectedProvider $Provider -ToolDefinitions $tools + while ($true) { if ($messages.Count -gt 1) { Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] 🤖 Continuing analysis..." } - Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] 🤖 Consulting Claude..." - $body = @{ - model = $Model - messages = $messages - max_tokens = 4096 - tools = $tools - } | ConvertTo-Json -Depth 10 - - $response = Invoke-RestMethod -Uri "https://api.anthropic.com/v1/messages" -Method Post -Headers @{ - "x-api-key" = $apiKey - "anthropic-version" = "2023-06-01" - "Content-Type" = "application/json" - } -Body $body + Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] 🤖 Consulting model..." + $response = Invoke-ModelRequest -SelectedProvider $Provider -SelectedModel $Model -MessageHistory $messages -ToolDefinitions $providerTools -Key $apiKey Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] 🤖 Response received, analyzing..." @@ -295,13 +410,13 @@ function Invoke-PSClaudeCode { } $toolResults += @{ + id = $toolUse.id + content = $result type = "tool_result" tool_use_id = $toolUse.id - content = $result } } - $userMessage = @{ role = "user"; content = $toolResults } - $messages += $userMessage + $messages = Append-ToolResults -SelectedProvider $Provider -MessageHistory $messages -ToolResults $toolResults } else { $textContent = ($response.content | Where-Object { $_.type -eq "text" } | ForEach-Object { $_.text }) -join "" @@ -310,4 +425,4 @@ function Invoke-PSClaudeCode { } } } -} \ No newline at end of file +} diff --git a/README.md b/README.md index 7fb188a..113149a 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Ever wondered how AI agents like Claude Code work their magic? Dive into this PowerShell implementation and build your own intelligent assistant from scratch! -Inspired by the original [Claude Code article](https://x.com/dabit3/status/2009668398691582315?s=20), this project demonstrates how to create a PowerShell AI agent using Anthropic's Claude API. Start with a simple command runner and evolve it into a sophisticated agent with function calling, file operations, and conversational capabilities. +Inspired by the original [Claude Code article](https://x.com/dabit3/status/2009668398691582315?s=20), this project demonstrates how to create a PowerShell AI agent using Anthropic or OpenAI-compatible APIs. Start with a simple command runner and evolve it into a sophisticated agent with function calling, file operations, and conversational capabilities. # In Action @@ -65,9 +65,9 @@ The examples above highlight how the agent can handle complex, multi-step tasks - **Agent Loop**: Iterative task completion with AI-driven decision making - **Structured Tools**: Function calling for file operations (read/write) and command execution - **Permission Checks**: Safe operations with user confirmation for dangerous actions -- **Model Selection**: Configurable Claude model selection via parameters +- **Model Selection**: Configurable model selection via parameters - **Sub-agent Support**: Delegates complex tasks to specialized sub-agents -- **PowerShell Native**: Built entirely in PowerShell, compatible with Anthropic Claude API +- **PowerShell Native**: Built entirely in PowerShell, compatible with Anthropic and OpenAI APIs - **Progressive Complexity**: Three agent versions showing evolution from simple to advanced - **Comment-Based Help**: Full PowerShell help documentation with `Get-Help Invoke-PSClaudeCode` - **Pipeline Input Support**: Accept context via pipeline for enhanced task descriptions @@ -75,7 +75,7 @@ The examples above highlight how the agent can handle complex, multi-step tasks ## Prerequisites - PowerShell 5.1 or higher -- Anthropic API key (set as environment variable `$env:ANTHROPIC_API_KEY`) +- Anthropic API key (`$env:ANTHROPIC_API_KEY`) or OpenAI API key (`$env:OPENAI_API_KEY`) ## Installation 1. Clone the repository: @@ -84,9 +84,11 @@ The examples above highlight how the agent can handle complex, multi-step tasks cd PSClaudeCode ``` -2. Set your Anthropic API key: +2. Set your API key: ```powershell $env:ANTHROPIC_API_KEY = "your-api-key-here" + # or + $env:OPENAI_API_KEY = "your-api-key-here" ``` 3. Import the module: @@ -125,7 +127,8 @@ Get-Help Invoke-PSClaudeCode -Parameter Task ### Parameters - **`-Task`**: The task description for the AI agent to complete (required) -- **`-Model`**: The Claude model to use (optional, defaults to "claude-sonnet-4-5-20250929") +- **`-Model`**: The model to use (optional, defaults to "claude-sonnet-4-5-20250929") +- **`-Provider`**: The LLM provider to use (`Anthropic` or `OpenAI`, defaults to `Anthropic`) - **`-dangerouslySkipPermissions`**: Switch to bypass user confirmation prompts for dangerous operations (use with caution) - **Pipeline Input**: Accepts pipeline input as additional context for the task @@ -135,7 +138,7 @@ Get-Help Invoke-PSClaudeCode -Parameter Task Invoke-PSClaudeCode -Task "Create a new file called 'test.txt' with 'Hello, World!'" # Specify a different model -Invoke-PSClaudeCode -Task "List all files in the current directory" -Model "claude-3-5-sonnet-20241022" +Invoke-PSClaudeCode -Task "List all files in the current directory" -Model "claude-3-5-sonnet-20241022" -Provider Anthropic # Bypass permission checks (use with caution) Invoke-PSClaudeCode -Task "Delete all .tmp files in the current directory" -dangerouslySkipPermissions @@ -180,7 +183,10 @@ Get-ChildItem "*.json" | Get-Content | Invoke-PSClaudeCode -Task "Compare these ### Using Different Models ```powershell # Use Claude 3.5 Sonnet -Invoke-PSClaudeCode -Task "Analyze the PowerShell scripts in this directory" -Model "claude-3-5-sonnet-20241022" +Invoke-PSClaudeCode -Task "Analyze the PowerShell scripts in this directory" -Model "claude-3-5-sonnet-20241022" -Provider Anthropic + +# Use OpenAI +Invoke-PSClaudeCode -Task "Summarize the README" -Model "gpt-4o-mini" -Provider OpenAI # Use the latest Claude Sonnet (default) Invoke-PSClaudeCode -Task "Create a summary of all .md files in the repository" From 4051f6f6668c1c5af8e6b40544236f195623f5eb Mon Sep 17 00:00:00 2001 From: Doug Finke Date: Sat, 7 Feb 2026 07:04:22 -0500 Subject: [PATCH 02/16] Update tests for provider support --- Tests/Invoke-PSClaudeCode.Tests.ps1 | 45 ++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/Tests/Invoke-PSClaudeCode.Tests.ps1 b/Tests/Invoke-PSClaudeCode.Tests.ps1 index 05eccaa..9c8c098 100644 --- a/Tests/Invoke-PSClaudeCode.Tests.ps1 +++ b/Tests/Invoke-PSClaudeCode.Tests.ps1 @@ -3,8 +3,9 @@ BeforeAll { $ModulePath = Join-Path (Split-Path $PSScriptRoot -Parent) 'PSClaudeCode.psd1' Import-Module $ModulePath -Force - # Store original API key + # Store original API keys $script:OriginalApiKey = $env:ANTHROPIC_API_KEY + $script:OriginalOpenAiKey = $env:OPENAI_API_KEY # Load function content once for all tests $script:FunctionPath = Join-Path (Join-Path (Split-Path $PSScriptRoot -Parent) 'Public') 'Invoke-PSClaudeCode.ps1' @@ -12,8 +13,9 @@ BeforeAll { } AfterAll { - # Restore original API key + # Restore original API keys $env:ANTHROPIC_API_KEY = $script:OriginalApiKey + $env:OPENAI_API_KEY = $script:OriginalOpenAiKey } Describe 'Invoke-PSClaudeCode' { @@ -35,6 +37,14 @@ Describe 'Invoke-PSClaudeCode' { $command.Parameters['dangerouslySkipPermissions'].SwitchParameter | Should -Be $true } + It 'Should have a Provider parameter with ValidateSet values' { + $command = Get-Command Invoke-PSClaudeCode + $command.Parameters.Keys | Should -Contain 'Provider' + $validateSet = $command.Parameters['Provider'].Attributes | Where-Object { $_.TypeId.Name -eq 'ValidateSetAttribute' } + $validateSet.ValidValues | Should -Contain 'Anthropic' + $validateSet.ValidValues | Should -Contain 'OpenAI' + } + It 'Should have an InputObject parameter that accepts pipeline input' { $command = Get-Command Invoke-PSClaudeCode $command.Parameters.Keys | Should -Contain 'InputObject' @@ -46,19 +56,22 @@ Describe 'Invoke-PSClaudeCode' { Context 'API Key Validation' { BeforeEach { - # Clear API key for these tests + # Clear API keys for these tests $env:ANTHROPIC_API_KEY = $null + $env:OPENAI_API_KEY = $null } AfterEach { - # Restore API key + # Restore API keys $env:ANTHROPIC_API_KEY = $script:OriginalApiKey + $env:OPENAI_API_KEY = $script:OriginalOpenAiKey } - It 'Should validate API key is set' { + It 'Should validate API keys are set' { # We can't directly test the exit behavior in Pester easily, # but we can verify the function requires the environment variable $env:ANTHROPIC_API_KEY | Should -BeNullOrEmpty + $env:OPENAI_API_KEY | Should -BeNullOrEmpty } } @@ -137,8 +150,9 @@ Describe 'Invoke-PSClaudeCode' { $script:FunctionContent | Should -Match 'MaxTurns.*=.*10' } - It 'Should call Anthropic API in sub-agent' { + It 'Should call supported API endpoints in sub-agent' { $script:FunctionContent | Should -Match 'api\.anthropic\.com/v1/messages' + $script:FunctionContent | Should -Match 'api\.openai\.com/v1/chat/completions' } } @@ -147,13 +161,15 @@ Describe 'Invoke-PSClaudeCode' { $script:FunctionContent | Should -Match 'Model\s*=\s*"claude-sonnet-4-5-20250929"' } - It 'Should call Anthropic API endpoint' { + It 'Should call supported API endpoints' { $script:FunctionContent | Should -Match 'https://api\.anthropic\.com/v1/messages' + $script:FunctionContent | Should -Match 'https://api\.openai\.com/v1/chat/completions' } It 'Should set correct API headers' { $script:FunctionContent | Should -Match 'x-api-key' $script:FunctionContent | Should -Match 'anthropic-version' + $script:FunctionContent | Should -Match 'Authorization' } It 'Should process tool uses from API response' { @@ -218,13 +234,13 @@ Describe 'Invoke-PSClaudeCode' { It 'Should have DESCRIPTION section' { $help = Get-Help Invoke-PSClaudeCode $help.Description | Should -Not -BeNullOrEmpty - $help.Description.Text | Should -Match 'Anthropic.*Claude' + $help.Description.Text | Should -Match 'Anthropic|OpenAI' } It 'Should have PARAMETERS section' { $help = Get-Help Invoke-PSClaudeCode $help.Parameters | Should -Not -BeNullOrEmpty - $help.Parameters.Parameter | Should -HaveCount 4 # Task, InputObject, Model, dangerouslySkipPermissions + $help.Parameters.Parameter | Should -HaveCount 5 # Task, InputObject, Model, Provider, dangerouslySkipPermissions } It 'Should have parameter help for Task' { @@ -238,7 +254,14 @@ Describe 'Invoke-PSClaudeCode' { $help = Get-Help Invoke-PSClaudeCode $modelParam = $help.Parameters.Parameter | Where-Object { $_.Name -eq 'Model' } $modelParam | Should -Not -BeNullOrEmpty - $modelParam.Description.Text | Should -Match 'Claude model' + $modelParam.Description.Text | Should -Match 'model' + } + + It 'Should have parameter help for Provider' { + $help = Get-Help Invoke-PSClaudeCode + $providerParam = $help.Parameters.Parameter | Where-Object { $_.Name -eq 'Provider' } + $providerParam | Should -Not -BeNullOrEmpty + $providerParam.Description.Text | Should -Match 'provider' } It 'Should have parameter help for dangerouslySkipPermissions' { @@ -257,7 +280,7 @@ Describe 'Invoke-PSClaudeCode' { It 'Should have NOTES section' { $help = Get-Help Invoke-PSClaudeCode $help.AlertSet | Should -Not -BeNullOrEmpty - $help.AlertSet.Alert.Text | Should -Match 'ANTHROPIC_API_KEY' + $help.AlertSet.Alert.Text | Should -Match 'ANTHROPIC_API_KEY|OPENAI_API_KEY' } } } From e58ca0f4e2942104503c69e908c81c918f23d662 Mon Sep 17 00:00:00 2001 From: Doug Finke Date: Sat, 7 Feb 2026 08:00:36 -0500 Subject: [PATCH 03/16] Fix OpenAI tool call handling --- Public/Invoke-PSClaudeCode.ps1 | 31 ++++++++++++++++++++++++++--- Tests/Invoke-PSClaudeCode.Tests.ps1 | 4 ++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/Public/Invoke-PSClaudeCode.ps1 b/Public/Invoke-PSClaudeCode.ps1 index 9f5aed3..2896418 100644 --- a/Public/Invoke-PSClaudeCode.ps1 +++ b/Public/Invoke-PSClaudeCode.ps1 @@ -107,6 +107,10 @@ function Invoke-PSClaudeCode { param([string]$SelectedProvider, $Response) if ($SelectedProvider -eq "OpenAI") { + if (-not $Response.choices) { + return @{ content = @(); rawMessage = $null } + } + $message = $Response.choices[0].message $contentItems = @() @@ -130,7 +134,10 @@ function Invoke-PSClaudeCode { } } - return @{ content = $contentItems } + return @{ + content = $contentItems + rawMessage = $message + } } return $Response @@ -312,7 +319,16 @@ function Invoke-PSClaudeCode { Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] 🤖 Response received, analyzing..." - $assistantMessage = @{ role = "assistant"; content = $response.content } + if ($Provider -eq "OpenAI") { + $assistantMessage = @{ + role = "assistant" + content = $response.rawMessage.content + tool_calls = $response.rawMessage.tool_calls + } + } + else { + $assistantMessage = @{ role = "assistant"; content = $response.content } + } $subMessages += $assistantMessage $toolUses = $response.content | Where-Object { $_.type -eq "tool_use" } @@ -387,7 +403,16 @@ function Invoke-PSClaudeCode { Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] 🤖 Response received, analyzing..." - $assistantMessage = @{ role = "assistant"; content = $response.content } + if ($Provider -eq "OpenAI") { + $assistantMessage = @{ + role = "assistant" + content = $response.rawMessage.content + tool_calls = $response.rawMessage.tool_calls + } + } + else { + $assistantMessage = @{ role = "assistant"; content = $response.content } + } $messages += $assistantMessage $toolUses = $response.content | Where-Object { $_.type -eq "tool_use" } diff --git a/Tests/Invoke-PSClaudeCode.Tests.ps1 b/Tests/Invoke-PSClaudeCode.Tests.ps1 index 9c8c098..5a50008 100644 --- a/Tests/Invoke-PSClaudeCode.Tests.ps1 +++ b/Tests/Invoke-PSClaudeCode.Tests.ps1 @@ -176,6 +176,10 @@ Describe 'Invoke-PSClaudeCode' { $script:FunctionContent | Should -Match 'tool_use' } + It 'Should handle OpenAI tool calls in responses' { + $script:FunctionContent | Should -Match 'tool_calls' + } + It 'Should handle text content in responses' { $script:FunctionContent | Should -Match 'type.*-eq.*"text"' } From 99f1602db2f61c9cf6230b45b98bfc5deb0e2dbb Mon Sep 17 00:00:00 2001 From: Doug Finke Date: Sat, 7 Feb 2026 08:21:05 -0500 Subject: [PATCH 04/16] Handle OpenAI chat model constraints --- Public/Invoke-PSClaudeCode.ps1 | 46 ++++++++++++++++++++++------- Tests/Invoke-PSClaudeCode.Tests.ps1 | 4 +++ 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/Public/Invoke-PSClaudeCode.ps1 b/Public/Invoke-PSClaudeCode.ps1 index 2896418..21e868b 100644 --- a/Public/Invoke-PSClaudeCode.ps1 +++ b/Public/Invoke-PSClaudeCode.ps1 @@ -153,18 +153,29 @@ function Invoke-PSClaudeCode { ) if ($SelectedProvider -eq "OpenAI") { + if ($SelectedModel -match "codex") { + return @{ + error = "OpenAI model '$SelectedModel' does not support the chat completions endpoint. Use a chat-capable model (for example, gpt-4.1 or gpt-5.2-chat-latest)." + } + } + $body = @{ model = $SelectedModel messages = $MessageHistory - max_tokens = 4096 + max_completion_tokens = 4096 tools = $ToolDefinitions tool_choice = "auto" } | ConvertTo-Json -Depth 10 - $response = Invoke-RestMethod -Uri "https://api.openai.com/v1/chat/completions" -Method Post -Headers @{ - "Authorization" = "Bearer $Key" - "Content-Type" = "application/json" - } -Body $body + try { + $response = Invoke-RestMethod -Uri "https://api.openai.com/v1/chat/completions" -Method Post -Headers @{ + "Authorization" = "Bearer $Key" + "Content-Type" = "application/json" + } -Body $body -ErrorAction Stop + } + catch { + return @{ error = $_.Exception.Message } + } return Normalize-Response -SelectedProvider $SelectedProvider -Response $response } @@ -176,11 +187,16 @@ function Invoke-PSClaudeCode { tools = $ToolDefinitions } | ConvertTo-Json -Depth 10 - $response = Invoke-RestMethod -Uri "https://api.anthropic.com/v1/messages" -Method Post -Headers @{ - "x-api-key" = $Key - "anthropic-version" = "2023-06-01" - "Content-Type" = "application/json" - } -Body $body + try { + $response = Invoke-RestMethod -Uri "https://api.anthropic.com/v1/messages" -Method Post -Headers @{ + "x-api-key" = $Key + "anthropic-version" = "2023-06-01" + "Content-Type" = "application/json" + } -Body $body -ErrorAction Stop + } + catch { + return @{ error = $_.Exception.Message } + } return $response } @@ -317,6 +333,11 @@ function Invoke-PSClaudeCode { Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] 🤖 Consulting model..." $response = Invoke-ModelRequest -SelectedProvider $Provider -SelectedModel $Model -MessageHistory $subMessages -ToolDefinitions $providerTools -Key $apiKey + if ($response.error) { + Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] 🚫 $($response.error)" + return "Sub-agent failed: $($response.error)" + } + Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] 🤖 Response received, analyzing..." if ($Provider -eq "OpenAI") { @@ -401,6 +422,11 @@ function Invoke-PSClaudeCode { Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] 🤖 Consulting model..." $response = Invoke-ModelRequest -SelectedProvider $Provider -SelectedModel $Model -MessageHistory $messages -ToolDefinitions $providerTools -Key $apiKey + if ($response.error) { + Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] 🚫 $($response.error)" + break + } + Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] 🤖 Response received, analyzing..." if ($Provider -eq "OpenAI") { diff --git a/Tests/Invoke-PSClaudeCode.Tests.ps1 b/Tests/Invoke-PSClaudeCode.Tests.ps1 index 5a50008..ee17239 100644 --- a/Tests/Invoke-PSClaudeCode.Tests.ps1 +++ b/Tests/Invoke-PSClaudeCode.Tests.ps1 @@ -172,6 +172,10 @@ Describe 'Invoke-PSClaudeCode' { $script:FunctionContent | Should -Match 'Authorization' } + It 'Should use max_completion_tokens for OpenAI requests' { + $script:FunctionContent | Should -Match 'max_completion_tokens' + } + It 'Should process tool uses from API response' { $script:FunctionContent | Should -Match 'tool_use' } From 3b904c8702fac4b6eb1c7a4a94784abcfbbdbe98 Mon Sep 17 00:00:00 2001 From: Doug Finke Date: Sat, 7 Feb 2026 08:29:26 -0500 Subject: [PATCH 05/16] Use OpenAI responses endpoint --- Public/Invoke-PSClaudeCode.ps1 | 108 ++++++++++++++++++++-------- README.md | 4 +- Tests/Invoke-PSClaudeCode.Tests.ps1 | 8 +-- 3 files changed, 85 insertions(+), 35 deletions(-) diff --git a/Public/Invoke-PSClaudeCode.ps1 b/Public/Invoke-PSClaudeCode.ps1 index 21e868b..aa7a728 100644 --- a/Public/Invoke-PSClaudeCode.ps1 +++ b/Public/Invoke-PSClaudeCode.ps1 @@ -91,11 +91,9 @@ function Invoke-PSClaudeCode { return $ToolDefinitions | ForEach-Object { @{ type = "function" - function = @{ - name = $_.name - description = $_.description - parameters = $_.input_schema - } + name = $_.name + description = $_.description + parameters = $_.input_schema } } } @@ -103,40 +101,98 @@ function Invoke-PSClaudeCode { return $ToolDefinitions } + function Convert-OpenAIInput { + param($MessageHistory) + + $inputs = @() + foreach ($message in $MessageHistory) { + $role = $message.role + $contentValue = $message.content + + if ($contentValue -is [System.Array]) { + $contentValue = ($contentValue | Where-Object { $_.type -eq "text" } | ForEach-Object { $_.text }) -join "" + } + + $inputMessage = @{ + role = $role + content = @(@{ + type = "text" + text = "$contentValue" + }) + } + + if ($message.tool_call_id) { + $inputMessage.tool_call_id = $message.tool_call_id + } + + if ($message.tool_calls) { + $inputMessage.tool_calls = $message.tool_calls + } + + $inputs += $inputMessage + } + + return $inputs + } + function Normalize-Response { param([string]$SelectedProvider, $Response) if ($SelectedProvider -eq "OpenAI") { - if (-not $Response.choices) { + if (-not $Response.output) { return @{ content = @(); rawMessage = $null } } - $message = $Response.choices[0].message $contentItems = @() - - if ($message.tool_calls) { - foreach ($toolCall in $message.tool_calls) { - $arguments = $toolCall.function.arguments - $inputObject = if ([string]::IsNullOrWhiteSpace($arguments)) { @{} } else { $arguments | ConvertFrom-Json } - $contentItems += @{ - type = "tool_use" - id = $toolCall.id - name = $toolCall.function.name - input = $inputObject + $toolCalls = @() + + $outputMessages = $Response.output | Where-Object { $_.type -eq "message" } + $outputTextItems = $Response.output | Where-Object { $_.type -eq "output_text" } + $toolCallItems = $Response.output | Where-Object { $_.type -eq "tool_call" } + + foreach ($outputMessage in $outputMessages) { + foreach ($content in $outputMessage.content) { + if ($content.type -eq "output_text") { + $contentItems += @{ + type = "text" + text = $content.text + } } } } - if ($message.content) { + foreach ($outputTextItem in $outputTextItems) { $contentItems += @{ type = "text" - text = $message.content + text = $outputTextItem.text + } + } + + foreach ($toolCall in $toolCallItems) { + $arguments = $toolCall.arguments + $inputObject = if ([string]::IsNullOrWhiteSpace($arguments)) { @{} } else { $arguments | ConvertFrom-Json } + $contentItems += @{ + type = "tool_use" + id = $toolCall.call_id + name = $toolCall.name + input = $inputObject + } + $toolCalls += @{ + id = $toolCall.call_id + type = "function" + function = @{ + name = $toolCall.name + arguments = $arguments + } } } return @{ content = $contentItems - rawMessage = $message + rawMessage = @{ + content = ($contentItems | Where-Object { $_.type -eq "text" } | ForEach-Object { $_.text }) -join "" + tool_calls = $toolCalls + } } } @@ -153,22 +209,16 @@ function Invoke-PSClaudeCode { ) if ($SelectedProvider -eq "OpenAI") { - if ($SelectedModel -match "codex") { - return @{ - error = "OpenAI model '$SelectedModel' does not support the chat completions endpoint. Use a chat-capable model (for example, gpt-4.1 or gpt-5.2-chat-latest)." - } - } - $body = @{ model = $SelectedModel - messages = $MessageHistory - max_completion_tokens = 4096 + input = Convert-OpenAIInput -MessageHistory $MessageHistory + max_output_tokens = 4096 tools = $ToolDefinitions tool_choice = "auto" } | ConvertTo-Json -Depth 10 try { - $response = Invoke-RestMethod -Uri "https://api.openai.com/v1/chat/completions" -Method Post -Headers @{ + $response = Invoke-RestMethod -Uri "https://api.openai.com/v1/responses" -Method Post -Headers @{ "Authorization" = "Bearer $Key" "Content-Type" = "application/json" } -Body $body -ErrorAction Stop diff --git a/README.md b/README.md index 113149a..5632b83 100644 --- a/README.md +++ b/README.md @@ -185,8 +185,8 @@ Get-ChildItem "*.json" | Get-Content | Invoke-PSClaudeCode -Task "Compare these # Use Claude 3.5 Sonnet Invoke-PSClaudeCode -Task "Analyze the PowerShell scripts in this directory" -Model "claude-3-5-sonnet-20241022" -Provider Anthropic -# Use OpenAI -Invoke-PSClaudeCode -Task "Summarize the README" -Model "gpt-4o-mini" -Provider OpenAI +# Use OpenAI (Responses API) +Invoke-PSClaudeCode -Task "Summarize the README" -Model "gpt-4.1" -Provider OpenAI # Use the latest Claude Sonnet (default) Invoke-PSClaudeCode -Task "Create a summary of all .md files in the repository" diff --git a/Tests/Invoke-PSClaudeCode.Tests.ps1 b/Tests/Invoke-PSClaudeCode.Tests.ps1 index ee17239..1d934bc 100644 --- a/Tests/Invoke-PSClaudeCode.Tests.ps1 +++ b/Tests/Invoke-PSClaudeCode.Tests.ps1 @@ -152,7 +152,7 @@ Describe 'Invoke-PSClaudeCode' { It 'Should call supported API endpoints in sub-agent' { $script:FunctionContent | Should -Match 'api\.anthropic\.com/v1/messages' - $script:FunctionContent | Should -Match 'api\.openai\.com/v1/chat/completions' + $script:FunctionContent | Should -Match 'api\.openai\.com/v1/responses' } } @@ -163,7 +163,7 @@ Describe 'Invoke-PSClaudeCode' { It 'Should call supported API endpoints' { $script:FunctionContent | Should -Match 'https://api\.anthropic\.com/v1/messages' - $script:FunctionContent | Should -Match 'https://api\.openai\.com/v1/chat/completions' + $script:FunctionContent | Should -Match 'https://api\.openai\.com/v1/responses' } It 'Should set correct API headers' { @@ -172,8 +172,8 @@ Describe 'Invoke-PSClaudeCode' { $script:FunctionContent | Should -Match 'Authorization' } - It 'Should use max_completion_tokens for OpenAI requests' { - $script:FunctionContent | Should -Match 'max_completion_tokens' + It 'Should use max_output_tokens for OpenAI requests' { + $script:FunctionContent | Should -Match 'max_output_tokens' } It 'Should process tool uses from API response' { From 02f1213c8be5f24f4d346543c4569463f59a4069 Mon Sep 17 00:00:00 2001 From: Doug Finke Date: Sat, 7 Feb 2026 08:32:35 -0500 Subject: [PATCH 06/16] Fix OpenAI responses input formatting --- Public/Invoke-PSClaudeCode.ps1 | 30 ++++++++++++++++------------- Tests/Invoke-PSClaudeCode.Tests.ps1 | 4 ++++ 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/Public/Invoke-PSClaudeCode.ps1 b/Public/Invoke-PSClaudeCode.ps1 index aa7a728..75f0ae2 100644 --- a/Public/Invoke-PSClaudeCode.ps1 +++ b/Public/Invoke-PSClaudeCode.ps1 @@ -113,20 +113,24 @@ function Invoke-PSClaudeCode { $contentValue = ($contentValue | Where-Object { $_.type -eq "text" } | ForEach-Object { $_.text }) -join "" } - $inputMessage = @{ - role = $role - content = @(@{ - type = "text" - text = "$contentValue" - }) - } - - if ($message.tool_call_id) { - $inputMessage.tool_call_id = $message.tool_call_id + if ($role -eq "tool") { + $inputMessage = @{ + role = $role + content = @(@{ + type = "tool_result" + tool_call_id = $message.tool_call_id + output = "$contentValue" + }) + } } - - if ($message.tool_calls) { - $inputMessage.tool_calls = $message.tool_calls + else { + $inputMessage = @{ + role = $role + content = @(@{ + type = "input_text" + text = "$contentValue" + }) + } } $inputs += $inputMessage diff --git a/Tests/Invoke-PSClaudeCode.Tests.ps1 b/Tests/Invoke-PSClaudeCode.Tests.ps1 index 1d934bc..0f5ca09 100644 --- a/Tests/Invoke-PSClaudeCode.Tests.ps1 +++ b/Tests/Invoke-PSClaudeCode.Tests.ps1 @@ -176,6 +176,10 @@ Describe 'Invoke-PSClaudeCode' { $script:FunctionContent | Should -Match 'max_output_tokens' } + It 'Should format OpenAI input as input_text items' { + $script:FunctionContent | Should -Match 'input_text' + } + It 'Should process tool uses from API response' { $script:FunctionContent | Should -Match 'tool_use' } From 04003e7b093e80eb81e1da29f465a053896ccd58 Mon Sep 17 00:00:00 2001 From: Doug Finke Date: Sat, 7 Feb 2026 08:38:07 -0500 Subject: [PATCH 07/16] Fix OpenAI tool schema for responses --- Public/Invoke-PSClaudeCode.ps1 | 8 +++++--- Tests/Invoke-PSClaudeCode.Tests.ps1 | 5 +++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/Public/Invoke-PSClaudeCode.ps1 b/Public/Invoke-PSClaudeCode.ps1 index 75f0ae2..fa3459b 100644 --- a/Public/Invoke-PSClaudeCode.ps1 +++ b/Public/Invoke-PSClaudeCode.ps1 @@ -91,9 +91,11 @@ function Invoke-PSClaudeCode { return $ToolDefinitions | ForEach-Object { @{ type = "function" - name = $_.name - description = $_.description - parameters = $_.input_schema + function = @{ + name = $_.name + description = $_.description + parameters = $_.input_schema + } } } } diff --git a/Tests/Invoke-PSClaudeCode.Tests.ps1 b/Tests/Invoke-PSClaudeCode.Tests.ps1 index 0f5ca09..13d1f7a 100644 --- a/Tests/Invoke-PSClaudeCode.Tests.ps1 +++ b/Tests/Invoke-PSClaudeCode.Tests.ps1 @@ -188,6 +188,11 @@ Describe 'Invoke-PSClaudeCode' { $script:FunctionContent | Should -Match 'tool_calls' } + It 'Should format OpenAI tools with function schema' { + $script:FunctionContent | Should -Match 'type\\s*=\\s*"function"' + $script:FunctionContent | Should -Match 'function\\s*=\\s*@\\{' + } + It 'Should handle text content in responses' { $script:FunctionContent | Should -Match 'type.*-eq.*"text"' } From fdcd16e049674cf769878e79ad1f20eb00e7b87e Mon Sep 17 00:00:00 2001 From: Doug Finke Date: Sat, 7 Feb 2026 09:05:00 -0500 Subject: [PATCH 08/16] Adjust OpenAI responses input roles --- Public/Invoke-PSClaudeCode.ps1 | 9 +++++++++ Tests/Invoke-PSClaudeCode.Tests.ps1 | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/Public/Invoke-PSClaudeCode.ps1 b/Public/Invoke-PSClaudeCode.ps1 index fa3459b..e1e659e 100644 --- a/Public/Invoke-PSClaudeCode.ps1 +++ b/Public/Invoke-PSClaudeCode.ps1 @@ -125,6 +125,15 @@ function Invoke-PSClaudeCode { }) } } + elseif ($role -eq "assistant") { + $inputMessage = @{ + role = $role + content = @(@{ + type = "output_text" + text = "$contentValue" + }) + } + } else { $inputMessage = @{ role = $role diff --git a/Tests/Invoke-PSClaudeCode.Tests.ps1 b/Tests/Invoke-PSClaudeCode.Tests.ps1 index 13d1f7a..f2717aa 100644 --- a/Tests/Invoke-PSClaudeCode.Tests.ps1 +++ b/Tests/Invoke-PSClaudeCode.Tests.ps1 @@ -180,6 +180,10 @@ Describe 'Invoke-PSClaudeCode' { $script:FunctionContent | Should -Match 'input_text' } + It 'Should format OpenAI assistant input as output_text items' { + $script:FunctionContent | Should -Match 'output_text' + } + It 'Should process tool uses from API response' { $script:FunctionContent | Should -Match 'tool_use' } From 079be6d5a62fd5f783c6d2c79ad9c128bb349720 Mon Sep 17 00:00:00 2001 From: Doug Finke Date: Sat, 7 Feb 2026 15:45:48 -0500 Subject: [PATCH 09/16] Fix OpenAI tool result input format --- Public/Invoke-PSClaudeCode.ps1 | 9 +++------ Tests/Invoke-PSClaudeCode.Tests.ps1 | 4 ++++ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Public/Invoke-PSClaudeCode.ps1 b/Public/Invoke-PSClaudeCode.ps1 index e1e659e..97305c5 100644 --- a/Public/Invoke-PSClaudeCode.ps1 +++ b/Public/Invoke-PSClaudeCode.ps1 @@ -117,12 +117,9 @@ function Invoke-PSClaudeCode { if ($role -eq "tool") { $inputMessage = @{ - role = $role - content = @(@{ - type = "tool_result" - tool_call_id = $message.tool_call_id - output = "$contentValue" - }) + type = "tool_result" + tool_call_id = $message.tool_call_id + output = "$contentValue" } } elseif ($role -eq "assistant") { diff --git a/Tests/Invoke-PSClaudeCode.Tests.ps1 b/Tests/Invoke-PSClaudeCode.Tests.ps1 index f2717aa..b53b6e6 100644 --- a/Tests/Invoke-PSClaudeCode.Tests.ps1 +++ b/Tests/Invoke-PSClaudeCode.Tests.ps1 @@ -184,6 +184,10 @@ Describe 'Invoke-PSClaudeCode' { $script:FunctionContent | Should -Match 'output_text' } + It 'Should format OpenAI tool results as tool_result items' { + $script:FunctionContent | Should -Match 'tool_result' + } + It 'Should process tool uses from API response' { $script:FunctionContent | Should -Match 'tool_use' } From 23e22ebf7eb9c5635894e7a5c95609cc67bfb19e Mon Sep 17 00:00:00 2001 From: Doug Finke Date: Sat, 7 Feb 2026 15:54:17 -0500 Subject: [PATCH 10/16] Include API error response details --- Public/Invoke-PSClaudeCode.ps1 | 27 +++++++++++++++++++++++++-- Tests/Invoke-PSClaudeCode.Tests.ps1 | 4 ++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/Public/Invoke-PSClaudeCode.ps1 b/Public/Invoke-PSClaudeCode.ps1 index 97305c5..64ebd4d 100644 --- a/Public/Invoke-PSClaudeCode.ps1 +++ b/Public/Invoke-PSClaudeCode.ps1 @@ -220,6 +220,29 @@ function Invoke-PSClaudeCode { [string]$Key ) + function Get-ErrorDetails { + param($Exception) + + $message = $Exception.Message + if ($Exception.Response) { + try { + $stream = $Exception.Response.GetResponseStream() + if ($stream) { + $reader = New-Object System.IO.StreamReader($stream) + $responseBody = $reader.ReadToEnd() + if ($responseBody) { + $message = "$message`n$responseBody" + } + } + } + catch { + return $message + } + } + + return $message + } + if ($SelectedProvider -eq "OpenAI") { $body = @{ model = $SelectedModel @@ -236,7 +259,7 @@ function Invoke-PSClaudeCode { } -Body $body -ErrorAction Stop } catch { - return @{ error = $_.Exception.Message } + return @{ error = Get-ErrorDetails -Exception $_.Exception } } return Normalize-Response -SelectedProvider $SelectedProvider -Response $response @@ -257,7 +280,7 @@ function Invoke-PSClaudeCode { } -Body $body -ErrorAction Stop } catch { - return @{ error = $_.Exception.Message } + return @{ error = Get-ErrorDetails -Exception $_.Exception } } return $response diff --git a/Tests/Invoke-PSClaudeCode.Tests.ps1 b/Tests/Invoke-PSClaudeCode.Tests.ps1 index b53b6e6..a38675b 100644 --- a/Tests/Invoke-PSClaudeCode.Tests.ps1 +++ b/Tests/Invoke-PSClaudeCode.Tests.ps1 @@ -201,6 +201,10 @@ Describe 'Invoke-PSClaudeCode' { $script:FunctionContent | Should -Match 'function\\s*=\\s*@\\{' } + It 'Should extract API error details from response stream' { + $script:FunctionContent | Should -Match 'GetResponseStream' + } + It 'Should handle text content in responses' { $script:FunctionContent | Should -Match 'type.*-eq.*"text"' } From 055dd79d1678249f16e864d5f3a51146ff9b8fd9 Mon Sep 17 00:00:00 2001 From: Doug Finke Date: Sat, 7 Feb 2026 16:01:05 -0500 Subject: [PATCH 11/16] Surface API error details --- Public/Invoke-PSClaudeCode.ps1 | 16 ++++++++++------ Tests/Invoke-PSClaudeCode.Tests.ps1 | 4 ++++ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Public/Invoke-PSClaudeCode.ps1 b/Public/Invoke-PSClaudeCode.ps1 index 64ebd4d..da240dd 100644 --- a/Public/Invoke-PSClaudeCode.ps1 +++ b/Public/Invoke-PSClaudeCode.ps1 @@ -221,12 +221,16 @@ function Invoke-PSClaudeCode { ) function Get-ErrorDetails { - param($Exception) + param($ErrorRecord) - $message = $Exception.Message - if ($Exception.Response) { + $message = $ErrorRecord.Exception.Message + if ($ErrorRecord.ErrorDetails -and $ErrorRecord.ErrorDetails.Message) { + return "$message`n$($ErrorRecord.ErrorDetails.Message)" + } + + if ($ErrorRecord.Exception.Response) { try { - $stream = $Exception.Response.GetResponseStream() + $stream = $ErrorRecord.Exception.Response.GetResponseStream() if ($stream) { $reader = New-Object System.IO.StreamReader($stream) $responseBody = $reader.ReadToEnd() @@ -259,7 +263,7 @@ function Invoke-PSClaudeCode { } -Body $body -ErrorAction Stop } catch { - return @{ error = Get-ErrorDetails -Exception $_.Exception } + return @{ error = Get-ErrorDetails -ErrorRecord $_ } } return Normalize-Response -SelectedProvider $SelectedProvider -Response $response @@ -280,7 +284,7 @@ function Invoke-PSClaudeCode { } -Body $body -ErrorAction Stop } catch { - return @{ error = Get-ErrorDetails -Exception $_.Exception } + return @{ error = Get-ErrorDetails -ErrorRecord $_ } } return $response diff --git a/Tests/Invoke-PSClaudeCode.Tests.ps1 b/Tests/Invoke-PSClaudeCode.Tests.ps1 index a38675b..2c6e411 100644 --- a/Tests/Invoke-PSClaudeCode.Tests.ps1 +++ b/Tests/Invoke-PSClaudeCode.Tests.ps1 @@ -205,6 +205,10 @@ Describe 'Invoke-PSClaudeCode' { $script:FunctionContent | Should -Match 'GetResponseStream' } + It 'Should use ErrorDetails when present for API errors' { + $script:FunctionContent | Should -Match 'ErrorDetails' + } + It 'Should handle text content in responses' { $script:FunctionContent | Should -Match 'type.*-eq.*"text"' } From da72609a40023827dc259558a7d11358cb08c1ff Mon Sep 17 00:00:00 2001 From: Doug Finke Date: Sat, 7 Feb 2026 16:08:56 -0500 Subject: [PATCH 12/16] Fix OpenAI input array return --- Public/Invoke-PSClaudeCode.ps1 | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/Public/Invoke-PSClaudeCode.ps1 b/Public/Invoke-PSClaudeCode.ps1 index da240dd..9ff13aa 100644 --- a/Public/Invoke-PSClaudeCode.ps1 +++ b/Public/Invoke-PSClaudeCode.ps1 @@ -144,7 +144,7 @@ function Invoke-PSClaudeCode { $inputs += $inputMessage } - return $inputs + return ,$inputs } function Normalize-Response { @@ -248,9 +248,10 @@ function Invoke-PSClaudeCode { } if ($SelectedProvider -eq "OpenAI") { + $openAiInput = @(Convert-OpenAIInput -MessageHistory $MessageHistory) $body = @{ model = $SelectedModel - input = Convert-OpenAIInput -MessageHistory $MessageHistory + input = $openAiInput max_output_tokens = 4096 tools = $ToolDefinitions tool_choice = "auto" @@ -460,12 +461,15 @@ function Invoke-PSClaudeCode { Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] 🚫 $result" } - $toolResults += @{ - id = $toolUse.id + $toolResult = @{ content = $result type = "tool_result" tool_use_id = $toolUse.id } + if ($Provider -eq "OpenAI") { + $toolResult.id = $toolUse.id + } + $toolResults += $toolResult } $subMessages = Append-ToolResults -SelectedProvider $Provider -MessageHistory $subMessages -ToolResults $toolResults } @@ -549,12 +553,15 @@ function Invoke-PSClaudeCode { Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] 🚫 $result" } - $toolResults += @{ - id = $toolUse.id + $toolResult = @{ content = $result type = "tool_result" tool_use_id = $toolUse.id } + if ($Provider -eq "OpenAI") { + $toolResult.id = $toolUse.id + } + $toolResults += $toolResult } $messages = Append-ToolResults -SelectedProvider $Provider -MessageHistory $messages -ToolResults $toolResults } From 9b6c13c84262df32efb61e73e738992bd6e2833d Mon Sep 17 00:00:00 2001 From: Doug Finke Date: Sat, 7 Feb 2026 16:13:35 -0500 Subject: [PATCH 13/16] Fix OpenAI input array nesting --- Public/Invoke-PSClaudeCode.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Public/Invoke-PSClaudeCode.ps1 b/Public/Invoke-PSClaudeCode.ps1 index 9ff13aa..ebfd814 100644 --- a/Public/Invoke-PSClaudeCode.ps1 +++ b/Public/Invoke-PSClaudeCode.ps1 @@ -144,7 +144,7 @@ function Invoke-PSClaudeCode { $inputs += $inputMessage } - return ,$inputs + return $inputs } function Normalize-Response { @@ -248,7 +248,7 @@ function Invoke-PSClaudeCode { } if ($SelectedProvider -eq "OpenAI") { - $openAiInput = @(Convert-OpenAIInput -MessageHistory $MessageHistory) + $openAiInput = Convert-OpenAIInput -MessageHistory $MessageHistory $body = @{ model = $SelectedModel input = $openAiInput From b2006b7894fef2ea356edc985324bd532fe194f9 Mon Sep 17 00:00:00 2001 From: Doug Finke Date: Sat, 7 Feb 2026 16:19:13 -0500 Subject: [PATCH 14/16] Align OpenAI tool schema with responses --- Public/Invoke-PSClaudeCode.ps1 | 8 +++----- Tests/Invoke-PSClaudeCode.Tests.ps1 | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Public/Invoke-PSClaudeCode.ps1 b/Public/Invoke-PSClaudeCode.ps1 index ebfd814..4982de6 100644 --- a/Public/Invoke-PSClaudeCode.ps1 +++ b/Public/Invoke-PSClaudeCode.ps1 @@ -91,11 +91,9 @@ function Invoke-PSClaudeCode { return $ToolDefinitions | ForEach-Object { @{ type = "function" - function = @{ - name = $_.name - description = $_.description - parameters = $_.input_schema - } + name = $_.name + description = $_.description + parameters = $_.input_schema } } } diff --git a/Tests/Invoke-PSClaudeCode.Tests.ps1 b/Tests/Invoke-PSClaudeCode.Tests.ps1 index 2c6e411..653f8e4 100644 --- a/Tests/Invoke-PSClaudeCode.Tests.ps1 +++ b/Tests/Invoke-PSClaudeCode.Tests.ps1 @@ -198,7 +198,7 @@ Describe 'Invoke-PSClaudeCode' { It 'Should format OpenAI tools with function schema' { $script:FunctionContent | Should -Match 'type\\s*=\\s*"function"' - $script:FunctionContent | Should -Match 'function\\s*=\\s*@\\{' + $script:FunctionContent | Should -Match 'name\\s*=\\s*\\$_\\.name' } It 'Should extract API error details from response stream' { From 8b5d92668a1163a8a0248ca00d30d8c3294e062d Mon Sep 17 00:00:00 2001 From: Doug Finke Date: Sat, 7 Feb 2026 16:49:43 -0500 Subject: [PATCH 15/16] Update OpenAI tool schema test --- Tests/Invoke-PSClaudeCode.Tests.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/Invoke-PSClaudeCode.Tests.ps1 b/Tests/Invoke-PSClaudeCode.Tests.ps1 index 653f8e4..0067169 100644 --- a/Tests/Invoke-PSClaudeCode.Tests.ps1 +++ b/Tests/Invoke-PSClaudeCode.Tests.ps1 @@ -199,6 +199,8 @@ Describe 'Invoke-PSClaudeCode' { It 'Should format OpenAI tools with function schema' { $script:FunctionContent | Should -Match 'type\\s*=\\s*"function"' $script:FunctionContent | Should -Match 'name\\s*=\\s*\\$_\\.name' + $script:FunctionContent | Should -Match 'description\\s*=\\s*\\$_\\.description' + $script:FunctionContent | Should -Match 'parameters\\s*=\\s*\\$_\\.input_schema' } It 'Should extract API error details from response stream' { From 2f0c8951cb0ffe83dd6a86db61f7a8007c846487 Mon Sep 17 00:00:00 2001 From: dfinke Date: Sat, 7 Feb 2026 17:30:52 -0500 Subject: [PATCH 16/16] Refactor OpenAI response handling and input conversion for improved clarity and structure --- Public/Invoke-PSClaudeCode.ps1 | 168 ++++++++++++++------------------- 1 file changed, 69 insertions(+), 99 deletions(-) diff --git a/Public/Invoke-PSClaudeCode.ps1 b/Public/Invoke-PSClaudeCode.ps1 index 4982de6..c4a79fa 100644 --- a/Public/Invoke-PSClaudeCode.ps1 +++ b/Public/Invoke-PSClaudeCode.ps1 @@ -90,7 +90,7 @@ function Invoke-PSClaudeCode { if ($SelectedProvider -eq "OpenAI") { return $ToolDefinitions | ForEach-Object { @{ - type = "function" + type = "function" name = $_.name description = $_.description parameters = $_.input_schema @@ -101,105 +101,48 @@ function Invoke-PSClaudeCode { return $ToolDefinitions } - function Convert-OpenAIInput { - param($MessageHistory) - - $inputs = @() - foreach ($message in $MessageHistory) { - $role = $message.role - $contentValue = $message.content - - if ($contentValue -is [System.Array]) { - $contentValue = ($contentValue | Where-Object { $_.type -eq "text" } | ForEach-Object { $_.text }) -join "" - } - - if ($role -eq "tool") { - $inputMessage = @{ - type = "tool_result" - tool_call_id = $message.tool_call_id - output = "$contentValue" - } - } - elseif ($role -eq "assistant") { - $inputMessage = @{ - role = $role - content = @(@{ - type = "output_text" - text = "$contentValue" - }) - } - } - else { - $inputMessage = @{ - role = $role - content = @(@{ - type = "input_text" - text = "$contentValue" - }) - } - } - - $inputs += $inputMessage - } - - return $inputs - } - function Normalize-Response { param([string]$SelectedProvider, $Response) if ($SelectedProvider -eq "OpenAI") { - if (-not $Response.output) { - return @{ content = @(); rawMessage = $null } - } - $contentItems = @() $toolCalls = @() - $outputMessages = $Response.output | Where-Object { $_.type -eq "message" } - $outputTextItems = $Response.output | Where-Object { $_.type -eq "output_text" } - $toolCallItems = $Response.output | Where-Object { $_.type -eq "tool_call" } - - foreach ($outputMessage in $outputMessages) { - foreach ($content in $outputMessage.content) { - if ($content.type -eq "output_text") { - $contentItems += @{ - type = "text" - text = $content.text + foreach ($item in $Response.output) { + if ($item.type -eq "message") { + foreach ($content in $item.content) { + if ($content.type -eq "output_text") { + $contentItems += @{ + type = "text" + text = $content.text + } } } } - } - - foreach ($outputTextItem in $outputTextItems) { - $contentItems += @{ - type = "text" - text = $outputTextItem.text - } - } - - foreach ($toolCall in $toolCallItems) { - $arguments = $toolCall.arguments - $inputObject = if ([string]::IsNullOrWhiteSpace($arguments)) { @{} } else { $arguments | ConvertFrom-Json } - $contentItems += @{ - type = "tool_use" - id = $toolCall.call_id - name = $toolCall.name - input = $inputObject - } - $toolCalls += @{ - id = $toolCall.call_id - type = "function" - function = @{ - name = $toolCall.name - arguments = $arguments + if ($item.type -eq "function_call") { + $arguments = $item.arguments + $inputObject = $arguments | ConvertFrom-Json + $contentItems += @{ + type = "tool_use" + id = $item.call_id + name = $item.name + input = $inputObject + } + $toolCalls += @{ + id = $item.call_id + type = "function" + function = @{ + name = $item.name + arguments = $arguments + } } } } return @{ - content = $contentItems - rawMessage = @{ + content = $contentItems + openai_output = $Response.output + rawMessage = @{ content = ($contentItems | Where-Object { $_.type -eq "text" } | ForEach-Object { $_.text }) -join "" tool_calls = $toolCalls } @@ -246,13 +189,40 @@ function Invoke-PSClaudeCode { } if ($SelectedProvider -eq "OpenAI") { - $openAiInput = Convert-OpenAIInput -MessageHistory $MessageHistory + # Convert MessageHistory to Responses API input format + # Wrap in @() to ensure array even with a single message + [array]$inputItems = @($MessageHistory | ForEach-Object { + $msg = $_ + if ($msg.role -eq "tool") { + # Responses API: tool results are "function_call_output" items + @{ + type = "function_call_output" + call_id = $msg.tool_call_id + output = $msg.tool_output + } + } + elseif ($msg.role -eq "assistant") { + # Re-emit the raw output items the model returned + # so the Responses API sees its own native format + foreach ($item in $msg.openai_output) { + $item + } + } + else { + @{ + type = "message" + role = $msg.role + content = @($msg.content) + } + } + }) + $body = @{ - model = $SelectedModel - input = $openAiInput + model = $SelectedModel + input = $inputItems max_output_tokens = 4096 - tools = $ToolDefinitions - tool_choice = "auto" + tools = $ToolDefinitions + tool_choice = "auto" } | ConvertTo-Json -Depth 10 try { @@ -301,7 +271,7 @@ function Invoke-PSClaudeCode { $MessageHistory += @{ role = "tool" tool_call_id = $toolResult.id - content = $toolResult.content + tool_output = $toolResult.content } } return $MessageHistory @@ -412,7 +382,7 @@ function Invoke-PSClaudeCode { param([string]$SubTask, [int]$MaxTurns = 10) Write-Host "[$((Get-Date).ToString('HH:mm:ss'))] 🤖 Starting sub-agent for: $SubTask" - $subMessages = @(@{ role = "user"; content = $SubTask }) + $subMessages = @(@{ role = "user"; content = @(@{ type = "text"; text = $SubTask }) }) $turns = 0 $providerTools = Convert-ToolsForProvider -SelectedProvider $Provider -ToolDefinitions $tools @@ -430,9 +400,9 @@ function Invoke-PSClaudeCode { if ($Provider -eq "OpenAI") { $assistantMessage = @{ - role = "assistant" - content = $response.rawMessage.content - tool_calls = $response.rawMessage.tool_calls + role = "assistant" + openai_output = $response.openai_output + content = $response.content } } else { @@ -501,7 +471,7 @@ function Invoke-PSClaudeCode { return $true } - $messages = @(@{ role = "user"; content = $Task }) + $messages = @(@{ role = "user"; content = @(@{ type = "input_text"; text = $Task }) }) $providerTools = Convert-ToolsForProvider -SelectedProvider $Provider -ToolDefinitions $tools @@ -522,9 +492,9 @@ function Invoke-PSClaudeCode { if ($Provider -eq "OpenAI") { $assistantMessage = @{ - role = "assistant" - content = $response.rawMessage.content - tool_calls = $response.rawMessage.tool_calls + role = "assistant" + openai_output = $response.openai_output + content = $response.content } } else {