fix: remove redundant record guards

This commit is contained in:
Armin Ronacher
2026-07-05 18:55:09 +02:00
parent ee24a9ec54
commit 035ea9c856
4 changed files with 71 additions and 88 deletions
+14 -28
View File
@@ -16,18 +16,6 @@ interface JsonSchemaObject {
oneOf?: JsonSchemaObject[];
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function isJsonSchemaObject(value: unknown): value is JsonSchemaObject {
return isRecord(value);
}
function hasTypeBoxMetadata(schema: unknown): boolean {
return isRecord(schema) && Object.getOwnPropertySymbols(schema).includes(TYPEBOX_KIND);
}
function getSchemaTypes(schema: JsonSchemaObject): string[] {
if (typeof schema.type === "string") {
return [schema.type];
@@ -53,22 +41,15 @@ function matchesJsonType(value: unknown, type: string): boolean {
case "array":
return Array.isArray(value);
case "object":
return isRecord(value) && !Array.isArray(value);
return typeof value === "object" && value !== null && !Array.isArray(value);
default:
return false;
}
}
function isValidatorSchema(value: unknown): value is Tool["parameters"] {
return isRecord(value);
}
function getSubSchemaValidator(schema: JsonSchemaObject): ReturnType<typeof Compile> | undefined {
if (!isValidatorSchema(schema)) {
return undefined;
}
try {
return getValidator(schema);
return getValidator(schema as Tool["parameters"]);
} catch {
return undefined;
}
@@ -161,7 +142,7 @@ function applySchemaObjectCoercion(value: Record<string, unknown>, schema: JsonS
}
}
if (schema.additionalProperties && isJsonSchemaObject(schema.additionalProperties)) {
if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
for (const [key, propertyValue] of Object.entries(value)) {
if (definedKeys.has(key)) {
continue;
@@ -183,7 +164,7 @@ function applySchemaArrayCoercion(value: unknown[], schema: JsonSchemaObject): v
return;
}
if (isJsonSchemaObject(schema.items)) {
if (schema.items && typeof schema.items === "object") {
for (let index = 0; index < value.length; index++) {
value[index] = coerceWithJsonSchema(value[index], schema.items);
}
@@ -232,8 +213,13 @@ function coerceWithJsonSchema(value: unknown, schema: JsonSchemaObject): unknown
}
}
if (schemaTypes.includes("object") && isRecord(nextValue) && !Array.isArray(nextValue)) {
applySchemaObjectCoercion(nextValue, schema);
if (
schemaTypes.includes("object") &&
typeof nextValue === "object" &&
nextValue !== null &&
!Array.isArray(nextValue)
) {
applySchemaObjectCoercion(nextValue as Record<string, unknown>, schema);
}
if (schemaTypes.includes("array") && Array.isArray(nextValue)) {
@@ -294,10 +280,10 @@ export function validateToolArguments(tool: Tool, toolCall: ToolCall): any {
Value.Convert(tool.parameters, args);
const validator = getValidator(tool.parameters);
if (!hasTypeBoxMetadata(tool.parameters) && isJsonSchemaObject(tool.parameters)) {
const coerced = coerceWithJsonSchema(args, tool.parameters);
if (!Object.getOwnPropertySymbols(tool.parameters).includes(TYPEBOX_KIND)) {
const coerced = coerceWithJsonSchema(args, tool.parameters as JsonSchemaObject);
if (coerced !== args) {
if (isRecord(args) && isRecord(coerced)) {
if (typeof args === "object" && args !== null && typeof coerced === "object" && coerced !== null) {
for (const key of Object.keys(args)) {
delete args[key];
}
@@ -24,27 +24,13 @@ const getImageTool: Tool<typeof getImageSchema> = {
parameters: getImageSchema,
};
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function isResponsePayload(value: unknown): value is { input: unknown[] } {
return isRecord(value) && Array.isArray(value.input);
}
function isFunctionCallOutputItem(
value: unknown,
): value is { type: "function_call_output"; output: string | ResponseFunctionCallOutputItemList } {
return isRecord(value) && value.type === "function_call_output" && "output" in value;
}
function isInputTextItem(value: unknown): value is { type: "input_text"; text: string } {
return isRecord(value) && value.type === "input_text" && typeof value.text === "string";
}
function isInputImageItem(value: unknown): value is { type: "input_image"; image_url: string } {
return isRecord(value) && value.type === "input_image" && typeof value.image_url === "string";
}
type CapturedResponsePayload = { input?: unknown[] };
type FunctionCallOutputItem = {
type: "function_call_output";
output: string | ResponseFunctionCallOutputItemList;
};
type InputTextItem = { type: "input_text"; text: string };
type InputImageItem = { type: "input_image"; image_url: string };
async function verifyToolResultImagesStayInFunctionCallOutput<TApi extends Api>(
model: Model<TApi>,
@@ -105,15 +91,19 @@ async function verifyToolResultImagesStayInFunctionCallOutput<TApi extends Api>(
expect(secondResponse.stopReason, `Error: ${secondResponse.errorMessage}`).toBe("stop");
expect(secondResponse.errorMessage).toBeFalsy();
expect(isResponsePayload(capturedPayload)).toBe(true);
if (!isResponsePayload(capturedPayload)) {
const responsePayload = capturedPayload as CapturedResponsePayload | undefined;
expect(Array.isArray(responsePayload?.input)).toBe(true);
if (!Array.isArray(responsePayload?.input)) {
throw new Error("Expected payload with input array");
}
const responseInput = responsePayload.input;
const functionCallOutputIndex = capturedPayload.input.findIndex((item) => isFunctionCallOutputItem(item));
const functionCallOutputIndex = responseInput.findIndex(
(item) => (item as { type?: unknown } | null)?.type === "function_call_output",
);
expect(functionCallOutputIndex).toBeGreaterThanOrEqual(0);
const functionCallOutput = capturedPayload.input[functionCallOutputIndex];
if (!isFunctionCallOutputItem(functionCallOutput)) {
const functionCallOutput = responseInput[functionCallOutputIndex] as FunctionCallOutputItem | undefined;
if (!functionCallOutput) {
throw new Error("Expected function_call_output item");
}
@@ -123,8 +113,12 @@ async function verifyToolResultImagesStayInFunctionCallOutput<TApi extends Api>(
}
const outputItems = functionCallOutput.output;
const textItem = outputItems.find((item) => isInputTextItem(item));
const imageItem = outputItems.find((item) => isInputImageItem(item));
const textItem = outputItems.find((item) => (item as { type?: unknown } | null)?.type === "input_text") as
| InputTextItem
| undefined;
const imageItem = outputItems.find((item) => (item as { type?: unknown } | null)?.type === "input_image") as
| InputImageItem
| undefined;
expect(textItem).toBeTruthy();
expect(imageItem).toBeTruthy();
@@ -135,9 +129,9 @@ async function verifyToolResultImagesStayInFunctionCallOutput<TApi extends Api>(
expect(textItem.text).toContain(toolText);
expect(imageItem.image_url.startsWith("data:image/png;base64,")).toBe(true);
const laterUserMessages = capturedPayload.input
const laterUserMessages = responseInput
.slice(functionCallOutputIndex + 1)
.filter((item) => isRecord(item) && item.role === "user");
.filter((item) => (item as { role?: unknown } | null)?.role === "user");
expect(laterUserMessages).toHaveLength(0);
const responseText = secondResponse.content