Files
pi-devin/src/models.ts
T
Kashyab Ambarani d36c0b3705 Initial pi-devin package: Devin Local models inside Pi.
Auth and catalog come from the local Devin CLI. Pi stays the harness.
2026-08-16 20:29:56 -07:00

224 lines
6.6 KiB
TypeScript

import type { ThinkingLevelMap } from "@earendil-works/pi-ai";
import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent";
import { runDevin } from "./cli.js";
export interface DevinVariant {
model_uid: string;
label: string;
max_context_tokens?: number;
max_output_tokens?: number;
cost_tier?: string;
cost_summary?: string;
is_new?: boolean;
is_beta?: boolean;
}
export interface DevinFamily {
family_label: string;
family_uid: string;
slug: string;
aliases?: string[];
variants: DevinVariant[];
}
export interface DevinCatalog {
families: DevinFamily[];
}
const THINKING_ORDER = ["off", "minimal", "low", "medium", "high", "xhigh", "max"] as const;
function parseCost(summary?: string): ProviderModelConfig["cost"] {
const empty = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
if (!summary) return empty;
const input = summary.match(/\$([0-9.]+)\s*\/\s*MTok In/i);
const output = summary.match(/\$([0-9.]+)\s*\/\s*MTok Out/i);
const inCost = input ? Number(input[1]) : 0;
const outCost = output ? Number(output[1]) : 0;
return {
input: inCost,
output: outCost,
cacheRead: Number((inCost * 0.1).toFixed(4)),
cacheWrite: Number((inCost * 1.25).toFixed(4)),
};
}
function variantKey(uid: string): string | null {
const suffixes = [
"none-priority",
"low-priority",
"medium-priority",
"high-priority",
"xhigh-priority",
"max-priority",
"low-fast",
"medium-fast",
"high-fast",
"xhigh-fast",
"max-fast",
"thinking-1m",
"thinking",
"none",
"low",
"medium",
"high",
"xhigh",
"max",
"minimal",
];
for (const suffix of suffixes) {
if (uid === suffix || uid.endsWith(`-${suffix}`)) return suffix;
}
return null;
}
function thinkingFromSuffix(suffix: string | null): keyof ThinkingLevelMap | null {
if (!suffix) return "high";
if (suffix === "none" || suffix === "none-priority") return "off";
if (suffix === "minimal") return "minimal";
if (suffix.startsWith("low")) return "low";
if (suffix.startsWith("medium")) return "medium";
if (suffix.startsWith("high") && !suffix.startsWith("xhigh")) return "high";
if (suffix.startsWith("xhigh")) return "xhigh";
if (suffix.startsWith("max")) return "max";
if (suffix.includes("thinking")) return "high";
return null;
}
function preferredDefault(map: ThinkingLevelMap): string | undefined {
for (const level of ["high", "medium", "max", "xhigh", "low", "minimal", "off"] as const) {
const value = map[level];
if (typeof value === "string") return value;
}
return undefined;
}
function familyToModels(family: DevinFamily): ProviderModelConfig[] {
const usable = family.variants.filter((variant) => {
const key = variantKey(variant.model_uid);
return !key || (!key.includes("priority") && !key.includes("fast") && key !== "thinking-1m");
});
const source = usable.length > 0 ? usable : family.variants;
const thinkingLevelMap: ThinkingLevelMap = {};
for (const variant of source) {
const level = thinkingFromSuffix(variantKey(variant.model_uid));
if (level && thinkingLevelMap[level] === undefined) {
thinkingLevelMap[level] = variant.model_uid;
}
}
const defaultUid = preferredDefault(thinkingLevelMap) ?? source[0]?.model_uid ?? family.family_uid;
const sample = source.find((variant) => variant.model_uid === defaultUid) ?? source[0];
if (!sample) return [];
const mappedLevels = THINKING_ORDER.filter((level) => typeof thinkingLevelMap[level] === "string");
const reasoning = mappedLevels.length > 1;
return [
{
id: defaultUid,
name: family.family_label || family.slug || defaultUid,
reasoning,
thinkingLevelMap: reasoning ? thinkingLevelMap : undefined,
input: ["text", "image"],
cost: parseCost(sample.cost_summary),
contextWindow: sample.max_context_tokens ?? 256_000,
maxTokens: sample.max_output_tokens ?? 128_000,
},
];
}
export const FALLBACK_MODELS: ProviderModelConfig[] = [
{
id: "claude-opus-5-high",
name: "Claude Opus 5",
reasoning: true,
thinkingLevelMap: {
low: "claude-opus-5-low",
medium: "claude-opus-5-medium",
high: "claude-opus-5-high",
xhigh: "claude-opus-5-xhigh",
max: "claude-opus-5-max",
},
input: ["text", "image"],
cost: { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
contextWindow: 1_000_000,
maxTokens: 128_000,
},
{
id: "claude-5-fable-high",
name: "Claude Fable 5",
reasoning: true,
thinkingLevelMap: {
low: "claude-5-fable-low",
medium: "claude-5-fable-medium",
high: "claude-5-fable-high",
xhigh: "claude-5-fable-xhigh",
max: "claude-5-fable-max",
},
input: ["text", "image"],
cost: { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
contextWindow: 1_000_000,
maxTokens: 128_000,
},
{
id: "gpt-5-6-sol-high",
name: "GPT-5.6 Sol",
reasoning: true,
thinkingLevelMap: {
off: "gpt-5-6-sol-none",
low: "gpt-5-6-sol-low",
medium: "gpt-5-6-sol-medium",
high: "gpt-5-6-sol-high",
xhigh: "gpt-5-6-sol-xhigh",
max: "gpt-5-6-sol-max",
},
input: ["text", "image"],
cost: { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 6.25 },
contextWindow: 1_000_000,
maxTokens: 128_000,
},
{
id: "swe-1-7",
name: "SWE-1.7",
reasoning: true,
input: ["text", "image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 262_000,
maxTokens: 128_000,
},
];
export function modelsFromCatalog(catalog: DevinCatalog | null): ProviderModelConfig[] {
if (!catalog?.families?.length) return FALLBACK_MODELS;
const models = catalog.families.flatMap(familyToModels);
return models.length > 0 ? models : FALLBACK_MODELS;
}
export async function loadCliCatalog(): Promise<DevinCatalog | null> {
const { stdout, code, stderr } = await runDevin(["models", "list", "--format", "json"], {
timeoutMs: 20_000,
});
if (code !== 0) {
throw new Error(stderr.trim() || `devin models list exited ${code}`);
}
const parsed = JSON.parse(stdout) as DevinCatalog;
if (!parsed?.families) return null;
return parsed;
}
export function resolveModelUid(
modelId: string,
thinkingLevelMap: ThinkingLevelMap | undefined,
reasoning?: string,
): string {
if (reasoning && thinkingLevelMap) {
const mapped = thinkingLevelMap[reasoning as keyof ThinkingLevelMap];
if (typeof mapped === "string") return mapped;
}
if (thinkingLevelMap) {
const fallback = preferredDefault(thinkingLevelMap);
if (fallback) return fallback;
}
return modelId;
}