@khanglvm/llm-router 2.0.0-beta.1 → 2.0.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 +27 -0
- package/README.md +163 -426
- package/package.json +3 -3
- package/src/cli/router-module.js +2773 -2587
- package/src/cli-entry.js +32 -103
- package/src/node/activity-log.js +119 -0
- package/src/node/coding-tool-config.js +85 -11
- package/src/node/config-workflows.js +51 -12
- package/src/node/instance-state.js +1 -1
- package/src/node/litellm-context-catalog.js +184 -0
- package/src/node/local-server.js +23 -3
- package/src/node/port-reclaim.js +2 -2
- package/src/node/start-command.js +22 -22
- package/src/node/startup-manager.js +3 -3
- package/src/node/web-command.js +1 -1
- package/src/node/web-console-assets.js +1 -1
- package/src/node/web-console-client.js +34 -29
- package/src/node/web-console-server.js +420 -38
- package/src/node/web-console-styles.generated.js +1 -1
- package/src/node/web-console-ui/buffered-text-input.js +133 -0
- package/src/node/web-console-ui/config-editor-utils.js +57 -4
- package/src/node/web-console-ui/dropdown-placement.js +153 -0
- package/src/node/web-console-ui/select-search-utils.js +6 -0
- package/src/node/web-console-ui/transient-integer-input-utils.js +12 -0
- package/src/runtime/balancer.js +78 -1
- package/src/runtime/codex-request-transformer.js +16 -7
- package/src/runtime/config.js +448 -12
- package/src/runtime/handler/amp-response.js +5 -3
- package/src/runtime/handler/amp-web-search.js +2232 -0
- package/src/runtime/handler/fallback.js +30 -2
- package/src/runtime/handler/provider-call.js +353 -36
- package/src/runtime/handler/provider-translation.js +14 -0
- package/src/runtime/handler/request.js +128 -2
- package/src/runtime/handler/route-debug.js +36 -0
- package/src/runtime/handler.js +210 -20
- package/src/runtime/subscription-provider.js +1 -1
- package/src/shared/coding-tool-bindings.js +49 -0
- package/src/shared/local-router-defaults.js +62 -0
- package/src/translator/request/claude-to-openai.js +43 -0
|
@@ -44,6 +44,18 @@ const POLICY_HINTS = [
|
|
|
44
44
|
"unsafe",
|
|
45
45
|
"flagged"
|
|
46
46
|
];
|
|
47
|
+
const CONTEXT_WINDOW_HINTS = [
|
|
48
|
+
"context window",
|
|
49
|
+
"maximum context length",
|
|
50
|
+
"maximum context size",
|
|
51
|
+
"context length exceeded",
|
|
52
|
+
"context_length_exceeded",
|
|
53
|
+
"prompt is too long",
|
|
54
|
+
"input is too long",
|
|
55
|
+
"request is too long",
|
|
56
|
+
"too many tokens",
|
|
57
|
+
"ran out of room in the model's context window"
|
|
58
|
+
];
|
|
47
59
|
const fallbackCircuitState = new Map();
|
|
48
60
|
|
|
49
61
|
export function shouldRetryStatus(status) {
|
|
@@ -235,9 +247,12 @@ export function setCandidateCooldown(candidate, cooldownMs, policy, status, now
|
|
|
235
247
|
}
|
|
236
248
|
|
|
237
249
|
async function readProviderErrorHint(result) {
|
|
238
|
-
|
|
250
|
+
const response = result?.upstreamResponse instanceof Response
|
|
251
|
+
? result.upstreamResponse
|
|
252
|
+
: (result?.response instanceof Response ? result.response : null);
|
|
253
|
+
if (!(response instanceof Response)) return "";
|
|
239
254
|
try {
|
|
240
|
-
const raw = await
|
|
255
|
+
const raw = await response.clone().text();
|
|
241
256
|
if (!raw) return "";
|
|
242
257
|
const limitedRaw = raw.slice(0, ERROR_TEXT_SCAN_LIMIT);
|
|
243
258
|
const parsed = parseJsonSafely(limitedRaw);
|
|
@@ -386,6 +401,19 @@ export async function classifyFailureResult(result, retryPolicy) {
|
|
|
386
401
|
};
|
|
387
402
|
}
|
|
388
403
|
|
|
404
|
+
if ([400, 413, 422].includes(status)) {
|
|
405
|
+
const hintText = await readProviderErrorHint(result);
|
|
406
|
+
if (hasAnyHint(hintText, CONTEXT_WINDOW_HINTS)) {
|
|
407
|
+
return {
|
|
408
|
+
category: "context_window_exceeded",
|
|
409
|
+
retryable: false,
|
|
410
|
+
retryOrigin: false,
|
|
411
|
+
allowFallback: true,
|
|
412
|
+
originCooldownMs: 0
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
389
417
|
if (status === 408 || status === 409 || status >= 500) {
|
|
390
418
|
return {
|
|
391
419
|
category: "temporary_error",
|
|
@@ -31,6 +31,11 @@ import {
|
|
|
31
31
|
handleCodexStreamToOpenAI
|
|
32
32
|
} from "../codex-response-transformer.js";
|
|
33
33
|
import { toBoolean } from "./utils.js";
|
|
34
|
+
import {
|
|
35
|
+
maybeInterceptAmpWebSearch,
|
|
36
|
+
rewriteProviderBodyForAmpWebSearch,
|
|
37
|
+
shouldInterceptAmpWebSearch
|
|
38
|
+
} from "./amp-web-search.js";
|
|
34
39
|
|
|
35
40
|
async function toProviderError(response) {
|
|
36
41
|
const raw = await response.text();
|
|
@@ -88,7 +93,8 @@ async function adaptProviderResponse({
|
|
|
88
93
|
fallbackModel,
|
|
89
94
|
requestKind,
|
|
90
95
|
requestBody,
|
|
91
|
-
clientType
|
|
96
|
+
clientType,
|
|
97
|
+
env
|
|
92
98
|
}) {
|
|
93
99
|
const buildSuccessResponse = async (resultResponse) => ({
|
|
94
100
|
ok: true,
|
|
@@ -97,7 +103,8 @@ async function adaptProviderResponse({
|
|
|
97
103
|
response: await maybeRewriteAmpClientResponse(resultResponse, {
|
|
98
104
|
clientType,
|
|
99
105
|
requestBody,
|
|
100
|
-
stream
|
|
106
|
+
stream,
|
|
107
|
+
env
|
|
101
108
|
})
|
|
102
109
|
});
|
|
103
110
|
|
|
@@ -205,6 +212,228 @@ function extractToolTypes(body) {
|
|
|
205
212
|
)];
|
|
206
213
|
}
|
|
207
214
|
|
|
215
|
+
function isOpenAIHostedWebSearchRequest(targetFormat, requestKind) {
|
|
216
|
+
return targetFormat === FORMATS.OPENAI && requestKind === "responses";
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function normalizeOpenAIHostedWebSearchType(value) {
|
|
220
|
+
return String(value || "").trim().toLowerCase();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function isOpenAIHostedWebSearchToolType(type) {
|
|
224
|
+
const normalized = normalizeOpenAIHostedWebSearchType(type);
|
|
225
|
+
return normalized === "web_search" || normalized.startsWith("web_search_preview");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function isOpenAINativeWebSearchToolType(type) {
|
|
229
|
+
const normalized = normalizeOpenAIHostedWebSearchType(type);
|
|
230
|
+
return normalized === "web_search"
|
|
231
|
+
|| (normalized.startsWith("web_search_") && !normalized.startsWith("web_search_preview"));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function hasOpenAIHostedWebSearchTool(body) {
|
|
235
|
+
const tools = Array.isArray(body?.tools) ? body.tools : [];
|
|
236
|
+
return tools.some((tool) =>
|
|
237
|
+
isOpenAIHostedWebSearchToolType(tool?.type) || isOpenAINativeWebSearchToolType(tool?.type)
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function normalizeOpenAIHostedWebSearchToolChoice(toolChoice, toolType) {
|
|
242
|
+
if (typeof toolChoice === "string") {
|
|
243
|
+
const normalized = normalizeOpenAIHostedWebSearchType(toolChoice);
|
|
244
|
+
if (isOpenAIHostedWebSearchToolType(normalized) || isOpenAINativeWebSearchToolType(normalized)) {
|
|
245
|
+
return "required";
|
|
246
|
+
}
|
|
247
|
+
return toolChoice;
|
|
248
|
+
}
|
|
249
|
+
if (!toolChoice || typeof toolChoice !== "object" || Array.isArray(toolChoice)) {
|
|
250
|
+
return toolChoice;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const normalizedType = normalizeOpenAIHostedWebSearchType(toolChoice.type);
|
|
254
|
+
if (normalizedType === "none" || normalizedType === "auto") {
|
|
255
|
+
return normalizedType;
|
|
256
|
+
}
|
|
257
|
+
if (!isOpenAIHostedWebSearchToolType(normalizedType) && !isOpenAINativeWebSearchToolType(normalizedType)) {
|
|
258
|
+
return toolChoice;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
void toolType;
|
|
262
|
+
return "required";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function rewriteProviderBodyForOpenAIHostedWebSearch(providerBody, toolType) {
|
|
266
|
+
if (!toolType || !isOpenAINativeWebSearchToolType(toolType)) {
|
|
267
|
+
return {
|
|
268
|
+
providerBody,
|
|
269
|
+
rewritten: false
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const tools = Array.isArray(providerBody?.tools) ? providerBody.tools : null;
|
|
274
|
+
let rewritten = false;
|
|
275
|
+
const nextTools = tools
|
|
276
|
+
? tools.map((tool) => {
|
|
277
|
+
if (!tool || typeof tool !== "object" || !isOpenAIHostedWebSearchToolType(tool.type)) {
|
|
278
|
+
return tool;
|
|
279
|
+
}
|
|
280
|
+
if (normalizeOpenAIHostedWebSearchType(tool.type) === toolType) {
|
|
281
|
+
return tool;
|
|
282
|
+
}
|
|
283
|
+
rewritten = true;
|
|
284
|
+
return {
|
|
285
|
+
...tool,
|
|
286
|
+
type: toolType
|
|
287
|
+
};
|
|
288
|
+
})
|
|
289
|
+
: tools;
|
|
290
|
+
|
|
291
|
+
const nextToolChoice = normalizeOpenAIHostedWebSearchToolChoice(providerBody?.tool_choice, toolType);
|
|
292
|
+
if (nextToolChoice !== providerBody?.tool_choice) {
|
|
293
|
+
rewritten = true;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (!rewritten) {
|
|
297
|
+
return {
|
|
298
|
+
providerBody,
|
|
299
|
+
rewritten: false
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
rewritten: true,
|
|
305
|
+
providerBody: {
|
|
306
|
+
...providerBody,
|
|
307
|
+
...(nextTools ? { tools: nextTools } : {}),
|
|
308
|
+
...(nextToolChoice !== undefined ? { tool_choice: nextToolChoice } : {})
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function getProviderOpenAIHostedWebSearchToolType(provider, { targetFormat, requestKind } = {}) {
|
|
314
|
+
if (!isOpenAIHostedWebSearchRequest(targetFormat, requestKind)) return "";
|
|
315
|
+
|
|
316
|
+
const subscriptionType = String(provider?.subscriptionType || provider?.subscription_type || "").trim().toLowerCase();
|
|
317
|
+
if (subscriptionType === "chatgpt-codex") {
|
|
318
|
+
return "web_search";
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const candidates = [
|
|
322
|
+
provider?.lastProbe?.openaiResponses?.webSearchToolType,
|
|
323
|
+
provider?.lastProbe?.toolSupport?.openaiResponses?.webSearchToolType,
|
|
324
|
+
provider?.metadata?.openaiResponses?.webSearchToolType,
|
|
325
|
+
provider?.metadata?.toolSupport?.openaiResponses?.webSearchToolType
|
|
326
|
+
];
|
|
327
|
+
|
|
328
|
+
for (const candidate of candidates) {
|
|
329
|
+
const normalized = normalizeOpenAIHostedWebSearchType(candidate);
|
|
330
|
+
if (isOpenAINativeWebSearchToolType(normalized)) {
|
|
331
|
+
return normalized;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return "";
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
async function readProviderErrorHint(response) {
|
|
339
|
+
if (!(response instanceof Response)) return "";
|
|
340
|
+
try {
|
|
341
|
+
const raw = await response.clone().text();
|
|
342
|
+
if (!raw) return "";
|
|
343
|
+
const parsed = parseJsonSafely(raw);
|
|
344
|
+
return [
|
|
345
|
+
parsed?.error?.code,
|
|
346
|
+
parsed?.error?.type,
|
|
347
|
+
parsed?.error?.message,
|
|
348
|
+
parsed?.error,
|
|
349
|
+
parsed?.code,
|
|
350
|
+
parsed?.type,
|
|
351
|
+
parsed?.message,
|
|
352
|
+
raw
|
|
353
|
+
]
|
|
354
|
+
.filter((entry) => entry !== undefined && entry !== null)
|
|
355
|
+
.map((entry) => String(entry).toLowerCase())
|
|
356
|
+
.join(" ");
|
|
357
|
+
} catch {
|
|
358
|
+
return "";
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function isUnsupportedOpenAIHostedWebSearchHint(hint) {
|
|
363
|
+
const normalized = String(hint || "").toLowerCase();
|
|
364
|
+
if (!normalized || !normalized.includes("web_search")) return false;
|
|
365
|
+
return normalized.includes("unsupported tool type")
|
|
366
|
+
|| normalized.includes("tool type is not supported")
|
|
367
|
+
|| normalized.includes("tool is not supported")
|
|
368
|
+
|| normalized.includes("does not support tool")
|
|
369
|
+
|| normalized.includes("unsupported tool");
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
async function maybeRetryOpenAIHostedWebSearchProviderRequest({
|
|
373
|
+
response,
|
|
374
|
+
executeProviderRequest,
|
|
375
|
+
providerBody,
|
|
376
|
+
targetFormat,
|
|
377
|
+
requestKind
|
|
378
|
+
} = {}) {
|
|
379
|
+
if (!(response instanceof Response) || typeof executeProviderRequest !== "function") {
|
|
380
|
+
return {
|
|
381
|
+
response,
|
|
382
|
+
providerBody,
|
|
383
|
+
retried: false
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
if (!isOpenAIHostedWebSearchRequest(targetFormat, requestKind) || !hasOpenAIHostedWebSearchTool(providerBody)) {
|
|
387
|
+
return {
|
|
388
|
+
response,
|
|
389
|
+
providerBody,
|
|
390
|
+
retried: false
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const rewritten = rewriteProviderBodyForOpenAIHostedWebSearch(providerBody, "web_search");
|
|
395
|
+
if (!rewritten.rewritten) {
|
|
396
|
+
return {
|
|
397
|
+
response,
|
|
398
|
+
providerBody,
|
|
399
|
+
retried: false
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const errorHint = await readProviderErrorHint(response);
|
|
404
|
+
if (!errorHint.includes("web_search_preview") || !isUnsupportedOpenAIHostedWebSearchHint(errorHint)) {
|
|
405
|
+
return {
|
|
406
|
+
response,
|
|
407
|
+
providerBody,
|
|
408
|
+
retried: false
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
try {
|
|
413
|
+
const retriedResponse = await executeProviderRequest(rewritten.providerBody);
|
|
414
|
+
return {
|
|
415
|
+
response: retriedResponse instanceof Response ? retriedResponse : response,
|
|
416
|
+
providerBody: rewritten.providerBody,
|
|
417
|
+
retried: retriedResponse instanceof Response
|
|
418
|
+
};
|
|
419
|
+
} catch {
|
|
420
|
+
return {
|
|
421
|
+
response,
|
|
422
|
+
providerBody,
|
|
423
|
+
retried: false
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async function resolveHostedWebSearchErrorKind(response, providerBody, { targetFormat, requestKind } = {}) {
|
|
429
|
+
if (!isOpenAIHostedWebSearchRequest(targetFormat, requestKind) || !hasOpenAIHostedWebSearchTool(providerBody)) {
|
|
430
|
+
return "";
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const errorHint = await readProviderErrorHint(response);
|
|
434
|
+
return isUnsupportedOpenAIHostedWebSearchHint(errorHint) ? "not_supported_error" : "";
|
|
435
|
+
}
|
|
436
|
+
|
|
208
437
|
function logToolRouting({ env, clientType, candidate, originalBody, providerBody, sourceFormat, targetFormat } = {}) {
|
|
209
438
|
if (!isProviderDebugEnabled(env)) return;
|
|
210
439
|
|
|
@@ -225,11 +454,19 @@ export async function makeProviderCall({
|
|
|
225
454
|
requestKind,
|
|
226
455
|
requestHeaders,
|
|
227
456
|
env,
|
|
228
|
-
clientType
|
|
457
|
+
clientType,
|
|
458
|
+
runtimeConfig,
|
|
459
|
+
stateStore
|
|
229
460
|
}) {
|
|
230
461
|
const provider = candidate.provider;
|
|
231
462
|
const targetFormat = candidate.targetFormat;
|
|
232
463
|
const translate = needsTranslation(sourceFormat, targetFormat);
|
|
464
|
+
const interceptAmpWebSearch = shouldInterceptAmpWebSearch({
|
|
465
|
+
clientType,
|
|
466
|
+
originalBody: body,
|
|
467
|
+
runtimeConfig,
|
|
468
|
+
env
|
|
469
|
+
});
|
|
233
470
|
|
|
234
471
|
let providerBody = { ...body };
|
|
235
472
|
if (translate) {
|
|
@@ -267,6 +504,20 @@ export async function makeProviderCall({
|
|
|
267
504
|
targetModel: candidate.backend,
|
|
268
505
|
requestHeaders
|
|
269
506
|
});
|
|
507
|
+
const declaredOpenAIHostedWebSearchToolType = getProviderOpenAIHostedWebSearchToolType(provider, {
|
|
508
|
+
targetFormat,
|
|
509
|
+
requestKind
|
|
510
|
+
});
|
|
511
|
+
const declaredOpenAIHostedWebSearchRewrite = rewriteProviderBodyForOpenAIHostedWebSearch(
|
|
512
|
+
providerBody,
|
|
513
|
+
declaredOpenAIHostedWebSearchToolType
|
|
514
|
+
);
|
|
515
|
+
if (declaredOpenAIHostedWebSearchRewrite.rewritten) {
|
|
516
|
+
providerBody = declaredOpenAIHostedWebSearchRewrite.providerBody;
|
|
517
|
+
}
|
|
518
|
+
if (interceptAmpWebSearch) {
|
|
519
|
+
providerBody = rewriteProviderBodyForAmpWebSearch(providerBody, targetFormat).providerBody;
|
|
520
|
+
}
|
|
270
521
|
logToolRouting({
|
|
271
522
|
env,
|
|
272
523
|
clientType,
|
|
@@ -279,13 +530,14 @@ export async function makeProviderCall({
|
|
|
279
530
|
|
|
280
531
|
if (isSubscriptionProvider(provider)) {
|
|
281
532
|
const subscriptionType = String(provider?.subscriptionType || provider?.subscription_type || "").trim().toLowerCase();
|
|
282
|
-
const
|
|
533
|
+
const executeSubscriptionRequest = async (requestBody) => makeSubscriptionProviderCall({
|
|
283
534
|
provider,
|
|
284
|
-
body:
|
|
535
|
+
body: requestBody,
|
|
285
536
|
// ChatGPT Codex backend expects stream=true; non-stream responses are reconstructed from SSE.
|
|
286
537
|
stream: subscriptionType === "chatgpt-codex" ? true : Boolean(stream),
|
|
287
538
|
env
|
|
288
539
|
});
|
|
540
|
+
const subscriptionResult = await executeSubscriptionRequest(providerBody);
|
|
289
541
|
|
|
290
542
|
if (!subscriptionResult?.ok) {
|
|
291
543
|
return subscriptionResult;
|
|
@@ -307,9 +559,27 @@ export async function makeProviderCall({
|
|
|
307
559
|
}
|
|
308
560
|
|
|
309
561
|
const fallbackModel = candidate?.backend || providerBody?.model || "unknown";
|
|
562
|
+
let upstreamResponse = subscriptionResult.response;
|
|
563
|
+
if (interceptAmpWebSearch) {
|
|
564
|
+
const intercepted = await maybeInterceptAmpWebSearch({
|
|
565
|
+
response: upstreamResponse,
|
|
566
|
+
providerBody,
|
|
567
|
+
targetFormat,
|
|
568
|
+
requestKind,
|
|
569
|
+
stream,
|
|
570
|
+
runtimeConfig,
|
|
571
|
+
env,
|
|
572
|
+
stateStore,
|
|
573
|
+
executeProviderRequest: async (followUpBody) => {
|
|
574
|
+
const followUpResult = await executeSubscriptionRequest(followUpBody);
|
|
575
|
+
return followUpResult?.response instanceof Response ? followUpResult.response : null;
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
upstreamResponse = intercepted.response;
|
|
579
|
+
}
|
|
310
580
|
if (subscriptionType !== "chatgpt-codex") {
|
|
311
581
|
return adaptProviderResponse({
|
|
312
|
-
response:
|
|
582
|
+
response: upstreamResponse,
|
|
313
583
|
stream,
|
|
314
584
|
translate,
|
|
315
585
|
sourceFormat,
|
|
@@ -317,7 +587,8 @@ export async function makeProviderCall({
|
|
|
317
587
|
fallbackModel,
|
|
318
588
|
requestKind,
|
|
319
589
|
requestBody: body,
|
|
320
|
-
clientType
|
|
590
|
+
clientType,
|
|
591
|
+
env
|
|
321
592
|
});
|
|
322
593
|
}
|
|
323
594
|
|
|
@@ -328,7 +599,7 @@ export async function makeProviderCall({
|
|
|
328
599
|
status: 200,
|
|
329
600
|
retryable: false,
|
|
330
601
|
response: await maybeRewriteAmpClientResponse(
|
|
331
|
-
passthroughResponseWithCors(
|
|
602
|
+
passthroughResponseWithCors(upstreamResponse, {
|
|
332
603
|
"Content-Type": "text/event-stream",
|
|
333
604
|
"Cache-Control": "no-cache",
|
|
334
605
|
Connection: "keep-alive"
|
|
@@ -336,13 +607,14 @@ export async function makeProviderCall({
|
|
|
336
607
|
{
|
|
337
608
|
clientType,
|
|
338
609
|
requestBody: body,
|
|
339
|
-
stream
|
|
610
|
+
stream,
|
|
611
|
+
env
|
|
340
612
|
}
|
|
341
613
|
)
|
|
342
614
|
};
|
|
343
615
|
}
|
|
344
616
|
|
|
345
|
-
const parsedSubscriptionResponse = await extractCodexFinalResponse(
|
|
617
|
+
const parsedSubscriptionResponse = await extractCodexFinalResponse(upstreamResponse);
|
|
346
618
|
if (!parsedSubscriptionResponse) {
|
|
347
619
|
return {
|
|
348
620
|
ok: false,
|
|
@@ -365,13 +637,14 @@ export async function makeProviderCall({
|
|
|
365
637
|
response: await maybeRewriteAmpClientResponse(jsonResponse(parsedSubscriptionResponse), {
|
|
366
638
|
clientType,
|
|
367
639
|
requestBody: body,
|
|
368
|
-
stream
|
|
640
|
+
stream,
|
|
641
|
+
env
|
|
369
642
|
})
|
|
370
643
|
};
|
|
371
644
|
}
|
|
372
645
|
|
|
373
646
|
if (stream) {
|
|
374
|
-
const openAIStreamResponse = handleCodexStreamToOpenAI(
|
|
647
|
+
const openAIStreamResponse = handleCodexStreamToOpenAI(upstreamResponse, {
|
|
375
648
|
fallbackModel
|
|
376
649
|
});
|
|
377
650
|
if (sourceFormat === FORMATS.CLAUDE) {
|
|
@@ -382,7 +655,8 @@ export async function makeProviderCall({
|
|
|
382
655
|
response: await maybeRewriteAmpClientResponse(handleOpenAIStreamToClaude(openAIStreamResponse), {
|
|
383
656
|
clientType,
|
|
384
657
|
requestBody: body,
|
|
385
|
-
stream
|
|
658
|
+
stream,
|
|
659
|
+
env
|
|
386
660
|
})
|
|
387
661
|
};
|
|
388
662
|
}
|
|
@@ -393,12 +667,13 @@ export async function makeProviderCall({
|
|
|
393
667
|
response: await maybeRewriteAmpClientResponse(openAIStreamResponse, {
|
|
394
668
|
clientType,
|
|
395
669
|
requestBody: body,
|
|
396
|
-
stream
|
|
670
|
+
stream,
|
|
671
|
+
env
|
|
397
672
|
})
|
|
398
673
|
};
|
|
399
674
|
}
|
|
400
675
|
|
|
401
|
-
const parsedSubscriptionResponse = await extractCodexFinalResponse(
|
|
676
|
+
const parsedSubscriptionResponse = await extractCodexFinalResponse(upstreamResponse);
|
|
402
677
|
if (!parsedSubscriptionResponse) {
|
|
403
678
|
return {
|
|
404
679
|
ok: false,
|
|
@@ -427,7 +702,8 @@ export async function makeProviderCall({
|
|
|
427
702
|
{
|
|
428
703
|
clientType,
|
|
429
704
|
requestBody: body,
|
|
430
|
-
stream
|
|
705
|
+
stream,
|
|
706
|
+
env
|
|
431
707
|
}
|
|
432
708
|
)
|
|
433
709
|
};
|
|
@@ -440,7 +716,8 @@ export async function makeProviderCall({
|
|
|
440
716
|
response: await maybeRewriteAmpClientResponse(jsonResponse(openAINonStreamResponse), {
|
|
441
717
|
clientType,
|
|
442
718
|
requestBody: body,
|
|
443
|
-
stream
|
|
719
|
+
stream,
|
|
720
|
+
env
|
|
444
721
|
})
|
|
445
722
|
};
|
|
446
723
|
}
|
|
@@ -451,6 +728,24 @@ export async function makeProviderCall({
|
|
|
451
728
|
requestHeaders,
|
|
452
729
|
targetFormat
|
|
453
730
|
);
|
|
731
|
+
const executeHttpProviderRequest = async (requestBody) => {
|
|
732
|
+
const timeoutMs = resolveUpstreamTimeoutMs(env);
|
|
733
|
+
const timeoutControl = buildTimeoutSignal(timeoutMs);
|
|
734
|
+
try {
|
|
735
|
+
const init = {
|
|
736
|
+
method: "POST",
|
|
737
|
+
headers,
|
|
738
|
+
body: JSON.stringify(requestBody)
|
|
739
|
+
};
|
|
740
|
+
if (timeoutControl.signal) {
|
|
741
|
+
init.signal = timeoutControl.signal;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
return await fetch(providerUrl, init);
|
|
745
|
+
} finally {
|
|
746
|
+
timeoutControl.cleanup();
|
|
747
|
+
}
|
|
748
|
+
};
|
|
454
749
|
|
|
455
750
|
if (!providerUrl) {
|
|
456
751
|
return {
|
|
@@ -469,23 +764,8 @@ export async function makeProviderCall({
|
|
|
469
764
|
}
|
|
470
765
|
|
|
471
766
|
let response;
|
|
472
|
-
let cleanupTimeout = () => {};
|
|
473
767
|
try {
|
|
474
|
-
|
|
475
|
-
const timeoutControl = buildTimeoutSignal(timeoutMs);
|
|
476
|
-
cleanupTimeout = timeoutControl.cleanup;
|
|
477
|
-
const init = {
|
|
478
|
-
method: "POST",
|
|
479
|
-
headers,
|
|
480
|
-
body: JSON.stringify(providerBody)
|
|
481
|
-
};
|
|
482
|
-
if (timeoutControl.signal) {
|
|
483
|
-
init.signal = timeoutControl.signal;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
response = await fetch(providerUrl, {
|
|
487
|
-
...init
|
|
488
|
-
});
|
|
768
|
+
response = await executeHttpProviderRequest(providerBody);
|
|
489
769
|
} catch (error) {
|
|
490
770
|
return {
|
|
491
771
|
ok: false,
|
|
@@ -500,20 +780,56 @@ export async function makeProviderCall({
|
|
|
500
780
|
}
|
|
501
781
|
}, 503)
|
|
502
782
|
};
|
|
503
|
-
} finally {
|
|
504
|
-
cleanupTimeout();
|
|
505
783
|
}
|
|
506
784
|
|
|
507
785
|
if (!response.ok) {
|
|
786
|
+
const retriedOpenAIHostedWebSearch = await maybeRetryOpenAIHostedWebSearchProviderRequest({
|
|
787
|
+
response,
|
|
788
|
+
executeProviderRequest: executeHttpProviderRequest,
|
|
789
|
+
providerBody,
|
|
790
|
+
targetFormat,
|
|
791
|
+
requestKind
|
|
792
|
+
});
|
|
793
|
+
response = retriedOpenAIHostedWebSearch.response;
|
|
794
|
+
providerBody = retriedOpenAIHostedWebSearch.providerBody;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
if (!response.ok) {
|
|
798
|
+
const hostedWebSearchErrorKind = await resolveHostedWebSearchErrorKind(response, providerBody, {
|
|
799
|
+
targetFormat,
|
|
800
|
+
requestKind
|
|
801
|
+
});
|
|
508
802
|
return {
|
|
509
803
|
ok: false,
|
|
510
804
|
status: response.status,
|
|
511
805
|
retryable: shouldRetryStatus(response.status),
|
|
806
|
+
...(hostedWebSearchErrorKind ? { errorKind: hostedWebSearchErrorKind } : {}),
|
|
512
807
|
upstreamResponse: response,
|
|
513
808
|
translateError: translate
|
|
514
809
|
};
|
|
515
810
|
}
|
|
516
811
|
|
|
812
|
+
if (interceptAmpWebSearch) {
|
|
813
|
+
const intercepted = await maybeInterceptAmpWebSearch({
|
|
814
|
+
response,
|
|
815
|
+
providerBody,
|
|
816
|
+
targetFormat,
|
|
817
|
+
requestKind,
|
|
818
|
+
stream,
|
|
819
|
+
runtimeConfig,
|
|
820
|
+
env,
|
|
821
|
+
stateStore,
|
|
822
|
+
executeProviderRequest: async (followUpBody) => {
|
|
823
|
+
try {
|
|
824
|
+
return await executeHttpProviderRequest(followUpBody);
|
|
825
|
+
} catch {
|
|
826
|
+
return null;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
response = intercepted.response;
|
|
831
|
+
}
|
|
832
|
+
|
|
517
833
|
return adaptProviderResponse({
|
|
518
834
|
response,
|
|
519
835
|
stream,
|
|
@@ -523,6 +839,7 @@ export async function makeProviderCall({
|
|
|
523
839
|
fallbackModel: candidate.backend,
|
|
524
840
|
requestKind,
|
|
525
841
|
requestBody: body,
|
|
526
|
-
clientType
|
|
842
|
+
clientType,
|
|
843
|
+
env
|
|
527
844
|
});
|
|
528
845
|
}
|
|
@@ -889,6 +889,16 @@ export function normalizeClaudePassthroughStream(response) {
|
|
|
889
889
|
state.messageStopped = true;
|
|
890
890
|
}
|
|
891
891
|
|
|
892
|
+
function beginNextClaudeMessage() {
|
|
893
|
+
state.messageStarted = false;
|
|
894
|
+
state.messageStopped = false;
|
|
895
|
+
state.terminalDeltaSeen = false;
|
|
896
|
+
state.hasToolUse = false;
|
|
897
|
+
state.stopReason = null;
|
|
898
|
+
state.stopSequence = undefined;
|
|
899
|
+
state.usage = undefined;
|
|
900
|
+
}
|
|
901
|
+
|
|
892
902
|
function processBlock(block, controller) {
|
|
893
903
|
if (!block || !block.trim()) return;
|
|
894
904
|
const parsedBlock = parseSseBlock(block);
|
|
@@ -913,6 +923,10 @@ export function normalizeClaudePassthroughStream(response) {
|
|
|
913
923
|
|
|
914
924
|
const eventType = String(payload?.type || parsedBlock.eventType || "").trim();
|
|
915
925
|
if (eventType === "message_start") {
|
|
926
|
+
if (state.messageStarted && !state.messageStopped) {
|
|
927
|
+
finalizeClaudeMessage(controller);
|
|
928
|
+
beginNextClaudeMessage();
|
|
929
|
+
}
|
|
916
930
|
state.messageStarted = true;
|
|
917
931
|
mergeClaudeUsage(state, payload.message?.usage);
|
|
918
932
|
enqueueRawBlock(controller, block);
|