@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.
Files changed (39) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +163 -426
  3. package/package.json +3 -3
  4. package/src/cli/router-module.js +2773 -2587
  5. package/src/cli-entry.js +32 -103
  6. package/src/node/activity-log.js +119 -0
  7. package/src/node/coding-tool-config.js +85 -11
  8. package/src/node/config-workflows.js +51 -12
  9. package/src/node/instance-state.js +1 -1
  10. package/src/node/litellm-context-catalog.js +184 -0
  11. package/src/node/local-server.js +23 -3
  12. package/src/node/port-reclaim.js +2 -2
  13. package/src/node/start-command.js +22 -22
  14. package/src/node/startup-manager.js +3 -3
  15. package/src/node/web-command.js +1 -1
  16. package/src/node/web-console-assets.js +1 -1
  17. package/src/node/web-console-client.js +34 -29
  18. package/src/node/web-console-server.js +420 -38
  19. package/src/node/web-console-styles.generated.js +1 -1
  20. package/src/node/web-console-ui/buffered-text-input.js +133 -0
  21. package/src/node/web-console-ui/config-editor-utils.js +57 -4
  22. package/src/node/web-console-ui/dropdown-placement.js +153 -0
  23. package/src/node/web-console-ui/select-search-utils.js +6 -0
  24. package/src/node/web-console-ui/transient-integer-input-utils.js +12 -0
  25. package/src/runtime/balancer.js +78 -1
  26. package/src/runtime/codex-request-transformer.js +16 -7
  27. package/src/runtime/config.js +448 -12
  28. package/src/runtime/handler/amp-response.js +5 -3
  29. package/src/runtime/handler/amp-web-search.js +2232 -0
  30. package/src/runtime/handler/fallback.js +30 -2
  31. package/src/runtime/handler/provider-call.js +353 -36
  32. package/src/runtime/handler/provider-translation.js +14 -0
  33. package/src/runtime/handler/request.js +128 -2
  34. package/src/runtime/handler/route-debug.js +36 -0
  35. package/src/runtime/handler.js +210 -20
  36. package/src/runtime/subscription-provider.js +1 -1
  37. package/src/shared/coding-tool-bindings.js +49 -0
  38. package/src/shared/local-router-defaults.js +62 -0
  39. 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
- if (!(result?.upstreamResponse instanceof Response)) return "";
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 result.upstreamResponse.clone().text();
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 subscriptionResult = await makeSubscriptionProviderCall({
533
+ const executeSubscriptionRequest = async (requestBody) => makeSubscriptionProviderCall({
283
534
  provider,
284
- body: providerBody,
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: subscriptionResult.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(subscriptionResult.response, {
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(subscriptionResult.response);
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(subscriptionResult.response, {
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(subscriptionResult.response);
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
- const timeoutMs = resolveUpstreamTimeoutMs(env);
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);