npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

llm-messages

v0.5.2

Published

Convert chat conversations and responses between OpenAI, Anthropic and Gemini. Tool calls, images, audio, documents and roles handled. Zero dependencies.

Readme

llm-messages

npm version npm downloads CI OpenSSF Scorecard license zero dependencies

Convert chat conversations between OpenAI, Anthropic and Gemini message formats, and normalize provider responses into the same OpenAI-compatible assistant shape. Tool calls, system prompts, roles and response metadata handled correctly. Zero dependencies.

Switching an agent from one provider to another (or running fallback across providers) means rewriting the whole conversation, and the differences are subtle enough to break at runtime:

  • The system prompt is a message in OpenAI, a top-level system field in Anthropic, and systemInstruction in Gemini.
  • The assistant role is assistant in OpenAI and Anthropic but model in Gemini.
  • Tool-call arguments are a JSON string in OpenAI but a parsed object in Anthropic and Gemini.
  • Tool results are a standalone role: "tool" message in OpenAI, a tool_result block inside a user turn in Anthropic, and a functionResponse part in Gemini.
  • Gemini can match tool calls to results by id when present or by function name when ids are omitted, while OpenAI and Anthropic require ids.
  • Anthropic and Gemini reject consecutive same-role turns; OpenAI does not.

llm-messages handles all of it. Write the conversation once, send it to any provider.

Install

npm install llm-messages

Requires Node 18+. Ships ESM and CommonJS with full TypeScript types.

CommonJS consumers can import the same package root:

const { toAnthropic, toGemini } = require('llm-messages');

Quick start

import { toAnthropic, toGemini, type OpenAIMessage } from 'llm-messages';

// A normal OpenAI Chat Completions conversation
const messages: OpenAIMessage[] = [
  { role: 'system', content: 'You are a weather assistant.' },
  { role: 'user', content: "What's the weather in Paris?" },
];

const anthropic = toAnthropic(messages);
// -> { system: 'You are a weather assistant.', messages: [{ role: 'user', content: "What's the weather in Paris?" }] }

const gemini = toGemini(messages);
// -> { systemInstruction: { parts: [{ text: 'You are a weather assistant.' }] },
//      contents: [{ role: 'user', parts: [{ text: "What's the weather in Paris?" }] }] }

API

The canonical hub

OpenAI Chat Completions is the canonical format. Every conversion routes through it, so you get a function for each direction:

import { toAnthropic, fromAnthropic, toGemini, fromGemini, convert } from 'llm-messages';

toAnthropic(openaiMessages); // OpenAI  -> Anthropic
fromAnthropic(anthropicBody); // Anthropic -> OpenAI
toGemini(openaiMessages); // OpenAI  -> Gemini
fromGemini(geminiBody); // Gemini  -> OpenAI

// Or convert between any two providers in one call:
convert(anthropicBody, { from: 'anthropic', to: 'gemini' });

convert is fully typed: the input and output shapes are inferred from the from and to providers.

Tool calls round trip losslessly

The hard part is tool use, and it survives a full round trip unchanged:

import { fromGemini, toGemini, type OpenAIMessage } from 'llm-messages';

const messages: OpenAIMessage[] = [
  {
    role: 'assistant',
    content: null,
    tool_calls: [
      { id: 'call_abc', type: 'function', function: { name: 'get_weather', arguments: '{"location":"Paris"}' } },
    ],
  },
  { role: 'tool', tool_call_id: 'call_abc', content: '15C partly cloudy' },
];

fromGemini(toGemini(messages)); // deep-equals the original `messages`

Arguments are parsed and re-serialized, ids are preserved (and regenerated deterministically when a Gemini payload does not provide a non-empty string id), and parallel tool results are grouped into the single user turn each provider expects. Anthropic tool_result.is_error is preserved as optional canonical tool-message metadata; standalone Gemini functionResponse.name is also preserved so orphaned tool results can be sent back to Gemini without renaming the function to the id. When Anthropic includes tool_result.tool_use_id or Gemini includes functionResponse.id, it is matched before provider-specific fallback behavior.

Conversion report

When typed provider payloads contain malformed tool-call or media fields, conversions make a deterministic choice and optionally report it, so you can surface or log what happened:

toGemini(messages, {
  onWarning: (w) => console.warn(`[${w.code}] ${w.message}`),
});

Warning codes: generated-id, unmapped-tool-result, merged-role, dropped-content, dropped-metadata, invalid-json-arguments, system-midstream, gemini-url-image, gemini-url-media, unsupported-modality.

Consumers that validate fixture metadata or warning filters can import the same stable list from the package root as warningCodes.

Reading responses

The same idea applies to the read side. Normalize a provider's response body into a canonical OpenAI assistant message, plus a neutral finish reason and token usage:

import {
  responseFromAnthropic,
  responseFromGemini,
  responseFromOpenAI,
  responseFromOpenAIResponses,
  normalizeResponse,
} from 'llm-messages';

const { message, finishReason, usage } = responseFromAnthropic(anthropicResponseBody);
// message     -> { role: 'assistant', content, tool_calls? }  (tool input re-serialized to a JSON string)
// finishReason -> 'stop' | 'tool_calls' | 'length' | 'content_filter' | 'unknown'
// usage       -> { inputTokens, outputTokens }

const responses = responseFromOpenAIResponses(openaiResponsesBody);
// OpenAI Responses API `output_text` items become assistant `content`.
// `function_call` items become Chat Completions-compatible `tool_calls`.

const chat = responseFromOpenAI(openaiChatBody);
// Chat Completions `choices[0].message.tool_calls` stay Chat Completions-compatible.

const gemini = responseFromGemini(geminiResponseBody);
// Gemini `functionCall` parts become assistant `tool_calls`.

// Or dispatch by provider:
normalizeResponse(geminiResponseBody, { from: 'gemini' });
normalizeResponse(openaiResponsesBody, { from: 'openai-responses' });

finishReason is normalized to tool_calls whenever the model called a tool, even for Gemini (which reports STOP) and Responses API bodies with function_call items. OpenAI Chat Completions, OpenAI Responses, Anthropic and Gemini tool calls without a non-empty string id get a deterministic one.

Format cheatsheet

| | OpenAI | Anthropic | Gemini | | ---------------- | ------------------------ | -------------------------------- | ------------------------------- | | System prompt | role: "system" message | top-level system | systemInstruction | | Assistant role | assistant | assistant | model | | Tool call | tool_calls[].function | tool_use block | functionCall part | | Call arguments | JSON string | object (input) | object (args) | | Tool result | role: "tool" message | tool_result block in user turn | functionResponse part in user | | Match key | tool_call_id | tool_use_id | id when present, else name | | Role alternation | not required | strict | strict |

Images, audio and documents

Image parts convert across all three providers:

import { toAnthropic, toGemini, type OpenAIMessage } from 'llm-messages';

const messages: OpenAIMessage[] = [
  {
    role: 'user',
    content: [
      { type: 'text', text: 'What is in this image?' },
      { type: 'image_url', image_url: { url: 'data:image/png;base64,iVBORw0KGgo...' } },
    ],
  },
];

toAnthropic(messages).messages[0]?.content;
// -> [{ type: 'text', ... }, { type: 'image', source: { type: 'base64', media_type: 'image/png', data: '...' } }]

toGemini(messages).contents[0]?.parts;
// -> [{ text: 'What is in this image?' }, { inlineData: { mimeType: 'image/png', data: '...' } }]

Base64 data URLs round trip losslessly. A remote https URL maps to an Anthropic url source; for Gemini it is emitted as fileData.fileUri with a gemini-url-image warning, since Gemini may require the Files API for non-Google URIs.

If you need to handle image payloads directly, parseDataUrl and toDataUrl are exported for the same base64 data URL shape used by the converters.

Audio (input_audio) and documents (file, e.g. PDF) convert too. Audio moves between OpenAI and Gemini; Anthropic has no audio input, so an audio part is dropped with an unsupported-modality warning. Base64 document payloads convert across all three providers (OpenAI file, Anthropic document, Gemini inlineData). OpenAI file_id document references map to Anthropic file sources; Gemini has no equivalent and drops them with unsupported-modality.

Scope

Version 0.x covers text, system prompts, tool calls/results, images, audio and documents, which is the core of every agent loop. Unsupported or lossy parts are reported through stable warning codes such as dropped-content, unsupported-modality or provider-specific media warnings rather than failing. Provider-only fields are preserved only when the canonical OpenAI-compatible shape has an explicit optional metadata field for them, such as Anthropic tool_result.is_error and standalone Gemini functionResponse.name. When that metadata has no target-provider equivalent, conversion continues and reports dropped-metadata.

Roadmap

See ROADMAP.md for current maintenance priorities, including OpenAI Responses API coverage, offline conformance fixtures and tool-call edge cases. The conformance fixtures guide describes how API credits should be used to refresh deterministic public fixtures without putting secrets in CI.

For teams evaluating the package, the adoption guide covers the OpenAI-compatible boundary, local validation and production checks.

Security posture is tracked in docs/security-posture.md, including CodeQL, OpenSSF Scorecard, Dependabot and branch rules.

Provider portability suite

llm-messages is the conversation boundary in a small provider-portability suite for OpenAI-compatible agent infrastructure:

  • tool-schema converts one JSON Schema into provider-specific tool/function schemas.
  • llm-sse parses streaming provider responses into unified events.
  • llm-errors normalizes provider errors, retry hints and fallback decisions.
  • json-from-llm extracts JSON before it enters a tool or message pipeline.
  • llm-portability-demo shows the whole flow offline, with no API key required.

Read the provider portability map for the package roles, OpenAI-compatible hub shape and demo flow.

License

MIT (c) Sebastian Legarraga. See LICENSE.