use std::fmt;
use std::sync::OnceLock;
use reqwest::header::{AUTHORIZATION, CONTENT_TYPE};
use serde_json::Value;
use tracing::debug;
use crate::config::{ApiProvider, Config};
use crate::core::events::TurnRoute;
use crate::route_receipt::{TurnRouteReceipt, endpoint_identity};
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct SuggestionRouteAuthority {
receipt: TurnRouteReceipt,
}
impl SuggestionRouteAuthority {
#[must_use]
pub fn provider(&self) -> ApiProvider {
self.receipt.provider()
}
#[must_use]
pub fn provider_identity(&self) -> &str {
self.receipt.provider_identity()
}
#[must_use]
pub fn model(&self) -> &str {
self.receipt.wire_model()
}
#[must_use]
pub fn endpoint_identity(&self) -> &str {
self.receipt.endpoint_identity()
}
fn authorizes(&self, base_url: &str, api_key: &str) -> bool {
self.receipt.matches_live_route(base_url, api_key)
}
#[cfg(test)]
pub(crate) fn from_receipt_for_test(receipt: TurnRouteReceipt) -> Self {
Self { receipt }
}
}
#[derive(Clone, Copy)]
pub struct SuggestionRouteSnapshot<'a> {
pub provider: ApiProvider,
pub provider_identity: &'a str,
pub model: &'a str,
pub authority: &'a SuggestionRouteAuthority,
pub actual_base_url: Option<&'a str>,
}
impl fmt::Debug for SuggestionRouteSnapshot<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SuggestionRouteSnapshot")
.field("provider", &self.provider)
.field("provider_identity", &self.provider_identity)
.field("model", &self.model)
.field("authority", &self.authority)
.field(
"actual_endpoint_identity",
&self.actual_base_url.map(endpoint_identity),
)
.finish()
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct SuggestionRouteCredentials {
pub api_key: String,
pub base_url: String,
pub model: String,
}
impl fmt::Debug for SuggestionRouteCredentials {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SuggestionRouteCredentials")
.field("api_key", &"<redacted>")
.field("endpoint_identity", &endpoint_identity(&self.base_url))
.field("model", &self.model)
.finish()
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct SuggestionLaunch {
pub api_key: String,
pub base_url: String,
pub model: String,
}
impl fmt::Debug for SuggestionLaunch {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SuggestionLaunch")
.field("api_key", &"<redacted>")
.field("endpoint_identity", &endpoint_identity(&self.base_url))
.field("model", &self.model)
.finish()
}
}
#[must_use]
pub fn route_is_supported_suggestion_provider(provider: ApiProvider) -> bool {
matches!(provider, ApiProvider::Deepseek | ApiProvider::DeepseekCN)
}
fn resolve_credentials_for_identity(
config: &Config,
provider: ApiProvider,
provider_identity: &str,
model: &str,
) -> Option<SuggestionRouteCredentials> {
if !route_is_supported_suggestion_provider(provider) {
return None;
}
let identity = config.resolve_provider_identity(provider_identity).ok()?;
if identity.provider != provider {
return None;
}
let resolved =
crate::route_runtime::resolve_runtime_route_for_identity(config, &identity, Some(model))
.ok()?;
if resolved.identity.provider != provider {
return None;
}
if resolved
.config
.provider_config_for(provider)
.and_then(|route| route.path_suffix.as_ref())
.is_some()
{
return None;
}
let api_key = resolved.config.deepseek_api_key().ok()?;
Some(SuggestionRouteCredentials {
api_key,
base_url: resolved.candidate.endpoint().base_url.clone(),
model: resolved.model.clone(),
})
}
#[must_use]
pub fn capture_route_authority(route: &TurnRoute) -> Option<SuggestionRouteAuthority> {
if !route_is_supported_suggestion_provider(route.provider) {
return None;
}
let provider_identity = route.provider_identity.trim();
let model = route.model.trim();
if provider_identity.is_empty() || model.is_empty() {
return None;
}
let receipt = route.receipt.as_ref()?;
if receipt.provider() != route.provider
|| receipt.provider_identity() != provider_identity
|| receipt.wire_model() != model
|| receipt.endpoint_identity().is_empty()
|| receipt.credential_generation().is_empty()
{
return None;
}
Some(SuggestionRouteAuthority {
receipt: receipt.clone(),
})
}
pub fn plan_suggestion_launch<F>(
turn_completed: bool,
suggestion_enabled: bool,
api_message_count: usize,
completed_route: Option<SuggestionRouteSnapshot<'_>>,
resolve_route_credentials: F,
) -> Option<SuggestionLaunch>
where
F: FnOnce(&SuggestionRouteSnapshot<'_>) -> Option<SuggestionRouteCredentials>,
{
if !turn_completed || !suggestion_enabled || api_message_count < 2 {
return None;
}
let route = completed_route?;
if !route_is_supported_suggestion_provider(route.provider) {
return None;
}
let identity = route.provider_identity.trim();
if identity.is_empty() {
return None;
}
let model = route.model.trim();
if model.is_empty() {
return None;
}
let authority = route.authority;
if authority.provider() != route.provider
|| authority.provider_identity() != identity
|| authority.model() != model
|| authority.endpoint_identity().is_empty()
{
return None;
}
let actual_endpoint = endpoint_identity(route.actual_base_url?);
if actual_endpoint.is_empty() || actual_endpoint != authority.endpoint_identity() {
return None;
}
let credentials = resolve_route_credentials(&route)?;
if credentials.api_key.trim().is_empty() {
return None;
}
if credentials.model.trim() != model {
return None;
}
if !authority.authorizes(&credentials.base_url, &credentials.api_key) {
return None;
}
Some(SuggestionLaunch {
api_key: credentials.api_key,
base_url: credentials.base_url,
model: model.to_string(),
})
}
#[must_use]
pub fn plan_suggestion_launch_with_config(
config: &Config,
turn_completed: bool,
suggestion_enabled: bool,
api_message_count: usize,
completed_route: Option<SuggestionRouteSnapshot<'_>>,
) -> Option<SuggestionLaunch> {
plan_suggestion_launch(
turn_completed,
suggestion_enabled,
api_message_count,
completed_route,
|route| {
resolve_credentials_for_identity(
config,
route.provider,
route.provider_identity.trim(),
route.model.trim(),
)
},
)
}
fn suggestion_client() -> &'static reqwest::Client {
static CLIENT: OnceLock<reqwest::Client> = OnceLock::new();
CLIENT.get_or_init(crate::tls::reqwest_client)
}
pub async fn generate_suggestion(
api_key: &str,
base_url: &str,
model: &str,
recent_messages: &str,
) -> Option<String> {
let client = suggestion_client();
let body = serde_json::json!({
"model": model,
"messages": [
{
"role": "system",
"content": "\
You are a helpful assistant. Based on the recent conversation context, generate \
ONE short follow-up question (under 60 characters) the user might want to ask \
next. Reply with ONLY the question text, nothing else — no quotes, no explanations, \
no prefixes."
},
{
"role": "user",
"content": format!(
"Recent conversation:\n{recent_messages}\n\n\
Generate ONE short follow-up question the user might ask next:"
)
}
],
"max_tokens": 64,
"temperature": 0.3,
"stream": false
});
let url = crate::client::api_url(base_url, "chat/completions");
debug!(
endpoint = %endpoint_identity(&url),
%model,
"generating prompt suggestion"
);
let response = match client
.post(&url)
.header(AUTHORIZATION, format!("Bearer {api_key}"))
.header(CONTENT_TYPE, "application/json")
.timeout(std::time::Duration::from_secs(10))
.json(&body)
.send()
.await
{
Ok(r) => r,
Err(_) => return None,
};
let value: Value = match response.json().await {
Ok(v) => v,
Err(_) => return None,
};
let suggestion = value["choices"][0]["message"]["content"]
.as_str()
.map(|s| s.trim().trim_matches('"').to_string())
.filter(|s| !s.is_empty() && s.len() <= 200)?;
debug!(
chars = suggestion.chars().count(),
"prompt suggestion generated"
);
Some(suggestion)
}
fn message_summary(m: &crate::models::Message) -> Option<String> {
let role = match m.role.as_str() {
"user" => "User",
"assistant" => "Assistant",
_ => return None,
};
let text = m
.content
.iter()
.filter_map(|block| match block {
crate::models::ContentBlock::Text { text, .. } => Some(text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join(" ");
let first_line = text.lines().next().unwrap_or("").trim();
if first_line.is_empty() {
return None;
}
let truncated: String = first_line
.chars()
.take(120)
.chain(if first_line.chars().count() > 120 {
Some('…')
} else {
None
})
.collect();
Some(format!("{role}: {truncated}"))
}
pub fn summarize_recent_messages(messages: &[crate::models::Message], limit: usize) -> String {
let start = messages.len().saturating_sub(limit);
messages[start..]
.iter()
.filter_map(message_summary)
.collect::<Vec<_>>()
.join("\n")
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use super::{
ApiProvider, Config, SuggestionRouteAuthority, SuggestionRouteCredentials,
SuggestionRouteSnapshot, TurnRoute, TurnRouteReceipt, capture_route_authority,
endpoint_identity, plan_suggestion_launch, plan_suggestion_launch_with_config,
resolve_credentials_for_identity,
};
use crate::config::ProvidersConfig;
use crate::test_support::{EnvVarGuard, TestEnvLock, lock_test_env};
const DEEPSEEK_BASE: &str = "https://api.deepseek.com/v1";
const DEEPSEEK_KEY: &str = "sk-deepseek-secret";
struct RecordingResolver {
available: Vec<(ApiProvider, &'static str, SuggestionRouteCredentials)>,
asked: RefCell<Vec<(ApiProvider, String)>>,
}
impl RecordingResolver {
fn new(available: Vec<(ApiProvider, &'static str, SuggestionRouteCredentials)>) -> Self {
Self {
available,
asked: RefCell::new(Vec::new()),
}
}
fn resolve(
&self,
route: &SuggestionRouteSnapshot<'_>,
) -> Option<SuggestionRouteCredentials> {
self.asked
.borrow_mut()
.push((route.provider, route.provider_identity.to_string()));
self.available
.iter()
.find(|(provider, identity, _)| {
*provider == route.provider && *identity == route.provider_identity
})
.map(|(_, _, credentials)| credentials.clone())
}
fn asked(&self) -> Vec<(ApiProvider, String)> {
self.asked.borrow().clone()
}
}
fn credentials(api_key: &str, base_url: &str, model: &str) -> SuggestionRouteCredentials {
SuggestionRouteCredentials {
api_key: api_key.to_string(),
base_url: base_url.to_string(),
model: model.to_string(),
}
}
fn deepseek_credentials(model: &str) -> SuggestionRouteCredentials {
credentials(DEEPSEEK_KEY, DEEPSEEK_BASE, model)
}
fn receipt(
provider: ApiProvider,
identity: &str,
model: &str,
base_url: &str,
api_key: &str,
) -> TurnRouteReceipt {
TurnRouteReceipt::new(provider, identity, model, base_url, api_key)
}
fn route_authority(
provider: ApiProvider,
identity: &str,
model: &str,
base_url: &str,
api_key: &str,
) -> SuggestionRouteAuthority {
SuggestionRouteAuthority::from_receipt_for_test(receipt(
provider, identity, model, base_url, api_key,
))
}
fn deepseek_authority(model: &str) -> SuggestionRouteAuthority {
route_authority(
ApiProvider::Deepseek,
"deepseek",
model,
DEEPSEEK_BASE,
DEEPSEEK_KEY,
)
}
fn snapshot<'a>(
provider: ApiProvider,
identity: &'a str,
model: &'a str,
authority: &'a SuggestionRouteAuthority,
) -> SuggestionRouteSnapshot<'a> {
SuggestionRouteSnapshot {
provider,
provider_identity: identity,
model,
authority,
actual_base_url: Some(DEEPSEEK_BASE),
}
}
#[test]
fn deepseek_route_uses_its_exact_wire_model_and_base_url() {
let resolver = RecordingResolver::new(vec![(
ApiProvider::Deepseek,
"deepseek",
deepseek_credentials("deepseek-reasoner"),
)]);
let authority = deepseek_authority("deepseek-reasoner");
let launch = plan_suggestion_launch(
true,
true,
2,
Some(snapshot(
ApiProvider::Deepseek,
"deepseek",
"deepseek-reasoner",
&authority,
)),
|route| resolver.resolve(route),
)
.expect("supported deepseek route with unchanged credentials must launch");
assert_eq!(launch.model, "deepseek-reasoner");
assert_eq!(launch.base_url, DEEPSEEK_BASE);
assert_eq!(launch.api_key, DEEPSEEK_KEY);
}
#[test]
fn non_deepseek_completion_never_touches_deepseek_credentials() {
let resolver = RecordingResolver::new(vec![(
ApiProvider::Deepseek,
"deepseek",
deepseek_credentials("deepseek-chat"),
)]);
for (provider, identity, model) in [
(ApiProvider::Anthropic, "anthropic", "claude-sonnet-4"),
(ApiProvider::Openai, "openai", "gpt-4.1"),
(ApiProvider::Openrouter, "openrouter", "some/model"),
(ApiProvider::Custom, "lm-studio", "local-model"),
(
ApiProvider::DeepseekAnthropic,
"deepseek-anthropic",
"deepseek-chat",
),
] {
let authority = route_authority(provider, identity, model, DEEPSEEK_BASE, DEEPSEEK_KEY);
let launch = plan_suggestion_launch(
true,
true,
8,
Some(snapshot(provider, identity, model, &authority)),
|route| resolver.resolve(route),
);
assert!(
launch.is_none(),
"{provider:?} completion must not launch a prompt suggestion"
);
}
assert!(
resolver.asked().is_empty(),
"credential resolution must never be attempted for unsupported routes, got {:?}",
resolver.asked()
);
}
#[test]
fn missing_credentials_fail_closed() {
let authority = deepseek_authority("deepseek-chat");
let empty = RecordingResolver::new(Vec::new());
assert!(
plan_suggestion_launch(
true,
true,
2,
Some(snapshot(
ApiProvider::Deepseek,
"deepseek",
"deepseek-chat",
&authority
)),
|route| empty.resolve(route),
)
.is_none(),
"unresolvable route credentials must fail closed"
);
for incomplete in [
credentials(" ", DEEPSEEK_BASE, "deepseek-chat"),
credentials(DEEPSEEK_KEY, "", "deepseek-chat"),
] {
assert!(
plan_suggestion_launch(
true,
true,
2,
Some(snapshot(
ApiProvider::Deepseek,
"deepseek",
"deepseek-chat",
&authority
)),
|_| Some(incomplete.clone()),
)
.is_none(),
"incomplete credentials must fail closed: {incomplete:?}"
);
}
}
#[test]
fn resolver_is_asked_only_about_the_completed_route_identity() {
const CN_BASE: &str = "https://api.deepseek.cn/v1";
let resolver = RecordingResolver::new(vec![
(
ApiProvider::Deepseek,
"deepseek",
deepseek_credentials("deepseek-chat"),
),
(
ApiProvider::DeepseekCN,
"deepseek-cn",
credentials("sk-cn", CN_BASE, "deepseek-chat"),
),
]);
let authority = route_authority(
ApiProvider::DeepseekCN,
"deepseek-cn",
"deepseek-chat",
CN_BASE,
"sk-cn",
);
let launch = plan_suggestion_launch(
true,
true,
4,
Some(SuggestionRouteSnapshot {
provider: ApiProvider::DeepseekCN,
provider_identity: "deepseek-cn",
model: "deepseek-chat",
authority: &authority,
actual_base_url: Some(CN_BASE),
}),
|route| resolver.resolve(route),
)
.expect("completed deepseek-cn route must launch on its own endpoint");
assert_eq!(launch.base_url, CN_BASE);
assert_eq!(launch.api_key, "sk-cn");
assert_eq!(
resolver.asked(),
vec![(ApiProvider::DeepseekCN, "deepseek-cn".to_string())],
"only the completed route identity may be inspected"
);
}
#[test]
fn model_substitution_by_the_resolver_fails_closed() {
let authority = deepseek_authority("deepseek-reasoner");
assert!(
plan_suggestion_launch(
true,
true,
2,
Some(snapshot(
ApiProvider::Deepseek,
"deepseek",
"deepseek-reasoner",
&authority
)),
|_| Some(deepseek_credentials("deepseek-chat")),
)
.is_none(),
"a resolver-substituted model must not be dispatched"
);
}
#[test]
fn same_identity_endpoint_or_key_rotation_fails_closed() {
let authority = deepseek_authority("deepseek-chat");
assert!(
plan_suggestion_launch(
true,
true,
2,
Some(snapshot(
ApiProvider::Deepseek,
"deepseek",
"deepseek-chat",
&authority
)),
|_| Some(credentials(
DEEPSEEK_KEY,
"https://exfil.example.com/v1",
"deepseek-chat"
)),
)
.is_none(),
"a same-identity endpoint mutation must fail closed"
);
assert!(
plan_suggestion_launch(
true,
true,
2,
Some(snapshot(
ApiProvider::Deepseek,
"deepseek",
"deepseek-chat",
&authority
)),
|_| Some(credentials(
"sk-rotated-elsewhere",
DEEPSEEK_BASE,
"deepseek-chat"
)),
)
.is_none(),
"a same-identity credential mutation must fail closed"
);
}
#[test]
fn userinfo_rotation_behind_identical_redaction_fails_closed() {
const ORIGINAL: &str = "https://svc:original@api.deepseek.com/v1";
const ROTATED: &str = "https://svc:rotated@api.deepseek.com/v1";
assert_eq!(endpoint_identity(ORIGINAL), endpoint_identity(ROTATED));
let authority = route_authority(
ApiProvider::Deepseek,
"deepseek",
"deepseek-chat",
ORIGINAL,
DEEPSEEK_KEY,
);
let route = SuggestionRouteSnapshot {
provider: ApiProvider::Deepseek,
provider_identity: "deepseek",
model: "deepseek-chat",
authority: &authority,
actual_base_url: Some(ORIGINAL),
};
assert!(
plan_suggestion_launch(true, true, 2, Some(route), |_| Some(credentials(
DEEPSEEK_KEY,
ROTATED,
"deepseek-chat"
)))
.is_none(),
"a URL-userinfo rotation hidden by redaction must fail closed"
);
assert!(
plan_suggestion_launch(true, true, 2, Some(route), |_| Some(credentials(
DEEPSEEK_KEY,
ORIGINAL,
"deepseek-chat"
)))
.is_some(),
"control: the unrotated endpoint still launches"
);
}
#[test]
fn actual_turn_endpoint_must_be_present_and_match_the_authority() {
let authority = deepseek_authority("deepseek-chat");
for actual_base_url in [None, Some("https://exfil.example.com/v1"), Some(" ")] {
let route = SuggestionRouteSnapshot {
provider: ApiProvider::Deepseek,
provider_identity: "deepseek",
model: "deepseek-chat",
authority: &authority,
actual_base_url,
};
assert!(
plan_suggestion_launch(true, true, 2, Some(route), |_| Some(deepseek_credentials(
"deepseek-chat"
)))
.is_none(),
"actual turn endpoint {actual_base_url:?} must fail closed"
);
}
let route = SuggestionRouteSnapshot {
provider: ApiProvider::Deepseek,
provider_identity: "deepseek",
model: "deepseek-chat",
authority: &authority,
actual_base_url: Some("https://api.deepseek.com/v1/"),
};
assert!(
plan_suggestion_launch(true, true, 2, Some(route), |_| Some(deepseek_credentials(
"deepseek-chat"
)))
.is_some(),
"trailing-slash normalization must not break the exact-route match"
);
}
#[test]
fn authority_from_a_different_route_fails_closed() {
let cn = route_authority(
ApiProvider::DeepseekCN,
"deepseek-cn",
"deepseek-chat",
"https://api.deepseek.cn/v1",
"sk-cn",
);
let route = SuggestionRouteSnapshot {
provider: ApiProvider::Deepseek,
provider_identity: "deepseek",
model: "deepseek-chat",
authority: &cn,
actual_base_url: Some(DEEPSEEK_BASE),
};
assert!(
plan_suggestion_launch(true, true, 2, Some(route), |_| Some(deepseek_credentials(
"deepseek-chat"
)))
.is_none(),
"an authority captured for another route must fail closed"
);
}
#[test]
fn missing_route_snapshot_or_disabled_gates_produce_no_request() {
let resolver = RecordingResolver::new(vec![(
ApiProvider::Deepseek,
"deepseek",
deepseek_credentials("deepseek-chat"),
)]);
let authority = deepseek_authority("deepseek-chat");
let route = snapshot(
ApiProvider::Deepseek,
"deepseek",
"deepseek-chat",
&authority,
);
assert!(plan_suggestion_launch(true, true, 4, None, |r| resolver.resolve(r)).is_none());
assert!(
plan_suggestion_launch(false, true, 4, Some(route), |r| resolver.resolve(r)).is_none()
);
assert!(
plan_suggestion_launch(true, false, 4, Some(route), |r| resolver.resolve(r)).is_none()
);
assert!(
plan_suggestion_launch(true, true, 1, Some(route), |r| resolver.resolve(r)).is_none()
);
let empty_identity = route_authority(
ApiProvider::Deepseek,
" ",
"deepseek-chat",
DEEPSEEK_BASE,
DEEPSEEK_KEY,
);
assert!(
plan_suggestion_launch(
true,
true,
4,
Some(snapshot(
ApiProvider::Deepseek,
" ",
"deepseek-chat",
&empty_identity
)),
|r| resolver.resolve(r),
)
.is_none()
);
let empty_model = route_authority(
ApiProvider::Deepseek,
"deepseek",
"",
DEEPSEEK_BASE,
DEEPSEEK_KEY,
);
assert!(
plan_suggestion_launch(
true,
true,
4,
Some(snapshot(
ApiProvider::Deepseek,
"deepseek",
"",
&empty_model
)),
|r| resolver.resolve(r),
)
.is_none()
);
assert!(
resolver.asked().is_empty(),
"gates must reject before credential resolution, got {:?}",
resolver.asked()
);
}
#[test]
fn debug_never_renders_credential_material_or_raw_urls() {
let secret_base = format!(
"https://{}:{}@api.deepseek.com/v1?api_key={}{}&token={}{}®ion=us-east",
"svc-user", "hunter2", "sk", "-live-abc123", "tok", "-secret-xyz"
);
let secrets = [
DEEPSEEK_KEY.to_string(),
"svc-user".to_string(),
"hunter2".to_string(),
["sk", "-live-abc123"].concat(),
["tok", "-secret-xyz"].concat(),
];
let credentials = credentials(DEEPSEEK_KEY, &secret_base, "deepseek-chat");
let authority = route_authority(
ApiProvider::Deepseek,
"deepseek",
"deepseek-chat",
&secret_base,
DEEPSEEK_KEY,
);
let route = SuggestionRouteSnapshot {
provider: ApiProvider::Deepseek,
provider_identity: "deepseek",
model: "deepseek-chat",
authority: &authority,
actual_base_url: Some(&secret_base),
};
let launch =
plan_suggestion_launch(true, true, 2, Some(route), |_| Some(credentials.clone()))
.expect("unchanged route must launch");
for rendered in [
format!("{credentials:?}"),
format!("{credentials:#?}"),
format!("{launch:?}"),
format!("{launch:#?}"),
format!("{authority:?}"),
format!("{authority:#?}"),
format!("{route:?}"),
format!("{route:#?}"),
] {
for secret in &secrets {
assert!(
!rendered.contains(secret),
"a rendered surface leaked {secret}: {rendered}"
);
}
assert!(
rendered.contains("api.deepseek.com"),
"endpoint identity must survive redaction: {rendered}"
);
assert!(
rendered.contains("region=us-east"),
"non-sensitive query values must survive redaction: {rendered}"
);
}
for rendered in [format!("{credentials:?}"), format!("{launch:?}")] {
assert!(
rendered.contains("<redacted>"),
"Debug must mark the redacted field: {rendered}"
);
}
assert_eq!(launch.base_url, secret_base);
assert_eq!(launch.api_key, DEEPSEEK_KEY);
}
struct SealedDeepseekEnv {
_guards: Vec<EnvVarGuard>,
_lock: TestEnvLock,
}
fn seal_deepseek_env() -> SealedDeepseekEnv {
let lock = lock_test_env();
let guards = [
"DEEPSEEK_API_KEY",
"DEEPSEEK_API_KEY_SOURCE",
"CODEWHALE_CLI_API_KEY",
"DEEPSEEK_BASE_URL",
"CODEWHALE_BASE_URL",
]
.into_iter()
.map(EnvVarGuard::remove)
.collect();
SealedDeepseekEnv {
_guards: guards,
_lock: lock,
}
}
fn deepseek_config(api_key: &str, base_url: &str) -> Config {
let mut config = Config {
provider: Some("deepseek".to_string()),
..Config::default()
};
let providers = config
.providers
.get_or_insert_with(ProvidersConfig::default);
providers.deepseek.api_key = Some(api_key.to_string());
providers.deepseek.base_url = Some(base_url.to_string());
config
}
fn deepseek_turn_route(config: &Config) -> TurnRoute {
let identity = config
.resolve_provider_identity("deepseek")
.expect("test config must expose the deepseek identity");
let resolved = crate::route_runtime::resolve_runtime_route_for_identity(
config,
&identity,
Some(crate::config::DEFAULT_TEXT_MODEL),
)
.expect("test config must resolve the deepseek route");
let model = resolved.model.clone();
let validated = resolved
.validate()
.expect("test config must preflight a deepseek client");
TurnRoute {
provider: ApiProvider::Deepseek,
provider_identity: "deepseek".to_string(),
model,
auto_model: false,
receipt: Some(validated.client.turn_route_receipt("deepseek")),
billing: Some(crate::core::events::RouteBillingEnvelope {
billing_surface: None,
endpoint_fingerprint: None,
billing_mode: crate::cost_status::RouteBillingMode::Unknown,
dispatched_at: chrono::Utc::now(),
}),
base_url: crate::config::DEFAULT_DEEPSEEK_BASE_URL.to_string(),
billing_product: crate::route_billing::RouteProduct::Unproven,
}
}
fn deepseek_actual_base_url(config: &Config, route: &TurnRoute) -> String {
resolve_credentials_for_identity(
config,
route.provider,
&route.provider_identity,
&route.model,
)
.expect("test config must resolve the deepseek route")
.base_url
}
fn route_mismatch_report(config: &Config, snapshot: &SuggestionRouteSnapshot<'_>) -> String {
let resolved = resolve_credentials_for_identity(
config,
snapshot.provider,
snapshot.provider_identity.trim(),
snapshot.model.trim(),
);
let credential_matches = resolved.as_ref().map(|credentials| {
snapshot
.authority
.authorizes(&credentials.base_url, &credentials.api_key)
});
format!(
"snapshot={snapshot:?}, resolved={resolved:?}, \
resolved_authorized_by_receipt={credential_matches:?}"
)
}
fn config_snapshot<'a>(
route: &'a TurnRoute,
authority: &'a SuggestionRouteAuthority,
actual_base_url: &'a str,
) -> SuggestionRouteSnapshot<'a> {
SuggestionRouteSnapshot {
provider: route.provider,
provider_identity: route.provider_identity.as_str(),
model: route.model.as_str(),
authority,
actual_base_url: Some(actual_base_url),
}
}
#[test]
fn config_exact_unchanged_completed_route_launches() {
let _env = seal_deepseek_env();
let config = deepseek_config(DEEPSEEK_KEY, DEEPSEEK_BASE);
let route = deepseek_turn_route(&config);
let actual_base_url = deepseek_actual_base_url(&config, &route);
let authority =
capture_route_authority(&route).expect("deepseek turn must capture authority");
let snapshot = config_snapshot(&route, &authority, &actual_base_url);
let launch = plan_suggestion_launch_with_config(&config, true, true, 4, Some(snapshot))
.unwrap_or_else(|| {
panic!(
"an unchanged deepseek route must launch: {}",
route_mismatch_report(&config, &snapshot)
)
});
assert_eq!(launch.base_url, actual_base_url);
assert_eq!(launch.api_key, DEEPSEEK_KEY);
assert_eq!(launch.model, route.model);
assert!(
launch.base_url.contains("api.deepseek.com"),
"unexpected endpoint host: {}",
endpoint_identity(&launch.base_url)
);
}
#[test]
fn configured_chat_path_override_fails_closed() {
let _env = seal_deepseek_env();
let mut config = deepseek_config(DEEPSEEK_KEY, DEEPSEEK_BASE);
config
.providers
.as_mut()
.expect("providers")
.deepseek
.path_suffix = Some("/private/chat".to_string());
let route = deepseek_turn_route(&config);
let authority =
capture_route_authority(&route).expect("deepseek turn must capture authority");
let identity = config
.resolve_provider_identity("deepseek")
.expect("deepseek identity");
let actual_base_url = crate::route_runtime::resolve_runtime_route_for_identity(
&config,
&identity,
Some(&route.model),
)
.expect("resolved route")
.candidate
.endpoint()
.base_url
.clone();
let snapshot = config_snapshot(&route, &authority, &actual_base_url);
assert!(
plan_suggestion_launch_with_config(&config, true, true, 4, Some(snapshot)).is_none(),
"the suggestion helper must not bypass a provider-specific path contract"
);
config
.providers
.as_mut()
.expect("providers")
.deepseek
.path_suffix = Some(" ".to_string());
assert!(
resolve_credentials_for_identity(
&config,
route.provider,
&route.provider_identity,
&route.model,
)
.is_none(),
"even a blank configured suffix is an installed transport override"
);
}
#[test]
fn config_mutated_before_turn_started_cannot_move_authority_off_route_a() {
let _env = seal_deepseek_env();
const KEY_A: &str = "sk-route-a-secret";
const BASE_A: &str = "https://api.deepseek.com/v1";
let config_a = deepseek_config(KEY_A, BASE_A);
let route = deepseek_turn_route(&config_a);
let actual_base_url = deepseek_actual_base_url(&config_a, &route);
const KEY_B: &str = "sk-route-b-attacker";
const BASE_B: &str = "https://exfil.example.com/v1";
let config_b = deepseek_config(KEY_B, BASE_B);
let authority =
capture_route_authority(&route).expect("deepseek turn must capture authority");
assert_eq!(
authority.endpoint_identity(),
endpoint_identity(&actual_base_url),
"authority must describe route A, not whatever config says now"
);
assert!(
!authority.endpoint_identity().contains("exfil.example.com"),
"authority leaked onto route B: {}",
authority.endpoint_identity()
);
assert!(
!authority.authorizes(BASE_B, KEY_B),
"route B must not be authorized by route A's receipt"
);
assert!(
authority.authorizes(&actual_base_url, KEY_A),
"route A must still authorize itself"
);
let snapshot = config_snapshot(&route, &authority, &actual_base_url);
assert!(
plan_suggestion_launch_with_config(&config_b, true, true, 4, Some(snapshot)).is_none(),
"completed-turn context must not be dispatchable under the mutated config"
);
let key_only_mutation = deepseek_config(KEY_B, BASE_A);
assert!(
plan_suggestion_launch_with_config(&key_only_mutation, true, true, 4, Some(snapshot))
.is_none(),
"a credential-only mutation must fail closed too"
);
let launch = plan_suggestion_launch_with_config(&config_a, true, true, 4, Some(snapshot))
.unwrap_or_else(|| {
panic!(
"route A must still launch on itself: {}",
route_mismatch_report(&config_a, &snapshot)
)
});
assert_eq!(launch.api_key, KEY_A);
assert_eq!(launch.base_url, actual_base_url);
assert_ne!(launch.api_key, KEY_B);
assert!(!launch.base_url.contains("exfil.example.com"));
}
#[test]
fn config_same_identity_base_url_mutation_fails_closed() {
let _env = seal_deepseek_env();
let config = deepseek_config(DEEPSEEK_KEY, DEEPSEEK_BASE);
let route = deepseek_turn_route(&config);
let actual_base_url = deepseek_actual_base_url(&config, &route);
let authority =
capture_route_authority(&route).expect("deepseek turn must capture authority");
let mutated = deepseek_config(DEEPSEEK_KEY, "https://exfil.example.com/v1");
assert!(
plan_suggestion_launch_with_config(
&mutated,
true,
true,
4,
Some(config_snapshot(&route, &authority, &actual_base_url)),
)
.is_none(),
"a same-identity endpoint mutation must send no context anywhere"
);
}
#[test]
fn config_same_identity_api_key_mutation_fails_closed() {
let _env = seal_deepseek_env();
let config = deepseek_config(DEEPSEEK_KEY, DEEPSEEK_BASE);
let route = deepseek_turn_route(&config);
let actual_base_url = deepseek_actual_base_url(&config, &route);
let authority =
capture_route_authority(&route).expect("deepseek turn must capture authority");
let mutated = deepseek_config("sk-attacker-rotated", DEEPSEEK_BASE);
assert_eq!(
deepseek_actual_base_url(&mutated, &route),
actual_base_url,
"this test must isolate the credential rotation, not an endpoint change"
);
assert!(
plan_suggestion_launch_with_config(
&mutated,
true,
true,
4,
Some(config_snapshot(&route, &authority, &actual_base_url)),
)
.is_none(),
"a same-identity credential mutation must send no context anywhere"
);
}
#[test]
fn config_selection_switch_cannot_redirect_the_completed_route() {
let _env = seal_deepseek_env();
let config = deepseek_config(DEEPSEEK_KEY, DEEPSEEK_BASE);
let route = deepseek_turn_route(&config);
let actual_base_url = deepseek_actual_base_url(&config, &route);
let authority =
capture_route_authority(&route).expect("deepseek turn must capture authority");
let mut switched = deepseek_config(DEEPSEEK_KEY, DEEPSEEK_BASE);
switched.provider = Some("openai".to_string());
{
let providers = switched
.providers
.get_or_insert_with(ProvidersConfig::default);
providers.openai.api_key = Some("sk-openai-secret".to_string());
providers.openai.base_url = Some("https://api.openai.com/v1".to_string());
}
let snapshot = config_snapshot(&route, &authority, &actual_base_url);
let launch = plan_suggestion_launch_with_config(&switched, true, true, 4, Some(snapshot))
.unwrap_or_else(|| {
panic!(
"the completed deepseek route stays valid across a selection switch: {}",
route_mismatch_report(&switched, &snapshot)
)
});
assert_eq!(launch.base_url, actual_base_url);
assert_eq!(launch.api_key, DEEPSEEK_KEY);
assert_ne!(launch.api_key, "sk-openai-secret");
assert!(!launch.base_url.contains("openai"));
}
#[test]
fn config_unsupported_providers_capture_no_authority() {
let _env = seal_deepseek_env();
let config = deepseek_config(DEEPSEEK_KEY, DEEPSEEK_BASE);
for (provider, identity, model) in [
(ApiProvider::Anthropic, "anthropic", "claude-sonnet-4"),
(ApiProvider::Openai, "openai", "gpt-4.1"),
(ApiProvider::Openrouter, "openrouter", "some/model"),
(ApiProvider::Custom, "lm-studio", "local-model"),
(
ApiProvider::DeepseekAnthropic,
"deepseek-anthropic",
"deepseek-chat",
),
] {
let route = TurnRoute {
provider,
provider_identity: identity.to_string(),
model: model.to_string(),
auto_model: false,
receipt: Some(receipt(
provider,
identity,
model,
DEEPSEEK_BASE,
DEEPSEEK_KEY,
)),
billing: Some(crate::core::events::RouteBillingEnvelope {
billing_surface: None,
endpoint_fingerprint: None,
billing_mode: crate::cost_status::RouteBillingMode::Unknown,
dispatched_at: chrono::Utc::now(),
}),
base_url: DEEPSEEK_BASE.to_string(),
billing_product: crate::route_billing::RouteProduct::Unproven,
};
assert!(
capture_route_authority(&route).is_none(),
"{provider:?} must not capture a suggestion authority"
);
}
assert!(
resolve_credentials_for_identity(
&config,
ApiProvider::DeepseekAnthropic,
"deepseek-anthropic",
"deepseek-chat",
)
.is_none(),
"DeepseekAnthropic must never reach a credential lookup"
);
}
#[test]
fn route_without_a_receipt_captures_no_authority() {
let _env = seal_deepseek_env();
let config = deepseek_config(DEEPSEEK_KEY, DEEPSEEK_BASE);
let mut route = deepseek_turn_route(&config);
route.receipt = None;
assert!(
capture_route_authority(&route).is_none(),
"a turn with no installed-client receipt has no provenance to trust"
);
}
#[test]
fn receipt_describing_another_route_captures_no_authority() {
let _env = seal_deepseek_env();
let config = deepseek_config(DEEPSEEK_KEY, DEEPSEEK_BASE);
let mut route = deepseek_turn_route(&config);
route.receipt = Some(receipt(
ApiProvider::Deepseek,
"deepseek",
"some-other-model",
DEEPSEEK_BASE,
DEEPSEEK_KEY,
));
assert!(capture_route_authority(&route).is_none());
route.receipt = Some(receipt(
ApiProvider::DeepseekCN,
"deepseek-cn",
&route.model,
DEEPSEEK_BASE,
DEEPSEEK_KEY,
));
assert!(capture_route_authority(&route).is_none());
}
#[test]
fn config_requires_the_endpoint_the_turn_actually_used() {
let _env = seal_deepseek_env();
let config = deepseek_config(DEEPSEEK_KEY, DEEPSEEK_BASE);
let route = deepseek_turn_route(&config);
let actual_base_url = deepseek_actual_base_url(&config, &route);
let authority =
capture_route_authority(&route).expect("deepseek turn must capture authority");
let baseline = config_snapshot(&route, &authority, &actual_base_url);
assert!(
plan_suggestion_launch_with_config(&config, true, true, 4, Some(baseline)).is_some(),
"baseline route must launch: {}",
route_mismatch_report(&config, &baseline)
);
let mut snapshot = config_snapshot(&route, &authority, &actual_base_url);
snapshot.actual_base_url = Some("https://exfil.example.com/v1");
assert!(
plan_suggestion_launch_with_config(&config, true, true, 4, Some(snapshot)).is_none(),
"a completed turn on a different endpoint must fail closed"
);
let mut snapshot = config_snapshot(&route, &authority, &actual_base_url);
snapshot.actual_base_url = None;
assert!(
plan_suggestion_launch_with_config(&config, true, true, 4, Some(snapshot)).is_none(),
"an absent turn endpoint must fail closed"
);
}
}