From 6b9ba782d8737a02bb92d38ce39059df67710706 Mon Sep 17 00:00:00 2001 From: xucailiang <74602715+xucailiang@users.noreply.github.com> Date: Fri, 10 Jul 2026 21:24:11 +0800 Subject: [PATCH] fix: complete Kling idempotency inputs Include every request field that can alter Kling video, image, avatar, or lip-sync media in the public idempotency contract. Add a shared regression matrix that detects future cache-key collisions while excluding transport-only controls. --- tests/contracts/test_kling_official_core.py | 110 ++++++++++++++++++++ tools/avatar/kling_avatar.py | 12 ++- tools/avatar/kling_lip_sync.py | 8 ++ tools/graphics/kling_official_image.py | 12 +++ tools/video/kling_official_video.py | 24 ++++- 5 files changed, 163 insertions(+), 3 deletions(-) diff --git a/tests/contracts/test_kling_official_core.py b/tests/contracts/test_kling_official_core.py index 2b3250ea..38376b25 100644 --- a/tests/contracts/test_kling_official_core.py +++ b/tests/contracts/test_kling_official_core.py @@ -401,6 +401,116 @@ def test_provider_agent_skills_reference_kling_official(): assert "kling-official" in KlingLipSync().agent_skills +@pytest.mark.parametrize( + ("tool", "output_variants"), + [ + ( + KlingOfficialVideo(), + { + "prompt": "changed prompt", + "operation": "image_to_video", + "api_family": "omni", + "model_name": "kling-v2-6", + "model_variant": "kling-v2-5-turbo", + "duration": "10", + "aspect_ratio": "9:16", + "resolution": "1080p", + "mode": "pro", + "sound": "on", + "negative_prompt": "blur", + "cfg_scale": 0.7, + "reference_image_url": "https://example.com/first.png", + "reference_image_path": "/tmp/first.png", + "reference_tail_image_url": "https://example.com/tail.png", + "reference_tail_image_path": "/tmp/tail.png", + "reference_image_urls": ["https://example.com/ref.png"], + "reference_image_paths": ["/tmp/ref.png"], + "reference_video_url": "https://example.com/ref.mp4", + "video_urls": ["https://example.com/ref-2.mp4"], + "image_list": [{"image_url": "https://example.com/list.png"}], + "video_list": [{"video_url": "https://example.com/list.mp4"}], + "element_list": [{"element_id": 123}], + "multi_shot": True, + "shot_type": "intelligence", + "multi_prompt": [{"prompt": "second shot", "duration": "3"}], + "camera_control": {"type": "simple", "config": {"horizontal": 1}}, + "watermark": True, + }, + ), + ( + KlingOfficialImage(), + { + "prompt": "changed prompt", + "negative_prompt": "blur", + "operation": "omni", + "api_family": "omni", + "model_name": "kling-image-o1", + "image_url": "https://example.com/source.png", + "image_path": "/tmp/source.png", + "image_urls": ["https://example.com/ref.png"], + "image_paths": ["/tmp/ref.png"], + "image_list": [{"image_url": "https://example.com/list.png"}], + "image_reference": "subject", + "image_fidelity": 0.7, + "human_fidelity": 0.8, + "resolution": "2k", + "aspect_ratio": "1:1", + "n": 2, + "result_type": "series", + "series_amount": "3", + "element_list": [{"element_id": 123}], + "watermark": True, + }, + ), + ( + KlingAvatar(), + { + "image_url": "https://example.com/avatar.png", + "image_path": "/tmp/avatar.png", + "audio_id": "audio-a", + "sound_file": "inline-audio", + "sound_file_url": "https://example.com/audio.mp3", + "sound_file_path": "/tmp/audio.mp3", + "audio_path": "/tmp/audio-alias.mp3", + "prompt": "natural presenter motion", + "mode": "pro", + }, + ), + ( + KlingLipSync(), + { + "operation": "full_lip_sync", + "video_id": "video-a", + "video_url": "https://example.com/video.mp4", + "session_id": "session-a", + "face_id": "face-a", + "face_choose": [{"face_id": "face-a", "audio_id": "audio-a"}], + "auto_select_face": True, + "audio_id": "audio-a", + "sound_file": "inline-audio", + "sound_file_url": "https://example.com/audio.mp3", + "sound_file_path": "/tmp/audio.mp3", + "audio_path": "/tmp/audio-alias.mp3", + "sound_start_time": 100, + "sound_end_time": 4100, + "sound_insert_time": 500, + "sound_volume": 1.2, + "original_audio_volume": 0.4, + }, + ), + ], +) +def test_kling_output_inputs_change_idempotency_keys(tool, output_variants): + baseline = tool.idempotency_key({}) + collisions = [ + field + for field, value in output_variants.items() + if tool.idempotency_key({field: value}) == baseline + ] + + assert collisions == [], f"{tool.name} idempotency key ignores: {collisions}" + + def test_phase3_does_not_register_audio_or_video_effect_tools(isolated_tool_registry): isolated_tool_registry.discover("tools") assert isolated_tool_registry.get("kling_audio") is None diff --git a/tools/avatar/kling_avatar.py b/tools/avatar/kling_avatar.py index 18804200..4b7661bd 100644 --- a/tools/avatar/kling_avatar.py +++ b/tools/avatar/kling_avatar.py @@ -124,7 +124,17 @@ class KlingAvatar(BaseTool): backoff_seconds=2.0, retryable_errors=["1302", "1303", "5000", "5001", "5002"], ) - idempotency_key_fields = ["image_url", "image_path", "audio_id", "sound_file", "sound_file_path", "mode"] + idempotency_key_fields = [ + "image_url", + "image_path", + "audio_id", + "sound_file", + "sound_file_url", + "sound_file_path", + "audio_path", + "prompt", + "mode", + ] side_effects = ["paid remote generation via official Kling API", "writes avatar video to output_path"] user_visible_verification = ["Watch generated avatar video for identity preservation and mouth motion"] quality_score = 0.82 diff --git a/tools/avatar/kling_lip_sync.py b/tools/avatar/kling_lip_sync.py index b27bab55..39525189 100644 --- a/tools/avatar/kling_lip_sync.py +++ b/tools/avatar/kling_lip_sync.py @@ -139,15 +139,23 @@ class KlingLipSync(BaseTool): retryable_errors=["1302", "1303", "5000", "5001", "5002"], ) idempotency_key_fields = [ + "operation", "video_id", "video_url", "session_id", "face_id", + "face_choose", + "auto_select_face", "audio_id", + "sound_file", + "sound_file_url", "sound_file_path", + "audio_path", "sound_start_time", "sound_end_time", "sound_insert_time", + "sound_volume", + "original_audio_volume", ] side_effects = [ "paid remote generation via official Kling API", diff --git a/tools/graphics/kling_official_image.py b/tools/graphics/kling_official_image.py index eeac58a1..cfefc2ab 100644 --- a/tools/graphics/kling_official_image.py +++ b/tools/graphics/kling_official_image.py @@ -122,13 +122,25 @@ class KlingOfficialImage(BaseTool): ) idempotency_key_fields = [ "prompt", + "negative_prompt", + "operation", "api_family", "model_name", "image_url", "image_path", + "image_urls", + "image_paths", + "image_list", + "image_reference", + "image_fidelity", + "human_fidelity", "aspect_ratio", "resolution", "n", + "result_type", + "series_amount", + "element_list", + "watermark", ] side_effects = [ "paid remote generation via official Kling API", diff --git a/tools/video/kling_official_video.py b/tools/video/kling_official_video.py index 1db87104..c010cfb1 100644 --- a/tools/video/kling_official_video.py +++ b/tools/video/kling_official_video.py @@ -142,13 +142,33 @@ class KlingOfficialVideo(BaseTool): ) idempotency_key_fields = [ "prompt", + "negative_prompt", "operation", "api_family", "model_name", - "reference_image_url", - "reference_image_path", + "model_variant", "duration", "aspect_ratio", + "resolution", + "mode", + "sound", + "cfg_scale", + "camera_control", + "reference_image_url", + "reference_image_path", + "reference_tail_image_url", + "reference_tail_image_path", + "reference_image_urls", + "reference_image_paths", + "reference_video_url", + "video_urls", + "image_list", + "video_list", + "element_list", + "multi_shot", + "shot_type", + "multi_prompt", + "watermark", ] side_effects = [ "paid remote generation via official Kling API",