Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions extensions/gemini/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,21 @@
"gemini.connection._type": {
"type": "string",
"scope": "InferenceProviderConnection",
"description": "Gemini provider type",
"description": "OpenShell provider type",
"hidden": true
},
"gemini.connection.GEMINI_API_KEY": {
"gemini.connection.OPENAI_API_KEY": {
"type": "string",
"scope": "InferenceProviderConnection",
"description": "API key secret reference",
"format": "password",
"hidden": true
},
"gemini.connection.OPENAI_BASE_URL": {
"type": "string",
"scope": "InferenceProviderConnection",
"description": "Gemini OpenAI-compatible base URL",
"hidden": true
}
}
}
Expand Down
21 changes: 14 additions & 7 deletions extensions/gemini/src/gemini.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ describe('factory', () => {
name: 'dum*****',
type: 'cloud',
llmMetadata: { name: 'gemini' },
endpoint: 'https://generativelanguage.googleapis.com/v1beta/openai/',
status: expect.any(Function),
lifecycle: {
delete: expect.any(Function),
Expand All @@ -313,7 +314,8 @@ describe('factory', () => {

expect(SAFE_STORAGE_MOCK.delete).toHaveBeenCalledWith('gemini:fake-uuid-1:token');
expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('gemini.connection._type', undefined);
expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('gemini.connection.GEMINI_API_KEY', undefined);
expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('gemini.connection.OPENAI_API_KEY', undefined);
expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('gemini.connection.OPENAI_BASE_URL', undefined);
});
});

Expand Down Expand Up @@ -353,7 +355,8 @@ describe('connection delete lifecycle', () => {

// Verify configuration clearing
expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('gemini.connection._type', undefined);
expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('gemini.connection.GEMINI_API_KEY', undefined);
expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('gemini.connection.OPENAI_API_KEY', undefined);
expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('gemini.connection.OPENAI_BASE_URL', undefined);

// Verify storage update - the tokens list is updated after per-connection secret
expect(SAFE_STORAGE_MOCK.store).toHaveBeenCalledWith(TOKENS_KEY, JSON.stringify([]));
Expand Down Expand Up @@ -415,11 +418,15 @@ describe('workspace configuration', () => {
expect(configuration.getConfiguration).toHaveBeenCalledWith(undefined, connection);

// Verify configuration updates
expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('gemini.connection._type', PROVIDER_ID);
expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('gemini.connection._type', 'openai');
expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith(
'gemini.connection.GEMINI_API_KEY',
'gemini.connection.OPENAI_API_KEY',
`${PROVIDER_ID}:fake-uuid-1:token`,
);
expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith(
'gemini.connection.OPENAI_BASE_URL',
'https://generativelanguage.googleapis.com/v1beta/openai/',
);
});

test('should set workspace configuration for each restored connection', async () => {
Expand All @@ -437,8 +444,8 @@ describe('workspace configuration', () => {
expect(SAFE_STORAGE_MOCK.store).toHaveBeenCalledWith(`${PROVIDER_ID}:id-2:token`, 'key2');

// Verify configuration updates for both connections
expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('gemini.connection._type', PROVIDER_ID);
expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('gemini.connection.GEMINI_API_KEY', `${PROVIDER_ID}:id-1:token`);
expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('gemini.connection.GEMINI_API_KEY', `${PROVIDER_ID}:id-2:token`);
expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('gemini.connection._type', 'openai');
expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('gemini.connection.OPENAI_API_KEY', `${PROVIDER_ID}:id-1:token`);
expect(CONFIG_UPDATE_MOCK).toHaveBeenCalledWith('gemini.connection.OPENAI_API_KEY', `${PROVIDER_ID}:id-2:token`);
});
});
21 changes: 16 additions & 5 deletions extensions/gemini/src/gemini.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ import { configuration } from '@openkaiden/api';
export const PROVIDER_ID = 'gemini';
export const TOKENS_KEY = 'gemini:tokens';

// OpenShell has no dedicated "gemini" provider type — it only understands
// protocol families (openai, anthropic, ...). Google publishes an
// OpenAI-compatible endpoint that accepts a plain Bearer API key, so the
// connection is registered with OpenShell as an "openai" provider pointed
// at that endpoint.
const OPENSHELL_PROVIDER_TYPE = 'openai';
const OPENAI_COMPAT_BASE_URL = 'https://generativelanguage.googleapis.com/v1beta/openai/';

export interface StoredConnection {
id: string;
token: string;
Expand Down Expand Up @@ -66,7 +74,7 @@ export class Gemini implements Disposable {
// register MCP Provider connection factory
this.provider?.setInferenceProviderConnectionFactory({
connectionTypes: ['cloud'],
llmMetadata: { name: 'gemini' },
llmMetadata: { name: PROVIDER_ID },
create: this.mcpFactory.bind(this),
});

Expand Down Expand Up @@ -126,8 +134,9 @@ export class Gemini implements Disposable {
await this.secrets.store(secretName, token);

const config = configuration.getConfiguration(undefined, connection);
await config.update('gemini.connection._type', PROVIDER_ID);
await config.update('gemini.connection.GEMINI_API_KEY', secretName);
await config.update('gemini.connection._type', OPENSHELL_PROVIDER_TYPE);
await config.update('gemini.connection.OPENAI_API_KEY', secretName);
await config.update('gemini.connection.OPENAI_BASE_URL', OPENAI_COMPAT_BASE_URL);
}

private async clearConnectionConfiguration(connection: InferenceProviderConnection): Promise<void> {
Expand All @@ -136,7 +145,8 @@ export class Gemini implements Disposable {

const config = configuration.getConfiguration(undefined, connection);
await config.update('gemini.connection._type', undefined);
await config.update('gemini.connection.GEMINI_API_KEY', undefined);
await config.update('gemini.connection.OPENAI_API_KEY', undefined);
await config.update('gemini.connection.OPENAI_BASE_URL', undefined);
}

private async registerInferenceProviderConnection({ id, token }: { id: string; token: string }): Promise<void> {
Expand Down Expand Up @@ -165,7 +175,8 @@ export class Gemini implements Disposable {
id,
name: this.maskKey(token),
type: 'cloud',
llmMetadata: { name: 'gemini' },
llmMetadata: { name: PROVIDER_ID },
endpoint: OPENAI_COMPAT_BASE_URL,
sdk: google,
status(): ProviderConnectionStatus {
return status;
Expand Down
2 changes: 1 addition & 1 deletion extensions/opencode/src/extension.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ describe('activate', () => {
ollama: {
name: 'ollama',
npm: '@ai-sdk/openai-compatible',
options: { baseURL: 'http://localhost:11434/v1' },
options: { apiKey: '{env:OPENAI_API_KEY}', baseURL: 'http://localhost:11434/v1' },
models: {
llama3: { _launch: true, name: 'llama3' },
},
Expand Down
2 changes: 1 addition & 1 deletion extensions/opencode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export async function activate(extensionContext: ExtensionContext): Promise<void

providerEntry.name = provider;
providerEntry.npm = NATIVE_PROVIDER_SDKS[provider] ?? '@ai-sdk/openai-compatible';
providerEntry.options = { ...providerEntry.options, baseURL: endpoint };
providerEntry.options = { apiKey: '{env:OPENAI_API_KEY}', ...providerEntry.options, baseURL: endpoint };

providerEntry.models ??= {};
providerEntry.models[modelName] ??= { _launch: true, name: modelName };
Expand Down
Loading