Remove provider-level tool validation, add validateToolCall helper

This commit is contained in:
Mario Zechner 2025-12-08 18:04:33 +01:00
parent 0196308266
commit 8bec289dc6
14 changed files with 59 additions and 68 deletions

View file

@ -8,3 +8,4 @@ export * from "./stream.js";
export * from "./types.js";
export * from "./utils/overflow.js";
export * from "./utils/typebox-helpers.js";
export * from "./utils/validation.js";

View file

@ -25,7 +25,7 @@ import type {
import { AssistantMessageEventStream } from "../utils/event-stream.js";
import { parseStreamingJson } from "../utils/json-parse.js";
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
import { validateToolArguments } from "../utils/validation.js";
import { transformMessages } from "./transorm-messages.js";
/**
@ -92,7 +92,6 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
options?: AnthropicOptions,
): AssistantMessageEventStream => {
const stream = new AssistantMessageEventStream();
const shouldValidateToolCalls = options?.validateToolCallsAtProvider !== false;
(async () => {
const output: AssistantMessage = {
@ -232,15 +231,6 @@ export const streamAnthropic: StreamFunction<"anthropic-messages"> = (
});
} else if (block.type === "toolCall") {
block.arguments = parseStreamingJson(block.partialJson);
// Validate tool arguments if tool definition is available
if (shouldValidateToolCalls && context.tools) {
const tool = context.tools.find((t) => t.name === block.name);
if (tool) {
block.arguments = validateToolArguments(tool, block);
}
}
delete (block as any).partialJson;
stream.push({
type: "toolcall_end",

View file

@ -23,7 +23,7 @@ import type {
} from "../types.js";
import { AssistantMessageEventStream } from "../utils/event-stream.js";
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
import { validateToolArguments } from "../utils/validation.js";
import { transformMessages } from "./transorm-messages.js";
export interface GoogleOptions extends StreamOptions {
@ -43,7 +43,6 @@ export const streamGoogle: StreamFunction<"google-generative-ai"> = (
options?: GoogleOptions,
): AssistantMessageEventStream => {
const stream = new AssistantMessageEventStream();
const shouldValidateToolCalls = options?.validateToolCallsAtProvider !== false;
(async () => {
const output: AssistantMessage = {
@ -167,14 +166,6 @@ export const streamGoogle: StreamFunction<"google-generative-ai"> = (
...(part.thoughtSignature && { thoughtSignature: part.thoughtSignature }),
};
// Validate tool arguments if tool definition is available
if (shouldValidateToolCalls && context.tools) {
const tool = context.tools.find((t) => t.name === toolCall.name);
if (tool) {
toolCall.arguments = validateToolArguments(tool, toolCall);
}
}
output.content.push(toolCall);
stream.push({ type: "toolcall_start", contentIndex: blockIndex(), partial: output });
stream.push({

View file

@ -23,7 +23,7 @@ import type {
import { AssistantMessageEventStream } from "../utils/event-stream.js";
import { parseStreamingJson } from "../utils/json-parse.js";
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
import { validateToolArguments } from "../utils/validation.js";
import { transformMessages } from "./transorm-messages.js";
export interface OpenAICompletionsOptions extends StreamOptions {
@ -37,7 +37,6 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
options?: OpenAICompletionsOptions,
): AssistantMessageEventStream => {
const stream = new AssistantMessageEventStream();
const shouldValidateToolCalls = options?.validateToolCallsAtProvider !== false;
(async () => {
const output: AssistantMessage = {
@ -85,15 +84,6 @@ export const streamOpenAICompletions: StreamFunction<"openai-completions"> = (
});
} else if (block.type === "toolCall") {
block.arguments = JSON.parse(block.partialArgs || "{}");
// Validate tool arguments if tool definition is available
if (shouldValidateToolCalls && context.tools) {
const tool = context.tools.find((t) => t.name === block.name);
if (tool) {
block.arguments = validateToolArguments(tool, block);
}
}
delete block.partialArgs;
stream.push({
type: "toolcall_end",

View file

@ -27,7 +27,7 @@ import type {
import { AssistantMessageEventStream } from "../utils/event-stream.js";
import { parseStreamingJson } from "../utils/json-parse.js";
import { sanitizeSurrogates } from "../utils/sanitize-unicode.js";
import { validateToolArguments } from "../utils/validation.js";
import { transformMessages } from "./transorm-messages.js";
// OpenAI Responses-specific options
@ -45,7 +45,6 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
options?: OpenAIResponsesOptions,
): AssistantMessageEventStream => {
const stream = new AssistantMessageEventStream();
const shouldValidateToolCalls = options?.validateToolCallsAtProvider !== false;
// Start async processing
(async () => {
@ -240,14 +239,6 @@ export const streamOpenAIResponses: StreamFunction<"openai-responses"> = (
arguments: JSON.parse(item.arguments),
};
// Validate tool arguments if tool definition is available
if (shouldValidateToolCalls && context.tools) {
const tool = context.tools.find((t) => t.name === toolCall.name);
if (tool) {
toolCall.arguments = validateToolArguments(tool, toolCall);
}
}
stream.push({ type: "toolcall_end", contentIndex: blockIndex(), toolCall, partial: output });
}
}

View file

@ -120,7 +120,6 @@ function mapOptionsForApi<TApi extends Api>(
maxTokens: options?.maxTokens || Math.min(model.maxTokens, 32000),
signal: options?.signal,
apiKey: apiKey || options?.apiKey,
validateToolCallsAtProvider: options?.validateToolCallsAtProvider ?? true,
};
switch (model.api) {

View file

@ -37,12 +37,6 @@ export interface StreamOptions {
maxTokens?: number;
signal?: AbortSignal;
apiKey?: string;
/**
* Controls whether providers validate streamed tool calls against the tool schema.
* Defaults to true. Set to false to skip provider-level validation and allow
* downstream consumers (e.g., agentLoop) to handle validation failures.
*/
validateToolCallsAtProvider?: boolean;
}
// Unified options with reasoning passed to streamSimple() and completeSimple()

View file

@ -27,6 +27,21 @@ if (!isBrowserExtension) {
}
}
/**
* Finds a tool by name and validates the tool call arguments against its TypeBox schema
* @param tools Array of tool definitions
* @param toolCall The tool call from the LLM
* @returns The validated arguments
* @throws Error if tool is not found or validation fails
*/
export function validateToolCall(tools: Tool[], toolCall: ToolCall): any {
const tool = tools.find((t) => t.name === toolCall.name);
if (!tool) {
throw new Error(`Tool "${toolCall.name}" not found`);
}
return validateToolArguments(tool, toolCall);
}
/**
* Validates tool call arguments against the tool's TypeBox schema
* @param tool The tool definition with TypeBox schema