UNPKG

lynkr

Version:

Self-hosted LLM gateway and tier-routing proxy for Claude Code, Cursor, and Codex. Routes across Ollama, AWS Bedrock, OpenRouter, Databricks, Azure OpenAI, llama.cpp, and LM Studio with prompt caching, MCP tools, and 60-80% cost savings.

182 lines (156 loc) 6.13 kB
/** * GPT-specific utilities for handling tool calls and responses * All settings are hardcoded - no env vars required * * This module addresses GPT model compatibility issues when using Azure OpenAI * through Lynkr proxy with Claude Code: * - GPT doesn't interpret "0 files found" as a final answer * - GPT retries the same tool expecting different results * - GPT needs explicit guidance on tool result interpretation */ const logger = require("../logger"); // Hardcoded GPT settings - optimized for GPT model behavior const GPT_SETTINGS = { toolLoopThreshold: 2, // Lower than Claude's 3 to catch loops earlier enhancedFormatting: true, // Always format results explicitly for GPT similarityThreshold: 0.8, // For detecting similar (not just identical) tool calls }; // Provider identifiers that use GPT models const GPT_PROVIDERS = ['azure-openai', 'openai']; /** * Check if a provider uses GPT models * @param {string} provider - Provider type (e.g., 'azure-openai', 'databricks') * @returns {boolean} - True if provider uses GPT models */ function isGPTProvider(provider) { if (!provider) return false; return GPT_PROVIDERS.includes(provider.toLowerCase()); } /** * Get the tool loop threshold for GPT models * @returns {number} - Threshold (2 for GPT, lower than Claude's 3) */ function getGPTToolLoopThreshold() { return GPT_SETTINGS.toolLoopThreshold; } /** * Format tool result with explicit structure for GPT models * GPT models need clear, unambiguous formatting to understand tool results * * @param {string} toolName - Name of the tool that was called * @param {string} content - The tool result content * @param {Object} args - The arguments passed to the tool * @returns {string} - Formatted result with explicit status and instructions */ function formatToolResultForGPT(toolName, content, args) { // Handle empty/no results explicitly - add clear messaging to prevent retries const isEmpty = !content || content.trim() === '' || content.includes('0 files found') || content.includes('No matches found') || content.includes('No results') || content.includes('Found 0') || /^Found \d+ files?\.$/.test(content.trim()) && content.includes('Found 0'); if (isEmpty) { // Only format empty results - add explicit "don't retry" instruction return `Tool "${toolName}" completed with no results found. Query: ${JSON.stringify(args)} This is a FINAL result - do not retry this query. Respond to the user based on this outcome.`; } // For successful results, return content as-is (don't add markers that might confuse GPT) return content; } /** * Get system prompt addendum for GPT models * This teaches GPT how to properly interpret and use tools * * @returns {string} - System prompt instructions for GPT */ function getGPTSystemPromptAddendum() { return `Use the Bash tool with ls command for listing files. After any tool returns results, respond to the user.`; } /** * Calculate string similarity using Jaccard index * Used to detect semantically similar tool calls * * @param {string} s1 - First string * @param {string} s2 - Second string * @returns {number} - Similarity score between 0 and 1 */ function stringSimilarity(s1, s2) { if (!s1 || !s2) return 0; if (s1 === s2) return 1; // Tokenize by whitespace and common delimiters const tokenize = (s) => new Set( s.toLowerCase() .split(/[\s\-_\/\.\,\:\;]+/) .filter(t => t.length > 0) ); const set1 = tokenize(s1); const set2 = tokenize(s2); const intersection = new Set([...set1].filter(x => set2.has(x))); const union = new Set([...set1, ...set2]); return union.size > 0 ? intersection.size / union.size : 0; } /** * Check if two tool calls are semantically similar * GPT often retries with slightly different parameters that are functionally equivalent * * @param {Object} call1 - First tool call {name, arguments} * @param {Object} call2 - Second tool call {name, arguments} * @returns {boolean} - True if calls are similar enough to be considered duplicates */ function areSimilarToolCalls(call1, call2) { if (!call1 || !call2) return false; // Must be the same tool const name1 = call1.function?.name ?? call1.name; const name2 = call2.function?.name ?? call2.name; if (name1 !== name2) return false; // Get arguments const args1 = call1.function?.arguments ?? call1.arguments ?? call1.input ?? {}; const args2 = call2.function?.arguments ?? call2.arguments ?? call2.input ?? {}; // Stringify for comparison const argsStr1 = typeof args1 === 'string' ? args1 : JSON.stringify(args1); const argsStr2 = typeof args2 === 'string' ? args2 : JSON.stringify(args2); // Exact match if (argsStr1 === argsStr2) return true; // For search-related tools, check semantic similarity const searchTools = ['grep', 'glob', 'search', 'find', 'read', 'bash', 'shell']; const toolName = (name1 || '').toLowerCase(); const isSearchTool = searchTools.some(t => toolName.includes(t)); if (isSearchTool) { const similarity = stringSimilarity(argsStr1, argsStr2); if (similarity >= GPT_SETTINGS.similarityThreshold) { logger.debug({ tool: name1, similarity, threshold: GPT_SETTINGS.similarityThreshold, args1: argsStr1.substring(0, 100), args2: argsStr2.substring(0, 100), }, "Similar tool call detected"); return true; } } return false; } /** * Get a signature for a tool call (for tracking in history) * @param {Object} call - Tool call object * @returns {string} - Unique signature for the call */ function getToolCallSignature(call) { const name = call.function?.name ?? call.name ?? 'unknown'; const args = call.function?.arguments ?? call.arguments ?? call.input ?? {}; const argsStr = typeof args === 'string' ? args : JSON.stringify(args); return `${name}:${argsStr}`; } module.exports = { GPT_SETTINGS, isGPTProvider, getGPTToolLoopThreshold, formatToolResultForGPT, getGPTSystemPromptAddendum, stringSimilarity, areSimilarToolCalls, getToolCallSignature, };