Private/Helpers.ps1

# Private helper functions for PowerCraft.AI

#region Provider Configuration

$script:Providers = @{
    openai = @{
        ChatEndpoint      = 'https://api.openai.com/v1/chat/completions'
        ResponsesEndpoint = 'https://api.openai.com/v1/responses'
        ModelsEndpoint    = 'https://api.openai.com/v1/models'
        SecretName        = 'openai-api-key'
        DefaultModel      = 'gpt-4o'
    }
    gemini = @{
        ChatEndpoint   = 'https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent'
        ModelsEndpoint = 'https://generativelanguage.googleapis.com/v1beta/models'
        SecretName     = 'gemini-api-key'
        DefaultModel   = 'gemini-2.5-flash'
    }
    anthropic = @{
        MessagesEndpoint = 'https://api.anthropic.com/v1/messages'
        SecretName       = 'anthropic-api-key'
        DefaultModel     = 'claude-sonnet-4-20250514'
    }
}

#endregion

#region Key Resolution

function Resolve-PCApiKey {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Provider
    )

    $secretName = $script:Providers[$Provider].SecretName

    # Try PowerCraft.Secrets first
    if (Get-Command Get-PCSecret -ErrorAction SilentlyContinue) {
        $key = Get-PCSecret -Name $secretName -ErrorAction SilentlyContinue
        if ($key) { return $key }
    }

    # Fallback to environment variables
    $envMap = @{
        openai    = @('OPENAI_API_KEY')
        gemini    = @('GEMINI_API_KEY', 'GOOGLE_API_KEY')
        anthropic = @('ANTHROPIC_API_KEY')
    }

    foreach ($envVar in $envMap[$Provider]) {
        $val = [Environment]::GetEnvironmentVariable($envVar)
        if ($val) { return $val }
    }

    return $null
}

#endregion

#region Provider Request Functions

function Invoke-PCOpenAIRequest {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$ApiKey,
        [Parameter(Mandatory)][string]$Model,
        [Parameter(Mandatory)][array]$Messages,
        [int]$MaxTokens = 4096,
        [double]$Temperature = 0.3,
        [switch]$JsonMode,
        [switch]$WebSearch,
        [int]$TimeoutSec = 120
    )

    $headers = @{
        'Authorization' = "Bearer $ApiKey"
        'Content-Type'  = 'application/json'
    }

    $isResponsesAPI = $Model -match '^(gpt-5|o[1-9])'

    if ($isResponsesAPI) {
        # Responses API for reasoning models
        $systemMsg = ($Messages | Where-Object { $_.role -eq 'system' } | ForEach-Object { $_.content }) -join "`n"
        $userMsg = ($Messages | Where-Object { $_.role -eq 'user' } | ForEach-Object { $_.content }) -join "`n"
        $combinedInput = if ($systemMsg) { "$systemMsg`n`n$userMsg" } else { $userMsg }

        $body = @{
            model = $Model
            input = $combinedInput
        }

        if ($WebSearch) {
            $body.tools = @(@{ type = 'web_search_preview' })
        }

        if ($JsonMode -and -not $WebSearch) {
            $body.text = @{ format = @{ type = 'json_object' } }
        }

        $response = Invoke-RestMethod `
            -Uri $script:Providers.openai.ResponsesEndpoint `
            -Method POST `
            -Headers $headers `
            -Body ($body | ConvertTo-Json -Depth 20) `
            -TimeoutSec $TimeoutSec

        $messageOutput = $response.output | Where-Object { $_.type -eq 'message' } | Select-Object -First 1
        if ($messageOutput -and $messageOutput.content) {
            $textContent = $messageOutput.content | Where-Object { $_.type -eq 'output_text' } | Select-Object -First 1
            if ($textContent) { return $textContent.text.Trim() }
        }
        throw 'No output_text found in OpenAI Responses API response'
    }
    else {
        # Standard Chat Completions API
        $body = @{
            model       = $Model
            messages    = $Messages
            max_tokens  = $MaxTokens
            temperature = $Temperature
        }

        if ($JsonMode) {
            $body.response_format = @{ type = 'json_object' }
        }

        $response = Invoke-RestMethod `
            -Uri $script:Providers.openai.ChatEndpoint `
            -Method POST `
            -Headers $headers `
            -Body ($body | ConvertTo-Json -Depth 20) `
            -TimeoutSec $TimeoutSec

        return $response.choices[0].message.content.Trim()
    }
}

function Invoke-PCGeminiRequest {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$ApiKey,
        [Parameter(Mandatory)][string]$Model,
        [Parameter(Mandatory)][array]$Messages,
        [int]$MaxTokens = 4096,
        [double]$Temperature = 0.3,
        [switch]$JsonMode,
        [int]$TimeoutSec = 120
    )

    $endpoint = $script:Providers.gemini.ChatEndpoint -replace '\{model\}', $Model
    $uri = "${endpoint}?key=$ApiKey"

    $systemInstruction = $null
    $contents = @()

    foreach ($msg in $Messages) {
        if ($msg.role -eq 'system') {
            $systemInstruction = @{ parts = @(@{ text = $msg.content }) }
        }
        elseif ($msg.role -eq 'user') {
            $contents += @{ role = 'user'; parts = @(@{ text = $msg.content }) }
        }
        elseif ($msg.role -eq 'assistant') {
            $contents += @{ role = 'model'; parts = @(@{ text = $msg.content }) }
        }
    }

    $body = @{
        contents         = $contents
        generationConfig = @{
            temperature     = $Temperature
            maxOutputTokens = $MaxTokens
        }
    }

    if ($systemInstruction) { $body.systemInstruction = $systemInstruction }
    if ($JsonMode) { $body.generationConfig.responseMimeType = 'application/json' }

    $response = Invoke-RestMethod `
        -Uri $uri `
        -Method POST `
        -Headers @{ 'Content-Type' = 'application/json' } `
        -Body ($body | ConvertTo-Json -Depth 20) `
        -TimeoutSec $TimeoutSec

    if ($response.candidates -and $response.candidates[0].content.parts) {
        $text = ($response.candidates[0].content.parts | ForEach-Object { $_.text }) -join ''
        return $text.Trim()
    }

    throw 'No content found in Gemini response'
}

function Invoke-PCAnthropicRequest {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$ApiKey,
        [Parameter(Mandatory)][string]$Model,
        [Parameter(Mandatory)][array]$Messages,
        [int]$MaxTokens = 4096,
        [double]$Temperature = 0.3,
        [switch]$JsonMode,
        [int]$TimeoutSec = 120
    )

    $headers = @{
        'x-api-key'         = $ApiKey
        'anthropic-version' = '2023-06-01'
        'Content-Type'      = 'application/json'
    }

    $systemContent = $null
    $apiMessages = @()

    foreach ($msg in $Messages) {
        if ($msg.role -eq 'system') {
            $systemContent = $msg.content
        }
        else {
            $apiMessages += @{ role = $msg.role; content = $msg.content }
        }
    }

    $body = @{
        model      = $Model
        max_tokens = $MaxTokens
        messages   = $apiMessages
    }

    if ($systemContent) { $body.system = $systemContent }
    if ($Temperature -ne 1.0) { $body.temperature = $Temperature }

    $response = Invoke-RestMethod `
        -Uri $script:Providers.anthropic.MessagesEndpoint `
        -Method POST `
        -Headers $headers `
        -Body ($body | ConvertTo-Json -Depth 20) `
        -TimeoutSec $TimeoutSec

    $textBlocks = $response.content | Where-Object { $_.type -eq 'text' }
    if ($textBlocks) {
        return ($textBlocks | ForEach-Object { $_.text }) -join ''
    }

    throw 'No text content found in Anthropic response'
}

#endregion