Skip to content
Open
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
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@
- [task] fixed `onDidStartTaskProcess` never firing for process tasks because `TaskServer.runTask` omitted the `task` argument to `fireTaskCreatedEvent` [#17663](https://github.com/eclipse-theia/theia/pull/17663)
- [terminal] fixed Cmd+V / Ctrl+V paste in the integrated terminal and restored the effect of the `terminal.enablePaste` and `terminal.enableCopy` preferences [#17603](https://github.com/eclipse-theia/theia/pull/17603)
- [core] added support for React 19 and declared React peer dependencies as `^18.3.1 || ^19.0.0`. [#17567](https://github.com/eclipse-theia/theia/pull/17567)
- [ai-anthropic, ai-google, ai-ollama, ai-openai, ai-copilot] added rebindable `<provider>LanguageModelFactory` bindings for instantiating provider language models [#17623](https://github.com/eclipse-theia/theia/pull/17623)
- [ai-core] executed a model turn's tool calls concurrently, including parallel agent delegations, instead of sequentially, via a new injectable `ToolCallExecutor` [#17623](https://github.com/eclipse-theia/theia/pull/17623)
- [ai-anthropic, ai-google, ai-ollama, ai-openai, ai-copilot] the provider language model classes are now `@injectable`, transient-scoped services constructed via their `<provider>LanguageModelFactory`, so adopters can rebind them to substitute custom implementations [#17623](https://github.com/eclipse-theia/theia/pull/17623)
- [ai-openai] added a rebindable `ChatCompletionStreamingAsyncIteratorFactory`; the chat-completion tool-call streaming iterator (used by the OpenAI and Copilot models) is now an injectable service that can be substituted [#17623](https://github.com/eclipse-theia/theia/pull/17623)

<a name="breaking_changes_1.73.0">[Breaking Changes:](#breaking_changes_1.73.0)</a>

Expand All @@ -27,6 +31,11 @@
- added container parameter to DefaultDebugSessionFactory and PluginDebugSessionFactory constructors
- renamed DebugSessionFactory.get to DebugSessionFactory.createSession and removed the manager parameter
- [terminal] `TerminalWidget` gained a new abstract method `paste(text: string)`; downstream subclasses must implement it (consistent with `getSelection()` / `hasSelection()` added in [#17290](https://github.com/eclipse-theia/theia/pull/17290)) [#17603](https://github.com/eclipse-theia/theia/pull/17603)
- [ai-openai] `OpenAiLanguageModelsManagerImpl` no longer injects `OpenAiModelUtils` or `OpenAiResponseApiUtils` (the `openAiModelUtils` and `responseApiUtils` protected fields were removed); provider models are now constructed via the injected `OpenAiLanguageModelFactory` [#17623](https://github.com/eclipse-theia/theia/pull/17623)
- [ai-openai, ai-copilot] `OpenAiModel.createTools()` and `CopilotLanguageModel.createTools()` now return `ChatCompletionTool[]` instead of `RunnableToolFunctionWithoutParse[]`, because the OpenAI SDK `runTools` runner is no longer used [#17623](https://github.com/eclipse-theia/theia/pull/17623)
- [ai-copilot] removed the `protected runnerOptions` field from `CopilotLanguageModel`; its only purpose was the `maxChatCompletions` turn cap, which is gone now that the OpenAI SDK `runTools` runner is unused, so the tool loop runs until the model stops requesting tools. Subclasses that read or overrode `runnerOptions` must adapt. `OpenAiModel.runnerOptions` is retained, but its `maxChatCompletions` now bounds only the Response API path, not the Chat Completions tool loop [#17623](https://github.com/eclipse-theia/theia/pull/17623)
- [ai-anthropic, ai-google, ai-ollama, ai-openai, ai-copilot] the provider language model classes (`AnthropicModel`, `GoogleModel`, `OllamaModel`, `OpenAiModel`, `CopilotLanguageModel`) no longer expose public constructors; they are `@injectable` and receive their configuration through an injected `<provider>ModelParams` object (a symbol) plus injected service dependencies. Instantiate them via the corresponding `<provider>LanguageModelFactory` (or the DI container) instead of `new`, and drop constructor overrides in subclasses [#17623](https://github.com/eclipse-theia/theia/pull/17623)
- [ai-openai] `OpenAiModelUtils` moved from `@theia/ai-openai/lib/node/openai-language-model` to `@theia/ai-openai/lib/node/openai-model-utils` [#17623](https://github.com/eclipse-theia/theia/pull/17623)

## 1.72.0 - 5/28/2026

Expand Down
12 changes: 11 additions & 1 deletion packages/ai-anthropic/src/node/anthropic-backend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,27 @@
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import { ContainerModule } from '@theia/core/shared/inversify';
import { Container, 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 { AnthropicLanguageModelFactory, AnthropicModel, AnthropicModelParams } from './anthropic-language-model';
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(AnthropicModel).toSelf().inTransientScope();
bind(AnthropicLanguageModelFactory).toFactory<AnthropicModel, [AnthropicModelParams]>(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great change, I wanted to use a factory approach for a longer time now. Which is one of the reasons I brought up #17520.

Personally I would like if it goes a step further and makes the model itself @injectable too. The params can then be injected as an object and the tool call executor would be injected separately.

({ container }) => params => {
const child = new Container();
child.parent = container;
child.bind(AnthropicModelParams).toConstantValue(params);
return child.get(AnthropicModel);
}
);
bind(ConnectionHandler).toDynamicValue(ctx =>
new RpcConnectionHandler(ANTHROPIC_LANGUAGE_MODELS_MANAGER_PATH, () => ctx.container.get(AnthropicLanguageModelsManager))
).inSingletonScope();
Expand Down
162 changes: 88 additions & 74 deletions packages/ai-anthropic/src/node/anthropic-language-model.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,13 @@
// *****************************************************************************

import { expect } from 'chai';
import { AnthropicModel, DEFAULT_MAX_TOKENS, addCacheControlToLastMessage, mergeConsecutiveSameRoleMessages } from './anthropic-language-model';
import { isUsageResponsePart, LanguageModelRequest, LanguageModelStreamResponsePart, ReasoningApi, ReasoningSupport, UserRequest } from '@theia/ai-core';
import { Container, injectable } from '@theia/core/shared/inversify';
import { ILogger } from '@theia/core';
import { MockLogger } from '@theia/core/lib/common/test/mock-logger';
import { AnthropicModel, AnthropicModelParams, DEFAULT_MAX_TOKENS, addCacheControlToLastMessage, mergeConsecutiveSameRoleMessages } from './anthropic-language-model';
import {
isUsageResponsePart, LanguageModelRequest, LanguageModelStreamResponsePart, ReasoningApi, ReasoningSupport, ToolCallExecutor, ToolCallExecutorImpl, UserRequest
} from '@theia/ai-core';
import type { Anthropic } from '@anthropic-ai/sdk';
import type { MessageParam } from '@anthropic-ai/sdk/resources';

Expand All @@ -26,82 +31,89 @@ const REASONING_SUPPORT: ReasoningSupport = {
};

/** Test helper that exposes the otherwise protected getSettings() method. */
@injectable()
class TestableAnthropicModel extends AnthropicModel {
public callGetSettings(request: LanguageModelRequest): Readonly<Record<string, unknown>> {
return this.getSettings(request);
}
}

function buildModel<T extends AnthropicModel>(modelType: new (...args: never[]) => T, params: AnthropicModelParams): T {
const parent = new Container();
parent.bind(ToolCallExecutor).to(ToolCallExecutorImpl);
parent.bind(ILogger).to(MockLogger);
parent.bind(modelType).toSelf().inTransientScope();

const child = new Container();
child.parent = parent;
child.bind(AnthropicModelParams).toConstantValue(params);
return child.get(modelType);
}

function createReasoningModel(
modelId: string, reasoningApi: ReasoningApi, supportsXHighEffort: boolean = false
): TestableAnthropicModel {
return new TestableAnthropicModel(
'test-id', modelId, { status: 'ready' }, true, false,
() => 'test-key', undefined, DEFAULT_MAX_TOKENS,
3, undefined, REASONING_SUPPORT, reasoningApi, supportsXHighEffort
);
return buildModel(TestableAnthropicModel, {
id: 'test-id', model: modelId, status: { status: 'ready' }, enableStreaming: true, useCaching: false,
apiKey: () => 'test-key', url: undefined, maxTokens: DEFAULT_MAX_TOKENS,
maxRetries: 3, reasoningSupport: REASONING_SUPPORT, reasoningApi, supportsXHighEffort
});
}

function createNonReasoningModel(modelId: string): TestableAnthropicModel {
return new TestableAnthropicModel(
'test-id', modelId, { status: 'ready' }, true, false,
() => 'test-key', undefined, DEFAULT_MAX_TOKENS
);
return buildModel(TestableAnthropicModel, {
id: 'test-id', model: modelId, status: { status: 'ready' }, enableStreaming: true, useCaching: false,
apiKey: () => 'test-key', url: undefined, maxTokens: DEFAULT_MAX_TOKENS
});
}

describe('AnthropicModel', () => {

describe('constructor', () => {
describe('parameters', () => {
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',
undefined,
DEFAULT_MAX_TOKENS
);
const model = buildModel(AnthropicModel, {
id: 'test-id',
model: 'claude-3-opus-20240229',
status: { status: 'ready' },
enableStreaming: true,
useCaching: true,
apiKey: () => 'test-api-key',
url: undefined,
maxTokens: 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',
undefined,
DEFAULT_MAX_TOKENS,
customMaxRetries
);
const model = buildModel(AnthropicModel, {
id: 'test-id',
model: 'claude-3-opus-20240229',
status: { status: 'ready' },
enableStreaming: true,
useCaching: true,
apiKey: () => 'test-api-key',
url: undefined,
maxTokens: DEFAULT_MAX_TOKENS,
maxRetries: 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',
undefined,
DEFAULT_MAX_TOKENS,
5
);
it('should preserve all other parameters', () => {
const model = buildModel(AnthropicModel, {
id: 'test-id',
model: 'claude-3-opus-20240229',
status: { status: 'ready' },
enableStreaming: true,
useCaching: true,
apiKey: () => 'test-api-key',
url: undefined,
maxTokens: DEFAULT_MAX_TOKENS,
maxRetries: 5
});

expect(model.id).to.equal('test-id');
expect(model.model).to.equal('claude-3-opus-20240229');
Expand All @@ -111,19 +123,17 @@ describe('AnthropicModel', () => {
});

it('should set custom url when provided', () => {
const model = new AnthropicModel(
'test-id',
'claude-3-opus-20240229',
{
status: 'ready'
},
true,
true,
() => 'test-api-key',
'custom-url',
DEFAULT_MAX_TOKENS,
5
);
const model = buildModel(AnthropicModel, {
id: 'test-id',
model: 'claude-3-opus-20240229',
status: { status: 'ready' },
enableStreaming: true,
useCaching: true,
apiKey: () => 'test-api-key',
url: 'custom-url',
maxTokens: DEFAULT_MAX_TOKENS,
maxRetries: 5
});

expect(model.url).to.equal('custom-url');
});
Expand Down Expand Up @@ -329,15 +339,17 @@ describe('AnthropicModel', () => {

function createModel(anthropicEventsByCall: object[][]): AnthropicModel {
let callIndex = 0;
return new class extends AnthropicModel {
@injectable()
class MockAnthropicModel extends AnthropicModel {
protected override initializeAnthropic(): Anthropic {
const events = anthropicEventsByCall[Math.min(callIndex++, anthropicEventsByCall.length - 1)];
return buildMockAnthropic(events);
}
}(
'test-id', 'claude-opus-4-5', { status: 'ready' },
true, false, () => 'test-key', undefined
);
}
return buildModel(MockAnthropicModel, {
id: 'test-id', model: 'claude-opus-4-5', status: { status: 'ready' },
enableStreaming: true, useCaching: false, apiKey: () => 'test-key', url: undefined
});
}

async function collectStreamParts(model: AnthropicModel, text: string): Promise<LanguageModelStreamResponsePart[]> {
Expand Down Expand Up @@ -480,14 +492,16 @@ describe('AnthropicModel', () => {
{ type: 'message_stop' },
];

const model = new class extends AnthropicModel {
@injectable()
class AbortingAnthropicModel extends AnthropicModel {
protected override initializeAnthropic(): Anthropic {
return buildAbortingAnthropic(events, 4);
}
}(
'test-id', 'claude-opus-4-5', { status: 'ready' },
true, false, () => 'test-key', undefined
);
}
const model = buildModel(AbortingAnthropicModel, {
id: 'test-id', model: 'claude-opus-4-5', status: { status: 'ready' },
enableStreaming: true, useCaching: false, apiKey: () => 'test-key', url: undefined
});

const request: UserRequest = {
messages: [{ actor: 'user', type: 'text', text: 'hi' }],
Expand Down
Loading
Loading