Skip to content

Add universal param configs#61

Merged
knjiang merged 5 commits intomainfrom
add_universal_params_between_completions_responses_and_anthropic
Feb 3, 2026
Merged

Add universal param configs#61
knjiang merged 5 commits intomainfrom
add_universal_params_between_completions_responses_and_anthropic

Conversation

@knjiang
Copy link
Contributor

@knjiang knjiang commented Jan 16, 2026

Summary

This PR adds cross-provider compatability for chat completions, responses, anthropic.

See -> https://github.com/braintrustdata/lingua/actions/runs/21328545339

Parameter mappings

Feature Chat Completions Responses Anthropic
Reasoning reasoning_effort reasoning.effort thinking.budget_tokens
Structured output response_format.json_schema text.format output_format
Tool selection tool_choice tool_choice tool_choice + disable_parallel_tool_use
Max tokens max_tokens / max_completion_tokens max_output_tokens max_tokens

Testing

For each provider pair (A → B) across Chat Completions / Responses / Anthropic, we validate the deserialized Universal payload:

  1. Universal of source payload

    • U₁ = A payload → Universal
  2. Translate across providers and re-canonicalize

    • U₂ = (A payload → Universal → B payload) → Universal
  3. Diff the canonical forms

    • Compare U₁ vs U₂
    • Emit field-level diffs for any lost / added / changed fields
  4. Enforce in CI

    • CI fails on any unexpected diffs

Expected differences

  • Known provider limitations / intentional lossy mappings are documented in expected_differences.json.
  • Diffs covered by this file are treated as allowed; anything else is flagged as a regression and fails CI.

@knjiang knjiang changed the title add universla param configs add universal param configs Jan 16, 2026
@knjiang knjiang force-pushed the add_universal_params_between_completions_responses_and_anthropic branch from 7fbb91f to 64f4bad Compare January 21, 2026 18:58
@knjiang knjiang force-pushed the add_anthropic_parameter_test_cases branch from 8d7eba7 to 9d2ccf3 Compare January 21, 2026 18:58
@knjiang knjiang marked this pull request as ready for review January 21, 2026 19:01
@knjiang knjiang force-pushed the add_universal_params_between_completions_responses_and_anthropic branch from 64f4bad to e569a99 Compare January 21, 2026 21:01
@knjiang knjiang force-pushed the add_anthropic_parameter_test_cases branch from 9d2ccf3 to bd8d790 Compare January 21, 2026 21:01
Comment on lines -59 to -62
/// Known request fields for OpenAI Responses API.
/// These are fields extracted into UniversalRequest/UniversalParams.
/// Fields not in this list go into `extras` for passthrough.
const RESPONSES_KNOWN_KEYS: &[&str] = &[
Copy link
Contributor Author

Choose a reason for hiding this comment

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

i move responses to responses_adapter.rs

@knjiang knjiang force-pushed the add_universal_params_between_completions_responses_and_anthropic branch 3 times, most recently from cf02e72 to 349a05d Compare January 22, 2026 10:06
target_adapter.display_name(),
test_case,
);
let roundtrip_result = compare_values(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

i tightened the runner to be more accurate, basically we now compare:

source -> universal

with

source -> universal -> target -> universal

we basically deserialize universal -> JSON and do diffing

@knjiang knjiang changed the title add universal param configs Add universal param configs Jan 22, 2026
@knjiang knjiang force-pushed the add_universal_params_between_completions_responses_and_anthropic branch 5 times, most recently from a75144e to 4e0703c Compare January 23, 2026 00:19
@knjiang knjiang force-pushed the add_anthropic_parameter_test_cases branch 2 times, most recently from f2ba481 to c8a3e48 Compare January 23, 2026 00:19
@knjiang knjiang force-pushed the add_universal_params_between_completions_responses_and_anthropic branch from 4e0703c to 5aa114b Compare January 23, 2026 00:20
@knjiang knjiang force-pushed the add_anthropic_parameter_test_cases branch from c8a3e48 to f2ba481 Compare January 23, 2026 00:20
@knjiang knjiang force-pushed the add_universal_params_between_completions_responses_and_anthropic branch 2 times, most recently from 4e0703c to 561180b Compare January 23, 2026 00:40
@knjiang knjiang force-pushed the add_anthropic_parameter_test_cases branch from f2ba481 to a38f078 Compare January 23, 2026 00:40
@knjiang knjiang force-pushed the add_universal_params_between_completions_responses_and_anthropic branch from 561180b to c94e076 Compare January 23, 2026 01:18
/// Tool selection strategy (varies by provider)
pub tool_choice: Option<Value>,
/// Number of top logprobs to return (0-20)
pub top_logprobs: Option<i64>,
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: if it's 1-20 can it be a smaller integer type like i8?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Comment on lines 135 to 150
// === Metadata and identification ===
/// Request metadata (user tracking, experiment tags, etc.)
pub metadata: Option<Value>,
Copy link
Contributor

Choose a reason for hiding this comment

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

what is this

Copy link
Contributor Author

Choose a reason for hiding this comment

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

responses/chatcompletions have it as:

metadata
map

Optional
Set of 16 key-value pairs that can be attached to an object. This can be useful for storing additional information about the object in a structured format, and querying for objects via API or the dashboard.

Keys are strings with a maximum length of 64 characters. Values are strings with a maximum length of 512 characters.

while anthrpoic has metadata as an object with one field user_id.

https://platform.openai.com/docs/api-reference/responses/create#responses_create-metadata
https://platform.claude.com/docs/en/api/messages

Copy link
Contributor

Choose a reason for hiding this comment

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

ahh ok. mind linking these in the comment

/// Example: OpenAI Chat extras stay in `provider_extras[ProviderFormat::OpenAI]`
/// and are only merged back when converting to OpenAI Chat, not to Anthropic.
#[serde(skip)]
pub provider_extras: HashMap<ProviderFormat, Map<String, Value>>,
Copy link
Contributor

Choose a reason for hiding this comment

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

it's slightly weird that this is not nested in params , at least to me. What as the rationale behind that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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.

Looks pretty good straightforward to me

  • It's a little out of date, but it would be useful to write some typescript examples (eg in examples/typescript/index.ts) or even some rust examples that show the ergonomics of using parameters, so we can double check the format
  • for parameters, i think it would be useful to write a "fuzz" style tester that for each provider, generates random values with respect to the openapi spec, and then roundtrips through UniversalParams (there is less entropy in parameters than raw requests, but this might just be generally useful)
  • Is there a creative way we can port the test cases we have in the proxy/ repo? We have had a bunch of historical challenges with translating reasoning for example that is well captured in those tests.

@knjiang knjiang force-pushed the add_universal_params_between_completions_responses_and_anthropic branch from b2a3e48 to 0ffbf84 Compare January 26, 2026 01:12
@knjiang knjiang requested a review from ankrgyl January 26, 2026 14:42
@ankrgyl
Copy link
Contributor

ankrgyl commented Jan 27, 2026

Looks pretty good straightforward to me

  • It's a little out of date, but it would be useful to write some typescript examples (eg in examples/typescript/index.ts) or even some rust examples that show the ergonomics of using parameters, so we can double check the format
  • for parameters, i think it would be useful to write a "fuzz" style tester that for each provider, generates random values with respect to the openapi spec, and then roundtrips through UniversalParams (there is less entropy in parameters than raw requests, but this might just be generally useful)
  • Is there a creative way we can port the test cases we have in the proxy/ repo? We have had a bunch of historical challenges with translating reasoning for example that is well captured in those tests.

just to clarify, did you address these too?

/// - ratio >= 0.65: high
pub fn budget_to_effort(budget: i64, max_tokens: Option<i64>) -> ReasoningEffort {
let max = max_tokens.unwrap_or(DEFAULT_MAX_TOKENS);
let ratio = budget as f64 / max as f64;
Copy link
Contributor

Choose a reason for hiding this comment

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

Are we enforcing that max is a strictly positive integer?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good catch! adddedtests

MissingRequiredField { field: String },

#[error("Invalid role: {role}")]
InvalidRole { role: String },
Copy link
Contributor

Choose a reason for hiding this comment

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

);

// Responses API function tools have strict: false by default
obj.insert("strict".into(), Value::Bool(false));
Copy link
Contributor

Choose a reason for hiding this comment

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

what if a request had strict: true ? wouldn't that override it ?

Copy link
Contributor Author

@knjiang knjiang Jan 27, 2026

Choose a reason for hiding this comment

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

chat completions don't have strict;true for tools. google and anthropic does though so i'll preemptively add support now.

i realized our anthropic spec is out of date

"completion_tokens": completion,
"total_tokens": prompt + completion
});
let obj_map = obj.as_object_mut().unwrap();
Copy link
Contributor

Choose a reason for hiding this comment

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

should we avoid using unwrap() and use expect() instead to give more context in case of crash? I think there are a few places in your PR where you use unwrap().

pub const EFFORT_HIGH_MULTIPLIER: f64 = 0.75;

/// Threshold below which budget is considered "low" effort
pub const EFFORT_LOW_THRESHOLD: f64 = 0.35;
Copy link
Contributor

Choose a reason for hiding this comment

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

are those thresholds documented anywhere? or is it just something you came up with (which would be fine)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

these are copied from the old proxy as a starting point.

Copy link
Contributor

@remh remh left a comment

Choose a reason for hiding this comment

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

A few comments that need to be addressed but otherwise it looks good to me.

@knjiang knjiang force-pushed the add_universal_params_between_completions_responses_and_anthropic branch from 0ffbf84 to 80528b1 Compare January 28, 2026 00:33
@knjiang
Copy link
Contributor Author

knjiang commented Jan 28, 2026

  • It's a little out of date, but it would be useful to write some typescript examples (eg in examples/typescript/index.ts) or even some rust examples that show the ergonomics of using parameters, so we can double check the format
  • for parameters, i think it would be useful to write a "fuzz" style tester that for each provider, generates random values with respect to the openapi spec, and then roundtrips through UniversalParams (there is less entropy in parameters than raw requests, but this might just be generally useful)
  • Is there a creative way we can port the test cases we have in the proxy/ repo? We have had a bunch of historical challenges with translating reasoning for example that is well captured in those tests.
  1. deferring to lingua-wasm bindings for request/response #69
  2. kk, i'll separate out the fuzz tester?
  3. done in this PR by pulling in the test cases -> f1340cb

there are still some proxy failures but i got most tests passing.

proxyAnthropicAudioError - audio PR
proxyOpenAIO3MiniReasoning - openai capabilitiescapabilities
proxyGoogleReasoning - google next PR
proxyGoogleAudioSupport -  google next PR
proxyGoogleVideoSupport - google next PR
proxyAzureParamFiltering - capabilities
proxyOpenAIO3MiniStreamingReasoning - openai capabilities
proxyOpenAIPdfUrlConversion - I'm not sure how to do this, we would have to make a network call and then encode(?)

@knjiang knjiang force-pushed the add_universal_params_between_completions_responses_and_anthropic branch from 52805d2 to af4a595 Compare January 29, 2026 05:38
@@ -0,0 +1,915 @@
import OpenAI from "openai";
Copy link
Contributor Author

Choose a reason for hiding this comment

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

all the cases i ported from proxy that "fit" into this. some of the proxy cases intercepted calls so that doesn't really work yet. I have a subsequent PR with a more "intercept"-like approach. https://app.graphite.com/github/pr/braintrustdata/lingua/72

there are still some proxy failures but i got most tests passing.

proxyAnthropicAudioError - audio PR
proxyOpenAIO3MiniReasoning - openai capabilitiescapabilities
proxyGoogleReasoning - google next PR
proxyGoogleAudioSupport -  google next PR
proxyGoogleVideoSupport - google next PR
proxyAzureParamFiltering - capabilities
proxyOpenAIO3MiniStreamingReasoning - openai capabilities
proxyOpenAIPdfUrlConversion - I'm not sure how to do this, we would have to make a network call and then encode(?)

@knjiang knjiang requested a review from remh January 30, 2026 20:00
This was referenced Feb 2, 2026
@knjiang knjiang merged commit c9d8323 into main Feb 3, 2026
5 checks passed
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.

3 participants