use super::OpenAiClient;
use crate::shared::api::contract::{
ChatChunk, ChatRequest, EngineBackend, FinishReason, VisionSupport,
};
use crate::shared::api::sse_stub::{collect, hello, muted};
const BASE: &str = "https://api.deepseek.com/v1";
const MODELS: &str = "deepseek-flash,deepseek-v4-pro";
fn clients() -> Option<Vec<(String, OpenAiClient)>> {
let Some(key) = std::env::var("MINDFORK_DEEPSEEK_KEY")
.ok()
.filter(|k| !k.trim().is_empty())
else {
eprintln!("skip: MINDFORK_DEEPSEEK_KEY not set");
return None;
};
let models = std::env::var("MINDFORK_LIVE_DEEPSEEK_MODELS")
.ok()
.filter(|m| !m.trim().is_empty())
.unwrap_or_else(|| MODELS.to_string());
Some(
models
.split(',')
.map(str::trim)
.filter(|m| !m.is_empty())
.map(|model| {
let client = OpenAiClient::new(BASE)
.with_api_key(Some(key.clone()))
.with_model(Some(model.to_string()));
(model.to_string(), client)
})
.collect(),
)
}
fn switched_on() -> ChatRequest {
let mut req = hello();
req.sampling.thinking = Some(true);
req.sampling.max_tokens = Some(2048);
req
}
async fn completed(client: &OpenAiClient, model: &str, what: &str, req: ChatRequest) -> u32 {
let stream = client
.chat_stream(req, Default::default())
.await
.unwrap_or_else(|e| panic!("{model}: {what} was refused: {e}"));
let chunks = collect(stream).await;
let mut text = String::new();
let (mut finish, mut reasoning) = (None, 0);
for chunk in chunks {
match chunk {
ChatChunk::Text(t) => text.push_str(&t),
ChatChunk::Usage(u) => reasoning = u.reasoning_tokens,
ChatChunk::Error { message, .. } => panic!("{model}: {what}: {message}"),
ChatChunk::Finished(r) => finish = Some(r),
_ => {}
}
}
println!(
"{model}: {what}: finish={finish:?} reasoning={reasoning} typed={} reply={}",
client.thinking_learned(),
text.trim()
);
assert!(
finish == Some(FinishReason::Stop) && !text.trim().is_empty(),
"{model}: {what}: finish={finish:?} text={text:?}"
);
reasoning
}
#[tokio::test]
#[ignore = "requires MINDFORK_DEEPSEEK_KEY (live DeepSeek API)"]
async fn a_muted_turn_and_a_thinking_turn_complete_on_deepseek() {
let Some(clients) = clients() else {
return;
};
for (model, client) in clients {
assert!(!client.thinking_learned(), "a fresh client knows nothing");
let reasoned = completed(&client, &model, "muted turn", muted()).await;
assert!(
client.thinking_learned(),
"{model}: the refusal of the boolean taught the object"
);
assert_eq!(reasoned, 0, "{model}: a muted turn must not reason");
completed(&client, &model, "thinking turn", switched_on()).await;
}
}
#[tokio::test]
#[ignore = "requires MINDFORK_DEEPSEEK_KEY (live DeepSeek API)"]
async fn the_first_turn_of_a_new_engine_completes_on_deepseek() {
let Some(clients) = clients() else {
return;
};
for (model, client) in clients {
completed(&client, &model, "first turn", switched_on()).await;
assert!(client.thinking_learned(), "{model}: learned on the way");
}
}
fn in_the_mode() -> Option<Vec<(String, OpenAiClient)>> {
Some(
clients()?
.into_iter()
.map(|(model, client)| (model, client.for_deepseek()))
.collect(),
)
}
#[tokio::test]
#[ignore = "requires MINDFORK_DEEPSEEK_KEY (live DeepSeek API)"]
async fn the_mode_mutes_and_reasons_on_deepseek() {
let Some(clients) = in_the_mode() else {
return;
};
for (model, client) in clients {
assert!(client.thinking_learned(), "{model}: told, not learned");
let muted = completed(&client, &model, "muted turn", muted()).await;
assert_eq!(muted, 0, "{model}: the switch off must mean no reasoning");
let mut sum = switched_on();
sum.messages = vec![crate::shared::api::contract::ApiMessage::user(
"What is 17 multiplied by 23? Reply with the number alone.",
)];
let on = completed(&client, &model, "thinking turn", sum).await;
assert!(on > 0, "{model}: the switch on must reason");
}
}
#[tokio::test]
#[ignore = "requires MINDFORK_DEEPSEEK_KEY (live DeepSeek API)"]
async fn the_mode_reads_the_window_and_the_images_on_deepseek() {
let Some(clients) = in_the_mode() else {
return;
};
for (model, client) in clients {
let caps = client.model_capabilities().await;
let vision = client.vision().await;
println!("{model}: caps={caps:?} vision={vision:?}");
let window = caps.and_then(|c| c.context_length);
assert!(window.is_some_and(|w| w >= 65_536), "{model}: {window:?}");
let expected = match model.as_str() {
"deepseek-flash" => VisionSupport::Supported,
"deepseek-v4-pro" => VisionSupport::Unsupported,
_ => continue,
};
assert_eq!(vision, expected, "{model}");
}
}