Makelore 2.0 initial clean snapshot
This commit is contained in:
456
electron/opencode/provider-config.ts
Normal file
456
electron/opencode/provider-config.ts
Normal file
@@ -0,0 +1,456 @@
|
||||
import type { ProviderAccount, ProviderProtocol } from '../shared/providers/types';
|
||||
import {
|
||||
getProviderBackendConfig,
|
||||
getProviderDefaultModel,
|
||||
getProviderDefinition,
|
||||
} from '../shared/providers/registry';
|
||||
import { getProviderService } from '../services/providers/provider-service';
|
||||
import { getApiKey } from '../utils/secure-storage';
|
||||
import {
|
||||
resolveOpencodeProviderId,
|
||||
sanitizeOpencodeProviderId,
|
||||
} from '../../shared/opencode-provider-id';
|
||||
import {
|
||||
NIANCODE_USER_MODEL_ACCOUNT_ID,
|
||||
normalizeImportedUserModelId,
|
||||
selectUserModelRuntimeAccounts,
|
||||
} from '../../shared/user-model-config';
|
||||
import { getImportedModelProfile } from '../../shared/imported-model-profile';
|
||||
|
||||
const OPENCODE_CONFIG_SCHEMA = 'https://opencode.ai/config.json';
|
||||
const OPENCODE_ENV_PREFIX = 'NIANCODE_OPENCODE';
|
||||
const OPENAI_COMPATIBLE_PACKAGE = '@ai-sdk/openai-compatible';
|
||||
const OPENAI_PACKAGE = '@ai-sdk/openai';
|
||||
const ANTHROPIC_PACKAGE = '@ai-sdk/anthropic';
|
||||
const WORKS_SQUARE_AI_GATEWAY_CREDENTIAL_MODE = 'works_square_ai_gateway';
|
||||
|
||||
const OPENCODE_BUILTIN_PROVIDER_IDS = new Set([
|
||||
'anthropic',
|
||||
'openai',
|
||||
'google',
|
||||
'openrouter',
|
||||
'moonshot',
|
||||
'deepseek',
|
||||
]);
|
||||
|
||||
export interface OpencodeProviderEntry {
|
||||
npm?: string;
|
||||
name?: string;
|
||||
options?: Record<string, unknown>;
|
||||
models?: Record<string, OpencodeProviderModelEntry>;
|
||||
}
|
||||
|
||||
export interface OpencodeMcpServerEntry {
|
||||
type: 'local';
|
||||
command: string[];
|
||||
enabled: boolean;
|
||||
env?: Record<string, string>;
|
||||
}
|
||||
|
||||
type OpencodeModelModality = 'text' | 'audio' | 'image' | 'pdf';
|
||||
|
||||
export interface OpencodeModelLimit {
|
||||
context: number;
|
||||
output: number;
|
||||
}
|
||||
|
||||
export interface OpencodeProviderModelEntry {
|
||||
name: string;
|
||||
modalities?: {
|
||||
input: OpencodeModelModality[];
|
||||
output: OpencodeModelModality[];
|
||||
};
|
||||
limit?: OpencodeModelLimit;
|
||||
}
|
||||
|
||||
export interface OpencodeRuntimeConfig {
|
||||
'$schema': string;
|
||||
enabled_providers: string[];
|
||||
permission: {
|
||||
external_directory: 'ask';
|
||||
};
|
||||
provider: Record<string, OpencodeProviderEntry>;
|
||||
mcp?: Record<string, OpencodeMcpServerEntry>;
|
||||
model?: string;
|
||||
small_model?: string;
|
||||
}
|
||||
|
||||
export interface OpencodeRuntimeConfigResult {
|
||||
config: OpencodeRuntimeConfig;
|
||||
env: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface OpencodeRuntimeConfigSummary {
|
||||
model: string | null;
|
||||
smallModel: string | null;
|
||||
providerIds: string[];
|
||||
enabledProviderIds: string[];
|
||||
providerCount: number;
|
||||
providers: OpencodeProviderSummary[];
|
||||
mcpServerIds: string[];
|
||||
mcpServerCount: number;
|
||||
mcpServers: OpencodeMcpServerSummary[];
|
||||
}
|
||||
|
||||
export interface OpencodeProviderSummary {
|
||||
id: string;
|
||||
name?: string;
|
||||
npm?: string;
|
||||
baseURL?: string;
|
||||
modelIds: string[];
|
||||
imageInputModelIds?: string[];
|
||||
modelLimits?: Record<string, OpencodeModelLimit>;
|
||||
hasApiKey: boolean;
|
||||
apiKeyEnv?: string;
|
||||
headerNames: string[];
|
||||
}
|
||||
|
||||
export interface OpencodeMcpServerSummary {
|
||||
id: string;
|
||||
type: string;
|
||||
enabled: boolean;
|
||||
command: string[];
|
||||
envKeys: string[];
|
||||
}
|
||||
|
||||
export type ResolveOpencodeProviderApiKey = (
|
||||
account: ProviderAccount,
|
||||
opencodeProviderId: string,
|
||||
) => Promise<string | null>;
|
||||
|
||||
export interface BuildOpencodeRuntimeConfigOptions {
|
||||
accounts: ProviderAccount[];
|
||||
defaultAccountId?: string | null;
|
||||
resolveApiKey: ResolveOpencodeProviderApiKey;
|
||||
mcpServers?: Record<string, OpencodeMcpServerEntry>;
|
||||
}
|
||||
|
||||
function envVarNameForAccount(accountId: string): string {
|
||||
const suffix = sanitizeOpencodeProviderId(accountId)
|
||||
.replace(/-/g, '_')
|
||||
.toUpperCase();
|
||||
return `${OPENCODE_ENV_PREFIX}_${suffix}_API_KEY`;
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(baseUrl: string | undefined): string | undefined {
|
||||
const trimmed = baseUrl?.trim().replace(/\/+$/, '');
|
||||
return trimmed || undefined;
|
||||
}
|
||||
|
||||
function normalizeRuntimeBaseUrlForAccount(
|
||||
account: ProviderAccount,
|
||||
baseUrl: string | undefined,
|
||||
): string | undefined {
|
||||
if (!baseUrl || account.metadata?.worksSquareCredentialMode !== WORKS_SQUARE_AI_GATEWAY_CREDENTIAL_MODE) {
|
||||
return baseUrl;
|
||||
}
|
||||
try {
|
||||
const parsed = new URL(baseUrl);
|
||||
if (parsed.pathname === '' || parsed.pathname === '/') {
|
||||
parsed.pathname = '/v1';
|
||||
return parsed.toString().replace(/\/+$/, '');
|
||||
}
|
||||
} catch {
|
||||
return baseUrl;
|
||||
}
|
||||
return baseUrl;
|
||||
}
|
||||
|
||||
function headersForAccount(
|
||||
account: ProviderAccount,
|
||||
apiKeyEnvVarName: string | undefined,
|
||||
): Record<string, string> | undefined {
|
||||
const headers = { ...(account.headers ?? {}) };
|
||||
if (
|
||||
apiKeyEnvVarName
|
||||
&& account.metadata?.worksSquareCredentialMode === WORKS_SQUARE_AI_GATEWAY_CREDENTIAL_MODE
|
||||
&& !Object.keys(headers).some((key) => key.toLowerCase() === 'authorization')
|
||||
) {
|
||||
headers.Authorization = `Bearer {env:${apiKeyEnvVarName}}`;
|
||||
}
|
||||
return Object.keys(headers).length > 0 ? headers : undefined;
|
||||
}
|
||||
|
||||
function protocolForAccount(account: ProviderAccount): ProviderProtocol | undefined {
|
||||
return account.apiProtocol ?? getProviderBackendConfig(account.vendorId)?.api;
|
||||
}
|
||||
|
||||
function packageForAccount(account: ProviderAccount): string | undefined {
|
||||
const hasExplicitRuntimeShape = Boolean(account.baseUrl || account.apiProtocol);
|
||||
if (
|
||||
!hasExplicitRuntimeShape
|
||||
&& OPENCODE_BUILTIN_PROVIDER_IDS.has(account.vendorId)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const protocol = protocolForAccount(account);
|
||||
if (protocol === 'openai-responses') {
|
||||
return OPENAI_PACKAGE;
|
||||
}
|
||||
if (protocol === 'anthropic-messages') {
|
||||
return ANTHROPIC_PACKAGE;
|
||||
}
|
||||
return OPENAI_COMPATIBLE_PACKAGE;
|
||||
}
|
||||
|
||||
function baseUrlForAccount(account: ProviderAccount): string | undefined {
|
||||
return normalizeRuntimeBaseUrlForAccount(account, normalizeBaseUrl(
|
||||
account.baseUrl
|
||||
?? getProviderBackendConfig(account.vendorId)?.baseUrl
|
||||
?? getProviderDefinition(account.vendorId)?.defaultBaseUrl,
|
||||
));
|
||||
}
|
||||
|
||||
function normalizeModelIdForAccount(
|
||||
rawModel: string | undefined,
|
||||
opencodeProviderId: string,
|
||||
account: ProviderAccount,
|
||||
): string | undefined {
|
||||
const trimmed = rawModel?.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const providerPrefix = `${opencodeProviderId}/`;
|
||||
const modelId = trimmed.startsWith(providerPrefix)
|
||||
? trimmed.slice(providerPrefix.length)
|
||||
: trimmed;
|
||||
return account.id === NIANCODE_USER_MODEL_ACCOUNT_ID
|
||||
? normalizeImportedUserModelId(modelId)
|
||||
: modelId;
|
||||
}
|
||||
|
||||
function modelIdsForAccount(account: ProviderAccount, opencodeProviderId: string): string[] {
|
||||
const modelIds: string[] = [];
|
||||
const seen = new Set<string>();
|
||||
const rawModels = [
|
||||
account.model ?? getProviderDefaultModel(account.vendorId),
|
||||
...(account.fallbackModels ?? []),
|
||||
];
|
||||
|
||||
for (const rawModel of rawModels) {
|
||||
const modelId = normalizeModelIdForAccount(rawModel, opencodeProviderId, account);
|
||||
if (!modelId || seen.has(modelId)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(modelId);
|
||||
modelIds.push(modelId);
|
||||
}
|
||||
|
||||
return modelIds;
|
||||
}
|
||||
|
||||
function modelEntryForAccount(account: ProviderAccount, modelId: string): OpencodeProviderModelEntry {
|
||||
const entry: OpencodeProviderModelEntry = { name: modelId };
|
||||
if (account.id !== NIANCODE_USER_MODEL_ACCOUNT_ID) return entry;
|
||||
|
||||
const profile = getImportedModelProfile(modelId);
|
||||
if (profile?.modalities) {
|
||||
entry.modalities = {
|
||||
input: [...profile.modalities.input],
|
||||
output: [...profile.modalities.output],
|
||||
};
|
||||
}
|
||||
if (profile?.limit) {
|
||||
entry.limit = { ...profile.limit };
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
function modelEntryHasImageInput(model: OpencodeProviderModelEntry | undefined): boolean {
|
||||
return model?.modalities?.input.includes('image') ?? false;
|
||||
}
|
||||
|
||||
function accountNeedsApiKey(account: ProviderAccount): boolean {
|
||||
return account.authMode !== 'local' && account.vendorId !== 'ollama';
|
||||
}
|
||||
|
||||
function sortAccountsForSelection(
|
||||
accounts: ProviderAccount[],
|
||||
defaultAccountId?: string | null,
|
||||
): ProviderAccount[] {
|
||||
return [...accounts].sort((left, right) => {
|
||||
if (left.id === defaultAccountId) return -1;
|
||||
if (right.id === defaultAccountId) return 1;
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
export async function buildOpencodeRuntimeConfig(
|
||||
options: BuildOpencodeRuntimeConfigOptions,
|
||||
): Promise<OpencodeRuntimeConfigResult> {
|
||||
const config: OpencodeRuntimeConfig = {
|
||||
'$schema': OPENCODE_CONFIG_SCHEMA,
|
||||
enabled_providers: [],
|
||||
permission: {
|
||||
external_directory: 'ask',
|
||||
},
|
||||
provider: {},
|
||||
};
|
||||
if (options.mcpServers && Object.keys(options.mcpServers).length > 0) {
|
||||
config.mcp = options.mcpServers;
|
||||
}
|
||||
const env: Record<string, string> = {};
|
||||
let selectedModel: string | undefined;
|
||||
|
||||
for (const account of sortAccountsForSelection(
|
||||
selectUserModelRuntimeAccounts(options.accounts),
|
||||
options.defaultAccountId,
|
||||
)) {
|
||||
if (!account.enabled) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const opencodeProviderId = resolveOpencodeProviderId(account);
|
||||
if (config.provider[opencodeProviderId]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const apiKey = await options.resolveApiKey(account, opencodeProviderId);
|
||||
if (!apiKey && accountNeedsApiKey(account)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const providerEntry: OpencodeProviderEntry = {};
|
||||
const providerPackage = packageForAccount(account);
|
||||
if (providerPackage) {
|
||||
providerEntry.npm = providerPackage;
|
||||
providerEntry.name = account.label;
|
||||
}
|
||||
|
||||
const entryOptions: Record<string, unknown> = {};
|
||||
const baseURL = providerPackage ? baseUrlForAccount(account) : normalizeBaseUrl(account.baseUrl);
|
||||
if (baseURL) {
|
||||
entryOptions.baseURL = baseURL;
|
||||
}
|
||||
let envVarName: string | undefined;
|
||||
if (apiKey) {
|
||||
envVarName = envVarNameForAccount(account.id);
|
||||
env[envVarName] = apiKey;
|
||||
entryOptions.apiKey = `{env:${envVarName}}`;
|
||||
}
|
||||
const headers = headersForAccount(account, envVarName);
|
||||
if (headers) {
|
||||
entryOptions.headers = headers;
|
||||
}
|
||||
if (Object.keys(entryOptions).length > 0) {
|
||||
providerEntry.options = entryOptions;
|
||||
}
|
||||
|
||||
const modelIds = modelIdsForAccount(account, opencodeProviderId);
|
||||
if (modelIds.length > 0) {
|
||||
providerEntry.models = Object.fromEntries(
|
||||
modelIds.map((modelId) => [modelId, modelEntryForAccount(account, modelId)]),
|
||||
);
|
||||
}
|
||||
|
||||
const primaryModelId = modelIds[0];
|
||||
|
||||
config.provider[opencodeProviderId] = providerEntry;
|
||||
|
||||
const modelRef = primaryModelId ? `${opencodeProviderId}/${primaryModelId}` : undefined;
|
||||
if (modelRef && (account.id === options.defaultAccountId || !selectedModel)) {
|
||||
selectedModel = modelRef;
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedModel) {
|
||||
config.model = selectedModel;
|
||||
}
|
||||
config.enabled_providers = Object.keys(config.provider);
|
||||
|
||||
return { config, env };
|
||||
}
|
||||
|
||||
async function resolveNianCodeApiKey(
|
||||
account: ProviderAccount,
|
||||
_opencodeProviderId: string,
|
||||
): Promise<string | null> {
|
||||
return await getApiKey(account.id);
|
||||
}
|
||||
|
||||
export interface BuildOpencodeRuntimeConfigFromNianCodeProvidersOptions {
|
||||
mcpServers?: Record<string, OpencodeMcpServerEntry>;
|
||||
}
|
||||
|
||||
export async function buildOpencodeRuntimeConfigFromNianCodeProviders(
|
||||
options: BuildOpencodeRuntimeConfigFromNianCodeProvidersOptions = {},
|
||||
): Promise<OpencodeRuntimeConfigResult> {
|
||||
const providerService = getProviderService();
|
||||
const [accounts, defaultAccountId] = await Promise.all([
|
||||
providerService.listAccounts(),
|
||||
providerService.getDefaultAccountId(),
|
||||
]);
|
||||
|
||||
return buildOpencodeRuntimeConfig({
|
||||
accounts,
|
||||
defaultAccountId,
|
||||
resolveApiKey: resolveNianCodeApiKey,
|
||||
mcpServers: options.mcpServers,
|
||||
});
|
||||
}
|
||||
|
||||
export function summarizeOpencodeRuntimeConfig(
|
||||
result: OpencodeRuntimeConfigResult,
|
||||
): OpencodeRuntimeConfigSummary {
|
||||
const providerIds = Object.keys(result.config.provider);
|
||||
const mcp = result.config.mcp ?? {};
|
||||
const mcpServerIds = Object.keys(mcp);
|
||||
const mcpServers = mcpServerIds.map((serverId) => {
|
||||
const server = mcp[serverId];
|
||||
return {
|
||||
id: serverId,
|
||||
type: server.type,
|
||||
enabled: server.enabled,
|
||||
command: [...server.command],
|
||||
envKeys: Object.keys(server.env ?? {}).sort(),
|
||||
};
|
||||
});
|
||||
const providers = providerIds.map((providerId) => {
|
||||
const provider = result.config.provider[providerId];
|
||||
const options = provider?.options ?? {};
|
||||
const rawApiKey = typeof options.apiKey === 'string' ? options.apiKey : undefined;
|
||||
const apiKeyEnv = rawApiKey?.match(/^\{env:([^}]+)\}$/)?.[1];
|
||||
const headers = options.headers && typeof options.headers === 'object' && !Array.isArray(options.headers)
|
||||
? options.headers as Record<string, unknown>
|
||||
: {};
|
||||
const models = provider?.models ?? {};
|
||||
const modelLimits: Record<string, OpencodeModelLimit> = Object.fromEntries(
|
||||
Object.entries(models).flatMap(([modelId, model]) => (
|
||||
model.limit ? [[modelId, { ...model.limit }] as const] : []
|
||||
)),
|
||||
);
|
||||
return {
|
||||
id: providerId,
|
||||
...(provider?.name ? { name: provider.name } : {}),
|
||||
...(provider?.npm ? { npm: provider.npm } : {}),
|
||||
...(typeof options.baseURL === 'string' ? { baseURL: options.baseURL } : {}),
|
||||
modelIds: Object.keys(models),
|
||||
imageInputModelIds: Object.entries(models).flatMap(([modelId, model]) => (
|
||||
modelEntryHasImageInput(model) ? [modelId] : []
|
||||
)),
|
||||
...(Object.keys(modelLimits).length > 0 ? { modelLimits } : {}),
|
||||
hasApiKey: Boolean(rawApiKey),
|
||||
...(apiKeyEnv ? { apiKeyEnv } : {}),
|
||||
headerNames: Object.keys(headers),
|
||||
};
|
||||
});
|
||||
return {
|
||||
model: result.config.model ?? null,
|
||||
smallModel: result.config.small_model ?? null,
|
||||
providerIds,
|
||||
enabledProviderIds: result.config.enabled_providers ?? providerIds,
|
||||
providerCount: providerIds.length,
|
||||
providers,
|
||||
mcpServerIds,
|
||||
mcpServerCount: mcpServerIds.length,
|
||||
mcpServers,
|
||||
};
|
||||
}
|
||||
|
||||
export async function buildOpencodeRuntimeConfigSummaryFromNianCodeProviders(
|
||||
options: BuildOpencodeRuntimeConfigFromNianCodeProvidersOptions = {},
|
||||
): Promise<OpencodeRuntimeConfigSummary> {
|
||||
return summarizeOpencodeRuntimeConfig(await buildOpencodeRuntimeConfigFromNianCodeProviders(options));
|
||||
}
|
||||
Reference in New Issue
Block a user