@oh-my-pi/pi-coding-agent 15.10.7 → 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 +14 -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/oauth-discovery.ts +8 -3
- package/src/mcp/oauth-flow.ts +12 -5
- package/src/modes/components/assistant-message.ts +28 -6
- 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
|
@@ -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
|
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) {
|
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
* Requests per-result summaries via `contents.summary` and synthesizes
|
|
7
7
|
* them into a combined `answer` string on the SearchResponse.
|
|
8
8
|
*/
|
|
9
|
-
import { type ApiKey, type AuthStorage, getEnvApiKey, withAuth } from "@oh-my-pi/pi-ai";
|
|
9
|
+
import { type ApiKey, type AuthStorage, type FetchImpl, getEnvApiKey, withAuth } from "@oh-my-pi/pi-ai";
|
|
10
10
|
import { settings } from "../../../config/settings";
|
|
11
|
-
import {
|
|
12
|
-
|
|
11
|
+
import { findApiKey, isSearchResponse } from "../../../exa/mcp-client";
|
|
12
|
+
import { parseSSE } from "../../../mcp/json-rpc";
|
|
13
13
|
import type { SearchResponse, SearchSource } from "../../../web/search/types";
|
|
14
14
|
import { SearchProviderError } from "../../../web/search/types";
|
|
15
15
|
import { dateToAgeSeconds } from "../utils";
|
|
@@ -32,6 +32,7 @@ export interface ExaSearchParams {
|
|
|
32
32
|
start_published_date?: string;
|
|
33
33
|
end_published_date?: string;
|
|
34
34
|
signal?: AbortSignal;
|
|
35
|
+
fetch?: FetchImpl;
|
|
35
36
|
/**
|
|
36
37
|
* Credential source. Resolved before falling back to `EXA_API_KEY` so
|
|
37
38
|
* Exa works when the key is stored via the broker/auth pipeline.
|
|
@@ -62,6 +63,48 @@ function asRecord(value: unknown): Record<string, unknown> | null {
|
|
|
62
63
|
return value as Record<string, unknown>;
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
function parseJsonContent(text: string): unknown | null {
|
|
67
|
+
try {
|
|
68
|
+
return JSON.parse(text) as unknown;
|
|
69
|
+
} catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeExaMcpPayload(payload: unknown): unknown {
|
|
75
|
+
const candidates: unknown[] = [];
|
|
76
|
+
const root = asRecord(payload);
|
|
77
|
+
|
|
78
|
+
if (root) {
|
|
79
|
+
if (root.structuredContent !== undefined) candidates.push(root.structuredContent);
|
|
80
|
+
if (root.data !== undefined) candidates.push(root.data);
|
|
81
|
+
if (root.result !== undefined) candidates.push(root.result);
|
|
82
|
+
candidates.push(root);
|
|
83
|
+
|
|
84
|
+
const content = root.content;
|
|
85
|
+
if (Array.isArray(content)) {
|
|
86
|
+
for (const item of content) {
|
|
87
|
+
const part = asRecord(item);
|
|
88
|
+
if (!part) continue;
|
|
89
|
+
const text = part.text;
|
|
90
|
+
if (typeof text !== "string" || text.trim().length === 0) continue;
|
|
91
|
+
const parsed = parseJsonContent(text);
|
|
92
|
+
if (parsed !== null) candidates.push(parsed);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
candidates.push(payload);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
for (const candidate of candidates) {
|
|
100
|
+
if (isSearchResponse(candidate)) {
|
|
101
|
+
return candidate;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return payload;
|
|
106
|
+
}
|
|
107
|
+
|
|
65
108
|
function parseOptionalField(section: string, label: string): string | null | undefined {
|
|
66
109
|
const regex = new RegExp(`(?:^|\\n)${label}:\\s*([^\\n]*)`);
|
|
67
110
|
const match = section.match(regex);
|
|
@@ -180,7 +223,8 @@ export function buildExaRequestBody(params: ExaSearchParams): Record<string, unk
|
|
|
180
223
|
async function callExaSearch(apiKey: string, params: ExaSearchParams): Promise<ExaSearchResponse> {
|
|
181
224
|
const body = buildExaRequestBody(params);
|
|
182
225
|
|
|
183
|
-
const
|
|
226
|
+
const fetchImpl = params.fetch ?? fetch;
|
|
227
|
+
const response = await fetchImpl(EXA_API_URL, {
|
|
184
228
|
method: "POST",
|
|
185
229
|
headers: {
|
|
186
230
|
"Content-Type": "application/json",
|
|
@@ -211,14 +255,52 @@ function buildExaMcpArgs(params: ExaSearchParams): Record<string, unknown> {
|
|
|
211
255
|
}
|
|
212
256
|
|
|
213
257
|
async function callExaMcpSearch(params: ExaSearchParams): Promise<ExaSearchResponse> {
|
|
214
|
-
const
|
|
258
|
+
const query = new URLSearchParams();
|
|
259
|
+
const apiKey = findApiKey();
|
|
260
|
+
if (apiKey) query.set("exaApiKey", apiKey);
|
|
261
|
+
query.set("tools", "web_search_exa");
|
|
262
|
+
const fetchImpl = params.fetch ?? fetch;
|
|
263
|
+
const response = await fetchImpl(`https://mcp.exa.ai/mcp?${query.toString()}`, {
|
|
264
|
+
method: "POST",
|
|
265
|
+
headers: {
|
|
266
|
+
"Content-Type": "application/json",
|
|
267
|
+
Accept: "application/json, text/event-stream",
|
|
268
|
+
},
|
|
269
|
+
body: JSON.stringify({
|
|
270
|
+
jsonrpc: "2.0",
|
|
271
|
+
id: Math.random().toString(36).slice(2),
|
|
272
|
+
method: "tools/call",
|
|
273
|
+
params: {
|
|
274
|
+
name: "web_search_exa",
|
|
275
|
+
arguments: buildExaMcpArgs(params),
|
|
276
|
+
},
|
|
277
|
+
}),
|
|
215
278
|
signal: withHardTimeout(params.signal),
|
|
216
279
|
});
|
|
217
|
-
if (
|
|
218
|
-
|
|
280
|
+
if (!response.ok) {
|
|
281
|
+
throw new Error(`MCP request failed: ${response.status} ${response.statusText}`);
|
|
282
|
+
}
|
|
283
|
+
const mcpResponse = parseSSE(await response.text()) as {
|
|
284
|
+
result?: {
|
|
285
|
+
content?: Array<{ type: string; text?: string }>;
|
|
286
|
+
};
|
|
287
|
+
error?: {
|
|
288
|
+
code: number;
|
|
289
|
+
message: string;
|
|
290
|
+
};
|
|
291
|
+
} | null;
|
|
292
|
+
if (!mcpResponse) {
|
|
293
|
+
throw new Error("Failed to parse MCP response");
|
|
294
|
+
}
|
|
295
|
+
if (mcpResponse.error) {
|
|
296
|
+
throw new Error(`MCP error: ${mcpResponse.error.message}`);
|
|
297
|
+
}
|
|
298
|
+
const responsePayload = normalizeExaMcpPayload(mcpResponse.result);
|
|
299
|
+
if (isSearchResponse(responsePayload)) {
|
|
300
|
+
return responsePayload as ExaSearchResponse;
|
|
219
301
|
}
|
|
220
302
|
|
|
221
|
-
const parsed = parseExaMcpTextPayload(
|
|
303
|
+
const parsed = parseExaMcpTextPayload(responsePayload);
|
|
222
304
|
if (parsed) {
|
|
223
305
|
return parsed;
|
|
224
306
|
}
|
|
@@ -312,6 +394,7 @@ export class ExaProvider extends SearchProvider {
|
|
|
312
394
|
signal: params.signal,
|
|
313
395
|
authStorage: params.authStorage,
|
|
314
396
|
sessionId: params.sessionId,
|
|
397
|
+
fetch: params.fetch,
|
|
315
398
|
});
|
|
316
399
|
}
|
|
317
400
|
}
|