UNPKG

@earendil-works/pi-coding-agent

Version:

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

124 lines • 6.1 kB
import { isModelType, lazyStream, } from "@earendil-works/pi-ai"; /** API id of virtual catalog entries. Requests for it fail unless routed first. */ export const VIRTUAL_MODEL_API = "pi-virtual"; /** Custom entry type that stores router state on the session branch. */ export const VIRTUAL_MODEL_STATE_ENTRY = "pi.virtual-model-state"; const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"]; /** Whether a model or message names a virtual model. Failed routing leaves the virtual model on its message. */ export function isVirtualModel(model) { return model.api === VIRTUAL_MODEL_API; } /** Latest successful response. Its model is physical: failed or aborted requests, including failed routing, are skipped. */ export function findLatestResponse(messages) { for (let i = messages.length - 1; i >= 0; i--) { const message = messages[i]; if (message.role === "assistant" && message.stopReason !== "error" && message.stopReason !== "aborted") { return message; } } return undefined; } /** * The model selection a session branch records. A virtual `model_change` holds until the next * `model_change`, because responses name the physical models it routed to. Otherwise the latest * physical response wins, as in sessions without virtual models. A virtual model that is no longer * registered does not hold, so the selection falls back to the physical model that answered last. * * Only the last `model_change` can hold, so this looks up at most one model in the catalog. */ export function getBranchSelection(branch, getModel) { for (let i = branch.length - 1; i >= 0; i--) { const entry = branch[i]; if (entry.type === "model_change") { return { provider: entry.provider, modelId: entry.modelId }; } if (entry.type === "message" && entry.message.role === "assistant" && !isVirtualModel(entry.message)) { const response = { provider: entry.message.provider, modelId: entry.message.model }; const change = findLastModelChange(branch, i); const model = change && getModel(change.provider, change.modelId); return change && model && isVirtualModel(model) ? change : response; } } return undefined; } function findLastModelChange(branch, before) { for (let i = before - 1; i >= 0; i--) { const entry = branch[i]; if (entry.type === "model_change") return { provider: entry.provider, modelId: entry.modelId }; } return undefined; } /** Latest router state a session branch stores for a virtual model. */ export function getVirtualModelState(branch, provider, modelId) { for (let i = branch.length - 1; i >= 0; i--) { const entry = branch[i]; if (entry.type !== "custom" || entry.customType !== VIRTUAL_MODEL_STATE_ENTRY) continue; const data = entry.data; if (data?.provider === provider && data.modelId === modelId) return data.state; } return undefined; } /** Build the catalog entry of a virtual model. */ export function createVirtualModel(definition) { const levels = definition.thinkingLevels ?? ["off"]; const thinkingLevelMap = {}; for (const level of THINKING_LEVELS) thinkingLevelMap[level] = levels.includes(level) ? level : null; return { id: definition.id, name: definition.name, api: VIRTUAL_MODEL_API, provider: definition.provider, baseUrl: "", reasoning: levels.some((level) => level !== "off"), thinkingLevelMap, input: definition.input ?? ["text", "image"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, contextWindow: definition.contextWindow ?? 0, maxTokens: definition.maxTokens ?? 0, }; } /** Stream for a virtual model that was not routed, e.g. `stream()` with API-specific options. */ function unroutedStream(model) { return lazyStream(model, async () => { throw new Error(`Virtual model ${model.provider}/${model.id} must be routed before streaming`); }); } /** * Add virtual models to a provider's catalog. Without a provider, the result is a keyless provider * that only lists the virtual models. A virtual model hides a physical chat model with the same id, * which a catalog refresh can add after registration. Availability follows the provider's auth. */ export function withVirtualModels(providerId, provider, virtualModels) { if (!provider) { return { id: providerId, name: providerId, auth: { apiKey: { name: "Virtual model", resolve: async () => ({ auth: {}, source: "virtual" }) } }, getModels: () => virtualModels, stream: unroutedStream, streamSimple: unroutedStream, }; } const ids = new Set(virtualModels.map((model) => model.id)); const physical = (models) => models.filter((model) => !isVirtualModel(model) && !(isModelType(model, "chat") && ids.has(model.id))); const virtual = (models) => models.filter((model) => isVirtualModel(model)); const { filterModels, filterAllModels } = provider; return { ...provider, getModels: () => [...physical(provider.getModels()), ...virtualModels], getAllModels: () => [...physical(provider.getAllModels?.() ?? provider.getModels()), ...virtualModels], filterModels: (models, credential) => { const real = physical(models); return [...(filterModels?.(real, credential) ?? real), ...virtual(models)]; }, filterAllModels: filterAllModels && ((models, credential) => [...filterAllModels(physical(models), credential), ...virtual(models)]), stream: (model, context, options) => isVirtualModel(model) ? unroutedStream(model) : provider.stream(model, context, options), streamSimple: (model, context, options) => isVirtualModel(model) ? unroutedStream(model) : provider.streamSimple(model, context, options), }; } //# sourceMappingURL=virtual-models.js.map