@aliou/pi-synthetic 0.4.4 → 0.4.6

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 CHANGED
@@ -1,5 +1,18 @@
1
1
  # @aliou/pi-synthetic
2
2
 
3
+ ## 0.4.6
4
+
5
+ ### Patch Changes
6
+
7
+ - 6180572: mark pi SDK peer deps as optional to prevent koffi OOM in Gondolin VMs
8
+ - fe8094f: register synthetic web search tool at init time and move availability checks to hooks
9
+
10
+ ## 0.4.5
11
+
12
+ ### Patch Changes
13
+
14
+ - 7489bc0: update model list: add nvidia/Kimi-K2.5-NVFP4, remove 6 discontinued models
15
+
3
16
  ## 0.4.4
4
17
 
5
18
  ### Patch Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliou/pi-synthetic",
3
- "version": "0.4.4",
3
+ "version": "0.4.6",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/aliou/pi-synthetic"
@@ -27,6 +27,11 @@
27
27
  "husky": "^9.1.7",
28
28
  "typescript": "^5.9.3"
29
29
  },
30
+ "peerDependenciesMeta": {
31
+ "@mariozechner/pi-coding-agent": {
32
+ "optional": true
33
+ }
34
+ },
30
35
  "scripts": {
31
36
  "typecheck": "tsc --noEmit",
32
37
  "lint": "biome check",
@@ -0,0 +1,129 @@
1
+ import type {
2
+ ExtensionAPI,
3
+ ExtensionContext,
4
+ } from "@mariozechner/pi-coding-agent";
5
+ import { SYNTHETIC_WEB_SEARCH_TOOL } from "../tools/search";
6
+
7
+ function notifyDebug(ctx: ExtensionContext, message: string): void {
8
+ ctx.ui.notify(`[pi-synthetic:web-search] ${message}`, "info");
9
+ }
10
+
11
+ async function checkSubscriptionAccess(
12
+ apiKey: string,
13
+ ): Promise<{ ok: true } | { ok: false; reason: string }> {
14
+ try {
15
+ const response = await fetch("https://api.synthetic.new/v2/quotas", {
16
+ method: "GET",
17
+ headers: {
18
+ Authorization: `Bearer ${apiKey}`,
19
+ },
20
+ });
21
+
22
+ if (!response.ok) {
23
+ return {
24
+ ok: false,
25
+ reason: `Quotas check failed (HTTP ${response.status})`,
26
+ };
27
+ }
28
+
29
+ const data = await response.json();
30
+ if (data?.subscription?.limit > 0) {
31
+ return { ok: true };
32
+ }
33
+
34
+ return {
35
+ ok: false,
36
+ reason: "No active subscription (search requires a subscription plan)",
37
+ };
38
+ } catch (error) {
39
+ const message =
40
+ error instanceof Error ? error.message : "Unknown error occurred";
41
+ return { ok: false, reason: `Quotas check failed: ${message}` };
42
+ }
43
+ }
44
+
45
+ export function registerSyntheticWebSearchHooks(pi: ExtensionAPI): void {
46
+ let accessCheckPromise:
47
+ | Promise<{ ok: true } | { ok: false; reason: string }>
48
+ | undefined;
49
+ let hasAccess = false;
50
+ let deniedReason: string | undefined;
51
+ let didNotifyDenied = false;
52
+
53
+ // Keep tool inactive at session start. Availability is decided before each agent run.
54
+ pi.on("session_start", (_event, ctx) => {
55
+ notifyDebug(ctx, "session_start: preparing web search tool");
56
+
57
+ const current = pi.getActiveTools();
58
+ if (current.includes(SYNTHETIC_WEB_SEARCH_TOOL)) {
59
+ pi.setActiveTools(
60
+ current.filter((toolName) => toolName !== SYNTHETIC_WEB_SEARCH_TOOL),
61
+ );
62
+ notifyDebug(ctx, "session_start: tool disabled until subscription check");
63
+ }
64
+ });
65
+
66
+ // Verify subscription only when user starts agent execution.
67
+ pi.on("before_agent_start", async (_event, ctx) => {
68
+ notifyDebug(ctx, "before_agent_start: ensuring tool availability");
69
+
70
+ const apiKey = process.env.SYNTHETIC_API_KEY;
71
+ if (!apiKey) {
72
+ hasAccess = false;
73
+ deniedReason = "SYNTHETIC_API_KEY is not configured";
74
+ accessCheckPromise = undefined;
75
+ notifyDebug(ctx, "before_agent_start: access denied (missing API key)");
76
+ } else {
77
+ if (deniedReason === "SYNTHETIC_API_KEY is not configured") {
78
+ deniedReason = undefined;
79
+ }
80
+
81
+ if (!hasAccess && !deniedReason) {
82
+ notifyDebug(ctx, "before_agent_start: checking subscription access");
83
+ accessCheckPromise ??= checkSubscriptionAccess(apiKey);
84
+ const access = await accessCheckPromise;
85
+
86
+ if (!access.ok) {
87
+ deniedReason = access.reason;
88
+ notifyDebug(
89
+ ctx,
90
+ `before_agent_start: access denied (${access.reason})`,
91
+ );
92
+ } else {
93
+ hasAccess = true;
94
+ didNotifyDenied = false;
95
+ notifyDebug(ctx, "before_agent_start: access granted");
96
+ }
97
+ }
98
+ }
99
+
100
+ if (deniedReason) {
101
+ const current = pi.getActiveTools();
102
+ if (current.includes(SYNTHETIC_WEB_SEARCH_TOOL)) {
103
+ pi.setActiveTools(
104
+ current.filter((toolName) => toolName !== SYNTHETIC_WEB_SEARCH_TOOL),
105
+ );
106
+ notifyDebug(ctx, "before_agent_start: tool kept disabled");
107
+ }
108
+
109
+ if (ctx.hasUI && !didNotifyDenied) {
110
+ ctx.ui.notify(
111
+ `Synthetic web search disabled: ${deniedReason}`,
112
+ "warning",
113
+ );
114
+ didNotifyDenied = true;
115
+ notifyDebug(
116
+ ctx,
117
+ "before_agent_start: user notified about disabled tool",
118
+ );
119
+ }
120
+ return;
121
+ }
122
+
123
+ const current = pi.getActiveTools();
124
+ if (!current.includes(SYNTHETIC_WEB_SEARCH_TOOL)) {
125
+ pi.setActiveTools([...current, SYNTHETIC_WEB_SEARCH_TOOL]);
126
+ notifyDebug(ctx, "before_agent_start: tool enabled");
127
+ }
128
+ });
129
+ }
package/src/index.ts CHANGED
@@ -1,14 +1,15 @@
1
1
  import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
2
2
  import { registerQuotasCommand } from "./commands/quotas";
3
+ import { registerSyntheticWebSearchHooks } from "./hooks/search-tool-availability";
3
4
  import { registerSyntheticProvider } from "./providers/index";
4
5
  import { registerSyntheticWebSearchTool } from "./tools/search";
5
6
 
6
7
  export default async function (pi: ExtensionAPI) {
7
8
  registerSyntheticProvider(pi);
9
+ registerSyntheticWebSearchTool(pi);
10
+ registerSyntheticWebSearchHooks(pi);
8
11
 
9
- // Only register quotas command and web search tool if API key is available
10
12
  if (process.env.SYNTHETIC_API_KEY) {
11
13
  registerQuotasCommand(pi);
12
- registerSyntheticWebSearchTool(pi);
13
14
  }
14
15
  }
@@ -100,36 +100,6 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
100
100
  contextWindow: 131072,
101
101
  maxTokens: 128000,
102
102
  },
103
- // models.dev: synthetic/hf:deepseek-ai/DeepSeek-V3.1 → ctx=128000, out=128000
104
- {
105
- id: "hf:deepseek-ai/DeepSeek-V3.1",
106
- name: "deepseek-ai/DeepSeek-V3.1",
107
- reasoning: false,
108
- input: ["text"],
109
- cost: {
110
- input: 0.56,
111
- output: 1.68,
112
- cacheRead: 0.56,
113
- cacheWrite: 0,
114
- },
115
- contextWindow: 131072,
116
- maxTokens: 128000,
117
- },
118
- // models.dev: synthetic/hf:deepseek-ai/DeepSeek-V3.1-Terminus → ctx=128000, out=128000
119
- {
120
- id: "hf:deepseek-ai/DeepSeek-V3.1-Terminus",
121
- name: "deepseek-ai/DeepSeek-V3.1-Terminus",
122
- reasoning: false,
123
- input: ["text"],
124
- cost: {
125
- input: 1.2,
126
- output: 1.2,
127
- cacheRead: 1.2,
128
- cacheWrite: 0,
129
- },
130
- contextWindow: 131072,
131
- maxTokens: 128000,
132
- },
133
103
  // models.dev: synthetic/hf:deepseek-ai/DeepSeek-V3.2 → ctx=162816, out=8000
134
104
  {
135
105
  id: "hf:deepseek-ai/DeepSeek-V3.2",
@@ -145,21 +115,6 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
145
115
  contextWindow: 162816,
146
116
  maxTokens: 8000,
147
117
  },
148
- // NOTE: not present in models.dev synthetic provider; maxTokens unchanged
149
- {
150
- id: "hf:Qwen/Qwen3-VL-235B-A22B-Instruct",
151
- name: "Qwen/Qwen3-VL-235B-A22B-Instruct",
152
- reasoning: true,
153
- input: ["text", "image"],
154
- cost: {
155
- input: 0.22,
156
- output: 0.88,
157
- cacheRead: 0.22,
158
- cacheWrite: 0,
159
- },
160
- contextWindow: 256000,
161
- maxTokens: 4096,
162
- },
163
118
  // models.dev: synthetic/hf:moonshotai/Kimi-K2-Instruct-0905 → ctx=262144, out=32768
164
119
  {
165
120
  id: "hf:moonshotai/Kimi-K2-Instruct-0905",
@@ -220,61 +175,31 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
220
175
  contextWindow: 262144,
221
176
  maxTokens: 32000,
222
177
  },
223
- // models.dev: synthetic/hf:Qwen/Qwen3-235B-A22B-Instruct-2507 → ctx=256000, out=32000
224
- {
225
- id: "hf:Qwen/Qwen3-235B-A22B-Instruct-2507",
226
- name: "Qwen/Qwen3-235B-A22B-Instruct-2507",
227
- reasoning: false,
228
- input: ["text"],
229
- cost: {
230
- input: 0.22,
231
- output: 0.88,
232
- cacheRead: 0.22,
233
- cacheWrite: 0,
234
- },
235
- contextWindow: 262144,
236
- maxTokens: 32000,
237
- },
238
- // models.dev: synthetic/hf:zai-org/GLM-4.6 → ctx=200000, out=64000
239
- {
240
- id: "hf:zai-org/GLM-4.6",
241
- name: "zai-org/GLM-4.6",
242
- reasoning: true,
243
- input: ["text"],
244
- cost: {
245
- input: 0.55,
246
- output: 2.19,
247
- cacheRead: 0.55,
248
- cacheWrite: 0,
249
- },
250
- contextWindow: 202752,
251
- maxTokens: 64000,
252
- },
253
- // models.dev: synthetic/hf:MiniMaxAI/MiniMax-M2 → ctx=196608, out=131000
178
+ // models.dev: synthetic/hf:moonshotai/Kimi-K2.5 → ctx=262144, out=65536
254
179
  {
255
- id: "hf:MiniMaxAI/MiniMax-M2",
256
- name: "MiniMaxAI/MiniMax-M2",
180
+ id: "hf:moonshotai/Kimi-K2.5",
181
+ name: "moonshotai/Kimi-K2.5",
257
182
  reasoning: true,
258
- input: ["text"],
183
+ input: ["text", "image"],
259
184
  cost: {
260
- input: 0.3,
185
+ input: 1.2,
261
186
  output: 1.2,
262
- cacheRead: 0.3,
187
+ cacheRead: 1.2,
263
188
  cacheWrite: 0,
264
189
  },
265
- contextWindow: 196608,
266
- maxTokens: 131000,
190
+ contextWindow: 262144,
191
+ maxTokens: 65536,
267
192
  },
268
- // models.dev: synthetic/hf:moonshotai/Kimi-K2.5 → ctx=262144, out=65536
193
+ // API: hf:nvidia/Kimi-K2.5-NVFP4 → ctx=262144, out=65536 (NVFP4 quantized)
269
194
  {
270
- id: "hf:moonshotai/Kimi-K2.5",
271
- name: "moonshotai/Kimi-K2.5",
195
+ id: "hf:nvidia/Kimi-K2.5-NVFP4",
196
+ name: "nvidia/Kimi-K2.5-NVFP4",
272
197
  reasoning: true,
273
198
  input: ["text", "image"],
274
199
  cost: {
275
- input: 1.2,
276
- output: 1.2,
277
- cacheRead: 1.2,
200
+ input: 0.6,
201
+ output: 3,
202
+ cacheRead: 0.6,
278
203
  cacheWrite: 0,
279
204
  },
280
205
  contextWindow: 262144,
@@ -8,7 +8,8 @@ import type {
8
8
  import { Text } from "@mariozechner/pi-tui";
9
9
  import { type Static, Type } from "@sinclair/typebox";
10
10
 
11
- // Types
11
+ export const SYNTHETIC_WEB_SEARCH_TOOL = "synthetic_web_search" as const;
12
+
12
13
  interface SyntheticSearchResult {
13
14
  url: string;
14
15
  title: string;
@@ -27,7 +28,6 @@ interface WebSearchDetails {
27
28
  isError?: boolean;
28
29
  }
29
30
 
30
- // Schema
31
31
  const SearchParams = Type.Object({
32
32
  query: Type.String({
33
33
  description: "The search query. Be specific for best results.",
@@ -36,80 +36,9 @@ const SearchParams = Type.Object({
36
36
 
37
37
  type SearchParamsType = Static<typeof SearchParams>;
38
38
 
39
- // Check if API key has subscription access by calling quotas endpoint.
40
- // Returns "ok" if the user has access, or an error message if not.
41
- async function checkSubscriptionAccess(
42
- apiKey: string,
43
- ): Promise<{ ok: true } | { ok: false; reason: string }> {
44
- try {
45
- const response = await fetch("https://api.synthetic.new/v2/quotas", {
46
- method: "GET",
47
- headers: {
48
- Authorization: `Bearer ${apiKey}`,
49
- },
50
- });
51
-
52
- if (!response.ok) {
53
- return {
54
- ok: false,
55
- reason: `Quotas check failed (HTTP ${response.status})`,
56
- };
57
- }
58
-
59
- const data = await response.json();
60
- if (data?.subscription?.limit > 0) {
61
- return { ok: true };
62
- }
63
-
64
- return {
65
- ok: false,
66
- reason: "No active subscription (search requires a subscription plan)",
67
- };
68
- } catch (error) {
69
- const message =
70
- error instanceof Error ? error.message : "Unknown error occurred";
71
- return { ok: false, reason: `Quotas check failed: ${message}` };
72
- }
73
- }
74
-
75
- // Tool Registration
76
- export function registerSyntheticWebSearchTool(pi: ExtensionAPI) {
77
- const apiKey = process.env.SYNTHETIC_API_KEY;
78
- if (!apiKey) {
79
- return;
80
- }
81
-
82
- // Register tool immediately so it's available when tools are collected
83
- registerTool(pi, apiKey);
84
-
85
- // On session start, remove tool from active set, check subscription, re-add if valid
86
- pi.on("session_start", async (_event, ctx) => {
87
- // Disable tool until subscription is verified
88
- const activeTools = pi.getActiveTools();
89
- pi.setActiveTools(activeTools.filter((t) => t !== "synthetic_web_search"));
90
-
91
- const access = await checkSubscriptionAccess(apiKey);
92
- if (!access.ok) {
93
- if (ctx.hasUI) {
94
- ctx.ui.notify(
95
- `Synthetic web search disabled: ${access.reason}`,
96
- "warning",
97
- );
98
- }
99
- return;
100
- }
101
-
102
- // Add tool back to active tools
103
- const current = pi.getActiveTools();
104
- if (!current.includes("synthetic_web_search")) {
105
- pi.setActiveTools([...current, "synthetic_web_search"]);
106
- }
107
- });
108
- }
109
-
110
- function registerTool(pi: ExtensionAPI, apiKey: string) {
39
+ export function registerSyntheticWebSearchTool(pi: ExtensionAPI): void {
111
40
  pi.registerTool<typeof SearchParams, WebSearchDetails>({
112
- name: "synthetic_web_search",
41
+ name: SYNTHETIC_WEB_SEARCH_TOOL,
113
42
  label: "Synthetic: Web Search",
114
43
  description:
115
44
  "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.",
@@ -124,14 +53,21 @@ function registerTool(pi: ExtensionAPI, apiKey: string) {
124
53
  | undefined,
125
54
  _ctx: ExtensionContext,
126
55
  ): Promise<AgentToolResult<WebSearchDetails>> {
127
- // Send progress update
128
56
  onUpdate?.({
129
57
  content: [{ type: "text", text: "Searching..." }],
130
58
  details: { query: params.query },
131
59
  });
132
60
 
133
61
  try {
134
- // Make API request
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
+
135
71
  const response = await fetch("https://api.synthetic.new/v2/search", {
136
72
  method: "POST",
137
73
  headers: {
@@ -151,7 +87,6 @@ function registerTool(pi: ExtensionAPI, apiKey: string) {
151
87
  };
152
88
  }
153
89
 
154
- // Parse response
155
90
  let data: SyntheticSearchResponse;
156
91
  try {
157
92
  data = await response.json();
@@ -166,7 +101,6 @@ function registerTool(pi: ExtensionAPI, apiKey: string) {
166
101
  };
167
102
  }
168
103
 
169
- // Format results for LLM
170
104
  let content = `Found ${data.results.length} result(s):\n\n`;
171
105
  for (const result of data.results) {
172
106
  content += `## ${result.title}\n`;
@@ -184,7 +118,6 @@ function registerTool(pi: ExtensionAPI, apiKey: string) {
184
118
  },
185
119
  };
186
120
  } catch (error) {
187
- // Handle abort signal
188
121
  if (error instanceof Error && error.name === "AbortError") {
189
122
  return {
190
123
  content: [{ type: "text", text: "Search cancelled" }],
@@ -192,7 +125,6 @@ function registerTool(pi: ExtensionAPI, apiKey: string) {
192
125
  };
193
126
  }
194
127
 
195
- // Handle other errors
196
128
  const message =
197
129
  error instanceof Error ? error.message : "Unknown error occurred";
198
130
  return {
@@ -215,7 +147,6 @@ function registerTool(pi: ExtensionAPI, apiKey: string) {
215
147
  ): Text {
216
148
  const { expanded, isPartial } = options;
217
149
 
218
- // Handle partial/loading state
219
150
  if (isPartial) {
220
151
  const text =
221
152
  result.content?.[0]?.type === "text"
@@ -225,8 +156,6 @@ function registerTool(pi: ExtensionAPI, apiKey: string) {
225
156
  }
226
157
 
227
158
  const details = result.details;
228
-
229
- // Handle error state
230
159
  if (details?.isError) {
231
160
  const errorMsg =
232
161
  result.content?.[0]?.type === "text"
@@ -235,21 +164,18 @@ function registerTool(pi: ExtensionAPI, apiKey: string) {
235
164
  return new Text(theme.fg("error", errorMsg), 0, 0);
236
165
  }
237
166
 
238
- // Handle success state
239
167
  const results = details?.results || [];
240
168
  let text = theme.fg("success", `✓ Found ${results.length} result(s)`);
241
169
 
242
- // Collapsed view
243
170
  if (!expanded && results.length > 0) {
244
171
  const first = results[0];
245
172
  text += `\n ${theme.fg("dim", `${first.title}`)}`;
246
173
  if (results.length > 1) {
247
174
  text += theme.fg("dim", ` (${results.length - 1} more)`);
248
175
  }
249
- text += theme.fg("muted", ` [Ctrl+O to expand]`);
176
+ text += theme.fg("muted", " [Ctrl+O to expand]");
250
177
  }
251
178
 
252
- // Expanded view
253
179
  if (expanded) {
254
180
  for (const r of results) {
255
181
  text += `\n\n${theme.fg("accent", theme.bold(r.title))}`;
package/.pi/settings.json DELETED
@@ -1,3 +0,0 @@
1
- {
2
- "packages": ["npm:@aliou/pi-extension-dev"]
3
- }