pricing: budget maxPayment generously to avoid PaymentTooHigh reverts

The previous formula computed maxPayment from the estimated input length
plus offering.maxContextTokens — but the on-chain offering for some
provider models has maxContextTokens=0, so output cost was 0 and the
posted maxPayment only covered the (under-estimated) input side. The
provider's claimJob then reverted with PaymentTooHigh, leaving the
gateway hung waiting for a PSS job_deliver that never came.

Match the t4t/container homelab gateway: assume both prompt and
completion can be (max_tokens + 1M-token headroom), budget at the full
split rate, let the JobEscrow contract refund the unused portion. The
provider's actual claim is always smaller, so PaymentTooHigh can't fire.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Felix FaerberandClaude Opus 4.7 committed 2026-05-27 17:29:50 +03:00
1 parent 3618f5baf6
commit 9503b19378
1 file changed
+18 -16
+18 -16
View File
@@ -4,6 +4,19 @@ import { env } from '../env';
const BPS_DIVISOR = 10_000n; const BPS_DIVISOR = 10_000n;
const TOKENS_PER_MILLION = 1_000_000n; const TOKENS_PER_MILLION = 1_000_000n;
// Conservative ceiling matches the t4t/container homelab gateway. We assume
// prompt and completion could each be `max_tokens + headroom` and budget both
// sides at the model's full split rate. The provider always claims the actual
// (smaller) amount; the JobEscrow contract refunds the remainder to the client.
//
// Why so generous: providers may apply chat templates that inflate the prompt
// token count well beyond the user-visible messages, and the on-chain
// `maxContextTokens` is sometimes unset (== 0) — in which case the previous
// per-actual-input formula posted a too-low maxPayment and the provider's
// claimJob reverted with PaymentTooHigh.
const DEFAULT_MAX_TOKENS = 1024n;
const HEADROOM_TOKENS = 1_000_000n;
export interface ChatRequest { export interface ChatRequest {
model: string; model: string;
max_tokens?: number; max_tokens?: number;
@@ -23,26 +36,15 @@ export function estimateMaxXbzz(
req: ChatRequest, req: ChatRequest,
offering: Pick<ModelOffering, 'inputPricePerMillionTokens' | 'outputPricePerMillionTokens' | 'maxContextTokens'>, offering: Pick<ModelOffering, 'inputPricePerMillionTokens' | 'outputPricePerMillionTokens' | 'maxContextTokens'>,
): { rawWei: bigint; withMarkupWei: bigint } { ): { rawWei: bigint; withMarkupWei: bigint } {
const maxInputTokens = estimateInputTokens(req); const maxTokens = BigInt(req.max_tokens ?? DEFAULT_MAX_TOKENS);
const maxOutputTokens = BigInt(req.max_tokens ?? Number(offering.maxContextTokens)); const budget = maxTokens + HEADROOM_TOKENS;
const inPay = BigInt(offering.inputPricePerMillionTokens) * budget;
const inputCost = (BigInt(maxInputTokens) * BigInt(offering.inputPricePerMillionTokens)) / TOKENS_PER_MILLION; const outPay = BigInt(offering.outputPricePerMillionTokens) * budget;
const outputCost = (maxOutputTokens * BigInt(offering.outputPricePerMillionTokens)) / TOKENS_PER_MILLION; const rawWei = (inPay + outPay) / TOKENS_PER_MILLION;
const rawWei = inputCost + outputCost;
const withMarkupWei = (rawWei * (BPS_DIVISOR + BigInt(env.MARKUP_BPS))) / BPS_DIVISOR; const withMarkupWei = (rawWei * (BPS_DIVISOR + BigInt(env.MARKUP_BPS))) / BPS_DIVISOR;
return { rawWei, withMarkupWei }; return { rawWei, withMarkupWei };
} }
/**
* Approximate input-token count from messages. Uses a 4-char-per-token
* heuristic — fine for an upper-bound estimate; the actual cost is recorded
* later from the JobClaimed event.
*/
function estimateInputTokens(req: ChatRequest): number {
const totalChars = (req.messages ?? []).reduce((acc, m) => acc + (m.content?.length ?? 0), 0);
return Math.ceil(totalChars / 4) + 16; // +16 for system / role overhead
}
/** Apply the same markup function used at reservation time to a settled cost. */ /** Apply the same markup function used at reservation time to a settled cost. */
export function applyMarkup(rawWei: bigint): bigint { export function applyMarkup(rawWei: bigint): bigint {
return (rawWei * (BPS_DIVISOR + BigInt(env.MARKUP_BPS))) / BPS_DIVISOR; return (rawWei * (BPS_DIVISOR + BigInt(env.MARKUP_BPS))) / BPS_DIVISOR;