Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 1 addition & 0 deletions extensions/evolink/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { EVOLINK_BASE_URL, EVOLINK_DEFAULT_MODEL_ID } from "./models.js";
56 changes: 56 additions & 0 deletions extensions/evolink/index.test.ts
Original file line number Diff line number Diff line change
@@ -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,
},
);
});
});
42 changes: 42 additions & 0 deletions extensions/evolink/index.ts
Original file line number Diff line number Diff line change
@@ -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" }),
},
});
58 changes: 58 additions & 0 deletions extensions/evolink/models.ts
Original file line number Diff line number Diff line change
@@ -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,
};
}
49 changes: 49 additions & 0 deletions extensions/evolink/onboard.test.ts
Original file line number Diff line number Diff line change
@@ -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");
});
});
26 changes: 26 additions & 0 deletions extensions/evolink/onboard.ts
Original file line number Diff line number Diff line change
@@ -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);
}
110 changes: 110 additions & 0 deletions extensions/evolink/openclaw.plugin.json
Original file line number Diff line number Diff line change
@@ -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 <key>",
"cliDescription": "EvoLink API key"
}
],
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {}
}
}
15 changes: 15 additions & 0 deletions extensions/evolink/package.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
}
14 changes: 14 additions & 0 deletions extensions/evolink/provider-catalog.ts
Original file line number Diff line number Diff line change
@@ -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),
};
}
5 changes: 5 additions & 0 deletions extensions/evolink/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.extensions.json",
"include": ["*.ts"],
"exclude": ["dist", "node_modules"]
}
Loading
Loading