diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index eb116f6b960f..c3c32ab63299 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1223,24 +1223,26 @@ const layer = Layer.effect( const bypassAgentCheck = lastUserMsg?.parts.some((p) => p.type === "agent") ?? false const promptOps = yield* ops() - const tools = yield* SessionTools.resolve({ - agent, - session, - model, - processor: handle, - bypassAgentCheck, - messages: msgs, - promptOps, - }).pipe( - Effect.provideService(Plugin.Service, plugin), - Effect.provideService(Permission.Service, permission), - Effect.provideService(ToolRegistry.Service, registry), - Effect.provideService(MCP.Service, mcp), - Effect.provideService(Truncate.Service, truncate), - Effect.provideService(RuntimeFlags.Service, flags), - ) + const tools: Record = model.capabilities.toolcall + ? yield* SessionTools.resolve({ + agent, + session, + model, + processor: handle, + bypassAgentCheck, + messages: msgs, + promptOps, + }).pipe( + Effect.provideService(Plugin.Service, plugin), + Effect.provideService(Permission.Service, permission), + Effect.provideService(ToolRegistry.Service, registry), + Effect.provideService(MCP.Service, mcp), + Effect.provideService(Truncate.Service, truncate), + Effect.provideService(RuntimeFlags.Service, flags), + ) + : {} - if (lastUser.format?.type === "json_schema") { + if (lastUser.format?.type === "json_schema" && model.capabilities.toolcall) { tools["StructuredOutput"] = createStructuredOutputTool({ schema: lastUser.format.schema, onSuccess(output) { @@ -1268,6 +1270,15 @@ const layer = Layer.effect( ...(skills ? [skills] : []), ] const format = lastUser.format ?? { type: "text" as const } + if (format.type === "json_schema" && !model.capabilities.toolcall) { + handle.message.error = new SessionV1.StructuredOutputError({ + message: "Structured output format requires tool calls, but tool_call is disabled for this model", + retries: 0, + }).toObject() + handle.message.time.completed = Date.now() + yield* sessions.updateMessage(handle.message) + return "break" as const + } if (format.type === "json_schema") system.push(STRUCTURED_OUTPUT_SYSTEM_PROMPT) const result = yield* handle.process({ user: lastUser, @@ -1282,7 +1293,7 @@ const layer = Layer.effect( ], tools, model, - toolChoice: format.type === "json_schema" ? "required" : undefined, + toolChoice: !model.capabilities.toolcall ? "none" : format.type === "json_schema" ? "required" : undefined, }) if (structured !== undefined) { diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index 491ad06aaf47..cf04deb6a16a 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -714,6 +714,25 @@ noLLMServer.instance.skip( { config: cfg }, ) +noLLMServer.instance( + "prompt loop does not crash when toolcall is false", + () => + Effect.gen(function* () { + const prompt = yield* SessionPrompt.Service + const sessions = yield* Session.Service + const session = yield* sessions.create({ title: "No toolcall" }) + yield* prompt.prompt({ + sessionID: session.id, + agent: "build", + noReply: true, + parts: [{ type: "text", text: "hello" }], + }) + const result = yield* prompt.loop({ sessionID: session.id }) + expect(result.info.role).toBe("assistant") + }), + { config: { ...cfg, provider: { test: { ...cfg.provider.test, models: { "test-model": { ...cfg.provider.test.models["test-model"], tool_call: false } } } } } }, +) + it.instance("static loop returns assistant text through local provider", () => Effect.gen(function* () { const { llm } = yield* useServerConfig(providerCfg)