gephyr 1.16.15

Gephyr is a headless local AI relay/proxy API handling OpenAI, Claude, and Gemini-compatible APIs
Documentation
<#
.SYNOPSIS
Smoke-test the dedicated proxy-pool runtime endpoint.

.DESCRIPTION
Validates that `POST /api/proxy/pool/runtime` can update proxy-pool runtime
knobs (`enabled`, `auto_failover`, `health_check_interval`) without full
`/api/config` writes, and that changes are visible via
`GET /api/proxy/pool/runtime`.

.PARAMETER Port
Proxy port. Default: 8045.

.PARAMETER ContainerName
Docker container name used by console.ps1. Default: gephyr.

.PARAMETER Image
Docker image tag used by console.ps1. Default: gephyr:latest.

.PARAMETER DataDir
Gephyr data directory passed to console.ps1.

.PARAMETER TargetEnabled
Optional explicit target for pool enabled flag (true/false).

.PARAMETER TargetAutoFailover
Optional explicit target for auto_failover flag (true/false).

.PARAMETER TargetHealthCheckInterval
Optional explicit target health check interval (seconds).
If omitted, script uses a derived alternate value.

.PARAMETER SkipStart
Use currently running server instead of starting via console.ps1.

.PARAMETER KeepChange
Do not restore original values after test.

.PARAMETER Help
Print usage/examples and exit.

.EXAMPLE
.\scripts\test-proxy-pool-runtime.ps1

.EXAMPLE
.\scripts\test-proxy-pool-runtime.ps1 -TargetEnabled $true -TargetAutoFailover $false -TargetHealthCheckInterval 120

.EXAMPLE
.\scripts\test-proxy-pool-runtime.ps1 -SkipStart -KeepChange

.EXAMPLE
Get-Help .\scripts\test-proxy-pool-runtime.ps1 -Detailed
#>
param(
    [int]$Port = 8045,
    [string]$ContainerName = "gephyr",
    [string]$Image = "gephyr:latest",
    [string]$DataDir = "$env:USERPROFILE\.gephyr",
    [Nullable[bool]]$TargetEnabled = $null,
    [Nullable[bool]]$TargetAutoFailover = $null,
    [Nullable[int]]$TargetHealthCheckInterval = $null,
    [switch]$SkipStart,
    [switch]$KeepChange,
    [Alias("h", "?")]
    [switch]$Help
)

$ErrorActionPreference = "Stop"

$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
$ConsoleScript = Join-Path $RepoRoot "console.ps1"
$EnvFile = Join-Path $RepoRoot ".env.local"
$BaseUrl = "http://127.0.0.1:$Port"

function Show-Usage {
    Write-Host ""
    Write-Host "Usage:" -ForegroundColor Cyan
    Write-Host "  .\scripts\test-proxy-pool-runtime.ps1 [options]"
    Write-Host ""
    Write-Host "Common examples:" -ForegroundColor Cyan
    Write-Host "  .\scripts\test-proxy-pool-runtime.ps1"
    Write-Host "  .\scripts\test-proxy-pool-runtime.ps1 -TargetEnabled `$true -TargetAutoFailover `$false -TargetHealthCheckInterval 120"
    Write-Host "  .\scripts\test-proxy-pool-runtime.ps1 -SkipStart -KeepChange"
    Write-Host ""
    Write-Host "Options:" -ForegroundColor Cyan
    Write-Host "  -Port <int>                        Default: 8045"
    Write-Host "  -TargetEnabled <bool?>             Optional explicit target"
    Write-Host "  -TargetAutoFailover <bool?>        Optional explicit target"
    Write-Host "  -TargetHealthCheckInterval <int?>  Optional explicit target (seconds)"
    Write-Host "  -SkipStart                         Uses currently running server"
    Write-Host "  -KeepChange                        Do not restore original values"
    Write-Host "  -Help                              Print this usage"
    Write-Host ""
    Write-Host "PowerShell native help:" -ForegroundColor Cyan
    Write-Host "  Get-Help .\scripts\test-proxy-pool-runtime.ps1 -Detailed"
}

function Write-Section {
    param([string]$Title)
    Write-Host ""
    Write-Host ("=" * 76) -ForegroundColor Cyan
    Write-Host $Title -ForegroundColor Cyan
    Write-Host ("=" * 76) -ForegroundColor Cyan
}

function Write-Step {
    param(
        [int]$Number,
        [string]$Title
    )
    Write-Host ""
    Write-Host ("[{0}] {1}" -f $Number, $Title) -ForegroundColor Yellow
}

function Load-EnvLocal {
    if (-not (Test-Path $EnvFile)) {
        return
    }

    foreach ($raw in Get-Content $EnvFile) {
        $line = $raw.Trim()
        if (-not $line -or $line.StartsWith("#") -or -not $line.Contains("=")) {
            continue
        }
        $parts = $line.Split("=", 2)
        $name = $parts[0].Trim()
        $value = $parts[1].Trim().Trim('"').Trim("'")
        if ($name -and $value -and -not (Get-Item "Env:$name" -ErrorAction SilentlyContinue)) {
            Set-Item -Path "Env:$name" -Value $value
        }
    }
}

function Get-AuthHeaders {
    if (-not $env:API_KEY) {
        throw "Missing API_KEY. Set env var or add it to .env.local."
    }
    return @{ Authorization = "Bearer $($env:API_KEY)" }
}

function Assert-DockerReady {
    if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
        Write-Host ""
        Write-Host "Docker CLI not found in PATH." -ForegroundColor Red
        Write-Host ""
        Write-Host "Install Docker:" -ForegroundColor Yellow
        Write-Host "  Docker Desktop : https://docs.docker.com/desktop/install/windows-install/"
        Write-Host "  winget         : winget install Docker.DockerDesktop"
        Write-Host "  choco          : choco install docker-desktop"
        throw "Missing prerequisite: docker"
    }
    docker info *> $null
    if ($LASTEXITCODE -ne 0) {
        throw "Docker daemon is not reachable. Start Docker Desktop first."
    }
}

function Wait-ServiceReady {
    param(
        [int]$Attempts = 50,
        [int]$DelayMs = 500
    )

    $headers = Get-AuthHeaders
    for ($i = 0; $i -lt $Attempts; $i++) {
        try {
            $resp = Invoke-WebRequest -Uri "$BaseUrl/health" -Headers $headers -UseBasicParsing -TimeoutSec 3
            if ($resp.StatusCode -eq 200) {
                return $true
            }
        } catch {
            Start-Sleep -Milliseconds $DelayMs
        }
    }
    return $false
}

function Start-Server {
    & $ConsoleScript -Command start -EnableAdminApi -Port $Port -ContainerName $ContainerName -Image $Image -DataDir $DataDir
}

function Api-Get {
    param([string]$Path)
    $headers = Get-AuthHeaders
    Invoke-RestMethod -Uri "$BaseUrl$Path" -Headers $headers -Method Get -TimeoutSec 30
}

function Api-PostJson {
    param(
        [string]$Path,
        [object]$Payload
    )
    $headers = Get-AuthHeaders
    $headers["Content-Type"] = "application/json"
    $json = $Payload | ConvertTo-Json -Depth 20
    Invoke-RestMethod -Uri "$BaseUrl$Path" -Headers $headers -Method Post -Body $json -TimeoutSec 30
}

if ($Help.IsPresent) {
    Show-Usage
    return
}

Write-Section "Gephyr Proxy-Pool Runtime Endpoint Smoke Test"
Write-Host "This script validates GET/POST /api/proxy/pool/runtime behavior." -ForegroundColor Gray

if (-not (Test-Path $ConsoleScript)) {
    throw "console.ps1 not found at $ConsoleScript"
}

Load-EnvLocal
Assert-DockerReady
[void](Get-AuthHeaders)

if ($TargetHealthCheckInterval.HasValue -and $TargetHealthCheckInterval.Value -lt 1) {
    throw "TargetHealthCheckInterval must be >= 1."
}

$before = $null
$after = $null
$restoreNeeded = $false

try {
    if (-not $SkipStart.IsPresent) {
        Write-Step 1 "Start server with admin API enabled"
        Start-Server
        if (-not (Wait-ServiceReady)) {
            throw "Service did not become ready on $BaseUrl"
        }
        Write-Host "Service is ready." -ForegroundColor Green
    } else {
        Write-Step 1 "Using running server (SkipStart)"
        if (-not (Wait-ServiceReady -Attempts 5 -DelayMs 300)) {
            throw "Server is not reachable on $BaseUrl."
        }
        Write-Host "Service is reachable." -ForegroundColor Green
    }

    Write-Step 2 "Verify route capability"
    $caps = Api-Get "/api/version/routes"
    if (-not $caps.routes.'GET /api/proxy/pool/runtime' -or -not $caps.routes.'POST /api/proxy/pool/runtime') {
        throw "Running image does not expose GET/POST /api/proxy/pool/runtime."
    }
    Write-Host ("Running version: {0}" -f $caps.version) -ForegroundColor Green

    Write-Step 3 "Read current runtime snapshot"
    $before = Api-Get "/api/proxy/pool/runtime"
    Write-Host ("Before: enabled={0}, auto_failover={1}, health_check_interval={2}, strategy={3}" -f `
        $before.enabled, $before.auto_failover, $before.health_check_interval, $before.strategy) -ForegroundColor Gray

    $nextEnabled = if ($TargetEnabled.HasValue) { $TargetEnabled.Value } else { -not [bool]$before.enabled }
    $nextAutoFailover = if ($TargetAutoFailover.HasValue) { $TargetAutoFailover.Value } else { -not [bool]$before.auto_failover }
    $nextInterval = if ($TargetHealthCheckInterval.HasValue) { $TargetHealthCheckInterval.Value } else { [Math]::Max(30, [int]$before.health_check_interval + 30) }

    if (($nextEnabled -eq [bool]$before.enabled) -and ($nextAutoFailover -eq [bool]$before.auto_failover) -and ($nextInterval -eq [int]$before.health_check_interval)) {
        $nextInterval = [Math]::Max(30, [int]$before.health_check_interval + 60)
        Write-Host ("Derived payload matched current state; bumping health_check_interval to {0}." -f $nextInterval) -ForegroundColor Yellow
    }

    Write-Step 4 "Update runtime knobs via dedicated endpoint"
    $post = Api-PostJson -Path "/api/proxy/pool/runtime" -Payload @{
        enabled = $nextEnabled
        auto_failover = $nextAutoFailover
        health_check_interval = $nextInterval
    }
    if (-not $post.ok -or -not $post.saved) {
        throw "Runtime update endpoint did not return success payload."
    }
    Write-Host ("Updated: enabled={0}, auto_failover={1}, health_check_interval={2}" -f `
        $post.proxy_pool.enabled, $post.proxy_pool.auto_failover, $post.proxy_pool.health_check_interval) -ForegroundColor Green

    Write-Step 5 "Verify persisted runtime snapshot"
    $after = Api-Get "/api/proxy/pool/runtime"
    if ([bool]$after.enabled -ne $nextEnabled) {
        throw "Expected enabled=$nextEnabled, got $($after.enabled)."
    }
    if ([bool]$after.auto_failover -ne $nextAutoFailover) {
        throw "Expected auto_failover=$nextAutoFailover, got $($after.auto_failover)."
    }
    if ([int]$after.health_check_interval -ne $nextInterval) {
        throw "Expected health_check_interval=$nextInterval, got $($after.health_check_interval)."
    }

    $restoreNeeded = -not $KeepChange.IsPresent

    Write-Step 6 "Result summary"
    Write-Host ("Before: enabled={0}, auto_failover={1}, health_check_interval={2}" -f `
        $before.enabled, $before.auto_failover, $before.health_check_interval)
    Write-Host ("After:  enabled={0}, auto_failover={1}, health_check_interval={2}" -f `
        $after.enabled, $after.auto_failover, $after.health_check_interval)
    Write-Host ""
    Write-Host "PASS: Dedicated pool-runtime endpoint is working." -ForegroundColor Green
}
finally {
    if ($restoreNeeded -and $before) {
        try {
            Api-PostJson -Path "/api/proxy/pool/runtime" -Payload @{
                enabled = [bool]$before.enabled
                auto_failover = [bool]$before.auto_failover
                health_check_interval = [int]$before.health_check_interval
            } | Out-Null
            Write-Host ("Restored original runtime knobs: enabled={0}, auto_failover={1}, health_check_interval={2}" -f `
                $before.enabled, $before.auto_failover, $before.health_check_interval) -ForegroundColor Gray
        } catch {
            Write-Host "Warning: failed to restore original runtime knobs automatically." -ForegroundColor Yellow
        }
    } elseif ($KeepChange.IsPresent) {
        Write-Host "KeepChange set: runtime knob changes were kept." -ForegroundColor Gray
    }
}