From 580bcb65b98d528493766dd0fbaca8ba1f124f77 Mon Sep 17 00:00:00 2001 From: Test Date: Sun, 13 Sep 2026 15:58:36 -0400 Subject: [PATCH] fix(session): pass tools through for the harmoniqs provider The app-harmoniqs-ai gateway now accepts tools and returns OpenAI-shaped tool_calls (Bedrock Converse tool support, live in prod). The no-tools strip made every harmoniqs turn chat-only: the model saw zero tools and ended the turn, so agents never continued. Remove the strip and invert the test to pin passthrough. --- packages/opencode/src/session/llm/request.ts | 20 ------------- ....ts => harmoniqs-tool-passthrough.test.ts} | 30 ++++++------------- 2 files changed, 9 insertions(+), 41 deletions(-) rename packages/opencode/test/provider/{harmoniqs-no-tools.test.ts => harmoniqs-tool-passthrough.test.ts} (76%) diff --git a/packages/opencode/src/session/llm/request.ts b/packages/opencode/src/session/llm/request.ts index 5b2ef4b0bc..24c8bbec2a 100644 --- a/packages/opencode/src/session/llm/request.ts +++ b/packages/opencode/src/session/llm/request.ts @@ -154,16 +154,6 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre ) const tools = resolveTools(input) - // Harmoniqs AI custom provider: the app-harmoniqs-ai gateway hard-rejects any - // request carrying a `tools` field (400 unsupported_feature — see - // app-harmoniqs-ai/src/worker/routes/chat-completions.ts). Agents default to - // tool calling, so without this every turn against this provider would 400 - // instead of just failing to use tools. Strip resolved tools here rather than - // relying on `capabilities.toolcall` alone, which is descriptive metadata and - // is not currently consulted by resolveTools. - if (isNoToolsProvider(input.model.providerID)) { - for (const key of Object.keys(tools)) delete tools[key] - } // Codex parity: OpenAI Responses-family providers hardcode `strict: false` // on every function tool so MCP-sourced and dynamic schemas that don't // satisfy OpenAI's structured-outputs constraints still register. @@ -231,16 +221,6 @@ function resolveTools(input: Pick input.user.tools?.[k] !== false && !disabled.has(k)) } -/** Provider IDs whose backend rejects `tools` outright (400 unsupported_feature - * style responses) rather than merely ignoring them. Chat-only/no-tools - * providers belong here so resolveTools' output is stripped before it ever - * reaches the wire, instead of failing on the first request. */ -const NO_TOOLS_PROVIDERS = new Set(["harmoniqs"]) - -export function isNoToolsProvider(providerID: string): boolean { - return NO_TOOLS_PROVIDERS.has(providerID) -} - export function hasToolCalls(messages: ModelMessage[]): boolean { for (const msg of messages) { if (!Array.isArray(msg.content)) continue diff --git a/packages/opencode/test/provider/harmoniqs-no-tools.test.ts b/packages/opencode/test/provider/harmoniqs-tool-passthrough.test.ts similarity index 76% rename from packages/opencode/test/provider/harmoniqs-no-tools.test.ts rename to packages/opencode/test/provider/harmoniqs-tool-passthrough.test.ts index 45293f87b6..c074e9c416 100644 --- a/packages/opencode/test/provider/harmoniqs-no-tools.test.ts +++ b/packages/opencode/test/provider/harmoniqs-tool-passthrough.test.ts @@ -1,27 +1,15 @@ import { describe, expect, test } from "bun:test" import { Effect } from "effect" import { jsonSchema } from "ai" -import { LLMRequestPrep, isNoToolsProvider } from "@/session/llm/request" +import { LLMRequestPrep } from "@/session/llm/request" // Harmoniqs AI is an OpenAI-compatible custom provider whose backend -// (app-harmoniqs-ai) hard-rejects any request carrying a `tools` field with a -// 400 unsupported_feature error. opencode agents default to tool calling, so -// LLMRequestPrep.prepare must strip resolved tools for this provider — see -// packages/opencode/src/session/llm/request.ts. +// (app-harmoniqs-ai) accepts `tools` and returns OpenAI-shaped `tool_calls` +// (Bedrock Converse tool-calling support). opencode agents default to tool +// calling, so LLMRequestPrep.prepare must pass resolved tools through for +// this provider — see packages/opencode/src/session/llm/request.ts. -describe("isNoToolsProvider", () => { - test("flags the harmoniqs provider", () => { - expect(isNoToolsProvider("harmoniqs")).toBe(true) - }) - - test("leaves other providers untouched", () => { - expect(isNoToolsProvider("anthropic")).toBe(false) - expect(isNoToolsProvider("openai")).toBe(false) - expect(isNoToolsProvider("")).toBe(false) - }) -}) - -describe("LLMRequestPrep.prepare - harmoniqs no-tools gate", () => { +describe("LLMRequestPrep.prepare - harmoniqs tool passthrough", () => { const sessionID = "test-session-harmoniqs" const harmoniqsModel = { @@ -37,7 +25,7 @@ describe("LLMRequestPrep.prepare - harmoniqs no-tools gate", () => { temperature: true, reasoning: false, attachment: false, - toolcall: false, + toolcall: true, input: { text: true, audio: false, image: false, video: false, pdf: false }, output: { text: true, audio: false, image: false, video: false, pdf: false }, interleaved: false, @@ -112,9 +100,9 @@ describe("LLMRequestPrep.prepare - harmoniqs no-tools gate", () => { } } - test("strips resolved tools for the harmoniqs provider", async () => { + test("passes resolved tools through for the harmoniqs provider", async () => { const result = await Effect.runPromise(LLMRequestPrep.prepare(baseInput(harmoniqsModel))) - expect(Object.keys(result.tools)).toHaveLength(0) + expect(Object.keys(result.tools)).toContain("lookup") }) test("leaves tools intact for other providers", async () => {