@bacnh85/pi-web 0.9.0 → 0.9.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/CHANGELOG.md +32 -1
- package/README.md +7 -4
- package/extensions/index.ts +11 -8
- package/extensions/lib/imageapi.ts +40 -6
- package/package.json +1 -1
- package/skills/pi-web/SKILL.md +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,36 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.2 (2026-09-13)
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
Review hardening of the `web_image` fallback chain (3 review rounds):
|
|
8
|
+
|
|
9
|
+
- **Cancellation semantics**: an aborted call now surfaces `AbortError`
|
|
10
|
+
immediately — before any provider client construction or fetch — instead of
|
|
11
|
+
walking the chain and reporting "all providers failed". An abort landing
|
|
12
|
+
mid-generation normalizes to `AbortError` with the in-flight provider error
|
|
13
|
+
preserved as `cause`; foreign abort-named errors from upstreams are recorded
|
|
14
|
+
as provider notes and the chain continues.
|
|
15
|
+
- **`n` transparency**: when the gemini web tier returns fewer images than the
|
|
16
|
+
requested `n`, the result states it explicitly (`n` applies to the
|
|
17
|
+
`zai`/`custom` API providers; the gemini web tier returns its own count).
|
|
18
|
+
`out_dir` now resolves against the session cwd, not the process cwd.
|
|
19
|
+
|
|
20
|
+
## 0.9.1 (2026-09-13)
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- **`zai` default model is `glm-image`** (live-verified against
|
|
25
|
+
`api.z.ai/api/paas/v4/images/generations` — the endpoint rejects
|
|
26
|
+
`cogview-4`/`cogview-3-flash` with "Unknown Model"; GLM-Image returns
|
|
27
|
+
URL results).
|
|
28
|
+
- **Failed image downloads no longer waste the generation** — some upstreams
|
|
29
|
+
return URL-only results and the CDN can be unreachable from the local
|
|
30
|
+
network (e.g. `mfile.z.ai` DNS-sinkholed). The tool now reports
|
|
31
|
+
"Not saved (image host unreachable…)" with the URL instead of failing the
|
|
32
|
+
provider; inline image blocks are still attached for saved files.
|
|
33
|
+
|
|
3
34
|
## 0.9.0 (2026-09-13)
|
|
4
35
|
|
|
5
36
|
### Added
|
|
@@ -7,7 +38,7 @@
|
|
|
7
38
|
- **`web_image` tool** — image generation from text via free upstream
|
|
8
39
|
providers, all direct-to-upstream (no self-host services), with automatic
|
|
9
40
|
fallback: `gemini` (gemini.google.com web tier via `gemini-reverse`, guest
|
|
10
|
-
or cookie auth) → `zai` (official `api.z.ai`
|
|
41
|
+
or cookie auth) → `zai` (official `api.z.ai` GLM-Image via `ZAI_API_KEY`)
|
|
11
42
|
→ `custom` (any OpenAI-compatible `/images/generations` endpoint via
|
|
12
43
|
`WEB_IMAGE_API_BASE_URL`). `provider: "auto"` walks the chain and the
|
|
13
44
|
result reports every fallback attempt.
|
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ Variables:
|
|
|
32
32
|
| `CRAWL4AI_API_TOKEN` | No (3) | — | Required if Crawl4AI auth enabled |
|
|
33
33
|
| `GEMINI_WEB_SECURE_1PSID` | No (4) | — | `__Secure-1PSID` cookie from gemini.google.com — enables authed `web_research` (Deep Research) |
|
|
34
34
|
| `GEMINI_WEB_PROXY` | No | — | Proxy URL for Gemini web calls (escape hatch if Google blocks the IP) |
|
|
35
|
-
| `ZAI_API_KEY` | No (5) | — | Z.ai API key — enables the `web_image` `zai` provider (
|
|
35
|
+
| `ZAI_API_KEY` | No (5) | — | Z.ai API key — enables the `web_image` `zai` provider (GLM-Image via the official `api.z.ai`); `Z_AI_API_KEY` also accepted |
|
|
36
36
|
| `WEB_IMAGE_API_BASE_URL` | No | — | `web_image` `custom` provider: any OpenAI-compatible images endpoint (e.g. `https://api.openai.com/v1`) |
|
|
37
37
|
| `WEB_IMAGE_API_KEY` | No | — | Bearer key for the `custom` endpoint |
|
|
38
38
|
| `WEB_IMAGE_API_LABEL` | No | — | Display label for the `custom` endpoint (default: host name) |
|
|
@@ -273,8 +273,8 @@ self-host services):
|
|
|
273
273
|
|
|
274
274
|
```
|
|
275
275
|
web_image(prompt="isometric cutaway of a container ship, technical illustration")
|
|
276
|
-
web_image(prompt="...", provider="zai") # pin
|
|
277
|
-
web_image(prompt="...", model="
|
|
276
|
+
web_image(prompt="...", provider="zai") # pin GLM-Image via api.z.ai
|
|
277
|
+
web_image(prompt="...", model="glm-image", n=2, out_dir="/tmp/imgs")
|
|
278
278
|
```
|
|
279
279
|
|
|
280
280
|
**Provider chain** (`provider: "auto"` tries in order; pin one to skip):
|
|
@@ -282,7 +282,7 @@ web_image(prompt="...", model="cogview-4", n=2, out_dir="/tmp/imgs")
|
|
|
282
282
|
| Provider | Upstream | Auth | Notes |
|
|
283
283
|
|---|---|---|---|
|
|
284
284
|
| `gemini` (default) | gemini.google.com web tier | none (guest) or `GEMINI_WEB_SECURE_1PSID` | free ≈ 20 images/day; availability varies by region/account |
|
|
285
|
-
| `zai` | `https://api.z.ai/api/paas/v4` (official API) | `ZAI_API_KEY` |
|
|
285
|
+
| `zai` | `https://api.z.ai/api/paas/v4` (official API) | `ZAI_API_KEY` | GLM-Image (`model` default), fully ToS-compliant |
|
|
286
286
|
| `custom` | any OpenAI-compatible `/images/generations` endpoint | `WEB_IMAGE_API_KEY` | e.g. official OpenAI `https://api.openai.com/v1` |
|
|
287
287
|
|
|
288
288
|
Results are saved to `out_dir` (default: fresh temp dir) and returned as file
|
|
@@ -290,6 +290,9 @@ paths **plus inline image blocks** (multimodal models see the render
|
|
|
290
290
|
immediately). `details` reports the winning provider, model, and fallback
|
|
291
291
|
attempts.
|
|
292
292
|
|
|
293
|
+
`n` (1–4) applies to the API providers (`zai`/`custom`); the Gemini web tier
|
|
294
|
+
returns its own image count (surfaced as a provider note when fewer than `n`).
|
|
295
|
+
|
|
293
296
|
**Guardrails** (soft, in-memory): per-provider `WEB_IMAGE_MIN_INTERVAL_MS`
|
|
294
297
|
(default 5 s) and a `WEB_IMAGE_DAILY_CAP` (default 20/day, applied to the
|
|
295
298
|
Gemini web tier only — keyed APIs are billed upstream and stay uncapped).
|
package/extensions/index.ts
CHANGED
|
@@ -108,7 +108,7 @@ const WEB_ROUTING_GUIDANCE = `## Web Tool Routing (pi-web)
|
|
|
108
108
|
- **web_crawl** — multi-page crawl: \`mode: "light"\` (Firecrawl, url) or \`mode: "full"\` (Crawl4AI, urls[]).
|
|
109
109
|
- **web_screenshot** / **web_pdf** — page capture (Crawl4AI).
|
|
110
110
|
- **web_research** — AI-synthesized research via Gemini web (mode "ask" = grounded answer, guest OK; mode "research" = Deep Research report, needs cookie + Gemini Advanced, takes minutes).
|
|
111
|
-
- **web_image** — text→image generation via free upstreams (auto: Gemini web → Z.ai
|
|
111
|
+
- **web_image** — text→image generation via free upstreams (auto: Gemini web → Z.ai GLM-Image → custom OpenAI-images endpoint; \`model\`/\`n\` params).
|
|
112
112
|
- **web_status** — provider config + health.
|
|
113
113
|
|
|
114
114
|
Rules: Firecrawl Search is weak on domain-specific queries — prefer SearXNG/Brave; Firecrawl Scrape fails on bot-protected sites — use Crawl4AI (\`mode: "full"\`) then agy (\`mode: "agy"\`); cite source URLs.`;
|
|
@@ -529,8 +529,8 @@ export default function piWebExtension(pi: ExtensionAPI) {
|
|
|
529
529
|
name: "web_image",
|
|
530
530
|
label: "Web Image Generation",
|
|
531
531
|
description:
|
|
532
|
-
"Generate images from text via free upstream providers, with fallback: Gemini web (gemini.google.com, guest or cookie auth), Z.ai official API (
|
|
533
|
-
promptSnippet: "Generate images via free upstreams (Gemini web, Z.ai
|
|
532
|
+
"Generate images from text via free upstream providers, with fallback: Gemini web (gemini.google.com, guest or cookie auth), Z.ai official API (GLM-Image via ZAI_API_KEY), or any custom OpenAI-compatible images endpoint (WEB_IMAGE_API_BASE_URL). Returns saved file paths plus the images inline.",
|
|
533
|
+
promptSnippet: "Generate images via free upstreams (Gemini web, Z.ai GLM-Image)",
|
|
534
534
|
promptGuidelines: [
|
|
535
535
|
"Use for image GENERATION from a text prompt. provider auto falls back gemini → zai → custom. Capturing an EXISTING page is web_screenshot, not this.",
|
|
536
536
|
],
|
|
@@ -540,8 +540,8 @@ export default function piWebExtension(pi: ExtensionAPI) {
|
|
|
540
540
|
[Type.Literal("auto"), Type.Literal("gemini"), Type.Literal("zai"), Type.Literal("custom")],
|
|
541
541
|
{ default: "auto", description: "auto = gemini → zai (if ZAI_API_KEY) → custom (if WEB_IMAGE_API_BASE_URL); pin one to skip fallback." },
|
|
542
542
|
)),
|
|
543
|
-
model: Type.Optional(Type.String({ description: "Provider-specific model (e.g.
|
|
544
|
-
n: Type.Optional(Type.Number({ default: 1, description: "Number of images, 1-4." })),
|
|
543
|
+
model: Type.Optional(Type.String({ description: "Provider-specific model (e.g. glm-image, or a Gemini image-capable model id). Omit for the provider default." })),
|
|
544
|
+
n: Type.Optional(Type.Number({ default: 1, description: "Number of images, 1-4 (applies to zai/custom; the gemini web tier returns its own count)." })),
|
|
545
545
|
out_dir: Type.Optional(Type.String({ description: "Directory for saved images (default: fresh temp dir)." })),
|
|
546
546
|
...sharedControlSchema,
|
|
547
547
|
}),
|
|
@@ -552,7 +552,7 @@ export default function piWebExtension(pi: ExtensionAPI) {
|
|
|
552
552
|
const n = Math.min(Math.max(Math.trunc((params.n as number) ?? 1) || 1, 1), 4);
|
|
553
553
|
const timeoutMs = Math.min(Math.max((params.timeout_ms as number) ?? 180_000, 10_000), 600_000);
|
|
554
554
|
const outDir = params.out_dir
|
|
555
|
-
? path.resolve(String(params.out_dir))
|
|
555
|
+
? path.resolve(cwd, String(params.out_dir))
|
|
556
556
|
: await fs.promises.mkdtemp(path.join(os.tmpdir(), "pi-web-image-"));
|
|
557
557
|
const result = await generateImageWithFallback({
|
|
558
558
|
prompt,
|
|
@@ -571,13 +571,16 @@ export default function piWebExtension(pi: ExtensionAPI) {
|
|
|
571
571
|
`Provider: ${result.provider}${result.model ? ` (${result.model})` : ""}`,
|
|
572
572
|
`Saved: ${result.paths.length} image(s)`,
|
|
573
573
|
...result.paths.map((p) => ` ${p}`),
|
|
574
|
-
result.
|
|
574
|
+
...(result.urls.length
|
|
575
|
+
? [`Not saved (image host unreachable from this machine — open directly):`, ...result.urls.map((u) => ` ${u}`)]
|
|
576
|
+
: []),
|
|
577
|
+
result.attempts.length ? `Provider notes: ${result.attempts.join(" | ")}` : null,
|
|
575
578
|
].filter(Boolean).join("\n");
|
|
576
579
|
const content: Array<{ type: "text"; text: string } | { type: "image"; data: string; mimeType: string }> = [
|
|
577
580
|
{ type: "text" as const, text },
|
|
578
581
|
...blocks,
|
|
579
582
|
];
|
|
580
|
-
return { content, details: { provider: result.provider, model: result.model, paths: result.paths, attempts: result.attempts } };
|
|
583
|
+
return { content, details: { provider: result.provider, model: result.model, paths: result.paths, urls: result.urls, attempts: result.attempts } };
|
|
581
584
|
},
|
|
582
585
|
});
|
|
583
586
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Generic OpenAI-compatible images client + fallback chain for web_image.
|
|
2
|
-
// Serves the `zai` preset (official api.z.ai,
|
|
2
|
+
// Serves the `zai` preset (official api.z.ai, GLM-Image) and any `custom`
|
|
3
3
|
// OpenAI-images endpoint — direct-to-upstream plain fetch, no self-host.
|
|
4
4
|
|
|
5
5
|
import fs from "node:fs";
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
// Config
|
|
19
19
|
// ---------------------------------------------------------------------------
|
|
20
20
|
|
|
21
|
-
export const ZAI_PRESET = { baseUrl: "https://api.z.ai/api/paas/v4", defaultModel: "
|
|
21
|
+
export const ZAI_PRESET = { baseUrl: "https://api.z.ai/api/paas/v4", defaultModel: "glm-image" } as const;
|
|
22
22
|
|
|
23
23
|
export interface ImageApiConfig {
|
|
24
24
|
zai?: { apiKey: string; source: string };
|
|
@@ -155,6 +155,8 @@ export interface FetchLike {
|
|
|
155
155
|
|
|
156
156
|
export interface ApiImageResult {
|
|
157
157
|
paths: string[];
|
|
158
|
+
/** Image URLs that could not be downloaded (e.g. CDN unreachable) — the generation still happened. */
|
|
159
|
+
urls: string[];
|
|
158
160
|
model?: string;
|
|
159
161
|
}
|
|
160
162
|
|
|
@@ -212,17 +214,25 @@ export async function apiGenerateImage(opts: {
|
|
|
212
214
|
if (!items.length) throw new Error(`upstream returned no image data (model ${opts.model ?? "default"})`);
|
|
213
215
|
fs.mkdirSync(opts.outDir, { recursive: true });
|
|
214
216
|
const paths: string[] = [];
|
|
217
|
+
const urls: string[] = [];
|
|
215
218
|
for (let i = 0; i < items.length; i++) {
|
|
216
219
|
const item = items[i];
|
|
217
220
|
if (typeof item?.b64_json === "string" && item.b64_json) {
|
|
218
221
|
paths.push(writeB64(opts.outDir, item.b64_json, i));
|
|
219
222
|
} else if (typeof item?.url === "string" && item.url) {
|
|
220
|
-
|
|
223
|
+
// A failed download must not waste the generation: surface the URL.
|
|
224
|
+
try {
|
|
225
|
+
paths.push(await downloadImage(fetchImpl, item.url, opts.outDir, i, opts.signal, opts.timeoutMs));
|
|
226
|
+
} catch (err) {
|
|
227
|
+
urls.push(item.url);
|
|
228
|
+
void err;
|
|
229
|
+
}
|
|
221
230
|
} else {
|
|
222
231
|
throw new Error(`image item ${i} had neither b64_json nor url`);
|
|
223
232
|
}
|
|
224
233
|
}
|
|
225
|
-
|
|
234
|
+
if (!paths.length && !urls.length) throw new Error(`upstream returned no image data (model ${opts.model ?? "default"})`);
|
|
235
|
+
return { paths, urls, model: typeof payload?.model === "string" ? payload.model : opts.model };
|
|
226
236
|
}
|
|
227
237
|
|
|
228
238
|
function writeB64(outDir: string, b64: string, i: number): string {
|
|
@@ -267,6 +277,7 @@ export interface ImageChainResult {
|
|
|
267
277
|
provider: ImageProvider;
|
|
268
278
|
model?: string;
|
|
269
279
|
paths: string[];
|
|
280
|
+
urls: string[];
|
|
270
281
|
attempts: string[];
|
|
271
282
|
}
|
|
272
283
|
|
|
@@ -297,6 +308,13 @@ export async function generateImageWithFallback(params: ImageChainParams): Promi
|
|
|
297
308
|
const chain = chainFor(params.provider);
|
|
298
309
|
const attempts: string[] = [];
|
|
299
310
|
for (const provider of chain) {
|
|
311
|
+
// Cancelled calls skip fallback entirely — before any provider client
|
|
312
|
+
// construction or fetch invocation.
|
|
313
|
+
if (params.signal?.aborted) {
|
|
314
|
+
const abortErr = new Error("web_image aborted");
|
|
315
|
+
abortErr.name = "AbortError";
|
|
316
|
+
throw abortErr;
|
|
317
|
+
}
|
|
300
318
|
const configured =
|
|
301
319
|
provider === "gemini" ? true : provider === "zai" ? Boolean(params.apiConfig.zai) : Boolean(params.apiConfig.custom);
|
|
302
320
|
if (!configured) {
|
|
@@ -309,7 +327,7 @@ export async function generateImageWithFallback(params: ImageChainParams): Promi
|
|
|
309
327
|
continue;
|
|
310
328
|
}
|
|
311
329
|
try {
|
|
312
|
-
let result: { paths: string[]; model?: string };
|
|
330
|
+
let result: { paths: string[]; urls?: string[]; model?: string };
|
|
313
331
|
if (provider === "gemini") {
|
|
314
332
|
result = await geminiGenerateImage(params.prompt, {
|
|
315
333
|
config: params.geminiConfig,
|
|
@@ -345,8 +363,24 @@ export async function generateImageWithFallback(params: ImageChainParams): Promi
|
|
|
345
363
|
});
|
|
346
364
|
}
|
|
347
365
|
imageRateRecord(provider);
|
|
348
|
-
|
|
366
|
+
if (provider === "gemini" && params.n && params.n > 1 && result.paths.length < params.n) {
|
|
367
|
+
attempts.push(`gemini: n=${params.n} requested — the gemini web tier returns its own image count (${result.paths.length}); n applies to zai/custom`);
|
|
368
|
+
}
|
|
369
|
+
return { provider, model: result.model, paths: result.paths, urls: result.urls ?? [], attempts };
|
|
349
370
|
} catch (err) {
|
|
371
|
+
// Cancellation is not a provider failure: rethrow so aborted tool calls
|
|
372
|
+
// surface as AbortError instead of an "all providers failed" listing —
|
|
373
|
+
// even when a genuine provider error (AuthError, a failed save, …) was
|
|
374
|
+
// the error in flight when the abort landed.
|
|
375
|
+
if (params.signal?.aborted) {
|
|
376
|
+
if ((err as Error)?.name === "AbortError") throw err;
|
|
377
|
+
// cause keeps the in-flight provider error for diagnostics.
|
|
378
|
+
const abortErr = new Error("web_image aborted", { cause: err });
|
|
379
|
+
abortErr.name = "AbortError";
|
|
380
|
+
throw abortErr;
|
|
381
|
+
}
|
|
382
|
+
// A foreign AbortError-named error (not from the caller's signal) is a
|
|
383
|
+
// provider failure like any other — record it and keep the chain going.
|
|
350
384
|
attempts.push(`${provider}: ${provider === "gemini" ? describeGeminiError(err) : describeImageApiError(err)}`);
|
|
351
385
|
}
|
|
352
386
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bacnh85/pi-web",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"description": "Pi extension for web search, page extraction, Firecrawl scraping/crawling, Crawl4AI headless browser crawling, Gemini web-tier research, and free upstream image generation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/skills/pi-web/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pi-web
|
|
3
|
-
description: Web search, content extraction, site crawling, page capture, Gemini web-tier research, and free upstream image generation via the pi-web extension. Use when the user needs current web search results, documentation lookup, factual research, AI-synthesized research with sources (Gemini Deep Research), image generation from text (Gemini web, Z.ai
|
|
3
|
+
description: Web search, content extraction, site crawling, page capture, Gemini web-tier research, and free upstream image generation via the pi-web extension. Use when the user needs current web search results, documentation lookup, factual research, AI-synthesized research with sources (Gemini Deep Research), image generation from text (Gemini web, Z.ai GLM-Image), source discovery, URL-to-markdown extraction, JSON extraction from websites, site URL discovery, site crawling, or page screenshots/PDFs. Use when the user mentions searching the web, finding docs, looking something up, researching deeply, generating/creating an image, scraping/extracting content from a URL, or capturing a page.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# pi-web — Unified Web Tools
|
|
@@ -18,7 +18,7 @@ Use the **9 unified tools** from the `pi-web` extension for all web-related task
|
|
|
18
18
|
| `web_screenshot` | Capture page screenshot as PNG | Crawl4AI daemon (public URLs) or local headless Chrome (localhost/LAN/file URLs — auto-detected) |
|
|
19
19
|
| `web_pdf` | Generate page PDF | Crawl4AI daemon (public URLs) or local headless Chrome (localhost/LAN/file URLs — auto-detected) |
|
|
20
20
|
| `web_research` | AI-synthesized research with sources | Gemini web tier: ask = grounded answer (guest OK); research = Deep Research report (cookie + Gemini Advanced) |
|
|
21
|
-
| `web_image` | Generate images from a text prompt | Gemini web (guest/cookie) → Z.ai
|
|
21
|
+
| `web_image` | Generate images from a text prompt | Gemini web (guest/cookie) → Z.ai GLM-Image (`ZAI_API_KEY`) → custom OpenAI-images endpoint |
|
|
22
22
|
| `web_status` | Check provider configuration and health | — |
|
|
23
23
|
|
|
24
24
|
## Decision Tree
|
|
@@ -62,8 +62,8 @@ What do you need?
|
|
|
62
62
|
│
|
|
63
63
|
├── Generate an image from a text prompt (NOT capturing an existing page)
|
|
64
64
|
│ → web_image
|
|
65
|
-
│ ├─ default: provider=auto (Gemini web → Z.ai
|
|
66
|
-
│ └─ pin/model: provider=zai model=
|
|
65
|
+
│ ├─ default: provider=auto (Gemini web → Z.ai GLM-Image → custom endpoint)
|
|
66
|
+
│ └─ pin/model: provider=zai model=glm-image, or any custom OpenAI-images endpoint
|
|
67
67
|
│
|
|
68
68
|
└── Check what web tools are configured
|
|
69
69
|
→ web_status
|