diff --git a/crates/lingua/src/processing/transform.rs b/crates/lingua/src/processing/transform.rs index 6d36ffa6..40f0abc8 100644 --- a/crates/lingua/src/processing/transform.rs +++ b/crates/lingua/src/processing/transform.rs @@ -1188,6 +1188,74 @@ mod tests { assert!(output.get("top_p").is_none(), "Should not have top_p"); } + #[test] + #[cfg(all(feature = "openai", feature = "google"))] + fn test_google_top_k_openai_model_drops_top_k_for_chat_completions() { + let payload = json!({ + "contents": [{ + "role": "user", + "parts": [{"text": "Write a short sentence about API gateways."}] + }], + "generationConfig": { + "topK": 1, + "maxOutputTokens": 1024 + } + }); + let input = to_bytes(&payload); + + let result = + transform_request(input, ProviderFormat::ChatCompletions, Some("gpt-5-nano")).unwrap(); + + let output: Value = crate::serde_json::from_slice(result.as_bytes()).unwrap(); + assert_eq!(result.source_format(), Some(ProviderFormat::Google)); + assert_eq!( + output.get("model").and_then(Value::as_str), + Some("gpt-5-nano") + ); + assert!( + output.get("top_k").is_none(), + "OpenAI Chat Completions should not receive top_k" + ); + assert_eq!( + output.get("max_completion_tokens").and_then(Value::as_i64), + Some(1024) + ); + } + + #[test] + #[cfg(all(feature = "openai", feature = "google"))] + fn test_google_top_k_openai_model_drops_top_k_for_responses() { + let payload = json!({ + "contents": [{ + "role": "user", + "parts": [{"text": "Write a short sentence about API gateways."}] + }], + "generationConfig": { + "topK": 1, + "maxOutputTokens": 1024 + } + }); + let input = to_bytes(&payload); + + let result = + transform_request(input, ProviderFormat::Responses, Some("gpt-5-nano")).unwrap(); + + let output: Value = crate::serde_json::from_slice(result.as_bytes()).unwrap(); + assert_eq!(result.source_format(), Some(ProviderFormat::Google)); + assert_eq!( + output.get("model").and_then(Value::as_str), + Some("gpt-5-nano") + ); + assert!( + output.get("top_k").is_none(), + "OpenAI Responses should not receive top_k" + ); + assert_eq!( + output.get("max_output_tokens").and_then(Value::as_i64), + Some(1024) + ); + } + #[test] #[cfg(feature = "openai")] fn test_non_reasoning_model_still_passthroughs() { diff --git a/payloads/cases/params.ts b/payloads/cases/params.ts index 2a1af361..66726065 100644 --- a/payloads/cases/params.ts +++ b/payloads/cases/params.ts @@ -1505,6 +1505,25 @@ export const paramsCases: TestCaseCollection = { bedrock: null, }, + googleOpenAIModelTopKParam: { + "chat-completions": null, + responses: null, + anthropic: null, + google: { + contents: [ + { + role: "user", + parts: [{ text: "Write a short sentence about API gateways." }], + }, + ], + generationConfig: { + topK: 1, + maxOutputTokens: 1024, + }, + }, + bedrock: null, + }, + streamParam: { "chat-completions": null, responses: null, diff --git a/payloads/scripts/transforms/__snapshots__/transforms.test.ts.snap b/payloads/scripts/transforms/__snapshots__/transforms.test.ts.snap index e8b426a6..a0308b1a 100644 --- a/payloads/scripts/transforms/__snapshots__/transforms.test.ts.snap +++ b/payloads/scripts/transforms/__snapshots__/transforms.test.ts.snap @@ -14997,6 +14997,46 @@ What maximum token limit would you like to set?", } `; +exports[`google → anthropic > googleOpenAIModelTopKParam > request 1`] = ` +{ + "max_tokens": 1024, + "messages": [ + { + "content": "Write a short sentence about API gateways.", + "role": "user", + }, + ], + "model": "claude-sonnet-4-5-20250929", + "top_k": 1, +} +`; + +exports[`google → anthropic > googleOpenAIModelTopKParam > response 1`] = ` +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "An API gateway acts as a single entry point that routes client requests to appropriate backend services while handling cross-cutting concerns like authentication, rate limiting, and logging.", + }, + ], + "role": "model", + }, + "finishReason": "STOP", + "index": 0, + }, + ], + "modelVersion": "claude-sonnet-4-5-20250929", + "usageMetadata": { + "cachedContentTokenCount": 0, + "candidatesTokenCount": 35, + "promptTokenCount": 17, + "totalTokenCount": 52, + }, +} +`; + exports[`google → anthropic > googleResponseSchemaPropertyOrderingParam > request 1`] = ` { "max_tokens": 128, @@ -17365,6 +17405,46 @@ Would you like me to set max_tokens to 1024 now, or provide a different value? I } `; +exports[`google → chat-completions > googleOpenAIModelTopKParam > request 1`] = ` +{ + "max_completion_tokens": 1024, + "messages": [ + { + "content": "Write a short sentence about API gateways.", + "role": "user", + }, + ], + "model": "gpt-5-nano", +} +`; + +exports[`google → chat-completions > googleOpenAIModelTopKParam > response 1`] = ` +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "An API gateway sits between clients and backend services, routing requests and enforcing security.", + }, + ], + "role": "model", + }, + "finishReason": "STOP", + "index": 0, + }, + ], + "modelVersion": "gpt-5-nano-2025-08-07", + "usageMetadata": { + "cachedContentTokenCount": 0, + "candidatesTokenCount": 25, + "promptTokenCount": 14, + "thoughtsTokenCount": 256, + "totalTokenCount": 295, + }, +} +`; + exports[`google → chat-completions > googleResponseSchemaPropertyOrderingParam > request 1`] = ` { "max_completion_tokens": 128, @@ -19608,6 +19688,59 @@ exports[`google → responses > exclusiveMinimumToolParam > response 1`] = ` } `; +exports[`google → responses > googleOpenAIModelTopKParam > request 1`] = ` +{ + "input": [ + { + "content": "Write a short sentence about API gateways.", + "role": "user", + }, + ], + "max_output_tokens": 1024, + "model": "gpt-5-nano", +} +`; + +exports[`google → responses > googleOpenAIModelTopKParam > response 1`] = ` +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "", + "thought": true, + }, + ], + "role": "model", + }, + "finishReason": "STOP", + "index": 0, + }, + { + "content": { + "parts": [ + { + "text": "An API gateway acts as a single entry point that handles authentication, rate limiting, and routing for backend services.", + }, + ], + "role": "model", + }, + "finishReason": "STOP", + "index": 1, + }, + ], + "modelVersion": "gpt-5-nano-2025-08-07", + "usageMetadata": { + "cachedContentTokenCount": 0, + "candidatesTokenCount": 62, + "promptTokenCount": 14, + "thoughtsTokenCount": 192, + "totalTokenCount": 268, + }, +} +`; + exports[`google → responses > googleResponseSchemaPropertyOrderingParam > request 1`] = ` { "input": [ diff --git a/payloads/snapshots/googleOpenAIModelTopKParam/google/followup-request.json b/payloads/snapshots/googleOpenAIModelTopKParam/google/followup-request.json new file mode 100644 index 00000000..4d474e80 --- /dev/null +++ b/payloads/snapshots/googleOpenAIModelTopKParam/google/followup-request.json @@ -0,0 +1,32 @@ +{ + "contents": [ + { + "role": "user", + "parts": [ + { + "text": "Write a short sentence about API gateways." + } + ] + }, + { + "role": "model", + "parts": [ + { + "text": "An API gateway acts as a single entry point for managing and securing access to your APIs." + } + ] + }, + { + "role": "user", + "parts": [ + { + "text": "What should I do next?" + } + ] + } + ], + "generationConfig": { + "topK": 1, + "maxOutputTokens": 1024 + } +} \ No newline at end of file diff --git a/payloads/snapshots/googleOpenAIModelTopKParam/google/followup-response-streaming.json b/payloads/snapshots/googleOpenAIModelTopKParam/google/followup-response-streaming.json new file mode 100644 index 00000000..d56780e1 --- /dev/null +++ b/payloads/snapshots/googleOpenAIModelTopKParam/google/followup-response-streaming.json @@ -0,0 +1,63 @@ +[ + { + "candidates": [ + { + "content": { + "parts": [ + { + "text": "That's a great question, and \"what you should do next\" really depends on your goal!\n\nTo help me guide you" + } + ], + "role": "model" + }, + "index": 0 + } + ], + "usageMetadata": { + "promptTokenCount": 35, + "candidatesTokenCount": 27, + "totalTokenCount": 1043, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 35 + } + ], + "thoughtsTokenCount": 981, + "serviceTier": "standard" + }, + "modelVersion": "gemini-2.5-flash", + "responseId": "3x8Cavq-EomD-8YPoO7ukQw" + }, + { + "candidates": [ + { + "content": { + "parts": [ + { + "text": " better, could you tell me:\n\n* **Are" + } + ], + "role": "model" + }, + "finishReason": "MAX_TOKENS", + "index": 0 + } + ], + "usageMetadata": { + "promptTokenCount": 35, + "candidatesTokenCount": 39, + "totalTokenCount": 1055, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 35 + } + ], + "thoughtsTokenCount": 981, + "serviceTier": "standard" + }, + "modelVersion": "gemini-2.5-flash", + "responseId": "3x8Cavq-EomD-8YPoO7ukQw" + } +] \ No newline at end of file diff --git a/payloads/snapshots/googleOpenAIModelTopKParam/google/followup-response.json b/payloads/snapshots/googleOpenAIModelTopKParam/google/followup-response.json new file mode 100644 index 00000000..1fdf4114 --- /dev/null +++ b/payloads/snapshots/googleOpenAIModelTopKParam/google/followup-response.json @@ -0,0 +1,31 @@ +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "That depends entirely on what your goals are! Since you just got a definition, here are a few paths you could take:\n\n1. **If you want to learn more about API Gateways:**\n * **Dive into their features:** Research common functionalities like rate limiting, authentication/authorization" + } + ], + "role": "model" + }, + "finishReason": "MAX_TOKENS", + "index": 0 + } + ], + "usageMetadata": { + "promptTokenCount": 35, + "candidatesTokenCount": 61, + "totalTokenCount": 1055, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 35 + } + ], + "thoughtsTokenCount": 959, + "serviceTier": "standard" + }, + "modelVersion": "gemini-2.5-flash", + "responseId": "3x8CauzREryt-8YPg7jH8A0" +} \ No newline at end of file diff --git a/payloads/snapshots/googleOpenAIModelTopKParam/google/request.json b/payloads/snapshots/googleOpenAIModelTopKParam/google/request.json new file mode 100644 index 00000000..17c74b13 --- /dev/null +++ b/payloads/snapshots/googleOpenAIModelTopKParam/google/request.json @@ -0,0 +1,16 @@ +{ + "contents": [ + { + "role": "user", + "parts": [ + { + "text": "Write a short sentence about API gateways." + } + ] + } + ], + "generationConfig": { + "topK": 1, + "maxOutputTokens": 1024 + } +} \ No newline at end of file diff --git a/payloads/snapshots/googleOpenAIModelTopKParam/google/response-streaming.json b/payloads/snapshots/googleOpenAIModelTopKParam/google/response-streaming.json new file mode 100644 index 00000000..35fb2644 --- /dev/null +++ b/payloads/snapshots/googleOpenAIModelTopKParam/google/response-streaming.json @@ -0,0 +1,33 @@ +[ + { + "candidates": [ + { + "content": { + "parts": [ + { + "text": "API gateways serve as a centralized entry point for managing and securing API requests." + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0 + } + ], + "usageMetadata": { + "promptTokenCount": 9, + "candidatesTokenCount": 15, + "totalTokenCount": 508, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 9 + } + ], + "thoughtsTokenCount": 484, + "serviceTier": "standard" + }, + "modelVersion": "gemini-2.5-flash", + "responseId": "2h8CasjSJra9sOIP-J-VqAM" + } +] \ No newline at end of file diff --git a/payloads/snapshots/googleOpenAIModelTopKParam/google/response.json b/payloads/snapshots/googleOpenAIModelTopKParam/google/response.json new file mode 100644 index 00000000..e04749b6 --- /dev/null +++ b/payloads/snapshots/googleOpenAIModelTopKParam/google/response.json @@ -0,0 +1,31 @@ +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "An API gateway acts as a single entry point for managing and securing access to your APIs." + } + ], + "role": "model" + }, + "finishReason": "STOP", + "index": 0 + } + ], + "usageMetadata": { + "promptTokenCount": 9, + "candidatesTokenCount": 18, + "totalTokenCount": 862, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 9 + } + ], + "thoughtsTokenCount": 835, + "serviceTier": "standard" + }, + "modelVersion": "gemini-2.5-flash", + "responseId": "2h8CatPEJomY-8YP-qrAsA0" +} \ No newline at end of file diff --git a/payloads/transforms/google_to_anthropic/googleOpenAIModelTopKParam.json b/payloads/transforms/google_to_anthropic/googleOpenAIModelTopKParam.json new file mode 100644 index 00000000..f4d838a0 --- /dev/null +++ b/payloads/transforms/google_to_anthropic/googleOpenAIModelTopKParam.json @@ -0,0 +1,27 @@ +{ + "model": "claude-sonnet-4-5-20250929", + "id": "msg_01HaceWFZKkAcRjrJxZGu4Ux", + "type": "message", + "role": "assistant", + "content": [ + { + "type": "text", + "text": "An API gateway acts as a single entry point that routes client requests to appropriate backend services while handling cross-cutting concerns like authentication, rate limiting, and logging." + } + ], + "stop_reason": "end_turn", + "stop_sequence": null, + "stop_details": null, + "usage": { + "input_tokens": 17, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "cache_creation": { + "ephemeral_5m_input_tokens": 0, + "ephemeral_1h_input_tokens": 0 + }, + "output_tokens": 35, + "service_tier": "standard", + "inference_geo": "not_available" + } +} \ No newline at end of file diff --git a/payloads/transforms/google_to_chat-completions/googleOpenAIModelTopKParam.json b/payloads/transforms/google_to_chat-completions/googleOpenAIModelTopKParam.json new file mode 100644 index 00000000..48a0e086 --- /dev/null +++ b/payloads/transforms/google_to_chat-completions/googleOpenAIModelTopKParam.json @@ -0,0 +1,35 @@ +{ + "id": "chatcmpl-DePf3UW2reZN7Bj9tefiRJf3P2m3o", + "object": "chat.completion", + "created": 1778524133, + "model": "gpt-5-nano-2025-08-07", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "An API gateway sits between clients and backend services, routing requests and enforcing security.", + "refusal": null, + "annotations": [] + }, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 14, + "completion_tokens": 281, + "total_tokens": 295, + "prompt_tokens_details": { + "cached_tokens": 0, + "audio_tokens": 0 + }, + "completion_tokens_details": { + "reasoning_tokens": 256, + "audio_tokens": 0, + "accepted_prediction_tokens": 0, + "rejected_prediction_tokens": 0 + } + }, + "service_tier": "default", + "system_fingerprint": null +} \ No newline at end of file diff --git a/payloads/transforms/google_to_responses/googleOpenAIModelTopKParam.json b/payloads/transforms/google_to_responses/googleOpenAIModelTopKParam.json new file mode 100644 index 00000000..3971870f --- /dev/null +++ b/payloads/transforms/google_to_responses/googleOpenAIModelTopKParam.json @@ -0,0 +1,78 @@ +{ + "id": "resp_071751ea08988f74006a021fe649a4819cbc1c52ace5d640dd", + "object": "response", + "created_at": 1778524134, + "status": "completed", + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1778524138, + "error": null, + "frequency_penalty": 0, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": 1024, + "max_tool_calls": null, + "model": "gpt-5-nano-2025-08-07", + "moderation": null, + "output": [ + { + "id": "rs_071751ea08988f74006a021fe8e118819cb6da5f875d4eda07", + "type": "reasoning", + "summary": [] + }, + { + "id": "msg_071751ea08988f74006a021fea1754819caf0df8e4c787f577", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "An API gateway acts as a single entry point that handles authentication, rate limiting, and routing for backend services." + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "presence_penalty": 0, + "previous_response_id": null, + "prompt_cache_key": null, + "prompt_cache_retention": "in_memory", + "reasoning": { + "effort": "medium", + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [], + "top_logprobs": 0, + "top_p": 1, + "truncation": "disabled", + "usage": { + "input_tokens": 14, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 254, + "output_tokens_details": { + "reasoning_tokens": 192 + }, + "total_tokens": 268 + }, + "user": null, + "metadata": {}, + "output_text": "An API gateway acts as a single entry point that handles authentication, rate limiting, and routing for backend services." +} \ No newline at end of file