-
Notifications
You must be signed in to change notification settings - Fork 0
fix(api): pin Wiki Compose LLM to google:gemini-3.5-flash #990
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| /** | ||
| * Tests for fixed Wiki Compose model id resolution. | ||
| */ | ||
| import { describe, expect, it, vi, beforeEach } from "vitest"; | ||
| import { | ||
| resolveWikiComposeModelId, | ||
| WIKI_COMPOSE_MODEL_ID, | ||
| } from "../../../../agents/core/llm/wikiComposeModelId.js"; | ||
|
|
||
| const mockDb = { | ||
| select: vi.fn(), | ||
| }; | ||
|
|
||
| function chainLimit(rows: unknown[]) { | ||
| const chain = { | ||
| from: vi.fn().mockReturnThis(), | ||
| where: vi.fn().mockReturnThis(), | ||
| limit: vi.fn().mockResolvedValue(rows), | ||
| }; | ||
| return chain; | ||
| } | ||
|
|
||
| beforeEach(() => { | ||
| vi.clearAllMocks(); | ||
| }); | ||
|
|
||
| describe("resolveWikiComposeModelId", () => { | ||
| it("returns the fixed id when the row is active and tier-accessible", async () => { | ||
| mockDb.select.mockReturnValueOnce(chainLimit([{ id: WIKI_COMPOSE_MODEL_ID }])); | ||
| const id = await resolveWikiComposeModelId("orchestrator", "free", mockDb as never); | ||
| expect(id).toBe(WIKI_COMPOSE_MODEL_ID); | ||
| }); | ||
|
|
||
| it("returns the fixed id even when no DB row matches", async () => { | ||
| mockDb.select.mockReturnValueOnce(chainLimit([])); | ||
| const id = await resolveWikiComposeModelId("draft", "pro", mockDb as never); | ||
| expect(id).toBe("google:gemini-3.5-flash"); | ||
| }); | ||
|
|
||
| it("uses the same id for orchestrator and draft roles", async () => { | ||
| mockDb.select.mockReturnValue(chainLimit([{ id: WIKI_COMPOSE_MODEL_ID }])); | ||
| const orchestrator = await resolveWikiComposeModelId("orchestrator", "free", mockDb as never); | ||
| const draft = await resolveWikiComposeModelId("draft", "free", mockDb as never); | ||
| expect(orchestrator).toBe(draft); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,13 +2,12 @@ | |
| * Pre-flight BYOK checks for Wiki Compose session creation (#951). | ||
| * Wiki Compose セッション作成前の BYOK 事前チェック(#951)。 | ||
| * | ||
| * Static env model ids are not validated here — provider matching is enforced at | ||
| * runtime via {@link resolveComposeModelId}. This function only verifies that | ||
| * the user has a stored credential when the graph will call an LLM. | ||
| * Wiki Compose uses a fixed Google model at runtime ({@link WIKI_COMPOSE_MODEL_ID}). | ||
| * BYOK sessions still need a stored credential when the graph calls an LLM; provider | ||
| * alignment with the fixed model is the caller's responsibility for now. | ||
| * | ||
| * 静的 env モデル id との provider 照合は行わない。実行時の | ||
| * `resolveComposeModelId` が provider 整合を担保する。本関数は LLM を呼ぶ | ||
| * グラフで credential が存在するかだけを確認する。 | ||
| * Wiki Compose は実行時に Google 固定モデルを使う。BYOK 時は LLM 呼び出し前に | ||
| * credential の有無だけをここで確認する。 | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since Wiki Compose is now pinned to To prevent runtime crashes and provide a clear error message, we should update export async function assertComposeBackendReady(input: {
backend: ExecutionBackend;
graphId: string;
userId: string;
tier: UserTier;
db: Database;
}): Promise<void> {
if (!isUserByokBackend(input.backend)) return;
const modelIds = getComposeModelIdsForGraph(input.graphId);
if (modelIds.length === 0) return;
const expectedProvider = backendToCredentialProvider(input.backend);
// Reject non-Google BYOK backends early for Wiki Compose
if (modelIds.includes(WIKI_COMPOSE_MODEL_ID) && expectedProvider !== "google") {
throw new HTTPException(400, {
message: `Wiki Compose is currently pinned to Google models. Backend "${input.backend}" is incompatible.`,
});
}
const key = await getUserAiCredentialPlaintext(input.userId, expectedProvider, input.db);
if (!key?.trim()) {
throw new HTTPException(400, {
message: `No API credential configured for backend "${input.backend}"`,
});
}
} |
||
| */ | ||
| import { HTTPException } from "hono/http-exception"; | ||
| import type { Database, UserTier } from "../../types/index.js"; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,54 @@ | ||
| /** | ||
| * Fixed Wiki Compose model id (temporary until per-role / per-backend selection returns). | ||
| * Wiki Compose 用の固定モデル id(将来ロール別選択に戻すまでの暫定)。 | ||
| */ | ||
| import { and, eq } from "drizzle-orm"; | ||
| import { aiModels } from "../../../schema/index.js"; | ||
| import type { Database, UserTier } from "../../../types/index.js"; | ||
| import type { ComposeModelRole } from "./resolveComposeModelId.js"; | ||
|
|
||
| /** | ||
| * `ai_models.id` used by every Wiki Compose LLM node and the `web_search` tool. | ||
| * すべての Wiki Compose LLM ノードと `web_search` ツールが使う `ai_models.id`。 | ||
| */ | ||
| export const WIKI_COMPOSE_MODEL_ID = "google:gemini-3.5-flash" as const; | ||
|
|
||
| function tierFilter(tier: UserTier) { | ||
| if (tier === "pro") return undefined; | ||
| return eq(aiModels.tierRequired, "free"); | ||
| } | ||
|
|
||
| /** | ||
| * When the fixed row exists and is accessible, return it; otherwise still return | ||
| * {@link WIKI_COMPOSE_MODEL_ID} so callers fail consistently in `validateModelAccess`. | ||
| * 行が active かつ tier 的に使えるときだけ DB 上の id を返し、それ以外は固定 id を返す。 | ||
| */ | ||
| async function fixedModelIdIfAccessible(db: Database, tier: UserTier): Promise<string> { | ||
| const tierClause = tierFilter(tier); | ||
| const [row] = await db | ||
| .select({ id: aiModels.id }) | ||
| .from(aiModels) | ||
| .where( | ||
| and( | ||
| eq(aiModels.id, WIKI_COMPOSE_MODEL_ID), | ||
| eq(aiModels.isActive, true), | ||
| ...(tierClause ? [tierClause] : []), | ||
| ), | ||
| ) | ||
| .limit(1); | ||
| return row?.id ?? WIKI_COMPOSE_MODEL_ID; | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The database query to resolve the active and accessible fixed model ID is duplicated in export async function resolveActiveWikiComposeModelId(
db: Database,
tier: UserTier,
): Promise<string | null> {
const tierClause = tierFilter(tier);
const [row] = await db
.select({ id: aiModels.id })
.from(aiModels)
.where(
and(
eq(aiModels.id, WIKI_COMPOSE_MODEL_ID),
eq(aiModels.isActive, true),
...(tierClause ? [tierClause] : []),
),
)
.limit(1);
return row?.id ?? null;
}
async function fixedModelIdIfAccessible(db: Database, tier: UserTier): Promise<string> {
return (await resolveActiveWikiComposeModelId(db, tier)) ?? WIKI_COMPOSE_MODEL_ID;
} |
||
|
|
||
| /** | ||
| * Resolve the model row id for Wiki Compose orchestrator / draft / research nodes. | ||
| * `role` is accepted for API stability; all roles map to {@link WIKI_COMPOSE_MODEL_ID} for now. | ||
| * | ||
| * Wiki Compose の model id 解決。現時点では role に関わらず gemini-3.5-flash 固定。 | ||
| */ | ||
| export async function resolveWikiComposeModelId( | ||
| _role: ComposeModelRole, | ||
| _tier: UserTier, | ||
| db: Database, | ||
| ): Promise<string> { | ||
| return fixedModelIdIfAccessible(db, _tier); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with 👍 / 👎. |
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,13 +5,14 @@ | |
| * `useGoogleSearch` for Google) を呼ぶため、Anthropic-only な選択では成立しない。 | ||
| * 本ヘルパは次の優先順で model を選ぶ: | ||
| * | ||
| * 1. `process.env.WIKI_COMPOSE_WEB_SEARCH_MODEL_ID` (explicit override; `ai_models.id`) | ||
| * 1. {@link WIKI_COMPOSE_MODEL_ID} when active and tier-accessible | ||
| * 2. `process.env.WIKI_COMPOSE_WEB_SEARCH_MODEL_ID` (explicit override; `ai_models.id`) | ||
| * — 必ず active かつ tier 通過することを DB 側で確認する(coderabbit review #956: | ||
| * 不正な override で `createZediChatModel` が失敗してエラー envelope になる | ||
| * のを防ぐ)。 | ||
| * 2. `ai_models` の active な OpenAI モデルで最安 (`input_cost_units` ASC, `output_cost_units` ASC) | ||
| * 3. `ai_models` の active な Google モデルで最安 | ||
| * 4. 何も無ければ `null` を返す(ツール側は empty result + note を返す)。 | ||
| * 3. `ai_models` の active な OpenAI モデルで最安 (`input_cost_units` ASC, `output_cost_units` ASC) | ||
| * 4. `ai_models` の active な Google モデルで最安 | ||
| * 5. 何も無ければ `null` を返す(ツール側は empty result + note を返す)。 | ||
| * | ||
| * Returns the `ai_models.id` so `createZediChatModel({ modelId })` can validate | ||
| * tier access and resolve the API key uniformly. Centralising the choice in one | ||
|
|
@@ -25,6 +26,7 @@ | |
| import { and, asc, eq, inArray } from "drizzle-orm"; | ||
| import { aiModels } from "../../../schema/index.js"; | ||
| import type { Database, UserTier } from "../../../types/index.js"; | ||
| import { WIKI_COMPOSE_MODEL_ID } from "../llm/wikiComposeModelId.js"; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| const ENV_OVERRIDE = "WIKI_COMPOSE_WEB_SEARCH_MODEL_ID"; | ||
|
|
||
|
|
@@ -48,6 +50,20 @@ export async function resolveWebSearchModelId( | |
| db: Database, | ||
| tier: UserTier, | ||
| ): Promise<string | null> { | ||
| const tierClause = tierFilter(tier); | ||
| const [fixedRow] = await db | ||
| .select({ id: aiModels.id }) | ||
| .from(aiModels) | ||
| .where( | ||
| and( | ||
| eq(aiModels.id, WIKI_COMPOSE_MODEL_ID), | ||
| eq(aiModels.isActive, true), | ||
| ...(tierClause ? [tierClause] : []), | ||
| ), | ||
| ) | ||
| .limit(1); | ||
| if (fixedRow) return fixedRow.id; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| const override = process.env[ENV_OVERRIDE]?.trim(); | ||
| if (override) { | ||
| // Validate the override before returning: it must be active and | ||
|
|
@@ -73,7 +89,6 @@ export async function resolveWebSearchModelId( | |
| // override が使えない場合は通常検索にフォールバックする。 | ||
| } | ||
|
|
||
| const tierClause = tierFilter(tier); | ||
| const rows = await db | ||
| .select({ | ||
| id: aiModels.id, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.