use super::OpenAiClient;
use super::gateway_tests::{JSON, ONE_CHUNK, Route, SSE, Seen, muted, run, stub, turn};
use crate::entities::sampling::{ReasoningEffort, SamplingConfig};
use crate::shared::api::contract::{EngineBackend, VisionSupport};
const LIST: &str = r#"{"object":"list","data":[
{"id":"deepseek-flash","name":"DeepSeek-V4.1-Flash","context_window":1048576,
"max_output_tokens":393216,"input_modalities":["text","image"],"output_modalities":["text"],
"effort":{"supported_levels":["low","high","max"],"default_level":"high"}},
{"id":"deepseek-v4-pro","name":"DeepSeek-V4-Pro","context_window":1048576,
"max_output_tokens":393216,"input_modalities":["text"],"output_modalities":["text"],
"effort":{"supported_levels":["low","high","max"],"default_level":"high"}}
]}"#;
const CHAT: Route = ("/v1/chat/completions", "200 OK", SSE, ONE_CHUNK);
const MODELS: Route = ("/v1/models", "200 OK", JSON, LIST);
fn deepseek(url: String, model: &str) -> OpenAiClient {
OpenAiClient::new(url)
.with_api_key(Some("k".into()))
.with_model(Some(model.into()))
.for_deepseek()
}
fn bodies(seen: &[Seen]) -> Vec<serde_json::Value> {
seen.iter()
.filter(|s| s.path() == "/v1/chat/completions")
.map(Seen::json)
.collect()
}
#[tokio::test]
async fn a_muted_turn_is_the_typed_switch_off_and_no_effort() {
const ROUTES: &[Route] = &[MODELS, CHAT];
let (url, server) = stub(5, ROUTES);
let client = deepseek(url, "deepseek-flash");
run(&client, muted()).await;
for sampling in [
SamplingConfig {
reasoning_effort: Some(ReasoningEffort::None),
..Default::default()
},
SamplingConfig {
reasoning_effort: Some(ReasoningEffort::None),
thinking: Some(true),
..Default::default()
},
SamplingConfig {
thinking: Some(false),
..Default::default()
},
] {
run(&client, turn(sampling)).await;
}
let seen = server.join().unwrap();
let sent = bodies(&seen);
assert_eq!(sent.len(), 4, "one request a turn: nothing was refused");
for body in &sent {
assert_eq!(body["thinking"], serde_json::json!({"type": "disabled"}));
assert!(body.get("reasoning_effort").is_none(), "{body}");
}
assert!(
seen.iter()
.all(|s| matches!(s.path(), "/v1/chat/completions" | "/v1/models")),
"no llama.cpp, Ollama or LM Studio route is asked: {:?}",
seen.iter().map(Seen::path).collect::<Vec<_>>()
);
}
#[tokio::test]
async fn a_reasoning_turn_keeps_its_effort_and_types_the_switch_on() {
const ROUTES: &[Route] = &[MODELS, CHAT];
let (url, server) = stub(5, ROUTES);
let client = deepseek(url, "deepseek-flash");
for effort in [
ReasoningEffort::Medium,
ReasoningEffort::XHigh,
ReasoningEffort::Max,
] {
run(
&client,
turn(SamplingConfig {
reasoning_effort: Some(effort),
thinking: Some(true),
..Default::default()
}),
)
.await;
}
run(
&client,
turn(SamplingConfig {
thinking: Some(true),
..Default::default()
}),
)
.await;
let seen = server.join().unwrap();
let sent = bodies(&seen);
let efforts: Vec<_> = sent
.iter()
.map(|b| b["reasoning_effort"].as_str().unwrap_or("-"))
.collect();
assert_eq!(efforts, ["medium", "xhigh", "max", "-"]);
for body in &sent {
assert_eq!(body["thinking"], serde_json::json!({"type": "enabled"}));
}
}
#[tokio::test]
async fn the_window_and_the_images_are_read_from_deepseeks_list() {
const ROUTES: &[Route] = &[MODELS];
for (model, vision) in [
("deepseek-flash", VisionSupport::Supported),
("deepseek-v4-pro", VisionSupport::Unsupported),
] {
let (url, server) = stub(1, ROUTES);
let client = deepseek(url, model);
let caps = client
.model_capabilities()
.await
.expect("a catalogue answer");
assert_eq!(caps.context_length, Some(1_048_576), "{model}");
assert_eq!(caps.sampling_fields, None, "{model}: DeepSeek lists none");
assert_eq!(client.vision().await, vision, "{model}");
assert_eq!(
client.context_budget().await,
None,
"{model}: the window is the catalogue's, and no server route is asked"
);
let seen = server.join().unwrap();
assert_eq!(
seen.iter().map(Seen::path).collect::<Vec<_>>(),
["/v1/models"],
"{model}: the list once, nothing else"
);
}
}
#[tokio::test]
async fn external_at_the_same_list_reads_nothing_new() {
const ROUTES: &[Route] = &[MODELS];
let (url, server) = stub(2, ROUTES);
let client = OpenAiClient::new(url)
.with_api_key(Some("k".into()))
.with_model(Some("deepseek-v4-pro".into()));
assert_eq!(client.model_capabilities().await, None);
assert_eq!(client.vision().await, VisionSupport::Unknown);
let seen = server.join().unwrap();
assert_eq!(
seen.iter().map(Seen::path).collect::<Vec<_>>(),
["/v1/models", "/props"],
"external still asks llama.cpp's route"
);
}
#[test]
fn deepseeks_keys_of_another_shape_are_silence() {
let entry = |json: &str| -> super::wire::ModelEntry { serde_json::from_str(json).unwrap() };
assert_eq!(
entry(r#"{"id":"a","context_window":"big"}"#).deepseek_window(),
None
);
assert_eq!(
entry(r#"{"id":"a","context_window":0}"#).deepseek_window(),
None
);
assert_eq!(
entry(r#"{"id":"a","context_window":99999999999}"#).deepseek_window(),
None
);
assert_eq!(
entry(r#"{"id":"a","context_window":65536}"#).deepseek_window(),
Some(65536)
);
assert_eq!(
entry(r#"{"id":"a","input_modalities":[]}"#).takes_images_top_level(),
None
);
assert_eq!(
entry(r#"{"id":"a","input_modalities":["text",1]}"#).takes_images_top_level(),
None
);
assert_eq!(
entry(r#"{"id":"a","input_modalities":"text"}"#).takes_images_top_level(),
None
);
assert_eq!(
entry(r#"{"id":"a","input_modalities":["text"]}"#).takes_images_top_level(),
Some(false)
);
}
#[tokio::test]
async fn a_continuation_goes_to_the_beta_route_as_a_prefix_completion() {
use crate::shared::api::contract::{ApiMessage, ToolSchema};
const BETA_CHAT: Route = ("/beta/chat/completions", "200 OK", SSE, ONE_CHUNK);
const ROUTES: &[Route] = &[MODELS, CHAT, BETA_CHAT];
let (url, server) = stub(3, ROUTES);
let client = deepseek(url, "deepseek-flash");
let mut cut = turn(SamplingConfig {
thinking: Some(true),
..Default::default()
});
cut.messages.push(ApiMessage::assistant("Paris is"));
cut.continue_final = true;
cut.tools = vec![ToolSchema {
name: "t".into(),
description: "d".into(),
parameters: serde_json::json!({"type": "object"}),
}];
run(&client, cut).await;
run(
&client,
turn(SamplingConfig {
reasoning_effort: Some(ReasoningEffort::High),
..Default::default()
}),
)
.await;
let seen = server.join().unwrap();
assert_eq!(
seen.iter().map(Seen::path).collect::<Vec<_>>(),
[
"/v1/models",
"/beta/chat/completions",
"/v1/chat/completions"
]
);
let prefix = seen[1].json();
assert_eq!(
prefix["messages"].as_array().unwrap().last().unwrap()["prefix"],
serde_json::json!(true)
);
assert!(prefix.get("tools").is_none(), "{prefix}");
assert_eq!(prefix["thinking"], serde_json::json!({"type": "disabled"}));
assert!(
!seen[2].json().to_string().contains("\"prefix\""),
"an ordinary turn names no prefix"
);
}
#[test]
fn the_beta_base_stands_in_for_the_version_segment() {
use super::client::deepseek_beta_base;
assert_eq!(
deepseek_beta_base("https://api.deepseek.com/v1"),
"https://api.deepseek.com/beta"
);
assert_eq!(
deepseek_beta_base("https://api.deepseek.com/v1/"),
"https://api.deepseek.com/beta"
);
assert_eq!(
deepseek_beta_base("https://api.deepseek.com"),
"https://api.deepseek.com/beta"
);
assert_eq!(
deepseek_beta_base("http://127.0.0.1:9000/v1"),
"http://127.0.0.1:9000/beta"
);
}