@jsonstudio/llms 0.6.2172 → 0.6.3214
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/conversion/args-mapping.js +8 -0
- package/dist/conversion/bridge-actions.js +367 -0
- package/dist/conversion/bridge-id-utils.js +13 -0
- package/dist/conversion/{shared/bridge-instructions.js → bridge-instructions.js} +2 -1
- package/dist/conversion/{shared/bridge-message-utils.d.ts → bridge-message-utils.d.ts} +1 -1
- package/dist/conversion/{shared/bridge-message-utils.js → bridge-message-utils.js} +5 -150
- package/dist/conversion/{shared/bridge-metadata.d.ts → bridge-metadata.d.ts} +1 -0
- package/dist/conversion/bridge-metadata.js +5 -0
- package/dist/conversion/bridge-policies.js +11 -0
- package/dist/conversion/codecs/gemini-openai-codec.js +27 -8
- package/dist/conversion/codecs/responses-openai-codec.js +1 -1
- package/dist/conversion/{shared/compaction-detect.d.ts → compaction-detect.d.ts} +1 -1
- package/dist/conversion/compaction-detect.js +4 -0
- package/dist/conversion/compat/actions/apply-patch-fixer.js +2 -2
- package/dist/conversion/compat/actions/deepseek-web-response.d.ts +0 -1
- package/dist/conversion/compat/actions/deepseek-web-response.js +15 -405
- package/dist/conversion/compat/actions/harvest-tool-calls-from-text.js +1 -1
- package/dist/conversion/compat/actions/lmstudio-responses-fc-ids.js +1 -1
- package/dist/conversion/compat/actions/qwen-transform.js +74 -2
- package/dist/conversion/compat/actions/snapshot.js +1 -1
- package/dist/conversion/compat/antigravity-session-signature.js +71 -1
- package/dist/conversion/compat/profiles/chat-deepseek-web.json +0 -22
- package/dist/conversion/compat/profiles/chat-gemini-cli.json +0 -7
- package/dist/conversion/compat/profiles/chat-gemini.json +0 -6
- package/dist/conversion/compat/profiles/chat-glm.json +251 -72
- package/dist/conversion/compat/profiles/chat-iflow.json +174 -39
- package/dist/conversion/compat/profiles/chat-lmstudio.json +43 -14
- package/dist/conversion/hub/core/detour-registry.d.ts +2 -0
- package/dist/conversion/hub/core/hub-context.d.ts +3 -1
- package/dist/conversion/hub/core/index.d.ts +1 -0
- package/dist/conversion/hub/core/stage-driver.js +2 -0
- package/dist/conversion/hub/format-adapters/anthropic-format-adapter.js +15 -4
- package/dist/conversion/hub/format-adapters/chat-format-adapter.js +15 -4
- package/dist/conversion/hub/format-adapters/gemini-format-adapter.js +15 -4
- package/dist/conversion/hub/format-adapters/responses-format-adapter.js +15 -4
- package/dist/conversion/hub/hub-feature.js +3 -2
- package/dist/conversion/hub/node-support.js +9 -4
- package/dist/conversion/hub/operation-table/operation-table-runner.js +12 -8
- package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.js +3 -3
- package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.js +16 -13
- package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +6 -10
- package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +21 -73
- package/dist/conversion/hub/ops/operations.js +2 -121
- package/dist/conversion/hub/pipeline/compat/compat-engine.js +6 -0
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.d.ts +1 -1
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +33 -1042
- package/dist/conversion/hub/pipeline/compat/compat-profile-resolver.js +2 -0
- package/dist/conversion/hub/pipeline/compat/compat-profile-store.js +2 -0
- package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +14 -17
- package/dist/conversion/hub/pipeline/compat/native-adapter-context.d.ts +3 -0
- package/dist/conversion/hub/pipeline/compat/native-adapter-context.js +39 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +1 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +165 -265
- package/dist/conversion/hub/pipeline/session-identifiers.js +6 -196
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage1_format_parse/index.d.ts +1 -2
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage1_format_parse/index.js +37 -1
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.d.ts +1 -1
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +69 -81
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/semantic-lift.d.ts +14 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/semantic-lift.js +24 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/archive/shell-like-tool-call-normalization-fallback.d.ts +2 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/archive/shell-like-tool-call-normalization-fallback.js +157 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/context-capture-orchestration.d.ts +16 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/context-capture-orchestration.js +29 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/context-factories.js +3 -1
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.d.ts +2 -15
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +8 -595
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/responses-context-snapshot.d.ts +8 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/responses-context-snapshot.js +28 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/tool-output-diagnostics.d.ts +2 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/tool-output-diagnostics.js +4 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/tool-output-snapshot.d.ts +10 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/tool-output-snapshot.js +12 -0
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/context-merge.d.ts +3 -0
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/context-merge.js +30 -0
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +9 -129
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.d.ts +1 -4
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.js +9 -26
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage3_compat/index.js +32 -14
- package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage1_tool_governance/index.d.ts +2 -2
- package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage1_tool_governance/index.js +48 -8
- package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage2_route_select/index.js +20 -3
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +11 -199
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/stream-json-sniffer.d.ts +3 -0
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/stream-json-sniffer.js +81 -0
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage2_format_parse/index.d.ts +1 -2
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage2_format_parse/index.js +36 -1
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.js +3 -1
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/chat-process-semantics-bridge.d.ts +6 -0
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/chat-process-semantics-bridge.js +17 -0
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/client-remap-protocol-switch.d.ts +9 -0
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/client-remap-protocol-switch.js +28 -0
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.d.ts +1 -2
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +14 -102
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage2_sse_stream/index.js +3 -1
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +54 -39
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage2_finalize/index.js +18 -5
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage3_servertool_orchestration/index.d.ts +11 -1
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage3_servertool_orchestration/index.js +10 -32
- package/dist/conversion/hub/pipeline/stages/utils.js +17 -1
- package/dist/conversion/hub/pipeline/target-utils.js +14 -91
- package/dist/conversion/hub/pipelines/inbound.js +3 -1
- package/dist/conversion/hub/pipelines/outbound.js +2 -0
- package/dist/conversion/hub/policy/policy-engine.js +9 -3
- package/dist/conversion/hub/policy/protocol-spec.js +21 -149
- package/dist/conversion/hub/process/chat-process-anthropic-alias.d.ts +2 -0
- package/dist/conversion/hub/process/chat-process-anthropic-alias.js +36 -0
- package/dist/conversion/hub/process/chat-process-clock-directive-parser.d.ts +5 -0
- package/dist/conversion/hub/process/chat-process-clock-directive-parser.js +48 -0
- package/dist/conversion/hub/process/chat-process-clock-directives.d.ts +24 -0
- package/dist/conversion/hub/process/chat-process-clock-directives.js +98 -0
- package/dist/conversion/hub/process/chat-process-clock-reminder-directives.d.ts +8 -0
- package/dist/conversion/hub/process/chat-process-clock-reminder-directives.js +42 -0
- package/dist/conversion/hub/process/chat-process-clock-reminder-finalize.d.ts +14 -0
- package/dist/conversion/hub/process/chat-process-clock-reminder-finalize.js +10 -0
- package/dist/conversion/hub/process/chat-process-clock-reminder-messages.d.ts +5 -0
- package/dist/conversion/hub/process/chat-process-clock-reminder-messages.js +10 -0
- package/dist/conversion/hub/process/chat-process-clock-reminder-orchestration.d.ts +30 -0
- package/dist/conversion/hub/process/chat-process-clock-reminder-orchestration.js +68 -0
- package/dist/conversion/hub/process/chat-process-clock-reminder-time-tag.d.ts +9 -0
- package/dist/conversion/hub/process/chat-process-clock-reminder-time-tag.js +18 -0
- package/dist/conversion/hub/process/chat-process-clock-reminders.d.ts +2 -0
- package/dist/conversion/hub/process/chat-process-clock-reminders.js +104 -0
- package/dist/conversion/hub/process/chat-process-clock-tool-schemas.d.ts +3 -0
- package/dist/conversion/hub/process/chat-process-clock-tool-schemas.js +233 -0
- package/dist/conversion/hub/process/chat-process-clock-tools.d.ts +6 -0
- package/dist/conversion/hub/process/chat-process-clock-tools.js +41 -0
- package/dist/conversion/hub/process/chat-process-continue-execution.d.ts +11 -0
- package/dist/conversion/hub/process/chat-process-continue-execution.js +82 -0
- package/dist/conversion/hub/process/chat-process-governance-context.d.ts +15 -0
- package/dist/conversion/hub/process/chat-process-governance-context.js +7 -0
- package/dist/conversion/hub/process/chat-process-governance-finalize.d.ts +16 -0
- package/dist/conversion/hub/process/chat-process-governance-finalize.js +11 -0
- package/dist/conversion/hub/process/chat-process-governance-orchestration.d.ts +9 -0
- package/dist/conversion/hub/process/chat-process-governance-orchestration.js +47 -0
- package/dist/conversion/hub/process/chat-process-governed-control-ops.d.ts +8 -0
- package/dist/conversion/hub/process/chat-process-governed-control-ops.js +5 -0
- package/dist/conversion/hub/process/chat-process-governed-filter-call.d.ts +12 -0
- package/dist/conversion/hub/process/chat-process-governed-filter-call.js +18 -0
- package/dist/conversion/hub/process/chat-process-governed-merge.d.ts +8 -0
- package/dist/conversion/hub/process/chat-process-governed-merge.js +11 -0
- package/dist/conversion/hub/process/chat-process-media.d.ts +3 -0
- package/dist/conversion/hub/process/chat-process-media.js +18 -0
- package/dist/conversion/hub/process/chat-process-node-result.d.ts +23 -0
- package/dist/conversion/hub/process/chat-process-node-result.js +24 -0
- package/dist/conversion/hub/process/chat-process-pending-tool-sync.d.ts +14 -0
- package/dist/conversion/hub/process/chat-process-pending-tool-sync.js +52 -0
- package/dist/conversion/hub/process/chat-process-post-governed-normalization.d.ts +8 -0
- package/dist/conversion/hub/process/chat-process-post-governed-normalization.js +16 -0
- package/dist/conversion/hub/process/chat-process-review.d.ts +2 -0
- package/dist/conversion/hub/process/chat-process-review.js +8 -0
- package/dist/conversion/hub/process/chat-process-servertool-orchestration.d.ts +8 -0
- package/dist/conversion/hub/process/chat-process-servertool-orchestration.js +22 -0
- package/dist/conversion/hub/process/chat-process-tool-normalization.d.ts +2 -0
- package/dist/conversion/hub/process/chat-process-tool-normalization.js +4 -0
- package/dist/conversion/hub/process/chat-process-web-search-intent.d.ts +12 -0
- package/dist/conversion/hub/process/chat-process-web-search-intent.js +13 -0
- package/dist/conversion/hub/process/chat-process-web-search-tool-schema.d.ts +3 -0
- package/dist/conversion/hub/process/chat-process-web-search-tool-schema.js +4 -0
- package/dist/conversion/hub/process/chat-process-web-search.d.ts +8 -0
- package/dist/conversion/hub/process/chat-process-web-search.js +26 -0
- package/dist/conversion/hub/process/chat-process.d.ts +2 -19
- package/dist/conversion/hub/process/chat-process.js +12 -1701
- package/dist/conversion/hub/process/client-inject-readiness.d.ts +1 -0
- package/dist/conversion/hub/process/client-inject-readiness.js +4 -0
- package/dist/conversion/hub/registry.js +5 -2
- package/dist/conversion/hub/response/chat-response-utils.js +5 -86
- package/dist/conversion/hub/response/provider-response.d.ts +9 -0
- package/dist/conversion/hub/response/provider-response.js +20 -26
- package/dist/conversion/hub/response/response-mappers.js +25 -27
- package/dist/conversion/hub/response/response-runtime.js +30 -98
- package/dist/conversion/hub/semantic-mappers/anthropic-mapper.d.ts +1 -0
- package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +1 -0
- package/dist/conversion/hub/semantic-mappers/chat-mapper.d.ts +1 -0
- package/dist/conversion/hub/semantic-mappers/chat-mapper.js +1 -0
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.d.ts +1 -0
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +1 -0
- package/dist/conversion/hub/semantic-mappers/responses-mapper.d.ts +1 -0
- package/dist/conversion/hub/semantic-mappers/responses-mapper.js +1 -0
- package/dist/conversion/hub/snapshot-recorder.js +11 -4
- package/dist/conversion/hub/standardized-bridge.js +11 -288
- package/dist/conversion/hub/tool-governance/engine.d.ts +8 -0
- package/dist/conversion/hub/tool-governance/engine.js +25 -63
- package/dist/conversion/hub/tool-governance/rules.js +73 -69
- package/dist/conversion/hub/tool-session-compat.d.ts +2 -2
- package/dist/conversion/hub/tool-session-compat.js +17 -231
- package/dist/conversion/hub/tool-surface/tool-surface-engine.js +6 -4
- package/dist/conversion/index.d.ts +1 -2
- package/dist/conversion/index.js +1 -2
- package/dist/conversion/jsonish.js +20 -0
- package/dist/conversion/mcp-injection.js +7 -0
- package/dist/conversion/media.js +4 -0
- package/dist/conversion/{shared/metadata-passthrough.d.ts → metadata-passthrough.d.ts} +1 -1
- package/dist/conversion/metadata-passthrough.js +20 -0
- package/dist/conversion/payload-budget.js +47 -0
- package/dist/conversion/protocol-field-allowlists.d.ts +7 -0
- package/dist/conversion/protocol-field-allowlists.js +9 -0
- package/dist/conversion/{shared/protocol-state.d.ts → protocol-state.d.ts} +2 -2
- package/dist/conversion/protocol-state.js +27 -0
- package/dist/conversion/{shared/errors.d.ts → provider-protocol-error.d.ts} +0 -3
- package/dist/conversion/provider-protocol-error.js +25 -0
- package/dist/conversion/responses/responses-host-policy.js +2 -12
- package/dist/conversion/responses/responses-openai-bridge/response-payload.js +14 -87
- package/dist/conversion/responses/responses-openai-bridge/types.d.ts +2 -1
- package/dist/conversion/responses/responses-openai-bridge.d.ts +1 -1
- package/dist/conversion/responses/responses-openai-bridge.js +62 -62
- package/dist/conversion/{shared/runtime-metadata.d.ts → runtime-metadata.d.ts} +1 -1
- package/dist/conversion/runtime-metadata.js +31 -0
- package/dist/conversion/shared/anthropic-message-utils.js +170 -21
- package/dist/conversion/shared/chat-envelope-validator.js +2 -126
- package/dist/conversion/shared/chat-output-normalizer.js +2 -54
- package/dist/conversion/shared/chat-request-filters.d.ts +3 -4
- package/dist/conversion/shared/chat-request-filters.js +22 -78
- package/dist/conversion/shared/gemini-tool-utils.d.ts +1 -1
- package/dist/conversion/shared/gemini-tool-utils.js +9 -524
- package/dist/conversion/shared/openai-finalizer.js +3 -1
- package/dist/conversion/shared/openai-message-normalize.js +13 -285
- package/dist/conversion/shared/output-content-normalizer.js +9 -112
- package/dist/conversion/shared/reasoning-mapping.js +2 -6
- package/dist/conversion/shared/reasoning-normalizer.js +10 -1
- package/dist/conversion/shared/reasoning-tool-normalizer.js +14 -126
- package/dist/conversion/shared/reasoning-tool-parser.js +4 -87
- package/dist/conversion/shared/reasoning-utils.js +7 -8
- package/dist/conversion/shared/responses-conversation-store.js +5 -83
- package/dist/conversion/shared/responses-output-builder.js +63 -55
- package/dist/conversion/shared/responses-reasoning-registry.d.ts +14 -2
- package/dist/conversion/shared/responses-reasoning-registry.js +41 -7
- package/dist/conversion/shared/responses-request-adapter.d.ts +7 -1
- package/dist/conversion/shared/responses-request-adapter.js +14 -1
- package/dist/conversion/shared/responses-response-utils.js +103 -14
- package/dist/conversion/shared/responses-tool-utils.d.ts +1 -0
- package/dist/conversion/shared/responses-tool-utils.js +91 -15
- package/dist/conversion/shared/streaming-text-extractor.js +2 -7
- package/dist/conversion/shared/text-markup-normalizer/normalize.d.ts +2 -2
- package/dist/conversion/shared/text-markup-normalizer/normalize.js +43 -17
- package/dist/conversion/shared/text-markup-normalizer.d.ts +3 -2
- package/dist/conversion/shared/text-markup-normalizer.js +2 -1
- package/dist/conversion/shared/thought-signature-validator.js +3 -2
- package/dist/conversion/shared/tool-argument-repairer.js +2 -2
- package/dist/conversion/shared/tool-call-id-manager.js +5 -7
- package/dist/conversion/shared/tool-call-utils.js +3 -45
- package/dist/conversion/shared/tool-filter-pipeline.js +5 -100
- package/dist/conversion/shared/tool-governor.d.ts +6 -0
- package/dist/conversion/shared/tool-governor.js +45 -127
- package/dist/conversion/shared/tool-harvester.js +2 -8
- package/dist/conversion/shared/tool-mapping.d.ts +1 -1
- package/dist/conversion/shared/tool-mapping.js +2 -5
- package/dist/conversion/shared/tooling.d.ts +0 -4
- package/dist/conversion/shared/tooling.js +18 -0
- package/dist/conversion/{shared/snapshot-utils.d.ts → snapshot-utils.d.ts} +11 -0
- package/dist/conversion/{shared/snapshot-utils.js → snapshot-utils.js} +14 -23
- package/dist/conversion/{shared → types}/bridge-message-types.d.ts +2 -0
- package/dist/conversion/types/text-markup-normalizer.d.ts +13 -0
- package/dist/filters/special/request-tools-normalize.js +1 -1
- package/dist/filters/special/response-tool-text-canonicalize.js +2 -2
- package/dist/native/router_hotpath_napi.node +0 -0
- package/dist/quota/quota-manager.js +31 -59
- package/dist/quota/quota-state.js +14 -7
- package/dist/router/virtual-router/bootstrap/profile-builder.d.ts +1 -0
- package/dist/router/virtual-router/bootstrap/profile-builder.js +13 -0
- package/dist/router/virtual-router/bootstrap/provider-normalization.d.ts +2 -0
- package/dist/router/virtual-router/bootstrap/provider-normalization.js +4 -1
- package/dist/router/virtual-router/bootstrap/streaming-helpers.d.ts +7 -0
- package/dist/router/virtual-router/bootstrap/streaming-helpers.js +44 -0
- package/dist/router/virtual-router/bootstrap.js +2 -0
- package/dist/router/virtual-router/engine/provider-key/parse.d.ts +1 -6
- package/dist/router/virtual-router/engine/provider-key/parse.js +1 -43
- package/dist/router/virtual-router/engine/routing-state/store.d.ts +1 -2
- package/dist/router/virtual-router/engine/routing-state/store.js +50 -14
- package/dist/router/virtual-router/engine-legacy/config.d.ts +11 -0
- package/dist/router/virtual-router/engine-legacy/config.js +108 -0
- package/dist/router/virtual-router/engine-legacy/direct-model.d.ts +10 -0
- package/dist/router/virtual-router/engine-legacy/direct-model.js +38 -0
- package/dist/router/virtual-router/engine-legacy/health.d.ts +13 -0
- package/dist/router/virtual-router/engine-legacy/health.js +104 -0
- package/dist/router/virtual-router/engine-legacy/helpers.d.ts +16 -0
- package/dist/router/virtual-router/engine-legacy/helpers.js +226 -0
- package/dist/router/virtual-router/engine-legacy/route-finalize.d.ts +9 -0
- package/dist/router/virtual-router/engine-legacy/route-finalize.js +84 -0
- package/dist/router/virtual-router/engine-legacy/route-selection.d.ts +17 -0
- package/dist/router/virtual-router/engine-legacy/route-selection.js +205 -0
- package/dist/router/virtual-router/engine-legacy/route-state-allowlist.d.ts +3 -0
- package/dist/router/virtual-router/engine-legacy/route-state-allowlist.js +36 -0
- package/dist/router/virtual-router/engine-legacy/route-state.d.ts +12 -0
- package/dist/router/virtual-router/engine-legacy/route-state.js +386 -0
- package/dist/router/virtual-router/engine-legacy/route-utils.d.ts +19 -0
- package/dist/router/virtual-router/engine-legacy/route-utils.js +212 -0
- package/dist/router/virtual-router/engine-legacy/routing.d.ts +8 -0
- package/dist/router/virtual-router/engine-legacy/routing.js +8 -0
- package/dist/router/virtual-router/engine-legacy/selection-core.d.ts +28 -0
- package/dist/router/virtual-router/engine-legacy/selection-core.js +112 -0
- package/dist/router/virtual-router/engine-legacy/selection-state.d.ts +16 -0
- package/dist/router/virtual-router/engine-legacy/selection-state.js +187 -0
- package/dist/router/virtual-router/engine-legacy/state-accessors.d.ts +21 -0
- package/dist/router/virtual-router/engine-legacy/state-accessors.js +118 -0
- package/dist/router/virtual-router/engine-legacy.d.ts +123 -0
- package/dist/router/virtual-router/engine-legacy.js +194 -0
- package/dist/router/virtual-router/engine-logging.d.ts +2 -0
- package/dist/router/virtual-router/engine-logging.js +11 -5
- package/dist/router/virtual-router/engine-selection/alias-selection.js +45 -83
- package/dist/router/virtual-router/engine-selection/key-parsing.js +9 -26
- package/dist/router/virtual-router/engine-selection/native-chat-process-clock-directive-parser.d.ts +20 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-clock-directive-parser.js +163 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-clock-reminder-directives.d.ts +7 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-clock-reminder-directives.js +103 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-clock-reminder-orchestration-semantics.d.ts +10 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-clock-reminder-orchestration-semantics.js +110 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-clock-reminder-semantics.d.ts +8 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-clock-reminder-semantics.js +281 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-clock-reminder-time-tag-semantics.d.ts +1 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-clock-reminder-time-tag-semantics.js +25 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-clock-reminders-semantics.d.ts +4 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-clock-reminders-semantics.js +44 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-clock-tool-schema-semantics.d.ts +2 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-clock-tool-schema-semantics.js +62 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-governance-semantics.d.ts +40 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-governance-semantics.js +484 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-governed-filter-semantics.d.ts +9 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-governed-filter-semantics.js +64 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-node-result-semantics.d.ts +5 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-node-result-semantics.js +163 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-post-governed-normalization-semantics.d.ts +1 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-post-governed-normalization-semantics.js +49 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-servertool-orchestration-semantics.d.ts +30 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-servertool-orchestration-semantics.js +446 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-web-search-intent-semantics.d.ts +1 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-web-search-intent-semantics.js +49 -0
- package/dist/router/virtual-router/engine-selection/native-chat-request-filter-semantics.d.ts +1 -0
- package/dist/router/virtual-router/engine-selection/native-chat-request-filter-semantics.js +54 -0
- package/dist/router/virtual-router/engine-selection/native-hub-bridge-action-semantics.d.ts +134 -0
- package/dist/router/virtual-router/engine-selection/native-hub-bridge-action-semantics.js +729 -0
- package/dist/router/virtual-router/engine-selection/native-hub-bridge-policy-semantics.d.ts +72 -0
- package/dist/router/virtual-router/engine-selection/native-hub-bridge-policy-semantics.js +405 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-edge-stage-semantics.d.ts +18 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-edge-stage-semantics.js +317 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-governance-semantics.d.ts +22 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-governance-semantics.js +154 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-inbound-outbound-semantics.d.ts +22 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-inbound-outbound-semantics.js +426 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-orchestration-semantics.d.ts +57 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-orchestration-semantics.js +705 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-req-inbound-semantics.d.ts +46 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-req-inbound-semantics.js +503 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-req-outbound-semantics.d.ts +146 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-req-outbound-semantics.js +570 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-req-process-semantics.d.ts +25 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-req-process-semantics.js +148 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-resp-semantics.d.ts +25 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-resp-semantics.js +637 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-session-identifiers-semantics.d.ts +11 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-session-identifiers-semantics.js +207 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-target-semantics.d.ts +3 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-target-semantics.js +128 -0
- package/dist/router/virtual-router/engine-selection/native-router-hotpath-analysis.d.ts +57 -0
- package/dist/router/virtual-router/engine-selection/native-router-hotpath-analysis.js +217 -0
- package/dist/router/virtual-router/engine-selection/native-router-hotpath-loader.d.ts +5 -0
- package/dist/router/virtual-router/engine-selection/native-router-hotpath-loader.js +320 -0
- package/dist/router/virtual-router/engine-selection/native-router-hotpath-policy.d.ts +5 -0
- package/dist/router/virtual-router/engine-selection/native-router-hotpath-policy.js +18 -0
- package/dist/router/virtual-router/engine-selection/native-router-hotpath-quota-buckets.d.ts +25 -0
- package/dist/router/virtual-router/engine-selection/native-router-hotpath-quota-buckets.js +85 -0
- package/dist/router/virtual-router/engine-selection/native-router-hotpath.d.ts +59 -0
- package/dist/router/virtual-router/engine-selection/native-router-hotpath.js +117 -0
- package/dist/router/virtual-router/engine-selection/native-shared-conversion-semantics.d.ts +151 -0
- package/dist/router/virtual-router/engine-selection/native-shared-conversion-semantics.js +1371 -0
- package/dist/router/virtual-router/engine-selection/native-snapshot-hooks.d.ts +2 -0
- package/dist/router/virtual-router/engine-selection/native-snapshot-hooks.js +69 -0
- package/dist/router/virtual-router/engine-selection/native-virtual-router-alias-selection-semantics.d.ts +16 -0
- package/dist/router/virtual-router/engine-selection/native-virtual-router-alias-selection-semantics.js +96 -0
- package/dist/router/virtual-router/engine-selection/native-virtual-router-engine-proxy.d.ts +16 -0
- package/dist/router/virtual-router/engine-selection/native-virtual-router-engine-proxy.js +14 -0
- package/dist/router/virtual-router/engine-selection/native-virtual-router-routing-instructions-semantics.d.ts +2 -0
- package/dist/router/virtual-router/engine-selection/native-virtual-router-routing-instructions-semantics.js +86 -0
- package/dist/router/virtual-router/engine-selection/native-virtual-router-stop-message-actions-semantics.d.ts +6 -0
- package/dist/router/virtual-router/engine-selection/native-virtual-router-stop-message-actions-semantics.js +85 -0
- package/dist/router/virtual-router/engine-selection/native-virtual-router-stop-message-semantics.d.ts +9 -0
- package/dist/router/virtual-router/engine-selection/native-virtual-router-stop-message-semantics.js +70 -0
- package/dist/router/virtual-router/engine-selection/native-virtual-router-stop-message-state-semantics.d.ts +2 -0
- package/dist/router/virtual-router/engine-selection/native-virtual-router-stop-message-state-semantics.js +76 -0
- package/dist/router/virtual-router/engine-selection/route-utils.js +1 -1
- package/dist/router/virtual-router/engine-selection/tier-selection-antigravity-session-lease.d.ts +10 -0
- package/dist/router/virtual-router/engine-selection/tier-selection-antigravity-session-lease.js +231 -0
- package/dist/router/virtual-router/engine-selection/tier-selection-antigravity-target-split.d.ts +4 -0
- package/dist/router/virtual-router/engine-selection/tier-selection-antigravity-target-split.js +43 -0
- package/dist/router/virtual-router/engine-selection/tier-selection-quota-integration.d.ts +27 -0
- package/dist/router/virtual-router/engine-selection/tier-selection-quota-integration.js +216 -0
- package/dist/router/virtual-router/engine-selection/tier-selection-select.d.ts +1 -1
- package/dist/router/virtual-router/engine-selection/tier-selection-select.js +128 -129
- package/dist/router/virtual-router/engine-selection/tier-selection.js +2 -265
- package/dist/router/virtual-router/engine.d.ts +22 -105
- package/dist/router/virtual-router/engine.js +274 -1632
- package/dist/router/virtual-router/features.js +2 -2
- package/dist/router/virtual-router/load-balancer.d.ts +8 -0
- package/dist/router/virtual-router/load-balancer.js +65 -2
- package/dist/router/virtual-router/provider-registry.js +2 -0
- package/dist/router/virtual-router/routing-instructions/clean.d.ts +3 -0
- package/dist/router/virtual-router/routing-instructions/clean.js +34 -0
- package/dist/router/virtual-router/routing-instructions/parse.d.ts +18 -0
- package/dist/router/virtual-router/routing-instructions/parse.js +377 -0
- package/dist/router/virtual-router/routing-instructions/state.d.ts +4 -0
- package/dist/router/virtual-router/routing-instructions/state.js +245 -0
- package/dist/router/virtual-router/routing-instructions/types.d.ts +70 -0
- package/dist/router/virtual-router/routing-instructions/types.js +2 -0
- package/dist/router/virtual-router/routing-instructions.d.ts +5 -91
- package/dist/router/virtual-router/routing-instructions.js +4 -628
- package/dist/router/virtual-router/routing-stop-message-actions.js +91 -112
- package/dist/router/virtual-router/routing-stop-message-parser.js +9 -132
- package/dist/router/virtual-router/routing-stop-message-state-codec.d.ts +1 -0
- package/dist/router/virtual-router/routing-stop-message-state-codec.js +58 -71
- package/dist/router/virtual-router/sticky-session-store.d.ts +4 -0
- package/dist/router/virtual-router/sticky-session-store.js +23 -83
- package/dist/router/virtual-router/stop-message-file-resolver.d.ts +1 -0
- package/dist/router/virtual-router/stop-message-file-resolver.js +10 -0
- package/dist/router/virtual-router/stop-message-state-sync.d.ts +1 -1
- package/dist/router/virtual-router/stop-message-state-sync.js +3 -7
- package/dist/router/virtual-router/token-counter.js +0 -9
- package/dist/router/virtual-router/tool-signals.js +21 -3
- package/dist/router/virtual-router/types.d.ts +13 -7
- package/dist/servertool/clock/config.js +23 -51
- package/dist/servertool/clock/io.js +1 -0
- package/dist/servertool/clock/session-scope.d.ts +2 -2
- package/dist/servertool/clock/session-scope.js +29 -40
- package/dist/servertool/engine.d.ts +9 -0
- package/dist/servertool/engine.js +267 -79
- package/dist/servertool/handlers/antigravity-thought-signature-bootstrap.js +3 -3
- package/dist/servertool/handlers/clock-auto.js +1 -1
- package/dist/servertool/handlers/clock.js +2 -2
- package/dist/servertool/handlers/compaction-detect.d.ts +1 -1
- package/dist/servertool/handlers/compaction-detect.js +1 -1
- package/dist/servertool/handlers/continue-execution.js +8 -4
- package/dist/servertool/handlers/followup-request-builder.js +18 -1
- package/dist/servertool/handlers/gemini-empty-reply-continue.js +8 -2
- package/dist/servertool/handlers/iflow-model-error-retry.js +1 -1
- package/dist/servertool/handlers/recursive-detection-guard.js +1 -1
- package/dist/servertool/handlers/review.d.ts +1 -0
- package/dist/servertool/handlers/review.js +180 -0
- package/dist/servertool/handlers/stop-message-auto/blocked-report.js +59 -1
- package/dist/servertool/handlers/stop-message-auto/iflow-followup.d.ts +23 -2
- package/dist/servertool/handlers/stop-message-auto/iflow-followup.js +398 -90
- package/dist/servertool/handlers/stop-message-auto/routing-state.d.ts +5 -15
- package/dist/servertool/handlers/stop-message-auto/routing-state.js +29 -55
- package/dist/servertool/handlers/stop-message-auto/runtime-utils.d.ts +6 -0
- package/dist/servertool/handlers/stop-message-auto/runtime-utils.js +36 -62
- package/dist/servertool/handlers/stop-message-auto.js +393 -77
- package/dist/servertool/handlers/vision.js +1 -1
- package/dist/servertool/handlers/web-search.js +1 -1
- package/dist/servertool/reenter-backend.js +1 -1
- package/dist/servertool/server-side-tools.d.ts +1 -0
- package/dist/servertool/server-side-tools.js +92 -54
- package/dist/servertool/stop-gateway-context.js +1 -1
- package/dist/servertool/stop-message-compare-context.js +1 -1
- package/dist/servertool/types.d.ts +17 -0
- package/dist/sse/json-to-sse/event-generators/responses.d.ts +4 -0
- package/dist/sse/json-to-sse/event-generators/responses.js +95 -1
- package/dist/sse/json-to-sse/sequencers/responses-sequencer.js +6 -4
- package/dist/sse/sse-to-json/builders/response-builder.d.ts +8 -0
- package/dist/sse/sse-to-json/builders/response-builder.js +162 -4
- package/dist/sse/sse-to-json/responses-sse-to-json-converter.js +2 -0
- package/dist/sse/types/responses-types.d.ts +6 -2
- package/dist/tools/apply-patch/patch-text/normalize.js +11 -0
- package/dist/tools/apply-patch/structured/coercion.js +5 -0
- package/dist/tools/args-json.js +29 -0
- package/dist/tools/exec-command/validator.d.ts +4 -1
- package/dist/tools/exec-command/validator.js +87 -3
- package/dist/tools/tool-registry.d.ts +7 -1
- package/dist/tools/tool-registry.js +3 -2
- package/package.json +121 -10
- package/dist/conversion/shared/args-mapping.js +0 -221
- package/dist/conversion/shared/bridge-actions.js +0 -881
- package/dist/conversion/shared/bridge-id-utils.js +0 -79
- package/dist/conversion/shared/bridge-metadata.js +0 -1
- package/dist/conversion/shared/bridge-policies.js +0 -195
- package/dist/conversion/shared/compaction-detect.js +0 -59
- package/dist/conversion/shared/errors.js +0 -31
- package/dist/conversion/shared/jsonish.js +0 -177
- package/dist/conversion/shared/mcp-injection.js +0 -173
- package/dist/conversion/shared/media.js +0 -9
- package/dist/conversion/shared/metadata-passthrough.js +0 -57
- package/dist/conversion/shared/payload-budget.js +0 -248
- package/dist/conversion/shared/protocol-field-allowlists.d.ts +0 -7
- package/dist/conversion/shared/protocol-field-allowlists.js +0 -149
- package/dist/conversion/shared/protocol-state.js +0 -23
- package/dist/conversion/shared/runtime-metadata.js +0 -23
- package/dist/conversion/shared/snapshot-hooks.d.ts +0 -11
- package/dist/conversion/shared/snapshot-hooks.js +0 -503
- package/dist/conversion/shared/text-markup-normalizer/extractors-apply-patch.d.ts +0 -2
- package/dist/conversion/shared/text-markup-normalizer/extractors-apply-patch.js +0 -129
- package/dist/conversion/shared/text-markup-normalizer/extractors-json.d.ts +0 -4
- package/dist/conversion/shared/text-markup-normalizer/extractors-json.js +0 -637
- package/dist/conversion/shared/text-markup-normalizer/extractors-shared.d.ts +0 -21
- package/dist/conversion/shared/text-markup-normalizer/extractors-shared.js +0 -177
- package/dist/conversion/shared/text-markup-normalizer/extractors-transcript.d.ts +0 -5
- package/dist/conversion/shared/text-markup-normalizer/extractors-transcript.js +0 -385
- package/dist/conversion/shared/text-markup-normalizer/extractors-xml.d.ts +0 -10
- package/dist/conversion/shared/text-markup-normalizer/extractors-xml.js +0 -602
- package/dist/conversion/shared/text-markup-normalizer/extractors.d.ts +0 -5
- package/dist/conversion/shared/text-markup-normalizer/extractors.js +0 -4
- package/dist/conversion/shared/tool-canonicalizer.d.ts +0 -2
- package/dist/conversion/shared/tool-canonicalizer.js +0 -42
- package/dist/servertool/handlers/stop-message-stage-policy/bd-runtime.d.ts +0 -18
- package/dist/servertool/handlers/stop-message-stage-policy/bd-runtime.js +0 -398
- package/dist/servertool/handlers/stop-message-stage-policy/decision.d.ts +0 -9
- package/dist/servertool/handlers/stop-message-stage-policy/decision.js +0 -127
- package/dist/servertool/handlers/stop-message-stage-policy/observation.d.ts +0 -2
- package/dist/servertool/handlers/stop-message-stage-policy/observation.js +0 -179
- package/dist/servertool/handlers/stop-message-stage-policy/templates.d.ts +0 -4
- package/dist/servertool/handlers/stop-message-stage-policy/templates.js +0 -96
- package/dist/servertool/handlers/stop-message-stage-policy/text-utils.d.ts +0 -9
- package/dist/servertool/handlers/stop-message-stage-policy/text-utils.js +0 -89
- package/dist/servertool/handlers/stop-message-stage-policy/types.d.ts +0 -59
- package/dist/servertool/handlers/stop-message-stage-policy.d.ts +0 -3
- package/dist/servertool/handlers/stop-message-stage-policy.js +0 -2
- /package/dist/conversion/{shared/args-mapping.d.ts → args-mapping.d.ts} +0 -0
- /package/dist/conversion/{shared/bridge-actions.d.ts → bridge-actions.d.ts} +0 -0
- /package/dist/conversion/{shared/bridge-id-utils.d.ts → bridge-id-utils.d.ts} +0 -0
- /package/dist/conversion/{shared/bridge-instructions.d.ts → bridge-instructions.d.ts} +0 -0
- /package/dist/conversion/{shared/bridge-policies.d.ts → bridge-policies.d.ts} +0 -0
- /package/dist/conversion/{shared/jsonish.d.ts → jsonish.d.ts} +0 -0
- /package/dist/conversion/{shared/mcp-injection.d.ts → mcp-injection.d.ts} +0 -0
- /package/dist/conversion/{shared/media.d.ts → media.d.ts} +0 -0
- /package/dist/conversion/{shared/payload-budget.d.ts → payload-budget.d.ts} +0 -0
- /package/dist/conversion/{shared → types}/bridge-message-types.js +0 -0
- /package/dist/{servertool/handlers/stop-message-stage-policy/types.js → conversion/types/text-markup-normalizer.js} +0 -0
|
@@ -1,1686 +1,328 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { VirtualRouterError, VirtualRouterErrorCode } from './types.js';
|
|
2
|
+
import { ROUTING_INSTRUCTION_MARKER_PATTERN } from './routing-instructions/types.js';
|
|
3
|
+
import { parseRoutingInstructions } from './routing-instructions/parse.js';
|
|
4
|
+
import { createVirtualRouterEngineProxy } from './engine-selection/native-virtual-router-engine-proxy.js';
|
|
5
|
+
import { cleanRoutingInstructionMarkersWithNative, parseRoutingInstructionKindsWithNative } from './engine-selection/native-virtual-router-routing-instructions-semantics.js';
|
|
6
|
+
import { getLatestUserTextFromResponsesContext, hasLatestUserRoutingInstructionMarker, hasRoutingInstructionMarkerInResponsesContext } from './engine-legacy/helpers.js';
|
|
2
7
|
import { ProviderRegistry } from './provider-registry.js';
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
import { applyQuotaDepletedImpl, applyQuotaRecoveryImpl, applySeriesCooldownImpl, applyAntigravityRiskPolicyImpl, handleProviderFailureImpl, mapProviderErrorImpl } from './engine/health/index.js';
|
|
15
|
-
import { hydrateAntigravityAliasLeaseStoreIfNeeded, recordAntigravitySessionLease, resolveAntigravityAliasReuseCooldownMs } from './engine/antigravity/alias-lease.js';
|
|
16
|
-
import { buildMetadataInstructions, resolveRoutingMode } from './engine/routing-state/metadata.js';
|
|
17
|
-
import { getRoutingInstructionState, persistRoutingInstructionState, resolveStopMessageScope } from './engine/routing-state/store.js';
|
|
18
|
-
import { ensureStopMessageModeMaxRepeats } from './routing-stop-message-state-codec.js';
|
|
19
|
-
import { validateStopMessageStageTemplatesCompleteness } from './stop-message-stage-template-files.js';
|
|
20
|
-
import { extractKeyAlias, extractKeyIndex, extractProviderId, getProviderModelId } from './engine/provider-key/parse.js';
|
|
21
|
-
import { resolveSessionScope as resolveSessionScopeImpl, resolveStickyKey as resolveStickyKeyImpl } from './engine/routing-state/keys.js';
|
|
22
|
-
import { RouteAnalytics } from './engine/route-analytics.js';
|
|
23
|
-
import { StickySessionManager } from './engine/sticky-session-manager.js';
|
|
24
|
-
import { CooldownManager } from './engine/cooldown-manager.js';
|
|
25
|
-
const DEFAULT_STOP_MESSAGE_MAX_REPEATS = 10;
|
|
26
|
-
function normalizeStopMessageStageMode(value) {
|
|
27
|
-
if (typeof value !== 'string') {
|
|
28
|
-
return undefined;
|
|
29
|
-
}
|
|
30
|
-
const normalized = value.trim().toLowerCase();
|
|
31
|
-
if (normalized === 'on' || normalized === 'off' || normalized === 'auto') {
|
|
32
|
-
return normalized;
|
|
33
|
-
}
|
|
34
|
-
return undefined;
|
|
35
|
-
}
|
|
36
|
-
function resolveStopMessageStageModeAfterInstructions(instructions, currentMode) {
|
|
37
|
-
let mode = normalizeStopMessageStageMode(currentMode);
|
|
38
|
-
for (const instruction of instructions) {
|
|
39
|
-
if (instruction.type === 'stopMessageClear') {
|
|
40
|
-
mode = undefined;
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
if (instruction.type === 'stopMessageMode') {
|
|
44
|
-
const incomingMode = normalizeStopMessageStageMode(instruction.stopMessageStageMode);
|
|
45
|
-
if (incomingMode) {
|
|
46
|
-
mode = incomingMode;
|
|
47
|
-
}
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
if (instruction.type === 'stopMessageSet') {
|
|
51
|
-
const incomingMode = normalizeStopMessageStageMode(instruction.stopMessageStageMode);
|
|
52
|
-
if (incomingMode) {
|
|
53
|
-
mode = incomingMode;
|
|
54
|
-
}
|
|
55
|
-
else if (mode === 'off') {
|
|
56
|
-
mode = 'on';
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return mode;
|
|
61
|
-
}
|
|
62
|
-
function hasRoutingInstructionMarker(messages) {
|
|
63
|
-
for (const message of messages) {
|
|
64
|
-
if (!message || message.role !== 'user') {
|
|
65
|
-
continue;
|
|
66
|
-
}
|
|
67
|
-
const content = extractMessageText(message);
|
|
68
|
-
if (!content) {
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
if (/<\*\*[^*]+\*\*>/.test(content)) {
|
|
72
|
-
return true;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return false;
|
|
76
|
-
}
|
|
77
|
-
function hasLatestUserRoutingInstructionMarker(messages) {
|
|
78
|
-
for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
|
|
79
|
-
const message = messages[idx];
|
|
80
|
-
if (!message) {
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
if (message.role !== 'user') {
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
const content = extractMessageText(message);
|
|
87
|
-
if (!content) {
|
|
88
|
-
return false;
|
|
8
|
+
import { resolveStopMessageScope } from './engine/routing-state/store.js';
|
|
9
|
+
export class VirtualRouterEngine {
|
|
10
|
+
nativeProxy;
|
|
11
|
+
registry;
|
|
12
|
+
routingInstructionStateStore;
|
|
13
|
+
constructor(deps) {
|
|
14
|
+
this.nativeProxy = createVirtualRouterEngineProxy();
|
|
15
|
+
this.registry = new ProviderRegistry();
|
|
16
|
+
this.routingInstructionStateStore = new Map();
|
|
17
|
+
if (deps) {
|
|
18
|
+
this.nativeProxy.updateDeps(deps);
|
|
89
19
|
}
|
|
90
|
-
return /<\*\*[^*]+\*\*>/.test(content);
|
|
91
20
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
function isServerToolFollowupRequest(metadata) {
|
|
95
|
-
if (!metadata || typeof metadata !== 'object') {
|
|
96
|
-
return false;
|
|
21
|
+
get antigravitySessionAliasStore() {
|
|
22
|
+
return this.nativeProxy.antigravitySessionAliasStore;
|
|
97
23
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return false;
|
|
24
|
+
get routingInstructionState() {
|
|
25
|
+
return this.routingInstructionStateStore;
|
|
101
26
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
(typeof flag === 'string' && flag.trim().toLowerCase() === 'true'));
|
|
105
|
-
}
|
|
106
|
-
export class VirtualRouterEngine {
|
|
107
|
-
routing = {};
|
|
108
|
-
providerRegistry = new ProviderRegistry();
|
|
109
|
-
healthManager = new ProviderHealthManager();
|
|
110
|
-
get providerCooldowns() { return this.cooldownManager.getCooldownMap(); }
|
|
111
|
-
loadBalancer = new RouteLoadBalancer();
|
|
112
|
-
classifier = new RoutingClassifier({});
|
|
113
|
-
contextAdvisor = new ContextAdvisor();
|
|
114
|
-
contextRouting;
|
|
115
|
-
routeAnalytics = new RouteAnalytics();
|
|
116
|
-
stickySessionManager = new StickySessionManager();
|
|
117
|
-
cooldownManager;
|
|
118
|
-
antigravityLeasePersistence = { loadedOnce: false, loadedMtimeMs: null, flushTimer: null };
|
|
119
|
-
debug = console; // thin hook; host may monkey-patch for colored logging
|
|
120
|
-
healthConfig = null;
|
|
121
|
-
statsCenter = getStatsCenter();
|
|
122
|
-
// Derived flags from VirtualRouterConfig/routing used by process / response layers.
|
|
123
|
-
webSearchForce = false;
|
|
124
|
-
healthStore;
|
|
125
|
-
routingStateStore = {
|
|
126
|
-
loadSync: loadRoutingInstructionStateSync,
|
|
127
|
-
saveAsync: saveRoutingInstructionStateAsync,
|
|
128
|
-
saveSync: saveRoutingInstructionStateSync
|
|
129
|
-
};
|
|
130
|
-
routingInstructionState = new Map();
|
|
131
|
-
quotaView;
|
|
132
|
-
/**
|
|
133
|
-
* Backward-compatible test/debug surface used by existing regression scripts.
|
|
134
|
-
* Keep this as a read-only view over StickySessionManager storage.
|
|
135
|
-
*/
|
|
136
|
-
get antigravitySessionAliasStore() {
|
|
137
|
-
return this.stickySessionManager.getAllStores().sessionAliasStore;
|
|
27
|
+
get providerRegistry() {
|
|
28
|
+
return this.registry;
|
|
138
29
|
}
|
|
139
|
-
|
|
140
|
-
this.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
quotaView: deps?.quotaView
|
|
144
|
-
});
|
|
145
|
-
if (deps?.healthStore) {
|
|
146
|
-
this.healthStore = deps.healthStore;
|
|
147
|
-
}
|
|
148
|
-
if (deps?.routingStateStore) {
|
|
149
|
-
this.routingStateStore = deps.routingStateStore;
|
|
150
|
-
}
|
|
151
|
-
if (deps?.quotaView) {
|
|
152
|
-
this.quotaView = deps.quotaView;
|
|
153
|
-
}
|
|
30
|
+
initialize(config) {
|
|
31
|
+
this.nativeProxy.initialize(JSON.stringify(config));
|
|
32
|
+
this.registry.load(config.providers ?? {});
|
|
33
|
+
this.routingInstructionStateStore.clear();
|
|
154
34
|
}
|
|
155
35
|
updateDeps(deps) {
|
|
156
|
-
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
if ('healthStore' in deps) {
|
|
160
|
-
this.healthStore = deps.healthStore ?? undefined;
|
|
161
|
-
this.cooldownManager.updateDeps({ healthStore: this.healthStore });
|
|
162
|
-
}
|
|
163
|
-
if ('routingStateStore' in deps) {
|
|
164
|
-
this.routingStateStore =
|
|
165
|
-
deps.routingStateStore ??
|
|
166
|
-
{
|
|
167
|
-
loadSync: loadRoutingInstructionStateSync,
|
|
168
|
-
saveAsync: saveRoutingInstructionStateAsync,
|
|
169
|
-
saveSync: saveRoutingInstructionStateSync
|
|
170
|
-
};
|
|
171
|
-
// Routing state store changes require clearing in-memory cache to avoid stale reads.
|
|
172
|
-
this.routingInstructionState.clear();
|
|
173
|
-
}
|
|
174
|
-
if ('quotaView' in deps) {
|
|
175
|
-
const prevQuotaEnabled = Boolean(this.quotaView);
|
|
176
|
-
this.quotaView = deps.quotaView ?? undefined;
|
|
177
|
-
this.cooldownManager.updateDeps({ quotaView: this.quotaView });
|
|
178
|
-
const nextQuotaEnabled = Boolean(this.quotaView);
|
|
179
|
-
// When quotaView is enabled, health/cooldown decisions must be driven by quotaView only.
|
|
180
|
-
// - Enabling quotaView: clear any legacy router-local cooldown TTLs immediately.
|
|
181
|
-
// - Disabling quotaView: reload legacy cooldown state from health snapshots.
|
|
182
|
-
if (!prevQuotaEnabled && nextQuotaEnabled) {
|
|
183
|
-
this.cooldownManager.clearAllCooldowns();
|
|
184
|
-
}
|
|
185
|
-
else if (prevQuotaEnabled && !nextQuotaEnabled) {
|
|
186
|
-
this.cooldownManager.clearAllCooldowns();
|
|
187
|
-
this.restoreHealthFromStore();
|
|
188
|
-
}
|
|
189
|
-
}
|
|
36
|
+
this.nativeProxy.updateDeps(deps);
|
|
190
37
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
return null;
|
|
195
|
-
}
|
|
196
|
-
const firstDot = raw.indexOf('.');
|
|
197
|
-
if (firstDot <= 0 || firstDot === raw.length - 1) {
|
|
198
|
-
return null;
|
|
199
|
-
}
|
|
200
|
-
const providerId = raw.slice(0, firstDot).trim();
|
|
201
|
-
const modelId = raw.slice(firstDot + 1).trim();
|
|
202
|
-
if (!providerId || !modelId) {
|
|
203
|
-
return null;
|
|
204
|
-
}
|
|
205
|
-
if (this.providerRegistry.listProviderKeys(providerId).length === 0) {
|
|
206
|
-
return null;
|
|
207
|
-
}
|
|
208
|
-
return { providerId, modelId };
|
|
38
|
+
updateVirtualRouterConfig(config) {
|
|
39
|
+
this.nativeProxy.updateVirtualRouterConfig(JSON.stringify(config));
|
|
40
|
+
this.registry.load(config.providers ?? {});
|
|
209
41
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
return false;
|
|
213
|
-
}
|
|
214
|
-
const providerId = direct.providerId.trim().toLowerCase();
|
|
215
|
-
const modelId = direct.modelId.trim().toLowerCase();
|
|
216
|
-
if (providerId !== 'qwen') {
|
|
217
|
-
return false;
|
|
218
|
-
}
|
|
219
|
-
const isQwen35Plus = modelId === 'qwen3.5-plus' || modelId === 'qwen3-5-plus' || modelId === 'qwen3_5-plus';
|
|
220
|
-
if (!isQwen35Plus) {
|
|
221
|
-
return false;
|
|
222
|
-
}
|
|
223
|
-
if (!(features.hasVideoAttachment === true && features.hasLocalVideoAttachment === true)) {
|
|
224
|
-
return false;
|
|
225
|
-
}
|
|
226
|
-
return this.routeHasTargets(this.routing.vision);
|
|
42
|
+
markProviderCooldown(providerKey, cooldownMs) {
|
|
43
|
+
this.nativeProxy.markProviderCooldown(providerKey, cooldownMs);
|
|
227
44
|
}
|
|
228
|
-
|
|
229
|
-
this.
|
|
230
|
-
this.routing = config.routing;
|
|
231
|
-
this.providerRegistry.load(config.providers);
|
|
232
|
-
this.healthManager.configure(config.health);
|
|
233
|
-
this.healthConfig = config.health ?? null;
|
|
234
|
-
this.healthManager.registerProviders(Object.keys(config.providers));
|
|
235
|
-
this.cooldownManager.clearAllCooldowns();
|
|
236
|
-
this.restoreHealthFromStore();
|
|
237
|
-
this.loadBalancer = new RouteLoadBalancer(config.loadBalancing);
|
|
238
|
-
const aliasReuseCooldownMs = resolveAntigravityAliasReuseCooldownMs(config);
|
|
239
|
-
this.stickySessionManager = new StickySessionManager(aliasReuseCooldownMs);
|
|
240
|
-
hydrateAntigravityAliasLeaseStoreIfNeeded({
|
|
241
|
-
force: true,
|
|
242
|
-
leaseStore: this.stickySessionManager.getAllStores().aliasLeaseStore,
|
|
243
|
-
persistence: this.antigravityLeasePersistence,
|
|
244
|
-
aliasReuseCooldownMs: this.stickySessionManager.getAliasReuseCooldownMs()
|
|
245
|
-
});
|
|
246
|
-
this.classifier = new RoutingClassifier(config.classifier);
|
|
247
|
-
this.contextRouting = config.contextRouting ?? { warnRatio: 0.9, hardLimit: false };
|
|
248
|
-
this.contextAdvisor.configure(this.contextRouting);
|
|
249
|
-
this.webSearchForce = config.webSearch?.force === true;
|
|
250
|
-
this.routeAnalytics.getAllRouteStats().clear();
|
|
251
|
-
for (const routeName of Object.keys(this.routing)) {
|
|
252
|
-
this.routeAnalytics.getRouteStats(routeName) || this.routeAnalytics.incrementRouteStat(routeName, '', { timestampMs: Date.now(), stopMessage: { active: false } });
|
|
253
|
-
}
|
|
45
|
+
clearProviderCooldown(providerKey) {
|
|
46
|
+
this.nativeProxy.clearProviderCooldown(providerKey);
|
|
254
47
|
}
|
|
255
48
|
route(request, metadata) {
|
|
256
|
-
const
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
// Routing instructions should be session/conversation-scoped when available (including /v1/responses),
|
|
260
|
-
// while auto-sticky for Responses remains request-chain scoped via resolveStickyKey().
|
|
261
|
-
const stateKey = sessionScope || stickyKey || 'default';
|
|
262
|
-
const baseState = getRoutingInstructionState(stateKey, this.routingInstructionState, this.routingStateStore);
|
|
263
|
-
let routingState = baseState;
|
|
264
|
-
const metadataInstructions = buildMetadataInstructions(metadata);
|
|
265
|
-
if (metadataInstructions.length > 0) {
|
|
266
|
-
routingState = applyRoutingInstructions(metadataInstructions, routingState);
|
|
267
|
-
}
|
|
268
|
-
const disableStickyRoutes = metadata &&
|
|
269
|
-
typeof metadata === 'object' &&
|
|
270
|
-
metadata.disableStickyRoutes === true;
|
|
271
|
-
if (disableStickyRoutes && (routingState.stickyTarget || routingState.preferTarget)) {
|
|
272
|
-
routingState = {
|
|
273
|
-
...routingState,
|
|
274
|
-
stickyTarget: undefined,
|
|
275
|
-
preferTarget: undefined
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
if (stopMessageScope) {
|
|
279
|
-
const sessionState = getRoutingInstructionState(stopMessageScope, this.routingInstructionState, this.routingStateStore);
|
|
280
|
-
if (ensureStopMessageModeMaxRepeats(sessionState)) {
|
|
281
|
-
this.routingInstructionState.set(stopMessageScope, sessionState);
|
|
282
|
-
persistRoutingInstructionState(stopMessageScope, sessionState, this.routingStateStore);
|
|
283
|
-
}
|
|
284
|
-
if (typeof sessionState.stopMessageText === 'string' ||
|
|
285
|
-
typeof sessionState.stopMessageMaxRepeats === 'number' ||
|
|
286
|
-
typeof sessionState.stopMessageStageMode === 'string') {
|
|
287
|
-
routingState = {
|
|
288
|
-
...routingState,
|
|
289
|
-
stopMessageText: sessionState.stopMessageText,
|
|
290
|
-
stopMessageMaxRepeats: sessionState.stopMessageMaxRepeats,
|
|
291
|
-
stopMessageUsed: sessionState.stopMessageUsed,
|
|
292
|
-
stopMessageUpdatedAt: sessionState.stopMessageUpdatedAt,
|
|
293
|
-
stopMessageLastUsedAt: sessionState.stopMessageLastUsedAt,
|
|
294
|
-
stopMessageStage: sessionState.stopMessageStage,
|
|
295
|
-
stopMessageStageMode: sessionState.stopMessageStageMode,
|
|
296
|
-
stopMessageObservationHash: sessionState.stopMessageObservationHash,
|
|
297
|
-
stopMessageObservationStableCount: sessionState.stopMessageObservationStableCount,
|
|
298
|
-
stopMessageBdWorkState: sessionState.stopMessageBdWorkState
|
|
299
|
-
};
|
|
300
|
-
}
|
|
301
|
-
if (typeof sessionState.preCommandScriptPath === 'string' && sessionState.preCommandScriptPath.trim()) {
|
|
302
|
-
routingState = {
|
|
303
|
-
...routingState,
|
|
304
|
-
preCommandSource: sessionState.preCommandSource,
|
|
305
|
-
preCommandScriptPath: sessionState.preCommandScriptPath,
|
|
306
|
-
preCommandUpdatedAt: sessionState.preCommandUpdatedAt
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
const parsedInstructions = parseRoutingInstructions(request.messages);
|
|
311
|
-
const serverToolFollowup = isServerToolFollowupRequest(metadata);
|
|
312
|
-
const latestUserHasMarker = hasLatestUserRoutingInstructionMarker(request.messages);
|
|
313
|
-
let instructions = parsedInstructions;
|
|
314
|
-
if (serverToolFollowup && instructions.length > 0) {
|
|
315
|
-
instructions = instructions.filter((entry) => entry.type !== 'stopMessageSet' &&
|
|
316
|
-
entry.type !== 'stopMessageMode' &&
|
|
317
|
-
entry.type !== 'stopMessageClear' &&
|
|
318
|
-
entry.type !== 'preCommandSet' &&
|
|
319
|
-
entry.type !== 'preCommandClear');
|
|
320
|
-
}
|
|
321
|
-
if (stopMessageScope && parsedInstructions.length > 0) {
|
|
322
|
-
const sessionState = getRoutingInstructionState(stopMessageScope, this.routingInstructionState, this.routingStateStore);
|
|
323
|
-
const hasStaleStopMessageInstruction = !latestUserHasMarker &&
|
|
324
|
-
parsedInstructions.some((entry) => entry.type === 'stopMessageSet' ||
|
|
325
|
-
entry.type === 'stopMessageMode' ||
|
|
326
|
-
entry.type === 'stopMessageClear');
|
|
327
|
-
if (hasStaleStopMessageInstruction) {
|
|
328
|
-
const hasActiveStopState = typeof sessionState.stopMessageText === 'string' ||
|
|
329
|
-
typeof sessionState.stopMessageMaxRepeats === 'number' ||
|
|
330
|
-
typeof sessionState.stopMessageStageMode === 'string';
|
|
331
|
-
const hasStopLifecycleStamp = (typeof sessionState.stopMessageUpdatedAt === 'number' && Number.isFinite(sessionState.stopMessageUpdatedAt)) ||
|
|
332
|
-
(typeof sessionState.stopMessageLastUsedAt === 'number' && Number.isFinite(sessionState.stopMessageLastUsedAt));
|
|
333
|
-
if (hasActiveStopState || hasStopLifecycleStamp) {
|
|
334
|
-
instructions = instructions.filter((entry) => entry.type !== 'stopMessageSet' &&
|
|
335
|
-
entry.type !== 'stopMessageMode' &&
|
|
336
|
-
entry.type !== 'stopMessageClear');
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
const hasGlobalClear = instructions.some((entry) => entry.type === 'clear');
|
|
340
|
-
const hasStopMessageClear = hasGlobalClear || instructions.some((entry) => entry.type === 'stopMessageClear');
|
|
341
|
-
const stopMessageSets = instructions.filter((entry) => entry.type === 'stopMessageSet');
|
|
342
|
-
if (!hasStopMessageClear && stopMessageSets.length > 0) {
|
|
343
|
-
const sessionText = typeof sessionState.stopMessageText === 'string' ? sessionState.stopMessageText.trim() : '';
|
|
344
|
-
const sessionMax = typeof sessionState.stopMessageMaxRepeats === 'number' && Number.isFinite(sessionState.stopMessageMaxRepeats)
|
|
345
|
-
? Math.floor(sessionState.stopMessageMaxRepeats)
|
|
346
|
-
: undefined;
|
|
347
|
-
const sessionMode = normalizeStopMessageStageMode(sessionState.stopMessageStageMode);
|
|
348
|
-
const allSame = stopMessageSets.every((entry) => {
|
|
349
|
-
const entryText = typeof entry.stopMessageText === 'string' ? entry.stopMessageText.trim() : '';
|
|
350
|
-
const entryMax = typeof entry.stopMessageMaxRepeats === 'number' && Number.isFinite(entry.stopMessageMaxRepeats)
|
|
351
|
-
? Math.floor(entry.stopMessageMaxRepeats)
|
|
352
|
-
: undefined;
|
|
353
|
-
const incomingMode = normalizeStopMessageStageMode(entry.stopMessageStageMode);
|
|
354
|
-
const entryMode = incomingMode ?? (sessionMode === 'off' ? 'on' : sessionMode);
|
|
355
|
-
return Boolean(entryText) && entryText === sessionText && entryMax === sessionMax && entryMode === sessionMode;
|
|
356
|
-
});
|
|
357
|
-
const used = typeof sessionState.stopMessageUsed === 'number' && Number.isFinite(sessionState.stopMessageUsed)
|
|
358
|
-
? Math.max(0, Math.floor(sessionState.stopMessageUsed))
|
|
359
|
-
: 0;
|
|
360
|
-
const hasLastUsedAt = typeof sessionState.stopMessageLastUsedAt === 'number' &&
|
|
361
|
-
Number.isFinite(sessionState.stopMessageLastUsedAt);
|
|
362
|
-
const alreadyArmed = used === 0 && !hasLastUsedAt;
|
|
363
|
-
if (allSame && alreadyArmed) {
|
|
364
|
-
instructions = instructions.filter((entry) => entry.type !== 'stopMessageSet');
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
// stopMessage must be session-scoped: require explicit sessionId in metadata.
|
|
369
|
-
// This prevents global/default persistence and ensures the trigger matches the setting sessionId.
|
|
370
|
-
if (instructions.length > 0) {
|
|
371
|
-
const hasSessionScopedInstruction = instructions.some((entry) => entry.type === 'stopMessageSet' ||
|
|
372
|
-
entry.type === 'stopMessageMode' ||
|
|
373
|
-
entry.type === 'stopMessageClear' ||
|
|
374
|
-
entry.type === 'preCommandSet' ||
|
|
375
|
-
entry.type === 'preCommandClear');
|
|
376
|
-
if (hasSessionScopedInstruction && !stopMessageScope) {
|
|
377
|
-
throw new VirtualRouterError('[stopMessage/precommand] requires sessionId (e.g. set x-session-id header or metadata.sessionId).', VirtualRouterErrorCode.CONFIG_ERROR, { requestId: metadata.requestId, entryEndpoint: metadata.entryEndpoint });
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
if (stopMessageScope && instructions.length > 0) {
|
|
381
|
-
const sessionState = getRoutingInstructionState(stopMessageScope, this.routingInstructionState, this.routingStateStore);
|
|
382
|
-
const nextStopMessageMode = resolveStopMessageStageModeAfterInstructions(instructions, sessionState.stopMessageStageMode);
|
|
383
|
-
if (nextStopMessageMode === 'on') {
|
|
384
|
-
const templateCheck = validateStopMessageStageTemplatesCompleteness();
|
|
385
|
-
if (!templateCheck.ok) {
|
|
386
|
-
const detail = templateCheck.missing
|
|
387
|
-
.map((entry) => `${entry.stage}:${entry.ref || '(no-ref)'}:${entry.error || 'invalid'}`)
|
|
388
|
-
.join('; ');
|
|
389
|
-
throw new VirtualRouterError(`[stopMessage] mode=on requires complete stage message files (${detail}).`, VirtualRouterErrorCode.CONFIG_ERROR, { requestId: metadata.requestId, entryEndpoint: metadata.entryEndpoint });
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
if (hasRoutingInstructionMarker(request.messages)) {
|
|
394
|
-
request.messages = cleanMessagesFromRoutingInstructions(request.messages);
|
|
395
|
-
}
|
|
396
|
-
if (instructions.length > 0) {
|
|
397
|
-
routingState = applyRoutingInstructions(instructions, routingState);
|
|
398
|
-
this.routingInstructionState.set(stateKey, routingState);
|
|
399
|
-
persistRoutingInstructionState(stateKey, routingState, this.routingStateStore);
|
|
400
|
-
// 对 stopMessage 指令补充一份基于 session/conversation 的持久化状态,
|
|
401
|
-
// 便于 server-side 工具通过 session:*/conversation:* scope 读取到相同配置。
|
|
402
|
-
// stopMessage is strictly session-scoped (sessionId only). Persist it under the session scope
|
|
403
|
-
// so servertool triggers always match the setting sessionId.
|
|
404
|
-
if (stopMessageScope) {
|
|
405
|
-
const hasStopMessageSet = instructions.some((entry) => entry.type === 'stopMessageSet');
|
|
406
|
-
const hasStopMessageMode = instructions.some((entry) => entry.type === 'stopMessageMode');
|
|
407
|
-
const hasGlobalClear = instructions.some((entry) => entry.type === 'clear');
|
|
408
|
-
const hasStopMessageClear = hasGlobalClear || instructions.some((entry) => entry.type === 'stopMessageClear');
|
|
409
|
-
if (hasStopMessageSet || hasStopMessageMode || hasStopMessageClear) {
|
|
410
|
-
const sessionState = getRoutingInstructionState(stopMessageScope, this.routingInstructionState, this.routingStateStore);
|
|
411
|
-
let nextSessionState = {
|
|
412
|
-
...sessionState
|
|
413
|
-
};
|
|
414
|
-
let shouldPersistSessionState = false;
|
|
415
|
-
if (hasStopMessageClear) {
|
|
416
|
-
const clearedAt = Date.now();
|
|
417
|
-
nextSessionState.stopMessageText = undefined;
|
|
418
|
-
nextSessionState.stopMessageMaxRepeats = undefined;
|
|
419
|
-
nextSessionState.stopMessageUsed = undefined;
|
|
420
|
-
nextSessionState.stopMessageUpdatedAt = clearedAt;
|
|
421
|
-
nextSessionState.stopMessageLastUsedAt = clearedAt;
|
|
422
|
-
nextSessionState.stopMessageSource = undefined;
|
|
423
|
-
nextSessionState.stopMessageStage = undefined;
|
|
424
|
-
nextSessionState.stopMessageStageMode = undefined;
|
|
425
|
-
nextSessionState.stopMessageObservationHash = undefined;
|
|
426
|
-
nextSessionState.stopMessageObservationStableCount = undefined;
|
|
427
|
-
nextSessionState.stopMessageBdWorkState = undefined;
|
|
428
|
-
shouldPersistSessionState = true;
|
|
429
|
-
}
|
|
430
|
-
else if (hasStopMessageSet) {
|
|
431
|
-
const text = typeof routingState.stopMessageText === 'string' ? routingState.stopMessageText : '';
|
|
432
|
-
const maxRepeats = routingState.stopMessageMaxRepeats;
|
|
433
|
-
const sameText = typeof sessionState.stopMessageText === 'string' &&
|
|
434
|
-
sessionState.stopMessageText.trim() === text.trim();
|
|
435
|
-
const sameMax = typeof sessionState.stopMessageMaxRepeats === 'number' &&
|
|
436
|
-
typeof maxRepeats === 'number' &&
|
|
437
|
-
Math.floor(sessionState.stopMessageMaxRepeats) === Math.floor(maxRepeats);
|
|
438
|
-
const currentMode = normalizeStopMessageStageMode(sessionState.stopMessageStageMode);
|
|
439
|
-
const nextMode = normalizeStopMessageStageMode(routingState.stopMessageStageMode) ?? currentMode;
|
|
440
|
-
const isSameMode = currentMode === nextMode;
|
|
441
|
-
const isSameInstruction = Boolean(text) && sameText && sameMax && isSameMode;
|
|
442
|
-
const used = typeof sessionState.stopMessageUsed === 'number' && Number.isFinite(sessionState.stopMessageUsed)
|
|
443
|
-
? Math.max(0, Math.floor(sessionState.stopMessageUsed))
|
|
444
|
-
: 0;
|
|
445
|
-
const hasLastUsedAt = typeof sessionState.stopMessageLastUsedAt === 'number' &&
|
|
446
|
-
Number.isFinite(sessionState.stopMessageLastUsedAt);
|
|
447
|
-
const shouldRearm = !isSameInstruction || used > 0 || hasLastUsedAt;
|
|
448
|
-
nextSessionState.stopMessageText = text || undefined;
|
|
449
|
-
nextSessionState.stopMessageMaxRepeats = maxRepeats;
|
|
450
|
-
nextSessionState.stopMessageSource = 'explicit';
|
|
451
|
-
nextSessionState.stopMessageStageMode = nextMode;
|
|
452
|
-
if (shouldRearm) {
|
|
453
|
-
nextSessionState.stopMessageUsed = 0;
|
|
454
|
-
nextSessionState.stopMessageUpdatedAt =
|
|
455
|
-
typeof routingState.stopMessageUpdatedAt === 'number'
|
|
456
|
-
? routingState.stopMessageUpdatedAt
|
|
457
|
-
: Date.now();
|
|
458
|
-
nextSessionState.stopMessageStage = undefined;
|
|
459
|
-
nextSessionState.stopMessageObservationHash = undefined;
|
|
460
|
-
nextSessionState.stopMessageObservationStableCount = 0;
|
|
461
|
-
nextSessionState.stopMessageBdWorkState = undefined;
|
|
462
|
-
nextSessionState.stopMessageLastUsedAt = undefined;
|
|
463
|
-
shouldPersistSessionState = true;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
else if (hasStopMessageMode) {
|
|
467
|
-
const mode = normalizeStopMessageStageMode(routingState.stopMessageStageMode);
|
|
468
|
-
const modeMaxRepeats = typeof routingState.stopMessageMaxRepeats === 'number' && Number.isFinite(routingState.stopMessageMaxRepeats)
|
|
469
|
-
? Math.floor(routingState.stopMessageMaxRepeats)
|
|
470
|
-
: 0;
|
|
471
|
-
if (mode === 'off') {
|
|
472
|
-
const changed = typeof nextSessionState.stopMessageText === 'string' ||
|
|
473
|
-
typeof nextSessionState.stopMessageMaxRepeats === 'number' ||
|
|
474
|
-
typeof nextSessionState.stopMessageUsed === 'number' ||
|
|
475
|
-
typeof nextSessionState.stopMessageSource === 'string' ||
|
|
476
|
-
typeof nextSessionState.stopMessageStage === 'string' ||
|
|
477
|
-
typeof nextSessionState.stopMessageObservationHash === 'string' ||
|
|
478
|
-
typeof nextSessionState.stopMessageObservationStableCount === 'number' ||
|
|
479
|
-
typeof nextSessionState.stopMessageBdWorkState === 'string' ||
|
|
480
|
-
normalizeStopMessageStageMode(nextSessionState.stopMessageStageMode) !== 'off';
|
|
481
|
-
nextSessionState.stopMessageText = undefined;
|
|
482
|
-
nextSessionState.stopMessageMaxRepeats = undefined;
|
|
483
|
-
nextSessionState.stopMessageUsed = undefined;
|
|
484
|
-
nextSessionState.stopMessageSource = undefined;
|
|
485
|
-
nextSessionState.stopMessageStage = undefined;
|
|
486
|
-
nextSessionState.stopMessageStageMode = 'off';
|
|
487
|
-
nextSessionState.stopMessageObservationHash = undefined;
|
|
488
|
-
nextSessionState.stopMessageObservationStableCount = undefined;
|
|
489
|
-
nextSessionState.stopMessageBdWorkState = undefined;
|
|
490
|
-
nextSessionState.stopMessageUpdatedAt = Date.now();
|
|
491
|
-
nextSessionState.stopMessageLastUsedAt = undefined;
|
|
492
|
-
shouldPersistSessionState = changed;
|
|
493
|
-
}
|
|
494
|
-
else if (mode) {
|
|
495
|
-
if (normalizeStopMessageStageMode(nextSessionState.stopMessageStageMode) !== mode) {
|
|
496
|
-
nextSessionState.stopMessageStageMode = mode;
|
|
497
|
-
shouldPersistSessionState = true;
|
|
498
|
-
}
|
|
499
|
-
const fallbackMaxRepeats = mode === 'on' || mode === 'auto'
|
|
500
|
-
? DEFAULT_STOP_MESSAGE_MAX_REPEATS
|
|
501
|
-
: 0;
|
|
502
|
-
const normalizedMaxRepeats = modeMaxRepeats > 0 ? modeMaxRepeats : fallbackMaxRepeats;
|
|
503
|
-
if (normalizedMaxRepeats > 0 &&
|
|
504
|
-
(typeof nextSessionState.stopMessageMaxRepeats !== 'number' ||
|
|
505
|
-
Math.floor(nextSessionState.stopMessageMaxRepeats) !== normalizedMaxRepeats)) {
|
|
506
|
-
nextSessionState.stopMessageMaxRepeats = normalizedMaxRepeats;
|
|
507
|
-
shouldPersistSessionState = true;
|
|
508
|
-
}
|
|
509
|
-
// Explicit mode marker from latest user turn should re-arm lifecycle counters,
|
|
510
|
-
// even when mode/max are unchanged (e.g. <**stopMessage:on,10**>).
|
|
511
|
-
if (mode === 'on' || mode === 'auto') {
|
|
512
|
-
nextSessionState.stopMessageUsed = 0;
|
|
513
|
-
nextSessionState.stopMessageUpdatedAt = Date.now();
|
|
514
|
-
nextSessionState.stopMessageLastUsedAt = undefined;
|
|
515
|
-
nextSessionState.stopMessageStage = undefined;
|
|
516
|
-
nextSessionState.stopMessageSource = 'explicit';
|
|
517
|
-
nextSessionState.stopMessageObservationHash = undefined;
|
|
518
|
-
nextSessionState.stopMessageObservationStableCount = 0;
|
|
519
|
-
nextSessionState.stopMessageBdWorkState = undefined;
|
|
520
|
-
shouldPersistSessionState = true;
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
if (shouldPersistSessionState) {
|
|
525
|
-
this.routingInstructionState.set(stopMessageScope, nextSessionState);
|
|
526
|
-
persistRoutingInstructionState(stopMessageScope, nextSessionState, this.routingStateStore);
|
|
527
|
-
}
|
|
528
|
-
else {
|
|
529
|
-
nextSessionState = sessionState;
|
|
530
|
-
}
|
|
531
|
-
// 日志展示使用 session scope 的 stopMessage 状态,避免每次解析重复刷新时间/次数。
|
|
532
|
-
if (typeof nextSessionState.stopMessageText === 'string' ||
|
|
533
|
-
typeof nextSessionState.stopMessageMaxRepeats === 'number' ||
|
|
534
|
-
typeof nextSessionState.stopMessageStageMode === 'string') {
|
|
535
|
-
routingState.stopMessageText = nextSessionState.stopMessageText;
|
|
536
|
-
routingState.stopMessageMaxRepeats = nextSessionState.stopMessageMaxRepeats;
|
|
537
|
-
routingState.stopMessageUsed = nextSessionState.stopMessageUsed;
|
|
538
|
-
routingState.stopMessageUpdatedAt = nextSessionState.stopMessageUpdatedAt;
|
|
539
|
-
routingState.stopMessageLastUsedAt = nextSessionState.stopMessageLastUsedAt;
|
|
540
|
-
routingState.stopMessageStage = nextSessionState.stopMessageStage;
|
|
541
|
-
routingState.stopMessageStageMode = nextSessionState.stopMessageStageMode;
|
|
542
|
-
routingState.stopMessageObservationHash = nextSessionState.stopMessageObservationHash;
|
|
543
|
-
routingState.stopMessageObservationStableCount = nextSessionState.stopMessageObservationStableCount;
|
|
544
|
-
routingState.stopMessageBdWorkState = nextSessionState.stopMessageBdWorkState;
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
if (instructions.length > 0 && stopMessageScope) {
|
|
550
|
-
const hasPreCommandSet = instructions.some((entry) => entry.type === 'preCommandSet');
|
|
551
|
-
const hasPreCommandClear = instructions.some((entry) => entry.type === 'preCommandClear');
|
|
552
|
-
if (hasPreCommandSet || hasPreCommandClear) {
|
|
553
|
-
const sessionState = getRoutingInstructionState(stopMessageScope, this.routingInstructionState, this.routingStateStore);
|
|
554
|
-
const nextSessionState = {
|
|
555
|
-
...sessionState
|
|
556
|
-
};
|
|
557
|
-
let changed = false;
|
|
558
|
-
if (hasPreCommandClear) {
|
|
559
|
-
changed =
|
|
560
|
-
typeof sessionState.preCommandScriptPath === 'string' ||
|
|
561
|
-
typeof sessionState.preCommandSource === 'string' ||
|
|
562
|
-
typeof sessionState.preCommandUpdatedAt === 'number';
|
|
563
|
-
nextSessionState.preCommandScriptPath = undefined;
|
|
564
|
-
nextSessionState.preCommandSource = undefined;
|
|
565
|
-
nextSessionState.preCommandUpdatedAt = Date.now();
|
|
566
|
-
}
|
|
567
|
-
if (hasPreCommandSet) {
|
|
568
|
-
const scriptPath = typeof routingState.preCommandScriptPath === 'string' ? routingState.preCommandScriptPath.trim() : '';
|
|
569
|
-
if (scriptPath) {
|
|
570
|
-
if (sessionState.preCommandScriptPath !== scriptPath) {
|
|
571
|
-
changed = true;
|
|
572
|
-
}
|
|
573
|
-
nextSessionState.preCommandScriptPath = scriptPath;
|
|
574
|
-
nextSessionState.preCommandSource = 'explicit';
|
|
575
|
-
nextSessionState.preCommandUpdatedAt =
|
|
576
|
-
typeof routingState.preCommandUpdatedAt === 'number' && Number.isFinite(routingState.preCommandUpdatedAt)
|
|
577
|
-
? routingState.preCommandUpdatedAt
|
|
578
|
-
: Date.now();
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
if (changed) {
|
|
582
|
-
this.routingInstructionState.set(stopMessageScope, nextSessionState);
|
|
583
|
-
persistRoutingInstructionState(stopMessageScope, nextSessionState, this.routingStateStore);
|
|
584
|
-
routingState.preCommandScriptPath = nextSessionState.preCommandScriptPath;
|
|
585
|
-
routingState.preCommandSource = nextSessionState.preCommandSource;
|
|
586
|
-
routingState.preCommandUpdatedAt = nextSessionState.preCommandUpdatedAt;
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
if (instructions.length === 0 && stopMessageScope) {
|
|
591
|
-
const sessionState = getRoutingInstructionState(stopMessageScope, this.routingInstructionState, this.routingStateStore);
|
|
592
|
-
if (typeof sessionState.stopMessageText === 'string' ||
|
|
593
|
-
typeof sessionState.stopMessageMaxRepeats === 'number' ||
|
|
594
|
-
typeof sessionState.stopMessageStageMode === 'string') {
|
|
595
|
-
routingState.stopMessageText = sessionState.stopMessageText;
|
|
596
|
-
routingState.stopMessageMaxRepeats = sessionState.stopMessageMaxRepeats;
|
|
597
|
-
routingState.stopMessageUsed = sessionState.stopMessageUsed;
|
|
598
|
-
routingState.stopMessageUpdatedAt = sessionState.stopMessageUpdatedAt;
|
|
599
|
-
routingState.stopMessageLastUsedAt = sessionState.stopMessageLastUsedAt;
|
|
600
|
-
routingState.stopMessageStage = sessionState.stopMessageStage;
|
|
601
|
-
routingState.stopMessageStageMode = sessionState.stopMessageStageMode;
|
|
602
|
-
routingState.stopMessageObservationHash = sessionState.stopMessageObservationHash;
|
|
603
|
-
routingState.stopMessageObservationStableCount = sessionState.stopMessageObservationStableCount;
|
|
604
|
-
routingState.stopMessageBdWorkState = sessionState.stopMessageBdWorkState;
|
|
605
|
-
}
|
|
606
|
-
if (typeof sessionState.preCommandScriptPath === 'string' && sessionState.preCommandScriptPath.trim()) {
|
|
607
|
-
routingState.preCommandScriptPath = sessionState.preCommandScriptPath;
|
|
608
|
-
routingState.preCommandSource = sessionState.preCommandSource;
|
|
609
|
-
routingState.preCommandUpdatedAt = sessionState.preCommandUpdatedAt;
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
// Guardrail: if a session is restricted to providers that do not exist in any routing pools,
|
|
613
|
-
// we must not hard-fail the request loop. Auto-clear the allowlist and fall back to normal routing.
|
|
614
|
-
if (routingState.allowedProviders.size > 0) {
|
|
615
|
-
const providersInRouting = new Set();
|
|
616
|
-
for (const pools of Object.values(this.routing)) {
|
|
617
|
-
if (!Array.isArray(pools))
|
|
618
|
-
continue;
|
|
619
|
-
for (const pool of pools) {
|
|
620
|
-
if (!pool || !Array.isArray(pool.targets))
|
|
621
|
-
continue;
|
|
622
|
-
for (const key of pool.targets) {
|
|
623
|
-
if (typeof key !== 'string' || !key)
|
|
624
|
-
continue;
|
|
625
|
-
const providerId = extractProviderId(key);
|
|
626
|
-
if (providerId) {
|
|
627
|
-
providersInRouting.add(providerId);
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
const allowed = Array.from(routingState.allowedProviders).filter((provider) => typeof provider === 'string');
|
|
633
|
-
const hasIntersection = allowed.some((provider) => providersInRouting.has(provider));
|
|
634
|
-
if (!hasIntersection) {
|
|
635
|
-
routingState = {
|
|
636
|
-
...routingState,
|
|
637
|
-
allowedProviders: new Set()
|
|
638
|
-
};
|
|
639
|
-
this.routingInstructionState.set(stateKey, routingState);
|
|
640
|
-
persistRoutingInstructionState(stateKey, routingState, this.routingStateStore);
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
const features = buildRoutingFeatures(request, metadata);
|
|
644
|
-
const directProviderModel = this.parseDirectProviderModel(request?.model);
|
|
645
|
-
let classification;
|
|
646
|
-
let requestedRoute;
|
|
647
|
-
let selection;
|
|
648
|
-
const selectionDeps = {
|
|
649
|
-
routing: this.routing,
|
|
650
|
-
providerRegistry: this.providerRegistry,
|
|
651
|
-
healthManager: this.healthManager,
|
|
652
|
-
contextAdvisor: this.contextAdvisor,
|
|
653
|
-
loadBalancer: this.loadBalancer,
|
|
654
|
-
isProviderCoolingDown: (key) => this.isProviderCoolingDown(key),
|
|
655
|
-
resolveStickyKey: (m) => this.resolveStickyKey(m),
|
|
656
|
-
quotaView: this.quotaView
|
|
657
|
-
};
|
|
658
|
-
if (directProviderModel) {
|
|
659
|
-
const forceMediaFallback = this.shouldFallbackDirectModelForMedia(directProviderModel, features);
|
|
660
|
-
const providerKeys = this.providerRegistry.listProviderKeys(directProviderModel.providerId);
|
|
661
|
-
let hasModel = false;
|
|
662
|
-
for (const key of providerKeys) {
|
|
663
|
-
try {
|
|
664
|
-
const profile = this.providerRegistry.get(key);
|
|
665
|
-
if (profile?.modelId === directProviderModel.modelId) {
|
|
666
|
-
hasModel = true;
|
|
667
|
-
break;
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
catch {
|
|
671
|
-
continue;
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
if (!hasModel) {
|
|
675
|
-
throw new VirtualRouterError(`Unknown model ${directProviderModel.modelId} for provider ${directProviderModel.providerId}`, VirtualRouterErrorCode.CONFIG_ERROR, { providerId: directProviderModel.providerId, modelId: directProviderModel.modelId });
|
|
676
|
-
}
|
|
677
|
-
if (!forceMediaFallback) {
|
|
678
|
-
const directSelection = selectDirectProviderModel(directProviderModel.providerId, directProviderModel.modelId, metadata, features, routingState, selectionDeps);
|
|
679
|
-
if (!directSelection) {
|
|
680
|
-
throw new VirtualRouterError(`All providers unavailable for model ${directProviderModel.providerId}.${directProviderModel.modelId}`, VirtualRouterErrorCode.PROVIDER_NOT_AVAILABLE, { providerId: directProviderModel.providerId, modelId: directProviderModel.modelId });
|
|
681
|
-
}
|
|
682
|
-
classification = {
|
|
683
|
-
routeName: 'direct',
|
|
684
|
-
confidence: 1,
|
|
685
|
-
reasoning: `direct_model:${directProviderModel.providerId}.${directProviderModel.modelId}`,
|
|
686
|
-
fallback: false,
|
|
687
|
-
candidates: ['direct']
|
|
688
|
-
};
|
|
689
|
-
requestedRoute = 'direct';
|
|
690
|
-
selection = directSelection;
|
|
691
|
-
}
|
|
692
|
-
else {
|
|
693
|
-
classification = this.classifier.classify(features);
|
|
694
|
-
requestedRoute = this.normalizeRouteAlias(classification.routeName || DEFAULT_ROUTE);
|
|
695
|
-
selection = this.selectProvider(requestedRoute, metadata, classification, features, routingState);
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
else {
|
|
699
|
-
// Prefer target (from "<**!provider.model**>") is evaluated before routing classification.
|
|
700
|
-
const preferTarget = routingState.preferTarget;
|
|
701
|
-
if (preferTarget && typeof preferTarget.provider === 'string' && preferTarget.provider.trim()) {
|
|
702
|
-
const providerId = preferTarget.provider.trim();
|
|
703
|
-
const keyAlias = typeof preferTarget.keyAlias === 'string' ? preferTarget.keyAlias.trim() : '';
|
|
704
|
-
const modelId = typeof preferTarget.model === 'string' ? preferTarget.model.trim() : '';
|
|
705
|
-
const keyIndex = typeof preferTarget.keyIndex === 'number' && Number.isFinite(preferTarget.keyIndex)
|
|
706
|
-
? Math.floor(preferTarget.keyIndex)
|
|
707
|
-
: undefined;
|
|
708
|
-
const candidateKeys = [];
|
|
709
|
-
if (keyIndex !== undefined && keyIndex > 0) {
|
|
710
|
-
const runtimeKey = this.providerRegistry.resolveRuntimeKeyByIndex(providerId, keyIndex);
|
|
711
|
-
if (runtimeKey) {
|
|
712
|
-
candidateKeys.push(runtimeKey);
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
else if (modelId) {
|
|
716
|
-
const allKeys = this.providerRegistry.listProviderKeys(providerId);
|
|
717
|
-
for (const key of allKeys) {
|
|
718
|
-
if (keyAlias) {
|
|
719
|
-
const prefix = `${providerId}.${keyAlias}.`;
|
|
720
|
-
if (!key.startsWith(prefix)) {
|
|
721
|
-
continue;
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
try {
|
|
725
|
-
const profile = this.providerRegistry.get(key);
|
|
726
|
-
if (profile?.modelId === modelId) {
|
|
727
|
-
candidateKeys.push(key);
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
catch {
|
|
731
|
-
continue;
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
}
|
|
735
|
-
const allowAliasRotation = !keyAlias && keyIndex === undefined;
|
|
736
|
-
const eligibleKeys = (() => {
|
|
737
|
-
if (candidateKeys.length === 0) {
|
|
738
|
-
return [];
|
|
739
|
-
}
|
|
740
|
-
const quotaView = selectionDeps.quotaView;
|
|
741
|
-
const now = quotaView ? Date.now() : 0;
|
|
742
|
-
return candidateKeys.filter((key) => {
|
|
743
|
-
if (!quotaView) {
|
|
744
|
-
if (this.isProviderCoolingDown(key)) {
|
|
745
|
-
return false;
|
|
746
|
-
}
|
|
747
|
-
if (!this.healthManager.isAvailable(key)) {
|
|
748
|
-
return false;
|
|
749
|
-
}
|
|
750
|
-
return true;
|
|
751
|
-
}
|
|
752
|
-
const entry = quotaView(key);
|
|
753
|
-
if (!entry) {
|
|
754
|
-
return true;
|
|
755
|
-
}
|
|
756
|
-
if (!entry.inPool) {
|
|
757
|
-
return false;
|
|
758
|
-
}
|
|
759
|
-
if (entry.cooldownUntil && entry.cooldownUntil > now) {
|
|
760
|
-
return false;
|
|
761
|
-
}
|
|
762
|
-
if (entry.blacklistUntil && entry.blacklistUntil > now) {
|
|
763
|
-
return false;
|
|
764
|
-
}
|
|
765
|
-
return true;
|
|
766
|
-
});
|
|
767
|
-
})();
|
|
768
|
-
const preferSelection = eligibleKeys.length > 0
|
|
769
|
-
? selectFromStickyPool(new Set(eligibleKeys), metadata, features, routingState, selectionDeps, {
|
|
770
|
-
allowAliasRotation
|
|
771
|
-
})
|
|
772
|
-
: null;
|
|
773
|
-
if (preferSelection) {
|
|
774
|
-
classification = {
|
|
775
|
-
routeName: 'prefer',
|
|
776
|
-
confidence: 1,
|
|
777
|
-
reasoning: keyIndex !== undefined ? `prefer_key:${providerId}.${keyIndex}` : `prefer_model:${providerId}.${modelId}`,
|
|
778
|
-
fallback: false,
|
|
779
|
-
candidates: ['prefer']
|
|
780
|
-
};
|
|
781
|
-
requestedRoute = 'prefer';
|
|
782
|
-
selection = {
|
|
783
|
-
...preferSelection,
|
|
784
|
-
routeUsed: 'prefer',
|
|
785
|
-
poolId: 'prefer-primary'
|
|
786
|
-
};
|
|
787
|
-
}
|
|
788
|
-
else if (routingState.preferTarget) {
|
|
789
|
-
// Auto-clear only when the target becomes invalid or blocked by explicit routing instructions.
|
|
790
|
-
// Do NOT clear for temporary unavailability (e.g. 429 cooldown, quota cooldown, transient health).
|
|
791
|
-
const shouldAutoClear = (() => {
|
|
792
|
-
if (candidateKeys.length === 0) {
|
|
793
|
-
return true;
|
|
794
|
-
}
|
|
795
|
-
// Prefer selection failed despite eligible keys existing: treat as a hard block (e.g. routing rules).
|
|
796
|
-
if (eligibleKeys.length > 0) {
|
|
797
|
-
return true;
|
|
798
|
-
}
|
|
799
|
-
// If quota explicitly marks the preferred target as out-of-pool, clear the prefer instruction so
|
|
800
|
-
// the router can fall back to other targets without repeatedly retrying an impossible preference.
|
|
801
|
-
if (selectionDeps.quotaView) {
|
|
802
|
-
for (const key of candidateKeys) {
|
|
803
|
-
const entry = selectionDeps.quotaView(key);
|
|
804
|
-
if (entry && entry.inPool === false) {
|
|
805
|
-
return true;
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
return false;
|
|
810
|
-
})();
|
|
811
|
-
if (shouldAutoClear) {
|
|
812
|
-
routingState = {
|
|
813
|
-
...routingState,
|
|
814
|
-
preferTarget: undefined
|
|
815
|
-
};
|
|
816
|
-
this.routingInstructionState.set(stateKey, routingState);
|
|
817
|
-
persistRoutingInstructionState(stateKey, routingState, this.routingStateStore);
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
if (!selection) {
|
|
822
|
-
classification = metadata.routeHint && metadata.routeHint.trim()
|
|
823
|
-
? {
|
|
824
|
-
routeName: metadata.routeHint.trim(),
|
|
825
|
-
confidence: 1,
|
|
826
|
-
reasoning: `route_hint:${metadata.routeHint.trim()}`,
|
|
827
|
-
fallback: false,
|
|
828
|
-
candidates: [metadata.routeHint.trim()]
|
|
829
|
-
}
|
|
830
|
-
: this.classifier.classify(features);
|
|
831
|
-
requestedRoute = this.normalizeRouteAlias(classification.routeName || DEFAULT_ROUTE);
|
|
832
|
-
selection = this.selectProvider(requestedRoute, metadata, classification, features, routingState);
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
const baseTarget = this.providerRegistry.buildTarget(selection.providerKey);
|
|
836
|
-
const forceVision = selection.routeUsed === 'vision' && this.routeHasForceFlag('vision');
|
|
837
|
-
const target = {
|
|
838
|
-
...baseTarget,
|
|
839
|
-
...(this.webSearchForce ? { forceWebSearch: true } : {}),
|
|
840
|
-
...(forceVision ? { forceVision: true } : {})
|
|
841
|
-
};
|
|
842
|
-
const instructionProcessMode = this.resolveInstructionProcessModeForSelection(selection.providerKey, routingState);
|
|
843
|
-
if (instructionProcessMode) {
|
|
844
|
-
target.processMode = instructionProcessMode;
|
|
845
|
-
}
|
|
846
|
-
recordAntigravitySessionLease({
|
|
847
|
-
metadata: features.metadata,
|
|
848
|
-
providerKey: selection.providerKey,
|
|
849
|
-
sessionKey: this.resolveSessionScope(features.metadata),
|
|
850
|
-
providerRegistry: this.providerRegistry,
|
|
851
|
-
leaseStore: this.stickySessionManager.getAllStores().aliasLeaseStore,
|
|
852
|
-
sessionAliasStore: this.stickySessionManager.getAllStores().sessionAliasStore,
|
|
853
|
-
persistence: this.antigravityLeasePersistence,
|
|
854
|
-
aliasReuseCooldownMs: this.stickySessionManager.getAliasReuseCooldownMs(),
|
|
855
|
-
commitSessionBinding: false,
|
|
856
|
-
debug: this.debug
|
|
857
|
-
});
|
|
858
|
-
const routingMode = resolveRoutingMode([...metadataInstructions, ...instructions], routingState);
|
|
859
|
-
const hitReason = buildHitReason(selection.routeUsed, selection.providerKey, classification, features, routingMode, { providerRegistry: this.providerRegistry, contextRouting: this.contextRouting });
|
|
860
|
-
const stickyScope = routingMode !== 'none' ? this.resolveSessionScope(metadata) : undefined;
|
|
861
|
-
const routeForLog = routingMode === 'sticky' ? 'sticky' : selection.routeUsed;
|
|
862
|
-
const hitRecord = createVirtualRouterHitRecord({
|
|
863
|
-
routeName: routeForLog,
|
|
864
|
-
poolId: selection.poolId,
|
|
865
|
-
providerKey: selection.providerKey,
|
|
866
|
-
modelId: target.modelId || undefined,
|
|
867
|
-
hitReason,
|
|
868
|
-
stickyScope,
|
|
869
|
-
routingState,
|
|
870
|
-
requestTokens: features.estimatedTokens,
|
|
871
|
-
selectionPenalty: this.resolveSelectionPenalty(selection.providerKey)
|
|
872
|
-
});
|
|
873
|
-
this.routeAnalytics.incrementRouteStat(selection.routeUsed, selection.providerKey, hitRecord);
|
|
49
|
+
const parseLog = buildRoutingInstructionParseLog(request, metadata);
|
|
50
|
+
const nativeMetadata = injectRuntimeNowMs(metadata);
|
|
51
|
+
let raw;
|
|
874
52
|
try {
|
|
875
|
-
this.
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
}
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
diagnostics: {
|
|
903
|
-
routeName: selection.routeUsed,
|
|
904
|
-
providerKey: selection.providerKey,
|
|
905
|
-
pool: selection.pool,
|
|
906
|
-
poolId: selection.poolId,
|
|
907
|
-
reasoning: classification.reasoning,
|
|
908
|
-
fallback: didFallback,
|
|
909
|
-
confidence: classification.confidence
|
|
910
|
-
}
|
|
911
|
-
};
|
|
53
|
+
raw = this.nativeProxy.route(JSON.stringify(request), JSON.stringify(nativeMetadata));
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
throw normalizeNativeVirtualRouterError(error);
|
|
57
|
+
}
|
|
58
|
+
if (typeof raw !== 'string') {
|
|
59
|
+
throw normalizeNativeVirtualRouterError(raw);
|
|
60
|
+
}
|
|
61
|
+
if (raw.startsWith('Error:') || raw.startsWith(VIRTUAL_ROUTER_ERROR_PREFIX)) {
|
|
62
|
+
throw normalizeNativeVirtualRouterError(raw);
|
|
63
|
+
}
|
|
64
|
+
const parsed = JSON.parse(raw);
|
|
65
|
+
emitRoutingInstructionParseLog(parseLog);
|
|
66
|
+
// Keep legacy observable behavior for callers/tests that inspect the request object
|
|
67
|
+
// after route(): instruction markers are stripped from forwarded payload structures.
|
|
68
|
+
cleanRoutingInstructionMarkersInPlace(request);
|
|
69
|
+
const stopScope = parseLog?.stopScope || resolveStopMessageScope(metadata);
|
|
70
|
+
const stopState = stopScope ? this.getStopMessageState(metadata) : null;
|
|
71
|
+
const forceStopStatusLabel = Boolean(parseLog?.stopMessageTypes.length ||
|
|
72
|
+
parseLog?.scopedTypes.some((type) => type === 'stopMessageSet' || type === 'stopMessageMode' || type === 'stopMessageClear'));
|
|
73
|
+
emitVirtualRouterHitLog(parsed, {
|
|
74
|
+
requestId: metadata.requestId,
|
|
75
|
+
stopScope,
|
|
76
|
+
stopState,
|
|
77
|
+
forceStopStatusLabel
|
|
78
|
+
});
|
|
79
|
+
return parsed;
|
|
912
80
|
}
|
|
913
81
|
getStopMessageState(metadata) {
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
-
return false;
|
|
917
|
-
}
|
|
918
|
-
const text = typeof candidate.stopMessageText === 'string' ? candidate.stopMessageText.trim() : '';
|
|
919
|
-
const maxRepeats = typeof candidate.stopMessageMaxRepeats === 'number' && Number.isFinite(candidate.stopMessageMaxRepeats)
|
|
920
|
-
? Math.max(1, Math.floor(candidate.stopMessageMaxRepeats))
|
|
921
|
-
: 0;
|
|
922
|
-
const mode = typeof candidate.stopMessageStageMode === 'string'
|
|
923
|
-
? candidate.stopMessageStageMode.trim().toLowerCase()
|
|
924
|
-
: '';
|
|
925
|
-
const allowModeOnly = !text && maxRepeats > 0 && (mode === 'on' || mode === 'auto');
|
|
926
|
-
return (Boolean(text) || allowModeOnly) && maxRepeats > 0;
|
|
927
|
-
};
|
|
928
|
-
const sessionScope = this.resolveSessionScope(metadata);
|
|
929
|
-
const sessionState = sessionScope
|
|
930
|
-
? getRoutingInstructionState(sessionScope, this.routingInstructionState, this.routingStateStore)
|
|
931
|
-
: null;
|
|
932
|
-
const stickyKey = this.resolveStickyKey(metadata);
|
|
933
|
-
const stickyState = stickyKey
|
|
934
|
-
? getRoutingInstructionState(stickyKey, this.routingInstructionState, this.routingStateStore)
|
|
935
|
-
: null;
|
|
936
|
-
const effectiveState = hasArmedStopState(sessionState)
|
|
937
|
-
? sessionState
|
|
938
|
-
: hasArmedStopState(stickyState)
|
|
939
|
-
? stickyState
|
|
940
|
-
: null;
|
|
941
|
-
if (!effectiveState) {
|
|
942
|
-
return null;
|
|
943
|
-
}
|
|
944
|
-
const text = typeof effectiveState.stopMessageText === 'string' ? effectiveState.stopMessageText.trim() : '';
|
|
945
|
-
const maxRepeats = typeof effectiveState.stopMessageMaxRepeats === 'number' &&
|
|
946
|
-
Number.isFinite(effectiveState.stopMessageMaxRepeats)
|
|
947
|
-
? Math.max(1, Math.floor(effectiveState.stopMessageMaxRepeats))
|
|
948
|
-
: 0;
|
|
949
|
-
const stageModeRaw = typeof effectiveState.stopMessageStageMode === 'string'
|
|
950
|
-
? effectiveState.stopMessageStageMode.trim().toLowerCase()
|
|
951
|
-
: '';
|
|
952
|
-
const stageModeNormalized = stageModeRaw === 'on' || stageModeRaw === 'off' || stageModeRaw === 'auto'
|
|
953
|
-
? stageModeRaw
|
|
954
|
-
: undefined;
|
|
955
|
-
const allowModeOnly = !text && maxRepeats > 0 && (stageModeNormalized === 'on' || stageModeNormalized === 'auto');
|
|
956
|
-
if ((!text && !allowModeOnly) || maxRepeats <= 0) {
|
|
957
|
-
return null;
|
|
958
|
-
}
|
|
959
|
-
return {
|
|
960
|
-
...(text ? { stopMessageText: text } : {}),
|
|
961
|
-
stopMessageMaxRepeats: maxRepeats,
|
|
962
|
-
...(typeof effectiveState.stopMessageSource === 'string' && effectiveState.stopMessageSource.trim()
|
|
963
|
-
? { stopMessageSource: effectiveState.stopMessageSource.trim() }
|
|
964
|
-
: {}),
|
|
965
|
-
...(typeof effectiveState.stopMessageUsed === 'number' && Number.isFinite(effectiveState.stopMessageUsed)
|
|
966
|
-
? { stopMessageUsed: Math.max(0, Math.floor(effectiveState.stopMessageUsed)) }
|
|
967
|
-
: {}),
|
|
968
|
-
...(typeof effectiveState.stopMessageUpdatedAt === 'number' &&
|
|
969
|
-
Number.isFinite(effectiveState.stopMessageUpdatedAt)
|
|
970
|
-
? { stopMessageUpdatedAt: effectiveState.stopMessageUpdatedAt }
|
|
971
|
-
: {}),
|
|
972
|
-
...(typeof effectiveState.stopMessageLastUsedAt === 'number' &&
|
|
973
|
-
Number.isFinite(effectiveState.stopMessageLastUsedAt)
|
|
974
|
-
? { stopMessageLastUsedAt: effectiveState.stopMessageLastUsedAt }
|
|
975
|
-
: {}),
|
|
976
|
-
...(typeof effectiveState.stopMessageStage === 'string' && effectiveState.stopMessageStage.trim()
|
|
977
|
-
? { stopMessageStage: effectiveState.stopMessageStage.trim() }
|
|
978
|
-
: {}),
|
|
979
|
-
...(stageModeNormalized ? { stopMessageStageMode: stageModeNormalized } : {}),
|
|
980
|
-
...(typeof effectiveState.stopMessageObservationHash === 'string' && effectiveState.stopMessageObservationHash.trim()
|
|
981
|
-
? { stopMessageObservationHash: effectiveState.stopMessageObservationHash.trim() }
|
|
982
|
-
: {}),
|
|
983
|
-
...(typeof effectiveState.stopMessageObservationStableCount === 'number' &&
|
|
984
|
-
Number.isFinite(effectiveState.stopMessageObservationStableCount)
|
|
985
|
-
? { stopMessageObservationStableCount: Math.max(0, Math.floor(effectiveState.stopMessageObservationStableCount)) }
|
|
986
|
-
: {}),
|
|
987
|
-
...(typeof effectiveState.stopMessageBdWorkState === 'string' && effectiveState.stopMessageBdWorkState.trim()
|
|
988
|
-
? { stopMessageBdWorkState: effectiveState.stopMessageBdWorkState.trim() }
|
|
989
|
-
: {})
|
|
990
|
-
};
|
|
82
|
+
const raw = this.nativeProxy.getStopMessageState(JSON.stringify(metadata));
|
|
83
|
+
return JSON.parse(raw);
|
|
991
84
|
}
|
|
992
85
|
getPreCommandState(metadata) {
|
|
993
|
-
const
|
|
994
|
-
|
|
995
|
-
? getRoutingInstructionState(sessionScope, this.routingInstructionState, this.routingStateStore)
|
|
996
|
-
: null;
|
|
997
|
-
const stickyKey = this.resolveStickyKey(metadata);
|
|
998
|
-
const stickyState = stickyKey
|
|
999
|
-
? getRoutingInstructionState(stickyKey, this.routingInstructionState, this.routingStateStore)
|
|
1000
|
-
: null;
|
|
1001
|
-
const effectiveState = sessionState && typeof sessionState.preCommandScriptPath === 'string' && sessionState.preCommandScriptPath.trim()
|
|
1002
|
-
? sessionState
|
|
1003
|
-
: stickyState;
|
|
1004
|
-
if (!effectiveState) {
|
|
1005
|
-
return null;
|
|
1006
|
-
}
|
|
1007
|
-
const scriptPath = typeof effectiveState.preCommandScriptPath === 'string' ? effectiveState.preCommandScriptPath.trim() : '';
|
|
1008
|
-
if (!scriptPath) {
|
|
1009
|
-
return null;
|
|
1010
|
-
}
|
|
1011
|
-
return {
|
|
1012
|
-
preCommandScriptPath: scriptPath,
|
|
1013
|
-
...(typeof effectiveState.preCommandSource === 'string' && effectiveState.preCommandSource.trim()
|
|
1014
|
-
? { preCommandSource: effectiveState.preCommandSource.trim() }
|
|
1015
|
-
: {}),
|
|
1016
|
-
...(typeof effectiveState.preCommandUpdatedAt === 'number' && Number.isFinite(effectiveState.preCommandUpdatedAt)
|
|
1017
|
-
? { preCommandUpdatedAt: effectiveState.preCommandUpdatedAt }
|
|
1018
|
-
: {})
|
|
1019
|
-
};
|
|
86
|
+
const raw = this.nativeProxy.getPreCommandState(JSON.stringify(metadata));
|
|
87
|
+
return JSON.parse(raw);
|
|
1020
88
|
}
|
|
1021
89
|
handleProviderFailure(event) {
|
|
1022
|
-
|
|
90
|
+
this.nativeProxy.handleProviderFailure(JSON.stringify(event));
|
|
1023
91
|
}
|
|
1024
92
|
handleProviderError(event) {
|
|
1025
|
-
|
|
1026
|
-
try {
|
|
1027
|
-
this.healthStore.recordProviderError(event);
|
|
1028
|
-
}
|
|
1029
|
-
catch {
|
|
1030
|
-
// ignore persistence errors
|
|
1031
|
-
}
|
|
1032
|
-
}
|
|
1033
|
-
// Quota routing mode: health/cooldown must be driven by quotaView only (host/core quota center).
|
|
1034
|
-
// VirtualRouter must not produce or persist its own cooldown state in this mode.
|
|
1035
|
-
// 当 Host 注入 quotaView 时,VirtualRouter 的入池/优先级决策应以 quota 为准;
|
|
1036
|
-
// 此时不再在 engine-health 内部进行 429/backoff/series cooldown 等健康决策,
|
|
1037
|
-
// 以避免与 daemon/quota-center 的长期熔断策略重复维护并导致日志噪声。
|
|
1038
|
-
if (this.quotaView) {
|
|
1039
|
-
return;
|
|
1040
|
-
}
|
|
1041
|
-
// Antigravity account safety policy uses router-local cooldown TTLs; only applies when quota routing is disabled.
|
|
1042
|
-
applyAntigravityRiskPolicyImpl(event, this.providerRegistry, this.healthManager, (key, ttl) => this.markProviderCooldown(key, ttl), this.debug);
|
|
1043
|
-
// 配额恢复事件优先处理:一旦识别到 virtualRouterQuotaRecovery,
|
|
1044
|
-
// 直接清理健康状态/冷却 TTL,避免继续走常规错误映射逻辑。
|
|
1045
|
-
const handledByQuota = applyQuotaRecoveryImpl(event, this.healthManager, (key) => this.clearProviderCooldown(key), this.debug);
|
|
1046
|
-
if (handledByQuota) {
|
|
1047
|
-
return;
|
|
1048
|
-
}
|
|
1049
|
-
const handledByQuotaDepleted = applyQuotaDepletedImpl(event, this.healthManager, (key, ttl) => this.markProviderCooldown(key, ttl), this.debug);
|
|
1050
|
-
if (handledByQuotaDepleted) {
|
|
1051
|
-
return;
|
|
1052
|
-
}
|
|
1053
|
-
applySeriesCooldownImpl(event, this.providerRegistry, this.healthManager, (key, ttl) => this.markProviderCooldown(key, ttl), this.debug);
|
|
1054
|
-
const derived = mapProviderErrorImpl(event, this.providerHealthConfig());
|
|
1055
|
-
if (!derived) {
|
|
1056
|
-
return;
|
|
1057
|
-
}
|
|
1058
|
-
this.handleProviderFailure(derived);
|
|
93
|
+
this.nativeProxy.handleProviderError(JSON.stringify(event));
|
|
1059
94
|
}
|
|
1060
95
|
handleProviderSuccess(event) {
|
|
1061
|
-
|
|
1062
|
-
return;
|
|
1063
|
-
}
|
|
1064
|
-
const providerKey = event.runtime && typeof event.runtime.providerKey === 'string' ? event.runtime.providerKey.trim() : '';
|
|
1065
|
-
if (!providerKey) {
|
|
1066
|
-
return;
|
|
1067
|
-
}
|
|
1068
|
-
const metadata = event.metadata && typeof event.metadata === 'object' && !Array.isArray(event.metadata)
|
|
1069
|
-
? event.metadata
|
|
1070
|
-
: null;
|
|
1071
|
-
if (!metadata) {
|
|
1072
|
-
return;
|
|
1073
|
-
}
|
|
1074
|
-
if (providerKey.toLowerCase().startsWith('antigravity.')) {
|
|
1075
|
-
recordAntigravitySessionLease({
|
|
1076
|
-
metadata: metadata,
|
|
1077
|
-
providerKey,
|
|
1078
|
-
sessionKey: this.resolveSessionScope(metadata),
|
|
1079
|
-
providerRegistry: this.providerRegistry,
|
|
1080
|
-
leaseStore: this.stickySessionManager.getAllStores().aliasLeaseStore,
|
|
1081
|
-
sessionAliasStore: this.stickySessionManager.getAllStores().sessionAliasStore,
|
|
1082
|
-
persistence: this.antigravityLeasePersistence,
|
|
1083
|
-
aliasReuseCooldownMs: this.stickySessionManager.getAliasReuseCooldownMs(),
|
|
1084
|
-
commitSessionBinding: true,
|
|
1085
|
-
debug: this.debug
|
|
1086
|
-
});
|
|
1087
|
-
}
|
|
96
|
+
this.nativeProxy.handleProviderSuccess(JSON.stringify(event));
|
|
1088
97
|
}
|
|
1089
98
|
getStatus() {
|
|
1090
|
-
|
|
1091
|
-
for (const [route, pools] of Object.entries(this.routing)) {
|
|
1092
|
-
const stats = this.routeAnalytics.getRouteStats(route);
|
|
1093
|
-
routes[route] = {
|
|
1094
|
-
providers: this.flattenPoolTargets(pools),
|
|
1095
|
-
hits: stats?.hits ?? 0,
|
|
1096
|
-
lastUsedProvider: stats?.lastProvider,
|
|
1097
|
-
...(stats?.lastHit ? { lastHit: { ...stats.lastHit } } : {})
|
|
1098
|
-
};
|
|
1099
|
-
}
|
|
1100
|
-
return {
|
|
1101
|
-
routes,
|
|
1102
|
-
health: this.healthManager.getSnapshot()
|
|
1103
|
-
};
|
|
1104
|
-
}
|
|
1105
|
-
/**
|
|
1106
|
-
* 将分类器产生的逻辑路由名直接归一化为配置中的路由键。
|
|
1107
|
-
* 不再维护 "websearch" 之类的别名,调用方应显式使用 "web_search" 或 "search" 等实际路由名。
|
|
1108
|
-
*/
|
|
1109
|
-
normalizeRouteAlias(routeName) {
|
|
1110
|
-
const base = routeName && routeName.trim() ? routeName.trim() : DEFAULT_ROUTE;
|
|
1111
|
-
return base;
|
|
1112
|
-
}
|
|
1113
|
-
validateConfig(config) {
|
|
1114
|
-
if (!config.routing || typeof config.routing !== 'object') {
|
|
1115
|
-
throw new VirtualRouterError('routing configuration is required', VirtualRouterErrorCode.CONFIG_ERROR);
|
|
1116
|
-
}
|
|
1117
|
-
if (!config.providers || Object.keys(config.providers).length === 0) {
|
|
1118
|
-
throw new VirtualRouterError('providers configuration is required', VirtualRouterErrorCode.CONFIG_ERROR);
|
|
1119
|
-
}
|
|
1120
|
-
const defaultPools = config.routing[DEFAULT_ROUTE];
|
|
1121
|
-
if (!this.routeHasTargets(defaultPools)) {
|
|
1122
|
-
throw new VirtualRouterError('default route must be configured with at least one provider', VirtualRouterErrorCode.CONFIG_ERROR);
|
|
1123
|
-
}
|
|
1124
|
-
if (!this.hasPrimaryPool(defaultPools)) {
|
|
1125
|
-
throw new VirtualRouterError('default route must define at least one non-backup pool', VirtualRouterErrorCode.CONFIG_ERROR);
|
|
1126
|
-
}
|
|
1127
|
-
const providerKeys = new Set(Object.keys(config.providers));
|
|
1128
|
-
for (const [routeName, pools] of Object.entries(config.routing)) {
|
|
1129
|
-
if (!this.routeHasTargets(pools)) {
|
|
1130
|
-
if (routeName === DEFAULT_ROUTE) {
|
|
1131
|
-
throw new VirtualRouterError('default route cannot be empty', VirtualRouterErrorCode.CONFIG_ERROR);
|
|
1132
|
-
}
|
|
1133
|
-
continue;
|
|
1134
|
-
}
|
|
1135
|
-
for (const pool of pools) {
|
|
1136
|
-
if (!Array.isArray(pool.targets) || !pool.targets.length) {
|
|
1137
|
-
continue;
|
|
1138
|
-
}
|
|
1139
|
-
for (const providerKey of pool.targets) {
|
|
1140
|
-
if (!providerKeys.has(providerKey)) {
|
|
1141
|
-
throw new VirtualRouterError(`Route ${routeName} references unknown provider ${providerKey}`, VirtualRouterErrorCode.CONFIG_ERROR);
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
}
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
selectProvider(requestedRoute, metadata, classification, features, routingState) {
|
|
1148
|
-
const activeState = routingState ||
|
|
1149
|
-
getRoutingInstructionState(this.resolveStickyKey(metadata), this.routingInstructionState, this.routingStateStore);
|
|
1150
|
-
return selectProviderImpl(requestedRoute, metadata, classification, features, activeState, {
|
|
1151
|
-
routing: this.routing,
|
|
1152
|
-
providerRegistry: this.providerRegistry,
|
|
1153
|
-
healthManager: this.healthManager,
|
|
1154
|
-
contextAdvisor: this.contextAdvisor,
|
|
1155
|
-
loadBalancer: this.loadBalancer,
|
|
1156
|
-
isProviderCoolingDown: (key) => this.isProviderCoolingDown(key),
|
|
1157
|
-
getProviderCooldownRemainingMs: (key) => this.getProviderCooldownRemainingMs(key),
|
|
1158
|
-
resolveStickyKey: (m) => this.resolveStickyKey(m),
|
|
1159
|
-
quotaView: this.quotaView,
|
|
1160
|
-
aliasQueueStore: this.stickySessionManager.getAllStores().aliasQueueStore,
|
|
1161
|
-
antigravityAliasLeaseStore: this.stickySessionManager.getAllStores().aliasLeaseStore,
|
|
1162
|
-
antigravitySessionAliasStore: this.stickySessionManager.getAllStores().sessionAliasStore,
|
|
1163
|
-
antigravityAliasReuseCooldownMs: this.stickySessionManager.getAliasReuseCooldownMs()
|
|
1164
|
-
}, { routingState });
|
|
1165
|
-
}
|
|
1166
|
-
resolveSelectionPenalty(providerKey) {
|
|
1167
|
-
if (!this.quotaView) {
|
|
1168
|
-
return undefined;
|
|
1169
|
-
}
|
|
1170
|
-
try {
|
|
1171
|
-
const entry = this.quotaView(providerKey);
|
|
1172
|
-
const raw = entry?.selectionPenalty;
|
|
1173
|
-
if (typeof raw !== 'number' || !Number.isFinite(raw) || raw <= 0) {
|
|
1174
|
-
return undefined;
|
|
1175
|
-
}
|
|
1176
|
-
return Math.floor(raw);
|
|
1177
|
-
}
|
|
1178
|
-
catch {
|
|
1179
|
-
return undefined;
|
|
1180
|
-
}
|
|
99
|
+
return JSON.parse(this.nativeProxy.getStatus());
|
|
1181
100
|
}
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
return
|
|
1187
|
-
}
|
|
1188
|
-
resolveSessionScope(metadata) {
|
|
1189
|
-
return resolveSessionScopeImpl(metadata);
|
|
101
|
+
}
|
|
102
|
+
const VIRTUAL_ROUTER_ERROR_PREFIX = 'VIRTUAL_ROUTER_ERROR:';
|
|
103
|
+
function normalizeNativeVirtualRouterError(error) {
|
|
104
|
+
if (error instanceof VirtualRouterError) {
|
|
105
|
+
return error;
|
|
1190
106
|
}
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
routingState.preferTarget
|
|
1196
|
-
];
|
|
1197
|
-
for (const candidate of candidates) {
|
|
1198
|
-
const processMode = candidate?.processMode;
|
|
1199
|
-
if (!processMode) {
|
|
1200
|
-
continue;
|
|
1201
|
-
}
|
|
1202
|
-
const resolved = this.resolveInstructionTarget(candidate);
|
|
1203
|
-
if (!resolved) {
|
|
1204
|
-
continue;
|
|
1205
|
-
}
|
|
1206
|
-
if (resolved.keys.includes(providerKey)) {
|
|
1207
|
-
return processMode;
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
|
-
return undefined;
|
|
107
|
+
const message = extractNativeErrorMessage(error);
|
|
108
|
+
const parsed = parseVirtualRouterErrorMessage(message);
|
|
109
|
+
if (parsed) {
|
|
110
|
+
return new VirtualRouterError(parsed.message, parsed.code);
|
|
1211
111
|
}
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
return null;
|
|
1215
|
-
}
|
|
1216
|
-
const providerId = target.provider;
|
|
1217
|
-
const providerKeys = this.providerRegistry.listProviderKeys(providerId);
|
|
1218
|
-
if (providerKeys.length === 0) {
|
|
1219
|
-
return null;
|
|
1220
|
-
}
|
|
1221
|
-
const alias = typeof target.keyAlias === 'string' ? target.keyAlias.trim() : '';
|
|
1222
|
-
const aliasExplicit = alias.length > 0 && target.pathLength === 3;
|
|
1223
|
-
if (aliasExplicit) {
|
|
1224
|
-
const runtimeKey = this.providerRegistry.resolveRuntimeKeyByAlias(providerId, alias);
|
|
1225
|
-
if (runtimeKey) {
|
|
1226
|
-
return { mode: 'exact', keys: [runtimeKey] };
|
|
1227
|
-
}
|
|
1228
|
-
}
|
|
1229
|
-
if (typeof target.keyIndex === 'number' && target.keyIndex > 0) {
|
|
1230
|
-
const runtimeKey = this.providerRegistry.resolveRuntimeKeyByIndex(providerId, target.keyIndex);
|
|
1231
|
-
if (runtimeKey) {
|
|
1232
|
-
return { mode: 'exact', keys: [runtimeKey] };
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
if (target.model && target.model.trim()) {
|
|
1236
|
-
const normalizedModel = target.model.trim();
|
|
1237
|
-
const matchingKeys = providerKeys.filter((key) => {
|
|
1238
|
-
const modelId = getProviderModelId(key, this.providerRegistry);
|
|
1239
|
-
return modelId === normalizedModel;
|
|
1240
|
-
});
|
|
1241
|
-
if (matchingKeys.length > 0) {
|
|
1242
|
-
return { mode: 'filter', keys: matchingKeys };
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
if (alias && !aliasExplicit) {
|
|
1246
|
-
const legacyKey = this.providerRegistry.resolveRuntimeKeyByAlias(providerId, alias);
|
|
1247
|
-
if (legacyKey) {
|
|
1248
|
-
return { mode: 'exact', keys: [legacyKey] };
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
return { mode: 'filter', keys: providerKeys };
|
|
112
|
+
if (isVirtualRouterErrorLike(error)) {
|
|
113
|
+
return new VirtualRouterError(typeof error.message === 'string' && error.message.trim() ? error.message : 'Virtual router error', error.code);
|
|
1252
114
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
if (state.allowedProviders.size === 0 &&
|
|
1259
|
-
state.disabledProviders.size === 0 &&
|
|
1260
|
-
state.disabledKeys.size === 0 &&
|
|
1261
|
-
state.disabledModels.size === 0) {
|
|
1262
|
-
return routes;
|
|
1263
|
-
}
|
|
1264
|
-
return routes.filter(routeName => {
|
|
1265
|
-
const pools = this.routing[routeName];
|
|
1266
|
-
if (!pools)
|
|
1267
|
-
return false;
|
|
1268
|
-
for (const pool of pools) {
|
|
1269
|
-
if (!Array.isArray(pool.targets) || pool.targets.length === 0) {
|
|
1270
|
-
continue;
|
|
1271
|
-
}
|
|
1272
|
-
for (const providerKey of pool.targets) {
|
|
1273
|
-
const providerId = extractProviderId(providerKey);
|
|
1274
|
-
// console.log('[filter] checking', providerKey, 'id=', providerId);
|
|
1275
|
-
if (!providerId)
|
|
1276
|
-
continue;
|
|
1277
|
-
if (state.allowedProviders.size > 0 && !state.allowedProviders.has(providerId)) {
|
|
1278
|
-
// console.log('[filter] dropped by allowed list');
|
|
1279
|
-
continue;
|
|
1280
|
-
}
|
|
1281
|
-
if (state.disabledProviders.has(providerId)) {
|
|
1282
|
-
continue;
|
|
1283
|
-
}
|
|
1284
|
-
const disabledKeys = state.disabledKeys.get(providerId);
|
|
1285
|
-
if (disabledKeys && disabledKeys.size > 0) {
|
|
1286
|
-
const keyAlias = extractKeyAlias(providerKey);
|
|
1287
|
-
const keyIndex = extractKeyIndex(providerKey);
|
|
1288
|
-
if (keyAlias && disabledKeys.has(keyAlias)) {
|
|
1289
|
-
continue;
|
|
1290
|
-
}
|
|
1291
|
-
if (keyIndex !== undefined && disabledKeys.has(keyIndex + 1)) {
|
|
1292
|
-
continue;
|
|
1293
|
-
}
|
|
1294
|
-
}
|
|
1295
|
-
const disabledModels = state.disabledModels.get(providerId);
|
|
1296
|
-
if (disabledModels && disabledModels.size > 0) {
|
|
1297
|
-
const modelId = getProviderModelId(providerKey, this.providerRegistry);
|
|
1298
|
-
if (modelId && disabledModels.has(modelId)) {
|
|
1299
|
-
continue;
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
return true;
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
return false;
|
|
1306
|
-
});
|
|
1307
|
-
}
|
|
1308
|
-
selectFromCandidates(routes, metadata, classification, features, state, requiredProviderKeys, allowAliasRotation) {
|
|
1309
|
-
// legacy helper kept for backward compatibility; selection logic moved to engine-selection.ts
|
|
1310
|
-
return selectProviderImpl(this.normalizeRouteAlias(classification.routeName || DEFAULT_ROUTE), metadata, classification, features, state, {
|
|
1311
|
-
routing: this.routing,
|
|
1312
|
-
providerRegistry: this.providerRegistry,
|
|
1313
|
-
healthManager: this.healthManager,
|
|
1314
|
-
contextAdvisor: this.contextAdvisor,
|
|
1315
|
-
loadBalancer: this.loadBalancer,
|
|
1316
|
-
isProviderCoolingDown: (key) => this.isProviderCoolingDown(key),
|
|
1317
|
-
getProviderCooldownRemainingMs: (key) => this.getProviderCooldownRemainingMs(key),
|
|
1318
|
-
resolveStickyKey: (m) => this.resolveStickyKey(m),
|
|
1319
|
-
quotaView: this.quotaView,
|
|
1320
|
-
aliasQueueStore: this.stickySessionManager.getAllStores().aliasQueueStore
|
|
1321
|
-
}, { routingState: state });
|
|
115
|
+
return error instanceof Error ? error : new Error(message || 'Virtual router error');
|
|
116
|
+
}
|
|
117
|
+
function extractNativeErrorMessage(error) {
|
|
118
|
+
if (typeof error === 'string') {
|
|
119
|
+
return error;
|
|
1322
120
|
}
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
* 并按 ROUTE_PRIORITY 进行排序;同时显式排除 tools 路由,保证一旦进入
|
|
1326
|
-
* sticky 模式,就不会再命中独立的 tools 池(例如 glm/qwen 工具模型)。
|
|
1327
|
-
* 若候选集合中完全没有挂载 sticky key 的路由,则尝试在 default 路由上兜底。
|
|
1328
|
-
*/
|
|
1329
|
-
buildStickyRouteCandidatesFromFiltered(filteredCandidates, stickyKeySet) {
|
|
1330
|
-
const routesWithSticky = [];
|
|
1331
|
-
const candidateSet = new Set(filteredCandidates.filter((name) => name && name !== 'tools'));
|
|
1332
|
-
for (const routeName of candidateSet) {
|
|
1333
|
-
const pools = this.routing[routeName];
|
|
1334
|
-
if (!this.routeHasTargets(pools)) {
|
|
1335
|
-
continue;
|
|
1336
|
-
}
|
|
1337
|
-
const targets = this.flattenPoolTargets(pools);
|
|
1338
|
-
if (!targets.some((key) => stickyKeySet.has(key))) {
|
|
1339
|
-
continue;
|
|
1340
|
-
}
|
|
1341
|
-
routesWithSticky.push(routeName);
|
|
1342
|
-
}
|
|
1343
|
-
// 若当前候选路由中没有任何挂载 sticky key 的路由,尝试直接在 default 路由上兜底;
|
|
1344
|
-
// 若 default 也不包含 sticky key,则视为 sticky 配置失效,由调用方回落到非 sticky 逻辑。
|
|
1345
|
-
if (routesWithSticky.length === 0) {
|
|
1346
|
-
const defaultPools = this.routing[DEFAULT_ROUTE];
|
|
1347
|
-
if (this.routeHasTargets(defaultPools)) {
|
|
1348
|
-
const targets = this.flattenPoolTargets(defaultPools);
|
|
1349
|
-
if (targets.some((key) => stickyKeySet.has(key))) {
|
|
1350
|
-
return [DEFAULT_ROUTE];
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
return [];
|
|
1354
|
-
}
|
|
1355
|
-
const ordered = this.sortByPriority(routesWithSticky);
|
|
1356
|
-
const result = [];
|
|
1357
|
-
let hasDefault = false;
|
|
1358
|
-
for (const routeName of ordered) {
|
|
1359
|
-
if (routeName === DEFAULT_ROUTE) {
|
|
1360
|
-
hasDefault = true;
|
|
1361
|
-
continue;
|
|
1362
|
-
}
|
|
1363
|
-
if (!result.includes(routeName)) {
|
|
1364
|
-
result.push(routeName);
|
|
1365
|
-
}
|
|
1366
|
-
}
|
|
1367
|
-
// default 路由若包含 sticky key,则始终放在候选列表最后,用于 sticky 模式兜底。
|
|
1368
|
-
if (hasDefault && !result.includes(DEFAULT_ROUTE)) {
|
|
1369
|
-
result.push(DEFAULT_ROUTE);
|
|
1370
|
-
}
|
|
1371
|
-
return result;
|
|
121
|
+
if (error instanceof Error) {
|
|
122
|
+
return error.message;
|
|
1372
123
|
}
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
* - 仍然尊重 allowed/disabledProviders、disabledKeys、disabledModels 以及上下文长度。
|
|
1378
|
-
*/
|
|
1379
|
-
selectFromStickyPool(stickyKeySet, metadata, features, state, allowAliasRotation) {
|
|
1380
|
-
if (!stickyKeySet || stickyKeySet.size === 0) {
|
|
1381
|
-
return null;
|
|
1382
|
-
}
|
|
1383
|
-
const allowedProviders = new Set(state.allowedProviders);
|
|
1384
|
-
const disabledProviders = new Set(state.disabledProviders);
|
|
1385
|
-
const disabledKeysMap = new Map(Array.from(state.disabledKeys.entries()).map(([provider, keys]) => [
|
|
1386
|
-
provider,
|
|
1387
|
-
new Set(Array.from(keys).map((k) => (typeof k === 'string' ? k : k + 1)))
|
|
1388
|
-
]));
|
|
1389
|
-
const disabledModels = new Map(Array.from(state.disabledModels.entries()).map(([provider, models]) => [provider, new Set(models)]));
|
|
1390
|
-
// 初始候选集合:sticky 池中的所有 key
|
|
1391
|
-
// In quota routing mode, cooldown is controlled by quotaView only.
|
|
1392
|
-
let candidates = Array.from(stickyKeySet).filter((key) => (this.quotaView ? true : !this.isProviderCoolingDown(key)));
|
|
1393
|
-
// 应用 provider 白名单 / 黑名单
|
|
1394
|
-
if (allowedProviders.size > 0) {
|
|
1395
|
-
candidates = candidates.filter((key) => {
|
|
1396
|
-
const providerId = extractProviderId(key);
|
|
1397
|
-
return providerId && allowedProviders.has(providerId);
|
|
1398
|
-
});
|
|
1399
|
-
}
|
|
1400
|
-
if (disabledProviders.size > 0) {
|
|
1401
|
-
candidates = candidates.filter((key) => {
|
|
1402
|
-
const providerId = extractProviderId(key);
|
|
1403
|
-
return providerId && !disabledProviders.has(providerId);
|
|
1404
|
-
});
|
|
1405
|
-
}
|
|
1406
|
-
// 应用 key / model 级别黑名单
|
|
1407
|
-
if (disabledKeysMap.size > 0 || disabledModels.size > 0) {
|
|
1408
|
-
candidates = candidates.filter((key) => {
|
|
1409
|
-
const providerId = extractProviderId(key);
|
|
1410
|
-
if (!providerId) {
|
|
1411
|
-
return true;
|
|
1412
|
-
}
|
|
1413
|
-
const disabledKeys = disabledKeysMap.get(providerId);
|
|
1414
|
-
if (disabledKeys && disabledKeys.size > 0) {
|
|
1415
|
-
const keyAlias = extractKeyAlias(key);
|
|
1416
|
-
const keyIndex = extractKeyIndex(key);
|
|
1417
|
-
if (keyAlias && disabledKeys.has(keyAlias)) {
|
|
1418
|
-
return false;
|
|
1419
|
-
}
|
|
1420
|
-
if (keyIndex !== undefined && disabledKeys.has(keyIndex + 1)) {
|
|
1421
|
-
return false;
|
|
1422
|
-
}
|
|
1423
|
-
}
|
|
1424
|
-
const disabledModelSet = disabledModels.get(providerId);
|
|
1425
|
-
if (disabledModelSet && disabledModelSet.size > 0) {
|
|
1426
|
-
const modelId = getProviderModelId(key, this.providerRegistry);
|
|
1427
|
-
if (modelId && disabledModelSet.has(modelId)) {
|
|
1428
|
-
return false;
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
return true;
|
|
1432
|
-
});
|
|
1433
|
-
}
|
|
1434
|
-
if (!candidates.length) {
|
|
1435
|
-
return null;
|
|
1436
|
-
}
|
|
1437
|
-
const stickyKey = allowAliasRotation ? undefined : this.resolveStickyKey(metadata);
|
|
1438
|
-
const estimatedTokens = typeof features.estimatedTokens === 'number' && Number.isFinite(features.estimatedTokens)
|
|
1439
|
-
? Math.max(0, features.estimatedTokens)
|
|
1440
|
-
: 0;
|
|
1441
|
-
// delegate to selection module
|
|
124
|
+
return '';
|
|
125
|
+
}
|
|
126
|
+
function parseVirtualRouterErrorMessage(message) {
|
|
127
|
+
if (!message) {
|
|
1442
128
|
return null;
|
|
1443
129
|
}
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
return
|
|
1447
|
-
}
|
|
1448
|
-
buildRouteCandidates(requestedRoute, classificationCandidates, features) {
|
|
1449
|
-
const forceVision = this.routeHasForceFlag('vision');
|
|
1450
|
-
const hasMultimodalTargets = this.routeHasTargets(this.routing.multimodal);
|
|
1451
|
-
const hasVisionTargets = this.routeHasTargets(this.routing.vision);
|
|
1452
|
-
const hasLocalVideoAttachment = features.hasVideoAttachment === true && features.hasLocalVideoAttachment === true;
|
|
1453
|
-
if (features.hasImageAttachment && hasLocalVideoAttachment && hasVisionTargets) {
|
|
1454
|
-
return ['vision'];
|
|
1455
|
-
}
|
|
1456
|
-
const normalized = this.normalizeRouteAlias(requestedRoute || DEFAULT_ROUTE);
|
|
1457
|
-
const baseList = [];
|
|
1458
|
-
if (classificationCandidates && classificationCandidates.length) {
|
|
1459
|
-
for (const candidate of classificationCandidates) {
|
|
1460
|
-
baseList.push(this.normalizeRouteAlias(candidate));
|
|
1461
|
-
}
|
|
1462
|
-
}
|
|
1463
|
-
else if (normalized) {
|
|
1464
|
-
baseList.push(normalized);
|
|
1465
|
-
}
|
|
1466
|
-
if (features.hasImageAttachment) {
|
|
1467
|
-
if (hasMultimodalTargets) {
|
|
1468
|
-
if (!baseList.includes('multimodal')) {
|
|
1469
|
-
baseList.unshift('multimodal');
|
|
1470
|
-
}
|
|
1471
|
-
}
|
|
1472
|
-
if (hasVisionTargets) {
|
|
1473
|
-
if (!baseList.includes('vision')) {
|
|
1474
|
-
baseList.push('vision');
|
|
1475
|
-
}
|
|
1476
|
-
}
|
|
1477
|
-
if (!forceVision && hasMultimodalTargets) {
|
|
1478
|
-
const visionAwareRoutes = [DEFAULT_ROUTE, 'thinking'];
|
|
1479
|
-
for (const routeName of visionAwareRoutes) {
|
|
1480
|
-
if (this.routeHasTargets(this.routing[routeName]) && !baseList.includes(routeName)) {
|
|
1481
|
-
baseList.push(routeName);
|
|
1482
|
-
}
|
|
1483
|
-
}
|
|
1484
|
-
}
|
|
1485
|
-
}
|
|
1486
|
-
let ordered = this.sortByPriority(baseList);
|
|
1487
|
-
if (features.hasImageAttachment && !forceVision && hasMultimodalTargets) {
|
|
1488
|
-
ordered = this.reorderForInlineVision(ordered);
|
|
1489
|
-
}
|
|
1490
|
-
if (features.hasImageAttachment && hasMultimodalTargets) {
|
|
1491
|
-
ordered = this.reorderForPreferredModel(ordered, 'kimi-k2.5');
|
|
1492
|
-
}
|
|
1493
|
-
const deduped = [];
|
|
1494
|
-
for (const routeName of ordered) {
|
|
1495
|
-
if (routeName && !deduped.includes(routeName)) {
|
|
1496
|
-
deduped.push(routeName);
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
if (!deduped.includes(DEFAULT_ROUTE)) {
|
|
1500
|
-
deduped.push(DEFAULT_ROUTE);
|
|
1501
|
-
}
|
|
1502
|
-
const filtered = deduped.filter((routeName) => this.routeHasTargets(this.routing[routeName]));
|
|
1503
|
-
if (!filtered.includes(DEFAULT_ROUTE) && this.routeHasTargets(this.routing[DEFAULT_ROUTE])) {
|
|
1504
|
-
filtered.push(DEFAULT_ROUTE);
|
|
1505
|
-
}
|
|
1506
|
-
return filtered.length ? filtered : [DEFAULT_ROUTE];
|
|
1507
|
-
}
|
|
1508
|
-
reorderForInlineVision(routeNames) {
|
|
1509
|
-
const unique = Array.from(new Set(routeNames.filter(Boolean)));
|
|
1510
|
-
if (!unique.length) {
|
|
1511
|
-
return unique;
|
|
1512
|
-
}
|
|
1513
|
-
// 仅当 default/thinking 中存在 Responses/Gemini 提供方时,才将其提前作为「一次完成」优先级。
|
|
1514
|
-
const inlinePreferred = [];
|
|
1515
|
-
const inlineRoutes = [DEFAULT_ROUTE, 'thinking'];
|
|
1516
|
-
for (const routeName of inlineRoutes) {
|
|
1517
|
-
if (this.routeSupportsInlineVision(routeName) && !inlinePreferred.includes(routeName)) {
|
|
1518
|
-
inlinePreferred.push(routeName);
|
|
1519
|
-
}
|
|
1520
|
-
}
|
|
1521
|
-
if (!inlinePreferred.length) {
|
|
1522
|
-
return unique;
|
|
1523
|
-
}
|
|
1524
|
-
const remaining = [];
|
|
1525
|
-
for (const routeName of unique) {
|
|
1526
|
-
if (!inlinePreferred.includes(routeName)) {
|
|
1527
|
-
remaining.push(routeName);
|
|
1528
|
-
}
|
|
1529
|
-
}
|
|
1530
|
-
return [...inlinePreferred, ...remaining];
|
|
130
|
+
const normalized = message.startsWith('Error:') ? message.replace(/^Error:\s*/, '') : message;
|
|
131
|
+
if (!normalized.startsWith(VIRTUAL_ROUTER_ERROR_PREFIX)) {
|
|
132
|
+
return null;
|
|
1531
133
|
}
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
}
|
|
1537
|
-
const preferred = unique.filter((routeName) => this.routeSupportsModel(routeName, modelId));
|
|
1538
|
-
if (!preferred.length) {
|
|
1539
|
-
return unique;
|
|
1540
|
-
}
|
|
1541
|
-
const remaining = unique.filter((routeName) => !preferred.includes(routeName));
|
|
1542
|
-
return [...preferred, ...remaining];
|
|
134
|
+
const remainder = normalized.slice(VIRTUAL_ROUTER_ERROR_PREFIX.length);
|
|
135
|
+
const idx = remainder.indexOf(':');
|
|
136
|
+
if (idx <= 0) {
|
|
137
|
+
return null;
|
|
1543
138
|
}
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
}
|
|
1549
|
-
const pools = this.routing[routeName];
|
|
1550
|
-
if (!Array.isArray(pools)) {
|
|
1551
|
-
return false;
|
|
1552
|
-
}
|
|
1553
|
-
for (const pool of pools) {
|
|
1554
|
-
if (!Array.isArray(pool.targets)) {
|
|
1555
|
-
continue;
|
|
1556
|
-
}
|
|
1557
|
-
for (const providerKey of pool.targets) {
|
|
1558
|
-
try {
|
|
1559
|
-
const profile = this.providerRegistry.get(providerKey);
|
|
1560
|
-
const candidate = typeof profile.modelId === 'string' ? profile.modelId.trim().toLowerCase() : '';
|
|
1561
|
-
if (candidate === normalizedModel) {
|
|
1562
|
-
return true;
|
|
1563
|
-
}
|
|
1564
|
-
}
|
|
1565
|
-
catch {
|
|
1566
|
-
// ignore unknown provider keys during capability probing
|
|
1567
|
-
}
|
|
1568
|
-
}
|
|
1569
|
-
}
|
|
1570
|
-
return false;
|
|
139
|
+
const code = remainder.slice(0, idx);
|
|
140
|
+
const detail = remainder.slice(idx + 1).trim();
|
|
141
|
+
if (!isVirtualRouterErrorCode(code)) {
|
|
142
|
+
return null;
|
|
1571
143
|
}
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
continue;
|
|
1580
|
-
}
|
|
1581
|
-
for (const providerKey of pool.targets) {
|
|
1582
|
-
try {
|
|
1583
|
-
const profile = this.providerRegistry.get(providerKey);
|
|
1584
|
-
if (profile.providerType === 'responses' || profile.providerType === 'gemini') {
|
|
1585
|
-
return true;
|
|
1586
|
-
}
|
|
1587
|
-
}
|
|
1588
|
-
catch {
|
|
1589
|
-
// ignore unknown provider keys during capability probing
|
|
1590
|
-
}
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
144
|
+
return { code, message: detail || 'Virtual router error' };
|
|
145
|
+
}
|
|
146
|
+
function isVirtualRouterErrorCode(value) {
|
|
147
|
+
return Object.values(VirtualRouterErrorCode).includes(value);
|
|
148
|
+
}
|
|
149
|
+
function isVirtualRouterErrorLike(error) {
|
|
150
|
+
if (!error || typeof error !== 'object') {
|
|
1593
151
|
return false;
|
|
1594
152
|
}
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
153
|
+
const record = error;
|
|
154
|
+
return typeof record.code === 'string' && isVirtualRouterErrorCode(record.code);
|
|
155
|
+
}
|
|
156
|
+
function injectRuntimeNowMs(metadata) {
|
|
157
|
+
const nowMs = Date.now();
|
|
158
|
+
const rt = metadata.__rt;
|
|
159
|
+
if (rt && typeof rt === 'object' && !Array.isArray(rt)) {
|
|
160
|
+
return { ...metadata, __rt: { ...rt, nowMs } };
|
|
1601
161
|
}
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
162
|
+
return { ...metadata, __rt: { nowMs } };
|
|
163
|
+
}
|
|
164
|
+
function buildRoutingInstructionParseLog(request, metadata) {
|
|
165
|
+
const messages = Array.isArray(request.messages)
|
|
166
|
+
? (request.messages ?? [])
|
|
167
|
+
: [];
|
|
168
|
+
if (!messages.length) {
|
|
169
|
+
return null;
|
|
1608
170
|
}
|
|
1609
|
-
|
|
1610
|
-
|
|
171
|
+
const latest = messages[messages.length - 1];
|
|
172
|
+
const latestText = typeof latest?.content === 'string' ? latest.content.trim() : '';
|
|
173
|
+
const parsedKinds = parseRoutingInstructionKindsWithNative(request);
|
|
174
|
+
const stopMessageTypes = parsedKinds.filter((type) => type === 'stopMessageSet' || type === 'stopMessageMode' || type === 'stopMessageClear');
|
|
175
|
+
const scopedTypes = parsedKinds.filter((type) => type === 'stopMessageSet' ||
|
|
176
|
+
type === 'stopMessageMode' ||
|
|
177
|
+
type === 'stopMessageClear' ||
|
|
178
|
+
type === 'preCommandSet' ||
|
|
179
|
+
type === 'preCommandClear');
|
|
180
|
+
const markerDetected = messages.some((message) => {
|
|
181
|
+
if (!message || typeof message !== 'object') {
|
|
1611
182
|
return false;
|
|
1612
183
|
}
|
|
1613
|
-
|
|
184
|
+
const record = message;
|
|
185
|
+
return record.role === 'user' && typeof record.content === 'string' && /<\*\*[\s\S]*?\*\*>/.test(record.content);
|
|
186
|
+
});
|
|
187
|
+
const hasStopKeyword = /stopmessage/i.test(latestText);
|
|
188
|
+
if (!hasStopKeyword && stopMessageTypes.length === 0 && scopedTypes.length === 0) {
|
|
189
|
+
return null;
|
|
1614
190
|
}
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
191
|
+
return {
|
|
192
|
+
requestId: metadata.requestId || 'n/a',
|
|
193
|
+
markerDetected,
|
|
194
|
+
preview: latestText.replace(/\s+/g, ' ').slice(0, 120),
|
|
195
|
+
stopMessageTypes,
|
|
196
|
+
scopedTypes,
|
|
197
|
+
stopScope: resolveStopMessageScope(metadata)
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function emitRoutingInstructionParseLog(log) {
|
|
201
|
+
if (!log) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const reset = '\x1b[0m';
|
|
205
|
+
const tagColor = '\x1b[38;5;39m';
|
|
206
|
+
const scopeColor = '\x1b[38;5;220m';
|
|
207
|
+
console.log(`${tagColor}[virtual-router][stop_message_parse]${reset} requestId=${log.requestId} marker=${log.markerDetected ? 'detected' : 'missing'} parsed=${log.stopMessageTypes.join(',') || 'none'} preview=${log.preview}`);
|
|
208
|
+
if (log.scopedTypes.length > 0) {
|
|
209
|
+
if (log.stopScope) {
|
|
210
|
+
console.log(`${scopeColor}[virtual-router][stop_scope]${reset} requestId=${log.requestId} stage=apply scope=${log.stopScope} instructions=${log.scopedTypes.join(',')}`);
|
|
1618
211
|
}
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
sortRoutePools(pools) {
|
|
1622
|
-
if (!Array.isArray(pools)) {
|
|
1623
|
-
return [];
|
|
212
|
+
else {
|
|
213
|
+
console.log(`${scopeColor}[virtual-router][stop_scope]${reset} requestId=${log.requestId} stage=drop reason=missing_tmux_scope instructions=${log.scopedTypes.join(',')}`);
|
|
1624
214
|
}
|
|
1625
|
-
return pools
|
|
1626
|
-
.filter((pool) => Array.isArray(pool.targets) && pool.targets.length > 0)
|
|
1627
|
-
.sort((a, b) => {
|
|
1628
|
-
if (a.backup && !b.backup)
|
|
1629
|
-
return 1;
|
|
1630
|
-
if (!a.backup && b.backup)
|
|
1631
|
-
return -1;
|
|
1632
|
-
if (a.priority !== b.priority) {
|
|
1633
|
-
return b.priority - a.priority;
|
|
1634
|
-
}
|
|
1635
|
-
return a.id.localeCompare(b.id);
|
|
1636
|
-
});
|
|
1637
215
|
}
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
216
|
+
}
|
|
217
|
+
function emitVirtualRouterHitLog(result, options) {
|
|
218
|
+
const reset = '\x1b[0m';
|
|
219
|
+
const prefixColor = '\x1b[38;5;208m';
|
|
220
|
+
const timeColor = '\x1b[90m';
|
|
221
|
+
const stopColor = '\x1b[38;5;214m';
|
|
222
|
+
const routeColorMap = {
|
|
223
|
+
multimodal: '\x1b[38;5;45m',
|
|
224
|
+
tools: '\x1b[38;5;214m',
|
|
225
|
+
thinking: '\x1b[34m',
|
|
226
|
+
coding: '\x1b[35m',
|
|
227
|
+
longcontext: '\x1b[38;5;141m',
|
|
228
|
+
web_search: '\x1b[32m',
|
|
229
|
+
search: '\x1b[38;5;34m',
|
|
230
|
+
vision: '\x1b[38;5;207m',
|
|
231
|
+
background: '\x1b[90m'
|
|
232
|
+
};
|
|
233
|
+
const now = new Date();
|
|
234
|
+
const timestamp = `${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}`;
|
|
235
|
+
const routeLabel = result.decision.poolId
|
|
236
|
+
? `${result.decision.routeName}/${result.decision.poolId}`
|
|
237
|
+
: result.decision.routeName;
|
|
238
|
+
const routeColor = routeColorMap[result.decision.routeName] ?? '\x1b[36m';
|
|
239
|
+
const providerKey = result.decision.providerKey || result.target.providerKey;
|
|
240
|
+
const modelSuffix = result.target.modelId ? `.${result.target.modelId}` : '';
|
|
241
|
+
const reason = result.decision.reasoning ? ` reason=${result.decision.reasoning}` : '';
|
|
242
|
+
const stopStatusLabel = formatStopMessageStatusLabel(options?.stopState ?? null, options?.stopScope, Boolean(options?.forceStopStatusLabel));
|
|
243
|
+
const requestId = typeof options?.requestId === 'string' ? options.requestId : '';
|
|
244
|
+
const requestLabel = requestId && !requestId.includes('unknown') ? ` req=${requestId}` : '';
|
|
245
|
+
console.log(`${prefixColor}[virtual-router-hit]${reset} ${timeColor}${timestamp}${reset}${requestLabel} ${routeColor}${routeLabel} -> ${providerKey}${modelSuffix}${reason}${reset}${stopStatusLabel ? ` ${stopColor}${stopStatusLabel}${reset}` : ''}`);
|
|
246
|
+
}
|
|
247
|
+
function formatStopMessageStatusLabel(snapshot, scope, forceShow) {
|
|
248
|
+
const scopeLabel = scope && scope.trim() ? scope.trim() : 'none';
|
|
249
|
+
if (!snapshot) {
|
|
250
|
+
if (!forceShow) {
|
|
251
|
+
return '';
|
|
252
|
+
}
|
|
253
|
+
return `[stopMessage:scope=${scopeLabel} active=no state=cleared]`;
|
|
254
|
+
}
|
|
255
|
+
const text = typeof snapshot.stopMessageText === 'string' ? snapshot.stopMessageText.trim() : '';
|
|
256
|
+
const safeText = text ? (text.length > 24 ? `${text.slice(0, 21)}...` : text) : '(mode-only)';
|
|
257
|
+
const mode = (snapshot.stopMessageStageMode || 'unset').toString().toLowerCase();
|
|
258
|
+
const maxRepeats = typeof snapshot.stopMessageMaxRepeats === 'number' && Number.isFinite(snapshot.stopMessageMaxRepeats)
|
|
259
|
+
? Math.max(0, Math.floor(snapshot.stopMessageMaxRepeats))
|
|
260
|
+
: 0;
|
|
261
|
+
const used = typeof snapshot.stopMessageUsed === 'number' && Number.isFinite(snapshot.stopMessageUsed)
|
|
262
|
+
? Math.max(0, Math.floor(snapshot.stopMessageUsed))
|
|
263
|
+
: 0;
|
|
264
|
+
const remaining = maxRepeats > 0 ? Math.max(0, maxRepeats - used) : -1;
|
|
265
|
+
const active = mode !== 'off' && Boolean(text) && maxRepeats > 0;
|
|
266
|
+
const rounds = maxRepeats > 0 ? `${used}/${maxRepeats}` : `${used}/-`;
|
|
267
|
+
const left = remaining >= 0 ? String(remaining) : 'n/a';
|
|
268
|
+
return `[stopMessage:scope=${scopeLabel} text="${safeText}" mode=${mode} round=${rounds} left=${left} active=${active ? 'yes' : 'no'}]`;
|
|
269
|
+
}
|
|
270
|
+
function cleanRoutingInstructionMarkersInPlace(request) {
|
|
271
|
+
const cleaned = cleanRoutingInstructionMarkersWithNative(request);
|
|
272
|
+
if (Array.isArray(cleaned.messages)) {
|
|
273
|
+
request.messages = cleaned.messages;
|
|
274
|
+
}
|
|
275
|
+
const cleanedSemantics = cleaned.semantics;
|
|
276
|
+
if (cleanedSemantics && typeof cleanedSemantics === 'object' && !Array.isArray(cleanedSemantics)) {
|
|
277
|
+
const cleanedResponses = cleanedSemantics.responses;
|
|
278
|
+
if (cleanedResponses && typeof cleanedResponses === 'object' && !Array.isArray(cleanedResponses)) {
|
|
279
|
+
const cleanedContext = cleanedResponses.context;
|
|
280
|
+
if (cleanedContext !== undefined) {
|
|
281
|
+
const semantics = request.semantics && typeof request.semantics === 'object' && !Array.isArray(request.semantics)
|
|
282
|
+
? request.semantics
|
|
283
|
+
: {};
|
|
284
|
+
const responses = semantics.responses && typeof semantics.responses === 'object' && !Array.isArray(semantics.responses)
|
|
285
|
+
? semantics.responses
|
|
286
|
+
: {};
|
|
287
|
+
responses.context = cleanedContext;
|
|
288
|
+
semantics.responses = responses;
|
|
289
|
+
request.semantics = semantics;
|
|
1651
290
|
}
|
|
1652
291
|
}
|
|
1653
|
-
return flattened;
|
|
1654
|
-
}
|
|
1655
|
-
markProviderCooldown(providerKey, cooldownMs) {
|
|
1656
|
-
this.cooldownManager.markProviderCooldown(providerKey, cooldownMs);
|
|
1657
|
-
}
|
|
1658
|
-
clearProviderCooldown(providerKey) {
|
|
1659
|
-
this.cooldownManager.clearProviderCooldown(providerKey);
|
|
1660
|
-
}
|
|
1661
|
-
isProviderCoolingDown(providerKey) {
|
|
1662
|
-
return this.cooldownManager.isProviderCoolingDown(providerKey);
|
|
1663
292
|
}
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
293
|
+
}
|
|
294
|
+
function collectClientScopedInstructions(request) {
|
|
295
|
+
const messages = Array.isArray(request.messages)
|
|
296
|
+
? request.messages
|
|
297
|
+
: [];
|
|
298
|
+
const responsesContext = request.semantics && typeof request.semantics === 'object'
|
|
299
|
+
? request.semantics.responses?.context
|
|
300
|
+
: undefined;
|
|
301
|
+
const responsesHasMarker = hasRoutingInstructionMarkerInResponsesContext(responsesContext);
|
|
302
|
+
const latestUserHasMarker = hasLatestUserRoutingInstructionMarker(messages) || responsesHasMarker;
|
|
303
|
+
if (!latestUserHasMarker && !messages.some((message) => {
|
|
304
|
+
if (!message || typeof message !== 'object') {
|
|
305
|
+
return false;
|
|
1671
306
|
}
|
|
1672
|
-
const
|
|
1673
|
-
return
|
|
1674
|
-
}
|
|
1675
|
-
|
|
1676
|
-
this.cooldownManager.restoreHealthFromStore();
|
|
307
|
+
const record = message;
|
|
308
|
+
return record.role === 'user' && typeof record.content === 'string' && ROUTING_INSTRUCTION_MARKER_PATTERN.test(record.content);
|
|
309
|
+
})) {
|
|
310
|
+
return { instructions: [], latestUserHasMarker };
|
|
1677
311
|
}
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
const
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
this.cooldownManager.persistHealthSnapshot();
|
|
312
|
+
let instructions = parseRoutingInstructions(messages);
|
|
313
|
+
if (instructions.length === 0 && responsesHasMarker) {
|
|
314
|
+
const responsesLatestUserText = getLatestUserTextFromResponsesContext(responsesContext);
|
|
315
|
+
if (responsesLatestUserText) {
|
|
316
|
+
instructions = parseRoutingInstructions([{ role: 'user', content: responsesLatestUserText }]);
|
|
317
|
+
}
|
|
1685
318
|
}
|
|
319
|
+
return { instructions, latestUserHasMarker };
|
|
320
|
+
}
|
|
321
|
+
function isClientScopedInstruction(instruction) {
|
|
322
|
+
return (instruction.type === 'stopMessageSet' ||
|
|
323
|
+
instruction.type === 'stopMessageMode' ||
|
|
324
|
+
instruction.type === 'stopMessageClear' ||
|
|
325
|
+
instruction.type === 'preCommandSet' ||
|
|
326
|
+
instruction.type === 'preCommandClear' ||
|
|
327
|
+
instruction.type === 'clear');
|
|
1686
328
|
}
|