451 lines
15 KiB
TypeScript
451 lines
15 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import type { ProviderAccount } from '@electron/shared/providers/types';
|
|
import {
|
|
buildOpencodeRuntimeConfig,
|
|
summarizeOpencodeRuntimeConfig,
|
|
} from '@electron/opencode/provider-config';
|
|
|
|
function createAccount(overrides: Partial<ProviderAccount> = {}): ProviderAccount {
|
|
return {
|
|
id: 'openai',
|
|
vendorId: 'openai',
|
|
label: 'OpenAI',
|
|
authMode: 'api_key',
|
|
model: 'gpt-5.4',
|
|
enabled: true,
|
|
isDefault: false,
|
|
createdAt: '2026-05-12T00:00:00.000Z',
|
|
updatedAt: '2026-05-12T00:00:00.000Z',
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe('buildOpencodeRuntimeConfig', () => {
|
|
it('creates env-backed opencode config for the default built-in provider', async () => {
|
|
const resolveApiKey = vi.fn().mockResolvedValue('sk-openai');
|
|
|
|
const result = await buildOpencodeRuntimeConfig({
|
|
accounts: [createAccount()],
|
|
defaultAccountId: 'openai',
|
|
resolveApiKey,
|
|
});
|
|
|
|
expect(result.config).toMatchObject({
|
|
'$schema': 'https://opencode.ai/config.json',
|
|
enabled_providers: ['openai'],
|
|
permission: {
|
|
external_directory: 'ask',
|
|
},
|
|
provider: {
|
|
openai: {
|
|
options: {
|
|
apiKey: '{env:NIANCODE_OPENCODE_OPENAI_API_KEY}',
|
|
},
|
|
},
|
|
},
|
|
model: 'openai/gpt-5.4',
|
|
});
|
|
expect(result.config.provider.openai).not.toHaveProperty('npm');
|
|
expect(result.env).toEqual({
|
|
NIANCODE_OPENCODE_OPENAI_API_KEY: 'sk-openai',
|
|
});
|
|
});
|
|
|
|
it('includes configured MCP servers in the runtime config and sanitized summary', async () => {
|
|
const result = await buildOpencodeRuntimeConfig({
|
|
accounts: [createAccount()],
|
|
defaultAccountId: 'openai',
|
|
resolveApiKey: vi.fn().mockResolvedValue('sk-openai'),
|
|
mcpServers: {
|
|
playwright: {
|
|
type: 'local',
|
|
command: ['node', 'node_modules/@playwright/mcp/cli.js'],
|
|
enabled: true,
|
|
env: {
|
|
ELECTRON_RUN_AS_NODE: '1',
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
expect(result.config.mcp).toEqual({
|
|
playwright: {
|
|
type: 'local',
|
|
command: ['node', 'node_modules/@playwright/mcp/cli.js'],
|
|
enabled: true,
|
|
env: {
|
|
ELECTRON_RUN_AS_NODE: '1',
|
|
},
|
|
},
|
|
});
|
|
expect(summarizeOpencodeRuntimeConfig(result)).toMatchObject({
|
|
mcpServerIds: ['playwright'],
|
|
mcpServerCount: 1,
|
|
mcpServers: [
|
|
{
|
|
id: 'playwright',
|
|
type: 'local',
|
|
enabled: true,
|
|
command: ['node', 'node_modules/@playwright/mcp/cli.js'],
|
|
envKeys: ['ELECTRON_RUN_AS_NODE'],
|
|
},
|
|
],
|
|
});
|
|
expect(JSON.stringify(summarizeOpencodeRuntimeConfig(result))).not.toContain('sk-openai');
|
|
});
|
|
|
|
it('creates a custom OpenAI-compatible provider entry with base URL and model metadata', async () => {
|
|
const result = await buildOpencodeRuntimeConfig({
|
|
accounts: [
|
|
createAccount({
|
|
id: 'custom-kimi',
|
|
vendorId: 'custom',
|
|
label: 'Kimi Proxy',
|
|
baseUrl: 'https://api.moonshot.cn/v1/',
|
|
apiProtocol: 'openai-completions',
|
|
headers: {
|
|
'X-Works-Square-AI-Token': '{env:NIANCODE_OPENCODE_NIANCODE_USER_MODELS_API_KEY}',
|
|
},
|
|
model: 'kimi-k2.6',
|
|
}),
|
|
],
|
|
defaultAccountId: 'custom-kimi',
|
|
resolveApiKey: vi.fn().mockResolvedValue('sk-kimi'),
|
|
});
|
|
|
|
expect(result.config).toMatchObject({
|
|
provider: {
|
|
'custom-kimi': {
|
|
npm: '@ai-sdk/openai-compatible',
|
|
name: 'Kimi Proxy',
|
|
options: {
|
|
apiKey: '{env:NIANCODE_OPENCODE_CUSTOM_KIMI_API_KEY}',
|
|
baseURL: 'https://api.moonshot.cn/v1',
|
|
headers: {
|
|
'X-Works-Square-AI-Token': '{env:NIANCODE_OPENCODE_NIANCODE_USER_MODELS_API_KEY}',
|
|
},
|
|
},
|
|
models: {
|
|
'kimi-k2.6': {
|
|
name: 'kimi-k2.6',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
model: 'custom-kimi/kimi-k2.6',
|
|
});
|
|
expect(result.env).toEqual({
|
|
NIANCODE_OPENCODE_CUSTOM_KIMI_API_KEY: 'sk-kimi',
|
|
});
|
|
});
|
|
|
|
it('expands fallback models as additional runtime models for one imported provider', async () => {
|
|
const result = await buildOpencodeRuntimeConfig({
|
|
accounts: [
|
|
createAccount({
|
|
id: 'niancode-user-models',
|
|
vendorId: 'custom',
|
|
label: 'Makelore Models',
|
|
baseUrl: 'https://one-api.example.com/v1',
|
|
apiProtocol: 'openai-completions',
|
|
model: 'gpt-4.1-mini',
|
|
fallbackModels: ['gpt-4o-mini', 'qwen3-coder', 'gpt-4o-mini'],
|
|
}),
|
|
],
|
|
defaultAccountId: 'niancode-user-models',
|
|
resolveApiKey: vi.fn().mockResolvedValue('sk-user-token'),
|
|
});
|
|
|
|
expect(result.config.provider['niancode-user-models'].models).toEqual({
|
|
'gpt-4.1-mini': { name: 'gpt-4.1-mini' },
|
|
'gpt-4o-mini': { name: 'gpt-4o-mini' },
|
|
'qwen3-coder': { name: 'qwen3-coder' },
|
|
});
|
|
expect(result.config.model).toBe('niancode-user-models/gpt-4.1-mini');
|
|
});
|
|
|
|
it('adds verified limits only to exact imported Qwen Plus profiles', async () => {
|
|
const result = await buildOpencodeRuntimeConfig({
|
|
accounts: [
|
|
createAccount({
|
|
id: 'niancode-user-models',
|
|
vendorId: 'custom',
|
|
label: '娉ュ湡 Models',
|
|
baseUrl: 'http://127.0.0.1:13210/api/ai-proxy/v1',
|
|
apiProtocol: 'openai-completions',
|
|
model: 'qwen3.6-plus',
|
|
fallbackModels: [
|
|
'qwen3.6-plus-2026-04-02',
|
|
'qwen3.7-plus',
|
|
'qwen3.7-plus-2026-05-26',
|
|
'qwen-vl-max',
|
|
'qwen-omni-turbo',
|
|
'qwen3.8-plus',
|
|
'qwen3.7-plus-latest',
|
|
'qwen3.7-plus-2026-06-15',
|
|
'deepseek-chat',
|
|
],
|
|
}),
|
|
],
|
|
defaultAccountId: 'niancode-user-models',
|
|
resolveApiKey: vi.fn().mockResolvedValue('local-host-api-token'),
|
|
});
|
|
|
|
const models = result.config.provider['niancode-user-models'].models;
|
|
expect(models).toMatchObject({
|
|
'qwen3.6-plus': {
|
|
modalities: { input: ['text', 'image'], output: ['text'] },
|
|
limit: { context: 1_000_000, output: 65_536 },
|
|
},
|
|
'qwen3.6-plus-2026-04-02': {
|
|
modalities: { input: ['text', 'image'], output: ['text'] },
|
|
limit: { context: 1_000_000, output: 65_536 },
|
|
},
|
|
'qwen3.7-plus': {
|
|
modalities: { input: ['text', 'image'], output: ['text'] },
|
|
limit: { context: 1_000_000, output: 65_536 },
|
|
},
|
|
'qwen3.7-plus-2026-05-26': {
|
|
modalities: { input: ['text', 'image'], output: ['text'] },
|
|
limit: { context: 1_000_000, output: 65_536 },
|
|
},
|
|
'qwen-vl-max': {
|
|
modalities: { input: ['text', 'image'], output: ['text'] },
|
|
},
|
|
'qwen-omni-turbo': {
|
|
modalities: { input: ['text', 'image'], output: ['text'] },
|
|
},
|
|
});
|
|
|
|
for (const modelId of ['qwen-vl-max', 'qwen-omni-turbo']) {
|
|
expect(models?.[modelId]).not.toHaveProperty('limit');
|
|
}
|
|
for (const modelId of [
|
|
'qwen3.8-plus',
|
|
'qwen3.7-plus-latest',
|
|
'qwen3.7-plus-2026-06-15',
|
|
'deepseek-chat',
|
|
]) {
|
|
expect(models?.[modelId]).not.toHaveProperty('modalities');
|
|
expect(models?.[modelId]).not.toHaveProperty('limit');
|
|
}
|
|
|
|
expect(summarizeOpencodeRuntimeConfig(result).providers[0]).toMatchObject({
|
|
imageInputModelIds: [
|
|
'qwen3.6-plus',
|
|
'qwen3.6-plus-2026-04-02',
|
|
'qwen3.7-plus',
|
|
'qwen3.7-plus-2026-05-26',
|
|
'qwen-vl-max',
|
|
'qwen-omni-turbo',
|
|
],
|
|
modelLimits: {
|
|
'qwen3.6-plus': { context: 1_000_000, output: 65_536 },
|
|
'qwen3.6-plus-2026-04-02': { context: 1_000_000, output: 65_536 },
|
|
'qwen3.7-plus': { context: 1_000_000, output: 65_536 },
|
|
'qwen3.7-plus-2026-05-26': { context: 1_000_000, output: 65_536 },
|
|
},
|
|
});
|
|
expect(JSON.stringify(summarizeOpencodeRuntimeConfig(result))).not.toContain('local-host-api-token');
|
|
});
|
|
|
|
it('uses only the imported Works Square model provider when it is available', async () => {
|
|
const result = await buildOpencodeRuntimeConfig({
|
|
accounts: [
|
|
createAccount({
|
|
id: 'openai',
|
|
vendorId: 'openai',
|
|
label: 'OpenAI',
|
|
model: 'gpt-5.4',
|
|
}),
|
|
createAccount({
|
|
id: 'niancode-user-models',
|
|
vendorId: 'custom',
|
|
label: 'Makelore Models',
|
|
baseUrl: 'https://one-api.example.com/v1',
|
|
apiProtocol: 'openai-completions',
|
|
model: 'gpt-4.1-mini',
|
|
fallbackModels: ['gpt-4o-mini'],
|
|
}),
|
|
],
|
|
defaultAccountId: 'openai',
|
|
resolveApiKey: vi.fn(async (account: ProviderAccount) => (
|
|
account.id === 'niancode-user-models' ? 'sk-user-token' : 'sk-openai'
|
|
)),
|
|
});
|
|
|
|
expect(Object.keys(result.config.provider)).toEqual(['niancode-user-models']);
|
|
expect(result.config.enabled_providers).toEqual(['niancode-user-models']);
|
|
expect(result.config.provider).not.toHaveProperty('openai');
|
|
expect(result.config.provider['niancode-user-models']).toMatchObject({
|
|
npm: '@ai-sdk/openai-compatible',
|
|
name: 'Makelore Models',
|
|
options: {
|
|
apiKey: '{env:NIANCODE_OPENCODE_NIANCODE_USER_MODELS_API_KEY}',
|
|
baseURL: 'https://one-api.example.com/v1',
|
|
},
|
|
models: {
|
|
'gpt-4.1-mini': { name: 'gpt-4.1-mini' },
|
|
'gpt-4o-mini': { name: 'gpt-4o-mini' },
|
|
},
|
|
});
|
|
expect(result.config.model).toBe('niancode-user-models/gpt-4.1-mini');
|
|
expect(result.env).toEqual({
|
|
NIANCODE_OPENCODE_NIANCODE_USER_MODELS_API_KEY: 'sk-user-token',
|
|
});
|
|
|
|
expect(summarizeOpencodeRuntimeConfig(result)).toMatchObject({
|
|
model: 'niancode-user-models/gpt-4.1-mini',
|
|
enabledProviderIds: ['niancode-user-models'],
|
|
providers: [
|
|
{
|
|
id: 'niancode-user-models',
|
|
npm: '@ai-sdk/openai-compatible',
|
|
baseURL: 'https://one-api.example.com/v1',
|
|
modelIds: ['gpt-4.1-mini', 'gpt-4o-mini'],
|
|
hasApiKey: true,
|
|
apiKeyEnv: 'NIANCODE_OPENCODE_NIANCODE_USER_MODELS_API_KEY',
|
|
},
|
|
],
|
|
});
|
|
expect(JSON.stringify(summarizeOpencodeRuntimeConfig(result))).not.toContain('sk-user-token');
|
|
});
|
|
|
|
it('normalizes an existing root Works Square AI gateway URL before passing it to opencode', async () => {
|
|
const result = await buildOpencodeRuntimeConfig({
|
|
accounts: [
|
|
createAccount({
|
|
id: 'niancode-user-models',
|
|
vendorId: 'custom',
|
|
label: 'Makelore Models',
|
|
baseUrl: 'https://token.nianxx.cn',
|
|
apiProtocol: 'openai-completions',
|
|
model: 'deepseek-chat',
|
|
headers: {
|
|
'X-Works-Square-AI-Token': '{env:NIANCODE_OPENCODE_NIANCODE_USER_MODELS_API_KEY}',
|
|
},
|
|
metadata: {
|
|
worksSquareCredentialMode: 'works_square_ai_gateway',
|
|
},
|
|
}),
|
|
],
|
|
defaultAccountId: 'niancode-user-models',
|
|
resolveApiKey: vi.fn().mockResolvedValue('sk-user-token'),
|
|
});
|
|
|
|
expect(result.config.provider['niancode-user-models'].options).toMatchObject({
|
|
baseURL: 'https://token.nianxx.cn/v1',
|
|
headers: {
|
|
Authorization: 'Bearer {env:NIANCODE_OPENCODE_NIANCODE_USER_MODELS_API_KEY}',
|
|
'X-Works-Square-AI-Token': '{env:NIANCODE_OPENCODE_NIANCODE_USER_MODELS_API_KEY}',
|
|
},
|
|
});
|
|
});
|
|
|
|
it('routes imported Works Square models through the local AI proxy without gateway-token headers', async () => {
|
|
const result = await buildOpencodeRuntimeConfig({
|
|
accounts: [
|
|
createAccount({
|
|
id: 'niancode-user-models',
|
|
vendorId: 'custom',
|
|
label: 'Makelore Models',
|
|
baseUrl: 'http://127.0.0.1:13210/api/ai-proxy/v1',
|
|
apiProtocol: 'openai-completions',
|
|
model: 'deepseek-chat',
|
|
fallbackModels: ['gpt-4.1-mini'],
|
|
metadata: {
|
|
worksSquareCredentialMode: 'works_square_ai_gateway_proxy',
|
|
worksSquareOneApiBaseUrl: 'https://open-api.example.com/v1',
|
|
},
|
|
}),
|
|
],
|
|
defaultAccountId: 'niancode-user-models',
|
|
resolveApiKey: vi.fn().mockResolvedValue('local-host-api-token'),
|
|
});
|
|
|
|
expect(result.config.provider['niancode-user-models'].options).toEqual({
|
|
apiKey: '{env:NIANCODE_OPENCODE_NIANCODE_USER_MODELS_API_KEY}',
|
|
baseURL: 'http://127.0.0.1:13210/api/ai-proxy/v1',
|
|
});
|
|
expect(JSON.stringify(result.config)).not.toContain('X-Works-Square-AI-Token');
|
|
expect(JSON.stringify(result.config)).not.toContain('https://open-api.example.com');
|
|
expect(result.env).toEqual({
|
|
NIANCODE_OPENCODE_NIANCODE_USER_MODELS_API_KEY: 'local-host-api-token',
|
|
});
|
|
});
|
|
|
|
it('strips DeepSeek routing prefixes from imported Works Square runtime models', async () => {
|
|
const result = await buildOpencodeRuntimeConfig({
|
|
accounts: [
|
|
createAccount({
|
|
id: 'niancode-user-models',
|
|
vendorId: 'custom',
|
|
label: 'Makelore Models',
|
|
baseUrl: 'https://one-api.example.com/v1',
|
|
apiProtocol: 'openai-completions',
|
|
model: 'deepseek/deepseek-v4-pro',
|
|
fallbackModels: ['deepseek/deepseek-chat'],
|
|
}),
|
|
],
|
|
defaultAccountId: 'niancode-user-models',
|
|
resolveApiKey: vi.fn().mockResolvedValue('sk-user-token'),
|
|
});
|
|
|
|
expect(result.config.provider['niancode-user-models'].models).toEqual({
|
|
'deepseek-v4-pro': { name: 'deepseek-v4-pro' },
|
|
'deepseek-chat': { name: 'deepseek-chat' },
|
|
});
|
|
expect(result.config.model).toBe('niancode-user-models/deepseek-v4-pro');
|
|
});
|
|
|
|
it('does not fall back to other providers when the imported Works Square account exists', async () => {
|
|
const result = await buildOpencodeRuntimeConfig({
|
|
accounts: [
|
|
createAccount({
|
|
id: 'openai',
|
|
vendorId: 'openai',
|
|
label: 'OpenAI',
|
|
model: 'gpt-5.4',
|
|
}),
|
|
createAccount({
|
|
id: 'niancode-user-models',
|
|
vendorId: 'custom',
|
|
label: 'Makelore Models',
|
|
baseUrl: 'https://one-api.example.com/v1',
|
|
apiProtocol: 'openai-completions',
|
|
model: 'gpt-4.1-mini',
|
|
enabled: false,
|
|
}),
|
|
],
|
|
defaultAccountId: 'openai',
|
|
resolveApiKey: vi.fn().mockResolvedValue('sk-openai'),
|
|
});
|
|
|
|
expect(result.config.provider).toEqual({});
|
|
expect(result.config.enabled_providers).toEqual([]);
|
|
expect(result.config.model).toBeUndefined();
|
|
expect(result.env).toEqual({});
|
|
});
|
|
|
|
it('skips disabled accounts and accounts without usable credentials', async () => {
|
|
const result = await buildOpencodeRuntimeConfig({
|
|
accounts: [
|
|
createAccount({ id: 'disabled-openai', enabled: false }),
|
|
createAccount({ id: 'missing-key-openai' }),
|
|
],
|
|
defaultAccountId: 'missing-key-openai',
|
|
resolveApiKey: vi.fn().mockResolvedValue(null),
|
|
});
|
|
|
|
expect(result.config).toEqual({
|
|
'$schema': 'https://opencode.ai/config.json',
|
|
enabled_providers: [],
|
|
permission: {
|
|
external_directory: 'ask',
|
|
},
|
|
provider: {},
|
|
});
|
|
expect(result.env).toEqual({});
|
|
});
|
|
});
|