@theia/ai-openai 1.54.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/README.md +31 -0
  2. package/lib/browser/openai-frontend-application-contribution.d.ts +10 -0
  3. package/lib/browser/openai-frontend-application-contribution.d.ts.map +1 -0
  4. package/lib/browser/openai-frontend-application-contribution.js +99 -0
  5. package/lib/browser/openai-frontend-application-contribution.js.map +1 -0
  6. package/lib/browser/openai-frontend-module.d.ts +4 -0
  7. package/lib/browser/openai-frontend-module.d.ts.map +1 -0
  8. package/lib/browser/openai-frontend-module.js +32 -0
  9. package/lib/browser/openai-frontend-module.js.map +1 -0
  10. package/lib/browser/openai-preferences.d.ts +6 -0
  11. package/lib/browser/openai-preferences.d.ts.map +1 -0
  12. package/lib/browser/openai-preferences.js +67 -0
  13. package/lib/browser/openai-preferences.js.map +1 -0
  14. package/lib/common/index.d.ts +2 -0
  15. package/lib/common/index.d.ts.map +1 -0
  16. package/lib/common/index.js +20 -0
  17. package/lib/common/index.js.map +1 -0
  18. package/lib/common/openai-language-models-manager.d.ts +23 -0
  19. package/lib/common/openai-language-models-manager.d.ts.map +1 -0
  20. package/lib/common/openai-language-models-manager.js +21 -0
  21. package/lib/common/openai-language-models-manager.js.map +1 -0
  22. package/lib/node/openai-backend-module.d.ts +5 -0
  23. package/lib/node/openai-backend-module.d.ts.map +1 -0
  24. package/lib/node/openai-backend-module.js +29 -0
  25. package/lib/node/openai-backend-module.js.map +1 -0
  26. package/lib/node/openai-language-model.d.ts +24 -0
  27. package/lib/node/openai-language-model.d.ts.map +1 -0
  28. package/lib/node/openai-language-model.js +174 -0
  29. package/lib/node/openai-language-model.js.map +1 -0
  30. package/lib/node/openai-language-models-manager-impl.d.ts +11 -0
  31. package/lib/node/openai-language-models-manager-impl.d.ts.map +1 -0
  32. package/lib/node/openai-language-models-manager-impl.js +77 -0
  33. package/lib/node/openai-language-models-manager-impl.js.map +1 -0
  34. package/lib/package.spec.d.ts +1 -0
  35. package/lib/package.spec.d.ts.map +1 -0
  36. package/lib/package.spec.js +26 -0
  37. package/lib/package.spec.js.map +1 -0
  38. package/package.json +54 -0
  39. package/src/browser/openai-frontend-application-contribution.ts +98 -0
  40. package/src/browser/openai-frontend-module.ts +31 -0
  41. package/src/browser/openai-preferences.ts +67 -0
  42. package/src/common/index.ts +16 -0
  43. package/src/common/openai-language-models-manager.ts +37 -0
  44. package/src/node/openai-backend-module.ts +30 -0
  45. package/src/node/openai-language-model.ts +187 -0
  46. package/src/node/openai-language-models-manager-impl.ts +73 -0
  47. package/src/package.spec.ts +28 -0
@@ -0,0 +1,187 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2024 EclipseSource GmbH.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import {
18
+ LanguageModel,
19
+ LanguageModelParsedResponse,
20
+ LanguageModelRequest,
21
+ LanguageModelRequestMessage,
22
+ LanguageModelResponse,
23
+ LanguageModelStreamResponsePart
24
+ } from '@theia/ai-core';
25
+ import { CancellationToken } from '@theia/core';
26
+ import OpenAI from 'openai';
27
+ import { ChatCompletionStream } from 'openai/lib/ChatCompletionStream';
28
+ import { RunnableToolFunctionWithoutParse } from 'openai/lib/RunnableFunction';
29
+ import { ChatCompletionMessageParam } from 'openai/resources';
30
+
31
+ export const OpenAiModelIdentifier = Symbol('OpenAiModelIdentifier');
32
+
33
+ function toOpenAIMessage(message: LanguageModelRequestMessage): ChatCompletionMessageParam {
34
+ return {
35
+ role: toOpenAiRole(message),
36
+ content: message.query || ''
37
+ };
38
+ }
39
+
40
+ function toOpenAiRole(message: LanguageModelRequestMessage): 'system' | 'user' | 'assistant' {
41
+ switch (message.actor) {
42
+ case 'system':
43
+ return 'system';
44
+ case 'ai':
45
+ return 'assistant';
46
+ default:
47
+ return 'user';
48
+ }
49
+ }
50
+
51
+ export class OpenAiModel implements LanguageModel {
52
+
53
+ /**
54
+ * @param id the unique id for this language model. It will be used to identify the model in the UI.
55
+ * @param model the model id as it is used by the OpenAI API
56
+ * @param openAIInitializer initializer for the OpenAI client, used for each request.
57
+ */
58
+ constructor(public readonly id: string, public model: string, protected apiKey: (() => string | undefined) | undefined, public url: string | undefined) { }
59
+
60
+ async request(request: LanguageModelRequest, cancellationToken?: CancellationToken): Promise<LanguageModelResponse> {
61
+ const openai = this.initializeOpenAi();
62
+
63
+ if (request.response_format?.type === 'json_schema' && this.supportsStructuredOutput()) {
64
+ return this.handleStructuredOutputRequest(openai, request);
65
+ }
66
+
67
+ let runner: ChatCompletionStream;
68
+ const tools = this.createTools(request);
69
+ if (tools) {
70
+ runner = openai.beta.chat.completions.runTools({
71
+ model: this.model,
72
+ messages: request.messages.map(toOpenAIMessage),
73
+ stream: true,
74
+ tools: tools,
75
+ tool_choice: 'auto',
76
+ ...request.settings
77
+ });
78
+ } else {
79
+ runner = openai.beta.chat.completions.stream({
80
+ model: this.model,
81
+ messages: request.messages.map(toOpenAIMessage),
82
+ stream: true,
83
+ ...request.settings
84
+ });
85
+ }
86
+ cancellationToken?.onCancellationRequested(() => {
87
+ runner.abort();
88
+ });
89
+
90
+ let runnerEnd = false;
91
+
92
+ let resolve: (part: LanguageModelStreamResponsePart) => void;
93
+ runner.on('error', error => {
94
+ console.error('Error in OpenAI chat completion stream:', error);
95
+ runnerEnd = true;
96
+ resolve({ content: error.message });
97
+ });
98
+ // we need to also listen for the emitted errors, as otherwise any error actually thrown by the API will not be caught
99
+ runner.emitted('error').then(error => {
100
+ console.error('Error in OpenAI chat completion stream:', error);
101
+ runnerEnd = true;
102
+ resolve({ content: error.message });
103
+ });
104
+ runner.emitted('abort').then(() => {
105
+ // do nothing, as the abort event is only emitted when the runner is aborted by us
106
+ });
107
+ runner.on('message', message => {
108
+ if (message.role === 'tool') {
109
+ resolve({ tool_calls: [{ id: message.tool_call_id, finished: true, result: this.getCompletionContent(message) }] });
110
+ }
111
+ console.debug('Received Open AI message', JSON.stringify(message));
112
+ });
113
+ runner.once('end', () => {
114
+ runnerEnd = true;
115
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
116
+ resolve(runner.finalChatCompletion as any);
117
+ });
118
+ const asyncIterator = {
119
+ async *[Symbol.asyncIterator](): AsyncIterator<LanguageModelStreamResponsePart> {
120
+ runner.on('chunk', chunk => {
121
+ if (chunk.choices[0]?.delta) {
122
+ resolve({ ...chunk.choices[0]?.delta });
123
+ }
124
+ });
125
+ while (!runnerEnd) {
126
+ const promise = new Promise<LanguageModelStreamResponsePart>((res, rej) => {
127
+ resolve = res;
128
+ });
129
+ yield promise;
130
+ }
131
+ }
132
+ };
133
+ return { stream: asyncIterator };
134
+ }
135
+
136
+ protected supportsStructuredOutput(): boolean {
137
+ // currently only the lastest 4o and 4o-mini models support structured output
138
+ // see https://platform.openai.com/docs/guides/structured-outputs
139
+ return this.model === 'gpt-4o-2024-08-06' || this.model === 'gpt-4o-mini';
140
+ }
141
+
142
+ protected async handleStructuredOutputRequest(openai: OpenAI, request: LanguageModelRequest): Promise<LanguageModelParsedResponse> {
143
+ // TODO implement tool support for structured output (parse() seems to require different tool format)
144
+ const result = await openai.beta.chat.completions.parse({
145
+ model: this.model,
146
+ messages: request.messages.map(toOpenAIMessage),
147
+ response_format: request.response_format,
148
+ ...request.settings
149
+ });
150
+ const message = result.choices[0].message;
151
+ if (message.refusal || message.parsed === undefined) {
152
+ console.error('Error in OpenAI chat completion stream:', JSON.stringify(message));
153
+ }
154
+ return {
155
+ content: message.content ?? '',
156
+ parsed: message.parsed
157
+ };
158
+ }
159
+
160
+ private getCompletionContent(message: OpenAI.Chat.Completions.ChatCompletionToolMessageParam): string {
161
+ if (Array.isArray(message.content)) {
162
+ return message.content.join('');
163
+ }
164
+ return message.content;
165
+ }
166
+
167
+ protected createTools(request: LanguageModelRequest): RunnableToolFunctionWithoutParse[] | undefined {
168
+ return request.tools?.map(tool => ({
169
+ type: 'function',
170
+ function: {
171
+ name: tool.name,
172
+ description: tool.description,
173
+ parameters: tool.parameters,
174
+ function: (args_string: string) => tool.handler(args_string)
175
+ }
176
+ } as RunnableToolFunctionWithoutParse));
177
+ }
178
+
179
+ protected initializeOpenAi(): OpenAI {
180
+ const apiKey = this.apiKey && this.apiKey();
181
+ if (!apiKey && !(this.url)) {
182
+ throw new Error('Please provide OPENAI_API_KEY in preferences or via environment variable');
183
+ }
184
+ // do not hand over API key to custom urls
185
+ return new OpenAI({ apiKey: this.url ? 'no-key' : apiKey, baseURL: this.url });
186
+ }
187
+ }
@@ -0,0 +1,73 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2024 EclipseSource GmbH.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { LanguageModelRegistry } from '@theia/ai-core';
18
+ import { inject, injectable } from '@theia/core/shared/inversify';
19
+ import { OpenAiModel } from './openai-language-model';
20
+ import { OpenAiLanguageModelsManager, OpenAiModelDescription } from '../common';
21
+
22
+ @injectable()
23
+ export class OpenAiLanguageModelsManagerImpl implements OpenAiLanguageModelsManager {
24
+
25
+ protected _apiKey: string | undefined;
26
+
27
+ @inject(LanguageModelRegistry)
28
+ protected readonly languageModelRegistry: LanguageModelRegistry;
29
+
30
+ get apiKey(): string | undefined {
31
+ return this._apiKey ?? process.env.OPENAI_API_KEY;
32
+ }
33
+
34
+ // Triggered from frontend. In case you want to use the models on the backend
35
+ // without a frontend then call this yourself
36
+ async createOrUpdateLanguageModels(...modelDescriptions: OpenAiModelDescription[]): Promise<void> {
37
+ for (const modelDescription of modelDescriptions) {
38
+ const model = await this.languageModelRegistry.getLanguageModel(modelDescription.id);
39
+ if (model) {
40
+ if (!(model instanceof OpenAiModel)) {
41
+ console.warn(`Open AI: model ${modelDescription.id} is not an OpenAI model`);
42
+ continue;
43
+ }
44
+ if (!modelDescription.url) {
45
+ // This seems to be an official model, but it was already created. This can happen during the initializing of more than one frontend.
46
+ console.info(`Open AI: skip creating model ${modelDescription.id} because it already exists`);
47
+ continue;
48
+ }
49
+ if (model.url !== modelDescription.url || model.model !== modelDescription.model) {
50
+ model.url = modelDescription.url;
51
+ model.model = modelDescription.model;
52
+ } else {
53
+ // This can happen during the initializing of more than one frontends.
54
+ console.info(`Open AI: skip creating or updating model ${modelDescription.id} because it already exists and is up to date`);
55
+ }
56
+ } else {
57
+ this.languageModelRegistry.addLanguageModels([new OpenAiModel(modelDescription.id, modelDescription.model, () => this.apiKey, modelDescription.url)]);
58
+ }
59
+ }
60
+ }
61
+
62
+ removeLanguageModels(...modelIds: string[]): void {
63
+ this.languageModelRegistry.removeLanguageModels(modelIds);
64
+ }
65
+
66
+ setApiKey(apiKey: string | undefined): void {
67
+ if (apiKey) {
68
+ this._apiKey = apiKey;
69
+ } else {
70
+ this._apiKey = undefined;
71
+ }
72
+ }
73
+ }
@@ -0,0 +1,28 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2024 EclipseSource GmbH and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ /* note: this bogus test file is required so that
18
+ we are able to run mocha unit tests on this
19
+ package, without having any actual unit tests in it.
20
+ This way a coverage report will be generated,
21
+ showing 0% coverage, instead of no report.
22
+ This file can be removed once we have real unit
23
+ tests in place. */
24
+
25
+ describe('ai-openai package', () => {
26
+
27
+ it('support code coverage statistics', () => true);
28
+ });