linguafranca
LLM API format converter with a Rust core and Python bindings.
Converts requests, responses, and streaming events between:
- OpenAI Chat Completions
- Anthropic Messages
- Open Responses
Also supports within-format conversions: collect a stream into a single response, or decompose a response into stream events.
Installation
# Python
pip install martian-linguafranca
# or
uv add martian-linguafranca
# Installs as 'martian-linguafranca', import as 'linguafranca'
# Rust
cargo add linguafranca
Supported formats
FormatName |
API |
|---|---|
FormatName.OPENAI_CHAT_COMPLETIONS |
OpenAI Chat Completions |
FormatName.ANTHROPIC_MESSAGES |
Anthropic Messages |
FormatName.OPEN_RESPONSES |
Open Responses |
Every pair is supported in both directions for requests and responses. Within-format collect (stream → response) and decompose (response → stream) are supported for all three formats.
Quick start
import linguafranca as lf
# Convert a Chat Completions request to Anthropic Messages
result = lf.convert_request_json(
{"model": "gpt-4.1-mini", "messages": [{"role": "user", "content": "hello"}]},
source_format=lf.FormatName.OPENAI_CHAT_COMPLETIONS,
target_format=lf.FormatName.ANTHROPIC_MESSAGES,
)
result.value # converted dict
result.warnings # list of lossy conversion warnings (dropped/modified fields)
Converting requests
import linguafranca as lf
# OpenAI Chat Completions -> Anthropic Messages
result = lf.convert_request_json(
{
"model": "gpt-4.1-mini",
"messages": [{"role": "user", "content": "hello"}],
"temperature": 0.7,
},
source_format=lf.FormatName.OPENAI_CHAT_COMPLETIONS,
target_format=lf.FormatName.ANTHROPIC_MESSAGES,
)
print(result.value)
# {"model": "gpt-4.1-mini", "max_tokens": 4096, "messages": [...], ...}
# Anthropic Messages -> OpenAI Chat Completions
result = lf.convert_request_json(
{
"model": "claude-3-5-sonnet",
"max_tokens": 64,
"messages": [{"role": "user", "content": "hello"}],
},
source_format=lf.FormatName.ANTHROPIC_MESSAGES,
target_format=lf.FormatName.OPENAI_CHAT_COMPLETIONS,
)
Convenience wrappers
When you always target the same format, convenience wrappers save some typing:
# Convert anything -> Anthropic Messages
result = lf.to_messages_request(
openai_request,
source_format=lf.FormatName.OPENAI_CHAT_COMPLETIONS,
)
# Convert anything -> OpenAI Chat Completions
result = lf.to_chat_completions_request(
anthropic_request,
source_format=lf.FormatName.ANTHROPIC_MESSAGES,
)
The same pattern works for responses with to_messages_response and to_chat_completions_response.
Converting responses
result = lf.convert_response_json(
{
"id": "chatcmpl-abc123",
"object": "chat.completion",
"model": "gpt-4.1-mini",
"choices": [{
"index": 0,
"message": {"role": "assistant", "content": "Hello!"},
"finish_reason": "stop",
}],
"usage": {"prompt_tokens": 5, "completion_tokens": 7, "total_tokens": 12},
},
source_format=lf.FormatName.OPENAI_CHAT_COMPLETIONS,
target_format=lf.FormatName.ANTHROPIC_MESSAGES,
)
print(result.value)
Streaming
Sync streaming with httpx
import json
import httpx
import linguafranca as lf
def parse_sse(response: httpx.Response):
"""Yield parsed JSON objects from an SSE stream."""
for line in response.iter_lines():
if line.startswith("data: ") and line != "data: [DONE]":
yield json.loads(line[6:])
headers = {"Authorization": "Bearer YOUR_KEY", "Content-Type": "application/json"}
payload = {
"model": "gpt-4.1-mini",
"messages": [{"role": "user", "content": "hello"}],
"stream": True,
}
with httpx.stream("POST", "https://api.openai.com/v1/chat/completions",
headers=headers, json=payload) as resp:
stream = lf.convert_response_stream_json(
parse_sse(resp),
source_format=lf.FormatName.OPENAI_CHAT_COMPLETIONS,
target_format=lf.FormatName.OPEN_RESPONSES,
)
for event in stream:
print(event)
# Check warnings after the stream is fully consumed
for w in stream.take_warnings():
print(f"{w.field}: {w.message}")
Async streaming with httpx
import json
import httpx
import linguafranca as lf
async def parse_sse(response: httpx.Response):
async for line in response.aiter_lines():
if line.startswith("data: ") and line != "data: [DONE]":
yield json.loads(line[6:])
async def main():
headers = {"Authorization": "Bearer YOUR_KEY", "Content-Type": "application/json"}
payload = {
"model": "gpt-4.1-mini",
"messages": [{"role": "user", "content": "hello"}],
"stream": True,
}
async with httpx.AsyncClient() as client:
async with client.stream("POST",
"https://api.openai.com/v1/chat/completions",
headers=headers, json=payload) as resp:
stream = lf.aconvert_response_stream(
parse_sse(resp),
source_format=lf.FormatName.OPENAI_CHAT_COMPLETIONS,
target_format=lf.FormatName.OPEN_RESPONSES,
)
async for event in stream:
print(event)
Collecting & decomposing streams
Besides converting streams between formats, you can convert within a format: collect streaming events into a single response, or decompose a response into the stream events a server would have produced.
All three formats are supported.
Collect: stream → response
import json
import httpx
import linguafranca as lf
def parse_sse(response: httpx.Response):
for line in response.iter_lines():
if line.startswith("data: ") and line != "data: [DONE]":
yield json.loads(line[6:])
headers = {"Authorization": "Bearer YOUR_KEY", "Content-Type": "application/json"}
payload = {
"model": "gpt-4.1-mini",
"messages": [{"role": "user", "content": "hello"}],
"stream": True,
}
with httpx.stream("POST", "https://api.openai.com/v1/chat/completions",
headers=headers, json=payload) as resp:
result = lf.collect_response_stream_json(
parse_sse(resp),
format_name=lf.FormatName.OPENAI_CHAT_COMPLETIONS,
)
print(result.value) # complete response dict
print(result.warnings) # any issues during collection
The events iterable is consumed lazily — you can pass a generator, list, or any iterator.
Decompose: response → stream
import linguafranca as lf
result = lf.decompose_response_to_stream_json(
response_dict,
format_name=lf.FormatName.ANTHROPIC_MESSAGES,
)
for event in result.value:
print(event["type"]) # message_start, content_block_start, ...
Async variants
import linguafranca as lf
# Async collect
result = await lf.acollect_response_stream_json(
async_sse_events,
format_name=lf.FormatName.OPENAI_CHAT_COMPLETIONS,
)
Typed event variants
The non-_json variants (collect_response_stream,
decompose_response_to_stream) accept dataclasses and Pydantic models in
addition to plain dicts:
result = lf.collect_response_stream(
typed_events,
format_name=lf.FormatName.OPEN_RESPONSES,
)
Typed payloads (recommended)
The package ships auto-generated @dataclass definitions for all three
formats via linguafranca.types. Using them gives you IDE autocompletion,
type checking, and catches mistakes before the payload hits the converter.
import linguafranca as lf
from linguafranca.types import (
ChatCompletionsOpenAiRequest,
ChatCompletionsMessageUser,
)
request = ChatCompletionsOpenAiRequest(
model="gpt-4.1-mini",
messages=[
ChatCompletionsMessageUser(content="hello", role="user"),
],
temperature=0.7,
)
result = lf.convert_request(
request,
source_format=lf.FormatName.OPENAI_CHAT_COMPLETIONS,
target_format=lf.FormatName.ANTHROPIC_MESSAGES,
)
print(result.value)
The non-_json variants (convert_request, convert_response,
convert_response_stream) accept any of:
linguafranca.typesdataclasses (recommended)- plain dicts
- Pydantic models — serialised via
model.model_dump()
The _json variants (convert_request_json, convert_response_json,
convert_response_stream_json) accept and return plain dicts only.
Conversion config
Request conversions accept an optional config parameter to control conversion behavior.
Stripping encrypted reasoning
When forwarding requests between providers, thinking/reasoning blocks carry provider-specific signatures that the target API will reject. Use strip_encrypted_reasoning to clean them:
import linguafranca as lf
result = lf.convert_request_json(
anthropic_request_with_thinking,
source_format=lf.FormatName.ANTHROPIC_MESSAGES,
target_format=lf.FormatName.OPEN_RESPONSES,
config=lf.ConversionConfig(strip_encrypted_reasoning=True),
)
You can also pass a plain dict:
result = lf.convert_request_json(
...,
config={"strip_encrypted_reasoning": True},
)
When strip_encrypted_reasoning is enabled:
- Anthropic -> Open Responses: Thinking blocks keep their summary text but
encrypted_contentis removed. Redacted thinking blocks (no summary) are dropped entirely. - Open Responses -> Anthropic: All reasoning items are dropped from the message history.
- The reasoning/thinking config (whether the model should think) is always preserved.
Warnings
Conversions between formats can be lossy — some fields exist in one format but not another. When this happens, the library returns warnings instead of failing:
result = lf.convert_request_json(
request,
source_format=lf.FormatName.OPENAI_CHAT_COMPLETIONS,
target_format=lf.FormatName.ANTHROPIC_MESSAGES,
)
for w in result.warnings:
print(f"{w.field}: {w.message}")
# e.g. "frequency_penalty: field not supported in Anthropic Messages, dropped"
For streaming, call stream.take_warnings() after the stream is consumed.
Error handling
All errors inherit from ConversionError:
import linguafranca as lf
# Invalid payload structure
try:
lf.convert_request_json(
{"not": "a valid request"},
source_format=lf.FormatName.OPENAI_CHAT_COMPLETIONS,
target_format=lf.FormatName.ANTHROPIC_MESSAGES,
)
except lf.SchemaValidationError as e:
print(e) # payload doesn't match the source format schema
# Unsupported conversion pair (streaming only)
try:
lf.convert_response_stream_json(
events,
source_format=lf.FormatName.OPEN_RESPONSES,
target_format=lf.FormatName.OPEN_RESPONSES,
)
except lf.UnsupportedConversionError as e:
print(e)
All available types
All request, response, and streaming event types for each format are available
under linguafranca.types:
from linguafranca.types import (
# OpenAI Chat Completions
ChatCompletionsOpenAiRequest,
ChatCompletionsMessageUser,
ChatCompletionsMessageSystem,
ChatCompletionsMessageAssistant,
ChatCompletionsResponse,
ChatCompletionsStreamChunk,
# Anthropic Messages
AnthropicRequest,
AnthropicMessage,
AnthropicResponse,
# Open Responses
OpenResponsesRequest,
OpenResponsesResponse,
# ... and all nested types (content parts, tool calls, etc.)
)
These are standard @dataclass definitions generated from the Rust schemas.
See Typed payloads for usage examples.
License
MIT
Release files for martian-linguafranca 0.3.14
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| martian_linguafranca-0.3.14.tar.gz | 273.5 kB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| martian_linguafranca-0.3.14-cp310-abi3-win_amd64.whl | CPython 3.10 | abi3 | Windows x86-64 | Details |
| martian_linguafranca-0.3.14-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | CPython 3.10 | abi3 | Linux glibc 2.17+ x86-64 | Details |
| martian_linguafranca-0.3.14-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | CPython 3.10 | abi3 | Linux glibc 2.17+ ARM64 | Details |
| martian_linguafranca-0.3.14-cp310-abi3-macosx_11_0_arm64.whl | CPython 3.10 | abi3 | macOS 11.0+ ARM64 | Details |
| martian_linguafranca-0.3.14-cp310-abi3-macosx_10_12_x86_64.whl | CPython 3.10 | abi3 | macOS 10.12+ x86-64 | Details |
Total release size: 7.1 MB
Release files / martian_linguafranca-0.3.14.tar.gz
| Download URL | martian_linguafranca-0.3.14.tar.gz |
|---|---|
| Size | 273.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ece556b5224dc9335b3148cf7dd691a5bb61dac3a91cb4a71b0b88622355c65f
|
|
BLAKE2b-256 checksum How to use checksums |
63958e3025906c33399cb6be0b0ecb462263085bd1873f6628bca64e0d9c9007
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / martian_linguafranca-0.3.14-cp310-abi3-win_amd64.whl
| Download URL | martian_linguafranca-0.3.14-cp310-abi3-win_amd64.whl |
|---|---|
| Size | 1.5 MB |
| Tags | CPython 3.10 Windows x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
3761b3498fc932e35fdb34bd73eb06f39ad085bb340c52b6d6d01e27f729b8de
|
|
BLAKE2b-256 checksum How to use checksums |
8a2e9b66c58765c7e0ccc72805d547387243295e082668441ad68e41c709eea3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / martian_linguafranca-0.3.14-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | martian_linguafranca-0.3.14-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 1.4 MB |
| Tags | CPython 3.10 Linux glibc 2.17+ x86-64 abi3 |
|
SHA-256 checksum How to use checksums |
721d8010021ea83a028b02d58ea00f99d0a5144995a4140a7aef6c24e1fb96aa
|
|
BLAKE2b-256 checksum How to use checksums |
a88a16b62d8d6d9d551ab553da6e1b783020f8beaf7a67ffa53468ad950a4ef3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / martian_linguafranca-0.3.14-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | martian_linguafranca-0.3.14-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 1.3 MB |
| Tags | CPython 3.10 Linux glibc 2.17+ ARM64 abi3 |
|
SHA-256 checksum How to use checksums |
6157b82c4d39c6e382a6f4ade14d6dd8945009b4d2fb8d2c9125021a4c8c5e2a
|
|
BLAKE2b-256 checksum How to use checksums |
c6f5668217308b53f0186c160dc5eafacf79e4d904afece246a62cf632ea0e00
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / martian_linguafranca-0.3.14-cp310-abi3-macosx_11_0_arm64.whl
| Download URL | martian_linguafranca-0.3.14-cp310-abi3-macosx_11_0_arm64.whl |
|---|---|
| Size | 1.3 MB |
| Tags | CPython 3.10 abi3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
5aa630b44a3a14c024b7b6c2c5e57c7a4deca5473dd761c62f8ac72b48249942
|
|
BLAKE2b-256 checksum How to use checksums |
321633dbd4f47d03beea7c9e5744a2f338788f43173f877449c64608d0bf566e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / martian_linguafranca-0.3.14-cp310-abi3-macosx_10_12_x86_64.whl
| Download URL | martian_linguafranca-0.3.14-cp310-abi3-macosx_10_12_x86_64.whl |
|---|---|
| Size | 1.4 MB |
| Tags | CPython 3.10 abi3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
92701e858f1d282ffa69fc4b622a8fffc849ad18d8242cddebb37a15b5fddb62
|
|
BLAKE2b-256 checksum How to use checksums |
6a222ca80b348eaa6966a75f6d60e57cc671d5349f470b86213e0c8dbc980752
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|