f2ef403ee9
Asked how many capacity commitments were on the book, Piggy answered "3". Production holds 5. It had called the idle-capacity tool, which filters to blocks above an idle threshold, and read the length of that list as the size of the book. The system prompt already forbade this in terms — "never report a filtered count as a total; pig_get_idle_capacity returns the blocks with idle hours, not the book" — and the model did it anyway. That is the second time this argument has been lost in the prompt, so it is settled in the payload instead: a result that cannot describe its own scope will be misread eventually, however firmly the prompt objects. Every tool that returns a count or a collection now carries one shape: what it covers, how many matched, out of how many, under which filters, and whether the list was truncated. The denominators are read from the database rather than inferred. The pre-formatted headline states the scope too, since that is the sentence a small model quotes most readily — the idle tool now opens "3 of 5 live capacity commitments on the book", which is the sentence that makes the original mistake impossible to phrase. Two details worth keeping. Record reads enumerate rather than filter, so their scope states a boundary instead of a ratio: these are that record's own figures, never book-wide totals. And the workspace summary's idle threshold is deliberately recorded as 0, distinct from the idle tool's 0.25 — that mismatch is why three different idle figures appeared across the UI, and naming it in the data is how it stops being invisible. Verified against the live model: the failing question now answers 5, demand deals 13 and contracts 20 — each drawn from a payload whose filtered figure was smaller — while "which blocks are sitting idle" still names exactly the blocks that are. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1165 lines
43 KiB
TypeScript
1165 lines
43 KiB
TypeScript
/**
|
|
* The tools interactive chat gets.
|
|
*
|
|
* Two layers, and the distinction between them is the whole design.
|
|
*
|
|
* FOCUSED tools answer where the user already is: the record the panel was
|
|
* opened from, or the page it is docked on. They take no id, because the id is
|
|
* the context, and a tool that could pivot would let the model wander off the
|
|
* thing the user is looking at.
|
|
*
|
|
* LOOKUP tools are the opposite, and exist because the focused layer capped
|
|
* every conversation at one question. "Compare Halcyon and Northwind", "which
|
|
* customer has the nearest renewal", "what can we buy H200 for" are all
|
|
* questions about rows nobody handed Piggy, and until it could find one by name
|
|
* the only honest answer was that it could not look.
|
|
*
|
|
* Lookup tools are offered on every message, so each is a permanent tax on the
|
|
* prompt and one more thing a 30B model can choose wrongly. Four earned that —
|
|
* see the note above `createLookupPigTools` for what was declined and why.
|
|
*/
|
|
import {
|
|
PIGGY_RECORD_TYPES,
|
|
formatCents,
|
|
isPageContext,
|
|
type PiggyRecordType,
|
|
} from '@pig/core';
|
|
import {
|
|
accounts,
|
|
allocations,
|
|
capacityCommitments,
|
|
contacts,
|
|
contractObligations,
|
|
contracts,
|
|
demandDeals,
|
|
slaMetricTargets,
|
|
slaTerms,
|
|
supplyDeals,
|
|
type Contract,
|
|
type Database,
|
|
type InventoryListing,
|
|
} from '@pig/db';
|
|
import { CapacityService } from '@pig/api/src/services/capacity';
|
|
import { renewalAlarm } from '@pig/api/src/services/contracts';
|
|
import { and, asc, count, eq, gt, ilike, inArray, isNotNull, isNull } from 'drizzle-orm';
|
|
import { z } from 'zod';
|
|
import type { PiggyChatContext } from './chat';
|
|
import { createAccountLifecycleTool } from './lifecycle-tools';
|
|
import { atLeast, createPagePigTools, resultScope, type ResultScope } from './page-tools';
|
|
import { defineTool, type AgentTool } from './provider';
|
|
|
|
const noInput = z.object({}).strict();
|
|
|
|
/**
|
|
* The per-collection cap on a record read.
|
|
*
|
|
* Named rather than repeated as a literal because the scope below reports it:
|
|
* a related list that came back exactly full is a list that was probably cut,
|
|
* and a cut list the reader cannot see is how "this account has 100 deals"
|
|
* gets said about an account with three hundred.
|
|
*/
|
|
const RELATED_LIMIT = 100;
|
|
|
|
/**
|
|
* The scope of a record read.
|
|
*
|
|
* Unlike the page tools, a record read filters nothing — it enumerates what
|
|
* belongs to one row — so `matched` equals `total` and the sentence is not
|
|
* hedged. What it must still say is the boundary, because the failure here is
|
|
* the same shape as the /capacity one measured in production: asked how many
|
|
* deals are on the book while an account is in focus, a model with only this
|
|
* payload counts the four in front of it. `totalLabel` therefore names the
|
|
* record and says the figures stop there.
|
|
*/
|
|
function recordScope(subject: string, collections: Record<string, readonly unknown[]>): ResultScope {
|
|
const entries = Object.entries(collections);
|
|
const rows = entries.reduce((sum, [, list]) => sum + list.length, 0);
|
|
const capped = entries.some(([, list]) => list.length >= RELATED_LIMIT);
|
|
const breakdown = entries.map(([label, list]) => `${list.length} ${label}`).join(', ');
|
|
return resultScope({
|
|
covers: `belong to ${subject}`,
|
|
matched: rows,
|
|
total: rows,
|
|
totalLabel: `record(s) belonging to ${subject} and to no other — ${breakdown}; these are that record's own figures, never book-wide totals`,
|
|
listed: rows,
|
|
filters: capped ? { rowCapPerCollection: RELATED_LIMIT } : {},
|
|
truncated: capped,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Interactive chat gets one scoped read tool for where it is, plus the lookup
|
|
* layer, and no ambient access.
|
|
*
|
|
* A record context gets `pig_get_record`, which takes no id and so always reads
|
|
* the row the user opened. A page context gets the single tool that answers
|
|
* that page — and never `pig_get_record`, because there is no record to read
|
|
* and a tool that would throw is a wasted turn out of four.
|
|
*/
|
|
export function createInteractivePigTools(
|
|
db: Database,
|
|
context: PiggyChatContext | undefined,
|
|
): AgentTool[] {
|
|
return [...focusedPigTools(db, context), ...createLookupPigTools(db)];
|
|
}
|
|
|
|
function focusedPigTools(db: Database, context: PiggyChatContext | undefined): AgentTool[] {
|
|
// No context is the dashboard case by another name: the same bounded
|
|
// workspace overview, rather than a second definition that could drift.
|
|
if (!context) return createPagePigTools(db, '/');
|
|
if (isPageContext(context)) return createPagePigTools(db, context.route);
|
|
const focused = defineTool({
|
|
name: 'pig_get_record',
|
|
description:
|
|
'Read the PIG record currently in focus and its directly related commercial data. ' +
|
|
'This tool accepts no id; use pig_get_record_by_id to read a different record.',
|
|
inputSchema: noInput,
|
|
execute: async () => readFocusedRecord(db, context),
|
|
});
|
|
if (context.type === 'account') return [focused, createAccountLifecycleTool(db, context.id)];
|
|
return [focused];
|
|
}
|
|
|
|
type PiggyRecordContext = Exclude<PiggyChatContext, { type: 'page' }>;
|
|
|
|
/**
|
|
* One not-found message for both entry points.
|
|
*
|
|
* `readFocusedRecord` is now reached from a context the panel supplied AND from
|
|
* an id the model chose, so "the account in focus no longer exists" was wrong
|
|
* half the time — and wrong in the direction that makes a model retry rather
|
|
* than correct the id it invented.
|
|
*/
|
|
function missingRecord(type: PiggyRecordType, id: string): Error {
|
|
return new Error(`No ${type} record exists with id ${id}.`);
|
|
}
|
|
|
|
async function readFocusedRecord(db: Database, context: PiggyRecordContext): Promise<unknown> {
|
|
if (context.type === 'account') {
|
|
const [account] = await db.select().from(accounts).where(eq(accounts.id, context.id)).limit(1);
|
|
if (!account) throw missingRecord(context.type, context.id);
|
|
const [people, demand, supply, paperwork] = await Promise.all([
|
|
db.select().from(contacts).where(eq(contacts.accountId, context.id)).limit(RELATED_LIMIT),
|
|
db.select().from(demandDeals).where(eq(demandDeals.accountId, context.id)).limit(RELATED_LIMIT),
|
|
db.select().from(supplyDeals).where(eq(supplyDeals.accountId, context.id)).limit(RELATED_LIMIT),
|
|
db.select().from(contracts).where(eq(contracts.accountId, context.id)).limit(RELATED_LIMIT),
|
|
]);
|
|
return {
|
|
scope: recordScope(`the account ${account.name}`, {
|
|
'contact(s)': people,
|
|
'demand deal(s)': demand,
|
|
'supply deal(s)': supply,
|
|
'contract(s)': paperwork,
|
|
}),
|
|
account,
|
|
contacts: people,
|
|
demandDeals: demand,
|
|
supplyDeals: supply,
|
|
contracts: paperwork,
|
|
};
|
|
}
|
|
|
|
if (context.type === 'contact') {
|
|
const [contact] = await db.select().from(contacts).where(eq(contacts.id, context.id)).limit(1);
|
|
if (!contact) throw missingRecord(context.type, context.id);
|
|
const [account] = contact.accountId
|
|
? await db.select().from(accounts).where(eq(accounts.id, contact.accountId)).limit(1)
|
|
: [];
|
|
return {
|
|
scope: recordScope(`the contact ${contact.fullName}`, {
|
|
'account(s)': account ? [account] : [],
|
|
}),
|
|
contact,
|
|
account: account ?? null,
|
|
};
|
|
}
|
|
|
|
if (context.type === 'demand_deal') {
|
|
const [deal] = await db.select().from(demandDeals).where(eq(demandDeals.id, context.id)).limit(1);
|
|
if (!deal) throw missingRecord(context.type, context.id);
|
|
const [account] = await db.select().from(accounts).where(eq(accounts.id, deal.accountId)).limit(1);
|
|
const reservations = await db
|
|
.select()
|
|
.from(allocations)
|
|
.where(eq(allocations.demandDealId, deal.id))
|
|
.limit(RELATED_LIMIT);
|
|
return {
|
|
scope: recordScope(`the demand deal ${deal.name}`, {
|
|
'allocation(s)': reservations,
|
|
'account(s)': account ? [account] : [],
|
|
}),
|
|
deal,
|
|
account: account ?? null,
|
|
allocations: reservations,
|
|
};
|
|
}
|
|
|
|
if (context.type === 'supply_deal') {
|
|
const [deal] = await db.select().from(supplyDeals).where(eq(supplyDeals.id, context.id)).limit(1);
|
|
if (!deal) throw missingRecord(context.type, context.id);
|
|
const [account] = await db.select().from(accounts).where(eq(accounts.id, deal.accountId)).limit(1);
|
|
const commitments = await db
|
|
.select()
|
|
.from(capacityCommitments)
|
|
.where(eq(capacityCommitments.supplyDealId, deal.id))
|
|
.limit(RELATED_LIMIT);
|
|
return {
|
|
scope: recordScope(`the supply deal ${deal.name}`, {
|
|
'capacity commitment(s)': commitments,
|
|
'account(s)': account ? [account] : [],
|
|
}),
|
|
deal,
|
|
account: account ?? null,
|
|
commitments,
|
|
};
|
|
}
|
|
|
|
if (context.type === 'commitment') {
|
|
const [commitment] = await db
|
|
.select()
|
|
.from(capacityCommitments)
|
|
.where(eq(capacityCommitments.id, context.id))
|
|
.limit(1);
|
|
if (!commitment) throw missingRecord(context.type, context.id);
|
|
const reservations = await db
|
|
.select()
|
|
.from(allocations)
|
|
.where(eq(allocations.capacityCommitmentId, commitment.id))
|
|
.limit(RELATED_LIMIT);
|
|
return {
|
|
scope: recordScope(`the capacity commitment ${commitment.name}`, {
|
|
'allocation(s)': reservations,
|
|
}),
|
|
commitment,
|
|
allocations: reservations,
|
|
};
|
|
}
|
|
|
|
const [contract] = await db.select().from(contracts).where(eq(contracts.id, context.id)).limit(1);
|
|
if (!contract) throw missingRecord(context.type, context.id);
|
|
const [serviceLevels, obligations] = await Promise.all([
|
|
db.select().from(slaTerms).where(eq(slaTerms.contractId, contract.id)).limit(10),
|
|
db
|
|
.select()
|
|
.from(contractObligations)
|
|
.where(eq(contractObligations.contractId, contract.id))
|
|
.limit(RELATED_LIMIT),
|
|
]);
|
|
const metrics = serviceLevels[0]
|
|
? await db
|
|
.select()
|
|
.from(slaMetricTargets)
|
|
.where(eq(slaMetricTargets.slaTermId, serviceLevels[0].id))
|
|
.limit(RELATED_LIMIT)
|
|
: [];
|
|
return {
|
|
scope: recordScope(`the contract ${contract.title}`, {
|
|
'SLA term(s)': serviceLevels,
|
|
'SLA metric target(s)': metrics,
|
|
'obligation(s)': obligations,
|
|
}),
|
|
contract,
|
|
slaTerms: serviceLevels,
|
|
slaMetricTargets: metrics,
|
|
obligations,
|
|
};
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// The lookup layer
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** Rows any one search may carry back per table, before ranking. */
|
|
const SEARCH_PER_TYPE = 5;
|
|
|
|
/** Rows a search result may carry in total, after ranking. */
|
|
const SEARCH_RESULTS = 12;
|
|
|
|
/** The longest name fragment a model may send. Long enough for any real name. */
|
|
const SEARCH_QUERY_MAX = 64;
|
|
|
|
/** Rows a ranked list may carry. Everything above it is reported as a count. */
|
|
const EXEMPLARS = 8;
|
|
|
|
/** Bound on an internal scan. Wide enough for a real book, still finite. */
|
|
const SCAN_LIMIT = 200;
|
|
|
|
/**
|
|
* The tools that are not about where the user is standing.
|
|
*
|
|
* Four, chosen against a fixed budget: every entry here is in the prompt of
|
|
* every message and is another candidate for a small model to pick wrongly.
|
|
*
|
|
* `pig_search_records` and `pig_get_record_by_id` are the pair that lifts the
|
|
* one-question ceiling — find a row by name, then open it. `readFocusedRecord`
|
|
* does the opening, so a record fetched by id is shaped exactly like the record
|
|
* the panel was opened from and the model has one shape to learn, not two.
|
|
*
|
|
* `pig_list_renewals` exists because the renewal deadline is expiry minus
|
|
* notice days, which nothing in a record payload states outright: given the raw
|
|
* contract a model has to do date arithmetic it is bad at, and a notice window
|
|
* that quietly opened last week is the most expensive thing in this book to
|
|
* miss. The calendar tool does surface renewal notices, but only on /calendar,
|
|
* mixed into thirteen other kinds, and without the contract ids.
|
|
*
|
|
* `pig_list_inventory` reads what providers are currently offering. Nothing
|
|
* else can see that table at all, and "what would this cost us to buy today"
|
|
* is the supply half of every pricing conversation.
|
|
*
|
|
* Declined, deliberately:
|
|
*
|
|
* - A commitment-comparison tool. `pig_search_records` plus two
|
|
* `pig_get_record_by_id` calls already answer it inside the four-turn budget,
|
|
* and `pig_get_margin_summary` already ranks live blocks by margin. A fifth
|
|
* tool would be a fifth wrong choice for a question two calls cover.
|
|
* - Anything over `activities`. There are 185 of them, they are prose, and a
|
|
* bounded slice of somebody's notes is the fastest way to spend a 1024-token
|
|
* answer on transcription. The lifecycle tool already carries the one
|
|
* activity fact that changes a decision — when the account last moved.
|
|
* - Contacts in the search index. A person is not a commercial record, the
|
|
* account read already returns its contacts, and every extra searched table
|
|
* dilutes the twelve result slots the model actually reads.
|
|
*/
|
|
export function createLookupPigTools(db: Database): AgentTool[] {
|
|
// Two things about the optional parameters below are load-bearing and
|
|
// invisible in TypeScript, both found by printing what the model is actually
|
|
// sent (`zodToJsonSchema(..., { target: 'openAi' })`).
|
|
//
|
|
// They are `.nullish()`, not `.optional()`. The OpenAI target emits an
|
|
// optional field as required-and-nullable, so a model that follows the schema
|
|
// it was given sends `{"side": null}` — which `.optional()` rejects, turning a
|
|
// correct call into a failed tool result.
|
|
//
|
|
// And `.describe()` comes BEFORE `.nullish()`. Applied after, the description
|
|
// is attached to the wrapper and dropped from the emitted schema, so the
|
|
// sentence explaining the parameter never reaches the model at all.
|
|
return [
|
|
defineTool({
|
|
name: 'pig_search_records',
|
|
description:
|
|
'Find PIG records by name when they are not already in focus. Matches the name or title ' +
|
|
'of accounts, demand deals, supply deals, contracts and capacity commitments, ' +
|
|
'case-insensitively, on a fragment. Returns each match with its type and id, for ' +
|
|
'pig_get_record_by_id. Names only: this does not search notes, activities or people.',
|
|
inputSchema: z
|
|
.object({
|
|
query: z
|
|
.string()
|
|
.trim()
|
|
.min(2)
|
|
.max(SEARCH_QUERY_MAX)
|
|
.describe(
|
|
`Name fragment, 2 to ${SEARCH_QUERY_MAX} characters. Use the distinctive word, not a whole sentence: "Halcyon", not "the Halcyon Research account".`,
|
|
),
|
|
})
|
|
.strict(),
|
|
execute: async ({ query }) => searchRecords(db, query),
|
|
}),
|
|
defineTool({
|
|
name: 'pig_get_record_by_id',
|
|
description:
|
|
'Read one PIG record by type and id, with its directly related commercial data. Use it ' +
|
|
'to open a result from pig_search_records. Ids must come from a tool result; never ' +
|
|
'invent one.',
|
|
inputSchema: z
|
|
.object({
|
|
type: z
|
|
.enum(PIGGY_RECORD_TYPES)
|
|
.describe('Record type, exactly as pig_search_records reported it.'),
|
|
id: z.string().uuid().describe('Record id from a previous tool result.'),
|
|
})
|
|
.strict(),
|
|
execute: async ({ type, id }) => readFocusedRecord(db, { type, id }),
|
|
}),
|
|
defineTool({
|
|
name: 'pig_list_renewals',
|
|
description:
|
|
'List executed contracts that have not yet expired, ordered by the nearest deadline: ' +
|
|
'the renewal-notice date where the contract auto-renews, otherwise the expiry date. ' +
|
|
'renewalState is "due" when the notice window is already open, "scheduled" when it is ' +
|
|
'still ahead, "not_applicable" when the contract does not auto-renew.',
|
|
inputSchema: z
|
|
.object({
|
|
side: z
|
|
.enum(['demand', 'supply'])
|
|
.describe('demand for customer paper, supply for provider paper. null for both.')
|
|
.nullish(),
|
|
})
|
|
.strict(),
|
|
execute: async ({ side }) => listRenewals(db, side ?? undefined),
|
|
}),
|
|
defineTool({
|
|
name: 'pig_list_inventory',
|
|
description:
|
|
'List the GPU capacity third-party providers currently offer for purchase, cheapest ' +
|
|
'first: provider, region, interconnect, stock level and on-demand price per GPU-hour. ' +
|
|
'This is capacity on offer, not capacity PIG already owns — what PIG owns is a capacity ' +
|
|
'commitment.',
|
|
inputSchema: z
|
|
.object({
|
|
gpuType: z
|
|
.string()
|
|
.trim()
|
|
.min(1)
|
|
.max(24)
|
|
.describe('GPU model fragment, matched loosely: "H100" finds H100_80GB. null for any.')
|
|
.nullish(),
|
|
minGpuCount: z
|
|
.number()
|
|
.int()
|
|
.min(1)
|
|
.max(100_000)
|
|
.describe('Smallest acceptable GPU count per listing. null for any.')
|
|
.nullish(),
|
|
requiresFastInterconnect: z
|
|
.boolean()
|
|
.describe('True to keep only Infiniband, RoCE or NVLink — the training-grade fabrics.')
|
|
.nullish(),
|
|
})
|
|
.strict(),
|
|
execute: async (input) =>
|
|
listInventory(db, {
|
|
gpuType: input.gpuType ?? undefined,
|
|
minGpuCount: input.minGpuCount ?? undefined,
|
|
requiresFastInterconnect: input.requiresFastInterconnect ?? undefined,
|
|
}),
|
|
}),
|
|
];
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Search
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** A hit plus the fields ranking needs and the payload does not. */
|
|
interface RankedHit {
|
|
rank: number;
|
|
name: string;
|
|
hit: Record<string, unknown>;
|
|
}
|
|
|
|
/**
|
|
* `%` and `_` are LIKE wildcards and this string arrives from a model. Left
|
|
* unescaped, a query of `%` matches every row in every table and the model is
|
|
* handed the first five rows of each as though they were answers.
|
|
*/
|
|
export function likeFragment(query: string): string {
|
|
return `%${query.replace(/[\\%_]/g, (character) => `\\${character}`)}%`;
|
|
}
|
|
|
|
/**
|
|
* Exact name first, then prefix, then anything containing the fragment.
|
|
*
|
|
* "Meridian" matches two accounts in the demo book — a sovereign customer and a
|
|
* supply partner — so the tie-break is not academic: an unranked list buries the
|
|
* exact match the user named behind whichever row the planner returned first.
|
|
*/
|
|
function matchRank(name: string, query: string): number {
|
|
const lowered = name.toLowerCase();
|
|
const needle = query.toLowerCase();
|
|
if (lowered === needle) return 0;
|
|
if (lowered.startsWith(needle)) return 1;
|
|
return 2;
|
|
}
|
|
|
|
/**
|
|
* The tie-break when the match quality is identical.
|
|
*
|
|
* Searching "Halcyon" in the demo book matches one account and three contracts,
|
|
* none of them a prefix match, so without this the list opens with a data
|
|
* processing addendum and the account — the record that reaches the other three
|
|
* through `pig_get_record_by_id` — is third. The hub record goes first.
|
|
*/
|
|
const TYPE_PRIORITY: Record<string, number> = {
|
|
account: 0,
|
|
demand_deal: 1,
|
|
supply_deal: 2,
|
|
commitment: 3,
|
|
contract: 4,
|
|
};
|
|
|
|
async function accountNames(
|
|
db: Database,
|
|
ids: readonly string[],
|
|
): Promise<Map<string, string>> {
|
|
const unique = [...new Set(ids)];
|
|
if (unique.length === 0) return new Map();
|
|
const rows = await db
|
|
.select({ id: accounts.id, name: accounts.name })
|
|
.from(accounts)
|
|
.where(inArray(accounts.id, unique));
|
|
return new Map(rows.map((row) => [row.id, row.name]));
|
|
}
|
|
|
|
/**
|
|
* Five bounded name searches, ranked into one list.
|
|
*
|
|
* Each table is read one row past its budget so the result can say it was cut
|
|
* rather than let the model report "two contracts match" over a capped five.
|
|
* The counts are per type because "which Meridian?" is answered by the shape of
|
|
* the result set, not by the first row of it.
|
|
*/
|
|
async function searchRecords(db: Database, query: string): Promise<unknown> {
|
|
const fragment = likeFragment(query);
|
|
const take = SEARCH_PER_TYPE + 1;
|
|
|
|
const [
|
|
accountRows,
|
|
demandRows,
|
|
supplyRows,
|
|
contractRows,
|
|
commitmentRows,
|
|
accountsAll,
|
|
demandAll,
|
|
supplyAll,
|
|
contractsAll,
|
|
commitmentsAll,
|
|
] = await Promise.all([
|
|
db
|
|
.select({
|
|
id: accounts.id,
|
|
name: accounts.name,
|
|
side: accounts.side,
|
|
customerSegment: accounts.customerSegment,
|
|
country: accounts.country,
|
|
})
|
|
.from(accounts)
|
|
.where(and(isNull(accounts.archivedAt), ilike(accounts.name, fragment)))
|
|
.limit(take),
|
|
db
|
|
.select({
|
|
id: demandDeals.id,
|
|
name: demandDeals.name,
|
|
accountId: demandDeals.accountId,
|
|
stage: demandDeals.stage,
|
|
acvCents: demandDeals.acvCents,
|
|
tcvCents: demandDeals.tcvCents,
|
|
expectedCloseDate: demandDeals.expectedCloseDate,
|
|
})
|
|
.from(demandDeals)
|
|
.where(ilike(demandDeals.name, fragment))
|
|
.limit(take),
|
|
db
|
|
.select({
|
|
id: supplyDeals.id,
|
|
name: supplyDeals.name,
|
|
accountId: supplyDeals.accountId,
|
|
stage: supplyDeals.stage,
|
|
gpuType: supplyDeals.gpuType,
|
|
gpuCount: supplyDeals.gpuCount,
|
|
targetCostPerGpuHourCents: supplyDeals.targetCostPerGpuHourCents,
|
|
})
|
|
.from(supplyDeals)
|
|
.where(ilike(supplyDeals.name, fragment))
|
|
.limit(take),
|
|
db
|
|
.select({
|
|
id: contracts.id,
|
|
title: contracts.title,
|
|
accountId: contracts.accountId,
|
|
contractType: contracts.type,
|
|
status: contracts.status,
|
|
side: contracts.side,
|
|
expiresAt: contracts.expiresAt,
|
|
valueCents: contracts.valueCents,
|
|
})
|
|
.from(contracts)
|
|
.where(ilike(contracts.title, fragment))
|
|
.limit(take),
|
|
db
|
|
.select({
|
|
id: capacityCommitments.id,
|
|
name: capacityCommitments.name,
|
|
accountId: capacityCommitments.accountId,
|
|
gpuType: capacityCommitments.gpuType,
|
|
gpuCount: capacityCommitments.gpuCount,
|
|
startsAt: capacityCommitments.startsAt,
|
|
endsAt: capacityCommitments.endsAt,
|
|
costPerGpuHourCents: capacityCommitments.costPerGpuHourCents,
|
|
})
|
|
.from(capacityCommitments)
|
|
.where(ilike(capacityCommitments.name, fragment))
|
|
.limit(take),
|
|
// The five denominators. A search that reports only its hits invites
|
|
// "there are 3 accounts" from a book of twenty-three, and these counts also
|
|
// make this tool able to answer how many of a thing exist at all.
|
|
db.select({ value: count() }).from(accounts).where(isNull(accounts.archivedAt)),
|
|
db.select({ value: count() }).from(demandDeals),
|
|
db.select({ value: count() }).from(supplyDeals),
|
|
db.select({ value: count() }).from(contracts),
|
|
db.select({ value: count() }).from(capacityCommitments),
|
|
]);
|
|
|
|
const names = await accountNames(db, [
|
|
...demandRows.map((row) => row.accountId),
|
|
...supplyRows.map((row) => row.accountId),
|
|
...contractRows.map((row) => row.accountId),
|
|
...commitmentRows.map((row) => row.accountId),
|
|
]);
|
|
|
|
const rows = (result: readonly { value: number }[]): number => result[0]?.value ?? 0;
|
|
|
|
return assembleSearchResult(query, {
|
|
accounts: accountRows,
|
|
demandDeals: demandRows,
|
|
supplyDeals: supplyRows,
|
|
contracts: contractRows,
|
|
commitments: commitmentRows,
|
|
accountNames: names,
|
|
totals: {
|
|
account: rows(accountsAll),
|
|
demand_deal: rows(demandAll),
|
|
supply_deal: rows(supplyAll),
|
|
contract: rows(contractsAll),
|
|
commitment: rows(commitmentsAll),
|
|
},
|
|
});
|
|
}
|
|
|
|
/** The five row sets a search reads, one row past each budget. */
|
|
export interface SearchRowSets {
|
|
accounts: readonly {
|
|
id: string;
|
|
name: string;
|
|
side: string;
|
|
customerSegment: string | null;
|
|
country: string | null;
|
|
}[];
|
|
demandDeals: readonly {
|
|
id: string;
|
|
name: string;
|
|
accountId: string;
|
|
stage: string;
|
|
acvCents: number | null;
|
|
tcvCents: number | null;
|
|
expectedCloseDate: Date | null;
|
|
}[];
|
|
supplyDeals: readonly {
|
|
id: string;
|
|
name: string;
|
|
accountId: string;
|
|
stage: string;
|
|
gpuType: string | null;
|
|
gpuCount: number | null;
|
|
targetCostPerGpuHourCents: number | null;
|
|
}[];
|
|
contracts: readonly {
|
|
id: string;
|
|
title: string;
|
|
accountId: string;
|
|
contractType: string;
|
|
status: string;
|
|
side: string;
|
|
expiresAt: Date | null;
|
|
valueCents: number | null;
|
|
}[];
|
|
commitments: readonly {
|
|
id: string;
|
|
name: string;
|
|
accountId: string;
|
|
gpuType: string;
|
|
gpuCount: number;
|
|
startsAt: Date;
|
|
endsAt: Date;
|
|
costPerGpuHourCents: number;
|
|
}[];
|
|
accountNames: ReadonlyMap<string, string>;
|
|
/**
|
|
* How many rows each searched table holds in total — the denominators the
|
|
* per-type match counts are drawn from. Keyed by the same names the results
|
|
* carry, so a model reading `counts.account: 1` beside `totals.account: 23`
|
|
* cannot mistake a name match for a census.
|
|
*/
|
|
totals: Record<SearchedRecordType, number>;
|
|
}
|
|
|
|
/** The five types a name search covers. People are deliberately not indexed. */
|
|
export type SearchedRecordType = Exclude<PiggyRecordType, 'contact'>;
|
|
|
|
/**
|
|
* Ranking, capping and counting, with no database in sight.
|
|
*
|
|
* Split out from the reads so the two things that can silently go wrong here —
|
|
* a count taken off a capped list, and an exact match sorted below a
|
|
* coincidental substring — are pinned by the unit suite. That suite runs in CI
|
|
* before the migration step, so anything it can reach must not need tables.
|
|
*/
|
|
export function assembleSearchResult(query: string, sets: SearchRowSets): unknown {
|
|
const names = sets.accountNames;
|
|
const cut = <Row>(rows: readonly Row[]): { rows: readonly Row[]; truncated: boolean } => ({
|
|
rows: rows.slice(0, SEARCH_PER_TYPE),
|
|
truncated: rows.length > SEARCH_PER_TYPE,
|
|
});
|
|
const accountsCut = cut(sets.accounts);
|
|
const demandCut = cut(sets.demandDeals);
|
|
const supplyCut = cut(sets.supplyDeals);
|
|
const contractsCut = cut(sets.contracts);
|
|
const commitmentsCut = cut(sets.commitments);
|
|
|
|
const ranked: RankedHit[] = [
|
|
...accountsCut.rows.map((row) => ({
|
|
rank: matchRank(row.name, query),
|
|
name: row.name,
|
|
hit: {
|
|
type: 'account',
|
|
id: row.id,
|
|
name: row.name,
|
|
side: row.side,
|
|
customerSegment: row.customerSegment,
|
|
country: row.country,
|
|
},
|
|
})),
|
|
...demandCut.rows.map((row) => ({
|
|
rank: matchRank(row.name, query),
|
|
name: row.name,
|
|
hit: {
|
|
type: 'demand_deal',
|
|
id: row.id,
|
|
name: row.name,
|
|
accountName: names.get(row.accountId) ?? null,
|
|
stage: row.stage,
|
|
acvCents: row.acvCents,
|
|
tcvCents: row.tcvCents,
|
|
expectedCloseDate: row.expectedCloseDate?.toISOString() ?? null,
|
|
},
|
|
})),
|
|
...supplyCut.rows.map((row) => ({
|
|
rank: matchRank(row.name, query),
|
|
name: row.name,
|
|
hit: {
|
|
type: 'supply_deal',
|
|
id: row.id,
|
|
name: row.name,
|
|
accountName: names.get(row.accountId) ?? null,
|
|
stage: row.stage,
|
|
gpuType: row.gpuType,
|
|
gpuCount: row.gpuCount,
|
|
targetCostPerGpuHourCents: row.targetCostPerGpuHourCents,
|
|
},
|
|
})),
|
|
...contractsCut.rows.map((row) => ({
|
|
rank: matchRank(row.title, query),
|
|
name: row.title,
|
|
hit: {
|
|
type: 'contract',
|
|
id: row.id,
|
|
name: row.title,
|
|
accountName: names.get(row.accountId) ?? null,
|
|
contractType: row.contractType,
|
|
status: row.status,
|
|
side: row.side,
|
|
expiresAt: row.expiresAt?.toISOString() ?? null,
|
|
valueCents: row.valueCents,
|
|
},
|
|
})),
|
|
...commitmentsCut.rows.map((row) => ({
|
|
rank: matchRank(row.name, query),
|
|
name: row.name,
|
|
hit: {
|
|
type: 'commitment',
|
|
id: row.id,
|
|
name: row.name,
|
|
accountName: names.get(row.accountId) ?? null,
|
|
gpuType: row.gpuType,
|
|
gpuCount: row.gpuCount,
|
|
startsAt: row.startsAt.toISOString(),
|
|
endsAt: row.endsAt.toISOString(),
|
|
costPerGpuHourCents: row.costPerGpuHourCents,
|
|
},
|
|
})),
|
|
].sort(
|
|
(a, b) =>
|
|
a.rank - b.rank ||
|
|
(TYPE_PRIORITY[String(a.hit.type)] ?? 9) - (TYPE_PRIORITY[String(b.hit.type)] ?? 9) ||
|
|
a.name.localeCompare(b.name),
|
|
);
|
|
|
|
const counts = {
|
|
account: accountsCut.rows.length,
|
|
demand_deal: demandCut.rows.length,
|
|
supply_deal: supplyCut.rows.length,
|
|
contract: contractsCut.rows.length,
|
|
commitment: commitmentsCut.rows.length,
|
|
};
|
|
const perTypeTruncated =
|
|
accountsCut.truncated ||
|
|
demandCut.truncated ||
|
|
supplyCut.truncated ||
|
|
contractsCut.truncated ||
|
|
commitmentsCut.truncated;
|
|
const results = ranked.slice(0, SEARCH_RESULTS);
|
|
const truncated = perTypeTruncated || ranked.length > results.length;
|
|
const searched = Object.values(sets.totals).reduce((sum, rows) => sum + rows, 0);
|
|
const breakdown = Object.entries(counts)
|
|
.filter(([, matches]) => matches > 0)
|
|
.map(([type, matches]) => `${matches} of ${sets.totals[type as SearchedRecordType]} ${type}(s)`)
|
|
.join(', ');
|
|
|
|
return {
|
|
headline:
|
|
results.length === 0
|
|
? `None of the ${searched} account(s), deal(s), contract(s) and capacity commitment(s) ` +
|
|
`on the book has a name containing "${query}".`
|
|
: `${truncated ? 'At least ' : ''}${ranked.length} of ${searched} searchable record(s) ` +
|
|
`match "${query}": ${breakdown}. Those are name matches, not totals; the counts they ` +
|
|
'were drawn from are beside them.',
|
|
scope: resultScope({
|
|
covers: `have a name containing "${query}"`,
|
|
matched: ranked.length,
|
|
total: searched,
|
|
totalLabel:
|
|
'record(s) searchable by name: accounts, demand deals, supply deals, contracts and capacity commitments',
|
|
listed: results.length,
|
|
filters: { query },
|
|
truncated,
|
|
}),
|
|
query,
|
|
truncated,
|
|
counts,
|
|
/** The denominator for each entry in `counts`, keyed identically. */
|
|
totals: sets.totals,
|
|
results: results.map((entry) => entry.hit),
|
|
};
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Renewals
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/**
|
|
* Executed paper that has not yet expired, nearest deadline first.
|
|
*
|
|
* The deadline is the notice date where one exists, because that — not the
|
|
* expiry — is the date after which the decision is no longer available. A
|
|
* contract whose notice window opened last week therefore sorts to the top with
|
|
* a negative `daysUntilDeadline` and `renewalState: "due"`, which is exactly the
|
|
* row somebody is looking for when they ask what they have missed.
|
|
*
|
|
* `renewalAlarm` is the API's own definition of that arithmetic and is called
|
|
* rather than repeated: two implementations of expiry-minus-notice would
|
|
* eventually disagree, and Piggy contradicting the contracts page is worse than
|
|
* Piggy having no renewals tool.
|
|
*/
|
|
async function listRenewals(db: Database, side: 'demand' | 'supply' | undefined): Promise<unknown> {
|
|
const now = new Date();
|
|
// The denominator is counted on the same side filter the list uses, so
|
|
// "4 of 20" and "4 of 11 on the demand side" are both answers to the
|
|
// question that was actually asked.
|
|
const [rows, all] = await Promise.all([
|
|
db
|
|
.select({ contract: contracts, accountName: accounts.name })
|
|
.from(contracts)
|
|
.leftJoin(accounts, eq(accounts.id, contracts.accountId))
|
|
.where(
|
|
and(
|
|
eq(contracts.status, 'executed'),
|
|
isNull(contracts.terminatedAt),
|
|
isNotNull(contracts.expiresAt),
|
|
gt(contracts.expiresAt, now),
|
|
side ? eq(contracts.side, side) : undefined,
|
|
),
|
|
)
|
|
.orderBy(asc(contracts.expiresAt))
|
|
.limit(SCAN_LIMIT + 1),
|
|
db
|
|
.select({ value: count() })
|
|
.from(contracts)
|
|
.where(side ? eq(contracts.side, side) : undefined),
|
|
]);
|
|
|
|
return assembleRenewals(rows.slice(0, SCAN_LIMIT), {
|
|
now,
|
|
side,
|
|
truncated: rows.length > SCAN_LIMIT,
|
|
totalContracts: all[0]?.value ?? 0,
|
|
});
|
|
}
|
|
|
|
/** Exactly the contract columns the renewal projection reads. */
|
|
export type RenewalContract = Pick<
|
|
Contract,
|
|
'id' | 'title' | 'side' | 'type' | 'isAutoRenew' | 'noticeDays' | 'expiresAt' | 'valueCents'
|
|
>;
|
|
|
|
export interface RenewalRow {
|
|
contract: RenewalContract;
|
|
accountName: string | null;
|
|
}
|
|
|
|
/**
|
|
* The deadline projection, with no database in sight.
|
|
*
|
|
* Separated from the read because the two things worth pinning here are the
|
|
* ordering — a lapsed notice must outrank a distant expiry — and the fact that
|
|
* `count` is the whole set while `renewals` is a capped slice of it.
|
|
*/
|
|
export function assembleRenewals(
|
|
rows: readonly RenewalRow[],
|
|
options: {
|
|
now: Date;
|
|
side?: 'demand' | 'supply';
|
|
truncated: boolean;
|
|
/** Every contract on this side, whatever its status. The denominator. */
|
|
totalContracts: number;
|
|
},
|
|
): unknown {
|
|
const { now, side, truncated, totalContracts } = options;
|
|
const renewals = rows
|
|
.flatMap(({ contract, accountName }) => {
|
|
// The query already requires an expiry; narrowing here rather than
|
|
// asserting keeps the sort key a date the compiler agrees exists.
|
|
const expiresAt = contract.expiresAt;
|
|
if (!expiresAt) return [];
|
|
const alarm = renewalAlarm(contract, now);
|
|
const deadlineAt = alarm.renewalNoticeAt ?? expiresAt;
|
|
return [{
|
|
id: contract.id,
|
|
title: contract.title,
|
|
accountName,
|
|
side: contract.side,
|
|
contractType: contract.type,
|
|
isAutoRenew: contract.isAutoRenew,
|
|
noticeDays: contract.noticeDays,
|
|
expiresAt: expiresAt.toISOString(),
|
|
renewalNoticeAt: alarm.renewalNoticeAt?.toISOString() ?? null,
|
|
renewalState: alarm.renewalState,
|
|
deadlineAt: deadlineAt.toISOString(),
|
|
deadlineKind: alarm.renewalNoticeAt ? ('renewal_notice' as const) : ('expiry' as const),
|
|
// Negative once the notice window has opened. Read alongside
|
|
// renewalState rather than on its own.
|
|
daysUntilDeadline: Math.ceil((deadlineAt.getTime() - now.getTime()) / 86_400_000),
|
|
valueCents: contract.valueCents,
|
|
}];
|
|
})
|
|
.sort((a, b) => a.deadlineAt.localeCompare(b.deadlineAt));
|
|
|
|
const noticeOpen = renewals.filter((row) => row.renewalState === 'due');
|
|
const nearest = renewals[0];
|
|
/**
|
|
* Master agreements routinely carry no `valueCents` — the money sits on the
|
|
* order forms beneath them. Summing nulls to zero and printing "$0.00 of
|
|
* stated contract value" reads as a worthless renewal rather than an
|
|
* unpriced one, so the clause is only stated where a figure exists.
|
|
*/
|
|
const statedValueCents = noticeOpen.reduce((sum, row) => sum + (row.valueCents ?? 0), 0);
|
|
const anyStatedValue = noticeOpen.some((row) => row.valueCents != null);
|
|
|
|
const sideLabel = side ? `${side}-side contract(s) on the book` : 'contract(s) on the book';
|
|
const listed = Math.min(renewals.length, EXEMPLARS);
|
|
|
|
return {
|
|
headline:
|
|
(nearest
|
|
? `${truncated ? 'At least ' : ''}${renewals.length} of ${totalContracts} ${sideLabel} ` +
|
|
'are executed and not yet expired' +
|
|
`${renewals.length > listed ? `; the nearest ${listed} are listed` : ''}. ` +
|
|
'Nearest deadline: the ' +
|
|
`${nearest.deadlineKind === 'renewal_notice' ? 'renewal notice' : 'expiry'} for ` +
|
|
`${nearest.title}${nearest.accountName ? ` (${nearest.accountName})` : ''} on ` +
|
|
`${nearest.deadlineAt.slice(0, 10)}` +
|
|
`${nearest.daysUntilDeadline < 0 ? ', which has already passed' : ''}.`
|
|
: `None of the ${totalContracts} ${sideLabel} is executed with an expiry date ahead of it.`) +
|
|
(noticeOpen.length > 0
|
|
? ` ${noticeOpen.length} of those ${renewals.length} have a notice window already open` +
|
|
(anyStatedValue
|
|
? `, covering ${formatCents(statedValueCents)} of stated contract value.`
|
|
: '; none of those contracts states a value of its own.')
|
|
: ''),
|
|
scope: resultScope({
|
|
covers: 'are executed, not terminated and not yet expired',
|
|
matched: renewals.length,
|
|
total: totalContracts,
|
|
totalLabel: sideLabel,
|
|
listed,
|
|
filters: { side: side ?? 'both', status: 'executed', expired: 'excluded' },
|
|
truncated,
|
|
}),
|
|
side: side ?? 'both',
|
|
truncated,
|
|
count: renewals.length,
|
|
totalContracts,
|
|
noticeWindowOpenCount: noticeOpen.length,
|
|
/** A filter over a filter, so it states its own denominator too. */
|
|
noticeWindowOpenScope: resultScope({
|
|
covers: 'have a renewal-notice window that is already open',
|
|
matched: noticeOpen.length,
|
|
total: renewals.length,
|
|
totalLabel: `executed, unexpired ${sideLabel}`,
|
|
listed: 0,
|
|
filters: { renewalState: 'due' },
|
|
truncated,
|
|
}),
|
|
renewals: renewals.slice(0, EXEMPLARS),
|
|
};
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Provider inventory
|
|
// ---------------------------------------------------------------------------
|
|
|
|
interface InventoryQuery {
|
|
gpuType?: string;
|
|
minGpuCount?: number;
|
|
requiresFastInterconnect?: boolean;
|
|
}
|
|
|
|
/**
|
|
* What providers are offering right now, cheapest first.
|
|
*
|
|
* `CapacityService.searchInventory` decides what counts as purchasable — it
|
|
* drops `Unavailable` stock — and is called rather than re-queried so that
|
|
* Piggy and the capacity page never disagree about what is on the market.
|
|
*
|
|
* Its `gpuType` filter is an exact match, which is wrong for this caller: a
|
|
* model asked about "H100" sends "H100", and the seeded SKU is `H100_80GB`, so
|
|
* an exact filter answers "nothing" to a question with eleven answers. The
|
|
* filter is therefore applied here as a case-insensitive fragment over a wide
|
|
* bounded read. The width never leaves this process; only EXEMPLARS rows do.
|
|
*/
|
|
async function listInventory(db: Database, query: InventoryQuery): Promise<unknown> {
|
|
const capacity = new CapacityService(db);
|
|
// The unfiltered read is the denominator, and it is taken through the same
|
|
// service rather than counted here: the service decides what "purchasable"
|
|
// means (it drops Unavailable stock), and a denominator computed from a
|
|
// second definition of that word would disagree with its own numerator.
|
|
const [listings, market] = await Promise.all([
|
|
capacity.searchInventory({
|
|
minGpuCount: query.minGpuCount,
|
|
requiresHighSpeedInterconnect: query.requiresFastInterconnect,
|
|
limit: SCAN_LIMIT,
|
|
}),
|
|
capacity.searchInventory({ limit: SCAN_LIMIT }),
|
|
]);
|
|
const providerNames = await accountNames(
|
|
db,
|
|
listings.flatMap((listing) => (listing.accountId ? [listing.accountId] : [])),
|
|
);
|
|
return assembleInventoryResult(query, listings, {
|
|
// The service caps at its own ceiling, so a full page is the only signal
|
|
// available that there was more behind it.
|
|
truncated: listings.length >= SCAN_LIMIT,
|
|
providerNames,
|
|
totalListings: market.length,
|
|
totalTruncated: market.length >= SCAN_LIMIT,
|
|
});
|
|
}
|
|
|
|
/** Exactly the listing columns the offer projection reads. */
|
|
export type InventoryOffer = Pick<
|
|
InventoryListing,
|
|
| 'accountId'
|
|
| 'providerSlug'
|
|
| 'gpuType'
|
|
| 'gpuCount'
|
|
| 'interconnectType'
|
|
| 'region'
|
|
| 'country'
|
|
| 'securityTier'
|
|
| 'stockStatus'
|
|
| 'isSpot'
|
|
| 'onDemandPriceCents'
|
|
| 'priceIsVariable'
|
|
| 'observedAt'
|
|
>;
|
|
|
|
/**
|
|
* Fragment matching, price ranking and the exemplar cap, with no database in
|
|
* sight — so the unit suite can pin the ordering that decides which offer a
|
|
* seller is shown first.
|
|
*/
|
|
export function assembleInventoryResult(
|
|
query: InventoryQuery,
|
|
listings: readonly InventoryOffer[],
|
|
options: {
|
|
truncated: boolean;
|
|
providerNames: ReadonlyMap<string, string>;
|
|
/** Purchasable listings on the market with no filter applied at all. */
|
|
totalListings: number;
|
|
totalTruncated: boolean;
|
|
},
|
|
): unknown {
|
|
const { truncated, providerNames: providers, totalListings, totalTruncated } = options;
|
|
const needle = query.gpuType?.toLowerCase();
|
|
const matched = needle
|
|
? listings.filter((listing) => listing.gpuType.toLowerCase().includes(needle))
|
|
: listings;
|
|
|
|
// Unpriced listings are real — some providers quote on request — but they
|
|
// cannot be ranked on price, so they sort last rather than as free capacity.
|
|
const ranked = [...matched].sort(
|
|
(a, b) => (a.onDemandPriceCents ?? Infinity) - (b.onDemandPriceCents ?? Infinity),
|
|
);
|
|
const cheapest = ranked.find((listing) => listing.onDemandPriceCents != null);
|
|
|
|
const filters = {
|
|
gpuType: query.gpuType ?? null,
|
|
minGpuCount: query.minGpuCount ?? null,
|
|
requiresFastInterconnect: query.requiresFastInterconnect ?? false,
|
|
};
|
|
const anyFilter = Object.values(filters).some((value) => value !== null && value !== false);
|
|
const listed = Math.min(ranked.length, EXEMPLARS);
|
|
const market = 'purchasable listing(s) on the market';
|
|
|
|
return {
|
|
headline:
|
|
ranked.length === 0
|
|
? `None of the ${atLeast(totalListings, totalTruncated)} ${market} matches that ` +
|
|
`request${query.gpuType ? ` for ${query.gpuType}` : ''}.`
|
|
: `${ranked.length} of ${atLeast(totalListings, totalTruncated)} ${market} match` +
|
|
`${anyFilter ? ' the filters given' : ' (no filter was applied)'}` +
|
|
`${query.gpuType ? `, including ${query.gpuType}` : ''}` +
|
|
(cheapest
|
|
? `; cheapest on-demand is ${formatCents(cheapest.onDemandPriceCents ?? 0)} per ` +
|
|
`GPU-hour for ${cheapest.gpuType}.`
|
|
: '; none of them carry a published on-demand price.') +
|
|
` ${listed} listed here.`,
|
|
scope: resultScope({
|
|
covers: anyFilter ? 'match the filters given' : 'are purchasable',
|
|
matched: ranked.length,
|
|
total: totalListings,
|
|
totalLabel: market,
|
|
listed,
|
|
// An unasked-for filter is not a filter: passing the three nulls through
|
|
// would have an unfiltered result describe itself as a slice.
|
|
filters: anyFilter ? filters : {},
|
|
truncated: truncated || totalTruncated,
|
|
}),
|
|
truncated,
|
|
count: ranked.length,
|
|
totalListings,
|
|
filters,
|
|
listings: ranked.slice(0, EXEMPLARS).map((listing) => shapeListing(listing, providers)),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* One listing, small enough to quote.
|
|
*
|
|
* The price column is stored as `onDemandPriceCents` but is normalised per GPU
|
|
* on the way in (`packages/prime/src/map.ts`), so it is renamed on the way out.
|
|
* A model that reads a bare "price" for an eight-GPU node as the node price
|
|
* quotes a rate eight times too low, and the suffix is what the units rule in
|
|
* the system prompt keys on.
|
|
*/
|
|
function shapeListing(
|
|
listing: InventoryOffer,
|
|
providers: ReadonlyMap<string, string>,
|
|
): Record<string, unknown> {
|
|
return {
|
|
providerName: listing.accountId ? providers.get(listing.accountId) ?? null : null,
|
|
providerSlug: listing.providerSlug,
|
|
gpuType: listing.gpuType,
|
|
gpuCount: listing.gpuCount,
|
|
interconnectType: listing.interconnectType,
|
|
region: listing.region,
|
|
country: listing.country,
|
|
securityTier: listing.securityTier,
|
|
stockStatus: listing.stockStatus,
|
|
isSpot: listing.isSpot,
|
|
onDemandPricePerGpuHourCents: listing.onDemandPriceCents,
|
|
priceIsVariable: listing.priceIsVariable,
|
|
// A listing nobody has confirmed for a week is a quote, not a price.
|
|
observedAt: listing.observedAt.toISOString(),
|
|
};
|
|
}
|