# pi-copilot-custom **Repository Path**: shadowedge/pi-copilot-custom ## Basic Information - **Project Name**: pi-copilot-custom - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-10-08 - **Last Updated**: 2026-10-08 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Copilot Plus — network-resilient GitHub Copilot sign-in for Pi > 快速安装 / Quick start (Gitee): > > ```bash > pi install git:gitee.com/shadowedge/pi-copilot-custom # 安装(可加 @v1.0.0 锁 tag) > pi # 重启 pi > /login copilot-plus # 登录,会逐行显示重试进度 > ``` > > 卸载:`pi remove git:gitee.com/shadowedge/pi-copilot-custom`。 > 注意:需要先按 [Release checklist](#release-checklist) 把 `package.json`、`extensions/`、`test/` > 提交并推送,否则克隆到的只有 README,扩展不会注册。 A Pi provider package (`copilot-plus`) for GitHub Copilot that signs in reliably on a slow or flaky link, and **coexists** with pi's built-in `github-copilot` provider instead of replacing it. It exists because pi 1.1.0's Copilot device flow hard-codes budgets that a bad network blows through, producing: ``` Error: Failed to login to GitHub Copilot: fetch failed Error: Failed to login to GitHub Copilot: The operation was aborted due to timeout ``` …even when the browser authorization already succeeded. Measured on the affected host: the first cold TCP connect to `github.com` failed with `UND_ERR_CONNECT_TIMEOUT` after 10.8 s and the retry succeeded in 0.8 s, while every Copilot auth request was capped at 5 s with no retry. ## What it does differently | | pi built-in `github-copilot` | `copilot-plus` | |---|---|---| | per-request cap | 5 000 ms, hard-coded | 20 000 ms (`PI_COPILOT_PLUS_REQUEST_TIMEOUT_MS`) | | transient network errors | abort the login | retried with jittered backoff (2 per stage attempt, 12 stage attempts) | | total patience per stage | none | 240 s (`PI_COPILOT_PLUS_LOGIN_RETRY_BUDGET_MS`) | | HTTP 429 | 2 tries inside a 5 s budget | 8 tries inside 180 s, honours `Retry-After` | | device-code polling | dies on the first hiccup | hiccups keep polling until GitHub's own expiry | | login dialog while retrying | silent (looks frozen) | one line per attempt/retry | | failures | `fetch failed` | `fetch failed [stage: Requesting device code from github.com, 12 attempts, 55200ms, last error: UND_ERR_CONNECT_TIMEOUT]` | | Esc during login | immediate | immediate — an aborted signal is never retried | | credentials | `auth.json["github-copilot"]` | `auth.json["copilot-plus"]` (independent login) | Protocol is unchanged: same OAuth client id (`Iv1.b507a08c87ecfe98`), same endpoints (`/login/device/code`, `/login/oauth/access_token`, `/copilot_internal/v2/token`, `/models`, `/models//policy`), same picker/policy rules, same `proxy-ep` → API host derivation, same Copilot identity headers. The model catalog is **cloned from pi's own `github-copilot` catalog at runtime** (`getBuiltinModels("github-copilot")`), so no metadata is vendored and streaming still uses pi's built-in `anthropic-messages` / `openai-completions` / `openai-responses` implementations. ## Install The canonical source is the Gitee repository: ```bash # recommended: install from git (tracks the default branch) pi install git:gitee.com/shadowedge/pi-copilot-custom # pin a release tag so `pi update` cannot move it (tags and commits stay pinned) pi install git:gitee.com/shadowedge/pi-copilot-custom@v1.0.0 # try it for a single run without touching settings pi -e git:gitee.com/shadowedge/pi-copilot-custom # working on the code: install from a checkout, it loads straight from that path pi install ./E:/git/ref/pi-copilot-custom # then /reload inside pi after edits # once published to npm pi install npm:pi-copilot-plus pi install npm:pi-copilot-plus@1.0.0 ``` Accepted source spellings for git (pi parses them into the identity `git://`): | form | example | works | |---|---|---| | `git:` prefix | `git:gitee.com/shadowedge/pi-copilot-custom@v1.0.0` | yes | | plain https URL | `https://gitee.com/shadowedge/pi-copilot-custom` | yes | | https URL with `.git` | `https://gitee.com/shadowedge/pi-copilot-custom.git` | yes | | ssh URL | `ssh://git@gitee.com/shadowedge/pi-copilot-custom.git` | yes | | ref pinning | `...@v1.0.0`, `...@master`, `...@` | yes (`@` only) | | scp-style ssh | `git@gitee.com:shadowedge/pi-copilot-custom.git` | **no** — treated as a local path | | `git://` protocol | `git://gitee.com/…` | **no** | | `#ref` suffix | `…#v1.0.0` | **no** | `package.json` declares the extension through the `pi` manifest and lists the host-provided packages as `peerDependencies`, so nothing is bundled and there are no runtime dependencies. A git source is cloned to `~/.pi/agent/git///` (needs `git` on PATH and access to Gitee), and every accepted spelling normalizes to the same identity `git:gitee.com/shadowedge/pi-copilot-custom`, so `pi remove`/`pi update` accept any of them. Local paths and git URLs are *different identities*: if you switch from one to the other, remove the old entry first, otherwise both load. ### Update, inspect, uninstall ```bash pi list # configured packages pi update git:gitee.com/shadowedge/pi-copilot-custom # reconcile this checkout (unpinned refs only) pi update --extensions # all packages pi update --self # only pi itself pi remove git:gitee.com/shadowedge/pi-copilot-custom # drop it from settings (`uninstall` is an alias) ``` ### Verify an install ```bash pi --model copilot-plus/claude-fable-5 --print hi # before login: "No API key found for copilot-plus" pi --model copilot-plus/bogus-id --print hi # warns the model id is unknown pi --list-models copilot-plus # "No models available" until you log in pi list # the package and its resolved checkout path ``` Inside a session: `/copilot-plus status`. ### Release checklist `pi install git:…` clones the pushed commit, so the package files must be committed and pushed first. Only `README.md`/`README.en.md` are in the initial commit today; `package.json`, `extensions/`, `test/` and `.gitignore` are still untracked, which would install an empty package: ```bash cd E:/git/ref/pi-copilot-custom git add .gitignore package.json extensions test README.md git commit -m "feat: Copilot Plus provider with resilient Copilot sign-in" git tag v1.0.0 git push origin master --tags node test/flaky-scenarios.ts # 12/12 before you push pi install git:gitee.com/shadowedge/pi-copilot-custom@v1.0.0 ``` If the package files are not pushed, `pi install` still reports success (the clone works) but registers nothing, and the first model use fails with `Error: Model "copilot-plus/claude-fable-5" not found.` — check `pi list` shows the checkout and that the cloned directory contains `package.json`. Gitee also created a boilerplate `README.en.md` ("Software architecture description …"); delete it or point it at this README so installers do not read placeholder text. ## Sign in ``` /login copilot-plus # Tab-completes; or pick "GitHub Copilot Plus" in the list ``` You will see stage lines while it works, then the usual device-code panel with the verification URL and `Enter code: XXXX`. Note that pi only auto-opens the browser for `auth_url` flows; for device codes it prints the URL as a hyperlink (Ctrl+click). Pi never shows the device code until the device-code request succeeds, so on a bad link the old behaviour looked like a frozen dialog. Here is what a degraded route now looks like (real host, github.com unreachable): ``` + 0ms Requesting device code from github.com... + 12414ms Requesting device code from github.com failed (UND_ERR_CONNECT_TIMEOUT), retry 1/12 in 500ms - press Esc to cancel + 12918ms Requesting device code from github.com: attempt 2/12 + 46398ms Requesting device code from github.com failed (UND_ERR_CONNECT_TIMEOUT), retry 2/12 in 1218ms - press Esc to cancel ``` ## Use the models ```bash pi --model copilot-plus/claude-fable-5 # or pick it in /model pi --list-models copilot-plus # empty until you are logged in ("No models available") /copilot-plus status # tuning, auth state, catalog state, model ids /copilot-plus models # force a catalog refresh now ``` Both providers stay listed and independently authenticated, so you can migrate model by model, or keep using `github-copilot` for chat and `copilot-plus` only until the native flow is fixed. ## Tuning Read at provider construction (restart pi to pick up changes). `PI_COPILOT_PLUS_*` wins; `PI_COPILOT_*` is accepted as a fallback so one shell profile can also drive the `~/.pi/agent/patches/pi-copilot-timeouts.mjs` patch. | env var | default | meaning | |---|---|---| | `PI_COPILOT_PLUS_REQUEST_TIMEOUT_MS` | 20000 | per-attempt timeout | | `PI_COPILOT_PLUS_NET_RETRIES` | 2 | transient-network retries per stage attempt (`0` = off) | | `PI_COPILOT_PLUS_NET_RETRY_BUDGET_MS` | 45000 | network retry budget per stage attempt | | `PI_COPILOT_PLUS_NET_RETRY_BASE_MS` | 500 | backoff base | | `PI_COPILOT_PLUS_NET_RETRY_MAX_MS` | 4000 | backoff cap | | `PI_COPILOT_PLUS_STAGE_RETRIES` | 12 | attempts per login stage | | `PI_COPILOT_PLUS_LOGIN_RETRY_BUDGET_MS` | 240000 | total patience per stage | | `PI_COPILOT_PLUS_MAX_RETRIES` | 8 | HTTP 429 retries | | `PI_COPILOT_PLUS_RETRY_BUDGET_MS` | 180000 | HTTP 429 budget | Stock-like behaviour (fails fast, useful for reproducing the bug): `PI_COPILOT_PLUS_REQUEST_TIMEOUT_MS=5000 PI_COPILOT_PLUS_NET_RETRIES=0 PI_COPILOT_PLUS_STAGE_RETRIES=1`. Requests go through pi's global HTTP dispatcher, so `HTTP_PROXY`/`HTTPS_PROXY` or `"httpProxy"` in `~/.pi/agent/settings.json` apply here too — usually the real cure for "browser works, pi does not". `headersTimeout`/`bodyTimeout` come from `"httpIdleTimeoutMs"`. ## Layout ``` package.json pi manifest + peerDependencies (no bundled deps) extensions/copilot-plus.ts registerProvider("copilot-plus", …) + /copilot-plus command extensions/lib/config.ts env tuning extensions/lib/net.ts transient classifier, fetch retry wrapper, 429 handling, stages extensions/lib/copilot.ts device flow, polling, token exchange, catalog, policy, login/refresh extensions/lib/catalog.ts clone pi's github-copilot catalog, credential projection test/flaky-scenarios.ts offline regression suite incl. negative "stock-like" controls test/live-device-flow.ts real github.com device code, then cancel ``` `copilot.ts` and `net.ts` import nothing from pi, so the flow is testable against a fake GitHub. ## Tests ```bash node test/flaky-scenarios.ts # 5 scenarios + 2 negative controls + classifier checks node test/live-device-flow.ts # real network; exit code 2 means "github.com unreachable" ``` Current suite result: 12/12 pass. The two negative controls re-run the same scenarios with stock-like tuning and must still fail with `The operation was aborted due to timeout` / `fetch failed`, which keeps the suite honest. ## Limits and notes - Chat models only. Copilot image/classifier entries are not re-registered (they need pi's image/classifier implementations for a non-builtin provider id). - New Copilot models appear once pi's built-in catalog has them (package upgrade), because the catalog is cloned rather than vendored. Models GitHub reports but pi does not know yet are listed by `/copilot-plus status` rather than guessed into the picker. - If an availability list matches nothing known, the full static catalog is kept so the picker can never be emptied by a catalog change. - `pi auth check --provider copilot-plus` reports `provider_not_found`: that command does not load extensions. Inside a session, `/copilot-plus status` shows the real auth state. - Token refresh (`refreshToken`) keeps the same retry policy, so the daily expiry refresh on a bad link does not drop the session; if the *catalog* refresh fails during a token refresh, the previous model list is kept. - This package needs no edit of pi's files. The separate `~/.pi/agent/patches/pi-copilot-timeouts.mjs` patch fixes the *built-in* provider in place; they are independent and share env-var names.