Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenmux/rosetta-ai",
"version": "1.0.15",
"version": "1.0.16",
"description": "Universal translator between AI provider protocols",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
90 changes: 89 additions & 1 deletion src/responses/__tests__/chat-completions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ describe("ResponsesToChatCompletionConverter", () => {
{
type: "function",
function: {
name: "agents_spawn_agent",
name: "agents___spawn_agent",
description: "Spawn a child agent.",
strict: false,
parameters: {
Expand Down Expand Up @@ -486,6 +486,70 @@ describe("ResponsesToChatCompletionConverter", () => {
expect(result.status).toBe("completed");
});

it("splits a namespaced function_call name into { namespace, name }", () => {
const result = converter.convertResponse(
makeCCResponse({
choices: [
{
index: 0,
message: {
role: "assistant",
content: null,
refusal: null,
tool_calls: [
{
id: "call_1",
type: "function",
function: { name: "agents___spawn_agent", arguments: "{}" },
},
],
},
finish_reason: "tool_calls",
logprobs: null,
},
],
})
);

const fcOutput = result.output.find((o: any) => o.type === "function_call") as any;
expect(fcOutput.name).toBe("spawn_agent");
expect(fcOutput.namespace).toBe("agents");
});

it("echoes namespace tools back as nested namespace tools", () => {
const result = converter.convertResponse(makeCCResponse(), {
model: "gpt-4o",
input: "Hi",
tools: [
{
type: "namespace",
name: "agents",
description: "Multi-agent collaboration tools.",
tools: [
{
type: "function",
name: "spawn_agent",
description: "Spawn a child agent.",
strict: false,
parameters: {
type: "object",
properties: { task_name: { type: "string" } },
required: ["task_name"],
additionalProperties: false,
},
},
],
},
] as any,
});

const tools = result.tools as any[];
expect(tools).toHaveLength(1);
expect(tools[0].type).toBe("namespace");
expect(tools[0].name).toBe("agents");
expect(tools[0].tools[0]).toMatchObject({ type: "function", name: "spawn_agent" });
});

it("maps finish_reason length to incomplete status", () => {
const result = converter.convertResponse(
makeCCResponse({
Expand Down Expand Up @@ -737,6 +801,30 @@ describe("ResponsesToChatCompletionConverter", () => {
expect(itemAdded.item.name).toBe("get_weather");
});

it("splits a namespaced function_call name in streaming events", () => {
const c = new ResponsesToChatCompletionConverter();
c.convertStreamChunk(makeChunk({ delta: { role: "assistant" } }));

const events = c.convertStreamChunk(
makeChunk({
delta: {
tool_calls: [
{
index: 0,
id: "call_1",
type: "function",
function: { name: "agents___spawn_agent", arguments: "" },
},
],
},
})
);

const itemAdded = events.find(e => e.type === "response.output_item.added") as any;
expect(itemAdded.item.name).toBe("spawn_agent");
expect(itemAdded.item.namespace).toBe("agents");
});

it("emits function_call_arguments.delta for tool arguments", () => {
const c = new ResponsesToChatCompletionConverter();
c.convertStreamChunk(makeChunk({ delta: { role: "assistant" } }));
Expand Down
30 changes: 29 additions & 1 deletion src/responses/__tests__/gemini.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ describe("ResponsesToGeminiConverter", () => {

const tools = result.config?.tools as any[];
expect(tools[0].functionDeclarations[0]).toEqual({
name: "agents_spawn_agent",
name: "agents___spawn_agent",
description: "Spawn a child agent.",
parametersJsonSchema: {
type: "object",
Expand Down Expand Up @@ -312,6 +312,34 @@ describe("ResponsesToGeminiConverter", () => {
expect(result.status).toBe("completed");
});

it("splits a namespaced functionCall name into { namespace, name }", () => {
const result = converter.convertResponse(
makeResponse({
candidates: [
{
content: {
role: "model",
parts: [
{
functionCall: {
id: "call_1",
name: "agents___spawn_agent",
args: { task_name: "child" },
},
},
],
},
finishReason: "STOP",
} as any,
],
})
);

const fcOutput = result.output.find((o: any) => o.type === "function_call") as any;
expect(fcOutput.name).toBe("spawn_agent");
expect(fcOutput.namespace).toBe("agents");
});

it("converts thought parts to reasoning output", () => {
const result = converter.convertResponse(
makeResponse({
Expand Down
23 changes: 22 additions & 1 deletion src/responses/__tests__/messages.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ describe("ResponsesToMessagesConverter", () => {
});

expect(result.tools![0]).toEqual({
name: "agents_spawn_agent",
name: "agents___spawn_agent",
description: "Spawn a child agent.",
input_schema: {
type: "object",
Expand Down Expand Up @@ -585,6 +585,27 @@ describe("ResponsesToMessagesConverter", () => {
expect(result.status).toBe("completed");
});

it("splits a namespaced tool_use name into { namespace, name }", () => {
const result = converter.convertResponse(
makeMessage({
content: [
{
type: "tool_use",
id: "tu_1",
name: "agents___spawn_agent",
input: { task_name: "child" },
caller: { type: "direct" },
} as any,
],
stop_reason: "tool_use",
})
);

const fcOutput = result.output.find((o: any) => o.type === "function_call") as any;
expect(fcOutput.name).toBe("spawn_agent");
expect(fcOutput.namespace).toBe("agents");
});

it("converts thinking blocks to reasoning output", () => {
const result = converter.convertResponse(
makeMessage({
Expand Down
27 changes: 22 additions & 5 deletions src/responses/chat-completions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type OpenAI from "openai";
import { expandNamespaceTools } from "./utils";
import { expandNamespaceTools, denamespaceResponse, denamespaceStreamEvents } from "./utils";

type RespResponse = OpenAI.Responses.Response;
type RespStreamEvent = OpenAI.Responses.ResponseStreamEvent;
Expand Down Expand Up @@ -265,7 +265,7 @@ export class ResponsesToChatCompletionConverter {
// that does not round-trip the request.
const echoed = this.echoRequestFields(params);

return {
const respResult = {
id: response.id,
object: "response",
created_at: response.created,
Expand Down Expand Up @@ -323,6 +323,11 @@ export class ResponsesToChatCompletionConverter {
}
: undefined,
} as unknown as RespResponse;

// Reverse the request-side namespace flattening: split namespaced
// function_call names into { namespace, name } and re-nest echoed tools.
denamespaceResponse(respResult);
return respResult;
}

/**
Expand Down Expand Up @@ -362,7 +367,7 @@ export class ResponsesToChatCompletionConverter {
};
}

const tools = (expandNamespaceTools(params.tools) ?? [])
const tools = (params.tools ?? [])
.map(t => this.toRespTool(t))
.filter(Boolean) as RespResponse["tools"];

Expand Down Expand Up @@ -390,6 +395,18 @@ export class ResponsesToChatCompletionConverter {
*/
private toRespTool(tool: OpenAI.Responses.Tool): RespResponse["tools"][number] | undefined {
const t = tool as any;
if (t.type === "namespace") {
// Echo the client's namespace tool back in its original nested shape,
// mapping each inner tool through the same function-tool normalization.
return {
type: "namespace",
name: t.name,
description: t.description,
tools: (Array.isArray(t.tools) ? t.tools : [])
.map((inner: any) => this.toRespTool(inner))
.filter(Boolean),
} as any;
}
if (t.type === "function") {
return {
type: "function",
Expand Down Expand Up @@ -482,7 +499,7 @@ export class ResponsesToChatCompletionConverter {
if (chunk.usage) {
events.push(...this.emitCompleted(chunk.usage));
}
return events;
return denamespaceStreamEvents(events);
}

const delta = choice.delta;
Expand Down Expand Up @@ -718,7 +735,7 @@ export class ResponsesToChatCompletionConverter {
}
}

return events;
return denamespaceStreamEvents(events);
}

/** Open a message output item and its first content part. */
Expand Down
10 changes: 7 additions & 3 deletions src/responses/gemini.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import type {
FunctionCallingConfigMode,
FinishReason,
} from "@google/genai";
import { expandNamespaceTools } from "./utils";
import { expandNamespaceTools, denamespaceResponse, denamespaceStreamEvents } from "./utils";

type RespResponse = OpenAI.Responses.Response;
type RespStreamEvent = OpenAI.Responses.ResponseStreamEvent;
Expand Down Expand Up @@ -173,7 +173,7 @@ export class ResponsesToGeminiConverter {
const thoughtsTokens = usage?.thoughtsTokenCount ?? 0;
const candidatesTokens = (usage?.candidatesTokenCount ?? 0) + thoughtsTokens;

return {
const respResult = {
id: response.responseId ?? `resp_${this.generateId()}`,
object: "response",
created_at: Math.floor(Date.now() / 1000),
Expand Down Expand Up @@ -207,6 +207,10 @@ export class ResponsesToGeminiConverter {
},
},
} as unknown as RespResponse;

// Split namespaced function_call names into { namespace, name }.
denamespaceResponse(respResult);
return respResult;
}

// --- Stream conversion (Gemini → Responses, backward) ---
Expand Down Expand Up @@ -399,7 +403,7 @@ export class ResponsesToGeminiConverter {
}
}

return events;
return denamespaceStreamEvents(events);
}

// --- Private: request helpers ---
Expand Down
10 changes: 7 additions & 3 deletions src/responses/messages.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type OpenAI from "openai";
import type Anthropic from "@anthropic-ai/sdk";
import { APIError } from "@anthropic-ai/sdk";
import { expandNamespaceTools } from "./utils";
import { expandNamespaceTools, denamespaceResponse, denamespaceStreamEvents } from "./utils";

type RespResponse = OpenAI.Responses.Response;
type RespStreamEvent = OpenAI.Responses.ResponseStreamEvent;
Expand Down Expand Up @@ -252,7 +252,7 @@ export class ResponsesToMessagesConverter {
const totalInputTokens =
usage.input_tokens + cacheRead + (usage.cache_creation_input_tokens ?? 0);

return {
const respResult = {
id: message.id,
object: "response",
created_at: Math.floor(Date.now() / 1000),
Expand Down Expand Up @@ -290,6 +290,10 @@ export class ResponsesToMessagesConverter {
},
},
} as unknown as RespResponse;

// Split namespaced function_call names into { namespace, name }.
denamespaceResponse(respResult);
return respResult;
}

// --- Stream conversion (Messages → Responses, backward) ---
Expand Down Expand Up @@ -812,7 +816,7 @@ export class ResponsesToMessagesConverter {
}
}

return events;
return denamespaceStreamEvents(events);
}

// --- Private: request helpers ---
Expand Down
Loading
Loading