mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-30 03:22:24 +00:00
219 lines
8.5 KiB
TypeScript
219 lines
8.5 KiB
TypeScript
import { expect, test } from "bun:test"
|
|
import type {
|
|
LanguageModelV3CallOptions,
|
|
LanguageModelV3FunctionTool,
|
|
LanguageModelV3ProviderTool,
|
|
} from "@ai-sdk/provider"
|
|
import { prepareResponsesTools } from "@opencode-ai/core/github-copilot/responses/openai-responses-prepare-tools"
|
|
|
|
function prepare(strict: boolean | undefined, strictJsonSchema: boolean) {
|
|
const tool: LanguageModelV3FunctionTool = {
|
|
type: "function",
|
|
name: "lookup",
|
|
inputSchema: { type: "object", properties: {} },
|
|
strict,
|
|
}
|
|
return prepareResponsesTools({ tools: [tool], strictJsonSchema }).tools?.[0]
|
|
}
|
|
|
|
test("function tools prefer explicit strictness over the global fallback", () => {
|
|
expect(prepare(true, false)).toMatchObject({ type: "function", strict: true })
|
|
expect(prepare(false, true)).toMatchObject({ type: "function", strict: false })
|
|
expect(prepare(undefined, true)).toMatchObject({ type: "function", strict: true })
|
|
expect(prepare(undefined, false)).toMatchObject({ type: "function", strict: false })
|
|
})
|
|
|
|
const webTools: LanguageModelV3ProviderTool[] = [
|
|
{ type: "provider", id: "openai.web_search", name: "current_web", args: {} },
|
|
{ type: "provider", id: "openai.web_search_preview", name: "preview_web", args: {} },
|
|
]
|
|
|
|
test.each([
|
|
{ order: webTools, toolChoice: undefined },
|
|
{ order: webTools.toReversed(), toolChoice: undefined },
|
|
{ order: webTools, toolChoice: { type: "auto" as const } },
|
|
{ order: webTools.toReversed(), toolChoice: { type: "auto" as const } },
|
|
{ order: webTools, toolChoice: { type: "required" as const } },
|
|
{ order: webTools.toReversed(), toolChoice: { type: "required" as const } },
|
|
])("rejects differently named web variants before automatic or required selection", ({ order, toolChoice }) => {
|
|
expect(() => prepareResponsesTools({ tools: order, toolChoice, strictJsonSchema: false })).toThrow(
|
|
"ambiguous web_search response for hosted tools",
|
|
)
|
|
})
|
|
|
|
test.each([
|
|
{ order: webTools, name: "current_web", wireType: "web_search" },
|
|
{ order: webTools.toReversed(), name: "current_web", wireType: "web_search" },
|
|
{ order: webTools, name: "preview_web", wireType: "web_search_preview" },
|
|
{ order: webTools.toReversed(), name: "preview_web", wireType: "web_search_preview" },
|
|
])("uses the uniquely forced web variant independent of declaration order", ({ order, name, wireType }) => {
|
|
const result = prepareResponsesTools({
|
|
tools: order,
|
|
toolChoice: { type: "tool", toolName: name },
|
|
strictJsonSchema: false,
|
|
})
|
|
|
|
expect(result.toolChoice).toEqual({ type: wireType })
|
|
expect(result.selectedHostedTool).toMatchObject({ name, type: wireType, responseType: "web_search" })
|
|
})
|
|
|
|
test.each([{ order: webTools }, { order: webTools.toReversed() }])(
|
|
"allows indistinguishable web variants when they share one logical name",
|
|
({ order }) => {
|
|
const tools = order.map((tool) => ({ ...tool, name: "web" }))
|
|
const result = prepareResponsesTools({ tools, toolChoice: { type: "required" }, strictJsonSchema: false })
|
|
|
|
expect(result.hostedTools.map((tool) => tool.name)).toEqual(["web", "web"])
|
|
},
|
|
)
|
|
|
|
const duplicateToolCases = [
|
|
[
|
|
{ type: "function", name: "lookup", inputSchema: { type: "object" } },
|
|
{ type: "provider", id: "openai.web_search", name: "lookup", args: {} },
|
|
],
|
|
[
|
|
{ type: "provider", id: "other.unsupported", name: "lookup", args: {} },
|
|
{ type: "provider", id: "openai.web_search", name: "lookup", args: {} },
|
|
],
|
|
[
|
|
{ type: "provider", id: "openai.web_search", name: "lookup", args: {} },
|
|
{ type: "provider", id: "openai.web_search_preview", name: "lookup", args: {} },
|
|
],
|
|
] satisfies Array<NonNullable<LanguageModelV3CallOptions["tools"]>>
|
|
|
|
test.each(duplicateToolCases.flatMap((tools) => [{ tools }, { tools: tools.toReversed() }]))(
|
|
"rejects duplicate forced definitions independent of type and order",
|
|
({ tools }) => {
|
|
expect(() =>
|
|
prepareResponsesTools({
|
|
tools,
|
|
toolChoice: { type: "tool", toolName: "lookup" },
|
|
strictJsonSchema: false,
|
|
}),
|
|
).toThrow("multiple tool definitions share this name")
|
|
},
|
|
)
|
|
|
|
const duplicateHostedToolCases = [
|
|
{ id: "openai.web_search", responseType: "web_search", args: {} },
|
|
{ id: "openai.web_search_preview", responseType: "web_search", args: {} },
|
|
{ id: "openai.file_search", responseType: "file_search", args: { vectorStoreIds: ["store_1"] } },
|
|
{ id: "openai.code_interpreter", responseType: "code_interpreter", args: {} },
|
|
{ id: "openai.image_generation", responseType: "image_generation", args: {} },
|
|
] as const
|
|
|
|
function duplicateHostedTools(testCase: (typeof duplicateHostedToolCases)[number], sameName = false) {
|
|
return [
|
|
{ type: "provider" as const, id: testCase.id, name: `${testCase.responseType}_one`, args: testCase.args },
|
|
{
|
|
type: "provider" as const,
|
|
id: testCase.id,
|
|
name: sameName ? `${testCase.responseType}_one` : `${testCase.responseType}_two`,
|
|
args: testCase.args,
|
|
},
|
|
]
|
|
}
|
|
|
|
test.each(
|
|
duplicateHostedToolCases.flatMap((testCase) =>
|
|
[undefined, { type: "auto" as const }, { type: "required" as const }].flatMap((toolChoice) => {
|
|
const tools = duplicateHostedTools(testCase)
|
|
return [
|
|
{ testCase, tools, toolChoice, selection: toolChoice?.type ?? "default" },
|
|
{ testCase, tools: tools.toReversed(), toolChoice, selection: toolChoice?.type ?? "default" },
|
|
]
|
|
}),
|
|
),
|
|
)(
|
|
"rejects differently named duplicate $testCase.id responses for $selection selection",
|
|
({ testCase, tools, toolChoice }) => {
|
|
expect(() => prepareResponsesTools({ tools, toolChoice, strictJsonSchema: false })).toThrow(
|
|
`ambiguous ${testCase.responseType} response for hosted tools`,
|
|
)
|
|
},
|
|
)
|
|
|
|
test.each(
|
|
duplicateHostedToolCases.flatMap((testCase) => {
|
|
const tools = duplicateHostedTools(testCase, true)
|
|
return [
|
|
{ testCase, tools },
|
|
{ testCase, tools: tools.toReversed() },
|
|
]
|
|
}),
|
|
)("allows duplicate $testCase.id responses with the same logical name", ({ tools }) => {
|
|
expect(
|
|
prepareResponsesTools({ tools, toolChoice: { type: "required" }, strictJsonSchema: false }).hostedTools.map(
|
|
(tool) => tool.name,
|
|
),
|
|
).toEqual([tools[0].name, tools[0].name])
|
|
})
|
|
|
|
test.each(
|
|
duplicateHostedToolCases.flatMap((testCase) => {
|
|
const tools = duplicateHostedTools(testCase)
|
|
return [
|
|
{ testCase, tools },
|
|
{ testCase, tools: tools.toReversed() },
|
|
]
|
|
}),
|
|
)("rejects a forced $testCase.id wire choice with multiple logical identities", ({ testCase, tools }) => {
|
|
expect(() =>
|
|
prepareResponsesTools({
|
|
tools,
|
|
toolChoice: { type: "tool", toolName: `${testCase.responseType}_one` },
|
|
strictJsonSchema: false,
|
|
}),
|
|
).toThrow(`ambiguous ${tools[0].id.replace("openai.", "")} tool choice for hosted tools`)
|
|
})
|
|
|
|
test.each(
|
|
duplicateHostedToolCases.flatMap((testCase) => {
|
|
const tools = duplicateHostedTools(testCase, true)
|
|
return [{ tools }, { tools: tools.toReversed() }]
|
|
}),
|
|
)("rejects a forced logical name shared by duplicate same-wire definitions", ({ tools }) => {
|
|
expect(() =>
|
|
prepareResponsesTools({
|
|
tools,
|
|
toolChoice: { type: "tool", toolName: tools[0].name },
|
|
strictJsonSchema: false,
|
|
}),
|
|
).toThrow("multiple tool definitions share this name")
|
|
})
|
|
|
|
test.each([...duplicateHostedToolCases])("skips ambiguous $id responses when tool choice is none", (testCase) => {
|
|
expect(
|
|
prepareResponsesTools({
|
|
tools: duplicateHostedTools(testCase),
|
|
toolChoice: { type: "none" },
|
|
strictJsonSchema: false,
|
|
}).toolChoice,
|
|
).toBe("none")
|
|
})
|
|
|
|
test.each([...duplicateHostedToolCases])("skips ambiguous $id responses for a uniquely forced function", (testCase) => {
|
|
expect(
|
|
prepareResponsesTools({
|
|
tools: [...duplicateHostedTools(testCase), { type: "function", name: "local", inputSchema: { type: "object" } }],
|
|
toolChoice: { type: "tool", toolName: "local" },
|
|
strictJsonSchema: false,
|
|
}).toolChoice,
|
|
).toEqual({ type: "function", name: "local" })
|
|
})
|
|
|
|
test.each([{ ambiguousWebTools: duplicateHostedTools(duplicateHostedToolCases[0]) }, { ambiguousWebTools: webTools }])(
|
|
"validates only the selected wire choice for a forced unrelated hosted tool",
|
|
({ ambiguousWebTools }) => {
|
|
const selected = duplicateHostedTools(duplicateHostedToolCases[2], true)[0]
|
|
const result = prepareResponsesTools({
|
|
tools: [...ambiguousWebTools, selected],
|
|
toolChoice: { type: "tool", toolName: selected.name },
|
|
strictJsonSchema: false,
|
|
})
|
|
|
|
expect(result.toolChoice).toEqual({ type: "file_search" })
|
|
expect(result.selectedHostedTool).toMatchObject({ name: selected.name, type: "file_search" })
|
|
},
|
|
)
|