wip: unfinished review fix-up (interrupted by the session usage limit)

Uncommitted edits from the fix-up stage, saved so nothing is lost. Not
compiled or tested after these edits; the next pass reviews and finishes them.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01R9rJEoMuUSRWznU6QjkE7h
This commit is contained in:
CodeWhale BotandClaude Opus 5.5 committed 2026-09-25 10:42:29 -07:00
1 parent b56e2fb252
commit 02cf8b1aa3
3 files changed
+150 -20

No files matched your search

+32 -12
View File
@@ -5130,18 +5130,26 @@ impl RuntimeThreadManager {
});
match restored {
Some((restored_kind, restored_id, model)) => {
let identity = thread_config
.resolve_persisted_provider_identity(
restored_kind.as_deref(),
restored_id.as_deref(),
)
.map_err(|reason| anyhow!(reason))?;
// A later provider switch outranks what an earlier Auto turn
// picked on the previous provider.
if identity.key == provider_identity.key {
resolve_runtime_thread_route_for_identity(config, &identity, Some(&model))
} else {
resolve_runtime_thread_route_for_identity(config, &provider_identity, None)
let identity = thread_config.resolve_persisted_provider_identity(
restored_kind.as_deref(),
restored_id.as_deref(),
);
// The saved thread provider is the authority. An earlier Auto
// pick is restored only when it was made on that same
// provider; a pick from before a provider switch, from a
// one-turn override, or from a provider no longer configured
// is ignored and the saved provider's default route applies.
match identity {
Ok(identity)
if identity.provider == provider_identity.provider
&& identity.key == provider_identity.key
&& identity.exact_id == provider_identity.exact_id =>
{
resolve_runtime_thread_route_for_identity(config, &identity, Some(&model))
}
_ => {
resolve_runtime_thread_route_for_identity(config, &provider_identity, None)
}
}
}
None => resolve_runtime_thread_route_for_identity(config, &provider_identity, None),
@@ -5214,6 +5222,18 @@ impl RuntimeThreadManager {
Some(&engine_model),
) {
Ok(route) => validated.push((thread_id, engine, route, active_turn_id)),
// An idle engine still carries the route of its last turn,
// which may predate a provider switch or have been a one-turn
// override. Its next turn resolves the saved thread route, so
// that is the route the new config has to serve.
Err(err) if active_turn_id.is_none() => match self
.store
.load_thread(&thread_id)
.and_then(|thread| self.resolved_route_for_thread(&new_config, &thread))
{
Ok(route) => validated.push((thread_id, engine, route, active_turn_id)),
Err(_) => failures.push(format!("{thread_id}: {err}")),
},
Err(err) => failures.push(format!("{thread_id}: {err}")),
}
}
+99 -7
View File
@@ -3645,13 +3645,14 @@ async fn thread_records_and_create_requests_preserve_provider_kind_id_pairing()
restored_turn.effective_provider_id = None;
restored_turn.effective_model = Some("custom-openai-model".to_string());
manager.store.save_turn(&restored_turn)?;
let turn_error = manager
.resolved_route_for_thread(&config, &auto_thread)
.expect_err("restored built-in turn must not be captured by custom endpoint")
.to_string();
assert!(
turn_error.contains("requires built-in 'openai'"),
"{turn_error}"
// A restored built-in pick is not the thread's saved provider, so it is
// ignored rather than captured: the thread keeps its exact custom route.
let ignored = manager.resolved_route_for_thread(&config, &auto_thread)?;
assert_eq!(ignored.identity.provider, ApiProvider::Custom);
assert_eq!(ignored.identity.key, "openai");
assert_eq!(
ignored.config.active_route_base_url(),
"http://127.0.0.1:18183/v1"
);
restored_turn.effective_provider = Some("custom".to_string());
@@ -4241,6 +4242,97 @@ async fn start_turn_provider_override_routes_one_turn_only() -> Result<()> {
Ok(())
}
/// `two_custom_route_config` with `custom-a` removed, as a user would after
/// moving every thread to `custom-b`.
fn only_custom_b_config() -> Config {
let mut config = two_custom_route_config();
config.provider = Some("custom-b".to_string());
if let Some(providers) = config.providers.as_mut() {
providers.custom.remove("custom-a");
}
config
}
#[tokio::test]
async fn auto_thread_follows_a_provider_switch_even_after_the_old_provider_is_removed() -> Result<()>
{
let config = two_custom_route_config();
let manager = RuntimeThreadManager::open(
config.clone(),
PathBuf::from("."),
test_manager_config(test_runtime_dir()),
)?;
let thread = manager
.create_thread(CreateThreadRequest {
model: Some("auto".to_string()),
model_provider: Some("custom-a".to_string()),
..Default::default()
})
.await?;
// An earlier Auto turn picked a model on custom-a.
let mut earlier = sample_turn(&thread.id, "turn_auto_on_a", RuntimeTurnStatus::Completed);
earlier.effective_provider = Some("custom".to_string());
earlier.effective_provider_id = Some("custom-a".to_string());
earlier.effective_model = Some("model-a".to_string());
manager.store.save_turn(&earlier)?;
let before = manager.resolved_route_for_thread(&config, &thread)?;
assert_eq!(before.identity.key, "custom-a");
assert_eq!(before.model, "model-a");
let switched = manager
.update_thread(
&thread.id,
UpdateThreadRequest {
model_provider: Some("custom-b".to_string()),
..UpdateThreadRequest::default()
},
)
.await?;
assert_eq!(switched.model, "auto");
let route = manager.resolved_route_for_thread(&config, &switched)?;
assert_eq!(route.identity.key, "custom-b");
assert_eq!(route.model, "model-b");
// The earlier pick names a provider that is gone; the thread still routes
// through its saved provider instead of failing on history.
let route = manager.resolved_route_for_thread(&only_custom_b_config(), &switched)?;
assert_eq!(route.identity.key, "custom-b");
assert_eq!(route.model, "model-b");
Ok(())
}
#[tokio::test]
async fn config_reload_accepts_removing_the_provider_an_idle_thread_switched_away_from()
-> Result<()> {
let manager = RuntimeThreadManager::open(
two_custom_route_config(),
PathBuf::from("."),
test_manager_config(test_runtime_dir()),
)?;
let thread = manager
.create_thread(CreateThreadRequest {
model_provider: Some("custom-a".to_string()),
..Default::default()
})
.await?;
// The loaded engine still carries custom-a until its next turn.
let _harness = install_mock_engine(&manager, &thread.id).await;
manager
.update_thread(
&thread.id,
UpdateThreadRequest {
model_provider: Some("custom-b".to_string()),
..UpdateThreadRequest::default()
},
)
.await?;
manager.reload_config(only_custom_b_config()).await?;
let saved = manager.get_thread(&thread.id).await?;
let route = manager.resolved_route_for_thread(&manager.read_config(), &saved)?;
assert_eq!(route.identity.key, "custom-b");
Ok(())
}
#[tokio::test]
async fn create_thread_scenario() -> Result<()> {
// Scenario consolidation of: create_thread_uses_requested_non_current_builtin_default_model, create_thread_defaults_auto_approve_to_false
+19 -1
View File
@@ -786,10 +786,22 @@ accept an empty string to clear a previously-set value. Added in v0.8.10 (#562):
"model": "deepseek-v4-pro",
"mode": "agent",
"title": "User-set thread title",
"system_prompt": "You are a useful assistant."
"system_prompt": "You are a useful assistant.",
"model_provider": "custom",
"model_provider_id": "lm-studio"
}
```
`model_provider` switches the provider the thread's future turns use. It
takes a built-in kind (`deepseek`, `xai`, ...) or a configured route name, as
`/provider` does. `model_provider_id` names one exact `[providers.<id>]` table
and wins over a route name. The target route is resolved and its client
preflighted before anything is saved, so an unknown or credential-less
provider is refused and nothing changes. Without `model`, the thread takes the
new provider's default model; an `auto` thread stays `auto`. The loaded
engine and conversation history are kept, and the next turn installs the new
route.
**Turns** (within a thread)
- `POST /v1/threads/{id}/turns`
- `POST /v1/threads/{id}/turns/{turn_id}/steer` - inject guidance into the running turn. The response is a receipt for what actually happened, not for what was attempted; see [Steer delivery](#steer-delivery).
@@ -812,6 +824,12 @@ accept an empty string to clear a previously-set value. Added in v0.8.10 (#562):
}
```
The same `model_provider` / `model_provider_id` fields on a turn route that
one turn through another provider. The saved thread keeps its provider.
Without `model`, the turn uses that provider's default model (an `auto` thread
stays `auto`). The override is always preflighted and is part of the
`operation_key` fingerprint.
Resolution is deterministic: a turn override wins over the thread default,
which wins over the Runtime's normal configuration. For tools, reaching normal
configuration means the ordinary configured catalog; `[]` is never treated as