From 3e96a41e399d51d3f561b4b0b91be287bcb99f99 Mon Sep 17 00:00:00 2001 From: southseact-3d Date: Sun, 15 Feb 2026 16:38:07 +0000 Subject: [PATCH] fix: prevent multiple HTTP requests for Chutes AI provider Chutes AI counts each HTTP API request separately. The existing fix using stepCountIs(1) only limited the Vercel AI SDK's internal loop, but the outer while(true) loops in processor.ts and prompt.ts continued to make additional HTTP requests after tool execution. This fix: - Returns singleStepTools flag from LLM.stream() to signal single-step mode - Breaks out of processor.ts inner loop after one iteration for Chutes - Breaks out of prompt.ts outer loop after one iteration for Chutes This ensures only one HTTP request is made per user message for providers like Chutes that bill per request. --- .../opencode/src/session/compaction.ts | 2 +- opencode/packages/opencode/src/session/llm.ts | 15 +++++++++++--- .../opencode/src/session/processor.ts | 20 +++++++++++-------- .../packages/opencode/src/session/prompt.ts | 5 +++-- 4 files changed, 28 insertions(+), 14 deletions(-) diff --git a/opencode/packages/opencode/src/session/compaction.ts b/opencode/packages/opencode/src/session/compaction.ts index 73a70af..81998b0 100644 --- a/opencode/packages/opencode/src/session/compaction.ts +++ b/opencode/packages/opencode/src/session/compaction.ts @@ -164,7 +164,7 @@ export namespace SessionCompaction { model, }) - if (result === "continue" && input.auto) { + if (result.status === "continue" && input.auto) { const continueMsg = await Session.updateMessage({ id: Identifier.ascending("message"), role: "user", diff --git a/opencode/packages/opencode/src/session/llm.ts b/opencode/packages/opencode/src/session/llm.ts index afa4984..befa5b9 100644 --- a/opencode/packages/opencode/src/session/llm.ts +++ b/opencode/packages/opencode/src/session/llm.ts @@ -45,7 +45,10 @@ export namespace LLM { allProviders?: Record } - export type StreamOutput = StreamTextResult + export type StreamOutput = { + result: StreamTextResult + singleStepTools: boolean + } export type RetryState = { attempt: number @@ -178,7 +181,7 @@ export namespace LLM { ProviderSwitch.recordSuccess(input.model.providerID, input.model.id) - return result + return { result, singleStepTools } } catch (error) { retryState.lastError = error as Error ProviderSwitch.recordFailure(input.model.providerID, input.model.id) @@ -454,10 +457,16 @@ export namespace LLM { ) } - export async function stream(input: StreamInput) { + export async function stream(input: StreamInput): Promise { return streamWithProvider(input) } + export function shouldLimitToolLoopForModel(model: Provider.Model): boolean { + if (model.providerID === "chutes") return true + const url = model.api.url?.toLowerCase() || "" + return url.includes("chutes.ai") || url.includes("/chutes/") + } + async function resolveTools(input: Pick) { const disabled = PermissionNext.disabled(Object.keys(input.tools), input.agent.permission) for (const tool of Object.keys(input.tools)) { diff --git a/opencode/packages/opencode/src/session/processor.ts b/opencode/packages/opencode/src/session/processor.ts index dc2528b..a5bf911 100644 --- a/opencode/packages/opencode/src/session/processor.ts +++ b/opencode/packages/opencode/src/session/processor.ts @@ -57,7 +57,7 @@ export namespace SessionProcessor { const log = Log.create({ service: "session.processor" }) export type Info = Awaited> - export type Result = Awaited> + export type Result = Awaited> & { singleStepTools: boolean } export function create(input: { assistantMessage: MessageV2.Assistant @@ -70,6 +70,7 @@ export namespace SessionProcessor { let blocked = false let attempt = 0 let needsCompaction = false + let singleStepTools = false const result = { get message() { @@ -78,7 +79,7 @@ export namespace SessionProcessor { partFromToolCall(toolCallID: string) { return toolcalls[toolCallID] }, - async process(streamInput: LLM.StreamInput & { allProviders?: Record }) { + async process(streamInput: LLM.StreamInput & { allProviders?: Record }): Promise<{ status: "compact" | "stop" | "continue"; singleStepTools: boolean }> { log.info("process") needsCompaction = false const shouldBreak = (await Config.get()).experimental?.continue_loop_on_deny !== true @@ -86,12 +87,13 @@ export namespace SessionProcessor { try { let currentText: MessageV2.TextPart | undefined let reasoningMap: Record = {} - const stream = await LLM.stream({ + const streamOutput = await LLM.stream({ ...streamInput, allProviders: streamInput.allProviders || await Provider.list(), }) + singleStepTools = streamOutput.singleStepTools - for await (const value of stream.fullStream) { + for await (const value of streamOutput.result.fullStream) { input.abort.throwIfAborted() switch (value.type) { case "start": @@ -438,11 +440,13 @@ export namespace SessionProcessor { } input.assistantMessage.time.completed = Date.now() await Session.updateMessage(input.assistantMessage) - if (needsCompaction) return "compact" - if (blocked) return "stop" - if (input.assistantMessage.error) return "stop" - return "continue" + if (needsCompaction) return { status: "compact" as const, singleStepTools } + if (blocked) return { status: "stop" as const, singleStepTools } + if (input.assistantMessage.error) return { status: "stop" as const, singleStepTools } + if (singleStepTools) return { status: "continue" as const, singleStepTools } + break } + return { status: "continue" as const, singleStepTools } }, } return result diff --git a/opencode/packages/opencode/src/session/prompt.ts b/opencode/packages/opencode/src/session/prompt.ts index e747579..4daf1e7 100644 --- a/opencode/packages/opencode/src/session/prompt.ts +++ b/opencode/packages/opencode/src/session/prompt.ts @@ -631,8 +631,8 @@ export namespace SessionPrompt { model, allProviders, }) - if (result === "stop") break - if (result === "compact") { + if (result.status === "stop") break + if (result.status === "compact") { await SessionCompaction.create({ sessionID, agent: lastUser.agent, @@ -640,6 +640,7 @@ export namespace SessionPrompt { auto: true, }) } + if (result.singleStepTools) break continue } SessionCompaction.prune({ sessionID })