diff --git a/.github/labeler.yml b/.github/labeler.yml index c46dd3bdb29..8aa082e3f67 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -448,6 +448,11 @@ - changed-files: - any-glob-to-any-file: - "extensions/kimi-coding/**" +"extensions: longcat": + - changed-files: + - any-glob-to-any-file: + - "extensions/longcat/**" + - "docs/providers/longcat.md" "extensions: minimax": - changed-files: - any-glob-to-any-file: diff --git a/docs/docs.json b/docs/docs.json index b46e7efe456..1f261af5965 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -1481,6 +1481,7 @@ "providers/kilocode", "providers/litellm", "providers/lmstudio", + "providers/longcat", "providers/minimax", "providers/mistral", "providers/moonshot", diff --git a/docs/docs_map.md b/docs/docs_map.md index ed281802f19..dc0d21c02e7 100644 --- a/docs/docs_map.md +++ b/docs/docs_map.md @@ -6261,6 +6261,24 @@ Do not edit it by hand; run `pnpm docs:map:gen`. - H2: Distribution - H2: Surface +## plugins/reference/logbook.md + +- Route: /plugins/reference/logbook +- Headings: + - H1: Logbook plugin + - H2: Distribution + - H2: Surface + - H2: Related docs + +## plugins/reference/longcat.md + +- Route: /plugins/reference/longcat +- Headings: + - H1: LongCat plugin + - H2: Distribution + - H2: Surface + - H2: Related docs + ## plugins/reference/matrix.md - Route: /plugins/reference/matrix @@ -7564,6 +7582,19 @@ Do not edit it by hand; run `pnpm docs:map:gen`. - H3: Authentication errors (HTTP 401) - H2: Related +## providers/longcat.md + +- Route: /providers/longcat +- Headings: + - H2: Install plugin + - H2: Getting started + - H3: Non-interactive setup + - H2: Reasoning behavior + - H2: Pricing + - H2: Self-hosted LongCat-2.0 + - H2: Troubleshooting + - H2: Related + ## providers/minimax.md - Route: /providers/minimax diff --git a/docs/plugins/plugin-inventory.md b/docs/plugins/plugin-inventory.md index 7380f8da729..bd9352fd2f0 100644 --- a/docs/plugins/plugin-inventory.md +++ b/docs/plugins/plugin-inventory.md @@ -51,7 +51,7 @@ Each entry lists the package, distribution route, and description. ## Core npm package -60 plugins +61 plugins - **[admin-http-rpc](/plugins/reference/admin-http-rpc)** (`@openclaw/admin-http-rpc`) - included in OpenClaw. OpenClaw admin HTTP RPC endpoint. @@ -105,6 +105,8 @@ Each entry lists the package, distribution route, and description. - **[lmstudio](/plugins/reference/lmstudio)** (`@openclaw/lmstudio-provider`) - included in OpenClaw. Adds LM Studio model provider support to OpenClaw. +- **[logbook](/plugins/reference/logbook)** (`@openclaw/logbook`) - included in OpenClaw. Automatic work journal: captures periodic screen snapshots from a paired node and turns them into a reviewable timeline of your day. + - **[memory-core](/plugins/reference/memory-core)** (`@openclaw/memory-core`) - included in OpenClaw. Adds agent-callable tools. - **[memory-wiki](/plugins/reference/memory-wiki)** (`@openclaw/memory-wiki`) - included in OpenClaw. Persistent wiki compiler and Obsidian-friendly knowledge vault for OpenClaw. @@ -175,7 +177,7 @@ Each entry lists the package, distribution route, and description. ## Official external packages -68 plugins +69 plugins - **[acpx](/plugins/reference/acpx)** (`@openclaw/acpx`) - npm; ClawHub. OpenClaw ACP runtime backend with plugin-owned session and transport management. @@ -247,6 +249,8 @@ Each entry lists the package, distribution route, and description. - **[lobster](/plugins/reference/lobster)** (`@openclaw/lobster`) - npm; ClawHub. Lobster workflow tool plugin for typed pipelines and resumable approvals. +- **[longcat](/plugins/reference/longcat)** (`@openclaw/longcat-provider`) - npm; ClawHub: `clawhub:@openclaw/longcat-provider`. OpenClaw LongCat provider plugin. + - **[matrix](/plugins/reference/matrix)** (`@openclaw/matrix`) - ClawHub: `clawhub:@openclaw/matrix`; npm. OpenClaw Matrix channel plugin for rooms and direct messages. - **[mattermost](/plugins/reference/mattermost)** (`@openclaw/mattermost`) - npm; ClawHub: `clawhub:@openclaw/mattermost`. Adds the Mattermost channel surface for sending and receiving OpenClaw messages. diff --git a/docs/plugins/reference.md b/docs/plugins/reference.md index 18e50dba55c..2a587a3c919 100644 --- a/docs/plugins/reference.md +++ b/docs/plugins/reference.md @@ -15,5 +15,5 @@ This page is generated from `extensions/*/package.json` and pnpm plugins:inventory:gen ``` -Use [Plugin inventory](/plugins/plugin-inventory) to browse all 130 +Use [Plugin inventory](/plugins/plugin-inventory) to browse all 132 generated plugin reference pages by distribution, package, and description. diff --git a/docs/plugins/reference/logbook.md b/docs/plugins/reference/logbook.md new file mode 100644 index 00000000000..6d3e3ce4474 --- /dev/null +++ b/docs/plugins/reference/logbook.md @@ -0,0 +1,23 @@ +--- +summary: "Automatic work journal: captures periodic screen snapshots from a paired node and turns them into a reviewable timeline of your day." +read_when: + - You are installing, configuring, or auditing the logbook plugin +title: "Logbook plugin" +--- + +# Logbook plugin + +Automatic work journal: captures periodic screen snapshots from a paired node and turns them into a reviewable timeline of your day. + +## Distribution + +- Package: `@openclaw/logbook` +- Install route: included in OpenClaw + +## Surface + +plugin + +## Related docs + +- [logbook](/plugins/logbook) diff --git a/docs/plugins/reference/longcat.md b/docs/plugins/reference/longcat.md new file mode 100644 index 00000000000..fe8a0c06fd3 --- /dev/null +++ b/docs/plugins/reference/longcat.md @@ -0,0 +1,23 @@ +--- +summary: "OpenClaw LongCat provider plugin." +read_when: + - You are installing, configuring, or auditing the longcat plugin +title: "LongCat plugin" +--- + +# LongCat plugin + +OpenClaw LongCat provider plugin. + +## Distribution + +- Package: `@openclaw/longcat-provider` +- Install route: npm; ClawHub: `clawhub:@openclaw/longcat-provider` + +## Surface + +providers: longcat + +## Related docs + +- [longcat](/providers/longcat) diff --git a/docs/providers/index.md b/docs/providers/index.md index 01b676b095b..9fc1e247387 100644 --- a/docs/providers/index.md +++ b/docs/providers/index.md @@ -52,6 +52,7 @@ Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/Mattermost (plugi - [Kilocode](/providers/kilocode) - [LiteLLM (unified gateway)](/providers/litellm) - [LM Studio (local models)](/providers/lmstudio) +- [LongCat](/providers/longcat) - [MiniMax](/providers/minimax) - [Mistral](/providers/mistral) - [Moonshot AI (Kimi + Kimi Coding)](/providers/moonshot) diff --git a/docs/providers/longcat.md b/docs/providers/longcat.md new file mode 100644 index 00000000000..d15b2f823dd --- /dev/null +++ b/docs/providers/longcat.md @@ -0,0 +1,129 @@ +--- +summary: "LongCat API setup for LongCat-2.0" +title: "LongCat" +read_when: + - You want to use LongCat-2.0 with OpenClaw + - You need the LongCat API key or model limits +--- + +[LongCat](https://longcat.ai) provides a hosted API for LongCat-2.0, a +reasoning model built for coding and agentic workloads. OpenClaw provides the +official `longcat` plugin for LongCat's OpenAI-compatible endpoint. + +| Property | Value | +| ---------- | ---------------------------------- | +| Provider | `longcat` | +| Auth | `LONGCAT_API_KEY` | +| API | OpenAI-compatible Chat Completions | +| Base URL | `https://api.longcat.chat/openai` | +| Model | `longcat/LongCat-2.0` | +| Context | 1,048,576 tokens | +| Max output | 131,072 tokens | +| Input | Text | + +## Install plugin + +Install the official package, then restart Gateway: + +```bash +openclaw plugins install @openclaw/longcat-provider +openclaw gateway restart +``` + +## Getting started + + + + Sign in to the [LongCat API Platform](https://longcat.chat/platform/) and + create a key on the [API Keys](https://longcat.chat/platform/api_keys) + page. + + + ```bash + openclaw onboard --auth-choice longcat-api-key + ``` + + + ```bash + openclaw models list --provider longcat + ``` + + + +Onboarding adds the hosted catalog and selects `longcat/LongCat-2.0` when no +primary model is already configured. + +### Non-interactive setup + +```bash +openclaw onboard --non-interactive \ + --mode local \ + --auth-choice longcat-api-key \ + --longcat-api-key "$LONGCAT_API_KEY" +``` + +## Reasoning behavior + +LongCat exposes binary thinking control. OpenClaw maps enabled thinking levels +to `thinking: { type: "enabled" }` and `/think off` to +`thinking: { type: "disabled" }`. LongCat does not currently document +`reasoning_effort`, so OpenClaw does not send it. + +LongCat returns reasoning in `reasoning_content`. OpenClaw preserves that field +when replaying assistant tool-call turns so multi-turn agent sessions retain +the provider's expected message shape. + +## Pricing + +The built-in catalog uses LongCat's pay-as-you-go list prices in USD per million +tokens: $0.75 uncached input, $0.015 cached input, and $2.95 output. LongCat may +offer temporary discounts; the [pricing page](https://longcat.chat/platform/docs/Pricing/LongCat-2.0.html) +and your billing records are authoritative. + +## Self-hosted LongCat-2.0 + +The `longcat` provider targets LongCat's hosted API. For the open weights on +[Hugging Face](https://huggingface.co/meituan-longcat/LongCat-2.0), serve the +model through an OpenAI-compatible runtime and use OpenClaw's existing +[vLLM](/providers/vllm) or [SGLang](/providers/sglang) provider instead. + +Keep the runtime's exact model identifier in the self-hosted provider catalog; +do not route a local deployment through `longcat/LongCat-2.0`. + +## Troubleshooting + + + + Daemon-managed Gateway processes do not inherit every interactive shell + variable. Put `LONGCAT_API_KEY` in `~/.openclaw/.env`, configure it through + onboarding, or use an approved secret reference. + + + + `402` means the account has insufficient token quota. `429` means the API + key hit a rate limit. Check [LongCat usage](https://longcat.chat/platform/usage) + and retry rate-limited requests after the provider's backoff window. + + + + Run `openclaw plugins list` and confirm the `longcat` plugin is + enabled, then run `openclaw models list --provider longcat`. + + + +## Related + + + + Provider configuration, model refs, and failover behavior. + + + Hosted API endpoints, authentication, limits, and examples. + + + Architecture, deployment guidance, and model details. + + + Store provider credentials without embedding plaintext in config. + + diff --git a/extensions/longcat/README.md b/extensions/longcat/README.md new file mode 100644 index 00000000000..4ff5ad656d3 --- /dev/null +++ b/extensions/longcat/README.md @@ -0,0 +1,13 @@ +# OpenClaw LongCat Provider + +Official OpenClaw provider plugin for LongCat-2.0 through LongCat's hosted +OpenAI-compatible API. + +Install from OpenClaw: + +```bash +openclaw plugins install @openclaw/longcat-provider +openclaw gateway restart +``` + +Configure `LONGCAT_API_KEY`, then select `longcat/LongCat-2.0`. diff --git a/extensions/longcat/api.ts b/extensions/longcat/api.ts new file mode 100644 index 00000000000..2ebcccc6c56 --- /dev/null +++ b/extensions/longcat/api.ts @@ -0,0 +1,10 @@ +// LongCat API module exposes the plugin public contract. +export { + LONGCAT_BASE_URL, + LONGCAT_DEFAULT_MODEL_ID, + LONGCAT_DEFAULT_MODEL_REF, + LONGCAT_MODEL_CATALOG, +} from "./models.js"; +export { applyLongCatConfig } from "./onboard.js"; +export { buildLongCatProvider } from "./provider-catalog.js"; +export { createLongCatThinkingWrapper } from "./stream.js"; diff --git a/extensions/longcat/index.test.ts b/extensions/longcat/index.test.ts new file mode 100644 index 00000000000..6565317a5fc --- /dev/null +++ b/extensions/longcat/index.test.ts @@ -0,0 +1,183 @@ +// LongCat tests cover provider registration, onboarding, and wire compatibility. +import { readFileSync } from "node:fs"; +import type { StreamFn } from "openclaw/plugin-sdk/agent-core"; +import { + createAssistantMessageEventStream, + type Context, + type Model, +} from "openclaw/plugin-sdk/llm"; +import { registerSingleProviderPlugin } from "openclaw/plugin-sdk/plugin-test-runtime"; +import { resolveAgentModelPrimaryValue } from "openclaw/plugin-sdk/provider-onboard"; +import { buildOpenAICompletionsParams } from "openclaw/plugin-sdk/provider-transport-runtime"; +import { describe, expect, it } from "vitest"; +import plugin from "./index.js"; +import { LONGCAT_DEFAULT_MODEL_REF } from "./models.js"; +import { applyLongCatConfig } from "./onboard.js"; +import { buildLongCatProvider } from "./provider-catalog.js"; +import { createLongCatThinkingWrapper } from "./stream.js"; + +function readManifest() { + return JSON.parse(readFileSync(new URL("./openclaw.plugin.json", import.meta.url), "utf8")) as { + providerAuthChoices?: Array<{ choiceId?: string; optionKey?: string; cliFlag?: string }>; + setup?: { providers?: Array<{ id?: string; envVars?: string[] }> }; + }; +} + +function requireLongCatModel(): Model<"openai-completions"> { + const model = buildLongCatProvider().models?.[0]; + if (!model) { + throw new Error("LongCat catalog did not provide a model"); + } + return { + ...model, + api: "openai-completions", + baseUrl: "https://api.longcat.chat/openai", + provider: "longcat", + input: ["text"], + cost: { ...model.cost }, + } as Model<"openai-completions">; +} + +describe("LongCat provider plugin", () => { + it("registers the manifest-owned API key onboarding flow", async () => { + const provider = await registerSingleProviderPlugin(plugin); + + expect(provider).toMatchObject({ + id: "longcat", + aliases: ["meituan-longcat"], + envVars: ["LONGCAT_API_KEY"], + }); + expect(provider.auth[0]).toMatchObject({ + id: "api-key", + kind: "api_key", + wizard: { choiceId: "longcat-api-key" }, + }); + expect(readManifest().providerAuthChoices).toEqual([ + expect.objectContaining({ + choiceId: "longcat-api-key", + optionKey: "longcatApiKey", + cliFlag: "--longcat-api-key", + }), + ]); + expect(readManifest().setup?.providers).toEqual([ + { id: "longcat", envVars: ["LONGCAT_API_KEY"] }, + ]); + }); + + it("exposes the hosted LongCat-2.0 catalog", () => { + expect(buildLongCatProvider()).toMatchObject({ + baseUrl: "https://api.longcat.chat/openai", + api: "openai-completions", + models: [ + expect.objectContaining({ + id: "LongCat-2.0", + reasoning: true, + contextWindow: 1_048_576, + maxTokens: 131_072, + compat: { + supportsStore: false, + supportsDeveloperRole: false, + supportsReasoningEffort: false, + supportsUsageInStreaming: false, + supportsStrictMode: false, + maxTokensField: "max_tokens", + requiresReasoningContentOnAssistantMessages: true, + thinkingFormat: "deepseek", + }, + }), + ], + }); + }); + + it("applies the LongCat catalog without replacing an existing primary model", () => { + const result = applyLongCatConfig({ + agents: { defaults: { model: { primary: "openai/gpt-5.5" } } }, + }); + + expect(resolveAgentModelPrimaryValue(result.agents?.defaults?.model)).toBe("openai/gpt-5.5"); + expect(result.agents?.defaults?.models?.[LONGCAT_DEFAULT_MODEL_REF]).toEqual({ + alias: "LongCat 2.0", + }); + }); + + it("uses LongCat thinking and replay fields without reasoning_effort", () => { + const model = requireLongCatModel(); + const context = { + systemPrompt: "system", + messages: [ + { role: "user", content: "read it", timestamp: 1 }, + { + role: "assistant", + api: "openai-completions", + provider: "longcat", + model: "LongCat-2.0", + content: [ + { + type: "thinking", + thinking: "use the read tool", + thinkingSignature: "reasoning_content", + }, + { type: "toolCall", id: "call_1", name: "read", arguments: {} }, + ], + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, + }, + stopReason: "toolUse", + timestamp: 2, + }, + ], + tools: [ + { + name: "read", + description: "Read data", + parameters: { type: "object", properties: {} }, + }, + ], + } as Context; + let payload: Record | undefined; + const baseStreamFn: StreamFn = (streamModel, streamContext, options) => { + const params = buildOpenAICompletionsParams( + streamModel as Model<"openai-completions">, + streamContext, + { maxTokens: 2048, reasoning: "high" } as never, + ) as Record; + options?.onPayload?.(params, streamModel); + const stream = createAssistantMessageEventStream(); + queueMicrotask(() => stream.end()); + return stream; + }; + const wrappedStreamFn = createLongCatThinkingWrapper(baseStreamFn, "high"); + + void wrappedStreamFn(model, context, { + onPayload: (nextPayload) => { + payload = nextPayload as Record; + }, + }); + if (!payload) { + throw new Error("LongCat payload was not captured"); + } + + expect(payload).toMatchObject({ + max_tokens: 2048, + thinking: { type: "enabled" }, + }); + expect(payload).not.toHaveProperty("max_completion_tokens"); + expect(payload).not.toHaveProperty("reasoning_effort"); + expect(payload).not.toHaveProperty("store"); + expect(payload).not.toHaveProperty("stream_options"); + expect(payload.messages).toEqual( + expect.arrayContaining([ + expect.objectContaining({ role: "system", content: "system" }), + expect.objectContaining({ + role: "assistant", + reasoning_content: "use the read tool", + }), + ]), + ); + }); +}); diff --git a/extensions/longcat/index.ts b/extensions/longcat/index.ts new file mode 100644 index 00000000000..6ea3b288ce5 --- /dev/null +++ b/extensions/longcat/index.ts @@ -0,0 +1,53 @@ +// LongCat plugin entrypoint registers its OpenClaw integration. +import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; +import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared"; +import { buildProviderToolCompatFamilyHooks } from "openclaw/plugin-sdk/provider-tools"; +import { LONGCAT_DEFAULT_MODEL_REF } from "./models.js"; +import { applyLongCatConfig } from "./onboard.js"; +import { buildLongCatProvider } from "./provider-catalog.js"; +import { createLongCatThinkingWrapper } from "./stream.js"; + +const PROVIDER_ID = "longcat"; + +export default defineSingleProviderPluginEntry({ + id: PROVIDER_ID, + name: "LongCat Provider", + description: "Official LongCat provider plugin", + provider: { + label: "LongCat", + docsPath: "/providers/longcat", + aliases: ["meituan-longcat"], + envVars: ["LONGCAT_API_KEY"], + auth: [ + { + methodId: "api-key", + label: "LongCat API key", + hint: "API key", + optionKey: "longcatApiKey", + flagName: "--longcat-api-key", + envVar: "LONGCAT_API_KEY", + promptMessage: "Enter LongCat API key", + defaultModel: LONGCAT_DEFAULT_MODEL_REF, + applyConfig: (cfg) => applyLongCatConfig(cfg), + noteTitle: "LongCat", + noteMessage: "Manage API keys at https://longcat.chat/platform/api_keys", + wizard: { + choiceId: "longcat-api-key", + choiceLabel: "LongCat API key", + groupId: "longcat", + groupLabel: "LongCat", + groupHint: "API key", + }, + }, + ], + catalog: { + buildProvider: buildLongCatProvider, + }, + ...buildProviderReplayFamilyHooks({ + family: "openai-compatible", + dropReasoningFromHistory: false, + }), + ...buildProviderToolCompatFamilyHooks("openai"), + wrapStreamFn: (ctx) => createLongCatThinkingWrapper(ctx.streamFn, ctx.thinkingLevel), + }, +}); diff --git a/extensions/longcat/models.ts b/extensions/longcat/models.ts new file mode 100644 index 00000000000..ba078199b71 --- /dev/null +++ b/extensions/longcat/models.ts @@ -0,0 +1,14 @@ +// LongCat plugin module implements models behavior. +import { buildManifestModelProviderConfig } from "openclaw/plugin-sdk/provider-catalog-shared"; +import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared"; +import manifest from "./openclaw.plugin.json" with { type: "json" }; + +const LONGCAT_MANIFEST_PROVIDER = buildManifestModelProviderConfig({ + providerId: "longcat", + catalog: manifest.modelCatalog.providers.longcat, +}); + +export const LONGCAT_BASE_URL = LONGCAT_MANIFEST_PROVIDER.baseUrl; +export const LONGCAT_MODEL_CATALOG: ModelDefinitionConfig[] = LONGCAT_MANIFEST_PROVIDER.models; +export const LONGCAT_DEFAULT_MODEL_ID = "LongCat-2.0"; +export const LONGCAT_DEFAULT_MODEL_REF = `longcat/${LONGCAT_DEFAULT_MODEL_ID}`; diff --git a/extensions/longcat/npm-shrinkwrap.json b/extensions/longcat/npm-shrinkwrap.json new file mode 100644 index 00000000000..b870e52704e --- /dev/null +++ b/extensions/longcat/npm-shrinkwrap.json @@ -0,0 +1,12 @@ +{ + "name": "@openclaw/longcat-provider", + "version": "2026.6.11", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@openclaw/longcat-provider", + "version": "2026.6.11" + } + } +} diff --git a/extensions/longcat/onboard.ts b/extensions/longcat/onboard.ts new file mode 100644 index 00000000000..efc5434d034 --- /dev/null +++ b/extensions/longcat/onboard.ts @@ -0,0 +1,21 @@ +// LongCat setup module handles plugin onboarding behavior. +import { + createModelCatalogPresetAppliers, + type OpenClawConfig, +} from "openclaw/plugin-sdk/provider-onboard"; +import { LONGCAT_BASE_URL, LONGCAT_DEFAULT_MODEL_REF, LONGCAT_MODEL_CATALOG } from "./models.js"; + +const longCatPresetAppliers = createModelCatalogPresetAppliers({ + primaryModelRef: LONGCAT_DEFAULT_MODEL_REF, + resolveParams: (_cfg: OpenClawConfig) => ({ + providerId: "longcat", + api: "openai-completions", + baseUrl: LONGCAT_BASE_URL, + catalogModels: LONGCAT_MODEL_CATALOG, + aliases: [{ modelRef: LONGCAT_DEFAULT_MODEL_REF, alias: "LongCat 2.0" }], + }), +}); + +export function applyLongCatConfig(cfg: OpenClawConfig): OpenClawConfig { + return longCatPresetAppliers.applyConfig(cfg); +} diff --git a/extensions/longcat/openclaw.plugin.json b/extensions/longcat/openclaw.plugin.json new file mode 100644 index 00000000000..0bd7f1d439f --- /dev/null +++ b/extensions/longcat/openclaw.plugin.json @@ -0,0 +1,82 @@ +{ + "id": "longcat", + "name": "LongCat", + "description": "OpenClaw LongCat provider plugin.", + "activation": { + "onStartup": false + }, + "enabledByDefault": true, + "providers": ["longcat"], + "providerRequest": { + "providers": { + "longcat": { + "family": "longcat" + } + } + }, + "modelCatalog": { + "providers": { + "longcat": { + "baseUrl": "https://api.longcat.chat/openai", + "api": "openai-completions", + "models": [ + { + "id": "LongCat-2.0", + "name": "LongCat 2.0", + "reasoning": true, + "input": ["text"], + "contextWindow": 1048576, + "maxTokens": 131072, + "cost": { + "input": 0.75, + "output": 2.95, + "cacheRead": 0.015, + "cacheWrite": 0.75 + }, + "compat": { + "supportsStore": false, + "supportsDeveloperRole": false, + "supportsReasoningEffort": false, + "supportsUsageInStreaming": false, + "supportsStrictMode": false, + "maxTokensField": "max_tokens", + "requiresReasoningContentOnAssistantMessages": true, + "thinkingFormat": "deepseek" + } + } + ] + } + }, + "discovery": { + "longcat": "static" + } + }, + "setup": { + "providers": [ + { + "id": "longcat", + "envVars": ["LONGCAT_API_KEY"] + } + ] + }, + "providerAuthChoices": [ + { + "provider": "longcat", + "method": "api-key", + "choiceId": "longcat-api-key", + "choiceLabel": "LongCat API key", + "groupId": "longcat", + "groupLabel": "LongCat", + "groupHint": "API key", + "optionKey": "longcatApiKey", + "cliFlag": "--longcat-api-key", + "cliOption": "--longcat-api-key ", + "cliDescription": "LongCat API key" + } + ], + "configSchema": { + "type": "object", + "additionalProperties": false, + "properties": {} + } +} diff --git a/extensions/longcat/package.json b/extensions/longcat/package.json new file mode 100644 index 00000000000..be5fa7c84de --- /dev/null +++ b/extensions/longcat/package.json @@ -0,0 +1,35 @@ +{ + "name": "@openclaw/longcat-provider", + "version": "2026.6.11", + "description": "OpenClaw LongCat provider plugin.", + "repository": { + "type": "git", + "url": "https://github.com/openclaw/openclaw" + }, + "type": "module", + "devDependencies": { + "@openclaw/plugin-sdk": "workspace:*" + }, + "openclaw": { + "extensions": [ + "./index.ts" + ], + "install": { + "clawhubSpec": "clawhub:@openclaw/longcat-provider", + "npmSpec": "@openclaw/longcat-provider", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + }, + "compat": { + "pluginApi": ">=2026.6.11" + }, + "build": { + "openclawVersion": "2026.6.11", + "bundledDist": false + }, + "release": { + "publishToClawHub": true, + "publishToNpm": true + } + } +} diff --git a/extensions/longcat/provider-catalog.ts b/extensions/longcat/provider-catalog.ts new file mode 100644 index 00000000000..ccc5b192afd --- /dev/null +++ b/extensions/longcat/provider-catalog.ts @@ -0,0 +1,11 @@ +// LongCat provider module implements model/runtime integration. +import { buildManifestModelProviderConfig } from "openclaw/plugin-sdk/provider-catalog-shared"; +import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared"; +import manifest from "./openclaw.plugin.json" with { type: "json" }; + +export function buildLongCatProvider(): ModelProviderConfig { + return buildManifestModelProviderConfig({ + providerId: "longcat", + catalog: manifest.modelCatalog.providers.longcat, + }); +} diff --git a/extensions/longcat/stream.ts b/extensions/longcat/stream.ts new file mode 100644 index 00000000000..f782727a297 --- /dev/null +++ b/extensions/longcat/stream.ts @@ -0,0 +1,20 @@ +// LongCat stream helpers apply the provider's binary thinking control. +import type { ProviderWrapStreamFnContext } from "openclaw/plugin-sdk/plugin-entry"; +import { createPayloadPatchStreamWrapper } from "openclaw/plugin-sdk/provider-stream-shared"; + +export function createLongCatThinkingWrapper( + baseStreamFn: ProviderWrapStreamFnContext["streamFn"], + thinkingLevel: ProviderWrapStreamFnContext["thinkingLevel"], +): NonNullable { + return createPayloadPatchStreamWrapper( + baseStreamFn, + ({ payload }) => { + payload.thinking = { type: thinkingLevel === "off" ? "disabled" : "enabled" }; + delete payload.reasoning_effort; + }, + { + shouldPatch: ({ model }) => + model.api === "openai-completions" && model.provider === "longcat", + }, + ); +} diff --git a/extensions/longcat/tsconfig.json b/extensions/longcat/tsconfig.json new file mode 100644 index 00000000000..b8a85a99ac3 --- /dev/null +++ b/extensions/longcat/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/package.json b/package.json index fdf898e12c9..20749f7b1cb 100644 --- a/package.json +++ b/package.json @@ -112,6 +112,7 @@ "!dist/extensions/line/**", "!dist/extensions/llama-cpp/**", "!dist/extensions/lobster/**", + "!dist/extensions/longcat/**", "!dist/extensions/mattermost/**", "!dist/extensions/memory-lancedb/**", "!dist/extensions/matrix/**", diff --git a/packages/ai/src/providers/openai-completions.test.ts b/packages/ai/src/providers/openai-completions.test.ts index 5f25403bc46..60f73e4874b 100644 --- a/packages/ai/src/providers/openai-completions.test.ts +++ b/packages/ai/src/providers/openai-completions.test.ts @@ -24,8 +24,9 @@ const mockChunksRef: { chunks: OpenAICompatibleChatCompletionChunk[]; stream?: AsyncIterable; } = { chunks: [] }; -const mockOpenAIOptionsRef: { options: unknown[]; requests: unknown[] } = { +const mockOpenAIOptionsRef: { options: unknown[]; payloads: unknown[]; requests: unknown[] } = { options: [], + payloads: [], requests: [], }; @@ -37,7 +38,8 @@ vi.mock("openai", () => { chat = { completions: { - create: (_params: unknown, requestOptions: unknown) => { + create: (params: unknown, requestOptions: unknown) => { + mockOpenAIOptionsRef.payloads.push(params); mockOpenAIOptionsRef.requests.push(requestOptions); return { withResponse: async () => { @@ -70,6 +72,7 @@ import { streamOpenAICompletions, streamSimpleOpenAICompletions } from "./openai beforeEach(() => { mockChunksRef.chunks = []; mockChunksRef.stream = undefined; + mockOpenAIOptionsRef.payloads = []; mockOpenAIOptionsRef.requests = []; }); @@ -161,6 +164,30 @@ function createNeverYieldingStream(): AsyncIterable { + it("omits reasoning_effort when deepseek-format compatibility disables it", async () => { + mockChunksRef.chunks = [makeTextChunk("ok"), makeFinishChunk("stop")]; + const compatibleModel = { + ...reasoningModel, + provider: "longcat", + baseUrl: "https://api.longcat.chat/openai", + compat: { + thinkingFormat: "deepseek" as const, + supportsReasoningEffort: false, + }, + } satisfies Model<"openai-completions">; + + const stream = streamOpenAICompletions(compatibleModel, context, { + apiKey: "sk-test", + reasoningEffort: "high", + }); + await stream.result(); + + expect(mockOpenAIOptionsRef.payloads[0]).toMatchObject({ + thinking: { type: "enabled" }, + }); + expect(mockOpenAIOptionsRef.payloads[0]).not.toHaveProperty("reasoning_effort"); + }); + it("configures the OpenAI SDK client with the host-built model fetch", async () => { mockOpenAIOptionsRef.options = []; mockChunksRef.chunks = [makeTextChunk("ok"), makeFinishChunk("stop")]; diff --git a/packages/ai/src/providers/openai-completions.ts b/packages/ai/src/providers/openai-completions.ts index 4f19ba0236e..6a3c58c4a25 100644 --- a/packages/ai/src/providers/openai-completions.ts +++ b/packages/ai/src/providers/openai-completions.ts @@ -755,7 +755,7 @@ function buildParams( }; } else if (compat.thinkingFormat === "deepseek" && model.reasoning) { params.thinking = { type: options?.reasoningEffort ? "enabled" : "disabled" }; - if (options?.reasoningEffort) { + if (options?.reasoningEffort && compat.supportsReasoningEffort) { params.reasoning_effort = model.thinkingLevelMap?.[options.reasoningEffort] ?? options.reasoningEffort; } diff --git a/packages/model-catalog-core/src/model-catalog-normalize.test.ts b/packages/model-catalog-core/src/model-catalog-normalize.test.ts index 1ee6bcaece2..5c6247055b1 100644 --- a/packages/model-catalog-core/src/model-catalog-normalize.test.ts +++ b/packages/model-catalog-core/src/model-catalog-normalize.test.ts @@ -67,6 +67,7 @@ describe("model catalog normalization", () => { sendSessionIdHeader: false, supportsEagerToolInputStreaming: false, supportsLongCacheRetention: true, + requiresReasoningContentOnAssistantMessages: true, supportsStore: "yes", thinkingFormat: "together", unknownFlag: true, @@ -163,6 +164,7 @@ describe("model catalog normalization", () => { sendSessionIdHeader: false, supportsEagerToolInputStreaming: false, supportsLongCacheRetention: true, + requiresReasoningContentOnAssistantMessages: true, thinkingFormat: "together", }, status: "preview", diff --git a/packages/model-catalog-core/src/model-catalog-normalize.ts b/packages/model-catalog-core/src/model-catalog-normalize.ts index b2051413650..d4dbc29dd15 100644 --- a/packages/model-catalog-core/src/model-catalog-normalize.ts +++ b/packages/model-catalog-core/src/model-catalog-normalize.ts @@ -383,6 +383,7 @@ function normalizeModelCatalogCompat(value: unknown): ModelCatalogCompatConfig | "requiresToolResultName", "requiresAssistantAfterToolResult", "requiresThinkingAsText", + "requiresReasoningContentOnAssistantMessages", "zaiToolStream", "sendSessionAffinityHeaders", "sendSessionIdHeader", diff --git a/packages/model-catalog-core/src/model-catalog-types.ts b/packages/model-catalog-core/src/model-catalog-types.ts index b185d4e468d..c5e008d57a7 100644 --- a/packages/model-catalog-core/src/model-catalog-types.ts +++ b/packages/model-catalog-core/src/model-catalog-types.ts @@ -47,6 +47,7 @@ export type ModelCatalogCompatConfig = { requiresToolResultName?: boolean; requiresAssistantAfterToolResult?: boolean; requiresThinkingAsText?: boolean; + requiresReasoningContentOnAssistantMessages?: boolean; openRouterRouting?: ModelCatalogOpenRouterRouting; vercelGatewayRouting?: ModelCatalogVercelGatewayRouting; zaiToolStream?: boolean; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a7e2d19dba3..f9a140f8fec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -969,6 +969,12 @@ importers: extensions/lmstudio: {} + extensions/longcat: + devDependencies: + '@openclaw/plugin-sdk': + specifier: workspace:* + version: link:../../packages/plugin-sdk + extensions/lobster: dependencies: '@clawdbot/lobster': diff --git a/scripts/generate-plugin-inventory-doc.mjs b/scripts/generate-plugin-inventory-doc.mjs index 0e83ef0579c..7eb212f8d8b 100644 --- a/scripts/generate-plugin-inventory-doc.mjs +++ b/scripts/generate-plugin-inventory-doc.mjs @@ -131,6 +131,7 @@ function humanizeId(value) { ["litellm", "LiteLLM"], ["llm", "LLM"], ["lmstudio", "LM Studio"], + ["longcat", "LongCat"], ["mdns", "mDNS"], ["minimax", "MiniMax"], ["modelstudio", "Model Studio"], diff --git a/scripts/lib/official-external-provider-catalog.json b/scripts/lib/official-external-provider-catalog.json index db5dfacc879..50aa25ad6b8 100644 --- a/scripts/lib/official-external-provider-catalog.json +++ b/scripts/lib/official-external-provider-catalog.json @@ -589,6 +589,49 @@ } } }, + { + "name": "@openclaw/longcat-provider", + "description": "OpenClaw LongCat provider plugin", + "source": "official", + "kind": "provider", + "openclaw": { + "plugin": { + "id": "longcat", + "label": "LongCat" + }, + "providers": [ + { + "id": "longcat", + "aliases": ["meituan-longcat"], + "name": "LongCat", + "docs": "/providers/longcat", + "categories": ["cloud", "llm"], + "envVars": ["LONGCAT_API_KEY"], + "authChoices": [ + { + "method": "api-key", + "choiceId": "longcat-api-key", + "choiceLabel": "LongCat API key", + "groupId": "longcat", + "groupLabel": "LongCat", + "groupHint": "OpenAI-compatible LongCat API", + "optionKey": "longcatApiKey", + "cliFlag": "--longcat-api-key", + "cliOption": "--longcat-api-key ", + "cliDescription": "LongCat API key", + "onboardingScopes": ["text-inference"] + } + ] + } + ], + "install": { + "clawhubSpec": "clawhub:@openclaw/longcat-provider", + "npmSpec": "@openclaw/longcat-provider", + "defaultChoice": "npm", + "minHostVersion": ">=2026.6.8" + } + } + }, { "name": "@openclaw/groq-provider", "description": "OpenClaw Groq media-understanding provider.", diff --git a/src/plugins/official-external-plugin-catalog.test.ts b/src/plugins/official-external-plugin-catalog.test.ts index 3ffd7856823..00819f43de7 100644 --- a/src/plugins/official-external-plugin-catalog.test.ts +++ b/src/plugins/official-external-plugin-catalog.test.ts @@ -1632,6 +1632,7 @@ describe("official external plugin catalog", () => { ["deepinfra", "@openclaw/deepinfra-provider"], ["deepseek", "@openclaw/deepseek-provider"], ["groq", "@openclaw/groq-provider"], + ["longcat", "@openclaw/longcat-provider"], ["kilocode", "@openclaw/kilocode-provider"], ["kimi", "@openclaw/kimi-provider"], ["qianfan", "@openclaw/qianfan-provider"], @@ -1752,6 +1753,19 @@ describe("official external plugin catalog", () => { }); }); + it("lists LongCat as an official external provider", () => { + const longcat = expectCatalogEntry("longcat"); + + expect(resolveOfficialExternalPluginId(longcat)).toBe("longcat"); + expect(getOfficialExternalPluginCatalogEntry("meituan-longcat")).toBe(longcat); + expect(resolveOfficialExternalPluginInstall(longcat)).toEqual({ + clawhubSpec: "clawhub:@openclaw/longcat-provider", + npmSpec: "@openclaw/longcat-provider", + defaultChoice: "npm", + minHostVersion: ">=2026.6.8", + }); + }); + it("resolves external provider aliases beyond the primary provider id", () => { const qwen = expectCatalogEntry("qwen"); @@ -1814,6 +1828,7 @@ describe("official external plugin catalog", () => { DEEPINFRA_API_KEY: "deepinfra-key", DEEPSEEK_API_KEY: "deepseek-key", GROQ_API_KEY: "groq-key", + LONGCAT_API_KEY: "longcat-key", KILOCODE_API_KEY: "kilocode-key", KIMICODE_API_KEY: "kimi-key", KIMI_API_KEY: "moonshot-kimi-key", @@ -1839,6 +1854,7 @@ describe("official external plugin catalog", () => { "groq", "kilocode", "kimi", + "longcat", "moonshot", "qianfan", "qwen", @@ -1849,6 +1865,7 @@ describe("official external plugin catalog", () => { "zai", ]); expect(resolveOfficialExternalProviderPluginIdsForEnv({ GROQ_API_KEY: " " })).toEqual([]); + expect(resolveOfficialExternalProviderPluginIdsForEnv({ LONGCAT_API_KEY: " " })).toEqual([]); }); it("keeps Tencent auth choices available through the cold-install auth catalog", () => {