coderouter-cli 2.0.0__py3-none-any.whl → 2.1.0__py3-none-any.whl
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.
- coderouter/config/schemas.py +103 -0
- coderouter/guards/continuous_probe.py +349 -0
- coderouter/guards/drift_actions.py +111 -0
- coderouter/guards/drift_detection.py +308 -0
- coderouter/ingress/anthropic_routes.py +75 -11
- coderouter/ingress/app.py +39 -0
- coderouter/logging.py +262 -0
- coderouter/metrics/collector.py +93 -0
- coderouter/metrics/prometheus.py +141 -0
- coderouter/routing/adaptive.py +23 -0
- coderouter/routing/fallback.py +285 -4
- {coderouter_cli-2.0.0.dist-info → coderouter_cli-2.1.0.dist-info}/METADATA +7 -6
- {coderouter_cli-2.0.0.dist-info → coderouter_cli-2.1.0.dist-info}/RECORD +16 -13
- {coderouter_cli-2.0.0.dist-info → coderouter_cli-2.1.0.dist-info}/WHEEL +0 -0
- {coderouter_cli-2.0.0.dist-info → coderouter_cli-2.1.0.dist-info}/entry_points.txt +0 -0
- {coderouter_cli-2.0.0.dist-info → coderouter_cli-2.1.0.dist-info}/licenses/LICENSE +0 -0
coderouter/routing/fallback.py
CHANGED
|
@@ -26,7 +26,10 @@ from __future__ import annotations
|
|
|
26
26
|
|
|
27
27
|
import time
|
|
28
28
|
from collections.abc import AsyncIterator
|
|
29
|
-
from typing import Final
|
|
29
|
+
from typing import TYPE_CHECKING, Any, Final
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from coderouter.guards.drift_detection import DriftVerdict
|
|
30
33
|
|
|
31
34
|
from coderouter.adapters.anthropic_native import AnthropicAdapter
|
|
32
35
|
from coderouter.adapters.base import (
|
|
@@ -469,11 +472,16 @@ class _StreamUsageAccumulator:
|
|
|
469
472
|
"""
|
|
470
473
|
|
|
471
474
|
__slots__ = (
|
|
475
|
+
"_current_block_text",
|
|
476
|
+
"_current_block_type",
|
|
472
477
|
"_observed",
|
|
478
|
+
"_text_blocks",
|
|
473
479
|
"cache_creation_input_tokens",
|
|
474
480
|
"cache_read_input_tokens",
|
|
481
|
+
"has_tool_use",
|
|
475
482
|
"input_tokens",
|
|
476
483
|
"output_tokens",
|
|
484
|
+
"stop_reason",
|
|
477
485
|
)
|
|
478
486
|
|
|
479
487
|
def __init__(self) -> None:
|
|
@@ -482,6 +490,32 @@ class _StreamUsageAccumulator:
|
|
|
482
490
|
self.cache_read_input_tokens = 0
|
|
483
491
|
self.cache_creation_input_tokens = 0
|
|
484
492
|
self._observed = False
|
|
493
|
+
# v2.0-G: tracked for drift detection observation at stream end.
|
|
494
|
+
self.has_tool_use: bool = False
|
|
495
|
+
self.stop_reason: str | None = None
|
|
496
|
+
# v2.0-H: partial content accumulation for mid-stream recovery.
|
|
497
|
+
# Completed text blocks are moved to _text_blocks on content_block_stop.
|
|
498
|
+
# In-progress text is in _current_block_text (list of str fragments).
|
|
499
|
+
self._text_blocks: list[str] = []
|
|
500
|
+
self._current_block_type: str | None = None
|
|
501
|
+
self._current_block_text: list[str] = []
|
|
502
|
+
|
|
503
|
+
@property
|
|
504
|
+
def partial_content(self) -> list[dict[str, Any]]:
|
|
505
|
+
"""Return accumulated text content as Anthropic content blocks.
|
|
506
|
+
|
|
507
|
+
Includes both completed blocks and any in-progress text block
|
|
508
|
+
(useful when the stream is interrupted mid-block). Tool_use blocks
|
|
509
|
+
are excluded because partial JSON is unusable.
|
|
510
|
+
"""
|
|
511
|
+
blocks: list[dict[str, Any]] = []
|
|
512
|
+
for text in self._text_blocks:
|
|
513
|
+
if text:
|
|
514
|
+
blocks.append({"type": "text", "text": text})
|
|
515
|
+
# Include in-progress text block if any
|
|
516
|
+
if self._current_block_type == "text" and self._current_block_text:
|
|
517
|
+
blocks.append({"type": "text", "text": "".join(self._current_block_text)})
|
|
518
|
+
return blocks
|
|
485
519
|
|
|
486
520
|
def observe(self, event: AnthropicStreamEvent) -> None:
|
|
487
521
|
"""Update counters from one stream event (no-op for non-usage events)."""
|
|
@@ -494,6 +528,33 @@ class _StreamUsageAccumulator:
|
|
|
494
528
|
usage = event.data.get("usage") if isinstance(event.data, dict) else None
|
|
495
529
|
if isinstance(usage, dict):
|
|
496
530
|
self._merge(usage)
|
|
531
|
+
# v2.0-G: capture stop_reason from the terminal message_delta.
|
|
532
|
+
delta = event.data.get("delta") if isinstance(event.data, dict) else None
|
|
533
|
+
if isinstance(delta, dict) and "stop_reason" in delta:
|
|
534
|
+
self.stop_reason = delta["stop_reason"]
|
|
535
|
+
elif event.type == "content_block_start":
|
|
536
|
+
# v2.0-G: detect tool_use content blocks for drift observation.
|
|
537
|
+
cb = event.data.get("content_block") if isinstance(event.data, dict) else None
|
|
538
|
+
if isinstance(cb, dict):
|
|
539
|
+
block_type = cb.get("type", "")
|
|
540
|
+
if block_type == "tool_use":
|
|
541
|
+
self.has_tool_use = True
|
|
542
|
+
# v2.0-H: start tracking a new content block.
|
|
543
|
+
self._current_block_type = block_type
|
|
544
|
+
self._current_block_text = []
|
|
545
|
+
elif event.type == "content_block_delta":
|
|
546
|
+
# v2.0-H: accumulate text_delta fragments.
|
|
547
|
+
delta = event.data.get("delta") if isinstance(event.data, dict) else None
|
|
548
|
+
if isinstance(delta, dict) and delta.get("type") == "text_delta":
|
|
549
|
+
text = delta.get("text", "")
|
|
550
|
+
if text:
|
|
551
|
+
self._current_block_text.append(text)
|
|
552
|
+
elif event.type == "content_block_stop":
|
|
553
|
+
# v2.0-H: finalize current block.
|
|
554
|
+
if self._current_block_type == "text" and self._current_block_text:
|
|
555
|
+
self._text_blocks.append("".join(self._current_block_text))
|
|
556
|
+
self._current_block_type = None
|
|
557
|
+
self._current_block_text = []
|
|
497
558
|
|
|
498
559
|
def _merge(self, usage: dict[str, object]) -> None:
|
|
499
560
|
any_nonzero = False
|
|
@@ -613,9 +674,18 @@ class MidStreamError(CodeRouterError):
|
|
|
613
674
|
one chunk to the client. Fallback is not attempted (the client has
|
|
614
675
|
received partial content, so switching providers would corrupt the
|
|
615
676
|
stream). Callers should surface this as a terminal error event.
|
|
677
|
+
|
|
678
|
+
v2.0-H: carries ``partial_content`` — the accumulated text blocks
|
|
679
|
+
generated before the failure. The ingress uses this to synthesize
|
|
680
|
+
a graceful stream termination when ``partial_stitch_action: surface``.
|
|
616
681
|
"""
|
|
617
682
|
|
|
618
|
-
def __init__(
|
|
683
|
+
def __init__(
|
|
684
|
+
self,
|
|
685
|
+
provider: str,
|
|
686
|
+
original: AdapterError,
|
|
687
|
+
partial_content: list[dict[str, Any]] | None = None,
|
|
688
|
+
) -> None:
|
|
619
689
|
"""Wrap the underlying :class:`AdapterError` with the provider name.
|
|
620
690
|
|
|
621
691
|
The ingress layer catches this and converts it into an in-stream
|
|
@@ -624,6 +694,7 @@ class MidStreamError(CodeRouterError):
|
|
|
624
694
|
"""
|
|
625
695
|
self.provider = provider
|
|
626
696
|
self.original = original
|
|
697
|
+
self.partial_content: list[dict[str, Any]] = partial_content or []
|
|
627
698
|
super().__init__(f"provider {provider!r} failed mid-stream: {original}")
|
|
628
699
|
|
|
629
700
|
|
|
@@ -747,6 +818,32 @@ class FallbackEngine:
|
|
|
747
818
|
# Distinct from v1.9-C ``adaptive`` which handles the
|
|
748
819
|
# gradient case via a rolling window.
|
|
749
820
|
self._backend_health_monitor: BackendHealthMonitor = BackendHealthMonitor()
|
|
821
|
+
# v2.0-G (L4): per-process drift detection window manager.
|
|
822
|
+
# Stores per-provider rolling observations; the detector is
|
|
823
|
+
# invoked after each provider-ok / provider-failed event and
|
|
824
|
+
# returns a verdict. Action dispatch (promote/reload) reuses
|
|
825
|
+
# the adaptive rank machinery.
|
|
826
|
+
from coderouter.guards.drift_detection import DriftWindow
|
|
827
|
+
|
|
828
|
+
self._drift_window: DriftWindow = DriftWindow()
|
|
829
|
+
# Track which providers are currently in drift-demoted state
|
|
830
|
+
# and when their cooldown expires (monotonic timestamp).
|
|
831
|
+
self._drift_demoted: dict[str, float] = {}
|
|
832
|
+
# Last drift verdict (set by _observe_drift_signal for ingress header).
|
|
833
|
+
self._last_drift_verdict: DriftVerdict | None = None
|
|
834
|
+
|
|
835
|
+
@property
|
|
836
|
+
def last_drift_severity(self) -> str | None:
|
|
837
|
+
"""Return the severity string of the most recent drift verdict, or None.
|
|
838
|
+
|
|
839
|
+
The ingress reads this after generate_anthropic / stream_anthropic to
|
|
840
|
+
set the ``X-CodeRouter-Drift`` response header. Returns ``"mild"`` or
|
|
841
|
+
``"severe"`` when drift was detected, ``None`` otherwise.
|
|
842
|
+
"""
|
|
843
|
+
v = self._last_drift_verdict
|
|
844
|
+
if v is None or not v.drifted:
|
|
845
|
+
return None
|
|
846
|
+
return v.severity
|
|
750
847
|
|
|
751
848
|
@property
|
|
752
849
|
def _adaptive(self) -> AdaptiveAdjuster:
|
|
@@ -794,12 +891,17 @@ class FallbackEngine:
|
|
|
794
891
|
return existing
|
|
795
892
|
|
|
796
893
|
@property
|
|
797
|
-
def
|
|
894
|
+
def backend_health(self) -> BackendHealthMonitor:
|
|
798
895
|
"""Return the L5 backend-health monitor, lazily building one if absent.
|
|
799
896
|
|
|
800
897
|
Same legacy-test compatibility pattern as the other guard
|
|
801
898
|
properties — ``__new__``-constructed engines get a fresh
|
|
802
899
|
empty monitor so ``state_for`` is always answerable.
|
|
900
|
+
|
|
901
|
+
v2.0-I: promoted from ``_backend_health`` to public ``backend_health``
|
|
902
|
+
so the continuous probe background task can feed results into the
|
|
903
|
+
same state machine. Internal callers continue to work (property
|
|
904
|
+
access is transparent).
|
|
803
905
|
"""
|
|
804
906
|
existing = getattr(self, "_backend_health_monitor", None)
|
|
805
907
|
if existing is None:
|
|
@@ -807,6 +909,11 @@ class FallbackEngine:
|
|
|
807
909
|
existing = self._backend_health_monitor
|
|
808
910
|
return existing
|
|
809
911
|
|
|
912
|
+
# Alias for backward compat with internal callers.
|
|
913
|
+
@property
|
|
914
|
+
def _backend_health(self) -> BackendHealthMonitor:
|
|
915
|
+
return self.backend_health
|
|
916
|
+
|
|
810
917
|
def _observe_provider_failure(
|
|
811
918
|
self,
|
|
812
919
|
provider: str,
|
|
@@ -925,6 +1032,132 @@ class FallbackEngine:
|
|
|
925
1032
|
consecutive_failures=transition.consecutive_failures,
|
|
926
1033
|
)
|
|
927
1034
|
|
|
1035
|
+
def _observe_drift_signal(
|
|
1036
|
+
self,
|
|
1037
|
+
provider: str,
|
|
1038
|
+
*,
|
|
1039
|
+
profile: str | None,
|
|
1040
|
+
output_tokens: int = 0,
|
|
1041
|
+
has_tool_use: bool = False,
|
|
1042
|
+
request_had_tools: bool = False,
|
|
1043
|
+
stop_reason: str | None = None,
|
|
1044
|
+
is_error: bool = False,
|
|
1045
|
+
stream: bool = False,
|
|
1046
|
+
) -> DriftVerdict | None:
|
|
1047
|
+
"""v2.0-G (L4): record an observation and check for drift.
|
|
1048
|
+
|
|
1049
|
+
Called after every provider-ok / provider-failed event on the
|
|
1050
|
+
Anthropic-shaped paths. Returns a :class:`DriftVerdict` when
|
|
1051
|
+
drift is detected (drifted=True), None otherwise.
|
|
1052
|
+
|
|
1053
|
+
Side effects on detection:
|
|
1054
|
+
- Emits ``drift-detected`` log.
|
|
1055
|
+
- If action is ``promote`` or ``reload``, demotes the provider
|
|
1056
|
+
via the adaptive rank machinery.
|
|
1057
|
+
"""
|
|
1058
|
+
from coderouter.guards.drift_detection import (
|
|
1059
|
+
SENSITIVITY_PRESETS,
|
|
1060
|
+
ResponseObservation,
|
|
1061
|
+
detect_drift,
|
|
1062
|
+
)
|
|
1063
|
+
from coderouter.logging import log_drift_detected, log_drift_promoted
|
|
1064
|
+
|
|
1065
|
+
chosen = profile or self.config.default_profile
|
|
1066
|
+
try:
|
|
1067
|
+
chain_cfg = self.config.profile_by_name(chosen)
|
|
1068
|
+
except (KeyError, ValueError):
|
|
1069
|
+
return None
|
|
1070
|
+
if chain_cfg.drift_detection_action == "off":
|
|
1071
|
+
return None
|
|
1072
|
+
|
|
1073
|
+
# Update window size if config differs from default
|
|
1074
|
+
self._drift_window.max_size = chain_cfg.drift_detection_window_size
|
|
1075
|
+
|
|
1076
|
+
# Record observation
|
|
1077
|
+
obs = ResponseObservation(
|
|
1078
|
+
provider=provider,
|
|
1079
|
+
output_tokens=output_tokens,
|
|
1080
|
+
has_tool_use=has_tool_use,
|
|
1081
|
+
request_had_tools=request_had_tools,
|
|
1082
|
+
stop_reason=stop_reason,
|
|
1083
|
+
is_error=is_error,
|
|
1084
|
+
stream=stream,
|
|
1085
|
+
)
|
|
1086
|
+
self._drift_window.record(obs)
|
|
1087
|
+
|
|
1088
|
+
# Check for cooldown recovery
|
|
1089
|
+
import time as _time
|
|
1090
|
+
|
|
1091
|
+
demote_expires = self._drift_demoted.get(provider)
|
|
1092
|
+
if demote_expires is not None and _time.monotonic() >= demote_expires:
|
|
1093
|
+
# Cooldown expired — restore rank and clear drift state
|
|
1094
|
+
from coderouter.logging import log_drift_recovered
|
|
1095
|
+
|
|
1096
|
+
elapsed = chain_cfg.drift_detection_cooldown_s
|
|
1097
|
+
log_drift_recovered(logger, provider=provider, profile=chosen, after_s=elapsed)
|
|
1098
|
+
self._drift_demoted.pop(provider, None)
|
|
1099
|
+
self._drift_window.clear(provider)
|
|
1100
|
+
return None
|
|
1101
|
+
|
|
1102
|
+
# Don't re-detect while in cooldown
|
|
1103
|
+
if provider in self._drift_demoted:
|
|
1104
|
+
return None
|
|
1105
|
+
|
|
1106
|
+
# Run detection
|
|
1107
|
+
window = self._drift_window.get_window(provider)
|
|
1108
|
+
thresholds = SENSITIVITY_PRESETS.get(
|
|
1109
|
+
chain_cfg.drift_detection_sensitivity, SENSITIVITY_PRESETS["normal"]
|
|
1110
|
+
)
|
|
1111
|
+
verdict = detect_drift(window, thresholds)
|
|
1112
|
+
|
|
1113
|
+
if not verdict.drifted:
|
|
1114
|
+
self._last_drift_verdict = None
|
|
1115
|
+
return None
|
|
1116
|
+
|
|
1117
|
+
# Store for ingress response header.
|
|
1118
|
+
self._last_drift_verdict = verdict
|
|
1119
|
+
|
|
1120
|
+
# Emit log
|
|
1121
|
+
log_drift_detected(
|
|
1122
|
+
logger,
|
|
1123
|
+
provider=provider,
|
|
1124
|
+
profile=chosen,
|
|
1125
|
+
severity=verdict.severity,
|
|
1126
|
+
reason=verdict.reason,
|
|
1127
|
+
action=chain_cfg.drift_detection_action,
|
|
1128
|
+
signals=verdict.signals,
|
|
1129
|
+
)
|
|
1130
|
+
|
|
1131
|
+
# Action: promote / reload
|
|
1132
|
+
if chain_cfg.drift_detection_action in ("promote", "reload"):
|
|
1133
|
+
import time as _time_mod
|
|
1134
|
+
|
|
1135
|
+
# Demote via adaptive rank
|
|
1136
|
+
self._adaptive.demote(provider, steps=2)
|
|
1137
|
+
log_drift_promoted(
|
|
1138
|
+
logger,
|
|
1139
|
+
provider=provider,
|
|
1140
|
+
profile=chosen,
|
|
1141
|
+
demoted_to_rank=2,
|
|
1142
|
+
cooldown_s=chain_cfg.drift_detection_cooldown_s,
|
|
1143
|
+
)
|
|
1144
|
+
# Record cooldown expiry
|
|
1145
|
+
self._drift_demoted[provider] = (
|
|
1146
|
+
_time_mod.monotonic() + chain_cfg.drift_detection_cooldown_s
|
|
1147
|
+
)
|
|
1148
|
+
|
|
1149
|
+
# v2.0-G: reload action — attempt Ollama KV cache flush
|
|
1150
|
+
# (best-effort, fire-and-forget background task).
|
|
1151
|
+
if chain_cfg.drift_detection_action == "reload":
|
|
1152
|
+
import asyncio
|
|
1153
|
+
|
|
1154
|
+
from coderouter.guards.drift_actions import attempt_reload
|
|
1155
|
+
|
|
1156
|
+
provider_config = self._adapters[provider].config
|
|
1157
|
+
self._reload_task = asyncio.create_task(attempt_reload(provider_config))
|
|
1158
|
+
|
|
1159
|
+
return verdict
|
|
1160
|
+
|
|
928
1161
|
def _resolve_profile_overrides(self, profile_name: str | None) -> ProviderCallOverrides:
|
|
929
1162
|
"""v0.6-B: build the ProviderCallOverrides for the active profile.
|
|
930
1163
|
|
|
@@ -1455,6 +1688,14 @@ class FallbackEngine:
|
|
|
1455
1688
|
self._observe_provider_failure(
|
|
1456
1689
|
adapter.name, exc, profile=request.profile
|
|
1457
1690
|
)
|
|
1691
|
+
# v2.0-G (L4): drift detection observation (failure path).
|
|
1692
|
+
self._observe_drift_signal(
|
|
1693
|
+
adapter.name,
|
|
1694
|
+
profile=request.profile,
|
|
1695
|
+
is_error=True,
|
|
1696
|
+
request_had_tools=bool(request.tools),
|
|
1697
|
+
stream=False,
|
|
1698
|
+
)
|
|
1458
1699
|
errors.append(exc)
|
|
1459
1700
|
if not exc.retryable:
|
|
1460
1701
|
break
|
|
@@ -1482,6 +1723,18 @@ class FallbackEngine:
|
|
|
1482
1723
|
self._observe_provider_success(
|
|
1483
1724
|
adapter.name, profile=request.profile
|
|
1484
1725
|
)
|
|
1726
|
+
# v2.0-G (L4): drift detection observation (success path).
|
|
1727
|
+
self._observe_drift_signal(
|
|
1728
|
+
adapter.name,
|
|
1729
|
+
profile=request.profile,
|
|
1730
|
+
output_tokens=resp.usage.output_tokens if resp.usage else 0,
|
|
1731
|
+
has_tool_use=any(
|
|
1732
|
+
getattr(b, "type", None) == "tool_use" for b in (resp.content or [])
|
|
1733
|
+
),
|
|
1734
|
+
request_had_tools=bool(request.tools),
|
|
1735
|
+
stop_reason=resp.stop_reason,
|
|
1736
|
+
stream=False,
|
|
1737
|
+
)
|
|
1485
1738
|
# v1.9-A: pair every successful Anthropic response with a
|
|
1486
1739
|
# cache-observed log line. Native Anthropic / LM Studio
|
|
1487
1740
|
# /v1/messages report cache_read_input_tokens /
|
|
@@ -1620,6 +1873,14 @@ class FallbackEngine:
|
|
|
1620
1873
|
self._observe_provider_failure(
|
|
1621
1874
|
adapter.name, exc, profile=request.profile
|
|
1622
1875
|
)
|
|
1876
|
+
# v2.0-G (L4): drift detection observation (stream failure).
|
|
1877
|
+
self._observe_drift_signal(
|
|
1878
|
+
adapter.name,
|
|
1879
|
+
profile=request.profile,
|
|
1880
|
+
is_error=True,
|
|
1881
|
+
request_had_tools=bool(request.tools),
|
|
1882
|
+
stream=True,
|
|
1883
|
+
)
|
|
1623
1884
|
errors.append(exc)
|
|
1624
1885
|
if not exc.retryable:
|
|
1625
1886
|
break
|
|
@@ -1662,7 +1923,27 @@ class FallbackEngine:
|
|
|
1662
1923
|
self._observe_provider_failure(
|
|
1663
1924
|
adapter.name, exc, profile=request.profile
|
|
1664
1925
|
)
|
|
1665
|
-
|
|
1926
|
+
# v2.0-G (L4): drift detection observation (mid-stream failure).
|
|
1927
|
+
self._observe_drift_signal(
|
|
1928
|
+
adapter.name,
|
|
1929
|
+
profile=request.profile,
|
|
1930
|
+
is_error=True,
|
|
1931
|
+
request_had_tools=bool(request.tools),
|
|
1932
|
+
stream=True,
|
|
1933
|
+
)
|
|
1934
|
+
raise MidStreamError(
|
|
1935
|
+
adapter.name, exc, partial_content=acc.partial_content
|
|
1936
|
+
) from exc
|
|
1937
|
+
# v2.0-G (L4): drift detection observation (stream success).
|
|
1938
|
+
self._observe_drift_signal(
|
|
1939
|
+
adapter.name,
|
|
1940
|
+
profile=request.profile,
|
|
1941
|
+
output_tokens=acc.output_tokens,
|
|
1942
|
+
has_tool_use=acc.has_tool_use,
|
|
1943
|
+
request_had_tools=bool(request.tools),
|
|
1944
|
+
stop_reason=acc.stop_reason,
|
|
1945
|
+
stream=True,
|
|
1946
|
+
)
|
|
1666
1947
|
# v1.9-B2: pair the successful stream with a cache-observed
|
|
1667
1948
|
# log line carrying the aggregated usage counters that the
|
|
1668
1949
|
# ``_StreamUsageAccumulator`` collected from the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coderouter-cli
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Local-first, free-first, fallback-built-in LLM router. Claude Code / OpenAI compatible.
|
|
5
5
|
Project-URL: Homepage, https://github.com/zephel01/CodeRouter
|
|
6
6
|
Project-URL: Repository, https://github.com/zephel01/CodeRouter
|
|
@@ -60,7 +60,7 @@ Description-Content-Type: text/markdown
|
|
|
60
60
|
<p align="center">
|
|
61
61
|
<a href="https://github.com/zephel01/CodeRouter/actions/workflows/ci.yml"><img src="https://github.com/zephel01/CodeRouter/actions/workflows/ci.yml/badge.svg?branch=main" alt="CI"></a>
|
|
62
62
|
<a href=""><img src="https://img.shields.io/badge/status-stable-brightgreen" alt="status"></a>
|
|
63
|
-
<a href=""><img src="https://img.shields.io/badge/version-2.
|
|
63
|
+
<a href=""><img src="https://img.shields.io/badge/version-2.1.0-blue" alt="version"></a>
|
|
64
64
|
<a href=""><img src="https://img.shields.io/badge/python-3.12%2B-blue" alt="python"></a>
|
|
65
65
|
<a href=""><img src="https://img.shields.io/badge/runtime%20deps-5-brightgreen" alt="deps"></a>
|
|
66
66
|
<a href=""><img src="https://img.shields.io/badge/license-MIT-yellow" alt="license"></a>
|
|
@@ -91,7 +91,8 @@ Description-Content-Type: text/markdown
|
|
|
91
91
|
- **v1.10.0 で Long-run reliability pillar が完成**: `cost.monthly_budget_usd` で provider 月次 USD 予算を強制、**L2 memory pressure detector**(Ollama / LM Studio が VRAM 切れで OOM になった時に自動クールダウン)、**L5 backend health 状態機械**(連続失敗で UNHEALTHY → chain 末尾に降格、1 回成功で即復帰)
|
|
92
92
|
- **v1.10.0 で auto-router が 6 matcher に揃う**: `has_image` / `code_fence_ratio_min` / `content_contains` / `content_regex` / `model_pattern`(Opus/Sonnet/Haiku 分岐)/ `content_token_count_min`(長文 → 1M ctx Gemini Flash 等へ自動切替)
|
|
93
93
|
- **v2.0.0 で Context Budget Management (L1 overflow 防止) を搭載**: 長時間 agent session で messages が context window に漸近 → backend 400 エラーで session 死亡する問題を根本解決。warn (80%) → auto trim (90%) の 2 段階 guard で **context overflow ゼロ**を実現。tool_use / tool_result ペアは atomic 保全、`X-CodeRouter-Context-Budget` ヘッダで状態通知、Prometheus メトリクス完備
|
|
94
|
-
-
|
|
94
|
+
- **v2.1.0 で Long-run Reliability 3 機能を追加搭載**: **Drift Detection** (L4 品質劣化検知 — 5 シグナル rolling window + warn/promote/reload 3 段階アクション)、**Partial Stitching** (L6 mid-stream 失敗時の蓄積テキスト返却)、**Continuous Probing** (P3 idle 時 1-token 定期 probe + model drift 検知 + backend health 自動更新)
|
|
95
|
+
- ランタイム依存 5 個(`fastapi` / `uvicorn` / `httpx` / `pydantic` / `pyyaml`)— 純 Python、MIT、テスト 950 本緑
|
|
95
96
|
|
|
96
97
|
→ **Claude Code / gemini-cli / codex + Ollama / llama.cpp / NVIDIA NIM で、破綻しない local-first agent が組める**
|
|
97
98
|
|
|
@@ -198,7 +199,7 @@ CodeRouter / Voice Bridge ともに独立した repo で進化していて、HTT
|
|
|
198
199
|
|
|
199
200
|
## クイックスタート(3 コマンド)
|
|
200
201
|
|
|
201
|
-
**v2.
|
|
202
|
+
**v2.1.0 で Long-run Reliability pillar 完成** — Context Budget (L1) + Drift Detection (L4) + Partial Stitching (L6) + Continuous Probing (P3)。`uvx` 一発で動きます (Python 3.12 以上必須):
|
|
202
203
|
|
|
203
204
|
```bash
|
|
204
205
|
# 1. サンプル設定を置く
|
|
@@ -266,9 +267,9 @@ CodeRouter 自体は純 Python 3.12+ で、実質的な OS 対応範囲は `min(
|
|
|
266
267
|
|
|
267
268
|
注意点や「ローカル GPU なし」向けレシピを含むフル版マトリクス: [利用ガイド §1](./docs/usage-guide.md#1-os-互換性)
|
|
268
269
|
|
|
269
|
-
## ステータス — v2.
|
|
270
|
+
## ステータス — v2.1.0 (2026-05)
|
|
270
271
|
|
|
271
|
-
**テスト
|
|
272
|
+
**テスト 950 本通過。ランタイム依存 5 個 (39 sub-release 連続据え置き)。macOS / Linux / Windows WSL2 で動作。** v2.1.0 で **Long-run Reliability pillar が完成** — Context Budget (L1) / Drift Detection (L4) / Partial Stitching (L6) / Continuous Probing (P3) の 4 sub-release を統合出荷。v1.10.0 までに **Long-run Reliability** (L2/L3/L5)、**Cost pillar**、**Auto-router 6 matcher** が完成済み。v1.0 の総まとめは [`docs/retrospectives/v1.0.md`](./docs/retrospectives/v1.0.md)。
|
|
272
273
|
|
|
273
274
|
今日の CodeRouter が届ける価値:
|
|
274
275
|
|
|
@@ -7,7 +7,7 @@ coderouter/doctor.py,sha256=2luNk6BHSRvpQStJnHcqzNvNi-SKdOuKV0WZdorZhVk,82854
|
|
|
7
7
|
coderouter/doctor_apply.py,sha256=r_J6xbu5-HivofPNriw4_vjNYs_VRs7GsGTS0oMEX10,24209
|
|
8
8
|
coderouter/env_security.py,sha256=FEBZnXfJ0xE39kmMMn39zk0W_DRRnmcB_REmP9f4xWo,14796
|
|
9
9
|
coderouter/errors.py,sha256=Xmq67lheyw8iv3Ox39jh2c4tvNI5RcUR4QkoxVDN6l4,1130
|
|
10
|
-
coderouter/logging.py,sha256=
|
|
10
|
+
coderouter/logging.py,sha256=DV1BwLsQK3wa8493omij5hiG7lU0WTnJFk9xA-ei5Zc,49831
|
|
11
11
|
coderouter/output_filters.py,sha256=rI4YgKVv5vviDBl3Xkf7rp6LaSSkdWyEV004q6HrkB0,15706
|
|
12
12
|
coderouter/token_estimation.py,sha256=1Ai1uT68hahpyr4LBhNyVRGq7y4yXItd6J4k5ApGX7M,5995
|
|
13
13
|
coderouter/adapters/__init__.py,sha256=7dIDSZ-FE_0iSqLSDc_lK1idRdLTKcM2hP9tCJipgPI,463
|
|
@@ -19,35 +19,38 @@ coderouter/config/__init__.py,sha256=FODEn74fN-qZnt4INPSHswqhOlEgpL6-_onxsitSx8g
|
|
|
19
19
|
coderouter/config/capability_registry.py,sha256=F6DetVL5oM03R4QeK1g6h_Q_zrXH0opnYDp3duZmkN4,15808
|
|
20
20
|
coderouter/config/env_file.py,sha256=CoMK27fuAXm-NtoLzXb8yN2E-wDFjHQuFwiIlmgTBQw,10356
|
|
21
21
|
coderouter/config/loader.py,sha256=FUEe8m4Tnmj_aul0vSctD8vKvNW-oLRoMRbTpSKqSmc,4077
|
|
22
|
-
coderouter/config/schemas.py,sha256=
|
|
22
|
+
coderouter/config/schemas.py,sha256=F0xHOnn9knwuaYNBL2sKOwum08mvKFP57JWKkS_-Sho,46407
|
|
23
23
|
coderouter/data/__init__.py,sha256=uNyfD9jaCvTWsBAWtaw1Fr25OSxzv3psGMfBjT1z0Cc,328
|
|
24
24
|
coderouter/data/model-capabilities.yaml,sha256=2ztY4PUbGN_cWG-UUB-iPy-baeVFnGV8OcZHJUfZE7c,19290
|
|
25
25
|
coderouter/guards/__init__.py,sha256=eYjuKo0OvE-GjJo7drYtU2XavUPF9OdiTB5IS76c92Y,855
|
|
26
26
|
coderouter/guards/backend_health.py,sha256=bW148sadK60ZHaz9n6HKeRMcWxfAEvieBVGQhGh_MwY,7686
|
|
27
27
|
coderouter/guards/context_budget.py,sha256=moWulVr5NtVci13vXxS0ucV4EvX2b7tbA1W1d9eQnkw,13281
|
|
28
|
+
coderouter/guards/continuous_probe.py,sha256=AKNMbJ7hUJG-FDoU160BCbSEQQUyw0hBxFYMTaBZg84,11681
|
|
29
|
+
coderouter/guards/drift_actions.py,sha256=A6pY5CR480Ct5rCVyjlBvjPFVc93eu_r5qcUpK9mWKc,3602
|
|
30
|
+
coderouter/guards/drift_detection.py,sha256=W6f9yhVZO-J4PCeyeM9MIc40BY0tYoHGewTPU0eze0g,11011
|
|
28
31
|
coderouter/guards/memory_pressure.py,sha256=mul1KXO9oE1i424cs92Sk6uzoRrV6Seck2Lk3bu-w68,7903
|
|
29
32
|
coderouter/guards/tool_loop.py,sha256=QRUh0fM9LJx-KFd4751dRp2Tv_8ewjcXaW-TIeWsfJQ,12724
|
|
30
33
|
coderouter/ingress/__init__.py,sha256=WQsCH2CGJCAhy0mS6GSEdeYZRkkQu2OHDsP4CJWTLug,155
|
|
31
|
-
coderouter/ingress/anthropic_routes.py,sha256=
|
|
32
|
-
coderouter/ingress/app.py,sha256=
|
|
34
|
+
coderouter/ingress/anthropic_routes.py,sha256=pfk0rWWH5IJvdPF36h6egxYvX40RBrP5yTVfzOJArcw,15726
|
|
35
|
+
coderouter/ingress/app.py,sha256=E9Ze1hCWeZVqhh1DrDjrc1uJpjmjoHMMfv89tbReO6g,8031
|
|
33
36
|
coderouter/ingress/dashboard_routes.py,sha256=jVFc_4Q67YrP0VIppuQfBLnLvnvaCWJJ4z7aZsHmu1s,21822
|
|
34
37
|
coderouter/ingress/metrics_routes.py,sha256=M22dwOGn24P05Ge4W3c7d7mYytSGWjIR-pPSPOAiHJY,3965
|
|
35
38
|
coderouter/ingress/openai_routes.py,sha256=Zw1efPw9DI6GgV8ZcLrzS6Cda0KLrFkKn2GBZWSe6Vo,6322
|
|
36
39
|
coderouter/metrics/__init__.py,sha256=7Es351DPS7yLM0yVF_F0eesmiD83n7Zzhie44chht38,1465
|
|
37
|
-
coderouter/metrics/collector.py,sha256=
|
|
38
|
-
coderouter/metrics/prometheus.py,sha256=
|
|
40
|
+
coderouter/metrics/collector.py,sha256=JbFqXex9VzInhPIhHcPtuXeGR1tXsMD2r7o0uF2qLuw,43717
|
|
41
|
+
coderouter/metrics/prometheus.py,sha256=YRqyT931s40zVkIj07D-M2UNfDhIEElVFRz3izdJcnQ,24419
|
|
39
42
|
coderouter/routing/__init__.py,sha256=g2vhutbozRx5QBThReqwPN3imk5qXdpDiaogILd3IRc,257
|
|
40
|
-
coderouter/routing/adaptive.py,sha256=
|
|
43
|
+
coderouter/routing/adaptive.py,sha256=G2o377twGSjbUh65wiIFx6klnpFGjsD_nI3oDvcBwhY,21257
|
|
41
44
|
coderouter/routing/auto_router.py,sha256=4_sQR0ztSED9FgQSvQqgqSiydyQVY_qOSRvwyZ5BfRc,12909
|
|
42
45
|
coderouter/routing/budget.py,sha256=XZQL24YIkusB2MwfgbIJ1uhW1ODtMMdpbZ5e_A7oSU4,7164
|
|
43
46
|
coderouter/routing/capability.py,sha256=ziIDuE5keH_jxYDlXSKufRVxxSYOAvUxJ6Rw5QkYDDU,18436
|
|
44
|
-
coderouter/routing/fallback.py,sha256=
|
|
47
|
+
coderouter/routing/fallback.py,sha256=cJW_LYghVKVHOsHmJFaGgEEAkJ4ZsfhFtstOODOI4y8,85983
|
|
45
48
|
coderouter/translation/__init__.py,sha256=PYXN7XVEwpG1uC8RLy6fvnGbzEZhhrEuUapH8IYOtG8,1788
|
|
46
49
|
coderouter/translation/anthropic.py,sha256=JpvIWNXHUPVqOGvps7o_6ZADhXuJuvpU7RdMqQFtwwM,6421
|
|
47
50
|
coderouter/translation/convert.py,sha256=-qyzFzmmr9hhQV6_Sg75kJnvCZvHe3n7vRdaZtk_JqQ,47269
|
|
48
51
|
coderouter/translation/tool_repair.py,sha256=fyxDb4kWHytO5JWq5y0i4tinJUtWqhMCkyfoCf5BjeM,8314
|
|
49
|
-
coderouter_cli-2.
|
|
50
|
-
coderouter_cli-2.
|
|
51
|
-
coderouter_cli-2.
|
|
52
|
-
coderouter_cli-2.
|
|
53
|
-
coderouter_cli-2.
|
|
52
|
+
coderouter_cli-2.1.0.dist-info/METADATA,sha256=5Jyg_kLzgs6PAuzyDdqjIVbabeUOwqu2k8c5V0ZTLrc,48570
|
|
53
|
+
coderouter_cli-2.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
54
|
+
coderouter_cli-2.1.0.dist-info/entry_points.txt,sha256=-dnLfD1YZ2WjH2zSdNCvlO65wYltM9bsHt9Fhg3yGss,51
|
|
55
|
+
coderouter_cli-2.1.0.dist-info/licenses/LICENSE,sha256=wkEzoR86jFw33jvfOHjULqmkGEfxTFMgMaJnpR8mPRw,1065
|
|
56
|
+
coderouter_cli-2.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|