Skip to content

Conversation

@remh
Copy link

@remh remh commented Nov 17, 2025

Summary

Add format detection, request transformation infrastructure, and OpenAI-specific preprocessing for gateway integration.

Changes

  • Format Detection: Struct-based validation to detect incoming request formats (OpenAI, Anthropic, Google, Bedrock, Mistral) via is_valid_for_format() and validate_or_transform() APIs
  • Request Transformation Pipeline: Convert payloads between any supported format through universal message representation with automatic provider defaults (e.g., max_tokens for Anthropic)
  • OpenAI Request Transformations: Request preprocessing via OpenAIRequestTransformer builder - reasoning model handling, structured output fallback, provider-specific field sanitization
  • Universal Message Transforms: System message extraction and consecutive message flattening for providers that require them
  • Media Utilities: URL parsing and base64 conversion utilities for images/PDFs

New Public API

use lingua::{ProviderFormat, TransformResult, TransformError};

// Check if payload is valid for target format (struct-based validation)
if lingua::is_valid_for_format(&payload, ProviderFormat::Anthropic) {
    // Use payload as-is (pass-through)
}

// Full request transformation with provider defaults
let result = lingua::transform_request(&payload, ProviderFormat::Anthropic)?;
match result {
    TransformResult::PassThrough => { /* use original payload */ }
    TransformResult::Transformed { payload, source_format } => {
        // payload has been transformed and defaults applied
    }
}

// Convert to/from universal message format
let messages: Vec<Message> = lingua::to_universal(&payload, ProviderFormat::OpenAI)?;
let anthropic_messages = lingua::from_universal(&messages, ProviderFormat::Anthropic)?;

// Apply provider-specific defaults (e.g., max_tokens for Anthropic)
lingua::apply_provider_defaults(&mut payload, ProviderFormat::Anthropic);#### OpenAI-specific transformations

use lingua::providers::openai::transformations::{OpenAIRequestTransformer, TargetProvider};

// Transform OpenAI requests for downstream providers
OpenAIRequestTransformer::new(&mut request)
    .with_target_provider(TargetProvider::Mistral)
    .with_provider_metadata(metadata)
    .with_supports_streaming(Some(true))
    .transform()?;#### Universal message transforms

use lingua::universal::{extract_system_messages, flatten_consecutive_messages};

// Extract system messages (for Anthropic, Google, Bedrock)
let system_prompts = extract_system_messages(&mut messages);

// Merge consecutive same-role messages
flatten_consecutive_messages(&mut messages);### Supported Formats
Format Detection To Universal From Universal
OpenAI
Anthropic
Google
Bedrock (Converse)
Mistral ✓ (via OpenAI) ✓ (via OpenAI)

Testing

  • Payload-based tests in payloads/transformations/ covering provider sanitization, reasoning models, structured output, and cross-format conversion
  • Unit tests for all transformation functions

@remh remh changed the title First pass at implementing edge cases transformations. Starting with OpenAI Gateway transformations Dec 3, 2025
remh added 20 commits December 3, 2025 13:05
- Add ProviderFormat enum with OpenAI, Anthropic, Google, Mistral, Converse, Js, and Unknown variants
- Implement heuristic-based format detection for Anthropic payloads (is_anthropic_format_heuristic)
- Add processing/detect module with unified parse() and parse_from_str() for multi-format detection
- Add processing/catalog module with configurable catalog_lookup function for format hints
- Create GooglePayload and BedrockPayload wrapper types for non-serde SDK types
- Add OpenAI detect module stub
- Re-export key types (ProviderFormat, DetectedPayload, TypedPayload) from lib.rs
Extract system message and message flattening logic into standalone
public helper functions, eliminating central provider dispatch.
Define GooglePayload, BedrockPayload, is_google_format, is_openai_format
unconditionally in detect.rs instead of using fallback modules with cfg gates.
…provider detection

- Add FormatDetector trait in src/processing/detector.rs
- Create providers/mistral module with MistralDetector
- Move Bedrock detection from detect.rs to providers/bedrock/detect.rs
- Add FormatDetector impl to anthropic, google, openai modules
- Replace hardcoded detection order with priority-based detector registry
- Add "Adding a Provider" section to README

Adding a new provider now requires 4 touchpoints instead of 5+:
1. Add ProviderFormat variant
2. Create provider detector implementing FormatDetector
3. Add TypedPayload variant (if needed)
4. Register in detectors() list
The confidence field in DetectedPayload and FormatDetector trait was
never used by any downstream consumers (gateway, braintrust-llm-router).
Move payload wrapper types to their respective provider modules and
add feature gates to TypedPayload enum variants. This eliminates
duplicate definitions that existed in both detect.rs and the provider
modules.

- Remove GooglePayload/BedrockPayload from processing/detect.rs
- Import these types from providers/google and providers/bedrock
- Add #[cfg(feature = "...")] to TypedPayload variants and match arms
- Update re-exports in processing/mod.rs and lib.rs to be feature-gated
Mistral now has its own feature flag instead of being bundled under
`openai`. The new flag depends on `openai` since Mistral uses
OpenAI-compatible types for parsing.
- Add integration test for detect → transform → serialize pipeline
- Add tests for Anthropic heuristic detection edge cases
- Add Copy derive to all zero-sized detector structs
- Refactor model prefix matching to use const arrays
- Remove trivial test_detector_trait tests
Catalog testing:
- Remove flaky unit tests that were unreliable due to global OnceLock state
- Add test_catalog.json as a mock catalog fixture
- Add integration tests (tests/catalog_test.rs) that run in separate
  processes with fresh OnceLock state
- Document testing approach in module header

Doc examples:
- Fix examples in detect.rs and detector.rs to be compilable
- Change from `ignore` to proper runnable/no_run examples
- Use correct import paths and realistic code snippets

Snapshot-based transformation tests:
- Add snapshot tests for structured output, PDF normalization
- Add provider sanitization tests (Mistral, Azure, Databricks, Fireworks)
- Add Vertex model normalization and Responses API routing tests
Copy link
Contributor

@choochootrain choochootrain left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks reasonable at a high level. i had 2 questions that can be addressed outside this PR, just thinking out loud about what end to end behavior we want

Copy link
Contributor

@ankrgyl ankrgyl left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just for bookkeeping purposes, undoing my quick approval

remh and others added 15 commits December 15, 2025 13:21
Remove the separate as_str() method since it was only used internally
by the Display implementation.
Return Err(()) instead of Ok(Unknown) for unrecognized format strings,
letting callers decide how to handle failures. Callers can use
.unwrap_or_default() to get the previous behavior.
Changes:
- Bedrock: Fix serialization to use camelCase (modelId, inferenceConfig)
  matching the actual AWS Bedrock API format. Change content block enums
  from tagged to untagged format.
- Bedrock tests: Construct actual ConverseRequest structs, serialize them,
  and test detection on the serialized JSON.
- OpenAI tests: Use CreateChatCompletionRequestClass from generated types.
- Mistral tests: Use OpenAI types (Mistral is OpenAI-compatible) with
  helper function to reduce boilerplate.
- Google tests: Add comment explaining why raw JSON is still used
  (protobuf types without serde support).
- Update validation tests to use new camelCase Bedrock format.

Now if provider types change and no longer produce valid API payloads,
the detection tests will fail to compile or fail at runtime.
Replace all heuristic-based payload format detection with direct struct
deserialization using serde. Format validation now uses try_parse_*
functions that attempt to deserialize into provider-specific structs.

Changes:
- Add try_parse_* and is_*_format_value() functions for all providers
- Add TryFromLLM trait implementations for Google and Bedrock types
- Add unified validate_or_transform() API for pass-through or conversion
- Add Serialize derives to Google structs for bidirectional conversion
- Box large enum variants in TypedPayload to fix clippy warnings
- Update transform.rs to use TryFromLLM consistently for all providers
- Remove old heuristic detection functions
Remove `is_*_format` and `is_*_format_value` wrapper functions across all
providers, keeping only the `try_parse_*` functions as the canonical API.

- Remove `is_anthropic_format`, `is_anthropic_format_value`, `detect_payload_format`, `PayloadFormat` enum
- Remove `is_openai_format`, `is_openai_format_value`
- Remove `is_mistral_format`, `is_mistral_format_value`
- Remove `is_bedrock_format_value`
- Remove `is_google_format_value`
- Update `is_valid_for_format` and `detect_source_format` to use `MistralDetector.detect()` directly
- Remove unused `JsonParseFailed` variant from `DetectionError` enums
- Update tests to use `try_parse_*` functions directly
Replace OnceLock with ArcSwap to allow the catalog lookup function
to be atomically swapped at runtime. This enables periodic catalog
refresh without requiring a restart.

Also adds unit tests for the catalog module, using serial_test
to ensure proper test isolation for global state.
- Make from_universal public for external callers to convert universal
  messages to any provider format
- Add universal_to_responses_input function to handle 1:N expansion for
  Tool messages in OpenAI Responses API format
- Re-export new functions at module and crate level
Add support for transforming payloads between provider formats (e.g., OpenAI
to Anthropic) when the target provider doesn't match the incoming format.

Changes:
- Add PayloadFormat::Transformed variant to track source→target transformations
- Add has_target_payload() method to check if request has ready-to-use payload
- Refactor from_payload to use lingua's validate_or_transform for unified flow
- Update anthropic provider to handle both native pass-through and transformed payloads
- Export to_universal from lingua for message extraction during transformation
Remove dead code that was never used by the gateway:

- Remove catalog.rs and catalog lookup infrastructure (set_catalog_lookup,
  catalog_lookup) - the gateway does format lookup at a higher level
- Remove TypedPayload enum and parse()/parse_from_str() functions - format
  detection is handled by validate_or_transform() in transform.rs instead
- Remove DetectedPayload struct and detect_format() function chain
- Remove BedrockPayload and GooglePayload wrapper types (only used by TypedPayload)
- Remove arc-swap and serial_test dependencies

The transform.rs path (validate_or_transform, to_universal, from_universal,
is_valid_for_format) remains the primary API for payload conversion.

This simplifies lingua to be a pure conversion library without global state.
- Add RequestDefaults trait for provider-specific parameter defaults
- Add AnthropicDefaults with DEFAULT_MAX_TOKENS = 4096
- Add transform_request() for full request transformation with defaults
- Pass-through mode has zero overhead (use original payload as-is)
Copy link
Contributor

knjiang commented Dec 21, 2025

This stack of pull requests is managed by Graphite. Learn more about stacking.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants