Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ big_serde_json = { path = "serde_json" }
serde_json = "1.0"
serde_yaml = "0.9"
serde_with = "3.14.1"
ts-rs = { version = "11.0", features = ["serde-compat"] }
ts-rs = { version = "11.0", features = ["serde-compat", "no-serde-warnings"] }
anyhow = "1.0"
thiserror = "2.0"
assert-json-diff = "2.0.2"
Expand Down
227 changes: 217 additions & 10 deletions bindings/python/src/lingua/__init__.pyi
Original file line number Diff line number Diff line change
@@ -1,6 +1,118 @@
"""Type stubs for Lingua Python bindings"""

from typing import Any
from typing import Any, Dict, List, Literal, Optional
from typing_extensions import TypedDict


# ============================================================================
# Provider format
# ============================================================================

ProviderFormat = Literal["openai", "anthropic", "google", "mistral", "converse", "responses", "unknown"]


# ============================================================================
# Enums as Literal types
# ============================================================================

SummaryMode = Literal["none", "auto", "detailed"]
ToolChoiceMode = Literal["auto", "none", "required", "tool"]
ResponseFormatType = Literal["text", "json_object", "json_schema"]
ReasoningEffort = Literal["low", "medium", "high"]
ReasoningCanonical = Literal["effort", "budget_tokens"]


# ============================================================================
# Config types
# ============================================================================

class ReasoningConfig(TypedDict, total=False):
"""Configuration for extended thinking / reasoning capabilities."""
enabled: Optional[bool]
effort: Optional[ReasoningEffort]
budget_tokens: Optional[int]
canonical: Optional[ReasoningCanonical]
summary: Optional[SummaryMode]


class ToolChoiceConfig(TypedDict, total=False):
"""Tool selection strategy configuration."""
mode: Optional[ToolChoiceMode]
tool_name: Optional[str]
disable_parallel: Optional[bool]


class JsonSchemaConfig(TypedDict, total=False):
"""JSON schema configuration for structured output."""
name: str
schema: Dict[str, Any]
strict: Optional[bool]
description: Optional[str]


class ResponseFormatConfig(TypedDict, total=False):
"""Response format configuration for structured output."""
format_type: Optional[ResponseFormatType]
json_schema: Optional[JsonSchemaConfig]


class UniversalTool(TypedDict, total=False):
"""A tool definition in universal format."""
name: str
description: Optional[str]
parameters: Optional[Dict[str, Any]]
strict: Optional[bool]
kind: Literal["function", "builtin"]
# For builtin tools:
provider: Optional[str]
builtin_type: Optional[str]
config: Optional[Dict[str, Any]]


class UniversalParams(TypedDict, total=False):
"""Common request parameters across providers."""
temperature: Optional[float]
top_p: Optional[float]
top_k: Optional[int]
seed: Optional[int]
presence_penalty: Optional[float]
frequency_penalty: Optional[float]
max_tokens: Optional[int]
stop: Optional[List[str]]
logprobs: Optional[bool]
top_logprobs: Optional[int]
tools: Optional[List[UniversalTool]]
tool_choice: Optional[ToolChoiceConfig]
parallel_tool_calls: Optional[bool]
response_format: Optional[ResponseFormatConfig]
reasoning: Optional[ReasoningConfig]
metadata: Optional[Dict[str, Any]]
store: Optional[bool]
service_tier: Optional[str]
stream: Optional[bool]


class UniversalRequest(TypedDict, total=False):
"""Universal request envelope for LLM API calls."""
model: Optional[str]
messages: List[Any]
params: UniversalParams


# ============================================================================
# Message types
# ============================================================================

class TextContentPart(TypedDict):
"""Text content part."""
type: Literal["text"]
text: str


class Message(TypedDict, total=False):
"""A message in universal format."""
role: Literal["system", "user", "assistant", "tool"]
content: Any


# ============================================================================
Expand All @@ -16,51 +128,119 @@ class ConversionError(Exception):
# Chat Completions API conversions
# ============================================================================

def chat_completions_messages_to_lingua(messages: list) -> list:
def chat_completions_messages_to_lingua(messages: List[Any]) -> List[Message]:
"""Convert array of Chat Completions messages to Lingua Messages."""
...


def lingua_to_chat_completions_messages(messages: list) -> list:
def lingua_to_chat_completions_messages(messages: List[Message]) -> List[Any]:
"""Convert array of Lingua Messages to Chat Completions messages."""
...


# ============================================================================
# Responses API conversions
# ============================================================================

def responses_messages_to_lingua(messages: list) -> list:
def responses_messages_to_lingua(messages: List[Any]) -> List[Message]:
"""Convert array of Responses API messages to Lingua Messages."""
...


def lingua_to_responses_messages(messages: list) -> list:
def lingua_to_responses_messages(messages: List[Message]) -> List[Any]:
"""Convert array of Lingua Messages to Responses API messages."""
...


# ============================================================================
# Anthropic conversions
# ============================================================================

def anthropic_messages_to_lingua(messages: list) -> list:
def anthropic_messages_to_lingua(messages: List[Any]) -> List[Message]:
"""Convert array of Anthropic messages to Lingua Messages."""
...


def lingua_to_anthropic_messages(messages: list) -> list:
def lingua_to_anthropic_messages(messages: List[Message]) -> List[Any]:
"""Convert array of Lingua Messages to Anthropic messages."""
...


# ============================================================================
# Processing functions
# ============================================================================

def deduplicate_messages(messages: list) -> list:
def deduplicate_messages(messages: List[Message]) -> List[Message]:
"""Deduplicate messages based on role and content."""
...


def import_messages_from_spans(spans: list) -> list:
def import_messages_from_spans(spans: List[Any]) -> List[Message]:
"""Import messages from spans."""
...


def import_and_deduplicate_messages(spans: list) -> list:
def import_and_deduplicate_messages(spans: List[Any]) -> List[Message]:
"""Import and deduplicate messages from spans in a single operation."""
...


# ============================================================================
# Transform functions
# ============================================================================

class TransformPassThroughResult(TypedDict):
"""Result when payload was already valid for target format."""
pass_through: Literal[True]
data: Any


class TransformTransformedResult(TypedDict):
"""Result when payload was transformed to target format."""
transformed: Literal[True]
data: Any
source_format: str


TransformResult = TransformPassThroughResult | TransformTransformedResult


def transform_request(
json: str,
target_format: ProviderFormat,
model: Optional[str] = None,
) -> TransformResult:
"""Transform a request payload to the target format.

Takes a JSON string and target format, auto-detects the source format,
and transforms to the target format.

Returns a dict with either:
- `{ "pass_through": True, "data": ... }` if payload is already valid for target
- `{ "transformed": True, "data": ..., "source_format": "..." }` if transformed
"""
...


def transform_response(json: str, target_format: ProviderFormat) -> TransformResult:
"""Transform a response payload from one format to another.

Takes a JSON string and target format, auto-detects the source format,
and transforms to the target format.

Returns a dict with either:
- `{ "pass_through": True, "data": ... }` if payload is already valid for target
- `{ "transformed": True, "data": ..., "source_format": "..." }` if transformed
"""
...


def extract_model(json: str) -> Optional[str]:
"""Extract model name from request without full transformation.

This is a fast path for routing decisions that only need the model name.
Returns the model string if found, or None if not present.
"""
...


Expand Down Expand Up @@ -93,16 +273,43 @@ def validate_anthropic_response(json_str: str) -> Any:
# ============================================================================

__all__ = [
# Error types
"ConversionError",
# Type definitions
"ProviderFormat",
"SummaryMode",
"ToolChoiceMode",
"ResponseFormatType",
"ReasoningEffort",
"ReasoningCanonical",
"ReasoningConfig",
"ToolChoiceConfig",
"JsonSchemaConfig",
"ResponseFormatConfig",
"UniversalTool",
"UniversalParams",
"UniversalRequest",
"Message",
"TextContentPart",
"TransformResult",
"TransformPassThroughResult",
"TransformTransformedResult",
# Conversion functions
"chat_completions_messages_to_lingua",
"lingua_to_chat_completions_messages",
"responses_messages_to_lingua",
"lingua_to_responses_messages",
"anthropic_messages_to_lingua",
"lingua_to_anthropic_messages",
# Processing functions
"deduplicate_messages",
"import_messages_from_spans",
"import_and_deduplicate_messages",
# Transform functions
"transform_request",
"transform_response",
"extract_model",
# Validation functions
"validate_openai_request",
"validate_openai_response",
"validate_anthropic_request",
Expand Down
22 changes: 22 additions & 0 deletions bindings/typescript/src/generated/JsonSchemaConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

/**
* JSON schema configuration for structured output.
*/
export type JsonSchemaConfig = {
/**
* Schema name (required by OpenAI)
*/
name: string,
/**
* The JSON schema definition
*/
schema: Record<string, unknown>,
/**
* Whether to enable strict schema validation
*/
strict: boolean | null,
/**
* Human-readable description of the schema
*/
description: string | null, };
12 changes: 12 additions & 0 deletions bindings/typescript/src/generated/ProviderFormat.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

/**
* Represents the API format/protocol used by an LLM provider.
*
* This enum is the single source of truth for provider formats across the ecosystem.
* When adding a new provider format:
* 1. Add a variant here
* 2. Update detection heuristics in `processing/detect.rs`
* 3. Add conversion logic in `providers/<name>/convert.rs` if needed
*/
export type ProviderFormat = "openai" | "anthropic" | "google" | "mistral" | "converse" | "responses" | "unknown";
9 changes: 9 additions & 0 deletions bindings/typescript/src/generated/ReasoningCanonical.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

/**
* Indicates which field is the canonical source of truth for reasoning configuration.
*
* When converting between providers, both `effort` and `budget_tokens` are always populated
* (one derived from the other). This field indicates which was the original source.
*/
export type ReasoningCanonical = "effort" | "budget_tokens";
38 changes: 38 additions & 0 deletions bindings/typescript/src/generated/ReasoningConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { ReasoningCanonical } from "./ReasoningCanonical";
import type { ReasoningEffort } from "./ReasoningEffort";
import type { SummaryMode } from "./SummaryMode";

/**
* Configuration for extended thinking / reasoning capabilities.
*
* Both `effort` and `budget_tokens` are always populated when reasoning is enabled.
* The `canonical` field indicates which was the original source of truth:
* - `Effort`: From OpenAI (effort is canonical, budget_tokens derived)
* - `BudgetTokens`: From Anthropic/Google (budget_tokens is canonical, effort derived)
*/
export type ReasoningConfig = {
/**
* Whether reasoning/thinking is enabled.
*/
enabled: boolean | null,
/**
* Reasoning effort level (low/medium/high).
* Always populated when enabled. Used by OpenAI Chat/Responses API.
*/
effort: ReasoningEffort | null,
/**
* Token budget for thinking.
* Always populated when enabled. Used by Anthropic/Google.
*/
budget_tokens: bigint | null,
/**
* Which field is the canonical source of truth.
* Indicates whether `effort` or `budget_tokens` was the original value.
*/
canonical: ReasoningCanonical | null,
/**
* Summary mode for reasoning output.
* Maps to OpenAI Responses API's `reasoning.summary` field.
*/
summary: SummaryMode | null, };
6 changes: 6 additions & 0 deletions bindings/typescript/src/generated/ReasoningEffort.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.

/**
* Reasoning effort level (portable across providers).
*/
export type ReasoningEffort = "low" | "medium" | "high";
Loading