@khanglvm/llm-router 2.3.7 → 2.4.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/CHANGELOG.md +12 -0
- package/README.md +12 -0
- package/package.json +1 -1
- package/src/node/instance-state.js +6 -2
- package/src/node/port-reclaim.js +4 -1
- package/src/node/web-console-server.js +175 -3
- package/src/node/web-console-ui/web-search-utils.js +42 -0
- package/src/runtime/config.js +53 -0
- package/src/runtime/handler/amp-web-search.js +75 -12
- package/src/runtime/handler/provider-call.js +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [2.4.0] - 2026-04-18
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Claude Code can now store a router-managed `claudeCode.webSearchProvider` selection, with validation plus Web UI controls for choosing either built-in web search providers or hosted search routes.
|
|
14
|
+
- Native Claude web search and page-fetch tool calls can now be intercepted locally for non-AMP clients, so Claude-compatible traffic can use the router-managed shared web search stack instead of falling back to upstream-native tools.
|
|
15
|
+
- `yarn dev` now exposes a one-click "Sync production config" action that clones the current production config into the dedicated dev config while preserving the dev router's local server settings.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- `yarn dev` now launches the detached router backend with the runtime start path, so the dedicated dev router port is honored instead of conflicting with the fixed production port.
|
|
19
|
+
- Dev-mode router reclaim now leaves startup-managed production instances alone when reclaiming non-fixed dev ports.
|
|
20
|
+
- The dev terminal and web console now clearly mark when you are operating in the isolated development sandbox.
|
|
21
|
+
|
|
10
22
|
## [2.3.7] - 2026-04-18
|
|
11
23
|
|
|
12
24
|
### Fixed
|
package/README.md
CHANGED
|
@@ -29,6 +29,8 @@ llr ai-help # agent-oriented setup brief
|
|
|
29
29
|
- **Model aliases with routing** — group models into stable alias names with weighted round-robin, quota-aware balancing, and automatic fallback
|
|
30
30
|
- **Rate limiting** — set request caps per model or across all models over configurable time windows
|
|
31
31
|
- **Coding tool routing** — one-click routing config for Codex CLI, Claude Code, Factory Droid, and AMP
|
|
32
|
+
- **Dev sandbox** — `yarn dev` runs the console against a dedicated dev config/router port, highlights dev mode in terminal + UI, and can clone the production config into the sandbox for quick iteration
|
|
33
|
+
- **Claude native web tools** — local handling for Claude web search and page fetch requests, with selectable Claude Code web-search providers from the shared Web Search config
|
|
32
34
|
- **Seamless local updates** — `llr update` keeps the fixed local router endpoint online, drains in-flight requests, and automatically retries through backend restart windows
|
|
33
35
|
- **Web search** — built-in web search for AMP and other router-managed tools
|
|
34
36
|
- **Deployable** — run locally or deploy to Cloudflare Workers
|
|
@@ -40,6 +42,14 @@ llr ai-help # agent-oriented setup brief
|
|
|
40
42
|
|
|
41
43
|
That means `llr update` can install a new package version and gracefully swap the backend without breaking active CLI or tool requests. Requests that arrive during the short backend handoff are deferred and retried automatically instead of failing immediately. The Web UI may reconnect during that window, but router-managed API traffic keeps the same public local endpoint.
|
|
42
44
|
|
|
45
|
+
## Development Sandbox
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
yarn dev
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Development mode uses the dedicated `~/.llm-router-dev.json` config and its own local router port so it can run alongside a startup-managed or manually started production router. The terminal and Web UI both show a dev-mode indicator, and the dev Web UI includes a one-click sync action to copy the current production config into the sandbox without changing the dev router binding.
|
|
52
|
+
|
|
43
53
|
## Web UI
|
|
44
54
|
|
|
45
55
|
### Alias & Fallback
|
|
@@ -66,6 +76,8 @@ Route Claude Code through the gateway with per-tier model bindings.
|
|
|
66
76
|
|
|
67
77
|

|
|
68
78
|
|
|
79
|
+
Claude Code can also select a shared Web Search provider or hosted search route from the router config. When Claude-compatible traffic uses native web-search or page-fetch tools, LLM Router can satisfy those calls through the selected shared web-search provider instead of relying on upstream-native web tooling.
|
|
80
|
+
|
|
69
81
|
### Factory Droid
|
|
70
82
|
|
|
71
83
|
Route Factory Droid through the gateway via a managed custom model entry with reasoning effort control.
|
package/package.json
CHANGED
|
@@ -239,6 +239,7 @@ export async function waitForRuntimeMatch(options = {}, deps = {}) {
|
|
|
239
239
|
|
|
240
240
|
export function spawnStartProcess({
|
|
241
241
|
cliPath,
|
|
242
|
+
startCommand = "start-runtime",
|
|
242
243
|
configPath,
|
|
243
244
|
host = FIXED_LOCAL_ROUTER_HOST,
|
|
244
245
|
port = FIXED_LOCAL_ROUTER_PORT,
|
|
@@ -256,7 +257,7 @@ export function spawnStartProcess({
|
|
|
256
257
|
|
|
257
258
|
const args = [
|
|
258
259
|
finalCliPath,
|
|
259
|
-
"start",
|
|
260
|
+
String(startCommand || "start-runtime").trim() || "start-runtime",
|
|
260
261
|
`--config=${configPath}`,
|
|
261
262
|
`--host=${host}`,
|
|
262
263
|
`--port=${port}`,
|
|
@@ -290,7 +291,10 @@ export async function startDetachedRouterService(options = {}, deps = {}) {
|
|
|
290
291
|
|
|
291
292
|
let child;
|
|
292
293
|
try {
|
|
293
|
-
child = spawnStartProcessFn(
|
|
294
|
+
child = spawnStartProcessFn({
|
|
295
|
+
...options,
|
|
296
|
+
startCommand: String(options?.startCommand || "start-runtime").trim() || "start-runtime"
|
|
297
|
+
}, {
|
|
294
298
|
detached: true,
|
|
295
299
|
stdio: ["ignore", "pipe", "pipe"],
|
|
296
300
|
unref: false,
|
package/src/node/port-reclaim.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { clearRuntimeState, getActiveRuntimeState } from "./instance-state.js";
|
|
3
|
+
import { FIXED_LOCAL_ROUTER_PORT } from "./local-server-settings.js";
|
|
3
4
|
import { startupStatus, stopStartup } from "./startup-manager.js";
|
|
4
5
|
|
|
5
6
|
export function parsePidList(text) {
|
|
@@ -119,7 +120,9 @@ export async function stopStartupManagedListener({ port, line, error }, deps = {
|
|
|
119
120
|
let shouldStopStartup = false;
|
|
120
121
|
if (activeRuntimeState?.managedByStartup) {
|
|
121
122
|
shouldStopStartup = Number(activeRuntimeState.port) === Number(port);
|
|
122
|
-
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!shouldStopStartup && Number(port) === Number(FIXED_LOCAL_ROUTER_PORT)) {
|
|
123
126
|
try {
|
|
124
127
|
const status = await startupStatusFn();
|
|
125
128
|
shouldStopStartup = Boolean(status?.running);
|
|
@@ -68,6 +68,7 @@ import {
|
|
|
68
68
|
OLLAMA_KEEP_ALIVE_PATTERN,
|
|
69
69
|
OLLAMA_PROVIDER_TYPE,
|
|
70
70
|
configHasProvider,
|
|
71
|
+
normalizeClaudeCodeWebSearchProvider,
|
|
71
72
|
normalizeOllamaConfig,
|
|
72
73
|
normalizeRuntimeConfig,
|
|
73
74
|
resolveProviderApiKey,
|
|
@@ -850,6 +851,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
850
851
|
host = "127.0.0.1",
|
|
851
852
|
port = 8788,
|
|
852
853
|
configPath = getDefaultConfigPath(),
|
|
854
|
+
productionConfigPath = getDefaultConfigPath(),
|
|
853
855
|
activityLogPath = "",
|
|
854
856
|
routerHost = FIXED_LOCAL_ROUTER_HOST,
|
|
855
857
|
routerPort = FIXED_LOCAL_ROUTER_PORT,
|
|
@@ -891,6 +893,8 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
891
893
|
? deps.loadWebConsoleDevAssets
|
|
892
894
|
: loadWebConsoleDevAssets;
|
|
893
895
|
const resolvedRouterCliPath = String(cliPathForRouter || process.env.LLM_ROUTER_CLI_PATH || process.argv[1] || "").trim();
|
|
896
|
+
const resolvedConfigPath = path.resolve(String(configPath || getDefaultConfigPath()).trim() || getDefaultConfigPath());
|
|
897
|
+
const resolvedProductionConfigPath = path.resolve(String(productionConfigPath || getDefaultConfigPath()).trim() || getDefaultConfigPath());
|
|
894
898
|
const resolvedActivityLogPath = resolveActivityLogPath(configPath, activityLogPath);
|
|
895
899
|
const startupControlsEnabled = !devMode;
|
|
896
900
|
const defaultRouterSettings = {
|
|
@@ -1326,6 +1330,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
1326
1330
|
async function readClaudeCodeGlobalRoutingState(settings = {}, config = null) {
|
|
1327
1331
|
const endpointUrl = buildAmpClientEndpointUrl(settings);
|
|
1328
1332
|
const apiKey = String(config?.masterKey || "").trim();
|
|
1333
|
+
const webSearchProvider = normalizeClaudeCodeWebSearchProvider(config?.claudeCode?.webSearchProvider);
|
|
1329
1334
|
try {
|
|
1330
1335
|
const state = await readClaudeCodeRoutingState({
|
|
1331
1336
|
endpointUrl,
|
|
@@ -1334,6 +1339,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
1334
1339
|
});
|
|
1335
1340
|
return {
|
|
1336
1341
|
...state,
|
|
1342
|
+
webSearchProvider,
|
|
1337
1343
|
endpointUrl,
|
|
1338
1344
|
error: ""
|
|
1339
1345
|
};
|
|
@@ -1354,6 +1360,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
1354
1360
|
subagentModel: "",
|
|
1355
1361
|
thinkingLevel: ""
|
|
1356
1362
|
},
|
|
1363
|
+
webSearchProvider,
|
|
1357
1364
|
endpointUrl,
|
|
1358
1365
|
error: error instanceof Error ? error.message : String(error)
|
|
1359
1366
|
};
|
|
@@ -2181,7 +2188,11 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
2181
2188
|
return savedConfig;
|
|
2182
2189
|
}
|
|
2183
2190
|
|
|
2184
|
-
async function writeAndBroadcastConfig(parsed, {
|
|
2191
|
+
async function writeAndBroadcastConfig(parsed, {
|
|
2192
|
+
source = "",
|
|
2193
|
+
preserveMissingKeys = true,
|
|
2194
|
+
successMessage = ""
|
|
2195
|
+
} = {}) {
|
|
2185
2196
|
const previousConfigState = await readConfigState(configPath);
|
|
2186
2197
|
const previousConfig = previousConfigState.normalizedConfig || buildDefaultConfigObject();
|
|
2187
2198
|
const previousLocalServer = getConfigLocalServer(previousConfigState);
|
|
@@ -2190,7 +2201,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
2190
2201
|
// This prevents partial writes (e.g. from scoped Ollama endpoints) from wiping
|
|
2191
2202
|
// unrelated config sections like masterKey, modelAliases, amp, metadata, etc.
|
|
2192
2203
|
const previousRaw = previousConfigState.rawConfig;
|
|
2193
|
-
if (previousRaw && typeof previousRaw === "object" && parsed && typeof parsed === "object") {
|
|
2204
|
+
if (preserveMissingKeys && previousRaw && typeof previousRaw === "object" && parsed && typeof parsed === "object") {
|
|
2194
2205
|
for (const key of Object.keys(previousRaw)) {
|
|
2195
2206
|
if (!(key in parsed)) {
|
|
2196
2207
|
parsed[key] = previousRaw[key];
|
|
@@ -2204,7 +2215,7 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
2204
2215
|
const nextLocalServer = readLocalServerSettings(savedConfig, previousLocalServer);
|
|
2205
2216
|
|
|
2206
2217
|
if (source !== "autosave") {
|
|
2207
|
-
addLog("success", `Config saved to ${path.basename(configPath)}.`);
|
|
2218
|
+
addLog("success", successMessage || `Config saved to ${path.basename(configPath)}.`);
|
|
2208
2219
|
}
|
|
2209
2220
|
|
|
2210
2221
|
const managedRuntime = await readManagedRuntime(previousLocalServer);
|
|
@@ -2269,6 +2280,54 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
2269
2280
|
};
|
|
2270
2281
|
}
|
|
2271
2282
|
|
|
2283
|
+
async function syncConfigFromProduction() {
|
|
2284
|
+
if (!devMode) {
|
|
2285
|
+
const error = new Error("Production config sync is only available in dev mode.");
|
|
2286
|
+
error.statusCode = 409;
|
|
2287
|
+
throw error;
|
|
2288
|
+
}
|
|
2289
|
+
|
|
2290
|
+
if (resolvedConfigPath === resolvedProductionConfigPath) {
|
|
2291
|
+
const error = new Error("Current config already points to the production config file.");
|
|
2292
|
+
error.statusCode = 409;
|
|
2293
|
+
throw error;
|
|
2294
|
+
}
|
|
2295
|
+
|
|
2296
|
+
const currentConfigState = await readConfigState(configPath);
|
|
2297
|
+
const currentLocalServer = getConfigLocalServer(currentConfigState);
|
|
2298
|
+
const productionConfigState = await readConfigState(resolvedProductionConfigPath);
|
|
2299
|
+
|
|
2300
|
+
if (!productionConfigState.summary?.exists) {
|
|
2301
|
+
const error = new Error(`Production config file was not found at ${resolvedProductionConfigPath}.`);
|
|
2302
|
+
error.statusCode = 404;
|
|
2303
|
+
throw error;
|
|
2304
|
+
}
|
|
2305
|
+
|
|
2306
|
+
if (productionConfigState.parseError) {
|
|
2307
|
+
const error = new Error(`Production config JSON must parse before syncing: ${productionConfigState.parseError}`);
|
|
2308
|
+
error.statusCode = 400;
|
|
2309
|
+
throw error;
|
|
2310
|
+
}
|
|
2311
|
+
|
|
2312
|
+
const clonedConfig = applyLocalServerSettings(
|
|
2313
|
+
productionConfigState.normalizedConfig || buildDefaultConfigObject(),
|
|
2314
|
+
currentLocalServer
|
|
2315
|
+
);
|
|
2316
|
+
|
|
2317
|
+
const result = await writeAndBroadcastConfig(clonedConfig, {
|
|
2318
|
+
source: "production-sync",
|
|
2319
|
+
preserveMissingKeys: false,
|
|
2320
|
+
successMessage: `Synced ${path.basename(resolvedConfigPath)} from ${path.basename(resolvedProductionConfigPath)}.`
|
|
2321
|
+
});
|
|
2322
|
+
|
|
2323
|
+
return {
|
|
2324
|
+
...result,
|
|
2325
|
+
message: `Synced dev config from ${resolvedProductionConfigPath}.`,
|
|
2326
|
+
syncedFrom: resolvedProductionConfigPath,
|
|
2327
|
+
syncedTo: resolvedConfigPath
|
|
2328
|
+
};
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2272
2331
|
let routerRestartPromise = null;
|
|
2273
2332
|
|
|
2274
2333
|
async function restartManagedRouterWithSettings(settings, {
|
|
@@ -2334,6 +2393,12 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
2334
2393
|
: { ok: false };
|
|
2335
2394
|
|
|
2336
2395
|
return {
|
|
2396
|
+
environment: {
|
|
2397
|
+
devMode,
|
|
2398
|
+
configPath: resolvedConfigPath,
|
|
2399
|
+
productionConfigPath: resolvedProductionConfigPath,
|
|
2400
|
+
canSyncProductionConfig: devMode && resolvedConfigPath !== resolvedProductionConfigPath
|
|
2401
|
+
},
|
|
2337
2402
|
web: {
|
|
2338
2403
|
host,
|
|
2339
2404
|
port: actualWebPort,
|
|
@@ -2827,6 +2892,17 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
2827
2892
|
return;
|
|
2828
2893
|
}
|
|
2829
2894
|
|
|
2895
|
+
if (method === "POST" && requestUrl.pathname === "/api/config/sync-production") {
|
|
2896
|
+
const synced = await syncConfigFromProduction();
|
|
2897
|
+
sendJson(res, 200, {
|
|
2898
|
+
...synced.snapshot,
|
|
2899
|
+
message: synced.message,
|
|
2900
|
+
syncedFrom: synced.syncedFrom,
|
|
2901
|
+
syncedTo: synced.syncedTo
|
|
2902
|
+
});
|
|
2903
|
+
return;
|
|
2904
|
+
}
|
|
2905
|
+
|
|
2830
2906
|
if (method === "POST" && requestUrl.pathname === "/api/config/test-provider") {
|
|
2831
2907
|
const body = await readJsonBody(req);
|
|
2832
2908
|
const endpoints = Array.isArray(body?.endpoints) ? body.endpoints.map((entry) => String(entry || "").trim()).filter(Boolean) : [];
|
|
@@ -3454,6 +3530,102 @@ export async function startWebConsoleServer(options = {}, deps = {}) {
|
|
|
3454
3530
|
return;
|
|
3455
3531
|
}
|
|
3456
3532
|
|
|
3533
|
+
if (method === "POST" && requestUrl.pathname === "/api/claude-code/search-provider") {
|
|
3534
|
+
const body = await readJsonBody(req);
|
|
3535
|
+
const currentConfigState = await readConfigState(configPath);
|
|
3536
|
+
|
|
3537
|
+
let parsed;
|
|
3538
|
+
try {
|
|
3539
|
+
if (body?.config && typeof body.config === "object" && !Array.isArray(body.config)) {
|
|
3540
|
+
parsed = body.config;
|
|
3541
|
+
} else if (typeof body?.rawText === "string") {
|
|
3542
|
+
const rawText = String(body.rawText || "");
|
|
3543
|
+
parsed = rawText.trim() ? JSON.parse(rawText) : {};
|
|
3544
|
+
} else {
|
|
3545
|
+
parsed = currentConfigState.rawConfig && typeof currentConfigState.rawConfig === "object" && !Array.isArray(currentConfigState.rawConfig)
|
|
3546
|
+
? { ...currentConfigState.rawConfig }
|
|
3547
|
+
: {};
|
|
3548
|
+
}
|
|
3549
|
+
} catch (error) {
|
|
3550
|
+
sendJson(res, 400, {
|
|
3551
|
+
error: `Config JSON parse failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3552
|
+
});
|
|
3553
|
+
return;
|
|
3554
|
+
}
|
|
3555
|
+
|
|
3556
|
+
const nextConfig = parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
3557
|
+
const selectedProvider = normalizeClaudeCodeWebSearchProvider(
|
|
3558
|
+
body?.webSearchProvider
|
|
3559
|
+
?? body?.searchProvider
|
|
3560
|
+
?? body?.providerId
|
|
3561
|
+
);
|
|
3562
|
+
|
|
3563
|
+
if (selectedProvider) {
|
|
3564
|
+
let normalizedConfig;
|
|
3565
|
+
try {
|
|
3566
|
+
normalizedConfig = normalizeRuntimeConfig(nextConfig, { migrateToVersion: CONFIG_VERSION });
|
|
3567
|
+
} catch (error) {
|
|
3568
|
+
sendJson(res, 400, {
|
|
3569
|
+
error: `Config normalization failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3570
|
+
});
|
|
3571
|
+
return;
|
|
3572
|
+
}
|
|
3573
|
+
const configuredProviders = Array.isArray(normalizedConfig?.webSearch?.providers) ? normalizedConfig.webSearch.providers : [];
|
|
3574
|
+
const selectedExists = configuredProviders.some((provider) => normalizeClaudeCodeWebSearchProvider(provider?.id) === selectedProvider);
|
|
3575
|
+
if (!selectedExists) {
|
|
3576
|
+
sendJson(res, 400, {
|
|
3577
|
+
error: `Claude Code web search provider '${selectedProvider}' must reference a configured webSearch provider.`
|
|
3578
|
+
});
|
|
3579
|
+
return;
|
|
3580
|
+
}
|
|
3581
|
+
}
|
|
3582
|
+
|
|
3583
|
+
if (selectedProvider) {
|
|
3584
|
+
const currentClaudeCode = nextConfig?.claudeCode && typeof nextConfig.claudeCode === "object" && !Array.isArray(nextConfig.claudeCode)
|
|
3585
|
+
? nextConfig.claudeCode
|
|
3586
|
+
: (nextConfig?.["claude-code"] && typeof nextConfig["claude-code"] === "object" && !Array.isArray(nextConfig["claude-code"])
|
|
3587
|
+
? nextConfig["claude-code"]
|
|
3588
|
+
: null);
|
|
3589
|
+
const nextClaudeCode = currentClaudeCode
|
|
3590
|
+
? { ...currentClaudeCode }
|
|
3591
|
+
: {};
|
|
3592
|
+
nextClaudeCode.webSearchProvider = selectedProvider;
|
|
3593
|
+
nextConfig.claudeCode = nextClaudeCode;
|
|
3594
|
+
delete nextConfig["claude-code"];
|
|
3595
|
+
} else {
|
|
3596
|
+
const currentClaudeCode = nextConfig?.claudeCode && typeof nextConfig.claudeCode === "object" && !Array.isArray(nextConfig.claudeCode)
|
|
3597
|
+
? nextConfig.claudeCode
|
|
3598
|
+
: (nextConfig?.["claude-code"] && typeof nextConfig["claude-code"] === "object" && !Array.isArray(nextConfig["claude-code"])
|
|
3599
|
+
? nextConfig["claude-code"]
|
|
3600
|
+
: null);
|
|
3601
|
+
if (currentClaudeCode) {
|
|
3602
|
+
const nextClaudeCode = { ...currentClaudeCode };
|
|
3603
|
+
delete nextClaudeCode.webSearchProvider;
|
|
3604
|
+
delete nextClaudeCode["web-search-provider"];
|
|
3605
|
+
delete nextClaudeCode.searchProvider;
|
|
3606
|
+
delete nextClaudeCode["search-provider"];
|
|
3607
|
+
if (Object.keys(nextClaudeCode).length > 0) {
|
|
3608
|
+
nextConfig.claudeCode = nextClaudeCode;
|
|
3609
|
+
} else {
|
|
3610
|
+
nextConfig.claudeCode = {};
|
|
3611
|
+
}
|
|
3612
|
+
delete nextConfig["claude-code"];
|
|
3613
|
+
}
|
|
3614
|
+
}
|
|
3615
|
+
|
|
3616
|
+
const { snapshot } = await writeAndBroadcastConfig(nextConfig, { source: "claude-code-search-provider" });
|
|
3617
|
+
addLog(
|
|
3618
|
+
"success",
|
|
3619
|
+
selectedProvider ? "Claude Code search capability updated." : "Claude Code search capability cleared.",
|
|
3620
|
+
selectedProvider || "Default router search order"
|
|
3621
|
+
);
|
|
3622
|
+
sendJson(res, 200, {
|
|
3623
|
+
...snapshot,
|
|
3624
|
+
message: selectedProvider ? "Claude Code search capability updated." : "Claude Code search capability cleared."
|
|
3625
|
+
});
|
|
3626
|
+
return;
|
|
3627
|
+
}
|
|
3628
|
+
|
|
3457
3629
|
if (method === "POST" && requestUrl.pathname === "/api/claude-code/effort-level") {
|
|
3458
3630
|
const body = await readJsonBody(req);
|
|
3459
3631
|
const effortLevel = String(body?.effortLevel || body?.thinkingLevel || "").trim();
|
|
@@ -306,6 +306,48 @@ export function buildWebSearchProviderRows(config = {}, snapshot = null) {
|
|
|
306
306
|
});
|
|
307
307
|
}
|
|
308
308
|
|
|
309
|
+
export function buildClaudeCodeWebSearchProviderOptions(providerRows = [], selectedProviderId = "") {
|
|
310
|
+
const options = [];
|
|
311
|
+
const seen = new Set();
|
|
312
|
+
|
|
313
|
+
for (const row of Array.isArray(providerRows) ? providerRows : []) {
|
|
314
|
+
if (!row?.configured) continue;
|
|
315
|
+
const optionValue = String(row?.id || "").trim();
|
|
316
|
+
const optionKey = normalizeWebSearchProviderKey(optionValue);
|
|
317
|
+
if (!optionValue || !optionKey || seen.has(optionKey)) continue;
|
|
318
|
+
|
|
319
|
+
const label = row.kind === "hosted"
|
|
320
|
+
? [String(row?.label || "").trim(), String(row?.modelId || "").trim()].filter(Boolean).join(" · ")
|
|
321
|
+
: (String(row?.label || optionValue).trim() || optionValue);
|
|
322
|
+
const hint = row.kind === "hosted"
|
|
323
|
+
? (String(row?.routeId || optionValue).trim() || "Hosted web search route")
|
|
324
|
+
: (row.active === false ? "Configured but not ready" : "Configured provider");
|
|
325
|
+
|
|
326
|
+
options.push({
|
|
327
|
+
value: optionValue,
|
|
328
|
+
label,
|
|
329
|
+
hint,
|
|
330
|
+
groupKey: row.kind === "hosted" ? "hosted-web-search-routes" : "builtin-web-search-providers",
|
|
331
|
+
groupLabel: row.kind === "hosted" ? "Hosted search routes" : "Built-in search providers"
|
|
332
|
+
});
|
|
333
|
+
seen.add(optionKey);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const selectedValue = String(selectedProviderId || "").trim();
|
|
337
|
+
const selectedKey = normalizeWebSearchProviderKey(selectedValue);
|
|
338
|
+
if (selectedValue && selectedKey && !seen.has(selectedKey)) {
|
|
339
|
+
options.unshift({
|
|
340
|
+
value: selectedValue,
|
|
341
|
+
label: selectedValue,
|
|
342
|
+
hint: "Current config (not configured)",
|
|
343
|
+
groupKey: "current-claude-selection",
|
|
344
|
+
groupLabel: "Current selection"
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return options;
|
|
349
|
+
}
|
|
350
|
+
|
|
309
351
|
export function updateWebSearchConfig(config = {}, updates = {}) {
|
|
310
352
|
const next = ensureAmpDraftConfigShape(config);
|
|
311
353
|
const webSearch = ensureWebSearchConfigShape(next);
|
package/src/runtime/config.js
CHANGED
|
@@ -559,6 +559,12 @@ function normalizeAmpWebSearchProviderId(value) {
|
|
|
559
559
|
return AMP_WEB_SEARCH_PROVIDER_IDS.includes(normalized) ? normalized : "";
|
|
560
560
|
}
|
|
561
561
|
|
|
562
|
+
export function normalizeClaudeCodeWebSearchProvider(value) {
|
|
563
|
+
const normalized = String(value || "").trim();
|
|
564
|
+
if (!normalized) return "";
|
|
565
|
+
return normalizeAmpWebSearchProviderId(normalized) || normalized;
|
|
566
|
+
}
|
|
567
|
+
|
|
562
568
|
function normalizeAmpWebSearchStrategy(value) {
|
|
563
569
|
const normalized = String(value || "").trim().toLowerCase();
|
|
564
570
|
if (!normalized) return "ordered";
|
|
@@ -750,6 +756,32 @@ function normalizeAmpWebSearchConfig(rawWebSearch) {
|
|
|
750
756
|
};
|
|
751
757
|
}
|
|
752
758
|
|
|
759
|
+
function normalizeClaudeCodeConfig(rawClaudeCode) {
|
|
760
|
+
if (!rawClaudeCode || typeof rawClaudeCode !== "object" || Array.isArray(rawClaudeCode)) {
|
|
761
|
+
return undefined;
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const source = rawClaudeCode;
|
|
765
|
+
const hasWebSearchProvider = hasOwn(source, "webSearchProvider")
|
|
766
|
+
|| hasOwn(source, "web-search-provider")
|
|
767
|
+
|| hasOwn(source, "searchProvider")
|
|
768
|
+
|| hasOwn(source, "search-provider");
|
|
769
|
+
const webSearchProvider = normalizeClaudeCodeWebSearchProvider(
|
|
770
|
+
source.webSearchProvider
|
|
771
|
+
?? source["web-search-provider"]
|
|
772
|
+
?? source.searchProvider
|
|
773
|
+
?? source["search-provider"]
|
|
774
|
+
);
|
|
775
|
+
|
|
776
|
+
if (!hasWebSearchProvider) {
|
|
777
|
+
return undefined;
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
return webSearchProvider
|
|
781
|
+
? { webSearchProvider }
|
|
782
|
+
: {};
|
|
783
|
+
}
|
|
784
|
+
|
|
753
785
|
function supportsOpenAIHostedWebSearchRoute(provider, model) {
|
|
754
786
|
const providerFormats = dedupeStrings([...(provider?.formats || []), provider?.format]);
|
|
755
787
|
if (!providerFormats.includes(FORMATS.OPENAI)) return false;
|
|
@@ -1759,11 +1791,13 @@ export function normalizeRuntimeConfig(rawConfig, options = {}) {
|
|
|
1759
1791
|
? rawAmp
|
|
1760
1792
|
: {};
|
|
1761
1793
|
const rawWebSearch = raw.webSearch ?? raw["web-search"];
|
|
1794
|
+
const rawClaudeCode = raw.claudeCode ?? raw["claude-code"];
|
|
1762
1795
|
const amp = normalizeAmpConfig(rawAmp);
|
|
1763
1796
|
const webSearch = normalizeAmpWebSearchConfig(rawWebSearch)
|
|
1764
1797
|
?? (amp?.webSearch && typeof amp.webSearch === "object" && !Array.isArray(amp.webSearch)
|
|
1765
1798
|
? amp.webSearch
|
|
1766
1799
|
: undefined);
|
|
1800
|
+
const claudeCode = normalizeClaudeCodeConfig(rawClaudeCode);
|
|
1767
1801
|
const normalizedAmp = webSearch && amp?.webSearch && typeof amp.webSearch === "object" && !Array.isArray(amp.webSearch)
|
|
1768
1802
|
? Object.fromEntries(
|
|
1769
1803
|
Object.entries(amp).filter(([key]) => key !== "webSearch")
|
|
@@ -1780,6 +1814,7 @@ export function normalizeRuntimeConfig(rawConfig, options = {}) {
|
|
|
1780
1814
|
modelAliases,
|
|
1781
1815
|
amp: normalizedAmp,
|
|
1782
1816
|
...(webSearch ? { webSearch } : {}),
|
|
1817
|
+
...(claudeCode && Object.keys(claudeCode).length > 0 ? { claudeCode } : {}),
|
|
1783
1818
|
ollama,
|
|
1784
1819
|
metadata: sanitizeRuntimeMetadata(raw.metadata)
|
|
1785
1820
|
};
|
|
@@ -2085,6 +2120,21 @@ function validateAmpConfig(config, routingIndex, errors) {
|
|
|
2085
2120
|
}
|
|
2086
2121
|
}
|
|
2087
2122
|
}
|
|
2123
|
+
|
|
2124
|
+
const claudeCode = config?.claudeCode;
|
|
2125
|
+
if (claudeCode && typeof claudeCode === "object" && !Array.isArray(claudeCode)) {
|
|
2126
|
+
const selectedWebSearchProvider = normalizeClaudeCodeWebSearchProvider(claudeCode.webSearchProvider);
|
|
2127
|
+
if (selectedWebSearchProvider) {
|
|
2128
|
+
const configuredProviders = Array.isArray(webSearch?.providers) ? webSearch.providers : [];
|
|
2129
|
+
const selectedExists = configuredProviders.some((provider) => {
|
|
2130
|
+
const providerId = normalizeClaudeCodeWebSearchProvider(provider?.id);
|
|
2131
|
+
return providerId && providerId === selectedWebSearchProvider;
|
|
2132
|
+
});
|
|
2133
|
+
if (!selectedExists) {
|
|
2134
|
+
errors.push(`claudeCode.webSearchProvider '${selectedWebSearchProvider}' must reference a configured webSearch provider.`);
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2088
2138
|
}
|
|
2089
2139
|
|
|
2090
2140
|
export function validateRuntimeConfig(config, { requireMasterKey = false, requireProvider = false } = {}) {
|
|
@@ -2325,6 +2375,9 @@ export function sanitizeConfigForDisplay(config) {
|
|
|
2325
2375
|
...config,
|
|
2326
2376
|
masterKey: config.masterKey ? maskSecret(config.masterKey) : undefined,
|
|
2327
2377
|
...(sanitizedWebSearch ? { webSearch: sanitizedWebSearch } : {}),
|
|
2378
|
+
...(config.claudeCode && typeof config.claudeCode === "object" && !Array.isArray(config.claudeCode)
|
|
2379
|
+
? { claudeCode: { ...config.claudeCode } }
|
|
2380
|
+
: {}),
|
|
2328
2381
|
amp: sanitizedAmp,
|
|
2329
2382
|
providers: (config.providers || []).map((provider) => ({
|
|
2330
2383
|
...provider,
|
|
@@ -5,6 +5,7 @@ import { buildCandidateKey } from "../state-store.js";
|
|
|
5
5
|
import { consumeCandidateRateLimits, resolveWindowRange } from "../rate-limits.js";
|
|
6
6
|
import {
|
|
7
7
|
buildProviderHeaders,
|
|
8
|
+
normalizeClaudeCodeWebSearchProvider,
|
|
8
9
|
resolveProviderFormat,
|
|
9
10
|
resolveProviderUrl,
|
|
10
11
|
resolveRouteReference
|
|
@@ -311,16 +312,29 @@ function hasSearchToolType(type) {
|
|
|
311
312
|
if (!normalized) return false;
|
|
312
313
|
return normalized === SEARCH_TOOL_NAME
|
|
313
314
|
|| normalized.startsWith("web_search_preview")
|
|
314
|
-
|| normalized
|
|
315
|
+
|| normalized.startsWith("web_search_");
|
|
315
316
|
}
|
|
316
317
|
|
|
317
318
|
function hasSearchToolName(name) {
|
|
318
|
-
const normalized = String(name || "").trim().toLowerCase();
|
|
319
|
-
return normalized === SEARCH_TOOL_NAME
|
|
319
|
+
const normalized = String(name || "").trim().toLowerCase().replace(/\s+/g, "_");
|
|
320
|
+
return normalized === SEARCH_TOOL_NAME
|
|
321
|
+
|| normalized === "web_search_preview"
|
|
322
|
+
|| normalized === "websearch";
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function hasReadWebPageToolType(type) {
|
|
326
|
+
const normalized = String(type || "").trim().toLowerCase();
|
|
327
|
+
if (!normalized) return false;
|
|
328
|
+
return normalized === READ_WEB_PAGE_TOOL_NAME
|
|
329
|
+
|| normalized === "web_fetch"
|
|
330
|
+
|| normalized.startsWith("web_fetch_");
|
|
320
331
|
}
|
|
321
332
|
|
|
322
333
|
function hasReadWebPageToolName(name) {
|
|
323
|
-
|
|
334
|
+
const normalized = String(name || "").trim().toLowerCase().replace(/\s+/g, "_");
|
|
335
|
+
return normalized === READ_WEB_PAGE_TOOL_NAME
|
|
336
|
+
|| normalized === "web_fetch"
|
|
337
|
+
|| normalized === "webfetch";
|
|
324
338
|
}
|
|
325
339
|
|
|
326
340
|
function hasInterceptableTool(tool) {
|
|
@@ -338,7 +352,7 @@ function hasInterceptableToolName(name) {
|
|
|
338
352
|
|
|
339
353
|
function getToolName(tool) {
|
|
340
354
|
if (!tool || typeof tool !== "object") return "";
|
|
341
|
-
if (hasReadWebPageToolName(tool.name) || hasReadWebPageToolName(tool.function?.name)) {
|
|
355
|
+
if (hasReadWebPageToolType(tool.type) || hasReadWebPageToolName(tool.name) || hasReadWebPageToolName(tool.function?.name)) {
|
|
342
356
|
return READ_WEB_PAGE_TOOL_NAME;
|
|
343
357
|
}
|
|
344
358
|
if (hasSearchToolType(tool.type) || hasSearchToolName(tool.name) || hasSearchToolName(tool.function?.name)) {
|
|
@@ -405,6 +419,28 @@ function normalizeSearchProviderId(value) {
|
|
|
405
419
|
return AMP_WEB_SEARCH_PROVIDER_META.has(normalized) ? normalized : "";
|
|
406
420
|
}
|
|
407
421
|
|
|
422
|
+
function normalizeSearchProviderSelection(value) {
|
|
423
|
+
return normalizeClaudeCodeWebSearchProvider(value) || "";
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function hasNativeClaudeWebSearchTool(tool) {
|
|
427
|
+
const normalizedType = String(tool?.type || "").trim().toLowerCase();
|
|
428
|
+
return normalizedType === "web_search"
|
|
429
|
+
|| (normalizedType.startsWith("web_search_") && !normalizedType.startsWith("web_search_preview"));
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function hasNativeClaudeWebFetchTool(tool) {
|
|
433
|
+
const normalizedType = String(tool?.type || "").trim().toLowerCase();
|
|
434
|
+
return normalizedType === "web_fetch" || normalizedType.startsWith("web_fetch_");
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function shouldUseClaudeCodeWebSearchSelection(runtimeConfig = {}, originalBody = {}) {
|
|
438
|
+
const selectedProvider = normalizeSearchProviderSelection(runtimeConfig?.claudeCode?.webSearchProvider);
|
|
439
|
+
if (!selectedProvider) return false;
|
|
440
|
+
const tools = Array.isArray(originalBody?.tools) ? originalBody.tools : [];
|
|
441
|
+
return tools.some((tool) => hasNativeClaudeWebSearchTool(tool) || hasNativeClaudeWebFetchTool(tool));
|
|
442
|
+
}
|
|
443
|
+
|
|
408
444
|
function normalizeSearchRoutingStrategy(value) {
|
|
409
445
|
const normalized = String(value || "").trim().toLowerCase();
|
|
410
446
|
if (!normalized) return "ordered";
|
|
@@ -561,7 +597,7 @@ function normalizeConfiguredSearchProviders(rawProviders, raw = {}, env = {}) {
|
|
|
561
597
|
];
|
|
562
598
|
}
|
|
563
599
|
|
|
564
|
-
export function resolveAmpWebSearchConfig(runtimeConfig = {}, env = {}) {
|
|
600
|
+
export function resolveAmpWebSearchConfig(runtimeConfig = {}, env = {}, options = {}) {
|
|
565
601
|
const amp = runtimeConfig?.amp && typeof runtimeConfig.amp === "object" ? runtimeConfig.amp : {};
|
|
566
602
|
const raw = runtimeConfig?.webSearch && typeof runtimeConfig.webSearch === "object" && !Array.isArray(runtimeConfig.webSearch)
|
|
567
603
|
? runtimeConfig.webSearch
|
|
@@ -574,11 +610,17 @@ export function resolveAmpWebSearchConfig(runtimeConfig = {}, env = {}) {
|
|
|
574
610
|
{ min: MIN_SEARCH_COUNT, max: MAX_SEARCH_COUNT }
|
|
575
611
|
);
|
|
576
612
|
const providers = normalizeConfiguredSearchProviders(raw.providers, raw, env);
|
|
613
|
+
const selectedProviderId = shouldUseClaudeCodeWebSearchSelection(runtimeConfig, options?.originalBody)
|
|
614
|
+
? normalizeSearchProviderSelection(runtimeConfig?.claudeCode?.webSearchProvider)
|
|
615
|
+
: "";
|
|
616
|
+
const selectedProviders = selectedProviderId
|
|
617
|
+
? providers.filter((provider) => normalizeSearchProviderSelection(provider?.id) === selectedProviderId)
|
|
618
|
+
: [];
|
|
577
619
|
|
|
578
620
|
return {
|
|
579
621
|
strategy: normalizeSearchRoutingStrategy(raw.strategy ?? env.AMP_WEB_SEARCH_STRATEGY),
|
|
580
622
|
count,
|
|
581
|
-
providers
|
|
623
|
+
providers: selectedProviders.length > 0 ? selectedProviders : providers
|
|
582
624
|
};
|
|
583
625
|
}
|
|
584
626
|
|
|
@@ -895,8 +937,8 @@ function buildSearchProviderStatus(provider, evaluation, runtimeConfig = {}) {
|
|
|
895
937
|
};
|
|
896
938
|
}
|
|
897
939
|
|
|
898
|
-
export async function buildAmpWebSearchSnapshot(runtimeConfig = {}, { env = {}, stateStore = null, now = Date.now() } = {}) {
|
|
899
|
-
const settings = resolveAmpWebSearchConfig(runtimeConfig, env);
|
|
940
|
+
export async function buildAmpWebSearchSnapshot(runtimeConfig = {}, { env = {}, stateStore = null, now = Date.now(), originalBody = null } = {}) {
|
|
941
|
+
const settings = resolveAmpWebSearchConfig(runtimeConfig, env, { originalBody });
|
|
900
942
|
const effectiveStateStore = resolveSearchStateStore(stateStore);
|
|
901
943
|
const providers = [];
|
|
902
944
|
|
|
@@ -1198,7 +1240,8 @@ export async function executeAmpWebSearch(query, runtimeConfig = {}, env = {}, o
|
|
|
1198
1240
|
const snapshot = await buildAmpWebSearchSnapshot(runtimeConfig, {
|
|
1199
1241
|
env,
|
|
1200
1242
|
stateStore: options.stateStore,
|
|
1201
|
-
now: options.now
|
|
1243
|
+
now: options.now,
|
|
1244
|
+
originalBody: options.originalBody
|
|
1202
1245
|
});
|
|
1203
1246
|
const stateStore = resolveSearchStateStore(options.stateStore);
|
|
1204
1247
|
const configuredProviders = snapshot.providers.filter((provider) => provider.ready);
|
|
@@ -1227,7 +1270,8 @@ export async function executeAmpWebSearch(query, runtimeConfig = {}, env = {}, o
|
|
|
1227
1270
|
const refreshedSnapshot = await buildAmpWebSearchSnapshot(runtimeConfig, {
|
|
1228
1271
|
env,
|
|
1229
1272
|
stateStore,
|
|
1230
|
-
now: options.now
|
|
1273
|
+
now: options.now,
|
|
1274
|
+
originalBody: options.originalBody
|
|
1231
1275
|
});
|
|
1232
1276
|
const refreshedProvider = refreshedSnapshot.providers.find((entry) => entry.id === providerStatus.id) || providerStatus;
|
|
1233
1277
|
return {
|
|
@@ -1271,12 +1315,29 @@ export function shouldInterceptAmpWebSearch({ clientType, originalBody, runtimeC
|
|
|
1271
1315
|
if (requestedToolNames.length === 0) {
|
|
1272
1316
|
return false;
|
|
1273
1317
|
}
|
|
1318
|
+
const requestsWebSearch = requestedToolNames.includes(SEARCH_TOOL_NAME);
|
|
1319
|
+
const requestsReadWebPage = requestedToolNames.includes(READ_WEB_PAGE_TOOL_NAME);
|
|
1274
1320
|
const readyProviders = resolveAmpWebSearchConfig(runtimeConfig, env).providers.filter((provider) => {
|
|
1275
1321
|
if (!isSearchProviderConfigured(provider)) return false;
|
|
1276
1322
|
if (!isHostedSearchProvider(provider)) return true;
|
|
1277
1323
|
const resolvedRoute = getResolvedHostedSearchRoute(runtimeConfig, provider);
|
|
1278
1324
|
return Boolean(resolvedRoute && supportsResolvedHostedSearchRoute(resolvedRoute.provider, resolvedRoute.model));
|
|
1279
1325
|
});
|
|
1326
|
+
if (
|
|
1327
|
+
clientType === "amp"
|
|
1328
|
+
&& requestsWebSearch
|
|
1329
|
+
&& !requestsReadWebPage
|
|
1330
|
+
&& readyProviders.length === 0
|
|
1331
|
+
&& runtimeConfig?.amp?.proxyWebSearchToUpstream === true
|
|
1332
|
+
&& String(runtimeConfig?.amp?.upstreamUrl || "").trim()
|
|
1333
|
+
&& String(runtimeConfig?.amp?.upstreamApiKey || "").trim()
|
|
1334
|
+
) {
|
|
1335
|
+
return false;
|
|
1336
|
+
}
|
|
1337
|
+
const hasNativeClaudeWebTool = tools.some((tool) => hasNativeClaudeWebSearchTool(tool) || hasNativeClaudeWebFetchTool(tool));
|
|
1338
|
+
if (hasNativeClaudeWebTool) {
|
|
1339
|
+
return true;
|
|
1340
|
+
}
|
|
1280
1341
|
if (readyProviders.length === 0) {
|
|
1281
1342
|
return clientType === "amp" && requestedToolNames.includes(READ_WEB_PAGE_TOOL_NAME);
|
|
1282
1343
|
}
|
|
@@ -2324,6 +2385,7 @@ export async function maybeInterceptAmpWebSearch({
|
|
|
2324
2385
|
runtimeConfig,
|
|
2325
2386
|
env,
|
|
2326
2387
|
stateStore,
|
|
2388
|
+
originalBody,
|
|
2327
2389
|
executeProviderRequest
|
|
2328
2390
|
} = {}) {
|
|
2329
2391
|
if (!(response instanceof Response)) {
|
|
@@ -2362,7 +2424,8 @@ export async function maybeInterceptAmpWebSearch({
|
|
|
2362
2424
|
runtimeConfig,
|
|
2363
2425
|
env,
|
|
2364
2426
|
{
|
|
2365
|
-
stateStore
|
|
2427
|
+
stateStore,
|
|
2428
|
+
originalBody
|
|
2366
2429
|
}
|
|
2367
2430
|
));
|
|
2368
2431
|
}
|
|
@@ -920,6 +920,7 @@ export async function makeProviderCall({
|
|
|
920
920
|
runtimeConfig,
|
|
921
921
|
env,
|
|
922
922
|
stateStore,
|
|
923
|
+
originalBody: body,
|
|
923
924
|
executeProviderRequest: async (followUpBody) => {
|
|
924
925
|
const followUpResult = await executeSubscriptionRequest(followUpBody);
|
|
925
926
|
return followUpResult?.response instanceof Response ? followUpResult.response : null;
|
|
@@ -1219,6 +1220,7 @@ export async function makeProviderCall({
|
|
|
1219
1220
|
runtimeConfig,
|
|
1220
1221
|
env,
|
|
1221
1222
|
stateStore,
|
|
1223
|
+
originalBody: body,
|
|
1222
1224
|
executeProviderRequest: async (followUpBody) => {
|
|
1223
1225
|
try {
|
|
1224
1226
|
return await executeHttpProviderRequest({
|