dist / toolBridge.js
dist / toolBridge.js
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ToolBridge = void 0;
const node_crypto_1 = require("node:crypto");
const jsonschema_1 = require("jsonschema");
class ToolBridge {
tools;
aliases = new Map();
originals = new Map();
normalized = new Map();
validator = new jsonschema_1.Validator();
constructor(definitions) {
const reserved = new Set(definitions.map(tool => tool.function.name));
const normalize = (name) => name.replace(/[^A-Za-z0-9_-]+/g, "_");
const counts = new Map();
for (const tool of definitions) {
const key = normalize(tool.function.name);
counts.set(key, (counts.get(key) ?? 0) + 1);
this.normalized.set(key, counts.get(key) === 1 ? tool.function.name : null);
}
const used = new Set();
for (const tool of definitions) {
const name = tool.function.name;
if (this.originals.has(name))
throw new Error(`Duplicate tool name: ${name}`);
this.originals.set(name, tool);
let alias = name;
if (!/^[A-Za-z0-9_-]{1,64}$/.test(name)) {
const slug = name.replace(/[^A-Za-z0-9_-]+/g, "_").slice(0, 40) || "tool";
const simple = normalize(name);
if (simple.length <= 64 && counts.get(simple) === 1 && !reserved.has(simple))
alias = simple;
else {
let salt = 0;
do {
const hash = (0, node_crypto_1.createHash)("sha256").update(`${name}:${salt++}`).digest("hex").slice(0, 16);
alias = `${slug}_${hash}`;
} while (reserved.has(alias) || used.has(alias));
}
}
used.add(alias);
this.aliases.set(name, alias);
}
const names = [...this.aliases.keys()].sort((a, b) => b.length - a.length);
const rewrite = (value) => {
if (typeof value === "string") {
// One pass avoids rewriting an alias a second time when names overlap.
const pattern = names.map(name => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
return pattern ? value.replace(new RegExp(pattern, "g"), name => this.aliases.get(name)) : value;
}
if (Array.isArray(value))
return value.map(rewrite);
if (value && typeof value === "object")
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, rewrite(item)]));
return value;
};
this.tools = definitions.map(tool => ({ type: "function", function: {
name: this.aliases.get(tool.function.name),
...(tool.function.description ? { description: rewrite(tool.function.description) } : {}),
// Keep schema keys, enums and defaults exact. Only rewrite natural-language descriptions.
parameters: this.rewriteSchemaDescriptions(tool.function.parameters ?? {}, rewrite),
} }));
}
rewriteSchemaDescriptions(value, rewrite) {
if (Array.isArray(value))
return value.map(item => this.rewriteSchemaDescriptions(item, rewrite));
if (value && typeof value === "object")
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key,
["enum", "const", "default", "examples"].includes(key) ? item :
key === "description" && typeof item === "string" ? rewrite(item) : this.rewriteSchemaDescriptions(item, rewrite)]));
return value;
}
original(name) {
if (this.originals.has(name))
return name;
for (const [original, alias] of this.aliases)
if (alias === name)
return original;
// Models sometimes normalize spaces themselves or omit a collision suffix.
// Only accept this spelling if exactly one currently declared tool matches.
return this.normalized.get(name) ?? undefined;
}
mapHistory(messages) {
return messages.map(message => {
if (!message.tool_calls)
return message;
return { ...message, tool_calls: message.tool_calls.map((entry) => ({ ...entry,
function: { ...entry.function, name: this.aliases.get(entry.function.name) ?? entry.function.name },
})) };
});
}
// Only an entire answer consisting of complete tool envelopes qualifies.
// Quoted examples, fenced code, prose, unknown tools and invalid arguments stay text.
recoverXml(text) {
const result = [];
const outer = /\s*<tool_call>\s*<function=([^<>\r\n]+)>\s*([\s\S]*?)\s*<\/function>\s*<\/tool_call>\s*/gy;
let at = 0;
while (at < text.length) {
outer.lastIndex = at;
const match = outer.exec(text);
if (!match)
return;
at = outer.lastIndex;
const name = this.original(match[1].trim());
if (!name || result.length >= 32)
return;
const schema = this.originals.get(name).function.parameters;
const args = Object.create(null);
const parameters = /\s*<parameter=([A-Za-z0-9_-]+)>\s*([\s\S]*?)\s*<\/parameter>\s*/gy;
let offset = 0;
while (offset < match[2].length) {
parameters.lastIndex = offset;
const parameter = parameters.exec(match[2]);
if (!parameter)
return;
offset = parameters.lastIndex;
const key = parameter[1];
if (Object.hasOwn(args, key) || !Object.hasOwn(schema?.properties ?? {}, key))
return;
const property = schema.properties[key];
const types = typeof property === "object" ? [property.type].flat() : [];
const value = parameter[2].trim();
if (types.includes("string"))
args[key] = value;
else {
try {
args[key] = JSON.parse(value);
}
catch {
return;
}
}
}
try {
if (schema && !this.validator.validate(args, schema).valid)
return;
}
catch {
return;
}
result.push({ id: `call_${(0, node_crypto_1.randomUUID)().replace(/-/g, "")}`, name, arguments: JSON.stringify(args), parsed: args });
}
return result.length ? result : undefined;
}
}
exports.ToolBridge = ToolBridge;
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ToolBridge = void 0;
const node_crypto_1 = require("node:crypto");
const jsonschema_1 = require("jsonschema");
class ToolBridge {
tools;
aliases = new Map();
originals = new Map();
normalized = new Map();
validator = new jsonschema_1.Validator();
constructor(definitions) {
const reserved = new Set(definitions.map(tool => tool.function.name));
const normalize = (name) => name.replace(/[^A-Za-z0-9_-]+/g, "_");
const counts = new Map();
for (const tool of definitions) {
const key = normalize(tool.function.name);
counts.set(key, (counts.get(key) ?? 0) + 1);
this.normalized.set(key, counts.get(key) === 1 ? tool.function.name : null);
}
const used = new Set();
for (const tool of definitions) {
const name = tool.function.name;
if (this.originals.has(name))
throw new Error(`Duplicate tool name: ${name}`);
this.originals.set(name, tool);
let alias = name;
if (!/^[A-Za-z0-9_-]{1,64}$/.test(name)) {
const slug = name.replace(/[^A-Za-z0-9_-]+/g, "_").slice(0, 40) || "tool";
const simple = normalize(name);
if (simple.length <= 64 && counts.get(simple) === 1 && !reserved.has(simple))
alias = simple;
else {
let salt = 0;
do {
const hash = (0, node_crypto_1.createHash)("sha256").update(`${name}:${salt++}`).digest("hex").slice(0, 16);
alias = `${slug}_${hash}`;
} while (reserved.has(alias) || used.has(alias));
}
}
used.add(alias);
this.aliases.set(name, alias);
}
const names = [...this.aliases.keys()].sort((a, b) => b.length - a.length);
const rewrite = (value) => {
if (typeof value === "string") {
// One pass avoids rewriting an alias a second time when names overlap.
const pattern = names.map(name => name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
return pattern ? value.replace(new RegExp(pattern, "g"), name => this.aliases.get(name)) : value;
}
if (Array.isArray(value))
return value.map(rewrite);
if (value && typeof value === "object")
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, rewrite(item)]));
return value;
};
this.tools = definitions.map(tool => ({ type: "function", function: {
name: this.aliases.get(tool.function.name),
...(tool.function.description ? { description: rewrite(tool.function.description) } : {}),
// Keep schema keys, enums and defaults exact. Only rewrite natural-language descriptions.
parameters: this.rewriteSchemaDescriptions(tool.function.parameters ?? {}, rewrite),
} }));
}
rewriteSchemaDescriptions(value, rewrite) {
if (Array.isArray(value))
return value.map(item => this.rewriteSchemaDescriptions(item, rewrite));
if (value && typeof value === "object")
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key,
["enum", "const", "default", "examples"].includes(key) ? item :
key === "description" && typeof item === "string" ? rewrite(item) : this.rewriteSchemaDescriptions(item, rewrite)]));
return value;
}
original(name) {
if (this.originals.has(name))
return name;
for (const [original, alias] of this.aliases)
if (alias === name)
return original;
// Models sometimes normalize spaces themselves or omit a collision suffix.
// Only accept this spelling if exactly one currently declared tool matches.
return this.normalized.get(name) ?? undefined;
}
mapHistory(messages) {
return messages.map(message => {
if (!message.tool_calls)
return message;
return { ...message, tool_calls: message.tool_calls.map((entry) => ({ ...entry,
function: { ...entry.function, name: this.aliases.get(entry.function.name) ?? entry.function.name },
})) };
});
}
// Only an entire answer consisting of complete tool envelopes qualifies.
// Quoted examples, fenced code, prose, unknown tools and invalid arguments stay text.
recoverXml(text) {
const result = [];
const outer = /\s*<tool_call>\s*<function=([^<>\r\n]+)>\s*([\s\S]*?)\s*<\/function>\s*<\/tool_call>\s*/gy;
let at = 0;
while (at < text.length) {
outer.lastIndex = at;
const match = outer.exec(text);
if (!match)
return;
at = outer.lastIndex;
const name = this.original(match[1].trim());
if (!name || result.length >= 32)
return;
const schema = this.originals.get(name).function.parameters;
const args = Object.create(null);
const parameters = /\s*<parameter=([A-Za-z0-9_-]+)>\s*([\s\S]*?)\s*<\/parameter>\s*/gy;
let offset = 0;
while (offset < match[2].length) {
parameters.lastIndex = offset;
const parameter = parameters.exec(match[2]);
if (!parameter)
return;
offset = parameters.lastIndex;
const key = parameter[1];
if (Object.hasOwn(args, key) || !Object.hasOwn(schema?.properties ?? {}, key))
return;
const property = schema.properties[key];
const types = typeof property === "object" ? [property.type].flat() : [];
const value = parameter[2].trim();
if (types.includes("string"))
args[key] = value;
else {
try {
args[key] = JSON.parse(value);
}
catch {
return;
}
}
}
try {
if (schema && !this.validator.validate(args, schema).valid)
return;
}
catch {
return;
}
result.push({ id: `call_${(0, node_crypto_1.randomUUID)().replace(/-/g, "")}`, name, arguments: JSON.stringify(args), parsed: args });
}
return result.length ? result : undefined;
}
}
exports.ToolBridge = ToolBridge;