[sgl-router] Rename chat encoder to chat formatter (#39459)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Kan Wu
2026-09-16 08:46:11 -07:00
committed by GitHub
co-authored by Claude Fable 5.1
parent 5aaa18207c
commit ad94978adf
8 changed files with 117 additions and 113 deletions
@@ -6,7 +6,7 @@
//! skips re-tokenizing the same prompt). Asserts the gating contract through
//! the real chat handler + a MockWorker backend:
//!
//! * A plain text chat request on the engine-equivalent chat-encoder path →
//! * A plain text chat request on the engine-equivalent chat-formatter path →
//! the forwarded body carries `input_ids` AND retains `messages`.
//! * A request carrying `tools` → `input_ids` omitted (the router's encoder
//! doesn't render tool schemas, so its ids would diverge from the engine).
@@ -35,8 +35,8 @@ fn build_ctx(url: String) -> Arc<AppContext> {
let cfg = config();
let tokenizers = Arc::new(TokenizerRegistry::load_from_config(&cfg).unwrap());
assert!(
tokenizers.has_chat_encoder(MODEL),
"deepseek-v4 model id must auto-attach the built-in chat encoder"
tokenizers.has_chat_formatter(MODEL),
"deepseek-v4 model id must auto-attach the built-in chat formatter"
);
let registry = Arc::new(WorkerRegistry::default());
let _ = registry.add(WorkerSpec {
@@ -4,7 +4,7 @@
//! Shared router config for the cache-aware proxy tests.
//!
//! The model id contains `deepseek-v4` so the tokenizer registry auto-attaches the
//! built-in V4 chat encoder — the engine-equivalent path — with no template fixture.
//! built-in V4 chat formatter — the engine-equivalent path — with no template fixture.
use sgl_router::config::{
ActiveLoadConfig, CacheAwareConfig, Config, DiscoveryBackend, ModelConfig, ObservabilityConfig,
@@ -2,10 +2,10 @@
// SPDX-License-Identifier: Apache-2.0
//! `input_ids` forwarding is policy-independent: a load-only **round-robin**
//! policy on a chat-encoder model still forwards `input_ids` to the engine
//! policy on a chat-formatter model still forwards `input_ids` to the engine
//! (the engine-tokenization offload), even though it picks workers round-robin
//! and ignores the tokens for routing. Tokenization is gated on the model's
//! chat encoder at ingress, not on the policy.
//! chat formatter at ingress, not on the policy.
use axum::body::Body;
use axum::http::{Request, StatusCode};
@@ -66,7 +66,7 @@ fn build_ctx(url: String) -> Arc<AppContext> {
// The handler tokenizes via the AppContext's registry (which carries the V4
// encoder); the RoundRobin policy itself needs no tokenizer.
let tokenizers = Arc::new(TokenizerRegistry::load_from_config(&cfg).unwrap());
assert!(tokenizers.has_chat_encoder(MODEL));
assert!(tokenizers.has_chat_formatter(MODEL));
let registry = Arc::new(WorkerRegistry::default());
let _ = registry.add(WorkerSpec {
id: WorkerId(url.clone()),
@@ -103,7 +103,7 @@ fn captured(mock: &MockWorker) -> Value {
}
/// A round-robin (load-only) policy still forwards `input_ids` on a
/// chat-encoder model — the offload is decoupled from routing.
/// chat-formatter model — the offload is decoupled from routing.
#[tokio::test]
async fn round_robin_plain_chat_forwards_input_ids() {
let mock = MockWorker::start(vec![]).await;
@@ -122,7 +122,7 @@ async fn round_robin_plain_chat_forwards_input_ids() {
let ids = body.get("input_ids").and_then(|v| v.as_array());
assert!(
ids.is_some_and(|a| !a.is_empty()),
"round-robin must forward input_ids on a chat-encoder model; got {body}"
"round-robin must forward input_ids on a chat-formatter model; got {body}"
);
assert!(
body.get("messages").is_some(),
@@ -154,7 +154,7 @@ async fn round_robin_tool_request_omits_input_ids() {
);
}
/// A successful plain-chat forward on a chat-encoder model must NOT emit
/// A successful plain-chat forward on a chat-formatter model must NOT emit
/// `sgl_router_ingress_tokenize_errors_total` — that counter fires only when the
/// offload was expected but the encoder failed. A tool request on the same model
/// is an *expected* omission (its ids are still engine-equivalent; the
@@ -3,8 +3,8 @@
//! Tokenize-once at ingress under the STICKY policy. The engine-tokenization
//! offload (`input_ids` forwarding) is a property of the MODEL — does it have a
//! chat encoder? — not of the routing policy, so a sticky-routed request on a
//! chat-encoder model must forward `input_ids` exactly like cache-aware does,
//! chat formatter? — not of the routing policy, so a sticky-routed request on a
//! chat-formatter model must forward `input_ids` exactly like cache-aware does,
//! while still pinning sessions O(1) by header.
//!
//! Asserts through the real chat handler + `MockWorker` backends:
@@ -17,7 +17,7 @@
//! routing is unchanged by the added tokenization).
//!
//! The model id contains `deepseek-v4` so the tokenizer registry auto-attaches
//! the built-in V4 chat encoder — the engine-equivalent path — without a
//! the built-in V4 chat formatter — the engine-equivalent path — without a
//! template fixture.
use axum::body::Body;
@@ -82,14 +82,14 @@ fn config() -> Config {
/// Build an `AppContext` running the sticky policy over the given workers.
/// The tokenizer registry is loaded from config (real tiny tokenizer + the
/// auto-attached V4 chat encoder) so the ingress can tokenize — the sticky
/// auto-attached V4 chat formatter) so the ingress can tokenize — the sticky
/// policy itself holds no tokenizer.
fn build_ctx(worker_urls: &[String]) -> Arc<AppContext> {
let cfg = config();
let tokenizers = Arc::new(TokenizerRegistry::load_from_config(&cfg).unwrap());
assert!(
tokenizers.has_chat_encoder(MODEL),
"deepseek-v4 model id must auto-attach the built-in chat encoder"
tokenizers.has_chat_formatter(MODEL),
"deepseek-v4 model id must auto-attach the built-in chat formatter"
);
let registry = Arc::new(WorkerRegistry::default());
for (i, url) in worker_urls.iter().enumerate() {