feat(models,landing): add OpenRouter free models and a free-models article (#60)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Yaowei Zheng
2026-07-26 15:05:44 +08:00
committed by GitHub
parent 6b8b631e37
commit 14b5e6384f
21 changed files with 477 additions and 10 deletions
+14 -1
View File
@@ -61,7 +61,13 @@ import { GlyphIcon } from "../../components/ui/glyph-icon";
import { SkillIcon } from "../skills/skill-icon-view";
import { ZoomableImage } from "../../components/ui/image-zoom";
import { ProviderLogo } from "../../components/ui/provider-logo";
import { hasConfiguredKey, sameModelRef, visibleChatModels } from "../models/model-grouping";
import { Badge } from "../../components/ui/badge";
import {
hasConfiguredKey,
isFreeModel,
sameModelRef,
visibleChatModels,
} from "../models/model-grouping";
import { filterAgents, matchMention, splitLeadingMention } from "./agent-mentions";
import { matchSlash, removeSlashToken } from "./slash-token";
import { SELECTABLE_THINKING_LEVELS, thinkingLevelLabel } from "./thinking-level";
@@ -348,6 +354,13 @@ function ModelSelect({
>
<ProviderLogo provider={m.provider} className="h-4 w-4 shrink-0" />
<span className="min-w-0 flex-1 truncate">{modelLabel(m)}</span>
{/* Zero-cost rows (all three price buckets 0): same light-yellow "Free" badge as
the model library card, so free models stand out while picking. */}
{isFreeModel(m.pricing) && (
<span className="shrink-0">
<Badge tone="yellow">{S.models.freeBadge}</Badge>
</span>
)}
{/* Key-less rows (visible via show-all / selected / default / no-key-at-all) carry a
struck-through key icon (the "no key" text lives in the title/aria-label). */}
{!hasConfiguredKey(m) && (
@@ -114,6 +114,24 @@ export function hasConfiguredKey(m: ModelCredentialRowLike): boolean {
return !!m.credential?.apiKeyMasked;
}
/**
* Free model detection (drives the light-yellow "Free" badge on the model card and in the
* chat model picker): the entry carries explicit pricing and all three buckets are 0 — covers the
* catalog's :free variants and the openrouter/free router — while unpriced models (no pricing
* at all, costs merely unknown) stay unbadged. Accepts the DTO's numeric pricing buckets or
* the model page's string-typed edit fields ("" = unpriced).
*/
export function isFreeModel(
pricing:
| { cacheRead: number | string; cacheWrite: number | string; output: number | string }
| undefined,
): boolean {
if (!pricing) return false;
return [pricing.cacheRead, pricing.cacheWrite, pricing.output].every((b) =>
typeof b === "string" ? b.trim() !== "" && Number(b) === 0 : b === 0,
);
}
export interface VisibleChatModelsOptions {
/** true = list every model (the dropdown's expanded "show all" state). */
showAll: boolean;
@@ -70,7 +70,7 @@ import {
resolveModelEnv,
} from "@prismshadow/penguin-core/model-catalog";
import type { ModelProviderInfo } from "@prismshadow/penguin-core/model-catalog";
import { groupModelRows, sameModelRef, userProviderInfo } from "./model-grouping";
import { groupModelRows, isFreeModel, sameModelRef, userProviderInfo } from "./model-grouping";
import { draftKey, loadDraft, saveDraft } from "../chat/draft-cache";
import { syncRowsWithCatalog } from "./catalog-sync";
import { tpsTone, ttftTone } from "./speed-test";
@@ -962,6 +962,10 @@ function ModelCard({
<span className="flex flex-wrap items-center gap-1.5">
<span className="text-sm font-medium">{row.displayName ?? row.modelId}</span>
{isDefault && <Badge tone="brand">{S.models.default}</Badge>}
{/* Free rows (all three price buckets 0, e.g. :free variants / openrouter/free): a
light-yellow badge so zero-cost models stand out at a glance (informational, kept
distinct from the amber warning tone the proxy-vision badge uses). */}
{isFreeModel(row) && <Badge tone="yellow">{S.models.freeBadge}</Badge>}
{row.vision && <Badge tone="green">{S.models.visionBadge}</Badge>}
{isVisionModel && <Badge tone="amber">{S.models.visionModelBadge}</Badge>}
</span>