@mupt-ai/dari-gateway 0.2.0 → 0.2.1
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/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 +23 -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 +81 -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 +6 -0
- package/dist/index.js +8 -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 +37 -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 +65 -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 +53 -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 +203 -0
- package/dist/interaction/types.js +5 -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 +8 -0
- package/dist/models/execution_provider.js +13 -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 +41 -0
- package/dist/models/runtime_overrides.d.ts +4 -0
- package/dist/models/runtime_overrides.js +209 -0
- package/dist/observability/errors.d.ts +8 -0
- package/dist/observability/errors.js +11 -0
- package/dist/platform/platform.d.ts +152 -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 +212 -0
- package/dist/protocol/input/chat_completions_validation.d.ts +2 -0
- package/dist/protocol/input/chat_completions_validation.js +257 -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 +319 -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 +398 -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 +292 -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 +31 -0
- package/dist/providers/subscription_limits.js +163 -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 +4 -0
- package/dist/runtime/explicit_model.js +53 -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 +167 -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/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 +171 -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 +55 -0
- package/dist/self_host/subscriptions/oauth.js +281 -0
- package/dist/self_host/subscriptions/store.d.ts +78 -0
- package/dist/self_host/subscriptions/store.js +235 -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 +94 -0
- package/dist/types.js +1 -0
- package/package.json +21 -6
- 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 +1 -1
- 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 +8 -8
- package/src/http/serve.ts +3 -3
- package/src/index.ts +6 -6
- package/src/ingress/anthropic_messages.ts +12 -12
- package/src/ingress/anthropic_models.ts +1 -1
- package/src/ingress/chat_completions.ts +9 -9
- package/src/ingress/claude_code/request_context.ts +2 -2
- package/src/ingress/codex.ts +10 -10
- package/src/ingress/codex_models.ts +4 -4
- package/src/ingress/json.ts +1 -1
- package/src/ingress/models.ts +9 -9
- 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/models/context_limits.ts +1 -1
- package/src/models/execution_provider.ts +1 -1
- package/src/models/ids.ts +1 -1
- package/src/models/provider_defaults.ts +1 -1
- package/src/models/runtime_overrides.ts +1 -1
- package/src/observability/errors.ts +2 -2
- package/src/platform/platform.ts +4 -4
- package/src/protocol/input/anthropic_messages.ts +7 -7
- package/src/protocol/input/chat_completions.ts +5 -5
- package/src/protocol/input/chat_completions_validation.ts +4 -4
- 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 +14 -14
- package/src/providers/pi_context.ts +6 -6
- package/src/providers/pi_options.ts +10 -10
- package/src/providers/pi_output.ts +5 -5
- package/src/providers/provider_attempt_runner.ts +1 -1
- package/src/providers/provider_call.ts +14 -14
- 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 +3 -3
- package/src/runtime/claude_code/auxiliary_routing.ts +5 -5
- package/src/runtime/customer_disconnect.ts +1 -1
- package/src/runtime/explicit_model.ts +4 -4
- 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 +13 -13
- 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/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 +6 -6
- 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/store.ts +1 -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
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { captureOperationalError, } from "../observability/errors.js";
|
|
2
|
+
import { reportedExecution, } from "../runtime/runtime_reporter.js";
|
|
3
|
+
import { reportRoutingUsage, } from "../telemetry/usage.js";
|
|
4
|
+
import { customerCanceledProviderMessage } from "../runtime/customer_disconnect.js";
|
|
5
|
+
import { estimatedCancelUsage, partialInteractionOutput, } from "./cancel_estimate.js";
|
|
6
|
+
/**
|
|
7
|
+
* A client cancel runs its finalization in the background, where a slow
|
|
8
|
+
* selector would only delay the record and, at shutdown, eat the drain budget.
|
|
9
|
+
* The record still gets the selector's run when it arrives within the cap;
|
|
10
|
+
* otherwise it is written without it. The response-owned path is not capped.
|
|
11
|
+
*/
|
|
12
|
+
export const CANCEL_REPORT_BARRIER_CAP_MS = 2_000;
|
|
13
|
+
export function createStreamFinalizer(args) {
|
|
14
|
+
let frozen;
|
|
15
|
+
let responseOwnedCompleted = false;
|
|
16
|
+
async function finalize(outcome, path) {
|
|
17
|
+
frozen ??= freezeStreamTerminal(args, outcome, path);
|
|
18
|
+
// Normal close calls onFinal after the response-owned barrier has already
|
|
19
|
+
// succeeded. A cancellation racing that barrier reaches this branch first
|
|
20
|
+
// and settles the same immutable payload.
|
|
21
|
+
if (path === "detached" && responseOwnedCompleted)
|
|
22
|
+
return;
|
|
23
|
+
await frozen.reporting;
|
|
24
|
+
// The response terminator waits for the fast prefix-state write. A client
|
|
25
|
+
// cancellation has nothing left to hold, so its onFinal path skips this.
|
|
26
|
+
if (path === "response_owned") {
|
|
27
|
+
await args.reporter.waitForPrefixEntryWrite();
|
|
28
|
+
responseOwnedCompleted = true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
beforeResponseClose: (outcome) => finalize(outcome, "response_owned"),
|
|
33
|
+
afterResponseDetached: (outcome) => finalize(outcome, "detached"),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function freezeStreamTerminal(args, outcome, path) {
|
|
37
|
+
const report = buildStreamTerminalReport(args, outcome);
|
|
38
|
+
args.reporter.recordProviderAttempt(report.attempt);
|
|
39
|
+
const terminal = args.reporter.freezeProviderTerminal(report.providerOutcome);
|
|
40
|
+
return {
|
|
41
|
+
terminal,
|
|
42
|
+
reporting: reportTerminalOnce(args, report, terminal, path),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
async function reportTerminalOnce(args, report, terminal, path) {
|
|
46
|
+
const capture = (error, code) => captureOperationalError(error, {
|
|
47
|
+
code,
|
|
48
|
+
requestId: args.requestId,
|
|
49
|
+
routerId: args.execution.usageContext?.routerId,
|
|
50
|
+
});
|
|
51
|
+
try {
|
|
52
|
+
await (path === "response_owned"
|
|
53
|
+
? args.reportBarrier
|
|
54
|
+
: Promise.race([
|
|
55
|
+
args.reportBarrier,
|
|
56
|
+
new Promise((resolve) => setTimeout(resolve, args.cancelBarrierCapMs ?? CANCEL_REPORT_BARRIER_CAP_MS)),
|
|
57
|
+
]));
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
capture(error, "routing_report_barrier_failed");
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
terminal.report();
|
|
64
|
+
if (report.reportManagedUsage && args.execution.usageContext) {
|
|
65
|
+
await reportRoutingUsage(args.execution.usageContext, report.providerMessage);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
capture(error, "stream_report_finalization_failed");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function buildStreamTerminalReport(args, outcome) {
|
|
73
|
+
const execution = reportedExecution(args.execution);
|
|
74
|
+
const attemptBase = {
|
|
75
|
+
model: args.decision.selectedModel,
|
|
76
|
+
reasoning_effort: args.decision.reasoningEffort,
|
|
77
|
+
role: args.role,
|
|
78
|
+
attempt: args.attempt,
|
|
79
|
+
execution,
|
|
80
|
+
};
|
|
81
|
+
if (outcome.status === "completed") {
|
|
82
|
+
const responseBody = args.protocol.responseBody({
|
|
83
|
+
id: args.requestId,
|
|
84
|
+
model: args.execution.model,
|
|
85
|
+
output: outcome.output,
|
|
86
|
+
modelCost: args.reporter.modelCost({
|
|
87
|
+
...attemptBase,
|
|
88
|
+
outcome: "completed",
|
|
89
|
+
message: outcome.message,
|
|
90
|
+
billingOutcome: "completed",
|
|
91
|
+
}),
|
|
92
|
+
request: args.interaction,
|
|
93
|
+
routing: args.decision,
|
|
94
|
+
conversationId: args.execution.conversationId,
|
|
95
|
+
});
|
|
96
|
+
return {
|
|
97
|
+
attempt: {
|
|
98
|
+
...attemptBase,
|
|
99
|
+
outcome: "completed",
|
|
100
|
+
message: outcome.message,
|
|
101
|
+
billingOutcome: "completed",
|
|
102
|
+
},
|
|
103
|
+
providerOutcome: {
|
|
104
|
+
status: "completed",
|
|
105
|
+
output: outcome.output,
|
|
106
|
+
message: outcome.message,
|
|
107
|
+
responseBody,
|
|
108
|
+
providerLatencyMs: args.providerLatencyMs(),
|
|
109
|
+
execution,
|
|
110
|
+
},
|
|
111
|
+
providerMessage: outcome.message,
|
|
112
|
+
reportManagedUsage: true,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
if (outcome.status === "provider_error") {
|
|
116
|
+
return {
|
|
117
|
+
attempt: {
|
|
118
|
+
...attemptBase,
|
|
119
|
+
outcome: "terminal_error",
|
|
120
|
+
message: outcome.message,
|
|
121
|
+
billingOutcome: "provider_failed",
|
|
122
|
+
},
|
|
123
|
+
providerOutcome: {
|
|
124
|
+
status: "provider_error",
|
|
125
|
+
message: outcome.message,
|
|
126
|
+
providerLatencyMs: args.providerLatencyMs(),
|
|
127
|
+
execution,
|
|
128
|
+
},
|
|
129
|
+
providerMessage: outcome.message,
|
|
130
|
+
reportManagedUsage: false,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
// The client left before the provider finished. The provider call was
|
|
134
|
+
// aborted at that moment, so bill the prompt plus what was delivered.
|
|
135
|
+
const delivered = args.delivered();
|
|
136
|
+
const usage = estimatedCancelUsage({
|
|
137
|
+
pricing: args.execution.pricing,
|
|
138
|
+
promptEstimate: args.execution.promptEstimate,
|
|
139
|
+
delivered,
|
|
140
|
+
});
|
|
141
|
+
const providerMessage = customerCanceledProviderMessage(usage);
|
|
142
|
+
return {
|
|
143
|
+
attempt: {
|
|
144
|
+
...attemptBase,
|
|
145
|
+
outcome: "canceled",
|
|
146
|
+
message: providerMessage,
|
|
147
|
+
billingOutcome: "customer_canceled",
|
|
148
|
+
},
|
|
149
|
+
providerOutcome: {
|
|
150
|
+
status: "customer_canceled",
|
|
151
|
+
message: providerMessage,
|
|
152
|
+
responseBody: outcome.responseBody,
|
|
153
|
+
providerLatencyMs: args.providerLatencyMs(),
|
|
154
|
+
execution,
|
|
155
|
+
partialOutput: partialInteractionOutput({
|
|
156
|
+
delivered,
|
|
157
|
+
usage,
|
|
158
|
+
source: args.source,
|
|
159
|
+
createdAtMs: args.startedAtMs,
|
|
160
|
+
}),
|
|
161
|
+
},
|
|
162
|
+
providerMessage,
|
|
163
|
+
reportManagedUsage: true,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ProviderKeySource } from "../types.js";
|
|
2
|
+
import type { CapturedProviderResponse } from "./response_capture.js";
|
|
3
|
+
export type SubscriptionOAuthProvider = "anthropic_claude_code" | "openai_codex";
|
|
4
|
+
export type SubscriptionUsageLimit = {
|
|
5
|
+
oauthProvider: SubscriptionOAuthProvider;
|
|
6
|
+
resetsAt: Date;
|
|
7
|
+
reason: string;
|
|
8
|
+
switched?: boolean;
|
|
9
|
+
};
|
|
10
|
+
export declare const DEFAULT_USAGE_LIMIT_WINDOW_MS: number;
|
|
11
|
+
export declare function subscriptionOAuthProviderFor(provider: string, keySource: ProviderKeySource): SubscriptionOAuthProvider | null;
|
|
12
|
+
/**
|
|
13
|
+
* Recognize a personal subscription that has run out of quota. Claude Code
|
|
14
|
+
* answers 429 with the unified rate-limit headers; the ChatGPT backend answers
|
|
15
|
+
* 429 with a structured `error.code` and `resets_at`. The Codex WebSocket
|
|
16
|
+
* transport bypasses fetch, so its usage-limit error only survives as message
|
|
17
|
+
* text and is matched there without a reset time.
|
|
18
|
+
*/
|
|
19
|
+
export declare function detectSubscriptionUsageLimit(args: {
|
|
20
|
+
provider: string;
|
|
21
|
+
keySource: ProviderKeySource;
|
|
22
|
+
captured: CapturedProviderResponse | undefined;
|
|
23
|
+
failure: Record<string, unknown>;
|
|
24
|
+
now?: Date;
|
|
25
|
+
}): SubscriptionUsageLimit | null;
|
|
26
|
+
/** Tag a provider failure so telemetry and the terminal error know why. */
|
|
27
|
+
export declare function failureWithSubscriptionUsageLimit(failure: Record<string, unknown>, limit: SubscriptionUsageLimit): Record<string, unknown>;
|
|
28
|
+
export declare function subscriptionUsageLimitFromFailure(failure: Record<string, unknown>): SubscriptionUsageLimit | null;
|
|
29
|
+
export declare function subscriptionUsageLimitMessage(limit: SubscriptionUsageLimit): string;
|
|
30
|
+
export declare function subscriptionSwitchedMessage(limit: SubscriptionUsageLimit): string;
|
|
31
|
+
export declare function retryAfterSeconds(limit: SubscriptionUsageLimit, now?: Date): number;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { isRecord } from "@mupt-ai/dari-router/policy-engine";
|
|
2
|
+
import { providerFailureMessage } from "../runtime/provider_stream_outcome.js";
|
|
3
|
+
// Neither provider guarantees a reset time on every usage-limit response.
|
|
4
|
+
// Without one, hold the subscription out for an hour rather than hammering an
|
|
5
|
+
// exhausted plan on every request.
|
|
6
|
+
export const DEFAULT_USAGE_LIMIT_WINDOW_MS = 60 * 60 * 1000;
|
|
7
|
+
const FAILURE_MARKER = "subscription_usage_limit";
|
|
8
|
+
const SUBSCRIPTION_LABEL = {
|
|
9
|
+
anthropic_claude_code: "Claude Code",
|
|
10
|
+
openai_codex: "ChatGPT",
|
|
11
|
+
};
|
|
12
|
+
export function subscriptionOAuthProviderFor(provider, keySource) {
|
|
13
|
+
if (keySource !== "subscription")
|
|
14
|
+
return null;
|
|
15
|
+
if (provider === "anthropic")
|
|
16
|
+
return "anthropic_claude_code";
|
|
17
|
+
if (provider === "openai" || provider === "openai-codex")
|
|
18
|
+
return "openai_codex";
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Recognize a personal subscription that has run out of quota. Claude Code
|
|
23
|
+
* answers 429 with the unified rate-limit headers; the ChatGPT backend answers
|
|
24
|
+
* 429 with a structured `error.code` and `resets_at`. The Codex WebSocket
|
|
25
|
+
* transport bypasses fetch, so its usage-limit error only survives as message
|
|
26
|
+
* text and is matched there without a reset time.
|
|
27
|
+
*/
|
|
28
|
+
export function detectSubscriptionUsageLimit(args) {
|
|
29
|
+
const oauthProvider = subscriptionOAuthProviderFor(args.provider, args.keySource);
|
|
30
|
+
if (oauthProvider === null)
|
|
31
|
+
return null;
|
|
32
|
+
const now = args.now ?? new Date();
|
|
33
|
+
if (oauthProvider === "anthropic_claude_code") {
|
|
34
|
+
return anthropicUsageLimit(args.captured, now);
|
|
35
|
+
}
|
|
36
|
+
return codexUsageLimit(args.captured, providerFailureMessage(args.failure), now);
|
|
37
|
+
}
|
|
38
|
+
function anthropicUsageLimit(captured, now) {
|
|
39
|
+
if (captured === undefined || captured.status !== 429)
|
|
40
|
+
return null;
|
|
41
|
+
const status = captured.headers.get("anthropic-ratelimit-unified-status");
|
|
42
|
+
const overageStatus = captured.headers.get("anthropic-ratelimit-unified-overage-status");
|
|
43
|
+
// A plain per-minute rate limit is transient; only a rejected unified
|
|
44
|
+
// window means the plan itself is exhausted.
|
|
45
|
+
if (status !== "rejected" || (overageStatus !== null && overageStatus !== "rejected")) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const claim = captured.headers.get("anthropic-ratelimit-unified-representative-claim");
|
|
49
|
+
return {
|
|
50
|
+
oauthProvider: "anthropic_claude_code",
|
|
51
|
+
resetsAt: resetFromUnixSeconds(captured.headers.get("anthropic-ratelimit-unified-reset"), now),
|
|
52
|
+
reason: claim ? `Claude Code ${claim.replaceAll("_", " ")} usage limit reached.` : "Claude Code usage limit reached.",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const CODEX_LIMIT_CODES = new Set(["usage_limit_reached", "usage_not_included"]);
|
|
56
|
+
const CODEX_LIMIT_MESSAGE = /usage_limit_reached|usage_not_included|hit your ChatGPT usage limit|the usage limit has been reached/i;
|
|
57
|
+
function codexUsageLimit(captured, failureMessage, now) {
|
|
58
|
+
if (captured !== undefined && captured.status === 429) {
|
|
59
|
+
const error = codexErrorBody(captured.bodyText);
|
|
60
|
+
const code = typeof error?.code === "string" ? error.code : "";
|
|
61
|
+
if (error !== null && (CODEX_LIMIT_CODES.has(code) || failureMessage.match(CODEX_LIMIT_MESSAGE))) {
|
|
62
|
+
const plan = typeof error.plan_type === "string" ? ` (${error.plan_type.toLowerCase()} plan)` : "";
|
|
63
|
+
return {
|
|
64
|
+
oauthProvider: "openai_codex",
|
|
65
|
+
resetsAt: laterReset(error.resets_at, captured.headers, now),
|
|
66
|
+
reason: `ChatGPT usage limit reached${plan}.`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
if (!CODEX_LIMIT_MESSAGE.test(failureMessage))
|
|
72
|
+
return null;
|
|
73
|
+
return {
|
|
74
|
+
oauthProvider: "openai_codex",
|
|
75
|
+
resetsAt: new Date(now.getTime() + DEFAULT_USAGE_LIMIT_WINDOW_MS),
|
|
76
|
+
reason: "ChatGPT usage limit reached.",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function codexErrorBody(bodyText) {
|
|
80
|
+
try {
|
|
81
|
+
const parsed = JSON.parse(bodyText);
|
|
82
|
+
return isRecord(parsed) && isRecord(parsed.error) ? parsed.error : null;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Codex may send both an absolute `resets_at` and Retry-After; the plan is
|
|
89
|
+
// back only once both have passed.
|
|
90
|
+
function laterReset(resetsAt, headers, now) {
|
|
91
|
+
const deadlines = [unixSecondsDeadline(resetsAt, now), retryAfterDeadline(headers, now)]
|
|
92
|
+
.filter((deadline) => deadline !== null);
|
|
93
|
+
if (deadlines.length === 0)
|
|
94
|
+
return new Date(now.getTime() + DEFAULT_USAGE_LIMIT_WINDOW_MS);
|
|
95
|
+
return new Date(Math.max(...deadlines.map((deadline) => deadline.getTime())));
|
|
96
|
+
}
|
|
97
|
+
function retryAfterDeadline(headers, now) {
|
|
98
|
+
const milliseconds = Number(headers.get("retry-after-ms") ?? NaN);
|
|
99
|
+
if (Number.isFinite(milliseconds) && milliseconds > 0) {
|
|
100
|
+
return new Date(now.getTime() + milliseconds);
|
|
101
|
+
}
|
|
102
|
+
const value = headers.get("retry-after")?.trim();
|
|
103
|
+
if (!value)
|
|
104
|
+
return null;
|
|
105
|
+
const seconds = Number(value);
|
|
106
|
+
const deadline = Number.isFinite(seconds)
|
|
107
|
+
? new Date(now.getTime() + seconds * 1000)
|
|
108
|
+
: new Date(value);
|
|
109
|
+
return deadline.getTime() > now.getTime() ? deadline : null;
|
|
110
|
+
}
|
|
111
|
+
function unixSecondsDeadline(value, now) {
|
|
112
|
+
const seconds = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
|
|
113
|
+
return Number.isFinite(seconds) && seconds * 1000 > now.getTime() ? new Date(seconds * 1000) : null;
|
|
114
|
+
}
|
|
115
|
+
function resetFromUnixSeconds(value, now) {
|
|
116
|
+
const seconds = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN;
|
|
117
|
+
if (Number.isFinite(seconds) && seconds * 1000 > now.getTime()) {
|
|
118
|
+
return new Date(seconds * 1000);
|
|
119
|
+
}
|
|
120
|
+
return new Date(now.getTime() + DEFAULT_USAGE_LIMIT_WINDOW_MS);
|
|
121
|
+
}
|
|
122
|
+
/** Tag a provider failure so telemetry and the terminal error know why. */
|
|
123
|
+
export function failureWithSubscriptionUsageLimit(failure, limit) {
|
|
124
|
+
return {
|
|
125
|
+
...failure,
|
|
126
|
+
[FAILURE_MARKER]: {
|
|
127
|
+
oauth_provider: limit.oauthProvider,
|
|
128
|
+
resets_at: limit.resetsAt.toISOString(),
|
|
129
|
+
reason: limit.reason,
|
|
130
|
+
...(limit.switched === true ? { switched: true } : {}),
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
export function subscriptionUsageLimitFromFailure(failure) {
|
|
135
|
+
const marker = failure[FAILURE_MARKER];
|
|
136
|
+
if (!isRecord(marker))
|
|
137
|
+
return null;
|
|
138
|
+
const oauthProvider = marker.oauth_provider;
|
|
139
|
+
const resetsAt = typeof marker.resets_at === "string" ? new Date(marker.resets_at) : null;
|
|
140
|
+
if ((oauthProvider !== "anthropic_claude_code" && oauthProvider !== "openai_codex")
|
|
141
|
+
|| resetsAt === null || Number.isNaN(resetsAt.getTime())) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
oauthProvider,
|
|
146
|
+
resetsAt,
|
|
147
|
+
reason: typeof marker.reason === "string" ? marker.reason : "Subscription usage limit reached.",
|
|
148
|
+
...(marker.switched === true ? { switched: true } : {}),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
export function subscriptionUsageLimitMessage(limit) {
|
|
152
|
+
return `Your ${SUBSCRIPTION_LABEL[limit.oauthProvider]} subscription usage limit is reached. `
|
|
153
|
+
+ `It resets at ${limit.resetsAt.toISOString()}. `
|
|
154
|
+
+ "Mark it available early at https://app.dari.dev/credentials.";
|
|
155
|
+
}
|
|
156
|
+
export function subscriptionSwitchedMessage(limit) {
|
|
157
|
+
const label = SUBSCRIPTION_LABEL[limit.oauthProvider];
|
|
158
|
+
return `Your ${label} subscription usage limit is reached. `
|
|
159
|
+
+ `Dari switched to your next connected ${label} subscription; retry the request.`;
|
|
160
|
+
}
|
|
161
|
+
export function retryAfterSeconds(limit, now = new Date()) {
|
|
162
|
+
return Math.max(0, Math.ceil((limit.resetsAt.getTime() - now.getTime()) / 1000));
|
|
163
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type ModelPricing } from "@mupt-ai/dari-router/policy-engine";
|
|
2
|
+
export type SubscriptionUsageContext = {
|
|
3
|
+
api: string;
|
|
4
|
+
model: string;
|
|
5
|
+
pricing: ModelPricing | null;
|
|
6
|
+
};
|
|
7
|
+
export declare function messageWithSubscriptionCacheWrites(message: Record<string, unknown>, context: SubscriptionUsageContext): Record<string, unknown>;
|
|
8
|
+
export declare function eventsWithSubscriptionCacheWrites(events: AsyncIterable<Record<string, unknown>>, context: SubscriptionUsageContext): AsyncIterable<Record<string, unknown>>;
|
|
9
|
+
export declare function subscriptionCacheWriteUsage(usage: unknown, context: SubscriptionUsageContext): unknown;
|
|
10
|
+
export declare function modelPricingFromPiModel(model: Record<string, unknown>): ModelPricing | null;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { isRecord, pricingForPrompt, providerMinCacheTokens } from "@mupt-ai/dari-router/policy-engine";
|
|
2
|
+
// The ChatGPT subscription backend reports cached prompt tokens but never
|
|
3
|
+
// cache_write_tokens, so every fresh token lands in the input bucket and is
|
|
4
|
+
// priced at the input rate. The public API bills those tokens as cache writes
|
|
5
|
+
// (the GPT-5.6 family accepts only 24h extended caching, and its responses
|
|
6
|
+
// report a write within a few tokens of the fresh count), so subscription rows
|
|
7
|
+
// otherwise read as $0.00 cache write while the identical API request records
|
|
8
|
+
// the premium. Attribute them here, where the provider identity and the
|
|
9
|
+
// pricing pi-ai used are both known, so telemetry, client usage, and the
|
|
10
|
+
// estimator — which already prices fresh input at the cache-write rate — agree.
|
|
11
|
+
const SUBSCRIPTION_RESPONSES_API = "openai-codex-responses";
|
|
12
|
+
const MTOK = 1_000_000;
|
|
13
|
+
export function messageWithSubscriptionCacheWrites(message, context) {
|
|
14
|
+
const usage = subscriptionCacheWriteUsage(message.usage, context);
|
|
15
|
+
return usage === message.usage ? message : { ...message, usage };
|
|
16
|
+
}
|
|
17
|
+
// Every stream consumer reads final usage off the terminal `done` message.
|
|
18
|
+
export async function* eventsWithSubscriptionCacheWrites(events, context) {
|
|
19
|
+
for await (const event of events) {
|
|
20
|
+
if (event.type === "done" && isRecord(event.message)) {
|
|
21
|
+
yield { ...event, message: messageWithSubscriptionCacheWrites(event.message, context) };
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
yield event;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function subscriptionCacheWriteUsage(usage, context) {
|
|
28
|
+
if (context.api !== SUBSCRIPTION_RESPONSES_API || !isRecord(usage))
|
|
29
|
+
return usage;
|
|
30
|
+
const pricing = context.pricing === null ? null : pricingForPrompt(context.pricing, (finiteNumber(usage.input) ?? 0) + (finiteNumber(usage.cacheRead) ?? 0) + (finiteNumber(usage.cacheWrite) ?? 0));
|
|
31
|
+
// Without a write premium the buckets price identically, so moving tokens
|
|
32
|
+
// between them would change the report without changing the cost.
|
|
33
|
+
if (!pricing || !(pricing.cacheWrite > pricing.input))
|
|
34
|
+
return usage;
|
|
35
|
+
const input = finiteNumber(usage.input) ?? 0;
|
|
36
|
+
const cacheRead = finiteNumber(usage.cacheRead) ?? 0;
|
|
37
|
+
const cacheWrite = finiteNumber(usage.cacheWrite) ?? 0;
|
|
38
|
+
// A reported write means this backend did account for the bucket; leave it.
|
|
39
|
+
if (cacheWrite > 0 || input <= 0)
|
|
40
|
+
return usage;
|
|
41
|
+
// Prompts under the provider minimum are never cached, so their fresh tokens
|
|
42
|
+
// are genuinely plain input. This path is gated to the OpenAI subscription
|
|
43
|
+
// API above, so the execution provider is openai by construction.
|
|
44
|
+
if (input + cacheRead < providerMinCacheTokens(context.model, "openai"))
|
|
45
|
+
return usage;
|
|
46
|
+
const cost = isRecord(usage.cost) ? usage.cost : {};
|
|
47
|
+
const outputCost = finiteNumber(cost.output) ?? 0;
|
|
48
|
+
const cacheReadCost = finiteNumber(cost.cacheRead) ?? 0;
|
|
49
|
+
const cacheWriteCost = (input * pricing.cacheWrite) / MTOK;
|
|
50
|
+
return {
|
|
51
|
+
...usage,
|
|
52
|
+
input: 0,
|
|
53
|
+
cacheWrite: input,
|
|
54
|
+
cost: {
|
|
55
|
+
...cost,
|
|
56
|
+
input: 0,
|
|
57
|
+
cacheRead: cacheReadCost,
|
|
58
|
+
cacheWrite: cacheWriteCost,
|
|
59
|
+
output: outputCost,
|
|
60
|
+
total: outputCost + cacheReadCost + cacheWriteCost,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export function modelPricingFromPiModel(model) {
|
|
65
|
+
const cost = model.cost;
|
|
66
|
+
if (!isRecord(cost))
|
|
67
|
+
return null;
|
|
68
|
+
const input = finiteNumber(cost.input);
|
|
69
|
+
const output = finiteNumber(cost.output);
|
|
70
|
+
const cacheRead = finiteNumber(cost.cacheRead);
|
|
71
|
+
const cacheWrite = finiteNumber(cost.cacheWrite);
|
|
72
|
+
if (input === null || output === null || cacheRead === null || cacheWrite === null)
|
|
73
|
+
return null;
|
|
74
|
+
const tiers = Array.isArray(cost.tiers) ? cost.tiers.filter((tier) => isRecord(tier) && [tier.inputTokensAbove, tier.input, tier.output, tier.cacheRead, tier.cacheWrite].every((rate) => finiteNumber(rate) !== null)) : [];
|
|
75
|
+
return { input, output, cacheRead, cacheWrite, ...(tiers.length ? { tiers } : {}) };
|
|
76
|
+
}
|
|
77
|
+
function finiteNumber(value) {
|
|
78
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
79
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { routingCandidateKey, } from "@mupt-ai/dari-router/policy-engine";
|
|
2
|
+
import { isBackgroundRequest } from "../../interaction/types.js";
|
|
3
|
+
import { HttpError } from "../../http/errors.js";
|
|
4
|
+
import { assertCreditAvailableBeforeRouting } from "../../telemetry/usage.js";
|
|
5
|
+
import { leaseFallbackDecision } from "../routing_lease.js";
|
|
6
|
+
// Explicit background classes bypass the selector and every lease path.
|
|
7
|
+
// Keep the existing deterministic cheapest-input-cost serving policy.
|
|
8
|
+
export function auxiliaryDecision(prepared) {
|
|
9
|
+
if (!isBackgroundRequest(prepared.interaction))
|
|
10
|
+
return null;
|
|
11
|
+
assertCreditAvailableBeforeRouting(prepared.billable, prepared.credentials);
|
|
12
|
+
const requestClass = prepared.interaction.agentContext.requestClass;
|
|
13
|
+
const cheapest = cheapestCandidate(prepared);
|
|
14
|
+
if (!cheapest) {
|
|
15
|
+
throw new HttpError(503, "Background requests require a routable model with known pricing.", "background_model_unavailable");
|
|
16
|
+
}
|
|
17
|
+
const decision = {
|
|
18
|
+
selectedModel: cheapest.model,
|
|
19
|
+
reasoningEffort: cheapest.reasoning_effort,
|
|
20
|
+
reason: `${requestClass} request served by the cheapest routable model without selection.`,
|
|
21
|
+
};
|
|
22
|
+
const fallbackDecision = leaseFallbackDecision({
|
|
23
|
+
candidates: prepared.routing.candidateResolution.candidates,
|
|
24
|
+
decision,
|
|
25
|
+
credentials: prepared.credentials,
|
|
26
|
+
});
|
|
27
|
+
if (fallbackDecision)
|
|
28
|
+
prepared.reporter.attachFallbackDecision(fallbackDecision);
|
|
29
|
+
return fallbackDecision ? { decision, fallbackDecision } : { decision };
|
|
30
|
+
}
|
|
31
|
+
/* Ranked on estimated input cost: these requests carry a whole transcript in
|
|
32
|
+
and a sentence out, so the prompt dominates. Estimates cover the configured
|
|
33
|
+
roster, so they are narrowed to the candidates pruning actually left
|
|
34
|
+
routable. Unpriced candidates are not comparable; fail explicitly if no
|
|
35
|
+
priced candidate is available, never fall through to selection. */
|
|
36
|
+
function cheapestCandidate(prepared) {
|
|
37
|
+
const routable = new Set(prepared.routing.candidateResolution.candidates.map(routingCandidateKey));
|
|
38
|
+
let cheapest = null;
|
|
39
|
+
for (const estimate of prepared.routing.candidateResolution.costEstimates) {
|
|
40
|
+
if (!estimate.pricing_known || estimate.est_input_cost_usd === null)
|
|
41
|
+
continue;
|
|
42
|
+
if (!routable.has(routingCandidateKey({
|
|
43
|
+
model: estimate.model,
|
|
44
|
+
reasoningEffort: estimate.reasoning_effort,
|
|
45
|
+
}))) {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (cheapest === null || cheaperThan(estimate, cheapest))
|
|
49
|
+
cheapest = estimate;
|
|
50
|
+
}
|
|
51
|
+
return cheapest;
|
|
52
|
+
}
|
|
53
|
+
function cheaperThan(candidate, incumbent) {
|
|
54
|
+
const candidateInput = candidate.est_input_cost_usd ?? Infinity;
|
|
55
|
+
const incumbentInput = incumbent.est_input_cost_usd ?? Infinity;
|
|
56
|
+
if (candidateInput !== incumbentInput)
|
|
57
|
+
return candidateInput < incumbentInput;
|
|
58
|
+
return (candidate.output_cost_per_mtok ?? Infinity)
|
|
59
|
+
< (incumbent.output_cost_per_mtok ?? Infinity);
|
|
60
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { HttpError } from "../http/errors.js";
|
|
2
|
+
export declare class CustomerDisconnectedError extends HttpError {
|
|
3
|
+
readonly name = "CustomerDisconnectedError";
|
|
4
|
+
constructor();
|
|
5
|
+
}
|
|
6
|
+
export type ProviderAttemptAbort = {
|
|
7
|
+
readonly signal: AbortSignal;
|
|
8
|
+
readonly aborted: boolean;
|
|
9
|
+
abort(): void;
|
|
10
|
+
dispose(): void;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Give every provider attempt its own cancellation boundary tied to the
|
|
14
|
+
* client's disconnect. A cancelled request stops paying the provider at once;
|
|
15
|
+
* its usage is estimated from what was delivered instead of drained.
|
|
16
|
+
*/
|
|
17
|
+
export declare function createProviderAttemptAbort(args: {
|
|
18
|
+
clientSignal?: AbortSignal;
|
|
19
|
+
}): ProviderAttemptAbort;
|
|
20
|
+
export declare function customerCanceledProviderMessage(usage?: Record<string, unknown>, responseId?: string): Record<string, unknown>;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { HttpError } from "../http/errors.js";
|
|
2
|
+
const CUSTOMER_DISCONNECTED_MESSAGE = "Client disconnected.";
|
|
3
|
+
export class CustomerDisconnectedError extends HttpError {
|
|
4
|
+
name = "CustomerDisconnectedError";
|
|
5
|
+
constructor() {
|
|
6
|
+
// 499 is the conventional server-side classification for a client that
|
|
7
|
+
// closed the request before a response could be delivered.
|
|
8
|
+
super(499, CUSTOMER_DISCONNECTED_MESSAGE, "client_disconnected");
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Give every provider attempt its own cancellation boundary tied to the
|
|
13
|
+
* client's disconnect. A cancelled request stops paying the provider at once;
|
|
14
|
+
* its usage is estimated from what was delivered instead of drained.
|
|
15
|
+
*/
|
|
16
|
+
export function createProviderAttemptAbort(args) {
|
|
17
|
+
const controller = new AbortController();
|
|
18
|
+
const abort = () => {
|
|
19
|
+
if (!controller.signal.aborted) {
|
|
20
|
+
controller.abort(new CustomerDisconnectedError());
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const clientSignal = args.clientSignal;
|
|
24
|
+
if (clientSignal?.aborted) {
|
|
25
|
+
abort();
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
clientSignal?.addEventListener("abort", abort, { once: true });
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
signal: controller.signal,
|
|
32
|
+
get aborted() {
|
|
33
|
+
return controller.signal.aborted;
|
|
34
|
+
},
|
|
35
|
+
abort,
|
|
36
|
+
dispose() {
|
|
37
|
+
clientSignal?.removeEventListener("abort", abort);
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export function customerCanceledProviderMessage(usage, responseId) {
|
|
42
|
+
return {
|
|
43
|
+
stopReason: "aborted",
|
|
44
|
+
errorMessage: CUSTOMER_DISCONNECTED_MESSAGE,
|
|
45
|
+
content: [],
|
|
46
|
+
...(usage ? { usage, usage_estimated: usage.estimated === true } : {}),
|
|
47
|
+
...(responseId ? { responseId } : {}),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { RoutedDecision } from "../strategies/route_decision.js";
|
|
2
|
+
import type { PreparedRoutedCompletion } from "./prepare_routed_completion.js";
|
|
3
|
+
export declare function explicitModelDecision(prepared: PreparedRoutedCompletion): RoutedDecision | null;
|
|
4
|
+
export declare function strictModelDecision(prepared: PreparedRoutedCompletion): RoutedDecision;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { HttpError } from "../http/errors.js";
|
|
2
|
+
import { explicitModelId } from "../protocol/input/router_model.js";
|
|
3
|
+
/* A request for `dari/model/<id>` is served by exactly that enabled model,
|
|
4
|
+
with no selector, lease, or fallback: the user chose it, like picking a
|
|
5
|
+
model in their agent's /model menu. The client's thinking level is kept
|
|
6
|
+
when the model supports it; otherwise the model's default level is used. */
|
|
7
|
+
export function explicitModelDecision(prepared) {
|
|
8
|
+
const modelId = explicitModelId(prepared.interaction.model);
|
|
9
|
+
if (modelId === null)
|
|
10
|
+
return null;
|
|
11
|
+
if (!prepared.credentials.enabled_models.includes(modelId)) {
|
|
12
|
+
throw new HttpError(400, `Model '${modelId}' is not enabled on this router.`, "model_not_enabled", "model");
|
|
13
|
+
}
|
|
14
|
+
const candidates = prepared.routing.candidateResolution.candidates.filter((candidate) => candidate.model === modelId);
|
|
15
|
+
if (candidates.length === 0) {
|
|
16
|
+
throw new HttpError(400, `Model '${modelId}' cannot serve this request.`, "model_unavailable", "model");
|
|
17
|
+
}
|
|
18
|
+
const candidate = pickCandidate(prepared, modelId, candidates);
|
|
19
|
+
const decision = {
|
|
20
|
+
selectedModel: candidate.model,
|
|
21
|
+
reasoningEffort: candidate.reasoningEffort,
|
|
22
|
+
reason: `Requested model ${modelId} served without selection.`,
|
|
23
|
+
};
|
|
24
|
+
return { decision };
|
|
25
|
+
}
|
|
26
|
+
function pickCandidate(prepared, modelId, candidates) {
|
|
27
|
+
const preferred = [
|
|
28
|
+
prepared.interaction.reasoning?.effort,
|
|
29
|
+
prepared.credentials.model_configs?.[modelId]?.default_thinking_level,
|
|
30
|
+
];
|
|
31
|
+
for (const effort of preferred) {
|
|
32
|
+
const match = candidates.find((candidate) => candidate.reasoningEffort === effort);
|
|
33
|
+
if (match)
|
|
34
|
+
return match;
|
|
35
|
+
}
|
|
36
|
+
return candidates[0];
|
|
37
|
+
}
|
|
38
|
+
/* A strict router serves only the pinned `dari/model/<id>` at exactly the
|
|
39
|
+
requested thinking level (the model's default when none is sent), and
|
|
40
|
+
refuses everything else instead of substituting. */
|
|
41
|
+
export function strictModelDecision(prepared) {
|
|
42
|
+
const modelId = explicitModelId(prepared.interaction.model);
|
|
43
|
+
if (modelId === null) {
|
|
44
|
+
throw new HttpError(400, "This router is strict: request a pinned model as 'dari/model/<id>'.", "strict_model_required", "model");
|
|
45
|
+
}
|
|
46
|
+
const { decision } = explicitModelDecision(prepared);
|
|
47
|
+
const requested = prepared.interaction.reasoning?.effort
|
|
48
|
+
?? prepared.credentials.model_configs?.[modelId]?.default_thinking_level;
|
|
49
|
+
if (requested !== undefined && requested !== null && decision.reasoningEffort !== requested) {
|
|
50
|
+
throw new HttpError(400, `Model '${modelId}' cannot serve thinking level '${requested}' on this router.`, "thinking_level_unavailable", "reasoning_effort");
|
|
51
|
+
}
|
|
52
|
+
return { decision };
|
|
53
|
+
}
|