fix(gateway): detect DeepSeek custom relay models

This commit is contained in:
zhefox
2026-09-01 22:49:25 +08:00
parent 3d87bbf230
commit e2154629ca
8 changed files with 115 additions and 8 deletions
@@ -291,6 +291,7 @@ pub(crate) fn apply_provider_request_routing_policy_to_decision_with_websocket_m
crate::ai_serving::openai_responses_reasoning_replay_policy( crate::ai_serving::openai_responses_reasoning_replay_policy(
transport.provider.provider_type.as_str(), transport.provider.provider_type.as_str(),
transport.endpoint.base_url.as_str(), transport.endpoint.base_url.as_str(),
provider_model.as_str(),
) )
}) })
.unwrap_or_default(); .unwrap_or_default();
@@ -183,6 +183,7 @@ pub(crate) async fn resolve_local_same_format_provider_candidate_payload_parts(
let reasoning_replay_policy = openai_responses_reasoning_replay_policy( let reasoning_replay_policy = openai_responses_reasoning_replay_policy(
prepared.transport.provider.provider_type.as_str(), prepared.transport.provider.provider_type.as_str(),
prepared.transport.endpoint.base_url.as_str(), prepared.transport.endpoint.base_url.as_str(),
prepared.mapped_model.as_str(),
); );
let redaction = resolve_provider_chat_pii_redaction( let redaction = resolve_provider_chat_pii_redaction(
state, state,
@@ -15,11 +15,25 @@ pub(crate) fn is_deepseek_provider(provider_type: &str, base_url: &str) -> bool
host == "deepseek.com" || host.ends_with(".deepseek.com") host == "deepseek.com" || host.ends_with(".deepseek.com")
} }
fn is_deepseek_model(provider_model: &str) -> bool {
let provider_model = provider_model.trim().to_ascii_lowercase();
let leaf = provider_model
.rsplit(['/', ':'])
.next()
.unwrap_or(provider_model.as_str());
leaf == "deepseek" || leaf.starts_with("deepseek-") || leaf.starts_with("deepseek_")
}
fn is_deepseek_upstream(provider_type: &str, base_url: &str, provider_model: &str) -> bool {
is_deepseek_provider(provider_type, base_url) || is_deepseek_model(provider_model)
}
pub(crate) fn openai_responses_reasoning_replay_policy( pub(crate) fn openai_responses_reasoning_replay_policy(
provider_type: &str, provider_type: &str,
base_url: &str, base_url: &str,
provider_model: &str,
) -> crate::ai_serving::OpenAiResponsesReasoningReplayPolicy { ) -> crate::ai_serving::OpenAiResponsesReasoningReplayPolicy {
if is_deepseek_provider(provider_type, base_url) { if is_deepseek_upstream(provider_type, base_url, provider_model) {
crate::ai_serving::OpenAiResponsesReasoningReplayPolicy::DeepSeekOpaque crate::ai_serving::OpenAiResponsesReasoningReplayPolicy::DeepSeekOpaque
} else { } else {
crate::ai_serving::OpenAiResponsesReasoningReplayPolicy::OpenAiItemIds crate::ai_serving::OpenAiResponsesReasoningReplayPolicy::OpenAiItemIds
@@ -33,7 +47,11 @@ pub(crate) fn apply_deepseek_tool_call_thinking_compat(
provider_api_format: &str, provider_api_format: &str,
original_request_body: Option<&Value>, original_request_body: Option<&Value>,
) { ) {
if !is_deepseek_provider(provider_type, base_url) { let provider_model = provider_request_body
.get("model")
.and_then(Value::as_str)
.unwrap_or_default();
if !is_deepseek_upstream(provider_type, base_url, provider_model) {
return; return;
} }
@@ -302,11 +320,35 @@ mod tests {
)); ));
assert!(!is_deepseek_provider("custom", "ftp://api.deepseek.com/v1")); assert!(!is_deepseek_provider("custom", "ftp://api.deepseek.com/v1"));
assert_eq!( assert_eq!(
openai_responses_reasoning_replay_policy("custom", "https://api.deepseek.com/v1"), openai_responses_reasoning_replay_policy(
"custom",
"https://api.deepseek.com/v1",
"deepseek-v4-flash",
),
crate::ai_serving::OpenAiResponsesReasoningReplayPolicy::DeepSeekOpaque crate::ai_serving::OpenAiResponsesReasoningReplayPolicy::DeepSeekOpaque
); );
assert_eq!( assert_eq!(
openai_responses_reasoning_replay_policy("openai", "https://api.openai.com/v1"), openai_responses_reasoning_replay_policy(
"openai",
"https://api.openai.com/v1",
"gpt-5.6-sol",
),
crate::ai_serving::OpenAiResponsesReasoningReplayPolicy::OpenAiItemIds
);
assert_eq!(
openai_responses_reasoning_replay_policy(
"custom",
"https://api.b.ai/v1",
"deepseek-v4-flash",
),
crate::ai_serving::OpenAiResponsesReasoningReplayPolicy::DeepSeekOpaque
);
assert_eq!(
openai_responses_reasoning_replay_policy(
"custom",
"https://api.b.ai/v1",
"not-deepseek-compatible",
),
crate::ai_serving::OpenAiResponsesReasoningReplayPolicy::OpenAiItemIds crate::ai_serving::OpenAiResponsesReasoningReplayPolicy::OpenAiItemIds
); );
} }
@@ -330,8 +372,11 @@ mod tests {
"input": reasoning_items.clone(), "input": reasoning_items.clone(),
"future_request_field": {"preserve": true} "future_request_field": {"preserve": true}
}); });
let replay_policy = let replay_policy = openai_responses_reasoning_replay_policy(
openai_responses_reasoning_replay_policy("custom", "https://api.deepseek.com/v1"); "custom",
"https://api.deepseek.com/v1",
"deepseek-v4-flash",
);
let mut provider_body = crate::ai_serving::build_standard_request_body_with_model_directives_and_request_headers_and_reasoning_replay_policy( let mut provider_body = crate::ai_serving::build_standard_request_body_with_model_directives_and_request_headers_and_reasoning_replay_policy(
&request, &request,
"openai:responses", "openai:responses",
@@ -373,7 +418,11 @@ mod tests {
crate::ai_serving::strip_incompatible_openai_responses_reasoning_items_with_policy( crate::ai_serving::strip_incompatible_openai_responses_reasoning_items_with_policy(
&mut deepseek, &mut deepseek,
"openai:responses", "openai:responses",
openai_responses_reasoning_replay_policy("custom", "https://api.deepseek.com/v1"), openai_responses_reasoning_replay_policy(
"custom",
"https://api.deepseek.com/v1",
"deepseek-v4-flash",
),
), ),
0 0
); );
@@ -383,7 +432,11 @@ mod tests {
crate::ai_serving::strip_incompatible_openai_responses_reasoning_items_with_policy( crate::ai_serving::strip_incompatible_openai_responses_reasoning_items_with_policy(
&mut openai, &mut openai,
"openai:responses", "openai:responses",
openai_responses_reasoning_replay_policy("openai", "https://api.openai.com/v1"), openai_responses_reasoning_replay_policy(
"openai",
"https://api.openai.com/v1",
"gpt-5.6-sol",
),
), ),
66 66
); );
@@ -417,6 +470,52 @@ mod tests {
assert_eq!(body["messages"][1]["reasoning_content"], ""); assert_eq!(body["messages"][1]["reasoning_content"], "");
} }
#[test]
fn custom_relay_deepseek_model_adds_chat_thinking_compat() {
let mut body = json!({
"model": "deepseek-v4-flash",
"messages": [
{"role": "user", "content": "inspect the repository"},
{"role": "assistant", "content": null, "tool_calls": [{
"id": "call_1",
"type": "function",
"function": {"name": "inspect", "arguments": "{}"}
}]},
{"role": "tool", "tool_call_id": "call_1", "content": "done"}
]
});
apply_deepseek_tool_call_thinking_compat(
&mut body,
"custom",
"https://api.b.ai/v1",
"openai:chat",
None,
);
assert_eq!(body["thinking"]["type"], "enabled");
assert_eq!(body["messages"][1]["reasoning_content"], "");
}
#[test]
fn custom_relay_non_deepseek_model_is_not_rewritten() {
let original = json!({
"model": "not-deepseek-compatible",
"messages": [{"role": "assistant", "content": "done"}]
});
let mut body = original.clone();
apply_deepseek_tool_call_thinking_compat(
&mut body,
"custom",
"https://api.b.ai/v1",
"openai:chat",
None,
);
assert_eq!(body, original);
}
#[test] #[test]
fn openai_chat_deepseek_honors_disabled_thinking() { fn openai_chat_deepseek_honors_disabled_thinking() {
let original = json!({"reasoning_effort": "none"}); let original = json!({"reasoning_effort": "none"});
@@ -597,6 +597,7 @@ pub(crate) async fn resolve_local_standard_candidate_payload_parts(
let reasoning_replay_policy = openai_responses_reasoning_replay_policy( let reasoning_replay_policy = openai_responses_reasoning_replay_policy(
transport.provider.provider_type.as_str(), transport.provider.provider_type.as_str(),
transport.endpoint.base_url.as_str(), transport.endpoint.base_url.as_str(),
prepared_candidate.mapped_model.as_str(),
); );
let redaction = resolve_provider_chat_pii_redaction( let redaction = resolve_provider_chat_pii_redaction(
state, state,
@@ -159,6 +159,7 @@ fn finalize_openai_chat_provider_request_body(
openai_responses_reasoning_replay_policy( openai_responses_reasoning_replay_policy(
transport.provider.provider_type.as_str(), transport.provider.provider_type.as_str(),
transport.endpoint.base_url.as_str(), transport.endpoint.base_url.as_str(),
mapped_model,
), ),
) )
.err() .err()
@@ -438,6 +438,7 @@ pub(crate) async fn resolve_local_openai_responses_candidate_payload_parts_with_
let reasoning_replay_policy = openai_responses_reasoning_replay_policy( let reasoning_replay_policy = openai_responses_reasoning_replay_policy(
transport.provider.provider_type.as_str(), transport.provider.provider_type.as_str(),
transport.endpoint.base_url.as_str(), transport.endpoint.base_url.as_str(),
mapped_model.as_str(),
); );
let redaction = resolve_provider_chat_pii_redaction( let redaction = resolve_provider_chat_pii_redaction(
state, state,
@@ -1005,6 +1005,7 @@ pub(crate) async fn maybe_build_responses_websocket_decision(
reasoning_replay_policy: openai_responses_reasoning_replay_policy( reasoning_replay_policy: openai_responses_reasoning_replay_policy(
transport.provider.provider_type.as_str(), transport.provider.provider_type.as_str(),
transport.endpoint.base_url.as_str(), transport.endpoint.base_url.as_str(),
mapped_model.as_str(),
), ),
model_directive_patch: input model_directive_patch: input
.model_directive_policy .model_directive_policy
@@ -3177,6 +3177,7 @@ async fn provider_query_execute_standard_test_candidate(
crate::ai_serving::openai_responses_reasoning_replay_policy( crate::ai_serving::openai_responses_reasoning_replay_policy(
transport.provider.provider_type.as_str(), transport.provider.provider_type.as_str(),
transport.endpoint.base_url.as_str(), transport.endpoint.base_url.as_str(),
request_model,
), ),
) )
else { else {
@@ -3238,6 +3239,7 @@ async fn provider_query_execute_standard_test_candidate(
crate::ai_serving::openai_responses_reasoning_replay_policy( crate::ai_serving::openai_responses_reasoning_replay_policy(
transport.provider.provider_type.as_str(), transport.provider.provider_type.as_str(),
transport.endpoint.base_url.as_str(), transport.endpoint.base_url.as_str(),
request_model,
), ),
) )
.is_err() .is_err()