From 5133b4d5ba1ebb6f92f8784bf230a3077d7d8adc Mon Sep 17 00:00:00 2001 From: Ken Jiang Date: Tue, 27 Jan 2026 20:32:56 -0500 Subject: [PATCH] request typescript and python bindings --- Cargo.toml | 2 +- bindings/python/src/lingua/__init__.pyi | 227 +++++++++++++++++- .../src/generated/JsonSchemaConfig.ts | 22 ++ .../src/generated/ProviderFormat.ts | 12 + .../src/generated/ReasoningCanonical.ts | 9 + .../src/generated/ReasoningConfig.ts | 38 +++ .../src/generated/ReasoningEffort.ts | 6 + .../src/generated/ResponseFormatConfig.ts | 22 ++ .../src/generated/ResponseFormatType.ts | 6 + .../typescript/src/generated/SummaryMode.ts | 6 + .../src/generated/ToolChoiceConfig.ts | 28 +++ .../src/generated/ToolChoiceMode.ts | 6 + .../src/generated/UniversalParams.ts | 127 ++++++++++ .../src/generated/UniversalRequest.ts | 22 ++ .../typescript/src/generated/UniversalTool.ts | 37 +++ .../src/generated/UniversalToolType.ts | 18 ++ bindings/typescript/src/index.ts | 20 ++ crates/lingua/src/capabilities/format.rs | 4 +- crates/lingua/src/python.rs | 139 +++++++++++ crates/lingua/src/universal/request.rs | 37 ++- crates/lingua/src/universal/tools.rs | 9 +- crates/lingua/src/wasm.rs | 115 +++++++++ examples/python/example.py | 147 ++++++++++++ examples/typescript/index.ts | 93 +++++++ 24 files changed, 1127 insertions(+), 25 deletions(-) create mode 100644 bindings/typescript/src/generated/JsonSchemaConfig.ts create mode 100644 bindings/typescript/src/generated/ProviderFormat.ts create mode 100644 bindings/typescript/src/generated/ReasoningCanonical.ts create mode 100644 bindings/typescript/src/generated/ReasoningConfig.ts create mode 100644 bindings/typescript/src/generated/ReasoningEffort.ts create mode 100644 bindings/typescript/src/generated/ResponseFormatConfig.ts create mode 100644 bindings/typescript/src/generated/ResponseFormatType.ts create mode 100644 bindings/typescript/src/generated/SummaryMode.ts create mode 100644 bindings/typescript/src/generated/ToolChoiceConfig.ts create mode 100644 bindings/typescript/src/generated/ToolChoiceMode.ts create mode 100644 bindings/typescript/src/generated/UniversalParams.ts create mode 100644 bindings/typescript/src/generated/UniversalRequest.ts create mode 100644 bindings/typescript/src/generated/UniversalTool.ts create mode 100644 bindings/typescript/src/generated/UniversalToolType.ts create mode 100644 examples/python/example.py diff --git a/Cargo.toml b/Cargo.toml index 053616e3..b294b54c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/bindings/python/src/lingua/__init__.pyi b/bindings/python/src/lingua/__init__.pyi index 62712c5d..415b255a 100644 --- a/bindings/python/src/lingua/__init__.pyi +++ b/bindings/python/src/lingua/__init__.pyi @@ -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 # ============================================================================ @@ -16,11 +128,13 @@ 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.""" ... @@ -28,11 +142,13 @@ def lingua_to_chat_completions_messages(messages: list) -> list: # 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.""" ... @@ -40,11 +156,13 @@ def lingua_to_responses_messages(messages: list) -> list: # 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.""" ... @@ -52,15 +170,77 @@ def lingua_to_anthropic_messages(messages: list) -> list: # 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. + """ ... @@ -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", diff --git a/bindings/typescript/src/generated/JsonSchemaConfig.ts b/bindings/typescript/src/generated/JsonSchemaConfig.ts new file mode 100644 index 00000000..dd123e33 --- /dev/null +++ b/bindings/typescript/src/generated/JsonSchemaConfig.ts @@ -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, +/** + * Whether to enable strict schema validation + */ +strict: boolean | null, +/** + * Human-readable description of the schema + */ +description: string | null, }; diff --git a/bindings/typescript/src/generated/ProviderFormat.ts b/bindings/typescript/src/generated/ProviderFormat.ts new file mode 100644 index 00000000..79d0809e --- /dev/null +++ b/bindings/typescript/src/generated/ProviderFormat.ts @@ -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//convert.rs` if needed + */ +export type ProviderFormat = "openai" | "anthropic" | "google" | "mistral" | "converse" | "responses" | "unknown"; diff --git a/bindings/typescript/src/generated/ReasoningCanonical.ts b/bindings/typescript/src/generated/ReasoningCanonical.ts new file mode 100644 index 00000000..02e93a3b --- /dev/null +++ b/bindings/typescript/src/generated/ReasoningCanonical.ts @@ -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"; diff --git a/bindings/typescript/src/generated/ReasoningConfig.ts b/bindings/typescript/src/generated/ReasoningConfig.ts new file mode 100644 index 00000000..60ea5d1a --- /dev/null +++ b/bindings/typescript/src/generated/ReasoningConfig.ts @@ -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, }; diff --git a/bindings/typescript/src/generated/ReasoningEffort.ts b/bindings/typescript/src/generated/ReasoningEffort.ts new file mode 100644 index 00000000..acb07bb6 --- /dev/null +++ b/bindings/typescript/src/generated/ReasoningEffort.ts @@ -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"; diff --git a/bindings/typescript/src/generated/ResponseFormatConfig.ts b/bindings/typescript/src/generated/ResponseFormatConfig.ts new file mode 100644 index 00000000..f0ba2bde --- /dev/null +++ b/bindings/typescript/src/generated/ResponseFormatConfig.ts @@ -0,0 +1,22 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonSchemaConfig } from "./JsonSchemaConfig"; +import type { ResponseFormatType } from "./ResponseFormatType"; + +/** + * Response format configuration for structured output. + * + * Provider mapping: + * - OpenAI Chat: `{ type: "text" | "json_object" | "json_schema", json_schema? }` + * - OpenAI Responses: nested under `text.format` + * - Google: `response_mime_type` + `response_schema` + * - Anthropic: `{ type: "json_schema", schema, name?, strict?, description? }` + */ +export type ResponseFormatConfig = { +/** + * Output format type + */ +format_type: ResponseFormatType | null, +/** + * JSON schema configuration (when format_type = JsonSchema) + */ +json_schema: JsonSchemaConfig | null, }; diff --git a/bindings/typescript/src/generated/ResponseFormatType.ts b/bindings/typescript/src/generated/ResponseFormatType.ts new file mode 100644 index 00000000..4244af09 --- /dev/null +++ b/bindings/typescript/src/generated/ResponseFormatType.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Response format type (portable across providers). + */ +export type ResponseFormatType = "Text" | "JsonObject" | "JsonSchema"; diff --git a/bindings/typescript/src/generated/SummaryMode.ts b/bindings/typescript/src/generated/SummaryMode.ts new file mode 100644 index 00000000..b01b29a8 --- /dev/null +++ b/bindings/typescript/src/generated/SummaryMode.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Summary mode for reasoning output. + */ +export type SummaryMode = "None" | "Auto" | "Detailed"; diff --git a/bindings/typescript/src/generated/ToolChoiceConfig.ts b/bindings/typescript/src/generated/ToolChoiceConfig.ts new file mode 100644 index 00000000..73177e3c --- /dev/null +++ b/bindings/typescript/src/generated/ToolChoiceConfig.ts @@ -0,0 +1,28 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ToolChoiceMode } from "./ToolChoiceMode"; + +/** + * Tool selection strategy configuration. + * + * Uses canonical fields (`mode`, `tool_name`) for cross-provider conversion. + * + * Provider mapping: + * - OpenAI Chat: `"auto"` | `"none"` | `"required"` | `{ type: "function", function: { name } }` + * - OpenAI Responses: `"auto"` | `{ type: "function", name }` + * - Anthropic: `{ type: "auto" | "any" | "none" | "tool", name?, disable_parallel_tool_use? }` + */ +export type ToolChoiceConfig = { +/** + * Selection mode - the semantic intent of the tool choice + */ +mode: ToolChoiceMode | null, +/** + * Specific tool name (when mode = Tool) + */ +tool_name: string | null, +/** + * Whether to disable parallel tool calls. + * Maps to Anthropic's `disable_parallel_tool_use` field. + * For OpenAI, this is handled via the separate `parallel_tool_calls` param. + */ +disable_parallel: boolean | null, }; diff --git a/bindings/typescript/src/generated/ToolChoiceMode.ts b/bindings/typescript/src/generated/ToolChoiceMode.ts new file mode 100644 index 00000000..1e808e83 --- /dev/null +++ b/bindings/typescript/src/generated/ToolChoiceMode.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Tool selection mode (portable across providers). + */ +export type ToolChoiceMode = "Auto" | "None" | "Required" | "Tool"; diff --git a/bindings/typescript/src/generated/UniversalParams.ts b/bindings/typescript/src/generated/UniversalParams.ts new file mode 100644 index 00000000..b7dc4faf --- /dev/null +++ b/bindings/typescript/src/generated/UniversalParams.ts @@ -0,0 +1,127 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ReasoningConfig } from "./ReasoningConfig"; +import type { ResponseFormatConfig } from "./ResponseFormatConfig"; +import type { ToolChoiceConfig } from "./ToolChoiceConfig"; +import type { UniversalTool } from "./UniversalTool"; + +/** + * Common request parameters across providers. + * + * Uses canonical names - adapters handle mapping to provider-specific names. + * Provider-specific fields without canonical mappings are stored in `extras`. + */ +export type UniversalParams = { +/** + * Controls randomness: 0 = deterministic, 2 = maximum randomness. + * + * **Providers:** OpenAI, Anthropic, Google (`generationConfig.temperature`), Bedrock (`inferenceConfig.temperature`) + */ +temperature: number | null, +/** + * Nucleus sampling: only consider tokens with cumulative probability ≤ top_p. + * + * **Providers:** OpenAI, Anthropic, Google (`generationConfig.topP`), Bedrock (`inferenceConfig.topP`) + */ +top_p: number | null, +/** + * Only sample from the top K most likely tokens. + * + * **Providers:** Anthropic, Google (`generationConfig.topK`) + */ +top_k: bigint | null, +/** + * Random seed for deterministic generation. + * + * **Providers:** OpenAI + */ +seed: bigint | null, +/** + * Penalize tokens based on whether they've appeared at all (-2.0 to 2.0). + * + * **Providers:** OpenAI + */ +presence_penalty: number | null, +/** + * Penalize tokens based on how often they've appeared (-2.0 to 2.0). + * + * **Providers:** OpenAI + */ +frequency_penalty: number | null, +/** + * Maximum tokens to generate in the response. + * + * **Providers:** OpenAI (`max_completion_tokens`), Anthropic, Google (`generationConfig.maxOutputTokens`), Bedrock (`inferenceConfig.maxTokens`) + */ +max_tokens: bigint | null, +/** + * Sequences that stop generation when encountered. + * + * **Providers:** OpenAI, Anthropic (`stop_sequences`), Google (`generationConfig.stopSequences`), Bedrock (`inferenceConfig.stopSequences`) + */ +stop: Array | null, +/** + * Return log probabilities of output tokens. + * + * **Providers:** OpenAI + */ +logprobs: boolean | null, +/** + * Number of most likely tokens to return log probabilities for (0-20). + * + * **Providers:** OpenAI + */ +top_logprobs: bigint | null, +/** + * Tool/function definitions the model can call. + * + * **Providers:** OpenAI, Anthropic, Google (`tools[].functionDeclarations`), Bedrock (`toolConfig.tools[].toolSpec`) + */ +tools: Array | null, +/** + * How the model should choose which tool to call. + * + * **Providers:** OpenAI, Anthropic + */ +tool_choice: ToolChoiceConfig | null, +/** + * Allow multiple tool calls in a single response. + * + * **Providers:** OpenAI, Anthropic (`tool_choice.disable_parallel_tool_use`) + */ +parallel_tool_calls: boolean | null, +/** + * Constrain output format (text, JSON, or JSON schema). + * + * **Providers:** OpenAI, Anthropic (`output_format`) + */ +response_format: ResponseFormatConfig | null, +/** + * Enable extended thinking / chain-of-thought reasoning. + * + * **Providers:** OpenAI (`reasoning_effort`), Anthropic (`thinking`), Google (`generationConfig.thinkingConfig`), Bedrock (`additionalModelRequestFields.thinking`) + */ +reasoning: ReasoningConfig | null, +/** + * Key-value metadata attached to the request. + * + * **Providers:** OpenAI, Anthropic (only `user_id`) + */ +metadata: Record | null, +/** + * Store the completion for later use in fine-tuning or evals. + * + * **Providers:** OpenAI + */ +store: boolean | null, +/** + * Request priority tier (e.g., "auto", "default"). + * + * **Providers:** OpenAI, Anthropic + */ +service_tier: string | null, +/** + * Stream the response as server-sent events. + * + * **Providers:** OpenAI, Anthropic + */ +stream: boolean | null, }; diff --git a/bindings/typescript/src/generated/UniversalRequest.ts b/bindings/typescript/src/generated/UniversalRequest.ts new file mode 100644 index 00000000..22ae2060 --- /dev/null +++ b/bindings/typescript/src/generated/UniversalRequest.ts @@ -0,0 +1,22 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { Message } from "./Message"; +import type { UniversalParams } from "./UniversalParams"; + +/** + * Universal request envelope for LLM API calls. + * + * This type captures the common structure across all provider request formats. + */ +export type UniversalRequest = { +/** + * Model identifier (may be None for providers that use endpoint-based model selection) + */ +model: string | null, +/** + * Conversation messages in universal format + */ +messages: Array, +/** + * Request parameters (canonical fields + provider-specific extras) + */ +params: UniversalParams, }; diff --git a/bindings/typescript/src/generated/UniversalTool.ts b/bindings/typescript/src/generated/UniversalTool.ts new file mode 100644 index 00000000..f958eb0c --- /dev/null +++ b/bindings/typescript/src/generated/UniversalTool.ts @@ -0,0 +1,37 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * A tool definition in universal format. + * + * This provides a typed representation that normalizes the different tool formats + * across providers (Anthropic, OpenAI Chat, OpenAI Responses API, etc.). + */ +export type UniversalTool = { +/** + * Tool name (required for all tool types) + */ +name: string, +/** + * Tool description + */ +description: string | null, +/** + * Parameters/input schema (JSON Schema) + */ +parameters: Record | null, +/** + * Whether to enforce strict schema validation (OpenAI Responses API) + */ +strict: boolean | null, } & ({ "kind": "function" } | { "kind": "builtin", +/** + * Provider identifier (e.g., "anthropic", "openai_responses") + */ +provider: string, +/** + * Original type name (e.g., "bash_20250124", "code_interpreter") + */ +builtin_type: string, +/** + * Provider-specific configuration + */ +config: Record | null, }); diff --git a/bindings/typescript/src/generated/UniversalToolType.ts b/bindings/typescript/src/generated/UniversalToolType.ts new file mode 100644 index 00000000..c4f194cb --- /dev/null +++ b/bindings/typescript/src/generated/UniversalToolType.ts @@ -0,0 +1,18 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +/** + * Classification of tool types. + */ +export type UniversalToolType = { "kind": "function" } | { "kind": "builtin", +/** + * Provider identifier (e.g., "anthropic", "openai_responses") + */ +provider: string, +/** + * Original type name (e.g., "bash_20250124", "code_interpreter") + */ +builtin_type: string, +/** + * Provider-specific configuration + */ +config: Record | null, }; diff --git a/bindings/typescript/src/index.ts b/bindings/typescript/src/index.ts index e27ea356..0063accc 100644 --- a/bindings/typescript/src/index.ts +++ b/bindings/typescript/src/index.ts @@ -33,6 +33,26 @@ export * from "./generated/ToolResultResponsePart"; export * from "./generated/UserContent"; export * from "./generated/UserContentPart"; +// Universal request types +export * from "./generated/UniversalRequest"; +export * from "./generated/UniversalParams"; +export * from "./generated/ProviderFormat"; + +// Configuration types +export * from "./generated/ReasoningConfig"; +export * from "./generated/ReasoningEffort"; +export * from "./generated/ReasoningCanonical"; +export * from "./generated/SummaryMode"; +export * from "./generated/ToolChoiceConfig"; +export * from "./generated/ToolChoiceMode"; +export * from "./generated/ResponseFormatConfig"; +export * from "./generated/ResponseFormatType"; +export * from "./generated/JsonSchemaConfig"; + +// Tool types +export * from "./generated/UniversalTool"; +export * from "./generated/UniversalToolType"; + // Main type aliases for convenience export type { Message } from "./generated/Message"; diff --git a/crates/lingua/src/capabilities/format.rs b/crates/lingua/src/capabilities/format.rs index 4c703845..46f84268 100644 --- a/crates/lingua/src/capabilities/format.rs +++ b/crates/lingua/src/capabilities/format.rs @@ -5,6 +5,7 @@ This enum represents the different LLM provider API formats that lingua can hand */ use serde::{Deserialize, Serialize}; +use ts_rs::TS; /// Represents the API format/protocol used by an LLM provider. /// @@ -13,7 +14,8 @@ use serde::{Deserialize, Serialize}; /// 1. Add a variant here /// 2. Update detection heuristics in `processing/detect.rs` /// 3. Add conversion logic in `providers//convert.rs` if needed -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default, TS)] +#[ts(export)] #[serde(rename_all = "lowercase")] pub enum ProviderFormat { /// OpenAI Chat Completions API format (also used by OpenAI-compatible providers) diff --git a/crates/lingua/src/python.rs b/crates/lingua/src/python.rs index 7e25e702..9dc05c20 100644 --- a/crates/lingua/src/python.rs +++ b/crates/lingua/src/python.rs @@ -240,6 +240,140 @@ fn validate_anthropic_response(py: Python, json: &str) -> PyResult { rust_to_py(py, &result) } +// ============================================================================ +// Transform functions +// ============================================================================ + +/// 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 +#[pyfunction] +#[pyo3(signature = (json, target_format, model=None))] +fn transform_request( + py: Python, + json: &str, + target_format: &str, + model: Option, +) -> PyResult { + use crate::capabilities::ProviderFormat; + use crate::processing::transform::{transform_request as transform, TransformResult}; + use bytes::Bytes; + + let target: ProviderFormat = target_format.parse().map_err(|_| { + PyErr::new::(format!( + "Unknown target format: {}", + target_format + )) + })?; + + let input_bytes = Bytes::from(json.to_owned()); + let result = transform(input_bytes, target, model.as_deref()) + .map_err(|e| PyErr::new::(e.to_string()))?; + + match result { + TransformResult::PassThrough(bytes) => { + let data: serde_json::Value = serde_json::from_slice(&bytes).map_err(|e| { + PyErr::new::(format!( + "Failed to parse result: {}", + e + )) + })?; + let dict = pyo3::types::PyDict::new(py); + dict.set_item("pass_through", true)?; + dict.set_item("data", rust_to_py(py, &data)?)?; + Ok(dict.into()) + } + TransformResult::Transformed { + bytes, + source_format, + } => { + let data: serde_json::Value = serde_json::from_slice(&bytes).map_err(|e| { + PyErr::new::(format!( + "Failed to parse result: {}", + e + )) + })?; + let dict = pyo3::types::PyDict::new(py); + dict.set_item("transformed", true)?; + dict.set_item("data", rust_to_py(py, &data)?)?; + dict.set_item("source_format", source_format.to_string())?; + Ok(dict.into()) + } + } +} + +/// 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 +#[pyfunction] +fn transform_response(py: Python, json: &str, target_format: &str) -> PyResult { + use crate::capabilities::ProviderFormat; + use crate::processing::transform::{transform_response as transform, TransformResult}; + use bytes::Bytes; + + let target: ProviderFormat = target_format.parse().map_err(|_| { + PyErr::new::(format!( + "Unknown target format: {}", + target_format + )) + })?; + + let input_bytes = Bytes::from(json.to_owned()); + let result = transform(input_bytes, target) + .map_err(|e| PyErr::new::(e.to_string()))?; + + match result { + TransformResult::PassThrough(bytes) => { + let data: serde_json::Value = serde_json::from_slice(&bytes).map_err(|e| { + PyErr::new::(format!( + "Failed to parse result: {}", + e + )) + })?; + let dict = pyo3::types::PyDict::new(py); + dict.set_item("pass_through", true)?; + dict.set_item("data", rust_to_py(py, &data)?)?; + Ok(dict.into()) + } + TransformResult::Transformed { + bytes, + source_format, + } => { + let data: serde_json::Value = serde_json::from_slice(&bytes).map_err(|e| { + PyErr::new::(format!( + "Failed to parse result: {}", + e + )) + })?; + let dict = pyo3::types::PyDict::new(py); + dict.set_item("transformed", true)?; + dict.set_item("data", rust_to_py(py, &data)?)?; + dict.set_item("source_format", source_format.to_string())?; + Ok(dict.into()) + } + } +} + +/// 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. +#[pyfunction] +fn extract_model(json: &str) -> Option { + use crate::processing::transform::extract_model as extract; + extract(json.as_bytes()) +} + // ============================================================================ // Python module definition // ============================================================================ @@ -277,5 +411,10 @@ fn _lingua(_py: Python, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(validate_anthropic_response, m)?)?; } + // Transform functions + m.add_function(wrap_pyfunction!(transform_request, m)?)?; + m.add_function(wrap_pyfunction!(transform_response, m)?)?; + m.add_function(wrap_pyfunction!(extract_model, m)?)?; + Ok(()) } diff --git a/crates/lingua/src/universal/request.rs b/crates/lingua/src/universal/request.rs index 837e14af..a9a8e330 100644 --- a/crates/lingua/src/universal/request.rs +++ b/crates/lingua/src/universal/request.rs @@ -34,6 +34,7 @@ use std::fmt; use std::str::FromStr; use serde::{Deserialize, Serialize}; +use ts_rs::TS; use crate::capabilities::ProviderFormat; use crate::error::ConvertError; @@ -44,7 +45,8 @@ use crate::universal::tools::UniversalTool; /// Universal request envelope for LLM API calls. /// /// This type captures the common structure across all provider request formats. -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, TS)] +#[ts(export)] pub struct UniversalRequest { /// Model identifier (may be None for providers that use endpoint-based model selection) pub model: Option, @@ -60,7 +62,8 @@ pub struct UniversalRequest { /// /// Uses canonical names - adapters handle mapping to provider-specific names. /// Provider-specific fields without canonical mappings are stored in `extras`. -#[derive(Debug, Clone, Default, Serialize)] +#[derive(Debug, Clone, Default, Serialize, TS)] +#[ts(export)] pub struct UniversalParams { // === Sampling parameters === /// Controls randomness: 0 = deterministic, 2 = maximum randomness. @@ -147,6 +150,7 @@ pub struct UniversalParams { /// Key-value metadata attached to the request. /// /// **Providers:** OpenAI, Anthropic (only `user_id`) + #[ts(type = "Record | null")] pub metadata: Option, /// Store the completion for later use in fine-tuning or evals. @@ -171,6 +175,7 @@ pub struct UniversalParams { /// Keyed by source `ProviderFormat` - only restored when converting back to /// the same provider (no cross-provider contamination). #[serde(skip)] + #[ts(skip)] pub extras: HashMap>, } @@ -229,7 +234,8 @@ impl UniversalParams { /// 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) -#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] +#[ts(export)] pub struct ReasoningConfig { /// Whether reasoning/thinking is enabled. #[serde(skip_serializing_if = "Option::is_none")] @@ -282,8 +288,9 @@ fn reasoning_should_skip(reasoning: &Option) -> bool { } /// Reasoning effort level (portable across providers). -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)] #[serde(rename_all = "lowercase")] +#[ts(export)] pub enum ReasoningEffort { Low, Medium, @@ -333,8 +340,9 @@ impl AsRef for ReasoningEffort { /// /// 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. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)] #[serde(rename_all = "snake_case")] +#[ts(export)] pub enum ReasoningCanonical { /// `effort` is the source of truth (from OpenAI Chat/Responses API) Effort, @@ -343,7 +351,8 @@ pub enum ReasoningCanonical { } /// Summary mode for reasoning output. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, TS)] +#[ts(export)] pub enum SummaryMode { /// No summary included in response. None, @@ -404,7 +413,8 @@ impl AsRef for SummaryMode { /// - OpenAI Chat: `"auto"` | `"none"` | `"required"` | `{ type: "function", function: { name } }` /// - OpenAI Responses: `"auto"` | `{ type: "function", name }` /// - Anthropic: `{ type: "auto" | "any" | "none" | "tool", name?, disable_parallel_tool_use? }` -#[derive(Debug, Clone, Default, Serialize)] +#[derive(Debug, Clone, Default, Serialize, TS)] +#[ts(export)] pub struct ToolChoiceConfig { /// Selection mode - the semantic intent of the tool choice pub mode: Option, @@ -419,7 +429,8 @@ pub struct ToolChoiceConfig { } /// Tool selection mode (portable across providers). -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, TS)] +#[ts(export)] pub enum ToolChoiceMode { /// Provider decides whether to use tools Auto, @@ -493,7 +504,8 @@ impl AsRef for ToolChoiceMode { /// - OpenAI Responses: nested under `text.format` /// - Google: `response_mime_type` + `response_schema` /// - Anthropic: `{ type: "json_schema", schema, name?, strict?, description? }` -#[derive(Debug, Clone, Default, Serialize)] +#[derive(Debug, Clone, Default, Serialize, TS)] +#[ts(export)] pub struct ResponseFormatConfig { /// Output format type pub format_type: Option, @@ -503,7 +515,8 @@ pub struct ResponseFormatConfig { } /// Response format type (portable across providers). -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, TS)] +#[ts(export)] pub enum ResponseFormatType { /// Plain text output (default) Text, @@ -553,12 +566,14 @@ impl AsRef for ResponseFormatType { } /// JSON schema configuration for structured output. -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, TS)] +#[ts(export)] pub struct JsonSchemaConfig { /// Schema name (required by OpenAI) pub name: String, /// The JSON schema definition + #[ts(type = "Record")] pub schema: Value, /// Whether to enable strict schema validation diff --git a/crates/lingua/src/universal/tools.rs b/crates/lingua/src/universal/tools.rs index 74ad7853..2a1c68a6 100644 --- a/crates/lingua/src/universal/tools.rs +++ b/crates/lingua/src/universal/tools.rs @@ -25,6 +25,7 @@ any provider format. It distinguishes between: */ use serde::{Deserialize, Serialize}; +use ts_rs::TS; use crate::error::ConvertError; use crate::serde_json::{json, Map, Value}; @@ -37,7 +38,8 @@ use crate::serde_json::{json, Map, Value}; /// /// This provides a typed representation that normalizes the different tool formats /// across providers (Anthropic, OpenAI Chat, OpenAI Responses API, etc.). -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, TS)] +#[ts(export)] pub struct UniversalTool { /// Tool name (required for all tool types) pub name: String, @@ -48,6 +50,7 @@ pub struct UniversalTool { /// Parameters/input schema (JSON Schema) #[serde(skip_serializing_if = "Option::is_none")] + #[ts(type = "Record | null")] pub parameters: Option, /// Whether to enforce strict schema validation (OpenAI Responses API) @@ -60,7 +63,8 @@ pub struct UniversalTool { } /// Classification of tool types. -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, TS)] +#[ts(export)] #[serde(tag = "kind")] pub enum UniversalToolType { /// User-defined function tool (works across all providers) @@ -77,6 +81,7 @@ pub enum UniversalToolType { builtin_type: String, /// Provider-specific configuration #[serde(skip_serializing_if = "Option::is_none")] + #[ts(type = "Record | null")] config: Option, }, } diff --git a/crates/lingua/src/wasm.rs b/crates/lingua/src/wasm.rs index 9caa1a1d..c4c17a45 100644 --- a/crates/lingua/src/wasm.rs +++ b/crates/lingua/src/wasm.rs @@ -262,3 +262,118 @@ pub fn validate_google_response(json: &str) -> Result { .map(|_| JsValue::NULL) .map_err(|e| JsValue::from_str(&e.to_string())) } + +// ============================================================================ +// Transform exports +// ============================================================================ + +/// 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 an object with either: +/// - `{ passThrough: true, data: ... }` if payload is already valid for target +/// - `{ transformed: true, data: ..., sourceFormat: "..." }` if transformed +#[wasm_bindgen] +pub fn transform_request( + input: &str, + target_format: &str, + model: Option, +) -> Result { + use crate::capabilities::ProviderFormat; + use crate::processing::transform::{transform_request as transform, TransformResult}; + use bytes::Bytes; + + let target: ProviderFormat = target_format + .parse() + .map_err(|_| JsValue::from_str(&format!("Unknown target format: {}", target_format)))?; + + let input_bytes = Bytes::from(input.to_owned()); + let result = transform(input_bytes, target, model.as_deref()) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + + // Use JS native JSON.parse to avoid serde_wasm_bindgen serialization issues + // (Map objects, $serde_json::private::Number from arbitrary_precision) + let (pass_through, bytes, source_format) = match result { + TransformResult::PassThrough(bytes) => (true, bytes, None), + TransformResult::Transformed { + bytes, + source_format, + } => (false, bytes, Some(source_format)), + }; + + let data_str = String::from_utf8_lossy(&bytes); + let data = + js_sys::JSON::parse(&data_str).map_err(|_| JsValue::from_str("Failed to parse JSON"))?; + + let obj = js_sys::Object::new(); + if pass_through { + js_sys::Reflect::set(&obj, &"passThrough".into(), &JsValue::TRUE)?; + } else { + js_sys::Reflect::set(&obj, &"transformed".into(), &JsValue::TRUE)?; + if let Some(sf) = source_format { + js_sys::Reflect::set(&obj, &"sourceFormat".into(), &sf.to_string().into())?; + } + } + js_sys::Reflect::set(&obj, &"data".into(), &data)?; + Ok(obj.into()) +} + +/// 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 an object with either: +/// - `{ passThrough: true, data: ... }` if payload is already valid for target +/// - `{ transformed: true, data: ..., sourceFormat: "..." }` if transformed +#[wasm_bindgen] +pub fn transform_response(input: &str, target_format: &str) -> Result { + use crate::capabilities::ProviderFormat; + use crate::processing::transform::{transform_response as transform, TransformResult}; + use bytes::Bytes; + + let target: ProviderFormat = target_format + .parse() + .map_err(|_| JsValue::from_str(&format!("Unknown target format: {}", target_format)))?; + + let input_bytes = Bytes::from(input.to_owned()); + let result = transform(input_bytes, target).map_err(|e| JsValue::from_str(&e.to_string()))?; + + // Use JS native JSON.parse to avoid serde_wasm_bindgen serialization issues + // (Map objects, $serde_json::private::Number from arbitrary_precision) + let (pass_through, bytes, source_format) = match result { + TransformResult::PassThrough(bytes) => (true, bytes, None), + TransformResult::Transformed { + bytes, + source_format, + } => (false, bytes, Some(source_format)), + }; + + let data_str = String::from_utf8_lossy(&bytes); + let data = + js_sys::JSON::parse(&data_str).map_err(|_| JsValue::from_str("Failed to parse JSON"))?; + + let obj = js_sys::Object::new(); + if pass_through { + js_sys::Reflect::set(&obj, &"passThrough".into(), &JsValue::TRUE)?; + } else { + js_sys::Reflect::set(&obj, &"transformed".into(), &JsValue::TRUE)?; + if let Some(sf) = source_format { + js_sys::Reflect::set(&obj, &"sourceFormat".into(), &sf.to_string().into())?; + } + } + js_sys::Reflect::set(&obj, &"data".into(), &data)?; + Ok(obj.into()) +} + +/// 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 undefined if not present. +#[wasm_bindgen] +pub fn extract_model(input: &str) -> Option { + use crate::processing::transform::extract_model as extract; + extract(input.as_bytes()) +} diff --git a/examples/python/example.py b/examples/python/example.py new file mode 100644 index 00000000..fceebeca --- /dev/null +++ b/examples/python/example.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +""" +Lingua Python Examples + +Demonstrates the ergonomics of using Lingua's universal types for LLM requests. +""" + +from typing import TYPE_CHECKING + +# For type checking, we can use the TypedDict definitions +if TYPE_CHECKING: + from lingua import ( + UniversalParams, + UniversalRequest, + UniversalTool, + ReasoningConfig, + ToolChoiceConfig, + ResponseFormatConfig, + JsonSchemaConfig, + Message, + ) + + +def example_typed_params(): + """ + Example: Creating a request with typed parameters. + + This demonstrates how to use TypedDict hints for IDE support + while passing plain dicts to Lingua functions. + """ + print("\nšŸ“‹ Example: Creating typed parameters") + + # Define tools with type hints for IDE support + tools: list["UniversalTool"] = [ + { + "name": "get_weather", + "description": "Get the current weather for a location", + "parameters": { + "type": "object", + "properties": { + "location": {"type": "string", "description": "City name"}, + "units": {"type": "string", "enum": ["celsius", "fahrenheit"]}, + }, + "required": ["location"], + }, + "kind": "function", + } + ] + + # Create params with type hints + params: "UniversalParams" = { + "temperature": 0.7, + "max_tokens": 1000, + "tools": tools, + "tool_choice": {"mode": "auto"}, + "response_format": { + "format_type": "json_schema", + "json_schema": { + "name": "weather_response", + "schema": { + "type": "object", + "properties": { + "temperature": {"type": "number"}, + "conditions": {"type": "string"}, + }, + }, + "strict": True, + }, + }, + "reasoning": { + "enabled": True, + "budget_tokens": 2048, + "summary": "auto", + }, + "metadata": {"user_id": "example-user"}, + } + + # Create the full request + request: "UniversalRequest" = { + "model": "gpt-5-mini", + "messages": [ + {"role": "user", "content": "What's the weather in San Francisco?"} + ], + "params": params, + } + + print(f" Model: {request['model']}") + print(f" Tools: {len(request['params'].get('tools', []))} tool(s)") + print(f" Reasoning enabled: {request['params'].get('reasoning', {}).get('enabled')}") + print(f" Response format: {request['params'].get('response_format', {}).get('format_type')}") + + return request + + +def example_provider_conversion(): + """ + Example: Converting messages between providers. + + This requires the lingua package to be installed with: + pip install lingua + """ + print("\nšŸ”„ Example: Provider conversion") + + try: + from lingua import ( + chat_completions_messages_to_lingua, + lingua_to_anthropic_messages, + ) + + # OpenAI Chat Completions format + openai_messages = [ + {"role": "user", "content": "Hello, how are you?"}, + {"role": "assistant", "content": "I'm doing well, thanks!"}, + ] + + # Convert to Lingua format + lingua_messages = chat_completions_messages_to_lingua(openai_messages) + print(f" Converted {len(openai_messages)} messages to Lingua format") + + # Convert to Anthropic format + anthropic_messages = lingua_to_anthropic_messages(lingua_messages) + print(f" Converted to Anthropic format: {len(anthropic_messages)} messages") + + except ImportError: + print(" āš ļø Skipping - lingua package not installed") + print(" Install with: pip install lingua") + + +def main(): + print("═" * 60) + print(" šŸŒ Lingua: Universal Types for LLM Requests") + print("═" * 60) + + # This example works without the lingua package installed + # (just demonstrates type hints) + example_typed_params() + + # This example requires the lingua package + example_provider_conversion() + + print("\n" + "═" * 60) + print(" ✨ One format. Any model. Type-safe. ✨") + print("═" * 60) + + +if __name__ == "__main__": + main() diff --git a/examples/typescript/index.ts b/examples/typescript/index.ts index dbfcf62d..4a9f0339 100644 --- a/examples/typescript/index.ts +++ b/examples/typescript/index.ts @@ -1,5 +1,9 @@ import { type Message, + type UniversalParams, + type UniversalRequest, + type UniversalTool, + type ProviderFormat, linguaToChatCompletionsMessages, linguaToAnthropicMessages, chatCompletionsMessagesToLingua, @@ -40,6 +44,9 @@ async function basicUsage() { } async function main() { + // Always run the typed request example (no API keys needed) + exampleTypedRequest(); + const hasOpenAiApiKey = !!process.env.OPENAI_API_KEY; const hasAnthropicApiKey = !!process.env.ANTHROPIC_API_KEY; @@ -95,6 +102,92 @@ const createAnthropicCompletion = async (messages: Message[]) => { return [anthropicResponse]; }; +/** + * Example: Creating a request with typed parameters + * + * This demonstrates the ergonomics of using UniversalParams and UniversalTool types. + */ +function exampleTypedRequest() { + console.log("\nšŸ“‹ Example: Creating a typed UniversalRequest"); + + // Define tools with full type safety + const tools: UniversalTool[] = [ + { + name: "get_weather", + description: "Get the current weather for a location", + parameters: { + type: "object", + properties: { + location: { type: "string", description: "City name" }, + units: { type: "string", enum: ["celsius", "fahrenheit"] }, + }, + required: ["location"], + }, + strict: null, + kind: "function", + }, + ]; + + // Create params with all the bells and whistles + const params: UniversalParams = { + temperature: 0.7, + max_tokens: BigInt(1000), + top_p: null, + top_k: null, + seed: null, + presence_penalty: null, + frequency_penalty: null, + stop: null, + logprobs: null, + top_logprobs: null, + tools: tools, + tool_choice: { + mode: "auto", + tool_name: null, + disable_parallel: null, + }, + parallel_tool_calls: null, + response_format: { + format_type: "json_schema", + json_schema: { + name: "weather_response", + schema: { + type: "object", + properties: { + temperature: { type: "number" }, + conditions: { type: "string" }, + }, + }, + strict: true, + description: null, + }, + }, + reasoning: { + enabled: true, + budget_tokens: BigInt(2048), + summary: "auto", + }, + metadata: { user_id: "example-user" }, + store: null, + service_tier: null, + stream: null, + }; + + // Create the full request + const request: UniversalRequest = { + model: "gpt-5-mini", + messages: [ + { role: "user", content: "What's the weather in San Francisco?" }, + ], + params: params, + }; + + console.log(" Model:", request.model); + console.log(" Tools:", request.params.tools?.length ?? 0, "tool(s)"); + console.log(" Reasoning enabled:", request.params.reasoning?.enabled); + console.log(" Response format:", request.params.response_format?.format_type); +} + /** * Test ideas: * - Agent loop