-
Notifications
You must be signed in to change notification settings - Fork 0
Add universal param configs #61
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Add universal param configs #61
Conversation
This stack of pull requests is managed by Graphite. Learn more about stacking. |
7fbb91f to
64f4bad
Compare
8d7eba7 to
9d2ccf3
Compare
64f4bad to
e569a99
Compare
9d2ccf3 to
bd8d790
Compare
| /// 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] = &[ |
There was a problem hiding this comment.
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
cf02e72 to
349a05d
Compare
| target_adapter.display_name(), | ||
| test_case, | ||
| ); | ||
| let roundtrip_result = compare_values( |
There was a problem hiding this comment.
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
a75144e to
4e0703c
Compare
f2ba481 to
c8a3e48
Compare
4e0703c to
5aa114b
Compare
c8a3e48 to
f2ba481
Compare
4e0703c to
561180b
Compare
f2ba481 to
a38f078
Compare
561180b to
c94e076
Compare
768934e to
2a5de09
Compare
517e490 to
7aaaece
Compare
2a5de09 to
768934e
Compare
7aaaece to
473a200
Compare
768934e to
166bef4
Compare
7657b1e to
b2a3e48
Compare
| } | ||
| } | ||
|
|
||
| fn normalize_user_content(content: &mut UserContent) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i couldn't find a good way to deserialize & compare UserContent since a single item text array and a string are semantically equivalent.
this is the only normalization i do -> https://github.com/braintrustdata/lingua/blob/main/crates/lingua/src/universal/message.rs#L31
i had an alternative approach of this code being normalized in the deserializer but that broke roundtrip tests so it lives here now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just to clarify, this is just for testing right? if so it seems totally fine
| #[derive(Debug, Clone, Serialize, Deserialize)] | ||
| pub struct ChatCompletionResponseMessageExt { | ||
| #[serde(flatten)] | ||
| pub base: openai::ChatCompletionResponseMessage, | ||
| #[serde(skip_serializing_if = "Option::is_none")] | ||
| pub reasoning: Option<String>, | ||
| /// Encrypted reasoning signature for cross-provider roundtrips (e.g., Anthropic's signature) | ||
| #[serde(skip_serializing_if = "Option::is_none")] | ||
| pub reasoning_signature: Option<String>, | ||
| } | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
extended the chat completion type with reasoning + reasoning_signature.
i think reasoning_signature will be useful for ant/gemini. I think reasoning is supported via -> vllm-project/vllm#27755 but reasoning_signature is smth i added alongside it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reason it's added is so that other model providers than OpenAI who use chat completions can propagate it?
| { "pattern": "params.response_format", "reason": "Anthropic doesn't support Text format type" }, | ||
| { "pattern": "params.metadata", "reason": "Anthropic only accepts user_id in metadata" }, | ||
| { "pattern": "params.parallel_tool_calls", "reason": "Anthropic only supports disable_parallel via tool_choice" }, | ||
| { "pattern": "params.tool_choice", "reason": "Anthropic requires tool_choice to express disable_parallel_tool_use" } | ||
| ], | ||
| "errors": [ | ||
| { "pattern": "does not support logprobs", "reason": "Anthropic doesn't support logprobs parameter" }, | ||
| { "pattern": "does not support top_logprobs", "reason": "Anthropic doesn't support top_logprobs parameter" }, | ||
| { "pattern": "does not support frequency_penalty", "reason": "Anthropic doesn't support frequency_penalty parameter" }, | ||
| { "pattern": "does not support presence_penalty", "reason": "Anthropic doesn't support presence_penalty parameter" }, | ||
| { "pattern": "does not support seed", "reason": "Anthropic doesn't support seed parameter" }, | ||
| { "pattern": "does not support store", "reason": "Anthropic doesn't support store parameter" }, | ||
| { "pattern": "does not support n > 1", "reason": "Anthropic doesn't support multiple completions" } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is something I was having trouble classifying whether we want to fail fast for unsupported parameters or silently drop when converting to a different model.
For now, I've gone with fail-fast with completely unsupported params.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think right now we silently drop. Maybe we should try this out on some other systems that do translation and see what they do?
Uusally people solve stuff like this by having a flag and then propagating the choice to the user. Eg. mysql defaults to flexible type conversions and has a strict mode. Postgres is the opposite.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ran
curl https://ai-gateway.vercel.sh/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <AI_GATEWAY_KEY>" \
-d '{
"model": "anthropic/claude-sonnet-4.5",
"messages": [
{
"role": "user",
"content": "Write a one-sentence bedtime story about a unicorn."
}
],
"stream": false,
"logprobs": true,
"top_logprobs": 3,
"frequency_penalty": 0.5
}'and the parameters were dropped
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah then maybe we should silently drop, and offer a strict mode (this could even be a lingua/universal param) that enforces parameter translation is not dropped.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| /// Required providers for CI: Anthropic <-> ChatCompletions <-> Responses | ||
| const REQUIRED_PROVIDERS: &[ProviderFormat] = &[ | ||
| ProviderFormat::Responses, | ||
| ProviderFormat::OpenAI, // ChatCompletions | ||
| ProviderFormat::Anthropic, | ||
| ]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The goal is to have this automatically run on every provider format, this is temporary while we incrementally make progress.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
makes sense. Maybe leave this as a comment in the code
| # Click into the job summary to see the actual coverage report | ||
| - name: Post coverage to job summary | ||
| if: always() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what does if: always() do?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oops let me reorder. if always() just lets the CI continue if a step fails
| /// Tool selection strategy (varies by provider) | ||
| pub tool_choice: Option<Value>, | ||
| /// Number of top logprobs to return (0-20) | ||
| pub top_logprobs: Option<i64>, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the openai generated type equivalent is i64 -> https://github.com/braintrustdata/lingua/blob/main/crates/lingua/src/providers/openai/generated.rs#L69
| // === Metadata and identification === | ||
| /// Request metadata (user tracking, experiment tags, etc.) | ||
| pub metadata: Option<Value>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is this
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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>>, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ankrgyl
left a comment
There was a problem hiding this 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.
b2a3e48 to
0ffbf84
Compare
just to clarify, did you address these too? |

Summary
This PR adds cross-provider compatability for chat completions, responses, anthropic.
See -> https://github.com/braintrustdata/lingua/actions/runs/21328545339
Parameter mappings
reasoning_effortreasoning.effortthinking.budget_tokensresponse_format.json_schematext.formatoutput_formattool_choicetool_choicetool_choice+disable_parallel_tool_usemax_tokens/max_completion_tokensmax_output_tokensmax_tokensTesting
For each provider pair (A → B) across Chat Completions / Responses / Anthropic, we validate the deserialized Universal payload:
Universal of source payload
U₁ = A payload → UniversalTranslate across providers and re-canonicalize
U₂ = (A payload → Universal → B payload) → UniversalDiff the canonical forms
U₁vsU₂Enforce in CI
Expected differences
expected_differences.json.