@velocirouter/pi 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +51 -0
  2. package/index.js +86 -0
  3. package/package.json +27 -0
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # @velocirouter/pi
2
+
3
+ [Pi](https://pi.dev) package for [VelociRouter](https://velocirouter.site).
4
+ It registers VelociRouter as a provider and loads the model catalog from the
5
+ API.
6
+
7
+ Requires **Pi 0.85.1 or later** from `@earendil-works/pi-coding-agent`.
8
+ The older `@mariozechner/pi-coding-agent` distribution does not support this
9
+ package's catalog refresh. Upgrade Pi before installing the package:
10
+
11
+ ```bash
12
+ npm uninstall -g @mariozechner/pi-coding-agent
13
+ npm install -g @earendil-works/pi-coding-agent@latest
14
+ pi install npm:@velocirouter/pi
15
+ ```
16
+
17
+ Then run `/login` inside pi, pick **VelociRouter**, and paste your `rt-…` key.
18
+ Pi refreshes the catalog right after login, so the models show up in `/model`
19
+ straight away.
20
+
21
+ ## What it registers
22
+
23
+ - **Provider** — `velocirouter`, an `openai-completions` endpoint.
24
+ - **Login** — pi's built-in API-key login under `/login`.
25
+ - **Models** — fetched from `/v1/models` and cached in pi's model store, so new
26
+ models appear without upgrading this package. Image support and the
27
+ reasoning-effort ladder come from the API; levels a model does not support
28
+ are hidden from pi's thinking picker. The context window is the API's
29
+ combined window minus the output reservation: pi compacts once accumulated
30
+ tokens pass it, and those tokens are the next request's input, so the
31
+ combined figure would let a long session overshoot the input ceiling.
32
+
33
+ Costs are reported as zero: VelociRouter bills your account directly, so pi's
34
+ per-session cost estimate would double-count.
35
+
36
+ The catalog is fetched when an interactive session starts, after `/login`, and
37
+ from the `/model` picker. `pi -p` and `pi --list-models` read the cached copy,
38
+ so open pi interactively once before using them.
39
+
40
+ ## Environment variables
41
+
42
+ | Variable | Effect |
43
+ | --- | --- |
44
+ | `VELOCIROUTER_API_KEY` | Used when no key was stored by `/login`. Set this in CI. |
45
+ | `VELOCIROUTER_BASE_URL` | Override the API host (default `https://api.velocirouter.site`). |
46
+
47
+ ## Publishing
48
+
49
+ ```bash
50
+ npm publish --access public
51
+ ```
package/index.js ADDED
@@ -0,0 +1,86 @@
1
+ const PROVIDER = "velocirouter"
2
+ const API = "openai-completions"
3
+ const DEFAULT_BASE_URL = "https://api.velocirouter.site"
4
+ const DEFAULT_CONTEXT_WINDOW = 128_000
5
+ const NO_COST = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }
6
+ const THINKING_LEVELS = ["minimal", "low", "medium", "high", "xhigh", "max"]
7
+
8
+ function baseUrl() {
9
+ const configured = process.env.VELOCIROUTER_BASE_URL || DEFAULT_BASE_URL
10
+ return `${configured.trim().replace(/\/+$/, "")}/v1`
11
+ }
12
+
13
+ async function getJson(url, apiKey, signal) {
14
+ const response = await fetch(url, {
15
+ headers: { Accept: "application/json", Authorization: `Bearer ${apiKey}` },
16
+ signal,
17
+ })
18
+ if (!response.ok) {
19
+ throw new Error(`VelociRouter returned HTTP ${response.status} for ${url}`)
20
+ }
21
+ return response.json()
22
+ }
23
+
24
+ // pi hides xhigh/max unless mapped and treats null as unsupported. The relay
25
+ // spells thinking-off as "none", which pi only sends when `off` is mapped.
26
+ function thinkingLevelMap(efforts) {
27
+ const supported = new Set(efforts)
28
+ const map = Object.fromEntries(THINKING_LEVELS.map((level) => [level, supported.has(level) ? level : null]))
29
+ map.off = supported.has("none") ? "none" : null
30
+ return map
31
+ }
32
+
33
+ function toModel(row) {
34
+ const totalWindow = row.context_length || row.context_window || DEFAULT_CONTEXT_WINDOW
35
+ const maxTokens = row.max_output_tokens || Math.min(totalWindow, DEFAULT_CONTEXT_WINDOW)
36
+ const efforts = row.reasoning_efforts ?? []
37
+ const input = (row.input_modalities ?? []).filter((modality) => modality === "text" || modality === "image")
38
+ return {
39
+ id: row.id,
40
+ name: row.display_name || row.id,
41
+ reasoning: Boolean(row.reasoning),
42
+ ...(row.reasoning && efforts.length ? { thinkingLevelMap: thinkingLevelMap(efforts) } : {}),
43
+ input: input.length ? input : ["text"],
44
+ cost: { ...NO_COST },
45
+ // pi compacts once accumulated tokens pass contextWindow, and those tokens
46
+ // are the next request's input. The API reports the combined window, so
47
+ // reserve the output share or a long session overshoots the input ceiling.
48
+ contextWindow: totalWindow > maxTokens ? totalWindow - maxTokens : totalWindow,
49
+ maxTokens,
50
+ }
51
+ }
52
+
53
+ // Stored models carry the baseUrl they were fetched from; dropping it lets
54
+ // the provider-level one win when VELOCIROUTER_BASE_URL changes.
55
+ function restore(stored) {
56
+ return stored?.models.filter((model) => model.provider === PROVIDER).map(({ baseUrl, ...model }) => model)
57
+ }
58
+
59
+ async function refreshModels({ credential, stored, publish, allowNetwork, signal }) {
60
+ if (!allowNetwork || !credential?.key) {
61
+ return restore(stored)
62
+ }
63
+ const payload = await getJson(`${baseUrl()}/models`, credential.key, signal)
64
+ const rows = Array.isArray(payload?.data) ? payload.data : []
65
+ const models = rows.filter((row) => typeof row?.id === "string" && row.id).map(toModel)
66
+ // An empty catalog would wipe the cache; keep what was there.
67
+ if (!models.length) {
68
+ return restore(stored)
69
+ }
70
+ const persisted = models.map((model) => ({ ...model, api: API, provider: PROVIDER, baseUrl: baseUrl() }))
71
+ await publish({ persist: { models: persisted, checkedAt: Date.now() } })
72
+ return models
73
+ }
74
+
75
+ export default function velocirouter(pi) {
76
+ pi.registerProvider(PROVIDER, {
77
+ name: "VelociRouter",
78
+ baseUrl: baseUrl(),
79
+ api: API,
80
+ apiKey: "$VELOCIROUTER_API_KEY",
81
+ // pi's User-Agent is "pi (linux ...)", so the relay cannot identify the
82
+ // app without these.
83
+ headers: { "HTTP-Referer": "https://pi.dev", "X-Title": "Pi" },
84
+ refreshModels,
85
+ })
86
+ }
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@velocirouter/pi",
3
+ "version": "1.0.0",
4
+ "description": "Pi coding agent package for VelociRouter — registers the provider and its model catalog.",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "license": "MIT",
8
+ "files": [
9
+ "index.js",
10
+ "README.md"
11
+ ],
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
15
+ "pi": {
16
+ "extensions": [
17
+ "index.js"
18
+ ]
19
+ },
20
+ "keywords": [
21
+ "pi-package",
22
+ "pi",
23
+ "pi-coding-agent",
24
+ "velocirouter"
25
+ ],
26
+ "homepage": "https://velocirouter.site/docs/coding-agents/pi"
27
+ }