From 8143266ead7b71af5384aee57035a1321c427d70 Mon Sep 17 00:00:00 2001 From: calesthio Date: Thu, 13 Aug 2026 09:18:49 -0700 Subject: [PATCH] fix: complete Hunyuan image provider integration --- .env.example | 3 ++ docs/PROVIDERS.md | 18 ++++++++++++ tests/tools/test_hunyuan_image.py | 46 +++++++++++++++++++++++++++++++ tools/graphics/hunyuan_image.py | 5 ++-- tools/graphics/image_selector.py | 11 ++++++++ 5 files changed, 81 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index d60f8987..5bb332ae 100644 --- a/.env.example +++ b/.env.example @@ -44,6 +44,9 @@ DOUBAO_SPEECH_VOICE_TYPE= # Default Doubao speaker/voice type, e.g. zh_female DASHSCOPE_API_KEY= # Qwen image gen (qwen-image-2.0-pro), TTS (qwen3-tts-flash), ASR with word timestamps (qwen3-asr-flash-filetrans) # Get one at https://dashscope.aliyun.com/ +# --- Tencent Hunyuan TokenHub --- +TENCENT_TOKENHUB_API_KEY= # Hunyuan Image 3.0 via Tencent TokenHub + # --- Music --- SUNO_API_KEY= # Suno AI music generation (full songs, instrumentals, any genre) diff --git a/docs/PROVIDERS.md b/docs/PROVIDERS.md index 7ad51bad..091c7402 100644 --- a/docs/PROVIDERS.md +++ b/docs/PROVIDERS.md @@ -188,6 +188,24 @@ The ASR tool (`qwen3-asr-flash-filetrans`) uses an async submit-poll pattern. Au --- +### Tencent Hunyuan Cloud — Image Generation + +> **Chinese-friendly first-party image generation.** `hunyuan_image` accesses +> Hunyuan Image 3.0 through Tencent TokenHub with Bearer-token authentication. +> It supports seeded text-to-image, up to three reference images, custom +> resolutions, prompt rewriting, and watermark controls. + +**Tool unlocked:** `hunyuan_image` + +**Env var:** `TENCENT_TOKENHUB_API_KEY` + +Generate an API key in the Tencent Cloud TokenHub console and add it to +`.env`. The tool reports approximately $0.08 per generated image based on +TokenHub's credit price. It is available through `image_selector`; shared +reference-image inputs are normalized to the provider's `images` array. + +--- + ### fal.ai — Multi-Model Gateway > **Broad single-key coverage.** One API key unlocks image and video providers across multiple models. diff --git a/tests/tools/test_hunyuan_image.py b/tests/tools/test_hunyuan_image.py index efb1f9d5..f4258bb7 100644 --- a/tests/tools/test_hunyuan_image.py +++ b/tests/tools/test_hunyuan_image.py @@ -46,6 +46,18 @@ def test_hunyuan_image_metadata(): assert info["supports"]["reference_image"] is True assert info["supports"]["prompt_rewrite"] is True assert info["supports"]["negative_prompt"] is False + assert "env:TENCENT_TOKENHUB_API_KEY" in info["dependencies"] + assert "visual-style" in info["agent_skills"] + + +def test_idempotency_includes_custom_watermark(): + from tools.graphics.hunyuan_image import HunyuanImage + + tool = HunyuanImage() + base = {"prompt": "x"} + assert tool.idempotency_key(base) != tool.idempotency_key( + {**base, "logo_param": {"logo_url": "https://example.com/logo.png"}} + ) # --------------------------------------------------------------------------- @@ -355,6 +367,40 @@ def test_execute_returns_error_without_api_key(monkeypatch): assert "TENCENT_TOKENHUB_API_KEY" in result.error +def test_image_selector_maps_shared_reference_input(monkeypatch, tmp_path): + from tools.base_tool import ToolResult + from tools.graphics.hunyuan_image import HunyuanImage + from tools.graphics.image_selector import ImageSelector + + monkeypatch.setenv("TENCENT_TOKENHUB_API_KEY", "test-key") + tool = HunyuanImage() + selector = ImageSelector() + monkeypatch.setattr(selector, "_providers", lambda: [tool]) + monkeypatch.setattr( + selector, + "_select_best_tool", + lambda _inputs, _candidates, _context: (tool, None), + ) + observed = {} + + def fake_execute(inputs): + observed.update(inputs) + return ToolResult(success=True, data={}, artifacts=[inputs["output_path"]]) + + monkeypatch.setattr(tool, "execute", fake_execute) + result = selector.execute( + { + "prompt": "adapt this frame", + "preferred_provider": "hunyuan_cloud", + "image_path": str(tmp_path / "reference.png"), + "output_path": str(tmp_path / "out.png"), + } + ) + assert result.success + assert observed["images"] == [str(tmp_path / "reference.png")] + assert result.data["selected_tool"] == "hunyuan_image" + + # --------------------------------------------------------------------------- # Dry run # --------------------------------------------------------------------------- diff --git a/tools/graphics/hunyuan_image.py b/tools/graphics/hunyuan_image.py index 65a52c8c..f017dad2 100644 --- a/tools/graphics/hunyuan_image.py +++ b/tools/graphics/hunyuan_image.py @@ -52,12 +52,12 @@ class HunyuanImage(BaseTool): determinism = Determinism.SEEDED runtime = ToolRuntime.API - dependencies = [] + dependencies = ["env:TENCENT_TOKENHUB_API_KEY"] install_instructions = ( "Set TENCENT_TOKENHUB_API_KEY to your Tencent Cloud TokenHub API key.\n" " Get it at https://console.cloud.tencent.com/tokenhub" ) - agent_skills = [] + agent_skills = ["visual-style"] capabilities = ["generate_image", "text_to_image"] supports = { @@ -196,6 +196,7 @@ class HunyuanImage(BaseTool): "seed", "revise", "logo_add", + "logo_param", ] side_effects = [ "writes image file to output_path", diff --git a/tools/graphics/image_selector.py b/tools/graphics/image_selector.py index b106fbde..5f9a0e43 100644 --- a/tools/graphics/image_selector.py +++ b/tools/graphics/image_selector.py @@ -242,6 +242,17 @@ class ImageSelector(BaseTool): props = tool.input_schema.get("properties", {}) if "query" in props and "query" not in adapted: adapted["query"] = adapted.get("prompt", "") + # Normalize the selector's shared reference-image inputs for + # providers whose native contract accepts an ``images`` array. + if "images" in props and "images" not in adapted: + refs = ( + adapted.get("image_paths") + or adapted.get("image_urls") + or ([adapted["image_path"]] if adapted.get("image_path") else None) + or ([adapted["image_url"]] if adapted.get("image_url") else None) + ) + if refs: + adapted["images"] = refs # Strip selector-only keys that downstream tools don't understand adapted.pop("preferred_provider", None)