add deferred tools support for kimi in openai-completions api
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { Type } from "typebox";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { convertMessages } from "../src/api/openai-completions.ts";
|
||||
import { getModel, streamSimple } from "../src/compat.ts";
|
||||
import type { Api, AssistantMessage, Context, Model, Tool, ToolResultMessage, UserMessage } from "../src/types.ts";
|
||||
import { estimateContextTokens } from "../src/utils/estimate.ts";
|
||||
@@ -49,6 +50,26 @@ interface OpenAIPayload {
|
||||
input?: Array<OpenAIToolSearchCall | OpenAIToolSearchOutput | { type?: string }>;
|
||||
}
|
||||
|
||||
interface KimiTool {
|
||||
type: "function";
|
||||
function: {
|
||||
name: string;
|
||||
description?: string;
|
||||
parameters?: Record<string, unknown>;
|
||||
};
|
||||
}
|
||||
|
||||
interface KimiMessage {
|
||||
role: string;
|
||||
content?: unknown;
|
||||
tools?: KimiTool[];
|
||||
}
|
||||
|
||||
interface KimiPayload {
|
||||
tools?: KimiTool[];
|
||||
messages: KimiMessage[];
|
||||
}
|
||||
|
||||
class PayloadCaptured extends Error {}
|
||||
|
||||
function makeTool(name: string): Tool {
|
||||
@@ -102,6 +123,22 @@ function makeContext(tools: Tool[], addedToolNames = ["late_tool"]): Context {
|
||||
};
|
||||
}
|
||||
|
||||
function makeKimiModel(deferredToolsMode?: "kimi"): Model<"openai-completions"> {
|
||||
return {
|
||||
id: "deferred-tools-model",
|
||||
name: "Deferred Tools Model",
|
||||
api: "openai-completions",
|
||||
provider: "moonshotai",
|
||||
baseUrl: "http://127.0.0.1:9/v1",
|
||||
reasoning: false,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128000,
|
||||
maxTokens: 4096,
|
||||
compat: deferredToolsMode ? { deferredToolsMode } : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
async function capturePayload<T>(model: Model<Api>, context: Context, apiKey = "fake-key"): Promise<T> {
|
||||
let captured: T | undefined;
|
||||
const stream = streamSimple({ ...model, baseUrl: "http://127.0.0.1:9" }, context, {
|
||||
@@ -297,6 +334,63 @@ describe("deferred tools", () => {
|
||||
expect(payload.tools?.find((tool) => tool.name === "late_tool")?.defer_loading).toBe(true);
|
||||
});
|
||||
|
||||
it("serializes Kimi deferred tools as system tool definitions", async () => {
|
||||
const context = makeContext([makeTool("base_tool"), makeTool("late_tool")]);
|
||||
const payload = await capturePayload<KimiPayload>(makeKimiModel("kimi"), context);
|
||||
|
||||
expect(payload.tools?.map((tool) => tool.function.name)).toEqual(["base_tool"]);
|
||||
const toolResultIndex = payload.messages.findIndex((message) => message.role === "tool");
|
||||
const systemToolIndex = payload.messages.findIndex((message) => message.tools !== undefined);
|
||||
expect(toolResultIndex).toBeGreaterThanOrEqual(0);
|
||||
expect(systemToolIndex).toBeGreaterThan(toolResultIndex);
|
||||
expect(payload.messages[systemToolIndex]?.tools?.map((tool) => tool.function.name)).toEqual(["late_tool"]);
|
||||
});
|
||||
|
||||
it("emits Kimi deferred schemas after all tool results in a batch", () => {
|
||||
const context = makeContext([makeTool("base_tool"), makeTool("late_tool"), makeTool("later_tool")]);
|
||||
context.messages.splice(3, 0, {
|
||||
...makeToolResult(["later_tool"]),
|
||||
toolCallId: "call_2",
|
||||
});
|
||||
|
||||
const messages = convertMessages(makeKimiModel("kimi"), context, {
|
||||
supportsStore: false,
|
||||
supportsDeveloperRole: false,
|
||||
supportsReasoningEffort: false,
|
||||
supportsUsageInStreaming: true,
|
||||
maxTokensField: "max_tokens",
|
||||
requiresToolResultName: false,
|
||||
requiresAssistantAfterToolResult: false,
|
||||
requiresThinkingAsText: false,
|
||||
requiresReasoningContentOnAssistantMessages: false,
|
||||
thinkingFormat: "openai",
|
||||
openRouterRouting: {},
|
||||
vercelGatewayRouting: {},
|
||||
chatTemplateKwargs: {},
|
||||
zaiToolStream: false,
|
||||
supportsStrictMode: false,
|
||||
cacheControlFormat: undefined,
|
||||
sendSessionAffinityHeaders: false,
|
||||
deferredToolsMode: "kimi",
|
||||
sessionAffinityFormat: "openai",
|
||||
supportsLongCacheRetention: false,
|
||||
});
|
||||
|
||||
expect(messages.map((message) => message.role)).toEqual(["user", "assistant", "tool", "tool", "system", "user"]);
|
||||
expect((messages[4] as { tools?: KimiTool[] }).tools?.map((tool) => tool.function.name)).toEqual([
|
||||
"late_tool",
|
||||
"later_tool",
|
||||
]);
|
||||
});
|
||||
|
||||
it("leaves OpenAI Completions tools unchanged without Kimi mode", async () => {
|
||||
const context = makeContext([makeTool("base_tool"), makeTool("late_tool")]);
|
||||
const payload = await capturePayload<KimiPayload>(makeKimiModel(), context);
|
||||
|
||||
expect(payload.tools?.map((tool) => tool.function.name)).toEqual(["base_tool", "late_tool"]);
|
||||
expect(payload.messages.some((message) => message.tools !== undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it("loads an OpenAI Responses tool through client tool search", async () => {
|
||||
const context = makeContext([makeTool("base_tool"), makeTool("late_tool")]);
|
||||
const payload = await capturePayload<OpenAIPayload>(getModel("openai", "gpt-5.4"), context);
|
||||
|
||||
@@ -41,8 +41,9 @@ const compat = {
|
||||
sendSessionAffinityHeaders: false,
|
||||
sessionAffinityFormat: "openai",
|
||||
supportsLongCacheRetention: true,
|
||||
} satisfies Required<Omit<OpenAICompletionsCompat, "cacheControlFormat">> & {
|
||||
} satisfies Omit<Required<OpenAICompletionsCompat>, "cacheControlFormat" | "deferredToolsMode"> & {
|
||||
cacheControlFormat?: OpenAICompletionsCompat["cacheControlFormat"];
|
||||
deferredToolsMode?: OpenAICompletionsCompat["deferredToolsMode"];
|
||||
};
|
||||
|
||||
function buildModel(baseUrl = "http://127.0.0.1:1"): Model<"openai-completions"> {
|
||||
|
||||
@@ -19,7 +19,9 @@ const emptyUsage: Usage = {
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
||||
};
|
||||
|
||||
const compat: Required<OpenAICompletionsCompat> = {
|
||||
const compat: Omit<Required<OpenAICompletionsCompat>, "deferredToolsMode"> & {
|
||||
deferredToolsMode?: OpenAICompletionsCompat["deferredToolsMode"];
|
||||
} = {
|
||||
supportsStore: true,
|
||||
supportsDeveloperRole: true,
|
||||
supportsReasoningEffort: true,
|
||||
|
||||
Reference in New Issue
Block a user