Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions src/routeplane/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,17 @@
from .async_client import AsyncRouteplane
from .client import Routeplane
from .headers import headers
from .meta import RouteplaneMeta
from .resources import Decision, RunStep
from .meta import RouteplaneMeta, RouteplaneRateLimits
from .resources import (
CacheSavings,
DailyPricingEvidence,
DailyUsageReport,
Decision,
RunStep,
SaverMetrics,
TimeseriesData,
UsageData,
)

__all__ = [
"Routeplane",
Expand All @@ -24,7 +33,14 @@
"AsyncRouteplaneStream",
"headers",
"RouteplaneMeta",
"RouteplaneRateLimits",
"Decision",
"CacheSavings",
"DailyPricingEvidence",
"DailyUsageReport",
"RunStep",
"SaverMetrics",
"TimeseriesData",
"UsageData",
"__version__",
]
20 changes: 18 additions & 2 deletions src/routeplane/headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"provider": "x-routeplane-provider",
"residency": "x-routeplane-residency",
"strategy": "x-routeplane-strategy",
"canary_share_bps": "x-routeplane-canary-share-bps",
"config": "x-routeplane-config",
"timeout_ms": "x-routeplane-timeout-ms",
"use_case": "x-routeplane-use-case",
Expand All @@ -43,8 +44,11 @@ def headers(
*,
provider: str | None = None,
residency: str | None = None,
strategy: Literal["priority", "weighted", "cost", "latency", "round_robin", "least_busy"]
strategy: Literal[
"priority", "weighted", "cost", "latency", "round_robin", "least_busy", "canary"
]
| None = None,
canary_share_bps: int | None = None,
config: dict[str, Any] | None = None,
timeout_ms: int | None = None,
use_case: str | None = None,
Expand Down Expand Up @@ -79,6 +83,10 @@ def headers(
gateway matches case-insensitively and falls back to ``"priority"``
for an unknown or empty value rather than erroring, so a typo
silently routes by priority. Superseded by a routing config.
canary_share_bps: Candidate share for ``strategy="canary"`` in basis
points (0..10000). Assignment is deterministic per virtual key and
promotes the second provider in the chain; it is not a per-request
random split.
config: Inline routing/policy config, JSON-serialized onto the wire.
timeout_ms: Per-request upstream timeout in milliseconds.
use_case: Free-form use-case label for analytics/FinOps attribution.
Expand All @@ -90,18 +98,26 @@ def headers(
output_mask: Output masking policy reference.
cache_control: Response-cache directive.
idempotency_key: Client-supplied idempotency key for safe retries.
cohort: Experiment/cohort label.
cohort: Prompt-variant cohort label. This is separate from provider
canary assignment and overrides the prompt registry's derived cohort.
batch: Batch identifier.
trace_id: Client-supplied distributed-trace id (echoed back on the response).

Returns:
A ``dict[str, str]`` of header name to value, suitable to splat into any
client's extra/default headers.
"""
if canary_share_bps is not None:
if isinstance(canary_share_bps, bool) or not isinstance(canary_share_bps, int):
raise TypeError("canary_share_bps must be an integer")
if not 0 <= canary_share_bps <= 10000:
raise ValueError("canary_share_bps must be between 0 and 10000")

values: dict[str, Any] = {
"provider": provider,
"residency": residency,
"strategy": strategy,
"canary_share_bps": canary_share_bps,
"config": config,
"timeout_ms": timeout_ms,
"use_case": use_case,
Expand Down
36 changes: 35 additions & 1 deletion src/routeplane/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from dataclasses import dataclass
from typing import Mapping, Optional

__all__ = ["RouteplaneMeta"]
__all__ = ["RouteplaneMeta", "RouteplaneRateLimits"]

_TRUE = {"true", "1", "yes", "on"}

Expand All @@ -25,6 +25,28 @@ def _flag(headers: Mapping[str, str], name: str) -> bool:
return value.strip().lower() in _TRUE


def _integer(headers: Mapping[str, str], name: str) -> Optional[int]:
value = headers.get(name)
if value is None:
return None
try:
return int(value)
except ValueError:
return None


@dataclass(frozen=True)
class RouteplaneRateLimits:
"""Advisory values from the gateway's standard ``x-ratelimit-*`` headers."""

requests_limit: Optional[int] = None
requests_remaining: Optional[int] = None
requests_reset: Optional[str] = None
tokens_limit: Optional[int] = None
tokens_remaining: Optional[int] = None
tokens_reset: Optional[str] = None


@dataclass(frozen=True)
class RouteplaneMeta:
"""Typed view over the ``x-routeplane-*`` response headers.
Expand All @@ -45,6 +67,7 @@ class RouteplaneMeta:
compliance_warning: Optional[str] = None
pii_masked: bool = False
idempotent_replayed: bool = False
rate_limits: Optional[RouteplaneRateLimits] = None

@classmethod
def from_headers(cls, headers: Mapping[str, str]) -> "RouteplaneMeta":
Expand All @@ -59,6 +82,16 @@ def from_headers(cls, headers: Mapping[str, str]) -> "RouteplaneMeta":
A populated :class:`RouteplaneMeta`. Missing headers fall back to
``None`` (strings) or ``False`` (flags).
"""
rate_limits: Optional[RouteplaneRateLimits] = RouteplaneRateLimits(
requests_limit=_integer(headers, "x-ratelimit-limit-requests"),
requests_remaining=_integer(headers, "x-ratelimit-remaining-requests"),
requests_reset=headers.get("x-ratelimit-reset-requests"),
tokens_limit=_integer(headers, "x-ratelimit-limit-tokens"),
tokens_remaining=_integer(headers, "x-ratelimit-remaining-tokens"),
tokens_reset=headers.get("x-ratelimit-reset-tokens"),
)
if rate_limits == RouteplaneRateLimits():
rate_limits = None
return cls(
provider=headers.get("x-routeplane-provider"),
trace_id=headers.get("x-routeplane-trace-id"),
Expand All @@ -72,4 +105,5 @@ def from_headers(cls, headers: Mapping[str, str]) -> "RouteplaneMeta":
compliance_warning=headers.get("x-routeplane-compliance-warning"),
pii_masked=_flag(headers, "x-routeplane-pii-masked"),
idempotent_replayed=_flag(headers, "x-routeplane-idempotent-replayed"),
rate_limits=rate_limits,
)
16 changes: 15 additions & 1 deletion src/routeplane/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,15 @@
from .analytics import AnalyticsResource
from .cache import CacheResource
from .feedback import FeedbackResource
from .finops import FinopsResource
from .finops import (
CacheSavings,
DailyPricingEvidence,
DailyUsageReport,
FinopsResource,
SaverMetrics,
TimeseriesData,
UsageData,
)
from .logs import LogsResource
from .mcp import Decision, McpResource, RunStep
from .models import ModelsResource
Expand All @@ -22,12 +30,18 @@
"BaseResource",
"AnalyticsResource",
"CacheResource",
"CacheSavings",
"DailyPricingEvidence",
"Decision",
"DailyUsageReport",
"FeedbackResource",
"FinopsResource",
"LogsResource",
"McpResource",
"RunStep",
"SaverMetrics",
"TimeseriesData",
"UsageData",
"ModelsResource",
"PromptsResource",
"ProvidersResource",
Expand Down
Loading
Loading