Files
makelore/electron/coding-runtime/pi/provider-config.ts

542 lines
19 KiB
TypeScript

import type {
ModelSummary,
ProviderAccount,
ProviderModelEntry,
ProviderProtocol,
ProviderSecret,
} from '../../shared/providers/types';
import {
getProviderBackendConfig,
getProviderDefaultModel,
getProviderDefinition,
} from '../../shared/providers/registry';
import type { ProductModelRef } from '../contracts';
import { atomicWriteJson } from '../../coding-projects/atomic-json';
import {
NIANCODE_USER_MODEL_ACCOUNT_ID,
normalizeImportedUserModelId,
selectUserModelRuntimeAccounts,
} from '../../../shared/user-model-config';
import { getImportedModelProfile } from '../../../shared/imported-model-profile';
const PI_ENV_PREFIX = 'MAKELORE_PI';
const WORKS_SQUARE_AI_GATEWAY_CREDENTIAL_MODE = 'works_square_ai_gateway';
const WORKS_SQUARE_AI_GATEWAY_PROXY_CREDENTIAL_MODE = 'works_square_ai_gateway_proxy';
const providerCatalogWriteTails = new Map<string, Promise<void>>();
export const PI_PROVIDER_APIS = [
'openai-completions',
'openai-responses',
'anthropic-messages',
'google-generative-ai',
] as const;
export type PiProviderApi = (typeof PI_PROVIDER_APIS)[number];
export interface PiProviderModelDescriptor {
id: string;
name: string;
input: Array<'text' | 'image'>;
reasoning: boolean;
contextWindow?: number;
maxOutputTokens?: number;
compat?: {
thinkingFormat: 'openrouter';
sessionAffinityFormat: 'openrouter';
};
}
export interface PiProviderDescriptor {
accountId: string;
runtimeProviderId: string;
api: PiProviderApi;
baseUrl?: string;
headers: Record<string, string>;
apiKeyEnv?: string;
models: PiProviderModelDescriptor[];
}
export interface PiModelsFile {
providers: Record<string, {
baseUrl?: string;
api: PiProviderApi;
apiKey?: string;
headers?: Record<string, string>;
models: Array<{
id: string;
name: string;
reasoning: boolean;
input: Array<'text' | 'image'>;
contextWindow?: number;
maxTokens?: number;
cost: { input: number; output: number; cacheRead: number; cacheWrite: number };
compat?: PiProviderModelDescriptor['compat'];
}>;
}>;
}
export interface PiProviderCatalogSummary {
providerCount: number;
providers: Array<{
runtimeProviderId: string;
api: PiProviderApi;
baseUrl?: string;
modelIds: string[];
imageInputModelIds: string[];
headerNames: string[];
hasCredentialReference: boolean;
}>;
}
export interface PiProviderCatalogResult {
descriptors: PiProviderDescriptor[];
modelsFile: PiModelsFile;
summary: PiProviderCatalogSummary;
}
export interface BuildPiProviderCatalogOptions {
accounts: readonly ProviderAccount[];
modelSummaries?: readonly ModelSummary[];
}
export async function buildPiProviderCatalogFromProviderService(
modelSummaries: readonly ModelSummary[] = [],
): Promise<PiProviderCatalogResult> {
const { getProviderService } = await import('../../services/providers/provider-service');
return buildPiProviderCatalog({
accounts: await getProviderService().listAccounts(),
modelSummaries,
});
}
export interface PiProviderSelection {
accountId: string;
runtimeProviderId: string;
modelId: string;
thinkingLevel: ProductModelRef['thinkingLevel'];
input: Array<'text' | 'image'>;
contextWindow?: number;
maxOutputTokens?: number;
}
export interface BuildPiWorkerCredentialProjectionOptions {
account: ProviderAccount;
descriptor: PiProviderDescriptor;
resolveCredential: (account: ProviderAccount) => Promise<string | null>;
localProxyCredential?: string;
}
export interface PiWorkerCredentialProjection {
env: Record<string, string>;
sensitiveValues: string[];
}
export interface PiWorkerCredentialProjectionSummary {
envKeys: string[];
sensitiveValueCount: number;
}
export class PiProviderConfigError extends Error {
constructor(
public readonly code: 'PROVIDER_INVALID' | 'PROVIDER_AUTH_REQUIRED' | 'MODEL_UNAVAILABLE',
message: string,
) {
super(message);
}
}
export function credentialValueForProviderSecret(secret: ProviderSecret | null): string | null {
if (!secret) return null;
if (secret.type === 'api_key') return secret.apiKey.trim() || null;
if (secret.type === 'oauth') return secret.accessToken.trim() || null;
return secret.apiKey?.trim() || null;
}
export async function resolvePiProviderCredentialFromSecretStore(
account: ProviderAccount,
): Promise<string | null> {
const { getProviderSecret } = await import('../../services/secrets/secret-store');
return credentialValueForProviderSecret(await getProviderSecret(account.id));
}
function accountHex(accountId: string): string {
const normalized = accountId.trim();
if (!normalized) throw new PiProviderConfigError('PROVIDER_INVALID', 'Provider account id is required');
return Buffer.from(normalized, 'utf8').toString('hex');
}
export function resolvePiRuntimeProviderId(accountId: string): string {
return `makelore-account-${accountHex(accountId)}`;
}
function apiKeyEnvForAccount(accountId: string): string {
return `${PI_ENV_PREFIX}_ACCOUNT_${accountHex(accountId).toUpperCase()}_API_KEY`;
}
function headerEnvForAccount(accountId: string, headerName: string): string {
const nameHex = Buffer.from(headerName.toLowerCase(), 'utf8').toString('hex').toUpperCase();
return `${PI_ENV_PREFIX}_ACCOUNT_${accountHex(accountId).toUpperCase()}_HEADER_${nameHex}`;
}
function normalizeBaseUrl(value: string | undefined): string | undefined {
const normalized = value?.trim().replace(/\/+$/, '');
return normalized || undefined;
}
function normalizeWorksGatewayBaseUrl(value: string | undefined): string | undefined {
const normalized = normalizeBaseUrl(value);
if (!normalized) return undefined;
try {
const parsed = new URL(normalized);
const pathname = parsed.pathname.replace(/\/+$/, '');
if (!pathname || pathname === '/') parsed.pathname = '/v1';
return parsed.toString().replace(/\/+$/, '');
} catch {
return normalized;
}
}
function defaultBaseUrlForVendor(vendorId: string): string | undefined {
if (vendorId === 'anthropic') return 'https://api.anthropic.com';
if (vendorId === 'google') return 'https://generativelanguage.googleapis.com/v1beta';
return getProviderBackendConfig(vendorId)?.baseUrl
?? getProviderDefinition(vendorId)?.defaultBaseUrl;
}
function baseUrlForAccount(account: ProviderAccount): string | undefined {
const baseUrl = account.baseUrl
?? account.metadata?.worksSquareOneApiBaseUrl
?? defaultBaseUrlForVendor(account.vendorId);
return account.metadata?.worksSquareCredentialMode === WORKS_SQUARE_AI_GATEWAY_CREDENTIAL_MODE
? normalizeWorksGatewayBaseUrl(baseUrl)
: normalizeBaseUrl(baseUrl);
}
function protocolForAccount(account: ProviderAccount): ProviderProtocol | 'google-generative-ai' | undefined {
if (account.apiProtocol) return account.apiProtocol;
const configured = getProviderBackendConfig(account.vendorId)?.api;
if (configured) return configured;
if (account.vendorId === 'anthropic') return 'anthropic-messages';
if (account.vendorId === 'google') return 'google-generative-ai';
if (account.vendorId === 'openai') return 'openai-responses';
if (account.vendorId === 'openrouter') return 'openrouter';
return undefined;
}
function mapProtocol(protocol: ReturnType<typeof protocolForAccount>): {
api: PiProviderApi;
compat?: PiProviderModelDescriptor['compat'];
} {
if (protocol === 'openrouter') {
return {
api: 'openai-completions',
compat: {
thinkingFormat: 'openrouter',
sessionAffinityFormat: 'openrouter',
},
};
}
if (protocol && PI_PROVIDER_APIS.includes(protocol as PiProviderApi)) {
return { api: protocol as PiProviderApi };
}
throw new PiProviderConfigError('PROVIDER_INVALID', 'Provider protocol is not supported by Pi');
}
function normalizeModelId(rawModelId: string | undefined, account: ProviderAccount): string | undefined {
const modelId = rawModelId?.trim();
if (!modelId) return undefined;
const runtimePrefix = `${resolvePiRuntimeProviderId(account.id)}/`;
const unqualified = modelId.startsWith(runtimePrefix)
? modelId.slice(runtimePrefix.length)
: modelId;
return account.id === NIANCODE_USER_MODEL_ACCOUNT_ID
? normalizeImportedUserModelId(unqualified)
: unqualified;
}
function backendModelEntries(account: ProviderAccount): Map<string, ProviderModelEntry> {
return new Map((getProviderBackendConfig(account.vendorId)?.models ?? []).map((model) => [model.id, model]));
}
function modelIdsForAccount(account: ProviderAccount): string[] {
const seen = new Set<string>();
const result: string[] = [];
const configuredModels = getProviderBackendConfig(account.vendorId)?.models ?? [];
for (const rawModelId of [
account.model ?? getProviderDefaultModel(account.vendorId),
...(account.fallbackModels ?? []),
...(account.metadata?.customModels ?? []),
...configuredModels.map((model) => model.id),
]) {
const modelId = normalizeModelId(rawModelId, account);
if (!modelId || seen.has(modelId)) continue;
seen.add(modelId);
result.push(modelId);
}
return result;
}
function finitePositiveInteger(value: unknown): number | undefined {
return typeof value === 'number' && Number.isSafeInteger(value) && value > 0
? value
: undefined;
}
function modelDescriptor(
account: ProviderAccount,
modelId: string,
summaries: readonly ModelSummary[],
compat: PiProviderModelDescriptor['compat'],
backendModels: Map<string, ProviderModelEntry>,
): PiProviderModelDescriptor {
const summary = summaries.find((candidate) => (
candidate.id === modelId
&& (candidate.accountId === account.id || (!candidate.accountId && candidate.vendorId === account.vendorId))
));
const backend = backendModels.get(modelId);
const profile = getImportedModelProfile(modelId);
const backendInput = Array.isArray(backend?.input)
? backend.input.filter((input): input is 'text' | 'image' => input === 'text' || input === 'image')
: [];
const supportsImage = Boolean(
summary?.supportsVision
|| profile?.modalities.input.includes('image')
|| backendInput.includes('image'),
);
const contextWindow = finitePositiveInteger(summary?.contextWindow)
?? finitePositiveInteger(profile?.limit?.context)
?? finitePositiveInteger(backend?.contextWindow);
const maxOutputTokens = finitePositiveInteger(profile?.limit?.output)
?? finitePositiveInteger(backend?.maxTokens);
return {
id: modelId,
name: summary?.name || (typeof backend?.name === 'string' && backend.name.trim()) || modelId,
input: supportsImage ? ['text', 'image'] : ['text'],
reasoning: summary?.supportsReasoning === true || backend?.reasoning === true,
...(contextWindow ? { contextWindow } : {}),
...(maxOutputTokens ? { maxOutputTokens } : {}),
...(compat ? { compat } : {}),
};
}
function rawHeadersForAccount(account: ProviderAccount): Record<string, string> {
const headers: Record<string, string> = {};
for (const [name, value] of Object.entries({
...(getProviderBackendConfig(account.vendorId)?.headers ?? {}),
...(account.headers ?? {}),
})) {
const duplicate = Object.keys(headers).find((candidate) => candidate.toLowerCase() === name.toLowerCase());
if (duplicate) delete headers[duplicate];
headers[name] = value;
}
return headers;
}
function headerNamesForAccount(account: ProviderAccount): string[] {
const names = Object.keys(rawHeadersForAccount(account));
if (
account.metadata?.worksSquareCredentialMode === WORKS_SQUARE_AI_GATEWAY_CREDENTIAL_MODE
&& !names.some((name) => name.toLowerCase() === 'authorization')
) {
names.push('Authorization');
}
return names.sort((left, right) => left.toLowerCase().localeCompare(right.toLowerCase()));
}
function descriptorForAccount(
account: ProviderAccount,
summaries: readonly ModelSummary[],
): PiProviderDescriptor {
const runtimeProviderId = resolvePiRuntimeProviderId(account.id);
const mapping = mapProtocol(protocolForAccount(account));
const baseUrl = baseUrlForAccount(account);
if (!baseUrl) {
throw new PiProviderConfigError(
'PROVIDER_INVALID',
`Provider account ${account.id} has no runtime base URL`,
);
}
const apiKeyEnv = apiKeyEnvForAccount(account.id);
const models = modelIdsForAccount(account).map((modelId) => modelDescriptor(
account,
modelId,
summaries,
mapping.compat,
backendModelEntries(account),
));
if (models.length === 0) {
throw new PiProviderConfigError(
'PROVIDER_INVALID',
`Provider account ${account.id} has no configured model`,
);
}
const headers = Object.fromEntries(headerNamesForAccount(account).map((headerName) => [
headerName,
`$${headerEnvForAccount(account.id, headerName)}`,
]));
return {
accountId: account.id,
runtimeProviderId,
api: mapping.api,
baseUrl,
headers,
apiKeyEnv,
models,
};
}
function modelsFileForDescriptors(descriptors: readonly PiProviderDescriptor[]): PiModelsFile {
return {
providers: Object.fromEntries(descriptors.map((descriptor) => [
descriptor.runtimeProviderId,
{
...(descriptor.baseUrl ? { baseUrl: descriptor.baseUrl } : {}),
api: descriptor.api,
...(descriptor.apiKeyEnv ? { apiKey: `$${descriptor.apiKeyEnv}` } : {}),
...(Object.keys(descriptor.headers).length > 0 ? { headers: { ...descriptor.headers } } : {}),
models: descriptor.models.map((model) => ({
id: model.id,
name: model.name,
reasoning: model.reasoning,
input: [...model.input],
...(model.contextWindow ? { contextWindow: model.contextWindow } : {}),
...(model.maxOutputTokens ? { maxTokens: model.maxOutputTokens } : {}),
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
...(model.compat ? { compat: { ...model.compat } } : {}),
})),
},
])),
};
}
export function buildPiProviderCatalog(
options: BuildPiProviderCatalogOptions,
): PiProviderCatalogResult {
const accounts = selectUserModelRuntimeAccounts([...options.accounts]).filter((account) => account.enabled);
const accountIds = new Set<string>();
const descriptors = accounts.map((account) => {
if (accountIds.has(account.id)) {
throw new PiProviderConfigError('PROVIDER_INVALID', `Duplicate Provider account id: ${account.id}`);
}
accountIds.add(account.id);
return descriptorForAccount(account, options.modelSummaries ?? []);
});
return {
descriptors,
modelsFile: modelsFileForDescriptors(descriptors),
summary: {
providerCount: descriptors.length,
providers: descriptors.map((descriptor) => ({
runtimeProviderId: descriptor.runtimeProviderId,
api: descriptor.api,
...(descriptor.baseUrl ? { baseUrl: descriptor.baseUrl } : {}),
modelIds: descriptor.models.map((model) => model.id),
imageInputModelIds: descriptor.models
.filter((model) => model.input.includes('image'))
.map((model) => model.id),
headerNames: Object.keys(descriptor.headers).sort(),
hasCredentialReference: Boolean(descriptor.apiKeyEnv),
})),
},
};
}
export function selectPiProviderModel(
catalog: PiProviderCatalogResult,
modelRef: ProductModelRef,
): PiProviderSelection {
const descriptor = catalog.descriptors.find((candidate) => candidate.accountId === modelRef.accountId);
const model = descriptor?.models.find((candidate) => candidate.id === modelRef.modelId);
if (!descriptor || !model) {
throw new PiProviderConfigError(
'MODEL_UNAVAILABLE',
'The selected Provider account or model is unavailable',
);
}
return {
accountId: descriptor.accountId,
runtimeProviderId: descriptor.runtimeProviderId,
modelId: model.id,
thinkingLevel: modelRef.thinkingLevel,
input: [...model.input],
...(model.contextWindow ? { contextWindow: model.contextWindow } : {}),
...(model.maxOutputTokens ? { maxOutputTokens: model.maxOutputTokens } : {}),
};
}
export async function writePiProviderCatalog(
filePath: string,
catalog: PiProviderCatalogResult,
modelRef?: ProductModelRef,
): Promise<void> {
if (modelRef) selectPiProviderModel(catalog, modelRef);
const previous = providerCatalogWriteTails.get(filePath) ?? Promise.resolve();
const write = previous
.catch(() => undefined)
.then(async () => await atomicWriteJson(filePath, catalog.modelsFile));
providerCatalogWriteTails.set(filePath, write);
try {
await write;
} finally {
if (providerCatalogWriteTails.get(filePath) === write) {
providerCatalogWriteTails.delete(filePath);
}
}
}
function replaceCredentialEnvReferences(value: string, credential: string | null): string {
if (!value.includes('{env:')) return value;
if (!credential) {
throw new PiProviderConfigError('PROVIDER_AUTH_REQUIRED', 'Provider credential is unavailable');
}
return value.replace(/\{env:[^}]+\}/g, credential);
}
export async function buildPiWorkerCredentialProjection(
options: BuildPiWorkerCredentialProjectionOptions,
): Promise<PiWorkerCredentialProjection> {
if (options.account.id !== options.descriptor.accountId) {
throw new PiProviderConfigError('PROVIDER_INVALID', 'Provider account does not match descriptor');
}
const useLocalProxy = options.account.metadata?.worksSquareCredentialMode
=== WORKS_SQUARE_AI_GATEWAY_PROXY_CREDENTIAL_MODE;
const resolved = useLocalProxy
? options.localProxyCredential?.trim() || null
: await options.resolveCredential(options.account);
const credential = resolved?.trim()
|| (options.account.authMode === 'local' ? 'local-provider' : null);
if (!credential) {
throw new PiProviderConfigError('PROVIDER_AUTH_REQUIRED', 'Provider credential is unavailable');
}
const env: Record<string, string> = {};
if (options.descriptor.apiKeyEnv) env[options.descriptor.apiKeyEnv] = credential;
const accountHeaders = rawHeadersForAccount(options.account);
for (const headerName of Object.keys(options.descriptor.headers)) {
const sourceEntry = Object.entries(accountHeaders).find(([name]) => (
name.toLowerCase() === headerName.toLowerCase()
));
let value = sourceEntry?.[1];
if (!value && headerName.toLowerCase() === 'authorization'
&& options.account.metadata?.worksSquareCredentialMode === WORKS_SQUARE_AI_GATEWAY_CREDENTIAL_MODE) {
value = `Bearer ${credential}`;
}
if (value === undefined) {
throw new PiProviderConfigError('PROVIDER_INVALID', `Provider header is unavailable: ${headerName}`);
}
env[headerEnvForAccount(options.account.id, headerName)] = replaceCredentialEnvReferences(value, credential);
}
return {
env,
sensitiveValues: [...new Set(Object.values(env).filter(Boolean))],
};
}
export function summarizePiWorkerCredentialProjection(
projection: PiWorkerCredentialProjection,
): PiWorkerCredentialProjectionSummary {
return {
envKeys: Object.keys(projection.env).sort(),
sensitiveValueCount: projection.sensitiveValues.length,
};
}