@bacnh85/pi-web 0.9.2 → 0.10.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/CHANGELOG.md +15 -0
- package/README.md +25 -1
- package/extensions/index.ts +60 -0
- package/extensions/lib/chatapi.ts +100 -0
- package/package.json +2 -2
- package/skills/pi-web/SKILL.md +6 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.10.0 (2026-09-13)
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **`web_chat` tool** — one-off chat completion against any OpenAI-compatible
|
|
8
|
+
gateway (`WEB_CHAT_API_BASE_URL` + optional `WEB_CHAT_API_KEY`): a ChatGPT
|
|
9
|
+
web bridge, official OpenAI (`https://api.openai.com/v1`), or any web2api
|
|
10
|
+
gateway. Non-streaming `POST /chat/completions` with optional `model`/
|
|
11
|
+
`system`; timeout capped at 300 s. Error mapping: 401/403 key hint,
|
|
12
|
+
429 quota, 502 → "empty account pool" hint, 5xx server error.
|
|
13
|
+
- `web_status` now reports `webChat` (configured/baseUrl/keyFound/source).
|
|
14
|
+
|
|
15
|
+
Supersedes the removed `pi-chatgpt-web` package (free web-chat-only models):
|
|
16
|
+
point `WEB_CHAT_API_BASE_URL` at the same gateway to keep using it.
|
|
17
|
+
|
|
3
18
|
## 0.9.2 (2026-09-13)
|
|
4
19
|
|
|
5
20
|
### Fixed
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @bacnh85/pi-web
|
|
2
2
|
|
|
3
|
-
Pi extension for **unified web search, content extraction, site crawling, page capture, Gemini web-tier research,
|
|
3
|
+
Pi extension for **unified web search, content extraction, site crawling, page capture, Gemini web-tier research, free upstream image generation, and one-off gateway chat**.
|
|
4
4
|
|
|
5
5
|
Auto-selects the best backend from SearXNG (self-hosted), Brave Search, Firecrawl, Crawl4AI, and agy (Gemini/Claude, when installed) — so agents don't have to know which backend to use. Search selection is adaptive: broad discovery prefers self-hosted SearXNG, while precision-sensitive searches and inline content prefer Brave. `web_research` adds AI-synthesized research with citations via your gemini.google.com session.
|
|
6
6
|
|
|
@@ -38,6 +38,8 @@ Variables:
|
|
|
38
38
|
| `WEB_IMAGE_API_LABEL` | No | — | Display label for the `custom` endpoint (default: host name) |
|
|
39
39
|
| `WEB_IMAGE_MIN_INTERVAL_MS` | No | `5000` | Min interval between `web_image` calls per provider |
|
|
40
40
|
| `WEB_IMAGE_DAILY_CAP` | No | `20` | Daily soft cap for the Gemini **web tier** `web_image` provider (keyed APIs stay uncapped) |
|
|
41
|
+
| `WEB_CHAT_API_BASE_URL` | No | — | `web_chat`: any OpenAI-compatible `/chat/completions` gateway (a ChatGPT web bridge, `https://api.openai.com/v1`, …) |
|
|
42
|
+
| `WEB_CHAT_API_KEY` | No | — | Bearer key for the `web_chat` gateway |
|
|
41
43
|
|
|
42
44
|
> (1) At least one search backend (SearXNG, Brave, or Firecrawl) must be configured for `web_search`.
|
|
43
45
|
> (2) Required for hosted Firecrawl; optional for self-hosted instances without auth.
|
|
@@ -309,6 +311,28 @@ non-human means", Z.ai alike). This tool therefore follows a risk ladder:
|
|
|
309
311
|
Smoke test: `npx tsx extensions/scripts/gemini-smoke.ts "a red cube on white background" image`
|
|
310
312
|
(or `… zai` for the Z.ai path).
|
|
311
313
|
|
|
314
|
+
### `web_chat` — one-off gateway chat
|
|
315
|
+
|
|
316
|
+
Single non-streaming chat completion against any OpenAI-compatible gateway —
|
|
317
|
+
the in-session equivalent of "ask another model quickly" without switching
|
|
318
|
+
your main provider:
|
|
319
|
+
|
|
320
|
+
```
|
|
321
|
+
web_chat(prompt="In one sentence: why is idempotency key needed here?")
|
|
322
|
+
web_chat(prompt="Summarize", model="gpt-5.3-mini", system="Be terse")
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
Configure once in `~/.pi/agent/.env.local`, then restart pi:
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
WEB_CHAT_API_BASE_URL=https://api.openai.com/v1 # or any OpenAI-compatible gateway
|
|
329
|
+
WEB_CHAT_API_KEY=sk-... # if the gateway needs a key
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
`web_status.webChat` shows configuration without printing secrets. Chat-only
|
|
333
|
+
by design (no tool calling through gateways); for grounded research with
|
|
334
|
+
sources use `web_research`, and `/model` switches your main model.
|
|
335
|
+
|
|
312
336
|
## Library structure
|
|
313
337
|
|
|
314
338
|
| Module | Contents |
|
package/extensions/index.ts
CHANGED
|
@@ -38,6 +38,11 @@ import {
|
|
|
38
38
|
imageRateSnapshot,
|
|
39
39
|
type ImageProvider,
|
|
40
40
|
} from "./lib/imageapi";
|
|
41
|
+
import {
|
|
42
|
+
chatgptChat,
|
|
43
|
+
describeChatApiError,
|
|
44
|
+
loadChatConfig,
|
|
45
|
+
} from "./lib/chatapi";
|
|
41
46
|
import { extractWithDiagnostics, type ExtractMode } from "./lib/extract";
|
|
42
47
|
import { firecrawlRequest, type FirecrawlResult } from "./lib/firecrawl";
|
|
43
48
|
import {
|
|
@@ -109,6 +114,7 @@ const WEB_ROUTING_GUIDANCE = `## Web Tool Routing (pi-web)
|
|
|
109
114
|
- **web_screenshot** / **web_pdf** — page capture (Crawl4AI).
|
|
110
115
|
- **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
116
|
- **web_image** — text→image generation via free upstreams (auto: Gemini web → Z.ai GLM-Image → custom OpenAI-images endpoint; \`model\`/\`n\` params).
|
|
117
|
+
- **web_chat** — one-off chat completion via an OpenAI-compatible gateway (\`WEB_CHAT_API_BASE_URL\`; non-streaming).
|
|
112
118
|
- **web_status** — provider config + health.
|
|
113
119
|
|
|
114
120
|
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.`;
|
|
@@ -584,6 +590,51 @@ export default function piWebExtension(pi: ExtensionAPI) {
|
|
|
584
590
|
},
|
|
585
591
|
});
|
|
586
592
|
|
|
593
|
+
// ── web_chat ─────────────────────────────────────────────────────────
|
|
594
|
+
pi.registerTool({
|
|
595
|
+
name: "web_chat",
|
|
596
|
+
label: "Web Chat (gateway)",
|
|
597
|
+
description:
|
|
598
|
+
"One-off chat completion via an OpenAI-compatible gateway (WEB_CHAT_API_BASE_URL — a ChatGPT web bridge, official OpenAI, or any web2api gateway). Non-streaming Q&A; not a provider — use /model to switch your main model.",
|
|
599
|
+
promptSnippet: "One-off chat via OpenAI-compatible gateway",
|
|
600
|
+
promptGuidelines: [
|
|
601
|
+
"Use for a quick one-off second opinion, classification, or short generation call. Grounded research with sources → web_research; switching your main chat model → /model.",
|
|
602
|
+
],
|
|
603
|
+
parameters: Type.Object({
|
|
604
|
+
prompt: Type.String({ description: "The question or instruction." }),
|
|
605
|
+
model: Type.Optional(Type.String({ description: "Gateway model id (e.g. gpt-5.3-mini). Omit for the gateway default." })),
|
|
606
|
+
system: Type.Optional(Type.String({ description: "Optional system prompt." })),
|
|
607
|
+
...sharedControlSchema,
|
|
608
|
+
}),
|
|
609
|
+
async execute(_id: string, params: Record<string, unknown>, signal: AbortSignal, _onUpdate: unknown, ctx: any) {
|
|
610
|
+
const cwd = cwdFromContext(ctx);
|
|
611
|
+
const trusted = includeProjectEnv(ctx);
|
|
612
|
+
const config = loadChatConfig(cwd, trusted);
|
|
613
|
+
if (!config) {
|
|
614
|
+
throw new Error(
|
|
615
|
+
"web_chat is not configured. Set WEB_CHAT_API_BASE_URL (and optional WEB_CHAT_API_KEY) in ~/.pi/agent/.env.local — any OpenAI-compatible /chat/completions gateway works (a ChatGPT web bridge, https://api.openai.com/v1, …) — then restart pi.",
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
const timeoutMs = Math.min(Math.max((params.timeout_ms as number) ?? 120_000, 10_000), 300_000);
|
|
619
|
+
try {
|
|
620
|
+
const result = await chatgptChat({
|
|
621
|
+
baseUrl: config.baseUrl,
|
|
622
|
+
apiKey: config.apiKey,
|
|
623
|
+
prompt: params.prompt as string,
|
|
624
|
+
model: params.model as string | undefined,
|
|
625
|
+
system: params.system as string | undefined,
|
|
626
|
+
timeoutMs,
|
|
627
|
+
signal,
|
|
628
|
+
});
|
|
629
|
+
const text = `Model: ${result.model ?? "gateway default"}\n\n${result.text}`;
|
|
630
|
+
return { content: [{ type: "text" as const, text: truncateText(text) }], details: { model: result.model } };
|
|
631
|
+
} catch (err) {
|
|
632
|
+
if ((err as Error)?.name === "AbortError") throw err;
|
|
633
|
+
throw new Error(describeChatApiError(err));
|
|
634
|
+
}
|
|
635
|
+
},
|
|
636
|
+
});
|
|
637
|
+
|
|
587
638
|
// ── web_status ───────────────────────────────────────────────────────
|
|
588
639
|
pi.registerTool({
|
|
589
640
|
name: "web_status",
|
|
@@ -639,6 +690,15 @@ export default function piWebExtension(pi: ExtensionAPI) {
|
|
|
639
690
|
: { configured: false },
|
|
640
691
|
rate: imageRateSnapshot(),
|
|
641
692
|
},
|
|
693
|
+
webChat: (() => {
|
|
694
|
+
const cfg = loadChatConfig(cwd, trusted);
|
|
695
|
+
return {
|
|
696
|
+
configured: Boolean(cfg),
|
|
697
|
+
baseUrl: cfg?.baseUrl,
|
|
698
|
+
keyFound: Boolean(cfg?.apiKey),
|
|
699
|
+
source: cfg?.source ?? "not set",
|
|
700
|
+
};
|
|
701
|
+
})(),
|
|
642
702
|
localChrome: { path: findChromeBinary() ?? "not found" },
|
|
643
703
|
};
|
|
644
704
|
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Generic OpenAI-compatible chat client for web_chat — one-off Q&A against
|
|
2
|
+
// any OpenAI-compatible /chat/completions gateway (WEB_CHAT_API_BASE_URL):
|
|
3
|
+
// a ChatGPT web bridge, official OpenAI, or any web2api gateway.
|
|
4
|
+
|
|
5
|
+
import { findEnvValue } from "./config";
|
|
6
|
+
import { raceGuard } from "./gemini";
|
|
7
|
+
import type { FetchLike } from "./imageapi";
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Config
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
export interface ChatConfig {
|
|
14
|
+
baseUrl: string;
|
|
15
|
+
apiKey?: string;
|
|
16
|
+
source: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function loadChatConfig(cwd = process.cwd(), includeCwdEnv = false): ChatConfig | null {
|
|
20
|
+
const base = findEnvValue("WEB_CHAT_API_BASE_URL", cwd, includeCwdEnv);
|
|
21
|
+
if (!base.value) return null;
|
|
22
|
+
const key = findEnvValue("WEB_CHAT_API_KEY", cwd, includeCwdEnv);
|
|
23
|
+
return {
|
|
24
|
+
baseUrl: base.value.replace(/\/+$/, ""),
|
|
25
|
+
...(key.value ? { apiKey: key.value } : {}),
|
|
26
|
+
source: base.source,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Error mapping
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
export class ChatApiError extends Error {
|
|
35
|
+
constructor(
|
|
36
|
+
public status: number,
|
|
37
|
+
message: string,
|
|
38
|
+
) {
|
|
39
|
+
super(message);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function describeChatApiError(err: unknown): string {
|
|
44
|
+
if (err instanceof ChatApiError) {
|
|
45
|
+
if (err.status === 401 || err.status === 403) return `gateway rejected the API key (HTTP ${err.status}): ${err.message}`;
|
|
46
|
+
if (err.status === 429) return `gateway rate limit/quota exhausted (HTTP 429): ${err.message}`;
|
|
47
|
+
if (err.status === 502) return `gateway returned 502 — likely an empty account pool (add an account via the gateway's admin panel): ${err.message}`;
|
|
48
|
+
if (err.status >= 500) return `gateway server error (HTTP ${err.status}): ${err.message}`;
|
|
49
|
+
return `gateway error (HTTP ${err.status}): ${err.message}`;
|
|
50
|
+
}
|
|
51
|
+
return err instanceof Error ? err.message : String(err);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Single non-streaming completion
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
export interface ChatResult {
|
|
59
|
+
text: string;
|
|
60
|
+
model?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export async function chatgptChat(opts: {
|
|
64
|
+
baseUrl: string;
|
|
65
|
+
apiKey?: string;
|
|
66
|
+
prompt: string;
|
|
67
|
+
model?: string;
|
|
68
|
+
system?: string;
|
|
69
|
+
timeoutMs?: number;
|
|
70
|
+
signal?: AbortSignal;
|
|
71
|
+
fetchImpl?: FetchLike;
|
|
72
|
+
}): Promise<ChatResult> {
|
|
73
|
+
const fetchImpl = opts.fetchImpl ?? (fetch as FetchLike);
|
|
74
|
+
const messages = [
|
|
75
|
+
...(opts.system ? [{ role: "system", content: opts.system }] : []),
|
|
76
|
+
{ role: "user", content: opts.prompt },
|
|
77
|
+
];
|
|
78
|
+
const res = await raceGuard(
|
|
79
|
+
fetchImpl(`${opts.baseUrl.replace(/\/+$/, "")}/chat/completions`, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: { "Content-Type": "application/json", ...(opts.apiKey ? { Authorization: `Bearer ${opts.apiKey}` } : {}) },
|
|
82
|
+
body: JSON.stringify({ model: opts.model, messages, stream: false }),
|
|
83
|
+
signal: opts.signal,
|
|
84
|
+
}),
|
|
85
|
+
{ signal: opts.signal, timeoutMs: opts.timeoutMs ?? 120_000, label: "web_chat" },
|
|
86
|
+
);
|
|
87
|
+
const payload = (await res.json().catch(() => null)) as Record<string, unknown> | null;
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
const errObj = payload?.error as { message?: string } | undefined;
|
|
90
|
+
const msg =
|
|
91
|
+
errObj?.message ??
|
|
92
|
+
(typeof payload?.message === "string" ? payload.message : undefined) ??
|
|
93
|
+
(payload ? JSON.stringify(payload).slice(0, 300) : res.statusText ?? "");
|
|
94
|
+
throw new ChatApiError(res.status, String(msg));
|
|
95
|
+
}
|
|
96
|
+
const choices = Array.isArray(payload?.choices) ? (payload!.choices as Array<{ message?: { content?: unknown } }>) : [];
|
|
97
|
+
const text = String(choices[0]?.message?.content ?? "");
|
|
98
|
+
if (!text) throw new Error(`gateway returned an empty completion${opts.model ? ` (model ${opts.model})` : ""}`);
|
|
99
|
+
return { text, model: typeof payload?.model === "string" ? payload.model : opts.model };
|
|
100
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bacnh85/pi-web",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Pi extension for web search, page extraction, Firecrawl scraping/crawling, Crawl4AI headless browser crawling, Gemini web-tier research,
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "Pi extension for web search, page extraction, Firecrawl scraping/crawling, Crawl4AI headless browser crawling, Gemini web-tier research, free upstream image generation, and one-off gateway chat.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"publishConfig": {
|
package/skills/pi-web/SKILL.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: pi-web
|
|
3
|
-
description: Web search, content extraction, site crawling, page capture, Gemini web-tier research,
|
|
3
|
+
description: Web search, content extraction, site crawling, page capture, Gemini web-tier research, free upstream image generation, and one-off gateway chat 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), one-off ChatGPT/gateway chat, 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, asking another model, scraping/extracting content from a URL, or capturing a page.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# pi-web — Unified Web Tools
|
|
7
7
|
|
|
8
|
-
Use the **
|
|
8
|
+
Use the **10 unified tools** from the `pi-web` extension for all web-related tasks. These tools automatically select the best backend from SearXNG, Brave Search, Firecrawl, Crawl4AI, and agy (when installed) — you don't need to know which backend to use. Search selection is adaptive: broad discovery prefers self-hosted SearXNG, while precision-sensitive queries and inline content prefer Brave.
|
|
9
9
|
|
|
10
10
|
## Quick Reference
|
|
11
11
|
|
|
@@ -19,6 +19,7 @@ Use the **9 unified tools** from the `pi-web` extension for all web-related task
|
|
|
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
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
|
+
| `web_chat` | One-off chat via an OpenAI-compatible gateway | `WEB_CHAT_API_BASE_URL` (ChatGPT web bridge, official OpenAI, …) |
|
|
22
23
|
| `web_status` | Check provider configuration and health | — |
|
|
23
24
|
|
|
24
25
|
## Decision Tree
|
|
@@ -65,6 +66,9 @@ What do you need?
|
|
|
65
66
|
│ ├─ default: provider=auto (Gemini web → Z.ai GLM-Image → custom endpoint)
|
|
66
67
|
│ └─ pin/model: provider=zai model=glm-image, or any custom OpenAI-images endpoint
|
|
67
68
|
│
|
|
69
|
+
├── One-off chat with another model (second opinion, classification)
|
|
70
|
+
│ → web_chat (WEB_CHAT_API_BASE_URL gateway; non-streaming; no tools)
|
|
71
|
+
│
|
|
68
72
|
└── Check what web tools are configured
|
|
69
73
|
→ web_status
|
|
70
74
|
```
|