@mupt-ai/dari-gateway 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -4
- package/dist/bun.d.ts +2 -0
- package/dist/bun.js +4 -0
- package/dist/context/eval_catalog.d.ts +3 -0
- package/dist/context/eval_catalog.js +7 -0
- package/dist/context/interaction_fingerprint.d.ts +27 -0
- package/dist/context/interaction_fingerprint.js +519 -0
- package/dist/context/interaction_fingerprint_format.d.ts +42 -0
- package/dist/context/interaction_fingerprint_format.js +113 -0
- package/dist/context/interaction_lease_depth.d.ts +2 -0
- package/dist/context/interaction_lease_depth.js +110 -0
- package/dist/context/lease_history.d.ts +7 -0
- package/dist/context/lease_history.js +158 -0
- package/dist/context/pricing.d.ts +4 -0
- package/dist/context/pricing.js +27 -0
- package/dist/context/provider_prompt_accounting.d.ts +14 -0
- package/dist/context/provider_prompt_accounting.js +187 -0
- package/dist/context/routing_context.d.ts +40 -0
- package/dist/context/routing_context.js +211 -0
- package/dist/http/auth.d.ts +2 -0
- package/dist/http/auth.js +35 -0
- package/dist/http/errors.d.ts +19 -0
- package/dist/http/errors.js +89 -0
- package/dist/http/handler.d.ts +12 -0
- package/dist/http/handler.js +88 -0
- package/dist/http/http2_server.d.ts +6 -0
- package/dist/http/http2_server.js +103 -0
- package/dist/http/serve.d.ts +20 -0
- package/dist/http/serve.js +78 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +9 -0
- package/dist/ingress/anthropic_messages.d.ts +3 -0
- package/dist/ingress/anthropic_messages.js +122 -0
- package/dist/ingress/anthropic_models.d.ts +1 -0
- package/dist/ingress/anthropic_models.js +23 -0
- package/dist/ingress/chat_completions.d.ts +2 -0
- package/dist/ingress/chat_completions.js +44 -0
- package/dist/ingress/claude_code/request_context.d.ts +2 -0
- package/dist/ingress/claude_code/request_context.js +31 -0
- package/dist/ingress/codex.d.ts +3 -0
- package/dist/ingress/codex.js +69 -0
- package/dist/ingress/codex_models.d.ts +2 -0
- package/dist/ingress/codex_models.js +89 -0
- package/dist/ingress/json.d.ts +1 -0
- package/dist/ingress/json.js +95 -0
- package/dist/ingress/models.d.ts +1 -0
- package/dist/ingress/models.js +65 -0
- package/dist/ingress/muse_code.d.ts +2 -0
- package/dist/ingress/muse_code.js +8 -0
- package/dist/ingress/muse_models.d.ts +9 -0
- package/dist/ingress/muse_models.js +32 -0
- package/dist/interaction/capabilities.d.ts +3 -0
- package/dist/interaction/capabilities.js +24 -0
- package/dist/interaction/image.d.ts +4 -0
- package/dist/interaction/image.js +13 -0
- package/dist/interaction/portable_reasoning_state.d.ts +4 -0
- package/dist/interaction/portable_reasoning_state.js +18 -0
- package/dist/interaction/provider_continuation_state.d.ts +6 -0
- package/dist/interaction/provider_continuation_state.js +27 -0
- package/dist/interaction/tool_identity.d.ts +10 -0
- package/dist/interaction/tool_identity.js +41 -0
- package/dist/interaction/types.d.ts +204 -0
- package/dist/interaction/types.js +5 -0
- package/dist/models/catalog.d.ts +35 -0
- package/dist/models/catalog.js +232 -0
- package/dist/models/context_limits.d.ts +2 -0
- package/dist/models/context_limits.js +7 -0
- package/dist/models/execution_provider.d.ts +9 -0
- package/dist/models/execution_provider.js +17 -0
- package/dist/models/gateways.d.ts +11 -0
- package/dist/models/gateways.js +45 -0
- package/dist/models/ids.d.ts +1 -0
- package/dist/models/ids.js +15 -0
- package/dist/models/provider_defaults.d.ts +4 -0
- package/dist/models/provider_defaults.js +44 -0
- package/dist/models/runtime_overrides.d.ts +7 -0
- package/dist/models/runtime_overrides.js +226 -0
- package/dist/models/subscriptions.d.ts +8 -0
- package/dist/models/subscriptions.js +37 -0
- package/dist/observability/errors.d.ts +8 -0
- package/dist/observability/errors.js +11 -0
- package/dist/platform/platform.d.ts +156 -0
- package/dist/platform/platform.js +11 -0
- package/dist/platform/routing_contract.d.ts +2 -0
- package/dist/platform/routing_contract.js +5 -0
- package/dist/protocol/input/anthropic_messages.d.ts +2 -0
- package/dist/protocol/input/anthropic_messages.js +494 -0
- package/dist/protocol/input/chat_completions.d.ts +4 -0
- package/dist/protocol/input/chat_completions.js +214 -0
- package/dist/protocol/input/chat_completions_validation.d.ts +2 -0
- package/dist/protocol/input/chat_completions_validation.js +258 -0
- package/dist/protocol/input/openai_responses.d.ts +2 -0
- package/dist/protocol/input/openai_responses.js +666 -0
- package/dist/protocol/input/router_model.d.ts +7 -0
- package/dist/protocol/input/router_model.js +43 -0
- package/dist/protocol/output/anthropic_messages.d.ts +10 -0
- package/dist/protocol/output/anthropic_messages.js +15 -0
- package/dist/protocol/output/anthropic_messages_output.d.ts +10 -0
- package/dist/protocol/output/anthropic_messages_output.js +124 -0
- package/dist/protocol/output/anthropic_messages_stream.d.ts +16 -0
- package/dist/protocol/output/anthropic_messages_stream.js +267 -0
- package/dist/protocol/output/chat_completions.d.ts +29 -0
- package/dist/protocol/output/chat_completions.js +406 -0
- package/dist/protocol/output/chat_reasoning_detail.d.ts +2 -0
- package/dist/protocol/output/chat_reasoning_detail.js +13 -0
- package/dist/protocol/output/delivered_stream_snapshot.d.ts +10 -0
- package/dist/protocol/output/delivered_stream_snapshot.js +31 -0
- package/dist/protocol/output/openai_responses.d.ts +10 -0
- package/dist/protocol/output/openai_responses.js +14 -0
- package/dist/protocol/output/openai_responses_output.d.ts +14 -0
- package/dist/protocol/output/openai_responses_output.js +125 -0
- package/dist/protocol/output/openai_responses_stream.d.ts +18 -0
- package/dist/protocol/output/openai_responses_stream.js +546 -0
- package/dist/protocol/output/sse_stream.d.ts +31 -0
- package/dist/protocol/output/sse_stream.js +67 -0
- package/dist/providers/anthropic_safeguards.d.ts +11 -0
- package/dist/providers/anthropic_safeguards.js +81 -0
- package/dist/providers/aws_assume_role.d.ts +17 -0
- package/dist/providers/aws_assume_role.js +107 -0
- package/dist/providers/cancel_estimate.d.ts +43 -0
- package/dist/providers/cancel_estimate.js +89 -0
- package/dist/providers/execution.d.ts +72 -0
- package/dist/providers/execution.js +491 -0
- package/dist/providers/model_capabilities.d.ts +11 -0
- package/dist/providers/model_capabilities.js +29 -0
- package/dist/providers/pi.d.ts +65 -0
- package/dist/providers/pi.js +325 -0
- package/dist/providers/pi_context.d.ts +7 -0
- package/dist/providers/pi_context.js +405 -0
- package/dist/providers/pi_options.d.ts +11 -0
- package/dist/providers/pi_options.js +410 -0
- package/dist/providers/pi_output.d.ts +2 -0
- package/dist/providers/pi_output.js +453 -0
- package/dist/providers/provider_attempt_runner.d.ts +55 -0
- package/dist/providers/provider_attempt_runner.js +90 -0
- package/dist/providers/provider_call.d.ts +77 -0
- package/dist/providers/provider_call.js +296 -0
- package/dist/providers/provider_reported_cost.d.ts +10 -0
- package/dist/providers/provider_reported_cost.js +123 -0
- package/dist/providers/reasoning.d.ts +12 -0
- package/dist/providers/reasoning.js +30 -0
- package/dist/providers/response_capture.d.ts +23 -0
- package/dist/providers/response_capture.js +47 -0
- package/dist/providers/stream_finalization.d.ts +46 -0
- package/dist/providers/stream_finalization.js +165 -0
- package/dist/providers/subscription_limits.d.ts +33 -0
- package/dist/providers/subscription_limits.js +179 -0
- package/dist/providers/subscription_usage.d.ts +10 -0
- package/dist/providers/subscription_usage.js +79 -0
- package/dist/runtime/claude_code/auxiliary_routing.d.ts +3 -0
- package/dist/runtime/claude_code/auxiliary_routing.js +60 -0
- package/dist/runtime/customer_disconnect.d.ts +20 -0
- package/dist/runtime/customer_disconnect.js +49 -0
- package/dist/runtime/explicit_model.d.ts +7 -0
- package/dist/runtime/explicit_model.js +57 -0
- package/dist/runtime/harness_identity/conversation.d.ts +33 -0
- package/dist/runtime/harness_identity/conversation.js +39 -0
- package/dist/runtime/harness_identity/extract.d.ts +8 -0
- package/dist/runtime/harness_identity/extract.js +214 -0
- package/dist/runtime/harness_identity/index.d.ts +2 -0
- package/dist/runtime/harness_identity/index.js +2 -0
- package/dist/runtime/prepare_routed_completion.d.ts +33 -0
- package/dist/runtime/prepare_routed_completion.js +186 -0
- package/dist/runtime/provider_stream_lifecycle.d.ts +31 -0
- package/dist/runtime/provider_stream_lifecycle.js +88 -0
- package/dist/runtime/provider_stream_outcome.d.ts +23 -0
- package/dist/runtime/provider_stream_outcome.js +55 -0
- package/dist/runtime/request_tags.d.ts +7 -0
- package/dist/runtime/request_tags.js +33 -0
- package/dist/runtime/routed_completion.d.ts +17 -0
- package/dist/runtime/routed_completion.js +80 -0
- package/dist/runtime/routed_protocol.d.ts +30 -0
- package/dist/runtime/routed_protocol.js +1 -0
- package/dist/runtime/routed_runtime.d.ts +40 -0
- package/dist/runtime/routed_runtime.js +535 -0
- package/dist/runtime/routing_lease.d.ts +26 -0
- package/dist/runtime/routing_lease.js +210 -0
- package/dist/runtime/runtime_reporter.d.ts +71 -0
- package/dist/runtime/runtime_reporter.js +35 -0
- package/dist/runtime/strict_provider_access.d.ts +6 -0
- package/dist/runtime/strict_provider_access.js +40 -0
- package/dist/self_host/config.d.ts +46 -0
- package/dist/self_host/config.js +105 -0
- package/dist/self_host/main.d.ts +16 -0
- package/dist/self_host/main.js +95 -0
- package/dist/self_host/platform.d.ts +13 -0
- package/dist/self_host/platform.js +50 -0
- package/dist/self_host/prefix_store.d.ts +46 -0
- package/dist/self_host/prefix_store.js +261 -0
- package/dist/self_host/resolve.d.ts +28 -0
- package/dist/self_host/resolve.js +167 -0
- package/dist/self_host/subscriptions/access.d.ts +10 -0
- package/dist/self_host/subscriptions/access.js +37 -0
- package/dist/self_host/subscriptions/http.d.ts +15 -0
- package/dist/self_host/subscriptions/http.js +187 -0
- package/dist/self_host/subscriptions/index.d.ts +24 -0
- package/dist/self_host/subscriptions/index.js +24 -0
- package/dist/self_host/subscriptions/oauth.d.ts +59 -0
- package/dist/self_host/subscriptions/oauth.js +376 -0
- package/dist/self_host/subscriptions/store.d.ts +78 -0
- package/dist/self_host/subscriptions/store.js +236 -0
- package/dist/strategies/auto_routing_selector.d.ts +12 -0
- package/dist/strategies/auto_routing_selector.js +61 -0
- package/dist/strategies/eval_relevance.d.ts +17 -0
- package/dist/strategies/eval_relevance.js +32 -0
- package/dist/strategies/fallback_decision.d.ts +8 -0
- package/dist/strategies/fallback_decision.js +17 -0
- package/dist/strategies/priority_selector.d.ts +7 -0
- package/dist/strategies/priority_selector.js +37 -0
- package/dist/strategies/route_decision.d.ts +22 -0
- package/dist/strategies/route_decision.js +65 -0
- package/dist/strategies/routing.d.ts +74 -0
- package/dist/strategies/routing.js +280 -0
- package/dist/strategies/selector_projection.d.ts +15 -0
- package/dist/strategies/selector_projection.js +161 -0
- package/dist/telemetry/background.d.ts +7 -0
- package/dist/telemetry/background.js +33 -0
- package/dist/telemetry/benchmark_replay.d.ts +37 -0
- package/dist/telemetry/benchmark_replay.js +108 -0
- package/dist/telemetry/billing_finalization.d.ts +64 -0
- package/dist/telemetry/billing_finalization.js +96 -0
- package/dist/telemetry/ingest_values.d.ts +4 -0
- package/dist/telemetry/ingest_values.js +12 -0
- package/dist/telemetry/model_cost.d.ts +3 -0
- package/dist/telemetry/model_cost.js +15 -0
- package/dist/telemetry/prefix_state.d.ts +47 -0
- package/dist/telemetry/prefix_state.js +116 -0
- package/dist/telemetry/report.d.ts +88 -0
- package/dist/telemetry/report.js +621 -0
- package/dist/telemetry/requests/interaction_observability.d.ts +45 -0
- package/dist/telemetry/requests/interaction_observability.js +180 -0
- package/dist/telemetry/requests/retained_interaction_request.d.ts +8 -0
- package/dist/telemetry/requests/retained_interaction_request.js +10 -0
- package/dist/telemetry/scrub.d.ts +2 -0
- package/dist/telemetry/scrub.js +31 -0
- package/dist/telemetry/skills/claude/catalog.d.ts +6 -0
- package/dist/telemetry/skills/claude/catalog.js +99 -0
- package/dist/telemetry/skills/claude/file_reads.d.ts +4 -0
- package/dist/telemetry/skills/claude/file_reads.js +110 -0
- package/dist/telemetry/skills/claude/observations.d.ts +3 -0
- package/dist/telemetry/skills/claude/observations.js +366 -0
- package/dist/telemetry/skills/codex/catalog.d.ts +11 -0
- package/dist/telemetry/skills/codex/catalog.js +138 -0
- package/dist/telemetry/skills/codex/exec_file_reads.d.ts +9 -0
- package/dist/telemetry/skills/codex/exec_file_reads.js +36 -0
- package/dist/telemetry/skills/codex/explicit.d.ts +3 -0
- package/dist/telemetry/skills/codex/explicit.js +90 -0
- package/dist/telemetry/skills/codex/file_reads.d.ts +4 -0
- package/dist/telemetry/skills/codex/file_reads.js +37 -0
- package/dist/telemetry/skills/codex/identity.d.ts +2 -0
- package/dist/telemetry/skills/codex/identity.js +20 -0
- package/dist/telemetry/skills/observations.d.ts +21 -0
- package/dist/telemetry/skills/observations.js +41 -0
- package/dist/telemetry/skills/paths.d.ts +11 -0
- package/dist/telemetry/skills/paths.js +61 -0
- package/dist/telemetry/skills/pi.d.ts +6 -0
- package/dist/telemetry/skills/pi.js +223 -0
- package/dist/telemetry/skills/shell_file_reads.d.ts +7 -0
- package/dist/telemetry/skills/shell_file_reads.js +226 -0
- package/dist/telemetry/skills/shell_words.d.ts +13 -0
- package/dist/telemetry/skills/shell_words.js +189 -0
- package/dist/telemetry/tools/input.d.ts +2 -0
- package/dist/telemetry/tools/input.js +14 -0
- package/dist/telemetry/tools/observations.d.ts +29 -0
- package/dist/telemetry/tools/observations.js +247 -0
- package/dist/telemetry/usage.d.ts +57 -0
- package/dist/telemetry/usage.js +136 -0
- package/dist/types.d.ts +97 -0
- package/dist/types.js +1 -0
- package/package.json +22 -7
- package/src/bun.ts +2 -2
- package/src/context/eval_catalog.ts +1 -1
- package/src/context/interaction_fingerprint.ts +9 -9
- package/src/context/interaction_fingerprint_format.ts +1 -1
- package/src/context/interaction_lease_depth.ts +1 -1
- package/src/context/pricing.ts +6 -3
- package/src/context/provider_prompt_accounting.ts +5 -5
- package/src/context/routing_context.ts +16 -16
- package/src/http/auth.ts +1 -1
- package/src/http/handler.ts +15 -8
- package/src/http/serve.ts +3 -3
- package/src/index.ts +14 -6
- package/src/ingress/anthropic_messages.ts +12 -12
- package/src/ingress/anthropic_models.ts +1 -1
- package/src/ingress/chat_completions.ts +17 -10
- package/src/ingress/claude_code/request_context.ts +2 -2
- package/src/ingress/codex.ts +14 -10
- package/src/ingress/codex_models.ts +4 -4
- package/src/ingress/json.ts +1 -1
- package/src/ingress/models.ts +22 -10
- package/src/ingress/muse_code.ts +9 -0
- package/src/ingress/muse_models.ts +36 -0
- package/src/interaction/capabilities.ts +1 -1
- package/src/interaction/portable_reasoning_state.ts +1 -1
- package/src/interaction/provider_continuation_state.ts +1 -1
- package/src/interaction/tool_identity.ts +2 -2
- package/src/interaction/types.ts +2 -0
- package/src/models/catalog.ts +310 -0
- package/src/models/context_limits.ts +1 -1
- package/src/models/execution_provider.ts +5 -2
- package/src/models/ids.ts +1 -1
- package/src/models/provider_defaults.ts +4 -1
- package/src/models/runtime_overrides.ts +23 -1
- package/src/models/subscriptions.ts +54 -0
- package/src/observability/errors.ts +2 -2
- package/src/platform/platform.ts +9 -5
- package/src/protocol/input/anthropic_messages.ts +7 -7
- package/src/protocol/input/chat_completions.ts +8 -6
- package/src/protocol/input/chat_completions_validation.ts +6 -5
- package/src/protocol/input/openai_responses.ts +6 -6
- package/src/protocol/output/anthropic_messages.ts +3 -3
- package/src/protocol/output/anthropic_messages_output.ts +6 -4
- package/src/protocol/output/anthropic_messages_stream.ts +7 -7
- package/src/protocol/output/chat_completions.ts +13 -13
- package/src/protocol/output/chat_reasoning_detail.ts +1 -1
- package/src/protocol/output/openai_responses.ts +3 -3
- package/src/protocol/output/openai_responses_output.ts +4 -2
- package/src/protocol/output/openai_responses_stream.ts +6 -6
- package/src/protocol/output/sse_stream.ts +3 -3
- package/src/providers/anthropic_safeguards.ts +2 -2
- package/src/providers/aws_assume_role.ts +2 -2
- package/src/providers/cancel_estimate.ts +1 -1
- package/src/providers/execution.ts +17 -17
- package/src/providers/pi.ts +34 -19
- package/src/providers/pi_context.ts +6 -6
- package/src/providers/pi_options.ts +27 -11
- package/src/providers/pi_output.ts +5 -5
- package/src/providers/provider_attempt_runner.ts +1 -1
- package/src/providers/provider_call.ts +21 -17
- package/src/providers/provider_reported_cost.ts +3 -3
- package/src/providers/reasoning.ts +2 -2
- package/src/providers/stream_finalization.ts +9 -9
- package/src/providers/subscription_limits.ts +24 -7
- package/src/runtime/claude_code/auxiliary_routing.ts +5 -5
- package/src/runtime/customer_disconnect.ts +1 -1
- package/src/runtime/explicit_model.ts +19 -20
- package/src/runtime/harness_identity/conversation.ts +1 -1
- package/src/runtime/harness_identity/extract.ts +3 -3
- package/src/runtime/harness_identity/index.ts +2 -2
- package/src/runtime/prepare_routed_completion.ts +35 -16
- package/src/runtime/provider_stream_lifecycle.ts +1 -1
- package/src/runtime/provider_stream_outcome.ts +2 -2
- package/src/runtime/request_tags.ts +2 -2
- package/src/runtime/routed_completion.ts +11 -11
- package/src/runtime/routed_protocol.ts +2 -2
- package/src/runtime/routed_runtime.ts +20 -19
- package/src/runtime/routing_lease.ts +7 -7
- package/src/runtime/runtime_reporter.ts +3 -3
- package/src/runtime/strict_provider_access.ts +42 -0
- package/src/self_host/main.ts +12 -12
- package/src/self_host/platform.ts +5 -5
- package/src/self_host/prefix_store.ts +2 -2
- package/src/self_host/resolve.ts +17 -31
- package/src/self_host/subscriptions/access.ts +2 -2
- package/src/self_host/subscriptions/http.ts +2 -2
- package/src/self_host/subscriptions/index.ts +4 -4
- package/src/self_host/subscriptions/oauth.ts +139 -8
- package/src/self_host/subscriptions/store.ts +2 -1
- package/src/strategies/auto_routing_selector.ts +2 -2
- package/src/strategies/eval_relevance.ts +3 -3
- package/src/strategies/priority_selector.ts +1 -1
- package/src/strategies/route_decision.ts +7 -7
- package/src/strategies/routing.ts +10 -10
- package/src/strategies/selector_projection.ts +2 -2
- package/src/telemetry/billing_finalization.ts +3 -3
- package/src/telemetry/model_cost.ts +1 -1
- package/src/telemetry/prefix_state.ts +4 -4
- package/src/telemetry/report.ts +21 -21
- package/src/telemetry/requests/interaction_observability.ts +1 -1
- package/src/telemetry/requests/retained_interaction_request.ts +1 -1
- package/src/telemetry/skills/claude/catalog.ts +1 -1
- package/src/telemetry/skills/claude/file_reads.ts +6 -6
- package/src/telemetry/skills/claude/observations.ts +3 -3
- package/src/telemetry/skills/codex/catalog.ts +3 -3
- package/src/telemetry/skills/codex/exec_file_reads.ts +4 -4
- package/src/telemetry/skills/codex/explicit.ts +3 -3
- package/src/telemetry/skills/codex/file_reads.ts +5 -5
- package/src/telemetry/skills/observations.ts +7 -7
- package/src/telemetry/skills/pi.ts +3 -3
- package/src/telemetry/skills/shell_file_reads.ts +2 -2
- package/src/telemetry/skills/shell_words.ts +1 -1
- package/src/telemetry/tools/input.ts +1 -1
- package/src/telemetry/tools/observations.ts +9 -9
- package/src/telemetry/usage.ts +5 -5
- package/src/types.ts +20 -3
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { prepareCandidates, } from "@mupt-ai/dari-router/policy-engine";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { HttpError } from "../http/errors.js";
|
|
4
|
+
import { effectiveContextWindow } from "../models/context_limits.js";
|
|
5
|
+
import { keySourceForModel } from "../models/execution_provider.js";
|
|
6
|
+
import { requiredCapabilitiesFor } from "../interaction/capabilities.js";
|
|
7
|
+
import { providerIdentityFromModel } from "../interaction/provider_continuation_state.js";
|
|
8
|
+
import { isBackgroundRequest } from "../interaction/types.js";
|
|
9
|
+
import { requireCandidateModels } from "../models/ids.js";
|
|
10
|
+
import { modelMetadataLookup } from "../providers/pi.js";
|
|
11
|
+
import { modelSupportsImageInput, modelSupportsHostedWebSearch, modelSupportsReasoningConfigurationUpdate, providerApiCanSerializeResponseFormat, } from "../providers/model_capabilities.js";
|
|
12
|
+
import { selectorMessagesForInteraction, } from "../strategies/selector_projection.js";
|
|
13
|
+
import { interactionPrefixContext, interactionPrefixLookupHashes, } from "./interaction_fingerprint.js";
|
|
14
|
+
import { pricingLookupForRouter } from "./pricing.js";
|
|
15
|
+
import { providerPromptAccounting } from "./provider_prompt_accounting.js";
|
|
16
|
+
import { leaseHistoryFromPrefixHits } from "./lease_history.js";
|
|
17
|
+
import { interactionLeaseFenceDepth } from "./interaction_lease_depth.js";
|
|
18
|
+
// Router resolution only needs lookup hashes. Model-specific chains are
|
|
19
|
+
// derived after resolve, when the enabled providers and APIs are known.
|
|
20
|
+
export function derivePrefixLookupHashes(routerId, interaction) {
|
|
21
|
+
try {
|
|
22
|
+
return interactionPrefixLookupHashes(routerId, interaction);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
console.error("prefix fingerprinting failed", error);
|
|
26
|
+
return [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function buildRoutingContext(args) {
|
|
30
|
+
const configuredCandidateModels = requireCandidateModels(args.credentials.enabled_models);
|
|
31
|
+
// With credits exhausted, route only among models a personal subscription
|
|
32
|
+
// serves. The credit gate refuses routers with none.
|
|
33
|
+
const fundedCandidateModels = args.credentials.credit_exhausted
|
|
34
|
+
? configuredCandidateModels.filter((model) => keySourceForModel(args.credentials, model) === "subscription")
|
|
35
|
+
: configuredCandidateModels;
|
|
36
|
+
const { messages: selectorMessages, images: selectorImages } = selectorMessagesForInteraction(args.interaction, {
|
|
37
|
+
imageInput: args.credentials.selector_image_input_enabled === true,
|
|
38
|
+
});
|
|
39
|
+
const leaseDepth = interactionLeaseFenceDepth(args.interaction);
|
|
40
|
+
const metadataLookup = modelMetadataLookup(args.credentials.model_configs);
|
|
41
|
+
const targetsByModel = new Map();
|
|
42
|
+
for (const model of configuredCandidateModels) {
|
|
43
|
+
targetsByModel.set(model, providerIdentityFromModel(metadataLookup(model).model));
|
|
44
|
+
}
|
|
45
|
+
const capabilityLookup = (modelId) => {
|
|
46
|
+
const metadata = metadataLookup(modelId);
|
|
47
|
+
return {
|
|
48
|
+
provider: typeof metadata.model.provider === "string" ? metadata.model.provider : "",
|
|
49
|
+
api: typeof metadata.model.api === "string" ? metadata.model.api : "",
|
|
50
|
+
supportsImageInput: modelSupportsImageInput(metadata),
|
|
51
|
+
supportsHostedWebSearch: modelSupportsHostedWebSearch(metadata),
|
|
52
|
+
supportsReasoningConfigurationUpdate: modelSupportsReasoningConfigurationUpdate(metadata),
|
|
53
|
+
supportsStructuredOutput: providerApiCanSerializeResponseFormat(metadata),
|
|
54
|
+
supportedThinkingLevels: metadata.supportedThinkingLevels,
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
// Image capability filtering (router setting, absent = ON) narrows the
|
|
58
|
+
// candidate set before strategy resolution — an image-bearing conversation
|
|
59
|
+
// must not route (or hold a lease) to a text-only model. Prefix chains and
|
|
60
|
+
// provider accounting still cover every configured model.
|
|
61
|
+
const imageFilter = resolveImageCapableCandidates({
|
|
62
|
+
candidateModels: fundedCandidateModels.length > 0 ? fundedCandidateModels : configuredCandidateModels,
|
|
63
|
+
requiredCapabilities: requiredCapabilitiesFor(args.interaction),
|
|
64
|
+
enabled: args.credentials.image_capability_filtering !== false,
|
|
65
|
+
supportsImageInput: (model) => capabilityLookup(model).supportsImageInput,
|
|
66
|
+
});
|
|
67
|
+
const prefix = derivePrefixContext({
|
|
68
|
+
routerId: args.routerId,
|
|
69
|
+
interaction: args.interaction,
|
|
70
|
+
targetsByModel,
|
|
71
|
+
});
|
|
72
|
+
const promptAccounting = providerPromptAccounting({
|
|
73
|
+
request: args.interaction,
|
|
74
|
+
prefix,
|
|
75
|
+
targetsByModel,
|
|
76
|
+
});
|
|
77
|
+
const pricing = pricingLookupForRouter(args.credentials);
|
|
78
|
+
// A validated harness identity is authoritative. Matching text from another
|
|
79
|
+
// thread must not contribute cache warmth or selector continuity.
|
|
80
|
+
const background = isBackgroundRequest(args.interaction);
|
|
81
|
+
const prefixHits = (background ? [] : args.credentials.prefix_hits ?? []).filter((hit) => args.explicitConversationId === undefined
|
|
82
|
+
|| hit.conversation_id === args.explicitConversationId);
|
|
83
|
+
const prepared = prepareCandidates({
|
|
84
|
+
candidateModels: imageFilter.candidateModels,
|
|
85
|
+
metadataLookup: capabilityLookup,
|
|
86
|
+
requiredCapabilities: imageFilter.requiredCapabilities,
|
|
87
|
+
requestedReasoningEffort: args.interaction.reasoning?.effort,
|
|
88
|
+
thinkingEnabled: args.interaction.reasoning?.enabled === true,
|
|
89
|
+
toolChoice: args.interaction.toolChoice,
|
|
90
|
+
modelThinkingLevels: args.credentials.model_thinking_levels,
|
|
91
|
+
strategy: args.credentials.routing_strategy === "custom" ? "custom" : "dari",
|
|
92
|
+
customConfig: args.credentials.custom_config,
|
|
93
|
+
modelPrices: args.credentials.model_prices ?? {},
|
|
94
|
+
pricing,
|
|
95
|
+
promptTokenLimitFor: (model) => effectiveContextWindow(args.credentials.model_configs?.[model], args.credentials.allow_long_context === true),
|
|
96
|
+
requiresPromptEstimateFor: (model) => args.credentials.allow_long_context !== true
|
|
97
|
+
&& args.credentials.model_configs?.[model]?.standard_context_limit != null,
|
|
98
|
+
averageOutputTokensByModel: Object.fromEntries(Object.entries(args.credentials.model_configs ?? {}).map(([model, config]) => [
|
|
99
|
+
model,
|
|
100
|
+
config.average_output_tokens ?? null,
|
|
101
|
+
])),
|
|
102
|
+
promptAccounting,
|
|
103
|
+
chainsByModel: prefix.chainByModel,
|
|
104
|
+
// Resolve returns hits up to the platform's conversation lookback;
|
|
105
|
+
// core narrows them to the warmth window for cost estimation and keeps the
|
|
106
|
+
// rest for conversation identity and the previous decision.
|
|
107
|
+
prefixHits,
|
|
108
|
+
nowMs: (args.receivedAt ?? new Date()).getTime(),
|
|
109
|
+
looseChain: prefix.looseChain,
|
|
110
|
+
conversationId: args.explicitConversationId,
|
|
111
|
+
toolChoiceFp: prefix.fingerprints.tool_choice_fp,
|
|
112
|
+
responseFormatFp: prefix.fingerprints.response_format_fp,
|
|
113
|
+
previousDecision: background ? undefined : args.previousDecision,
|
|
114
|
+
});
|
|
115
|
+
for (const warning of prepared.warnings) {
|
|
116
|
+
console.error("cache cost estimation failed", warning.error);
|
|
117
|
+
}
|
|
118
|
+
const conversationId = args.explicitConversationId
|
|
119
|
+
?? prepared.conversationId
|
|
120
|
+
?? randomUUID();
|
|
121
|
+
// A harness identity always produces an explicit conversation id, so it
|
|
122
|
+
// cannot distinguish a brand-new session from a recovered one on its own.
|
|
123
|
+
// Prefix lookup is already scoped to that id; no recovered conversation
|
|
124
|
+
// therefore means this is its first observed turn.
|
|
125
|
+
const isNewConversation = prepared.conversationId === null;
|
|
126
|
+
// Lease context (router setting, absent = ON) feeds the selector the
|
|
127
|
+
// original task and reconstructed lease history. Disabled routers skip the
|
|
128
|
+
// reconstruction entirely so their selector inputs stay byte-identical to
|
|
129
|
+
// the pre-lease-context behavior.
|
|
130
|
+
const leaseHistory = !background && args.credentials.lease_context_enabled !== false
|
|
131
|
+
? leaseHistoryFromPrefixHits({
|
|
132
|
+
prefixHits,
|
|
133
|
+
conversationId,
|
|
134
|
+
candidates: prepared.candidateResolution.candidates,
|
|
135
|
+
messages: selectorMessages,
|
|
136
|
+
})
|
|
137
|
+
: undefined;
|
|
138
|
+
return {
|
|
139
|
+
leaseDepth,
|
|
140
|
+
hits: prepared.hits,
|
|
141
|
+
reportingCostEstimates: prepared.costEstimates,
|
|
142
|
+
...(prepared.previousDecision !== undefined
|
|
143
|
+
? { previousDecision: prepared.previousDecision }
|
|
144
|
+
: {}),
|
|
145
|
+
...(prepared.activeLease !== undefined ? { activeLease: prepared.activeLease } : {}),
|
|
146
|
+
...(prepared.pendingLease !== undefined ? { pendingLease: prepared.pendingLease } : {}),
|
|
147
|
+
pricing,
|
|
148
|
+
conversationId,
|
|
149
|
+
isNewConversation,
|
|
150
|
+
identityMatchDepth: prepared.identityMatchDepth,
|
|
151
|
+
...(prepared.identityHit !== undefined
|
|
152
|
+
? { identityHit: prepared.identityHit }
|
|
153
|
+
: {}),
|
|
154
|
+
zdr: args.credentials.zdr === true,
|
|
155
|
+
candidateResolution: prepared.candidateResolution,
|
|
156
|
+
...(imageFilter.prunedModels.length > 0
|
|
157
|
+
? { imageCapabilityPrunedModels: imageFilter.prunedModels }
|
|
158
|
+
: {}),
|
|
159
|
+
prefix,
|
|
160
|
+
targetsByModel,
|
|
161
|
+
selectorMessages,
|
|
162
|
+
selectorImages,
|
|
163
|
+
...(leaseHistory !== undefined ? { leaseHistory } : {}),
|
|
164
|
+
providerBlockDepthFor: promptAccounting.providerBlockDepthFor,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
// When the filter is disabled, "image_input" is also dropped from the hard
|
|
168
|
+
// capabilities so core preparation keeps today's pass-through behavior instead
|
|
169
|
+
// of re-applying the same exclusion. When enabled, the capability stays as a
|
|
170
|
+
// core-side invariant over the already-narrowed set.
|
|
171
|
+
function resolveImageCapableCandidates(args) {
|
|
172
|
+
if (!args.requiredCapabilities.includes("image_input")) {
|
|
173
|
+
return {
|
|
174
|
+
candidateModels: args.candidateModels,
|
|
175
|
+
requiredCapabilities: args.requiredCapabilities,
|
|
176
|
+
prunedModels: [],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
if (!args.enabled) {
|
|
180
|
+
return {
|
|
181
|
+
candidateModels: args.candidateModels,
|
|
182
|
+
requiredCapabilities: args.requiredCapabilities.filter((capability) => capability !== "image_input"),
|
|
183
|
+
prunedModels: [],
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
const capable = args.candidateModels.filter(args.supportsImageInput);
|
|
187
|
+
if (capable.length === 0) {
|
|
188
|
+
throw new HttpError(400, "This conversation contains image input, but no enabled model on this router accepts images.", "no_image_capable_candidates");
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
candidateModels: capable,
|
|
192
|
+
requiredCapabilities: args.requiredCapabilities,
|
|
193
|
+
prunedModels: args.candidateModels
|
|
194
|
+
.filter((model) => !args.supportsImageInput(model))
|
|
195
|
+
.map((model) => ({ model, reason: "image_input_unsupported" })),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
function derivePrefixContext(args) {
|
|
199
|
+
try {
|
|
200
|
+
return interactionPrefixContext(args.routerId, args.interaction, args.targetsByModel);
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
console.error("prefix fingerprinting failed", error);
|
|
204
|
+
return {
|
|
205
|
+
chainByModel: new Map([...args.targetsByModel].map(([model]) => [model, []])),
|
|
206
|
+
itemIndexByHashByModel: new Map([...args.targetsByModel].map(([model]) => [model, new Map()])),
|
|
207
|
+
looseChain: [],
|
|
208
|
+
fingerprints: { tool_choice_fp: "", response_format_fp: "" },
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { HttpError } from "./errors.js";
|
|
3
|
+
export function bearerToken(headers) {
|
|
4
|
+
const value = headers.get("authorization") ?? "";
|
|
5
|
+
const [scheme, token] = value.split(/\s+/, 2);
|
|
6
|
+
if (scheme?.toLowerCase() !== "bearer" || !token) {
|
|
7
|
+
throw new HttpError(401, "Missing or invalid routing API key.", "authentication_error");
|
|
8
|
+
}
|
|
9
|
+
return publicRoutingToken(token);
|
|
10
|
+
}
|
|
11
|
+
export function anthropicToken(headers) {
|
|
12
|
+
const authorization = headers.get("authorization");
|
|
13
|
+
if (authorization)
|
|
14
|
+
return bearerToken(headers);
|
|
15
|
+
const apiKey = headers.get("x-api-key");
|
|
16
|
+
if (!apiKey) {
|
|
17
|
+
throw new HttpError(401, "Missing or invalid routing API key.", "authentication_error");
|
|
18
|
+
}
|
|
19
|
+
return publicRoutingToken(apiKey);
|
|
20
|
+
}
|
|
21
|
+
function publicRoutingToken(token) {
|
|
22
|
+
const internalToken = process.env.DARI_ROUTING_INTERNAL_TOKEN;
|
|
23
|
+
if (internalToken && matchesInternalToken(token, internalToken)) {
|
|
24
|
+
throw new HttpError(401, "Internal routing credentials cannot authorize public inference.", "authentication_error");
|
|
25
|
+
}
|
|
26
|
+
return token;
|
|
27
|
+
}
|
|
28
|
+
function matchesInternalToken(token, internalToken) {
|
|
29
|
+
// Compare byte lengths, not character counts: timingSafeEqual throws on
|
|
30
|
+
// buffers of different sizes, and a multi-byte character can match the
|
|
31
|
+
// secret's character count without matching its byte length.
|
|
32
|
+
const candidate = Buffer.from(token);
|
|
33
|
+
const expected = Buffer.from(internalToken);
|
|
34
|
+
return candidate.length === expected.length && timingSafeEqual(candidate, expected);
|
|
35
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { RouterCoreError } from "@mupt-ai/dari-router/policy-engine";
|
|
2
|
+
export declare class HttpError extends Error {
|
|
3
|
+
readonly status: number;
|
|
4
|
+
readonly code: string;
|
|
5
|
+
readonly param?: string;
|
|
6
|
+
readonly headers?: Record<string, string>;
|
|
7
|
+
constructor(status: number, message: string, code?: string, param?: string, headers?: Record<string, string>);
|
|
8
|
+
}
|
|
9
|
+
export declare function errorResponseHeaders(error: unknown): Record<string, string>;
|
|
10
|
+
export declare function httpErrorFrom(error: RouterCoreError): HttpError;
|
|
11
|
+
export declare function rethrowFrameworkErrorAsHttp(error: unknown, status: number): never;
|
|
12
|
+
export declare function errorCode(error: unknown, fallback: string): string;
|
|
13
|
+
export declare function errorResponse(error: unknown): Response;
|
|
14
|
+
export declare function normalizeError(error: unknown): {
|
|
15
|
+
status: number;
|
|
16
|
+
message: string;
|
|
17
|
+
code: string;
|
|
18
|
+
param?: string;
|
|
19
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { RouterFrameworkError } from "@mupt-ai/dari-router";
|
|
2
|
+
import { RouterCoreError } from "@mupt-ai/dari-router/policy-engine";
|
|
3
|
+
export class HttpError extends Error {
|
|
4
|
+
status;
|
|
5
|
+
code;
|
|
6
|
+
param;
|
|
7
|
+
// Response headers the error carries to the client, such as retry-after.
|
|
8
|
+
headers;
|
|
9
|
+
constructor(status, message, code = "invalid_request_error", param, headers) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.status = status;
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.param = param;
|
|
14
|
+
this.headers = headers;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function errorResponseHeaders(error) {
|
|
18
|
+
return error instanceof HttpError && error.headers ? error.headers : {};
|
|
19
|
+
}
|
|
20
|
+
// @mupt-ai/dari-router is transport-agnostic; its error kinds map onto the HTTP
|
|
21
|
+
// statuses this service always used for the same conditions.
|
|
22
|
+
const CORE_ERROR_STATUS = {
|
|
23
|
+
invalid_request: 400,
|
|
24
|
+
selector_output: 502,
|
|
25
|
+
configuration: 500,
|
|
26
|
+
};
|
|
27
|
+
export function httpErrorFrom(error) {
|
|
28
|
+
return new HttpError(CORE_ERROR_STATUS[error.kind], error.message, error.code, error.param);
|
|
29
|
+
}
|
|
30
|
+
export function rethrowFrameworkErrorAsHttp(error, status) {
|
|
31
|
+
throw error instanceof RouterFrameworkError
|
|
32
|
+
? new HttpError(status, error.message, error.code, error.param)
|
|
33
|
+
: error;
|
|
34
|
+
}
|
|
35
|
+
// Error code for logging/telemetry, covering both transport and core errors.
|
|
36
|
+
export function errorCode(error, fallback) {
|
|
37
|
+
if (error instanceof HttpError
|
|
38
|
+
|| error instanceof RouterCoreError
|
|
39
|
+
|| error instanceof RouterFrameworkError) {
|
|
40
|
+
return error.code;
|
|
41
|
+
}
|
|
42
|
+
return fallback;
|
|
43
|
+
}
|
|
44
|
+
export function errorResponse(error) {
|
|
45
|
+
const normalized = normalizeError(error);
|
|
46
|
+
return Response.json({
|
|
47
|
+
error: {
|
|
48
|
+
message: normalized.message,
|
|
49
|
+
type: openAiErrorType(normalized.status),
|
|
50
|
+
code: normalized.code,
|
|
51
|
+
...(normalized.param ? { param: normalized.param } : {}),
|
|
52
|
+
},
|
|
53
|
+
}, { status: normalized.status, headers: errorResponseHeaders(error) });
|
|
54
|
+
}
|
|
55
|
+
function openAiErrorType(status) {
|
|
56
|
+
if (status === 429)
|
|
57
|
+
return "rate_limit_error";
|
|
58
|
+
return status >= 500 ? "server_error" : "invalid_request_error";
|
|
59
|
+
}
|
|
60
|
+
export function normalizeError(error) {
|
|
61
|
+
if (error instanceof HttpError) {
|
|
62
|
+
return {
|
|
63
|
+
status: error.status,
|
|
64
|
+
message: error.message,
|
|
65
|
+
code: error.code,
|
|
66
|
+
param: error.param,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (error instanceof RouterCoreError) {
|
|
70
|
+
return {
|
|
71
|
+
status: CORE_ERROR_STATUS[error.kind],
|
|
72
|
+
message: error.message,
|
|
73
|
+
code: error.code,
|
|
74
|
+
param: error.param,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (error instanceof RouterFrameworkError) {
|
|
78
|
+
return {
|
|
79
|
+
status: error.status,
|
|
80
|
+
message: error.message,
|
|
81
|
+
code: error.code,
|
|
82
|
+
param: error.param,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
if (error instanceof Error) {
|
|
86
|
+
return { status: 500, message: error.message, code: "internal_error" };
|
|
87
|
+
}
|
|
88
|
+
return { status: 500, message: "Internal server error", code: "internal_error" };
|
|
89
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type GatewayPlatform } from "../platform/platform.js";
|
|
2
|
+
import type { SelectorBackend } from "../strategies/routing.js";
|
|
3
|
+
export type GatewayHandlerOptions = {
|
|
4
|
+
platform: GatewayPlatform;
|
|
5
|
+
selectorBackend: SelectorBackend;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* The gateway's routed HTTP surface: `/v1/{chat/completions,responses,
|
|
9
|
+
* messages,models}` on the default router and `/{router_id}/v1/...` on a
|
|
10
|
+
* named one. Binds `options.platform` for every request it serves.
|
|
11
|
+
*/
|
|
12
|
+
export declare function createRequestHandler(options: GatewayHandlerOptions): (request: Request) => Promise<Response>;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { handleResponses, handleResponsesWebSocketFallback, } from "../ingress/codex.js";
|
|
2
|
+
import { handleModels } from "../ingress/models.js";
|
|
3
|
+
import { handleMuseCodeModels } from "../ingress/muse_models.js";
|
|
4
|
+
import { handleChatCompletions } from "../ingress/chat_completions.js";
|
|
5
|
+
import { handleAnthropicMessages } from "../ingress/anthropic_messages.js";
|
|
6
|
+
import { bindGatewayPlatform } from "../platform/platform.js";
|
|
7
|
+
import { DEFAULT_ROUTER_ALIAS } from "../platform/routing_contract.js";
|
|
8
|
+
import { errorResponse, HttpError } from "./errors.js";
|
|
9
|
+
/**
|
|
10
|
+
* The gateway's routed HTTP surface: `/v1/{chat/completions,responses,
|
|
11
|
+
* messages,models}` on the default router and `/{router_id}/v1/...` on a
|
|
12
|
+
* named one. Binds `options.platform` for every request it serves.
|
|
13
|
+
*/
|
|
14
|
+
export function createRequestHandler(options) {
|
|
15
|
+
const { selectorBackend } = options;
|
|
16
|
+
bindGatewayPlatform(options.platform);
|
|
17
|
+
return async function handleRequest(request) {
|
|
18
|
+
const url = new URL(request.url);
|
|
19
|
+
if (request.method === "GET" &&
|
|
20
|
+
(url.pathname === "/healthz" || url.pathname === "/health")) {
|
|
21
|
+
return Response.json({ status: "ok" });
|
|
22
|
+
}
|
|
23
|
+
// Muse Code reads its model list from its base URL's origin; `dari muse`
|
|
24
|
+
// forwards it to the router's own path.
|
|
25
|
+
const museModels = url.pathname.match(/^(?:\/([^/]+))?\/muse-code\/models$/);
|
|
26
|
+
if (request.method === "GET" && museModels !== null) {
|
|
27
|
+
return await handleMuseCodeModels(request, museModels[1] ?? DEFAULT_ROUTER_ALIAS);
|
|
28
|
+
}
|
|
29
|
+
// Claude Code probes this first-party endpoint before its Messages call
|
|
30
|
+
// when ANTHROPIC_BASE_URL points at a compatible gateway.
|
|
31
|
+
if (request.method === "HEAD" && url.pathname === "/api/hello") {
|
|
32
|
+
return new Response(null, { status: 200 });
|
|
33
|
+
}
|
|
34
|
+
const endpoint = routedEndpoint(url.pathname);
|
|
35
|
+
if (endpoint?.protocol === "chat" && request.method === "POST") {
|
|
36
|
+
return await handleChatCompletions(request, endpoint.routerId, selectorBackend);
|
|
37
|
+
}
|
|
38
|
+
if (endpoint?.protocol === "responses") {
|
|
39
|
+
if (request.method === "GET")
|
|
40
|
+
return handleResponsesWebSocketFallback();
|
|
41
|
+
if (request.method === "POST") {
|
|
42
|
+
return await handleResponses(request, endpoint.routerId, selectorBackend);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (endpoint?.protocol === "messages" && request.method === "POST") {
|
|
46
|
+
return await handleAnthropicMessages(request, endpoint.routerId, selectorBackend);
|
|
47
|
+
}
|
|
48
|
+
if (endpoint?.protocol === "models" && request.method === "GET") {
|
|
49
|
+
return await handleModels(request, endpoint.routerId);
|
|
50
|
+
}
|
|
51
|
+
return errorResponse(new HttpError(404, "Not found", "not_found"));
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function routedEndpoint(pathname) {
|
|
55
|
+
const defaultProtocol = protocolForSuffix(pathname, "/v1");
|
|
56
|
+
if (defaultProtocol !== null) {
|
|
57
|
+
return { routerId: DEFAULT_ROUTER_ALIAS, protocol: defaultProtocol };
|
|
58
|
+
}
|
|
59
|
+
const match = pathname.match(/^\/([^/]+)(?:\/v1)?\/(chat\/completions|responses|messages|models)$/);
|
|
60
|
+
if (match === null)
|
|
61
|
+
return null;
|
|
62
|
+
return {
|
|
63
|
+
routerId: match[1],
|
|
64
|
+
protocol: protocolForRoute(match[2]),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function protocolForSuffix(pathname, prefix) {
|
|
68
|
+
if (pathname === `${prefix}/chat/completions`)
|
|
69
|
+
return "chat";
|
|
70
|
+
if (pathname === `${prefix}/responses`)
|
|
71
|
+
return "responses";
|
|
72
|
+
if (pathname === `${prefix}/messages`)
|
|
73
|
+
return "messages";
|
|
74
|
+
if (pathname === `${prefix}/models`)
|
|
75
|
+
return "models";
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
function protocolForRoute(route) {
|
|
79
|
+
if (route === "chat/completions")
|
|
80
|
+
return "chat";
|
|
81
|
+
if (route === "responses")
|
|
82
|
+
return "responses";
|
|
83
|
+
if (route === "messages")
|
|
84
|
+
return "messages";
|
|
85
|
+
if (route === "models")
|
|
86
|
+
return "models";
|
|
87
|
+
throw new Error(`Unsupported routed endpoint: ${route}`);
|
|
88
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type Http2RequestHandler = (request: Request) => Response | Promise<Response>;
|
|
2
|
+
export type Http2RoutingServer = {
|
|
3
|
+
readonly port: number;
|
|
4
|
+
stop(force?: boolean): Promise<void>;
|
|
5
|
+
};
|
|
6
|
+
export declare function startHttp2Server(port: number, handler: Http2RequestHandler): Http2RoutingServer;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { createServer, } from "node:http2";
|
|
2
|
+
import { Readable } from "node:stream";
|
|
3
|
+
import { pipeline } from "node:stream/promises";
|
|
4
|
+
const HTTP2_FORBIDDEN_RESPONSE_HEADERS = new Set([
|
|
5
|
+
"connection",
|
|
6
|
+
"keep-alive",
|
|
7
|
+
"proxy-connection",
|
|
8
|
+
"transfer-encoding",
|
|
9
|
+
"upgrade",
|
|
10
|
+
]);
|
|
11
|
+
export function startHttp2Server(port, handler) {
|
|
12
|
+
const sessions = new Set();
|
|
13
|
+
let closePromise;
|
|
14
|
+
const server = createServer((request, response) => {
|
|
15
|
+
void handleRequest(request, response, handler);
|
|
16
|
+
});
|
|
17
|
+
server.on("session", (session) => {
|
|
18
|
+
sessions.add(session);
|
|
19
|
+
session.once("close", () => sessions.delete(session));
|
|
20
|
+
});
|
|
21
|
+
server.listen(port, "0.0.0.0");
|
|
22
|
+
const address = server.address();
|
|
23
|
+
if (address === null || typeof address === "string") {
|
|
24
|
+
throw new Error("HTTP/2 routing server did not bind to a TCP port.");
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
port: address.port,
|
|
28
|
+
stop(force = false) {
|
|
29
|
+
for (const session of sessions) {
|
|
30
|
+
if (force)
|
|
31
|
+
session.destroy();
|
|
32
|
+
else
|
|
33
|
+
session.close();
|
|
34
|
+
}
|
|
35
|
+
closePromise ??= new Promise((resolve, reject) => {
|
|
36
|
+
server.close((error) => {
|
|
37
|
+
if (error)
|
|
38
|
+
reject(error);
|
|
39
|
+
else
|
|
40
|
+
resolve();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
return closePromise;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
async function handleRequest(incoming, outgoing, handler) {
|
|
48
|
+
const abort = new AbortController();
|
|
49
|
+
incoming.once("aborted", () => abort.abort());
|
|
50
|
+
outgoing.once("close", () => {
|
|
51
|
+
if (!outgoing.writableEnded)
|
|
52
|
+
abort.abort();
|
|
53
|
+
});
|
|
54
|
+
try {
|
|
55
|
+
const response = await handler(toRequest(incoming, abort.signal));
|
|
56
|
+
outgoing.statusCode = response.status;
|
|
57
|
+
for (const [name, value] of response.headers) {
|
|
58
|
+
if (!HTTP2_FORBIDDEN_RESPONSE_HEADERS.has(name.toLowerCase())) {
|
|
59
|
+
outgoing.setHeader(name, value);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (response.body === null || incoming.method === "HEAD") {
|
|
63
|
+
outgoing.end();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
await pipeline(Readable.fromWeb(response.body), outgoing);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
if (abort.signal.aborted || outgoing.destroyed)
|
|
70
|
+
return;
|
|
71
|
+
console.error("routing HTTP/2 request failed", error);
|
|
72
|
+
if (outgoing.headersSent) {
|
|
73
|
+
outgoing.destroy();
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
outgoing.statusCode = 500;
|
|
77
|
+
outgoing.setHeader("content-type", "application/json");
|
|
78
|
+
outgoing.end(JSON.stringify({ error: { message: "Internal server error." } }));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function toRequest(incoming, signal) {
|
|
82
|
+
const headers = new Headers();
|
|
83
|
+
for (const [name, value] of Object.entries(incoming.headers)) {
|
|
84
|
+
if (name.startsWith(":"))
|
|
85
|
+
continue;
|
|
86
|
+
if (Array.isArray(value)) {
|
|
87
|
+
for (const item of value)
|
|
88
|
+
headers.append(name, item);
|
|
89
|
+
}
|
|
90
|
+
else if (value !== undefined) {
|
|
91
|
+
headers.set(name, value);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const method = incoming.method ?? "GET";
|
|
95
|
+
const hasBody = method !== "GET" && method !== "HEAD";
|
|
96
|
+
return new Request(`${incoming.scheme ?? "http"}://${incoming.authority ?? "localhost"}${incoming.url}`, {
|
|
97
|
+
method,
|
|
98
|
+
headers,
|
|
99
|
+
body: hasBody ? Readable.toWeb(incoming) : undefined,
|
|
100
|
+
signal,
|
|
101
|
+
duplex: hasBody ? "half" : undefined,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
type StoppableServer = {
|
|
2
|
+
stop(force?: boolean): Promise<void>;
|
|
3
|
+
};
|
|
4
|
+
export declare function startServer(listenPort: number, handler: (request: Request) => Promise<Response>, options?: {
|
|
5
|
+
http2?: boolean;
|
|
6
|
+
}): import("./http2_server.js").Http2RoutingServer | Bun.Server<undefined>;
|
|
7
|
+
/**
|
|
8
|
+
* On SIGTERM/SIGINT: stop accepting work, let active requests finish, drain
|
|
9
|
+
* background reporting within the shutdown budget, run `closeClients`, exit.
|
|
10
|
+
*/
|
|
11
|
+
export declare function installShutdownHandlers(server: StoppableServer, closeClients: () => Promise<unknown>): void;
|
|
12
|
+
/**
|
|
13
|
+
* stop(false) refuses new work and lets active requests finish; if that does
|
|
14
|
+
* not settle in time, one stop(true) destroys what is left so stream
|
|
15
|
+
* cancellation registers its finalizers before the background drain starts.
|
|
16
|
+
* Both calls are bounded: a hung stop promise must not consume the shutdown
|
|
17
|
+
* budget.
|
|
18
|
+
*/
|
|
19
|
+
export declare function stopServerForShutdown(server: StoppableServer, gracefulTimeoutMs?: number, forceStopSettleTimeoutMs?: number): Promise<void>;
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { captureOperationalError } from "../observability/errors.js";
|
|
2
|
+
import { drainBackgroundTelemetryFor } from "../telemetry/background.js";
|
|
3
|
+
import { startHttp2Server } from "./http2_server.js";
|
|
4
|
+
const GRACEFUL_SERVER_STOP_TIMEOUT_MS = 5_000;
|
|
5
|
+
const FORCE_STOP_SETTLE_TIMEOUT_MS = 250;
|
|
6
|
+
// Container platforms commonly send SIGKILL 10 seconds after SIGTERM. Reserve
|
|
7
|
+
// the final 2 seconds for closing telemetry clients and 500ms for process
|
|
8
|
+
// scheduling.
|
|
9
|
+
const SHUTDOWN_WORK_TIMEOUT_MS = 7_500;
|
|
10
|
+
export function startServer(listenPort, handler, options = {}) {
|
|
11
|
+
if (options.http2) {
|
|
12
|
+
return startHttp2Server(listenPort, handler);
|
|
13
|
+
}
|
|
14
|
+
// The selector runs before the first response byte, and routed models can
|
|
15
|
+
// remain silent while reasoning. Disable Bun's default 10s idle timeout;
|
|
16
|
+
// clients and infrastructure still bound total request lifetime.
|
|
17
|
+
return Bun.serve({
|
|
18
|
+
port: listenPort,
|
|
19
|
+
fetch: handler,
|
|
20
|
+
idleTimeout: 0,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* On SIGTERM/SIGINT: stop accepting work, let active requests finish, drain
|
|
25
|
+
* background reporting within the shutdown budget, run `closeClients`, exit.
|
|
26
|
+
*/
|
|
27
|
+
export function installShutdownHandlers(server, closeClients) {
|
|
28
|
+
let shuttingDown = false;
|
|
29
|
+
const shutdown = async (signal) => {
|
|
30
|
+
if (shuttingDown)
|
|
31
|
+
return;
|
|
32
|
+
shuttingDown = true;
|
|
33
|
+
const workDeadlineAtMs = performance.now() + SHUTDOWN_WORK_TIMEOUT_MS;
|
|
34
|
+
console.info("routing server shutting down", JSON.stringify({ signal }));
|
|
35
|
+
try {
|
|
36
|
+
await stopServerForShutdown(server);
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
const remaining = await drainBackgroundTelemetryFor(Math.max(0, workDeadlineAtMs - performance.now()));
|
|
40
|
+
if (remaining > 0) {
|
|
41
|
+
console.warn("routing server background drain timed out", JSON.stringify({ remaining_task_count: remaining }));
|
|
42
|
+
captureOperationalError(new Error(`Routing shutdown abandoned ${remaining} background task(s).`), { code: "background_drain_timeout" });
|
|
43
|
+
}
|
|
44
|
+
await closeClients();
|
|
45
|
+
process.exit(0);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
process.once("SIGTERM", () => void shutdown("SIGTERM"));
|
|
49
|
+
process.once("SIGINT", () => void shutdown("SIGINT"));
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* stop(false) refuses new work and lets active requests finish; if that does
|
|
53
|
+
* not settle in time, one stop(true) destroys what is left so stream
|
|
54
|
+
* cancellation registers its finalizers before the background drain starts.
|
|
55
|
+
* Both calls are bounded: a hung stop promise must not consume the shutdown
|
|
56
|
+
* budget.
|
|
57
|
+
*/
|
|
58
|
+
export async function stopServerForShutdown(server, gracefulTimeoutMs = GRACEFUL_SERVER_STOP_TIMEOUT_MS, forceStopSettleTimeoutMs = FORCE_STOP_SETTLE_TIMEOUT_MS) {
|
|
59
|
+
if ((await stopWithin(server, false, gracefulTimeoutMs)) !== "timed_out") {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if ((await stopWithin(server, true, forceStopSettleTimeoutMs)) === "timed_out") {
|
|
63
|
+
reportServerStopFailure(new Error("Routing server force-stop settlement timed out."));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function stopWithin(server, force, timeoutMs) {
|
|
67
|
+
return Promise.race([
|
|
68
|
+
new Promise((resolve) => resolve(server.stop(force))).then(() => "stopped", (error) => {
|
|
69
|
+
reportServerStopFailure(error);
|
|
70
|
+
return "failed";
|
|
71
|
+
}),
|
|
72
|
+
Bun.sleep(timeoutMs).then(() => "timed_out"),
|
|
73
|
+
]);
|
|
74
|
+
}
|
|
75
|
+
function reportServerStopFailure(error) {
|
|
76
|
+
console.warn("routing server shutdown failed", error instanceof Error ? error.name : "UnknownError");
|
|
77
|
+
captureOperationalError(error, { code: "server_force_stop_failed" });
|
|
78
|
+
}
|