From 8a5296f3cbab997fb9452f986488aefcfbe9b14e Mon Sep 17 00:00:00 2001 From: The Lord Argus Date: Thu, 23 Jul 2026 07:16:39 +0530 Subject: [PATCH] fix: merge provider base URL config from DB (#1676) Co-authored-by: The Lord Argus <215461619+TheLordArgus@users.noreply.github.com> --- src/core/config.ts | 34 ++++++++++++++++++++- test/cli-multimodal-integration.test.ts | 40 ++++++++++++++++++++++++- test/loadConfig-merge.test.ts | 29 ++++++++++++++++++ 3 files changed, 101 insertions(+), 2 deletions(-) diff --git a/src/core/config.ts b/src/core/config.ts index 4ca00cdc9..e81954886 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -620,7 +620,10 @@ export function loadConfig(): GBrainConfig | null { * size the schema and must be stable across engine connect. */ export async function loadConfigWithEngine( - engine: { getConfig(key: string): Promise }, + engine: { + getConfig(key: string): Promise; + listConfigKeys?(prefix: string): Promise; + }, base?: GBrainConfig | null, ): Promise { // Codex /ship finding #3: when there's no file config AND no env DB URL, @@ -657,11 +660,31 @@ export async function loadConfigWithEngine( return undefined; } } + async function dbPrefixMap(prefix: string): Promise | undefined> { + if (typeof engine.listConfigKeys !== 'function') return undefined; + let keys: string[]; + try { + keys = await engine.listConfigKeys(prefix); + } catch { + return undefined; + } + + const out: Record = {}; + for (const key of keys.sort()) { + if (!key.startsWith(prefix)) continue; + const leaf = key.slice(prefix.length); + if (!leaf) continue; + const value = await dbStr(key); + if (value !== undefined) out[leaf] = value; + } + return Object.keys(out).length > 0 ? out : undefined; + } const dbMultimodal = await dbBool('embedding_multimodal'); const dbMultimodalModel = await dbStr('embedding_multimodal_model'); const dbOcr = await dbBool('embedding_image_ocr'); const dbOcrModel = await dbStr('embedding_image_ocr_model'); + const dbProviderBaseUrls = await dbPrefixMap('provider_base_urls.'); // v0.36 (D7) — embedding-column registry merge. Stored as JSON string in // the config table. Parse + shape-check here; full registry validation // (regex on keys, type/dim/provider field shapes) runs in the resolver at @@ -685,6 +708,15 @@ export async function loadConfigWithEngine( if (merged.embedding_image_ocr_model === undefined && dbOcrModel !== undefined) { merged.embedding_image_ocr_model = dbOcrModel; } + if (dbProviderBaseUrls !== undefined) { + const next = { ...(merged.provider_base_urls ?? {}) }; + for (const [providerId, baseUrl] of Object.entries(dbProviderBaseUrls)) { + if (next[providerId] === undefined) next[providerId] = baseUrl; + } + if (Object.keys(next).length > 0) { + merged.provider_base_urls = next; + } + } if (merged.embedding_columns === undefined && dbEmbeddingColumns !== undefined) { try { const parsed = JSON.parse(dbEmbeddingColumns); diff --git a/test/cli-multimodal-integration.test.ts b/test/cli-multimodal-integration.test.ts index 640ecf31c..b4894faad 100644 --- a/test/cli-multimodal-integration.test.ts +++ b/test/cli-multimodal-integration.test.ts @@ -8,13 +8,15 @@ // // PGLite-only: in-memory engine, no DATABASE_URL needed. -import { afterAll, beforeAll, beforeEach, describe, expect, test } from 'bun:test'; +import { afterAll, afterEach, 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 { + __setRerankTransportForTests, configureGateway, getEmbeddingModel, getMultimodalModel, + rerank, resetGateway, } from '../src/core/ai/gateway.ts'; import type { AIGatewayConfig } from '../src/core/ai/types.ts'; @@ -52,10 +54,16 @@ afterAll(async () => { beforeEach(async () => { resetGateway(); + __setRerankTransportForTests(null); // 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', ''); + await engine.setConfig('provider_base_urls.llama-server-reranker', ''); +}); + +afterEach(() => { + __setRerankTransportForTests(null); }); describe('cli connectEngine — embedding_multimodal_model DB→gateway plumbing', () => { @@ -122,4 +130,34 @@ describe('cli connectEngine — embedding_multimodal_model DB→gateway plumbing expect(getEmbeddingModel()).toBe('openai:text-embedding-3-large'); expect(getMultimodalModel()).toBeUndefined(); }); + + test('DB-set provider_base_urls.llama-server-reranker flows to gateway.rerank URL', async () => { + await engine.setConfig('provider_base_urls.llama-server-reranker', 'http://127.0.0.1:8091/v1'); + + const baseConfig: GBrainConfig = { + engine: 'pglite', + embedding_model: 'openai:text-embedding-3-large', + embedding_dimensions: 1536, + }; + + const merged = await loadConfigWithEngine(engine, baseConfig); + configureGateway(buildGatewayConfig(merged!)); + + let capturedUrl = ''; + __setRerankTransportForTests(async (url) => { + capturedUrl = url; + return new Response(JSON.stringify({ results: [{ index: 0, relevance_score: 0.9 }] }), { + status: 200, + headers: { 'content-type': 'application/json' }, + }); + }); + + await rerank({ + query: 'q', + documents: ['d'], + model: 'llama-server-reranker:qwen3-reranker-4b', + }); + + expect(capturedUrl).toBe('http://127.0.0.1:8091/v1/rerank'); + }); }); diff --git a/test/loadConfig-merge.test.ts b/test/loadConfig-merge.test.ts index 7a8ab08af..e5a861960 100644 --- a/test/loadConfig-merge.test.ts +++ b/test/loadConfig-merge.test.ts @@ -9,6 +9,7 @@ import { loadConfigWithEngine, type GBrainConfig } from '../src/core/config.ts'; interface FakeEngine { getConfig(key: string): Promise; + listConfigKeys?(prefix: string): Promise; } function makeEngine(map: Record): FakeEngine { @@ -16,6 +17,9 @@ function makeEngine(map: Record): FakeEngine async getConfig(key: string) { return map[key]; }, + async listConfigKeys(prefix: string) { + return Object.keys(map).filter(key => key.startsWith(prefix)); + }, }; } @@ -92,6 +96,31 @@ describe('loadConfigWithEngine (Phase 4 / F3)', () => { expect(merged?.embedding_image_ocr).toBe(true); }); + test('DB provider_base_urls. fills the gateway base URL map', async () => { + const base: GBrainConfig = { engine: 'pglite' }; + const engine = makeEngine({ + 'provider_base_urls.llama-server-reranker': 'http://127.0.0.1:8091/v1', + }); + const merged = await loadConfigWithEngine(engine, base); + expect(merged?.provider_base_urls?.['llama-server-reranker']).toBe('http://127.0.0.1:8091/v1'); + }); + + test('provider_base_urls merge is per-provider: file value wins and DB fills siblings', async () => { + const base: GBrainConfig = { + engine: 'pglite', + provider_base_urls: { + 'llama-server-reranker': 'http://file.example/v1', + }, + }; + const engine = makeEngine({ + 'provider_base_urls.llama-server-reranker': 'http://db.example/v1', + 'provider_base_urls.openrouter': 'http://openrouter.example/v1', + }); + const merged = await loadConfigWithEngine(engine, base); + expect(merged?.provider_base_urls?.['llama-server-reranker']).toBe('http://file.example/v1'); + expect(merged?.provider_base_urls?.openrouter).toBe('http://openrouter.example/v1'); + }); + test('engine.getConfig throwing is non-fatal — file/env config still returned', async () => { const base: GBrainConfig = { engine: 'pglite',