feat(tencent): add Tencent Hy3 provider (TokenHub and TokenPlan) (#99076)

Summary:
- Merged feat(tencent): add Tencent Hy3 provider (TokenHub and TokenPlan) after ClawSweeper review.

Automerge notes:
- Ran the ClawSweeper repair loop before final review.
- Included post-review commit in the final squash: fix(tencent): preserve TokenHub auth compatibility
- Included post-review commit in the final squash: refactor(tencent): unify TokenPlan env/flag naming with TokenHub
- Included post-review commit in the final squash: docs: refresh Tencent provider docs metadata
- Included post-review commit in the final squash: fix: allow TokenPlan provider config overlays
- Included post-review commit in the final squash: docs: dedupe Tencent provider glossary labels
- Included post-review commit in the final squash: fix(tencent): repair TokenHub model defaults

Validation:
- ClawSweeper review passed for head 30c9fc130f26a038d1430c14e710eb77d99fb0d4.
- Required merge gates passed before the squash merge.

Prepared head SHA: 30c9fc130f26a038d1430c14e710eb77d99fb0d4
Review: https://github.com/openclaw/openclaw/pull/99076#issuecomment-4888527271

Co-authored-by: leisang <leisang@tencent.com>
Co-authored-by: Mason Huang <masonxhuang@tencent.com>
Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: hxy91819
This commit is contained in:
Sulaga 2026-07-06 12:29:48 +08:00 committed by GitHub
parent cfcdf2a519
commit 2b51c255f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1080 additions and 99 deletions

View file

@ -992,8 +992,8 @@
"target": "轨迹导出"
},
{
"source": "Tencent Cloud (TokenHub)",
"target": "腾讯云TokenHub"
"source": "Tencent Cloud (TokenHub / TokenPlan)",
"target": "腾讯云TokenHub / TokenPlan"
},
{
"source": "Codex Harness Context Engine Port",

View file

@ -7813,7 +7813,6 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
- H2: Quick start
- H2: Non-interactive setup
- H2: Built-in catalog
- H2: Tiered pricing
- H2: Advanced configuration
- H2: Related

View file

@ -291,7 +291,7 @@ Each entry lists the package, distribution route, and description.
- **[tavily](/plugins/reference/tavily)** (`@openclaw/tavily-plugin`) - npm; ClawHub: `clawhub:@openclaw/tavily-plugin`. Adds agent-callable tools. Adds web search provider support.
- **[tencent](/plugins/reference/tencent)** (`@openclaw/tencent-provider`) - npm; ClawHub: `clawhub:@openclaw/tencent-provider`. Adds Tencent TokenHub model provider support to OpenClaw.
- **[tencent](/plugins/reference/tencent)** (`@openclaw/tencent-provider`) - npm; ClawHub: `clawhub:@openclaw/tencent-provider`. Adds Tencent TokenHub and TokenPlan model provider support to OpenClaw.
- **[tlon](/plugins/reference/tlon)** (`@openclaw/tlon`) - npm; ClawHub. OpenClaw Tlon/Urbit channel plugin for chat workflows.

View file

@ -1,5 +1,5 @@
---
summary: "Adds Tencent TokenHub model provider support to OpenClaw."
summary: "Adds Tencent Hy3 model provider support (TokenHub and TokenPlan) to OpenClaw."
read_when:
- You are installing, configuring, or auditing the tencent plugin
title: "Tencent plugin"
@ -7,7 +7,7 @@ title: "Tencent plugin"
# Tencent plugin
Adds Tencent TokenHub model provider support to OpenClaw.
Adds Tencent TokenHub and TokenPlan model provider support to OpenClaw.
## Distribution
@ -16,7 +16,7 @@ Adds Tencent TokenHub model provider support to OpenClaw.
## Surface
providers: tencent-tokenhub
providers: tencent-tokenhub, tencent-tokenplan
## Related docs

View file

@ -72,7 +72,7 @@ Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/Mattermost (plugi
- [SGLang (local models)](/providers/sglang)
- [StepFun](/providers/stepfun)
- [Synthetic](/providers/synthetic)
- [Tencent Cloud (TokenHub)](/providers/tencent)
- [Tencent Cloud (TokenHub / TokenPlan)](/providers/tencent)
- [Together AI](/providers/together)
- [Venice (Venice AI, privacy-focused)](/providers/venice)
- [Vercel AI Gateway](/providers/vercel-ai-gateway)

View file

@ -1,50 +1,61 @@
---
summary: "Tencent Cloud TokenHub setup for Hy3 preview"
title: "Tencent Cloud (TokenHub)"
summary: "Tencent Cloud TokenHub and TokenPlan setup for hy3"
title: "Tencent Cloud (TokenHub / TokenPlan)"
read_when:
- You want to use Tencent Hy3 preview with OpenClaw
- You need the TokenHub API key setup
- You want to use Tencent hy3 with OpenClaw
- You need the TokenHub or TokenPlan API key setup
---
Install the official Tencent Cloud provider plugin to access Tencent Hy3 preview through the TokenHub endpoint (`tencent-tokenhub`) using an OpenAI-compatible API.
Install the official Tencent Cloud provider plugin to access Tencent Hy3 through two endpoints — TokenHub (`tencent-tokenhub`) and TokenPlan (`tencent-tokenplan`) — using an OpenAI-compatible API.
| Property | Value |
| --------------- | ---------------------------------------- |
| Provider id | `tencent-tokenhub` |
| Package | `@openclaw/tencent-provider` |
| Auth env var | `TOKENHUB_API_KEY` |
| Onboarding flag | `--auth-choice tokenhub-api-key` |
| Direct CLI flag | `--tokenhub-api-key <key>` |
| API | OpenAI-compatible (`openai-completions`) |
| Base URL | `https://tokenhub.tencentmaas.com/v1` |
| Default model | `tencent-tokenhub/hy3-preview` |
| Property | Value |
| ------------------------- | ----------------------------------------------------- |
| Provider ids | `tencent-tokenhub`, `tencent-tokenplan` |
| Package | `@openclaw/tencent-provider` |
| TokenHub auth env var | `TOKENHUB_API_KEY` |
| TokenPlan auth env var | `TOKENPLAN_API_KEY` |
| TokenHub onboarding flag | `--auth-choice tokenhub-api-key` |
| TokenPlan onboarding flag | `--auth-choice tokenplan-api-key` |
| TokenHub direct CLI flag | `--tokenhub-api-key <key>` |
| TokenPlan direct CLI flag | `--tokenplan-api-key <key>` |
| API | OpenAI-compatible (`openai-completions`) |
| TokenHub base URL | `https://tokenhub.tencentmaas.com/v1` |
| TokenHub global base URL | `https://tokenhub-intl.tencentmaas.com/v1` (override) |
| TokenPlan base URL | `https://api.lkeap.cloud.tencent.com/plan/v3` |
| Default model | `tencent-tokenhub/hy3` |
## Quick start
<Steps>
<Step title="Install the plugin">
```bash
openclaw plugins install @openclaw/tencent-provider
```
</Step>
<Step title="Create a TokenHub API key">
Create an API key in Tencent Cloud TokenHub. If you choose a limited access scope for the key, include **Hy3 preview** in the allowed models.
<Step title="Create a Tencent API key">
Create an API key for Tencent Cloud TokenHub and TokenPlan. If you choose a limited access scope for the key, include **hy3** (and **hy3 preview** if you plan to use it on TokenHub) in the allowed models.
</Step>
<Step title="Run onboarding">
<CodeGroup>
```bash Onboarding
```bash TokenHub onboarding
openclaw onboard --auth-choice tokenhub-api-key
```
```bash Direct flag
```bash TokenHub direct flag
openclaw onboard --non-interactive \
--auth-choice tokenhub-api-key \
--tokenhub-api-key "$TOKENHUB_API_KEY"
```
```bash TokenPlan onboarding
openclaw onboard --auth-choice tokenplan-api-key
```
```bash TokenPlan direct flag
openclaw onboard --non-interactive \
--auth-choice tokenplan-api-key \
--tokenplan-api-key "$TOKENPLAN_API_KEY"
```
```bash Env only
export TOKENHUB_API_KEY=...
export TOKENPLAN_API_KEY=...
```
</CodeGroup>
@ -53,6 +64,7 @@ export TOKENHUB_API_KEY=...
<Step title="Verify the model">
```bash
openclaw models list --provider tencent-tokenhub
openclaw models list --provider tencent-tokenplan
```
</Step>
</Steps>
@ -60,12 +72,21 @@ export TOKENHUB_API_KEY=...
## Non-interactive setup
```bash
# TokenHub
openclaw onboard --non-interactive \
--mode local \
--auth-choice tokenhub-api-key \
--tokenhub-api-key "$TOKENHUB_API_KEY" \
--skip-health \
--accept-risk
# TokenPlan
openclaw onboard --non-interactive \
--mode local \
--auth-choice tokenplan-api-key \
--tokenplan-api-key "$TOKENPLAN_API_KEY" \
--skip-health \
--accept-risk
```
<Note>
@ -74,28 +95,18 @@ openclaw onboard --non-interactive \
## Built-in catalog
| Model ref | Name | Input | Context | Max output | Notes |
| ------------------------------ | ---------------------- | ----- | ------- | ---------- | -------------------------- |
| `tencent-tokenhub/hy3-preview` | Hy3 preview (TokenHub) | text | 256,000 | 64,000 | Default; reasoning-enabled |
| Model ref | Name | Input | Context | Max output | Notes |
| ------------------------------ | ---------------------- | ----- | ------- | ---------- | ----------------- |
| `tencent-tokenhub/hy3-preview` | hy3 preview (TokenHub) | text | 256,000 | 64,000 | reasoning-enabled |
| `tencent-tokenhub/hy3` | hy3 (TokenHub) | text | 256,000 | 64,000 | reasoning-enabled |
| `tencent-tokenplan/hy3` | hy3 (TokenPlan) | text | 256,000 | 64,000 | reasoning-enabled |
Hy3 preview is Tencent Hunyuan's large MoE language model for reasoning, long-context instruction following, code, and agent workflows. It supports standard chat-completions tool calling plus `reasoning_effort`.
hy3 is Tencent Hunyuan's large MoE language model for reasoning, long-context instruction following, code, and agent workflows. Tencent's OpenAI-compatible examples use `hy3` as the model id and support standard chat-completions tool calling plus `reasoning_effort`.
<Tip>
The model id is `hy3-preview`. Do not confuse it with Tencent's `HY-3D-*` models, which are 3D generation APIs and are not the OpenClaw chat model configured by this provider.
The model id is `hy3`. Do not confuse it with Tencent's `HY-3D-*` models, which are 3D generation APIs and are not the OpenClaw chat model configured by this provider.
</Tip>
## Tiered pricing
The provider catalog ships tiered cost metadata that scales with input window length, so cost estimates are populated without manual overrides.
| Input tokens range | Input rate | Output rate | Cache read |
| ------------------ | ---------- | ----------- | ---------- |
| 0 - 16,000 | 0.176 | 0.587 | 0.059 |
| 16,000 - 32,000 | 0.235 | 0.939 | 0.088 |
| 32,000+ | 0.293 | 1.173 | 0.117 |
Rates are per million tokens in USD as advertised by Tencent. Override pricing under `models.providers.tencent-tokenhub` only when you need a different surface.
## Advanced configuration
<AccordionGroup>
@ -109,7 +120,7 @@ Rates are per million tokens in USD as advertised by Tencent. Override pricing u
</Accordion>
<Accordion title="Environment availability for the daemon">
If the Gateway runs as a managed service (launchd, systemd, Docker), `TOKENHUB_API_KEY` must be visible to that process. Set it in `~/.openclaw/.env` or via `env.shellEnv` so launchd, systemd, or Docker exec environments can read it.
If the Gateway runs as a managed service (launchd, systemd, Docker), `TOKENHUB_API_KEY` and `TOKENPLAN_API_KEY` must be visible to that process. Set them in `~/.openclaw/.env` or via `env.shellEnv` so launchd, systemd, or Docker exec environments can read them.
<Warning>
Keys exported only in an interactive shell are not visible to managed gateway processes. Use the env file or config seam for persistent availability.

View file

@ -1,8 +1,12 @@
// Tencent API module exposes the plugin public contract.
export {
buildTokenHubModelDefinition,
buildTokenPlanModelDefinition,
TOKENHUB_BASE_URL,
TOKENHUB_MODEL_CATALOG,
TOKENHUB_PROVIDER_ID,
TOKENPLAN_BASE_URL,
TOKENPLAN_MODEL_CATALOG,
TOKENPLAN_PROVIDER_ID,
} from "./models.js";
export { buildTokenHubProvider } from "./provider-catalog.js";
export { buildTokenHubProvider, buildTokenPlanProvider } from "./provider-catalog.js";

View file

@ -0,0 +1,112 @@
// Tencent tests cover config compatibility repair behavior.
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
import { describe, expect, it } from "vitest";
import {
migrateTencentTokenHubModelDefaults,
TENCENT_TOKENHUB_DEFAULT_MODEL_REF,
TENCENT_TOKENHUB_PREVIEW_MODEL_REF,
} from "./config-compat.js";
describe("Tencent config compatibility", () => {
it("adds the stable TokenHub model and makes it primary for old preview defaults", () => {
const config = {
agents: {
defaults: {
model: {
primary: TENCENT_TOKENHUB_PREVIEW_MODEL_REF,
fallbacks: ["openai/gpt-5.5"],
},
models: {
[TENCENT_TOKENHUB_PREVIEW_MODEL_REF]: {
alias: "Preview",
},
},
},
},
} as OpenClawConfig;
const result = migrateTencentTokenHubModelDefaults(config);
expect(result.changes).toEqual([
`Updated Tencent TokenHub agent model defaults to include ${TENCENT_TOKENHUB_DEFAULT_MODEL_REF} and ${TENCENT_TOKENHUB_PREVIEW_MODEL_REF}.`,
`Changed Tencent TokenHub primary default from ${TENCENT_TOKENHUB_PREVIEW_MODEL_REF} to ${TENCENT_TOKENHUB_DEFAULT_MODEL_REF}.`,
]);
expect(result.config.agents?.defaults?.model).toEqual({
primary: TENCENT_TOKENHUB_DEFAULT_MODEL_REF,
fallbacks: ["openai/gpt-5.5"],
});
expect(result.config.agents?.defaults?.models).toEqual({
[TENCENT_TOKENHUB_PREVIEW_MODEL_REF]: {
alias: "Preview",
},
[TENCENT_TOKENHUB_DEFAULT_MODEL_REF]: {
alias: "Hy3 (TokenHub)",
},
});
expect(config.agents?.defaults?.models).not.toHaveProperty(TENCENT_TOKENHUB_DEFAULT_MODEL_REF);
});
it("adds the preview TokenHub model for hy3-only intermediate configs", () => {
const config = {
agents: {
defaults: {
model: TENCENT_TOKENHUB_DEFAULT_MODEL_REF,
models: {
[TENCENT_TOKENHUB_DEFAULT_MODEL_REF]: {},
},
},
},
} as OpenClawConfig;
const result = migrateTencentTokenHubModelDefaults(config);
expect(result.config.agents?.defaults?.model).toBe(TENCENT_TOKENHUB_DEFAULT_MODEL_REF);
expect(result.config.agents?.defaults?.models).toEqual({
[TENCENT_TOKENHUB_DEFAULT_MODEL_REF]: {
alias: "Hy3 (TokenHub)",
},
[TENCENT_TOKENHUB_PREVIEW_MODEL_REF]: {
alias: "Hy3 preview (TokenHub)",
},
});
});
it("does not create a model allowlist when TokenHub models are not already configured", () => {
const config = {
models: {
providers: {
"tencent-tokenhub": {
baseUrl: "https://tokenhub.tencentmaas.com/v1",
models: [],
},
},
},
} as OpenClawConfig;
const result = migrateTencentTokenHubModelDefaults(config);
expect(result).toEqual({ config, changes: [] });
});
it("does not report changes after TokenHub defaults are already repaired", () => {
const config = {
agents: {
defaults: {
model: { primary: TENCENT_TOKENHUB_DEFAULT_MODEL_REF },
models: {
[TENCENT_TOKENHUB_DEFAULT_MODEL_REF]: {
alias: "Hy3 (TokenHub)",
},
[TENCENT_TOKENHUB_PREVIEW_MODEL_REF]: {
alias: "Hy3 preview (TokenHub)",
},
},
},
},
} as OpenClawConfig;
const result = migrateTencentTokenHubModelDefaults(config);
expect(result).toEqual({ config, changes: [] });
});
});

View file

@ -0,0 +1,120 @@
// Tencent config compatibility repairs shipped TokenHub model allowlists.
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
export const TENCENT_TOKENHUB_DEFAULT_MODEL_REF = "tencent-tokenhub/hy3";
export const TENCENT_TOKENHUB_PREVIEW_MODEL_REF = "tencent-tokenhub/hy3-preview";
const TOKENHUB_DEFAULT_ALIAS = "Hy3 (TokenHub)";
const TOKENHUB_PREVIEW_ALIAS = "Hy3 preview (TokenHub)";
type AgentDefaults = NonNullable<NonNullable<OpenClawConfig["agents"]>["defaults"]>;
type AgentDefaultModel = AgentDefaults["model"];
type AgentModelEntry = NonNullable<AgentDefaults["models"]>[string];
function isTokenHubModelMapConfigured(models: Record<string, AgentModelEntry>): boolean {
return (
Object.hasOwn(models, TENCENT_TOKENHUB_DEFAULT_MODEL_REF) ||
Object.hasOwn(models, TENCENT_TOKENHUB_PREVIEW_MODEL_REF)
);
}
function withDefaultAlias(entry: AgentModelEntry | undefined, alias: string): AgentModelEntry {
return {
...entry,
alias: entry?.alias ?? alias,
};
}
function needsDefaultAlias(entry: AgentModelEntry | undefined): boolean {
return entry?.alias === undefined;
}
function migrateDefaultModel(model: AgentDefaultModel): {
model: AgentDefaultModel;
changed: boolean;
} {
if (model === TENCENT_TOKENHUB_PREVIEW_MODEL_REF) {
return {
model: { primary: TENCENT_TOKENHUB_DEFAULT_MODEL_REF },
changed: true,
};
}
if (
model &&
typeof model === "object" &&
"primary" in model &&
model.primary === TENCENT_TOKENHUB_PREVIEW_MODEL_REF
) {
return {
model: {
...model,
primary: TENCENT_TOKENHUB_DEFAULT_MODEL_REF,
},
changed: true,
};
}
return { model, changed: false };
}
export function migrateTencentTokenHubModelDefaults(cfg: OpenClawConfig): {
config: OpenClawConfig;
changes: string[];
} {
const existingModels = cfg.agents?.defaults?.models;
if (!existingModels || !isTokenHubModelMapConfigured(existingModels)) {
return { config: cfg, changes: [] };
}
const needsDefaultRepair =
!Object.hasOwn(existingModels, TENCENT_TOKENHUB_DEFAULT_MODEL_REF) ||
needsDefaultAlias(existingModels[TENCENT_TOKENHUB_DEFAULT_MODEL_REF]);
const needsPreviewRepair =
!Object.hasOwn(existingModels, TENCENT_TOKENHUB_PREVIEW_MODEL_REF) ||
needsDefaultAlias(existingModels[TENCENT_TOKENHUB_PREVIEW_MODEL_REF]);
const migratedModel = migrateDefaultModel(cfg.agents?.defaults?.model);
if (!needsDefaultRepair && !needsPreviewRepair && !migratedModel.changed) {
return { config: cfg, changes: [] };
}
const nextModels = {
...existingModels,
[TENCENT_TOKENHUB_DEFAULT_MODEL_REF]: withDefaultAlias(
existingModels[TENCENT_TOKENHUB_DEFAULT_MODEL_REF],
TOKENHUB_DEFAULT_ALIAS,
),
[TENCENT_TOKENHUB_PREVIEW_MODEL_REF]: withDefaultAlias(
existingModels[TENCENT_TOKENHUB_PREVIEW_MODEL_REF],
TOKENHUB_PREVIEW_ALIAS,
),
};
const nextConfig: OpenClawConfig = {
...cfg,
agents: {
...cfg.agents,
defaults: {
...cfg.agents?.defaults,
models: nextModels,
...(migratedModel.model !== undefined ? { model: migratedModel.model } : undefined),
},
},
};
const changes = [
`Updated Tencent TokenHub agent model defaults to include ${TENCENT_TOKENHUB_DEFAULT_MODEL_REF} and ${TENCENT_TOKENHUB_PREVIEW_MODEL_REF}.`,
];
if (migratedModel.changed) {
changes.push(
`Changed Tencent TokenHub primary default from ${TENCENT_TOKENHUB_PREVIEW_MODEL_REF} to ${TENCENT_TOKENHUB_DEFAULT_MODEL_REF}.`,
);
}
return { config: nextConfig, changes };
}
export function normalizeCompatibilityConfig({ cfg }: { cfg: OpenClawConfig }): {
config: OpenClawConfig;
changes: string[];
} {
return migrateTencentTokenHubModelDefaults(cfg);
}

View file

@ -0,0 +1,320 @@
// Tencent tests cover index plugin behavior.
import type { StreamFn } from "openclaw/plugin-sdk/agent-core";
import type { Context, Model } from "openclaw/plugin-sdk/llm";
import {
registerProviderPlugin,
requireRegisteredProvider,
resolveProviderPluginChoice,
} from "openclaw/plugin-sdk/plugin-test-runtime";
import { buildOpenAICompletionsParams } from "openclaw/plugin-sdk/provider-transport-runtime";
import { describe, expect, it } from "vitest";
import { runSingleProviderCatalog } from "../test-support/provider-model-test-helpers.js";
import tencentPlugin from "./index.js";
type OpenAICompletionsModel = Model<"openai-completions">;
const registerTencentPlugin = () =>
registerProviderPlugin({
plugin: tencentPlugin,
id: "tencent",
name: "Tencent Cloud Provider",
});
async function getTokenHubProvider() {
const { providers } = await registerTencentPlugin();
return requireRegisteredProvider(providers, "tencent-tokenhub");
}
async function getTokenPlanProvider() {
const { providers } = await registerTencentPlugin();
return requireRegisteredProvider(providers, "tencent-tokenplan");
}
function hyReasoningModel(params: {
provider: "tencent-tokenhub" | "tencent-tokenplan";
id: "hy3" | "hy3-preview";
baseUrl: string;
supportedReasoningEfforts?: string[];
}): OpenAICompletionsModel {
return {
provider: params.provider,
id: params.id,
name: params.id,
api: "openai-completions",
baseUrl: params.baseUrl,
reasoning: true,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 256_000,
maxTokens: 64_000,
compat: {
supportsUsageInStreaming: true,
supportsReasoningEffort: true,
supportedReasoningEfforts: params.supportedReasoningEfforts ?? ["none", "high"],
},
} as OpenAICompletionsModel;
}
function captureTencentPayload(params: {
provider: Pick<Awaited<ReturnType<typeof getTokenHubProvider>>, "wrapStreamFn">;
model: OpenAICompletionsModel;
reasoning: string;
}) {
let captured: Record<string, unknown> | undefined;
const baseStreamFn: StreamFn = (_model, context, options) => {
const payload = buildOpenAICompletionsParams(_model as OpenAICompletionsModel, context, options);
options?.onPayload?.(payload, _model);
captured = payload;
return {} as ReturnType<StreamFn>;
};
const wrapped = params.provider.wrapStreamFn?.({
streamFn: baseStreamFn,
provider: params.model.provider,
modelId: params.model.id,
model: params.model,
thinkingLevel: "high",
});
if (!wrapped) {
throw new Error("expected Tencent provider stream wrapper");
}
void wrapped(
params.model,
{ messages: [] } as never,
{
reasoning: params.reasoning,
} as never,
);
return captured;
}
describe("tencent provider plugin", () => {
it("registers Tencent TokenHub api-key auth metadata", async () => {
const { providers } = await registerTencentPlugin();
const provider = requireRegisteredProvider(providers, "tencent-tokenhub");
const resolved = resolveProviderPluginChoice({
providers,
choice: "tokenhub-api-key",
});
expect(provider.id).toBe("tencent-tokenhub");
expect(provider.label).toBe("Tencent TokenHub");
expect(provider.envVars).toEqual(["TOKENHUB_API_KEY"]);
expect(provider.auth).toHaveLength(1);
if (!resolved) {
throw new Error("expected Tencent TokenHub api-key auth choice");
}
expect(resolved.provider.id).toBe("tencent-tokenhub");
expect(resolved.method.id).toBe("api-key");
});
it("registers Tencent TokenPlan api-key auth metadata", async () => {
const { providers } = await registerTencentPlugin();
const provider = requireRegisteredProvider(providers, "tencent-tokenplan");
const resolved = resolveProviderPluginChoice({
providers,
choice: "tokenplan-api-key",
});
expect(provider.id).toBe("tencent-tokenplan");
expect(provider.label).toBe("Tencent TokenPlan");
expect(provider.envVars).toEqual(["TOKENPLAN_API_KEY"]);
expect(provider.auth).toHaveLength(1);
if (!resolved) {
throw new Error("expected Tencent TokenPlan api-key auth choice");
}
expect(resolved.provider.id).toBe("tencent-tokenplan");
expect(resolved.method.id).toBe("api-key");
});
it("builds the static Tencent TokenHub model catalog with reasoning flags", async () => {
const provider = await getTokenHubProvider();
const catalogProvider = await runSingleProviderCatalog(provider);
expect(catalogProvider.api).toBe("openai-completions");
expect(catalogProvider.baseUrl).toBe("https://tokenhub.tencentmaas.com/v1");
const modelIds = catalogProvider.models?.map((m) => m.id);
expect(modelIds).toContain("hy3");
expect(modelIds).toContain("hy3-preview");
const hy3 = catalogProvider.models?.find((m) => m.id === "hy3");
expect(hy3?.reasoning).toBe(true);
expect(hy3?.compat?.supportsReasoningEffort).toBe(true);
expect(hy3?.compat?.supportedReasoningEfforts).toEqual(["none", "high"]);
const hy3Preview = catalogProvider.models?.find((m) => m.id === "hy3-preview");
expect(hy3Preview?.reasoning).toBe(true);
expect(hy3Preview?.compat?.supportsReasoningEffort).toBe(true);
expect(hy3Preview?.compat?.supportedReasoningEfforts).toEqual(["none", "low", "high"]);
});
it("builds the static Tencent TokenPlan model catalog with reasoning flags", async () => {
const provider = await getTokenPlanProvider();
const catalogProvider = await runSingleProviderCatalog(provider);
expect(catalogProvider.api).toBe("openai-completions");
expect(catalogProvider.baseUrl).toBe("https://api.lkeap.cloud.tencent.com/plan/v3");
const modelIds = catalogProvider.models?.map((m) => m.id);
expect(modelIds).toEqual(["hy3"]);
const hy3 = catalogProvider.models?.find((m) => m.id === "hy3");
expect(hy3?.reasoning).toBe(true);
expect(hy3?.compat?.supportsReasoningEffort).toBe(true);
expect(hy3?.compat?.supportedReasoningEfforts).toEqual(["none", "high"]);
});
it("injects reasoning_effort into TokenPlan hy3 chat-completions payload", async () => {
const model = hyReasoningModel({
provider: "tencent-tokenplan",
id: "hy3",
baseUrl: "https://api.lkeap.cloud.tencent.com/plan/v3",
});
const context = { messages: [{ role: "user", content: "hi", timestamp: 1 }] } as Context;
const payload = buildOpenAICompletionsParams(model, context, {
reasoning: "high",
} as never);
expect(payload.model).toBe("hy3");
expect(payload.reasoning_effort).toBe("high");
});
it("emits reasoning_effort=high when high effort is requested for TokenHub hy3", async () => {
const model = hyReasoningModel({
provider: "tencent-tokenhub",
id: "hy3",
baseUrl: "https://tokenhub.tencentmaas.com/v1",
});
const context = { messages: [{ role: "user", content: "hi", timestamp: 1 }] } as Context;
const payload = buildOpenAICompletionsParams(model, context, {
reasoning: "high",
} as never);
expect(payload.reasoning_effort).toBe("high");
});
it("emits reasoning_effort=none when none effort is requested for TokenHub hy3", async () => {
const model = hyReasoningModel({
provider: "tencent-tokenhub",
id: "hy3",
baseUrl: "https://tokenhub.tencentmaas.com/v1",
});
const context = { messages: [{ role: "user", content: "hi", timestamp: 1 }] } as Context;
const payload = buildOpenAICompletionsParams(model, context, {
reasoning: "none",
} as never);
expect(payload.reasoning_effort).toBe("none");
});
it("defaults hy3-preview reasoning_effort to high when no effort is provided", async () => {
const model = hyReasoningModel({
provider: "tencent-tokenhub",
id: "hy3-preview",
baseUrl: "https://tokenhub.tencentmaas.com/v1",
supportedReasoningEfforts: ["none", "low", "high"],
});
const context = { messages: [{ role: "user", content: "hi", timestamp: 1 }] } as Context;
const payload = buildOpenAICompletionsParams(model, context, undefined);
expect(payload.reasoning_effort).toBe("high");
});
it("preserves low reasoning_effort for TokenHub hy3-preview", async () => {
const provider = await getTokenHubProvider();
const model = hyReasoningModel({
provider: "tencent-tokenhub",
id: "hy3-preview",
baseUrl: "https://tokenhub.tencentmaas.com/v1",
supportedReasoningEfforts: ["none", "low", "high"],
});
const payload = captureTencentPayload({
provider,
model,
reasoning: "low",
});
expect(payload?.reasoning_effort).toBe("low");
});
it("keeps TokenHub hy3 explicit high and none reasoning_effort unchanged", async () => {
const provider = await getTokenHubProvider();
const model = hyReasoningModel({
provider: "tencent-tokenhub",
id: "hy3",
baseUrl: "https://tokenhub.tencentmaas.com/v1",
});
const highPayload = captureTencentPayload({
provider,
model,
reasoning: "high",
});
const nonePayload = captureTencentPayload({
provider,
model,
reasoning: "none",
});
expect(highPayload?.reasoning_effort).toBe("high");
expect(nonePayload?.reasoning_effort).toBe("none");
});
it("keeps minimal reasoning enabled for TokenHub and TokenPlan hy3", async () => {
const tokenHubProvider = await getTokenHubProvider();
const tokenPlanProvider = await getTokenPlanProvider();
const tokenHubModel = hyReasoningModel({
provider: "tencent-tokenhub",
id: "hy3",
baseUrl: "https://tokenhub.tencentmaas.com/v1",
});
const tokenPlanModel = hyReasoningModel({
provider: "tencent-tokenplan",
id: "hy3",
baseUrl: "https://api.lkeap.cloud.tencent.com/plan/v3",
});
const tokenHubPayload = captureTencentPayload({
provider: tokenHubProvider,
model: tokenHubModel,
reasoning: "minimal",
});
const tokenPlanPayload = captureTencentPayload({
provider: tokenPlanProvider,
model: tokenPlanModel,
reasoning: "minimal",
});
expect(tokenHubPayload?.reasoning_effort).toBe("high");
expect(tokenPlanPayload?.reasoning_effort).toBe("high");
});
it("keeps TokenHub hy3-preview unsupported efforts on the model fallback path", async () => {
const provider = await getTokenHubProvider();
const model = hyReasoningModel({
provider: "tencent-tokenhub",
id: "hy3-preview",
baseUrl: "https://tokenhub.tencentmaas.com/v1",
supportedReasoningEfforts: ["none", "low", "high"],
});
const minimalPayload = captureTencentPayload({
provider,
model,
reasoning: "minimal",
});
const mediumPayload = captureTencentPayload({
provider,
model,
reasoning: "medium",
});
expect(minimalPayload?.reasoning_effort).toBe("low");
expect(mediumPayload?.reasoning_effort).toBe("low");
});
});

View file

@ -2,9 +2,20 @@
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 } from "./models.js";
import { applyTokenHubConfig, TOKENHUB_DEFAULT_MODEL_REF } from "./onboard.js";
import { buildTokenHubProvider } from "./provider-catalog.js";
import {
TOKENHUB_MODEL_CATALOG,
TOKENHUB_PROVIDER_ID,
TOKENPLAN_MODEL_CATALOG,
TOKENPLAN_PROVIDER_ID,
} from "./models.js";
import {
applyTokenHubConfig,
TOKENHUB_DEFAULT_MODEL_REF,
applyTokenPlanConfig,
TOKENPLAN_DEFAULT_MODEL_REF,
} from "./onboard.js";
import { buildTokenHubProvider, buildTokenPlanProvider } from "./provider-catalog.js";
import { wrapTencentProviderStream } from "./stream.js";
function buildStaticCatalogEntries(providerId: string, catalog: typeof TOKENHUB_MODEL_CATALOG) {
return catalog.map((entry) => ({
@ -17,38 +28,40 @@ function buildStaticCatalogEntries(providerId: string, catalog: typeof TOKENHUB_
}));
}
function createTokenHubApiKeyAuthMethod() {
return 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: applyTokenHubConfig,
wizard: {
choiceId: "tokenhub-api-key",
choiceLabel: "Tencent TokenHub",
groupId: "tencent",
groupLabel: "Tencent Cloud",
groupHint: "Tencent TokenHub",
},
});
}
export default definePluginEntry({
id: "tencent",
name: "Tencent Cloud Provider",
description: "Bundled Tencent Cloud provider plugin (TokenHub)",
description: "Bundled Tencent Cloud provider plugin (TokenHub, TokenPlan)",
register(api) {
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: "Tencent TokenHub",
},
}),
],
auth: [createTokenHubApiKeyAuthMethod()],
catalog: {
order: "simple",
run: (ctx) =>
@ -60,6 +73,48 @@ export default definePluginEntry({
},
augmentModelCatalog: () =>
buildStaticCatalogEntries(TOKENHUB_PROVIDER_ID, TOKENHUB_MODEL_CATALOG),
wrapStreamFn: wrapTencentProviderStream,
});
api.registerProvider({
id: TOKENPLAN_PROVIDER_ID,
label: "Tencent TokenPlan",
docsPath: "/providers/tencent",
envVars: ["TOKENPLAN_API_KEY"],
auth: [
createProviderApiKeyAuthMethod({
providerId: TOKENPLAN_PROVIDER_ID,
methodId: "api-key",
label: "Tencent TokenPlan",
hint: "Hy via Tencent TokenPlan Gateway",
optionKey: "tokenplanApiKey",
flagName: "--tokenplan-api-key",
envVar: "TOKENPLAN_API_KEY",
promptMessage: "Enter Tencent TokenPlan API key",
defaultModel: TOKENPLAN_DEFAULT_MODEL_REF,
expectedProviders: [TOKENPLAN_PROVIDER_ID],
applyConfig: applyTokenPlanConfig,
wizard: {
choiceId: "tokenplan-api-key",
choiceLabel: "Tencent TokenPlan",
groupId: "tencent",
groupLabel: "Tencent Cloud",
groupHint: "Tencent TokenPlan",
},
}),
],
catalog: {
order: "simple",
run: (ctx) =>
buildSingleProviderApiKeyCatalog({
ctx,
providerId: TOKENPLAN_PROVIDER_ID,
buildProvider: buildTokenPlanProvider,
}),
},
augmentModelCatalog: () =>
buildStaticCatalogEntries(TOKENPLAN_PROVIDER_ID, TOKENPLAN_MODEL_CATALOG),
wrapStreamFn: wrapTencentProviderStream,
});
},
});

View file

@ -24,3 +24,25 @@ export function buildTokenHubModelDefinition(
api: "openai-completions",
};
}
// ---------- TokenPlan provider ----------
export const TOKENPLAN_PROVIDER_ID = "tencent-tokenplan";
const TOKENPLAN_MANIFEST_PROVIDER = buildManifestModelProviderConfig({
providerId: TOKENPLAN_PROVIDER_ID,
catalog: manifest.modelCatalog.providers[TOKENPLAN_PROVIDER_ID],
});
export const TOKENPLAN_BASE_URL = TOKENPLAN_MANIFEST_PROVIDER.baseUrl;
export const TOKENPLAN_MODEL_CATALOG: ModelDefinitionConfig[] = TOKENPLAN_MANIFEST_PROVIDER.models;
export function buildTokenPlanModelDefinition(
model: (typeof TOKENPLAN_MODEL_CATALOG)[number],
): ModelDefinitionConfig {
return {
...model,
api: "openai-completions",
};
}

View file

@ -6,20 +6,29 @@ import {
} from "openclaw/plugin-sdk/provider-onboard";
import {
buildTokenHubModelDefinition,
buildTokenPlanModelDefinition,
TOKENHUB_BASE_URL,
TOKENHUB_MODEL_CATALOG,
TOKENHUB_PROVIDER_ID,
TOKENPLAN_BASE_URL,
TOKENPLAN_MODEL_CATALOG,
TOKENPLAN_PROVIDER_ID,
} from "./api.js";
// ---------- TokenHub ----------
export const TOKENHUB_DEFAULT_MODEL_REF = `${TOKENHUB_PROVIDER_ID}/hy3-preview`;
export const TOKENHUB_DEFAULT_MODEL_REF = `${TOKENHUB_PROVIDER_ID}/hy3`;
export const TOKENHUB_PREVIEW_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],
alias: models[TOKENHUB_DEFAULT_MODEL_REF]?.alias ?? "Hy3 preview (TokenHub)",
alias: models[TOKENHUB_DEFAULT_MODEL_REF]?.alias ?? "Hy3 (TokenHub)",
};
models[TOKENHUB_PREVIEW_MODEL_REF] = {
...models[TOKENHUB_PREVIEW_MODEL_REF],
alias: models[TOKENHUB_PREVIEW_MODEL_REF]?.alias ?? "Hy3 preview (TokenHub)",
};
return applyProviderConfigWithModelCatalog(cfg, {
@ -37,3 +46,30 @@ export function applyTokenHubConfig(cfg: OpenClawConfig): OpenClawConfig {
TOKENHUB_DEFAULT_MODEL_REF,
);
}
// ---------- TokenPlan ----------
export const TOKENPLAN_DEFAULT_MODEL_REF = `${TOKENPLAN_PROVIDER_ID}/hy3`;
function applyTokenPlanProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
const models = { ...cfg.agents?.defaults?.models };
models[TOKENPLAN_DEFAULT_MODEL_REF] = {
...models[TOKENPLAN_DEFAULT_MODEL_REF],
alias: models[TOKENPLAN_DEFAULT_MODEL_REF]?.alias ?? "Hy3 (TokenPlan)",
};
return applyProviderConfigWithModelCatalog(cfg, {
agentModels: models,
providerId: TOKENPLAN_PROVIDER_ID,
api: "openai-completions",
baseUrl: TOKENPLAN_BASE_URL,
catalogModels: TOKENPLAN_MODEL_CATALOG.map(buildTokenPlanModelDefinition),
});
}
export function applyTokenPlanConfig(cfg: OpenClawConfig): OpenClawConfig {
return applyAgentDefaultModelPrimary(
applyTokenPlanProviderConfig(cfg),
TOKENPLAN_DEFAULT_MODEL_REF,
);
}

View file

@ -5,7 +5,7 @@
},
"enabledByDefault": true,
"providerCatalogEntry": "./provider-discovery.ts",
"providers": ["tencent-tokenhub"],
"providers": ["tencent-tokenhub", "tencent-tokenplan"],
"modelCatalog": {
"providers": {
"tencent-tokenhub": {
@ -50,14 +50,48 @@
},
"compat": {
"supportsUsageInStreaming": true,
"supportsReasoningEffort": true
"supportsReasoningEffort": true,
"supportedReasoningEfforts": ["none", "low", "high"]
}
},
{
"id": "hy3",
"name": "Hy3 (TokenHub)",
"reasoning": true,
"input": ["text"],
"contextWindow": 256000,
"maxTokens": 64000,
"compat": {
"supportsUsageInStreaming": true,
"supportsReasoningEffort": true,
"supportedReasoningEfforts": ["none", "high"]
}
}
]
},
"tencent-tokenplan": {
"baseUrl": "https://api.lkeap.cloud.tencent.com/plan/v3",
"api": "openai-completions",
"models": [
{
"id": "hy3",
"name": "Hy3 (TokenPlan)",
"reasoning": true,
"input": ["text"],
"contextWindow": 256000,
"maxTokens": 64000,
"compat": {
"supportsUsageInStreaming": true,
"supportsReasoningEffort": true,
"supportedReasoningEfforts": ["none", "high"]
}
}
]
}
},
"discovery": {
"tencent-tokenhub": "static"
"tencent-tokenhub": "static",
"tencent-tokenplan": "static"
}
},
"setup": {
@ -65,9 +99,20 @@
{
"id": "tencent-tokenhub",
"envVars": ["TOKENHUB_API_KEY"]
},
{
"id": "tencent-tokenplan",
"envVars": ["TOKENPLAN_API_KEY"]
}
]
},
"configContracts": {
"compatibilityMigrationPaths": [
"models.providers.tencent-tokenhub",
"agents.defaults.models.tencent-tokenhub/hy3",
"agents.defaults.models.tencent-tokenhub/hy3-preview"
]
},
"providerAuthChoices": [
{
"provider": "tencent-tokenhub",
@ -81,6 +126,19 @@
"cliFlag": "--tokenhub-api-key",
"cliOption": "--tokenhub-api-key <key>",
"cliDescription": "Tencent TokenHub API key"
},
{
"provider": "tencent-tokenplan",
"method": "api-key",
"choiceId": "tokenplan-api-key",
"choiceLabel": "Tencent TokenPlan",
"groupId": "tencent",
"groupLabel": "Tencent Cloud",
"groupHint": "Tencent TokenPlan",
"optionKey": "tokenplanApiKey",
"cliFlag": "--tokenplan-api-key",
"cliOption": "--tokenplan-api-key <key>",
"cliDescription": "Tencent TokenPlan API key"
}
],
"configSchema": {

View file

@ -10,6 +10,7 @@
"extensions": [
"./index.ts"
],
"setupEntry": "./setup-api.ts",
"install": {
"clawhubSpec": "clawhub:@openclaw/tencent-provider",
"npmSpec": "@openclaw/tencent-provider",

View file

@ -2,8 +2,11 @@
import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared";
import {
buildTokenHubModelDefinition,
buildTokenPlanModelDefinition,
TOKENHUB_BASE_URL,
TOKENHUB_MODEL_CATALOG,
TOKENPLAN_BASE_URL,
TOKENPLAN_MODEL_CATALOG,
} from "./models.js";
export function buildTokenHubProvider(): ModelProviderConfig {
@ -13,3 +16,11 @@ export function buildTokenHubProvider(): ModelProviderConfig {
models: TOKENHUB_MODEL_CATALOG.map(buildTokenHubModelDefinition),
};
}
export function buildTokenPlanProvider(): ModelProviderConfig {
return {
baseUrl: TOKENPLAN_BASE_URL,
api: "openai-completions",
models: TOKENPLAN_MODEL_CATALOG.map(buildTokenPlanModelDefinition),
};
}

View file

@ -1,18 +1,32 @@
// Tencent provider module implements model/runtime integration.
import type { ProviderPlugin } from "openclaw/plugin-sdk/provider-model-shared";
import { buildTokenHubProvider } from "./provider-catalog.js";
import { buildTokenHubProvider, buildTokenPlanProvider } from "./provider-catalog.js";
const tencentProviderDiscovery: ProviderPlugin = {
id: "tencent-tokenhub",
label: "Tencent TokenHub",
docsPath: "/providers/models",
auth: [],
staticCatalog: {
order: "simple",
run: async () => ({
provider: buildTokenHubProvider(),
}),
const tencentProviderDiscovery: ProviderPlugin[] = [
{
id: "tencent-tokenhub",
label: "Tencent TokenHub",
docsPath: "/providers/tencent",
auth: [],
staticCatalog: {
order: "simple",
run: async () => ({
provider: buildTokenHubProvider(),
}),
},
},
};
{
id: "tencent-tokenplan",
label: "Tencent TokenPlan",
docsPath: "/providers/tencent",
auth: [],
staticCatalog: {
order: "simple",
run: async () => ({
provider: buildTokenPlanProvider(),
}),
},
},
];
export default tencentProviderDiscovery;

View file

@ -0,0 +1,12 @@
// Tencent setup entrypoint exposes lightweight config repair hooks.
import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
import { migrateTencentTokenHubModelDefaults } from "./config-compat.js";
export default definePluginEntry({
id: "tencent",
name: "Tencent Cloud Provider Setup",
description: "Lightweight Tencent Cloud provider setup hooks",
register(api) {
api.registerConfigMigration((config) => migrateTencentTokenHubModelDefaults(config));
},
});

View file

@ -0,0 +1,92 @@
import type { StreamFn } from "openclaw/plugin-sdk/agent-core";
import type { ProviderWrapStreamFnContext } from "openclaw/plugin-sdk/plugin-entry";
import {
createPayloadPatchStreamWrapper,
type OpenAICompatibleThinkingLevel,
} from "openclaw/plugin-sdk/provider-stream-shared";
import { TOKENHUB_PROVIDER_ID, TOKENPLAN_PROVIDER_ID } from "./models.js";
const TENCENT_PROVIDER_IDS: ReadonlySet<string> = new Set([
TOKENHUB_PROVIDER_ID,
TOKENPLAN_PROVIDER_ID,
]);
type StreamModel = Parameters<StreamFn>[0];
type StreamOptions = Parameters<StreamFn>[2];
const TENCENT_REASONING_EFFORT_MAP: Readonly<Record<string, string>> = Object.freeze({
off: "none",
none: "none",
minimal: "high",
low: "high",
medium: "high",
high: "high",
xhigh: "high",
});
const TOKENHUB_HY3_PREVIEW_REASONING_EFFORTS = new Set(["none", "low", "high"]);
function resolveRequestedEffort(
thinkingLevel: OpenAICompatibleThinkingLevel,
options: StreamOptions,
): string | undefined {
const withEffort = (options ?? {}) as { reasoningEffort?: unknown; reasoning?: unknown };
const raw =
(typeof withEffort.reasoningEffort === "string" && withEffort.reasoningEffort) ||
(typeof withEffort.reasoning === "string" && withEffort.reasoning) ||
(typeof thinkingLevel === "string" && thinkingLevel) ||
undefined;
return raw ? raw.trim().toLowerCase() : undefined;
}
function mapEffortForTencent(model: StreamModel, effort: string | undefined): string | undefined {
if (!effort) {
return undefined;
}
if (
(model as { provider?: unknown }).provider === TOKENHUB_PROVIDER_ID &&
(model as { id?: unknown }).id === "hy3-preview"
) {
if (effort === "off") {
return "none";
}
// Preview supports low directly. Leave unsupported requests to the shared
// model fallback that already normalized the underlying payload.
return TOKENHUB_HY3_PREVIEW_REASONING_EFFORTS.has(effort) ? effort : undefined;
}
return TENCENT_REASONING_EFFORT_MAP[effort];
}
function isTencentCompletionsCall(model: StreamModel): boolean {
const provider = (model as { provider?: unknown }).provider;
const api = (model as { api?: unknown }).api;
return (
typeof provider === "string" &&
TENCENT_PROVIDER_IDS.has(provider) &&
api === "openai-completions"
);
}
export function wrapTencentProviderStream(ctx: ProviderWrapStreamFnContext): StreamFn {
return createPayloadPatchStreamWrapper(
ctx.streamFn,
({ payload, model, options }) => {
const requested = resolveRequestedEffort(ctx.thinkingLevel, options);
const mapped = mapEffortForTencent(model, requested);
if (mapped === undefined) {
return;
}
if (mapped === "none" || mapped === "off") {
payload.reasoning_effort = "none";
return;
}
payload.reasoning_effort = mapped;
},
{
shouldPatch: ({ model }) => isTencentCompletionsCall(model),
},
);
}

View file

@ -1221,6 +1221,36 @@
]
}
]
},
{
"id": "tencent-tokenplan",
"name": "Tencent TokenPlan",
"docs": "/providers/tencent",
"categories": [
"cloud",
"llm"
],
"envVars": [
"TOKENPLAN_API_KEY"
],
"authChoices": [
{
"method": "api-key",
"choiceId": "tokenplan-api-key",
"choiceLabel": "Tencent TokenPlan",
"choiceHint": "Hy via Tencent TokenPlan Gateway",
"groupId": "tencent",
"groupLabel": "Tencent Cloud",
"groupHint": "Tencent TokenPlan",
"optionKey": "tokenplanApiKey",
"cliFlag": "--tokenplan-api-key",
"cliOption": "--tokenplan-api-key <key>",
"cliDescription": "Tencent TokenPlan API key",
"onboardingScopes": [
"text-inference"
]
}
]
}
],
"install": {

View file

@ -77,11 +77,14 @@ describe("model provider localService config", () => {
}
});
it("accepts standalone timeout overlays for Xiaomi Token Plan", () => {
it.each([
{ provider: "xiaomi-token-plan", name: "Xiaomi Token Plan" },
{ provider: "tencent-tokenplan", name: "Tencent TokenPlan" },
] as const)("accepts standalone timeout overlays for $name", ({ provider }) => {
const result = validateConfigObjectRaw({
models: {
providers: {
"xiaomi-token-plan": {
[provider]: {
timeoutSeconds: 600,
},
},
@ -90,9 +93,9 @@ describe("model provider localService config", () => {
expect(result.ok).toBe(true);
if (result.ok) {
expect(result.config.models?.providers?.["xiaomi-token-plan"]?.timeoutSeconds).toBe(600);
expect(result.config.models?.providers?.["xiaomi-token-plan"]?.models).toEqual([]);
expect(result.config.models?.providers?.["xiaomi-token-plan"]?.baseUrl).toBe("");
expect(result.config.models?.providers?.[provider]?.timeoutSeconds).toBe(600);
expect(result.config.models?.providers?.[provider]?.models).toEqual([]);
expect(result.config.models?.providers?.[provider]?.baseUrl).toBe("");
}
});

View file

@ -497,6 +497,7 @@ const BUILT_IN_MODEL_PROVIDER_OVERLAY_IDS = new Set([
"stepfun-plan",
"synthetic",
"tencent-tokenhub",
"tencent-tokenplan",
"together",
"venice",
"vercel-ai-gateway",

View file

@ -80,6 +80,7 @@ const BLOCKED_PROVIDER_AUTH_WORKSPACE_DOTENV_KEYS = [
"TAVILY_API_KEY",
"TOGETHER_API_KEY",
"TOKENHUB_API_KEY",
"TOKENPLAN_API_KEY",
"VENICE_API_KEY",
"VLLM_API_KEY",
"VOLCANO_ENGINE_API_KEY",

View file

@ -1823,6 +1823,7 @@ describe("official external plugin catalog", () => {
STEPFUN_API_KEY: "stepfun-key",
FIREWORKS_API_KEY: "fireworks-key",
TOKENHUB_API_KEY: "tokenhub-key",
TOKENPLAN_API_KEY: "tokenplan-key",
VENICE_API_KEY: "venice-key",
AI_GATEWAY_API_KEY: "gateway-key",
ZAI_API_KEY: "zai-key",
@ -1850,6 +1851,31 @@ describe("official external plugin catalog", () => {
expect(resolveOfficialExternalProviderPluginIdsForEnv({ GROQ_API_KEY: " " })).toEqual([]);
});
it("keeps Tencent auth choices available through the cold-install auth catalog", () => {
const tencent = expectCatalogEntry("tencent");
const tokenHub = tencent.openclaw?.providers?.find(
(provider) => provider.id === "tencent-tokenhub",
);
const tokenPlan = tencent.openclaw?.providers?.find(
(provider) => provider.id === "tencent-tokenplan",
);
expect(tokenHub?.envVars).toEqual(["TOKENHUB_API_KEY"]);
expect(tokenHub?.authChoices).toEqual([
expect.objectContaining({
choiceId: "tokenhub-api-key",
optionKey: "tokenhubApiKey",
cliFlag: "--tokenhub-api-key",
}),
]);
expect(tokenPlan?.envVars).toEqual(["TOKENPLAN_API_KEY"]);
expect(tokenPlan?.authChoices?.[0]).toMatchObject({
choiceId: "tokenplan-api-key",
optionKey: "tokenplanApiKey",
cliFlag: "--tokenplan-api-key",
});
});
it("keeps Groq available through the cold-install auth catalog", () => {
const groq = expectCatalogEntry("groq");
const authChoice = groq.openclaw?.providers?.find((provider) => provider.id === "groq")

View file

@ -0,0 +1,38 @@
// Covers Tencent setup config migration registration in the plugin setup registry.
import path from "node:path";
import { describe, expect, test } from "vitest";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { runPluginSetupConfigMigrations } from "./setup-registry.js";
describe("Tencent setup config migration", () => {
test("repairs TokenHub model defaults through setup registry", () => {
const result = runPluginSetupConfigMigrations({
env: {
...process.env,
OPENCLAW_BUNDLED_PLUGINS_DIR: path.resolve("extensions"),
},
config: {
agents: {
defaults: {
model: { primary: "tencent-tokenhub/hy3-preview" },
models: {
"tencent-tokenhub/hy3-preview": {},
},
},
},
} as OpenClawConfig,
});
expect(result.changes).toEqual([
"Updated Tencent TokenHub agent model defaults to include tencent-tokenhub/hy3 and tencent-tokenhub/hy3-preview.",
"Changed Tencent TokenHub primary default from tencent-tokenhub/hy3-preview to tencent-tokenhub/hy3.",
]);
expect(result.config.agents?.defaults?.model).toEqual({
primary: "tencent-tokenhub/hy3",
});
expect(Object.keys(result.config.agents?.defaults?.models ?? {}).toSorted()).toEqual([
"tencent-tokenhub/hy3",
"tencent-tokenhub/hy3-preview",
]);
});
});

View file

@ -105,4 +105,19 @@ describe("plugin npm runtime build planning", () => {
expect(plan.packageFiles).toContain("dist/**");
}
});
it("builds Tencent setup metadata for installed-package migrations", () => {
const plan = expectPluginNpmRuntimeBuildPlan(
resolvePluginNpmRuntimeBuildPlan({
repoRoot,
packageDir: path.join(repoRoot, "extensions", "tencent"),
}),
);
expect(plan.entry["setup-api"]).toBe(
path.join(repoRoot, "extensions", "tencent", "setup-api.ts"),
);
expect(plan.runtimeSetupEntry).toBe("./dist/setup-api.js");
expect(plan.runtimeBuildOutputs).toContain("./dist/setup-api.js");
});
});