@aliou/pi-synthetic 0.17.0 → 0.17.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/package.json
CHANGED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Synthetic-specific context overflow error detection.
|
|
3
|
+
*
|
|
4
|
+
* Some Synthetic backend errors are not matched by Pi's built-in
|
|
5
|
+
* isContextOverflow() patterns. This module provides the regex
|
|
6
|
+
* to detect them so the provider extension can normalize the
|
|
7
|
+
* errorMessage with the `context_length_exceeded:` prefix that
|
|
8
|
+
* Pi recognizes.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Matches Synthetic context overflow errors that Pi's built-in
|
|
13
|
+
* overflow detector does not catch:
|
|
14
|
+
*
|
|
15
|
+
* 1. "Error from inference backend: 400 The input (N tokens) is longer
|
|
16
|
+
* than the model's context length (M tokens)."
|
|
17
|
+
* 2. "Context limit exceeded"
|
|
18
|
+
*/
|
|
19
|
+
export const SYNTHETIC_OVERFLOW_PATTERN =
|
|
20
|
+
/input \(\d+ tokens\) is longer than the model's context length|Context limit exceeded/i;
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
type SyntheticQuotasRequestPayload,
|
|
28
28
|
} from "../../types/quotas";
|
|
29
29
|
import { fetchQuotas } from "../../utils/quotas";
|
|
30
|
+
import { SYNTHETIC_OVERFLOW_PATTERN } from "./context-overflow";
|
|
30
31
|
import { SYNTHETIC_MODELS } from "./models";
|
|
31
32
|
|
|
32
33
|
export function buildSyntheticProviderModels(includeProxiedModels: boolean) {
|
|
@@ -111,6 +112,24 @@ export default async function (pi: ExtensionAPI) {
|
|
|
111
112
|
if (quotas) quotaStore.ingest(quotas, "header");
|
|
112
113
|
});
|
|
113
114
|
|
|
115
|
+
pi.on("message_end", (event) => {
|
|
116
|
+
const msg = event.message;
|
|
117
|
+
if (msg.role !== "assistant") return;
|
|
118
|
+
if (msg.stopReason !== "error") return;
|
|
119
|
+
if (msg.provider !== "synthetic") return;
|
|
120
|
+
|
|
121
|
+
const errorMessage = msg.errorMessage ?? "";
|
|
122
|
+
if (errorMessage.includes("context_length_exceeded")) return;
|
|
123
|
+
if (!SYNTHETIC_OVERFLOW_PATTERN.test(errorMessage)) return;
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
message: {
|
|
127
|
+
...msg,
|
|
128
|
+
errorMessage: `context_length_exceeded: ${errorMessage}`,
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
|
|
114
133
|
pi.events.on(SYNTHETIC_QUOTAS_REQUEST_EVENT, async (data: unknown) => {
|
|
115
134
|
const payload = data as SyntheticQuotasRequestPayload | undefined;
|
|
116
135
|
const snapshot = await quotaStore.refreshFromApi(fetchQuotasFromAuth);
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { writeFile } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
1
5
|
import { ToolCallHeader, ToolFooter } from "@aliou/pi-utils-ui";
|
|
2
6
|
import type {
|
|
3
7
|
AgentToolResult,
|
|
@@ -6,8 +10,14 @@ import type {
|
|
|
6
10
|
Theme,
|
|
7
11
|
ToolRenderResultOptions,
|
|
8
12
|
} from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import {
|
|
10
|
-
|
|
13
|
+
import {
|
|
14
|
+
DEFAULT_MAX_BYTES,
|
|
15
|
+
DEFAULT_MAX_LINES,
|
|
16
|
+
formatSize,
|
|
17
|
+
keyHint,
|
|
18
|
+
truncateHead,
|
|
19
|
+
} from "@earendil-works/pi-coding-agent";
|
|
20
|
+
import { Container, Text } from "@earendil-works/pi-tui";
|
|
11
21
|
import { type Static, Type } from "typebox";
|
|
12
22
|
import { configLoader } from "../../config";
|
|
13
23
|
import { getSyntheticApiKey } from "../../lib/env";
|
|
@@ -25,8 +35,20 @@ interface SyntheticSearchResponse {
|
|
|
25
35
|
results: SyntheticSearchResult[];
|
|
26
36
|
}
|
|
27
37
|
|
|
38
|
+
interface WebSearchResultDetails {
|
|
39
|
+
title: string;
|
|
40
|
+
url: string;
|
|
41
|
+
published: string;
|
|
42
|
+
truncated: boolean;
|
|
43
|
+
tempFilePath?: string;
|
|
44
|
+
totalLines: number;
|
|
45
|
+
totalBytes: number;
|
|
46
|
+
outputLines: number;
|
|
47
|
+
outputBytes: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
28
50
|
interface WebSearchDetails {
|
|
29
|
-
results?:
|
|
51
|
+
results?: WebSearchResultDetails[];
|
|
30
52
|
query?: string;
|
|
31
53
|
}
|
|
32
54
|
|
|
@@ -42,8 +64,7 @@ export function registerSyntheticWebSearchTool(pi: ExtensionAPI): void {
|
|
|
42
64
|
pi.registerTool<typeof SearchParams, WebSearchDetails>({
|
|
43
65
|
name: SYNTHETIC_WEB_SEARCH_TOOL,
|
|
44
66
|
label: "Synthetic: Web Search",
|
|
45
|
-
description:
|
|
46
|
-
"Search the web using Synthetic's zero-data-retention API. Returns search results with titles, URLs, content snippets, and publication dates. Use for finding documentation, articles, recent information, or any web content. Results are fresh and not cached by Synthetic.",
|
|
67
|
+
description: `Search the web using Synthetic's zero-data-retention API. Returns search results with titles, URLs, content snippets, and publication dates. Use for finding documentation, articles, recent information, or any web content. Results are fresh and not cached by Synthetic. Results are truncated to ${DEFAULT_MAX_LINES} lines or ${formatSize(DEFAULT_MAX_BYTES)} (whichever is hit first). If truncated, full output is saved to a temp file.`,
|
|
47
68
|
promptSnippet: "Search the web using Synthetic's zero-data-retention API",
|
|
48
69
|
promptGuidelines: [
|
|
49
70
|
"Use synthetic_web_search for finding documentation, articles, recent information, or any web content.",
|
|
@@ -106,18 +127,55 @@ export function registerSyntheticWebSearchTool(pi: ExtensionAPI): void {
|
|
|
106
127
|
}
|
|
107
128
|
|
|
108
129
|
let content = `Found ${data.results.length} result(s):\n\n`;
|
|
109
|
-
|
|
130
|
+
const resultDetails: WebSearchResultDetails[] = [];
|
|
131
|
+
|
|
132
|
+
for (let i = 0; i < data.results.length; i++) {
|
|
133
|
+
const result = data.results[i];
|
|
134
|
+
const slug = result.title
|
|
135
|
+
.toLowerCase()
|
|
136
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
137
|
+
.replace(/(^-|-$)/g, "")
|
|
138
|
+
.slice(0, 40);
|
|
139
|
+
const truncation = truncateHead(result.text, {
|
|
140
|
+
maxLines: DEFAULT_MAX_LINES,
|
|
141
|
+
maxBytes: DEFAULT_MAX_BYTES,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
let preview = truncation.content;
|
|
145
|
+
let tempFilePath: string | undefined;
|
|
146
|
+
|
|
147
|
+
if (truncation.truncated) {
|
|
148
|
+
tempFilePath = join(
|
|
149
|
+
tmpdir(),
|
|
150
|
+
`pi-synthetic-search-${slug}-${randomBytes(4).toString("hex")}.md`,
|
|
151
|
+
);
|
|
152
|
+
await writeFile(tempFilePath, result.text, "utf8");
|
|
153
|
+
preview += `\n\n[Result truncated: ${truncation.outputLines} of ${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}). Full result: ${tempFilePath}]`;
|
|
154
|
+
}
|
|
155
|
+
|
|
110
156
|
content += `## ${result.title}\n`;
|
|
111
157
|
content += `URL: ${result.url}\n`;
|
|
112
158
|
content += `Published: ${result.published}\n`;
|
|
113
|
-
content += `\n${
|
|
159
|
+
content += `\n${preview}\n`;
|
|
114
160
|
content += "\n---\n\n";
|
|
161
|
+
|
|
162
|
+
resultDetails.push({
|
|
163
|
+
title: result.title,
|
|
164
|
+
url: result.url,
|
|
165
|
+
published: result.published,
|
|
166
|
+
truncated: truncation.truncated,
|
|
167
|
+
tempFilePath,
|
|
168
|
+
totalLines: truncation.totalLines,
|
|
169
|
+
totalBytes: truncation.totalBytes,
|
|
170
|
+
outputLines: truncation.outputLines,
|
|
171
|
+
outputBytes: truncation.outputBytes,
|
|
172
|
+
});
|
|
115
173
|
}
|
|
116
174
|
|
|
117
175
|
return {
|
|
118
176
|
content: [{ type: "text", text: content }],
|
|
119
177
|
details: {
|
|
120
|
-
results:
|
|
178
|
+
results: resultDetails,
|
|
121
179
|
query: params.query,
|
|
122
180
|
},
|
|
123
181
|
};
|
|
@@ -140,7 +198,6 @@ export function registerSyntheticWebSearchTool(pi: ExtensionAPI): void {
|
|
|
140
198
|
theme: Theme,
|
|
141
199
|
) {
|
|
142
200
|
const { expanded, isPartial } = options;
|
|
143
|
-
const SNIPPET_LINES = 5;
|
|
144
201
|
|
|
145
202
|
if (isPartial) {
|
|
146
203
|
return new Text(
|
|
@@ -165,6 +222,8 @@ export function registerSyntheticWebSearchTool(pi: ExtensionAPI): void {
|
|
|
165
222
|
return container;
|
|
166
223
|
}
|
|
167
224
|
|
|
225
|
+
const hasTruncation = results.some((r) => r.truncated);
|
|
226
|
+
|
|
168
227
|
if (results.length === 0) {
|
|
169
228
|
container.addChild(
|
|
170
229
|
new Text(theme.fg("muted", "Synthetic: WebSearch: no results"), 0, 0),
|
|
@@ -172,6 +231,9 @@ export function registerSyntheticWebSearchTool(pi: ExtensionAPI): void {
|
|
|
172
231
|
} else if (!expanded) {
|
|
173
232
|
// Collapsed: show result count + first result title
|
|
174
233
|
let text = theme.fg("success", `Found ${results.length} result(s)`);
|
|
234
|
+
if (hasTruncation) {
|
|
235
|
+
text += theme.fg("warning", " (truncated)");
|
|
236
|
+
}
|
|
175
237
|
const first = results[0];
|
|
176
238
|
if (first) {
|
|
177
239
|
text += `\n ${theme.fg("dim", first.title)}`;
|
|
@@ -214,26 +276,40 @@ export function registerSyntheticWebSearchTool(pi: ExtensionAPI): void {
|
|
|
214
276
|
);
|
|
215
277
|
}
|
|
216
278
|
|
|
217
|
-
if (r.
|
|
218
|
-
container.addChild(new Text("", 0, 0));
|
|
219
|
-
const snippet = r.text
|
|
220
|
-
.split("\n")
|
|
221
|
-
.slice(0, SNIPPET_LINES)
|
|
222
|
-
.map((line) => `> ${line}`)
|
|
223
|
-
.join("\n");
|
|
279
|
+
if (r.truncated) {
|
|
224
280
|
container.addChild(
|
|
225
|
-
new
|
|
226
|
-
|
|
227
|
-
|
|
281
|
+
new Text(
|
|
282
|
+
` ${theme.fg("warning", `Truncated: ${r.outputLines} of ${r.totalLines} lines (${formatSize(r.outputBytes)} of ${formatSize(r.totalBytes)}). Full content: ${r.tempFilePath}`)}`,
|
|
283
|
+
0,
|
|
284
|
+
0,
|
|
285
|
+
),
|
|
228
286
|
);
|
|
229
287
|
}
|
|
230
288
|
}
|
|
231
289
|
}
|
|
232
290
|
|
|
291
|
+
const footerItems: { label: string; value: string }[] = [];
|
|
292
|
+
footerItems.push({
|
|
293
|
+
label: "results",
|
|
294
|
+
value: `${results.length} result(s)`,
|
|
295
|
+
});
|
|
296
|
+
if (hasTruncation) {
|
|
297
|
+
const truncatedCount = results.filter((r) => r.truncated).length;
|
|
298
|
+
footerItems.push({
|
|
299
|
+
label: "truncated",
|
|
300
|
+
value: `${truncatedCount}`,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
if (!expanded) {
|
|
304
|
+
footerItems.push({
|
|
305
|
+
label: "",
|
|
306
|
+
value: keyHint("app.tools.expand", "to expand"),
|
|
307
|
+
});
|
|
308
|
+
}
|
|
233
309
|
container.addChild(new Text("", 0, 0));
|
|
234
310
|
container.addChild(
|
|
235
311
|
new ToolFooter(theme, {
|
|
236
|
-
items:
|
|
312
|
+
items: footerItems,
|
|
237
313
|
separator: " | ",
|
|
238
314
|
}),
|
|
239
315
|
);
|