@gtrabanco/pi-nan-provider 0.6.10 → 0.6.11

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 CHANGED
@@ -93,10 +93,13 @@ Every PR that changes code MUST bump `package.json` version in the same PR; CI p
93
93
  `~/.pi/agent/npm/node_modules`; its `estimateMessageTokens` lacks the `system`
94
94
  branch and crashes pi 0.87's string-content `system` transcript with
95
95
  `block.name.length`. `src/pi-ai-loader.ts` therefore binds the streaming
96
- factory to the same package instance as the bare-root import: use the root
97
- export when present, else derive a FILE URL from
98
- `import.meta.resolve("@earendil-works/pi-ai")`
99
- (`api/openai-completions.lazy.js`, then `compat.js`). No bare pi-ai subpath
96
+ factory to the same package instance the host loaded: use the root export
97
+ when present, else resolve the bare root **from the host process entrypoint**
98
+ (`process.argv[1]`) via
99
+ `import.meta.resolve("@earendil-works/pi-ai", hostAnchor)` — an
100
+ extension-relative resolve returns the extension tree's stale copy (the
101
+ v0.6.10 regression that left #8 open) — then derive a FILE URL for
102
+ `api/openai-completions.lazy.js` (then `compat.js`). No bare pi-ai subpath
100
103
  specifier is imported anywhere in `src/` (static or dynamic); failure is loud
101
104
  (`PiAiStreamingApiResolutionError`). Guarded by `test/extension-load.test.ts`
102
105
  and `test/issue-8-pi-ai-instance.test.ts`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gtrabanco/pi-nan-provider",
3
- "version": "0.6.10",
3
+ "version": "0.6.11",
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-21T23:54:45.052Z
4
+ // Source: https://models.dev/api.json (provider "nan"), fetched 2026-09-22T08:55:04.775Z
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.
@@ -382,7 +382,7 @@ export const NAN_GENERATED_MODELS: readonly GeneratedModelEntry[] = [
382
382
  export const GENERATED_CATALOG_META = {
383
383
  source: "https://models.dev/api.json",
384
384
  modelsDevProvider: "nan",
385
- fetchedAt: "2026-09-21T23:54:45.052Z",
385
+ fetchedAt: "2026-09-22T08:55:04.775Z",
386
386
  modelCount: 6,
387
387
  models: ["deepseek-v4-flash","gemma4","glm5.3-flash","mimo-v2.5","qwen3.6","qwen3.8-flash"],
388
388
  notes: [
@@ -18,13 +18,15 @@
18
18
  * `undefined is not an object (evaluating 'block.name.length')` — before the
19
19
  * request is ever sent, so it reads like a NaN/gateway failure.
20
20
  *
21
- * `import.meta.resolve("@earendil-works/pi-ai")` returns the *same instance*
22
- * the bare-root static import used in every environment measured (both the
23
- * host's 0.87 core on pi-web, and the extension-tree copy under plain
24
- * node/jiti). It is therefore the anchor: derive a **file URL** for the
25
- * sibling `api/openai-completions.lazy.js` (then `compat.js`) from that
26
- * root and dynamic-import the URL. A file URL bypasses package resolution
27
- * entirely, so the loaded module is guaranteed to be the host's instance.
21
+ * The anchor is the **host process entrypoint** (`process.argv[1]`), passed to
22
+ * `import.meta.resolve(specifier, parent)`. An extension-relative resolve is
23
+ * NOT enough: it returns whatever the extension's own tree holds, which under
24
+ * pi-web is exactly the stale 0.85.1 copy (that was the v0.6.10 regression:
25
+ * the "host-resolved root" was still the extension's copy). From the
26
+ * host-resolved root derive a **file URL** for the sibling
27
+ * `api/openai-completions.lazy.js` (then `compat.js`) and dynamic-import the
28
+ * URL. A file URL bypasses package resolution entirely, so the loaded module is
29
+ * guaranteed to be the host's instance.
28
30
  *
29
31
  * Under the bundled CLI / Node-mode aliases / compiled binary, the bare root
30
32
  * is the compat entrypoint and already exposes the factory, so the first
@@ -42,6 +44,7 @@
42
44
  import * as piAi from "@earendil-works/pi-ai";
43
45
  import type { ProviderStreams } from "@earendil-works/pi-ai";
44
46
  import { createRequire } from "node:module";
47
+ import { pathToFileURL } from "node:url";
45
48
 
46
49
  /** The only pi-ai specifier this package may import. */
47
50
  export const PI_AI_PACKAGE_SPECIFIER = "@earendil-works/pi-ai";
@@ -106,22 +109,83 @@ export function openAICompletionsApiFrom(namespace: unknown): OpenAICompletionsA
106
109
  * `import.meta.resolve` is absent from bun-types' `ImportMeta`, so read it
107
110
  * through an explicit shape. Bun/Node expose it at runtime; when it is missing
108
111
  * or throws, `createRequire` resolves the same bare root from this module.
112
+ *
113
+ * Referenced directly (not via a variable or type cast) so pi's jiti loader
114
+ * can rewrite it. PR #11 (jiti compatibility) and PR #12 (host-anchored
115
+ * resolution) merge: the direct reference fixes jiti, the host anchor fixes
116
+ * pi-web.
109
117
  */
110
- type ImportMetaWithResolve = ImportMeta & { resolve?: (specifier: string) => string };
118
+ function readImportMetaResolve(): ((specifier: string, parent?: string) => string) | undefined {
119
+ if (typeof import.meta.resolve !== "function") return undefined;
120
+ return (specifier, parent) =>
121
+ parent === undefined ? import.meta.resolve(specifier) : import.meta.resolve(specifier, parent);
122
+ }
111
123
 
112
- const defaultPiAiLoaderHost: PiAiLoaderHost = {
113
- namespace: piAi as unknown as ModuleNamespace,
114
- resolveSpecifier(specifier: string): string {
115
- const meta = import.meta as ImportMetaWithResolve;
116
- if (typeof meta.resolve === "function") {
124
+ /**
125
+ * File URL of the host process entrypoint (pi-web's `sessiond.js`, the pi CLI,
126
+ * a test runner). Resolving FROM it pins the result to the host's module graph
127
+ * instead of the extension's own tree.
128
+ */
129
+ export function hostAnchorUrl(entry: string | undefined): string | undefined {
130
+ if (typeof entry !== "string" || entry.length === 0) return undefined;
131
+ try {
132
+ return pathToFileURL(entry).href;
133
+ } catch {
134
+ return undefined;
135
+ }
136
+ }
137
+
138
+ /** Injection seam for {@link resolvePiAiSpecifier}. */
139
+ export interface PiAiSpecifierResolution {
140
+ /** Anchor URL to resolve FROM; omitted means extension-relative resolution. */
141
+ anchorUrl?: string;
142
+ /** `import.meta.resolve`, when the runtime exposes it. */
143
+ resolve?: (specifier: string, parent?: string) => string;
144
+ /** Last-resort CJS resolver; only used when neither `resolve` call succeeds. */
145
+ fallback?: (specifier: string) => string;
146
+ }
147
+
148
+ /**
149
+ * Resolve a pi-ai specifier the way the host runtime does.
150
+ *
151
+ * The `anchorUrl` is the whole point. Under pi-web the extension's own tree
152
+ * holds a stale hoisted `@earendil-works/pi-ai` (0.85.1), so an
153
+ * extension-relative resolve returns a package the host never loaded, and the
154
+ * derived "host-resolved root" is still that stale copy. Passing the host
155
+ * entrypoint as the resolver's parent returns the instance the host itself
156
+ * uses (the v0.6.10 fix anchored on `import.meta.resolve` alone and therefore
157
+ * still selected the stale copy).
158
+ */
159
+ export function resolvePiAiSpecifier(
160
+ specifier: string,
161
+ options: PiAiSpecifierResolution = {},
162
+ ): string {
163
+ const resolve = options.resolve ?? readImportMetaResolve();
164
+ const fallback =
165
+ options.fallback ?? ((value: string) => createRequire(import.meta.url).resolve(value));
166
+ if (resolve !== undefined) {
167
+ if (options.anchorUrl !== undefined) {
117
168
  try {
118
- const resolved = meta.resolve(specifier);
119
- if (typeof resolved === "string" && resolved.length > 0) return resolved;
169
+ const anchored = resolve(specifier, options.anchorUrl);
170
+ if (typeof anchored === "string" && anchored.length > 0) return anchored;
120
171
  } catch {
121
- // Fall through to createRequire — same bare root, same instance.
172
+ // Runtimes without parent support fall through to the bare call.
122
173
  }
123
174
  }
124
- return createRequire(import.meta.url).resolve(specifier);
175
+ try {
176
+ const resolved = resolve(specifier);
177
+ if (typeof resolved === "string" && resolved.length > 0) return resolved;
178
+ } catch {
179
+ // Fall through to createRequire — same bare root, same instance.
180
+ }
181
+ }
182
+ return fallback(specifier);
183
+ }
184
+
185
+ const defaultPiAiLoaderHost: PiAiLoaderHost = {
186
+ namespace: piAi as unknown as ModuleNamespace,
187
+ resolveSpecifier(specifier: string): string {
188
+ return resolvePiAiSpecifier(specifier, { anchorUrl: hostAnchorUrl(process.argv[1]) });
125
189
  },
126
190
  importModule: (url: string) => import(url) as Promise<ModuleNamespace>,
127
191
  };