@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,50 @@
|
|
|
1
|
+
import { errorCode } from "../http/errors.js";
|
|
2
|
+
import { resolveSelfHostedRouter } from "./resolve.js";
|
|
3
|
+
/**
|
|
4
|
+
* The self-hosted platform: routers from the config file, subscriptions and
|
|
5
|
+
* prefix-cache state in the gateway's SQLite database, and one content-free
|
|
6
|
+
* log line per request.
|
|
7
|
+
*/
|
|
8
|
+
export function createSelfHostedPlatform(deps) {
|
|
9
|
+
const resolver = {
|
|
10
|
+
...deps,
|
|
11
|
+
prefixHits: (args) => deps.prefixes.hits(args),
|
|
12
|
+
};
|
|
13
|
+
return {
|
|
14
|
+
resolveRouter: (request) => resolveSelfHostedRouter(resolver, request),
|
|
15
|
+
writePrefixState: async (write) => {
|
|
16
|
+
deps.prefixes.write(write);
|
|
17
|
+
},
|
|
18
|
+
writePendingLease: async (write) => {
|
|
19
|
+
deps.prefixes.writePendingLease(write, deps.config.routers[write.router_id]?.zdr ?? false);
|
|
20
|
+
},
|
|
21
|
+
markSubscriptionUnavailable: async (hold) => hold.credentialId !== undefined &&
|
|
22
|
+
deps.subscriptions.markUnavailable(hold.credentialId, hold.limit.resetsAt, hold.limit.reason),
|
|
23
|
+
async recordUsage() {
|
|
24
|
+
// Resolution never reports a "managed" key source here.
|
|
25
|
+
throw new Error("The self-hosted gateway has no managed provider keys to account.");
|
|
26
|
+
},
|
|
27
|
+
recordRequest(record) {
|
|
28
|
+
console.info("routing request", JSON.stringify(record.summary));
|
|
29
|
+
},
|
|
30
|
+
captureRequestError(error, context) {
|
|
31
|
+
if (context.status < 500)
|
|
32
|
+
return;
|
|
33
|
+
console.error("routing request error", JSON.stringify({
|
|
34
|
+
router_id: context.routerId,
|
|
35
|
+
request_id: context.requestId ?? null,
|
|
36
|
+
source_protocol: context.sourceProtocol,
|
|
37
|
+
status: context.status,
|
|
38
|
+
error_code: errorCode(error, "internal_error"),
|
|
39
|
+
}));
|
|
40
|
+
},
|
|
41
|
+
captureOperationalError(error, context) {
|
|
42
|
+
console.error("gateway operational error", JSON.stringify({
|
|
43
|
+
code: context.code,
|
|
44
|
+
request_id: context.requestId ?? null,
|
|
45
|
+
router_id: context.routerId ?? null,
|
|
46
|
+
error: error instanceof Error ? error.message : String(error),
|
|
47
|
+
}));
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite storage for per-turn conversation prefix state: the entries that
|
|
3
|
+
* resolve returns as prefix hits (cache warmth, conversation identity, the
|
|
4
|
+
* previous routing decision) and the prefetched lease stamped onto them.
|
|
5
|
+
*
|
|
6
|
+
* Rows hold hashes, identifiers, and token counts. Under ZDR the free-text
|
|
7
|
+
* fields (`reason`, pending-lease reason and selector output) are dropped
|
|
8
|
+
* before they are written.
|
|
9
|
+
*
|
|
10
|
+
* Freshness only advances: every write is guarded by `updated_at`, so
|
|
11
|
+
* duplicate or out-of-order applications of the same turn are no-ops.
|
|
12
|
+
* Timestamps are stored as epoch milliseconds.
|
|
13
|
+
*/
|
|
14
|
+
import type { Database } from "bun:sqlite";
|
|
15
|
+
import type { RouterPrefixHit } from "@mupt-ai/dari-router/policy-engine";
|
|
16
|
+
import type { PendingLeaseWrite, PrefixStateWrite } from "../platform/platform.js";
|
|
17
|
+
export declare class PrefixStore {
|
|
18
|
+
private readonly db;
|
|
19
|
+
constructor(db: Database);
|
|
20
|
+
/**
|
|
21
|
+
* Apply one turn's prefix entry and/or refresh the entry whose cache it
|
|
22
|
+
* read. Freshness is stamped with the request's `received_at` (provider
|
|
23
|
+
* caches are read at the start of the serving request), never write time.
|
|
24
|
+
* Returns false, writing nothing, when the entry violates a constraint.
|
|
25
|
+
*/
|
|
26
|
+
write(write: PrefixStateWrite, now?: Date): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Stamp (or clear, with a null lease) the conversation's prefetched lease
|
|
29
|
+
* on every recent row: the prefetch lands asynchronously, and the expiry
|
|
30
|
+
* turn's lookup must find it on whichever row is deepest by then. Writes
|
|
31
|
+
* decided shallower than the last applied one are stale and skipped.
|
|
32
|
+
* `zdr` is the router's setting. Returns the number of rows updated.
|
|
33
|
+
*/
|
|
34
|
+
writePendingLease(write: PendingLeaseWrite, zdr: boolean, now?: Date): number;
|
|
35
|
+
/**
|
|
36
|
+
* Recent entries of the router matching `prefixHashes` on either hash tier,
|
|
37
|
+
* then (for lease recovery) recent entries of `conversationId`. Each lookup
|
|
38
|
+
* is newest-first, deepest-first within a timestamp, and capped; duplicates
|
|
39
|
+
* keep their first position.
|
|
40
|
+
*/
|
|
41
|
+
hits(args: {
|
|
42
|
+
routerId: string;
|
|
43
|
+
prefixHashes: readonly string[];
|
|
44
|
+
conversationId?: string;
|
|
45
|
+
}, now?: Date): RouterPrefixHit[];
|
|
46
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { ROUTING_PREFIX_HASHES_MAX_COUNT } from "../platform/routing_contract.js";
|
|
2
|
+
// Resolve returns entries up to this age so a continuation after a pause still
|
|
3
|
+
// recovers its conversation id and previous decision; the runtime itself
|
|
4
|
+
// downgrades hits past the provider cache-warmth window to identity-only.
|
|
5
|
+
const CONVERSATION_LOOKBACK_MS = 30 * 60_000;
|
|
6
|
+
// Rows older than this are deleted. Must stay above the lookback.
|
|
7
|
+
const RETENTION_MS = 60 * 60_000;
|
|
8
|
+
const CONVERSATION_ID_MAX_CHARS = 64;
|
|
9
|
+
const MODEL_NAME_MAX_CHARS = 240;
|
|
10
|
+
const REASON_MAX_CHARS = 2000;
|
|
11
|
+
const REASONING_BUCKET_MAX_CHARS = 32;
|
|
12
|
+
// A 512-token thinking trace is ~3k chars; the cap only guards against a
|
|
13
|
+
// runaway completion being fanned across every recent row.
|
|
14
|
+
const PENDING_LEASE_OUTPUT_TEXT_MAX_CHARS = 20_000;
|
|
15
|
+
// Pending-lease columns are owned by writePendingLease: re-applying a turn
|
|
16
|
+
// must not wipe a stamp that landed in between, so the upsert leaves them alone
|
|
17
|
+
// (the fence column is only ever raised).
|
|
18
|
+
const ENTRY_COLUMNS = [
|
|
19
|
+
"prefix_hash", "loose_prefix_hash", "router_id", "organization_id", "conversation_id",
|
|
20
|
+
"model", "provider", "next_model", "next_reasoning_effort", "message_depth",
|
|
21
|
+
"provider_block_depth", "prompt_anchor_depth", "input_tokens", "cache_read_tokens",
|
|
22
|
+
"cache_write_tokens", "prompt_tokens", "benchmark_replay_depth", "tool_choice_fp",
|
|
23
|
+
"response_format_fp", "reasoning_bucket", "lease_turns_remaining", "reason", "updated_at",
|
|
24
|
+
];
|
|
25
|
+
const PENDING_COLUMNS = [
|
|
26
|
+
"pending_lease_model", "pending_lease_reasoning_effort", "pending_lease_turns",
|
|
27
|
+
"pending_lease_reason", "pending_lease_output_text", "pending_lease_decided_at_depth",
|
|
28
|
+
];
|
|
29
|
+
const UPSERT_SQL = `INSERT INTO routing_prefix_entries (${[...ENTRY_COLUMNS, ...PENDING_COLUMNS].join(", ")})
|
|
30
|
+
VALUES (${[...ENTRY_COLUMNS, ...PENDING_COLUMNS].map((column) => `$${column}`).join(", ")})
|
|
31
|
+
ON CONFLICT (prefix_hash) DO UPDATE SET
|
|
32
|
+
${ENTRY_COLUMNS.filter((column) => column !== "prefix_hash").map((column) => `${column} = excluded.${column}`).join(",\n ")},
|
|
33
|
+
pending_lease_decided_at_depth = max(
|
|
34
|
+
coalesce(routing_prefix_entries.pending_lease_decided_at_depth, 0),
|
|
35
|
+
excluded.pending_lease_decided_at_depth
|
|
36
|
+
)
|
|
37
|
+
WHERE routing_prefix_entries.updated_at < excluded.updated_at`;
|
|
38
|
+
export class PrefixStore {
|
|
39
|
+
db;
|
|
40
|
+
constructor(db) {
|
|
41
|
+
this.db = db;
|
|
42
|
+
db.run(`CREATE TABLE IF NOT EXISTS routing_prefix_entries (
|
|
43
|
+
prefix_hash TEXT PRIMARY KEY,
|
|
44
|
+
loose_prefix_hash TEXT,
|
|
45
|
+
router_id TEXT NOT NULL,
|
|
46
|
+
organization_id TEXT,
|
|
47
|
+
conversation_id TEXT NOT NULL,
|
|
48
|
+
model TEXT NOT NULL,
|
|
49
|
+
provider TEXT,
|
|
50
|
+
next_model TEXT,
|
|
51
|
+
next_reasoning_effort TEXT,
|
|
52
|
+
message_depth INTEGER NOT NULL,
|
|
53
|
+
provider_block_depth INTEGER,
|
|
54
|
+
prompt_anchor_depth INTEGER,
|
|
55
|
+
input_tokens INTEGER NOT NULL,
|
|
56
|
+
cache_read_tokens INTEGER NOT NULL,
|
|
57
|
+
cache_write_tokens INTEGER NOT NULL,
|
|
58
|
+
prompt_tokens INTEGER NOT NULL,
|
|
59
|
+
benchmark_replay_depth INTEGER,
|
|
60
|
+
tool_choice_fp TEXT NOT NULL,
|
|
61
|
+
response_format_fp TEXT NOT NULL,
|
|
62
|
+
reasoning_bucket TEXT,
|
|
63
|
+
lease_turns_remaining INTEGER,
|
|
64
|
+
pending_lease_model TEXT,
|
|
65
|
+
pending_lease_reasoning_effort TEXT,
|
|
66
|
+
pending_lease_turns INTEGER,
|
|
67
|
+
pending_lease_reason TEXT,
|
|
68
|
+
pending_lease_output_text TEXT,
|
|
69
|
+
pending_lease_decided_at_depth INTEGER,
|
|
70
|
+
reason TEXT,
|
|
71
|
+
updated_at INTEGER NOT NULL
|
|
72
|
+
)`);
|
|
73
|
+
db.run("CREATE INDEX IF NOT EXISTS ix_routing_prefix_entries_updated_at ON routing_prefix_entries (updated_at)");
|
|
74
|
+
db.run("CREATE INDEX IF NOT EXISTS ix_routing_prefix_entries_router_id ON routing_prefix_entries (router_id)");
|
|
75
|
+
db.run("CREATE INDEX IF NOT EXISTS ix_routing_prefix_entries_conversation_id ON routing_prefix_entries (conversation_id)");
|
|
76
|
+
db.run("CREATE INDEX IF NOT EXISTS ix_routing_prefix_entries_loose_prefix_hash ON routing_prefix_entries (loose_prefix_hash)");
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Apply one turn's prefix entry and/or refresh the entry whose cache it
|
|
80
|
+
* read. Freshness is stamped with the request's `received_at` (provider
|
|
81
|
+
* caches are read at the start of the serving request), never write time.
|
|
82
|
+
* Returns false, writing nothing, when the entry violates a constraint.
|
|
83
|
+
*/
|
|
84
|
+
write(write, now = new Date()) {
|
|
85
|
+
const cacheClock = Date.parse(write.received_at);
|
|
86
|
+
if (!Number.isFinite(cacheClock))
|
|
87
|
+
throw new Error("received_at must be an ISO timestamp.");
|
|
88
|
+
const apply = this.db.transaction(() => {
|
|
89
|
+
const entry = write.prefix_entry;
|
|
90
|
+
if (entry !== null) {
|
|
91
|
+
this.db.query(UPSERT_SQL).run(entryValues(write, entry, cacheClock));
|
|
92
|
+
}
|
|
93
|
+
if (write.touched_prefix_hash !== null) {
|
|
94
|
+
this.db
|
|
95
|
+
.query("UPDATE routing_prefix_entries SET updated_at = ? WHERE prefix_hash = ? AND updated_at < ?")
|
|
96
|
+
.run(cacheClock, write.touched_prefix_hash, cacheClock);
|
|
97
|
+
}
|
|
98
|
+
// Deterministic sweep; expired rows are already invisible to hits().
|
|
99
|
+
this.db.query("DELETE FROM routing_prefix_entries WHERE updated_at < ?").run(now.getTime() - RETENTION_MS);
|
|
100
|
+
});
|
|
101
|
+
try {
|
|
102
|
+
apply();
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (isConstraintError(error))
|
|
106
|
+
return false;
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Stamp (or clear, with a null lease) the conversation's prefetched lease
|
|
113
|
+
* on every recent row: the prefetch lands asynchronously, and the expiry
|
|
114
|
+
* turn's lookup must find it on whichever row is deepest by then. Writes
|
|
115
|
+
* decided shallower than the last applied one are stale and skipped.
|
|
116
|
+
* `zdr` is the router's setting. Returns the number of rows updated.
|
|
117
|
+
*/
|
|
118
|
+
writePendingLease(write, zdr, now = new Date()) {
|
|
119
|
+
const lease = write.pending_lease;
|
|
120
|
+
const result = this.db
|
|
121
|
+
.query(`UPDATE routing_prefix_entries SET
|
|
122
|
+
pending_lease_model = $model,
|
|
123
|
+
pending_lease_reasoning_effort = $effort,
|
|
124
|
+
pending_lease_turns = $turns,
|
|
125
|
+
pending_lease_reason = $reason,
|
|
126
|
+
pending_lease_output_text = $output,
|
|
127
|
+
pending_lease_decided_at_depth = $depth
|
|
128
|
+
WHERE router_id = $router AND conversation_id = $conversation AND updated_at > $cutoff
|
|
129
|
+
AND (pending_lease_decided_at_depth IS NULL OR pending_lease_decided_at_depth <= $depth)`)
|
|
130
|
+
.run({
|
|
131
|
+
$model: lease?.model ?? null,
|
|
132
|
+
$effort: lease?.reasoning_effort ?? null,
|
|
133
|
+
$turns: lease?.turns ?? null,
|
|
134
|
+
$reason: zdr ? null : lease?.reason ?? null,
|
|
135
|
+
$output: zdr ? null : clip(lease?.output_text, PENDING_LEASE_OUTPUT_TEXT_MAX_CHARS),
|
|
136
|
+
$depth: write.decided_at_depth,
|
|
137
|
+
$router: write.router_id,
|
|
138
|
+
$conversation: write.conversation_id,
|
|
139
|
+
$cutoff: now.getTime() - CONVERSATION_LOOKBACK_MS,
|
|
140
|
+
});
|
|
141
|
+
return result.changes;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Recent entries of the router matching `prefixHashes` on either hash tier,
|
|
145
|
+
* then (for lease recovery) recent entries of `conversationId`. Each lookup
|
|
146
|
+
* is newest-first, deepest-first within a timestamp, and capped; duplicates
|
|
147
|
+
* keep their first position.
|
|
148
|
+
*/
|
|
149
|
+
hits(args, now = new Date()) {
|
|
150
|
+
const hashes = args.prefixHashes.slice(-ROUTING_PREFIX_HASHES_MAX_COUNT);
|
|
151
|
+
const cutoff = now.getTime() - CONVERSATION_LOOKBACK_MS;
|
|
152
|
+
const recent = (filter, params) => this.db
|
|
153
|
+
.query(`SELECT * FROM routing_prefix_entries
|
|
154
|
+
WHERE router_id = ? AND updated_at > ? AND (${filter})
|
|
155
|
+
ORDER BY updated_at DESC, message_depth DESC
|
|
156
|
+
LIMIT ${ROUTING_PREFIX_HASHES_MAX_COUNT}`)
|
|
157
|
+
.all(args.routerId, cutoff, ...params);
|
|
158
|
+
const rows = [];
|
|
159
|
+
if (hashes.length > 0) {
|
|
160
|
+
const placeholders = hashes.map(() => "?").join(", ");
|
|
161
|
+
rows.push(...recent(`prefix_hash IN (${placeholders}) OR loose_prefix_hash IN (${placeholders})`, [...hashes, ...hashes]));
|
|
162
|
+
}
|
|
163
|
+
if (args.conversationId)
|
|
164
|
+
rows.push(...recent("conversation_id = ?", [args.conversationId]));
|
|
165
|
+
const unique = new Map();
|
|
166
|
+
for (const row of rows)
|
|
167
|
+
if (!unique.has(row.prefix_hash))
|
|
168
|
+
unique.set(row.prefix_hash, row);
|
|
169
|
+
return [...unique.values()].map(toHit);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function entryValues(write, entry, cacheClock) {
|
|
173
|
+
const text = (key, maxChars) => {
|
|
174
|
+
const value = entry[key];
|
|
175
|
+
if (value === undefined || value === null)
|
|
176
|
+
return null;
|
|
177
|
+
if (typeof value !== "string")
|
|
178
|
+
throw new Error(`prefix_entry.${key} must be a string.`);
|
|
179
|
+
return maxChars === undefined ? value : clip(value, maxChars);
|
|
180
|
+
};
|
|
181
|
+
const int = (key) => {
|
|
182
|
+
const value = entry[key];
|
|
183
|
+
if (value === undefined || value === null)
|
|
184
|
+
return null;
|
|
185
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
186
|
+
throw new Error(`prefix_entry.${key} must be an integer.`);
|
|
187
|
+
}
|
|
188
|
+
return value;
|
|
189
|
+
};
|
|
190
|
+
return {
|
|
191
|
+
$prefix_hash: text("prefix_hash"),
|
|
192
|
+
$loose_prefix_hash: text("loose_prefix_hash"),
|
|
193
|
+
$router_id: write.router_id,
|
|
194
|
+
$organization_id: write.organization_id,
|
|
195
|
+
$conversation_id: text("conversation_id", CONVERSATION_ID_MAX_CHARS),
|
|
196
|
+
$model: text("model", MODEL_NAME_MAX_CHARS),
|
|
197
|
+
$provider: text("provider", MODEL_NAME_MAX_CHARS),
|
|
198
|
+
$next_model: text("next_model", MODEL_NAME_MAX_CHARS),
|
|
199
|
+
$next_reasoning_effort: text("next_reasoning_effort"),
|
|
200
|
+
$message_depth: int("message_depth"),
|
|
201
|
+
$provider_block_depth: int("provider_block_depth"),
|
|
202
|
+
$prompt_anchor_depth: int("prompt_anchor_depth"),
|
|
203
|
+
$input_tokens: int("input_tokens"),
|
|
204
|
+
$cache_read_tokens: int("cache_read_tokens"),
|
|
205
|
+
$cache_write_tokens: int("cache_write_tokens"),
|
|
206
|
+
$prompt_tokens: int("prompt_tokens"),
|
|
207
|
+
$benchmark_replay_depth: int("benchmark_replay_depth"),
|
|
208
|
+
$tool_choice_fp: text("tool_choice_fp"),
|
|
209
|
+
$response_format_fp: text("response_format_fp"),
|
|
210
|
+
$reasoning_bucket: text("reasoning_bucket", REASONING_BUCKET_MAX_CHARS),
|
|
211
|
+
$lease_turns_remaining: int("lease_turns_remaining"),
|
|
212
|
+
$reason: write.zdr ? null : text("reason", REASON_MAX_CHARS),
|
|
213
|
+
$updated_at: cacheClock,
|
|
214
|
+
$pending_lease_model: text("pending_lease_model"),
|
|
215
|
+
$pending_lease_reasoning_effort: text("pending_lease_reasoning_effort"),
|
|
216
|
+
$pending_lease_turns: int("pending_lease_turns"),
|
|
217
|
+
$pending_lease_reason: write.zdr ? null : text("pending_lease_reason"),
|
|
218
|
+
$pending_lease_output_text: write.zdr ? null : text("pending_lease_output_text", PENDING_LEASE_OUTPUT_TEXT_MAX_CHARS),
|
|
219
|
+
// Every row carries a fence at its own conversation depth so a slow
|
|
220
|
+
// prefetch decided earlier cannot stamp rows appended after a newer write.
|
|
221
|
+
$pending_lease_decided_at_depth: int("lease_depth"),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
// Stripped, truncated, and null when blank.
|
|
225
|
+
function clip(value, maxChars) {
|
|
226
|
+
const normalized = value?.trim();
|
|
227
|
+
return normalized ? normalized.slice(0, maxChars) : null;
|
|
228
|
+
}
|
|
229
|
+
function isConstraintError(error) {
|
|
230
|
+
return error instanceof Error && "code" in error && String(error.code).startsWith("SQLITE_CONSTRAINT");
|
|
231
|
+
}
|
|
232
|
+
function toHit(row) {
|
|
233
|
+
return {
|
|
234
|
+
hash: row.prefix_hash,
|
|
235
|
+
loose_hash: row.loose_prefix_hash,
|
|
236
|
+
conversation_id: row.conversation_id,
|
|
237
|
+
model: row.model,
|
|
238
|
+
provider: row.provider,
|
|
239
|
+
next_model: row.next_model,
|
|
240
|
+
next_reasoning_effort: row.next_reasoning_effort,
|
|
241
|
+
message_depth: row.message_depth,
|
|
242
|
+
provider_block_depth: row.provider_block_depth,
|
|
243
|
+
prompt_anchor_depth: row.prompt_anchor_depth,
|
|
244
|
+
prompt_tokens: row.prompt_tokens,
|
|
245
|
+
benchmark_replay_depth: row.benchmark_replay_depth,
|
|
246
|
+
input_tokens: row.input_tokens,
|
|
247
|
+
cache_read_tokens: row.cache_read_tokens,
|
|
248
|
+
cache_write_tokens: row.cache_write_tokens,
|
|
249
|
+
tool_choice_fp: row.tool_choice_fp,
|
|
250
|
+
response_format_fp: row.response_format_fp,
|
|
251
|
+
reasoning_bucket: row.reasoning_bucket,
|
|
252
|
+
lease_turns_remaining: row.lease_turns_remaining,
|
|
253
|
+
pending_lease_model: row.pending_lease_model,
|
|
254
|
+
pending_lease_reasoning_effort: row.pending_lease_reasoning_effort,
|
|
255
|
+
pending_lease_turns: row.pending_lease_turns,
|
|
256
|
+
pending_lease_reason: row.pending_lease_reason,
|
|
257
|
+
pending_lease_output_text: row.pending_lease_output_text,
|
|
258
|
+
reason: row.reason,
|
|
259
|
+
updated_at: new Date(row.updated_at).toISOString(),
|
|
260
|
+
};
|
|
261
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type RouterPrefixHit } from "@mupt-ai/dari-router/policy-engine";
|
|
2
|
+
import type { ResolveRouterRequest } from "../platform/platform.js";
|
|
3
|
+
import type { ResolvedRouter } from "../types.js";
|
|
4
|
+
import type { GatewayConfig } from "./config.js";
|
|
5
|
+
import { type OAuthFetch, type SubscriptionStore } from "./subscriptions/index.js";
|
|
6
|
+
export declare const SELF_HOSTED_ORGANIZATION_ID = "self-hosted";
|
|
7
|
+
export declare const SELF_HOSTED_USER_ID = "self-hosted";
|
|
8
|
+
export type SelfHostedResolverDeps = {
|
|
9
|
+
config: GatewayConfig;
|
|
10
|
+
gatewayKeys: readonly string[];
|
|
11
|
+
subscriptions: SubscriptionStore;
|
|
12
|
+
prefixHits: (args: {
|
|
13
|
+
routerId: string;
|
|
14
|
+
prefixHashes: readonly string[];
|
|
15
|
+
conversationId?: string;
|
|
16
|
+
}) => RouterPrefixHit[];
|
|
17
|
+
providerApiKey: (provider: string) => Promise<string | undefined>;
|
|
18
|
+
env: Record<string, string | undefined>;
|
|
19
|
+
fetch: OAuthFetch;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Resolve a configured router for one request. Each provider is served by a
|
|
23
|
+
* connected personal subscription when the router allows it and one is
|
|
24
|
+
* available, otherwise by the provider's API key; models whose provider has
|
|
25
|
+
* neither are left out.
|
|
26
|
+
*/
|
|
27
|
+
export declare function resolveSelfHostedRouter(deps: SelfHostedResolverDeps, request: ResolveRouterRequest): Promise<ResolvedRouter>;
|
|
28
|
+
export declare function gatewayKeyIndex(keys: readonly string[], presented: string): number | null;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { nativeModelId, providerForModel, } from "@mupt-ai/dari-router/policy-engine";
|
|
3
|
+
import { HttpError } from "../http/errors.js";
|
|
4
|
+
import { codexModelConfig } from "../models/subscriptions.js";
|
|
5
|
+
import { registeredModelPrice } from "../providers/pi.js";
|
|
6
|
+
import { freshAccessToken, } from "./subscriptions/index.js";
|
|
7
|
+
// A self-hosted gateway is one tenant; these stand in for the hosted
|
|
8
|
+
// organization and the user a personal subscription belongs to.
|
|
9
|
+
export const SELF_HOSTED_ORGANIZATION_ID = "self-hosted";
|
|
10
|
+
export const SELF_HOSTED_USER_ID = "self-hosted";
|
|
11
|
+
/**
|
|
12
|
+
* Resolve a configured router for one request. Each provider is served by a
|
|
13
|
+
* connected personal subscription when the router allows it and one is
|
|
14
|
+
* available, otherwise by the provider's API key; models whose provider has
|
|
15
|
+
* neither are left out.
|
|
16
|
+
*/
|
|
17
|
+
export async function resolveSelfHostedRouter(deps, request) {
|
|
18
|
+
const keyIndex = gatewayKeyIndex(deps.gatewayKeys, request.apiKey);
|
|
19
|
+
if (keyIndex === null) {
|
|
20
|
+
throw new HttpError(401, "Missing or invalid router/API key.", "routing_key_resolution_failed");
|
|
21
|
+
}
|
|
22
|
+
const router = deps.config.routers[request.routerId];
|
|
23
|
+
if (router === undefined) {
|
|
24
|
+
throw new HttpError(404, `Router ${request.routerId} is not configured on this gateway.`, "router_not_found");
|
|
25
|
+
}
|
|
26
|
+
const now = new Date();
|
|
27
|
+
const credentials = new Map();
|
|
28
|
+
const exhausted = new Set();
|
|
29
|
+
for (const provider of new Set(router.models.map((model) => providerForModel(model.id)))) {
|
|
30
|
+
const subscription = router.subscriptions
|
|
31
|
+
? subscriptionProviderFor(provider, request.clientHarness)
|
|
32
|
+
: null;
|
|
33
|
+
const active = subscription === null
|
|
34
|
+
? null
|
|
35
|
+
: deps.subscriptions.activeCredential(subscription, now);
|
|
36
|
+
if (active !== null) {
|
|
37
|
+
credentials.set(provider, {
|
|
38
|
+
key: await freshAccessToken(deps.subscriptions, active, { fetch: deps.fetch }),
|
|
39
|
+
source: "subscription",
|
|
40
|
+
subscription: active,
|
|
41
|
+
});
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (subscription !== null && deps.subscriptions.list(subscription).length > 0) {
|
|
45
|
+
exhausted.add(subscription);
|
|
46
|
+
}
|
|
47
|
+
const apiKey = await providerApiKey(deps, provider);
|
|
48
|
+
credentials.set(provider, apiKey ? { key: apiKey, source: "user" } : null);
|
|
49
|
+
}
|
|
50
|
+
const models = router.models.filter((model) => credentials.get(providerForModel(model.id)) != null);
|
|
51
|
+
if (models.length === 0)
|
|
52
|
+
throw unavailableRouterError(request.routerId, exhausted);
|
|
53
|
+
const resolved = {
|
|
54
|
+
router_id: request.routerId,
|
|
55
|
+
organization_id: SELF_HOSTED_ORGANIZATION_ID,
|
|
56
|
+
api_key_id: `gateway_key_${keyIndex + 1}`,
|
|
57
|
+
attributed_user_id: SELF_HOSTED_USER_ID,
|
|
58
|
+
enabled_models: models.map((model) => model.id),
|
|
59
|
+
provider_keys: {},
|
|
60
|
+
provider_key_sources: {},
|
|
61
|
+
subscription_credential_ids: {},
|
|
62
|
+
zdr: router.zdr,
|
|
63
|
+
speculative_routing: false,
|
|
64
|
+
primary_retries: router.retries,
|
|
65
|
+
model_fallback_enabled: router.fallback,
|
|
66
|
+
fallback_requires_different_provider: router.fallbackRequiresDifferentProvider,
|
|
67
|
+
lease_context_enabled: false,
|
|
68
|
+
routing_strategy: "dari",
|
|
69
|
+
custom_config: null,
|
|
70
|
+
evals: [],
|
|
71
|
+
model_thinking_levels: thinkingLevels(router),
|
|
72
|
+
model_prices: {},
|
|
73
|
+
model_configs: {},
|
|
74
|
+
prefix_hits: deps.prefixHits({
|
|
75
|
+
routerId: request.routerId,
|
|
76
|
+
prefixHashes: request.prefixHashes ?? [],
|
|
77
|
+
...(request.conversationId ? { conversationId: request.conversationId } : {}),
|
|
78
|
+
}),
|
|
79
|
+
};
|
|
80
|
+
for (const [provider, credential] of credentials) {
|
|
81
|
+
if (credential == null)
|
|
82
|
+
continue;
|
|
83
|
+
for (const servingProvider of servingProviders(provider, credential)) {
|
|
84
|
+
resolved.provider_keys[servingProvider] = credential.key;
|
|
85
|
+
resolved.provider_key_sources[servingProvider] = credential.source;
|
|
86
|
+
if (credential.subscription) {
|
|
87
|
+
resolved.subscription_credential_ids[servingProvider] = credential.subscription.id;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
for (const model of models) {
|
|
92
|
+
const price = registeredModelPrice(model.id);
|
|
93
|
+
if (price !== null)
|
|
94
|
+
resolved.model_prices[model.id] = price;
|
|
95
|
+
const provider = providerForModel(model.id);
|
|
96
|
+
const endpoint = deps.config.providers[provider];
|
|
97
|
+
if (credentials.get(provider)?.subscription?.provider === "openai_codex") {
|
|
98
|
+
resolved.model_configs[model.id] = codexModelConfig(model.id, price);
|
|
99
|
+
}
|
|
100
|
+
else if (endpoint !== undefined) {
|
|
101
|
+
resolved.model_configs[model.id] = {
|
|
102
|
+
provider,
|
|
103
|
+
native_model_id: nativeModelId(model.id),
|
|
104
|
+
api: endpoint.api,
|
|
105
|
+
base_url: endpoint.baseUrl,
|
|
106
|
+
cost: price ?? { input: 0, output: 0 },
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return resolved;
|
|
111
|
+
}
|
|
112
|
+
// Configured endpoints read their own key variable, or send a placeholder
|
|
113
|
+
// when they take none; known providers use their standard variable.
|
|
114
|
+
async function providerApiKey(deps, provider) {
|
|
115
|
+
const endpoint = deps.config.providers[provider];
|
|
116
|
+
if (endpoint === undefined)
|
|
117
|
+
return (await deps.providerApiKey(provider))?.trim() || undefined;
|
|
118
|
+
if (endpoint.apiKeyEnv === undefined)
|
|
119
|
+
return "no-key";
|
|
120
|
+
return deps.env[endpoint.apiKeyEnv]?.trim() || undefined;
|
|
121
|
+
}
|
|
122
|
+
export function gatewayKeyIndex(keys, presented) {
|
|
123
|
+
const candidate = Buffer.from(presented);
|
|
124
|
+
for (const [index, key] of keys.entries()) {
|
|
125
|
+
const expected = Buffer.from(key);
|
|
126
|
+
if (expected.length === candidate.length && timingSafeEqual(expected, candidate)) {
|
|
127
|
+
return index;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
// Claude Code plans serve only Claude Code itself and Muse Code plans only
|
|
133
|
+
// Muse Code; ChatGPT plans serve any client through the Codex backend.
|
|
134
|
+
function subscriptionProviderFor(provider, clientHarness) {
|
|
135
|
+
if (provider === "anthropic") {
|
|
136
|
+
return clientHarness === "claude_code" ? "anthropic_claude_code" : null;
|
|
137
|
+
}
|
|
138
|
+
if (provider === "meta")
|
|
139
|
+
return clientHarness === "muse_code" ? "meta_muse" : null;
|
|
140
|
+
if (provider === "openai")
|
|
141
|
+
return "openai_codex";
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
// A ChatGPT plan serves OpenAI models as the `openai-codex` provider, and the
|
|
145
|
+
// runtime looks keys up by serving provider.
|
|
146
|
+
function servingProviders(provider, credential) {
|
|
147
|
+
return credential.subscription?.provider === "openai_codex"
|
|
148
|
+
? [provider, "openai-codex"]
|
|
149
|
+
: [provider];
|
|
150
|
+
}
|
|
151
|
+
function thinkingLevels(router) {
|
|
152
|
+
return Object.fromEntries(router.models.flatMap((model) => model.thinking ? [[model.id, model.thinking]] : []));
|
|
153
|
+
}
|
|
154
|
+
const SUBSCRIPTION_PLAN_NAMES = {
|
|
155
|
+
anthropic_claude_code: "Claude Code",
|
|
156
|
+
openai_codex: "ChatGPT",
|
|
157
|
+
meta_muse: "Muse Code",
|
|
158
|
+
};
|
|
159
|
+
function unavailableRouterError(routerId, exhausted) {
|
|
160
|
+
if (exhausted.size > 0) {
|
|
161
|
+
const plans = [...exhausted]
|
|
162
|
+
.map((provider) => SUBSCRIPTION_PLAN_NAMES[provider])
|
|
163
|
+
.join(" and ");
|
|
164
|
+
return new HttpError(424, `Every connected ${plans} subscription is at its usage limit, and no API key can serve router ${routerId} instead.`, "personal_subscription_unavailable");
|
|
165
|
+
}
|
|
166
|
+
return new HttpError(424, `No credentials can serve router ${routerId}: set a provider API key or connect a subscription.`, "provider_credentials_missing");
|
|
167
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type OAuthFetch } from "./oauth.js";
|
|
2
|
+
import type { SubscriptionCredential, SubscriptionStore } from "./store.js";
|
|
3
|
+
/**
|
|
4
|
+
* A usable access token for a subscription, refreshed and persisted first
|
|
5
|
+
* when it expires within five minutes. Records the credential as used.
|
|
6
|
+
*/
|
|
7
|
+
export declare function freshAccessToken(store: SubscriptionStore, credential: SubscriptionCredential, options: {
|
|
8
|
+
fetch: OAuthFetch;
|
|
9
|
+
now?: Date;
|
|
10
|
+
}): Promise<string>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { refreshOAuth } from "./oauth.js";
|
|
2
|
+
const REFRESH_MARGIN_MS = 5 * 60_000;
|
|
3
|
+
// One refresh per credential at a time in this process. Providers rotate
|
|
4
|
+
// refresh tokens, so a second concurrent refresh with the old token would fail.
|
|
5
|
+
const inFlight = new Map();
|
|
6
|
+
/**
|
|
7
|
+
* A usable access token for a subscription, refreshed and persisted first
|
|
8
|
+
* when it expires within five minutes. Records the credential as used.
|
|
9
|
+
*/
|
|
10
|
+
export async function freshAccessToken(store, credential, options) {
|
|
11
|
+
const now = options.now ?? new Date();
|
|
12
|
+
store.touch(credential.id, now);
|
|
13
|
+
if (!expiresSoon(credential, now))
|
|
14
|
+
return credential.accessToken;
|
|
15
|
+
const pending = inFlight.get(credential.id);
|
|
16
|
+
if (pending !== undefined)
|
|
17
|
+
return pending;
|
|
18
|
+
const refresh = (async () => {
|
|
19
|
+
// Another caller may have refreshed since `credential` was read.
|
|
20
|
+
const current = store.get(credential.id);
|
|
21
|
+
if (current === null)
|
|
22
|
+
throw new Error("Connected subscription credential is no longer available.");
|
|
23
|
+
if (!expiresSoon(current, now))
|
|
24
|
+
return current.accessToken;
|
|
25
|
+
const tokens = await refreshOAuth(current, { fetch: options.fetch, now });
|
|
26
|
+
if (tokens.provider !== current.provider) {
|
|
27
|
+
throw new Error("Subscription OAuth provider returned a mismatched credential.");
|
|
28
|
+
}
|
|
29
|
+
store.updateTokens(current.id, tokens, now);
|
|
30
|
+
return tokens.accessToken;
|
|
31
|
+
})().finally(() => inFlight.delete(credential.id));
|
|
32
|
+
inFlight.set(credential.id, refresh);
|
|
33
|
+
return refresh;
|
|
34
|
+
}
|
|
35
|
+
function expiresSoon(credential, now) {
|
|
36
|
+
return credential.expiresAt.getTime() <= now.getTime() + REFRESH_MARGIN_MS;
|
|
37
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The subscription endpoints the Dari CLI calls, with the same paths and JSON
|
|
3
|
+
* shapes as the hosted API. The gateway is single-tenant: `current` is the
|
|
4
|
+
* only organization, and every caller that passes the gateway's own auth owns
|
|
5
|
+
* every subscription. Errors are `{"detail": "..."}`, which the CLI prints.
|
|
6
|
+
*/
|
|
7
|
+
import { type OAuthFetch } from "./oauth.js";
|
|
8
|
+
import { type SubscriptionStore } from "./store.js";
|
|
9
|
+
export type SubscriptionHttpDeps = {
|
|
10
|
+
store: SubscriptionStore;
|
|
11
|
+
fetch: OAuthFetch;
|
|
12
|
+
now?: () => Date;
|
|
13
|
+
};
|
|
14
|
+
/** Handle a subscription route, or return null when the request is not one. */
|
|
15
|
+
export declare function handleSubscriptionRequest(request: Request, deps: SubscriptionHttpDeps): Promise<Response | null>;
|