mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
fix: merge provider base URL config from DB (#1676)
Co-authored-by: The Lord Argus <215461619+TheLordArgus@users.noreply.github.com>
This commit is contained in:
co-authored by
The Lord Argus
parent
8837bfe5f2
commit
8a5296f3cb
+33
-1
@@ -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<string | null | undefined> },
|
||||
engine: {
|
||||
getConfig(key: string): Promise<string | null | undefined>;
|
||||
listConfigKeys?(prefix: string): Promise<string[]>;
|
||||
},
|
||||
base?: GBrainConfig | null,
|
||||
): Promise<GBrainConfig | null> {
|
||||
// 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<Record<string, string> | undefined> {
|
||||
if (typeof engine.listConfigKeys !== 'function') return undefined;
|
||||
let keys: string[];
|
||||
try {
|
||||
keys = await engine.listConfigKeys(prefix);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const out: Record<string, string> = {};
|
||||
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);
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,6 +9,7 @@ import { loadConfigWithEngine, type GBrainConfig } from '../src/core/config.ts';
|
||||
|
||||
interface FakeEngine {
|
||||
getConfig(key: string): Promise<string | null | undefined>;
|
||||
listConfigKeys?(prefix: string): Promise<string[]>;
|
||||
}
|
||||
|
||||
function makeEngine(map: Record<string, string | null | undefined>): FakeEngine {
|
||||
@@ -16,6 +17,9 @@ function makeEngine(map: Record<string, string | null | undefined>): 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.<provider> 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',
|
||||
|
||||
Reference in New Issue
Block a user