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:
co-authored by
Serhii Dziupin
parent
9cccc8d667
commit
321794a621
@@ -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({});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user