diff --git a/litellm-rust/crates/cache/src/semantic.rs b/litellm-rust/crates/cache/src/semantic.rs index 5b619151285..b27ab269edd 100644 --- a/litellm-rust/crates/cache/src/semantic.rs +++ b/litellm-rust/crates/cache/src/semantic.rs @@ -199,10 +199,13 @@ fn collect_input_text(value: &Value, parts: &mut Vec) { return; } for key in ["text", "output", "input_text", "output_text"] { - if let Some(Value::String(text)) = map.get(key) - && push_trimmed(text, parts) - { - return; + match map.get(key) { + Some(nested @ Value::Array(_)) => { + collect_input_text(nested, parts); + return; + } + Some(Value::String(text)) if push_trimmed(text, parts) => return, + _ => {} } } } diff --git a/litellm-rust/crates/cache/tests/semantic.rs b/litellm-rust/crates/cache/tests/semantic.rs index ab6b904af1a..a29844bc241 100644 --- a/litellm-rust/crates/cache/tests/semantic.rs +++ b/litellm-rust/crates/cache/tests/semantic.rs @@ -240,6 +240,14 @@ fn prompt_from_messages_reads_messages_only( ])), Some("update the config\n{\"name\":\"write_file\",\"arguments\":\"{\\\"path\\\":\\\"a.yaml\\\"}\"}\nok"), )] +#[case::responses_structured_function_call_output( + None, + Some(json!([ + {"type": "function_call", "call_id": "c1", "name": "write_file", "arguments": "{}"}, + {"type": "function_call_output", "call_id": "c1", "output": [{"type": "input_text", "text": "denied"}]}, + ])), + Some("{\"name\":\"write_file\",\"arguments\":\"{}\"}\ndenied"), +)] fn prompt_from_context_matches_python( #[case] messages: Option, #[case] input: Option, diff --git a/litellm/caching/redis_semantic_cache.py b/litellm/caching/redis_semantic_cache.py index bac58026f18..cd0f0770eec 100644 --- a/litellm/caching/redis_semantic_cache.py +++ b/litellm/caching/redis_semantic_cache.py @@ -299,6 +299,9 @@ class RedisSemanticCache(BaseCache): for text_key in ("text", "output", "input_text", "output_text"): text_value = value.get(text_key) + if isinstance(text_value, (list, tuple)): + cls._collect_responses_input_text(text_value, prompt_parts) + return if isinstance(text_value, str): stripped_text = text_value.strip() if stripped_text: diff --git a/tests/unit/caching/test_redis_semantic_cache.py b/tests/unit/caching/test_redis_semantic_cache.py index 1e4a6c2bdc6..38700dce62c 100644 --- a/tests/unit/caching/test_redis_semantic_cache.py +++ b/tests/unit/caching/test_redis_semantic_cache.py @@ -614,6 +614,27 @@ def test_redis_semantic_cache_prompt_extraction_keeps_responses_function_calls() assert prompt == 'update the config\n{"name":"write_file","arguments":"{\\"path\\":\\"a.yaml\\"}"}\nok' +def test_redis_semantic_cache_prompt_extraction_keeps_structured_function_call_outputs(): + from litellm.caching.redis_semantic_cache import RedisSemanticCache + + def prompt_for(output_text: str) -> str | None: + return RedisSemanticCache._get_prompt_from_kwargs( + input=[ + {"type": "message", "role": "user", "content": [{"type": "input_text", "text": "write hello"}]}, + {"type": "function_call", "call_id": "c1", "name": "write_file", "arguments": '{"path":"a.txt"}'}, + { + "type": "function_call_output", + "call_id": "c1", + "output": [{"type": "input_text", "text": output_text}], + }, + ] + ) + + expected_call = '{"name":"write_file","arguments":"{\\"path\\":\\"a.txt\\"}"}' + assert prompt_for("wrote 5 bytes") == f"write hello\n{expected_call}\nwrote 5 bytes" + assert prompt_for("wrote 5 bytes") != prompt_for("PermissionError") + + def test_redis_semantic_cache_prompt_extraction_handles_model_objects(): from litellm.caching.redis_semantic_cache import RedisSemanticCache