feat: prefer reseller for matching gateway requests
This commit is contained in:
@@ -34,6 +34,8 @@ services:
|
||||
# Accept/Reclaim/Submit. Same values the buyer/suppliers use on mainnet.
|
||||
ESCROW_REF_UTXO: c8d84c6d67ec67a1efe5e9c6c06d53020e05d1bb96d1c55ecb1eb7d5010c4d54#0
|
||||
ADVERT_REF_UTXO: c8d84c6d67ec67a1efe5e9c6c06d53020e05d1bb96d1c55ecb1eb7d5010c4d54#1
|
||||
# Prefer the balance-aware reseller for matching models; fall back when unavailable.
|
||||
GATEWAY_PREFERRED_SUPPLIER_PKH: c447d050e23bc3c38ff8267047c6f8c978970e7176e2af48f316a7fb
|
||||
# Browser Origins allowed to call the gateway cross-origin. The buyer
|
||||
# frontend's "API Keys" page (served from marketplace.vector…) POSTs to
|
||||
# /signup here. Comma-separated; empty disables CORS entirely.
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
const HEX64_RE = /^[0-9a-fA-F]{64}$/;
|
||||
const POS_INT_RE = /^[1-9]\d*$/;
|
||||
const PKH_RE = /^[0-9a-fA-F]{56}$/;
|
||||
|
||||
export interface GatewayConfig {
|
||||
masterKeyHex: string;
|
||||
@@ -56,6 +57,8 @@ export interface GatewayConfig {
|
||||
/** Capability id the /v1/ocr/extract route escrows against. Model-scoped
|
||||
* per the marketplace convention (one model, one id). Env: OCR_CAPABILITY_ID. */
|
||||
ocrCapabilityId: string;
|
||||
/** Seller preferred for matching one-shot models. Other suppliers remain fallback. */
|
||||
preferredSupplierPkh?: string;
|
||||
}
|
||||
|
||||
function requireField(env: Record<string, string | undefined>, name: string): string {
|
||||
@@ -115,6 +118,17 @@ export function loadConfig(env: Record<string, string | undefined>): GatewayConf
|
||||
}
|
||||
networkId = networkIdStr === "1" ? 1 : 0;
|
||||
}
|
||||
const preferredSupplierPkhRaw =
|
||||
(env.GATEWAY_PREFERRED_SUPPLIER_PKH ?? "").trim();
|
||||
if (
|
||||
preferredSupplierPkhRaw !== "" &&
|
||||
!PKH_RE.test(preferredSupplierPkhRaw)
|
||||
) {
|
||||
throw new Error(
|
||||
"loadConfig: GATEWAY_PREFERRED_SUPPLIER_PKH must be a 28-byte hex payment-key hash",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
return {
|
||||
masterKeyHex,
|
||||
@@ -142,6 +156,9 @@ export function loadConfig(env: Record<string, string | undefined>): GatewayConf
|
||||
walletHealthIntervalMs: posInt(env, "WALLET_HEALTH_INTERVAL_MS", 10 * 60 * 1000),
|
||||
sdkRegistryMax: posInt(env, "SDK_REGISTRY_MAX", 500),
|
||||
ocrCapabilityId: env.OCR_CAPABILITY_ID ?? "ocr.page.extract.chandra-ocr-2.v1",
|
||||
preferredSupplierPkh: preferredSupplierPkhRaw === ""
|
||||
? undefined
|
||||
: preferredSupplierPkhRaw.toLowerCase(),
|
||||
corsOrigins: (env.GATEWAY_CORS_ORIGINS ?? "")
|
||||
.split(",")
|
||||
.map((o) => o.trim().replace(/\/+$/, ""))
|
||||
|
||||
@@ -97,6 +97,7 @@ async function runOneShot(
|
||||
model: parsed.model,
|
||||
capabilityId: CAPABILITY,
|
||||
supplierPkh: parsed.supplierPkh,
|
||||
preferredSupplierPkh: config.preferredSupplierPkh,
|
||||
fetchFn,
|
||||
});
|
||||
if (candidates.length === 0) {
|
||||
|
||||
@@ -60,6 +60,8 @@ export interface SelectSupplierOpts {
|
||||
capabilityId: string;
|
||||
/** Optional exact seller identity pin for deterministic agent routing. */
|
||||
supplierPkh?: string;
|
||||
/** Preferred seller ordered first when it is eligible. */
|
||||
preferredSupplierPkh?: string;
|
||||
fetchFn?: typeof globalThis.fetch;
|
||||
/** supplier_pkhs eligible regardless of polled status — used after the demo
|
||||
* executor frees a supplier the indexer poller (~20 s cadence) still reports
|
||||
@@ -111,8 +113,16 @@ export async function selectCandidates(opts: SelectSupplierOpts): Promise<Suppli
|
||||
status: raw.status,
|
||||
});
|
||||
}
|
||||
// Prefer known-free over unknown so we route to confirmed-available first.
|
||||
candidates.sort((a, b) => (a.status === "free" ? 0 : 1) - (b.status === "free" ? 0 : 1));
|
||||
// Prefer the configured seller, then known-free over unknown.
|
||||
candidates.sort((left, right) => {
|
||||
const leftRank =
|
||||
(left.supplierPkh === opts.preferredSupplierPkh ? 0 : 2) +
|
||||
(left.status === "free" ? 0 : 1);
|
||||
const rightRank =
|
||||
(right.supplierPkh === opts.preferredSupplierPkh ? 0 : 2) +
|
||||
(right.status === "free" ? 0 : 1);
|
||||
return leftRank - rightRank;
|
||||
});
|
||||
return candidates;
|
||||
}
|
||||
|
||||
|
||||
@@ -81,6 +81,17 @@ describe("routing/selectSupplier", () => {
|
||||
expect(got.map((candidate) => candidate.supplierPkh)).toEqual(["s2"]);
|
||||
});
|
||||
|
||||
it("orders the preferred supplier first and keeps fallback candidates", async () => {
|
||||
const got = await selectCandidates({
|
||||
indexerUrl: "http://ix",
|
||||
model: "m",
|
||||
capabilityId: "llm.text.generate.v1",
|
||||
preferredSupplierPkh: "s2",
|
||||
fetchFn: jsonResponse(rows),
|
||||
});
|
||||
expect(got.map((candidate) => candidate.supplierPkh)).toEqual(["s2", "s1"]);
|
||||
});
|
||||
|
||||
it("routes chat.v1 separately", async () => {
|
||||
const got = await selectCandidates({
|
||||
indexerUrl: "http://ix",
|
||||
|
||||
Reference in New Issue
Block a user