From 85d5d9451977890646d1e5d24b31d0b2da2bdbdb Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Wed, 17 Jun 2026 15:53:11 +0800 Subject: [PATCH] feat(cohere): add provider plugin --- docs/.i18n/glossary.zh-CN.json | 4 + docs/concepts/model-providers.md | 1 + docs/docs.json | 1 + docs/providers/cohere.md | 63 +++++++++++++++ docs/providers/index.md | 1 + docs/providers/models.md | 1 + extensions/cohere/index.test.ts | 108 +++++++++++++++++++++++++ extensions/cohere/index.ts | 37 +++++++++ extensions/cohere/models.ts | 27 +++++++ extensions/cohere/onboard.test.ts | 49 +++++++++++ extensions/cohere/onboard.ts | 27 +++++++ extensions/cohere/openclaw.plugin.json | 67 +++++++++++++++ extensions/cohere/package.json | 15 ++++ extensions/cohere/provider-catalog.ts | 10 +++ extensions/cohere/stream.ts | 17 ++++ extensions/cohere/tsconfig.json | 16 ++++ pnpm-lock.yaml | 6 ++ 17 files changed, 450 insertions(+) create mode 100644 docs/providers/cohere.md create mode 100644 extensions/cohere/index.test.ts create mode 100644 extensions/cohere/index.ts create mode 100644 extensions/cohere/models.ts create mode 100644 extensions/cohere/onboard.test.ts create mode 100644 extensions/cohere/onboard.ts create mode 100644 extensions/cohere/openclaw.plugin.json create mode 100644 extensions/cohere/package.json create mode 100644 extensions/cohere/provider-catalog.ts create mode 100644 extensions/cohere/stream.ts create mode 100644 extensions/cohere/tsconfig.json diff --git a/docs/.i18n/glossary.zh-CN.json b/docs/.i18n/glossary.zh-CN.json index 32127016fead..41c528f68141 100644 --- a/docs/.i18n/glossary.zh-CN.json +++ b/docs/.i18n/glossary.zh-CN.json @@ -1178,5 +1178,9 @@ { "source": "Z.AI (GLM)", "target": "Z.AI (GLM)" + }, + { + "source": "Cohere", + "target": "Cohere" } ] diff --git a/docs/concepts/model-providers.md b/docs/concepts/model-providers.md index 81ac21f61455..98ad2e29d609 100644 --- a/docs/concepts/model-providers.md +++ b/docs/concepts/model-providers.md @@ -296,6 +296,7 @@ See [/providers/kilocode](/providers/kilocode) for setup details. | --------------------------------------- | -------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------- | | BytePlus | `byteplus` / `byteplus-plan` | `BYTEPLUS_API_KEY` | `byteplus-plan/ark-code-latest` | | Cerebras | `cerebras` | `CEREBRAS_API_KEY` | `cerebras/zai-glm-4.7` | +| Cohere | `cohere` | `COHERE_API_KEY` | `cohere/command-a-03-2025` | | Cloudflare AI Gateway | `cloudflare-ai-gateway` | `CLOUDFLARE_AI_GATEWAY_API_KEY` | - | | DeepInfra | `deepinfra` | `DEEPINFRA_API_KEY` | `deepinfra/deepseek-ai/DeepSeek-V4-Flash` | | DeepSeek | `deepseek` | `DEEPSEEK_API_KEY` | `deepseek/deepseek-v4-flash` | diff --git a/docs/docs.json b/docs/docs.json index 810e4a6c5ca0..8ce420cafe8f 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -1417,6 +1417,7 @@ "providers/azure-speech", "providers/cerebras", "providers/chutes", + "providers/cohere", "providers/claude-max-api-proxy", "providers/cloudflare-ai-gateway", "providers/comfy", diff --git a/docs/providers/cohere.md b/docs/providers/cohere.md new file mode 100644 index 000000000000..9bb4af98baca --- /dev/null +++ b/docs/providers/cohere.md @@ -0,0 +1,63 @@ +--- +summary: "Cohere setup (auth + model selection)" +title: "Cohere" +read_when: + - You want to use Cohere with OpenClaw + - You need the Cohere API key env var or CLI auth choice +--- + +[Cohere](https://cohere.com) provides OpenAI-compatible inference through its Compatibility API. OpenClaw includes a bundled Cohere provider plugin with the Command A model catalog. + +| Property | Value | +| --------------- | ---------------------------------------- | +| Provider id | `cohere` | +| Plugin | bundled, `enabledByDefault: true` | +| Auth env var | `COHERE_API_KEY` | +| Onboarding flag | `--auth-choice cohere-api-key` | +| Direct CLI flag | `--cohere-api-key ` | +| API | OpenAI-compatible (`openai-completions`) | +| Base URL | `https://api.cohere.ai/compatibility/v1` | +| Default model | `cohere/command-a-03-2025` | + +## Get started + +1. Create a Cohere API key. +2. Run onboarding: + +```bash +openclaw onboard --non-interactive \ + --auth-choice cohere-api-key \ + --cohere-api-key "$COHERE_API_KEY" +``` + +3. Confirm the catalog is available: + +```bash +openclaw models list --provider cohere +``` + +The default model is set only when no primary model is already configured. + +## Environment-only setup + +Make `COHERE_API_KEY` available to the Gateway process, then select the bundled model: + +```json5 +{ + agents: { + defaults: { + model: { primary: "cohere/command-a-03-2025" }, + }, + }, +} +``` + + +If the Gateway runs as a daemon or in Docker, configure `COHERE_API_KEY` for that service. Exporting it only in an interactive shell does not make it available to an already-running Gateway. + + +## Related + +- [Model providers](/concepts/model-providers) +- [Models CLI](/cli/models) +- [Provider directory](/providers) diff --git a/docs/providers/index.md b/docs/providers/index.md index 02bcafa299df..086764443e5a 100644 --- a/docs/providers/index.md +++ b/docs/providers/index.md @@ -33,6 +33,7 @@ Looking for chat channel docs (WhatsApp/Telegram/Discord/Slack/Mattermost (plugi - [BytePlus (International)](/concepts/model-providers#byteplus-international) - [Cerebras](/providers/cerebras) - [Chutes](/providers/chutes) +- [Cohere](/providers/cohere) - [Cloudflare AI Gateway](/providers/cloudflare-ai-gateway) - [ComfyUI](/providers/comfy) - [DeepSeek](/providers/deepseek) diff --git a/docs/providers/models.md b/docs/providers/models.md index 2b0eeff9099e..d569bcf7b078 100644 --- a/docs/providers/models.md +++ b/docs/providers/models.md @@ -27,6 +27,7 @@ model as `provider/model`. - [Anthropic (API + Claude CLI)](/providers/anthropic) - [BytePlus (International)](/concepts/model-providers#byteplus-international) - [Chutes](/providers/chutes) +- [Cohere](/providers/cohere) - [ComfyUI](/providers/comfy) - [Cloudflare AI Gateway](/providers/cloudflare-ai-gateway) - [DeepInfra](/providers/deepinfra) diff --git a/extensions/cohere/index.test.ts b/extensions/cohere/index.test.ts new file mode 100644 index 000000000000..b3e773ae6e8f --- /dev/null +++ b/extensions/cohere/index.test.ts @@ -0,0 +1,108 @@ +import { readFileSync } from "node:fs"; +import type { StreamFn } from "openclaw/plugin-sdk/agent-core"; +import type { Context, Model } from "openclaw/plugin-sdk/llm"; +import { registerSingleProviderPlugin } from "openclaw/plugin-sdk/plugin-test-runtime"; +import { buildOpenAICompletionsParams } from "openclaw/plugin-sdk/provider-transport-runtime"; +import { describe, expect, it } from "vitest"; +import plugin from "./index.js"; +import { buildCohereProvider } from "./provider-catalog.js"; +import { createCohereCompletionsWrapper } 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 requireCohereModel(): Model<"openai-completions"> { + const model = buildCohereProvider().models?.[0]; + if (!model) { + throw new Error("Cohere catalog did not provide a model"); + } + return model as Model<"openai-completions">; +} + +function captureCoherePayload(context: Context): Record { + let captured: Record | undefined; + const baseStreamFn: StreamFn = (model, streamContext, options) => { + const payload = buildOpenAICompletionsParams( + model as Model<"openai-completions">, + streamContext, + { maxTokens: 2048 } as never, + ); + options?.onPayload?.(payload, model); + captured = payload; + return {} as ReturnType; + }; + + void createCohereCompletionsWrapper(baseStreamFn)(requireCohereModel(), context, {}); + if (!captured) { + throw new Error("Cohere payload was not captured"); + } + return captured; +} + +describe("Cohere provider plugin", () => { + it("registers the manifest-owned API key onboarding flow", async () => { + const provider = await registerSingleProviderPlugin(plugin); + + expect(provider.auth.map((method) => method.wizard?.choiceId)).toEqual(["cohere-api-key"]); + expect(provider).toMatchObject({ + id: "cohere", + envVars: ["COHERE_API_KEY"], + }); + expect(provider.auth[0]).toMatchObject({ + id: "api-key", + kind: "api_key", + wizard: { choiceId: "cohere-api-key" }, + }); + expect(readManifest().providerAuthChoices).toEqual([ + expect.objectContaining({ + choiceId: "cohere-api-key", + optionKey: "cohereApiKey", + cliFlag: "--cohere-api-key", + }), + ]); + expect(readManifest().setup?.providers).toEqual([ + { id: "cohere", envVars: ["COHERE_API_KEY"] }, + ]); + }); + + it("exposes the static Cohere catalog", () => { + expect(buildCohereProvider()).toMatchObject({ + baseUrl: "https://api.cohere.ai/compatibility/v1", + api: "openai-completions", + models: [ + expect.objectContaining({ + id: "command-a-03-2025", + compat: { + supportsStore: false, + supportsUsageInStreaming: false, + maxTokensField: "max_tokens", + }, + }), + ], + }); + }); + + it("uses Cohere's OpenAI-compatible completions payload fields", () => { + const params = captureCoherePayload({ + systemPrompt: "system", + messages: [], + tools: [ + { + name: "lookup", + description: "Look up a value", + parameters: { type: "object", properties: {} }, + }, + ], + } as Context); + + expect(params.max_tokens).toBe(2048); + expect(params).not.toHaveProperty("max_completion_tokens"); + expect(params).not.toHaveProperty("store"); + expect(params).not.toHaveProperty("stream_options"); + expect(params).not.toHaveProperty("tool_choice"); + }); +}); diff --git a/extensions/cohere/index.ts b/extensions/cohere/index.ts new file mode 100644 index 000000000000..cb3d531a7edf --- /dev/null +++ b/extensions/cohere/index.ts @@ -0,0 +1,37 @@ +import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; +import { applyCohereConfig, COHERE_DEFAULT_MODEL_REF } from "./onboard.js"; +import { buildCohereProvider } from "./provider-catalog.js"; +import { createCohereCompletionsWrapper } from "./stream.js"; + +export default defineSingleProviderPluginEntry({ + id: "cohere", + name: "Cohere Provider", + description: "Bundled Cohere provider plugin", + provider: { + label: "Cohere", + docsPath: "/providers/cohere", + auth: [ + { + methodId: "api-key", + label: "Cohere API key", + hint: "OpenAI-compatible inference", + optionKey: "cohereApiKey", + flagName: "--cohere-api-key", + envVar: "COHERE_API_KEY", + promptMessage: "Enter Cohere API key", + defaultModel: COHERE_DEFAULT_MODEL_REF, + applyConfig: (cfg) => applyCohereConfig(cfg), + wizard: { + groupLabel: "Cohere", + groupHint: "OpenAI-compatible inference", + }, + }, + ], + catalog: { + buildProvider: buildCohereProvider, + buildStaticProvider: buildCohereProvider, + }, + wrapStreamFn: (ctx) => createCohereCompletionsWrapper(ctx.streamFn), + wrapSimpleCompletionStreamFn: (ctx) => createCohereCompletionsWrapper(ctx.streamFn), + }, +}); diff --git a/extensions/cohere/models.ts b/extensions/cohere/models.ts new file mode 100644 index 000000000000..3d4974682195 --- /dev/null +++ b/extensions/cohere/models.ts @@ -0,0 +1,27 @@ +/** + * Cohere model catalog helpers derived from the plugin manifest. + */ +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 COHERE_MANIFEST_CATALOG = manifest.modelCatalog.providers.cohere; + +export const COHERE_BASE_URL = COHERE_MANIFEST_CATALOG.baseUrl; +export const COHERE_MODEL_CATALOG = COHERE_MANIFEST_CATALOG.models; + +export function buildCohereCatalogModels(): ModelDefinitionConfig[] { + return buildManifestModelProviderConfig({ + providerId: "cohere", + catalog: COHERE_MANIFEST_CATALOG, + }).models; +} + +export function buildCohereModelDefinition( + model: (typeof COHERE_MODEL_CATALOG)[number], +): ModelDefinitionConfig { + return buildManifestModelProviderConfig({ + providerId: "cohere", + catalog: { ...COHERE_MANIFEST_CATALOG, models: [model] }, + }).models[0]; +} diff --git a/extensions/cohere/onboard.test.ts b/extensions/cohere/onboard.test.ts new file mode 100644 index 000000000000..a35fe3b5a317 --- /dev/null +++ b/extensions/cohere/onboard.test.ts @@ -0,0 +1,49 @@ +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; +import { resolveAgentModelPrimaryValue } from "openclaw/plugin-sdk/provider-onboard"; +import { describe, expect, it } from "vitest"; +import { buildCohereCatalogModels, COHERE_BASE_URL, COHERE_MODEL_CATALOG } from "./models.js"; +import { + applyCohereConfig, + applyCohereProviderConfig, + COHERE_DEFAULT_MODEL_ID, + COHERE_DEFAULT_MODEL_REF, +} from "./onboard.js"; + +describe("Cohere onboarding", () => { + it("registers the manifest catalog through the compatibility endpoint", () => { + const result = applyCohereProviderConfig({}); + const provider = result.models?.providers?.cohere; + + expect(provider).toMatchObject({ + baseUrl: COHERE_BASE_URL, + api: "openai-completions", + }); + expect(provider?.models?.map((model) => model.id)).toEqual([COHERE_DEFAULT_MODEL_ID]); + expect(buildCohereCatalogModels()).toHaveLength(COHERE_MODEL_CATALOG.length); + }); + + it("sets Cohere only when there is no primary model", () => { + const existing: OpenClawConfig = { + agents: { + defaults: { + model: { primary: "openai/gpt-5.5" }, + }, + }, + }; + + const result = applyCohereConfig(existing); + + expect(resolveAgentModelPrimaryValue(result.agents?.defaults?.model)).toBe("openai/gpt-5.5"); + expect(result.agents?.defaults?.models?.[COHERE_DEFAULT_MODEL_REF]).toEqual({ + alias: "Cohere Command A", + }); + }); + + it("uses Cohere as the first configured primary model", () => { + const result = applyCohereConfig({}); + + expect(resolveAgentModelPrimaryValue(result.agents?.defaults?.model)).toBe( + COHERE_DEFAULT_MODEL_REF, + ); + }); +}); diff --git a/extensions/cohere/onboard.ts b/extensions/cohere/onboard.ts new file mode 100644 index 000000000000..243cb35eee1a --- /dev/null +++ b/extensions/cohere/onboard.ts @@ -0,0 +1,27 @@ +import { + createModelCatalogPresetAppliers, + type OpenClawConfig, +} from "openclaw/plugin-sdk/provider-onboard"; +import { buildCohereModelDefinition, COHERE_BASE_URL, COHERE_MODEL_CATALOG } from "./models.js"; + +export const COHERE_DEFAULT_MODEL_ID = "command-a-03-2025"; +export const COHERE_DEFAULT_MODEL_REF = `cohere/${COHERE_DEFAULT_MODEL_ID}`; + +const coherePresetAppliers = createModelCatalogPresetAppliers({ + primaryModelRef: COHERE_DEFAULT_MODEL_REF, + resolveParams: (_cfg: OpenClawConfig) => ({ + providerId: "cohere", + api: "openai-completions", + baseUrl: COHERE_BASE_URL, + catalogModels: COHERE_MODEL_CATALOG.map(buildCohereModelDefinition), + aliases: [{ modelRef: COHERE_DEFAULT_MODEL_REF, alias: "Cohere Command A" }], + }), +}); + +export function applyCohereProviderConfig(cfg: OpenClawConfig): OpenClawConfig { + return coherePresetAppliers.applyProviderConfig(cfg); +} + +export function applyCohereConfig(cfg: OpenClawConfig): OpenClawConfig { + return coherePresetAppliers.applyConfig(cfg); +} diff --git a/extensions/cohere/openclaw.plugin.json b/extensions/cohere/openclaw.plugin.json new file mode 100644 index 000000000000..445f265e6f3e --- /dev/null +++ b/extensions/cohere/openclaw.plugin.json @@ -0,0 +1,67 @@ +{ + "id": "cohere", + "activation": { + "onStartup": false + }, + "enabledByDefault": true, + "providers": ["cohere"], + "modelCatalog": { + "providers": { + "cohere": { + "baseUrl": "https://api.cohere.ai/compatibility/v1", + "api": "openai-completions", + "models": [ + { + "id": "command-a-03-2025", + "name": "Command A", + "input": ["text"], + "contextWindow": 256000, + "maxTokens": 8000, + "cost": { + "input": 2.5, + "output": 10, + "cacheRead": 0, + "cacheWrite": 0 + }, + "compat": { + "supportsStore": false, + "supportsUsageInStreaming": false, + "maxTokensField": "max_tokens" + } + } + ] + } + }, + "discovery": { + "cohere": "static" + } + }, + "setup": { + "providers": [ + { + "id": "cohere", + "envVars": ["COHERE_API_KEY"] + } + ] + }, + "providerAuthChoices": [ + { + "provider": "cohere", + "method": "api-key", + "choiceId": "cohere-api-key", + "choiceLabel": "Cohere API key", + "groupId": "cohere", + "groupLabel": "Cohere", + "groupHint": "OpenAI-compatible inference", + "optionKey": "cohereApiKey", + "cliFlag": "--cohere-api-key", + "cliOption": "--cohere-api-key ", + "cliDescription": "Cohere API key" + } + ], + "configSchema": { + "type": "object", + "additionalProperties": false, + "properties": {} + } +} diff --git a/extensions/cohere/package.json b/extensions/cohere/package.json new file mode 100644 index 000000000000..60de7077845a --- /dev/null +++ b/extensions/cohere/package.json @@ -0,0 +1,15 @@ +{ + "name": "@openclaw/cohere-provider", + "version": "2026.6.8", + "private": true, + "description": "OpenClaw Cohere provider plugin", + "type": "module", + "devDependencies": { + "@openclaw/plugin-sdk": "workspace:*" + }, + "openclaw": { + "extensions": [ + "./index.ts" + ] + } +} diff --git a/extensions/cohere/provider-catalog.ts b/extensions/cohere/provider-catalog.ts new file mode 100644 index 000000000000..d22b6256f2c9 --- /dev/null +++ b/extensions/cohere/provider-catalog.ts @@ -0,0 +1,10 @@ +import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared"; +import { buildCohereCatalogModels, COHERE_BASE_URL } from "./models.js"; + +export function buildCohereProvider(): ModelProviderConfig { + return { + baseUrl: COHERE_BASE_URL, + api: "openai-completions", + models: buildCohereCatalogModels(), + }; +} diff --git a/extensions/cohere/stream.ts b/extensions/cohere/stream.ts new file mode 100644 index 000000000000..032800a8437a --- /dev/null +++ b/extensions/cohere/stream.ts @@ -0,0 +1,17 @@ +import type { ProviderWrapStreamFnContext } from "openclaw/plugin-sdk/plugin-entry"; +import { createPayloadPatchStreamWrapper } from "openclaw/plugin-sdk/provider-stream-shared"; + +export function createCohereCompletionsWrapper( + baseStreamFn: ProviderWrapStreamFnContext["streamFn"], +): ProviderWrapStreamFnContext["streamFn"] { + return createPayloadPatchStreamWrapper( + baseStreamFn, + ({ payload }) => { + // Cohere lets tool-capable models choose a tool when tool_choice is omitted. + delete payload.tool_choice; + }, + { + shouldPatch: ({ model }) => model.provider === "cohere" && model.api === "openai-completions", + }, + ); +} diff --git a/extensions/cohere/tsconfig.json b/extensions/cohere/tsconfig.json new file mode 100644 index 000000000000..b8a85a99ac3d --- /dev/null +++ b/extensions/cohere/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 dd8938ed6bde..59a0da9b5a24 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -473,6 +473,12 @@ importers: specifier: workspace:* version: link:../../packages/plugin-sdk + extensions/cohere: + devDependencies: + '@openclaw/plugin-sdk': + specifier: workspace:* + version: link:../../packages/plugin-sdk + extensions/cerebras: devDependencies: '@openclaw/plugin-sdk':