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:
1 parent
6b8b631e37
commit
14b5e6384f
21 files changed
+477
-10
No files matched your search
@@ -0,0 +1,70 @@
|
||||
---
|
||||
title: Introducing the free models in PenguinHarness
|
||||
date: 2026-07-24
|
||||
category: news
|
||||
excerpt: The preset catalog now carries four zero-cost OpenRouter entries — Nemotron 3 Ultra (free), the new Ling 3.0 Flash (free) and Laguna M.1 (free), and the new Free Models Router. One OpenRouter API key and an agent is running, no balance required. Here is the lineup, how to switch it on, and an honest account of what the free tier does and does not buy you.
|
||||
---
|
||||
|
||||
Trying an agent harness should not start with a top-up. PenguinHarness ships free models in its preset catalog: rows priced at $0 per million tokens, wired up like every other preset — protocol, base URL, pricing and context window pre-filled — so the only thing between you and a running agent is an OpenRouter API key, which is itself free to create.
|
||||
|
||||
As of today the free lineup is four entries, all in the OpenRouter group:
|
||||
|
||||
| Provider group | Model id | Context | Price |
|
||||
| -------------- | ---------------------------------------- | -------------: | ----- |
|
||||
| OpenRouter | `nvidia/nemotron-3-ultra-550b-a55b:free` | 1,000,000 | $0 |
|
||||
| OpenRouter | `inclusionai/ling-3.0-flash:free` | 262,144 | $0 |
|
||||
| OpenRouter | `poolside/laguna-m.1:free` | 262,144 | $0 |
|
||||
| OpenRouter | `openrouter/free` | 128,000 (conservative) | $0 |
|
||||
|
||||

|
||||
|
||||
## Nemotron 3 Ultra (free)
|
||||
|
||||
The catalog's first free row, and still its largest: NVIDIA's open frontier-reasoning and orchestration model, a Mixture-of-Experts with 55B active parameters out of 550B total on a hybrid Transformer–Mamba architecture, with a 1M-token context window. If you want to see what the harness's planning-heavy loops look like on a big reasoning model — without paying big-reasoning-model prices — this is the row.
|
||||
|
||||
## New: Ling 3.0 Flash (free)
|
||||
|
||||
Released by inclusionAI on July 23, in the catalog the next day. Ling-3.0-flash is a 124B-parameter MoE that activates only ~5.1B parameters per token, and inclusionAI's stated design priorities are token efficiency and production-scale agentic inference, tool calling included. That reads like a description of what an agent harness does all day: dozens of short round trips, each carrying a tool schema and a growing transcript, where efficiency per step is the whole cost model. A sparse, tool-tuned model at $0 is a very good default for exactly that traffic. Context is 262K; text only.
|
||||
|
||||
## New: Laguna M.1 (free)
|
||||
|
||||
Alongside Ling comes the free tier of Poolside's flagship coding-agent model. Laguna M.1 is optimized for complex software-engineering work — agentic coding workflows with tool calling and reasoning, which is precisely the traffic a harness generates. Context is 262K; text only.
|
||||
|
||||
## New: Free Models Router
|
||||
|
||||
`openrouter/free` is not a model but OpenRouter's unified free-tier endpoint: each request is routed to a random free model currently available on OpenRouter, filtered so the target supports what the request actually needs — tool calling, structured outputs, and so on. Free models come and go upstream; the router keeps answering, and you never chase the current list yourself.
|
||||
|
||||
Two catalog decisions are worth knowing. The routed target's real context window varies per request, so the row records a deliberately conservative 128,000 rather than any single target's real figure — long Sessions compact early instead of growing toward a window the routed model may not have. And it is marked text-only on purpose: the router itself accepts images, but the model behind any given request may not, so PenguinHarness keeps images off this route and falls back to its usual text-only hand-off (file path plus `describe_image`).
|
||||
|
||||
## Switching it on
|
||||
|
||||
1. Create an API key at [openrouter.ai](https://openrouter.ai/) — the free tier needs no payment method.
|
||||
2. A new Project carries the presets already: open the **Models** page and paste the key on the OpenRouter group's bulk key button. An existing Project picks the new rows up with one click on **Sync presets** next to the Models page's search box — locally added models and stored credentials are untouched.
|
||||
3. Or from the terminal:
|
||||
|
||||
```bash
|
||||
penguin config model add --provider openrouter --model-id inclusionai/ling-3.0-flash:free --api-key <your-key> --set-default
|
||||
penguin config model list
|
||||
```
|
||||
|
||||
Set a free row as the Project default, or leave the default alone and pick one in the model selector when starting a Session — models are chosen per Session, not bound to an Agent. Free rows carry a light-yellow "Free" badge on the Models page and in the model picker, so they are easy to spot.
|
||||
|
||||
## What free buys you, and what it does not
|
||||
|
||||
The caveats, plainly:
|
||||
|
||||
- **Rate limits.** OpenRouter's free tier caps requests per minute and per day; a long Session or a benchmark run can hit them.
|
||||
- **Data policy.** Free models run under OpenRouter's free-model terms, and prompts may be used by the upstream provider as its terms allow. Send nothing you would not share.
|
||||
- **Availability and quality vary.** Free capacity is whatever providers choose to offer; models get busy, get slower, and get withdrawn.
|
||||
- **The router's target changes per request.** Its real context window varies with it — hence the conservative recorded window, which makes long Sessions compact early — and consistency is not what `openrouter/free` is for; a fixed free row gives you more of it, a paid row the most.
|
||||
|
||||
Free models are a genuine way to experience the full harness — Workspaces, tools, Skills, subagents, the Cost center reading a clean $0 — and to run light automation. For serious work, pick a paid model; the same catalog carries plenty.
|
||||
|
||||
## Get it
|
||||
|
||||
```bash
|
||||
curl -fsSL https://penguin.ooo/install.sh | sh
|
||||
penguin web
|
||||
```
|
||||
|
||||
Then open the Models page, drop in an OpenRouter key, and pick a free row.
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
title: 介绍 PenguinHarness 里的免费模型
|
||||
date: 2026-07-24
|
||||
category: news
|
||||
excerpt: 预置目录现有四条 $0 价格的 OpenRouter 条目——已有的 Nemotron 3 Ultra (free),以及新增的 Ling 3.0 Flash (free)、Laguna M.1 (free) 与 Free Models Router。一个免费注册的 OpenRouter API Key 就能把 Agent 跑起来,无需充值。本文介绍这套免费阵容、开启方式,以及免费档能换来什么、换不来什么。
|
||||
---
|
||||
|
||||
试用一个 Agent Harness,不应该从充值开始。PenguinHarness 的预置目录里带着免费模型:每百万 Token 价格为 $0 的模型行,和其他预置条目一样填好了协议、base URL、价格与上下文窗口——你与一个跑起来的 Agent 之间,只隔着一个 OpenRouter API Key,而注册它本身也是免费的。
|
||||
|
||||
截至今天,免费阵容共四条,全部在 OpenRouter 分组下:
|
||||
|
||||
| 提供方分组 | 模型 ID | 上下文 | 价格 |
|
||||
| ---------- | ---------------------------------------- | ---------------: | ---- |
|
||||
| OpenRouter | `nvidia/nemotron-3-ultra-550b-a55b:free` | 1,000,000 | $0 |
|
||||
| OpenRouter | `inclusionai/ling-3.0-flash:free` | 262,144 | $0 |
|
||||
| OpenRouter | `poolside/laguna-m.1:free` | 262,144 | $0 |
|
||||
| OpenRouter | `openrouter/free` | 128,000(保守值) | $0 |
|
||||
|
||||

|
||||
|
||||
## Nemotron 3 Ultra (free)
|
||||
|
||||
目录里的第一条免费行,也仍是最大的一条:NVIDIA 的开放前沿推理与编排模型,MoE 架构,总参数 550B、每 Token 激活 55B,混合 Transformer–Mamba,上下文窗口 1M Token。想看看 Harness 的重规划循环跑在大推理模型上是什么样、又不想付大推理模型的价格,就选这一行。
|
||||
|
||||
## 新增:Ling 3.0 Flash (free)
|
||||
|
||||
inclusionAI 于 7 月 23 日发布,次日进入目录。Ling-3.0-flash 是一个 124B 参数的 MoE 模型,每个 Token 只激活约 5.1B 参数;inclusionAI 给它的设计目标是 Token 效率与生产规模的 Agent 推理,含工具调用。这几乎就是在描述一个 Agent Harness 每天做的事:几十次短往返,每一次都带着工具 Schema 和不断增长的对话记录,单步效率就是全部的成本模型。一个稀疏、面向工具调用调优、价格为 $0 的模型,正适合承接这类流量。上下文 262K,仅文本。
|
||||
|
||||
## 新增:Laguna M.1 (free)
|
||||
|
||||
与 Ling 一同进入目录的,还有 Poolside 旗舰编码 Agent 模型的免费档。Laguna M.1 面向复杂软件工程任务优化——支持工具调用与推理的 Agent 编码工作流,而这正是 Harness 产生的流量形态。上下文 262K,仅文本。
|
||||
|
||||
## 新增:Free Models Router
|
||||
|
||||
`openrouter/free` 不是一个模型,而是 OpenRouter 的统一免费端点:每个请求被随机路由到 OpenRouter 上当前可用的某个免费模型,并按请求真正需要的能力过滤——工具调用、结构化输出等。上游的免费模型来来去去,这个路由始终有答案,你不必自己盯着最新清单。
|
||||
|
||||
有两个目录层面的决定值得说明。被路由到的目标随请求变化,其真实上下文窗口也随之变化,所以这一行刻意记录一个保守值 128,000,而非任何单一目标的真实窗口——长会话会提前压缩,而不是向被路由模型未必拥有的窗口继续膨胀。它也被刻意标记为仅文本:路由本身接受图片,但任一请求背后的模型未必支持,所以 PenguinHarness 不向这条路由发送图片,改走常规的纯文本交接(文件路径加 `describe_image`)。
|
||||
|
||||
## 开启方式
|
||||
|
||||
1. 在 [openrouter.ai](https://openrouter.ai/) 注册并创建 API Key——免费档不需要绑定支付方式。
|
||||
2. 新建 Project 自带这些预置条目:打开 **Models** 页面,用 OpenRouter 分组的批量填 Key 按钮贴上 Key 即可。已有 Project 点一下搜索框旁的**同步预置**就能拿到新行——本地添加的模型与已存凭证都不会被动到。
|
||||
3. 也可以走终端:
|
||||
|
||||
```bash
|
||||
penguin config model add --provider openrouter --model-id inclusionai/ling-3.0-flash:free --api-key <your-key> --set-default
|
||||
penguin config model list
|
||||
```
|
||||
|
||||
把某条免费行设为 Project 默认,或者不动默认、在创建会话时于模型选择器里现选一条——模型按 Session 选择,不绑定 Agent。免费行在 Models 页面与模型选择器中都带淡黄色「免费」标签,一眼可辨。
|
||||
|
||||
## 免费换来什么,换不来什么
|
||||
|
||||
把话说在前面:
|
||||
|
||||
- **速率限制。** OpenRouter 免费档限制每分钟与每天的请求数;一段长会话或一轮评测就可能碰到上限。
|
||||
- **数据政策。** 免费模型按 OpenRouter 的免费模型条款运行,Prompt 可能在上游条款允许的范围内被其使用。不要发送任何你不愿意公开的内容。
|
||||
- **可用性与质量会波动。** 免费容量取决于提供方的意愿;模型会变忙、变慢,也会下线。
|
||||
- **路由目标逐请求变化。** 目标的真实上下文窗口也随之变化——因此目录记录的是上面那个保守值,让长会话尽早压缩;一致性本就不是 `openrouter/free` 的目标,固定选一条免费行会更一致,付费行最一致。
|
||||
|
||||
免费模型足以真实地体验完整的 Harness——Workspace、工具、Skill、子 Agent,以及费用中心里干干净净的 $0——也能承担轻量自动化。正经的工作请换付费模型,同一份目录里有的是。
|
||||
|
||||
## 获取方式
|
||||
|
||||
```bash
|
||||
curl -fsSL https://penguin.ooo/install.sh | sh
|
||||
penguin web
|
||||
```
|
||||
|
||||
然后打开 Models 页面,填入 OpenRouter Key,选一条免费行。
|
||||
@@ -10,7 +10,8 @@
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsc --noEmit -p tsconfig.json",
|
||||
"test": "vitest run --passWithNoTests",
|
||||
"shots": "node scripts/capture-shots.mjs"
|
||||
"shots": "node scripts/capture-shots.mjs",
|
||||
"blog-shots": "node scripts/capture-blog-shots.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.1.0",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 124 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 131 KiB |
@@ -0,0 +1,193 @@
|
||||
/**
|
||||
* Capture blog-post screenshots of the product's Models page (the free-model rows).
|
||||
*
|
||||
* A deliberately lean sibling of capture-shots.mjs: that script drives a full scripted
|
||||
* LLM conversation for the landing hero shots, while the Models page needs no LLM at
|
||||
* all — a fresh user's default Project already presets the entire built-in model
|
||||
* catalog, so the free rows (Ling 3.0 Flash / Nemotron 3 Ultra / Laguna M.1 /
|
||||
* openrouter/free) render their light-yellow "Free" badges straight from the $0
|
||||
* pricing. Flow: boot the Web server against a temp data root serving the built web
|
||||
* dist -> provision one demo user per UI language (password rotated once so the
|
||||
* initial-password banner never shows) -> open /models, scroll the OpenRouter group
|
||||
* to the top of the viewport (Claude Opus 5 sits on its first card row, the free
|
||||
* rows a few rows below) -> screenshot, light theme, zh + en, into
|
||||
* public/blog-assets/ as free-models-page-<lang>-light.webp (re-encoded to WebP
|
||||
* inside Chromium, same as capture-shots.mjs).
|
||||
*
|
||||
* Prereqs: `pnpm --filter @prismshadow/penguin-{skills,core,server,web} build` and
|
||||
* Playwright's chromium. Run: `node scripts/capture-blog-shots.mjs` (or
|
||||
* `pnpm --filter @prismshadow/penguin-landing blog-shots`).
|
||||
*/
|
||||
import { spawn } from "node:child_process";
|
||||
import { mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { chromium } from "@playwright/test";
|
||||
|
||||
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
||||
const ROOT = path.resolve(HERE, "../../..");
|
||||
const OUT_DIR = path.resolve(HERE, "../public/blog-assets");
|
||||
const SRV_PORT = 8944; // Distinct from capture-shots.mjs (8940/8941) so both can run.
|
||||
// On loopback binds the App is canonically served on `localhost`; the 127.0.0.1
|
||||
// counterpart is the Workspace-preview host, where /api deliberately answers 401
|
||||
// (see server app.ts's canonical-host guard).
|
||||
const BASE = `http://localhost:${SRV_PORT}`;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Server + API helpers (same shape as capture-shots.mjs).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function waitFor(url, tries = 60) {
|
||||
for (let i = 0; i < tries; i++) {
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (res.ok) return;
|
||||
} catch {}
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
}
|
||||
throw new Error(`server not ready: ${url}`);
|
||||
}
|
||||
|
||||
async function api(cookie, method, url, body) {
|
||||
const res = await fetch(`${BASE}${url}`, {
|
||||
method,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
...(cookie ? { cookie } : {}),
|
||||
},
|
||||
...(body ? { body: JSON.stringify(body) } : {}),
|
||||
});
|
||||
if (!res.ok) throw new Error(`${method} ${url} -> ${res.status} ${await res.text()}`);
|
||||
return { json: await res.json().catch(() => ({})), setCookie: res.headers.get("set-cookie") };
|
||||
}
|
||||
|
||||
async function login(userId, password) {
|
||||
const { setCookie } = await api(null, "POST", "/api/auth/login", { userId, password });
|
||||
if (!setCookie) throw new Error("no session cookie from login");
|
||||
return setCookie.split(";")[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-language demo user (same ids as capture-shots.mjs). Unlike that script it does NOT
|
||||
* touch the model table: the fresh Project's preset catalog is exactly what the shot is
|
||||
* about. The password is rotated once so the initial-password banner stays out of frame.
|
||||
*/
|
||||
async function provisionUser(adminCookie, lang) {
|
||||
const userId = lang === "zh" ? "demo" : "alex";
|
||||
const initial = `${userId}12345`;
|
||||
await api(adminCookie, "POST", "/api/admin/users", { userId, password: initial }).catch((e) => {
|
||||
if (!String(e).includes("409")) throw e;
|
||||
});
|
||||
let password = initial;
|
||||
const cookie = await login(userId, initial);
|
||||
try {
|
||||
await api(cookie, "PUT", "/api/me/password", {
|
||||
oldPassword: initial,
|
||||
newPassword: `penguin-${userId}-2026`,
|
||||
});
|
||||
password = `penguin-${userId}-2026`;
|
||||
} catch {}
|
||||
return { userId, password };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const dataRoot = mkdtempSync(path.join(os.tmpdir(), "penguin-blog-shots-"));
|
||||
mkdirSync(OUT_DIR, { recursive: true });
|
||||
|
||||
const srv = spawn("node", [path.join(ROOT, "packages/server/dist/index.js")], {
|
||||
env: {
|
||||
...process.env,
|
||||
PENGUIN_HOME: path.join(dataRoot, "home"),
|
||||
PENGUIN_WEB_DB: path.join(dataRoot, "web.db"),
|
||||
PENGUIN_WEB_DIST: path.join(ROOT, "packages/web/dist"),
|
||||
PORT: String(SRV_PORT),
|
||||
HOST: "127.0.0.1",
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
srv.stderr.on("data", (d) => process.stderr.write(`[srv!] ${d}`));
|
||||
process.on("exit", () => {
|
||||
try {
|
||||
srv.kill();
|
||||
} catch {}
|
||||
});
|
||||
|
||||
try {
|
||||
await waitFor(`${BASE}/`);
|
||||
console.log(`[blog-shots] server ready on ${BASE}`);
|
||||
|
||||
const adminCookie = await login("admin", "penguin-2026");
|
||||
const browser = await chromium.launch();
|
||||
|
||||
// WebP encoder: Chromium re-encodes the PNG screenshot buffer via canvas (same
|
||||
// convention and quality as capture-shots.mjs, keeping repo assets small).
|
||||
const encoderPage = await browser.newPage();
|
||||
async function saveWebp(pngBuffer, fileName) {
|
||||
const dataUrl = await encoderPage.evaluate(async (b64) => {
|
||||
const img = new Image();
|
||||
img.src = `data:image/png;base64,${b64}`;
|
||||
await img.decode();
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
canvas.getContext("2d").drawImage(img, 0, 0);
|
||||
return canvas.toDataURL("image/webp", 0.82);
|
||||
}, pngBuffer.toString("base64"));
|
||||
writeFileSync(path.join(OUT_DIR, fileName), Buffer.from(dataUrl.split(",")[1], "base64"));
|
||||
console.log(`[blog-shots] ${fileName}`);
|
||||
}
|
||||
|
||||
for (const lang of ["zh", "en"]) {
|
||||
const user = await provisionUser(adminCookie, lang);
|
||||
|
||||
// 1280x900 @1.5x, light theme (the blog embeds a single light variant per language,
|
||||
// like rag-app-<lang>-light.webp): tall enough that the OpenRouter group shows Claude
|
||||
// Opus 5 (first card row) together with the free rows and their badges.
|
||||
const context = await browser.newContext({
|
||||
viewport: { width: 1280, height: 900 },
|
||||
deviceScaleFactor: 1.5,
|
||||
locale: lang === "zh" ? "zh-CN" : "en-US",
|
||||
});
|
||||
await context.addInitScript(
|
||||
([t, l]) => {
|
||||
localStorage.setItem("penguin.theme", t);
|
||||
localStorage.setItem("penguin.lang", l);
|
||||
},
|
||||
["light", lang],
|
||||
);
|
||||
const page = await context.newPage();
|
||||
await page.goto(`${BASE}/login`);
|
||||
const loginRes = await page.request.post(`${BASE}/api/auth/login`, {
|
||||
data: { userId: user.userId, password: user.password },
|
||||
});
|
||||
if (!loginRes.ok()) throw new Error(`browser login failed: ${loginRes.status()}`);
|
||||
|
||||
await page.goto(`${BASE}/models`);
|
||||
// The catalog is loaded once a free row's card is rendered (its "Free" badge with it).
|
||||
const lingCard = page.getByText("Ling 3.0 Flash (free)").first();
|
||||
await lingCard.waitFor({ timeout: 20000 });
|
||||
// Scroll the OpenRouter group section to the top of the viewport so the group header,
|
||||
// Claude Opus 5 (first card row) and the badged free rows all sit in frame.
|
||||
await page
|
||||
.getByRole("button", { name: /OpenRouter/ })
|
||||
.first()
|
||||
.evaluate((el) =>
|
||||
el.closest("section").scrollIntoView({ block: "start", behavior: "instant" }),
|
||||
);
|
||||
await page.waitForTimeout(1200);
|
||||
await saveWebp(await page.screenshot(), `free-models-page-${lang}-light.webp`);
|
||||
|
||||
await context.close();
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
console.log(`[blog-shots] done -> ${OUT_DIR}`);
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
console.error("[blog-shots] FAILED:", err);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -20,6 +20,7 @@ const ROTATE_MS = 6000;
|
||||
* freshest news goes at the front.
|
||||
*/
|
||||
const ITEMS = [
|
||||
{ key: "freeModels", to: "/blog/free-models-in-penguin-harness" },
|
||||
{ key: "gemini", to: "/blog/gemini-3-6-in-penguinharness" },
|
||||
{ key: "models", to: "/blog/introducing-penguinharness" },
|
||||
{ key: "fireworks", to: "/blog/fireworks-credits-amd" },
|
||||
@@ -33,6 +34,7 @@ function prefersReducedMotion(): boolean {
|
||||
|
||||
export function AnnouncementBar() {
|
||||
const texts: Record<(typeof ITEMS)[number]["key"], string> = {
|
||||
freeModels: S.announcement.freeModels,
|
||||
gemini: S.announcement.gemini,
|
||||
models: S.announcement.models,
|
||||
fireworks: S.announcement.fireworks,
|
||||
|
||||
@@ -12,6 +12,7 @@ export const en: Strings = {
|
||||
label: "Announcements",
|
||||
prev: "Previous announcement",
|
||||
next: "Next announcement",
|
||||
freeModels: "Free models Ling 3.0 Flash and the Free Models Router are now in PenguinHarness",
|
||||
gemini: "Gemini 3.6 Flash is now available in PenguinHarness",
|
||||
models: "Kimi K3 and Qwen 3.8 Max are now available in PenguinHarness",
|
||||
fireworks: "Claim $50 in Fireworks API credits with the AMD Developer Program",
|
||||
|
||||
@@ -13,6 +13,7 @@ export const zh = {
|
||||
label: "公告",
|
||||
prev: "上一条公告",
|
||||
next: "下一条公告",
|
||||
freeModels: "Ling 3.0 Flash 与 Free Models Router 等免费模型现已在 PenguinHarness 可用",
|
||||
gemini: "Gemini 3.6 Flash 现已在 PenguinHarness 可用",
|
||||
models: "Kimi K3 与 Qwen 3.8 Max 模型现已在 PenguinHarness 可用",
|
||||
fireworks: "携手 AMD 开发者计划:$50 Fireworks API 额度免费领取中",
|
||||
|
||||
@@ -96,7 +96,7 @@ describe("frontmatter mapping (author / pinned / category)", () => {
|
||||
it("reads the pinned flag and sorts the pinned post first", () => {
|
||||
for (const locale of ["en", "zh"] as const) {
|
||||
const posts = postsFor(locale);
|
||||
expect(posts.length).toBe(10);
|
||||
expect(posts.length).toBe(11);
|
||||
// The launch post stays the single pinned post; newer posts sort under it by date.
|
||||
expect(posts.filter((p) => p.pinned).map((p) => p.slug)).toEqual([
|
||||
"introducing-penguinharness",
|
||||
@@ -120,6 +120,7 @@ describe("frontmatter mapping (author / pinned / category)", () => {
|
||||
it("filters by the news category, newest first", () => {
|
||||
expect(postsFor("en", "news").map((p) => p.slug)).toEqual([
|
||||
"introducing-penguinharness",
|
||||
"free-models-in-penguin-harness",
|
||||
"gemini-3-6-in-penguinharness",
|
||||
"fireworks-credits-amd",
|
||||
]);
|
||||
|
||||
Reference in new issue
Block a user