@aliou/pi-synthetic 0.19.0 → 0.20.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/README.md +9 -3
- package/package.json +1 -1
- package/src/config.ts +103 -1
- package/src/extensions/command-quotas/command.ts +34 -8
- package/src/extensions/provider/index.ts +34 -15
- package/src/extensions/web-search/tool.ts +35 -12
- package/src/lib/utility-api.ts +85 -0
- package/src/utils/quotas.ts +31 -6
package/README.md
CHANGED
|
@@ -64,7 +64,7 @@ The `provider` field in `src/extensions/provider/models.ts` is for maintenance o
|
|
|
64
64
|
|
|
65
65
|
### Web Search Tool
|
|
66
66
|
|
|
67
|
-
The extension registers `synthetic_web_search` — a zero-data-retention web search tool. The tool is always visible; it fails with a clear message if credentials are missing
|
|
67
|
+
The extension registers `synthetic_web_search` — a zero-data-retention web search tool. The tool is always visible; it fails with a clear message if credentials are missing, the account lacks a subscription, or the configured utility proxy rejects the request.
|
|
68
68
|
|
|
69
69
|
### Reasoning Levels
|
|
70
70
|
|
|
@@ -85,6 +85,12 @@ Check your API usage:
|
|
|
85
85
|
/synthetic:quotas
|
|
86
86
|
```
|
|
87
87
|
|
|
88
|
+
### Utility API Proxy
|
|
89
|
+
|
|
90
|
+
Web search and quotas can use a proxy instead of `https://api.synthetic.new`. Configure it with `/synthetic:settings` under **Connection > Utility API Proxy**. The proxy is only used for `/v2/search` and `/v2/quotas`; model provider calls still use Synthetic's OpenAI-compatible endpoint directly.
|
|
91
|
+
|
|
92
|
+
If the proxy requires auth, pi-synthetic still requires a Synthetic API key and sends `Authorization: Bearer ...`. If the proxy does not require auth, disable **Requires auth** and these utility calls skip API key checks and omit `Authorization`.
|
|
93
|
+
|
|
88
94
|
### Usage Status
|
|
89
95
|
|
|
90
96
|
When a Synthetic model is active, the footer status bar shows live quota usage (e.g. `week:82% (↺in 3d) 5h:95%`). Colors follow the same severity assessment as quota warnings: green by default, yellow/red only when projected usage is at risk. The status auto-refreshes every 60 seconds and after each turn.
|
|
@@ -107,7 +113,7 @@ pi config extensions.disabled add @aliou/pi-synthetic/quota-warnings
|
|
|
107
113
|
|
|
108
114
|
This prevents the quota-warnings extension from loading while keeping the rest of pi-synthetic active. Replace `quota-warnings` with `web-search`, `command-quotas`, `sub-bar-integration`, `usage-status`, or `provider` to disable other features.
|
|
109
115
|
|
|
110
|
-
The **Proxied Models**
|
|
116
|
+
The **Proxied Models** and **Utility API Proxy** settings are not loadable extension features. They are regular settings controlled through `/synthetic:settings`.
|
|
111
117
|
|
|
112
118
|
## Adding or Updating Models
|
|
113
119
|
|
|
@@ -181,7 +187,7 @@ This repository uses [Changesets](https://github.com/changesets/changesets) for
|
|
|
181
187
|
## Requirements
|
|
182
188
|
|
|
183
189
|
- Pi coding agent v0.77.0+
|
|
184
|
-
- Synthetic API key (configured in `~/.pi/agent/auth.json` or via `SYNTHETIC_API_KEY`)
|
|
190
|
+
- Synthetic API key (configured in `~/.pi/agent/auth.json` or via `SYNTHETIC_API_KEY`) for model provider calls and authenticated utility API calls
|
|
185
191
|
|
|
186
192
|
## Links
|
|
187
193
|
|
package/package.json
CHANGED
package/src/config.ts
CHANGED
|
@@ -2,11 +2,17 @@ import {
|
|
|
2
2
|
ConfigLoader,
|
|
3
3
|
type Migration,
|
|
4
4
|
registerSettingsCommand,
|
|
5
|
+
SettingsDetailEditor,
|
|
5
6
|
type SettingsSection,
|
|
6
7
|
} from "@aliou/pi-utils-settings";
|
|
7
8
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
8
9
|
import type { SettingItem } from "@earendil-works/pi-tui";
|
|
9
10
|
import pkg from "../package.json" with { type: "json" };
|
|
11
|
+
import {
|
|
12
|
+
formatSyntheticUtilityApiProxySummary,
|
|
13
|
+
hasSyntheticUtilityApiProxy,
|
|
14
|
+
validateSyntheticUtilityApiProxyUrl,
|
|
15
|
+
} from "./lib/utility-api";
|
|
10
16
|
|
|
11
17
|
export type SyntheticFeatureId =
|
|
12
18
|
| "webSearch"
|
|
@@ -36,6 +42,8 @@ export interface SyntheticConfig {
|
|
|
36
42
|
quotaWarnings?: boolean;
|
|
37
43
|
subBarIntegration?: boolean;
|
|
38
44
|
proxiedModels?: boolean;
|
|
45
|
+
proxyUrl?: string;
|
|
46
|
+
proxyRequiresAuth?: boolean;
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
export interface ResolvedSyntheticConfig {
|
|
@@ -46,6 +54,8 @@ export interface ResolvedSyntheticConfig {
|
|
|
46
54
|
quotaWarnings: boolean;
|
|
47
55
|
subBarIntegration: boolean;
|
|
48
56
|
proxiedModels: boolean;
|
|
57
|
+
proxyUrl: string;
|
|
58
|
+
proxyRequiresAuth: boolean;
|
|
49
59
|
}
|
|
50
60
|
|
|
51
61
|
const DEFAULT_CONFIG: ResolvedSyntheticConfig = {
|
|
@@ -56,6 +66,8 @@ const DEFAULT_CONFIG: ResolvedSyntheticConfig = {
|
|
|
56
66
|
quotaWarnings: false,
|
|
57
67
|
subBarIntegration: true,
|
|
58
68
|
proxiedModels: false,
|
|
69
|
+
proxyUrl: "",
|
|
70
|
+
proxyRequiresAuth: true,
|
|
59
71
|
};
|
|
60
72
|
|
|
61
73
|
export const pendingMessages: string[] = [];
|
|
@@ -165,7 +177,7 @@ export function registerSyntheticSettings(
|
|
|
165
177
|
commandDescription: "Configure Synthetic extension settings",
|
|
166
178
|
title: "Synthetic Settings",
|
|
167
179
|
configStore: configLoader,
|
|
168
|
-
buildSections: (tabConfig, resolved): SettingsSection[] => {
|
|
180
|
+
buildSections: (tabConfig, resolved, ctx): SettingsSection[] => {
|
|
169
181
|
const loaded = getLoadedFeatures();
|
|
170
182
|
const webSearch = tabConfig?.webSearch ?? resolved.webSearch;
|
|
171
183
|
const quotasCommand = tabConfig?.quotasCommand ?? resolved.quotasCommand;
|
|
@@ -174,10 +186,100 @@ export function registerSyntheticSettings(
|
|
|
174
186
|
const subBarIntegration =
|
|
175
187
|
tabConfig?.subBarIntegration ?? resolved.subBarIntegration;
|
|
176
188
|
const proxiedModels = tabConfig?.proxiedModels ?? resolved.proxiedModels;
|
|
189
|
+
const proxyUrl = tabConfig?.proxyUrl ?? resolved.proxyUrl;
|
|
190
|
+
const proxyRequiresAuth =
|
|
191
|
+
tabConfig?.proxyRequiresAuth ?? resolved.proxyRequiresAuth;
|
|
177
192
|
|
|
178
193
|
const sections: SettingsSection[] = [];
|
|
179
194
|
|
|
180
195
|
sections.push(
|
|
196
|
+
{
|
|
197
|
+
label: "Connection",
|
|
198
|
+
items: [
|
|
199
|
+
{
|
|
200
|
+
id: "utilityApiProxy",
|
|
201
|
+
label: "Utility API Proxy",
|
|
202
|
+
description:
|
|
203
|
+
"Override the Synthetic quotas and web search API root. The provider endpoint is not proxied.",
|
|
204
|
+
currentValue: formatSyntheticUtilityApiProxySummary({
|
|
205
|
+
proxyUrl,
|
|
206
|
+
proxyRequiresAuth,
|
|
207
|
+
}),
|
|
208
|
+
submenu: (_current, done) => {
|
|
209
|
+
const current: SyntheticConfig =
|
|
210
|
+
tabConfig ?? (ctx.scope === "memory" ? resolved : {});
|
|
211
|
+
let nextProxyUrl = proxyUrl;
|
|
212
|
+
let nextProxyRequiresAuth =
|
|
213
|
+
proxyUrl.trim() || proxyRequiresAuth
|
|
214
|
+
? proxyRequiresAuth
|
|
215
|
+
: true;
|
|
216
|
+
|
|
217
|
+
const syncDraft = () => {
|
|
218
|
+
ctx.setDraft({
|
|
219
|
+
...current,
|
|
220
|
+
proxyUrl: nextProxyUrl.trim() || undefined,
|
|
221
|
+
proxyRequiresAuth: nextProxyRequiresAuth,
|
|
222
|
+
});
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
return new SettingsDetailEditor({
|
|
226
|
+
title: "Utility API Proxy",
|
|
227
|
+
theme: ctx.theme,
|
|
228
|
+
fields: [
|
|
229
|
+
{
|
|
230
|
+
id: "proxyUrl.detail",
|
|
231
|
+
type: "text",
|
|
232
|
+
label: "Proxy URL",
|
|
233
|
+
description:
|
|
234
|
+
"Leave empty to call https://api.synthetic.new directly for quotas and web search.",
|
|
235
|
+
getValue: () => nextProxyUrl,
|
|
236
|
+
setValue: (value) => {
|
|
237
|
+
nextProxyUrl = value;
|
|
238
|
+
// Requires auth only makes sense with a proxy.
|
|
239
|
+
// Enforce enabled state when the URL is empty.
|
|
240
|
+
if (!value.trim()) nextProxyRequiresAuth = true;
|
|
241
|
+
syncDraft();
|
|
242
|
+
},
|
|
243
|
+
validate: validateSyntheticUtilityApiProxyUrl,
|
|
244
|
+
emptyValueText: "direct",
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
id: "proxyRequiresAuth.detail",
|
|
248
|
+
type: "boolean",
|
|
249
|
+
label: "Requires auth",
|
|
250
|
+
description:
|
|
251
|
+
"When disabled, quotas and web search skip the Synthetic API key check and omit Authorization. Only effective when a proxy URL is set.",
|
|
252
|
+
getValue: () =>
|
|
253
|
+
hasSyntheticUtilityApiProxy({
|
|
254
|
+
proxyUrl: nextProxyUrl,
|
|
255
|
+
proxyRequiresAuth: nextProxyRequiresAuth,
|
|
256
|
+
})
|
|
257
|
+
? nextProxyRequiresAuth
|
|
258
|
+
: true,
|
|
259
|
+
setValue: (value) => {
|
|
260
|
+
nextProxyRequiresAuth = hasSyntheticUtilityApiProxy({
|
|
261
|
+
proxyUrl: nextProxyUrl,
|
|
262
|
+
proxyRequiresAuth: nextProxyRequiresAuth,
|
|
263
|
+
})
|
|
264
|
+
? value
|
|
265
|
+
: true;
|
|
266
|
+
syncDraft();
|
|
267
|
+
},
|
|
268
|
+
trueLabel: "enabled",
|
|
269
|
+
falseLabel: "disabled",
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
onDone: done,
|
|
273
|
+
getDoneSummary: () =>
|
|
274
|
+
formatSyntheticUtilityApiProxySummary({
|
|
275
|
+
proxyUrl: nextProxyUrl,
|
|
276
|
+
proxyRequiresAuth: nextProxyRequiresAuth,
|
|
277
|
+
}),
|
|
278
|
+
});
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
},
|
|
181
283
|
{
|
|
182
284
|
label: "Models",
|
|
183
285
|
items: [
|
|
@@ -1,17 +1,38 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { configLoader } from "../../config";
|
|
3
3
|
import { getSyntheticApiKey } from "../../lib/env";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
resolveSyntheticUtilityApiAuth,
|
|
6
|
+
type SyntheticUtilityApiConfig,
|
|
7
|
+
} from "../../lib/utility-api";
|
|
8
|
+
import { type FetchQuotasOptions, fetchQuotas } from "../../utils/quotas";
|
|
5
9
|
import { QuotasComponent } from "./components/quotas-display";
|
|
6
10
|
|
|
7
11
|
const MISSING_AUTH_MESSAGE =
|
|
8
|
-
"Synthetic quotas requires a Synthetic subscription. Add credentials to ~/.pi/agent/auth.json
|
|
12
|
+
"Synthetic quotas requires a Synthetic subscription or an unauthenticated proxy. Add credentials to ~/.pi/agent/auth.json, set SYNTHETIC_API_KEY, or disable proxy auth in /synthetic:settings.";
|
|
13
|
+
|
|
14
|
+
async function buildQuotasOptions(
|
|
15
|
+
config: SyntheticUtilityApiConfig,
|
|
16
|
+
authStorage: NonNullable<Parameters<typeof getSyntheticApiKey>[0]>,
|
|
17
|
+
): Promise<FetchQuotasOptions | undefined> {
|
|
18
|
+
const auth = await resolveSyntheticUtilityApiAuth(config, () =>
|
|
19
|
+
getSyntheticApiKey(authStorage),
|
|
20
|
+
);
|
|
21
|
+
if (!auth) return undefined;
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
apiKey: auth.apiKey,
|
|
25
|
+
proxyUrl: config.proxyUrl,
|
|
26
|
+
requiresAuth: auth.requiresAuth,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
9
29
|
|
|
10
30
|
export function registerQuotasCommand(pi: ExtensionAPI): void {
|
|
11
31
|
pi.registerCommand("synthetic:quotas", {
|
|
12
32
|
description: "Display Synthetic API usage quotas",
|
|
13
33
|
handler: async (_args, ctx) => {
|
|
14
|
-
|
|
34
|
+
const config = configLoader.getConfig();
|
|
35
|
+
if (!config.quotasCommand) {
|
|
15
36
|
ctx.ui.notify(
|
|
16
37
|
"Synthetic quotas command is disabled. Restart Pi to unload the command after re-enabling or disabling it.",
|
|
17
38
|
"warning",
|
|
@@ -19,12 +40,14 @@ export function registerQuotasCommand(pi: ExtensionAPI): void {
|
|
|
19
40
|
return;
|
|
20
41
|
}
|
|
21
42
|
|
|
22
|
-
const
|
|
23
|
-
|
|
43
|
+
const quotasOptions = await buildQuotasOptions(
|
|
44
|
+
config,
|
|
45
|
+
ctx.modelRegistry.authStorage,
|
|
46
|
+
);
|
|
47
|
+
if (!quotasOptions) {
|
|
24
48
|
ctx.ui.notify(MISSING_AUTH_MESSAGE, "warning");
|
|
25
49
|
return;
|
|
26
50
|
}
|
|
27
|
-
const key: string = apiKey;
|
|
28
51
|
|
|
29
52
|
const result = await ctx.ui.custom<null>((tui, theme, _kb, done) => {
|
|
30
53
|
const controller = new AbortController();
|
|
@@ -43,7 +66,10 @@ export function registerQuotasCommand(pi: ExtensionAPI): void {
|
|
|
43
66
|
);
|
|
44
67
|
|
|
45
68
|
async function loadQuotas(): Promise<void> {
|
|
46
|
-
const fetchResult = await fetchQuotas(
|
|
69
|
+
const fetchResult = await fetchQuotas({
|
|
70
|
+
...quotasOptions,
|
|
71
|
+
signal: controller.signal,
|
|
72
|
+
});
|
|
47
73
|
if (controller.signal.aborted) return;
|
|
48
74
|
if (fetchResult.success) {
|
|
49
75
|
component.setState({
|
|
@@ -74,7 +100,7 @@ export function registerQuotasCommand(pi: ExtensionAPI): void {
|
|
|
74
100
|
|
|
75
101
|
// Non-interactive fallback (RPC, print, JSON modes)
|
|
76
102
|
if (result === undefined) {
|
|
77
|
-
const fetchResult = await fetchQuotas(
|
|
103
|
+
const fetchResult = await fetchQuotas(quotasOptions);
|
|
78
104
|
if (!fetchResult.success) {
|
|
79
105
|
ctx.ui.notify(
|
|
80
106
|
JSON.stringify({ error: fetchResult.error.message }),
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
seedSyntheticConfigIfMissing,
|
|
17
17
|
} from "../../config";
|
|
18
18
|
import { getSyntheticApiKey } from "../../lib/env";
|
|
19
|
+
import { resolveSyntheticUtilityApiAuth } from "../../lib/utility-api";
|
|
19
20
|
import { QuotaStore } from "../../services/quota-store";
|
|
20
21
|
import {
|
|
21
22
|
parseQuotaHeader,
|
|
@@ -94,13 +95,29 @@ export default async function (pi: ExtensionAPI) {
|
|
|
94
95
|
await configLoader.load();
|
|
95
96
|
await seedSyntheticConfigIfMissing();
|
|
96
97
|
|
|
97
|
-
const
|
|
98
|
+
const initialConfig = configLoader.getConfig();
|
|
99
|
+
const includeProxiedModels = initialConfig.proxiedModels;
|
|
100
|
+
let utilityApiProxyUrl = initialConfig.proxyUrl;
|
|
101
|
+
let utilityApiProxyRequiresAuth = initialConfig.proxyRequiresAuth;
|
|
102
|
+
const quotaStore = new QuotaStore();
|
|
103
|
+
let currentAuthStorage: AuthStorage | undefined;
|
|
104
|
+
|
|
98
105
|
registerSyntheticProvider(pi, { includeProxiedModels });
|
|
99
106
|
|
|
100
107
|
pi.events.on(SYNTHETIC_CONFIG_UPDATED_EVENT, (data: unknown) => {
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
108
|
+
const config = (data as SyntheticConfigUpdatedPayload).config;
|
|
109
|
+
registerSyntheticProvider(pi, {
|
|
110
|
+
includeProxiedModels: config.proxiedModels,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (
|
|
114
|
+
config.proxyUrl !== utilityApiProxyUrl ||
|
|
115
|
+
config.proxyRequiresAuth !== utilityApiProxyRequiresAuth
|
|
116
|
+
) {
|
|
117
|
+
quotaStore.clear();
|
|
118
|
+
utilityApiProxyUrl = config.proxyUrl;
|
|
119
|
+
utilityApiProxyRequiresAuth = config.proxyRequiresAuth;
|
|
120
|
+
}
|
|
104
121
|
});
|
|
105
122
|
|
|
106
123
|
const loadedFeatures = new Set<SyntheticFeatureId>();
|
|
@@ -114,14 +131,20 @@ export default async function (pi: ExtensionAPI) {
|
|
|
114
131
|
getLoadedFeatures: () => loadedFeatures,
|
|
115
132
|
});
|
|
116
133
|
|
|
117
|
-
const quotaStore = new QuotaStore();
|
|
118
|
-
let currentAuthStorage: AuthStorage | undefined;
|
|
119
|
-
|
|
120
134
|
async function fetchQuotasFromAuth(): Promise<QuotasResponse | undefined> {
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
135
|
+
const config = configLoader.getConfig();
|
|
136
|
+
const auth = await resolveSyntheticUtilityApiAuth(config, () =>
|
|
137
|
+
currentAuthStorage
|
|
138
|
+
? getSyntheticApiKey(currentAuthStorage)
|
|
139
|
+
: Promise.resolve(undefined),
|
|
140
|
+
);
|
|
141
|
+
if (!auth) return undefined;
|
|
142
|
+
|
|
143
|
+
const result = await fetchQuotas({
|
|
144
|
+
apiKey: auth.apiKey,
|
|
145
|
+
proxyUrl: config.proxyUrl,
|
|
146
|
+
requiresAuth: auth.requiresAuth,
|
|
147
|
+
});
|
|
125
148
|
return result.success ? result.data.quotas : undefined;
|
|
126
149
|
}
|
|
127
150
|
|
|
@@ -194,9 +217,5 @@ export default async function (pi: ExtensionAPI) {
|
|
|
194
217
|
currentAuthStorage = ctx.modelRegistry.authStorage;
|
|
195
218
|
pi.events.emit(SYNTHETIC_EXTENSIONS_REQUEST_EVENT, undefined);
|
|
196
219
|
emitSyntheticConfigUpdated(pi);
|
|
197
|
-
|
|
198
|
-
if (ctx.model?.provider === "synthetic") {
|
|
199
|
-
await quotaStore.refreshFromApi(fetchQuotasFromAuth);
|
|
200
|
-
}
|
|
201
220
|
});
|
|
202
221
|
}
|
|
@@ -14,6 +14,11 @@ import { Container, Text } from "@earendil-works/pi-tui";
|
|
|
14
14
|
import { type Static, Type } from "typebox";
|
|
15
15
|
import { configLoader } from "../../config";
|
|
16
16
|
import { getSyntheticApiKey } from "../../lib/env";
|
|
17
|
+
import {
|
|
18
|
+
resolveSyntheticUtilityApiAuth,
|
|
19
|
+
resolveSyntheticUtilityApiBaseUrl,
|
|
20
|
+
syntheticUtilityApiUrl,
|
|
21
|
+
} from "../../lib/utility-api";
|
|
17
22
|
|
|
18
23
|
export const SYNTHETIC_WEB_SEARCH_TOOL = "synthetic_web_search" as const;
|
|
19
24
|
|
|
@@ -69,28 +74,46 @@ export const syntheticWebSearchTool = defineTool({
|
|
|
69
74
|
details: { query: params.query },
|
|
70
75
|
});
|
|
71
76
|
|
|
72
|
-
|
|
77
|
+
const config = configLoader.getConfig();
|
|
78
|
+
if (!config.webSearch) {
|
|
73
79
|
throw new Error(
|
|
74
80
|
"Synthetic web search is disabled. Re-enable it with synthetic:settings or pi config.",
|
|
75
81
|
);
|
|
76
82
|
}
|
|
77
83
|
|
|
78
|
-
const
|
|
79
|
-
|
|
84
|
+
const auth = await resolveSyntheticUtilityApiAuth(config, () =>
|
|
85
|
+
getSyntheticApiKey(ctx.modelRegistry.authStorage),
|
|
86
|
+
);
|
|
87
|
+
if (!auth) {
|
|
80
88
|
throw new Error(
|
|
81
|
-
"Synthetic web search requires a Synthetic subscription. Add credentials to ~/.pi/agent/auth.json
|
|
89
|
+
"Synthetic web search requires a Synthetic subscription or an unauthenticated proxy. Add credentials to ~/.pi/agent/auth.json, set SYNTHETIC_API_KEY, or disable proxy auth in /synthetic:settings.",
|
|
82
90
|
);
|
|
83
91
|
}
|
|
84
92
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
93
|
+
let baseUrl: string;
|
|
94
|
+
try {
|
|
95
|
+
baseUrl = resolveSyntheticUtilityApiBaseUrl(config.proxyUrl);
|
|
96
|
+
} catch (err: unknown) {
|
|
97
|
+
const message = err instanceof Error ? err.message : "Invalid proxy URL";
|
|
98
|
+
throw new Error(`Synthetic web search: ${message}`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const headers: Record<string, string> = {
|
|
102
|
+
"Content-Type": "application/json",
|
|
103
|
+
};
|
|
104
|
+
if (auth.apiKey) {
|
|
105
|
+
headers.Authorization = `Bearer ${auth.apiKey}`;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const response = await fetch(
|
|
109
|
+
syntheticUtilityApiUrl(baseUrl, "/v2/search"),
|
|
110
|
+
{
|
|
111
|
+
method: "POST",
|
|
112
|
+
headers,
|
|
113
|
+
body: JSON.stringify({ query: params.query }),
|
|
114
|
+
signal,
|
|
90
115
|
},
|
|
91
|
-
|
|
92
|
-
signal,
|
|
93
|
-
});
|
|
116
|
+
);
|
|
94
117
|
|
|
95
118
|
if (!response.ok) {
|
|
96
119
|
const errorText = await response.text();
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export const DEFAULT_SYNTHETIC_API_BASE_URL = "https://api.synthetic.new";
|
|
2
|
+
|
|
3
|
+
export interface SyntheticUtilityApiConfig {
|
|
4
|
+
proxyUrl?: string;
|
|
5
|
+
proxyRequiresAuth?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function hasSyntheticUtilityApiProxy(
|
|
9
|
+
config: SyntheticUtilityApiConfig,
|
|
10
|
+
): boolean {
|
|
11
|
+
return !!config.proxyUrl?.trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function syntheticUtilityApiRequiresAuth(
|
|
15
|
+
config: SyntheticUtilityApiConfig,
|
|
16
|
+
): boolean {
|
|
17
|
+
return (
|
|
18
|
+
!hasSyntheticUtilityApiProxy(config) || config.proxyRequiresAuth !== false
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function validateSyntheticUtilityApiProxyUrl(
|
|
23
|
+
value: string,
|
|
24
|
+
): string | null {
|
|
25
|
+
const trimmed = value.trim();
|
|
26
|
+
if (!trimmed) return null;
|
|
27
|
+
|
|
28
|
+
let url: URL;
|
|
29
|
+
try {
|
|
30
|
+
url = new URL(trimmed);
|
|
31
|
+
} catch {
|
|
32
|
+
return "Proxy URL must be a valid URL";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
36
|
+
return "Proxy URL must use http or https";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function resolveSyntheticUtilityApiBaseUrl(proxyUrl?: string): string {
|
|
43
|
+
const raw = proxyUrl?.trim() || DEFAULT_SYNTHETIC_API_BASE_URL;
|
|
44
|
+
const error = validateSyntheticUtilityApiProxyUrl(raw);
|
|
45
|
+
if (error) {
|
|
46
|
+
throw new Error(`Synthetic utility API: ${error}`);
|
|
47
|
+
}
|
|
48
|
+
return raw.replace(/\/+$/, "");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function syntheticUtilityApiUrl(baseUrl: string, path: string): string {
|
|
52
|
+
return `${baseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function formatSyntheticUtilityApiProxySummary(
|
|
56
|
+
config: SyntheticUtilityApiConfig,
|
|
57
|
+
): string {
|
|
58
|
+
if (!hasSyntheticUtilityApiProxy(config)) return "direct";
|
|
59
|
+
const auth = syntheticUtilityApiRequiresAuth(config) ? "auth" : "no auth";
|
|
60
|
+
return `${config.proxyUrl?.trim()} · ${auth}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface ResolvedSyntheticUtilityApiAuth {
|
|
64
|
+
apiKey: string | undefined;
|
|
65
|
+
requiresAuth: boolean;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resolve the auth state for a utility API request.
|
|
70
|
+
*
|
|
71
|
+
* - When auth is required (direct calls, or proxy with auth enabled), the
|
|
72
|
+
* provided `getApiKey` callback is awaited. Returns `null` if no key is
|
|
73
|
+
* available, so callers can surface a missing-credentials error.
|
|
74
|
+
* - When auth is not required (unauthenticated proxy), `apiKey` is left
|
|
75
|
+
* `undefined` and the Synthetic API key check is skipped.
|
|
76
|
+
*/
|
|
77
|
+
export async function resolveSyntheticUtilityApiAuth(
|
|
78
|
+
config: SyntheticUtilityApiConfig,
|
|
79
|
+
getApiKey: () => Promise<string | undefined>,
|
|
80
|
+
): Promise<ResolvedSyntheticUtilityApiAuth | null> {
|
|
81
|
+
const requiresAuth = syntheticUtilityApiRequiresAuth(config);
|
|
82
|
+
const apiKey = requiresAuth ? await getApiKey() : undefined;
|
|
83
|
+
if (requiresAuth && !apiKey) return null;
|
|
84
|
+
return { apiKey, requiresAuth };
|
|
85
|
+
}
|
package/src/utils/quotas.ts
CHANGED
|
@@ -1,7 +1,18 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveSyntheticUtilityApiBaseUrl,
|
|
3
|
+
syntheticUtilityApiUrl,
|
|
4
|
+
} from "../lib/utility-api";
|
|
1
5
|
import type { QuotasResponse, QuotasResult } from "../types/quotas";
|
|
2
6
|
|
|
3
7
|
const FETCH_TIMEOUT_MS = 15_000;
|
|
4
8
|
|
|
9
|
+
export interface FetchQuotasOptions {
|
|
10
|
+
apiKey?: string;
|
|
11
|
+
proxyUrl?: string;
|
|
12
|
+
requiresAuth?: boolean;
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
}
|
|
15
|
+
|
|
5
16
|
function isTimeoutReason(reason: unknown): boolean {
|
|
6
17
|
return (
|
|
7
18
|
(reason instanceof DOMException && reason.name === "TimeoutError") ||
|
|
@@ -10,23 +21,37 @@ function isTimeoutReason(reason: unknown): boolean {
|
|
|
10
21
|
}
|
|
11
22
|
|
|
12
23
|
export async function fetchQuotas(
|
|
13
|
-
|
|
14
|
-
signal?: AbortSignal,
|
|
24
|
+
options: FetchQuotasOptions,
|
|
15
25
|
): Promise<QuotasResult> {
|
|
16
|
-
|
|
26
|
+
const requiresAuth = options.requiresAuth ?? true;
|
|
27
|
+
if (requiresAuth && !options.apiKey) {
|
|
17
28
|
return {
|
|
18
29
|
success: false,
|
|
19
30
|
error: { message: "No API key provided", kind: "config" },
|
|
20
31
|
};
|
|
21
32
|
}
|
|
22
33
|
|
|
34
|
+
let url: string;
|
|
35
|
+
try {
|
|
36
|
+
const baseUrl = resolveSyntheticUtilityApiBaseUrl(options.proxyUrl);
|
|
37
|
+
url = syntheticUtilityApiUrl(baseUrl, "/v2/quotas");
|
|
38
|
+
} catch (err: unknown) {
|
|
39
|
+
const message = err instanceof Error ? err.message : "Invalid proxy URL";
|
|
40
|
+
return { success: false, error: { message, kind: "config" } };
|
|
41
|
+
}
|
|
42
|
+
|
|
23
43
|
const signals: AbortSignal[] = [AbortSignal.timeout(FETCH_TIMEOUT_MS)];
|
|
24
|
-
if (signal) signals.push(signal);
|
|
44
|
+
if (options.signal) signals.push(options.signal);
|
|
25
45
|
const combined = AbortSignal.any(signals);
|
|
26
46
|
|
|
27
47
|
try {
|
|
28
|
-
const
|
|
29
|
-
|
|
48
|
+
const headers: Record<string, string> = {};
|
|
49
|
+
if (options.apiKey) {
|
|
50
|
+
headers.Authorization = `Bearer ${options.apiKey}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const response = await fetch(url, {
|
|
54
|
+
headers,
|
|
30
55
|
signal: combined,
|
|
31
56
|
});
|
|
32
57
|
|