bare-agent 0.16.2 → 0.18.0
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/README.md +36 -30
- package/bareagent.context.md +60 -1
- package/index.d.ts +7 -1
- package/index.js +12 -1
- package/package.json +5 -4
- package/src/bareguard-adapter.js +22 -4
- package/src/complexity.d.ts +12 -0
- package/src/complexity.js +33 -4
- package/src/evaluator.d.ts +147 -0
- package/src/evaluator.js +270 -0
- package/src/loop.d.ts +75 -4
- package/src/loop.js +233 -44
- package/src/memory.d.ts +10 -3
- package/src/memory.js +15 -4
- package/src/provider-anthropic.d.ts +13 -0
- package/src/provider-anthropic.js +36 -1
- package/src/provider-gemini.d.ts +70 -0
- package/src/provider-gemini.js +197 -0
- package/src/provider-openai.d.ts +10 -0
- package/src/provider-openai.js +20 -4
- package/src/providers.d.ts +2 -1
- package/src/providers.js +3 -0
- package/src/refine.d.ts +86 -0
- package/src/refine.js +65 -0
- package/src/remember.d.ts +118 -0
- package/src/remember.js +163 -0
- package/src/skills.d.ts +71 -0
- package/src/skills.js +200 -0
- package/src/stash.d.ts +44 -0
- package/src/stash.js +342 -0
- package/types/index.d.ts +57 -1
package/src/loop.js
CHANGED
|
@@ -7,6 +7,7 @@ const { ToolError, HaltError } = require('./errors');
|
|
|
7
7
|
/** @typedef {import('../types').ToolDef} ToolDef */
|
|
8
8
|
/** @typedef {import('../types').ToolCall} ToolCall */
|
|
9
9
|
/** @typedef {import('../types').Usage} Usage */
|
|
10
|
+
/** @typedef {import('../types').RunMetrics} RunMetrics */
|
|
10
11
|
/** @typedef {import('../types').GenerateResult} GenerateResult */
|
|
11
12
|
/** @typedef {import('../types').Store} Store */
|
|
12
13
|
/** @typedef {import('./checkpoint').Checkpoint} Checkpoint */
|
|
@@ -52,24 +53,36 @@ const { ToolError, HaltError } = require('./errors');
|
|
|
52
53
|
*/
|
|
53
54
|
|
|
54
55
|
// Average pricing per 1K tokens (USD). Adjust these to match your provider's rates.
|
|
55
|
-
// Last updated: 2026-
|
|
56
|
-
|
|
56
|
+
// Last updated: 2026-06-22. Source: public provider pricing pages (Anthropic rates + cache multipliers
|
|
57
|
+
// cross-checked against the claude-api reference). Rates are USD per 1K tokens. `cacheReadMult` /
|
|
58
|
+
// `cacheWriteMult` are multipliers ON the input rate for the two cache tiers (see estimateCost); when
|
|
59
|
+
// omitted they default to Anthropic's convention (read 0.1×, write 1.25×). OpenAI/Gemini have no
|
|
60
|
+
// cache-WRITE surcharge (their providers report cacheCreationTokens=0), so only cacheReadMult matters
|
|
61
|
+
// there. NOTE: OpenAI's cached discount is ~0.5× on the 4o family; some newer models (4.1/o-series) are
|
|
62
|
+
// ~0.25× — set to 0.5× here as the documented general value; refine per-model against current pricing.
|
|
63
|
+
/** @type {Record<string, {in: number, out: number, cacheReadMult?: number, cacheWriteMult?: number}>} */
|
|
57
64
|
const COST_PER_1K = {
|
|
58
|
-
// OpenAI
|
|
59
|
-
'gpt-4o': { in: 0.0025, out: 0.01 },
|
|
60
|
-
'gpt-4o-mini': { in: 0.00015, out: 0.0006 },
|
|
61
|
-
'gpt-4.1': { in: 0.002, out: 0.008 },
|
|
62
|
-
'gpt-4.1-mini': { in: 0.0004, out: 0.0016 },
|
|
63
|
-
'gpt-4.1-nano': { in: 0.0001, out: 0.0004 },
|
|
64
|
-
'o3-mini': { in: 0.0011, out: 0.0044 },
|
|
65
|
-
// Anthropic — Claude
|
|
66
|
-
'claude-
|
|
65
|
+
// OpenAI — cached input ~0.5× (no write tier)
|
|
66
|
+
'gpt-4o': { in: 0.0025, out: 0.01, cacheReadMult: 0.5 },
|
|
67
|
+
'gpt-4o-mini': { in: 0.00015, out: 0.0006, cacheReadMult: 0.5 },
|
|
68
|
+
'gpt-4.1': { in: 0.002, out: 0.008, cacheReadMult: 0.5 },
|
|
69
|
+
'gpt-4.1-mini': { in: 0.0004, out: 0.0016, cacheReadMult: 0.5 },
|
|
70
|
+
'gpt-4.1-nano': { in: 0.0001, out: 0.0004, cacheReadMult: 0.5 },
|
|
71
|
+
'o3-mini': { in: 0.0011, out: 0.0044, cacheReadMult: 0.5 },
|
|
72
|
+
// Anthropic — Claude current generation (2026-06). Cache tiers use the default 0.1×/1.25×.
|
|
73
|
+
'claude-fable-5': { in: 0.01, out: 0.05 },
|
|
74
|
+
'claude-opus-4-8': { in: 0.005, out: 0.025 },
|
|
75
|
+
'claude-opus-4-7': { in: 0.005, out: 0.025 },
|
|
76
|
+
'claude-opus-4-6': { in: 0.005, out: 0.025 },
|
|
67
77
|
'claude-sonnet-4-6': { in: 0.003, out: 0.015 },
|
|
68
|
-
'claude-haiku-4-5-20251001': { in: 0.
|
|
69
|
-
'claude-haiku-4-5': { in: 0.
|
|
70
|
-
// Anthropic — earlier 4
|
|
78
|
+
'claude-haiku-4-5-20251001': { in: 0.001, out: 0.005 },
|
|
79
|
+
'claude-haiku-4-5': { in: 0.001, out: 0.005 },
|
|
80
|
+
// Anthropic — earlier snapshots (the original Opus 4 / Sonnet 4 generation, genuinely different rates)
|
|
71
81
|
'claude-sonnet-4-20250514': { in: 0.003, out: 0.015 },
|
|
72
82
|
'claude-opus-4-20250514': { in: 0.015, out: 0.075 },
|
|
83
|
+
// Google Gemini — cached content ~0.25× (no write tier). Native provider lands in a following piece.
|
|
84
|
+
'gemini-2.5-flash': { in: 0.0003, out: 0.0025, cacheReadMult: 0.25 },
|
|
85
|
+
'gemini-2.5-pro': { in: 0.00125, out: 0.01, cacheReadMult: 0.25 },
|
|
73
86
|
// Fallback average across popular models (~$0.002 in, ~$0.008 out per 1K)
|
|
74
87
|
'_default': { in: 0.002, out: 0.008 },
|
|
75
88
|
};
|
|
@@ -105,6 +118,11 @@ function sealDanglingToolCalls(msgs, rule) {
|
|
|
105
118
|
}
|
|
106
119
|
|
|
107
120
|
/**
|
|
121
|
+
* Estimate the USD cost of one round's usage, pricing the FOUR token tiers separately (D9/L7):
|
|
122
|
+
* uncached input, output, cache-read, and cache-creation. Folding cache tokens into the full input
|
|
123
|
+
* rate mis-prices badly — a warm prompt is mostly cache-read (~0.1–0.5× input) and Anthropic's
|
|
124
|
+
* cache-creation is a ~1.25× premium — so each tier gets its own rate. Returns null (not 0) when the
|
|
125
|
+
* model is unknown/absent so the caller can mark the round `unpriced` rather than silently free.
|
|
108
126
|
* @param {string|null} model
|
|
109
127
|
* @param {Usage|null} usage
|
|
110
128
|
* @returns {number|null}
|
|
@@ -112,10 +130,20 @@ function sealDanglingToolCalls(msgs, rule) {
|
|
|
112
130
|
function estimateCost(model, usage) {
|
|
113
131
|
if (!usage || !model) return null;
|
|
114
132
|
const rates = COST_PER_1K[model] || COST_PER_1K['_default'];
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
133
|
+
const readMult = rates.cacheReadMult ?? 0.1; // Anthropic convention when unspecified
|
|
134
|
+
const writeMult = rates.cacheWriteMult ?? 1.25;
|
|
135
|
+
const cost = (
|
|
136
|
+
(usage.inputTokens || 0) * rates.in +
|
|
137
|
+
(usage.outputTokens || 0) * rates.out +
|
|
138
|
+
(usage.cacheReadTokens || 0) * rates.in * readMult +
|
|
139
|
+
(usage.cacheCreationTokens || 0) * rates.in * writeMult
|
|
140
|
+
) / 1000;
|
|
141
|
+
// A non-finite cost (±Infinity from runaway token counts, NaN from a garbage rate-table entry) is a
|
|
142
|
+
// COULDN'T-PRICE, not a price. Return null so the round is marked `unpriced` and the value never
|
|
143
|
+
// poisons `totalCost`, `result.metrics.costUsd`, or — via onLlmResult → the gate — `spentUsd`. The
|
|
144
|
+
// last is the dangerous one: `NaN >= cap` is false, which would DISABLE a budget cap, not just
|
|
145
|
+
// under-count it. Same silent-unenforceable class as a null model (§3.7).
|
|
146
|
+
return Number.isFinite(cost) ? cost : null;
|
|
119
147
|
}
|
|
120
148
|
|
|
121
149
|
// R-C6: default instruction for the provider-bound `ctx.summarize` lent to the assemble seam.
|
|
@@ -262,9 +290,11 @@ class Loop {
|
|
|
262
290
|
/**
|
|
263
291
|
* Run the think/act/observe loop.
|
|
264
292
|
* @param {Message[]} messages - Conversation messages in OpenAI format.
|
|
265
|
-
* @param {ToolDef[]} [tools=[]] - Tool definitions
|
|
293
|
+
* @param {ToolDef[] | (() => ToolDef[])} [tools=[]] - Tool definitions, or a thunk returning them. A
|
|
294
|
+
* thunk is re-evaluated each round (D4/eval-assist F2) so a tool set that grows mid-run — e.g. a skill
|
|
295
|
+
* unlocking its tools — is offered on the next round; a static array is resolved once at wire time.
|
|
266
296
|
* @param {Record<string, any>} [options={}] - Per-run overrides (system, temperature, ctx, etc.).
|
|
267
|
-
* @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, msgs: Message[]}>}
|
|
297
|
+
* @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, msgs: Message[], metrics: RunMetrics}>}
|
|
268
298
|
* On halt the returned `error` is `halt:<rule>` (or `halt:unknown` if the
|
|
269
299
|
* thrown HaltError carried no `rule`), and `msgs` is sanitized so any
|
|
270
300
|
* dangling assistant `tool_calls` from the halted round are paired with
|
|
@@ -281,29 +311,118 @@ class Loop {
|
|
|
281
311
|
const msgs = system
|
|
282
312
|
? [{ role: 'system', content: system }, ...messages]
|
|
283
313
|
: [...messages];
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
//
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
314
|
+
// D4 (eval-assist F2): `tools` may be a `() => ToolDef[]` thunk, re-evaluated each round so a set that
|
|
315
|
+
// grows mid-run (e.g. a skill unlocking its tools via skill_use last round) is offered to the model on
|
|
316
|
+
// the NEXT round's generate(). Loop stays skill-agnostic — it only gains the general tools-as-thunk
|
|
317
|
+
// capability, the same independence as the assemble/trim seams. A static array keeps today's exact
|
|
318
|
+
// behavior (resolved once at wire time, never changes).
|
|
319
|
+
const toolsThunk = typeof tools === 'function' ? tools : null;
|
|
320
|
+
const resolveTools = () => {
|
|
321
|
+
const list = toolsThunk ? toolsThunk() : tools;
|
|
322
|
+
if (!Array.isArray(list)) {
|
|
323
|
+
throw new Error(`[Loop] tools must be an array of ToolDef or a () => ToolDef[] thunk returning one, got ${typeof list}.`);
|
|
293
324
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
325
|
+
return list;
|
|
326
|
+
};
|
|
327
|
+
// A non-string description is a soft warning, not a throw — but validateTools re-runs every round for a
|
|
328
|
+
// thunk, so dedup the warning by tool name to avoid per-round log spam (warn once per offending tool/run).
|
|
329
|
+
const warnedBadDesc = new Set();
|
|
330
|
+
/** Validate a resolved tool list — wire-time contract, re-checked each round for thunks. @param {ToolDef[]} list */
|
|
331
|
+
const validateTools = (list) => {
|
|
332
|
+
for (const tool of list) {
|
|
333
|
+
if (typeof tool.name !== 'string' || !tool.name) {
|
|
334
|
+
throw new Error(`[Loop] Tool is missing a name (got ${JSON.stringify(tool.name)}). Every tool must have a non-empty string name.`);
|
|
335
|
+
}
|
|
336
|
+
if (typeof tool.execute !== 'function') {
|
|
337
|
+
throw new Error(`[Loop] Tool "${tool.name}" is missing an execute() function.`);
|
|
338
|
+
}
|
|
339
|
+
if (tool.description !== undefined && typeof tool.description !== 'string' && !warnedBadDesc.has(tool.name)) {
|
|
340
|
+
warnedBadDesc.add(tool.name);
|
|
341
|
+
console.warn(`[Loop] Tool "${tool.name}" has a non-string description — providers may ignore it.`);
|
|
342
|
+
}
|
|
343
|
+
if (tool.parameters !== undefined && (typeof tool.parameters !== 'object' || tool.parameters === null)) {
|
|
344
|
+
throw new Error(`[Loop] Tool "${tool.name}" has invalid parameters — expected an object, got ${typeof tool.parameters}.`);
|
|
345
|
+
}
|
|
299
346
|
}
|
|
300
|
-
}
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
// Resolve + validate once at wire time (this throws for a misconfigured static array OR thunk —
|
|
350
|
+
// fail-fast on setup). Per-round re-resolution for thunks happens at the top of the loop below.
|
|
351
|
+
let activeTools = resolveTools();
|
|
352
|
+
validateTools(activeTools);
|
|
353
|
+
let toolMap = new Map(activeTools.map(t => [t.name, t]));
|
|
301
354
|
|
|
302
355
|
this._safeEmit({ type: 'loop:start', data: { messageCount: msgs.length } });
|
|
303
356
|
|
|
304
357
|
let lastUsage = { inputTokens: 0, outputTokens: 0 };
|
|
305
358
|
let totalCost = 0;
|
|
306
359
|
|
|
360
|
+
// The meter (Feature 3): bareagent is the canonical run counter. Accumulates across rounds and is
|
|
361
|
+
// returned as `result.metrics`. `tokens` is CUMULATIVE over all four tiers (fixes the last-round-only
|
|
362
|
+
// `result.usage` bug — result.usage stays last-round for back-compat; metrics.tokens is the run total).
|
|
363
|
+
// `costUsd` is the priced cumulative (null only if NOTHING could be priced — the explicit-unknown
|
|
364
|
+
// signal, distinct from a genuine 0); `unpricedRounds` makes an unenforceable-on-budget run visible.
|
|
365
|
+
const meterStartedAt = Date.now();
|
|
366
|
+
let pricedAny = false;
|
|
367
|
+
const metrics = {
|
|
368
|
+
turns: 0,
|
|
369
|
+
toolCalls: 0,
|
|
370
|
+
/** @type {Record<string, number>} */
|
|
371
|
+
byTool: {},
|
|
372
|
+
tokens: { input: 0, output: 0, cacheCreation: 0, cacheRead: 0 },
|
|
373
|
+
unpricedRounds: 0,
|
|
374
|
+
// §3.6 CE-activity rollup — convenience counts derived in-place from the same events already
|
|
375
|
+
// on the Stream (loop:trim, loop:summarize), not a second source. `compactions` counts
|
|
376
|
+
// destructive trim evictions; `summaries` counts ctx.summarize calls; `tokensTrimmed` is an
|
|
377
|
+
// APPROXIMATE (~4 chars/token) count of tokens evicted from the canonical transcript — evicted
|
|
378
|
+
// spans have no exact provider count, so we estimate rather than emit a silent zero (§3.10).
|
|
379
|
+
context: { compactions: 0, summaries: 0, tokensTrimmed: 0 },
|
|
380
|
+
// §3.6 memory footprint — the ops bareagent INITIATES, reported via the loop-lent `ctx.recordMemoryOp`
|
|
381
|
+
// hook (channel A: the originating module announces; the loop counts + emits `loop:memory`). Bounded
|
|
382
|
+
// PER RUN by construction: the hook is re-attached each loop.run() and closes over THIS run's meter,
|
|
383
|
+
// and result.metrics is a copy taken at run end. `stashed`/`episodes` flow through the stash fold;
|
|
384
|
+
// The Memory wrapper is metered on BOTH sides, symmetrically and opt-in: `recalls` (Memory.search,
|
|
385
|
+
// the read) and `stored` (Memory.store, the generic durable write). The caller threads the run's ctx so
|
|
386
|
+
// the op counts against that run (a session-scoped LiteCtx → bounded to that session). `facts` now has its
|
|
387
|
+
// writer: `remember` (src/remember.js, the consolidation pass) distills durable facts and announces each via
|
|
388
|
+
// recordMemoryOp('facts'). It is DISJOINT from `stored` (remember writes through the socket WITHOUT ctx, so a
|
|
389
|
+
// distilled fact counts once, as a fact). litectx's own episode→fact promotion stays litectx-internal to
|
|
390
|
+
// surface (recentActivity/promotionCandidates) — that is NOT this counter.
|
|
391
|
+
memory: { stashed: 0, episodes: 0, recalls: 0, stored: 0, facts: 0 },
|
|
392
|
+
};
|
|
393
|
+
/** Accumulate one usage object into the cumulative token tiers. @param {Usage|null|undefined} u */
|
|
394
|
+
const addUsage = (u) => {
|
|
395
|
+
if (!u) return;
|
|
396
|
+
metrics.tokens.input += u.inputTokens || 0;
|
|
397
|
+
metrics.tokens.output += u.outputTokens || 0;
|
|
398
|
+
metrics.tokens.cacheCreation += u.cacheCreationTokens || 0;
|
|
399
|
+
metrics.tokens.cacheRead += u.cacheReadTokens || 0;
|
|
400
|
+
};
|
|
401
|
+
/** Snapshot the meter for a return — finalizes the run-scoped fields. @returns {RunMetrics} */
|
|
402
|
+
const finalizeMetrics = () => ({
|
|
403
|
+
turns: metrics.turns,
|
|
404
|
+
toolCalls: metrics.toolCalls,
|
|
405
|
+
byTool: metrics.byTool,
|
|
406
|
+
tokens: { ...metrics.tokens },
|
|
407
|
+
costUsd: pricedAny ? totalCost : null,
|
|
408
|
+
unpricedRounds: metrics.unpricedRounds,
|
|
409
|
+
spawned: metrics.byTool.spawn || 0, // §3.6 — spawn-tool invocations (byTool counts every call, incl. denied)
|
|
410
|
+
context: { ...metrics.context }, // §3.6 CE-activity rollup
|
|
411
|
+
memory: { ...metrics.memory }, // §3.6 memory footprint (stashed/episodes/recalls/stored/facts; see init note)
|
|
412
|
+
durationMs: Date.now() - meterStartedAt,
|
|
413
|
+
});
|
|
414
|
+
// Approximate token count of a message array (~4 chars/token over the stringified message).
|
|
415
|
+
// Used ONLY for the observability rollup metrics.context.tokensTrimmed — NEVER for pricing or
|
|
416
|
+
// governance, which use exact provider counts. A deliberate estimate (§3.10): the trim event
|
|
417
|
+
// carries message COUNTS and evicted spans have no exact provider token count.
|
|
418
|
+
/** @param {any[]} arr @returns {number} */
|
|
419
|
+
const estimateTokens = (arr) => {
|
|
420
|
+
if (!Array.isArray(arr)) return 0;
|
|
421
|
+
let chars = 0;
|
|
422
|
+
for (const m of arr) { try { chars += JSON.stringify(m).length; } catch { /* unstringifiable — skip */ } }
|
|
423
|
+
return Math.ceil(chars / 4);
|
|
424
|
+
};
|
|
425
|
+
|
|
307
426
|
// R-C6: lend a provider-bound summarizer to the assemble seam via `ctx.summarize`. litectx owns
|
|
308
427
|
// the trigger/N/splice (its restorable COMPRESS path keeps summarized turns recoverable by id);
|
|
309
428
|
// bareagent lends ONLY the single model call. Attached NON-ENUMERABLE so it never shows up in the
|
|
@@ -333,7 +452,9 @@ class Loop {
|
|
|
333
452
|
const usage = (result && result.usage) || null;
|
|
334
453
|
const model = (result && result.model) || loop.provider.model || null;
|
|
335
454
|
const cost = estimateCost(model, usage);
|
|
336
|
-
if (cost !== null) totalCost += cost;
|
|
455
|
+
if (cost !== null) { totalCost += cost; pricedAny = true; }
|
|
456
|
+
addUsage(usage); // summarize tokens are real spend → count them in the cumulative meter
|
|
457
|
+
metrics.context.summaries++; // §3.6 CE-activity rollup
|
|
337
458
|
loop._safeEmit({ type: 'loop:summarize', data: { usage, costUsd: cost, durationMs: Date.now() - startedAt } });
|
|
338
459
|
if (loop.onLlmResult) {
|
|
339
460
|
try {
|
|
@@ -342,6 +463,7 @@ class Loop {
|
|
|
342
463
|
provider: loop.provider.name || null,
|
|
343
464
|
usage,
|
|
344
465
|
costUsd: cost,
|
|
466
|
+
pricing: cost === null ? 'unpriced' : 'priced',
|
|
345
467
|
durationMs: Date.now() - startedAt,
|
|
346
468
|
ctx,
|
|
347
469
|
kind: 'summarize',
|
|
@@ -361,12 +483,46 @@ class Loop {
|
|
|
361
483
|
} catch (err) {
|
|
362
484
|
this._reportError('summarize-attach', err);
|
|
363
485
|
}
|
|
486
|
+
// §3.6 memory footprint (channel A). Lend a recorder the originating module calls when it
|
|
487
|
+
// initiates a memory op (stash.js at a lossless park → 'stashed'; at an episode write → 'episodes').
|
|
488
|
+
// The loop owns the count + the Stream emit; the module stays Loop-agnostic (it just calls an
|
|
489
|
+
// optional ctx hook, exactly like ctx.summarize). Non-enumerable so it never leaks into the
|
|
490
|
+
// assemble(units, ctx) identity contract. Unknown kinds are ignored (forward-compatible).
|
|
491
|
+
const recordMemoryOp = (/** @type {string} */ kind) => {
|
|
492
|
+
if (Object.prototype.hasOwnProperty.call(metrics.memory, kind)) {
|
|
493
|
+
metrics.memory[kind]++;
|
|
494
|
+
loop._safeEmit({ type: 'loop:memory', data: { op: kind } });
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
try {
|
|
498
|
+
Object.defineProperty(ctx, 'recordMemoryOp', { value: recordMemoryOp, enumerable: false, configurable: true, writable: true });
|
|
499
|
+
} catch (err) {
|
|
500
|
+
this._reportError('recordMemoryOp-attach', err);
|
|
501
|
+
}
|
|
364
502
|
}
|
|
365
503
|
|
|
366
504
|
try {
|
|
367
505
|
for (let round = 0; round < HARD_ROUND_LIMIT; round++) {
|
|
368
506
|
if (this._stopped) break;
|
|
369
507
|
|
|
508
|
+
// D4: re-evaluate the tools thunk for THIS round, so a tool unlocked last round (e.g. by skill_use)
|
|
509
|
+
// is now offered to the model. Static arrays skip this (toolsThunk === null) and keep their wire-time
|
|
510
|
+
// set. Round 0 also skips — the wire-time resolution above already produced its set, so the thunk is
|
|
511
|
+
// called exactly once per round, never twice for round 0. Fail-OPEN like the assemble/trim seams: a
|
|
512
|
+
// thunk fault degrades to the previous round's set (a tool-discovery bug must not halt the agent); a
|
|
513
|
+
// HaltError is a governance exit and propagates.
|
|
514
|
+
if (toolsThunk && round > 0) {
|
|
515
|
+
try {
|
|
516
|
+
const next = resolveTools();
|
|
517
|
+
validateTools(next);
|
|
518
|
+
activeTools = next;
|
|
519
|
+
toolMap = new Map(activeTools.map(t => [t.name, t]));
|
|
520
|
+
} catch (err) {
|
|
521
|
+
if (err instanceof HaltError) throw err;
|
|
522
|
+
this._reportError('tools', err, { round });
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
370
526
|
// RT-2: destructive transcript-trim chokepoint — bound the canonical transcript before assembling
|
|
371
527
|
// the window. Runs BEFORE assemble (trim shrinks canonical; assemble shapes the per-call view of
|
|
372
528
|
// what remains). The trimmer harvests every evicted turn BEFORE returning the smaller set, so this
|
|
@@ -378,11 +534,17 @@ class Loop {
|
|
|
378
534
|
if (this.trim) {
|
|
379
535
|
try {
|
|
380
536
|
const before = msgs.length;
|
|
537
|
+
const beforeTokens = estimateTokens(msgs); // pre-trim estimate (msgs not yet spliced)
|
|
381
538
|
const kept = await this.trim(msgs, ctx);
|
|
382
539
|
if (Array.isArray(kept) && kept !== msgs) {
|
|
540
|
+
const trimmed = beforeTokens - estimateTokens(kept); // approx tokens evicted from the transcript
|
|
383
541
|
msgs.length = 0;
|
|
384
542
|
msgs.push(...kept);
|
|
385
|
-
if (msgs.length !== before)
|
|
543
|
+
if (msgs.length !== before) {
|
|
544
|
+
metrics.context.compactions++;
|
|
545
|
+
if (trimmed > 0) metrics.context.tokensTrimmed += trimmed;
|
|
546
|
+
this._safeEmit({ type: 'loop:trim', data: { round, before, after: msgs.length, tokensTrimmed: trimmed > 0 ? trimmed : 0 } });
|
|
547
|
+
}
|
|
386
548
|
}
|
|
387
549
|
} catch (err) {
|
|
388
550
|
if (err instanceof HaltError) throw err;
|
|
@@ -412,21 +574,36 @@ class Loop {
|
|
|
412
574
|
let result;
|
|
413
575
|
const llmStartedAt = Date.now();
|
|
414
576
|
try {
|
|
415
|
-
const generate = () => this.provider.generate(toSend,
|
|
577
|
+
const generate = () => this.provider.generate(toSend, activeTools, options);
|
|
416
578
|
result = this.retry ? await this.retry.call(generate) : await generate();
|
|
417
579
|
} catch (err) {
|
|
418
580
|
this._reportError('provider', err, { round });
|
|
419
581
|
if (this.throwOnError) throw err;
|
|
420
|
-
return { text: '', toolCalls: [], usage: lastUsage, cost: totalCost, error: err.message, msgs };
|
|
582
|
+
return { text: '', toolCalls: [], usage: lastUsage, cost: totalCost, error: err.message, msgs, metrics: finalizeMetrics() };
|
|
421
583
|
}
|
|
422
584
|
|
|
423
585
|
lastUsage = result.usage || lastUsage;
|
|
586
|
+
// Publish the latest measured usage to ctx (non-enumerable, fail-open) so a transcript-bound seam —
|
|
587
|
+
// e.g. F2 stash auto-compaction — can read EXACT provider-counted `inputTokens` to gauge context
|
|
588
|
+
// pressure on the NEXT round's trim. Symmetric with lending ctx.summarize; the Loop stays unaware of
|
|
589
|
+
// the consumer. A frozen/sealed ctx simply doesn't get it (reported, never fatal).
|
|
590
|
+
if (ctx && typeof ctx === 'object') {
|
|
591
|
+
try { Object.defineProperty(ctx, 'usage', { value: lastUsage, enumerable: false, configurable: true, writable: true }); }
|
|
592
|
+
catch (err) { this._reportError('usage-attach', err); }
|
|
593
|
+
}
|
|
424
594
|
// Prefer the model the response reports (robust when provider.model is absent or varies per
|
|
425
595
|
// response — e.g. FallbackProvider, or a CircuitBreaker-wrapped provider that drops .model).
|
|
426
596
|
const model = result.model || this.provider.model || null;
|
|
427
597
|
const roundCost = estimateCost(model, lastUsage);
|
|
428
598
|
if (roundCost !== null) totalCost += roundCost;
|
|
429
599
|
|
|
600
|
+
// Meter this round: count the turn, accumulate the four token tiers, and classify pricing —
|
|
601
|
+
// an unpriced round (null cost: no model / no rate) is tallied so the run is observably
|
|
602
|
+
// unenforceable on budget rather than silently free (the #3 cost contract).
|
|
603
|
+
metrics.turns++;
|
|
604
|
+
addUsage(result.usage);
|
|
605
|
+
if (roundCost === null) metrics.unpricedRounds++; else pricedAny = true;
|
|
606
|
+
|
|
430
607
|
// BA1: forward LLM usage to gate.record (via wireGate) so budget.maxCostUsd
|
|
431
608
|
// covers token-heavy / tool-light workloads. Callback errors route through
|
|
432
609
|
// _reportError but never kill the loop — governance failure ≠ run failure.
|
|
@@ -437,6 +614,9 @@ class Loop {
|
|
|
437
614
|
provider: this.provider.name || null,
|
|
438
615
|
usage: result.usage || null,
|
|
439
616
|
costUsd: roundCost,
|
|
617
|
+
// Priced vs unpriced is explicit so the gate never mistakes "couldn't price" (null) for
|
|
618
|
+
// "free" (0) — the silent-zero that made #3's budget cap a no-op. (D5 / §3.7.)
|
|
619
|
+
pricing: roundCost === null ? 'unpriced' : 'priced',
|
|
440
620
|
durationMs: Date.now() - llmStartedAt,
|
|
441
621
|
ctx,
|
|
442
622
|
kind: 'turn',
|
|
@@ -462,7 +642,7 @@ class Loop {
|
|
|
462
642
|
try { await flush(msgs, ctx); }
|
|
463
643
|
catch (err) { if (err instanceof HaltError) throw err; this._reportError('trim-flush', err, { round }); }
|
|
464
644
|
}
|
|
465
|
-
return { text: result.text, toolCalls: [], usage: lastUsage, cost: totalCost, error: null, msgs };
|
|
645
|
+
return { text: result.text, toolCalls: [], usage: lastUsage, cost: totalCost, error: null, msgs, metrics: finalizeMetrics() };
|
|
466
646
|
}
|
|
467
647
|
|
|
468
648
|
// Execute tool calls
|
|
@@ -479,6 +659,11 @@ class Loop {
|
|
|
479
659
|
for (const tc of result.toolCalls) {
|
|
480
660
|
if (this._stopped) break;
|
|
481
661
|
|
|
662
|
+
// Meter every tool call the model makes (per-tool tally), regardless of outcome — a denied or
|
|
663
|
+
// unknown call is still an invocation the operator wants to see.
|
|
664
|
+
metrics.toolCalls++;
|
|
665
|
+
metrics.byTool[tc.name] = (metrics.byTool[tc.name] || 0) + 1;
|
|
666
|
+
|
|
482
667
|
const tool = toolMap.get(tc.name);
|
|
483
668
|
if (!tool) {
|
|
484
669
|
const errMsg = `[Loop] Unknown tool: ${tc.name}`;
|
|
@@ -555,6 +740,10 @@ class Loop {
|
|
|
555
740
|
msgs.push({ role: 'tool', tool_call_id: tc.id, content });
|
|
556
741
|
this._safeEmit({ type: 'loop:tool_result', data: { tool: tc.name, result: content } });
|
|
557
742
|
} catch (err) {
|
|
743
|
+
// A HaltError from a tool body is a deliberate governance exit, not a tool failure — re-throw it
|
|
744
|
+
// like every other seam (the outer catch pairs dangling tool_calls + returns halt cleanly). Ordinary
|
|
745
|
+
// errors (plain Error) still become a ToolError, preserving the tool-as-untrusted-execution boundary.
|
|
746
|
+
if (err instanceof HaltError) throw err;
|
|
558
747
|
toolError = err instanceof ToolError ? err : new ToolError(err.message, { context: { tool: tc.name } });
|
|
559
748
|
const errMsg = `[Loop] Tool error: ${toolError.message}`;
|
|
560
749
|
msgs.push({ role: 'tool', tool_call_id: tc.id, content: errMsg });
|
|
@@ -592,7 +781,7 @@ class Loop {
|
|
|
592
781
|
sealDanglingToolCalls(msgs, rule);
|
|
593
782
|
this._reportError('halt', err, { rule, reason: err.decision?.reason ?? null });
|
|
594
783
|
this._safeEmit({ type: 'loop:done', data: { text: '', halted: true, rule, cost: totalCost } });
|
|
595
|
-
return { text: '', toolCalls: [], usage: lastUsage, cost: totalCost, error: `halt:${rule}`, msgs };
|
|
784
|
+
return { text: '', toolCalls: [], usage: lastUsage, cost: totalCost, error: `halt:${rule}`, msgs, metrics: finalizeMetrics() };
|
|
596
785
|
}
|
|
597
786
|
throw err;
|
|
598
787
|
}
|
|
@@ -601,7 +790,7 @@ class Loop {
|
|
|
601
790
|
// limits.maxTurns (or the LLM's natural completion) ends the loop first.
|
|
602
791
|
const warning = `[Loop] hit internal safety limit of ${HARD_ROUND_LIMIT} rounds. Wire bareguard for proper governance — see bare-agent/bareguard.`;
|
|
603
792
|
this._safeEmit({ type: 'loop:done', data: { text: '', warning, cost: totalCost } });
|
|
604
|
-
return { text: '', toolCalls: [], usage: lastUsage, cost: totalCost, error: warning, msgs };
|
|
793
|
+
return { text: '', toolCalls: [], usage: lastUsage, cost: totalCost, error: warning, msgs, metrics: finalizeMetrics() };
|
|
605
794
|
}
|
|
606
795
|
|
|
607
796
|
/**
|
|
@@ -674,7 +863,7 @@ class Loop {
|
|
|
674
863
|
* @param {string} text - User message.
|
|
675
864
|
* @param {ToolDef[]} [tools=[]] - Tool definitions.
|
|
676
865
|
* @param {Record<string, any>} [options={}] - Per-run overrides.
|
|
677
|
-
* @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, msgs: Message[]}>}
|
|
866
|
+
* @returns {Promise<{text: string, toolCalls: ToolCall[], usage: Usage, cost: number, error: string|null, msgs: Message[], metrics: RunMetrics}>}
|
|
678
867
|
*/
|
|
679
868
|
async chat(text, tools = [], options = {}) {
|
|
680
869
|
this._history.push({ role: 'user', content: text });
|
|
@@ -691,4 +880,4 @@ class Loop {
|
|
|
691
880
|
}
|
|
692
881
|
}
|
|
693
882
|
|
|
694
|
-
module.exports = { Loop };
|
|
883
|
+
module.exports = { Loop, estimateCost, COST_PER_1K };
|
package/src/memory.d.ts
CHANGED
|
@@ -27,13 +27,20 @@ export class Memory {
|
|
|
27
27
|
_store: Store;
|
|
28
28
|
/**
|
|
29
29
|
* @param {any} content
|
|
30
|
-
* @param {Record<string, any>} [metadata]
|
|
30
|
+
* @param {Record<string, any>} [metadata] - Stored alongside the content; persisted to the backend.
|
|
31
|
+
* @param {Record<string, any>} [opts] - Transient call options (NOT persisted). If `opts.ctx` carries a
|
|
32
|
+
* Loop-lent `recordMemoryOp` hook, this write is counted against that run's `result.metrics.memory.stored`
|
|
33
|
+
* (§3.6, opt-in — the write-side mirror of search's `recalls`). `ctx` rides here, never in `metadata`,
|
|
34
|
+
* precisely because `metadata` is persisted. Memory stays Loop-agnostic: it only calls an optional hook.
|
|
31
35
|
* @returns {any} id
|
|
32
36
|
*/
|
|
33
|
-
store(content: any, metadata?: Record<string, any>): any;
|
|
37
|
+
store(content: any, metadata?: Record<string, any>, opts?: Record<string, any>): any;
|
|
34
38
|
/**
|
|
35
39
|
* @param {string} query
|
|
36
|
-
* @param {Record<string, any>} [options]
|
|
40
|
+
* @param {Record<string, any>} [options] - Store search options. If `options.ctx` carries a Loop-lent
|
|
41
|
+
* `recordMemoryOp` hook, this recall is counted against that run's `result.metrics.memory.recalls`
|
|
42
|
+
* (§3.6, opt-in — the hook is absent unless the caller threads the run's ctx). Memory stays
|
|
43
|
+
* Loop-agnostic: it only calls an optional hook, and `ctx` is stripped, never forwarded to the store.
|
|
37
44
|
* @returns {any}
|
|
38
45
|
*/
|
|
39
46
|
search(query: string, options?: Record<string, any>): any;
|
package/src/memory.js
CHANGED
|
@@ -30,20 +30,31 @@ class Memory {
|
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* @param {any} content
|
|
33
|
-
* @param {Record<string, any>} [metadata]
|
|
33
|
+
* @param {Record<string, any>} [metadata] - Stored alongside the content; persisted to the backend.
|
|
34
|
+
* @param {Record<string, any>} [opts] - Transient call options (NOT persisted). If `opts.ctx` carries a
|
|
35
|
+
* Loop-lent `recordMemoryOp` hook, this write is counted against that run's `result.metrics.memory.stored`
|
|
36
|
+
* (§3.6, opt-in — the write-side mirror of search's `recalls`). `ctx` rides here, never in `metadata`,
|
|
37
|
+
* precisely because `metadata` is persisted. Memory stays Loop-agnostic: it only calls an optional hook.
|
|
34
38
|
* @returns {any} id
|
|
35
39
|
*/
|
|
36
|
-
store(content, metadata = {}) {
|
|
40
|
+
store(content, metadata = {}, opts = {}) {
|
|
41
|
+
const ctx = opts && opts.ctx;
|
|
42
|
+
if (ctx && typeof ctx.recordMemoryOp === 'function') ctx.recordMemoryOp('stored');
|
|
37
43
|
return this._store.store(content, metadata);
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
/**
|
|
41
47
|
* @param {string} query
|
|
42
|
-
* @param {Record<string, any>} [options]
|
|
48
|
+
* @param {Record<string, any>} [options] - Store search options. If `options.ctx` carries a Loop-lent
|
|
49
|
+
* `recordMemoryOp` hook, this recall is counted against that run's `result.metrics.memory.recalls`
|
|
50
|
+
* (§3.6, opt-in — the hook is absent unless the caller threads the run's ctx). Memory stays
|
|
51
|
+
* Loop-agnostic: it only calls an optional hook, and `ctx` is stripped, never forwarded to the store.
|
|
43
52
|
* @returns {any}
|
|
44
53
|
*/
|
|
45
54
|
search(query, options = {}) {
|
|
46
|
-
|
|
55
|
+
const { ctx, ...storeOptions } = options || {};
|
|
56
|
+
if (ctx && typeof ctx.recordMemoryOp === 'function') ctx.recordMemoryOp('recalls');
|
|
57
|
+
return this._store.search(query, storeOptions);
|
|
47
58
|
}
|
|
48
59
|
|
|
49
60
|
/**
|
|
@@ -10,6 +10,14 @@ export type AnthropicOptions = {
|
|
|
10
10
|
* - Model ID.
|
|
11
11
|
*/
|
|
12
12
|
model?: string | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* - API base (override for proxies/gateways; the request posts to `${baseUrl}/messages`).
|
|
15
|
+
*/
|
|
16
|
+
baseUrl?: string | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* - Opt-in prompt caching: send the system prompt with a `cache_control` breakpoint so Anthropic caches it. Anthropic does NOT auto-cache, so without this its cache tiers are always 0. Overridable per call via `generate(..., { cacheSystem })`.
|
|
19
|
+
*/
|
|
20
|
+
cacheSystem?: boolean | undefined;
|
|
13
21
|
/**
|
|
14
22
|
* - Attach the full upstream response to `err.body` on HTTP errors (off by default to avoid leaking unexpected fields through error logs; `err.message` still carries the API error).
|
|
15
23
|
*/
|
|
@@ -22,6 +30,8 @@ export type AnthropicOptions = {
|
|
|
22
30
|
* @typedef {object} AnthropicOptions
|
|
23
31
|
* @property {string} [apiKey] - Anthropic API key (required).
|
|
24
32
|
* @property {string} [model='claude-haiku-4-5-20251001'] - Model ID.
|
|
33
|
+
* @property {string} [baseUrl='https://api.anthropic.com/v1'] - API base (override for proxies/gateways; the request posts to `${baseUrl}/messages`).
|
|
34
|
+
* @property {boolean} [cacheSystem=false] - Opt-in prompt caching: send the system prompt with a `cache_control` breakpoint so Anthropic caches it. Anthropic does NOT auto-cache, so without this its cache tiers are always 0. Overridable per call via `generate(..., { cacheSystem })`.
|
|
25
35
|
* @property {boolean} [exposeErrorBody=false] - Attach the full upstream response to `err.body` on HTTP errors (off by default to avoid leaking unexpected fields through error logs; `err.message` still carries the API error).
|
|
26
36
|
*/
|
|
27
37
|
export class AnthropicProvider {
|
|
@@ -32,6 +42,8 @@ export class AnthropicProvider {
|
|
|
32
42
|
constructor(options?: AnthropicOptions);
|
|
33
43
|
apiKey: string;
|
|
34
44
|
model: string;
|
|
45
|
+
baseUrl: string;
|
|
46
|
+
cacheSystem: boolean;
|
|
35
47
|
exposeErrorBody: boolean;
|
|
36
48
|
/**
|
|
37
49
|
* Generate a response from the Anthropic API.
|
|
@@ -52,4 +64,5 @@ export class AnthropicProvider {
|
|
|
52
64
|
* @returns {Promise<any>}
|
|
53
65
|
*/
|
|
54
66
|
_request(body: Record<string, any>): Promise<any>;
|
|
67
|
+
_warnedInsecure: boolean | undefined;
|
|
55
68
|
}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const https = require('https');
|
|
4
|
+
const http = require('http');
|
|
4
5
|
const { ProviderError } = require('./errors');
|
|
5
6
|
|
|
7
|
+
/** @param {string} hostname @returns {boolean} */
|
|
8
|
+
function isLoopbackHost(hostname) {
|
|
9
|
+
const h = hostname.replace(/^\[|\]$/g, ''); // strip IPv6 brackets
|
|
10
|
+
return h === 'localhost' || h === '127.0.0.1' || h === '::1' || h.startsWith('127.');
|
|
11
|
+
}
|
|
12
|
+
|
|
6
13
|
/** @typedef {import('../types').Message} Message */
|
|
7
14
|
/** @typedef {import('../types').ToolDef} ToolDef */
|
|
8
15
|
/** @typedef {import('../types').GenerateResult} GenerateResult */
|
|
@@ -11,6 +18,8 @@ const { ProviderError } = require('./errors');
|
|
|
11
18
|
* @typedef {object} AnthropicOptions
|
|
12
19
|
* @property {string} [apiKey] - Anthropic API key (required).
|
|
13
20
|
* @property {string} [model='claude-haiku-4-5-20251001'] - Model ID.
|
|
21
|
+
* @property {string} [baseUrl='https://api.anthropic.com/v1'] - API base (override for proxies/gateways; the request posts to `${baseUrl}/messages`).
|
|
22
|
+
* @property {boolean} [cacheSystem=false] - Opt-in prompt caching: send the system prompt with a `cache_control` breakpoint so Anthropic caches it. Anthropic does NOT auto-cache, so without this its cache tiers are always 0. Overridable per call via `generate(..., { cacheSystem })`.
|
|
14
23
|
* @property {boolean} [exposeErrorBody=false] - Attach the full upstream response to `err.body` on HTTP errors (off by default to avoid leaking unexpected fields through error logs; `err.message` still carries the API error).
|
|
15
24
|
*/
|
|
16
25
|
|
|
@@ -23,6 +32,11 @@ class AnthropicProvider {
|
|
|
23
32
|
if (!options.apiKey) throw new Error('[AnthropicProvider] requires apiKey');
|
|
24
33
|
this.apiKey = options.apiKey.trim();
|
|
25
34
|
this.model = options.model || 'claude-haiku-4-5-20251001';
|
|
35
|
+
this.baseUrl = options.baseUrl || 'https://api.anthropic.com/v1';
|
|
36
|
+
// Opt-in prompt caching: when true, the system prompt is sent with a cache_control breakpoint so
|
|
37
|
+
// Anthropic caches it (unlike OpenAI/Gemini, Anthropic does NOT auto-cache — without this its
|
|
38
|
+
// cache_read/cache_creation tiers are always 0). Default off keeps requests byte-identical to before.
|
|
39
|
+
this.cacheSystem = options.cacheSystem === true;
|
|
26
40
|
// See OpenAIProvider: attach full upstream body to err.body only on opt-in.
|
|
27
41
|
this.exposeErrorBody = options.exposeErrorBody === true;
|
|
28
42
|
}
|
|
@@ -52,6 +66,15 @@ class AnthropicProvider {
|
|
|
52
66
|
// Override with options.system if provided
|
|
53
67
|
if (options.system) system = options.system;
|
|
54
68
|
|
|
69
|
+
// Opt-in caching: mark a string system prompt as a cache breakpoint. Anthropic caches the prefix
|
|
70
|
+
// up to this point (min ~1024-4096 tok depending on model; a shorter prompt silently won't cache —
|
|
71
|
+
// harmless). A per-call options.cacheSystem can override the instance default. Arrays are passed
|
|
72
|
+
// through untouched (the caller already shaped their own cache_control blocks).
|
|
73
|
+
const cacheSystem = options.cacheSystem != null ? options.cacheSystem === true : this.cacheSystem;
|
|
74
|
+
if (cacheSystem && typeof system === 'string' && system.length > 0) {
|
|
75
|
+
system = [{ type: 'text', text: system, cache_control: { type: 'ephemeral' } }];
|
|
76
|
+
}
|
|
77
|
+
|
|
55
78
|
/** @type {Record<string, any>} */
|
|
56
79
|
const body = {
|
|
57
80
|
model: this.model,
|
|
@@ -84,9 +107,13 @@ class AnthropicProvider {
|
|
|
84
107
|
text,
|
|
85
108
|
toolCalls,
|
|
86
109
|
model: data.model || this.model,
|
|
110
|
+
// Anthropic's `input_tokens` is ALREADY the uncached remainder (cached tokens are reported
|
|
111
|
+
// separately, not folded in — verified live), so no subtraction here, unlike OpenAI/Gemini.
|
|
87
112
|
usage: {
|
|
88
113
|
inputTokens: data.usage?.input_tokens || 0,
|
|
89
114
|
outputTokens: data.usage?.output_tokens || 0,
|
|
115
|
+
cacheReadTokens: data.usage?.cache_read_input_tokens || 0,
|
|
116
|
+
cacheCreationTokens: data.usage?.cache_creation_input_tokens || 0,
|
|
90
117
|
},
|
|
91
118
|
};
|
|
92
119
|
}
|
|
@@ -134,7 +161,15 @@ class AnthropicProvider {
|
|
|
134
161
|
_request(body) {
|
|
135
162
|
return new Promise((resolve, reject) => {
|
|
136
163
|
const payload = JSON.stringify(body);
|
|
137
|
-
const
|
|
164
|
+
const url = new URL(this.baseUrl + '/messages');
|
|
165
|
+
const transport = url.protocol === 'https:' ? https : http;
|
|
166
|
+
// Plaintext key to a remote host exposes it on the wire; loopback (test servers / local
|
|
167
|
+
// proxies) is the legitimate http case. Warn once, mirror OpenAIProvider.
|
|
168
|
+
if (url.protocol === 'http:' && !isLoopbackHost(url.hostname) && !this._warnedInsecure) {
|
|
169
|
+
this._warnedInsecure = true;
|
|
170
|
+
console.warn(`[AnthropicProvider] sending x-api-key over PLAINTEXT http to ${url.hostname} — key exposed on the wire. Use https.`);
|
|
171
|
+
}
|
|
172
|
+
const req = transport.request(url, {
|
|
138
173
|
method: 'POST',
|
|
139
174
|
headers: {
|
|
140
175
|
'Content-Type': 'application/json',
|