diff --git a/.github/labeler.yml b/.github/labeler.yml index 76f4e45d..49423877 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -314,6 +314,10 @@ - changed-files: - any-glob-to-any-file: - "extensions/deepseek/**" +"extensions: evolink": + - changed-files: + - any-glob-to-any-file: + - "extensions/evolink/**" "extensions: tencent": - changed-files: - any-glob-to-any-file: diff --git a/extensions/evolink/api.ts b/extensions/evolink/api.ts new file mode 100644 index 00000000..4ce3fe88 --- /dev/null +++ b/extensions/evolink/api.ts @@ -0,0 +1 @@ +export { EVOLINK_BASE_URL, EVOLINK_DEFAULT_MODEL_ID } from "./models.js"; diff --git a/extensions/evolink/index.test.ts b/extensions/evolink/index.test.ts new file mode 100644 index 00000000..30510c95 --- /dev/null +++ b/extensions/evolink/index.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "vitest"; +import { resolveProviderPluginChoice } from "../../src/plugins/provider-auth-choice.runtime.js"; +import { registerSingleProviderPlugin } from "../../test/helpers/plugins/plugin-registration.js"; +import { runSingleProviderCatalog } from "../test-support/provider-model-test-helpers.js"; +import evolinkPlugin from "./index.js"; + +describe("evolink provider plugin", () => { + it("registers EvoLink with api-key auth wizard metadata", async () => { + const provider = await registerSingleProviderPlugin(evolinkPlugin); + const resolved = resolveProviderPluginChoice({ + providers: [provider], + choice: "evolink-api-key", + }); + + expect(provider.id).toBe("evolink"); + expect(provider.label).toBe("EvoLink"); + expect(provider.envVars).toEqual(["EVOLINK_API_KEY"]); + expect(provider.auth).toHaveLength(1); + expect(resolved).not.toBeNull(); + expect(resolved?.provider.id).toBe("evolink"); + expect(resolved?.method.id).toBe("api-key"); + }); + + it("builds the static EvoLink model catalog", async () => { + const provider = await registerSingleProviderPlugin(evolinkPlugin); + const catalogProvider = await runSingleProviderCatalog(provider); + + expect(catalogProvider.api).toBe("openai-completions"); + expect(catalogProvider.baseUrl).toBe("https://direct.evolink.ai/v1"); + expect(catalogProvider.models?.map((model) => model.id)).toEqual([ + "gpt-5.2", + "gpt-5.1", + "gemini-3.1-flash-lite-preview", + "deepseek-v4-flash", + ]); + expect(catalogProvider.models?.find((model) => model.id === "gpt-5.2")).toMatchObject({ + reasoning: true, + input: ["text"], + contextWindow: 200_000, + maxTokens: 8192, + }); + }); + + it("owns OpenAI-compatible replay policy", async () => { + const provider = await registerSingleProviderPlugin(evolinkPlugin); + + expect(provider.buildReplayPolicy?.({ modelApi: "openai-completions" } as never)).toMatchObject( + { + sanitizeToolCallIds: true, + toolCallIdMode: "strict", + validateGeminiTurns: true, + validateAnthropicTurns: true, + }, + ); + }); +}); diff --git a/extensions/evolink/index.ts b/extensions/evolink/index.ts new file mode 100644 index 00000000..e57f7ef8 --- /dev/null +++ b/extensions/evolink/index.ts @@ -0,0 +1,42 @@ +import { defineSingleProviderPluginEntry } from "openclaw/plugin-sdk/provider-entry"; +import { buildProviderReplayFamilyHooks } from "openclaw/plugin-sdk/provider-model-shared"; +import { applyEvoLinkConfig, EVOLINK_DEFAULT_MODEL_REF } from "./onboard.js"; +import { buildEvoLinkProvider } from "./provider-catalog.js"; + +const PROVIDER_ID = "evolink"; + +export default defineSingleProviderPluginEntry({ + id: PROVIDER_ID, + name: "EvoLink Provider", + description: "Bundled EvoLink provider plugin", + provider: { + label: "EvoLink", + docsPath: "/providers/models", + auth: [ + { + methodId: "api-key", + label: "EvoLink API key", + hint: "OpenAI-compatible API key", + optionKey: "evolinkApiKey", + flagName: "--evolink-api-key", + envVar: "EVOLINK_API_KEY", + promptMessage: "Enter EvoLink API key", + defaultModel: EVOLINK_DEFAULT_MODEL_REF, + applyConfig: (cfg) => applyEvoLinkConfig(cfg), + noteMessage: [ + "EvoLink exposes chat models through an OpenAI-compatible endpoint.", + "Get your API key in the EvoLink dashboard: https://evolink.ai/dashboard/keys", + ].join("\n"), + noteTitle: "EvoLink", + wizard: { + groupLabel: "EvoLink", + }, + }, + ], + catalog: { + buildProvider: buildEvoLinkProvider, + buildStaticProvider: buildEvoLinkProvider, + }, + ...buildProviderReplayFamilyHooks({ family: "openai-compatible" }), + }, +}); diff --git a/extensions/evolink/models.ts b/extensions/evolink/models.ts new file mode 100644 index 00000000..2e01ac3d --- /dev/null +++ b/extensions/evolink/models.ts @@ -0,0 +1,58 @@ +import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared"; + +export const EVOLINK_BASE_URL = "https://direct.evolink.ai/v1"; +export const EVOLINK_DEFAULT_MODEL_ID = "gpt-5.2"; + +export const EVOLINK_MODEL_CATALOG = [ + { + id: EVOLINK_DEFAULT_MODEL_ID, + name: "GPT-5.2", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 200_000, + maxTokens: 8192, + }, + { + id: "gpt-5.1", + name: "GPT-5.1", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 200_000, + maxTokens: 8192, + }, + { + id: "gemini-3.1-flash-lite-preview", + name: "Gemini 3.1 Flash Lite Preview", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 1_000_000, + maxTokens: 8192, + }, + { + id: "deepseek-v4-flash", + name: "DeepSeek V4 Flash", + reasoning: true, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 1_000_000, + maxTokens: 384_000, + }, +] as const; + +export function buildEvoLinkModelDefinition( + model: (typeof EVOLINK_MODEL_CATALOG)[number], +): ModelDefinitionConfig { + return { + id: model.id, + name: model.name, + api: "openai-completions", + reasoning: model.reasoning, + input: [...model.input], + cost: model.cost, + contextWindow: model.contextWindow, + maxTokens: model.maxTokens, + }; +} diff --git a/extensions/evolink/onboard.test.ts b/extensions/evolink/onboard.test.ts new file mode 100644 index 00000000..bcbc0415 --- /dev/null +++ b/extensions/evolink/onboard.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from "vitest"; +import { + expectProviderOnboardMergedLegacyConfig, + expectProviderOnboardPrimaryAndFallbacks, +} from "../../test/helpers/plugins/provider-onboard.js"; +import { + applyEvoLinkConfig, + applyEvoLinkProviderConfig, + EVOLINK_DEFAULT_MODEL_REF, +} from "./onboard.js"; + +describe("evolink onboard", () => { + it("adds EvoLink provider with correct settings", () => { + const cfg = applyEvoLinkConfig({}); + expect(cfg.models?.providers?.evolink).toMatchObject({ + baseUrl: "https://direct.evolink.ai/v1", + api: "openai-completions", + }); + expectProviderOnboardPrimaryAndFallbacks({ + applyConfig: applyEvoLinkConfig, + modelRef: EVOLINK_DEFAULT_MODEL_REF, + }); + }); + + it("merges EvoLink models and keeps existing provider overrides", () => { + const provider = expectProviderOnboardMergedLegacyConfig({ + applyProviderConfig: applyEvoLinkProviderConfig, + providerId: "evolink", + providerApi: "openai-completions", + baseUrl: "https://direct.evolink.ai/v1", + legacyApi: "anthropic-messages", + legacyModelId: "custom-evolink-model", + legacyModelName: "Custom EvoLink Model", + }); + + expect(provider?.models.map((model) => model.id)).toEqual([ + "custom-evolink-model", + "gpt-5.2", + "gpt-5.1", + "gemini-3.1-flash-lite-preview", + "deepseek-v4-flash", + ]); + }); + + it("adds the expected alias for the default model", () => { + const cfg = applyEvoLinkProviderConfig({}); + expect(cfg.agents?.defaults?.models?.[EVOLINK_DEFAULT_MODEL_REF]?.alias).toBe("EvoLink"); + }); +}); diff --git a/extensions/evolink/onboard.ts b/extensions/evolink/onboard.ts new file mode 100644 index 00000000..465e4ace --- /dev/null +++ b/extensions/evolink/onboard.ts @@ -0,0 +1,26 @@ +import { + createModelCatalogPresetAppliers, + type OpenClawConfig, +} from "openclaw/plugin-sdk/provider-onboard"; +import { buildEvoLinkModelDefinition, EVOLINK_BASE_URL, EVOLINK_MODEL_CATALOG } from "./models.js"; + +export const EVOLINK_DEFAULT_MODEL_REF = "evolink/gpt-5.2"; + +const evoLinkPresetAppliers = createModelCatalogPresetAppliers({ + primaryModelRef: EVOLINK_DEFAULT_MODEL_REF, + resolveParams: (_cfg: OpenClawConfig) => ({ + providerId: "evolink", + api: "openai-completions", + baseUrl: EVOLINK_BASE_URL, + catalogModels: EVOLINK_MODEL_CATALOG.map(buildEvoLinkModelDefinition), + aliases: [{ modelRef: EVOLINK_DEFAULT_MODEL_REF, alias: "EvoLink" }], + }), +}); + +export function applyEvoLinkProviderConfig(cfg: OpenClawConfig): OpenClawConfig { + return evoLinkPresetAppliers.applyProviderConfig(cfg); +} + +export function applyEvoLinkConfig(cfg: OpenClawConfig): OpenClawConfig { + return evoLinkPresetAppliers.applyConfig(cfg); +} diff --git a/extensions/evolink/openclaw.plugin.json b/extensions/evolink/openclaw.plugin.json new file mode 100644 index 00000000..afec932d --- /dev/null +++ b/extensions/evolink/openclaw.plugin.json @@ -0,0 +1,110 @@ +{ + "id": "evolink", + "enabledByDefault": true, + "providers": ["evolink"], + "providerEndpoints": [ + { + "endpointClass": "evolink-openai-compatible", + "hosts": ["direct.evolink.ai"] + } + ], + "providerRequest": { + "providers": { + "evolink": { + "family": "openai-compatible" + } + } + }, + "modelCatalog": { + "providers": { + "evolink": { + "baseUrl": "https://direct.evolink.ai/v1", + "api": "openai-completions", + "models": [ + { + "id": "gpt-5.2", + "name": "GPT-5.2", + "reasoning": true, + "input": ["text"], + "contextWindow": 200000, + "maxTokens": 8192, + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + } + }, + { + "id": "gpt-5.1", + "name": "GPT-5.1", + "reasoning": true, + "input": ["text"], + "contextWindow": 200000, + "maxTokens": 8192, + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + } + }, + { + "id": "gemini-3.1-flash-lite-preview", + "name": "Gemini 3.1 Flash Lite Preview", + "reasoning": false, + "input": ["text"], + "contextWindow": 1000000, + "maxTokens": 8192, + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + } + }, + { + "id": "deepseek-v4-flash", + "name": "DeepSeek V4 Flash", + "reasoning": true, + "input": ["text"], + "contextWindow": 1000000, + "maxTokens": 384000, + "cost": { + "input": 0, + "output": 0, + "cacheRead": 0, + "cacheWrite": 0 + } + } + ] + } + }, + "discovery": { + "evolink": "static" + } + }, + "providerAuthEnvVars": { + "evolink": ["EVOLINK_API_KEY"] + }, + "providerAuthChoices": [ + { + "provider": "evolink", + "method": "api-key", + "choiceId": "evolink-api-key", + "choiceLabel": "EvoLink API key", + "groupId": "evolink", + "groupLabel": "EvoLink", + "groupHint": "OpenAI-compatible API key", + "optionKey": "evolinkApiKey", + "cliFlag": "--evolink-api-key", + "cliOption": "--evolink-api-key ", + "cliDescription": "EvoLink API key" + } + ], + "configSchema": { + "type": "object", + "additionalProperties": false, + "properties": {} + } +} diff --git a/extensions/evolink/package.json b/extensions/evolink/package.json new file mode 100644 index 00000000..04693844 --- /dev/null +++ b/extensions/evolink/package.json @@ -0,0 +1,15 @@ +{ + "name": "@openclaw/evolink-provider", + "version": "2026.4.26", + "private": true, + "description": "OpenClaw EvoLink provider plugin", + "type": "module", + "devDependencies": { + "@openclaw/plugin-sdk": "workspace:*" + }, + "openclaw": { + "extensions": [ + "./index.ts" + ] + } +} diff --git a/extensions/evolink/provider-catalog.ts b/extensions/evolink/provider-catalog.ts new file mode 100644 index 00000000..7e54a44d --- /dev/null +++ b/extensions/evolink/provider-catalog.ts @@ -0,0 +1,14 @@ +import type { ModelProviderConfig } from "openclaw/plugin-sdk/provider-model-shared"; +import { + buildEvoLinkModelDefinition, + EVOLINK_BASE_URL, + EVOLINK_MODEL_CATALOG, +} from "./models.js"; + +export function buildEvoLinkProvider(): ModelProviderConfig { + return { + baseUrl: EVOLINK_BASE_URL, + api: "openai-completions", + models: EVOLINK_MODEL_CATALOG.map(buildEvoLinkModelDefinition), + }; +} diff --git a/extensions/evolink/tsconfig.json b/extensions/evolink/tsconfig.json new file mode 100644 index 00000000..6b5ce2e4 --- /dev/null +++ b/extensions/evolink/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.extensions.json", + "include": ["*.ts"], + "exclude": ["dist", "node_modules"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9321502a..ae662b60 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -572,6 +572,12 @@ importers: specifier: workspace:* version: link:../../packages/plugin-sdk + extensions/evolink: + devDependencies: + '@openclaw/plugin-sdk': + specifier: workspace:* + version: link:../../packages/plugin-sdk + extensions/exa: devDependencies: '@openclaw/plugin-sdk': diff --git a/test/vitest/vitest.extension-provider-paths.mjs b/test/vitest/vitest.extension-provider-paths.mjs index b06cccb8..dea66933 100644 --- a/test/vitest/vitest.extension-provider-paths.mjs +++ b/test/vitest/vitest.extension-provider-paths.mjs @@ -9,6 +9,7 @@ export const providerExtensionIds = [ "chutes", "comfy", "deepseek", + "evolink", "github-copilot", "google", "groq",