@oh-my-pi/pi-coding-agent 17.2.12 → 17.2.13
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 +57 -0
- package/dist/{CHANGELOG-k9ghy5sn.md → CHANGELOG-d8xh7keh.md} +57 -0
- package/dist/cli.js +3069 -3047
- 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 +4 -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 +2 -0
- package/dist/types/session/agent-session.d.ts +22 -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 +24 -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/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/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 +5 -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/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 +1 -1
- package/src/registry/persisted-agents.ts +43 -8
- package/src/sdk.ts +139 -8
- package/src/session/agent-session-types.ts +2 -0
- package/src/session/agent-session.ts +66 -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 +321 -184
- 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/fetch.ts +21 -2
- package/src/tools/image-gen.ts +6 -8
- 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
|
@@ -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,
|
|
@@ -150,6 +150,7 @@ interface PerplexityOAuthStreamEvent {
|
|
|
150
150
|
error_code?: string;
|
|
151
151
|
error_message?: string;
|
|
152
152
|
display_model?: string;
|
|
153
|
+
user_selected_model?: string;
|
|
153
154
|
uuid?: string;
|
|
154
155
|
}
|
|
155
156
|
|
|
@@ -327,7 +328,11 @@ export interface PerplexitySearchParams {
|
|
|
327
328
|
system_prompt?: string;
|
|
328
329
|
/** Pre-parsed view of `query` from the search pipeline; parsed locally when absent. */
|
|
329
330
|
parsedQuery?: StructuredQuery;
|
|
331
|
+
/** Direct API model. Defaults to `PI_PERPLEXITY_API_MODEL`, then `sonar-pro`. */
|
|
332
|
+
api_model?: string;
|
|
330
333
|
search_recency_filter?: "hour" | "day" | "week" | "month" | "year";
|
|
334
|
+
/** Consumer subscription model preference. Defaults to `PI_PERPLEXITY_MODEL`, then Sonar (`experimental`). */
|
|
335
|
+
subscription_model?: string;
|
|
331
336
|
num_results?: number;
|
|
332
337
|
/** Maximum output tokens. Defaults to 8192. */
|
|
333
338
|
max_tokens?: number;
|
|
@@ -539,6 +544,15 @@ async function callPerplexityApi(
|
|
|
539
544
|
return parseStreamedApiResponse(message, metadata);
|
|
540
545
|
}
|
|
541
546
|
|
|
547
|
+
function oauthSourceKey(url: string): string {
|
|
548
|
+
const trimmed = url.trim().replace(/\/$/, "");
|
|
549
|
+
try {
|
|
550
|
+
return new URL(trimmed).href.replace(/\/$/, "");
|
|
551
|
+
} catch {
|
|
552
|
+
return trimmed.toLowerCase();
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
542
556
|
function buildOAuthSources(event: PerplexityOAuthStreamEvent): SearchSource[] {
|
|
543
557
|
const results =
|
|
544
558
|
event.blocks?.find(block => block.intended_usage === "web_results")?.web_result_block?.web_results ?? [];
|
|
@@ -607,6 +621,7 @@ async function callPerplexityAsk(
|
|
|
607
621
|
params: PerplexitySearchParams,
|
|
608
622
|
filters: PerplexityNativeFilters,
|
|
609
623
|
): Promise<{ answer: string; sources: SearchSource[]; model?: string; requestId?: string }> {
|
|
624
|
+
const subscriptionModel = params.subscription_model?.trim() || $env.PI_PERPLEXITY_MODEL?.trim() || "experimental";
|
|
610
625
|
const requestId = crypto.randomUUID();
|
|
611
626
|
// The consumer `perplexity_ask` endpoint is itself a research assistant and
|
|
612
627
|
// has no system-message slot. Prepending the API-style system prompt to the
|
|
@@ -645,14 +660,14 @@ async function callPerplexityAsk(
|
|
|
645
660
|
query_str: effectiveQuery,
|
|
646
661
|
search_focus: "internet",
|
|
647
662
|
mode: "copilot",
|
|
648
|
-
model_preference:
|
|
663
|
+
model_preference: subscriptionModel,
|
|
649
664
|
sources: ["web"],
|
|
650
665
|
attachments: [],
|
|
651
666
|
frontend_uuid: crypto.randomUUID(),
|
|
652
667
|
frontend_context_uuid: crypto.randomUUID(),
|
|
653
668
|
version: OAUTH_API_VERSION,
|
|
654
669
|
language: "en-US",
|
|
655
|
-
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
670
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone ?? "UTC",
|
|
656
671
|
// Recency cannot be combined with absolute date filters; explicit
|
|
657
672
|
// before:/after: bounds take precedence.
|
|
658
673
|
search_recency_filter: filters.afterDate || filters.beforeDate ? null : (params.search_recency_filter ?? null),
|
|
@@ -740,12 +755,14 @@ async function callPerplexityAsk(
|
|
|
740
755
|
if (eventAnswer.length > 0) {
|
|
741
756
|
answer = eventAnswer;
|
|
742
757
|
}
|
|
743
|
-
|
|
744
758
|
for (const source of buildOAuthSources(mergedEvent)) {
|
|
745
|
-
sourcesByUrl.set(source.url, source);
|
|
759
|
+
sourcesByUrl.set(oauthSourceKey(source.url), source);
|
|
746
760
|
}
|
|
747
761
|
|
|
748
|
-
|
|
762
|
+
const reportedModel = [mergedEvent.user_selected_model, mergedEvent.display_model].find(
|
|
763
|
+
candidate => candidate && candidate !== "turbo",
|
|
764
|
+
);
|
|
765
|
+
if (reportedModel) model = reportedModel;
|
|
749
766
|
if (mergedEvent.uuid) finalRequestId = mergedEvent.uuid;
|
|
750
767
|
if (mergedEvent.final || mergedEvent.status === "COMPLETED") {
|
|
751
768
|
break;
|
|
@@ -755,7 +772,7 @@ async function callPerplexityAsk(
|
|
|
755
772
|
return {
|
|
756
773
|
answer,
|
|
757
774
|
sources: [...sourcesByUrl.values()],
|
|
758
|
-
model,
|
|
775
|
+
model: model ?? (auth.type === "anonymous" ? mergedEvent.display_model : subscriptionModel),
|
|
759
776
|
requestId: finalRequestId ?? requestId,
|
|
760
777
|
};
|
|
761
778
|
}
|
|
@@ -872,7 +889,7 @@ export async function searchPerplexity(params: PerplexitySearchParams): Promise<
|
|
|
872
889
|
messages.push({ role: "user", content: filters.query });
|
|
873
890
|
|
|
874
891
|
const request: PerplexityRequest = {
|
|
875
|
-
model: "sonar-pro",
|
|
892
|
+
model: params.api_model?.trim() || $env.PI_PERPLEXITY_API_MODEL?.trim() || "sonar-pro",
|
|
876
893
|
messages,
|
|
877
894
|
max_tokens: params.max_tokens ?? DEFAULT_MAX_TOKENS,
|
|
878
895
|
temperature: params.temperature ?? DEFAULT_TEMPERATURE,
|