Skip to content

Commit aa99b21

Browse files
committed
Release 0.7.19 remove session sticky routing
1 parent f471533 commit aa99b21

4 files changed

Lines changed: 144 additions & 73 deletions

File tree

openclaw-plugin/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@anjieyang/uncommon-route",
3-
"version": "0.7.18",
3+
"version": "0.7.19",
44
"description": "OpenClaw plugin for UncommonRoute, the local LLM router that cuts premium-model spend",
55
"type": "module",
66
"main": "src/index.js",

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uncommon-route"
3-
version = "0.7.18"
3+
version = "0.7.19"
44
description = "Local LLM router that cuts premium-model spend with adaptive 3-tier routing, OpenAI + Anthropic compatible"
55
requires-python = ">=3.11"
66
license = "MIT"

tests/test_proxy.py

Lines changed: 140 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -973,7 +973,7 @@ def handler(request: httpx.Request) -> httpx.Response:
973973

974974

975975
class TestRoutingContinuity:
976-
def test_agent_tool_session_locks_route_pool_to_previous_model(
976+
def test_agent_tool_session_keeps_full_route_pool(
977977
self,
978978
monkeypatch: pytest.MonkeyPatch,
979979
) -> None:
@@ -993,7 +993,7 @@ def fake_route(*_args, **kwargs) -> RoutingDecision:
993993
mode=RoutingMode.AUTO,
994994
confidence=0.9,
995995
method="pool",
996-
reasoning="sticky session route",
996+
reasoning="session route",
997997
cost_estimate=0.001,
998998
baseline_cost=0.002,
999999
savings=0.5,
@@ -1064,11 +1064,147 @@ def handler(request: httpx.Request) -> httpx.Response:
10641064
)
10651065

10661066
assert resp.status_code == 200
1067-
assert routed["available_models"] == ["anthropic/claude-opus-4-5"]
1067+
assert routed["available_models"] == [
1068+
"anthropic/claude-opus-4-5",
1069+
"anthropic/claude-sonnet-4-6",
1070+
]
10681071
assert captured["body"]["model"] == "anthropic/claude-opus-4-5"
10691072
trace = traces.find(resp.headers["x-uncommon-route-request-id"])
10701073
assert trace is not None
1071-
assert "session-sticky=previous-model=anthropic/claude-opus-4-5" in trace["route_reasoning"]
1074+
assert "session-sticky" not in trace["route_reasoning"]
1075+
assert "previous-model" not in trace["route_reasoning"]
1076+
finally:
1077+
asyncio.run(async_client.aclose())
1078+
1079+
def test_semantic_tool_failure_keeps_full_route_pool_and_records_failure(
1080+
self,
1081+
monkeypatch: pytest.MonkeyPatch,
1082+
) -> None:
1083+
routed: dict[str, object] = {}
1084+
captured: dict[str, object] = {}
1085+
1086+
def fake_route(*_args, **kwargs) -> RoutingDecision:
1087+
available_models = list(kwargs.get("available_models") or [])
1088+
routed["available_models"] = available_models
1089+
selected_model = available_models[0]
1090+
return RoutingDecision(
1091+
model=selected_model,
1092+
tier=Tier.COMPLEX,
1093+
capability_lane=CapabilityLane.GENERAL,
1094+
served_quality=ServedQuality.PREMIUM,
1095+
served_quality_target=ServedQuality.PREMIUM,
1096+
served_quality_floor=ServedQuality.BALANCED,
1097+
continuity_quality_floor=kwargs["routing_features"].continuity_quality_floor,
1098+
mode=RoutingMode.AUTO,
1099+
confidence=0.8,
1100+
method="pool",
1101+
reasoning="semantic failure route",
1102+
cost_estimate=0.001,
1103+
baseline_cost=0.002,
1104+
savings=0.5,
1105+
routing_features=kwargs["routing_features"],
1106+
)
1107+
1108+
def handler(request: httpx.Request) -> httpx.Response:
1109+
captured["body"] = json.loads(request.content.decode("utf-8"))
1110+
return httpx.Response(
1111+
200,
1112+
json={
1113+
"id": "chatcmpl_semantic_retry",
1114+
"object": "chat.completion",
1115+
"created": 1,
1116+
"model": captured["body"].get("model", "google/gemini-2.5-pro"),
1117+
"choices": [{
1118+
"index": 0,
1119+
"message": {"role": "assistant", "content": "retrying"},
1120+
"finish_reason": "stop",
1121+
}],
1122+
"usage": {"prompt_tokens": 1, "completion_tokens": 1, "total_tokens": 2},
1123+
},
1124+
headers={"content-type": "application/json"},
1125+
)
1126+
1127+
traces = TraceStore(storage=InMemoryTraceStorage(), now_fn=lambda: 1.0)
1128+
traces.record(RequestTrace(
1129+
timestamp=1.0,
1130+
request_id="prev_req",
1131+
requested_model="uncommon-route/auto",
1132+
model="google/gemini-2.5-pro",
1133+
status_code=200,
1134+
api_format="openai",
1135+
endpoint="chat_completions",
1136+
is_virtual=True,
1137+
session_id="agent-session",
1138+
step_type="tool-result-followup",
1139+
transport="openai-chat",
1140+
served_quality="premium",
1141+
))
1142+
1143+
async_client = httpx.AsyncClient(transport=httpx.MockTransport(handler))
1144+
monkeypatch.setattr("uncommon_route.proxy._get_client", lambda: async_client)
1145+
monkeypatch.setattr("uncommon_route.proxy.route", fake_route)
1146+
1147+
try:
1148+
app = create_app(
1149+
upstream="https://api.example.test/v1",
1150+
model_mapper=_build_test_mapper(
1151+
"google/gemini-2.5-pro",
1152+
"anthropic/claude-opus-4-6",
1153+
"openai/gpt-5.4-2026-03-05",
1154+
),
1155+
trace_store=traces,
1156+
spend_control=SpendControl(storage=InMemorySpendControlStorage()),
1157+
)
1158+
client = TestClient(app, raise_server_exceptions=False)
1159+
resp = client.post(
1160+
"/v1/chat/completions",
1161+
json={
1162+
"model": "uncommon-route/auto",
1163+
"messages": [
1164+
{"role": "user", "content": "Run the tests and fix the bug."},
1165+
{
1166+
"role": "assistant",
1167+
"tool_calls": [{
1168+
"id": "call_1",
1169+
"type": "function",
1170+
"function": {
1171+
"name": "run_tests",
1172+
"arguments": json.dumps({"command": "pytest"}),
1173+
},
1174+
}],
1175+
},
1176+
{
1177+
"role": "tool",
1178+
"tool_call_id": "call_1",
1179+
"content": (
1180+
"pytest verification failed: 1 failed\n"
1181+
"AssertionError: expected 2 actual 3\n"
1182+
"<returncode>1</returncode>"
1183+
),
1184+
},
1185+
],
1186+
"tools": [{
1187+
"type": "function",
1188+
"function": {"name": "run_tests", "parameters": {"type": "object"}},
1189+
}],
1190+
},
1191+
headers={"x-session-id": "agent-session"},
1192+
)
1193+
1194+
assert resp.status_code == 200
1195+
assert set(routed["available_models"]) == {
1196+
"google/gemini-2.5-pro",
1197+
"anthropic/claude-opus-4-6",
1198+
"openai/gpt-5.4-2026-03-05",
1199+
}
1200+
assert "google/gemini-2.5-pro" in routed["available_models"]
1201+
assert captured["body"]["model"] == routed["available_models"][0]
1202+
trace = traces.find(resp.headers["x-uncommon-route-request-id"])
1203+
assert trace is not None
1204+
assert "session-sticky" not in trace["route_reasoning"]
1205+
assert "session-retry" not in trace["route_reasoning"]
1206+
assert trace["routing_features_payload"]["verification_failed"] is True
1207+
assert trace["routing_features_payload"]["failure_kind"] == "semantic"
10721208
finally:
10731209
asyncio.run(async_client.aclose())
10741210

uncommon_route/proxy.py

Lines changed: 2 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -2269,61 +2269,6 @@ def _merge_available_models(
22692269
return merged
22702270

22712271

2272-
def _latest_successful_session_model(
2273-
*,
2274-
session_id: str | None,
2275-
trace_store: TraceStore,
2276-
step_types: tuple[str, ...] = ("tool-selection", "tool-result-followup", "general"),
2277-
) -> str | None:
2278-
previous_trace = trace_store.latest_for_session(
2279-
session_id,
2280-
step_types=step_types,
2281-
) if session_id else None
2282-
if (
2283-
previous_trace is None
2284-
or previous_trace.status_code >= 400
2285-
):
2286-
return None
2287-
previous_model = str(previous_trace.model or "").strip()
2288-
if not previous_model or _is_virtual_model_name(previous_model):
2289-
return None
2290-
return previous_model
2291-
2292-
2293-
def _should_use_session_sticky_model(features: RoutingFeatures) -> bool:
2294-
if not features.session_present:
2295-
return False
2296-
if features.step_type in {"tool-selection", "tool-result-followup"}:
2297-
return True
2298-
if features.has_tool_results:
2299-
return True
2300-
if features.agent_step_count >= 2:
2301-
return True
2302-
return bool(features.is_agentic and (features.needs_tool_calling or features.is_coding))
2303-
2304-
2305-
def _filter_session_sticky_context(
2306-
*,
2307-
available_models: list[str],
2308-
session_id: str | None,
2309-
routing_features: RoutingFeatures,
2310-
trace_store: TraceStore,
2311-
) -> tuple[list[str], str]:
2312-
if len(available_models) <= 1:
2313-
return list(available_models), ""
2314-
if not _should_use_session_sticky_model(routing_features):
2315-
return list(available_models), ""
2316-
previous_model = _latest_successful_session_model(
2317-
session_id=session_id,
2318-
trace_store=trace_store,
2319-
)
2320-
if not previous_model:
2321-
return list(available_models), ""
2322-
if previous_model in available_models:
2323-
return [previous_model], f"session-sticky=previous-model={previous_model}"
2324-
return list(available_models), f"session-sticky=previous-unavailable={previous_model}"
2325-
2326-
23272272
def _reuse_anthropic_source_body(
23282273
*,
23292274
source_body: dict[str, Any],
@@ -2481,6 +2426,8 @@ def _serialize_routing_features(features: RoutingFeatures) -> dict[str, object]:
24812426
"capability_lane": features.capability_lane.value if features.capability_lane is not None else None,
24822427
"previous_served_quality": features.previous_served_quality.value if features.previous_served_quality is not None else None,
24832428
"continuity_quality_floor": features.continuity_quality_floor.value if features.continuity_quality_floor is not None else None,
2429+
"verification_failed": features.verification_failed,
2430+
"failure_kind": features.failure_kind,
24842431
"tags": list(features.tags()),
24852432
}
24862433

@@ -4118,7 +4065,6 @@ async def _handle_chat_core(
41184065
hints = routing_features.workload_hints()
41194066
step_type = routing_features.step_type
41204067
user_keyed = _providers.keyed_models() or None
4121-
route_pool_notes: list[str] = []
41224068
try:
41234069
base_available_models = _mapper.routable_models if _mapper.discovered else list(DEFAULT_MODEL_PRICING.keys())
41244070
if user_keyed:
@@ -4128,14 +4074,6 @@ async def _handle_chat_core(
41284074
scene_pool = _active_scene.model_pool()
41294075
available_scene_models = [m for m in scene_pool if m in route_available_models]
41304076
route_available_models = available_scene_models or scene_pool
4131-
route_available_models, session_sticky_note = _filter_session_sticky_context(
4132-
available_models=route_available_models,
4133-
session_id=session_id,
4134-
routing_features=routing_features,
4135-
trace_store=_traces,
4136-
)
4137-
if session_sticky_note:
4138-
route_pool_notes.append(session_sticky_note)
41394077

41404078
if _active_scene and _active_scene.hard_pin:
41414079
from uncommon_route.router.types import (
@@ -4351,9 +4289,6 @@ async def _handle_chat_core(
43514289
)
43524290
reasoning = decision.reasoning
43534291
route_reasoning = decision.reasoning
4354-
if route_pool_notes:
4355-
route_reasoning = f"{route_reasoning} | {' | '.join(route_pool_notes)}"
4356-
reasoning = route_reasoning
43574292
estimated_cost = decision.cost_estimate
43584293
baseline_cost = decision.baseline_cost
43594294
confidence = decision.confidence

0 commit comments

Comments
 (0)