feat: configurable tool name prefix (#199) and custom rpm/tpm rate limits (#198)

- Issue #199: proxy_ prefix on tool names is now automatically disabled
  when routing to non-Claude backends (Gemini, Compatible, etc.).
  Prevents tool name mismatches in OpenCode, Cursor, and other clients.

- Issue #198: Added customRpm/customTpm support per provider connection.
  Users can configure custom rate limits via connection settings,
  overriding the default auto-learned limits from response headers.
This commit is contained in:
diegosouzapw
2026-03-05 01:41:34 -03:00
parent baa0208fa9
commit 5a53c17e81
4 changed files with 52 additions and 14 deletions

View File

@@ -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;

View File

@@ -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)`
);
}

View File

@@ -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<string, unknown>;
@@ -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,

View File

@@ -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) {