mod config;
use super::{EXTENSION_FACTORIES, ExtensionFactory, PRIORITY_JET, command_arg, util::tighter_u32};
use crate::{
core::{
extensions::{
Extension, ExtensionCommand, ExtensionMode, ExtensionTool, ForkProjectInfo, ModelRoute,
ModelRouteReason, ModelRouteRequest, RouteTarget, SubcommandDef,
VirtualModelDefinition, VirtualModelRouter,
},
model_resolver::{self, ModelEntry},
provider::{
AgentMessage, ClassifierAnswer, ClassifierContext, ClassifierQuestion, ModelType,
classify,
},
settings_manager, virtual_models,
},
error::Result,
extensions::util::truncate_chars_with_postfix,
modes::interactive::{
agent_actor::AgentCommand,
app::{App, MsgLevel},
handlers::register_slash_command,
},
};
use config::{ClassifierConfig, JetConfig, RouterConfig, Target};
use futures_util::future::BoxFuture;
use serde_json::{Value, json};
use std::{collections::BTreeMap, sync::Arc};
use strum_macros::{EnumString, IntoStaticStr};
const EXT: &str = "jet";
const CMD: &str = "jet";
const VIRTUAL_PROVIDER: &str = "jet";
const VIRTUAL_ID: &str = "auto";
const VIRTUAL_NAME: &str = "Auto (Jet)";
const COMPLEXITY_ID: &str = "complexity";
const PROMPT_FIELD: &str = "prompt";
const PROMPT_MAX_CHARS: usize = 16_000;
const COMPLEX_THRESHOLD: f64 = 0.5;
const EDIT_TOOLS: &[&str] = &["edit", "write"];
const SUBCOMMANDS: &[SubcommandDef] = &[
SubcommandDef {
name: "set",
description: "Set the classifier model: /jet set <provider> <model-id>",
},
SubcommandDef {
name: "router",
description: "Set routing targets: /jet router [--provider <p>] [--planning <id>] \
[--planning-complex <id>] [--implementation <id>] \
[--planning-provider <p>] [--planning-complex-provider <p>] \
[--implementation-provider <p>]",
},
SubcommandDef {
name: "reset",
description: "Clear the jet configuration (unregisters jet/auto)",
},
];
const SET_USAGE: &str =
"Usage: /jet set <provider> <model-id> (e.g. /jet set openrouter ~typesafe/jev-latest)";
const ROUTER_USAGE: &str = "Usage: /jet router [--provider <p>] [--planning <id>] \
[--planning-complex <id>] [--implementation <id>] \
[--planning-provider <p>] [--planning-complex-provider <p>] \
[--implementation-provider <p>]";
#[linkme::distributed_slice(EXTENSION_FACTORIES)]
static JET_FACTORY: ExtensionFactory = ExtensionFactory {
priority: PRIORITY_JET,
make: || -> Arc<dyn Extension> { Arc::new(Jet) },
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, EnumString, IntoStaticStr)]
#[strum(serialize_all = "lowercase")]
enum Phase {
Planning,
Implementation,
}
impl Phase {
fn as_str(self) -> &'static str {
self.into()
}
fn parse(s: &str) -> Option<Self> {
s.parse().ok()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct JetState {
phase: Phase,
model: String,
}
impl JetState {
fn new(phase: Phase, model: &str) -> Self {
JetState {
phase,
model: model.to_string(),
}
}
fn from_value(value: &Value) -> Option<Self> {
let phase = Phase::parse(value.get("phase")?.as_str()?)?;
let model = value.get("model")?.as_str()?;
if model.trim().is_empty() {
return None;
}
Some(JetState::new(phase, model))
}
fn to_value(&self) -> Value {
json!({ "phase": self.phase.as_str(), "model": self.model })
}
}
struct RouteInput<'a> {
reason: ModelRouteReason,
thinking_level: Option<&'a str>,
previous: Option<&'a RouteTarget>,
state: Option<&'a Value>,
messages: &'a [AgentMessage],
}
impl<'a> From<&'a ModelRouteRequest<'a>> for RouteInput<'a> {
fn from(request: &'a ModelRouteRequest<'a>) -> Self {
RouteInput {
reason: request.reason,
thinking_level: request.thinking_level,
previous: request.previous.as_ref(),
state: request.state,
messages: request.messages,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Complexity {
Complex,
Standard,
Unknown,
}
struct JetRouter {
router: RouterConfig,
classifier: Option<ClassifierConfig>,
}
impl JetRouter {
async fn decide(&self, input: RouteInput<'_>) -> Result<ModelRoute> {
if input.reason == ModelRouteReason::Direct {
return Ok(self.route_to(&self.router.implementation, &input, None));
}
let state = input.state.and_then(JetState::from_value);
if state.as_ref().is_some_and(|s| s.phase == Phase::Planning)
&& edited_this_turn(input.messages)
{
let target = &self.router.implementation;
let next = JetState::new(Phase::Implementation, &target.model);
return Ok(self.route_to(target, &input, Some(next)));
}
if let Some(state) = state
&& let Some(target) = self.target_for_state(&state)
{
return Ok(self.route_to(target, &input, None));
}
let target = self.choose_planning(&input).await;
let state = JetState::new(Phase::Planning, &target.model);
Ok(self.route_to(target, &input, Some(state)))
}
fn route_to(
&self,
target: &Target,
input: &RouteInput<'_>,
state: Option<JetState>,
) -> ModelRoute {
ModelRoute {
provider: target.provider.clone(),
model_id: target.model.clone(),
thinking_level: input.thinking_level.map(str::to_string),
state: state.map(|s| s.to_value()),
}
}
fn target_for_state(&self, state: &JetState) -> Option<&Target> {
let router = &self.router;
match state.phase {
Phase::Planning => [Some(&router.planning), router.complex()]
.into_iter()
.flatten()
.find(|target| target.model == state.model),
Phase::Implementation => {
(router.implementation.model == state.model).then_some(&router.implementation)
}
}
}
async fn choose_planning(&self, input: &RouteInput<'_>) -> &Target {
let router = &self.router;
if let Some(previous) = input.previous
&& let Some(target) = [Some(&router.planning), router.complex()]
.into_iter()
.flatten()
.find(|target| target.matches(&previous.provider, &previous.model_id))
{
return target;
}
let complexity = self.complexity(input.messages).await;
plan_target(router, complexity)
}
async fn complexity(&self, messages: &[AgentMessage]) -> Complexity {
if self.router.complex().is_none() {
return Complexity::Unknown;
}
let Some(classifier) = self.classifier.as_ref().filter(|c| c.is_complete()) else {
return Complexity::Unknown;
};
match classify_complexity(classifier, messages).await {
Some(true) => Complexity::Complex,
Some(false) => Complexity::Standard,
None => Complexity::Unknown,
}
}
}
impl VirtualModelRouter for JetRouter {
fn route<'a>(&'a self, request: ModelRouteRequest<'a>) -> BoxFuture<'a, Result<ModelRoute>> {
Box::pin(async move {
let input = RouteInput::from(&request);
self.decide(input).await
})
}
}
fn plan_target(router: &RouterConfig, complexity: Complexity) -> &Target {
match (complexity, router.complex()) {
(Complexity::Complex, Some(complex)) => complex,
_ => &router.planning,
}
}
fn edited_this_turn(messages: &[AgentMessage]) -> bool {
let Some(last_user) = messages.iter().rposition(|m| m.role == "user") else {
return false;
};
messages[last_user + 1..].iter().any(|m| {
m.role == "toolResult"
&& !m.is_error
&& m.tool_name
.as_deref()
.is_some_and(|name| EDIT_TOOLS.contains(&name))
})
}
fn last_user_text(messages: &[AgentMessage]) -> String {
let text = messages
.iter()
.rev()
.find(|m| m.role == "user")
.map(AgentMessage::text)
.unwrap_or_default();
truncate_chars_with_postfix(&text, PROMPT_MAX_CHARS, "")
}
fn classifier_context(prompt: &str) -> ClassifierContext {
ClassifierContext {
state: json!({ PROMPT_FIELD: prompt }),
images: None,
questions: BTreeMap::from([(
COMPLEXITY_ID.to_string(),
ClassifierQuestion::Choice {
instructions:
"How demanding is the software engineering work requested in `prompt`?"
.to_string(),
criteria: BTreeMap::from([
(
"standard".to_string(),
"Ordinary features, fixes, reviews, or questions".to_string(),
),
(
"complex".to_string(),
"Subtle design, cross-cutting changes, or hard debugging".to_string(),
),
]),
},
)]),
}
}
async fn classify_complexity(
classifier: &config::ClassifierConfig,
messages: &[AgentMessage],
) -> Option<bool> {
let model = model_resolver::configured_model_of_type(
&classifier.provider,
&classifier.model,
ModelType::Classifier,
None,
None,
)
.ok()?;
let context = classifier_context(&last_user_text(messages));
let result = classify(&model, &context).await;
if result.stop_reason != "stop" {
return None;
}
match result.answers.get(COMPLEXITY_ID)? {
ClassifierAnswer::Choice { probabilities, .. } => {
Some(probabilities.get("complex").copied().unwrap_or(0.0) >= COMPLEX_THRESHOLD)
}
_ => None,
}
}
fn check_physical(
provider: &str,
id: &str,
kind: ModelType,
label: &str,
) -> std::result::Result<ModelEntry, String> {
let provider = provider.trim();
if provider.is_empty() {
return Err(format!(
"{label} needs a provider (/jet set <provider> <model-id> first)"
));
}
let id = id.trim();
if id.is_empty() {
return Err(format!(
"{label} needs a model id (/jet set <provider> <model-id> first)"
));
}
if provider == VIRTUAL_PROVIDER && id == VIRTUAL_ID {
return Err(format!(
"{label} must not be the virtual model {VIRTUAL_PROVIDER}/{VIRTUAL_ID}"
));
}
if virtual_models::is_active_virtual(provider, id) {
return Err(format!(
"{label} must be a physical model, got the virtual model {provider}/{id}"
));
}
model_resolver::find_model_of_type(provider, id, kind).map_err(|e| format!("{label}: {e}"))
}
fn supported_levels(entry: &ModelEntry) -> Vec<String> {
model_resolver::model_config_from_entry(entry, None, None)
.supported_thinking_levels()
.into_iter()
.map(str::to_string)
.collect()
}
fn thinking_intersection(entries: &[&ModelEntry]) -> Vec<String> {
let Some((first, rest)) = entries.split_first() else {
return Vec::new();
};
let mut levels = supported_levels(first);
for entry in rest {
let supported = supported_levels(entry);
levels.retain(|level| supported.contains(level));
}
levels
}
fn build_definition(cfg: &JetConfig) -> std::result::Result<VirtualModelDefinition, String> {
let router = &cfg.router;
if !router.is_complete() {
return Err(format!(
"routing targets are incomplete (need --planning and --implementation; {ROUTER_USAGE})"
));
}
let planning = check_physical(
&router.planning.provider,
&router.planning.model,
ModelType::Chat,
"--planning",
)?;
let implementation = check_physical(
&router.implementation.provider,
&router.implementation.model,
ModelType::Chat,
"--implementation",
)?;
let complex = match router.complex() {
Some(target) => Some(check_physical(
&target.provider,
&target.model,
ModelType::Chat,
"--planning-complex",
)?),
None => None,
};
let context_window = match &complex {
Some(complex) => tighter_u32(planning.context_window, complex.context_window),
None => planning.context_window,
};
let max_tokens = match &complex {
Some(complex) => tighter_u32(planning.max_tokens, complex.max_tokens),
None => planning.max_tokens,
};
let mut targets = vec![&planning, &implementation];
if let Some(complex) = &complex {
targets.push(complex);
}
let levels = thinking_intersection(&targets);
let classifier = cfg.classifier.is_complete().then(|| cfg.classifier.clone());
let router = Arc::new(JetRouter {
router: router.clone(),
classifier,
});
Ok(
VirtualModelDefinition::new(VIRTUAL_PROVIDER, VIRTUAL_ID, VIRTUAL_NAME, router)
.with_thinking_levels(levels)
.with_limits(context_window, max_tokens),
)
}
fn sync_registration() {
match build_definition(&config::load()) {
Ok(definition) => virtual_models::register(definition, EXT),
Err(_) => _ = virtual_models::unregister(VIRTUAL_PROVIDER, VIRTUAL_ID),
}
}
pub struct Jet;
impl Extension for Jet {
fn name(&self) -> &str {
EXT
}
fn description(&self) -> &str {
"Virtual model jet/auto: plan on a strong model and implement on a cheap one (routes on \
the first successful edit/write). Configure with /jet set (classifier) and /jet router \
(targets); stored in agent_dir()/extensions/jet.json"
}
fn fork_project(&self) -> Option<ForkProjectInfo> {
Some(ForkProjectInfo {
plugin_name: "pi".to_string(),
plugin_version: "0.99.1".to_string(),
url: "https://github.com/earendil-works/pi".to_string(),
})
}
fn default_enabled(&self) -> bool {
false
}
fn modes(&self) -> Vec<ExtensionMode> {
vec![ExtensionMode::Dev, ExtensionMode::Creator]
}
fn tools(&self) -> Vec<ExtensionTool> {
Vec::new()
}
fn virtual_models(&self) -> Vec<VirtualModelDefinition> {
build_definition(&config::load()).into_iter().collect()
}
fn commands(&self) -> Vec<ExtensionCommand> {
vec![ExtensionCommand {
name: CMD.to_string(),
description: "Jev-style router: configure the jet/auto virtual model".to_string(),
busy_safe: true,
subcommands: SUBCOMMANDS.to_vec(),
}]
}
fn on_registered(&self) {
register_slash_command(EXT, CMD, command_jet);
}
fn on_enabled_changed(&self, enabled: bool) {
if enabled {
sync_registration();
}
}
}
#[derive(Debug, Default)]
struct RouterArgs {
provider: Option<String>,
planning: Option<String>,
planning_complex: Option<String>,
implementation: Option<String>,
planning_provider: Option<String>,
planning_complex_provider: Option<String>,
implementation_provider: Option<String>,
}
impl RouterArgs {
fn is_empty(&self) -> bool {
self.provider.is_none()
&& self.planning.is_none()
&& self.planning_complex.is_none()
&& self.implementation.is_none()
&& self.planning_provider.is_none()
&& self.planning_complex_provider.is_none()
&& self.implementation_provider.is_none()
}
}
fn parse_router_args(rest: &str) -> std::result::Result<RouterArgs, String> {
let mut args = RouterArgs::default();
let mut tokens = rest.split_whitespace();
while let Some(token) = tokens.next() {
let (name, inline) = match token.split_once('=') {
Some((name, value)) => (name, Some(value.to_string())),
None => (token, None),
};
let slot = match name {
"--provider" => &mut args.provider,
"--planning" => &mut args.planning,
"--planning-complex" => &mut args.planning_complex,
"--implementation" => &mut args.implementation,
"--planning-provider" => &mut args.planning_provider,
"--planning-complex-provider" => &mut args.planning_complex_provider,
"--implementation-provider" => &mut args.implementation_provider,
other => return Err(format!("unknown flag `{other}`")),
};
let value = match inline {
Some(value) => value,
None => tokens
.next()
.map(str::to_string)
.ok_or_else(|| format!("{name} requires a value"))?,
};
*slot = Some(value);
}
if args.is_empty() {
return Err("no flags given".to_string());
}
Ok(args)
}
fn validate_router_change(
args: &RouterArgs,
before: &RouterConfig,
candidate: &RouterConfig,
) -> std::result::Result<(), String> {
for (flag, provider) in [
("--provider", args.provider.as_deref()),
("--planning-provider", args.planning_provider.as_deref()),
(
"--planning-complex-provider",
args.planning_complex_provider.as_deref(),
),
(
"--implementation-provider",
args.implementation_provider.as_deref(),
),
] {
let Some(provider) = provider else {
continue;
};
let provider = provider.trim();
if provider.is_empty() {
return Err(format!("{flag} must not be empty"));
}
if !model_resolver::is_known_provider(provider) {
return Err(format!(
"unknown provider `{provider}` (no model catalog; check /login or models.json)"
));
}
}
for (flag, value) in [
("--planning", args.planning.as_deref()),
("--planning-complex", args.planning_complex.as_deref()),
("--implementation", args.implementation.as_deref()),
] {
if value.is_some_and(|v| v.trim().is_empty()) {
return Err(format!(
"{flag} must not be empty (omit the flag to keep the current value)"
));
}
}
for ((flag, old), (_, new)) in before
.targets_all()
.into_iter()
.zip(candidate.targets_all())
{
if old == new || !new.is_complete() {
continue;
}
check_physical(&new.provider, &new.model, ModelType::Chat, flag)?;
}
Ok(())
}
fn target_summary(target: &Target) -> String {
if target.is_complete() {
format!("{}/{}", target.provider, target.model)
} else {
"(unset)".to_string()
}
}
fn router_summary(router: &RouterConfig) -> String {
router
.targets()
.into_iter()
.map(|(flag, target)| {
format!(
"{} {}",
flag.trim_start_matches("--"),
target_summary(target)
)
})
.collect::<Vec<_>>()
.join(", ")
}
fn report_registration(st: &mut App, cfg: &JetConfig) {
match build_definition(cfg) {
Ok(_) => st.push_msg(
format!("jet/auto: ready ({})", router_summary(&cfg.router)),
MsgLevel::Success,
),
Err(reason) => st.push_msg(
format!("jet/auto: not registered — {reason}"),
MsgLevel::Warning,
),
}
}
fn report_classifier(st: &mut App, cfg: &JetConfig) {
let classifier = &cfg.classifier;
if !classifier.is_complete() {
st.push_msg(
"jet classifier: (unset — planning complexity is not classified)".to_string(),
MsgLevel::Info,
);
return;
}
let label = format!("{}/{}", classifier.provider, classifier.model);
match check_physical(
&classifier.provider,
&classifier.model,
ModelType::Classifier,
"jet classifier",
) {
Ok(_) => st.push_msg(format!("jet classifier: {label}"), MsgLevel::Info),
Err(err) => st.push_msg(
format!("jet classifier: {label} is unusable — {err}"),
MsgLevel::Warning,
),
}
}
fn show_status(st: &mut App) {
let cfg = config::load();
report_registration(st, &cfg);
report_classifier(st, &cfg);
}
fn set_command(st: &mut App, rest: &str) {
let parts: Vec<&str> = rest.split_whitespace().collect();
if parts.len() != 2 {
st.push_msg(format!("jet: {SET_USAGE}"), MsgLevel::Warning);
return;
}
let (provider, model) = (parts[0], parts[1]);
if let Err(err) = check_physical(provider, model, ModelType::Classifier, "jet classifier") {
st.push_msg(format!("jet: {err}"), MsgLevel::Warning);
return;
}
let cfg = config::set_classifier(provider, model);
sync_registration();
st.push_msg(
format!("jet classifier set: {provider}/{model}"),
MsgLevel::Success,
);
report_registration(st, &cfg);
}
fn router_command(st: &mut App, rest: &str) {
let args = match parse_router_args(rest) {
Ok(args) => args,
Err(err) => {
st.push_msg(format!("jet: {err}. {ROUTER_USAGE}"), MsgLevel::Warning);
return;
}
};
let before = config::load().router;
let mut candidate = before.clone();
if let Some(provider) = args.provider.as_deref() {
let provider = provider.trim().to_string();
candidate.planning.provider = provider.clone();
candidate.planning_complex.provider = provider.clone();
candidate.implementation.provider = provider;
}
for (slot, value) in [
(&mut candidate.planning.provider, &args.planning_provider),
(
&mut candidate.planning_complex.provider,
&args.planning_complex_provider,
),
(
&mut candidate.implementation.provider,
&args.implementation_provider,
),
] {
if let Some(value) = value {
*slot = value.trim().to_string();
}
}
for (slot, value) in [
(&mut candidate.planning.model, &args.planning),
(
&mut candidate.planning_complex.model,
&args.planning_complex,
),
(&mut candidate.implementation.model, &args.implementation),
] {
if let Some(value) = value {
*slot = value.trim().to_string();
}
}
if let Err(err) = validate_router_change(&args, &before, &candidate) {
st.push_msg(format!("jet: {err}"), MsgLevel::Warning);
return;
}
let cfg = config::set_router(&candidate);
sync_registration();
st.push_msg(
format!("jet router set: {}", router_summary(&cfg.router)),
MsgLevel::Success,
);
report_registration(st, &cfg);
}
fn reset_command(st: &mut App) {
config::reset();
sync_registration();
st.push_msg(
"jet: configuration cleared (jet/auto unregistered)".to_string(),
MsgLevel::Success,
);
switch_away_from_virtual(st);
}
fn switch_away_from_virtual(st: &mut App) {
let selected_virtual = st.current_provider.as_deref() == Some(VIRTUAL_PROVIDER)
&& st.current_model.as_deref() == Some(VIRTUAL_ID);
if !selected_virtual {
return;
}
let Some((provider, model_id)) = switch_target(&st.messages) else {
st.push_msg(
format!(
"jet: switch away from {VIRTUAL_PROVIDER}/{VIRTUAL_ID} manually (/model): no usable fallback model"
),
MsgLevel::Warning,
);
return;
};
st.worker.send(AgentCommand::SwitchModel {
provider: provider.clone(),
model_id: model_id.clone(),
persist: false,
});
st.push_msg(
format!(
"jet: current session was on {VIRTUAL_PROVIDER}/{VIRTUAL_ID}; switched to {provider}/{model_id}"
),
MsgLevel::Info,
);
}
fn switch_target(messages: &[AgentMessage]) -> Option<(String, String)> {
virtual_models::previous_route(messages)
.map(|route| (route.provider, route.model_id))
.or_else(|| {
model_resolver::resolve_provider_model(None, None, &settings_manager::read_settings())
.ok()
.map(|(provider, model_id, _)| (provider, model_id))
})
}
fn command_jet(st: &mut App, raw: &str) -> bool {
let arg = command_arg(raw).trim();
let (sub, rest) = match arg.split_once(char::is_whitespace) {
Some((sub, rest)) => (sub, rest.trim()),
None => (arg, ""),
};
match sub {
"" => show_status(st),
"set" => set_command(st, rest),
"router" => router_command(st, rest),
"reset" => reset_command(st),
other => st.push_msg(
format!("Unknown /jet subcommand: {other}. Use `set`, `router` or `reset`."),
MsgLevel::Warning,
),
}
false
}
#[cfg(test)]
mod tests {
use super::*;
use crate::modes::interactive::app::sys_spans_text;
use std::sync::MutexGuard;
use tokio::{
io::{AsyncReadExt as _, AsyncWriteExt as _},
net::TcpListener,
};
fn setup() -> (crate::test_support::AgentDirGuard, MutexGuard<'static, ()>) {
let guard = crate::test_support::AUTH_TEST_LOCK
.lock()
.unwrap_or_else(|e| e.into_inner());
(crate::test_support::AgentDirGuard::temp(), guard)
}
fn msg(value: Value) -> AgentMessage {
serde_json::from_value(value).expect("valid AgentMessage fixture")
}
fn user(text: &str) -> AgentMessage {
AgentMessage::user_text(text)
}
fn tool_result(name: &str, is_error: bool) -> AgentMessage {
msg(json!({
"role": "toolResult",
"content": [],
"toolName": name,
"isError": is_error,
}))
}
fn assistant(provider: &str, model: &str) -> AgentMessage {
msg(json!({
"role": "assistant",
"content": [],
"provider": provider,
"model": model,
}))
}
fn target(provider: &str, model: &str) -> config::Target {
config::Target {
provider: provider.to_string(),
model: model.to_string(),
}
}
fn catalog_model(id: &str) -> String {
model_resolver::find_model_of_type("openai", id, ModelType::Chat)
.unwrap_or_else(|e| panic!("catalog fixture {id} missing: {e}"))
.id
}
fn first_chat(provider: &str) -> String {
model_resolver::list_models(provider)
.first()
.map(|(id, _)| id.clone())
.unwrap_or_else(|| panic!("provider {provider} 应有 chat 模型"))
}
fn router_cfg() -> config::RouterConfig {
config::RouterConfig {
planning: target("openai", &catalog_model("gpt-5.6-terra")),
planning_complex: target("openai", &catalog_model("gpt-5.6-sol")),
implementation: target("openai", &catalog_model("gpt-5.6-luna")),
}
}
fn router(
router: config::RouterConfig,
classifier: Option<config::ClassifierConfig>,
) -> JetRouter {
JetRouter { router, classifier }
}
fn cfg_of(router: config::RouterConfig) -> config::JetConfig {
config::JetConfig {
router,
classifier: config::ClassifierConfig::default(),
}
}
fn input<'a>(
reason: ModelRouteReason,
previous: Option<&'a RouteTarget>,
state: Option<&'a Value>,
messages: &'a [AgentMessage],
) -> RouteInput<'a> {
RouteInput {
reason,
thinking_level: Some("high"),
previous,
state,
messages,
}
}
fn gate_err(cfg: &config::JetConfig) -> String {
match build_definition(cfg) {
Ok(_) => panic!("expected the registration gate to reject {cfg:?}"),
Err(err) => err,
}
}
fn all_msgs(st: &App) -> Vec<String> {
st.system_messages
.iter()
.map(|(_, msg)| sys_spans_text(&msg.spans))
.collect()
}
fn has_msg(st: &App, needle: &str) -> bool {
all_msgs(st).iter().any(|text| text.contains(needle))
}
#[test]
fn state_round_trips_and_rejects_invalid() {
let state = JetState::new(Phase::Planning, "m1");
assert_eq!(
state.to_value(),
json!({ "phase": "planning", "model": "m1" })
);
assert_eq!(JetState::from_value(&state.to_value()), Some(state));
assert_eq!(JetState::from_value(&json!({ "phase": "planning" })), None);
assert_eq!(JetState::from_value(&json!({ "model": "m1" })), None);
assert_eq!(
JetState::from_value(&json!({ "phase": "paused", "model": "m1" })),
None
);
assert_eq!(
JetState::from_value(&json!({ "phase": "planning", "model": " " })),
None
);
}
#[test]
fn edited_this_turn_detects_only_successful_edits_after_last_user() {
assert!(edited_this_turn(&[user("hi"), tool_result("edit", false)]));
assert!(edited_this_turn(&[user("hi"), tool_result("write", false)]));
assert!(!edited_this_turn(&[user("hi"), tool_result("edit", true)]));
assert!(!edited_this_turn(&[user("hi"), tool_result("read", false)]));
assert!(!edited_this_turn(&[
user("old"),
tool_result("edit", false),
user("new"),
]));
assert!(!edited_this_turn(&[tool_result("edit", false)]));
}
#[test]
fn plan_target_prefers_complex_only_when_available() {
let mut router = router_cfg();
assert_eq!(
plan_target(&router, Complexity::Complex),
&router.planning_complex
);
assert_eq!(plan_target(&router, Complexity::Standard), &router.planning);
assert_eq!(plan_target(&router, Complexity::Unknown), &router.planning);
router.planning_complex = config::Target::default();
assert_eq!(plan_target(&router, Complexity::Complex), &router.planning);
}
#[test]
fn last_user_text_joins_blocks_and_truncates() {
let messages = [user("first"), assistant("openai", "m"), user("second")];
assert_eq!(last_user_text(&messages), "second");
let long = "x".repeat(PROMPT_MAX_CHARS + 10);
let messages = [user(&long)];
assert_eq!(last_user_text(&messages).chars().count(), PROMPT_MAX_CHARS);
assert_eq!(last_user_text(&[assistant("openai", "m")]), "");
}
#[test]
fn classifier_context_asks_the_complexity_choice() {
let context = classifier_context("do the thing");
assert_eq!(context.state, json!({ "prompt": "do the thing" }));
let question = context.questions.get(COMPLEXITY_ID).expect("question");
match question {
ClassifierQuestion::Choice {
instructions,
criteria,
} => {
assert!(instructions.contains("software engineering work"));
assert_eq!(
criteria.keys().collect::<Vec<_>>(),
vec!["complex", "standard"]
);
}
other => panic!("expected a choice question, got {other:?}"),
}
}
#[test]
fn parse_router_args_accepts_both_forms_and_rejects_bad_input() {
let args = parse_router_args(
"--provider openai --planning=a --implementation b --implementation-provider anthropic",
)
.unwrap();
assert_eq!(args.provider.as_deref(), Some("openai"));
assert_eq!(args.planning.as_deref(), Some("a"));
assert_eq!(args.implementation.as_deref(), Some("b"));
assert_eq!(args.implementation_provider.as_deref(), Some("anthropic"));
assert_eq!(args.planning_complex, None);
assert!(
parse_router_args("--provider")
.unwrap_err()
.contains("requires a value")
);
assert!(
parse_router_args("--nope x")
.unwrap_err()
.contains("unknown flag")
);
assert!(
parse_router_args(" ")
.unwrap_err()
.contains("no flags given")
);
}
#[tokio::test]
async fn route_direct_goes_to_implementation_without_state() {
let router = router(router_cfg(), None);
let state = json!({ "phase": "planning", "model": router.router.planning.model });
let out = router
.decide(input(ModelRouteReason::Direct, None, Some(&state), &[]))
.await
.unwrap();
assert_eq!(out.provider, router.router.implementation.provider);
assert_eq!(out.model_id, router.router.implementation.model);
assert_eq!(out.thinking_level.as_deref(), Some("high"));
assert_eq!(out.state, None);
}
#[tokio::test]
async fn route_new_session_plans_and_writes_state() {
let router = router(router_cfg(), None);
let out = router
.decide(input(ModelRouteReason::User, None, None, &[user("hi")]))
.await
.unwrap();
assert_eq!(out.provider, router.router.planning.provider);
assert_eq!(out.model_id, router.router.planning.model);
assert_eq!(
out.state,
Some(json!({ "phase": "planning", "model": router.router.planning.model }))
);
}
#[tokio::test]
async fn route_reuses_previous_planning_model() {
let router = router(router_cfg(), None);
let complex = router.router.planning_complex.clone();
let previous = RouteTarget {
provider: complex.provider.clone(),
model_id: complex.model.clone(),
thinking_level: None,
};
let out = router
.decide(input(ModelRouteReason::User, Some(&previous), None, &[]))
.await
.unwrap();
assert_eq!(out.model_id, complex.model);
assert_eq!(
out.state,
Some(json!({ "phase": "planning", "model": complex.model }))
);
let previous = RouteTarget {
provider: "anthropic".to_string(),
model_id: complex.model,
thinking_level: None,
};
let out = router
.decide(input(ModelRouteReason::User, Some(&previous), None, &[]))
.await
.unwrap();
assert_eq!(out.model_id, router.router.planning.model);
}
#[tokio::test]
async fn route_switches_to_implementation_after_first_edit() {
let router = router(router_cfg(), None);
let state = json!({ "phase": "planning", "model": router.router.planning.model });
let messages = [user("do it"), tool_result("edit", false)];
let out = router
.decide(input(
ModelRouteReason::Continuation,
None,
Some(&state),
&messages,
))
.await
.unwrap();
assert_eq!(out.model_id, router.router.implementation.model);
assert_eq!(
out.state,
Some(json!({ "phase": "implementation", "model": router.router.implementation.model }))
);
let state =
json!({ "phase": "implementation", "model": router.router.implementation.model });
let out = router
.decide(input(
ModelRouteReason::Continuation,
None,
Some(&state),
&messages,
))
.await
.unwrap();
assert_eq!(out.model_id, router.router.implementation.model);
assert_eq!(out.state, None);
}
#[tokio::test]
async fn route_keeps_state_model_without_rewriting_state() {
let router = router(router_cfg(), None);
let state = json!({ "phase": "planning", "model": router.router.planning.model });
let out = router
.decide(input(
ModelRouteReason::Continuation,
None,
Some(&state),
&[user("more")],
))
.await
.unwrap();
assert_eq!(out.model_id, router.router.planning.model);
assert_eq!(out.state, None, "沿用状态时不得重复落条目");
}
#[tokio::test]
async fn route_replans_when_state_model_left_the_config() {
let router = router(router_cfg(), None);
let state = json!({ "phase": "implementation", "model": "retired-model" });
let out = router
.decide(input(
ModelRouteReason::Continuation,
None,
Some(&state),
&[],
))
.await
.unwrap();
assert_eq!(out.model_id, router.router.planning.model);
assert_eq!(
out.state,
Some(json!({ "phase": "planning", "model": router.router.planning.model }))
);
}
#[test]
fn definition_gate_requires_complete_and_resolvable_targets() {
let (_ad, _g) = setup();
let definition =
build_definition(&cfg_of(router_cfg())).expect("完整且可解析的配置应可注册");
assert_eq!(definition.provider, VIRTUAL_PROVIDER);
assert_eq!(definition.id, VIRTUAL_ID);
assert_eq!(definition.name, VIRTUAL_NAME);
let mut incomplete = router_cfg();
incomplete.implementation = config::Target::default();
assert!(gate_err(&cfg_of(incomplete)).contains("incomplete"));
let mut half = router_cfg();
half.planning.model = String::new();
assert!(gate_err(&cfg_of(half)).contains("incomplete"));
let mut unknown = router_cfg();
unknown.implementation.model = "no-such-model".to_string();
assert!(gate_err(&cfg_of(unknown)).contains("--implementation"));
let mut unknown_provider = router_cfg();
unknown_provider.implementation.provider = "no-such-provider".to_string();
assert!(gate_err(&cfg_of(unknown_provider)).contains("no-such-provider"));
let openrouter = |kind: ModelType| {
model_resolver::list_models_of_type("openrouter", kind)
.into_iter()
.map(|(id, _)| id)
.collect::<Vec<_>>()
};
let chats = openrouter(ModelType::Chat);
let image = openrouter(ModelType::Image)
.first()
.cloned()
.expect("image 条目");
let classifier = openrouter(ModelType::Classifier)
.first()
.cloned()
.expect("classifier 条目");
let mut wrong_type = router_cfg();
wrong_type.implementation = target("openrouter", &image);
assert!(gate_err(&cfg_of(wrong_type)).contains("--implementation"));
let mut wrong_type = router_cfg();
wrong_type.planning = target("openrouter", &classifier);
wrong_type.planning_complex = target("openrouter", &chats[0]);
assert!(gate_err(&cfg_of(wrong_type)).contains("--planning"));
let mut self_ref = router_cfg();
self_ref.planning = target(VIRTUAL_PROVIDER, VIRTUAL_ID);
assert!(gate_err(&cfg_of(self_ref)).contains("virtual model"));
}
#[test]
fn definition_inherits_limits_and_thinking_levels() {
let (_ad, _g) = setup();
let router_cfg = router_cfg();
let definition = build_definition(&cfg_of(router_cfg.clone())).unwrap();
let planning = check_physical(
&router_cfg.planning.provider,
&router_cfg.planning.model,
ModelType::Chat,
"p",
)
.unwrap();
let complex = check_physical(
&router_cfg.planning_complex.provider,
&router_cfg.planning_complex.model,
ModelType::Chat,
"c",
)
.unwrap();
assert_eq!(
definition.context_window,
tighter_u32(planning.context_window, complex.context_window)
);
assert_eq!(
definition.max_tokens,
tighter_u32(planning.max_tokens, complex.max_tokens)
);
let implementation = check_physical(
&router_cfg.implementation.provider,
&router_cfg.implementation.model,
ModelType::Chat,
"i",
)
.unwrap();
let expected = thinking_intersection(&[&planning, &implementation, &complex]);
let expected: Vec<String> = if expected.is_empty() {
vec!["off".to_string()]
} else {
expected
};
assert_eq!(definition.resolved_thinking_levels(), expected);
}
#[tokio::test]
#[allow(clippy::await_holding_lock)] async fn targets_may_come_from_different_providers() {
let (_ad, _g) = setup();
let anthropic = first_chat("anthropic");
let mut router_cfg = router_cfg();
router_cfg.implementation = target("anthropic", &anthropic);
let definition =
build_definition(&cfg_of(router_cfg.clone())).expect("跨 provider 的三档应可注册");
let planning =
check_physical("openai", &router_cfg.planning.model, ModelType::Chat, "p").unwrap();
let implementation = check_physical("anthropic", &anthropic, ModelType::Chat, "i").unwrap();
let complex = check_physical(
"openai",
&router_cfg.planning_complex.model,
ModelType::Chat,
"c",
)
.unwrap();
let expected = thinking_intersection(&[&planning, &implementation, &complex]);
let expected: Vec<String> = if expected.is_empty() {
vec!["off".to_string()]
} else {
expected
};
assert_eq!(
definition.resolved_thinking_levels(),
expected,
"thinking 档位是三个目标(可能来自不同 provider)的交集"
);
let jet = router(router_cfg, None);
let out = jet
.decide(input(ModelRouteReason::Direct, None, None, &[]))
.await
.unwrap();
assert_eq!(
(out.provider.as_str(), out.model_id.as_str()),
("anthropic", anthropic.as_str()),
"实现档用自己的 provider"
);
let out = jet
.decide(input(ModelRouteReason::User, None, None, &[]))
.await
.unwrap();
assert_eq!(out.provider, "openai", "规划档仍是 openai");
assert_eq!(out.model_id, jet.router.planning.model);
}
#[tokio::test]
#[allow(clippy::await_holding_lock)] async fn state_follows_target_after_provider_change() {
let (_ad, _g) = setup();
let model = catalog_model("gpt-5.6-terra");
let mut before = router_cfg();
before.planning = target("openai", &model);
let state = json!({ "phase": "planning", "model": model });
let mut after = before.clone();
after.planning = target("openrouter", &model);
let jet = router(after, None);
let out = jet
.decide(input(
ModelRouteReason::Continuation,
None,
Some(&state),
&[user("more")],
))
.await
.unwrap();
assert_eq!(
(out.provider.as_str(), out.model_id.as_str()),
("openrouter", model.as_str())
);
assert_eq!(out.state, None, "沿用状态时不重复落条目");
}
#[test]
fn virtual_models_follow_configuration() {
let (_ad, _g) = setup();
let ext = Jet;
assert!(ext.virtual_models().is_empty());
config::set_router(&router_cfg());
let definitions = ext.virtual_models();
assert_eq!(definitions.len(), 1);
assert_eq!(definitions[0].provider, VIRTUAL_PROVIDER);
assert_eq!(definitions[0].id, VIRTUAL_ID);
config::reset();
assert!(ext.virtual_models().is_empty());
}
#[test]
fn router_command_writes_incrementally_and_rejects_bad_input() {
let (_ad, _g) = setup();
let mut st = App::new();
command_jet(&mut st, "jet");
assert!(has_msg(&st, "not registered"), "{:?}", all_msgs(&st));
command_jet(&mut st, "jet router --provider openai");
assert!(has_msg(&st, "not registered"), "{:?}", all_msgs(&st));
let router = config::load().router;
assert_eq!(router.planning.provider, "openai");
assert_eq!(router.implementation.provider, "openai");
assert_eq!(router.planning_complex.provider, "openai", "批量设三档");
command_jet(&mut st, "jet router --planning nope --nope x");
assert!(has_msg(&st, "unknown flag"), "{:?}", all_msgs(&st));
assert_eq!(config::load().router.planning.model, "");
command_jet(&mut st, "jet router --planning no-such-model");
assert!(has_msg(&st, "--planning"), "{:?}", all_msgs(&st));
assert_eq!(config::load().router.planning.model, "");
command_jet(&mut st, "jet router --provider no-such-provider");
assert!(has_msg(&st, "unknown provider"), "{:?}", all_msgs(&st));
assert_eq!(config::load().router.planning.provider, "openai");
command_jet(
&mut st,
"jet router --implementation-provider no-such-provider",
);
assert!(has_msg(&st, "unknown provider"), "{:?}", all_msgs(&st));
assert_eq!(config::load().router.implementation.provider, "openai");
command_jet(
&mut st,
&format!(
"jet router --planning {} --implementation {}",
catalog_model("gpt-5.6-terra"),
catalog_model("gpt-5.6-luna")
),
);
assert!(has_msg(&st, "ready"), "{:?}", all_msgs(&st));
let cfg = config::load();
assert!(cfg.router.is_complete());
assert!(build_definition(&cfg).is_ok());
command_jet(&mut st, "jet router --implementation=gpt-5.6-sol");
let cfg = config::load();
assert_eq!(cfg.router.implementation.model, "gpt-5.6-sol");
assert_eq!(cfg.router.planning.model, catalog_model("gpt-5.6-terra"));
let anthropic = first_chat("anthropic");
command_jet(
&mut st,
&format!("jet router --implementation-provider anthropic --implementation {anthropic}"),
);
let cfg = config::load();
assert_eq!(
(
cfg.router.implementation.provider.as_str(),
cfg.router.implementation.model.as_str()
),
("anthropic", anthropic.as_str())
);
assert_eq!(cfg.router.planning.provider, "openai", "其他档不受影响");
assert!(build_definition(&cfg).is_ok());
command_jet(
&mut st,
"jet router --provider openai --implementation-provider anthropic",
);
let cfg = config::load();
assert_eq!(cfg.router.planning.provider, "openai");
assert_eq!(cfg.router.implementation.provider, "anthropic");
}
#[test]
fn set_command_requires_a_classifier_entry() {
let (_ad, _g) = setup();
let mut st = App::new();
command_jet(&mut st, "jet set openrouter");
assert!(has_msg(&st, "Usage: /jet set"), "{:?}", all_msgs(&st));
command_jet(&mut st, "jet set openai gpt-5.6-terra");
assert!(has_msg(&st, "type: classifier"), "{:?}", all_msgs(&st));
assert!(!config::load().classifier.is_complete());
let classifier = model_resolver::list_models_of_type("openrouter", ModelType::Classifier)
.first()
.map(|(id, _)| id.clone())
.expect("openrouter 目录应有 classifier 条目");
command_jet(&mut st, &format!("jet set openrouter {classifier}"));
assert!(has_msg(&st, "not registered"), "{:?}", all_msgs(&st));
assert_eq!(config::load().classifier.model, classifier);
}
#[test]
fn reset_clears_config_and_switches_away_from_virtual() {
let (_ad, _g) = setup();
config::set_router(&router_cfg());
config::set_classifier("openrouter", "~typesafe/jev-latest");
let mut st = App::new();
st.current_provider = Some(VIRTUAL_PROVIDER.to_string());
st.current_model = Some(VIRTUAL_ID.to_string());
st.messages = vec![assistant("anthropic", "claude-real")];
command_jet(&mut st, "jet reset");
assert_eq!(config::load(), config::JetConfig::default());
assert!(build_definition(&config::load()).is_err());
assert!(
has_msg(&st, "switched to anthropic/claude-real"),
"reset 应切走当前会话选中的 jet/auto:{:?}",
all_msgs(&st)
);
let mut st = App::new();
st.current_provider = Some("anthropic".to_string());
st.current_model = Some("claude-real".to_string());
command_jet(&mut st, "jet reset");
assert!(
!has_msg(&st, "switched to"),
"未选中 jet/auto 时不应切换:{:?}",
all_msgs(&st)
);
}
#[test]
fn switch_target_prefers_previous_physical_model() {
let (_ad, _g) = setup();
let messages = [user("hi"), assistant("anthropic", "claude-real")];
assert_eq!(
switch_target(&messages),
Some(("anthropic".to_string(), "claude-real".to_string()))
);
let fallback = switch_target(&[]).expect("应能解析兜底模型");
assert!(!fallback.0.is_empty() && !fallback.1.is_empty());
}
async fn mock_classifier(
status: u16,
body: &'static str,
) -> (String, tokio::task::JoinHandle<String>) {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let handle = tokio::spawn(async move {
let (mut socket, _) = listener.accept().await.unwrap();
let mut buf = Vec::new();
let mut tmp = [0u8; 4096];
let mut header_end = None;
loop {
let n = socket.read(&mut tmp).await.unwrap();
if n == 0 {
break;
}
buf.extend_from_slice(&tmp[..n]);
if header_end.is_none() {
header_end = buf.windows(4).position(|w| w == b"\r\n\r\n").map(|p| p + 4);
}
if let Some(he) = header_end {
let head = String::from_utf8_lossy(&buf[..he]).to_lowercase();
let len: usize = head
.lines()
.find_map(|l| l.strip_prefix("content-length:"))
.and_then(|v| v.trim().parse().ok())
.unwrap_or(0);
if buf.len() >= he + len {
break;
}
}
}
let request = String::from_utf8_lossy(&buf).to_string();
let reason = if status == 200 { "OK" } else { "Bad Request" };
let response = format!(
"HTTP/1.1 {status} {reason}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
body.len()
);
socket.write_all(response.as_bytes()).await.unwrap();
socket.shutdown().await.unwrap();
request
});
(format!("http://127.0.0.1:{}", addr.port()), handle)
}
fn write_classifier_provider(base_url: &str) {
std::fs::write(
settings_manager::agent_dir().join("models.json"),
json!({"providers":{"jet-cls":{
"apiKey":"sk-test",
"baseUrl":base_url,
"api":"typesafe-system-one",
"models":[{"id":"jev","name":"Jev","type":"classifier","api":"typesafe-system-one"}]
}}})
.to_string(),
)
.unwrap();
}
#[test]
#[allow(clippy::await_holding_lock)] fn classifier_picks_complex_planning_model() {
crate::test_support::run_with_timeout(std::time::Duration::from_secs(90), || {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let (base, handle) = mock_classifier(
200,
r#"{"answers":{"complexity":{"type":"choice","choice":"complex",
"probabilities":{"complex":0.9,"standard":0.1},"confidence":0.9}}}"#,
)
.await;
let (_ad, _g) = setup();
write_classifier_provider(&base);
let router_cfg = router_cfg();
let complex = router_cfg.planning_complex.clone();
let jet = router(
router_cfg.clone(),
Some(config::ClassifierConfig {
provider: "jet-cls".to_string(),
model: "jev".to_string(),
}),
);
let messages = [user("refactor the scheduler")];
let out = jet
.decide(input(ModelRouteReason::User, None, None, &messages))
.await
.unwrap();
assert_eq!(out.model_id, complex.model);
assert_eq!(out.provider, complex.provider);
let request = handle.await.unwrap();
assert!(request.starts_with("POST /systemone "), "{request}");
assert!(request.contains("refactor the scheduler"), "{request}");
});
});
}
#[test]
#[allow(clippy::await_holding_lock)] fn classifier_entry_wins_over_same_id_chat_entry() {
crate::test_support::run_with_timeout(std::time::Duration::from_secs(90), || {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let (base, handle) = mock_classifier(
200,
r#"{"answers":{"complexity":{"type":"choice","choice":"complex",
"probabilities":{"complex":0.9,"standard":0.1},"confidence":0.9}}}"#,
)
.await;
let (_ad, _g) = setup();
std::fs::write(
settings_manager::agent_dir().join("models.json"),
json!({"providers":{"jet-cls":{
"apiKey":"sk-test",
"baseUrl":base,
"api":"typesafe-system-one",
"models":[
{"id":"jev","name":"Jev Chat","type":"chat","api":"openai-completions"},
{"id":"jev","name":"Jev","type":"classifier","api":"typesafe-system-one"}
]
}}})
.to_string(),
)
.unwrap();
let router_cfg = router_cfg();
let complex = router_cfg.planning_complex.clone();
let jet = router(
router_cfg,
Some(config::ClassifierConfig {
provider: "jet-cls".to_string(),
model: "jev".to_string(),
}),
);
let out = jet
.decide(input(
ModelRouteReason::User,
None,
None,
&[user("refactor the scheduler")],
))
.await
.unwrap();
assert_eq!(out.model_id, complex.model);
assert_eq!(out.provider, complex.provider);
let request = handle.await.unwrap();
assert!(request.starts_with("POST /systemone "), "{request}");
});
});
}
#[test]
#[allow(clippy::await_holding_lock)] fn classifier_failure_falls_back_to_plain_planning() {
crate::test_support::run_with_timeout(std::time::Duration::from_secs(90), || {
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
let (base, _handle) = mock_classifier(400, "{}").await;
let (_ad, _g) = setup();
write_classifier_provider(&base);
let router_cfg = router_cfg();
let planning = router_cfg.planning.clone();
let jet = router(
router_cfg,
Some(config::ClassifierConfig {
provider: "jet-cls".to_string(),
model: "jev".to_string(),
}),
);
let out = jet
.decide(input(ModelRouteReason::User, None, None, &[user("hi")]))
.await
.unwrap();
assert_eq!(out.model_id, planning.model);
assert_eq!(out.provider, planning.provider);
});
});
}
}