@animalabs/connectome-host 0.3.5 → 0.3.7
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/.env.example +6 -0
- package/README.md +5 -0
- package/bun.lock +8 -4
- package/docs/AGENT-ONBOARDING.md +6 -1
- package/package.json +3 -3
- package/scripts/import-codex-rollout.ts +288 -0
- package/src/call-ledger.ts +371 -0
- package/src/call-pricing.ts +119 -0
- package/src/index.ts +78 -27
- package/src/logging-adapter.ts +72 -9
- package/src/modules/fleet-module.ts +17 -7
- package/src/modules/mcpl-admin-module.ts +6 -0
- package/src/modules/time-module.ts +15 -9
- package/src/modules/web-ui-module.ts +25 -0
- package/src/modules/web-ui-observers.ts +16 -3
- package/src/recipe.ts +48 -3
- package/src/strategies/frontdesk-strategy.ts +10 -4
- package/src/web/protocol.ts +89 -0
- package/test/call-ledger.test.ts +91 -0
- package/test/call-pricing.test.ts +69 -0
- package/test/frontdesk-strategy.test.ts +10 -0
- package/test/import-codex-rollout.test.ts +36 -0
- package/test/logging-adapter.test.ts +58 -1
- package/test/recipe-cache-ttl.test.ts +7 -3
- package/test/recipe-provider.test.ts +36 -0
- package/test/recipe-timezone.test.ts +20 -0
- package/test/time-module.test.ts +13 -0
- package/test/web-ui-observers.test.ts +43 -0
- package/web/src/App.tsx +9 -0
- package/web/src/ObserverGate.tsx +21 -11
- package/web/src/Usage.tsx +116 -2
- package/web/src/wire.ts +5 -2
package/src/recipe.ts
CHANGED
|
@@ -75,6 +75,10 @@ export interface RecipeStrategy {
|
|
|
75
75
|
export interface RecipeAgent {
|
|
76
76
|
name?: string;
|
|
77
77
|
model?: string;
|
|
78
|
+
/** IANA zone used when rendering wall-clock times to the agent. */
|
|
79
|
+
timezone?: string;
|
|
80
|
+
/** Provider transport. Omitted preserves the historical Anthropic default. */
|
|
81
|
+
provider?: 'anthropic' | 'openai-responses';
|
|
78
82
|
systemPrompt: string;
|
|
79
83
|
maxTokens?: number;
|
|
80
84
|
/**
|
|
@@ -86,9 +90,8 @@ export interface RecipeAgent {
|
|
|
86
90
|
/** Per-agent context compile budget (input tokens). When unset, the
|
|
87
91
|
* ContextManager default (100k) applies. Raise for large-context models. */
|
|
88
92
|
contextBudgetTokens?: number;
|
|
89
|
-
/** Prompt-cache TTL ('5m' | '1h') forwarded to the provider.
|
|
90
|
-
*
|
|
91
|
-
* the full context to cache after every gap. Unset: provider default (5m). */
|
|
93
|
+
/** Prompt-cache TTL ('5m' | '1h') forwarded to the provider. Defaults to
|
|
94
|
+
* '1h'; set '5m' explicitly for high-frequency, sub-5-minute workloads. */
|
|
92
95
|
cacheTtl?: '5m' | '1h';
|
|
93
96
|
strategy?: RecipeStrategy;
|
|
94
97
|
/**
|
|
@@ -100,6 +103,13 @@ export interface RecipeAgent {
|
|
|
100
103
|
enabled: boolean;
|
|
101
104
|
budgetTokens?: number;
|
|
102
105
|
};
|
|
106
|
+
/** Stateless OpenAI Responses settings. Only used with
|
|
107
|
+
* `provider: "openai-responses"`. */
|
|
108
|
+
responses?: {
|
|
109
|
+
reasoningEffort?: 'none' | 'low' | 'medium' | 'high' | 'xhigh' | 'max';
|
|
110
|
+
reasoningContext?: 'current_turn' | 'all_turns';
|
|
111
|
+
compactThreshold?: number;
|
|
112
|
+
};
|
|
103
113
|
/**
|
|
104
114
|
* Content-refusal handling. When `autoRewind` is on, a `stop_reason: refusal`
|
|
105
115
|
* turn triggers an automatic rewind of the triggering turn + retry (keeping
|
|
@@ -500,6 +510,7 @@ export const DEFAULT_RECIPE: Recipe = {
|
|
|
500
510
|
description: 'General-purpose assistant with tool access',
|
|
501
511
|
agent: {
|
|
502
512
|
name: 'agent',
|
|
513
|
+
cacheTtl: '1h',
|
|
503
514
|
systemPrompt: [
|
|
504
515
|
'You are a helpful assistant. You have access to tools provided by connected MCP servers.',
|
|
505
516
|
'Use them to help the user with their tasks.',
|
|
@@ -678,6 +689,39 @@ export function validateRecipe(raw: unknown): Recipe {
|
|
|
678
689
|
throw new Error('Recipe agent must have a "systemPrompt" string');
|
|
679
690
|
}
|
|
680
691
|
|
|
692
|
+
if (agent.provider !== undefined && agent.provider !== 'anthropic' && agent.provider !== 'openai-responses') {
|
|
693
|
+
throw new Error(`Recipe agent.provider must be 'anthropic' or 'openai-responses', got ${JSON.stringify(agent.provider)}.`);
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (agent.timezone !== undefined) {
|
|
697
|
+
if (typeof agent.timezone !== 'string' || !agent.timezone.trim()) {
|
|
698
|
+
throw new Error('Recipe agent.timezone must be a non-empty IANA time zone string.');
|
|
699
|
+
}
|
|
700
|
+
try {
|
|
701
|
+
new Intl.DateTimeFormat('en-US', { timeZone: agent.timezone }).format(new Date(0));
|
|
702
|
+
} catch {
|
|
703
|
+
throw new Error(`Recipe agent.timezone is not a valid IANA time zone: ${JSON.stringify(agent.timezone)}.`);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (agent.responses !== undefined) {
|
|
708
|
+
if (!agent.responses || typeof agent.responses !== 'object' || Array.isArray(agent.responses)) {
|
|
709
|
+
throw new Error('Recipe agent.responses must be an object.');
|
|
710
|
+
}
|
|
711
|
+
const responses = agent.responses as Record<string, unknown>;
|
|
712
|
+
const efforts = ['none', 'low', 'medium', 'high', 'xhigh', 'max'];
|
|
713
|
+
if (responses.reasoningEffort !== undefined && !efforts.includes(String(responses.reasoningEffort))) {
|
|
714
|
+
throw new Error(`Invalid agent.responses.reasoningEffort ${JSON.stringify(responses.reasoningEffort)}.`);
|
|
715
|
+
}
|
|
716
|
+
if (responses.reasoningContext !== undefined && responses.reasoningContext !== 'current_turn' && responses.reasoningContext !== 'all_turns') {
|
|
717
|
+
throw new Error(`Invalid agent.responses.reasoningContext ${JSON.stringify(responses.reasoningContext)}.`);
|
|
718
|
+
}
|
|
719
|
+
if (responses.compactThreshold !== undefined &&
|
|
720
|
+
(typeof responses.compactThreshold !== 'number' || responses.compactThreshold <= 0)) {
|
|
721
|
+
throw new Error('Recipe agent.responses.compactThreshold must be a positive number.');
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
681
725
|
if (agent.maxStreamTokens !== undefined && (typeof agent.maxStreamTokens !== 'number' || agent.maxStreamTokens <= 0)) {
|
|
682
726
|
throw new Error('Recipe agent.maxStreamTokens must be a positive number.');
|
|
683
727
|
}
|
|
@@ -687,6 +731,7 @@ export function validateRecipe(raw: unknown): Recipe {
|
|
|
687
731
|
if (agent.cacheTtl !== undefined && agent.cacheTtl !== '5m' && agent.cacheTtl !== '1h') {
|
|
688
732
|
throw new Error(`Recipe agent.cacheTtl must be '5m' or '1h', got ${JSON.stringify(agent.cacheTtl)}.`);
|
|
689
733
|
}
|
|
734
|
+
agent.cacheTtl ??= '1h';
|
|
690
735
|
|
|
691
736
|
// Validate agent.thinking if present. Catches typos and constraint
|
|
692
737
|
// violations (notably max_tokens > budget_tokens) at recipe-load time
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
SummaryEntry,
|
|
10
10
|
} from '@animalabs/context-manager';
|
|
11
11
|
import type { ContentBlock } from '@animalabs/membrane';
|
|
12
|
+
import { formatZonedTime, resolveTimeZone } from '@animalabs/agent-framework';
|
|
12
13
|
|
|
13
14
|
// Structural mirror of AutobiographicalStrategy's internal Chunk.
|
|
14
15
|
// Kept inline because @animalabs/context-manager does not currently export it.
|
|
@@ -24,7 +25,7 @@ interface Chunk {
|
|
|
24
25
|
phaseType?: string;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
export type FrontdeskStrategyOptions = Partial<AutobiographicalConfig
|
|
28
|
+
export type FrontdeskStrategyOptions = Partial<AutobiographicalConfig> & { timeZone?: string };
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* Chatbot-flavoured context strategy for agents that receive messages via
|
|
@@ -43,6 +44,13 @@ export class FrontdeskStrategy extends AutobiographicalStrategy {
|
|
|
43
44
|
override readonly name: string = 'frontdesk';
|
|
44
45
|
|
|
45
46
|
private salientSourceIds: Set<string> = new Set();
|
|
47
|
+
private readonly timeZone: string;
|
|
48
|
+
|
|
49
|
+
constructor(options: FrontdeskStrategyOptions = {}) {
|
|
50
|
+
const { timeZone, ...strategyOptions } = options;
|
|
51
|
+
super(strategyOptions);
|
|
52
|
+
this.timeZone = resolveTimeZone(timeZone);
|
|
53
|
+
}
|
|
46
54
|
|
|
47
55
|
override select(
|
|
48
56
|
store: MessageStoreView,
|
|
@@ -146,9 +154,7 @@ export class FrontdeskStrategy extends AutobiographicalStrategy {
|
|
|
146
154
|
}
|
|
147
155
|
if (!d && msgTs instanceof Date && !isNaN(msgTs.getTime())) d = msgTs;
|
|
148
156
|
if (!d) return null;
|
|
149
|
-
|
|
150
|
-
const mm = String(d.getMinutes()).padStart(2, '0');
|
|
151
|
-
return `${hh}:${mm}`;
|
|
157
|
+
return formatZonedTime(d, this.timeZone);
|
|
152
158
|
}
|
|
153
159
|
|
|
154
160
|
// ==========================================================================
|
package/src/web/protocol.ts
CHANGED
|
@@ -93,6 +93,9 @@ export interface WelcomeMessage {
|
|
|
93
93
|
/** Per-agent cost breakdown for the parent process, present when the
|
|
94
94
|
* framework's usage tracker has data. Empty during cold start. */
|
|
95
95
|
perAgentCost?: PerAgentCost[];
|
|
96
|
+
/** Recent provider calls with cache verdicts. Present when the host's
|
|
97
|
+
* provider adapter exposes the call ledger. */
|
|
98
|
+
callLedger?: CallLedgerSnapshot;
|
|
96
99
|
}
|
|
97
100
|
|
|
98
101
|
/**
|
|
@@ -164,6 +167,91 @@ export interface UsageMessage {
|
|
|
164
167
|
perAgentCost?: PerAgentCost[];
|
|
165
168
|
}
|
|
166
169
|
|
|
170
|
+
/** Live replacement snapshot emitted after each provider call. */
|
|
171
|
+
export interface CallLedgerMessage {
|
|
172
|
+
type: 'call-ledger';
|
|
173
|
+
ledger: CallLedgerSnapshot;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export type CallLedgerVerdict =
|
|
177
|
+
| 'HIT'
|
|
178
|
+
| 'hit+extend'
|
|
179
|
+
| 'uncached'
|
|
180
|
+
| 'rewrite:expired'
|
|
181
|
+
| 'rewrite:prefix-mutated'
|
|
182
|
+
| 'rewrite:prefix-truncated'
|
|
183
|
+
| 'rewrite:unexplained'
|
|
184
|
+
| 'first-write'
|
|
185
|
+
| 'ERROR'
|
|
186
|
+
| 'empty'
|
|
187
|
+
| 'unknown';
|
|
188
|
+
|
|
189
|
+
export interface CallLedgerRow {
|
|
190
|
+
id: string;
|
|
191
|
+
timestamp: string;
|
|
192
|
+
kind: 'complete' | 'stream';
|
|
193
|
+
/** Honest call-class estimate; `~` means the trigger plumbing did not
|
|
194
|
+
* provide a definitive origin. */
|
|
195
|
+
originEstimate: 'turn~' | 'aux~';
|
|
196
|
+
model: string;
|
|
197
|
+
messages: number;
|
|
198
|
+
durationMs: number;
|
|
199
|
+
tokens: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
200
|
+
cost?: CallCostBreakdown;
|
|
201
|
+
cache: {
|
|
202
|
+
/** Undefined for older compact logs that predate marker summaries. */
|
|
203
|
+
breakpoints?: number;
|
|
204
|
+
ttls: string[];
|
|
205
|
+
effectiveTtl: '5m' | '1h';
|
|
206
|
+
};
|
|
207
|
+
verdict: CallLedgerVerdict;
|
|
208
|
+
cause: string;
|
|
209
|
+
stopReason?: string;
|
|
210
|
+
error?: string;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export interface CallCostBreakdown {
|
|
214
|
+
input: number;
|
|
215
|
+
cacheWrite5m: number;
|
|
216
|
+
cacheWrite1h: number;
|
|
217
|
+
cacheRead: number;
|
|
218
|
+
output: number;
|
|
219
|
+
total: number;
|
|
220
|
+
currency: 'USD';
|
|
221
|
+
/** `billing` means every charged usage bucket and pricing modifier was
|
|
222
|
+
* known. Unknown/mixed buckets are left unpriced rather than estimated. */
|
|
223
|
+
grade: 'billing';
|
|
224
|
+
pricingVersion: string;
|
|
225
|
+
rates: {
|
|
226
|
+
inputPerMillion: number;
|
|
227
|
+
outputPerMillion: number;
|
|
228
|
+
cacheWrite5mPerMillion: number;
|
|
229
|
+
cacheWrite1hPerMillion: number;
|
|
230
|
+
cacheReadPerMillion: number;
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export interface CallLedgerSnapshot {
|
|
235
|
+
/** Oldest to newest; clients may reverse for display. */
|
|
236
|
+
rows: CallLedgerRow[];
|
|
237
|
+
summary: {
|
|
238
|
+
calls: number;
|
|
239
|
+
input: number;
|
|
240
|
+
output: number;
|
|
241
|
+
cacheRead: number;
|
|
242
|
+
cacheWrite: number;
|
|
243
|
+
cacheHitRatio: number;
|
|
244
|
+
cost?: {
|
|
245
|
+
total: number;
|
|
246
|
+
currency: 'USD';
|
|
247
|
+
pricedCalls: number;
|
|
248
|
+
unpricedCalls: number;
|
|
249
|
+
pricingVersion: string;
|
|
250
|
+
};
|
|
251
|
+
byVerdict: Partial<Record<CallLedgerVerdict, number>>;
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
167
255
|
export interface TokenUsage {
|
|
168
256
|
input: number;
|
|
169
257
|
output: number;
|
|
@@ -352,6 +440,7 @@ export type WebUiServerMessage =
|
|
|
352
440
|
| ChildEventMessage
|
|
353
441
|
| CommandResultMessage
|
|
354
442
|
| UsageMessage
|
|
443
|
+
| CallLedgerMessage
|
|
355
444
|
| BranchChangedMessage
|
|
356
445
|
| SessionChangedMessage
|
|
357
446
|
| PeekMessage
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { CallLedger, summarizeCacheControls, type ProviderCallRecord } from '../src/call-ledger.js';
|
|
3
|
+
|
|
4
|
+
const at = (seconds: number): string => new Date(Date.UTC(2026, 0, 1, 0, 0, seconds)).toISOString();
|
|
5
|
+
|
|
6
|
+
function call(overrides: Partial<ProviderCallRecord> = {}): ProviderCallRecord {
|
|
7
|
+
return {
|
|
8
|
+
timestamp: at(0),
|
|
9
|
+
kind: 'stream',
|
|
10
|
+
durationMs: 1000,
|
|
11
|
+
model: 'claude-fable-5',
|
|
12
|
+
messages: 10,
|
|
13
|
+
inputTokens: 2,
|
|
14
|
+
outputTokens: 100,
|
|
15
|
+
cacheReadTokens: 0,
|
|
16
|
+
cacheWriteTokens: 0,
|
|
17
|
+
cacheBreakpoints: 4,
|
|
18
|
+
cacheTtls: ['1h', '1h', '1h', '1h'],
|
|
19
|
+
stopReason: 'end_turn',
|
|
20
|
+
...overrides,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe('CallLedger cache verdicts', () => {
|
|
25
|
+
test('distinguishes first write, hit, and expired rewrite using the effective TTL', () => {
|
|
26
|
+
const ledger = new CallLedger({ hydrate: false, defaultTtl: '1h' });
|
|
27
|
+
ledger.record(call({ timestamp: at(0), cacheWriteTokens: 100_000 }));
|
|
28
|
+
ledger.record(call({ timestamp: at(1800), cacheReadTokens: 100_000 }));
|
|
29
|
+
ledger.record(call({ timestamp: at(5500), cacheWriteTokens: 101_000 }));
|
|
30
|
+
|
|
31
|
+
const rows = ledger.snapshot().rows;
|
|
32
|
+
expect(rows.map((r) => r.verdict)).toEqual(['first-write', 'HIT', 'rewrite:expired']);
|
|
33
|
+
expect(rows[2]!.cause).toContain('gap 3700s > ttl 3600s');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('calls without cache flags are visibly uncached', () => {
|
|
37
|
+
const ledger = new CallLedger({ hydrate: false });
|
|
38
|
+
ledger.record(call({ kind: 'complete', cacheBreakpoints: 0, cacheTtls: [], inputTokens: 48_000 }));
|
|
39
|
+
expect(ledger.snapshot().rows[0]).toMatchObject({
|
|
40
|
+
verdict: 'uncached',
|
|
41
|
+
cause: 'no cache_control flags in request',
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('reports hit+extend and an aggregate cache ratio', () => {
|
|
46
|
+
const ledger = new CallLedger({ hydrate: false });
|
|
47
|
+
ledger.record(call({ inputTokens: 100, cacheReadTokens: 900, cacheWriteTokens: 100 }));
|
|
48
|
+
const snap = ledger.snapshot();
|
|
49
|
+
expect(snap.rows[0]!.verdict).toBe('hit+extend');
|
|
50
|
+
expect(snap.summary.cacheHitRatio).toBeCloseTo(900 / 1100);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('allocates an auditable per-call cost and reconciles the retained total', () => {
|
|
54
|
+
const ledger = new CallLedger({ hydrate: false });
|
|
55
|
+
ledger.record(call({
|
|
56
|
+
cacheWriteTokens: 336_010,
|
|
57
|
+
cacheWrite5mTokens: 336_010,
|
|
58
|
+
cacheWrite1hTokens: 0,
|
|
59
|
+
cacheWriteBucketsAuthoritative: true,
|
|
60
|
+
cacheTtls: ['5m'],
|
|
61
|
+
outputTokens: 639,
|
|
62
|
+
serviceTier: 'standard',
|
|
63
|
+
inferenceGeo: 'global',
|
|
64
|
+
}));
|
|
65
|
+
ledger.record(call({ model: 'unknown-model' }));
|
|
66
|
+
|
|
67
|
+
const snap = ledger.snapshot();
|
|
68
|
+
expect(snap.rows[0]!.cost?.total).toBeCloseTo(4.232095, 9);
|
|
69
|
+
expect(snap.rows[1]!.cost).toBeUndefined();
|
|
70
|
+
expect(snap.summary.cost).toMatchObject({
|
|
71
|
+
total: 4.232095,
|
|
72
|
+
pricedCalls: 1,
|
|
73
|
+
unpricedCalls: 1,
|
|
74
|
+
currency: 'USD',
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('does not guess a cache-write TTL bucket from request flags', () => {
|
|
79
|
+
const ledger = new CallLedger({ hydrate: false });
|
|
80
|
+
ledger.record(call({ cacheWriteTokens: 100_000, cacheTtls: ['1h'] }));
|
|
81
|
+
expect(ledger.snapshot().rows[0]!.cost).toBeUndefined();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('summarizeCacheControls inventories markers without retaining content', () => {
|
|
86
|
+
const raw = {
|
|
87
|
+
system: [{ type: 'text', text: 'secret', cache_control: { type: 'ephemeral', ttl: '1h' } }],
|
|
88
|
+
messages: [{ role: 'user', content: [{ type: 'text', text: 'private', cache_control: { type: 'ephemeral' } }] }],
|
|
89
|
+
};
|
|
90
|
+
expect(summarizeCacheControls(raw)).toEqual({ count: 2, ttls: ['1h', 'default'] });
|
|
91
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { ANTHROPIC_PRICING_VERSION, priceAnthropicCall } from '../src/call-pricing.js';
|
|
3
|
+
|
|
4
|
+
const timestamp = '2026-07-13T12:00:00.000Z';
|
|
5
|
+
|
|
6
|
+
function usage(overrides: Partial<Parameters<typeof priceAnthropicCall>[2]> = {}) {
|
|
7
|
+
return {
|
|
8
|
+
inputTokens: 0,
|
|
9
|
+
outputTokens: 0,
|
|
10
|
+
cacheReadTokens: 0,
|
|
11
|
+
cacheWrite5mTokens: 0,
|
|
12
|
+
cacheWrite1hTokens: 0,
|
|
13
|
+
unclassifiedCacheWriteTokens: 0,
|
|
14
|
+
serviceTier: 'standard',
|
|
15
|
+
inferenceGeo: 'global',
|
|
16
|
+
...overrides,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe('Anthropic per-call pricing', () => {
|
|
21
|
+
test('reproduces the ledger-dashboard Fable/Mythos 5m sample exactly', () => {
|
|
22
|
+
const cost = priceAnthropicCall('claude-fable-5', timestamp, usage({
|
|
23
|
+
inputTokens: 2,
|
|
24
|
+
cacheWrite5mTokens: 336_010,
|
|
25
|
+
outputTokens: 639,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
expect(cost?.total).toBeCloseTo(4.232095, 9);
|
|
29
|
+
expect(cost).toMatchObject({
|
|
30
|
+
cacheWrite5m: 4.200125,
|
|
31
|
+
cacheWrite1h: 0,
|
|
32
|
+
currency: 'USD',
|
|
33
|
+
grade: 'billing',
|
|
34
|
+
pricingVersion: ANTHROPIC_PRICING_VERSION,
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('prices 1h writes at 2x base input and cache reads at 0.1x', () => {
|
|
39
|
+
const write = priceAnthropicCall('claude-mythos-5', timestamp, usage({ cacheWrite1hTokens: 336_010 }));
|
|
40
|
+
const read = priceAnthropicCall('claude-mythos-5', timestamp, usage({ cacheReadTokens: 336_010 }));
|
|
41
|
+
expect(write?.cacheWrite1h).toBeCloseTo(6.7202, 9);
|
|
42
|
+
expect(read?.cacheRead).toBeCloseTo(0.33601, 9);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('applies the US-only inference multiplier to every token category', () => {
|
|
46
|
+
const global = priceAnthropicCall('claude-fable-5', timestamp, usage({ inputTokens: 1_000_000 }));
|
|
47
|
+
const us = priceAnthropicCall('claude-fable-5', timestamp, usage({
|
|
48
|
+
inputTokens: 1_000_000,
|
|
49
|
+
inferenceGeo: 'us',
|
|
50
|
+
}));
|
|
51
|
+
expect(global?.total).toBe(10);
|
|
52
|
+
expect(us?.total).toBeCloseTo(11, 9);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('leaves unknown rates, custom tiers, and unclassified writes unpriced', () => {
|
|
56
|
+
expect(priceAnthropicCall('unknown-model', timestamp, usage())).toBeUndefined();
|
|
57
|
+
expect(priceAnthropicCall('claude-fable-5', timestamp, usage({ serviceTier: 'priority' }))).toBeUndefined();
|
|
58
|
+
expect(priceAnthropicCall('claude-fable-5', timestamp, usage({
|
|
59
|
+
unclassifiedCacheWriteTokens: 1,
|
|
60
|
+
}))).toBeUndefined();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('honors the published Sonnet 5 promotional cutoff', () => {
|
|
64
|
+
const promo = priceAnthropicCall('claude-sonnet-5', '2026-08-31T23:59:59Z', usage({ inputTokens: 1_000_000 }));
|
|
65
|
+
const standard = priceAnthropicCall('claude-sonnet-5', '2026-09-01T00:00:00Z', usage({ inputTokens: 1_000_000 }));
|
|
66
|
+
expect(promo?.total).toBe(2);
|
|
67
|
+
expect(standard?.total).toBe(3);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -81,6 +81,7 @@ function makeStrategy(): TestFrontdesk {
|
|
|
81
81
|
targetChunkTokens: 50,
|
|
82
82
|
autoTickOnNewMessage: false,
|
|
83
83
|
maxMessageTokens: 0,
|
|
84
|
+
timeZone: 'UTC',
|
|
84
85
|
});
|
|
85
86
|
}
|
|
86
87
|
|
|
@@ -110,6 +111,15 @@ describe('provenance wrapping', () => {
|
|
|
110
111
|
expect(header!.endsWith('\n')).toBe(true);
|
|
111
112
|
});
|
|
112
113
|
|
|
114
|
+
test('renders provenance time in the configured zone', () => {
|
|
115
|
+
const s = new TestFrontdesk({ timeZone: 'America/Los_Angeles' });
|
|
116
|
+
const m = msg('User', 'hello', {
|
|
117
|
+
serverId: 'zulip',
|
|
118
|
+
timestamp: '2026-07-17T14:32:00Z',
|
|
119
|
+
});
|
|
120
|
+
expect(s.pub_buildHeader(m)).toContain('07:32');
|
|
121
|
+
});
|
|
122
|
+
|
|
113
123
|
test('wrapProvenance prepends header into the first text block', () => {
|
|
114
124
|
const s = makeStrategy();
|
|
115
125
|
const m = msg('User', 'hello', {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import {
|
|
3
|
+
projectItem,
|
|
4
|
+
reconstructEffectiveHistory,
|
|
5
|
+
} from '../scripts/import-codex-rollout.js';
|
|
6
|
+
|
|
7
|
+
describe('Codex rollout import', () => {
|
|
8
|
+
test('uses replacement_history as the authoritative compaction boundary', () => {
|
|
9
|
+
const history = reconstructEffectiveHistory([
|
|
10
|
+
{ type: 'response_item', payload: { type: 'message', id: 'discarded' } },
|
|
11
|
+
{
|
|
12
|
+
type: 'compacted', timestamp: '2026-07-13T00:00:00Z',
|
|
13
|
+
payload: { replacement_history: [
|
|
14
|
+
{ type: 'message', role: 'user', content: 'kept' },
|
|
15
|
+
{ type: 'compaction', id: 'cmp_1', encrypted_content: 'opaque' },
|
|
16
|
+
] },
|
|
17
|
+
},
|
|
18
|
+
{ type: 'response_item', payload: { type: 'reasoning', id: 'rs_2', encrypted_content: 'tail' } },
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
expect(history.map(entry => entry.item.id ?? entry.item.type)).toEqual([
|
|
22
|
+
'message', 'cmp_1', 'rs_2',
|
|
23
|
+
]);
|
|
24
|
+
expect(history[0]!.restoredByCompaction).toBe(true);
|
|
25
|
+
expect(history[2]!.restoredByCompaction).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('keeps the exact native item on the Chronicle projection block', () => {
|
|
29
|
+
const item = {
|
|
30
|
+
type: 'message', id: 'msg_1', role: 'assistant', phase: 'commentary',
|
|
31
|
+
content: [{ type: 'output_text', text: 'hello' }],
|
|
32
|
+
};
|
|
33
|
+
const [block] = projectItem(item);
|
|
34
|
+
expect(block).toMatchObject({ type: 'text', text: 'hello', rawItem: item });
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, test, expect } from 'bun:test';
|
|
2
2
|
import { LoggingAnthropicAdapter } from '../src/logging-adapter.js';
|
|
3
|
+
import type { ProviderCallRecord } from '../src/call-ledger.js';
|
|
3
4
|
import type { ProviderRequest, ProviderResponse } from '@animalabs/membrane';
|
|
4
5
|
|
|
5
6
|
// Regression guard for the reasoning passthrough. `withReasoning` injects
|
|
@@ -57,7 +58,7 @@ describe('LoggingAnthropicAdapter.withReasoning', () => {
|
|
|
57
58
|
describe('LoggingAnthropicAdapter request logging', () => {
|
|
58
59
|
const adapter = new LoggingAnthropicAdapter({ apiKey: 'test' }, '/dev/null');
|
|
59
60
|
const internals = adapter as unknown as {
|
|
60
|
-
requestSummary(r: ProviderRequest): Record<string, unknown>;
|
|
61
|
+
requestSummary(r: ProviderRequest, raw?: unknown): Record<string, unknown>;
|
|
61
62
|
refusalRawRequest(r: ProviderResponse, raw: unknown): unknown;
|
|
62
63
|
};
|
|
63
64
|
|
|
@@ -76,6 +77,20 @@ describe('LoggingAnthropicAdapter request logging', () => {
|
|
|
76
77
|
});
|
|
77
78
|
});
|
|
78
79
|
|
|
80
|
+
test('summarizes provider cache markers without retaining prompt content', () => {
|
|
81
|
+
const raw = {
|
|
82
|
+
messages: [{
|
|
83
|
+
role: 'user',
|
|
84
|
+
content: [{ type: 'text', text: 'do not log me', cache_control: { type: 'ephemeral', ttl: '1h' } }],
|
|
85
|
+
}],
|
|
86
|
+
};
|
|
87
|
+
expect(internals.requestSummary(baseRequest, raw)).toMatchObject({
|
|
88
|
+
cacheBreakpoints: 1,
|
|
89
|
+
cacheTtls: ['1h'],
|
|
90
|
+
});
|
|
91
|
+
expect(JSON.stringify(internals.requestSummary(baseRequest, raw))).not.toContain('do not log me');
|
|
92
|
+
});
|
|
93
|
+
|
|
79
94
|
test('retains the raw request only for refusals', () => {
|
|
80
95
|
const rawRequest = { messages: ['forensic context'] };
|
|
81
96
|
const success = { raw: { stop_reason: 'end_turn' } } as unknown as ProviderResponse;
|
|
@@ -84,4 +99,46 @@ describe('LoggingAnthropicAdapter request logging', () => {
|
|
|
84
99
|
expect(internals.refusalRawRequest(success, rawRequest)).toBeUndefined();
|
|
85
100
|
expect(internals.refusalRawRequest(refusal, rawRequest)).toBe(rawRequest);
|
|
86
101
|
});
|
|
102
|
+
|
|
103
|
+
test('forwards authoritative billing buckets from the provider response', () => {
|
|
104
|
+
const calls: ProviderCallRecord[] = [];
|
|
105
|
+
const observed = new LoggingAnthropicAdapter(
|
|
106
|
+
{ apiKey: 'test' },
|
|
107
|
+
'/dev/null',
|
|
108
|
+
undefined,
|
|
109
|
+
(call) => calls.push(call),
|
|
110
|
+
) as unknown as {
|
|
111
|
+
observeCall(
|
|
112
|
+
kind: 'complete' | 'stream',
|
|
113
|
+
timestamp: string,
|
|
114
|
+
durationMs: number,
|
|
115
|
+
request: ProviderRequest,
|
|
116
|
+
rawRequest: unknown,
|
|
117
|
+
response: ProviderResponse,
|
|
118
|
+
): void;
|
|
119
|
+
};
|
|
120
|
+
const response = {
|
|
121
|
+
usage: { inputTokens: 2, outputTokens: 10, cacheCreationTokens: 100, cacheReadTokens: 50 },
|
|
122
|
+
raw: {
|
|
123
|
+
usage: {
|
|
124
|
+
cache_creation: {
|
|
125
|
+
ephemeral_5m_input_tokens: 25,
|
|
126
|
+
ephemeral_1h_input_tokens: 75,
|
|
127
|
+
},
|
|
128
|
+
service_tier: 'standard',
|
|
129
|
+
inference_geo: 'global',
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
} as unknown as ProviderResponse;
|
|
133
|
+
|
|
134
|
+
observed.observeCall('stream', '2026-07-13T00:00:00Z', 10, baseRequest, {}, response);
|
|
135
|
+
expect(calls[0]).toMatchObject({
|
|
136
|
+
cacheWriteTokens: 100,
|
|
137
|
+
cacheWrite5mTokens: 25,
|
|
138
|
+
cacheWrite1hTokens: 75,
|
|
139
|
+
cacheWriteBucketsAuthoritative: true,
|
|
140
|
+
serviceTier: 'standard',
|
|
141
|
+
inferenceGeo: 'global',
|
|
142
|
+
});
|
|
143
|
+
});
|
|
87
144
|
});
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* inference).
|
|
5
5
|
*/
|
|
6
6
|
import { describe, test, expect } from 'bun:test';
|
|
7
|
-
import { validateRecipe } from '../src/recipe.js';
|
|
7
|
+
import { DEFAULT_RECIPE, validateRecipe } from '../src/recipe.js';
|
|
8
8
|
|
|
9
9
|
function recipeWithCacheTtl(cacheTtl?: unknown) {
|
|
10
10
|
return {
|
|
@@ -17,6 +17,10 @@ function recipeWithCacheTtl(cacheTtl?: unknown) {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
describe('recipe agent.cacheTtl validation', () => {
|
|
20
|
+
test('the shipped default recipe explicitly uses "1h"', () => {
|
|
21
|
+
expect(DEFAULT_RECIPE.agent.cacheTtl).toBe('1h');
|
|
22
|
+
});
|
|
23
|
+
|
|
20
24
|
test('accepts "5m"', () => {
|
|
21
25
|
expect(validateRecipe(recipeWithCacheTtl('5m')).agent.cacheTtl).toBe('5m');
|
|
22
26
|
});
|
|
@@ -25,8 +29,8 @@ describe('recipe agent.cacheTtl validation', () => {
|
|
|
25
29
|
expect(validateRecipe(recipeWithCacheTtl('1h')).agent.cacheTtl).toBe('1h');
|
|
26
30
|
});
|
|
27
31
|
|
|
28
|
-
test('
|
|
29
|
-
expect(validateRecipe(recipeWithCacheTtl()).agent.cacheTtl).
|
|
32
|
+
test('defaults unset cacheTtl to "1h"', () => {
|
|
33
|
+
expect(validateRecipe(recipeWithCacheTtl()).agent.cacheTtl).toBe('1h');
|
|
30
34
|
});
|
|
31
35
|
|
|
32
36
|
test('rejects a typo\'d TTL at load time', () => {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { validateRecipe } from '../src/recipe.js';
|
|
3
|
+
|
|
4
|
+
function recipe(agent: Record<string, unknown> = {}) {
|
|
5
|
+
return { name: 'provider-test', agent: { systemPrompt: 'sys', ...agent } };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
describe('recipe provider validation', () => {
|
|
9
|
+
test('preserves Anthropic as the omitted provider and accepts Responses', () => {
|
|
10
|
+
expect(validateRecipe(recipe()).agent.provider).toBeUndefined();
|
|
11
|
+
expect(validateRecipe(recipe({ provider: 'openai-responses' })).agent.provider)
|
|
12
|
+
.toBe('openai-responses');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('accepts Responses reasoning and compaction settings', () => {
|
|
16
|
+
expect(validateRecipe(recipe({
|
|
17
|
+
provider: 'openai-responses',
|
|
18
|
+
responses: {
|
|
19
|
+
reasoningEffort: 'xhigh',
|
|
20
|
+
reasoningContext: 'all_turns',
|
|
21
|
+
compactThreshold: 100_000,
|
|
22
|
+
},
|
|
23
|
+
})).agent.responses).toEqual({
|
|
24
|
+
reasoningEffort: 'xhigh',
|
|
25
|
+
reasoningContext: 'all_turns',
|
|
26
|
+
compactThreshold: 100_000,
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('rejects unknown providers and malformed Responses settings', () => {
|
|
31
|
+
expect(() => validateRecipe(recipe({ provider: 'openai-chat' }))).toThrow(/agent.provider/);
|
|
32
|
+
expect(() => validateRecipe(recipe({ responses: { reasoningEffort: 'ultra' } }))).toThrow(/reasoningEffort/);
|
|
33
|
+
expect(() => validateRecipe(recipe({ responses: { reasoningContext: 'previous_turn' } }))).toThrow(/reasoningContext/);
|
|
34
|
+
expect(() => validateRecipe(recipe({ responses: { compactThreshold: 0 } }))).toThrow(/compactThreshold/);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { validateRecipe } from '../src/recipe.js';
|
|
3
|
+
|
|
4
|
+
function recipe(timezone?: unknown) {
|
|
5
|
+
return { name: 'tz', agent: { systemPrompt: 'test', ...(timezone === undefined ? {} : { timezone }) } };
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
describe('recipe agent.timezone validation', () => {
|
|
9
|
+
test('accepts an IANA timezone', () => {
|
|
10
|
+
expect(validateRecipe(recipe('America/Los_Angeles')).agent.timezone).toBe('America/Los_Angeles');
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test('allows the setting to be omitted', () => {
|
|
14
|
+
expect(validateRecipe(recipe()).agent.timezone).toBeUndefined();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('rejects invalid timezone names', () => {
|
|
18
|
+
expect(() => validateRecipe(recipe('Pacific/Definitely_Not'))).toThrow(/valid IANA time zone/);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test';
|
|
2
|
+
import { formatNow } from '../src/modules/time-module.js';
|
|
3
|
+
|
|
4
|
+
describe('TimeModule presentation timezone', () => {
|
|
5
|
+
test('formats current time independently of the host timezone', () => {
|
|
6
|
+
expect(formatNow(new Date('2026-07-15T12:34:56.789Z'), 'America/Los_Angeles')).toEqual({
|
|
7
|
+
iso: '2026-07-15T05:34:56.789-07:00',
|
|
8
|
+
local: '2026-07-15T05:34:56.789-07:00 [America/Los_Angeles]',
|
|
9
|
+
timezone: 'America/Los_Angeles',
|
|
10
|
+
unixMs: 1_784_118_896_789,
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
});
|