mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-11 08:38:24 +00:00
fix(gateway): translated chat answers carry a chatcmpl id and object (#47366)
The provider configs leave id out because Python mints it on its own ModelResponse, so non-streaming Anthropic, Bedrock, Vertex and Gemini answers reached gateway clients with no id and no object. The chat handler now names each translated answer as Python does: a minted chatcmpl- id, or Gemini's responseId, and object chat.completion. It does so before the answer is cached, so a cache hit replays the same id.
This commit is contained in:
parent
28feeab36c
commit
afebcb3455
3 changed files with 53 additions and 2 deletions
|
|
@ -474,6 +474,23 @@ async fn bedrock_signs_with_the_keys_region_and_endpoint_in_the_config() {
|
|||
);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::anthropic("claude")]
|
||||
#[case::bedrock("bedrock")]
|
||||
#[tokio::test]
|
||||
async fn a_translated_answer_carries_a_chat_completion_id_and_object(#[case] model: &str) {
|
||||
let harness = harness().await;
|
||||
let answer = harness
|
||||
.json(json!({"model": model, "messages": answered("tooluse_1"), "max_tokens": 16}))
|
||||
.await;
|
||||
|
||||
assert!(
|
||||
answer["id"].as_str().unwrap().starts_with("chatcmpl-"),
|
||||
"{answer}"
|
||||
);
|
||||
assert_eq!(answer["object"], "chat.completion");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn bedrock_tool_round_trip() {
|
||||
let harness = harness().await;
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@ pub(super) async fn execute(
|
|||
})?;
|
||||
config
|
||||
.transform_response(&model, ProviderChatResponseData { body }, &stream_shape)
|
||||
.map(named)
|
||||
.map_err(Error::from)
|
||||
.map_err(as_response_error)
|
||||
},
|
||||
|
|
@ -130,6 +131,20 @@ pub(super) async fn execute(
|
|||
.await
|
||||
}
|
||||
|
||||
/// The answer named as Python's `ModelResponse` names it: by the id the config kept (Gemini's
|
||||
/// `responseId`), else a minted `chatcmpl-` id, with `object: chat.completion`. Named before it
|
||||
/// is cached, so a cache hit replays the id the answer was first sent with.
|
||||
fn named(mut response: ChatCompletionsResponse) -> ChatCompletionsResponse {
|
||||
response
|
||||
.extra
|
||||
.entry("id")
|
||||
.or_insert_with(|| format!("chatcmpl-{}", uuid::Uuid::new_v4()).into());
|
||||
response
|
||||
.extra
|
||||
.insert("object".into(), "chat.completion".into());
|
||||
response
|
||||
}
|
||||
|
||||
/// Re-tag an error raised while normalizing a response the provider already
|
||||
/// returned.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -1461,7 +1461,14 @@ pub fn transform_generate_content_response(
|
|||
}
|
||||
let usage = calculate_usage(&body)?;
|
||||
let [grounding, url_context, safety, citations] = metadata;
|
||||
let extra = Map::from_iter([
|
||||
// Python names the answer by Gemini's `responseId` when there is one; the host mints an id
|
||||
// otherwise.
|
||||
let response_id = body
|
||||
.get("responseId")
|
||||
.and_then(Value::as_str)
|
||||
.filter(|id| !id.is_empty())
|
||||
.map(|id| ("id".to_owned(), Value::from(id)));
|
||||
let extra = Map::from_iter(response_id.into_iter().chain([
|
||||
(
|
||||
"vertex_ai_grounding_metadata".to_owned(),
|
||||
Value::Array(grounding),
|
||||
|
|
@ -1475,7 +1482,7 @@ pub fn transform_generate_content_response(
|
|||
"vertex_ai_citation_metadata".to_owned(),
|
||||
Value::Array(citations),
|
||||
),
|
||||
]);
|
||||
]));
|
||||
Ok(ChatCompletionsResponse {
|
||||
created: unix_now(),
|
||||
model,
|
||||
|
|
@ -2060,6 +2067,18 @@ mod tests {
|
|||
.unwrap()
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::named(json!("resp_1"), Some(json!("resp_1")))]
|
||||
#[case::unnamed(Value::Null, None)]
|
||||
fn the_answer_is_named_by_the_response_id(#[case] id: Value, #[case] expected: Option<Value>) {
|
||||
let answer = response(json!({
|
||||
"responseId": id,
|
||||
"candidates": [{"content": {"role": "model", "parts": [{"text": "Hi"}]}, "finishReason": "STOP"}],
|
||||
"usageMetadata": {"promptTokenCount": 1, "candidatesTokenCount": 1, "totalTokenCount": 2}
|
||||
}));
|
||||
assert_eq!(answer.get("id").cloned(), expected);
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
fn text_reasoning_and_usage_come_back_in_openai_shape() {
|
||||
let answer = response(json!({
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue