@jsonstudio/rcc 0.89.333 → 0.89.524

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 (165) hide show
  1. package/dist/build-info.js +3 -3
  2. package/dist/build-info.js.map +1 -1
  3. package/dist/cli.js +62 -0
  4. package/dist/cli.js.map +1 -1
  5. package/dist/commands/token-daemon.d.ts +2 -0
  6. package/dist/commands/token-daemon.js +183 -0
  7. package/dist/commands/token-daemon.js.map +1 -0
  8. package/dist/index.js +4 -3
  9. package/dist/index.js.map +1 -1
  10. package/dist/modules/llmswitch/bridge.d.ts +1 -1
  11. package/dist/modules/llmswitch/bridge.js +3 -2
  12. package/dist/modules/llmswitch/bridge.js.map +1 -1
  13. package/dist/modules/pipeline/utils/colored-logger.js +3 -1
  14. package/dist/modules/pipeline/utils/colored-logger.js.map +1 -1
  15. package/dist/providers/auth/gemini-cli-userinfo-helper.js +12 -2
  16. package/dist/providers/auth/gemini-cli-userinfo-helper.js.map +1 -1
  17. package/dist/providers/auth/oauth-lifecycle.js +261 -22
  18. package/dist/providers/auth/oauth-lifecycle.js.map +1 -1
  19. package/dist/providers/core/config/oauth-flows.d.ts +23 -0
  20. package/dist/providers/core/config/oauth-flows.js +92 -5
  21. package/dist/providers/core/config/oauth-flows.js.map +1 -1
  22. package/dist/providers/core/config/provider-oauth-configs.js +9 -3
  23. package/dist/providers/core/config/provider-oauth-configs.js.map +1 -1
  24. package/dist/providers/core/config/service-profiles.js +18 -10
  25. package/dist/providers/core/config/service-profiles.js.map +1 -1
  26. package/dist/providers/core/runtime/gemini-cli-http-provider.js +87 -20
  27. package/dist/providers/core/runtime/gemini-cli-http-provider.js.map +1 -1
  28. package/dist/providers/core/runtime/http-request-executor.d.ts +1 -0
  29. package/dist/providers/core/runtime/http-request-executor.js +41 -1
  30. package/dist/providers/core/runtime/http-request-executor.js.map +1 -1
  31. package/dist/providers/core/runtime/http-transport-provider.d.ts +2 -0
  32. package/dist/providers/core/runtime/http-transport-provider.js +37 -2
  33. package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
  34. package/dist/providers/core/runtime/responses-provider.js +8 -3
  35. package/dist/providers/core/runtime/responses-provider.js.map +1 -1
  36. package/dist/providers/core/runtime/vision-debug-utils.d.ts +13 -0
  37. package/dist/providers/core/runtime/vision-debug-utils.js +114 -0
  38. package/dist/providers/core/runtime/vision-debug-utils.js.map +1 -0
  39. package/dist/providers/core/strategies/oauth-auth-code-flow.js +75 -26
  40. package/dist/providers/core/strategies/oauth-auth-code-flow.js.map +1 -1
  41. package/dist/providers/core/utils/http-client.js +2 -1
  42. package/dist/providers/core/utils/http-client.js.map +1 -1
  43. package/dist/providers/core/utils/snapshot-writer.d.ts +1 -1
  44. package/dist/providers/core/utils/snapshot-writer.js.map +1 -1
  45. package/dist/server/handlers/sse-dispatcher.js +22 -2
  46. package/dist/server/handlers/sse-dispatcher.js.map +1 -1
  47. package/dist/server/runtime/http-server/index.d.ts +9 -0
  48. package/dist/server/runtime/http-server/index.js +512 -144
  49. package/dist/server/runtime/http-server/index.js.map +1 -1
  50. package/dist/server/runtime/http-server/request-executor.d.ts +10 -0
  51. package/dist/server/runtime/http-server/request-executor.js +553 -159
  52. package/dist/server/runtime/http-server/request-executor.js.map +1 -1
  53. package/dist/server/runtime/http-server/routes.d.ts +5 -0
  54. package/dist/server/runtime/http-server/routes.js +69 -0
  55. package/dist/server/runtime/http-server/routes.js.map +1 -1
  56. package/dist/server/runtime/http-server/runtime-manager.js +18 -0
  57. package/dist/server/runtime/http-server/runtime-manager.js.map +1 -1
  58. package/dist/server/utils/utf8-chunk-buffer.d.ts +43 -0
  59. package/dist/server/utils/utf8-chunk-buffer.js +132 -0
  60. package/dist/server/utils/utf8-chunk-buffer.js.map +1 -0
  61. package/dist/token-daemon/index.d.ts +7 -0
  62. package/dist/token-daemon/index.js +242 -0
  63. package/dist/token-daemon/index.js.map +1 -0
  64. package/dist/token-daemon/server-utils.d.ts +33 -0
  65. package/dist/token-daemon/server-utils.js +155 -0
  66. package/dist/token-daemon/server-utils.js.map +1 -0
  67. package/dist/token-daemon/token-daemon.d.ts +20 -0
  68. package/dist/token-daemon/token-daemon.js +144 -0
  69. package/dist/token-daemon/token-daemon.js.map +1 -0
  70. package/dist/token-daemon/token-types.d.ts +44 -0
  71. package/dist/token-daemon/token-types.js +18 -0
  72. package/dist/token-daemon/token-types.js.map +1 -0
  73. package/dist/token-daemon/token-utils.d.ts +17 -0
  74. package/dist/token-daemon/token-utils.js +153 -0
  75. package/dist/token-daemon/token-utils.js.map +1 -0
  76. package/dist/tools/semantic-replay.js +7 -6
  77. package/dist/tools/semantic-replay.js.map +1 -1
  78. package/dist/utils/error-handler-registry.d.ts +36 -0
  79. package/dist/utils/error-handler-registry.js +93 -7
  80. package/dist/utils/error-handler-registry.js.map +1 -1
  81. package/node_modules/@jsonstudio/llms/README.md +2 -0
  82. package/node_modules/@jsonstudio/llms/dist/conversion/codecs/gemini-openai-codec.js +137 -5
  83. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/gemini-web-search.d.ts +17 -0
  84. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/gemini-web-search.js +68 -0
  85. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/glm-image-content.d.ts +2 -0
  86. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/glm-image-content.js +83 -0
  87. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/glm-vision-prompt.d.ts +11 -0
  88. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/glm-vision-prompt.js +177 -0
  89. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/glm-web-search.d.ts +2 -0
  90. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/glm-web-search.js +63 -0
  91. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/universal-shape-filter.js +11 -0
  92. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-gemini.json +17 -0
  93. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-glm.json +190 -181
  94. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-iflow.json +195 -195
  95. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-lmstudio.json +43 -43
  96. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-qwen.json +20 -20
  97. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/responses-c4m.json +42 -42
  98. package/node_modules/@jsonstudio/llms/dist/conversion/config/sample-config.json +1 -1
  99. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +24 -0
  100. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/compat/compat-types.d.ts +8 -0
  101. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/hub-pipeline.js +39 -4
  102. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/target-utils.js +6 -0
  103. package/node_modules/@jsonstudio/llms/dist/conversion/hub/process/chat-process.js +213 -1
  104. package/node_modules/@jsonstudio/llms/dist/conversion/hub/response/provider-response.d.ts +34 -0
  105. package/node_modules/@jsonstudio/llms/dist/conversion/hub/response/provider-response.js +84 -24
  106. package/node_modules/@jsonstudio/llms/dist/conversion/hub/response/server-side-tools.d.ts +26 -0
  107. package/node_modules/@jsonstudio/llms/dist/conversion/hub/response/server-side-tools.js +383 -0
  108. package/node_modules/@jsonstudio/llms/dist/conversion/hub/semantic-mappers/gemini-mapper.js +241 -14
  109. package/node_modules/@jsonstudio/llms/dist/conversion/hub/semantic-mappers/responses-mapper.js +17 -1
  110. package/node_modules/@jsonstudio/llms/dist/conversion/hub/standardized-bridge.js +14 -0
  111. package/node_modules/@jsonstudio/llms/dist/conversion/hub/types/standardized.d.ts +1 -0
  112. package/node_modules/@jsonstudio/llms/dist/conversion/responses/responses-openai-bridge.js +82 -3
  113. package/node_modules/@jsonstudio/llms/dist/conversion/shared/anthropic-message-utils.js +92 -3
  114. package/node_modules/@jsonstudio/llms/dist/conversion/shared/bridge-message-utils.js +137 -10
  115. package/node_modules/@jsonstudio/llms/dist/conversion/shared/responses-output-builder.js +43 -2
  116. package/node_modules/@jsonstudio/llms/dist/conversion/shared/snapshot-utils.js +17 -47
  117. package/node_modules/@jsonstudio/llms/dist/conversion/shared/tool-filter-pipeline.js +1 -0
  118. package/node_modules/@jsonstudio/llms/dist/conversion/shared/tool-mapping.js +25 -2
  119. package/node_modules/@jsonstudio/llms/dist/index.d.ts +1 -0
  120. package/node_modules/@jsonstudio/llms/dist/index.js +1 -0
  121. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/bootstrap.js +308 -43
  122. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/classifier.js +11 -17
  123. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/context-advisor.d.ts +0 -2
  124. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/context-advisor.js +0 -12
  125. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/engine.d.ts +17 -2
  126. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/engine.js +332 -95
  127. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/features.js +1 -1
  128. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/message-utils.js +36 -24
  129. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/provider-registry.js +2 -1
  130. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/token-counter.js +14 -3
  131. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/types.d.ts +66 -2
  132. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/types.js +2 -1
  133. package/node_modules/@jsonstudio/llms/dist/servertool/engine.d.ts +27 -0
  134. package/node_modules/@jsonstudio/llms/dist/servertool/engine.js +60 -0
  135. package/node_modules/@jsonstudio/llms/dist/servertool/flow-types.d.ts +40 -0
  136. package/node_modules/@jsonstudio/llms/dist/servertool/flow-types.js +1 -0
  137. package/node_modules/@jsonstudio/llms/dist/servertool/handlers/vision.d.ts +1 -0
  138. package/node_modules/@jsonstudio/llms/dist/servertool/handlers/vision.js +194 -0
  139. package/node_modules/@jsonstudio/llms/dist/servertool/handlers/web-search.d.ts +1 -0
  140. package/node_modules/@jsonstudio/llms/dist/servertool/handlers/web-search.js +638 -0
  141. package/node_modules/@jsonstudio/llms/dist/servertool/orchestration-types.d.ts +33 -0
  142. package/node_modules/@jsonstudio/llms/dist/servertool/orchestration-types.js +1 -0
  143. package/node_modules/@jsonstudio/llms/dist/servertool/registry.d.ts +18 -0
  144. package/node_modules/@jsonstudio/llms/dist/servertool/registry.js +27 -0
  145. package/node_modules/@jsonstudio/llms/dist/servertool/server-side-tools.d.ts +8 -0
  146. package/node_modules/@jsonstudio/llms/dist/servertool/server-side-tools.js +208 -0
  147. package/node_modules/@jsonstudio/llms/dist/servertool/types.d.ts +88 -0
  148. package/node_modules/@jsonstudio/llms/dist/servertool/types.js +1 -0
  149. package/node_modules/@jsonstudio/llms/dist/servertool/vision-tool.d.ts +2 -0
  150. package/node_modules/@jsonstudio/llms/dist/servertool/vision-tool.js +185 -0
  151. package/node_modules/@jsonstudio/llms/dist/sse/json-to-sse/event-generators/responses.js +15 -3
  152. package/node_modules/@jsonstudio/llms/dist/sse/sse-to-json/builders/response-builder.js +6 -3
  153. package/node_modules/@jsonstudio/llms/dist/sse/sse-to-json/gemini-sse-to-json-converter.js +27 -1
  154. package/node_modules/@jsonstudio/llms/dist/sse/types/gemini-types.d.ts +20 -1
  155. package/node_modules/@jsonstudio/llms/dist/sse/types/responses-types.js +1 -1
  156. package/node_modules/@jsonstudio/llms/dist/telemetry/stats-center.d.ts +73 -0
  157. package/node_modules/@jsonstudio/llms/dist/telemetry/stats-center.js +280 -0
  158. package/node_modules/@jsonstudio/llms/package.json +1 -1
  159. package/package.json +2 -2
  160. package/scripts/pack-mode.mjs +2 -1
  161. package/scripts/publish-rcc.mjs +20 -4
  162. package/scripts/tests/virtual-router-health.mjs +141 -6
  163. package/dist/tools/replay-request.d.ts +0 -0
  164. package/dist/tools/replay-request.js +0 -2
  165. package/dist/tools/replay-request.js.map +0 -1
@@ -18,7 +18,7 @@ import { buildProviderProfiles } from '../../../providers/profile/provider-profi
18
18
  import { emitProviderError } from '../../../providers/core/utils/provider-error-reporter.js';
19
19
  import { isStageLoggingEnabled, logPipelineStage } from '../../utils/stage-logger.js';
20
20
  import { registerDefaultMiddleware } from './middleware.js';
21
- import { registerHttpRoutes } from './routes.js';
21
+ import { registerHttpRoutes, registerOAuthPortalRoute } from './routes.js';
22
22
  import { mapProviderProtocol, normalizeProviderType, resolveProviderIdentity, asRecord } from './provider-utils.js';
23
23
  import { resolveRepoRoot, loadLlmswitchModule } from './llmswitch-loader.js';
24
24
  import { importCoreModule } from '../../../modules/llmswitch/core-loader.js';
@@ -98,6 +98,11 @@ export class RouteCodexHttpServer {
98
98
  console.warn('[RouteCodexHttpServer] Failed to initialize PipelineDebugLogger; falling back to noop logger.', error);
99
99
  this.pipelineLogger = createNoopPipelineLogger();
100
100
  }
101
+ // Register critical routes early (before provider initialization)
102
+ // This ensures OAuth Portal is available when providers check token validity
103
+ registerDefaultMiddleware(this.app);
104
+ registerOAuthPortalRoute(this.app);
105
+ console.log('[RouteCodexHttpServer] OAuth Portal route registered (early initialization)');
101
106
  console.log('[RouteCodexHttpServer] Initialized (pipeline=hub)');
102
107
  }
103
108
  resolveVirtualRouterInput(userConfig) {
@@ -334,7 +339,8 @@ export class RouteCodexHttpServer {
334
339
  // 初始化错误处理
335
340
  await this.errorHandling.initialize();
336
341
  await this.initializeRouteErrorHub();
337
- registerDefaultMiddleware(this.app);
342
+ // registerDefaultMiddleware and registerOAuthPortalRoute already called in constructor
343
+ // Register remaining HTTP routes
338
344
  registerHttpRoutes({
339
345
  app: this.app,
340
346
  config: this.config,
@@ -624,14 +630,14 @@ export class RouteCodexHttpServer {
624
630
  if (!this.isPipelineReady()) {
625
631
  throw new Error('Hub pipeline runtime is not initialized');
626
632
  }
627
- const metadata = this.buildRequestMetadata(input);
633
+ const initialMetadata = this.buildRequestMetadata(input);
628
634
  const providerRequestId = input.requestId;
629
- const clientRequestId = typeof metadata.clientRequestId === 'string' && metadata.clientRequestId.trim()
630
- ? metadata.clientRequestId.trim()
635
+ const clientRequestId = typeof initialMetadata.clientRequestId === 'string' && initialMetadata.clientRequestId.trim()
636
+ ? initialMetadata.clientRequestId.trim()
631
637
  : providerRequestId;
632
638
  this.logStage('request.received', providerRequestId, {
633
639
  endpoint: input.entryEndpoint,
634
- stream: input.metadata?.stream === true
640
+ stream: initialMetadata.stream === true
635
641
  });
636
642
  try {
637
643
  const headerUa = (typeof input.headers?.['user-agent'] === 'string' && input.headers['user-agent']) ||
@@ -644,7 +650,7 @@ export class RouteCodexHttpServer {
644
650
  headers: asRecord(input.headers),
645
651
  body: input.body,
646
652
  metadata: {
647
- ...metadata,
653
+ ...initialMetadata,
648
654
  userAgent: headerUa,
649
655
  clientOriginator: headerOriginator
650
656
  }
@@ -654,155 +660,161 @@ export class RouteCodexHttpServer {
654
660
  // snapshot failure should not block request path
655
661
  }
656
662
  const pipelineLabel = 'hub';
657
- this.logStage(`${pipelineLabel}.start`, providerRequestId, {
658
- endpoint: input.entryEndpoint,
659
- stream: metadata.stream
660
- });
661
- const originalRequestSnapshot = this.cloneRequestPayload(input.body);
662
- const pipelineResult = await this.runHubPipeline(input, metadata);
663
- const pipelineMetadata = pipelineResult.metadata ?? {};
664
- const mergedMetadata = { ...metadata, ...pipelineMetadata };
665
- this.logStage(`${pipelineLabel}.completed`, providerRequestId, {
666
- route: pipelineResult.routingDecision?.routeName,
667
- target: pipelineResult.target?.providerKey
668
- });
669
- const providerPayload = pipelineResult.providerPayload;
670
- const target = pipelineResult.target;
671
- if (!providerPayload || !target?.providerKey) {
672
- throw Object.assign(new Error('Virtual router did not produce a provider target'), {
673
- code: 'ERR_NO_PROVIDER_TARGET',
674
- requestId: input.requestId
675
- });
676
- }
677
- const runtimeKey = target.runtimeKey || this.providerKeyToRuntimeKey.get(target.providerKey);
678
- if (!runtimeKey) {
679
- throw Object.assign(new Error(`Runtime for provider ${target.providerKey} not initialized`), {
680
- code: 'ERR_RUNTIME_NOT_FOUND',
681
- requestId: input.requestId
682
- });
683
- }
684
- const handle = this.providerHandles.get(runtimeKey);
685
- if (!handle) {
686
- throw Object.assign(new Error(`Provider runtime ${runtimeKey} not found`), {
687
- code: 'ERR_PROVIDER_NOT_FOUND',
688
- requestId: input.requestId
663
+ let iterationMetadata = initialMetadata;
664
+ let followupTriggered = false;
665
+ while (true) {
666
+ this.logStage(`${pipelineLabel}.start`, providerRequestId, {
667
+ endpoint: input.entryEndpoint,
668
+ stream: iterationMetadata.stream
689
669
  });
690
- }
691
- const providerProtocol = target.outboundProfile ||
692
- handle.providerProtocol;
693
- const metadataModel = mergedMetadata?.target && typeof mergedMetadata.target === 'object'
694
- ? mergedMetadata.target.clientModelId
695
- : undefined;
696
- const rawModel = this.extractProviderModel(providerPayload) ||
697
- (typeof metadataModel === 'string' ? metadataModel : undefined);
698
- const providerIdToken = target.providerKey || handle.providerId || runtimeKey;
699
- if (!providerIdToken) {
700
- throw Object.assign(new Error('Provider identifier missing for request'), {
701
- code: 'ERR_PROVIDER_ID_MISSING',
702
- requestId: providerRequestId
670
+ const originalRequestSnapshot = this.cloneRequestPayload(input.body);
671
+ const pipelineResult = await this.runHubPipeline(input, iterationMetadata);
672
+ const pipelineMetadata = pipelineResult.metadata ?? {};
673
+ const mergedMetadata = { ...iterationMetadata, ...pipelineMetadata };
674
+ this.logStage(`${pipelineLabel}.completed`, providerRequestId, {
675
+ route: pipelineResult.routingDecision?.routeName,
676
+ target: pipelineResult.target?.providerKey
703
677
  });
704
- }
705
- const enhancedRequestId = enhanceProviderRequestId(providerRequestId, {
706
- entryEndpoint: input.entryEndpoint,
707
- providerId: providerIdToken,
708
- model: rawModel
709
- });
710
- if (providerProtocol === 'openai-responses') {
711
- try {
712
- await rebindResponsesConversationRequestId(pipelineResult.requestId, enhancedRequestId);
678
+ const providerPayload = pipelineResult.providerPayload;
679
+ const target = pipelineResult.target;
680
+ if (!providerPayload || !target?.providerKey) {
681
+ throw Object.assign(new Error('Virtual router did not produce a provider target'), {
682
+ code: 'ERR_NO_PROVIDER_TARGET',
683
+ requestId: input.requestId
684
+ });
713
685
  }
714
- catch {
715
- /* ignore rebind failures */
686
+ const runtimeKey = target.runtimeKey || this.providerKeyToRuntimeKey.get(target.providerKey);
687
+ if (!runtimeKey) {
688
+ throw Object.assign(new Error(`Runtime for provider ${target.providerKey} not initialized`), {
689
+ code: 'ERR_RUNTIME_NOT_FOUND',
690
+ requestId: input.requestId
691
+ });
716
692
  }
717
- }
718
- if (enhancedRequestId !== input.requestId) {
719
- input.requestId = enhancedRequestId;
720
- }
721
- mergedMetadata.clientRequestId = clientRequestId;
722
- const providerModel = rawModel;
723
- const providerLabel = this.buildProviderLabel(target.providerKey, providerModel);
724
- this.logStage('provider.prepare', input.requestId, {
725
- providerKey: target.providerKey,
726
- runtimeKey,
727
- protocol: providerProtocol,
728
- providerType: handle.providerType,
729
- providerFamily: handle.providerFamily,
730
- model: providerModel,
731
- providerLabel
732
- });
733
- attachProviderRuntimeMetadata(providerPayload, {
734
- requestId: input.requestId,
735
- providerId: handle.providerId,
736
- providerKey: target.providerKey,
737
- providerType: handle.providerType,
738
- providerFamily: handle.providerFamily,
739
- providerProtocol,
740
- pipelineId: target.providerKey,
741
- routeName: pipelineResult.routingDecision?.routeName,
742
- runtimeKey,
743
- target,
744
- metadata: mergedMetadata
745
- });
746
- this.logStage('provider.send.start', input.requestId, {
747
- providerKey: target.providerKey,
748
- runtimeKey,
749
- protocol: providerProtocol,
750
- providerType: handle.providerType,
751
- providerFamily: handle.providerFamily,
752
- model: providerModel,
753
- providerLabel
754
- });
755
- try {
756
- const providerResponse = await handle.instance.processIncoming(providerPayload);
757
- const responseStatus = this.extractResponseStatus(providerResponse);
758
- this.logStage('provider.send.completed', input.requestId, {
759
- providerKey: target.providerKey,
760
- status: responseStatus,
761
- providerType: handle.providerType,
762
- providerFamily: handle.providerFamily,
763
- model: providerModel,
764
- providerLabel
765
- });
766
- const normalized = this.normalizeProviderResponse(providerResponse);
767
- return await this.convertProviderResponseIfNeeded({
693
+ const handle = this.providerHandles.get(runtimeKey);
694
+ if (!handle) {
695
+ throw Object.assign(new Error(`Provider runtime ${runtimeKey} not found`), {
696
+ code: 'ERR_PROVIDER_NOT_FOUND',
697
+ requestId: input.requestId
698
+ });
699
+ }
700
+ const providerProtocol = target.outboundProfile ||
701
+ handle.providerProtocol;
702
+ const metadataModel = mergedMetadata?.target && typeof mergedMetadata.target === 'object'
703
+ ? mergedMetadata.target.clientModelId
704
+ : undefined;
705
+ const rawModel = this.extractProviderModel(providerPayload) ||
706
+ (typeof metadataModel === 'string' ? metadataModel : undefined);
707
+ const providerIdToken = target.providerKey || handle.providerId || runtimeKey;
708
+ if (!providerIdToken) {
709
+ throw Object.assign(new Error('Provider identifier missing for request'), {
710
+ code: 'ERR_PROVIDER_ID_MISSING',
711
+ requestId: providerRequestId
712
+ });
713
+ }
714
+ const enhancedRequestId = enhanceProviderRequestId(providerRequestId, {
768
715
  entryEndpoint: input.entryEndpoint,
769
- providerType: handle.providerType,
770
- requestId: input.requestId,
771
- wantsStream: Boolean(input.metadata?.inboundStream ?? input.metadata?.stream),
772
- originalRequest: originalRequestSnapshot,
773
- processMode: pipelineResult.processMode,
774
- response: normalized,
775
- pipelineMetadata: mergedMetadata
716
+ providerId: providerIdToken,
717
+ model: rawModel
776
718
  });
777
- }
778
- catch (error) {
779
- this.logStage('provider.send.error', input.requestId, {
719
+ if (providerProtocol === 'openai-responses') {
720
+ try {
721
+ await rebindResponsesConversationRequestId(pipelineResult.requestId, enhancedRequestId);
722
+ }
723
+ catch {
724
+ /* ignore rebind failures */
725
+ }
726
+ }
727
+ if (enhancedRequestId !== input.requestId) {
728
+ input.requestId = enhancedRequestId;
729
+ }
730
+ mergedMetadata.clientRequestId = clientRequestId;
731
+ const providerModel = rawModel;
732
+ const providerLabel = this.buildProviderLabel(target.providerKey, providerModel);
733
+ this.logStage('provider.prepare', input.requestId, {
780
734
  providerKey: target.providerKey,
781
- message: error instanceof Error ? error.message : String(error ?? 'Unknown error'),
735
+ runtimeKey,
736
+ protocol: providerProtocol,
782
737
  providerType: handle.providerType,
783
738
  providerFamily: handle.providerFamily,
784
739
  model: providerModel,
785
740
  providerLabel
786
741
  });
787
- const runtimeMetadata = {
742
+ attachProviderRuntimeMetadata(providerPayload, {
788
743
  requestId: input.requestId,
789
- providerKey: target.providerKey,
790
744
  providerId: handle.providerId,
745
+ providerKey: target.providerKey,
791
746
  providerType: handle.providerType,
747
+ providerFamily: handle.providerFamily,
792
748
  providerProtocol,
793
- routeName: pipelineResult.routingDecision?.routeName,
794
749
  pipelineId: target.providerKey,
750
+ routeName: pipelineResult.routingDecision?.routeName,
795
751
  runtimeKey,
796
- target
797
- };
798
- runtimeMetadata.providerFamily = handle.providerFamily;
799
- emitProviderError({
800
- error,
801
- stage: 'provider.send',
802
- runtime: runtimeMetadata,
803
- dependencies: this.getModuleDependencies()
752
+ target,
753
+ metadata: mergedMetadata
804
754
  });
805
- throw error;
755
+ this.logStage('provider.send.start', input.requestId, {
756
+ providerKey: target.providerKey,
757
+ runtimeKey,
758
+ protocol: providerProtocol,
759
+ providerType: handle.providerType,
760
+ providerFamily: handle.providerFamily,
761
+ model: providerModel,
762
+ providerLabel
763
+ });
764
+ try {
765
+ const providerResponse = await handle.instance.processIncoming(providerPayload);
766
+ const responseStatus = this.extractResponseStatus(providerResponse);
767
+ this.logStage('provider.send.completed', input.requestId, {
768
+ providerKey: target.providerKey,
769
+ status: responseStatus,
770
+ providerType: handle.providerType,
771
+ providerFamily: handle.providerFamily,
772
+ model: providerModel,
773
+ providerLabel
774
+ });
775
+ const wantsStreamBase = Boolean(input.metadata?.inboundStream ?? input.metadata?.stream);
776
+ const normalized = this.normalizeProviderResponse(providerResponse);
777
+ const converted = await this.convertProviderResponseIfNeeded({
778
+ entryEndpoint: input.entryEndpoint,
779
+ providerType: handle.providerType,
780
+ requestId: input.requestId,
781
+ wantsStream: wantsStreamBase,
782
+ originalRequest: originalRequestSnapshot,
783
+ processMode: pipelineResult.processMode,
784
+ response: normalized,
785
+ pipelineMetadata: mergedMetadata
786
+ });
787
+ return converted;
788
+ }
789
+ catch (error) {
790
+ this.logStage('provider.send.error', input.requestId, {
791
+ providerKey: target.providerKey,
792
+ message: error instanceof Error ? error.message : String(error ?? 'Unknown error'),
793
+ providerType: handle.providerType,
794
+ providerFamily: handle.providerFamily,
795
+ model: providerModel,
796
+ providerLabel
797
+ });
798
+ const runtimeMetadata = {
799
+ requestId: input.requestId,
800
+ providerKey: target.providerKey,
801
+ providerId: handle.providerId,
802
+ providerType: handle.providerType,
803
+ providerProtocol,
804
+ routeName: pipelineResult.routingDecision?.routeName,
805
+ pipelineId: target.providerKey,
806
+ runtimeKey,
807
+ target
808
+ };
809
+ runtimeMetadata.providerFamily = handle.providerFamily;
810
+ emitProviderError({
811
+ error,
812
+ stage: 'provider.send',
813
+ runtime: runtimeMetadata,
814
+ dependencies: this.getModuleDependencies()
815
+ });
816
+ throw error;
817
+ }
806
818
  }
807
819
  }
808
820
  async runHubPipeline(input, metadata) {
@@ -940,12 +952,28 @@ export class RouteCodexHttpServer {
940
952
  const metadataBag = asRecord(options.pipelineMetadata);
941
953
  const aliasMap = extractAnthropicToolAliasMap(metadataBag);
942
954
  const originalModelId = this.extractClientModelId(metadataBag, options.originalRequest);
943
- const adapterContext = {
944
- requestId: options.requestId,
945
- entryEndpoint: options.entryEndpoint || entry,
946
- providerProtocol,
947
- originalModelId
955
+ // HubPipeline metadata 为基础构建 AdapterContext,确保诸如
956
+ // capturedChatRequest / webSearch / routeHint 等字段在响应侧可见,
957
+ // 便于 llmswitch-core 内部实现 servertool/web_search 的第三跳。
958
+ const baseContext = {
959
+ ...(metadataBag ?? {})
948
960
  };
961
+ // 将 HubPipeline metadata.routeName 映射为 AdapterContext.routeId,
962
+ // 便于 llmswitch-core 在第三跳中使用 routeHint 复用首次路由决策。
963
+ if (typeof metadataBag?.routeName === 'string') {
964
+ baseContext.routeId = metadataBag.routeName;
965
+ }
966
+ baseContext.requestId = options.requestId;
967
+ baseContext.entryEndpoint = options.entryEndpoint || entry;
968
+ baseContext.providerProtocol = providerProtocol;
969
+ baseContext.originalModelId = originalModelId;
970
+ const adapterContext = baseContext;
971
+ // 将 serverToolFollowup 等标记从 pipelineMetadata 透传到 AdapterContext,
972
+ // 便于 convertProviderResponse 正确识别内部二跳请求并跳过 servertool。
973
+ if (metadataBag && Object.prototype.hasOwnProperty.call(metadataBag, 'serverToolFollowup')) {
974
+ adapterContext.serverToolFollowup = metadataBag
975
+ .serverToolFollowup;
976
+ }
949
977
  const compatProfile = metadataBag &&
950
978
  typeof metadataBag === 'object' &&
951
979
  metadataBag.target &&
@@ -959,18 +987,85 @@ export class RouteCodexHttpServer {
959
987
  if (aliasMap) {
960
988
  adapterContext.anthropicToolNameMap = aliasMap;
961
989
  }
990
+ if (metadataBag && typeof metadataBag === 'object') {
991
+ const webSearchConfig = metadataBag.webSearch;
992
+ if (webSearchConfig && typeof webSearchConfig === 'object') {
993
+ adapterContext.webSearch = webSearchConfig;
994
+ }
995
+ if (metadataBag.forceWebSearch === true) {
996
+ adapterContext.forceWebSearch = true;
997
+ }
998
+ if (metadataBag.forceVision === true) {
999
+ adapterContext.forceVision = true;
1000
+ }
1001
+ }
962
1002
  const [convertProviderResponse, createSnapshotRecorder] = await Promise.all([
963
1003
  loadConvertProviderResponse(),
964
1004
  loadSnapshotRecorderFactory()
965
1005
  ]);
966
- const stageRecorder = createSnapshotRecorder(adapterContext, adapterContext.entryEndpoint);
1006
+ const stageRecorder = createSnapshotRecorder(adapterContext, typeof adapterContext.entryEndpoint === 'string'
1007
+ ? adapterContext.entryEndpoint
1008
+ : options.entryEndpoint || entry);
1009
+ const providerInvoker = async (invokeOptions) => {
1010
+ const runtimeKey = this.providerKeyToRuntimeKey.get(invokeOptions.providerKey) || invokeOptions.providerKey;
1011
+ const handle = this.providerHandles.get(runtimeKey);
1012
+ if (!handle) {
1013
+ throw new Error(`Provider runtime ${runtimeKey} not found`);
1014
+ }
1015
+ const providerResponse = await handle.instance.processIncoming(invokeOptions.payload);
1016
+ const normalized = this.normalizeProviderResponse(providerResponse);
1017
+ const bodyPayload = normalized.body && typeof normalized.body === 'object'
1018
+ ? normalized.body
1019
+ : normalized;
1020
+ return { providerResponse: bodyPayload };
1021
+ };
1022
+ const reenterPipeline = async (reenterOpts) => {
1023
+ const nestedEntry = reenterOpts.entryEndpoint || options.entryEndpoint || entry;
1024
+ const nestedExtra = asRecord(reenterOpts.metadata) ?? {};
1025
+ const nestedEntryLower = nestedEntry.toLowerCase();
1026
+ // 基于首次 HubPipeline metadata + 调用方注入的 metadata 构建新的请求 metadata。
1027
+ // 不在 Host 层编码 servertool/web_search 等语义,由 llmswitch-core 负责。
1028
+ const nestedMetadata = {
1029
+ ...(metadataBag ?? {}),
1030
+ ...nestedExtra,
1031
+ entryEndpoint: nestedEntry,
1032
+ direction: 'request',
1033
+ stage: 'inbound'
1034
+ };
1035
+ // 针对 reenterPipeline 的入口端点,纠正 providerProtocol,避免沿用外层协议。
1036
+ if (nestedEntryLower.includes('/v1/chat/completions')) {
1037
+ nestedMetadata.providerProtocol = 'openai-chat';
1038
+ }
1039
+ else if (nestedEntryLower.includes('/v1/responses')) {
1040
+ nestedMetadata.providerProtocol = 'openai-responses';
1041
+ }
1042
+ else if (nestedEntryLower.includes('/v1/messages')) {
1043
+ nestedMetadata.providerProtocol = 'anthropic-messages';
1044
+ }
1045
+ const nestedInput = {
1046
+ entryEndpoint: nestedEntry,
1047
+ method: 'POST',
1048
+ requestId: reenterOpts.requestId,
1049
+ headers: {},
1050
+ query: {},
1051
+ body: reenterOpts.body,
1052
+ metadata: nestedMetadata
1053
+ };
1054
+ const nestedResult = await this.executePipeline(nestedInput);
1055
+ const nestedBody = nestedResult.body && typeof nestedResult.body === 'object'
1056
+ ? nestedResult.body
1057
+ : undefined;
1058
+ return { body: nestedBody };
1059
+ };
967
1060
  const converted = await convertProviderResponse({
968
1061
  providerProtocol,
969
1062
  providerResponse: body,
970
1063
  context: adapterContext,
971
1064
  entryEndpoint: options.entryEndpoint || entry,
972
1065
  wantsStream: options.wantsStream,
973
- stageRecorder
1066
+ providerInvoker,
1067
+ stageRecorder,
1068
+ reenterPipeline
974
1069
  });
975
1070
  if (converted.__sse_responses) {
976
1071
  return {
@@ -1020,6 +1115,279 @@ export class RouteCodexHttpServer {
1020
1115
  return undefined;
1021
1116
  }
1022
1117
  }
1118
+ buildVisionFollowupPayload(options) {
1119
+ const { originalPayload, visionResponse } = options;
1120
+ if (!originalPayload || typeof originalPayload !== 'object') {
1121
+ return null;
1122
+ }
1123
+ const clone = this.cloneRequestPayload(originalPayload) ?? { ...originalPayload };
1124
+ if (!clone) {
1125
+ return null;
1126
+ }
1127
+ const visionText = this.extractVisionDescription(visionResponse?.body);
1128
+ if (!visionText) {
1129
+ return null;
1130
+ }
1131
+ if (this.rewriteResponsesInput(clone, visionText)) {
1132
+ return clone;
1133
+ }
1134
+ if (this.rewriteChatMessages(clone, visionText)) {
1135
+ return clone;
1136
+ }
1137
+ return null;
1138
+ }
1139
+ rewriteResponsesInput(payload, visionText) {
1140
+ const inputList = payload.input;
1141
+ if (!Array.isArray(inputList)) {
1142
+ return false;
1143
+ }
1144
+ for (let i = inputList.length - 1; i >= 0; i -= 1) {
1145
+ const item = inputList[i];
1146
+ if (!item || typeof item !== 'object') {
1147
+ continue;
1148
+ }
1149
+ const role = typeof item.role === 'string'
1150
+ ? item.role
1151
+ : '';
1152
+ if (role !== 'user') {
1153
+ continue;
1154
+ }
1155
+ const contentBlocks = Array.isArray(item.content)
1156
+ ? [...item.content]
1157
+ : [];
1158
+ const originalText = this.extractTextFromContentBlocks(contentBlocks, ['input_text', 'text']);
1159
+ const textType = this.detectContentTextType(contentBlocks, 'input_text');
1160
+ const composed = this.composeVisionUserText(visionText, originalText);
1161
+ item.content = [
1162
+ {
1163
+ type: textType,
1164
+ text: composed
1165
+ }
1166
+ ];
1167
+ inputList[i] = item;
1168
+ return true;
1169
+ }
1170
+ return false;
1171
+ }
1172
+ rewriteChatMessages(payload, visionText) {
1173
+ const messages = payload.messages;
1174
+ if (!Array.isArray(messages)) {
1175
+ return false;
1176
+ }
1177
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
1178
+ const message = messages[i];
1179
+ if (!message || typeof message !== 'object') {
1180
+ continue;
1181
+ }
1182
+ const role = typeof message.role === 'string'
1183
+ ? message.role
1184
+ : '';
1185
+ if (role !== 'user') {
1186
+ continue;
1187
+ }
1188
+ const contentBlocks = Array.isArray(message.content)
1189
+ ? [...message.content]
1190
+ : [];
1191
+ const originalText = this.extractTextFromContentBlocks(contentBlocks, ['text']);
1192
+ const textType = this.detectContentTextType(contentBlocks, 'text');
1193
+ const composed = this.composeVisionUserText(visionText, originalText);
1194
+ message.content = [
1195
+ {
1196
+ type: textType,
1197
+ text: composed
1198
+ }
1199
+ ];
1200
+ messages[i] = message;
1201
+ return true;
1202
+ }
1203
+ return false;
1204
+ }
1205
+ extractTextFromContentBlocks(content, allowedTypes) {
1206
+ if (typeof content === 'string') {
1207
+ return content;
1208
+ }
1209
+ if (!Array.isArray(content)) {
1210
+ return '';
1211
+ }
1212
+ const collected = [];
1213
+ for (const block of content) {
1214
+ if (!block || typeof block !== 'object') {
1215
+ continue;
1216
+ }
1217
+ const typeValue = typeof block.type === 'string'
1218
+ ? block.type
1219
+ : '';
1220
+ if (allowedTypes.length && !allowedTypes.includes(typeValue)) {
1221
+ continue;
1222
+ }
1223
+ const textValue = block.text;
1224
+ if (typeof textValue === 'string' && textValue.trim()) {
1225
+ collected.push(textValue.trim());
1226
+ }
1227
+ }
1228
+ return collected.join('\n');
1229
+ }
1230
+ detectContentTextType(content, fallback) {
1231
+ if (Array.isArray(content)) {
1232
+ for (const block of content) {
1233
+ if (!block || typeof block !== 'object') {
1234
+ continue;
1235
+ }
1236
+ const typeValue = block.type;
1237
+ if (typeof typeValue === 'string' && (typeValue === 'text' || typeValue === 'input_text')) {
1238
+ return typeValue;
1239
+ }
1240
+ }
1241
+ }
1242
+ return fallback;
1243
+ }
1244
+ composeVisionUserText(visionText, originalText) {
1245
+ const sections = [];
1246
+ const cleanedVision = (visionText || '').trim();
1247
+ if (cleanedVision) {
1248
+ sections.push(`【图片分析】\n${cleanedVision}`);
1249
+ }
1250
+ const cleanedOriginal = (originalText || '').trim();
1251
+ if (cleanedOriginal) {
1252
+ sections.push(`【用户原始请求】\n${cleanedOriginal}`);
1253
+ }
1254
+ return sections.join('\n\n');
1255
+ }
1256
+ extractVisionDescription(body) {
1257
+ if (!body) {
1258
+ return null;
1259
+ }
1260
+ if (typeof body === 'string') {
1261
+ const trimmed = body.trim();
1262
+ return trimmed.length ? trimmed : null;
1263
+ }
1264
+ if (typeof body !== 'object') {
1265
+ return null;
1266
+ }
1267
+ const record = body;
1268
+ const direct = this.extractTextCandidate(record);
1269
+ if (direct) {
1270
+ return direct;
1271
+ }
1272
+ if (record.response && typeof record.response === 'object') {
1273
+ const responseNode = record.response;
1274
+ const nested = this.extractTextCandidate(responseNode);
1275
+ if (nested) {
1276
+ return nested;
1277
+ }
1278
+ const output = responseNode.output;
1279
+ if (Array.isArray(output)) {
1280
+ for (const entry of output) {
1281
+ if (entry && typeof entry === 'object') {
1282
+ const nestedText = this.extractTextCandidate(entry);
1283
+ if (nestedText) {
1284
+ return nestedText;
1285
+ }
1286
+ }
1287
+ }
1288
+ }
1289
+ }
1290
+ if (Array.isArray(record.output)) {
1291
+ for (const entry of record.output) {
1292
+ if (entry && typeof entry === 'object') {
1293
+ const nested = this.extractTextCandidate(entry);
1294
+ if (nested) {
1295
+ return nested;
1296
+ }
1297
+ }
1298
+ }
1299
+ }
1300
+ const choices = record.choices;
1301
+ if (Array.isArray(choices)) {
1302
+ for (const choice of choices) {
1303
+ if (!choice || typeof choice !== 'object') {
1304
+ continue;
1305
+ }
1306
+ const message = choice.message;
1307
+ if (message && typeof message === 'object') {
1308
+ const msg = message;
1309
+ const content = msg.content;
1310
+ if (typeof content === 'string' && content.trim()) {
1311
+ return content.trim();
1312
+ }
1313
+ if (Array.isArray(content)) {
1314
+ for (const part of content) {
1315
+ if (part && typeof part === 'object' && typeof part.text === 'string') {
1316
+ const textValue = part.text.trim();
1317
+ if (textValue) {
1318
+ return textValue;
1319
+ }
1320
+ }
1321
+ }
1322
+ }
1323
+ }
1324
+ }
1325
+ }
1326
+ return null;
1327
+ }
1328
+ extractTextCandidate(record) {
1329
+ const candidates = [
1330
+ { key: 'output_text', allowJson: true },
1331
+ { key: 'text' },
1332
+ { key: 'content' }
1333
+ ];
1334
+ for (const candidate of candidates) {
1335
+ if (!(candidate.key in record)) {
1336
+ continue;
1337
+ }
1338
+ const text = this.normalizeTextCandidateValue(record[candidate.key], candidate.allowJson === true);
1339
+ if (text) {
1340
+ return text;
1341
+ }
1342
+ }
1343
+ return null;
1344
+ }
1345
+ normalizeTextCandidateValue(value, allowJsonStringify = false) {
1346
+ if (!value) {
1347
+ return null;
1348
+ }
1349
+ if (typeof value === 'string') {
1350
+ const trimmed = value.trim();
1351
+ return trimmed.length ? trimmed : null;
1352
+ }
1353
+ if (Array.isArray(value)) {
1354
+ const collected = [];
1355
+ for (const entry of value) {
1356
+ const nested = this.normalizeTextCandidateValue(entry, allowJsonStringify);
1357
+ if (nested) {
1358
+ collected.push(nested);
1359
+ }
1360
+ }
1361
+ return collected.length ? collected.join('\n') : null;
1362
+ }
1363
+ if (typeof value === 'object') {
1364
+ const bag = value;
1365
+ const textField = bag.text;
1366
+ if (typeof textField === 'string' && textField.trim()) {
1367
+ return textField.trim();
1368
+ }
1369
+ const summaryField = bag.summary;
1370
+ if (typeof summaryField === 'string' && summaryField.trim()) {
1371
+ return summaryField.trim();
1372
+ }
1373
+ if ('content' in bag) {
1374
+ const nested = this.normalizeTextCandidateValue(bag.content, allowJsonStringify);
1375
+ if (nested) {
1376
+ return nested;
1377
+ }
1378
+ }
1379
+ if (allowJsonStringify) {
1380
+ try {
1381
+ const serialized = JSON.stringify(value, null, 2);
1382
+ return serialized.trim() || null;
1383
+ }
1384
+ catch {
1385
+ return null;
1386
+ }
1387
+ }
1388
+ }
1389
+ return null;
1390
+ }
1023
1391
  async initializeRouteErrorHub() {
1024
1392
  try {
1025
1393
  this.routeErrorHub = initializeRouteErrorHub({ errorHandlingCenter: this.errorHandling });