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:
1 parent
4d8d549e04
commit
9ba87748f8
11 files changed
+177
-8
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 の既定値に従います。",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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 默认值。",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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; }
|
||||
@@ -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 } });
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in new issue
Block a user