feat: use provider model tags for resale

This commit is contained in:
david
2026-08-27 10:15:15 +02:00
parent 27d23dd7a4
commit 6b9f0f2284
7 changed files with 85 additions and 10 deletions

View File

@@ -2,7 +2,7 @@
"networkId": 1, "networkId": 1,
"ogmiosUrl": "https://ogmios.vector.mainnet.apexfusion.org", "ogmiosUrl": "https://ogmios.vector.mainnet.apexfusion.org",
"endpointUrl": "https://mp-suppliers-openrouter-reseller.vector.apexfusion.org", "endpointUrl": "https://mp-suppliers-openrouter-reseller.vector.apexfusion.org",
"marketplaceModel": "vector/deepseek-v4-flash-spot", "marketplaceModel": "deepseek/deepseek-v4-flash",
"openRouterModel": "deepseek/deepseek-v4-flash", "openRouterModel": "deepseek/deepseek-v4-flash",
"maxInputTokens": 8192, "maxInputTokens": 8192,
"maxOutputTokens": 512, "maxOutputTokens": 512,

View File

@@ -62,6 +62,23 @@ export async function runResellerDemo(
const controller = new AbortController(); const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), options.timeoutMs); const timer = setTimeout(() => controller.abort(), options.timeoutMs);
try { try {
const sellerResponse = await fetch(`${options.dashboardUrl}/api/status`, {
cache: "no-store",
signal: controller.signal,
});
const sellerBody = await sellerResponse.json().catch(() => null) as
| Record<string, unknown>
| null;
if (!sellerResponse.ok) {
throw new Error(
`seller status returned HTTP ${sellerResponse.status}`,
);
}
const supplierPkh = sellerBody?.supplier_pkh;
if (typeof supplierPkh !== "string" || !/^[0-9a-fA-F]{56}$/.test(supplierPkh)) {
throw new Error("seller status omitted a valid supplier_pkh");
}
const response = await fetch( const response = await fetch(
`${options.gatewayUrl}/openai/v1/chat/completions`, `${options.gatewayUrl}/openai/v1/chat/completions`,
{ {
@@ -76,6 +93,7 @@ export async function runResellerDemo(
max_tokens: options.maxTokens, max_tokens: options.maxTokens,
stream: false, stream: false,
public_preview: true, public_preview: true,
x_vector: { supplier_pkh: supplierPkh },
}), }),
signal: controller.signal, signal: controller.signal,
}, },
@@ -101,6 +119,7 @@ export async function runResellerDemo(
completion_id: body?.id ?? null, completion_id: body?.id ?? null,
escrow_ref: escrowRef, escrow_ref: escrowRef,
model: body?.model ?? options.model, model: body?.model ?? options.model,
supplier_pkh: supplierPkh,
upstream_cost_usd: job.upstream_cost_usd, upstream_cost_usd: job.upstream_cost_usd,
ap3x_payout: job.ap3x_payout, ap3x_payout: job.ap3x_payout,
dashboard_url: options.dashboardUrl, dashboard_url: options.dashboardUrl,

View File

@@ -96,6 +96,7 @@ async function runOneShot(
indexerUrl: config.indexerUrl, indexerUrl: config.indexerUrl,
model: parsed.model, model: parsed.model,
capabilityId: CAPABILITY, capabilityId: CAPABILITY,
supplierPkh: parsed.supplierPkh,
fetchFn, fetchFn,
}); });
if (candidates.length === 0) { if (candidates.length === 0) {

View File

@@ -13,6 +13,7 @@ export interface ParsedChatRequest {
tools?: unknown[]; tools?: unknown[];
toolChoice?: unknown; toolChoice?: unknown;
publicPreview: boolean; publicPreview: boolean;
supplierPkh?: string;
} }
const ROLES = new Set(["system", "user", "assistant"]); const ROLES = new Set(["system", "user", "assistant"]);
@@ -92,6 +93,38 @@ export function parseChatRequest(body: unknown, opts?: { allowTools?: boolean })
} }
const publicPreview = b.public_preview === true; const publicPreview = b.public_preview === true;
let supplierPkh: string | undefined;
if (b.x_vector !== undefined) {
if (
typeof b.x_vector !== "object" ||
b.x_vector === null ||
Array.isArray(b.x_vector)
) {
throw badRequest("invalid_x_vector", "`x_vector` must be an object");
}
const requested = (b.x_vector as Record<string, unknown>).supplier_pkh;
if (
requested !== undefined &&
(typeof requested !== "string" || !/^[0-9a-fA-F]{56}$/.test(requested))
) {
throw badRequest(
"invalid_supplier_pkh",
"`x_vector.supplier_pkh` must be a 28-byte hex payment-key hash",
);
}
if (typeof requested === "string") supplierPkh = requested.toLowerCase();
}
const stream = b.stream === true; const stream = b.stream === true;
return { model, messages, maxTokens, stream, tools, toolChoice, publicPreview }; return {
model,
messages,
maxTokens,
stream,
tools,
toolChoice,
publicPreview,
supplierPkh,
};
} }

View File

@@ -58,6 +58,8 @@ export interface SelectSupplierOpts {
* already pins the model). */ * already pins the model). */
model: string; model: string;
capabilityId: string; capabilityId: string;
/** Optional exact seller identity pin for deterministic agent routing. */
supplierPkh?: string;
fetchFn?: typeof globalThis.fetch; fetchFn?: typeof globalThis.fetch;
/** supplier_pkhs eligible regardless of polled status — used after the demo /** supplier_pkhs eligible regardless of polled status — used after the demo
* executor frees a supplier the indexer poller (~20 s cadence) still reports * executor frees a supplier the indexer poller (~20 s cadence) still reports
@@ -86,6 +88,9 @@ export async function selectCandidates(opts: SelectSupplierOpts): Promise<Suppli
for (const raw of body as SupplierView[]) { for (const raw of body as SupplierView[]) {
if (raw.capability_id !== opts.capabilityId) continue; if (raw.capability_id !== opts.capabilityId) continue;
if (opts.model !== "" && raw.model !== opts.model) continue; if (opts.model !== "" && raw.model !== opts.model) continue;
if (opts.supplierPkh !== undefined && raw.supplier_pkh !== opts.supplierPkh) {
continue;
}
if (raw.advert_status !== "Active") continue; if (raw.advert_status !== "Active") continue;
if (raw.status !== "free" && raw.status !== "unknown" && !opts.ignoreStatusFor?.has(raw.supplier_pkh)) { if (raw.status !== "free" && raw.status !== "unknown" && !opts.ignoreStatusFor?.has(raw.supplier_pkh)) {
continue; continue;

View File

@@ -70,6 +70,17 @@ describe("routing/selectSupplier", () => {
expect(got[0].priceLovelace).toBe(2n); expect(got[0].priceLovelace).toBe(2n);
}); });
it("pins routing to an exact supplier when requested", async () => {
const got = await selectCandidates({
indexerUrl: "http://ix",
model: "m",
capabilityId: "llm.text.generate.v1",
supplierPkh: "s2",
fetchFn: jsonResponse(rows),
});
expect(got.map((candidate) => candidate.supplierPkh)).toEqual(["s2"]);
});
it("routes chat.v1 separately", async () => { it("routes chat.v1 separately", async () => {
const got = await selectCandidates({ const got = await selectCandidates({
indexerUrl: "http://ix", indexerUrl: "http://ix",
@@ -118,6 +129,20 @@ describe("openai/validate", () => {
expect(p.maxTokens).toBe(50); expect(p.maxTokens).toBe(50);
expect(p.stream).toBe(false); expect(p.stream).toBe(false);
}); });
it("accepts and validates a Vector supplier pin", () => {
const supplierPkh = "a".repeat(56);
const parsed = parseChatRequest({
model: "m",
messages: [{ role: "user", content: "hi" }],
x_vector: { supplier_pkh: supplierPkh },
});
expect(parsed.supplierPkh).toBe(supplierPkh);
expect(() => parseChatRequest({
model: "m",
messages: [{ role: "user", content: "hi" }],
x_vector: { supplier_pkh: "bad" },
})).toThrow(GatewayError);
});
it("rejects tools/functions", () => { it("rejects tools/functions", () => {
expect(() => parseChatRequest({ model: "m", messages: [{ role: "user", content: "x" }], tools: [] })).toThrow(GatewayError); expect(() => parseChatRequest({ model: "m", messages: [{ role: "user", content: "x" }], tools: [] })).toThrow(GatewayError);
}); });

View File

@@ -298,14 +298,6 @@ async function findOrPostAdvert(
// Ignore unrelated or malformed script outputs. // Ignore unrelated or malformed script outputs.
} }
} }
const otherSeller = sameModel.find(
(candidate) => candidate.datum.supplier_pkh !== expected.supplier_pkh,
);
if (otherSeller) {
throw new Error(
`marketplace model ${expected.model} is already advertised by another supplier`,
);
}
const own = sameModel.filter( const own = sameModel.filter(
(candidate) => candidate.datum.supplier_pkh === expected.supplier_pkh, (candidate) => candidate.datum.supplier_pkh === expected.supplier_pkh,
); );