fix(nvidia): normalize tool names and call IDs (#9236)

Validated in local merge-train (devbox-vm-06-dev002) @ combined-tip (FAST gates — only pre-existing audit.test.ts flake).
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-08-05 22:41:06 -03:00
committed by GitHub
parent b8d2478333
commit 3a1e42d985
13 changed files with 651 additions and 67 deletions

View File

@@ -0,0 +1 @@
- **fix(nvidia):** normalize tool names and call ids for NVIDIA compatibility. (thanks @minhnhat166)

View File

@@ -8,6 +8,7 @@ export const nvidiaProvider: RegistryEntry = {
baseUrl: "https://integrate.api.nvidia.com/v1/chat/completions",
authType: "apikey",
authHeader: "bearer",
toolNameMaxLength: 64,
// #6773: nvidia multiplexes 17 models from 9 different upstream vendors
// (z-ai/, minimaxai/, deepseek-ai/, qwen/, mistralai/, stepfun-ai/,
// moonshotai/, openai/, nvidia/) behind ONE connection — mark it passthrough

View File

@@ -141,6 +141,8 @@ export interface RegistryEntry {
passthroughModels?: boolean;
/** Default context window for all models in this provider (can be overridden per-model) */
defaultContextLength?: number;
/** Maximum OpenAI-compatible function name length accepted by this provider. */
toolNameMaxLength?: number;
/** Optional session pool config for rate limit management */
poolConfig?: Record<string, unknown>;
/**

View File

@@ -1,3 +1,5 @@
import { createHash } from "node:crypto";
import { BaseExecutor, type ExecuteInput } from "./base.ts";
import { mapNvidiaGlm52ReasoningParams } from "./base/reasoningEffort.ts";
import { PROVIDERS, OAUTH_ENDPOINTS } from "../config/constants.ts";
@@ -18,6 +20,7 @@ import {
import { isOfficialAnthropicBaseUrl } from "../utils/anthropicHost.ts";
import { applyProviderRequestDefaults } from "../services/providerRequestDefaults.ts";
import { stripUnsupportedParams } from "../translator/paramSupport.ts";
import { normalizeOpenAIToolNames } from "../translator/helpers/toolCallHelper.ts";
import {
injectReasoningContentForThinkingModel,
shouldInjectReasoningContentPlaceholder,
@@ -63,6 +66,38 @@ import { usesCcWireImage } from "../services/ccWireImageBuiltins.ts";
import type { PoolConfig } from "../services/sessionPool/types.ts";
const NVIDIA_TOOL_CALL_ID_PATTERN = /^[A-Za-z0-9]{9}$/;
function normalizeNvidiaToolCallId(id: unknown): unknown {
if (id === null || id === undefined) return id;
const value = String(id);
if (NVIDIA_TOOL_CALL_ID_PATTERN.test(value)) return value;
return createHash("sha256").update(value).digest("hex").slice(0, 9);
}
function normalizeNvidiaToolCallIds(body: unknown): void {
if (!body || typeof body !== "object" || Array.isArray(body)) return;
const messages = (body as Record<string, unknown>).messages;
if (!Array.isArray(messages)) return;
for (const message of messages) {
if (!message || typeof message !== "object" || Array.isArray(message)) continue;
const record = message as Record<string, unknown>;
if (Array.isArray(record.tool_calls)) {
for (const toolCall of record.tool_calls) {
if (!toolCall || typeof toolCall !== "object" || Array.isArray(toolCall)) continue;
const call = toolCall as Record<string, unknown>;
if (call.id !== null && call.id !== undefined) {
call.id = normalizeNvidiaToolCallId(call.id);
}
}
}
if (record.tool_call_id !== null && record.tool_call_id !== undefined) {
record.tool_call_id = normalizeNvidiaToolCallId(record.tool_call_id);
}
}
}
/**
* Apply operator-configured per-provider custom headers onto an outgoing header
* map. Defense-in-depth on top of the Zod `customHeadersSchema`:
@@ -638,6 +673,10 @@ export class DefaultExecutor extends BaseExecutor {
withDefaults = this.applyJsonSchemaFallback(withDefaults);
withDefaults = this.defaultResponsesTextFormat(withDefaults);
if (this.provider === "nvidia") {
normalizeNvidiaToolCallIds(withDefaults);
}
// Port of decolua/9router commit d652300e:
// Cerebras returns 400 (wrong_api_format), Mistral returns 422
// (extra_forbidden), and NVIDIA's OpenAI-compatible wrapper returns 400
@@ -823,6 +862,34 @@ export class DefaultExecutor extends BaseExecutor {
}
}
const toolNameMaxLength = getRegistryEntry(this.provider)?.toolNameMaxLength;
if (
toolNameMaxLength &&
withDefaults &&
typeof withDefaults === "object" &&
!Array.isArray(withDefaults)
) {
const toolNameMap = normalizeOpenAIToolNames(withDefaults, toolNameMaxLength);
if (toolNameMap.size > 0) {
const existingToolNameMap =
(withDefaults as Record<string, unknown>)._toolNameMap instanceof Map
? ((withDefaults as Record<string, unknown>)._toolNameMap as Map<string, string>)
: null;
const responseToolNameMap = existingToolNameMap
? new Map(existingToolNameMap)
: new Map<string, string>();
for (const [alias, original] of toolNameMap) {
responseToolNameMap.set(alias, original);
}
Object.defineProperty(withDefaults, "_toolNameMap", {
value: responseToolNameMap,
enumerable: false,
configurable: true,
writable: true,
});
}
}
return withDefaults;
}

View File

@@ -222,8 +222,8 @@ import { recordCost } from "@/domain/costRules";
import { calculateCost } from "@/lib/usage/costCalculator";
import {
buildClaudePassthroughToolNameMap,
restoreClaudePassthroughToolNames,
mergeResponseToolNameMap,
normalizeOpenAIToolFinishReasons,
restoreNonStreamingToolNames,
} from "./chatCore/passthroughToolNames.ts";
import { resolveCompressionSettings } from "./chatCore/compressionSettings.ts";
import { isCompressionExcluded } from "../services/compression/exclusions.ts";
@@ -4195,14 +4195,14 @@ export async function handleChatCore({
}
}
const responseToolNameMap = mergeResponseToolNameMap(
const restoreClaudeNames = sourceFormat === FORMATS.CLAUDE && targetFormat === FORMATS.CLAUDE;
let responseToolNameMap: Map<string, string> | null;
[responseBody, responseToolNameMap] = restoreNonStreamingToolNames(
responseBody,
toolNameMap,
(finalBody as Record<string, unknown> | null | undefined) ?? null
finalBody,
restoreClaudeNames
);
if (sourceFormat === FORMATS.CLAUDE && targetFormat === FORMATS.CLAUDE) {
responseBody = restoreClaudePassthroughToolNames(responseBody, responseToolNameMap);
}
reqLogger.logProviderResponse(
providerResponse.status,
providerResponse.statusText,
@@ -4298,17 +4298,7 @@ export async function handleChatCore({
}
// T18: Normalize finish_reason to 'tool_calls' if tool calls are present
if (translatedResponse?.choices) {
for (const choice of translatedResponse.choices) {
if (
choice.message?.tool_calls &&
choice.message.tool_calls.length > 0 &&
choice.finish_reason !== "tool_calls"
) {
choice.finish_reason = "tool_calls";
}
}
}
normalizeOpenAIToolFinishReasons(translatedResponse);
// Reasoning Replay Cache (#1628): Capture reasoning_content from non-streaming responses
// with tool_calls so it can be replayed on subsequent turns (DeepSeek V4, Kimi K2, etc.)

View File

@@ -1,6 +1,11 @@
import { CLAUDE_OAUTH_TOOL_PREFIX } from "../../translator/request/openai-to-claude.ts";
import { restoreOpenAIToolNames } from "../../translator/helpers/toolCallHelper.ts";
export function buildClaudePassthroughToolNameMap(body: Record<string, unknown> | null | undefined) {
type JsonRecord = Record<string, unknown>;
export function buildClaudePassthroughToolNameMap(
body: Record<string, unknown> | null | undefined
) {
if (!body || !Array.isArray(body.tools)) return null;
const toolNameMap = new Map<string, string>();
@@ -47,11 +52,15 @@ export function restoreClaudePassthroughToolNames(
export function mergeResponseToolNameMap(
baseToolNameMap: Map<string, string> | null,
transformedBody: Record<string, unknown> | null | undefined
transformedBody: unknown
) {
const transformedRecord =
transformedBody && typeof transformedBody === "object" && !Array.isArray(transformedBody)
? (transformedBody as JsonRecord)
: null;
const executorToolNameMap =
transformedBody && transformedBody._toolNameMap instanceof Map
? (transformedBody._toolNameMap as Map<string, string>)
transformedRecord?._toolNameMap instanceof Map
? (transformedRecord._toolNameMap as Map<string, string>)
: null;
if (!executorToolNameMap?.size) return baseToolNameMap;
@@ -63,3 +72,30 @@ export function mergeResponseToolNameMap(
}
return merged;
}
export function restoreNonStreamingToolNames(
responseBody: JsonRecord,
baseToolNameMap: Map<string, string> | null,
transformedBody: unknown,
restoreClaudeNames: boolean
): [JsonRecord, Map<string, string> | null] {
const responseToolNameMap = mergeResponseToolNameMap(baseToolNameMap, transformedBody);
const restoredBody = restoreClaudeNames
? restoreClaudePassthroughToolNames(responseBody, responseToolNameMap)
: responseBody;
restoreOpenAIToolNames(restoredBody, responseToolNameMap);
return [restoredBody, responseToolNameMap];
}
export function normalizeOpenAIToolFinishReasons(responseBody: unknown): void {
const response = responseBody as {
choices?: Array<JsonRecord & { message?: { tool_calls?: unknown[] } }>;
} | null;
if (!response?.choices) return;
for (const choice of response.choices) {
if (choice.message?.tool_calls?.length > 0 && choice.finish_reason !== "tool_calls") {
choice.finish_reason = "tool_calls";
}
}
}

View File

@@ -6,6 +6,7 @@ import {
import { normalizeOpenAICompatibleFinishReasonString } from "../utils/finishReason.ts";
import { containsTextualToolCallMarker } from "../utils/textualToolCall.ts";
import { getAnyReasoningValue } from "../utils/reasoningFields.ts";
import { restoreOpenAIToolNames } from "../translator/helpers/toolCallHelper.ts";
type JsonRecord = Record<string, unknown>;
@@ -137,11 +138,18 @@ export function translateNonStreamingResponse(
): unknown {
// If already in source format, return as-is
if (targetFormat === sourceFormat) {
if (targetFormat === FORMATS.OPENAI) {
restoreOpenAIToolNames(responseBody, toolNameMap);
}
return responseBody;
}
let intermediateOpenAI = responseBody;
if (targetFormat === FORMATS.OPENAI) {
restoreOpenAIToolNames(intermediateOpenAI, toolNameMap);
}
// Handle OpenAI Responses API format
if (targetFormat === FORMATS.OPENAI_RESPONSES) {
const responseRoot = toRecord(responseBody);

View File

@@ -1,7 +1,130 @@
import { createHash } from "node:crypto";
// Tool call helper functions for translator
const ALPHANUM9 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
type JsonRecord = Record<string, unknown>;
type ToolNameAliases = Map<string, string>;
interface ToolFunction extends JsonRecord {
name?: unknown;
arguments?: unknown;
}
interface ToolCallRecord extends JsonRecord {
id?: unknown;
type?: unknown;
function?: ToolFunction;
}
interface ToolContentBlock extends JsonRecord {
type?: unknown;
id?: unknown;
tool_use_id?: unknown;
}
interface ToolMessage extends JsonRecord {
role?: unknown;
tool_calls?: ToolCallRecord[];
tool_call_id?: unknown;
content?: unknown;
}
interface ToolCallBody extends JsonRecord {
messages?: ToolMessage[];
}
function toRecord(value: unknown): JsonRecord | null {
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : null;
}
function aliasOpenAIToolName(name: unknown, maxLength: number, aliases: ToolNameAliases): unknown {
if (typeof name !== "string" || name.length === 0) return name;
const safe = name.replace(/[^A-Za-z0-9_-]/g, "_");
if (safe === name && safe.length <= maxLength) return safe;
const hash = createHash("sha256").update(name).digest("hex").slice(0, 12);
const prefixLength = Math.max(0, maxLength - hash.length - 1);
const shortened =
prefixLength > 0 ? `${safe.slice(0, prefixLength)}_${hash}` : hash.slice(0, maxLength);
aliases.set(shortened, name);
return shortened;
}
/**
* Mutates an OpenAI-compatible request so every function name satisfies a
* provider's maximum length and `[A-Za-z0-9_-]` character constraints.
* Returns alias → original entries for response restoration.
*/
export function normalizeOpenAIToolNames(body: unknown, maxLength: number): ToolNameAliases {
const aliases: ToolNameAliases = new Map();
const root = toRecord(body);
if (!root || !Number.isInteger(maxLength) || maxLength < 1) return aliases;
const alias = (name: unknown): unknown => aliasOpenAIToolName(name, maxLength, aliases);
if (Array.isArray(root.tools)) {
for (const tool of root.tools) {
const fn = toRecord(toRecord(tool)?.function);
if (fn && typeof fn.name === "string") fn.name = alias(fn.name);
}
}
const toolChoiceFunction = toRecord(toRecord(root.tool_choice)?.function);
if (toolChoiceFunction && typeof toolChoiceFunction.name === "string") {
toolChoiceFunction.name = alias(toolChoiceFunction.name);
}
if (Array.isArray(root.messages)) {
for (const message of root.messages) {
const msg = toRecord(message);
if (!msg) continue;
if (Array.isArray(msg.tool_calls)) {
for (const toolCall of msg.tool_calls) {
const fn = toRecord(toRecord(toolCall)?.function);
if (fn && typeof fn.name === "string") fn.name = alias(fn.name);
}
}
if (msg.role === "tool" && typeof msg.name === "string") {
msg.name = alias(msg.name);
}
}
}
return aliases;
}
/** Restore normalized function names in OpenAI Chat Completions responses. */
export function restoreOpenAIToolNames(body: unknown, aliases: unknown): boolean {
if (!(aliases instanceof Map) || aliases.size === 0) return false;
const root = toRecord(body);
if (!root || !Array.isArray(root.choices)) return false;
let changed = false;
const restoreCalls = (calls: unknown): void => {
if (!Array.isArray(calls)) return;
for (const toolCall of calls) {
const fn = toRecord(toRecord(toolCall)?.function);
if (!fn || typeof fn.name !== "string") continue;
const original = aliases.get(fn.name);
if (typeof original !== "string" || original === fn.name) continue;
fn.name = original;
changed = true;
}
};
for (const choice of root.choices) {
const record = toRecord(choice);
if (!record) continue;
restoreCalls(toRecord(record.delta)?.tool_calls);
restoreCalls(toRecord(record.message)?.tool_calls);
}
return changed;
}
// Fallback streaming tool_call id when a provider response omits one (index optional).
// `call_<ts>` when no index is given; `call_<index>_<ts>` when an index is supplied.
export function fallbackToolCallId(index?: number): string {
@@ -23,7 +146,10 @@ function generateToolCallId9(): string {
}
/** @param options.use9CharId - When true, normalize ids to 9-char [a-zA-Z0-9] (e.g. Mistral); when false, only fix type/arguments, leave ids as-is */
export function ensureToolCallIds(body, options?: { use9CharId?: boolean }) {
export function ensureToolCallIds<T extends ToolCallBody>(
body: T,
options?: { use9CharId?: boolean }
): T {
if (!body.messages || !Array.isArray(body.messages)) return body;
const use9CharId = options?.use9CharId === true;
@@ -79,23 +205,23 @@ export function ensureToolCallIds(body, options?: { use9CharId?: boolean }) {
}
// Get tool_call ids from assistant message (OpenAI format: tool_calls, Claude format: tool_use in content)
export function getToolCallIds(msg) {
export function getToolCallIds(msg: ToolMessage): string[] {
if (msg.role !== "assistant") return [];
const ids = [];
const ids: string[] = [];
// OpenAI format: tool_calls array
if (msg.tool_calls && Array.isArray(msg.tool_calls)) {
for (const tc of msg.tool_calls) {
if (tc.id) ids.push(tc.id);
if (tc.id) ids.push(String(tc.id));
}
}
// Claude format: tool_use blocks in content
if (Array.isArray(msg.content)) {
for (const block of msg.content) {
for (const block of msg.content as ToolContentBlock[]) {
if (block.type === "tool_use" && block.id) {
ids.push(block.id);
ids.push(String(block.id));
}
}
}
@@ -104,18 +230,25 @@ export function getToolCallIds(msg) {
}
// Check if user message has tool_result for given ids (OpenAI format: role=tool, Claude format: tool_result in content)
export function hasToolResults(msg, toolCallIds) {
export function hasToolResults(
msg: ToolMessage | null | undefined,
toolCallIds: string[]
): boolean {
if (!msg || !toolCallIds.length) return false;
// OpenAI format: role = "tool" with tool_call_id
if (msg.role === "tool" && msg.tool_call_id) {
return toolCallIds.includes(msg.tool_call_id);
return toolCallIds.includes(String(msg.tool_call_id));
}
// Claude format: tool_result blocks in user message content
if (msg.role === "user" && Array.isArray(msg.content)) {
for (const block of msg.content) {
if (block.type === "tool_result" && toolCallIds.includes(block.tool_use_id)) {
for (const block of msg.content as ToolContentBlock[]) {
if (
block.type === "tool_result" &&
block.tool_use_id &&
toolCallIds.includes(String(block.tool_use_id))
) {
return true;
}
}
@@ -127,10 +260,10 @@ export function hasToolResults(msg, toolCallIds) {
// Fix missing tool responses - insert empty tool_result if assistant has tool_use but next message has no tool_result.
// Inserts in the same shape as the opening assistant message: OpenAI tool_calls → role:"tool";
// Claude tool_use blocks → role:"user" with tool_result content blocks.
export function fixMissingToolResponses(body) {
export function fixMissingToolResponses<T extends ToolCallBody>(body: T): T {
if (!body.messages || !Array.isArray(body.messages)) return body;
const newMessages = [];
const newMessages: ToolMessage[] = [];
for (let i = 0; i < body.messages.length; i++) {
const msg = body.messages[i];
@@ -179,7 +312,7 @@ export function fixMissingToolResponses(body) {
// role:"tool" messages and Claude-format tool_result content blocks. Drops a
// user message entirely if stripping empties its content array. Returns the
// same body reference when nothing needs to change (no-op fast path).
export function stripOrphanedToolResults(body) {
export function stripOrphanedToolResults<T extends ToolCallBody>(body: T): T {
if (!body.messages || !Array.isArray(body.messages)) return body;
const knownCallIds = new Set<string>();
@@ -190,11 +323,11 @@ export function stripOrphanedToolResults(body) {
}
let changed = false;
const filteredMessages = [];
const filteredMessages: ToolMessage[] = [];
for (const msg of body.messages) {
if (msg.role === "tool" && msg.tool_call_id) {
if (knownCallIds.has(msg.tool_call_id)) {
if (knownCallIds.has(String(msg.tool_call_id))) {
filteredMessages.push(msg);
} else {
changed = true;
@@ -203,7 +336,7 @@ export function stripOrphanedToolResults(body) {
}
if (Array.isArray(msg.content)) {
const cleanedContent = msg.content.filter((block) => {
const cleanedContent = (msg.content as ToolContentBlock[]).filter((block) => {
if (block?.type !== "tool_result") return true;
return typeof block.tool_use_id === "string" && knownCallIds.has(block.tool_use_id);
});

View File

@@ -284,6 +284,7 @@ export function openaiToClaudeResponse(chunk, state) {
// Strip the Claude OAuth prefix from an incoming tool name (if any).
const incomingName = (() => {
let n = tc.function?.name || "";
n = state.toolNameMap?.get(n) || n;
if (n.startsWith(CLAUDE_OAUTH_TOOL_PREFIX)) n = n.slice(CLAUDE_OAUTH_TOOL_PREFIX.length);
return n;
})();

View File

@@ -0,0 +1,33 @@
import { restoreOpenAIToolNames } from "../translator/helpers/toolCallHelper.ts";
type JsonRecord = Record<string, unknown>;
export function normalizeFinalOpenAIStreamChunk(
parsed: JsonRecord,
toolNameMap: unknown
): { changed: boolean; hasFinishReason: boolean } {
let changed = false;
if (parsed.id != null && typeof parsed.id !== "string") {
parsed.id = String(parsed.id);
changed = true;
}
if (Array.isArray(parsed.choices)) {
for (const choice of parsed.choices as JsonRecord[]) {
const delta = (choice as JsonRecord | null | undefined)?.delta as JsonRecord | undefined;
if (!Array.isArray(delta?.tool_calls)) continue;
for (const toolCall of delta.tool_calls as JsonRecord[]) {
if (toolCall?.id != null && typeof toolCall.id !== "string") {
toolCall.id = String(toolCall.id);
changed = true;
}
}
}
}
changed = restoreOpenAIToolNames(parsed, toolNameMap) || changed;
const firstChoice = Array.isArray(parsed.choices)
? (parsed.choices[0] as JsonRecord | undefined)
: undefined;
return { changed, hasFinishReason: Boolean(firstChoice?.finish_reason) };
}

View File

@@ -45,6 +45,7 @@ export type PassthroughTailProcessorContext = {
setPassthroughResponsesCurrentFunctionCallKey: (value: string | null) => void;
hasPassthroughToolCalls: () => boolean;
toResponsesCompletedWithToolCalls: (parsed: JsonRecord) => JsonRecord;
restoreOpenAIToolNames: (parsed: JsonRecord) => boolean;
};
function asRecord(value: unknown): JsonRecord {
@@ -290,7 +291,9 @@ export function processBufferedPassthroughLine(
if (isResponses) {
output = handleResponsesTailPayload(parsed, output, context);
} else if (!isClaude) {
const restoredToolName = context.restoreOpenAIToolNames(parsed);
handleOpenAiTailPayload(parsed, context);
if (restoredToolName) output = `data: ${JSON.stringify(parsed)}\n\n`;
}
context.pushClientPayload(parsed);

View File

@@ -64,6 +64,8 @@ import {
hasUnsupportedReasoningSignal,
} from "./reasoningFields.ts";
import { applyThinkTag, flushThink, initThinkState } from "./thinkTagParser.ts";
import { restoreOpenAIToolNames } from "../translator/helpers/toolCallHelper.ts";
import { normalizeFinalOpenAIStreamChunk } from "./openAIStreamChunk.ts";
/**
* Race a response body read against a timeout.
@@ -1735,6 +1737,7 @@ export function createSSEStream(options: StreamOptions = {}) {
continue;
}
const restoredOpenAIToolName = restoreOpenAIToolNames(parsed, toolNameMap);
const idFixed = hadNonStringTopLevelId ? false : fixInvalidId(parsed);
if (!hasValuableContent(parsed, FORMATS.OPENAI)) {
@@ -1923,7 +1926,8 @@ export function createSSEStream(options: StreamOptions = {}) {
needsReserialization ||
toolCallIdCoerced ||
hadNonStringToolCallId ||
hadNonStringTopLevelId
hadNonStringTopLevelId ||
restoredOpenAIToolName
) {
output = `data: ${JSON.stringify(parsed)}\n\n`;
injectedUsage = true;
@@ -2253,6 +2257,8 @@ export function createSSEStream(options: StreamOptions = {}) {
toResponsesCompletedWithToolCalls(parsed, [
...passthroughToolCalls.values(),
]) as JsonRecord,
restoreOpenAIToolNames: (parsed: JsonRecord) =>
restoreOpenAIToolNames(parsed, toolNameMap),
};
for (const line of normalizedTailLines) {
@@ -2300,36 +2306,12 @@ export function createSSEStream(options: StreamOptions = {}) {
output = `data: ${JSON.stringify(flushedParsed)}\n\n`;
}
} else if (!isClaude) {
let flushChanged = false;
const flushedHadNonStringTopLevelId =
flushedParsed?.id != null && typeof flushedParsed.id !== "string";
if (flushedHadNonStringTopLevelId) {
flushedParsed.id = String(flushedParsed.id);
flushChanged = true;
}
if (Array.isArray(flushedParsed.choices)) {
for (const choice of flushedParsed.choices as JsonRecord[]) {
const tcs = (choice as JsonRecord | undefined)?.delta as
JsonRecord | undefined;
if (Array.isArray(tcs?.tool_calls)) {
for (const tc of tcs.tool_calls as JsonRecord[]) {
if (tc?.id != null && typeof tc.id !== "string") {
tc.id = String(tc.id);
flushChanged = true;
}
}
}
}
}
const { changed: flushChanged, hasFinishReason } =
normalizeFinalOpenAIStreamChunk(flushedParsed, toolNameMap);
// #7800: track finish_reason in the flush path too, so a
// final chunk without trailing newline still suppresses the
// synthetic finish_reason synthesis.
if (
Array.isArray(flushedParsed.choices) &&
(flushedParsed.choices[0] as JsonRecord | undefined)?.finish_reason
) {
passthroughSawFinishReason = true;
}
if (hasFinishReason) passthroughSawFinishReason = true;
if (flushChanged) {
output = `data: ${JSON.stringify(flushedParsed)}\n\n`;
}

View File

@@ -0,0 +1,327 @@
import assert from "node:assert/strict";
import test from "node:test";
import { DefaultExecutor } from "../../open-sse/executors/default.ts";
import { getRegistryEntry } from "../../open-sse/config/providerRegistry.ts";
import { translateNonStreamingResponse } from "../../open-sse/handlers/responseTranslator.ts";
import {
normalizeOpenAIToolNames,
restoreOpenAIToolNames,
} from "../../open-sse/translator/helpers/toolCallHelper.ts";
import { FORMATS } from "../../open-sse/translator/formats.ts";
import { openaiToClaudeResponse } from "../../open-sse/translator/response/openai-to-claude.ts";
import { createPassthroughStreamWithLogger } from "../../open-sse/utils/stream.ts";
test("NVIDIA keeps tool calls and tool results linked with deterministic 9-character IDs", () => {
const body = {
messages: [
{
role: "assistant",
tool_calls: [
{
id: "6075034-0",
type: "function",
function: { name: "lookup", arguments: "{}" },
},
],
},
{ role: "tool", tool_call_id: "6075034-0", content: "ok" },
],
};
const first = new DefaultExecutor("nvidia").transformRequest(
"mistralai/mistral-medium-3.5-128b",
structuredClone(body),
false,
null
);
const second = new DefaultExecutor("nvidia").transformRequest(
"mistralai/mistral-medium-3.5-128b",
structuredClone(body),
false,
null
);
const callId = first.messages[0].tool_calls[0].id;
assert.match(callId, /^[A-Za-z0-9]{9}$/);
assert.equal(first.messages[1].tool_call_id, callId);
assert.equal(second.messages[0].tool_calls[0].id, callId);
const valid = new DefaultExecutor("nvidia").transformRequest(
"mistralai/mistral-medium-3.5-128b",
{
messages: [
{
role: "assistant",
tool_calls: [
{
id: "Abc123XyZ",
type: "function",
function: { name: "lookup", arguments: "{}" },
},
],
},
{ role: "tool", tool_call_id: "Abc123XyZ", content: "ok" },
],
},
false,
null
);
assert.equal(valid.messages[0].tool_calls[0].id, "Abc123XyZ");
assert.equal(valid.messages[1].tool_call_id, "Abc123XyZ");
});
test("NVIDIA aliases every OpenAI tool-name surface and can restore the provider response", () => {
const original = "mcp__plugin_chrome-devtools-mcp_chrome-devtools__get.console/message";
const body = {
tools: [{ type: "function", function: { name: original } }],
tool_choice: { type: "function", function: { name: original } },
messages: [
{
role: "assistant",
tool_calls: [{ type: "function", function: { name: original, arguments: "{}" } }],
},
{ role: "tool", name: original, tool_call_id: "valid1234", content: "ok" },
],
};
const aliases = normalizeOpenAIToolNames(body, 64);
const alias = body.tools[0].function.name;
assert.match(alias, /^[A-Za-z0-9_-]{1,64}$/);
assert.notEqual(alias, original);
assert.equal(body.tool_choice.function.name, alias);
assert.equal(body.messages[0].tool_calls[0].function.name, alias);
assert.equal(body.messages[1].name, alias);
const response = {
choices: [
{
message: {
tool_calls: [{ type: "function", function: { name: alias, arguments: "{}" } }],
},
},
],
};
assert.equal(restoreOpenAIToolNames(response, aliases), true);
assert.equal(response.choices[0].message.tool_calls[0].function.name, original);
const collisions = {
tools: ["a.b", "a?b"].map((name) => ({ type: "function", function: { name } })),
};
normalizeOpenAIToolNames(collisions, 64);
assert.notEqual(collisions.tools[0].function.name, collisions.tools[1].function.name);
});
test("NVIDIA executor applies its registry-scoped tool-name limit without changing OpenAI", () => {
const original = "mcp__plugin_chrome-devtools-mcp_chrome-devtools__get.console/message";
const request = {
tools: [{ type: "function", function: { name: original } }],
messages: [
{
role: "assistant",
tool_calls: [{ type: "function", function: { name: original, arguments: "{}" } }],
},
],
};
assert.equal(getRegistryEntry("nvidia")?.toolNameMaxLength, 64);
const nvidia = new DefaultExecutor("nvidia").transformRequest(
"mistralai/mistral-medium-3.5-128b",
structuredClone(request),
false,
null
);
const openai = new DefaultExecutor("openai").transformRequest(
"gpt-5.4",
structuredClone(request),
false,
null
);
const alias = nvidia.tools[0].function.name;
assert.match(alias, /^[A-Za-z0-9_-]{1,64}$/);
assert.equal(nvidia.messages[0].tool_calls[0].function.name, alias);
assert.equal(nvidia._toolNameMap.get(alias), original);
assert.equal(Object.prototype.propertyIsEnumerable.call(nvidia, "_toolNameMap"), false);
assert.equal(openai.tools[0].function.name, original);
assert.equal(openai._toolNameMap, undefined);
});
test("NVIDIA restores aliases in non-streaming OpenAI and OpenAI-to-Claude responses", () => {
const original = "mcp__plugin_chrome-devtools-mcp_chrome-devtools__get.console/message";
const request = { tools: [{ type: "function", function: { name: original } }] };
const aliases = normalizeOpenAIToolNames(request, 64);
const alias = request.tools[0].function.name;
const providerResponse = {
id: "chatcmpl_1",
object: "chat.completion",
choices: [
{
message: {
role: "assistant",
content: null,
tool_calls: [
{
id: "call_1",
type: "function",
function: { name: alias, arguments: "{}" },
},
],
},
finish_reason: "tool_calls",
},
],
};
const openai = structuredClone(providerResponse);
assert.equal(restoreOpenAIToolNames(openai, aliases), true);
assert.equal(openai.choices[0].message.tool_calls[0].function.name, original);
const claude = translateNonStreamingResponse(
structuredClone(providerResponse),
FORMATS.OPENAI,
FORMATS.CLAUDE,
aliases
);
const toolUse = claude.content.find((block) => block.type === "tool_use");
assert.equal(toolUse.name, original);
});
test("NVIDIA restores aliases in passthrough OpenAI SSE chunks", async () => {
const original = "mcp__plugin_chrome-devtools-mcp_chrome-devtools__get.console/message";
const request = { tools: [{ type: "function", function: { name: original } }] };
const aliases = normalizeOpenAIToolNames(request, 64);
const alias = request.tools[0].function.name;
const transform = createPassthroughStreamWithLogger("nvidia", null, aliases);
const writer = transform.writable.getWriter();
const reader = transform.readable.getReader();
const output = (async () => {
let result = "";
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) return result;
result += decoder.decode(value);
}
})();
await writer.write(
new TextEncoder().encode(
`data: ${JSON.stringify({
id: "chatcmpl_1",
object: "chat.completion.chunk",
choices: [
{
index: 0,
delta: {
tool_calls: [
{
index: 0,
id: "call_1",
type: "function",
function: { name: alias, arguments: "" },
},
],
},
finish_reason: null,
},
],
})}\n\n`
)
);
await writer.close();
const text = await output;
assert.match(text, new RegExp(original.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
assert.doesNotMatch(text, new RegExp(alias));
});
test("NVIDIA restores aliases before emitting translated OpenAI-to-Claude tool blocks", () => {
const original = "mcp__plugin_chrome-devtools-mcp_chrome-devtools__get.console/message";
const request = { tools: [{ type: "function", function: { name: original } }] };
const aliases = normalizeOpenAIToolNames(request, 64);
const alias = request.tools[0].function.name;
const translated = openaiToClaudeResponse(
{
id: "chatcmpl_1",
model: "mistralai/mistral-medium-3.5-128b",
choices: [
{
delta: {
tool_calls: [
{
index: 0,
id: "call_1",
type: "function",
function: { name: alias, arguments: "" },
},
],
},
finish_reason: null,
},
],
},
{
toolNameMap: aliases,
toolCalls: new Map(),
nextBlockIndex: 0,
textBlockIndex: -1,
thinkingBlockIndex: -1,
}
);
const block = translated.find((event) => event.type === "content_block_start");
assert.equal(block.content_block.name, original);
});
test("NVIDIA restores an alias in the final passthrough SSE chunk without a trailing newline", async () => {
const original = "mcp__plugin_chrome-devtools-mcp_chrome-devtools__get.console/message";
const request = { tools: [{ type: "function", function: { name: original } }] };
const aliases = normalizeOpenAIToolNames(request, 64);
const alias = request.tools[0].function.name;
const transform = createPassthroughStreamWithLogger("nvidia", null, aliases);
const writer = transform.writable.getWriter();
const reader = transform.readable.getReader();
const output = (async () => {
let result = "";
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) return result;
result += decoder.decode(value);
}
})();
await writer.write(
new TextEncoder().encode(
`data: ${JSON.stringify({
id: "chatcmpl_2",
object: "chat.completion.chunk",
choices: [
{
index: 0,
delta: {
tool_calls: [
{
index: 0,
id: "call_2",
type: "function",
function: { name: alias, arguments: "{}" },
},
],
},
finish_reason: "tool_calls",
},
],
})}`
)
);
await writer.close();
const text = await output;
assert.match(text, new RegExp(original.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")));
assert.doesNotMatch(text, new RegExp(alias));
});