deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
10
packages/ai-anthropic/.eslintrc.js
Normal file
10
packages/ai-anthropic/.eslintrc.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
extends: [
|
||||
'../../configs/build.eslintrc.json'
|
||||
],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: 'tsconfig.json'
|
||||
}
|
||||
};
|
||||
33
packages/ai-anthropic/README.md
Normal file
33
packages/ai-anthropic/README.md
Normal file
@@ -0,0 +1,33 @@
|
||||
<div align='center'>
|
||||
|
||||
<br />
|
||||
|
||||
<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />
|
||||
|
||||
<h2>ECLIPSE THEIA - ANTHROPIC EXTENSION</h2>
|
||||
|
||||
<hr />
|
||||
|
||||
</div>
|
||||
|
||||
## Description
|
||||
|
||||
The `@theia/anthropic` integrates Anthropic's models with Theia AI.
|
||||
The Anthropic API key and the models to use can be configured via preferences.
|
||||
Alternatively the API key can also be handed in via the `ANTHROPIC_API_KEY` environment variable.
|
||||
|
||||
## Additional Information
|
||||
|
||||
- [API documentation for `@theia/ai-anthropic`](https://eclipse-theia.github.io/theia/docs/next/modules/_theia_ai-anthropic.html)
|
||||
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
|
||||
- [Theia - Website](https://theia-ide.org/)
|
||||
|
||||
## License
|
||||
|
||||
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
|
||||
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
|
||||
|
||||
## Trademark
|
||||
|
||||
"Theia" is a trademark of the Eclipse Foundation
|
||||
<https://www.eclipse.org/theia>
|
||||
51
packages/ai-anthropic/package.json
Normal file
51
packages/ai-anthropic/package.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"name": "@theia/ai-anthropic",
|
||||
"version": "1.68.0",
|
||||
"description": "Theia - Anthropic Integration",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.65.0",
|
||||
"@theia/ai-core": "1.68.0",
|
||||
"@theia/core": "1.68.0",
|
||||
"undici": "^7.16.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"theiaExtensions": [
|
||||
{
|
||||
"frontend": "lib/browser/anthropic-frontend-module",
|
||||
"backend": "lib/node/anthropic-backend-module"
|
||||
}
|
||||
],
|
||||
"keywords": [
|
||||
"theia-extension"
|
||||
],
|
||||
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/eclipse-theia/theia.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/eclipse-theia/theia/issues"
|
||||
},
|
||||
"homepage": "https://github.com/eclipse-theia/theia",
|
||||
"files": [
|
||||
"lib",
|
||||
"src"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "theiaext build",
|
||||
"clean": "theiaext clean",
|
||||
"compile": "theiaext compile",
|
||||
"lint": "theiaext lint",
|
||||
"test": "theiaext test",
|
||||
"watch": "theiaext watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@theia/ext-scripts": "1.68.0"
|
||||
},
|
||||
"nyc": {
|
||||
"extends": "../../configs/nyc.json"
|
||||
},
|
||||
"gitHead": "21358137e41342742707f660b8e222f940a27652"
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 EclipseSource GmbH.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { AnthropicLanguageModelsManager, AnthropicModelDescription } from '../common';
|
||||
import { API_KEY_PREF, MODELS_PREF } from '../common/anthropic-preferences';
|
||||
import { AICorePreferences, PREFERENCE_NAME_MAX_RETRIES } from '@theia/ai-core/lib/common/ai-core-preferences';
|
||||
import { PreferenceService } from '@theia/core';
|
||||
|
||||
const ANTHROPIC_PROVIDER_ID = 'anthropic';
|
||||
|
||||
// Model-specific maxTokens values
|
||||
const DEFAULT_MODEL_MAX_TOKENS: Record<string, number> = {
|
||||
'claude-3-opus-latest': 4096,
|
||||
'claude-3-5-haiku-latest': 8192,
|
||||
'claude-3-5-sonnet-latest': 8192,
|
||||
'claude-3-7-sonnet-latest': 64000,
|
||||
'claude-opus-4-20250514': 32000,
|
||||
'claude-sonnet-4-20250514': 64000,
|
||||
'claude-sonnet-4-5': 64000,
|
||||
'claude-sonnet-4-0': 64000,
|
||||
'claude-opus-4-5': 64000,
|
||||
'claude-opus-4-1': 32000
|
||||
};
|
||||
|
||||
@injectable()
|
||||
export class AnthropicFrontendApplicationContribution implements FrontendApplicationContribution {
|
||||
|
||||
@inject(PreferenceService)
|
||||
protected preferenceService: PreferenceService;
|
||||
|
||||
@inject(AnthropicLanguageModelsManager)
|
||||
protected manager: AnthropicLanguageModelsManager;
|
||||
|
||||
@inject(AICorePreferences)
|
||||
protected aiCorePreferences: AICorePreferences;
|
||||
|
||||
protected prevModels: string[] = [];
|
||||
|
||||
onStart(): void {
|
||||
this.preferenceService.ready.then(() => {
|
||||
const apiKey = this.preferenceService.get<string>(API_KEY_PREF, undefined);
|
||||
this.manager.setApiKey(apiKey);
|
||||
|
||||
const proxyUri = this.preferenceService.get<string>('http.proxy', undefined);
|
||||
this.manager.setProxyUrl(proxyUri);
|
||||
|
||||
const models = this.preferenceService.get<string[]>(MODELS_PREF, []);
|
||||
this.manager.createOrUpdateLanguageModels(...models.map(modelId => this.createAnthropicModelDescription(modelId)));
|
||||
this.prevModels = [...models];
|
||||
|
||||
this.preferenceService.onPreferenceChanged(event => {
|
||||
if (event.preferenceName === API_KEY_PREF) {
|
||||
this.manager.setApiKey(this.preferenceService.get<string>(API_KEY_PREF, undefined));
|
||||
this.updateAllModels();
|
||||
} else if (event.preferenceName === MODELS_PREF) {
|
||||
this.handleModelChanges(this.preferenceService.get<string[]>(MODELS_PREF, []));
|
||||
} else if (event.preferenceName === 'http.proxy') {
|
||||
this.manager.setProxyUrl(this.preferenceService.get<string>('http.proxy', undefined));
|
||||
}
|
||||
});
|
||||
|
||||
this.aiCorePreferences.onPreferenceChanged(event => {
|
||||
if (event.preferenceName === PREFERENCE_NAME_MAX_RETRIES) {
|
||||
this.updateAllModels();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected handleModelChanges(newModels: string[]): void {
|
||||
const oldModels = new Set(this.prevModels);
|
||||
const updatedModels = new Set(newModels);
|
||||
|
||||
const modelsToRemove = [...oldModels].filter(model => !updatedModels.has(model));
|
||||
const modelsToAdd = [...updatedModels].filter(model => !oldModels.has(model));
|
||||
|
||||
this.manager.removeLanguageModels(...modelsToRemove.map(model => `${ANTHROPIC_PROVIDER_ID}/${model}`));
|
||||
this.manager.createOrUpdateLanguageModels(...modelsToAdd.map(modelId => this.createAnthropicModelDescription(modelId)));
|
||||
this.prevModels = newModels;
|
||||
}
|
||||
|
||||
protected updateAllModels(): void {
|
||||
const models = this.preferenceService.get<string[]>(MODELS_PREF, []);
|
||||
this.manager.createOrUpdateLanguageModels(...models.map(modelId => this.createAnthropicModelDescription(modelId)));
|
||||
}
|
||||
|
||||
protected createAnthropicModelDescription(modelId: string): AnthropicModelDescription {
|
||||
const id = `${ANTHROPIC_PROVIDER_ID}/${modelId}`;
|
||||
const maxTokens = DEFAULT_MODEL_MAX_TOKENS[modelId];
|
||||
const maxRetries = this.aiCorePreferences.get(PREFERENCE_NAME_MAX_RETRIES) ?? 3;
|
||||
|
||||
const description: AnthropicModelDescription = {
|
||||
id: id,
|
||||
model: modelId,
|
||||
apiKey: true,
|
||||
enableStreaming: true,
|
||||
useCaching: true,
|
||||
maxRetries: maxRetries
|
||||
};
|
||||
|
||||
if (maxTokens !== undefined) {
|
||||
description.maxTokens = maxTokens;
|
||||
} else {
|
||||
description.maxTokens = 64000;
|
||||
}
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 EclipseSource GmbH.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ContainerModule } from '@theia/core/shared/inversify';
|
||||
import { AnthropicPreferencesSchema } from '../common/anthropic-preferences';
|
||||
import { FrontendApplicationContribution, RemoteConnectionProvider, ServiceConnectionProvider } from '@theia/core/lib/browser';
|
||||
import { AnthropicFrontendApplicationContribution } from './anthropic-frontend-application-contribution';
|
||||
import { ANTHROPIC_LANGUAGE_MODELS_MANAGER_PATH, AnthropicLanguageModelsManager } from '../common';
|
||||
import { PreferenceContribution } from '@theia/core';
|
||||
|
||||
export default new ContainerModule(bind => {
|
||||
bind(PreferenceContribution).toConstantValue({ schema: AnthropicPreferencesSchema });
|
||||
bind(AnthropicFrontendApplicationContribution).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(AnthropicFrontendApplicationContribution);
|
||||
bind(AnthropicLanguageModelsManager).toDynamicValue(ctx => {
|
||||
const provider = ctx.container.get<ServiceConnectionProvider>(RemoteConnectionProvider);
|
||||
return provider.createProxy<AnthropicLanguageModelsManager>(ANTHROPIC_LANGUAGE_MODELS_MANAGER_PATH);
|
||||
}).inSingletonScope();
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 EclipseSource GmbH.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
export const ANTHROPIC_LANGUAGE_MODELS_MANAGER_PATH = '/services/anthropic/language-model-manager';
|
||||
export const AnthropicLanguageModelsManager = Symbol('AnthropicLanguageModelsManager');
|
||||
export interface AnthropicModelDescription {
|
||||
/**
|
||||
* The identifier of the model which will be shown in the UI.
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* The model ID as used by the Anthropic API.
|
||||
*/
|
||||
model: string;
|
||||
/**
|
||||
* The key for the model. If 'true' is provided the global Anthropic API key will be used.
|
||||
*/
|
||||
apiKey: string | true | undefined;
|
||||
/**
|
||||
* Indicate whether the streaming API shall be used.
|
||||
*/
|
||||
enableStreaming: boolean;
|
||||
/**
|
||||
* Indicate whether the model supports prompt caching.
|
||||
*/
|
||||
useCaching: boolean;
|
||||
/**
|
||||
* Maximum number of tokens to generate. Default is 4096.
|
||||
*/
|
||||
maxTokens?: number;
|
||||
/**
|
||||
* Maximum number of retry attempts when a request fails. Default is 3.
|
||||
*/
|
||||
maxRetries: number;
|
||||
|
||||
}
|
||||
export interface AnthropicLanguageModelsManager {
|
||||
apiKey: string | undefined;
|
||||
setApiKey(key: string | undefined): void;
|
||||
setProxyUrl(proxyUrl: string | undefined): void;
|
||||
createOrUpdateLanguageModels(...models: AnthropicModelDescription[]): Promise<void>;
|
||||
removeLanguageModels(...modelIds: string[]): void
|
||||
}
|
||||
42
packages/ai-anthropic/src/common/anthropic-preferences.ts
Normal file
42
packages/ai-anthropic/src/common/anthropic-preferences.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 EclipseSource GmbH.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { AI_CORE_PREFERENCES_TITLE } from '@theia/ai-core/lib/common/ai-core-preferences';
|
||||
import { nls, PreferenceSchema } from '@theia/core';
|
||||
|
||||
export const API_KEY_PREF = 'ai-features.anthropic.AnthropicApiKey';
|
||||
export const MODELS_PREF = 'ai-features.anthropic.AnthropicModels';
|
||||
|
||||
export const AnthropicPreferencesSchema: PreferenceSchema = {
|
||||
properties: {
|
||||
[API_KEY_PREF]: {
|
||||
type: 'string',
|
||||
markdownDescription: nls.localize('theia/ai/anthropic/apiKey/description',
|
||||
'Enter an API Key of your official Anthropic Account. **Please note:** By using this preference the Anthropic API key will be stored in clear text\
|
||||
on the machine running Theia. Use the environment variable `ANTHROPIC_API_KEY` to set the key securely.'),
|
||||
title: AI_CORE_PREFERENCES_TITLE,
|
||||
},
|
||||
[MODELS_PREF]: {
|
||||
type: 'array',
|
||||
description: nls.localize('theia/ai/anthropic/models/description', 'Official Anthropic models to use'),
|
||||
title: AI_CORE_PREFERENCES_TITLE,
|
||||
default: ['claude-sonnet-4-5', 'claude-sonnet-4-0', 'claude-3-7-sonnet-latest', 'claude-opus-4-5', 'claude-opus-4-1'],
|
||||
items: {
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
16
packages/ai-anthropic/src/common/index.ts
Normal file
16
packages/ai-anthropic/src/common/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 EclipseSource GmbH.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
export * from './anthropic-language-models-manager';
|
||||
36
packages/ai-anthropic/src/node/anthropic-backend-module.ts
Normal file
36
packages/ai-anthropic/src/node/anthropic-backend-module.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 EclipseSource GmbH.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ContainerModule } from '@theia/core/shared/inversify';
|
||||
import { ANTHROPIC_LANGUAGE_MODELS_MANAGER_PATH, AnthropicLanguageModelsManager } from '../common/anthropic-language-models-manager';
|
||||
import { ConnectionHandler, PreferenceContribution, RpcConnectionHandler } from '@theia/core';
|
||||
import { AnthropicLanguageModelsManagerImpl } from './anthropic-language-models-manager-impl';
|
||||
import { ConnectionContainerModule } from '@theia/core/lib/node/messaging/connection-container-module';
|
||||
import { AnthropicPreferencesSchema } from '../common/anthropic-preferences';
|
||||
|
||||
// We use a connection module to handle AI services separately for each frontend.
|
||||
const anthropicConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService, bindFrontendService }) => {
|
||||
bind(AnthropicLanguageModelsManagerImpl).toSelf().inSingletonScope();
|
||||
bind(AnthropicLanguageModelsManager).toService(AnthropicLanguageModelsManagerImpl);
|
||||
bind(ConnectionHandler).toDynamicValue(ctx =>
|
||||
new RpcConnectionHandler(ANTHROPIC_LANGUAGE_MODELS_MANAGER_PATH, () => ctx.container.get(AnthropicLanguageModelsManager))
|
||||
).inSingletonScope();
|
||||
});
|
||||
|
||||
export default new ContainerModule(bind => {
|
||||
bind(PreferenceContribution).toConstantValue({ schema: AnthropicPreferencesSchema });
|
||||
bind(ConnectionContainerModule).toConstantValue(anthropicConnectionModule);
|
||||
});
|
||||
167
packages/ai-anthropic/src/node/anthropic-language-model.spec.ts
Normal file
167
packages/ai-anthropic/src/node/anthropic-language-model.spec.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2025 EclipseSource GmbH.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { expect } from 'chai';
|
||||
import { AnthropicModel, DEFAULT_MAX_TOKENS, addCacheControlToLastMessage } from './anthropic-language-model';
|
||||
|
||||
describe('AnthropicModel', () => {
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should set default maxRetries to 3 when not provided', () => {
|
||||
const model = new AnthropicModel(
|
||||
'test-id',
|
||||
'claude-3-opus-20240229',
|
||||
{
|
||||
status: 'ready'
|
||||
},
|
||||
true,
|
||||
true,
|
||||
() => 'test-api-key',
|
||||
DEFAULT_MAX_TOKENS
|
||||
);
|
||||
|
||||
expect(model.maxRetries).to.equal(3);
|
||||
});
|
||||
|
||||
it('should set custom maxRetries when provided', () => {
|
||||
const customMaxRetries = 5;
|
||||
const model = new AnthropicModel(
|
||||
'test-id',
|
||||
'claude-3-opus-20240229',
|
||||
{
|
||||
status: 'ready'
|
||||
},
|
||||
true,
|
||||
true,
|
||||
() => 'test-api-key',
|
||||
DEFAULT_MAX_TOKENS,
|
||||
customMaxRetries
|
||||
);
|
||||
|
||||
expect(model.maxRetries).to.equal(customMaxRetries);
|
||||
});
|
||||
|
||||
it('should preserve all other constructor parameters', () => {
|
||||
const model = new AnthropicModel(
|
||||
'test-id',
|
||||
'claude-3-opus-20240229',
|
||||
{
|
||||
status: 'ready'
|
||||
},
|
||||
true,
|
||||
true,
|
||||
() => 'test-api-key',
|
||||
DEFAULT_MAX_TOKENS,
|
||||
5
|
||||
);
|
||||
|
||||
expect(model.id).to.equal('test-id');
|
||||
expect(model.model).to.equal('claude-3-opus-20240229');
|
||||
expect(model.enableStreaming).to.be.true;
|
||||
expect(model.maxTokens).to.equal(DEFAULT_MAX_TOKENS);
|
||||
expect(model.maxRetries).to.equal(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addCacheControlToLastMessage', () => {
|
||||
it('should preserve all content blocks when adding cache control to parallel tool calls', () => {
|
||||
const messages = [
|
||||
{
|
||||
role: 'user' as const,
|
||||
content: [
|
||||
{ type: 'tool_result' as const, tool_use_id: 'tool1', content: 'result1' },
|
||||
{ type: 'tool_result' as const, tool_use_id: 'tool2', content: 'result2' },
|
||||
{ type: 'tool_result' as const, tool_use_id: 'tool3', content: 'result3' }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const result = addCacheControlToLastMessage(messages);
|
||||
|
||||
expect(result).to.have.lengthOf(1);
|
||||
expect(result[0].content).to.be.an('array').with.lengthOf(3);
|
||||
expect(result[0].content[0]).to.deep.equal({ type: 'tool_result', tool_use_id: 'tool1', content: 'result1' });
|
||||
expect(result[0].content[1]).to.deep.equal({ type: 'tool_result', tool_use_id: 'tool2', content: 'result2' });
|
||||
expect(result[0].content[2]).to.deep.equal({
|
||||
type: 'tool_result',
|
||||
tool_use_id: 'tool3',
|
||||
content: 'result3',
|
||||
cache_control: { type: 'ephemeral' }
|
||||
});
|
||||
});
|
||||
|
||||
it('should add cache control to last non-thinking block in mixed content', () => {
|
||||
const messages = [
|
||||
{
|
||||
role: 'assistant' as const,
|
||||
content: [
|
||||
{ type: 'text' as const, text: 'Some text' },
|
||||
{ type: 'tool_use' as const, id: 'tool1', name: 'getTool', input: {} },
|
||||
{ type: 'thinking' as const, thinking: 'thinking content', signature: 'signature' }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const result = addCacheControlToLastMessage(messages);
|
||||
|
||||
expect(result).to.have.lengthOf(1);
|
||||
expect(result[0].content).to.be.an('array').with.lengthOf(3);
|
||||
expect(result[0].content[0]).to.deep.equal({ type: 'text', text: 'Some text' });
|
||||
expect(result[0].content[1]).to.deep.equal({
|
||||
type: 'tool_use',
|
||||
id: 'tool1',
|
||||
name: 'getTool',
|
||||
input: {},
|
||||
cache_control: { type: 'ephemeral' }
|
||||
});
|
||||
expect(result[0].content[2]).to.deep.equal({ type: 'thinking', thinking: 'thinking content', signature: 'signature' });
|
||||
});
|
||||
|
||||
it('should handle string content by converting to content block', () => {
|
||||
const messages = [
|
||||
{
|
||||
role: 'user' as const,
|
||||
content: 'Simple text message'
|
||||
}
|
||||
];
|
||||
|
||||
const result = addCacheControlToLastMessage(messages);
|
||||
|
||||
expect(result).to.have.lengthOf(1);
|
||||
expect(result[0].content).to.be.an('array').with.lengthOf(1);
|
||||
expect(result[0].content[0]).to.deep.equal({
|
||||
type: 'text',
|
||||
text: 'Simple text message',
|
||||
cache_control: { type: 'ephemeral' }
|
||||
});
|
||||
});
|
||||
|
||||
it('should not modify original messages', () => {
|
||||
const originalMessages = [
|
||||
{
|
||||
role: 'user' as const,
|
||||
content: [
|
||||
{ type: 'tool_result' as const, tool_use_id: 'tool1', content: 'result1' }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
addCacheControlToLastMessage(originalMessages);
|
||||
|
||||
expect(originalMessages[0].content[0]).to.not.have.property('cache_control');
|
||||
});
|
||||
});
|
||||
});
|
||||
454
packages/ai-anthropic/src/node/anthropic-language-model.ts
Normal file
454
packages/ai-anthropic/src/node/anthropic-language-model.ts
Normal file
@@ -0,0 +1,454 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 EclipseSource GmbH.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import {
|
||||
createToolCallError,
|
||||
ImageContent,
|
||||
ImageMimeType,
|
||||
LanguageModel,
|
||||
LanguageModelMessage,
|
||||
LanguageModelRequest,
|
||||
LanguageModelResponse,
|
||||
LanguageModelStatus,
|
||||
LanguageModelStreamResponse,
|
||||
LanguageModelStreamResponsePart,
|
||||
LanguageModelTextResponse,
|
||||
TokenUsageParams,
|
||||
TokenUsageService,
|
||||
ToolCallResult,
|
||||
ToolInvocationContext,
|
||||
UserRequest
|
||||
} from '@theia/ai-core';
|
||||
import { CancellationToken, isArray } from '@theia/core';
|
||||
import { Anthropic } from '@anthropic-ai/sdk';
|
||||
import type { Base64ImageSource, ImageBlockParam, Message, MessageParam, TextBlockParam, ToolResultBlockParam } from '@anthropic-ai/sdk/resources';
|
||||
import * as undici from 'undici';
|
||||
|
||||
export const DEFAULT_MAX_TOKENS = 4096;
|
||||
|
||||
interface ToolCallback {
|
||||
readonly name: string;
|
||||
readonly id: string;
|
||||
readonly index: number;
|
||||
args: string;
|
||||
}
|
||||
|
||||
const createMessageContent = (message: LanguageModelMessage): MessageParam['content'] => {
|
||||
if (LanguageModelMessage.isTextMessage(message)) {
|
||||
return [{ type: 'text', text: message.text }];
|
||||
} else if (LanguageModelMessage.isThinkingMessage(message)) {
|
||||
return [{ signature: message.signature, thinking: message.thinking, type: 'thinking' }];
|
||||
} else if (LanguageModelMessage.isToolUseMessage(message)) {
|
||||
return [{ id: message.id, input: message.input, name: message.name, type: 'tool_use' }];
|
||||
} else if (LanguageModelMessage.isToolResultMessage(message)) {
|
||||
return [{ type: 'tool_result', tool_use_id: message.tool_use_id, content: formatToolCallResult(message.content) }];
|
||||
} else if (LanguageModelMessage.isImageMessage(message)) {
|
||||
if (ImageContent.isBase64(message.image)) {
|
||||
return [{ type: 'image', source: { type: 'base64', media_type: mimeTypeToMediaType(message.image.mimeType), data: message.image.base64data } }];
|
||||
} else {
|
||||
return [{ type: 'image', source: { type: 'url', url: message.image.url } }];
|
||||
}
|
||||
}
|
||||
throw new Error(`Unknown message type:'${JSON.stringify(message)}'`);
|
||||
};
|
||||
|
||||
function mimeTypeToMediaType(mimeType: ImageMimeType): Base64ImageSource['media_type'] {
|
||||
switch (mimeType) {
|
||||
case 'image/gif':
|
||||
return 'image/gif';
|
||||
case 'image/jpeg':
|
||||
return 'image/jpeg';
|
||||
case 'image/png':
|
||||
return 'image/png';
|
||||
case 'image/webp':
|
||||
return 'image/webp';
|
||||
default:
|
||||
return 'image/jpeg';
|
||||
}
|
||||
}
|
||||
|
||||
type NonThinkingParam = Exclude<Anthropic.Messages.ContentBlockParam, Anthropic.Messages.ThinkingBlockParam | Anthropic.Messages.RedactedThinkingBlockParam>;
|
||||
function isNonThinkingParam(
|
||||
content: Anthropic.Messages.ContentBlockParam
|
||||
): content is NonThinkingParam {
|
||||
return content.type !== 'thinking' && content.type !== 'redacted_thinking';
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms Theia language model messages to Anthropic API format
|
||||
* @param messages Array of LanguageModelRequestMessage to transform
|
||||
* @returns Object containing transformed messages and optional system message
|
||||
*/
|
||||
function transformToAnthropicParams(
|
||||
messages: readonly LanguageModelMessage[],
|
||||
addCacheControl: boolean = true
|
||||
): { messages: MessageParam[]; systemMessage?: Anthropic.Messages.TextBlockParam[] } {
|
||||
// Extract the system message (if any), as it is a separate parameter in the Anthropic API.
|
||||
const systemMessageObj = messages.find(message => message.actor === 'system');
|
||||
const systemMessageText = systemMessageObj && LanguageModelMessage.isTextMessage(systemMessageObj) && systemMessageObj.text || undefined;
|
||||
const systemMessage: Anthropic.Messages.TextBlockParam[] | undefined =
|
||||
systemMessageText ? [{ type: 'text', text: systemMessageText, cache_control: addCacheControl ? { type: 'ephemeral' } : undefined }] : undefined;
|
||||
|
||||
const convertedMessages = messages
|
||||
.filter(message => message.actor !== 'system')
|
||||
.map(message => ({
|
||||
role: toAnthropicRole(message),
|
||||
content: createMessageContent(message)
|
||||
}));
|
||||
|
||||
return {
|
||||
messages: convertedMessages,
|
||||
systemMessage,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* If possible adds a cache control to the last message in the conversation.
|
||||
* This is used to enable incremental caching of the conversation.
|
||||
* @param messages The messages to process
|
||||
* @returns A new messages array with the last message adapted to include cache control. If no cache control can be added, the original messages are returned.
|
||||
* In any case, the original messages are not modified
|
||||
*/
|
||||
export function addCacheControlToLastMessage(messages: Anthropic.Messages.MessageParam[]): Anthropic.Messages.MessageParam[] {
|
||||
const clonedMessages = [...messages];
|
||||
const latestMessage = clonedMessages.pop();
|
||||
if (latestMessage) {
|
||||
if (typeof latestMessage.content === 'string') {
|
||||
// Wrap the string content into a content block with cache control
|
||||
const cachedContent: NonThinkingParam = {
|
||||
type: 'text',
|
||||
text: latestMessage.content,
|
||||
cache_control: { type: 'ephemeral' }
|
||||
};
|
||||
return [...clonedMessages, { ...latestMessage, content: [cachedContent] }];
|
||||
} else if (Array.isArray(latestMessage.content)) {
|
||||
// Update the last non-thinking content block to include cache control
|
||||
const updatedContent = [...latestMessage.content];
|
||||
for (let i = updatedContent.length - 1; i >= 0; i--) {
|
||||
if (isNonThinkingParam(updatedContent[i])) {
|
||||
updatedContent[i] = {
|
||||
...updatedContent[i],
|
||||
cache_control: { type: 'ephemeral' }
|
||||
} as NonThinkingParam;
|
||||
return [...clonedMessages, { ...latestMessage, content: updatedContent }];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return messages;
|
||||
}
|
||||
|
||||
export const AnthropicModelIdentifier = Symbol('AnthropicModelIdentifier');
|
||||
|
||||
/**
|
||||
* Converts Theia message actor to Anthropic role
|
||||
* @param message The message to convert
|
||||
* @returns Anthropic role ('user' or 'assistant')
|
||||
*/
|
||||
function toAnthropicRole(message: LanguageModelMessage): 'user' | 'assistant' {
|
||||
switch (message.actor) {
|
||||
case 'ai':
|
||||
return 'assistant';
|
||||
default:
|
||||
return 'user';
|
||||
}
|
||||
}
|
||||
|
||||
function formatToolCallResult(result: ToolCallResult): ToolResultBlockParam['content'] {
|
||||
if (typeof result === 'object' && result && 'content' in result && Array.isArray(result.content)) {
|
||||
return result.content.map<TextBlockParam | ImageBlockParam>(content => {
|
||||
if (content.type === 'text') {
|
||||
return { type: 'text', text: content.text };
|
||||
} else if (content.type === 'image') {
|
||||
return { type: 'image', source: { type: 'base64', data: content.base64data, media_type: mimeTypeToMediaType(content.mimeType) } };
|
||||
} else {
|
||||
return { type: 'text', text: content.data };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (isArray(result)) {
|
||||
return result.map(r => ({ type: 'text', text: r as string }));
|
||||
}
|
||||
|
||||
if (typeof result === 'object') {
|
||||
return JSON.stringify(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements the Anthropic language model integration for Theia
|
||||
*/
|
||||
export class AnthropicModel implements LanguageModel {
|
||||
|
||||
constructor(
|
||||
public readonly id: string,
|
||||
public model: string,
|
||||
public status: LanguageModelStatus,
|
||||
public enableStreaming: boolean,
|
||||
public useCaching: boolean,
|
||||
public apiKey: () => string | undefined,
|
||||
public maxTokens: number = DEFAULT_MAX_TOKENS,
|
||||
public maxRetries: number = 3,
|
||||
protected readonly tokenUsageService?: TokenUsageService,
|
||||
protected proxy?: string
|
||||
) { }
|
||||
|
||||
protected getSettings(request: LanguageModelRequest): Readonly<Record<string, unknown>> {
|
||||
return request.settings ?? {};
|
||||
}
|
||||
|
||||
async request(request: UserRequest, cancellationToken?: CancellationToken): Promise<LanguageModelResponse> {
|
||||
if (!request.messages?.length) {
|
||||
throw new Error('Request must contain at least one message');
|
||||
}
|
||||
|
||||
const anthropic = this.initializeAnthropic();
|
||||
|
||||
try {
|
||||
if (this.enableStreaming) {
|
||||
return this.handleStreamingRequest(anthropic, request, cancellationToken);
|
||||
}
|
||||
return this.handleNonStreamingRequest(anthropic, request);
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
||||
throw new Error(`Anthropic API request failed: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
protected async handleStreamingRequest(
|
||||
anthropic: Anthropic,
|
||||
request: UserRequest,
|
||||
cancellationToken?: CancellationToken,
|
||||
toolMessages?: readonly Anthropic.Messages.MessageParam[]
|
||||
): Promise<LanguageModelStreamResponse> {
|
||||
const settings = this.getSettings(request);
|
||||
const { messages, systemMessage } = transformToAnthropicParams(request.messages, this.useCaching);
|
||||
|
||||
let anthropicMessages = [...messages, ...(toolMessages ?? [])];
|
||||
|
||||
if (this.useCaching && anthropicMessages.length) {
|
||||
anthropicMessages = addCacheControlToLastMessage(anthropicMessages);
|
||||
}
|
||||
|
||||
const tools = this.createTools(request);
|
||||
const params: Anthropic.MessageCreateParams = {
|
||||
max_tokens: this.maxTokens,
|
||||
messages: anthropicMessages,
|
||||
tools,
|
||||
tool_choice: tools ? { type: 'auto' } : undefined,
|
||||
model: this.model,
|
||||
...(systemMessage && { system: systemMessage }),
|
||||
...settings
|
||||
};
|
||||
const stream = anthropic.messages.stream(params, { maxRetries: this.maxRetries });
|
||||
|
||||
cancellationToken?.onCancellationRequested(() => {
|
||||
stream.abort();
|
||||
});
|
||||
const that = this;
|
||||
|
||||
const asyncIterator = {
|
||||
async *[Symbol.asyncIterator](): AsyncIterator<LanguageModelStreamResponsePart> {
|
||||
|
||||
const toolCalls: ToolCallback[] = [];
|
||||
let toolCall: ToolCallback | undefined;
|
||||
const currentMessages: Message[] = [];
|
||||
let currentMessage: Message | undefined = undefined;
|
||||
|
||||
for await (const event of stream) {
|
||||
if (event.type === 'content_block_start') {
|
||||
const contentBlock = event.content_block;
|
||||
|
||||
if (contentBlock.type === 'thinking') {
|
||||
yield { thought: contentBlock.thinking, signature: contentBlock.signature ?? '' };
|
||||
}
|
||||
if (contentBlock.type === 'text') {
|
||||
yield { content: contentBlock.text };
|
||||
}
|
||||
if (contentBlock.type === 'tool_use') {
|
||||
toolCall = { name: contentBlock.name!, args: '', id: contentBlock.id!, index: event.index };
|
||||
yield { tool_calls: [{ finished: false, id: toolCall.id, function: { name: toolCall.name, arguments: toolCall.args } }] };
|
||||
}
|
||||
} else if (event.type === 'content_block_delta') {
|
||||
const delta = event.delta;
|
||||
if (delta.type === 'thinking_delta') {
|
||||
yield { thought: delta.thinking, signature: '' };
|
||||
}
|
||||
if (delta.type === 'signature_delta') {
|
||||
yield { thought: '', signature: delta.signature };
|
||||
}
|
||||
if (delta.type === 'text_delta') {
|
||||
yield { content: delta.text };
|
||||
}
|
||||
if (toolCall && delta.type === 'input_json_delta') {
|
||||
toolCall.args += delta.partial_json;
|
||||
yield { tool_calls: [{ function: { arguments: delta.partial_json } }] };
|
||||
}
|
||||
} else if (event.type === 'content_block_stop') {
|
||||
if (toolCall && toolCall.index === event.index) {
|
||||
toolCalls.push(toolCall);
|
||||
toolCall = undefined;
|
||||
}
|
||||
} else if (event.type === 'message_delta') {
|
||||
if (event.delta.stop_reason === 'max_tokens') {
|
||||
if (toolCall) {
|
||||
yield { tool_calls: [{ finished: true, id: toolCall.id }] };
|
||||
}
|
||||
throw new Error(`The response was stopped because it exceeded the max token limit of ${event.usage.output_tokens}.`);
|
||||
}
|
||||
} else if (event.type === 'message_start') {
|
||||
currentMessages.push(event.message);
|
||||
currentMessage = event.message;
|
||||
} else if (event.type === 'message_stop') {
|
||||
if (currentMessage) {
|
||||
yield { input_tokens: currentMessage.usage.input_tokens, output_tokens: currentMessage.usage.output_tokens };
|
||||
// Record token usage if token usage service is available
|
||||
if (that.tokenUsageService && currentMessage.usage) {
|
||||
const tokenUsageParams: TokenUsageParams = {
|
||||
inputTokens: currentMessage.usage.input_tokens,
|
||||
outputTokens: currentMessage.usage.output_tokens,
|
||||
cachedInputTokens: currentMessage.usage.cache_creation_input_tokens || undefined,
|
||||
readCachedInputTokens: currentMessage.usage.cache_read_input_tokens || undefined,
|
||||
requestId: request.requestId
|
||||
};
|
||||
await that.tokenUsageService.recordTokenUsage(that.id, tokenUsageParams);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
if (toolCalls.length > 0) {
|
||||
const toolResult = await Promise.all(toolCalls.map(async tc => {
|
||||
const tool = request.tools?.find(t => t.name === tc.name);
|
||||
const argsObject = tc.args.length === 0 ? '{}' : tc.args;
|
||||
const handlerResult = tool
|
||||
? await tool.handler(argsObject, ToolInvocationContext.create(tc.id))
|
||||
: createToolCallError(`Tool '${tc.name}' not found in the available tools for this request.`, 'tool-not-available');
|
||||
|
||||
return { name: tc.name, result: handlerResult, id: tc.id, arguments: argsObject };
|
||||
|
||||
}));
|
||||
|
||||
const calls = toolResult.map(tr => ({ finished: true, id: tr.id, result: tr.result, function: { name: tr.name, arguments: tr.arguments } }));
|
||||
yield { tool_calls: calls };
|
||||
|
||||
const toolResponseMessage: Anthropic.Messages.MessageParam = {
|
||||
role: 'user',
|
||||
content: toolResult.map(call => ({
|
||||
type: 'tool_result',
|
||||
tool_use_id: call.id!,
|
||||
content: formatToolCallResult(call.result)
|
||||
}))
|
||||
};
|
||||
const result = await that.handleStreamingRequest(
|
||||
anthropic,
|
||||
request,
|
||||
cancellationToken,
|
||||
[
|
||||
...(toolMessages ?? []),
|
||||
...currentMessages.map(m => ({ role: m.role, content: m.content })),
|
||||
toolResponseMessage
|
||||
]);
|
||||
for await (const nestedEvent of result.stream) {
|
||||
yield nestedEvent;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
stream.on('error', (error: Error) => {
|
||||
console.error('Error in Anthropic streaming:', error);
|
||||
});
|
||||
|
||||
return { stream: asyncIterator };
|
||||
}
|
||||
|
||||
protected createTools(request: LanguageModelRequest): Anthropic.Messages.Tool[] | undefined {
|
||||
if (request.tools?.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
const tools = request.tools?.map(tool => ({
|
||||
name: tool.name,
|
||||
description: tool.description,
|
||||
input_schema: tool.parameters
|
||||
} as Anthropic.Messages.Tool));
|
||||
if (this.useCaching) {
|
||||
if (tools?.length) {
|
||||
tools[tools.length - 1].cache_control = { type: 'ephemeral' };
|
||||
}
|
||||
}
|
||||
return tools;
|
||||
}
|
||||
|
||||
protected async handleNonStreamingRequest(
|
||||
anthropic: Anthropic,
|
||||
request: UserRequest
|
||||
): Promise<LanguageModelTextResponse> {
|
||||
const settings = this.getSettings(request);
|
||||
const { messages, systemMessage } = transformToAnthropicParams(request.messages);
|
||||
|
||||
const params: Anthropic.MessageCreateParams = {
|
||||
max_tokens: this.maxTokens,
|
||||
messages,
|
||||
model: this.model,
|
||||
...(systemMessage && { system: systemMessage }),
|
||||
...settings,
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await anthropic.messages.create(params);
|
||||
const textContent = response.content[0];
|
||||
|
||||
// Record token usage if token usage service is available
|
||||
if (this.tokenUsageService && response.usage) {
|
||||
const tokenUsageParams: TokenUsageParams = {
|
||||
inputTokens: response.usage.input_tokens,
|
||||
outputTokens: response.usage.output_tokens,
|
||||
requestId: request.requestId
|
||||
};
|
||||
await this.tokenUsageService.recordTokenUsage(this.id, tokenUsageParams);
|
||||
}
|
||||
|
||||
if (textContent?.type === 'text') {
|
||||
return { text: textContent.text };
|
||||
}
|
||||
|
||||
return { text: '' };
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to get response from Anthropic API: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
}
|
||||
|
||||
protected initializeAnthropic(): Anthropic {
|
||||
const apiKey = this.apiKey();
|
||||
if (!apiKey) {
|
||||
throw new Error('Please provide ANTHROPIC_API_KEY in preferences or via environment variable');
|
||||
}
|
||||
|
||||
let fo;
|
||||
if (this.proxy) {
|
||||
const proxyAgent = new undici.ProxyAgent(this.proxy);
|
||||
fo = {
|
||||
dispatcher: proxyAgent,
|
||||
};
|
||||
}
|
||||
|
||||
return new Anthropic({ apiKey, fetchOptions: fo });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 EclipseSource GmbH.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { LanguageModelRegistry, LanguageModelStatus, TokenUsageService } from '@theia/ai-core';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { AnthropicModel, DEFAULT_MAX_TOKENS } from './anthropic-language-model';
|
||||
import { AnthropicLanguageModelsManager, AnthropicModelDescription } from '../common';
|
||||
|
||||
@injectable()
|
||||
export class AnthropicLanguageModelsManagerImpl implements AnthropicLanguageModelsManager {
|
||||
|
||||
protected _apiKey: string | undefined;
|
||||
protected _proxyUrl: string | undefined;
|
||||
|
||||
@inject(LanguageModelRegistry)
|
||||
protected readonly languageModelRegistry: LanguageModelRegistry;
|
||||
|
||||
@inject(TokenUsageService)
|
||||
protected readonly tokenUsageService: TokenUsageService;
|
||||
|
||||
get apiKey(): string | undefined {
|
||||
return this._apiKey ?? process.env.ANTHROPIC_API_KEY;
|
||||
}
|
||||
|
||||
async createOrUpdateLanguageModels(...modelDescriptions: AnthropicModelDescription[]): Promise<void> {
|
||||
for (const modelDescription of modelDescriptions) {
|
||||
const model = await this.languageModelRegistry.getLanguageModel(modelDescription.id);
|
||||
const apiKeyProvider = () => {
|
||||
if (modelDescription.apiKey === true) {
|
||||
return this.apiKey;
|
||||
}
|
||||
if (modelDescription.apiKey) {
|
||||
return modelDescription.apiKey;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
const proxyUrlProvider = () => {
|
||||
// first check if the proxy url is provided via Theia settings
|
||||
if (this._proxyUrl) {
|
||||
return this._proxyUrl;
|
||||
}
|
||||
|
||||
// if not fall back to the environment variables
|
||||
return process.env['https_proxy'];
|
||||
};
|
||||
|
||||
// Determine status based on API key presence
|
||||
const effectiveApiKey = apiKeyProvider();
|
||||
const status = this.getStatusForApiKey(effectiveApiKey);
|
||||
|
||||
if (model) {
|
||||
if (!(model instanceof AnthropicModel)) {
|
||||
console.warn(`Anthropic: model ${modelDescription.id} is not an Anthropic model`);
|
||||
continue;
|
||||
}
|
||||
await this.languageModelRegistry.patchLanguageModel<AnthropicModel>(modelDescription.id, {
|
||||
model: modelDescription.model,
|
||||
enableStreaming: modelDescription.enableStreaming,
|
||||
apiKey: apiKeyProvider,
|
||||
status,
|
||||
maxTokens: modelDescription.maxTokens !== undefined ? modelDescription.maxTokens : DEFAULT_MAX_TOKENS,
|
||||
maxRetries: modelDescription.maxRetries
|
||||
});
|
||||
} else {
|
||||
this.languageModelRegistry.addLanguageModels([
|
||||
new AnthropicModel(
|
||||
modelDescription.id,
|
||||
modelDescription.model,
|
||||
status,
|
||||
modelDescription.enableStreaming,
|
||||
modelDescription.useCaching,
|
||||
apiKeyProvider,
|
||||
modelDescription.maxTokens,
|
||||
modelDescription.maxRetries,
|
||||
this.tokenUsageService,
|
||||
proxyUrlProvider()
|
||||
)
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
removeLanguageModels(...modelIds: string[]): void {
|
||||
this.languageModelRegistry.removeLanguageModels(modelIds);
|
||||
}
|
||||
|
||||
setApiKey(apiKey: string | undefined): void {
|
||||
if (apiKey) {
|
||||
this._apiKey = apiKey;
|
||||
} else {
|
||||
this._apiKey = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
setProxyUrl(proxyUrl: string | undefined): void {
|
||||
if (proxyUrl) {
|
||||
this._proxyUrl = proxyUrl;
|
||||
} else {
|
||||
this._proxyUrl = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the status for a language model based on the presence of an API key.
|
||||
*/
|
||||
protected getStatusForApiKey(effectiveApiKey: string | undefined): LanguageModelStatus {
|
||||
return effectiveApiKey
|
||||
? { status: 'ready' }
|
||||
: { status: 'unavailable', message: 'No Anthropic API key set' };
|
||||
}
|
||||
}
|
||||
|
||||
28
packages/ai-anthropic/src/package.spec.ts
Normal file
28
packages/ai-anthropic/src/package.spec.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 EclipseSource GmbH and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
/* note: this bogus test file is required so that
|
||||
we are able to run mocha unit tests on this
|
||||
package, without having any actual unit tests in it.
|
||||
This way a coverage report will be generated,
|
||||
showing 0% coverage, instead of no report.
|
||||
This file can be removed once we have real unit
|
||||
tests in place. */
|
||||
|
||||
describe('ai-anthropic package', () => {
|
||||
|
||||
it('support code coverage statistics', () => true);
|
||||
});
|
||||
19
packages/ai-anthropic/tsconfig.json
Normal file
19
packages/ai-anthropic/tsconfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "../../configs/base.tsconfig",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../ai-core"
|
||||
},
|
||||
{
|
||||
"path": "../core"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user