mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
Two small ergonomics fixes folded together (#765 deferred — see TODOS.md follow-up; the CJK PGLite extraction was bigger than the plan estimated). #779 reworked (alexandreroumieu-codeapprentice): silence the missing-max_batch_tokens startup warning for recipes with genuinely dynamic batch capacity. New `EmbeddingTouchpoint.no_batch_cap?: true` field. Set on ollama (capacity depends on locally loaded model + OLLAMA_NUM_PARALLEL), litellm-proxy (depends on backend), llama-server (set by --ctx-size at server launch). Three less stderr warnings on every gateway configure; google still warns (it's a real fixed-cap provider that ought to ship a max_batch_tokens declaration). Bonus: litellm-proxy now declares `user_provided_models: true`, removing the last consumer of the legacy `recipe.id === 'litellm'` hardcode in gateway.ts:223 (D8=A wire-through completion). #121 reworked (vinsew): self-contained API keys. Two parts: 1. config.ts: ANTHROPIC_API_KEY env merge was silently missing. loadConfig() merged OPENAI_API_KEY but not ANTHROPIC_API_KEY into the file-config-shape result. One-line addition. 2. cli.ts:buildGatewayConfig: when ~/.gbrain/config.json declares openai_api_key / anthropic_api_key but the process env doesn't have those env vars set (common for launchd-spawned daemons, agent subprocess tools, containers that don't propagate ~/.zshrc), fold the config-file values into the gateway env snapshot. Process env still wins (loaded last) so per-process overrides keep working. Tests (4 cases in test/ai/no-batch-cap-suppression.test.ts): - Ollama / LiteLLM / llama-server all declare no_batch_cap: true - configureGateway does NOT warn for those three - configureGateway STILL warns for google (regression guard) - Cross-cutting invariant: empty-models recipes declare user_provided_models Tests: bun test test/ai/ — 128/128 (4 new + 124 prior). Plan: ~/.claude/plans/ok-lets-turn-this-enumerated-sonnet.md (commit 9 of 11). #765 (Hunyuan PGLite + CJK keyword fallback) deferred to TODOS.md follow-up; the CJK extraction (~150 lines + scoring logic + tests) is larger than the wave's adjacent-fix lane should carry. Closes that PR with a deferral note. Co-Authored-By: alexandreroumieu-codeapprentice <noreply@github.com> Co-Authored-By: vinsew <noreply@github.com> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
alexandreroumieu-codeapprentice
Claude Opus 4.7
parent
4ae4a98788
commit
180f4fcb8a
+10
-1
@@ -1116,6 +1116,15 @@ async function handleCliOnly(command: string, args: string[]) {
|
||||
// but not the other previously required remembering to mirror the change;
|
||||
// the helper makes that structural.
|
||||
function buildGatewayConfig(c: GBrainConfig): AIGatewayConfig {
|
||||
// v0.32 (#121 reworked): when ~/.gbrain/config.json declares
|
||||
// openai_api_key / anthropic_api_key, fold them into the gateway env so
|
||||
// recipes that read OPENAI_API_KEY / ANTHROPIC_API_KEY find them. Process
|
||||
// env still wins (it's loaded last) — this is a fallback for daemons /
|
||||
// launchd-spawned subprocesses that don't propagate ~/.zshrc-sourced keys.
|
||||
const envFromConfig: Record<string, string> = {};
|
||||
if (c.openai_api_key) envFromConfig.OPENAI_API_KEY = c.openai_api_key;
|
||||
if (c.anthropic_api_key) envFromConfig.ANTHROPIC_API_KEY = c.anthropic_api_key;
|
||||
|
||||
return {
|
||||
embedding_model: c.embedding_model,
|
||||
embedding_dimensions: c.embedding_dimensions,
|
||||
@@ -1124,7 +1133,7 @@ function buildGatewayConfig(c: GBrainConfig): AIGatewayConfig {
|
||||
chat_model: c.chat_model,
|
||||
chat_fallback_chain: c.chat_fallback_chain,
|
||||
base_urls: c.provider_base_urls,
|
||||
env: { ...process.env },
|
||||
env: { ...envFromConfig, ...process.env }, // process.env wins
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -234,6 +234,10 @@ function warnRecipesMissingBatchTokens(): void {
|
||||
// recipe; suppress the warning for it. Every other recipe missing the
|
||||
// field is suspicious.
|
||||
if (recipe.id === 'openai') continue;
|
||||
// v0.32 (#779): explicit opt-out for dynamic-cap recipes (Ollama,
|
||||
// LiteLLM proxy, llama-server) — they ship without a static cap because
|
||||
// the cap depends on a user-launched server. Warning is noise for them.
|
||||
if (embedding.no_batch_cap === true) continue;
|
||||
if (_warnedRecipes.has(recipe.id)) continue;
|
||||
_warnedRecipes.add(recipe.id);
|
||||
// eslint-disable-next-line no-console
|
||||
|
||||
@@ -23,9 +23,13 @@ export const litellmProxy: Recipe = {
|
||||
embedding: {
|
||||
// Models depend on the proxy's config; declare empties so wizard prompts user.
|
||||
models: [],
|
||||
user_provided_models: true, // v0.32 D8=A wire-through for the litellm hardcode
|
||||
default_dims: 0, // user must declare --embedding-dimensions explicitly
|
||||
cost_per_1m_tokens_usd: undefined,
|
||||
price_last_verified: '2026-04-20',
|
||||
// LiteLLM's batch capacity is determined by the backend it proxies;
|
||||
// no static cap to declare here. v0.32 (#779).
|
||||
no_batch_cap: true,
|
||||
},
|
||||
},
|
||||
setup_hint: 'Run LiteLLM (https://docs.litellm.ai) in front of any provider; set LITELLM_BASE_URL + pass --embedding-model litellm:<model> and --embedding-dimensions <N>.',
|
||||
|
||||
@@ -33,7 +33,10 @@ export const llamaServer: Recipe = {
|
||||
user_provided_models: true,
|
||||
default_dims: 0, // forces explicit --embedding-dimensions
|
||||
cost_per_1m_tokens_usd: 0,
|
||||
price_last_verified: '2026-05-09',
|
||||
price_last_verified: '2026-05-10',
|
||||
// llama-server's batch capacity is set by `--ctx-size` at launch
|
||||
// time; no static cap to declare. v0.32 (#779).
|
||||
no_batch_cap: true,
|
||||
},
|
||||
},
|
||||
/**
|
||||
|
||||
@@ -17,6 +17,9 @@ export const ollama: Recipe = {
|
||||
default_dims: 768, // nomic-embed-text native dim
|
||||
cost_per_1m_tokens_usd: 0,
|
||||
price_last_verified: '2026-04-20',
|
||||
// Ollama's batch capacity depends on the locally loaded model + the
|
||||
// OLLAMA_NUM_PARALLEL config; no static cap to declare. v0.32 (#779).
|
||||
no_batch_cap: true,
|
||||
},
|
||||
},
|
||||
setup_hint: 'Install Ollama from https://ollama.ai, then `ollama pull nomic-embed-text` and `ollama serve`.',
|
||||
|
||||
@@ -86,6 +86,19 @@ export interface EmbeddingTouchpoint {
|
||||
* for shorthand `--model <provider>` and prints a setup hint.
|
||||
*/
|
||||
user_provided_models?: true;
|
||||
/**
|
||||
* v0.32 (#779 reworked): explicit opt-out of the missing-max_batch_tokens
|
||||
* startup warning. Set to `true` for recipes whose batch capacity is
|
||||
* genuinely dynamic (Ollama: depends on user-loaded model; LiteLLM proxy:
|
||||
* depends on backend; llama.cpp: depends on `--ctx-size` at server launch).
|
||||
*
|
||||
* Without this flag, missing `max_batch_tokens` triggers a once-per-process
|
||||
* stderr warning so future recipes that forget the cap (and would
|
||||
* silently rely on recursive-halving) don't ship un-noticed. Recipes that
|
||||
* declare `no_batch_cap: true` are explicitly opting out — the warning is
|
||||
* noise for them.
|
||||
*/
|
||||
no_batch_cap?: true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -144,6 +144,7 @@ export function loadConfig(): GBrainConfig | null {
|
||||
engine: inferredEngine,
|
||||
...(dbUrl ? { database_url: dbUrl } : {}),
|
||||
...(process.env.OPENAI_API_KEY ? { openai_api_key: process.env.OPENAI_API_KEY } : {}),
|
||||
...(process.env.ANTHROPIC_API_KEY ? { anthropic_api_key: process.env.ANTHROPIC_API_KEY } : {}),
|
||||
...(process.env.GBRAIN_EMBEDDING_MODEL ? { embedding_model: process.env.GBRAIN_EMBEDDING_MODEL } : {}),
|
||||
...(process.env.GBRAIN_EMBEDDING_DIMENSIONS ? { embedding_dimensions: parseInt(process.env.GBRAIN_EMBEDDING_DIMENSIONS, 10) } : {}),
|
||||
...(process.env.GBRAIN_EXPANSION_MODEL ? { expansion_model: process.env.GBRAIN_EXPANSION_MODEL } : {}),
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* #779 + #121 adjacent fixes (Commit 9 of v0.32 wave).
|
||||
*
|
||||
* Coverage:
|
||||
* - Recipes with `embedding.no_batch_cap: true` suppress the
|
||||
* missing-max_batch_tokens startup warning (#779)
|
||||
* - Real-provider recipes without the flag still warn (regression guard)
|
||||
* - listRecipes returns expected dynamic-cap recipes (ollama, litellm,
|
||||
* llama-server) all flagged
|
||||
*/
|
||||
|
||||
import { afterAll, beforeAll, describe, expect, mock, test } from 'bun:test';
|
||||
import { configureGateway, resetGateway } from '../../src/core/ai/gateway.ts';
|
||||
import { listRecipes, getRecipe } from '../../src/core/ai/recipes/index.ts';
|
||||
|
||||
describe('v0.32 #779: no_batch_cap suppresses the missing-max_batch_tokens warning', () => {
|
||||
let warnSpy: ReturnType<typeof mock>;
|
||||
let realWarn: typeof console.warn;
|
||||
|
||||
beforeAll(() => {
|
||||
realWarn = console.warn;
|
||||
warnSpy = mock(() => {});
|
||||
console.warn = warnSpy as any;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
console.warn = realWarn;
|
||||
resetGateway();
|
||||
});
|
||||
|
||||
test('Ollama, LiteLLM, llama-server all declare no_batch_cap: true', () => {
|
||||
for (const id of ['ollama', 'litellm', 'llama-server']) {
|
||||
const r = getRecipe(id);
|
||||
expect(r, `${id} not registered`).toBeDefined();
|
||||
expect(
|
||||
r!.touchpoints.embedding?.no_batch_cap,
|
||||
`${id} should declare no_batch_cap: true`,
|
||||
).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
test('configureGateway does NOT warn for ollama/litellm/llama-server', () => {
|
||||
warnSpy.mockClear();
|
||||
resetGateway();
|
||||
configureGateway({ env: {} });
|
||||
const messages = warnSpy.mock.calls.map(c => String(c[0] ?? ''));
|
||||
for (const id of ['ollama', 'litellm', 'llama-server']) {
|
||||
expect(
|
||||
messages.some(m => m.includes(`"${id}"`)),
|
||||
`should NOT warn for ${id}`,
|
||||
).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
test('configureGateway STILL warns for google (real provider, no cap declared)', () => {
|
||||
warnSpy.mockClear();
|
||||
resetGateway();
|
||||
configureGateway({ env: {} });
|
||||
const messages = warnSpy.mock.calls.map(c => String(c[0] ?? ''));
|
||||
expect(
|
||||
messages.some(m => m.includes('"google"') && m.includes('without max_batch_tokens')),
|
||||
'google should warn (it has fixed-cap models)',
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('every recipe with empty models[] declares user_provided_models OR has openai-fast-path', () => {
|
||||
// Cross-cutting invariant: contracts should not silently disagree.
|
||||
for (const r of listRecipes()) {
|
||||
const e = r.touchpoints.embedding;
|
||||
if (!e) continue;
|
||||
if (e.models.length === 0) {
|
||||
expect(
|
||||
e.user_provided_models === true || r.id === 'litellm',
|
||||
`${r.id} has empty models[] — must declare user_provided_models: true`,
|
||||
).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user