# Skill Proxy Migration Guide (via Gateway) This document explains how to migrate a skill script from directly calling a third-party API to using DeerFlow Gateway's transparent proxy, with unified billing orchestration (reserve/finalize). Applicable scenarios: - Async third-party task skills (image/video/audio generation, etc.) - Existing scripts that directly call providers (for example, RunningHub) ## 1. Migration Goals 1. The skill no longer calls third-party domains directly. 2. Real API keys are replaced by an API key marker in scripts being migrated. 3. All requests go through `/api/proxy/{provider}/...`. 4. Gateway handles: - API key injection - Idempotent submit deduplication - Billing reserve/finalize orchestration - Query terminal-state detection and settlement ## 2. Core Principles 1. Keep provider names stable (for example, `runninghub`); do not encode model paths in provider names. 2. Only submit requests should carry `X-Idempotency-Key`; query requests should not. 3. Use `X-Thread-Id` as a common context header whenever available. 4. Replace API key usage in scripts with marker `API_KEY_MARKER = "__API_KEY_MARKER__"` at the actual usage location: - If the script uses header auth, replace with `Authorization: Bearer __API_KEY_MARKER__`. - If the script uses body auth, replace the body `apiKey` field value with `__API_KEY_MARKER__`. - Choose header/body based on the script's real provider contract; do not force a single style. 5. Use shorthand dot-paths in config extraction fields: - Correct: `taskId`, `status`, `usage.thirdPartyConsumeMoney` - Incorrect: `$.taskId`, `'$'.taskId` ## 3. Skill Script Migration Steps The examples below assume Python + requests. ### Step 1: Add gateway config loaders Add: - `load_skill_env()`: loads skill-local `.env` - `get_gateway_config()`: reads - `DEER_FLOW_GATEWAY_URL` (default `http://host.docker.internal:8001`) - `RUNNINGHUB_PROXY_PROVIDER` (default `runninghub`) ### Step 2: Centralize proxy headers Implement: - `build_proxy_headers(include_idempotency: bool = False)` - always sets `Content-Type: application/json` - optionally sets `X-Thread-Id` - sets `X-Idempotency-Key` only when `include_idempotency=True` - if this script authenticates via header, set `Authorization: Bearer __API_KEY_MARKER__` ### Step 3: Route submit calls through gateway Replace: - `https://www.runninghub.cn/openapi/v2/` With: - `{gateway}/api/proxy/{provider}/openapi/v2/` And use: - `headers=build_proxy_headers(include_idempotency=True)` ### Step 4: Route query calls through gateway Replace: - `https://www.runninghub.cn/openapi/v2/query` With: - `{gateway}/api/proxy/{provider}/openapi/v2/query` And use: - `headers=build_proxy_headers()` ### Step 5: Replace third-party API key values with marker (do not keep real keys in scripts) Replace in the script being migrated: - Any real `Authorization: Bearer ` value -> `Authorization: Bearer __API_KEY_MARKER__` - Any real body `apiKey` value -> `"apiKey": "__API_KEY_MARKER__"` Keep the original auth location (header vs body) unchanged unless provider API requirements changed. Reason: gateway injects real credentials by replacing marker values at proxy forwarding time. ### Step 6: Keep essential error handling Recommended checks: - `response.raise_for_status()` - submit fallback when `taskId` is missing - query loop timeout/failure handling ### Step 7: Clean API key instructions from skill.md After migrating a skill to gateway proxy, remove any user-facing instructions in `skill.md` that ask users to configure third-party provider keys (for example, `RUNNINGHUB_API_KEY`). What to remove from `skill.md`: - "Set `RUNNINGHUB_API_KEY` in .env" - "Create an API key on provider platform" - Any step that tells users to pass `Authorization: Bearer ...` What to keep/add in `skill.md`: - Mention that third-party credentials are handled by gateway config - Keep only skill runtime inputs (prompt, output path, optional style/duration) - Optionally mention gateway-related vars if needed by local debugging: - `DEER_FLOW_GATEWAY_URL` - `RUNNINGHUB_PROXY_PROVIDER` Suggested replacement sentence: - "This skill uses DeerFlow Gateway third-party proxy. Provider credentials are configured centrally in gateway and are not required in this skill's local `.env`." ## 4. Proxy Config Migration (config.yaml) Configure submit/query routes under `third_party_proxy.providers.`. Example (RunningHub): ```yaml third_party_proxy: enabled: true providers: runninghub: base_url: https://www.runninghub.cn api_key_env: RUNNINGHUB_API_KEY timeout_seconds: 30.0 frozen_amount: 10.0 frozen_type: 2 submit_routes: - path_pattern: "/openapi/v2/rhart-image/z-image/turbo-lora" task_id_jsonpath: "taskId" frozen_amount: 0.03 frozen_type: 2 - path_pattern: "/openapi/v2/vidu/text-to-video-q3-turbo" task_id_jsonpath: "taskId" frozen_amount: 11.2 frozen_type: 2 query_routes: - path_pattern: "/openapi/v2/query" request_task_id_jsonpath: "taskId" status_jsonpath: "status" success_values: ["SUCCESS"] failure_values: ["FAILED", "CANCELLED"] usage_jsonpath: "usage.thirdPartyConsumeMoney" ``` Notes: - Provider-level `frozen_amount`/`frozen_type` are defaults. - Submit-route values can override defaults per model endpoint. ## 5. Reusable Function Template ```python import os from pathlib import Path from dotenv import dotenv_values API_KEY_MARKER = "__API_KEY_MARKER__" def load_skill_env() -> dict[str, str]: """Load skill-local .env values.""" env_path = Path(__file__).parent / ".env" return { key: value for key, value in dotenv_values(env_path).items() if isinstance(key, str) and isinstance(value, str) } def get_gateway_config() -> tuple[str, str]: """Get DeerFlow gateway base URL and proxy provider name.""" env_vars = load_skill_env() gateway_url = os.getenv("DEER_FLOW_GATEWAY_URL") or env_vars.get( "DEER_FLOW_GATEWAY_URL", "http://host.docker.internal:8001", ) provider = os.getenv("RUNNINGHUB_PROXY_PROVIDER") or env_vars.get( "RUNNINGHUB_PROXY_PROVIDER", "runninghub", ) return gateway_url.rstrip("/"), provider def build_proxy_headers(*, include_idempotency: bool = False) -> dict[str, str]: headers = {"Content-Type": "application/json"} thread_id = os.getenv("THREAD_ID") if thread_id: headers["X-Thread-Id"] = thread_id if include_idempotency: from uuid import uuid4 headers["X-Idempotency-Key"] = str(uuid4()) return headers # Optional helper when provider requires header auth in script contract: def build_proxy_headers_with_auth(*, include_idempotency: bool = False) -> dict[str, str]: headers = build_proxy_headers(include_idempotency=include_idempotency) headers["Authorization"] = f"Bearer {API_KEY_MARKER}" return headers ``` ## 6. Common Pitfalls ### 6.1 Response contains taskId but extraction fails Usually caused by wrong config path syntax: - Wrong: `$.taskId` or `'$'.taskId` - Right: `taskId` ### 6.2 Why query should not include X-Idempotency-Key Idempotency keys are for submit deduplication (to avoid duplicate task creation). Query requests are polling and should not generate new idempotency keys. ### 6.3 Sandbox cannot reach gateway For Docker-based sandbox execution, use: - `DEER_FLOW_GATEWAY_URL=http://host.docker.internal:8001` ## 7. Validation Checklist 1. No direct third-party domain calls remain in the skill script. 2. The skill script no longer contains real third-party API key values. 3. Submit uses proxy URL + `include_idempotency=True`. 4. Query uses proxy URL + `include_idempotency=False`. 5. Config extraction fields use shorthand dot-paths only. 6. Submit returns `taskId`, then query reaches `RUNNING/SUCCESS`. 7. Gateway logs show submit/query route hits and finalize flow. 8. `skill.md` no longer contains instructions to configure third-party API keys. ## 8. Reference Implementations - `skills/public/image-generation/scripts/generate.py` - `skills/public/video-generation/scripts/generate.py` - `backend/app/gateway/routers/third_party.py` - `backend/app/gateway/third_party_proxy/proxy.py` - `third_party_proxy` section in `config.yaml`