@pi-unipi/unipi 2.2.1 → 2.2.2
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/package.json
CHANGED
|
@@ -72,10 +72,20 @@ let imagesModelsAttempted = false;
|
|
|
72
72
|
/**
|
|
73
73
|
* Load pi-ai's built-in images collection.
|
|
74
74
|
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
75
|
+
* ⚠️ Do NOT reach for `getImageModels`/`getImageProviders`/`generateImages`:
|
|
76
|
+
*
|
|
77
|
+
* - They are NOT re-exported from the pi-ai package root. Importing
|
|
78
|
+
* `@earendil-works/pi-ai` and calling `getImageModels("openrouter")`
|
|
79
|
+
* returns an EMPTY ARRAY rather than throwing, so the mistake looks like
|
|
80
|
+
* "no models are installed" and costs a long debugging session.
|
|
81
|
+
* - The file that defines them, `dist/image-models.js`, is not a permitted
|
|
82
|
+
* subpath in pi-ai's `exports` map, so importing it directly throws
|
|
83
|
+
* ERR_PACKAGE_PATH_NOT_EXPORTED.
|
|
84
|
+
*
|
|
85
|
+
* The supported entry point is `@earendil-works/pi-ai/providers/all` →
|
|
86
|
+
* `builtinImagesModels()`, which returns the catalog AND resolves auth.
|
|
87
|
+
* A failure is not fatal: it degrades to null and callers report that no
|
|
88
|
+
* models are available.
|
|
79
89
|
*/
|
|
80
90
|
export async function getImagesModels(): Promise<ImagesModelsLike | null> {
|
|
81
91
|
if (cachedImagesModels || imagesModelsAttempted) return cachedImagesModels;
|
|
@@ -106,6 +116,111 @@ export async function listImageGenModels(): Promise<ImageGenModel[]> {
|
|
|
106
116
|
}
|
|
107
117
|
}
|
|
108
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Heuristic: does a chat-registry model look like an image generator?
|
|
121
|
+
*
|
|
122
|
+
* Third-party providers registered by other extensions (pi-omniroute-bridge,
|
|
123
|
+
* for example) surface text-to-image endpoints as ordinary chat models. They
|
|
124
|
+
* declare no `output` modality at all, so a strict `output.includes("image")`
|
|
125
|
+
* check finds nothing and the user sees only pi-ai's built-in OpenRouter
|
|
126
|
+
* catalog. We therefore accept an explicit image output when present, and
|
|
127
|
+
* otherwise fall back to well-known generator naming.
|
|
128
|
+
*/
|
|
129
|
+
const GENERATOR_NAME_HINTS = [
|
|
130
|
+
"text-to-image",
|
|
131
|
+
"flux",
|
|
132
|
+
"dall-e",
|
|
133
|
+
"dalle",
|
|
134
|
+
"imagen",
|
|
135
|
+
"recraft",
|
|
136
|
+
"seedream",
|
|
137
|
+
"riverflow",
|
|
138
|
+
"grok-imagine",
|
|
139
|
+
"stable-diffusion",
|
|
140
|
+
"sdxl",
|
|
141
|
+
"midjourney",
|
|
142
|
+
"ideogram",
|
|
143
|
+
"nano-banana",
|
|
144
|
+
];
|
|
145
|
+
|
|
146
|
+
export function looksLikeImageGenerator(model: {
|
|
147
|
+
id: string;
|
|
148
|
+
name?: string;
|
|
149
|
+
output?: string[];
|
|
150
|
+
}): boolean {
|
|
151
|
+
// An explicit declaration always wins.
|
|
152
|
+
if (Array.isArray(model.output)) {
|
|
153
|
+
if (model.output.includes("image")) return true;
|
|
154
|
+
// Declared, but text-only — trust it and do not guess from the name.
|
|
155
|
+
if (model.output.length > 0) return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const haystack = `${model.id} ${model.name ?? ""}`.toLowerCase();
|
|
159
|
+
// Match "image" as a delimited segment ("gpt-5-image", "gemini-3-pro-image"),
|
|
160
|
+
// not the bare word anywhere — which would wrongly catch vision models such
|
|
161
|
+
// as "claude-image-understanding". \b handles the whitespace between id and
|
|
162
|
+
// name; a character class alone missed ids ending the id portion.
|
|
163
|
+
if (/(^|[/\-_\s])image\b/.test(haystack) && !haystack.includes("understand")) {
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
return GENERATOR_NAME_HINTS.some((hint) => haystack.includes(hint));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Image-generation models discovered from the chat registry — i.e. providers
|
|
171
|
+
* registered by other extensions, which pi-ai's built-in catalog knows nothing
|
|
172
|
+
* about.
|
|
173
|
+
*/
|
|
174
|
+
export function listRegistryImageGenModels(
|
|
175
|
+
registry: ChatModelRegistry,
|
|
176
|
+
): ImageGenModel[] {
|
|
177
|
+
let models: unknown[];
|
|
178
|
+
try {
|
|
179
|
+
models = registry.getAvailable?.() ?? registry.getAll();
|
|
180
|
+
} catch {
|
|
181
|
+
return [];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const out: ImageGenModel[] = [];
|
|
185
|
+
for (const model of models) {
|
|
186
|
+
if (model === null || typeof model !== "object") continue;
|
|
187
|
+
const candidate = model as Partial<ImageGenModel>;
|
|
188
|
+
if (typeof candidate.id !== "string" || typeof candidate.provider !== "string") {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (!looksLikeImageGenerator(candidate as ImageGenModel)) continue;
|
|
192
|
+
out.push({
|
|
193
|
+
id: candidate.id,
|
|
194
|
+
provider: candidate.provider,
|
|
195
|
+
name: candidate.name,
|
|
196
|
+
api: candidate.api ?? "",
|
|
197
|
+
...(candidate.output ? { output: candidate.output } : {}),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Every selectable generation model: pi-ai's built-in catalog plus anything
|
|
205
|
+
* contributed by registered providers, de-duplicated by "provider/id".
|
|
206
|
+
*/
|
|
207
|
+
export async function listAllImageGenModels(
|
|
208
|
+
registry?: ChatModelRegistry | null,
|
|
209
|
+
): Promise<ImageGenModel[]> {
|
|
210
|
+
const builtin = await listImageGenModels();
|
|
211
|
+
const fromRegistry = registry ? listRegistryImageGenModels(registry) : [];
|
|
212
|
+
|
|
213
|
+
const seen = new Set(builtin.map((m) => formatModelRef(m).toLowerCase()));
|
|
214
|
+
const merged = [...builtin];
|
|
215
|
+
for (const model of fromRegistry) {
|
|
216
|
+
const key = formatModelRef(model).toLowerCase();
|
|
217
|
+
if (seen.has(key)) continue;
|
|
218
|
+
seen.add(key);
|
|
219
|
+
merged.push(model);
|
|
220
|
+
}
|
|
221
|
+
return merged;
|
|
222
|
+
}
|
|
223
|
+
|
|
109
224
|
/** Inject a stub images collection. Test-only. */
|
|
110
225
|
export function __setImagesModelsForTests(models: ImagesModelsLike | null): void {
|
|
111
226
|
cachedImagesModels = models;
|
|
@@ -14,7 +14,7 @@ import { generateImage } from "./generate.js";
|
|
|
14
14
|
import { loadImage } from "./image-source.js";
|
|
15
15
|
import {
|
|
16
16
|
formatModelRef,
|
|
17
|
-
|
|
17
|
+
listAllImageGenModels,
|
|
18
18
|
resolveImageGenModel,
|
|
19
19
|
resolveVisionModel,
|
|
20
20
|
splitModelRef,
|
|
@@ -108,7 +108,10 @@ function registerGenerateTool(pi: ExtensionAPI): void {
|
|
|
108
108
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
109
109
|
try {
|
|
110
110
|
const config = loadConfig();
|
|
111
|
-
const
|
|
111
|
+
const registry = getRegistry(ctx);
|
|
112
|
+
// Include image models contributed by registered providers, so the
|
|
113
|
+
// tool can resolve anything the settings picker offers.
|
|
114
|
+
const models = await listAllImageGenModels(registry);
|
|
112
115
|
|
|
113
116
|
const requested = params.model?.trim() || config.generate.model;
|
|
114
117
|
const resolved = resolveImageGenModel(requested, models);
|
|
@@ -116,7 +119,6 @@ function registerGenerateTool(pi: ExtensionAPI): void {
|
|
|
116
119
|
|
|
117
120
|
// pi-ai resolves image auth from its own credential store; only fall
|
|
118
121
|
// back to pi's chat-provider key when that comes up empty.
|
|
119
|
-
const registry = getRegistry(ctx);
|
|
120
122
|
const fallbackKey = await resolveApiKey(registry, resolved.provider);
|
|
121
123
|
|
|
122
124
|
const result = await generateImage({
|
|
@@ -56,6 +56,13 @@ export class ImageModelSelectorOverlay implements Component {
|
|
|
56
56
|
invalidate(): void {}
|
|
57
57
|
|
|
58
58
|
handleInput(data: string): void {
|
|
59
|
+
// Ctrl+C must always escape, even mid-filter. Without this the overlay traps
|
|
60
|
+
// the user with no way out.
|
|
61
|
+
if (data === "\x03") {
|
|
62
|
+
this.onClose?.();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
59
66
|
if (this.filterMode) {
|
|
60
67
|
this.handleFilterInput(data);
|
|
61
68
|
return;
|
|
@@ -137,7 +144,9 @@ export class ImageModelSelectorOverlay implements Component {
|
|
|
137
144
|
this.onSelect?.(`${model.provider}/${model.id}`);
|
|
138
145
|
this.saved = true;
|
|
139
146
|
this.error = null;
|
|
140
|
-
|
|
147
|
+
// Close immediately. A deferred close leaves the overlay focused while the
|
|
148
|
+
// caller resumes, which is what let input reach two components at once.
|
|
149
|
+
this.onClose?.();
|
|
141
150
|
}
|
|
142
151
|
|
|
143
152
|
// ─── Theme helpers ───────────────────────────────────────────────────
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
} from "../settings.js";
|
|
18
18
|
import {
|
|
19
19
|
formatModelRef,
|
|
20
|
-
|
|
20
|
+
listAllImageGenModels,
|
|
21
21
|
listVisionModels,
|
|
22
22
|
type ChatModelRegistry,
|
|
23
23
|
} from "../models.js";
|
|
@@ -168,7 +168,7 @@ async function pickModel(
|
|
|
168
168
|
if (models.length === 0) {
|
|
169
169
|
ctx.ui.notify(
|
|
170
170
|
kind === "generate"
|
|
171
|
-
? "No image models available.
|
|
171
|
+
? "No image models available. Add an OpenRouter key with /login, or register a provider that exposes image models."
|
|
172
172
|
: "No vision-capable models configured. Add a model that accepts image input.",
|
|
173
173
|
"warning",
|
|
174
174
|
);
|
|
@@ -183,18 +183,22 @@ async function pickModel(
|
|
|
183
183
|
const current =
|
|
184
184
|
kind === "generate" ? config.generate.model : config.recognize.model;
|
|
185
185
|
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
// `ctx.ui.custom` resolves only when the factory calls `done()`. It MUST be
|
|
187
|
+
// awaited: returning early leaves the overlay on screen while the settings
|
|
188
|
+
// loop mounts the next `ctx.ui.select`, so two focused components fight over
|
|
189
|
+
// the same keystrokes and neither can be closed.
|
|
190
|
+
let picked: string | undefined;
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
picked = await ctx.ui.custom<string | undefined>(
|
|
188
194
|
(tui, theme, _keybindings, done) => {
|
|
189
195
|
const overlay = new ImageModelSelectorOverlay(kind, models, current);
|
|
190
196
|
overlay.setTheme(theme);
|
|
197
|
+
overlay.requestRender = () => tui.requestRender();
|
|
191
198
|
overlay.onSelect = (modelRef) => {
|
|
192
|
-
|
|
193
|
-
if (kind === "generate") next.generate.model = modelRef;
|
|
194
|
-
else next.recognize.model = modelRef;
|
|
195
|
-
saveConfig(next);
|
|
199
|
+
picked = modelRef;
|
|
196
200
|
};
|
|
197
|
-
overlay.onClose = () => done(
|
|
201
|
+
overlay.onClose = () => done(picked);
|
|
198
202
|
return {
|
|
199
203
|
render: (width: number) => overlay.render(width),
|
|
200
204
|
invalidate: () => overlay.invalidate(),
|
|
@@ -209,21 +213,36 @@ async function pickModel(
|
|
|
209
213
|
overlayOptions: { width: "80%", minWidth: 50, anchor: "center", margin: 2 },
|
|
210
214
|
},
|
|
211
215
|
);
|
|
212
|
-
|
|
213
|
-
|
|
216
|
+
} catch (err) {
|
|
217
|
+
ctx.ui.notify(`Model selector error: ${err}`, "error");
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Persist only after the overlay has fully closed, so a cancel leaves the
|
|
222
|
+
// existing config untouched.
|
|
223
|
+
if (!picked) return;
|
|
224
|
+
|
|
225
|
+
const next = loadConfig();
|
|
226
|
+
if (kind === "generate") next.generate.model = picked;
|
|
227
|
+
else next.recognize.model = picked;
|
|
228
|
+
saveConfig(next);
|
|
229
|
+
ctx.ui.notify(`${kind === "generate" ? "Generation" : "Recognition"} model set to ${picked}`, "info");
|
|
214
230
|
}
|
|
215
231
|
|
|
216
232
|
async function collectModels(
|
|
217
233
|
ctx: ExtensionCommandContext,
|
|
218
234
|
kind: "generate" | "recognize",
|
|
219
235
|
): Promise<SelectableModel[]> {
|
|
236
|
+
const registry = (ctx as unknown as { modelRegistry?: ChatModelRegistry })
|
|
237
|
+
.modelRegistry;
|
|
238
|
+
|
|
220
239
|
if (kind === "generate") {
|
|
221
|
-
|
|
240
|
+
// Include models from providers registered by other extensions, not just
|
|
241
|
+
// pi-ai's built-in OpenRouter catalog.
|
|
242
|
+
const models = await listAllImageGenModels(registry);
|
|
222
243
|
return models.map((m) => ({ provider: m.provider, id: m.id, name: m.name }));
|
|
223
244
|
}
|
|
224
245
|
|
|
225
|
-
const registry = (ctx as unknown as { modelRegistry?: ChatModelRegistry })
|
|
226
|
-
.modelRegistry;
|
|
227
246
|
if (!registry) return [];
|
|
228
247
|
|
|
229
248
|
return listVisionModels(registry).map((m) => ({
|