@oh-my-pi/pi-coding-agent 15.1.8 → 15.2.1
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 +52 -1
- package/dist/types/cli/update-cli.d.ts +18 -0
- package/dist/types/config/settings-schema.d.ts +10 -0
- package/dist/types/eval/py/kernel.d.ts +6 -0
- package/dist/types/goals/state.d.ts +1 -1
- package/dist/types/goals/tools/goal-tool.d.ts +4 -0
- package/dist/types/hashline/parser.d.ts +6 -2
- package/dist/types/internal-urls/memory-protocol.d.ts +6 -0
- package/dist/types/main.d.ts +25 -1
- package/dist/types/modes/theme/shimmer.d.ts +27 -0
- package/dist/types/slash-commands/helpers/format.d.ts +4 -1
- package/dist/types/tools/ast-edit.d.ts +3 -0
- package/dist/types/tools/ast-grep.d.ts +3 -0
- package/dist/types/tools/find.d.ts +3 -0
- package/dist/types/tools/search.d.ts +3 -0
- package/dist/types/tui/file-list.d.ts +6 -0
- package/dist/types/tui/hyperlink.d.ts +42 -0
- package/dist/types/tui/index.d.ts +1 -0
- package/dist/types/utils/tool-choice.d.ts +2 -1
- package/dist/types/web/search/providers/utils.d.ts +27 -1
- package/package.json +7 -7
- package/src/cli/update-cli.ts +78 -36
- package/src/config/model-registry.ts +23 -12
- package/src/config/settings-schema.ts +12 -0
- package/src/config/settings.ts +28 -5
- package/src/edit/renderer.ts +5 -3
- package/src/eval/py/executor.ts +12 -1
- package/src/eval/py/kernel.ts +24 -8
- package/src/extensibility/plugins/legacy-pi-compat.ts +2 -2
- package/src/goals/runtime.ts +9 -3
- package/src/goals/state.ts +1 -1
- package/src/goals/tools/goal-tool.ts +12 -2
- package/src/hashline/diff.ts +1 -1
- package/src/hashline/execute.ts +2 -2
- package/src/hashline/parser.ts +87 -12
- package/src/internal-urls/memory-protocol.ts +1 -1
- package/src/main.ts +13 -2
- package/src/modes/interactive-mode.ts +29 -1
- package/src/modes/theme/shimmer.ts +79 -0
- package/src/prompts/agents/oracle.md +15 -16
- package/src/prompts/tools/goal.md +7 -2
- package/src/session/agent-session.ts +12 -75
- package/src/slash-commands/helpers/format.ts +23 -3
- package/src/task/executor.ts +115 -19
- package/src/tools/ast-edit.ts +39 -6
- package/src/tools/ast-grep.ts +38 -6
- package/src/tools/find.ts +13 -2
- package/src/tools/read.ts +46 -6
- package/src/tools/search.ts +447 -265
- package/src/tui/file-list.ts +10 -2
- package/src/tui/hyperlink.ts +126 -0
- package/src/tui/index.ts +1 -0
- package/src/utils/tool-choice.ts +7 -7
- package/src/web/kagi.ts +2 -2
- package/src/web/parallel.ts +3 -3
- package/src/web/search/index.ts +20 -9
- package/src/web/search/providers/anthropic.ts +4 -2
- package/src/web/search/providers/brave.ts +4 -2
- package/src/web/search/providers/codex.ts +4 -1
- package/src/web/search/providers/exa.ts +4 -1
- package/src/web/search/providers/gemini.ts +4 -1
- package/src/web/search/providers/jina.ts +4 -2
- package/src/web/search/providers/kagi.ts +5 -1
- package/src/web/search/providers/kimi.ts +4 -2
- package/src/web/search/providers/parallel.ts +5 -1
- package/src/web/search/providers/perplexity.ts +7 -2
- package/src/web/search/providers/searxng.ts +4 -1
- package/src/web/search/providers/synthetic.ts +4 -2
- package/src/web/search/providers/tavily.ts +4 -2
- package/src/web/search/providers/utils.ts +63 -1
- package/src/web/search/providers/zai.ts +4 -2
package/src/tui/file-list.ts
CHANGED
|
@@ -7,6 +7,9 @@ import { renderTreeList } from "./tree-list";
|
|
|
7
7
|
|
|
8
8
|
export interface FileEntry {
|
|
9
9
|
path: string;
|
|
10
|
+
/** Absolute filesystem path. When provided together with {@link FileListOptions.hyperlinkFn}, the
|
|
11
|
+
* rendered path text is wrapped in an OSC 8 hyperlink. */
|
|
12
|
+
absPath?: string;
|
|
10
13
|
isDirectory?: boolean;
|
|
11
14
|
meta?: string;
|
|
12
15
|
}
|
|
@@ -16,10 +19,13 @@ export interface FileListOptions {
|
|
|
16
19
|
expanded?: boolean;
|
|
17
20
|
maxCollapsed?: number;
|
|
18
21
|
showIcons?: boolean;
|
|
22
|
+
/** When provided, called with the entry's absolute path and the ANSI-styled display string to
|
|
23
|
+
* optionally wrap the path in an OSC 8 hyperlink. Only invoked when {@link FileEntry.absPath} is set. */
|
|
24
|
+
hyperlinkFn?: (absPath: string, displayText: string) => string;
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
export function renderFileList(options: FileListOptions, theme: Theme): string[] {
|
|
22
|
-
const { files, expanded = false, maxCollapsed = 8, showIcons = true } = options;
|
|
28
|
+
const { files, expanded = false, maxCollapsed = 8, showIcons = true, hyperlinkFn } = options;
|
|
23
29
|
|
|
24
30
|
return renderTreeList(
|
|
25
31
|
{
|
|
@@ -39,7 +45,9 @@ export function renderFileList(options: FileListOptions, theme: Theme): string[]
|
|
|
39
45
|
const labelColor = isDirectory ? "accent" : "toolOutput";
|
|
40
46
|
const meta = entry.meta ? ` ${theme.fg("dim", entry.meta)}` : "";
|
|
41
47
|
const iconPrefix = icon ? `${icon} ` : "";
|
|
42
|
-
|
|
48
|
+
const pathStr = theme.fg(labelColor, displayPath);
|
|
49
|
+
const linkedPath = entry.absPath && hyperlinkFn ? hyperlinkFn(entry.absPath, pathStr) : pathStr;
|
|
50
|
+
return `${iconPrefix}${linkedPath}${meta}`;
|
|
43
51
|
},
|
|
44
52
|
},
|
|
45
53
|
theme,
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OSC 8 terminal hyperlink support for file paths.
|
|
3
|
+
*
|
|
4
|
+
* Wraps display text in `ESC ] 8 ; id=HASH ; URI ESC \ TEXT ESC ] 8 ; ; ESC \`
|
|
5
|
+
* sequences when the active terminal supports hyperlinks and the user setting
|
|
6
|
+
* permits it. Falls back to plain text when disabled.
|
|
7
|
+
*/
|
|
8
|
+
import { TERMINAL } from "@oh-my-pi/pi-tui";
|
|
9
|
+
import { settings } from "../config/settings";
|
|
10
|
+
import {
|
|
11
|
+
LocalProtocolHandler,
|
|
12
|
+
memoryRootsFromRegistry,
|
|
13
|
+
parseInternalUrl,
|
|
14
|
+
resolveLocalUrlToPath,
|
|
15
|
+
resolveMemoryUrlToPath,
|
|
16
|
+
} from "../internal-urls";
|
|
17
|
+
|
|
18
|
+
const OSC = "\x1b]";
|
|
19
|
+
const ST = "\x1b\\";
|
|
20
|
+
|
|
21
|
+
/** Stable 8-char hex ID derived from a URI — hints terminals to coalesce identical adjacent links. */
|
|
22
|
+
function buildLinkId(uri: string): string {
|
|
23
|
+
let h = 0;
|
|
24
|
+
for (let i = 0; i < uri.length; i++) {
|
|
25
|
+
// FNV-1a-inspired mix — good enough for a UI hint, no deps
|
|
26
|
+
h = (Math.imul(31, h) + uri.charCodeAt(i)) | 0;
|
|
27
|
+
}
|
|
28
|
+
return (h >>> 0).toString(16).padStart(8, "0");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Build a `file://` URI for an absolute path with optional line/col query params. */
|
|
32
|
+
function buildFileUri(absPath: string, opts?: { line?: number; col?: number }): string {
|
|
33
|
+
// Normalize backslashes for Windows paths before constructing the URL.
|
|
34
|
+
const normalized = absPath.replaceAll("\\", "/");
|
|
35
|
+
const prefix = normalized.startsWith("/") ? "file://" : "file:///";
|
|
36
|
+
// Split on slashes, encode each component, reassemble.
|
|
37
|
+
const encoded = normalized
|
|
38
|
+
.split("/")
|
|
39
|
+
.map(segment => encodeURIComponent(segment))
|
|
40
|
+
.join("/");
|
|
41
|
+
const params: string[] = [];
|
|
42
|
+
if (opts?.line !== undefined) params.push(`line=${opts.line}`);
|
|
43
|
+
if (opts?.col !== undefined) params.push(`col=${opts.col}`);
|
|
44
|
+
const query = params.length > 0 ? `?${params.join("&")}` : "";
|
|
45
|
+
return `${prefix}${encoded}${query}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Returns true when OSC 8 hyperlinks should be emitted.
|
|
50
|
+
*
|
|
51
|
+
* Respects `tui.hyperlinks` setting:
|
|
52
|
+
* - `"off"`: never
|
|
53
|
+
* - `"auto"`: when `process.stdout.isTTY`, `NO_COLOR` is unset, and the detected terminal reports hyperlink support
|
|
54
|
+
* - `"always"`: unconditionally (useful for viewers that support OSC 8 without advertising it)
|
|
55
|
+
*/
|
|
56
|
+
export function isHyperlinkEnabled(): boolean {
|
|
57
|
+
const mode = settings.get("tui.hyperlinks");
|
|
58
|
+
if (mode === "off") return false;
|
|
59
|
+
if (mode === "always") return true;
|
|
60
|
+
// auto: respect terminal capabilities and NO_COLOR
|
|
61
|
+
if (Bun.env.NO_COLOR) return false;
|
|
62
|
+
if (!process.stdout.isTTY) return false;
|
|
63
|
+
return TERMINAL.hyperlinks;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Wrap `displayText` in an OSC 8 hyperlink pointing at the given absolute file path.
|
|
68
|
+
*
|
|
69
|
+
* Returns `displayText` unchanged when hyperlinks are disabled or when
|
|
70
|
+
* the text already contains an OSC 8 sequence (prevents double-wrapping).
|
|
71
|
+
*
|
|
72
|
+
* The caller is responsible for passing an absolute path. Relative paths
|
|
73
|
+
* produce invalid `file://` URIs and are accepted silently to avoid runtime
|
|
74
|
+
* errors in renderer hot paths.
|
|
75
|
+
*
|
|
76
|
+
* @param absPath - Absolute filesystem path
|
|
77
|
+
* @param displayText - Text to render as the hyperlink anchor (may contain ANSI codes)
|
|
78
|
+
* @param opts - Optional line/col position appended as `?line=N&col=M` query params
|
|
79
|
+
*/
|
|
80
|
+
export function fileHyperlink(absPath: string, displayText: string, opts?: { line?: number; col?: number }): string {
|
|
81
|
+
if (!isHyperlinkEnabled()) return displayText;
|
|
82
|
+
// Do not double-wrap if the text already embeds an OSC 8 sequence.
|
|
83
|
+
if (displayText.includes("\x1b]8;")) return displayText;
|
|
84
|
+
const uri = buildFileUri(absPath, opts);
|
|
85
|
+
const id = buildLinkId(uri);
|
|
86
|
+
return `${OSC}8;id=${id};${uri}${ST}${displayText}${OSC}8;;${ST}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Synchronously resolve a filesystem-backed internal URL (e.g. `local://foo.md`,
|
|
91
|
+
* `memory://root/notes.md`) to its absolute filesystem path. Returns `undefined`
|
|
92
|
+
* for inputs that aren't fs-backed, aren't resolvable in the current session
|
|
93
|
+
* registry, or fail to parse.
|
|
94
|
+
*
|
|
95
|
+
* Used by renderers to wrap fs-backed internal URLs in OSC 8 hyperlinks even
|
|
96
|
+
* when the resolved path isn't yet available from tool result details (e.g.
|
|
97
|
+
* during the call/streaming phase before a result lands).
|
|
98
|
+
*
|
|
99
|
+
* Async-resolved schemes (`artifact://`, `agent://`, `skill://`, `rule://`,
|
|
100
|
+
* `omp://`) are not handled here — those rely on `details.resolvedPath` set
|
|
101
|
+
* by the read tool's router resolution.
|
|
102
|
+
*/
|
|
103
|
+
export function tryResolveInternalUrlSync(input: string): string | undefined {
|
|
104
|
+
try {
|
|
105
|
+
if (input.startsWith("local://")) {
|
|
106
|
+
const opts = LocalProtocolHandler.resolveOptions();
|
|
107
|
+
if (!opts) return undefined;
|
|
108
|
+
return resolveLocalUrlToPath(input, opts);
|
|
109
|
+
}
|
|
110
|
+
if (input.startsWith("memory://")) {
|
|
111
|
+
const url = parseInternalUrl(input);
|
|
112
|
+
const roots = memoryRootsFromRegistry();
|
|
113
|
+
for (const root of roots) {
|
|
114
|
+
try {
|
|
115
|
+
return resolveMemoryUrlToPath(url, root);
|
|
116
|
+
} catch {
|
|
117
|
+
// Try the next root; some sessions may not have this namespace mounted.
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return undefined;
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
package/src/tui/index.ts
CHANGED
package/src/utils/tool-choice.ts
CHANGED
|
@@ -2,7 +2,8 @@ import type { Api, Model, ToolChoice } from "@oh-my-pi/pi-ai";
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Build a provider-aware tool choice that targets one specific tool when supported.
|
|
5
|
-
*
|
|
5
|
+
* Providers that only expose required/any forcing may still honor named choices by
|
|
6
|
+
* narrowing their request tool list before transport.
|
|
6
7
|
*/
|
|
7
8
|
export function buildNamedToolChoice(toolName: string, model?: Model<Api>): ToolChoice | undefined {
|
|
8
9
|
if (!model) return undefined;
|
|
@@ -20,12 +21,11 @@ export function buildNamedToolChoice(toolName: string, model?: Model<Api>): Tool
|
|
|
20
21
|
return { type: "function", name: toolName };
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
if (
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
) {
|
|
24
|
+
if (model.api === "ollama-chat") {
|
|
25
|
+
return { type: "function", name: toolName };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (model.api === "google-generative-ai" || model.api === "google-gemini-cli" || model.api === "google-vertex") {
|
|
29
29
|
return "required";
|
|
30
30
|
}
|
|
31
31
|
|
package/src/web/kagi.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getEnvApiKey } from "@oh-my-pi/pi-ai";
|
|
2
|
-
import { findCredential } from "./search/providers/utils";
|
|
2
|
+
import { findCredential, withHardTimeout } from "./search/providers/utils";
|
|
3
3
|
|
|
4
4
|
const KAGI_SEARCH_URL = "https://kagi.com/api/v0/search";
|
|
5
5
|
|
|
@@ -138,7 +138,7 @@ export async function searchWithKagi(query: string, options: KagiSearchOptions =
|
|
|
138
138
|
|
|
139
139
|
const response = await fetch(requestUrl, {
|
|
140
140
|
headers: getAuthHeaders(apiKey),
|
|
141
|
-
signal: options.signal,
|
|
141
|
+
signal: withHardTimeout(options.signal),
|
|
142
142
|
});
|
|
143
143
|
if (!response.ok) {
|
|
144
144
|
throw parseKagiErrorResponse(response.status, await response.text());
|
package/src/web/parallel.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getEnvApiKey } from "@oh-my-pi/pi-ai";
|
|
2
|
-
import { findCredential } from "./search/providers/utils";
|
|
2
|
+
import { findCredential, withHardTimeout } from "./search/providers/utils";
|
|
3
3
|
|
|
4
4
|
const PARALLEL_API_URL = "https://api.parallel.ai";
|
|
5
5
|
const PARALLEL_SEARCH_URL = `${PARALLEL_API_URL}/v1beta/search`;
|
|
@@ -304,7 +304,7 @@ export async function searchWithParallel(
|
|
|
304
304
|
max_chars_per_result: options.maxCharsPerResult ?? 10_000,
|
|
305
305
|
},
|
|
306
306
|
}),
|
|
307
|
-
signal: options.signal,
|
|
307
|
+
signal: withHardTimeout(options.signal),
|
|
308
308
|
});
|
|
309
309
|
if (!response.ok) {
|
|
310
310
|
throw parseParallelErrorResponse(response.status, await response.text());
|
|
@@ -335,7 +335,7 @@ export async function extractWithParallel(
|
|
|
335
335
|
excerpts: options.excerpts ?? true,
|
|
336
336
|
full_content: options.fullContent ?? false,
|
|
337
337
|
}),
|
|
338
|
-
signal: options.signal,
|
|
338
|
+
signal: withHardTimeout(options.signal),
|
|
339
339
|
});
|
|
340
340
|
if (!response.ok) {
|
|
341
341
|
throw parseParallelErrorResponse(response.status, await response.text());
|
package/src/web/search/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import webSearchSystemPrompt from "../../prompts/system/web-search.md" with { ty
|
|
|
14
14
|
import webSearchDescription from "../../prompts/tools/web-search.md" with { type: "text" };
|
|
15
15
|
import type { ToolSession } from "../../tools";
|
|
16
16
|
import { formatAge } from "../../tools/render-utils";
|
|
17
|
+
import { throwIfAborted } from "../../tools/tool-errors";
|
|
17
18
|
import { getSearchProvider, getSearchProviderLabel, resolveProviderChain, type SearchProvider } from "./provider";
|
|
18
19
|
import { renderSearchCall, renderSearchResult, type SearchRenderDetails } from "./render";
|
|
19
20
|
import type { SearchProviderId, SearchResponse } from "./types";
|
|
@@ -35,10 +36,6 @@ export interface SearchQueryParams extends SearchToolParams {
|
|
|
35
36
|
provider?: SearchProviderId | "auto";
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
function formatProviderList(providers: SearchProvider[]): string {
|
|
39
|
-
return providers.map(provider => provider.label).join(", ");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
39
|
function formatProviderError(error: unknown, provider: SearchProvider): string {
|
|
43
40
|
if (error instanceof SearchProviderError) {
|
|
44
41
|
if (error.provider === "anthropic" && error.status === 404) {
|
|
@@ -137,9 +134,8 @@ async function executeSearch(
|
|
|
137
134
|
};
|
|
138
135
|
}
|
|
139
136
|
|
|
140
|
-
|
|
137
|
+
const failures: Array<{ provider: SearchProvider; error: unknown }> = [];
|
|
141
138
|
let lastProvider = providers[0];
|
|
142
|
-
|
|
143
139
|
for (const provider of providers) {
|
|
144
140
|
lastProvider = provider;
|
|
145
141
|
try {
|
|
@@ -161,14 +157,29 @@ async function executeSearch(
|
|
|
161
157
|
details: { response },
|
|
162
158
|
};
|
|
163
159
|
} catch (error) {
|
|
164
|
-
|
|
160
|
+
// Surface user-initiated cancellation immediately so the session sees
|
|
161
|
+
// a clean abort instead of a generic "all providers failed" message.
|
|
162
|
+
// Without this, an AbortError from `fetch()` is treated as a provider
|
|
163
|
+
// failure and the loop falls through to the next provider (or to the
|
|
164
|
+
// summary error), masking the cancellation.
|
|
165
|
+
throwIfAborted(signal);
|
|
166
|
+
failures.push({ provider, error });
|
|
165
167
|
}
|
|
166
168
|
}
|
|
167
169
|
|
|
168
|
-
const
|
|
170
|
+
const lastFailure = failures[failures.length - 1];
|
|
171
|
+
const baseMessage = lastFailure
|
|
172
|
+
? formatProviderError(lastFailure.error, lastFailure.provider)
|
|
173
|
+
: `Unknown error from ${lastProvider.label}`;
|
|
169
174
|
const message =
|
|
170
175
|
providers.length > 1
|
|
171
|
-
? `All web search providers failed
|
|
176
|
+
? `All web search providers failed: ${failures
|
|
177
|
+
.map(f =>
|
|
178
|
+
f.error instanceof SearchProviderError
|
|
179
|
+
? f.error.message
|
|
180
|
+
: `${f.provider.id}: ${formatProviderError(f.error, f.provider)}`,
|
|
181
|
+
)
|
|
182
|
+
.join("; ")}`
|
|
172
183
|
: baseMessage;
|
|
173
184
|
|
|
174
185
|
return {
|
|
@@ -24,12 +24,12 @@ import type {
|
|
|
24
24
|
import { SearchProviderError } from "../../../web/search/types";
|
|
25
25
|
import type { SearchParams } from "./base";
|
|
26
26
|
import { SearchProvider } from "./base";
|
|
27
|
+
import { classifyProviderHttpError, withHardTimeout } from "./utils";
|
|
27
28
|
|
|
28
29
|
const DEFAULT_MODEL = "claude-haiku-4-5";
|
|
29
30
|
const DEFAULT_MAX_TOKENS = 4096;
|
|
30
31
|
const WEB_SEARCH_TOOL_NAME = "web_search";
|
|
31
32
|
const WEB_SEARCH_TOOL_TYPE = "web_search_20250305";
|
|
32
|
-
|
|
33
33
|
export interface AnthropicSearchParams {
|
|
34
34
|
query: string;
|
|
35
35
|
system_prompt?: string;
|
|
@@ -118,11 +118,13 @@ async function callSearch(
|
|
|
118
118
|
method: "POST",
|
|
119
119
|
headers,
|
|
120
120
|
body: JSON.stringify(body),
|
|
121
|
-
signal,
|
|
121
|
+
signal: withHardTimeout(signal),
|
|
122
122
|
});
|
|
123
123
|
|
|
124
124
|
if (!response.ok) {
|
|
125
125
|
const errorText = await response.text();
|
|
126
|
+
const classified = classifyProviderHttpError("anthropic", response.status, errorText);
|
|
127
|
+
if (classified) throw classified;
|
|
126
128
|
throw new SearchProviderError(
|
|
127
129
|
"anthropic",
|
|
128
130
|
`Anthropic API error (${response.status}): ${errorText}`,
|
|
@@ -10,7 +10,7 @@ import { SearchProviderError } from "../../../web/search/types";
|
|
|
10
10
|
import { clampNumResults, dateToAgeSeconds } from "../utils";
|
|
11
11
|
import type { SearchParams } from "./base";
|
|
12
12
|
import { SearchProvider } from "./base";
|
|
13
|
-
import { isApiKeyAvailable } from "./utils";
|
|
13
|
+
import { classifyProviderHttpError, isApiKeyAvailable, withHardTimeout } from "./utils";
|
|
14
14
|
|
|
15
15
|
const BRAVE_SEARCH_URL = "https://api.search.brave.com/res/v1/web/search";
|
|
16
16
|
const DEFAULT_NUM_RESULTS = 10;
|
|
@@ -85,11 +85,13 @@ async function callBraveSearch(
|
|
|
85
85
|
Accept: "application/json",
|
|
86
86
|
"X-Subscription-Token": apiKey,
|
|
87
87
|
},
|
|
88
|
-
signal: params.signal,
|
|
88
|
+
signal: withHardTimeout(params.signal),
|
|
89
89
|
});
|
|
90
90
|
|
|
91
91
|
if (!response.ok) {
|
|
92
92
|
const errorText = await response.text();
|
|
93
|
+
const classified = classifyProviderHttpError("brave", response.status, errorText);
|
|
94
|
+
if (classified) throw classified;
|
|
93
95
|
throw new SearchProviderError("brave", `Brave API error (${response.status}): ${errorText}`, response.status);
|
|
94
96
|
}
|
|
95
97
|
|
|
@@ -15,6 +15,7 @@ import type { SearchResponse, SearchSource } from "../../../web/search/types";
|
|
|
15
15
|
import { SearchProviderError } from "../../../web/search/types";
|
|
16
16
|
import type { SearchParams } from "./base";
|
|
17
17
|
import { SearchProvider } from "./base";
|
|
18
|
+
import { classifyProviderHttpError, withHardTimeout } from "./utils";
|
|
18
19
|
|
|
19
20
|
const CODEX_BASE_URL = "https://chatgpt.com/backend-api";
|
|
20
21
|
const CODEX_RESPONSES_PATH = "/codex/responses";
|
|
@@ -338,11 +339,13 @@ async function callCodexSearch(
|
|
|
338
339
|
method: "POST",
|
|
339
340
|
headers,
|
|
340
341
|
body: JSON.stringify(body),
|
|
341
|
-
signal: options.signal,
|
|
342
|
+
signal: withHardTimeout(options.signal),
|
|
342
343
|
});
|
|
343
344
|
|
|
344
345
|
if (!response.ok) {
|
|
345
346
|
const errorText = await response.text();
|
|
347
|
+
const classified = classifyProviderHttpError("codex", response.status, errorText);
|
|
348
|
+
if (classified) throw classified;
|
|
346
349
|
throw new SearchProviderError("codex", `Codex API error (${response.status}): ${errorText}`, response.status);
|
|
347
350
|
}
|
|
348
351
|
|
|
@@ -14,6 +14,7 @@ import { SearchProviderError } from "../../../web/search/types";
|
|
|
14
14
|
import { dateToAgeSeconds } from "../utils";
|
|
15
15
|
import type { SearchParams } from "./base";
|
|
16
16
|
import { SearchProvider } from "./base";
|
|
17
|
+
import { classifyProviderHttpError, withHardTimeout } from "./utils";
|
|
17
18
|
|
|
18
19
|
const EXA_API_URL = "https://api.exa.ai/search";
|
|
19
20
|
|
|
@@ -180,11 +181,13 @@ async function callExaSearch(apiKey: string, params: ExaSearchParams): Promise<E
|
|
|
180
181
|
"x-api-key": apiKey,
|
|
181
182
|
},
|
|
182
183
|
body: JSON.stringify(body),
|
|
183
|
-
signal: params.signal,
|
|
184
|
+
signal: withHardTimeout(params.signal),
|
|
184
185
|
});
|
|
185
186
|
|
|
186
187
|
if (!response.ok) {
|
|
187
188
|
const errorText = await response.text();
|
|
189
|
+
const classified = classifyProviderHttpError("exa", response.status, errorText);
|
|
190
|
+
if (classified) throw classified;
|
|
188
191
|
throw new SearchProviderError("exa", `Exa API error (${response.status}): ${errorText}`, response.status);
|
|
189
192
|
}
|
|
190
193
|
|
|
@@ -15,6 +15,7 @@ import type { SearchCitation, SearchResponse, SearchSource } from "../../../web/
|
|
|
15
15
|
import { SearchProviderError } from "../../../web/search/types";
|
|
16
16
|
import type { SearchParams } from "./base";
|
|
17
17
|
import { SearchProvider } from "./base";
|
|
18
|
+
import { classifyProviderHttpError, withHardTimeout } from "./utils";
|
|
18
19
|
|
|
19
20
|
const DEFAULT_ENDPOINT = "https://cloudcode-pa.googleapis.com";
|
|
20
21
|
const ANTIGRAVITY_DAILY_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
|
|
@@ -310,7 +311,7 @@ async function callGeminiSearch(
|
|
|
310
311
|
...headers,
|
|
311
312
|
},
|
|
312
313
|
body: JSON.stringify(requestBody),
|
|
313
|
-
signal,
|
|
314
|
+
signal: withHardTimeout(signal),
|
|
314
315
|
});
|
|
315
316
|
const urlFor = (attempt: number) =>
|
|
316
317
|
`${endpoints[Math.min(attempt, endpoints.length - 1)]}/v1internal:streamGenerateContent?alt=sse`;
|
|
@@ -340,6 +341,8 @@ async function callGeminiSearch(
|
|
|
340
341
|
|
|
341
342
|
if (!response.ok) {
|
|
342
343
|
const errorText = await response.text();
|
|
344
|
+
const classified = classifyProviderHttpError("gemini", response.status, errorText);
|
|
345
|
+
if (classified) throw classified;
|
|
343
346
|
throw new SearchProviderError(
|
|
344
347
|
"gemini",
|
|
345
348
|
`Gemini Cloud Code API error (${response.status}): ${errorText}`,
|
|
@@ -10,7 +10,7 @@ import type { SearchResponse, SearchSource } from "../../../web/search/types";
|
|
|
10
10
|
import { SearchProviderError } from "../../../web/search/types";
|
|
11
11
|
import type { SearchParams } from "./base";
|
|
12
12
|
import { SearchProvider } from "./base";
|
|
13
|
-
import { isApiKeyAvailable } from "./utils";
|
|
13
|
+
import { classifyProviderHttpError, isApiKeyAvailable, withHardTimeout } from "./utils";
|
|
14
14
|
|
|
15
15
|
const JINA_SEARCH_URL = "https://s.jina.ai";
|
|
16
16
|
|
|
@@ -41,11 +41,13 @@ async function callJinaSearch(apiKey: string, query: string, signal?: AbortSigna
|
|
|
41
41
|
Accept: "application/json",
|
|
42
42
|
Authorization: `Bearer ${apiKey}`,
|
|
43
43
|
},
|
|
44
|
-
signal,
|
|
44
|
+
signal: withHardTimeout(signal),
|
|
45
45
|
});
|
|
46
46
|
|
|
47
47
|
if (!response.ok) {
|
|
48
48
|
const errorText = await response.text();
|
|
49
|
+
const classified = classifyProviderHttpError("jina", response.status, errorText);
|
|
50
|
+
if (classified) throw classified;
|
|
49
51
|
throw new SearchProviderError("jina", `Jina API error (${response.status}): ${errorText}`, response.status);
|
|
50
52
|
}
|
|
51
53
|
|
|
@@ -9,7 +9,7 @@ import { findKagiApiKey, KagiApiError, searchWithKagi } from "../../kagi";
|
|
|
9
9
|
import { clampNumResults } from "../utils";
|
|
10
10
|
import type { SearchParams } from "./base";
|
|
11
11
|
import { SearchProvider } from "./base";
|
|
12
|
-
import { toSearchSources } from "./utils";
|
|
12
|
+
import { classifyProviderHttpError, toSearchSources } from "./utils";
|
|
13
13
|
|
|
14
14
|
const DEFAULT_NUM_RESULTS = 10;
|
|
15
15
|
const MAX_NUM_RESULTS = 40;
|
|
@@ -36,6 +36,10 @@ export async function searchKagi(params: {
|
|
|
36
36
|
};
|
|
37
37
|
} catch (err) {
|
|
38
38
|
if (err instanceof KagiApiError) {
|
|
39
|
+
if (typeof err.statusCode === "number") {
|
|
40
|
+
const classified = classifyProviderHttpError("kagi", err.statusCode, err.message);
|
|
41
|
+
if (classified) throw classified;
|
|
42
|
+
}
|
|
39
43
|
throw new SearchProviderError("kagi", err.message, err.statusCode);
|
|
40
44
|
}
|
|
41
45
|
throw err;
|
|
@@ -11,7 +11,7 @@ import { SearchProviderError } from "../../../web/search/types";
|
|
|
11
11
|
import { clampNumResults, dateToAgeSeconds } from "../utils";
|
|
12
12
|
import type { SearchParams } from "./base";
|
|
13
13
|
import { SearchProvider } from "./base";
|
|
14
|
-
import { findCredential, isApiKeyAvailable } from "./utils";
|
|
14
|
+
import { classifyProviderHttpError, findCredential, isApiKeyAvailable, withHardTimeout } from "./utils";
|
|
15
15
|
|
|
16
16
|
const KIMI_SEARCH_URL = "https://api.kimi.com/coding/v1/search";
|
|
17
17
|
|
|
@@ -78,11 +78,13 @@ async function callKimiSearch(
|
|
|
78
78
|
enable_page_crawling: params.includeContent,
|
|
79
79
|
timeout_seconds: DEFAULT_TIMEOUT_SECONDS,
|
|
80
80
|
}),
|
|
81
|
-
signal: params.signal,
|
|
81
|
+
signal: withHardTimeout(params.signal),
|
|
82
82
|
});
|
|
83
83
|
|
|
84
84
|
if (!response.ok) {
|
|
85
85
|
const errorText = await response.text();
|
|
86
|
+
const classified = classifyProviderHttpError("kimi", response.status, errorText);
|
|
87
|
+
if (classified) throw classified;
|
|
86
88
|
throw new SearchProviderError(
|
|
87
89
|
"kimi",
|
|
88
90
|
`Kimi search API error (${response.status}): ${errorText}`,
|
|
@@ -4,7 +4,7 @@ import { findParallelApiKey, ParallelApiError, searchWithParallel } from "../../
|
|
|
4
4
|
import { clampNumResults } from "../utils";
|
|
5
5
|
import type { SearchParams } from "./base";
|
|
6
6
|
import { SearchProvider } from "./base";
|
|
7
|
-
import { toSearchSources } from "./utils";
|
|
7
|
+
import { classifyProviderHttpError, toSearchSources } from "./utils";
|
|
8
8
|
|
|
9
9
|
const DEFAULT_NUM_RESULTS = 10;
|
|
10
10
|
const MAX_NUM_RESULTS = 40;
|
|
@@ -30,6 +30,10 @@ export async function searchParallel(params: {
|
|
|
30
30
|
};
|
|
31
31
|
} catch (err) {
|
|
32
32
|
if (err instanceof ParallelApiError) {
|
|
33
|
+
if (typeof err.statusCode === "number") {
|
|
34
|
+
const classified = classifyProviderHttpError("parallel", err.statusCode, err.message);
|
|
35
|
+
if (classified) throw classified;
|
|
36
|
+
}
|
|
33
37
|
throw new SearchProviderError("parallel", err.message, err.statusCode);
|
|
34
38
|
}
|
|
35
39
|
throw err;
|
|
@@ -22,6 +22,7 @@ import { SearchProviderError } from "../../../web/search/types";
|
|
|
22
22
|
import { dateToAgeSeconds } from "../utils";
|
|
23
23
|
import type { SearchParams } from "./base";
|
|
24
24
|
import { SearchProvider } from "./base";
|
|
25
|
+
import { classifyProviderHttpError, withHardTimeout } from "./utils";
|
|
25
26
|
|
|
26
27
|
const PERPLEXITY_API_URL = "https://api.perplexity.ai/chat/completions";
|
|
27
28
|
const PERPLEXITY_OAUTH_ASK_URL = "https://www.perplexity.ai/rest/sse/perplexity_ask";
|
|
@@ -247,11 +248,13 @@ async function callPerplexityApi(
|
|
|
247
248
|
"Content-Type": "application/json",
|
|
248
249
|
},
|
|
249
250
|
body: JSON.stringify(request),
|
|
250
|
-
signal,
|
|
251
|
+
signal: withHardTimeout(signal),
|
|
251
252
|
});
|
|
252
253
|
|
|
253
254
|
if (!response.ok) {
|
|
254
255
|
const errorText = await response.text();
|
|
256
|
+
const classified = classifyProviderHttpError("perplexity", response.status, errorText);
|
|
257
|
+
if (classified) throw classified;
|
|
255
258
|
throw new SearchProviderError(
|
|
256
259
|
"perplexity",
|
|
257
260
|
`Perplexity API error (${response.status}): ${errorText}`,
|
|
@@ -364,11 +367,13 @@ async function callPerplexityOAuth(
|
|
|
364
367
|
skip_search_enabled: true,
|
|
365
368
|
},
|
|
366
369
|
}),
|
|
367
|
-
signal: params.signal,
|
|
370
|
+
signal: withHardTimeout(params.signal),
|
|
368
371
|
});
|
|
369
372
|
|
|
370
373
|
if (!response.ok) {
|
|
371
374
|
const errorText = await response.text();
|
|
375
|
+
const classified = classifyProviderHttpError("perplexity", response.status, errorText);
|
|
376
|
+
if (classified) throw classified;
|
|
372
377
|
throw new SearchProviderError(
|
|
373
378
|
"perplexity",
|
|
374
379
|
`Perplexity OAuth API error (${response.status}): ${errorText}`,
|
|
@@ -31,6 +31,7 @@ import { SearchProviderError } from "../../../web/search/types";
|
|
|
31
31
|
import { clampNumResults, dateToAgeSeconds } from "../utils";
|
|
32
32
|
import type { SearchParams } from "./base";
|
|
33
33
|
import { SearchProvider } from "./base";
|
|
34
|
+
import { classifyProviderHttpError, withHardTimeout } from "./utils";
|
|
34
35
|
|
|
35
36
|
const DEFAULT_NUM_RESULTS = 10;
|
|
36
37
|
const MAX_NUM_RESULTS = 20;
|
|
@@ -211,11 +212,13 @@ async function callSearXNGSearch(
|
|
|
211
212
|
|
|
212
213
|
const response = await fetch(url, {
|
|
213
214
|
headers,
|
|
214
|
-
signal: params.signal,
|
|
215
|
+
signal: withHardTimeout(params.signal),
|
|
215
216
|
});
|
|
216
217
|
|
|
217
218
|
if (!response.ok) {
|
|
218
219
|
const errorText = await response.text();
|
|
220
|
+
const classified = classifyProviderHttpError("searxng", response.status, errorText);
|
|
221
|
+
if (classified) throw classified;
|
|
219
222
|
throw new SearchProviderError("searxng", `SearXNG API error (${response.status}): ${errorText}`, response.status);
|
|
220
223
|
}
|
|
221
224
|
|
|
@@ -10,7 +10,7 @@ import type { SearchResponse, SearchSource } from "../../../web/search/types";
|
|
|
10
10
|
import { SearchProviderError } from "../../../web/search/types";
|
|
11
11
|
import type { SearchParams } from "./base";
|
|
12
12
|
import { SearchProvider } from "./base";
|
|
13
|
-
import { findCredential, isApiKeyAvailable } from "./utils";
|
|
13
|
+
import { classifyProviderHttpError, findCredential, isApiKeyAvailable, withHardTimeout } from "./utils";
|
|
14
14
|
|
|
15
15
|
const SYNTHETIC_SEARCH_URL = "https://api.synthetic.new/v2/search";
|
|
16
16
|
|
|
@@ -43,11 +43,13 @@ async function callSyntheticSearch(
|
|
|
43
43
|
Authorization: `Bearer ${apiKey}`,
|
|
44
44
|
},
|
|
45
45
|
body: JSON.stringify({ query }),
|
|
46
|
-
signal,
|
|
46
|
+
signal: withHardTimeout(signal),
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
if (!response.ok) {
|
|
50
50
|
const errorText = await response.text();
|
|
51
|
+
const classified = classifyProviderHttpError("synthetic", response.status, errorText);
|
|
52
|
+
if (classified) throw classified;
|
|
51
53
|
throw new SearchProviderError(
|
|
52
54
|
"synthetic",
|
|
53
55
|
`Synthetic API error (${response.status}): ${errorText}`,
|
|
@@ -10,7 +10,7 @@ import { SearchProviderError } from "../../../web/search/types";
|
|
|
10
10
|
import { clampNumResults, dateToAgeSeconds } from "../utils";
|
|
11
11
|
import type { SearchParams } from "./base";
|
|
12
12
|
import { SearchProvider } from "./base";
|
|
13
|
-
import { findCredential, isApiKeyAvailable } from "./utils";
|
|
13
|
+
import { classifyProviderHttpError, findCredential, isApiKeyAvailable, withHardTimeout } from "./utils";
|
|
14
14
|
|
|
15
15
|
const TAVILY_SEARCH_URL = "https://api.tavily.com/search";
|
|
16
16
|
const DEFAULT_NUM_RESULTS = 5;
|
|
@@ -92,11 +92,13 @@ async function callTavilySearch(apiKey: string, params: TavilySearchParams): Pro
|
|
|
92
92
|
Authorization: `Bearer ${apiKey}`,
|
|
93
93
|
},
|
|
94
94
|
body: JSON.stringify(buildRequestBody(params)),
|
|
95
|
-
signal: params.signal,
|
|
95
|
+
signal: withHardTimeout(params.signal),
|
|
96
96
|
});
|
|
97
97
|
|
|
98
98
|
if (!response.ok) {
|
|
99
99
|
const errorText = await response.text();
|
|
100
|
+
const classified = classifyProviderHttpError("tavily", response.status, errorText);
|
|
101
|
+
if (classified) throw classified;
|
|
100
102
|
let message = errorText.trim();
|
|
101
103
|
if (message.length === 0) {
|
|
102
104
|
message = response.statusText;
|