Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 0 additions & 20 deletions packages/opencode/src/session/llm/request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -231,16 +221,6 @@ function resolveTools(input: Pick<PrepareInput, "tools" | "agent" | "permission"
return Record.filter(input.tools, (_, k) => 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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,
Expand Down Expand Up @@ -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 () => {
Expand Down
Loading