mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
fix(dream): gate patterns phase on gateway provider reachability, not ANTHROPIC_API_KEY (takeover of #2279)
Absorbs PR #2279's intent (drop the hardcoded ANTHROPIC_API_KEY gate) with the end-state the gateway world actually wants: the patterns phase now probes the RESOLVED patterns model through probeChatModel(normalizeModelId) — the same semantics as think/index.ts and synthesize's makeJudgeClient. Fixes two misclassifications of the old env gate: - Non-Anthropic stacks (litellm, deepseek, openrouter, ...) were skipped as "no upstream" even though the subagent routes them through the gateway (agent.use_gateway_loop). They now pass the gate; their auth is checked lazily at dispatch and surfaces in the job outcome. - Anthropic keys set via `gbrain config set anthropic_api_key` (stdio MCP launches without shell env) were treated as missing. hasAnthropicKey inside probeChatModel reads both sources. Skip reason renames no_api_key → no_provider (carrying the probe's detail). Both pinning tests updated: the structural test asserts the probe wiring; the PGLite E2E swaps its env-only helper for the shared hermetic withoutAnthropicKey (env + config file) so it can't flip to a live LLM call on a dev machine with a config-file key. Co-authored-by: brettdavies <brettdavies@users.noreply.github.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
brettdavies
Claude Fable 5
parent
3aeb622dc7
commit
c46b457978
@@ -27,6 +27,8 @@ import { waitForCompletion, TimeoutError } from '../minions/wait-for-completion.
|
||||
import type { MinionJobInput, SubagentHandlerData } from '../minions/types.ts';
|
||||
import { serializeMarkdown } from '../markdown.ts';
|
||||
import type { Page, PageType } from '../types.ts';
|
||||
import { probeChatModel } from '../ai/gateway.ts';
|
||||
import { normalizeModelId } from '../model-id.ts';
|
||||
|
||||
export interface PatternsPhaseOpts {
|
||||
brainDir: string;
|
||||
@@ -63,9 +65,20 @@ export async function runPhasePatterns(
|
||||
});
|
||||
}
|
||||
|
||||
// Submit one subagent for pattern detection.
|
||||
if (!process.env.ANTHROPIC_API_KEY) {
|
||||
return skipped('no_api_key', 'ANTHROPIC_API_KEY unset; pattern detection skipped');
|
||||
// Submit one subagent for pattern detection. The subagent dispatches via
|
||||
// the gateway model-tier resolver, so gate on "is the resolved model's
|
||||
// provider reachable" rather than ANTHROPIC_API_KEY specifically — a
|
||||
// hardcoded env gate misclassified non-Anthropic stacks (litellm,
|
||||
// deepseek, openrouter, ...) as "no upstream" even though the subagent
|
||||
// routes them through the gateway (agent.use_gateway_loop), and it missed
|
||||
// Anthropic keys set via `gbrain config set anthropic_api_key`. Same
|
||||
// probe semantics as think/index.ts + synthesize's makeJudgeClient:
|
||||
// unknown provider/model or Anthropic-without-key skips cheaply; other
|
||||
// providers' auth is checked lazily at dispatch and surfaces in the job
|
||||
// outcome. (Takeover of PR #2279's intent by @brettdavies.)
|
||||
const probe = probeChatModel(normalizeModelId(config.model));
|
||||
if (!probe.ok) {
|
||||
return skipped('no_provider', `pattern detection skipped: ${probe.detail}`);
|
||||
}
|
||||
|
||||
const allowedSlugPrefixes = await loadAllowedSlugPrefixes();
|
||||
|
||||
@@ -39,9 +39,14 @@ describe('patterns phase wiring', () => {
|
||||
expect(patternsSrc).toContain("tool_name = 'brain_put_page'");
|
||||
});
|
||||
|
||||
test('skips when ANTHROPIC_API_KEY missing', () => {
|
||||
expect(patternsSrc).toContain('ANTHROPIC_API_KEY');
|
||||
expect(patternsSrc).toContain('no_api_key');
|
||||
test('gates on gateway provider reachability, not ANTHROPIC_API_KEY (PR #2279)', () => {
|
||||
// The gate must probe the RESOLVED patterns model through the gateway
|
||||
// (any configured provider can run patterns), not hardcode the Anthropic
|
||||
// env var — that misclassified non-Anthropic stacks as "no upstream".
|
||||
expect(patternsSrc).toContain('probeChatModel');
|
||||
expect(patternsSrc).toContain('normalizeModelId');
|
||||
expect(patternsSrc).toContain('no_provider');
|
||||
expect(patternsSrc).not.toContain('process.env.ANTHROPIC_API_KEY');
|
||||
});
|
||||
|
||||
test('skips when reflections below min_evidence', () => {
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
* Anthropic call:
|
||||
* - disabled: dream.patterns.enabled=false → skipped
|
||||
* - insufficient_evidence: <min_evidence reflections → skipped
|
||||
* - no_api_key: enough reflections, no ANTHROPIC_API_KEY → skipped
|
||||
* - no_provider: enough reflections, no reachable provider for the
|
||||
* resolved patterns model (default: Anthropic with no key in env OR
|
||||
* config) → skipped
|
||||
* - dry-run: passes through with reflections_considered + zero pages
|
||||
*
|
||||
* The Sonnet detection path is structurally covered in
|
||||
@@ -23,6 +25,7 @@
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import { PGLiteEngine } from '../../src/core/pglite-engine.ts';
|
||||
import { runPhasePatterns } from '../../src/core/cycle/patterns.ts';
|
||||
import { withoutAnthropicKey } from '../helpers/no-anthropic-key.ts';
|
||||
|
||||
interface TestRig {
|
||||
engine: PGLiteEngine;
|
||||
@@ -43,17 +46,6 @@ async function setupRig(): Promise<TestRig> {
|
||||
};
|
||||
}
|
||||
|
||||
async function withoutAnthropicKey<T>(body: () => Promise<T>): Promise<T> {
|
||||
const saved = process.env.ANTHROPIC_API_KEY;
|
||||
delete process.env.ANTHROPIC_API_KEY;
|
||||
try {
|
||||
return await body();
|
||||
} finally {
|
||||
if (saved === undefined) delete process.env.ANTHROPIC_API_KEY;
|
||||
else process.env.ANTHROPIC_API_KEY = saved;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert N reflection pages directly via engine.putPage so the patterns
|
||||
* gather query has data without going through the synthesize phase.
|
||||
@@ -141,18 +133,23 @@ describe('E2E patterns — insufficient_evidence', () => {
|
||||
}, 30_000);
|
||||
});
|
||||
|
||||
describe('E2E patterns — no API key', () => {
|
||||
test('enough reflections, no ANTHROPIC_API_KEY → skipped no_api_key', async () => {
|
||||
describe('E2E patterns — no reachable provider', () => {
|
||||
test('enough reflections, no Anthropic key in env OR config → skipped no_provider', async () => {
|
||||
const rig = await setupRig();
|
||||
try {
|
||||
await seedReflections(rig.engine, 5); // above default min_evidence (3)
|
||||
// Default patterns model resolves to Anthropic; with no key reachable
|
||||
// from EITHER source (env + config file — the shared helper neuters
|
||||
// both) the gateway probe reports the provider unavailable. A
|
||||
// non-Anthropic stack (litellm, deepseek, ...) passes this gate and
|
||||
// dispatches through the gateway instead (PR #2279).
|
||||
await withoutAnthropicKey(async () => {
|
||||
const result = await runPhasePatterns(rig.engine, {
|
||||
brainDir: rig.brainDir,
|
||||
dryRun: false,
|
||||
});
|
||||
expect(result.status).toBe('skipped');
|
||||
expect((result.details as { reason?: string }).reason).toBe('no_api_key');
|
||||
expect((result.details as { reason?: string }).reason).toBe('no_provider');
|
||||
});
|
||||
} finally {
|
||||
await rig.cleanup();
|
||||
|
||||
Reference in New Issue
Block a user