feat(droid): sync reasoning effort across CLI and dashboard

This commit is contained in:
Tam Nhu Tran committed 2026-02-26 12:27:50 +07:00
1 parent 1f3db584eb
commit eedb53b49e
15 files changed
+1228 -18

No files matched your search

@@ -94,6 +94,79 @@ describe('droid-config-manager', () => {
expect(settings.customModels[0].baseUrl).toBe('http://localhost:8318');
});
it('should persist generic provider reasoning_effort from override', async () => {
await upsertCcsModel('glm', {
model: 'glm-4.7',
displayName: 'CCS glm',
baseUrl: 'https://api.z.ai/api/coding/paas/v4',
apiKey: 'glm-key',
provider: 'generic-chat-completion-api',
reasoningOverride: 'high',
});
const settingsPath = path.join(tmpDir, '.factory', 'settings.json');
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
expect(settings.customModels[0].extraArgs?.reasoning_effort).toBe('high');
expect(settings.customModels[0].extraArgs?.reasoning).toBeUndefined();
expect(settings.customModels[0].extraArgs?.thinking).toBeUndefined();
});
it('should persist openai provider reasoning.effort from --effort alias override', async () => {
await upsertCcsModel('codex', {
model: 'gpt-5.2',
displayName: 'CCS codex',
baseUrl: 'https://api.openai.com/v1',
apiKey: 'openai-key',
provider: 'openai',
reasoningOverride: 'xhigh',
});
const settingsPath = path.join(tmpDir, '.factory', 'settings.json');
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
expect(settings.customModels[0].extraArgs?.reasoning?.effort).toBe('xhigh');
expect(settings.customModels[0].extraArgs?.reasoning_effort).toBeUndefined();
});
it('should persist anthropic thinking budget from numeric override', async () => {
await upsertCcsModel('agy', {
model: 'claude-opus-4-5-thinking',
displayName: 'CCS agy',
baseUrl: 'https://api.anthropic.com',
apiKey: 'anthropic-key',
provider: 'anthropic',
reasoningOverride: 40960,
});
const settingsPath = path.join(tmpDir, '.factory', 'settings.json');
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
expect(settings.customModels[0].extraArgs?.thinking?.type).toBe('enabled');
expect(settings.customModels[0].extraArgs?.thinking?.budget_tokens).toBe(40960);
});
it('should clear prior reasoning config when override disables thinking', async () => {
await upsertCcsModel('glm', {
model: 'glm-4.7',
displayName: 'CCS glm',
baseUrl: 'https://api.z.ai/api/coding/paas/v4',
apiKey: 'glm-key',
provider: 'generic-chat-completion-api',
reasoningOverride: 'high',
});
await upsertCcsModel('glm', {
model: 'glm-4.7',
displayName: 'CCS glm',
baseUrl: 'https://api.z.ai/api/coding/paas/v4',
apiKey: 'glm-key',
provider: 'generic-chat-completion-api',
reasoningOverride: 'off',
});
const settingsPath = path.join(tmpDir, '.factory', 'settings.json');
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
expect(settings.customModels[0].extraArgs).toBeUndefined();
});
it('should preserve user entries', async () => {
// Create existing settings with user's own custom model
const factoryDir = path.join(tmpDir, '.factory');
@@ -0,0 +1,37 @@
import { describe, expect, it } from 'bun:test';
import {
DroidReasoningFlagError,
resolveDroidReasoningRuntime,
} from '../../../src/targets/droid-reasoning-runtime';
describe('droid-reasoning-runtime', () => {
it('extracts --thinking and strips CCS reasoning flags from args', () => {
const runtime = resolveDroidReasoningRuntime(['--thinking', 'high', '--verbose'], undefined);
expect(runtime.reasoningOverride).toBe('high');
expect(runtime.sourceFlag).toBe('--thinking');
expect(runtime.argsWithoutReasoningFlags).toEqual(['--verbose']);
});
it('extracts --effort alias and strips inline value', () => {
const runtime = resolveDroidReasoningRuntime(['--effort=xhigh', '--help'], undefined);
expect(runtime.reasoningOverride).toBe('xhigh');
expect(runtime.sourceFlag).toBe('--effort');
expect(runtime.argsWithoutReasoningFlags).toEqual(['--help']);
});
it('uses CCS_THINKING env fallback when no flag is provided', () => {
const runtime = resolveDroidReasoningRuntime(['--verbose'], 'medium');
expect(runtime.reasoningOverride).toBe('medium');
expect(runtime.sourceFlag).toBeUndefined();
expect(runtime.argsWithoutReasoningFlags).toEqual(['--verbose']);
});
it('throws on missing reasoning flag value', () => {
expect(() => resolveDroidReasoningRuntime(['--thinking'], undefined)).toThrow(
DroidReasoningFlagError
);
});
});
@@ -191,6 +191,31 @@ describe('DroidAdapter', () => {
}
});
it('prepareCredentials should persist reasoning override into Droid extraArgs', async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-droid-adapter-reasoning-test-'));
const originalCcsHome = process.env.CCS_HOME;
process.env.CCS_HOME = tmpDir;
try {
await adapter.prepareCredentials({
profile: 'codex',
baseUrl: 'https://api.openai.com/v1',
apiKey: 'dummy-key',
model: 'gpt-5.2',
provider: 'openai',
reasoningOverride: 'high',
});
const settingsPath = path.join(tmpDir, '.factory', 'settings.json');
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
expect(settings.customModels?.[0]?.extraArgs?.reasoning?.effort).toBe('high');
} finally {
if (originalCcsHome !== undefined) process.env.CCS_HOME = originalCcsHome;
else delete process.env.CCS_HOME;
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
it('buildArgs should use selector returned from Droid settings entry', async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-droid-selector-test-'));
const originalCcsHome = process.env.CCS_HOME;
@@ -89,6 +89,26 @@ describe('droid-dashboard-service', () => {
expect(summary.customModels[0].apiKeyPreview).toBe('***1234');
});
it('supports legacy snake_case model fields in summaries', () => {
const summary = summarizeDroidCustomModels([
{
model_display_name: 'Kimi K2 Thinking Nvidia',
model: 'moonshotai/kimi-k2-thinking',
base_url: 'https://integrate.api.nvidia.com/v1',
api_key: 'legacy-token-1234',
provider: 'generic-chat-completion-api',
max_tokens: 220000,
},
]);
expect(summary.customModelCount).toBe(1);
expect(summary.invalidModelEntryCount).toBe(0);
expect(summary.providerBreakdown['generic-chat-completion-api']).toBe(1);
expect(summary.customModels[0].displayName).toBe('Kimi K2 Thinking Nvidia');
expect(summary.customModels[0].maxOutputTokens).toBe(220000);
expect(summary.customModels[0].apiKeyPreview).toBe('***1234');
});
it('returns raw settings payload for missing settings file', async () => {
const raw = await getDroidRawSettings();
@@ -124,6 +144,58 @@ describe('droid-dashboard-service', () => {
).toBe(true);
});
it('falls back to legacy config custom_models when settings customModels is absent', async () => {
const settingsDir = path.join(testRoot, '.factory');
fs.mkdirSync(settingsDir, { recursive: true });
fs.writeFileSync(path.join(settingsDir, 'settings.json'), JSON.stringify({ model: 'custom:legacy' }));
fs.writeFileSync(
path.join(settingsDir, 'config.json'),
JSON.stringify({
custom_models: [
{
model_display_name: 'Legacy OpenAI',
model: 'gpt-5.2',
base_url: 'https://api.openai.com/v1',
api_key: 'legacy-openai-1234',
provider: 'openai',
},
],
})
);
const diagnostics = await getDroidDashboardDiagnostics();
expect(diagnostics.byok.customModelCount).toBe(1);
expect(diagnostics.byok.customModels[0].displayName).toBe('Legacy OpenAI');
expect(diagnostics.byok.customModels[0].provider).toBe('openai');
});
it('warns when settings.json uses legacy custom_models key', async () => {
const settingsDir = path.join(testRoot, '.factory');
fs.mkdirSync(settingsDir, { recursive: true });
fs.writeFileSync(
path.join(settingsDir, 'settings.json'),
JSON.stringify({
custom_models: [
{
model_display_name: 'Legacy Generic',
model: 'glm-4.7',
base_url: 'https://api.z.ai/api/coding/paas/v4',
api_key: 'legacy-zai-1234',
provider: 'generic-chat-completion-api',
},
],
})
);
const diagnostics = await getDroidDashboardDiagnostics();
expect(
diagnostics.warnings.some((warning) => warning.includes('legacy "custom_models" key'))
).toBe(true);
expect(diagnostics.byok.customModelCount).toBe(1);
});
it('saves valid raw settings content', async () => {
const result = await saveDroidRawSettings({
rawText: JSON.stringify({