From 006fc081247e10df03ac7b4311fffba7f5637cc6 Mon Sep 17 00:00:00 2001 From: Ken Jiang Date: Wed, 1 Apr 2026 14:35:10 -0400 Subject: [PATCH 1/2] fix errors from when max_tokens is lower than 1 character response --- .../src/requests_expected_differences.json | 9 + .../src/responses_expected_differences.json | 9 + crates/lingua/src/providers/google/convert.rs | 6 +- payloads/cases/simple.ts | 47 +++ .../transforms-streaming.test.ts.snap | 92 ++++++ .../__snapshots__/transforms.test.ts.snap | 312 ++++++++++++++++++ .../anthropic/followup-request.json | 23 ++ .../followup-response-streaming.json | 64 ++++ .../anthropic/followup-response.json | 27 ++ .../anthropic/request.json | 10 + .../anthropic/response-streaming.json | 64 ++++ .../anthropic/response.json | 27 ++ .../chat-completions/error.json | 3 + .../chat-completions/request.json | 10 + .../google/followup-request.json | 26 ++ .../google/followup-response-streaming.json | 25 ++ .../google/followup-response.json | 23 ++ .../google/request.json | 15 + .../google/response-streaming.json | 25 ++ .../google/response.json | 23 ++ .../responses/error.json | 3 + .../responses/request.json | 10 + .../simpleRequestTruncated.json | 4 + .../simpleRequestTruncated.json | 23 ++ .../simpleRequestTruncated.json | 4 + .../simpleRequestTruncated-streaming.json | 64 ++++ .../simpleRequestTruncated.json | 27 ++ .../simpleRequestTruncated.json | 23 ++ .../simpleRequestTruncated.json | 27 ++ .../simpleRequestTruncated.json | 4 + .../simpleRequestTruncated.json | 4 + .../simpleRequestTruncated.json | 27 ++ .../simpleRequestTruncated.json | 23 ++ payloads/transforms/transform_errors.json | 12 +- 34 files changed, 1087 insertions(+), 8 deletions(-) create mode 100644 payloads/snapshots/simpleRequestTruncated/anthropic/followup-request.json create mode 100644 payloads/snapshots/simpleRequestTruncated/anthropic/followup-response-streaming.json create mode 100644 payloads/snapshots/simpleRequestTruncated/anthropic/followup-response.json create mode 100644 payloads/snapshots/simpleRequestTruncated/anthropic/request.json create mode 100644 payloads/snapshots/simpleRequestTruncated/anthropic/response-streaming.json create mode 100644 payloads/snapshots/simpleRequestTruncated/anthropic/response.json create mode 100644 payloads/snapshots/simpleRequestTruncated/chat-completions/error.json create mode 100644 payloads/snapshots/simpleRequestTruncated/chat-completions/request.json create mode 100644 payloads/snapshots/simpleRequestTruncated/google/followup-request.json create mode 100644 payloads/snapshots/simpleRequestTruncated/google/followup-response-streaming.json create mode 100644 payloads/snapshots/simpleRequestTruncated/google/followup-response.json create mode 100644 payloads/snapshots/simpleRequestTruncated/google/request.json create mode 100644 payloads/snapshots/simpleRequestTruncated/google/response-streaming.json create mode 100644 payloads/snapshots/simpleRequestTruncated/google/response.json create mode 100644 payloads/snapshots/simpleRequestTruncated/responses/error.json create mode 100644 payloads/snapshots/simpleRequestTruncated/responses/request.json create mode 100644 payloads/transforms/anthropic_to_chat-completions/simpleRequestTruncated.json create mode 100644 payloads/transforms/anthropic_to_google/simpleRequestTruncated.json create mode 100644 payloads/transforms/anthropic_to_responses/simpleRequestTruncated.json create mode 100644 payloads/transforms/chat-completions_to_anthropic/simpleRequestTruncated-streaming.json create mode 100644 payloads/transforms/chat-completions_to_anthropic/simpleRequestTruncated.json create mode 100644 payloads/transforms/chat-completions_to_google/simpleRequestTruncated.json create mode 100644 payloads/transforms/google_to_anthropic/simpleRequestTruncated.json create mode 100644 payloads/transforms/google_to_chat-completions/simpleRequestTruncated.json create mode 100644 payloads/transforms/google_to_responses/simpleRequestTruncated.json create mode 100644 payloads/transforms/responses_to_anthropic/simpleRequestTruncated.json create mode 100644 payloads/transforms/responses_to_google/simpleRequestTruncated.json diff --git a/crates/coverage-report/src/requests_expected_differences.json b/crates/coverage-report/src/requests_expected_differences.json index 113759bf..83c1371f 100644 --- a/crates/coverage-report/src/requests_expected_differences.json +++ b/crates/coverage-report/src/requests_expected_differences.json @@ -440,6 +440,15 @@ "fields": [ { "pattern": "messages.length", "reason": "Parallel tool results grouped in one Tool message expand to separate function_call_output items in Responses API" } ] + }, + { + "testCase": "simpleRequestTruncated", + "source": "Google", + "target": "*", + "fields": [ + { "pattern": "messages[*].content", "reason": "Empty assistant content (no parts) from truncated Gemini response gains a placeholder empty text part when roundtripped through other providers" }, + { "pattern": "messages.length", "reason": "Empty assistant content from truncated Gemini response may change message count when roundtripped through providers that split/merge messages" } + ] } ] } diff --git a/crates/coverage-report/src/responses_expected_differences.json b/crates/coverage-report/src/responses_expected_differences.json index 9573d6f7..dd9d1286 100644 --- a/crates/coverage-report/src/responses_expected_differences.json +++ b/crates/coverage-report/src/responses_expected_differences.json @@ -283,6 +283,15 @@ "fields": [ { "pattern": "messages.length", "reason": "Google reasoning content expands to separate output items in Responses format" } ] + }, + { + "testCase": "simpleRequestTruncated", + "source": "Google", + "target": "*", + "fields": [ + { "pattern": "messages[*].content", "reason": "Empty assistant content (no parts) from truncated Gemini response gains a placeholder empty text part when roundtripped through other providers" }, + { "pattern": "messages.length", "reason": "Empty assistant content from truncated Gemini response may change message count when roundtripped through providers that split/merge messages" } + ] } ] } diff --git a/crates/lingua/src/providers/google/convert.rs b/crates/lingua/src/providers/google/convert.rs index 809ead7b..04e17fcd 100644 --- a/crates/lingua/src/providers/google/convert.rs +++ b/crates/lingua/src/providers/google/convert.rs @@ -121,9 +121,7 @@ impl TryFromLLM for Message { .ok_or(ConvertError::MissingRequiredField { field: "role".to_string(), })?; - let parts = content.parts.ok_or(ConvertError::MissingRequiredField { - field: "parts".to_string(), - })?; + let parts = content.parts.unwrap_or_default(); match role { "model" => { @@ -454,7 +452,7 @@ impl TryFromLLM for GoogleContent { Ok(GoogleContent { role: Some(role), - parts: Some(parts), + parts: if parts.is_empty() { None } else { Some(parts) }, }) } } diff --git a/payloads/cases/simple.ts b/payloads/cases/simple.ts index 11c152c2..73ad8147 100644 --- a/payloads/cases/simple.ts +++ b/payloads/cases/simple.ts @@ -270,6 +270,53 @@ export const simpleCases: TestCaseCollection = { }, }, + simpleRequestTruncated: { + "chat-completions": { + model: OPENAI_CHAT_COMPLETIONS_MODEL, + messages: [ + { + role: "user", + content: "Write a very long essay about the ocean.", + }, + ], + max_completion_tokens: 1, + }, + responses: { + model: OPENAI_RESPONSES_MODEL, + max_output_tokens: 1, + input: [ + { + role: "user", + content: "Write a very long essay about the ocean.", + }, + ], + }, + anthropic: { + model: ANTHROPIC_MODEL, + max_tokens: 1, + messages: [ + { + role: "user", + content: "Write a very long essay about the ocean.", + }, + ], + }, + google: { + contents: [ + { + role: "user", + parts: [{ text: "Write a very long essay about the ocean." }], + }, + ], + generationConfig: { + maxOutputTokens: 1, + }, + }, + bedrock: null, + "bedrock-anthropic": null, + "vertex-anthropic": null, + }, + toolCallRequest: { "chat-completions": { model: OPENAI_CHAT_COMPLETIONS_MODEL, diff --git a/payloads/scripts/transforms/__snapshots__/transforms-streaming.test.ts.snap b/payloads/scripts/transforms/__snapshots__/transforms-streaming.test.ts.snap index 051c510f..9ee5bdbf 100644 --- a/payloads/scripts/transforms/__snapshots__/transforms-streaming.test.ts.snap +++ b/payloads/scripts/transforms/__snapshots__/transforms-streaming.test.ts.snap @@ -8196,6 +8196,98 @@ exports[`streaming: chat-completions → anthropic > simpleRequest > streaming 1 ] `; +exports[`streaming: chat-completions → anthropic > simpleRequestTruncated > streaming 1`] = ` +[ + { + "data": { + "choices": [ + { + "delta": { + "content": "", + "role": "assistant", + }, + "finish_reason": null, + "index": 0, + }, + ], + "id": "msg_019jRYmhVcfMrQYN3eA6idKL", + "model": "claude-sonnet-4-5-20250929", + "object": "chat.completion.chunk", + "usage": { + "completion_tokens": 1, + "prompt_tokens": 16, + "prompt_tokens_details": { + "cached_tokens": 0, + }, + "total_tokens": 17, + }, + }, + "sourceFormat": "anthropic", + "transformed": true, + }, + { + "data": { + "choices": [], + "object": "chat.completion.chunk", + }, + "sourceFormat": "anthropic", + "transformed": true, + }, + { + "data": { + "choices": [ + { + "delta": { + "content": "#", + "role": "assistant", + }, + "finish_reason": null, + "index": 0, + }, + ], + "object": "chat.completion.chunk", + }, + "sourceFormat": "anthropic", + "transformed": true, + }, + { + "data": { + "choices": [], + "object": "chat.completion.chunk", + }, + "sourceFormat": "anthropic", + "transformed": true, + }, + { + "data": { + "choices": [ + { + "delta": {}, + "finish_reason": "length", + "index": 0, + }, + ], + "object": "chat.completion.chunk", + "usage": { + "completion_tokens": 1, + "prompt_tokens": 16, + "prompt_tokens_details": { + "cached_tokens": 0, + }, + "total_tokens": 17, + }, + }, + "sourceFormat": "anthropic", + "transformed": true, + }, + { + "data": {}, + "sourceFormat": "anthropic", + "transformed": true, + }, +] +`; + exports[`streaming: chat-completions → anthropic > systemMessageArrayContent > streaming 1`] = ` [ { diff --git a/payloads/scripts/transforms/__snapshots__/transforms.test.ts.snap b/payloads/scripts/transforms/__snapshots__/transforms.test.ts.snap index 670ffce8..a7cc013c 100644 --- a/payloads/scripts/transforms/__snapshots__/transforms.test.ts.snap +++ b/payloads/scripts/transforms/__snapshots__/transforms.test.ts.snap @@ -930,6 +930,19 @@ exports[`anthropic → chat-completions > simpleRequest > response 1`] = ` } `; +exports[`anthropic → chat-completions > simpleRequestTruncated > request 1`] = ` +{ + "max_completion_tokens": 1, + "messages": [ + { + "content": "Write a very long essay about the ocean.", + "role": "user", + }, + ], + "model": "gpt-5-nano", +} +`; + exports[`anthropic → chat-completions > stopSequencesParam > request 1`] = ` { "max_completion_tokens": 1024, @@ -2936,6 +2949,41 @@ exports[`anthropic → google > simpleRequest > response 1`] = ` } `; +exports[`anthropic → google > simpleRequestTruncated > request 1`] = ` +{ + "contents": [ + { + "parts": [ + { + "text": "Write a very long essay about the ocean.", + }, + ], + "role": "user", + }, + ], + "generationConfig": { + "maxOutputTokens": 1, + "responseSchema": null, + }, + "model": "gemini-2.5-flash", +} +`; + +exports[`anthropic → google > simpleRequestTruncated > response 1`] = ` +{ + "content": [], + "id": "msg_transformed", + "model": "gemini-2.5-flash", + "role": "assistant", + "stop_reason": "max_tokens", + "type": "message", + "usage": { + "input_tokens": 10, + "output_tokens": 0, + }, +} +`; + exports[`anthropic → google > stopSequencesParam > request 1`] = ` { "contents": [ @@ -5082,6 +5130,19 @@ exports[`anthropic → responses > simpleRequest > response 1`] = ` } `; +exports[`anthropic → responses > simpleRequestTruncated > request 1`] = ` +{ + "input": [ + { + "content": "Write a very long essay about the ocean.", + "role": "user", + }, + ], + "max_output_tokens": 1, + "model": "gpt-5-nano", +} +`; + exports[`anthropic → responses > stopSequencesParam > request 1`] = ` { "input": [ @@ -6986,6 +7047,47 @@ exports[`chat-completions → anthropic > simpleRequest > response 1`] = ` } `; +exports[`chat-completions → anthropic > simpleRequestTruncated > request 1`] = ` +{ + "max_tokens": 1, + "messages": [ + { + "content": "Write a very long essay about the ocean.", + "role": "user", + }, + ], + "model": "claude-sonnet-4-5-20250929", +} +`; + +exports[`chat-completions → anthropic > simpleRequestTruncated > response 1`] = ` +{ + "choices": [ + { + "finish_reason": "length", + "index": 0, + "message": { + "annotations": [], + "content": "#", + "role": "assistant", + }, + }, + ], + "created": 0, + "id": "chatcmpl-01RPssM6iLPaEsv3XtZE3Uuu", + "model": "claude-sonnet-4-5-20250929", + "object": "chat.completion", + "usage": { + "completion_tokens": 1, + "prompt_tokens": 16, + "prompt_tokens_details": { + "cached_tokens": 0, + }, + "total_tokens": 17, + }, +} +`; + exports[`chat-completions → anthropic > stopSequencesParam > request 1`] = ` { "max_tokens": 4096, @@ -9123,6 +9225,50 @@ Alright, a straightforward question: "What is the capital of France?" Okay, let' } `; +exports[`chat-completions → google > simpleRequestTruncated > request 1`] = ` +{ + "contents": [ + { + "parts": [ + { + "text": "Write a very long essay about the ocean.", + }, + ], + "role": "user", + }, + ], + "generationConfig": { + "maxOutputTokens": 1, + "responseSchema": null, + }, + "model": "gemini-2.5-flash", +} +`; + +exports[`chat-completions → google > simpleRequestTruncated > response 1`] = ` +{ + "choices": [ + { + "finish_reason": "length", + "index": 0, + "message": { + "annotations": [], + "role": "assistant", + }, + }, + ], + "created": 0, + "id": "chatcmpl-transformed", + "model": "gemini-2.5-flash", + "object": "chat.completion", + "usage": { + "completion_tokens": 0, + "prompt_tokens": 10, + "total_tokens": 10, + }, +} +`; + exports[`chat-completions → google > stopSequencesParam > request 1`] = ` { "contents": [ @@ -10582,6 +10728,45 @@ exports[`google → anthropic > simpleRequest > response 1`] = ` } `; +exports[`google → anthropic > simpleRequestTruncated > request 1`] = ` +{ + "max_tokens": 1, + "messages": [ + { + "content": "Write a very long essay about the ocean.", + "role": "user", + }, + ], + "model": "claude-sonnet-4-5-20250929", +} +`; + +exports[`google → anthropic > simpleRequestTruncated > response 1`] = ` +{ + "candidates": [ + { + "content": { + "parts": [ + { + "text": "#", + }, + ], + "role": "model", + }, + "finishReason": "MAX_TOKENS", + "index": 0, + }, + ], + "modelVersion": "claude-sonnet-4-5-20250929", + "usageMetadata": { + "cachedContentTokenCount": 0, + "candidatesTokenCount": 1, + "promptTokenCount": 16, + "totalTokenCount": 17, + }, +} +`; + exports[`google → anthropic > stopSequencesParam > request 1`] = ` { "max_tokens": 4096, @@ -11839,6 +12024,19 @@ exports[`google → chat-completions > simpleRequest > response 1`] = ` } `; +exports[`google → chat-completions > simpleRequestTruncated > request 1`] = ` +{ + "max_completion_tokens": 1, + "messages": [ + { + "content": "Write a very long essay about the ocean.", + "role": "user", + }, + ], + "model": "gpt-5-nano", +} +`; + exports[`google → chat-completions > stopSequencesParam > request 1`] = ` { "messages": [ @@ -13285,6 +13483,19 @@ exports[`google → responses > simpleRequest > response 1`] = ` } `; +exports[`google → responses > simpleRequestTruncated > request 1`] = ` +{ + "input": [ + { + "content": "Write a very long essay about the ocean.", + "role": "user", + }, + ], + "max_output_tokens": 1, + "model": "gpt-5-nano", +} +`; + exports[`google → responses > stopSequencesParam > request 1`] = ` { "input": [ @@ -15075,6 +15286,60 @@ exports[`responses → anthropic > simpleRequest > response 1`] = ` } `; +exports[`responses → anthropic > simpleRequestTruncated > request 1`] = ` +{ + "max_tokens": 1, + "messages": [ + { + "content": "Write a very long essay about the ocean.", + "role": "user", + }, + ], + "model": "claude-sonnet-4-5-20250929", +} +`; + +exports[`responses → anthropic > simpleRequestTruncated > response 1`] = ` +{ + "created_at": 0, + "id": "resp_01REYmqzNs2TtvsJhjED1FZD", + "incomplete_details": null, + "model": "claude-sonnet-4-5-20250929", + "object": "response", + "output": [ + { + "content": [ + { + "annotations": [], + "text": "#", + "type": "output_text", + }, + ], + "id": "msg_transformed_item_0", + "role": "assistant", + "status": "completed", + "type": "message", + }, + ], + "output_text": "#", + "parallel_tool_calls": false, + "status": "incomplete", + "tool_choice": "none", + "tools": [], + "usage": { + "input_tokens": 16, + "input_tokens_details": { + "cached_tokens": 0, + }, + "output_tokens": 1, + "output_tokens_details": { + "reasoning_tokens": 0, + }, + "total_tokens": 17, + }, +} +`; + exports[`responses → anthropic > storeDisabledParam > request 1`] = ` { "max_tokens": 4096, @@ -17523,6 +17788,53 @@ Okay, here's what I'm thinking. First, I need to understand the fundamental ques } `; +exports[`responses → google > simpleRequestTruncated > request 1`] = ` +{ + "contents": [ + { + "parts": [ + { + "text": "Write a very long essay about the ocean.", + }, + ], + "role": "user", + }, + ], + "generationConfig": { + "maxOutputTokens": 1, + "responseSchema": null, + }, + "model": "gemini-2.5-flash", +} +`; + +exports[`responses → google > simpleRequestTruncated > response 1`] = ` +{ + "created_at": 0, + "id": "resp_transformed", + "incomplete_details": null, + "model": "gemini-2.5-flash", + "object": "response", + "output": [], + "output_text": "", + "parallel_tool_calls": false, + "status": "incomplete", + "tool_choice": "none", + "tools": [], + "usage": { + "input_tokens": 10, + "input_tokens_details": { + "cached_tokens": 0, + }, + "output_tokens": 0, + "output_tokens_details": { + "reasoning_tokens": 0, + }, + "total_tokens": 10, + }, +} +`; + exports[`responses → google > storeDisabledParam > request 1`] = ` { "contents": [ diff --git a/payloads/snapshots/simpleRequestTruncated/anthropic/followup-request.json b/payloads/snapshots/simpleRequestTruncated/anthropic/followup-request.json new file mode 100644 index 00000000..2d93e462 --- /dev/null +++ b/payloads/snapshots/simpleRequestTruncated/anthropic/followup-request.json @@ -0,0 +1,23 @@ +{ + "model": "claude-sonnet-4-5-20250929", + "max_tokens": 1, + "messages": [ + { + "role": "user", + "content": "Write a very long essay about the ocean." + }, + { + "role": "assistant", + "content": [ + { + "type": "text", + "text": "#" + } + ] + }, + { + "role": "user", + "content": "What should I do next?" + } + ] +} \ No newline at end of file diff --git a/payloads/snapshots/simpleRequestTruncated/anthropic/followup-response-streaming.json b/payloads/snapshots/simpleRequestTruncated/anthropic/followup-response-streaming.json new file mode 100644 index 00000000..8599e759 --- /dev/null +++ b/payloads/snapshots/simpleRequestTruncated/anthropic/followup-response-streaming.json @@ -0,0 +1,64 @@ +[ + { + "type": "message_start", + "message": { + "model": "claude-sonnet-4-5-20250929", + "id": "msg_01AuHfpojdb7upqvrpKfxMrF", + "type": "message", + "role": "assistant", + "content": [], + "stop_reason": null, + "stop_sequence": null, + "stop_details": null, + "usage": { + "input_tokens": 29, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "cache_creation": { + "ephemeral_5m_input_tokens": 0, + "ephemeral_1h_input_tokens": 0 + }, + "output_tokens": 1, + "service_tier": "standard", + "inference_geo": "not_available" + } + } + }, + { + "type": "content_block_start", + "index": 0, + "content_block": { + "type": "text", + "text": "" + } + }, + { + "type": "content_block_delta", + "index": 0, + "delta": { + "type": "text_delta", + "text": "Based" + } + }, + { + "type": "content_block_stop", + "index": 0 + }, + { + "type": "message_delta", + "delta": { + "stop_reason": "max_tokens", + "stop_sequence": null, + "stop_details": null + }, + "usage": { + "input_tokens": 29, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "output_tokens": 1 + } + }, + { + "type": "message_stop" + } +] \ No newline at end of file diff --git a/payloads/snapshots/simpleRequestTruncated/anthropic/followup-response.json b/payloads/snapshots/simpleRequestTruncated/anthropic/followup-response.json new file mode 100644 index 00000000..390e9bcd --- /dev/null +++ b/payloads/snapshots/simpleRequestTruncated/anthropic/followup-response.json @@ -0,0 +1,27 @@ +{ + "model": "claude-sonnet-4-5-20250929", + "id": "msg_01TFxbLmDpareBszF1f7mgYq", + "type": "message", + "role": "assistant", + "content": [ + { + "type": "text", + "text": "Here" + } + ], + "stop_reason": "max_tokens", + "stop_sequence": null, + "stop_details": null, + "usage": { + "input_tokens": 29, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "cache_creation": { + "ephemeral_5m_input_tokens": 0, + "ephemeral_1h_input_tokens": 0 + }, + "output_tokens": 1, + "service_tier": "standard", + "inference_geo": "not_available" + } +} \ No newline at end of file diff --git a/payloads/snapshots/simpleRequestTruncated/anthropic/request.json b/payloads/snapshots/simpleRequestTruncated/anthropic/request.json new file mode 100644 index 00000000..82d5e79e --- /dev/null +++ b/payloads/snapshots/simpleRequestTruncated/anthropic/request.json @@ -0,0 +1,10 @@ +{ + "model": "claude-sonnet-4-5-20250929", + "max_tokens": 1, + "messages": [ + { + "role": "user", + "content": "Write a very long essay about the ocean." + } + ] +} \ No newline at end of file diff --git a/payloads/snapshots/simpleRequestTruncated/anthropic/response-streaming.json b/payloads/snapshots/simpleRequestTruncated/anthropic/response-streaming.json new file mode 100644 index 00000000..a915f4a5 --- /dev/null +++ b/payloads/snapshots/simpleRequestTruncated/anthropic/response-streaming.json @@ -0,0 +1,64 @@ +[ + { + "type": "message_start", + "message": { + "model": "claude-sonnet-4-5-20250929", + "id": "msg_01683KyvA46TSV6Coh22BwH4", + "type": "message", + "role": "assistant", + "content": [], + "stop_reason": null, + "stop_sequence": null, + "stop_details": null, + "usage": { + "input_tokens": 16, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "cache_creation": { + "ephemeral_5m_input_tokens": 0, + "ephemeral_1h_input_tokens": 0 + }, + "output_tokens": 1, + "service_tier": "standard", + "inference_geo": "not_available" + } + } + }, + { + "type": "content_block_start", + "index": 0, + "content_block": { + "type": "text", + "text": "" + } + }, + { + "type": "content_block_delta", + "index": 0, + "delta": { + "type": "text_delta", + "text": "#" + } + }, + { + "type": "content_block_stop", + "index": 0 + }, + { + "type": "message_delta", + "delta": { + "stop_reason": "max_tokens", + "stop_sequence": null, + "stop_details": null + }, + "usage": { + "input_tokens": 16, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "output_tokens": 1 + } + }, + { + "type": "message_stop" + } +] \ No newline at end of file diff --git a/payloads/snapshots/simpleRequestTruncated/anthropic/response.json b/payloads/snapshots/simpleRequestTruncated/anthropic/response.json new file mode 100644 index 00000000..ffbe7eb2 --- /dev/null +++ b/payloads/snapshots/simpleRequestTruncated/anthropic/response.json @@ -0,0 +1,27 @@ +{ + "model": "claude-sonnet-4-5-20250929", + "id": "msg_01RAgYa2meYUio8U6DsxPGD1", + "type": "message", + "role": "assistant", + "content": [ + { + "type": "text", + "text": "#" + } + ], + "stop_reason": "max_tokens", + "stop_sequence": null, + "stop_details": null, + "usage": { + "input_tokens": 16, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "cache_creation": { + "ephemeral_5m_input_tokens": 0, + "ephemeral_1h_input_tokens": 0 + }, + "output_tokens": 1, + "service_tier": "standard", + "inference_geo": "not_available" + } +} \ No newline at end of file diff --git a/payloads/snapshots/simpleRequestTruncated/chat-completions/error.json b/payloads/snapshots/simpleRequestTruncated/chat-completions/error.json new file mode 100644 index 00000000..53af711d --- /dev/null +++ b/payloads/snapshots/simpleRequestTruncated/chat-completions/error.json @@ -0,0 +1,3 @@ +{ + "error": "Error: 400 Could not finish the message because max_tokens or model output limit was reached. Please try again with higher max_tokens." +} \ No newline at end of file diff --git a/payloads/snapshots/simpleRequestTruncated/chat-completions/request.json b/payloads/snapshots/simpleRequestTruncated/chat-completions/request.json new file mode 100644 index 00000000..025bab59 --- /dev/null +++ b/payloads/snapshots/simpleRequestTruncated/chat-completions/request.json @@ -0,0 +1,10 @@ +{ + "model": "gpt-5-nano", + "messages": [ + { + "role": "user", + "content": "Write a very long essay about the ocean." + } + ], + "max_completion_tokens": 1 +} \ No newline at end of file diff --git a/payloads/snapshots/simpleRequestTruncated/google/followup-request.json b/payloads/snapshots/simpleRequestTruncated/google/followup-request.json new file mode 100644 index 00000000..b2a10d8b --- /dev/null +++ b/payloads/snapshots/simpleRequestTruncated/google/followup-request.json @@ -0,0 +1,26 @@ +{ + "contents": [ + { + "role": "user", + "parts": [ + { + "text": "Write a very long essay about the ocean." + } + ] + }, + { + "role": "model" + }, + { + "role": "user", + "parts": [ + { + "text": "What should I do next?" + } + ] + } + ], + "generationConfig": { + "maxOutputTokens": 1 + } +} \ No newline at end of file diff --git a/payloads/snapshots/simpleRequestTruncated/google/followup-response-streaming.json b/payloads/snapshots/simpleRequestTruncated/google/followup-response-streaming.json new file mode 100644 index 00000000..39116d0c --- /dev/null +++ b/payloads/snapshots/simpleRequestTruncated/google/followup-response-streaming.json @@ -0,0 +1,25 @@ +[ + { + "candidates": [ + { + "content": { + "role": "model" + }, + "finishReason": "MAX_TOKENS", + "index": 0 + } + ], + "usageMetadata": { + "promptTokenCount": 18, + "totalTokenCount": 18, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 18 + } + ] + }, + "modelVersion": "gemini-2.5-flash", + "responseId": "5GDNae2iH-fJ-8YP6teu0Qk" + } +] \ No newline at end of file diff --git a/payloads/snapshots/simpleRequestTruncated/google/followup-response.json b/payloads/snapshots/simpleRequestTruncated/google/followup-response.json new file mode 100644 index 00000000..a5bd5b00 --- /dev/null +++ b/payloads/snapshots/simpleRequestTruncated/google/followup-response.json @@ -0,0 +1,23 @@ +{ + "candidates": [ + { + "content": { + "role": "model" + }, + "finishReason": "MAX_TOKENS", + "index": 0 + } + ], + "usageMetadata": { + "promptTokenCount": 18, + "totalTokenCount": 18, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 18 + } + ] + }, + "modelVersion": "gemini-2.5-flash", + "responseId": "5GDNaf2uH8P4jrEPzfnG2QE" +} \ No newline at end of file diff --git a/payloads/snapshots/simpleRequestTruncated/google/request.json b/payloads/snapshots/simpleRequestTruncated/google/request.json new file mode 100644 index 00000000..c86b2332 --- /dev/null +++ b/payloads/snapshots/simpleRequestTruncated/google/request.json @@ -0,0 +1,15 @@ +{ + "contents": [ + { + "role": "user", + "parts": [ + { + "text": "Write a very long essay about the ocean." + } + ] + } + ], + "generationConfig": { + "maxOutputTokens": 1 + } +} \ No newline at end of file diff --git a/payloads/snapshots/simpleRequestTruncated/google/response-streaming.json b/payloads/snapshots/simpleRequestTruncated/google/response-streaming.json new file mode 100644 index 00000000..01b6bf13 --- /dev/null +++ b/payloads/snapshots/simpleRequestTruncated/google/response-streaming.json @@ -0,0 +1,25 @@ +[ + { + "candidates": [ + { + "content": { + "role": "model" + }, + "finishReason": "MAX_TOKENS", + "index": 0 + } + ], + "usageMetadata": { + "promptTokenCount": 10, + "totalTokenCount": 10, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 10 + } + ] + }, + "modelVersion": "gemini-2.5-flash", + "responseId": "42DNaencO6_O-8YP9eqRkAY" + } +] \ No newline at end of file diff --git a/payloads/snapshots/simpleRequestTruncated/google/response.json b/payloads/snapshots/simpleRequestTruncated/google/response.json new file mode 100644 index 00000000..c232a13c --- /dev/null +++ b/payloads/snapshots/simpleRequestTruncated/google/response.json @@ -0,0 +1,23 @@ +{ + "candidates": [ + { + "content": { + "role": "model" + }, + "finishReason": "MAX_TOKENS", + "index": 0 + } + ], + "usageMetadata": { + "promptTokenCount": 10, + "totalTokenCount": 10, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 10 + } + ] + }, + "modelVersion": "gemini-2.5-flash", + "responseId": "5GDNaZQUzImOsQ-80pZh" +} \ No newline at end of file diff --git a/payloads/snapshots/simpleRequestTruncated/responses/error.json b/payloads/snapshots/simpleRequestTruncated/responses/error.json new file mode 100644 index 00000000..6ce5d3eb --- /dev/null +++ b/payloads/snapshots/simpleRequestTruncated/responses/error.json @@ -0,0 +1,3 @@ +{ + "error": "Error: 400 Invalid 'max_output_tokens': integer below minimum value. Expected a value >= 16, but got 1 instead." +} \ No newline at end of file diff --git a/payloads/snapshots/simpleRequestTruncated/responses/request.json b/payloads/snapshots/simpleRequestTruncated/responses/request.json new file mode 100644 index 00000000..ac5c407f --- /dev/null +++ b/payloads/snapshots/simpleRequestTruncated/responses/request.json @@ -0,0 +1,10 @@ +{ + "model": "gpt-5-nano", + "max_output_tokens": 1, + "input": [ + { + "role": "user", + "content": "Write a very long essay about the ocean." + } + ] +} \ No newline at end of file diff --git a/payloads/transforms/anthropic_to_chat-completions/simpleRequestTruncated.json b/payloads/transforms/anthropic_to_chat-completions/simpleRequestTruncated.json new file mode 100644 index 00000000..41c704a8 --- /dev/null +++ b/payloads/transforms/anthropic_to_chat-completions/simpleRequestTruncated.json @@ -0,0 +1,4 @@ +{ + "error": "400 Could not finish the message because max_tokens or model output limit was reached. Please try again with higher max_tokens.", + "name": "Error" +} \ No newline at end of file diff --git a/payloads/transforms/anthropic_to_google/simpleRequestTruncated.json b/payloads/transforms/anthropic_to_google/simpleRequestTruncated.json new file mode 100644 index 00000000..3a12e2ab --- /dev/null +++ b/payloads/transforms/anthropic_to_google/simpleRequestTruncated.json @@ -0,0 +1,23 @@ +{ + "candidates": [ + { + "content": { + "role": "model" + }, + "finishReason": "MAX_TOKENS", + "index": 0 + } + ], + "usageMetadata": { + "promptTokenCount": 10, + "totalTokenCount": 10, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 10 + } + ] + }, + "modelVersion": "gemini-2.5-flash", + "responseId": "5mDNaaamLdWhsOIP3_TssQk" +} \ No newline at end of file diff --git a/payloads/transforms/anthropic_to_responses/simpleRequestTruncated.json b/payloads/transforms/anthropic_to_responses/simpleRequestTruncated.json new file mode 100644 index 00000000..f7f6bb9a --- /dev/null +++ b/payloads/transforms/anthropic_to_responses/simpleRequestTruncated.json @@ -0,0 +1,4 @@ +{ + "error": "400 Invalid 'max_output_tokens': integer below minimum value. Expected a value >= 16, but got 1 instead.", + "name": "Error" +} \ No newline at end of file diff --git a/payloads/transforms/chat-completions_to_anthropic/simpleRequestTruncated-streaming.json b/payloads/transforms/chat-completions_to_anthropic/simpleRequestTruncated-streaming.json new file mode 100644 index 00000000..2a43b125 --- /dev/null +++ b/payloads/transforms/chat-completions_to_anthropic/simpleRequestTruncated-streaming.json @@ -0,0 +1,64 @@ +[ + { + "type": "message_start", + "message": { + "model": "claude-sonnet-4-5-20250929", + "id": "msg_019jRYmhVcfMrQYN3eA6idKL", + "type": "message", + "role": "assistant", + "content": [], + "stop_reason": null, + "stop_sequence": null, + "stop_details": null, + "usage": { + "input_tokens": 16, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "cache_creation": { + "ephemeral_5m_input_tokens": 0, + "ephemeral_1h_input_tokens": 0 + }, + "output_tokens": 1, + "service_tier": "standard", + "inference_geo": "not_available" + } + } + }, + { + "type": "content_block_start", + "index": 0, + "content_block": { + "type": "text", + "text": "" + } + }, + { + "type": "content_block_delta", + "index": 0, + "delta": { + "type": "text_delta", + "text": "#" + } + }, + { + "type": "content_block_stop", + "index": 0 + }, + { + "type": "message_delta", + "delta": { + "stop_reason": "max_tokens", + "stop_sequence": null, + "stop_details": null + }, + "usage": { + "input_tokens": 16, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "output_tokens": 1 + } + }, + { + "type": "message_stop" + } +] \ No newline at end of file diff --git a/payloads/transforms/chat-completions_to_anthropic/simpleRequestTruncated.json b/payloads/transforms/chat-completions_to_anthropic/simpleRequestTruncated.json new file mode 100644 index 00000000..b096811a --- /dev/null +++ b/payloads/transforms/chat-completions_to_anthropic/simpleRequestTruncated.json @@ -0,0 +1,27 @@ +{ + "model": "claude-sonnet-4-5-20250929", + "id": "msg_01RPssM6iLPaEsv3XtZE3Uuu", + "type": "message", + "role": "assistant", + "content": [ + { + "type": "text", + "text": "#" + } + ], + "stop_reason": "max_tokens", + "stop_sequence": null, + "stop_details": null, + "usage": { + "input_tokens": 16, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "cache_creation": { + "ephemeral_5m_input_tokens": 0, + "ephemeral_1h_input_tokens": 0 + }, + "output_tokens": 1, + "service_tier": "standard", + "inference_geo": "not_available" + } +} \ No newline at end of file diff --git a/payloads/transforms/chat-completions_to_google/simpleRequestTruncated.json b/payloads/transforms/chat-completions_to_google/simpleRequestTruncated.json new file mode 100644 index 00000000..17be0ee4 --- /dev/null +++ b/payloads/transforms/chat-completions_to_google/simpleRequestTruncated.json @@ -0,0 +1,23 @@ +{ + "candidates": [ + { + "content": { + "role": "model" + }, + "finishReason": "MAX_TOKENS", + "index": 0 + } + ], + "usageMetadata": { + "promptTokenCount": 10, + "totalTokenCount": 10, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 10 + } + ] + }, + "modelVersion": "gemini-2.5-flash", + "responseId": "5mDNacKCFZeU-8YP2tDdgQM" +} \ No newline at end of file diff --git a/payloads/transforms/google_to_anthropic/simpleRequestTruncated.json b/payloads/transforms/google_to_anthropic/simpleRequestTruncated.json new file mode 100644 index 00000000..49e90255 --- /dev/null +++ b/payloads/transforms/google_to_anthropic/simpleRequestTruncated.json @@ -0,0 +1,27 @@ +{ + "model": "claude-sonnet-4-5-20250929", + "id": "msg_01VmtawEdpucoahcYXGMZiyY", + "type": "message", + "role": "assistant", + "content": [ + { + "type": "text", + "text": "#" + } + ], + "stop_reason": "max_tokens", + "stop_sequence": null, + "stop_details": null, + "usage": { + "input_tokens": 16, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "cache_creation": { + "ephemeral_5m_input_tokens": 0, + "ephemeral_1h_input_tokens": 0 + }, + "output_tokens": 1, + "service_tier": "standard", + "inference_geo": "not_available" + } +} \ No newline at end of file diff --git a/payloads/transforms/google_to_chat-completions/simpleRequestTruncated.json b/payloads/transforms/google_to_chat-completions/simpleRequestTruncated.json new file mode 100644 index 00000000..41c704a8 --- /dev/null +++ b/payloads/transforms/google_to_chat-completions/simpleRequestTruncated.json @@ -0,0 +1,4 @@ +{ + "error": "400 Could not finish the message because max_tokens or model output limit was reached. Please try again with higher max_tokens.", + "name": "Error" +} \ No newline at end of file diff --git a/payloads/transforms/google_to_responses/simpleRequestTruncated.json b/payloads/transforms/google_to_responses/simpleRequestTruncated.json new file mode 100644 index 00000000..f7f6bb9a --- /dev/null +++ b/payloads/transforms/google_to_responses/simpleRequestTruncated.json @@ -0,0 +1,4 @@ +{ + "error": "400 Invalid 'max_output_tokens': integer below minimum value. Expected a value >= 16, but got 1 instead.", + "name": "Error" +} \ No newline at end of file diff --git a/payloads/transforms/responses_to_anthropic/simpleRequestTruncated.json b/payloads/transforms/responses_to_anthropic/simpleRequestTruncated.json new file mode 100644 index 00000000..faf3dd5e --- /dev/null +++ b/payloads/transforms/responses_to_anthropic/simpleRequestTruncated.json @@ -0,0 +1,27 @@ +{ + "model": "claude-sonnet-4-5-20250929", + "id": "msg_01REYmqzNs2TtvsJhjED1FZD", + "type": "message", + "role": "assistant", + "content": [ + { + "type": "text", + "text": "#" + } + ], + "stop_reason": "max_tokens", + "stop_sequence": null, + "stop_details": null, + "usage": { + "input_tokens": 16, + "cache_creation_input_tokens": 0, + "cache_read_input_tokens": 0, + "cache_creation": { + "ephemeral_5m_input_tokens": 0, + "ephemeral_1h_input_tokens": 0 + }, + "output_tokens": 1, + "service_tier": "standard", + "inference_geo": "not_available" + } +} \ No newline at end of file diff --git a/payloads/transforms/responses_to_google/simpleRequestTruncated.json b/payloads/transforms/responses_to_google/simpleRequestTruncated.json new file mode 100644 index 00000000..02dfbe94 --- /dev/null +++ b/payloads/transforms/responses_to_google/simpleRequestTruncated.json @@ -0,0 +1,23 @@ +{ + "candidates": [ + { + "content": { + "role": "model" + }, + "finishReason": "MAX_TOKENS", + "index": 0 + } + ], + "usageMetadata": { + "promptTokenCount": 10, + "totalTokenCount": 10, + "promptTokensDetails": [ + { + "modality": "TEXT", + "tokenCount": 10 + } + ] + }, + "modelVersion": "gemini-2.5-flash", + "responseId": "52DNaZfiB7GR-8YPkuPesA4" +} \ No newline at end of file diff --git a/payloads/transforms/transform_errors.json b/payloads/transforms/transform_errors.json index 6bc1ccbe..8c346203 100644 --- a/payloads/transforms/transform_errors.json +++ b/payloads/transforms/transform_errors.json @@ -16,7 +16,8 @@ "serviceTierParam": "OpenAI rejected: 'max_tokens' is not supported with this model, use 'max_completion_tokens' instead", "metadataParam": "OpenAI rejected: 'metadata' parameter is only allowed when 'store' is enabled", "safetyIdentifierParam": "OpenAI rejected: 'metadata' parameter is only allowed when 'store' is enabled", - "streamParam": "Streaming response cannot be deserialized as a non-streaming chat completions response" + "streamParam": "Streaming response cannot be deserialized as a non-streaming chat completions response", + "simpleRequestTruncated": "Deserialization failed: missing field `choices`" }, "anthropic_to_responses": { "webSearchToolParam": "Tool 'web_search' of type 'web_search_20250305' is not supported by responses", @@ -29,7 +30,8 @@ "documentContentParam": "Anthropic document content (File type) is not supported by OpenAI responses", "temperatureParam": "OpenAI rejected: 'temperature' is not supported with this model", "topPParam": "OpenAI rejected: 'top_p' is not supported with this model", - "streamParam": "Streaming response cannot be deserialized as a responses API response" + "streamParam": "Streaming response cannot be deserialized as a responses API response", + "simpleRequestTruncated": "OpenAI rejected: Invalid 'max_output_tokens': integer below minimum value. Expected a value >= 16, but got 1 instead." }, "google_to_anthropic": { "toolCallRequest": "Anthropic rejected: tool input_schema type must be 'object', not null", @@ -49,7 +51,8 @@ "toolChoiceAutoParam": "OpenAI rejected: tool parameter schema must be 'object' or 'boolean', not null", "toolChoiceAnyParam": "OpenAI rejected: tool parameter schema must be 'object' or 'boolean', not null", "toolChoiceNoneParam": "OpenAI rejected: tool parameter schema must be 'object' or 'boolean', not null", - "toolModeValidatedParam": "OpenAI rejected: tool parameter schema must be 'object' or 'boolean', not null" + "toolModeValidatedParam": "OpenAI rejected: tool parameter schema must be 'object' or 'boolean', not null", + "simpleRequestTruncated": "Deserialization failed: missing field `choices`" }, "google_to_responses": { "toolCallRequest": "OpenAI rejected: tool parameter schema must be 'object' or 'boolean', not null", @@ -60,6 +63,7 @@ "toolChoiceAutoParam": "OpenAI rejected: tool parameter schema must be 'object' or 'boolean', not null", "toolChoiceAnyParam": "OpenAI rejected: tool parameter schema must be 'object' or 'boolean', not null", "toolChoiceNoneParam": "OpenAI rejected: tool parameter schema must be 'object' or 'boolean', not null", - "toolModeValidatedParam": "OpenAI rejected: tool parameter schema must be 'object' or 'boolean', not null" + "toolModeValidatedParam": "OpenAI rejected: tool parameter schema must be 'object' or 'boolean', not null", + "simpleRequestTruncated": "OpenAI rejected: Invalid 'max_output_tokens': integer below minimum value. Expected a value >= 16, but got 1 instead." } } From de3a48fcfa409c883ffb3c49c965eff75500a2f8 Mon Sep 17 00:00:00 2001 From: Ken Jiang Date: Wed, 1 Apr 2026 14:53:37 -0400 Subject: [PATCH 2/2] fix small bug --- crates/lingua/src/providers/google/convert.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/lingua/src/providers/google/convert.rs b/crates/lingua/src/providers/google/convert.rs index 04e17fcd..d937b394 100644 --- a/crates/lingua/src/providers/google/convert.rs +++ b/crates/lingua/src/providers/google/convert.rs @@ -121,7 +121,17 @@ impl TryFromLLM for Message { .ok_or(ConvertError::MissingRequiredField { field: "role".to_string(), })?; - let parts = content.parts.unwrap_or_default(); + // Only allow missing parts for "model" role — Gemini may omit parts when + // maxOutputTokens is exhausted (MAX_TOKENS finish reason). For other roles, + // missing parts means this isn't a Google-format message (e.g. Anthropic + // messages have "content" not "parts"). + let parts: Vec = if role == "model" { + content.parts.unwrap_or_default() + } else { + content.parts.ok_or(ConvertError::MissingRequiredField { + field: "parts".to_string(), + })? + }; match role { "model" => {