fix: manage Surplus multiplier offers
This commit is contained in:
@@ -381,7 +381,10 @@ function parseOffer(value: unknown, index: number): SurplusOffer {
|
|||||||
return {
|
return {
|
||||||
id: offerId(item, `Surplus offer ${index}`),
|
id: offerId(item, `Surplus offer ${index}`),
|
||||||
model: string(item.model, `Surplus offer ${index}.model`),
|
model: string(item.model, `Surplus offer ${index}.model`),
|
||||||
sellerBaseUrl: string(item.seller_base_url, `Surplus offer ${index}.seller_base_url`),
|
sellerBaseUrl: string(
|
||||||
|
item.seller_base_url ?? item.base_url,
|
||||||
|
`Surplus offer ${index}.base_url`,
|
||||||
|
),
|
||||||
status,
|
status,
|
||||||
capDailyUsd: optionalNonNegativeNumber(item.cap_daily_usd, `Surplus offer ${index}.cap_daily_usd`),
|
capDailyUsd: optionalNonNegativeNumber(item.cap_daily_usd, `Surplus offer ${index}.cap_daily_usd`),
|
||||||
costMultiplierPpm: optionalMultiplierPpm(
|
costMultiplierPpm: optionalMultiplierPpm(
|
||||||
|
|||||||
@@ -359,44 +359,40 @@ export class SurplusSellerController {
|
|||||||
const pending = this.state;
|
const pending = this.state;
|
||||||
const offers = await this.client.listAllOffers();
|
const offers = await this.client.listAllOffers();
|
||||||
const active = offers.filter((offer) => offer.status === "active");
|
const active = offers.filter((offer) => offer.status === "active");
|
||||||
const matches = active
|
const activeMatches = active
|
||||||
.filter((offer) =>
|
.filter((offer) =>
|
||||||
offer.model === pending.intent.model &&
|
offer.model === pending.intent.model &&
|
||||||
normalizeUrl(offer.sellerBaseUrl) ===
|
normalizeUrl(offer.sellerBaseUrl) ===
|
||||||
normalizeUrl(this.config.providerBaseUrl)
|
normalizeUrl(this.config.providerBaseUrl)
|
||||||
)
|
)
|
||||||
.sort((left, right) => left.id.localeCompare(right.id));
|
.sort((left, right) => left.id.localeCompare(right.id));
|
||||||
if (matches.length === 0) {
|
const inactiveMatches = offers
|
||||||
const inactiveMatch = offers.some((offer) =>
|
.filter((offer) =>
|
||||||
offer.status === "inactive" &&
|
offer.status === "inactive" &&
|
||||||
offer.model === pending.intent.model &&
|
offer.model === pending.intent.model &&
|
||||||
normalizeUrl(offer.sellerBaseUrl) ===
|
normalizeUrl(offer.sellerBaseUrl) ===
|
||||||
normalizeUrl(this.config.providerBaseUrl)
|
normalizeUrl(this.config.providerBaseUrl)
|
||||||
);
|
)
|
||||||
if (inactiveMatch && active.length === 0) {
|
.sort((left, right) => left.id.localeCompare(right.id));
|
||||||
const selecting: SurplusControllerState = {
|
const primary = activeMatches[0] ?? inactiveMatches[0];
|
||||||
version: 1,
|
if (!primary) {
|
||||||
phase: "selecting",
|
|
||||||
};
|
|
||||||
await this.stateStore.save(selecting);
|
|
||||||
this.state = selecting;
|
|
||||||
this.markSuccess("suspended", null, null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (active.length > 0) await this.pauseOffers(active);
|
if (active.length > 0) await this.pauseOffers(active);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Create result is ambiguous and no matching offer is visible; refusing another POST",
|
"Create result is ambiguous and no matching offer is visible; refusing another POST",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const primary = matches[0];
|
|
||||||
await this.pauseOffers(active.filter((offer) => offer.id !== primary.id));
|
await this.pauseOffers(active.filter((offer) => offer.id !== primary.id));
|
||||||
const confirmedOffers = await this.client.listAllOffers();
|
const confirmedOffers = await this.client.listAllOffers();
|
||||||
const confirmedActive = confirmedOffers.filter(
|
const confirmedActive = confirmedOffers.filter(
|
||||||
(offer) => offer.status === "active",
|
(offer) => offer.status === "active",
|
||||||
);
|
);
|
||||||
if (confirmedActive.length !== 1 || confirmedActive[0].id !== primary.id) {
|
const expectedActive = primary.status === "active" ? 1 : 0;
|
||||||
|
if (
|
||||||
|
confirmedActive.length !== expectedActive ||
|
||||||
|
(expectedActive === 1 && confirmedActive[0].id !== primary.id)
|
||||||
|
) {
|
||||||
await this.pauseOffers(confirmedActive);
|
await this.pauseOffers(confirmedActive);
|
||||||
throw new Error("Adopted Surplus create did not reconcile to one active offer");
|
throw new Error("Adopted Surplus create did not reconcile safely");
|
||||||
}
|
}
|
||||||
const adopted: SurplusControllerState = {
|
const adopted: SurplusControllerState = {
|
||||||
version: 1,
|
version: 1,
|
||||||
@@ -413,6 +409,10 @@ export class SurplusSellerController {
|
|||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
this.state = adopted;
|
this.state = adopted;
|
||||||
|
if (primary.status === "inactive") {
|
||||||
|
await this.manageActiveOffer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.markSuccess("active", null, {
|
this.markSuccess("active", null, {
|
||||||
intent: adopted.intent,
|
intent: adopted.intent,
|
||||||
offerId: adopted.offerId,
|
offerId: adopted.offerId,
|
||||||
@@ -492,7 +492,8 @@ export class SurplusSellerController {
|
|||||||
outputMicroUsdPer1m: quote.outputMicroUsdPer1m,
|
outputMicroUsdPer1m: quote.outputMicroUsdPer1m,
|
||||||
};
|
};
|
||||||
const priceChanged =
|
const priceChanged =
|
||||||
managed.costMultiplierPpm !== quote.costMultiplierPpm ||
|
managed.costMultiplierPpm === null ||
|
||||||
|
Math.abs(managed.costMultiplierPpm - quote.costMultiplierPpm) > 1 ||
|
||||||
managed.capDailyUsd !== Number(this.config.perOfferCapUsd);
|
managed.capDailyUsd !== Number(this.config.perOfferCapUsd);
|
||||||
if (this.config.live && priceChanged) {
|
if (this.config.live && priceChanged) {
|
||||||
await this.client.updateOffer(currentState.offerId, {
|
await this.client.updateOffer(currentState.offerId, {
|
||||||
|
|||||||
@@ -260,6 +260,42 @@ describe("SurplusSellerController", () => {
|
|||||||
await restarted.stop();
|
await restarted.stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("adopts and resumes an inactive offer after an ambiguous create", async () => {
|
||||||
|
const client = new FakeSurplusClient();
|
||||||
|
client.offer = {
|
||||||
|
id: "managed-offer",
|
||||||
|
model: "beta-model",
|
||||||
|
sellerBaseUrl: "https://openrouter.ai/api/v1",
|
||||||
|
status: "inactive",
|
||||||
|
capDailyUsd: 1,
|
||||||
|
costMultiplierPpm: 49_950,
|
||||||
|
inputMicroUsdPer1m: 3_972,
|
||||||
|
outputMicroUsdPer1m: 7_944,
|
||||||
|
};
|
||||||
|
const store = new MemoryStateStore();
|
||||||
|
store.state = {
|
||||||
|
version: 1,
|
||||||
|
phase: "create_pending",
|
||||||
|
intent: {
|
||||||
|
model: "beta-model",
|
||||||
|
providerModelId: "vendor/beta-model",
|
||||||
|
costMultiplierPpm: 49_950,
|
||||||
|
inputMicroUsdPer1m: 3_972,
|
||||||
|
outputMicroUsdPer1m: 7_944,
|
||||||
|
dailyCapUsd: 1,
|
||||||
|
idempotencyKey: "create-1",
|
||||||
|
createdAt: "2026-08-27T10:00:00.000Z",
|
||||||
|
baselineTrades24h: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
await controller(client, store).runOnce();
|
||||||
|
|
||||||
|
expect(client.createCalls).toBe(0);
|
||||||
|
expect(client.offer.status).toBe("active");
|
||||||
|
expect(store.state.phase).toBe("active");
|
||||||
|
});
|
||||||
|
|
||||||
it("never retries an ambiguous create when no matching offer is visible", async () => {
|
it("never retries an ambiguous create when no matching offer is visible", async () => {
|
||||||
const client = new FakeSurplusClient();
|
const client = new FakeSurplusClient();
|
||||||
client.createError = new SurplusHttpError("network lost", null);
|
client.createError = new SurplusHttpError("network lost", null);
|
||||||
|
|||||||
Reference in New Issue
Block a user