Public/Invoke-PCCompletionCached.ps1

function Invoke-PCCompletionCached {
    <#
    .SYNOPSIS
        Send a prompt to an AI provider with file-based response caching.
    .DESCRIPTION
        Same as Invoke-PCCompletion but caches responses to disk.
        Cache hits return instantly without making API calls.
    .EXAMPLE
        Invoke-PCCompletionCached -UserPrompt "Analyze this" -CachePath ./cache -CacheKey "item-001"
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$UserPrompt,

        [Parameter(Mandatory)]
        [string]$CachePath,

        [Parameter(Mandatory)]
        [string]$CacheKey,

        [string]$SystemPrompt,

        [ValidateSet('openai', 'gemini', 'anthropic')]
        [string]$Provider,

        [string]$Model,

        [int]$MaxTokens = 4096,

        [ValidateRange(0.0, 2.0)]
        [double]$Temperature = 0.3,

        [switch]$JsonMode,

        [switch]$WebSearch,

        [int]$TimeoutSec = 120,

        [int]$CacheTTLHours = -1
    )

    # Ensure cache directory exists
    if (-not (Test-Path $CachePath)) {
        New-Item -ItemType Directory -Path $CachePath -Force | Out-Null
    }

    # Sanitize cache key for filename
    $safeKey = $CacheKey -replace '[^\w\-\.]', '_'
    $cacheFile = Join-Path $CachePath "$safeKey.json"

    # Check cache
    if (Test-Path $cacheFile) {
        $cached = Get-Content $cacheFile -Raw | ConvertFrom-Json
        if ($cached.schema -eq 'ai-response-cache') {
            # Check TTL
            if ($CacheTTLHours -gt 0) {
                $cachedTime = [datetime]$cached.metadata.timestamp
                $age = (Get-Date) - $cachedTime
                if ($age.TotalHours -lt $CacheTTLHours) {
                    Write-Verbose "Cache HIT: $CacheKey (age: $([int]$age.TotalMinutes) min)"
                    return $cached.raw_response
                }
                Write-Verbose "Cache EXPIRED: $CacheKey (age: $([int]$age.TotalHours) hours)"
            }
            else {
                Write-Verbose "Cache HIT: $CacheKey (no TTL)"
                return $cached.raw_response
            }
        }
    }

    Write-Verbose "Cache MISS: $CacheKey — calling provider"

    # Build params for Invoke-PCCompletion
    $splat = @{
        UserPrompt  = $UserPrompt
        MaxTokens   = $MaxTokens
        Temperature = $Temperature
        TimeoutSec  = $TimeoutSec
    }
    if ($SystemPrompt) { $splat['SystemPrompt'] = $SystemPrompt }
    if ($Provider) { $splat['Provider'] = $Provider }
    if ($Model) { $splat['Model'] = $Model }
    if ($JsonMode) { $splat['JsonMode'] = $true }
    if ($WebSearch) { $splat['WebSearch'] = $true }

    $startTime = Get-Date
    $response = Invoke-PCCompletion @splat
    $duration = ((Get-Date) - $startTime).TotalMilliseconds

    # Resolve actual provider/model for metadata
    $actualProvider = if ($Provider) { $Provider }
                      elseif ($Model -match '^gemini') { 'gemini' }
                      elseif ($Model -match '^claude') { 'anthropic' }
                      else { 'openai' }
    $actualModel = if ($Model) { $Model } else { $script:Providers[$actualProvider].DefaultModel }

    # Write cache
    $cacheObj = @{
        schema       = 'ai-response-cache'
        version      = '1.0'
        cache_key    = $CacheKey
        metadata     = @{
            model           = $actualModel
            provider        = $actualProvider
            timestamp       = (Get-Date).ToString('o')
            duration_ms     = [int]$duration
            max_tokens      = $MaxTokens
            response_length = $response.Length
        }
        raw_response = $response
    }

    $cacheObj | ConvertTo-Json -Depth 5 | Set-Content -Path $cacheFile -Encoding UTF8
    Write-Verbose "Cached response: $cacheFile ($($response.Length) chars)"

    $response
}