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
10 changes: 7 additions & 3 deletions docs/field-conversions.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,9 +188,10 @@ Complete mapping of all fields across the 12 pairwise converters between OpenAI
| `choices[0].message.annotations` | `content[]` web_search_tool_result | |
| `choices[0].message.tool_calls` | `content[]` tool_use | |
| `finish_reason` | `stop_reason` | stop→end_turn, tool_calls→tool_use, length→max_tokens, content_filter→refusal |
| `usage.prompt_tokens` | `usage.input_tokens` | |
| `usage.prompt_tokens` - (`cached_tokens` + `cache_write_tokens`) | `usage.input_tokens` | |
| `usage.completion_tokens` | `usage.output_tokens` | |
| `prompt_tokens_details.cached_tokens` | `cache_read_input_tokens` | |
| `prompt_tokens_details.cache_write_tokens` | `cache_creation_input_tokens` | |
| `prompt_tokens_details.web_search` | `server_tool_use.web_search_requests` | |

Usage also re-emits enriched OpenAI-style fields (`prompt_tokens`, `completion_tokens`, `total_tokens`, `prompt_tokens_details`, `completion_tokens_details`, audio token fields); origin `*_details` objects are merged in to preserve extra fields. Streaming reuses the same `buildUsage`, so `message_delta` carries real `output_tokens` from the trailing usage chunk.
Expand Down Expand Up @@ -233,7 +234,10 @@ Usage also re-emits enriched OpenAI-style fields (`prompt_tokens`, `completion_t
| `output[]` function_call | `content[]` tool_use | |
| `output[]` message | `content[]` text | |
| `status` | `stop_reason` | completed→end_turn/tool_use, incomplete→max_tokens, failed→end_turn |
| `usage.*` | `usage.*` | cached_tokens→cache_read_input_tokens |
| `usage.input_tokens` - (`cached_tokens` + `cache_write_tokens`) | `usage.input_tokens` | |
| `usage.input_tokens_details.cached_tokens` | `usage.cache_read_input_tokens` | |
| `usage.input_tokens_details.cache_write_tokens` | `usage.cache_creation_input_tokens` | |
| Other `usage.*` | `usage.*` | Reasoning and enriched OpenAI-style fields preserved |

### Stream: text ✅ tool_calls ✅ thinking ✅ usage ✅

Expand Down Expand Up @@ -273,7 +277,7 @@ Usage also re-emits enriched OpenAI-style fields (`prompt_tokens`, `completion_t
| `candidates[0].content.parts[]` text | `content[]` text | |
| `candidates[0].content.parts[]` functionCall | `content[]` tool_use | |
| `candidates[0].finishReason` | `stop_reason` | STOP→end_turn, MAX_TOKENS→max_tokens, SAFETY→refusal |
| `usageMetadata.promptTokenCount` + `toolUsePromptTokenCount` | `usage.input_tokens` | Combined |
| `usageMetadata.promptTokenCount` + `toolUsePromptTokenCount` - `cachedContentTokenCount` | `usage.input_tokens` | Combined |
| `usageMetadata.candidatesTokenCount` + `thoughtsTokenCount` | `usage.output_tokens` | Combined |
| `usageMetadata.cachedContentTokenCount` | `cache_read_input_tokens` | |

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@zenmux/rosetta-ai",
"version": "1.0.16",
"version": "1.0.17",
"description": "Universal translator between AI provider protocols",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down Expand Up @@ -60,4 +60,4 @@
"typescript": "^6.0.2",
"typescript-eslint": "^8.58.2"
}
}
}
23 changes: 17 additions & 6 deletions src/messages/__tests__/chat-completions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -972,7 +972,7 @@ describe("MessagesToChatCompletionConverter", () => {
expect(converter.convertResponse(input).stop_reason).toBe("tool_use");
});

it("converts usage with cache details", () => {
it("separates cache read and write tokens from input_tokens", () => {
const input: OpenAI.ChatCompletion = {
id: "id",
object: "chat.completion",
Expand All @@ -990,13 +990,16 @@ describe("MessagesToChatCompletionConverter", () => {
prompt_tokens: 100,
completion_tokens: 50,
total_tokens: 150,
prompt_tokens_details: { cached_tokens: 30 },
},
prompt_tokens_details: { cached_tokens: 30, cache_write_tokens: 20 },
} as any,
};
const result = converter.convertResponse(input);
expect(result.usage.input_tokens).toBe(100);
expect(result.usage.input_tokens).toBe(50);
expect(result.usage.output_tokens).toBe(50);
expect(result.usage.cache_read_input_tokens).toBe(30);
expect(result.usage.cache_creation_input_tokens).toBe(20);
expect((result.usage as any).prompt_tokens).toBe(100);
expect((result.usage as any).total_tokens).toBe(150);
});

it("enriches usage with OpenAI-style fields", () => {
Expand Down Expand Up @@ -1030,6 +1033,7 @@ describe("MessagesToChatCompletionConverter", () => {
expect(usage.prompt_tokens_details).toEqual({ cached_tokens: 30, audio_tokens: 4 });
expect(usage.audio_input_tokens).toBe(4);
expect(usage.service_tier).toBe("standard");
expect(usage.input_tokens).toBe(70);
expect(usage.cache_creation_input_tokens).toBe(0);
});

Expand Down Expand Up @@ -1491,7 +1495,12 @@ describe("MessagesToChatCompletionConverter", () => {
created: 1700000000,
model: "gpt-4o",
choices: [],
usage: { prompt_tokens: 12, completion_tokens: 7, total_tokens: 19 },
usage: {
prompt_tokens: 12,
completion_tokens: 7,
total_tokens: 19,
prompt_tokens_details: { cached_tokens: 3, cache_write_tokens: 2 },
},
} as unknown as OpenAI.ChatCompletionChunk,
];

Expand All @@ -1502,7 +1511,9 @@ describe("MessagesToChatCompletionConverter", () => {

expect(msgDelta).toBeDefined();
expect(msgDelta.usage.output_tokens).toBe(7);
expect(msgDelta.usage.input_tokens).toBe(12);
expect(msgDelta.usage.input_tokens).toBe(7);
expect(msgDelta.usage.cache_read_input_tokens).toBe(3);
expect(msgDelta.usage.cache_creation_input_tokens).toBe(2);
expect((msgDelta.usage as any).completion_tokens).toBe(7);
expect((msgDelta.usage as any).prompt_tokens).toBe(12);
expect((msgDelta.usage as any).total_tokens).toBe(19);
Expand Down
6 changes: 6 additions & 0 deletions src/messages/__tests__/gemini.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,9 @@ describe("MessagesToGeminiConverter", () => {
expect(usage.prompt_tokens).toBe(110);
expect(usage.completion_tokens).toBe(70);
expect(usage.total_tokens).toBe(180);
expect(usage.input_tokens).toBe(80);
expect(usage.cache_read_input_tokens).toBe(30);
expect(usage.cache_creation_input_tokens).toBe(0);
expect(usage.prompt_tokens_details.cached_tokens).toBe(30);
expect(usage.completion_tokens_details.reasoning_tokens).toBe(20);
expect(usage.tool_use).toBe(10);
Expand Down Expand Up @@ -945,13 +948,16 @@ describe("MessagesToGeminiConverter", () => {
promptTokenCount: 10,
candidatesTokenCount: 5,
totalTokenCount: 15,
cachedContentTokenCount: 3,
} as any,
})
);

const msgDelta = events.find(e => e.type === "message_delta") as any;
expect(msgDelta).toBeDefined();
expect(msgDelta.delta.stop_reason).toBe("end_turn");
expect(msgDelta.usage.input_tokens).toBe(7);
expect(msgDelta.usage.cache_read_input_tokens).toBe(3);

const msgStop = events.find(e => e.type === "message_stop");
expect(msgStop).toBeDefined();
Expand Down
16 changes: 11 additions & 5 deletions src/messages/__tests__/responses.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,22 +341,25 @@ describe("MessagesToResponsesConverter", () => {
expect(result.stop_reason).toBe("max_tokens");
});

it("includes cache tokens in usage", () => {
it("separates cache read and write tokens from input_tokens", () => {
const result = converter.convertResponse(
makeResponse({
usage: {
input_tokens: 100,
output_tokens: 50,
total_tokens: 150,
input_tokens_details: { cached_tokens: 30 },
input_tokens_details: { cached_tokens: 30, cache_write_tokens: 20 },
output_tokens_details: { reasoning_tokens: 0 },
},
} as any,
})
);

expect(result.usage.input_tokens).toBe(100);
expect(result.usage.input_tokens).toBe(50);
expect(result.usage.output_tokens).toBe(50);
expect(result.usage.cache_read_input_tokens).toBe(30);
expect(result.usage.cache_creation_input_tokens).toBe(20);
expect((result.usage as any).prompt_tokens).toBe(100);
expect((result.usage as any).total_tokens).toBe(150);
});

it("counts only completed web search calls", () => {
Expand Down Expand Up @@ -651,7 +654,7 @@ describe("MessagesToResponsesConverter", () => {
input_tokens: 10,
output_tokens: 5,
total_tokens: 15,
input_tokens_details: { cached_tokens: 3 },
input_tokens_details: { cached_tokens: 3, cache_write_tokens: 2 },
output_tokens_details: { reasoning_tokens: 0 },
},
} as any,
Expand All @@ -661,7 +664,10 @@ describe("MessagesToResponsesConverter", () => {
const msgDelta = events.find(
e => e.type === "message_delta"
) as Anthropic.RawMessageDeltaEvent;
expect(msgDelta.usage.input_tokens).toBe(5);
expect(msgDelta.usage.output_tokens).toBe(5);
expect(msgDelta.usage.cache_read_input_tokens).toBe(3);
expect(msgDelta.usage.cache_creation_input_tokens).toBe(2);
});

it("emits tool_use stop_reason when function_call is in output", () => {
Expand Down
6 changes: 4 additions & 2 deletions src/messages/chat-completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,8 @@ export class MessagesToChatCompletionConverter {
const outputTokens = u.completion_tokens ?? 0;
const totalTokens = u.total_tokens ?? inputTokens + outputTokens;
const cachedTokens = u.prompt_tokens_details?.cached_tokens ?? 0;
const cacheWriteTokens = (u.prompt_tokens_details as any)?.cache_write_tokens ?? 0;
const uncachedInputTokens = Math.max(0, inputTokens - cachedTokens - cacheWriteTokens);
const reasoningTokens = u.completion_tokens_details?.reasoning_tokens ?? 0;
const webSearch: number = (u.prompt_tokens_details as any)?.web_search ?? 0;

Expand All @@ -199,7 +201,7 @@ export class MessagesToChatCompletionConverter {
cached_tokens: cachedTokens,
...(webSearch > 0 ? { web_search: webSearch } : {}),
},
input_tokens: inputTokens,
input_tokens: uncachedInputTokens,
output_tokens: outputTokens,
cache_read_input_tokens: cachedTokens,
server_tool_use:
Expand All @@ -209,7 +211,7 @@ export class MessagesToChatCompletionConverter {
web_search_requests: webSearch,
}
: null,
cache_creation_input_tokens: 0,
cache_creation_input_tokens: cacheWriteTokens,
service_tier: "standard",
audio_input_tokens: u.prompt_tokens_details?.audio_tokens ?? 0,
audio_cache_read_tokens: (u.prompt_tokens_details as any)?.audio_cached_tokens ?? 0,
Expand Down
3 changes: 2 additions & 1 deletion src/messages/gemini.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ export class MessagesToGeminiConverter {

const promptTokens = promptTokenCount + toolUsePromptTokenCount;
const completionTokens = candidatesTokenCount + thoughtsTokenCount;
const uncachedInputTokens = Math.max(0, promptTokens - cachedContentTokenCount);

return {
prompt_tokens: promptTokens,
Expand All @@ -233,7 +234,7 @@ export class MessagesToGeminiConverter {
web_search_queries: webSearchQueriesCount,
tool_use: toolUsePromptTokenCount,
trafficType,
input_tokens: promptTokens,
input_tokens: uncachedInputTokens,
output_tokens: completionTokens,
cache_read_input_tokens: cachedContentTokenCount,
cache_creation_input_tokens: 0,
Expand Down
6 changes: 4 additions & 2 deletions src/messages/responses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ export class MessagesToResponsesConverter {
const outputTokens = u.output_tokens ?? 0;
const totalTokens = u.total_tokens ?? inputTokens + outputTokens;
const cachedTokens = u.input_tokens_details?.cached_tokens ?? 0;
const cacheWriteTokens = u.input_tokens_details?.cache_write_tokens ?? 0;
const uncachedInputTokens = Math.max(0, inputTokens - cachedTokens - cacheWriteTokens);
const reasoningTokens = u.output_tokens_details?.reasoning_tokens ?? 0;

return {
Expand All @@ -185,7 +187,7 @@ export class MessagesToResponsesConverter {
cached_tokens: cachedTokens,
...(webSearchCount > 0 ? { web_search: webSearchCount } : {}),
},
input_tokens: inputTokens,
input_tokens: uncachedInputTokens,
output_tokens: outputTokens,
cache_read_input_tokens: cachedTokens,
server_tool_use:
Expand All @@ -195,7 +197,7 @@ export class MessagesToResponsesConverter {
web_search_requests: webSearchCount,
}
: null,
cache_creation_input_tokens: 0,
cache_creation_input_tokens: cacheWriteTokens,
service_tier: "standard",
audio_input_tokens: 0,
audio_cache_read_tokens: 0,
Expand Down
Loading