castle-web-cli 0.4.102 → 0.4.103
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-failures.d.ts +2 -1
- package/dist/agent-failures.js +9 -0
- package/dist/agent.js +155 -102
- package/dist/anthropic-key-helper.d.ts +1 -0
- package/dist/anthropic-key-helper.js +7 -0
- package/dist/byo-auth.d.ts +23 -0
- package/dist/byo-auth.js +130 -0
- package/dist/ide.js +18 -2
- package/dist/metering.d.ts +18 -0
- package/dist/metering.js +42 -0
- package/dist/native/openrouter.d.ts +3 -0
- package/dist/native/openrouter.js +63 -2
- package/dist/shell/assets/{index-BYQR39EF.js → index-BNvlZx4-.js} +38 -38
- package/dist/shell/assets/{index-BfN3y__V.css → index-DIcWN-RS.css} +1 -1
- package/dist/shell/index.html +2 -2
- package/dist/vitePlugins.js +30 -2
- package/kits/basic-2d/castle.json +26 -1
- package/kits/physics-2d/behaviors/Collider.jsx +21 -46
- package/kits/physics-2d/castle.json +27 -2
- package/kits/physics-2d/engine/blueprint.js +16 -1
- package/package.json +1 -1
package/dist/metering.d.ts
CHANGED
|
@@ -30,6 +30,24 @@ export declare function reportCursorRun(opts: {
|
|
|
30
30
|
durationMs: number;
|
|
31
31
|
ok: boolean;
|
|
32
32
|
}): void;
|
|
33
|
+
/** Mirrors `CastleBudgetResponse` in castle-sandboxes/shared. */
|
|
34
|
+
export interface CastleBudget {
|
|
35
|
+
usedMicros: number;
|
|
36
|
+
limitMicros: number | null;
|
|
37
|
+
resetAtMs: number;
|
|
38
|
+
blocked: boolean;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The daily Castle-paid AI budget for the user this sandbox belongs to, or null
|
|
42
|
+
* when there is nothing to say: outside a sandbox, on a host whose proxy
|
|
43
|
+
* predates this route, or on any failure at all.
|
|
44
|
+
*
|
|
45
|
+
* Null means "don't gate, don't display" in every caller. The proxy is the
|
|
46
|
+
* actual enforcement point -- this read exists so a blocked user gets a
|
|
47
|
+
* sentence instead of a spawned agent failing against a 403, and so the editor
|
|
48
|
+
* can show the bar. Neither is worth failing a run over.
|
|
49
|
+
*/
|
|
50
|
+
export declare function fetchBudget(): Promise<CastleBudget | null>;
|
|
33
51
|
export declare function meteringHeaders(opts: {
|
|
34
52
|
deckDir: string;
|
|
35
53
|
sessionId: string;
|
package/dist/metering.js
CHANGED
|
@@ -100,6 +100,48 @@ export function reportCursorRun(opts) {
|
|
|
100
100
|
/* best-effort: metering must never surface in a finished run */
|
|
101
101
|
});
|
|
102
102
|
}
|
|
103
|
+
// Path on the proxy that reports this sandbox's daily budget. Must match
|
|
104
|
+
// CASTLE_BUDGET_PATH in castle-sandboxes/shared/src/index.ts.
|
|
105
|
+
const CASTLE_BUDGET_PATH = "/castle/budget";
|
|
106
|
+
// Read before a run starts, so it must not be able to hold one up for long.
|
|
107
|
+
const BUDGET_FETCH_TIMEOUT_MS = 5_000;
|
|
108
|
+
/**
|
|
109
|
+
* The daily Castle-paid AI budget for the user this sandbox belongs to, or null
|
|
110
|
+
* when there is nothing to say: outside a sandbox, on a host whose proxy
|
|
111
|
+
* predates this route, or on any failure at all.
|
|
112
|
+
*
|
|
113
|
+
* Null means "don't gate, don't display" in every caller. The proxy is the
|
|
114
|
+
* actual enforcement point -- this read exists so a blocked user gets a
|
|
115
|
+
* sentence instead of a spawned agent failing against a 403, and so the editor
|
|
116
|
+
* can show the bar. Neither is worth failing a run over.
|
|
117
|
+
*/
|
|
118
|
+
export async function fetchBudget() {
|
|
119
|
+
const base = process.env.CASTLE_LLM_PROXY_URL;
|
|
120
|
+
const token = process.env.CASTLE_LLM_PROXY_TOKEN;
|
|
121
|
+
if (!base || !token)
|
|
122
|
+
return null;
|
|
123
|
+
try {
|
|
124
|
+
const resp = await fetch(`${base}${CASTLE_BUDGET_PATH}`, {
|
|
125
|
+
headers: { authorization: `Bearer ${token}` },
|
|
126
|
+
signal: AbortSignal.timeout(BUDGET_FETCH_TIMEOUT_MS),
|
|
127
|
+
});
|
|
128
|
+
if (!resp.ok)
|
|
129
|
+
return null;
|
|
130
|
+
const body = (await resp.json());
|
|
131
|
+
if (typeof body?.usedMicros !== "number" || typeof body.blocked !== "boolean") {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
usedMicros: body.usedMicros,
|
|
136
|
+
limitMicros: typeof body.limitMicros === "number" ? body.limitMicros : null,
|
|
137
|
+
resetAtMs: typeof body.resetAtMs === "number" ? body.resetAtMs : 0,
|
|
138
|
+
blocked: body.blocked,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
103
145
|
export function meteringHeaders(opts) {
|
|
104
146
|
if (opts.direct || !proxyInjected(opts.route))
|
|
105
147
|
return {};
|
|
@@ -222,11 +222,65 @@ function finalizeToolCalls(pending) {
|
|
|
222
222
|
};
|
|
223
223
|
});
|
|
224
224
|
}
|
|
225
|
+
// Anthropic prompt caching is EXPLICIT: without `cache_control` breakpoints
|
|
226
|
+
// nothing is cached, and a tool loop re-pays for its whole transcript every
|
|
227
|
+
// call. (Google caches implicitly, which is why gemini showed a ~66% hit rate
|
|
228
|
+
// here while sonnet showed 0%.) Measured: sonnet through this loop cost $8.10
|
|
229
|
+
// uncached versus $2.27 through the claude CLI, which sets breakpoints itself.
|
|
230
|
+
//
|
|
231
|
+
// Two breakpoints, the most a growing transcript needs: the system message,
|
|
232
|
+
// which never changes, and the tail of what we are about to send, so the NEXT
|
|
233
|
+
// call finds this whole turn already cached. Anthropic matches the longest
|
|
234
|
+
// cached prefix ending at a breakpoint.
|
|
235
|
+
//
|
|
236
|
+
// Only for providers that need the marker. Slugs that don't support it would
|
|
237
|
+
// carry an uninterpreted field at best, and OpenRouter's docs scope
|
|
238
|
+
// cache_control to Anthropic-family models.
|
|
239
|
+
function needsExplicitCacheControl(model) {
|
|
240
|
+
return model.startsWith("anthropic/");
|
|
241
|
+
}
|
|
242
|
+
function markCached(message) {
|
|
243
|
+
const { content } = message;
|
|
244
|
+
if (typeof content === "string") {
|
|
245
|
+
if (!content)
|
|
246
|
+
return message;
|
|
247
|
+
return { ...message, content: [{ type: "text", text: content, cache_control: { type: "ephemeral" } }] };
|
|
248
|
+
}
|
|
249
|
+
if (!Array.isArray(content) || content.length === 0)
|
|
250
|
+
return message;
|
|
251
|
+
const parts = content.slice();
|
|
252
|
+
const last = parts[parts.length - 1];
|
|
253
|
+
if (last.type !== "text")
|
|
254
|
+
return message;
|
|
255
|
+
parts[parts.length - 1] = { ...last, cache_control: { type: "ephemeral" } };
|
|
256
|
+
return { ...message, content: parts };
|
|
257
|
+
}
|
|
258
|
+
function withCacheBreakpoints(messages, model) {
|
|
259
|
+
if (!needsExplicitCacheControl(model) || messages.length === 0)
|
|
260
|
+
return messages;
|
|
261
|
+
const out = messages.slice();
|
|
262
|
+
const systemIndex = out.findIndex((m) => m.role === "system");
|
|
263
|
+
if (systemIndex !== -1)
|
|
264
|
+
out[systemIndex] = markCached(out[systemIndex]);
|
|
265
|
+
const lastIndex = out.length - 1;
|
|
266
|
+
if (lastIndex !== systemIndex)
|
|
267
|
+
out[lastIndex] = markCached(out[lastIndex]);
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
// A hard thinking budget, when one is asked for, else the effort word. Read
|
|
271
|
+
// from the environment rather than settings: this exists to A/B the two forms
|
|
272
|
+
// against each other, and a run wants it applied to every call it makes.
|
|
273
|
+
function reasoningField(effort) {
|
|
274
|
+
const cap = Number(process.env.CASTLE_REASONING_MAX_TOKENS);
|
|
275
|
+
if (Number.isFinite(cap) && cap > 0)
|
|
276
|
+
return { reasoning: { max_tokens: Math.floor(cap) } };
|
|
277
|
+
return effort ? { reasoning: { effort } } : {};
|
|
278
|
+
}
|
|
225
279
|
export async function streamChatCompletion(opts) {
|
|
226
280
|
const body = {
|
|
227
281
|
// Routing mode rides the slug as a suffix (see applyRoutingMode).
|
|
228
282
|
model: applyRoutingMode(opts.model, opts.routing),
|
|
229
|
-
messages: opts.messages,
|
|
283
|
+
messages: withCacheBreakpoints(opts.messages, opts.model),
|
|
230
284
|
stream: true,
|
|
231
285
|
// Deprecated on OpenRouter's side (usage is always included now) but
|
|
232
286
|
// harmless to send -- keeps this client correct against providers that
|
|
@@ -235,7 +289,14 @@ export async function streamChatCompletion(opts) {
|
|
|
235
289
|
...(opts.tools && opts.tools.length > 0 ? { tools: opts.tools } : {}),
|
|
236
290
|
// See ORReasoningEffort / StreamChatOpts.reasoningEffort above for the
|
|
237
291
|
// doc reference and the graceful-degradation guarantee this relies on.
|
|
238
|
-
|
|
292
|
+
//
|
|
293
|
+
// `effort` means different things per provider: Anthropic derives a thinking
|
|
294
|
+
// budget as a fraction of max_tokens, so it caps; Gemini 3 maps it straight
|
|
295
|
+
// to `thinkingLevel`, so it FLOORS. A hard `reasoning.max_tokens` is the one
|
|
296
|
+
// form that means the same thing everywhere, so it is available as an
|
|
297
|
+
// override for measuring that difference. OpenRouter treats the two as
|
|
298
|
+
// mutually exclusive -- send one or the other, never both.
|
|
299
|
+
...reasoningField(opts.reasoningEffort),
|
|
239
300
|
// Pin a provider tier when requested (see StreamChatOpts.providerTier).
|
|
240
301
|
...(opts.providerTier
|
|
241
302
|
? { provider: { order: [opts.providerTier], allow_fallbacks: true } }
|