@oh-my-pi/pi-coding-agent 15.10.6 → 15.10.8
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 +21 -1
- package/dist/types/config/model-registry.d.ts +4 -2
- package/dist/types/config/model-resolver.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +9 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +3 -1
- package/dist/types/mcp/oauth-discovery.d.ts +4 -1
- package/dist/types/mcp/oauth-flow.d.ts +6 -1
- package/dist/types/tools/fetch.d.ts +2 -1
- package/dist/types/tools/index.d.ts +3 -1
- package/dist/types/tools/report-tool-issue.d.ts +5 -0
- package/dist/types/web/kagi.d.ts +2 -1
- package/dist/types/web/parallel.d.ts +3 -0
- package/dist/types/web/search/providers/anthropic.d.ts +2 -1
- package/dist/types/web/search/providers/base.d.ts +2 -1
- package/dist/types/web/search/providers/brave.d.ts +2 -1
- package/dist/types/web/search/providers/codex.d.ts +2 -1
- package/dist/types/web/search/providers/exa.d.ts +2 -1
- package/dist/types/web/search/providers/gemini.d.ts +2 -1
- package/dist/types/web/search/providers/jina.d.ts +7 -2
- package/dist/types/web/search/providers/kagi.d.ts +7 -2
- package/dist/types/web/search/providers/kimi.d.ts +7 -2
- package/dist/types/web/search/providers/parallel.d.ts +2 -1
- package/dist/types/web/search/providers/perplexity.d.ts +2 -1
- package/dist/types/web/search/providers/searxng.d.ts +2 -1
- package/dist/types/web/search/providers/synthetic.d.ts +7 -3
- package/dist/types/web/search/providers/tavily.d.ts +2 -1
- package/dist/types/web/search/providers/zai.d.ts +2 -1
- package/package.json +9 -9
- package/src/config/model-registry.ts +13 -7
- package/src/config/model-resolver.ts +57 -2
- package/src/config/settings-schema.ts +6 -0
- package/src/extensibility/custom-tools/types.ts +3 -1
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/mcp/manager.ts +17 -16
- package/src/mcp/oauth-discovery.ts +8 -3
- package/src/mcp/oauth-flow.ts +12 -5
- package/src/modes/components/assistant-message.ts +28 -6
- package/src/prompts/tools/read.md +0 -1
- package/src/tools/fetch.ts +22 -5
- package/src/tools/image-gen.ts +33 -11
- package/src/tools/index.ts +4 -2
- package/src/tools/report-tool-issue.ts +7 -1
- package/src/web/kagi.ts +5 -2
- package/src/web/parallel.ts +7 -3
- package/src/web/search/providers/anthropic.ts +5 -1
- package/src/web/search/providers/base.ts +2 -1
- package/src/web/search/providers/brave.ts +5 -2
- package/src/web/search/providers/codex.ts +6 -2
- package/src/web/search/providers/exa.ts +91 -8
- package/src/web/search/providers/gemini.ts +6 -0
- package/src/web/search/providers/jina.ts +15 -5
- package/src/web/search/providers/kagi.ts +9 -2
- package/src/web/search/providers/kimi.ts +18 -4
- package/src/web/search/providers/parallel.ts +6 -2
- package/src/web/search/providers/perplexity.ts +7 -4
- package/src/web/search/providers/searxng.ts +6 -2
- package/src/web/search/providers/synthetic.ts +9 -5
- package/src/web/search/providers/tavily.ts +4 -2
- package/src/web/search/providers/zai.ts +15 -4
package/src/mcp/oauth-flow.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import type { OAuthCallbackFlowOptions } from "@oh-my-pi/pi-ai/oauth/callback-server";
|
|
9
9
|
import { OAuthCallbackFlow } from "@oh-my-pi/pi-ai/oauth/callback-server";
|
|
10
10
|
import type { OAuthController, OAuthCredentials } from "@oh-my-pi/pi-ai/oauth/types";
|
|
11
|
+
import type { FetchImpl } from "@oh-my-pi/pi-ai/types";
|
|
11
12
|
|
|
12
13
|
const DEFAULT_PORT = 3000;
|
|
13
14
|
const CALLBACK_PATH = "/callback";
|
|
@@ -114,6 +115,8 @@ export interface MCPOAuthConfig {
|
|
|
114
115
|
callbackPort?: number;
|
|
115
116
|
/** Custom callback path (default: /callback or redirectUri pathname) */
|
|
116
117
|
callbackPath?: string;
|
|
118
|
+
/** Fetch implementation for token exchange and discovery requests. */
|
|
119
|
+
fetch?: FetchImpl;
|
|
117
120
|
}
|
|
118
121
|
|
|
119
122
|
/**
|
|
@@ -124,6 +127,7 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
124
127
|
#resolvedClientId?: string;
|
|
125
128
|
#registeredClientSecret?: string;
|
|
126
129
|
#codeVerifier?: string;
|
|
130
|
+
#fetch: FetchImpl;
|
|
127
131
|
|
|
128
132
|
constructor(
|
|
129
133
|
private config: MCPOAuthConfig,
|
|
@@ -131,6 +135,7 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
131
135
|
) {
|
|
132
136
|
super(ctrl, resolveCallbackOptions(config));
|
|
133
137
|
this.#resolvedClientId = this.#resolveClientId(config);
|
|
138
|
+
this.#fetch = config.fetch ?? ctrl.fetch ?? fetch;
|
|
134
139
|
}
|
|
135
140
|
|
|
136
141
|
/**
|
|
@@ -212,7 +217,7 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
212
217
|
params.set("client_secret", clientSecret);
|
|
213
218
|
}
|
|
214
219
|
|
|
215
|
-
const response = await fetch(this.config.tokenUrl, {
|
|
220
|
+
const response = await this.#fetch(this.config.tokenUrl, {
|
|
216
221
|
method: "POST",
|
|
217
222
|
headers: {
|
|
218
223
|
"Content-Type": "application/x-www-form-urlencoded",
|
|
@@ -289,7 +294,7 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
289
294
|
if (!registrationEndpoint) return;
|
|
290
295
|
|
|
291
296
|
try {
|
|
292
|
-
const response = await fetch(registrationEndpoint, {
|
|
297
|
+
const response = await this.#fetch(registrationEndpoint, {
|
|
293
298
|
method: "POST",
|
|
294
299
|
headers: {
|
|
295
300
|
"Content-Type": "application/json",
|
|
@@ -357,7 +362,7 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
357
362
|
|
|
358
363
|
async #tryWellKnownForRegistration(wellKnownUrl: string): Promise<string | null> {
|
|
359
364
|
try {
|
|
360
|
-
const response = await fetch(wellKnownUrl, {
|
|
365
|
+
const response = await this.#fetch(wellKnownUrl, {
|
|
361
366
|
method: "GET",
|
|
362
367
|
headers: { Accept: "application/json" },
|
|
363
368
|
});
|
|
@@ -374,7 +379,7 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
|
|
|
374
379
|
|
|
375
380
|
async #assertClientIdNotRequired(authorizationUrl: string): Promise<void> {
|
|
376
381
|
try {
|
|
377
|
-
const response = await fetch(authorizationUrl, {
|
|
382
|
+
const response = await this.#fetch(authorizationUrl, {
|
|
378
383
|
method: "GET",
|
|
379
384
|
redirect: "manual",
|
|
380
385
|
headers: { Accept: "text/plain,text/html,application/json" },
|
|
@@ -402,7 +407,9 @@ export async function refreshMCPOAuthToken(
|
|
|
402
407
|
refreshToken: string,
|
|
403
408
|
clientId?: string,
|
|
404
409
|
clientSecret?: string,
|
|
410
|
+
opts?: { fetch?: FetchImpl },
|
|
405
411
|
): Promise<OAuthCredentials> {
|
|
412
|
+
const fetchImpl: FetchImpl = opts?.fetch ?? fetch;
|
|
406
413
|
const params = new URLSearchParams({
|
|
407
414
|
grant_type: "refresh_token",
|
|
408
415
|
refresh_token: refreshToken,
|
|
@@ -410,7 +417,7 @@ export async function refreshMCPOAuthToken(
|
|
|
410
417
|
if (clientId) params.set("client_id", clientId);
|
|
411
418
|
if (clientSecret) params.set("client_secret", clientSecret);
|
|
412
419
|
|
|
413
|
-
const response = await
|
|
420
|
+
const response = await fetchImpl(tokenUrl, {
|
|
414
421
|
method: "POST",
|
|
415
422
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
416
423
|
body: params.toString(),
|
|
@@ -5,7 +5,16 @@ import { settings } from "../../config/settings";
|
|
|
5
5
|
import type { AssistantThinkingRenderer } from "../../extensibility/extensions/types";
|
|
6
6
|
import { getMarkdownTheme, theme } from "../../modes/theme/theme";
|
|
7
7
|
import { resolveAbortLabel, shouldRenderAbortReason } from "../../session/messages";
|
|
8
|
-
import { resolveImageOptions } from "../../tools/render-utils";
|
|
8
|
+
import { getPreviewLines, resolveImageOptions, TRUNCATE_LENGTHS } from "../../tools/render-utils";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Max lines of a turn-ending provider error rendered inline in the transcript.
|
|
12
|
+
* Bounds pathological error bodies — e.g. a proxy 502 whose body is a full HTML
|
|
13
|
+
* page — so they can't flood the scrollback. Blank lines are dropped and each
|
|
14
|
+
* line is width-truncated by {@link getPreviewLines}. Full text is still kept in
|
|
15
|
+
* the persisted session.
|
|
16
|
+
*/
|
|
17
|
+
const MAX_TRANSCRIPT_ERROR_LINES = 8;
|
|
9
18
|
|
|
10
19
|
/**
|
|
11
20
|
* Component that renders a complete assistant message
|
|
@@ -78,6 +87,22 @@ export class AssistantMessageComponent extends Container {
|
|
|
78
87
|
this.#transcriptBlockFinalized = true;
|
|
79
88
|
}
|
|
80
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Render a turn-ending provider error inline. Drops blank lines, clamps the
|
|
92
|
+
* line count to {@link MAX_TRANSCRIPT_ERROR_LINES}, and width-truncates each
|
|
93
|
+
* line so a pathological body — e.g. the HTML page a proxy returns on a 502 —
|
|
94
|
+
* can't flood the transcript. Mirrors {@link ErrorBannerComponent}.
|
|
95
|
+
*/
|
|
96
|
+
#appendErrorBlock(message: string): void {
|
|
97
|
+
const lines = getPreviewLines(message, MAX_TRANSCRIPT_ERROR_LINES, TRUNCATE_LENGTHS.LINE);
|
|
98
|
+
if (lines.length === 0) lines.push("Unknown error");
|
|
99
|
+
this.#contentContainer.addChild(new Spacer(1));
|
|
100
|
+
this.#contentContainer.addChild(new Text(theme.fg("error", `Error: ${lines[0]}`), 1, 0));
|
|
101
|
+
for (const line of lines.slice(1)) {
|
|
102
|
+
this.#contentContainer.addChild(new Text(theme.fg("error", ` ${line}`), 1, 0));
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
81
106
|
setToolResultImages(toolCallId: string, images: ImageContent[]): void {
|
|
82
107
|
if (!toolCallId) return;
|
|
83
108
|
const validImages = images.filter(img => img.type === "image" && img.data && img.mimeType);
|
|
@@ -249,9 +274,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
249
274
|
}
|
|
250
275
|
this.#contentContainer.addChild(new Text(theme.fg("error", abortMessage), 1, 0));
|
|
251
276
|
} else if (message.stopReason === "error" && !this.#errorPinned) {
|
|
252
|
-
|
|
253
|
-
this.#contentContainer.addChild(new Spacer(1));
|
|
254
|
-
this.#contentContainer.addChild(new Text(theme.fg("error", `Error: ${errorMsg}`), 1, 0));
|
|
277
|
+
this.#appendErrorBlock(message.errorMessage || "Unknown error");
|
|
255
278
|
}
|
|
256
279
|
}
|
|
257
280
|
if (
|
|
@@ -260,8 +283,7 @@ export class AssistantMessageComponent extends Container {
|
|
|
260
283
|
message.stopReason !== "aborted" &&
|
|
261
284
|
message.stopReason !== "error"
|
|
262
285
|
) {
|
|
263
|
-
this.#
|
|
264
|
-
this.#contentContainer.addChild(new Text(theme.fg("error", `Error: ${message.errorMessage}`), 1, 0));
|
|
286
|
+
this.#appendErrorBlock(message.errorMessage);
|
|
265
287
|
}
|
|
266
288
|
|
|
267
289
|
// Token usage metadata
|
|
@@ -79,7 +79,6 @@ For `.sqlite`, `.sqlite3`, `.db`, `.db3`:
|
|
|
79
79
|
<critical>
|
|
80
80
|
- You MUST use `read` for every file, directory, archive, and URL inspection. `cat`, `head`, `tail`, `less`, `more`, `ls`, `tar`, `unzip`, `curl`, `wget` are FORBIDDEN — any such bash call is a bug, regardless of how short or convenient it looks.
|
|
81
81
|
- You MUST prefer `read` over a browser/puppeteer tool for URL content; only reach for a browser when `read` cannot deliver reasonable content.
|
|
82
|
-
- You MUST always include `path`. NEVER call `read` with `{}`.
|
|
83
82
|
- For line ranges, append the selector to `path` (`path="src/foo.ts:50-200"`, `path="src/foo.ts:50+150"`). NEVER substitute `sed -n`, `awk NR`, or `head`/`tail` pipelines.
|
|
84
83
|
- Summary footer says `read <path>:raw …`? Re-issue the exact selector it names. NEVER guess what's inside `..` / `…` markers — they carry no content.
|
|
85
84
|
- You MAY combine selectors with URL reads and internal URIs; both paginate the cached resolved output.
|
package/src/tools/fetch.ts
CHANGED
|
@@ -3,7 +3,7 @@ import * as fs from "node:fs/promises";
|
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import type { AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
6
|
-
import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
6
|
+
import type { FetchImpl, ImageContent, TextContent } from "@oh-my-pi/pi-ai";
|
|
7
7
|
import { htmlToMarkdown } from "@oh-my-pi/pi-natives";
|
|
8
8
|
import { type Component, Text } from "@oh-my-pi/pi-tui";
|
|
9
9
|
import { $which, ptree, truncate } from "@oh-my-pi/pi-utils";
|
|
@@ -637,6 +637,7 @@ export async function renderHtmlToText(
|
|
|
637
637
|
settings: Settings,
|
|
638
638
|
userSignal: AbortSignal | undefined,
|
|
639
639
|
storage: AgentStorage | null,
|
|
640
|
+
fetchOverride?: FetchImpl,
|
|
640
641
|
): Promise<{ content: string; ok: boolean; method: string }> {
|
|
641
642
|
const overallSignal = ptree.combineSignals(userSignal, timeout * 1000);
|
|
642
643
|
const execOptions = {
|
|
@@ -650,6 +651,7 @@ export async function renderHtmlToText(
|
|
|
650
651
|
// Per-attempt budget for remote endpoints so one stall cannot consume the
|
|
651
652
|
// whole reader-mode budget and starve the local fallbacks.
|
|
652
653
|
const remoteSignal = () => ptree.combineSignals(userSignal, remoteBudgetMs);
|
|
654
|
+
const fetchImpl = fetchOverride ?? fetch;
|
|
653
655
|
|
|
654
656
|
const runners: Record<FetchProvider, () => Promise<string | null>> = {
|
|
655
657
|
// Purely local, no network/subprocess: still works on already-loaded HTML
|
|
@@ -670,14 +672,20 @@ export async function renderHtmlToText(
|
|
|
670
672
|
if (!findParallelApiKey(storage)) return null;
|
|
671
673
|
const parallelResult = await extractWithParallel(
|
|
672
674
|
[url],
|
|
673
|
-
{
|
|
675
|
+
{
|
|
676
|
+
objective: "Extract the main content",
|
|
677
|
+
excerpts: true,
|
|
678
|
+
fullContent: false,
|
|
679
|
+
signal: remoteSignal(),
|
|
680
|
+
fetch: fetchImpl,
|
|
681
|
+
},
|
|
674
682
|
storage,
|
|
675
683
|
);
|
|
676
684
|
const firstDocument = parallelResult.results[0];
|
|
677
685
|
return firstDocument ? getParallelExtractContent(firstDocument) : null;
|
|
678
686
|
},
|
|
679
687
|
jina: async () => {
|
|
680
|
-
const response = await
|
|
688
|
+
const response = await fetchImpl(`https://r.jina.ai/${url}`, {
|
|
681
689
|
headers: { Accept: "text/markdown" },
|
|
682
690
|
signal: remoteSignal(),
|
|
683
691
|
});
|
|
@@ -1052,6 +1060,7 @@ async function renderUrl(
|
|
|
1052
1060
|
settings: Settings,
|
|
1053
1061
|
signal: AbortSignal | undefined,
|
|
1054
1062
|
storage: AgentStorage | null,
|
|
1063
|
+
fetchOverride?: FetchImpl,
|
|
1055
1064
|
): Promise<FetchRenderResult> {
|
|
1056
1065
|
const notes: string[] = [];
|
|
1057
1066
|
const fetchedAt = new Date().toISOString();
|
|
@@ -1425,7 +1434,15 @@ async function renderUrl(
|
|
|
1425
1434
|
}
|
|
1426
1435
|
|
|
1427
1436
|
// 5E: Render HTML via the reader-backend chain (native/trafilatura/lynx/parallel/jina)
|
|
1428
|
-
const htmlResult = await renderHtmlToText(
|
|
1437
|
+
const htmlResult = await renderHtmlToText(
|
|
1438
|
+
finalUrl,
|
|
1439
|
+
rawContent,
|
|
1440
|
+
timeout,
|
|
1441
|
+
settings,
|
|
1442
|
+
signal,
|
|
1443
|
+
storage,
|
|
1444
|
+
fetchOverride,
|
|
1445
|
+
);
|
|
1429
1446
|
if (!htmlResult.ok) {
|
|
1430
1447
|
notes.push("html rendering failed (no reader backend produced usable output)");
|
|
1431
1448
|
|
|
@@ -1626,7 +1643,7 @@ async function buildReadUrlCacheEntry(
|
|
|
1626
1643
|
}
|
|
1627
1644
|
|
|
1628
1645
|
const storage = session.settings.getStorage();
|
|
1629
|
-
const result = await renderUrl(url, effectiveTimeout, raw, session.settings, signal, storage);
|
|
1646
|
+
const result = await renderUrl(url, effectiveTimeout, raw, session.settings, signal, storage, session.fetch);
|
|
1630
1647
|
const output = buildUrlReadOutput(result, result.content);
|
|
1631
1648
|
const artifactId = options?.ensureArtifact ? await persistReadUrlArtifact(session, output) : undefined;
|
|
1632
1649
|
|
package/src/tools/image-gen.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import * as os from "node:os";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
type ApiKey,
|
|
5
|
+
type FetchImpl,
|
|
6
|
+
getAntigravityUserAgent,
|
|
7
|
+
getEnvApiKey,
|
|
8
|
+
type Model,
|
|
9
|
+
withAuth,
|
|
10
|
+
} from "@oh-my-pi/pi-ai";
|
|
4
11
|
import {
|
|
5
12
|
CODEX_BASE_URL,
|
|
6
13
|
getCodexAccountId,
|
|
@@ -366,7 +373,11 @@ function toDataUrl(image: InlineImageData): string {
|
|
|
366
373
|
return `data:${image.mimeType};base64,${image.data}`;
|
|
367
374
|
}
|
|
368
375
|
|
|
369
|
-
async function loadImageFromUrl(
|
|
376
|
+
async function loadImageFromUrl(
|
|
377
|
+
imageUrl: string,
|
|
378
|
+
fetchImpl: FetchImpl,
|
|
379
|
+
signal?: AbortSignal,
|
|
380
|
+
): Promise<InlineImageData> {
|
|
370
381
|
if (imageUrl.startsWith("data:")) {
|
|
371
382
|
const normalized = normalizeDataUrl(imageUrl.trim());
|
|
372
383
|
if (!normalized.mimeType) {
|
|
@@ -378,7 +389,7 @@ async function loadImageFromUrl(imageUrl: string, signal?: AbortSignal): Promise
|
|
|
378
389
|
return { data: normalized.data, mimeType: normalized.mimeType };
|
|
379
390
|
}
|
|
380
391
|
|
|
381
|
-
const response = await
|
|
392
|
+
const response = await fetchImpl(imageUrl, { signal });
|
|
382
393
|
if (!response.ok) {
|
|
383
394
|
const rawText = await response.text();
|
|
384
395
|
throw new Error(`Image download failed (${response.status}): ${rawText}`);
|
|
@@ -850,13 +861,14 @@ async function generateOpenAIHostedImage(
|
|
|
850
861
|
model: Model,
|
|
851
862
|
params: ImageGenParams,
|
|
852
863
|
inputImages: InlineImageData[],
|
|
864
|
+
fetchImpl: FetchImpl,
|
|
853
865
|
signal: AbortSignal | undefined,
|
|
854
866
|
sessionId: string | undefined,
|
|
855
867
|
): Promise<OpenAIHostedImageResult> {
|
|
856
868
|
const promptText = assemblePrompt(params);
|
|
857
869
|
const stream = model.api === "openai-codex-responses" || model.provider === "openai-codex";
|
|
858
870
|
const requestBody = buildOpenAIHostedImageRequest(model, promptText, params, inputImages, stream);
|
|
859
|
-
const response = await
|
|
871
|
+
const response = await fetchImpl(getOpenAIResponsesUrl(model), {
|
|
860
872
|
method: "POST",
|
|
861
873
|
headers: buildOpenAIImageHeaders(model, apiKey, sessionId),
|
|
862
874
|
body: JSON.stringify(requestBody),
|
|
@@ -1035,6 +1047,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
|
|
|
1035
1047
|
}
|
|
1036
1048
|
|
|
1037
1049
|
const requestSignal = ptree.combineSignals(signal, IMAGE_TIMEOUT);
|
|
1050
|
+
const fetchImpl = ctx.fetch ?? fetch;
|
|
1038
1051
|
|
|
1039
1052
|
if (provider === "openai" || provider === "openai-codex") {
|
|
1040
1053
|
if (!apiKey.model) {
|
|
@@ -1049,7 +1062,16 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
|
|
|
1049
1062
|
|
|
1050
1063
|
const parsed = await withAuth(
|
|
1051
1064
|
hostedKey,
|
|
1052
|
-
key =>
|
|
1065
|
+
key =>
|
|
1066
|
+
generateOpenAIHostedImage(
|
|
1067
|
+
key,
|
|
1068
|
+
hostedModel,
|
|
1069
|
+
params,
|
|
1070
|
+
resolvedImages,
|
|
1071
|
+
fetchImpl,
|
|
1072
|
+
requestSignal,
|
|
1073
|
+
sessionId,
|
|
1074
|
+
),
|
|
1053
1075
|
{ signal: requestSignal },
|
|
1054
1076
|
);
|
|
1055
1077
|
|
|
@@ -1117,7 +1139,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
|
|
|
1117
1139
|
resolvedImages,
|
|
1118
1140
|
);
|
|
1119
1141
|
|
|
1120
|
-
const resp = await
|
|
1142
|
+
const resp = await fetchImpl(`${ANTIGRAVITY_ENDPOINT}/v1internal:streamGenerateContent?alt=sse`, {
|
|
1121
1143
|
method: "POST",
|
|
1122
1144
|
headers: {
|
|
1123
1145
|
Authorization: `Bearer ${bearer}`,
|
|
@@ -1225,7 +1247,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
|
|
|
1225
1247
|
const xaiRawText = await withAuth(
|
|
1226
1248
|
xaiKey,
|
|
1227
1249
|
async key => {
|
|
1228
|
-
const resp = await
|
|
1250
|
+
const resp = await fetchImpl(`${xaiCreds.baseURL}${xaiEndpoint}`, {
|
|
1229
1251
|
method: "POST",
|
|
1230
1252
|
headers: {
|
|
1231
1253
|
Authorization: `Bearer ${key}`,
|
|
@@ -1263,7 +1285,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
|
|
|
1263
1285
|
const mimeType = parseImageMetadata(bytes)?.mimeType ?? "image/png";
|
|
1264
1286
|
xaiInlineImages.push({ data: entry.b64_json, mimeType });
|
|
1265
1287
|
} else if (entry.url) {
|
|
1266
|
-
xaiInlineImages.push(await loadImageFromUrl(entry.url, requestSignal));
|
|
1288
|
+
xaiInlineImages.push(await loadImageFromUrl(entry.url, fetchImpl, requestSignal));
|
|
1267
1289
|
}
|
|
1268
1290
|
}
|
|
1269
1291
|
|
|
@@ -1309,7 +1331,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
|
|
|
1309
1331
|
};
|
|
1310
1332
|
|
|
1311
1333
|
const rawText = await withAuth(apiKey.apiKey, async key => {
|
|
1312
|
-
const resp = await
|
|
1334
|
+
const resp = await fetchImpl("https://openrouter.ai/api/v1/chat/completions", {
|
|
1313
1335
|
method: "POST",
|
|
1314
1336
|
headers: {
|
|
1315
1337
|
"Content-Type": "application/json",
|
|
@@ -1343,7 +1365,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
|
|
|
1343
1365
|
const imageUrls = extractOpenRouterImageUrls(message);
|
|
1344
1366
|
const inlineImages: InlineImageData[] = [];
|
|
1345
1367
|
for (const imageUrl of imageUrls) {
|
|
1346
|
-
inlineImages.push(await loadImageFromUrl(imageUrl, requestSignal));
|
|
1368
|
+
inlineImages.push(await loadImageFromUrl(imageUrl, fetchImpl, requestSignal));
|
|
1347
1369
|
}
|
|
1348
1370
|
|
|
1349
1371
|
if (inlineImages.length === 0) {
|
|
@@ -1404,7 +1426,7 @@ export const imageGenTool: CustomTool<typeof imageGenSchema, ImageGenToolDetails
|
|
|
1404
1426
|
};
|
|
1405
1427
|
|
|
1406
1428
|
const rawText = await withAuth(apiKey.apiKey, async key => {
|
|
1407
|
-
const resp = await
|
|
1429
|
+
const resp = await fetchImpl(
|
|
1408
1430
|
`https://generativelanguage.googleapis.com/v1beta/models/${encodeURIComponent(model)}:generateContent`,
|
|
1409
1431
|
{
|
|
1410
1432
|
method: "POST",
|
package/src/tools/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { InMemorySnapshotStore } from "@oh-my-pi/hashline";
|
|
2
2
|
import type { AgentTelemetryConfig, AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
3
|
-
import type { ToolChoice } from "@oh-my-pi/pi-ai";
|
|
3
|
+
import type { FetchImpl, ToolChoice } from "@oh-my-pi/pi-ai";
|
|
4
4
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
5
5
|
import type { AsyncJobManager } from "../async/job-manager";
|
|
6
6
|
import type { PromptTemplate } from "../config/prompt-templates";
|
|
@@ -142,6 +142,8 @@ export interface ToolSession {
|
|
|
142
142
|
cwd: string;
|
|
143
143
|
/** Whether UI is available */
|
|
144
144
|
hasUI: boolean;
|
|
145
|
+
/** Optional fetch implementation injected into the URL read pipeline (tests, proxies). Defaults to global fetch. */
|
|
146
|
+
fetch?: FetchImpl;
|
|
145
147
|
/** Skip Python kernel availability check and warmup */
|
|
146
148
|
skipPythonPreflight?: boolean;
|
|
147
149
|
/** Pre-loaded context files (AGENTS.md, etc) */
|
|
@@ -492,7 +494,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
492
494
|
const isToolAllowed = (name: string) => {
|
|
493
495
|
if (name === "goal") return goalEnabled && goalModeActive;
|
|
494
496
|
if (name === "lsp") return enableLsp && session.settings.get("lsp.enabled");
|
|
495
|
-
if (name === "bash") return
|
|
497
|
+
if (name === "bash") return session.settings.get("bash.enabled");
|
|
496
498
|
if (name === "eval") return allowEval;
|
|
497
499
|
if (name === "debug") return session.settings.get("debug.enabled");
|
|
498
500
|
if (name === "todo") return !includeYield && session.settings.get("todo.enabled");
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import { Database } from "bun:sqlite";
|
|
23
23
|
import path from "node:path";
|
|
24
24
|
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
25
|
+
import type { FetchImpl } from "@oh-my-pi/pi-ai";
|
|
25
26
|
import { $env, $flag, getAgentDir, getInstallId, logger, VERSION } from "@oh-my-pi/pi-utils";
|
|
26
27
|
import * as z from "zod/v4";
|
|
27
28
|
import type { Settings } from "..";
|
|
@@ -260,6 +261,10 @@ export interface FlushOptions {
|
|
|
260
261
|
* future debug recipes); never set from the tool's auto-flush path.
|
|
261
262
|
*/
|
|
262
263
|
bypassConsent?: boolean;
|
|
264
|
+
/**
|
|
265
|
+
* Fetch implementation for the push POST. Defaults to global fetch.
|
|
266
|
+
*/
|
|
267
|
+
fetch?: FetchImpl;
|
|
263
268
|
/**
|
|
264
269
|
* Fires once at the start of the loop with the snapshot count of
|
|
265
270
|
* unpushed rows. Subsequent inserts won't be reflected (the count is
|
|
@@ -345,6 +350,7 @@ async function performFlush(db: Database, config: PushConfig, options: FlushOpti
|
|
|
345
350
|
const totalRow = db.prepare("SELECT COUNT(*) AS n FROM grievances WHERE pushed = 0").get() as { n: number };
|
|
346
351
|
options.onStart(totalRow.n);
|
|
347
352
|
}
|
|
353
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
348
354
|
let totalPushed = 0;
|
|
349
355
|
for (;;) {
|
|
350
356
|
const rows = selectStmt.all(FLUSH_BATCH_SIZE) as GrievanceRow[];
|
|
@@ -366,7 +372,7 @@ async function performFlush(db: Database, config: PushConfig, options: FlushOpti
|
|
|
366
372
|
|
|
367
373
|
let response: Response;
|
|
368
374
|
try {
|
|
369
|
-
response = await
|
|
375
|
+
response = await fetchImpl(config.endpoint, {
|
|
370
376
|
method: "POST",
|
|
371
377
|
headers,
|
|
372
378
|
body,
|
package/src/web/kagi.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* through the shared {@link AuthStorage} broker (Bearer token), and responses
|
|
7
7
|
* are categorized result buckets rather than the legacy flat object array.
|
|
8
8
|
*/
|
|
9
|
-
import type { AuthStorage } from "@oh-my-pi/pi-ai";
|
|
9
|
+
import type { AuthStorage, FetchImpl } from "@oh-my-pi/pi-ai";
|
|
10
10
|
import { withHardTimeout } from "./search/providers/utils";
|
|
11
11
|
|
|
12
12
|
const KAGI_SEARCH_URL = "https://kagi.com/api/v1/search";
|
|
@@ -156,6 +156,7 @@ export interface KagiSearchOptions {
|
|
|
156
156
|
recency?: "day" | "week" | "month" | "year";
|
|
157
157
|
sessionId?: string;
|
|
158
158
|
signal?: AbortSignal;
|
|
159
|
+
fetch?: FetchImpl;
|
|
159
160
|
}
|
|
160
161
|
|
|
161
162
|
export interface KagiSearchSource {
|
|
@@ -251,7 +252,9 @@ export async function searchWithKagi(
|
|
|
251
252
|
throw new KagiApiError("Kagi credentials not found. Set KAGI_API_KEY or login with 'omp /login kagi'.");
|
|
252
253
|
}
|
|
253
254
|
|
|
254
|
-
const
|
|
255
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
256
|
+
|
|
257
|
+
const response = await fetchImpl(KAGI_SEARCH_URL, {
|
|
255
258
|
method: "POST",
|
|
256
259
|
headers: {
|
|
257
260
|
Authorization: `Bearer ${apiKey}`,
|
package/src/web/parallel.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getEnvApiKey } from "@oh-my-pi/pi-ai";
|
|
1
|
+
import { type FetchImpl, getEnvApiKey } from "@oh-my-pi/pi-ai";
|
|
2
2
|
import type { AgentStorage } from "../session/agent-storage";
|
|
3
3
|
import { findCredential, withHardTimeout } from "./search/providers/utils";
|
|
4
4
|
|
|
@@ -54,6 +54,7 @@ export interface ParallelSearchOptions {
|
|
|
54
54
|
mode?: "fast" | "research";
|
|
55
55
|
maxCharsPerResult?: number;
|
|
56
56
|
signal?: AbortSignal;
|
|
57
|
+
fetch?: FetchImpl;
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
export interface ParallelExtractOptions {
|
|
@@ -62,6 +63,7 @@ export interface ParallelExtractOptions {
|
|
|
62
63
|
excerpts?: boolean;
|
|
63
64
|
fullContent?: boolean;
|
|
64
65
|
signal?: AbortSignal;
|
|
66
|
+
fetch?: FetchImpl;
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
export class ParallelApiError extends Error {
|
|
@@ -295,7 +297,8 @@ export async function searchWithParallel(
|
|
|
295
297
|
);
|
|
296
298
|
}
|
|
297
299
|
|
|
298
|
-
const
|
|
300
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
301
|
+
const response = await fetchImpl(PARALLEL_SEARCH_URL, {
|
|
299
302
|
method: "POST",
|
|
300
303
|
headers: getAuthHeaders(apiKey),
|
|
301
304
|
body: JSON.stringify({
|
|
@@ -328,7 +331,8 @@ export async function extractWithParallel(
|
|
|
328
331
|
);
|
|
329
332
|
}
|
|
330
333
|
|
|
331
|
-
const
|
|
334
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
335
|
+
const response = await fetchImpl(PARALLEL_EXTRACT_URL, {
|
|
332
336
|
method: "POST",
|
|
333
337
|
headers: getAuthHeaders(apiKey),
|
|
334
338
|
body: JSON.stringify({
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
buildAnthropicSearchHeaders,
|
|
14
14
|
buildAnthropicSystemBlocks,
|
|
15
15
|
buildAnthropicUrl,
|
|
16
|
+
type FetchImpl,
|
|
16
17
|
stripClaudeToolPrefix,
|
|
17
18
|
withAuth,
|
|
18
19
|
} from "@oh-my-pi/pi-ai";
|
|
@@ -40,6 +41,7 @@ export interface AnthropicSearchParams {
|
|
|
40
41
|
max_tokens?: number;
|
|
41
42
|
temperature?: number;
|
|
42
43
|
signal?: AbortSignal;
|
|
44
|
+
fetch?: FetchImpl;
|
|
43
45
|
}
|
|
44
46
|
|
|
45
47
|
/**
|
|
@@ -89,6 +91,7 @@ async function callSearch(
|
|
|
89
91
|
maxTokens?: number,
|
|
90
92
|
temperature?: number,
|
|
91
93
|
signal?: AbortSignal,
|
|
94
|
+
fetchImpl: FetchImpl = fetch,
|
|
92
95
|
): Promise<AnthropicApiResponse> {
|
|
93
96
|
const url = buildAnthropicUrl(auth);
|
|
94
97
|
const headers = buildAnthropicSearchHeaders(auth);
|
|
@@ -115,7 +118,7 @@ async function callSearch(
|
|
|
115
118
|
body.system = systemBlocks;
|
|
116
119
|
}
|
|
117
120
|
|
|
118
|
-
const response = await
|
|
121
|
+
const response = await fetchImpl(url, {
|
|
119
122
|
method: "POST",
|
|
120
123
|
headers,
|
|
121
124
|
body: JSON.stringify(body),
|
|
@@ -275,6 +278,7 @@ export async function searchAnthropic(
|
|
|
275
278
|
maxTokens,
|
|
276
279
|
params.temperature,
|
|
277
280
|
params.signal,
|
|
281
|
+
params.fetch,
|
|
278
282
|
),
|
|
279
283
|
{
|
|
280
284
|
signal: params.signal,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AuthStorage } from "@oh-my-pi/pi-ai";
|
|
1
|
+
import type { AuthStorage, FetchImpl } from "@oh-my-pi/pi-ai";
|
|
2
2
|
import type { SearchProviderId, SearchResponse } from "../types";
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -30,6 +30,7 @@ export interface SearchParams {
|
|
|
30
30
|
recency?: "day" | "week" | "month" | "year";
|
|
31
31
|
systemPrompt: string;
|
|
32
32
|
signal?: AbortSignal;
|
|
33
|
+
fetch?: FetchImpl;
|
|
33
34
|
maxOutputTokens?: number;
|
|
34
35
|
numSearchResults?: number;
|
|
35
36
|
temperature?: number;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Calls Brave's web search REST API and maps results into the unified
|
|
5
5
|
* SearchResponse shape used by the web search tool.
|
|
6
6
|
*/
|
|
7
|
-
import { type AuthStorage, getEnvApiKey } from "@oh-my-pi/pi-ai";
|
|
7
|
+
import { type AuthStorage, type FetchImpl, getEnvApiKey } from "@oh-my-pi/pi-ai";
|
|
8
8
|
import type { SearchResponse, SearchSource } from "../../../web/search/types";
|
|
9
9
|
import { SearchProviderError } from "../../../web/search/types";
|
|
10
10
|
import { clampNumResults, dateToAgeSeconds } from "../utils";
|
|
@@ -28,6 +28,7 @@ export interface BraveSearchParams {
|
|
|
28
28
|
num_results?: number;
|
|
29
29
|
recency?: "day" | "week" | "month" | "year";
|
|
30
30
|
signal?: AbortSignal;
|
|
31
|
+
fetch?: FetchImpl;
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
interface BraveSearchResult {
|
|
@@ -80,7 +81,8 @@ async function callBraveSearch(
|
|
|
80
81
|
url.searchParams.set("freshness", RECENCY_MAP[params.recency]);
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
const
|
|
84
|
+
const fetchImpl = params.fetch ?? fetch;
|
|
85
|
+
const response = await fetchImpl(url, {
|
|
84
86
|
headers: {
|
|
85
87
|
Accept: "application/json",
|
|
86
88
|
"X-Subscription-Token": apiKey,
|
|
@@ -144,6 +146,7 @@ export class BraveProvider extends SearchProvider {
|
|
|
144
146
|
num_results: params.numSearchResults ?? params.limit,
|
|
145
147
|
recency: params.recency,
|
|
146
148
|
signal: params.signal,
|
|
149
|
+
fetch: params.fetch,
|
|
147
150
|
});
|
|
148
151
|
}
|
|
149
152
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* SQLite store, never POSTs the broker sentinel to an OpenAI token endpoint.
|
|
8
8
|
*/
|
|
9
9
|
import * as os from "node:os";
|
|
10
|
-
import { type AuthStorage, getBundledModels } from "@oh-my-pi/pi-ai";
|
|
10
|
+
import { type AuthStorage, type FetchImpl, getBundledModels } from "@oh-my-pi/pi-ai";
|
|
11
11
|
import { decodeJwt } from "@oh-my-pi/pi-ai/oauth/openai-codex";
|
|
12
12
|
import { $env, readSseJson } from "@oh-my-pi/pi-utils";
|
|
13
13
|
import packageJson from "../../../../package.json" with { type: "json" };
|
|
@@ -66,6 +66,7 @@ function shouldRetryWithNextDefaultModel(error: unknown): boolean {
|
|
|
66
66
|
|
|
67
67
|
export interface CodexSearchParams {
|
|
68
68
|
signal?: AbortSignal;
|
|
69
|
+
fetch?: FetchImpl;
|
|
69
70
|
query: string;
|
|
70
71
|
system_prompt?: string;
|
|
71
72
|
num_results?: number;
|
|
@@ -322,6 +323,7 @@ async function callCodexSearch(
|
|
|
322
323
|
systemPrompt?: string;
|
|
323
324
|
searchContextSize?: "low" | "medium" | "high";
|
|
324
325
|
modelId: string;
|
|
326
|
+
fetch?: FetchImpl;
|
|
325
327
|
},
|
|
326
328
|
): Promise<{
|
|
327
329
|
answer: string;
|
|
@@ -356,7 +358,8 @@ async function callCodexSearch(
|
|
|
356
358
|
instructions: options.systemPrompt ?? DEFAULT_INSTRUCTIONS,
|
|
357
359
|
};
|
|
358
360
|
|
|
359
|
-
const
|
|
361
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
362
|
+
const response = await fetchImpl(url, {
|
|
360
363
|
method: "POST",
|
|
361
364
|
headers,
|
|
362
365
|
body: JSON.stringify(body),
|
|
@@ -522,6 +525,7 @@ export async function searchCodex(params: SearchParams): Promise<SearchResponse>
|
|
|
522
525
|
systemPrompt: params.systemPrompt,
|
|
523
526
|
searchContextSize: "high",
|
|
524
527
|
modelId,
|
|
528
|
+
fetch: params.fetch,
|
|
525
529
|
});
|
|
526
530
|
break;
|
|
527
531
|
} catch (error) {
|