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.16

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.16
File Size Uploaded Provenance Actions
martian_linguafranca-0.3.16.tar.gz 288.1 kB Without provenance Details for martian_linguafranca-0.3.16.tar.gz

Built distributions (wheels)

Table of built distributions (wheels) for martian-linguafranca 0.3.16
File Actions
martian_linguafranca-0.3.16-cp310-abi3-win_amd64.whl CPython 3.10 abi3 Windows x86-64 Details for martian_linguafranca-0.3.16-cp310-abi3-win_amd64.whl
martian_linguafranca-0.3.16-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.16-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
martian_linguafranca-0.3.16-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl CPython 3.10 abi3 Linux glibc 2.17+ ARM64 Details for martian_linguafranca-0.3.16-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
martian_linguafranca-0.3.16-cp310-abi3-macosx_11_0_arm64.whl CPython 3.10 abi3 macOS 11.0+ ARM64 Details for martian_linguafranca-0.3.16-cp310-abi3-macosx_11_0_arm64.whl
martian_linguafranca-0.3.16-cp310-abi3-macosx_10_12_x86_64.whl CPython 3.10 abi3 macOS 10.12+ x86-64 Details for martian_linguafranca-0.3.16-cp310-abi3-macosx_10_12_x86_64.whl

Total release size: 7.4 MB

Release files / martian_linguafranca-0.3.16.tar.gz

Download URL martian_linguafranca-0.3.16.tar.gz
Size 288.1 kB
Tags Source
SHA-256 checksum
How to use checksums
84acef43b5abe1dde9af0a01c513087f4f7019f477917ce649fcd4e7c81a68d6
BLAKE2b-256 checksum
How to use checksums
9a1dd4c0ec9efd6c3df0fbb172827a27d6ffabbf5627d841188dfa68371e20df
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.16

Release files / martian_linguafranca-0.3.16-cp310-abi3-win_amd64.whl

Download URL martian_linguafranca-0.3.16-cp310-abi3-win_amd64.whl
Size 1.5 MB
Tags CPython 3.10 Windows x86-64 abi3
SHA-256 checksum
How to use checksums
a857384be118358dfc0ff2fed1f05420ec2a48ee09324d0107b174ea7cba8dc9
BLAKE2b-256 checksum
How to use checksums
74628b0ffcb06edd99c1735546e7ef99bec90b94144f958ae59ed9d4e5e5e076
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.16

Release files / martian_linguafranca-0.3.16-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl

Download URL martian_linguafranca-0.3.16-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Size 1.5 MB
Tags CPython 3.10 Linux glibc 2.17+ x86-64 abi3
SHA-256 checksum
How to use checksums
ed4f44fef9b1c190260f734725749918594eb5194b5e85860af405982d26750c
BLAKE2b-256 checksum
How to use checksums
ed60def21ba44d964b1f83d32c2d1f3669e68f9d59188f4402aaede6e252256d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.16

Release files / martian_linguafranca-0.3.16-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl

Download URL martian_linguafranca-0.3.16-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Size 1.4 MB
Tags CPython 3.10 Linux glibc 2.17+ ARM64 abi3
SHA-256 checksum
How to use checksums
6ede47680b41f8b0fe25458beba802c32c1eb7119a6bfb97939c7da9b7e64270
BLAKE2b-256 checksum
How to use checksums
1802c60a2e9cad6fd7c74ecd9de450db090e008602087b5df66f09a42a5f13a8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.16

Release files / martian_linguafranca-0.3.16-cp310-abi3-macosx_11_0_arm64.whl

Download URL martian_linguafranca-0.3.16-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
cf02c1bdedb50ea27dc1936e9d895ab29f462269cc355ebc0b7408ba77bf775a
BLAKE2b-256 checksum
How to use checksums
fbf794fb75e3a07fddb7e75762eefbea01628fb4368985215353f380a875dfcc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.16

Release files / martian_linguafranca-0.3.16-cp310-abi3-macosx_10_12_x86_64.whl

Download URL martian_linguafranca-0.3.16-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
5868f27a2f735e2fd698a0b98af8ec34bc7815c1e10d7528fdcfcf4deef6f8e2
BLAKE2b-256 checksum
How to use checksums
709383a2c43fd06d733ea57301a41c0852dc5973c1dc3dd71dceb9a131de4236
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.16

Security for martian-linguafranca 0.3.16

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.16 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