Skip to content

Commit d86c211

Browse files
committed
review: optional advisory fields, LLM/tool parity test, undate CHANGELOG
Address CodeRabbit feedback on #53: - BudgetAdvisory.expectedCost / estCallsRemaining are now optional in TS, so the additive fields don't break any code constructing an advisory literal (advisory() still always sets them). Python already ships defaulted fields. - Add a JS test asserting expectedCost is the costlier of the last LLM and tool call, matching the Python suite. - Keep the CHANGELOG entry under Unreleased instead of a future release date.
1 parent 4d6a17b commit d86c211

3 files changed

Lines changed: 20 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@ packages — `floe-guard` on [PyPI](https://pypi.org/project/floe-guard/) and
88
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and
99
both packages adhere to [Semantic Versioning](https://semver.org/).
1010

11-
## Unreleased
12-
13-
## py 0.11.0 / js 0.8.0 — 2026-07-25
11+
## Unreleased — py 0.11.0 / js 0.8.0
1412

1513
### Added (py + js)
1614

js/src/guard.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,14 +117,18 @@ export interface BudgetAdvisory {
117117
* The guard's own next-call estimate (the costlier of the last LLM and last
118118
* tool call — the same value the default reservation uses). 0 until the first
119119
* call is recorded, so a planner can't divide by a cold estimate.
120+
*
121+
* Optional so adding it stays a non-breaking, additive change for any code
122+
* that constructs a `BudgetAdvisory` literal; `advisory()` always sets it.
120123
*/
121-
expectedCost: number;
124+
expectedCost?: number;
122125
/**
123126
* How many more calls the remaining budget buys at expectedCost:
124127
* floor(remainingUsd / expectedCost). null when expectedCost is 0 (no call
125-
* recorded yet) — unknown, not zero.
128+
* recorded yet) — unknown, not zero. Optional for the same additive reason as
129+
* expectedCost; `advisory()` always sets it.
126130
*/
127-
estCallsRemaining: number | null;
131+
estCallsRemaining?: number | null;
128132
}
129133

130134
export class BudgetGuard {

js/test/advisory.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,18 @@ describe("BudgetGuard.advisory", () => {
6666
expect(a.estCallsRemaining).toBe(9); // floor(0.90 / 0.10)
6767
});
6868

69+
it("expectedCost is the costlier of the last LLM and tool call", () => {
70+
// Parity with the Python suite: a cheap tool after an expensive LLM call
71+
// must not shrink the estimate — the max wins (conservative).
72+
const g = new BudgetGuard(1.0);
73+
g.record("gpt-4o", 1_000, 1_000); // $0.0125 LLM
74+
g.recordTool("exa.search", 0.001); // cheaper tool, spent = $0.0135
75+
const a = g.advisory();
76+
expect(a.expectedCost).toBeCloseTo(0.0125, 9); // LLM side, not the cheaper tool
77+
// floor((1.0 - 0.0135) / 0.0125) = floor(78.92) = 78
78+
expect(a.estCallsRemaining).toBe(78);
79+
});
80+
6981
it("rejects an out-of-range or non-integer nearLimitBps", () => {
7082
expect(() => new BudgetGuard(1.0, { nearLimitBps: -1 })).toThrow(RangeError);
7183
expect(() => new BudgetGuard(1.0, { nearLimitBps: 10001 })).toThrow(RangeError);

0 commit comments

Comments
 (0)