@aliou/pi-synthetic 0.16.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 +8 -8
- package/src/config.ts +2 -2
- package/src/extensions/command-quotas/command.ts +1 -1
- package/src/extensions/command-quotas/components/quotas-display.ts +4 -4
- package/src/extensions/command-quotas/index.ts +1 -1
- package/src/extensions/provider/context-overflow.ts +20 -0
- package/src/extensions/provider/index.ts +23 -1
- package/src/extensions/provider/models.ts +1 -1
- package/src/extensions/quota-warnings/index.ts +1 -1
- package/src/extensions/sub-bar-integration/index.ts +1 -1
- package/src/extensions/usage-status/index.ts +1 -1
- package/src/extensions/web-search/index.ts +1 -1
- package/src/extensions/web-search/tool.ts +97 -21
- package/src/lib/env.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aliou/pi-synthetic",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -32,18 +32,18 @@
|
|
|
32
32
|
"README.md"
|
|
33
33
|
],
|
|
34
34
|
"peerDependencies": {
|
|
35
|
-
"@
|
|
36
|
-
"@
|
|
35
|
+
"@earendil-works/pi-coding-agent": "0.74.0",
|
|
36
|
+
"@earendil-works/pi-tui": "0.74.0"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@aliou/pi-utils-settings": "^0.
|
|
40
|
-
"@aliou/pi-utils-ui": "^0.
|
|
39
|
+
"@aliou/pi-utils-settings": "^0.15.0",
|
|
40
|
+
"@aliou/pi-utils-ui": "^0.4.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@aliou/biome-plugins": "^0.7.0",
|
|
44
44
|
"@biomejs/biome": "^2.4.2",
|
|
45
45
|
"@changesets/cli": "^2.27.11",
|
|
46
|
-
"@
|
|
46
|
+
"@earendil-works/pi-coding-agent": "0.74.0",
|
|
47
47
|
"typebox": "^1.1.37",
|
|
48
48
|
"@types/node": "^25.0.10",
|
|
49
49
|
"husky": "^9.1.7",
|
|
@@ -51,10 +51,10 @@
|
|
|
51
51
|
"vitest": "^4.0.18"
|
|
52
52
|
},
|
|
53
53
|
"peerDependenciesMeta": {
|
|
54
|
-
"@
|
|
54
|
+
"@earendil-works/pi-coding-agent": {
|
|
55
55
|
"optional": true
|
|
56
56
|
},
|
|
57
|
-
"@
|
|
57
|
+
"@earendil-works/pi-tui": {
|
|
58
58
|
"optional": true
|
|
59
59
|
}
|
|
60
60
|
},
|
package/src/config.ts
CHANGED
|
@@ -4,8 +4,8 @@ import {
|
|
|
4
4
|
registerSettingsCommand,
|
|
5
5
|
type SettingsSection,
|
|
6
6
|
} from "@aliou/pi-utils-settings";
|
|
7
|
-
import type { ExtensionAPI } from "@
|
|
8
|
-
import type { SettingItem } from "@
|
|
7
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
import type { SettingItem } from "@earendil-works/pi-tui";
|
|
9
9
|
import pkg from "../package.json" with { type: "json" };
|
|
10
10
|
|
|
11
11
|
export type SyntheticFeatureId =
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { configLoader } from "../../config";
|
|
3
3
|
import { getSyntheticApiKey } from "../../lib/env";
|
|
4
4
|
import { fetchQuotas } from "../../utils/quotas";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type { Theme } from "@
|
|
2
|
-
import { DynamicBorder } from "@
|
|
3
|
-
import type { Component, TUI } from "@
|
|
4
|
-
import { Loader, matchesKey, truncateToWidth } from "@
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { Component, TUI } from "@earendil-works/pi-tui";
|
|
4
|
+
import { Loader, matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
|
|
5
5
|
import type { QuotasResponse } from "../../../types/quotas";
|
|
6
6
|
import {
|
|
7
7
|
assessWindow,
|
|
@@ -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;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
AuthStorage,
|
|
3
|
+
ExtensionAPI,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
2
5
|
import {
|
|
3
6
|
configLoader,
|
|
4
7
|
emitSyntheticConfigUpdated,
|
|
@@ -24,6 +27,7 @@ import {
|
|
|
24
27
|
type SyntheticQuotasRequestPayload,
|
|
25
28
|
} from "../../types/quotas";
|
|
26
29
|
import { fetchQuotas } from "../../utils/quotas";
|
|
30
|
+
import { SYNTHETIC_OVERFLOW_PATTERN } from "./context-overflow";
|
|
27
31
|
import { SYNTHETIC_MODELS } from "./models";
|
|
28
32
|
|
|
29
33
|
export function buildSyntheticProviderModels(includeProxiedModels: boolean) {
|
|
@@ -108,6 +112,24 @@ export default async function (pi: ExtensionAPI) {
|
|
|
108
112
|
if (quotas) quotaStore.ingest(quotas, "header");
|
|
109
113
|
});
|
|
110
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
|
+
|
|
111
133
|
pi.events.on(SYNTHETIC_QUOTAS_REQUEST_EVENT, async (data: unknown) => {
|
|
112
134
|
const payload = data as SyntheticQuotasRequestPayload | undefined;
|
|
113
135
|
const snapshot = await quotaStore.refreshFromApi(fetchQuotasFromAuth);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Source: https://api.synthetic.new/openai/v1/models
|
|
3
3
|
// maxTokens sourced from https://models.dev/api.json (synthetic provider)
|
|
4
4
|
|
|
5
|
-
import type { ProviderModelConfig } from "@
|
|
5
|
+
import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
|
|
7
7
|
export interface SyntheticModelConfig extends ProviderModelConfig {
|
|
8
8
|
/** Upstream backend Synthetic proxies this model through (e.g. "fireworks", "together", "synthetic"). */
|
|
@@ -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,
|
|
@@ -5,9 +9,15 @@ import type {
|
|
|
5
9
|
ExtensionContext,
|
|
6
10
|
Theme,
|
|
7
11
|
ToolRenderResultOptions,
|
|
8
|
-
} from "@
|
|
9
|
-
import {
|
|
10
|
-
|
|
12
|
+
} from "@earendil-works/pi-coding-agent";
|
|
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
|
);
|
package/src/lib/env.ts
CHANGED