File size: 3,899 Bytes
eb1202c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 | import { describe, expect, test } from "bun:test"
import { Effect } from "effect"
import { LLM } from "../src"
import { OpenAIChat } from "../src/protocols"
import { ToolSchemaProjection } from "../src/protocols/utils/tool-schema"
import { Auth, LLMClient } from "../src/route"
import { it } from "./lib/effect"
describe("tool schema projections", () => {
test("moonshot strips $ref siblings and converts tuple arrays to a schema object", () => {
expect(
ToolSchemaProjection.moonshot({
type: "object",
properties: {
linked: { $ref: "#/$defs/Linked", description: "drop me" },
tuple: { type: "array", items: [{ type: "string" }, { type: "number" }] },
prefixTuple: { type: "array", prefixItems: [{ type: "boolean" }, { type: "string" }] },
},
}),
).toEqual({
type: "object",
properties: {
linked: { $ref: "#/$defs/Linked" },
tuple: { type: "array", items: { anyOf: [{ type: "string" }, { type: "number" }] } },
prefixTuple: { type: "array", items: { anyOf: [{ type: "boolean" }, { type: "string" }] } },
},
})
})
test("gemini handles numeric enums, dangling required fields, untyped arrays, and scalar object keys", () => {
expect(
ToolSchemaProjection.gemini({
type: "object",
required: ["status", "missing"],
properties: {
status: { type: "integer", enum: [1, 2] },
tags: { type: "array" },
name: { type: "string", properties: { ignored: { type: "string" } }, required: ["ignored"] },
},
}),
).toEqual({
type: "object",
required: ["status"],
properties: {
status: { type: "string", enum: ["1", "2"] },
tags: { type: "array", items: { type: "string" } },
name: { type: "string" },
},
})
})
test("openai keeps one flat object top-level schema", () => {
expect(
ToolSchemaProjection.openAI({
anyOf: [
{
type: "object",
properties: {
path: { type: "string" },
maybe: { anyOf: [{ type: "string" }, { type: "null" }] },
},
},
{ type: "object", properties: { resource: { type: "string" } } },
],
}),
).toEqual({
type: "object",
properties: {
path: { type: "string" },
maybe: { type: "string" },
resource: { type: "string" },
},
additionalProperties: false,
})
})
it.effect("applies model compatibility before protocol projection", () =>
Effect.gen(function* () {
const model = OpenAIChat.route
.with({ endpoint: { baseURL: "https://api.openai.test/v1/" }, auth: Auth.bearer("test") })
.model({ id: "kimi-k2", compatibility: { toolSchema: "moonshot" } })
const prepared = yield* LLMClient.prepare<OpenAIChat.OpenAIChatBody>(
LLM.request({
model,
prompt: "Use the tool.",
tools: [
{
name: "lookup",
description: "Lookup data.",
inputSchema: {
type: "object",
anyOf: [
{
type: "object",
properties: {
tuple: { type: "array", items: [{ type: "string" }, { type: "number" }] },
linked: { $ref: "#/$defs/Linked", description: "drop me" },
},
},
],
},
},
],
}),
)
expect(prepared.body.tools?.[0]?.function.parameters).toEqual({
type: "object",
properties: {
tuple: { type: "array", items: { anyOf: [{ type: "string" }, { type: "number" }] } },
linked: { $ref: "#/$defs/Linked" },
},
additionalProperties: false,
})
}),
)
})
|