mirror of
https://github.com/Darkatse/TauriTavern.git
synced 2026-10-19 15:45:12 +00:00
refactor(deepseek): simplify compatible request handling
This commit is contained in:
parent
9cd47f9e28
commit
ad9bc6d796
4 changed files with 347 additions and 447 deletions
|
|
@ -58,6 +58,8 @@ Connection Profiles(Connection Manager 扩展):
|
|||
- `gemini_interactions` → 构造 `/interactions`
|
||||
- `gemini_generate_content` → 复用 MakerSuite 翻译器与传输链路,Custom 语义见 §4.4
|
||||
|
||||
DeepSeek 的原生与 Custom/OpenCode 兼容请求共享后端适配,保留各来源的参数与提示词策略;适配在协议构建后、用户覆盖前执行。
|
||||
|
||||
### 2.3 HTTP 调用 + Stream 处理(Rust repository)
|
||||
|
||||
仓库层对 `ChatCompletionSource::Custom` 以 **endpoint_path** 再分流(`http_chat_completion_repository/mod.rs`):
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
use serde_json::{Map, Value};
|
||||
use tt_ports::repositories::chat_completion_repository::ChatCompletionSource;
|
||||
|
||||
use crate::errors::ApplicationError;
|
||||
|
||||
use super::super::exchange::ChatCompletionProviderFormat;
|
||||
use super::super::model_capabilities::{
|
||||
RequestedReasoningEffort, parse_known_reasoning_effort, unsupported_reasoning_effort,
|
||||
};
|
||||
|
|
@ -16,24 +18,114 @@ enum DeepSeekThinkingMode {
|
|||
Disabled,
|
||||
}
|
||||
|
||||
/// Resolved from the TT request, before a wire builder can drop provider controls.
|
||||
pub(super) struct RequestOptions {
|
||||
thinking_mode: Option<DeepSeekThinkingMode>,
|
||||
reasoning_effort: Option<Value>,
|
||||
strip_sampling: bool,
|
||||
}
|
||||
|
||||
impl RequestOptions {
|
||||
pub(super) fn from_payload(
|
||||
source: ChatCompletionSource,
|
||||
payload: &Map<String, Value>,
|
||||
) -> Result<Option<Self>, ApplicationError> {
|
||||
let model = payload
|
||||
.get("model")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default();
|
||||
|
||||
if source == ChatCompletionSource::DeepSeek {
|
||||
let thinking_mode = resolve_thinking_mode(payload, model);
|
||||
let reasoning_effort = if thinking_mode == Some(DeepSeekThinkingMode::Enabled) {
|
||||
normalize_reasoning_effort(
|
||||
payload
|
||||
.get("reasoning_effort")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default(),
|
||||
)?
|
||||
.map(|effort| Value::String(effort.to_string()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
return Ok(Some(Self {
|
||||
thinking_mode,
|
||||
reasoning_effort,
|
||||
strip_sampling: true,
|
||||
}));
|
||||
}
|
||||
|
||||
if !matches!(
|
||||
source,
|
||||
ChatCompletionSource::Custom | ChatCompletionSource::OpenCode
|
||||
) || ChatCompletionProviderFormat::from_payload(source, payload)?
|
||||
!= ChatCompletionProviderFormat::OpenAiCompatible
|
||||
|| !is_deepseek_v4_model(model)
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let thinking_mode =
|
||||
if payload.get("include_reasoning").and_then(Value::as_bool) == Some(false) {
|
||||
DeepSeekThinkingMode::Disabled
|
||||
} else {
|
||||
DeepSeekThinkingMode::Enabled
|
||||
};
|
||||
// Compatible gateways own their parameter contract. Preserve explicit values,
|
||||
// including effort that the OpenCode wire builder would otherwise discard.
|
||||
Ok(Some(Self {
|
||||
thinking_mode: Some(thinking_mode),
|
||||
reasoning_effort: payload.get("reasoning_effort").cloned(),
|
||||
strip_sampling: false,
|
||||
}))
|
||||
}
|
||||
|
||||
pub(super) fn apply(self, body: &mut Map<String, Value>) {
|
||||
if let Some(mode) = self.thinking_mode {
|
||||
body.insert(
|
||||
"thinking".to_string(),
|
||||
serde_json::json!({
|
||||
"type": match mode {
|
||||
DeepSeekThinkingMode::Enabled => "enabled",
|
||||
DeepSeekThinkingMode::Disabled => "disabled",
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(effort) = self.reasoning_effort {
|
||||
body.insert("reasoning_effort".to_string(), effort);
|
||||
} else {
|
||||
body.remove("reasoning_effort");
|
||||
}
|
||||
|
||||
if self.thinking_mode == Some(DeepSeekThinkingMode::Enabled) {
|
||||
if self.strip_sampling {
|
||||
for key in [
|
||||
"temperature",
|
||||
"top_p",
|
||||
"presence_penalty",
|
||||
"frequency_penalty",
|
||||
] {
|
||||
body.remove(key);
|
||||
}
|
||||
}
|
||||
let has_tools = body
|
||||
.get("tools")
|
||||
.and_then(Value::as_array)
|
||||
.is_some_and(|tools| !tools.is_empty());
|
||||
if let Some(messages) = body.get_mut("messages").and_then(Value::as_array_mut) {
|
||||
fill_missing_tool_reasoning_content(messages, has_tools);
|
||||
}
|
||||
}
|
||||
|
||||
strip_empty_required_arrays_from_tools(body);
|
||||
}
|
||||
}
|
||||
|
||||
/// The native source owns its prompt preset; compatible sources keep the user's preset.
|
||||
pub(super) fn build(mut payload: Map<String, Value>) -> Result<(String, Value), ApplicationError> {
|
||||
let names = PromptNames::from_payload(&payload);
|
||||
let model = payload
|
||||
.get("model")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.trim()
|
||||
.to_string();
|
||||
let thinking_mode = resolve_thinking_mode(&payload, &model);
|
||||
let reasoning_effort = match thinking_mode {
|
||||
Some(DeepSeekThinkingMode::Enabled) => normalize_reasoning_effort(
|
||||
payload
|
||||
.get("reasoning_effort")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default(),
|
||||
)?,
|
||||
_ => None,
|
||||
};
|
||||
let has_tools = payload
|
||||
.get("tools")
|
||||
.and_then(Value::as_array)
|
||||
|
|
@ -54,25 +146,12 @@ pub(super) fn build(mut payload: Map<String, Value>) -> Result<(String, Value),
|
|||
add_assistant_prefix(&mut processed, "prefix");
|
||||
}
|
||||
|
||||
if thinking_mode == Some(DeepSeekThinkingMode::Enabled) {
|
||||
ensure_tool_context_reasoning_content(&mut processed, has_tools)?;
|
||||
}
|
||||
|
||||
let processed = Value::Array(processed);
|
||||
tool_calls::validate_openai_chat_tool_transcript(Some(&processed), false)?;
|
||||
payload.insert("messages".to_string(), processed);
|
||||
}
|
||||
|
||||
strip_empty_required_arrays_from_tools(&mut payload);
|
||||
|
||||
let (endpoint, mut upstream_payload) = openai::build(payload)?;
|
||||
if endpoint == "/chat/completions"
|
||||
&& let (Some(mode), Some(body)) = (thinking_mode, upstream_payload.as_object_mut())
|
||||
{
|
||||
apply_thinking_mode(body, mode, reasoning_effort);
|
||||
}
|
||||
|
||||
Ok((endpoint, upstream_payload))
|
||||
openai::build(payload)
|
||||
}
|
||||
|
||||
fn resolve_thinking_mode(
|
||||
|
|
@ -101,79 +180,14 @@ fn resolve_thinking_mode(
|
|||
}
|
||||
}
|
||||
|
||||
/// DeepSeek V4 家族(v4* / flash)判定,用于 OpenAI 兼容源的模型名匹配。
|
||||
/// 取 `/` 分隔的最后一段再匹配,容忍网关前缀(如 `GO/deepseek-flash`、
|
||||
/// `OR/deepseek-v4.1-flash`、`newapi/openrouter/deepseek-v4`)。
|
||||
/// DeepSeek 3.x 不命中。
|
||||
pub(super) fn is_deepseek_v4_model(model: &str) -> bool {
|
||||
/// Match the final segment to allow gateway prefixes such as `GO/deepseek-flash`.
|
||||
fn is_deepseek_v4_model(model: &str) -> bool {
|
||||
let normalized = model.trim().to_ascii_lowercase();
|
||||
let last_segment = normalized.rsplit('/').next().unwrap_or_default();
|
||||
|
||||
last_segment.starts_with("deepseek-v4") || last_segment.starts_with("deepseek-flash")
|
||||
}
|
||||
|
||||
/// 对 OpenAI 兼容源(Custom/OpenCode OpenAiCompat)构建后的请求体应用
|
||||
/// DeepSeek V4 语义对齐:当模型命中 V4 家族时,对齐官方 DeepSeek 渠道的
|
||||
/// thinking 控制、effort 归一化、采样参数清理、工具上下文 reasoning_content
|
||||
/// 补齐与空 required 剥离。非 V4 模型零改动。`include_reasoning` 由调用方
|
||||
/// 在 builder 白名单化之前从 TT payload 读出传入(思考开关意图),它本身
|
||||
/// 不透传给上游。
|
||||
pub(super) fn apply_deepseek_v4_compat(
|
||||
upstream_payload: &mut Value,
|
||||
include_reasoning: Option<bool>,
|
||||
) -> Result<(), ApplicationError> {
|
||||
let Some(body) = upstream_payload.as_object_mut() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let model = body
|
||||
.get("model")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
if !is_deepseek_v4_model(&model) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 未提供时与官方渠道默认一致:V4 家族默认开启思考。
|
||||
let thinking_mode = match include_reasoning {
|
||||
Some(true) => DeepSeekThinkingMode::Enabled,
|
||||
Some(false) => DeepSeekThinkingMode::Disabled,
|
||||
None => DeepSeekThinkingMode::Enabled,
|
||||
};
|
||||
|
||||
let reasoning_effort = match thinking_mode {
|
||||
DeepSeekThinkingMode::Enabled => normalize_reasoning_effort(
|
||||
body.get("reasoning_effort")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default(),
|
||||
)?,
|
||||
DeepSeekThinkingMode::Disabled => None,
|
||||
};
|
||||
|
||||
let has_tools = body
|
||||
.get("tools")
|
||||
.and_then(Value::as_array)
|
||||
.is_some_and(|tools| !tools.is_empty());
|
||||
|
||||
if thinking_mode == DeepSeekThinkingMode::Enabled
|
||||
&& let Some(messages) = body.get_mut("messages").and_then(Value::as_array_mut)
|
||||
{
|
||||
ensure_tool_context_reasoning_content(messages, has_tools)?;
|
||||
}
|
||||
|
||||
strip_empty_required_arrays_from_tools(body);
|
||||
|
||||
// CUSTOM 源会无条件转发 reasoning_effort;官方渠道语义是 disabled 时
|
||||
// 不发送 effort,这里对齐(enabled 时 apply_thinking_mode 内部会重写)。
|
||||
if thinking_mode == DeepSeekThinkingMode::Disabled {
|
||||
body.remove("reasoning_effort");
|
||||
}
|
||||
|
||||
apply_thinking_mode(body, thinking_mode, reasoning_effort);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
fn normalize_reasoning_effort(value: &str) -> Result<Option<&'static str>, ApplicationError> {
|
||||
match parse_known_reasoning_effort(value, "DeepSeek")? {
|
||||
RequestedReasoningEffort::Auto => Ok(None),
|
||||
|
|
@ -186,44 +200,7 @@ fn normalize_reasoning_effort(value: &str) -> Result<Option<&'static str>, Appli
|
|||
}
|
||||
}
|
||||
|
||||
fn apply_thinking_mode(
|
||||
body: &mut Map<String, Value>,
|
||||
mode: DeepSeekThinkingMode,
|
||||
reasoning_effort: Option<&str>,
|
||||
) {
|
||||
body.insert(
|
||||
"thinking".to_string(),
|
||||
serde_json::json!({
|
||||
"type": match mode {
|
||||
DeepSeekThinkingMode::Enabled => "enabled",
|
||||
DeepSeekThinkingMode::Disabled => "disabled",
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
if mode == DeepSeekThinkingMode::Enabled {
|
||||
for key in [
|
||||
"temperature",
|
||||
"top_p",
|
||||
"presence_penalty",
|
||||
"frequency_penalty",
|
||||
] {
|
||||
body.remove(key);
|
||||
}
|
||||
|
||||
if let Some(reasoning_effort) = reasoning_effort {
|
||||
body.insert(
|
||||
"reasoning_effort".to_string(),
|
||||
Value::String(reasoning_effort.to_string()),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_tool_context_reasoning_content(
|
||||
messages: &mut [Value],
|
||||
has_tools: bool,
|
||||
) -> Result<(), ApplicationError> {
|
||||
fn fill_missing_tool_reasoning_content(messages: &mut [Value], has_tools: bool) {
|
||||
let has_tool_context = has_tools
|
||||
|| messages.iter().any(|message| {
|
||||
let Some(message_object) = message.as_object() else {
|
||||
|
|
@ -238,7 +215,7 @@ fn ensure_tool_context_reasoning_content(
|
|||
});
|
||||
|
||||
if !has_tool_context {
|
||||
return Ok(());
|
||||
return;
|
||||
}
|
||||
|
||||
for message in messages {
|
||||
|
|
@ -250,24 +227,11 @@ fn ensure_tool_context_reasoning_content(
|
|||
continue;
|
||||
}
|
||||
|
||||
match message_object.get("reasoning_content") {
|
||||
Some(Value::String(_)) => {}
|
||||
Some(_) => {
|
||||
return Err(ApplicationError::ValidationError(
|
||||
"DeepSeek thinking assistant messages in tool context must have string reasoning_content"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
None => {
|
||||
message_object.insert(
|
||||
"reasoning_content".to_string(),
|
||||
Value::String(String::new()),
|
||||
);
|
||||
}
|
||||
}
|
||||
// Preserve explicit values for later body overrides and upstream handling.
|
||||
message_object
|
||||
.entry("reasoning_content")
|
||||
.or_insert_with(|| Value::String(String::new()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn strip_empty_required_arrays_from_tools(payload: &mut Map<String, Value>) {
|
||||
|
|
@ -276,26 +240,16 @@ fn strip_empty_required_arrays_from_tools(payload: &mut Map<String, Value>) {
|
|||
};
|
||||
|
||||
for tool in tools {
|
||||
let should_remove = tool
|
||||
.as_object()
|
||||
.and_then(|tool| tool.get("function"))
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|function| function.get("parameters"))
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|parameters| parameters.get("required"))
|
||||
.and_then(Value::as_array)
|
||||
.is_some_and(|required| required.is_empty());
|
||||
|
||||
if !should_remove {
|
||||
let Some(parameters) = tool
|
||||
.pointer_mut("/function/parameters")
|
||||
.and_then(Value::as_object_mut)
|
||||
else {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(parameters) = tool
|
||||
.as_object_mut()
|
||||
.and_then(|tool| tool.get_mut("function"))
|
||||
.and_then(Value::as_object_mut)
|
||||
.and_then(|function| function.get_mut("parameters"))
|
||||
.and_then(Value::as_object_mut)
|
||||
};
|
||||
if parameters
|
||||
.get("required")
|
||||
.and_then(Value::as_array)
|
||||
.is_some_and(|required| required.is_empty())
|
||||
{
|
||||
parameters.remove("required");
|
||||
}
|
||||
|
|
@ -304,9 +258,14 @@ fn strip_empty_required_arrays_from_tools(payload: &mut Map<String, Value>) {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::{Value, json};
|
||||
use serde_json::{Map, Value, json};
|
||||
use tt_ports::repositories::chat_completion_repository::ChatCompletionSource;
|
||||
|
||||
use super::build;
|
||||
use crate::errors::ApplicationError;
|
||||
|
||||
fn build(payload: Map<String, Value>) -> Result<(String, Value), ApplicationError> {
|
||||
super::super::build_payload(ChatCompletionSource::DeepSeek, payload)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deepseek_build_marks_assistant_prefill_as_prefix() {
|
||||
|
|
@ -338,16 +297,16 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn deepseek_v4_enables_thinking_and_maps_effort() {
|
||||
for model in [
|
||||
"deepseek-v4-flash",
|
||||
"deepseek-v4-flash-vision-exp",
|
||||
"deepseek-flash",
|
||||
"deepseek-v4-pro",
|
||||
for (model, requested_effort, expected_effort) in [
|
||||
("deepseek-v4-flash", "medium", Some("high")),
|
||||
("deepseek-v4-flash-vision-exp", "minimal", Some("low")),
|
||||
("deepseek-flash", "auto", None),
|
||||
("deepseek-v4-pro", "max", Some("max")),
|
||||
] {
|
||||
let payload = json!({
|
||||
"model": model,
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
"reasoning_effort": "max",
|
||||
"reasoning_effort": requested_effort,
|
||||
"temperature": 1.2,
|
||||
"top_p": 0.7,
|
||||
"presence_penalty": 0.1,
|
||||
|
|
@ -369,8 +328,8 @@ mod tests {
|
|||
Some("enabled")
|
||||
);
|
||||
assert_eq!(
|
||||
body.get("reasoning_effort").and_then(Value::as_str),
|
||||
Some("max")
|
||||
body.get("reasoning_effort"),
|
||||
expected_effort.map(|effort| json!(effort)).as_ref()
|
||||
);
|
||||
assert!(body.get("temperature").is_none());
|
||||
assert!(body.get("top_p").is_none());
|
||||
|
|
@ -490,35 +449,6 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deepseek_thinking_tool_context_rejects_non_string_reasoning_content() {
|
||||
let payload = json!({
|
||||
"model": "deepseek-v4-flash",
|
||||
"messages": [
|
||||
{"role":"user","content":"weather"},
|
||||
{
|
||||
"role":"assistant",
|
||||
"content":"",
|
||||
"reasoning_content": {"text":"need weather"},
|
||||
"tool_calls":[{
|
||||
"id":"call_1",
|
||||
"type":"function",
|
||||
"function":{"name":"weather","arguments":"{}"}
|
||||
}]
|
||||
},
|
||||
{"role":"tool","tool_call_id":"call_1","content":"cloudy"}
|
||||
],
|
||||
"include_reasoning": true,
|
||||
"chat_completion_source": "deepseek"
|
||||
})
|
||||
.as_object()
|
||||
.cloned()
|
||||
.expect("payload must be object");
|
||||
|
||||
let error = build(payload).expect_err("non-string reasoning_content must fail");
|
||||
assert!(error.to_string().contains("string reasoning_content"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deepseek_rejects_unknown_reasoning_effort() {
|
||||
let payload = json!({
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ pub(super) fn build_payload(
|
|||
payload: Map<String, Value>,
|
||||
) -> Result<(String, Value), ApplicationError> {
|
||||
let mut payload = payload;
|
||||
// Capture provider controls before wire builders discard internal fields.
|
||||
let deepseek_options = deepseek::RequestOptions::from_payload(source, &payload)?;
|
||||
let opencode_format = (source == ChatCompletionSource::OpenCode)
|
||||
.then(|| opencode::format_from_payload(&payload))
|
||||
.transpose()?;
|
||||
|
|
@ -58,22 +60,18 @@ pub(super) fn build_payload(
|
|||
return openai_responses::build(payload);
|
||||
}
|
||||
|
||||
match source {
|
||||
let (endpoint, mut body) = match source {
|
||||
ChatCompletionSource::OpenAi
|
||||
| ChatCompletionSource::Groq
|
||||
| ChatCompletionSource::SiliconFlow
|
||||
| ChatCompletionSource::Pollinations => openai::build(payload),
|
||||
ChatCompletionSource::OpenCode => {
|
||||
let include_reasoning = payload
|
||||
.get("include_reasoning")
|
||||
.and_then(Value::as_bool);
|
||||
let built = match opencode_format.expect("OpenCode format resolved") {
|
||||
match opencode_format.expect("OpenCode format resolved") {
|
||||
OpenCodeApiFormat::OpenAiCompat => openai::build_chat(payload),
|
||||
OpenCodeApiFormat::OpenAiResponses => openai_responses::build(payload),
|
||||
OpenCodeApiFormat::ClaudeMessages => claude_messages::build(payload),
|
||||
OpenCodeApiFormat::Gemini => makersuite::build(payload),
|
||||
}?;
|
||||
apply_deepseek_v4_compat_if_chat(include_reasoning, built)
|
||||
}
|
||||
}
|
||||
ChatCompletionSource::DeepSeek => deepseek::build(payload),
|
||||
ChatCompletionSource::Cohere => Ok(cohere::build(payload)?),
|
||||
|
|
@ -85,33 +83,19 @@ pub(super) fn build_payload(
|
|||
ChatCompletionSource::OpenRouter => openrouter::build(payload),
|
||||
ChatCompletionSource::Zai => zai::build(payload),
|
||||
ChatCompletionSource::MiniMax => Ok(minimax::build(payload)),
|
||||
ChatCompletionSource::Custom => {
|
||||
// builder 白名单会丢弃 include_reasoning,先读出思考开关意图。
|
||||
let include_reasoning = payload
|
||||
.get("include_reasoning")
|
||||
.and_then(Value::as_bool);
|
||||
let built = custom::build(payload)?;
|
||||
apply_deepseek_v4_compat_if_chat(include_reasoning, built)
|
||||
}
|
||||
ChatCompletionSource::Custom => custom::build(payload),
|
||||
ChatCompletionSource::Claude => Ok(claude::build(payload)?),
|
||||
ChatCompletionSource::AwsBedrock => Ok(aws_bedrock::build(payload)?),
|
||||
ChatCompletionSource::Makersuite => Ok(makersuite::build(payload)?),
|
||||
ChatCompletionSource::VertexAi => Ok(vertexai::build(payload)?),
|
||||
}
|
||||
}
|
||||
}?;
|
||||
|
||||
/// OpenAI 兼容源(Custom/OpenCode)经 OpenAI 兼容上游访问 DeepSeek V4 时,
|
||||
/// 对齐官方 DeepSeek 渠道语义(thinking 控制、effort 归一化等)。
|
||||
/// 仅处理 /chat/completions 端点;模型不命中时零改动。
|
||||
fn apply_deepseek_v4_compat_if_chat(
|
||||
include_reasoning: Option<bool>,
|
||||
built: (String, Value),
|
||||
) -> Result<(String, Value), ApplicationError> {
|
||||
let (endpoint, mut upstream_payload) = built;
|
||||
if endpoint == "/chat/completions" {
|
||||
deepseek::apply_deepseek_v4_compat(&mut upstream_payload, include_reasoning)?;
|
||||
if endpoint == "/chat/completions"
|
||||
&& let Some(options) = deepseek_options
|
||||
{
|
||||
options.apply(body.as_object_mut().expect("wire builders return objects"));
|
||||
}
|
||||
Ok((endpoint, upstream_payload))
|
||||
Ok((endpoint, body))
|
||||
}
|
||||
|
||||
pub(super) fn validate_upstream_tool_transcript(
|
||||
|
|
@ -129,6 +113,7 @@ pub(super) fn validate_upstream_tool_transcript(
|
|||
mod tests {
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
use super::super::additional_parameters::AdditionalParameters;
|
||||
use super::build_payload;
|
||||
use tt_ports::repositories::chat_completion_repository::ChatCompletionSource;
|
||||
|
||||
|
|
@ -339,224 +324,202 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
/// 兼容源(Custom/OpenCode OpenAI 兼容格式)命中 DeepSeek V4 模型时,
|
||||
/// 对齐官方 DeepSeek 渠道语义:显式 thinking 控制、effort 归一化、
|
||||
/// 关闭思考时保留采样参数、开启思考时清理采样参数、
|
||||
/// 工具上下文补 reasoning_content、剥离空 required。
|
||||
fn deepseek_v4_compat_payload(model: &str) -> Map<String, Value> {
|
||||
fn deepseek_compat_payload(source: ChatCompletionSource, model: &str) -> Map<String, Value> {
|
||||
json!({
|
||||
"chat_completion_source": "custom",
|
||||
"custom_api_format": "openai_compat",
|
||||
"opencode_endpoint": "zen",
|
||||
"opencode_api_format": "openai_compat",
|
||||
"chat_completion_source": source.key(),
|
||||
"model": model,
|
||||
"messages": [
|
||||
{"role":"user","content":"weather"},
|
||||
{"role":"assistant","content":"I'll check."},
|
||||
{
|
||||
"role":"assistant",
|
||||
"content":"",
|
||||
"tool_calls":[{
|
||||
"id":"call_1",
|
||||
"type":"function",
|
||||
"function":{"name":"weather","arguments":"{}"}
|
||||
}]
|
||||
},
|
||||
{"role":"tool","tool_call_id":"call_1","content":"cloudy"}
|
||||
{"role": "user", "content": "weather"},
|
||||
{"role": "assistant", "content": "I'll check.", "reasoning_content": " original\nreasoning "},
|
||||
{"role": "assistant", "content": "", "tool_calls": [{
|
||||
"id": "call_1", "type": "function",
|
||||
"function": {"name": "weather", "arguments": "{}"}
|
||||
}]},
|
||||
{"role": "tool", "tool_call_id": "call_1", "content": "cloudy"}
|
||||
],
|
||||
"tools": [{"type":"function","function":{"name":"weather","parameters":{
|
||||
"type":"object","properties":{"city":{"type":"string"}},"required":[]
|
||||
"tools": [{"type": "function", "function": {"name": "weather", "parameters": {
|
||||
"type": "object", "properties": {"city": {"type": "string"}}, "required": []
|
||||
}}}],
|
||||
"temperature": 1.2,
|
||||
"top_p": 0.7,
|
||||
"presence_penalty": 0.1,
|
||||
"frequency_penalty": 0.2,
|
||||
"reasoning_effort": "medium"
|
||||
})
|
||||
.as_object()
|
||||
.cloned()
|
||||
.expect("payload must be object")
|
||||
}).as_object().unwrap().clone()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compat_sources_align_deepseek_v4_thinking_when_show_thoughts_on() {
|
||||
fn compat_deepseek_preserves_parameters_and_repairs_tool_continuations() {
|
||||
for source in [ChatCompletionSource::Custom, ChatCompletionSource::OpenCode] {
|
||||
for model in [
|
||||
" GO/deepseek-flash ",
|
||||
"OR/DeepSeek-v4.1-flash",
|
||||
"deepseek-v4-pro",
|
||||
] {
|
||||
for include_reasoning in [None, Some(true), Some(false)] {
|
||||
let mut payload = deepseek_compat_payload(source, model);
|
||||
if let Some(include_reasoning) = include_reasoning {
|
||||
payload.insert("include_reasoning".into(), json!(include_reasoning));
|
||||
}
|
||||
let (endpoint, body) = build_payload(source, payload).unwrap();
|
||||
let thinking_enabled = include_reasoning != Some(false);
|
||||
assert_eq!(endpoint, "/chat/completions");
|
||||
assert_eq!(
|
||||
body["thinking"]["type"],
|
||||
if thinking_enabled {
|
||||
"enabled"
|
||||
} else {
|
||||
"disabled"
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
body["reasoning_effort"], "medium",
|
||||
"{source:?}: raw effort must survive the wire builder"
|
||||
);
|
||||
assert_eq!(body["temperature"], 1.2);
|
||||
assert_eq!(body["top_p"], 0.7);
|
||||
assert_eq!(body["presence_penalty"], 0.1);
|
||||
assert_eq!(body["frequency_penalty"], 0.2);
|
||||
assert_eq!(
|
||||
body["messages"][1]["reasoning_content"],
|
||||
" original\nreasoning "
|
||||
);
|
||||
assert_eq!(
|
||||
body["messages"][2].get("reasoning_content"),
|
||||
thinking_enabled.then_some(&json!(""))
|
||||
);
|
||||
assert!(
|
||||
body["tools"][0]["function"]["parameters"]
|
||||
.get("required")
|
||||
.is_none()
|
||||
);
|
||||
assert!(body.get("include_reasoning").is_none());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deepseek_tool_reasoning_allows_final_body_overrides() {
|
||||
for source in [
|
||||
ChatCompletionSource::DeepSeek,
|
||||
ChatCompletionSource::Custom,
|
||||
ChatCompletionSource::OpenCode,
|
||||
] {
|
||||
for model in [
|
||||
"GO/deepseek-flash",
|
||||
"OR/deepseek-v4.1-flash",
|
||||
"deepseek-v4-pro",
|
||||
for reasoning in [Value::Null, json!({"text": "need weather"})] {
|
||||
let mut payload = deepseek_compat_payload(source, "deepseek-v4-flash");
|
||||
payload.get_mut("messages").unwrap()[2]["reasoning_content"] = reasoning.clone();
|
||||
let mut repaired_messages = payload["messages"].clone();
|
||||
repaired_messages[2]["reasoning_content"] = json!("need weather");
|
||||
payload.insert(
|
||||
"custom_include_body".into(),
|
||||
json!({"messages": repaired_messages}),
|
||||
);
|
||||
let overrides = AdditionalParameters::from_payload(&payload).unwrap();
|
||||
|
||||
let (endpoint, mut body) = build_payload(source, payload)
|
||||
.expect("reasoning must not block final body overrides");
|
||||
assert_eq!(
|
||||
body["messages"][2].get("reasoning_content"),
|
||||
Some(&reasoning),
|
||||
"{source:?}"
|
||||
);
|
||||
|
||||
overrides.apply_body_overrides(&mut body).unwrap();
|
||||
super::validate_upstream_tool_transcript(&endpoint, &body).unwrap();
|
||||
assert_eq!(body["messages"], repaired_messages, "{source:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compat_deepseek_does_not_validate_or_guess_explicit_effort() {
|
||||
for source in [ChatCompletionSource::Custom, ChatCompletionSource::OpenCode] {
|
||||
// Auto in the UI omits the field; a supplied value belongs to the gateway.
|
||||
for effort in [
|
||||
None,
|
||||
Some(json!("max")),
|
||||
Some(json!("none")),
|
||||
Some(json!("provider-specific")),
|
||||
Some(json!("auto")),
|
||||
] {
|
||||
let (endpoint, upstream) = build_payload(
|
||||
source,
|
||||
deepseek_v4_compat_payload(model),
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("{source:?} {model}: {error}"));
|
||||
assert_eq!(endpoint, "/chat/completions");
|
||||
|
||||
let body = upstream.as_object().expect("body must be object");
|
||||
assert_eq!(
|
||||
body.get("thinking")
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|thinking| thinking.get("type"))
|
||||
.and_then(Value::as_str),
|
||||
Some("enabled"),
|
||||
"{source:?} {model}: thinking"
|
||||
);
|
||||
assert_eq!(
|
||||
body.get("reasoning_effort").and_then(Value::as_str),
|
||||
Some("high"),
|
||||
"{source:?} {model}: effort medium 归一化为 high"
|
||||
);
|
||||
assert!(body.get("temperature").is_none(), "{source:?} {model}");
|
||||
assert!(body.get("top_p").is_none(), "{source:?} {model}");
|
||||
assert!(body.get("presence_penalty").is_none(), "{source:?} {model}");
|
||||
assert!(body.get("frequency_penalty").is_none(), "{source:?} {model}");
|
||||
|
||||
let messages = body
|
||||
.get("messages")
|
||||
.and_then(Value::as_array)
|
||||
.expect("messages must be array");
|
||||
for index in [1_usize, 2] {
|
||||
let assistant = messages
|
||||
.get(index)
|
||||
.and_then(Value::as_object)
|
||||
.unwrap_or_else(|| panic!("{source:?} {model}: message {index}"));
|
||||
assert_eq!(
|
||||
assistant.get("reasoning_content").and_then(Value::as_str),
|
||||
Some(""),
|
||||
"{source:?} {model}: assistant {index} 补空串"
|
||||
);
|
||||
let mut payload = deepseek_compat_payload(source, "deepseek-v4-pro");
|
||||
payload.remove("reasoning_effort");
|
||||
if let Some(effort) = &effort {
|
||||
payload.insert("reasoning_effort".into(), effort.clone());
|
||||
}
|
||||
let (_, body) = build_payload(source, payload).unwrap();
|
||||
assert_eq!(body.get("reasoning_effort"), effort.as_ref(), "{source:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let required = &body["tools"][0]["function"]["parameters"]["required"];
|
||||
assert!(
|
||||
required.is_null(),
|
||||
"{source:?} {model}: 空 required 必须剥离, got {required}"
|
||||
#[test]
|
||||
fn deepseek_compat_leaves_other_models_and_sources_unchanged() {
|
||||
for source in [
|
||||
ChatCompletionSource::Custom,
|
||||
ChatCompletionSource::OpenCode,
|
||||
ChatCompletionSource::OpenRouter,
|
||||
] {
|
||||
for model in [
|
||||
"deepseek-3.2",
|
||||
"qwen3-max",
|
||||
"gpt-5.5",
|
||||
"gateway/deepseek-v4-pro",
|
||||
] {
|
||||
if source != ChatCompletionSource::OpenRouter && model.ends_with("deepseek-v4-pro")
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let payload = deepseek_compat_payload(source, model);
|
||||
let expected = match source {
|
||||
ChatCompletionSource::Custom => super::custom::build(payload.clone()),
|
||||
ChatCompletionSource::OpenCode => super::openai::build_chat(payload.clone()),
|
||||
_ => super::openrouter::build(payload.clone()),
|
||||
}
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
build_payload(source, payload).unwrap(),
|
||||
expected,
|
||||
"{source:?}: {model}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compat_sources_disable_deepseek_v4_thinking_when_show_thoughts_off() {
|
||||
let mut payload = deepseek_v4_compat_payload("GO/deepseek-flash");
|
||||
payload.insert("include_reasoning".to_string(), Value::Bool(false));
|
||||
|
||||
let (endpoint, upstream) =
|
||||
build_payload(ChatCompletionSource::Custom, payload).expect("payload should build");
|
||||
assert_eq!(endpoint, "/chat/completions");
|
||||
|
||||
let body = upstream.as_object().expect("body must be object");
|
||||
assert_eq!(
|
||||
body.get("thinking")
|
||||
.and_then(Value::as_object)
|
||||
.and_then(|thinking| thinking.get("type"))
|
||||
.and_then(Value::as_str),
|
||||
Some("disabled")
|
||||
);
|
||||
// 关闭思考:不转发 effort,采样参数保留,不补 reasoning_content
|
||||
assert!(body.get("reasoning_effort").is_none());
|
||||
assert_eq!(
|
||||
body.get("temperature").and_then(Value::as_f64),
|
||||
Some(1.2),
|
||||
"关闭思考时采样参数必须保留"
|
||||
);
|
||||
|
||||
let messages = body
|
||||
.get("messages")
|
||||
.and_then(Value::as_array)
|
||||
.expect("messages must be array");
|
||||
for index in [1_usize, 2] {
|
||||
let assistant = messages
|
||||
.get(index)
|
||||
.and_then(Value::as_object)
|
||||
.unwrap_or_else(|| panic!("message {index}"));
|
||||
assert!(
|
||||
assistant.get("reasoning_content").is_none(),
|
||||
"关闭思考时不补 reasoning_content at {index}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compat_sources_keep_non_deepseek_models_untouched() {
|
||||
for model in ["deepseek-3.2", "qwen3-max", "gpt-5.5"] {
|
||||
let (endpoint, upstream) =
|
||||
build_payload(ChatCompletionSource::Custom, deepseek_v4_compat_payload(model))
|
||||
.expect("payload should build");
|
||||
assert_eq!(endpoint, "/chat/completions");
|
||||
|
||||
let body = upstream.as_object().expect("body must be object");
|
||||
assert!(
|
||||
body.get("thinking").is_none(),
|
||||
"{model}: 不注入 thinking"
|
||||
);
|
||||
// CUSTOM 源现状:reasoning_effort 无条件转发,保持不动
|
||||
fn deepseek_compat_leaves_native_protocols_to_their_builders() {
|
||||
for (source, format) in [
|
||||
(ChatCompletionSource::Custom, "openai_responses"),
|
||||
(ChatCompletionSource::Custom, "claude_messages"),
|
||||
(ChatCompletionSource::Custom, "gemini_generate_content"),
|
||||
(ChatCompletionSource::Custom, "gemini_interactions"),
|
||||
(ChatCompletionSource::OpenCode, "openai_responses"),
|
||||
(ChatCompletionSource::OpenCode, "claude_messages"),
|
||||
(ChatCompletionSource::OpenCode, "gemini"),
|
||||
] {
|
||||
let mut payload = deepseek_compat_payload(source, "gateway/deepseek-v4-pro");
|
||||
let format_key = if source == ChatCompletionSource::Custom {
|
||||
"custom_api_format"
|
||||
} else {
|
||||
"opencode_api_format"
|
||||
};
|
||||
payload.insert(format_key.into(), json!(format));
|
||||
// Each selected protocol retains its own thinking/schema representation.
|
||||
let expected = match source {
|
||||
ChatCompletionSource::Custom => super::custom::build(payload.clone()),
|
||||
_ => match format {
|
||||
"openai_responses" => super::openai_responses::build(payload.clone()),
|
||||
"claude_messages" => super::claude_messages::build(payload.clone()),
|
||||
_ => super::makersuite::build(payload.clone()),
|
||||
},
|
||||
}
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
body.get("reasoning_effort").and_then(Value::as_str),
|
||||
Some("medium"),
|
||||
"{model}: effort 转发行为保持现状"
|
||||
build_payload(source, payload).unwrap(),
|
||||
expected,
|
||||
"{source:?}: {format}"
|
||||
);
|
||||
assert!(body.get("temperature").is_some(), "{model}: 采样参数不动");
|
||||
assert!(
|
||||
!body["tools"][0]["function"]["parameters"]["required"].is_null(),
|
||||
"{model}: required 不动"
|
||||
);
|
||||
|
||||
let messages = body
|
||||
.get("messages")
|
||||
.and_then(Value::as_array)
|
||||
.expect("messages must be array");
|
||||
for index in [1_usize, 2] {
|
||||
let assistant = messages
|
||||
.get(index)
|
||||
.and_then(Value::as_object)
|
||||
.unwrap_or_else(|| panic!("message {index}"));
|
||||
assert!(
|
||||
assistant.get("reasoning_content").is_none(),
|
||||
"{model}: 不补 reasoning_content at {index}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compat_sources_reject_invalid_deepseek_v4_reasoning_effort() {
|
||||
let mut payload = deepseek_v4_compat_payload("GO/deepseek-flash");
|
||||
payload.insert("reasoning_effort".to_string(), Value::String("auto-ish".to_string()));
|
||||
|
||||
let error = build_payload(ChatCompletionSource::Custom, payload)
|
||||
.expect_err("非法 effort 必须报错");
|
||||
assert!(
|
||||
error
|
||||
.to_string()
|
||||
.contains("Unsupported DeepSeek reasoning_effort")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn openrouter_source_keeps_deepseek_v4_without_thinking_injection() {
|
||||
// OR 源有自己的 reasoning 方言(include_reasoning/reasoning.effort),
|
||||
// fix-up 不得往 OR 请求里注入 DeepSeek 的 thinking 字段。
|
||||
let mut payload = deepseek_v4_compat_payload("deepseek/deepseek-v4.1-flash");
|
||||
payload.insert("include_reasoning".to_string(), Value::Bool(false));
|
||||
|
||||
let (endpoint, upstream) = build_payload(ChatCompletionSource::OpenRouter, payload)
|
||||
.expect("payload should build");
|
||||
assert_eq!(endpoint, "/chat/completions");
|
||||
|
||||
let body = upstream.as_object().expect("body must be object");
|
||||
assert!(
|
||||
body.get("thinking").is_none(),
|
||||
"OR 源不得注入 thinking 字段"
|
||||
);
|
||||
assert_eq!(
|
||||
body.get("include_reasoning").and_then(Value::as_bool),
|
||||
Some(false),
|
||||
"OR 自己的 include_reasoning 方言保持"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1007,6 +1007,8 @@ function setOpenAIMessages(chat, stripOldToolCalls = false) {
|
|||
const currentApi = oai_settings.chat_completion_source;
|
||||
const currentModel = getChatCompletionModel();
|
||||
const requestContext = getChatCompletionRequestContext(oai_settings, currentModel);
|
||||
const includeReasoningContent = oai_settings.show_thoughts
|
||||
&& usesDeepSeekReasoningContent(oai_settings, currentModel);
|
||||
const includeClaudeNative = usesClaudeMessagesSemantics(oai_settings, currentModel);
|
||||
const includeNative = includeClaudeNative
|
||||
|| (currentApi === chat_completion_sources.CUSTOM
|
||||
|
|
@ -1077,11 +1079,8 @@ function setOpenAIMessages(chat, stripOldToolCalls = false) {
|
|||
&& (!includeClaudeNative || hasClaudeToolUse(metadataMessage?.extra?.native))
|
||||
? metadataMessage?.extra?.native
|
||||
: null;
|
||||
const shouldReplayReasoningContent = (currentApi === chat_completion_sources.DEEPSEEK
|
||||
|| isDeepSeekV4CompatSource(currentApi, currentModel))
|
||||
&& oai_settings.show_thoughts
|
||||
&& isSameModel && !isOtherGroupMember;
|
||||
const reasoningContent = shouldReplayReasoningContent ? metadataMessage?.extra?.tool_reasoning_content : null;
|
||||
const reasoningContent = includeReasoningContent && isSameModel && !isOtherGroupMember
|
||||
? metadataMessage?.extra?.tool_reasoning_content : null;
|
||||
// Remove provider metadata from invocations if the API/model/speaker don't match.
|
||||
if (Array.isArray(invocations) && invocations.length > 0) {
|
||||
invocations.forEach((invocation, index) => {
|
||||
|
|
@ -1651,10 +1650,9 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
|
|||
&& [custom_api_formats.GEMINI_INTERACTIONS, custom_api_formats.GEMINI_GENERATE_CONTENT].includes(settings.custom_api_format));
|
||||
const canIncludeNative = native => includeNative
|
||||
&& (!includeClaudeNative || hasClaudeToolUse(native));
|
||||
// DeepSeek V4 经兼容源走 reasoning_content 方言,明文 reasoning 与之
|
||||
// 冲突(官方 DeepSeek 渠道同样不发明文 reasoning),这里排除。
|
||||
// DeepSeek tool turns use reasoning_content instead of reasoning.
|
||||
const isToolReasoningProvider = interleaved_reasoning_providers.includes(settings.chat_completion_source)
|
||||
&& !isDeepSeekV4CompatSource(settings.chat_completion_source, getChatCompletionModel(settings));
|
||||
&& !usesDeepSeekReasoningContent(settings);
|
||||
const toolReasoningMode = isToolReasoningProvider
|
||||
? getEffectiveToolReasoningMode(settings)
|
||||
: tool_reasoning_modes.DISABLED;
|
||||
|
|
@ -1752,6 +1750,7 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
|
|||
return clone;
|
||||
});
|
||||
const toolResultMessages = await Promise.all(invocations.slice().reverse().map((invocation) => Message.createAsync('tool', invocation.result, invocation.id, assemblyTokenHandler)));
|
||||
chatMessage.reasoningContent = chatPrompt.reasoningContent ?? null;
|
||||
await chatMessage.setToolCalls(invocations, includeSignature, includeToolReasoning, assemblyTokenHandler);
|
||||
if (includeSignature && chatPrompt.signature) {
|
||||
chatMessage.signature = chatPrompt.signature;
|
||||
|
|
@ -1759,9 +1758,6 @@ async function populateChatHistory(messages, prompts, chatCompletion, type = nul
|
|||
if (canIncludeNative(chatPrompt.native)) {
|
||||
chatMessage.native = chatPrompt.native;
|
||||
}
|
||||
if (chatPrompt.reasoningContent) {
|
||||
chatMessage.reasoningContent = chatPrompt.reasoningContent;
|
||||
}
|
||||
if (Array.isArray(chatPrompt.media) && chatPrompt.media.length) {
|
||||
if (chatPrompt.mediaDisplay === MEDIA_DISPLAY.LIST) {
|
||||
for (const media of chatPrompt.media) {
|
||||
|
|
@ -5250,6 +5246,7 @@ class Message {
|
|||
...(this.name ? { name: this.name } : {}),
|
||||
tool_calls: JSON.stringify(this.tool_calls),
|
||||
...(this.reasoning ? { reasoning: this.reasoning } : {}),
|
||||
...(this.reasoningContent ? { reasoning_content: this.reasoningContent } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -8474,15 +8471,23 @@ function isCustomGeminiGenerateContent(settings = oai_settings) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Check whether an OpenAI-compatible source points at a DeepSeek V4 family model.
|
||||
* Matches the last `/` segment so gateway-prefixed ids (GO/deepseek-flash,
|
||||
* OR/deepseek-v4.1-flash) resolve; DeepSeek 3.x never matches.
|
||||
* @param {string} source Chat completion source id
|
||||
* Check whether the selected connection replays DeepSeek tool reasoning.
|
||||
* Keep this replay predicate scoped to Chat Completions; native-format continuations
|
||||
* have their own representation. Request normalization remains in the backend.
|
||||
* @param {ChatCompletionSettings} settings Settings object to use
|
||||
* @param {string} model Model id
|
||||
* @returns {boolean} True when the compat source serves a DeepSeek V4 model
|
||||
* @returns {boolean} Whether tool turns use DeepSeek reasoning_content
|
||||
*/
|
||||
function isDeepSeekV4CompatSource(source, model) {
|
||||
if (source !== chat_completion_sources.CUSTOM && source !== chat_completion_sources.OPENCODE) {
|
||||
function usesDeepSeekReasoningContent(settings, model = getChatCompletionModel(settings)) {
|
||||
const source = settings.chat_completion_source;
|
||||
if (source === chat_completion_sources.DEEPSEEK) {
|
||||
return true;
|
||||
}
|
||||
const isCompat = (source === chat_completion_sources.CUSTOM
|
||||
&& (settings.custom_api_format ?? custom_api_formats.OPENAI_COMPAT) === custom_api_formats.OPENAI_COMPAT)
|
||||
|| (source === chat_completion_sources.OPENCODE
|
||||
&& (settings.opencode_api_format ?? OPENCODE_API_FORMAT.OPENAI_COMPAT) === OPENCODE_API_FORMAT.OPENAI_COMPAT);
|
||||
if (!isCompat) {
|
||||
return false;
|
||||
}
|
||||
const lastSegment = String(model ?? '').trim().toLowerCase().split('/').pop() ?? '';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue