diff --git a/deploy/mainnet/openrouter-reseller.config.json b/deploy/mainnet/openrouter-reseller.config.json index 401a167..f95f56d 100644 --- a/deploy/mainnet/openrouter-reseller.config.json +++ b/deploy/mainnet/openrouter-reseller.config.json @@ -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, diff --git a/gateway/src/cli/reseller-demo.ts b/gateway/src/cli/reseller-demo.ts index 0d8dc2b..5044585 100644 --- a/gateway/src/cli/reseller-demo.ts +++ b/gateway/src/cli/reseller-demo.ts @@ -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 + | 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, diff --git a/gateway/src/openai/chatCompletions.ts b/gateway/src/openai/chatCompletions.ts index e6ae98b..7eb2306 100644 --- a/gateway/src/openai/chatCompletions.ts +++ b/gateway/src/openai/chatCompletions.ts @@ -96,6 +96,7 @@ async function runOneShot( indexerUrl: config.indexerUrl, model: parsed.model, capabilityId: CAPABILITY, + supplierPkh: parsed.supplierPkh, fetchFn, }); if (candidates.length === 0) { diff --git a/gateway/src/openai/validate.ts b/gateway/src/openai/validate.ts index be27fc8..338086b 100644 --- a/gateway/src/openai/validate.ts +++ b/gateway/src/openai/validate.ts @@ -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).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, + }; } diff --git a/gateway/src/routing/selectSupplier.ts b/gateway/src/routing/selectSupplier.ts index 39e2ab5..c1708b8 100644 --- a/gateway/src/routing/selectSupplier.ts +++ b/gateway/src/routing/selectSupplier.ts @@ -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 { 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); }); diff --git a/supplier/src/cli/setup-reseller.ts b/supplier/src/cli/setup-reseller.ts index b23048d..29892c6 100644 --- a/supplier/src/cli/setup-reseller.ts +++ b/supplier/src/cli/setup-reseller.ts @@ -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, );