diff --git a/docs/SKILL_PROXY_MIGRATION_GUIDE.md b/docs/SKILL_PROXY_MIGRATION_GUIDE.md new file mode 100644 index 00000000..7ba3540a --- /dev/null +++ b/docs/SKILL_PROXY_MIGRATION_GUIDE.md @@ -0,0 +1,203 @@ +# 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. The skill no longer manages third-party API keys itself. +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. 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` + +### 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: Remove third-party API key logic from the skill + +Remove: +- Loading `RUNNINGHUB_API_KEY` in the script +- Building `Authorization: Bearer ...` in the script + +Reason: third-party credentials are injected by gateway. + +### Step 6: Keep essential error handling + +Recommended checks: +- `response.raise_for_status()` +- submit fallback when `taskId` is missing +- query loop timeout/failure handling + +## 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 + api_key_header: Authorization + api_key_prefix: "Bearer " + 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 + + +def load_skill_env() -> dict[str, str]: + """Load skill-local .env values.""" + env_path = Path(__file__).parent.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 +``` + +## 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 reads third-party API keys. +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. 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`