@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.
- package/README.md +51 -0
- package/index.js +86 -0
- 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
|
+
}
|