mirror of
https://github.com/garrytan/gbrain.git
synced 2026-07-27 22:15:33 +00:00
* feat(ai): add default_headers / resolveDefaultHeaders seam to Recipe Generalizes per-recipe header attachment so attribution headers (OpenRouter's HTTP-Referer + X-OpenRouter-Title) ride alongside Bearer auth on every openai-compatible touchpoint. Two safety guards fire at applyResolveAuth time: declaring both default_headers AND resolveDefaultHeaders throws AIConfigError (mutual exclusion); a default header whose key shadows the resolved auth header (Authorization, the resolver's custom header) also throws. Reranker HTTP path at gateway.ts:2281 now merges both Authorization Bearer AND auth.headers (where default_headers flow) into the request Headers map. Pre-fix the ternary picked one or the other; default_headers would have been silently dropped on the manual rerank path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(ai): add OpenRouter provider recipe One key, many hosted models. Configures openrouter:<provider>/<model> for chat (GPT-5.2 family, Claude 4.5/4.6/4.7, Gemini 3 Flash Preview, DeepSeek) and embedding (OpenAI text-embedding-3-small with Matryoshka dims_options). max_batch_tokens=300_000 (OpenAI's aggregate per-request token cap, not the per-input 8192 the original PR conflated). resolveDefaultHeaders returns HTTP-Referer + X-OpenRouter-Title + X-Title (back-compat alias) so traffic is attributed to gbrain on OR's leaderboard. Forks override via OPENROUTER_REFERER / OPENROUTER_TITLE env vars. supports_subagent_loop: false is informational — gbrain's subagent infra is hard-pinned to Anthropic-direct via isAnthropicProvider() upstream regardless of this flag. Filed as TODO to verify tool_use_id stability through OR. Cherry-picked from PR #1210. Contributed by @davemorin. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(cli): export buildGatewayConfig + thread OPENROUTER_BASE_URL Exports buildGatewayConfig for unit-test access. Adds one-line passthrough for OPENROUTER_BASE_URL matching the existing LITELLM/OLLAMA/LMSTUDIO/ LLAMA_SERVER pattern so users can point at a self-hosted OR-compatible proxy. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * test(ai): cover OpenRouter recipe + default_headers seam + wire-level headers Four test additions: - test/ai/recipe-openrouter.test.ts (11 cases) — recipe shape, Matryoshka dims_options, max_batch_tokens=300K, arbitrary-ID acceptance via assertTouchpoint, defaultResolveAuth happy/error, resolveDefaultHeaders defaults + fork-override path, setup_hint coverage. Shape regression on every chat/embedding model ID (catches typos without pinning the dynamic catalog). - test/ai/recipes-existing-regression.test.ts (+6 cases) — IRON RULE preserved; adds default_headers contract: Bearer+defaults returns both apiKey AND headers, custom-header+defaults merges with resolver winning, mutual-exclusion guard, Authorization-shadow guard, custom-auth-shadow guard, cross-touchpoint parity for all four (embedding/expansion/chat/ reranker). - test/ai/header-transport.test.ts (3 cases) — proves headers actually reach the wire. Synthetic recipes with resolveOpenAICompatConfig fetch wrappers capture outgoing Headers on embed/chat/rerank. Asserts Authorization + HTTP-Referer + X-OpenRouter-Title + X-Title all present. Codex flagged the return-shape-only coverage gap during plan review. - test/ai/build-gateway-config.test.ts (7 cases) — 5-way env-baseURL passthrough sweep through the now-exported buildGatewayConfig. Uses withEnv() from test/helpers/with-env.ts for isolation compliance. Mops up pre-existing untested drift on LLAMA_SERVER/OLLAMA/LMSTUDIO/LITELLM in the same pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: add OpenRouter to embedding-providers + bump recipe count 15 -> 16 recipes. Adds OpenRouter row to the TL;DR table, a setup section covering the value-prop (one key, many hosted models), env-var overrides (OPENROUTER_BASE_URL, OPENROUTER_REFERER, OPENROUTER_TITLE), the subagent- loop limitation (isAnthropicProvider() gate), and a "One key for many hosted models" bullet under the decision tree. README updated to match. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: bump v0.37.2.0 version refs to v0.37.4.0 across in-tree comments v0.37.2.0 was claimed by master's takes_resolution_consistency hotfix (#1211) before this branch could land. This commit re-stamps the source comments that reference the OpenRouter recipe / default_headers seam to v0.37.4.0 so the in-tree version markers match the actual landing version. No behavior change — comments only. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v0.37.4.0) One key, many hosted models — OpenRouter recipe lands. Cherry-picked from #1210 (@davemorin), with codex review corrections folded in: - recipe count math (16 not 17) - current OR attribution header name (X-OpenRouter-Title, X-Title back-compat) - max_batch_tokens semantic (300K aggregate per-request, not 8192 per-input) - Matryoshka dims_options for text-embedding-3-small - auth-shadow guard at applyResolveAuth Adds the generic Recipe.default_headers / resolveDefaultHeaders seam so attribution headers ride alongside Bearer auth. Future Together/Groq adoption tracked in TODOS.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore: rebump to v0.37.6.0 (queue moved past v0.37.4/v0.37.5) VERSION + package.json + CHANGELOG header + CLAUDE.md + TODOS.md + in-tree source comments + llms regen. No code-behavior change. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
88 lines
3.5 KiB
TypeScript
88 lines
3.5 KiB
TypeScript
/**
|
|
* buildGatewayConfig env-baseURL passthrough sweep (v0.37.2.0).
|
|
*
|
|
* Mops up pre-existing untested drift: every `_BASE_URL` env var the CLI
|
|
* reads (LLAMA_SERVER, OLLAMA, LMSTUDIO, LITELLM, OPENROUTER) was previously
|
|
* uncovered by unit tests. The helper was file-local so the test surface
|
|
* didn't exist; v0.37.2.0 exports it for the OR passthrough plus the four
|
|
* legacy passthroughs by parameterized sweep.
|
|
*
|
|
* Behavior contract:
|
|
* - When the env var is set, buildGatewayConfig(c).base_urls[recipeId] === envValue.
|
|
* - When the env var is unset, base_urls[recipeId] is undefined (no spurious key).
|
|
* - Caller-provided cfg.provider_base_urls overrides the env value.
|
|
*
|
|
* Env-mutation discipline: every env mutation routes through `withEnv()` from
|
|
* `test/helpers/with-env.ts`. Process-global env mutations would leak across
|
|
* files in the same shard. `withEnv` save/restore via try/finally is the
|
|
* canonical pattern (enforced by scripts/check-test-isolation.sh).
|
|
*/
|
|
|
|
import { describe, expect, test } from 'bun:test';
|
|
import { buildGatewayConfig } from '../../src/cli.ts';
|
|
import type { GBrainConfig } from '../../src/core/config.ts';
|
|
import { withEnv } from '../helpers/with-env.ts';
|
|
|
|
const PASSTHROUGHS: Array<{ envVar: string; recipeId: string }> = [
|
|
{ envVar: 'LLAMA_SERVER_BASE_URL', recipeId: 'llama-server' },
|
|
{ envVar: 'OLLAMA_BASE_URL', recipeId: 'ollama' },
|
|
{ envVar: 'LMSTUDIO_BASE_URL', recipeId: 'lmstudio' },
|
|
{ envVar: 'LITELLM_BASE_URL', recipeId: 'litellm' },
|
|
{ envVar: 'OPENROUTER_BASE_URL', recipeId: 'openrouter' },
|
|
];
|
|
|
|
const TEST_VALUE = 'http://proxy.example.test/v1';
|
|
|
|
const baseConfig: GBrainConfig = {} as unknown as GBrainConfig;
|
|
|
|
/**
|
|
* Build an env-override object that clears every passthrough and sets one.
|
|
* Other tests in the same shard may have set these; clearing all first ensures
|
|
* the test asserts on a clean slate without manual saveEnv/restoreEnv bookkeeping.
|
|
*/
|
|
function envFor(target: { envVar: string } | null): Record<string, string | undefined> {
|
|
const overrides: Record<string, string | undefined> = {};
|
|
for (const { envVar } of PASSTHROUGHS) {
|
|
overrides[envVar] = target?.envVar === envVar ? TEST_VALUE : undefined;
|
|
}
|
|
return overrides;
|
|
}
|
|
|
|
describe('buildGatewayConfig env-baseURL passthrough', () => {
|
|
for (const passthrough of PASSTHROUGHS) {
|
|
test(`${passthrough.envVar} flows through to base_urls.${passthrough.recipeId}`, async () => {
|
|
await withEnv(envFor(passthrough), async () => {
|
|
const cfg = buildGatewayConfig(baseConfig);
|
|
expect(
|
|
cfg.base_urls?.[passthrough.recipeId],
|
|
`${passthrough.envVar} → base_urls.${passthrough.recipeId}`,
|
|
).toBe(TEST_VALUE);
|
|
});
|
|
});
|
|
}
|
|
|
|
test('unset env vars do NOT populate base_urls keys', async () => {
|
|
await withEnv(envFor(null), async () => {
|
|
const cfg = buildGatewayConfig(baseConfig);
|
|
for (const { recipeId } of PASSTHROUGHS) {
|
|
expect(
|
|
cfg.base_urls?.[recipeId],
|
|
`${recipeId} key should be absent when env unset`,
|
|
).toBeUndefined();
|
|
}
|
|
});
|
|
});
|
|
|
|
test('caller-provided provider_base_urls override env (config wins)', async () => {
|
|
await withEnv(
|
|
{ ...envFor(null), OPENROUTER_BASE_URL: 'http://env.example/v1' },
|
|
async () => {
|
|
const cfg = buildGatewayConfig({
|
|
provider_base_urls: { openrouter: 'http://config.example/v1' },
|
|
} as unknown as GBrainConfig);
|
|
expect(cfg.base_urls?.openrouter).toBe('http://config.example/v1');
|
|
},
|
|
);
|
|
});
|
|
});
|