feat: use provider model tags for resale
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user