Skip to content
Open
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
6 changes: 6 additions & 0 deletions bin/fetch-catalog-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ interface CatalogModel {
context_length: number | null;
max_output_tokens: number | null;
supports_async: boolean;
// Zero Data Retention. Optional because older catalog API responses
// omit the field entirely — declaring it here keeps `JSON.stringify`
// round-trips type-safe rather than relying on the cast hole at the
// JSON-write step.
zdr?: boolean;
zdr_comment?: string | null;
examples: Array<{
name: string;
description?: string;
Expand Down
69 changes: 47 additions & 22 deletions src/components/models/ModelBadges.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,47 +9,72 @@ const CATEGORY_BADGE: Record<string, string> = {

type ModelType = WorkersAIModelsSchema | ResolvedModel | ModelCardData;

type Badge = {
variant: string;
text: string;
/**
* Optional supplementary text rendered as the HTML `title` attribute on
* the badge span. Currently only the ZDR badge carries one (sourced from
* `zdr_comment` on the catalog row) — leaving every other badge without
* a tooltip.
*/
title?: string;
};

const ModelBadges = ({ model }: { model: ModelType }) => {
// Provider badge: every card surfaces where the model runs (Cloudflare's
// infrastructure vs proxied to a third-party). Defaults to
// "Cloudflare-hosted" for legacy models that pre-date the hosting field.
const isProxied = "hosting" in model && model.hosting === "proxied";
const providerBadge = {
const providerBadge: Badge = {
variant: "default",
text: isProxied ? "Third-party" : "Cloudflare-hosted",
};

const propertyBadges = model.properties.flatMap(({ property_id, value }) => {
// Boolean capability badges (data-driven)
if (property_id in CAPABILITY_PROPERTIES && value === "true") {
const def = CAPABILITY_PROPERTIES[property_id];
return {
variant: CATEGORY_BADGE[def.category] ?? "default",
text: def.label,
};
}

// Special case: deprecation badge (not a boolean capability)
if (property_id === "planned_deprecation_date") {
const timestamp = Math.floor(new Date(value as string).getTime());

if (Date.now() > timestamp) {
return { variant: "danger", text: "Deprecated" };
// Catalog rows carry `zdr_comment` on the resolved model rather than in the
// properties array (Property.value is string-only). Look it up once so the
// per-badge map below can attach it as a `title` tooltip on the ZDR span.
const zdrComment = "zdrComment" in model ? (model.zdrComment ?? null) : null;

const propertyBadges: Badge[] = model.properties.flatMap(
({ property_id, value }) => {
// Boolean capability badges (data-driven)
if (property_id in CAPABILITY_PROPERTIES && value === "true") {
const def = CAPABILITY_PROPERTIES[property_id];
const badge: Badge = {
variant: CATEGORY_BADGE[def.category] ?? "default",
text: def.label,
};
if (property_id === "zdr" && zdrComment) {
badge.title = zdrComment;
}
return badge;
}

return { variant: "danger", text: "Planned deprecation" };
}
// Special case: deprecation badge (not a boolean capability)
if (property_id === "planned_deprecation_date") {
const timestamp = Math.floor(new Date(value as string).getTime());

if (Date.now() > timestamp) {
return { variant: "danger", text: "Deprecated" };
}

return [];
});
return { variant: "danger", text: "Planned deprecation" };
}

return [];
},
);

const badges = [providerBadge, ...propertyBadges];

return (
<ul className="m-0 flex list-none flex-wrap items-center gap-1.5 p-0 text-xs [&>li]:m-0">
{badges.map((badge) => (
<li key={badge.text}>
<span className={`sl-badge ${badge.variant}`}>{badge.text}</span>
<span className={`sl-badge ${badge.variant}`} title={badge.title}>
{badge.text}
</span>
</li>
))}
</ul>
Expand Down
7 changes: 7 additions & 0 deletions src/schemas/catalog-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,13 @@ export const catalogModelsSchema = z.object({
max_output_tokens: z.number().nullable(),
supports_async: z.boolean(),

// Zero Data Retention. Optional because older catalog API responses omit
// the field entirely — the UI treats `undefined` the same as `false`
// (badge stays hidden). `zdr_comment` carries an optional supplementary
// note such as plan requirements when the upstream provider needs one.
zdr: z.boolean().optional(),
zdr_comment: z.string().nullable().optional(),

// Examples
examples: modelExampleSchema.array(),
default_example: defaultExampleSchema.nullable().optional(),
Expand Down
1 change: 1 addition & 0 deletions src/util/model-properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const CAPABILITY_PROPERTIES: Record<string, PropertyDef> = {
function_calling: { label: "Function calling", category: "model" },
reasoning: { label: "Reasoning", category: "model" },
vision: { label: "Vision", category: "model" },
zdr: { label: "Zero data retention", category: "model" },
// Platform properties
lora: { label: "LoRA", category: "platform" },
partner: { label: "Partner", category: "platform" },
Expand Down
11 changes: 11 additions & 0 deletions src/util/model-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ function catalogToResolved(model: CatalogModelsSchema): ResolvedModel {
properties.push({ property_id: "async_queue", value: "true" });
}

// Zero Data Retention. Optional on the catalog row — older API responses
// omit the field entirely, in which case the badge stays hidden. The
// supplementary `zdr_comment` flows through `zdrComment` on the resolved
// model rather than the properties array because `Property.value` is
// string-only and the badge needs the raw comment for its tooltip.
if (model.zdr === true) {
properties.push({ property_id: "zdr", value: "true" });
}

// Extract additional properties from metadata
const metadata = model.metadata || {};
if (metadata.lora) {
Expand Down Expand Up @@ -107,6 +116,7 @@ function catalogToResolved(model: CatalogModelsSchema): ResolvedModel {
properties,
dataSource: "catalog",
hosting: "proxied",
zdrComment: model.zdr_comment ?? null,
};
}

Expand Down Expand Up @@ -237,5 +247,6 @@ export function toModelCardData(model: ResolvedModel): ModelCardData {
properties: model.properties,
dataSource: model.dataSource,
hosting: model.hosting,
zdrComment: model.zdrComment,
};
}
14 changes: 14 additions & 0 deletions src/util/model-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ export interface ModelCardData {
}>;
dataSource: "catalog" | "legacy";
hosting: "proxied" | "hosted";
/**
* Optional supplementary note about ZDR support (plan requirements,
* conditions, etc.). Rendered as a `title` tooltip on the ZDR badge
* when present. Null/undefined leaves the badge without a tooltip.
*/
zdrComment?: string | null;
}

/**
Expand Down Expand Up @@ -117,4 +123,12 @@ export interface ResolvedModel {
// hosted models run on Cloudflare infrastructure.
// Currently inferred from data source; will eventually come from the Deus CMS.
hosting: "proxied" | "hosted";

/**
* Optional supplementary note about ZDR support (plan requirements,
* conditions, etc.). Surfaced from `zdr_comment` on the catalog row;
* legacy Workers AI models never set it. Rendered as a `title`
* tooltip on the ZDR badge in `ModelBadges`.
*/
zdrComment?: string | null;
}