diff --git a/README.md b/README.md index fc8c7b6..6882a0b 100644 --- a/README.md +++ b/README.md @@ -86,11 +86,11 @@ Four Skill groups ship in the box ([docs](https://penguin.ooo/docs/skills)); age | Model | Providers | | ---------------- | -------------------------------------------------------------------------------- | -| DeepSeek V4 | DeepSeek, OpenRouter, Fireworks AI, SiliconFlow, Qwen Token Plan | +| DeepSeek V4 | DeepSeek, OpenRouter, Fireworks AI, SiliconFlow, Qwen Token Plan, Qwen Pay-As-You-Go | | Kimi K3 | Moonshot AI, OpenRouter, Qwen Pay-As-You-Go | | GLM 5.2 | Z.AI, OpenRouter, Fireworks AI, SiliconFlow, Qwen Token Plan, Qwen Pay-As-You-Go | | Hunyuan 3 | OpenRouter | -| Qwen 3.8 Max | Qwen Token Plan (preview) | +| Qwen 3.8 Max | Qwen Token Plan, Qwen Pay-As-You-Go | | GPT 5.6 | OpenRouter | | Gemini 3.6 Flash | Google Gemini, OpenRouter | | Claude 5 | Anthropic, OpenRouter | @@ -174,7 +174,7 @@ cd penguin-install The same engine, scriptable — made to be driven by agents (and agents building agents): ```bash -penguin config model add --provider deepseek --model-id deepseek-v4-pro --api-key sk-... --set-default +penguin config model add --provider deepseek --model-id deepseek-v4-flash --api-key sk-... --set-default penguin run -m "Create hello.txt containing Hello, Penguin" # one-shot task penguin chat # interactive REPL (/compact, /exit, Ctrl-C to interrupt) penguin server # headless service (same API the Web App uses) diff --git a/README.zh.md b/README.zh.md index 9abed35..ca3f3c8 100644 --- a/README.zh.md +++ b/README.zh.md @@ -86,11 +86,11 @@ https://github.com/user-attachments/assets/aec49ae9-b743-467b-b247-37bedfeaa36e | 模型 | 可用供应商 | | ---------------- | -------------------------------------------------------------------------------- | -| DeepSeek V4 | DeepSeek, OpenRouter, Fireworks AI, SiliconFlow, Qwen Token Plan | +| DeepSeek V4 | DeepSeek, OpenRouter, Fireworks AI, SiliconFlow, Qwen Token Plan, Qwen Pay-As-You-Go | | Kimi K3 | Moonshot AI, OpenRouter, Qwen Pay-As-You-Go | | GLM 5.2 | Z.AI, OpenRouter, Fireworks AI, SiliconFlow, Qwen Token Plan, Qwen Pay-As-You-Go | | Hunyuan 3 | OpenRouter | -| Qwen 3.8 Max | Qwen Token Plan(预览) | +| Qwen 3.8 Max | Qwen Token Plan, Qwen Pay-As-You-Go | | GPT 5.6 | OpenRouter | | Gemini 3.6 Flash | Google Gemini, OpenRouter | | Claude 5 | Anthropic, OpenRouter | @@ -174,7 +174,7 @@ cd penguin-install 同一引擎、可脚本化——为被 Agent 驱动而生(以及让 Agent 构建 Agent): ```bash -penguin config model add --provider deepseek --model-id deepseek-v4-pro --api-key sk-... --set-default +penguin config model add --provider deepseek --model-id deepseek-v4-flash --api-key sk-... --set-default penguin run -m "Create hello.txt containing Hello, Penguin" # 单次任务 penguin chat # 交互式 REPL(/compact、/exit、Ctrl-C 中断) penguin server # 无界面服务(与 Web 应用同一套 API) diff --git a/packages/core/src/state/model-catalog.ts b/packages/core/src/state/model-catalog.ts index a7410d0..072676e 100644 --- a/packages/core/src/state/model-catalog.ts +++ b/packages/core/src/state/model-catalog.ts @@ -219,32 +219,27 @@ export const MODEL_CATALOG: ModelCatalogEntry[] = [ pricing: cny(0.025, 3, 6), supportsVision: false, }, - // -- OpenRouter (gateway: OpenAI-compatible protocol, preset base URL). Entries added - // 2026-07-20 list no cache pricing on their OpenRouter pages, so cache_read carries the - // standard input price; the :free tier and the openrouter/free Free Models Router store a - // genuine $0 price (not "unknown"), so costs correctly compute to 0. GPT models are - // uniformly vision-capable (OpenAI product-line policy) even where the gateway page omits - // the modality. - // Two cache_read conventions coexist in this block: rows whose upstream publishes a - // cache-hit price store that real price (the google/gemini-3.6-flash and - // google/gemini-3.5-flash-lite entries below), while the 2026-07-20 rows still repeat the - // input price. Several of those older rows do have a published cache price upstream and - // should be re-read in one pass; until then treat their cache_read as an upper bound. -- + // -- OpenRouter (gateway: OpenAI-compatible protocol, preset base URL). Prices re-read in + // one pass on 2026-08-03 from the models API (/api/v1/models): cache_read stores the + // published input_cache_read (falling back to the input price for the few rows without + // one — qwen3.6-35b-a3b and the :free rows); cache_write stores input_cache_write only + // when it is a genuine per-token write premium (the Anthropic and GPT rows, 1.25x input) — + // Gemini's field is an hourly cache-STORAGE rate, not a per-token price, so those rows + // keep the input price — and otherwise also carries the input price. The :free tier and + // the openrouter/free Free Models Router store a genuine $0 price (not "unknown"), so + // costs correctly compute to 0. GPT models are uniformly vision-capable (OpenAI + // product-line policy) even where the gateway page omits the modality. -- { modelId: "anthropic/claude-fable-5", displayName: "Claude Fable 5", provider: "openrouter", contextWindow: 1000000, - pricing: usd(10, 10, 50), + pricing: usd(1, 12.5, 50), supportsVision: true, clientType: "openai", baseUrl: OPENROUTER_BASE_URL, }, { - // Upstream publishes full cache pricing for this row (2026-07-24, per the OpenRouter - // models API: $0.50/mtok cache hit, $6.25 cache write = 1.25 x input, $5 input, $25 - // output), so cache_read stores the real discounted price — same convention as the - // Gemini rows below — instead of repeating the input price. modelId: "anthropic/claude-opus-5", displayName: "Claude Opus 5", provider: "openrouter", @@ -259,7 +254,7 @@ export const MODEL_CATALOG: ModelCatalogEntry[] = [ displayName: "Claude Opus 4.8", provider: "openrouter", contextWindow: 1000000, - pricing: usd(5, 5, 25), + pricing: usd(0.5, 6.25, 25), supportsVision: true, clientType: "openai", baseUrl: OPENROUTER_BASE_URL, @@ -269,7 +264,7 @@ export const MODEL_CATALOG: ModelCatalogEntry[] = [ displayName: "Claude Opus 4.7", provider: "openrouter", contextWindow: 1000000, - pricing: usd(5, 5, 25), + pricing: usd(0.5, 6.25, 25), supportsVision: true, clientType: "openai", baseUrl: OPENROUTER_BASE_URL, @@ -279,17 +274,27 @@ export const MODEL_CATALOG: ModelCatalogEntry[] = [ displayName: "Claude Sonnet 5", provider: "openrouter", contextWindow: 1000000, - pricing: usd(2, 2, 10), + pricing: usd(0.2, 2.5, 10), supportsVision: true, clientType: "openai", baseUrl: OPENROUTER_BASE_URL, }, + { + modelId: "deepseek/deepseek-v4-flash-0731", + displayName: "DeepSeek V4 Flash 0731", + provider: "openrouter", + contextWindow: 1000000, + pricing: usd(0.018, 0.09, 0.18), + supportsVision: false, + clientType: "openai", + baseUrl: OPENROUTER_BASE_URL, + }, { modelId: "deepseek/deepseek-v4-flash", displayName: "DeepSeek V4 Flash", provider: "openrouter", contextWindow: 1000000, - pricing: usd(0.09, 0.09, 0.18), + pricing: usd(0.028, 0.14, 0.28), supportsVision: false, clientType: "openai", baseUrl: OPENROUTER_BASE_URL, @@ -299,19 +304,16 @@ export const MODEL_CATALOG: ModelCatalogEntry[] = [ displayName: "DeepSeek V4 Pro", provider: "openrouter", contextWindow: 1000000, - pricing: usd(0.435, 0.435, 0.87), + pricing: usd(0.003625, 0.435, 0.87), supportsVision: false, clientType: "openai", baseUrl: OPENROUTER_BASE_URL, }, { - // Unlike the older OpenRouter entries above, upstream **does** publish a cache-hit price - // for the Gemini rows (2026-07-22: $0.15/mtok here, agreed by the OpenRouter models API - // and AgentHub's own supported-model registry), so cache_read stores the real discounted - // price rather than repeating the input price: cache_read is billed as its own bucket in - // the cost center, and an input-priced cache_read overstates cache-heavy spend 10x. - // cache_write repeats the input price (no separate per-token cache-write fee), matching - // the direct-vendor Gemini rows below. + // cache_read is billed as its own bucket in the cost center, and an input-priced + // cache_read would overstate cache-heavy Gemini spend 10x; cache_write repeats the input + // price (see the block comment — Gemini publishes storage-per-hour, not per-token write), + // matching the direct-vendor Gemini rows below. modelId: "google/gemini-3.6-flash", displayName: "Gemini 3.6 Flash", provider: "openrouter", @@ -326,7 +328,7 @@ export const MODEL_CATALOG: ModelCatalogEntry[] = [ displayName: "Gemini 3.5 Flash", provider: "openrouter", contextWindow: 1048576, - pricing: usd(1.5, 1.5, 9), + pricing: usd(0.15, 1.5, 9), supportsVision: true, clientType: "openai", baseUrl: OPENROUTER_BASE_URL, @@ -372,7 +374,7 @@ export const MODEL_CATALOG: ModelCatalogEntry[] = [ displayName: "Kimi K3", provider: "openrouter", contextWindow: 1000000, - pricing: usd(3, 3, 15), + pricing: usd(0.3, 3, 15), supportsVision: true, clientType: "openai", baseUrl: OPENROUTER_BASE_URL, @@ -382,7 +384,7 @@ export const MODEL_CATALOG: ModelCatalogEntry[] = [ displayName: "Kimi K2.6", provider: "openrouter", contextWindow: 262144, - pricing: usd(0.144, 0.684, 3.42), + pricing: usd(0.2, 0.6, 3.41), supportsVision: true, clientType: "openai", baseUrl: OPENROUTER_BASE_URL, @@ -397,12 +399,25 @@ export const MODEL_CATALOG: ModelCatalogEntry[] = [ clientType: "openai", baseUrl: OPENROUTER_BASE_URL, }, + { + // The page shows the $0.10/$0.60 rate under a "50% off" banner; the models API bills the + // same numbers (with real hit/write prices), so this row stores them — re-read when the + // promotion ends. + modelId: "openai/gpt-5.6-luna", + displayName: "GPT-5.6 Luna", + provider: "openrouter", + contextWindow: 1000000, + pricing: usd(0.01, 0.125, 0.6), + supportsVision: true, + clientType: "openai", + baseUrl: OPENROUTER_BASE_URL, + }, { modelId: "openai/gpt-5.6-sol", displayName: "GPT-5.6 Sol", provider: "openrouter", contextWindow: 1000000, - pricing: usd(5, 5, 30), + pricing: usd(0.5, 6.25, 30), supportsVision: true, clientType: "openai", baseUrl: OPENROUTER_BASE_URL, @@ -412,7 +427,7 @@ export const MODEL_CATALOG: ModelCatalogEntry[] = [ displayName: "GPT-5.6 Terra", provider: "openrouter", contextWindow: 1000000, - pricing: usd(2.5, 2.5, 15), + pricing: usd(0.1, 1.25, 6), supportsVision: true, clientType: "openai", baseUrl: OPENROUTER_BASE_URL, @@ -422,7 +437,7 @@ export const MODEL_CATALOG: ModelCatalogEntry[] = [ displayName: "GPT-5.5", provider: "openrouter", contextWindow: 1000000, - pricing: usd(5, 5, 30), + pricing: usd(0.5, 5, 30), supportsVision: true, clientType: "openai", baseUrl: OPENROUTER_BASE_URL, @@ -444,19 +459,6 @@ export const MODEL_CATALOG: ModelCatalogEntry[] = [ clientType: "openai", baseUrl: OPENROUTER_BASE_URL, }, - { - // Poolside's free tier of Laguna M.1, its flagship coding-agent model (agentic coding - // workflows with tool calling and reasoning), text-only; context and $0 pricing per the - // OpenRouter models API (2026-07-24). - modelId: "poolside/laguna-m.1:free", - displayName: "Laguna M.1 (free)", - provider: "openrouter", - contextWindow: 262144, - pricing: usd(0, 0, 0), - supportsVision: false, - clientType: "openai", - baseUrl: OPENROUTER_BASE_URL, - }, { // Neither the OpenRouter page nor AgentHub's registry publishes a cache price for this // model, so cache_read repeats the input price (no discount assumed). @@ -485,7 +487,7 @@ export const MODEL_CATALOG: ModelCatalogEntry[] = [ displayName: "Hy3", provider: "openrouter", contextWindow: 262144, - pricing: usd(0.035, 0.14, 0.58), + pricing: usd(0.033, 0.132, 0.528), supportsVision: false, clientType: "openai", baseUrl: OPENROUTER_BASE_URL, @@ -495,7 +497,7 @@ export const MODEL_CATALOG: ModelCatalogEntry[] = [ displayName: "Grok 4.5", provider: "openrouter", contextWindow: 500000, - pricing: usd(2, 2, 6), + pricing: usd(0.3, 2, 6), supportsVision: true, clientType: "openai", baseUrl: OPENROUTER_BASE_URL, @@ -515,7 +517,7 @@ export const MODEL_CATALOG: ModelCatalogEntry[] = [ displayName: "GLM-5.2", provider: "openrouter", contextWindow: 1000000, - pricing: usd(0.93, 0.93, 3), + pricing: usd(0.221, 1.19, 3.74), supportsVision: false, clientType: "openai", baseUrl: OPENROUTER_BASE_URL, @@ -634,16 +636,17 @@ export const MODEL_CATALOG: ModelCatalogEntry[] = [ clientType: "openai", baseUrl: SILICONFLOW_BASE_URL, }, - // The three Pro/ and Qwen/ entries below carry no pricing: AgentHub's registry publishes - // none for them, and SiliconFlow's price list sits behind an authenticated API (the public - // /v1/models endpoint returns 401 and the console page is client-rendered). Rather than - // invent a rate, the entries ship unpriced — the same state as qwen3.8-max-preview, so their - // cost reads as 0 until a published price can be filled in. + // The Pro/ and Qwen/ entries below were unpriced until 2026-08-03 (SiliconFlow's price + // list sits behind an authenticated console); prices below are its official CNY list + // prices. GLM-5.1 bills in two input-length tiers ([0, 32k) and [32k, +inf) for hit/input/ + // output alike); the catalog stores one number per bucket, so these rows keep the LOWER + // tier — treat its cost as a floor for long-context use. { modelId: "Pro/moonshotai/Kimi-K2.6", displayName: "Kimi K2.6", provider: "siliconflow", contextWindow: 262144, + pricing: cny(1.1, 6.5, 27), supportsVision: true, clientType: "openai", baseUrl: SILICONFLOW_BASE_URL, @@ -653,15 +656,18 @@ export const MODEL_CATALOG: ModelCatalogEntry[] = [ displayName: "GLM-5.1", provider: "siliconflow", contextWindow: 200000, + pricing: cny(1.3, 6, 24), supportsVision: false, clientType: "openai", baseUrl: SILICONFLOW_BASE_URL, }, { + // No cache-hit price on the list, so cache_read carries the input price. modelId: "Qwen/Qwen3.6-35B-A3B", displayName: "Qwen 3.6 35B A3B", provider: "siliconflow", contextWindow: 262144, + pricing: cny(1.8, 1.8, 10.8), supportsVision: true, clientType: "openai", baseUrl: SILICONFLOW_BASE_URL, @@ -679,9 +685,18 @@ export const MODEL_CATALOG: ModelCatalogEntry[] = [ // -- Qwen Token Plan (subscription gateway; vision flags per the plan's supported-model // table). Pricing and context windows from each model's page at // www.qianwenai.com/models/ (official CNY list prices; limited-time promotions such as - // the 20%/50% off discounts are not stored). qwen3.8-max-preview is preview-only with a - // quota-multiplier promotion and publishes no per-token list price nor a context window, so - // it carries no pricing and uses its family's 1M window. -- + // the 20%/50% off discounts are not stored). Lineup updated 2026-08-03: qwen3.8-max and + // deepseek-v4-flash-0731 join; qwen3.8-max-preview and qwen3.7-max leave the plan. -- + { + modelId: "deepseek-v4-flash-0731", + displayName: "DeepSeek V4 Flash 0731", + provider: "qwen-token-plan", + contextWindow: 1000000, + pricing: cny(0.2, 1, 2), + supportsVision: false, + clientType: "openai", + baseUrl: QWEN_TOKEN_PLAN_BASE_URL, + }, { modelId: "deepseek-v4-pro", displayName: "DeepSeek V4 Pro", @@ -703,24 +718,15 @@ export const MODEL_CATALOG: ModelCatalogEntry[] = [ baseUrl: QWEN_TOKEN_PLAN_BASE_URL, }, { - modelId: "qwen3.8-max-preview", - displayName: "Qwen 3.8 Max Preview", + modelId: "qwen3.8-max", + displayName: "Qwen 3.8 Max", provider: "qwen-token-plan", contextWindow: 1000000, + pricing: cny(1.5, 12, 36), supportsVision: true, clientType: "openai", baseUrl: QWEN_TOKEN_PLAN_BASE_URL, }, - { - modelId: "qwen3.7-max", - displayName: "Qwen 3.7 Max", - provider: "qwen-token-plan", - contextWindow: 1000000, - pricing: cny(2.4, 12, 36), - supportsVision: false, - clientType: "openai", - baseUrl: QWEN_TOKEN_PLAN_BASE_URL, - }, { modelId: "qwen3.7-plus", displayName: "Qwen 3.7 Plus", @@ -733,7 +739,18 @@ export const MODEL_CATALOG: ModelCatalogEntry[] = [ }, // -- Qwen Pay-As-You-Go (DashScope's OpenAI-compatible pay-per-token marketplace; official // CNY list prices and specs from each model's page at www.qianwenai.com/models/ — - // resold third-party models keep their vendor-prefixed upstream ids) -- + // resold third-party models keep their upstream ids exactly as the page lists them: kimi/ + // and ZHIPU/ carry vendor prefixes, DeepSeek is listed bare) -- + { + modelId: "deepseek-v4-flash-0731", + displayName: "DeepSeek V4 Flash 0731", + provider: "qwen-pay-as-you-go", + contextWindow: 1000000, + pricing: cny(0.2, 1, 2), + supportsVision: false, + clientType: "openai", + baseUrl: QWEN_PAYG_BASE_URL, + }, { modelId: "kimi/kimi-k3", displayName: "Kimi K3", @@ -745,12 +762,12 @@ export const MODEL_CATALOG: ModelCatalogEntry[] = [ baseUrl: QWEN_PAYG_BASE_URL, }, { - modelId: "qwen3.7-max", - displayName: "Qwen 3.7 Max", + modelId: "qwen3.8-max", + displayName: "Qwen 3.8 Max", provider: "qwen-pay-as-you-go", contextWindow: 1000000, - pricing: cny(2.4, 12, 36), - supportsVision: false, + pricing: cny(1.5, 12, 36), + supportsVision: true, clientType: "openai", baseUrl: QWEN_PAYG_BASE_URL, }, @@ -1044,16 +1061,13 @@ export function presetModelEntries(): ModelEntry[] { /** * The model's own homepage/detail page for the frontend's model-card link. Gateway groups * have a stable per-model URL pattern (works for user-added ids in those groups too); - * direct-vendor models link to the vendor's model list/docs page; the Token Plan preview - * model has no dedicated page and links to the plan's model overview; custom and - * user-defined groups have no page to vouch for. + * direct-vendor models link to the vendor's model list/docs page; custom and user-defined + * groups have no page to vouch for. */ export function modelHomepageUrl(provider: string, modelId: string): string | undefined { if (provider === "openrouter") return `https://openrouter.ai/${modelId}`; if (provider === "qwen-token-plan") { - return modelId === "qwen3.8-max-preview" - ? providerInfo(provider)?.modelsUrl - : `https://www.qianwenai.com/models/${modelId}`; + return `https://www.qianwenai.com/models/${modelId}`; } if (provider === "fireworks") { // API id "accounts//models/" -> page "app.fireworks.ai/models//"; diff --git a/packages/core/src/state/project-config.ts b/packages/core/src/state/project-config.ts index aeb9896..8394742 100644 --- a/packages/core/src/state/project-config.ts +++ b/packages/core/src/state/project-config.ts @@ -133,7 +133,7 @@ export interface ProjectConfig { */ export function defaultProjectConfig(): ProjectConfig { return { - default_model: { provider: "deepseek", model_id: "deepseek-v4-pro" }, + default_model: { provider: "deepseek", model_id: "deepseek-v4-flash" }, models: presetModelEntries(), }; } diff --git a/packages/core/test/agent.test.ts b/packages/core/test/agent.test.ts index 8b13c48..d0f5a70 100644 --- a/packages/core/test/agent.test.ts +++ b/packages/core/test/agent.test.ts @@ -169,9 +169,9 @@ describe("Agent.createSession model reference ((provider, model_id) pair)", () = // (same source that Trace writes). const meta = session.metaMessage.payload as { provider: string; model_id: string }; expect(meta.provider).toBe("deepseek"); - expect(meta.model_id).toBe("deepseek-v4-pro"); + expect(meta.model_id).toBe("deepseek-v4-flash"); expect(session.provider).toBe("deepseek"); - expect(session.modelId).toBe("deepseek-v4-pro"); + expect(session.modelId).toBe("deepseek-v4-flash"); } finally { session.dispose(); } @@ -229,7 +229,7 @@ describe("Agent.createSession model reference ((provider, model_id) pair)", () = const session = await agent.createSession({ workspaceDir: ws }); try { expect(session.provider).toBe("deepseek"); - expect(session.modelId).toBe("deepseek-v4-pro"); + expect(session.modelId).toBe("deepseek-v4-flash"); } finally { session.dispose(); } diff --git a/packages/core/test/model-catalog.test.ts b/packages/core/test/model-catalog.test.ts index e7dd921..7211cf1 100644 --- a/packages/core/test/model-catalog.test.ts +++ b/packages/core/test/model-catalog.test.ts @@ -68,22 +68,12 @@ describe("model-catalog", () => { } }); - it("price buckets are positive (preview models without a list price omit pricing); context_window is a positive integer", () => { - // Models with no obtainable published price. qwen3.8-max-preview: the plan runs a - // quota-multiplier promotion instead of a per-token list price. The three SiliconFlow - // entries: AgentHub's registry publishes no pricing for them and SiliconFlow's price list - // is only reachable with an authenticated token, so no number can be sourced. All of them - // carry no pricing and their costs read as 0, same as unpriced user models. - const UNPRICED = new Set([ - "qwen-token-plan\0qwen3.8-max-preview", - "siliconflow\0Pro/moonshotai/Kimi-K2.6", - "siliconflow\0Pro/zai-org/GLM-5.1", - "siliconflow\0Qwen/Qwen3.6-35B-A3B", - ]); + it("every entry is priced (free-tier rows store a genuine $0); context_window is a positive integer", () => { + // As of 2026-08-03 no catalog entry ships unpriced: the last holdouts (the three + // SiliconFlow Pro//Qwen/ rows) got their official CNY list prices. Unpriced remains a + // legal state for user-added models only. for (const m of MODEL_CATALOG) { - if (UNPRICED.has(`${m.provider}\0${m.modelId}`)) { - expect(m.pricing, m.modelId).toBeUndefined(); - } else if (m.modelId.endsWith(":free") || m.modelId === "openrouter/free") { + if (m.modelId.endsWith(":free") || m.modelId === "openrouter/free") { // Free-tier gateway model (:free variants and the openrouter/free router): a genuine // $0 price (not "unknown"), so costs compute to 0. expect(m.pricing, m.modelId).toBeDefined(); @@ -111,8 +101,8 @@ describe("model-catalog", () => { // It matches on (group, upstream id) pairs, so an identically named upstream id never // matches across the wrong group. There is no bare-id lookup at all: a gateway reselling a // vendor model keeps the vendor's upstream id, so a bare id names no single catalog entry - // and the catalog never offers to pick one (`glm-5.2`, `qwen3.7-max`, `qwen3.7-plus` and - // `deepseek-v4-pro` each appear under two groups). + // and the catalog never offers to pick one (`glm-5.2`, `deepseek-v4-pro`, `qwen3.8-max`, + // `qwen3.7-plus` and `deepseek-v4-flash-0731` each appear under two groups). expect(catalogEntryFor("anthropic", "claude-sonnet-4-6")?.displayName).toBe( "Claude Sonnet 4.6", ); @@ -121,6 +111,14 @@ describe("model-catalog", () => { expect(catalogEntryFor("openrouter", "xiaomi/mimo-v2.5")?.displayName).toBe("MiMo-V2.5"); expect(catalogEntryFor("custom", "my-own")).toBeUndefined(); // Each group's entry for a resold id is reached only through that group. + expect(catalogEntryFor("qwen-token-plan", "qwen3.8-max")?.provider).toBe("qwen-token-plan"); + expect(catalogEntryFor("qwen-pay-as-you-go", "qwen3.8-max")?.provider).toBe( + "qwen-pay-as-you-go", + ); + // The bare resold id never leaks into the vendor's own group (deepseek's flash revision is + // sold there as deepseek-v4-flash, without the date suffix). + expect(catalogEntryFor("deepseek", "deepseek-v4-flash-0731")).toBeUndefined(); + expect(catalogEntryFor("deepseek", "deepseek-v4-flash")?.provider).toBe("deepseek"); expect(catalogEntryFor("zhipu", "glm-5.2")?.contextWindow).toBe(1000000); expect(catalogEntryFor("qwen-token-plan", "glm-5.2")?.contextWindow).toBe(1048576); expect(catalogEntryFor("deepseek", "deepseek-v4-pro")?.provider).toBe("deepseek"); @@ -157,6 +155,7 @@ describe("model-catalog", () => { "anthropic/claude-opus-4.8", "anthropic/claude-opus-4.7", "anthropic/claude-sonnet-5", + "deepseek/deepseek-v4-flash-0731", "deepseek/deepseek-v4-flash", "deepseek/deepseek-v4-pro", "google/gemini-3.6-flash", @@ -167,11 +166,11 @@ describe("model-catalog", () => { "moonshotai/kimi-k3", "moonshotai/kimi-k2.6", "nvidia/nemotron-3-ultra-550b-a55b:free", + "openai/gpt-5.6-luna", "openai/gpt-5.6-sol", "openai/gpt-5.6-terra", "openai/gpt-5.5", "openrouter/free", - "poolside/laguna-m.1:free", "qwen/qwen3.6-35b-a3b", "stepfun/step-3.7-flash", "tencent/hy3", @@ -216,28 +215,29 @@ describe("model-catalog", () => { } const qtp = MODEL_CATALOG.filter((m) => m.provider === "qwen-token-plan"); expect(qtp.map((m) => m.modelId)).toEqual([ + "deepseek-v4-flash-0731", "deepseek-v4-pro", "glm-5.2", - "qwen3.8-max-preview", - "qwen3.7-max", + "qwen3.8-max", "qwen3.7-plus", ]); for (const m of qtp) { expect(m.clientType).toBe("openai"); expect(m.baseUrl).toBe("https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1"); } - // Vision flags per the plan's supported-model table: 3.8-max-preview and 3.7-plus see images. + // Vision flags per the plan's supported-model table: 3.8-max and 3.7-plus see images. expect(qtp.map((m) => [m.modelId, m.supportsVision])).toEqual([ + ["deepseek-v4-flash-0731", false], ["deepseek-v4-pro", false], ["glm-5.2", false], - ["qwen3.8-max-preview", true], - ["qwen3.7-max", false], + ["qwen3.8-max", true], ["qwen3.7-plus", true], ]); const qpayg = MODEL_CATALOG.filter((m) => m.provider === "qwen-pay-as-you-go"); expect(qpayg.map((m) => [m.modelId, m.supportsVision])).toEqual([ + ["deepseek-v4-flash-0731", false], ["kimi/kimi-k3", true], - ["qwen3.7-max", false], + ["qwen3.8-max", true], ["qwen3.7-plus", true], ["ZHIPU/GLM-5.2", false], ]); @@ -280,11 +280,16 @@ describe("model-catalog", () => { } } const gateway = [...or, ...fw, ...sf, ...qtp, ...qpayg]; - // Pricing (USD): MiMo v2.5 and Hy3. + // Pricing (USD, per the 2026-08-03 models-API re-read): MiMo v2.5 and Hy3 publish a real + // cache-hit price and no per-token write premium, so cache_write carries the input price. const mimo = MODEL_CATALOG.find((m) => m.modelId === "xiaomi/mimo-v2.5")!.pricing!; expect([mimo.cache_read, mimo.cache_write, mimo.output]).toEqual([0.0028, 0.14, 0.28]); const hy3 = MODEL_CATALOG.find((m) => m.modelId === "tencent/hy3")!.pricing!; - expect([hy3.cache_read, hy3.cache_write, hy3.output]).toEqual([0.035, 0.14, 0.58]); + expect([hy3.cache_read, hy3.cache_write, hy3.output]).toEqual([0.033, 0.132, 0.528]); + // Anthropic/GPT rows publish a genuine 1.25x per-token cache-write premium; it is stored + // as-is (cache_write > input would be wrong to collapse back to input). + const sonnet5 = catalogEntryFor("openrouter", "anthropic/claude-sonnet-5")!.pricing!; + expect([sonnet5.cache_read, sonnet5.cache_write, sonnet5.output]).toEqual([0.2, 2.5, 10]); // Gemini 3.6 Flash and 3.5 Flash Lite: upstream publishes a cache-hit price, so cache_read // stores the real discounted price (not the input price) — cache_read is its own billing // bucket in the cost center. cache_write repeats input (no per-token cache-write fee). @@ -453,9 +458,9 @@ describe("resolveModelEnv (PRN-021: env fallback resolved by AgentHub routing ru expect(modelHomepageUrl("qwen-pay-as-you-go", "ZHIPU/GLM-5.2")).toBe( "https://www.qianwenai.com/models/ZHIPU%2FGLM-5.2", ); - // The preview model has no dedicated page: falls back to the plan's model overview. - expect(modelHomepageUrl("qwen-token-plan", "qwen3.8-max-preview")).toBe( - providerInfo("qwen-token-plan")!.modelsUrl, + // Token Plan models link to their qianwenai model page (bare ids, no encoding needed). + expect(modelHomepageUrl("qwen-token-plan", "qwen3.8-max")).toBe( + "https://www.qianwenai.com/models/qwen3.8-max", ); // Direct vendors link to the vendor's model docs page. expect(modelHomepageUrl("deepseek", "deepseek-v4-pro")).toBe( diff --git a/packages/core/test/state.test.ts b/packages/core/test/state.test.ts index 1a9e0ea..7179998 100644 --- a/packages/core/test/state.test.ts +++ b/packages/core/test/state.test.ts @@ -1143,9 +1143,9 @@ describe("project-config round trip", () => { expect(entry?.vision).toBeUndefined(); }); - it("default config presets the full model catalog (default = deepseek deepseek-v4-pro)", () => { + it("default config presets the full model catalog (default = deepseek deepseek-v4-flash)", () => { const cfg = defaultProjectConfig(); - expect(cfg.default_model).toEqual({ provider: "deepseek", model_id: "deepseek-v4-pro" }); + expect(cfg.default_model).toEqual({ provider: "deepseek", model_id: "deepseek-v4-flash" }); // The catalog is presented in full: provider and model_id are separate columns, model_id // being the plain upstream id (vision is only persisted as false for models that don't // support images). @@ -1157,8 +1157,8 @@ describe("project-config round trip", () => { (c) => c.provider === entry.provider && c.modelId === entry.model_id, )!; expect(entry.vision).toBe(cat.supportsVision ? undefined : false); - // A catalog entry without a list price (the Token Plan preview model) presets no - // pricing; every other catalog entry stores USD pricing. + // A catalog entry without a list price would preset no pricing (none currently); + // every priced catalog entry stores USD pricing. if (cat.pricing === undefined) expect(entry.pricing).toBeUndefined(); else expect(entry.pricing?.unit).toBe("usd_per_mtok"); // A model that auto-routes leaves client_type unset; a gateway model (OpenRouter) diff --git a/packages/docs/content/configuration.en.md b/packages/docs/content/configuration.en.md index 8b0025f..b0f0b4e 100644 --- a/packages/docs/content/configuration.en.md +++ b/packages/docs/content/configuration.en.md @@ -67,11 +67,11 @@ Model entry (`[[models]]`) fields: | `created_at` | Write timestamp of `api_key` (ISO 8601; a display field maintained by the interface layer) | ```toml -default_model = { provider = "deepseek", model_id = "deepseek-v4-pro" } +default_model = { provider = "deepseek", model_id = "deepseek-v4-flash" } [[models]] provider = "deepseek" -model_id = "deepseek-v4-pro" +model_id = "deepseek-v4-flash" context_window = 1000000 vision = false api_key = "sk-..." diff --git a/packages/docs/content/configuration.zh.md b/packages/docs/content/configuration.zh.md index 96ceb4d..2c0a599 100644 --- a/packages/docs/content/configuration.zh.md +++ b/packages/docs/content/configuration.zh.md @@ -67,11 +67,11 @@ openrouter、fireworks、siliconflow、qwen-token-plan、qwen-pay-as-you-go 与 | `created_at` | `api_key` 写入时间(ISO 8601,界面维护的展示字段) | ```toml -default_model = { provider = "deepseek", model_id = "deepseek-v4-pro" } +default_model = { provider = "deepseek", model_id = "deepseek-v4-flash" } [[models]] provider = "deepseek" -model_id = "deepseek-v4-pro" +model_id = "deepseek-v4-flash" context_window = 1000000 vision = false api_key = "sk-..." diff --git a/packages/docs/content/models.en.md b/packages/docs/content/models.en.md index b097fa9..e5c24ad 100644 --- a/packages/docs/content/models.en.md +++ b/packages/docs/content/models.en.md @@ -29,17 +29,17 @@ Each Project's available models are recorded in the hidden `.project_config.toml | `pricing` | Three price buckets (unit `usd_per_mtok`, USD per million tokens): `cache_read` / `cache_write` / `output` | | `api_key` / `base_url` | Inlined credentials, both optional; when blank, AgentHub falls back to environment variables | -A fresh Project defaults to deepseek-v4-pro. A `vision_model` entry can additionally designate the proxy model that `describe_image` uses for text-only session models (see [Tools & Approval](/tools)); it is unset by default. +A fresh Project defaults to deepseek-v4-flash. A `vision_model` entry can additionally designate the proxy model that `describe_image` uses for text-only session models (see [Tools & Approval](/tools)); it is unset by default. File shape (illustrative): ```toml -default_model = { provider = "deepseek", model_id = "deepseek-v4-pro" } +default_model = { provider = "deepseek", model_id = "deepseek-v4-flash" } vision_model = { provider = "google", model_id = "gemini-3.1-pro-preview" } [[models]] provider = "deepseek" -model_id = "deepseek-v4-pro" +model_id = "deepseek-v4-flash" context_window = 1000000 [[models]] @@ -73,9 +73,9 @@ Built-in groups and their env-var fallbacks (catalog source: `packages/core/src/ The gateway groups (openrouter / fireworks / siliconflow / qwen-token-plan / qwen-pay-as-you-go) go through AgentHub's OpenAI client, so with blank credentials they read `OPENAI_API_KEY` — not a gateway-specific variable. -The preset catalog also carries OpenRouter's free tier: `:free` model variants (e.g. `inclusionai/ling-3.0-flash:free`, `poolside/laguna-m.1:free`) and the `openrouter/free` unified Free Models Router. They cost nothing, but are subject to OpenRouter's free-tier rate limits and data policy. +The preset catalog also carries OpenRouter's free tier: `:free` model variants (e.g. `inclusionai/ling-3.0-flash:free`, `nvidia/nemotron-3-ultra-550b-a55b:free`) and the `openrouter/free` unified Free Models Router. They cost nothing, but are subject to OpenRouter's free-tier rate limits and data policy. -Some models in the preset catalog: deepseek-v4-pro / deepseek-v4-flash, gemini-3.1-pro-preview, claude-opus-4-8 / claude-sonnet-4-6, gpt-5.5, glm-5.2, kimi-k2.6, qwen3.8-max-preview (not exhaustive). +Some models in the preset catalog: deepseek-v4-pro / deepseek-v4-flash, gemini-3.1-pro-preview, claude-opus-4-8 / claude-sonnet-4-6, gpt-5.5, glm-5.2, kimi-k2.6, qwen3.8-max (not exhaustive). ## Thinking levels diff --git a/packages/docs/content/models.zh.md b/packages/docs/content/models.zh.md index 94852b1..9194577 100644 --- a/packages/docs/content/models.zh.md +++ b/packages/docs/content/models.zh.md @@ -29,17 +29,17 @@ description: 经 AgentHub 单一网关接入模型,以 (provider, model_id) | `pricing` | 三档价格(单位 `usd_per_mtok`,USD 每百万 Token):`cache_read` / `cache_write` / `output` | | `api_key` / `base_url` | 内联凭证,可留空;留空时 AgentHub 回退读环境变量 | -新建 Project 的默认模型是 deepseek-v4-pro。另可配置一条 `vision_model`,作为 text-only 模型使用 `describe_image` 时的代读模型(见 [工具与审批](/tools));默认不配置。 +新建 Project 的默认模型是 deepseek-v4-flash。另可配置一条 `vision_model`,作为 text-only 模型使用 `describe_image` 时的代读模型(见 [工具与审批](/tools));默认不配置。 文件形态(示意): ```toml -default_model = { provider = "deepseek", model_id = "deepseek-v4-pro" } +default_model = { provider = "deepseek", model_id = "deepseek-v4-flash" } vision_model = { provider = "google", model_id = "gemini-3.1-pro-preview" } [[models]] provider = "deepseek" -model_id = "deepseek-v4-pro" +model_id = "deepseek-v4-flash" context_window = 1000000 [[models]] @@ -73,9 +73,9 @@ api_key = "sk-..." 网关分组(openrouter / fireworks / siliconflow / qwen-token-plan / qwen-pay-as-you-go)经 AgentHub 的 OpenAI 客户端请求,因此凭证留空时读取的是 `OPENAI_API_KEY`,而非网关自己的变量名。 -预置目录还收录了 OpenRouter 的免费档:`:free` 模型变体(如 `inclusionai/ling-3.0-flash:free`、`poolside/laguna-m.1:free`)与统一路由 `openrouter/free`(Free Models Router),零成本可用,但受 OpenRouter 免费档速率限制与数据政策约束。 +预置目录还收录了 OpenRouter 的免费档:`:free` 模型变体(如 `inclusionai/ling-3.0-flash:free`、`nvidia/nemotron-3-ultra-550b-a55b:free`)与统一路由 `openrouter/free`(Free Models Router),零成本可用,但受 OpenRouter 免费档速率限制与数据政策约束。 -预置目录中的部分模型:deepseek-v4-pro / deepseek-v4-flash、gemini-3.1-pro-preview、claude-opus-4-8 / claude-sonnet-4-6、gpt-5.5、glm-5.2、kimi-k2.6、qwen3.8-max-preview 等(非完整清单)。 +预置目录中的部分模型:deepseek-v4-pro / deepseek-v4-flash、gemini-3.1-pro-preview、claude-opus-4-8 / claude-sonnet-4-6、gpt-5.5、glm-5.2、kimi-k2.6、qwen3.8-max 等(非完整清单)。 ## 思考等级 diff --git a/packages/docs/content/quickstart.en.md b/packages/docs/content/quickstart.en.md index 6df4b3c..fb12a52 100644 --- a/packages/docs/content/quickstart.en.md +++ b/packages/docs/content/quickstart.en.md @@ -18,7 +18,7 @@ For other options (npm, from source), see [Installation](/installation). PenguinHarness ships with no built-in model credentials, so configure a model first. Use the Models page in the Web UI, or the CLI: ```bash -penguin config model add --provider deepseek --model-id deepseek-v4-pro --api-key sk-... --set-default +penguin config model add --provider deepseek --model-id deepseek-v4-flash --api-key sk-... --set-default ``` - A model is always referenced as a `(provider, model_id)` pair, so `--provider` and `--model-id` are both required — the Provider is never inferred from the model id. See [Models & Providers](/models) for the built-in groups. diff --git a/packages/docs/content/quickstart.zh.md b/packages/docs/content/quickstart.zh.md index 39b0ddd..3121597 100644 --- a/packages/docs/content/quickstart.zh.md +++ b/packages/docs/content/quickstart.zh.md @@ -18,7 +18,7 @@ curl -fsSL https://penguin.ooo/install.sh | sh PenguinHarness 不内置任何模型凭据,使用前需要先配置一个模型。可以在 Web UI 的 Models 页面完成,也可以用 CLI: ```bash -penguin config model add --provider deepseek --model-id deepseek-v4-pro --api-key sk-... --set-default +penguin config model add --provider deepseek --model-id deepseek-v4-flash --api-key sk-... --set-default ``` - 模型引用始终是 `(provider, model_id)` 二元组,因此 `--provider` 与 `--model-id` 均为必填——Provider 绝不由模型 id 推断。内置分组见[模型与 Provider](/models)。 diff --git a/packages/server/test/auth.test.ts b/packages/server/test/auth.test.ts index 2c1f9ee..38063b0 100644 --- a/packages/server/test/auth.test.ts +++ b/packages/server/test/auth.test.ts @@ -79,7 +79,7 @@ describe("auth", () => { ); expect(toml).toContain('name = "bob"'); expect(toml).toContain( - 'default_model = { provider = "deepseek", model_id = "deepseek-v4-pro" }', + 'default_model = { provider = "deepseek", model_id = "deepseek-v4-flash" }', ); }); diff --git a/packages/server/test/models.test.ts b/packages/server/test/models.test.ts index 73e2031..2e278e7 100644 --- a/packages/server/test/models.test.ts +++ b/packages/server/test/models.test.ts @@ -97,7 +97,7 @@ describe("models preset & catalog enrichment", () => { const res = await api.get(url()); expect(res.status).toBe(200); const body = (await res.json()) as ModelsResponse; - expect(body.defaultModel).toEqual({ provider: "deepseek", modelId: "deepseek-v4-pro" }); + expect(body.defaultModel).toEqual({ provider: "deepseek", modelId: "deepseek-v4-flash" }); expect(body.models.map(pairKey)).toEqual(catalogPairs); const sonnet = pick(body, "anthropic", "claude-sonnet-4-6"); @@ -112,10 +112,10 @@ describe("models preset & catalog enrichment", () => { expect(sonnet.credential).toBeUndefined(); expect(sonnet.clientType).toBeUndefined(); - const deepseek = pick(body, "deepseek", "deepseek-v4-flash"); + const deepseek = pick(body, "deepseek", "deepseek-v4-pro"); expect(deepseek.vision).toBe(false); expect(deepseek.envKey).toBe("DEEPSEEK_API_KEY"); - expect(pick(body, "deepseek", "deepseek-v4-pro").isDefault).toBe(true); + expect(pick(body, "deepseek", "deepseek-v4-flash").isDefault).toBe(true); // OpenRouter gateway model: the upstream id contains `/`, but under column storage it's just a // plain string; openai protocol + a preset base URL inlined on the entry (no secret). @@ -255,7 +255,7 @@ describe("default_project presets", () => { const res = await api.get("/api/projects/default_project/models"); expect(res.status).toBe(200); const body = (await res.json()) as ModelsResponse; - expect(body.defaultModel).toEqual({ provider: "deepseek", modelId: "deepseek-v4-pro" }); + expect(body.defaultModel).toEqual({ provider: "deepseek", modelId: "deepseek-v4-flash" }); expect(body.models.map(pairKey)).toEqual(catalogPairs); // The point of presets is "works out of the box": creating a Session should succeed without passing a model ref. @@ -266,7 +266,7 @@ describe("default_project presets", () => { expect(created.status).toBe(201); const { session } = (await created.json()) as SessionCreateResponse; expect(session.provider).toBe("deepseek"); - expect(session.modelId).toBe("deepseek-v4-pro"); + expect(session.modelId).toBe("deepseek-v4-flash"); }); it("a default_project that already has models configured is left untouched (existing CLI config is not overwritten)", async () => { diff --git a/packages/skills/skills/agenthub-models/SKILL.md b/packages/skills/skills/agenthub-models/SKILL.md index 558b0ff..a49ef33 100644 --- a/packages/skills/skills/agenthub-models/SKILL.md +++ b/packages/skills/skills/agenthub-models/SKILL.md @@ -3,7 +3,7 @@ name: agenthub-models description: Call model APIs through @prismshadow/agenthub — streaming text generation, image generation, speech synthesis, embeddings and the supported-model registry with one client. short_description: Call model APIs with one AgentHub client. short_description_zh: 用一个 AgentHub 客户端调用模型 API。 -version: 8 +version: 9 updated: 2026-07-22T00:00:00Z --- @@ -60,7 +60,7 @@ Use exact model ids. If an id is not in the table below and the user has not giv | OpenAI embedding | `text-embedding-3-small`, `text-embedding-3-large` | — | | Kimi K3 | `kimi-k3` | OpenRouter `moonshotai/kimi-k3` | | Kimi K2.6 | `kimi-k2.6` | OpenRouter `moonshotai/kimi-k2.6`; SiliconFlow `Pro/moonshotai/Kimi-K2.6` | -| DeepSeek V4 | `deepseek-v4-pro`, `deepseek-v4-flash` | OpenRouter `deepseek/deepseek-v4-pro`, `deepseek/deepseek-v4-flash`; SiliconFlow `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | +| DeepSeek V4 | `deepseek-v4-pro`, `deepseek-v4-flash` | OpenRouter `deepseek/deepseek-v4-pro`, `deepseek/deepseek-v4-flash`, `deepseek/deepseek-v4-flash-0731`; SiliconFlow `deepseek-ai/DeepSeek-V4-Pro`, `deepseek-ai/DeepSeek-V4-Flash` | | GLM 5.2 | `glm-5.2` | OpenRouter `z-ai/glm-5.2`; SiliconFlow `zai-org/GLM-5.2` | | GLM 5.1 | `glm-5.1` | OpenRouter `z-ai/glm-5.1`; SiliconFlow `Pro/zai-org/GLM-5.1` | | Qwen 3.6 | — | OpenRouter `qwen/qwen3.6-35b-a3b`; SiliconFlow `Qwen/Qwen3.6-35B-A3B` | diff --git a/packages/skills/skills/penguin-sdk/SKILL.md b/packages/skills/skills/penguin-sdk/SKILL.md index c943057..3e20a48 100644 --- a/packages/skills/skills/penguin-sdk/SKILL.md +++ b/packages/skills/skills/penguin-sdk/SKILL.md @@ -3,7 +3,7 @@ name: penguin-sdk description: Build AI apps on the Penguin Harness SDK — self-contained projects, the createSession/run streaming loop with thinking and image messages, and a complete RAG recipe that ingests documents into a knowledge base and answers with citations behind a web UI. short_description: Build AI and RAG apps on the Penguin Harness SDK. short_description_zh: 基于 Penguin SDK 构建 AI 与 RAG 应用。 -version: 17 +version: 18 updated: 2026-07-30T11:10:00Z --- @@ -56,7 +56,7 @@ If the package is not on your npm registry (it is developed in the PenguinHarnes Configure a model for the app's data root, in this order — stop at the first that works: 1. `penguin config model add --root --provider --model-id --api-key [--base-url ] [--client-type openai] --set-default` — prefer `--client-type openai --base-url ` (works with any OpenAI-compatible endpoint; exact ids in the agenthub-models skill). `--provider` is required: a model is always the `(provider, model_id)` pair and the group is never inferred from the id (`custom` for an endpoint outside the built-in groups). -2. Environment variables cover the **credential only** (`DEEPSEEK_API_KEY`, `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, …) — model selection still comes from the project config, whose preset default is `deepseek-v4-pro`. Env-only setup therefore works out of the box only with `DEEPSEEK_API_KEY`; for another vendor either run the CLI command above or pass a configured `{ provider, modelId }` pair to `createSession`. +2. Environment variables cover the **credential only** (`DEEPSEEK_API_KEY`, `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, …) — model selection still comes from the project config, whose preset default is `deepseek-v4-flash`. Env-only setup therefore works out of the box only with `DEEPSEEK_API_KEY`; for another vendor either run the CLI command above or pass a configured `{ provider, modelId }` pair to `createSession`. Keep model API keys **project-local**: configure them with the penguin CLI into the app's own data root under the working directory, so the project stays self-contained and movable. When building an AI app, **always pass `--root ` pointing at the app's data directory inside the current working directory** (the same path you give `createAgent({ root })`, e.g. `./penguin_data`) — never run `penguin config ...` without `--root`, or it writes to the global `~/.penguin/data` instead of the project. Never read, copy or fall back to model keys stored in the user's global `~/.penguin` directory — that config belongs to the person running Penguin, not to the app you are building.