mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-28 06:23:01 +00:00
Add provider_chat_options alongside provider_base_urls and thread it through the gateway config path into chat(). The chat request now deep-merges provider-scoped options and model-scoped overrides into providerOptions keyed by recipe id, preserving existing gateway-built options such as Anthropic cacheControl and leaving absent-config behavior unchanged. This lets operators disable thinking for small-budget hybrid-reasoning utility calls without hardcoding that behavior for every use of those models.
126 lines
4.9 KiB
TypeScript
126 lines
4.9 KiB
TypeScript
// v0.28.11 (PR #719, D5): cli connectEngine() DB→gateway plumbing.
|
|
//
|
|
// The unit tests for loadConfigWithEngine and embedMultimodal cover their
|
|
// own contracts but don't exercise the cli.ts glue that ties them together.
|
|
// Codex F3 flagged this as the actual bug site. This file drives the same
|
|
// merge + reconfigure sequence connectEngine() runs and asserts the gateway
|
|
// observed the DB-set value through buildGatewayConfig.
|
|
//
|
|
// PGLite-only: in-memory engine, no DATABASE_URL needed.
|
|
|
|
import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'bun:test';
|
|
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
|
|
import { loadConfigWithEngine, type GBrainConfig } from '../src/core/config.ts';
|
|
import {
|
|
configureGateway,
|
|
getEmbeddingModel,
|
|
getMultimodalModel,
|
|
resetGateway,
|
|
} from '../src/core/ai/gateway.ts';
|
|
import type { AIGatewayConfig } from '../src/core/ai/types.ts';
|
|
|
|
// Mirror the cli.ts buildGatewayConfig helper exactly. Keeping a copy here
|
|
// (instead of exporting from cli.ts) is intentional: the test asserts the
|
|
// shape of the contract, not the helper's identity. If cli.ts drifts, the
|
|
// e2e behavior these tests care about (DB-set value lands in gateway) still
|
|
// holds, but a helper-shape test would also catch the drift in PR review.
|
|
function buildGatewayConfig(c: GBrainConfig): AIGatewayConfig {
|
|
return {
|
|
embedding_model: c.embedding_model,
|
|
embedding_dimensions: c.embedding_dimensions,
|
|
embedding_multimodal_model: c.embedding_multimodal_model,
|
|
expansion_model: c.expansion_model,
|
|
chat_model: c.chat_model,
|
|
chat_fallback_chain: c.chat_fallback_chain,
|
|
base_urls: c.provider_base_urls,
|
|
provider_chat_options: c.provider_chat_options,
|
|
env: { ...process.env },
|
|
};
|
|
}
|
|
|
|
let engine: PGLiteEngine;
|
|
|
|
beforeAll(async () => {
|
|
engine = new PGLiteEngine();
|
|
await engine.connect({});
|
|
await engine.initSchema();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await engine.disconnect();
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
resetGateway();
|
|
// Clear any prior config rows so tests are independent. setConfig with
|
|
// empty string is treated as undefined by loadConfigWithEngine (per
|
|
// dbStr semantics), so this is safe to call between tests.
|
|
await engine.setConfig('embedding_multimodal_model', '');
|
|
});
|
|
|
|
describe('cli connectEngine — embedding_multimodal_model DB→gateway plumbing', () => {
|
|
test('DB-set multimodal_model flows to gateway via merge + reconfigure', async () => {
|
|
await engine.setConfig('embedding_multimodal_model', 'voyage:voyage-multimodal-3');
|
|
|
|
const baseConfig: GBrainConfig = {
|
|
engine: 'pglite',
|
|
embedding_model: 'openai:text-embedding-3-large',
|
|
embedding_dimensions: 1536,
|
|
};
|
|
|
|
// First call mirrors cli.ts:715 (pre-engine-connect).
|
|
configureGateway(buildGatewayConfig(baseConfig));
|
|
expect(getMultimodalModel()).toBeUndefined();
|
|
|
|
// Second call mirrors cli.ts:776 (post-DB-merge).
|
|
const merged = await loadConfigWithEngine(engine, baseConfig);
|
|
expect(merged).not.toBeNull();
|
|
configureGateway(buildGatewayConfig(merged!));
|
|
|
|
// Primary embedding_model stays put (file/env wins); multimodal_model
|
|
// arrived via DB.
|
|
expect(getEmbeddingModel()).toBe('openai:text-embedding-3-large');
|
|
expect(getMultimodalModel()).toBe('voyage:voyage-multimodal-3');
|
|
});
|
|
|
|
test('file value wins over DB value (env > file > DB precedence at gateway level)', async () => {
|
|
await engine.setConfig('embedding_multimodal_model', 'voyage:voyage-3-large');
|
|
|
|
const baseConfig: GBrainConfig = {
|
|
engine: 'pglite',
|
|
embedding_model: 'openai:text-embedding-3-large',
|
|
embedding_dimensions: 1536,
|
|
embedding_multimodal_model: 'voyage:voyage-multimodal-3', // file plane
|
|
};
|
|
|
|
const merged = await loadConfigWithEngine(engine, baseConfig);
|
|
configureGateway(buildGatewayConfig(merged!));
|
|
|
|
expect(getMultimodalModel()).toBe('voyage:voyage-multimodal-3');
|
|
});
|
|
|
|
test('un-gated re-config: merged DB has no multimodal_model → gateway still gets re-configured', async () => {
|
|
// Codex F5 was about whether the un-gated re-config weakens an
|
|
// intentional contract. This test pins the actual behavior (D6 = B):
|
|
// re-config always fires when merge succeeds, even when no DB key
|
|
// changed. Schema-sizing fields stay stable because loadConfigWithEngine
|
|
// respects file/env first.
|
|
const baseConfig: GBrainConfig = {
|
|
engine: 'pglite',
|
|
embedding_model: 'openai:text-embedding-3-large',
|
|
embedding_dimensions: 1536,
|
|
};
|
|
|
|
configureGateway(buildGatewayConfig(baseConfig));
|
|
expect(getEmbeddingModel()).toBe('openai:text-embedding-3-large');
|
|
|
|
const merged = await loadConfigWithEngine(engine, baseConfig);
|
|
configureGateway(buildGatewayConfig(merged!));
|
|
|
|
// Primary model unchanged (DB had no override); the re-config is a
|
|
// semantic no-op for these fields.
|
|
expect(getEmbeddingModel()).toBe('openai:text-embedding-3-large');
|
|
expect(getMultimodalModel()).toBeUndefined();
|
|
});
|
|
});
|