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
This commit is contained in:
lzt404 committed 2026-09-11 16:25:14 +08:00
1 parent 4d8d549e04
commit 9ba87748f8
11 files changed
+177 -8

No files matched your search

+5 -1
View File
@@ -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);
+37 -2
View File
@@ -160,6 +160,41 @@ pub(crate) fn harness_api_metadata(item: &Value) -> Option<Value> {
(!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::<Vec<_>>()}))?;
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<Vec<AgentModelOption>, 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<HarnessEditorSnapshot, String> {
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<HarnessEditorSnapshot, String> {
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()
@@ -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");
+5
View File
@@ -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 の既定値に従います。",
+5
View File
@@ -20,6 +20,11 @@ export const en: Record<MessageKey, string> = {
"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.",
+5
View File
@@ -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 默认值。",
+27 -4
View File
@@ -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<string | null>(null);
return <div className="harness-context-window">
<label><span>{label}{value !== undefined ? <small className="harness-override">{t('agents.catalog.customized')}</small> : null}</span>
<input aria-label={label} type="number" min={1} step={1} value={editing ?? value ?? String(inherited.value)}
onFocus={event => setEditing(event.currentTarget.value)}
onChange={event => { setEditing(event.currentTarget.value); onChange(event.currentTarget.value); }}
onBlur={() => setEditing(null)} />
</label>
<div className="harness-context-source">
<small>{t(`agents.harness.contextSource.${value !== undefined ? 'custom' : inherited.source}`)}</small>
{value !== undefined ? <button type="button" className="secondary-button" onClick={() => { setEditing(null); onChange(''); }}>{t('agents.harness.resetContext')}</button> : null}
</div>
</div>;
}
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 <HarnessContextWindowInput key={key} label={label(field.name)} value={draft[key]} inherited={contextDefault ?? harnessContextDefault({}, {})} onChange={set} />;
}
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) ? <>
<div className="codex-catalog-model-heading"><div><h3>{active ? active.id : t('agents.harness.provider')}</h3><span>{t('agents.harness.inheritHint')}</span></div><button className="secondary-button" onClick={() => { if (active) setDrafts(current => ({ ...current, [active.id]: {} })); else setProvider({}); changed(); }}><RotateCcw size={14} />{t('agents.harness.reset')}</button></div>
{active ? <p className="codex-catalog-hint" role="note">{t('agents.harness.apiInput', { value: Array.isArray(active.defaults.input) ? active.defaults.input.join(', ') : t('agents.harness.unknown') })}</p> : <p className="codex-catalog-hint">{t('agents.harness.managedConnection', { url: api === 'anthropic-messages' ? snapshot.baseUrl.replace(/\/v1$/, '') : snapshot.baseUrl })}</p>}
<HarnessFields key={active?.id ?? 'provider'} group={active ? 'model' : 'provider'} draft={active ? drafts[active.id] ?? {} : provider} defaults={active?.defaults} api={api} onChange={next => { if (active) setDrafts(current => ({ ...current, [active.id]: next })); else setProvider(next); changed(); }} />
<HarnessFields key={active?.id ?? 'provider'} group={active ? 'model' : 'provider'} draft={active ? drafts[active.id] ?? {} : provider} defaults={active?.defaults} api={api} contextDefault={harnessContextDefault(active?.defaults ?? {}, active ? provider : {})} onChange={next => { if (active) setDrafts(current => ({ ...current, [active.id]: next })); else setProvider(next); changed(); }} />
</> : null}
</fieldset></main>
</div>
+10
View File
@@ -15,6 +15,16 @@ export type HarnessField = {
exclusiveMin?: number;
};
export const harnessSchema = schemaSource as unknown as Record<string, HarnessField[]>;
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 = {
+6
View File
@@ -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; }
+7 -1
View File
@@ -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 } });
+31
View File
@@ -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 });