From 9ba87748f87865a862f1dfb10ffa46407349424b Mon Sep 17 00:00:00 2001 From: lzt404 <2596933790@qq.com> Date: Fri, 11 Sep 2026 15:10:48 +0800 Subject: [PATCH] fix(agents): display resolved DeepSeek Harness context window values - Read context limits from the core catalog and configuration - Show numeric values with their source instead of automatic placeholders - Preserve manual overrides and support resetting to inherited values --- src-tauri/src/agents/commands.rs | 6 ++- src-tauri/src/agents/deepseek_harness.rs | 39 ++++++++++++++++++- .../src/tests/deepseek_harness_catalog.rs | 39 +++++++++++++++++++ src/i18n/ja.ts | 5 +++ src/i18n/locales/en.ts | 5 +++ src/i18n/locales/zh-CN.ts | 5 +++ src/pages/DeepSeekHarnessCatalogDialog.tsx | 31 +++++++++++++-- src/services/deepSeekHarnessCatalog.ts | 10 +++++ src/styles.css | 6 +++ tests/deepSeekHarnessCatalog.test.ts | 8 +++- tests/deepseek-harness-catalog-ui.cjs | 31 +++++++++++++++ 11 files changed, 177 insertions(+), 8 deletions(-) diff --git a/src-tauri/src/agents/commands.rs b/src-tauri/src/agents/commands.rs index e75f992..1b70043 100644 --- a/src-tauri/src/agents/commands.rs +++ b/src-tauri/src/agents/commands.rs @@ -826,7 +826,11 @@ pub(crate) async fn fetch_prepared_agent_models( let runtime_models = fetch_codex_catalog_runtime_models(config).await?; prepare_codex_agent_models(&runtime_models) } else { - let mut models = fetch_agent_models(config.port, api_key).await?; + let mut models = if client == AgentClient::DeepSeekHarness { + fetch_deepseek_harness_models(config).await? + } else { + fetch_agent_models(config.port, api_key).await? + }; if agent_uses_cpa_runtime_context_windows(client) { let runtime_models = fetch_codex_runtime_models(config.port, api_key).await?; codex_catalog::merge_runtime_context_windows(&mut models, &runtime_models); diff --git a/src-tauri/src/agents/deepseek_harness.rs b/src-tauri/src/agents/deepseek_harness.rs index 3c6ab66..d3259fe 100644 --- a/src-tauri/src/agents/deepseek_harness.rs +++ b/src-tauri/src/agents/deepseek_harness.rs @@ -160,6 +160,41 @@ pub(crate) fn harness_api_metadata(item: &Value) -> Option { (!fields.is_empty()).then_some(Value::Object(fields)) } +pub(crate) fn enrich_harness_context_windows( + models: &mut [AgentModelOption], + runtime: &[codex_catalog::CodexRuntimeModel], + configuration: Option<&str>, +) -> Result<(), String> { + codex_catalog::merge_runtime_context_windows(models, runtime); + if let Some(configuration) = configuration { + let mut configured = + codex_catalog::parse_runtime_models(&json!({"data": models.iter().map(|model| { + json!({"id": model.name, "context_window": model.context_window}) + }).collect::>()}))?; + codex_catalog::apply_configured_context_limits(&mut configured, configuration)?; + codex_catalog::merge_runtime_context_windows(models, &configured); + } + Ok(()) +} + +pub(crate) async fn fetch_deepseek_harness_models( + config: &GuiConfigFile, +) -> Result, String> { + let api_key = effective_agent_api_key(config); + let (models, runtime, configuration) = tokio::join!( + fetch_agent_models(config.port, api_key), + fetch_codex_runtime_models(config.port, api_key), + fetch_management_config_yaml(config), + ); + let mut models = models?; + enrich_harness_context_windows( + &mut models, + &runtime.unwrap_or_default(), + configuration.as_deref().ok(), + )?; + Ok(models) +} + fn harness_model_defaults(model: &AgentModelOption, api: &str) -> Profile { let mut fields = model .harness_metadata @@ -657,7 +692,7 @@ pub(crate) async fn get_deepseek_harness_model_catalog_editor( gui_config_state: tauri::State<'_, GuiConfigState>, ) -> Result { let config = gui_config_state.snapshot()?; - let models = fetch_agent_models(config.port, effective_agent_api_key(&config)).await?; + let models = fetch_deepseek_harness_models(&config).await?; let home = app.path().home_dir().map_err(|_| "无法获取用户目录")?; let _guard = AGENT_CONFIG_FILE_LOCK .lock() @@ -672,7 +707,7 @@ pub(crate) async fn save_deepseek_harness_model_catalog_editor( request: HarnessEditorRequest, ) -> Result { let config = gui_config_state.snapshot()?; - let models = fetch_agent_models(config.port, effective_agent_api_key(&config)).await?; + let models = fetch_deepseek_harness_models(&config).await?; let home = app.path().home_dir().map_err(|_| "无法获取用户目录")?; let _guard = AGENT_CONFIG_FILE_LOCK .lock() diff --git a/src-tauri/src/tests/deepseek_harness_catalog.rs b/src-tauri/src/tests/deepseek_harness_catalog.rs index 19c27f6..7f32d70 100644 --- a/src-tauri/src/tests/deepseek_harness_catalog.rs +++ b/src-tauri/src/tests/deepseek_harness_catalog.rs @@ -108,6 +108,45 @@ async fn harness_discovery_uses_authenticated_models_endpoint() { assert!(models[1].input_modalities.is_none()); } +#[test] +fn harness_context_enrichment_matches_codex_and_preserves_manual_overrides() { + let home = agent_test_home("harness-context-enrichment"); + let mut models = models(); + let runtime = codex_catalog::parse_runtime_models(&json!({"models":[ + {"slug":"VISION","context_window":272000,"max_context_window":872000,"input_modalities":["text"]}, + {"slug":"text-only","max_context_window":131072,"input_modalities":["text","image"]}, + {"slug":"unknown","input_modalities":["text","image"]} + ]})).unwrap(); + enrich_harness_context_windows(&mut models, &runtime, None).unwrap(); + assert_eq!(models[0].context_window, Some(272000)); + assert_eq!(models[1].context_window, Some(131072)); + assert_eq!(models[2].context_window, None); + assert_eq!(models[1].input_modalities, Some(vec!["text".into()])); + assert_eq!(models[2].input_modalities, None); + enrich_harness_context_windows(&mut models, &[], Some("codex-api-key:\n - models:\n - name: upstream-model\n alias: vision\n max-context-length: 64000\n")).unwrap(); + let snapshot = harness_editor_snapshot(&home, &models, 8317).unwrap(); + assert_eq!(snapshot.models[0].defaults["contextWindow"], 64000); + assert!(snapshot + .models + .iter() + .all(|model| model.configuration.is_empty())); + apply(&home, &models); + assert_eq!(provider(&home)["models"][0]["contextWindow"], 64000); + let mut edit = request(harness_editor_snapshot(&home, &models, 8317).unwrap()); + edit.models[0] + .configuration + .insert("contextWindow".into(), json!(96000)); + save_harness_editor(&home, &models, 8317, edit).unwrap(); + enrich_harness_context_windows(&mut models, &runtime, None).unwrap(); + apply(&home, &models); + assert_eq!(provider(&home)["models"][0]["contextWindow"], 96000); + let mut reset = request(harness_editor_snapshot(&home, &models, 8317).unwrap()); + reset.models[0].configuration.clear(); + save_harness_editor(&home, &models, 8317, reset).unwrap(); + assert_eq!(provider(&home)["models"][0]["contextWindow"], 272000); + fs::remove_dir_all(home).unwrap(); +} + #[test] fn harness_refreshes_api_defaults_and_preserves_catalog_overrides() { let home = agent_test_home("harness-catalog-refresh"); diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index 3ea014f..1059953 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -70,6 +70,11 @@ export const jaOverrides = { "agents.harness.title": "DeepSeek Harness モデル設定", "agents.harness.subtitle": "API のモデル一覧を取得し、モデル能力とプロバイダー設定を調整します。", "agents.harness.provider": "プロバイダー設定", + "agents.harness.contextSource.model": "取得元:コアのモデル一覧 / モデル API", + "agents.harness.contextSource.provider": "取得元:プロバイダーの既定コンテキスト", + "agents.harness.contextSource.default": "取得元:DSH の既定値", + "agents.harness.contextSource.custom": "取得元:手動設定", + "agents.harness.resetContext": "コンテキストをリセット", "agents.harness.auto": "自動", "agents.harness.apiSource": "API に従う", "agents.harness.inheritHint": "空欄は API または DSH の既定値に従います。", diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index d8f6470..ecba040 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -20,6 +20,11 @@ export const en: Record = { "agents.harness.title": "DeepSeek Harness model configuration", "agents.harness.subtitle": "Load the model catalog from the API and customize models and provider settings.", "agents.harness.provider": "Provider settings", + "agents.harness.contextSource.model": "Source: core model catalog / model API", + "agents.harness.contextSource.provider": "Source: provider default context", + "agents.harness.contextSource.default": "Source: DSH default", + "agents.harness.contextSource.custom": "Source: manual override", + "agents.harness.resetContext": "Reset context window", "agents.harness.auto": "Automatic", "agents.harness.apiSource": "Follow API", "agents.harness.inheritHint": "Leave fields empty to follow the API or DSH defaults.", diff --git a/src/i18n/locales/zh-CN.ts b/src/i18n/locales/zh-CN.ts index 50c7e9e..28e265d 100644 --- a/src/i18n/locales/zh-CN.ts +++ b/src/i18n/locales/zh-CN.ts @@ -19,6 +19,11 @@ export const zhCN = { "agents.harness.title": "DeepSeek Harness 模型配置", "agents.harness.subtitle": "从 API 获取整个模型目录,按需覆盖模型能力和供应商参数。", "agents.harness.provider": "供应商设置", + "agents.harness.contextSource.model": "来源:内核模型目录 / 模型 API", + "agents.harness.contextSource.provider": "来源:供应商默认上下文", + "agents.harness.contextSource.default": "来源:DSH 默认值", + "agents.harness.contextSource.custom": "来源:手动设置", + "agents.harness.resetContext": "恢复默认上下文", "agents.harness.auto": "自动", "agents.harness.apiSource": "跟随 API", "agents.harness.inheritHint": "留空即跟随 API 或 DSH 默认值。", diff --git a/src/pages/DeepSeekHarnessCatalogDialog.tsx b/src/pages/DeepSeekHarnessCatalogDialog.tsx index ae0dd9a..63275c3 100644 --- a/src/pages/DeepSeekHarnessCatalogDialog.tsx +++ b/src/pages/DeepSeekHarnessCatalogDialog.tsx @@ -4,12 +4,32 @@ import { LoaderCircle, RefreshCw, RotateCcw, Search, Settings2, X } from 'lucide import { useI18n } from '../i18n'; import type { MessageKey } from '../i18n/resources'; import { - harnessDraft, harnessReasoningLevels, harnessSchema, isHarnessRecord, parseHarnessDraft, sameHarnessDraft, updateHarnessDraft, - type HarnessDraft, type HarnessEditorSnapshot, type HarnessProfile, + harnessContextDefault, harnessDraft, harnessReasoningLevels, harnessSchema, isHarnessRecord, parseHarnessDraft, sameHarnessDraft, updateHarnessDraft, + type HarnessContextDefault, type HarnessDraft, type HarnessEditorSnapshot, type HarnessProfile, } from '../services/deepSeekHarnessCatalog'; -function HarnessFields({ group, draft, defaults = {}, prefix = '', api, onChange }: { +function HarnessContextWindowInput({ label, value, inherited, onChange }: { + label: string; value?: string; inherited: HarnessContextDefault; onChange: (value: string) => void; +}) { + const { t } = useI18n(); + const [editing, setEditing] = useState(null); + return
+ +
+ {t(`agents.harness.contextSource.${value !== undefined ? 'custom' : inherited.source}`)} + {value !== undefined ? : null} +
+
; +} + +function HarnessFields({ group, draft, defaults = {}, prefix = '', api, contextDefault, onChange }: { group: string; draft: HarnessDraft; defaults?: HarnessProfile; prefix?: string; api: string; + contextDefault?: HarnessContextDefault; onChange: (next: HarnessDraft) => void; }) { const { t } = useI18n(); @@ -20,6 +40,9 @@ function HarnessFields({ group, draft, defaults = {}, prefix = '', api, onChange const supplied = defaults[field.name] ?? field.default; const automatic = `${t('agents.harness.auto')}${supplied === undefined ? '' : ` (${typeof supplied === 'string' ? supplied : JSON.stringify(supplied)})`}`; const set = (next: string) => onChange(updateHarnessDraft(draft, key, next)); + if (field.name === 'contextWindow' || field.name === 'defaultContextWindow') { + return ; + } if (field.apis && !field.apis.includes(api) && !value) return null; if (field.group) { const customized = Object.keys(draft).some(k => k.startsWith(`${key}.`)); @@ -155,7 +178,7 @@ export function DeepSeekHarnessCatalogDialog({ onClose, onSaved }: { onClose: () {snapshot && (active || selected === null) ? <>

{active ? active.id : t('agents.harness.provider')}

{t('agents.harness.inheritHint')}
{active ?

{t('agents.harness.apiInput', { value: Array.isArray(active.defaults.input) ? active.defaults.input.join(', ') : t('agents.harness.unknown') })}

:

{t('agents.harness.managedConnection', { url: api === 'anthropic-messages' ? snapshot.baseUrl.replace(/\/v1$/, '') : snapshot.baseUrl })}

} - { if (active) setDrafts(current => ({ ...current, [active.id]: next })); else setProvider(next); changed(); }} /> + { if (active) setDrafts(current => ({ ...current, [active.id]: next })); else setProvider(next); changed(); }} /> : null} diff --git a/src/services/deepSeekHarnessCatalog.ts b/src/services/deepSeekHarnessCatalog.ts index 86d1e29..12edbb8 100644 --- a/src/services/deepSeekHarnessCatalog.ts +++ b/src/services/deepSeekHarnessCatalog.ts @@ -15,6 +15,16 @@ export type HarnessField = { exclusiveMin?: number; }; export const harnessSchema = schemaSource as unknown as Record; +export type HarnessContextDefault = { value: number; source: 'model' | 'provider' | 'default' }; + +export function harnessContextDefault(defaults: HarnessProfile, provider: HarnessDraft): HarnessContextDefault { + const valid = (value: unknown): value is number => typeof value === 'number' && Number.isSafeInteger(value) && value > 0; + if (valid(defaults.contextWindow)) return { value: defaults.contextWindow, source: 'model' }; + const configured = Number(provider.defaultContextWindow); + if (valid(configured)) return { value: configured, source: 'provider' }; + return { value: harnessSchema.provider.find(field => field.name === 'defaultContextWindow')!.default as number, source: 'default' }; +} + export const harnessReasoningLevels = ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']; export type HarnessEditorModel = { id: string; defaults: HarnessProfile; configuration: HarnessProfile }; export type HarnessEditorSnapshot = { diff --git a/src/styles.css b/src/styles.css index dcbdbba..3ed5673 100644 --- a/src/styles.css +++ b/src/styles.css @@ -13860,3 +13860,9 @@ body.table-col-resizing * { .harness-catalog-dialog .secondary-button { display: inline-flex; align-items: center; justify-content: center; gap: 7px; } .harness-catalog-form small { font-weight: 400; } .harness-catalog-form { margin-top: 12px; } + +.harness-context-window { display: grid; align-content: start; gap: 8px; min-width: 0; } +.harness-context-window > label { display: grid; gap: 8px; font-size: var(--font-size-label); font-weight: 600; } +.harness-context-window > label > span { display: flex; flex-wrap: wrap; gap: 6px; } +.harness-context-source { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; } +.harness-context-source > button { padding: 4px 8px; font-size: 12px; } diff --git a/tests/deepSeekHarnessCatalog.test.ts b/tests/deepSeekHarnessCatalog.test.ts index 9533be5..a7f2fd5 100644 --- a/tests/deepSeekHarnessCatalog.test.ts +++ b/tests/deepSeekHarnessCatalog.test.ts @@ -1,7 +1,13 @@ import { describe, expect, test } from 'bun:test'; -import { harnessDraft, parseHarnessDraft, sameHarnessDraft, updateHarnessDraft } from '../src/services/deepSeekHarnessCatalog'; +import { harnessContextDefault, harnessDraft, parseHarnessDraft, sameHarnessDraft, updateHarnessDraft } from '../src/services/deepSeekHarnessCatalog'; describe('DSH catalog overrides', () => { + test('context values use model metadata, provider configuration, then DSH defaults', () => { + expect(harnessContextDefault({ contextWindow: 128000 }, { defaultContextWindow: '64000' })).toEqual({ value: 128000, source: 'model' }); + expect(harnessContextDefault({}, { defaultContextWindow: '64000' })).toEqual({ value: 64000, source: 'provider' }); + expect(harnessContextDefault({}, {})).toEqual({ value: 262144, source: 'default' }); + expect(harnessContextDefault({ contextWindow: null }, { defaultContextWindow: '0' })).toEqual({ value: 262144, source: 'default' }); + }); test('empty values inherit while explicit false and zero remain overrides', () => { const draft = harnessDraft({ timeoutMs: 0, retryPolicy: { mode: 'normal', maxRetries: 0 }, compat: { supportsStore: false } }, 'provider'); expect(parseHarnessDraft(draft, 'provider', 'openai-completions')).toEqual({ timeoutMs: 0, retryPolicy: { mode: 'normal', maxRetries: 0 }, compat: { supportsStore: false } }); diff --git a/tests/deepseek-harness-catalog-ui.cjs b/tests/deepseek-harness-catalog-ui.cjs index 60c1161..1067d69 100644 --- a/tests/deepseek-harness-catalog-ui.cjs +++ b/tests/deepseek-harness-catalog-ui.cjs @@ -22,15 +22,40 @@ const path = require('node:path'); try { for (const mode of ['', 'embedded&']) { await open(mode); + const context = () => dialog().getByRole('spinbutton', { name: '上下文窗口(tokens)' }); + assert.equal(await context().inputValue(), '128000'); + await dialog().getByText('来源:内核模型目录 / 模型 API', { exact: true }).waitFor(); + await context().focus(); + await page.keyboard.press('Tab'); + assert.equal(await dialog().getByRole('button', { name: '保存', exact: true }).isDisabled(), true); + await context().fill('192000'); + await dialog().getByRole('button', { name: '恢复默认上下文', exact: true }).click(); + assert.equal(await context().inputValue(), '128000'); + assert.equal(await dialog().getByRole('button', { name: '保存', exact: true }).isDisabled(), true); assert.equal(await dialog().getByRole('combobox', { name: '输入类型', exact: true }).inputValue(), ''); await dialog().getByRole('option', { name: /unknown-model/ }).click(); await dialog().getByText(/未提供(不代表不支持)/).waitFor(); + assert.equal(await context().inputValue(), '262144'); + await dialog().getByText('来源:DSH 默认值', { exact: true }).waitFor(); await dialog().getByRole('combobox', { name: '输入类型', exact: true }).selectOption('["text","image"]'); await dialog().getByRole('option', { name: /gpt-two/ }).click(); await dialog().getByRole('spinbutton', { name: '输出上限(tokens)' }).fill('4096'); await dialog().getByRole('combobox', { name: '推理等级', exact: true }).selectOption('custom'); await dialog().getByRole('textbox', { name: 'high reasoning_effort' }).fill('custom-high'); await dialog().getByRole('button', { name: '供应商设置', exact: true }).click(); + const providerContext = dialog().getByRole('spinbutton', { name: '默认上下文(tokens)', exact: true }); + assert.equal(await providerContext.inputValue(), '262144'); + await providerContext.fill('512000'); + await dialog().getByRole('option', { name: /unknown-model/ }).click(); + assert.equal(await context().inputValue(), '512000'); + await dialog().getByText('来源:供应商默认上下文', { exact: true }).waitFor(); + await context().fill('256000'); + await context().fill(''); + await page.keyboard.press('Tab'); + assert.equal(await context().inputValue(), '512000'); + await dialog().getByRole('option', { name: /gpt-one/ }).click(); + assert.equal(await context().inputValue(), '128000'); + await dialog().getByRole('button', { name: '供应商设置', exact: true }).click(); await dialog().getByRole('combobox', { name: '默认输入类型', exact: true }).selectOption('["text","image"]'); await dialog().getByRole('spinbutton', { name: '请求超时(ms)' }).fill('60000'); await save(); @@ -39,6 +64,8 @@ const path = require('node:path'); assert.equal(request.models.find(m => m.id === 'gpt-two').configuration.maxTokens, 4096); assert.equal(request.models.find(m => m.id === 'gpt-two').configuration.reasoningEfforts.high, 'custom-high'); assert.equal(request.provider.timeoutMs, 60000); + assert.equal(request.provider.defaultContextWindow, 512000); + assert(request.models.every(m => m.configuration.contextWindow === undefined)); assert.equal(await page.evaluate(() => window.fixtureCalls.some(c => c.cmd === 'update_agent_config')), false); await dialog().getByRole('button', { name: '关闭', exact: true }).click(); assert.match(await page.locator('.agent-model-trigger').innerText(), /gpt-one/); @@ -65,6 +92,10 @@ const path = require('node:path'); await page.getByRole('alertdialog').waitFor(); await page.getByRole('button', { name: '继续编辑', exact: true }).click(); await save(); + assert.equal(await context().inputValue(), '64000'); + await dialog().getByRole('button', { name: '恢复默认上下文', exact: true }).click(); + assert.equal(await context().inputValue(), '512000'); + await save(); } const screenshots = path.join(os.tmpdir(), 'cpa-harness-catalog-ui'); fs.mkdirSync(screenshots, { recursive: true });