@velocirouter/pi 1.0.1 → 1.1.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 CHANGED
@@ -4,13 +4,10 @@
4
4
  It registers VelociRouter as a provider and loads the model catalog from the
5
5
  API.
6
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:
7
+ Requires **Pi 0.86.1 or later** from `@earendil-works/pi-coding-agent`.
8
+ If you already have a supported version, install the package:
10
9
 
11
10
  ```bash
12
- npm uninstall -g @mariozechner/pi-coding-agent
13
- npm install -g @earendil-works/pi-coding-agent@latest
14
11
  pi install npm:@velocirouter/pi
15
12
  ```
16
13
 
@@ -18,6 +15,27 @@ Then run `/login` inside pi, pick **VelociRouter**, and paste your `rt-…` key.
18
15
  Pi refreshes the catalog right after login, so the models show up in `/model`
19
16
  straight away.
20
17
 
18
+ ## Install or update Pi (if needed)
19
+
20
+ If Pi is not installed, or your version is below 0.86.1, install the latest
21
+ version, then run the package installation command above:
22
+
23
+ ```bash
24
+ npm install -g @earendil-works/pi-coding-agent@latest
25
+ ```
26
+
27
+ ### Migrating from the legacy npm package
28
+
29
+ Only if you installed Pi through `@mariozechner/pi-coding-agent`, replace it
30
+ with the current distribution using the commands below. The legacy distribution
31
+ does not support this package's catalog refresh. Then run the package
32
+ installation command above.
33
+
34
+ ```bash
35
+ npm uninstall -g @mariozechner/pi-coding-agent
36
+ npm install -g @earendil-works/pi-coding-agent@latest
37
+ ```
38
+
21
39
  ## What it registers
22
40
 
23
41
  - **Provider** — `velocirouter`, an `openai-completions` endpoint.
@@ -30,8 +48,11 @@ straight away.
30
48
  tokens pass it, and those tokens are the next request's input, so the
31
49
  combined figure would let a long session overshoot the input ceiling.
32
50
 
33
- Costs are reported as zero: VelociRouter bills your account directly, so pi's
34
- per-session cost estimate would double-count.
51
+ Session costs use the billed USD total returned by VelociRouter in streaming
52
+ `usage.cost`. Enable **Cost in response** for your user in VelociRouter first.
53
+ This is reporting only; it does not add another charge. If the server omits cost,
54
+ the default model prices remain zero. Per-category cost breakdowns are not
55
+ available, and earlier turns recorded with zero cost are not updated.
35
56
 
36
57
  The catalog is fetched when an interactive session starts, after `/login`, and
37
58
  from the `/model` picker. `pi -p` and `pi --list-models` read the cached copy,
@@ -44,6 +65,19 @@ so open pi interactively once before using them.
44
65
  | `VELOCIROUTER_API_KEY` | Used when no key was stored by `/login`. Set this in CI. |
45
66
  | `VELOCIROUTER_BASE_URL` | Override the API host (default `https://api.velocirouter.site`). |
46
67
 
68
+ ## Development checks
69
+
70
+ Run `npm test` with the Pi AI peer dependency available. To also check host
71
+ module resolution and streaming through Pi's actual extension loader, run:
72
+
73
+ ```bash
74
+ JITI_FS_CACHE=0 PI_TEST_INSTALL_DIR="$(npm root -g)/@earendil-works/pi-coding-agent" \
75
+ node --test loader.test.js
76
+ ```
77
+
78
+ The loader check uses the host Pi installation and needs no local Pi AI install.
79
+ It is skipped when `PI_TEST_INSTALL_DIR` is unset.
80
+
47
81
  ## Publishing
48
82
 
49
83
  ```bash
package/cost-stream.js ADDED
@@ -0,0 +1,71 @@
1
+ import { createAssistantMessageEventStream, openAICompletionsApi } from "@earendil-works/pi-ai/compat"
2
+
3
+ const completions = openAICompletionsApi()
4
+
5
+ // Observe bytes inline: cloning the response would buffer a second copy and
6
+ // could finish reading usage after Pi has already emitted its final message.
7
+ function observeCost(body, onCost) {
8
+ const decoder = new TextDecoder()
9
+ let pending = ""
10
+ function consume(text) {
11
+ pending += text
12
+ let boundary
13
+ while ((boundary = /\r\n\r\n|\n\n|\r\r/.exec(pending))) {
14
+ const event = pending.slice(0, boundary.index)
15
+ pending = pending.slice(boundary.index + boundary[0].length)
16
+ const data = event.split(/\r\n|\r|\n/)
17
+ .filter((line) => line.startsWith("data:"))
18
+ .map((line) => line.slice(5).replace(/^ /, ""))
19
+ .join("\n")
20
+ try {
21
+ const cost = JSON.parse(data)?.usage?.cost
22
+ if (typeof cost === "number" && Number.isFinite(cost) && cost >= 0) onCost(cost)
23
+ } catch {
24
+ // Non-JSON events (including [DONE]) remain the SDK's responsibility.
25
+ }
26
+ }
27
+ }
28
+ return body.pipeThrough(new TransformStream({
29
+ transform(chunk, controller) {
30
+ consume(decoder.decode(chunk, { stream: true }))
31
+ controller.enqueue(chunk)
32
+ },
33
+ flush() {
34
+ consume(decoder.decode() + "\n\n")
35
+ },
36
+ }))
37
+ }
38
+
39
+ export function streamWithCost(model, context, options = {}) {
40
+ const output = createAssistantMessageEventStream()
41
+ const upstreamFetch = options.fetch ?? globalThis.fetch
42
+ let billedCost
43
+ const fetch = async (...args) => {
44
+ billedCost = undefined
45
+ const response = await upstreamFetch(...args)
46
+ if (!response.ok || !response.body || !response.headers.get("content-type")?.includes("text/event-stream")) {
47
+ return response
48
+ }
49
+ return new Response(observeCost(response.body, (cost) => { billedCost = cost }), {
50
+ status: response.status,
51
+ statusText: response.statusText,
52
+ headers: response.headers,
53
+ })
54
+ }
55
+ // The built-in adapter retains ownership of requests, retries, tools, and errors.
56
+ const upstream = completions.streamSimple(model, context, { ...options, fetch })
57
+ void (async () => {
58
+ for await (const event of upstream) {
59
+ if (event.type === "done" || event.type === "error") {
60
+ const message = event.type === "done" ? event.message : event.error
61
+ if (billedCost !== undefined) {
62
+ // The server supplies a total, not a per-token-category breakdown.
63
+ message.usage.cost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: billedCost }
64
+ }
65
+ }
66
+ output.push(event)
67
+ }
68
+ output.end()
69
+ })()
70
+ return output
71
+ }
package/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ import { streamWithCost } from "./cost-stream.js"
2
+
1
3
  const PROVIDER = "velocirouter"
2
4
  const API = "openai-completions"
3
5
  const DEFAULT_BASE_URL = "https://api.velocirouter.site"
@@ -87,5 +89,6 @@ export default function velocirouter(pi) {
87
89
  // app without these.
88
90
  headers: { "HTTP-Referer": "https://pi.dev", "X-Title": "Pi" },
89
91
  refreshModels,
92
+ streamSimple: streamWithCost,
90
93
  })
91
94
  }
package/package.json CHANGED
@@ -1,16 +1,23 @@
1
1
  {
2
2
  "name": "@velocirouter/pi",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Pi coding agent package for VelociRouter — registers the provider and its model catalog.",
5
5
  "type": "module",
6
6
  "main": "index.js",
7
7
  "license": "MIT",
8
8
  "files": [
9
9
  "index.js",
10
+ "cost-stream.js",
10
11
  "README.md"
11
12
  ],
13
+ "scripts": {
14
+ "test": "node --test"
15
+ },
16
+ "peerDependencies": {
17
+ "@earendil-works/pi-ai": ">=0.86.1"
18
+ },
12
19
  "engines": {
13
- "node": ">=18"
20
+ "node": ">=22.19.0"
14
21
  },
15
22
  "pi": {
16
23
  "extensions": [