Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 88 additions & 1 deletion apps/codex-plus-manager/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3102,7 +3102,26 @@ pub fn save_relay_file(request: SaveRelayFileRequest) -> CommandResult<RelayFile
}),
);
};
match save_relay_file_in_home(&home, &request.kind, &request.contents)
let active_profile = SettingsStore::default()
.load()
.unwrap_or_default()
.active_relay_profile();
let contents =
match prepare_relay_file_contents(&request.kind, &request.contents, &active_profile) {
Ok(contents) => contents,
Err(error) => {
return failed(
&format!("保存配置文件失败:{error}"),
relay_files_payload_from_home(&home).unwrap_or_else(|_| RelayFilesPayload {
config_path: home.join("config.toml").to_string_lossy().to_string(),
auth_path: home.join("auth.json").to_string_lossy().to_string(),
config_contents: String::new(),
auth_contents: String::new(),
}),
);
}
};
match save_relay_file_in_home(&home, &request.kind, &contents)
.and_then(|_| relay_files_payload_from_home(&home))
{
Ok(payload) => ok("配置文件已保存。", payload),
Expand Down Expand Up @@ -4324,6 +4343,19 @@ fn save_relay_file_in_home(
Ok(())
}

fn prepare_relay_file_contents(
kind: &str,
contents: &str,
profile: &RelayProfile,
) -> anyhow::Result<String> {
if kind == "config" {
return codex_plus_core::relay_config::apply_deepseek_responses_compatibility(
profile, contents,
);
}
Ok(contents.to_string())
}

fn read_optional_text_file(path: &std::path::Path) -> anyhow::Result<String> {
match std::fs::read_to_string(path) {
Ok(contents) => Ok(contents),
Expand Down Expand Up @@ -5629,6 +5661,61 @@ mod tests {
assert!(save_relay_file_in_home(temp.path(), "../bad", "").is_err());
}

#[test]
fn config_save_applies_official_deepseek_responses_compatibility() {
let profile = RelayProfile {
id: "custom-deepseek".to_string(),
base_url: "https://api.deepseek.com/".to_string(),
upstream_base_url: "https://api.deepseek.com/".to_string(),
protocol: codex_plus_core::settings::RelayProtocol::Responses,
..RelayProfile::default()
};
let config = r#"[features]
unified_exec = true
code_mode_only = true

[features.code_mode]
enabled = true
"#;

let prepared = prepare_relay_file_contents("config", config, &profile).unwrap();

assert!(prepared.contains("unified_exec = true"));
assert!(prepared.contains("code_mode_only = false"));
assert!(prepared.contains("enabled = false"));
}

#[test]
fn config_save_preserves_third_party_responses_and_deepseek_chat_completions() {
let config = r#"[features]
code_mode_only = true

[features.code_mode]
enabled = true
"#;
let third_party = RelayProfile {
base_url: "https://relay.example/v1".to_string(),
upstream_base_url: "https://relay.example/v1".to_string(),
protocol: codex_plus_core::settings::RelayProtocol::Responses,
..RelayProfile::default()
};
let chat_completions = RelayProfile {
base_url: "https://api.deepseek.com/".to_string(),
upstream_base_url: "https://api.deepseek.com/".to_string(),
protocol: codex_plus_core::settings::RelayProtocol::ChatCompletions,
..RelayProfile::default()
};

assert_eq!(
prepare_relay_file_contents("config", config, &third_party).unwrap(),
config
);
assert_eq!(
prepare_relay_file_contents("config", config, &chat_completions).unwrap(),
config
);
}

#[test]
fn normalize_settings_before_save_preserves_profile_context_until_manual_extract() {
let settings = BackendSettings {
Expand Down
13 changes: 13 additions & 0 deletions apps/codex-plus-manager/src/presets.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import assert from "node:assert/strict";
import test from "node:test";

import { PRESETS } from "./presets.ts";

test("DeepSeek preset uses the official Responses integration", () => {
const preset = PRESETS.find((candidate) => candidate.id === "deepseek");
assert.ok(preset);
assert.equal(preset.baseUrl, "https://api.deepseek.com/");
assert.equal(preset.protocol, "responses");
assert.equal(preset.model, "deepseek-v4-flash");
assert.deepEqual(preset.modelList, ["deepseek-v4-flash", "deepseek-v4-pro"]);
});
4 changes: 2 additions & 2 deletions apps/codex-plus-manager/src/presets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ export const PRESETS: ProviderPreset[] = [
websiteUrl: "https://platform.deepseek.com",
apiKeyUrl: "https://platform.deepseek.com/api_keys",
category: "cn_official",
baseUrl: "https://api.deepseek.com",
protocol: "chatCompletions",
baseUrl: "https://api.deepseek.com/",
protocol: "responses",
model: "deepseek-v4-flash",
modelList: ["deepseek-v4-flash", "deepseek-v4-pro"],
},
Expand Down
82 changes: 82 additions & 0 deletions assets/deepseek-model-metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
{
"models": [
{
"slug": "deepseek-v4-flash",
"display_name": "DeepSeek-V4-Flash",
"description": "Latest frontier agentic coding model.",
"prefer_websockets": false,
"support_verbosity": true,
"default_verbosity": "low",
"apply_patch_tool_type": "freeform",
"web_search_tool_type": "text",
"input_modalities": ["text"],
"supports_image_detail_original": false,
"truncation_policy": { "mode": "tokens", "limit": 10000 },
"supports_parallel_tool_calls": true,
"additional_speed_tiers": [],
"service_tiers": [],
"tool_mode": null,
"supports_search_tool": false,
"multi_agent_version": "v2",
"use_responses_lite": false,
"include_skills_usage_instructions": false,
"context_window": 1048576,
"max_context_window": 1048576,
"effective_context_window_percent": 95,
"auto_compact_token_limit": null,
"comp_hash": "3000",
"reasoning_summary_format": "experimental",
"default_reasoning_summary": "none",
"default_reasoning_level": "high",
"supported_reasoning_levels": [
{ "effort": "low", "description": "Fast responses with lighter reasoning" },
{ "effort": "high", "description": "Extra high reasoning depth for complex problems" },
{ "effort": "max", "description": "Maximum reasoning depth for the hardest problems" }
],
"shell_type": "shell_command",
"visibility": "list",
"minimal_client_version": "0.144.0",
"supported_in_api": true,
"priority": 1
},
{
"slug": "deepseek-v4-pro",
"display_name": "DeepSeek-V4-Pro",
"description": "Most capable frontier agentic coding model.",
"prefer_websockets": false,
"support_verbosity": true,
"default_verbosity": "low",
"apply_patch_tool_type": "freeform",
"web_search_tool_type": "text",
"input_modalities": ["text"],
"supports_image_detail_original": false,
"truncation_policy": { "mode": "tokens", "limit": 10000 },
"supports_parallel_tool_calls": true,
"additional_speed_tiers": [],
"service_tiers": [],
"tool_mode": null,
"supports_search_tool": false,
"multi_agent_version": "v2",
"use_responses_lite": false,
"include_skills_usage_instructions": false,
"context_window": 1048576,
"max_context_window": 1048576,
"effective_context_window_percent": 95,
"auto_compact_token_limit": null,
"comp_hash": "3000",
"reasoning_summary_format": "experimental",
"default_reasoning_summary": "none",
"default_reasoning_level": "high",
"supported_reasoning_levels": [
{ "effort": "low", "description": "Fast responses with lighter reasoning" },
{ "effort": "high", "description": "Extra high reasoning depth for complex problems" },
{ "effort": "max", "description": "Maximum reasoning depth for the hardest problems" }
],
"shell_type": "shell_command",
"visibility": "list",
"minimal_client_version": "0.144.0",
"supported_in_api": true,
"priority": 2
}
]
}
50 changes: 40 additions & 10 deletions crates/codex-plus-core/src/model_suffix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,11 @@ const GPT56_METADATA_JSON: &str = include_str!(concat!(
"/../../assets/gpt56-model-metadata-compat.json"
));

const DEEPSEEK_METADATA_JSON: &str = include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../assets/deepseek-model-metadata.json"
));

pub fn requires_bundled_metadata_catalog(slug: &str) -> bool {
gpt56_metadata_entry(slug).is_some()
}
Expand Down Expand Up @@ -204,7 +209,7 @@ pub fn build_model_catalog_json(
entries: &[ModelCatalogEntry],
fallback_window: Option<u64>,
) -> String {
build_model_catalog_json_with_capabilities(entries, fallback_window, None, None)
build_model_catalog_json_with_capabilities(entries, fallback_window, None, None, false)
}

/// 使用指定模板(或内置 bundled 模板)构建 catalog。
Expand All @@ -214,7 +219,7 @@ pub fn build_model_catalog_json_with_template(
fallback_window: Option<u64>,
template: Option<&Value>,
) -> String {
build_model_catalog_json_with_capabilities(entries, fallback_window, template, None)
build_model_catalog_json_with_capabilities(entries, fallback_window, template, None, false)
}

/// 使用显式 provider capability 构建 catalog。
Expand All @@ -225,15 +230,21 @@ pub(crate) fn build_model_catalog_json_with_capabilities(
fallback_window: Option<u64>,
template: Option<&Value>,
use_responses_lite_override: Option<bool>,
deepseek_metadata: bool,
) -> String {
let models: Vec<Value> = entries
.iter()
.enumerate()
.map(|(index, entry)| {
let (mut model, has_model_metadata) = template
.cloned()
.map(|template| (template, false))
.unwrap_or_else(|| model_template_entry(&entry.slug));
let (mut model, has_model_metadata) = if deepseek_metadata {
deepseek_model_template_entry(&entry.slug)
.unwrap_or_else(|| model_template_entry(&entry.slug))
} else {
template
.cloned()
.map(|template| (template, false))
.unwrap_or_else(|| model_template_entry(&entry.slug))
};
let metadata_window = model.get("context_window").and_then(Value::as_u64);
let context_window = entry
.suffix_window
Expand All @@ -247,12 +258,16 @@ pub(crate) fn build_model_catalog_json_with_capabilities(
}
model["context_window"] = json!(context_window);
model["max_context_window"] = json!(context_window);
// 默认 95 会让 1M 显示为 950K,显式写 100 以显示真实窗口。
model["effective_context_window_percent"] = json!(100);
// 通用自定义模型显示完整窗口;DeepSeek Responses 保留官方目录的 95%。
if !deepseek_metadata {
model["effective_context_window_percent"] = json!(100);
}
model["auto_compact_token_limit"] = Value::Null;
model["priority"] = json!(1000 + index);
model["visibility"] = json!("list");
model["supported_in_api"] = json!(true);
if !deepseek_metadata {
model["supported_in_api"] = json!(true);
}
if let Some(use_responses_lite) = use_responses_lite_override {
model["use_responses_lite"] = json!(use_responses_lite);
}
Expand All @@ -268,6 +283,17 @@ pub(crate) fn build_model_catalog_json_with_capabilities(
serde_json::to_string_pretty(&json!({ "models": models })).unwrap_or_default()
}

fn deepseek_model_template_entry(slug: &str) -> Option<(Value, bool)> {
let compatibility = catalog_metadata_entry(DEEPSEEK_METADATA_JSON, slug)?;
let mut template = first_bundled_template_entry().unwrap_or_else(|| json!({}));
if let (Some(target), Some(source)) = (template.as_object_mut(), compatibility.as_object()) {
for (key, value) in source {
target.insert(key.clone(), value.clone());
}
}
Some((template, true))
}

fn model_template_entry(slug: &str) -> (Value, bool) {
if let Some(entry) = bundled_template_entry(slug) {
return (entry, true);
Expand Down Expand Up @@ -304,7 +330,11 @@ fn first_bundled_template_entry() -> Option<Value> {
}

fn gpt56_metadata_entry(slug: &str) -> Option<Value> {
let catalog: Value = serde_json::from_str(GPT56_METADATA_JSON).ok()?;
catalog_metadata_entry(GPT56_METADATA_JSON, slug)
}

fn catalog_metadata_entry(catalog_json: &str, slug: &str) -> Option<Value> {
let catalog: Value = serde_json::from_str(catalog_json).ok()?;
catalog
.get("models")?
.as_array()?
Expand Down
Loading
Loading