feat: add Pi provider managed resources
This commit is contained in:
529
electron/coding-runtime/pi/provider-config.ts
Normal file
529
electron/coding-runtime/pi/provider-config.ts
Normal file
@@ -0,0 +1,529 @@
|
||||
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';
|
||||
|
||||
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);
|
||||
await atomicWriteJson(filePath, catalog.modelsFile);
|
||||
}
|
||||
|
||||
function replaceOpenCodeEnvReferences(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)] = replaceOpenCodeEnvReferences(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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user