agf-wiring-multi-llm-sdk

$npx mdskill add pcliangx/AppGenesisForge/agf-wiring-multi-llm-sdk

Wires up or switches between China-domestic LLM providers using OpenAI-compatible adapter pattern

  • Adds support for DeepSeek, Doubao/Volc Ark, Qwen/DashScope, and MiniMax LLMs.
  • Deploys OpenAI-compatible SDK with custom base URL to integrate with different providers.
  • Chooses the `openai` Python SDK as the default due to fewer dependencies and easier swapping between providers.
  • For bespoke integrations, uses additional SDKs for specific features like image generation or streaming.

SKILL.md

.github/skills/agf-wiring-multi-llm-sdkView on GitHub ↗
---
name: agf-wiring-multi-llm-sdk
description: Use when wiring up or switching between China-domestic LLM providers (DeepSeek, Doubao/Volc Ark, Qwen/DashScope, MiniMax). Provides OpenAI-compatible adapter pattern, env-var contracts, fallback strategy, cost guardrails, and minimum verifications before declaring integration done.
---

# Wiring Multi-LLM SDK (DeepSeek / Doubao / Qwen / MiniMax)

Use this skill when:

- You add a new LLM provider to `backend/app/agents/` or any backend module
- You switch the default provider (e.g. DeepSeek → Doubao for cost reasons)
- You wire fallback / retry logic between providers
- You suspect a provider mismatch is the cause of a bug

## Decision: which SDK style?

All four providers expose **OpenAI-compatible endpoints**. Default to the `openai` Python SDK with a custom `base_url` rather than each vendor's bespoke SDK — fewer dependencies, easier to swap, less drift.

Bespoke SDK exceptions:
- Doubao multimodal (image/video gen): use `volcengine-python-sdk` for Ark image API
- MiniMax video / TTS: use `minimax` official SDK
- Streaming nuance: confirm OAI-compat client handles vendor's stream chunk format

Before wiring any SDK, pull its **current** docs via Context7 (`resolve-library-id` → `query-docs`) — all four vendors iterate fast and training-data memory of their APIs is likely stale. Context7 coverage of domestic SDKs varies; if a library isn't indexed, fall back to WebFetch on official docs.

## Env var contract (locked)

All providers follow the same pattern. **Never hardcode keys.** Each is read from environment at module init; a missing key raises early.

| Provider | Endpoint env | Key env | Default model env |
|---|---|---|---|
| DeepSeek | `DEEPSEEK_BASE_URL` (default `https://api.deepseek.com/v1`) | `DEEPSEEK_API_KEY` | `DEEPSEEK_MODEL` (e.g. `deepseek-chat`) |
| Doubao (Volc Ark) | `ARK_BASE_URL` (default `https://ark.cn-beijing.volces.com/api/v3`) | `ARK_API_KEY` | `ARK_MODEL_ENDPOINT_ID` (vendor-specific endpoint id, NOT model name) |
| Qwen (DashScope) | `DASHSCOPE_BASE_URL` (default `https://dashscope.aliyuncs.com/compatible-mode/v1`) | `DASHSCOPE_API_KEY` | `QWEN_MODEL` (e.g. `qwen-plus`) |
| MiniMax | `MINIMAX_BASE_URL` (default `https://api.minimaxi.com/v1`) | `MINIMAX_API_KEY` | `MINIMAX_MODEL` (e.g. `abab6.5s-chat`) |

> **Doubao gotcha:** the "model name" in OAI-compat call is actually the Ark endpoint id (`ep-2024xxxx`), not a public model id like `doubao-pro-32k`. Get the endpoint id from Volc Ark console.

## Adapter skeleton (Python / FastAPI)

```python
# backend/app/agents/llm_clients.py
import os
from openai import OpenAI

def get_client(provider: str) -> tuple[OpenAI, str]:
    if provider == "deepseek":
        return OpenAI(
            api_key=os.environ["DEEPSEEK_API_KEY"],
            base_url=os.getenv("DEEPSEEK_BASE_URL", "https://api.deepseek.com/v1"),
        ), os.getenv("DEEPSEEK_MODEL", "deepseek-chat")
    if provider == "doubao":
        return OpenAI(
            api_key=os.environ["ARK_API_KEY"],
            base_url=os.getenv("ARK_BASE_URL", "https://ark.cn-beijing.volces.com/api/v3"),
        ), os.environ["ARK_MODEL_ENDPOINT_ID"]
    if provider == "qwen":
        return OpenAI(
            api_key=os.environ["DASHSCOPE_API_KEY"],
            base_url=os.getenv("DASHSCOPE_BASE_URL", "https://dashscope.aliyuncs.com/compatible-mode/v1"),
        ), os.getenv("QWEN_MODEL", "qwen-plus")
    if provider == "minimax":
        return OpenAI(
            api_key=os.environ["MINIMAX_API_KEY"],
            base_url=os.getenv("MINIMAX_BASE_URL", "https://api.minimaxi.com/v1"),
        ), os.getenv("MINIMAX_MODEL", "abab6.5s-chat")
    raise ValueError(f"unknown provider: {provider}")
```

## Fallback strategy

Default order (tunable in CLAUDE.md per project): **DeepSeek → Doubao → Qwen → MiniMax**.

- 5xx / network → next provider
- 4xx (auth / quota) → DO NOT failover; raise (signals config bug)
- Latency P95 breach (> 5s for non-streaming) → tier down silently, log warning

Implement with `tenacity` retry + a thin orchestrator that walks the list. **Never silently swap models** without telemetry — every failover emits a structured log line per `observability.md`.

## Token + cost telemetry

Every call must record the LLM fields mandated by `observability.md:17` plus `provider`. DeepSeek + Doubao support prompt caching — read `cache_hit_ratio` off the response usage object.

## Verifications before "done"

Before claiming the integration works, run this checklist explicitly. **Verify outputs match expectations — do not assume.**

- [ ] Smoke: each enabled provider answers "你好" with non-empty text and >0 token usage
- [ ] Streaming: chunk delivery is real (not buffered all-at-once); first-token latency logged
- [ ] Failover: simulate one provider down (point base_url at `localhost:1`) and confirm auto-fallback + log line
- [ ] Cost: token usage written to log + DB on every call
- [ ] Secrets: no key appears in logs / git diff (run `git diff | grep -iE 'api[_-]?key|secret|token'` before commit)
- [ ] Unit test mocks at the OpenAI SDK boundary (not at HTTP level) — survives base_url changes
- [ ] SDK surface: API calls cross-checked against current docs (Context7 or official site) — not from training memory

## Anti-patterns

- ❌ Hardcoding `base_url` strings — always read from env
- ❌ Passing public model id to Doubao — use endpoint id
- ❌ Catching all exceptions and returning empty string — masks auth failures and quota issues
- ❌ Assuming all providers return the same `usage` shape — validate and normalize
- ❌ Storing keys in CLAUDE.md / settings.json — only `.env` (gitignored) or secret manager

## References

- DeepSeek: https://platform.deepseek.com/api-docs
- Doubao / Volc Ark: https://www.volcengine.com/docs/82379
- Qwen / DashScope: https://help.aliyun.com/zh/dashscope/developer-reference/compatibility-of-openai-with-dashscope
- MiniMax: https://platform.minimaxi.com/document/ChatCompletion

More from pcliangx/AppGenesisForge

SkillDescription
agf-code-mapDeeply Understand (codemap) — 持久化代码图谱 + 变更影响分析 + 理解地图。接手遗留项目 Day-1 / PR 影响分析 / 解释陌生代码 / PRD·ADR 前现状理解时用。编排 tools/codemap/ 的 codemap CLI
agf-deploying-uatUse when deploy-engineer is about to deploy the merged-to-main code to the isolated local UAT stack (after code review + SIT Audit pass and merge, before qa-engineer runs E2E/UAT). Provides the applicability gate, pre-flight checks, isolated compose bring-up (independent project name + port offset +900), in-container migration, real-output smoke test, hand-off, and the deploy-report skeleton. Pairs with deployment.md "UAT 环境部署" contract and slash /agf-deploy-uat.
agf-design-disciplineUse when uiux-designer is about to produce a design spec (spec.md) or static HTML prototype, or frontend-dev is about to build UI from a design. Provides the anti-AI-slop design discipline layer — Brief Inference (Design Read), three aesthetic dials tuned for product UI, AI Tells blacklist with overrides, mechanically-checkable Pre-Flight. Sits above the token layer (DESIGN.md) and mechanical review (code-reviewer); does not redeclare tokens or guide non-shadcn design systems. Inspired by taste-skill, cropped for AGF product UI per ADR-013.
agf-releasing-appleUse when apple-release-engineer is about to build the signed distributable (TestFlight build / notarized DMG / internal package) from merged-to-main code (after apple code review + SIT Audit pass and merge, before apple-qa-engineer runs E2E/UAT). Provides the applicability gate, pre-flight checks, lane execution per channel, notarization, real-output smoke test, hand-off, and the release-report skeleton. Pairs with deployment.md §7 "Apple 发布" contract and slash /agf-apple-release.
agf-running-apple-sitUse when apple-dev has finished feature code + Unit tests (Swift Testing) and is about to enter code-review. Provides the Apple SIT scope (xcodebuild test + simulator per declared target), the AC-driven integration walk, APIProtocol-mock discipline, and evidence sink (progress/apple-dev.md). SIT is dev-owned; apple-code-reviewer audits the evidence.
agf-running-release-retroUse when product-lead is about to run a release retrospective after a successful MAJOR or MINOR release push (PATCH skipped). Provides applicability gate, pre-conditions, 7-step execution sequence, anti-patterns, and the verification gate before commit. Pairs with template docs/reviews/retro-_TEMPLATE.md and slash /agf-release-retro.
agf-running-sit-testsUse when an execution-layer dev (frontend-dev / backend-dev / ai-agent-dev / ml-engineer / miniapp-dev) has finished feature code + Unit tests and is about to enter code-review. Provides the SIT scope, environment, AC-driven integration walk, and evidence sink (progress/<role>.md). SIT is now a dev-owned step, not a separate QA stage.
agf-skill-stocktakeUse when PL / tech-lead audits the content quality of AGF's own skills (`.claude/skills/agf-*/SKILL.md`). Provides the 5-dimension checklist (Actionability / Scope fit / Uniqueness vs CLAUDE.md+standards / Currency of pointers / Gate-binding validity), 5-verdict rubric (Keep/Improve/Update/Retire/Merge), reason-quality bar (反敷衍), and output skeleton. Triggered each MINOR release retro or manually.
agf-wiring-apple-llmUse when wiring LLM features into the Apple client (macOS / iOS) — streaming chat via the FastAPI multi-LLM gateway, or on-device inference (Apple Foundation Models / Core ML). Provides the route decision (gateway vs on-device), streaming transport pattern, env/config contract, offline & cost guardrails, and minimum verifications before declaring the integration done.
agf-writing-adrUse when tech-lead is about to record an architecture decision (new tech stack member, deviation from baseline, deployment / observability / auth scheme choice). Provides ADR structure, version-audit appendix format, and "what NOT to ADR" guidance. Replaces ad-hoc copy from ADR-000.