Files
puter/src/backend/drivers/ai-chat/utils/FunctionCalling.js
T
Daniel Salazar 9fab4742c9 fix: complete the malformed-input guards, including the two paths still live (#3663)
Review of the previous change found three of its claims unmet.

The image-generation crash it reported fixed is still reachable. The assert was
scattered across three helpers, and Gemini and OpenAI call `isHttpUrl` directly
on `input_images` without going through any of them — so two of seven providers
still 500 on a non-string. `isHttpUrl` now refuses a non-string itself, and the
shape is settled once in `ImageGenerationDriver.generate`, where the driver call
arrives, rather than per helper. That also covers `input_images` that isn't an
array, which produced a different crash per provider.

The sixth case in the ticket, previously unlocated, is
`Messages.js` reading `tool_call.function.name` with no guard — reachable with
`{"messages":[{"role":"assistant","tool_calls":[{"id":"x"}]}]}`. Guarded, along
with the same shape in `make_claude_tools`: a TypeError there carries no status,
so the retry loop reads it as a provider failure and marks the route unhealthy
for every caller.

`#hardExpiryFromExpiresIn` returning null for a bad type moved the failure past
the session INSERT, leaving an orphaned non-expiring row and still answering
500. Reverted; the controller guard is the fix, now covering fractions,
negatives and unparseable durations rather than only wrong types.

Also: the batch write handlers check that the body is an array but not what is
in it, so a null element 500s the same way; `#requireObjectBody` accepted an
array despite its name; `handleCreateAccessToken` destructured a body that may
be absent; and two AGPL notices had been rewrapped with a Markdown link.
2026-08-28 11:14:30 -07:00

159 lines
4.4 KiB
JavaScript

/*
* Copyright (C) 2024-present Puter Technologies Inc.
*
* This file is part of Puter.
*
* Puter is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { HttpError } from '@heyputer/backend/src/core/http';
export const normalize_json_schema = (schema) => {
if (!schema) return schema;
if (schema.type === 'object') {
if (!schema.properties) {
return schema;
}
const keys = Object.keys(schema.properties);
for (const key of keys) {
schema.properties[key] = normalize_json_schema(
schema.properties[key],
);
}
}
if (schema.type === 'array') {
if (!schema.items) {
schema.items = {};
} else {
schema.items = normalize_json_schema(schema.items);
}
}
return schema;
};
/**
* Normalizes the 'tools' object in-place.
*
* This function will accept an array of tools provided by the user, and produce
* a normalized object that can then be converted to the apprpriate
* representation for another service.
*
* We will accept conventions from either service that a user might expect to
* work, prioritizing the OpenAI convention when conflicting conventions are
* present.
*
* @param {any} tools
*/
export const normalize_tools_object = (tools) => {
for (let i = 0; i < tools.length; i++) {
const tool = tools[i];
if (tool.type === 'web_search') {
// OpenAI Responses specific
continue;
}
let normalized_tool = {};
const normalize_function = (fn) => {
const normal_fn = {};
let parameters = fn.parameters || fn.input_schema;
if (!parameters || typeof parameters !== 'object') {
parameters = { type: 'object' };
} else if (!parameters.type) {
parameters.type = 'object';
}
normal_fn.parameters = parameters;
if (parameters.properties) {
parameters = normalize_json_schema(parameters);
}
if (fn.name) {
normal_fn.name = fn.name;
}
if (fn.description) {
normal_fn.description = fn.description;
}
return normal_fn;
};
if (tool.input_schema) {
normalized_tool = {
type: 'function',
function: normalize_function(tool),
};
} else if (tool.type === 'function') {
normalized_tool = {
type: 'function',
function: normalize_function(tool.function || tool),
};
} else {
normalized_tool = {
type: 'function',
function: normalize_function(tool),
};
}
tools[i] = normalized_tool;
}
return tools;
};
/**
* This function will convert a normalized tools object to the format expected
* by OpenAI.
*
* @param {any} tools
* @returns
*/
export const make_openai_tools = (tools) => {
return tools;
};
/**
* This function will convert a normalized tools object to the format expected
* by Claude.
*
* @param {any} tools
* @returns
*/
export const make_claude_tools = (tools) => {
if (!tools) return undefined;
return tools.map((tool) => {
// A TypeError here carries no status, so it is read as a provider
// failure and marks the route unhealthy for everyone.
if (!tool?.function) {
throw new HttpError(
400,
"each tool must have a 'function' property",
{ legacyCode: 'bad_request' },
);
}
const { name, description, parameters } = tool.function;
return {
name,
description,
input_schema: parameters,
};
});
};