fix(config): honor a configured default_text_model; align the unset default with deepseek-flash

The V4.1 change moved the DeepSeek default to deepseek-flash but left two
paths disagreeing:

- Route/thread resolution ignored an operator-configured
  default_text_model and fell through to the provider catalog default, so
  a config declaring default_text_model = "deepseek-v4-pro" created new
  threads on flash (runtime_threads::create_thread ->
  resolve_runtime_route_for_identity with no selector).
- The TUI DEFAULT_TEXT_MODEL still said deepseek-v4-pro, so a bare config
  reported one default and routed another.

Resolution now mirrors provider_default_model precedence: the active
provider configured default wins over the provider catalog default. The
constant moves to deepseek-flash, matching DEFAULT_DEEPSEEK_MODEL in
codewhale-config and the shipped default.

The composed-frame test now pins the #6041 contract: the duplicate session
reading is suppressed on a first turn and returns once a finished turn
makes the totals differ.

Evidence (each: 1 passed; 0 failed):
- config::tests::remembered_deepseek_cn_and_layered_root_models_keep_their_precedence
- doctor_endpoint_tests::doctor_api_target_reports_default_endpoint
- doctor_endpoint_tests::doctor_api_target_routes_deepseek_cn_alias_to_beta_endpoint
- runtime_api::configured_model_api_tests::scoped_runtime_selections_leave_device_memory_unchanged
- runtime_api::tests::fleet_status_runtime_api_exposes_state_and_actions
- tui::ui::frame::one_owner_tests::composed_frame_paints_each_fact_in_exactly_one_row
- tui::ui::tests::provider_switch_to_deepseek_canonicalizes_openrouter_default_model
- tui::ui::tests::provider_switch_to_deepseek_drops_stale_xiaomi_root_base_url
This commit is contained in:
CodeWhale Bot committed 2026-09-10 12:17:17 -07:00
1 parent 93e511c2c7
commit 7d144a286b
3 files changed
+46 -11

No files matched your search

+1 -1
View File
@@ -6,7 +6,7 @@
//! re-exported from `crate::config` via `pub use models::*;`, so every existing
//! `crate::config::<CONST>` path keeps resolving unchanged (#3311).
pub const DEFAULT_TEXT_MODEL: &str = "deepseek-v4-pro";
pub const DEFAULT_TEXT_MODEL: &str = "deepseek-flash";
pub const DEFAULT_DEEPSEEK_BASE_URL: &str = "https://api.deepseek.com/beta";
pub const DEFAULT_DEEPSEEK_ANTHROPIC_MODEL: &str = DEFAULT_TEXT_MODEL;
pub const DEFAULT_DEEPSEEK_ANTHROPIC_BASE_URL: &str = "https://api.deepseek.com/anthropic";
+13 -1
View File
@@ -748,7 +748,19 @@ pub(crate) fn resolve_runtime_route_for_identity(
)?;
let provider = identity.provider;
let mut route_config = prepared_route_config(config, &identity, model_selector);
let saved_provider_model = configured_model_for_route(&route_config, provider);
// The operator's effective default for the active provider is the
// route's default too; mirror `provider_default_model` precedence so a
// configured choice is not displaced by the provider catalog's default
// (deepseek-flash). `auto` stays the resolver's sentinel.
let configured_default = (provider == config.api_provider()
&& config.default_text_model.is_some())
.then(|| config.default_model())
.filter(|model| {
let model = model.trim();
!model.is_empty() && !model.eq_ignore_ascii_case("auto")
});
let saved_provider_model = configured_model_for_route(&route_config, provider)
.or(configured_default.as_deref());
// #5034: with no explicit selector and no saved model, a Codex route
// would fall back to the resolver's static seed offering. Prefer the
// live Codex roster head so a provider switch lands on the current
+32 -9
View File
@@ -211,20 +211,43 @@ fn composed_frame_paints_each_fact_in_exactly_one_row() {
// session half needs 120 columns here, the turn half 160. Each
// paints in exactly one row wherever it paints. The metrics line
// carries no repository, branch or provider.
let clock_facts: &[(u16, &str)] =
&[(120, "worked 1m 15s"), (160, "sub-agents underway 1m 15s")];
for (needs, needle) in clock_facts {
if width < *needs {
continue;
}
assert!(rows[posture].contains(needle), "{}", rows[posture]);
// First turn: the turn half names the phase and stays; the session
// reading is the identical duration, so it is suppressed rather than
// stated twice (#6041) — the turn half is the one that needs 160
// columns, so a narrower row keeps the affordances and no stopwatch.
let turn_needle = "sub-agents underway 1m 15s";
if width >= 160 {
assert!(rows[posture].contains(turn_needle), "{}", rows[posture]);
assert_eq!(
count_rows_containing(&rows, needle),
count_rows_containing(&rows, turn_needle),
1,
"{width}x{height}: {needle:?} paints in exactly one row:\n{}",
"{width}x{height}: {turn_needle:?} paints in exactly one row:\n{}",
rows.join("\n")
);
}
assert!(
!rows[posture].contains("worked 1m 15s"),
"{width}x{height}: the duplicate session reading must not be stated: {}",
rows[posture]
);
// After a finished turn the totals differ and the worked chip
// returns: 1m of finished turns plus the live 1m 15s reads 2m 15s.
let mut worked = working_app();
worked.cumulative_turn_duration = Duration::from_secs(60);
let rows = draw(&mut worked, width, height);
if width >= 120 {
let worked_needle = "worked 2m 15s";
assert!(rows[posture].contains(worked_needle), "{}", rows[posture]);
assert_eq!(
count_rows_containing(&rows, worked_needle),
1,
"{width}x{height}: {worked_needle:?} paints in exactly one row:\n{}",
rows.join("\n")
);
}
if width >= 160 {
assert!(rows[posture].contains(turn_needle), "{}", rows[posture]);
}
assert!(!rows[metrics].contains('⑂'), "{}", rows[metrics]);
// No dead key hints anywhere in the frame.
for row in &rows {