feat: use provider model tags for resale
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
"networkId": 1,
|
||||
"ogmiosUrl": "https://ogmios.vector.mainnet.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",
|
||||
"maxInputTokens": 8192,
|
||||
"maxOutputTokens": 512,
|
||||
|
||||
@@ -62,6 +62,23 @@ export async function runResellerDemo(
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), options.timeoutMs);
|
||||
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(
|
||||
`${options.gatewayUrl}/openai/v1/chat/completions`,
|
||||
{
|
||||
@@ -76,6 +93,7 @@ export async function runResellerDemo(
|
||||
max_tokens: options.maxTokens,
|
||||
stream: false,
|
||||
public_preview: true,
|
||||
x_vector: { supplier_pkh: supplierPkh },
|
||||
}),
|
||||
signal: controller.signal,
|
||||
},
|
||||
@@ -101,6 +119,7 @@ export async function runResellerDemo(
|
||||
completion_id: body?.id ?? null,
|
||||
escrow_ref: escrowRef,
|
||||
model: body?.model ?? options.model,
|
||||
supplier_pkh: supplierPkh,
|
||||
upstream_cost_usd: job.upstream_cost_usd,
|
||||
ap3x_payout: job.ap3x_payout,
|
||||
dashboard_url: options.dashboardUrl,
|
||||
|
||||
@@ -96,6 +96,7 @@ async function runOneShot(
|
||||
indexerUrl: config.indexerUrl,
|
||||
model: parsed.model,
|
||||
capabilityId: CAPABILITY,
|
||||
supplierPkh: parsed.supplierPkh,
|
||||
fetchFn,
|
||||
});
|
||||
if (candidates.length === 0) {
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface ParsedChatRequest {
|
||||
tools?: unknown[];
|
||||
toolChoice?: unknown;
|
||||
publicPreview: boolean;
|
||||
supplierPkh?: string;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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;
|
||||
return { model, messages, maxTokens, stream, tools, toolChoice, publicPreview };
|
||||
return {
|
||||
model,
|
||||
messages,
|
||||
maxTokens,
|
||||
stream,
|
||||
tools,
|
||||
toolChoice,
|
||||
publicPreview,
|
||||
supplierPkh,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -58,6 +58,8 @@ export interface SelectSupplierOpts {
|
||||
* already pins the model). */
|
||||
model: string;
|
||||
capabilityId: string;
|
||||
/** Optional exact seller identity pin for deterministic agent routing. */
|
||||
supplierPkh?: 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
|
||||
@@ -86,6 +88,9 @@ export async function selectCandidates(opts: SelectSupplierOpts): Promise<Suppli
|
||||
for (const raw of body as SupplierView[]) {
|
||||
if (raw.capability_id !== opts.capabilityId) 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.status !== "free" && raw.status !== "unknown" && !opts.ignoreStatusFor?.has(raw.supplier_pkh)) {
|
||||
continue;
|
||||
|
||||
@@ -70,6 +70,17 @@ describe("routing/selectSupplier", () => {
|
||||
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 () => {
|
||||
const got = await selectCandidates({
|
||||
indexerUrl: "http://ix",
|
||||
@@ -118,6 +129,20 @@ describe("openai/validate", () => {
|
||||
expect(p.maxTokens).toBe(50);
|
||||
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", () => {
|
||||
expect(() => parseChatRequest({ model: "m", messages: [{ role: "user", content: "x" }], tools: [] })).toThrow(GatewayError);
|
||||
});
|
||||
|
||||
@@ -298,14 +298,6 @@ async function findOrPostAdvert(
|
||||
// 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(
|
||||
(candidate) => candidate.datum.supplier_pkh === expected.supplier_pkh,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user