mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
fix(translator): ESM-safe registry + tool-id pairing + responses max_tokens; add real-creds tests
- translator/index.js: replace require() with static side-effect imports (ESM-safe), lazy-init registry maps to survive circular import order - openai-responses->openai: map max_output_tokens -> max_tokens (avoid leaking field upstream) - gemini/antigravity -> openai: derive deterministic tool_call id from name so functionCall/functionResponse pair correctly (fixes provider tool-pairing 400s) - add offline unit tests (finish-reason, usage, session-manager, ollama malformed args, const guard) - add real-creds integration tests (provider-cases + all-formats matrix: 6 inbound formats x 4 scenarios) Includes co-located provider registry refactor (pricing/capabilities/media providers) and sessionManager updates. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,7 +3,7 @@ import { DEFAULT_THINKING_CLAUDE_SIGNATURE } from "../../config/defaultThinkingS
|
||||
import { ROLE, CLAUDE_BLOCK } from "../schema/index.js";
|
||||
import { adjustMaxTokens } from "./maxTokens.js";
|
||||
import { applyCloaking } from "../../utils/claudeCloaking.js";
|
||||
import { deriveSessionId } from "../../utils/sessionManager.js";
|
||||
import { resolveSessionId } from "../../utils/sessionManager.js";
|
||||
import { PROVIDERS } from "../../providers/index.js";
|
||||
|
||||
// Check if message has valid non-empty content
|
||||
@@ -252,7 +252,7 @@ export function prepareClaudeRequest(body, provider = null, apiKey = null, conne
|
||||
// Apply cloaking for OAuth tokens (billing header + fake user ID)
|
||||
// session_id in user_id must match X-Claude-Code-Session-Id for fingerprint consistency
|
||||
if ((provider === "claude" || provider?.startsWith("anthropic-compatible")) && apiKey) {
|
||||
const sessionId = connectionId ? deriveSessionId(connectionId) : null;
|
||||
const sessionId = resolveSessionId({ body, connectionId, scope: "claude" });
|
||||
body = applyCloaking(body, apiKey, sessionId);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,15 +7,16 @@ import { normalizeThinkingConfig } from "../services/provider.js";
|
||||
import { AntigravityExecutor } from "../executors/antigravity.js";
|
||||
import { PROVIDERS } from "../providers/index.js";
|
||||
|
||||
// Registry for translators
|
||||
const requestRegistry = new Map();
|
||||
const responseRegistry = new Map();
|
||||
|
||||
// Track initialization state
|
||||
let initialized = false;
|
||||
// Registry for translators. Lazy-init guards against circular-import order:
|
||||
// translator modules call register() (side-effect) before this module's body runs.
|
||||
// var (not let): hoisted as undefined so register() can run during circular import (no TDZ).
|
||||
var requestRegistry;
|
||||
var responseRegistry;
|
||||
|
||||
// Register translator
|
||||
export function register(from, to, requestFn, responseFn) {
|
||||
requestRegistry ??= new Map();
|
||||
responseRegistry ??= new Map();
|
||||
const key = `${from}:${to}`;
|
||||
if (requestFn) {
|
||||
requestRegistry.set(key, requestFn);
|
||||
@@ -25,35 +26,8 @@ export function register(from, to, requestFn, responseFn) {
|
||||
}
|
||||
}
|
||||
|
||||
// Lazy load translators (called once on first use)
|
||||
function ensureInitialized() {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
|
||||
// Request translators - sync require pattern for bundler
|
||||
require("./request/claude-to-openai.js");
|
||||
require("./request/openai-to-claude.js");
|
||||
require("./request/gemini-to-openai.js");
|
||||
require("./request/openai-to-gemini.js");
|
||||
require("./request/openai-to-vertex.js");
|
||||
require("./request/antigravity-to-openai.js");
|
||||
require("./request/openai-responses.js");
|
||||
require("./request/openai-to-kiro.js");
|
||||
require("./request/openai-to-cursor.js");
|
||||
require("./request/openai-to-ollama.js");
|
||||
require("./request/openai-to-commandcode.js");
|
||||
|
||||
// Response translators
|
||||
require("./response/claude-to-openai.js");
|
||||
require("./response/openai-to-claude.js");
|
||||
require("./response/gemini-to-openai.js");
|
||||
require("./response/openai-to-antigravity.js");
|
||||
require("./response/openai-responses.js");
|
||||
require("./response/kiro-to-openai.js");
|
||||
require("./response/cursor-to-openai.js");
|
||||
require("./response/ollama-to-openai.js");
|
||||
require("./response/commandcode-to-openai.js");
|
||||
}
|
||||
// No-op: translators self-register via the static imports at the bottom of this file.
|
||||
function ensureInitialized() {}
|
||||
|
||||
// Strip specific content types from messages (explicit opt-in via strip[] in PROVIDER_MODELS)
|
||||
function stripContentTypes(body, stripList = []) {
|
||||
@@ -246,7 +220,29 @@ export function initState(sourceFormat) {
|
||||
return base;
|
||||
}
|
||||
|
||||
// Initialize all translators (kept for backward compatibility)
|
||||
// Kept for backward compatibility; translators are already registered at import time.
|
||||
export function initTranslators() {
|
||||
ensureInitialized();
|
||||
}
|
||||
|
||||
// Static side-effect imports: each module calls register() at load (works in ESM + bundler).
|
||||
import "./request/claude-to-openai.js";
|
||||
import "./request/openai-to-claude.js";
|
||||
import "./request/gemini-to-openai.js";
|
||||
import "./request/openai-to-gemini.js";
|
||||
import "./request/openai-to-vertex.js";
|
||||
import "./request/antigravity-to-openai.js";
|
||||
import "./request/openai-responses.js";
|
||||
import "./request/openai-to-kiro.js";
|
||||
import "./request/openai-to-cursor.js";
|
||||
import "./request/openai-to-ollama.js";
|
||||
import "./request/openai-to-commandcode.js";
|
||||
import "./response/claude-to-openai.js";
|
||||
import "./response/openai-to-claude.js";
|
||||
import "./response/gemini-to-openai.js";
|
||||
import "./response/openai-to-antigravity.js";
|
||||
import "./response/openai-responses.js";
|
||||
import "./response/kiro-to-openai.js";
|
||||
import "./response/cursor-to-openai.js";
|
||||
import "./response/ollama-to-openai.js";
|
||||
import "./response/commandcode-to-openai.js";
|
||||
|
||||
@@ -160,7 +160,8 @@ function convertContent(content) {
|
||||
// Function call
|
||||
if (part.functionCall) {
|
||||
toolCalls.push({
|
||||
id: part.functionCall.id || `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
||||
// Deterministic id from name so the matching functionResponse pairs correctly.
|
||||
id: part.functionCall.id || `call_${part.functionCall.name}`,
|
||||
type: OPENAI_BLOCK.FUNCTION,
|
||||
function: {
|
||||
name: part.functionCall.name,
|
||||
@@ -173,7 +174,7 @@ function convertContent(content) {
|
||||
if (part.functionResponse) {
|
||||
toolResults.push({
|
||||
role: ROLE.TOOL,
|
||||
tool_call_id: part.functionResponse.id || part.functionResponse.name,
|
||||
tool_call_id: part.functionResponse.id || `call_${part.functionResponse.name}`,
|
||||
content: JSON.stringify(part.functionResponse.response?.result || part.functionResponse.response || {})
|
||||
});
|
||||
}
|
||||
|
||||
@@ -97,8 +97,10 @@ function convertGeminiContent(content) {
|
||||
}
|
||||
|
||||
if (part.functionCall) {
|
||||
// Gemini lacks a native call id; derive a deterministic one from the name so the
|
||||
// matching functionResponse maps to the same tool_call_id (providers require pairing).
|
||||
toolCalls.push({
|
||||
id: `call_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
|
||||
id: part.functionCall.id || `call_${part.functionCall.name}`,
|
||||
type: OPENAI_BLOCK.FUNCTION,
|
||||
function: {
|
||||
name: part.functionCall.name,
|
||||
@@ -110,7 +112,7 @@ function convertGeminiContent(content) {
|
||||
if (part.functionResponse) {
|
||||
return {
|
||||
role: ROLE.TOOL,
|
||||
tool_call_id: part.functionResponse.id || part.functionResponse.name,
|
||||
tool_call_id: part.functionResponse.id || `call_${part.functionResponse.name}`,
|
||||
content: JSON.stringify(part.functionResponse.response?.result || part.functionResponse.response || {})
|
||||
};
|
||||
}
|
||||
|
||||
@@ -177,6 +177,12 @@ export function openaiResponsesToOpenAIRequest(model, body, stream, credentials)
|
||||
}
|
||||
|
||||
// Cleanup Responses API specific fields
|
||||
// Map Responses-only max_output_tokens to Chat max_tokens (avoid leaking unknown field upstream)
|
||||
if (result.max_output_tokens !== undefined) {
|
||||
if (result.max_tokens === undefined) result.max_tokens = result.max_output_tokens;
|
||||
delete result.max_output_tokens;
|
||||
}
|
||||
|
||||
delete result.input;
|
||||
delete result.instructions;
|
||||
delete result.include;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { register } from "../index.js";
|
||||
import { FORMATS } from "../formats.js";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { resolveSessionId } from "../../utils/sessionManager.js";
|
||||
import {
|
||||
resolveKiroModel,
|
||||
isThinkingEnabled,
|
||||
@@ -546,7 +547,7 @@ export function openaiToKiroRequest(model, body, stream, credentials) {
|
||||
const payload = {
|
||||
conversationState: {
|
||||
chatTriggerType: "MANUAL",
|
||||
conversationId: uuidv4(),
|
||||
conversationId: resolveSessionId({ headers: credentials?.rawHeaders, body, connectionId: credentials?.connectionId, scope: "kiro" }),
|
||||
currentMessage: {
|
||||
userInputMessage: {
|
||||
content: finalContent,
|
||||
|
||||
Reference in New Issue
Block a user