@adecore/agents 0.31.4 → 0.31.6
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/dist/chat/acp-backend.d.ts +7 -0
- package/dist/chat/acp-backend.js +87 -6
- package/dist/chat/acp-protocol.d.ts +6 -0
- package/dist/chat/acp-protocol.js +34 -9
- package/dist/providers/accounts/acp-account.d.ts +1 -0
- package/dist/providers/accounts/acp-account.js +25 -3
- package/dist/providers/acp-agent-state.d.ts +35 -0
- package/dist/providers/acp-agent-state.js +170 -0
- package/dist/providers/acp-provider.d.ts +2 -0
- package/dist/providers/acp-provider.js +29 -33
- package/dist/providers/catalog.d.ts +2 -0
- package/dist/providers/catalog.js +14 -4
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.js +1 -0
- package/dist/providers/provider.d.ts +1 -0
- package/dist/providers/registry.d.ts +1 -0
- package/dist/providers/registry.js +9 -0
- package/dist/testing/fake-acp.d.ts +3 -0
- package/dist/testing/fake-acp.js +11 -4
- package/dist/usage/aggregate.js +1 -1
- package/dist/usage/index-file.d.ts +2 -0
- package/dist/usage/index-file.js +19 -10
- package/dist/usage/pricing.d.ts +3 -2
- package/dist/usage/pricing.js +42 -8
- package/dist/usage/readers/codex.d.ts +9 -0
- package/dist/usage/readers/codex.js +75 -14
- package/dist/usage/record.d.ts +1 -0
- package/dist/usage/scanner.js +32 -8
- package/package.json +3 -3
- package/src/chat/acp-backend.ts +93 -5
- package/src/chat/acp-protocol.ts +41 -9
- package/src/providers/accounts/acp-account.ts +34 -3
- package/src/providers/acp-agent-state.ts +206 -0
- package/src/providers/acp-provider.ts +33 -37
- package/src/providers/catalog.ts +19 -5
- package/src/providers/index.ts +1 -0
- package/src/providers/provider.ts +2 -0
- package/src/providers/registry.ts +10 -0
- package/src/testing/fake-acp.ts +17 -4
- package/src/usage/aggregate.ts +1 -1
- package/src/usage/index-file.ts +25 -18
- package/src/usage/pricing.ts +46 -8
- package/src/usage/readers/codex.ts +86 -14
- package/src/usage/record.ts +1 -0
- package/src/usage/scanner.ts +35 -8
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { createAcpBackend } from '../chat/acp-backend.js';
|
|
2
2
|
import { askAcpAccount } from './accounts/acp-account.js';
|
|
3
|
+
import { AcpAgentState } from './acp-agent-state.js';
|
|
3
4
|
import { writeAcpUsage } from './acp-usage-log.js';
|
|
4
5
|
import { detectCli } from './detect.js';
|
|
5
6
|
/*
|
|
6
|
-
* What every ACP agent can do through this backend. A file change comes as the text
|
|
7
|
-
* a message names its mentions and attachments by path,
|
|
7
|
+
* What every ACP agent can do through this backend, before what it shows of itself. A file change comes as the text
|
|
8
|
+
* before and after; a message names its mentions and attachments by path, and carries the files themselves where the agent takes them.
|
|
8
9
|
*/
|
|
9
10
|
export const ACP_CAPABILITIES = {
|
|
10
11
|
chat: true,
|
|
@@ -25,38 +26,22 @@ export const ACP_CAPABILITIES = {
|
|
|
25
26
|
};
|
|
26
27
|
/* Agents whose own usage files the scanner reads; a log of their turns would count every one twice. */
|
|
27
28
|
const OWN_USAGE_FILES = new Set(['grok']);
|
|
28
|
-
/* An agent's model id may reach a command line, so one that reads as a flag is left out. */
|
|
29
|
-
function adoptableModels(info) {
|
|
30
|
-
return (info.models?.models ?? []).filter((model) => model.id !== '' && !model.id.startsWith('-')).map((model) => ({ slug: model.id, name: model.name }));
|
|
31
|
-
}
|
|
32
|
-
/* The reasoning effort a session offers, as the `effort` option the composer draws for every provider. */
|
|
33
|
-
function effortOption(info) {
|
|
34
|
-
const level = info.thoughtLevel;
|
|
35
|
-
if (level === null || !level.choices.some((choice) => choice.id === level.current)) {
|
|
36
|
-
return [];
|
|
37
|
-
}
|
|
38
|
-
return [
|
|
39
|
-
{
|
|
40
|
-
id: 'effort',
|
|
41
|
-
label: 'Reasoning',
|
|
42
|
-
type: 'select',
|
|
43
|
-
defaultChoice: level.current,
|
|
44
|
-
choices: level.choices.map((choice) => ({
|
|
45
|
-
id: choice.id,
|
|
46
|
-
label: choice.name,
|
|
47
|
-
...(choice.description === null ? {} : { description: choice.description })
|
|
48
|
-
}))
|
|
49
|
-
}
|
|
50
|
-
];
|
|
51
|
-
}
|
|
52
29
|
export function createAcpProvider(spec) {
|
|
53
|
-
const
|
|
30
|
+
const state = new AcpAgentState({
|
|
31
|
+
catalog: spec.catalog,
|
|
32
|
+
adoptModels: spec.adoptModels !== false,
|
|
33
|
+
store: spec.catalogStore ?? null,
|
|
34
|
+
base: ACP_CAPABILITIES,
|
|
35
|
+
overrides: spec.capabilities ?? {}
|
|
36
|
+
});
|
|
54
37
|
const onSession = (info) => {
|
|
55
|
-
|
|
56
|
-
spec.catalog.adopt(adoptableModels(info), info.models.current, effortOption(info));
|
|
57
|
-
}
|
|
38
|
+
state.session(info);
|
|
58
39
|
spec.onSession?.(info);
|
|
59
40
|
};
|
|
41
|
+
const onCommands = (commands) => {
|
|
42
|
+
state.commandsListed(commands);
|
|
43
|
+
spec.onCommands?.(commands);
|
|
44
|
+
};
|
|
60
45
|
const usageLog = spec.usageLog === undefined || OWN_USAGE_FILES.has(spec.kind) ? null : spec.usageLog;
|
|
61
46
|
const onUsage = usageLog === null
|
|
62
47
|
? spec.onUsage
|
|
@@ -68,7 +53,8 @@ export function createAcpProvider(spec) {
|
|
|
68
53
|
kind: spec.kind,
|
|
69
54
|
name: spec.name,
|
|
70
55
|
catalog: spec.catalog,
|
|
71
|
-
capabilities,
|
|
56
|
+
capabilities: state.capabilities,
|
|
57
|
+
onChange: (listener) => state.onChange(listener),
|
|
72
58
|
command: [spec.command, ...(spec.args ?? [])],
|
|
73
59
|
resumeCommand: spec.resumeCommand ?? '',
|
|
74
60
|
...(spec.ignoresProjectConfig === undefined ? {} : { ignoresProjectConfig: spec.ignoresProjectConfig }),
|
|
@@ -90,9 +76,19 @@ export function createAcpProvider(spec) {
|
|
|
90
76
|
command,
|
|
91
77
|
env: { ...env, ...spec.env },
|
|
92
78
|
...(spec.client ? { client: spec.client } : {}),
|
|
93
|
-
onSession
|
|
79
|
+
onSession,
|
|
80
|
+
onCommands
|
|
94
81
|
})
|
|
95
82
|
}),
|
|
96
|
-
createBackend: createAcpBackend({
|
|
83
|
+
createBackend: createAcpBackend({
|
|
84
|
+
...spec,
|
|
85
|
+
onSession,
|
|
86
|
+
onCommands,
|
|
87
|
+
onContextReport: () => {
|
|
88
|
+
state.contextReported();
|
|
89
|
+
spec.onContextReport?.();
|
|
90
|
+
},
|
|
91
|
+
...(onUsage === undefined ? {} : { onUsage })
|
|
92
|
+
})
|
|
97
93
|
};
|
|
98
94
|
}
|
|
@@ -5,12 +5,14 @@ export type ModelCatalogInput = Omit<ModelCatalogData, 'updatedAt'> & {
|
|
|
5
5
|
export declare class ModelCatalog {
|
|
6
6
|
private readonly shippedAt;
|
|
7
7
|
private data;
|
|
8
|
+
private hostWindows;
|
|
8
9
|
constructor(data?: ModelCatalogInput);
|
|
9
10
|
get defaultModel(): string;
|
|
10
11
|
replace(data: ModelCatalogData): boolean;
|
|
11
12
|
adopt(models: ReadonlyArray<{
|
|
12
13
|
slug: string;
|
|
13
14
|
name: string;
|
|
15
|
+
contextWindow?: number;
|
|
14
16
|
}>, defaultModel: string, options?: readonly ModelOptionDescriptor[]): boolean;
|
|
15
17
|
list(): ModelInfo[];
|
|
16
18
|
nameOf(slug: string): string;
|
|
@@ -11,6 +11,8 @@ function timeOf(data) {
|
|
|
11
11
|
export class ModelCatalog {
|
|
12
12
|
shippedAt;
|
|
13
13
|
data;
|
|
14
|
+
// The sizes the host's own default had before an agent's models replaced it, for an adopted model that names none.
|
|
15
|
+
hostWindows;
|
|
14
16
|
constructor(data = manifest) {
|
|
15
17
|
this.shippedAt = timeOf(data);
|
|
16
18
|
this.data = data;
|
|
@@ -25,13 +27,14 @@ export class ModelCatalog {
|
|
|
25
27
|
}
|
|
26
28
|
const changed = JSON.stringify(data) !== JSON.stringify(this.data);
|
|
27
29
|
this.data = data;
|
|
30
|
+
this.hostWindows = undefined;
|
|
28
31
|
return changed;
|
|
29
32
|
}
|
|
30
33
|
/*
|
|
31
34
|
* Takes the models an agent reported for itself in place of the list, the agent's current model as the
|
|
32
35
|
* default and its order kept; answers whether the models changed. Every model gets the profile the
|
|
33
36
|
* default has now, as an agent names its models but not what each can be asked for, with `options`
|
|
34
|
-
* in place of the profile's options of the same id.
|
|
37
|
+
* in place of the profile's options of the same id, and the context window in tokens a model names as its only size.
|
|
35
38
|
*/
|
|
36
39
|
adopt(models, defaultModel, options = []) {
|
|
37
40
|
if (models.length === 0 || !models.some((model) => model.slug === defaultModel)) {
|
|
@@ -39,12 +42,19 @@ export class ModelCatalog {
|
|
|
39
42
|
}
|
|
40
43
|
const base = this.profileOf(this.data.defaultModel) ?? { options: [], contextWindowTokens: {} };
|
|
41
44
|
const replaced = new Set(options.map((option) => option.id));
|
|
42
|
-
|
|
45
|
+
this.hostWindows ??= base.contextWindowTokens;
|
|
46
|
+
const profile = { ...base, contextWindowTokens: this.hostWindows, options: [...base.options.filter((option) => !replaced.has(option.id)), ...options] };
|
|
47
|
+
const profileName = (window) => (window === undefined ? ADOPTED_PROFILE : `${ADOPTED_PROFILE}-${window}`);
|
|
48
|
+
const profiles = {};
|
|
49
|
+
for (const model of models) {
|
|
50
|
+
profiles[profileName(model.contextWindow)] =
|
|
51
|
+
model.contextWindow === undefined ? profile : { ...profile, contextWindowTokens: { '*': model.contextWindow } };
|
|
52
|
+
}
|
|
43
53
|
const data = {
|
|
44
54
|
...(this.data.updatedAt === undefined ? {} : { updatedAt: this.data.updatedAt }),
|
|
45
55
|
defaultModel,
|
|
46
|
-
profiles
|
|
47
|
-
models: models.map((model) => ({ slug: model.slug, name: model.name, profile:
|
|
56
|
+
profiles,
|
|
57
|
+
models: models.map((model) => ({ slug: model.slug, name: model.name, profile: profileName(model.contextWindow) }))
|
|
48
58
|
};
|
|
49
59
|
const changed = JSON.stringify(data) !== JSON.stringify(this.data);
|
|
50
60
|
this.data = data;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { type AcpCatalogStore } from './acp-agent-state.ts';
|
|
1
2
|
export { ACP_CAPABILITIES, type AcpProviderSpec, createAcpProvider } from './acp-provider.ts';
|
|
2
3
|
export { type AcpAccountProbe, askAcpAccount, type ProbeTimer } from './accounts/acp-account.ts';
|
|
3
4
|
export { AccountError, type AccountLaunches, storedAccount, launchEnv } from './accounts/launch.ts';
|
package/dist/providers/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Public API: exports.test.ts snapshots this list, so a name added or dropped here is an API change.
|
|
2
|
+
export {} from './acp-agent-state.js';
|
|
2
3
|
export { ACP_CAPABILITIES, createAcpProvider } from './acp-provider.js';
|
|
3
4
|
export { askAcpAccount } from './accounts/acp-account.js';
|
|
4
5
|
export { AccountError, storedAccount, launchEnv } from './accounts/launch.js';
|
|
@@ -25,5 +25,6 @@ export interface ChatProvider {
|
|
|
25
25
|
detect(command: string, env: Record<string, string | undefined>): Promise<CliDetection>;
|
|
26
26
|
enabled?(): boolean;
|
|
27
27
|
askAccount?(command: readonly string[], env: Record<string, string | undefined>): Promise<AccountReading>;
|
|
28
|
+
onChange?(listener: () => void): () => void;
|
|
28
29
|
createBackend(launch: BackendLaunch, host: BackendHost): ChatBackend;
|
|
29
30
|
}
|
|
@@ -23,6 +23,7 @@ export declare class ProviderRegistry {
|
|
|
23
23
|
oneShotProvider(preferred?: AgentKind): Promise<ChatProvider | null>;
|
|
24
24
|
catalogFor(kind: AgentKind): ModelCatalog;
|
|
25
25
|
enabled(kind: AgentKind): boolean;
|
|
26
|
+
onChange(listener: (kind: AgentKind) => void): () => void;
|
|
26
27
|
invalidate(kind: AgentKind): void;
|
|
27
28
|
protected fallback(kind: AgentKind): ChatProvider;
|
|
28
29
|
private detection;
|
|
@@ -97,6 +97,15 @@ export class ProviderRegistry {
|
|
|
97
97
|
enabled(kind) {
|
|
98
98
|
return this.get(kind).enabled?.() !== false;
|
|
99
99
|
}
|
|
100
|
+
/* Calls the listener with the kind of every provider whose models or capabilities changed, so a host can list them again; answers how to stop. */
|
|
101
|
+
onChange(listener) {
|
|
102
|
+
const stops = this.providers.flatMap((provider) => (provider.onChange ? [provider.onChange(() => listener(provider.kind))] : []));
|
|
103
|
+
return () => {
|
|
104
|
+
for (const stop of stops) {
|
|
105
|
+
stop();
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
100
109
|
invalidate(kind) {
|
|
101
110
|
this.cache.delete(kind);
|
|
102
111
|
}
|
|
@@ -8,6 +8,9 @@ export interface FakeAcpOptions {
|
|
|
8
8
|
closeSession?: boolean;
|
|
9
9
|
turnUsage?: Record<string, number>;
|
|
10
10
|
reasoningEffort?: 'accept' | 'reject';
|
|
11
|
+
contextWindows?: boolean;
|
|
12
|
+
commands?: readonly string[];
|
|
13
|
+
embeddedContext?: boolean;
|
|
11
14
|
}
|
|
12
15
|
export declare function fakeAcpWith(options: FakeAcpOptions): FakeCli;
|
|
13
16
|
export declare const fakeAcp: FakeCli;
|
package/dist/testing/fake-acp.js
CHANGED
|
@@ -38,9 +38,10 @@ export function fakeAcpWith(options) {
|
|
|
38
38
|
prompt = null;
|
|
39
39
|
};
|
|
40
40
|
const sessionState = () => {
|
|
41
|
+
const meta = (totalContextTokens) => (options.contextWindows ? { _meta: { totalContextTokens } } : {});
|
|
41
42
|
const modelChoices = [
|
|
42
|
-
{ value: 'fake-small', name: 'Fake small' },
|
|
43
|
-
{ value: 'fake-large', name: 'Fake large' }
|
|
43
|
+
{ value: 'fake-small', name: 'Fake small', ...meta(128000) },
|
|
44
|
+
{ value: 'fake-large', name: 'Fake large', ...meta(1000000) }
|
|
44
45
|
];
|
|
45
46
|
const effortChoices = [
|
|
46
47
|
{ value: 'low', name: 'Low', description: 'Answers quickly' },
|
|
@@ -53,7 +54,7 @@ export function fakeAcpWith(options) {
|
|
|
53
54
|
return {
|
|
54
55
|
...(modes.length === 0 ? {} : { modes: { currentModeId: mode, availableModes: modes.map((id) => ({ id, name: id })) } }),
|
|
55
56
|
...(options.legacyModels
|
|
56
|
-
? { models: { currentModelId: model, availableModels: modelChoices.map((choice) => ({ modelId:
|
|
57
|
+
? { models: { currentModelId: model, availableModels: modelChoices.map(({ value, ...choice }) => ({ modelId: value, ...choice })) } }
|
|
57
58
|
: {
|
|
58
59
|
configOptions: [
|
|
59
60
|
{ id: 'model', name: 'Model', type: 'select', category: 'model', currentValue: model, options: modelChoices },
|
|
@@ -193,7 +194,7 @@ export function fakeAcpWith(options) {
|
|
|
193
194
|
protocolVersion: 1,
|
|
194
195
|
agentCapabilities: {
|
|
195
196
|
loadSession: options.loadSession ?? true,
|
|
196
|
-
promptCapabilities: { image: true, audio: false, embeddedContext:
|
|
197
|
+
promptCapabilities: { image: true, audio: false, embeddedContext: options.embeddedContext === true },
|
|
197
198
|
sessionCapabilities: { ...(options.resumeSession ? { resume: {} } : {}), ...(options.closeSession ? { close: {} } : {}) }
|
|
198
199
|
},
|
|
199
200
|
authMethods: options.authMethod === undefined ? [] : [{ id: options.authMethod, name: 'Sign in' }],
|
|
@@ -211,6 +212,12 @@ export function fakeAcpWith(options) {
|
|
|
211
212
|
}
|
|
212
213
|
sessionId = `fake-session-${Math.random().toString(36).slice(2, 8)}`;
|
|
213
214
|
sessionHistory.set(sessionId, []);
|
|
215
|
+
if (options.commands !== undefined) {
|
|
216
|
+
update({
|
|
217
|
+
sessionUpdate: 'available_commands_update',
|
|
218
|
+
availableCommands: options.commands.map((name) => ({ name, description: name }))
|
|
219
|
+
});
|
|
220
|
+
}
|
|
214
221
|
respond(id, { sessionId, ...sessionState() });
|
|
215
222
|
return;
|
|
216
223
|
case 'session/load':
|
package/dist/usage/aggregate.js
CHANGED
|
@@ -111,7 +111,7 @@ export async function aggregate(records, payload, prices, known) {
|
|
|
111
111
|
if (day < payload.from || day > payload.to) {
|
|
112
112
|
continue;
|
|
113
113
|
}
|
|
114
|
-
const lookup = prices.look(record.model);
|
|
114
|
+
const lookup = prices.look(record.model, record.serviceTier, record.timestampMs);
|
|
115
115
|
const cost = record.costUsd ?? (lookup.price === null ? 0 : costOf(record.totals, lookup.price));
|
|
116
116
|
foldBucket(buckets, slot, record, account, lookup.price, cost);
|
|
117
117
|
foldModel(models, record, account, lookup, cost);
|
|
@@ -10,6 +10,8 @@ export interface IndexedFile {
|
|
|
10
10
|
tail: UsageRecord[];
|
|
11
11
|
codex: CodexParserState | null;
|
|
12
12
|
head: string | null;
|
|
13
|
+
parserVersion?: number;
|
|
14
|
+
compressed?: boolean;
|
|
13
15
|
}
|
|
14
16
|
export type UsageIndex = Map<string, IndexedFile>;
|
|
15
17
|
export declare function encodeIndex(index: UsageIndex): string;
|
package/dist/usage/index-file.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { UsageProviderSchema } from '@adecore/agent-contracts';
|
|
2
|
-
/*
|
|
3
|
-
const INDEX_VERSION =
|
|
2
|
+
/* Old records survive deleted transcripts; the scanner reparses existing Codex files with its current reader. */
|
|
3
|
+
const INDEX_VERSION = 3;
|
|
4
4
|
/* Before accounts: every record was the default account's, so it reads as that without a scan. */
|
|
5
5
|
const ACCOUNTLESS_VERSION = 1;
|
|
6
6
|
class Interner {
|
|
@@ -39,8 +39,11 @@ export function encodeIndex(index) {
|
|
|
39
39
|
record.dedupeKey ?? 0,
|
|
40
40
|
record.account === undefined ? 0 : accounts.index(record.account) + 1
|
|
41
41
|
];
|
|
42
|
-
if (record.costUsd !== undefined) {
|
|
43
|
-
counted
|
|
42
|
+
if (record.costUsd !== undefined || record.serviceTier !== undefined) {
|
|
43
|
+
counted[13] = record.costUsd;
|
|
44
|
+
}
|
|
45
|
+
if (record.serviceTier !== undefined) {
|
|
46
|
+
counted[14] = record.serviceTier;
|
|
44
47
|
}
|
|
45
48
|
return counted;
|
|
46
49
|
};
|
|
@@ -53,7 +56,9 @@ export function encodeIndex(index) {
|
|
|
53
56
|
c: file.codex,
|
|
54
57
|
r: file.records.map(row),
|
|
55
58
|
t: file.tail.map(row),
|
|
56
|
-
...(file.head === null ? {} : { h: file.head })
|
|
59
|
+
...(file.head === null ? {} : { h: file.head }),
|
|
60
|
+
...(file.parserVersion === undefined ? {} : { v: file.parserVersion }),
|
|
61
|
+
...(file.compressed === undefined ? {} : { z: file.compressed })
|
|
57
62
|
};
|
|
58
63
|
}
|
|
59
64
|
const document = {
|
|
@@ -68,11 +73,12 @@ export function encodeIndex(index) {
|
|
|
68
73
|
}
|
|
69
74
|
function isRow(row, version) {
|
|
70
75
|
return (Array.isArray(row) &&
|
|
71
|
-
(version === ACCOUNTLESS_VERSION ? row.length === 12 : row.length
|
|
76
|
+
(version === ACCOUNTLESS_VERSION ? row.length === 12 : row.length >= 13 && row.length <= (version === INDEX_VERSION ? 15 : 14)) &&
|
|
72
77
|
row.slice(0, 11).every((value) => typeof value === 'number') &&
|
|
73
78
|
(typeof row[11] === 'string' || row[11] === 0) &&
|
|
74
79
|
(version === ACCOUNTLESS_VERSION || typeof row[12] === 'number') &&
|
|
75
|
-
(row.length < 14 || typeof row[13] === 'number')
|
|
80
|
+
(row.length < 14 || row[13] === null || row[13] === undefined || typeof row[13] === 'number') &&
|
|
81
|
+
(row.length < 15 || typeof row[14] === 'string'));
|
|
76
82
|
}
|
|
77
83
|
/*
|
|
78
84
|
* A row that does not read back costs a cold parse of the file it belonged to, never a wrong total,
|
|
@@ -88,7 +94,7 @@ export function decodeIndex(text) {
|
|
|
88
94
|
return index;
|
|
89
95
|
}
|
|
90
96
|
const version = document?.version;
|
|
91
|
-
if ((version !== INDEX_VERSION && version !== ACCOUNTLESS_VERSION) ||
|
|
97
|
+
if ((version !== INDEX_VERSION && version !== 2 && version !== ACCOUNTLESS_VERSION) ||
|
|
92
98
|
!Array.isArray(document.models) ||
|
|
93
99
|
!Array.isArray(document.sessions) ||
|
|
94
100
|
!Array.isArray(document.folders)) {
|
|
@@ -126,7 +132,8 @@ export function decodeIndex(text) {
|
|
|
126
132
|
cwd,
|
|
127
133
|
...(account === undefined ? {} : { account }),
|
|
128
134
|
totals: { calls: row[4], input: row[5], cacheRead: row[6], cacheWrite: row[7], cacheWrite1h: row[8], output: row[9], reasoning: row[10] },
|
|
129
|
-
...(row[13] ===
|
|
135
|
+
...(typeof row[13] === 'number' ? { costUsd: row[13] } : {}),
|
|
136
|
+
...(typeof row[14] === 'string' ? { serviceTier: row[14] } : {}),
|
|
130
137
|
dedupeKey: row[11] === 0 ? null : row[11]
|
|
131
138
|
});
|
|
132
139
|
}
|
|
@@ -143,7 +150,9 @@ export function decodeIndex(text) {
|
|
|
143
150
|
records,
|
|
144
151
|
tail,
|
|
145
152
|
codex: file.c ?? null,
|
|
146
|
-
head: typeof file.h === 'string' ? file.h : null
|
|
153
|
+
head: typeof file.h === 'string' ? file.h : null,
|
|
154
|
+
...(typeof file.v === 'number' ? { parserVersion: file.v } : {}),
|
|
155
|
+
...(typeof file.z === 'boolean' ? { compressed: file.z } : {})
|
|
147
156
|
});
|
|
148
157
|
}
|
|
149
158
|
}
|
package/dist/usage/pricing.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface ModelPrice {
|
|
|
6
6
|
cacheRead: number;
|
|
7
7
|
cacheWrite: number;
|
|
8
8
|
cacheWrite1h: number;
|
|
9
|
+
tiers?: Partial<Record<string, ModelPrice>>;
|
|
9
10
|
}
|
|
10
11
|
export type PriceTable = Map<string, ModelPrice>;
|
|
11
12
|
export interface PriceLookup {
|
|
@@ -14,7 +15,7 @@ export interface PriceLookup {
|
|
|
14
15
|
pricedAs: string | null;
|
|
15
16
|
}
|
|
16
17
|
export declare function parsePriceTable(document: unknown): PriceTable;
|
|
17
|
-
export declare function lookupPrice(table: PriceTable, model: string): PriceLookup;
|
|
18
|
+
export declare function lookupPrice(table: PriceTable, model: string, serviceTier?: string, timestampMs?: number): PriceLookup;
|
|
18
19
|
export declare function costOf(totals: UsageTotals, price: ModelPrice): number;
|
|
19
20
|
export declare function cacheSavingsOf(totals: UsageTotals, price: ModelPrice): number;
|
|
20
21
|
export declare class PriceBook {
|
|
@@ -27,7 +28,7 @@ export declare class PriceBook {
|
|
|
27
28
|
private inFlight;
|
|
28
29
|
constructor(home: string, allowFetch?: boolean);
|
|
29
30
|
get pricing(): UsagePricing;
|
|
30
|
-
look(model: string): PriceLookup;
|
|
31
|
+
look(model: string, serviceTier?: string, timestampMs?: number): PriceLookup;
|
|
31
32
|
load(): Promise<void>;
|
|
32
33
|
ensure(): Promise<boolean>;
|
|
33
34
|
private get file();
|
package/dist/usage/pricing.js
CHANGED
|
@@ -50,6 +50,23 @@ export function parsePriceTable(document) {
|
|
|
50
50
|
cacheWrite: rate(value.cache_creation_input_token_cost) ?? input * CACHE_WRITE_SHARE,
|
|
51
51
|
cacheWrite1h: input * CACHE_WRITE_1H_SHARE
|
|
52
52
|
};
|
|
53
|
+
const tiers = {};
|
|
54
|
+
for (const [tier, multiplier] of [
|
|
55
|
+
['priority', 2],
|
|
56
|
+
['flex', 0.5],
|
|
57
|
+
['ultrafast', 6]
|
|
58
|
+
]) {
|
|
59
|
+
tiers[tier] = {
|
|
60
|
+
input: rate(value[`input_cost_per_token_${tier}`]) ?? input * multiplier,
|
|
61
|
+
output: rate(value[`output_cost_per_token_${tier}`]) ?? output * multiplier,
|
|
62
|
+
cacheRead: rate(value[`cache_read_input_token_cost_${tier}`]) ?? price.cacheRead * multiplier,
|
|
63
|
+
cacheWrite: rate(value[`cache_creation_input_token_cost_${tier}`]) ?? price.cacheWrite * multiplier,
|
|
64
|
+
cacheWrite1h: price.cacheWrite1h * multiplier
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (value.litellm_provider === 'openai') {
|
|
68
|
+
price.tiers = tiers;
|
|
69
|
+
}
|
|
53
70
|
const key = name.trim().toLowerCase();
|
|
54
71
|
table.set(key, price);
|
|
55
72
|
const bare = key.slice(key.lastIndexOf('/') + 1);
|
|
@@ -64,12 +81,23 @@ function withoutDate(key) {
|
|
|
64
81
|
const match = /^(.*)-\d{6,8}$/.exec(key);
|
|
65
82
|
return match === null ? null : match[1];
|
|
66
83
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
84
|
+
// The internal review alias has no published price; expose ccusage's dated estimate as a family match.
|
|
85
|
+
// https://github.com/ccusage/ccusage/blob/main/rust/adapters/codex/src/codex-auto-review-fallbacks.json
|
|
86
|
+
const REVIEW_MODELS = [
|
|
87
|
+
['2026-07-30', 'gpt-5.6-luna'],
|
|
88
|
+
['2026-03-05', 'gpt-5.4'],
|
|
89
|
+
['2026-02-05', 'gpt-5.3-codex'],
|
|
90
|
+
['2025-12-11', 'gpt-5.2-codex'],
|
|
91
|
+
['2025-11-13', 'gpt-5.1-codex'],
|
|
92
|
+
['2025-09-15', 'gpt-5-codex'],
|
|
93
|
+
['2025-08-07', 'gpt-5']
|
|
94
|
+
];
|
|
95
|
+
export function lookupPrice(table, model, serviceTier, timestampMs) {
|
|
96
|
+
const found = lookupBasePrice(table, model, timestampMs);
|
|
97
|
+
const tier = serviceTier === 'fast' ? 'priority' : serviceTier;
|
|
98
|
+
return { ...found, price: (tier === undefined ? undefined : found.price?.tiers?.[tier]) ?? found.price };
|
|
99
|
+
}
|
|
100
|
+
function lookupBasePrice(table, model, timestampMs) {
|
|
73
101
|
const key = model.trim().toLowerCase().split('[')[0];
|
|
74
102
|
const bare = key.slice(key.lastIndexOf('/') + 1);
|
|
75
103
|
if (bare === '' || AMBIGUOUS.has(bare)) {
|
|
@@ -79,6 +107,12 @@ export function lookupPrice(table, model) {
|
|
|
79
107
|
if (exact !== undefined) {
|
|
80
108
|
return { price: exact, basis: 'exact', pricedAs: null };
|
|
81
109
|
}
|
|
110
|
+
if (bare === 'codex-auto-review' && timestampMs !== undefined && Number.isFinite(timestampMs)) {
|
|
111
|
+
const date = new Date(timestampMs).toISOString().slice(0, 10);
|
|
112
|
+
const fallback = REVIEW_MODELS.find(([released]) => date >= released)?.[1] ?? 'gpt-5';
|
|
113
|
+
const found = lookupBasePrice(table, fallback);
|
|
114
|
+
return { price: found.price, basis: found.price === null ? 'unknown' : 'family', pricedAs: found.price === null ? null : fallback };
|
|
115
|
+
}
|
|
82
116
|
const dated = withoutDate(bare);
|
|
83
117
|
const undated = dated === null ? undefined : table.get(dated);
|
|
84
118
|
if (undated !== undefined && dated !== null) {
|
|
@@ -128,8 +162,8 @@ export class PriceBook {
|
|
|
128
162
|
get pricing() {
|
|
129
163
|
return { source: this.table.size === 0 ? 'none' : this.source, fetchedAt: this.fetchedAt, models: this.table.size };
|
|
130
164
|
}
|
|
131
|
-
look(model) {
|
|
132
|
-
return lookupPrice(this.table, model);
|
|
165
|
+
look(model, serviceTier, timestampMs) {
|
|
166
|
+
return lookupPrice(this.table, model, serviceTier, timestampMs);
|
|
133
167
|
}
|
|
134
168
|
/* Reads the table an earlier run stored, once; asking LiteLLM is left to `ensure`. */
|
|
135
169
|
load() {
|
|
@@ -16,6 +16,15 @@ export interface CodexParserState {
|
|
|
16
16
|
suppressing: boolean;
|
|
17
17
|
anchorMs: number;
|
|
18
18
|
lastSignature: string | null;
|
|
19
|
+
serviceTier: string | null;
|
|
20
|
+
pendingCompactions: Record<string, {
|
|
21
|
+
record: UsageRecord;
|
|
22
|
+
counts: CodexCounts;
|
|
23
|
+
threadTotal: CodexCounts | null;
|
|
24
|
+
}>;
|
|
25
|
+
compacted: string[];
|
|
26
|
+
handledResponses: string[];
|
|
27
|
+
latestResponse: string | null;
|
|
19
28
|
}
|
|
20
29
|
export declare function createCodexState(): CodexParserState;
|
|
21
30
|
export declare function cloneCodexState(state: CodexParserState): CodexParserState;
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { asObject, asString, int } from '../record.js';
|
|
2
|
-
/* A `token_count` line carries the counts; the other two carry the model and the directory they belong to. */
|
|
3
2
|
export function codexMightCarryUsage(line) {
|
|
4
|
-
return
|
|
3
|
+
return ['token_count', 'turn_context', 'session_meta', 'thread_settings_applied', 'token_usage_record', 'compacted'].some((type) => line.includes(type));
|
|
5
4
|
}
|
|
6
5
|
/* A fork replays its ancestors' events into the new file the moment it is created, so anything this
|
|
7
6
|
close to the file's own start is a copy of work that was already counted somewhere else. */
|
|
@@ -15,11 +14,16 @@ export function createCodexState() {
|
|
|
15
14
|
sawSessionMeta: false,
|
|
16
15
|
suppressing: false,
|
|
17
16
|
anchorMs: 0,
|
|
18
|
-
lastSignature: null
|
|
17
|
+
lastSignature: null,
|
|
18
|
+
serviceTier: null,
|
|
19
|
+
pendingCompactions: {},
|
|
20
|
+
compacted: [],
|
|
21
|
+
handledResponses: [],
|
|
22
|
+
latestResponse: null
|
|
19
23
|
};
|
|
20
24
|
}
|
|
21
25
|
export function cloneCodexState(state) {
|
|
22
|
-
return
|
|
26
|
+
return structuredClone(state);
|
|
23
27
|
}
|
|
24
28
|
function countsOf(usage) {
|
|
25
29
|
return {
|
|
@@ -52,13 +56,8 @@ export function isForkOrSubagent(payload) {
|
|
|
52
56
|
const spawn = asObject(asObject(payload.source)?.subagent)?.thread_spawn;
|
|
53
57
|
return typeof asObject(spawn)?.parent_thread_id === 'string';
|
|
54
58
|
}
|
|
55
|
-
/*
|
|
56
|
-
|
|
57
|
-
* reports a cumulative total per turn, so the call is the difference with the line before it; when
|
|
58
|
-
* that difference falls the run started over and `last_token_usage` is the turn on its own. Codex
|
|
59
|
-
* counts cache reads and writes inside `input_tokens`, unlike Anthropic, so what is left after
|
|
60
|
-
* taking both out is the input that was actually paid for at the full rate.
|
|
61
|
-
*/
|
|
59
|
+
/* Codex includes cache reads and writes in input_tokens. Compaction can appear separately from
|
|
60
|
+
cumulative token_count events, so response identities prevent charging the same request twice. */
|
|
62
61
|
export function parseCodexLine(line, state) {
|
|
63
62
|
let parsed;
|
|
64
63
|
try {
|
|
@@ -97,6 +96,16 @@ export function parseCodexLine(line, state) {
|
|
|
97
96
|
}
|
|
98
97
|
return null;
|
|
99
98
|
}
|
|
99
|
+
if (payload.type === 'thread_settings_applied') {
|
|
100
|
+
const settings = asObject(payload.thread_settings);
|
|
101
|
+
if (settings !== null && typeof settings.service_tier === 'string') {
|
|
102
|
+
state.serviceTier = settings.service_tier;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
if (record.type === 'token_usage_record' || record.type === 'compacted') {
|
|
107
|
+
return parseCompaction(record, payload, state);
|
|
108
|
+
}
|
|
100
109
|
if (payload.type !== 'token_count') {
|
|
101
110
|
return null;
|
|
102
111
|
}
|
|
@@ -129,6 +138,58 @@ export function parseCodexLine(line, state) {
|
|
|
129
138
|
// The first event that is not part of the replayed burst is this thread's own work.
|
|
130
139
|
state.suppressing = false;
|
|
131
140
|
}
|
|
141
|
+
let dedupeKey = null;
|
|
142
|
+
const pending = state.latestResponse === null ? undefined : state.pendingCompactions[state.latestResponse];
|
|
143
|
+
if (pending !== undefined && (sameCounts(pending.counts, counts) || sameCounts(pending.threadTotal, cumulative))) {
|
|
144
|
+
dedupeKey = `codex:response:${state.latestResponse}`;
|
|
145
|
+
delete state.pendingCompactions[state.latestResponse];
|
|
146
|
+
state.handledResponses.push(state.latestResponse);
|
|
147
|
+
}
|
|
148
|
+
return usageRecord(counts, timestampMs, state.model, state, dedupeKey);
|
|
149
|
+
}
|
|
150
|
+
function sameCounts(first, second) {
|
|
151
|
+
return (first !== null &&
|
|
152
|
+
first.input === second.input &&
|
|
153
|
+
first.cached === second.cached &&
|
|
154
|
+
first.cacheWrite === second.cacheWrite &&
|
|
155
|
+
first.output === second.output &&
|
|
156
|
+
first.reasoning === second.reasoning);
|
|
157
|
+
}
|
|
158
|
+
function parseCompaction(record, payload, state) {
|
|
159
|
+
const id = asString(record.type === 'compacted' ? payload.compaction_response_id : payload.response_id);
|
|
160
|
+
if (id === '' || state.handledResponses.includes(id)) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
if (record.type === 'compacted') {
|
|
164
|
+
if (!state.compacted.includes(id)) {
|
|
165
|
+
state.compacted.push(id);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
const usage = asObject(payload.usage);
|
|
170
|
+
const model = asString(payload.model) || state.model;
|
|
171
|
+
const timestampMs = Date.parse(asString(record.timestamp));
|
|
172
|
+
if (usage === null || model === null || !Number.isFinite(timestampMs)) {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
const counts = countsOf(usage);
|
|
176
|
+
const parsed = usageRecord(counts, timestampMs, model, state, `codex:response:${id}`);
|
|
177
|
+
if (parsed === null) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
state.latestResponse = id;
|
|
181
|
+
const threadTotal = asObject(payload.thread_token_usage);
|
|
182
|
+
state.pendingCompactions[id] ??= { record: parsed, counts, threadTotal: threadTotal === null ? null : countsOf(threadTotal) };
|
|
183
|
+
}
|
|
184
|
+
const pending = state.pendingCompactions[id];
|
|
185
|
+
if (!state.compacted.includes(id) || pending === undefined) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
state.handledResponses.push(id);
|
|
189
|
+
delete state.pendingCompactions[id];
|
|
190
|
+
return pending.record;
|
|
191
|
+
}
|
|
192
|
+
function usageRecord(counts, timestampMs, model, state, dedupeKey) {
|
|
132
193
|
const uncached = Math.max(0, counts.input - counts.cached - counts.cacheWrite);
|
|
133
194
|
const totals = {
|
|
134
195
|
calls: 1,
|
|
@@ -145,11 +206,11 @@ export function parseCodexLine(line, state) {
|
|
|
145
206
|
return {
|
|
146
207
|
provider: 'codex',
|
|
147
208
|
timestampMs,
|
|
148
|
-
model
|
|
209
|
+
model,
|
|
149
210
|
sessionId: state.sessionId,
|
|
150
211
|
cwd: state.cwd,
|
|
151
212
|
totals,
|
|
152
|
-
|
|
153
|
-
dedupeKey
|
|
213
|
+
...(state.serviceTier === null ? {} : { serviceTier: state.serviceTier }),
|
|
214
|
+
dedupeKey
|
|
154
215
|
};
|
|
155
216
|
}
|