@decocms/mesh-sdk 1.2.1 → 1.2.3
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/README.md +10 -10
- package/package.json +7 -4
- package/src/context/index.ts +5 -1
- package/src/context/project-context.tsx +68 -29
- package/src/hooks/index.ts +10 -0
- package/src/hooks/use-collections.ts +179 -63
- package/src/hooks/use-connection.ts +50 -4
- package/src/hooks/use-mcp-client.ts +81 -11
- package/src/hooks/use-mcp-prompts.ts +16 -6
- package/src/hooks/use-mcp-resources.ts +15 -5
- package/src/hooks/use-virtual-mcp.ts +64 -0
- package/src/index.ts +119 -4
- package/src/lib/bridge-transport.test.ts +368 -0
- package/src/lib/bridge-transport.ts +6 -0
- package/src/lib/constants.test.ts +26 -0
- package/src/lib/constants.ts +193 -36
- package/src/lib/default-model.ts +281 -0
- package/src/lib/mcp-oauth.ts +139 -17
- package/src/lib/query-keys.ts +20 -4
- package/src/lib/server-client-bridge.ts +4 -0
- package/src/lib/usage.test.ts +229 -0
- package/src/lib/usage.ts +187 -0
- package/src/plugins/index.ts +15 -0
- package/src/plugins/plugin-context-provider.tsx +99 -0
- package/src/plugins/topbar-portal.tsx +118 -0
- package/src/types/ai-providers.ts +86 -0
- package/src/types/connection.ts +43 -20
- package/src/types/decopilot-events.test.ts +78 -0
- package/src/types/decopilot-events.ts +171 -0
- package/src/types/index.ts +48 -1
- package/src/types/virtual-mcp.test.ts +202 -0
- package/src/types/virtual-mcp.ts +514 -109
package/src/lib/constants.ts
CHANGED
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
* This module provides constants and factory functions for creating standard MCP connections.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
ConnectionCreateData,
|
|
10
|
+
ConnectionEntity,
|
|
11
|
+
} from "../types/connection";
|
|
9
12
|
import type { VirtualMCPEntity } from "../types/virtual-mcp";
|
|
10
13
|
|
|
11
14
|
/**
|
|
@@ -21,6 +24,10 @@ export const WellKnownOrgMCPId = {
|
|
|
21
24
|
REGISTRY: (org: string) => `${org}_registry`,
|
|
22
25
|
/** Community MCP registry */
|
|
23
26
|
COMMUNITY_REGISTRY: (org: string) => `${org}_community-registry`,
|
|
27
|
+
/** Dev Assets MCP - local file storage for development */
|
|
28
|
+
DEV_ASSETS: (org: string) => `${org}_dev-assets`,
|
|
29
|
+
/** Site Diagnostics agent (note: prefix-first format, not org-first) */
|
|
30
|
+
SITE_DIAGNOSTICS: (org: string) => `site-diagnostics_${org}`,
|
|
24
31
|
};
|
|
25
32
|
|
|
26
33
|
/**
|
|
@@ -30,6 +37,13 @@ export const WellKnownOrgMCPId = {
|
|
|
30
37
|
*/
|
|
31
38
|
export const SELF_MCP_ALIAS_ID = "self";
|
|
32
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Frontend connection ID for the dev-assets MCP endpoint.
|
|
42
|
+
* Use this constant when calling object storage tools from the frontend in dev mode.
|
|
43
|
+
* The endpoint is exposed at /mcp/dev-assets.
|
|
44
|
+
*/
|
|
45
|
+
export const DEV_ASSETS_MCP_ALIAS_ID = "dev-assets";
|
|
46
|
+
|
|
33
47
|
/**
|
|
34
48
|
* Get well-known connection definition for the Deco Store registry.
|
|
35
49
|
* This can be used by both frontend and backend to create registry connections.
|
|
@@ -44,7 +58,7 @@ export function getWellKnownRegistryConnection(
|
|
|
44
58
|
title: "Deco Store",
|
|
45
59
|
description: "Official deco MCP registry with curated integrations",
|
|
46
60
|
connection_type: "HTTP",
|
|
47
|
-
connection_url: "https://
|
|
61
|
+
connection_url: "https://studio.decocms.com/org/deco/registry/mcp",
|
|
48
62
|
icon: "https://assets.decocache.com/decocms/00ccf6c3-9e13-4517-83b0-75ab84554bb9/596364c63320075ca58483660156b6d9de9b526e.png",
|
|
49
63
|
app_name: "deco-registry",
|
|
50
64
|
app_id: null,
|
|
@@ -72,7 +86,7 @@ export function getWellKnownCommunityRegistryConnection(): ConnectionCreateData
|
|
|
72
86
|
title: "MCP Registry",
|
|
73
87
|
description: "Community MCP registry with thousands of handy MCPs",
|
|
74
88
|
connection_type: "HTTP",
|
|
75
|
-
connection_url: "https://sites-registry.
|
|
89
|
+
connection_url: "https://sites-registry.deco.site/mcp",
|
|
76
90
|
icon: "https://assets.decocache.com/decocms/cd7ca472-0f72-463a-b0de-6e44bdd0f9b4/mcp.png",
|
|
77
91
|
app_name: "mcp-registry",
|
|
78
92
|
app_id: null,
|
|
@@ -101,8 +115,8 @@ export function getWellKnownSelfConnection(
|
|
|
101
115
|
): ConnectionCreateData {
|
|
102
116
|
return {
|
|
103
117
|
id: WellKnownOrgMCPId.SELF(orgId),
|
|
104
|
-
title: "
|
|
105
|
-
description: "The MCP for the
|
|
118
|
+
title: "Deco CMS",
|
|
119
|
+
description: "The MCP for the CMS API",
|
|
106
120
|
connection_type: "HTTP",
|
|
107
121
|
// Custom url for targeting this mcp. It's a standalone endpoint that exposes all management tools.
|
|
108
122
|
connection_url: `${baseUrl}/mcp/${SELF_MCP_ALIAS_ID}`,
|
|
@@ -121,33 +135,38 @@ export function getWellKnownSelfConnection(
|
|
|
121
135
|
}
|
|
122
136
|
|
|
123
137
|
/**
|
|
124
|
-
* Get well-known connection definition for
|
|
125
|
-
*
|
|
138
|
+
* Get well-known connection definition for Dev Assets MCP.
|
|
139
|
+
* This is a dev-only MCP that provides local file storage at /data/assets/<org_id>/.
|
|
140
|
+
* It implements the OBJECT_STORAGE_BINDING interface.
|
|
141
|
+
*
|
|
142
|
+
* @param baseUrl - The base URL for the MCP server (e.g., "http://localhost:3000")
|
|
143
|
+
* @param orgId - The organization ID
|
|
144
|
+
* @returns ConnectionCreateData for the Dev Assets MCP
|
|
126
145
|
*/
|
|
127
|
-
export function
|
|
128
|
-
|
|
146
|
+
export function getWellKnownDevAssetsConnection(
|
|
147
|
+
baseUrl: string,
|
|
148
|
+
orgId: string,
|
|
129
149
|
): ConnectionCreateData {
|
|
130
150
|
return {
|
|
131
|
-
id:
|
|
132
|
-
title: "
|
|
133
|
-
description:
|
|
134
|
-
|
|
135
|
-
app_name: "openrouter",
|
|
136
|
-
app_id: "openrouter",
|
|
151
|
+
id: WellKnownOrgMCPId.DEV_ASSETS(orgId),
|
|
152
|
+
title: "Local Files",
|
|
153
|
+
description:
|
|
154
|
+
"Local file storage for development. Files are stored in /data/assets/.",
|
|
137
155
|
connection_type: "HTTP",
|
|
138
|
-
connection_url:
|
|
156
|
+
connection_url: `${baseUrl}/mcp/${DEV_ASSETS_MCP_ALIAS_ID}`,
|
|
157
|
+
// Folder icon
|
|
158
|
+
icon: "https://api.iconify.design/lucide:folder.svg?color=%23888",
|
|
159
|
+
app_name: "@deco/dev-assets-mcp",
|
|
160
|
+
app_id: null,
|
|
139
161
|
connection_token: null,
|
|
140
162
|
connection_headers: null,
|
|
141
163
|
oauth_config: null,
|
|
142
164
|
configuration_state: null,
|
|
143
165
|
configuration_scopes: null,
|
|
144
166
|
metadata: {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
toolsCount: 0,
|
|
149
|
-
publishedAt: null,
|
|
150
|
-
repository: null,
|
|
167
|
+
isFixed: true,
|
|
168
|
+
devOnly: true,
|
|
169
|
+
type: "dev-assets",
|
|
151
170
|
},
|
|
152
171
|
};
|
|
153
172
|
}
|
|
@@ -178,27 +197,165 @@ export function getWellKnownMcpStudioConnection(): ConnectionCreateData {
|
|
|
178
197
|
}
|
|
179
198
|
|
|
180
199
|
/**
|
|
181
|
-
*
|
|
182
|
-
*
|
|
200
|
+
* Build a paired `{ is, get }` helper for an org-scoped well-known agent id
|
|
201
|
+
* of shape `${prefix}${orgId}`. Centralizes the trivial check/slice/format
|
|
202
|
+
* logic that every well-known agent used to hand-roll.
|
|
183
203
|
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
204
|
+
* `is(id)` returns the orgId when `id` matches the prefix; `null` otherwise.
|
|
205
|
+
* `get(orgId)` mints the well-known id.
|
|
186
206
|
*/
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
207
|
+
function createWellKnownAgentPrefix(prefix: string): {
|
|
208
|
+
is: (id: string | null | undefined) => string | null;
|
|
209
|
+
get: (organizationId: string) => string;
|
|
210
|
+
} {
|
|
190
211
|
return {
|
|
191
|
-
id
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
212
|
+
is(id) {
|
|
213
|
+
if (!id) return null;
|
|
214
|
+
if (!id.startsWith(prefix)) return null;
|
|
215
|
+
return id.slice(prefix.length) || null;
|
|
216
|
+
},
|
|
217
|
+
get(organizationId) {
|
|
218
|
+
return `${prefix}${organizationId}`;
|
|
219
|
+
},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Build a well-known agent VirtualMCPEntity with sensible defaults
|
|
225
|
+
* (status active, system creator, empty connections, etc.). Only the
|
|
226
|
+
* id/title/description/icon and optional instructions vary across agents.
|
|
227
|
+
*/
|
|
228
|
+
function defineWellKnownAgentVMCP(opts: {
|
|
229
|
+
id: string;
|
|
230
|
+
organizationId: string;
|
|
231
|
+
title: string;
|
|
232
|
+
description: string;
|
|
233
|
+
icon: string;
|
|
234
|
+
instructions?: string | null;
|
|
235
|
+
}): VirtualMCPEntity {
|
|
236
|
+
return {
|
|
237
|
+
id: opts.id,
|
|
238
|
+
organization_id: opts.organizationId,
|
|
239
|
+
title: opts.title,
|
|
240
|
+
description: opts.description,
|
|
241
|
+
icon: opts.icon,
|
|
197
242
|
status: "active",
|
|
198
243
|
created_at: new Date().toISOString(),
|
|
199
244
|
updated_at: new Date().toISOString(),
|
|
200
245
|
created_by: "system",
|
|
201
246
|
updated_by: undefined,
|
|
202
|
-
|
|
247
|
+
metadata: { instructions: opts.instructions ?? null },
|
|
248
|
+
pinned: false,
|
|
249
|
+
connections: [],
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ---- Decopilot ----
|
|
254
|
+
// Default agent that aggregates ALL org connections. Gateway populates
|
|
255
|
+
// the connections array at lookup time.
|
|
256
|
+
const decopilotPrefix = createWellKnownAgentPrefix("decopilot_");
|
|
257
|
+
export const isDecopilot = decopilotPrefix.is;
|
|
258
|
+
export const getDecopilotId = decopilotPrefix.get;
|
|
259
|
+
|
|
260
|
+
export function getWellKnownDecopilotVirtualMCP(
|
|
261
|
+
organizationId: string,
|
|
262
|
+
): VirtualMCPEntity {
|
|
263
|
+
return defineWellKnownAgentVMCP({
|
|
264
|
+
id: getDecopilotId(organizationId),
|
|
265
|
+
organizationId,
|
|
266
|
+
title: "Decopilot",
|
|
267
|
+
description: "Default agent that aggregates all organization connections",
|
|
268
|
+
icon: "https://assets.decocache.com/decocms/fd07a578-6b1c-40f1-bc05-88a3b981695d/f7fc4ffa81aec04e37ae670c3cd4936643a7b269.png",
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ---- Brand-Context Setup ----
|
|
273
|
+
// Guided-onboarding agent for the brand-context preset task. The
|
|
274
|
+
// `brand_context_setup` built-in is injected by `dispatchRun` when this
|
|
275
|
+
// id is seen; the system prompt lives in `metadata.instructions`.
|
|
276
|
+
const brandContextSetupPrefix = createWellKnownAgentPrefix(
|
|
277
|
+
"brand-context-setup_",
|
|
278
|
+
);
|
|
279
|
+
export const isBrandContextSetup = brandContextSetupPrefix.is;
|
|
280
|
+
export const getBrandContextSetupId = brandContextSetupPrefix.get;
|
|
281
|
+
|
|
282
|
+
const BRAND_CONTEXT_SETUP_INSTRUCTIONS = `
|
|
283
|
+
You are running the brand-context onboarding for the user's organization. Your only job in this thread is to set up the organization's brand context:
|
|
284
|
+
|
|
285
|
+
1. If the user hasn't already given you a website URL, ask for it in one short message. Accept whatever URL they give — don't quibble about format.
|
|
286
|
+
2. As soon as you have a URL, call the \`brand_context_setup\` tool exactly once with that URL.
|
|
287
|
+
3. After the tool returns success, briefly confirm to the user what was captured (brand name + domain) in one or two sentences. Do not list every color or font.
|
|
288
|
+
4. Do NOT call any other tools in this thread. Do NOT call \`brand_context_setup\` more than once.
|
|
289
|
+
|
|
290
|
+
If the tool returns an error, surface the error message to the user and ask whether they want to try a different URL.
|
|
291
|
+
`.trim();
|
|
292
|
+
|
|
293
|
+
export function getWellKnownBrandContextSetupVirtualMCP(
|
|
294
|
+
organizationId: string,
|
|
295
|
+
): VirtualMCPEntity {
|
|
296
|
+
return defineWellKnownAgentVMCP({
|
|
297
|
+
id: getBrandContextSetupId(organizationId),
|
|
298
|
+
organizationId,
|
|
299
|
+
title: "Brand context setup",
|
|
300
|
+
description:
|
|
301
|
+
"Guided onboarding agent that extracts brand context from a website URL.",
|
|
302
|
+
icon: "https://assets.decocache.com/decocms/fd07a578-6b1c-40f1-bc05-88a3b981695d/f7fc4ffa81aec04e37ae670c3cd4936643a7b269.png",
|
|
303
|
+
instructions: BRAND_CONTEXT_SETUP_INSTRUCTIONS,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// ---- Site Diagnostics ----
|
|
308
|
+
const siteDiagnosticsPrefix = createWellKnownAgentPrefix("site-diagnostics_");
|
|
309
|
+
export const isSiteDiagnostics = siteDiagnosticsPrefix.is;
|
|
310
|
+
export const getSiteDiagnosticsId = siteDiagnosticsPrefix.get;
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Studio Pack agent ID generators (org-scoped)
|
|
314
|
+
*/
|
|
315
|
+
export const StudioPackAgentId = {
|
|
316
|
+
AGENT_MANAGER: (orgId: string) => `studio-agent-manager_${orgId}`,
|
|
317
|
+
AUTOMATION_MANAGER: (orgId: string) => `studio-automation-manager_${orgId}`,
|
|
318
|
+
CONNECTION_MANAGER: (orgId: string) => `studio-connection-manager_${orgId}`,
|
|
319
|
+
STORE_MANAGER: (orgId: string) => `studio-store-manager_${orgId}`,
|
|
320
|
+
BRAND_MANAGER: (orgId: string) => `studio-brand-manager_${orgId}`,
|
|
321
|
+
} as const;
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Check if a connection or virtual MCP ID is a Studio Pack agent.
|
|
325
|
+
*/
|
|
326
|
+
export function isStudioPackAgent(id: string | null | undefined): boolean {
|
|
327
|
+
if (!id) return false;
|
|
328
|
+
return (
|
|
329
|
+
id.startsWith("studio-agent-manager_") ||
|
|
330
|
+
id.startsWith("studio-automation-manager_") ||
|
|
331
|
+
id.startsWith("studio-connection-manager_") ||
|
|
332
|
+
id.startsWith("studio-store-manager_") ||
|
|
333
|
+
id.startsWith("studio-brand-manager_")
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function getWellKnownDecopilotConnection(
|
|
338
|
+
organizationId: string,
|
|
339
|
+
): ConnectionEntity {
|
|
340
|
+
const virtual = getWellKnownDecopilotVirtualMCP(organizationId);
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
...virtual,
|
|
344
|
+
id: virtual.id!,
|
|
345
|
+
connection_type: "VIRTUAL",
|
|
346
|
+
connection_url: `virtual://${virtual.id}`,
|
|
347
|
+
app_name: "decopilot",
|
|
348
|
+
app_id: "decopilot",
|
|
349
|
+
connection_token: null,
|
|
350
|
+
connection_headers: null,
|
|
351
|
+
oauth_config: null,
|
|
352
|
+
configuration_state: null,
|
|
353
|
+
configuration_scopes: null,
|
|
354
|
+
metadata: {
|
|
355
|
+
isDefault: true,
|
|
356
|
+
type: "decopilot",
|
|
357
|
+
},
|
|
358
|
+
tools: [],
|
|
359
|
+
bindings: [],
|
|
203
360
|
};
|
|
204
361
|
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AiProviderModel,
|
|
3
|
+
AiProviderKey,
|
|
4
|
+
ProviderId,
|
|
5
|
+
} from "../types/ai-providers";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Preferred default models for each well-known provider.
|
|
9
|
+
*
|
|
10
|
+
* Each entry is an ordered list of candidate model ID strings — lower indexes
|
|
11
|
+
* have higher priority. The selector first tries exact matches across the full
|
|
12
|
+
* list, then falls back to substring matches in the same priority order.
|
|
13
|
+
*/
|
|
14
|
+
export const DEFAULT_MODEL_PREFERENCES: Partial<Record<ProviderId, string[]>> =
|
|
15
|
+
{
|
|
16
|
+
anthropic: ["claude-sonnet-4-6", "claude-sonnet", "claude"],
|
|
17
|
+
openrouter: [
|
|
18
|
+
"anthropic/claude-opus-4.8",
|
|
19
|
+
"anthropic/claude-sonnet-4-6",
|
|
20
|
+
"anthropic/claude-sonnet",
|
|
21
|
+
"anthropic/claude",
|
|
22
|
+
],
|
|
23
|
+
deco: [
|
|
24
|
+
"anthropic/claude-haiku-4-5",
|
|
25
|
+
"anthropic/claude-haiku",
|
|
26
|
+
"anthropic/claude",
|
|
27
|
+
],
|
|
28
|
+
google: ["gemini-3-flash"],
|
|
29
|
+
"claude-code": [
|
|
30
|
+
"claude-code:sonnet",
|
|
31
|
+
"claude-code:opus",
|
|
32
|
+
"claude-code:haiku",
|
|
33
|
+
],
|
|
34
|
+
codex: ["codex:gpt-5.4"],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Preferred fast/cheap models per provider — used for lightweight tasks
|
|
39
|
+
* like title generation where latency and cost matter more than capability.
|
|
40
|
+
*/
|
|
41
|
+
export const FAST_MODEL_PREFERENCES: Partial<Record<ProviderId, string[]>> = {
|
|
42
|
+
anthropic: ["claude-haiku-4-5", "claude-haiku"],
|
|
43
|
+
openrouter: [
|
|
44
|
+
"qwen/qwen3.5-flash",
|
|
45
|
+
"anthropic/claude-haiku-4.5",
|
|
46
|
+
"anthropic/claude-haiku",
|
|
47
|
+
"google/gemini-3-flash",
|
|
48
|
+
],
|
|
49
|
+
deco: ["qwen/qwen3.5-flash", "anthropic/claude-haiku"],
|
|
50
|
+
google: ["gemini-2.5-flash", "gemini-3-flash"],
|
|
51
|
+
"claude-code": ["claude-code:haiku", "claude-code:sonnet"],
|
|
52
|
+
codex: ["codex:gpt-5.4-mini"],
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Return the preferred fast model ID for a given provider.
|
|
57
|
+
* Returns the first candidate or `null` if no preference is configured.
|
|
58
|
+
*/
|
|
59
|
+
export function getFastModel(providerId: ProviderId): string | null {
|
|
60
|
+
const candidates = FAST_MODEL_PREFERENCES[providerId];
|
|
61
|
+
return candidates?.[0] ?? null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Preferred smart (balanced) models per provider — used as the "Smart" tier
|
|
66
|
+
* in Simple Model Mode.
|
|
67
|
+
*/
|
|
68
|
+
export const SMART_MODEL_PREFERENCES: Partial<Record<ProviderId, string[]>> = {
|
|
69
|
+
anthropic: ["claude-sonnet-4-6", "claude-sonnet"],
|
|
70
|
+
openrouter: [
|
|
71
|
+
"anthropic/claude-sonnet-4.6",
|
|
72
|
+
"anthropic/claude-sonnet",
|
|
73
|
+
"anthropic/claude-opus-4.8",
|
|
74
|
+
"google/gemini-3-pro",
|
|
75
|
+
],
|
|
76
|
+
deco: [
|
|
77
|
+
"anthropic/claude-sonnet-4.6",
|
|
78
|
+
"anthropic/claude-sonnet",
|
|
79
|
+
"anthropic/claude",
|
|
80
|
+
],
|
|
81
|
+
google: ["gemini-3-pro", "gemini-3-flash"],
|
|
82
|
+
"claude-code": ["claude-code:sonnet"],
|
|
83
|
+
codex: ["codex:gpt-5.4"],
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Preferred thinking/reasoning models per provider — used as the "Thinking" tier
|
|
88
|
+
* in Simple Model Mode.
|
|
89
|
+
*/
|
|
90
|
+
export const THINKING_MODEL_PREFERENCES: Partial<Record<ProviderId, string[]>> =
|
|
91
|
+
{
|
|
92
|
+
anthropic: ["claude-opus-4-8", "claude-sonnet-4-6", "claude-sonnet"],
|
|
93
|
+
openrouter: [
|
|
94
|
+
"anthropic/claude-opus-4.8",
|
|
95
|
+
"anthropic/claude-sonnet-4.6:extended",
|
|
96
|
+
"anthropic/claude-sonnet-4.6",
|
|
97
|
+
"google/gemini-3-pro",
|
|
98
|
+
],
|
|
99
|
+
deco: [
|
|
100
|
+
"anthropic/claude-opus",
|
|
101
|
+
"anthropic/claude-sonnet-4.6",
|
|
102
|
+
"anthropic/claude-sonnet",
|
|
103
|
+
],
|
|
104
|
+
google: ["gemini-3-pro"],
|
|
105
|
+
"claude-code": ["claude-code:opus", "claude-code:sonnet"],
|
|
106
|
+
codex: ["codex:gpt-5.5"],
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Preferred image generation models per provider.
|
|
111
|
+
* Falls back to first model with "image" capability.
|
|
112
|
+
*/
|
|
113
|
+
export const IMAGE_MODEL_PREFERENCES: Partial<Record<ProviderId, string[]>> = {
|
|
114
|
+
openrouter: ["openai/gpt-image-1", "google/gemini-2.0-flash-image"],
|
|
115
|
+
deco: ["openai/gpt-image-1", "google/gemini-2.0-flash-image"],
|
|
116
|
+
google: ["gemini-2.0-flash-image"],
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Preferred web research models per provider.
|
|
121
|
+
* Falls back to first model whose id includes "sonar" or "deepresearch".
|
|
122
|
+
*/
|
|
123
|
+
export const WEB_RESEARCH_MODEL_PREFERENCES: Partial<
|
|
124
|
+
Record<ProviderId, string[]>
|
|
125
|
+
> = {
|
|
126
|
+
openrouter: [
|
|
127
|
+
"perplexity/sonar",
|
|
128
|
+
"perplexity/sonar-pro",
|
|
129
|
+
"perplexity/deep-research",
|
|
130
|
+
],
|
|
131
|
+
deco: [
|
|
132
|
+
"perplexity/sonar",
|
|
133
|
+
"perplexity/sonar-pro",
|
|
134
|
+
"perplexity/deep-research",
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export interface SimpleModeModelSlot {
|
|
139
|
+
keyId: string;
|
|
140
|
+
modelId: string;
|
|
141
|
+
title?: string;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface SimpleModeDefaults {
|
|
145
|
+
chat: {
|
|
146
|
+
fast: SimpleModeModelSlot | null;
|
|
147
|
+
smart: SimpleModeModelSlot | null;
|
|
148
|
+
thinking: SimpleModeModelSlot | null;
|
|
149
|
+
};
|
|
150
|
+
image: SimpleModeModelSlot | null;
|
|
151
|
+
webResearch: SimpleModeModelSlot | null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function resolveSlot(
|
|
155
|
+
models: AiProviderModel[],
|
|
156
|
+
keyId: string,
|
|
157
|
+
preferences: string[],
|
|
158
|
+
fallback?: (m: AiProviderModel) => boolean,
|
|
159
|
+
): SimpleModeModelSlot | null {
|
|
160
|
+
for (const candidate of preferences) {
|
|
161
|
+
const exact = models.find((m) => m.modelId === candidate);
|
|
162
|
+
if (exact) return { keyId, modelId: exact.modelId, title: exact.title };
|
|
163
|
+
}
|
|
164
|
+
for (const candidate of preferences) {
|
|
165
|
+
const partial = models.find((m) => m.modelId.includes(candidate));
|
|
166
|
+
if (partial)
|
|
167
|
+
return { keyId, modelId: partial.modelId, title: partial.title };
|
|
168
|
+
}
|
|
169
|
+
if (fallback) {
|
|
170
|
+
const found = models.find(fallback);
|
|
171
|
+
if (found) return { keyId, modelId: found.modelId, title: found.title };
|
|
172
|
+
}
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Compute sensible Simple Mode defaults from the currently-connected keys and
|
|
178
|
+
* their available models. Each slot picks the best candidate per the tier
|
|
179
|
+
* preference lists, falling back to capability-based detection for image/web.
|
|
180
|
+
*
|
|
181
|
+
* @param keys The org's connected AI provider keys.
|
|
182
|
+
* @param modelsByKeyId Map of keyId → available model list.
|
|
183
|
+
*/
|
|
184
|
+
export function pickSimpleModeDefaults(
|
|
185
|
+
keys: AiProviderKey[],
|
|
186
|
+
modelsByKeyId: Record<string, AiProviderModel[]>,
|
|
187
|
+
): SimpleModeDefaults {
|
|
188
|
+
const result: SimpleModeDefaults = {
|
|
189
|
+
chat: { fast: null, smart: null, thinking: null },
|
|
190
|
+
image: null,
|
|
191
|
+
webResearch: null,
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
for (const key of keys) {
|
|
195
|
+
const models = modelsByKeyId[key.id] ?? [];
|
|
196
|
+
const providerId = key.providerId as ProviderId;
|
|
197
|
+
|
|
198
|
+
if (!result.chat.fast) {
|
|
199
|
+
result.chat.fast = resolveSlot(
|
|
200
|
+
models,
|
|
201
|
+
key.id,
|
|
202
|
+
FAST_MODEL_PREFERENCES[providerId] ?? [],
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
if (!result.chat.smart) {
|
|
206
|
+
result.chat.smart = resolveSlot(
|
|
207
|
+
models,
|
|
208
|
+
key.id,
|
|
209
|
+
SMART_MODEL_PREFERENCES[providerId] ?? [],
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
if (!result.chat.thinking) {
|
|
213
|
+
result.chat.thinking = resolveSlot(
|
|
214
|
+
models,
|
|
215
|
+
key.id,
|
|
216
|
+
THINKING_MODEL_PREFERENCES[providerId] ?? [],
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
if (!result.image) {
|
|
220
|
+
result.image = resolveSlot(
|
|
221
|
+
models,
|
|
222
|
+
key.id,
|
|
223
|
+
IMAGE_MODEL_PREFERENCES[providerId] ?? [],
|
|
224
|
+
(m) => m.capabilities?.includes("image") === true,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
if (!result.webResearch) {
|
|
228
|
+
result.webResearch = resolveSlot(
|
|
229
|
+
models,
|
|
230
|
+
key.id,
|
|
231
|
+
WEB_RESEARCH_MODEL_PREFERENCES[providerId] ?? [],
|
|
232
|
+
(m) => {
|
|
233
|
+
const n = m.modelId.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
234
|
+
return n.includes("sonar") || n.includes("deepresearch");
|
|
235
|
+
},
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return result;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Select the best default model from a loaded list for a given provider.
|
|
245
|
+
*
|
|
246
|
+
* Resolution order:
|
|
247
|
+
* 1. Exact `modelId` match — walk candidates in priority order.
|
|
248
|
+
* 2. Substring match — walk candidates in priority order, return the first
|
|
249
|
+
* model whose `modelId` contains the candidate string.
|
|
250
|
+
* 3. First model in the list.
|
|
251
|
+
* 4. `null` if the list is empty.
|
|
252
|
+
*
|
|
253
|
+
* @param models Full model list returned by the provider for this key.
|
|
254
|
+
* @param providerId The provider that owns the key.
|
|
255
|
+
* @param keyId Credential key ID to attach — mirrors what
|
|
256
|
+
* `handleModelSelect` does on explicit user selection.
|
|
257
|
+
*/
|
|
258
|
+
export function selectDefaultModel(
|
|
259
|
+
models: AiProviderModel[],
|
|
260
|
+
providerId: ProviderId,
|
|
261
|
+
keyId?: string,
|
|
262
|
+
): AiProviderModel | null {
|
|
263
|
+
if (models.length === 0) return null;
|
|
264
|
+
|
|
265
|
+
const candidates = DEFAULT_MODEL_PREFERENCES[providerId] ?? [];
|
|
266
|
+
|
|
267
|
+
const withKey = (model: AiProviderModel): AiProviderModel =>
|
|
268
|
+
keyId !== undefined ? { ...model, keyId } : model;
|
|
269
|
+
|
|
270
|
+
for (const candidate of candidates) {
|
|
271
|
+
const exact = models.find((m) => m.modelId === candidate);
|
|
272
|
+
if (exact) return withKey(exact);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
for (const candidate of candidates) {
|
|
276
|
+
const partial = models.find((m) => m.modelId.includes(candidate));
|
|
277
|
+
if (partial) return withKey(partial);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return withKey(models[0] as AiProviderModel);
|
|
281
|
+
}
|