mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 00:48:18 +00:00
feat: support OpenRouter API key in config (#1714)
* feat: support OpenRouter API key in config * fixup: dedupe openrouter_api_key vs master, drop no-op compile-guard test Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Garry Tan <garrytan@gmail.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
Garry Tan
parent
3a28d2612a
commit
4320527785
+2
-1
@@ -39,10 +39,11 @@ gbrain migrate --to pglite # Postgres → PGLite (rare)
|
||||
|
||||
For shared / large / multi-machine deployments (a team or company brain with multiple users hitting one server over HTTP MCP with OAuth scoping per user), follow the dedicated walkthrough: **[Tutorial: set up GBrain as your company brain](tutorials/company-brain.md)**.
|
||||
|
||||
API keys live in `~/.gbrain/config.json` (file plane) or env vars (`OPENAI_API_KEY`, `ZEROENTROPY_API_KEY`, `VOYAGE_API_KEY`, `ANTHROPIC_API_KEY`). Set via CLI:
|
||||
API keys live in `~/.gbrain/config.json` (file plane) or env vars (`OPENAI_API_KEY`, `OPENROUTER_API_KEY`, `ZEROENTROPY_API_KEY`, `VOYAGE_API_KEY`, `ANTHROPIC_API_KEY`). Set via CLI:
|
||||
|
||||
```bash
|
||||
gbrain config set zeroentropy_api_key sk-...
|
||||
gbrain config set openrouter_api_key sk-or-...
|
||||
gbrain config set anthropic_api_key sk-ant-...
|
||||
```
|
||||
|
||||
|
||||
@@ -159,7 +159,8 @@ proxy for worker env.
|
||||
If a brain DB ever traverses a trust boundary, secrets stay out.
|
||||
- **Free-form names.** `inherit:` accepts any snake_case config-key on your
|
||||
worker — `database_url`, `anthropic_api_key`, `openai_api_key`,
|
||||
`voyage_api_key`, `groq_api_key`, `zeroentropy_api_key`, or any custom
|
||||
`openrouter_api_key`, `voyage_api_key`, `groq_api_key`,
|
||||
`zeroentropy_api_key`, or any custom
|
||||
field you stuff into `~/.gbrain/config.json`. The agent picks what it
|
||||
needs.
|
||||
- **`env:` still works** for non-secret values, or for cases where you
|
||||
|
||||
@@ -155,6 +155,7 @@ child-spawn time:
|
||||
- `inherit: ["database_url"]` → child env `GBRAIN_DATABASE_URL`
|
||||
- `inherit: ["anthropic_api_key"]` → child env `ANTHROPIC_API_KEY`
|
||||
- `inherit: ["openai_api_key"]` → child env `OPENAI_API_KEY`
|
||||
- `inherit: ["openrouter_api_key"]` → child env `OPENROUTER_API_KEY`
|
||||
- `inherit: ["voyage_api_key"]` → child env `VOYAGE_API_KEY`
|
||||
- `inherit: ["groq_api_key", "zeroentropy_api_key"]` → both injected
|
||||
- Or any arbitrary config-key your worker has (`my_custom_field` →
|
||||
|
||||
@@ -103,7 +103,7 @@ For GCP service-account / Vertex AI auth (production deployments), see the v0.32
|
||||
|
||||
### OpenRouter
|
||||
|
||||
Single OpenAI-compatible API for fan-out to OpenAI, Anthropic, Google, DeepSeek, Meta Llama, Qwen, and dozens of other hosted providers. One key, many models. Set `OPENROUTER_API_KEY` and use `openrouter:<provider>/<model>` (e.g. `openrouter:openai/gpt-5.2`, `openrouter:anthropic/claude-sonnet-4.6`).
|
||||
Single OpenAI-compatible API for fan-out to OpenAI, Anthropic, Google, DeepSeek, Meta Llama, Qwen, and dozens of other hosted providers. One key, many models. Set `OPENROUTER_API_KEY` or `openrouter_api_key` in `~/.gbrain/config.json`, then use `openrouter:<provider>/<model>` (e.g. `openrouter:openai/gpt-5.2`, `openrouter:anthropic/claude-sonnet-4.6`).
|
||||
|
||||
**Embedding**: `openai/text-embedding-3-small` (1536d default, Matryoshka shrink to 512/768/1024). OR's embedding catalog also includes `text-embedding-3-large`, `google/gemini-embedding-2-preview`, `qwen/qwen3-embedding-8b`, `bge-m3` — opt in via `--embedding-model openrouter:<id>`. Pricing matches the upstream provider (OR adds a small markup).
|
||||
|
||||
|
||||
@@ -231,6 +231,6 @@ export const openrouter: Recipe = {
|
||||
},
|
||||
},
|
||||
setup_hint:
|
||||
'Get an API key at https://openrouter.ai/settings/keys, then `export OPENROUTER_API_KEY=...` and use `openrouter:<provider>/<model>`. Optional overrides: OPENROUTER_BASE_URL (proxy), OPENROUTER_REFERER (attribution URL), OPENROUTER_TITLE (attribution name).',
|
||||
'Get an API key at https://openrouter.ai/settings/keys, then `export OPENROUTER_API_KEY=...` or set `openrouter_api_key` in ~/.gbrain/config.json and use `openrouter:<provider>/<model>`. Optional overrides: OPENROUTER_BASE_URL (proxy), OPENROUTER_REFERER (attribution URL), OPENROUTER_TITLE (attribution name).',
|
||||
compat: { fetch: openrouterCompatFetch },
|
||||
};
|
||||
|
||||
@@ -24,6 +24,7 @@ describe('KNOWN_CONFIG_KEYS', () => {
|
||||
expect(KNOWN_CONFIG_KEYS).toContain('embedding_disabled'); // v0.37 D9
|
||||
expect(KNOWN_CONFIG_KEYS).toContain('expansion_model');
|
||||
expect(KNOWN_CONFIG_KEYS).toContain('chat_model');
|
||||
expect(KNOWN_CONFIG_KEYS).toContain('openrouter_api_key');
|
||||
expect(KNOWN_CONFIG_KEYS).toContain('provider_chat_options');
|
||||
});
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ describe('isSensitiveConfigKey (v0.36.x #892 regression)', () => {
|
||||
test('matches common sensitive key shapes', () => {
|
||||
expect(isSensitiveConfigKey('openai_api_key')).toBe(true);
|
||||
expect(isSensitiveConfigKey('anthropic_api_key')).toBe(true);
|
||||
expect(isSensitiveConfigKey('openrouter_api_key')).toBe(true);
|
||||
expect(isSensitiveConfigKey('voyage_api_key')).toBe(true);
|
||||
expect(isSensitiveConfigKey('admin_token')).toBe(true);
|
||||
expect(isSensitiveConfigKey('database.password')).toBe(true);
|
||||
@@ -93,6 +94,7 @@ describe('isSensitiveConfigKey (v0.36.x #892 regression)', () => {
|
||||
describe('redactConfigValue (v0.36.x #892 — set output regression)', () => {
|
||||
test('redacts sensitive keys to ***', () => {
|
||||
expect(redactConfigValue('openai_api_key', 'sk-test-123')).toBe('***');
|
||||
expect(redactConfigValue('openrouter_api_key', 'sk-or-test-123')).toBe('***');
|
||||
expect(redactConfigValue('admin_token', 'eyJhbGciOiJIUzI1NiJ9')).toBe('***');
|
||||
});
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ describe('INHERIT_NAME_RE', () => {
|
||||
'database_url',
|
||||
'anthropic_api_key',
|
||||
'openai_api_key',
|
||||
'openrouter_api_key',
|
||||
'voyage_api_key',
|
||||
'groq_api_key',
|
||||
'zeroentropy_api_key',
|
||||
@@ -61,6 +62,9 @@ describe('deriveEnvKey', () => {
|
||||
test('openai_api_key → OPENAI_API_KEY', () => {
|
||||
expect(deriveEnvKey('openai_api_key')).toBe('OPENAI_API_KEY');
|
||||
});
|
||||
test('openrouter_api_key → OPENROUTER_API_KEY', () => {
|
||||
expect(deriveEnvKey('openrouter_api_key')).toBe('OPENROUTER_API_KEY');
|
||||
});
|
||||
test('voyage_api_key → VOYAGE_API_KEY', () => {
|
||||
expect(deriveEnvKey('voyage_api_key')).toBe('VOYAGE_API_KEY');
|
||||
});
|
||||
@@ -108,11 +112,13 @@ describe('integration: deriveEnvKey + resolveInheritValue work together', () =>
|
||||
database_url: 'postgresql://x',
|
||||
anthropic_api_key: 'sk-ant-x',
|
||||
openai_api_key: 'sk-x',
|
||||
openrouter_api_key: 'sk-or-x',
|
||||
};
|
||||
test.each([
|
||||
['database_url', 'GBRAIN_DATABASE_URL', 'postgresql://x'],
|
||||
['anthropic_api_key', 'ANTHROPIC_API_KEY', 'sk-ant-x'],
|
||||
['openai_api_key', 'OPENAI_API_KEY', 'sk-x'],
|
||||
['openrouter_api_key', 'OPENROUTER_API_KEY', 'sk-or-x'],
|
||||
])('name %s resolves to envKey %s with value %s', (name, expectedEnvKey, expectedValue) => {
|
||||
expect(deriveEnvKey(name)).toBe(expectedEnvKey);
|
||||
expect(resolveInheritValue(cfg, name)).toBe(expectedValue);
|
||||
|
||||
@@ -141,6 +141,7 @@ describe('v0.37 Lane B — init paths', () => {
|
||||
process.env.GBRAIN_EMBEDDING_MODEL = 'voyage:voyage-3-large';
|
||||
process.env.GBRAIN_EMBEDDING_DIMENSIONS = '2048';
|
||||
process.env.OPENAI_API_KEY = 'sk-from-env';
|
||||
process.env.OPENROUTER_API_KEY = 'sk-or-from-env';
|
||||
|
||||
// Force re-import to pick up env state (the module-level resolver in
|
||||
// config.ts reads process.env at call time, so this is safe).
|
||||
@@ -152,16 +153,19 @@ describe('v0.37 Lane B — init paths', () => {
|
||||
expect(fileOnly?.embedding_dimensions).toBe(1536);
|
||||
// CDX-5 regression: env keys must NOT leak into file-only loader.
|
||||
expect(fileOnly?.openai_api_key).toBeUndefined();
|
||||
expect(fileOnly?.openrouter_api_key).toBeUndefined();
|
||||
|
||||
// Control: loadConfig() DOES merge env.
|
||||
const merged = loadConfig();
|
||||
expect(merged?.embedding_model).toBe('voyage:voyage-3-large');
|
||||
expect(merged?.embedding_dimensions).toBe(2048);
|
||||
expect(merged?.openai_api_key).toBe('sk-from-env');
|
||||
expect(merged?.openrouter_api_key).toBe('sk-or-from-env');
|
||||
|
||||
delete process.env.GBRAIN_EMBEDDING_MODEL;
|
||||
delete process.env.GBRAIN_EMBEDDING_DIMENSIONS;
|
||||
delete process.env.OPENAI_API_KEY;
|
||||
delete process.env.OPENROUTER_API_KEY;
|
||||
});
|
||||
|
||||
test('B.4 / CDX-5: loadConfigFileOnly does NOT infer engine from DATABASE_URL', async () => {
|
||||
@@ -203,9 +207,11 @@ describe('v0.37 Lane C.3 — ZE key reaches buildGatewayConfig', () => {
|
||||
const savedZe = process.env.ZEROENTROPY_API_KEY;
|
||||
const savedOai = process.env.OPENAI_API_KEY;
|
||||
const savedAnth = process.env.ANTHROPIC_API_KEY;
|
||||
const savedOr = process.env.OPENROUTER_API_KEY;
|
||||
delete process.env.ZEROENTROPY_API_KEY;
|
||||
delete process.env.OPENAI_API_KEY;
|
||||
delete process.env.ANTHROPIC_API_KEY;
|
||||
delete process.env.OPENROUTER_API_KEY;
|
||||
try {
|
||||
const { buildGatewayConfig } = await import('../src/cli.ts');
|
||||
const cfg = {
|
||||
@@ -213,16 +219,19 @@ describe('v0.37 Lane C.3 — ZE key reaches buildGatewayConfig', () => {
|
||||
zeroentropy_api_key: 'test-ze-key',
|
||||
openai_api_key: 'test-oai',
|
||||
anthropic_api_key: 'test-anth',
|
||||
openrouter_api_key: 'test-or',
|
||||
};
|
||||
const gwCfg = buildGatewayConfig(cfg as any);
|
||||
expect(gwCfg.env?.ZEROENTROPY_API_KEY).toBe('test-ze-key');
|
||||
// Regression on the existing two keys.
|
||||
expect(gwCfg.env?.OPENAI_API_KEY).toBe('test-oai');
|
||||
expect(gwCfg.env?.ANTHROPIC_API_KEY).toBe('test-anth');
|
||||
expect(gwCfg.env?.OPENROUTER_API_KEY).toBe('test-or');
|
||||
} finally {
|
||||
if (savedZe !== undefined) process.env.ZEROENTROPY_API_KEY = savedZe;
|
||||
if (savedOai !== undefined) process.env.OPENAI_API_KEY = savedOai;
|
||||
if (savedAnth !== undefined) process.env.ANTHROPIC_API_KEY = savedAnth;
|
||||
if (savedOr !== undefined) process.env.OPENROUTER_API_KEY = savedOr;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user