@gtrabanco/pi-nan-provider 0.10.3 → 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/AGENTS.md +68 -9
- package/README.es.md +57 -9
- package/README.md +57 -9
- package/package.json +5 -5
- package/scripts/models.generated.ts +2 -2
- package/src/commands.ts +6 -4
- package/src/context-overflow-classifier.ts +114 -23
- package/src/fetch-models.ts +147 -2
- package/src/images.ts +408 -0
- package/src/index.ts +5 -2
- package/src/provider-factory.ts +102 -30
package/src/provider-factory.ts
CHANGED
|
@@ -22,6 +22,12 @@
|
|
|
22
22
|
* Streaming API resolution: bare root under pi's compat alias is the instance
|
|
23
23
|
* anchor; otherwise a file URL from import.meta.resolve; never a bare subpath
|
|
24
24
|
* (loud failure — issue #8). Full logic in src/pi-ai-loader.ts.
|
|
25
|
+
*
|
|
26
|
+
* The provider also exposes NaN image models (flux-2-klein, qwen-image-2.1)
|
|
27
|
+
* through an image API ("nan-images"): `baselineImageModels` are added to the
|
|
28
|
+
* model set and a `ProviderImages` implementation is wired via
|
|
29
|
+
* `createNanImagesApi`, so pi's model registry can route
|
|
30
|
+
* `generateImages()` calls through it.
|
|
25
31
|
*/
|
|
26
32
|
|
|
27
33
|
import * as piAi from "@earendil-works/pi-ai";
|
|
@@ -31,11 +37,17 @@ import type {
|
|
|
31
37
|
RefreshModelsContext,
|
|
32
38
|
} from "@earendil-works/pi-ai";
|
|
33
39
|
import { withContextOverflowClassification } from "./context-overflow-classifier.ts";
|
|
40
|
+
import { NAN_IMAGE_API, baselineImageModels, createNanImagesApi } from "./images.ts";
|
|
34
41
|
import {
|
|
35
42
|
baselineModels,
|
|
36
43
|
DEFAULT_MODELS_TIMEOUT_MS,
|
|
37
44
|
resolveCatalog,
|
|
38
45
|
type CatalogSource,
|
|
46
|
+
overlayGeneratedCapabilities,
|
|
47
|
+
buildOverlayMap,
|
|
48
|
+
applyOverlay,
|
|
49
|
+
type OverlayEntry,
|
|
50
|
+
NAN_GENERATED_MODELS,
|
|
39
51
|
} from "./fetch-models.ts";
|
|
40
52
|
import { resolveOpenAICompletionsApi } from "./pi-ai-loader.ts";
|
|
41
53
|
import { sanitizeOpenAICompatPayload } from "./openai-compat-sanitizer.ts";
|
|
@@ -126,6 +138,55 @@ export function wrapApiForStrictSanitization(api: ProviderStreams): ProviderStre
|
|
|
126
138
|
};
|
|
127
139
|
}
|
|
128
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Stale-catalog overlay (issue #19): pi-ai's `createProvider` builds an
|
|
143
|
+
* internal `refreshModels` where phase 1 restores `context.stored.models`
|
|
144
|
+
* verbatim and phase 2 calls the input `fetchModels` + persists. The
|
|
145
|
+
* `dynamicModels` list is a closure variable — external code cannot assign
|
|
146
|
+
* it. When pi restores a persisted store entry written by an older version
|
|
147
|
+
* of this package (e.g. without `thinkingLevelMap`), that stale copy is
|
|
148
|
+
* used as-is and never refreshed (because `checkedAt` is recent).
|
|
149
|
+
*
|
|
150
|
+
* `currentModels()` merges static `input.models` with `dynamicModels` and a
|
|
151
|
+
* dynamic entry REPLACES the baseline entry with the same id+type (the
|
|
152
|
+
* shadowing mechanism). The `ModelsImpl` in pi resolves models exclusively
|
|
153
|
+
* via `getModels()` / `getAllModels()` on the provider object — it never
|
|
154
|
+
* inspects the internal dynamicModels list.
|
|
155
|
+
*
|
|
156
|
+
* The fix: wrap `getModels` and `getAllModels` on the returned provider so
|
|
157
|
+
* every call re-checks the generated catalog and applies capability data
|
|
158
|
+
* when the id exists there. The store decides which ids exist; the
|
|
159
|
+
* generated catalog decides capability data. Read-time overlay is the only
|
|
160
|
+
* provider-side hook that wins over the stale restore.
|
|
161
|
+
*
|
|
162
|
+
* `overlayMap` is prebuilt once at factory time from BOTH the generated
|
|
163
|
+
* chat catalog AND `baselineImageModels(source)`. Both `getModels`
|
|
164
|
+
* and `getAllModels` use `applyOverlay` for O(1) per-model lookup (GAP 2).
|
|
165
|
+
* This also covers image models: pi-ai's image models are only visible
|
|
166
|
+
* through `getAllModels()`, so a stale stored IMAGE entry would otherwise
|
|
167
|
+
* replace the baseline image entry with no overlay (GAP 1).
|
|
168
|
+
*
|
|
169
|
+
* We do NOT override `refreshModels` — the built-in phases stay intact. The
|
|
170
|
+
* overlay only affects read paths.
|
|
171
|
+
*/
|
|
172
|
+
function withStaleCatalogOverlay(
|
|
173
|
+
provider: Provider<"openai-completions">,
|
|
174
|
+
overlayMap: Map<string, OverlayEntry>,
|
|
175
|
+
): Provider<"openai-completions"> {
|
|
176
|
+
return {
|
|
177
|
+
...provider,
|
|
178
|
+
getModels: () => {
|
|
179
|
+
const models = provider.getModels();
|
|
180
|
+
return models.map((m) => applyOverlay(m, overlayMap)) as typeof models;
|
|
181
|
+
},
|
|
182
|
+
getAllModels: () => {
|
|
183
|
+
const all = provider.getAllModels?.();
|
|
184
|
+
if (!all) return [];
|
|
185
|
+
return all.map((m) => applyOverlay(m, overlayMap));
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
129
190
|
/**
|
|
130
191
|
* Build a complete pi-ai Provider for an OpenAI-compatible endpoint:
|
|
131
192
|
*
|
|
@@ -133,11 +194,15 @@ export function wrapApiForStrictSanitization(api: ProviderStreams): ProviderStre
|
|
|
133
194
|
* `/login <id>` prompts for the key (pi's `envApiKeyAuth` semantics — the
|
|
134
195
|
* same precedence the built-in providers use). No prompt is needed when the
|
|
135
196
|
* env var is set.
|
|
136
|
-
* - models: the generated fallback catalog
|
|
197
|
+
* - models: the generated chat-model fallback catalog plus NaN image models
|
|
198
|
+
* (flux-2-klein, qwen-image-2.1) as static baseline, so all models are
|
|
137
199
|
* available with zero network.
|
|
138
200
|
* - fetchModels: live `/models` IDs × generated capability data; falls back
|
|
139
201
|
* to the baseline when the endpoint is unreachable. pi's Models runtime
|
|
140
202
|
* drives refreshes (startup/periodic) and persists the overlay.
|
|
203
|
+
* - images: a `ProviderImages` implementation for NaN's OpenAI-compatible
|
|
204
|
+
* image endpoints, so `generateImages()` calls are routed through pi's
|
|
205
|
+
* model registry.
|
|
141
206
|
* - api: the openai-completions streaming implementation resolved via
|
|
142
207
|
* `resolveOpenAICompletionsApi` (bare root under pi's compat alias as
|
|
143
208
|
* the instance anchor; otherwise a file URL from import.meta.resolve;
|
|
@@ -157,33 +222,40 @@ export async function createNanCompatibleProvider(
|
|
|
157
222
|
// `available` (e.g. premium-tier models you are not subscribed to).
|
|
158
223
|
let liveIds: Set<string> | undefined;
|
|
159
224
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
225
|
+
const imageModels = baselineImageModels(source);
|
|
226
|
+
const overlayMap = buildOverlayMap(source, NAN_GENERATED_MODELS, imageModels);
|
|
227
|
+
|
|
228
|
+
return withStaleCatalogOverlay(
|
|
229
|
+
piAi.createProvider({
|
|
230
|
+
id: config.id,
|
|
231
|
+
name: config.name,
|
|
232
|
+
baseUrl: config.baseUrl,
|
|
233
|
+
auth: { apiKey: piAi.envApiKeyAuth(`${config.name} API key`, config.envVars) },
|
|
234
|
+
models: [...baselineModels(source), ...baselineImageModels(source)],
|
|
235
|
+
images: { [NAN_IMAGE_API]: createNanImagesApi({ fetchImpl: options.fetchImpl, timeoutMs: options.timeoutMs }) },
|
|
236
|
+
fetchModels: async (context: RefreshModelsContext) => {
|
|
237
|
+
const credential = context.credential;
|
|
238
|
+
const resolved = await resolveCatalog(source, {
|
|
239
|
+
apiKey: credential?.type === "api_key" ? credential.key : undefined,
|
|
240
|
+
timeoutMs: options.timeoutMs ?? DEFAULT_MODELS_TIMEOUT_MS,
|
|
241
|
+
fetchImpl: options.fetchImpl,
|
|
242
|
+
});
|
|
243
|
+
liveIds = resolved.liveIds;
|
|
244
|
+
return resolved.models;
|
|
245
|
+
},
|
|
246
|
+
filterModels: (models) => {
|
|
247
|
+
// Snapshot: TS can't prove `liveIds` unchanged across the closure boundary.
|
|
248
|
+
const current = liveIds;
|
|
249
|
+
return current ? models.filter((model) => current.has(model.id)) : models;
|
|
250
|
+
},
|
|
251
|
+
// Sanitize the payload for NaN's strict schema, then classify an opaque
|
|
252
|
+
// generic 400 as a context overflow when the request we just sent was over
|
|
253
|
+
// the model's window. The second layer keeps a replayed cross-model
|
|
254
|
+
// reasoning trace (or any other over-window request the context-hook guard
|
|
255
|
+
// cannot reach, e.g. NAN_THINKING_GUARD=0) recoverable instead of wedging
|
|
256
|
+
// the session — see src/context-overflow-classifier.ts.
|
|
257
|
+
api: withContextOverflowClassification(wrapApiForStrictSanitization(apiFactory())),
|
|
258
|
+
}),
|
|
259
|
+
overlayMap,
|
|
260
|
+
);
|
|
189
261
|
}
|