From 4c9cfdd7ae0e9b9a84885a46c92e17a6a85a4ebc Mon Sep 17 00:00:00 2001 From: jucheng Date: Thu, 12 Jun 2025 13:56:02 +0800 Subject: [PATCH 01/18] feat: prioritize explicit provider config over model registry lookup --- apps/claude-bridge/src/utils/provider.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/apps/claude-bridge/src/utils/provider.ts b/apps/claude-bridge/src/utils/provider.ts index 7b5e7e8..0a270af 100644 --- a/apps/claude-bridge/src/utils/provider.ts +++ b/apps/claude-bridge/src/utils/provider.ts @@ -28,18 +28,12 @@ import type { MessageCreateParamsBase } from "@anthropic-ai/sdk/resources/messag * Create provider-agnostic client for a given model */ export async function createProviderClient(config: BridgeConfig): Promise { - // For known models, use the registry const modelData = findModelData(config.model); let provider: Provider; let client: ChatClient; - if (modelData) { - // Known model - use standard approach - provider = getProviderForModel(config.model as AllModels) as Provider; - const providerConfig = buildProviderConfig(provider, config); - client = createClientForModel(config.model as AllModels, providerConfig); - } else { - // Unknown model - use the configured provider directly + // If a provider is explicitly configured, use it first + if (config.provider) { provider = config.provider; const providerConfig = buildProviderConfig(provider, config); @@ -64,6 +58,16 @@ export async function createProviderClient(config: BridgeConfig): Promise Date: Thu, 12 Jun 2025 14:30:28 +0800 Subject: [PATCH 02/18] fix: improve JSON parsing robustness in OpenAI tool arguments --- packages/lemmy/src/clients/openai.ts | 40 +++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/packages/lemmy/src/clients/openai.ts b/packages/lemmy/src/clients/openai.ts index 0ac4634..5336421 100644 --- a/packages/lemmy/src/clients/openai.ts +++ b/packages/lemmy/src/clients/openai.ts @@ -291,15 +291,49 @@ export class OpenAIClient implements ChatClient { if (argsString.trim() === "") { argsString = "{}"; } - const parsedArgs = JSON.parse(argsString); + + // Try to parse the JSON, with fallback handling for malformed JSON + let parsedArgs; + try { + parsedArgs = JSON.parse(argsString); + } catch (parseError) { + // Log the problematic JSON for debugging + console.error(`Failed to parse tool arguments for tool ${toolCallData.name}:`, parseError); + console.error( + `Problematic JSON string (length: ${argsString.length}):`, + JSON.stringify(argsString), + ); + + // Try to fix common issues with malformed JSON + let cleanedArgsString = argsString.trim(); + + // Remove any trailing non-JSON content after the last closing brace + const lastBraceIndex = cleanedArgsString.lastIndexOf("}"); + if (lastBraceIndex !== -1 && lastBraceIndex < cleanedArgsString.length - 1) { + console.log(`Trimming extra content after position ${lastBraceIndex}`); + cleanedArgsString = cleanedArgsString.substring(0, lastBraceIndex + 1); + } + + // Try parsing the cleaned string + try { + parsedArgs = JSON.parse(cleanedArgsString); + console.log(`Successfully parsed cleaned JSON for tool ${toolCallData.name}`); + } catch (secondParseError) { + console.error(`Still failed to parse cleaned JSON:`, secondParseError); + console.error(`Cleaned JSON string:`, JSON.stringify(cleanedArgsString)); + // Fall back to empty object if we can't parse it + parsedArgs = {}; + } + } + toolCalls.push({ id: toolCallData.id, name: toolCallData.name, arguments: parsedArgs, }); } catch (error) { - // Invalid JSON in tool arguments - we'll handle this as an error - console.error("Failed to parse tool arguments:", error); + // Unexpected error in tool call processing + console.error("Unexpected error processing tool call:", error); } } } From 08667417bf008d16eba674727fbad9ce99bffbc3 Mon Sep 17 00:00:00 2001 From: jucheng Date: Thu, 12 Jun 2025 14:33:00 +0800 Subject: [PATCH 03/18] debug: add complete toolCallData logging for better error diagnosis --- packages/lemmy/src/clients/openai.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/lemmy/src/clients/openai.ts b/packages/lemmy/src/clients/openai.ts index 5336421..ec39473 100644 --- a/packages/lemmy/src/clients/openai.ts +++ b/packages/lemmy/src/clients/openai.ts @@ -299,6 +299,7 @@ export class OpenAIClient implements ChatClient { } catch (parseError) { // Log the problematic JSON for debugging console.error(`Failed to parse tool arguments for tool ${toolCallData.name}:`, parseError); + console.error(`Complete toolCallData:`, toolCallData); console.error( `Problematic JSON string (length: ${argsString.length}):`, JSON.stringify(argsString), From 751577427e6a49206e1828e88cbd2c0828ea8fb8 Mon Sep 17 00:00:00 2001 From: jucheng Date: Thu, 12 Jun 2025 14:38:51 +0800 Subject: [PATCH 04/18] fix: handle concatenated JSON objects in OpenAI tool arguments --- packages/lemmy/src/clients/openai.ts | 93 +++++++++++++++++++++++----- 1 file changed, 77 insertions(+), 16 deletions(-) diff --git a/packages/lemmy/src/clients/openai.ts b/packages/lemmy/src/clients/openai.ts index ec39473..6b67133 100644 --- a/packages/lemmy/src/clients/openai.ts +++ b/packages/lemmy/src/clients/openai.ts @@ -308,22 +308,83 @@ export class OpenAIClient implements ChatClient { // Try to fix common issues with malformed JSON let cleanedArgsString = argsString.trim(); - // Remove any trailing non-JSON content after the last closing brace - const lastBraceIndex = cleanedArgsString.lastIndexOf("}"); - if (lastBraceIndex !== -1 && lastBraceIndex < cleanedArgsString.length - 1) { - console.log(`Trimming extra content after position ${lastBraceIndex}`); - cleanedArgsString = cleanedArgsString.substring(0, lastBraceIndex + 1); - } - - // Try parsing the cleaned string - try { - parsedArgs = JSON.parse(cleanedArgsString); - console.log(`Successfully parsed cleaned JSON for tool ${toolCallData.name}`); - } catch (secondParseError) { - console.error(`Still failed to parse cleaned JSON:`, secondParseError); - console.error(`Cleaned JSON string:`, JSON.stringify(cleanedArgsString)); - // Fall back to empty object if we can't parse it - parsedArgs = {}; + // Check if we have multiple JSON objects concatenated (like {"a":1}{"b":2}) + const jsonObjectPattern = /\}\s*\{/g; + const hasMultipleObjects = jsonObjectPattern.test(cleanedArgsString); + + if (hasMultipleObjects) { + console.log(`Detected concatenated JSON objects, attempting to merge`); + try { + // Split on '}{' and reconstruct individual JSON objects + const objects = []; + let currentPos = 0; + let braceCount = 0; + let currentObject = ""; + + for (let i = 0; i < cleanedArgsString.length; i++) { + const char = cleanedArgsString[i]; + currentObject += char; + + if (char === "{") { + braceCount++; + } else if (char === "}") { + braceCount--; + if (braceCount === 0) { + // Complete object found + try { + const parsed = JSON.parse(currentObject.trim()); + objects.push(parsed); + currentObject = ""; + } catch (e) { + // Skip invalid objects + currentObject = ""; + } + } + } + } + + // Merge all parsed objects into one + if (objects.length > 0) { + parsedArgs = Object.assign({}, ...objects); + console.log( + `Successfully merged ${objects.length} JSON objects for tool ${toolCallData.name}`, + ); + } else { + throw new Error("No valid JSON objects found"); + } + } catch (mergeError) { + console.error(`Failed to merge concatenated JSON objects:`, mergeError); + // Fall back to trying the first complete JSON object + const firstObjectMatch = cleanedArgsString.match(/^\{[^}]*\}/); + if (firstObjectMatch) { + try { + parsedArgs = JSON.parse(firstObjectMatch[0]); + console.log(`Used first JSON object as fallback for tool ${toolCallData.name}`); + } catch (e) { + parsedArgs = {}; + } + } else { + parsedArgs = {}; + } + } + } else { + // Remove any trailing non-JSON content after the last closing brace + const lastBraceIndex = cleanedArgsString.lastIndexOf("}"); + if (lastBraceIndex !== -1 && lastBraceIndex < cleanedArgsString.length - 1) { + console.log(`Trimming extra content after position ${lastBraceIndex}`); + cleanedArgsString = cleanedArgsString.substring(0, lastBraceIndex + 1); + } + + // Try parsing the cleaned string + try { + parsedArgs = JSON.parse(cleanedArgsString); + console.log(`Successfully parsed cleaned JSON for tool ${toolCallData.name}`); + } catch (secondParseError) { + console.error(`Still failed to parse cleaned JSON:`, secondParseError); + console.error(`Cleaned JSON string:`, JSON.stringify(cleanedArgsString)); + // Fall back to empty object if we can't parse it + parsedArgs = {}; + } } } From d8f9d97f72921fc80259dc54bfd180c9e357a6d9 Mon Sep 17 00:00:00 2001 From: jucheng Date: Thu, 12 Jun 2025 15:24:44 +0800 Subject: [PATCH 05/18] fix: handle Claude-style concatenated JSON in OpenAI tool arguments and add LLM response debugging --- packages/lemmy/src/clients/openai.ts | 98 ++++++++++++---------------- 1 file changed, 43 insertions(+), 55 deletions(-) diff --git a/packages/lemmy/src/clients/openai.ts b/packages/lemmy/src/clients/openai.ts index 6b67133..37bd7ab 100644 --- a/packages/lemmy/src/clients/openai.ts +++ b/packages/lemmy/src/clients/openai.ts @@ -229,9 +229,12 @@ export class OpenAIClient implements ChatClient { let stopReason: string | undefined; let toolCalls: ToolCall[] = []; const currentToolCalls = new Map(); + let allChunks: OpenAI.Chat.ChatCompletionChunk[] = []; // Store all chunks for debugging try { for await (const chunk of stream) { + // Store chunk for debugging + allChunks.push(chunk); // Handle usage information (comes in final chunk with stream_options) if (chunk.usage) { inputTokens = chunk.usage.prompt_tokens || 0; @@ -305,69 +308,54 @@ export class OpenAIClient implements ChatClient { JSON.stringify(argsString), ); - // Try to fix common issues with malformed JSON - let cleanedArgsString = argsString.trim(); - - // Check if we have multiple JSON objects concatenated (like {"a":1}{"b":2}) - const jsonObjectPattern = /\}\s*\{/g; - const hasMultipleObjects = jsonObjectPattern.test(cleanedArgsString); + // Log relevant streaming chunks for debugging + console.error(`LLM Response Debug - Tool call chunks for ${toolCallData.name}:`); + const relevantChunks = allChunks.filter((chunk) => + chunk.choices?.[0]?.delta?.tool_calls?.some((tc) => tc.id === toolCallData.id), + ); + if (relevantChunks.length > 0) { + console.error(`Found ${relevantChunks.length} relevant chunks:`); + relevantChunks.forEach((chunk, i) => { + console.error(`Chunk ${i}:`, JSON.stringify(chunk.choices?.[0]?.delta?.tool_calls, null, 2)); + }); + } else { + console.error(`No relevant chunks found for tool ID ${toolCallData.id}`); + } - if (hasMultipleObjects) { - console.log(`Detected concatenated JSON objects, attempting to merge`); + // Check if this looks like concatenated JSON objects (Claude-style) + if (argsString.includes("}{")) { + console.log("Detected concatenated JSON objects, attempting to merge"); try { - // Split on '}{' and reconstruct individual JSON objects - const objects = []; - let currentPos = 0; - let braceCount = 0; - let currentObject = ""; - - for (let i = 0; i < cleanedArgsString.length; i++) { - const char = cleanedArgsString[i]; - currentObject += char; - - if (char === "{") { - braceCount++; - } else if (char === "}") { - braceCount--; - if (braceCount === 0) { - // Complete object found - try { - const parsed = JSON.parse(currentObject.trim()); - objects.push(parsed); - currentObject = ""; - } catch (e) { - // Skip invalid objects - currentObject = ""; - } - } + // Split by }{ and reconstruct as separate objects + const jsonStrings = argsString.split("}{").map((part, index, array) => { + if (index === 0) return part + "}"; + if (index === array.length - 1) return "{" + part; + return "{" + part + "}"; + }); + + // Parse each JSON object and merge them + const mergedArgs: Record = {}; + for (const jsonStr of jsonStrings) { + try { + const parsed = JSON.parse(jsonStr); + Object.assign(mergedArgs, parsed); + } catch (err) { + console.error(`Failed to parse individual JSON object: ${jsonStr}`, err); } } - // Merge all parsed objects into one - if (objects.length > 0) { - parsedArgs = Object.assign({}, ...objects); - console.log( - `Successfully merged ${objects.length} JSON objects for tool ${toolCallData.name}`, - ); - } else { - throw new Error("No valid JSON objects found"); - } + parsedArgs = mergedArgs; + console.log( + `Successfully merged ${jsonStrings.length} JSON objects for tool ${toolCallData.name}`, + ); } catch (mergeError) { - console.error(`Failed to merge concatenated JSON objects:`, mergeError); - // Fall back to trying the first complete JSON object - const firstObjectMatch = cleanedArgsString.match(/^\{[^}]*\}/); - if (firstObjectMatch) { - try { - parsedArgs = JSON.parse(firstObjectMatch[0]); - console.log(`Used first JSON object as fallback for tool ${toolCallData.name}`); - } catch (e) { - parsedArgs = {}; - } - } else { - parsedArgs = {}; - } + console.error(`Failed to merge concatenated JSON:`, mergeError); + parsedArgs = {}; } } else { + // Try to fix common issues with malformed JSON + let cleanedArgsString = argsString.trim(); + // Remove any trailing non-JSON content after the last closing brace const lastBraceIndex = cleanedArgsString.lastIndexOf("}"); if (lastBraceIndex !== -1 && lastBraceIndex < cleanedArgsString.length - 1) { From 80f32460f1fe8a21f37e589bd92f9c2d7990091c Mon Sep 17 00:00:00 2001 From: jucheng Date: Thu, 12 Jun 2025 15:33:53 +0800 Subject: [PATCH 06/18] fix: make Claude-style JSON concatenation handling primary path instead of fallback --- packages/lemmy/src/clients/openai.ts | 114 +++++++++++++++------------ 1 file changed, 62 insertions(+), 52 deletions(-) diff --git a/packages/lemmy/src/clients/openai.ts b/packages/lemmy/src/clients/openai.ts index 37bd7ab..22e638a 100644 --- a/packages/lemmy/src/clients/openai.ts +++ b/packages/lemmy/src/clients/openai.ts @@ -295,64 +295,74 @@ export class OpenAIClient implements ChatClient { argsString = "{}"; } - // Try to parse the JSON, with fallback handling for malformed JSON + // Handle tool arguments parsing with Claude-style concatenated JSON support let parsedArgs; - try { - parsedArgs = JSON.parse(argsString); - } catch (parseError) { - // Log the problematic JSON for debugging - console.error(`Failed to parse tool arguments for tool ${toolCallData.name}:`, parseError); - console.error(`Complete toolCallData:`, toolCallData); - console.error( - `Problematic JSON string (length: ${argsString.length}):`, - JSON.stringify(argsString), - ); - - // Log relevant streaming chunks for debugging - console.error(`LLM Response Debug - Tool call chunks for ${toolCallData.name}:`); - const relevantChunks = allChunks.filter((chunk) => - chunk.choices?.[0]?.delta?.tool_calls?.some((tc) => tc.id === toolCallData.id), - ); - if (relevantChunks.length > 0) { - console.error(`Found ${relevantChunks.length} relevant chunks:`); - relevantChunks.forEach((chunk, i) => { - console.error(`Chunk ${i}:`, JSON.stringify(chunk.choices?.[0]?.delta?.tool_calls, null, 2)); - }); - } else { - console.error(`No relevant chunks found for tool ID ${toolCallData.id}`); - } - // Check if this looks like concatenated JSON objects (Claude-style) - if (argsString.includes("}{")) { - console.log("Detected concatenated JSON objects, attempting to merge"); - try { - // Split by }{ and reconstruct as separate objects - const jsonStrings = argsString.split("}{").map((part, index, array) => { - if (index === 0) return part + "}"; - if (index === array.length - 1) return "{" + part; - return "{" + part + "}"; - }); + // Check if this looks like concatenated JSON objects (Claude-style) first + if (argsString.includes("}{")) { + console.log("Detected concatenated JSON objects, attempting to merge"); + try { + // Split by }{ and reconstruct as separate objects + const jsonStrings = argsString.split("}{").map((part, index, array) => { + if (index === 0) return part + "}"; + if (index === array.length - 1) return "{" + part; + return "{" + part + "}"; + }); - // Parse each JSON object and merge them - const mergedArgs: Record = {}; - for (const jsonStr of jsonStrings) { - try { - const parsed = JSON.parse(jsonStr); - Object.assign(mergedArgs, parsed); - } catch (err) { - console.error(`Failed to parse individual JSON object: ${jsonStr}`, err); - } + // Parse each JSON object and merge them + const mergedArgs: Record = {}; + for (const jsonStr of jsonStrings) { + try { + const parsed = JSON.parse(jsonStr); + Object.assign(mergedArgs, parsed); + } catch (err) { + console.error(`Failed to parse individual JSON object: ${jsonStr}`, err); } + } - parsedArgs = mergedArgs; - console.log( - `Successfully merged ${jsonStrings.length} JSON objects for tool ${toolCallData.name}`, - ); - } catch (mergeError) { - console.error(`Failed to merge concatenated JSON:`, mergeError); - parsedArgs = {}; + parsedArgs = mergedArgs; + console.log( + `Successfully merged ${jsonStrings.length} JSON objects for tool ${toolCallData.name}`, + ); + } catch (mergeError) { + console.error(`Failed to merge concatenated JSON:`, mergeError); + console.error(`Complete toolCallData:`, toolCallData); + console.error( + `Problematic JSON string (length: ${argsString.length}):`, + JSON.stringify(argsString), + ); + parsedArgs = {}; + } + } else { + // Standard JSON parsing + try { + parsedArgs = JSON.parse(argsString); + } catch (parseError) { + // Log the problematic JSON for debugging + console.error(`Failed to parse tool arguments for tool ${toolCallData.name}:`, parseError); + console.error(`Complete toolCallData:`, toolCallData); + console.error( + `Problematic JSON string (length: ${argsString.length}):`, + JSON.stringify(argsString), + ); + + // Log relevant streaming chunks for debugging + console.error(`LLM Response Debug - Tool call chunks for ${toolCallData.name}:`); + const relevantChunks = allChunks.filter((chunk) => + chunk.choices?.[0]?.delta?.tool_calls?.some((tc) => tc.id === toolCallData.id), + ); + if (relevantChunks.length > 0) { + console.error(`Found ${relevantChunks.length} relevant chunks:`); + relevantChunks.forEach((chunk, i) => { + console.error( + `Chunk ${i}:`, + JSON.stringify(chunk.choices?.[0]?.delta?.tool_calls, null, 2), + ); + }); + } else { + console.error(`No relevant chunks found for tool ID ${toolCallData.id}`); } - } else { + // Try to fix common issues with malformed JSON let cleanedArgsString = argsString.trim(); From 7d4b1a1d51ac50c28ce0b11cfcbd5d829b45c65f Mon Sep 17 00:00:00 2001 From: jucheng Date: Thu, 12 Jun 2025 15:36:20 +0800 Subject: [PATCH 07/18] fix: handle duplicate keys in concatenated JSON by converting to arrays --- packages/lemmy/src/clients/openai.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/lemmy/src/clients/openai.ts b/packages/lemmy/src/clients/openai.ts index 22e638a..7aca1a5 100644 --- a/packages/lemmy/src/clients/openai.ts +++ b/packages/lemmy/src/clients/openai.ts @@ -309,12 +309,25 @@ export class OpenAIClient implements ChatClient { return "{" + part + "}"; }); - // Parse each JSON object and merge them + // Parse each JSON object and handle duplicate keys properly const mergedArgs: Record = {}; + const arrayKeys = new Set(); + for (const jsonStr of jsonStrings) { try { const parsed = JSON.parse(jsonStr); - Object.assign(mergedArgs, parsed); + for (const [key, value] of Object.entries(parsed)) { + if (mergedArgs[key] !== undefined) { + // Duplicate key found - convert to array + if (!arrayKeys.has(key)) { + mergedArgs[key] = [mergedArgs[key]]; + arrayKeys.add(key); + } + (mergedArgs[key] as unknown[]).push(value); + } else { + mergedArgs[key] = value; + } + } } catch (err) { console.error(`Failed to parse individual JSON object: ${jsonStr}`, err); } From 9c9af1eb124f13259ff057a54410af5aed554a2a Mon Sep 17 00:00:00 2001 From: jucheng Date: Fri, 13 Jun 2025 22:14:47 +0800 Subject: [PATCH 08/18] Revert "fix: handle duplicate keys in concatenated JSON by converting to arrays" This reverts commit 7d4b1a1d51ac50c28ce0b11cfcbd5d829b45c65f. --- packages/lemmy/src/clients/openai.ts | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/packages/lemmy/src/clients/openai.ts b/packages/lemmy/src/clients/openai.ts index 7aca1a5..22e638a 100644 --- a/packages/lemmy/src/clients/openai.ts +++ b/packages/lemmy/src/clients/openai.ts @@ -309,25 +309,12 @@ export class OpenAIClient implements ChatClient { return "{" + part + "}"; }); - // Parse each JSON object and handle duplicate keys properly + // Parse each JSON object and merge them const mergedArgs: Record = {}; - const arrayKeys = new Set(); - for (const jsonStr of jsonStrings) { try { const parsed = JSON.parse(jsonStr); - for (const [key, value] of Object.entries(parsed)) { - if (mergedArgs[key] !== undefined) { - // Duplicate key found - convert to array - if (!arrayKeys.has(key)) { - mergedArgs[key] = [mergedArgs[key]]; - arrayKeys.add(key); - } - (mergedArgs[key] as unknown[]).push(value); - } else { - mergedArgs[key] = value; - } - } + Object.assign(mergedArgs, parsed); } catch (err) { console.error(`Failed to parse individual JSON object: ${jsonStr}`, err); } From 6323502004ad369f293a444bc64c00f6ff2b290d Mon Sep 17 00:00:00 2001 From: jucheng Date: Fri, 13 Jun 2025 22:14:55 +0800 Subject: [PATCH 09/18] Revert "fix: make Claude-style JSON concatenation handling primary path instead of fallback" This reverts commit 80f32460f1fe8a21f37e589bd92f9c2d7990091c. --- packages/lemmy/src/clients/openai.ts | 114 ++++++++++++--------------- 1 file changed, 52 insertions(+), 62 deletions(-) diff --git a/packages/lemmy/src/clients/openai.ts b/packages/lemmy/src/clients/openai.ts index 22e638a..37bd7ab 100644 --- a/packages/lemmy/src/clients/openai.ts +++ b/packages/lemmy/src/clients/openai.ts @@ -295,74 +295,64 @@ export class OpenAIClient implements ChatClient { argsString = "{}"; } - // Handle tool arguments parsing with Claude-style concatenated JSON support + // Try to parse the JSON, with fallback handling for malformed JSON let parsedArgs; - - // Check if this looks like concatenated JSON objects (Claude-style) first - if (argsString.includes("}{")) { - console.log("Detected concatenated JSON objects, attempting to merge"); - try { - // Split by }{ and reconstruct as separate objects - const jsonStrings = argsString.split("}{").map((part, index, array) => { - if (index === 0) return part + "}"; - if (index === array.length - 1) return "{" + part; - return "{" + part + "}"; + try { + parsedArgs = JSON.parse(argsString); + } catch (parseError) { + // Log the problematic JSON for debugging + console.error(`Failed to parse tool arguments for tool ${toolCallData.name}:`, parseError); + console.error(`Complete toolCallData:`, toolCallData); + console.error( + `Problematic JSON string (length: ${argsString.length}):`, + JSON.stringify(argsString), + ); + + // Log relevant streaming chunks for debugging + console.error(`LLM Response Debug - Tool call chunks for ${toolCallData.name}:`); + const relevantChunks = allChunks.filter((chunk) => + chunk.choices?.[0]?.delta?.tool_calls?.some((tc) => tc.id === toolCallData.id), + ); + if (relevantChunks.length > 0) { + console.error(`Found ${relevantChunks.length} relevant chunks:`); + relevantChunks.forEach((chunk, i) => { + console.error(`Chunk ${i}:`, JSON.stringify(chunk.choices?.[0]?.delta?.tool_calls, null, 2)); }); + } else { + console.error(`No relevant chunks found for tool ID ${toolCallData.id}`); + } + + // Check if this looks like concatenated JSON objects (Claude-style) + if (argsString.includes("}{")) { + console.log("Detected concatenated JSON objects, attempting to merge"); + try { + // Split by }{ and reconstruct as separate objects + const jsonStrings = argsString.split("}{").map((part, index, array) => { + if (index === 0) return part + "}"; + if (index === array.length - 1) return "{" + part; + return "{" + part + "}"; + }); - // Parse each JSON object and merge them - const mergedArgs: Record = {}; - for (const jsonStr of jsonStrings) { - try { - const parsed = JSON.parse(jsonStr); - Object.assign(mergedArgs, parsed); - } catch (err) { - console.error(`Failed to parse individual JSON object: ${jsonStr}`, err); + // Parse each JSON object and merge them + const mergedArgs: Record = {}; + for (const jsonStr of jsonStrings) { + try { + const parsed = JSON.parse(jsonStr); + Object.assign(mergedArgs, parsed); + } catch (err) { + console.error(`Failed to parse individual JSON object: ${jsonStr}`, err); + } } - } - parsedArgs = mergedArgs; - console.log( - `Successfully merged ${jsonStrings.length} JSON objects for tool ${toolCallData.name}`, - ); - } catch (mergeError) { - console.error(`Failed to merge concatenated JSON:`, mergeError); - console.error(`Complete toolCallData:`, toolCallData); - console.error( - `Problematic JSON string (length: ${argsString.length}):`, - JSON.stringify(argsString), - ); - parsedArgs = {}; - } - } else { - // Standard JSON parsing - try { - parsedArgs = JSON.parse(argsString); - } catch (parseError) { - // Log the problematic JSON for debugging - console.error(`Failed to parse tool arguments for tool ${toolCallData.name}:`, parseError); - console.error(`Complete toolCallData:`, toolCallData); - console.error( - `Problematic JSON string (length: ${argsString.length}):`, - JSON.stringify(argsString), - ); - - // Log relevant streaming chunks for debugging - console.error(`LLM Response Debug - Tool call chunks for ${toolCallData.name}:`); - const relevantChunks = allChunks.filter((chunk) => - chunk.choices?.[0]?.delta?.tool_calls?.some((tc) => tc.id === toolCallData.id), - ); - if (relevantChunks.length > 0) { - console.error(`Found ${relevantChunks.length} relevant chunks:`); - relevantChunks.forEach((chunk, i) => { - console.error( - `Chunk ${i}:`, - JSON.stringify(chunk.choices?.[0]?.delta?.tool_calls, null, 2), - ); - }); - } else { - console.error(`No relevant chunks found for tool ID ${toolCallData.id}`); + parsedArgs = mergedArgs; + console.log( + `Successfully merged ${jsonStrings.length} JSON objects for tool ${toolCallData.name}`, + ); + } catch (mergeError) { + console.error(`Failed to merge concatenated JSON:`, mergeError); + parsedArgs = {}; } - + } else { // Try to fix common issues with malformed JSON let cleanedArgsString = argsString.trim(); From 66d7351caf612042a5eb9a5e91bb65ad2c217eea Mon Sep 17 00:00:00 2001 From: jucheng Date: Fri, 13 Jun 2025 22:15:01 +0800 Subject: [PATCH 10/18] Revert "fix: handle Claude-style concatenated JSON in OpenAI tool arguments and add LLM response debugging" This reverts commit d8f9d97f72921fc80259dc54bfd180c9e357a6d9. --- packages/lemmy/src/clients/openai.ts | 98 ++++++++++++++++------------ 1 file changed, 55 insertions(+), 43 deletions(-) diff --git a/packages/lemmy/src/clients/openai.ts b/packages/lemmy/src/clients/openai.ts index 37bd7ab..6b67133 100644 --- a/packages/lemmy/src/clients/openai.ts +++ b/packages/lemmy/src/clients/openai.ts @@ -229,12 +229,9 @@ export class OpenAIClient implements ChatClient { let stopReason: string | undefined; let toolCalls: ToolCall[] = []; const currentToolCalls = new Map(); - let allChunks: OpenAI.Chat.ChatCompletionChunk[] = []; // Store all chunks for debugging try { for await (const chunk of stream) { - // Store chunk for debugging - allChunks.push(chunk); // Handle usage information (comes in final chunk with stream_options) if (chunk.usage) { inputTokens = chunk.usage.prompt_tokens || 0; @@ -308,54 +305,69 @@ export class OpenAIClient implements ChatClient { JSON.stringify(argsString), ); - // Log relevant streaming chunks for debugging - console.error(`LLM Response Debug - Tool call chunks for ${toolCallData.name}:`); - const relevantChunks = allChunks.filter((chunk) => - chunk.choices?.[0]?.delta?.tool_calls?.some((tc) => tc.id === toolCallData.id), - ); - if (relevantChunks.length > 0) { - console.error(`Found ${relevantChunks.length} relevant chunks:`); - relevantChunks.forEach((chunk, i) => { - console.error(`Chunk ${i}:`, JSON.stringify(chunk.choices?.[0]?.delta?.tool_calls, null, 2)); - }); - } else { - console.error(`No relevant chunks found for tool ID ${toolCallData.id}`); - } + // Try to fix common issues with malformed JSON + let cleanedArgsString = argsString.trim(); - // Check if this looks like concatenated JSON objects (Claude-style) - if (argsString.includes("}{")) { - console.log("Detected concatenated JSON objects, attempting to merge"); + // Check if we have multiple JSON objects concatenated (like {"a":1}{"b":2}) + const jsonObjectPattern = /\}\s*\{/g; + const hasMultipleObjects = jsonObjectPattern.test(cleanedArgsString); + + if (hasMultipleObjects) { + console.log(`Detected concatenated JSON objects, attempting to merge`); try { - // Split by }{ and reconstruct as separate objects - const jsonStrings = argsString.split("}{").map((part, index, array) => { - if (index === 0) return part + "}"; - if (index === array.length - 1) return "{" + part; - return "{" + part + "}"; - }); - - // Parse each JSON object and merge them - const mergedArgs: Record = {}; - for (const jsonStr of jsonStrings) { - try { - const parsed = JSON.parse(jsonStr); - Object.assign(mergedArgs, parsed); - } catch (err) { - console.error(`Failed to parse individual JSON object: ${jsonStr}`, err); + // Split on '}{' and reconstruct individual JSON objects + const objects = []; + let currentPos = 0; + let braceCount = 0; + let currentObject = ""; + + for (let i = 0; i < cleanedArgsString.length; i++) { + const char = cleanedArgsString[i]; + currentObject += char; + + if (char === "{") { + braceCount++; + } else if (char === "}") { + braceCount--; + if (braceCount === 0) { + // Complete object found + try { + const parsed = JSON.parse(currentObject.trim()); + objects.push(parsed); + currentObject = ""; + } catch (e) { + // Skip invalid objects + currentObject = ""; + } + } } } - parsedArgs = mergedArgs; - console.log( - `Successfully merged ${jsonStrings.length} JSON objects for tool ${toolCallData.name}`, - ); + // Merge all parsed objects into one + if (objects.length > 0) { + parsedArgs = Object.assign({}, ...objects); + console.log( + `Successfully merged ${objects.length} JSON objects for tool ${toolCallData.name}`, + ); + } else { + throw new Error("No valid JSON objects found"); + } } catch (mergeError) { - console.error(`Failed to merge concatenated JSON:`, mergeError); - parsedArgs = {}; + console.error(`Failed to merge concatenated JSON objects:`, mergeError); + // Fall back to trying the first complete JSON object + const firstObjectMatch = cleanedArgsString.match(/^\{[^}]*\}/); + if (firstObjectMatch) { + try { + parsedArgs = JSON.parse(firstObjectMatch[0]); + console.log(`Used first JSON object as fallback for tool ${toolCallData.name}`); + } catch (e) { + parsedArgs = {}; + } + } else { + parsedArgs = {}; + } } } else { - // Try to fix common issues with malformed JSON - let cleanedArgsString = argsString.trim(); - // Remove any trailing non-JSON content after the last closing brace const lastBraceIndex = cleanedArgsString.lastIndexOf("}"); if (lastBraceIndex !== -1 && lastBraceIndex < cleanedArgsString.length - 1) { From 0cf2760b2386e7df5c044fe9ea7f2e6b443af9d6 Mon Sep 17 00:00:00 2001 From: jucheng Date: Fri, 13 Jun 2025 22:15:08 +0800 Subject: [PATCH 11/18] Revert "fix: handle concatenated JSON objects in OpenAI tool arguments" This reverts commit 751577427e6a49206e1828e88cbd2c0828ea8fb8. --- packages/lemmy/src/clients/openai.ts | 93 +++++----------------------- 1 file changed, 16 insertions(+), 77 deletions(-) diff --git a/packages/lemmy/src/clients/openai.ts b/packages/lemmy/src/clients/openai.ts index 6b67133..ec39473 100644 --- a/packages/lemmy/src/clients/openai.ts +++ b/packages/lemmy/src/clients/openai.ts @@ -308,83 +308,22 @@ export class OpenAIClient implements ChatClient { // Try to fix common issues with malformed JSON let cleanedArgsString = argsString.trim(); - // Check if we have multiple JSON objects concatenated (like {"a":1}{"b":2}) - const jsonObjectPattern = /\}\s*\{/g; - const hasMultipleObjects = jsonObjectPattern.test(cleanedArgsString); - - if (hasMultipleObjects) { - console.log(`Detected concatenated JSON objects, attempting to merge`); - try { - // Split on '}{' and reconstruct individual JSON objects - const objects = []; - let currentPos = 0; - let braceCount = 0; - let currentObject = ""; - - for (let i = 0; i < cleanedArgsString.length; i++) { - const char = cleanedArgsString[i]; - currentObject += char; - - if (char === "{") { - braceCount++; - } else if (char === "}") { - braceCount--; - if (braceCount === 0) { - // Complete object found - try { - const parsed = JSON.parse(currentObject.trim()); - objects.push(parsed); - currentObject = ""; - } catch (e) { - // Skip invalid objects - currentObject = ""; - } - } - } - } - - // Merge all parsed objects into one - if (objects.length > 0) { - parsedArgs = Object.assign({}, ...objects); - console.log( - `Successfully merged ${objects.length} JSON objects for tool ${toolCallData.name}`, - ); - } else { - throw new Error("No valid JSON objects found"); - } - } catch (mergeError) { - console.error(`Failed to merge concatenated JSON objects:`, mergeError); - // Fall back to trying the first complete JSON object - const firstObjectMatch = cleanedArgsString.match(/^\{[^}]*\}/); - if (firstObjectMatch) { - try { - parsedArgs = JSON.parse(firstObjectMatch[0]); - console.log(`Used first JSON object as fallback for tool ${toolCallData.name}`); - } catch (e) { - parsedArgs = {}; - } - } else { - parsedArgs = {}; - } - } - } else { - // Remove any trailing non-JSON content after the last closing brace - const lastBraceIndex = cleanedArgsString.lastIndexOf("}"); - if (lastBraceIndex !== -1 && lastBraceIndex < cleanedArgsString.length - 1) { - console.log(`Trimming extra content after position ${lastBraceIndex}`); - cleanedArgsString = cleanedArgsString.substring(0, lastBraceIndex + 1); - } - - // Try parsing the cleaned string - try { - parsedArgs = JSON.parse(cleanedArgsString); - console.log(`Successfully parsed cleaned JSON for tool ${toolCallData.name}`); - } catch (secondParseError) { - console.error(`Still failed to parse cleaned JSON:`, secondParseError); - console.error(`Cleaned JSON string:`, JSON.stringify(cleanedArgsString)); - // Fall back to empty object if we can't parse it - parsedArgs = {}; - } + // Remove any trailing non-JSON content after the last closing brace + const lastBraceIndex = cleanedArgsString.lastIndexOf("}"); + if (lastBraceIndex !== -1 && lastBraceIndex < cleanedArgsString.length - 1) { + console.log(`Trimming extra content after position ${lastBraceIndex}`); + cleanedArgsString = cleanedArgsString.substring(0, lastBraceIndex + 1); + } + + // Try parsing the cleaned string + try { + parsedArgs = JSON.parse(cleanedArgsString); + console.log(`Successfully parsed cleaned JSON for tool ${toolCallData.name}`); + } catch (secondParseError) { + console.error(`Still failed to parse cleaned JSON:`, secondParseError); + console.error(`Cleaned JSON string:`, JSON.stringify(cleanedArgsString)); + // Fall back to empty object if we can't parse it + parsedArgs = {}; } } From 03f037a3796d697d3f3994b386ab26ff5316ce82 Mon Sep 17 00:00:00 2001 From: jucheng Date: Fri, 13 Jun 2025 22:15:15 +0800 Subject: [PATCH 12/18] Revert "debug: add complete toolCallData logging for better error diagnosis" This reverts commit 08667417bf008d16eba674727fbad9ce99bffbc3. --- packages/lemmy/src/clients/openai.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/lemmy/src/clients/openai.ts b/packages/lemmy/src/clients/openai.ts index ec39473..5336421 100644 --- a/packages/lemmy/src/clients/openai.ts +++ b/packages/lemmy/src/clients/openai.ts @@ -299,7 +299,6 @@ export class OpenAIClient implements ChatClient { } catch (parseError) { // Log the problematic JSON for debugging console.error(`Failed to parse tool arguments for tool ${toolCallData.name}:`, parseError); - console.error(`Complete toolCallData:`, toolCallData); console.error( `Problematic JSON string (length: ${argsString.length}):`, JSON.stringify(argsString), From c2406980c85fe7ead840ad963b7669b938772f86 Mon Sep 17 00:00:00 2001 From: jucheng Date: Fri, 13 Jun 2025 22:15:22 +0800 Subject: [PATCH 13/18] Revert "fix: improve JSON parsing robustness in OpenAI tool arguments" This reverts commit c8fc2bed1fa5753e29a7e4effcb280967611af1e. --- packages/lemmy/src/clients/openai.ts | 40 +++------------------------- 1 file changed, 3 insertions(+), 37 deletions(-) diff --git a/packages/lemmy/src/clients/openai.ts b/packages/lemmy/src/clients/openai.ts index 5336421..0ac4634 100644 --- a/packages/lemmy/src/clients/openai.ts +++ b/packages/lemmy/src/clients/openai.ts @@ -291,49 +291,15 @@ export class OpenAIClient implements ChatClient { if (argsString.trim() === "") { argsString = "{}"; } - - // Try to parse the JSON, with fallback handling for malformed JSON - let parsedArgs; - try { - parsedArgs = JSON.parse(argsString); - } catch (parseError) { - // Log the problematic JSON for debugging - console.error(`Failed to parse tool arguments for tool ${toolCallData.name}:`, parseError); - console.error( - `Problematic JSON string (length: ${argsString.length}):`, - JSON.stringify(argsString), - ); - - // Try to fix common issues with malformed JSON - let cleanedArgsString = argsString.trim(); - - // Remove any trailing non-JSON content after the last closing brace - const lastBraceIndex = cleanedArgsString.lastIndexOf("}"); - if (lastBraceIndex !== -1 && lastBraceIndex < cleanedArgsString.length - 1) { - console.log(`Trimming extra content after position ${lastBraceIndex}`); - cleanedArgsString = cleanedArgsString.substring(0, lastBraceIndex + 1); - } - - // Try parsing the cleaned string - try { - parsedArgs = JSON.parse(cleanedArgsString); - console.log(`Successfully parsed cleaned JSON for tool ${toolCallData.name}`); - } catch (secondParseError) { - console.error(`Still failed to parse cleaned JSON:`, secondParseError); - console.error(`Cleaned JSON string:`, JSON.stringify(cleanedArgsString)); - // Fall back to empty object if we can't parse it - parsedArgs = {}; - } - } - + const parsedArgs = JSON.parse(argsString); toolCalls.push({ id: toolCallData.id, name: toolCallData.name, arguments: parsedArgs, }); } catch (error) { - // Unexpected error in tool call processing - console.error("Unexpected error processing tool call:", error); + // Invalid JSON in tool arguments - we'll handle this as an error + console.error("Failed to parse tool arguments:", error); } } } From 4c73a6dc52fd0496cd45a030dc5177f832992ee2 Mon Sep 17 00:00:00 2001 From: jucheng Date: Fri, 13 Jun 2025 22:15:31 +0800 Subject: [PATCH 14/18] Revert "feat: prioritize explicit provider config over model registry lookup" This reverts commit 4c9cfdd7ae0e9b9a84885a46c92e17a6a85a4ebc. --- apps/claude-bridge/src/utils/provider.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/apps/claude-bridge/src/utils/provider.ts b/apps/claude-bridge/src/utils/provider.ts index 0a270af..7b5e7e8 100644 --- a/apps/claude-bridge/src/utils/provider.ts +++ b/apps/claude-bridge/src/utils/provider.ts @@ -28,12 +28,18 @@ import type { MessageCreateParamsBase } from "@anthropic-ai/sdk/resources/messag * Create provider-agnostic client for a given model */ export async function createProviderClient(config: BridgeConfig): Promise { + // For known models, use the registry const modelData = findModelData(config.model); let provider: Provider; let client: ChatClient; - // If a provider is explicitly configured, use it first - if (config.provider) { + if (modelData) { + // Known model - use standard approach + provider = getProviderForModel(config.model as AllModels) as Provider; + const providerConfig = buildProviderConfig(provider, config); + client = createClientForModel(config.model as AllModels, providerConfig); + } else { + // Unknown model - use the configured provider directly provider = config.provider; const providerConfig = buildProviderConfig(provider, config); @@ -58,16 +64,6 @@ export async function createProviderClient(config: BridgeConfig): Promise Date: Sat, 14 Jun 2025 11:43:25 +0800 Subject: [PATCH 15/18] feat: add proxy provider with OpenAI-compatible API and Anthropic-style tool parsing --- apps/claude-bridge/src/types.ts | 4 +- apps/claude-bridge/src/utils/provider.ts | 146 ++++++++++++++++++----- 2 files changed, 119 insertions(+), 31 deletions(-) diff --git a/apps/claude-bridge/src/types.ts b/apps/claude-bridge/src/types.ts index ac6e180..d13ada3 100644 --- a/apps/claude-bridge/src/types.ts +++ b/apps/claude-bridge/src/types.ts @@ -24,7 +24,7 @@ export interface RawPair { note?: string; } -export type Provider = "anthropic" | "openai" | "google"; +export type Provider = "anthropic" | "openai" | "google" | "proxy"; // JSON Schema types export interface JSONSchema { @@ -73,7 +73,7 @@ export interface ProviderClientInfo { modelData: ModelData | null; // null for unknown models } -export type ProviderConfig = AnthropicConfig | OpenAIConfig | GoogleConfig; +export type ProviderConfig = AnthropicConfig | OpenAIConfig | GoogleConfig | OpenAIConfig; // Proxy uses OpenAI config export interface TransformationEntry { timestamp: number; diff --git a/apps/claude-bridge/src/utils/provider.ts b/apps/claude-bridge/src/utils/provider.ts index 7b5e7e8..5a8601d 100644 --- a/apps/claude-bridge/src/utils/provider.ts +++ b/apps/claude-bridge/src/utils/provider.ts @@ -24,53 +24,138 @@ import type { import type { MessageCreateParamsBase } from "@anthropic-ai/sdk/resources/messages/messages.js"; +// Add debug logging wrapper for clients +function wrapClientWithLogging(client: ChatClient, provider: Provider, debug: boolean): ChatClient { + if (!debug) return client; + + return { + ...client, + ask: async (...args: Parameters) => { + const request = args[0]; + const config = (client as any).config || {}; + const fullUrl = `${config.baseURL}/chat/completions`; + console.debug(`[${provider}] Full Request Details:`, { + baseURL: config.baseURL, + fullUrl, + headers: { + Authorization: "Bearer " + (config.apiKey ? "***" + config.apiKey.slice(-4) : "undefined"), + "Content-Type": "application/json", + }, + body: request, + }); + const response = await client.ask(...args); + console.debug(`[${provider}] Response:`, JSON.stringify(response, null, 2)); + return response; + }, + }; +} + /** * Create provider-agnostic client for a given model */ export async function createProviderClient(config: BridgeConfig): Promise { - // For known models, use the registry - const modelData = findModelData(config.model); let provider: Provider; let client: ChatClient; - if (modelData) { - // Known model - use standard approach - provider = getProviderForModel(config.model as AllModels) as Provider; + // If proxy provider is specified, use it directly without model registry lookup + if (config.provider === "proxy") { + provider = "proxy"; const providerConfig = buildProviderConfig(provider, config); - client = createClientForModel(config.model as AllModels, providerConfig); + + // Create OpenAI-compatible client for proxy + const { lemmy } = await import("@mariozechner/lemmy"); + const baseURL = providerConfig.baseURL?.endsWith("/v1/chat/completions") + ? providerConfig.baseURL + : providerConfig.baseURL?.endsWith("/v1") + ? `${providerConfig.baseURL}/chat/completions` + : `${providerConfig.baseURL}/v1/chat/completions`; + + console.debug(`[${provider}] Setting up client with baseURL:`, baseURL); + + client = lemmy.openai({ + ...(providerConfig as OpenAIConfig), + baseURL, + }); + + // Wrap the client to handle Anthropic-style tool parsing and request transformation + client = { + ...client, + ask: async (options: any) => { + // Transform request to OpenAI format + const openaiRequest: any = { + model: options.model || config.model, + messages: [ + { + role: "user", + content: options.content, + }, + ], + stream: false, + max_tokens: options.maxOutputTokens || 512, + temperature: 0.7, + }; + + // Parse tools in Anthropic style before sending to OpenAI-compatible API + if (options.tools) { + openaiRequest.tools = options.tools.map((tool: any) => ({ + type: "function", + function: { + name: tool.name, + description: tool.description, + parameters: tool.input_schema, + }, + })); + } + + return client.ask(openaiRequest); + }, + }; } else { - // Unknown model - use the configured provider directly - provider = config.provider; - const providerConfig = buildProviderConfig(provider, config); + // For known models, use the registry + const modelData = findModelData(config.model); - // Create client directly using lemmy's provider factories - switch (provider) { - case "openai": { - const { lemmy } = await import("@mariozechner/lemmy"); - client = lemmy.openai(providerConfig as OpenAIConfig); - break; - } - case "google": { - const { lemmy } = await import("@mariozechner/lemmy"); - client = lemmy.google(providerConfig as GoogleConfig); - break; - } - case "anthropic": { - const { lemmy } = await import("@mariozechner/lemmy"); - client = lemmy.anthropic(providerConfig as AnthropicConfig); - break; + if (modelData) { + // Known model - use standard approach + provider = getProviderForModel(config.model as AllModels) as Provider; + const providerConfig = buildProviderConfig(provider, config); + client = createClientForModel(config.model as AllModels, providerConfig); + } else { + // Unknown model - use the configured provider directly + provider = config.provider; + const providerConfig = buildProviderConfig(provider, config); + + // Create client directly using lemmy's provider factories + switch (provider) { + case "openai": { + const { lemmy } = await import("@mariozechner/lemmy"); + client = lemmy.openai(providerConfig as OpenAIConfig); + break; + } + case "google": { + const { lemmy } = await import("@mariozechner/lemmy"); + client = lemmy.google(providerConfig as GoogleConfig); + break; + } + case "anthropic": { + const { lemmy } = await import("@mariozechner/lemmy"); + client = lemmy.anthropic(providerConfig as AnthropicConfig); + break; + } + default: + const _exhaustiveCheck: never = provider; + throw new Error(`Unsupported provider: ${_exhaustiveCheck}`); } - default: - const _exhaustiveCheck: never = provider; - throw new Error(`Unsupported provider: ${_exhaustiveCheck}`); } } + // Wrap all clients with debug logging if debug mode is enabled + client = wrapClientWithLogging(client, provider, config.debug || false); + return { client, provider, model: config.model, - modelData: modelData || null, // null for unknown models + modelData: findModelData(config.model) || null, }; } @@ -89,6 +174,7 @@ function buildProviderConfig(provider: Provider, config: BridgeConfig): Provider case "anthropic": return baseConfig as AnthropicConfig; case "openai": + case "proxy": return baseConfig as OpenAIConfig; case "google": return baseConfig as GoogleConfig; @@ -109,6 +195,7 @@ function getDefaultApiKey(provider: Provider): string { if (!anthropicKey) throw new Error("ANTHROPIC_API_KEY environment variable is required"); return anthropicKey; case "openai": + case "proxy": const openaiKey = process.env["OPENAI_API_KEY"]; if (!openaiKey) throw new Error("OPENAI_API_KEY environment variable is required"); return openaiKey; @@ -208,6 +295,7 @@ export function convertThinkingParameters( return options; case "openai": + case "proxy": return { ...baseOptions, ...(anthropicRequest.thinking?.type == "enabled" && { From 0c0fd12db7f1e6abed33510a724f98aa3aae5112 Mon Sep 17 00:00:00 2001 From: jucheng Date: Mon, 16 Jun 2025 08:58:26 +0800 Subject: [PATCH 16/18] Fix tool related bugs --- apps/claude-bridge/src/interceptor.ts | 13 +- apps/claude-bridge/src/types.ts | 2 +- apps/claude-bridge/src/utils/provider.ts | 165 +++++++++++++++++------ apps/diffy-mcp/package.json | 9 +- apps/snap-happy/package.json | 2 +- package-lock.json | 11 +- 6 files changed, 140 insertions(+), 62 deletions(-) diff --git a/apps/claude-bridge/src/interceptor.ts b/apps/claude-bridge/src/interceptor.ts index 10d1c77..169ba01 100644 --- a/apps/claude-bridge/src/interceptor.ts +++ b/apps/claude-bridge/src/interceptor.ts @@ -12,7 +12,7 @@ import { import { transformAnthropicToLemmy } from "./transforms/anthropic-to-lemmy.js"; import { createAnthropicSSE } from "./transforms/lemmy-to-anthropic.js"; import { jsonSchemaToZod } from "./transforms/tool-schemas.js"; -import type { MessageCreateParamsBase } from "@anthropic-ai/sdk/resources/messages/messages.js"; +import type { MessageCreateParamsBase, ThinkingConfigEnabled } from "@anthropic-ai/sdk/resources/messages/messages.js"; import { Context, type AskResult, @@ -233,7 +233,16 @@ export class ClaudeBridgeInterceptor { } // Convert thinking parameters for provider - const askOptions = convertThinkingParameters(this.clientInfo.provider, originalRequest); + this.logger.log(`Original thinking config: ${JSON.stringify(originalRequest.thinking)}`); + const askOptions: any = { + ...(originalRequest.thinking?.type === "enabled" && { + thinking: { + type: "enabled", + budget_tokens: originalRequest.thinking.budget_tokens, + } as ThinkingConfigEnabled, + }), + }; + this.logger.log(`Converted thinking config: ${JSON.stringify(askOptions.thinking)}`); // Apply capability adjustments if (validation.adjustments.maxOutputTokens) { diff --git a/apps/claude-bridge/src/types.ts b/apps/claude-bridge/src/types.ts index d13ada3..a3b9b90 100644 --- a/apps/claude-bridge/src/types.ts +++ b/apps/claude-bridge/src/types.ts @@ -1,5 +1,5 @@ import type { SerializedContext, ChatClient } from "@mariozechner/lemmy"; -import type { MessageCreateParamsBase } from "@anthropic-ai/sdk/resources/messages/messages.js"; +import type { MessageCreateParamsBase, ThinkingConfigEnabled } from "@anthropic-ai/sdk/resources/messages/messages.js"; import type { AnthropicConfig, OpenAIConfig, GoogleConfig } from "@mariozechner/lemmy"; import type { ModelData } from "@mariozechner/lemmy"; diff --git a/apps/claude-bridge/src/utils/provider.ts b/apps/claude-bridge/src/utils/provider.ts index 5a8601d..1fc864a 100644 --- a/apps/claude-bridge/src/utils/provider.ts +++ b/apps/claude-bridge/src/utils/provider.ts @@ -22,7 +22,11 @@ import type { ProviderConfig, } from "../types.js"; -import type { MessageCreateParamsBase } from "@anthropic-ai/sdk/resources/messages/messages.js"; +import type { + MessageCreateParamsBase, + ThinkingConfigEnabled, + ThinkingBlock, +} from "@anthropic-ai/sdk/resources/messages/messages.js"; // Add debug logging wrapper for clients function wrapClientWithLogging(client: ChatClient, provider: Provider, debug: boolean): ChatClient { @@ -34,14 +38,14 @@ function wrapClientWithLogging(client: ChatClient, provider: Provider, debug: bo const request = args[0]; const config = (client as any).config || {}; const fullUrl = `${config.baseURL}/chat/completions`; - console.debug(`[${provider}] Full Request Details:`, { + console.debug(`[${provider}] Request:`, { baseURL: config.baseURL, fullUrl, headers: { Authorization: "Bearer " + (config.apiKey ? "***" + config.apiKey.slice(-4) : "undefined"), "Content-Type": "application/json", }, - body: request, + body: JSON.stringify(request, null, 2), }); const response = await client.ask(...args); console.debug(`[${provider}] Response:`, JSON.stringify(response, null, 2)); @@ -87,7 +91,14 @@ export async function createProviderClient(config: BridgeConfig): Promise ({ + type: "tool_result", + tool_use_id: result.toolCallId, + content: result.content, + })), + ); + } + // Parse tools in Anthropic style before sending to OpenAI-compatible API if (options.tools) { openaiRequest.tools = options.tools.map((tool: any) => ({ @@ -107,7 +130,54 @@ export async function createProviderClient(config: BridgeConfig): Promise { + const content = Array.isArray(msg.content) + ? msg.content + : [ + { + type: "text", + text: msg.content, + }, + ]; + + // Add thinking block to content + const thinkingBlock: ThinkingBlock = { + type: "thinking", + thinking: "", + signature: "", + }; + content.push(thinkingBlock); + + return { + ...msg, + content, + }; + }); + } + + // Log the request + console.debug(`[${provider}] Request:`, { + baseURL: config.baseURL, + fullUrl: `${config.baseURL}/chat/completions`, + headers: { + Authorization: "Bearer " + (config.apiKey ? "***" + config.apiKey.slice(-4) : "undefined"), + "Content-Type": "application/json", + }, + body: JSON.stringify(openaiRequest, null, 2), + }); + + const response = await client.ask(openaiRequest); + console.debug(`[${provider}] Response:`, JSON.stringify(response, null, 2)); + return response; }, }; } else { @@ -255,57 +325,64 @@ export function validateCapabilities( }; } +/** + * Validate thinking parameters + */ +function validateThinkingParameters(thinking: ThinkingConfigEnabled | undefined): void { + if (!thinking) return; + + if (thinking.type !== "enabled") { + throw new Error("Invalid thinking type. Only 'enabled' is supported."); + } + + if (thinking.budget_tokens !== undefined) { + if (typeof thinking.budget_tokens !== "number" || thinking.budget_tokens < 1024) { + throw new Error("Invalid thinking budget_tokens. Must be a number >= 1024."); + } + } +} + /** * Convert thinking parameters based on provider type */ -export function convertThinkingParameters( - provider: Provider, - anthropicRequest: MessageCreateParamsBase, -): AnthropicAskOptions | OpenAIAskOptions | GoogleAskOptions { - const baseOptions = { - maxOutputTokens: anthropicRequest.max_tokens, - }; +export function convertThinkingParameters(thinking: ThinkingConfigEnabled | undefined, provider: Provider) { + if (!thinking) { + return undefined; + } + // Validate thinking parameters + if (thinking.type !== "enabled") { + throw new Error('Invalid thinking configuration: type must be "enabled"'); + } + + if (typeof thinking.budget_tokens !== "number" || thinking.budget_tokens < 1024) { + throw new Error("Invalid thinking configuration: budget_tokens must be a number >= 1024"); + } + + // Convert based on provider switch (provider) { case "anthropic": return { - ...baseOptions, - // Anthropic uses the same thinking parameters - ...(anthropicRequest.thinking?.type == "enabled" && { - thinkingEnabled: true, - }), - ...(anthropicRequest.thinking?.type == "enabled" && - anthropicRequest.thinking.budget_tokens !== undefined && { - maxThinkingTokens: anthropicRequest.thinking.budget_tokens, - }), - } as AnthropicAskOptions; - + type: "enabled", + budget_tokens: thinking.budget_tokens, + }; case "google": - const options: GoogleAskOptions = { - ...baseOptions, - // Google uses includeThoughts for thinking - ...(anthropicRequest.thinking?.type == "enabled" && { - includeThoughts: true, - }), - ...(anthropicRequest.thinking?.type == "enabled" && - anthropicRequest.thinking.budget_tokens !== undefined && { - thinkingBudget: anthropicRequest.thinking.budget_tokens, - }), + return { + includeThoughts: true, + thinkingBudget: thinking.budget_tokens, }; - return options; - case "openai": + return { + reasoningEffort: "high", + }; case "proxy": return { - ...baseOptions, - ...(anthropicRequest.thinking?.type == "enabled" && { - reasoningEffort: "medium" as const, - }), - } as OpenAIAskOptions; - + thinking: { + type: "enabled", + budget_tokens: thinking.budget_tokens, + }, + }; default: - // TypeScript exhaustiveness check - const _exhaustiveCheck: never = provider; - throw new Error(`Unsupported provider: ${_exhaustiveCheck}`); + return undefined; } } diff --git a/apps/diffy-mcp/package.json b/apps/diffy-mcp/package.json index e562a34..41b94fb 100644 --- a/apps/diffy-mcp/package.json +++ b/apps/diffy-mcp/package.json @@ -6,12 +6,13 @@ "packages/*" ], "scripts": { - "build": "npm run build --workspace=packages/server --workspace=packages/frontend", - "dev": "npm run dev --workspace=packages/server --workspace=packages/frontend", - "test": "npm run test --workspace=packages/server --workspace=packages/frontend", + "prebuild": "npm install", + "build": "cd packages/server && npm run build && cd ../frontend && npm run build", + "dev": "cd packages/server && npm run dev && cd ../frontend && npm run dev", + "test": "cd packages/server && npm run test && cd ../frontend && npm run test", "test:manual": "node scripts/test-manual.js", "test:auto": "node scripts/test-auto.js", - "clean": "npm run clean --workspace=packages/server --workspace=packages/frontend" + "clean": "cd packages/server && npm run clean && cd ../frontend && npm run clean" }, "keywords": [ "mcp", diff --git a/apps/snap-happy/package.json b/apps/snap-happy/package.json index 1da159a..68164c0 100644 --- a/apps/snap-happy/package.json +++ b/apps/snap-happy/package.json @@ -8,7 +8,7 @@ "snap-happy": "dist/index.js" }, "scripts": { - "build": "tsc && chmod +x dist/index.js && cd native && make universal", + "build": "tsc && chmod +x dist/index.js && ([ \"$(uname)\" = \"Darwin\" ] && cd native && make universal || echo 'Skipping native build on non-macOS platform')", "build:dev": "tsc && chmod +x dist/index.js && cd native && make dev", "build:native": "cd native && make universal", "build:native:dev": "cd native && make dev", diff --git a/package-lock.json b/package-lock.json index e480cd3..be4e530 100644 --- a/package-lock.json +++ b/package-lock.json @@ -126,6 +126,7 @@ }, "apps/code-search-mcp": { "version": "1.0.0", + "extraneous": true, "license": "MIT", "dependencies": { "web-tree-sitter": "^0.24.4" @@ -3837,10 +3838,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/code-search-mcp": { - "resolved": "apps/code-search-mcp", - "link": true - }, "node_modules/collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", @@ -10944,12 +10941,6 @@ "node": ">= 14" } }, - "node_modules/web-tree-sitter": { - "version": "0.24.7", - "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.24.7.tgz", - "integrity": "sha512-CdC/TqVFbXqR+C51v38hv6wOPatKEUGxa39scAeFSm98wIhZxAYonhRQPSMmfZ2w7JDI0zQDdzdmgtNk06/krQ==", - "license": "MIT" - }, "node_modules/webidl-conversions": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", From aebd09142353e55b6b70b6dbe1b6210968e6a51f Mon Sep 17 00:00:00 2001 From: wolvever Date: Fri, 4 Jul 2025 16:31:20 +0800 Subject: [PATCH 17/18] fix: claude-bridge proxy request format and debug logging - Fix request format issue where OpenAI client with custom baseURL was sending malformed requests - Add claude-opus-4-20250514 model support to OpenAI provider for proxy usage - Enable debug logging for all providers to show request/response details - Remove duplicate logging that was interfering with request transformation - Update claude-bridge version to 1.0.13 This resolves the issue where requests were being sent with {"content": "..."} format instead of the proper OpenAI format with {"messages": [...], "model": "..."} --- apps/claude-bridge/package.json | 2 +- apps/claude-bridge/src/interceptor.ts | 8 +- apps/claude-bridge/src/utils/provider.ts | 138 +++++++++++++++++------ packages/lemmy/src/generated/models.ts | 13 ++- 4 files changed, 121 insertions(+), 40 deletions(-) diff --git a/apps/claude-bridge/package.json b/apps/claude-bridge/package.json index c1a9ca8..2de87d7 100644 --- a/apps/claude-bridge/package.json +++ b/apps/claude-bridge/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/claude-bridge", - "version": "1.0.10", + "version": "1.0.13", "description": "Use non-Anthropic models with Claude Code by proxying requests through the lemmy unified interface", "type": "module", "main": "dist/index.js", diff --git a/apps/claude-bridge/src/interceptor.ts b/apps/claude-bridge/src/interceptor.ts index 169ba01..f21b169 100644 --- a/apps/claude-bridge/src/interceptor.ts +++ b/apps/claude-bridge/src/interceptor.ts @@ -31,7 +31,6 @@ import { parseSSE, extractAssistantFromSSE } from "./utils/sse.js"; import { parseAnthropicMessageCreateRequest, parseResponse, - isAnthropicAPI, generateRequestId, type ParsedRequestData, } from "./utils/request-parser.js"; @@ -465,3 +464,10 @@ export async function initializeInterceptor(config?: BridgeConfig): Promise) => { const request = args[0]; const config = (client as any).config || {}; - const fullUrl = `${config.baseURL}/chat/completions`; - console.debug(`[${provider}] Request:`, { - baseURL: config.baseURL, - fullUrl, - headers: { - Authorization: "Bearer " + (config.apiKey ? "***" + config.apiKey.slice(-4) : "undefined"), - "Content-Type": "application/json", - }, - body: JSON.stringify(request, null, 2), - }); - const response = await client.ask(...args); - console.debug(`[${provider}] Response:`, JSON.stringify(response, null, 2)); - return response; + + // For OpenAI provider, the request will be transformed internally + // so we just log what we're sending to the lemmy client + if (provider === "openai") { + console.debug(`[${provider}] Sending request to lemmy client:`, { + model: config.model, + baseURL: config.baseURL, + request: JSON.stringify(request, null, 2), + }); + } else { + const fullUrl = config.baseURL?.endsWith("/completions") + ? config.baseURL + : `${config.baseURL}/chat/completions`; + + // Generate curl command for easy debugging + const curlCommand = [ + "curl", + "-X POST", + `"${fullUrl}"`, + '-H "Content-Type: application/json"', + `-H "Authorization: Bearer ${config.apiKey || "YOUR_API_KEY"}"`, + `-d '${JSON.stringify(request)}'`, + ].join(" \\\n "); + + console.debug(`[${provider}] Request as curl command:\n${curlCommand}`); + + console.debug(`[${provider}] Request details:`, { + baseURL: config.baseURL, + fullUrl, + headers: { + Authorization: "Bearer " + (config.apiKey ? "***" + config.apiKey.slice(-4) : "undefined"), + "Content-Type": "application/json", + }, + body: JSON.stringify(request, null, 2), + }); + } + + try { + const response = await client.ask(...args); + console.debug(`[${provider}] Response:`, JSON.stringify(response, null, 2)); + return response; + } catch (error) { + console.debug(`[${provider}] Error:`, error); + throw error; + } }, }; } @@ -68,11 +100,13 @@ export async function createProviderClient(config: BridgeConfig): Promise { // Transform request to OpenAI format + const contentBlocks = Array.isArray(options.content) + ? options.content + : [{ type: "text", text: options.content }]; + const openaiRequest: any = { model: options.model || config.model, messages: [ { role: "user", - content: Array.isArray(options.content) - ? options.content - : [ - { - type: "text", - text: options.content, - }, - ], + content: + contentBlocks.length === 1 && contentBlocks[0]?.type === "text" + ? contentBlocks[0].text + : contentBlocks, }, ], stream: false, max_tokens: options.maxOutputTokens || 512, - temperature: 0.7, + temperature: options.temperature || 0.7, }; // Handle tool results if present if (options.toolResults && Array.isArray(options.toolResults)) { + // Ensure content is an array before pushing tool results + if (typeof openaiRequest.messages[0].content === "string") { + openaiRequest.messages[0].content = [{ type: "text", text: openaiRequest.messages[0].content }]; + } // Add tool results to the content array of the first message openaiRequest.messages[0].content.push( ...options.toolResults.map((result: any) => ({ @@ -165,15 +203,33 @@ export async function createProviderClient(config: BridgeConfig): Promise Date: Fri, 4 Jul 2025 21:45:59 +0800 Subject: [PATCH 18/18] fix: claude-bridge tool parameter handling and bump to v1.0.14 - Fix missing tool parameters in lemmy-to-anthropic transformation - Tool call content_block.input now uses toolCall.arguments instead of empty object - Resolves issue where Write tool 'content' parameter was missing - Bump version from 1.0.13 to 1.0.14 --- apps/claude-bridge/package.json | 2 +- apps/claude-bridge/src/transforms/lemmy-to-anthropic.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/claude-bridge/package.json b/apps/claude-bridge/package.json index 2de87d7..9fddac6 100644 --- a/apps/claude-bridge/package.json +++ b/apps/claude-bridge/package.json @@ -1,6 +1,6 @@ { "name": "@mariozechner/claude-bridge", - "version": "1.0.13", + "version": "1.0.14", "description": "Use non-Anthropic models with Claude Code by proxying requests through the lemmy unified interface", "type": "module", "main": "dist/index.js", diff --git a/apps/claude-bridge/src/transforms/lemmy-to-anthropic.ts b/apps/claude-bridge/src/transforms/lemmy-to-anthropic.ts index f920174..869dc2b 100644 --- a/apps/claude-bridge/src/transforms/lemmy-to-anthropic.ts +++ b/apps/claude-bridge/src/transforms/lemmy-to-anthropic.ts @@ -84,7 +84,7 @@ export function createAnthropicSSE(askResult: AskResult, model: string): Readabl writeEvent("content_block_start", { type: "content_block_start", index: blockIndex, - content_block: { type: "tool_use", id: toolCall.id, name: toolCall.name, input: {} }, + content_block: { type: "tool_use", id: toolCall.id, name: toolCall.name, input: toolCall.arguments }, }); const argsJson = JSON.stringify(toolCall.arguments); for (let i = 0; i < argsJson.length; i += 50) {