mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-14 09:03:16 +00:00
Merge a50fc3c7a0 into 3f13c1ca34
This commit is contained in:
commit
ffd56256a4
5 changed files with 150 additions and 20 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue