mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-08 17:10:50 -10:00
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:
1 parent
b56e2fb252
commit
02cf8b1aa3
3 files changed
+150
-20
No files matched your search
@@ -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}")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in new issue
Block a user