@decocms/mesh-sdk 1.2.2 → 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/src/lib/usage.ts CHANGED
@@ -14,6 +14,18 @@ export interface UsageData {
14
14
  outputTokens?: number;
15
15
  reasoningTokens?: number;
16
16
  totalTokens?: number;
17
+ /**
18
+ * AI SDK normalizes cache token counts across providers via
19
+ * usage.inputTokenDetails — populated identically by the anthropic,
20
+ * openai, google and openrouter adapters. `cachedInputTokens` is the
21
+ * convenience shorthand the AI SDK also surfaces (= cacheReadTokens).
22
+ */
23
+ cachedInputTokens?: number;
24
+ inputTokenDetails?: {
25
+ cacheReadTokens?: number;
26
+ cacheWriteTokens?: number;
27
+ noCacheTokens?: number;
28
+ };
17
29
  providerMetadata?: {
18
30
  [key: string]: unknown;
19
31
  };
@@ -25,6 +37,10 @@ export interface UsageStats {
25
37
  reasoningTokens: number;
26
38
  totalTokens: number;
27
39
  cost: number;
40
+ /** Tokens read from prompt cache (anthropic / openrouter / openai / google). */
41
+ cacheReadTokens: number;
42
+ /** Tokens written to prompt cache (Anthropic only — others auto-cache without separate billing). */
43
+ cacheWriteTokens: number;
28
44
  }
29
45
 
30
46
  type ProviderCostExtractor = (
@@ -124,6 +140,8 @@ export function emptyUsageStats(): UsageStats {
124
140
  reasoningTokens: 0,
125
141
  totalTokens: 0,
126
142
  cost: 0,
143
+ cacheReadTokens: 0,
144
+ cacheWriteTokens: 0,
127
145
  };
128
146
  }
129
147
 
@@ -137,6 +155,12 @@ export function addUsage(
137
155
  ): UsageStats {
138
156
  if (!stepUsage) return accumulated;
139
157
 
158
+ const cacheRead =
159
+ stepUsage.inputTokenDetails?.cacheReadTokens ??
160
+ stepUsage.cachedInputTokens ??
161
+ 0;
162
+ const cacheWrite = stepUsage.inputTokenDetails?.cacheWriteTokens ?? 0;
163
+
140
164
  return {
141
165
  inputTokens: accumulated.inputTokens + (stepUsage.inputTokens ?? 0),
142
166
  outputTokens: accumulated.outputTokens + (stepUsage.outputTokens ?? 0),
@@ -144,6 +168,8 @@ export function addUsage(
144
168
  accumulated.reasoningTokens + (stepUsage.reasoningTokens ?? 0),
145
169
  totalTokens: accumulated.totalTokens + (stepUsage.totalTokens ?? 0),
146
170
  cost: accumulated.cost + getCostFromUsage(stepUsage),
171
+ cacheReadTokens: accumulated.cacheReadTokens + cacheRead,
172
+ cacheWriteTokens: accumulated.cacheWriteTokens + cacheWrite,
147
173
  };
148
174
  }
149
175
 
@@ -8,6 +8,8 @@ export const PROVIDER_IDS = [
8
8
  "openrouter",
9
9
  "google",
10
10
  "claude-code",
11
+ "codex",
12
+ "openai-compatible",
11
13
  ] as const;
12
14
 
13
15
  export type ProviderId = (typeof PROVIDER_IDS)[number];
@@ -45,6 +47,16 @@ export interface AiProviderModel {
45
47
  capabilities: ModelCapability[];
46
48
  limits: AiProviderModelLimits | null;
47
49
  costs: AiProviderModelCosts | null;
50
+ /** When true the upstream provider has flagged this model as deprecated. */
51
+ deprecated?: boolean;
52
+ /**
53
+ * When true, this model can ONLY be used through the provider's
54
+ * `AsyncResearchProvider` capability (e.g. Gemini Deep Research via the
55
+ * Interactions API). It is unusable as a Thinking/Coding/Fast/Image model
56
+ * because `streamText` / `generateContent` will reject it. UIs should
57
+ * restrict it to the deep-research slot.
58
+ */
59
+ asyncResearch?: boolean;
48
60
  /** Client-side only — the credential key ID used to fetch this model. */
49
61
  keyId?: string;
50
62
  }
@@ -53,6 +65,11 @@ export interface AiProviderKey {
53
65
  id: string;
54
66
  providerId: ProviderId;
55
67
  label: string;
68
+ /**
69
+ * Frontend preset id (e.g. "litellm", "ollama") for openai-compatible keys
70
+ * that were created from a branded preset card. Null otherwise.
71
+ */
72
+ presetId: string | null;
56
73
  createdBy: string;
57
74
  createdAt: string;
58
75
  }
@@ -62,7 +79,8 @@ export interface AiProviderInfo {
62
79
  name: string;
63
80
  description: string;
64
81
  logo?: string | null;
65
- supportedMethods: ("api-key" | "oauth-pkce")[];
82
+ supportedMethods: ("api-key" | "oauth-pkce" | "cli-activate")[];
66
83
  supportsTopUp?: boolean;
67
84
  supportsCredits?: boolean;
85
+ supportsProvision?: boolean;
68
86
  }
@@ -102,6 +102,11 @@ export const ConnectionEntitySchema = z.object({
102
102
  icon: z.string().nullable().describe("Icon URL for the connection"),
103
103
  app_name: z.string().nullable().describe("Associated app name"),
104
104
  app_id: z.string().nullable().describe("Associated app ID"),
105
+ slug: z
106
+ .string()
107
+ .nullable()
108
+ .optional()
109
+ .describe("URL-safe slug derived from app_name, connection_url, or title"),
105
110
 
106
111
  connection_type: z
107
112
  .enum(["HTTP", "SSE", "Websocket", "STDIO", "VIRTUAL"])
@@ -0,0 +1,78 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ createDecopilotThreadStatusEvent,
4
+ DECOPILOT_EVENTS,
5
+ } from "./decopilot-events";
6
+
7
+ describe("createDecopilotThreadStatusEvent", () => {
8
+ test("carries virtualMcpId, createdBy, and triggerId on data", () => {
9
+ const e = createDecopilotThreadStatusEvent("task-1", "in_progress", {
10
+ virtualMcpId: "vm-1",
11
+ createdBy: "user-1",
12
+ triggerId: "trig-1",
13
+ });
14
+ expect(e.type).toBe(DECOPILOT_EVENTS.THREAD_STATUS);
15
+ expect(e.subject).toBe("task-1");
16
+ expect(e.data.status).toBe("in_progress");
17
+ expect(e.data.virtual_mcp_id).toBe("vm-1");
18
+ expect(e.data.created_by).toBe("user-1");
19
+ expect(e.data.trigger_id).toBe("trig-1");
20
+ });
21
+
22
+ test("omits optional fields when not provided", () => {
23
+ const e = createDecopilotThreadStatusEvent("task-1", "completed");
24
+ expect(e.data.status).toBe("completed");
25
+ expect(e.data.virtual_mcp_id).toBeUndefined();
26
+ expect(e.data.created_by).toBeUndefined();
27
+ expect(e.data.trigger_id).toBeUndefined();
28
+ });
29
+
30
+ test("preserves explicit null trigger_id (human-initiated thread)", () => {
31
+ const e = createDecopilotThreadStatusEvent("task-1", "completed", {
32
+ triggerId: null,
33
+ });
34
+ expect(e.data.trigger_id).toBeNull();
35
+ });
36
+
37
+ test("works with only virtualMcpId provided (migration shape)", () => {
38
+ const e = createDecopilotThreadStatusEvent("task-1", "in_progress", {
39
+ virtualMcpId: "vm-1",
40
+ });
41
+ expect(e.data.virtual_mcp_id).toBe("vm-1");
42
+ expect(e.data.created_by).toBeUndefined();
43
+ expect(e.data.trigger_id).toBeUndefined();
44
+ });
45
+ });
46
+
47
+ describe("createDecopilotThreadStatusEvent — enriched fields", () => {
48
+ test("round-trips title, branch, createdAt, updatedAt", () => {
49
+ const e = createDecopilotThreadStatusEvent("task-1", "in_progress", {
50
+ virtualMcpId: "vm-1",
51
+ title: "Refactor login",
52
+ branch: "feature/login",
53
+ createdAt: "2026-05-19T00:00:00.000Z",
54
+ updatedAt: "2026-05-19T00:05:00.000Z",
55
+ });
56
+ expect(e.data.title).toBe("Refactor login");
57
+ expect(e.data.branch).toBe("feature/login");
58
+ expect(e.data.created_at).toBe("2026-05-19T00:00:00.000Z");
59
+ expect(e.data.updated_at).toBe("2026-05-19T00:05:00.000Z");
60
+ });
61
+
62
+ test("omits the new fields when not provided", () => {
63
+ const e = createDecopilotThreadStatusEvent("task-1", "in_progress", {
64
+ virtualMcpId: "vm-1",
65
+ });
66
+ expect(e.data.title).toBeUndefined();
67
+ expect(e.data.branch).toBeUndefined();
68
+ expect(e.data.created_at).toBeUndefined();
69
+ expect(e.data.updated_at).toBeUndefined();
70
+ });
71
+
72
+ test("explicit null branch is preserved", () => {
73
+ const e = createDecopilotThreadStatusEvent("task-1", "in_progress", {
74
+ branch: null,
75
+ });
76
+ expect(e.data.branch).toBeNull();
77
+ });
78
+ });
@@ -66,7 +66,28 @@ export interface DecopilotFinishEvent extends BaseDecopilotEvent {
66
66
 
67
67
  export interface DecopilotThreadStatusEvent extends BaseDecopilotEvent {
68
68
  type: typeof DECOPILOT_EVENTS.THREAD_STATUS;
69
- data: { status: ThreadStatus };
69
+ data: {
70
+ status: ThreadStatus;
71
+ virtual_mcp_id?: string;
72
+ /** User who created the thread; needed to populate filter-complete cache rows on the client. */
73
+ created_by?: string;
74
+ /** Automation trigger id; null for human-initiated, omitted when unknown. */
75
+ trigger_id?: string | null;
76
+ /** Thread title at emit time. Absent if caller didn't load the row. */
77
+ title?: string;
78
+ /** Branch this thread is pinned to (null when unpinned). Absent if caller didn't load the row. */
79
+ branch?: string | null;
80
+ /** Thread creation timestamp. Absent if caller didn't load the row. */
81
+ created_at?: string;
82
+ /** Last update timestamp; useful for the client to sort/dedupe. Absent if caller didn't load the row. */
83
+ updated_at?: string;
84
+ /** Free-form thread metadata snapshot. The chat UI keys off
85
+ * metadata.kind to switch between agent-thread and tool_call_run
86
+ * renderings (avatar, message-renderer), so the workflow that
87
+ * spawns those threads must include it on the first event or the
88
+ * row renders with the wrong icon until a refetch. */
89
+ metadata?: Record<string, unknown>;
90
+ };
70
91
  }
71
92
 
72
93
  export type DecopilotSSEEvent =
@@ -86,43 +107,65 @@ export interface DecopilotEventMap {
86
107
  // ============================================================================
87
108
 
88
109
  export function createDecopilotStepEvent(
89
- threadId: string,
110
+ taskId: string,
90
111
  stepCount: number,
91
112
  ): DecopilotStepEvent {
92
113
  return {
93
114
  id: crypto.randomUUID(),
94
115
  type: DECOPILOT_EVENTS.STEP,
95
116
  source: "decopilot",
96
- subject: threadId,
117
+ subject: taskId,
97
118
  data: { stepCount },
98
119
  time: new Date().toISOString(),
99
120
  };
100
121
  }
101
122
 
102
123
  export function createDecopilotFinishEvent(
103
- threadId: string,
124
+ taskId: string,
104
125
  status: ThreadStatus,
105
126
  ): DecopilotFinishEvent {
106
127
  return {
107
128
  id: crypto.randomUUID(),
108
129
  type: DECOPILOT_EVENTS.FINISH,
109
130
  source: "decopilot",
110
- subject: threadId,
131
+ subject: taskId,
111
132
  data: { status },
112
133
  time: new Date().toISOString(),
113
134
  };
114
135
  }
115
136
 
116
137
  export function createDecopilotThreadStatusEvent(
117
- threadId: string,
138
+ taskId: string,
118
139
  status: ThreadStatus,
140
+ opts?: {
141
+ virtualMcpId?: string;
142
+ createdBy?: string;
143
+ triggerId?: string | null;
144
+ title?: string;
145
+ branch?: string | null;
146
+ createdAt?: string;
147
+ updatedAt?: string;
148
+ metadata?: Record<string, unknown>;
149
+ },
119
150
  ): DecopilotThreadStatusEvent {
120
151
  return {
121
152
  id: crypto.randomUUID(),
122
153
  type: DECOPILOT_EVENTS.THREAD_STATUS,
123
154
  source: "decopilot",
124
- subject: threadId,
125
- data: { status },
155
+ subject: taskId,
156
+ data: {
157
+ status,
158
+ ...(opts?.virtualMcpId !== undefined && {
159
+ virtual_mcp_id: opts.virtualMcpId,
160
+ }),
161
+ ...(opts?.createdBy !== undefined && { created_by: opts.createdBy }),
162
+ ...(opts?.triggerId !== undefined && { trigger_id: opts.triggerId }),
163
+ ...(opts?.title !== undefined && { title: opts.title }),
164
+ ...(opts?.branch !== undefined && { branch: opts.branch }),
165
+ ...(opts?.createdAt !== undefined && { created_at: opts.createdAt }),
166
+ ...(opts?.updatedAt !== undefined && { updated_at: opts.updatedAt }),
167
+ ...(opts?.metadata !== undefined && { metadata: opts.metadata }),
168
+ },
126
169
  time: new Date().toISOString(),
127
170
  };
128
171
  }
@@ -19,10 +19,28 @@ export {
19
19
  VirtualMCPEntitySchema,
20
20
  VirtualMCPCreateDataSchema,
21
21
  VirtualMCPUpdateDataSchema,
22
+ VirtualMcpUILayoutSchema,
23
+ VirtualMcpUILayoutTabSchema,
22
24
  type VirtualMCPEntity,
23
25
  type VirtualMCPCreateData,
24
26
  type VirtualMCPUpdateData,
25
27
  type VirtualMCPConnection,
28
+ type VirtualMcpUILayout,
29
+ type VirtualMcpUILayoutTab,
30
+ type VirtualMcpHomeTile,
31
+ getHomeTiles,
32
+ type GithubRepo,
33
+ SandboxMapSchema,
34
+ type SandboxMap,
35
+ SandboxRecordSchema,
36
+ type SandboxRecord,
37
+ type RuntimeMetadata,
38
+ type RuntimeEnvEntry,
39
+ ENV_VAR_KEY_RE,
40
+ parseSandboxRecord,
41
+ parseBranchMap,
42
+ normalizeSandboxMap,
43
+ type SandboxProviderKind,
26
44
  } from "./virtual-mcp";
27
45
 
28
46
  export {
@@ -0,0 +1,202 @@
1
+ import { describe, expect, it, test } from "bun:test";
2
+ import {
3
+ VirtualMCPEntitySchema,
4
+ VirtualMcpUILayoutSchema,
5
+ VirtualMCPUpdateDataSchema,
6
+ SandboxRecordSchema,
7
+ parseSandboxRecord,
8
+ parseBranchMap,
9
+ } from "./virtual-mcp";
10
+
11
+ describe("VirtualMcpUILayoutSchema tabs", () => {
12
+ it("parses a tabs array with ext-app view", () => {
13
+ const parsed = VirtualMcpUILayoutSchema.parse({
14
+ tabs: [
15
+ {
16
+ id: "analytics",
17
+ title: "Analytics",
18
+ icon: "BarChart",
19
+ view: { type: "ext-app", appId: "app_abc", args: { range: "7d" } },
20
+ },
21
+ ],
22
+ defaultMainView: null,
23
+ });
24
+ expect(parsed.tabs).toHaveLength(1);
25
+ expect(parsed.tabs?.[0]!.view.type).toBe("ext-app");
26
+ expect(parsed.tabs?.[0]!.view.appId).toBe("app_abc");
27
+ });
28
+
29
+ it("accepts tabs omitted (backwards compatible)", () => {
30
+ const parsed = VirtualMcpUILayoutSchema.parse({
31
+ defaultMainView: null,
32
+ });
33
+ expect(parsed.tabs).toBeUndefined();
34
+ });
35
+
36
+ it("rejects a tab view with unknown type", () => {
37
+ const result = VirtualMcpUILayoutSchema.safeParse({
38
+ tabs: [
39
+ {
40
+ id: "bad",
41
+ title: "Bad",
42
+ view: { type: "mystery", appId: "app_x" },
43
+ },
44
+ ],
45
+ });
46
+ expect(result.success).toBe(false);
47
+ });
48
+ });
49
+
50
+ test("metadata.runtime is typed and round-trips through parse", () => {
51
+ const parsed = VirtualMCPEntitySchema.parse({
52
+ id: "x",
53
+ title: "x",
54
+ description: null,
55
+ icon: null,
56
+ created_at: "t",
57
+ updated_at: "t",
58
+ created_by: "u",
59
+ organization_id: "o",
60
+ status: "active",
61
+ pinned: false,
62
+ metadata: {
63
+ instructions: null,
64
+ runtime: { selected: "pnpm", port: "3000" },
65
+ },
66
+ connections: [],
67
+ });
68
+ expect(parsed.metadata.runtime?.selected).toBe("pnpm");
69
+ expect(parsed.metadata.runtime?.port).toBe("3000");
70
+ });
71
+
72
+ test("metadata.runtime accepts null/empty values", () => {
73
+ const parsed = VirtualMCPEntitySchema.parse({
74
+ id: "x",
75
+ title: "x",
76
+ description: null,
77
+ icon: null,
78
+ created_at: "t",
79
+ updated_at: "t",
80
+ created_by: "u",
81
+ organization_id: "o",
82
+ status: "active",
83
+ pinned: false,
84
+ metadata: {
85
+ instructions: null,
86
+ runtime: { selected: null, port: null },
87
+ },
88
+ connections: [],
89
+ });
90
+ expect(parsed.metadata.runtime?.selected).toBeNull();
91
+ expect(parsed.metadata.runtime?.port).toBeNull();
92
+ });
93
+
94
+ test("VirtualMCPUpdateDataSchema accepts metadata.runtime", () => {
95
+ const parsed = VirtualMCPUpdateDataSchema.parse({
96
+ metadata: { runtime: { selected: "bun", port: null } },
97
+ });
98
+ expect(parsed.metadata?.runtime?.selected).toBe("bun");
99
+ expect(parsed.metadata?.runtime?.port).toBeNull();
100
+ });
101
+
102
+ test("SandboxRecord.startedWith is optional with nullable packageManager/port/path", () => {
103
+ const a = SandboxRecordSchema.parse({ sandboxHandle: "v", previewUrl: null });
104
+ expect(a.startedWith).toBeUndefined();
105
+ const b = SandboxRecordSchema.parse({
106
+ sandboxHandle: "v",
107
+ previewUrl: null,
108
+ startedWith: { packageManager: "pnpm", port: "3000", path: "apps/web" },
109
+ });
110
+ expect(b.startedWith?.packageManager).toBe("pnpm");
111
+ expect(b.startedWith?.port).toBe("3000");
112
+ expect(b.startedWith?.path).toBe("apps/web");
113
+ const c = SandboxRecordSchema.parse({
114
+ sandboxHandle: "v",
115
+ previewUrl: null,
116
+ startedWith: { packageManager: null, port: null, path: null },
117
+ });
118
+ expect(c.startedWith?.packageManager).toBeNull();
119
+ expect(c.startedWith?.port).toBeNull();
120
+ expect(c.startedWith?.path).toBeNull();
121
+ });
122
+
123
+ describe("parseBranchMap", () => {
124
+ test("parses 3-level (kind-keyed) map with canonical kinds", () => {
125
+ const result = parseBranchMap({
126
+ cluster: {
127
+ sandboxHandle: "v1",
128
+ previewUrl: null,
129
+ sandboxProviderKind: "cluster",
130
+ },
131
+ "user-desktop": {
132
+ sandboxHandle: "v2",
133
+ previewUrl: null,
134
+ sandboxProviderKind: "user-desktop",
135
+ },
136
+ });
137
+ expect(result["cluster"]?.sandboxHandle).toBe("v1");
138
+ expect(result["user-desktop"]?.sandboxHandle).toBe("v2");
139
+ });
140
+
141
+ test("returns empty object for null/undefined/arrays", () => {
142
+ expect(parseBranchMap(null)).toEqual({});
143
+ expect(parseBranchMap(undefined)).toEqual({});
144
+ expect(parseBranchMap([])).toEqual({});
145
+ });
146
+
147
+ test("skips entries under legacy/retired kind keys", () => {
148
+ // Migrations 092/097 rewrote/dropped every legacy and retired key; reader
149
+ // no longer normalizes unknown keys, it just ignores them.
150
+ const result = parseBranchMap({
151
+ docker: {
152
+ sandboxHandle: "v-legacy",
153
+ previewUrl: null,
154
+ sandboxProviderKind: "cluster",
155
+ },
156
+ "local-docker": {
157
+ sandboxHandle: "v-retired",
158
+ previewUrl: null,
159
+ sandboxProviderKind: "cluster",
160
+ },
161
+ });
162
+ expect(result).toEqual({});
163
+ });
164
+ });
165
+
166
+ describe("parseSandboxRecord", () => {
167
+ test("accepts canonical sandboxProviderKind", () => {
168
+ const result = parseSandboxRecord({
169
+ sandboxHandle: "v1",
170
+ previewUrl: null,
171
+ sandboxProviderKind: "cluster",
172
+ });
173
+ expect(result.sandboxProviderKind).toBe("cluster");
174
+ });
175
+
176
+ test("rejects legacy/retired kind values", () => {
177
+ expect(() =>
178
+ parseSandboxRecord({
179
+ sandboxHandle: "v1",
180
+ previewUrl: null,
181
+ sandboxProviderKind: "docker",
182
+ }),
183
+ ).toThrow();
184
+ expect(() =>
185
+ parseSandboxRecord({
186
+ sandboxHandle: "v1",
187
+ previewUrl: null,
188
+ sandboxProviderKind: "local-docker",
189
+ }),
190
+ ).toThrow();
191
+ });
192
+
193
+ test("rejects rows missing `sandboxHandle` (legacy `vmId` no longer accepted)", () => {
194
+ expect(() =>
195
+ parseSandboxRecord({
196
+ vmId: "v-pre-rename",
197
+ previewUrl: null,
198
+ sandboxProviderKind: "cluster",
199
+ }),
200
+ ).toThrow();
201
+ });
202
+ });