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 { resolveWorkingDirectoryChange } from './workingDirectoryChange';
|
||||||
import { registerManagedProcess, unregisterManagedProcess, reapOrphanedProcesses } from './opencodeProcessRegistry';
|
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 t = vscode.l10n.t;
|
||||||
|
|
||||||
const READY_CHECK_TIMEOUT_MS = 30000;
|
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 launch = resolveWindowsLaunchSpec(binary, ['serve', '--hostname', '127.0.0.1', '--port', String(port)]);
|
||||||
const child = spawn(launch.binary, launch.args, {
|
const child = spawn(launch.binary, launch.args, {
|
||||||
cwd: workingDirectory,
|
cwd: workingDirectory,
|
||||||
env: { ...process.env },
|
env: applyProviderEnvAliases({ ...process.env }),
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
windowsHide: true,
|
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/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/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/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-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/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.
|
- `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
|
be replaced by injected values. External OpenCode processes receive no
|
||||||
OpenChamber tool injection.
|
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.
|
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.
|
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 { spawn, spawnSync } from 'node:child_process';
|
||||||
import net from 'node:net';
|
import net from 'node:net';
|
||||||
import { registerManagedProcess, unregisterManagedProcess, reapOrphanedProcesses } from './managed-process-registry.js';
|
import { registerManagedProcess, unregisterManagedProcess, reapOrphanedProcesses } from './managed-process-registry.js';
|
||||||
|
import { applyProviderEnvAliases } from './provider-env-aliases.js';
|
||||||
import { recordStartupPerformance } from './startup-performance.js';
|
import { recordStartupPerformance } from './startup-performance.js';
|
||||||
|
|
||||||
const parsePositiveInt = (value, fallback) => {
|
const parsePositiveInt = (value, fallback) => {
|
||||||
@@ -518,13 +519,13 @@ export const createOpenCodeLifecycleRuntime = (deps) => {
|
|||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
cwd: state.openCodeWorkingDirectory,
|
cwd: state.openCodeWorkingDirectory,
|
||||||
shellEnvKeysCount: Object.keys(shellEnv).length,
|
shellEnvKeysCount: Object.keys(shellEnv).length,
|
||||||
env: {
|
env: applyProviderEnvAliases({
|
||||||
...shellEnv,
|
...shellEnv,
|
||||||
...process.env,
|
...process.env,
|
||||||
...managedOpenCodeEnv,
|
...managedOpenCodeEnv,
|
||||||
PATH: envPath,
|
PATH: envPath,
|
||||||
OPENCODE_SERVER_PASSWORD: openCodePassword,
|
OPENCODE_SERVER_PASSWORD: openCodePassword,
|
||||||
},
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!serverInstance || !serverInstance.url) {
|
if (!serverInstance || !serverInstance.url) {
|
||||||
|
|||||||
@@ -313,6 +313,51 @@ describe('OpenCode lifecycle', () => {
|
|||||||
await server.close();
|
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 () => {
|
it('falls back to buildAugmentedPath when buildManagedOpenCodePath is not provided', async () => {
|
||||||
delete process.env.OPENCODE_BINARY;
|
delete process.env.OPENCODE_BINARY;
|
||||||
const child = createMockChild();
|
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