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>
52 lines
2.2 KiB
TypeScript
52 lines
2.2 KiB
TypeScript
import type { ModelOffering } from '../t4t/lib/types';
|
|
import { env } from '../env';
|
|
|
|
const BPS_DIVISOR = 10_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 {
|
|
model: string;
|
|
max_tokens?: number;
|
|
messages?: Array<{ content: string }>;
|
|
}
|
|
|
|
/**
|
|
* Rough upper-bound xBZZ cost for a single chat completion. Mirrors the
|
|
* `maxPayment` semantics on JobEscrow.postJob.
|
|
*
|
|
* Returns `{ raw, withMarkup }` so the caller can:
|
|
* - post the *raw* amount on-chain as `maxPayment` (markup is operator
|
|
* revenue, not paid to providers)
|
|
* - reserve the *with-markup* amount off-chain
|
|
*/
|
|
export function estimateMaxXbzz(
|
|
req: ChatRequest,
|
|
offering: Pick<ModelOffering, 'inputPricePerMillionTokens' | 'outputPricePerMillionTokens' | 'maxContextTokens'>,
|
|
): { rawWei: bigint; withMarkupWei: bigint } {
|
|
const maxTokens = BigInt(req.max_tokens ?? DEFAULT_MAX_TOKENS);
|
|
const budget = maxTokens + HEADROOM_TOKENS;
|
|
const inPay = BigInt(offering.inputPricePerMillionTokens) * budget;
|
|
const outPay = BigInt(offering.outputPricePerMillionTokens) * budget;
|
|
const rawWei = (inPay + outPay) / TOKENS_PER_MILLION;
|
|
const withMarkupWei = (rawWei * (BPS_DIVISOR + BigInt(env.MARKUP_BPS))) / BPS_DIVISOR;
|
|
return { rawWei, withMarkupWei };
|
|
}
|
|
|
|
/** Apply the same markup function used at reservation time to a settled cost. */
|
|
export function applyMarkup(rawWei: bigint): bigint {
|
|
return (rawWei * (BPS_DIVISOR + BigInt(env.MARKUP_BPS))) / BPS_DIVISOR;
|
|
}
|