@oh-my-pi/pi-coding-agent 15.1.9 → 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 +30 -1
- 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/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/web/search/providers/utils.d.ts +2 -1
- package/package.json +7 -7
- 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/modes/interactive-mode.ts +29 -1
- package/src/modes/theme/shimmer.ts +79 -0
- 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/web/search/index.ts +13 -9
- package/src/web/search/providers/anthropic.ts +3 -1
- package/src/web/search/providers/brave.ts +3 -1
- package/src/web/search/providers/codex.ts +3 -1
- package/src/web/search/providers/exa.ts +3 -1
- package/src/web/search/providers/gemini.ts +3 -1
- package/src/web/search/providers/jina.ts +3 -1
- package/src/web/search/providers/kagi.ts +5 -1
- package/src/web/search/providers/kimi.ts +3 -1
- package/src/web/search/providers/parallel.ts +5 -1
- package/src/web/search/providers/perplexity.ts +5 -1
- package/src/web/search/providers/searxng.ts +3 -1
- package/src/web/search/providers/synthetic.ts +3 -1
- package/src/web/search/providers/tavily.ts +3 -1
- package/src/web/search/providers/utils.ts +33 -1
- package/src/web/search/providers/zai.ts +3 -1
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/web/search/index.ts
CHANGED
|
@@ -36,10 +36,6 @@ export interface SearchQueryParams extends SearchToolParams {
|
|
|
36
36
|
provider?: SearchProviderId | "auto";
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
function formatProviderList(providers: SearchProvider[]): string {
|
|
40
|
-
return providers.map(provider => provider.label).join(", ");
|
|
41
|
-
}
|
|
42
|
-
|
|
43
39
|
function formatProviderError(error: unknown, provider: SearchProvider): string {
|
|
44
40
|
if (error instanceof SearchProviderError) {
|
|
45
41
|
if (error.provider === "anthropic" && error.status === 404) {
|
|
@@ -138,9 +134,8 @@ async function executeSearch(
|
|
|
138
134
|
};
|
|
139
135
|
}
|
|
140
136
|
|
|
141
|
-
|
|
137
|
+
const failures: Array<{ provider: SearchProvider; error: unknown }> = [];
|
|
142
138
|
let lastProvider = providers[0];
|
|
143
|
-
|
|
144
139
|
for (const provider of providers) {
|
|
145
140
|
lastProvider = provider;
|
|
146
141
|
try {
|
|
@@ -168,14 +163,23 @@ async function executeSearch(
|
|
|
168
163
|
// failure and the loop falls through to the next provider (or to the
|
|
169
164
|
// summary error), masking the cancellation.
|
|
170
165
|
throwIfAborted(signal);
|
|
171
|
-
|
|
166
|
+
failures.push({ provider, error });
|
|
172
167
|
}
|
|
173
168
|
}
|
|
174
169
|
|
|
175
|
-
const
|
|
170
|
+
const lastFailure = failures[failures.length - 1];
|
|
171
|
+
const baseMessage = lastFailure
|
|
172
|
+
? formatProviderError(lastFailure.error, lastFailure.provider)
|
|
173
|
+
: `Unknown error from ${lastProvider.label}`;
|
|
176
174
|
const message =
|
|
177
175
|
providers.length > 1
|
|
178
|
-
? `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("; ")}`
|
|
179
183
|
: baseMessage;
|
|
180
184
|
|
|
181
185
|
return {
|
|
@@ -24,7 +24,7 @@ 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 { withHardTimeout } from "./utils";
|
|
27
|
+
import { classifyProviderHttpError, withHardTimeout } from "./utils";
|
|
28
28
|
|
|
29
29
|
const DEFAULT_MODEL = "claude-haiku-4-5";
|
|
30
30
|
const DEFAULT_MAX_TOKENS = 4096;
|
|
@@ -123,6 +123,8 @@ async function callSearch(
|
|
|
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, withHardTimeout } 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;
|
|
@@ -90,6 +90,8 @@ async function callBraveSearch(
|
|
|
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,7 +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 { withHardTimeout } from "./utils";
|
|
18
|
+
import { classifyProviderHttpError, withHardTimeout } from "./utils";
|
|
19
19
|
|
|
20
20
|
const CODEX_BASE_URL = "https://chatgpt.com/backend-api";
|
|
21
21
|
const CODEX_RESPONSES_PATH = "/codex/responses";
|
|
@@ -344,6 +344,8 @@ async function callCodexSearch(
|
|
|
344
344
|
|
|
345
345
|
if (!response.ok) {
|
|
346
346
|
const errorText = await response.text();
|
|
347
|
+
const classified = classifyProviderHttpError("codex", response.status, errorText);
|
|
348
|
+
if (classified) throw classified;
|
|
347
349
|
throw new SearchProviderError("codex", `Codex API error (${response.status}): ${errorText}`, response.status);
|
|
348
350
|
}
|
|
349
351
|
|
|
@@ -14,7 +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 { withHardTimeout } from "./utils";
|
|
17
|
+
import { classifyProviderHttpError, withHardTimeout } from "./utils";
|
|
18
18
|
|
|
19
19
|
const EXA_API_URL = "https://api.exa.ai/search";
|
|
20
20
|
|
|
@@ -186,6 +186,8 @@ async function callExaSearch(apiKey: string, params: ExaSearchParams): Promise<E
|
|
|
186
186
|
|
|
187
187
|
if (!response.ok) {
|
|
188
188
|
const errorText = await response.text();
|
|
189
|
+
const classified = classifyProviderHttpError("exa", response.status, errorText);
|
|
190
|
+
if (classified) throw classified;
|
|
189
191
|
throw new SearchProviderError("exa", `Exa API error (${response.status}): ${errorText}`, response.status);
|
|
190
192
|
}
|
|
191
193
|
|
|
@@ -15,7 +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 { withHardTimeout } from "./utils";
|
|
18
|
+
import { classifyProviderHttpError, withHardTimeout } from "./utils";
|
|
19
19
|
|
|
20
20
|
const DEFAULT_ENDPOINT = "https://cloudcode-pa.googleapis.com";
|
|
21
21
|
const ANTIGRAVITY_DAILY_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
|
|
@@ -341,6 +341,8 @@ async function callGeminiSearch(
|
|
|
341
341
|
|
|
342
342
|
if (!response.ok) {
|
|
343
343
|
const errorText = await response.text();
|
|
344
|
+
const classified = classifyProviderHttpError("gemini", response.status, errorText);
|
|
345
|
+
if (classified) throw classified;
|
|
344
346
|
throw new SearchProviderError(
|
|
345
347
|
"gemini",
|
|
346
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, withHardTimeout } from "./utils";
|
|
13
|
+
import { classifyProviderHttpError, isApiKeyAvailable, withHardTimeout } from "./utils";
|
|
14
14
|
|
|
15
15
|
const JINA_SEARCH_URL = "https://s.jina.ai";
|
|
16
16
|
|
|
@@ -46,6 +46,8 @@ async function callJinaSearch(apiKey: string, query: string, signal?: AbortSigna
|
|
|
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, withHardTimeout } 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
|
|
|
@@ -83,6 +83,8 @@ async function callKimiSearch(
|
|
|
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,7 +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 { withHardTimeout } from "./utils";
|
|
25
|
+
import { classifyProviderHttpError, withHardTimeout } from "./utils";
|
|
26
26
|
|
|
27
27
|
const PERPLEXITY_API_URL = "https://api.perplexity.ai/chat/completions";
|
|
28
28
|
const PERPLEXITY_OAUTH_ASK_URL = "https://www.perplexity.ai/rest/sse/perplexity_ask";
|
|
@@ -253,6 +253,8 @@ async function callPerplexityApi(
|
|
|
253
253
|
|
|
254
254
|
if (!response.ok) {
|
|
255
255
|
const errorText = await response.text();
|
|
256
|
+
const classified = classifyProviderHttpError("perplexity", response.status, errorText);
|
|
257
|
+
if (classified) throw classified;
|
|
256
258
|
throw new SearchProviderError(
|
|
257
259
|
"perplexity",
|
|
258
260
|
`Perplexity API error (${response.status}): ${errorText}`,
|
|
@@ -370,6 +372,8 @@ async function callPerplexityOAuth(
|
|
|
370
372
|
|
|
371
373
|
if (!response.ok) {
|
|
372
374
|
const errorText = await response.text();
|
|
375
|
+
const classified = classifyProviderHttpError("perplexity", response.status, errorText);
|
|
376
|
+
if (classified) throw classified;
|
|
373
377
|
throw new SearchProviderError(
|
|
374
378
|
"perplexity",
|
|
375
379
|
`Perplexity OAuth API error (${response.status}): ${errorText}`,
|
|
@@ -31,7 +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 { withHardTimeout } from "./utils";
|
|
34
|
+
import { classifyProviderHttpError, withHardTimeout } from "./utils";
|
|
35
35
|
|
|
36
36
|
const DEFAULT_NUM_RESULTS = 10;
|
|
37
37
|
const MAX_NUM_RESULTS = 20;
|
|
@@ -217,6 +217,8 @@ async function callSearXNGSearch(
|
|
|
217
217
|
|
|
218
218
|
if (!response.ok) {
|
|
219
219
|
const errorText = await response.text();
|
|
220
|
+
const classified = classifyProviderHttpError("searxng", response.status, errorText);
|
|
221
|
+
if (classified) throw classified;
|
|
220
222
|
throw new SearchProviderError("searxng", `SearXNG API error (${response.status}): ${errorText}`, response.status);
|
|
221
223
|
}
|
|
222
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, withHardTimeout } 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
|
|
|
@@ -48,6 +48,8 @@ async function callSyntheticSearch(
|
|
|
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, withHardTimeout } 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;
|
|
@@ -97,6 +97,8 @@ async function callTavilySearch(apiKey: string, params: TavilySearchParams): Pro
|
|
|
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;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getAgentDbPath } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import { AgentStorage } from "../../../session/agent-storage";
|
|
3
|
-
import type
|
|
3
|
+
import { SearchProviderError, type SearchProviderId, type SearchSource } from "../../../web/search/types";
|
|
4
4
|
import { dateToAgeSeconds } from "../utils";
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -100,3 +100,35 @@ export function toSearchSources(
|
|
|
100
100
|
ageSeconds: dateToAgeSeconds(source.publishedDate),
|
|
101
101
|
}));
|
|
102
102
|
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Quota/auth signals across providers. Telemetry on 15.1.7/15.1.8 showed users
|
|
106
|
+
* hitting credit-exhaustion and 401/402/403 responses that were surfaced as
|
|
107
|
+
* raw HTTP error text. Map those into compact, provider-tagged messages so
|
|
108
|
+
* the orchestrator can chain-advance cleanly and the final summary stays
|
|
109
|
+
* legible when every provider rejects the request.
|
|
110
|
+
*
|
|
111
|
+
* Returns `null` when the response does not match a known quota/auth signal,
|
|
112
|
+
* leaving the caller to throw its provider-specific fallback error.
|
|
113
|
+
*/
|
|
114
|
+
const CREDIT_BODY_PATTERN = /credits?\s*(?:exhausted|exceeded)|quota|insufficient/i;
|
|
115
|
+
|
|
116
|
+
export function classifyProviderHttpError(
|
|
117
|
+
provider: SearchProviderId,
|
|
118
|
+
status: number,
|
|
119
|
+
body: string,
|
|
120
|
+
): SearchProviderError | null {
|
|
121
|
+
if (CREDIT_BODY_PATTERN.test(body)) {
|
|
122
|
+
return new SearchProviderError(provider, `${provider}: credits exhausted`, status);
|
|
123
|
+
}
|
|
124
|
+
if (status === 402) {
|
|
125
|
+
return new SearchProviderError(provider, `${provider}: 402 credits exhausted`, status);
|
|
126
|
+
}
|
|
127
|
+
if (status === 401) {
|
|
128
|
+
return new SearchProviderError(provider, `${provider}: 401 unauthorized`, status);
|
|
129
|
+
}
|
|
130
|
+
if (status === 403) {
|
|
131
|
+
return new SearchProviderError(provider, `${provider}: 403 forbidden`, status);
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
@@ -11,7 +11,7 @@ import { SearchProviderError } from "../../../web/search/types";
|
|
|
11
11
|
import { dateToAgeSeconds } from "../utils";
|
|
12
12
|
import type { SearchParams } from "./base";
|
|
13
13
|
import { SearchProvider } from "./base";
|
|
14
|
-
import { findCredential, isApiKeyAvailable, withHardTimeout } from "./utils";
|
|
14
|
+
import { classifyProviderHttpError, findCredential, isApiKeyAvailable, withHardTimeout } from "./utils";
|
|
15
15
|
|
|
16
16
|
const ZAI_MCP_URL = "https://api.z.ai/api/mcp/web_search_prime/mcp";
|
|
17
17
|
const ZAI_TOOL_NAME = "web_search_prime";
|
|
@@ -78,6 +78,8 @@ async function callZaiTool(apiKey: string, args: Record<string, unknown>, signal
|
|
|
78
78
|
|
|
79
79
|
if (!response.ok) {
|
|
80
80
|
const errorText = await response.text();
|
|
81
|
+
const classified = classifyProviderHttpError("zai", response.status, errorText);
|
|
82
|
+
if (classified) throw classified;
|
|
81
83
|
throw new SearchProviderError("zai", `Z.AI MCP error (${response.status}): ${errorText}`, response.status);
|
|
82
84
|
}
|
|
83
85
|
|