mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
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:
@@ -13,6 +13,7 @@ import type {
|
||||
ListDirectoryResponse,
|
||||
OpencodeModelsResponse,
|
||||
OpencodeModelSummary,
|
||||
OpencodeReasoningEffortResponse,
|
||||
PathExistsResponse,
|
||||
SlashCommandsResponse,
|
||||
UploadFileResponse
|
||||
@@ -37,6 +38,7 @@ export type RpcCursorModel = CursorModelSummary
|
||||
export type RpcListCursorModelsResponse = CursorModelsResponse
|
||||
export type RpcOpencodeModel = OpencodeModelSummary
|
||||
export type RpcListOpencodeModelsResponse = OpencodeModelsResponse
|
||||
export type RpcListOpencodeReasoningEffortOptionsResponse = OpencodeReasoningEffortResponse
|
||||
|
||||
export class RpcGateway {
|
||||
constructor(
|
||||
@@ -258,6 +260,10 @@ export class RpcGateway {
|
||||
return await this.machineRpc(machineId, RPC_METHODS.ListOpencodeModelsForCwd, { cwd }) as RpcListOpencodeModelsResponse
|
||||
}
|
||||
|
||||
async listOpencodeReasoningEffortOptionsForSession(sessionId: string): Promise<RpcListOpencodeReasoningEffortOptionsResponse> {
|
||||
return await this.sessionRpc(sessionId, RPC_METHODS.ListOpencodeReasoningEffortOptions, {}) as RpcListOpencodeReasoningEffortOptionsResponse
|
||||
}
|
||||
|
||||
private async sessionRpc(
|
||||
sessionId: string,
|
||||
method: string,
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
type RpcListCodexModelsResponse,
|
||||
type RpcListCursorModelsResponse,
|
||||
type RpcListOpencodeModelsResponse,
|
||||
type RpcListOpencodeReasoningEffortOptionsResponse,
|
||||
type RpcCursorModel,
|
||||
type RpcOpencodeModel,
|
||||
type RpcPathExistsResponse,
|
||||
@@ -49,6 +50,7 @@ export type {
|
||||
RpcListCodexModelsResponse,
|
||||
RpcListCursorModelsResponse,
|
||||
RpcListOpencodeModelsResponse,
|
||||
RpcListOpencodeReasoningEffortOptionsResponse,
|
||||
RpcCursorModel,
|
||||
RpcOpencodeModel,
|
||||
RpcPathExistsResponse,
|
||||
@@ -1088,4 +1090,8 @@ export class SyncEngine {
|
||||
async listOpencodeModelsForCwd(machineId: string, cwd: string): Promise<RpcListOpencodeModelsResponse> {
|
||||
return await this.rpcGateway.listOpencodeModelsForCwd(machineId, cwd)
|
||||
}
|
||||
|
||||
async listOpencodeReasoningEffortOptionsForSession(sessionId: string): Promise<RpcListOpencodeReasoningEffortOptionsResponse> {
|
||||
return await this.rpcGateway.listOpencodeReasoningEffortOptionsForSession(sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,14 @@ function createApp(session: Session, opts?: {
|
||||
],
|
||||
currentModelId: 'ollama/exaone:4.5-33b-q8'
|
||||
})
|
||||
const listOpencodeReasoningEffortOptionsForSession = async () => ({
|
||||
success: true,
|
||||
options: [
|
||||
{ value: 'low', name: 'Low' },
|
||||
{ value: 'medium', name: 'Medium' }
|
||||
],
|
||||
currentValue: 'low'
|
||||
})
|
||||
const listCursorModelsForSession = async () => ({
|
||||
success: true,
|
||||
availableModels: [
|
||||
@@ -103,6 +111,7 @@ function createApp(session: Session, opts?: {
|
||||
listCodexModelsForSession,
|
||||
listCursorModelsForSession,
|
||||
listOpencodeModelsForSession,
|
||||
listOpencodeReasoningEffortOptionsForSession,
|
||||
resumeSession,
|
||||
reopenSession,
|
||||
getSessionExport: opts?.getSessionExport ?? (() => ({
|
||||
@@ -543,6 +552,33 @@ describe('sessions routes', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('returns OpenCode reasoning effort options for active OpenCode sessions', async () => {
|
||||
const session = createSession({
|
||||
metadata: { path: '/tmp/project', host: 'localhost', flavor: 'opencode' }
|
||||
})
|
||||
const { app } = createApp(session)
|
||||
|
||||
const response = await app.request('/api/sessions/session-1/opencode-reasoning-effort-options')
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(await response.json()).toEqual({
|
||||
success: true,
|
||||
options: [
|
||||
{ value: 'low', name: 'Low' },
|
||||
{ value: 'medium', name: 'Medium' }
|
||||
],
|
||||
currentValue: 'low'
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects opencode-reasoning-effort-options for non-OpenCode sessions', async () => {
|
||||
const { app } = createApp(createSession())
|
||||
|
||||
const response = await app.request('/api/sessions/session-1/opencode-reasoning-effort-options')
|
||||
|
||||
expect(response.status).toBe(400)
|
||||
})
|
||||
|
||||
it('returns OpenCode models for active OpenCode sessions', async () => {
|
||||
const session = createSession({
|
||||
metadata: { path: '/tmp/project', host: 'localhost', flavor: 'opencode' }
|
||||
|
||||
@@ -688,6 +688,36 @@ export function createSessionsRoutes(getSyncEngine: () => SyncEngine | null): Ho
|
||||
}
|
||||
})
|
||||
|
||||
app.get('/sessions/:id/opencode-reasoning-effort-options', async (c) => {
|
||||
const engine = requireSyncEngine(c, getSyncEngine)
|
||||
if (engine instanceof Response) {
|
||||
return engine
|
||||
}
|
||||
|
||||
const sessionResult = requireSessionFromParam(c, engine, { requireActive: true })
|
||||
if (sessionResult instanceof Response) {
|
||||
return sessionResult
|
||||
}
|
||||
|
||||
const flavor = sessionResult.session.metadata?.flavor ?? 'claude'
|
||||
if (flavor !== 'opencode') {
|
||||
return c.json({
|
||||
success: false,
|
||||
error: 'OpenCode reasoning effort options are only available for OpenCode sessions'
|
||||
}, 400)
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await engine.listOpencodeReasoningEffortOptionsForSession(sessionResult.sessionId)
|
||||
return c.json(result)
|
||||
} catch (error) {
|
||||
return c.json({
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Failed to list OpenCode reasoning effort options'
|
||||
}, 500)
|
||||
}
|
||||
})
|
||||
|
||||
app.get('/sessions/:id/cursor-models', async (c) => {
|
||||
const engine = requireSyncEngine(c, getSyncEngine)
|
||||
if (engine instanceof Response) {
|
||||
|
||||
Reference in New Issue
Block a user