@aliou/pi-synthetic 0.18.0 → 0.18.2
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/README.md +18 -5
- package/package.json +10 -5
- package/src/extensions/provider/index.ts +39 -12
- package/src/extensions/provider/models.ts +134 -124
- package/src/extensions/web-search/tool.ts +213 -231
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ pi install npm:@aliou/pi-synthetic
|
|
|
41
41
|
pi install git:github.com/aliou/pi-synthetic
|
|
42
42
|
|
|
43
43
|
# Local development
|
|
44
|
-
pi -e
|
|
44
|
+
pi -e .
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
## Usage
|
|
@@ -56,6 +56,8 @@ Once installed, select `synthetic` as your provider and choose from available mo
|
|
|
56
56
|
|
|
57
57
|
All models are accessed through Synthetic's API. Some models are hosted by Synthetic directly (`provider: "synthetic"` in the model config); others are proxied by Synthetic to upstream backends such as Fireworks or Together.
|
|
58
58
|
|
|
59
|
+
Synthetic also provides permanent aliases (`syn:large:text`, `syn:small:text`, `syn:large:vision`, `syn:small:vision`) that route to the current best model for each category. These aliases are stable across model rotations — using an alias means no reconfiguration when models change. Alias models are always visible even when Proxied Models is disabled.
|
|
60
|
+
|
|
59
61
|
By default, new installs show only Synthetic-hosted models. You can enable proxied models in `/synthetic:settings` under **Models > Proxied Models**. Existing configurations keep proxied models enabled to preserve prior behavior.
|
|
60
62
|
|
|
61
63
|
The `provider` field in `src/extensions/provider/models.ts` is for maintenance only and is stripped before registering models with Pi, so users always select the `synthetic` provider.
|
|
@@ -109,13 +111,24 @@ The **Proxied Models** setting is not a loadable extension feature. It is a regu
|
|
|
109
111
|
|
|
110
112
|
## Adding or Updating Models
|
|
111
113
|
|
|
112
|
-
Models are hardcoded in `src/extensions/provider/models.ts`.
|
|
114
|
+
Models are hardcoded in `src/extensions/provider/models.ts`. Entries are a union of concrete models and thin aliases (`syn:*` IDs).
|
|
115
|
+
|
|
116
|
+
### Adding a concrete model
|
|
113
117
|
|
|
114
118
|
1. Edit `src/extensions/provider/models.ts`
|
|
115
|
-
2.
|
|
119
|
+
2. Append a concrete model following the `SyntheticModelConfig` interface
|
|
116
120
|
3. Set `provider` to the upstream backend Synthetic uses for that model, such as `synthetic`, `fireworks`, or `together`
|
|
117
121
|
4. Run `pnpm run typecheck` to verify
|
|
118
122
|
|
|
123
|
+
### Adding an alias model
|
|
124
|
+
|
|
125
|
+
1. Add a thin `{ id, name, aliasFor }` entry at the top of `SYNTHETIC_MODELS`
|
|
126
|
+
2. Set `id` and `name` from the Synthetic API
|
|
127
|
+
3. Set `aliasFor` to `"hf:" + hugging_face_id` from the Synthetic API
|
|
128
|
+
4. The resolved alias inherits all fields from the target at build time
|
|
129
|
+
|
|
130
|
+
When Synthetic changes which model an alias routes to, update only the `aliasFor` field.
|
|
131
|
+
|
|
119
132
|
## Development
|
|
120
133
|
|
|
121
134
|
### Setup
|
|
@@ -152,7 +165,7 @@ pnpm run test
|
|
|
152
165
|
### Test Locally
|
|
153
166
|
|
|
154
167
|
```bash
|
|
155
|
-
pi -e
|
|
168
|
+
pi -e .
|
|
156
169
|
```
|
|
157
170
|
|
|
158
171
|
## Release
|
|
@@ -167,7 +180,7 @@ This repository uses [Changesets](https://github.com/changesets/changesets) for
|
|
|
167
180
|
|
|
168
181
|
## Requirements
|
|
169
182
|
|
|
170
|
-
- Pi coding agent v0.
|
|
183
|
+
- Pi coding agent v0.77.0+
|
|
171
184
|
- Synthetic API key (configured in `~/.pi/agent/auth.json` or via `SYNTHETIC_API_KEY`)
|
|
172
185
|
|
|
173
186
|
## Links
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aliou/pi-synthetic",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -33,8 +33,9 @@
|
|
|
33
33
|
"!src/**/*.test.ts"
|
|
34
34
|
],
|
|
35
35
|
"peerDependencies": {
|
|
36
|
-
"@earendil-works/pi-coding-agent": "
|
|
37
|
-
"@earendil-works/pi-tui": "
|
|
36
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
37
|
+
"@earendil-works/pi-tui": "*",
|
|
38
|
+
"typebox": "*"
|
|
38
39
|
},
|
|
39
40
|
"dependencies": {
|
|
40
41
|
"@aliou/pi-utils-settings": "^0.15.0",
|
|
@@ -44,12 +45,13 @@
|
|
|
44
45
|
"@aliou/biome-plugins": "^0.8.1",
|
|
45
46
|
"@biomejs/biome": "^2.4.15",
|
|
46
47
|
"@changesets/cli": "^2.27.11",
|
|
47
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
48
|
+
"@earendil-works/pi-coding-agent": "0.77.0",
|
|
48
49
|
"typebox": "^1.1.37",
|
|
49
50
|
"@types/node": "^25.0.10",
|
|
50
51
|
"husky": "^9.1.7",
|
|
51
52
|
"typescript": "^5.9.3",
|
|
52
|
-
"vitest": "^4.0.18"
|
|
53
|
+
"vitest": "^4.0.18",
|
|
54
|
+
"@earendil-works/pi-tui": "0.77.0"
|
|
53
55
|
},
|
|
54
56
|
"peerDependenciesMeta": {
|
|
55
57
|
"@earendil-works/pi-coding-agent": {
|
|
@@ -57,6 +59,9 @@
|
|
|
57
59
|
},
|
|
58
60
|
"@earendil-works/pi-tui": {
|
|
59
61
|
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"typebox": {
|
|
64
|
+
"optional": true
|
|
60
65
|
}
|
|
61
66
|
},
|
|
62
67
|
"scripts": {
|
|
@@ -28,19 +28,46 @@ import {
|
|
|
28
28
|
} from "../../types/quotas";
|
|
29
29
|
import { fetchQuotas } from "../../utils/quotas";
|
|
30
30
|
import { SYNTHETIC_OVERFLOW_PATTERN } from "./context-overflow";
|
|
31
|
-
import {
|
|
31
|
+
import {
|
|
32
|
+
type ConcreteSyntheticModelConfig,
|
|
33
|
+
isAlias,
|
|
34
|
+
SYNTHETIC_MODELS,
|
|
35
|
+
} from "./models";
|
|
32
36
|
|
|
33
37
|
export function buildSyntheticProviderModels(includeProxiedModels: boolean) {
|
|
34
|
-
|
|
35
|
-
(
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
const concreteModels = SYNTHETIC_MODELS.filter(
|
|
39
|
+
(m): m is ConcreteSyntheticModelConfig => !isAlias(m),
|
|
40
|
+
);
|
|
41
|
+
const byId = new Map(concreteModels.map((m) => [m.id, m]));
|
|
42
|
+
|
|
43
|
+
const resolved = SYNTHETIC_MODELS.map((entry) => {
|
|
44
|
+
if (!isAlias(entry)) return entry;
|
|
45
|
+
|
|
46
|
+
const target = byId.get(entry.aliasFor);
|
|
47
|
+
if (!target) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Synthetic alias "${entry.id}" references missing model "${entry.aliasFor}"`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
...target,
|
|
55
|
+
id: entry.id,
|
|
56
|
+
name: entry.name,
|
|
57
|
+
provider: "synthetic" as const,
|
|
58
|
+
};
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return resolved
|
|
62
|
+
.filter((model) => includeProxiedModels || model.provider === "synthetic")
|
|
63
|
+
.map(({ provider: _provider, ...model }) => ({
|
|
64
|
+
...model,
|
|
65
|
+
compat: {
|
|
66
|
+
supportsDeveloperRole: false,
|
|
67
|
+
maxTokensField: "max_tokens" as const,
|
|
68
|
+
...model.compat,
|
|
69
|
+
},
|
|
70
|
+
}));
|
|
44
71
|
}
|
|
45
72
|
|
|
46
73
|
interface RegisterSyntheticProviderOptions {
|
|
@@ -53,7 +80,7 @@ export function registerSyntheticProvider(
|
|
|
53
80
|
): void {
|
|
54
81
|
pi.registerProvider("synthetic", {
|
|
55
82
|
baseUrl: "https://api.synthetic.new/openai/v1",
|
|
56
|
-
apiKey: "SYNTHETIC_API_KEY",
|
|
83
|
+
apiKey: "$SYNTHETIC_API_KEY",
|
|
57
84
|
api: "openai-completions",
|
|
58
85
|
headers: {
|
|
59
86
|
Referer: "https://pi.dev",
|
|
@@ -9,14 +9,68 @@ export interface SyntheticModelConfig extends ProviderModelConfig {
|
|
|
9
9
|
provider: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
/** A thin alias that resolves to a concrete model at build time. */
|
|
13
|
+
export interface SyntheticModelAliasConfig {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
/** Full model ID of the concrete target this alias resolves to. */
|
|
17
|
+
aliasFor: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Concrete model with full spec; aliases are excluded. */
|
|
21
|
+
export type ConcreteSyntheticModelConfig = SyntheticModelConfig & {
|
|
22
|
+
aliasFor?: never;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function isAlias(
|
|
26
|
+
entry: ConcreteSyntheticModelConfig | SyntheticModelAliasConfig,
|
|
27
|
+
): entry is SyntheticModelAliasConfig {
|
|
28
|
+
return "aliasFor" in entry;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type SyntheticModelEntry =
|
|
32
|
+
| ConcreteSyntheticModelConfig
|
|
33
|
+
| SyntheticModelAliasConfig;
|
|
34
|
+
|
|
35
|
+
export const SYNTHETIC_MODELS: SyntheticModelEntry[] = [
|
|
36
|
+
// API: syn:large:text → alias for hf:zai-org/GLM-5.1
|
|
37
|
+
{
|
|
38
|
+
id: "syn:large:text",
|
|
39
|
+
name: "syn:large:text",
|
|
40
|
+
aliasFor: "hf:zai-org/GLM-5.1",
|
|
41
|
+
},
|
|
42
|
+
// API: syn:small:text → alias for hf:zai-org/GLM-4.7-Flash
|
|
43
|
+
{
|
|
44
|
+
id: "syn:small:text",
|
|
45
|
+
name: "syn:small:text",
|
|
46
|
+
aliasFor: "hf:zai-org/GLM-4.7-Flash",
|
|
47
|
+
},
|
|
48
|
+
// API: syn:large:vision → alias for hf:moonshotai/Kimi-K2.6
|
|
49
|
+
{
|
|
50
|
+
id: "syn:large:vision",
|
|
51
|
+
name: "syn:large:vision",
|
|
52
|
+
aliasFor: "hf:moonshotai/Kimi-K2.6",
|
|
53
|
+
},
|
|
54
|
+
// API: syn:small:vision → alias for hf:Qwen/Qwen3.6-27B
|
|
55
|
+
{
|
|
56
|
+
id: "syn:small:vision",
|
|
57
|
+
name: "syn:small:vision",
|
|
58
|
+
aliasFor: "hf:Qwen/Qwen3.6-27B",
|
|
59
|
+
},
|
|
13
60
|
// API: hf:zai-org/GLM-4.7 → ctx=202752
|
|
14
61
|
{
|
|
15
62
|
id: "hf:zai-org/GLM-4.7",
|
|
16
63
|
name: "zai-org/GLM-4.7",
|
|
17
64
|
provider: "synthetic",
|
|
18
65
|
reasoning: true,
|
|
19
|
-
thinkingLevelMap: {
|
|
66
|
+
thinkingLevelMap: {
|
|
67
|
+
off: "none",
|
|
68
|
+
minimal: null,
|
|
69
|
+
low: null,
|
|
70
|
+
medium: "medium",
|
|
71
|
+
high: null,
|
|
72
|
+
xhigh: null,
|
|
73
|
+
},
|
|
20
74
|
compat: {
|
|
21
75
|
supportsReasoningEffort: true,
|
|
22
76
|
},
|
|
@@ -30,33 +84,20 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
|
30
84
|
contextWindow: 202752,
|
|
31
85
|
maxTokens: 65536,
|
|
32
86
|
},
|
|
33
|
-
// API: hf:zai-org/GLM-5 → ctx=196608, out=65536
|
|
34
|
-
{
|
|
35
|
-
id: "hf:zai-org/GLM-5",
|
|
36
|
-
name: "zai-org/GLM-5",
|
|
37
|
-
provider: "synthetic",
|
|
38
|
-
reasoning: true,
|
|
39
|
-
thinkingLevelMap: { minimal: null, xhigh: null },
|
|
40
|
-
compat: {
|
|
41
|
-
supportsReasoningEffort: true,
|
|
42
|
-
},
|
|
43
|
-
input: ["text"],
|
|
44
|
-
cost: {
|
|
45
|
-
input: 1,
|
|
46
|
-
output: 3,
|
|
47
|
-
cacheRead: 1,
|
|
48
|
-
cacheWrite: 0,
|
|
49
|
-
},
|
|
50
|
-
contextWindow: 196608,
|
|
51
|
-
maxTokens: 65536,
|
|
52
|
-
},
|
|
53
87
|
// API: hf:zai-org/GLM-5.1 → ctx=196608, out=65536
|
|
54
88
|
{
|
|
55
89
|
id: "hf:zai-org/GLM-5.1",
|
|
56
90
|
name: "zai-org/GLM-5.1",
|
|
57
91
|
provider: "synthetic",
|
|
58
92
|
reasoning: true,
|
|
59
|
-
thinkingLevelMap: {
|
|
93
|
+
thinkingLevelMap: {
|
|
94
|
+
off: "none",
|
|
95
|
+
minimal: null,
|
|
96
|
+
low: null,
|
|
97
|
+
medium: "medium",
|
|
98
|
+
high: null,
|
|
99
|
+
xhigh: null,
|
|
100
|
+
},
|
|
60
101
|
compat: {
|
|
61
102
|
supportsReasoningEffort: true,
|
|
62
103
|
supportsDeveloperRole: false,
|
|
@@ -77,7 +118,14 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
|
77
118
|
name: "zai-org/GLM-4.7-Flash",
|
|
78
119
|
provider: "synthetic",
|
|
79
120
|
reasoning: true,
|
|
80
|
-
thinkingLevelMap: {
|
|
121
|
+
thinkingLevelMap: {
|
|
122
|
+
off: "none",
|
|
123
|
+
minimal: null,
|
|
124
|
+
low: null,
|
|
125
|
+
medium: "medium",
|
|
126
|
+
high: null,
|
|
127
|
+
xhigh: null,
|
|
128
|
+
},
|
|
81
129
|
compat: {
|
|
82
130
|
supportsReasoningEffort: true,
|
|
83
131
|
},
|
|
@@ -91,22 +139,6 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
|
91
139
|
contextWindow: 196608,
|
|
92
140
|
maxTokens: 65536,
|
|
93
141
|
},
|
|
94
|
-
// models.dev: synthetic/hf:deepseek-ai/DeepSeek-V3.2 → ctx=162816, out=8000
|
|
95
|
-
{
|
|
96
|
-
id: "hf:deepseek-ai/DeepSeek-V3.2",
|
|
97
|
-
name: "deepseek-ai/DeepSeek-V3.2",
|
|
98
|
-
provider: "fireworks",
|
|
99
|
-
reasoning: true,
|
|
100
|
-
input: ["text"],
|
|
101
|
-
cost: {
|
|
102
|
-
input: 0.56,
|
|
103
|
-
output: 1.68,
|
|
104
|
-
cacheRead: 0.56,
|
|
105
|
-
cacheWrite: 0,
|
|
106
|
-
},
|
|
107
|
-
contextWindow: 162816,
|
|
108
|
-
maxTokens: 8000,
|
|
109
|
-
},
|
|
110
142
|
// models.dev: synthetic/hf:openai/gpt-oss-120b → ctx=128000, out=32768
|
|
111
143
|
{
|
|
112
144
|
id: "hf:openai/gpt-oss-120b",
|
|
@@ -145,7 +177,14 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
|
145
177
|
name: "moonshotai/Kimi-K2.6",
|
|
146
178
|
provider: "synthetic",
|
|
147
179
|
reasoning: true,
|
|
148
|
-
thinkingLevelMap: {
|
|
180
|
+
thinkingLevelMap: {
|
|
181
|
+
off: "none",
|
|
182
|
+
minimal: null,
|
|
183
|
+
low: null,
|
|
184
|
+
medium: "medium",
|
|
185
|
+
high: null,
|
|
186
|
+
xhigh: null,
|
|
187
|
+
},
|
|
149
188
|
compat: {
|
|
150
189
|
supportsReasoningEffort: true,
|
|
151
190
|
},
|
|
@@ -165,6 +204,17 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
|
165
204
|
name: "Qwen/Qwen3.5-397B-A17B",
|
|
166
205
|
provider: "together",
|
|
167
206
|
reasoning: true,
|
|
207
|
+
thinkingLevelMap: {
|
|
208
|
+
off: "none",
|
|
209
|
+
minimal: null,
|
|
210
|
+
low: null,
|
|
211
|
+
medium: "medium",
|
|
212
|
+
high: null,
|
|
213
|
+
xhigh: null,
|
|
214
|
+
},
|
|
215
|
+
compat: {
|
|
216
|
+
supportsReasoningEffort: true,
|
|
217
|
+
},
|
|
168
218
|
input: ["text", "image"],
|
|
169
219
|
cost: {
|
|
170
220
|
input: 0.6,
|
|
@@ -175,123 +225,83 @@ export const SYNTHETIC_MODELS: SyntheticModelConfig[] = [
|
|
|
175
225
|
contextWindow: 262144,
|
|
176
226
|
maxTokens: 65536,
|
|
177
227
|
},
|
|
178
|
-
// API: hf:
|
|
228
|
+
// API: hf:Qwen/Qwen3.6-27B → ctx=262144, out=65536
|
|
179
229
|
{
|
|
180
|
-
id: "hf:
|
|
181
|
-
name: "
|
|
230
|
+
id: "hf:Qwen/Qwen3.6-27B",
|
|
231
|
+
name: "Qwen/Qwen3.6-27B",
|
|
182
232
|
provider: "synthetic",
|
|
183
233
|
reasoning: true,
|
|
184
|
-
thinkingLevelMap: {
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
},
|
|
192
|
-
contextWindow: 191488,
|
|
193
|
-
maxTokens: 65536,
|
|
194
|
-
compat: {
|
|
195
|
-
supportsReasoningEffort: true,
|
|
196
|
-
maxTokensField: "max_completion_tokens",
|
|
234
|
+
thinkingLevelMap: {
|
|
235
|
+
off: "none",
|
|
236
|
+
minimal: null,
|
|
237
|
+
low: null,
|
|
238
|
+
medium: "medium",
|
|
239
|
+
high: null,
|
|
240
|
+
xhigh: null,
|
|
197
241
|
},
|
|
198
|
-
},
|
|
199
|
-
// API: hf:nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4 → ctx=262144, out=65536
|
|
200
|
-
{
|
|
201
|
-
id: "hf:nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4",
|
|
202
|
-
name: "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4",
|
|
203
|
-
provider: "synthetic",
|
|
204
|
-
reasoning: true,
|
|
205
|
-
thinkingLevelMap: { minimal: null, low: null, xhigh: null },
|
|
206
242
|
compat: {
|
|
207
243
|
supportsReasoningEffort: true,
|
|
208
244
|
},
|
|
209
|
-
input: ["text"],
|
|
245
|
+
input: ["text", "image"],
|
|
210
246
|
cost: {
|
|
211
|
-
input: 0.
|
|
212
|
-
output:
|
|
213
|
-
cacheRead: 0.
|
|
247
|
+
input: 0.45,
|
|
248
|
+
output: 3.6,
|
|
249
|
+
cacheRead: 0.45,
|
|
214
250
|
cacheWrite: 0,
|
|
215
251
|
},
|
|
216
252
|
contextWindow: 262144,
|
|
217
253
|
maxTokens: 65536,
|
|
218
254
|
},
|
|
219
|
-
// API:
|
|
255
|
+
// API: hf:MiniMaxAI/MiniMax-M2.5 → ctx=191488, out=65536
|
|
220
256
|
{
|
|
221
|
-
id: "
|
|
222
|
-
name: "
|
|
257
|
+
id: "hf:MiniMaxAI/MiniMax-M2.5",
|
|
258
|
+
name: "MiniMaxAI/MiniMax-M2.5",
|
|
223
259
|
provider: "synthetic",
|
|
224
260
|
reasoning: true,
|
|
225
|
-
thinkingLevelMap: {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
261
|
+
thinkingLevelMap: {
|
|
262
|
+
off: null,
|
|
263
|
+
minimal: null,
|
|
264
|
+
low: null,
|
|
265
|
+
medium: "medium",
|
|
266
|
+
high: null,
|
|
267
|
+
xhigh: null,
|
|
229
268
|
},
|
|
230
269
|
input: ["text"],
|
|
231
270
|
cost: {
|
|
232
|
-
input:
|
|
233
|
-
output:
|
|
234
|
-
cacheRead:
|
|
271
|
+
input: 0.4,
|
|
272
|
+
output: 2,
|
|
273
|
+
cacheRead: 0.4,
|
|
235
274
|
cacheWrite: 0,
|
|
236
275
|
},
|
|
237
|
-
contextWindow:
|
|
276
|
+
contextWindow: 191488,
|
|
238
277
|
maxTokens: 65536,
|
|
239
|
-
},
|
|
240
|
-
// API: syn:small:text → alias for hf:zai-org/GLM-4.7-Flash → ctx=196608, out=65536
|
|
241
|
-
{
|
|
242
|
-
id: "syn:small:text",
|
|
243
|
-
name: "syn:small:text",
|
|
244
|
-
provider: "synthetic",
|
|
245
|
-
reasoning: true,
|
|
246
|
-
thinkingLevelMap: { minimal: null, xhigh: null },
|
|
247
278
|
compat: {
|
|
248
279
|
supportsReasoningEffort: true,
|
|
280
|
+
maxTokensField: "max_completion_tokens",
|
|
249
281
|
},
|
|
250
|
-
input: ["text"],
|
|
251
|
-
cost: {
|
|
252
|
-
input: 0.1,
|
|
253
|
-
output: 0.5,
|
|
254
|
-
cacheRead: 0.1,
|
|
255
|
-
cacheWrite: 0,
|
|
256
|
-
},
|
|
257
|
-
contextWindow: 196608,
|
|
258
|
-
maxTokens: 65536,
|
|
259
282
|
},
|
|
260
|
-
// API:
|
|
283
|
+
// API: hf:nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4 → ctx=262144, out=65536
|
|
261
284
|
{
|
|
262
|
-
id: "
|
|
263
|
-
name: "
|
|
285
|
+
id: "hf:nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4",
|
|
286
|
+
name: "nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4",
|
|
264
287
|
provider: "synthetic",
|
|
265
288
|
reasoning: true,
|
|
266
|
-
thinkingLevelMap: {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
output: 4,
|
|
274
|
-
cacheRead: 0.95,
|
|
275
|
-
cacheWrite: 0,
|
|
289
|
+
thinkingLevelMap: {
|
|
290
|
+
off: "none",
|
|
291
|
+
minimal: null,
|
|
292
|
+
low: null,
|
|
293
|
+
medium: "medium",
|
|
294
|
+
high: null,
|
|
295
|
+
xhigh: null,
|
|
276
296
|
},
|
|
277
|
-
contextWindow: 262144,
|
|
278
|
-
maxTokens: 65536,
|
|
279
|
-
},
|
|
280
|
-
// API: syn:small:vision → alias for hf:moonshotai/Kimi-K2.6 → ctx=262144, out=65536
|
|
281
|
-
{
|
|
282
|
-
id: "syn:small:vision",
|
|
283
|
-
name: "syn:small:vision",
|
|
284
|
-
provider: "synthetic",
|
|
285
|
-
reasoning: true,
|
|
286
|
-
thinkingLevelMap: { minimal: null, low: null, xhigh: null },
|
|
287
297
|
compat: {
|
|
288
298
|
supportsReasoningEffort: true,
|
|
289
299
|
},
|
|
290
|
-
input: ["text"
|
|
300
|
+
input: ["text"],
|
|
291
301
|
cost: {
|
|
292
|
-
input: 0.
|
|
293
|
-
output:
|
|
294
|
-
cacheRead: 0.
|
|
302
|
+
input: 0.3,
|
|
303
|
+
output: 1,
|
|
304
|
+
cacheRead: 0.3,
|
|
295
305
|
cacheWrite: 0,
|
|
296
306
|
},
|
|
297
307
|
contextWindow: 262144,
|
|
@@ -3,16 +3,11 @@ import { writeFile } from "node:fs/promises";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { ToolCallHeader, ToolFooter } from "@aliou/pi-utils-ui";
|
|
6
|
-
import type {
|
|
7
|
-
AgentToolResult,
|
|
8
|
-
ExtensionAPI,
|
|
9
|
-
ExtensionContext,
|
|
10
|
-
Theme,
|
|
11
|
-
ToolRenderResultOptions,
|
|
12
|
-
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
|
|
13
7
|
import {
|
|
14
8
|
DEFAULT_MAX_BYTES,
|
|
15
9
|
DEFAULT_MAX_LINES,
|
|
10
|
+
defineTool,
|
|
16
11
|
formatSize,
|
|
17
12
|
keyHint,
|
|
18
13
|
truncateHead,
|
|
@@ -60,261 +55,248 @@ const SearchParams = Type.Object({
|
|
|
60
55
|
|
|
61
56
|
type SearchParamsType = Static<typeof SearchParams>;
|
|
62
57
|
|
|
63
|
-
export
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
58
|
+
export const syntheticWebSearchTool = defineTool({
|
|
59
|
+
name: SYNTHETIC_WEB_SEARCH_TOOL,
|
|
60
|
+
label: "Synthetic: Web Search",
|
|
61
|
+
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.`,
|
|
62
|
+
promptSnippet: "Search the web using Synthetic's zero-data-retention API",
|
|
63
|
+
promptGuidelines: [
|
|
64
|
+
"Use synthetic_web_search for finding documentation, articles, recent information, or any web content.",
|
|
65
|
+
"Write specific queries with names, dates, versions, or locations for synthetic_web_search.",
|
|
66
|
+
"synthetic_web_search results are fresh and not cached by Synthetic.",
|
|
67
|
+
],
|
|
68
|
+
parameters: SearchParams,
|
|
69
|
+
|
|
70
|
+
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
71
|
+
onUpdate?.({
|
|
72
|
+
content: [{ type: "text", text: "Searching..." }],
|
|
73
|
+
details: { query: params.query },
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (!configLoader.getConfig().webSearch) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
"Synthetic web search is disabled. Re-enable it with synthetic:settings or pi config.",
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const apiKey = await getSyntheticApiKey(ctx.modelRegistry.authStorage);
|
|
83
|
+
if (!apiKey) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
"Synthetic web search requires a Synthetic subscription. Add credentials to ~/.pi/agent/auth.json or set SYNTHETIC_API_KEY environment variable.",
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const response = await fetch("https://api.synthetic.new/v2/search", {
|
|
90
|
+
method: "POST",
|
|
91
|
+
headers: {
|
|
92
|
+
Authorization: `Bearer ${apiKey}`,
|
|
93
|
+
"Content-Type": "application/json",
|
|
94
|
+
},
|
|
95
|
+
body: JSON.stringify({ query: params.query }),
|
|
96
|
+
signal,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
const errorText = await response.text();
|
|
101
|
+
throw new Error(`Search API error: ${response.status} ${errorText}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let data: SyntheticSearchResponse;
|
|
105
|
+
try {
|
|
106
|
+
data = await response.json();
|
|
107
|
+
} catch (parseError) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
parseError instanceof Error
|
|
110
|
+
? `Failed to parse search results: ${parseError.message}`
|
|
111
|
+
: "Failed to parse search results",
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let content = `Found ${data.results.length} result(s):\n\n`;
|
|
116
|
+
const resultDetails: WebSearchResultDetails[] = [];
|
|
117
|
+
|
|
118
|
+
for (let i = 0; i < data.results.length; i++) {
|
|
119
|
+
const result = data.results[i];
|
|
120
|
+
const slug = result.title
|
|
121
|
+
.toLowerCase()
|
|
122
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
123
|
+
.replace(/(^-|-$)/g, "")
|
|
124
|
+
.slice(0, 40);
|
|
125
|
+
const truncation = truncateHead(result.text, {
|
|
126
|
+
maxLines: DEFAULT_MAX_LINES,
|
|
127
|
+
maxBytes: DEFAULT_MAX_BYTES,
|
|
88
128
|
});
|
|
89
129
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
"Synthetic web search is disabled. Re-enable it with synthetic:settings or pi config.",
|
|
93
|
-
);
|
|
94
|
-
}
|
|
130
|
+
let preview = truncation.content;
|
|
131
|
+
let tempFilePath: string | undefined;
|
|
95
132
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
133
|
+
if (truncation.truncated) {
|
|
134
|
+
tempFilePath = join(
|
|
135
|
+
tmpdir(),
|
|
136
|
+
`pi-synthetic-search-${slug}-${randomBytes(4).toString("hex")}.md`,
|
|
100
137
|
);
|
|
138
|
+
await writeFile(tempFilePath, result.text, "utf8");
|
|
139
|
+
preview += `\n\n[Result truncated: ${truncation.outputLines} of ${truncation.totalLines} lines (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}). Full result: ${tempFilePath}]`;
|
|
101
140
|
}
|
|
102
141
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
142
|
+
content += `## ${result.title}\n`;
|
|
143
|
+
content += `URL: ${result.url}\n`;
|
|
144
|
+
content += `Published: ${result.published}\n`;
|
|
145
|
+
content += `\n${preview}\n`;
|
|
146
|
+
content += "\n---\n\n";
|
|
147
|
+
|
|
148
|
+
resultDetails.push({
|
|
149
|
+
title: result.title,
|
|
150
|
+
url: result.url,
|
|
151
|
+
published: result.published,
|
|
152
|
+
truncated: truncation.truncated,
|
|
153
|
+
tempFilePath,
|
|
154
|
+
totalLines: truncation.totalLines,
|
|
155
|
+
totalBytes: truncation.totalBytes,
|
|
156
|
+
outputLines: truncation.outputLines,
|
|
157
|
+
outputBytes: truncation.outputBytes,
|
|
111
158
|
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
content: [{ type: "text", text: content }],
|
|
163
|
+
details: {
|
|
164
|
+
results: resultDetails,
|
|
165
|
+
query: params.query,
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
renderCall(args: SearchParamsType, theme: Theme) {
|
|
171
|
+
return new ToolCallHeader(
|
|
172
|
+
{
|
|
173
|
+
toolName: "Synthetic: WebSearch",
|
|
174
|
+
mainArg: `"${args.query}"`,
|
|
175
|
+
showColon: true,
|
|
176
|
+
},
|
|
177
|
+
theme,
|
|
178
|
+
);
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
renderResult(result, options, theme: Theme) {
|
|
182
|
+
const { expanded, isPartial } = options;
|
|
183
|
+
|
|
184
|
+
if (isPartial) {
|
|
185
|
+
return new Text(
|
|
186
|
+
theme.fg("muted", "Synthetic: WebSearch: fetching..."),
|
|
187
|
+
0,
|
|
188
|
+
0,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const details = result.details as WebSearchDetails | undefined;
|
|
193
|
+
const results = details?.results || [];
|
|
194
|
+
const container = new Container();
|
|
195
|
+
|
|
196
|
+
// When the tool throws, the framework calls renderResult with
|
|
197
|
+
// details={} (empty object) and the error message in content.
|
|
198
|
+
// Detect this by checking for missing results in details.
|
|
199
|
+
if (!details?.results) {
|
|
200
|
+
const textBlock = result.content.find((c) => c.type === "text");
|
|
201
|
+
const errorMsg =
|
|
202
|
+
(textBlock?.type === "text" && textBlock.text) || "Search failed";
|
|
203
|
+
container.addChild(new Text(theme.fg("error", errorMsg), 0, 0));
|
|
204
|
+
return container;
|
|
205
|
+
}
|
|
112
206
|
|
|
113
|
-
|
|
114
|
-
const errorText = await response.text();
|
|
115
|
-
throw new Error(`Search API error: ${response.status} ${errorText}`);
|
|
116
|
-
}
|
|
207
|
+
const hasTruncation = results.some((r) => r.truncated);
|
|
117
208
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
);
|
|
209
|
+
if (results.length === 0) {
|
|
210
|
+
container.addChild(
|
|
211
|
+
new Text(theme.fg("muted", "Synthetic: WebSearch: no results"), 0, 0),
|
|
212
|
+
);
|
|
213
|
+
} else if (!expanded) {
|
|
214
|
+
// Collapsed: show result count + first result title
|
|
215
|
+
let text = theme.fg("success", `Found ${results.length} result(s)`);
|
|
216
|
+
if (hasTruncation) {
|
|
217
|
+
text += theme.fg("warning", " (truncated)");
|
|
127
218
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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}]`;
|
|
219
|
+
const first = results[0];
|
|
220
|
+
if (first) {
|
|
221
|
+
text += `\n ${theme.fg("dim", first.title)}`;
|
|
222
|
+
if (results.length > 1) {
|
|
223
|
+
text += theme.fg("dim", ` (+${results.length - 1} more)`);
|
|
154
224
|
}
|
|
155
|
-
|
|
156
|
-
content += `## ${result.title}\n`;
|
|
157
|
-
content += `URL: ${result.url}\n`;
|
|
158
|
-
content += `Published: ${result.published}\n`;
|
|
159
|
-
content += `\n${preview}\n`;
|
|
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
|
-
});
|
|
173
225
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
};
|
|
182
|
-
},
|
|
183
|
-
|
|
184
|
-
renderCall(args: SearchParamsType, theme: Theme) {
|
|
185
|
-
return new ToolCallHeader(
|
|
186
|
-
{
|
|
187
|
-
toolName: "Synthetic: WebSearch",
|
|
188
|
-
mainArg: `"${args.query}"`,
|
|
189
|
-
showColon: true,
|
|
190
|
-
},
|
|
191
|
-
theme,
|
|
192
|
-
);
|
|
193
|
-
},
|
|
194
|
-
|
|
195
|
-
renderResult(
|
|
196
|
-
result: AgentToolResult<WebSearchDetails>,
|
|
197
|
-
options: ToolRenderResultOptions,
|
|
198
|
-
theme: Theme,
|
|
199
|
-
) {
|
|
200
|
-
const { expanded, isPartial } = options;
|
|
201
|
-
|
|
202
|
-
if (isPartial) {
|
|
203
|
-
return new Text(
|
|
204
|
-
theme.fg("muted", "Synthetic: WebSearch: fetching..."),
|
|
226
|
+
text += theme.fg("muted", ` ${keyHint("app.tools.expand", "to expand")}`);
|
|
227
|
+
container.addChild(new Text(text, 0, 0));
|
|
228
|
+
} else {
|
|
229
|
+
// Expanded: show each result with title, URL, date, and snippet
|
|
230
|
+
container.addChild(
|
|
231
|
+
new Text(
|
|
232
|
+
theme.fg("success", `Found ${results.length} result(s)`),
|
|
205
233
|
0,
|
|
206
234
|
0,
|
|
207
|
-
)
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const details = result.details;
|
|
211
|
-
const results = details?.results || [];
|
|
212
|
-
const container = new Container();
|
|
213
|
-
|
|
214
|
-
// When the tool throws, the framework calls renderResult with
|
|
215
|
-
// details={} (empty object) and the error message in content.
|
|
216
|
-
// Detect this by checking for missing results in details.
|
|
217
|
-
if (!details?.results) {
|
|
218
|
-
const textBlock = result.content.find((c) => c.type === "text");
|
|
219
|
-
const errorMsg =
|
|
220
|
-
(textBlock?.type === "text" && textBlock.text) || "Search failed";
|
|
221
|
-
container.addChild(new Text(theme.fg("error", errorMsg), 0, 0));
|
|
222
|
-
return container;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const hasTruncation = results.some((r) => r.truncated);
|
|
235
|
+
),
|
|
236
|
+
);
|
|
226
237
|
|
|
227
|
-
|
|
228
|
-
container.addChild(
|
|
229
|
-
new Text(theme.fg("muted", "Synthetic: WebSearch: no results"), 0, 0),
|
|
230
|
-
);
|
|
231
|
-
} else if (!expanded) {
|
|
232
|
-
// Collapsed: show result count + first result title
|
|
233
|
-
let text = theme.fg("success", `Found ${results.length} result(s)`);
|
|
234
|
-
if (hasTruncation) {
|
|
235
|
-
text += theme.fg("warning", " (truncated)");
|
|
236
|
-
}
|
|
237
|
-
const first = results[0];
|
|
238
|
-
if (first) {
|
|
239
|
-
text += `\n ${theme.fg("dim", first.title)}`;
|
|
240
|
-
if (results.length > 1) {
|
|
241
|
-
text += theme.fg("dim", ` (+${results.length - 1} more)`);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
text += theme.fg(
|
|
245
|
-
"muted",
|
|
246
|
-
` ${keyHint("app.tools.expand", "to expand")}`,
|
|
247
|
-
);
|
|
248
|
-
container.addChild(new Text(text, 0, 0));
|
|
249
|
-
} else {
|
|
250
|
-
// Expanded: show each result with title, URL, date, and snippet
|
|
238
|
+
for (const r of results) {
|
|
239
|
+
container.addChild(new Text("", 0, 0));
|
|
251
240
|
container.addChild(
|
|
252
241
|
new Text(
|
|
253
|
-
theme.fg("
|
|
242
|
+
`${theme.fg("dim", ">")} ${theme.fg("accent", theme.bold(r.title))}`,
|
|
254
243
|
0,
|
|
255
244
|
0,
|
|
256
245
|
),
|
|
257
246
|
);
|
|
247
|
+
container.addChild(new Text(` ${theme.fg("dim", r.url)}`, 0, 0));
|
|
248
|
+
if (r.published) {
|
|
249
|
+
container.addChild(
|
|
250
|
+
new Text(
|
|
251
|
+
` ${theme.fg("muted", `Published: ${r.published}`)}`,
|
|
252
|
+
0,
|
|
253
|
+
0,
|
|
254
|
+
),
|
|
255
|
+
);
|
|
256
|
+
}
|
|
258
257
|
|
|
259
|
-
|
|
260
|
-
container.addChild(new Text("", 0, 0));
|
|
258
|
+
if (r.truncated) {
|
|
261
259
|
container.addChild(
|
|
262
260
|
new Text(
|
|
263
|
-
|
|
261
|
+
` ${theme.fg("warning", `Truncated: ${r.outputLines} of ${r.totalLines} lines (${formatSize(r.outputBytes)} of ${formatSize(r.totalBytes)}). Full content: ${r.tempFilePath}`)}`,
|
|
264
262
|
0,
|
|
265
263
|
0,
|
|
266
264
|
),
|
|
267
265
|
);
|
|
268
|
-
container.addChild(new Text(` ${theme.fg("dim", r.url)}`, 0, 0));
|
|
269
|
-
if (r.published) {
|
|
270
|
-
container.addChild(
|
|
271
|
-
new Text(
|
|
272
|
-
` ${theme.fg("muted", `Published: ${r.published}`)}`,
|
|
273
|
-
0,
|
|
274
|
-
0,
|
|
275
|
-
),
|
|
276
|
-
);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (r.truncated) {
|
|
280
|
-
container.addChild(
|
|
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
|
-
),
|
|
286
|
-
);
|
|
287
|
-
}
|
|
288
266
|
}
|
|
289
267
|
}
|
|
290
|
-
|
|
291
|
-
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const footerItems: { label: string; value: string }[] = [];
|
|
271
|
+
footerItems.push({
|
|
272
|
+
label: "results",
|
|
273
|
+
value: `${results.length} result(s)`,
|
|
274
|
+
});
|
|
275
|
+
if (hasTruncation) {
|
|
276
|
+
const truncatedCount = results.filter((r) => r.truncated).length;
|
|
292
277
|
footerItems.push({
|
|
293
|
-
label: "
|
|
294
|
-
value: `${
|
|
278
|
+
label: "truncated",
|
|
279
|
+
value: `${truncatedCount}`,
|
|
295
280
|
});
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}),
|
|
315
|
-
);
|
|
281
|
+
}
|
|
282
|
+
if (!expanded) {
|
|
283
|
+
footerItems.push({
|
|
284
|
+
label: "",
|
|
285
|
+
value: keyHint("app.tools.expand", "to expand"),
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
container.addChild(new Text("", 0, 0));
|
|
289
|
+
container.addChild(
|
|
290
|
+
new ToolFooter(theme, {
|
|
291
|
+
items: footerItems,
|
|
292
|
+
separator: " | ",
|
|
293
|
+
}),
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
return container;
|
|
297
|
+
},
|
|
298
|
+
});
|
|
316
299
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
});
|
|
300
|
+
export function registerSyntheticWebSearchTool(pi: ExtensionAPI): void {
|
|
301
|
+
pi.registerTool(syntheticWebSearchTool);
|
|
320
302
|
}
|