diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e20f992..da26946 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: run: brew install shellcheck - name: shellcheck - run: shellcheck -S warning install.sh qbraid-code statusline.sh tests/statusline.sh tests/extractors.sh + run: shellcheck -S warning install.sh qbraid-code statusline.sh tests/statusline.sh tests/extractors.sh tests/model-routing.sh - name: syntax run: | @@ -32,6 +32,9 @@ jobs: - name: extractor tests run: tests/extractors.sh + - name: model routing tests + run: tests/model-routing.sh + - name: installer --help does not touch the machine run: bash install.sh --help diff --git a/README.md b/README.md index 5dd8dd1..1ffb2f5 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,24 @@ qbraid-code --doctor # check your setup Every other flag goes straight through to `claude`, so `-c`, `--model`, `--allowedTools` and the rest behave normally. +### GPT models + +The gateway also serves OpenAI GPT models, and `qbraid-code` can use them: + +```bash +qbraid-code --model gpt-5.6-sol +qbraid-code --model gpt-5.4-mini -p "explain this error" +``` + +Claude Code speaks the Anthropic API and the gateway serves GPT only on its +OpenAI-compatible surface, so the first GPT request starts a small local +translation proxy (CLIProxyAPI, loopback only, installed by the installer). +Claude models never touch it. `qbraid-code --stop` shuts it down. + +Two caveats: GPT models accept at most 128 tools, so with many MCP servers +add `--strict-mcp-config`; and the `/model` picker inside a session lists +Claude models only — choose a GPT model at launch with `--model`. + A session looks like this: ``` @@ -104,6 +122,9 @@ claude mcp login qbraid | `~/.qbraid-code/statusline.sh` | statusline script (`statusline.ps1` on Windows) | | `~/.qbraid-code/credits.cache` | last known credit balance, refreshed every 60s | | `~/.qbraid-code/credits.attempt` | when a refresh was last tried, so failures back off | +| `~/.qbraid-code/proxy-config.yaml` | GPT translation proxy config (mode `600`, holds your key) | +| `~/.qbraid-code/proxy.key` | loopback bearer for the proxy | +| `~/.qbraid-code/proxy.log` | proxy output | | `~/.local/bin/qbraid-code` | the launcher (`qbraid-code.cmd` on Windows) | | `~/.claude/settings.json` | statusline wiring, plus gateway env with `--global` | @@ -146,6 +167,7 @@ bash install.sh ## Uninstall ```bash +qbraid-code --stop rm -rf ~/.qbraid-code ~/.local/bin/qbraid-code claude mcp remove qbraid ``` diff --git a/doctor.ps1 b/doctor.ps1 index 68b9c37..c91489a 100644 --- a/doctor.ps1 +++ b/doctor.ps1 @@ -72,3 +72,11 @@ if ($LASTEXITCODE -eq 0) { } Write-Host "model: $model" + +$proxyBin = $settings['QBRAID_CODE_PROXY_BIN'] +if ($proxyBin -and (Test-Path $proxyBin)) { + $st = & powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $HomeDir 'qbraid-proxy.ps1') status 2>$null + Write-Host "gpt: proxy $st" +} else { + Write-Host 'gpt: NOT AVAILABLE - re-run the installer to add GPT models' +} diff --git a/install.ps1 b/install.ps1 index 0884242..a7082f8 100644 --- a/install.ps1 +++ b/install.ps1 @@ -304,6 +304,7 @@ function Fetch-File { Write-RawText $Dest $text } +$ProxyHelperPath = Join-Path $HomeDir 'qbraid-proxy.ps1' $LauncherPath = Join-Path $BinDir 'qbraid-code.cmd' $StatuslinePath = Join-Path $HomeDir 'statusline.ps1' $DoctorPath = Join-Path $HomeDir 'doctor.ps1' @@ -313,6 +314,7 @@ Fetch-File 'statusline.ps1' $StatuslinePath Ok "statusline installed to $StatuslinePath" # `qbraid-code --doctor` shells out to this; the .cmd cannot parse JSON itself. Fetch-File 'doctor.ps1' $DoctorPath +Fetch-File 'qbraid-proxy.ps1' $ProxyHelperPath # Put the launcher on PATH for future terminals. $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') @@ -321,6 +323,87 @@ if ($userPath -notlike "*$BinDir*") { Ok "added $BinDir to your PATH (new terminals only)" } +# ------------------------------------------------ 7b. GPT models (local proxy) + +# Non-fatal: Claude models work without any of this. +Say 'GPT models' +$ProxyBin = '' +$existing = Join-Path $HomeDir 'cliproxyapi.exe' +if (Test-Path $existing) { + $ProxyBin = $existing + Ok "using existing $ProxyBin" +} else { + try { + $rel = Invoke-RestMethod -Uri 'https://api.github.com/repos/router-for-me/CLIProxyAPI/releases/latest' -TimeoutSec 20 + $tag = $rel.tag_name; $ver = $tag.TrimStart('v') + $url = "https://github.com/router-for-me/CLIProxyAPI/releases/download/$tag/CLIProxyAPI_${ver}_windows_amd64.zip" + $zip = Join-Path $env:TEMP 'cpa.zip' + Invoke-WebRequest -Uri $url -OutFile $zip -TimeoutSec 180 -UseBasicParsing + $tmp = Join-Path $env:TEMP 'cpa-extract' + if (Test-Path $tmp) { Remove-Item $tmp -Recurse -Force } + Expand-Archive -Path $zip -DestinationPath $tmp + $exe = Get-ChildItem $tmp -Recurse -Filter 'cli-proxy-api*.exe' | Select-Object -First 1 + if ($exe) { + Copy-Item $exe.FullName $existing -Force + $ProxyBin = $existing + Ok "proxy installed to $ProxyBin" + } + Remove-Item $zip, $tmp -Recurse -Force -ErrorAction SilentlyContinue + } catch { + Warn "could not install CLIProxyAPI: $($_.Exception.Message)" + } +} + +if ($ProxyBin) { + $gptModels = @() + try { + $list = Invoke-RestMethod -Uri "$GatewayUrl/models" -Headers @{ 'X-API-Key' = $ApiKey } -TimeoutSec 25 + $gptModels = @(Get-Prop $list 'data' | ForEach-Object { Get-Prop $_ 'id' } | Where-Object { $_ -like 'gpt-*' }) + } catch { } + if ($gptModels.Count -eq 0) { + Warn 'could not list GPT models from the gateway - skipping proxy config' + } else { + $keyFile = Join-Path $HomeDir 'proxy.key' + if (-not (Test-Path $keyFile)) { + $bytes = New-Object byte[] 24 + [Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes) + Write-RawText $keyFile (($bytes | ForEach-Object { $_.ToString('x2') }) -join '') + } + $localKey = (Get-Content $keyFile -Raw).Trim() + $yaml = @() + $yaml += '# Generated by the qbraid-code installer. Loopback only.' + $yaml += 'host: "127.0.0.1"' + $yaml += 'port: 8320' + $yaml += 'tls:' + $yaml += ' enable: false' + $yaml += "auth-dir: `"$($HomeDir -replace '\\','/')/proxy-auth`"" + $yaml += 'api-keys:' + $yaml += " - `"$localKey`"" + $yaml += 'remote-management:' + $yaml += ' allow-remote: false' + $yaml += ' disable-control-panel: true' + $yaml += 'debug: false' + $yaml += 'openai-compatibility:' + $yaml += ' - name: "qbraid-gateway"' + $yaml += " base-url: `"$GatewayUrl`"" + $yaml += ' api-key-entries:' + $yaml += " - api-key: `"$ApiKey`"" + $yaml += ' models:' + foreach ($gm in $gptModels) { + $yaml += " - name: `"$gm`"" + $yaml += " alias: `"$gm`"" + } + Write-RawText (Join-Path $HomeDir 'proxy-config.yaml') (($yaml -join "`n") + "`n") + New-Item -ItemType Directory -Force -Path (Join-Path $HomeDir 'proxy-auth') | Out-Null + Ok "proxy configured for $($gptModels.Count) GPT models (starts on demand)" + } +} else { + Warn 'CLIProxyAPI unavailable - GPT models will not work; Claude models are unaffected.' +} + +# Appended after the env file exists. +Add-Content -Path $envPath -Value @("QBRAID_CODE_PROXY_PORT=8320", "QBRAID_CODE_PROXY_BIN=$ProxyBin") + # --------------------------------------------------------- 8. first-run flags Say 'Claude Code first run' diff --git a/install.sh b/install.sh index 8a4e663..448e605 100755 --- a/install.sh +++ b/install.sh @@ -18,6 +18,12 @@ GATEWAY_HOST="api-v2.qbraid.com" API_BASE="https://${GATEWAY_HOST}/api/v1" GATEWAY_URL="${API_BASE}/ai" MCP_NAME="qbraid" +# Local translation proxy for the gateway's GPT models. Claude Code speaks the +# Anthropic Messages API; the gateway serves GPT only on its OpenAI-compat +# surface. CLIProxyAPI bridges the two on loopback. Port is qbraid-code's own — +# claudeseek and other tools use neighbouring ports. +PROXY_PORT="${QBRAID_CODE_PROXY_PORT:-8320}" +PROXY_REPO="router-for-me/CLIProxyAPI" MCP_URL="https://mcp.qbraid.com/mcp" KEYS_URL="https://account.qbraid.com/account/api-keys" @@ -432,6 +438,101 @@ fetch_file statusline.sh "$HOME_DIR/statusline.sh" chmod 0755 "$HOME_DIR/statusline.sh" ok "statusline installed to $HOME_DIR/statusline.sh" +# ------------------------------------------------ 7b. GPT models (local proxy) + +# Non-fatal throughout: Claude models work without any of this. If a step +# fails, the install continues and `qbraid-code --model gpt-*` explains itself. +say "GPT models" +PROXY_BIN="" +if command -v cliproxyapi >/dev/null 2>&1; then + PROXY_BIN="$(command -v cliproxyapi)" + ok "using existing $PROXY_BIN" +elif [ -x "$HOME_DIR/cliproxyapi" ]; then + PROXY_BIN="$HOME_DIR/cliproxyapi" + ok "using existing $PROXY_BIN" +elif [ "$OS" = darwin ] && command -v brew >/dev/null 2>&1; then + if brew install cliproxyapi >/dev/null 2>&1; then + PROXY_BIN="$(command -v cliproxyapi)" + ok "proxy installed via Homebrew" + fi +fi +if [ -z "$PROXY_BIN" ]; then + PROXY_ARCH="$ARCH"; [ "$PROXY_ARCH" = arm64 ] && PROXY_ARCH=aarch64 + [ "$PROXY_ARCH" = x64 ] && PROXY_ARCH=amd64 + TAG=$( (set +o pipefail; curl -fsSL -m 20 "https://api.github.com/repos/$PROXY_REPO/releases/latest" 2>/dev/null | grep -o '"tag_name": *"[^"]*"' | head -1 | sed 's/.*"tag_name": *"//; s/"$//') || true) + if [ -n "$TAG" ]; then + VER="${TAG#v}" + PROXY_URL="https://github.com/$PROXY_REPO/releases/download/$TAG/CLIProxyAPI_${VER}_${OS}_${PROXY_ARCH}.tar.gz" + PROXY_TMP=$(mktemp -d) + if curl -fsSL -m 120 -o "$PROXY_TMP/cpa.tar.gz" "$PROXY_URL" 2>/dev/null \ + && tar xzf "$PROXY_TMP/cpa.tar.gz" -C "$PROXY_TMP" cli-proxy-api 2>/dev/null; then + install -m 0755 "$PROXY_TMP/cli-proxy-api" "$HOME_DIR/cliproxyapi" + PROXY_BIN="$HOME_DIR/cliproxyapi" + ok "proxy installed to $PROXY_BIN" + fi + rm -rf "$PROXY_TMP" + fi +fi + +GPT_MODELS="" +if [ -n "$PROXY_BIN" ]; then + # The gateway's OpenAI-compat surface lists every model; only the gpt-* ones + # need the proxy — Claude models go to the Anthropic surface directly. + api_get "$GATEWAY_URL/models" "$API_KEY" + GPT_MODELS=$(set +o pipefail; printf '%s' "$API_BODY" | grep -o '"id":"gpt-[^"]*"' | sed 's/"id":"//; s/"$//') + if [ -z "$GPT_MODELS" ]; then + warn "could not list GPT models from the gateway — skipping proxy config" + else + if [ ! -s "$HOME_DIR/proxy.key" ]; then + # Loopback bearer so only processes on this machine can use the proxy. + OLD_UMASK=$(umask); umask 077 + od -An -tx1 -N 24 /dev/urandom | tr -d ' \n' > "$HOME_DIR/proxy.key" + umask "$OLD_UMASK" + fi + PROXY_LOCAL_KEY=$(cat "$HOME_DIR/proxy.key") + OLD_UMASK=$(umask); umask 077 + { + cat < "$HOME_DIR/proxy-config.yaml" + chmod 600 "$HOME_DIR/proxy-config.yaml" + umask "$OLD_UMASK" + mkdir -p "$HOME_DIR/proxy-auth" + GPT_COUNT=$(printf '%s\n' "$GPT_MODELS" | wc -l | tr -d ' ') + ok "proxy configured for $GPT_COUNT GPT models (starts on demand)" + fi +else + warn "CLIProxyAPI unavailable — GPT models will not work; Claude models are unaffected." +fi + +# Appended here rather than written in section 6: PROXY_BIN does not exist yet +# when the env file is first created. +{ + printf 'QBRAID_CODE_PROXY_PORT=%s\n' "$PROXY_PORT" + printf 'QBRAID_CODE_PROXY_BIN=%s\n' "$PROXY_BIN" +} >> "$HOME_DIR/env" + # --------------------------------------------------------- 8. first-run flags say "Claude Code first run" diff --git a/qbraid-code b/qbraid-code index 6be6a31..8f40c8c 100755 --- a/qbraid-code +++ b/qbraid-code @@ -22,6 +22,55 @@ BASE_URL="${QBRAID_CODE_BASE_URL:-}" API_BASE="${QBRAID_CODE_API_BASE:-}" TOKEN="${QBRAID_CODE_TOKEN:-}" MODEL="${QBRAID_CODE_MODEL:-}" +PROXY_PORT="${QBRAID_CODE_PROXY_PORT:-8320}" +PROXY_BIN="${QBRAID_CODE_PROXY_BIN:-}" +PROXY_URL="http://127.0.0.1:$PROXY_PORT" + +# ---------------------------------------------------------------- GPT proxy +# The gateway serves GPT models only on its OpenAI-compat surface, which +# Claude Code cannot speak. A loopback CLIProxyAPI translates; it is started +# here on demand and only when a gpt-* model is actually requested. + +proxy_listening() { + [ -s "$HOME_DIR/proxy.key" ] || return 1 + curl -fs -m 3 -o /dev/null "$PROXY_URL/v1/models" \ + -H "Authorization: Bearer $(cat "$HOME_DIR/proxy.key")" 2>/dev/null +} + +start_proxy() { + proxy_listening && return 0 + [ -n "$PROXY_BIN" ] && [ -x "$PROXY_BIN" ] || { + echo "qbraid-code: GPT models need the local proxy, which is not installed." >&2 + echo "Re-run the installer: curl -fsSL https://qbraid.com/code.sh | bash" >&2 + return 1 + } + [ -s "$HOME_DIR/proxy-config.yaml" ] || { + echo "qbraid-code: proxy config missing — re-run the installer." >&2 + return 1 + } + nohup "$PROXY_BIN" -config "$HOME_DIR/proxy-config.yaml" \ + >> "$HOME_DIR/proxy.log" 2>&1 & + i=0 + while [ "$i" -lt 40 ]; do + proxy_listening && return 0 + sleep 0.3 + i=$((i + 1)) + done + echo "qbraid-code: proxy failed to start — see $HOME_DIR/proxy.log" >&2 + return 1 +} + +# The requested model decides the route: an explicit --model wins over the +# configured default. Only the value matters; everything else passes through. +requested_model() { + local prev="" a + for a in "$@"; do + if [ "$prev" = "--model" ]; then printf '%s' "$a"; return; fi + case "$a" in --model=*) printf '%s' "${a#--model=}"; return ;; esac + prev="$a" + done + printf '%s' "$MODEL" +} missing_keys() { local missing="" @@ -33,6 +82,13 @@ missing_keys() { } case "${1:-}" in + --stop) + if [ -n "$PROXY_BIN" ] && pkill -f "$PROXY_BIN -config $HOME_DIR/proxy-config.yaml" 2>/dev/null; then + echo "qbraid-code: proxy stopped" + else + echo "qbraid-code: proxy was not running" + fi + exit 0 ;; --doctor) command -v claude >/dev/null 2>&1 \ && echo "claude: $(claude --version 2>/dev/null || echo present)" \ @@ -93,6 +149,16 @@ case "${1:-}" in fi echo "model: ${MODEL:-not set}" + + if [ -n "$PROXY_BIN" ] && [ -x "$PROXY_BIN" ]; then + if proxy_listening; then + echo "gpt: proxy running on $PROXY_URL" + else + echo "gpt: proxy installed (starts on demand with --model gpt-...)" + fi + else + echo "gpt: NOT AVAILABLE — re-run the installer to add GPT models" + fi exit 0 ;; --help|-h) cat </dev/null 2>&1 || { exit 1 } +RUN_MODEL=$(requested_model "$@") +RUN_BASE="$BASE_URL" +RUN_TOKEN="$TOKEN" +case "$RUN_MODEL" in + gpt-*) + # GPT route: loopback proxy translates Anthropic Messages to the + # gateway's OpenAI surface. Azure caps `tools` at 128 — with many MCP + # servers Claude Code exceeds it; --strict-mcp-config avoids that. + start_proxy || exit 1 + RUN_BASE="$PROXY_URL" + RUN_TOKEN=$(cat "$HOME_DIR/proxy.key") + ;; +esac + # ANTHROPIC_AUTH_TOKEN sends `Authorization: Bearer `, which the gateway # accepts. ANTHROPIC_API_KEY would work too, but it makes Claude Code ask the # user to approve a custom API key on first run — a prompt with no good answer @@ -131,14 +213,14 @@ command -v claude >/dev/null 2>&1 || { # # MAX_THINKING_TOKENS=0: recent Claude Code sends `thinking: {type: "adaptive"}`, # which the gateway's Anthropic surface rejects (it accepts enabled|disabled — -# qbraid-api ai-request.validators.ts), turning every request into a 400. +# qbraid-api ai-request.validators.ts) and GPT models reject outright. # FORCED, not defaulted: an inherited MAX_THINKING_TOKENS from the user's -# shell re-enables thinking and turns every request into that 400. Remove the -# whole line once the gateway accepts "adaptive". +# shell re-enables thinking and turns every request into that 400. Relax to +# Claude-only once the gateway accepts "adaptive". MAX_THINKING_TOKENS=0 \ -ANTHROPIC_BASE_URL="$BASE_URL" \ -ANTHROPIC_AUTH_TOKEN="$TOKEN" \ -ANTHROPIC_MODEL="$MODEL" \ -ANTHROPIC_SMALL_FAST_MODEL="$MODEL" \ -CLAUDE_CODE_SUBAGENT_MODEL="$MODEL" \ +ANTHROPIC_BASE_URL="$RUN_BASE" \ +ANTHROPIC_AUTH_TOKEN="$RUN_TOKEN" \ +ANTHROPIC_MODEL="$RUN_MODEL" \ +ANTHROPIC_SMALL_FAST_MODEL="$RUN_MODEL" \ +CLAUDE_CODE_SUBAGENT_MODEL="$RUN_MODEL" \ exec claude "$@" diff --git a/qbraid-code.cmd b/qbraid-code.cmd index e8ae7b9..cdbbe2c 100644 --- a/qbraid-code.cmd +++ b/qbraid-code.cmd @@ -27,6 +27,11 @@ set "QBRAID_CODE_TOKEN=" set "QBRAID_CODE_MODEL=" for /f "usebackq eol=# tokens=1,* delims==" %%a in ("%QC_HOME%\env") do set "%%a=%%b" +if /i "%~1"=="--stop" ( + powershell -NoProfile -ExecutionPolicy Bypass -File "%QC_HOME%\qbraid-proxy.ps1" stop + exit /b !ERRORLEVEL! +) + if /i "%~1"=="--doctor" ( powershell -NoProfile -ExecutionPolicy Bypass -File "%QC_HOME%\doctor.ps1" exit /b !ERRORLEVEL! @@ -39,6 +44,27 @@ if not defined QBRAID_CODE_TOKEN goto :incomplete if not defined QBRAID_CODE_BASE_URL goto :incomplete if not defined QBRAID_CODE_MODEL goto :incomplete +rem Determine the model actually requested: an explicit --model wins over the +rem configured default. Only the value matters; args still pass through whole. +set "RUNMODEL=%QBRAID_CODE_MODEL%" +set "PREV=" +for %%a in (%*) do ( + if defined PREV set "RUNMODEL=%%~a" & set "PREV=" + if /i "%%~a"=="--model" set "PREV=1" +) + +set "RUNBASE=%QBRAID_CODE_BASE_URL%" +set "RUNTOKEN=%QBRAID_CODE_TOKEN%" +if /i not "%RUNMODEL:~0,4%"=="gpt-" goto :direct +rem GPT route: the loopback proxy translates Anthropic Messages to the +rem gateway's OpenAI surface. Azure caps tools at 128; with many MCP servers +rem use --strict-mcp-config. +powershell -NoProfile -ExecutionPolicy Bypass -File "%QC_HOME%\qbraid-proxy.ps1" ensure +if errorlevel 1 exit /b 1 +set /p RUNTOKEN=<"%QC_HOME%\proxy.key" +set "RUNBASE=http://127.0.0.1:8320" +:direct + where claude >nul 2>&1 if errorlevel 1 ( echo qbraid-code: Claude Code is not installed. 1>&2 @@ -55,11 +81,11 @@ rem which the gateway rejects (accepts enabled/disabled only) - every request rem would 400. Forced: an inherited value re-enables thinking and breaks every rem request. Remove once the gateway accepts adaptive. set "MAX_THINKING_TOKENS=0" -set "ANTHROPIC_BASE_URL=%QBRAID_CODE_BASE_URL%" -set "ANTHROPIC_AUTH_TOKEN=%QBRAID_CODE_TOKEN%" -set "ANTHROPIC_MODEL=%QBRAID_CODE_MODEL%" -set "ANTHROPIC_SMALL_FAST_MODEL=%QBRAID_CODE_MODEL%" -set "CLAUDE_CODE_SUBAGENT_MODEL=%QBRAID_CODE_MODEL%" +set "ANTHROPIC_BASE_URL=%RUNBASE%" +set "ANTHROPIC_AUTH_TOKEN=%RUNTOKEN%" +set "ANTHROPIC_MODEL=%RUNMODEL%" +set "ANTHROPIC_SMALL_FAST_MODEL=%RUNMODEL%" +set "CLAUDE_CODE_SUBAGENT_MODEL=%RUNMODEL%" claude %* exit /b !ERRORLEVEL! diff --git a/qbraid-proxy.ps1 b/qbraid-proxy.ps1 new file mode 100644 index 0000000..15c3842 --- /dev/null +++ b/qbraid-proxy.ps1 @@ -0,0 +1,77 @@ +<# +.SYNOPSIS + Manages the loopback CLIProxyAPI that gives qbraid-code its GPT models. + +.DESCRIPTION + Claude Code speaks the Anthropic Messages API; the qBraid gateway serves GPT + models only on its OpenAI-compat surface. This proxy translates between the + two on 127.0.0.1. Invoked by qbraid-code.cmd — ensure|status|stop. +#> +param( + [Parameter(Position = 0)] + [ValidateSet('ensure', 'status', 'stop')] + [string]$Action = 'ensure' +) +$ErrorActionPreference = 'Stop' + +$HomeDir = if ($env:QBRAID_CODE_HOME) { $env:QBRAID_CODE_HOME } else { Join-Path $env:USERPROFILE '.qbraid-code' } +$Port = 8320 +$Bin = '' +$envPath = Join-Path $HomeDir 'env' +if (Test-Path $envPath) { + foreach ($line in Get-Content $envPath) { + if ($line -match '^\s*QBRAID_CODE_PROXY_PORT\s*=\s*(.*)$') { $Port = [int]$Matches[1] } + if ($line -match '^\s*QBRAID_CODE_PROXY_BIN\s*=\s*(.*)$') { $Bin = $Matches[1] } + } +} +$KeyFile = Join-Path $HomeDir 'proxy.key' +$Config = Join-Path $HomeDir 'proxy-config.yaml' +$LogFile = Join-Path $HomeDir 'proxy.log' +$BaseUrl = "http://127.0.0.1:$Port" + +function Test-Proxy { + if (-not (Test-Path $KeyFile)) { return $false } + try { + $key = (Get-Content $KeyFile -Raw).Trim() + Invoke-RestMethod -Uri "$BaseUrl/v1/models" -TimeoutSec 3 ` + -Headers @{ Authorization = "Bearer $key" } | Out-Null + return $true + } catch { return $false } +} + +switch ($Action) { + 'status' { + if (Test-Proxy) { Write-Output "running on $BaseUrl" } else { Write-Output 'not running' } + exit 0 + } + 'stop' { + $procs = Get-CimInstance Win32_Process -Filter "Name like '%cliproxyapi%'" -ErrorAction SilentlyContinue | + Where-Object { $_.CommandLine -like "*$Config*" } + if ($procs) { + $procs | ForEach-Object { Stop-Process -Id $_.ProcessId -Force -ErrorAction SilentlyContinue } + Write-Output 'proxy stopped' + } else { + Write-Output 'proxy was not running' + } + exit 0 + } + 'ensure' { + if (Test-Proxy) { exit 0 } + if (-not $Bin -or -not (Test-Path $Bin)) { + Write-Error 'GPT models need the local proxy, which is not installed. Re-run: irm https://qbraid.com/code.ps1 | iex' + exit 1 + } + if (-not (Test-Path $Config)) { + Write-Error 'proxy config missing - re-run the installer.' + exit 1 + } + Start-Process -FilePath $Bin -ArgumentList '-config', $Config ` + -WindowStyle Hidden -RedirectStandardOutput $LogFile -RedirectStandardError "$LogFile.err" + for ($i = 0; $i -lt 40; $i++) { + if (Test-Proxy) { exit 0 } + Start-Sleep -Milliseconds 300 + } + Write-Error "proxy failed to start - see $LogFile" + exit 1 + } +} diff --git a/tests/model-routing.sh b/tests/model-routing.sh new file mode 100755 index 0000000..db49270 --- /dev/null +++ b/tests/model-routing.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# The launcher routes gpt-* models through the local proxy and Claude models +# direct. requested_model() decides that; test the REAL definition from the +# launcher against arg shapes, under the launcher's own shell options. +set -uo pipefail +cd "$(dirname "$0")/.." || exit 1 + +FN=$(awk '/^requested_model\(\) \{/{f=1} f{print} f&&/^\}$/{exit}' qbraid-code) +[ -n "$FN" ] || { echo " FAIL could not extract requested_model"; exit 1; } + +pass=0; fail=0 +check() { # check [args...] + local name="$1" def="$2" want="$3"; shift 3 + local got + got=$(bash -c "set -euo pipefail; MODEL=\"$def\"; $FN; requested_model \"\$@\"" _ "$@") + if [ "$got" = "$want" ]; then + pass=$((pass + 1)); printf ' ok %s\n' "$name" + else + fail=$((fail + 1)); printf ' FAIL %s: got [%s] want [%s]\n' "$name" "$got" "$want" + fi +} + +check "default model when no --model" claude-sonnet-4-6 claude-sonnet-4-6 -p "hi" +check "--model value wins" claude-sonnet-4-6 gpt-5.6-sol --model gpt-5.6-sol -p "hi" +check "--model=value wins" claude-sonnet-4-6 gpt-5.4 --model=gpt-5.4 +check "--model anywhere in args" claude-sonnet-4-6 claude-opus-5 -p "hi" --model claude-opus-5 +check "gpt default with no args" gpt-5.6-sol gpt-5.6-sol +check "no args, no crash" claude-sonnet-4-6 claude-sonnet-4-6 + +printf '\n%d passed, %d failed\n' "$pass" "$fail" +[ "$fail" -eq 0 ]