@oh-my-pi/pi-coding-agent 17.2.12 → 17.2.14
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 +63 -0
- package/dist/{CHANGELOG-k9ghy5sn.md → CHANGELOG-za420td8.md} +63 -0
- package/dist/cli.js +3658 -3627
- package/dist/types/advisor/delta-split.d.ts +24 -0
- package/dist/types/advisor/runtime.d.ts +2 -2
- package/dist/types/async/job-manager.d.ts +8 -1
- package/dist/types/cli/update-cli.d.ts +53 -1
- package/dist/types/config/keybindings.d.ts +10 -0
- package/dist/types/config/model-resolver.d.ts +15 -2
- package/dist/types/config/settings-schema.d.ts +14 -0
- package/dist/types/discovery/agents-md.d.ts +10 -1
- package/dist/types/eval/runner-cache.d.ts +12 -0
- package/dist/types/extensibility/extensions/runner.d.ts +12 -3
- package/dist/types/extensibility/extensions/types.d.ts +15 -4
- package/dist/types/extensibility/plugins/marketplace/manager.d.ts +4 -1
- package/dist/types/lib/xai-http.d.ts +0 -1
- package/dist/types/mcp/tool-bridge.d.ts +8 -5
- package/dist/types/modes/components/agent-hub-renderer.d.ts +6 -1
- package/dist/types/modes/components/status-line/types.d.ts +4 -0
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -4
- package/dist/types/modes/interactive-mode.d.ts +17 -8
- package/dist/types/modes/types.d.ts +7 -10
- package/dist/types/modes/utils/hotkeys-markdown.d.ts +1 -1
- package/dist/types/session/agent-session-types.d.ts +4 -0
- package/dist/types/session/agent-session.d.ts +24 -3
- package/dist/types/session/messages.d.ts +20 -0
- package/dist/types/session/retry-fallback-chains.d.ts +13 -0
- package/dist/types/session/session-advisors.d.ts +1 -1
- package/dist/types/session/session-history-format.d.ts +10 -0
- package/dist/types/session/session-maintenance.d.ts +1 -1
- package/dist/types/session/session-tools.d.ts +35 -5
- package/dist/types/session/turn-recovery.d.ts +34 -5
- package/dist/types/slash-commands/types.d.ts +5 -1
- package/dist/types/task/executor.d.ts +1 -1
- package/dist/types/tools/approval.d.ts +7 -0
- package/dist/types/tools/builtin-names.d.ts +1 -2
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/think.d.ts +38 -0
- package/dist/types/tools/todo.d.ts +14 -15
- package/dist/types/tools/write.d.ts +2 -2
- package/dist/types/utils/local-date.d.ts +2 -0
- package/dist/types/vibe/runtime.d.ts +1 -1
- package/dist/types/web/parallel.d.ts +1 -0
- package/dist/types/web/search/providers/brave.d.ts +8 -3
- package/dist/types/web/search/providers/codex.d.ts +6 -0
- package/dist/types/web/search/providers/firecrawl.d.ts +3 -2
- package/dist/types/web/search/providers/jina.d.ts +3 -3
- package/dist/types/web/search/providers/parallel.d.ts +1 -0
- package/dist/types/web/search/providers/perplexity.d.ts +4 -0
- package/dist/types/web/search/providers/tinyfish.d.ts +2 -0
- package/package.json +13 -13
- package/src/advisor/delta-split.ts +98 -0
- package/src/advisor/runtime.ts +321 -69
- package/src/async/job-manager.ts +14 -3
- package/src/cli/gallery-fixtures/agentic.ts +16 -0
- package/src/cli/plugin-cli.ts +30 -2
- package/src/cli/update-cli.ts +259 -24
- package/src/config/keybindings.ts +52 -9
- package/src/config/model-resolver.ts +19 -3
- package/src/config/settings-schema.ts +16 -0
- package/src/cursor.ts +10 -5
- package/src/discovery/agents-md.ts +61 -23
- package/src/eval/jl/kernel.ts +2 -20
- package/src/eval/py/kernel.ts +2 -20
- package/src/eval/rb/kernel.ts +2 -20
- package/src/eval/runner-cache.ts +41 -0
- package/src/exec/non-interactive-env.ts +14 -3
- package/src/extensibility/extensions/loader.ts +5 -2
- package/src/extensibility/extensions/runner.ts +184 -66
- package/src/extensibility/extensions/types.ts +26 -2
- package/src/extensibility/extensions/wrapper.ts +13 -7
- package/src/extensibility/plugins/marketplace/manager.ts +6 -2
- package/src/hindsight/client.ts +1 -1
- package/src/lib/xai-http.ts +0 -4
- package/src/lsp/client.ts +2 -0
- package/src/lsp/servers.ts +1 -1
- package/src/mcp/tool-bridge.ts +15 -6
- package/src/modes/components/agent-hub-renderer.ts +9 -3
- package/src/modes/components/agent-hub.ts +2 -1
- package/src/modes/components/status-line/component.ts +58 -6
- package/src/modes/components/status-line/segments.ts +12 -1
- package/src/modes/components/status-line/types.ts +1 -0
- package/src/modes/components/user-message.ts +20 -5
- package/src/modes/controllers/event-controller.ts +48 -0
- package/src/modes/controllers/extension-ui-controller.ts +14 -7
- package/src/modes/controllers/input-controller.ts +25 -6
- package/src/modes/controllers/selector-controller.ts +5 -0
- package/src/modes/interactive-mode.ts +315 -129
- package/src/modes/rpc/rpc-frame.ts +13 -5
- package/src/modes/theme/tui-adapters.ts +4 -5
- package/src/modes/types.ts +13 -7
- package/src/modes/utils/hotkeys-markdown.ts +10 -6
- package/src/prompts/system/system-prompt.md +10 -1
- package/src/registry/persisted-agents.ts +43 -8
- package/src/sdk.ts +146 -9
- package/src/session/agent-session-types.ts +4 -0
- package/src/session/agent-session.ts +91 -10
- package/src/session/messages.ts +98 -28
- package/src/session/retry-fallback-chains.ts +14 -0
- package/src/session/session-advisors.ts +32 -15
- package/src/session/session-history-format.ts +15 -1
- package/src/session/session-maintenance.ts +8 -8
- package/src/session/session-manager.ts +6 -2
- package/src/session/session-tools.ts +355 -183
- package/src/session/turn-recovery.ts +225 -47
- package/src/slash-commands/builtin-modes.ts +41 -12
- package/src/slash-commands/types.ts +5 -1
- package/src/task/executor.ts +92 -45
- package/src/task/structured-subagent.ts +5 -5
- package/src/tools/approval.ts +44 -10
- package/src/tools/builtin-names.ts +1 -2
- package/src/tools/fetch.ts +21 -2
- package/src/tools/image-gen.ts +6 -8
- package/src/tools/index.ts +8 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/think.ts +62 -0
- package/src/tools/todo.ts +70 -26
- package/src/tools/tts.ts +3 -2
- package/src/tools/write.ts +7 -3
- package/src/utils/local-date.ts +13 -0
- package/src/utils/tools-manager.ts +2 -2
- package/src/vibe/runtime.ts +22 -14
- package/src/web/kagi.ts +91 -34
- package/src/web/parallel.ts +11 -2
- package/src/web/scrapers/crates-io.ts +2 -2
- package/src/web/scrapers/discogs.ts +2 -2
- package/src/web/scrapers/docs-rs.ts +2 -2
- package/src/web/scrapers/github.ts +2 -2
- package/src/web/scrapers/musicbrainz.ts +1 -2
- package/src/web/scrapers/pubmed.ts +2 -2
- package/src/web/scrapers/sec-edgar.ts +2 -2
- package/src/web/search/providers/brave.ts +121 -46
- package/src/web/search/providers/codex.ts +88 -12
- package/src/web/search/providers/exa.ts +45 -10
- package/src/web/search/providers/firecrawl.ts +53 -11
- package/src/web/search/providers/gemini.ts +139 -27
- package/src/web/search/providers/jina.ts +48 -25
- package/src/web/search/providers/parallel.ts +23 -9
- package/src/web/search/providers/perplexity.ts +24 -7
- package/src/web/search/providers/searxng.ts +77 -1
- package/src/web/search/providers/tavily.ts +23 -22
- package/src/web/search/providers/tinyfish.ts +44 -10
- package/src/web/search/providers/xai.ts +85 -14
|
@@ -20,7 +20,7 @@ import type { SearchParams } from "./base";
|
|
|
20
20
|
import { SearchProvider } from "./base";
|
|
21
21
|
import { classifyProviderHttpError, withHardTimeout } from "./utils";
|
|
22
22
|
|
|
23
|
-
const
|
|
23
|
+
const FIRECRAWL_DEFAULT_BASE_URL = "https://api.firecrawl.dev/v2";
|
|
24
24
|
const DEFAULT_NUM_RESULTS = 10;
|
|
25
25
|
const MAX_NUM_RESULTS = 100;
|
|
26
26
|
|
|
@@ -30,6 +30,28 @@ const RECENCY_TBS: Record<NonNullable<SearchParams["recency"]>, string> = {
|
|
|
30
30
|
month: "qdr:m",
|
|
31
31
|
year: "qdr:y",
|
|
32
32
|
};
|
|
33
|
+
function resolveSearchUrl(): string {
|
|
34
|
+
const configured = process.env.FIRECRAWL_BASE_URL ?? process.env.FIRECRAWL_API_URL;
|
|
35
|
+
if (!configured?.trim()) return `${FIRECRAWL_DEFAULT_BASE_URL}/search`;
|
|
36
|
+
let url: URL;
|
|
37
|
+
try {
|
|
38
|
+
url = new URL(configured.trim());
|
|
39
|
+
} catch {
|
|
40
|
+
throw new Error("Invalid Firecrawl base URL: expected an HTTP or HTTPS URL");
|
|
41
|
+
}
|
|
42
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
43
|
+
throw new Error("Invalid Firecrawl base URL: expected an HTTP or HTTPS URL");
|
|
44
|
+
}
|
|
45
|
+
if (url.username || url.password) {
|
|
46
|
+
throw new Error("Invalid Firecrawl base URL: URL credentials are not allowed");
|
|
47
|
+
}
|
|
48
|
+
url.search = "";
|
|
49
|
+
url.hash = "";
|
|
50
|
+
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
51
|
+
if (!/\/v[12]$/i.test(url.pathname)) url.pathname += "/v2";
|
|
52
|
+
url.pathname += "/search";
|
|
53
|
+
return url.toString();
|
|
54
|
+
}
|
|
33
55
|
|
|
34
56
|
export interface FirecrawlSearchParams {
|
|
35
57
|
query: string;
|
|
@@ -46,14 +68,23 @@ interface FirecrawlWebResult {
|
|
|
46
68
|
title?: string | null;
|
|
47
69
|
url?: string | null;
|
|
48
70
|
description?: string | null;
|
|
71
|
+
snippet?: string | null;
|
|
49
72
|
markdown?: string | null;
|
|
50
73
|
}
|
|
51
74
|
|
|
52
75
|
interface FirecrawlSearchResponse {
|
|
76
|
+
success?: boolean;
|
|
77
|
+
error?: string | null;
|
|
53
78
|
id?: string | null;
|
|
54
|
-
data?:
|
|
55
|
-
|
|
56
|
-
|
|
79
|
+
data?:
|
|
80
|
+
| FirecrawlWebResult[]
|
|
81
|
+
| {
|
|
82
|
+
web?: FirecrawlWebResult[] | null;
|
|
83
|
+
news?: FirecrawlWebResult[] | null;
|
|
84
|
+
images?: FirecrawlWebResult[] | null;
|
|
85
|
+
}
|
|
86
|
+
| null;
|
|
87
|
+
results?: FirecrawlWebResult[] | null;
|
|
57
88
|
}
|
|
58
89
|
|
|
59
90
|
/** Resolve Firecrawl API key through the shared auth storage pipeline. */
|
|
@@ -88,7 +119,7 @@ async function callFirecrawlSearch(
|
|
|
88
119
|
if (apiKey) {
|
|
89
120
|
headers.Authorization = `Bearer ${apiKey}`;
|
|
90
121
|
}
|
|
91
|
-
const response = await (params.fetch ?? fetch)(
|
|
122
|
+
const response = await (params.fetch ?? fetch)(resolveSearchUrl(), {
|
|
92
123
|
method: "POST",
|
|
93
124
|
headers,
|
|
94
125
|
body: JSON.stringify(buildRequestBody(params)),
|
|
@@ -106,7 +137,11 @@ async function callFirecrawlSearch(
|
|
|
106
137
|
);
|
|
107
138
|
}
|
|
108
139
|
|
|
109
|
-
|
|
140
|
+
const data = (await response.json()) as FirecrawlSearchResponse;
|
|
141
|
+
if (data.success === false) {
|
|
142
|
+
throw new SearchProviderError("firecrawl", data.error?.trim() || "Firecrawl request failed");
|
|
143
|
+
}
|
|
144
|
+
return data;
|
|
110
145
|
}
|
|
111
146
|
|
|
112
147
|
/** ISO `YYYY-MM-DD` to Google `MM/DD/YYYY` for `tbs=cdr` custom date ranges. */
|
|
@@ -128,6 +163,11 @@ function buildDateTbs(parsed: StructuredQuery): string | undefined {
|
|
|
128
163
|
return parts.join(",");
|
|
129
164
|
}
|
|
130
165
|
|
|
166
|
+
function getWebResults(data: FirecrawlSearchResponse): FirecrawlWebResult[] {
|
|
167
|
+
if (Array.isArray(data.data)) return data.data;
|
|
168
|
+
if (data.data && Array.isArray(data.data.web)) return data.data.web;
|
|
169
|
+
return data.results ?? [];
|
|
170
|
+
}
|
|
131
171
|
/** Execute Firecrawl web search. */
|
|
132
172
|
export async function searchFirecrawl(params: SearchParams): Promise<SearchResponse> {
|
|
133
173
|
const parsed = params.parsedQuery ?? parseSearchQuery(params.query);
|
|
@@ -169,12 +209,12 @@ export async function searchFirecrawl(params: SearchParams): Promise<SearchRespo
|
|
|
169
209
|
|
|
170
210
|
const sources: SearchSource[] = [];
|
|
171
211
|
|
|
172
|
-
for (const result of data
|
|
212
|
+
for (const result of getWebResults(data)) {
|
|
173
213
|
if (!result.url) continue;
|
|
174
214
|
sources.push({
|
|
175
215
|
title: result.title ?? result.url,
|
|
176
216
|
url: result.url,
|
|
177
|
-
snippet: result.description ?? result.markdown ?? undefined,
|
|
217
|
+
snippet: result.description ?? result.snippet ?? result.markdown ?? undefined,
|
|
178
218
|
});
|
|
179
219
|
}
|
|
180
220
|
|
|
@@ -192,11 +232,13 @@ export class FirecrawlProvider extends SearchProvider {
|
|
|
192
232
|
readonly label = "Firecrawl";
|
|
193
233
|
|
|
194
234
|
/**
|
|
195
|
-
* Auto-chain admission
|
|
196
|
-
*
|
|
235
|
+
* Auto-chain admission requires either a credential or an explicitly
|
|
236
|
+
* configured self-hosted endpoint. Hosted keyless mode remains explicit-only
|
|
237
|
+
* so it does not displace providers the user configured.
|
|
197
238
|
*/
|
|
198
239
|
isAvailable(authStorage: AuthStorage): boolean {
|
|
199
|
-
|
|
240
|
+
const configuredBaseUrl = process.env.FIRECRAWL_BASE_URL ?? process.env.FIRECRAWL_API_URL;
|
|
241
|
+
return !!configuredBaseUrl?.trim() || authStorage.hasAuth("firecrawl") || !!getEnvApiKey("firecrawl");
|
|
200
242
|
}
|
|
201
243
|
|
|
202
244
|
/**
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
getAntigravityUserAgent,
|
|
15
15
|
getGeminiCliHeaders,
|
|
16
16
|
} from "@oh-my-pi/pi-catalog/wire/gemini-headers";
|
|
17
|
-
import { fetchWithRetry } from "@oh-my-pi/pi-utils";
|
|
17
|
+
import { fetchWithRetry, USER_AGENT } from "@oh-my-pi/pi-utils";
|
|
18
18
|
|
|
19
19
|
import type { SearchCitation, SearchResponse, SearchSource } from "../../../web/search/types";
|
|
20
20
|
import { SearchProviderError } from "../../../web/search/types";
|
|
@@ -25,7 +25,9 @@ import { classifyProviderHttpError, withHardTimeout } from "./utils";
|
|
|
25
25
|
|
|
26
26
|
const DEFAULT_ENDPOINT = "https://cloudcode-pa.googleapis.com";
|
|
27
27
|
const DEVELOPER_API_PROVIDER = "google";
|
|
28
|
-
const
|
|
28
|
+
const CLOUDFLARE_GATEWAY_PROVIDER = "cloudflare-ai-gateway";
|
|
29
|
+
const DEFAULT_DEVELOPER_API_HOST = "https://generativelanguage.googleapis.com";
|
|
30
|
+
const DEVELOPER_API_VERSION = "v1beta";
|
|
29
31
|
const ANTIGRAVITY_DAILY_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
|
|
30
32
|
const ANTIGRAVITY_SANDBOX_ENDPOINT = "https://daily-cloudcode-pa.sandbox.googleapis.com";
|
|
31
33
|
const ANTIGRAVITY_ENDPOINT_FALLBACKS = [ANTIGRAVITY_DAILY_ENDPOINT, ANTIGRAVITY_SANDBOX_ENDPOINT] as const;
|
|
@@ -41,6 +43,32 @@ function resolveGeminiSearchModel(configuredModel: string | undefined): string {
|
|
|
41
43
|
return model || DEFAULT_MODEL;
|
|
42
44
|
}
|
|
43
45
|
|
|
46
|
+
interface GeminiDeveloperEndpoint {
|
|
47
|
+
url: string;
|
|
48
|
+
authProvider: typeof DEVELOPER_API_PROVIDER | typeof CLOUDFLARE_GATEWAY_PROVIDER;
|
|
49
|
+
isCloudflareGateway: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function resolveGeminiDeveloperEndpoint(): GeminiDeveloperEndpoint {
|
|
53
|
+
const configuredHost = Bun.env.GOOGLE_GEMINI_BASE_URL?.trim().replace(/\/+$/, "");
|
|
54
|
+
const host = configuredHost || DEFAULT_DEVELOPER_API_HOST;
|
|
55
|
+
let parsed: URL;
|
|
56
|
+
try {
|
|
57
|
+
parsed = new URL(host);
|
|
58
|
+
} catch {
|
|
59
|
+
throw new SearchProviderError("gemini", "GOOGLE_GEMINI_BASE_URL must be a valid absolute URL", 400);
|
|
60
|
+
}
|
|
61
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
62
|
+
throw new SearchProviderError("gemini", "GOOGLE_GEMINI_BASE_URL must use HTTP or HTTPS", 400);
|
|
63
|
+
}
|
|
64
|
+
const isCloudflareGateway = parsed.hostname === "gateway.ai.cloudflare.com";
|
|
65
|
+
return {
|
|
66
|
+
url: `${host}/${DEVELOPER_API_VERSION}`,
|
|
67
|
+
authProvider: isCloudflareGateway ? CLOUDFLARE_GATEWAY_PROVIDER : DEVELOPER_API_PROVIDER,
|
|
68
|
+
isCloudflareGateway,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
44
72
|
const GEMINI_PROVIDERS = ["google-gemini-cli", "google-antigravity"] as const;
|
|
45
73
|
type GeminiProviderId = (typeof GEMINI_PROVIDERS)[number];
|
|
46
74
|
|
|
@@ -292,6 +320,79 @@ async function parseGeminiSearchStream(
|
|
|
292
320
|
};
|
|
293
321
|
}
|
|
294
322
|
|
|
323
|
+
function isGroundingRedirectUrl(url: string): boolean {
|
|
324
|
+
try {
|
|
325
|
+
const parsed = new URL(url);
|
|
326
|
+
return (
|
|
327
|
+
parsed.hostname === "vertexaisearch.cloud.google.com" && parsed.pathname.includes("/grounding-api-redirect")
|
|
328
|
+
);
|
|
329
|
+
} catch {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
async function resolveGroundingRedirect(
|
|
335
|
+
proxyUrl: string,
|
|
336
|
+
fetchImpl: FetchImpl | undefined,
|
|
337
|
+
signal: AbortSignal | undefined,
|
|
338
|
+
): Promise<string> {
|
|
339
|
+
try {
|
|
340
|
+
const response = await (fetchImpl ?? fetch)(proxyUrl, {
|
|
341
|
+
method: "HEAD",
|
|
342
|
+
redirect: "manual",
|
|
343
|
+
signal: withHardTimeout(signal, 5000),
|
|
344
|
+
});
|
|
345
|
+
const location = response.headers.get("location");
|
|
346
|
+
if (!location) return proxyUrl;
|
|
347
|
+
const resolved = new URL(location, proxyUrl);
|
|
348
|
+
return resolved.protocol === "http:" || resolved.protocol === "https:" ? resolved.toString() : proxyUrl;
|
|
349
|
+
} catch {
|
|
350
|
+
return proxyUrl;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function finalizeGeminiSearchResult(
|
|
355
|
+
result: GeminiSearchResult,
|
|
356
|
+
fetchImpl: FetchImpl | undefined,
|
|
357
|
+
signal: AbortSignal | undefined,
|
|
358
|
+
): Promise<GeminiSearchResult> {
|
|
359
|
+
if (!result.answer && result.sources.length === 0) {
|
|
360
|
+
throw new SearchProviderError("gemini", "Gemini API returned an empty grounded response", 502);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const redirectUrls = new Set<string>();
|
|
364
|
+
for (const source of result.sources) {
|
|
365
|
+
if (isGroundingRedirectUrl(source.url)) redirectUrls.add(source.url);
|
|
366
|
+
}
|
|
367
|
+
for (const citation of result.citations) {
|
|
368
|
+
if (isGroundingRedirectUrl(citation.url)) redirectUrls.add(citation.url);
|
|
369
|
+
}
|
|
370
|
+
if (redirectUrls.size === 0) return result;
|
|
371
|
+
|
|
372
|
+
signal?.throwIfAborted();
|
|
373
|
+
const resolvedEntries = await Promise.all(
|
|
374
|
+
[...redirectUrls].map(async url => [url, await resolveGroundingRedirect(url, fetchImpl, signal)] as const),
|
|
375
|
+
);
|
|
376
|
+
signal?.throwIfAborted();
|
|
377
|
+
const resolvedUrls = new Map(resolvedEntries);
|
|
378
|
+
for (const source of result.sources) {
|
|
379
|
+
source.url = resolvedUrls.get(source.url) ?? source.url;
|
|
380
|
+
}
|
|
381
|
+
for (const citation of result.citations) {
|
|
382
|
+
citation.url = resolvedUrls.get(citation.url) ?? citation.url;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const seenUrls = new Set<string>();
|
|
386
|
+
let writeIndex = 0;
|
|
387
|
+
for (const source of result.sources) {
|
|
388
|
+
if (seenUrls.has(source.url)) continue;
|
|
389
|
+
seenUrls.add(source.url);
|
|
390
|
+
result.sources[writeIndex++] = source;
|
|
391
|
+
}
|
|
392
|
+
result.sources.length = writeIndex;
|
|
393
|
+
return result;
|
|
394
|
+
}
|
|
395
|
+
|
|
295
396
|
/**
|
|
296
397
|
* Calls the Cloud Code Assist API with Google Search grounding enabled.
|
|
297
398
|
*
|
|
@@ -335,8 +436,8 @@ async function callGeminiSearch(
|
|
|
335
436
|
requestId: `agent-${crypto.randomUUID()}`,
|
|
336
437
|
}
|
|
337
438
|
: {
|
|
338
|
-
userAgent:
|
|
339
|
-
requestId: `
|
|
439
|
+
userAgent: USER_AGENT,
|
|
440
|
+
requestId: `omp-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
|
|
340
441
|
};
|
|
341
442
|
|
|
342
443
|
const normalizedSystemPrompt = systemPrompt?.toWellFormed();
|
|
@@ -420,7 +521,8 @@ async function callGeminiSearch(
|
|
|
420
521
|
}
|
|
421
522
|
|
|
422
523
|
if (!response?.ok) {
|
|
423
|
-
const
|
|
524
|
+
const rawErrorText = response ? await response.text() : "Network error";
|
|
525
|
+
const errorText = auth.accessToken ? rawErrorText.split(auth.accessToken).join("[redacted]") : rawErrorText;
|
|
424
526
|
const status = response?.status ?? 502;
|
|
425
527
|
const classified = classifyProviderHttpError("gemini", status, errorText);
|
|
426
528
|
if (classified) throw classified;
|
|
@@ -431,11 +533,12 @@ async function callGeminiSearch(
|
|
|
431
533
|
throw new SearchProviderError("gemini", "Gemini API returned no response body", 500);
|
|
432
534
|
}
|
|
433
535
|
|
|
434
|
-
return parseGeminiSearchStream(response.body, model);
|
|
536
|
+
return finalizeGeminiSearchResult(await parseGeminiSearchStream(response.body, model), fetchImpl, signal);
|
|
435
537
|
}
|
|
436
538
|
|
|
437
539
|
async function callGeminiDeveloperSearch(
|
|
438
540
|
apiKey: string,
|
|
541
|
+
endpoint: GeminiDeveloperEndpoint,
|
|
439
542
|
model: string,
|
|
440
543
|
query: string,
|
|
441
544
|
systemPrompt: string | undefined,
|
|
@@ -473,26 +576,26 @@ async function callGeminiDeveloperSearch(
|
|
|
473
576
|
requestBody.generationConfig = generationConfig;
|
|
474
577
|
}
|
|
475
578
|
|
|
476
|
-
const response = await fetchWithRetry(
|
|
477
|
-
|
|
478
|
-
{
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
"x-goog-api-key": apiKey,
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
},
|
|
485
|
-
body: JSON.stringify(requestBody),
|
|
486
|
-
signal: withHardTimeout(signal, timeoutMs),
|
|
487
|
-
fetch: fetchImpl,
|
|
488
|
-
maxAttempts: MAX_RETRIES + 1,
|
|
489
|
-
defaultDelayMs: attempt => BASE_DELAY_MS * 2 ** attempt,
|
|
490
|
-
maxDelayMs: RATE_LIMIT_BUDGET_MS,
|
|
579
|
+
const response = await fetchWithRetry(() => `${endpoint.url}/models/${model}:streamGenerateContent?alt=sse`, {
|
|
580
|
+
method: "POST",
|
|
581
|
+
headers: {
|
|
582
|
+
...(endpoint.isCloudflareGateway
|
|
583
|
+
? { "cf-aig-authorization": `Bearer ${apiKey}` }
|
|
584
|
+
: { "x-goog-api-key": apiKey }),
|
|
585
|
+
"Content-Type": "application/json",
|
|
586
|
+
Accept: "text/event-stream",
|
|
491
587
|
},
|
|
492
|
-
|
|
588
|
+
body: JSON.stringify(requestBody),
|
|
589
|
+
signal: withHardTimeout(signal, timeoutMs),
|
|
590
|
+
fetch: fetchImpl,
|
|
591
|
+
maxAttempts: MAX_RETRIES + 1,
|
|
592
|
+
defaultDelayMs: attempt => BASE_DELAY_MS * 2 ** attempt,
|
|
593
|
+
maxDelayMs: RATE_LIMIT_BUDGET_MS,
|
|
594
|
+
});
|
|
493
595
|
|
|
494
596
|
if (!response.ok) {
|
|
495
|
-
const
|
|
597
|
+
const rawErrorText = await response.text();
|
|
598
|
+
const errorText = apiKey ? rawErrorText.split(apiKey).join("[redacted]") : rawErrorText;
|
|
496
599
|
const classified = classifyProviderHttpError("gemini", response.status, errorText);
|
|
497
600
|
if (classified) throw classified;
|
|
498
601
|
throw new SearchProviderError(
|
|
@@ -506,7 +609,7 @@ async function callGeminiDeveloperSearch(
|
|
|
506
609
|
throw new SearchProviderError("gemini", "Gemini API returned no response body", 500);
|
|
507
610
|
}
|
|
508
611
|
|
|
509
|
-
return parseGeminiSearchStream(response.body, model);
|
|
612
|
+
return finalizeGeminiSearchResult(await parseGeminiSearchStream(response.body, model), fetchImpl, signal);
|
|
510
613
|
}
|
|
511
614
|
|
|
512
615
|
/**
|
|
@@ -557,16 +660,20 @@ export async function searchGemini(params: GeminiSearchParams): Promise<SearchRe
|
|
|
557
660
|
{ sessionId: params.sessionId, signal: params.signal, seed: seed.access },
|
|
558
661
|
);
|
|
559
662
|
} else {
|
|
560
|
-
const
|
|
663
|
+
const endpoint = resolveGeminiDeveloperEndpoint();
|
|
664
|
+
const apiKey = await params.authStorage.getApiKey(endpoint.authProvider, params.sessionId, {
|
|
561
665
|
signal: params.signal,
|
|
562
666
|
});
|
|
563
667
|
if (!apiKey) {
|
|
564
668
|
throw new Error(
|
|
565
|
-
|
|
669
|
+
endpoint.isCloudflareGateway
|
|
670
|
+
? 'No Cloudflare AI Gateway credential found. Configure provider "cloudflare-ai-gateway" or set CLOUDFLARE_AI_GATEWAY_API_KEY.'
|
|
671
|
+
: "No Gemini credentials found. Set GEMINI_API_KEY, configure an API key for provider \"google\", or login with 'omp /login google-gemini-cli' / 'omp /login google-antigravity' to enable Gemini web search.",
|
|
566
672
|
);
|
|
567
673
|
}
|
|
568
674
|
result = await callGeminiDeveloperSearch(
|
|
569
675
|
apiKey,
|
|
676
|
+
endpoint,
|
|
570
677
|
selectedModel,
|
|
571
678
|
searchQuery,
|
|
572
679
|
params.system_prompt,
|
|
@@ -609,7 +716,12 @@ export class GeminiProvider extends SearchProvider {
|
|
|
609
716
|
// Cheap, in-memory check — avoids driving the refresh pipeline during
|
|
610
717
|
// the provider-chain probe. `searchGemini` refreshes OAuth lazily on the
|
|
611
718
|
// actual request and resolves developer API keys through AuthStorage.
|
|
612
|
-
|
|
719
|
+
if (hasGeminiOAuth(authStorage)) return true;
|
|
720
|
+
try {
|
|
721
|
+
return authStorage.hasAuth(resolveGeminiDeveloperEndpoint().authProvider);
|
|
722
|
+
} catch {
|
|
723
|
+
return false;
|
|
724
|
+
}
|
|
613
725
|
}
|
|
614
726
|
|
|
615
727
|
search(params: SearchParams): Promise<SearchResponse> {
|
|
@@ -5,19 +5,24 @@
|
|
|
5
5
|
* cleaned content.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { type AuthStorage, type FetchImpl,
|
|
8
|
+
import { type ApiKey, type AuthStorage, type FetchImpl, withAuth } from "@oh-my-pi/pi-ai";
|
|
9
9
|
import type { SearchResponse, SearchSource } from "../../../web/search/types";
|
|
10
10
|
import { SearchProviderError } from "../../../web/search/types";
|
|
11
11
|
import { formatQuery, parseSearchQuery } from "../query";
|
|
12
|
+
import { clampNumResults } from "../utils";
|
|
12
13
|
import type { SearchParams } from "./base";
|
|
13
14
|
import { SearchProvider } from "./base";
|
|
14
15
|
import { classifyProviderHttpError, withHardTimeout } from "./utils";
|
|
15
16
|
|
|
16
17
|
const JINA_SEARCH_URL = "https://s.jina.ai";
|
|
18
|
+
const DEFAULT_NUM_RESULTS = 5;
|
|
19
|
+
const MAX_NUM_RESULTS = 20;
|
|
17
20
|
type SearchParamsWithFetch = SearchParams & { fetch?: FetchImpl };
|
|
18
21
|
|
|
19
22
|
export interface JinaSearchParams {
|
|
20
23
|
query: string;
|
|
24
|
+
authStorage: AuthStorage;
|
|
25
|
+
sessionId?: string;
|
|
21
26
|
num_results?: number;
|
|
22
27
|
/** Single bare host for Jina's `X-Site` in-site search header. */
|
|
23
28
|
site?: string;
|
|
@@ -29,31 +34,37 @@ export interface JinaSearchParams {
|
|
|
29
34
|
interface JinaSearchResult {
|
|
30
35
|
title?: string | null;
|
|
31
36
|
url?: string | null;
|
|
37
|
+
description?: string | null;
|
|
32
38
|
content?: string | null;
|
|
33
39
|
}
|
|
34
40
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
export function findApiKey(): string | null {
|
|
39
|
-
return getEnvApiKey("jina") ?? null;
|
|
41
|
+
interface JinaSearchEnvelope {
|
|
42
|
+
code?: unknown;
|
|
43
|
+
data?: unknown;
|
|
40
44
|
}
|
|
41
45
|
|
|
46
|
+
type JinaSearchResponse = JinaSearchResult[];
|
|
47
|
+
|
|
42
48
|
/** Call Jina Reader search API. */
|
|
43
49
|
async function callJinaSearch(
|
|
44
50
|
apiKey: string,
|
|
45
51
|
query: string,
|
|
52
|
+
numResults: number,
|
|
46
53
|
site?: string,
|
|
47
54
|
signal?: AbortSignal,
|
|
48
55
|
fetchImpl: FetchImpl = fetch,
|
|
49
56
|
timeoutMs?: number,
|
|
50
57
|
): Promise<JinaSearchResponse> {
|
|
51
|
-
const requestUrl = `${JINA_SEARCH_URL}/${encodeURIComponent(query)}
|
|
58
|
+
const requestUrl = new URL(`${JINA_SEARCH_URL}/${encodeURIComponent(query)}`);
|
|
59
|
+
requestUrl.searchParams.set("count", String(numResults));
|
|
60
|
+
|
|
52
61
|
const headers: Record<string, string> = {
|
|
53
62
|
Accept: "application/json",
|
|
54
63
|
Authorization: `Bearer ${apiKey}`,
|
|
55
64
|
};
|
|
56
65
|
if (site) headers["X-Site"] = site;
|
|
66
|
+
headers["X-Respond-With"] = "no-content";
|
|
67
|
+
headers["X-Retain-Images"] = "none";
|
|
57
68
|
const response = await fetchImpl(requestUrl, {
|
|
58
69
|
headers,
|
|
59
70
|
signal: withHardTimeout(signal, timeoutMs),
|
|
@@ -66,24 +77,34 @@ async function callJinaSearch(
|
|
|
66
77
|
throw new SearchProviderError("jina", `Jina API error (${response.status}): ${errorText}`, response.status);
|
|
67
78
|
}
|
|
68
79
|
|
|
69
|
-
const payload = (await response.json()) as
|
|
70
|
-
|
|
80
|
+
const payload = (await response.json()) as JinaSearchEnvelope | JinaSearchResponse | null;
|
|
81
|
+
if (Array.isArray(payload)) return payload;
|
|
82
|
+
if (!payload || typeof payload !== "object") {
|
|
83
|
+
throw new SearchProviderError("jina", "Jina API returned invalid response: expected an object or array");
|
|
84
|
+
}
|
|
85
|
+
if (typeof payload.code === "number" && payload.code !== 200) {
|
|
86
|
+
throw new SearchProviderError("jina", `Jina API response reported failure (${payload.code})`, payload.code);
|
|
87
|
+
}
|
|
88
|
+
if (!Array.isArray(payload.data)) {
|
|
89
|
+
throw new SearchProviderError("jina", "Jina API returned invalid response: expected data array");
|
|
90
|
+
}
|
|
91
|
+
return payload.data as JinaSearchResponse;
|
|
71
92
|
}
|
|
72
93
|
|
|
73
94
|
/** Execute Jina web search. */
|
|
74
95
|
export async function searchJina(params: JinaSearchParams): Promise<SearchResponse> {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
apiKey
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
96
|
+
const numResults = clampNumResults(params.num_results, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS);
|
|
97
|
+
const keyOrResolver: ApiKey = params.authStorage.resolver("jina", {
|
|
98
|
+
sessionId: params.sessionId,
|
|
99
|
+
});
|
|
100
|
+
const response = await withAuth(
|
|
101
|
+
keyOrResolver,
|
|
102
|
+
apiKey =>
|
|
103
|
+
callJinaSearch(apiKey, params.query, numResults, params.site, params.signal, params.fetch, params.timeoutMs),
|
|
104
|
+
{
|
|
105
|
+
signal: params.signal,
|
|
106
|
+
missingKeyMessage: 'Jina credentials not found. Set JINA_API_KEY or configure an API key for provider "jina".',
|
|
107
|
+
},
|
|
87
108
|
);
|
|
88
109
|
const sources: SearchSource[] = [];
|
|
89
110
|
|
|
@@ -92,11 +113,11 @@ export async function searchJina(params: JinaSearchParams): Promise<SearchRespon
|
|
|
92
113
|
sources.push({
|
|
93
114
|
title: result.title ?? result.url,
|
|
94
115
|
url: result.url,
|
|
95
|
-
snippet: result.content
|
|
116
|
+
snippet: result.description?.trim() || result.content?.trim() || undefined,
|
|
96
117
|
});
|
|
97
118
|
}
|
|
98
119
|
|
|
99
|
-
const limitedSources =
|
|
120
|
+
const limitedSources = sources.slice(0, numResults);
|
|
100
121
|
|
|
101
122
|
return {
|
|
102
123
|
provider: "jina",
|
|
@@ -109,8 +130,8 @@ export class JinaProvider extends SearchProvider {
|
|
|
109
130
|
readonly id = "jina";
|
|
110
131
|
readonly label = "Jina";
|
|
111
132
|
|
|
112
|
-
isAvailable(
|
|
113
|
-
return
|
|
133
|
+
isAvailable(authStorage: AuthStorage): boolean {
|
|
134
|
+
return authStorage.hasAuth("jina");
|
|
114
135
|
}
|
|
115
136
|
|
|
116
137
|
search(params: SearchParamsWithFetch): Promise<SearchResponse> {
|
|
@@ -134,6 +155,8 @@ export class JinaProvider extends SearchProvider {
|
|
|
134
155
|
|
|
135
156
|
return searchJina({
|
|
136
157
|
query,
|
|
158
|
+
authStorage: params.authStorage,
|
|
159
|
+
sessionId: params.sessionId,
|
|
137
160
|
num_results: params.numSearchResults ?? params.limit,
|
|
138
161
|
site,
|
|
139
162
|
signal: params.signal,
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
ParallelApiError,
|
|
8
8
|
type ParallelSearchResult,
|
|
9
9
|
parseParallelErrorResponse,
|
|
10
|
+
parseParallelJsonResponse,
|
|
10
11
|
parseParallelSearchPayload,
|
|
11
12
|
} from "../../parallel";
|
|
12
13
|
import { formatQuery, parseSearchQuery, type StructuredQuery } from "../query";
|
|
@@ -28,6 +29,13 @@ interface ParallelSourcePolicy {
|
|
|
28
29
|
after_date?: string;
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
const RECENCY_DAYS: Record<NonNullable<SearchParams["recency"]>, number> = {
|
|
33
|
+
day: 1,
|
|
34
|
+
week: 7,
|
|
35
|
+
month: 30,
|
|
36
|
+
year: 365,
|
|
37
|
+
};
|
|
38
|
+
|
|
31
39
|
/** Site values may carry paths (`github.com/anthropics`); Parallel takes bare hosts. */
|
|
32
40
|
function toHosts(sites: readonly string[]): string[] {
|
|
33
41
|
const hosts = new Set<string>();
|
|
@@ -39,19 +47,23 @@ function toHosts(sites: readonly string[]): string[] {
|
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
/**
|
|
42
|
-
* Map parsed `site:`/`-site:`/`after:` directives
|
|
43
|
-
* `source_policy`.
|
|
44
|
-
*
|
|
45
|
-
* list (the central lenient
|
|
50
|
+
* Map parsed `site:`/`-site:`/`after:` directives and the relative recency
|
|
51
|
+
* option onto Parallel's `source_policy`. An explicit `after:` bound wins.
|
|
52
|
+
* Per Parallel docs, `exclude_domains` is ignored when `include_domains` is
|
|
53
|
+
* set, so exclusions are only sent without an allow list (the central lenient
|
|
54
|
+
* filter enforces them regardless).
|
|
46
55
|
*/
|
|
47
|
-
function toSourcePolicy(parsed: StructuredQuery): ParallelSourcePolicy | undefined {
|
|
56
|
+
function toSourcePolicy(parsed: StructuredQuery, recency?: SearchParams["recency"]): ParallelSourcePolicy | undefined {
|
|
48
57
|
const policy: ParallelSourcePolicy = {};
|
|
49
58
|
const include = toHosts(parsed.sites);
|
|
50
59
|
const exclude = toHosts(parsed.excludedSites);
|
|
51
60
|
if (include.length) policy.include_domains = include;
|
|
52
61
|
else if (exclude.length) policy.exclude_domains = exclude;
|
|
53
62
|
if (parsed.after) policy.after_date = parsed.after;
|
|
54
|
-
|
|
63
|
+
else if (recency) {
|
|
64
|
+
policy.after_date = new Date(Date.now() - RECENCY_DAYS[recency] * 86_400_000).toISOString().slice(0, 10);
|
|
65
|
+
}
|
|
66
|
+
return policy.include_domains || policy.exclude_domains || policy.after_date ? policy : undefined;
|
|
55
67
|
}
|
|
56
68
|
|
|
57
69
|
async function searchWithAuthStorage(
|
|
@@ -105,7 +117,7 @@ async function searchWithAuthStorage(
|
|
|
105
117
|
throw parseParallelErrorResponse(response.status, await response.text());
|
|
106
118
|
}
|
|
107
119
|
|
|
108
|
-
const payload
|
|
120
|
+
const payload = await parseParallelJsonResponse(response, "search");
|
|
109
121
|
return parseParallelSearchPayload(payload, { parseMetadata: false });
|
|
110
122
|
},
|
|
111
123
|
{ signal: params.signal },
|
|
@@ -116,6 +128,7 @@ export async function searchParallel(
|
|
|
116
128
|
params: {
|
|
117
129
|
query: string;
|
|
118
130
|
num_results?: number;
|
|
131
|
+
recency?: SearchParams["recency"];
|
|
119
132
|
signal?: AbortSignal;
|
|
120
133
|
timeoutMs?: number;
|
|
121
134
|
fetch?: FetchImpl;
|
|
@@ -126,9 +139,9 @@ export async function searchParallel(
|
|
|
126
139
|
): Promise<SearchResponse> {
|
|
127
140
|
const numResults = clampNumResults(params.num_results, DEFAULT_NUM_RESULTS, MAX_NUM_RESULTS);
|
|
128
141
|
const parsed = params.parsedQuery ?? parseSearchQuery(params.query);
|
|
129
|
-
//
|
|
142
|
+
// Directives are removed only where Parallel has a native equivalent.
|
|
130
143
|
const query = parsed.hasDirectives ? formatQuery(parsed, PARALLEL_QUERY_SYNTAX) : params.query;
|
|
131
|
-
const sourcePolicy =
|
|
144
|
+
const sourcePolicy = toSourcePolicy(parsed, params.recency);
|
|
132
145
|
|
|
133
146
|
try {
|
|
134
147
|
const result = await searchWithAuthStorage(
|
|
@@ -174,6 +187,7 @@ export class ParallelProvider extends SearchProvider {
|
|
|
174
187
|
{
|
|
175
188
|
query: params.query,
|
|
176
189
|
num_results: params.numSearchResults ?? params.limit,
|
|
190
|
+
recency: params.recency,
|
|
177
191
|
signal: params.signal,
|
|
178
192
|
timeoutMs: params.timeoutMs,
|
|
179
193
|
fetch: params.fetch,
|