pi-ccs

extension archived

UNMAINTAINED (final release 0.3.6): use CC Switch v3.20.0+ for native Pi support. Switch providers/models from local cc-switch, mirror them into models.json for subagents, and set per-subagent models.

by — · v0.3.6 · published 2d ago

$ pi install npm:pi-ccs
downloads/mo
284
stars
2
last push
2d ago
open issues
0

Signals

license: MITtestspi manifest: missinginstall size: —deps: 0peer deps: 0

Download trend

No downloads in the last 12 weeks.

README

pi-switch

CI npm License: MIT

English | 中文

[!CAUTION] This repository is unmaintained.

CC Switch v3.20.0 added first-class Pi support (ninth managed app): providers, prompts, skills, session browsing, and usage. Use CC Switch to manage Pi.

The last published package is pi-ccs@0.3.6 — a final exception carved out before the sunset: it makes cc-switch Providers resolvable in subagent child processes and adds the subagent model picker (see the Provider mirror section below). No further features, fixes, or support are planned after it. Existing installs keep working.

Successor

Use CC Switch ≥ v3.20.0:

  • Manage Pi providers in the CC Switch desktop app (writes ~/.pi/agent/models.json).
  • Prompts, SYSTEM.md / APPEND_SYSTEM.md, slash-command templates, and Skills are managed there too.
  • CC Switch never touches Pi login, defaultProvider, or defaultModel.
  • If you no longer need the in-Pi /ps-* switcher, uninstall npm:pi-ccs from Pi.

The documentation below describes the last release of this bridge. It is kept for historical reference.

pi-switch is a Pi extension package built on top of cc-switch. It uses cc-switch as the source of provider and model configuration, then exposes a fast provider/model switcher directly inside Pi.

pi-switch does not replace cc-switch and does not modify the cc-switch database. It reads the local cc-switch SQLite database in read-only mode, registers the selected provider in Pi, and stores the active model in Pi settings.

Preview

The screenshots below are sample illustrations of the interaction flow. Actual providers, models, and paths depend on your local cc-switch data.

Provider type picker

Model picker

Switch success

Features

  • Open the interactive switcher in Pi with /ps-config (optional alias: /ccs).
  • /ps quick switch: pins + recents on one screen, one Enter for the daily hot path.
  • Load provider configuration from the local cc-switch SQLite database in read-only mode.
  • Use a progressive three-level picker: provider type → provider name → model.
  • Search (/), manually enter model IDs, refresh remote lists, pin favorites with p, and page through long lists with PgUp/PgDn.
  • Remember last-N successful switches locally (no expose / multi-tool config center).
  • Parse and map common API protocols: Anthropic Messages, OpenAI Responses, OpenAI Chat Completions, and Google Generative AI.
  • Inject CLI-like fingerprints by default (Codex UA + originator + X-Codex-Window-ID, Claude Code claude-cli/... (external, cli) + anthropic-version/anthropic-beta, GeminiCLI UA + x-goog-api-client).
  • Override model parameters via presets or a native dialog (/ps-override or picker key o) — e.g. 中转兼容 sets reasoning=false when a relay rejects thinking.
  • Give subagents a different model than the session: picker key s or /ps-subagents writes subagents.defaultModel / subagents.agentOverrides.<agent>.model, discovering agent definitions from the project, user, and installed package roots.
  • Mirror every provider registration into Pi's models.json so a cc-switch Provider stays resolvable in processes that never loaded this extension — including pi-subagents' detached subagent runner.
  • For an exact model whose provider profile advertises ultra, /ps-override offers an explicit lossy Pi max -> provider ultra opt-in; it is never inferred globally.
  • Run structured health checks with /ps-doctor (PASS/WARN/FAIL + fix hints).
  • Run a read-only compatibility probe with /ps-probe (basic / reasoning / tool contracts, structured evidence, JSON in headless/CI).
  • Repair evidence-driven with /ps-repair (interactive only): re-probe → whitelist Recipe → confirm → in-memory candidate verify → CAS commit, without switching the Session Model.
  • Persist the latest selection so the next switcher session can highlight and reuse it.
  • Ship a diagnose-upstream skill as supplemental knowledge for upstream / relay troubleshooting.

See SPEC.md for the full product contract.

Built on cc-switch

cc-switch is the upstream configuration manager. pi-switch depends on the local cc-switch data model and treats cc-switch as the source of truth for providers.

pi-switch is intentionally scoped as a Pi-side bridge:

  • cc-switch owns provider creation, editing, deletion, and storage.
  • pi-switch reads cc-switch providers from ~/.cc-switch/cc-switch.db.
  • pi-switch normalizes provider settings into Pi-compatible provider registrations.
  • pi-switch switches the active Pi model without changing cc-switch state.

This means you should configure providers in cc-switch first, then use pi-switch to select and activate them inside Pi.

What this project is (and is not)

This project (Bandersnatch0x/pi-switch)Not this project
cc-switch → Pi bridgeLocal HTTP gateway / reverse proxy
Read-only consumer of cc-switch.dbAll-in-One provider CRUD manager
In-process Pi extension (/ps-config)Standalone daemon with WebUI
Local pin / recent shortcuts onlyMulti-tool expose / config center

If you need a local gateway that terminates requests and manages providers itself, look at projects such as @cokefenta/pi-switch / CallmeLins/pi-switch. This repo intentionally stays a thin Pi-side bridge on top of cc-switch.

Architecture

┌──────────────────────┐
│      cc-switch       │
│ provider management  │
└──────────┬───────────┘
           │ read-only SQLite
           ▼
┌──────────────────────┐
│      pi-switch       │
│ DB read + normalize  │
└──────────┬───────────┘
           │ parsed providers
           ▼
┌──────────────────────┐
│   interactive picker │
│ type → name → model  │
│   (+ override dialog)│
└──────────┬───────────┘
           │ selected provider/model
           ▼
┌──────────────────────┐
│          Pi          │
│ register + setModel  │
└──────────────────────┘

Main modules:

pi-switch/
├─ extensions/
│  └─ index.ts                 # Pi entry: /ps-config, /ps-doctor, /ps-override
├─ src/
│  ├─ db.ts                    # Read the cc-switch SQLite database
│  ├─ register.ts              # Build and register Pi providers
│  ├─ settings.ts              # Pi settings, selection, pins/recent, overrides
│  ├─ model-meta.ts            # modelMeta presets + resolution
│  ├─ doctor.ts                # /ps-doctor pure checks
│  ├─ sqlite-path.ts           # sqlite3 executable resolution
│  ├─ models-fetch.ts          # Remote model discovery and merging
│  ├─ headers/                 # Header rule loading, merge, and vars
│  ├─ parse/                   # cc-switch provider config parsers
│  └─ ui/
│     ├─ three-level-pick.ts   # Progressive type → name → model picker
│     ├─ model-meta-dialog.ts  # Non-interactive dialog (fallback / tests)
│     ├─ model-meta-form.ts    # TUI SettingsList form for modelMeta overrides
│     ├─ labels.ts             # Display labels and status text
│     └─ tabs.ts               # Tab helpers
├─ skills/
│  └─ diagnose-upstream/       # Upstream / relay diagnostics skill
├─ defaults/
│  └─ headers.json             # Default header rules
├─ docs/
│  └─ images/                  # README sample screenshots
├─ tests/                      # Bun tests
├─ SPEC.md                     # Product contract (maintainers)
└─ package.json

Installation

From npm (recommended)

pi install npm:pi-ccs

After the package is published publicly on npm with the pi-package keyword, it can also appear in the Pi package catalog. There is no separate submission form — catalog discovery is based on public npm metadata (keywords includes pi-package, plus a valid package.json pi manifest).

Direct catalog page after listing:

https://pi.dev/packages/pi-ccs

From GitHub

pi install git:github.com/Bandersnatch0x/pi-switch

Git installs work even before npm / catalog listing.

Update and enable

pi update npm:pi-ccs
pi config

Pi packages usually land under ~/.pi/agent/npm/. With project-local installation, they are placed under .pi/npm/ in the current project.

Usage

In a Pi session, run:

/ps-config

Aliases:

/ccs

Quick switch for the hot path (pins + recents, one screen):

/ps

To edit model parameter overrides (for example disable reasoning for a Claude-protocol → GLM relay):

/ps-override

In the provider picker, after the Name column is revealed, press o to open the same override dialog for the focused provider. The footer shows o override.

Press s in the same picker to assign a model to subagents (see below); the footer shows s subagent.

Subagent models

pi-subagents resolves each child's model in this order:

per-run `model`  →  subagents.agentOverrides.<agent>.model  →
agent frontmatter `model`  →  subagents.defaultModel  →  parent session model

pi-switch owns the two settings layers, so a review or research subagent can run a different model than the session you are typing in.

/ps-subagents

Or press s in the provider picker once a provider is focused — the focused provider/model becomes a one-keystroke choice for the subagent you pick next.

The flow lists the global default row plus every discovered agent (<project>/.pi/agents, ~/.pi/agent/agents, and the installed pi-subagents package's agents/; first root wins per name). Each row shows which layer currently supplies the model — override / frontmatter / default / inherit session model — and the value list offers:

  • use the focused provider/model (only when s came from the picker),
  • clear the layer so it falls back to the next one,
  • pick a cc-switch Provider/Model (read-only, same picker as /ps-probe),
  • type any model id, including one this DB does not list.

Writes go into the host's settings.json atomically (subagents.defaultModel / subagents.agentOverrides.<agent>.model), preserving every other key; clearing a value prunes the empty scaffolding instead of leaving {} behind.

A subagent runs in its own process, so it can only resolve a provider that exists outside the parent session. pi-switch mirrors its registrations into models.json for exactly that reason — see Provider mirror. Providers that only exist in Pi's built-in catalog (for example radius/…) work without the mirror.

Compatibility probe & repair

Switching a provider/model means “the model is listed” ≠ “requests actually work”. Verify and repair out-of-band:

/ps-probe

Read-only probe against the current/selected Target. Sends isolated synthetic requests (never your conversation history):

  • basic — plain-text contract
  • reasoning — controlled thinking contract (only when the target claims reasoning support)
  • tool — side-effect-free probe_echo tool contract

Outputs structured evidence grouped into wide failure categories (auth / model / protocol / streaming / tool / client-gate); ambiguous evidence yields unknown (no guessing). Hard budget: max 9 requests, 15s each, ≤32 output tokens; 401/429/5xx stop immediately. headless/CI emits JSON.

/ps-repair

Evidence-driven repair, interactive only (headless is rejected — a persistent config change needs consent). Re-probes fresh each run → matches a whitelist Repair Recipe → one plan-level confirmation (target, recipe order, each patch, affected models) → candidate verified on an in-memory probe target first (the same contract must pass twice consecutively before commit) → CAS commit of one recipe. Session Model stays unchanged; an explicit “switch to repaired target” is offered on success.

First-version whitelist recipes:

  1. Upstream rejects reasoning/thinking → exact-model reasoning=false.
  2. Client fingerprint gate, signature uniquely mapped to Claude Code / Codex / Gemini → provider-level fingerprint (+ optional claudeCodeCompat); non-unique → unknown.
  3. Gemini tool empty-args / schema evidence → enable per-provider geminiToolCompat (report-only when already enabled and still failing).

Every write re-checks the config version (CAS); a concurrent external edit aborts the repair and preserves external content. Each probe/repair is recorded as a Repair Case (redacted summary in context, detailed redacted evidence out of context).

Typical flow:

  1. Choose a provider type, such as Claude Code, Codex, Gemini, or OpenCode.
  2. Choose a specific provider.
  3. Choose a model, or manually enter a model ID.
  4. pi-switch registers the provider and switches the current Pi model.

After selection, Pi uses the selected provider baseUrl, apiKey, protocol type, and model ID for subsequent requests.

Override dialog

In a terminal (TUI) Pi runs a single-screen SettingsList overlay form (Pi's own settings-list primitive): one row per field, Enter/Space cycles enum values, count/预设/作用域 rows open a SelectList submenu, custom counts accept a 200k / 1M input. In non-interactive modes (RPC / headless / tests) it falls back to the chained select / input / confirm popup in model-meta-dialog.ts. Both paths return the same result shape.

Parameter override · elysiver-claude · model glm-4.6 ✱
  scope              model glm-g4           ▸   § submenu switch layer
  preset             select…               ▸   § relay-safe / full-reasoning
  reasoning          inherit true             ∘ inline: § true / false / inherit
  contextWindow      override 200k           ▸   § 200k 256k 500k 1M / custom
  maxTokens          default 64k             ▸   § 4k 8k 16k 32k 64k 128k / custom
  thinkingFormat     override deepseek     ∘   inline-cycle enum
  Provider ultra     available (Pi max -> provider ultra)  § exact-model opt-in
  — clear this layer                  ▸
  — clear all for provider           ▸
  save                                   ✱ save (Title shows ✱ when dirty)
  cancel
Enter/Space switch or open submenu · Esc back · s save

Each row reads one of four states: override (set in this scope), inherit (a lower user-config layer), built-in (built-in compat profile), default (protocol tier). Count fields offer common presets (200k, 256k, 500k, 1M) plus custom input (k/M suffix). Saving writes providerOverrides keyed by the cc-switch dbId; model-scope edits go under modelOverrides[modelId] (default scope is the preselected model when opened from the picker's o key; the § submenu switches to provider-scope or another model/glob). If that provider is currently active, pi-switch re-registers it immediately.

Requirements

  • Pi is installed and extension packages are enabled.
  • cc-switch is installed and configured.
  • The local cc-switch database exists.
  • sqlite3 is available on the system.

Default database path:

~/.cc-switch/cc-switch.db

sqlite3 resolution order:

SQLITE3_PATH → ~/.pi/agent/pi-switch.json sqlitePath → sqlite3 from PATH

Windows users should explicitly configure SQLITE3_PATH if sqlite3.exe is not globally available.

Configuration

Optional configuration file:

~/.pi/agent/pi-switch.json

Example:

{
  "sqlitePath": "C:/tools/sqlite3.exe",
  "tabs": ["claude", "codex", "gemini", "opencode"],
  "vars": {
    "codexVersion": "0.144.5",
    "claudeCodeVersion": "2.1.190"
  },
  "debug": false
}
FieldDescription
sqlitePathOverrides the sqlite3 executable path (null disables lookup)
tabsPreferred provider-type order in the picker
varsOptional overrides for UA template versions (otherwise auto-detected)
providerOverridesPer-provider label, fingerprint, headers, modelMeta, and per-model modelOverrides (keyed by dbId)
aliasCcsRegister /ccs alias (default true)
persistProvidersMirror in-process registrations into Pi's models.json (default true). false keeps provider definitions process-local
debugEnables debug output

Database path is not in this file — use env CC_SWITCH_DB or the default ~/.cc-switch/cc-switch.db.

Provider mirror (models.json)

pi.registerProvider() only affects the process that ran it. A subagent child builds its own model registry — pi-subagents' detached runner never inherits the parent's — so a provider/model that exists only inside the parent session fails to resolve there and the child dies before its first request.

With persistProviders: true (default) pi-switch therefore mirrors each registration into Pi's cross-process provider file:

~/.pi/agent/models.json                           # providers.<pi-name>: baseUrl/apiKey/api/headers/models
~/.pi/agent/pi-switch-persisted-providers.json    # which entries pi-switch owns

Both follow PI_CODING_AGENT_DIR (and ~) when the host moved its agent directory. Rules that keep the file safe to share with the user and other tools:

  • Only providers.<name> is touched; every other key and provider is preserved.
  • Only entries pi-switch wrote (recorded in the sidecar by content digest) are rewritten or pruned. A same-named entry with foreign content is reported and left untouched.
  • Writes are atomic with compare-and-swap, so a concurrent external edit aborts instead of being clobbered.
  • Switching providers prunes the previous entry; probe/repair targets (runtime-only activations) are never mirrored.
  • A models.json that is not valid JSON is left alone with a warning rather than rebuilt from scratch.
  • persistProviders: false keeps provider definitions and API keys out of models.json; subagents then need a model Pi can resolve without this extension.

Uninstalling: set persistProviders: false, restart Pi, then switch to a non-cc-switch model so the mirror is pruned — or delete the pi-switch-persisted-providers.json sidecar's listed entries from models.json by hand. The sidecar never contains keys itself.

Parameter overrides (providerOverrides)

Some gateways reject Anthropic-style fields. A common case is Claude-protocol → GLM relays returning:

Unsupported parameter(s): `reasoning`

Use the popup dialog (/ps-override or picker key o) to set modelMeta.reasoning to false, and optionally set a short label. The dialog is scope-aware: edit 全部模型 (provider level) or pick one model id. Values are persisted under the provider's cc-switch dbId in ~/.pi/agent/pi-switch.json:

{
  "providerOverrides": {
    "claude": {
      "dooongai-1775180253543": {
        "label": "elysiver-claude",
        "modelMeta": {
          "reasoning": false
        },
        "modelOverrides": {
          "glm-4.6": { "reasoning": false, "maxTokens": 8192 },
          "gpt-5*":  { "reasoning": true }
        }
      }
    }
  }
}

Layering (later wins per field, unset fields never clobber a lower layer):

defaultModelMeta  ⊕  providerOverrides[appType][dbId].modelMeta  ⊕  providerOverrides[appType][dbId].modelOverrides[modelId]

The canonical override path is providerOverrides.<appType>.<dbId>; legacy top-level dbId entries remain readable and are absorbed into the canonical layer on write. modelOverrides keys may be exact ids or globs (gpt-5* / *sonnet*). Match order: exact → case-insensitive → most specific glob.

Optional fingerprint field forces a CLI disguise preset regardless of protocol:

ValueEffect
claude-codeclaude-cli/<ver> (external, cli) + anthropic version/beta
codexcodex_cli_rs/<ver> (...) + originator + per-process X-Codex-Window-ID
geminiGeminiCLI/<ver> + x-goog-api-client
noneSkip default/api-matched rule injection; only explicit headers (if any) remain

Explicit headers always win over the preset on conflicts.

Supported modelMeta fields (stored flat in pi-switch.json; registration reshapes into Pi's modern layout):

FieldDescription
reasoningWhether Pi may send reasoning/thinking parameters
thinkingFormatOne of: openai / openrouter / together / deepseek / zai / qwen / chat-template / qwen-chat-template / string-thinking / ant-ling → registered as compat.thinkingFormat
contextWindowContext window size (drives Pi compact: contextTokens > contextWindow - reserveTokens)
maxTokensMax output tokens
thinkingLevelMapOptional map of Pi levels (off / minimal / low / medium / high / xhigh / max) → provider effort strings, or null for unsupported → registered top-level
supportsDeveloperRoleOpenAI-compatible upstream accepts role: "developer"; set true to preserve it, otherwise pi-switch conservatively rewrites it to system
requiresReasoningContentOnAssistantMessagesOpenAI-compat: require empty reasoning_content on assistant turns → registered under compat
useBuiltInCompatpi-switch only (not sent to Pi): false disables the whole built-in compat profile; unset/true keeps the default (apply when id matches)

The UI edits the common scalar fields (reasoning / thinkingFormat / contextWindow / maxTokens) plus the 内置compat toggle. A full arbitrary thinkingLevelMap editor is intentionally not exposed. The only UI map operation is Provider ultra: it appears at exact-model scope only when the shared provider profile advertises ultra, writes { "max": "ultra" } through a tuple-validated writer, and warns that provider-native max becomes unavailable. ultracode is not treated as an alias. Other object-map edits remain config/API-only for backwards compatibility. Each form row shows override / inherit / built-in / default (built-in = matched profile and not opted out).

/ps-doctor and /ps-info render the same registration-time thinking projection, including exact/lossy status, collisions, provider-default behavior, unrepresented native values, unsupported runtime controls, and stale last-good evidence. A reviewed Pi release with an unsupported tuple/control is shown as unsupported-runtime; an unreviewed release is unverified. Pi 0.84.2 is in the reviewed payload-fixture matrix. Neither display reconstructs a profile from the model name.

An advanced exact-model profile can describe custom/relay reasoning controls that are not in the reviewed built-in table. It is accepted only at this path (no provider/default/glob scope):

{
  "providerOverrides": {
    "hermes": {
      "<provider-db-id>": {
        "modelOverrides": {
          "<exact-model-id>": {
            "reasoningProfile": {
              "profileVersion": "relay-contract/v1",
              "control": { "type": "toggle" },
              "variants": [
                {
                  "name": "off",
                  "native": { "type": "toggle", "enabled": false },
                  "piLevel": "off",
                  "effectiveLevel": "off"
                },
                {
                  "name": "high",
                  "native": { "type": "toggle", "enabled": true },
                  "piLevel": "high",
                  "effectiveLevel": "high"
                }
              ],
              "observedAt": "2026-08-18T00:00:00.000Z"
            }
          }
        }
      }
    }
  }
}

Profile authority is exact user > provider snapshot metadata/catalog > reviewed built-in. pi-switch derives the full provider tuple and source=user from the exact config scope. Provider metadata may express complete effort, toggle, token-budget, or composite native variants; Codex catalog { "value": "high" } remains an effort-only shorthand.

Advanced fields such as supportsDeveloperRole (exact-model tuple / flat meta) are config-only (edit pi-switch.json or call the write APIs).

Built-in compat profiles

models.dev covers capability scalars (contextWindow / maxTokens / reasoning) only. Some models also need compat fields such as thinkingFormat to register correctly. pi-switch ships a small in-code profile table for known families:

Model id matchBuilt-in fields
deepseek*thinkingFormat=deepseek, requiresReasoningContentOnAssistantMessages=true, DeepSeek-style thinkingLevelMap
qwen*thinkingFormat=qwen

Precedence: user override > built-in profile. Profiles never set contextWindow / maxTokens / reasoning (those still flow models.dev → protocol default).

Disable the whole profile: in /ps-override set 内置compat → 关闭, or write:

{
  "modelOverrides": {
    "deepseek-v4-flash": { "useBuiltInCompat": false }
  }
}

Provider-scope modelMeta.useBuiltInCompat: false turns it off for all matching models under that provider; a model-scope true re-enables one id. /ps-doctor and the post-switch notify share the same effective meta; doctor sources look like 用户: …;内置: deepseek* (omitted when disabled).

DeepSeek V4 Flash example

Compact is executed by Pi itself; pi-switch does not compress sessions. Per-switch registration means the same model id can use different contextWindow / compat under different providers.

Thinking compat for deepseek* is already supplied by the built-in profile. To raise the window / enable reasoning when models.dev misses, override only the capability fields:

{
  "providerOverrides": {
    "hermes": {
      "<dbId>": {
        "modelOverrides": {
          "deepseek-v4-flash": {
            "reasoning": true,
            "contextWindow": 1000000,
            "maxTokens": 384000
          }
        }
      }
    }
  }
}

At register time this becomes Pi model config with top-level thinkingLevelMap and nested compat (no top-level thinkingFormat). The [1M] model-id tag only sets contextWindow=1000000; it does not replace the DeepSeek thinking map (that comes from the built-in profile or a user override).

After save, if the provider is currently active, pi-switch re-registers it so the override applies immediately.

The latest selection is stored as piSwitchSelection in Pi settings, so it can be highlighted the next time the switcher opens.

Note: remote model list fetching currently returns model IDs only. Per-model parameters are not imported from /models; use protocol defaults plus built-in compat profiles plus providerOverrides.modelMeta / modelOverrides instead.

Header Rules

Default header rules are stored at:

defaults/headers.json

Optional user override file:

~/.pi/agent/provider-headers.json

pi-switch only merges allowlisted headers to avoid injecting arbitrary sensitive fields into provider configuration. Allowlist:

HeaderDefault rules inject?Notes
User-AgentyesVersion/os auto-detected; overridable per provider / fingerprint
anthropic-versionyes (claude)Protocol-required for Anthropic Messages
anthropic-betayes (claude)Claude Code beta flags (template via vars.anthropicBeta)
originatoryes (codex)Codex CLI private header (template via vars.codexOriginator)
X-Codex-Window-IDyes (codex)Per-process UUID required by official-client relay gates
x-goog-api-clientyes (gemini)Gemini CLI client id (gemini-cli/<ver>)

Authorization / x-api-key / Host / etc. are never injectable via rules or overrides.

Rule precedence: defaults/headers.json < ~/.pi/agent/provider-headers.json < providerOverrides[dbId].headers.

Branch protection

Branch and release-tag protection is documented in .github/branch-protection.md.

Development

Install dependencies:

bun install

Run tests:

bun test

Typecheck:

bun run typecheck

Pre-publish check:

bun run prepublishOnly

Run the isolated TUI smoke (requires pi and sqlite3 on PATH):

bun run smoke:tui

This drives the interactive slash commands through a Pi RPC subprocess under a temporary HOME with a faux OpenAI relay, asserting on state outcomes rather than visual rendering. Real settings.json, pi-switch.json, models.json, the pi-switch ownership sidecar, the cc-switch DB, and its SQLite sidecars are snapshotted and verified unchanged even when a flow fails. It covers the six main flows:

  • /ps-override — provider-scope modelMeta write round-trip.
  • /ps-config — 3-level pick, provider registration, selection persistence, and the models.json provider mirror (entry + ownership record).
  • /ps-info — effective-config summary.
  • /ps-doctor — diagnostics (offline models.dev/routing items degrade to warn, not fail).
  • /ps — quick switch off a pinned/recent entry.
  • /ps-subagents — target list → value list → manual id entry, asserting subagents.defaultModel lands in settings.json.

Use --flow=<name> to run one flow, or KEEP_SMOKE_TEMP=1 to retain the temp HOME for inspection.

Run the isolated end-to-end /ps-repair smoke (requires pi and sqlite3 on PATH):

bun run smoke:probe-repair

This starts a local faux OpenAI relay and a Pi RPC subprocess under a temporary HOME. It runs 3 repair scenarios against the same faux target:

  1. reasoning-false — The faux target passes basic/tool requests but rejects reasoning, matching reasoning-false. Writes modelOverrides[model].reasoning=false.
  2. client-fingerprint — Sets fingerprint="codex"; the relay validates real originator: codex_cli_rs headers and User-Agent.
  3. gemini-tool-compat — Sets geminiToolCompat=true; the relay validates Gemini-style payload (toolConfig.functionCallingConfig.mode=AUTO, parameters instead of parametersJsonSchema).

Each scenario: verifies the candidate twice, declines the post-repair Session Model switch, asserts real Pi settings/config and cc-switch DB state remain unchanged, and deletes temporary state after success. Use --recipe=<id> to run a single scenario, or --keep / KEEP_SMOKE_TEMP=1 to retain temp files.

End-to-end: detached subagent resolves a mirrored provider

bun run smoke:subagent-mirror

Requires pi, sqlite3, node, and pi-subagents installed under ~/.pi/agent/npm/node_modules/pi-subagents. It is the only check that exercises the real thing the mirror exists for — a detached subagent child process resolving a cc-switch Provider:

  1. /ps-config switches to the temp DB's provider → asserts the mirrored entry (base URL, model) and the ownership sidecar in the temp HOME's models.json.
  2. /run delegate[model=<provider>/<model>] … --bg → the detached runner starts; pi-subagents' run status records the resolved provider/model, the child's request reaches the faux relay, and the child output is the relay's text.
  3. Negative control: the mirror entry is deleted, the same run is dispatched again, and it fails without ever reaching the relay (the silent-death signature: workflow run stuck running, no child step, no resolved model).
  4. Recovery: switching again rebuilds the mirror and the same run succeeds.

Ambient extensions are disabled for the children (subagents.defaultExtensions: []), so only models.json can supply the provider — the check cannot be satisfied by the parent process registry. Real settings.json, pi-switch.json, models.json and the sidecar are verified unchanged; the real cc-switch DB is excluded because the CC Switch desktop app writes it concurrently.

The Pi subprocess is spawned through Node here: Bun 1.3.11 panics (cannot resolve DirInfo for non-absolute path) inside pi-subagents' detached runner, which kills Pi. The other smokes run fine under bun run.

Release and GitHub auto-publish

Publishing is modeled after a release-gate flow (similar to vibe-designing-playbook):

  1. Local dry-run gates (tree / version / test / pack / tag)
  2. Create vX.Y.Z tag after gates pass
  3. Push the tag; GitHub Actions publishes to npm

One-time setup on GitHub:

  1. Create an npm Automation access token with publish permission
  2. Repo → Settings → Secrets and variables → Actions → New repository secret
  3. Name: NPM_TOKEN, value: the token

Release steps:

# 1) bump version in package.json (keep semver)
# 2) commit all release changes
bun run release              # dry-run gates (no tag)
bun run release:apply        # create tag vX.Y.Z after gates pass
git push origin main
git push origin v0.1.0       # triggers Actions publish

Manual re-publish is also available from Actions → CI → Run workflow with publish=true (the matching vX.Y.Z tag must already point at that commit).

The workflow:

  • runs tests + pack dry-run on push/PR
  • publishes only on v* tags (or manual dispatch)
  • verifies tag version == package.json version
  • skips if that version already exists on npm
  • uses npm publish --access public --provenance

Supported Configuration Sources

pi-switch parses provider configuration from the cc-switch providers table and normalizes it into Pi-registerable providers where possible.

  • Claude / Claude Code config parsing
  • Codex config parsing
  • Gemini config parsing
  • Grok Build config parsing
  • OpenCode config parsing
  • Hermes config parsing
  • Generic / OpenAI-compatible config parsing

If a provider protocol cannot be mapped to a Pi-supported API type, it is shown as non-switchable in the UI instead of being force-registered.

Out of Scope

  • Does not edit the cc-switch database.
  • Does not add, delete, reorder, or migrate providers.
  • Does not include an API key manager.
  • Does not track quota or cost.
  • Does not replace cc-switch; it only acts as a switcher entry inside Pi.
  • Does not import per-model metadata from remote /models responses (IDs only).

License

MIT