This commit is contained in:
Greg Fuener 2026-11-06 00:06:52 +00:00 • committed by GitHub
commit ffd56256a4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 150 additions and 20 deletions

View file

@ -679,12 +679,23 @@ Omit `reasoning_keywords` to use the built-in list, or supply a nonempty list to
Reasoning markers in the system prompt do **not** trigger the reasoning override. This prevents system prompts like "Think step by step before answering" from forcing all requests to the reasoning tier.
For requests identified by a `claude-cli/` or `claude-code/` user agent, the LLM classifier omits caller system
`classifier_caller_system_prompt` controls whether the LLM and JEV classifiers see the caller's system text.
The routed completion receives the original system text in every mode
With the default `auto`, requests identified by a `claude-cli/` or `claude-code/` user agent omit caller system
text to avoid classifying environment, agent, and skill catalogs. The current ask, configured prior-turn context,
and trajectory signal remain unchanged. The routed completion still receives the original system text. This
also excludes genuine task constraints supplied only in Claude Code system messages. Other clients keep the
existing system-context behavior. The browser routing preview has no client-identity field and retains that
generic behavior; use the real client when checking Claude Code routing.
and trajectory signal remain unchanged. This also excludes genuine task constraints supplied only in Claude Code
system messages. Other clients keep the existing system-context behavior. The browser routing preview has no
client-identity field and retains that generic behavior, so use the real client when checking Claude Code routing
Set `omit` when the router fronts other coding agents or harnesses whose system prompt is a per-session constant,
since quoting the same instructions on every request tells the classifier nothing about the request itself. Set
`include` to quote system text for every client, Claude Code included
```yaml
classifier_type: llm
classifier_caller_system_prompt: omit
```
### Harness Reminder Blocks

View file

@ -2666,16 +2666,23 @@ class ComplexityRouter(CustomLogger):
def _classifier_caller_constraints(
self, system_prompt: str | None, request_kwargs: Mapping[str, object] | None
) -> str | None:
"""Exclude Claude Code's environment and skill catalogs from task forecasts."""
return (
None
if any(
is_claude_code_user_agent(user_agent)
for metadata in (self._iter_metadata_dicts(request_kwargs) if request_kwargs is not None else ())
if isinstance(user_agent := metadata.get("user_agent"), str)
)
else system_prompt
)
match self.config.classifier_caller_system_prompt:
case "include":
return system_prompt
case "omit":
return None
case "auto":
return (
None
if any(
is_claude_code_user_agent(user_agent)
for metadata in (
self._iter_metadata_dicts(request_kwargs) if request_kwargs is not None else ()
)
if isinstance(user_agent := metadata.get("user_agent"), str)
)
else system_prompt
)
def _classifier_context_payload(
self,
@ -2733,7 +2740,7 @@ class ComplexityRouter(CustomLogger):
Args:
prompt: The current user ask text (already extracted as the real human ask, not tool results)
system_prompt: Caller task constraints, omitted from classification for Claude Code requests
system_prompt: Caller task constraints, filtered by classifier_caller_system_prompt
request_kwargs: Request metadata for spend attribution
messages: Full message history for extracting prior turns and the trajectory signal
"""

View file

@ -1316,8 +1316,8 @@ class ComplexityRouterConfig(LiteLLMBaseModel):
"classifier_context_include_assistant_turns is enabled. These turns are sent to the classifier "
"model (the configured TypeSafe endpoint for JEV), which may "
"be a different deployment or provider than the routed completion model; that call carries "
"the current user ask and, except for Claude Code requests, the extracted system-role text in full. "
"Claude Code system text is omitted to avoid classifying harness instructions; the routed "
"the current user ask and the extracted system-role text in full, unless "
"classifier_caller_system_prompt omits it; the routed "
"completion still receives it. Set to 0 to omit prior turns and the conversation-depth summary; "
"the current ask and selected system text are still sent. Applies to LLM and JEV classification."
),
@ -1330,8 +1330,8 @@ class ComplexityRouterConfig(LiteLLMBaseModel):
"context window, per classification call. Turns are taken newest first and quoted whole "
"while they fit, so a conversation small enough to quote entirely is never cut; once the "
"budget runs out the older turns are dropped whole and only the turn straddling the "
"boundary is truncated, into whatever space is left. The current ask and, except for Claude "
"Code requests, the extracted system-role text sit outside this budget and are sent in full, as does "
"boundary is truncated, into whatever space is left. The current ask and the extracted "
"system-role text, when classifier_caller_system_prompt sends it, sit outside this budget and are sent in full, as does "
"the numbering each quoted turn carries. A budget under 120 leaves no room to quote a turn and "
"suppresses the block; set classifier_context_window_size to 0 to turn context off "
"deliberately. Applies to LLM and JEV classification."
@ -1363,6 +1363,18 @@ class ComplexityRouterConfig(LiteLLMBaseModel):
"spend, for an already-deployed router. Applies to LLM and JEV classification."
),
)
classifier_caller_system_prompt: Literal["auto", "include", "omit"] = Field(
default="auto",
description=(
"Whether the caller's system prompt is quoted to the LLM or JEV classifier as task context. "
"'auto' omits it for Claude Code requests, whose system text is harness instructions and "
"tool catalogs rather than the task, and includes it for every other client. 'omit' never "
"sends it, which suits any coding agent or harness whose system prompt is a per-session "
"constant that says nothing about the request being routed. 'include' always sends it, "
"Claude Code included. The routed completion receives the original system prompt in every "
"mode. Applies to LLM and JEV classification."
),
)
adaptive: bool = Field(
default=False,

View file

@ -271,6 +271,63 @@ async def test_jev_accounts_once_with_parent_identity_even_when_the_verdict_fail
assert (standard["prompt_tokens"], standard["completion_tokens"], standard["total_tokens"]) == (3, 2, 5)
@pytest.mark.asyncio
@pytest.mark.parametrize("classifier", [{"provider": "jev"}, {"provider": "laya", "model": "english"}])
@pytest.mark.parametrize(
("mode", "user_agent", "forwards_system"),
[
("auto", "opencode/1.18.20", True),
("auto", "claude-cli/2.1.233", False),
("omit", "opencode/1.18.20", False),
("include", "claude-cli/2.1.233", True),
],
)
async def test_oss_classifier_honors_classifier_caller_system_prompt(
classifier: dict[str, str], mode: str, user_agent: str, forwards_system: bool
) -> None:
captured: list[Mapping[str, object]] = []
def respond(request: httpx.Request) -> httpx.Response:
captured.append(json.loads(request.content))
return httpx.Response(200, json={"answers": {"tier": _answer().model_dump()}})
handler: Final = AsyncHTTPHandler()
handler.client = httpx.AsyncClient(transport=httpx.MockTransport(respond))
router: Final = ComplexityRouter(
"oss-system-prompt",
RoutingContext(litellm.Router(model_list=[])),
{
"classifier_type": "oss_classifier",
"opensource_classifier_config": classifier,
"classifier_caller_system_prompt": mode,
"tiers": {"SIMPLE": "cheap"},
"session_affinity": False,
"deployment_affinity": False,
},
jev_client=HttpJevClassifierClient("test", "https://typesafe.test", handler),
derive_savings_baseline=False,
)
messages: Final = [
{"role": "system", "content": "HARNESS_SYSTEM_PROMPT"},
{"role": "user", "content": "ok, go ahead"},
]
result: Final = await router.async_pre_routing_hook(
model="oss-system-prompt",
messages=messages,
request_kwargs={"metadata": {"user_agent": user_agent}},
)
await GLOBAL_LOGGING_WORKER.flush()
await handler.client.aclose()
assert result is not None and result.routing_decision is not None
assert result.routing_decision["cause"] == "jev_classifier"
assert len(captured) == 1
state: Final = str(captured[0]["state"])
assert "ok, go ahead" in state
assert ("HARNESS_SYSTEM_PROMPT" in state) is forwards_system
@pytest.mark.asyncio
@pytest.mark.parametrize("include_assistant", [False, True])
async def test_jev_uses_bounded_history_and_separates_operator_instructions(include_assistant: bool) -> None:

View file

@ -10052,6 +10052,49 @@ class TestContextAwareClassifier:
assert "TOP_LEVEL_HARNESS_SYSTEM" not in payload
assert "Conversation so far: ~35 tokens across the request" in payload
@pytest.mark.asyncio
@pytest.mark.parametrize(
"mode,user_agent,forwards_system",
[
("auto", "claude-cli/2.1.233", False),
("auto", "codex_cli_rs/0.60.1", True),
("omit", "codex_cli_rs/0.60.1", False),
("omit", None, False),
("include", "claude-cli/2.1.233", True),
("include", None, True),
],
)
async def test_classifier_caller_system_prompt_mode(
self,
llm_classifier_config: dict[str, object],
mode: str,
user_agent: str | None,
forwards_system: bool,
) -> None:
dependency: Final = MagicMock(acompletion=AsyncMock(return_value=_llm_response('{"tier": "COMPLEX"}')))
router: Final = ComplexityRouter(
"test-complexity-router",
RoutingContext(dependency),
{**llm_classifier_config, "classifier_caller_system_prompt": mode},
)
messages: Final = [
{"role": "system", "content": "HARNESS_SYSTEM_PROMPT"},
{"role": "user", "content": "Now prove it cannot livelock"},
]
request_kwargs: Final = {"metadata": {} if user_agent is None else {"user_agent": user_agent}}
result: Final = await router.async_pre_routing_hook("test-complexity-router", request_kwargs, messages)
assert result is not None and result.routing_decision["cause"] == "llm_classifier"
assert result.messages == messages
payload: Final = dependency.acompletion.call_args.kwargs["messages"][1]["content"]
assert payload.endswith("Classify this message:\nNow prove it cannot livelock")
assert ("HARNESS_SYSTEM_PROMPT" in payload) is forwards_system
def test_classifier_caller_system_prompt_rejects_unknown_mode(self, llm_classifier_config: dict[str, object]):
with pytest.raises(ValidationError):
ComplexityRouterConfig(**{**llm_classifier_config, "classifier_caller_system_prompt": "never"})
@pytest.mark.asyncio
async def test_claude_code_first_turn_without_context_omits_harness_system_prompt(
self, llm_classifier_config: dict[str, object]