Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
73 changes: 51 additions & 22 deletions src/components/models/ModelBadges.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,47 +9,76 @@ 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;
}
Loading