Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/server/responses/run-turn-execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,7 @@ export async function executeResponsesRunTurn(
if (preflight.replayUnsafe || preflight.error?.status !== 429
|| preflight.error.code === SEND_BUDGET_EXHAUSTED_CODE) return;
const { httpStatus, error } = adapterFailureFromEvent(preflight.error);
transportState.bindKeyUsageFromBridge(preflight.error.usage);
cancelResponseCompletion();
runTurnAbort.abort();
queue.close();
Expand Down
4 changes: 3 additions & 1 deletion structure/transports/responses-spend.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,9 @@ during this process's lifetime can still be released for free.
Settlement follows what the request learned. The terminal usage belongs to the last send that
left, so that one settles with the real figure; every earlier send failed without reporting usage
of its own and may still have been billed, so it becomes unresolved spend rather than free. A
request that reports no usage at all leaves all of them unresolved. If deferred settlement reaches a tracker with reserved sends after its ledger lease ends, only `SPEND_LEDGER_OWNER_NOT_HELD` is dropped with the discarded ledger. Other owner and storage failures propagate with pending send IDs intact so settlement can be retried.
request that reports no usage at all leaves all of them unresolved. A pre-output Grok/Devin 429
binds usage carried by its error event before returning the HTTP refusal, just as the ordinary
streaming and buffered bridges bind terminal usage. If deferred settlement reaches a tracker with reserved sends after its ledger lease ends, only `SPEND_LEDGER_OWNER_NOT_HELD` is dropped with the discarded ledger. Other owner and storage failures propagate with pending send IDs intact so settlement can be retried.

Replay resolves what nobody is left to settle, and resolves it as unresolved spend whatever state
it was in. Giving an undispatched one its tokens back would assume the journal is complete up to
Expand Down
29 changes: 28 additions & 1 deletion tests/responses/responses-grok-devin-preflight.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { afterEach, beforeEach, expect, mock, test } from "bun:test";
import { existsSync, readFileSync } from "node:fs";
import { join } from "node:path";
import type { ProviderAdapter } from "../../src/adapters/base";
import type { AdapterEvent, OcxConfig, OcxProviderConfig } from "../../src/types";
import { saveCredential } from "../../src/oauth/store";
Expand Down Expand Up @@ -31,6 +33,8 @@ mock.module("../../src/server/adapter-resolve", () => ({ ...resolver,
},
}));
const { handleResponses } = await import("../../src/server/responses");
const { addFinalRequestLog } = await import("../../src/server/request-log");
let requestLog: Parameters<typeof handleResponses>[2];
let home: ReturnType<typeof createTempHome>;
let release: (() => void) | undefined;
beforeEach(async () => {
Expand Down Expand Up @@ -63,10 +67,11 @@ function run({
stallTimeoutSec,
providers: { devin: { adapter: "devin", authMode: "oauth", baseUrl: "https://server.codeium.com", models: ["swe-2"] } },
} as OcxConfig;
requestLog = { model: "", provider: "", surface };
return handleResponses(new Request("http://localhost/v1/responses", {
method: "POST", headers: { "content-type": "application/json" },
body: JSON.stringify({ model: "devin/swe-2", input: "answer", stream }),
}), config, { model: "", provider: "", surface }, { comboAttempt, abortSignal });
}), config, requestLog, { comboAttempt, abortSignal });
}

async function waitForPreflightResponse(pending: Promise<Response>, started: Promise<void>, resume: () => void) {
Expand Down Expand Up @@ -103,6 +108,28 @@ test.each([
expect(calls).toBe(1);
});

test.each([true, false])("pre-output 429 preserves metered usage (stream=%s)", async stream => {
const usage = { inputTokens: 23, outputTokens: 5, totalTokens: 28 };
events = [{ ...limit, usage }];
expect((await run({ stream })).status).toBe(429);
expect(requestLog.usage).toEqual(usage);
});
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

test.each([true, false])("pre-output 429 settles durable spend in usage.jsonl (stream=%s)", async stream => {
const usage = { inputTokens: 23, outputTokens: 5, totalTokens: 28 };
events = [{ ...limit, usage }];
expect((await run({ stream })).status).toBe(429);
// handleResponses only populates the log context; the route layer's addFinalRequestLog is the
// seam that settles the spend tracker and appends usage.jsonl. Drive it with this request's
// context so the durable row proves the bound usage survives finalization.
addFinalRequestLog(`preflight-usage-${stream}`, Date.now(), requestLog, 429, { closeReason: "non_stream" });
const journalPath = join(home.root, "usage.jsonl");
expect(existsSync(journalPath)).toBe(true);
const rows = readFileSync(journalPath, "utf8").trim().split("\n").filter(Boolean)
.map(line => JSON.parse(line) as { usage?: typeof usage });
expect(rows.at(-1)?.usage).toMatchObject({ inputTokens: 23, outputTokens: 5 });
});

test.each([false, true])("first text is replayed once and later errors stay SSE (failure=%s)", async failure => {
events = [{ type: "heartbeat" }, { type: "text_delta", text: "answer" }, failure ? limit : { type: "done" }];
const response = await run();
Expand Down
Loading