@toolplex/ai-engine 0.1.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.
- package/LICENSE +98 -0
- package/README.md +292 -0
- package/dist/adapters/index.d.ts +9 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +9 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/types.d.ts +137 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +14 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/core/ChatEngine.d.ts +47 -0
- package/dist/core/ChatEngine.d.ts.map +1 -0
- package/dist/core/ChatEngine.js +355 -0
- package/dist/core/ChatEngine.js.map +1 -0
- package/dist/core/ToolBuilder.d.ts +25 -0
- package/dist/core/ToolBuilder.d.ts.map +1 -0
- package/dist/core/ToolBuilder.js +215 -0
- package/dist/core/ToolBuilder.js.map +1 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +6 -0
- package/dist/core/index.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/DefaultStdioTransportFactory.d.ts +27 -0
- package/dist/mcp/DefaultStdioTransportFactory.d.ts.map +1 -0
- package/dist/mcp/DefaultStdioTransportFactory.js +60 -0
- package/dist/mcp/DefaultStdioTransportFactory.js.map +1 -0
- package/dist/mcp/MCPClient.d.ts +60 -0
- package/dist/mcp/MCPClient.d.ts.map +1 -0
- package/dist/mcp/MCPClient.js +164 -0
- package/dist/mcp/MCPClient.js.map +1 -0
- package/dist/mcp/index.d.ts +10 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +11 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/paths.d.ts +16 -0
- package/dist/mcp/paths.d.ts.map +1 -0
- package/dist/mcp/paths.js +58 -0
- package/dist/mcp/paths.js.map +1 -0
- package/dist/mcp/types.d.ts +85 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +7 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/providers/index.d.ts +40 -0
- package/dist/providers/index.d.ts.map +1 -0
- package/dist/providers/index.js +148 -0
- package/dist/providers/index.js.map +1 -0
- package/dist/providers/toolplex.d.ts +43 -0
- package/dist/providers/toolplex.d.ts.map +1 -0
- package/dist/providers/toolplex.js +168 -0
- package/dist/providers/toolplex.js.map +1 -0
- package/dist/types/index.d.ts +218 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/index.d.ts +8 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +8 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/models.d.ts +30 -0
- package/dist/utils/models.d.ts.map +1 -0
- package/dist/utils/models.js +52 -0
- package/dist/utils/models.js.map +1 -0
- package/dist/utils/schema.d.ts +74 -0
- package/dist/utils/schema.d.ts.map +1 -0
- package/dist/utils/schema.js +253 -0
- package/dist/utils/schema.js.map +1 -0
- package/package.json +70 -0
- package/src/adapters/index.ts +9 -0
- package/src/adapters/types.ts +241 -0
- package/src/core/ChatEngine.ts +464 -0
- package/src/core/ToolBuilder.ts +323 -0
- package/src/core/index.ts +6 -0
- package/src/index.ts +86 -0
- package/src/mcp/DefaultStdioTransportFactory.ts +71 -0
- package/src/mcp/MCPClient.ts +209 -0
- package/src/mcp/index.ts +24 -0
- package/src/mcp/paths.ts +91 -0
- package/src/mcp/types.ts +93 -0
- package/src/providers/index.ts +177 -0
- package/src/providers/toolplex.ts +217 -0
- package/src/types/index.ts +290 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/models.ts +59 -0
- package/src/utils/schema.ts +307 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @toolplex/ai-engine - Core Type Definitions
|
|
3
|
+
*
|
|
4
|
+
* Platform-agnostic types for the AI chat engine.
|
|
5
|
+
* These types are used across desktop, cloud, and CLI environments.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CoreMessage } from "ai";
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// Provider Types
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Provider configuration
|
|
16
|
+
*/
|
|
17
|
+
export interface ProviderConfig {
|
|
18
|
+
id: string;
|
|
19
|
+
apiKey?: string;
|
|
20
|
+
baseURL?: string;
|
|
21
|
+
timeout?: number;
|
|
22
|
+
maxRetries?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Credentials passed to provider factory
|
|
27
|
+
*/
|
|
28
|
+
export interface ProviderCredentials {
|
|
29
|
+
openaiKey?: string;
|
|
30
|
+
anthropicKey?: string;
|
|
31
|
+
googleKey?: string;
|
|
32
|
+
openrouterKey?: string;
|
|
33
|
+
deepseekKey?: string;
|
|
34
|
+
moonshotKey?: string;
|
|
35
|
+
toolplexApiKey?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Provider interface - wraps AI SDK providers
|
|
40
|
+
*/
|
|
41
|
+
export interface AIProvider {
|
|
42
|
+
id: string;
|
|
43
|
+
chat: (modelId: string) => any; // Returns LanguageModelV1 from AI SDK
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ============================================================================
|
|
47
|
+
// Stream Types
|
|
48
|
+
// ============================================================================
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Model configuration flags (subset from server-side ModelConfig)
|
|
52
|
+
*/
|
|
53
|
+
export interface ModelConfigFlags {
|
|
54
|
+
preserveEmptyContentBlocks?: boolean;
|
|
55
|
+
enforceMaxTokens?: boolean;
|
|
56
|
+
maxOutputTokens?: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* File attachment structure
|
|
61
|
+
*/
|
|
62
|
+
export interface FileAttachment {
|
|
63
|
+
name: string;
|
|
64
|
+
mimeType: string;
|
|
65
|
+
data: string; // base64 encoded
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Streaming options for chat requests
|
|
70
|
+
*/
|
|
71
|
+
export interface StreamOptions {
|
|
72
|
+
streamId?: string;
|
|
73
|
+
sessionId: string;
|
|
74
|
+
modelId: string;
|
|
75
|
+
provider: string;
|
|
76
|
+
messages: CoreMessage[];
|
|
77
|
+
tools?: any;
|
|
78
|
+
temperature?: number;
|
|
79
|
+
maxTokens?: number;
|
|
80
|
+
topP?: number;
|
|
81
|
+
fileAttachments?: FileAttachment[];
|
|
82
|
+
streamingMessageId?: string;
|
|
83
|
+
modelConfig?: ModelConfigFlags;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Tool call data structure
|
|
88
|
+
*/
|
|
89
|
+
export interface ToolCallData {
|
|
90
|
+
id: string;
|
|
91
|
+
type: "function";
|
|
92
|
+
function: {
|
|
93
|
+
name: string;
|
|
94
|
+
arguments: string;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Usage/token data structure
|
|
100
|
+
*/
|
|
101
|
+
export interface UsageData {
|
|
102
|
+
promptTokens: number;
|
|
103
|
+
completionTokens: number;
|
|
104
|
+
totalTokens: number;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Stream event types
|
|
109
|
+
*/
|
|
110
|
+
export type StreamEvent =
|
|
111
|
+
| { type: "chunk"; content: string }
|
|
112
|
+
| { type: "tool_call"; toolCall: ToolCallData }
|
|
113
|
+
| { type: "complete"; fullText: string; usage?: UsageData }
|
|
114
|
+
| { type: "error"; error: string };
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Stream result from the engine
|
|
118
|
+
*/
|
|
119
|
+
export interface StreamResult {
|
|
120
|
+
streamId: string;
|
|
121
|
+
textStream: AsyncIterable<string>;
|
|
122
|
+
fullStream: AsyncIterable<any>;
|
|
123
|
+
onFinishPromise: Promise<void>;
|
|
124
|
+
abort: () => Promise<void>;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ============================================================================
|
|
128
|
+
// MCP Types
|
|
129
|
+
// ============================================================================
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* MCP tool definition from server
|
|
133
|
+
*/
|
|
134
|
+
export interface MCPTool {
|
|
135
|
+
name: string;
|
|
136
|
+
description?: string;
|
|
137
|
+
inputSchema?: any;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* MCP session information
|
|
142
|
+
*/
|
|
143
|
+
export interface MCPSessionInfo {
|
|
144
|
+
exists: boolean;
|
|
145
|
+
isConnected?: boolean;
|
|
146
|
+
serverPath?: string;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Result from MCP operations
|
|
151
|
+
*/
|
|
152
|
+
export interface MCPResult {
|
|
153
|
+
success: boolean;
|
|
154
|
+
error?: string;
|
|
155
|
+
data?: any;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* MCP tool call result
|
|
160
|
+
*/
|
|
161
|
+
export interface MCPToolResult {
|
|
162
|
+
content: Array<{
|
|
163
|
+
type: string;
|
|
164
|
+
text?: string;
|
|
165
|
+
data?: any;
|
|
166
|
+
}>;
|
|
167
|
+
isError?: boolean;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ============================================================================
|
|
171
|
+
// Confirmation Types
|
|
172
|
+
// ============================================================================
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Types of tool confirmations
|
|
176
|
+
*/
|
|
177
|
+
export type ConfirmationType =
|
|
178
|
+
| "install"
|
|
179
|
+
| "uninstall"
|
|
180
|
+
| "missing-servers"
|
|
181
|
+
| "save-playbook"
|
|
182
|
+
| "submit-feedback"
|
|
183
|
+
| "large-result";
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Confirmation request from engine to adapter
|
|
187
|
+
*/
|
|
188
|
+
export interface ConfirmationRequest {
|
|
189
|
+
type: ConfirmationType;
|
|
190
|
+
data: any;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Confirmation result from adapter
|
|
195
|
+
*/
|
|
196
|
+
export interface ConfirmationResult {
|
|
197
|
+
allowed: boolean;
|
|
198
|
+
reason?: string;
|
|
199
|
+
action?: string;
|
|
200
|
+
editedConfig?: any;
|
|
201
|
+
wasEdited?: boolean;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ============================================================================
|
|
205
|
+
// Engine Events
|
|
206
|
+
// ============================================================================
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Events emitted by the engine during streaming
|
|
210
|
+
*/
|
|
211
|
+
export interface EngineEvents {
|
|
212
|
+
// Stream events
|
|
213
|
+
onStreamChunk: (streamId: string, chunk: string) => void;
|
|
214
|
+
onStreamComplete: (
|
|
215
|
+
streamId: string,
|
|
216
|
+
fullText: string,
|
|
217
|
+
usage?: UsageData,
|
|
218
|
+
) => void;
|
|
219
|
+
onStreamError: (streamId: string, error: string) => void;
|
|
220
|
+
|
|
221
|
+
// Tool events
|
|
222
|
+
onToolInputStart: (
|
|
223
|
+
streamId: string,
|
|
224
|
+
toolCallId: string,
|
|
225
|
+
toolName: string,
|
|
226
|
+
) => void;
|
|
227
|
+
onToolInputDelta: (
|
|
228
|
+
streamId: string,
|
|
229
|
+
toolCallId: string,
|
|
230
|
+
argsDelta: string,
|
|
231
|
+
) => void;
|
|
232
|
+
onToolResult: (
|
|
233
|
+
streamId: string,
|
|
234
|
+
toolCallId: string,
|
|
235
|
+
result: MCPToolResult,
|
|
236
|
+
toolName: string,
|
|
237
|
+
args: any,
|
|
238
|
+
) => void;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// ============================================================================
|
|
242
|
+
// Engine Configuration
|
|
243
|
+
// ============================================================================
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Configuration for the AI engine
|
|
247
|
+
*/
|
|
248
|
+
export interface EngineConfig {
|
|
249
|
+
/** Maximum number of tool execution steps per request */
|
|
250
|
+
maxSteps?: number;
|
|
251
|
+
|
|
252
|
+
/** Whether to enable debug logging */
|
|
253
|
+
debug?: boolean;
|
|
254
|
+
|
|
255
|
+
/** Tools to hide from the AI agent */
|
|
256
|
+
hiddenTools?: string[];
|
|
257
|
+
|
|
258
|
+
/** Client version for API requests */
|
|
259
|
+
clientVersion?: string;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ============================================================================
|
|
263
|
+
// Session Types
|
|
264
|
+
// ============================================================================
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Chat session information
|
|
268
|
+
*/
|
|
269
|
+
export interface ChatSession {
|
|
270
|
+
id: string;
|
|
271
|
+
createdAt: Date;
|
|
272
|
+
updatedAt: Date;
|
|
273
|
+
metadata?: Record<string, any>;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Message in a chat session
|
|
278
|
+
*/
|
|
279
|
+
export interface ChatMessage {
|
|
280
|
+
id: string;
|
|
281
|
+
sessionId: string;
|
|
282
|
+
role: "user" | "assistant" | "system" | "tool";
|
|
283
|
+
content: string | any[];
|
|
284
|
+
toolCallId?: string;
|
|
285
|
+
toolCalls?: ToolCallData[];
|
|
286
|
+
createdAt: Date;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Re-export CoreMessage from ai sdk
|
|
290
|
+
export type { CoreMessage };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @toolplex/ai-engine - Model Utilities
|
|
3
|
+
*
|
|
4
|
+
* Utility functions for model detection and handling.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Check if a model is a ChatGPT/OpenAI model
|
|
9
|
+
* Includes both direct OpenAI models (gpt-*) and OpenRouter proxied models (openai/*)
|
|
10
|
+
*/
|
|
11
|
+
export function isChatGPTModel(modelId: string): boolean {
|
|
12
|
+
return modelId.startsWith("gpt-") || modelId.startsWith("openai/gpt-");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Check if the model is a Google Gemini model
|
|
17
|
+
*/
|
|
18
|
+
export function isGoogleGeminiModel(modelId: string): boolean {
|
|
19
|
+
return modelId.startsWith("google/") || modelId.startsWith("gemini");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if the model is an Anthropic Claude model
|
|
24
|
+
*/
|
|
25
|
+
export function isAnthropicModel(modelId: string): boolean {
|
|
26
|
+
return modelId.startsWith("anthropic/") || modelId.startsWith("claude");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Extract provider and model ID from a combined model string
|
|
31
|
+
* Format: "provider/model-id" or just "model-id"
|
|
32
|
+
*
|
|
33
|
+
* @param modelId - Combined model ID string
|
|
34
|
+
* @returns Object with providerId and actualModelId
|
|
35
|
+
*/
|
|
36
|
+
export function parseModelId(modelId: string): {
|
|
37
|
+
providerId: string;
|
|
38
|
+
actualModelId: string;
|
|
39
|
+
} {
|
|
40
|
+
const parts = modelId.split("/");
|
|
41
|
+
if (parts.length === 1) {
|
|
42
|
+
// No provider prefix, try to detect from model name
|
|
43
|
+
if (modelId.startsWith("gpt-") || modelId.startsWith("o1")) {
|
|
44
|
+
return { providerId: "openai", actualModelId: modelId };
|
|
45
|
+
}
|
|
46
|
+
if (modelId.startsWith("claude")) {
|
|
47
|
+
return { providerId: "anthropic", actualModelId: modelId };
|
|
48
|
+
}
|
|
49
|
+
if (modelId.startsWith("gemini")) {
|
|
50
|
+
return { providerId: "google", actualModelId: modelId };
|
|
51
|
+
}
|
|
52
|
+
// Default to toolplex
|
|
53
|
+
return { providerId: "toolplex", actualModelId: modelId };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const providerId = parts[0];
|
|
57
|
+
const actualModelId = parts.slice(1).join("/");
|
|
58
|
+
return { providerId, actualModelId };
|
|
59
|
+
}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @toolplex/ai-engine - Schema Utilities
|
|
3
|
+
*
|
|
4
|
+
* Pure utility functions for JSON Schema manipulation.
|
|
5
|
+
* Used for tool schema processing before passing to AI SDK.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { LoggerAdapter } from "../adapters/types.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Deep sanitize tool parameters by recursively parsing stringified JSON values
|
|
12
|
+
*
|
|
13
|
+
* Some LLMs incorrectly stringify nested objects in tool parameters.
|
|
14
|
+
* This function recursively detects and parses such stringified values while
|
|
15
|
+
* respecting the tool's input schema to avoid corrupting legitimate string parameters.
|
|
16
|
+
*
|
|
17
|
+
* CRITICAL FIX: This function is now schema-aware. It will NOT parse strings that
|
|
18
|
+
* the schema explicitly declares as type "string". This prevents catastrophic bugs
|
|
19
|
+
* where tools expecting JSON content as a string (e.g., write_file) would receive
|
|
20
|
+
* a parsed object instead.
|
|
21
|
+
*
|
|
22
|
+
* FIELD-AWARE FIX: Special handling for the 'arguments' field in call_tool.
|
|
23
|
+
* ChatGPT and other models sometimes stringify this field even though it should
|
|
24
|
+
* always be an object. This is a documented OpenAI issue (July 2024).
|
|
25
|
+
*
|
|
26
|
+
* @param params - The parameters to sanitize
|
|
27
|
+
* @param schema - The JSON Schema for these parameters (optional but recommended)
|
|
28
|
+
* @param fieldName - The name of the field being processed (for field-aware logic)
|
|
29
|
+
* @param logger - Optional logger for debug output
|
|
30
|
+
* @returns Sanitized parameters
|
|
31
|
+
*/
|
|
32
|
+
export function deepSanitizeParams(
|
|
33
|
+
params: any,
|
|
34
|
+
schema?: any,
|
|
35
|
+
fieldName?: string,
|
|
36
|
+
logger?: LoggerAdapter,
|
|
37
|
+
): any {
|
|
38
|
+
if (params === null || params === undefined) {
|
|
39
|
+
return params;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// If it's a string, check schema before attempting to parse
|
|
43
|
+
if (typeof params === "string") {
|
|
44
|
+
const trimmed = params.trim();
|
|
45
|
+
|
|
46
|
+
// Only consider parsing if it looks like JSON (starts with { or [)
|
|
47
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
48
|
+
// CRITICAL: If schema explicitly says this should be a string, NEVER parse
|
|
49
|
+
// This prevents corrupting legitimate JSON content that tools expect as strings
|
|
50
|
+
if (schema?.type === "string") {
|
|
51
|
+
return params;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// If schema says this should be an object or array, parse it
|
|
55
|
+
// This fixes LLMs that accidentally stringify nested objects
|
|
56
|
+
if (schema?.type === "object" || schema?.type === "array") {
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(params);
|
|
59
|
+
return deepSanitizeParams(parsed, schema, fieldName, logger);
|
|
60
|
+
} catch {
|
|
61
|
+
// Invalid JSON, return as-is
|
|
62
|
+
return params;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// FIELD-AWARE FIX: Special handling for 'arguments' field
|
|
67
|
+
// Known issue: ChatGPT sometimes stringifies the arguments field in call_tool
|
|
68
|
+
// even though it should always be an object (OpenAI bug documented July 2024)
|
|
69
|
+
if (fieldName === "arguments") {
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(params);
|
|
72
|
+
logger?.info(
|
|
73
|
+
'Parsed stringified "arguments" field (ChatGPT workaround)',
|
|
74
|
+
{
|
|
75
|
+
fieldName,
|
|
76
|
+
originalType: typeof params,
|
|
77
|
+
parsedType: typeof parsed,
|
|
78
|
+
hadSchema: !!schema,
|
|
79
|
+
},
|
|
80
|
+
);
|
|
81
|
+
return deepSanitizeParams(parsed, schema, fieldName, logger);
|
|
82
|
+
} catch {
|
|
83
|
+
// Invalid JSON, let validation fail naturally
|
|
84
|
+
logger?.warn('Failed to parse stringified "arguments" field', {
|
|
85
|
+
fieldName,
|
|
86
|
+
});
|
|
87
|
+
return params;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// No schema or ambiguous schema - be conservative and don't parse
|
|
92
|
+
return params;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Regular string (doesn't look like JSON), return as-is
|
|
96
|
+
return params;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// If it's an array, recursively sanitize each element
|
|
100
|
+
if (Array.isArray(params)) {
|
|
101
|
+
const itemSchema = schema?.items;
|
|
102
|
+
return params.map((item) =>
|
|
103
|
+
deepSanitizeParams(item, itemSchema, fieldName, logger),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// If it's an object, recursively sanitize each property with its schema
|
|
108
|
+
if (typeof params === "object") {
|
|
109
|
+
const sanitized: any = {};
|
|
110
|
+
const properties = schema?.properties || {};
|
|
111
|
+
|
|
112
|
+
for (const [key, value] of Object.entries(params)) {
|
|
113
|
+
const propertySchema = properties[key];
|
|
114
|
+
// Pass the key as fieldName for field-aware logic
|
|
115
|
+
sanitized[key] = deepSanitizeParams(value, propertySchema, key, logger);
|
|
116
|
+
}
|
|
117
|
+
return sanitized;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Primitive value, return as-is
|
|
121
|
+
return params;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Resolve $ref references in JSON Schema by inlining from $defs
|
|
126
|
+
*
|
|
127
|
+
* Some MCP servers (like ElevenLabs) return tool schemas with $ref references
|
|
128
|
+
* that point to $defs entries. The AI SDK's jsonSchema() validator (via AJV)
|
|
129
|
+
* fails to resolve these references if they're not properly structured.
|
|
130
|
+
*
|
|
131
|
+
* This function:
|
|
132
|
+
* 1. Extracts $defs from the root schema (if present)
|
|
133
|
+
* 2. Recursively replaces $ref references with their actual definitions
|
|
134
|
+
* 3. Falls back to permissive schema ({}) for unresolved references
|
|
135
|
+
*
|
|
136
|
+
* @param schema - The JSON Schema to process
|
|
137
|
+
* @param defs - The $defs object from the root schema (optional, extracted from schema if not provided)
|
|
138
|
+
* @param logger - Optional logger for warnings
|
|
139
|
+
* @returns Schema with $ref references resolved
|
|
140
|
+
*/
|
|
141
|
+
export function resolveSchemaRefs(
|
|
142
|
+
schema: any,
|
|
143
|
+
defs?: Record<string, any>,
|
|
144
|
+
logger?: LoggerAdapter,
|
|
145
|
+
): any {
|
|
146
|
+
if (!schema || typeof schema !== "object") return schema;
|
|
147
|
+
|
|
148
|
+
// Extract $defs from root schema if not provided
|
|
149
|
+
if (!defs && schema.$defs) {
|
|
150
|
+
defs = schema.$defs;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Handle arrays
|
|
154
|
+
if (Array.isArray(schema)) {
|
|
155
|
+
return schema.map((item) => resolveSchemaRefs(item, defs, logger));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Check if this is a $ref
|
|
159
|
+
if (schema.$ref && typeof schema.$ref === "string") {
|
|
160
|
+
// Parse the $ref to extract the definition name
|
|
161
|
+
// Format: "#/$defs/DefinitionName" or "#/definitions/DefinitionName"
|
|
162
|
+
const refMatch = schema.$ref.match(/^#\/(\$defs|definitions)\/(.+)$/);
|
|
163
|
+
|
|
164
|
+
if (refMatch && defs) {
|
|
165
|
+
const defName = refMatch[2];
|
|
166
|
+
const resolvedDef = defs[defName];
|
|
167
|
+
|
|
168
|
+
if (resolvedDef) {
|
|
169
|
+
// Recursively resolve any nested $refs in the definition
|
|
170
|
+
// Merge any other properties from the $ref schema (like description)
|
|
171
|
+
const { $ref: _ref, ...otherProps } = schema;
|
|
172
|
+
const resolved = resolveSchemaRefs(resolvedDef, defs, logger);
|
|
173
|
+
return { ...resolved, ...otherProps };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Unresolved $ref - replace with permissive schema
|
|
178
|
+
logger?.warn("Unresolved $ref in tool schema, using permissive schema", {
|
|
179
|
+
ref: schema.$ref,
|
|
180
|
+
availableDefs: defs ? Object.keys(defs) : [],
|
|
181
|
+
});
|
|
182
|
+
return {}; // Permissive schema - accepts anything
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Recursively process object properties
|
|
186
|
+
const result: any = {};
|
|
187
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
188
|
+
if (key === "$defs" || key === "definitions") {
|
|
189
|
+
// Keep $defs in result for nested resolution
|
|
190
|
+
result[key] = value;
|
|
191
|
+
} else {
|
|
192
|
+
result[key] = resolveSchemaRefs(value, defs, logger);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Sanitize JSON Schema for Google Gemini compatibility
|
|
201
|
+
*
|
|
202
|
+
* Gemini has strict schema requirements for function calling:
|
|
203
|
+
* - No oneOf, anyOf, allOf constructs
|
|
204
|
+
* - No const values
|
|
205
|
+
* - enum only allowed for string types
|
|
206
|
+
* - Object types must have properties defined (even if empty)
|
|
207
|
+
* - required arrays cause issues with AI SDK transformation
|
|
208
|
+
*/
|
|
209
|
+
export function sanitizeSchemaForGemini(schema: any): any {
|
|
210
|
+
if (!schema || typeof schema !== "object") return schema;
|
|
211
|
+
if (Array.isArray(schema)) {
|
|
212
|
+
return schema.map((item) => sanitizeSchemaForGemini(item));
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const result: any = {};
|
|
216
|
+
|
|
217
|
+
for (const [key, value] of Object.entries(schema)) {
|
|
218
|
+
// Skip unsupported constructs
|
|
219
|
+
if (
|
|
220
|
+
key === "oneOf" ||
|
|
221
|
+
key === "anyOf" ||
|
|
222
|
+
key === "allOf" ||
|
|
223
|
+
key === "const"
|
|
224
|
+
) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Skip enum on non-string types
|
|
229
|
+
if (key === "enum" && schema.type !== "string") {
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Skip required arrays entirely - Gemini has issues with AI SDK's transformation
|
|
234
|
+
if (key === "required") {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Recursively sanitize nested structures
|
|
239
|
+
if (key === "properties" && typeof value === "object") {
|
|
240
|
+
result[key] = {};
|
|
241
|
+
for (const [propName, propValue] of Object.entries(
|
|
242
|
+
value as Record<string, any>,
|
|
243
|
+
)) {
|
|
244
|
+
result[key][propName] = sanitizeSchemaForGemini(propValue);
|
|
245
|
+
}
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (
|
|
250
|
+
(key === "items" || key === "additionalProperties") &&
|
|
251
|
+
typeof value === "object"
|
|
252
|
+
) {
|
|
253
|
+
result[key] = sanitizeSchemaForGemini(value);
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (typeof value === "object" && value !== null) {
|
|
258
|
+
result[key] = sanitizeSchemaForGemini(value);
|
|
259
|
+
} else {
|
|
260
|
+
result[key] = value;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Ensure object types have properties defined
|
|
265
|
+
if (result.type === "object" && !result.properties) {
|
|
266
|
+
result.properties = {};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return result;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Clean a tool schema for AI SDK consumption
|
|
274
|
+
*
|
|
275
|
+
* Combines all schema processing steps:
|
|
276
|
+
* 1. Remove $schema reference
|
|
277
|
+
* 2. Resolve $ref references
|
|
278
|
+
* 3. Apply Gemini sanitization if needed
|
|
279
|
+
*
|
|
280
|
+
* @param schema - Raw tool input schema from MCP
|
|
281
|
+
* @param isGemini - Whether the target model is Google Gemini
|
|
282
|
+
* @param logger - Optional logger
|
|
283
|
+
* @returns Cleaned schema ready for AI SDK
|
|
284
|
+
*/
|
|
285
|
+
export function cleanToolSchema(
|
|
286
|
+
schema: any,
|
|
287
|
+
isGemini: boolean = false,
|
|
288
|
+
logger?: LoggerAdapter,
|
|
289
|
+
): any {
|
|
290
|
+
if (!schema) {
|
|
291
|
+
return { type: "object", properties: {} };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Deep clone to avoid mutating original
|
|
295
|
+
const cleanedSchema = JSON.parse(JSON.stringify(schema));
|
|
296
|
+
delete cleanedSchema.$schema;
|
|
297
|
+
|
|
298
|
+
// Resolve $ref references
|
|
299
|
+
const resolvedSchema = resolveSchemaRefs(cleanedSchema, undefined, logger);
|
|
300
|
+
|
|
301
|
+
// Remove $defs after resolution
|
|
302
|
+
delete resolvedSchema.$defs;
|
|
303
|
+
delete resolvedSchema.definitions;
|
|
304
|
+
|
|
305
|
+
// Apply Gemini-specific sanitization if needed
|
|
306
|
+
return isGemini ? sanitizeSchemaForGemini(resolvedSchema) : resolvedSchema;
|
|
307
|
+
}
|