castle-web-cli 0.4.90 → 0.4.91
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/agent.js +23 -4
- package/dist/metering.d.ts +21 -0
- package/dist/metering.js +75 -0
- package/dist/native/loop.js +1 -0
- package/dist/native/openrouter.d.ts +1 -0
- package/dist/native/openrouter.js +1 -0
- package/dist/native/types.d.ts +1 -0
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -24,6 +24,7 @@ import { rawDataToString } from "./ide.js";
|
|
|
24
24
|
import { buildRouterPrompt, buildTaskPrompt, userTurnInstruction, CLAUDE_TASK_SYSTEM_REMINDER, } from "./agent-prompts.js";
|
|
25
25
|
import { checkOpenrouterKey, checkOpenrouterModel, openrouterCatalogEntry, primeOpenrouterCatalog, } from "./openrouter-catalog.js";
|
|
26
26
|
import { classifyProviderError, failureCopy, } from "./agent-failures.js";
|
|
27
|
+
import { meteringHeaders, newAgentSessionId, withCustomHeaders, } from "./metering.js";
|
|
27
28
|
import { runAgentNative } from "./native/loop.js";
|
|
28
29
|
import { createPlaytestBrowserManager } from "./native/playtest-browser.js";
|
|
29
30
|
import { createPlaywrightPlaytestExecutor } from "./native/playtest-executor.js";
|
|
@@ -343,7 +344,11 @@ const OPENROUTER_ALLOWED_TOOLS = "--allowedTools=Edit,Write,NotebookEdit,Bash";
|
|
|
343
344
|
function buildAgentInvocation(backend, role, prompt, claudeModel,
|
|
344
345
|
// Already resolved for this role by the caller (router turns pass
|
|
345
346
|
// settings.routerOpenrouterModel, task spawns settings.tasksOpenrouterModel).
|
|
346
|
-
openrouterModel
|
|
347
|
+
openrouterModel,
|
|
348
|
+
// This run's ledger identity, and the deck dir its castle.json is read from.
|
|
349
|
+
// Only the claude branch can carry it: cursor-agent runs on its own key and
|
|
350
|
+
// never traverses the llm-proxy.
|
|
351
|
+
metering) {
|
|
347
352
|
if (backend === "claude") {
|
|
348
353
|
const viaOpenrouter = claudeModel === "openrouter";
|
|
349
354
|
return {
|
|
@@ -379,9 +384,13 @@ openrouterModel) {
|
|
|
379
384
|
: []),
|
|
380
385
|
prompt,
|
|
381
386
|
],
|
|
382
|
-
env: viaOpenrouter
|
|
387
|
+
env: withCustomHeaders(viaOpenrouter
|
|
383
388
|
? envForOpenrouterSpawn(openrouterApiKey())
|
|
384
|
-
: envForAgentSpawn(backend),
|
|
389
|
+
: envForAgentSpawn(backend), meteringHeaders({
|
|
390
|
+
deckDir: metering.deckDir,
|
|
391
|
+
sessionId: metering.sessionId,
|
|
392
|
+
route: viaOpenrouter ? "openrouter" : "anthropic",
|
|
393
|
+
})),
|
|
385
394
|
};
|
|
386
395
|
}
|
|
387
396
|
return {
|
|
@@ -1456,6 +1465,7 @@ async function runAgentSmith(opts) {
|
|
|
1456
1465
|
const result = await runAgentNative({
|
|
1457
1466
|
cwd: opts.cwd,
|
|
1458
1467
|
role: opts.role,
|
|
1468
|
+
extraHeaders: opts.extraHeaders,
|
|
1459
1469
|
model: opts.model,
|
|
1460
1470
|
apiKey: openrouterApiKey(),
|
|
1461
1471
|
reasoningEffort: opts.openrouterTuning?.reasoningEffort,
|
|
@@ -1574,10 +1584,19 @@ async function runAgentTurn(opts) {
|
|
|
1574
1584
|
crashed: false,
|
|
1575
1585
|
};
|
|
1576
1586
|
}
|
|
1587
|
+
// One id per call: a router turn, or a single task attempt (the retry loop
|
|
1588
|
+
// in runTaskAgentIn calls this once per attempt, and each attempt is its own
|
|
1589
|
+
// conversation -- nothing is resumed).
|
|
1590
|
+
const sessionId = newAgentSessionId(opts.role);
|
|
1577
1591
|
if (opts.backend === "smith") {
|
|
1578
1592
|
return runAgentSmith({
|
|
1579
1593
|
cwd: opts.cwd,
|
|
1580
1594
|
role: opts.role,
|
|
1595
|
+
extraHeaders: meteringHeaders({
|
|
1596
|
+
deckDir: opts.cwd,
|
|
1597
|
+
sessionId,
|
|
1598
|
+
route: "openrouter",
|
|
1599
|
+
}),
|
|
1581
1600
|
model: opts.openrouterModel,
|
|
1582
1601
|
prompt: opts.prompt,
|
|
1583
1602
|
// Mirrors claude's --append-system-prompt for tasks (the native loop
|
|
@@ -1596,7 +1615,7 @@ async function runAgentTurn(opts) {
|
|
|
1596
1615
|
onSpawn: opts.onSpawn,
|
|
1597
1616
|
});
|
|
1598
1617
|
}
|
|
1599
|
-
const invocation = buildAgentInvocation(opts.backend, opts.role, opts.prompt, opts.claudeModel, opts.openrouterModel);
|
|
1618
|
+
const invocation = buildAgentInvocation(opts.backend, opts.role, opts.prompt, opts.claudeModel, opts.openrouterModel, { sessionId, deckDir: opts.cwd });
|
|
1600
1619
|
return runAgentCli({
|
|
1601
1620
|
cwd: opts.cwd,
|
|
1602
1621
|
command: invocation.command,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type MeteringRoute = "anthropic" | "openrouter";
|
|
2
|
+
/**
|
|
3
|
+
* A fresh id for one agent run -- one router turn (one user message) or one
|
|
4
|
+
* task attempt. Every provider call in that run's agentic loop carries it, so
|
|
5
|
+
* the ledger can price a single turn even though concurrent tasks interleave
|
|
6
|
+
* with chat in wall-clock time. The role prefix keeps "chat spend vs.
|
|
7
|
+
* background-task spend" a prefix match with no extra column.
|
|
8
|
+
*/
|
|
9
|
+
export declare function newAgentSessionId(role: "router" | "task"): string;
|
|
10
|
+
export declare function meteringHeaders(opts: {
|
|
11
|
+
deckDir: string;
|
|
12
|
+
sessionId: string;
|
|
13
|
+
route: MeteringRoute;
|
|
14
|
+
}): Record<string, string>;
|
|
15
|
+
/**
|
|
16
|
+
* Merge metering headers into a claude spawn env. ANTHROPIC_CUSTOM_HEADERS is
|
|
17
|
+
* the claude CLI's newline-separated `Name: Value` list, forwarded on every
|
|
18
|
+
* request it makes to ANTHROPIC_BASE_URL. An inherited value is kept, first,
|
|
19
|
+
* rather than clobbered.
|
|
20
|
+
*/
|
|
21
|
+
export declare function withCustomHeaders(env: NodeJS.ProcessEnv, headers: Record<string, string>): NodeJS.ProcessEnv;
|
package/dist/metering.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Per-request metering identity for agent runs inside a Castle sandbox.
|
|
2
|
+
//
|
|
3
|
+
// The per-host llm-proxy (castle-sandboxes/llm-proxy) terminates every
|
|
4
|
+
// provider call a sandbox makes, records a usage row, and strips these two
|
|
5
|
+
// headers before forwarding upstream -- so they never reach Anthropic or
|
|
6
|
+
// OpenRouter. Names must match LLM_SESSION_HEADER / LLM_DECK_HEADER in
|
|
7
|
+
// castle-sandboxes/shared/src/index.ts.
|
|
8
|
+
//
|
|
9
|
+
// The proxy already knows the user (its token is minted per sandbox) and the
|
|
10
|
+
// sandbox. These headers add the two dimensions only this process knows: which
|
|
11
|
+
// deck the work belongs to, and which agent run issued the call.
|
|
12
|
+
import * as fs from "fs";
|
|
13
|
+
import * as path from "path";
|
|
14
|
+
import { randomBytes } from "crypto";
|
|
15
|
+
const SESSION_HEADER = "x-castle-agent-session-id";
|
|
16
|
+
const DECK_HEADER = "x-castle-deck-id";
|
|
17
|
+
/**
|
|
18
|
+
* A fresh id for one agent run -- one router turn (one user message) or one
|
|
19
|
+
* task attempt. Every provider call in that run's agentic loop carries it, so
|
|
20
|
+
* the ledger can price a single turn even though concurrent tasks interleave
|
|
21
|
+
* with chat in wall-clock time. The role prefix keeps "chat spend vs.
|
|
22
|
+
* background-task spend" a prefix match with no extra column.
|
|
23
|
+
*/
|
|
24
|
+
export function newAgentSessionId(role) {
|
|
25
|
+
return `${role === "router" ? "r" : "t"}-${randomBytes(12).toString("hex")}`;
|
|
26
|
+
}
|
|
27
|
+
// Outside a sandbox nothing injects a base URL, and the headers would be sent
|
|
28
|
+
// to the provider itself -- handing a deck id to a third party to no purpose.
|
|
29
|
+
function proxyInjected(route) {
|
|
30
|
+
return Boolean(route === "anthropic"
|
|
31
|
+
? process.env.ANTHROPIC_BASE_URL
|
|
32
|
+
: process.env.OPENROUTER_BASE_URL);
|
|
33
|
+
}
|
|
34
|
+
// castle.json is written by the first save (see save-deck.ts), so an unsaved
|
|
35
|
+
// deck has no Castle id and its spend stays unattributed rather than being
|
|
36
|
+
// labelled with the sandbox-local directory name, which never becomes the
|
|
37
|
+
// deck id. Re-read per run: a deck saved mid-session starts attributing on its
|
|
38
|
+
// next turn.
|
|
39
|
+
function deckIdFor(deckDir) {
|
|
40
|
+
let parsed;
|
|
41
|
+
try {
|
|
42
|
+
parsed = JSON.parse(fs.readFileSync(path.join(deckDir, "castle.json"), "utf8"));
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
if (typeof parsed.deckId !== "string" || !parsed.deckId)
|
|
48
|
+
return null;
|
|
49
|
+
return parsed.deckId.replace(/[\r\n]/g, "") || null;
|
|
50
|
+
}
|
|
51
|
+
export function meteringHeaders(opts) {
|
|
52
|
+
if (!proxyInjected(opts.route))
|
|
53
|
+
return {};
|
|
54
|
+
const deckId = deckIdFor(opts.deckDir);
|
|
55
|
+
return {
|
|
56
|
+
[SESSION_HEADER]: opts.sessionId,
|
|
57
|
+
...(deckId ? { [DECK_HEADER]: deckId } : {}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Merge metering headers into a claude spawn env. ANTHROPIC_CUSTOM_HEADERS is
|
|
62
|
+
* the claude CLI's newline-separated `Name: Value` list, forwarded on every
|
|
63
|
+
* request it makes to ANTHROPIC_BASE_URL. An inherited value is kept, first,
|
|
64
|
+
* rather than clobbered.
|
|
65
|
+
*/
|
|
66
|
+
export function withCustomHeaders(env, headers) {
|
|
67
|
+
const lines = Object.entries(headers).map(([name, value]) => `${name}: ${value}`);
|
|
68
|
+
if (lines.length === 0)
|
|
69
|
+
return env;
|
|
70
|
+
const inherited = env.ANTHROPIC_CUSTOM_HEADERS;
|
|
71
|
+
return {
|
|
72
|
+
...env,
|
|
73
|
+
ANTHROPIC_CUSTOM_HEADERS: [...(inherited ? [inherited] : []), ...lines].join("\n"),
|
|
74
|
+
};
|
|
75
|
+
}
|
package/dist/native/loop.js
CHANGED
|
@@ -607,6 +607,7 @@ async function runLoop(opts, toolSchemas, log) {
|
|
|
607
607
|
reasoningEffort: opts.reasoningEffort ?? REASONING_EFFORT[opts.role],
|
|
608
608
|
routing: opts.routing,
|
|
609
609
|
providerTier: opts.providerTier,
|
|
610
|
+
extraHeaders: opts.extraHeaders,
|
|
610
611
|
maxTokens: MAX_COMPLETION_TOKENS,
|
|
611
612
|
signal: controller.signal,
|
|
612
613
|
onDelta: opts.onDelta,
|
package/dist/native/types.d.ts
CHANGED