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:
@@ -3,12 +3,16 @@
|
||||
*/
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export type BadgeTone = "gray" | "brand" | "green" | "amber" | "red";
|
||||
export type BadgeTone = "gray" | "brand" | "green" | "yellow" | "amber" | "red";
|
||||
|
||||
const toneClass: Record<BadgeTone, string> = {
|
||||
gray: "bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-300",
|
||||
brand: "bg-gray-200/80 text-gray-700 dark:bg-gray-700/60 dark:text-gray-200",
|
||||
green: "bg-emerald-50 text-emerald-700 dark:bg-emerald-950 dark:text-emerald-300",
|
||||
// Light-yellow: a neutral informational tag (the "Free" model badge). Kept on the yellow
|
||||
// palette so it stays visually distinct from the amber tone below, which existing badges
|
||||
// use with warning semantics (aborted stop_reason, the proxy-vision badge on the same card).
|
||||
yellow: "bg-yellow-50 text-yellow-700 dark:bg-yellow-950 dark:text-yellow-300",
|
||||
amber: "bg-amber-50 text-amber-700 dark:bg-amber-950 dark:text-amber-300",
|
||||
red: "bg-red-50 text-red-700 dark:bg-red-950 dark:text-red-300",
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -329,6 +329,7 @@ export const en: Strings = {
|
||||
vision: "Vision support",
|
||||
visionOffProxyHint: "Images are read via the vision proxy model",
|
||||
visionBadge: "Vision",
|
||||
freeBadge: "Free",
|
||||
visionModelBadge: "Proxy vision",
|
||||
setVisionModel: "Set as proxy vision model",
|
||||
visionModelHint: "Describes images via describe_image for models without vision",
|
||||
|
||||
@@ -308,6 +308,8 @@ export const zh = {
|
||||
/** Shown only while the vision switch is OFF: images are then read via the configured vision proxy model (describe_image). */
|
||||
visionOffProxyHint: "使用视觉代理模型读图",
|
||||
visionBadge: "视觉",
|
||||
/** Light-yellow badge on zero-cost models (all three price buckets 0, e.g. the :free variants and openrouter/free). */
|
||||
freeBadge: "免费",
|
||||
visionModelBadge: "视觉代理",
|
||||
setVisionModel: "设为视觉代理模型",
|
||||
visionModelHint: "供不支持图片的模型经 describe_image 代读图片",
|
||||
|
||||
Reference in New Issue
Block a user