Skip to main content

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,
)

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.types dataclasses (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_content is 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.

Namespaced tools

Open Responses namespace tools have no equivalent in Chat Completions or Anthropic Messages. Conversion to those formats flattens each tool in a namespace to {namespace}--{name}, prepends the namespace description to the tool description, and encodes calls from earlier turns the same way. Conversion back to Open Responses splits a tool name at its first -- into namespace and name, so clients that dispatch by namespace (for example Codex) receive the original identity. Anthropic tool search results are regrouped into namespace tools the same way.

  • Conversion fails if a namespace is empty, contains --, ends with -, or a flattened name collides with another tool.
  • A plain tool whose name contains -- produces a warning: calls to it are reported as namespaced in Open Responses output.
  • Flattened names longer than 128 characters produce a warning because targets may reject them.

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

Metadata

Release files for martian-linguafranca 0.3.15

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for martian-linguafranca 0.3.15
File Size Uploaded Provenance Actions
martian_linguafranca-0.3.15.tar.gz 282.8 kB Without provenance Details for martian_linguafranca-0.3.15.tar.gz

Built distributions (wheels)

Table of built distributions (wheels) for martian-linguafranca 0.3.15
File Actions
martian_linguafranca-0.3.15-cp310-abi3-win_amd64.whl CPython 3.10 abi3 Windows x86-64 Details for martian_linguafranca-0.3.15-cp310-abi3-win_amd64.whl
martian_linguafranca-0.3.15-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl CPython 3.10 abi3 Linux glibc 2.17+ x86-64 Details for martian_linguafranca-0.3.15-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
martian_linguafranca-0.3.15-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.10 abi3 Linux glibc 2.17+ ARM64 Details for martian_linguafranca-0.3.15-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
martian_linguafranca-0.3.15-cp310-abi3-macosx_11_0_arm64.whl CPython 3.10 abi3 macOS 11.0+ ARM64 Details for martian_linguafranca-0.3.15-cp310-abi3-macosx_11_0_arm64.whl
martian_linguafranca-0.3.15-cp310-abi3-macosx_10_12_x86_64.whl CPython 3.10 abi3 macOS 10.12+ x86-64 Details for martian_linguafranca-0.3.15-cp310-abi3-macosx_10_12_x86_64.whl

Total release size: 7.3 MB

Release files / martian_linguafranca-0.3.15.tar.gz

Download URL martian_linguafranca-0.3.15.tar.gz
Size 282.8 kB
Tags Source
SHA-256 checksum
How to use checksums
baa80d1a70b3afdd3859dd6707bd2b952ee42a84a3ad6842cc2b766c7f47f969
BLAKE2b-256 checksum
How to use checksums
c4e5083c25ab97f217c2806db53bf2923b985c1ec286aafe8849e2d69301a00d
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.15-cp310-abi3-win_amd64.whl

Download URL martian_linguafranca-0.3.15-cp310-abi3-win_amd64.whl
Size 1.5 MB
Tags CPython 3.10 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
6b9358bbbcb339989fb0aefed9522a0b1d961613d61733bc86075cf69d87307c
BLAKE2b-256 checksum
How to use checksums
7f3263961dc164472cb270a3b123a98ea87517b3e45df61d195f13dd3e1f2633
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.15-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL martian_linguafranca-0.3.15-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
4dc214cf57f9c7a04d9801be29cc80134c3070a19cb288bb06bf86cb0b3e5573
BLAKE2b-256 checksum
How to use checksums
799a7e3f38aed4e1be52f59c399df085b68a504217e14508e5cff6fcfb9a83d8
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.15-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL martian_linguafranca-0.3.15-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
919018c4cf762726f6cc0fbc3f826a405a4c60379131c01c142f099bcafa6e03
BLAKE2b-256 checksum
How to use checksums
db6e4b21e11ca79df11e2a37e5fd10475508c228b907c82a31c008af053d1297
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.15-cp310-abi3-macosx_11_0_arm64.whl

Download URL martian_linguafranca-0.3.15-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
a13fddddfb5a5a2bdd3944b8d0fe468f5d4c0c67b301752edd9b3e0ec81d2797
BLAKE2b-256 checksum
How to use checksums
e47d0395c6fdd8d23d50705458127ba11aaee29a4cc6e1e07e836bcba4dc60cd
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.15-cp310-abi3-macosx_10_12_x86_64.whl

Download URL martian_linguafranca-0.3.15-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
8f781d41a2227fdd7fae3bc526e12aaad5e44c03bfb5a8c3bed951e8b0cd031d
BLAKE2b-256 checksum
How to use checksums
15f01aeb60a0f1d2d0f0121ad1b198f024da322e982ce6c09030af0f4bc4c8c2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Security for martian-linguafranca 0.3.15

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.3.15 This release

6 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page