From 198b6c9ade1f9fc0f69a8917795d71bb4be35154 Mon Sep 17 00:00:00 2001 From: Eugen Neufeld Date: Wed, 15 Jul 2026 09:44:32 +0200 Subject: [PATCH 1/2] Remove strict schema conversion for OpenAI Response API (#17754) Drop the recursiveStrictJSONSchema transform and strict:true flag when converting tools for the Response API, passing tool parameters through unchanged. Strict mode is opt-in and was self-imposed, causing repeated 400 errors on MCP tools with open-ended maps (e.g. additionalProperties). Also removes the related preference note and strictify unit tests. --- .../src/common/openai-preferences.ts | 8 +- .../src/node/openai-model-utils.spec.ts | 104 +----------------- .../node/openai-response-api-utils.spec.ts | 33 ++++++ .../src/node/openai-response-api-utils.ts | 63 +---------- 4 files changed, 41 insertions(+), 167 deletions(-) diff --git a/packages/ai-openai/src/common/openai-preferences.ts b/packages/ai-openai/src/common/openai-preferences.ts index 37a78b33fe08f..50335dd4b0b82 100644 --- a/packages/ai-openai/src/common/openai-preferences.ts +++ b/packages/ai-openai/src/common/openai-preferences.ts @@ -53,12 +53,8 @@ on the machine running Theia. Use the environment variable `OPENAI_API_KEY` to s default: false, title: AI_CORE_PREFERENCES_TITLE, markdownDescription: nls.localize('theia/ai/openai/useResponseApi/mdDescription', - 'Use the newer OpenAI Response API instead of the Chat Completion API for official OpenAI models.\ -\ -This setting only applies to official OpenAI models - custom providers must configure this individually.\ -\ -Note that for the response API, tool call definitions must satisfy Open AI\'s [strict schema definition](https://platform.openai.com/docs/guides/function-calling#strict-mode).\ -Best effort is made to convert non-conformant schemas, but errors are still possible.') + 'Use the newer OpenAI Response API instead of the Chat Completion API for official OpenAI models. ' + + 'This setting only applies to official OpenAI models - custom providers must configure this individually.') }, [SERVER_SIDE_COMPACTION_PREF]: { type: 'string', diff --git a/packages/ai-openai/src/node/openai-model-utils.spec.ts b/packages/ai-openai/src/node/openai-model-utils.spec.ts index 49934bdb1999e..7499fad93846f 100644 --- a/packages/ai-openai/src/node/openai-model-utils.spec.ts +++ b/packages/ai-openai/src/node/openai-model-utils.spec.ts @@ -16,8 +16,7 @@ import { expect } from 'chai'; import { OpenAiModelUtils } from './openai-language-model'; import { LanguageModelMessage } from '@theia/ai-core'; -import { OpenAiResponseApiUtils, recursiveStrictJSONSchema } from './openai-response-api-utils'; -import type { JSONSchema, JSONSchemaDefinition } from 'openai/lib/jsonschema'; +import { OpenAiResponseApiUtils } from './openai-response-api-utils'; const utils = new OpenAiModelUtils(); const responseUtils = new OpenAiResponseApiUtils(); @@ -495,105 +494,4 @@ describe('OpenAiModelUtils - processMessagesForResponseApi', () => { }); }); - describe('recursiveStrictJSONSchema', () => { - it('should return the same object and not modify it when schema has no properties to strictify', () => { - const schema: JSONSchema = { type: 'string', description: 'Simple string' }; - const originalJson = JSON.stringify(schema); - - const result = recursiveStrictJSONSchema(schema); - - expect(result).to.equal(schema); - expect(JSON.stringify(schema)).to.equal(originalJson); - const resultObj = result as JSONSchema; - expect(resultObj).to.not.have.property('additionalProperties'); - expect(resultObj).to.not.have.property('required'); - }); - - it('should not mutate original but return a new strictified schema when branching applies (properties/items)', () => { - const original: JSONSchema = { - type: 'object', - properties: { - path: { type: 'string' }, - data: { - type: 'array', - items: { - type: 'object', - properties: { - a: { type: 'string' } - } - } - } - } - }; - const originalClone = JSON.parse(JSON.stringify(original)); - - const resultDef = recursiveStrictJSONSchema(original); - const result = resultDef as JSONSchema; - - expect(result).to.not.equal(original); - expect(original).to.deep.equal(originalClone); - - expect(result.additionalProperties).to.equal(false); - expect(result.required).to.have.members(['path', 'data']); - - const itemsDef = (result.properties?.data as JSONSchema).items as JSONSchemaDefinition; - expect(itemsDef).to.be.ok; - const itemsObj = itemsDef as JSONSchema; - expect(itemsObj.additionalProperties).to.equal(false); - expect(itemsObj.required).to.have.members(['a']); - - const originalItems = ((original.properties!.data as JSONSchema).items) as JSONSchema; - expect(originalItems).to.not.have.property('additionalProperties'); - expect(originalItems).to.not.have.property('required'); - }); - - it('should strictify nested parameters schema and not mutate the original', () => { - const replacementProperties: Record = { - oldContent: { type: 'string', description: 'The exact content to be replaced. Must match exactly, including whitespace, comments, etc.' }, - newContent: { type: 'string', description: 'The new content to insert in place of matched old content.' }, - multiple: { type: 'boolean', description: 'Set to true if multiple occurrences of the oldContent are expected to be replaced.' } - }; - - const parameters: JSONSchema = { - type: 'object', - properties: { - path: { type: 'string', description: 'The path of the file where content will be replaced.' }, - replacements: { - type: 'array', - items: { - type: 'object', - properties: replacementProperties, - required: ['oldContent', 'newContent'] - }, - description: 'An array of replacement objects, each containing oldContent and newContent strings.' - }, - reset: { - type: 'boolean', - description: 'Set to true to clear any existing pending changes for this file and start fresh. Default is false, which merges with existing changes.' - } - }, - required: ['path', 'replacements'] - }; - - const originalClone = JSON.parse(JSON.stringify(parameters)); - - const strictifiedDef = recursiveStrictJSONSchema(parameters); - const strictified = strictifiedDef as JSONSchema; - - expect(strictified).to.not.equal(parameters); - expect(parameters).to.deep.equal(originalClone); - - expect(strictified.additionalProperties).to.equal(false); - expect(strictified.required).to.have.members(['path', 'replacements', 'reset']); - - const items = (strictified.properties!.replacements as JSONSchema).items as JSONSchema; - expect(items.additionalProperties).to.equal(false); - expect(items.required).to.have.members(['oldContent', 'newContent', 'multiple']); - - const origItems = ((parameters.properties!.replacements as JSONSchema).items) as JSONSchema; - expect(origItems.required).to.deep.equal(['oldContent', 'newContent']); - expect(origItems).to.not.have.property('additionalProperties'); - }); - }); - }); diff --git a/packages/ai-openai/src/node/openai-response-api-utils.spec.ts b/packages/ai-openai/src/node/openai-response-api-utils.spec.ts index 83bf5672af3e6..1e63ba89c348b 100644 --- a/packages/ai-openai/src/node/openai-response-api-utils.spec.ts +++ b/packages/ai-openai/src/node/openai-response-api-utils.spec.ts @@ -28,6 +28,39 @@ async function* toStream(events: unknown[]): AsyncIterable { } describe('OpenAiResponseApiUtils', () => { + it('passes tool parameters through unchanged in non-strict mode', () => { + const utils = new OpenAiResponseApiUtils(); + const parameters = { + type: 'object' as const, + properties: { + metadata: { + type: 'object' as const, + additionalProperties: { type: 'string' } + } + } + }; + + const converted = utils.convertToolsForResponseApi([{ + id: 'lookup', + name: 'lookup', + parameters, + handler: async () => 'result' + }]); + + expect(converted).to.have.lengthOf(1); + const convertedTool = converted?.[0]; + expect(convertedTool).to.include({ + type: 'function', + name: 'lookup', + parameters, + strict: false + }); + if (convertedTool?.type !== 'function') { + throw new Error('Expected a function tool'); + } + expect(convertedTool.parameters).to.equal(parameters); + }); + it('emits per-iteration usage for Response API tool calls instead of accumulated usage', async () => { const utils = new OpenAiResponseApiUtils(); const streams = [ diff --git a/packages/ai-openai/src/node/openai-response-api-utils.ts b/packages/ai-openai/src/node/openai-response-api-utils.ts index 2f4f25901baca..c6e38aaa17682 100644 --- a/packages/ai-openai/src/node/openai-response-api-utils.ts +++ b/packages/ai-openai/src/node/openai-response-api-utils.ts @@ -23,7 +23,6 @@ import { TextMessage, ToolInvocationContext, ToolRequest, - ToolRequestParameters, UserRequest } from '@theia/ai-core'; import { CancellationToken, nls, unreachable } from '@theia/core'; @@ -44,7 +43,6 @@ import type { } from 'openai/resources/responses/responses'; import type { ResponsesModel } from 'openai/resources/shared'; import { DeveloperMessageSettings, OpenAiModelUtils } from './openai-language-model'; -import { JSONSchema, JSONSchemaDefinition } from 'openai/lib/jsonschema'; /** * User-facing name under which the provider's built-in deferred-tool search is surfaced in the chat UI. @@ -159,11 +157,11 @@ export class OpenAiResponseApiUtils { type: 'function' as const, name: tool.name, description: tool.description || '', - // The Response API is very strict re: JSON schema: all properties must be listed as required, - // and additional properties must be disallowed. - // https://platform.openai.com/docs/guides/function-calling#strict-mode - parameters: this.recursiveStrictToolCallParameters(tool.parameters), - strict: true, + // Tool schemas are passed through as-is (non-strict). Strict mode (`strict: true`) is opt-in and + // only supports a JSON-schema subset (all properties required, `additionalProperties: false` + // everywhere), which cannot express the open-ended maps common in MCP tool schemas. + parameters: tool.parameters as unknown as FunctionTool['parameters'], + strict: false, defer_loading: deferred.has(tool.id) ? true : undefined })); if (deferred.size > 0) { @@ -173,10 +171,6 @@ export class OpenAiResponseApiUtils { return converted; } - recursiveStrictToolCallParameters(schema: ToolRequestParameters): FunctionTool['parameters'] { - return recursiveStrictJSONSchema(schema) as FunctionTool['parameters']; - } - protected createSimpleResponseApiStreamIterator( stream: AsyncIterable, cancellationToken?: CancellationToken @@ -883,50 +877,3 @@ export function processSystemMessages( } return messages; } - -export function recursiveStrictJSONSchema(schema: JSONSchemaDefinition): JSONSchemaDefinition { - if (typeof schema === 'boolean') { return schema; } - let result: JSONSchema | undefined = undefined; - if (schema.properties) { - result ??= { ...schema }; - result.additionalProperties = false; - result.required = Object.keys(schema.properties); - result.properties = Object.fromEntries(Object.entries(schema.properties).map(([key, props]) => [key, recursiveStrictJSONSchema(props)])); - } - if (schema.items) { - result ??= { ...schema }; - result.items = Array.isArray(schema.items) - ? schema.items.map(recursiveStrictJSONSchema) - : recursiveStrictJSONSchema(schema.items); - } - if (schema.oneOf) { - result ??= { ...schema }; - result.oneOf = schema.oneOf.map(recursiveStrictJSONSchema); - } - if (schema.anyOf) { - result ??= { ...schema }; - result.anyOf = schema.anyOf.map(recursiveStrictJSONSchema); - } - if (schema.allOf) { - result ??= { ...schema }; - result.allOf = schema.allOf.map(recursiveStrictJSONSchema); - } - if (schema.if) { - result ??= { ...schema }; - result.if = recursiveStrictJSONSchema(schema.if); - } - if (schema.then) { - result ??= { ...schema }; - result.then = recursiveStrictJSONSchema(schema.then); - } - if (schema.else) { - result ??= { ...schema }; - result.else = recursiveStrictJSONSchema(schema.else); - } - if (schema.not) { - result ??= { ...schema }; - result.not = recursiveStrictJSONSchema(schema.not); - } - - return result ?? schema; -} From 0728fc6379964faf30f67476e8cc9bb2d891be13 Mon Sep 17 00:00:00 2001 From: Nina Doschek Date: Wed, 15 Jul 2026 10:50:55 +0200 Subject: [PATCH 2/2] chore: deprecate @theia/ai-vercel-ai package (#17781) - marked `@theia/ai-vercel-ai` as deprecated and stopped publishing it on npm - removed `@theia/ai-vercel-ai` from the browser, browser-only, and electron example apps Resolves GH-17780 Contributed on behalf of STMicroelectronics --- CHANGELOG.md | 2 ++ doc/Migration.md | 16 ++++++++++++++++ examples/browser/package.json | 1 - examples/browser/tsconfig.json | 3 --- examples/electron/package.json | 1 - examples/electron/tsconfig.json | 3 --- package-lock.json | 2 -- packages/ai-vercel-ai/README.md | 1 + packages/ai-vercel-ai/package.json | 1 + 9 files changed, 20 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20aa94709a1fc..94a0f040e1d2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,11 +9,13 @@ - [terminal] fixed the file link provider logging a `UriError` when a printed URL (e.g. `https://example.com/path`) was matched as a local file candidate; such `//host/path` candidates are now skipped and left to the URL link provider [#17767](https://github.com/eclipse-theia/theia/pull/17767) - [ai-anthropic, ai-chat, ai-chat-ui, ai-core, ai-openai] added provider-native server-side compaction for chat sessions, modeled as a model capability (`LanguageModelMetaData.serverSideCompactionSupport`) plus layered activation: a global preference (`ai-features.chat.serverSideCompaction`, default on), a per-provider override, and a per-session override (in the session settings dialog) that wins over both. Honored by the Anthropic Messages API (beta) and the OpenAI Responses API; the compaction marker is persisted in the session, replayed on subsequent requests, surfaced as an inline chat marker, and reflected in the token-usage tooltip (cumulative usage and a "compacted Nx" count). Providers/models without the capability ignore it [#17636](https://github.com/eclipse-theia/theia/issues/17636) - [ai-chat-ui] replaced scroll-direction heuristics in `ChatViewTreeWidget` with Virtuoso's `atBottomStateChange` for reliable auto-scroll during streaming [#17728](https://github.com/eclipse-theia/theia/pull/17728) +- [ai-vercel-ai] deprecated `@theia/ai-vercel-ai` package [#TBD](https://github.com/eclipse-theia/theia/pull/TBD) - [electron] upgraded Electron from 39.8.7 to 42.3.0 (Node 24, Chromium 148) [#17586](https://github.com/eclipse-theia/theia/pull/17586) [Breaking Changes:](#breaking_changes_1.74.0) - [ai-chat-ui] `ChatViewTreeWidget` scroll-lock is now driven by Virtuoso's `atBottomStateChange` callback instead of scroll-direction heuristics; the following `protected` members have been removed: `handleScrollEvent()`, `getCurrentScrollTop()`, `isAtAbsoluteBottom()`, `updateScrollToBottomButtonState()`, `lastScrollTop`, `_scrollButtonDebounceTimer`, `SCROLL_BUTTON_GRACE_PERIOD`, and the `updateScrollToRow()` override. Subclasses that override any of these should use the new `handleAtBottomStateChange(isAtBottom: boolean)` method instead [#17728](https://github.com/eclipse-theia/theia/pull/17728) +- [ai-vercel-ai] deprecated the `@theia/ai-vercel-ai` experimental extension and stopped publishing it on npm. Please use the dedicated `@theia/ai-openai` and `@theia/ai-anthropic` providers instead, which cover the same models with first-class support. [#TBD](https://github.com/eclipse-theia/theia/pull/TBD) - [core] removed `TreeWidget.onScroll`, `TreeWidget.onScrollEmitter`, `TreeWidget.getVirtualizedScrollState()`, `TreeWidget.isScrolledToBottom()`, the `TreeScrollEvent` interface, and the `TreeScrollState` interface. Use `TreeWidget.onAtBottomStateChange` to react to bottom-state changes instead [#17728](https://github.com/eclipse-theia/theia/pull/17728) - [electron] upgraded Electron from 39.8.7 to 42.3.0. Downstream Electron applications must update their `electron` devDependency to `42.3.0`. Electron 42 bundles Node 24 (up from Node 22), which may affect native modules or Node APIs used by downstream applications. See the Electron breaking changes for [40.0](https://www.electronjs.org/docs/latest/breaking-changes#planned-breaking-api-changes-400), [41.0](https://www.electronjs.org/docs/latest/breaking-changes#planned-breaking-api-changes-410), and [42.0](https://www.electronjs.org/docs/latest/breaking-changes#planned-breaking-api-changes-420). [#17586](https://github.com/eclipse-theia/theia/pull/17586) diff --git a/doc/Migration.md b/doc/Migration.md index 96fe70af99795..106b565ca55b7 100644 --- a/doc/Migration.md +++ b/doc/Migration.md @@ -101,6 +101,22 @@ For example, in an `electron-builder` configuration, ensure the `lib/backend/she The `lib/**/*` glob already covers `lib/backend/shell-integrations/`. If you use a more restrictive `files` pattern, make sure `lib/backend/shell-integrations/**/*` is explicitly included, as `ShellIntegrationInjector` resolves these scripts relative to `__dirname` (i.e. `lib/backend/`). +### v1.74.0 + +#### Deprecation of @theia/ai-vercel-ai package + +The `@theia/ai-vercel-ai` package has been marked as deprecated and is no longer published on npm. It will be removed from the Theia codebase after a deprecation period. The package only wrapped OpenAI and Anthropic models, both of which are already covered by the dedicated `@theia/ai-openai` and `@theia/ai-anthropic` providers with first-class support. + +If your application depends on `@theia/ai-vercel-ai`, migrate as follows: + +- **OpenAI models**: use `@theia/ai-openai`. Move `ai-features.vercelAi.openaiApiKey` to `ai-features.openAiOfficial.openAiApiKey` and define models in `ai-features.openAiOfficial.officialOpenAiModels`. +- **Anthropic models**: use `@theia/ai-anthropic`. Move `ai-features.vercelAi.anthropicApiKey` to `ai-features.anthropic.AnthropicApiKey` and define models in `ai-features.anthropic.AnthropicModels`. +- **Custom endpoints** (`ai-features.vercelAi.customModels`): split entries by their `provider` field into the matching provider-specific preference: + - `provider: 'openai'` → `ai-features.openAiCustom.customOpenAiModels` + - `provider: 'anthropic'` → `ai-features.anthropicCustom.customAnthropicModels` + +The dedicated provider preferences offer the same fields plus additional ones (e.g. Azure `apiVersion`/`deployment`, `useResponseApi`, `reasoningSupport` for OpenAI; `useCaching`, `maxRetries` for Anthropic). + ### v1.73.0 #### Custom agents reorganized into per-agent folders [#17523](https://github.com/eclipse-theia/theia/pull/17523) diff --git a/examples/browser/package.json b/examples/browser/package.json index 314156c00878c..1601fc2da7d79 100644 --- a/examples/browser/package.json +++ b/examples/browser/package.json @@ -46,7 +46,6 @@ "@theia/ai-registry": "1.73.0", "@theia/ai-scanoss": "1.73.0", "@theia/ai-terminal": "1.73.0", - "@theia/ai-vercel-ai": "1.73.0", "@theia/api-provider-sample": "1.73.0", "@theia/api-samples": "1.73.0", "@theia/bulk-edit": "1.73.0", diff --git a/examples/browser/tsconfig.json b/examples/browser/tsconfig.json index 723c9527f9b18..65c70a3777002 100644 --- a/examples/browser/tsconfig.json +++ b/examples/browser/tsconfig.json @@ -77,9 +77,6 @@ { "path": "../../packages/ai-terminal" }, - { - "path": "../../packages/ai-vercel-ai" - }, { "path": "../../packages/bulk-edit" }, diff --git a/examples/electron/package.json b/examples/electron/package.json index 3f8a05283c1d3..2ab1597209a05 100644 --- a/examples/electron/package.json +++ b/examples/electron/package.json @@ -50,7 +50,6 @@ "@theia/ai-registry": "1.73.0", "@theia/ai-scanoss": "1.73.0", "@theia/ai-terminal": "1.73.0", - "@theia/ai-vercel-ai": "1.73.0", "@theia/api-provider-sample": "1.73.0", "@theia/api-samples": "1.73.0", "@theia/bulk-edit": "1.73.0", diff --git a/examples/electron/tsconfig.json b/examples/electron/tsconfig.json index 0246164f27ebf..31ab9abd53e6d 100644 --- a/examples/electron/tsconfig.json +++ b/examples/electron/tsconfig.json @@ -80,9 +80,6 @@ { "path": "../../packages/ai-terminal" }, - { - "path": "../../packages/ai-vercel-ai" - }, { "path": "../../packages/bulk-edit" }, diff --git a/package-lock.json b/package-lock.json index 141b5c1e28863..074946e0e0a9a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -564,7 +564,6 @@ "@theia/ai-registry": "1.73.0", "@theia/ai-scanoss": "1.73.0", "@theia/ai-terminal": "1.73.0", - "@theia/ai-vercel-ai": "1.73.0", "@theia/api-provider-sample": "1.73.0", "@theia/api-samples": "1.73.0", "@theia/bulk-edit": "1.73.0", @@ -710,7 +709,6 @@ "@theia/ai-registry": "1.73.0", "@theia/ai-scanoss": "1.73.0", "@theia/ai-terminal": "1.73.0", - "@theia/ai-vercel-ai": "1.73.0", "@theia/api-provider-sample": "1.73.0", "@theia/api-samples": "1.73.0", "@theia/bulk-edit": "1.73.0", diff --git a/packages/ai-vercel-ai/README.md b/packages/ai-vercel-ai/README.md index 9124227024238..61988d20355d6 100644 --- a/packages/ai-vercel-ai/README.md +++ b/packages/ai-vercel-ai/README.md @@ -5,6 +5,7 @@ theia-ext-logo

ECLIPSE THEIA - VERCEL AI SDK INTEGRATION

+

This extension is DEPRECATED!


diff --git a/packages/ai-vercel-ai/package.json b/packages/ai-vercel-ai/package.json index 5526e496f8623..44ac6992c9051 100644 --- a/packages/ai-vercel-ai/package.json +++ b/packages/ai-vercel-ai/package.json @@ -2,6 +2,7 @@ "name": "@theia/ai-vercel-ai", "version": "1.73.0", "description": "Theia - Vercel AI SDK Integration", + "private": true, "dependencies": { "@ai-sdk/anthropic": "^1.2.12", "@ai-sdk/openai": "^1.3.24",