From dbf2b3f56294b5671abf3cbed6ad2266e985ed92 Mon Sep 17 00:00:00 2001 From: Time Attakc <89218912+time-attack@users.noreply.github.com> Date: Mon, 20 Jul 2026 15:05:32 -0700 Subject: [PATCH] fix(takes): default takes extraction to the configured chat_model instead of hardcoded cloud Haiku (#2997) (#3021) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit extractTakesFromPages hardcoded anthropic:claude-haiku-4-5 as the classifier model. On an OAuth/local-only install (no ANTHROPIC_API_KEY; chat routed through a gateway model) every takes extraction died with llm_unavailable — the takes layer silently never populated and the takes_count health check stayed red despite a working configured chat_model. Resolution is now `opts.model || getChatModel()` — the same file-plane gateway-config idiom enrich.ts uses — NOT engine.getConfig('chat_model') (the DB config plane), keeping model routing on the single config plane the rest of the codebase reads. Explicit opts.model still wins; unconfigured installs fall through to the gateway's DEFAULT_CHAT_MODEL. Adds a regression test that pins the file-plane read: a conflicting DB-plane config.chat_model row is ignored, the gateway-configured chat_model is used when opts.model is unset, and explicit opts.model wins. Verified the file-plane test fails against the pre-fix code. Takeover of #2997 by @Nazim22 with the model read moved from the DB config plane to the file-plane gateway idiom. Co-authored-by: Garry Tan Co-authored-by: Nazz Co-authored-by: Claude Fable 5 --- src/core/extract-takes-from-pages.ts | 8 +- test/extract-takes-model-resolution.test.ts | 93 +++++++++++++++++++++ 2 files changed, 99 insertions(+), 2 deletions(-) create mode 100644 test/extract-takes-model-resolution.test.ts diff --git a/src/core/extract-takes-from-pages.ts b/src/core/extract-takes-from-pages.ts index 784f152ac..a6c26c939 100644 --- a/src/core/extract-takes-from-pages.ts +++ b/src/core/extract-takes-from-pages.ts @@ -15,7 +15,7 @@ import type { BrainEngine } from './engine.ts'; import type { TakeBatchInput, TakeKind } from './engine.ts'; -import { chat, isAvailable } from './ai/gateway.ts'; +import { chat, getChatModel, isAvailable } from './ai/gateway.ts'; export const ALLOWED_PAGE_TYPES = [ 'concept', 'atom', 'lore', 'briefing', 'writing', 'originals', @@ -190,7 +190,11 @@ export async function extractTakesFromPages( let response: { text: string }; try { response = await chat({ - model: opts.model ?? 'anthropic:claude-haiku-4-5', + // #2997 — default to the configured chat model (file-plane gateway + // config, same idiom as enrich.ts) instead of hardcoded cloud Haiku. + // On OAuth/local-only installs the hardcoded model made every takes + // extraction die with llm_unavailable despite a working chat_model. + model: opts.model || getChatModel(), system: CLASSIFIER_SYSTEM, messages: [ { diff --git a/test/extract-takes-model-resolution.test.ts b/test/extract-takes-model-resolution.test.ts new file mode 100644 index 000000000..d0715fa29 --- /dev/null +++ b/test/extract-takes-model-resolution.test.ts @@ -0,0 +1,93 @@ +/** + * Takes-extraction model resolution regression (#2997). + * + * extractTakesFromPages hardcoded `anthropic:claude-haiku-4-5` as the + * classifier model. On OAuth/local-only installs (no ANTHROPIC_API_KEY; + * chat routed through a gateway model) every extraction died with + * llm_unavailable even though a working chat_model was configured. + * + * Pins the fix's resolution order AND its config plane: + * opts.model → getChatModel() (file-plane gateway config, the enrich.ts + * idiom) — NOT the DB config plane (engine.getConfig('chat_model')). + */ +import { describe, test, expect, beforeAll, afterAll, beforeEach } from 'bun:test'; +import { PGLiteEngine } from '../src/core/pglite-engine.ts'; +import { + configureGateway, + resetGateway, + __setChatTransportForTests, +} from '../src/core/ai/gateway.ts'; +import { extractTakesFromPages } from '../src/core/extract-takes-from-pages.ts'; + +let engine: PGLiteEngine; +const seenModels: string[] = []; +let pageN = 0; + +/** Each test seeds a fresh uncovered page so the extraction loop fires. */ +async function seedPage(): Promise { + const body = 'An opinion-bearing body long enough to clear the 200-char eligibility floor. '.repeat(5); + await engine.putPage(`concepts/model-resolution-${pageN++}`, { + type: 'concept', title: `M${pageN}`, compiled_truth: body, frontmatter: {}, + }); +} + +beforeAll(async () => { + engine = new PGLiteEngine(); + await engine.connect({}); + await engine.initSchema(); + + __setChatTransportForTests(async (opts) => { + seenModels.push(opts.model ?? '(unset)'); + return { + text: '[{"claim":"a stubbed claim","kind":"take","weight":0.7}]', + blocks: [{ type: 'text' as const, text: '[{"claim":"a stubbed claim","kind":"take","weight":0.7}]' }], + stopReason: 'end' as const, + usage: { input_tokens: 1, output_tokens: 1, cache_read_tokens: 0, cache_creation_tokens: 0 }, + model: opts.model ?? '(unset)', + providerId: 'test', + }; + }); +}); + +afterAll(async () => { + __setChatTransportForTests(null); + resetGateway(); + await engine.disconnect(); +}); + +beforeEach(() => { + seenModels.length = 0; +}); + +describe('extractTakesFromPages — model resolution (#2997)', () => { + test('defaults to the configured chat_model from the file-plane gateway config', async () => { + configureGateway({ + chat_model: 'openai:gpt-config-plane-test', + env: { OPENAI_API_KEY: 'sk-test-model-resolution' }, + }); + // A conflicting DB-plane value must be IGNORED — model config is the + // config-file plane (getChatModel), not the brain DB config table. + await engine.setConfig('chat_model', 'wrong:db-plane-model'); + await seedPage(); + + const r = await extractTakesFromPages(engine, { bootstrapEnabled: true, maxPages: 50 }); + expect(r.pages_scanned).toBe(1); + expect(seenModels).toEqual(['openai:gpt-config-plane-test']); + }); + + test('explicit opts.model wins over the configured chat_model', async () => { + configureGateway({ + chat_model: 'openai:gpt-config-plane-test', + env: { OPENAI_API_KEY: 'sk-test-model-resolution' }, + }); + await seedPage(); + + const r = await extractTakesFromPages(engine, { + bootstrapEnabled: true, + maxPages: 50, + model: 'anthropic:claude-haiku-4-5', + }); + expect(r.pages_scanned).toBe(1); + expect(seenModels).toEqual(['anthropic:claude-haiku-4-5']); + }); +});