mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-11 01:32:22 +03:00
fix(skills): encode tool names with @ and . for providers rejecting them
Skill tools were advertised as 'name@version' (e.g. test-fr2@1.0.0), but DeepSeek/Groq/OpenAI reject function names not matching ^[a-zA-Z0-9_-]+$. Names already valid are left untouched; invalid ones are reversibly encoded as omr_skill_<base64url> and decoded in interception before registry lookup.
This commit is contained in:
@@ -25,11 +25,37 @@ interface GeminiTool {
|
||||
parameters: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// Provider tool/function names must match ^[a-zA-Z0-9_-]+$ (OpenAI, DeepSeek,
|
||||
// Groq, etc.). Skill identifiers are name@version (and names may contain any
|
||||
// characters), so encode identifiers that would violate the pattern into a
|
||||
// reversible base64url form. decodeSkillToolName() must be applied on the way
|
||||
// back in interception before resolving against the registry.
|
||||
const SKILL_TOOL_NAME_PREFIX = "omr_skill_";
|
||||
|
||||
export function encodeSkillToolName(name: string, version: string): string {
|
||||
const identifier = `${name}@${version}`;
|
||||
if (/^[a-zA-Z0-9_-]+$/.test(identifier)) {
|
||||
return identifier;
|
||||
}
|
||||
return `${SKILL_TOOL_NAME_PREFIX}${Buffer.from(identifier, "utf8").toString("base64url")}`;
|
||||
}
|
||||
|
||||
export function decodeSkillToolName(toolName: string): string {
|
||||
if (!toolName.startsWith(SKILL_TOOL_NAME_PREFIX)) {
|
||||
return toolName;
|
||||
}
|
||||
try {
|
||||
return Buffer.from(toolName.slice(SKILL_TOOL_NAME_PREFIX.length), "base64url").toString("utf8");
|
||||
} catch {
|
||||
return toolName;
|
||||
}
|
||||
}
|
||||
|
||||
function skillToOpenAI(skill: Skill): OpenAITool {
|
||||
return {
|
||||
type: "function",
|
||||
function: {
|
||||
name: `${skill.name}@${skill.version}`,
|
||||
name: encodeSkillToolName(skill.name, skill.version),
|
||||
description: skill.description,
|
||||
parameters: skill.schema.input,
|
||||
},
|
||||
@@ -38,7 +64,7 @@ function skillToOpenAI(skill: Skill): OpenAITool {
|
||||
|
||||
function skillToClaude(skill: Skill): ClaudeTool {
|
||||
return {
|
||||
name: `${skill.name}@${skill.version}`,
|
||||
name: encodeSkillToolName(skill.name, skill.version),
|
||||
description: skill.description,
|
||||
input_schema: skill.schema.input,
|
||||
};
|
||||
@@ -46,7 +72,7 @@ function skillToClaude(skill: Skill): ClaudeTool {
|
||||
|
||||
function skillToGemini(skill: Skill): GeminiTool {
|
||||
return {
|
||||
name: `${skill.name}@${skill.version}`,
|
||||
name: encodeSkillToolName(skill.name, skill.version),
|
||||
description: skill.description,
|
||||
parameters: skill.schema.input,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { skillExecutor } from "./executor";
|
||||
import { skillRegistry } from "./registry";
|
||||
import { builtinSkills } from "./builtins";
|
||||
import { detectProvider } from "./injection";
|
||||
import { detectProvider, decodeSkillToolName } from "./injection";
|
||||
import { OMNIROUTE_WEB_SEARCH_FALLBACK_TOOL_NAME } from "@omniroute/open-sse/services/webSearchFallback.ts";
|
||||
import { OMNIROUTE_WEB_FETCH_FALLBACK_TOOL_NAME } from "@omniroute/open-sse/services/webFetchInterception.ts";
|
||||
import { logger } from "../../../open-sse/utils/logger.ts";
|
||||
@@ -113,9 +113,10 @@ export async function interceptToolCalls(
|
||||
};
|
||||
}
|
||||
|
||||
const [name, version] = call.name.includes("@")
|
||||
? call.name.split("@")
|
||||
: [call.name, "latest"];
|
||||
const decodedName = decodeSkillToolName(call.name);
|
||||
const [name, version] = decodedName.includes("@")
|
||||
? decodedName.split("@")
|
||||
: [decodedName, "latest"];
|
||||
|
||||
const skillName = version === "latest" ? name : `${name}@${version}`;
|
||||
|
||||
@@ -226,7 +227,10 @@ function parseArguments(args: string | Record<string, unknown>): Record<string,
|
||||
}
|
||||
|
||||
function isRegisteredCustomSkill(toolName: string, apiKeyId: string): boolean {
|
||||
const [name, version] = toolName.includes("@") ? toolName.split("@", 2) : [toolName, undefined];
|
||||
const decodedName = decodeSkillToolName(toolName);
|
||||
const [name, version] = decodedName.includes("@")
|
||||
? decodedName.split("@", 2)
|
||||
: [decodedName, undefined];
|
||||
const identifier = version ? `${name}@${version}` : name;
|
||||
return skillRegistry.getSkill(identifier, apiKeyId) != null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user