Skip to content
Closed
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
22 changes: 22 additions & 0 deletions electron/shared/providers/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,28 @@ export const PROVIDER_DEFINITIONS: ProviderDefinition[] = [
},
},
},
{
id: 'agione',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Add harness coverage for the AGIone provider scenario

AGENTS.md requires new user-visible OpenClaw scenarios to add or update the relevant harness task/scenario/rule specs, and this preset introduces a new provider flow that writes runtime provider config. This commit adds the provider and tests but no harness/specs change, so future AI-coding validation will not cover the AGIone provider/runtime behavior; please add the corresponding harness spec coverage for this scenario.

Useful? React with 👍 / 👎.

name: 'AGIone',
icon: '🧩',
placeholder: 'ak-...',
model: 'Multi-Model',
requiresApiKey: true,
defaultBaseUrl: 'https://agione.pro/hyperone/xapi/api/v1',
showModelId: true,
modelIdPlaceholder: 'qwen/qwen3.5-plus/cec84',
defaultModelId: 'qwen/qwen3.5-plus/cec84',
category: 'compatible',
envVar: 'AGIONE_API_KEY',
supportedAuthModes: ['api_key'],
defaultAuthMode: 'api_key',
supportsMultipleAccounts: false,
providerConfig: {
baseUrl: 'https://agione.pro/hyperone/xapi/api/v1',
api: 'openai-completions',
apiKeyEnv: 'AGIONE_API_KEY',
},
},
{
id: 'ark',
name: 'ByteDance Ark',
Expand Down
2 changes: 2 additions & 0 deletions electron/shared/providers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export const PROVIDER_TYPES = [
'openai',
'google',
'openrouter',
'agione',
'ark',
'moonshot',
'moonshot-global',
Expand All @@ -20,6 +21,7 @@ export const BUILTIN_PROVIDER_TYPES = [
'openai',
'google',
'openrouter',
'agione',
'ark',
'moonshot',
'moonshot-global',
Expand Down
1 change: 1 addition & 0 deletions shared/host-api/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ export type ProviderType =
| 'openai'
| 'google'
| 'openrouter'
| 'agione'
| 'ark'
| 'moonshot'
| 'moonshot-global'
Expand Down
3 changes: 3 additions & 0 deletions src/lib/providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const PROVIDER_TYPES = [
'openai',
'google',
'openrouter',
'agione',
'ark',
'moonshot',
'moonshot-global',
Expand Down Expand Up @@ -40,6 +41,7 @@ export const BUILTIN_PROVIDER_TYPES = [
'openai',
'google',
'openrouter',
'agione',
'ark',
'moonshot',
'moonshot-global',
Expand Down Expand Up @@ -186,6 +188,7 @@ export const PROVIDER_TYPE_INFO: ProviderTypeInfo[] = [
apiKeyUrl: 'https://aistudio.google.com/app/apikey',
},
{ id: 'openrouter', name: 'OpenRouter', icon: '🌐', placeholder: 'sk-or-v1-...', model: 'Multi-Model', requiresApiKey: true, showModelId: true, modelIdPlaceholder: 'openai/gpt-5.5', defaultModelId: 'openai/gpt-5.5', docsUrl: 'https://openrouter.ai/models' },
{ id: 'agione', name: 'AGIone', icon: '🧩', placeholder: 'ak-...', model: 'Multi-Model', requiresApiKey: true, defaultBaseUrl: 'https://agione.pro/hyperone/xapi/api/v1', showModelId: true, modelIdPlaceholder: 'qwen/qwen3.5-plus/cec84', defaultModelId: 'qwen/qwen3.5-plus/cec84', apiKeyUrl: 'https://agione.pro', docsUrl: 'https://agione.pro' },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Add E2E coverage for AGIone provider UI

AGENTS.md's UI change validation rule requires user-visible UI changes to include or update an Electron E2E spec; this line makes AGIone appear in the Add Provider dialog, but the commit only adds unit coverage in tests/unit/providers.test.ts. Please add a Playwright/Electron E2E that covers selecting/adding the AGIone provider so the new visible flow is validated.

Useful? React with 👍 / 👎.

{ id: 'minimax-portal-cn', name: 'MiniMax (CN)', icon: '☁️', placeholder: 'sk-...', model: 'MiniMax', requiresApiKey: false, isOAuth: true, supportsApiKey: true, defaultModelId: 'MiniMax-M3', showModelId: true, modelIdPlaceholder: 'MiniMax-M3', apiKeyUrl: 'https://platform.minimaxi.com/' },
{ id: 'moonshot', name: 'Moonshot (CN)', icon: '🌙', placeholder: 'sk-...', model: 'Kimi', requiresApiKey: true, defaultBaseUrl: 'https://api.moonshot.cn/v1', showModelId: true, defaultModelId: 'kimi-k2.6', modelIdPlaceholder: 'kimi-k2.6', docsUrl: 'https://platform.moonshot.cn/' },
{ id: 'moonshot-global', name: 'Moonshot (Global)', icon: '🌙', placeholder: 'sk-...', model: 'Kimi', requiresApiKey: true, defaultBaseUrl: 'https://api.moonshot.ai/v1', showModelId: true, defaultModelId: 'kimi-k2.6', modelIdPlaceholder: 'kimi-k2.6', docsUrl: 'https://platform.moonshot.ai/' },
Expand Down
139 changes: 139 additions & 0 deletions tests/e2e/provider-lifecycle.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,145 @@ test.describe('ClawX provider lifecycle', () => {
await expect(page.getByTestId('add-provider-api-key-input')).toHaveCount(0);
});

test('adds AGIone from the add-provider dialog with its default model preset', async ({ electronApp, page }) => {
await completeSetup(page);

await electronApp.evaluate(async ({ app: _app }) => {
const { ipcMain } = process.mainModule!.require('electron') as typeof import('electron');

type AgioneProviderE2EState = {
validateRequests: Array<{
providerId: unknown;
apiKey: unknown;
options: unknown;
}>;
createRequests: Array<{
account: Record<string, unknown>;
apiKey: unknown;
}>;
defaultAccountId: string | null;
};

const state: AgioneProviderE2EState = {
validateRequests: [],
createRequests: [],
defaultAccountId: null,
};
(globalThis as unknown as { __agioneProviderE2E?: AgioneProviderE2EState }).__agioneProviderE2E = state;

let accounts: Array<Record<string, unknown>> = [];
let keyInfo: Array<{ accountId: string; hasKey: boolean; keyMasked: string | null }> = [];
const originalHostInvoke = (ipcMain as unknown as {
_invokeHandlers?: Map<string, (event: unknown, request: unknown) => Promise<unknown>>;
})._invokeHandlers?.get('host:invoke');

const respond = (id: unknown, data: unknown) => ({
id: typeof id === 'string' ? id : undefined,
ok: true,
data,
});

ipcMain.removeHandler('host:invoke');
ipcMain.handle('host:invoke', async (event: unknown, request: {
id?: string;
module?: string;
action?: string;
payload?: Record<string, unknown>;
}) => {
if (request?.module !== 'providers') {
return originalHostInvoke?.(event, request) ?? respond(request?.id, undefined);
}

const body = request.payload ?? {};
if (request.action === 'accounts') return respond(request.id, accounts);
if (request.action === 'accountKeyInfo') return respond(request.id, keyInfo);
if (request.action === 'vendors') return respond(request.id, []);
if (request.action === 'getDefaultAccount') return respond(request.id, { accountId: state.defaultAccountId });
if (request.action === 'list') return respond(request.id, []);

if (request.action === 'validateKey') {
state.validateRequests.push({
providerId: body.providerId,
apiKey: body.apiKey,
options: body.options,
});
return respond(request.id, { valid: true });
}

if (request.action === 'createAccount') {
const account = body.account as Record<string, unknown>;
state.createRequests.push({ account, apiKey: body.apiKey });
accounts = [account];
keyInfo = [{
accountId: String(account.id),
hasKey: Boolean(body.apiKey),
keyMasked: body.apiKey ? 'ak-***' : null,
}];
return respond(request.id, { success: true, account });
}

if (request.action === 'setDefaultAccount') {
state.defaultAccountId = typeof body.accountId === 'string' ? body.accountId : null;
return respond(request.id, { success: true });
}

return respond(request.id, {});
});
});

await page.getByTestId('sidebar-nav-models').click();
await expect(page.getByTestId('providers-settings')).toBeVisible();

await page.getByTestId('providers-add-button').click();
await expect(page.getByTestId('add-provider-dialog')).toBeVisible();

await page.getByTestId('add-provider-type-agione').click();
await expect(page.getByTestId('add-provider-name-input')).toHaveValue('AGIone');
await expect(page.getByTestId('add-provider-model-id-input')).toHaveValue('qwen/qwen3.5-plus/cec84');

await page.getByTestId('add-provider-api-key-input').fill(' ak-agione-test\n');
await page.getByTestId('add-provider-submit-button').click();

await expect(page.getByTestId('provider-card-agione')).toContainText('AGIone');
await expect(page.getByTestId('provider-card-agione')).toContainText('qwen/qwen3.5-plus/cec84');

const observed = await electronApp.evaluate(() => (
globalThis as unknown as {
__agioneProviderE2E?: {
validateRequests: Array<Record<string, unknown>>;
createRequests: Array<{
account: Record<string, unknown>;
apiKey: unknown;
}>;
defaultAccountId: string | null;
};
}
).__agioneProviderE2E);

expect(observed?.validateRequests).toEqual([
{
providerId: 'agione',
apiKey: 'ak-agione-test',
options: {
baseUrl: 'https://agione.pro/hyperone/xapi/api/v1',
},
},
]);
expect(observed?.createRequests).toEqual([
{
account: expect.objectContaining({
id: 'agione',
vendorId: 'agione',
label: 'AGIone',
baseUrl: 'https://agione.pro/hyperone/xapi/api/v1',
model: 'qwen/qwen3.5-plus/cec84',
}),
apiKey: 'ak-agione-test',
},
]);
expect(observed?.defaultAccountId).toBe('agione');
});

test('trims whitespace before validating and saving a custom provider key', async ({ electronApp, page }) => {
await completeSetup(page);

Expand Down
39 changes: 37 additions & 2 deletions tests/unit/providers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,30 @@ describe('provider metadata', () => {
});
});

it('includes AGIone as an OpenAI-compatible provider', () => {
expect(PROVIDER_TYPES).toContain('agione');
expect(BUILTIN_PROVIDER_TYPES).toContain('agione');
expect(getProviderEnvVar('agione')).toBe('AGIONE_API_KEY');
expect(getProviderConfig('agione')).toEqual({
baseUrl: 'https://agione.pro/hyperone/xapi/api/v1',
api: 'openai-completions',
apiKeyEnv: 'AGIONE_API_KEY',
});

expect(PROVIDER_TYPE_INFO).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: 'agione',
name: 'AGIone',
defaultBaseUrl: 'https://agione.pro/hyperone/xapi/api/v1',
showModelId: true,
defaultModelId: 'qwen/qwen3.5-plus/cec84',
docsUrl: 'https://agione.pro',
}),
])
);
});

it('uses a single canonical env key for moonshot provider', () => {
expect(getProviderEnvVar('moonshot')).toBe('MOONSHOT_API_KEY');
expect(getProviderEnvVars('moonshot')).toEqual(['MOONSHOT_API_KEY']);
Expand All @@ -59,7 +83,7 @@ describe('provider metadata', () => {

it('keeps builtin provider sources in sync', () => {
expect(BUILTIN_PROVIDER_TYPES).toEqual(
expect.arrayContaining(['anthropic', 'openai', 'google', 'openrouter', 'ark', 'moonshot', 'siliconflow', 'minimax-portal', 'minimax-portal-cn', 'modelstudio', 'ollama'])
expect.arrayContaining(['anthropic', 'openai', 'google', 'openrouter', 'agione', 'ark', 'moonshot', 'siliconflow', 'minimax-portal', 'minimax-portal-cn', 'modelstudio', 'ollama'])
);
});

Expand All @@ -80,6 +104,7 @@ describe('provider metadata', () => {
it('exposes provider documentation links', () => {
const anthropic = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'anthropic');
const openrouter = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'openrouter');
const agione = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'agione');
const moonshot = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'moonshot');
const siliconflow = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'siliconflow');
const ark = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'ark');
Expand All @@ -90,6 +115,7 @@ describe('provider metadata', () => {
});
expect(getProviderDocsUrl(anthropic, 'en')).toBe('https://platform.claude.com/docs/en/api/overview');
expect(getProviderDocsUrl(openrouter, 'en')).toBe('https://openrouter.ai/models');
expect(getProviderDocsUrl(agione, 'en')).toBe('https://agione.pro');
expect(getProviderDocsUrl(moonshot, 'en')).toBe('https://platform.moonshot.cn/');
expect(getProviderDocsUrl(siliconflow, 'en')).toBe('https://docs.siliconflow.cn/cn/userguide/introduction');
expect(getProviderDocsUrl(ark, 'en')).toBe('https://www.volcengine.com/');
Expand All @@ -104,6 +130,7 @@ describe('provider metadata', () => {
it('exposes editable model id with default for built-in providers, mirroring OpenRouter', () => {
const anthropic = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'anthropic');
const openrouter = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'openrouter');
const agione = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'agione');
const siliconflow = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'siliconflow');
const deepseek = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'deepseek');
const moonshot = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'moonshot');
Expand All @@ -118,6 +145,11 @@ describe('provider metadata', () => {
showModelId: true,
defaultModelId: 'openai/gpt-5.5',
});
expect(agione).toMatchObject({
showModelId: true,
defaultModelId: 'qwen/qwen3.5-plus/cec84',
modelIdPlaceholder: 'qwen/qwen3.5-plus/cec84',
});
expect(siliconflow).toMatchObject({
showModelId: true,
defaultModelId: 'deepseek-ai/DeepSeek-V3',
Expand All @@ -137,7 +169,7 @@ describe('provider metadata', () => {
modelIdPlaceholder: 'kimi-k2.6',
});

for (const provider of [anthropic, openrouter, siliconflow, deepseek, moonshot, moonshotGlobal]) {
for (const provider of [anthropic, openrouter, agione, siliconflow, deepseek, moonshot, moonshotGlobal]) {
expect(provider?.showModelIdInDevModeOnly).toBeUndefined();
expect(shouldShowProviderModelId(provider, false)).toBe(true);
expect(shouldShowProviderModelId(provider, true)).toBe(true);
Expand Down Expand Up @@ -188,16 +220,19 @@ describe('provider metadata', () => {

it('saves user-entered or default model overrides for built-in providers without dev mode', () => {
const openrouter = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'openrouter');
const agione = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'agione');
const siliconflow = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'siliconflow');
const anthropic = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'anthropic');
const ark = PROVIDER_TYPE_INFO.find((provider) => provider.id === 'ark');

expect(resolveProviderModelForSave(openrouter, 'openai/gpt-5', false)).toBe('openai/gpt-5');
expect(resolveProviderModelForSave(agione, 'qwen/qwen3.5-plus/cec84', false)).toBe('qwen/qwen3.5-plus/cec84');
expect(resolveProviderModelForSave(siliconflow, 'Qwen/Qwen3-Coder-480B-A35B-Instruct', false))
.toBe('Qwen/Qwen3-Coder-480B-A35B-Instruct');
expect(resolveProviderModelForSave(anthropic, 'claude-sonnet-4-5', false)).toBe('claude-sonnet-4-5');

expect(resolveProviderModelForSave(openrouter, ' ', false)).toBe('openai/gpt-5.5');
expect(resolveProviderModelForSave(agione, ' ', false)).toBe('qwen/qwen3.5-plus/cec84');
expect(resolveProviderModelForSave(siliconflow, ' ', false)).toBe('deepseek-ai/DeepSeek-V3');
expect(resolveProviderModelForSave(anthropic, ' ', false)).toBe('claude-opus-4-6');
expect(resolveProviderModelForSave(ark, ' ep-custom-model ', false)).toBe('ep-custom-model');
Expand Down