Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand All @@ -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

Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

```
Expand Down Expand Up @@ -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` |

Expand Down Expand Up @@ -146,6 +167,7 @@ bash install.sh
## Uninstall

```bash
qbraid-code --stop
rm -rf ~/.qbraid-code ~/.local/bin/qbraid-code
claude mcp remove qbraid
```
Expand Down
8 changes: 8 additions & 0 deletions doctor.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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'
}
83 changes: 83 additions & 0 deletions install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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')
Expand All @@ -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'
Expand Down
101 changes: 101 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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 <<PEOF
# Generated by the qbraid-code installer. Loopback only.
host: "127.0.0.1"
port: $PROXY_PORT
tls:
enable: false
auth-dir: "$HOME_DIR/proxy-auth"
api-keys:
- "$PROXY_LOCAL_KEY"
remote-management:
allow-remote: false
disable-control-panel: true
debug: false
openai-compatibility:
- name: "qbraid-gateway"
base-url: "$GATEWAY_URL"
api-key-entries:
- api-key: "$API_KEY"
models:
PEOF
printf '%s\n' "$GPT_MODELS" | while IFS= read -r gm; do
printf ' - name: "%s"\n alias: "%s"\n' "$gm" "$gm"
done
} > "$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"
Expand Down
Loading
Loading