스테이지 라우터는 신호가 떨어진 자리에서만 판정 모델에게
묻습니다. type = "llm_classifier"는 대신 턴 전체를 LLM 판정 모델에게 넘깁니다. 항목에는
판정 모델과 그것이 고를 수 있는 목적지, 그리고 세 가지 판정 형태 중 하나를 정하는 mode를
적습니다.
mode |
판정 모델이 돌려주는 것 | 턴이 가는 곳 |
|---|---|---|
capability |
작업의 해결 확률 | base_threshold 이상이면 weak_target, 아니면 strong_target |
custom |
직접 쓴 스키마에 맞는 JSON. JSON Pointer가 거기서 모델 그룹을 집어냅니다 | 그 그룹의 첫 모델 |
escalation |
완성된 약한 턴에 대한 판단 | 에스컬레이션 참고 |
이 페이지는 턴이 실행되기 전에 결정하고 턴을 붙잡아 두지 않는 capability와 custom을
다룹니다. escalation은 이미 존재하는 턴을 판정하므로
별도 페이지가 있습니다.
mode는 필수입니다. 업스트림 스키마는 capability를 기본값으로 두지만 여기서는
그렇지 않습니다. 세 모드는 서로 다른 원리로 라우팅하고, 그중 하나(escalation)는 제공할
턴을 버퍼링하므로, mode를 생략한 설정이 조용히 어느 한 모드로 읽혀서는 안 됩니다.
mode = "capability"
패키지 판정 모델이 작업의 해결 확률을 돌려줍니다. 그 값이 base_threshold 이상이면 턴은
weak_target에, 미만이면 strong_target에 갑니다.
[[models]]
id = "claude-judged"
[models.router]
type = "llm_classifier"
mode = "capability"
classifier_target = "claude-haiku-4-5"
strong_target = "claude-opus-4-8"
weak_target = "claude-sonnet-4-6"
base_threshold = 0.5
# threshold_step = 0.0
# classify_trigger = "every_request"threshold_step은 판정 모델이 덜 확신하는 판정의 기준을 높입니다. 불확실하거나 매칭되지
않은 판정에는 한 번, 지원되지 않는 판정에는 두 번 더해집니다. prompt는 패키지 capability
프롬프트를 대체합니다. 스키마는 구조화 출력 설정으로 따로 보내므로 프롬프트에
{{RESPONSE_SCHEMA}}가 들어가면 안 됩니다.
mode = "custom"
프롬프트와 JSON 스키마를 직접 주고, JSON Pointer가 판정에서 모델 그룹 이름을
집어냅니다. 그 그룹의 첫 모델이 턴을 처리합니다. any와 judge는 예약된 필수 그룹이고,
나머지 이름은 전부 여러분의 것입니다. 한 항목이 둘보다 많은 모델 가운데서 고를 수 있는
이유가 이것입니다.
[models.router]
type = "llm_classifier"
mode = "custom"
models = { judge = ["claude-haiku-4-5"], capable = ["claude-opus-4-8"], efficient = ["claude-sonnet-4-6"], any = ["claude-sonnet-4-6", "claude-opus-4-8"] }
default_target = "efficient"
prompt = "이 턴의 타깃을 정확히 하나 고르세요. 응답 스키마에 맞는 JSON만 반환하세요."
response_schema = '''
{"type": "object",
"properties": {"target": {"type": "string", "enum": ["capable", "efficient"]}},
"required": ["target"],
"additionalProperties": false}
'''
policy = { type = "target_selector", selector = "/target" }models.any는 고를 수 있는 모든 목적지를 나열하며, 다른 답변 그룹의 타깃도 전부 여기에
있어야 합니다. models.judge는 순서 있는 판정 후보를 하나 이상 담으며, 물어보기만 하고
제공하지 않습니다. default_target은 판정 모델이 쓸 만한 판정을 돌려주지 않았을 때 쓰는
그룹입니다.
판정 모델이 도는 시점
classify_trigger는 두 모드 모두에서 판정 호출을 얼마나 자주 치를지 정합니다.
classify_trigger |
판정 모델이 도는 때 |
|---|---|
every_request(기본값) |
도구 연속 턴까지 포함해 모든 턴 |
user_turn |
새 사람 턴마다. 그 사이의 도구 호출 동안 그 타깃을 유지합니다 |
new_session |
한 번. 세션 내내 그 타깃을 다시 씁니다 |
message_hash_fallback = true는 세션 id를 보내지 않는 클라이언트를 위해 대신 첫 사용자
메시지로 유지 키를 잡으며, classify_trigger = "new_session"이 필요합니다.
recent_turn_window는 판정 모델이 추가로 보는 최근 턴을 더하고, max_output_tokens는
판정 응답에 상한을 겁니다.
세션은 알고리즘 안에 있습니다. classify_trigger의 유지 상태는 업스트림의 것이고
라우터 인스턴스 안에 들어 있으며, shunt는 그 인스턴스를 설정을 읽을 때마다 한 번 만듭니다.
핫 리로드는 그것을 다시 만들기 때문에, 리로드하면 각 세션이 들고 있던 타깃을 잊습니다 —
prefill_router와 같은 성질입니다.
max_judge_calls는 shunt 자신의 것이며 (세션, 에이전트)마다 셉니다. 그래서 위임된
자식은 부모가 아니라 자기 예산을 씁니다. 세션 id가 없는 요청은 아예 추적하지 않으므로,
그런 호출자에게는 이 한도가 요청 단위로 걸립니다. 예산은 판정 호출을 보낼 때 차감하므로,
판정이 필요 없는 턴 — 보존된 배정을 재생하는 new_session이나 user_turn 턴 — 은 예산을
다 쓴 뒤에도 그 배정대로 처리됩니다. 판정 호출을 거부당한 턴은 판정이 실패한 턴처럼
닫힙니다. 아래의 기본 타깃으로 가고 route source는 classifier_fail_open이며, 판정 호출
결과는 budget_exhausted로 기록됩니다. classifier 형태의
[models.subagents] 오버레이도 똑같이 동작합니다.
판정이 오지 않으면
판정 호출이 어떤 식으로든 실패하면 — 타임아웃, 크기 초과, 업스트림 오류, 400, 해석
불가 — 판정이 없는 것으로 보고 턴은 알고리즘 자신의 기본값으로 갑니다. capability 모드는
strong_target, custom 모드는 default_target 그룹의 첫 모델입니다. 판정하는 턴 하나에
판정 호출은 정확히 한 번, 첫 판정 후보에게만 갑니다. 그래서 실패해도 models.judge를
따라 내려가며 재시도하지 않습니다. 턴은 그대로 응답되고, 라우트 소스는
classifier_fail_open이며, 클라이언트는 여전히 200을 받습니다.
프로브는 판정 없이 해석됩니다
count_tokens 요청은 판정 모델을 부르지 않고, max_judge_calls 예산을 쓰지 않으며, 세션
상태도 바꾸지 않습니다. new_session이나 user_turn에서는 세션의 마지막 턴이 처리된
타깃으로 응답합니다 — user_turn에서는 프로브의 마지막 메시지가 새 사용자 턴이어도
그렇습니다. 그 밖의 경우에는 fail-open 타깃으로 응답합니다. every_request일 때, 아직
분류되지 않은 세션일 때, x-claude-code-session-id가 없는 요청이나 에이전트 id가 없는 위임
요청일 때가 그렇습니다(message_hash_fallback은 프로브에 적용되지 않습니다). 요청 본문이
없는 표면 — GET /routes, /v1/models 디스커버리, shunt check — 은 fail-open 타깃을
라우트 소스 classifier_default로 보고합니다.
클라이언트가 보는 것
이 절은 capability와 custom 모드를 다룹니다. mode = "escalation"은 자기 소스를
따로 보고하며, Escalation 페이지에 나와 있습니다.
라우팅된 턴은 x-gateway-routed-model(타깃)과 x-gateway-route-source를 실어 보냅니다.
뒤의 헤더는 턴이 어떻게 그 타깃에 이르렀는지 알려 줍니다. count_tokens 프로브가 아닌
승인된 요청이라면 같은 라벨이 shunt.router.decisions의 source가 되므로,
classifier_default는 그 지표에 나타나지 않습니다.
| 소스 | 의미 |
|---|---|
llm-classifier |
이 턴에 판정 모델이 답했고, 그 판정이 타깃을 골랐습니다 |
classifier_retained |
user_turn이나 new_session에서 앞선 턴이 이미 얻은 타깃을 재생했으므로 판정 호출이 없었습니다 |
classifier_fail_open |
판정이 턴을 결정하지 못했습니다. 판정 모델이 실패했거나, 답을 쓸 수 없었거나, max_judge_calls가 호출을 거부해 기본 타깃이 응답했습니다 |
classifier_default |
요청 본문이 없는 표면이 fail-open 타깃을 보고했습니다 |
타깃과 판정 모델
타깃과 판정 모델은 모두 스테이지 라우터와 같은 한 홉 규칙을 지키는 평범한 공개 model id이고, 판정 모델은 passthrough 라우트로 해석되면 안 됩니다. 판정 호출은 호출자의 자격 증명을 하나도 싣지 않으므로 passthrough 라우트에는 돌릴 것이 남지 않습니다. 자격 증명, 입장 심사, 호출당 한도처럼 모든 판정 모델이 따르는 일반 규칙은 판정 호출에 있습니다.
모든 키와 기본값, 제약은 설정 레퍼런스에 있습니다.