@earendil-works/pi-coding-agent
Version:
Coding agent CLI with read, bash, edit, write tools and session management
375 lines • 18.2 kB
JavaScript
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