Mirror Google API key env aliases into managed OpenCode.

OpenCode can mark Google connected via GEMINI_API_KEY while the Generative AI
SDK only reads GOOGLE_GENERATIVE_AI_API_KEY, so chat asked for a key that was
already present. Alias unset sibling names on managed launch for web and VS Code.

Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
This commit is contained in:
Cursor Agent
2026-08-03 13:18:27 +00:00
co-authored by Serhii Dziupin
parent 9cccc8d667
commit 321794a621
6 changed files with 148 additions and 3 deletions
+25 -1
View File
@@ -11,6 +11,30 @@ import { normalizeWindowsDriveLetter } from './pathUtils';
import { resolveWorkingDirectoryChange } from './workingDirectoryChange';
import { registerManagedProcess, unregisterManagedProcess, reapOrphanedProcesses } from './opencodeProcessRegistry';
/** Keep in sync with packages/web/server/lib/opencode/provider-env-aliases.js */
const GOOGLE_API_KEY_ALIASES = [
'GOOGLE_GENERATIVE_AI_API_KEY',
'GOOGLE_API_KEY',
'GEMINI_API_KEY',
] as const;
function applyProviderEnvAliases(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
const next: NodeJS.ProcessEnv = { ...env };
const googleValue = GOOGLE_API_KEY_ALIASES
.map((key) => next[key])
.find((value) => typeof value === 'string' && value.trim().length > 0);
if (googleValue) {
for (const key of GOOGLE_API_KEY_ALIASES) {
if (typeof next[key] !== 'string' || next[key]!.trim().length === 0) {
next[key] = googleValue;
}
}
}
return next;
}
const t = vscode.l10n.t;
const READY_CHECK_TIMEOUT_MS = 30000;
@@ -667,7 +691,7 @@ async function spawnManagedOpenCodeServer(
const launch = resolveWindowsLaunchSpec(binary, ['serve', '--hostname', '127.0.0.1', '--port', String(port)]);
const child = spawn(launch.binary, launch.args, {
cwd: workingDirectory,
env: { ...process.env },
env: applyProviderEnvAliases({ ...process.env }),
stdio: ['ignore', 'pipe', 'pipe'],
windowsHide: true,
});
@@ -11,6 +11,7 @@ This module provides OpenCode server integration utilities for the web server ru
- `packages/web/server/lib/opencode/cli-entry-runtime.js`: CLI entrypoint runtime that detects direct execution, parses CLI options, and starts server bootstrap.
- `packages/web/server/lib/opencode/routes.js`: OpenCode/provider settings and auth-related route registration.
- `packages/web/server/lib/opencode/lifecycle.js`: OpenCode process lifecycle runtime (startup, restart, readiness, health monitoring). After readiness it warms the most recently used directories (`getWarmupDirectories` dep, sequential and best-effort) because OpenCode initializes each directory lazily on first request and that cost would otherwise be paid by the user's first interactive session open.
- `packages/web/server/lib/opencode/provider-env-aliases.js`: mirrors known provider credential env aliases into the managed OpenCode process environment (for example `GEMINI_API_KEY``GOOGLE_GENERATIVE_AI_API_KEY`) so OpenCode connection detection and the upstream AI SDK agree on the same key names.
- `packages/web/server/lib/opencode/env-runtime.js`: OpenCode CLI/binary resolution and shell environment runtime.
- `packages/web/server/lib/opencode/env-config.js`: OpenCode-related environment variable parsing and validation (host/port/hostname).
- `packages/web/server/lib/opencode/hmr-state-runtime.js`: HMR-persistent runtime state initialization, auth-state bootstrap, and HMR sync helpers.
@@ -123,6 +124,12 @@ runtime. PATH and `OPENCODE_SERVER_PASSWORD` remain lifecycle-owned and cannot
be replaced by injected values. External OpenCode processes receive no
OpenChamber tool injection.
Before spawn, `applyProviderEnvAliases` fills unset Google credential aliases
from any present sibling (`GOOGLE_GENERATIVE_AI_API_KEY`, `GOOGLE_API_KEY`,
`GEMINI_API_KEY`) so a shell that only exports `GEMINI_API_KEY` still satisfies
the Generative AI SDK path used at chat time. Existing non-empty values are
never overwritten.
Set `OPENCHAMBER_STARTUP_PERF=1` to emit bounded startup phase records for server listen, managed OpenCode preparation/readiness, and proxy readiness holds. Every OpenCode bootstrap emits one terminal `opencode.bootstrap.ready` or `opencode.bootstrap.error` event, including reused and external server paths. Records contain controlled phase/outcome/route labels and timing values only; they never contain request URLs, runtime keys, directories, session IDs, credentials, or content.
macOS `say` voice enumeration starts concurrently with server composition. The server listener and managed OpenCode startup do not wait for it; `/api/tts/say/status` awaits the same authoritative capability promise when queried before enumeration completes.
@@ -1,6 +1,7 @@
import { spawn, spawnSync } from 'node:child_process';
import net from 'node:net';
import { registerManagedProcess, unregisterManagedProcess, reapOrphanedProcesses } from './managed-process-registry.js';
import { applyProviderEnvAliases } from './provider-env-aliases.js';
import { recordStartupPerformance } from './startup-performance.js';
const parsePositiveInt = (value, fallback) => {
@@ -518,13 +519,13 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
timeout: 30000,
cwd: state.openCodeWorkingDirectory,
shellEnvKeysCount: Object.keys(shellEnv).length,
env: {
env: applyProviderEnvAliases({
...shellEnv,
...process.env,
...managedOpenCodeEnv,
PATH: envPath,
OPENCODE_SERVER_PASSWORD: openCodePassword,
},
}),
});
if (!serverInstance || !serverInstance.url) {
@@ -313,6 +313,51 @@ describe('OpenCode lifecycle', () => {
await server.close();
});
it('mirrors Google credential env aliases into the managed OpenCode environment', async () => {
const previousGemini = process.env.GEMINI_API_KEY;
const previousGoogleGen = process.env.GOOGLE_GENERATIVE_AI_API_KEY;
const previousGoogle = process.env.GOOGLE_API_KEY;
process.env.GEMINI_API_KEY = 'AIza-from-gemini';
delete process.env.GOOGLE_GENERATIVE_AI_API_KEY;
delete process.env.GOOGLE_API_KEY;
try {
const child = createMockChild();
spawnMock.mockImplementationOnce(() => {
queueMicrotask(() => {
child.stdout.emit('data', 'opencode server listening on http://127.0.0.1:45678\n');
});
return child;
});
const runtime = createRuntime();
const server = await runtime.startOpenCode();
const [, , options] = spawnMock.mock.calls[0];
expect(options.env.GEMINI_API_KEY).toBe('AIza-from-gemini');
expect(options.env.GOOGLE_API_KEY).toBe('AIza-from-gemini');
expect(options.env.GOOGLE_GENERATIVE_AI_API_KEY).toBe('AIza-from-gemini');
await server.close();
} finally {
if (typeof previousGemini === 'string') {
process.env.GEMINI_API_KEY = previousGemini;
} else {
delete process.env.GEMINI_API_KEY;
}
if (typeof previousGoogleGen === 'string') {
process.env.GOOGLE_GENERATIVE_AI_API_KEY = previousGoogleGen;
} else {
delete process.env.GOOGLE_GENERATIVE_AI_API_KEY;
}
if (typeof previousGoogle === 'string') {
process.env.GOOGLE_API_KEY = previousGoogle;
} else {
delete process.env.GOOGLE_API_KEY;
}
}
});
it('falls back to buildAugmentedPath when buildManagedOpenCodePath is not provided', async () => {
delete process.env.OPENCODE_BINARY;
const child = createMockChild();
@@ -0,0 +1,35 @@
/**
* Normalize provider credential env aliases for managed OpenCode.
*
* OpenCode may mark a provider as connected when any listed env key is present
* (e.g. GEMINI_API_KEY), while the upstream AI SDK only reads a different name
* (GOOGLE_GENERATIVE_AI_API_KEY). Mirror known aliases so chat works without
* forcing the user to paste the same key again in Settings.
*/
const GOOGLE_API_KEY_ALIASES = [
'GOOGLE_GENERATIVE_AI_API_KEY',
'GOOGLE_API_KEY',
'GEMINI_API_KEY',
];
export function applyProviderEnvAliases(env) {
if (!env || typeof env !== 'object') {
return {};
}
const next = { ...env };
const googleValue = GOOGLE_API_KEY_ALIASES
.map((key) => next[key])
.find((value) => typeof value === 'string' && value.trim().length > 0);
if (googleValue) {
for (const key of GOOGLE_API_KEY_ALIASES) {
if (typeof next[key] !== 'string' || next[key].trim().length === 0) {
next[key] = googleValue;
}
}
}
return next;
}
@@ -0,0 +1,33 @@
import { describe, expect, test } from 'bun:test';
import { applyProviderEnvAliases } from './provider-env-aliases.js';
describe('applyProviderEnvAliases', () => {
test('mirrors GEMINI_API_KEY onto Google Generative AI env names', () => {
expect(applyProviderEnvAliases({
GEMINI_API_KEY: 'AIza-demo',
PATH: '/usr/bin',
})).toEqual({
GEMINI_API_KEY: 'AIza-demo',
GOOGLE_API_KEY: 'AIza-demo',
GOOGLE_GENERATIVE_AI_API_KEY: 'AIza-demo',
PATH: '/usr/bin',
});
});
test('does not overwrite an already-set preferred Google key', () => {
expect(applyProviderEnvAliases({
GEMINI_API_KEY: 'from-gemini',
GOOGLE_GENERATIVE_AI_API_KEY: 'from-google',
})).toEqual({
GEMINI_API_KEY: 'from-gemini',
GOOGLE_API_KEY: 'from-google',
GOOGLE_GENERATIVE_AI_API_KEY: 'from-google',
});
});
test('returns empty object for invalid input', () => {
expect(applyProviderEnvAliases(null)).toEqual({});
expect(applyProviderEnvAliases(undefined)).toEqual({});
});
});