diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 5a0fc39e5d..2e0305b166 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -151,11 +151,18 @@ export async function handleChatCore({ // Translate request (pass reqLogger for intermediate logging) let translatedBody = body; try { + // Issue #199: Disable tool name prefix when routing Claude-format requests + // to non-Claude backends (prefix causes tool name mismatches) + const claudeProviders = ["claude", "anthropic"]; + if (targetFormat === FORMATS.CLAUDE && !claudeProviders.includes(provider?.toLowerCase?.())) { + translatedBody = { ...translatedBody, _disableToolPrefix: true }; + } + translatedBody = translateRequest( sourceFormat, targetFormat, model, - body, + translatedBody, stream, credentials, provider, @@ -202,6 +209,7 @@ export async function handleChatCore({ // Extract toolNameMap for response translation (Claude OAuth) const toolNameMap = translatedBody._toolNameMap; delete translatedBody._toolNameMap; + delete translatedBody._disableToolPrefix; // Update model in body translatedBody.model = model; diff --git a/open-sse/services/rateLimitManager.ts b/open-sse/services/rateLimitManager.ts index dbeee45b55..0baa363a99 100644 --- a/open-sse/services/rateLimitManager.ts +++ b/open-sse/services/rateLimitManager.ts @@ -81,6 +81,7 @@ export async function initializeRateLimits() { const connections = await getProviderConnections(); let explicitCount = 0; let autoCount = 0; + let customCount = 0; for (const connRaw of connections as unknown[]) { const conn = toRecord(connRaw); @@ -88,9 +89,33 @@ export async function initializeRateLimits() { const provider = typeof conn.provider === "string" ? conn.provider : ""; const isActive = conn.isActive === true; const rateLimitProtection = conn.rateLimitProtection === true; + const customRpm = toNumber(conn.customRpm, 0); + const customTpm = toNumber(conn.customTpm, 0); if (!connectionId || !provider) continue; - if (rateLimitProtection) { + // Custom rpm/tpm configured — enable rate limiting with user-defined values (#198) + if (customRpm > 0 || customTpm > 0) { + enabledConnections.add(connectionId); + customCount++; + + const key = `${provider}:${connectionId}`; + const rpm = customRpm > 0 ? customRpm : DEFAULT_API_LIMITS.requestsPerMinute; + const minTime = Math.max(0, Math.floor(60000 / rpm) - 10); + + if (!limiters.has(key)) { + limiters.set( + key, + new Bottleneck({ + maxConcurrent: DEFAULT_API_LIMITS.concurrentRequests, + minTime, + reservoir: rpm, + reservoirRefreshAmount: rpm, + reservoirRefreshInterval: 60 * 1000, + id: key, + }) + ); + } + } else if (rateLimitProtection) { // Explicitly enabled by user enabledConnections.add(connectionId); explicitCount++; @@ -117,9 +142,9 @@ export async function initializeRateLimits() { } } - if (explicitCount > 0 || autoCount > 0) { + if (explicitCount > 0 || autoCount > 0 || customCount > 0) { console.log( - `🛡️ [RATE-LIMIT] Loaded ${explicitCount} explicit + ${autoCount} auto-enabled (API key) protection(s)` + `🛡️ [RATE-LIMIT] Loaded ${explicitCount} explicit + ${autoCount} auto-enabled + ${customCount} custom rpm/tpm protection(s)` ); } diff --git a/open-sse/translator/request/openai-to-claude.ts b/open-sse/translator/request/openai-to-claude.ts index 2958e53fec..2f5a913424 100644 --- a/open-sse/translator/request/openai-to-claude.ts +++ b/open-sse/translator/request/openai-to-claude.ts @@ -5,7 +5,8 @@ import { adjustMaxTokens } from "../helpers/maxTokensHelper.ts"; import { DEFAULT_THINKING_CLAUDE_SIGNATURE } from "../../config/defaultThinkingSignature.ts"; // Prefix for Claude OAuth tool names to avoid conflicts -const CLAUDE_OAUTH_TOOL_PREFIX = "proxy_"; +// Can be disabled per-request via body._disableToolPrefix = true +export const CLAUDE_OAUTH_TOOL_PREFIX = "proxy_"; const CLAUDE_TOOL_CHOICE_REQUIRED = "an" + "y"; type ClaudeContentBlock = Record; @@ -27,6 +28,9 @@ type ClaudeTool = { // Convert OpenAI request to Claude format export function openaiToClaudeRequest(model, body, stream) { + // Check if tool prefix should be disabled (configured per-provider or global) + const disableToolPrefix = body?._disableToolPrefix === true; + // Tool name mapping for Claude OAuth (capitalizedName → originalName) const toolNameMap = new Map(); const result: { @@ -82,7 +86,7 @@ export function openaiToClaudeRequest(model, body, stream) { for (const msg of nonSystemMessages) { const newRole = msg.role === "user" || msg.role === "tool" ? "user" : "assistant"; - const blocks = getContentBlocksFromMessage(msg, toolNameMap); + const blocks = getContentBlocksFromMessage(msg, toolNameMap, disableToolPrefix); const hasToolUse = blocks.some((b) => b.type === "tool_use"); const hasToolResult = blocks.some((b) => b.type === "tool_result"); @@ -155,10 +159,13 @@ export function openaiToClaudeRequest(model, body, stream) { const originalName = toolData.name; // Claude OAuth requires prefixed tool names to avoid conflicts - const toolName = CLAUDE_OAUTH_TOOL_PREFIX + originalName; + // When prefix is disabled (non-Claude backends), use original name + const toolName = disableToolPrefix ? originalName : CLAUDE_OAUTH_TOOL_PREFIX + originalName; // Store mapping for response translation (prefixed → original) - toolNameMap.set(toolName, originalName); + if (!disableToolPrefix) { + toolNameMap.set(toolName, originalName); + } return { name: toolName, @@ -196,7 +203,7 @@ export function openaiToClaudeRequest(model, body, stream) { } // Get content blocks from single message -function getContentBlocksFromMessage(msg, toolNameMap = new Map()) { +function getContentBlocksFromMessage(msg, toolNameMap = new Map(), disableToolPrefix = false) { const blocks = []; if (msg.role === "tool") { @@ -277,8 +284,8 @@ function getContentBlocksFromMessage(msg, toolNameMap = new Map()) { const fnName = tc.function?.name; if (!fnName || !fnName.trim()) continue; - // Apply prefix to tool name - const toolName = CLAUDE_OAUTH_TOOL_PREFIX + fnName; + // Apply prefix to tool name (skip if disabled) + const toolName = disableToolPrefix ? fnName : CLAUDE_OAUTH_TOOL_PREFIX + fnName; blocks.push({ type: "tool_use", id: tc.id, diff --git a/open-sse/translator/response/openai-to-claude.ts b/open-sse/translator/response/openai-to-claude.ts index 82bbab4987..3d4afd154d 100644 --- a/open-sse/translator/response/openai-to-claude.ts +++ b/open-sse/translator/response/openai-to-claude.ts @@ -1,8 +1,6 @@ import { register } from "../registry.ts"; import { FORMATS } from "../formats.ts"; - -// Prefix for Claude OAuth tool names (must match request translator) -const CLAUDE_OAUTH_TOOL_PREFIX = "proxy_"; +import { CLAUDE_OAUTH_TOOL_PREFIX } from "../request/openai-to-claude.ts"; // Helper: stop thinking block if started function stopThinkingBlock(state, results) {