From 012c8a7d89047d5889fef9ac45c281cc5e43565d Mon Sep 17 00:00:00 2001 From: Hong Jiarong Date: Wed, 8 Jul 2026 11:48:16 +0800 Subject: [PATCH] fix(hub): default to OpenRouter Sonnet model --- hub/.env.example | 5 ++++- hub/src/agent/defaultModels.ts | 32 ++++++++++++++++++++++++++++ hub/src/server.ts | 18 +++++----------- hub/test/unit/default-models.test.ts | 25 ++++++++++++++++++++++ 4 files changed, 66 insertions(+), 14 deletions(-) create mode 100644 hub/src/agent/defaultModels.ts create mode 100644 hub/test/unit/default-models.test.ts diff --git a/hub/.env.example b/hub/.env.example index 32ed939..e8024ec 100644 --- a/hub/.env.example +++ b/hub/.env.example @@ -10,7 +10,10 @@ ANTHROPIC_BASE_URL="https://openrouter.ai/api" ANTHROPIC_AUTH_TOKEN="" ANTHROPIC_API_KEY="" -# Model override (optional). Defaults to Claude Sonnet. To use GLM: +# Model override (optional). Defaults to anthropic/claude-sonnet-5. +# Use an OpenRouter model slug, for example: +# ANTHROPIC_DEFAULT_SONNET_MODEL="~anthropic/claude-sonnet-latest" +# To use GLM: # ANTHROPIC_DEFAULT_SONNET_MODEL="z-ai/glm-4.7" # ANTHROPIC_DEFAULT_OPUS_MODEL="z-ai/glm-4.7" # ANTHROPIC_DEFAULT_HAIKU_MODEL="z-ai/glm-4.6" diff --git a/hub/src/agent/defaultModels.ts b/hub/src/agent/defaultModels.ts new file mode 100644 index 0000000..91103e6 --- /dev/null +++ b/hub/src/agent/defaultModels.ts @@ -0,0 +1,32 @@ +import { InMemoryModelRegistry } from "./models.js"; + +type Env = Readonly>; + +const DEFAULT_SONNET_MODEL = "anthropic/claude-sonnet-5"; +const DEFAULT_SONNET_LABEL = "Claude Sonnet 5"; + +function readEnv(env: Env, name: string): string | undefined { + const value = env[name]?.trim(); + return value === undefined || value === "" ? undefined : value; +} + +export function defaultSonnetModel(env: Env = process.env): string { + return readEnv(env, "ANTHROPIC_DEFAULT_SONNET_MODEL") ?? DEFAULT_SONNET_MODEL; +} + +export function createDefaultModelRegistry(env: Env = process.env): InMemoryModelRegistry { + const sonnetModel = defaultSonnetModel(env); + const sonnetLabel = + readEnv(env, "ANTHROPIC_DEFAULT_SONNET_MODEL_NAME") ?? + (sonnetModel === DEFAULT_SONNET_MODEL ? DEFAULT_SONNET_LABEL : sonnetModel); + + return new InMemoryModelRegistry( + [ + { id: sonnetModel, label: sonnetLabel, toolCapable: true }, + ], + [ + { id: "draft", label: "草稿", defaultModel: sonnetModel }, + { id: "review", label: "审校", defaultModel: sonnetModel }, + ], + ); +} diff --git a/hub/src/server.ts b/hub/src/server.ts index d78c60c..f8f2a3a 100644 --- a/hub/src/server.ts +++ b/hub/src/server.ts @@ -8,12 +8,12 @@ * loop are ADR-0003-authorized (chat must match binding). * * Env (see .env.example): - * DATABASE_URL, OPENROUTER_API_KEY, FEISHU_APP_ID/SECRET/BOT_OPEN_ID, PORT + * DATABASE_URL, ANTHROPIC_AUTH_TOKEN, FEISHU_APP_ID/SECRET/BOT_OPEN_ID, PORT */ import "dotenv/config"; import Fastify from "fastify"; import { prisma } from "./db.js"; -import { InMemoryModelRegistry } from "./agent/models.js"; +import { createDefaultModelRegistry } from "./agent/defaultModels.js"; import { createLarkClient, startFeishuListenerWithClient, type FeishuRuntime } from "./feishu/client.js"; import { makeTriggerHandler } from "./feishu/trigger.js"; @@ -54,17 +54,9 @@ async function main(): Promise { app.get("/api/healthz", async () => ({ ok: true, ts: Date.now() })); - // Model registry (role-as-data, ADR-0017). Claude Code SDK uses model - // aliases like 'sonnet', 'haiku' — the registry maps roles to these. - const models = new InMemoryModelRegistry( - [ - { id: "claude-sonnet-4-20250514", label: "Claude Sonnet", toolCapable: true }, - ], - [ - { id: "draft", label: "草稿", defaultModel: "claude-sonnet-4-20250514" }, - { id: "review", label: "审校", defaultModel: "claude-sonnet-4-20250514" }, - ], - ); + // Model registry (role-as-data, ADR-0017). Defaults are OpenRouter model + // slugs; ANTHROPIC_DEFAULT_SONNET_MODEL can override the role default. + const models = createDefaultModelRegistry(); // --- Feishu listener --- const feishuConfig = { appId: feishuAppId, appSecret: feishuAppSecret, botOpenId: feishuBotOpenId }; diff --git a/hub/test/unit/default-models.test.ts b/hub/test/unit/default-models.test.ts new file mode 100644 index 0000000..8253770 --- /dev/null +++ b/hub/test/unit/default-models.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from "vitest"; +import { createDefaultModelRegistry } from "../../src/agent/defaultModels.js"; + +describe("default model registry", () => { + it("uses OpenRouter model slugs for Sonnet defaults", () => { + const models = createDefaultModelRegistry({}); + + expect(models.listModels()[0]).toMatchObject({ + id: "anthropic/claude-sonnet-5", + label: "Claude Sonnet 5", + toolCapable: true, + }); + expect(models.resolve(undefined, "draft")).toBe("anthropic/claude-sonnet-5"); + expect(models.resolve(undefined, "review")).toBe("anthropic/claude-sonnet-5"); + }); + + it("honors ANTHROPIC_DEFAULT_SONNET_MODEL when configured", () => { + const models = createDefaultModelRegistry({ + ANTHROPIC_DEFAULT_SONNET_MODEL: "z-ai/glm-4.7", + }); + + expect(models.listModels()[0]?.id).toBe("z-ai/glm-4.7"); + expect(models.resolve(undefined, "draft")).toBe("z-ai/glm-4.7"); + }); +});