@pi-unipi/image 2.2.4 → 2.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/image",
3
- "version": "2.2.4",
3
+ "version": "2.3.0",
4
4
  "description": "Image generation and image recognition tools for the Pi coding agent",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -34,7 +34,7 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
37
- "@pi-unipi/core": "2.2.0"
37
+ "@pi-unipi/core": "2.3.0"
38
38
  },
39
39
  "peerDependencies": {
40
40
  "@earendil-works/pi-ai": "^0.80.0",
package/src/generate.ts CHANGED
@@ -103,6 +103,26 @@ export function saveImage(
103
103
  }
104
104
  }
105
105
 
106
+ /** Providers pi-ai's images collection can actually generate with. */
107
+ function supportedProviders(imagesApi: ImagesModelsLike): string[] {
108
+ try {
109
+ return [...new Set(imagesApi.getModels().map((m) => m.provider))];
110
+ } catch {
111
+ return [];
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Whether generation can route to a provider. Unknown/empty catalogs are
117
+ * treated as capable so a stubbed or future pi-ai is never blocked by this
118
+ * check — the real call still reports its own error.
119
+ */
120
+ function providerCanGenerate(imagesApi: ImagesModelsLike, provider: string): boolean {
121
+ const providers = supportedProviders(imagesApi);
122
+ if (providers.length === 0) return true;
123
+ return providers.includes(provider);
124
+ }
125
+
106
126
  export interface GenerateOptions {
107
127
  prompt: string;
108
128
  model: ImageGenModel;
@@ -134,6 +154,22 @@ export async function generateImage(options: GenerateOptions): Promise<GenerateR
134
154
  );
135
155
  }
136
156
 
157
+ // pi-ai's images collection carries its own provider set (currently only
158
+ // `openrouter`) and is entirely separate from pi's chat model registry.
159
+ // A chat provider registered by another extension can therefore list image
160
+ // models that generation cannot actually drive — pi-ai answers with a bare
161
+ // "Unknown provider: x". Detect that here and say something useful.
162
+ if (!providerCanGenerate(imagesApi, model.provider)) {
163
+ const supported = supportedProviders(imagesApi);
164
+ throw new Error(
165
+ `Provider "${model.provider}" cannot generate images.\n` +
166
+ `→ Image generation is served by: ${supported.join(", ") || "openrouter"}.\n` +
167
+ `→ "${model.provider}" is a chat provider; its image models are listed for ` +
168
+ `recognition and reference, but generation must go through a supported provider.\n` +
169
+ "→ Pick one with /unipi:image-settings.",
170
+ );
171
+ }
172
+
137
173
  // Prefer pi-ai's own credential store, then the caller-supplied fallback so
138
174
  // a bare OPENROUTER_API_KEY still works.
139
175
  let apiKey: string | undefined;
package/src/models.ts CHANGED
@@ -105,6 +105,23 @@ export async function getImagesModels(): Promise<ImagesModelsLike | null> {
105
105
  return cachedImagesModels;
106
106
  }
107
107
 
108
+ /**
109
+ * Providers pi-ai's images collection can actually generate with.
110
+ *
111
+ * This is NOT the same set as pi's chat model registry: a chat provider
112
+ * registered by another extension may list image models that image generation
113
+ * cannot drive. Empty means "unknown", which callers treat as permissive.
114
+ */
115
+ export async function getGeneratingProviders(): Promise<string[]> {
116
+ const images = await getImagesModels();
117
+ if (!images) return [];
118
+ try {
119
+ return [...new Set(images.getModels().map((m) => m.provider))];
120
+ } catch {
121
+ return [];
122
+ }
123
+ }
124
+
108
125
  /** List available image-generation models. Empty when unavailable. */
109
126
  export async function listImageGenModels(): Promise<ImageGenModel[]> {
110
127
  const images = await getImagesModels();
@@ -14,6 +14,8 @@ export interface SelectableModel {
14
14
  provider: string;
15
15
  id: string;
16
16
  name?: string;
17
+ /** Set when the model is listed but not usable, with the reason. */
18
+ unavailable?: string;
17
19
  }
18
20
 
19
21
  export type ModelSelectorKind = "generate" | "recognize";
@@ -212,6 +214,12 @@ export class ImageModelSelectorOverlay implements Component {
212
214
  this.error = "No model selected";
213
215
  return;
214
216
  }
217
+ // Require a deliberate second Enter on a model that cannot be used, rather
218
+ // than silently saving a choice that will fail at call time.
219
+ if (model.unavailable && this.error === null) {
220
+ this.error = `${model.id} ${model.unavailable} — press Enter again to select anyway`;
221
+ return;
222
+ }
215
223
 
216
224
  this.onSelect?.(`${model.provider}/${model.id}`);
217
225
  this.saved = true;
@@ -335,9 +343,12 @@ export class ImageModelSelectorOverlay implements Component {
335
343
  const marker = isSelected ? this.fg("accent", "▸") : " ";
336
344
  const label = model.name || model.id;
337
345
  const providerTag = this.fg("dim", `[${model.provider}]`);
338
- const display = isSelected
346
+ const base = isSelected
339
347
  ? `${providerTag} ${this.bold(label)}`
340
348
  : `${providerTag} ${this.fg("dim", label)}`;
349
+ const display = model.unavailable
350
+ ? `${base} ${this.fg("warning", `(${model.unavailable})`)}`
351
+ : base;
341
352
  lines.push(this.frameLine(` ${marker} ${display}`, innerWidth));
342
353
  }
343
354
  }
@@ -17,6 +17,7 @@ import {
17
17
  } from "../settings.js";
18
18
  import {
19
19
  formatModelRef,
20
+ getGeneratingProviders,
20
21
  listAllImageGenModels,
21
22
  listVisionModels,
22
23
  type ChatModelRegistry,
@@ -241,7 +242,20 @@ async function collectModels(
241
242
  // Include models from providers registered by other extensions, not just
242
243
  // pi-ai's built-in OpenRouter catalog.
243
244
  const models = await listAllImageGenModels(registry);
244
- return models.map((m) => ({ provider: m.provider, id: m.id, name: m.name }));
245
+ const generating = await getGeneratingProviders();
246
+
247
+ return models.map((m) => ({
248
+ provider: m.provider,
249
+ id: m.id,
250
+ name: m.name,
251
+ // pi-ai's images collection has its own provider set. A chat provider's
252
+ // image models are listed for reference but cannot actually generate,
253
+ // so flag them rather than letting the user pick a dead option.
254
+ unavailable:
255
+ generating.length > 0 && !generating.includes(m.provider)
256
+ ? "cannot generate"
257
+ : undefined,
258
+ }));
245
259
  }
246
260
 
247
261
  if (!registry) return [];