@aliou/pi-synthetic 0.6.3 → 0.8.0

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliou/pi-synthetic",
3
- "version": "0.6.3",
3
+ "version": "0.8.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "private": false,
@@ -32,6 +32,7 @@
32
32
  },
33
33
  "devDependencies": {
34
34
  "@aliou/biome-plugins": "^0.3.2",
35
+ "@aliou/pi-utils-ui": "^0.1.2",
35
36
  "@biomejs/biome": "^2.4.2",
36
37
  "@changesets/cli": "^2.27.11",
37
38
  "@mariozechner/pi-coding-agent": "0.52.7",
@@ -294,7 +294,7 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
294
294
  input: ["text", "image"],
295
295
  cost: {
296
296
  input: 0.6,
297
- output: 3,
297
+ output: 3.6,
298
298
  cacheRead: 0.6,
299
299
  cacheWrite: 0,
300
300
  },
@@ -321,4 +321,23 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
321
321
  maxTokensField: "max_completion_tokens",
322
322
  },
323
323
  },
324
+ // API: hf:nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4 → ctx=262144, out=65536
325
+ {
326
+ id: "hf:nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4",
327
+ name: "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4",
328
+ reasoning: true,
329
+ compat: {
330
+ supportsReasoningEffort: true,
331
+ reasoningEffortMap: SYNTHETIC_REASONING_EFFORT_MAP,
332
+ },
333
+ input: ["text"],
334
+ cost: {
335
+ input: 0.6,
336
+ output: 3,
337
+ cacheRead: 0.6,
338
+ cacheWrite: 0,
339
+ },
340
+ contextWindow: 262144,
341
+ maxTokens: 65536,
342
+ },
324
343
  ];
@@ -1,3 +1,4 @@
1
+ import { ToolCallHeader, ToolFooter } from "@aliou/pi-utils-ui";
1
2
  import type {
2
3
  AgentToolResult,
3
4
  ExtensionAPI,
@@ -5,7 +6,8 @@ import type {
5
6
  Theme,
6
7
  ToolRenderResultOptions,
7
8
  } from "@mariozechner/pi-coding-agent";
8
- import { Text } from "@mariozechner/pi-tui";
9
+ import { getMarkdownTheme, keyHint } from "@mariozechner/pi-coding-agent";
10
+ import { Container, Markdown, Text } from "@mariozechner/pi-tui";
9
11
  import { type Static, Type } from "@sinclair/typebox";
10
12
 
11
13
  export const SYNTHETIC_WEB_SEARCH_TOOL = "synthetic_web_search" as const;
@@ -24,8 +26,6 @@ interface SyntheticSearchResponse {
24
26
  interface WebSearchDetails {
25
27
  results?: SyntheticSearchResult[];
26
28
  query?: string;
27
- error?: string;
28
- isError?: boolean;
29
29
  }
30
30
 
31
31
  const SearchParams = Type.Object({
@@ -58,139 +58,168 @@ export function registerSyntheticWebSearchTool(pi: ExtensionAPI): void {
58
58
  details: { query: params.query },
59
59
  });
60
60
 
61
- try {
62
- const apiKey = process.env.SYNTHETIC_API_KEY;
63
- if (!apiKey) {
64
- const error = "SYNTHETIC_API_KEY is not configured";
65
- return {
66
- content: [{ type: "text", text: `Error: ${error}` }],
67
- details: { error, isError: true },
68
- };
69
- }
70
-
71
- const response = await fetch("https://api.synthetic.new/v2/search", {
72
- method: "POST",
73
- headers: {
74
- Authorization: `Bearer ${apiKey}`,
75
- "Content-Type": "application/json",
76
- },
77
- body: JSON.stringify({ query: params.query }),
78
- signal,
79
- });
80
-
81
- if (!response.ok) {
82
- const errorText = await response.text();
83
- const error = `Search API error: ${response.status} ${errorText}`;
84
- return {
85
- content: [{ type: "text", text: `Error: ${error}` }],
86
- details: { error, isError: true },
87
- };
88
- }
61
+ const apiKey = process.env.SYNTHETIC_API_KEY;
62
+ if (!apiKey) {
63
+ throw new Error("SYNTHETIC_API_KEY is not configured");
64
+ }
89
65
 
90
- let data: SyntheticSearchResponse;
91
- try {
92
- data = await response.json();
93
- } catch (parseError) {
94
- const error =
95
- parseError instanceof Error
96
- ? `Failed to parse search results: ${parseError.message}`
97
- : "Failed to parse search results";
98
- return {
99
- content: [{ type: "text", text: `Error: ${error}` }],
100
- details: { error, isError: true },
101
- };
102
- }
66
+ const response = await fetch("https://api.synthetic.new/v2/search", {
67
+ method: "POST",
68
+ headers: {
69
+ Authorization: `Bearer ${apiKey}`,
70
+ "Content-Type": "application/json",
71
+ },
72
+ body: JSON.stringify({ query: params.query }),
73
+ signal,
74
+ });
103
75
 
104
- let content = `Found ${data.results.length} result(s):\n\n`;
105
- for (const result of data.results) {
106
- content += `## ${result.title}\n`;
107
- content += `URL: ${result.url}\n`;
108
- content += `Published: ${result.published}\n`;
109
- content += `\n${result.text}\n`;
110
- content += "\n---\n\n";
111
- }
76
+ if (!response.ok) {
77
+ const errorText = await response.text();
78
+ throw new Error(`Search API error: ${response.status} ${errorText}`);
79
+ }
112
80
 
113
- return {
114
- content: [{ type: "text", text: content }],
115
- details: {
116
- results: data.results,
117
- query: params.query,
118
- },
119
- };
120
- } catch (error) {
121
- if (error instanceof Error && error.name === "AbortError") {
122
- return {
123
- content: [{ type: "text", text: "Search cancelled" }],
124
- details: { query: params.query },
125
- };
126
- }
81
+ let data: SyntheticSearchResponse;
82
+ try {
83
+ data = await response.json();
84
+ } catch (parseError) {
85
+ throw new Error(
86
+ parseError instanceof Error
87
+ ? `Failed to parse search results: ${parseError.message}`
88
+ : "Failed to parse search results",
89
+ );
90
+ }
127
91
 
128
- const message =
129
- error instanceof Error ? error.message : "Unknown error occurred";
130
- return {
131
- content: [{ type: "text", text: `Error: ${message}` }],
132
- details: { error: message, isError: true },
133
- };
92
+ let content = `Found ${data.results.length} result(s):\n\n`;
93
+ for (const result of data.results) {
94
+ content += `## ${result.title}\n`;
95
+ content += `URL: ${result.url}\n`;
96
+ content += `Published: ${result.published}\n`;
97
+ content += `\n${result.text}\n`;
98
+ content += "\n---\n\n";
134
99
  }
100
+
101
+ return {
102
+ content: [{ type: "text", text: content }],
103
+ details: {
104
+ results: data.results,
105
+ query: params.query,
106
+ },
107
+ };
135
108
  },
136
109
 
137
- renderCall(args: SearchParamsType, theme: Theme): Text {
138
- let text = theme.fg("toolTitle", theme.bold("Synthetic: WebSearch "));
139
- text += theme.fg("accent", `"${args.query}"`);
140
- return new Text(text, 0, 0);
110
+ renderCall(args: SearchParamsType, theme: Theme) {
111
+ return new ToolCallHeader(
112
+ {
113
+ toolName: "Synthetic: WebSearch",
114
+ mainArg: `"${args.query}"`,
115
+ showColon: true,
116
+ },
117
+ theme,
118
+ );
141
119
  },
142
120
 
143
121
  renderResult(
144
122
  result: AgentToolResult<WebSearchDetails>,
145
123
  options: ToolRenderResultOptions,
146
124
  theme: Theme,
147
- ): Text {
125
+ ) {
148
126
  const { expanded, isPartial } = options;
127
+ const SNIPPET_LINES = 5;
149
128
 
150
129
  if (isPartial) {
151
- const text =
152
- result.content?.[0]?.type === "text"
153
- ? result.content[0].text
154
- : "Searching...";
155
- return new Text(theme.fg("dim", text), 0, 0);
130
+ return new Text(
131
+ theme.fg("muted", "Synthetic: WebSearch: fetching..."),
132
+ 0,
133
+ 0,
134
+ );
156
135
  }
157
136
 
158
137
  const details = result.details;
159
- if (details?.isError) {
138
+ const results = details?.results || [];
139
+ const container = new Container();
140
+
141
+ // When the tool throws, the framework calls renderResult with
142
+ // details={} (empty object) and the error message in content.
143
+ // Detect this by checking for missing results in details.
144
+ if (!details?.results) {
145
+ const textBlock = result.content.find((c) => c.type === "text");
160
146
  const errorMsg =
161
- result.content?.[0]?.type === "text"
162
- ? result.content[0].text
163
- : "Error occurred";
164
- return new Text(theme.fg("error", errorMsg), 0, 0);
147
+ (textBlock?.type === "text" && textBlock.text) || "Search failed";
148
+ container.addChild(new Text(theme.fg("error", errorMsg), 0, 0));
149
+ return container;
165
150
  }
166
151
 
167
- const results = details?.results || [];
168
- let text = theme.fg("success", `✓ Found ${results.length} result(s)`);
169
-
170
- if (!expanded && results.length > 0) {
152
+ if (results.length === 0) {
153
+ container.addChild(
154
+ new Text(theme.fg("muted", "Synthetic: WebSearch: no results"), 0, 0),
155
+ );
156
+ } else if (!expanded) {
157
+ // Collapsed: show result count + first result title
158
+ let text = theme.fg("success", `Found ${results.length} result(s)`);
171
159
  const first = results[0];
172
- text += `\n ${theme.fg("dim", `${first.title}`)}`;
173
- if (results.length > 1) {
174
- text += theme.fg("dim", ` (${results.length - 1} more)`);
160
+ if (first) {
161
+ text += `\n ${theme.fg("dim", first.title)}`;
162
+ if (results.length > 1) {
163
+ text += theme.fg("dim", ` (+${results.length - 1} more)`);
164
+ }
175
165
  }
176
- text += theme.fg("muted", " [Ctrl+O to expand]");
177
- }
166
+ text += theme.fg("muted", ` ${keyHint("expandTools", "to expand")}`);
167
+ container.addChild(new Text(text, 0, 0));
168
+ } else {
169
+ // Expanded: show each result with title, URL, date, and snippet
170
+ container.addChild(
171
+ new Text(
172
+ theme.fg("success", `Found ${results.length} result(s)`),
173
+ 0,
174
+ 0,
175
+ ),
176
+ );
178
177
 
179
- if (expanded) {
180
178
  for (const r of results) {
181
- text += `\n\n${theme.fg("accent", theme.bold(r.title))}`;
182
- text += `\n${theme.fg("dim", r.url)}`;
179
+ container.addChild(new Text("", 0, 0));
180
+ container.addChild(
181
+ new Text(
182
+ `${theme.fg("dim", ">")} ${theme.fg("accent", theme.bold(r.title))}`,
183
+ 0,
184
+ 0,
185
+ ),
186
+ );
187
+ container.addChild(new Text(` ${theme.fg("dim", r.url)}`, 0, 0));
188
+ if (r.published) {
189
+ container.addChild(
190
+ new Text(
191
+ ` ${theme.fg("muted", `Published: ${r.published}`)}`,
192
+ 0,
193
+ 0,
194
+ ),
195
+ );
196
+ }
197
+
183
198
  if (r.text) {
184
- const preview = r.text.slice(0, 200);
185
- text += `\n${theme.fg("muted", preview)}`;
186
- if (r.text.length > 200) {
187
- text += theme.fg("dim", "...");
188
- }
199
+ container.addChild(new Text("", 0, 0));
200
+ const snippet = r.text
201
+ .split("\n")
202
+ .slice(0, SNIPPET_LINES)
203
+ .map((line) => `> ${line}`)
204
+ .join("\n");
205
+ container.addChild(
206
+ new Markdown(snippet, 0, 0, getMarkdownTheme(), {
207
+ color: (text: string) => theme.fg("toolOutput", text),
208
+ }),
209
+ );
189
210
  }
190
211
  }
191
212
  }
192
213
 
193
- return new Text(text, 0, 0);
214
+ container.addChild(new Text("", 0, 0));
215
+ container.addChild(
216
+ new ToolFooter(theme, {
217
+ items: [{ label: "results", value: `${results.length} result(s)` }],
218
+ separator: " | ",
219
+ }),
220
+ );
221
+
222
+ return container;
194
223
  },
195
224
  });
196
225
  }