Merge pull request #23 from southseact-3d/codex/analyze-tool-call-request-counting-issue

Cap Chutes provider tool orchestration to a single SDK step
This commit is contained in:
Liam Hetherington
2026-02-11 15:04:58 +00:00
committed by GitHub
2 changed files with 129 additions and 1 deletions

View File

@@ -2,6 +2,7 @@ import { Installation } from "@/installation"
import { Provider } from "@/provider/provider"
import { Log } from "@/util/log"
import {
stepCountIs,
streamText,
wrapLanguageModel,
type ModelMessage,
@@ -116,6 +117,8 @@ export namespace LLM {
providerOptions: ProviderTransform.providerOptions(input.model, params.options),
activeTools: Object.keys(tools).filter((x) => x !== "invalid"),
tools,
// Chutes accounts each provider round-trip as a separate request, so keep SDK orchestration to one step.
stopWhen: input.model.providerID === "chutes" ? stepCountIs(1) : undefined,
maxOutputTokens,
abortSignal: input.abort,
headers: {

View File

@@ -1,6 +1,6 @@
import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test"
import path from "path"
import type { ModelMessage } from "ai"
import { type ModelMessage, jsonSchema, tool } from "ai"
import { LLM } from "../../src/session/llm"
import { Global } from "../../src/global"
import { Instance } from "../../src/project/instance"
@@ -579,6 +579,131 @@ describe("session.llm.stream", () => {
})
})
test("limits chutes tool runs to a single SDK step", async () => {
const server = state.server
if (!server) throw new Error("Server not initialized")
const providerID = "chutes"
const modelID = "NousResearch/Hermes-4.3-36B"
const fixture = await loadFixture(providerID, modelID)
const model = fixture.model
const request = waitRequest(
"/chat/completions",
createEventResponse(
[
{
id: "chatcmpl-1",
object: "chat.completion.chunk",
choices: [{ delta: { role: "assistant" } }],
},
{
id: "chatcmpl-1",
object: "chat.completion.chunk",
choices: [
{
delta: {
tool_calls: [
{
index: 0,
id: "call_1",
type: "function",
function: {
name: "echo",
arguments: '{"value":"hello"}',
},
},
],
},
},
],
},
{
id: "chatcmpl-1",
object: "chat.completion.chunk",
choices: [{ delta: {}, finish_reason: "tool_calls" }],
},
],
true,
),
)
await using tmp = await tmpdir({
init: async (dir) => {
await Bun.write(
path.join(dir, "opencode.json"),
JSON.stringify({
$schema: "https://opencode.ai/config.json",
enabled_providers: [providerID],
provider: {
[providerID]: {
options: {
apiKey: "test-chutes-key",
baseURL: `${server.url.origin}/v1`,
},
},
},
}),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const resolved = await Provider.getModel(providerID, model.id)
const sessionID = "session-test-5"
const agent = {
name: "test",
mode: "primary",
options: {},
permission: [{ permission: "*", pattern: "*", action: "allow" }],
temperature: 0.4,
} satisfies Agent.Info
const user = {
id: "user-5",
sessionID,
role: "user",
time: { created: Date.now() },
agent: agent.name,
model: { providerID, modelID: resolved.id },
} satisfies MessageV2.User
const stream = await LLM.stream({
user,
sessionID,
model: resolved,
agent,
system: ["You are a helpful assistant."],
abort: new AbortController().signal,
messages: [{ role: "user", content: "Use echo" }],
tools: {
echo: tool({
inputSchema: jsonSchema({
type: "object",
properties: {
value: { type: "string" },
},
required: ["value"],
additionalProperties: false,
}),
execute: async () => "ok",
}),
},
})
for await (const _ of stream.fullStream) {
}
const capture = await request
expect(capture.url.pathname.endsWith("/chat/completions")).toBe(true)
expect(state.queue.length).toBe(0)
},
})
})
test("sends Google API payload for Gemini models", async () => {
const server = state.server
if (!server) {