codeep 1.3.41 → 2.0.0
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/README.md +208 -0
- package/dist/acp/commands.js +770 -7
- package/dist/acp/protocol.d.ts +11 -2
- package/dist/acp/server.js +179 -11
- package/dist/acp/session.d.ts +3 -0
- package/dist/acp/session.js +5 -0
- package/dist/api/index.js +39 -6
- package/dist/config/index.d.ts +13 -0
- package/dist/config/index.js +46 -1
- package/dist/config/providers.js +76 -1
- package/dist/renderer/App.d.ts +12 -0
- package/dist/renderer/App.js +96 -4
- package/dist/renderer/agentExecution.js +5 -0
- package/dist/renderer/commands.js +348 -2
- package/dist/renderer/components/Login.d.ts +1 -0
- package/dist/renderer/components/Login.js +24 -9
- package/dist/renderer/handlers.d.ts +11 -1
- package/dist/renderer/handlers.js +30 -0
- package/dist/renderer/main.js +73 -0
- package/dist/utils/agent.d.ts +17 -0
- package/dist/utils/agent.js +91 -7
- package/dist/utils/agentChat.d.ts +10 -2
- package/dist/utils/agentChat.js +48 -9
- package/dist/utils/agentStream.js +6 -2
- package/dist/utils/checkpoints.d.ts +93 -0
- package/dist/utils/checkpoints.js +205 -0
- package/dist/utils/context.d.ts +24 -0
- package/dist/utils/context.js +57 -0
- package/dist/utils/customCommands.d.ts +62 -0
- package/dist/utils/customCommands.js +201 -0
- package/dist/utils/hooks.d.ts +97 -0
- package/dist/utils/hooks.js +223 -0
- package/dist/utils/mcpClient.d.ts +229 -0
- package/dist/utils/mcpClient.js +497 -0
- package/dist/utils/mcpConfig.d.ts +55 -0
- package/dist/utils/mcpConfig.js +177 -0
- package/dist/utils/mcpMarketplace.d.ts +49 -0
- package/dist/utils/mcpMarketplace.js +175 -0
- package/dist/utils/mcpRegistry.d.ts +129 -0
- package/dist/utils/mcpRegistry.js +427 -0
- package/dist/utils/mcpSamplingBridge.d.ts +32 -0
- package/dist/utils/mcpSamplingBridge.js +88 -0
- package/dist/utils/mcpStreamableHttp.d.ts +65 -0
- package/dist/utils/mcpStreamableHttp.js +207 -0
- package/dist/utils/openrouterPrefs.d.ts +36 -0
- package/dist/utils/openrouterPrefs.js +83 -0
- package/dist/utils/skillBundles.d.ts +84 -0
- package/dist/utils/skillBundles.js +257 -0
- package/dist/utils/skillBundlesCloud.d.ts +66 -0
- package/dist/utils/skillBundlesCloud.js +196 -0
- package/dist/utils/tokenTracker.d.ts +14 -2
- package/dist/utils/tokenTracker.js +59 -45
- package/dist/utils/toolExecution.d.ts +17 -1
- package/dist/utils/toolExecution.js +184 -6
- package/dist/utils/tools.d.ts +22 -6
- package/dist/utils/tools.js +83 -8
- package/package.json +3 -2
- package/bin/codeep-macos-arm64 +0 -0
- package/bin/codeep-macos-x64 +0 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codeep skill marketplace — CLI ↔ codeep.dev/api/skills bridge.
|
|
3
|
+
*
|
|
4
|
+
* Talks to the REST endpoints in `Codeep-web/src/app/api/skills/`:
|
|
5
|
+
* GET /api/skills?q=&mine=1 → browse
|
|
6
|
+
* POST /api/skills → publish (auth required)
|
|
7
|
+
* GET /api/skills/:owner/:slug → fetch one (auth req for private)
|
|
8
|
+
* DELETE /api/skills/:owner/:slug → unpublish (owner only)
|
|
9
|
+
*
|
|
10
|
+
* Auth uses the same `x-sync-token` header `codeepCloud.ts` already sends
|
|
11
|
+
* for /api/tasks and friends.
|
|
12
|
+
*/
|
|
13
|
+
export interface RemoteSkill {
|
|
14
|
+
id: number;
|
|
15
|
+
github_id: string;
|
|
16
|
+
owner_username: string | null;
|
|
17
|
+
slug: string;
|
|
18
|
+
name: string;
|
|
19
|
+
description: string;
|
|
20
|
+
body: string;
|
|
21
|
+
version: string | null;
|
|
22
|
+
visibility: 'public' | 'private';
|
|
23
|
+
install_count: number;
|
|
24
|
+
updated_at: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Publish a project-scoped skill bundle to codeep.dev.
|
|
28
|
+
* The bundle MUST exist under `<workspaceRoot>/.codeep/skills/<slug>/SKILL.md`
|
|
29
|
+
* — global skills aren't publishable from this helper (intentional: users
|
|
30
|
+
* publish work from a project they're owning, not from their global hoard).
|
|
31
|
+
*/
|
|
32
|
+
export declare function publishBundle(workspaceRoot: string, slug: string, opts?: {
|
|
33
|
+
isPublic?: boolean;
|
|
34
|
+
}): Promise<{
|
|
35
|
+
ok: boolean;
|
|
36
|
+
skill?: RemoteSkill;
|
|
37
|
+
error?: string;
|
|
38
|
+
}>;
|
|
39
|
+
/**
|
|
40
|
+
* Install a skill from the marketplace into the project's
|
|
41
|
+
* `.codeep/skills/<slug>/SKILL.md`. `idOrPath` may be either
|
|
42
|
+
* `<owner>/<slug>` (preferred) or a numeric id.
|
|
43
|
+
*/
|
|
44
|
+
export declare function installBundle(workspaceRoot: string, idOrPath: string): Promise<{
|
|
45
|
+
ok: boolean;
|
|
46
|
+
name?: string;
|
|
47
|
+
error?: string;
|
|
48
|
+
}>;
|
|
49
|
+
/** List public skills (or own when `mine=true`). Returns up to 100. */
|
|
50
|
+
export declare function browseSkills(opts?: {
|
|
51
|
+
query?: string;
|
|
52
|
+
mine?: boolean;
|
|
53
|
+
}): Promise<{
|
|
54
|
+
ok: boolean;
|
|
55
|
+
skills?: RemoteSkill[];
|
|
56
|
+
error?: string;
|
|
57
|
+
}>;
|
|
58
|
+
/** Unpublish a skill (owner only). */
|
|
59
|
+
export declare function unpublishBundle(idOrPath: string): Promise<{
|
|
60
|
+
ok: boolean;
|
|
61
|
+
error?: string;
|
|
62
|
+
}>;
|
|
63
|
+
/** Read raw SKILL.md from disk — used when we want the unmodified bytes. */
|
|
64
|
+
export declare function readRawSkillMd(workspaceRoot: string, slug: string): string | null;
|
|
65
|
+
/** Delete the local copy of an installed skill bundle (for /skills uninstall). */
|
|
66
|
+
export declare function uninstallLocalBundle(workspaceRoot: string, slug: string): boolean;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codeep skill marketplace — CLI ↔ codeep.dev/api/skills bridge.
|
|
3
|
+
*
|
|
4
|
+
* Talks to the REST endpoints in `Codeep-web/src/app/api/skills/`:
|
|
5
|
+
* GET /api/skills?q=&mine=1 → browse
|
|
6
|
+
* POST /api/skills → publish (auth required)
|
|
7
|
+
* GET /api/skills/:owner/:slug → fetch one (auth req for private)
|
|
8
|
+
* DELETE /api/skills/:owner/:slug → unpublish (owner only)
|
|
9
|
+
*
|
|
10
|
+
* Auth uses the same `x-sync-token` header `codeepCloud.ts` already sends
|
|
11
|
+
* for /api/tasks and friends.
|
|
12
|
+
*/
|
|
13
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync, rmSync } from 'fs';
|
|
14
|
+
import { join } from 'path';
|
|
15
|
+
import { homedir } from 'os';
|
|
16
|
+
import { getSyncToken } from '../config/index.js';
|
|
17
|
+
import { findSkillBundle } from './skillBundles.js';
|
|
18
|
+
const API_BASE = 'https://codeep.dev';
|
|
19
|
+
/**
|
|
20
|
+
* Publish a project-scoped skill bundle to codeep.dev.
|
|
21
|
+
* The bundle MUST exist under `<workspaceRoot>/.codeep/skills/<slug>/SKILL.md`
|
|
22
|
+
* — global skills aren't publishable from this helper (intentional: users
|
|
23
|
+
* publish work from a project they're owning, not from their global hoard).
|
|
24
|
+
*/
|
|
25
|
+
export async function publishBundle(workspaceRoot, slug, opts = {}) {
|
|
26
|
+
const token = getSyncToken();
|
|
27
|
+
if (!token)
|
|
28
|
+
return { ok: false, error: 'Not linked to codeep.dev — run `codeep account` first.' };
|
|
29
|
+
const bundle = findSkillBundle(slug, workspaceRoot);
|
|
30
|
+
if (!bundle || bundle.scope !== 'project') {
|
|
31
|
+
return { ok: false, error: `Skill bundle "${slug}" not found in this project. Run \`/skills create-bundle ${slug}\` first.` };
|
|
32
|
+
}
|
|
33
|
+
// Build the SKILL.md text we'll publish. We re-serialise from the loaded
|
|
34
|
+
// bundle rather than reading the file again — that way frontmatter
|
|
35
|
+
// normalisation (e.g. defaulted name from dir) is reflected on the
|
|
36
|
+
// server and the next install round-trips correctly.
|
|
37
|
+
const skillMd = serialiseSkillMd(bundle);
|
|
38
|
+
try {
|
|
39
|
+
const res = await fetch(`${API_BASE}/api/skills`, {
|
|
40
|
+
method: 'POST',
|
|
41
|
+
headers: { 'Content-Type': 'application/json', 'x-sync-token': token },
|
|
42
|
+
body: JSON.stringify({
|
|
43
|
+
slug: bundle.name,
|
|
44
|
+
name: bundle.name,
|
|
45
|
+
description: bundle.description,
|
|
46
|
+
body: skillMd,
|
|
47
|
+
version: bundle.version ?? null,
|
|
48
|
+
visibility: opts.isPublic ? 'public' : 'private',
|
|
49
|
+
}),
|
|
50
|
+
});
|
|
51
|
+
const data = await res.json().catch(() => ({}));
|
|
52
|
+
if (!res.ok || !data.ok) {
|
|
53
|
+
return { ok: false, error: data.error ?? `HTTP ${res.status}` };
|
|
54
|
+
}
|
|
55
|
+
return { ok: true, skill: data.skill };
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
return { ok: false, error: err.message };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Install a skill from the marketplace into the project's
|
|
63
|
+
* `.codeep/skills/<slug>/SKILL.md`. `idOrPath` may be either
|
|
64
|
+
* `<owner>/<slug>` (preferred) or a numeric id.
|
|
65
|
+
*/
|
|
66
|
+
export async function installBundle(workspaceRoot, idOrPath) {
|
|
67
|
+
const token = getSyncToken(); // optional — public skills are readable without auth
|
|
68
|
+
const headers = {};
|
|
69
|
+
if (token)
|
|
70
|
+
headers['x-sync-token'] = token;
|
|
71
|
+
try {
|
|
72
|
+
// ?install=1 so the server bumps install_count
|
|
73
|
+
const url = `${API_BASE}/api/skills/${encodeURIComponent(idOrPath)}?install=1`;
|
|
74
|
+
const res = await fetch(url, { headers });
|
|
75
|
+
const data = await res.json().catch(() => ({}));
|
|
76
|
+
if (!res.ok || !data.ok || !data.skill) {
|
|
77
|
+
return { ok: false, error: data.error ?? `HTTP ${res.status}` };
|
|
78
|
+
}
|
|
79
|
+
const skill = data.skill;
|
|
80
|
+
const dir = join(workspaceRoot, '.codeep', 'skills', skill.slug);
|
|
81
|
+
if (existsSync(dir)) {
|
|
82
|
+
return { ok: false, error: `A bundle already exists at .codeep/skills/${skill.slug}/ — remove it before re-installing.` };
|
|
83
|
+
}
|
|
84
|
+
mkdirSync(dir, { recursive: true });
|
|
85
|
+
writeFileSync(join(dir, 'SKILL.md'), skill.body);
|
|
86
|
+
return { ok: true, name: skill.slug };
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
return { ok: false, error: err.message };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/** List public skills (or own when `mine=true`). Returns up to 100. */
|
|
93
|
+
export async function browseSkills(opts = {}) {
|
|
94
|
+
const token = getSyncToken();
|
|
95
|
+
const headers = {};
|
|
96
|
+
if (token)
|
|
97
|
+
headers['x-sync-token'] = token;
|
|
98
|
+
if (opts.mine && !token)
|
|
99
|
+
return { ok: false, error: '`mine=1` requires `codeep account` login.' };
|
|
100
|
+
try {
|
|
101
|
+
const u = new URL(`${API_BASE}/api/skills`);
|
|
102
|
+
if (opts.query)
|
|
103
|
+
u.searchParams.set('q', opts.query);
|
|
104
|
+
if (opts.mine)
|
|
105
|
+
u.searchParams.set('mine', '1');
|
|
106
|
+
const res = await fetch(u.toString(), { headers });
|
|
107
|
+
const data = await res.json().catch(() => ({}));
|
|
108
|
+
if (!res.ok || !data.ok)
|
|
109
|
+
return { ok: false, error: data.error ?? `HTTP ${res.status}` };
|
|
110
|
+
return { ok: true, skills: data.skills ?? [] };
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
return { ok: false, error: err.message };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/** Unpublish a skill (owner only). */
|
|
117
|
+
export async function unpublishBundle(idOrPath) {
|
|
118
|
+
const token = getSyncToken();
|
|
119
|
+
if (!token)
|
|
120
|
+
return { ok: false, error: 'Not linked to codeep.dev — run `codeep account` first.' };
|
|
121
|
+
try {
|
|
122
|
+
const res = await fetch(`${API_BASE}/api/skills/${encodeURIComponent(idOrPath)}`, {
|
|
123
|
+
method: 'DELETE',
|
|
124
|
+
headers: { 'x-sync-token': token },
|
|
125
|
+
});
|
|
126
|
+
if (res.status === 404)
|
|
127
|
+
return { ok: false, error: 'Skill not found (or not yours).' };
|
|
128
|
+
if (!res.ok)
|
|
129
|
+
return { ok: false, error: `HTTP ${res.status}` };
|
|
130
|
+
return { ok: true };
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
return { ok: false, error: err.message };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Re-serialise a loaded SkillBundle back into the SKILL.md text format.
|
|
138
|
+
* Used by publish so the round-trip is lossless (sort of — we drop
|
|
139
|
+
* unknown frontmatter keys for now to keep the published format stable).
|
|
140
|
+
*/
|
|
141
|
+
function serialiseSkillMd(bundle) {
|
|
142
|
+
const meta = ['---'];
|
|
143
|
+
meta.push(`name: ${bundle.name}`);
|
|
144
|
+
meta.push(`description: ${bundle.description}`);
|
|
145
|
+
if (bundle.version)
|
|
146
|
+
meta.push(`version: ${bundle.version}`);
|
|
147
|
+
if (bundle.author)
|
|
148
|
+
meta.push(`author: ${bundle.author}`);
|
|
149
|
+
if (bundle.codeepMinVersion)
|
|
150
|
+
meta.push(`codeep-min-version: ${bundle.codeepMinVersion}`);
|
|
151
|
+
if (bundle.requiresMcp.length) {
|
|
152
|
+
meta.push(`codeep-requires-mcp:`);
|
|
153
|
+
for (const s of bundle.requiresMcp)
|
|
154
|
+
meta.push(` - ${s}`);
|
|
155
|
+
}
|
|
156
|
+
if (bundle.allowedTools.length) {
|
|
157
|
+
meta.push(`allowed-tools:`);
|
|
158
|
+
for (const s of bundle.allowedTools)
|
|
159
|
+
meta.push(` - ${s}`);
|
|
160
|
+
}
|
|
161
|
+
if (bundle.triggers.length) {
|
|
162
|
+
meta.push(`triggers:`);
|
|
163
|
+
for (const s of bundle.triggers)
|
|
164
|
+
meta.push(` - ${s}`);
|
|
165
|
+
}
|
|
166
|
+
meta.push('---', '');
|
|
167
|
+
return meta.join('\n') + bundle.body;
|
|
168
|
+
}
|
|
169
|
+
/** Read raw SKILL.md from disk — used when we want the unmodified bytes. */
|
|
170
|
+
export function readRawSkillMd(workspaceRoot, slug) {
|
|
171
|
+
const file = join(workspaceRoot, '.codeep', 'skills', slug, 'SKILL.md');
|
|
172
|
+
if (!existsSync(file))
|
|
173
|
+
return null;
|
|
174
|
+
try {
|
|
175
|
+
return readFileSync(file, 'utf-8');
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/** Delete the local copy of an installed skill bundle (for /skills uninstall). */
|
|
182
|
+
export function uninstallLocalBundle(workspaceRoot, slug) {
|
|
183
|
+
const dir = join(workspaceRoot, '.codeep', 'skills', slug);
|
|
184
|
+
if (!existsSync(dir))
|
|
185
|
+
return false;
|
|
186
|
+
try {
|
|
187
|
+
rmSync(dir, { recursive: true, force: true });
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// Silence unused-import warning when builds strip unused — homedir is here
|
|
195
|
+
// for future global-install support (planned 2.1).
|
|
196
|
+
void homedir;
|
|
@@ -20,6 +20,8 @@ interface TokenRecord {
|
|
|
20
20
|
totalTokens: number;
|
|
21
21
|
model: string;
|
|
22
22
|
provider: string;
|
|
23
|
+
/** Authoritative per-call USD from the provider (OpenRouter), if available. */
|
|
24
|
+
actualCostUsd?: number;
|
|
23
25
|
}
|
|
24
26
|
/**
|
|
25
27
|
* Get context window size for a model (falls back to 128k if unknown)
|
|
@@ -31,9 +33,13 @@ export declare function getPricingTable(): {
|
|
|
31
33
|
outputPer1M: number;
|
|
32
34
|
}[];
|
|
33
35
|
/**
|
|
34
|
-
* Record token usage from an API response
|
|
36
|
+
* Record token usage from an API response. The optional `actualCostUsd`
|
|
37
|
+
* argument lets aggregator providers (OpenRouter) pass through the
|
|
38
|
+
* authoritative per-call cost they returned in `usage.cost`, instead of
|
|
39
|
+
* forcing us to look it up in `MODEL_PRICING` (which we don't maintain
|
|
40
|
+
* for every OpenRouter-listed model — there are 100+).
|
|
35
41
|
*/
|
|
36
|
-
export declare function recordTokenUsage(usage: TokenUsage, model: string, provider: string): void;
|
|
42
|
+
export declare function recordTokenUsage(usage: TokenUsage, model: string, provider: string, actualCostUsd?: number): void;
|
|
37
43
|
/**
|
|
38
44
|
* Extract token usage from OpenAI-format API response
|
|
39
45
|
*/
|
|
@@ -69,4 +75,10 @@ export declare function formatTokenCount(tokens: number): string;
|
|
|
69
75
|
* Reset session tracking
|
|
70
76
|
*/
|
|
71
77
|
export declare function resetTokenTracking(): void;
|
|
78
|
+
/**
|
|
79
|
+
* Format a session cost report as a Markdown block. Used by `/cost` in both
|
|
80
|
+
* the TUI and ACP command handlers. Returns a "no usage yet" message if the
|
|
81
|
+
* session hasn't made any API calls.
|
|
82
|
+
*/
|
|
83
|
+
export declare function formatCostReport(): string;
|
|
72
84
|
export {};
|
|
@@ -1,31 +1,24 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Token and cost tracking for API usage
|
|
3
3
|
*/
|
|
4
|
-
// Context window sizes per model (in tokens)
|
|
4
|
+
// Context window sizes per model (in tokens).
|
|
5
|
+
// Keep this table in lockstep with `providers.ts` — entries for models that
|
|
6
|
+
// aren't in the provider catalogue only show up if a user types an id by hand
|
|
7
|
+
// and produce phantom estimates against the wrong context size.
|
|
5
8
|
const MODEL_CONTEXT_WINDOWS = {
|
|
6
9
|
// Z.AI / ZhipuAI
|
|
7
10
|
'glm-5.1': 131_072,
|
|
8
11
|
'glm-5': 80_000,
|
|
9
12
|
'glm-5-turbo': 202_752,
|
|
10
|
-
'glm-4.5-air': 131_072,
|
|
11
|
-
'glm-4.7-flash': 202_752,
|
|
12
13
|
// OpenAI
|
|
13
14
|
'gpt-5.5': 1_200_000,
|
|
14
15
|
'gpt-5.4': 1_050_000,
|
|
15
16
|
'gpt-5.4-mini': 400_000,
|
|
16
17
|
'gpt-5.4-nano': 400_000,
|
|
17
|
-
'gpt-4.1': 1_000_000,
|
|
18
|
-
'gpt-4.1-mini': 1_000_000,
|
|
19
|
-
'gpt-4.1-nano': 1_000_000,
|
|
20
|
-
'o3': 200_000,
|
|
21
|
-
'o4-mini': 200_000,
|
|
22
|
-
'gpt-4o': 128_000,
|
|
23
18
|
// Anthropic
|
|
24
|
-
'claude-mythos-preview': 1_000_000,
|
|
25
19
|
'claude-opus-4-7': 1_000_000,
|
|
26
20
|
'claude-opus-4-6': 1_000_000,
|
|
27
21
|
'claude-sonnet-4-6': 1_000_000,
|
|
28
|
-
'claude-sonnet-4-5-20250929': 200_000,
|
|
29
22
|
'claude-haiku-4-5-20251001': 200_000,
|
|
30
23
|
// DeepSeek
|
|
31
24
|
'deepseek-v4-pro': 1_000_000,
|
|
@@ -33,17 +26,8 @@ const MODEL_CONTEXT_WINDOWS = {
|
|
|
33
26
|
// Google
|
|
34
27
|
'gemini-3.1-pro-preview': 1_048_576,
|
|
35
28
|
'gemini-3-flash-preview': 1_000_000,
|
|
36
|
-
'gemini-3.1-flash-lite-preview': 1_000_000,
|
|
37
|
-
'gemini-2.5-pro': 1_000_000,
|
|
38
|
-
'gemini-2.5-flash': 1_000_000,
|
|
39
|
-
'gemini-2.5-flash-lite': 1_000_000,
|
|
40
29
|
// MiniMax
|
|
41
30
|
'MiniMax-M2.7': 204_800,
|
|
42
|
-
'MiniMax-M2.5': 196_608,
|
|
43
|
-
'MiniMax-M2.5-highspeed': 196_608,
|
|
44
|
-
'MiniMax-M2.1': 196_608,
|
|
45
|
-
'MiniMax-M2.1-highspeed': 196_608,
|
|
46
|
-
'MiniMax-M2': 196_608,
|
|
47
31
|
};
|
|
48
32
|
const DEFAULT_CONTEXT_WINDOW = 128_000;
|
|
49
33
|
/**
|
|
@@ -52,31 +36,23 @@ const DEFAULT_CONTEXT_WINDOW = 128_000;
|
|
|
52
36
|
export function getModelContextWindow(model) {
|
|
53
37
|
return MODEL_CONTEXT_WINDOWS[model] ?? DEFAULT_CONTEXT_WINDOW;
|
|
54
38
|
}
|
|
55
|
-
// Pricing table — USD per 1M tokens
|
|
39
|
+
// Pricing table — USD per 1M tokens. Same rule as MODEL_CONTEXT_WINDOWS:
|
|
40
|
+
// only list model ids that exist in `providers.ts`, otherwise typing an id
|
|
41
|
+
// by hand can produce phantom cost estimates against stale rates.
|
|
56
42
|
const MODEL_PRICING = {
|
|
57
43
|
// Z.AI / ZhipuAI
|
|
58
44
|
'glm-5.1': { inputPer1M: 1.00, outputPer1M: 3.20 },
|
|
59
45
|
'glm-5': { inputPer1M: 0.72, outputPer1M: 2.30 },
|
|
60
46
|
'glm-5-turbo': { inputPer1M: 1.20, outputPer1M: 4.00 },
|
|
61
|
-
'glm-4.5-air': { inputPer1M: 0.20, outputPer1M: 1.10 },
|
|
62
|
-
'glm-4.7-flash': { inputPer1M: 0.06, outputPer1M: 0.40 },
|
|
63
47
|
// OpenAI
|
|
64
48
|
'gpt-5.5': { inputPer1M: 5.00, outputPer1M: 30.00 },
|
|
65
49
|
'gpt-5.4': { inputPer1M: 2.50, outputPer1M: 15.00 },
|
|
66
50
|
'gpt-5.4-mini': { inputPer1M: 0.75, outputPer1M: 4.50 },
|
|
67
51
|
'gpt-5.4-nano': { inputPer1M: 0.20, outputPer1M: 1.25 },
|
|
68
|
-
'gpt-4.1': { inputPer1M: 2.00, outputPer1M: 8.00 },
|
|
69
|
-
'gpt-4.1-mini': { inputPer1M: 0.40, outputPer1M: 1.60 },
|
|
70
|
-
'gpt-4.1-nano': { inputPer1M: 0.10, outputPer1M: 0.40 },
|
|
71
|
-
'o3': { inputPer1M: 2.00, outputPer1M: 8.00 },
|
|
72
|
-
'o4-mini': { inputPer1M: 0.55, outputPer1M: 2.20 },
|
|
73
|
-
'gpt-4o': { inputPer1M: 2.50, outputPer1M: 10.00 },
|
|
74
52
|
// Anthropic
|
|
75
|
-
'claude-mythos-preview': { inputPer1M: 6.00, outputPer1M: 30.00 },
|
|
76
53
|
'claude-opus-4-7': { inputPer1M: 5.00, outputPer1M: 25.00 },
|
|
77
54
|
'claude-opus-4-6': { inputPer1M: 5.00, outputPer1M: 25.00 },
|
|
78
55
|
'claude-sonnet-4-6': { inputPer1M: 3.00, outputPer1M: 15.00 },
|
|
79
|
-
'claude-sonnet-4-5-20250929': { inputPer1M: 3.00, outputPer1M: 15.00 },
|
|
80
56
|
'claude-haiku-4-5-20251001': { inputPer1M: 1.00, outputPer1M: 5.00 },
|
|
81
57
|
// DeepSeek (cache-miss input pricing)
|
|
82
58
|
'deepseek-v4-pro': { inputPer1M: 1.74, outputPer1M: 3.48 },
|
|
@@ -84,17 +60,8 @@ const MODEL_PRICING = {
|
|
|
84
60
|
// Google
|
|
85
61
|
'gemini-3.1-pro-preview': { inputPer1M: 2.00, outputPer1M: 12.00 },
|
|
86
62
|
'gemini-3-flash-preview': { inputPer1M: 0.50, outputPer1M: 3.00 },
|
|
87
|
-
'gemini-3.1-flash-lite-preview': { inputPer1M: 0.25, outputPer1M: 1.50 },
|
|
88
|
-
'gemini-2.5-pro': { inputPer1M: 1.00, outputPer1M: 10.00 },
|
|
89
|
-
'gemini-2.5-flash': { inputPer1M: 0.30, outputPer1M: 2.50 },
|
|
90
|
-
'gemini-2.5-flash-lite': { inputPer1M: 0.10, outputPer1M: 0.40 },
|
|
91
63
|
// MiniMax
|
|
92
64
|
'MiniMax-M2.7': { inputPer1M: 0.30, outputPer1M: 1.20 },
|
|
93
|
-
'MiniMax-M2.5': { inputPer1M: 0.118, outputPer1M: 0.99 },
|
|
94
|
-
'MiniMax-M2.5-highspeed': { inputPer1M: 0.30, outputPer1M: 2.40 },
|
|
95
|
-
'MiniMax-M2.1': { inputPer1M: 0.27, outputPer1M: 0.95 },
|
|
96
|
-
'MiniMax-M2.1-highspeed': { inputPer1M: 0.27, outputPer1M: 0.95 },
|
|
97
|
-
'MiniMax-M2': { inputPer1M: 0.30, outputPer1M: 1.20 },
|
|
98
65
|
};
|
|
99
66
|
export function getPricingTable() {
|
|
100
67
|
return Object.entries(MODEL_PRICING).map(([model, p]) => ({ model, ...p }));
|
|
@@ -102,9 +69,13 @@ export function getPricingTable() {
|
|
|
102
69
|
// Session-level accumulator
|
|
103
70
|
const records = [];
|
|
104
71
|
/**
|
|
105
|
-
* Record token usage from an API response
|
|
72
|
+
* Record token usage from an API response. The optional `actualCostUsd`
|
|
73
|
+
* argument lets aggregator providers (OpenRouter) pass through the
|
|
74
|
+
* authoritative per-call cost they returned in `usage.cost`, instead of
|
|
75
|
+
* forcing us to look it up in `MODEL_PRICING` (which we don't maintain
|
|
76
|
+
* for every OpenRouter-listed model — there are 100+).
|
|
106
77
|
*/
|
|
107
|
-
export function recordTokenUsage(usage, model, provider) {
|
|
78
|
+
export function recordTokenUsage(usage, model, provider, actualCostUsd) {
|
|
108
79
|
records.push({
|
|
109
80
|
timestamp: Date.now(),
|
|
110
81
|
promptTokens: usage.promptTokens,
|
|
@@ -112,6 +83,7 @@ export function recordTokenUsage(usage, model, provider) {
|
|
|
112
83
|
totalTokens: usage.totalTokens,
|
|
113
84
|
model,
|
|
114
85
|
provider,
|
|
86
|
+
actualCostUsd,
|
|
115
87
|
});
|
|
116
88
|
}
|
|
117
89
|
/**
|
|
@@ -148,11 +120,20 @@ export function getCostBreakdown() {
|
|
|
148
120
|
for (const record of records) {
|
|
149
121
|
const key = `${record.provider}/${record.model}`;
|
|
150
122
|
const existing = grouped.get(key) ?? { provider: record.provider, model: record.model, promptTokens: 0, completionTokens: 0, estimatedCost: 0 };
|
|
151
|
-
const pricing = MODEL_PRICING[record.model];
|
|
152
123
|
existing.promptTokens += record.promptTokens;
|
|
153
124
|
existing.completionTokens += record.completionTokens;
|
|
154
|
-
|
|
155
|
-
|
|
125
|
+
// Cost source priority:
|
|
126
|
+
// 1. Provider-reported USD (OpenRouter, MaxiCloud, etc.) — most accurate.
|
|
127
|
+
// 2. Our MODEL_PRICING table — for built-in providers we maintain rates for.
|
|
128
|
+
// 3. Zero — model isn't in our table; flag in formatCostReport.
|
|
129
|
+
if (typeof record.actualCostUsd === 'number' && Number.isFinite(record.actualCostUsd)) {
|
|
130
|
+
existing.estimatedCost += record.actualCostUsd;
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
const pricing = MODEL_PRICING[record.model];
|
|
134
|
+
if (pricing) {
|
|
135
|
+
existing.estimatedCost += (record.promptTokens / 1_000_000) * pricing.inputPer1M + (record.completionTokens / 1_000_000) * pricing.outputPer1M;
|
|
136
|
+
}
|
|
156
137
|
}
|
|
157
138
|
grouped.set(key, existing);
|
|
158
139
|
}
|
|
@@ -201,3 +182,36 @@ export function formatTokenCount(tokens) {
|
|
|
201
182
|
export function resetTokenTracking() {
|
|
202
183
|
records.length = 0;
|
|
203
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Format a session cost report as a Markdown block. Used by `/cost` in both
|
|
187
|
+
* the TUI and ACP command handlers. Returns a "no usage yet" message if the
|
|
188
|
+
* session hasn't made any API calls.
|
|
189
|
+
*/
|
|
190
|
+
export function formatCostReport() {
|
|
191
|
+
const stats = getSessionStats();
|
|
192
|
+
if (stats.requestCount === 0) {
|
|
193
|
+
return '_No API requests in this session yet._';
|
|
194
|
+
}
|
|
195
|
+
const breakdown = getCostBreakdown();
|
|
196
|
+
const lines = [
|
|
197
|
+
'## Session Cost',
|
|
198
|
+
'',
|
|
199
|
+
`**Requests:** ${stats.requestCount} · **Input:** ${formatTokenCount(stats.totalPromptTokens)} · **Output:** ${formatTokenCount(stats.totalCompletionTokens)} · **Total:** ${formatTokenCount(stats.totalTokens)}`,
|
|
200
|
+
`**Estimated cost:** $${stats.estimatedCost.toFixed(4)}`,
|
|
201
|
+
'',
|
|
202
|
+
];
|
|
203
|
+
if (breakdown.length > 1 || (breakdown.length === 1 && breakdown[0].estimatedCost > 0)) {
|
|
204
|
+
lines.push('| Provider / Model | Input | Output | Cost |');
|
|
205
|
+
lines.push('|---|---:|---:|---:|');
|
|
206
|
+
for (const b of breakdown) {
|
|
207
|
+
lines.push(`| \`${b.provider}\` / \`${b.model}\` | ${formatTokenCount(b.promptTokens)} | ${formatTokenCount(b.completionTokens)} | $${b.estimatedCost.toFixed(4)} |`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// Models with no pricing entry don't contribute to cost — flag so users
|
|
211
|
+
// aren't surprised the total looks low.
|
|
212
|
+
const untracked = breakdown.filter(b => b.estimatedCost === 0 && (b.promptTokens + b.completionTokens) > 0);
|
|
213
|
+
if (untracked.length > 0) {
|
|
214
|
+
lines.push('', `_Note: ${untracked.length} model${untracked.length === 1 ? '' : 's'} (${untracked.map(u => `\`${u.model}\``).join(', ')}) have no pricing entry — token counts are tracked but not priced._`);
|
|
215
|
+
}
|
|
216
|
+
return lines.join('\n');
|
|
217
|
+
}
|
|
@@ -17,10 +17,26 @@ export declare function validatePath(path: string, projectRoot: string): {
|
|
|
17
17
|
absolutePath: string;
|
|
18
18
|
error?: string;
|
|
19
19
|
};
|
|
20
|
+
/**
|
|
21
|
+
* Optional filesystem delegation. When an ACP client advertises `fs`
|
|
22
|
+
* capability (Zed always does, VS Code may), the server should route
|
|
23
|
+
* read/write through the client instead of touching disk directly — that
|
|
24
|
+
* way the client's unsaved buffers, undo history, and virtual filesystems
|
|
25
|
+
* stay authoritative. Callbacks must already use absolute paths.
|
|
26
|
+
*/
|
|
27
|
+
export interface FsCallbacks {
|
|
28
|
+
readTextFile?: (absolutePath: string) => Promise<string>;
|
|
29
|
+
writeTextFile?: (absolutePath: string, content: string) => Promise<void>;
|
|
30
|
+
}
|
|
20
31
|
/**
|
|
21
32
|
* Execute a tool call and return the result.
|
|
33
|
+
*
|
|
34
|
+
* `fs` is optional — if provided and the relevant method is defined, file
|
|
35
|
+
* read/write is delegated to the client. Otherwise we fall back to direct
|
|
36
|
+
* disk I/O. A delegated call that throws also falls back to disk so a
|
|
37
|
+
* single client hiccup doesn't kill the agent loop.
|
|
22
38
|
*/
|
|
23
|
-
export declare function executeTool(toolCall: ToolCall, projectRoot: string): Promise<ToolResult>;
|
|
39
|
+
export declare function executeTool(toolCall: ToolCall, projectRoot: string, fs?: FsCallbacks, mcpSessionId?: string): Promise<ToolResult>;
|
|
24
40
|
/**
|
|
25
41
|
* Create action log from tool result
|
|
26
42
|
*/
|