mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-14 20:17:03 +00:00
feat(droid): sync reasoning effort across CLI and dashboard
This commit is contained in:
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({
|
||||
|
||||
Reference in new issue
Block a user