fix: stop Surplus offer at provider spend cap

This commit is contained in:
david
2026-08-27 13:31:50 +02:00
parent ced400f928
commit 799c7d8256
3 changed files with 60 additions and 1 deletions

View File

@@ -262,6 +262,7 @@ export class SurplusSellerController {
costMultiplierPpm: quote.costMultiplierPpm, costMultiplierPpm: quote.costMultiplierPpm,
inputMicroUsdPer1m: quote.inputMicroUsdPer1m, inputMicroUsdPer1m: quote.inputMicroUsdPer1m,
outputMicroUsdPer1m: quote.outputMicroUsdPer1m, outputMicroUsdPer1m: quote.outputMicroUsdPer1m,
baselineRemainingUsdNanos: allowance.remainingUsdNanos.toString(),
dailyCapUsd: Number(this.config.perOfferCapUsd), dailyCapUsd: Number(this.config.perOfferCapUsd),
idempotencyKey: this.mutationId(), idempotencyKey: this.mutationId(),
createdAt: new Date(this.now()).toISOString(), createdAt: new Date(this.now()).toISOString(),
@@ -445,6 +446,28 @@ export class SurplusSellerController {
allowance, allowance,
parseUsdDecimal(this.config.perOfferCapUsd, "ceil"), parseUsdDecimal(this.config.perOfferCapUsd, "ceil"),
); );
const baselineAllowance = BigInt(
currentState.intent.baselineRemainingUsdNanos,
);
const providerSpend = baselineAllowance > allowance.remainingUsdNanos
? baselineAllowance - allowance.remainingUsdNanos
: 0n;
const spendCap = parseUsdDecimal(this.config.perOfferCapUsd, "ceil");
if (providerSpend >= spendCap) {
const stopping: SurplusControllerState = {
version: 1,
phase: "stopping",
offerId: currentState.offerId,
intent: currentState.intent,
highestTrades24h: currentState.highestTrades24h,
tradeObservedAt: currentState.intent.createdAt,
pauseIdempotencyKey: this.mutationId(),
};
await this.stateStore.save(stopping);
this.state = stopping;
await this.finishStopping();
return;
}
const discovered = await this.client.discoverModels( const discovered = await this.client.discoverModels(
this.config.providerApiKey, this.config.providerApiKey,
this.config.providerBaseUrl, this.config.providerBaseUrl,

View File

@@ -18,6 +18,7 @@ export interface SurplusOfferIntent {
inputMicroUsdPer1m: number; inputMicroUsdPer1m: number;
outputMicroUsdPer1m: number; outputMicroUsdPer1m: number;
dailyCapUsd: number; dailyCapUsd: number;
baselineRemainingUsdNanos: string;
idempotencyKey: string; idempotencyKey: string;
createdAt: string; createdAt: string;
baselineTrades24h: number; baselineTrades24h: number;
@@ -283,6 +284,10 @@ function parseIntent(value: unknown): SurplusOfferIntent {
intent.dailyCapUsd, intent.dailyCapUsd,
"Surplus state intent.dailyCapUsd", "Surplus state intent.dailyCapUsd",
), ),
baselineRemainingUsdNanos: nonNegativeIntegerString(
intent.baselineRemainingUsdNanos,
"Surplus state intent.baselineRemainingUsdNanos",
),
idempotencyKey: nonEmptyString( idempotencyKey: nonEmptyString(
intent.idempotencyKey, intent.idempotencyKey,
"Surplus state intent.idempotencyKey", "Surplus state intent.idempotencyKey",
@@ -309,6 +314,13 @@ function nonEmptyString(value: unknown, field: string): string {
return value; return value;
} }
function nonNegativeIntegerString(value: unknown, field: string): string {
if (typeof value !== "string" || !/^(?:0|[1-9]\d*)$/.test(value)) {
throw new Error(`${field} must be a non-negative integer string`);
}
return value;
}
function isoTimestamp(value: unknown, field: string): string { function isoTimestamp(value: unknown, field: string): string {
const timestamp = nonEmptyString(value, field); const timestamp = nonEmptyString(value, field);
if (!Number.isFinite(Date.parse(timestamp))) { if (!Number.isFinite(Date.parse(timestamp))) {

View File

@@ -208,12 +208,15 @@ const ALLOWANCE: OpenRouterKeyAllowance = {
function controller( function controller(
client: FakeSurplusClient, client: FakeSurplusClient,
store: MemoryStateStore, store: MemoryStateStore,
allowance: { readAllowance(): Promise<OpenRouterKeyAllowance> } = {
readAllowance: async () => ALLOWANCE,
},
): SurplusSellerController { ): SurplusSellerController {
let mutation = 0; let mutation = 0;
return new SurplusSellerController({ return new SurplusSellerController({
config: config(), config: config(),
client, client,
allowance: { readAllowance: async () => ALLOWANCE }, allowance,
stateStore: store, stateStore: store,
now: () => Date.parse("2026-08-27T10:00:00.000Z"), now: () => Date.parse("2026-08-27T10:00:00.000Z"),
mutationId: () => `mutation-${++mutation}`, mutationId: () => `mutation-${++mutation}`,
@@ -260,6 +263,26 @@ describe("SurplusSellerController", () => {
await restarted.stop(); await restarted.stop();
}); });
it("pauses when provider spend reaches the per-offer cap", async () => {
const client = new FakeSurplusClient();
const store = new MemoryStateStore();
let remaining = 20_000_000_000n;
const activeController = controller(client, store, {
readAllowance: async () => ({
...ALLOWANCE,
remainingUsdNanos: remaining,
}),
});
await activeController.runOnce();
expect(client.offer?.status).toBe("active");
remaining = 18_700_000_000n;
await activeController.runOnce();
expect(client.offer?.status).toBe("inactive");
expect(store.state.phase).toBe("awaiting_settlement");
});
it("adopts and resumes an inactive offer after an ambiguous create", async () => { it("adopts and resumes an inactive offer after an ambiguous create", async () => {
const client = new FakeSurplusClient(); const client = new FakeSurplusClient();
client.offer = { client.offer = {
@@ -283,6 +306,7 @@ describe("SurplusSellerController", () => {
inputMicroUsdPer1m: 3_972, inputMicroUsdPer1m: 3_972,
outputMicroUsdPer1m: 7_944, outputMicroUsdPer1m: 7_944,
dailyCapUsd: 1, dailyCapUsd: 1,
baselineRemainingUsdNanos: "20000000000",
idempotencyKey: "create-1", idempotencyKey: "create-1",
createdAt: "2026-08-27T10:00:00.000Z", createdAt: "2026-08-27T10:00:00.000Z",
baselineTrades24h: 0, baselineTrades24h: 0,