Skip to content

Commit 0bbca0d

Browse files
committed
Support anyOf in function parameters
- Added support for `anyOf` types in function parameters. - Modified the language model and added new test cases for validation. - Ensures compatibility with MCP servers using `anyOf`, like the official `mcp-server-git`. Fixed #15011
1 parent acb6170 commit 0bbca0d

File tree

3 files changed

+178
-16
lines changed

3 files changed

+178
-16
lines changed

packages/ai-core/src/common/language-model.ts

+46-11
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,14 @@ export const isLanguageModelRequestMessage = (obj: unknown): obj is LanguageMode
3232
'query' in obj &&
3333
typeof (obj as { query: unknown }).query === 'string'
3434
);
35-
export type ToolRequestParametersProperties = Record<string, { type: string, [key: string]: unknown }>;
35+
36+
export interface ToolRequestParameterProperty {
37+
type?: string;
38+
anyOf?: ToolRequestParameterProperty[];
39+
[key: string]: unknown;
40+
}
41+
42+
export type ToolRequestParametersProperties = Record<string, ToolRequestParameterProperty>;
3643
export interface ToolRequestParameters {
3744
type?: 'object';
3845
properties: ToolRequestParametersProperties;
@@ -48,17 +55,45 @@ export interface ToolRequest {
4855
}
4956

5057
export namespace ToolRequest {
58+
function isToolRequestParameterProperty(obj: unknown): obj is ToolRequestParameterProperty {
59+
if (!obj || typeof obj !== 'object') {
60+
return false;
61+
}
62+
const record = obj as Record<string, unknown>;
63+
64+
// Check that at least one of "type" or "anyOf" exists
65+
if (!('type' in record) && !('anyOf' in record)) {
66+
return false;
67+
}
68+
69+
// If an "anyOf" field is present, it must be an array where each item is also a valid property.
70+
if ('anyOf' in record) {
71+
if (!Array.isArray(record.anyOf)) {
72+
return false;
73+
}
74+
for (const item of record.anyOf) {
75+
if (!isToolRequestParameterProperty(item)) {
76+
return false;
77+
}
78+
}
79+
}
80+
if ('type' in record && typeof record.type !== 'string') {
81+
return false;
82+
}
83+
84+
// No further checks required for additional properties.
85+
return true;
86+
}
5187
export function isToolRequestParametersProperties(obj: unknown): obj is ToolRequestParametersProperties {
52-
if (!obj || typeof obj !== 'object') { return false; };
53-
54-
return Object.entries(obj).every(([key, value]) =>
55-
typeof key === 'string' &&
56-
value &&
57-
typeof value === 'object' &&
58-
'type' in value &&
59-
typeof value.type === 'string' &&
60-
Object.keys(value).every(k => typeof k === 'string')
61-
);
88+
if (!obj || typeof obj !== 'object') {
89+
return false;
90+
}
91+
return Object.entries(obj).every(([key, value]) => {
92+
if (typeof key !== 'string') {
93+
return false;
94+
}
95+
return isToolRequestParameterProperty(value);
96+
});
6297
}
6398
export function isToolRequestParameters(obj: unknown): obj is ToolRequestParameters {
6499
return !!obj && typeof obj === 'object' &&
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// *****************************************************************************
2+
// Copyright (C) 2025 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+
import { expect } from 'chai';
17+
import { ToolRequest } from '../common/language-model';
18+
19+
describe('isToolRequestParameters', () => {
20+
it('should return true for valid ToolRequestParameters', () => {
21+
const validParams = {
22+
type: 'object',
23+
properties: {
24+
param1: {
25+
type: 'string'
26+
},
27+
param2: {
28+
type: 'number'
29+
}
30+
},
31+
required: ['param1']
32+
};
33+
expect(ToolRequest.isToolRequestParameters(validParams)).to.be.true;
34+
});
35+
36+
it('should return false for ToolRequestParameters without type or anyOf', () => {
37+
const paramsWithoutType = {
38+
properties: {
39+
param1: {
40+
description: 'string'
41+
}
42+
},
43+
required: ['param1']
44+
};
45+
expect(ToolRequest.isToolRequestParameters(paramsWithoutType)).to.be.false;
46+
});
47+
48+
it('should return false for invalid ToolRequestParameters with wrong property type', () => {
49+
const invalidParams = {
50+
type: 'object',
51+
properties: {
52+
param1: {
53+
type: 123
54+
}
55+
}
56+
};
57+
expect(ToolRequest.isToolRequestParameters(invalidParams)).to.be.false;
58+
});
59+
60+
it('should return false if properties is not an object', () => {
61+
const invalidParams = {
62+
type: 'object',
63+
properties: 'not-an-object',
64+
};
65+
expect(ToolRequest.isToolRequestParameters(invalidParams)).to.be.false;
66+
});
67+
68+
it('should return true if required is missing', () => {
69+
const missingRequiredParams = {
70+
type: 'object',
71+
properties: {
72+
param1: {
73+
type: 'string'
74+
}
75+
}
76+
};
77+
expect(ToolRequest.isToolRequestParameters(missingRequiredParams)).to.be.true;
78+
});
79+
80+
it('should return false if required is not an array', () => {
81+
const invalidRequiredParams = {
82+
type: 'object',
83+
properties: {
84+
param1: {
85+
type: 'string'
86+
}
87+
},
88+
required: 'param1'
89+
};
90+
expect(ToolRequest.isToolRequestParameters(invalidRequiredParams)).to.be.false;
91+
});
92+
93+
it('should return false if a required field is not a string', () => {
94+
const invalidRequiredParams = {
95+
type: 'object',
96+
properties: {
97+
param1: {
98+
type: 'string'
99+
}
100+
},
101+
required: [123]
102+
};
103+
expect(ToolRequest.isToolRequestParameters(invalidRequiredParams)).to.be.false;
104+
});
105+
106+
it('should validate properties with anyOf correctly', () => {
107+
const paramsWithAnyOf = {
108+
type: 'object',
109+
properties: {
110+
param1: {
111+
anyOf: [
112+
{ type: 'string' },
113+
{ type: 'number' }
114+
]
115+
}
116+
},
117+
required: ['param1']
118+
};
119+
expect(ToolRequest.isToolRequestParameters(paramsWithAnyOf)).to.be.true;
120+
});
121+
});

packages/ai-ollama/src/node/ollama-language-model.ts

+11-5
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ import {
2323
LanguageModelStreamResponse,
2424
LanguageModelStreamResponsePart,
2525
ToolCall,
26-
ToolRequest
26+
ToolRequest,
27+
ToolRequestParametersProperties
2728
} from '@theia/ai-core';
2829
import { CancellationToken } from '@theia/core';
2930
import { ChatRequest, ChatResponse, Message, Ollama, Options, Tool } from 'ollama';
@@ -200,16 +201,21 @@ export class OllamaModel implements LanguageModel {
200201
}
201202

202203
protected toOllamaTool(tool: ToolRequest): ToolWithHandler {
203-
const transform = (props: Record<string, { [key: string]: unknown; type: string; }> | undefined) => {
204+
const transform = (props: ToolRequestParametersProperties | undefined) => {
204205
if (!props) {
205206
return undefined;
206207
}
207208
const result: Record<string, { type: string, description: string }> = {};
208209
for (const key in props) {
209210
if (Object.prototype.hasOwnProperty.call(props, key)) {
210-
result[key] = {
211-
type: props[key].type,
212-
description: key
211+
const type = props[key].type;
212+
if (!type) {
213+
// Todo: Should handle anyOf, but this is not supported by the Ollama type yet
214+
} else {
215+
result[key] = {
216+
type: type,
217+
description: key
218+
};
213219
};
214220
}
215221
}

0 commit comments

Comments
 (0)