llm-messages 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,128 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.5.2] - 2026-06-12
10
+
11
+ ### Summary
12
+
13
+ - Added an offline conformance suite for OpenAI Chat Completions, OpenAI
14
+ Responses, Anthropic and Gemini edge cases around tool-call ids, arguments,
15
+ refusals and tool-result mapping.
16
+ - Strengthened the release path with one local validation command, package
17
+ dry-run checks, installed ESM/CommonJS/type smoke tests and a Node 18 consumer
18
+ smoke in CI.
19
+ - Documented the response-normalization helpers, stable warning-code export and
20
+ fixture workflow so users and maintainers can audit the supported portability
21
+ surface before publishing.
22
+
23
+ ### Changed
24
+
25
+ - Added an offline conformance fixture harness and committed fixtures for
26
+ OpenAI Chat Completions, OpenAI Responses, Anthropic and Gemini request and
27
+ response conversions.
28
+ - Covered deterministic id generation and reservation, duplicate provider ids,
29
+ malformed function names, invalid argument payloads, refusals, unsupported
30
+ OpenAI tool-call types, Anthropic `tool_result` mapping and Gemini
31
+ `functionResponse` matching.
32
+ - Documented direct response normalizers, stable warning codes, Gemini id/name
33
+ matching, the fixture contract and inventory, roadmap distinctions,
34
+ contributor validation commands and release staging review steps.
35
+ - Aligned the pull request checklist with the conformance fixture inventory
36
+ workflow so fixture changes are reviewed with their docs update.
37
+ - Added `npm run check`, `pack:check` and `examples:check`, then aligned CI and
38
+ release validation around format, typecheck, lint, tests, build, examples,
39
+ pack dry-run and package smoke checks.
40
+ - Added a CommonJS usage example and extended packaged examples to cover
41
+ response normalization and the `warningCodes` export.
42
+ - Strengthened package smoke testing to install the packed tarball, exercise
43
+ ESM/CommonJS/types, run bundled examples, verify intentional public files,
44
+ reject private source/test/script/config leaks, validate package metadata and
45
+ inspect source maps and audit packaged Markdown file and heading-anchor links.
46
+ - Added a fixture-backed self-test for the packaged Markdown link checker so the
47
+ smoke script verifies missing files, missing anchors and package-escaping links
48
+ before auditing the real tarball.
49
+ - Exported, froze and documented the `warningCodes` runtime list from the
50
+ package root so tests and consumers can validate warning-code values against
51
+ the public API.
52
+ - Tightened the conformance warning-code guard so stale public warning codes
53
+ fail tests if they are no longer emitted by the source.
54
+
55
+ ### Fixed
56
+
57
+ - Drop Gemini `thought: true` (reasoning) response parts with a
58
+ `dropped-content` warning instead of mixing the model's reasoning text into
59
+ the canonical assistant content.
60
+ - Preserve OpenAI Chat Completions refusal text and refusal content parts when
61
+ assistant `content` is empty or array-shaped.
62
+ - Drop unsupported OpenAI Chat Completions `tool_calls[]` types with a
63
+ `dropped-content` warning instead of emitting malformed canonical function
64
+ calls.
65
+ - Warn and continue when OpenAI Chat Completions `tool_calls` payloads or
66
+ entries are malformed instead of silently skipping provider output.
67
+ - Normalize legacy OpenAI Chat Completions `message.function_call` responses
68
+ into canonical `tool_calls` with deterministic ids.
69
+ - Warn and fall back to empty canonical arguments when OpenAI Chat Completions,
70
+ OpenAI Responses, Anthropic or Gemini tool-call argument payloads are
71
+ malformed JSON, JSON non-objects or explicit non-object provider-native
72
+ values.
73
+ - Warn and fall back to empty canonical arguments when provider-native argument
74
+ objects cannot be JSON serialized.
75
+ - Preserve malformed response-level tool calls from OpenAI Chat Completions,
76
+ OpenAI Responses, Anthropic and Gemini by substituting the same stable
77
+ fallback function name used by conversation converters.
78
+ - Warn with `dropped-content` when OpenAI Chat Completions, OpenAI Responses,
79
+ Anthropic or Gemini response-level content parts are unsupported instead of
80
+ silently dropping provider output parts.
81
+ - Warn with `dropped-content` when OpenAI Chat Completions, OpenAI Responses,
82
+ Anthropic or Gemini response normalizers receive malformed top-level response
83
+ bodies.
84
+ - Warn with `dropped-content` when OpenAI Chat Completions, Anthropic or Gemini
85
+ response bodies omit or malform their primary output arrays instead of
86
+ silently returning an empty assistant message.
87
+ - Warn with `dropped-content` when OpenAI Chat Completions response choices omit
88
+ or malform their assistant `message` object instead of silently dropping the
89
+ choice payload.
90
+ - Warn with `dropped-content` when OpenAI Responses top-level `output[]` items
91
+ are malformed or unsupported instead of silently skipping provider output
92
+ items.
93
+ - Treat whitespace-only, OpenAI-incompatible and longer-than-64-character
94
+ provider function names as malformed before emitting canonical tool calls.
95
+ - Generate deterministic OpenAI tool-call ids for OpenAI Chat Completions,
96
+ OpenAI Responses, Anthropic and Gemini values that are omitted, empty or
97
+ non-string.
98
+ - Reserve provider-supplied tool-call ids before generating fallbacks, avoid
99
+ generated-id collisions and regenerate duplicate provider-supplied ids with a
100
+ `generated-id` warning.
101
+ - Preserve Gemini `functionResponse` behavior by preferring explicit ids,
102
+ warning on unmapped ids without consuming same-name pending calls, recovering
103
+ omitted or malformed names from id matches, ignoring non-string ids before
104
+ falling back by name and preserving mixed user text/result order.
105
+ - Warn and fall back to an empty canonical tool result when Gemini
106
+ `functionResponse.response` payloads cannot be JSON serialized.
107
+ - Map Gemini policy-blocked finish reasons, including blocklist, prohibited
108
+ content, SPII, Model Armor and image safety stops, to the neutral
109
+ `content_filter` finish reason instead of `unknown`.
110
+ - Preserve empty canonical user turns, with `dropped-content` warnings, when
111
+ Anthropic or Gemini document URLs cannot be represented in OpenAI Chat
112
+ Completions content.
113
+ - Use the tool-call id as the Gemini `functionResponse.name` fallback when a
114
+ standalone canonical tool message has an empty name.
115
+ - Warn on Anthropic `tool_result.tool_use_id` values that are missing or do not
116
+ match a prior `tool_use`, generating deterministic canonical ids for missing
117
+ values while preserving explicit unmapped ids.
118
+ - Regenerate later duplicate Anthropic `tool_result.tool_use_id` values when
119
+ they are explicit but do not match any prior `tool_use`.
120
+ - Emit an empty Anthropic user string, not an empty content-block array, when
121
+ every OpenAI user content part is dropped as unsupported.
122
+ - Reserve Anthropic `tool_result.tool_use_id` values before generating fallback
123
+ `tool_use` ids so explicit result references are not reused by id-less calls.
124
+
125
+ ## [0.5.1] - 2026-06-11
126
+
127
+ ### Changed
128
+
129
+ - Published README download badge updates so the npm package page shows the refreshed 30-day download badge.
130
+
9
131
  ## [0.5.0] - 2026-06-07
10
132
 
11
133
  ### Added
@@ -102,7 +224,9 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
102
224
  - Audio and document content parts. Audio (OpenAI `input_audio`) converts between
103
225
  OpenAI and Gemini; Anthropic has no audio input, so audio is dropped with an
104
226
  `unsupported-modality` warning. Documents (OpenAI `file`, Anthropic `document`,
105
- Gemini `inlineData` / `fileData`) convert across all three, base64 losslessly.
227
+ Gemini `inlineData`) convert across all three when base64-backed; OpenAI
228
+ `file_id` references map to Anthropic file sources and are dropped for Gemini
229
+ with an `unsupported-modality` warning.
106
230
  Adds the `MediaPart` type and `unsupported-modality` / `gemini-url-media`
107
231
  warning codes. (#5)
108
232
 
package/README.md CHANGED
@@ -1,13 +1,16 @@
1
1
  # llm-messages
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/llm-messages.svg)](https://www.npmjs.com/package/llm-messages)
4
- [![npm downloads](https://img.shields.io/npm/dm/llm-messages.svg)](https://www.npmjs.com/package/llm-messages)
4
+ [![npm downloads](https://img.shields.io/endpoint?url=https%3A%2F%2Fraw.githubusercontent.com%2Fslegarraga%2Fllm-messages%2Fmain%2Fbadges%2Fnpm-downloads%2Fllm-messages.json)](https://www.npmjs.com/package/llm-messages)
5
5
  [![CI](https://github.com/slegarraga/llm-messages/actions/workflows/ci.yml/badge.svg)](https://github.com/slegarraga/llm-messages/actions/workflows/ci.yml)
6
6
  [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/slegarraga/llm-messages/badge)](https://scorecard.dev/viewer/?uri=github.com/slegarraga/llm-messages)
7
7
  [![license](https://img.shields.io/npm/l/llm-messages.svg)](./LICENSE)
8
8
  [![zero dependencies](https://img.shields.io/badge/dependencies-0-brightgreen.svg)](./package.json)
9
9
 
10
- Convert chat conversations between **OpenAI**, **Anthropic** and **Gemini** message formats. Tool calls, system prompts and roles handled correctly. Zero dependencies.
10
+ Convert chat conversations between **OpenAI**, **Anthropic** and **Gemini**
11
+ message formats, and normalize provider responses into the same
12
+ OpenAI-compatible assistant shape. Tool calls, system prompts, roles and
13
+ response metadata handled correctly. Zero dependencies.
11
14
 
12
15
  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:
13
16
 
@@ -15,7 +18,8 @@ Switching an agent from one provider to another (or running fallback across prov
15
18
  - The assistant role is `assistant` in OpenAI and Anthropic but `model` in Gemini.
16
19
  - Tool-call arguments are a **JSON string** in OpenAI but a **parsed object** in Anthropic and Gemini.
17
20
  - 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.
18
- - Gemini matches tool calls to results **by function name**, while OpenAI and Anthropic use ids.
21
+ - Gemini can match tool calls to results **by id when present** or by function
22
+ name when ids are omitted, while OpenAI and Anthropic require ids.
19
23
  - Anthropic and Gemini reject consecutive same-role turns; OpenAI does not.
20
24
 
21
25
  `llm-messages` handles all of it. Write the conversation once, send it to any provider.
@@ -28,13 +32,19 @@ npm install llm-messages
28
32
 
29
33
  Requires Node 18+. Ships ESM and CommonJS with full TypeScript types.
30
34
 
35
+ CommonJS consumers can import the same package root:
36
+
37
+ ```js
38
+ const { toAnthropic, toGemini } = require('llm-messages');
39
+ ```
40
+
31
41
  ## Quick start
32
42
 
33
43
  ```ts
34
- import { toAnthropic, toGemini } from 'llm-messages';
44
+ import { toAnthropic, toGemini, type OpenAIMessage } from 'llm-messages';
35
45
 
36
46
  // A normal OpenAI Chat Completions conversation
37
- const messages = [
47
+ const messages: OpenAIMessage[] = [
38
48
  { role: 'system', content: 'You are a weather assistant.' },
39
49
  { role: 'user', content: "What's the weather in Paris?" },
40
50
  ];
@@ -47,7 +57,9 @@ const gemini = toGemini(messages);
47
57
  // contents: [{ role: 'user', parts: [{ text: "What's the weather in Paris?" }] }] }
48
58
  ```
49
59
 
50
- ## The canonical hub
60
+ ## API
61
+
62
+ ### The canonical hub
51
63
 
52
64
  OpenAI Chat Completions is the canonical format. Every conversion routes through
53
65
  it, so you get a function for each direction:
@@ -67,12 +79,14 @@ convert(anthropicBody, { from: 'anthropic', to: 'gemini' });
67
79
  `convert` is fully typed: the input and output shapes are inferred from the
68
80
  `from` and `to` providers.
69
81
 
70
- ## Tool calls round trip losslessly
82
+ ### Tool calls round trip losslessly
71
83
 
72
84
  The hard part is tool use, and it survives a full round trip unchanged:
73
85
 
74
86
  ```ts
75
- const messages = [
87
+ import { fromGemini, toGemini, type OpenAIMessage } from 'llm-messages';
88
+
89
+ const messages: OpenAIMessage[] = [
76
90
  {
77
91
  role: 'assistant',
78
92
  content: null,
@@ -87,16 +101,19 @@ fromGemini(toGemini(messages)); // deep-equals the original `messages`
87
101
  ```
88
102
 
89
103
  Arguments are parsed and re-serialized, ids are preserved (and regenerated
90
- deterministically when a Gemini payload omits them), and parallel tool results
91
- are grouped into the single user turn each provider expects. Anthropic
104
+ deterministically when a Gemini payload does not provide a non-empty string id), and
105
+ parallel tool results are grouped into the single user turn each provider expects. Anthropic
92
106
  `tool_result.is_error` is preserved as optional canonical tool-message metadata;
93
107
  standalone Gemini `functionResponse.name` is also preserved so orphaned tool
94
- results can be sent back to Gemini without renaming the function to the id.
108
+ results can be sent back to Gemini without renaming the function to the id. When
109
+ Anthropic includes `tool_result.tool_use_id` or Gemini includes
110
+ `functionResponse.id`, it is matched before provider-specific fallback behavior.
95
111
 
96
- ## Conversion report
112
+ ### Conversion report
97
113
 
98
- Conversions never throw on malformed input. Instead they make a deterministic
99
- choice and optionally report it, so you can surface or log what happened:
114
+ When typed provider payloads contain malformed tool-call or media fields,
115
+ conversions make a deterministic choice and optionally report it, so you can
116
+ surface or log what happened:
100
117
 
101
118
  ```ts
102
119
  toGemini(messages, {
@@ -109,13 +126,22 @@ Warning codes: `generated-id`, `unmapped-tool-result`, `merged-role`,
109
126
  `system-midstream`, `gemini-url-image`, `gemini-url-media`,
110
127
  `unsupported-modality`.
111
128
 
112
- ## Reading responses
129
+ Consumers that validate fixture metadata or warning filters can import the same
130
+ stable list from the package root as `warningCodes`.
131
+
132
+ ### Reading responses
113
133
 
114
134
  The same idea applies to the read side. Normalize a provider's response body into
115
135
  a canonical OpenAI assistant message, plus a neutral finish reason and token usage:
116
136
 
117
137
  ```ts
118
- import { responseFromAnthropic, responseFromOpenAIResponses, normalizeResponse } from 'llm-messages';
138
+ import {
139
+ responseFromAnthropic,
140
+ responseFromGemini,
141
+ responseFromOpenAI,
142
+ responseFromOpenAIResponses,
143
+ normalizeResponse,
144
+ } from 'llm-messages';
119
145
 
120
146
  const { message, finishReason, usage } = responseFromAnthropic(anthropicResponseBody);
121
147
  // message -> { role: 'assistant', content, tool_calls? } (tool input re-serialized to a JSON string)
@@ -126,6 +152,12 @@ const responses = responseFromOpenAIResponses(openaiResponsesBody);
126
152
  // OpenAI Responses API `output_text` items become assistant `content`.
127
153
  // `function_call` items become Chat Completions-compatible `tool_calls`.
128
154
 
155
+ const chat = responseFromOpenAI(openaiChatBody);
156
+ // Chat Completions `choices[0].message.tool_calls` stay Chat Completions-compatible.
157
+
158
+ const gemini = responseFromGemini(geminiResponseBody);
159
+ // Gemini `functionCall` parts become assistant `tool_calls`.
160
+
129
161
  // Or dispatch by provider:
130
162
  normalizeResponse(geminiResponseBody, { from: 'gemini' });
131
163
  normalizeResponse(openaiResponsesBody, { from: 'openai-responses' });
@@ -133,9 +165,10 @@ normalizeResponse(openaiResponsesBody, { from: 'openai-responses' });
133
165
 
134
166
  `finishReason` is normalized to `tool_calls` whenever the model called a tool, even
135
167
  for Gemini (which reports `STOP`) and Responses API bodies with `function_call`
136
- items. Gemini tool calls without an id get a deterministic one.
168
+ items. OpenAI Chat Completions, OpenAI Responses, Anthropic and Gemini tool
169
+ calls without a non-empty string id get a deterministic one.
137
170
 
138
- ## Format cheatsheet
171
+ ### Format cheatsheet
139
172
 
140
173
  | | OpenAI | Anthropic | Gemini |
141
174
  | ---------------- | ------------------------ | -------------------------------- | ------------------------------- |
@@ -144,15 +177,17 @@ items. Gemini tool calls without an id get a deterministic one.
144
177
  | Tool call | `tool_calls[].function` | `tool_use` block | `functionCall` part |
145
178
  | Call arguments | JSON string | object (`input`) | object (`args`) |
146
179
  | Tool result | `role: "tool"` message | `tool_result` block in user turn | `functionResponse` part in user |
147
- | Match key | `tool_call_id` | `tool_use_id` | function `name` (id optional) |
180
+ | Match key | `tool_call_id` | `tool_use_id` | `id` when present, else `name` |
148
181
  | Role alternation | not required | strict | strict |
149
182
 
150
- ## Images, audio and documents
183
+ ### Images, audio and documents
151
184
 
152
185
  Image parts convert across all three providers:
153
186
 
154
187
  ```ts
155
- const messages = [
188
+ import { toAnthropic, toGemini, type OpenAIMessage } from 'llm-messages';
189
+
190
+ const messages: OpenAIMessage[] = [
156
191
  {
157
192
  role: 'user',
158
193
  content: [
@@ -162,8 +197,11 @@ const messages = [
162
197
  },
163
198
  ];
164
199
 
165
- toAnthropic(messages); // -> { type: 'image', source: { type: 'base64', media_type: 'image/png', data: '...' } }
166
- toGemini(messages); // -> { inlineData: { mimeType: 'image/png', data: '...' } }
200
+ toAnthropic(messages).messages[0]?.content;
201
+ // -> [{ type: 'text', ... }, { type: 'image', source: { type: 'base64', media_type: 'image/png', data: '...' } }]
202
+
203
+ toGemini(messages).contents[0]?.parts;
204
+ // -> [{ text: 'What is in this image?' }, { inlineData: { mimeType: 'image/png', data: '...' } }]
167
205
  ```
168
206
 
169
207
  Base64 data URLs round trip losslessly. A remote `https` URL maps to an Anthropic
@@ -171,28 +209,35 @@ Base64 data URLs round trip losslessly. A remote `https` URL maps to an Anthropi
171
209
  `gemini-url-image` warning, since Gemini may require the Files API for non-Google
172
210
  URIs.
173
211
 
212
+ If you need to handle image payloads directly, `parseDataUrl` and `toDataUrl`
213
+ are exported for the same base64 data URL shape used by the converters.
214
+
174
215
  **Audio** (`input_audio`) and **documents** (`file`, e.g. PDF) convert too. Audio
175
216
  moves between OpenAI and Gemini; Anthropic has no audio input, so an audio part is
176
- dropped with an `unsupported-modality` warning. Documents convert across all three
177
- (OpenAI `file`, Anthropic `document`, Gemini `inlineData`).
217
+ dropped with an `unsupported-modality` warning. Base64 document payloads convert
218
+ across all three providers (OpenAI `file`, Anthropic `document`, Gemini
219
+ `inlineData`). OpenAI `file_id` document references map to Anthropic `file`
220
+ sources; Gemini has no equivalent and drops them with `unsupported-modality`.
178
221
 
179
222
  ## Scope
180
223
 
181
224
  Version 0.x covers text, system prompts, tool calls/results, images, audio and
182
- documents, which is the core of every agent loop. Unsupported parts are reported
183
- via `dropped-content` rather than failing. Provider-only fields are preserved
184
- only when the canonical OpenAI-compatible shape has an explicit optional
185
- metadata field for them, such as Anthropic `tool_result.is_error` and standalone
186
- Gemini `functionResponse.name`. When that metadata has no target-provider
187
- equivalent, conversion continues and reports `dropped-metadata`.
225
+ documents, which is the core of every agent loop. Unsupported or lossy parts are
226
+ reported through stable warning codes such as `dropped-content`,
227
+ `unsupported-modality` or provider-specific media warnings rather than failing.
228
+ Provider-only fields are preserved only when the canonical OpenAI-compatible
229
+ shape has an explicit optional metadata field for them, such as Anthropic
230
+ `tool_result.is_error` and standalone Gemini `functionResponse.name`. When that
231
+ metadata has no target-provider equivalent, conversion continues and reports
232
+ `dropped-metadata`.
188
233
 
189
234
  ## Roadmap
190
235
 
191
236
  See [ROADMAP.md](./ROADMAP.md) for current maintenance priorities, including
192
- OpenAI Responses API coverage, live conformance fixtures and tool-call edge
193
- cases. The [conformance fixtures plan](./docs/conformance-fixtures.md) describes
194
- how API credits should be used to refresh deterministic public fixtures without
195
- putting secrets in CI.
237
+ OpenAI Responses API coverage, offline conformance fixtures and tool-call edge
238
+ cases. The [conformance fixtures guide](./docs/conformance-fixtures.md)
239
+ describes how API credits should be used to refresh deterministic public
240
+ fixtures without putting secrets in CI.
196
241
 
197
242
  For teams evaluating the package, the
198
243
  [adoption guide](./docs/adoption-guide.md) covers the OpenAI-compatible boundary,
package/ROADMAP.md CHANGED
@@ -15,11 +15,11 @@ fallback behavior matter.
15
15
 
16
16
  Public issue: https://github.com/slegarraga/llm-messages/issues/6
17
17
 
18
- 2. **Live conformance fixtures**
18
+ 2. **Provider-backed fixture refreshes**
19
19
 
20
20
  Add provider-backed fixture generation for OpenAI, Anthropic and Gemini
21
- payloads, keeping the committed test fixtures deterministic and safe to run
22
- without API keys.
21
+ payloads, while keeping the committed conformance fixtures deterministic,
22
+ offline, and safe to run without API keys.
23
23
 
24
24
  Plan: [docs/conformance-fixtures.md](./docs/conformance-fixtures.md)
25
25
 
package/SECURITY.md ADDED
@@ -0,0 +1,24 @@
1
+ # Security Policy
2
+
3
+ ## Supported versions
4
+
5
+ The latest published `0.x` release receives security fixes.
6
+
7
+ ## Reporting a vulnerability
8
+
9
+ Please report security issues privately rather than opening a public issue.
10
+
11
+ - Use GitHub's [private vulnerability reporting](https://github.com/slegarraga/llm-messages/security/advisories/new), or
12
+ - Email **sebastian@0a.cl** with the details.
13
+
14
+ Include a description, a reproduction, and the impact. You can expect an initial
15
+ response within a few days. Once a fix is released, we are happy to credit you in
16
+ the advisory unless you prefer to remain anonymous.
17
+
18
+ ## Scope
19
+
20
+ `llm-messages` has zero runtime dependencies and performs only in-memory data
21
+ transformation: it does not make network requests, read or write files, or
22
+ execute code from its input. The most relevant risks are denial of service from
23
+ pathological input (for example, deeply nested structures). Reports along those
24
+ lines are welcome.