@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.
Files changed (88) hide show
  1. package/LICENSE +98 -0
  2. package/README.md +292 -0
  3. package/dist/adapters/index.d.ts +9 -0
  4. package/dist/adapters/index.d.ts.map +1 -0
  5. package/dist/adapters/index.js +9 -0
  6. package/dist/adapters/index.js.map +1 -0
  7. package/dist/adapters/types.d.ts +137 -0
  8. package/dist/adapters/types.d.ts.map +1 -0
  9. package/dist/adapters/types.js +14 -0
  10. package/dist/adapters/types.js.map +1 -0
  11. package/dist/core/ChatEngine.d.ts +47 -0
  12. package/dist/core/ChatEngine.d.ts.map +1 -0
  13. package/dist/core/ChatEngine.js +355 -0
  14. package/dist/core/ChatEngine.js.map +1 -0
  15. package/dist/core/ToolBuilder.d.ts +25 -0
  16. package/dist/core/ToolBuilder.d.ts.map +1 -0
  17. package/dist/core/ToolBuilder.js +215 -0
  18. package/dist/core/ToolBuilder.js.map +1 -0
  19. package/dist/core/index.d.ts +6 -0
  20. package/dist/core/index.d.ts.map +1 -0
  21. package/dist/core/index.js +6 -0
  22. package/dist/core/index.js.map +1 -0
  23. package/dist/index.d.ts +41 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +49 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/mcp/DefaultStdioTransportFactory.d.ts +27 -0
  28. package/dist/mcp/DefaultStdioTransportFactory.d.ts.map +1 -0
  29. package/dist/mcp/DefaultStdioTransportFactory.js +60 -0
  30. package/dist/mcp/DefaultStdioTransportFactory.js.map +1 -0
  31. package/dist/mcp/MCPClient.d.ts +60 -0
  32. package/dist/mcp/MCPClient.d.ts.map +1 -0
  33. package/dist/mcp/MCPClient.js +164 -0
  34. package/dist/mcp/MCPClient.js.map +1 -0
  35. package/dist/mcp/index.d.ts +10 -0
  36. package/dist/mcp/index.d.ts.map +1 -0
  37. package/dist/mcp/index.js +11 -0
  38. package/dist/mcp/index.js.map +1 -0
  39. package/dist/mcp/paths.d.ts +16 -0
  40. package/dist/mcp/paths.d.ts.map +1 -0
  41. package/dist/mcp/paths.js +58 -0
  42. package/dist/mcp/paths.js.map +1 -0
  43. package/dist/mcp/types.d.ts +85 -0
  44. package/dist/mcp/types.d.ts.map +1 -0
  45. package/dist/mcp/types.js +7 -0
  46. package/dist/mcp/types.js.map +1 -0
  47. package/dist/providers/index.d.ts +40 -0
  48. package/dist/providers/index.d.ts.map +1 -0
  49. package/dist/providers/index.js +148 -0
  50. package/dist/providers/index.js.map +1 -0
  51. package/dist/providers/toolplex.d.ts +43 -0
  52. package/dist/providers/toolplex.d.ts.map +1 -0
  53. package/dist/providers/toolplex.js +168 -0
  54. package/dist/providers/toolplex.js.map +1 -0
  55. package/dist/types/index.d.ts +218 -0
  56. package/dist/types/index.d.ts.map +1 -0
  57. package/dist/types/index.js +8 -0
  58. package/dist/types/index.js.map +1 -0
  59. package/dist/utils/index.d.ts +8 -0
  60. package/dist/utils/index.d.ts.map +1 -0
  61. package/dist/utils/index.js +8 -0
  62. package/dist/utils/index.js.map +1 -0
  63. package/dist/utils/models.d.ts +30 -0
  64. package/dist/utils/models.d.ts.map +1 -0
  65. package/dist/utils/models.js +52 -0
  66. package/dist/utils/models.js.map +1 -0
  67. package/dist/utils/schema.d.ts +74 -0
  68. package/dist/utils/schema.d.ts.map +1 -0
  69. package/dist/utils/schema.js +253 -0
  70. package/dist/utils/schema.js.map +1 -0
  71. package/package.json +70 -0
  72. package/src/adapters/index.ts +9 -0
  73. package/src/adapters/types.ts +241 -0
  74. package/src/core/ChatEngine.ts +464 -0
  75. package/src/core/ToolBuilder.ts +323 -0
  76. package/src/core/index.ts +6 -0
  77. package/src/index.ts +86 -0
  78. package/src/mcp/DefaultStdioTransportFactory.ts +71 -0
  79. package/src/mcp/MCPClient.ts +209 -0
  80. package/src/mcp/index.ts +24 -0
  81. package/src/mcp/paths.ts +91 -0
  82. package/src/mcp/types.ts +93 -0
  83. package/src/providers/index.ts +177 -0
  84. package/src/providers/toolplex.ts +217 -0
  85. package/src/types/index.ts +290 -0
  86. package/src/utils/index.ts +8 -0
  87. package/src/utils/models.ts +59 -0
  88. 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,8 @@
1
+ /**
2
+ * @toolplex/ai-engine - Utilities
3
+ *
4
+ * Export all utility functions.
5
+ */
6
+
7
+ export * from "./schema.js";
8
+ export * from "./models.js";
@@ -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
+ }