@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
package/src/mcp/index.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @toolplex/ai-engine - MCP Module
|
|
3
|
+
*
|
|
4
|
+
* MCP (Model Context Protocol) client for ToolPlex.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export { MCPClient } from "./MCPClient.js";
|
|
8
|
+
export type {
|
|
9
|
+
MCPSession,
|
|
10
|
+
MCPResult,
|
|
11
|
+
MCPTool,
|
|
12
|
+
MCPToolResult,
|
|
13
|
+
TransportFactory,
|
|
14
|
+
MCPClientConfig,
|
|
15
|
+
} from "./types.js";
|
|
16
|
+
|
|
17
|
+
// Path utilities
|
|
18
|
+
export { getToolplexClientPath } from "./paths.js";
|
|
19
|
+
|
|
20
|
+
// Default transport factory (uses system Node.js)
|
|
21
|
+
export {
|
|
22
|
+
DefaultStdioTransportFactory,
|
|
23
|
+
defaultStdioTransportFactory,
|
|
24
|
+
} from "./DefaultStdioTransportFactory.js";
|
package/src/mcp/paths.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @toolplex/ai-engine - MCP Paths
|
|
3
|
+
*
|
|
4
|
+
* Utilities for locating @toolplex/client MCP server.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import path from "path";
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import { fileURLToPath } from "url";
|
|
10
|
+
import { createRequire } from "module";
|
|
11
|
+
|
|
12
|
+
// Create __dirname equivalent for ESM
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
|
|
16
|
+
// Create require function for ESM (for require.resolve)
|
|
17
|
+
const require = createRequire(import.meta.url);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get the path to @toolplex/client's MCP server entry point.
|
|
21
|
+
*
|
|
22
|
+
* This resolves the path regardless of where npm installs the package
|
|
23
|
+
* (hoisted or nested in node_modules).
|
|
24
|
+
*
|
|
25
|
+
* @returns Path to the MCP server index.js
|
|
26
|
+
* @throws Error if @toolplex/client cannot be found
|
|
27
|
+
*/
|
|
28
|
+
export function getToolplexClientPath(): string {
|
|
29
|
+
// Try to resolve using require.resolve
|
|
30
|
+
try {
|
|
31
|
+
const clientPackageJson = require.resolve("@toolplex/client/package.json");
|
|
32
|
+
const clientDir = path.dirname(clientPackageJson);
|
|
33
|
+
const mcpServerPath = path.join(
|
|
34
|
+
clientDir,
|
|
35
|
+
"dist",
|
|
36
|
+
"mcp-server",
|
|
37
|
+
"index.js",
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
if (fs.existsSync(mcpServerPath)) {
|
|
41
|
+
return mcpServerPath;
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
// require.resolve failed, try fallback paths
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Fallback: try common paths
|
|
48
|
+
const fallbackPaths = [
|
|
49
|
+
// From process.cwd() (development)
|
|
50
|
+
path.resolve(
|
|
51
|
+
process.cwd(),
|
|
52
|
+
"node_modules/@toolplex/client/dist/mcp-server/index.js",
|
|
53
|
+
),
|
|
54
|
+
// From this module's location (when installed as dependency)
|
|
55
|
+
path.resolve(
|
|
56
|
+
__dirname,
|
|
57
|
+
"../../node_modules/@toolplex/client/dist/mcp-server/index.js",
|
|
58
|
+
),
|
|
59
|
+
path.resolve(
|
|
60
|
+
__dirname,
|
|
61
|
+
"../../../node_modules/@toolplex/client/dist/mcp-server/index.js",
|
|
62
|
+
),
|
|
63
|
+
path.resolve(
|
|
64
|
+
__dirname,
|
|
65
|
+
"../../../../node_modules/@toolplex/client/dist/mcp-server/index.js",
|
|
66
|
+
),
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
// Add Electron production path if available (process.resourcesPath is Electron-specific)
|
|
70
|
+
const electronProcess = process as typeof process & {
|
|
71
|
+
resourcesPath?: string;
|
|
72
|
+
};
|
|
73
|
+
if (electronProcess.resourcesPath) {
|
|
74
|
+
fallbackPaths.push(
|
|
75
|
+
path.resolve(
|
|
76
|
+
electronProcess.resourcesPath,
|
|
77
|
+
"app/node_modules/@toolplex/client/dist/mcp-server/index.js",
|
|
78
|
+
),
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const fallbackPath of fallbackPaths) {
|
|
83
|
+
if (fs.existsSync(fallbackPath)) {
|
|
84
|
+
return fallbackPath;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
throw new Error(
|
|
89
|
+
"@toolplex/client not found. Make sure @toolplex/ai-engine is properly installed.",
|
|
90
|
+
);
|
|
91
|
+
}
|
package/src/mcp/types.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @toolplex/ai-engine - MCP Types
|
|
3
|
+
*
|
|
4
|
+
* Type definitions for MCP (Model Context Protocol) integration.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* MCP Session - holds the client connection
|
|
9
|
+
* Client type is 'any' to avoid version mismatches between ai-engine and consumers
|
|
10
|
+
*/
|
|
11
|
+
export interface MCPSession {
|
|
12
|
+
client: any; // MCP Client instance
|
|
13
|
+
transport: any; // Transport type varies by platform
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Result from MCP operations
|
|
18
|
+
*/
|
|
19
|
+
export interface MCPResult {
|
|
20
|
+
success: boolean;
|
|
21
|
+
data?: any;
|
|
22
|
+
error?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* MCP Tool definition from server
|
|
27
|
+
*/
|
|
28
|
+
export interface MCPTool {
|
|
29
|
+
name: string;
|
|
30
|
+
description?: string;
|
|
31
|
+
inputSchema?: any;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* MCP Tool call result
|
|
36
|
+
*/
|
|
37
|
+
export interface MCPToolResult {
|
|
38
|
+
content: Array<{
|
|
39
|
+
type: string;
|
|
40
|
+
text?: string;
|
|
41
|
+
data?: any;
|
|
42
|
+
[key: string]: any;
|
|
43
|
+
}>;
|
|
44
|
+
isError?: boolean;
|
|
45
|
+
[key: string]: any;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Transport factory interface - platforms implement this
|
|
50
|
+
*/
|
|
51
|
+
export interface TransportFactory {
|
|
52
|
+
/**
|
|
53
|
+
* Create a transport and connect to the MCP server
|
|
54
|
+
* @param apiKey - ToolPlex API key
|
|
55
|
+
* @param sessionResumeHistory - Optional session history for resuming
|
|
56
|
+
* @returns Connected MCP session
|
|
57
|
+
*/
|
|
58
|
+
createTransport(
|
|
59
|
+
apiKey: string,
|
|
60
|
+
sessionResumeHistory?: string,
|
|
61
|
+
): Promise<MCPSession>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Close a transport
|
|
65
|
+
*/
|
|
66
|
+
closeTransport(session: MCPSession): Promise<void>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* MCP Client configuration
|
|
71
|
+
*/
|
|
72
|
+
export interface MCPClientConfig {
|
|
73
|
+
transportFactory: TransportFactory;
|
|
74
|
+
logger?: {
|
|
75
|
+
debug: (message: string, meta?: any) => void;
|
|
76
|
+
info: (message: string, meta?: any) => void;
|
|
77
|
+
warn: (message: string, meta?: any) => void;
|
|
78
|
+
error: (message: string, meta?: any) => void;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Optional image handler for processing image results
|
|
82
|
+
*/
|
|
83
|
+
imageHandler?: {
|
|
84
|
+
initialize(userId: string): Promise<void>;
|
|
85
|
+
processToolResult(
|
|
86
|
+
result: any,
|
|
87
|
+
): Promise<{ content: any[]; savedImages?: any[] }>;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Optional function to get current user ID (for image handling)
|
|
91
|
+
*/
|
|
92
|
+
getCurrentUserId?: () => string | null;
|
|
93
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @toolplex/ai-engine - Provider Factory
|
|
3
|
+
*
|
|
4
|
+
* Central provider management for the AI engine.
|
|
5
|
+
* Handles instantiation of both built-in AI SDK providers and custom providers.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
9
|
+
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
10
|
+
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
11
|
+
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
|
|
12
|
+
import { createToolPlex } from "./toolplex.js";
|
|
13
|
+
import type { AIProvider, ProviderCredentials } from "../types/index.js";
|
|
14
|
+
import type { LoggerAdapter } from "../adapters/types.js";
|
|
15
|
+
|
|
16
|
+
export { toolplexUsageMap } from "./toolplex.js";
|
|
17
|
+
|
|
18
|
+
export interface GetProviderOptions {
|
|
19
|
+
logger?: LoggerAdapter;
|
|
20
|
+
clientVersion?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Get provider instance by ID
|
|
25
|
+
*
|
|
26
|
+
* @param providerId - Provider identifier (e.g., 'toolplex', 'openai', 'anthropic')
|
|
27
|
+
* @param credentials - API keys and credentials
|
|
28
|
+
* @param options - Optional logger and client version
|
|
29
|
+
* @returns Provider instance
|
|
30
|
+
*/
|
|
31
|
+
export function getProvider(
|
|
32
|
+
providerId: string,
|
|
33
|
+
credentials: ProviderCredentials,
|
|
34
|
+
options?: GetProviderOptions,
|
|
35
|
+
): AIProvider {
|
|
36
|
+
switch (providerId.toLowerCase()) {
|
|
37
|
+
case "toolplex": {
|
|
38
|
+
if (!credentials.toolplexApiKey) {
|
|
39
|
+
throw new Error("ToolPlex API key is required");
|
|
40
|
+
}
|
|
41
|
+
const toolplexProvider = createToolPlex({
|
|
42
|
+
apiKey: credentials.toolplexApiKey,
|
|
43
|
+
baseURL: "https://api.toolplex.ai",
|
|
44
|
+
clientVersion: options?.clientVersion,
|
|
45
|
+
logger: options?.logger,
|
|
46
|
+
});
|
|
47
|
+
return {
|
|
48
|
+
id: "toolplex",
|
|
49
|
+
chat: (modelId: string) => toolplexProvider(modelId),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
case "openai": {
|
|
54
|
+
if (!credentials.openaiKey) {
|
|
55
|
+
throw new Error("OpenAI API key is required");
|
|
56
|
+
}
|
|
57
|
+
const openaiProvider = createOpenAI({ apiKey: credentials.openaiKey });
|
|
58
|
+
return {
|
|
59
|
+
id: "openai",
|
|
60
|
+
chat: (modelId: string) => openaiProvider(modelId),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
case "anthropic": {
|
|
65
|
+
if (!credentials.anthropicKey) {
|
|
66
|
+
throw new Error("Anthropic API key is required");
|
|
67
|
+
}
|
|
68
|
+
const anthropicProvider = createAnthropic({
|
|
69
|
+
apiKey: credentials.anthropicKey,
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
id: "anthropic",
|
|
73
|
+
chat: (modelId: string) => anthropicProvider(modelId),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
case "google": {
|
|
78
|
+
if (!credentials.googleKey) {
|
|
79
|
+
throw new Error("Google API key is required");
|
|
80
|
+
}
|
|
81
|
+
const googleProvider = createGoogleGenerativeAI({
|
|
82
|
+
apiKey: credentials.googleKey,
|
|
83
|
+
});
|
|
84
|
+
return {
|
|
85
|
+
id: "google",
|
|
86
|
+
chat: (modelId: string) => googleProvider(modelId),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
case "openrouter": {
|
|
91
|
+
if (!credentials.openrouterKey) {
|
|
92
|
+
throw new Error("OpenRouter API key is required");
|
|
93
|
+
}
|
|
94
|
+
const openrouterProvider = createOpenRouter({
|
|
95
|
+
apiKey: credentials.openrouterKey,
|
|
96
|
+
});
|
|
97
|
+
return {
|
|
98
|
+
id: "openrouter",
|
|
99
|
+
chat: (modelId: string) => openrouterProvider(modelId),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
case "deepseek": {
|
|
104
|
+
if (!credentials.deepseekKey) {
|
|
105
|
+
throw new Error("DeepSeek API key is required");
|
|
106
|
+
}
|
|
107
|
+
// DeepSeek uses OpenAI-compatible API
|
|
108
|
+
const deepseekProvider = createOpenAI({
|
|
109
|
+
apiKey: credentials.deepseekKey,
|
|
110
|
+
baseURL: "https://api.deepseek.com/v1",
|
|
111
|
+
});
|
|
112
|
+
return {
|
|
113
|
+
id: "deepseek",
|
|
114
|
+
chat: (modelId: string) => deepseekProvider(modelId),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
case "moonshot": {
|
|
119
|
+
if (!credentials.moonshotKey) {
|
|
120
|
+
throw new Error("Moonshot API key is required");
|
|
121
|
+
}
|
|
122
|
+
// Moonshot uses OpenAI-compatible API
|
|
123
|
+
const moonshotProvider = createOpenAI({
|
|
124
|
+
apiKey: credentials.moonshotKey,
|
|
125
|
+
baseURL: "https://api.moonshot.cn/v1",
|
|
126
|
+
});
|
|
127
|
+
return {
|
|
128
|
+
id: "moonshot",
|
|
129
|
+
chat: (modelId: string) => moonshotProvider(modelId),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
default:
|
|
134
|
+
throw new Error(`Unknown provider: ${providerId}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get model instance for streaming
|
|
140
|
+
*
|
|
141
|
+
* @param modelId - Full model identifier (e.g., 'anthropic/claude-sonnet-4')
|
|
142
|
+
* @param credentials - API keys and credentials
|
|
143
|
+
* @param options - Optional logger and client version
|
|
144
|
+
* @returns Language model instance ready for streamText()
|
|
145
|
+
*/
|
|
146
|
+
export function getModel(
|
|
147
|
+
modelId: string,
|
|
148
|
+
credentials: ProviderCredentials,
|
|
149
|
+
options?: GetProviderOptions,
|
|
150
|
+
) {
|
|
151
|
+
// Parse model ID format: "provider/model-name"
|
|
152
|
+
const parts = modelId.split("/");
|
|
153
|
+
const providerId = parts[0];
|
|
154
|
+
const actualModelId = parts.slice(1).join("/");
|
|
155
|
+
|
|
156
|
+
const provider = getProvider(providerId, credentials, options);
|
|
157
|
+
return provider.chat(actualModelId);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Check if a provider is available (has required credentials)
|
|
162
|
+
*
|
|
163
|
+
* @param providerId - Provider identifier
|
|
164
|
+
* @param credentials - API keys and credentials
|
|
165
|
+
* @returns True if provider can be instantiated
|
|
166
|
+
*/
|
|
167
|
+
export function isProviderAvailable(
|
|
168
|
+
providerId: string,
|
|
169
|
+
credentials: ProviderCredentials,
|
|
170
|
+
): boolean {
|
|
171
|
+
try {
|
|
172
|
+
getProvider(providerId, credentials);
|
|
173
|
+
return true;
|
|
174
|
+
} catch {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @toolplex/ai-engine - ToolPlex Provider
|
|
3
|
+
*
|
|
4
|
+
* Custom ToolPlex Provider for Vercel AI SDK.
|
|
5
|
+
* Wraps the ToolPlex AI API backend (api.toolplex.ai) which proxies OpenRouter
|
|
6
|
+
* and returns OpenRouter-format SSE streaming responses.
|
|
7
|
+
*
|
|
8
|
+
* The backend handles:
|
|
9
|
+
* - Authentication via ToolPlex API keys (x-api-key header)
|
|
10
|
+
* - Usage tracking and enforcement
|
|
11
|
+
* - OpenRouter model access
|
|
12
|
+
* - Tool call handling
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
|
|
16
|
+
import type { LoggerAdapter } from "../adapters/types.js";
|
|
17
|
+
|
|
18
|
+
export interface ToolPlexConfig {
|
|
19
|
+
apiKey: string;
|
|
20
|
+
baseURL?: string;
|
|
21
|
+
clientVersion?: string;
|
|
22
|
+
logger?: LoggerAdapter;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Global map to store usage data from DONE events
|
|
27
|
+
* Key: sessionId, Value: Usage data from backend
|
|
28
|
+
* This is cleared after being read by the engine
|
|
29
|
+
*/
|
|
30
|
+
export const toolplexUsageMap = new Map<
|
|
31
|
+
string,
|
|
32
|
+
{
|
|
33
|
+
prompt_tokens: number;
|
|
34
|
+
completion_tokens: number;
|
|
35
|
+
total_tokens: number;
|
|
36
|
+
cost?: number;
|
|
37
|
+
}
|
|
38
|
+
>();
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* ToolPlex model factory function type
|
|
42
|
+
*/
|
|
43
|
+
export type ToolPlexModelFactory = (modelId: string) => any;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create a ToolPlex provider instance
|
|
47
|
+
*
|
|
48
|
+
* Uses the specialized OpenRouter SDK provider with custom configuration to work with
|
|
49
|
+
* the ToolPlex backend API which proxies OpenRouter and returns OpenRouter's SSE format.
|
|
50
|
+
*/
|
|
51
|
+
export function createToolPlex(config: ToolPlexConfig): ToolPlexModelFactory {
|
|
52
|
+
const baseURL = config.baseURL || "https://api.toolplex.ai";
|
|
53
|
+
const logger = config.logger;
|
|
54
|
+
|
|
55
|
+
// Build headers
|
|
56
|
+
const headers: Record<string, string> = {
|
|
57
|
+
"x-api-key": config.apiKey,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
if (config.clientVersion) {
|
|
61
|
+
headers["X-Client-Version"] = config.clientVersion;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Create OpenRouter provider with custom configuration for ToolPlex backend
|
|
65
|
+
const provider = createOpenRouter({
|
|
66
|
+
apiKey: config.apiKey,
|
|
67
|
+
baseURL: `${baseURL}/ai`,
|
|
68
|
+
headers,
|
|
69
|
+
// Custom fetch to transform requests and intercept DONE events for usage data
|
|
70
|
+
fetch: async (url, init) => {
|
|
71
|
+
// Add provider field and session_id to request body
|
|
72
|
+
if (init?.body && typeof init.body === "string") {
|
|
73
|
+
try {
|
|
74
|
+
const body = JSON.parse(init.body);
|
|
75
|
+
|
|
76
|
+
// Extract session ID from headers if present
|
|
77
|
+
let sessionId: string | null = null;
|
|
78
|
+
if (init?.headers) {
|
|
79
|
+
if (init.headers instanceof Headers) {
|
|
80
|
+
sessionId = init.headers.get("x-session-id");
|
|
81
|
+
} else if (Array.isArray(init.headers)) {
|
|
82
|
+
const sessionHeader = init.headers.find(
|
|
83
|
+
([key]) => key.toLowerCase() === "x-session-id",
|
|
84
|
+
);
|
|
85
|
+
sessionId = sessionHeader ? sessionHeader[1] : null;
|
|
86
|
+
} else {
|
|
87
|
+
sessionId =
|
|
88
|
+
(init.headers as Record<string, string>)["x-session-id"] ||
|
|
89
|
+
null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
logger?.debug("ToolPlex provider: Transforming request", {
|
|
94
|
+
hasSessionId: !!sessionId,
|
|
95
|
+
sessionId: sessionId ? sessionId.substring(0, 8) + "..." : null,
|
|
96
|
+
bodyKeys: Object.keys(body),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const toolplexBody = {
|
|
100
|
+
...body,
|
|
101
|
+
provider: "openrouter",
|
|
102
|
+
...(sessionId && { session_id: sessionId }),
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
init = {
|
|
106
|
+
...init,
|
|
107
|
+
body: JSON.stringify(toolplexBody),
|
|
108
|
+
};
|
|
109
|
+
} catch (error) {
|
|
110
|
+
logger?.error("Failed to transform ToolPlex request", { error });
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const response = await fetch(url, init);
|
|
115
|
+
|
|
116
|
+
// Intercept SSE stream to capture usage from DONE event
|
|
117
|
+
if (
|
|
118
|
+
response.body &&
|
|
119
|
+
response.headers.get("content-type")?.includes("text/event-stream")
|
|
120
|
+
) {
|
|
121
|
+
const originalBody = response.body;
|
|
122
|
+
const reader = originalBody.getReader();
|
|
123
|
+
const decoder = new TextDecoder();
|
|
124
|
+
|
|
125
|
+
// Create a new readable stream that intercepts SSE events
|
|
126
|
+
const transformedStream = new ReadableStream({
|
|
127
|
+
async start(controller) {
|
|
128
|
+
let buffer = "";
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
while (true) {
|
|
132
|
+
const { done, value } = await reader.read();
|
|
133
|
+
|
|
134
|
+
if (done) {
|
|
135
|
+
controller.close();
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Decode chunk and add to buffer
|
|
140
|
+
buffer += decoder.decode(value, { stream: true });
|
|
141
|
+
|
|
142
|
+
// Process complete SSE events (separated by \n\n)
|
|
143
|
+
const events = buffer.split("\n\n");
|
|
144
|
+
buffer = events.pop() || ""; // Keep incomplete event in buffer
|
|
145
|
+
|
|
146
|
+
// Filter out DONE events and track usage
|
|
147
|
+
const filteredEvents: string[] = [];
|
|
148
|
+
|
|
149
|
+
for (const event of events) {
|
|
150
|
+
if (event.trim()) {
|
|
151
|
+
// Parse SSE event (format: "data: {...}")
|
|
152
|
+
const match = event.match(/^data: (.+)$/m);
|
|
153
|
+
if (match) {
|
|
154
|
+
try {
|
|
155
|
+
const data = JSON.parse(match[1]);
|
|
156
|
+
|
|
157
|
+
// Check for DONE event with usage data
|
|
158
|
+
if (data.done === true && data.usage) {
|
|
159
|
+
const sessionId = (init?.headers as any)?.[
|
|
160
|
+
"x-session-id"
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
if (sessionId) {
|
|
164
|
+
toolplexUsageMap.set(sessionId, {
|
|
165
|
+
prompt_tokens: data.usage.prompt_tokens || 0,
|
|
166
|
+
completion_tokens:
|
|
167
|
+
data.usage.completion_tokens || 0,
|
|
168
|
+
total_tokens: data.usage.total_tokens || 0,
|
|
169
|
+
cost: data.usage.cost,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Skip this event to prevent AI SDK from seeing invalid chunk format
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
} catch {
|
|
177
|
+
// Not JSON or different format - keep the event
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Keep this event
|
|
183
|
+
filteredEvents.push(event);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Reconstruct the filtered stream
|
|
187
|
+
if (filteredEvents.length > 0) {
|
|
188
|
+
const filteredBuffer = filteredEvents.join("\n\n") + "\n\n";
|
|
189
|
+
controller.enqueue(Buffer.from(filteredBuffer, "utf8"));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
} catch (error) {
|
|
193
|
+
logger?.error("Error intercepting ToolPlex SSE stream", {
|
|
194
|
+
error,
|
|
195
|
+
});
|
|
196
|
+
controller.error(error);
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// Return a new response with the transformed stream
|
|
202
|
+
return new Response(transformedStream, {
|
|
203
|
+
status: response.status,
|
|
204
|
+
statusText: response.statusText,
|
|
205
|
+
headers: response.headers,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return response;
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Return a function that creates model instances
|
|
214
|
+
return function (modelId: string) {
|
|
215
|
+
return provider.chat(modelId);
|
|
216
|
+
};
|
|
217
|
+
}
|