takomi 2.1.44 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.pi/README.md +9 -8
- package/.pi/agents/architect.md +2 -2
- package/.pi/agents/designer.md +2 -2
- package/.pi/agents/worker.md +32 -0
- package/.pi/extensions/oauth-router/commands.ts +66 -35
- package/.pi/extensions/oauth-router/index.ts +51 -7
- package/.pi/extensions/oauth-router/report-ui.ts +205 -0
- package/.pi/extensions/takomi-context-manager/diagnostics-tools.ts +41 -3
- package/.pi/extensions/takomi-context-manager/diagnostics.ts +26 -0
- package/.pi/extensions/takomi-context-manager/index.ts +3 -2
- package/.pi/extensions/takomi-context-manager/model-policy-gate.ts +50 -117
- package/.pi/extensions/takomi-context-manager/policy-tools.ts +93 -42
- package/.pi/extensions/takomi-context-manager/skill-categories.d.ts +11 -0
- package/.pi/extensions/takomi-context-manager/skill-categories.js +212 -0
- package/.pi/extensions/takomi-context-manager/skill-registry.ts +179 -8
- package/.pi/extensions/takomi-context-manager/skill-tools.ts +208 -103
- package/.pi/extensions/takomi-context-manager/tool-renderers.ts +112 -0
- package/.pi/extensions/takomi-context-manager/types.ts +6 -0
- package/.pi/extensions/takomi-runtime/command-text.ts +5 -4
- package/.pi/extensions/takomi-runtime/commands.ts +58 -25
- package/.pi/extensions/takomi-runtime/gate-provenance.ts +24 -0
- package/.pi/extensions/takomi-runtime/index.ts +385 -124
- package/.pi/extensions/takomi-runtime/model-routing-defaults.ts +262 -326
- package/.pi/extensions/takomi-runtime/profile.ts +9 -8
- package/.pi/extensions/takomi-runtime/routing-policy.ts +97 -66
- package/.pi/extensions/takomi-runtime/shared.ts +7 -30
- package/.pi/extensions/takomi-runtime/takomi-stats.js +46 -26
- package/.pi/extensions/takomi-runtime/tool-renderers.ts +234 -0
- package/.pi/extensions/takomi-runtime/workflow-catalog.ts +54 -0
- package/.pi/extensions/takomi-subagents/agents.ts +9 -0
- package/.pi/extensions/takomi-subagents/async-lifecycle.ts +401 -0
- package/.pi/extensions/takomi-subagents/detached-results.ts +940 -0
- package/.pi/extensions/takomi-subagents/index.ts +50 -1
- package/.pi/extensions/takomi-subagents/native-render.ts +250 -188
- package/.pi/extensions/takomi-subagents/pi-subagents-engine.ts +62 -47
- package/.pi/extensions/takomi-subagents/pi-subagents-internal.ts +11 -5
- package/.pi/extensions/takomi-subagents/result-heartbeat.ts +55 -0
- package/.pi/extensions/takomi-subagents/subagent-ux.ts +162 -0
- package/.pi/extensions/takomi-subagents/tool-runner.ts +198 -29
- package/.pi/settings.json +39 -36
- package/.pi/takomi/model-routing.md +288 -3
- package/.pi/takomi-profile.json +54 -50
- package/assets/.agent/skills/shared-resend-portfolio/SKILL.md +124 -0
- package/package.json +4 -3
- package/src/pi-takomi-core/orchestration.ts +39 -21
- package/src/pi-takomi-core/routing.ts +8 -8
- package/src/pi-takomi-core/types.ts +35 -5
- package/src/pi-takomi-core/workflows.ts +86 -45
- package/src/skills-catalog.js +2 -202
- package/src/skills-installer.js +4 -1
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import type {
|
|
5
5
|
TakomiDispatchDefaults,
|
|
6
6
|
TakomiProfile,
|
|
7
|
-
|
|
7
|
+
TakomiPersona,
|
|
8
8
|
VibeLifecycleStage,
|
|
9
9
|
} from "../../../src/pi-takomi-core";
|
|
10
10
|
|
|
@@ -18,14 +18,15 @@ export const DEFAULT_TAKOMI_PROFILE: TakomiProfile = {
|
|
|
18
18
|
roles: {
|
|
19
19
|
orchestrator: { agent: "orchestrator", dispatchPolicy: "subagent" },
|
|
20
20
|
architect: { agent: "architect", dispatchPolicy: "subagent" },
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
designer: { agent: "designer", dispatchPolicy: "subagent" },
|
|
22
|
+
coder: { agent: "coder", dispatchPolicy: "subagent" },
|
|
23
|
+
worker: { agent: "worker", dispatchPolicy: "subagent" },
|
|
24
|
+
reviewer: { agent: "reviewer", dispatchPolicy: "review-first" },
|
|
24
25
|
},
|
|
25
26
|
stages: {
|
|
26
27
|
genesis: { agent: "architect", dispatchPolicy: "subagent" },
|
|
27
28
|
design: { agent: "designer", dispatchPolicy: "subagent" },
|
|
28
|
-
build: { agent: "
|
|
29
|
+
build: { agent: "coder", dispatchPolicy: "subagent" },
|
|
29
30
|
},
|
|
30
31
|
review: {
|
|
31
32
|
enabled: true,
|
|
@@ -57,7 +58,7 @@ function mergeDefaults(
|
|
|
57
58
|
function mergeProfile(base: TakomiProfile, next?: Partial<TakomiProfile>): TakomiProfile {
|
|
58
59
|
if (!next) return base;
|
|
59
60
|
const roles = { ...(base.roles ?? {}) };
|
|
60
|
-
for (const [role, defaults] of Object.entries(next.roles ?? {}) as Array<[
|
|
61
|
+
for (const [role, defaults] of Object.entries(next.roles ?? {}) as Array<[TakomiPersona, TakomiDispatchDefaults]>) {
|
|
61
62
|
roles[role] = mergeDefaults(roles[role], defaults);
|
|
62
63
|
}
|
|
63
64
|
|
|
@@ -99,12 +100,12 @@ export async function loadTakomiProfile(cwd: string): Promise<TakomiProfile> {
|
|
|
99
100
|
const userProfilePath = path.join(os.homedir(), ".pi", "agent", "takomi", "profile.json");
|
|
100
101
|
const projectProfile = await readProfileFile(projectProfilePath);
|
|
101
102
|
const userProfile = await readProfileFile(userProfilePath);
|
|
102
|
-
return mergeProfile(mergeProfile(DEFAULT_TAKOMI_PROFILE,
|
|
103
|
+
return mergeProfile(mergeProfile(DEFAULT_TAKOMI_PROFILE, userProfile), projectProfile);
|
|
103
104
|
}
|
|
104
105
|
|
|
105
106
|
export function getProfileDefaults(
|
|
106
107
|
profile: TakomiProfile,
|
|
107
|
-
role:
|
|
108
|
+
role: TakomiPersona,
|
|
108
109
|
stage?: VibeLifecycleStage,
|
|
109
110
|
): TakomiDispatchDefaults {
|
|
110
111
|
return {
|
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
|
|
6
6
|
export const TAKOMI_ROUTING_POLICY_RELATIVE = path.join(".pi", "takomi", "model-routing.md");
|
|
7
|
-
export const GLOBAL_TAKOMI_ROUTING_POLICY_PATH = path.join(os.homedir(), ".pi", "takomi", "model-routing.md");
|
|
7
|
+
export const GLOBAL_TAKOMI_ROUTING_POLICY_PATH = path.join(os.homedir(), ".pi", "agent", "takomi", "model-routing.md");
|
|
8
8
|
export const GLOBAL_PI_SETTINGS_PATH = path.join(os.homedir(), ".pi", "agent", "settings.json");
|
|
9
9
|
export const PROJECT_PI_SETTINGS_RELATIVE = path.join(".pi", "settings.json");
|
|
10
10
|
export const BUNDLED_TAKOMI_ROUTING_POLICY_PATH = path.resolve(
|
|
@@ -99,59 +99,6 @@ function normalizeForSettings(filePath: string): string {
|
|
|
99
99
|
return filePath.replaceAll(path.sep, "/");
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
function extractPreferredProvider(policy: string): string | undefined {
|
|
103
|
-
const match = policy.match(/(?:preferred|default)\s+(?:provider|router)(?:\s*\/\s*(?:provider|router))?\s*:\s*([a-z0-9-]+)/i)
|
|
104
|
-
?? policy.match(/use\s+([a-z0-9-]+)\s+as\s+(?:the\s+)?(?:provider|router)/i);
|
|
105
|
-
return match?.[1];
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function findExplicitProviderModel(policy: string, family: RegExp): string | undefined {
|
|
109
|
-
const refs = policy.match(/[a-z0-9-]+\/[a-z0-9._-]+/gi) ?? [];
|
|
110
|
-
return refs.find((ref) => family.test(ref));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function providerModel(preferredProvider: string | undefined, model: string): string | undefined {
|
|
114
|
-
return preferredProvider ? `${preferredProvider}/${model}` : undefined;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function withOptionalModel(model: string | undefined, thinking: string, extra: JsonObject = {}): JsonObject {
|
|
118
|
-
return model ? { model, thinking, ...extra } : { thinking, ...extra };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function deriveSubagentDefaults(policy: string): { overrides: JsonObject; detected: string[] } {
|
|
122
|
-
const lower = policy.toLowerCase();
|
|
123
|
-
const has55 = /gpt[- ]?5\.5/.test(lower);
|
|
124
|
-
const has54 = /gpt[- ]?5\.4(?!\s*mini)/.test(lower);
|
|
125
|
-
const hasMini = /gpt[- ]?5\.4\s*mini/.test(lower);
|
|
126
|
-
if (!has55 && !has54 && !hasMini) return { overrides: {}, detected: [] };
|
|
127
|
-
|
|
128
|
-
// Keep generated settings provider-agnostic unless the policy explicitly
|
|
129
|
-
// declares provider-qualified models or a preferred provider/router header.
|
|
130
|
-
const preferredProvider = extractPreferredProvider(policy);
|
|
131
|
-
const model55 = findExplicitProviderModel(policy, /gpt[-_.]?5\.5/i) ?? providerModel(preferredProvider, "gpt-5.5");
|
|
132
|
-
const model54 = findExplicitProviderModel(policy, /gpt[-_.]?5\.4(?![-_.]?mini)/i) ?? providerModel(preferredProvider, "gpt-5.4");
|
|
133
|
-
const modelMini = findExplicitProviderModel(policy, /gpt[-_.]?5\.4[-_.]?mini/i) ?? providerModel(preferredProvider, "gpt-5.4-mini");
|
|
134
|
-
const overrides: JsonObject = {};
|
|
135
|
-
const detected: string[] = [];
|
|
136
|
-
|
|
137
|
-
if (has55) {
|
|
138
|
-
overrides.orchestrator = withOptionalModel(model55, "high");
|
|
139
|
-
overrides.architect = withOptionalModel(model55, "high");
|
|
140
|
-
overrides.reviewer = withOptionalModel(model55, "high");
|
|
141
|
-
detected.push(model55 ? `orchestrator/architect/reviewer → ${model55} high` : "orchestrator/architect/reviewer → GPT-5.5 high intent");
|
|
142
|
-
}
|
|
143
|
-
if (has54) {
|
|
144
|
-
overrides.general = withOptionalModel(model54, "high", model55 ? { fallbackModels: [`${model55}:low`] } : {});
|
|
145
|
-
overrides.coder = withOptionalModel(model54, "high", model55 ? { fallbackModels: [`${model55}:low`] } : {});
|
|
146
|
-
overrides.designer = withOptionalModel(model54, "high", model55 ? { fallbackModels: [`${model55}:low`] } : {});
|
|
147
|
-
detected.push(model54 ? `general/coder/designer → ${model54} high` : "general/coder/designer → GPT-5.4 high intent");
|
|
148
|
-
}
|
|
149
|
-
if (hasMini) {
|
|
150
|
-
detected.push(modelMini ? `GPT-5.4 Mini available for explicit small-task overrides: ${modelMini}` : "GPT-5.4 Mini available as small-task intent only");
|
|
151
|
-
}
|
|
152
|
-
return { overrides, detected };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
102
|
export async function resolveTakomiRoutingPolicy(cwd: string): Promise<ResolvedRoutingPolicy> {
|
|
156
103
|
const projectSettingsPath = path.join(cwd, PROJECT_PI_SETTINGS_RELATIVE);
|
|
157
104
|
const projectSettings = await readJsonObject(projectSettingsPath);
|
|
@@ -205,7 +152,7 @@ export async function resolveTakomiRoutingPolicy(cwd: string): Promise<ResolvedR
|
|
|
205
152
|
return { source: "missing" };
|
|
206
153
|
}
|
|
207
154
|
|
|
208
|
-
export function previewTakomiRoutingPolicy(cwd: string, input: string, options: { scope?: RoutingPolicyInstallScope } = {}): RoutingPolicyPreviewResult {
|
|
155
|
+
export function previewTakomiRoutingPolicy(cwd: string, input: string, options: { scope?: RoutingPolicyInstallScope; availableModels?: string[] } = {}): RoutingPolicyPreviewResult {
|
|
209
156
|
const policy = extractQuotedPolicy(input);
|
|
210
157
|
if (!policy) throw new Error("No routing policy text found. Paste the policy after /takomi routing or inside triple quotes.");
|
|
211
158
|
|
|
@@ -216,8 +163,22 @@ export function previewTakomiRoutingPolicy(cwd: string, input: string, options:
|
|
|
216
163
|
const settingsPath = scope === "project"
|
|
217
164
|
? path.join(cwd, PROJECT_PI_SETTINGS_RELATIVE)
|
|
218
165
|
: GLOBAL_PI_SETTINGS_PATH;
|
|
219
|
-
const
|
|
220
|
-
|
|
166
|
+
const overrides: JsonObject = {};
|
|
167
|
+
const detected: string[] = [];
|
|
168
|
+
|
|
169
|
+
// Markdown is advisory model-facing guidance. It is never parsed into executable defaults.
|
|
170
|
+
// Resolve named model families from Pi's registry only for preview visibility.
|
|
171
|
+
// are conditional routing intents, not role-wide defaults, so do not invent
|
|
172
|
+
// agentOverrides merely to make the extraction non-empty.
|
|
173
|
+
const availableModels = [...new Set(options.availableModels ?? [])];
|
|
174
|
+
for (const alias of ["luna", "sol", "terra"]) {
|
|
175
|
+
if (!new RegExp(`\\b${alias}\\b`, "i").test(policy)) continue;
|
|
176
|
+
const matches = availableModels.filter((model) => new RegExp(`(?:^|/)gpt[-_.]?5\\.6[-_.]?${alias}$`, "i").test(model));
|
|
177
|
+
if (matches.length === 1) detected.push(`${alias[0].toUpperCase()}${alias.slice(1)} intent resolves to ${matches[0]} (conditional route)`);
|
|
178
|
+
else if (matches.length > 1) detected.push(`${alias[0].toUpperCase()}${alias.slice(1)} intent matches available models: ${matches.join(", ")}`);
|
|
179
|
+
else detected.push(`${alias[0].toUpperCase()}${alias.slice(1)} remains a providerless conditional routing intent`);
|
|
180
|
+
}
|
|
181
|
+
return { scope, policy, policyPath, settingsPath, detectedDefaults: [...new Set(detected)], overrides };
|
|
221
182
|
}
|
|
222
183
|
|
|
223
184
|
export async function installTakomiRoutingPolicy(cwd: string, input: string, options: { scope?: RoutingPolicyInstallScope } = {}): Promise<RoutingPolicyInstallResult> {
|
|
@@ -234,13 +195,6 @@ export async function installTakomiRoutingPolicy(cwd: string, input: string, opt
|
|
|
234
195
|
: normalizeForSettings(GLOBAL_TAKOMI_ROUTING_POLICY_PATH);
|
|
235
196
|
settings.takomi = takomi;
|
|
236
197
|
|
|
237
|
-
if (Object.keys(overrides).length > 0) {
|
|
238
|
-
const subagents = asObject(settings.subagents);
|
|
239
|
-
const existingOverrides = asObject(subagents.agentOverrides);
|
|
240
|
-
subagents.agentOverrides = { ...existingOverrides, ...overrides };
|
|
241
|
-
settings.subagents = subagents;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
198
|
await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
|
|
245
199
|
return { policyPath, settingsPath, settingsUpdated: true, detectedDefaults };
|
|
246
200
|
}
|
|
@@ -252,14 +206,91 @@ export function renderRoutingPolicyPreview(preview: RoutingPolicyPreviewResult):
|
|
|
252
206
|
`Policy path: ${preview.policyPath}`,
|
|
253
207
|
`Settings path: ${preview.settingsPath}`,
|
|
254
208
|
"",
|
|
255
|
-
preview.detectedDefaults.length ? "
|
|
209
|
+
preview.detectedDefaults.length ? "Advisory model concepts recognized:" : "Advisory model concepts recognized: none",
|
|
256
210
|
...preview.detectedDefaults.map((item) => `- ${item}`),
|
|
257
211
|
"",
|
|
258
|
-
overrideLines.length ? "
|
|
212
|
+
overrideLines.length ? "Executable settings changes:" : "Executable settings changes: none (Markdown is advisory only)",
|
|
259
213
|
...overrideLines,
|
|
260
214
|
].join("\n");
|
|
261
215
|
}
|
|
262
216
|
|
|
217
|
+
export type TakomiRoutingConfigUpdate = {
|
|
218
|
+
defaultProvider?: string;
|
|
219
|
+
approvedModels?: string[];
|
|
220
|
+
roleDefaults?: Record<string, { model?: string; thinking?: string; fallbackModels?: string[] }>;
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
export type TakomiRoutingConfigPreview = {
|
|
224
|
+
scope: RoutingPolicyInstallScope;
|
|
225
|
+
settingsPath: string;
|
|
226
|
+
before: JsonObject;
|
|
227
|
+
after: JsonObject;
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const CANONICAL_ROUTING_ROLES = new Set(["architect", "designer", "coder", "worker", "reviewer", "orchestrator"]);
|
|
231
|
+
|
|
232
|
+
function validateRoutingConfig(update: TakomiRoutingConfigUpdate, availableModels: string[] = []): void {
|
|
233
|
+
const allConfigured = [
|
|
234
|
+
...(update.approvedModels ?? []),
|
|
235
|
+
...Object.values(update.roleDefaults ?? {}).flatMap((entry) => [entry.model, ...(entry.fallbackModels ?? [])]).filter((item): item is string => Boolean(item)),
|
|
236
|
+
];
|
|
237
|
+
for (const role of Object.keys(update.roleDefaults ?? {})) {
|
|
238
|
+
if (!CANONICAL_ROUTING_ROLES.has(role)) throw new Error(`Unknown Takomi persona '${role}'. Use architect, designer, coder, worker, reviewer, or orchestrator.`);
|
|
239
|
+
}
|
|
240
|
+
for (const model of allConfigured) {
|
|
241
|
+
const base = model.replace(/:(?:off|minimal|low|medium|high|xhigh)$/i, "");
|
|
242
|
+
if (!base.includes("/")) throw new Error(`Model '${model}' must be provider-qualified.`);
|
|
243
|
+
if (availableModels.length && !availableModels.includes(base)) throw new Error(`Model '${base}' is not enabled in Pi's available model registry.`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export async function previewTakomiRoutingConfig(
|
|
248
|
+
cwd: string,
|
|
249
|
+
scope: RoutingPolicyInstallScope,
|
|
250
|
+
update: TakomiRoutingConfigUpdate,
|
|
251
|
+
availableModels: string[] = [],
|
|
252
|
+
): Promise<TakomiRoutingConfigPreview> {
|
|
253
|
+
validateRoutingConfig(update, availableModels);
|
|
254
|
+
const settingsPath = scope === "project" ? path.join(cwd, PROJECT_PI_SETTINGS_RELATIVE) : GLOBAL_PI_SETTINGS_PATH;
|
|
255
|
+
const settings = await readJsonObject(settingsPath);
|
|
256
|
+
const takomi = asObject(settings.takomi);
|
|
257
|
+
const before = asObject(takomi.routing);
|
|
258
|
+
const existingRoles = asObject(before.roleDefaults);
|
|
259
|
+
const nextRoles = { ...existingRoles };
|
|
260
|
+
for (const [role, value] of Object.entries(update.roleDefaults ?? {})) {
|
|
261
|
+
nextRoles[role] = { ...asObject(existingRoles[role]), ...value };
|
|
262
|
+
}
|
|
263
|
+
const after: JsonObject = {
|
|
264
|
+
...before,
|
|
265
|
+
...(update.defaultProvider !== undefined ? { defaultProvider: update.defaultProvider } : {}),
|
|
266
|
+
...(update.approvedModels !== undefined ? { approvedModels: [...new Set(update.approvedModels)] } : {}),
|
|
267
|
+
...(update.roleDefaults !== undefined ? { roleDefaults: nextRoles } : {}),
|
|
268
|
+
};
|
|
269
|
+
return { scope, settingsPath, before, after };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export async function installTakomiRoutingConfig(preview: TakomiRoutingConfigPreview): Promise<void> {
|
|
273
|
+
const settings = await readJsonObject(preview.settingsPath);
|
|
274
|
+
const takomi = asObject(settings.takomi);
|
|
275
|
+
takomi.routing = preview.after;
|
|
276
|
+
settings.takomi = takomi;
|
|
277
|
+
await mkdir(path.dirname(preview.settingsPath), { recursive: true });
|
|
278
|
+
await writeFile(preview.settingsPath, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function renderTakomiRoutingConfigPreview(preview: TakomiRoutingConfigPreview): string {
|
|
282
|
+
return [
|
|
283
|
+
`Scope: ${preview.scope}`,
|
|
284
|
+
`Settings: ${preview.settingsPath}`,
|
|
285
|
+
"",
|
|
286
|
+
"Before:",
|
|
287
|
+
JSON.stringify(preview.before, null, 2),
|
|
288
|
+
"",
|
|
289
|
+
"After:",
|
|
290
|
+
JSON.stringify(preview.after, null, 2),
|
|
291
|
+
].join("\n");
|
|
292
|
+
}
|
|
293
|
+
|
|
263
294
|
export async function loadTakomiRoutingPolicy(cwd: string): Promise<string | undefined> {
|
|
264
295
|
return (await resolveTakomiRoutingPolicy(cwd)).text;
|
|
265
296
|
}
|
|
@@ -443,38 +443,15 @@ function modelCandidates(requested?: string, fallback?: string | string[]): stri
|
|
|
443
443
|
return [requested, ...fallbackList].filter(Boolean) as string[];
|
|
444
444
|
}
|
|
445
445
|
|
|
446
|
-
export async function resolvePreferredModel(ctx: ExtensionContext, requested?: string,
|
|
447
|
-
|
|
448
|
-
if (candidates.length === 0) return {};
|
|
446
|
+
export async function resolvePreferredModel(ctx: ExtensionContext, requested?: string, _fallback?: string | string[]): Promise<{ model?: string; warning?: string }> {
|
|
447
|
+
if (!requested) return {};
|
|
449
448
|
const keys = await getAvailableModelKeys(ctx);
|
|
450
|
-
const exact =
|
|
449
|
+
const exact = keys.find((key) => key === requested) ?? keys.find((key) => key.toLowerCase() === requested.toLowerCase());
|
|
451
450
|
if (exact) return { model: exact };
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
if (idx >= 0) return { model: keys[idx] };
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
for (const candidate of candidates) {
|
|
460
|
-
const idx = loweredKeys.findIndex((key) => key.includes(candidate.toLowerCase()) || candidate.toLowerCase().includes(key));
|
|
461
|
-
if (idx >= 0) {
|
|
462
|
-
return {
|
|
463
|
-
model: keys[idx],
|
|
464
|
-
warning: `Requested model '${candidate}' was unavailable; using '${keys[idx]}' instead.`,
|
|
465
|
-
};
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
const firstAvailable = keys.find((key) => key.includes("/"));
|
|
470
|
-
if (firstAvailable) {
|
|
471
|
-
return {
|
|
472
|
-
model: firstAvailable,
|
|
473
|
-
warning: `Requested models '${candidates.join("', '")}' were unavailable; using '${firstAvailable}' instead.`,
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
return { warning: `No available signed-in model matched '${candidates.join("', '")}'.` };
|
|
451
|
+
return {
|
|
452
|
+
model: requested,
|
|
453
|
+
warning: `Requested model '${requested}' is not currently enabled in Pi's model registry. The exact provider-qualified ID was preserved; no equivalent provider was substituted.`,
|
|
454
|
+
};
|
|
478
455
|
}
|
|
479
456
|
|
|
480
457
|
export async function listModelsViaPi(cwd: string, signal?: AbortSignal): Promise<{ ok: boolean; output: string }> {
|
|
@@ -15,29 +15,47 @@ const pc = {
|
|
|
15
15
|
magenta: ansi(35, 39),
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
// USD per million tokens: input, cache read, output, cache write.
|
|
19
|
+
// OAuth-router's configured catalog is merged over these fallbacks at runtime,
|
|
20
|
+
// keeping Stats aligned with custom/new models without another hard-coded edit.
|
|
18
21
|
const PRICES = {
|
|
19
|
-
'gpt-5.
|
|
20
|
-
'gpt-5.
|
|
21
|
-
'gpt-5.
|
|
22
|
-
'gpt-5.
|
|
23
|
-
'gpt-5.
|
|
24
|
-
'gpt-5.
|
|
25
|
-
'gpt-5-
|
|
26
|
-
'gpt-5.
|
|
27
|
-
'gpt-5.
|
|
28
|
-
'gpt-5': [1.25, 0.125, 10.00],
|
|
29
|
-
'gpt-5
|
|
30
|
-
'gpt-
|
|
31
|
-
'gpt-
|
|
32
|
-
'
|
|
33
|
-
'
|
|
22
|
+
'gpt-5.6-luna': [1.00, 0.10, 6.00, 1.25],
|
|
23
|
+
'gpt-5.6-sol': [5.00, 0.50, 30.00, 6.25],
|
|
24
|
+
'gpt-5.6-terra': [2.50, 0.25, 15.00, 3.125],
|
|
25
|
+
'gpt-5.5': [5.00, 0.50, 30.00, 6.25],
|
|
26
|
+
'gpt-5.4': [2.50, 0.25, 15.00, 3.125],
|
|
27
|
+
'gpt-5.4-mini': [0.75, 0.075, 4.50, 0.9375],
|
|
28
|
+
'gpt-5.4-nano': [0.20, 0.02, 1.25, 0.25],
|
|
29
|
+
'gpt-5.3-codex': [2.50, 0.25, 15.00, 3.125],
|
|
30
|
+
'gpt-5.2-codex': [1.75, 0.175, 14.00, 2.1875],
|
|
31
|
+
'gpt-5-codex': [1.25, 0.125, 10.00, 1.5625],
|
|
32
|
+
'gpt-5.2': [1.75, 0.175, 14.00, 2.1875],
|
|
33
|
+
'gpt-5.1': [1.25, 0.125, 10.00, 1.5625],
|
|
34
|
+
'gpt-5': [1.25, 0.125, 10.00, 1.5625],
|
|
35
|
+
'gpt-5-mini': [0.25, 0.025, 2.00, 0.3125],
|
|
36
|
+
'gpt-4.1': [2.00, 0.50, 8.00, 2.00],
|
|
37
|
+
'gpt-4o': [2.50, 1.25, 10.00, 2.50],
|
|
38
|
+
'o4-mini': [1.10, 0.275, 4.40, 1.10],
|
|
39
|
+
'claude-sonnet-4-6': [3.00, 0.30, 15.00, 3.75],
|
|
34
40
|
};
|
|
35
41
|
|
|
36
42
|
async function exists(target) { try { await fs.access(target); return true; } catch { return false; } }
|
|
37
43
|
function safeJson(line) { try { return JSON.parse(line); } catch { return null; } }
|
|
38
44
|
function dayOf(ts) { return typeof ts === 'string' && ts.length >= 10 ? ts.slice(0, 10) : 'unknown'; }
|
|
39
|
-
function add(map, key, patch) { const row = map.get(key) || { key, input: 0, cache: 0, output: 0, total: 0, cost: 0, events: 0 }; for (const [k,v] of Object.entries(patch)) row[k] = (row[k] || 0) + (Number(v) || 0); if (!Object.prototype.hasOwnProperty.call(patch, 'events')) row.events += 1; map.set(key, row); }
|
|
40
|
-
function cost(model, input, cache, output,
|
|
45
|
+
function add(map, key, patch) { const row = map.get(key) || { key, input: 0, cache: 0, cacheWrite: 0, output: 0, total: 0, cost: 0, events: 0 }; for (const [k,v] of Object.entries(patch)) row[k] = (row[k] || 0) + (Number(v) || 0); if (!Object.prototype.hasOwnProperty.call(patch, 'events')) row.events += 1; map.set(key, row); }
|
|
46
|
+
function cost(model, input, cache, output, cacheWrite = 0, prices = PRICES) { const p = prices[model]; if (!p) return 0; return (input*p[0] + cache*p[1] + output*p[2] + cacheWrite*(p[3] ?? p[0])) / 1_000_000; }
|
|
47
|
+
async function loadPrices(home) {
|
|
48
|
+
const prices = { ...PRICES };
|
|
49
|
+
const configPath = path.join(home, '.pi', 'agent', 'oauth-router', 'config.json');
|
|
50
|
+
try {
|
|
51
|
+
const config = JSON.parse(await fs.readFile(configPath, 'utf8'));
|
|
52
|
+
for (const model of config.models || []) {
|
|
53
|
+
if (!model?.id || !model?.cost) continue;
|
|
54
|
+
prices[model.id] = [model.cost.input || 0, model.cost.cacheRead || 0, model.cost.output || 0, model.cost.cacheWrite ?? model.cost.input ?? 0];
|
|
55
|
+
}
|
|
56
|
+
} catch {}
|
|
57
|
+
return prices;
|
|
58
|
+
}
|
|
41
59
|
function fmtTokens(n) { if (n >= 1e9) return `${(n/1e9).toFixed(2)}B`; if (n >= 1e6) return `${(n/1e6).toFixed(1)}M`; if (n >= 1e3) return `${(n/1e3).toFixed(1)}K`; return String(Math.round(n || 0)); }
|
|
42
60
|
function fmtMoney(n) { return `$${(n || 0).toFixed(n > 100 ? 0 : 2)}`; }
|
|
43
61
|
function ms(n) { if (!n) return '-'; const s = Math.round(n/1000); if (s < 60) return `${s}s`; const m = Math.floor(s/60); if (m < 60) return `${m}m ${s%60}s`; const h = Math.floor(m/60); return `${h}h ${m%60}m`; }
|
|
@@ -117,7 +135,7 @@ function pushTask(taskRows, task) {
|
|
|
117
135
|
taskRows.push(task);
|
|
118
136
|
}
|
|
119
137
|
|
|
120
|
-
async function scanPiSessions(root, source, events, sessionRows = [], taskRows = []) {
|
|
138
|
+
async function scanPiSessions(root, source, events, sessionRows = [], taskRows = [], prices = PRICES) {
|
|
121
139
|
for (const file of await files(root)) {
|
|
122
140
|
let provider = 'unknown', model = 'unknown', session = path.basename(file, '.jsonl'), cwd = '', currentTask = null;
|
|
123
141
|
const row = { key: session, session, source, file, project: projectKey(file), cwd, start: '', end: '', turns: 0, messages: 0, toolCalls: 0, subagentCalls: 0, roles: new Map(), stages: new Map(), workflows: new Map(), activeMs: 0, activityTimestamps: [] };
|
|
@@ -170,7 +188,7 @@ async function scanPiSessions(root, source, events, sessionRows = [], taskRows =
|
|
|
170
188
|
}
|
|
171
189
|
}
|
|
172
190
|
const u = msg && msg.usage;
|
|
173
|
-
if (u) events.push({ source, file, timestamp: obj.timestamp, day: dayOf(obj.timestamp), session, provider, model, project: projectKey(file), kind: 'usage', input: +u.input||0, cache: +u.cacheRead||0, output: +u.output||0, total: +u.totalTokens||0, cost: cost(model, +u.input||0, +u.cacheRead||0, +u.output||0,
|
|
191
|
+
if (u) events.push({ source, file, timestamp: obj.timestamp, day: dayOf(obj.timestamp), session, provider, model, project: projectKey(file), kind: 'usage', input: +u.input||0, cache: +u.cacheRead||0, cacheWrite: +u.cacheWrite||0, output: +u.output||0, total: +u.totalTokens||0, cost: cost(model, +u.input||0, +u.cacheRead||0, +u.output||0, +u.cacheWrite||0, prices) });
|
|
174
192
|
}
|
|
175
193
|
} catch {
|
|
176
194
|
continue;
|
|
@@ -193,18 +211,19 @@ export async function collectTakomiStats(opts = {}) {
|
|
|
193
211
|
const home = opts.home || os.homedir();
|
|
194
212
|
const cwd = opts.cwd || process.cwd();
|
|
195
213
|
const rawEvents = [], rawSessions = [], rawTasks = [];
|
|
214
|
+
const prices = await loadPrices(home);
|
|
196
215
|
const globalSessions = path.resolve(path.join(home, '.pi', 'agent', 'sessions'));
|
|
197
216
|
const projectSessions = path.resolve(path.join(cwd, '.pi', 'agent', 'sessions'));
|
|
198
|
-
await scanPiSessions(globalSessions, 'pi-global', rawEvents, rawSessions, rawTasks);
|
|
199
|
-
if (projectSessions !== globalSessions) await scanPiSessions(projectSessions, 'pi-project', rawEvents, rawSessions, rawTasks);
|
|
200
|
-
await scanPiSessions(path.join(cwd, '.pi', 'takomi'), 'takomi-project', rawEvents, rawSessions, rawTasks);
|
|
217
|
+
await scanPiSessions(globalSessions, 'pi-global', rawEvents, rawSessions, rawTasks, prices);
|
|
218
|
+
if (projectSessions !== globalSessions) await scanPiSessions(projectSessions, 'pi-project', rawEvents, rawSessions, rawTasks, prices);
|
|
219
|
+
await scanPiSessions(path.join(cwd, '.pi', 'takomi'), 'takomi-project', rawEvents, rawSessions, rawTasks, prices);
|
|
201
220
|
const sinceDay = parseSince(opts.since);
|
|
202
221
|
const events = rawEvents.filter(e => !sinceDay || e.day >= sinceDay);
|
|
203
222
|
const sessionRows = rawSessions.filter(s => !sinceDay || dayOf(s.end || s.start) >= sinceDay);
|
|
204
223
|
const taskRows = rawTasks.filter(t => !sinceDay || dayOf(t.end || t.start) >= sinceDay);
|
|
205
224
|
const runs = await scanRunHistory(path.join(home, '.pi', 'agent', 'run-history.jsonl'));
|
|
206
225
|
const byDay = new Map(), byModel = new Map(), bySource = new Map(), byProject = new Map(), byTool = new Map(), byRole = new Map(), byStage = new Map(), byWorkflow = new Map();
|
|
207
|
-
let totals = { input: 0, cache: 0, output: 0, total: 0, cost: 0, events: events.filter(e => e.kind === 'usage').length, toolCalls: 0, turns: 0 };
|
|
226
|
+
let totals = { input: 0, cache: 0, cacheWrite: 0, output: 0, total: 0, cost: 0, events: events.filter(e => e.kind === 'usage').length, toolCalls: 0, turns: 0 };
|
|
208
227
|
for (const s of sessionRows) { totals.toolCalls += s.toolCalls; totals.turns += s.turns; }
|
|
209
228
|
for (const e of events) {
|
|
210
229
|
if (e.kind === 'tool') { add(byTool, e.tool || 'unknown', { total: 0, events: 1 }); continue; }
|
|
@@ -214,7 +233,7 @@ export async function collectTakomiStats(opts = {}) {
|
|
|
214
233
|
add(byWorkflow, e.workflow || 'unknown', { total: 0, events: 1 });
|
|
215
234
|
continue;
|
|
216
235
|
}
|
|
217
|
-
totals.input += e.input; totals.cache += e.cache; totals.output += e.output; totals.total += e.total; totals.cost += e.cost;
|
|
236
|
+
totals.input += e.input; totals.cache += e.cache; totals.cacheWrite += e.cacheWrite || 0; totals.output += e.output; totals.total += e.total; totals.cost += e.cost;
|
|
218
237
|
add(byDay, e.day, e); add(byModel, e.model, e); add(bySource, e.source, e); add(byProject, e.project, e);
|
|
219
238
|
}
|
|
220
239
|
const byAgent = new Map(); let longestRun = null;
|
|
@@ -509,7 +528,8 @@ export function renderTakomiStats(stats, opts = {}) {
|
|
|
509
528
|
// ── Stat Cards Row 1 ─────────────────────────────────────────────────
|
|
510
529
|
const cards1 = [
|
|
511
530
|
statCard(fmtTokens(stats.totals.total), 'Lifetime Tokens'),
|
|
512
|
-
statCard(fmtTokens(stats.totals.cache), 'Cache
|
|
531
|
+
statCard(fmtTokens(stats.totals.cache), 'Cache Reads'),
|
|
532
|
+
statCard(fmtTokens(stats.totals.cacheWrite), 'Cache Writes'),
|
|
513
533
|
statCard(fmtMoney(stats.totals.cost), 'Est. Cost'),
|
|
514
534
|
statCard(String(stats.sessions), 'Sessions'),
|
|
515
535
|
statCard(String(stats.totals.turns), 'Main Turns'),
|
|
@@ -677,7 +697,7 @@ export function renderTakomiStats(stats, opts = {}) {
|
|
|
677
697
|
lines.push('');
|
|
678
698
|
lines.push(' ' + pc.dim(hrule(W - 4)));
|
|
679
699
|
lines.push(' ' + pc.dim('Privacy: metadata only · no raw prompts or transcripts'));
|
|
680
|
-
lines.push(' ' + pc.dim('Costs
|
|
700
|
+
lines.push(' ' + pc.dim('Costs use the oauth-router catalog when available; cache reads and writes are priced separately.'));
|
|
681
701
|
lines.push('');
|
|
682
702
|
|
|
683
703
|
return lines.join('\n');
|