Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/model-catalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
{ "id": "claude-sonnet-5", "label": "Sonnet 5", "minAppVersion": null, "maxAppVersion": null },
{ "id": "claude-sonnet-4-6", "label": "Sonnet 4.6", "minAppVersion": null, "maxAppVersion": null },
{ "id": "claude-sonnet-4-6[1m]", "label": "Sonnet 4.6 · 1M", "minAppVersion": null, "maxAppVersion": null },
{ "id": "claude-haiku-5-5", "label": "Haiku 5.5", "minAppVersion": null, "maxAppVersion": null },
{ "id": "claude-haiku-4-5-20251001", "label": "Haiku 4.5", "minAppVersion": null, "maxAppVersion": null }
],
"codex": [
Expand Down
2 changes: 1 addition & 1 deletion src/main/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -813,7 +813,7 @@ export function resetConfig(): HarnessConfig {
* src/renderer/src/store/config.ts reads. */
const MODEL_GOD = 'claude-opus-4-8'; // orchestration — highest capability
const MODEL_WORKER = 'claude-sonnet-4-6'; // general execution
const MODEL_HELPER = 'claude-haiku-4-5-20251001'; // narrow, cheap helpers
const MODEL_HELPER = 'claude-haiku-5-5'; // narrow, cheap helpers

/** Minimal structural shape for tiering — a subset of AgentMeta so config.ts
* stays free of a hive.ts import. */
Expand Down
3 changes: 3 additions & 0 deletions src/main/pricing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ export interface ModelPrice {
const OPUS: ModelPrice = { inputPerM: 15, outputPerM: 75, cacheReadPerM: 1.5, cacheWritePerM: 18.75 };
const SONNET: ModelPrice = { inputPerM: 3, outputPerM: 15, cacheReadPerM: 0.3, cacheWritePerM: 3.75 };
const HAIKU: ModelPrice = { inputPerM: 0.8, outputPerM: 4, cacheReadPerM: 0.08, cacheWritePerM: 1.0 };
// Haiku 5.5 lists far below the row above, so it cannot share it.
const HAIKU_5_5: ModelPrice = { inputPerM: 0.1, outputPerM: 0.5, cacheReadPerM: 0.01, cacheWritePerM: 0.125 };

/** When the model id is unknown, assume Sonnet (the historical default). */
const DEFAULT_PRICE: ModelPrice = SONNET;
Expand All @@ -45,6 +47,7 @@ export function normalizeModel(model: string | undefined | null): string {
export function priceFor(model: string | undefined | null): ModelPrice {
const m = normalizeModel(model).toLowerCase();
if (m.includes('opus')) return OPUS;
if (m.includes('haiku-5-5')) return HAIKU_5_5;
if (m.includes('haiku')) return HAIKU;
if (m.includes('sonnet')) return SONNET;
return DEFAULT_PRICE;
Expand Down
2 changes: 1 addition & 1 deletion src/main/reflect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import { runHiddenClaude } from './hiddenClaude';
/** Total memory.md budget — mirrors the janitor's CONTEXT_BUDGET_BYTES (128 KB). */
const BUDGET_BYTES = 131_072;
/** Cheap tail-summarizer (DECIDED by god). The verify gate covers quality. */
const CONDENSE_MODEL = 'claude-haiku-4-5';
const CONDENSE_MODEL = 'claude-haiku-5-5';
/** Hard cap so a wedged headless run can't stall the reflect loop, for a small
* eviction payload. Scaled up (see `summarize`) for larger ones — a fixed cap
* was a chunk of the 'hidden session timed out' abort class: a big eviction
Expand Down
1 change: 1 addition & 0 deletions src/shared/modelCatalog.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
{ "id": "claude-sonnet-5", "label": "Sonnet 5", "minAppVersion": null, "maxAppVersion": null },
{ "id": "claude-sonnet-4-6", "label": "Sonnet 4.6", "minAppVersion": null, "maxAppVersion": null },
{ "id": "claude-sonnet-4-6[1m]", "label": "Sonnet 4.6 · 1M", "minAppVersion": null, "maxAppVersion": null },
{ "id": "claude-haiku-5-5", "label": "Haiku 5.5", "minAppVersion": null, "maxAppVersion": null },
{ "id": "claude-haiku-4-5-20251001", "label": "Haiku 4.5", "minAppVersion": null, "maxAppVersion": null }
],
"codex": [
Expand Down
28 changes: 28 additions & 0 deletions test/helper-model.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use strict';

/**
* The model a narrow helper agent (triage, routing, verification, formatting)
* gets by default. It has to be one the app ships in its catalog: the settings
* picker can only show what the catalog lists, and an id the CLI has never
* shipped fails every turn.
*/

const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const loadTs = require('./load-ts.cjs');

const { modelForRole } = loadTs('src/main/config.ts');
const catalog = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'src/shared/modelCatalog.json'), 'utf8'));

test('a narrow helper defaults to Haiku 5.5', () => {
for (const role of ['triage', 'router', 'verifier', 'formatter', 'summarizer', 'classifier', 'labeler']) {
assert.equal(modelForRole({ role }), 'claude-haiku-5-5', role);
}
});

test('the helper default is a model the catalog lists', () => {
const helper = modelForRole({ role: 'triage' });
assert.ok(catalog.providers.claude.some((m) => m.id === helper), helper);
});
1 change: 1 addition & 0 deletions test/model-catalog.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const SHIPPED = {
["claude-sonnet-5", "Sonnet 5"],
["claude-sonnet-4-6", "Sonnet 4.6"],
["claude-sonnet-4-6[1m]", "Sonnet 4.6 · 1M"],
["claude-haiku-5-5", "Haiku 5.5"],
["claude-haiku-4-5-20251001", "Haiku 4.5"]
],
antigravity: [
Expand Down
31 changes: 31 additions & 0 deletions test/pricing-haiku.test.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
'use strict';

/**
* The fallback price table, for the Haiku generations. The live path takes
* Claude's own per-model cost; this table is what a cost estimate falls back to
* when that is missing, so a Haiku 5.5 session priced as Haiku 4.5 would read
* eight times too expensive.
*/

const test = require('node:test');
const assert = require('node:assert/strict');
const loadTs = require('./load-ts.cjs');

const { priceFor, estimateCostUsd } = loadTs('src/main/pricing.ts');

test('Haiku 5.5 falls back to its own list price', () => {
assert.deepEqual(priceFor('claude-haiku-5-5'), {
inputPerM: 0.1, outputPerM: 0.5, cacheReadPerM: 0.01, cacheWritePerM: 0.125,
});
// A million of each kind of token, priced end to end.
const usd = estimateCostUsd('claude-haiku-5-5', {
inputTokens: 1e6, outputTokens: 1e6, cacheReadTokens: 1e6, cacheWriteTokens: 1e6,
});
assert.ok(Math.abs(usd - 0.735) < 1e-9, String(usd));
});

test('Haiku 4.5 keeps the price it had', () => {
assert.deepEqual(priceFor('claude-haiku-4-5-20251001'), {
inputPerM: 0.8, outputPerM: 4, cacheReadPerM: 0.08, cacheWritePerM: 1.0,
});
});
13 changes: 13 additions & 0 deletions test/reflect.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,19 @@ test('timeout budget scales with the eviction payload size', async () => {
assert.ok(calls[0].opts.timeoutMs > 180_000, 'a large eviction payload must get more than the base budget');
});

test('the condenser summarizes with Haiku 5.5, a model the app ships in its catalog', async () => {
// A slug the CLI does not know fails every condense with a 400, and the janitor
// would abort on every pass without ever shrinking a memory file.
const { home } = homeWith('a1', fixture({ sections: [section('a', 300), section('b', 300)] }));
const { MemoryReflector, calls } = loadReflectWithStub(() => ({ ok: false, error: 'hidden session timed out' }));
const reflector = makeReflector({ MemoryReflector }, home, { recentKeep: 1 });
await reflector.reflectNow('a1');
assert.equal(calls.length, 1);
assert.equal(calls[0].opts.model, 'claude-haiku-5-5');
const catalog = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'src/shared/modelCatalog.json'), 'utf8'));
assert.ok(catalog.providers.claude.some((m) => m.id === calls[0].opts.model), 'the condenser model is in the catalog');
});

test('repro: unparseable response aborts cleanly, no wasted retry', async () => {
const { home, mem } = homeWith('a1', fixture({
sections: [section('old one', 300), section('old two', 300), section('newest', 300)]
Expand Down
Loading