langmart-gateway-type3 3.0.45 → 3.0.47
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/dist/admin-tools.d.ts +46 -0
- package/dist/admin-tools.d.ts.map +1 -0
- package/dist/admin-tools.js +933 -0
- package/dist/admin-tools.js.map +1 -0
- package/dist/agent-tools.d.ts +38 -0
- package/dist/agent-tools.d.ts.map +1 -0
- package/dist/agent-tools.js +813 -0
- package/dist/agent-tools.js.map +1 -0
- package/dist/billing-tools.d.ts +25 -0
- package/dist/billing-tools.d.ts.map +1 -0
- package/dist/billing-tools.js +283 -0
- package/dist/billing-tools.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +792 -0
- package/dist/cli.js.map +1 -0
- package/dist/collection-tools.d.ts +26 -0
- package/dist/collection-tools.d.ts.map +1 -0
- package/dist/collection-tools.js +347 -0
- package/dist/collection-tools.js.map +1 -0
- package/dist/lib/tool-schema-validator.d.ts +35 -0
- package/dist/lib/tool-schema-validator.d.ts.map +1 -0
- package/dist/lib/tool-schema-validator.js +146 -0
- package/dist/lib/tool-schema-validator.js.map +1 -0
- package/dist/marketplace-tools.d.ts +116 -0
- package/dist/marketplace-tools.d.ts.map +1 -0
- package/dist/marketplace-tools.js +3089 -0
- package/dist/marketplace-tools.js.map +1 -0
- package/dist/organization-tools.d.ts +37 -0
- package/dist/organization-tools.d.ts.map +1 -0
- package/dist/organization-tools.js +609 -0
- package/dist/organization-tools.js.map +1 -0
- package/dist/seller-tools.d.ts +28 -0
- package/dist/seller-tools.d.ts.map +1 -0
- package/dist/seller-tools.js +437 -0
- package/dist/seller-tools.js.map +1 -0
- package/dist/support-tools.d.ts +23 -0
- package/dist/support-tools.d.ts.map +1 -0
- package/dist/support-tools.js +292 -0
- package/dist/support-tools.js.map +1 -0
- package/dist/test-key-redaction-integration.d.ts +7 -0
- package/dist/test-key-redaction-integration.d.ts.map +1 -0
- package/dist/test-key-redaction-integration.js +80 -0
- package/dist/test-key-redaction-integration.js.map +1 -0
- package/dist/test-key-redaction.d.ts +6 -0
- package/dist/test-key-redaction.d.ts.map +1 -0
- package/dist/test-key-redaction.js +115 -0
- package/dist/test-key-redaction.js.map +1 -0
- package/dist/test-vault-migration.d.ts +2 -0
- package/dist/test-vault-migration.d.ts.map +1 -0
- package/dist/test-vault-migration.js +130 -0
- package/dist/test-vault-migration.js.map +1 -0
- package/dist/user-tools.d.ts +40 -0
- package/dist/user-tools.d.ts.map +1 -0
- package/dist/user-tools.js +685 -0
- package/dist/user-tools.js.map +1 -0
- package/package.json +2 -70
- package/scripts/start.ps1 +24 -3
|
@@ -0,0 +1,3089 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.MarketplaceTools = void 0;
|
|
7
|
+
const debug_utils_1 = require("./debug-utils");
|
|
8
|
+
/**
|
|
9
|
+
* Marketplace Tools - Built-in MCP Tools for LangMart Self-Hosted Gateway CLI
|
|
10
|
+
*
|
|
11
|
+
* Provides AI assistant with tools to manage marketplace connections,
|
|
12
|
+
* providers, and models through natural language conversation.
|
|
13
|
+
*
|
|
14
|
+
* These tools call LangMart Marketplace API endpoints.
|
|
15
|
+
*/
|
|
16
|
+
const axios_1 = __importDefault(require("axios"));
|
|
17
|
+
class MarketplaceTools {
|
|
18
|
+
constructor(marketplaceUrl, apiKey, managementUrl = 'http://localhost:8083', keyVault) {
|
|
19
|
+
this.connectionMap = new Map(); // sequence number → UUID mapping
|
|
20
|
+
this.providerMap = new Map(); // sequence number → UUID mapping
|
|
21
|
+
this.marketplaceUrl = marketplaceUrl;
|
|
22
|
+
this.apiKey = apiKey;
|
|
23
|
+
this.managementUrl = managementUrl;
|
|
24
|
+
this.keyVault = keyVault;
|
|
25
|
+
this.client = axios_1.default.create({
|
|
26
|
+
baseURL: marketplaceUrl,
|
|
27
|
+
timeout: 30000,
|
|
28
|
+
maxRedirects: 0, // Don't follow redirects
|
|
29
|
+
validateStatus: (status) => status < 500, // Accept any non-5xx response
|
|
30
|
+
headers: {
|
|
31
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
32
|
+
'Content-Type': 'application/json'
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
(0, debug_utils_1.debugLog)(`[MarketplaceTools] Initialized with baseURL: ${marketplaceUrl}`);
|
|
36
|
+
}
|
|
37
|
+
static getInstance(marketplaceUrl, apiKey, managementUrl, keyVault) {
|
|
38
|
+
if (!MarketplaceTools.instance && marketplaceUrl && apiKey && keyVault) {
|
|
39
|
+
MarketplaceTools.instance = new MarketplaceTools(marketplaceUrl, apiKey, managementUrl || 'http://localhost:8083', keyVault);
|
|
40
|
+
}
|
|
41
|
+
if (!MarketplaceTools.instance) {
|
|
42
|
+
throw new Error('MarketplaceTools not initialized. Provide marketplaceUrl, apiKey, and keyVault.');
|
|
43
|
+
}
|
|
44
|
+
return MarketplaceTools.instance;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Get tool definitions for AI
|
|
48
|
+
*/
|
|
49
|
+
getTools() {
|
|
50
|
+
return [
|
|
51
|
+
{
|
|
52
|
+
type: 'function',
|
|
53
|
+
function: {
|
|
54
|
+
name: 'marketplace_connections_list',
|
|
55
|
+
description: 'List all user connections from the marketplace. Shows provider, endpoint type, gateway type (Managed/Self-hosted), and status for each connection. Connections are numbered [1], [2], [3], etc. in order of creation. No parameters required - call this tool directly when user asks to list, show, or see connections. You can use the connection numbers in subsequent tool calls (e.g., marketplace_connections_test with connection_id "1").',
|
|
56
|
+
parameters: {
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: {},
|
|
59
|
+
required: []
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
type: 'function',
|
|
65
|
+
function: {
|
|
66
|
+
name: 'marketplace_connections_add',
|
|
67
|
+
description: 'Add a new connection to the marketplace. Creates a connection to an LLM provider (OpenAI, Anthropic, Groq, Ollama, etc.) with specified API key and gateway type. The endpoint_type (protocol translator) is automatically determined from the provider - do NOT ask the user for it. Ask the user for: display name, provider name (must match a provider from marketplace_providers_list), API key, endpoint URL, and gateway type. Marketplace-Managed Compute (Type 2): marketplace stores encrypted API keys and manages gateway - convenient but requires trust. Provider API must be accessible from marketplace network (cloud). Self-Hosted Compute (Type 3): you run local gateway and store API keys in local vault - full control over credentials. Provider API must be accessible from YOUR network (can access localhost/private IPs). Both types work with ANY provider. IMPORTANT: After adding the connection, show the tool result directly to the user with the connection details and ask what they want to do next (test, discover models, update, or remove). DO NOT automatically call marketplace_providers_list or marketplace_models_discover - let the user decide.',
|
|
68
|
+
parameters: {
|
|
69
|
+
type: 'object',
|
|
70
|
+
properties: {
|
|
71
|
+
name: {
|
|
72
|
+
type: 'string',
|
|
73
|
+
description: 'Display name for the connection (e.g., "Claude (Anthropic)", "GPT-4 (OpenAI)")'
|
|
74
|
+
},
|
|
75
|
+
provider_name: {
|
|
76
|
+
type: 'string',
|
|
77
|
+
description: 'Provider key from marketplace_providers_list: "anthropic", "openai", "groq", "google", "ollama", etc. IMPORTANT: Use marketplace_providers_list first to see available providers and their keys.'
|
|
78
|
+
},
|
|
79
|
+
endpoint_url: {
|
|
80
|
+
type: 'string',
|
|
81
|
+
description: 'Provider API endpoint URL (e.g., "https://api.anthropic.com", "https://api.groq.com/openai/v1"). The endpoint_type will be automatically determined from the selected provider.'
|
|
82
|
+
},
|
|
83
|
+
api_key: {
|
|
84
|
+
type: 'string',
|
|
85
|
+
description: 'Provider API key for authentication'
|
|
86
|
+
},
|
|
87
|
+
gateway_type: {
|
|
88
|
+
type: 'number',
|
|
89
|
+
description: 'Gateway type: 2 (Marketplace-Managed Compute - marketplace stores API keys, provider must be reachable from marketplace network), 3 (Self-Hosted Compute - you store API keys locally, provider must be reachable from YOUR network, can access localhost). Both work with ANY provider (OpenAI, Anthropic, Groq, Ollama, etc.)'
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
required: ['name', 'provider_name', 'endpoint_url', 'api_key', 'gateway_type']
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
type: 'function',
|
|
98
|
+
function: {
|
|
99
|
+
name: 'marketplace_connections_remove',
|
|
100
|
+
description: 'Remove a connection from the marketplace (soft delete). IMPORTANT: You can use EITHER a sequence number (1, 2, 3) OR a UUID. If the user says "remove 1" or "remove connection 1", use connection_id: "1" - the tool will automatically resolve it. If the user does not provide any identifier, ask which connection they want to remove.',
|
|
101
|
+
parameters: {
|
|
102
|
+
type: 'object',
|
|
103
|
+
properties: {
|
|
104
|
+
connection_id: {
|
|
105
|
+
type: 'string',
|
|
106
|
+
description: 'Connection identifier - can be EITHER a sequence number from the connections list (e.g., "1", "2", "3") OR a UUID. The tool automatically resolves sequence numbers to UUIDs. Examples: "1" (sequence number) or "b536a9ab-398c-4c81-9d12-f96a12653f26" (UUID).'
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
required: ['connection_id']
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
type: 'function',
|
|
115
|
+
function: {
|
|
116
|
+
name: 'marketplace_connections_test',
|
|
117
|
+
description: 'Test a connection to verify API key and endpoint are working. Works for BOTH Marketplace-Managed Compute and Self-Hosted Compute connections. For Self-Hosted connections, the test is automatically routed to your local gateway instance. IMPORTANT: You can use EITHER a sequence number (1, 2, 3) OR a UUID. If the user says "test 1" or "test connection 1", use connection_id: "1" - the tool will automatically resolve it. If user provides just "test connection" without any number, ask which connection they want to test.',
|
|
118
|
+
parameters: {
|
|
119
|
+
type: 'object',
|
|
120
|
+
properties: {
|
|
121
|
+
connection_id: {
|
|
122
|
+
type: 'string',
|
|
123
|
+
description: 'Connection identifier - can be EITHER a sequence number from the connections list (e.g., "1", "2", "3") OR a UUID. The tool automatically resolves sequence numbers to UUIDs. Examples: "1" (sequence number) or "0238a0c6-0d78-4f40-a08c-b9b797df35d0" (UUID).'
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
required: ['connection_id']
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
type: 'function',
|
|
132
|
+
function: {
|
|
133
|
+
name: 'marketplace_connections_update_key',
|
|
134
|
+
description: 'Update the API key for an existing connection. IMPORTANT: You can use EITHER a sequence number (1, 2, 3) OR a UUID for the connection. If the user says "update key for connection 1", use connection_id: "1" - the tool will automatically resolve it. Always ask the user for the new API key if not provided.',
|
|
135
|
+
parameters: {
|
|
136
|
+
type: 'object',
|
|
137
|
+
properties: {
|
|
138
|
+
connection_id: {
|
|
139
|
+
type: 'string',
|
|
140
|
+
description: 'Connection identifier - can be EITHER a sequence number from the connections list (e.g., "1", "2", "3") OR a UUID. The tool automatically resolves sequence numbers to UUIDs. Examples: "1" (sequence number) or "0238a0c6-0d78-4f40-a08c-b9b797df35d0" (UUID).'
|
|
141
|
+
},
|
|
142
|
+
api_key: {
|
|
143
|
+
type: 'string',
|
|
144
|
+
description: 'New API key (e.g., "sk-..." for OpenAI, "gsk_..." for Groq, etc.). You MUST ask the user for this if not provided.'
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
required: ['connection_id', 'api_key']
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
type: 'function',
|
|
153
|
+
function: {
|
|
154
|
+
name: 'marketplace_providers_list',
|
|
155
|
+
description: 'List all available LLM providers in the marketplace. Shows provider names, base URLs, and active status. No parameters required - call this tool directly when user asks to list, show, or see available providers. IMPORTANT: After displaying the provider list, ALWAYS ask the user: "Would you like to add a connection to any of these providers?" Explain that they will need: (1) Provider API key from the provider, (2) Display name for the connection, (3) Gateway type: Marketplace-Managed Compute (provider must be internet-accessible from marketplace) or Self-Hosted Compute (provider can be on your local network, including localhost). Then use marketplace_connections_add to create the connection. Wait for user response.',
|
|
156
|
+
parameters: {
|
|
157
|
+
type: 'object',
|
|
158
|
+
properties: {},
|
|
159
|
+
required: []
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
type: 'function',
|
|
165
|
+
function: {
|
|
166
|
+
name: 'marketplace_models_discover',
|
|
167
|
+
description: 'Discover and store models from a provider. Fetches available models from the provider API and adds them to the marketplace. IMPORTANT: This requires detailed provider information. If the user wants to discover models from an existing connection, first call marketplace_connections_list to get the connection details (provider, endpoint_url, endpoint_type, gateway_type), then ask the user for their API key. If user wants to discover from a new provider, ask them for: provider name, API key, endpoint URL, endpoint type, and gateway type (2 for Marketplace-Managed Compute - provider must be internet-accessible, 3 for Self-Hosted Compute - provider can be on local network).',
|
|
168
|
+
parameters: {
|
|
169
|
+
type: 'object',
|
|
170
|
+
properties: {
|
|
171
|
+
provider: {
|
|
172
|
+
type: 'string',
|
|
173
|
+
description: 'Provider key (e.g., "groq", "openai", "anthropic"). Use marketplace_providers_list to see available providers.'
|
|
174
|
+
},
|
|
175
|
+
apiKey: {
|
|
176
|
+
type: 'string',
|
|
177
|
+
description: 'Provider API key (e.g., "sk-..." for OpenAI, "gsk_..." for Groq). You MUST ask the user for this.'
|
|
178
|
+
},
|
|
179
|
+
gatewayType: {
|
|
180
|
+
type: 'number',
|
|
181
|
+
description: 'Gateway type: 2 (Marketplace-Managed Compute - provider must be internet-accessible from marketplace), 3 (Self-Hosted Compute - provider can be on local network, localhost accessible). Both work with ANY provider.'
|
|
182
|
+
},
|
|
183
|
+
endpointUrl: {
|
|
184
|
+
type: 'string',
|
|
185
|
+
description: 'Provider endpoint URL (e.g., "https://api.groq.com/openai/v1", "https://api.openai.com/v1")'
|
|
186
|
+
},
|
|
187
|
+
endpointType: {
|
|
188
|
+
type: 'string',
|
|
189
|
+
description: 'Endpoint type: "openai-v1" for OpenAI-compatible APIs, "anthropic-messages" for Anthropic, "google-gemini" for Google'
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
required: ['provider', 'apiKey', 'gatewayType', 'endpointUrl', 'endpointType']
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
type: 'function',
|
|
198
|
+
function: {
|
|
199
|
+
name: 'marketplace_models_list',
|
|
200
|
+
description: 'List and filter available models for the user with pagination. Shows model names, providers, context windows, pricing, and capabilities. Supports filtering by publication status (public/private), price range, provider, keyword search, capabilities, and context window.\n\n**Filter behavior (same as UI tab filter):**\n- Space-separated keywords use AND condition (all must match)\n- Matches against model_id and model_name\n- Example: "groq llama" matches models containing BOTH "groq" AND "llama"\n\nUse filters when user asks for specific model types (e.g., "cheap models", "vision models", "Groq models", "published models", "private models", "models with 100k+ context"). Pagination defaults to 10 models per page - use offset to get next pages. HELPFUL TIP: After showing models, explain that they can use these models for chat by selecting them with Tab or /model command.',
|
|
201
|
+
parameters: {
|
|
202
|
+
type: 'object',
|
|
203
|
+
properties: {
|
|
204
|
+
limit: {
|
|
205
|
+
type: 'number',
|
|
206
|
+
description: 'Number of models to return per page (default: 10, max: 100). Example: 20 for 20 models per page'
|
|
207
|
+
},
|
|
208
|
+
offset: {
|
|
209
|
+
type: 'number',
|
|
210
|
+
description: 'Number of models to skip for pagination (default: 0). Example: 10 for page 2, 20 for page 3, etc.'
|
|
211
|
+
},
|
|
212
|
+
keyword: {
|
|
213
|
+
type: 'string',
|
|
214
|
+
description: 'Filter by keyword(s) - SAME LOGIC AS UI TAB FILTER. Matches against model_id and model_name only. Space-separated keywords use AND matching - ALL keywords must match. Case-insensitive, partial match. Examples: "llama" (single keyword), "groq llama" (must match both "groq" AND "llama" in either model_id or model_name), "google flash lite" (must match all three keywords)'
|
|
215
|
+
},
|
|
216
|
+
access_level: {
|
|
217
|
+
type: 'string',
|
|
218
|
+
description: 'Filter by publication status. Options: "public" (published models available to all marketplace users), "private" (unpublished models only visible to owner). Omit to see all models (both public and private). Example: use "public" to list only published models.'
|
|
219
|
+
},
|
|
220
|
+
provider: {
|
|
221
|
+
type: 'string',
|
|
222
|
+
description: 'Filter by provider name (case-insensitive). Example: "groq", "openai", "anthropic", "google". Use marketplace_providers_list to see available providers.'
|
|
223
|
+
},
|
|
224
|
+
max_input_price: {
|
|
225
|
+
type: 'number',
|
|
226
|
+
description: 'Maximum input price per million tokens (USD). Example: 0.5 for models costing at most $0.50/M input tokens'
|
|
227
|
+
},
|
|
228
|
+
max_output_price: {
|
|
229
|
+
type: 'number',
|
|
230
|
+
description: 'Maximum output price per million tokens (USD). Example: 1.5 for models costing at most $1.50/M output tokens'
|
|
231
|
+
},
|
|
232
|
+
min_context_window: {
|
|
233
|
+
type: 'number',
|
|
234
|
+
description: 'Minimum context window size in tokens. Example: 100000 for models with at least 100k context'
|
|
235
|
+
},
|
|
236
|
+
has_vision: {
|
|
237
|
+
type: 'boolean',
|
|
238
|
+
description: 'Filter for models with vision/image understanding capability'
|
|
239
|
+
},
|
|
240
|
+
has_reasoning: {
|
|
241
|
+
type: 'boolean',
|
|
242
|
+
description: 'Filter for models with advanced reasoning capability (e.g., o1, o3 series)'
|
|
243
|
+
},
|
|
244
|
+
has_tool_use: {
|
|
245
|
+
type: 'boolean',
|
|
246
|
+
description: 'Filter for models with function/tool calling support'
|
|
247
|
+
},
|
|
248
|
+
has_image_gen: {
|
|
249
|
+
type: 'boolean',
|
|
250
|
+
description: 'Filter for models that can generate images (e.g., DALL-E)'
|
|
251
|
+
},
|
|
252
|
+
has_audio: {
|
|
253
|
+
type: 'boolean',
|
|
254
|
+
description: 'Filter for models with audio processing capability'
|
|
255
|
+
},
|
|
256
|
+
has_embedding: {
|
|
257
|
+
type: 'boolean',
|
|
258
|
+
description: 'Filter for embedding models'
|
|
259
|
+
},
|
|
260
|
+
order_by: {
|
|
261
|
+
type: 'string',
|
|
262
|
+
description: 'Sort order for results (default: "model_id_asc"). Options: "model_id_asc", "model_id_desc", "model_name_asc", "model_name_desc", "provider_asc", "provider_desc", "context_window_asc", "context_window_desc", "input_price_asc", "input_price_desc", "output_price_asc", "output_price_desc". Example: "input_price_asc" for cheapest first.'
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
required: []
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
type: 'function',
|
|
271
|
+
function: {
|
|
272
|
+
name: 'marketplace_models_update_capabilities',
|
|
273
|
+
description: 'Update capability flags for a model. Allows setting vision support, reasoning capability, tool/function calling, image generation, video generation, audio processing, and embedding capabilities. Use when user wants to update or configure what their model can do. Each capability is a boolean flag. IMPORTANT: Only the model owner can update capabilities.',
|
|
274
|
+
parameters: {
|
|
275
|
+
type: 'object',
|
|
276
|
+
properties: {
|
|
277
|
+
model_id: {
|
|
278
|
+
type: 'string',
|
|
279
|
+
description: 'Model ID (e.g., "groq/llama-3.3-70b-versatile"). Use marketplace_models_list to get model IDs.'
|
|
280
|
+
},
|
|
281
|
+
supports_vision: {
|
|
282
|
+
type: 'boolean',
|
|
283
|
+
description: 'Can the model process and understand images? (multimodal vision input)'
|
|
284
|
+
},
|
|
285
|
+
supports_reasoning: {
|
|
286
|
+
type: 'boolean',
|
|
287
|
+
description: 'Does the model have advanced reasoning capabilities? (e.g., o1, o3 series)'
|
|
288
|
+
},
|
|
289
|
+
supports_tool_use: {
|
|
290
|
+
type: 'boolean',
|
|
291
|
+
description: 'Does the model support function/tool calling?'
|
|
292
|
+
},
|
|
293
|
+
supports_image_gen: {
|
|
294
|
+
type: 'boolean',
|
|
295
|
+
description: 'Can the model generate images? (e.g., DALL-E, Imagen)'
|
|
296
|
+
},
|
|
297
|
+
supports_video_gen: {
|
|
298
|
+
type: 'boolean',
|
|
299
|
+
description: 'Can the model generate videos?'
|
|
300
|
+
},
|
|
301
|
+
supports_audio: {
|
|
302
|
+
type: 'boolean',
|
|
303
|
+
description: 'Can the model process or generate audio?'
|
|
304
|
+
},
|
|
305
|
+
supports_embedding: {
|
|
306
|
+
type: 'boolean',
|
|
307
|
+
description: 'Is the model designed for text embeddings?'
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
required: ['model_id']
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
type: 'function',
|
|
316
|
+
function: {
|
|
317
|
+
name: 'marketplace_models_get',
|
|
318
|
+
description: 'Get detailed information about a specific model including pricing and capabilities. Returns model name, display name, provider, context window, input/output pricing per million tokens, and all capability flags (vision, reasoning, tool_use, etc.). Use when user asks for details about a specific model, its price, or what it can do.',
|
|
319
|
+
parameters: {
|
|
320
|
+
type: 'object',
|
|
321
|
+
properties: {
|
|
322
|
+
model_id: {
|
|
323
|
+
type: 'string',
|
|
324
|
+
description: 'Model identifier (e.g., "groq/llama-3.3-70b-versatile", "google/gemini-2.5-flash"). Use marketplace_models_list to find available model IDs.'
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
required: ['model_id']
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
{
|
|
332
|
+
type: 'function',
|
|
333
|
+
function: {
|
|
334
|
+
name: 'marketplace_request_logs_list',
|
|
335
|
+
description: 'Query user\'s request logs with pagination and filtering. Shows recent API requests including method, endpoint, status code, model used, IP address, and timestamp. Use this to debug issues, view usage history, or find specific requests. Each log has an x-request-id for tracking. Supports filtering by status code, endpoint path, and model name. Call when user asks to see logs, requests, history, or wants to debug an issue.',
|
|
336
|
+
parameters: {
|
|
337
|
+
type: 'object',
|
|
338
|
+
properties: {
|
|
339
|
+
limit: {
|
|
340
|
+
type: 'number',
|
|
341
|
+
description: 'Number of logs to return (default: 20, max: 100)'
|
|
342
|
+
},
|
|
343
|
+
offset: {
|
|
344
|
+
type: 'number',
|
|
345
|
+
description: 'Pagination offset (default: 0)'
|
|
346
|
+
},
|
|
347
|
+
status: {
|
|
348
|
+
type: 'number',
|
|
349
|
+
description: 'Filter by HTTP status code (e.g., 200 for success, 400 for bad request, 500 for server error)'
|
|
350
|
+
},
|
|
351
|
+
endpoint: {
|
|
352
|
+
type: 'string',
|
|
353
|
+
description: 'Filter by endpoint path (partial match, e.g., "chat/completions", "models")'
|
|
354
|
+
},
|
|
355
|
+
model: {
|
|
356
|
+
type: 'string',
|
|
357
|
+
description: 'Filter by model name (partial match, e.g., "gpt-4", "claude", "gemini")'
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
required: []
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
type: 'function',
|
|
366
|
+
function: {
|
|
367
|
+
name: 'marketplace_request_logs_get',
|
|
368
|
+
description: 'Get detailed log entry by ID including full request/response bodies, headers, status codes, and timing information. Useful for debugging specific failed requests. The log ID can be found from request_logs_list or from the x-request-id response header. Sensitive data (API keys, auth tokens) are automatically redacted. Body content is truncated by default for readability - use show_full_body to see complete content.',
|
|
369
|
+
parameters: {
|
|
370
|
+
type: 'object',
|
|
371
|
+
properties: {
|
|
372
|
+
log_id: {
|
|
373
|
+
type: 'string',
|
|
374
|
+
description: 'Log ID (UUID) from request_logs_list or x-request-id header'
|
|
375
|
+
},
|
|
376
|
+
show_full_body: {
|
|
377
|
+
type: 'boolean',
|
|
378
|
+
description: 'Show full request/response bodies without truncation (default: false, truncates to 500 chars)'
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
required: ['log_id']
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
type: 'function',
|
|
387
|
+
function: {
|
|
388
|
+
name: 'marketplace_request_logs_stats',
|
|
389
|
+
description: 'Get aggregated statistics about user\'s API usage. Shows total requests, success rate, top models used, top endpoints, and daily request counts. Use this for usage analytics, monitoring, or understanding request patterns. No parameters required.',
|
|
390
|
+
parameters: {
|
|
391
|
+
type: 'object',
|
|
392
|
+
properties: {
|
|
393
|
+
start_date: {
|
|
394
|
+
type: 'string',
|
|
395
|
+
description: 'Start date in ISO 8601 format (default: 30 days ago)'
|
|
396
|
+
},
|
|
397
|
+
end_date: {
|
|
398
|
+
type: 'string',
|
|
399
|
+
description: 'End date in ISO 8601 format (default: now)'
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
required: []
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
type: 'function',
|
|
408
|
+
function: {
|
|
409
|
+
name: 'marketplace_vault_status',
|
|
410
|
+
description: 'Get comprehensive vault status showing LocalVault (persistent encrypted storage) and KeyVault (in-memory protection). Returns pre-formatted text with box-drawing characters. IMPORTANT: Display the output exactly as received - do not summarize or reformat. Use when user asks about vault, credentials, or security.',
|
|
411
|
+
parameters: {
|
|
412
|
+
type: 'object',
|
|
413
|
+
properties: {},
|
|
414
|
+
required: []
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
type: 'function',
|
|
420
|
+
function: {
|
|
421
|
+
name: 'marketplace_vault_localvault',
|
|
422
|
+
description: 'List all credentials in LocalVault (persistent encrypted storage at ~/.langmart/keystore.enc). Returns pre-formatted text. IMPORTANT: Display output exactly as received - do not summarize. Use when user asks about stored credentials.',
|
|
423
|
+
parameters: {
|
|
424
|
+
type: 'object',
|
|
425
|
+
properties: {},
|
|
426
|
+
required: []
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
type: 'function',
|
|
432
|
+
function: {
|
|
433
|
+
name: 'marketplace_vault_keyvault',
|
|
434
|
+
description: 'Show KeyVault status (in-memory chat protection that redacts API keys from LLM messages). Returns pre-formatted text. IMPORTANT: Display output exactly as received. Use when user asks about key protection or redaction.',
|
|
435
|
+
parameters: {
|
|
436
|
+
type: 'object',
|
|
437
|
+
properties: {},
|
|
438
|
+
required: []
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
type: 'function',
|
|
444
|
+
function: {
|
|
445
|
+
name: 'marketplace_connections_quota_manage',
|
|
446
|
+
description: 'Set or update sales quota limits for a connection. Quota applies to ALL models under the connection and tracks total consumption (owner usage + published model sales). Daily quota resets at midnight UTC, monthly resets on 1st. When quota exceeded, all models auto-disable. Can enable/disable enforcement. Use connection_id from marketplace_connections_list.',
|
|
447
|
+
parameters: {
|
|
448
|
+
type: 'object',
|
|
449
|
+
properties: {
|
|
450
|
+
connection_id: {
|
|
451
|
+
type: 'string',
|
|
452
|
+
description: 'Connection identifier - can be sequence number (1, 2, 3) or UUID'
|
|
453
|
+
},
|
|
454
|
+
max_daily_sales_usd: {
|
|
455
|
+
type: 'number',
|
|
456
|
+
description: 'Maximum daily sales in USD (e.g., 100 for $100/day). Omit or set to null for no daily limit.'
|
|
457
|
+
},
|
|
458
|
+
max_monthly_sales_usd: {
|
|
459
|
+
type: 'number',
|
|
460
|
+
description: 'Maximum monthly sales in USD (e.g., 1000 for $1000/month). Omit or set to null for no monthly limit.'
|
|
461
|
+
},
|
|
462
|
+
quota_enabled: {
|
|
463
|
+
type: 'boolean',
|
|
464
|
+
description: 'Enable quota enforcement (true = auto-disable models when exceeded, false = track only without disabling)'
|
|
465
|
+
}
|
|
466
|
+
},
|
|
467
|
+
required: ['connection_id', 'quota_enabled']
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
type: 'function',
|
|
473
|
+
function: {
|
|
474
|
+
name: 'marketplace_connections_quota_get',
|
|
475
|
+
description: 'Get current quota status and usage for a specific connection. Shows daily/monthly limits, current consumption, percentage used, quota exceeded status, affected models count, and usage history (last 30 days). Use connection_id from marketplace_connections_list.',
|
|
476
|
+
parameters: {
|
|
477
|
+
type: 'object',
|
|
478
|
+
properties: {
|
|
479
|
+
connection_id: {
|
|
480
|
+
type: 'string',
|
|
481
|
+
description: 'Connection identifier - can be sequence number (1, 2, 3) or UUID'
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
required: ['connection_id']
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
type: 'function',
|
|
490
|
+
function: {
|
|
491
|
+
name: 'marketplace_connections_quota_list',
|
|
492
|
+
description: 'List quota status for all connections owned by the user. Shows quota settings, current usage, and enforcement status for each connection. Useful for getting an overview of quota usage across all provider connections.',
|
|
493
|
+
parameters: {
|
|
494
|
+
type: 'object',
|
|
495
|
+
properties: {},
|
|
496
|
+
required: []
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
type: 'function',
|
|
502
|
+
function: {
|
|
503
|
+
name: 'marketplace_models_update_pricing',
|
|
504
|
+
description: 'Update pricing for one or multiple models using filter-based selection. Supports both single model updates and bulk operations.\n\n**TWO PRICING METHODS:**\n1. Exact Price: Set specific prices with input_tokens_per_1k and output_tokens_per_1k\n2. Percentage Adjustment: Increase/decrease current prices by percentage\n\n**TWO TARGET MODES:**\n1. Single Model: Provide model_id to update one model\n2. Bulk Mode: Provide filter (space-separated keywords) to update multiple models using AND-condition matching\n\n**PERCENTAGE EXAMPLES:**\n- "increase all groq models by 10%" → filter="groq", increase_percentage=10\n- "decrease all llama models by 15%" → filter="llama", decrease_percentage=15\n- "increase my published models by 20%" → filter="", source_access_level="public", increase_percentage=20\n- "increase all image models by 10%" → filter="image", increase_percentage=10\n\n**BULK UPDATE WORKFLOW:**\n- Default (auto_confirm: false): Shows matched models, asks for confirmation\n- With auto_confirm: true: Immediately updates all matched models\n- Use source_access_level to pre-filter by publication status\n\n**ENHANCED FILTER LOGIC:**\n- Space-separated keywords use AND condition (all must match)\n- Matches against model_id, model_name, provider name, AND capabilities\n- Capability keywords: image, vision, tool, reasoning, audio, video, embedding\n- Example: "groq llama" matches models with BOTH keywords\n- Example: "image" matches all models with vision or image generation capability\n- Example: "google tool" matches Google models with tool use capability\n\nOnly model owners can update pricing. Use marketplace_models_list to find model IDs.',
|
|
505
|
+
parameters: {
|
|
506
|
+
type: 'object',
|
|
507
|
+
properties: {
|
|
508
|
+
model_id: {
|
|
509
|
+
type: 'string',
|
|
510
|
+
description: 'Single model UUID from marketplace_models_list (the id field, NOT model_id field). Mutually exclusive with filter. Use for single model updates.'
|
|
511
|
+
},
|
|
512
|
+
filter: {
|
|
513
|
+
type: 'string',
|
|
514
|
+
description: 'Space-separated keywords for bulk operation (e.g., "groq", "llama 70b", "gemini flash", "image", "vision", "tool"). Uses AND condition - all keywords must match. Mutually exclusive with model_id. Matches against model_id, model_name, provider name, and capabilities (vision, image, reasoning, tool, audio, video, embedding).'
|
|
515
|
+
},
|
|
516
|
+
source_access_level: {
|
|
517
|
+
type: 'string',
|
|
518
|
+
enum: ['public', 'private', 'disabled'],
|
|
519
|
+
description: 'Pre-filter models by current publication status before applying keyword filter. Useful for "update pricing for all my published models".'
|
|
520
|
+
},
|
|
521
|
+
auto_confirm: {
|
|
522
|
+
type: 'boolean',
|
|
523
|
+
description: 'For bulk operations: false (default) = show matches and ask confirmation, true = immediately update all matches. Recommended: use false first to preview, then true to confirm.'
|
|
524
|
+
},
|
|
525
|
+
input_tokens_per_1k: {
|
|
526
|
+
type: 'number',
|
|
527
|
+
description: 'EXACT PRICE MODE: Price per 1000 input tokens in USD (e.g., 0.0001 for $0.0001/1K tokens). Mutually exclusive with percentage adjustments.'
|
|
528
|
+
},
|
|
529
|
+
output_tokens_per_1k: {
|
|
530
|
+
type: 'number',
|
|
531
|
+
description: 'EXACT PRICE MODE: Price per 1000 output tokens in USD (e.g., 0.0003 for $0.0003/1K tokens). Mutually exclusive with percentage adjustments.'
|
|
532
|
+
},
|
|
533
|
+
cached_input_tokens_per_1k: {
|
|
534
|
+
type: 'number',
|
|
535
|
+
description: 'EXACT PRICE MODE: Optional - Price per 1000 cached input tokens in USD (for models with caching)'
|
|
536
|
+
},
|
|
537
|
+
reasoning_tokens_per_1k: {
|
|
538
|
+
type: 'number',
|
|
539
|
+
description: 'EXACT PRICE MODE: Optional - Price per 1000 reasoning tokens in USD (for reasoning models like o1)'
|
|
540
|
+
},
|
|
541
|
+
increase_percentage: {
|
|
542
|
+
type: 'number',
|
|
543
|
+
description: 'PERCENTAGE MODE: Increase current prices by this percentage (e.g., 10 for 10% increase). Mutually exclusive with decrease_percentage and exact prices. Current price × (1 + percentage/100). Example: $1.00 with 10% increase = $1.10'
|
|
544
|
+
},
|
|
545
|
+
decrease_percentage: {
|
|
546
|
+
type: 'number',
|
|
547
|
+
description: 'PERCENTAGE MODE: Decrease current prices by this percentage (e.g., 15 for 15% decrease). Mutually exclusive with increase_percentage and exact prices. Current price × (1 - percentage/100). Example: $1.00 with 15% decrease = $0.85'
|
|
548
|
+
}
|
|
549
|
+
},
|
|
550
|
+
required: []
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
},
|
|
554
|
+
{
|
|
555
|
+
type: 'function',
|
|
556
|
+
function: {
|
|
557
|
+
name: 'marketplace_models_publish',
|
|
558
|
+
description: 'Publish or unpublish models by setting access level. Supports BOTH single model (by UUID) and bulk operations (by filter).\n\n**Two modes:**\n1. Single model: Provide `model_id` (UUID from marketplace_models_list)\n2. Bulk operation: Provide `filter` (space-separated keywords, same as UI filter)\n\n**IMPORTANT - Automatic filtering logic:**\n- When unpublishing (`access_level: "private"`): Automatically searches only your PUBLIC models\n- When publishing (`access_level: "public"`): Automatically searches only your PRIVATE models\n- When disabling (`access_level: "disabled"`): Searches all your models\n\n**Filter behavior:**\n- Space-separated keywords use AND condition (all must match)\n- Matches against model_id and model_name\n- Example: "groq llama" matches models containing BOTH "groq" AND "llama"\n\n**Confirmation:**\n- `auto_confirm: false` (default): Shows matched models, asks user to confirm\n- `auto_confirm: true`: Immediately updates all matched models\n\n**Access levels:**\n- "public": Available to all users in marketplace (publish)\n- "private": Only owner can use (delist/unpublish)\n- "disabled": Not available to anyone',
|
|
559
|
+
parameters: {
|
|
560
|
+
type: 'object',
|
|
561
|
+
properties: {
|
|
562
|
+
model_id: {
|
|
563
|
+
type: 'string',
|
|
564
|
+
description: 'Single model UUID from marketplace_models_list (the "id" field). Use this for single model update. Mutually exclusive with filter. Format: "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"'
|
|
565
|
+
},
|
|
566
|
+
filter: {
|
|
567
|
+
type: 'string',
|
|
568
|
+
description: 'Filter keywords for bulk operation (space-separated, AND condition). Example: "groq llama" matches models with BOTH keywords. Mutually exclusive with model_id. IMPORTANT: Tool automatically filters by correct source access level (public models when unpublishing, private models when publishing).'
|
|
569
|
+
},
|
|
570
|
+
access_level: {
|
|
571
|
+
type: 'string',
|
|
572
|
+
description: 'Target access level: "public" (publish to marketplace), "private" (delist/unpublish), "disabled" (completely unavailable)',
|
|
573
|
+
enum: ['public', 'private', 'disabled']
|
|
574
|
+
},
|
|
575
|
+
auto_confirm: {
|
|
576
|
+
type: 'boolean',
|
|
577
|
+
description: 'If true, skip confirmation and immediately update all matched models. If false (default), show matched models and ask user to confirm first. Default: false',
|
|
578
|
+
default: false
|
|
579
|
+
},
|
|
580
|
+
source_access_level: {
|
|
581
|
+
type: 'string',
|
|
582
|
+
description: 'OPTIONAL: Override automatic source filtering. Leave empty to use automatic logic (recommended). Only use if you need to override default behavior.',
|
|
583
|
+
enum: ['public', 'private', 'disabled']
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
required: ['access_level']
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
},
|
|
590
|
+
// ========================================================================
|
|
591
|
+
// WORKFLOW QUERY TOOLS - For agents to query workflow execution state
|
|
592
|
+
// ========================================================================
|
|
593
|
+
{
|
|
594
|
+
type: 'function',
|
|
595
|
+
function: {
|
|
596
|
+
name: 'workflow_get_status',
|
|
597
|
+
description: 'Get the overall status of a workflow execution by trace ID. Returns the workflow name, overall status (pending/running/completed/failed), start time, completion time, and progress summary. Use this to check if the workflow is still running or has completed.',
|
|
598
|
+
parameters: {
|
|
599
|
+
type: 'object',
|
|
600
|
+
properties: {
|
|
601
|
+
trace_id: {
|
|
602
|
+
type: 'string',
|
|
603
|
+
description: 'The trace ID of the workflow execution. This is provided in your system prompt as "Trace ID".'
|
|
604
|
+
}
|
|
605
|
+
},
|
|
606
|
+
required: ['trace_id']
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
type: 'function',
|
|
612
|
+
function: {
|
|
613
|
+
name: 'workflow_list_instances',
|
|
614
|
+
description: 'List all agent instances in a workflow execution. Shows each agent\'s name, role (START/AGENT/END), execution order, status, and any error messages. Use this to see what other agents are in your workflow and their current status.',
|
|
615
|
+
parameters: {
|
|
616
|
+
type: 'object',
|
|
617
|
+
properties: {
|
|
618
|
+
trace_id: {
|
|
619
|
+
type: 'string',
|
|
620
|
+
description: 'The trace ID of the workflow execution. This is provided in your system prompt as "Trace ID".'
|
|
621
|
+
}
|
|
622
|
+
},
|
|
623
|
+
required: ['trace_id']
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
},
|
|
627
|
+
{
|
|
628
|
+
type: 'function',
|
|
629
|
+
function: {
|
|
630
|
+
name: 'workflow_get_instance_output',
|
|
631
|
+
description: 'Get the output from a specific agent instance in the workflow. Returns the agent\'s response/output from its most recent session. Use this to retrieve results from agents that have already completed.',
|
|
632
|
+
parameters: {
|
|
633
|
+
type: 'object',
|
|
634
|
+
properties: {
|
|
635
|
+
instance_id: {
|
|
636
|
+
type: 'string',
|
|
637
|
+
description: 'The instance ID of the agent. Get instance IDs from workflow_list_instances.'
|
|
638
|
+
}
|
|
639
|
+
},
|
|
640
|
+
required: ['instance_id']
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
];
|
|
645
|
+
}
|
|
646
|
+
/**
|
|
647
|
+
* Format tools for AI system prompt
|
|
648
|
+
*/
|
|
649
|
+
formatToolsForPrompt() {
|
|
650
|
+
const tools = this.getTools();
|
|
651
|
+
const formatTool = (tool) => {
|
|
652
|
+
const func = tool.function;
|
|
653
|
+
const params = func.parameters?.properties || {};
|
|
654
|
+
const required = func.parameters?.required || [];
|
|
655
|
+
const paramDescriptions = Object.entries(params).map(([key, value]) => {
|
|
656
|
+
const isRequired = required.includes(key);
|
|
657
|
+
const desc = value.description || 'No description';
|
|
658
|
+
return ` ${key}${isRequired ? ' (required)' : ''}: ${desc}`;
|
|
659
|
+
}).join('\n');
|
|
660
|
+
return paramDescriptions
|
|
661
|
+
? ` - ${func.name}: ${func.description}\n${paramDescriptions}`
|
|
662
|
+
: ` - ${func.name}: ${func.description}`;
|
|
663
|
+
};
|
|
664
|
+
return tools.map(formatTool).join('\n\n');
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Execute a marketplace tool
|
|
668
|
+
*/
|
|
669
|
+
async executeTool(toolName, args) {
|
|
670
|
+
try {
|
|
671
|
+
switch (toolName) {
|
|
672
|
+
case 'marketplace_connections_list':
|
|
673
|
+
return await this.listConnections();
|
|
674
|
+
case 'marketplace_connections_add':
|
|
675
|
+
return await this.addConnection(args);
|
|
676
|
+
case 'marketplace_connections_remove':
|
|
677
|
+
return await this.removeConnection(args.connection_id);
|
|
678
|
+
case 'marketplace_connections_test':
|
|
679
|
+
return await this.testConnection(args.connection_id);
|
|
680
|
+
case 'marketplace_connections_update_key':
|
|
681
|
+
return await this.updateConnectionKey(args.connection_id, args.api_key);
|
|
682
|
+
case 'marketplace_providers_list':
|
|
683
|
+
return await this.listProviders();
|
|
684
|
+
case 'marketplace_models_discover':
|
|
685
|
+
return await this.discoverModels(args);
|
|
686
|
+
case 'marketplace_models_list':
|
|
687
|
+
return await this.listModels(args);
|
|
688
|
+
case 'marketplace_models_update_capabilities':
|
|
689
|
+
return await this.updateModelCapabilities(args);
|
|
690
|
+
case 'marketplace_models_get':
|
|
691
|
+
return await this.getModelInfo(args.model_id);
|
|
692
|
+
case 'marketplace_request_logs_list':
|
|
693
|
+
return await this.listRequestLogs(args);
|
|
694
|
+
case 'marketplace_request_logs_get':
|
|
695
|
+
return await this.getRequestLog(args.log_id, args.show_full_body);
|
|
696
|
+
case 'marketplace_request_logs_stats':
|
|
697
|
+
return await this.getRequestLogStats(args);
|
|
698
|
+
case 'marketplace_vault_status':
|
|
699
|
+
return await this.getVaultStatus();
|
|
700
|
+
case 'marketplace_vault_localvault':
|
|
701
|
+
return await this.getLocalVaultList();
|
|
702
|
+
case 'marketplace_vault_keyvault':
|
|
703
|
+
return await this.getKeyVaultStatus();
|
|
704
|
+
case 'marketplace_connections_quota_manage':
|
|
705
|
+
return await this.manageConnectionQuota(args);
|
|
706
|
+
case 'marketplace_connections_quota_get':
|
|
707
|
+
return await this.getConnectionQuota(args.connection_id);
|
|
708
|
+
case 'marketplace_connections_quota_list':
|
|
709
|
+
return await this.listConnectionQuotas();
|
|
710
|
+
case 'marketplace_models_update_pricing':
|
|
711
|
+
return await this.updateModelPricing(args);
|
|
712
|
+
case 'marketplace_models_publish':
|
|
713
|
+
return await this.publishModel(args);
|
|
714
|
+
// Workflow query tools
|
|
715
|
+
case 'workflow_get_status':
|
|
716
|
+
return await this.getWorkflowStatus(args.trace_id);
|
|
717
|
+
case 'workflow_list_instances':
|
|
718
|
+
return await this.listWorkflowInstances(args.trace_id);
|
|
719
|
+
case 'workflow_get_instance_output':
|
|
720
|
+
return await this.getInstanceOutput(args.instance_id);
|
|
721
|
+
default:
|
|
722
|
+
return {
|
|
723
|
+
success: false,
|
|
724
|
+
error: `Unknown tool: ${toolName}`
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
catch (error) {
|
|
729
|
+
return {
|
|
730
|
+
success: false,
|
|
731
|
+
error: error.message
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
// ========================================================================
|
|
736
|
+
// HELPER METHODS
|
|
737
|
+
// ========================================================================
|
|
738
|
+
/**
|
|
739
|
+
* Resolve connection identifier to UUID
|
|
740
|
+
* Accepts either:
|
|
741
|
+
* - Sequence number (1, 2, 3, etc.) - looks up in connectionMap
|
|
742
|
+
* - UUID string - returns as-is
|
|
743
|
+
*/
|
|
744
|
+
resolveConnectionId(identifier) {
|
|
745
|
+
// If it's a number, look up in the map
|
|
746
|
+
if (typeof identifier === 'number') {
|
|
747
|
+
const uuid = this.connectionMap.get(identifier);
|
|
748
|
+
if (!uuid) {
|
|
749
|
+
throw new Error(`Connection #${identifier} not found. Run 'connections' first to see available connections.`);
|
|
750
|
+
}
|
|
751
|
+
return uuid;
|
|
752
|
+
}
|
|
753
|
+
// If it's a string that looks like a number, parse and look up
|
|
754
|
+
const numMatch = identifier.match(/^#?(\d+)$/);
|
|
755
|
+
if (numMatch) {
|
|
756
|
+
const seqNum = parseInt(numMatch[1]);
|
|
757
|
+
const uuid = this.connectionMap.get(seqNum);
|
|
758
|
+
if (!uuid) {
|
|
759
|
+
throw new Error(`Connection #${seqNum} not found. Run 'connections' first to see available connections.`);
|
|
760
|
+
}
|
|
761
|
+
return uuid;
|
|
762
|
+
}
|
|
763
|
+
// Otherwise assume it's a UUID
|
|
764
|
+
return identifier;
|
|
765
|
+
}
|
|
766
|
+
// ========================================================================
|
|
767
|
+
// TOOL IMPLEMENTATIONS
|
|
768
|
+
// ========================================================================
|
|
769
|
+
async listConnections() {
|
|
770
|
+
try {
|
|
771
|
+
(0, debug_utils_1.debugLog)(`[MarketplaceTools] Listing connections from: ${this.marketplaceUrl}/api/connections`);
|
|
772
|
+
const response = await this.client.get('/api/connections');
|
|
773
|
+
(0, debug_utils_1.debugLog)(`[MarketplaceTools] List response status: ${response.status}`);
|
|
774
|
+
(0, debug_utils_1.debugLog)(`[MarketplaceTools] List response data keys:`, Object.keys(response.data || {}));
|
|
775
|
+
const connections = response.data.connections || [];
|
|
776
|
+
if (connections.length === 0) {
|
|
777
|
+
return {
|
|
778
|
+
success: true,
|
|
779
|
+
output: 'No connections found. Use marketplace.connections.add to create one.'
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
// Sort by created_at to maintain consistent ordering
|
|
783
|
+
connections.sort((a, b) => {
|
|
784
|
+
const dateA = new Date(a.created_at || 0).getTime();
|
|
785
|
+
const dateB = new Date(b.created_at || 0).getTime();
|
|
786
|
+
return dateA - dateB;
|
|
787
|
+
});
|
|
788
|
+
// Clear and rebuild connection map
|
|
789
|
+
this.connectionMap.clear();
|
|
790
|
+
connections.forEach((conn, idx) => {
|
|
791
|
+
this.connectionMap.set(idx + 1, conn.id);
|
|
792
|
+
});
|
|
793
|
+
// Build output with sequence IDs and UUID mapping
|
|
794
|
+
const output = connections.map((conn, idx) => {
|
|
795
|
+
const seqId = idx + 1;
|
|
796
|
+
const gatewayLabel = conn.gateway_type === 2 ? 'Managed' : 'Self-hosted';
|
|
797
|
+
return `[${seqId}] ${conn.provider?.name || 'Unknown'} - ${conn.endpoint_type} (${gatewayLabel})\n` +
|
|
798
|
+
` Status: ${conn.status || 'active'}\n` +
|
|
799
|
+
` Endpoint: ${conn.endpoint_url}\n` +
|
|
800
|
+
` Connection ID: ${conn.id}`;
|
|
801
|
+
}).join('\n\n');
|
|
802
|
+
return {
|
|
803
|
+
success: true,
|
|
804
|
+
output: `Found ${connections.length} connection(s):\n\n${output}\n\n` +
|
|
805
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` +
|
|
806
|
+
`📋 Available Actions:\n` +
|
|
807
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n` +
|
|
808
|
+
` ➕ Add a New Connection\n` +
|
|
809
|
+
` Create connection to LLM provider (OpenAI, Groq, etc.)\n\n` +
|
|
810
|
+
` 🔍 Discover Models\n` +
|
|
811
|
+
` Fetch available models from provider API and add to marketplace\n\n` +
|
|
812
|
+
` 🔑 Update API Key\n` +
|
|
813
|
+
` Change API key for existing connection (Marketplace-Managed only)\n\n` +
|
|
814
|
+
` 📊 Manage Quota\n` +
|
|
815
|
+
` Set usage limits for connection (daily/monthly spending caps)\n\n` +
|
|
816
|
+
` 📈 View Quota Status\n` +
|
|
817
|
+
` Get detailed quota usage and history for a connection\n\n` +
|
|
818
|
+
` 📋 List All Quotas\n` +
|
|
819
|
+
` View quota status for all connections at once\n\n` +
|
|
820
|
+
` 🧪 Test Connection\n` +
|
|
821
|
+
` Verify connection works by discovering models\n\n` +
|
|
822
|
+
` 🗑️ Delete Connection\n` +
|
|
823
|
+
` Remove connection and all its models from marketplace\n\n` +
|
|
824
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
catch (error) {
|
|
828
|
+
console.error(`[MarketplaceTools] List connections error:`);
|
|
829
|
+
console.error(`[MarketplaceTools] Error code:`, error.code);
|
|
830
|
+
console.error(`[MarketplaceTools] Error message:`, error.message);
|
|
831
|
+
console.error(`[MarketplaceTools] Response status:`, error.response?.status);
|
|
832
|
+
console.error(`[MarketplaceTools] Response data:`, error.response?.data);
|
|
833
|
+
// Socket hang up / connection reset errors
|
|
834
|
+
if (error.code === 'ECONNRESET' || error.code === 'EPIPE' ||
|
|
835
|
+
error.message?.toLowerCase().includes('socket hang up')) {
|
|
836
|
+
return {
|
|
837
|
+
success: false,
|
|
838
|
+
error: `🔌 Connection lost while listing connections\n\n` +
|
|
839
|
+
`The marketplace API connection was interrupted.\n` +
|
|
840
|
+
`This may happen if:\n` +
|
|
841
|
+
` • The gateway is restarting\n` +
|
|
842
|
+
` • Network is unstable\n` +
|
|
843
|
+
` • Request timed out\n\n` +
|
|
844
|
+
`💡 Try again in a few seconds.\n\n` +
|
|
845
|
+
`Error details: ${error.message}\n` +
|
|
846
|
+
`Error code: ${error.code || 'unknown'}`
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
return {
|
|
850
|
+
success: false,
|
|
851
|
+
error: error.response?.data?.error?.message || error.message
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
async addConnection(args) {
|
|
856
|
+
try {
|
|
857
|
+
const response = await this.client.post('/api/connections', args);
|
|
858
|
+
const conn = response.data.connection;
|
|
859
|
+
// Build detailed connection info
|
|
860
|
+
const gatewayTypeLabel = conn.gateway_type === 2 ? 'Marketplace-Managed Compute' : 'Self-Hosted Compute';
|
|
861
|
+
return {
|
|
862
|
+
success: true,
|
|
863
|
+
output: `✅ Connection added successfully!\n\n` +
|
|
864
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` +
|
|
865
|
+
`📋 Connection Details:\n` +
|
|
866
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` +
|
|
867
|
+
` Name: ${conn.name || args.name}\n` +
|
|
868
|
+
` Provider: ${conn.provider?.name || args.provider_name}\n` +
|
|
869
|
+
` Endpoint: ${conn.endpoint_url}\n` +
|
|
870
|
+
` Endpoint Type: ${conn.endpoint_type}\n` +
|
|
871
|
+
` Gateway Type: ${gatewayTypeLabel}\n` +
|
|
872
|
+
` Status: ${conn.status || 'active'}\n` +
|
|
873
|
+
` Connection ID: ${conn.id}\n` +
|
|
874
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n` +
|
|
875
|
+
`💡 What would you like to do next?\n\n` +
|
|
876
|
+
` 1️⃣ Test this connection\n` +
|
|
877
|
+
` Use: marketplace.connections.test\n\n` +
|
|
878
|
+
` 2️⃣ Discover available models\n` +
|
|
879
|
+
` Use: marketplace.models.discover\n\n` +
|
|
880
|
+
` 3️⃣ Update the API key\n` +
|
|
881
|
+
` Use: marketplace.connections.updateKey\n\n` +
|
|
882
|
+
` 4️⃣ Remove this connection\n` +
|
|
883
|
+
` Use: marketplace.connections.remove\n\n` +
|
|
884
|
+
`IMPORTANT: Show this output to the user and ask what they'd like to do next. DO NOT automatically call other tools.`
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
catch (error) {
|
|
888
|
+
return {
|
|
889
|
+
success: false,
|
|
890
|
+
error: error.response?.data?.error?.message || error.message
|
|
891
|
+
};
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
async removeConnection(connectionId) {
|
|
895
|
+
try {
|
|
896
|
+
// Resolve sequence number to UUID if needed
|
|
897
|
+
const resolvedId = this.resolveConnectionId(connectionId);
|
|
898
|
+
await this.client.delete(`/api/connections/${resolvedId}`);
|
|
899
|
+
return {
|
|
900
|
+
success: true,
|
|
901
|
+
output: `✅ Connection removed successfully (ID: ${resolvedId})`
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
catch (error) {
|
|
905
|
+
return {
|
|
906
|
+
success: false,
|
|
907
|
+
error: error.response?.data?.error?.message || error.message
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
async testConnection(connectionId) {
|
|
912
|
+
try {
|
|
913
|
+
// Resolve sequence number to UUID if needed
|
|
914
|
+
const resolvedId = this.resolveConnectionId(connectionId);
|
|
915
|
+
const testUrl = `/api/connections/${resolvedId}/test`;
|
|
916
|
+
(0, debug_utils_1.debugLog)(`[MarketplaceTools] Testing connection: ${connectionId} (resolved to: ${resolvedId})`);
|
|
917
|
+
(0, debug_utils_1.debugLog)(`[MarketplaceTools] Full URL: ${this.marketplaceUrl}${testUrl}`);
|
|
918
|
+
const response = await this.client.post(testUrl);
|
|
919
|
+
(0, debug_utils_1.debugLog)(`[MarketplaceTools] Response status: ${response.status}`);
|
|
920
|
+
(0, debug_utils_1.debugLog)(`[MarketplaceTools] Response data:`, response.data);
|
|
921
|
+
const data = response.data;
|
|
922
|
+
// Check for non-2xx responses (axios doesn't throw for < 500)
|
|
923
|
+
if (response.status >= 400 || data.success === false) {
|
|
924
|
+
// Handle 404 - Connection not found
|
|
925
|
+
if (response.status === 404) {
|
|
926
|
+
return {
|
|
927
|
+
success: false,
|
|
928
|
+
error: `❌ Connection not found\n\n` +
|
|
929
|
+
`Connection: ${connectionId} (resolved to: ${resolvedId})\n\n` +
|
|
930
|
+
`💡 Use marketplace.connections.list to see available connections.`
|
|
931
|
+
};
|
|
932
|
+
}
|
|
933
|
+
// Handle other 4xx errors using the standardized error format
|
|
934
|
+
const errorData = data.error || data;
|
|
935
|
+
if (errorData.code === 'rate_limit_exceeded') {
|
|
936
|
+
return {
|
|
937
|
+
success: false,
|
|
938
|
+
error: `⚠️ ${errorData.error || 'Rate limit exceeded'}\n\n` +
|
|
939
|
+
`Provider: ${errorData.provider}\n` +
|
|
940
|
+
`Status: ${errorData.status} (Rate Limited)\n` +
|
|
941
|
+
(errorData.details ? `Details: ${errorData.details}\n` : '') +
|
|
942
|
+
`\n💡 ${errorData.suggestion || 'Please wait a few minutes before trying again.'}\n` +
|
|
943
|
+
(errorData.retry_after ? ` Retry after: ${errorData.retry_after}\n` : '')
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
if (errorData.code === 'auth_failed') {
|
|
947
|
+
return {
|
|
948
|
+
success: false,
|
|
949
|
+
error: `🔒 ${errorData.error || 'Authentication failed'}\n\n` +
|
|
950
|
+
`Provider: ${errorData.provider}\n` +
|
|
951
|
+
`Status: ${errorData.status} (Authentication Failed)\n` +
|
|
952
|
+
(errorData.details ? `Details: ${errorData.details}\n` : '') +
|
|
953
|
+
`\n💡 Check your API key and update it if needed using:\n` +
|
|
954
|
+
` marketplace.connections.updateKey`
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
if (errorData.code === 'type3_gateway_unavailable') {
|
|
958
|
+
return {
|
|
959
|
+
success: false,
|
|
960
|
+
error: `🔧 ${errorData.error || 'Type 3 gateway not available'}\n\n` +
|
|
961
|
+
(errorData.details ? `${errorData.details}\n` : '') +
|
|
962
|
+
`\n💡 Start your local Self-Hosted Gateway to test self-hosted connections.`
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
if (errorData.code === 'connection_failed') {
|
|
966
|
+
return {
|
|
967
|
+
success: false,
|
|
968
|
+
error: `🔌 ${errorData.error || 'Connection failed'}\n\n` +
|
|
969
|
+
(errorData.details ? `Details: ${errorData.details}\n` : '') +
|
|
970
|
+
`\n💡 Verify the endpoint URL is correct and the provider is accessible.`
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
// Generic 4xx error
|
|
974
|
+
return {
|
|
975
|
+
success: false,
|
|
976
|
+
error: `❌ Connection test failed\n\n` +
|
|
977
|
+
`Status: ${response.status}\n` +
|
|
978
|
+
`${errorData.error || errorData.message || 'Unknown error'}\n` +
|
|
979
|
+
(errorData.details ? `\nDetails: ${errorData.details}` : '')
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
// Success case
|
|
983
|
+
return {
|
|
984
|
+
success: true,
|
|
985
|
+
output: `✅ Connection test successful!\n\n` +
|
|
986
|
+
`Provider: ${data.provider}\n` +
|
|
987
|
+
`Gateway Type: ${data.gateway_type || 'Unknown'}\n` +
|
|
988
|
+
`Status: ${data.status || 'OK'}\n` +
|
|
989
|
+
(data.models_available ? `Models Available: ${data.models_available}\n` : '') +
|
|
990
|
+
(data.gateway_type === 3 ? `\n(Tested via your local Self-Hosted Gateway instance)` : '')
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
catch (error) {
|
|
994
|
+
console.error(`[MarketplaceTools] Test connection error for ${connectionId}:`);
|
|
995
|
+
console.error(`[MarketplaceTools] Error code:`, error.code);
|
|
996
|
+
console.error(`[MarketplaceTools] Error message:`, error.message);
|
|
997
|
+
console.error(`[MarketplaceTools] Response status:`, error.response?.status);
|
|
998
|
+
console.error(`[MarketplaceTools] Response data:`, error.response?.data);
|
|
999
|
+
// Socket hang up / connection reset errors
|
|
1000
|
+
if (error.code === 'ECONNRESET' || error.code === 'EPIPE' ||
|
|
1001
|
+
error.message?.toLowerCase().includes('socket hang up')) {
|
|
1002
|
+
return {
|
|
1003
|
+
success: false,
|
|
1004
|
+
error: `🔌 Connection lost during test\n\n` +
|
|
1005
|
+
`The marketplace API connection was interrupted.\n` +
|
|
1006
|
+
`This may happen if:\n` +
|
|
1007
|
+
` • The gateway is restarting\n` +
|
|
1008
|
+
` • Network is unstable\n` +
|
|
1009
|
+
` • Request timed out\n\n` +
|
|
1010
|
+
`💡 Try again in a few seconds.\n\n` +
|
|
1011
|
+
`Error details: ${error.message}\n` +
|
|
1012
|
+
`Error code: ${error.code || 'unknown'}`
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
// ECONNREFUSED - Gateway not running
|
|
1016
|
+
if (error.code === 'ECONNREFUSED') {
|
|
1017
|
+
return {
|
|
1018
|
+
success: false,
|
|
1019
|
+
error: `🔌 Cannot connect to marketplace API\n\n` +
|
|
1020
|
+
`The marketplace API is not reachable at ${this.marketplaceUrl}\n\n` +
|
|
1021
|
+
`💡 Ensure the LangMart Marketplace is running on the correct port.`
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
// Generic error fallback
|
|
1025
|
+
const errorData = error.response?.data;
|
|
1026
|
+
return {
|
|
1027
|
+
success: false,
|
|
1028
|
+
error: `❌ Connection test failed\n\n` +
|
|
1029
|
+
(errorData?.error || error.message) +
|
|
1030
|
+
(errorData?.details ? `\n\nDetails: ${errorData.details}` : '')
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
async updateConnectionKey(connectionId, apiKey) {
|
|
1035
|
+
try {
|
|
1036
|
+
// Resolve sequence number to UUID if needed
|
|
1037
|
+
const resolvedId = this.resolveConnectionId(connectionId);
|
|
1038
|
+
await this.client.post(`/api/connections/${resolvedId}/update-key`, { api_key: apiKey });
|
|
1039
|
+
return {
|
|
1040
|
+
success: true,
|
|
1041
|
+
output: `✅ API key updated successfully for connection ${connectionId} (ID: ${resolvedId})`
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
catch (error) {
|
|
1045
|
+
return {
|
|
1046
|
+
success: false,
|
|
1047
|
+
error: error.response?.data?.error?.message || error.message
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
async listProviders() {
|
|
1052
|
+
try {
|
|
1053
|
+
const response = await this.client.get('/api/public/providers');
|
|
1054
|
+
const providers = response.data.providers || [];
|
|
1055
|
+
if (providers.length === 0) {
|
|
1056
|
+
return {
|
|
1057
|
+
success: true,
|
|
1058
|
+
output: 'No providers found in marketplace.'
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
const output = providers.map((p, idx) => `${idx + 1}. ${p.name} (${p.key})\n` +
|
|
1062
|
+
` Base URL: ${p.base_url}\n` +
|
|
1063
|
+
` Active: ${p.is_active ? 'Yes' : 'No'}`).join('\n\n');
|
|
1064
|
+
return {
|
|
1065
|
+
success: true,
|
|
1066
|
+
output: `Found ${providers.length} provider(s):\n\n${output}\n\n` +
|
|
1067
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` +
|
|
1068
|
+
`💡 Would you like to add a connection?\n` +
|
|
1069
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n` +
|
|
1070
|
+
`To add a connection to any provider, you'll need:\n\n` +
|
|
1071
|
+
` 📋 Required Information:\n` +
|
|
1072
|
+
` • Provider name (e.g., "groq", "openai", "anthropic")\n` +
|
|
1073
|
+
` • API key from the provider\n` +
|
|
1074
|
+
` • Display name for your connection\n` +
|
|
1075
|
+
` • Gateway type:\n` +
|
|
1076
|
+
` - Type 2: Cloud providers (OpenAI, Groq, Anthropic, Google)\n` +
|
|
1077
|
+
` - Type 3: Self-hosted (Ollama, local LLMs)\n\n` +
|
|
1078
|
+
` 🔧 Example:\n` +
|
|
1079
|
+
` "Add a Groq connection with API key gsk_..."\n\n` +
|
|
1080
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n` +
|
|
1081
|
+
`IMPORTANT: Ask the user if they'd like to add a connection to any of these providers and wait for their response.`
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
catch (error) {
|
|
1085
|
+
return {
|
|
1086
|
+
success: false,
|
|
1087
|
+
error: error.response?.data?.error?.message || error.message
|
|
1088
|
+
};
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
async discoverModels(args) {
|
|
1092
|
+
try {
|
|
1093
|
+
const response = await this.client.post('/api/models/discover', args);
|
|
1094
|
+
const data = response.data;
|
|
1095
|
+
return {
|
|
1096
|
+
success: true,
|
|
1097
|
+
output: `✅ Model discovery complete!\n\n` +
|
|
1098
|
+
`Provider: ${data.provider}\n` +
|
|
1099
|
+
`Discovered: ${data.discovered} models\n` +
|
|
1100
|
+
`Stored: ${data.stored} models\n` +
|
|
1101
|
+
`Failed: ${data.failed} models\n` +
|
|
1102
|
+
`Source: ${data.source}\n\n` +
|
|
1103
|
+
`Use marketplace.models.list to see all available models.`
|
|
1104
|
+
};
|
|
1105
|
+
}
|
|
1106
|
+
catch (error) {
|
|
1107
|
+
return {
|
|
1108
|
+
success: false,
|
|
1109
|
+
error: error.response?.data?.error?.message || error.message
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
async listModels(args = {}) {
|
|
1114
|
+
try {
|
|
1115
|
+
// Build query parameters for server-side filtering
|
|
1116
|
+
const params = {};
|
|
1117
|
+
if (args.keyword) {
|
|
1118
|
+
params.keyword = args.keyword;
|
|
1119
|
+
}
|
|
1120
|
+
if (args.access_level) {
|
|
1121
|
+
params.access_level = args.access_level;
|
|
1122
|
+
}
|
|
1123
|
+
const response = await this.client.get('/api/models', { params });
|
|
1124
|
+
let models = response.data.models || [];
|
|
1125
|
+
if (models.length === 0) {
|
|
1126
|
+
return {
|
|
1127
|
+
success: true,
|
|
1128
|
+
output: 'No models found. Use marketplace.models.discover to discover models from your connections.'
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
// Pagination parameters
|
|
1132
|
+
const limit = Math.min(Math.max(args.limit || 10, 1), 100); // Default 10, max 100
|
|
1133
|
+
const offset = Math.max(args.offset || 0, 0);
|
|
1134
|
+
// Build active filters list for display
|
|
1135
|
+
const activeFilters = [];
|
|
1136
|
+
// Apply filters
|
|
1137
|
+
if (args.keyword) {
|
|
1138
|
+
// Split by spaces and apply AND logic - all keywords must match
|
|
1139
|
+
// IMPORTANT: This matches the EXACT same logic as UI tab filter
|
|
1140
|
+
const keywords = args.keyword.trim().split(/\s+/).filter((k) => k.length > 0).map((k) => k.toLowerCase());
|
|
1141
|
+
models = models.filter((m) => {
|
|
1142
|
+
const modelId = (m.model_id || '').toLowerCase();
|
|
1143
|
+
const modelName = (m.model_name || '').toLowerCase();
|
|
1144
|
+
// ALL keywords must match (AND logic) - same as UI filter
|
|
1145
|
+
// Matches against: model_id OR model_name only
|
|
1146
|
+
return keywords.every((keyword) => {
|
|
1147
|
+
return modelId.includes(keyword) || modelName.includes(keyword);
|
|
1148
|
+
});
|
|
1149
|
+
});
|
|
1150
|
+
activeFilters.push(`Keywords: "${args.keyword}" (all must match)`);
|
|
1151
|
+
}
|
|
1152
|
+
if (args.access_level) {
|
|
1153
|
+
const accessLabel = args.access_level === 'organization' ? 'Published (Public)' : 'Private (Unpublished)';
|
|
1154
|
+
activeFilters.push(`Access Level: ${accessLabel}`);
|
|
1155
|
+
}
|
|
1156
|
+
if (args.provider) {
|
|
1157
|
+
const provider = args.provider.toLowerCase();
|
|
1158
|
+
models = models.filter((m) => {
|
|
1159
|
+
const providerName = (m.provider?.name || '').toLowerCase();
|
|
1160
|
+
const providerKey = (m.provider?.key || '').toLowerCase();
|
|
1161
|
+
return providerName.includes(provider) || providerKey.includes(provider);
|
|
1162
|
+
});
|
|
1163
|
+
activeFilters.push(`Provider: "${args.provider}"`);
|
|
1164
|
+
}
|
|
1165
|
+
if (typeof args.max_input_price === 'number') {
|
|
1166
|
+
models = models.filter((m) => {
|
|
1167
|
+
const pricePerMillion = parseFloat(m.pricing?.input_per_1k || 0) * 1000;
|
|
1168
|
+
return pricePerMillion <= args.max_input_price;
|
|
1169
|
+
});
|
|
1170
|
+
activeFilters.push(`Max Input Price: $${args.max_input_price}/M tokens`);
|
|
1171
|
+
}
|
|
1172
|
+
if (typeof args.max_output_price === 'number') {
|
|
1173
|
+
models = models.filter((m) => {
|
|
1174
|
+
const pricePerMillion = parseFloat(m.pricing?.output_per_1k || 0) * 1000;
|
|
1175
|
+
return pricePerMillion <= args.max_output_price;
|
|
1176
|
+
});
|
|
1177
|
+
activeFilters.push(`Max Output Price: $${args.max_output_price}/M tokens`);
|
|
1178
|
+
}
|
|
1179
|
+
if (typeof args.min_context_window === 'number') {
|
|
1180
|
+
models = models.filter((m) => {
|
|
1181
|
+
const contextWindow = m.context_window || 0;
|
|
1182
|
+
return contextWindow >= args.min_context_window;
|
|
1183
|
+
});
|
|
1184
|
+
activeFilters.push(`Min Context: ${args.min_context_window.toLocaleString()} tokens`);
|
|
1185
|
+
}
|
|
1186
|
+
// Capability filters
|
|
1187
|
+
if (args.has_vision === true) {
|
|
1188
|
+
models = models.filter((m) => m.capabilities?.vision === true);
|
|
1189
|
+
activeFilters.push(`Has Vision: Yes`);
|
|
1190
|
+
}
|
|
1191
|
+
if (args.has_reasoning === true) {
|
|
1192
|
+
models = models.filter((m) => m.capabilities?.reasoning === true);
|
|
1193
|
+
activeFilters.push(`Has Reasoning: Yes`);
|
|
1194
|
+
}
|
|
1195
|
+
if (args.has_tool_use === true) {
|
|
1196
|
+
models = models.filter((m) => m.capabilities?.tool_use === true);
|
|
1197
|
+
activeFilters.push(`Has Tool Use: Yes`);
|
|
1198
|
+
}
|
|
1199
|
+
if (args.has_image_gen === true) {
|
|
1200
|
+
models = models.filter((m) => m.capabilities?.image_gen === true);
|
|
1201
|
+
activeFilters.push(`Has Image Gen: Yes`);
|
|
1202
|
+
}
|
|
1203
|
+
if (args.has_audio === true) {
|
|
1204
|
+
models = models.filter((m) => m.capabilities?.audio === true);
|
|
1205
|
+
activeFilters.push(`Has Audio: Yes`);
|
|
1206
|
+
}
|
|
1207
|
+
if (args.has_embedding === true) {
|
|
1208
|
+
models = models.filter((m) => m.capabilities?.embedding === true);
|
|
1209
|
+
activeFilters.push(`Has Embedding: Yes`);
|
|
1210
|
+
}
|
|
1211
|
+
// Apply sorting (default: model_id ascending)
|
|
1212
|
+
const orderBy = args.order_by || 'model_id_asc';
|
|
1213
|
+
models.sort((a, b) => {
|
|
1214
|
+
let compareA;
|
|
1215
|
+
let compareB;
|
|
1216
|
+
let ascending = true;
|
|
1217
|
+
switch (orderBy) {
|
|
1218
|
+
case 'model_id_asc':
|
|
1219
|
+
compareA = (a.model_id || '').toLowerCase();
|
|
1220
|
+
compareB = (b.model_id || '').toLowerCase();
|
|
1221
|
+
ascending = true;
|
|
1222
|
+
break;
|
|
1223
|
+
case 'model_id_desc':
|
|
1224
|
+
compareA = (a.model_id || '').toLowerCase();
|
|
1225
|
+
compareB = (b.model_id || '').toLowerCase();
|
|
1226
|
+
ascending = false;
|
|
1227
|
+
break;
|
|
1228
|
+
case 'model_name_asc':
|
|
1229
|
+
compareA = (a.model_name || a.model_id || '').toLowerCase();
|
|
1230
|
+
compareB = (b.model_name || b.model_id || '').toLowerCase();
|
|
1231
|
+
ascending = true;
|
|
1232
|
+
break;
|
|
1233
|
+
case 'model_name_desc':
|
|
1234
|
+
compareA = (a.model_name || a.model_id || '').toLowerCase();
|
|
1235
|
+
compareB = (b.model_name || b.model_id || '').toLowerCase();
|
|
1236
|
+
ascending = false;
|
|
1237
|
+
break;
|
|
1238
|
+
case 'provider_asc':
|
|
1239
|
+
compareA = (a.provider?.name || '').toLowerCase();
|
|
1240
|
+
compareB = (b.provider?.name || '').toLowerCase();
|
|
1241
|
+
ascending = true;
|
|
1242
|
+
break;
|
|
1243
|
+
case 'provider_desc':
|
|
1244
|
+
compareA = (a.provider?.name || '').toLowerCase();
|
|
1245
|
+
compareB = (b.provider?.name || '').toLowerCase();
|
|
1246
|
+
ascending = false;
|
|
1247
|
+
break;
|
|
1248
|
+
case 'context_window_asc':
|
|
1249
|
+
compareA = a.context_window || 0;
|
|
1250
|
+
compareB = b.context_window || 0;
|
|
1251
|
+
ascending = true;
|
|
1252
|
+
break;
|
|
1253
|
+
case 'context_window_desc':
|
|
1254
|
+
compareA = a.context_window || 0;
|
|
1255
|
+
compareB = b.context_window || 0;
|
|
1256
|
+
ascending = false;
|
|
1257
|
+
break;
|
|
1258
|
+
case 'input_price_asc':
|
|
1259
|
+
compareA = parseFloat(a.pricing?.input_per_1k || 0);
|
|
1260
|
+
compareB = parseFloat(b.pricing?.input_per_1k || 0);
|
|
1261
|
+
ascending = true;
|
|
1262
|
+
break;
|
|
1263
|
+
case 'input_price_desc':
|
|
1264
|
+
compareA = parseFloat(a.pricing?.input_per_1k || 0);
|
|
1265
|
+
compareB = parseFloat(b.pricing?.input_per_1k || 0);
|
|
1266
|
+
ascending = false;
|
|
1267
|
+
break;
|
|
1268
|
+
case 'output_price_asc':
|
|
1269
|
+
compareA = parseFloat(a.pricing?.output_per_1k || 0);
|
|
1270
|
+
compareB = parseFloat(b.pricing?.output_per_1k || 0);
|
|
1271
|
+
ascending = true;
|
|
1272
|
+
break;
|
|
1273
|
+
case 'output_price_desc':
|
|
1274
|
+
compareA = parseFloat(a.pricing?.output_per_1k || 0);
|
|
1275
|
+
compareB = parseFloat(b.pricing?.output_per_1k || 0);
|
|
1276
|
+
ascending = false;
|
|
1277
|
+
break;
|
|
1278
|
+
default:
|
|
1279
|
+
// Default to model_id_asc
|
|
1280
|
+
compareA = (a.model_id || '').toLowerCase();
|
|
1281
|
+
compareB = (b.model_id || '').toLowerCase();
|
|
1282
|
+
ascending = true;
|
|
1283
|
+
}
|
|
1284
|
+
if (typeof compareA === 'string') {
|
|
1285
|
+
// String comparison
|
|
1286
|
+
if (ascending) {
|
|
1287
|
+
return compareA.localeCompare(compareB);
|
|
1288
|
+
}
|
|
1289
|
+
else {
|
|
1290
|
+
return compareB.localeCompare(compareA);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
else {
|
|
1294
|
+
// Numeric comparison
|
|
1295
|
+
if (ascending) {
|
|
1296
|
+
return compareA - compareB;
|
|
1297
|
+
}
|
|
1298
|
+
else {
|
|
1299
|
+
return compareB - compareA;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
});
|
|
1303
|
+
// Store total count before pagination
|
|
1304
|
+
const totalModels = models.length;
|
|
1305
|
+
// Build sort info (define early so it can be used in error messages)
|
|
1306
|
+
const sortLabels = {
|
|
1307
|
+
'model_id_asc': 'Model ID (A→Z)',
|
|
1308
|
+
'model_id_desc': 'Model ID (Z→A)',
|
|
1309
|
+
'model_name_asc': 'Model Name (A→Z)',
|
|
1310
|
+
'model_name_desc': 'Model Name (Z→A)',
|
|
1311
|
+
'provider_asc': 'Provider (A→Z)',
|
|
1312
|
+
'provider_desc': 'Provider (Z→A)',
|
|
1313
|
+
'context_window_asc': 'Context Window (Smallest→Largest)',
|
|
1314
|
+
'context_window_desc': 'Context Window (Largest→Smallest)',
|
|
1315
|
+
'input_price_asc': 'Input Price (Cheapest→Most Expensive)',
|
|
1316
|
+
'input_price_desc': 'Input Price (Most Expensive→Cheapest)',
|
|
1317
|
+
'output_price_asc': 'Output Price (Cheapest→Most Expensive)',
|
|
1318
|
+
'output_price_desc': 'Output Price (Most Expensive→Cheapest)'
|
|
1319
|
+
};
|
|
1320
|
+
const sortLabel = sortLabels[orderBy] || sortLabels['model_id_asc'];
|
|
1321
|
+
// Check if filters removed all models
|
|
1322
|
+
if (totalModels === 0) {
|
|
1323
|
+
return {
|
|
1324
|
+
success: true,
|
|
1325
|
+
output: `No models match the specified filters.\n\n` +
|
|
1326
|
+
`Active Filters:\n ${activeFilters.join('\n ')}\n` +
|
|
1327
|
+
`Sorted By: ${sortLabel}\n\n` +
|
|
1328
|
+
`Try relaxing the filters or use marketplace.models.discover to add more models.`
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
// Apply pagination
|
|
1332
|
+
const paginatedModels = models.slice(offset, offset + limit);
|
|
1333
|
+
const hasMore = offset + limit < totalModels;
|
|
1334
|
+
const nextOffset = offset + limit;
|
|
1335
|
+
// Check if offset is beyond available models
|
|
1336
|
+
if (offset >= totalModels) {
|
|
1337
|
+
return {
|
|
1338
|
+
success: true,
|
|
1339
|
+
output: `No models at offset ${offset}. Total models available: ${totalModels}\n\n` +
|
|
1340
|
+
`💡 Try offset=0 to see the first page, or use a smaller offset value.`
|
|
1341
|
+
};
|
|
1342
|
+
}
|
|
1343
|
+
// Build filter header
|
|
1344
|
+
const filterHeader = activeFilters.length > 0
|
|
1345
|
+
? `Active Filters:\n ${activeFilters.join('\n ')}\n\nSorted By: ${sortLabel}\n\n`
|
|
1346
|
+
: `Sorted By: ${sortLabel}\n\n`;
|
|
1347
|
+
// Format model capabilities as icons
|
|
1348
|
+
const formatCaps = (caps) => {
|
|
1349
|
+
if (!caps)
|
|
1350
|
+
return '';
|
|
1351
|
+
const icons = [];
|
|
1352
|
+
if (caps.vision)
|
|
1353
|
+
icons.push('📷');
|
|
1354
|
+
if (caps.reasoning)
|
|
1355
|
+
icons.push('💭');
|
|
1356
|
+
if (caps.tool_use)
|
|
1357
|
+
icons.push('🔧');
|
|
1358
|
+
if (caps.image_gen)
|
|
1359
|
+
icons.push('🎨');
|
|
1360
|
+
if (caps.audio)
|
|
1361
|
+
icons.push('🔊');
|
|
1362
|
+
if (caps.embedding)
|
|
1363
|
+
icons.push('📊');
|
|
1364
|
+
return icons.length > 0 ? ` [${icons.join(' ')}]` : '';
|
|
1365
|
+
};
|
|
1366
|
+
const output = paginatedModels.map((m, idx) => {
|
|
1367
|
+
const inputPricePerMillion = parseFloat(m.pricing?.input_per_1k || 0) * 1000;
|
|
1368
|
+
const outputPricePerMillion = parseFloat(m.pricing?.output_per_1k || 0) * 1000;
|
|
1369
|
+
return `${offset + idx + 1}. ${m.model_name || m.model_id || 'Unknown'}${formatCaps(m.capabilities)}\n` +
|
|
1370
|
+
` Model ID (for delisting): ${m.id}\n` +
|
|
1371
|
+
` Provider: ${m.provider?.name || 'Unknown'}\n` +
|
|
1372
|
+
(m.context_window ? ` Context Window: ${m.context_window.toLocaleString()} tokens\n` : '') +
|
|
1373
|
+
(m.pricing?.input_per_1k !== undefined
|
|
1374
|
+
? ` Pricing: $${inputPricePerMillion.toFixed(4)}/M input, $${outputPricePerMillion.toFixed(4)}/M output\n`
|
|
1375
|
+
: '');
|
|
1376
|
+
}).join('\n');
|
|
1377
|
+
// Build pagination info
|
|
1378
|
+
const paginationInfo = [
|
|
1379
|
+
`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`,
|
|
1380
|
+
`Showing ${offset + 1}-${offset + paginatedModels.length} of ${totalModels} models`,
|
|
1381
|
+
hasMore ? `\n💡 Next page: Use offset=${nextOffset} to see more models` : '',
|
|
1382
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`
|
|
1383
|
+
].filter(line => line).join('\n');
|
|
1384
|
+
return {
|
|
1385
|
+
success: true,
|
|
1386
|
+
output: `Found ${totalModels} model(s)${activeFilters.length > 0 ? ' matching filters' : ''}:\n\n${filterHeader}${output}${paginationInfo}\n\n` +
|
|
1387
|
+
`💡 Capability Icons: 📷 Vision | 💭 Reasoning | 🔧 Tools | 🎨 Images | 🔊 Audio | 📊 Embeddings`
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
catch (error) {
|
|
1391
|
+
return {
|
|
1392
|
+
success: false,
|
|
1393
|
+
error: error.response?.data?.error?.message || error.message
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
/**
|
|
1398
|
+
* Get detailed information about a specific model
|
|
1399
|
+
*/
|
|
1400
|
+
async getModelInfo(modelId) {
|
|
1401
|
+
try {
|
|
1402
|
+
const response = await this.client.get(`/api/models/${encodeURIComponent(modelId)}`);
|
|
1403
|
+
const model = response.data.model;
|
|
1404
|
+
// Format capabilities
|
|
1405
|
+
const caps = model.capabilities || {};
|
|
1406
|
+
const capsList = [];
|
|
1407
|
+
if (caps.vision)
|
|
1408
|
+
capsList.push('📷 Vision');
|
|
1409
|
+
if (caps.reasoning)
|
|
1410
|
+
capsList.push('💭 Reasoning');
|
|
1411
|
+
if (caps.tool_use)
|
|
1412
|
+
capsList.push('🔧 Tools/Functions');
|
|
1413
|
+
if (caps.image_gen)
|
|
1414
|
+
capsList.push('🎨 Image Generation');
|
|
1415
|
+
if (caps.video_gen)
|
|
1416
|
+
capsList.push('🎬 Video Generation');
|
|
1417
|
+
if (caps.audio)
|
|
1418
|
+
capsList.push('🔊 Audio');
|
|
1419
|
+
if (caps.embedding)
|
|
1420
|
+
capsList.push('📊 Embeddings');
|
|
1421
|
+
const output = [
|
|
1422
|
+
`Model: ${model.model_name || model.model_key || 'Unknown'}`,
|
|
1423
|
+
`Model ID: ${model.model_id || 'Unknown'}`,
|
|
1424
|
+
`Provider Model ID: ${model.model_name_at_endpoint || 'Unknown'}`,
|
|
1425
|
+
`Provider: ${model.provider?.name || model.provider_name || 'Unknown'}`,
|
|
1426
|
+
'',
|
|
1427
|
+
`Context Window: ${model.context_window ? model.context_window.toLocaleString() + ' tokens' : 'Unknown'}`,
|
|
1428
|
+
'',
|
|
1429
|
+
`Pricing:`,
|
|
1430
|
+
` Input: $${model.input_price_per_million || '0'}/M tokens`,
|
|
1431
|
+
` Output: $${model.output_price_per_million || '0'}/M tokens`,
|
|
1432
|
+
'',
|
|
1433
|
+
`Capabilities:`,
|
|
1434
|
+
capsList.length > 0 ? capsList.map(c => ` ${c}`).join('\n') : ' (No special capabilities)'
|
|
1435
|
+
].join('\n');
|
|
1436
|
+
return {
|
|
1437
|
+
success: true,
|
|
1438
|
+
output
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
catch (error) {
|
|
1442
|
+
if (error.response?.status === 404) {
|
|
1443
|
+
return {
|
|
1444
|
+
success: false,
|
|
1445
|
+
error: `Model "${modelId}" not found. Use marketplace.models.list to see available models.`
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
return {
|
|
1449
|
+
success: false,
|
|
1450
|
+
error: error.response?.data?.error?.message || error.message
|
|
1451
|
+
};
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
/**
|
|
1455
|
+
* Update model capabilities
|
|
1456
|
+
*/
|
|
1457
|
+
async updateModelCapabilities(args) {
|
|
1458
|
+
try {
|
|
1459
|
+
const { model_id, ...capabilities } = args;
|
|
1460
|
+
if (!model_id) {
|
|
1461
|
+
return {
|
|
1462
|
+
success: false,
|
|
1463
|
+
error: 'Model ID is required'
|
|
1464
|
+
};
|
|
1465
|
+
}
|
|
1466
|
+
// Build request body with only provided capabilities
|
|
1467
|
+
const body = {};
|
|
1468
|
+
if (typeof capabilities.supports_vision === 'boolean')
|
|
1469
|
+
body.supports_vision = capabilities.supports_vision;
|
|
1470
|
+
if (typeof capabilities.supports_reasoning === 'boolean')
|
|
1471
|
+
body.supports_reasoning = capabilities.supports_reasoning;
|
|
1472
|
+
if (typeof capabilities.supports_tool_use === 'boolean')
|
|
1473
|
+
body.supports_tool_use = capabilities.supports_tool_use;
|
|
1474
|
+
if (typeof capabilities.supports_image_gen === 'boolean')
|
|
1475
|
+
body.supports_image_gen = capabilities.supports_image_gen;
|
|
1476
|
+
if (typeof capabilities.supports_video_gen === 'boolean')
|
|
1477
|
+
body.supports_video_gen = capabilities.supports_video_gen;
|
|
1478
|
+
if (typeof capabilities.supports_audio === 'boolean')
|
|
1479
|
+
body.supports_audio = capabilities.supports_audio;
|
|
1480
|
+
if (typeof capabilities.supports_embedding === 'boolean')
|
|
1481
|
+
body.supports_embedding = capabilities.supports_embedding;
|
|
1482
|
+
if (Object.keys(body).length === 0) {
|
|
1483
|
+
return {
|
|
1484
|
+
success: false,
|
|
1485
|
+
error: 'At least one capability must be provided'
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1488
|
+
const response = await this.client.patch(`/api/models/${encodeURIComponent(model_id)}/capabilities`, body);
|
|
1489
|
+
if (!response.data.success) {
|
|
1490
|
+
return {
|
|
1491
|
+
success: false,
|
|
1492
|
+
error: response.data.error?.message || 'Failed to update model capabilities'
|
|
1493
|
+
};
|
|
1494
|
+
}
|
|
1495
|
+
const model = response.data.model;
|
|
1496
|
+
const caps = model.capabilities;
|
|
1497
|
+
const capabilityLabels = [];
|
|
1498
|
+
if (caps.vision)
|
|
1499
|
+
capabilityLabels.push('📷 Vision');
|
|
1500
|
+
if (caps.reasoning)
|
|
1501
|
+
capabilityLabels.push('💭 Reasoning');
|
|
1502
|
+
if (caps.tool_use)
|
|
1503
|
+
capabilityLabels.push('🔧 Tools');
|
|
1504
|
+
if (caps.image_gen)
|
|
1505
|
+
capabilityLabels.push('🎨 Image Gen');
|
|
1506
|
+
if (caps.video_gen)
|
|
1507
|
+
capabilityLabels.push('🎬 Video Gen');
|
|
1508
|
+
if (caps.audio)
|
|
1509
|
+
capabilityLabels.push('🔊 Audio');
|
|
1510
|
+
if (caps.embedding)
|
|
1511
|
+
capabilityLabels.push('📊 Embedding');
|
|
1512
|
+
return {
|
|
1513
|
+
success: true,
|
|
1514
|
+
output: `✅ Model capabilities updated successfully!\n\n` +
|
|
1515
|
+
`Model: ${model.model_id}\n` +
|
|
1516
|
+
`Name: ${model.model_name}\n\n` +
|
|
1517
|
+
`Capabilities:\n` +
|
|
1518
|
+
(capabilityLabels.length > 0
|
|
1519
|
+
? capabilityLabels.map(c => ` • ${c}`).join('\n')
|
|
1520
|
+
: ' (No capabilities enabled)') +
|
|
1521
|
+
`\n\nUpdated: ${new Date(model.updated_at).toLocaleString()}`
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
catch (error) {
|
|
1525
|
+
return {
|
|
1526
|
+
success: false,
|
|
1527
|
+
error: error.response?.data?.error?.message || error.message
|
|
1528
|
+
};
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
/**
|
|
1532
|
+
* List request logs with pagination and filtering
|
|
1533
|
+
*/
|
|
1534
|
+
async listRequestLogs(args) {
|
|
1535
|
+
try {
|
|
1536
|
+
const params = {
|
|
1537
|
+
limit: args.limit || 20,
|
|
1538
|
+
offset: args.offset || 0
|
|
1539
|
+
};
|
|
1540
|
+
if (args.status)
|
|
1541
|
+
params.status = args.status;
|
|
1542
|
+
if (args.endpoint)
|
|
1543
|
+
params.endpoint = args.endpoint;
|
|
1544
|
+
if (args.model)
|
|
1545
|
+
params.model = args.model;
|
|
1546
|
+
const response = await this.client.get('/api/account/request-logs', { params });
|
|
1547
|
+
if (!response.data.success) {
|
|
1548
|
+
return {
|
|
1549
|
+
success: false,
|
|
1550
|
+
error: response.data.error || 'Failed to fetch request logs'
|
|
1551
|
+
};
|
|
1552
|
+
}
|
|
1553
|
+
const { data, pagination } = response.data;
|
|
1554
|
+
if (data.length === 0) {
|
|
1555
|
+
// Show active filters in the "no results" message
|
|
1556
|
+
const activeFilters = [];
|
|
1557
|
+
if (args.status)
|
|
1558
|
+
activeFilters.push(`Status: ${args.status}`);
|
|
1559
|
+
if (args.endpoint)
|
|
1560
|
+
activeFilters.push(`Endpoint: ${args.endpoint}`);
|
|
1561
|
+
if (args.model)
|
|
1562
|
+
activeFilters.push(`Model: ${args.model}`);
|
|
1563
|
+
const filterMsg = activeFilters.length > 0
|
|
1564
|
+
? `\n\nActive filters:\n ${activeFilters.join('\n ')}`
|
|
1565
|
+
: '';
|
|
1566
|
+
return {
|
|
1567
|
+
success: true,
|
|
1568
|
+
output: `No request logs found.${filterMsg}`
|
|
1569
|
+
};
|
|
1570
|
+
}
|
|
1571
|
+
// Build filter summary
|
|
1572
|
+
const filterSummary = [];
|
|
1573
|
+
if (args.status)
|
|
1574
|
+
filterSummary.push(`Status: ${args.status}`);
|
|
1575
|
+
if (args.endpoint)
|
|
1576
|
+
filterSummary.push(`Endpoint: *${args.endpoint}*`);
|
|
1577
|
+
if (args.model)
|
|
1578
|
+
filterSummary.push(`Model: *${args.model}*`);
|
|
1579
|
+
const filterHeader = filterSummary.length > 0
|
|
1580
|
+
? `Filters: ${filterSummary.join(' | ')}\n\n`
|
|
1581
|
+
: '';
|
|
1582
|
+
const output = data.map((log, index) => {
|
|
1583
|
+
const statusEmoji = log.response_status >= 200 && log.response_status < 300 ? '✅' :
|
|
1584
|
+
log.response_status >= 400 && log.response_status < 500 ? '⚠️' : '❌';
|
|
1585
|
+
const lines = [
|
|
1586
|
+
`[${pagination.offset + index + 1}] ${statusEmoji} ${log.method} ${log.endpoint}`,
|
|
1587
|
+
` Status: ${log.response_status}`,
|
|
1588
|
+
` Time: ${new Date(log.created_at).toLocaleString()}`,
|
|
1589
|
+
` IP: ${log.ip_address}`
|
|
1590
|
+
];
|
|
1591
|
+
// Add model info if available
|
|
1592
|
+
if (log.model_used) {
|
|
1593
|
+
lines.push(` Model: ${log.model_used}`);
|
|
1594
|
+
}
|
|
1595
|
+
lines.push(` Request ID: ${log.request_id}`);
|
|
1596
|
+
lines.push(` Log ID: ${log.id}`);
|
|
1597
|
+
return lines.join('\n');
|
|
1598
|
+
}).join('\n\n');
|
|
1599
|
+
const paginationInfo = [
|
|
1600
|
+
`\nShowing ${pagination.offset + 1}-${pagination.offset + pagination.returned} of ${pagination.total} logs`,
|
|
1601
|
+
pagination.has_more ? `\n💡 Next page: offset=${pagination.next_offset}` : ''
|
|
1602
|
+
].join('');
|
|
1603
|
+
return {
|
|
1604
|
+
success: true,
|
|
1605
|
+
output: `Request Logs:\n\n${filterHeader}${output}${paginationInfo}`
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
catch (error) {
|
|
1609
|
+
return {
|
|
1610
|
+
success: false,
|
|
1611
|
+
error: error.response?.data?.error?.message || error.message
|
|
1612
|
+
};
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Get detailed request log by ID
|
|
1617
|
+
*/
|
|
1618
|
+
async getRequestLog(logId, showFullBody = false) {
|
|
1619
|
+
try {
|
|
1620
|
+
const response = await this.client.get(`/api/account/request-logs/${logId}`);
|
|
1621
|
+
if (!response.data.success) {
|
|
1622
|
+
return {
|
|
1623
|
+
success: false,
|
|
1624
|
+
error: response.data.error || 'Failed to fetch log details'
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
const log = response.data.data;
|
|
1628
|
+
// Helper function to truncate JSON strings
|
|
1629
|
+
const formatBody = (body, maxLength = 500) => {
|
|
1630
|
+
if (!body)
|
|
1631
|
+
return ' (empty)';
|
|
1632
|
+
const jsonStr = JSON.stringify(body, null, 2);
|
|
1633
|
+
if (showFullBody || jsonStr.length <= maxLength) {
|
|
1634
|
+
return ` ${jsonStr.split('\n').join('\n ')}`;
|
|
1635
|
+
}
|
|
1636
|
+
const truncated = jsonStr.substring(0, maxLength);
|
|
1637
|
+
const lines = truncated.split('\n');
|
|
1638
|
+
const remainingChars = jsonStr.length - maxLength;
|
|
1639
|
+
return ` ${lines.join('\n ')}\n ... (truncated, ${remainingChars} more characters)\n 💡 Use show_full_body: true to see complete content`;
|
|
1640
|
+
};
|
|
1641
|
+
// Helper to format headers
|
|
1642
|
+
const formatHeaders = (headers) => {
|
|
1643
|
+
if (!headers || Object.keys(headers).length === 0)
|
|
1644
|
+
return ' (no headers)';
|
|
1645
|
+
const formatted = Object.entries(headers)
|
|
1646
|
+
.map(([key, value]) => ` ${key}: ${value}`)
|
|
1647
|
+
.join('\n');
|
|
1648
|
+
return formatted;
|
|
1649
|
+
};
|
|
1650
|
+
// Status emoji
|
|
1651
|
+
const statusEmoji = log.response_status >= 200 && log.response_status < 300 ? '✅' :
|
|
1652
|
+
log.response_status >= 400 && log.response_status < 500 ? '⚠️' : '❌';
|
|
1653
|
+
const output = [
|
|
1654
|
+
`═══════════════════════════════════════════════════════`,
|
|
1655
|
+
` ${statusEmoji} REQUEST LOG DETAILS`,
|
|
1656
|
+
`═══════════════════════════════════════════════════════`,
|
|
1657
|
+
``,
|
|
1658
|
+
`📋 Basic Information:`,
|
|
1659
|
+
` Log ID: ${log.id}`,
|
|
1660
|
+
` Request ID: ${log.request_id}`,
|
|
1661
|
+
` User ID: ${log.user_id}`,
|
|
1662
|
+
` Timestamp: ${new Date(log.created_at).toLocaleString()}`,
|
|
1663
|
+
``,
|
|
1664
|
+
`🌐 Request Information:`,
|
|
1665
|
+
` Method: ${log.method}`,
|
|
1666
|
+
` Endpoint: ${log.endpoint}`,
|
|
1667
|
+
` IP Address: ${log.ip_address}`,
|
|
1668
|
+
` User Agent: ${log.user_agent || '(not provided)'}`,
|
|
1669
|
+
log.model_used ? ` Model Used: ${log.model_used}` : null,
|
|
1670
|
+
``,
|
|
1671
|
+
`📊 Response Information:`,
|
|
1672
|
+
` Status Code: ${log.response_status} ${statusEmoji}`,
|
|
1673
|
+
log.error_message ? ` Error: ${log.error_message}` : null,
|
|
1674
|
+
``,
|
|
1675
|
+
`─────────────────────────────────────────────────────────`,
|
|
1676
|
+
``,
|
|
1677
|
+
`📤 Request Headers:`,
|
|
1678
|
+
formatHeaders(log.request_headers),
|
|
1679
|
+
``,
|
|
1680
|
+
`📤 Request Body:`,
|
|
1681
|
+
formatBody(log.request_body),
|
|
1682
|
+
``,
|
|
1683
|
+
`─────────────────────────────────────────────────────────`,
|
|
1684
|
+
``,
|
|
1685
|
+
`📥 Response Headers:`,
|
|
1686
|
+
formatHeaders(log.response_headers),
|
|
1687
|
+
``,
|
|
1688
|
+
`📥 Response Body:`,
|
|
1689
|
+
formatBody(log.response_body),
|
|
1690
|
+
``,
|
|
1691
|
+
`═══════════════════════════════════════════════════════`
|
|
1692
|
+
].filter(line => line !== null).join('\n');
|
|
1693
|
+
return {
|
|
1694
|
+
success: true,
|
|
1695
|
+
output
|
|
1696
|
+
};
|
|
1697
|
+
}
|
|
1698
|
+
catch (error) {
|
|
1699
|
+
return {
|
|
1700
|
+
success: false,
|
|
1701
|
+
error: error.response?.data?.error?.message || error.message
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
/**
|
|
1706
|
+
* Get request log statistics
|
|
1707
|
+
*/
|
|
1708
|
+
async getRequestLogStats(args) {
|
|
1709
|
+
try {
|
|
1710
|
+
const params = {
|
|
1711
|
+
display_friendly: 'true' // Always use display-friendly names for MCP tool
|
|
1712
|
+
};
|
|
1713
|
+
if (args.start_date)
|
|
1714
|
+
params.start_date = args.start_date;
|
|
1715
|
+
if (args.end_date)
|
|
1716
|
+
params.end_date = args.end_date;
|
|
1717
|
+
const response = await this.client.get('/api/account/request-logs/stats', { params });
|
|
1718
|
+
if (!response.data.success) {
|
|
1719
|
+
return {
|
|
1720
|
+
success: false,
|
|
1721
|
+
error: response.data.error || 'Failed to fetch statistics'
|
|
1722
|
+
};
|
|
1723
|
+
}
|
|
1724
|
+
const { period, summary, top_models, top_endpoints, daily_counts } = response.data.data;
|
|
1725
|
+
const output = [
|
|
1726
|
+
`Request Statistics`,
|
|
1727
|
+
``,
|
|
1728
|
+
`Period: ${new Date(period.start_date).toLocaleDateString()} - ${new Date(period.end_date).toLocaleDateString()}`,
|
|
1729
|
+
``,
|
|
1730
|
+
`Summary:`,
|
|
1731
|
+
` Total Requests: ${summary.total_requests}`,
|
|
1732
|
+
` Success Rate: ${summary.success_rate}`,
|
|
1733
|
+
` Successful: ${summary.successful_requests}`,
|
|
1734
|
+
` Client Errors (4xx): ${summary.client_errors}`,
|
|
1735
|
+
` Server Errors (5xx): ${summary.server_errors}`,
|
|
1736
|
+
` Days Active: ${summary.days_with_requests}`,
|
|
1737
|
+
` Unique Models: ${summary.unique_models_used}`,
|
|
1738
|
+
` Unique Endpoints: ${summary.unique_endpoints}`,
|
|
1739
|
+
``,
|
|
1740
|
+
`Top Models (by request count):`,
|
|
1741
|
+
...top_models.slice(0, 5).map((m, i) => {
|
|
1742
|
+
const modelName = m.model_name || m.category_id;
|
|
1743
|
+
return ` ${i + 1}. ${modelName}: ${m.request_count} requests`;
|
|
1744
|
+
}),
|
|
1745
|
+
``,
|
|
1746
|
+
`Top Endpoints (by request count):`,
|
|
1747
|
+
...top_endpoints.slice(0, 5).map((e, i) => {
|
|
1748
|
+
const displayEndpoint = e.display_endpoint || e.endpoint;
|
|
1749
|
+
return ` ${i + 1}. ${displayEndpoint}: ${e.request_count} requests`;
|
|
1750
|
+
}),
|
|
1751
|
+
``,
|
|
1752
|
+
`Recent Daily Activity (last 7 days):`,
|
|
1753
|
+
...daily_counts.slice(0, 7).map((d) => ` ${new Date(d.date).toLocaleDateString()}: ${d.request_count} requests (${d.successful_count} success, ${d.error_count} errors)`)
|
|
1754
|
+
].join('\n');
|
|
1755
|
+
return {
|
|
1756
|
+
success: true,
|
|
1757
|
+
output
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
catch (error) {
|
|
1761
|
+
return {
|
|
1762
|
+
success: false,
|
|
1763
|
+
error: error.response?.data?.error?.message || error.message
|
|
1764
|
+
};
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* Get comprehensive vault status showing both LocalVault and KeyVault
|
|
1769
|
+
*/
|
|
1770
|
+
async getVaultStatus() {
|
|
1771
|
+
try {
|
|
1772
|
+
// Get LocalVault credentials from management API
|
|
1773
|
+
const vaultResponse = await axios_1.default.get(`${this.managementUrl}/providers`);
|
|
1774
|
+
const providers = vaultResponse.data.providers || [];
|
|
1775
|
+
// Get KeyVault statistics
|
|
1776
|
+
const keyVaultStats = this.keyVault.getStatistics();
|
|
1777
|
+
const lines = [
|
|
1778
|
+
'═══════════════════════════════════════════════════════',
|
|
1779
|
+
' 🗝️ VAULT STATUS',
|
|
1780
|
+
'═══════════════════════════════════════════════════════',
|
|
1781
|
+
'',
|
|
1782
|
+
' Two vault systems protect your API keys:',
|
|
1783
|
+
'',
|
|
1784
|
+
' 💾 LocalVault (Persistent Storage)',
|
|
1785
|
+
' • Stores connection API keys on disk',
|
|
1786
|
+
' • Encrypted with AES-256-GCM',
|
|
1787
|
+
' • Location: ~/.langmart/keystore.enc',
|
|
1788
|
+
' • Survives gateway restarts',
|
|
1789
|
+
'',
|
|
1790
|
+
' 🧠 KeyVault (In-Memory Protection)',
|
|
1791
|
+
' • Redacts API keys in chat messages',
|
|
1792
|
+
' • Prevents keys from being sent to LLMs',
|
|
1793
|
+
' • Temporary protection (session only)',
|
|
1794
|
+
' • Cleared on gateway restart',
|
|
1795
|
+
'',
|
|
1796
|
+
'─────────────────────────────────────────────────────────',
|
|
1797
|
+
'',
|
|
1798
|
+
` 💾 LocalVault - Persistent Storage`,
|
|
1799
|
+
` Status: ${providers.length > 0 ? '✅ Active' : '⚠️ Empty'}`,
|
|
1800
|
+
` Credentials: ${providers.length} stored`,
|
|
1801
|
+
` Encryption: AES-256-GCM`,
|
|
1802
|
+
` File: ~/.langmart/keystore.enc`,
|
|
1803
|
+
''
|
|
1804
|
+
];
|
|
1805
|
+
if (providers.length === 0) {
|
|
1806
|
+
lines.push(' 📋 No credentials stored');
|
|
1807
|
+
lines.push('');
|
|
1808
|
+
lines.push(' 💡 Add connections via /menu → Connections');
|
|
1809
|
+
}
|
|
1810
|
+
else {
|
|
1811
|
+
lines.push(' 📋 Stored Credentials:');
|
|
1812
|
+
lines.push('');
|
|
1813
|
+
for (let i = 0; i < providers.length; i++) {
|
|
1814
|
+
const provider = providers[i];
|
|
1815
|
+
if (provider.connection_id === '__gateway_auth__') {
|
|
1816
|
+
lines.push(` ${i + 1}. 🔐 Gateway Authentication`);
|
|
1817
|
+
lines.push(` Type: Marketplace API Key`);
|
|
1818
|
+
lines.push(` Status: ✅ Encrypted & Stored`);
|
|
1819
|
+
}
|
|
1820
|
+
else {
|
|
1821
|
+
lines.push(` ${i + 1}. 🔑 ${provider.access_key_name || provider.connection_id}`);
|
|
1822
|
+
if (provider.provider_name) {
|
|
1823
|
+
lines.push(` Provider: ${provider.provider_name}`);
|
|
1824
|
+
}
|
|
1825
|
+
if (provider.endpoint) {
|
|
1826
|
+
lines.push(` Endpoint: ${provider.endpoint}`);
|
|
1827
|
+
}
|
|
1828
|
+
if (provider.gateway_type) {
|
|
1829
|
+
const gatewayName = provider.gateway_type === 2
|
|
1830
|
+
? 'Marketplace-Managed Compute'
|
|
1831
|
+
: 'Self-Hosted Compute';
|
|
1832
|
+
lines.push(` Gateway: ${gatewayName}`);
|
|
1833
|
+
}
|
|
1834
|
+
lines.push(` Status: ✅ Encrypted & Stored`);
|
|
1835
|
+
}
|
|
1836
|
+
if (i < providers.length - 1) {
|
|
1837
|
+
lines.push('');
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
lines.push('');
|
|
1842
|
+
lines.push('─────────────────────────────────────────────────────────');
|
|
1843
|
+
lines.push('');
|
|
1844
|
+
lines.push(` 🧠 KeyVault - In-Memory Chat Protection`);
|
|
1845
|
+
lines.push(` Status: ${keyVaultStats.totalKeys > 0 ? '✅ Active' : '💤 Idle'}`);
|
|
1846
|
+
lines.push(` Protected Keys: ${keyVaultStats.totalKeys}`);
|
|
1847
|
+
lines.push(` Session Only: RAM (lost on restart)`);
|
|
1848
|
+
lines.push('');
|
|
1849
|
+
if (keyVaultStats.totalKeys === 0) {
|
|
1850
|
+
lines.push(' 📋 No keys currently protected in memory');
|
|
1851
|
+
lines.push('');
|
|
1852
|
+
lines.push(' 💡 KeyVault automatically detects and redacts API keys');
|
|
1853
|
+
lines.push(' when you type them in chat messages.');
|
|
1854
|
+
}
|
|
1855
|
+
else {
|
|
1856
|
+
lines.push(' 📋 Currently Protected Keys:');
|
|
1857
|
+
lines.push('');
|
|
1858
|
+
for (let i = 0; i < keyVaultStats.keys.length; i++) {
|
|
1859
|
+
const key = keyVaultStats.keys[i];
|
|
1860
|
+
lines.push(` ${i + 1}. ${key.provider.toUpperCase()} API Key`);
|
|
1861
|
+
lines.push(` Placeholder: ${key.redactedPrefix}`);
|
|
1862
|
+
lines.push(` Detected: ${new Date(key.detectedAt).toLocaleTimeString()}`);
|
|
1863
|
+
lines.push(` Age: ${key.age} minutes`);
|
|
1864
|
+
lines.push(` Used: ${key.useCount} times`);
|
|
1865
|
+
if (i < keyVaultStats.keys.length - 1) {
|
|
1866
|
+
lines.push('');
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
lines.push('');
|
|
1870
|
+
lines.push(' ℹ️ Keys older than 15 minutes are auto-removed');
|
|
1871
|
+
}
|
|
1872
|
+
lines.push('');
|
|
1873
|
+
lines.push('─────────────────────────────────────────────────────────');
|
|
1874
|
+
lines.push('');
|
|
1875
|
+
lines.push(' ℹ️ Information');
|
|
1876
|
+
lines.push('');
|
|
1877
|
+
lines.push(' • LocalVault stores connection credentials permanently');
|
|
1878
|
+
lines.push(' • KeyVault protects chat messages from exposing keys');
|
|
1879
|
+
lines.push(' • Both systems work together for complete security');
|
|
1880
|
+
lines.push(' • Keys are never sent to LLMs in plaintext');
|
|
1881
|
+
lines.push('');
|
|
1882
|
+
lines.push('═══════════════════════════════════════════════════════');
|
|
1883
|
+
return {
|
|
1884
|
+
success: true,
|
|
1885
|
+
output: lines.join('\n')
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
catch (error) {
|
|
1889
|
+
return {
|
|
1890
|
+
success: false,
|
|
1891
|
+
error: error.response?.data?.error?.message || error.message
|
|
1892
|
+
};
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
/**
|
|
1896
|
+
* List all credentials in LocalVault (persistent storage)
|
|
1897
|
+
*/
|
|
1898
|
+
async getLocalVaultList() {
|
|
1899
|
+
try {
|
|
1900
|
+
const vaultResponse = await axios_1.default.get(`${this.managementUrl}/providers`);
|
|
1901
|
+
const providers = vaultResponse.data.providers || [];
|
|
1902
|
+
const lines = [
|
|
1903
|
+
'═══════════════════════════════════════════════════════',
|
|
1904
|
+
' 💾 LocalVault - Stored Credentials',
|
|
1905
|
+
'═══════════════════════════════════════════════════════',
|
|
1906
|
+
'',
|
|
1907
|
+
` Total Credentials: ${providers.length}`,
|
|
1908
|
+
` Storage Location: ~/.langmart/keystore.enc`,
|
|
1909
|
+
` Encryption: AES-256-GCM`,
|
|
1910
|
+
''
|
|
1911
|
+
];
|
|
1912
|
+
if (providers.length === 0) {
|
|
1913
|
+
lines.push(' 📋 No credentials stored');
|
|
1914
|
+
lines.push('');
|
|
1915
|
+
lines.push(' 💡 Add connections via /menu → Connections');
|
|
1916
|
+
}
|
|
1917
|
+
else {
|
|
1918
|
+
lines.push(' 📋 Stored Credentials:');
|
|
1919
|
+
lines.push('');
|
|
1920
|
+
for (let i = 0; i < providers.length; i++) {
|
|
1921
|
+
const provider = providers[i];
|
|
1922
|
+
if (provider.connection_id === '__gateway_auth__') {
|
|
1923
|
+
lines.push(` ${i + 1}. 🔐 Gateway Authentication`);
|
|
1924
|
+
lines.push(` Type: Marketplace API Key`);
|
|
1925
|
+
lines.push(` Purpose: Authenticate with marketplace`);
|
|
1926
|
+
lines.push(` Status: ✅ Encrypted & Stored`);
|
|
1927
|
+
}
|
|
1928
|
+
else {
|
|
1929
|
+
lines.push(` ${i + 1}. 🔑 ${provider.access_key_name || provider.connection_id}`);
|
|
1930
|
+
if (provider.provider_name) {
|
|
1931
|
+
lines.push(` Provider: ${provider.provider_name}`);
|
|
1932
|
+
}
|
|
1933
|
+
if (provider.endpoint) {
|
|
1934
|
+
lines.push(` Endpoint: ${provider.endpoint}`);
|
|
1935
|
+
}
|
|
1936
|
+
if (provider.gateway_type) {
|
|
1937
|
+
const gatewayName = provider.gateway_type === 2
|
|
1938
|
+
? 'Marketplace-Managed Compute'
|
|
1939
|
+
: 'Self-Hosted Compute';
|
|
1940
|
+
lines.push(` Gateway: ${gatewayName}`);
|
|
1941
|
+
}
|
|
1942
|
+
lines.push(` Connection: ${provider.connection_id}`);
|
|
1943
|
+
lines.push(` Status: ✅ Encrypted & Stored`);
|
|
1944
|
+
}
|
|
1945
|
+
if (i < providers.length - 1) {
|
|
1946
|
+
lines.push('');
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
lines.push('');
|
|
1951
|
+
lines.push('═══════════════════════════════════════════════════════');
|
|
1952
|
+
return {
|
|
1953
|
+
success: true,
|
|
1954
|
+
output: lines.join('\n')
|
|
1955
|
+
};
|
|
1956
|
+
}
|
|
1957
|
+
catch (error) {
|
|
1958
|
+
return {
|
|
1959
|
+
success: false,
|
|
1960
|
+
error: error.response?.data?.error?.message || error.message
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
/**
|
|
1965
|
+
* Show KeyVault status (in-memory chat protection)
|
|
1966
|
+
*/
|
|
1967
|
+
async getKeyVaultStatus() {
|
|
1968
|
+
try {
|
|
1969
|
+
const keyVaultStats = this.keyVault.getStatistics();
|
|
1970
|
+
const lines = [
|
|
1971
|
+
'═══════════════════════════════════════════════════════',
|
|
1972
|
+
' 🧠 KeyVault - In-Memory Chat Protection',
|
|
1973
|
+
'═══════════════════════════════════════════════════════',
|
|
1974
|
+
'',
|
|
1975
|
+
` Status: ${keyVaultStats.totalKeys > 0 ? '✅ Active' : '💤 Idle'}`,
|
|
1976
|
+
` Protected Keys: ${keyVaultStats.totalKeys}`,
|
|
1977
|
+
` Storage: RAM (temporary, session only)`,
|
|
1978
|
+
` Persistence: Lost on gateway restart`,
|
|
1979
|
+
''
|
|
1980
|
+
];
|
|
1981
|
+
if (keyVaultStats.totalKeys === 0) {
|
|
1982
|
+
lines.push(' 📋 No keys currently protected in memory');
|
|
1983
|
+
lines.push('');
|
|
1984
|
+
lines.push(' 💡 How KeyVault Works:');
|
|
1985
|
+
lines.push('');
|
|
1986
|
+
lines.push(' 1. Automatically detects API keys in chat messages');
|
|
1987
|
+
lines.push(' 2. Replaces them with random placeholders');
|
|
1988
|
+
lines.push(' 3. Sends placeholder to LLM (real key never exposed)');
|
|
1989
|
+
lines.push(' 4. Restores real key in responses (for tool calls)');
|
|
1990
|
+
lines.push(' 5. Auto-removes keys after 15 minutes of inactivity');
|
|
1991
|
+
lines.push('');
|
|
1992
|
+
lines.push(' Supported Providers:');
|
|
1993
|
+
lines.push(' • OpenAI (sk-proj-...)');
|
|
1994
|
+
lines.push(' • Anthropic (sk-ant-api...)');
|
|
1995
|
+
lines.push(' • Google Gemini (AIza...)');
|
|
1996
|
+
lines.push(' • Groq (gsk_...)');
|
|
1997
|
+
lines.push(' • DeepSeek (sk-...)');
|
|
1998
|
+
}
|
|
1999
|
+
else {
|
|
2000
|
+
lines.push(' 📋 Currently Protected Keys:');
|
|
2001
|
+
lines.push('');
|
|
2002
|
+
for (let i = 0; i < keyVaultStats.keys.length; i++) {
|
|
2003
|
+
const key = keyVaultStats.keys[i];
|
|
2004
|
+
const detectedTime = new Date(key.detectedAt);
|
|
2005
|
+
lines.push(` ${i + 1}. ${key.provider.toUpperCase()} API Key`);
|
|
2006
|
+
lines.push(` Placeholder: ${key.redactedPrefix}`);
|
|
2007
|
+
lines.push(` Detected At: ${detectedTime.toLocaleString()}`);
|
|
2008
|
+
lines.push(` Age: ${key.age} minutes ago`);
|
|
2009
|
+
lines.push(` Restored: ${key.useCount} times`);
|
|
2010
|
+
lines.push(` Status: ${key.age < 15 ? '✅ Active' : '⚠️ Expiring soon'}`);
|
|
2011
|
+
if (i < keyVaultStats.keys.length - 1) {
|
|
2012
|
+
lines.push('');
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
lines.push('');
|
|
2016
|
+
lines.push(' ⏰ Auto-Cleanup:');
|
|
2017
|
+
lines.push(' Keys older than 15 minutes are automatically removed');
|
|
2018
|
+
lines.push(' from memory to prevent stale key accumulation.');
|
|
2019
|
+
}
|
|
2020
|
+
lines.push('');
|
|
2021
|
+
lines.push('═══════════════════════════════════════════════════════');
|
|
2022
|
+
return {
|
|
2023
|
+
success: true,
|
|
2024
|
+
output: lines.join('\n')
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
catch (error) {
|
|
2028
|
+
return {
|
|
2029
|
+
success: false,
|
|
2030
|
+
error: error.response?.data?.error?.message || error.message
|
|
2031
|
+
};
|
|
2032
|
+
}
|
|
2033
|
+
}
|
|
2034
|
+
/**
|
|
2035
|
+
* Manage connection quota (set daily/monthly limits and enable/disable enforcement)
|
|
2036
|
+
*/
|
|
2037
|
+
async manageConnectionQuota(args) {
|
|
2038
|
+
try {
|
|
2039
|
+
const { connection_id, max_daily_sales_usd, max_monthly_sales_usd, quota_enabled } = args;
|
|
2040
|
+
if (!connection_id) {
|
|
2041
|
+
return {
|
|
2042
|
+
success: false,
|
|
2043
|
+
error: 'Connection ID is required'
|
|
2044
|
+
};
|
|
2045
|
+
}
|
|
2046
|
+
if (typeof quota_enabled !== 'boolean') {
|
|
2047
|
+
return {
|
|
2048
|
+
success: false,
|
|
2049
|
+
error: 'quota_enabled must be a boolean (true or false)'
|
|
2050
|
+
};
|
|
2051
|
+
}
|
|
2052
|
+
// Resolve sequence number to UUID if needed
|
|
2053
|
+
const resolvedId = this.resolveConnectionId(connection_id);
|
|
2054
|
+
// Build request body
|
|
2055
|
+
const body = {
|
|
2056
|
+
quota_enabled
|
|
2057
|
+
};
|
|
2058
|
+
// Add quota limits if provided (null is valid for "no limit")
|
|
2059
|
+
// Note: Endpoint expects max_daily_sales_quota_usd, not max_daily_sales_usd
|
|
2060
|
+
if (max_daily_sales_usd !== undefined) {
|
|
2061
|
+
body.max_daily_sales_quota_usd = max_daily_sales_usd;
|
|
2062
|
+
}
|
|
2063
|
+
if (max_monthly_sales_usd !== undefined) {
|
|
2064
|
+
body.max_monthly_sales_quota_usd = max_monthly_sales_usd;
|
|
2065
|
+
}
|
|
2066
|
+
const response = await this.client.patch(`/api/connections/${resolvedId}/quota`, body);
|
|
2067
|
+
if (!response.data.success) {
|
|
2068
|
+
return {
|
|
2069
|
+
success: false,
|
|
2070
|
+
error: response.data.error?.message || 'Failed to update connection quota'
|
|
2071
|
+
};
|
|
2072
|
+
}
|
|
2073
|
+
const data = response.data.data;
|
|
2074
|
+
const quota = data.quota;
|
|
2075
|
+
// Format quota limits
|
|
2076
|
+
const dailyLimit = quota.max_daily_sales_usd
|
|
2077
|
+
? `$${quota.max_daily_sales_usd}/day`
|
|
2078
|
+
: 'No limit';
|
|
2079
|
+
const monthlyLimit = quota.max_monthly_sales_usd
|
|
2080
|
+
? `$${quota.max_monthly_sales_usd}/month`
|
|
2081
|
+
: 'No limit';
|
|
2082
|
+
const enforcementStatus = quota.enabled
|
|
2083
|
+
? '✅ Enabled (models auto-disable when exceeded)'
|
|
2084
|
+
: '⚠️ Disabled (tracking only, no enforcement)';
|
|
2085
|
+
const modelsAffected = quota.models_affected_count || '0';
|
|
2086
|
+
return {
|
|
2087
|
+
success: true,
|
|
2088
|
+
output: `✅ Connection quota updated successfully!\n\n` +
|
|
2089
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` +
|
|
2090
|
+
`📊 Quota Settings:\n` +
|
|
2091
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` +
|
|
2092
|
+
` Connection ID: ${resolvedId}\n` +
|
|
2093
|
+
` Access Key: ${data.access_key}\n` +
|
|
2094
|
+
` Models Affected: ${modelsAffected}\n\n` +
|
|
2095
|
+
` Daily Limit: ${dailyLimit}\n` +
|
|
2096
|
+
` Monthly Limit: ${monthlyLimit}\n` +
|
|
2097
|
+
` Enforcement: ${enforcementStatus}\n\n` +
|
|
2098
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n` +
|
|
2099
|
+
`ℹ️ Quota applies to:\n` +
|
|
2100
|
+
` • ALL models under this connection\n` +
|
|
2101
|
+
` • Your own consumption via this connection\n` +
|
|
2102
|
+
` • Other users consuming your published models\n\n` +
|
|
2103
|
+
`⏰ Reset Schedule:\n` +
|
|
2104
|
+
` • Daily quota resets at midnight UTC\n` +
|
|
2105
|
+
` • Monthly quota resets on 1st of month\n\n` +
|
|
2106
|
+
`📝 ${data.message}\n\n` +
|
|
2107
|
+
`Updated: ${new Date(data.updated_at).toLocaleString()}`
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
catch (error) {
|
|
2111
|
+
return {
|
|
2112
|
+
success: false,
|
|
2113
|
+
error: error.response?.data?.error?.message || error.message
|
|
2114
|
+
};
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
/**
|
|
2118
|
+
* Get quota status for a specific connection
|
|
2119
|
+
*/
|
|
2120
|
+
async getConnectionQuota(connection_id) {
|
|
2121
|
+
try {
|
|
2122
|
+
if (!connection_id) {
|
|
2123
|
+
return {
|
|
2124
|
+
success: false,
|
|
2125
|
+
error: 'Connection ID is required'
|
|
2126
|
+
};
|
|
2127
|
+
}
|
|
2128
|
+
// Resolve sequence number to UUID if needed
|
|
2129
|
+
const resolvedId = this.resolveConnectionId(connection_id);
|
|
2130
|
+
const response = await this.client.get(`/api/connections/${resolvedId}/quota`);
|
|
2131
|
+
if (!response.data.success) {
|
|
2132
|
+
return {
|
|
2133
|
+
success: false,
|
|
2134
|
+
error: response.data.error?.message || 'Failed to get connection quota'
|
|
2135
|
+
};
|
|
2136
|
+
}
|
|
2137
|
+
const data = response.data.data;
|
|
2138
|
+
const quota = data.quota;
|
|
2139
|
+
const models = data.models;
|
|
2140
|
+
const history = data.history || [];
|
|
2141
|
+
// Format quota limits
|
|
2142
|
+
const dailyLimit = quota.max_daily_sales_usd
|
|
2143
|
+
? `$${quota.max_daily_sales_usd}`
|
|
2144
|
+
: 'No limit';
|
|
2145
|
+
const monthlyLimit = quota.max_monthly_sales_usd
|
|
2146
|
+
? `$${quota.max_monthly_sales_usd}`
|
|
2147
|
+
: 'No limit';
|
|
2148
|
+
const enforcementStatus = quota.enabled
|
|
2149
|
+
? '✅ Enabled'
|
|
2150
|
+
: '⚠️ Disabled (tracking only)';
|
|
2151
|
+
// Format current usage
|
|
2152
|
+
const dailyUsage = quota.current_daily_sales_usd || 0;
|
|
2153
|
+
const monthlyUsage = quota.current_monthly_sales_usd || 0;
|
|
2154
|
+
const dailyPercentage = quota.daily_quota_percentage || '0.00';
|
|
2155
|
+
const monthlyPercentage = quota.monthly_quota_percentage || '0.00';
|
|
2156
|
+
// Build output
|
|
2157
|
+
let output = `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
|
|
2158
|
+
output += `📊 Quota Status for Connection ${connection_id}\n`;
|
|
2159
|
+
output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`;
|
|
2160
|
+
output += `🔧 Configuration:\n`;
|
|
2161
|
+
output += ` Connection ID: ${data.connection_id}\n`;
|
|
2162
|
+
output += ` Access Key: ${data.access_key}\n`;
|
|
2163
|
+
output += ` Enforcement: ${enforcementStatus}\n\n`;
|
|
2164
|
+
output += `💰 Quota Limits:\n`;
|
|
2165
|
+
output += ` Daily Limit: ${dailyLimit}\n`;
|
|
2166
|
+
output += ` Monthly Limit: ${monthlyLimit}\n\n`;
|
|
2167
|
+
output += `📈 Current Usage:\n`;
|
|
2168
|
+
output += ` Daily: $${dailyUsage.toFixed(4)} (${dailyPercentage}%)\n`;
|
|
2169
|
+
output += ` Monthly: $${monthlyUsage.toFixed(4)} (${monthlyPercentage}%)\n\n`;
|
|
2170
|
+
if (quota.quota_exceeded) {
|
|
2171
|
+
output += `❌ QUOTA EXCEEDED\n`;
|
|
2172
|
+
output += ` Exceeded At: ${new Date(quota.quota_exceeded_at).toLocaleString()}\n\n`;
|
|
2173
|
+
}
|
|
2174
|
+
output += `🎯 Affected Models:\n`;
|
|
2175
|
+
output += ` Total Models: ${models.total_count}\n`;
|
|
2176
|
+
output += ` Available: ${models.available_count}\n`;
|
|
2177
|
+
output += ` Disabled: ${models.disabled_count}\n\n`;
|
|
2178
|
+
// Show recent history if available
|
|
2179
|
+
if (history.length > 0) {
|
|
2180
|
+
output += `📅 Recent Usage History (Last ${Math.min(history.length, 7)} days):\n\n`;
|
|
2181
|
+
const recentHistory = history.slice(0, 7);
|
|
2182
|
+
recentHistory.forEach((day) => {
|
|
2183
|
+
const date = new Date(day.date).toLocaleDateString();
|
|
2184
|
+
const dayUsage = parseFloat(day.daily_sales_usd || 0);
|
|
2185
|
+
const requests = day.daily_requests || 0;
|
|
2186
|
+
const exceeded = day.daily_quota_exceeded ? '❌' : '✅';
|
|
2187
|
+
output += ` ${date}: $${dayUsage.toFixed(4)} (${requests} requests) ${exceeded}\n`;
|
|
2188
|
+
});
|
|
2189
|
+
output += `\n`;
|
|
2190
|
+
}
|
|
2191
|
+
output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`;
|
|
2192
|
+
return {
|
|
2193
|
+
success: true,
|
|
2194
|
+
output
|
|
2195
|
+
};
|
|
2196
|
+
}
|
|
2197
|
+
catch (error) {
|
|
2198
|
+
return {
|
|
2199
|
+
success: false,
|
|
2200
|
+
error: error.response?.data?.error?.message || error.message
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
/**
|
|
2205
|
+
* List quota status for all connections
|
|
2206
|
+
*/
|
|
2207
|
+
async listConnectionQuotas() {
|
|
2208
|
+
try {
|
|
2209
|
+
// First get all connections
|
|
2210
|
+
const connectionsResponse = await this.client.get('/api/connections');
|
|
2211
|
+
const connections = connectionsResponse.data.connections || [];
|
|
2212
|
+
if (connections.length === 0) {
|
|
2213
|
+
return {
|
|
2214
|
+
success: true,
|
|
2215
|
+
output: 'No connections found. Use marketplace.connections.add to create one.'
|
|
2216
|
+
};
|
|
2217
|
+
}
|
|
2218
|
+
// Sort by created_at to maintain consistent ordering
|
|
2219
|
+
connections.sort((a, b) => {
|
|
2220
|
+
const dateA = new Date(a.created_at || 0).getTime();
|
|
2221
|
+
const dateB = new Date(b.created_at || 0).getTime();
|
|
2222
|
+
return dateA - dateB;
|
|
2223
|
+
});
|
|
2224
|
+
let output = `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
|
|
2225
|
+
output += `📊 Quota Status - All Connections\n`;
|
|
2226
|
+
output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`;
|
|
2227
|
+
// Fetch and display quota for each connection
|
|
2228
|
+
for (let idx = 0; idx < connections.length; idx++) {
|
|
2229
|
+
const conn = connections[idx];
|
|
2230
|
+
const seqId = idx + 1;
|
|
2231
|
+
try {
|
|
2232
|
+
const quotaResponse = await this.client.get(`/api/connections/${conn.id}/quota`);
|
|
2233
|
+
if (quotaResponse.data.success) {
|
|
2234
|
+
const data = quotaResponse.data.data;
|
|
2235
|
+
const quota = data.quota;
|
|
2236
|
+
const dailyLimit = quota.max_daily_sales_usd ? `$${quota.max_daily_sales_usd}` : 'No limit';
|
|
2237
|
+
const monthlyLimit = quota.max_monthly_sales_usd ? `$${quota.max_monthly_sales_usd}` : 'No limit';
|
|
2238
|
+
const dailyUsage = quota.current_daily_sales_usd || 0;
|
|
2239
|
+
const monthlyUsage = quota.current_monthly_sales_usd || 0;
|
|
2240
|
+
const enforcementIcon = quota.enabled ? '✅' : '⚠️';
|
|
2241
|
+
const exceededIcon = quota.quota_exceeded ? '❌' : '✅';
|
|
2242
|
+
output += `[${seqId}] ${conn.provider?.name || 'Unknown'} - ${conn.endpoint_type}\n`;
|
|
2243
|
+
output += ` ${enforcementIcon} Enforcement: ${quota.enabled ? 'Enabled' : 'Disabled'}\n`;
|
|
2244
|
+
output += ` Daily: $${dailyUsage.toFixed(2)} / ${dailyLimit} (${quota.daily_quota_percentage || '0'}%)\n`;
|
|
2245
|
+
output += ` Monthly: $${monthlyUsage.toFixed(2)} / ${monthlyLimit} (${quota.monthly_quota_percentage || '0'}%)\n`;
|
|
2246
|
+
output += ` Status: ${exceededIcon} ${quota.quota_exceeded ? 'EXCEEDED' : 'Within limits'}\n`;
|
|
2247
|
+
output += ` Models: ${data.models.available_count}/${data.models.total_count} available\n`;
|
|
2248
|
+
}
|
|
2249
|
+
else {
|
|
2250
|
+
output += `[${seqId}] ${conn.provider?.name || 'Unknown'} - ${conn.endpoint_type}\n`;
|
|
2251
|
+
output += ` ⚠️ Could not fetch quota status\n`;
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
catch (error) {
|
|
2255
|
+
output += `[${seqId}] ${conn.provider?.name || 'Unknown'} - ${conn.endpoint_type}\n`;
|
|
2256
|
+
output += ` ❌ Error fetching quota: ${error.message}\n`;
|
|
2257
|
+
}
|
|
2258
|
+
if (idx < connections.length - 1) {
|
|
2259
|
+
output += `\n`;
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
output += `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
|
|
2263
|
+
output += `💡 Use marketplace.connections.quota.get to see detailed history for a specific connection\n`;
|
|
2264
|
+
output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`;
|
|
2265
|
+
return {
|
|
2266
|
+
success: true,
|
|
2267
|
+
output
|
|
2268
|
+
};
|
|
2269
|
+
}
|
|
2270
|
+
catch (error) {
|
|
2271
|
+
return {
|
|
2272
|
+
success: false,
|
|
2273
|
+
error: error.response?.data?.error?.message || error.message
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
/**
|
|
2278
|
+
* Update model pricing (input/output/cached/reasoning tokens)
|
|
2279
|
+
* Supports both single model and bulk operations with filter
|
|
2280
|
+
*/
|
|
2281
|
+
async updateModelPricing(args) {
|
|
2282
|
+
try {
|
|
2283
|
+
const { model_id, filter, source_access_level, auto_confirm = false, input_tokens_per_1k, output_tokens_per_1k, cached_input_tokens_per_1k, reasoning_tokens_per_1k, increase_percentage, decrease_percentage } = args;
|
|
2284
|
+
// Determine pricing mode: exact or percentage
|
|
2285
|
+
const hasExactPrices = typeof input_tokens_per_1k === 'number' && typeof output_tokens_per_1k === 'number';
|
|
2286
|
+
const hasPercentage = typeof increase_percentage === 'number' || typeof decrease_percentage === 'number';
|
|
2287
|
+
// Validate pricing parameters
|
|
2288
|
+
if (!hasExactPrices && !hasPercentage) {
|
|
2289
|
+
return {
|
|
2290
|
+
success: false,
|
|
2291
|
+
error: 'Must provide either exact prices (input_tokens_per_1k + output_tokens_per_1k) OR percentage adjustment (increase_percentage or decrease_percentage)'
|
|
2292
|
+
};
|
|
2293
|
+
}
|
|
2294
|
+
if (hasExactPrices && hasPercentage) {
|
|
2295
|
+
return {
|
|
2296
|
+
success: false,
|
|
2297
|
+
error: 'Cannot specify both exact prices and percentage adjustment. Choose one pricing method.'
|
|
2298
|
+
};
|
|
2299
|
+
}
|
|
2300
|
+
if (typeof increase_percentage === 'number' && typeof decrease_percentage === 'number') {
|
|
2301
|
+
return {
|
|
2302
|
+
success: false,
|
|
2303
|
+
error: 'Cannot specify both increase_percentage and decrease_percentage. Choose one direction.'
|
|
2304
|
+
};
|
|
2305
|
+
}
|
|
2306
|
+
// Validate percentage range
|
|
2307
|
+
if (typeof increase_percentage === 'number' && (increase_percentage <= 0 || increase_percentage > 1000)) {
|
|
2308
|
+
return {
|
|
2309
|
+
success: false,
|
|
2310
|
+
error: 'increase_percentage must be between 0 and 1000'
|
|
2311
|
+
};
|
|
2312
|
+
}
|
|
2313
|
+
if (typeof decrease_percentage === 'number' && (decrease_percentage <= 0 || decrease_percentage >= 100)) {
|
|
2314
|
+
return {
|
|
2315
|
+
success: false,
|
|
2316
|
+
error: 'decrease_percentage must be between 0 and 100 (cannot decrease by 100% or more)'
|
|
2317
|
+
};
|
|
2318
|
+
}
|
|
2319
|
+
// Check mutually exclusive parameters
|
|
2320
|
+
if (model_id && filter) {
|
|
2321
|
+
return {
|
|
2322
|
+
success: false,
|
|
2323
|
+
error: 'Cannot specify both model_id and filter. Use model_id for single update or filter for bulk update.'
|
|
2324
|
+
};
|
|
2325
|
+
}
|
|
2326
|
+
if (!model_id && !filter) {
|
|
2327
|
+
return {
|
|
2328
|
+
success: false,
|
|
2329
|
+
error: 'Either model_id or filter is required. Use model_id for single update or filter for bulk update.'
|
|
2330
|
+
};
|
|
2331
|
+
}
|
|
2332
|
+
// MODE 1: Single model update
|
|
2333
|
+
if (model_id && !filter) {
|
|
2334
|
+
// For percentage mode, fetch current pricing first
|
|
2335
|
+
if (hasPercentage) {
|
|
2336
|
+
const modelResponse = await this.client.get(`/api/models/${model_id}`);
|
|
2337
|
+
const model = modelResponse.data;
|
|
2338
|
+
if (!model) {
|
|
2339
|
+
return {
|
|
2340
|
+
success: false,
|
|
2341
|
+
error: `Model with id ${model_id} not found`
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
2344
|
+
// Calculate new prices based on percentage
|
|
2345
|
+
const multiplier = typeof increase_percentage === 'number'
|
|
2346
|
+
? (1 + increase_percentage / 100)
|
|
2347
|
+
: (1 - decrease_percentage / 100);
|
|
2348
|
+
const currentInput = parseFloat(model.pricing?.input_per_1k || 0);
|
|
2349
|
+
const currentOutput = parseFloat(model.pricing?.output_per_1k || 0);
|
|
2350
|
+
const currentCached = parseFloat(model.pricing?.cached_input_per_1k || 0);
|
|
2351
|
+
const currentReasoning = parseFloat(model.pricing?.reasoning_per_1k || 0);
|
|
2352
|
+
const newInput = currentInput * multiplier;
|
|
2353
|
+
const newOutput = currentOutput * multiplier;
|
|
2354
|
+
const newCached = currentCached > 0 ? currentCached * multiplier : undefined;
|
|
2355
|
+
const newReasoning = currentReasoning > 0 ? currentReasoning * multiplier : undefined;
|
|
2356
|
+
return await this.updateSingleModelPricing(model_id, newInput, newOutput, newCached, newReasoning);
|
|
2357
|
+
}
|
|
2358
|
+
// Exact price mode
|
|
2359
|
+
return await this.updateSingleModelPricing(model_id, input_tokens_per_1k, output_tokens_per_1k, cached_input_tokens_per_1k, reasoning_tokens_per_1k);
|
|
2360
|
+
}
|
|
2361
|
+
// MODE 2: Bulk update using filter
|
|
2362
|
+
if (filter !== undefined && !model_id) {
|
|
2363
|
+
// Step 1: Fetch all models with optional access level filter
|
|
2364
|
+
let fetchUrl = '/api/models?limit=1000';
|
|
2365
|
+
if (source_access_level) {
|
|
2366
|
+
fetchUrl += `&access_level=${source_access_level}`;
|
|
2367
|
+
}
|
|
2368
|
+
const response = await this.client.get(fetchUrl);
|
|
2369
|
+
const allModels = response.data.models || [];
|
|
2370
|
+
if (allModels.length === 0) {
|
|
2371
|
+
return {
|
|
2372
|
+
success: false,
|
|
2373
|
+
error: source_access_level
|
|
2374
|
+
? `🔍 No ${source_access_level} models found.`
|
|
2375
|
+
: '🔍 No models found.'
|
|
2376
|
+
};
|
|
2377
|
+
}
|
|
2378
|
+
// Step 2: Apply keyword filter (enhanced to include capabilities)
|
|
2379
|
+
const keywords = filter.trim().split(/\s+/).filter((k) => k.length > 0).map((k) => k.toLowerCase());
|
|
2380
|
+
const matchedModels = allModels.filter((m) => {
|
|
2381
|
+
const modelId = (m.model_id || '').toLowerCase();
|
|
2382
|
+
const modelName = (m.model_name || '').toLowerCase();
|
|
2383
|
+
const providerName = (m.provider?.name || '').toLowerCase();
|
|
2384
|
+
// Build searchable capabilities string
|
|
2385
|
+
const capabilities = [];
|
|
2386
|
+
if (m.capabilities?.vision)
|
|
2387
|
+
capabilities.push('vision', 'image', 'photo');
|
|
2388
|
+
if (m.capabilities?.reasoning)
|
|
2389
|
+
capabilities.push('reasoning', 'think', 'o1');
|
|
2390
|
+
if (m.capabilities?.tool_use)
|
|
2391
|
+
capabilities.push('tool', 'tools', 'function');
|
|
2392
|
+
if (m.capabilities?.image_gen)
|
|
2393
|
+
capabilities.push('image', 'dalle', 'generate', 'imagen');
|
|
2394
|
+
if (m.capabilities?.video_gen)
|
|
2395
|
+
capabilities.push('video');
|
|
2396
|
+
if (m.capabilities?.audio)
|
|
2397
|
+
capabilities.push('audio', 'sound', 'speech');
|
|
2398
|
+
if (m.capabilities?.embedding)
|
|
2399
|
+
capabilities.push('embedding', 'vector');
|
|
2400
|
+
const capabilitiesText = capabilities.join(' ');
|
|
2401
|
+
// ALL keywords must match (AND condition) - check model fields AND capabilities
|
|
2402
|
+
return keywords.every((keyword) => {
|
|
2403
|
+
return modelId.includes(keyword)
|
|
2404
|
+
|| modelName.includes(keyword)
|
|
2405
|
+
|| providerName.includes(keyword)
|
|
2406
|
+
|| capabilitiesText.includes(keyword);
|
|
2407
|
+
});
|
|
2408
|
+
});
|
|
2409
|
+
if (matchedModels.length === 0) {
|
|
2410
|
+
return {
|
|
2411
|
+
success: false,
|
|
2412
|
+
error: `🔍 No models matched filter: "${filter}"\n\n` +
|
|
2413
|
+
`Tip: Use space-separated keywords for AND matching.\n` +
|
|
2414
|
+
`Example: "groq llama" matches models with BOTH keywords.`
|
|
2415
|
+
};
|
|
2416
|
+
}
|
|
2417
|
+
// Step 3: Calculate prices for each model (different logic for percentage vs exact)
|
|
2418
|
+
const modelsWithPricing = matchedModels.map((m) => {
|
|
2419
|
+
if (hasPercentage) {
|
|
2420
|
+
// Calculate new prices based on percentage
|
|
2421
|
+
const multiplier = typeof increase_percentage === 'number'
|
|
2422
|
+
? (1 + increase_percentage / 100)
|
|
2423
|
+
: (1 - decrease_percentage / 100);
|
|
2424
|
+
const currentInput = parseFloat(m.pricing?.input_per_1k || 0);
|
|
2425
|
+
const currentOutput = parseFloat(m.pricing?.output_per_1k || 0);
|
|
2426
|
+
const currentCached = parseFloat(m.pricing?.cached_input_per_1k || 0);
|
|
2427
|
+
const currentReasoning = parseFloat(m.pricing?.reasoning_per_1k || 0);
|
|
2428
|
+
return {
|
|
2429
|
+
...m,
|
|
2430
|
+
newPricing: {
|
|
2431
|
+
input: currentInput * multiplier,
|
|
2432
|
+
output: currentOutput * multiplier,
|
|
2433
|
+
cached: currentCached > 0 ? currentCached * multiplier : undefined,
|
|
2434
|
+
reasoning: currentReasoning > 0 ? currentReasoning * multiplier : undefined
|
|
2435
|
+
}
|
|
2436
|
+
};
|
|
2437
|
+
}
|
|
2438
|
+
else {
|
|
2439
|
+
// Exact pricing mode
|
|
2440
|
+
return {
|
|
2441
|
+
...m,
|
|
2442
|
+
newPricing: {
|
|
2443
|
+
input: input_tokens_per_1k,
|
|
2444
|
+
output: output_tokens_per_1k,
|
|
2445
|
+
cached: cached_input_tokens_per_1k,
|
|
2446
|
+
reasoning: reasoning_tokens_per_1k
|
|
2447
|
+
}
|
|
2448
|
+
};
|
|
2449
|
+
}
|
|
2450
|
+
});
|
|
2451
|
+
// Step 4: Show confirmation if auto_confirm is false
|
|
2452
|
+
if (!auto_confirm) {
|
|
2453
|
+
let pricingHeader = '';
|
|
2454
|
+
if (hasPercentage) {
|
|
2455
|
+
const adjType = typeof increase_percentage === 'number' ? 'Increase' : 'Decrease';
|
|
2456
|
+
const percentage = typeof increase_percentage === 'number' ? increase_percentage : decrease_percentage;
|
|
2457
|
+
pricingHeader = `${adjType} all prices by ${percentage}%`;
|
|
2458
|
+
}
|
|
2459
|
+
else {
|
|
2460
|
+
const inputPerMillion = (input_tokens_per_1k * 1000).toFixed(4);
|
|
2461
|
+
const outputPerMillion = (output_tokens_per_1k * 1000).toFixed(4);
|
|
2462
|
+
pricingHeader = `Set exact prices:\n - Input: $${inputPerMillion}/M ($${input_tokens_per_1k}/1K)\n - Output: $${outputPerMillion}/M ($${output_tokens_per_1k}/1K)`;
|
|
2463
|
+
}
|
|
2464
|
+
const modelList = modelsWithPricing.slice(0, 10).map((m, idx) => {
|
|
2465
|
+
const currentInput = parseFloat(m.pricing?.input_per_1k || 0) * 1000;
|
|
2466
|
+
const currentOutput = parseFloat(m.pricing?.output_per_1k || 0) * 1000;
|
|
2467
|
+
const newInput = m.newPricing.input * 1000;
|
|
2468
|
+
const newOutput = m.newPricing.output * 1000;
|
|
2469
|
+
return `${idx + 1}. ${m.model_name || m.model_id}\n` +
|
|
2470
|
+
` Current: $${currentInput.toFixed(4)}/M in, $${currentOutput.toFixed(4)}/M out\n` +
|
|
2471
|
+
` New: $${newInput.toFixed(4)}/M in, $${newOutput.toFixed(4)}/M out`;
|
|
2472
|
+
}).join('\n\n');
|
|
2473
|
+
const remaining = modelsWithPricing.length > 10 ? `\n\n... and ${modelsWithPricing.length - 10} more models` : '';
|
|
2474
|
+
return {
|
|
2475
|
+
success: false,
|
|
2476
|
+
error: `🔍 Found ${modelsWithPricing.length} model(s) matching filter: "${filter}"\n\n` +
|
|
2477
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` +
|
|
2478
|
+
`💰 Pricing Change:\n` +
|
|
2479
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n` +
|
|
2480
|
+
pricingHeader + '\n\n' +
|
|
2481
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` +
|
|
2482
|
+
`📋 Models to Update (showing current → new):\n` +
|
|
2483
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n` +
|
|
2484
|
+
modelList + remaining + '\n\n' +
|
|
2485
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n` +
|
|
2486
|
+
`⚠️ To proceed, please confirm by calling this tool again with:\n` +
|
|
2487
|
+
` auto_confirm: true\n\n` +
|
|
2488
|
+
`Or cancel by not making the call.`
|
|
2489
|
+
};
|
|
2490
|
+
}
|
|
2491
|
+
// Step 5: Update all matched models with rate limiting
|
|
2492
|
+
const results = [];
|
|
2493
|
+
const errors = [];
|
|
2494
|
+
// Helper function to delay between requests to avoid rate limits
|
|
2495
|
+
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
2496
|
+
for (let i = 0; i < modelsWithPricing.length; i++) {
|
|
2497
|
+
const model = modelsWithPricing[i];
|
|
2498
|
+
try {
|
|
2499
|
+
const result = await this.updateSingleModelPricing(model.id, model.newPricing.input, model.newPricing.output, model.newPricing.cached, model.newPricing.reasoning);
|
|
2500
|
+
if (result.success) {
|
|
2501
|
+
results.push({
|
|
2502
|
+
id: model.id,
|
|
2503
|
+
name: model.model_name || model.model_id
|
|
2504
|
+
});
|
|
2505
|
+
}
|
|
2506
|
+
else {
|
|
2507
|
+
errors.push({
|
|
2508
|
+
id: model.id,
|
|
2509
|
+
name: model.model_name || model.model_id,
|
|
2510
|
+
error: result.error
|
|
2511
|
+
});
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
catch (err) {
|
|
2515
|
+
// Check if it's a rate limit error
|
|
2516
|
+
if (err.response?.status === 429) {
|
|
2517
|
+
errors.push({
|
|
2518
|
+
id: model.id,
|
|
2519
|
+
name: model.model_name || model.model_id,
|
|
2520
|
+
error: 'Rate limit exceeded - try again with fewer models or wait a moment'
|
|
2521
|
+
});
|
|
2522
|
+
// Stop processing further to avoid more rate limit errors
|
|
2523
|
+
break;
|
|
2524
|
+
}
|
|
2525
|
+
errors.push({
|
|
2526
|
+
id: model.id,
|
|
2527
|
+
name: model.model_name || model.model_id,
|
|
2528
|
+
error: err.response?.data?.error?.message || err.message
|
|
2529
|
+
});
|
|
2530
|
+
}
|
|
2531
|
+
// Add delay between requests to avoid rate limiting (200ms per request)
|
|
2532
|
+
if (i < modelsWithPricing.length - 1) {
|
|
2533
|
+
await delay(200);
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
// Step 6: Build response
|
|
2537
|
+
let pricingSummary = '';
|
|
2538
|
+
if (hasPercentage) {
|
|
2539
|
+
const adjType = typeof increase_percentage === 'number' ? 'increased' : 'decreased';
|
|
2540
|
+
const percentage = typeof increase_percentage === 'number' ? increase_percentage : decrease_percentage;
|
|
2541
|
+
pricingSummary = `Pricing ${adjType} by ${percentage}%`;
|
|
2542
|
+
}
|
|
2543
|
+
else {
|
|
2544
|
+
const inputPerMillion = (input_tokens_per_1k * 1000).toFixed(4);
|
|
2545
|
+
const outputPerMillion = (output_tokens_per_1k * 1000).toFixed(4);
|
|
2546
|
+
pricingSummary = `New Pricing: $${inputPerMillion}/M input, $${outputPerMillion}/M output`;
|
|
2547
|
+
}
|
|
2548
|
+
let output = `✅ Bulk pricing update completed!\n\n`;
|
|
2549
|
+
if (results.length > 0) {
|
|
2550
|
+
output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
|
|
2551
|
+
output += `✅ Successfully updated (${results.length} models):\n`;
|
|
2552
|
+
output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`;
|
|
2553
|
+
output += `${pricingSummary}\n\n`;
|
|
2554
|
+
results.forEach((r, idx) => {
|
|
2555
|
+
output += `${idx + 1}. ${r.name}\n`;
|
|
2556
|
+
});
|
|
2557
|
+
}
|
|
2558
|
+
if (errors.length > 0) {
|
|
2559
|
+
output += `\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
|
|
2560
|
+
output += `❌ Failed (${errors.length} models):\n`;
|
|
2561
|
+
output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`;
|
|
2562
|
+
errors.forEach((e, idx) => {
|
|
2563
|
+
output += `${idx + 1}. ${e.name}\n Error: ${e.error}\n\n`;
|
|
2564
|
+
});
|
|
2565
|
+
}
|
|
2566
|
+
return {
|
|
2567
|
+
success: results.length > 0,
|
|
2568
|
+
output
|
|
2569
|
+
};
|
|
2570
|
+
}
|
|
2571
|
+
// Error: Must provide either model_id or filter
|
|
2572
|
+
return {
|
|
2573
|
+
success: false,
|
|
2574
|
+
error: 'Must provide either model_id (for single update) or filter (for bulk update), but not both'
|
|
2575
|
+
};
|
|
2576
|
+
}
|
|
2577
|
+
catch (error) {
|
|
2578
|
+
const errorMessage = error.response?.data?.error?.message
|
|
2579
|
+
|| error.response?.data?.message
|
|
2580
|
+
|| error.message
|
|
2581
|
+
|| 'An unexpected error occurred while updating pricing';
|
|
2582
|
+
console.error('[updateModelPricing] Error:', errorMessage, error);
|
|
2583
|
+
return {
|
|
2584
|
+
success: false,
|
|
2585
|
+
error: errorMessage
|
|
2586
|
+
};
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
/**
|
|
2590
|
+
* Helper: Update pricing for a single model
|
|
2591
|
+
*/
|
|
2592
|
+
async updateSingleModelPricing(model_id, input_tokens_per_1k, output_tokens_per_1k, cached_input_tokens_per_1k, reasoning_tokens_per_1k) {
|
|
2593
|
+
// Build request body
|
|
2594
|
+
const body = {
|
|
2595
|
+
price_per_1k_input_tokens: input_tokens_per_1k,
|
|
2596
|
+
price_per_1k_output_tokens: output_tokens_per_1k
|
|
2597
|
+
};
|
|
2598
|
+
// Add optional pricing
|
|
2599
|
+
if (typeof cached_input_tokens_per_1k === 'number') {
|
|
2600
|
+
body.price_per_1k_cached_input_tokens = cached_input_tokens_per_1k;
|
|
2601
|
+
}
|
|
2602
|
+
if (typeof reasoning_tokens_per_1k === 'number') {
|
|
2603
|
+
body.price_per_1k_reasoning_tokens = reasoning_tokens_per_1k;
|
|
2604
|
+
}
|
|
2605
|
+
const response = await this.client.patch(`/api/models/${encodeURIComponent(model_id)}/pricing`, body);
|
|
2606
|
+
if (!response.data.success) {
|
|
2607
|
+
return {
|
|
2608
|
+
success: false,
|
|
2609
|
+
error: response.data.error?.message || 'Failed to update model pricing'
|
|
2610
|
+
};
|
|
2611
|
+
}
|
|
2612
|
+
const model = response.data.model;
|
|
2613
|
+
// Calculate per-million prices for display
|
|
2614
|
+
const inputPerMillion = (input_tokens_per_1k * 1000).toFixed(4);
|
|
2615
|
+
const outputPerMillion = (output_tokens_per_1k * 1000).toFixed(4);
|
|
2616
|
+
const pricingLines = [
|
|
2617
|
+
` Input Tokens: $${inputPerMillion}/M tokens ($${input_tokens_per_1k}/1K)`,
|
|
2618
|
+
` Output Tokens: $${outputPerMillion}/M tokens ($${output_tokens_per_1k}/1K)`
|
|
2619
|
+
];
|
|
2620
|
+
if (cached_input_tokens_per_1k !== undefined) {
|
|
2621
|
+
const cachedPerMillion = (cached_input_tokens_per_1k * 1000).toFixed(4);
|
|
2622
|
+
pricingLines.push(` Cached Input: $${cachedPerMillion}/M tokens ($${cached_input_tokens_per_1k}/1K)`);
|
|
2623
|
+
}
|
|
2624
|
+
if (reasoning_tokens_per_1k !== undefined) {
|
|
2625
|
+
const reasoningPerMillion = (reasoning_tokens_per_1k * 1000).toFixed(4);
|
|
2626
|
+
pricingLines.push(` Reasoning Tokens: $${reasoningPerMillion}/M tokens ($${reasoning_tokens_per_1k}/1K)`);
|
|
2627
|
+
}
|
|
2628
|
+
return {
|
|
2629
|
+
success: true,
|
|
2630
|
+
output: `✅ Model pricing updated successfully!\n\n` +
|
|
2631
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` +
|
|
2632
|
+
`💰 Pricing Details:\n` +
|
|
2633
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` +
|
|
2634
|
+
` Model: ${model.model_name || model_id}\n` +
|
|
2635
|
+
` Model ID: ${model_id}\n` +
|
|
2636
|
+
` Provider: ${model.provider?.name || 'Unknown'}\n\n` +
|
|
2637
|
+
pricingLines.join('\n') + '\n\n' +
|
|
2638
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n` +
|
|
2639
|
+
`Updated: ${new Date(model.updated_at).toLocaleString()}`
|
|
2640
|
+
};
|
|
2641
|
+
}
|
|
2642
|
+
/**
|
|
2643
|
+
* Publish or unpublish a model (set access level)
|
|
2644
|
+
*/
|
|
2645
|
+
async publishModel(args) {
|
|
2646
|
+
try {
|
|
2647
|
+
const { model_id, filter, access_level, auto_confirm = false, source_access_level } = args;
|
|
2648
|
+
if (!access_level) {
|
|
2649
|
+
return {
|
|
2650
|
+
success: false,
|
|
2651
|
+
error: 'access_level is required (must be "public", "private", or "disabled")'
|
|
2652
|
+
};
|
|
2653
|
+
}
|
|
2654
|
+
// Validate access level
|
|
2655
|
+
const validLevels = ['public', 'private', 'disabled'];
|
|
2656
|
+
if (!validLevels.includes(access_level)) {
|
|
2657
|
+
return {
|
|
2658
|
+
success: false,
|
|
2659
|
+
error: `Invalid access_level: "${access_level}". Must be one of: ${validLevels.join(', ')}`
|
|
2660
|
+
};
|
|
2661
|
+
}
|
|
2662
|
+
// Check mutually exclusive parameters
|
|
2663
|
+
if (model_id && filter) {
|
|
2664
|
+
return {
|
|
2665
|
+
success: false,
|
|
2666
|
+
error: 'Cannot specify both model_id and filter. Use model_id for single update or filter for bulk update.'
|
|
2667
|
+
};
|
|
2668
|
+
}
|
|
2669
|
+
if (!model_id && !filter) {
|
|
2670
|
+
return {
|
|
2671
|
+
success: false,
|
|
2672
|
+
error: 'Either model_id or filter is required. Use model_id for single update or filter for bulk update.'
|
|
2673
|
+
};
|
|
2674
|
+
}
|
|
2675
|
+
// SINGLE MODEL MODE
|
|
2676
|
+
if (model_id) {
|
|
2677
|
+
return await this.updateSingleModelAccess(model_id, access_level);
|
|
2678
|
+
}
|
|
2679
|
+
// BULK MODE - Filter-based operation
|
|
2680
|
+
// Step 1: Determine source access level based on target
|
|
2681
|
+
// When unpublishing -> fetch public models
|
|
2682
|
+
// When publishing -> fetch private models
|
|
2683
|
+
// When disabling -> can fetch any (or use source_access_level if provided)
|
|
2684
|
+
let effectiveSourceLevel = source_access_level;
|
|
2685
|
+
if (!effectiveSourceLevel) {
|
|
2686
|
+
if (access_level === 'private') {
|
|
2687
|
+
// Unpublishing: fetch currently public models
|
|
2688
|
+
effectiveSourceLevel = 'public';
|
|
2689
|
+
}
|
|
2690
|
+
else if (access_level === 'organization') {
|
|
2691
|
+
// Publishing: fetch currently private models
|
|
2692
|
+
effectiveSourceLevel = 'private';
|
|
2693
|
+
}
|
|
2694
|
+
else if (access_level === 'disabled') {
|
|
2695
|
+
// Disabling: can disable any active model (public or private)
|
|
2696
|
+
// Don't set effectiveSourceLevel, fetch all
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
// Step 2: Fetch models with correct access level filter
|
|
2700
|
+
const fetchParams = { limit: 1000 }; // High limit for bulk operations
|
|
2701
|
+
if (effectiveSourceLevel) {
|
|
2702
|
+
fetchParams.access_level = effectiveSourceLevel;
|
|
2703
|
+
}
|
|
2704
|
+
const listResponse = await this.client.get('/api/models', { params: fetchParams });
|
|
2705
|
+
if (!listResponse.data.success) {
|
|
2706
|
+
return {
|
|
2707
|
+
success: false,
|
|
2708
|
+
error: 'Failed to fetch models for filtering'
|
|
2709
|
+
};
|
|
2710
|
+
}
|
|
2711
|
+
let allModels = listResponse.data.models || [];
|
|
2712
|
+
// Step 3: Apply keyword filter (same logic as UI)
|
|
2713
|
+
const keywords = filter.trim().split(/\s+/).filter((k) => k.length > 0).map((k) => k.toLowerCase());
|
|
2714
|
+
const matchedModels = allModels.filter((m) => {
|
|
2715
|
+
const modelId = (m.model_id || '').toLowerCase();
|
|
2716
|
+
const modelName = (m.model_name || '').toLowerCase();
|
|
2717
|
+
// All keywords must match (AND condition)
|
|
2718
|
+
return keywords.every((keyword) => {
|
|
2719
|
+
return modelId.includes(keyword) || modelName.includes(keyword);
|
|
2720
|
+
});
|
|
2721
|
+
});
|
|
2722
|
+
if (matchedModels.length === 0) {
|
|
2723
|
+
const actionHint = access_level === 'private'
|
|
2724
|
+
? '(only searching in your public models)'
|
|
2725
|
+
: access_level === 'organization'
|
|
2726
|
+
? '(only searching in your private models)'
|
|
2727
|
+
: '';
|
|
2728
|
+
return {
|
|
2729
|
+
success: true,
|
|
2730
|
+
output: `🔍 No models matched filter: "${filter}" ${actionHint}\n\n` +
|
|
2731
|
+
`Tip: Use space-separated keywords for AND matching.\n` +
|
|
2732
|
+
`Example: "groq llama" matches models with BOTH keywords.`
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2735
|
+
// Step 3: Show matched models and ask for confirmation (unless auto_confirm)
|
|
2736
|
+
const formatAccessLevel = (level) => {
|
|
2737
|
+
switch (level) {
|
|
2738
|
+
case 'public': return '🌍 Public';
|
|
2739
|
+
case 'private': return '🔒 Private';
|
|
2740
|
+
case 'disabled': return '🚫 Disabled';
|
|
2741
|
+
default: return level;
|
|
2742
|
+
}
|
|
2743
|
+
};
|
|
2744
|
+
const modelList = matchedModels.map((m, idx) => {
|
|
2745
|
+
return `${idx + 1}. ${m.model_name || m.model_id}\n` +
|
|
2746
|
+
` ID: ${m.id}\n` +
|
|
2747
|
+
` Current: ${formatAccessLevel(m.access_level)} → Target: ${formatAccessLevel(access_level)}`;
|
|
2748
|
+
}).join('\n\n');
|
|
2749
|
+
if (!auto_confirm) {
|
|
2750
|
+
// Return matched models for user confirmation
|
|
2751
|
+
const actionVerb = access_level === 'organization' ? 'publish' : access_level === 'private' ? 'delist/unpublish' : 'disable';
|
|
2752
|
+
return {
|
|
2753
|
+
success: true,
|
|
2754
|
+
output: `🔍 Found ${matchedModels.length} model(s) matching filter: "${filter}"\n\n` +
|
|
2755
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` +
|
|
2756
|
+
`📋 Models to ${actionVerb}:\n` +
|
|
2757
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n` +
|
|
2758
|
+
modelList + '\n\n' +
|
|
2759
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n` +
|
|
2760
|
+
`⚠️ To proceed, please confirm by calling this tool again with:\n` +
|
|
2761
|
+
` auto_confirm: true\n\n` +
|
|
2762
|
+
`Or cancel by not making the call.`
|
|
2763
|
+
};
|
|
2764
|
+
}
|
|
2765
|
+
// Step 4: Update all matched models with rate limiting
|
|
2766
|
+
const results = [];
|
|
2767
|
+
const errors = [];
|
|
2768
|
+
// Helper function to delay between requests to avoid rate limits
|
|
2769
|
+
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
2770
|
+
for (let i = 0; i < matchedModels.length; i++) {
|
|
2771
|
+
const model = matchedModels[i];
|
|
2772
|
+
try {
|
|
2773
|
+
const response = await this.client.patch(`/api/models/${encodeURIComponent(model.id)}/access`, {
|
|
2774
|
+
access_level
|
|
2775
|
+
});
|
|
2776
|
+
if (response.data.success) {
|
|
2777
|
+
results.push({
|
|
2778
|
+
id: model.id,
|
|
2779
|
+
name: model.model_name || model.model_id,
|
|
2780
|
+
success: true
|
|
2781
|
+
});
|
|
2782
|
+
}
|
|
2783
|
+
else {
|
|
2784
|
+
errors.push({
|
|
2785
|
+
id: model.id,
|
|
2786
|
+
name: model.model_name || model.model_id,
|
|
2787
|
+
error: response.data.error?.message || 'Unknown error'
|
|
2788
|
+
});
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
catch (err) {
|
|
2792
|
+
// Check if it's a rate limit error
|
|
2793
|
+
if (err.response?.status === 429) {
|
|
2794
|
+
errors.push({
|
|
2795
|
+
id: model.id,
|
|
2796
|
+
name: model.model_name || model.model_id,
|
|
2797
|
+
error: 'Rate limit exceeded - try again with fewer models or wait a moment'
|
|
2798
|
+
});
|
|
2799
|
+
// Stop processing further to avoid more rate limit errors
|
|
2800
|
+
break;
|
|
2801
|
+
}
|
|
2802
|
+
else {
|
|
2803
|
+
errors.push({
|
|
2804
|
+
id: model.id,
|
|
2805
|
+
name: model.model_name || model.model_id,
|
|
2806
|
+
error: err.response?.data?.error?.message || err.message
|
|
2807
|
+
});
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
// Add delay between requests to avoid rate limiting (200ms per request)
|
|
2811
|
+
// Skip delay for last item
|
|
2812
|
+
if (i < matchedModels.length - 1) {
|
|
2813
|
+
await delay(200);
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
// Step 5: Format results
|
|
2817
|
+
const actionVerb = access_level === 'organization' ? 'published' : access_level === 'private' ? 'delisted' : 'disabled';
|
|
2818
|
+
let output = `✅ Bulk update completed!\n\n`;
|
|
2819
|
+
if (results.length > 0) {
|
|
2820
|
+
output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
|
|
2821
|
+
output += `✅ Successfully ${actionVerb} (${results.length} model${results.length > 1 ? 's' : ''}):\n`;
|
|
2822
|
+
output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`;
|
|
2823
|
+
results.forEach((r, idx) => {
|
|
2824
|
+
output += `${idx + 1}. ${r.name}\n`;
|
|
2825
|
+
});
|
|
2826
|
+
output += '\n';
|
|
2827
|
+
}
|
|
2828
|
+
if (errors.length > 0) {
|
|
2829
|
+
output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
|
|
2830
|
+
output += `❌ Failed (${errors.length} model${errors.length > 1 ? 's' : ''}):\n`;
|
|
2831
|
+
output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`;
|
|
2832
|
+
errors.forEach((e, idx) => {
|
|
2833
|
+
output += `${idx + 1}. ${e.name}\n Error: ${e.error}\n\n`;
|
|
2834
|
+
});
|
|
2835
|
+
}
|
|
2836
|
+
return {
|
|
2837
|
+
success: true,
|
|
2838
|
+
output
|
|
2839
|
+
};
|
|
2840
|
+
}
|
|
2841
|
+
catch (error) {
|
|
2842
|
+
return {
|
|
2843
|
+
success: false,
|
|
2844
|
+
error: error.response?.data?.error?.message || error.message
|
|
2845
|
+
};
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
async updateSingleModelAccess(model_id, access_level) {
|
|
2849
|
+
try {
|
|
2850
|
+
const response = await this.client.patch(`/api/models/${encodeURIComponent(model_id)}/access`, {
|
|
2851
|
+
access_level
|
|
2852
|
+
});
|
|
2853
|
+
if (!response.data.success) {
|
|
2854
|
+
return {
|
|
2855
|
+
success: false,
|
|
2856
|
+
error: response.data.error?.message || 'Failed to update model access level'
|
|
2857
|
+
};
|
|
2858
|
+
}
|
|
2859
|
+
const model = response.data.data;
|
|
2860
|
+
// Format access level description
|
|
2861
|
+
let accessDescription = '';
|
|
2862
|
+
let statusEmoji = '';
|
|
2863
|
+
switch (access_level) {
|
|
2864
|
+
case 'public':
|
|
2865
|
+
statusEmoji = '🌍';
|
|
2866
|
+
accessDescription = 'Public - Available to all marketplace users';
|
|
2867
|
+
break;
|
|
2868
|
+
case 'private':
|
|
2869
|
+
statusEmoji = '🔒';
|
|
2870
|
+
accessDescription = 'Private - Only you can use this model';
|
|
2871
|
+
break;
|
|
2872
|
+
case 'disabled':
|
|
2873
|
+
statusEmoji = '🚫';
|
|
2874
|
+
accessDescription = 'Disabled - Not available to anyone';
|
|
2875
|
+
break;
|
|
2876
|
+
}
|
|
2877
|
+
return {
|
|
2878
|
+
success: true,
|
|
2879
|
+
output: `✅ ${model.message || 'Model access level updated successfully!'}\n\n` +
|
|
2880
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` +
|
|
2881
|
+
`${statusEmoji} Model Access Settings:\n` +
|
|
2882
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` +
|
|
2883
|
+
` Model: ${model.display_name || model.category_key || model_id}\n` +
|
|
2884
|
+
` Model ID: ${model_id}\n\n` +
|
|
2885
|
+
` Access Level: ${access_level.toUpperCase()}\n` +
|
|
2886
|
+
` Description: ${accessDescription}\n\n` +
|
|
2887
|
+
`━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n` +
|
|
2888
|
+
(access_level === 'organization'
|
|
2889
|
+
? `ℹ️ Your model is now listed in the marketplace and available for\n` +
|
|
2890
|
+
` other users to consume. You'll earn revenue when others use it.\n\n`
|
|
2891
|
+
: access_level === 'private'
|
|
2892
|
+
? `ℹ️ Your model is now private and only accessible to you.\n\n`
|
|
2893
|
+
: `ℹ️ Your model is now disabled and not available for use.\n\n`) +
|
|
2894
|
+
`Updated: ${new Date(model.updated_at).toLocaleString()}`
|
|
2895
|
+
};
|
|
2896
|
+
}
|
|
2897
|
+
catch (error) {
|
|
2898
|
+
return {
|
|
2899
|
+
success: false,
|
|
2900
|
+
error: error.response?.data?.error?.message || error.message
|
|
2901
|
+
};
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
// ========================================================================
|
|
2905
|
+
// WORKFLOW QUERY TOOL IMPLEMENTATIONS
|
|
2906
|
+
// ========================================================================
|
|
2907
|
+
/**
|
|
2908
|
+
* Get overall status of a workflow execution
|
|
2909
|
+
*/
|
|
2910
|
+
async getWorkflowStatus(traceId) {
|
|
2911
|
+
try {
|
|
2912
|
+
(0, debug_utils_1.debugLog)(`[MarketplaceTools] Getting workflow status for trace: ${traceId}`);
|
|
2913
|
+
const response = await this.client.get(`/api/agents/executions/${traceId}/trace`);
|
|
2914
|
+
if (!response.data.success) {
|
|
2915
|
+
return {
|
|
2916
|
+
success: false,
|
|
2917
|
+
error: response.data.error || 'Failed to get workflow status'
|
|
2918
|
+
};
|
|
2919
|
+
}
|
|
2920
|
+
const { workflowName, status, startedAt, completedAt, instances } = response.data;
|
|
2921
|
+
// Calculate progress
|
|
2922
|
+
const total = instances?.length || 0;
|
|
2923
|
+
const completed = instances?.filter((i) => i.status === 'completed').length || 0;
|
|
2924
|
+
const failed = instances?.filter((i) => i.status === 'failed').length || 0;
|
|
2925
|
+
const running = instances?.filter((i) => i.status === 'active' || i.status === 'running').length || 0;
|
|
2926
|
+
let statusEmoji = '⏳';
|
|
2927
|
+
if (status === 'completed')
|
|
2928
|
+
statusEmoji = '✅';
|
|
2929
|
+
else if (status === 'failed')
|
|
2930
|
+
statusEmoji = '❌';
|
|
2931
|
+
else if (status === 'running')
|
|
2932
|
+
statusEmoji = '🔄';
|
|
2933
|
+
let output = `${statusEmoji} Workflow Execution Status\n`;
|
|
2934
|
+
output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`;
|
|
2935
|
+
output += ` Workflow: ${workflowName || 'Unnamed Workflow'}\n`;
|
|
2936
|
+
output += ` Trace ID: ${traceId}\n`;
|
|
2937
|
+
output += ` Status: ${status?.toUpperCase() || 'UNKNOWN'}\n\n`;
|
|
2938
|
+
output += ` Progress: ${completed}/${total} agents completed\n`;
|
|
2939
|
+
if (running > 0)
|
|
2940
|
+
output += ` Running: ${running} agent(s)\n`;
|
|
2941
|
+
if (failed > 0)
|
|
2942
|
+
output += ` Failed: ${failed} agent(s)\n`;
|
|
2943
|
+
output += `\n`;
|
|
2944
|
+
if (startedAt)
|
|
2945
|
+
output += ` Started: ${new Date(startedAt).toLocaleString()}\n`;
|
|
2946
|
+
if (completedAt)
|
|
2947
|
+
output += ` Completed: ${new Date(completedAt).toLocaleString()}\n`;
|
|
2948
|
+
return {
|
|
2949
|
+
success: true,
|
|
2950
|
+
output
|
|
2951
|
+
};
|
|
2952
|
+
}
|
|
2953
|
+
catch (error) {
|
|
2954
|
+
return {
|
|
2955
|
+
success: false,
|
|
2956
|
+
error: error.response?.data?.error || error.message
|
|
2957
|
+
};
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2960
|
+
/**
|
|
2961
|
+
* List all instances in a workflow execution
|
|
2962
|
+
*/
|
|
2963
|
+
async listWorkflowInstances(traceId) {
|
|
2964
|
+
try {
|
|
2965
|
+
(0, debug_utils_1.debugLog)(`[MarketplaceTools] Listing workflow instances for trace: ${traceId}`);
|
|
2966
|
+
const response = await this.client.get(`/api/agents/executions/${traceId}/trace`);
|
|
2967
|
+
if (!response.data.success) {
|
|
2968
|
+
return {
|
|
2969
|
+
success: false,
|
|
2970
|
+
error: response.data.error || 'Failed to list workflow instances'
|
|
2971
|
+
};
|
|
2972
|
+
}
|
|
2973
|
+
const { workflowName, instances } = response.data;
|
|
2974
|
+
if (!instances || instances.length === 0) {
|
|
2975
|
+
return {
|
|
2976
|
+
success: true,
|
|
2977
|
+
output: 'No instances found for this workflow execution.'
|
|
2978
|
+
};
|
|
2979
|
+
}
|
|
2980
|
+
let output = `📋 Workflow Instances\n`;
|
|
2981
|
+
output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
|
|
2982
|
+
output += ` Workflow: ${workflowName || 'Unnamed Workflow'}\n`;
|
|
2983
|
+
output += ` Total Agents: ${instances.length}\n\n`;
|
|
2984
|
+
for (const instance of instances) {
|
|
2985
|
+
let statusEmoji = '⏳';
|
|
2986
|
+
if (instance.status === 'completed')
|
|
2987
|
+
statusEmoji = '✅';
|
|
2988
|
+
else if (instance.status === 'failed')
|
|
2989
|
+
statusEmoji = '❌';
|
|
2990
|
+
else if (instance.status === 'active' || instance.status === 'running')
|
|
2991
|
+
statusEmoji = '🔄';
|
|
2992
|
+
let roleEmoji = '🤖';
|
|
2993
|
+
if (instance.nodeType === 'START')
|
|
2994
|
+
roleEmoji = '🚀';
|
|
2995
|
+
else if (instance.nodeType === 'END')
|
|
2996
|
+
roleEmoji = '🏁';
|
|
2997
|
+
output += `${roleEmoji} [${instance.executionOrder}] ${instance.definitionName || instance.name}\n`;
|
|
2998
|
+
output += ` ID: ${instance.id}\n`;
|
|
2999
|
+
output += ` Role: ${instance.nodeType}\n`;
|
|
3000
|
+
output += ` Status: ${statusEmoji} ${instance.status?.toUpperCase() || 'PENDING'}\n`;
|
|
3001
|
+
if (instance.errorMessage) {
|
|
3002
|
+
output += ` Error: ${instance.errorMessage}\n`;
|
|
3003
|
+
}
|
|
3004
|
+
output += `\n`;
|
|
3005
|
+
}
|
|
3006
|
+
return {
|
|
3007
|
+
success: true,
|
|
3008
|
+
output
|
|
3009
|
+
};
|
|
3010
|
+
}
|
|
3011
|
+
catch (error) {
|
|
3012
|
+
return {
|
|
3013
|
+
success: false,
|
|
3014
|
+
error: error.response?.data?.error || error.message
|
|
3015
|
+
};
|
|
3016
|
+
}
|
|
3017
|
+
}
|
|
3018
|
+
/**
|
|
3019
|
+
* Get output from a specific agent instance
|
|
3020
|
+
*/
|
|
3021
|
+
async getInstanceOutput(instanceId) {
|
|
3022
|
+
try {
|
|
3023
|
+
(0, debug_utils_1.debugLog)(`[MarketplaceTools] Getting instance output for: ${instanceId}`);
|
|
3024
|
+
// Get the instance trace which includes sessions
|
|
3025
|
+
const traceResponse = await this.client.get(`/api/agents/instances/${instanceId}/trace`);
|
|
3026
|
+
if (!traceResponse.data?.success) {
|
|
3027
|
+
return {
|
|
3028
|
+
success: false,
|
|
3029
|
+
error: traceResponse.data?.error || 'Instance not found'
|
|
3030
|
+
};
|
|
3031
|
+
}
|
|
3032
|
+
const instance = traceResponse.data.instance;
|
|
3033
|
+
const sessions = traceResponse.data.sessions || [];
|
|
3034
|
+
if (sessions.length === 0) {
|
|
3035
|
+
return {
|
|
3036
|
+
success: true,
|
|
3037
|
+
output: `📦 Instance Output\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n` +
|
|
3038
|
+
` Instance: ${instance.name || instanceId}\n` +
|
|
3039
|
+
` Status: No sessions found - agent has not run yet.\n`
|
|
3040
|
+
};
|
|
3041
|
+
}
|
|
3042
|
+
const latestSession = sessions[0];
|
|
3043
|
+
let statusEmoji = '⏳';
|
|
3044
|
+
if (latestSession.status === 'completed')
|
|
3045
|
+
statusEmoji = '✅';
|
|
3046
|
+
else if (latestSession.status === 'failed')
|
|
3047
|
+
statusEmoji = '❌';
|
|
3048
|
+
else if (latestSession.status === 'active')
|
|
3049
|
+
statusEmoji = '🔄';
|
|
3050
|
+
let output = `📦 Instance Output\n`;
|
|
3051
|
+
output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`;
|
|
3052
|
+
output += ` Instance: ${instance.name || instanceId}\n`;
|
|
3053
|
+
output += ` Definition: ${instance.definition_name || 'Unknown'}\n`;
|
|
3054
|
+
output += ` Status: ${statusEmoji} ${latestSession.status?.toUpperCase() || 'UNKNOWN'}\n\n`;
|
|
3055
|
+
if (latestSession.error_message) {
|
|
3056
|
+
output += ` ❌ Error: ${latestSession.error_message}\n\n`;
|
|
3057
|
+
}
|
|
3058
|
+
// Try to get the assistant's response from the session
|
|
3059
|
+
if (latestSession.response) {
|
|
3060
|
+
output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
|
|
3061
|
+
output += `Response:\n`;
|
|
3062
|
+
output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`;
|
|
3063
|
+
output += latestSession.response;
|
|
3064
|
+
}
|
|
3065
|
+
else if (latestSession.metadata?.output) {
|
|
3066
|
+
output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
|
|
3067
|
+
output += `Output:\n`;
|
|
3068
|
+
output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n`;
|
|
3069
|
+
output += JSON.stringify(latestSession.metadata.output, null, 2);
|
|
3070
|
+
}
|
|
3071
|
+
else {
|
|
3072
|
+
output += ` No output available yet.\n`;
|
|
3073
|
+
}
|
|
3074
|
+
return {
|
|
3075
|
+
success: true,
|
|
3076
|
+
output
|
|
3077
|
+
};
|
|
3078
|
+
}
|
|
3079
|
+
catch (error) {
|
|
3080
|
+
return {
|
|
3081
|
+
success: false,
|
|
3082
|
+
error: error.response?.data?.error || error.message
|
|
3083
|
+
};
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
exports.MarketplaceTools = MarketplaceTools;
|
|
3088
|
+
MarketplaceTools.instance = null;
|
|
3089
|
+
//# sourceMappingURL=marketplace-tools.js.map
|