@kolbo/mcp 1.60.0 → 1.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/apps/index.js +67 -29
- package/src/tools/chat.js +9 -1
- package/src/tools/generate.js +19 -13
- package/src/tools/models.js +4 -0
- package/src/tools/projects.js +19 -6
- package/src/tools/visual_dna.js +17 -7
package/package.json
CHANGED
package/src/apps/index.js
CHANGED
|
@@ -170,7 +170,7 @@ function uiResult(uri, text, structured) {
|
|
|
170
170
|
/* ------------------------------------------------------------------ */
|
|
171
171
|
|
|
172
172
|
const ICON_TTL_MS = 10 * 60 * 1000;
|
|
173
|
-
const infoCache = new Map(); // apiBase → { at, byKey: Map<lowername,
|
|
173
|
+
const infoCache = new Map(); // apiBase → { at, byKey: Map<lowername, info>, all: info[] }
|
|
174
174
|
|
|
175
175
|
/**
|
|
176
176
|
* Resolve a Model.avatar value to an absolute URL. Avatars are bare filenames
|
|
@@ -189,11 +189,12 @@ function resolveAvatarUrl(avatar) {
|
|
|
189
189
|
return `${ICON_CDN_BASE}/${encodeURIComponent(avatar)}`;
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
async function
|
|
192
|
+
async function modelCatalog(client) {
|
|
193
193
|
const cacheKey = client.apiBase || 'default';
|
|
194
194
|
const hit = infoCache.get(cacheKey);
|
|
195
|
-
if (hit && Date.now() - hit.at < ICON_TTL_MS) return hit
|
|
195
|
+
if (hit && Date.now() - hit.at < ICON_TTL_MS) return hit;
|
|
196
196
|
const byKey = new Map();
|
|
197
|
+
const all = [];
|
|
197
198
|
try {
|
|
198
199
|
const res = await client.request('GET', '/v1/models');
|
|
199
200
|
const models = res?.models || res?.data?.models || [];
|
|
@@ -206,10 +207,17 @@ async function modelInfoMap(client) {
|
|
|
206
207
|
// `name` is the CLEAN display name ("Google TTS"); it is what widgets show.
|
|
207
208
|
// Without it the model chip fell back to whatever raw string the caller or
|
|
208
209
|
// the status endpoint supplied ("google_tts", "fal-ai/bytedance/omnihuman/v1.5").
|
|
209
|
-
|
|
210
|
+
// `types` is the catalog `type` array ("text_to_video", "img_to_video", …) —
|
|
211
|
+
// the ONLY thing that tells two same-named variants apart. See canonicalModelId.
|
|
212
|
+
const raw = m.types !== undefined ? m.types : m.type;
|
|
213
|
+
const types = (Array.isArray(raw) ? raw : [raw]).filter(Boolean).map(String);
|
|
214
|
+
const info = { icon, eta, id: m.identifier || null, name: m.name || null, types };
|
|
215
|
+
all.push(info);
|
|
210
216
|
// Display names collide across variants ("Nano Banana 2" names both the
|
|
211
217
|
// t2i model and its editing sibling) — on collision keep the model with
|
|
212
|
-
// the SHORTEST identifier (the base model), deterministically.
|
|
218
|
+
// the SHORTEST identifier (the base model), deterministically. This map is
|
|
219
|
+
// for ICONS/ETAs, where the variant doesn't matter; identifier resolution
|
|
220
|
+
// must NOT use it (that is what made "Kling 2.6 Pro" always mean the t2v one).
|
|
213
221
|
const setName = (k) => {
|
|
214
222
|
const prev = byKey.get(k);
|
|
215
223
|
if (!prev || !prev.id || (info.id && info.id.length < prev.id.length)) byKey.set(k, info);
|
|
@@ -220,11 +228,16 @@ async function modelInfoMap(client) {
|
|
|
220
228
|
} catch (_) {
|
|
221
229
|
/* fail open — widgets fall back to monogram chips, no ETA */
|
|
222
230
|
}
|
|
231
|
+
const entry = { at: Date.now(), byKey, all };
|
|
223
232
|
// Never cache an empty map: the first request in a fresh worker (typical
|
|
224
233
|
// right after a deploy restart) can fail transiently, and caching that
|
|
225
234
|
// failure blanks every model icon for the TTL window.
|
|
226
|
-
if (byKey.size > 0) infoCache.set(cacheKey,
|
|
227
|
-
return
|
|
235
|
+
if (byKey.size > 0) infoCache.set(cacheKey, entry);
|
|
236
|
+
return entry;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function modelInfoMap(client) {
|
|
240
|
+
return (await modelCatalog(client)).byKey;
|
|
228
241
|
}
|
|
229
242
|
|
|
230
243
|
/** Resolve one model's { icon, eta, name }; missing → all null. */
|
|
@@ -291,6 +304,22 @@ const AUTO_ALIASES = new Set([
|
|
|
291
304
|
'auto', 'autoselect', 'smartselect', 'kolbosmartselectrouter', 'default', 'none',
|
|
292
305
|
]);
|
|
293
306
|
|
|
307
|
+
/**
|
|
308
|
+
* Narrow several models that answer to the same string down to one identifier.
|
|
309
|
+
* The CALLING TOOL's catalog type decides: a display name like "Kling 2.6 Pro"
|
|
310
|
+
* names one model PER MODALITY (…/text-to-video and …/image-to-video), and only
|
|
311
|
+
* the caller knows which it wants. No type match (or no type given) → shortest
|
|
312
|
+
* identifier, the same deterministic tiebreak the icon map uses.
|
|
313
|
+
*/
|
|
314
|
+
function pickForType(candidates, types) {
|
|
315
|
+
if (!candidates.length) return null;
|
|
316
|
+
const typed = types.length
|
|
317
|
+
? candidates.filter((i) => i.types.some((t) => types.includes(t)))
|
|
318
|
+
: [];
|
|
319
|
+
const pool = typed.length ? typed : candidates;
|
|
320
|
+
return pool.reduce((a, b) => (b.id.length < a.id.length ? b : a)).id;
|
|
321
|
+
}
|
|
322
|
+
|
|
294
323
|
/**
|
|
295
324
|
* Lenient model-identifier resolution for LLM-supplied model args.
|
|
296
325
|
* Users say "z-image"; the real identifier is "z-image/turbo" — the backend
|
|
@@ -299,6 +328,12 @@ const AUTO_ALIASES = new Set([
|
|
|
299
328
|
* else a separator-insensitive hit ("flux-2-flash" → "flux-2/flash"); else a
|
|
300
329
|
* UNIQUE prefix match ("z-image" → "z-image/turbo").
|
|
301
330
|
*
|
|
331
|
+
* `type` is the calling tool's catalog type (a string, or an array when the
|
|
332
|
+
* tool spans several — lipsync, 3D). It is what makes resolution MODALITY-AWARE:
|
|
333
|
+
* without it, "Kling 2.6 Pro" from generate_video_from_image resolved to
|
|
334
|
+
* kling-video/v2.6/pro/text-to-video (2026-08-10), so the image-to-video
|
|
335
|
+
* pipeline submitted the TEXT-to-video endpoint and billed against it.
|
|
336
|
+
*
|
|
302
337
|
* Still unresolved: throw with the near misses named. The API answers a bad
|
|
303
338
|
* identifier with a bare INVALID_*_MODEL and no hint, which on 2026-08-09 sent
|
|
304
339
|
* an agent guessing "minimax-hailuo-3" (real id: "minimax-h3") and then
|
|
@@ -307,43 +342,46 @@ const AUTO_ALIASES = new Set([
|
|
|
307
342
|
* unchanged, so identifiers the catalog does not publish (hidden models) still
|
|
308
343
|
* reach the API and it stays the source of truth.
|
|
309
344
|
*/
|
|
310
|
-
async function canonicalModelId(client, input) {
|
|
345
|
+
async function canonicalModelId(client, input, type) {
|
|
311
346
|
if (!input || typeof input !== 'string') return input;
|
|
312
347
|
const key = input.toLowerCase().trim();
|
|
313
348
|
const want = normId(key);
|
|
314
349
|
if (!want || AUTO_ALIASES.has(want)) return input;
|
|
315
350
|
|
|
316
|
-
let
|
|
351
|
+
let all;
|
|
317
352
|
try {
|
|
318
|
-
|
|
353
|
+
all = (await modelCatalog(client)).all;
|
|
319
354
|
} catch (_) {
|
|
320
355
|
return input; // fail open — never block a generation on a catalog hiccup
|
|
321
356
|
}
|
|
322
|
-
|
|
357
|
+
const models = (all || []).filter((i) => i.id);
|
|
358
|
+
if (!models.length) return input;
|
|
359
|
+
|
|
360
|
+
const types = (Array.isArray(type) ? type : [type]).filter(Boolean);
|
|
361
|
+
const dashed = key.replace(/\s+/g, '-');
|
|
323
362
|
|
|
324
363
|
// 1. exact name / identifier hit
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
const
|
|
339
|
-
|
|
340
|
-
if (prefixed.size === 1) return [...prefixed][0];
|
|
364
|
+
const exact = pickForType(models.filter((i) => [i.id, i.name].some(
|
|
365
|
+
(k) => k && (k.toLowerCase() === key || k.toLowerCase() === dashed)
|
|
366
|
+
)), types);
|
|
367
|
+
if (exact) return exact;
|
|
368
|
+
|
|
369
|
+
// 2. separator-insensitive exact ("flux-2-flash" → "flux-2/flash")
|
|
370
|
+
const loose = pickForType(models.filter((i) => normId(i.id) === want || normId(i.name) === want), types);
|
|
371
|
+
if (loose) return loose;
|
|
372
|
+
|
|
373
|
+
// 3. unique prefix ("z-image" → "z-image/turbo") — the modality filter runs
|
|
374
|
+
// FIRST, so a stem shared by a t2v/i2v pair is no longer ambiguous.
|
|
375
|
+
const prefixed = models.filter((i) => normId(i.id).startsWith(want) || normId(i.name).startsWith(want));
|
|
376
|
+
const narrowed = types.length ? prefixed.filter((i) => i.types.some((t) => types.includes(t))) : [];
|
|
377
|
+
const ids = new Set((narrowed.length ? narrowed : prefixed).map((i) => i.id));
|
|
378
|
+
if (ids.size === 1) return [...ids][0];
|
|
341
379
|
|
|
342
380
|
// 4. unknown — name the near misses instead of dead-ending at the API.
|
|
343
381
|
const stem = normId(key.split(/[\s._/-]+/).filter(Boolean)[0] || key);
|
|
344
382
|
const near = [...new Set(
|
|
345
|
-
|
|
346
|
-
.filter((i) =>
|
|
383
|
+
models
|
|
384
|
+
.filter((i) => stem && (normId(i.id).startsWith(stem) || normId(i.name).startsWith(stem)))
|
|
347
385
|
.map((i) => (i.name ? `${i.id} (${i.name})` : i.id))
|
|
348
386
|
)].sort().slice(0, 12);
|
|
349
387
|
if (!near.length) return input;
|
package/src/tools/chat.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
const { z } = require('zod');
|
|
7
7
|
const { pollOrTimedOut, creditFields, projectIdField } = require('./_shared');
|
|
8
|
+
const { canonicalModelId } = require('../apps');
|
|
8
9
|
|
|
9
10
|
function registerChatTools(server, client) {
|
|
10
11
|
// ─── chat_send_message ─────────────────────────────────────
|
|
@@ -13,7 +14,7 @@ function registerChatTools(server, client) {
|
|
|
13
14
|
'Send a chat message to Kolbo AI. Starts a new conversation (omit session_id) or continues an existing one. Returns the assistant response when complete. Supports image/video/audio analysis via media_urls — pass public URLs and the model auto-routes to a vision-capable model (e.g. Gemini) when media is detected. Supports web search and deep think modes.',
|
|
14
15
|
{
|
|
15
16
|
message: z.string().describe('The user message to send'),
|
|
16
|
-
model: z.string().optional().describe('Model identifier
|
|
17
|
+
model: z.string().optional().describe('Model identifier from list_models type="text". Identifiers resolve leniently, so the DISPLAY NAME that list_models shows works too ("Grok 4.5" → its identifier). Do NOT hardcode an id you have not seen in list_models — the text catalog turns over fast. Prefer passing a SPECIFIC model — omitting falls back to Smart Select auto-routing, which we avoid unless the user explicitly asks for auto-pick. Exception: when media_urls contains video or audio, omitting is fine — routing goes to a Gemini vision model regardless of this field.'),
|
|
17
18
|
session_id: z.string().optional().describe('Existing chat session ID to continue. Omit to start a new conversation.'),
|
|
18
19
|
system_prompt: z.string().optional().describe('System prompt for the conversation. Only applied when creating a new session.'),
|
|
19
20
|
web_search: z.boolean().optional().describe('Enable web search for this message. Default: false'),
|
|
@@ -23,6 +24,13 @@ function registerChatTools(server, client) {
|
|
|
23
24
|
project_id: projectIdField
|
|
24
25
|
},
|
|
25
26
|
async ({ message, model, session_id, system_prompt, web_search, deep_think, enhance_prompt = false, media_urls, project_id }) => {
|
|
27
|
+
// Every generate_* tool resolves its model this way; chat was the one
|
|
28
|
+
// `model` arg that went straight to the API, which has no fuzzy matching.
|
|
29
|
+
// So the display names list_models hands back ("Claude Fable 5") came
|
|
30
|
+
// back as a bare `Model not found: Claude Fable 5 [MODEL_NOT_FOUND]` —
|
|
31
|
+
// discovery had no path to use.
|
|
32
|
+
model = await canonicalModelId(client, model, 'text'); // lenient id resolution ("Grok 4.5" → its identifier)
|
|
33
|
+
|
|
26
34
|
const gen = await client.post('/v1/chat', {
|
|
27
35
|
message,
|
|
28
36
|
model,
|
package/src/tools/generate.js
CHANGED
|
@@ -139,7 +139,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
139
139
|
},
|
|
140
140
|
async ({ prompt, prompts, model, aspect_ratio, enhance_prompt = false, num_images, reference_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, quality, preset_id, cinematic, skip_color_palette, project_id }) => {
|
|
141
141
|
if (!prompt && !(prompts && prompts.length)) throw new Error('Provide prompt or prompts');
|
|
142
|
-
model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
142
|
+
model = await canonicalModelId(client, model, 'text_to_img'); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
143
143
|
const shared = {
|
|
144
144
|
model, aspect_ratio, enhance_prompt,
|
|
145
145
|
reference_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, quality, preset_id, cinematic, skip_color_palette, project_id
|
|
@@ -210,7 +210,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
210
210
|
project_id: projectIdField
|
|
211
211
|
},
|
|
212
212
|
async ({ prompt, model, source_images, aspect_ratio, enhance_prompt = false, num_images, visual_dna_ids, moodboard_id, enable_web_search, resolution, cinematic, skip_color_palette, project_id }) => {
|
|
213
|
-
model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
213
|
+
model = await canonicalModelId(client, model, 'image_editing'); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
214
214
|
const gen = await client.post('/v1/generate/image-edit', {
|
|
215
215
|
prompt, model, source_images, aspect_ratio, enhance_prompt, num_images,
|
|
216
216
|
visual_dna_ids, moodboard_id, enable_web_search, resolution, cinematic, skip_color_palette, project_id
|
|
@@ -270,7 +270,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
270
270
|
project_id: projectIdField
|
|
271
271
|
},
|
|
272
272
|
async ({ prompt, scene_count, model, aspect_ratio, workflow_type, duration, enhance_prompt = false, reference_images, visual_dna_ids, moodboard_id, moodboard_ids, resolution, project_id }) => {
|
|
273
|
-
model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
273
|
+
model = await canonicalModelId(client, model, workflow_type === 'video' ? 'text_to_video' : 'text_to_img'); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
274
274
|
const gen = await client.post('/v1/generate/creative-director', {
|
|
275
275
|
prompt, scene_count, model, aspect_ratio, workflow_type, duration,
|
|
276
276
|
enhance_prompt, reference_images, visual_dna_ids, moodboard_id, moodboard_ids, resolution, project_id
|
|
@@ -423,7 +423,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
423
423
|
},
|
|
424
424
|
async ({ prompt, prompts, model, aspect_ratio, duration, enhance_prompt = false, reference_images, resolution, preset_id, sound_enabled, skip_color_palette, project_id }) => {
|
|
425
425
|
if (!prompt && !(prompts && prompts.length)) throw new Error('Provide prompt or prompts');
|
|
426
|
-
model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
426
|
+
model = await canonicalModelId(client, model, 'text_to_video'); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
427
427
|
const shared = {
|
|
428
428
|
model, aspect_ratio, duration, enhance_prompt, reference_images, resolution, preset_id, sound_enabled, skip_color_palette, project_id
|
|
429
429
|
};
|
|
@@ -495,7 +495,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
495
495
|
project_id: projectIdField
|
|
496
496
|
},
|
|
497
497
|
async ({ image_url, prompt, model, aspect_ratio, duration, enhance_prompt = false, visual_dna_ids, resolution, sound_enabled, skip_color_palette, project_id }) => {
|
|
498
|
-
model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
498
|
+
model = await canonicalModelId(client, model, 'img_to_video'); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
499
499
|
const gen = await client.post('/v1/generate/video/from-image', {
|
|
500
500
|
image_url, prompt, model, aspect_ratio, duration, enhance_prompt, visual_dna_ids, resolution, sound_enabled, skip_color_palette, project_id
|
|
501
501
|
});
|
|
@@ -558,7 +558,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
558
558
|
project_id: projectIdField
|
|
559
559
|
},
|
|
560
560
|
async ({ prompt, model, style, title, instrumental, lyrics, vocal_gender, negative_tags, duration_seconds, enhance_prompt = false, preset_id, style_weight, weirdness, audio_weight, persona_id, use_composition_plan, singing_dna_id, singing_voice_id, project_id }) => {
|
|
561
|
-
model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
561
|
+
model = await canonicalModelId(client, model, 'music_gen'); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
562
562
|
const gen = await client.post('/v1/generate/music', {
|
|
563
563
|
prompt, model, style, title, instrumental, lyrics, vocal_gender, negative_tags,
|
|
564
564
|
duration_seconds, enhance_prompt, preset_id,
|
|
@@ -630,7 +630,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
630
630
|
project_id: projectIdField
|
|
631
631
|
},
|
|
632
632
|
async ({ text, voice, model, language, style_instructions, selected_style, emotion, speaking_speed, similarity_boost, style, use_speaker_boost, variance, tempo, promptBoost, seed, accentControl, voiceTitle, minimax_pitch, minimax_vol, minimax_intensity, minimax_timbre, project_id }) => {
|
|
633
|
-
model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
633
|
+
model = await canonicalModelId(client, model, 'text_to_speech'); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
634
634
|
// Resolve the requested voice against the REAL catalog (cached) so the card
|
|
635
635
|
// can show its display name + portrait instead of a raw id, and so an id
|
|
636
636
|
// that does not exist is reported instead of rendering silently: Google
|
|
@@ -704,7 +704,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
704
704
|
project_id: projectIdField
|
|
705
705
|
},
|
|
706
706
|
async ({ prompt, model, duration, prompt_influence, cfg_strength, sound_loop, sound_tempo, sound_key, seed_voice, seed_speed, seed_volume, seed_pitch, seed_reference_audio_urls, seed_reference_image_url, project_id }) => {
|
|
707
|
-
model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
707
|
+
model = await canonicalModelId(client, model, 'text_to_sound'); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
708
708
|
const gen = await client.post('/v1/generate/sound', {
|
|
709
709
|
prompt, model, duration, prompt_influence,
|
|
710
710
|
cfg_strength, sound_loop, sound_tempo, sound_key,
|
|
@@ -920,7 +920,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
920
920
|
project_id: projectIdField
|
|
921
921
|
},
|
|
922
922
|
async ({ prompt, model, reference_images, reference_videos, reference_audio_urls, audio_url, files, duration, aspect_ratio, motion, preset_id, enhance_prompt = false, visual_dna_ids, resolution, keyframes, project_id }) => {
|
|
923
|
-
model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
923
|
+
model = await canonicalModelId(client, model, 'elements'); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
924
924
|
if (!prompt) throw new Error('prompt is required');
|
|
925
925
|
|
|
926
926
|
let startResponse;
|
|
@@ -1001,7 +1001,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1001
1001
|
project_id: projectIdField
|
|
1002
1002
|
},
|
|
1003
1003
|
async ({ first_frame_url, last_frame_url, first_frame, last_frame, prompt, model, duration, aspect_ratio, enhance_prompt = false, visual_dna_ids, resolution, project_id }) => {
|
|
1004
|
-
model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
1004
|
+
model = await canonicalModelId(client, model, 'firstlastgenerations'); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
1005
1005
|
const urlMode = first_frame_url && last_frame_url;
|
|
1006
1006
|
const fileMode = first_frame && last_frame;
|
|
1007
1007
|
if (!urlMode && !fileMode) {
|
|
@@ -1091,7 +1091,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1091
1091
|
project_id: projectIdField
|
|
1092
1092
|
},
|
|
1093
1093
|
async ({ source, audio, text_prompt, model, bounding_box_target, sync_mode, model_mode, emotion, temperature, occlusion_detection_enabled, active_speaker_detection, project_id }) => {
|
|
1094
|
-
model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
1094
|
+
model = await canonicalModelId(client, model, ['lipsync-image', 'lipsync-video']); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
1095
1095
|
if (!source) throw new Error('source is required (URL or absolute local path to image/video)');
|
|
1096
1096
|
if (!audio) throw new Error('audio is required (URL or absolute local path to audio file)');
|
|
1097
1097
|
|
|
@@ -1212,7 +1212,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1212
1212
|
project_id: projectIdField
|
|
1213
1213
|
},
|
|
1214
1214
|
async ({ source_video, prompt, model, aspect_ratio, duration, enhance_prompt = false, visual_dna_ids, resolution, reference_images, reference_videos, elements, preset, source_language, translation_language, srt_content, srt_file_url, vocabulary, customization, project_id }) => {
|
|
1215
|
-
model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
1215
|
+
model = await canonicalModelId(client, model, 'video_to_video'); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
1216
1216
|
if (!source_video) throw new Error('source_video is required');
|
|
1217
1217
|
|
|
1218
1218
|
const isUrl = /^https?:\/\//i.test(source_video);
|
|
@@ -1371,7 +1371,7 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1371
1371
|
project_id: projectIdField
|
|
1372
1372
|
},
|
|
1373
1373
|
async ({ prompt, reference_images, mode, texture_prompt, model, topology, target_polycount, enable_tpose, enable_pbr, project_id }) => {
|
|
1374
|
-
model = await canonicalModelId(client, model); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
1374
|
+
model = await canonicalModelId(client, model, ['3d_text_to_model', '3d_image_to_model', '3d_multi_image_to_model', '3d_world']); // lenient id resolution ("z-image" → "z-image/turbo")
|
|
1375
1375
|
if (!prompt && !(reference_images && reference_images.length > 0)) {
|
|
1376
1376
|
throw new Error('Provide prompt (text mode) or reference_images (single/multi mode)');
|
|
1377
1377
|
}
|
|
@@ -1514,6 +1514,9 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1514
1514
|
zoom_out_percentage, expand_left, expand_right, expand_top, expand_bottom,
|
|
1515
1515
|
project_id
|
|
1516
1516
|
}) => {
|
|
1517
|
+
// No `type` argument: these are operation-routed tools (upscale / reframe /
|
|
1518
|
+
// removebg / …), each operation with its own model family — there is no single
|
|
1519
|
+
// catalog type to disambiguate against.
|
|
1517
1520
|
model = await canonicalModelId(client, model);
|
|
1518
1521
|
|
|
1519
1522
|
// Basic validation
|
|
@@ -1667,6 +1670,9 @@ function registerGenerateTools(server, client, options = {}) {
|
|
|
1667
1670
|
start_time,
|
|
1668
1671
|
project_id
|
|
1669
1672
|
}) => {
|
|
1673
|
+
// No `type` argument: these are operation-routed tools (upscale / reframe /
|
|
1674
|
+
// removebg / …), each operation with its own model family — there is no single
|
|
1675
|
+
// catalog type to disambiguate against.
|
|
1670
1676
|
model = await canonicalModelId(client, model);
|
|
1671
1677
|
|
|
1672
1678
|
// Validation
|
package/src/tools/models.js
CHANGED
|
@@ -67,6 +67,10 @@ function buildCatalogStructured(models, type, compact) {
|
|
|
67
67
|
if (g.models.length >= 6) continue; // curated cap — full list lives in the text payload
|
|
68
68
|
g.models.push({
|
|
69
69
|
name: m.name,
|
|
70
|
+
// The widget renders `name`; the AGENT reads the same rows (hosts hand it
|
|
71
|
+
// structuredContent). Without the identifier the default call was a dead
|
|
72
|
+
// end — it named six models and gave no way to pass any of them on.
|
|
73
|
+
identifier: m.identifier,
|
|
70
74
|
icon: resolveAvatarUrl(m.avatar),
|
|
71
75
|
description: String(m.smartSelect_StrengthsSummary || m.summary || m.description || '').slice(0, 90),
|
|
72
76
|
chips: modelChips(m),
|
package/src/tools/projects.js
CHANGED
|
@@ -12,21 +12,34 @@ function registerProjectTools(server, client, options = {}) {
|
|
|
12
12
|
// ─── list_projects ─────────────────────────────────────────
|
|
13
13
|
server.tool(
|
|
14
14
|
'list_projects',
|
|
15
|
-
'List the user\'s platform projects (owned + shared with edit/full/owner permission). Use this to resolve a project NAME the user mentioned ("put this in my Acme Campaign project") into the project ObjectId you pass back as `project_id` on generation / chat / upload / move tools. Whenever the user mentions a project by name OR location, you MUST call this first — those tools accept only ObjectIds, not names — and then pass the resolved `project_id` on EVERY subsequent call in the conversation (it is per-call, not sticky; omitting it drops work into the default bucket). Returns id, name, role, and
|
|
16
|
-
{
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
'List the user\'s platform projects (owned + shared with edit/full/owner permission). Use this to resolve a project NAME the user mentioned ("put this in my Acme Campaign project") into the project ObjectId you pass back as `project_id` on generation / chat / upload / move tools. Whenever the user mentions a project by name OR location, you MUST call this first — those tools accept only ObjectIds, not names — and then pass the resolved `project_id` on EVERY subsequent call in the conversation (it is per-call, not sticky; omitting it drops work into the default bucket). Returns id, name, role, is_default, and is_archived. The project flagged `is_default: true` is the auto-created "API Generations" bucket every SDK generation lands in when project_id is omitted. Accounts routinely have HUNDREDS of projects, so this is paginated: when you already know the name, pass `search` — it is far cheaper than listing everything. Default page size is 50; use `page` to walk the rest (`pagination.has_more` tells you when to stop). Archived projects are hidden unless you pass `include_archived: true`.',
|
|
16
|
+
{
|
|
17
|
+
search: z.string().optional().describe('Case-insensitive substring match on the project name. Use this whenever the user named a project — it turns a full listing into a one-item answer.'),
|
|
18
|
+
page: z.number().optional().describe('Page number, 1-indexed. Default: 1'),
|
|
19
|
+
limit: z.number().optional().describe('Results per page, max 200. Default: 50'),
|
|
20
|
+
include_archived: z.boolean().optional().describe('Also return archived projects. Default false — archived projects are hidden here exactly as they are in the web app.')
|
|
21
|
+
},
|
|
22
|
+
async ({ search, page, limit, include_archived }) => {
|
|
23
|
+
const params = new URLSearchParams();
|
|
24
|
+
if (search) params.set('search', search);
|
|
25
|
+
if (page) params.set('page', String(page));
|
|
26
|
+
if (limit) params.set('limit', String(limit));
|
|
27
|
+
if (include_archived) params.set('include_archived', 'true');
|
|
28
|
+
const qs = params.toString();
|
|
29
|
+
const result = await client.get(`/v1/projects${qs ? '?' + qs : ''}`);
|
|
19
30
|
const projects = (result.projects || []).map(p => ({
|
|
20
31
|
id: p.id,
|
|
21
32
|
name: p.name,
|
|
22
33
|
role: p.role,
|
|
23
34
|
is_default: !!p.is_default,
|
|
35
|
+
is_archived: !!p.is_archived,
|
|
24
36
|
open_url: buildProjectUrl(p.id, { is_default: !!p.is_default })
|
|
25
37
|
}));
|
|
26
38
|
const text = JSON.stringify({
|
|
27
39
|
projects,
|
|
28
40
|
count: projects.length,
|
|
29
|
-
|
|
41
|
+
pagination: result.pagination || null,
|
|
42
|
+
_hint: 'Pass the chosen `id` as `project_id` on any generate_* tool to drop the generation into that project. Omit project_id to use the project flagged is_default:true. `open_url` opens that project\'s media in the web app (share it with the user). If `pagination.has_more` is true there are more projects — narrow with `search` rather than paging through everything.'
|
|
30
43
|
}, null, 2);
|
|
31
44
|
|
|
32
45
|
if (ui()) {
|
|
@@ -36,7 +49,7 @@ function registerProjectTools(server, client, options = {}) {
|
|
|
36
49
|
items: projects.map(p => ({
|
|
37
50
|
id: p.id,
|
|
38
51
|
title: p.name,
|
|
39
|
-
subtitle: p.role + (p.is_default ? ' · default' : ''),
|
|
52
|
+
subtitle: p.role + (p.is_default ? ' · default' : '') + (p.is_archived ? ' · archived' : ''),
|
|
40
53
|
open_url: p.open_url,
|
|
41
54
|
use_hint: 'Use my "{TITLE}" project (project_id: {ID}) for what I do next.'
|
|
42
55
|
})),
|
package/src/tools/visual_dna.js
CHANGED
|
@@ -87,15 +87,19 @@ function registerVisualDnaTools(server, client, options = {}) {
|
|
|
87
87
|
+ 'because dumping them buries the user\'s own handful. Only pass scope="global" (optionally with '
|
|
88
88
|
+ '`collection` and `search`) when the user explicitly wants to BROWSE the preset cast — e.g. "find me a '
|
|
89
89
|
+ 'character", "show me street style models", "I need a location DNA" — and they have not named one of '
|
|
90
|
-
+ 'their own. Use scope="all" only if the user genuinely wants both at once.'
|
|
90
|
+
+ 'their own. Use scope="all" only if the user genuinely wants both at once. '
|
|
91
|
+
+ 'The response reports `total` and `_truncated`; when there are more matches than one '
|
|
92
|
+
+ 'page holds, raise `limit` or walk `page` — do not tell the user the extras do not exist.',
|
|
91
93
|
{
|
|
92
94
|
scope: z.enum(['all', 'personal', 'global', 'organization']).optional().describe('Default: "personal" — the user\'s own DNAs (plus a shared project\'s when project_id is set). "global" = the ~1000 system cast/preset DNAs, for browsing when the user needs a character and has none of their own. "organization" = org-shared. "all" = everything, rarely wanted.'),
|
|
93
|
-
search: z.string().optional().describe('Search by name, tags, or description (case-insensitive)'),
|
|
95
|
+
search: z.string().optional().describe('Search by name, tags, or description (case-insensitive). Matches at WORD STARTS, so "man" finds "Man"/"Manager" but not "woman" or "romantic". Name matches are ranked first.'),
|
|
94
96
|
collection: z.string().optional().describe('Filter global presets by collection: cast, influencers, props, locations, styles, glamour, street'),
|
|
95
97
|
tags: z.string().optional().describe('Comma-separated tags to filter by (OR logic)'),
|
|
98
|
+
page: z.number().optional().describe('Page number, 1-indexed. Default: 1. Needed to reach the global cast beyond the first page.'),
|
|
99
|
+
limit: z.number().optional().describe('Results per page, max 100. Default: 50'),
|
|
96
100
|
project_id: projectScopeReadField
|
|
97
101
|
},
|
|
98
|
-
async ({ scope, search, collection, tags, project_id } = {}) => {
|
|
102
|
+
async ({ scope, search, collection, tags, page, limit, project_id } = {}) => {
|
|
99
103
|
const params = new URLSearchParams();
|
|
100
104
|
// Default to the user's OWN DNAs. The API defaults to "all", which pulls
|
|
101
105
|
// in ~1000 global cast presets and buries the handful the user actually
|
|
@@ -106,17 +110,23 @@ function registerVisualDnaTools(server, client, options = {}) {
|
|
|
106
110
|
if (search) params.set('search', search);
|
|
107
111
|
if (collection) params.set('collection', collection);
|
|
108
112
|
if (tags) params.set('tags', tags);
|
|
113
|
+
// Always paged. Without these the ~1000-item global cast came back whole and
|
|
114
|
+
// was silently cut to the display cap, so nothing past the first screen was
|
|
115
|
+
// reachable through this tool at all.
|
|
116
|
+
params.set('page', String(page && page > 0 ? Math.floor(page) : 1));
|
|
117
|
+
params.set('limit', String(limit && limit > 0 ? Math.min(Math.floor(limit), 100) : 50));
|
|
109
118
|
if (project_id) params.set('project_id', project_id);
|
|
110
119
|
const qs = params.toString();
|
|
111
120
|
const result = await client.get(`/v1/visual-dna${qs ? '?' + qs : ''}`);
|
|
112
121
|
const dnas = result.visual_dnas || [];
|
|
122
|
+
const total = result.total != null ? result.total : (result.count || dnas.length);
|
|
113
123
|
// Full profiles measured 74,310 chars — the embedded analysis/description
|
|
114
124
|
// blobs are large and the model only needs enough to pick an id.
|
|
115
125
|
const text = compactList(dnas, {
|
|
116
126
|
fields: ['id', 'name', 'type', 'folder_id', 'tags', 'thumbnail'],
|
|
117
127
|
cap: 60,
|
|
118
|
-
total
|
|
119
|
-
note: 'Narrow with `search`, `tags`, or `collection
|
|
128
|
+
total,
|
|
129
|
+
note: 'Narrow with `search`, `tags`, or `collection`, or pass `page`/`limit` for the rest; get_visual_dna returns one in full.',
|
|
120
130
|
});
|
|
121
131
|
|
|
122
132
|
if (ui()) {
|
|
@@ -131,8 +141,8 @@ function registerVisualDnaTools(server, client, options = {}) {
|
|
|
131
141
|
media_type: 'image',
|
|
132
142
|
use_hint: 'Use Visual DNA "{TITLE}" (id: {ID}) in my next generation for character/style consistency.'
|
|
133
143
|
})),
|
|
134
|
-
total
|
|
135
|
-
has_more: dnas.length > 24
|
|
144
|
+
total,
|
|
145
|
+
has_more: result.has_more || dnas.length > 24
|
|
136
146
|
});
|
|
137
147
|
}
|
|
138
148
|
|