fix(opencode): use ACP-reported reasoning effort options (#853)

* fix(opencode): use ACP-reported reasoning effort options

Expose thought_level options from OpenCode ACP to the web UI via RPC/API
instead of hardcoded presets, and validate effort values before setConfigOption.

Fixes #852

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(opencode): sync hub effort after coerced setConfigOption

When resolveThoughtLevelEffort falls back to a different supported value,
roll back session state after a successful ACP update so keepalive and the
web UI do not keep advertising the rejected effort.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
SSU-WEI HUANG
2026-06-10 13:57:03 +08:00
committed by GitHub
co-authored by Cursor
parent 3473a88d67
commit cad58cfa0b
18 changed files with 486 additions and 35 deletions
@@ -255,7 +255,48 @@ describe('opencodeRemoteLauncher inline model switch', () => {
expect(harness.promptCount).toBe(2);
});
it('rejects unsupported reasoning effort values before calling setConfigOption', async () => {
harness.thoughtLevelOption = {
id: 'effort',
currentValue: 'low',
options: [
{ value: 'low', name: 'Low' },
{ value: 'medium', name: 'Medium' }
]
};
const { session, setModelReasoningEffort } = createSessionStub([
{ message: 'first', mode: createModeWithEffort(undefined, 'high') }
]);
await opencodeRemoteLauncher(session as never);
expect(harness.setConfigOptionArgs).toEqual([]);
expect(setModelReasoningEffort).toHaveBeenCalledWith('low');
expect(harness.promptCount).toBe(1);
});
it('syncs hub effort state after coercing an unsupported request to a different supported value', async () => {
harness.thoughtLevelOption = {
id: 'effort',
currentValue: 'high',
options: [
{ value: 'low', name: 'Low' },
{ value: 'medium', name: 'Medium' }
]
};
const { session, setModelReasoningEffort, pushKeepAlive } = createSessionStub([
{ message: 'first', mode: createModeWithEffort(undefined, 'max') }
]);
await opencodeRemoteLauncher(session as never);
expect(harness.setConfigOptionArgs).toEqual([
{ sessionId: 'acp-session-1', configId: 'effort', value: 'low' }
]);
expect(setModelReasoningEffort).toHaveBeenCalledWith('low');
expect(pushKeepAlive).toHaveBeenCalledTimes(1);
expect(harness.promptCount).toBe(1);
});
it('resets to the backend launch-time default model when the queued mode.model is null', async () => {
// Seed the backend with a launch-time default model so the launcher
@@ -414,6 +455,48 @@ describe('opencodeRemoteLauncher inline model switch', () => {
});
});
it('registers a listOpencodeReasoningEffortOptions RPC handler that returns ACP options', async () => {
harness.thoughtLevelOption = {
id: 'effort',
currentValue: 'low',
options: [
{ value: 'low', name: 'Low' },
{ value: 'medium', name: 'Medium' }
]
};
const { session, rpcHandlers } = createSessionStub([
{ message: 'first', mode: createMode() }
]);
await opencodeRemoteLauncher(session as never);
const handler = rpcHandlers.get('listOpencodeReasoningEffortOptions');
expect(handler).toBeDefined();
const result = await handler!(undefined) as Record<string, unknown>;
expect(result).toEqual({
success: true,
options: [
{ value: 'low', name: 'Low' },
{ value: 'medium', name: 'Medium' }
],
currentValue: 'low'
});
});
it('listOpencodeReasoningEffortOptions handler returns unavailable when backend has no thought level option', async () => {
const { session, rpcHandlers } = createSessionStub([
{ message: 'first', mode: createMode() }
]);
await opencodeRemoteLauncher(session as never);
const handler = rpcHandlers.get('listOpencodeReasoningEffortOptions');
expect(handler).toBeDefined();
const result = await handler!(undefined) as Record<string, unknown>;
expect(result).toEqual({
success: false,
error: 'OpenCode reasoning effort options are not available'
});
});
it('serializes setModel after the previous prompt resolves', async () => {
const { session } = createSessionStub([
{ message: 'first', mode: createMode('ollama/a') },