fix: quote Surplus text offers by multiplier
This commit is contained in:
@@ -4,6 +4,7 @@ export interface SurplusOffer {
|
||||
sellerBaseUrl: string;
|
||||
status: "active" | "inactive";
|
||||
capDailyUsd: number | null;
|
||||
costMultiplierPpm: number | null;
|
||||
inputMicroUsdPer1m: number | null;
|
||||
outputMicroUsdPer1m: number | null;
|
||||
}
|
||||
@@ -50,16 +51,14 @@ export interface SurplusOfferWrite {
|
||||
model: string;
|
||||
apiKey: string;
|
||||
sellerBaseUrl: string;
|
||||
inputUsdPer1m: number;
|
||||
outputUsdPer1m: number;
|
||||
costMultiplier: number;
|
||||
dailyCapUsd: number;
|
||||
payoutAddress: string;
|
||||
idempotencyKey: string;
|
||||
}
|
||||
|
||||
export interface SurplusOfferPatch {
|
||||
inputUsdPer1m: number;
|
||||
outputUsdPer1m: number;
|
||||
costMultiplier: number;
|
||||
dailyCapUsd: number;
|
||||
idempotencyKey: string;
|
||||
}
|
||||
@@ -223,9 +222,8 @@ export class SurplusClient {
|
||||
model: input.model,
|
||||
api_key: input.apiKey,
|
||||
seller_base_url: input.sellerBaseUrl,
|
||||
pricing_mode: "per_token",
|
||||
price_input_per_1m: input.inputUsdPer1m,
|
||||
price_output_per_1m: input.outputUsdPer1m,
|
||||
pricing_mode: "cost_multiplier",
|
||||
cost_multiplier: input.costMultiplier,
|
||||
cap_daily_usd: input.dailyCapUsd,
|
||||
payout_address: input.payoutAddress,
|
||||
}),
|
||||
@@ -239,9 +237,8 @@ export class SurplusClient {
|
||||
method: "PATCH",
|
||||
headers: mutationHeaders(patch.idempotencyKey),
|
||||
body: JSON.stringify({
|
||||
pricing_mode: "per_token",
|
||||
price_input_per_1m: patch.inputUsdPer1m,
|
||||
price_output_per_1m: patch.outputUsdPer1m,
|
||||
pricing_mode: "cost_multiplier",
|
||||
cost_multiplier: patch.costMultiplier,
|
||||
cap_daily_usd: patch.dailyCapUsd,
|
||||
}),
|
||||
});
|
||||
@@ -387,6 +384,10 @@ function parseOffer(value: unknown, index: number): SurplusOffer {
|
||||
sellerBaseUrl: string(item.seller_base_url, `Surplus offer ${index}.seller_base_url`),
|
||||
status,
|
||||
capDailyUsd: optionalNonNegativeNumber(item.cap_daily_usd, `Surplus offer ${index}.cap_daily_usd`),
|
||||
costMultiplierPpm: optionalMultiplierPpm(
|
||||
item.cost_multiplier,
|
||||
`Surplus offer ${index}.cost_multiplier`,
|
||||
),
|
||||
inputMicroUsdPer1m: optionalMicroUsd(item.price_input_per_1m, `Surplus offer ${index}.price_input_per_1m`),
|
||||
outputMicroUsdPer1m: optionalMicroUsd(item.price_output_per_1m, `Surplus offer ${index}.price_output_per_1m`),
|
||||
};
|
||||
@@ -516,6 +517,18 @@ function optionalNonNegativeNumber(value: unknown, field: string): number | null
|
||||
return nonNegativeNumber(value, field);
|
||||
}
|
||||
|
||||
function optionalMultiplierPpm(value: unknown, field: string): number | null {
|
||||
if (value === null || value === undefined) return null;
|
||||
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
||||
throw new Error(`${field} must be a positive finite number`);
|
||||
}
|
||||
const ppm = Math.round(value * 1_000_000);
|
||||
if (!Number.isSafeInteger(ppm) || ppm <= 0) {
|
||||
throw new Error(`${field} cannot be represented at six decimal places`);
|
||||
}
|
||||
return ppm;
|
||||
}
|
||||
|
||||
function sanitizedErrorSnippet(
|
||||
value: string,
|
||||
secrets: readonly string[],
|
||||
|
||||
@@ -14,7 +14,7 @@ import type {
|
||||
} from "./openRouterKey.js";
|
||||
import {
|
||||
formatMicroUsd,
|
||||
quoteSurplusPrice,
|
||||
quoteSurplusMultiplier,
|
||||
} from "./policy.js";
|
||||
import {
|
||||
eligibleCompetitors,
|
||||
@@ -236,7 +236,7 @@ export class SurplusSellerController {
|
||||
this.config.sellerWallet,
|
||||
ownOfferIds,
|
||||
);
|
||||
const quote = quoteSurplusPrice({
|
||||
const quote = quoteSurplusMultiplier({
|
||||
upstreamInputMicroUsdPer1m: candidate.upstreamInputMicroUsdPer1m,
|
||||
upstreamOutputMicroUsdPer1m: candidate.upstreamOutputMicroUsdPer1m,
|
||||
recoveryBps: this.config.recoveryBps,
|
||||
@@ -259,6 +259,7 @@ export class SurplusSellerController {
|
||||
intent = {
|
||||
model: candidate.model,
|
||||
providerModelId: candidate.providerModelId,
|
||||
costMultiplierPpm: quote.costMultiplierPpm,
|
||||
inputMicroUsdPer1m: quote.inputMicroUsdPer1m,
|
||||
outputMicroUsdPer1m: quote.outputMicroUsdPer1m,
|
||||
dailyCapUsd: Number(this.config.perOfferCapUsd),
|
||||
@@ -296,8 +297,7 @@ export class SurplusSellerController {
|
||||
model: intent.model,
|
||||
apiKey: this.config.providerApiKey,
|
||||
sellerBaseUrl: this.config.providerBaseUrl,
|
||||
inputUsdPer1m: intent.inputMicroUsdPer1m / 1_000_000,
|
||||
outputUsdPer1m: intent.outputMicroUsdPer1m / 1_000_000,
|
||||
costMultiplier: intent.costMultiplierPpm / 1_000_000,
|
||||
dailyCapUsd: intent.dailyCapUsd,
|
||||
payoutAddress: this.config.payoutAddress,
|
||||
idempotencyKey: intent.idempotencyKey,
|
||||
@@ -478,7 +478,7 @@ export class SurplusSellerController {
|
||||
this.config.sellerWallet,
|
||||
ownIds,
|
||||
);
|
||||
const quote = quoteSurplusPrice({
|
||||
const quote = quoteSurplusMultiplier({
|
||||
upstreamInputMicroUsdPer1m: Math.ceil(selected.inputUsdPer1m * 1_000_000),
|
||||
upstreamOutputMicroUsdPer1m: Math.ceil(selected.outputUsdPer1m * 1_000_000),
|
||||
recoveryBps: this.config.recoveryBps,
|
||||
@@ -487,16 +487,16 @@ export class SurplusSellerController {
|
||||
});
|
||||
const nextIntent: SurplusOfferIntent = {
|
||||
...currentState.intent,
|
||||
costMultiplierPpm: quote.costMultiplierPpm,
|
||||
inputMicroUsdPer1m: quote.inputMicroUsdPer1m,
|
||||
outputMicroUsdPer1m: quote.outputMicroUsdPer1m,
|
||||
};
|
||||
const priceChanged = managed.inputMicroUsdPer1m !== quote.inputMicroUsdPer1m ||
|
||||
managed.outputMicroUsdPer1m !== quote.outputMicroUsdPer1m ||
|
||||
const priceChanged =
|
||||
managed.costMultiplierPpm !== quote.costMultiplierPpm ||
|
||||
managed.capDailyUsd !== Number(this.config.perOfferCapUsd);
|
||||
if (this.config.live && priceChanged) {
|
||||
await this.client.updateOffer(currentState.offerId, {
|
||||
inputUsdPer1m: quote.inputUsdPer1m,
|
||||
outputUsdPer1m: quote.outputUsdPer1m,
|
||||
costMultiplier: quote.costMultiplier,
|
||||
dailyCapUsd: Number(this.config.perOfferCapUsd),
|
||||
idempotencyKey: this.mutationId(),
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@ export interface SurplusCompetitor {
|
||||
outputMicroUsdPer1m: number;
|
||||
}
|
||||
|
||||
export interface SurplusPricePolicyInput {
|
||||
export interface SurplusMultiplierPolicyInput {
|
||||
upstreamInputMicroUsdPer1m: number;
|
||||
upstreamOutputMicroUsdPer1m: number;
|
||||
recoveryBps: number;
|
||||
@@ -12,23 +12,23 @@ export interface SurplusPricePolicyInput {
|
||||
competitors: SurplusCompetitor[];
|
||||
}
|
||||
|
||||
export interface SurplusPriceQuote {
|
||||
export interface SurplusMultiplierQuote {
|
||||
costMultiplierPpm: number;
|
||||
costMultiplier: number;
|
||||
inputMicroUsdPer1m: number;
|
||||
outputMicroUsdPer1m: number;
|
||||
inputUsdPer1m: number;
|
||||
outputUsdPer1m: number;
|
||||
floorInputMicroUsdPer1m: number;
|
||||
floorOutputMicroUsdPer1m: number;
|
||||
floorMultiplierPpm: number;
|
||||
competitorId: string | null;
|
||||
competitive: boolean;
|
||||
}
|
||||
|
||||
const BPS_SCALE = 10_000n;
|
||||
const PPM_SCALE = 1_000_000n;
|
||||
const MICRO_USD = 1_000_000;
|
||||
|
||||
export function quoteSurplusPrice(
|
||||
input: SurplusPricePolicyInput,
|
||||
): SurplusPriceQuote {
|
||||
export function quoteSurplusMultiplier(
|
||||
input: SurplusMultiplierPolicyInput,
|
||||
): SurplusMultiplierQuote {
|
||||
positiveSafeInteger(
|
||||
input.upstreamInputMicroUsdPer1m,
|
||||
"upstream input price",
|
||||
@@ -40,32 +40,50 @@ export function quoteSurplusPrice(
|
||||
boundedBps(input.recoveryBps, "recoveryBps", 1, 10_000);
|
||||
boundedBps(input.undercutBps, "undercutBps", 0, 9_999);
|
||||
|
||||
const floorInput = multiplyBpsCeil(
|
||||
input.upstreamInputMicroUsdPer1m,
|
||||
input.recoveryBps,
|
||||
);
|
||||
const floorOutput = multiplyBpsCeil(
|
||||
input.upstreamOutputMicroUsdPer1m,
|
||||
input.recoveryBps,
|
||||
);
|
||||
|
||||
const floorMultiplierPpm = input.recoveryBps * 100;
|
||||
const competitor = cheapestCompetitor(input.competitors);
|
||||
const candidateInput = competitor
|
||||
? undercut(competitor.inputMicroUsdPer1m, input.undercutBps)
|
||||
: floorInput;
|
||||
const candidateOutput = competitor
|
||||
? undercut(competitor.outputMicroUsdPer1m, input.undercutBps)
|
||||
: floorOutput;
|
||||
const quotedInput = Math.max(floorInput, candidateInput);
|
||||
const quotedOutput = Math.max(floorOutput, candidateOutput);
|
||||
const competitorMultiplierPpm = competitor
|
||||
? Math.min(
|
||||
ratioPpm(
|
||||
competitor.inputMicroUsdPer1m,
|
||||
input.upstreamInputMicroUsdPer1m,
|
||||
),
|
||||
ratioPpm(
|
||||
competitor.outputMicroUsdPer1m,
|
||||
input.upstreamOutputMicroUsdPer1m,
|
||||
),
|
||||
)
|
||||
: floorMultiplierPpm;
|
||||
let undercutMultiplierPpm = Number(
|
||||
BigInt(competitorMultiplierPpm) *
|
||||
BigInt(10_000 - input.undercutBps) /
|
||||
BPS_SCALE,
|
||||
);
|
||||
if (
|
||||
input.undercutBps > 0 &&
|
||||
undercutMultiplierPpm >= competitorMultiplierPpm
|
||||
) {
|
||||
undercutMultiplierPpm = Math.max(1, competitorMultiplierPpm - 1);
|
||||
}
|
||||
const quotedMultiplierPpm = Math.max(
|
||||
floorMultiplierPpm,
|
||||
undercutMultiplierPpm,
|
||||
);
|
||||
const quotedInput = applyMultiplier(
|
||||
input.upstreamInputMicroUsdPer1m,
|
||||
quotedMultiplierPpm,
|
||||
);
|
||||
const quotedOutput = applyMultiplier(
|
||||
input.upstreamOutputMicroUsdPer1m,
|
||||
quotedMultiplierPpm,
|
||||
);
|
||||
|
||||
return {
|
||||
costMultiplierPpm: quotedMultiplierPpm,
|
||||
costMultiplier: quotedMultiplierPpm / Number(PPM_SCALE),
|
||||
inputMicroUsdPer1m: quotedInput,
|
||||
outputMicroUsdPer1m: quotedOutput,
|
||||
inputUsdPer1m: quotedInput / MICRO_USD,
|
||||
outputUsdPer1m: quotedOutput / MICRO_USD,
|
||||
floorInputMicroUsdPer1m: floorInput,
|
||||
floorOutputMicroUsdPer1m: floorOutput,
|
||||
floorMultiplierPpm,
|
||||
competitorId: competitor?.id ?? null,
|
||||
competitive: competitor === null || (
|
||||
quotedInput < competitor.inputMicroUsdPer1m &&
|
||||
@@ -119,17 +137,12 @@ function compareCompetitors(
|
||||
);
|
||||
}
|
||||
|
||||
function undercut(value: number, bps: number): number {
|
||||
const discounted = Number(
|
||||
(BigInt(value) * BigInt(10_000 - bps)) / BPS_SCALE,
|
||||
);
|
||||
if (discounted >= value && value > 1) return value - 1;
|
||||
return Math.max(1, discounted);
|
||||
function ratioPpm(numerator: number, denominator: number): number {
|
||||
return Number(BigInt(numerator) * PPM_SCALE / BigInt(denominator));
|
||||
}
|
||||
|
||||
function multiplyBpsCeil(value: number, bps: number): number {
|
||||
const numerator = BigInt(value) * BigInt(bps);
|
||||
return Number((numerator + BPS_SCALE - 1n) / BPS_SCALE);
|
||||
function applyMultiplier(value: number, multiplierPpm: number): number {
|
||||
return Number(BigInt(value) * BigInt(multiplierPpm) / PPM_SCALE);
|
||||
}
|
||||
|
||||
function positiveSafeInteger(value: number, field: string): void {
|
||||
|
||||
@@ -4,10 +4,10 @@ import type {
|
||||
SurplusOrderBookOffer,
|
||||
} from "./client.js";
|
||||
import {
|
||||
quoteSurplusPrice,
|
||||
quoteSurplusMultiplier,
|
||||
usdPer1mToMicroUsd,
|
||||
type SurplusCompetitor,
|
||||
type SurplusPriceQuote,
|
||||
type SurplusMultiplierQuote,
|
||||
} from "./policy.js";
|
||||
|
||||
export interface SurplusModelCandidate {
|
||||
@@ -17,7 +17,7 @@ export interface SurplusModelCandidate {
|
||||
upstreamOutputMicroUsdPer1m: number;
|
||||
requests24h: number;
|
||||
volume24h: number;
|
||||
summaryQuote: SurplusPriceQuote;
|
||||
summaryQuote: SurplusMultiplierQuote;
|
||||
}
|
||||
|
||||
export function selectSurplusModelCandidates(
|
||||
@@ -54,7 +54,7 @@ export function selectSurplusModelCandidates(
|
||||
model.outputUsdPer1m,
|
||||
`${model.model} discovery output price`,
|
||||
);
|
||||
const quote = quoteSurplusPrice({
|
||||
const quote = quoteSurplusMultiplier({
|
||||
upstreamInputMicroUsdPer1m: inputPrice,
|
||||
upstreamOutputMicroUsdPer1m: outputPrice,
|
||||
recoveryBps,
|
||||
|
||||
@@ -14,6 +14,7 @@ import { randomUUID } from "node:crypto";
|
||||
export interface SurplusOfferIntent {
|
||||
model: string;
|
||||
providerModelId: string;
|
||||
costMultiplierPpm: number;
|
||||
inputMicroUsdPer1m: number;
|
||||
outputMicroUsdPer1m: number;
|
||||
dailyCapUsd: number;
|
||||
@@ -266,6 +267,10 @@ function parseIntent(value: unknown): SurplusOfferIntent {
|
||||
intent.providerModelId,
|
||||
"Surplus state intent.providerModelId",
|
||||
),
|
||||
costMultiplierPpm: positiveInteger(
|
||||
intent.costMultiplierPpm,
|
||||
"Surplus state intent.costMultiplierPpm",
|
||||
),
|
||||
inputMicroUsdPer1m: positiveInteger(
|
||||
intent.inputMicroUsdPer1m,
|
||||
"Surplus state intent.inputMicroUsdPer1m",
|
||||
|
||||
@@ -222,7 +222,7 @@ describe("SurplusClient", () => {
|
||||
expect(String(fetchFn.mock.calls[1][0])).toContain("alpha%3Amodel");
|
||||
});
|
||||
|
||||
it("sends one absolute PATCH with plain USD prices and the cap", async () => {
|
||||
it("sends one absolute cost-multiplier PATCH with the cap", async () => {
|
||||
let request: RequestInit | undefined;
|
||||
const client = new SurplusClient({
|
||||
apiBaseUrl: "https://api.surplusintelligence.ai",
|
||||
@@ -235,17 +235,15 @@ describe("SurplusClient", () => {
|
||||
});
|
||||
|
||||
await client.updateOffer("offer/one", {
|
||||
inputUsdPer1m: 0.003972,
|
||||
outputUsdPer1m: 0.007944,
|
||||
costMultiplier: 0.04995,
|
||||
dailyCapUsd: 1,
|
||||
idempotencyKey: "mutation-1",
|
||||
});
|
||||
|
||||
expect(request?.method).toBe("PATCH");
|
||||
expect(JSON.parse(String(request?.body))).toEqual({
|
||||
pricing_mode: "per_token",
|
||||
price_input_per_1m: 0.003972,
|
||||
price_output_per_1m: 0.007944,
|
||||
pricing_mode: "cost_multiplier",
|
||||
cost_multiplier: 0.04995,
|
||||
cap_daily_usd: 1,
|
||||
});
|
||||
const headers = new Headers(request?.headers);
|
||||
|
||||
@@ -147,8 +147,9 @@ class FakeSurplusClient implements SurplusControllerClient {
|
||||
sellerBaseUrl: input.sellerBaseUrl,
|
||||
status: "active",
|
||||
capDailyUsd: input.dailyCapUsd,
|
||||
inputMicroUsdPer1m: Math.round(input.inputUsdPer1m * 1_000_000),
|
||||
outputMicroUsdPer1m: Math.round(input.outputUsdPer1m * 1_000_000),
|
||||
costMultiplierPpm: Math.round(input.costMultiplier * 1_000_000),
|
||||
inputMicroUsdPer1m: Math.floor(79_520 * input.costMultiplier),
|
||||
outputMicroUsdPer1m: Math.floor(159_040 * input.costMultiplier),
|
||||
};
|
||||
return this.offer.id;
|
||||
}
|
||||
@@ -157,8 +158,15 @@ class FakeSurplusClient implements SurplusControllerClient {
|
||||
this.externalCalls += 1;
|
||||
this.patchPayload = patch;
|
||||
if (!this.offer) throw new Error("missing fake offer");
|
||||
this.offer.inputMicroUsdPer1m = Math.round(patch.inputUsdPer1m * 1_000_000);
|
||||
this.offer.outputMicroUsdPer1m = Math.round(patch.outputUsdPer1m * 1_000_000);
|
||||
this.offer.costMultiplierPpm = Math.round(
|
||||
patch.costMultiplier * 1_000_000,
|
||||
);
|
||||
this.offer.inputMicroUsdPer1m = Math.floor(
|
||||
79_520 * patch.costMultiplier,
|
||||
);
|
||||
this.offer.outputMicroUsdPer1m = Math.floor(
|
||||
159_040 * patch.costMultiplier,
|
||||
);
|
||||
this.offer.capDailyUsd = patch.dailyCapUsd;
|
||||
}
|
||||
|
||||
@@ -223,8 +231,7 @@ describe("SurplusSellerController", () => {
|
||||
expect(client.createCalls).toBe(1);
|
||||
expect(client.createdPayload).toMatchObject({
|
||||
model: "beta-model",
|
||||
inputUsdPer1m: 0.003972,
|
||||
outputUsdPer1m: 0.007944,
|
||||
costMultiplier: 0.04995,
|
||||
dailyCapUsd: 1,
|
||||
payoutAddress: SELLER_WALLET,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
quoteSurplusPrice,
|
||||
quoteSurplusMultiplier,
|
||||
} from "../../supplier/src/surplus/policy.js";
|
||||
import {
|
||||
eligibleCompetitors,
|
||||
@@ -41,8 +41,8 @@ function market(model: string, requests24h: number): SurplusMarketSummary {
|
||||
}
|
||||
|
||||
describe("Surplus bounded price policy", () => {
|
||||
it("uses exact recovery floors and undercuts in integer micro-USD", () => {
|
||||
const quote = quoteSurplusPrice({
|
||||
it("uses an exact recovery multiplier and undercuts in integer micro-USD", () => {
|
||||
const quote = quoteSurplusMultiplier({
|
||||
upstreamInputMicroUsdPer1m: 79_520,
|
||||
upstreamOutputMicroUsdPer1m: 159_040,
|
||||
recoveryBps: 490,
|
||||
@@ -54,17 +54,16 @@ describe("Surplus bounded price policy", () => {
|
||||
}],
|
||||
});
|
||||
|
||||
expect(quote.floorInputMicroUsdPer1m).toBe(3_897);
|
||||
expect(quote.floorOutputMicroUsdPer1m).toBe(7_793);
|
||||
expect(quote.floorMultiplierPpm).toBe(49_000);
|
||||
expect(quote.costMultiplierPpm).toBe(49_950);
|
||||
expect(quote.costMultiplier).toBe(0.04995);
|
||||
expect(quote.inputMicroUsdPer1m).toBe(3_972);
|
||||
expect(quote.outputMicroUsdPer1m).toBe(7_944);
|
||||
expect(quote.inputUsdPer1m).toBe(0.003972);
|
||||
expect(quote.outputUsdPer1m).toBe(0.007944);
|
||||
expect(quote.competitive).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps the recovery floor when the cheapest offer is too low", () => {
|
||||
const quote = quoteSurplusPrice({
|
||||
const quote = quoteSurplusMultiplier({
|
||||
upstreamInputMicroUsdPer1m: 79_520,
|
||||
upstreamOutputMicroUsdPer1m: 159_040,
|
||||
recoveryBps: 490,
|
||||
@@ -76,8 +75,8 @@ describe("Surplus bounded price policy", () => {
|
||||
}],
|
||||
});
|
||||
|
||||
expect(quote.inputMicroUsdPer1m).toBe(3_897);
|
||||
expect(quote.outputMicroUsdPer1m).toBe(7_793);
|
||||
expect(quote.floorMultiplierPpm).toBe(49_000);
|
||||
expect(quote.costMultiplierPpm).toBe(49_000);
|
||||
expect(quote.competitive).toBe(false);
|
||||
});
|
||||
|
||||
@@ -91,9 +90,9 @@ describe("Surplus bounded price policy", () => {
|
||||
const left = { id: "left", inputMicroUsdPer1m: 100, outputMicroUsdPer1m: 300 };
|
||||
const right = { id: "right", inputMicroUsdPer1m: 200, outputMicroUsdPer1m: 200 };
|
||||
|
||||
expect(quoteSurplusPrice({ ...input, competitors: [right, left] }).competitorId)
|
||||
expect(quoteSurplusMultiplier({ ...input, competitors: [right, left] }).competitorId)
|
||||
.toBe("left");
|
||||
expect(quoteSurplusPrice({ ...input, competitors: [left, right] }).competitorId)
|
||||
expect(quoteSurplusMultiplier({ ...input, competitors: [left, right] }).competitorId)
|
||||
.toBe("left");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user