@gtrabanco/pi-nan-provider 1.2.1 → 1.3.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/AGENTS.md +20 -2
- package/README.es.md +8 -0
- package/README.md +7 -0
- package/package.json +3 -3
- package/scripts/models.generated.ts +2 -2
- package/scripts/prepare-sdk-update-pr.mjs +145 -0
- package/src/cross-model-thinking-guard.ts +42 -3
- package/src/index.ts +21 -6
- package/src/pi-ai-version.ts +92 -0
package/AGENTS.md
CHANGED
|
@@ -344,8 +344,26 @@ Since 1.0.0 the package version **aligns its MAJOR.MINOR with the pi release lin
|
|
|
344
344
|
`src/reasoning-only-stream-guard.ts` (issue #21) keys on that marker;
|
|
345
345
|
earlier comment/message text said "60,000 characters" and was corrected to
|
|
346
346
|
the documented figures.
|
|
347
|
-
- **pi 1.
|
|
348
|
-
baseline):
|
|
347
|
+
- **pi 1.3.0 / pi-ai 1.3.0 compat** (verified 2026-10-15, the current verified
|
|
348
|
+
baseline): devDependencies bumped 1.2.0 → 1.3.0; the peer range stays
|
|
349
|
+
`">=0.99.0 <2"`. The pi-ai 1.3.0 `compat.d.ts` and `api/openai-completions.d.ts`
|
|
350
|
+
surfaces are byte-identical to 1.2.0; `index.d.ts` only drops the
|
|
351
|
+
`utils/assistant-message-frame` re-export (unused here), and the
|
|
352
|
+
`thinkingLevelMap.off` branch in `buildRequest` still fires. **pi-ai 1.3.0 lands
|
|
353
|
+
the upstream fix for issue #3**: `transformMessages` now DROPS cross-model
|
|
354
|
+
`thinking` blocks (`if (!isSameModel) return []`) instead of downgrading them to
|
|
355
|
+
unbounded text — the cross-model-thinking guard is therefore VERSION-GATED
|
|
356
|
+
(`CROSS_MODEL_GUARD_PI_AI_CEILING = "1.3.0"`; registers only on hosts older than
|
|
357
|
+
that, unknown version keeps it on) and is DEPRECATED for removal in the next
|
|
358
|
+
package release once the peer floor reaches `>=1.3.0`. `RETRYABLE_PROVIDER_ERROR_PATTERN`
|
|
359
|
+
expands with gateway/upstream wording ("service is temporarily unavailable" #10965,
|
|
360
|
+
upstream wrapper text #10842) and #10926 fixes OpenAI-compatible streams where
|
|
361
|
+
LiteLLM sends reasoning and content in the same chunk — both relevant to NaN's
|
|
362
|
+
LiteLLM gateway, both upstream-side. Package tests (319) and typecheck pass against
|
|
363
|
+
1.3.0; the issue-3 acceptance tests were re-pointed at the new upstream reality
|
|
364
|
+
(thinking scenario pins pi-ai's drop; the opaque-400 reclassification contract is
|
|
365
|
+
exercised via oversized user content pi-ai cannot shrink).
|
|
366
|
+
- **pi 1.2.0 / pi-ai 1.2.0 compat** (verified 2026-10-14): the peer range stays `">=0.99.0 <2"` (1.2.0 is inside it). The pi-ai
|
|
349
367
|
1.2.0 index/compat type surface is byte-identical to 1.0.0 (`index.d.ts` diff is
|
|
350
368
|
empty); the only additions are `KnownProvider` gaining `"azure"` (renamed from
|
|
351
369
|
`"azure-openai-responses"` in pi 1.0.3 — not used here), a new
|
package/README.es.md
CHANGED
|
@@ -52,6 +52,8 @@ Al cambiar de modelo, pi-ai reenvía el razonamiento del modelo anterior como te
|
|
|
52
52
|
|
|
53
53
|
Este paquete **elimina todos los bloques de razonamiento cross-model reenviados**, de modo que cambiar de un modelo de 1M de contexto a uno de 262K (`qwen3.6`) ya no desborda la ventana. Las respuestas y los tool results de los modelos no se tocan — solo se quitan sus trazas internas de razonamiento, así que `qwen3.6` puede seguir respondiendo sobre lo que hizo otro modelo. El razonamiento del mismo modelo no se toca nunca, y el guard solo actúa sobre peticiones dirigidas a los proveedores de este paquete. Pon `NAN_THINKING_GUARD=0` para desactivarlo.
|
|
54
54
|
|
|
55
|
+
> **pi 1.3.0+: este guard ya no se registra.** pi-ai 1.3.0 elimina el thinking cross-model por sí mismo (el fix upstream ya está publicado), así que el guard solo se activa en pi < 1.3.0 y está **deprecado — se eliminará en la próxima versión del paquete**. Si usas pi < 1.3.0, actualiza pi para mantener la mitigación; en pi ≥ 1.3.0 no hay que hacer nada.
|
|
56
|
+
|
|
55
57
|
Si una petición sigue desbordando — guard desactivado, inflado que no es un bloque de razonamiento (tool outputs grandes, imágenes) o una ventana de destino más pequeña — NaN responde con el mismo 400 genérico en lugar de nombrar el desbordamiento, y la auto-compactación de pi no lo reconoce, así que la sesión se queda atascada en el techo. Por eso el proveedor **vuelve a comprobar el tamaño de la petición a la salida**: cuando llega ese 400 genérico para una petición estimada por encima de la ventana del modelo, el error se reescribe como un mensaje de context overflow que pi reconoce, de modo que compacta y reintenta en vez de bloquearse. Un 400 genérico en una petición dentro de la ventana no se toca, así que nunca se etiquetan mal errores no relacionados.
|
|
56
58
|
|
|
57
59
|
### ⏱️ Streams truncados intermitentes (auto-retry, sin stall silencioso)
|
|
@@ -239,6 +241,12 @@ bun run check-nan-mcp-server --json # JSON para máquinas
|
|
|
239
241
|
bun run check-nan-mcp-server --issue # crea/refresca el issue
|
|
240
242
|
```
|
|
241
243
|
|
|
244
|
+
Las actualizaciones del SDK de pi (`@earendil-works/pi-ai` / `@earendil-works/pi-coding-agent`)
|
|
245
|
+
también están automatizadas: el workflow semanal `check-pi-sdk-versions` abre el issue
|
|
246
|
+
`sdk-monitor` **y** prepara un PR con el bump de devDependencies (`bun test` +
|
|
247
|
+
`bun run typecheck` + diff de superficie de tipos incluidos) para revisión humana — ver
|
|
248
|
+
`docs/sdk-version-monitoring.md`.
|
|
249
|
+
|
|
242
250
|
---
|
|
243
251
|
|
|
244
252
|
## 📊 Uso de Cuotas: `/nan-usage`
|
package/README.md
CHANGED
|
@@ -52,6 +52,8 @@ When you switch models, pi-ai replays the previous model's reasoning as plain as
|
|
|
52
52
|
|
|
53
53
|
This package **drops every replayed cross-model reasoning block**, so switching from a 1M-context model to a 262K one (`qwen3.6`) no longer overflows the window. The models' answers and tool results are untouched — only their internal reasoning traces are removed, so `qwen3.6` can still answer about what another model did. Same-model reasoning is never altered, and the guard only acts on requests targeting this package's providers. Set `NAN_THINKING_GUARD=0` to disable it.
|
|
54
54
|
|
|
55
|
+
> **pi 1.3.0+: this guard no longer registers.** pi-ai 1.3.0 drops cross-model thinking itself (the upstream fix landed), so the guard only activates on pi < 1.3.0 and is **deprecated — it will be removed in the next package release**. If you run pi < 1.3.0, update pi to keep the mitigation; no action is needed on pi ≥ 1.3.0.
|
|
56
|
+
|
|
55
57
|
If a request still overflows — the guard is disabled, the inflation is not a reasoning block (large tool outputs, images), or the destination window is simply smaller — NaN answers the same generic 400 instead of naming the overflow, and pi's auto-compaction does not recognize it, so the session wedges at the ceiling. The provider therefore **re-checks the request size on the way out**: when that generic 400 arrives for a request estimated over the model's window, the error is rewritten into a context-overflow message pi recognizes, so it compacts and retries instead of stalling. A generic 400 on a within-window request is left untouched, so unrelated errors are never mislabelled.
|
|
56
58
|
|
|
57
59
|
### ⏱️ Intermittent truncated streams (auto-retry, no silent stall)
|
|
@@ -236,6 +238,11 @@ bun run check-nan-mcp-server --json # machine-readable JSON
|
|
|
236
238
|
bun run check-nan-mcp-server --issue # create/refresh the issue
|
|
237
239
|
```
|
|
238
240
|
|
|
241
|
+
PI SDK updates (`@earendil-works/pi-ai` / `@earendil-works/pi-coding-agent`) are automated
|
|
242
|
+
too: the weekly `check-pi-sdk-versions` workflow files the `sdk-monitor` issue **and**
|
|
243
|
+
prepares a devDependencies-bump PR (`bun test` + `bun run typecheck` + type-surface diff
|
|
244
|
+
included) for human review — see `docs/sdk-version-monitoring.md`.
|
|
245
|
+
|
|
239
246
|
---
|
|
240
247
|
|
|
241
248
|
## 📊 Quota Usage: `/nan-usage`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gtrabanco/pi-nan-provider",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "NaN Builders (api.nan.builders) model provider for pi - OpenAI-compatible registration with a models.dev-generated fallback, tier-aware live catalog, and an optional community media MCP bridge",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|
|
@@ -46,8 +46,8 @@
|
|
|
46
46
|
"@earendil-works/pi-coding-agent": ">=0.99.0 <2"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@earendil-works/pi-ai": "1.
|
|
50
|
-
"@earendil-works/pi-coding-agent": "1.
|
|
49
|
+
"@earendil-works/pi-ai": "1.3.0",
|
|
50
|
+
"@earendil-works/pi-coding-agent": "1.3.0",
|
|
51
51
|
"@types/bun": "1.4",
|
|
52
52
|
"typescript": "7"
|
|
53
53
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// This file is auto-generated by scripts/generate-models.ts
|
|
2
2
|
// Do not edit manually — run `bun run generate-models` to update.
|
|
3
3
|
//
|
|
4
|
-
// Source: https://models.dev/api.json (provider "nan"), fetched 2026-10-
|
|
4
|
+
// Source: https://models.dev/api.json (provider "nan"), fetched 2026-10-19T22:52:49.128Z
|
|
5
5
|
// Provenance: every contextWindow/maxTokens/input/cost value traces to
|
|
6
6
|
// models.dev or to the per-entry notes below. Nothing is invented; entries
|
|
7
7
|
// models.dev documents incompletely are omitted and flagged instead.
|
|
@@ -459,7 +459,7 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
|
|
|
459
459
|
export const GENERATED_CATALOG_META = {
|
|
460
460
|
source: "https://models.dev/api.json",
|
|
461
461
|
modelsDevProvider: "nan",
|
|
462
|
-
fetchedAt: "2026-10-
|
|
462
|
+
fetchedAt: "2026-10-19T22:52:49.128Z",
|
|
463
463
|
modelCount: 6,
|
|
464
464
|
models: ["deepseek-v4-flash","gemma4","glm5.3-flash","mimo-v2.6-flash","qwen3.6","qwen3.8-flash"],
|
|
465
465
|
notes: [
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Prepare an SDK update PR for the packages monitored by
|
|
4
|
+
* scripts/check-pi-sdk-versions.mjs: bump their devDependencies in
|
|
5
|
+
* package.json and emit the branch/PR metadata the
|
|
6
|
+
* `.github/workflows/check-pi-sdk-versions.yml` workflow needs to open the PR.
|
|
7
|
+
*
|
|
8
|
+
* This script ONLY edits package.json. Installing, testing, typechecking, the
|
|
9
|
+
* type-surface diff, git and `gh pr create` stay in the workflow so a failure
|
|
10
|
+
* anywhere aborts the PR.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* node scripts/prepare-sdk-update-pr.mjs --body-file /tmp/pr-body.md
|
|
14
|
+
*
|
|
15
|
+
* Output (stdout, single JSON line):
|
|
16
|
+
* {"updates":false}
|
|
17
|
+
* {"updates":true,"branch":"chore/pi-sdk-1.3.0","title":"...","bodyFile":"...",
|
|
18
|
+
* "bumps":[{"name":"@earendil-works/pi-ai","from":"1.2.0","to":"1.3.0"}]}
|
|
19
|
+
*
|
|
20
|
+
* Exit codes: 0 = nothing to do or prepared; 2 = unexpected failure.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import https from "node:https";
|
|
24
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
26
|
+
|
|
27
|
+
const SDK_PACKAGES = ["@earendil-works/pi-ai", "@earendil-works/pi-coding-agent"];
|
|
28
|
+
|
|
29
|
+
function npmLatest(packageName) {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const req = https.get(
|
|
32
|
+
`https://registry.npmjs.org/${encodeURIComponent(packageName)}`,
|
|
33
|
+
{ headers: { accept: "application/json" } },
|
|
34
|
+
(res) => {
|
|
35
|
+
if (res.statusCode !== 200) {
|
|
36
|
+
reject(new Error(`npm registry returned ${res.statusCode} for ${packageName}`));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
let data = "";
|
|
40
|
+
res.on("data", (chunk) => (data += chunk));
|
|
41
|
+
res.on("end", () => {
|
|
42
|
+
const latest = JSON.parse(data)["dist-tags"]?.["latest"];
|
|
43
|
+
if (!latest) reject(new Error(`No "latest" dist-tag for ${packageName}`));
|
|
44
|
+
else resolve(latest);
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
req.on("error", reject);
|
|
49
|
+
req.setTimeout(15_000, () => {
|
|
50
|
+
req.destroy();
|
|
51
|
+
reject(new Error(`Timeout fetching ${packageName} from npm`));
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function main() {
|
|
57
|
+
const bodyFileArg = process.argv.indexOf("--body-file");
|
|
58
|
+
const bodyFile =
|
|
59
|
+
bodyFileArg !== -1 ? process.argv[bodyFileArg + 1] : "/tmp/sdk-update-pr-body.md";
|
|
60
|
+
|
|
61
|
+
const packageUrl = new URL("../package.json", import.meta.url);
|
|
62
|
+
const raw = readFileSync(packageUrl, "utf-8");
|
|
63
|
+
const pkg = JSON.parse(raw);
|
|
64
|
+
|
|
65
|
+
const bumps = [];
|
|
66
|
+
for (const name of SDK_PACKAGES) {
|
|
67
|
+
const from = pkg.devDependencies?.[name];
|
|
68
|
+
if (!from) throw new Error(`${name} is not a devDependency of this package`);
|
|
69
|
+
const to = await npmLatest(name);
|
|
70
|
+
if (to !== from) bumps.push({ name, from, to });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (bumps.length === 0) {
|
|
74
|
+
console.log(JSON.stringify({ updates: false }));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Bump via exact-string replacement so the rest of package.json (ordering,
|
|
79
|
+
// indentation, other fields) is untouched.
|
|
80
|
+
let updated = raw;
|
|
81
|
+
for (const bump of bumps) {
|
|
82
|
+
const needle = `"${bump.name}": "${bump.from}"`;
|
|
83
|
+
if (!updated.includes(needle)) {
|
|
84
|
+
throw new Error(`Could not locate ${needle} in package.json to bump it`);
|
|
85
|
+
}
|
|
86
|
+
updated = updated.replace(needle, `"${bump.name}": "${bump.to}"`);
|
|
87
|
+
}
|
|
88
|
+
writeFileSync(packageUrl, updated);
|
|
89
|
+
|
|
90
|
+
const versionTag = bumps.map((b) => b.to).join("-");
|
|
91
|
+
const branch = `chore/pi-sdk-${versionTag}`;
|
|
92
|
+
const title = `chore: bump PI SDK to ${bumps.map((b) => `${b.name.split("/")[1]} ${b.to}`).join(" / ")}`;
|
|
93
|
+
const peer = pkg.peerDependencies ?? {};
|
|
94
|
+
|
|
95
|
+
const body = `## PI SDK update (automated)
|
|
96
|
+
|
|
97
|
+
Prepared from the weekly \`check-pi-sdk-versions\` run. Resolves the \`sdk-monitor\` issue
|
|
98
|
+
for this cycle. **A human must review the upstream changelog before merging** — a minor
|
|
99
|
+
pi release can change provider-relevant behavior (see \`AGENTS.md\` "Verified API facts").
|
|
100
|
+
|
|
101
|
+
### Version bumps
|
|
102
|
+
|
|
103
|
+
| Package | From | To | Peer range |
|
|
104
|
+
| --- | --- | --- | --- |
|
|
105
|
+
${bumps.map((b) => `| \`${b.name}\` | ${b.from} | **${b.to}** | \`${peer[b.name] ?? "n/a"}\` |`).join("\n")}
|
|
106
|
+
|
|
107
|
+
### Review checklist
|
|
108
|
+
|
|
109
|
+
- [ ] Upstream release notes reviewed (https://pi.dev/changelog)
|
|
110
|
+
- [ ] \`bun test\` + \`bun run typecheck\` green (executed by CI before this PR was opened)
|
|
111
|
+
- [ ] Type-surface diff (below) contains no removals this package uses
|
|
112
|
+
- [ ] Version policy applied: package MAJOR.MINOR realigns with the pi release line
|
|
113
|
+
(\`package.json\` version bump included in this PR when the pi MINOR moved)
|
|
114
|
+
|
|
115
|
+
The results of the two sections below were produced by CI while preparing this PR.
|
|
116
|
+
|
|
117
|
+
## Verification
|
|
118
|
+
|
|
119
|
+
<!-- VERIFICATION -->
|
|
120
|
+
|
|
121
|
+
## Type-surface diff (previous → new)
|
|
122
|
+
|
|
123
|
+
<details><summary>Click to expand</summary>
|
|
124
|
+
|
|
125
|
+
<!-- SURFACE_DIFF -->
|
|
126
|
+
|
|
127
|
+
</details>
|
|
128
|
+
`;
|
|
129
|
+
|
|
130
|
+
writeFileSync(bodyFile, body);
|
|
131
|
+
console.log(
|
|
132
|
+
JSON.stringify({
|
|
133
|
+
updates: true,
|
|
134
|
+
branch,
|
|
135
|
+
title,
|
|
136
|
+
bodyFile,
|
|
137
|
+
bumps,
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
main().catch((err) => {
|
|
143
|
+
console.error("Fatal:", err.message);
|
|
144
|
+
process.exit(2);
|
|
145
|
+
});
|
|
@@ -27,14 +27,53 @@
|
|
|
27
27
|
* provider stream). It therefore sees the original `thinking` blocks and can
|
|
28
28
|
* remove them before they become text.
|
|
29
29
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
30
|
+
* UPSTREAM UPDATE (pi-ai 1.3.0, verified 2026-10-15): `transformMessages` now
|
|
31
|
+
* drops cross-model `thinking` blocks itself (`if (!isSameModel) return []`),
|
|
32
|
+
* which is the upstream fix this guard was mitigating. The guard is therefore
|
|
33
|
+
* GATED BY VERSION: it only registers when the host's pi-ai is older than
|
|
34
|
+
* {@link CROSS_MODEL_GUARD_PI_AI_CEILING} (1.3.0). An unknown host version
|
|
35
|
+
* keeps the guard on — the conservative default, since the guard is harmless
|
|
36
|
+
* where pi-ai already drops the blocks and essential where it still replays
|
|
37
|
+
* them.
|
|
38
|
+
*
|
|
39
|
+
* DEPRECATED — REMOVE THIS MODULE when the peer range floor rises to
|
|
40
|
+
* `>=1.3.0`: at that point every supported pi ships the upstream drop and this
|
|
41
|
+
* file (plus the `NAN_THINKING_GUARD` opt-out) is dead code. Users on
|
|
42
|
+
* pi < 1.3.0 should update pi to keep getting the mitigation for free.
|
|
33
43
|
*/
|
|
34
44
|
|
|
45
|
+
import { parsePiAiVersion } from "./pi-ai-version.ts";
|
|
46
|
+
|
|
35
47
|
/** Env var that opts out of the guard (`0`, `false`, `no` or `off`). Default: enabled. */
|
|
36
48
|
export const NAN_THINKING_GUARD_ENV = "NAN_THINKING_GUARD";
|
|
37
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Exclusive pi-ai version ceiling for this guard: it registers only while the
|
|
52
|
+
* host's pi-ai is OLDER than this version. pi-ai 1.3.0 drops cross-model
|
|
53
|
+
* thinking in `transformMessages` (upstream fix for issue #3), so from 1.3.0
|
|
54
|
+
* on the guard is redundant. Remove this constant together with the module
|
|
55
|
+
* once the peer range floor reaches this version.
|
|
56
|
+
*/
|
|
57
|
+
export const CROSS_MODEL_GUARD_PI_AI_CEILING = "1.3.0";
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Whether the guard still applies to a host running the given pi-ai version.
|
|
61
|
+
* `undefined`/`null` (unknown host version) and unparseable versions return
|
|
62
|
+
* `true` — the conservative older-pi default. `false` means the host's pi-ai
|
|
63
|
+
* already drops cross-model thinking itself and the guard must not register.
|
|
64
|
+
*/
|
|
65
|
+
export function crossModelThinkingGuardCovers(piAiVersion: string | null | undefined): boolean {
|
|
66
|
+
if (piAiVersion === null || piAiVersion === undefined) return true;
|
|
67
|
+
const parsed = parsePiAiVersion(piAiVersion);
|
|
68
|
+
if (!parsed) return true;
|
|
69
|
+
const ceiling = parsePiAiVersion(CROSS_MODEL_GUARD_PI_AI_CEILING);
|
|
70
|
+
if (!ceiling) return true;
|
|
71
|
+
return (
|
|
72
|
+
parsed.major < ceiling.major ||
|
|
73
|
+
(parsed.major === ceiling.major && parsed.minor < ceiling.minor)
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
38
77
|
export interface CrossModelThinkingGuardOptions {
|
|
39
78
|
/** Provider ids this guard applies to (the caller's registered providers). */
|
|
40
79
|
providerIds: ReadonlySet<string>;
|
package/src/index.ts
CHANGED
|
@@ -30,9 +30,11 @@ import type { Provider } from "@earendil-works/pi-ai";
|
|
|
30
30
|
import { registerNanMcpCommand } from "./commands.ts";
|
|
31
31
|
import { registerNanUsageCommand } from "./usage.ts";
|
|
32
32
|
import {
|
|
33
|
+
crossModelThinkingGuardCovers,
|
|
33
34
|
crossModelThinkingGuardEnabled,
|
|
34
35
|
stripCrossModelThinking,
|
|
35
36
|
} from "./cross-model-thinking-guard.ts";
|
|
37
|
+
import { resolvePiAiVersion } from "./pi-ai-version.ts";
|
|
36
38
|
import { baselineModels } from "./fetch-models.ts";
|
|
37
39
|
import { createNanCompatibleProvider, type OpenAICompatibleProviderConfig } from "./provider-factory.ts";
|
|
38
40
|
import { PROVIDERS } from "./providers.ts";
|
|
@@ -146,21 +148,34 @@ async function registerMcpServersNative(pi: ExtensionAPI): Promise<void> {
|
|
|
146
148
|
/**
|
|
147
149
|
* Drop the reasoning pi-ai replays across a model switch.
|
|
148
150
|
*
|
|
149
|
-
* pi-ai
|
|
150
|
-
* to plain text with no size bound (verified on 0.87.1), and
|
|
151
|
-
* bounds the sum across messages — measured at 30–60% of the whole
|
|
152
|
-
* real sessions. Switching from a 1M-context model to a 262K one
|
|
153
|
-
* then overflows the window, and NaN's gateway answers a generic
|
|
151
|
+
* On pi-ai < 1.3.0, `transformMessages` downgrades a previous model's
|
|
152
|
+
* `thinking` blocks to plain text with no size bound (verified on 0.87.1), and
|
|
153
|
+
* nothing bounds the sum across messages — measured at 30–60% of the whole
|
|
154
|
+
* context on real sessions. Switching from a 1M-context model to a 262K one
|
|
155
|
+
* (`qwen3.6`) then overflows the window, and NaN's gateway answers a generic
|
|
154
156
|
* `400 Invalid request. Check your request parameters.` This hook runs before
|
|
155
157
|
* pi-ai converts the blocks, so removing the cross-model reasoning here keeps
|
|
156
158
|
* the replayed context small. Answers and tool results are untouched. See
|
|
157
159
|
* src/cross-model-thinking-guard.ts.
|
|
158
160
|
*
|
|
161
|
+
* VERSION-GATED: pi-ai 1.3.0 drops cross-model thinking itself (upstream fix
|
|
162
|
+
* for issue #3), so the guard only registers when the host's resolved pi-ai is
|
|
163
|
+
* older than 1.3.0; an undeterminable host version keeps it on (conservative
|
|
164
|
+
* older-pi default). DEPRECATED — remove this whole guard once the peer range
|
|
165
|
+
* floor rises to `>=1.3.0`; users on pi < 1.3.0 should update pi.
|
|
166
|
+
*
|
|
159
167
|
* Scope: only requests targeting this package's providers are touched, and
|
|
160
168
|
* only messages from a DIFFERENT model — same-model reasoning is never removed.
|
|
161
169
|
*/
|
|
162
|
-
export function registerCrossModelThinkingGuard(
|
|
170
|
+
export function registerCrossModelThinkingGuard(
|
|
171
|
+
pi: ExtensionAPI,
|
|
172
|
+
options: { piAiVersion?: string | null } = {},
|
|
173
|
+
): void {
|
|
163
174
|
if (typeof pi.on !== "function") return; // old pi without the context hook
|
|
175
|
+
// `null` forces the unknown-version path (guard on); omitted resolves the
|
|
176
|
+
// host's pi-ai version at runtime; a string pins the version (tests).
|
|
177
|
+
const piAiVersion = options.piAiVersion !== undefined ? options.piAiVersion : resolvePiAiVersion();
|
|
178
|
+
if (!crossModelThinkingGuardCovers(piAiVersion)) return;
|
|
164
179
|
const providerIds = new Set(PROVIDERS.map((provider) => provider.id));
|
|
165
180
|
pi.on("context", (event, ctx) => {
|
|
166
181
|
if (!crossModelThinkingGuardEnabled()) return;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime pi-ai version detection.
|
|
3
|
+
*
|
|
4
|
+
* Resolves the `@earendil-works/pi-ai` package instance the HOST loaded — the
|
|
5
|
+
* same host-anchored strategy `src/pi-ai-loader.ts` uses, because an
|
|
6
|
+
* extension-relative resolve can return a stale copy from the extension's own
|
|
7
|
+
* npm tree (issue #8) — and reads that copy's package.json version. The result
|
|
8
|
+
* feeds version-gated behavior, currently the cross-model thinking guard
|
|
9
|
+
* (registered only while the host's pi-ai is older than 1.3.0, where pi-ai
|
|
10
|
+
* still replays cross-model thinking as unbounded text).
|
|
11
|
+
*
|
|
12
|
+
* Failure is NOT an error: an unresolvable package or an unreadable version
|
|
13
|
+
* returns `undefined`, and callers fall back to the conservative older-pi
|
|
14
|
+
* behavior (the guard stays on — harmless on modern hosts, essential on old
|
|
15
|
+
* ones). Guarded by `test/pi-ai-version.test.ts`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readFileSync } from "node:fs";
|
|
19
|
+
import { dirname, join } from "node:path";
|
|
20
|
+
import { fileURLToPath } from "node:url";
|
|
21
|
+
import { hostAnchorUrl, PI_AI_PACKAGE_SPECIFIER, resolvePiAiSpecifier } from "./pi-ai-loader.ts";
|
|
22
|
+
|
|
23
|
+
/** A parsed semver triple (`1.3.0-beta.1` → `{ major: 1, minor: 3, patch: 0 }`). */
|
|
24
|
+
export interface PiAiVersion {
|
|
25
|
+
major: number;
|
|
26
|
+
minor: number;
|
|
27
|
+
patch: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Parse a semver string; prerelease/build suffixes are ignored. */
|
|
31
|
+
export function parsePiAiVersion(version: string): PiAiVersion | undefined {
|
|
32
|
+
const match = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(version.trim());
|
|
33
|
+
if (!match) return undefined;
|
|
34
|
+
return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]) };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Read the pi-ai version from a resolved specifier (path or `file://` URL) of
|
|
39
|
+
* any entry file inside the package. Walks up from the entry's directory until
|
|
40
|
+
* a package.json whose `name` is exactly `@earendil-works/pi-ai` is found —
|
|
41
|
+
* matching the name prevents picking up an unrelated package.json in a
|
|
42
|
+
* workspace layout — and returns its `version`. Bounded walk (8 levels);
|
|
43
|
+
* `undefined` when nothing matches.
|
|
44
|
+
*/
|
|
45
|
+
export function piAiVersionFromResolvedEntry(entry: string): string | undefined {
|
|
46
|
+
let dir: string;
|
|
47
|
+
try {
|
|
48
|
+
dir = dirname(entry.startsWith("file://") ? fileURLToPath(entry) : entry);
|
|
49
|
+
} catch {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
for (let depth = 0; depth < 8; depth++) {
|
|
53
|
+
try {
|
|
54
|
+
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf-8")) as {
|
|
55
|
+
name?: unknown;
|
|
56
|
+
version?: unknown;
|
|
57
|
+
};
|
|
58
|
+
if (pkg.name === PI_AI_PACKAGE_SPECIFIER && typeof pkg.version === "string") {
|
|
59
|
+
return pkg.version;
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
// No readable package.json at this level — keep walking up.
|
|
63
|
+
}
|
|
64
|
+
const parent = dirname(dir);
|
|
65
|
+
if (parent === dir) return undefined;
|
|
66
|
+
dir = parent;
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Cache for {@link resolvePiAiVersion}; resolution runs once per process. */
|
|
72
|
+
let cachedVersion: string | undefined;
|
|
73
|
+
let resolved = false;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Version of the pi-ai instance the host loaded, or `undefined` when it cannot
|
|
77
|
+
* be determined (unresolvable specifier, unreadable package.json, odd runtime).
|
|
78
|
+
* Resolution happens once and is cached for the process lifetime.
|
|
79
|
+
*/
|
|
80
|
+
export function resolvePiAiVersion(): string | undefined {
|
|
81
|
+
if (resolved) return cachedVersion;
|
|
82
|
+
resolved = true;
|
|
83
|
+
try {
|
|
84
|
+
const entry = resolvePiAiSpecifier(PI_AI_PACKAGE_SPECIFIER, {
|
|
85
|
+
anchorUrl: hostAnchorUrl(process.argv[1]),
|
|
86
|
+
});
|
|
87
|
+
cachedVersion = piAiVersionFromResolvedEntry(entry);
|
|
88
|
+
} catch {
|
|
89
|
+
cachedVersion = undefined;
|
|
90
|
+
}
|
|
91
|
+
return cachedVersion;
|
|
92
|
+
}
|