@gtrabanco/pi-nan-provider 0.6.3 → 0.6.4
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 +21 -0
- package/README.es.md +2 -2
- package/README.md +2 -2
- package/package.json +1 -1
- package/scripts/models.generated.ts +2 -2
- package/src/cross-model-thinking-guard.ts +25 -45
- package/src/index.ts +12 -15
package/AGENTS.md
CHANGED
|
@@ -37,6 +37,27 @@ Regenerate the catalog after touching `scripts/generate-models.ts`:
|
|
|
37
37
|
bun run generate-models
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
+
## Live NaN API during diagnosis
|
|
41
|
+
|
|
42
|
+
Diagnostic calls against the real gateway are allowed — they spend the
|
|
43
|
+
maintainer's quota, so they are **permission-gated**:
|
|
44
|
+
|
|
45
|
+
- **Ask the maintainer before running any live probe, with an approximate token
|
|
46
|
+
cost (input + output).** No silent probing. If the cost is not worth it, report
|
|
47
|
+
the behavior to NaN and let them reproduce it instead of debugging it here.
|
|
48
|
+
- **Tests must never hit the network.** `bunfig.toml` preloads
|
|
49
|
+
`test/network-guard.ts`, which makes any un-injected `fetch` throw. Keep it:
|
|
50
|
+
inject `fetchImpl` / `options.fetch`, or use the local fixture. The permission
|
|
51
|
+
gate covers ad-hoc diagnosis only, never `bun test`.
|
|
52
|
+
- **Default to `qwen3.6` — it is unlimited.**
|
|
53
|
+
- **For massive/bulk probes prefer a model the maintainer uses less with a large
|
|
54
|
+
token budget, e.g. `mimo-v2.5`** (1M context).
|
|
55
|
+
- **When the model under investigation is the point** (e.g. reproducing a
|
|
56
|
+
model-specific 400), use it, but minimize tokens: smallest viable prompt,
|
|
57
|
+
lowest `max_tokens`, stop at the first decisive response.
|
|
58
|
+
- Repro commands that run a real `pi` session (`pi --fork ... -p ...`) use the
|
|
59
|
+
same key; keep them minimal and delete the forked session files afterwards.
|
|
60
|
+
|
|
40
61
|
## One shared implementation for all providers
|
|
41
62
|
|
|
42
63
|
`nan` (and any future provider, e.g. `helmcode`) must stay behind the single shared
|
package/README.es.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @gtrabanco/pi-nan-provider
|
|
2
2
|
|
|
3
3
|
[](https://opensource.org/licenses/MIT)
|
|
4
|
-
[](https://github.com/gtrabanco/pi-nan-provider/releases)
|
|
5
5
|
|
|
6
6
|
[NaN Builders](https://nan.builders) model provider + MCP bridges para [pi](https://github.com/earendil-works/pi).
|
|
7
7
|
|
|
@@ -47,7 +47,7 @@ El registro es síncrono a propósito: el catálogo de fallback está disponible
|
|
|
47
47
|
|
|
48
48
|
Al cambiar de modelo, pi-ai reenvía el razonamiento del modelo anterior como texto plano de asistente — **sin límite de tamaño**. Un único razonamiento largo o degenerado puede desbordar la ventana de un modelo de 262K, y NaN responde con un `400 Invalid request. Check your request parameters.` genérico que parece un bug del proveedor (seguimiento upstream: [pi-nan-provider#3](https://github.com/gtrabanco/pi-nan-provider/issues/3); issue abierta upstream: [pi#6167](https://github.com/earendil-works/pi/issues/6167)).
|
|
49
49
|
|
|
50
|
-
Este paquete
|
|
50
|
+
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.
|
|
51
51
|
|
|
52
52
|
## 🔑 Autenticación
|
|
53
53
|
|
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @gtrabanco/pi-nan-provider
|
|
2
2
|
|
|
3
3
|
[](https://opensource.org/licenses/MIT)
|
|
4
|
-
[](https://github.com/gtrabanco/pi-nan-provider/releases)
|
|
5
5
|
|
|
6
6
|
[NaN Builders](https://nan.builders) model provider + MCP bridges for [pi](https://github.com/earendil-works/pi).
|
|
7
7
|
|
|
@@ -47,7 +47,7 @@ The registration is synchronous on purpose: the generated fallback catalog is av
|
|
|
47
47
|
|
|
48
48
|
When you switch models, pi-ai replays the previous model's reasoning as plain assistant text — with **no size bound**. A single long or degenerate reasoning trace can therefore overflow a 262K-context model's window, and NaN answers with a generic `400 Invalid request. Check your request parameters.` that looks like a provider bug (upstream tracking: [pi-nan-provider#3](https://github.com/gtrabanco/pi-nan-provider/issues/3); open upstream issue: [pi#6167](https://github.com/earendil-works/pi/issues/6167)).
|
|
49
49
|
|
|
50
|
-
This package
|
|
50
|
+
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.
|
|
51
51
|
|
|
52
52
|
## 🔑 Authentication
|
|
53
53
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gtrabanco/pi-nan-provider",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
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 MCP bridges (official web search + optional community media server)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi",
|
|
@@ -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-09-
|
|
4
|
+
// Source: https://models.dev/api.json (provider "nan"), fetched 2026-09-11T11:47:14.840Z
|
|
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.
|
|
@@ -381,7 +381,7 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
|
|
|
381
381
|
export const GENERATED_CATALOG_META = {
|
|
382
382
|
source: "https://models.dev/api.json",
|
|
383
383
|
modelsDevProvider: "nan",
|
|
384
|
-
fetchedAt: "2026-09-
|
|
384
|
+
fetchedAt: "2026-09-11T11:47:14.840Z",
|
|
385
385
|
modelCount: 6,
|
|
386
386
|
models: ["deepseek-v4-flash","gemma4","glm5.3-flash","mimo-v2.5","qwen3.6","qwen3.8-flash"],
|
|
387
387
|
notes: [
|
|
@@ -6,45 +6,36 @@
|
|
|
6
6
|
* every non-redacted `thinking` block to a plain `text` block verbatim
|
|
7
7
|
* (`packages/ai/src/api/transform-messages.ts`), and `openai-completions`
|
|
8
8
|
* serializes that into the assistant `content` string. Nothing bounds
|
|
9
|
-
* `block.thinking`,
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
9
|
+
* `block.thinking`, and nothing bounds the SUM across messages.
|
|
10
|
+
*
|
|
11
|
+
* Measured on real sessions: replayed reasoning is 30–60% of the whole context
|
|
12
|
+
* (e.g. 356,723 of 903,464 chars in one session; 349,882 of 749,525 in another).
|
|
13
|
+
* Switching from a 1M-context model (glm5.3-flash, deepseek-v4-flash) to a
|
|
14
|
+
* 262K-context one (`qwen3.6`) then overflows the window, and NaN's gateway
|
|
15
|
+
* answers a generic `400 Invalid request. Check your request parameters.`
|
|
16
|
+
*
|
|
17
|
+
* A per-block cap was tried first and is NOT enough: many medium blocks sum to
|
|
18
|
+
* hundreds of thousands of tokens. This guard therefore DROPS every replayed
|
|
19
|
+
* cross-model `thinking` block outright — it is the reasoning trace that
|
|
20
|
+
* pi-ai would have replayed as plain text, not the model's answers or tool
|
|
21
|
+
* results, so qwen can still answer questions about what glm/deepseek did.
|
|
22
|
+
* Same-model reasoning is never touched (signatures and continuity depend on
|
|
23
|
+
* it), and the guard only acts on requests targeting this package's providers.
|
|
17
24
|
*
|
|
18
25
|
* The extension runs in pi's `context` hook, which fires BEFORE pi-ai's
|
|
19
26
|
* `transformMessages` (pi-agent-core `transformContext` → `convertToLlm` →
|
|
20
27
|
* provider stream). It therefore sees the original `thinking` blocks and can
|
|
21
|
-
*
|
|
22
|
-
* differ from the target are touched; same-model replay keeps its reasoning
|
|
23
|
-
* byte-for-byte because signatures/continuity depend on it.
|
|
28
|
+
* remove them before they become text.
|
|
24
29
|
*
|
|
25
|
-
* This is a bounded mitigation, not
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* https://github.com/earendil-works/pi/issues/9433).
|
|
30
|
+
* This is a bounded mitigation, not the fix: the unbounded conversion (and pi
|
|
31
|
+
* not re-checking the context size on a model switch) belongs upstream. See
|
|
32
|
+
* https://github.com/gtrabanco/pi-nan-provider/issues/3.
|
|
29
33
|
*/
|
|
30
34
|
|
|
31
35
|
/** Env var that opts out of the guard (`0`, `false`, `no` or `off`). Default: enabled. */
|
|
32
36
|
export const NAN_THINKING_GUARD_ENV = "NAN_THINKING_GUARD";
|
|
33
37
|
|
|
34
|
-
/**
|
|
35
|
-
* Maximum characters of a single cross-model reasoning block replayed as text.
|
|
36
|
-
* 16,000 chars is ~4K tokens — far above any real reasoning trace, far below
|
|
37
|
-
* the 445,888-char degenerate trace that caused the 400.
|
|
38
|
-
*/
|
|
39
|
-
export const MAX_CROSS_MODEL_THINKING_CHARS = 16_000;
|
|
40
|
-
|
|
41
|
-
/** Appended after the kept prefix so the substitution is visible, never silent. */
|
|
42
|
-
export const CROSS_MODEL_THINKING_TRUNCATION_MARKER =
|
|
43
|
-
"\n\n[…previous-model reasoning truncated by pi-nan-provider to keep the request within NaN's context]";
|
|
44
|
-
|
|
45
38
|
export interface CrossModelThinkingGuardOptions {
|
|
46
|
-
/** Max chars kept per replayed cross-model reasoning block. */
|
|
47
|
-
maxCharsPerBlock?: number;
|
|
48
39
|
/** Provider ids this guard applies to (the caller's registered providers). */
|
|
49
40
|
providerIds: ReadonlySet<string>;
|
|
50
41
|
}
|
|
@@ -57,7 +48,6 @@ interface GuardTarget {
|
|
|
57
48
|
|
|
58
49
|
interface ThinkingBlock {
|
|
59
50
|
type?: string;
|
|
60
|
-
thinking?: unknown;
|
|
61
51
|
[k: string]: unknown;
|
|
62
52
|
}
|
|
63
53
|
|
|
@@ -84,19 +74,17 @@ function isThinkingBlock(value: unknown): value is ThinkingBlock {
|
|
|
84
74
|
}
|
|
85
75
|
|
|
86
76
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
* cloning the whole context on the common path. Never mutates the input.
|
|
77
|
+
* Remove every `thinking` block from assistant messages produced by a model
|
|
78
|
+
* other than the target. Returns the SAME array reference when nothing changed,
|
|
79
|
+
* so callers can skip cloning on the common path. Never mutates the input.
|
|
91
80
|
*/
|
|
92
|
-
export function
|
|
81
|
+
export function stripCrossModelThinking<T>(
|
|
93
82
|
messages: readonly T[],
|
|
94
83
|
target: GuardTarget | undefined,
|
|
95
84
|
options: CrossModelThinkingGuardOptions,
|
|
96
85
|
): readonly T[] {
|
|
97
86
|
if (!target?.provider || !options.providerIds.has(target.provider)) return messages;
|
|
98
87
|
|
|
99
|
-
const maxChars = options.maxCharsPerBlock ?? MAX_CROSS_MODEL_THINKING_CHARS;
|
|
100
88
|
let changed = false;
|
|
101
89
|
|
|
102
90
|
const next = messages.map((raw) => {
|
|
@@ -106,16 +94,8 @@ export function boundCrossModelThinking<T>(
|
|
|
106
94
|
const isSameModel = raw.provider === target.provider && raw.api === target.api && raw.model === target.id;
|
|
107
95
|
if (isSameModel) return raw;
|
|
108
96
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
if (!isThinkingBlock(block)) return block;
|
|
112
|
-
const text = block.thinking;
|
|
113
|
-
if (typeof text !== "string" || text.length <= maxChars) return block;
|
|
114
|
-
messageChanged = true;
|
|
115
|
-
return { ...block, thinking: text.slice(0, maxChars) + CROSS_MODEL_THINKING_TRUNCATION_MARKER };
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
if (!messageChanged) return raw;
|
|
97
|
+
const content = (raw.content as unknown[]).filter((block) => !isThinkingBlock(block));
|
|
98
|
+
if (content.length === (raw.content as unknown[]).length) return raw;
|
|
119
99
|
changed = true;
|
|
120
100
|
return { ...raw, content };
|
|
121
101
|
});
|
package/src/index.ts
CHANGED
|
@@ -29,9 +29,8 @@ import type { ContextEvent, ExtensionAPI, ProviderConfig } from "@earendil-works
|
|
|
29
29
|
import type { Provider } from "@earendil-works/pi-ai";
|
|
30
30
|
import { registerNanMcpCommand } from "./commands.ts";
|
|
31
31
|
import {
|
|
32
|
-
boundCrossModelThinking,
|
|
33
32
|
crossModelThinkingGuardEnabled,
|
|
34
|
-
|
|
33
|
+
stripCrossModelThinking,
|
|
35
34
|
} from "./cross-model-thinking-guard.ts";
|
|
36
35
|
import { baselineModels } from "./fetch-models.ts";
|
|
37
36
|
import { createNanWebSearchTool, webSearchBridgeEnabled, NAN_API_KEY_ENV } from "./mcp/nan-search.ts";
|
|
@@ -114,29 +113,27 @@ function registerMcpToolsCompat(pi: ExtensionAPI): void {
|
|
|
114
113
|
}
|
|
115
114
|
|
|
116
115
|
/**
|
|
117
|
-
*
|
|
116
|
+
* Drop the reasoning pi-ai replays across a model switch.
|
|
118
117
|
*
|
|
119
118
|
* pi-ai's `transformMessages` downgrades a previous model's `thinking` blocks
|
|
120
|
-
* to plain text with no size bound (still true on 0.85.1 / main),
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* the replayed context
|
|
119
|
+
* to plain text with no size bound (still true on 0.85.1 / main), and nothing
|
|
120
|
+
* bounds the sum across messages — measured at 30–60% of the whole context on
|
|
121
|
+
* real sessions. Switching from a 1M-context model to a 262K one (`qwen3.6`)
|
|
122
|
+
* then overflows the window, and NaN's gateway answers a generic
|
|
123
|
+
* `400 Invalid request. Check your request parameters.` This hook runs before
|
|
124
|
+
* pi-ai converts the blocks, so removing the cross-model reasoning here keeps
|
|
125
|
+
* the replayed context small. Answers and tool results are untouched. See
|
|
126
|
+
* src/cross-model-thinking-guard.ts.
|
|
127
127
|
*
|
|
128
128
|
* Scope: only requests targeting this package's providers are touched, and
|
|
129
|
-
* only messages from a DIFFERENT model — same-model reasoning is never
|
|
129
|
+
* only messages from a DIFFERENT model — same-model reasoning is never removed.
|
|
130
130
|
*/
|
|
131
131
|
export function registerCrossModelThinkingGuard(pi: ExtensionAPI): void {
|
|
132
132
|
if (typeof pi.on !== "function") return; // old pi without the context hook
|
|
133
133
|
const providerIds = new Set(PROVIDERS.map((provider) => provider.id));
|
|
134
134
|
pi.on("context", (event, ctx) => {
|
|
135
135
|
if (!crossModelThinkingGuardEnabled()) return;
|
|
136
|
-
const guarded =
|
|
137
|
-
maxCharsPerBlock: MAX_CROSS_MODEL_THINKING_CHARS,
|
|
138
|
-
providerIds,
|
|
139
|
-
});
|
|
136
|
+
const guarded = stripCrossModelThinking(event.messages, ctx.model, { providerIds });
|
|
140
137
|
if (guarded === event.messages) return;
|
|
141
138
|
return { messages: guarded as ContextEvent["messages"] };
|
|
142
139
|
});
|