UNPKG

@earendil-works/pi-coding-agent

Version:

Coding agent CLI with read, bash, edit, write tools and session management

375 lines 18.2 kB
import { lazyStream, } from "@earendil-works/pi-ai"; import { getApiProvider } from "@earendil-works/pi-ai/compat"; import { clearConfigValueCache, getConfigValueEnvVarNames, isCommandConfigValue, isConfigValueConfigured, resolveConfigValueOrThrow, resolveHeadersOrThrow, } from "./resolve-config-value.js"; export const clearApiKeyCache = clearConfigValueCache; function mergeCompat(base, override) { if (!override) return base; const merged = { ...base, ...override }; const baseNested = base; const overrideNested = override; const mergedNested = merged; for (const key of ["openRouterRouting", "vercelGatewayRouting", "chatTemplateKwargs"]) { const baseValue = baseNested?.[key]; const overrideValue = overrideNested[key]; if ((typeof baseValue === "object" && baseValue !== null) || (typeof overrideValue === "object" && overrideValue !== null)) { mergedNested[key] = { ...baseValue, ...overrideValue }; } } return merged; } function applyModelOverride(model, override) { return { ...model, name: override.name ?? model.name, reasoning: override.reasoning ?? model.reasoning, thinkingLevelMap: override.thinkingLevelMap ? { ...model.thinkingLevelMap, ...override.thinkingLevelMap } : model.thinkingLevelMap, input: override.input ?? model.input, cost: override.cost ? { input: override.cost.input ?? model.cost.input, output: override.cost.output ?? model.cost.output, cacheRead: override.cost.cacheRead ?? model.cost.cacheRead, cacheWrite: override.cost.cacheWrite ?? model.cost.cacheWrite, tiers: override.cost.tiers ?? model.cost.tiers, } : model.cost, contextWindow: override.contextWindow ?? model.contextWindow, maxTokens: override.maxTokens ?? model.maxTokens, compat: mergeCompat(model.compat, override.compat), }; } function modelFromJson(providerId, definition, providerConfig, defaults) { const api = definition.api ?? providerConfig.api ?? defaults?.api; if (!api) { throw new Error(`Provider ${providerId}, model ${definition.id}: no "api" specified. Set at provider or model level.`); } const baseUrl = definition.baseUrl ?? providerConfig.baseUrl ?? defaults?.baseUrl; if (!baseUrl) throw new Error(`Provider ${providerId}: "baseUrl" is required when defining custom models.`); if (definition.contextWindow !== undefined && definition.contextWindow <= 0) { throw new Error(`Provider ${providerId}, model ${definition.id}: invalid contextWindow`); } if (definition.maxTokens !== undefined && definition.maxTokens <= 0) { throw new Error(`Provider ${providerId}, model ${definition.id}: invalid maxTokens`); } return { id: definition.id, name: definition.name ?? definition.id, api: api, provider: providerId, baseUrl, reasoning: definition.reasoning ?? false, thinkingLevelMap: definition.thinkingLevelMap, input: (definition.input ?? ["text"]), cost: definition.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: definition.contextWindow ?? 128000, maxTokens: definition.maxTokens ?? 16384, headers: undefined, compat: mergeCompat(providerConfig.compat, definition.compat), }; } function applyModelsJson(providerId, baseModels, config) { if (!config) return [...baseModels]; if (config.oauth && !config.baseUrl) { throw new Error(`Provider ${providerId}: "baseUrl" is required when "oauth" is set.`); } const hasOverrides = config.modelOverrides && Object.keys(config.modelOverrides).length > 0; if (!config.models?.length && !config.baseUrl && !config.headers && !config.compat && !hasOverrides && !config.apiKey && !config.oauth && config.authHeader === undefined) { throw new Error(`Provider ${providerId}: must specify "baseUrl", "headers", "compat", "modelOverrides", or "models".`); } const models = baseModels.map((model) => ({ ...model, baseUrl: config.oauth === "radius" ? model.baseUrl : (config.baseUrl ?? model.baseUrl), compat: mergeCompat(model.compat, config.compat), })); for (const definition of config.models ?? []) { const existingIndex = models.findIndex((model) => model.id === definition.id); const defaults = existingIndex >= 0 ? models[existingIndex] : models[0]; const model = modelFromJson(providerId, definition, config, defaults); if (existingIndex >= 0) models[existingIndex] = model; else models.push(model); } return models; } function applyExtension(providerId, models, config) { if (!config) return [...models]; if (!config.models) { return config.baseUrl ? models.map((model) => ({ ...model, baseUrl: config.baseUrl })) : [...models]; } return config.models.map((definition) => { const defaults = models.find((model) => model.id === definition.id) ?? models[0]; const api = definition.api ?? config.api ?? defaults?.api; if (!api) { throw new Error(`Provider ${providerId}, model ${definition.id}: no "api" specified. Set at provider or model level.`); } const baseUrl = definition.baseUrl ?? config.baseUrl ?? defaults?.baseUrl; if (!baseUrl) throw new Error(`Provider ${providerId}: "baseUrl" is required when defining custom models.`); return { ...definition, api, provider: providerId, baseUrl, headers: undefined, }; }); } function adaptOAuth(config) { return { name: config.name, login: async (callbacks) => { const credential = await config.login({ onAuth: (info) => callbacks.notify({ type: "auth_url", ...info }), onDeviceCode: (info) => callbacks.notify({ type: "device_code", ...info }), onPrompt: (prompt) => callbacks.prompt({ type: "text", ...prompt }), onProgress: (message) => callbacks.notify({ type: "progress", message }), onManualCodeInput: () => callbacks.prompt({ type: "manual_code", message: "Paste the authorization code" }), onSelect: (prompt) => callbacks.prompt({ type: "select", ...prompt }), signal: callbacks.signal, }); return { ...credential, type: "oauth" }; }, refresh: async (credential) => ({ ...(await config.refreshToken(credential)), type: "oauth" }), toAuth: async (credential) => ({ apiKey: config.getApiKey(credential) }), }; } function withConfiguredAuth(auth, headers, authHeader) { let mergedHeaders = auth.headers || headers ? { ...auth.headers, ...headers } : undefined; if (authHeader) { if (!auth.apiKey) throw new Error("authHeader requires a resolved API key"); mergedHeaders = { ...mergedHeaders, Authorization: `Bearer ${auth.apiKey}` }; } return { ...auth, headers: mergedHeaders }; } function configuredApiKey(config, extension) { return extension?.apiKey ?? config?.apiKey; } function configuredHeaders(config, extension) { if (!config?.headers && !extension?.headers) return undefined; return { ...config?.headers, ...extension?.headers }; } async function configContextEnv(values, ctx, explicit) { const env = { ...explicit }; for (const name of new Set(values.flatMap(getConfigValueEnvVarNames))) { if (env[name] !== undefined) continue; const value = await ctx.env(name); if (value !== undefined) env[name] = value; } return Object.keys(env).length > 0 ? env : undefined; } function composeApiKeyAuth(providerId, base, config, extension) { const inherited = base?.auth.apiKey; const rawKey = configuredApiKey(config, extension); const oauth = extension?.oauth ?? base?.auth.oauth; // OAuth-only providers get no fabricated API-key login method. if (!inherited && rawKey === undefined && oauth) return undefined; const rawHeaders = configuredHeaders(config, extension); const authHeader = extension?.authHeader ?? config?.authHeader ?? false; return { name: inherited?.name ?? "API key", login: inherited?.login ?? (async (interaction) => ({ type: "api_key", key: await interaction.prompt({ type: "secret", message: "Enter API key" }), })), check: async (input) => { if (input.credential) { if (inherited?.check) return inherited.check(input); if (input.credential.key) return { type: "api_key", source: "stored credential" }; const resolved = await inherited?.resolve(input); return resolved ? { type: "api_key", source: resolved.source } : undefined; } if (rawKey !== undefined) { if (isCommandConfigValue(rawKey)) return { type: "api_key", source: "configured API key" }; const envNames = getConfigValueEnvVarNames(rawKey); for (const name of envNames) { if ((await input.ctx.env(name)) === undefined) return undefined; } return { type: "api_key", source: "configured API key" }; } if (inherited?.check) return inherited.check(input); const resolved = await inherited?.resolve(input); return resolved ? { type: "api_key", source: resolved.source } : undefined; }, resolve: async (input) => { let result; if (input.credential) { result = inherited ? await inherited.resolve(input) : input.credential.key ? { auth: { apiKey: input.credential.key }, env: input.credential.env, source: "stored credential" } : undefined; } else if (rawKey !== undefined) { const env = await configContextEnv([rawKey], input.ctx); const key = resolveConfigValueOrThrow(rawKey, `API key for provider "${providerId}"`, env); result = inherited ? await inherited.resolve({ ...input, credential: { type: "api_key", key } }) : { auth: { apiKey: key }, source: "configured API key" }; } else { result = await inherited?.resolve(input); } if (!result) return undefined; const explicitEnv = { ...(input.credential?.env ?? {}), ...(result.env ?? {}) }; const headerEnv = await configContextEnv(Object.values(rawHeaders ?? {}), input.ctx, explicitEnv); const headers = resolveHeadersOrThrow(rawHeaders, `provider "${providerId}"`, headerEnv); return { ...result, auth: withConfiguredAuth(result.auth, headers, authHeader) }; }, }; } function composeOAuthAuth(providerId, base, config, extension) { const oauth = extension?.oauth ? adaptOAuth(extension.oauth) : base?.auth.oauth; if (!oauth) return undefined; const rawHeaders = configuredHeaders(config, extension); const authHeader = extension?.authHeader ?? config?.authHeader ?? false; return { ...oauth, toAuth: async (credential) => { const auth = await oauth.toAuth(credential); const env = credential.env; const headers = resolveHeadersOrThrow(rawHeaders, `provider "${providerId}"`, typeof env === "object" && env !== null ? env : undefined); return withConfiguredAuth(auth, headers, authHeader); }, }; } function rawModelHeaders(model, config, extension) { const definition = config?.models?.find((entry) => entry.id === model.id); const extensionModel = extension?.models?.find((entry) => entry.id === model.id); const headers = { ...config?.modelOverrides?.[model.id]?.headers, ...definition?.headers, ...extensionModel?.headers, }; return Object.keys(headers).length > 0 ? headers : undefined; } export function validateExtensionProvider(providerId, base, modelsConfig, extension) { if (extension.streamSimple && !extension.api) { throw new Error(`Provider ${providerId}: "api" is required when registering streamSimple.`); } applyExtension(providerId, applyModelsJson(providerId, base?.getModels() ?? [], modelsConfig), extension); } /** Compose built-in, models.json, and extension layers without reading credentials. */ export function composeModelProvider(providerId, base, modelConfig, extension) { const config = modelConfig.getProvider(providerId); let extensionOAuthCredential; let refreshedExtensionModels; const currentExtension = () => extension && refreshedExtensionModels ? { ...extension, models: refreshedExtensionModels } : extension; // models.json modelOverrides are the topmost user-config layer: they apply once, // after custom-model upserts, extension model replacement, and legacy OAuth projection. const getModels = () => { let models = applyExtension(providerId, applyModelsJson(providerId, base?.getModels() ?? [], config), currentExtension()); if (extensionOAuthCredential && extension?.oauth?.modifyModels) { models = extension.oauth.modifyModels(models, extensionOAuthCredential); } return models.map((model) => { const override = config?.modelOverrides?.[model.id]; return override ? applyModelOverride(model, override) : model; }); }; // Validate eagerly so registration/reload reports structural errors immediately. getModels(); const apiKey = composeApiKeyAuth(providerId, base, config, extension); const oauth = composeOAuthAuth(providerId, base, config, extension); if (!apiKey && !oauth) throw new Error(`Provider ${providerId}: no authentication method configured.`); const supportsBaseApi = (model) => base?.getModels().some((entry) => entry.api === model.api) ?? false; const streamWith = (model, context, options, simple) => lazyStream(model, async () => { if (extension?.streamSimple && model.api === extension.api) { return extension.streamSimple(model, context, options); } if (base && supportsBaseApi(model)) { return simple ? base.streamSimple(model, context, options) : base.stream(model, context, options); } const api = getApiProvider(model.api); if (!api) throw new Error(`No API provider registered for api: ${model.api}`); return simple ? api.streamSimple(model, context, options) : api.stream(model, context, options); }); return { id: providerId, name: extension?.name ?? config?.name ?? base?.name ?? extension?.oauth?.name ?? providerId, baseUrl: extension?.baseUrl ?? config?.baseUrl ?? base?.baseUrl, headers: base?.headers, auth: { ...(apiKey ? { apiKey } : {}), ...(oauth ? { oauth } : {}) }, getModels, refreshModels: base?.refreshModels || extension?.refreshModels || extension?.oauth?.modifyModels ? async (context) => { await base?.refreshModels?.(context); if (extension?.refreshModels) { const refreshed = await extension.refreshModels(context); if (!context.signal?.aborted) { // Validate before publishing the new synchronous list. applyExtension(providerId, applyModelsJson(providerId, base?.getModels() ?? [], config), { ...extension, models: refreshed, }); refreshedExtensionModels = refreshed; } } extensionOAuthCredential = context.credential?.type === "oauth" ? context.credential : undefined; } : undefined, filterModels: base?.filterModels ? (models, credential) => base.filterModels(models, credential) : undefined, stream: (model, context, options) => streamWith(model, context, options, false), streamSimple: (model, context, options) => streamWith(model, context, options, true), }; } export function resolveConfiguredModelHeaders(model, config, extension, env) { return resolveHeadersOrThrow(rawModelHeaders(model, config, extension), `model "${model.provider}/${model.id}"`, env); } export function resolveCompatibilityRequestConfig(model, config, extension) { const configured = resolveHeadersOrThrow({ ...configuredHeaders(config, extension), ...rawModelHeaders(model, config, extension) }, `model "${model.provider}/${model.id}"`); return { headers: model.headers || configured ? { ...model.headers, ...configured } : undefined, authHeader: extension?.authHeader ?? config?.authHeader ?? false, }; } export function configuredRequestAuthStatus(config, extension) { const value = configuredApiKey(config, extension); if (value === undefined) return undefined; if (isCommandConfigValue(value)) return { configured: true, source: "models_json_command" }; const names = getConfigValueEnvVarNames(value); if (names.length > 0) { return isConfigValueConfigured(value) ? { configured: true, source: "environment", label: names.join(", ") } : { configured: false }; } return { configured: true, source: extension?.apiKey !== undefined ? "fallback" : "models_json_key" }; } //# sourceMappingURL=provider-composer.js.map