From d8b9be468a56e28158ed5a858640928c6d2ab994 Mon Sep 17 00:00:00 2001 From: JuniperSling <80268751+JuniperSling@users.noreply.github.com> Date: Wed, 22 Apr 2026 12:59:22 +0800 Subject: [PATCH] feat(tencent): add bundled Tencent Cloud provider plugin (Tokenhub + Token Plan) (#68460) * feat(tencent): add bundled Tencent Cloud provider plugin (Tokenhub + Token Plan) * fix(tencent): use provider-specific default model aliases Both Tencent providers previously defaulted to the same alias "HY3 Preview", which collides in buildModelAliasIndex (single alias map, keyed by normalized alias). When both providers are onboarded, alias-based selection routed to whichever provider was configured last. Disambiguate the fallback aliases so resolution is deterministic regardless of onboarding order: - tencent-tokenhub -> "HY3 Preview (TokenHub)" - tencent-token-plan -> "HY3 Preview (Token Plan)" * docs(tencent): rename model to "Hy3 preview" and drop "HY3" family name Align with the external-facing product name: - model display name: "HY3 Preview" -> "Hy3 preview" - family/umbrella references in docs and auth hints: "HY3" -> "Hy3 preview" - internal cost constant: HY3_COST -> HY_COST Model call id (hy3-preview) is unchanged. * docs(tencent): use "Hy" as the family name in generic references Keep specific model references as "Hy3 preview" (model catalog names, onboarding aliases, Available-models docs entries), but switch family/umbrella references to the plain "Hy" family name so future Hy versions fit without doc churn: - auth hints: "Hy via Tencent TokenHub Gateway" / "Hy via Token Plan" - docs intro + Use-case table: "Tencent Hy models" / "call Hy via ..." - models.ts pricing comment: "Hy pricing" * feat(tencent): add tiered pricing for Hy3 preview model --------- Co-authored-by: albertxyu --- .env.example | 2 + .github/labeler.yml | 4 + docs/docs.json | 1 + docs/providers/tencent.md | 90 ++++++++++++++++++ extensions/tencent/api.ts | 11 +++ extensions/tencent/index.ts | 116 ++++++++++++++++++++++++ extensions/tencent/models.ts | 93 +++++++++++++++++++ extensions/tencent/onboard.ts | 73 +++++++++++++++ extensions/tencent/openclaw.plugin.json | 42 +++++++++ extensions/tencent/package.json | 15 +++ extensions/tencent/provider-catalog.ts | 25 +++++ extensions/tencent/tsconfig.json | 16 ++++ pnpm-lock.yaml | 6 ++ 13 files changed, 494 insertions(+) create mode 100644 docs/providers/tencent.md create mode 100644 extensions/tencent/api.ts create mode 100644 extensions/tencent/index.ts create mode 100644 extensions/tencent/models.ts create mode 100644 extensions/tencent/onboard.ts create mode 100644 extensions/tencent/openclaw.plugin.json create mode 100644 extensions/tencent/package.json create mode 100644 extensions/tencent/provider-catalog.ts create mode 100644 extensions/tencent/tsconfig.json diff --git a/.env.example b/.env.example index 44c30da44cd..f7ed308c713 100644 --- a/.env.example +++ b/.env.example @@ -54,6 +54,8 @@ OPENCLAW_GATEWAY_TOKEN= # Optional additional providers # ZAI_API_KEY=... # AI_GATEWAY_API_KEY=... +# TOKENHUB_API_KEY=... +# LKEAP_API_KEY=... # MINIMAX_API_KEY=... # SYNTHETIC_API_KEY=... diff --git a/.github/labeler.yml b/.github/labeler.yml index d697bcb96f1..a833b4b4d3e 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -269,6 +269,10 @@ - changed-files: - any-glob-to-any-file: - "extensions/deepseek/**" +"extensions: tencent": + - changed-files: + - any-glob-to-any-file: + - "extensions/tencent/**" "extensions: stepfun": - changed-files: - any-glob-to-any-file: diff --git a/docs/docs.json b/docs/docs.json index f19cc718e64..6fdc0576762 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -1292,6 +1292,7 @@ "providers/sglang", "providers/stepfun", "providers/synthetic", + "providers/tencent", "providers/together", "providers/venice", "providers/vercel-ai-gateway", diff --git a/docs/providers/tencent.md b/docs/providers/tencent.md new file mode 100644 index 00000000000..8c3ea17d38c --- /dev/null +++ b/docs/providers/tencent.md @@ -0,0 +1,90 @@ +--- +title: "Tencent Cloud (TokenHub + Token Plan)" +summary: "Tencent Cloud TokenHub and Token Plan setup (separate keys)" +read_when: + - You want to use Tencent Hy models with OpenClaw + - You need the TokenHub API key or Token Plan (LKEAP) setup +--- + +# Tencent Cloud (TokenHub + Token Plan) + +The Tencent Cloud provider gives access to Tencent Hy models via two endpoints +with separate API keys: + +- **TokenHub** (`tencent-tokenhub`) — call Hy via Tencent TokenHub Gateway +- **Token Plan** (`tencent-token-plan`) — call Hy via the LKEAP + Token Plan endpoint + +Both providers use OpenAI-compatible APIs. + +## Quick start + +TokenHub: + +```bash +openclaw onboard --auth-choice tokenhub-api-key +``` + +Token Plan: + +```bash +openclaw onboard --auth-choice tencent-token-plan-api-key +``` + +## Non-interactive example + +```bash +# TokenHub +openclaw onboard --non-interactive \ + --mode local \ + --auth-choice tokenhub-api-key \ + --tokenhub-api-key "$TOKENHUB_API_KEY" \ + --skip-health \ + --accept-risk + +# Token Plan +openclaw onboard --non-interactive \ + --mode local \ + --auth-choice tencent-token-plan-api-key \ + --tencent-token-plan-api-key "$LKEAP_API_KEY" \ + --skip-health \ + --accept-risk +``` + +## Providers and endpoints + +| Provider | Endpoint | Use case | +| -------------------- | ------------------------------------- | ----------------------- | +| `tencent-tokenhub` | `tokenhub.tencentmaas.com/v1` | Hy via Tencent TokenHub | +| `tencent-token-plan` | `api.lkeap.cloud.tencent.com/plan/v3` | Hy via LKEAP Token Plan | + +Each provider uses its own API key. Setup registers only the selected provider. + +## Available models + +### tencent-tokenhub + +- **hy3-preview** — Hy3 preview (256K context, reasoning, default) + +### tencent-token-plan + +- **hy3-preview** — Hy3 preview (256K context, reasoning, default) + +## Notes + +- TokenHub model refs use `tencent-tokenhub/`. Token Plan model refs + use `tencent-token-plan/`. +- Override pricing and context metadata in `models.providers` if needed. + +## Environment note + +If the Gateway runs as a daemon (launchd/systemd), make sure `TOKENHUB_API_KEY` +or `LKEAP_API_KEY` is available to that process (for example, in +`~/.openclaw/.env` or via `env.shellEnv`). + +## Related documentation + +- [OpenClaw Configuration](/configuration) +- [Model Providers](/concepts/model-providers) +- [Tencent TokenHub](https://cloud.tencent.com/document/product/1823/130050) +- [Tencent Token Plan API](https://cloud.tencent.com/document/product/1823/130060) diff --git a/extensions/tencent/api.ts b/extensions/tencent/api.ts new file mode 100644 index 00000000000..62bafadda2b --- /dev/null +++ b/extensions/tencent/api.ts @@ -0,0 +1,11 @@ +export { + buildTokenHubModelDefinition, + buildTokenPlanModelDefinition, + TOKENHUB_BASE_URL, + TOKENHUB_MODEL_CATALOG, + TOKENHUB_PROVIDER_ID, + TOKEN_PLAN_BASE_URL, + TOKEN_PLAN_MODEL_CATALOG, + TOKEN_PLAN_PROVIDER_ID, +} from "./models.js"; +export { buildTokenHubProvider, buildTokenPlanProvider } from "./provider-catalog.js"; diff --git a/extensions/tencent/index.ts b/extensions/tencent/index.ts new file mode 100644 index 00000000000..2aef592d2ee --- /dev/null +++ b/extensions/tencent/index.ts @@ -0,0 +1,116 @@ +import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry"; +import { createProviderApiKeyAuthMethod } from "openclaw/plugin-sdk/provider-auth-api-key"; +import { buildSingleProviderApiKeyCatalog } from "openclaw/plugin-sdk/provider-catalog-shared"; +import { + TOKENHUB_MODEL_CATALOG, + TOKENHUB_PROVIDER_ID, + TOKEN_PLAN_MODEL_CATALOG, + TOKEN_PLAN_PROVIDER_ID, +} from "./models.js"; +import { + applyTokenHubConfig, + TOKENHUB_DEFAULT_MODEL_REF, + applyTokenPlanConfig, + TOKEN_PLAN_DEFAULT_MODEL_REF, +} from "./onboard.js"; +import { buildTokenHubProvider, buildTokenPlanProvider } from "./provider-catalog.js"; + +function buildStaticCatalogEntries(providerId: string, catalog: typeof TOKENHUB_MODEL_CATALOG) { + return catalog.map((entry) => ({ + provider: providerId, + id: entry.id, + name: entry.name, + reasoning: entry.reasoning, + input: [...entry.input], + contextWindow: entry.contextWindow, + })); +} + +export default definePluginEntry({ + id: "tencent", + name: "Tencent Cloud Provider", + description: "Bundled Tencent Cloud provider plugins (TokenHub + Token Plan)", + register(api) { + // ---------- TokenHub provider ---------- + api.registerProvider({ + id: TOKENHUB_PROVIDER_ID, + label: "Tencent TokenHub", + docsPath: "/providers/tencent", + envVars: ["TOKENHUB_API_KEY"], + auth: [ + createProviderApiKeyAuthMethod({ + providerId: TOKENHUB_PROVIDER_ID, + methodId: "api-key", + label: "Tencent TokenHub", + hint: "Hy via Tencent TokenHub Gateway", + optionKey: "tokenhubApiKey", + flagName: "--tokenhub-api-key", + envVar: "TOKENHUB_API_KEY", + promptMessage: "Enter Tencent TokenHub API key", + defaultModel: TOKENHUB_DEFAULT_MODEL_REF, + expectedProviders: [TOKENHUB_PROVIDER_ID], + applyConfig: (cfg) => applyTokenHubConfig(cfg), + wizard: { + choiceId: "tokenhub-api-key", + choiceLabel: "Tencent TokenHub", + groupId: "tencent", + groupLabel: "Tencent Cloud", + groupHint: "TokenHub + Token Plan", + }, + }), + ], + catalog: { + order: "simple", + run: (ctx) => + buildSingleProviderApiKeyCatalog({ + ctx, + providerId: TOKENHUB_PROVIDER_ID, + buildProvider: buildTokenHubProvider, + }), + }, + augmentModelCatalog: () => + buildStaticCatalogEntries(TOKENHUB_PROVIDER_ID, TOKENHUB_MODEL_CATALOG), + }); + + // ---------- Token Plan provider ---------- + api.registerProvider({ + id: TOKEN_PLAN_PROVIDER_ID, + label: "Tencent Token Plan", + docsPath: "/providers/tencent", + envVars: ["LKEAP_API_KEY"], + auth: [ + createProviderApiKeyAuthMethod({ + providerId: TOKEN_PLAN_PROVIDER_ID, + methodId: "api-key", + label: "Tencent Token Plan", + hint: "Hy via Token Plan", + optionKey: "tencentTokenPlanApiKey", + flagName: "--tencent-token-plan-api-key", + envVar: "LKEAP_API_KEY", + promptMessage: "Enter Tencent Token Plan API key", + defaultModel: TOKEN_PLAN_DEFAULT_MODEL_REF, + expectedProviders: [TOKEN_PLAN_PROVIDER_ID], + applyConfig: (cfg) => applyTokenPlanConfig(cfg), + wizard: { + choiceId: "tencent-token-plan-api-key", + choiceLabel: "Tencent Token Plan", + groupId: "tencent", + groupLabel: "Tencent Cloud", + groupHint: "TokenHub + Token Plan", + }, + }), + ], + catalog: { + order: "simple", + run: (ctx) => + buildSingleProviderApiKeyCatalog({ + ctx, + providerId: TOKEN_PLAN_PROVIDER_ID, + buildProvider: buildTokenPlanProvider, + }), + }, + augmentModelCatalog: () => + buildStaticCatalogEntries(TOKEN_PLAN_PROVIDER_ID, TOKEN_PLAN_MODEL_CATALOG), + }); + }, +}); diff --git a/extensions/tencent/models.ts b/extensions/tencent/models.ts new file mode 100644 index 00000000000..131942c64b7 --- /dev/null +++ b/extensions/tencent/models.ts @@ -0,0 +1,93 @@ +import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared"; + +// ---------- TokenHub provider ---------- + +export const TOKENHUB_BASE_URL = "https://tokenhub.tencentmaas.com/v1"; +export const TOKENHUB_PROVIDER_ID = "tencent-tokenhub"; + +// Hy3 preview pricing ($ per 1M tokens), tiered by input context length. +// Flat rates mirror the first tier; tieredPricing drives actual cost calculation. +const HY3_PREVIEW_COST = { + input: 0.176, + output: 0.587, + cacheRead: 0.059, + cacheWrite: 0, + tieredPricing: [ + { + input: 0.176, + output: 0.587, + cacheRead: 0.059, + cacheWrite: 0, + range: [0, 16_000] as [number, number], + }, + { + input: 0.235, + output: 0.939, + cacheRead: 0.088, + cacheWrite: 0, + range: [16_000, 32_000] as [number, number], + }, + { + input: 0.293, + output: 1.173, + cacheRead: 0.117, + cacheWrite: 0, + range: [32_000] as [number], + }, + ], +}; + +export const TOKENHUB_MODEL_CATALOG: ModelDefinitionConfig[] = [ + { + id: "hy3-preview", + name: "Hy3 preview (TokenHub)", + reasoning: true, + input: ["text"], + contextWindow: 256_000, + maxTokens: 64_000, + cost: HY3_PREVIEW_COST, + compat: { + supportsUsageInStreaming: true, + supportsReasoningEffort: true, + }, + }, +]; + +export function buildTokenHubModelDefinition( + model: (typeof TOKENHUB_MODEL_CATALOG)[number], +): ModelDefinitionConfig { + return { + ...model, + api: "openai-completions", + }; +} + +// ---------- Token Plan provider ---------- + +export const TOKEN_PLAN_BASE_URL = "https://api.lkeap.cloud.tencent.com/plan/v3"; +export const TOKEN_PLAN_PROVIDER_ID = "tencent-token-plan"; + +export const TOKEN_PLAN_MODEL_CATALOG: ModelDefinitionConfig[] = [ + { + id: "hy3-preview", + name: "Hy3 preview (Token Plan)", + reasoning: true, + input: ["text"], + contextWindow: 256_000, + maxTokens: 64_000, + cost: HY3_PREVIEW_COST, + compat: { + supportsUsageInStreaming: true, + supportsReasoningEffort: true, + }, + }, +]; + +export function buildTokenPlanModelDefinition( + model: (typeof TOKEN_PLAN_MODEL_CATALOG)[number], +): ModelDefinitionConfig { + return { + ...model, + api: "openai-completions", + }; +} diff --git a/extensions/tencent/onboard.ts b/extensions/tencent/onboard.ts new file mode 100644 index 00000000000..1f66ad5d639 --- /dev/null +++ b/extensions/tencent/onboard.ts @@ -0,0 +1,73 @@ +import { + applyAgentDefaultModelPrimary, + applyProviderConfigWithModelCatalog, + type OpenClawConfig, +} from "openclaw/plugin-sdk/provider-onboard"; +import { + buildTokenHubModelDefinition, + buildTokenPlanModelDefinition, + TOKENHUB_BASE_URL, + TOKENHUB_MODEL_CATALOG, + TOKENHUB_PROVIDER_ID, + TOKEN_PLAN_BASE_URL, + TOKEN_PLAN_MODEL_CATALOG, + TOKEN_PLAN_PROVIDER_ID, +} from "./api.js"; + +// ---------- TokenHub ---------- + +export const TOKENHUB_DEFAULT_MODEL_REF = `${TOKENHUB_PROVIDER_ID}/hy3-preview`; + +function applyTokenHubProviderConfig(cfg: OpenClawConfig): OpenClawConfig { + const models = { ...cfg.agents?.defaults?.models }; + models[TOKENHUB_DEFAULT_MODEL_REF] = { + ...models[TOKENHUB_DEFAULT_MODEL_REF], + // Provider-specific alias to keep alias resolution deterministic when + // both Tencent providers are enabled (see buildModelAliasIndex). + alias: models[TOKENHUB_DEFAULT_MODEL_REF]?.alias ?? "Hy3 preview (TokenHub)", + }; + + return applyProviderConfigWithModelCatalog(cfg, { + agentModels: models, + providerId: TOKENHUB_PROVIDER_ID, + api: "openai-completions", + baseUrl: TOKENHUB_BASE_URL, + catalogModels: TOKENHUB_MODEL_CATALOG.map(buildTokenHubModelDefinition), + }); +} + +export function applyTokenHubConfig(cfg: OpenClawConfig): OpenClawConfig { + return applyAgentDefaultModelPrimary( + applyTokenHubProviderConfig(cfg), + TOKENHUB_DEFAULT_MODEL_REF, + ); +} + +// ---------- Token Plan ---------- + +export const TOKEN_PLAN_DEFAULT_MODEL_REF = `${TOKEN_PLAN_PROVIDER_ID}/hy3-preview`; + +function applyTokenPlanProviderConfig(cfg: OpenClawConfig): OpenClawConfig { + const models = { ...cfg.agents?.defaults?.models }; + models[TOKEN_PLAN_DEFAULT_MODEL_REF] = { + ...models[TOKEN_PLAN_DEFAULT_MODEL_REF], + // Provider-specific alias to keep alias resolution deterministic when + // both Tencent providers are enabled (see buildModelAliasIndex). + alias: models[TOKEN_PLAN_DEFAULT_MODEL_REF]?.alias ?? "Hy3 preview (Token Plan)", + }; + + return applyProviderConfigWithModelCatalog(cfg, { + agentModels: models, + providerId: TOKEN_PLAN_PROVIDER_ID, + api: "openai-completions", + baseUrl: TOKEN_PLAN_BASE_URL, + catalogModels: TOKEN_PLAN_MODEL_CATALOG.map(buildTokenPlanModelDefinition), + }); +} + +export function applyTokenPlanConfig(cfg: OpenClawConfig): OpenClawConfig { + return applyAgentDefaultModelPrimary( + applyTokenPlanProviderConfig(cfg), + TOKEN_PLAN_DEFAULT_MODEL_REF, + ); +} diff --git a/extensions/tencent/openclaw.plugin.json b/extensions/tencent/openclaw.plugin.json new file mode 100644 index 00000000000..7fa34e875b0 --- /dev/null +++ b/extensions/tencent/openclaw.plugin.json @@ -0,0 +1,42 @@ +{ + "id": "tencent", + "enabledByDefault": true, + "providers": ["tencent-tokenhub", "tencent-token-plan"], + "providerAuthEnvVars": { + "tencent-tokenhub": ["TOKENHUB_API_KEY"], + "tencent-token-plan": ["LKEAP_API_KEY"] + }, + "providerAuthChoices": [ + { + "provider": "tencent-tokenhub", + "method": "api-key", + "choiceId": "tokenhub-api-key", + "choiceLabel": "Tencent TokenHub", + "groupId": "tencent", + "groupLabel": "Tencent Cloud", + "groupHint": "TokenHub + Token Plan", + "optionKey": "tokenhubApiKey", + "cliFlag": "--tokenhub-api-key", + "cliOption": "--tokenhub-api-key ", + "cliDescription": "Tencent TokenHub API key" + }, + { + "provider": "tencent-token-plan", + "method": "api-key", + "choiceId": "tencent-token-plan-api-key", + "choiceLabel": "Tencent Token Plan", + "groupId": "tencent", + "groupLabel": "Tencent Cloud", + "groupHint": "TokenHub + Token Plan", + "optionKey": "tencentTokenPlanApiKey", + "cliFlag": "--tencent-token-plan-api-key", + "cliOption": "--tencent-token-plan-api-key ", + "cliDescription": "Tencent Token Plan API key" + } + ], + "configSchema": { + "type": "object", + "additionalProperties": false, + "properties": {} + } +} diff --git a/extensions/tencent/package.json b/extensions/tencent/package.json new file mode 100644 index 00000000000..8825dff9606 --- /dev/null +++ b/extensions/tencent/package.json @@ -0,0 +1,15 @@ +{ + "name": "@openclaw/tencent-provider", + "version": "2026.4.10", + "private": true, + "description": "OpenClaw Tencent Cloud provider plugin (TokenHub + Token Plan)", + "type": "module", + "devDependencies": { + "@openclaw/plugin-sdk": "workspace:*" + }, + "openclaw": { + "extensions": [ + "./index.ts" + ] + } +} diff --git a/extensions/tencent/provider-catalog.ts b/extensions/tencent/provider-catalog.ts new file mode 100644 index 00000000000..43fe05f1816 --- /dev/null +++ b/extensions/tencent/provider-catalog.ts @@ -0,0 +1,25 @@ +import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared"; +import { + buildTokenHubModelDefinition, + buildTokenPlanModelDefinition, + TOKENHUB_BASE_URL, + TOKENHUB_MODEL_CATALOG, + TOKEN_PLAN_BASE_URL, + TOKEN_PLAN_MODEL_CATALOG, +} from "./models.js"; + +export function buildTokenHubProvider(): ModelProviderConfig { + return { + baseUrl: TOKENHUB_BASE_URL, + api: "openai-completions", + models: TOKENHUB_MODEL_CATALOG.map(buildTokenHubModelDefinition), + }; +} + +export function buildTokenPlanProvider(): ModelProviderConfig { + return { + baseUrl: TOKEN_PLAN_BASE_URL, + api: "openai-completions", + models: TOKEN_PLAN_MODEL_CATALOG.map(buildTokenPlanModelDefinition), + }; +} diff --git a/extensions/tencent/tsconfig.json b/extensions/tencent/tsconfig.json new file mode 100644 index 00000000000..b8a85a99ac3 --- /dev/null +++ b/extensions/tencent/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../tsconfig.package-boundary.base.json", + "compilerOptions": { + "rootDir": "." + }, + "include": ["./*.ts", "./src/**/*.ts"], + "exclude": [ + "./**/*.test.ts", + "./dist/**", + "./node_modules/**", + "./src/test-support/**", + "./src/**/*test-helpers.ts", + "./src/**/*test-harness.ts", + "./src/**/*test-support.ts" + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9dbd3b5c73a..83ad3526310 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1182,6 +1182,12 @@ importers: specifier: workspace:* version: link:../../packages/plugin-sdk + extensions/tencent: + devDependencies: + '@openclaw/plugin-sdk': + specifier: workspace:* + version: link:../../packages/plugin-sdk + extensions/tlon: dependencies: '@aws-sdk/client-s3':