mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-03 07:18:03 +01:00
autoplan shadow: reply files never follow a link; the retry reserves against the current cap
- a model reply is written by temp file and rename, so a link at the reply path is replaced, not written through - each shadow reservation reads the run's current spend cap, so a budget change during the call is not undone Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
3bc7341b02
commit
d5bf54316d
2 files changed
+28
-2
No files matched your search
@@ -308,7 +308,7 @@ export async function runShadow(out: string, claim: ShadowClaim, o: RunShadowOpt
|
||||
const remaining = deadline - Date.now();
|
||||
if (remaining <= 0) throw new ShadowFailure('timeout', `the ${(o.deadlineMs ?? SHADOW_DEADLINE_MS) / 1000} s deadline passed before the call`);
|
||||
const attempt = `shadow-r${rev}-${callIds(readJournal(out), rev).length + 1}`;
|
||||
try { reserve(ledgerFile, attempt, estimate(maxTokens), { cap: state.spend_cap_usd, label: 'shadow' }); }
|
||||
try { reserve(ledgerFile, attempt, estimate(maxTokens), { cap: readState(out).spend_cap_usd, label: 'shadow' }); }
|
||||
catch (e: any) { throw new ShadowFailure('budget', e.message); }
|
||||
journal(out, { event: 'shadow_dispatched', gate_rev: rev, attempt, model, max_tokens: maxTokens, estimate_usd: estimate(maxTokens), retrospective: claim.retrospective });
|
||||
let result: ApiCallResult;
|
||||
@@ -322,7 +322,7 @@ export async function runShadow(out: string, claim: ShadowClaim, o: RunShadowOpt
|
||||
const { input_tokens: inTok, output_tokens: outTok } = result.usage;
|
||||
const usd: number | 'unknown' = typeof inTok === 'number' && typeof outTok === 'number' ? estimateCostUsd({ input: inTok, output: outTok }, model) : 'unknown';
|
||||
settle(ledgerFile, attempt, usd);
|
||||
fs.writeFileSync(path.join(shadowDir(out), `${attempt}.reply.txt`), result.text);
|
||||
writeAtomic(path.join(shadowDir(out), `${attempt}.reply.txt`), result.text);
|
||||
const reply = { result, usd, attempt };
|
||||
replies.push(reply);
|
||||
return reply;
|
||||
|
||||
@@ -216,6 +216,32 @@ describe('runShadow: retry, truncation, failures and the deadline', async () =>
|
||||
expect(readLedger(path.join(out, 'spend.json')).settled.filter(s => s.label === 'shadow').map(s => [s.attempt, s.usd])).toEqual([['shadow-r1-1', 0.014]]);
|
||||
expect(fs.existsSync(path.join(out, 'shadow', 'r1.pending'))).toBe(false);
|
||||
});
|
||||
test('a link at a reply path is replaced, never written through', async () => {
|
||||
const f = await fake('linked-reply', [{ mode: 'valid' }]);
|
||||
const out = gateRun();
|
||||
const { claim } = claimShadow(out);
|
||||
const sentinel = path.join(TMP, 'sentinel.txt');
|
||||
fs.writeFileSync(sentinel, 'keep');
|
||||
const reply = path.join(out, 'shadow', 'shadow-r1-1.reply.txt');
|
||||
fs.symlinkSync(sentinel, reply);
|
||||
expect((await runShadow(out, claim!, { env: { ...process.env, ...f.env } })).status).toBe('recorded');
|
||||
expect(fs.readFileSync(sentinel, 'utf8')).toBe('keep');
|
||||
expect(fs.lstatSync(reply).isSymbolicLink()).toBe(false);
|
||||
expect(fs.readFileSync(reply, 'utf8')).toContain('fake prediction');
|
||||
});
|
||||
test('a cap changed during the call admits the retry against the new cap and is not written back over', async () => {
|
||||
const f = await fake('cap-moved', [{ mode: 'truncated', delay_ms: 400 }, { mode: 'valid' }]);
|
||||
const out = gateRun();
|
||||
const { claim } = claimShadow(out);
|
||||
const pending = runShadow(out, claim!, { env: { ...process.env, ...f.env } });
|
||||
await Bun.sleep(150);
|
||||
const run = readRun(out);
|
||||
run.spend_cap_usd = 2;
|
||||
fs.writeFileSync(path.join(out, 'run.json'), JSON.stringify(run, null, 2) + '\n');
|
||||
expect((await pending).status).toBe('recorded');
|
||||
expect(f.requests().length).toBe(2);
|
||||
expect(readLedger(path.join(out, 'spend.json')).cap_usd).toBe(2);
|
||||
});
|
||||
test('a malformed page is asked once more; items valid in either reply record, the rest record malformed', async () => {
|
||||
const fixed = await shadowed('missing-then-valid', [{ mode: 'missing' }, { mode: 'valid' }]);
|
||||
expect(fixed.f.requests().length).toBe(2);
|
||||
|
||||
Reference in new issue
Block a user