feat(agents): add configurable DeepSeek Harness model catalog
- Separate default model selection from catalog configuration - Use API metadata with per-model and provider overrides - Expose documented capability, reasoning, protocol, and request settings - Preserve customizations with conflict detection and transactional rollback
This commit is contained in:
1 parent
145b585c3f
commit
4d8d549e04
28 files changed
+2047
-20
No files matched your search
@@ -0,0 +1,31 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { harnessDraft, parseHarnessDraft, sameHarnessDraft, updateHarnessDraft } from '../src/services/deepSeekHarnessCatalog';
|
||||
|
||||
describe('DSH catalog overrides', () => {
|
||||
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 } });
|
||||
expect(parseHarnessDraft(updateHarnessDraft(draft, 'timeoutMs', ''), 'provider', 'openai-completions').timeoutMs).toBeUndefined();
|
||||
expect(parseHarnessDraft({}, 'model', 'openai-completions')).toEqual({});
|
||||
expect(parseHarnessDraft({ input: '[]' }, 'model', 'openai-completions')).toEqual({ input: [] });
|
||||
});
|
||||
test('reasoning levels keep wire values and only off allows null', () => {
|
||||
expect(parseHarnessDraft({ reasoningEfforts: '{"off":null,"high":"custom-high"}' }, 'model', 'openai-completions')).toEqual({ reasoningEfforts: { off: null, high: 'custom-high' } });
|
||||
expect(parseHarnessDraft({ reasoningEfforts: 'false' }, 'model', 'openai-completions')).toEqual({ reasoningEfforts: false });
|
||||
expect(() => parseHarnessDraft({ reasoningEfforts: '{"high":null}' }, 'model', 'openai-completions')).toThrow();
|
||||
expect(() => parseHarnessDraft({ reasoningEfforts: '{}' }, 'model', 'openai-completions')).toThrow();
|
||||
});
|
||||
test('invalid numbers, JSON, headers and incompatible protocol switches are rejected', () => {
|
||||
for (const draft of [{ maxTokens: '-1' }, { input: '["audio"]' }, { contextWindow: '2.5' }, { 'compat.supportsStore': 'null' }, { 'compat.supportsTemperature': 'true' }]) {
|
||||
expect(() => parseHarnessDraft(draft, 'model', 'openai-completions')).toThrow();
|
||||
}
|
||||
expect(() => parseHarnessDraft({ headers: '{bad json}' }, 'provider', 'openai-completions')).toThrow();
|
||||
expect(() => parseHarnessDraft({ headers: '{"x-test":"a\\r\\nb"}' }, 'provider', 'openai-completions')).toThrow();
|
||||
expect(() => parseHarnessDraft({ 'retryPolicy.backoff.initialDelayMs': '20000' }, 'provider', 'openai-completions')).toThrow();
|
||||
});
|
||||
test('nested groups round trip and follow current protocol', () => {
|
||||
const profile = { api: 'anthropic-messages', compat: { supportsTemperature: false }, retryPolicy: { mode: 'always', backoff: { maxDelayMs: 60000 } }, thinkingBudgets: { high: 8192 } };
|
||||
expect(parseHarnessDraft(harnessDraft(profile, 'provider'), 'provider', 'anthropic-messages')).toEqual(profile);
|
||||
expect(sameHarnessDraft({ input: '["text"]', name: 'A' }, { name: 'A', input: '["text"]' })).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch({ channel: 'msedge', headless: true });
|
||||
const page = await browser.newPage();
|
||||
const errors = [];
|
||||
page.on('pageerror', e => errors.push(e.message));
|
||||
const open = async (query = '') => {
|
||||
await page.goto(`http://127.0.0.1:1421/tests/fixtures/agent-backups.html?client=deepseek-harness&reset-selections&${query}`);
|
||||
await page.getByRole('button', { name: '配置模型', exact: true }).click();
|
||||
await page.getByRole('option', { name: /gpt-one/ }).waitFor();
|
||||
};
|
||||
const dialog = () => page.getByRole('dialog', { name: 'DeepSeek Harness 模型配置' });
|
||||
const save = async () => {
|
||||
await dialog().getByRole('button', { name: '保存', exact: true }).click();
|
||||
await dialog().getByRole('status').filter({ hasText: /已保存/ }).waitFor();
|
||||
};
|
||||
try {
|
||||
for (const mode of ['', 'embedded&']) {
|
||||
await open(mode);
|
||||
assert.equal(await dialog().getByRole('combobox', { name: '输入类型', exact: true }).inputValue(), '');
|
||||
await dialog().getByRole('option', { name: /unknown-model/ }).click();
|
||||
await dialog().getByText(/未提供(不代表不支持)/).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();
|
||||
await dialog().getByRole('combobox', { name: '默认输入类型', exact: true }).selectOption('["text","image"]');
|
||||
await dialog().getByRole('spinbutton', { name: '请求超时(ms)' }).fill('60000');
|
||||
await save();
|
||||
const request = await page.evaluate(() => window.fixtureCalls.filter(c => c.cmd === 'save_deepseek_harness_model_catalog_editor').at(-1).args.request);
|
||||
assert.deepEqual(request.models.find(m => m.id === 'unknown-model').configuration.input, ['text','image']);
|
||||
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(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/);
|
||||
await page.evaluate(() => window.fixtureRemount());
|
||||
await page.getByRole('button', { name: '配置模型', exact: true }).click();
|
||||
await dialog().getByRole('option', { name: /unknown-model/ }).click();
|
||||
assert.equal(await dialog().getByRole('combobox', { name: '输入类型', exact: true }).inputValue(), '["text","image"]');
|
||||
await dialog().getByRole('button', { name: '恢复自动', exact: true }).click();
|
||||
await save();
|
||||
assert.equal(await dialog().getByRole('combobox', { name: '输入类型', exact: true }).inputValue(), '');
|
||||
await dialog().getByRole('spinbutton', { name: '上下文窗口(tokens)' }).fill('0');
|
||||
await dialog().getByRole('button', { name: '保存', exact: true }).click();
|
||||
await dialog().getByRole('alert').filter({ hasText: /contextWindow/ }).waitFor();
|
||||
await dialog().getByRole('spinbutton', { name: '上下文窗口(tokens)' }).fill('64000');
|
||||
await page.evaluate(() => window.fixtureFailHarnessSave = true);
|
||||
await dialog().getByRole('button', { name: '保存', exact: true }).click();
|
||||
await dialog().getByRole('alert').filter({ hasText: /模拟模型配置保存失败/ }).waitFor();
|
||||
assert.equal(await dialog().getByRole('spinbutton', { name: '上下文窗口(tokens)' }).inputValue(), '64000');
|
||||
await page.evaluate(() => { window.fixtureFailHarnessSave = false; window.fixtureStaleHarnessSave = true; });
|
||||
await dialog().getByRole('button', { name: '保存', exact: true }).click();
|
||||
await dialog().getByRole('alert').filter({ hasText: /模型列表或配置已变化/ }).waitFor();
|
||||
await page.evaluate(() => window.fixtureStaleHarnessSave = false);
|
||||
await page.keyboard.press('Escape');
|
||||
await page.getByRole('alertdialog').waitFor();
|
||||
await page.getByRole('button', { name: '继续编辑', exact: true }).click();
|
||||
await save();
|
||||
}
|
||||
const screenshots = path.join(os.tmpdir(), 'cpa-harness-catalog-ui');
|
||||
fs.mkdirSync(screenshots, { recursive: true });
|
||||
for (const width of [1280, 540]) for (const theme of ['light', 'dark']) {
|
||||
await page.setViewportSize({ width, height: 900 });
|
||||
await open(`theme=${theme}`);
|
||||
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth > innerWidth), false);
|
||||
await page.screenshot({ path: path.join(screenshots, `${width}-${theme}-model.png`) });
|
||||
await dialog().getByRole('button', { name: '供应商设置', exact: true }).click();
|
||||
assert.equal(await page.evaluate(() => document.querySelector('.codex-catalog-editor').scrollWidth > document.querySelector('.codex-catalog-editor').clientWidth), false);
|
||||
await page.screenshot({ path: path.join(screenshots, `${width}-${theme}-provider.png`) });
|
||||
}
|
||||
assert.deepEqual(errors, []);
|
||||
console.log('PASS: default model preserved, catalog overrides, API reset, reasoning, validation, retry, stale save, responsive themes');
|
||||
console.log(screenshots);
|
||||
} finally { await browser.close(); }
|
||||
})().catch(e => { console.error(e); process.exit(1); });
|
||||
Vendored
+22
-1
@@ -12,6 +12,15 @@ localStorage.setItem('cpa-gui.agent-selected-client.v1', params.get('client') ||
|
||||
const ids = ['claude-code','claude-desktop','codex','opencode','openclaw','hermes','deepseek-harness','zcode','kimi-code','grok-build','pi'];
|
||||
let count=0; let backupCount=0; const backups:any[]=[]; let currentModel=params.has('fresh')?null:'gpt-one';
|
||||
let currentOauth=false; const currentMappings:Record<string,any>={};
|
||||
let harnessProvider:Record<string,unknown>={};
|
||||
const harnessConfigurations:Record<string,Record<string,unknown>>={};
|
||||
let harnessRevision=1;
|
||||
const harnessModels=[
|
||||
{id:'gpt-one',defaults:{input:['text','image'],contextWindow:128000}},
|
||||
{id:'gpt-two',defaults:{input:['text']}},
|
||||
{id:'unknown-model',defaults:{}},
|
||||
];
|
||||
const harnessSnapshot=()=>({revision:String(harnessRevision),provider:harnessProvider,baseUrl:'http://127.0.0.1:8317/v1',defaultModel:currentModel,configured:!!currentModel,models:harnessModels.map(m=>({...m,configuration:harnessConfigurations[m.id]??{}}))});
|
||||
let harnessStatus={running:params.has('running'),pid:params.has('running')?100:null as number|null,mode:params.has('running')?params.get('harness-mode')||'web':null as string|null};
|
||||
let appliedCount=0;
|
||||
const calls:any[]=[];(window as any).fixtureCalls=calls;
|
||||
@@ -22,7 +31,10 @@ mockIPC(async (cmd,args:any) => {
|
||||
if(cmd==='plugin:event|listen') return 1;
|
||||
if(cmd==='plugin:event|unlisten'||cmd==='set_app_locale') return null;
|
||||
if(cmd==='get_agent_config_statuses'||cmd==='refresh_agent_config_statuses') return ids.map(id=>({id,name:id,supportedPlatform:true,installed:!params.has('not-installed'),pluginInstalled:!params.has('no-plugin'),launchTargets:params.has('not-installed')?[]:['claude-desktop','zcode'].includes(id)?[{id:'app',label:id,detail:'test desktop'}]:['codex','opencode'].includes(id)&&!params.has('cli-only')?[...(params.has('app-only')?[]:[{id:'cli',label:'CLI',detail:'test CLI'}]),{id:'app',label:'APP',detail:'test desktop'}]:[{id:'cli',label:'CLI',detail:'test CLI'}],version:'1.0',cliVersion:'1.0',appVersion:null,pluginVersion:'1.0',configValid:params.get('state')!=='invalid',connectionState:params.get('state') || (currentModel?'configured':'not-configured'),configured:!!currentModel,configurationSynchronized:!!currentModel,currentModel,oauthConfiguration:id==='codex'&¤tOauth,modificationEnabled:!!currentModel,modificationState:currentModel?'applied':'unconfigured',backupAvailable:false,appliedModel:currentModel,claudeCodeModelMappings:id==='claude-code'?currentMappings[id]??null:null,claudeDesktopModelMappings:id==='claude-desktop'?currentMappings[id]??null:null,warnings:[],error:null}));
|
||||
if(cmd==='get_agent_models') return [{name:'gpt-one'},{name:'gpt-two'}];
|
||||
if(cmd==='get_agent_models') {
|
||||
if(args.client==='deepseek-harness')return harnessModels.map(m=>({name:m.id,inputModalities:m.defaults.input}));
|
||||
return [{name:'gpt-one'},{name:'gpt-two'}];
|
||||
}
|
||||
if(cmd==='list_codex_sessions') {
|
||||
const {offset,limit}=args.request;
|
||||
const sessionIds:string[]=[...(window as any).fixtureSessionIds];
|
||||
@@ -63,6 +75,15 @@ mockIPC(async (cmd,args:any) => {
|
||||
if(cmd==='preview_agent_config_template') return {revision:'template1',files:['C:/test/.codex/config.toml','C:/test/.codex/auth.json','C:/test/.codex/models.json']};
|
||||
if(cmd==='apply_agent_config_template') {currentModel=args.model;return {outcome:'updated'};}
|
||||
if(cmd==='check_pi_provider_update') return {installedVersion:'1.0',latestVersion:params.has('update')?'1.1':'1.0',updateAvailable:params.has('update')};
|
||||
if(cmd==='get_deepseek_harness_model_catalog_editor')return harnessSnapshot();
|
||||
if(cmd==='save_deepseek_harness_model_catalog_editor') {
|
||||
if((window as any).fixtureFailHarnessSave)throw new Error('模拟模型配置保存失败');
|
||||
if((window as any).fixtureStaleHarnessSave)throw new Error('DSH_MODEL_CATALOG_CHANGED');
|
||||
if((window as any).fixtureDeferHarnessSave)await new Promise<void>(resolve=>{(window as any).fixtureFinishHarnessSave=resolve;});
|
||||
harnessProvider=args.request.provider;
|
||||
for(const m of args.request.models)harnessConfigurations[m.id]=m.configuration;
|
||||
harnessRevision++;return harnessSnapshot();
|
||||
}
|
||||
if(cmd==='get_codex_model_catalog_editor') return {models:[],hiddenModels:[],customizations:{}};
|
||||
if(cmd==='check_codex_oauth_login')return null;
|
||||
throw new Error('Unhandled fixture command: '+cmd);
|
||||
|
||||
Reference in new issue
Block a user