Merge pull request #24 from southseact-3d/codex/fix-multiple-request-counting-issue

Limit tool-run loop for Chutes-like OpenAI routes and add regression test
This commit is contained in:
Liam Hetherington
2026-02-12 09:41:22 +00:00
committed by GitHub
2 changed files with 155 additions and 2 deletions

View File

@@ -73,6 +73,13 @@ export namespace LLM {
retryState: RetryState, retryState: RetryState,
): Promise<StreamOutput> { ): Promise<StreamOutput> {
const { getLanguage, cfg, provider, auth, isCodex, system, params, options, headers, maxOutputTokens, tools, rateLimitConfig } = config const { getLanguage, cfg, provider, auth, isCodex, system, params, options, headers, maxOutputTokens, tools, rateLimitConfig } = config
const activeTools = Object.keys(tools).filter((x) => x !== "invalid")
const singleStepTools = shouldLimitToolLoop({
model: input.model,
provider,
options,
activeTools,
})
while (retryState.attempt <= (rateLimitConfig.maxRetries || 0)) { while (retryState.attempt <= (rateLimitConfig.maxRetries || 0)) {
try { try {
@@ -115,10 +122,10 @@ export namespace LLM {
topP: params.topP, topP: params.topP,
topK: params.topK, topK: params.topK,
providerOptions: ProviderTransform.providerOptions(input.model, params.options), providerOptions: ProviderTransform.providerOptions(input.model, params.options),
activeTools: Object.keys(tools).filter((x) => x !== "invalid"), activeTools,
tools, tools,
// Chutes accounts each provider round-trip as a separate request, so keep SDK orchestration to one step. // 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, stopWhen: singleStepTools ? stepCountIs(1) : undefined,
maxOutputTokens, maxOutputTokens,
abortSignal: input.abort, abortSignal: input.abort,
headers: { headers: {
@@ -467,4 +474,26 @@ export namespace LLM {
} }
return false return false
} }
function shouldLimitToolLoop(input: {
model: Provider.Model
provider: Provider.Info
options: Record<string, any>
activeTools: string[]
}) {
if (input.activeTools.length === 0) return false
if (input.model.providerID === "chutes") return true
const url = [
input.model.api.url,
input.provider.options?.baseURL,
input.provider.options?.baseUrl,
input.options?.baseURL,
input.options?.baseUrl,
]
.filter((x) => typeof x === "string")
.map((x) => x.toLowerCase())
return url.some((x) => x.includes("chutes.ai") || x.includes("/chutes/"))
}
} }

View File

@@ -704,6 +704,130 @@ describe("session.llm.stream", () => {
}) })
}) })
test("limits tool runs to a single SDK step when OpenAI provider targets Chutes baseURL", async () => {
const server = state.server
if (!server) throw new Error("Server not initialized")
const providerID = "openai"
const modelID = "gpt-4o-mini"
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-openai-key",
baseURL: `${server.url.origin}/chutes/v1`,
},
},
},
}),
)
},
})
await Instance.provide({
directory: tmp.path,
fn: async () => {
const resolved = await Provider.getModel(providerID, model.id)
const sessionID = "session-test-6"
const agent = {
name: "test",
mode: "primary",
options: {},
permission: [{ permission: "*", pattern: "*", action: "allow" }],
temperature: 0.4,
} satisfies Agent.Info
const user = {
id: "user-6",
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 () => { test("sends Google API payload for Gemini models", async () => {
const server = state.server const server = state.server
if (!server) { if (!server) {