Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 57 additions & 5 deletions packages/onboarding/test/complete-credential.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,17 @@ describe("completeCredentialSetup", () => {
cookies: [],
};
}
if (
method === "GET" &&
(path === `/api/tenants/${TENANT_ID}/catalog/offerings` ||
path.startsWith(`/api/tenants/${TENANT_ID}/catalog/offerings?`))
) {
return {
status: 200,
data: { data: [], nextCursor: null },
cookies: [],
};
}
throw new Error(`unexpected call: ${method} ${path}`);
};

Expand Down Expand Up @@ -1004,7 +1015,12 @@ describe("completeCredentialSetup", () => {
const deployments: { definitionAssetId: string; id: string }[] = [];
const catalogModels: Row[] = [];
const catalogProviders: Row[] = [];
const catalogOfferings: { modelId: string; providerId: string }[] = [];
const catalogOfferings: {
id: string;
modelId: string;
providerId: string;
priority: number;
}[] = [];
const providers: Row[] = [];
const credentials: Row[] = [];
let assetCreatePosts = 0;
Expand Down Expand Up @@ -1407,21 +1423,31 @@ describe("completeCredentialSetup", () => {
method === "POST" &&
path === `/api/tenants/${TENANT_ID}/catalog/offerings`
) {
const b = body as { modelId: string; providerId: string };
const b = body as {
modelId: string;
providerId: string;
priority: number;
};
const existing = catalogOfferings.find(
(o) => o.modelId === b.modelId && o.providerId === b.providerId,
);
if (existing) return { status: 409, data: {}, cookies: [] };
catalogOfferingCreatePosts += 1;
catalogOfferings.push({ modelId: b.modelId, providerId: b.providerId });
const id = `off_${catalogOfferings.length + 1}`;
catalogOfferings.push({
id,
modelId: b.modelId,
providerId: b.providerId,
priority: b.priority,
});
return {
status: 201,
data: {
id: `off_${catalogOfferings.length}`,
id,
tenantId: TENANT_ID,
modelId: b.modelId,
providerId: b.providerId,
priority: 0,
priority: b.priority,
deploymentTags: [],
capabilities: [],
quirks: null,
Expand All @@ -1432,6 +1458,32 @@ describe("completeCredentialSetup", () => {
cookies: [],
};
}
if (
method === "GET" &&
(path === `/api/tenants/${TENANT_ID}/catalog/offerings` ||
path.startsWith(`/api/tenants/${TENANT_ID}/catalog/offerings?`))
) {
return {
status: 200,
data: {
data: catalogOfferings.map((o) => ({
id: o.id,
tenantId: TENANT_ID,
modelId: o.modelId,
providerId: o.providerId,
priority: o.priority,
deploymentTags: [],
capabilities: [],
quirks: null,
disabled: false,
createdAt: TIMESTAMP,
updatedAt: TIMESTAMP,
})),
nextCursor: null,
},
cookies: [],
};
}
throw new Error(`unexpected call: ${method} ${path}`);
};

Expand Down
3 changes: 3 additions & 0 deletions packages/onboarding/test/huggingface-connect-routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,9 @@ function mockHub() {
201,
),
);
hub.get("/api/tenants/ten_1/catalog/offerings", (c) =>
c.json({ data: [], nextCursor: null }),
);
return hub;
}

Expand Down
3 changes: 3 additions & 0 deletions packages/onboarding/test/openrouter-connect-routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ function mockHub() {
201,
),
);
hub.get("/api/tenants/ten_1/catalog/offerings", (c) =>
c.json({ data: [], nextCursor: null }),
);
return hub;
}

Expand Down
67 changes: 56 additions & 11 deletions packages/seeding/src/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1343,7 +1343,52 @@ async function ensureCatalogOffering(
return;
}
if (created.status === 409) {
log("catalog offering already exists (skipped)");
let cursor: string | null = null;
let existing: typeof ModelOfferingResponse.infer | undefined;
do {
const listed = await api(
"GET",
`/api/tenants/${args.tenantId}/catalog/offerings${cursor === null ? "" : `?cursor=${encodeURIComponent(cursor)}`}`,
undefined,
cookies,
);
const page = parseAs(
paginatedSchema(ModelOfferingResponse),
listed.data,
"catalog offerings response",
);
existing = page.data.find(
(offering) =>
offering.modelId === args.modelId &&
offering.providerId === args.providerId,
);
cursor = page.nextCursor;
} while (existing === undefined && cursor !== null);
if (!existing) {
throw new HubApiError(
"catalog offering reported a conflict but is not listable on the bench",
"check the hub logs for the underlying failure, then re-run: workbench seed",
);
}
if (existing.priority === args.priority) {
log("catalog offering already exists (skipped)");
return;
}

const updated = await api(
"PATCH",
`/api/tenants/${args.tenantId}/catalog/offerings/${existing.id}`,
{ priority: args.priority },
cookies,
);
if (updated.status !== 200) {
throw new HubApiError(
`the hub rejected updating the catalog offering priority with status ${updated.status}: ${JSON.stringify(updated.data)}`,
"check the hub logs for the underlying failure, then re-run: workbench seed",
);
}
parseAs(ModelOfferingResponse, updated.data, "catalog offering response");
log("updated catalog offering priority");
return;
}
throw new HubApiError(
Expand Down Expand Up @@ -1570,14 +1615,14 @@ export async function seedCatalog(
},
log,
);
// Priority = the provider's CATALOG_SEEDS declaration index (anthropic
// first), so when several connected providers serve the same model the
// fallback order is deterministic instead of an all-zeroes tie broken
// by insertion accident.
const offeringPriority = Math.max(
0,
Object.keys(CATALOG_SEEDS).indexOf(provider),
);
// Flatten the curated provider/model declaration order into one priority
// sequence. Provider order still controls cross-provider fallback, while
// model order makes each provider's declared default the first choice.
let offeringPriorityOffset = 0;
for (const [seedProvider, providerSeed] of Object.entries(CATALOG_SEEDS)) {
if (seedProvider === provider) break;
offeringPriorityOffset += providerSeed.models.length;
}
// What each deployment can do, resolved from the pinned catalog's probe
// results. Until this, every seeded offering stored an empty capability
// list, so no capability filter — this repo's concept resolution or the
Expand All @@ -1589,7 +1634,7 @@ export async function seedCatalog(
canonicalName: string;
capabilities: readonly string[];
}[] = [];
for (const model of seededModels) {
for (const [modelIndex, model] of seededModels.entries()) {
// Ollama's dynamic entries already carry their own live-probed
// capabilities (`fetchOllamaModelCatalog`, CL-6366) — narrowed against
// the real `Capability` enum here, the trust boundary, rather than
Expand Down Expand Up @@ -1631,7 +1676,7 @@ export async function seedCatalog(
tenantId,
modelId: model.id,
providerId: catalogProviderId,
priority: offeringPriority,
priority: offeringPriorityOffset + modelIndex,
capabilities,
...(quirks !== undefined ? { quirks } : {}),
},
Expand Down
Loading
Loading