fix(takes): default takes extraction to the configured chat_model instead of hardcoded cloud Haiku (#2997) (#3021)

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 <garrytan@gmail.com>
Co-authored-by: Nazz <nazim.mj@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Time Attakc
2026-07-20 15:05:32 -07:00
committed by GitHub
co-authored by Garry Tan Nazz Claude Fable 5
parent 9ed53e4e1c
commit dbf2b3f562
2 changed files with 99 additions and 2 deletions
+6 -2
View File
@@ -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: [
{
@@ -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<void> {
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']);
});
});