@vellumai/assistant 0.10.2-dev.202606250202.4960321 → 0.10.2-dev.202606250318.5e7cfb0
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/__tests__/config-loader-backfill.test.ts +10 -9
- package/src/__tests__/mcp-config-secret-boundary.test.ts +390 -0
- package/src/config/__tests__/sync-gated-profiles.test.ts +11 -3
- package/src/config/feature-flag-registry.json +1 -1
- package/src/config/seed-inference-profiles.ts +7 -6
- package/src/config/sync-gated-profiles.ts +12 -13
- package/src/runtime/routes/conversation-query-routes.ts +72 -0
package/package.json
CHANGED
|
@@ -1703,8 +1703,8 @@ describe("seedInferenceProfiles BYOK-mode managed profile labels", () => {
|
|
|
1703
1703
|
// ---------------------------------------------------------------------------
|
|
1704
1704
|
// Tests: OS Beta flag-gated managed profile. The template is defined but
|
|
1705
1705
|
// intentionally NOT part of MANAGED_PROFILE_TEMPLATES, so seedInferenceProfiles
|
|
1706
|
-
// must never create it.
|
|
1707
|
-
// feature flag.
|
|
1706
|
+
// must never create it. The flag-gated reconcile creates or removes it based on
|
|
1707
|
+
// the `os-beta` feature flag.
|
|
1708
1708
|
// ---------------------------------------------------------------------------
|
|
1709
1709
|
|
|
1710
1710
|
describe("OS Beta managed profile template", () => {
|
|
@@ -1751,20 +1751,21 @@ describe("OS Beta managed profile template", () => {
|
|
|
1751
1751
|
expect(MANAGED_PROFILE_NAMES.has("os-beta")).toBe(true);
|
|
1752
1752
|
});
|
|
1753
1753
|
|
|
1754
|
-
test("materializeProfile
|
|
1754
|
+
test("materializeProfile resolves OS Beta to the Balanced model with low effort", () => {
|
|
1755
1755
|
const entry = materializeProfile(
|
|
1756
1756
|
OS_BETA_PROFILE_TEMPLATE,
|
|
1757
|
-
"
|
|
1758
|
-
"
|
|
1757
|
+
"together",
|
|
1758
|
+
"together-managed",
|
|
1759
1759
|
);
|
|
1760
1760
|
|
|
1761
|
-
expect(entry.model).toBe("
|
|
1762
|
-
expect(entry.provider_connection).toBe("
|
|
1763
|
-
expect(entry.provider).toBe("
|
|
1761
|
+
expect(entry.model).toBe("MiniMaxAI/MiniMax-M3");
|
|
1762
|
+
expect(entry.provider_connection).toBe("together-managed");
|
|
1763
|
+
expect(entry.provider).toBe("together");
|
|
1764
1764
|
expect(entry.label).toBe("OS Beta");
|
|
1765
1765
|
expect(entry.source).toBe("managed");
|
|
1766
1766
|
expect(entry.maxTokens).toBe(32000);
|
|
1767
|
-
expect(entry.effort).toBe("
|
|
1767
|
+
expect(entry.effort).toBe("low");
|
|
1768
1768
|
expect(entry.thinking?.enabled).toBe(true);
|
|
1769
|
+
expect(entry.topP).toBe(0.95);
|
|
1769
1770
|
});
|
|
1770
1771
|
});
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { makeMockLogger } from "./helpers/mock-logger.js";
|
|
4
|
+
|
|
5
|
+
mock.module("../util/logger.js", () => ({
|
|
6
|
+
LOG_FILE_PATTERN: /^assistant-(\d{4}-\d{2}-\d{2})\.log$/,
|
|
7
|
+
getCliLogger: () => makeMockLogger(),
|
|
8
|
+
getLogger: () => makeMockLogger(),
|
|
9
|
+
initLogger: () => {},
|
|
10
|
+
pruneOldLogFiles: () => 0,
|
|
11
|
+
truncateForLog: (value: string, maxLen = 500) => value.slice(0, maxLen),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
let rawConfig: Record<string, unknown> = {};
|
|
15
|
+
let savedRawConfig: Record<string, unknown> | null = null;
|
|
16
|
+
|
|
17
|
+
function deepMerge(
|
|
18
|
+
target: Record<string, unknown>,
|
|
19
|
+
patch: Record<string, unknown>,
|
|
20
|
+
): void {
|
|
21
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
22
|
+
if (
|
|
23
|
+
value !== null &&
|
|
24
|
+
typeof value === "object" &&
|
|
25
|
+
!Array.isArray(value) &&
|
|
26
|
+
target[key] !== null &&
|
|
27
|
+
typeof target[key] === "object" &&
|
|
28
|
+
!Array.isArray(target[key])
|
|
29
|
+
) {
|
|
30
|
+
deepMerge(
|
|
31
|
+
target[key] as Record<string, unknown>,
|
|
32
|
+
value as Record<string, unknown>,
|
|
33
|
+
);
|
|
34
|
+
} else {
|
|
35
|
+
target[key] = value;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function setNestedValue(
|
|
41
|
+
obj: Record<string, unknown>,
|
|
42
|
+
path: string,
|
|
43
|
+
value: unknown,
|
|
44
|
+
): void {
|
|
45
|
+
const keys = path.split(".");
|
|
46
|
+
let current = obj;
|
|
47
|
+
for (const key of keys.slice(0, -1)) {
|
|
48
|
+
if (
|
|
49
|
+
current[key] === null ||
|
|
50
|
+
typeof current[key] !== "object" ||
|
|
51
|
+
Array.isArray(current[key])
|
|
52
|
+
) {
|
|
53
|
+
current[key] = {};
|
|
54
|
+
}
|
|
55
|
+
current = current[key] as Record<string, unknown>;
|
|
56
|
+
}
|
|
57
|
+
current[keys[keys.length - 1]!] = value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
mock.module("../config/loader.js", () => ({
|
|
61
|
+
API_KEY_PROVIDERS: [],
|
|
62
|
+
applyNestedDefaults: (config: unknown) => config,
|
|
63
|
+
loadRawConfig: () => structuredClone(savedRawConfig ?? rawConfig),
|
|
64
|
+
saveRawConfig: (raw: Record<string, unknown>) => {
|
|
65
|
+
savedRawConfig = structuredClone(raw);
|
|
66
|
+
},
|
|
67
|
+
deepMergeOverwrite: deepMerge,
|
|
68
|
+
fillContextDefaultsForMissingKeys: () => {},
|
|
69
|
+
loadConfig: () => structuredClone(savedRawConfig ?? rawConfig),
|
|
70
|
+
getConfig: () => structuredClone(savedRawConfig ?? rawConfig),
|
|
71
|
+
getConfigReadOnly: () => structuredClone(savedRawConfig ?? rawConfig),
|
|
72
|
+
getDeploymentContextDefaults: () => ({}),
|
|
73
|
+
getNestedValue: (obj: Record<string, unknown>, path: string) =>
|
|
74
|
+
path.split(".").reduce<unknown>((current, key) => {
|
|
75
|
+
if (
|
|
76
|
+
current === null ||
|
|
77
|
+
typeof current !== "object" ||
|
|
78
|
+
Array.isArray(current)
|
|
79
|
+
) {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
return (current as Record<string, unknown>)[key];
|
|
83
|
+
}, obj),
|
|
84
|
+
invalidateConfigCache: () => {},
|
|
85
|
+
mergeDefaultWorkspaceConfig: () => ({
|
|
86
|
+
merged: false,
|
|
87
|
+
config: structuredClone(savedRawConfig ?? rawConfig),
|
|
88
|
+
}),
|
|
89
|
+
setNestedValue,
|
|
90
|
+
withSuppressedConfigDiskWrites: async (fn: () => unknown) => fn(),
|
|
91
|
+
withSuppressedConfigDiskWritesSync: (fn: () => unknown) => fn(),
|
|
92
|
+
_writeQuarantineNotice: () => {},
|
|
93
|
+
}));
|
|
94
|
+
|
|
95
|
+
mock.module("../daemon/config-watcher.js", () => ({
|
|
96
|
+
getConfigWatcher: () => ({
|
|
97
|
+
suppressConfigReload: false,
|
|
98
|
+
timers: { schedule: () => {} },
|
|
99
|
+
updateFingerprint: () => {},
|
|
100
|
+
}),
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
mock.module("../providers/registry.js", () => ({
|
|
104
|
+
clearConnectionProviderCache: () => {},
|
|
105
|
+
getProvider: () => {
|
|
106
|
+
throw new Error("provider registry mock not implemented");
|
|
107
|
+
},
|
|
108
|
+
getProviderRoutingSource: () => null,
|
|
109
|
+
initializeProviders: async () => {},
|
|
110
|
+
isNativeWebSearchCapableProvider: () => false,
|
|
111
|
+
listProviders: () => [],
|
|
112
|
+
resolveProviderFromConnection: async () => null,
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
mock.module("../memory/embedding-backend.js", () => ({
|
|
116
|
+
EmbeddingBackendUnavailableError: class EmbeddingBackendUnavailableError extends Error {},
|
|
117
|
+
SPARSE_EMBEDDING_VERSION: 4,
|
|
118
|
+
clearEmbeddingBackendCache: () => {},
|
|
119
|
+
embedWithBackend: async () => ({
|
|
120
|
+
provider: "local",
|
|
121
|
+
model: "test",
|
|
122
|
+
vectors: [],
|
|
123
|
+
}),
|
|
124
|
+
generateSparseEmbedding: () => ({ indices: [], values: [] }),
|
|
125
|
+
getMemoryBackendStatus: async () => ({
|
|
126
|
+
enabled: false,
|
|
127
|
+
provider: null,
|
|
128
|
+
model: null,
|
|
129
|
+
}),
|
|
130
|
+
resetLocalEmbeddingFailureState: () => {},
|
|
131
|
+
selectEmbeddingBackend: async () => null,
|
|
132
|
+
selectedBackendSupportsMultimodal: async () => false,
|
|
133
|
+
}));
|
|
134
|
+
|
|
135
|
+
mock.module("../security/secret-allowlist.js", () => ({
|
|
136
|
+
isAllowlisted: () => false,
|
|
137
|
+
loadAllowlist: () => {},
|
|
138
|
+
resetAllowlist: () => {},
|
|
139
|
+
validateAllowlistFile: () => null,
|
|
140
|
+
}));
|
|
141
|
+
|
|
142
|
+
const { ROUTES } =
|
|
143
|
+
await import("../runtime/routes/conversation-query-routes.js");
|
|
144
|
+
const { BadRequestError } = await import("../runtime/routes/errors.js");
|
|
145
|
+
|
|
146
|
+
function findRoute(operationId: string) {
|
|
147
|
+
const route = ROUTES.find((r) => r.operationId === operationId);
|
|
148
|
+
if (!route) throw new Error(`Route not found: ${operationId}`);
|
|
149
|
+
return route;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const configGetRoute = findRoute("config_get");
|
|
153
|
+
const configPatchRoute = findRoute("config_patch");
|
|
154
|
+
const configSetRoute = findRoute("config_set");
|
|
155
|
+
|
|
156
|
+
describe("MCP config secret boundary", () => {
|
|
157
|
+
beforeEach(() => {
|
|
158
|
+
rawConfig = {};
|
|
159
|
+
savedRawConfig = null;
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("config_get omits legacy MCP transport headers from settings-read responses", () => {
|
|
163
|
+
rawConfig = {
|
|
164
|
+
mcp: {
|
|
165
|
+
servers: {
|
|
166
|
+
remote: {
|
|
167
|
+
transport: {
|
|
168
|
+
type: "streamable-http",
|
|
169
|
+
url: "https://mcp.example.com",
|
|
170
|
+
headers: {
|
|
171
|
+
Authorization: "Bearer mcp-secret",
|
|
172
|
+
"X-API-Key": "mcp-api-secret",
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const result = configGetRoute.handler({}) as Record<string, unknown>;
|
|
181
|
+
|
|
182
|
+
expect(JSON.stringify(result)).not.toContain("mcp-secret");
|
|
183
|
+
expect(JSON.stringify(result)).not.toContain("mcp-api-secret");
|
|
184
|
+
const mcp = result.mcp as {
|
|
185
|
+
servers: { remote: { transport: Record<string, unknown> } };
|
|
186
|
+
};
|
|
187
|
+
expect(mcp.servers.remote.transport).toEqual({
|
|
188
|
+
type: "streamable-http",
|
|
189
|
+
url: "https://mcp.example.com",
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("config_get omits headers inside malformed MCP server trees", () => {
|
|
194
|
+
rawConfig = {
|
|
195
|
+
mcp: {
|
|
196
|
+
servers: [
|
|
197
|
+
{
|
|
198
|
+
transport: {
|
|
199
|
+
headers: { Authorization: "Bearer malformed-secret" },
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
],
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const result = configGetRoute.handler({}) as Record<string, unknown>;
|
|
207
|
+
|
|
208
|
+
expect(JSON.stringify(result)).not.toContain("malformed-secret");
|
|
209
|
+
expect(result).toEqual({
|
|
210
|
+
mcp: {
|
|
211
|
+
servers: [
|
|
212
|
+
{
|
|
213
|
+
transport: {},
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("config_get preserves an MCP server named headers", () => {
|
|
221
|
+
rawConfig = {
|
|
222
|
+
mcp: {
|
|
223
|
+
servers: {
|
|
224
|
+
headers: {
|
|
225
|
+
transport: {
|
|
226
|
+
type: "streamable-http",
|
|
227
|
+
url: "https://mcp.example.com",
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const result = configGetRoute.handler({}) as Record<string, unknown>;
|
|
235
|
+
|
|
236
|
+
expect(result).toEqual(rawConfig);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("config_get preserves non-credential headers env vars", () => {
|
|
240
|
+
rawConfig = {
|
|
241
|
+
mcp: {
|
|
242
|
+
servers: {
|
|
243
|
+
local: {
|
|
244
|
+
transport: {
|
|
245
|
+
type: "stdio",
|
|
246
|
+
command: "npx",
|
|
247
|
+
env: {
|
|
248
|
+
headers: "not-a-transport-header",
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const result = configGetRoute.handler({}) as Record<string, unknown>;
|
|
257
|
+
|
|
258
|
+
expect(result).toEqual(rawConfig);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("config_patch rejects MCP transport headers so generic writes cannot reintroduce plaintext credentials", async () => {
|
|
262
|
+
await expect(
|
|
263
|
+
configPatchRoute.handler({
|
|
264
|
+
body: {
|
|
265
|
+
mcp: {
|
|
266
|
+
servers: {
|
|
267
|
+
remote: {
|
|
268
|
+
transport: {
|
|
269
|
+
type: "streamable-http",
|
|
270
|
+
url: "https://mcp.example.com",
|
|
271
|
+
headers: { Authorization: "Bearer mcp-secret" },
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
}),
|
|
278
|
+
).rejects.toThrow(BadRequestError);
|
|
279
|
+
expect(savedRawConfig).toBeNull();
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("config_patch allows an MCP server named headers when its value has no header credentials", async () => {
|
|
283
|
+
const result = await configPatchRoute.handler({
|
|
284
|
+
body: {
|
|
285
|
+
mcp: {
|
|
286
|
+
servers: {
|
|
287
|
+
headers: {
|
|
288
|
+
transport: {
|
|
289
|
+
type: "streamable-http",
|
|
290
|
+
url: "https://mcp.example.com",
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
expect(result).toEqual({
|
|
299
|
+
mcp: {
|
|
300
|
+
servers: {
|
|
301
|
+
headers: {
|
|
302
|
+
transport: {
|
|
303
|
+
type: "streamable-http",
|
|
304
|
+
url: "https://mcp.example.com",
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test("config_patch allows non-credential headers env vars", async () => {
|
|
313
|
+
const result = await configPatchRoute.handler({
|
|
314
|
+
body: {
|
|
315
|
+
mcp: {
|
|
316
|
+
servers: {
|
|
317
|
+
local: {
|
|
318
|
+
transport: {
|
|
319
|
+
type: "stdio",
|
|
320
|
+
command: "npx",
|
|
321
|
+
env: {
|
|
322
|
+
headers: "not-a-transport-header",
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
expect(result).toEqual({
|
|
332
|
+
mcp: {
|
|
333
|
+
servers: {
|
|
334
|
+
local: {
|
|
335
|
+
transport: {
|
|
336
|
+
type: "stdio",
|
|
337
|
+
command: "npx",
|
|
338
|
+
env: {
|
|
339
|
+
headers: "not-a-transport-header",
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("config_set rejects malformed MCP server trees containing headers", async () => {
|
|
349
|
+
await expect(
|
|
350
|
+
configSetRoute.handler({
|
|
351
|
+
body: {
|
|
352
|
+
path: "mcp.servers",
|
|
353
|
+
value: [
|
|
354
|
+
{
|
|
355
|
+
transport: {
|
|
356
|
+
headers: { Authorization: "Bearer malformed-secret" },
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
],
|
|
360
|
+
},
|
|
361
|
+
}),
|
|
362
|
+
).rejects.toThrow(BadRequestError);
|
|
363
|
+
expect(savedRawConfig).toBeNull();
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test("config_set rejects direct MCP transport header paths", async () => {
|
|
367
|
+
rawConfig = {
|
|
368
|
+
mcp: {
|
|
369
|
+
servers: {
|
|
370
|
+
remote: {
|
|
371
|
+
transport: {
|
|
372
|
+
type: "streamable-http",
|
|
373
|
+
url: "https://mcp.example.com",
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
await expect(
|
|
381
|
+
configSetRoute.handler({
|
|
382
|
+
body: {
|
|
383
|
+
path: "mcp.servers.remote.transport.headers.Authorization",
|
|
384
|
+
value: "Bearer mcp-secret",
|
|
385
|
+
},
|
|
386
|
+
}),
|
|
387
|
+
).rejects.toThrow(BadRequestError);
|
|
388
|
+
expect(savedRawConfig).toBeNull();
|
|
389
|
+
});
|
|
390
|
+
});
|
|
@@ -109,10 +109,13 @@ describe("reconcileFlagGatedProfiles", () => {
|
|
|
109
109
|
|
|
110
110
|
const raw = readConfig();
|
|
111
111
|
const osBeta = raw.llm.profiles["os-beta"]!;
|
|
112
|
-
expect(osBeta.model).toBe("
|
|
113
|
-
expect(osBeta.provider_connection).toBe("
|
|
112
|
+
expect(osBeta.model).toBe("MiniMaxAI/MiniMax-M3");
|
|
113
|
+
expect(osBeta.provider_connection).toBe("together-managed");
|
|
114
|
+
expect(osBeta.provider).toBe("together");
|
|
114
115
|
expect(osBeta.source).toBe("managed");
|
|
115
116
|
expect(osBeta.label).toBe("OS Beta");
|
|
117
|
+
expect(osBeta.effort).toBe("low");
|
|
118
|
+
expect(osBeta.topP).toBe(0.95);
|
|
116
119
|
|
|
117
120
|
const order = raw.llm.profileOrder;
|
|
118
121
|
expect(order.indexOf("os-beta")).toBe(order.indexOf("balanced") + 1);
|
|
@@ -128,6 +131,7 @@ describe("reconcileFlagGatedProfiles", () => {
|
|
|
128
131
|
expect(osBeta.status).toBe("disabled");
|
|
129
132
|
expect(osBeta.label).toBe("OS Beta (Managed)");
|
|
130
133
|
expect(osBeta.source).toBe("managed");
|
|
134
|
+
expect(osBeta.effort).toBe("low");
|
|
131
135
|
});
|
|
132
136
|
|
|
133
137
|
test("flag on is idempotent across repeated runs", () => {
|
|
@@ -150,6 +154,7 @@ describe("reconcileFlagGatedProfiles", () => {
|
|
|
150
154
|
raw.llm.profiles["os-beta"]!.label = "My OS Beta";
|
|
151
155
|
raw.llm.profiles["os-beta"]!.status = "disabled";
|
|
152
156
|
raw.llm.profiles["os-beta"]!.advisorEnabled = true;
|
|
157
|
+
raw.llm.profiles["os-beta"]!.topP = 0.8;
|
|
153
158
|
writeConfig(raw);
|
|
154
159
|
invalidateConfigCache();
|
|
155
160
|
|
|
@@ -159,7 +164,10 @@ describe("reconcileFlagGatedProfiles", () => {
|
|
|
159
164
|
expect(after.label).toBe("My OS Beta");
|
|
160
165
|
expect(after.status).toBe("disabled");
|
|
161
166
|
expect(after.advisorEnabled).toBe(true);
|
|
162
|
-
expect(after.
|
|
167
|
+
expect(after.topP).toBe(0.8);
|
|
168
|
+
expect(after.model).toBe("MiniMaxAI/MiniMax-M3");
|
|
169
|
+
expect(after.provider_connection).toBe("together-managed");
|
|
170
|
+
expect(after.effort).toBe("low");
|
|
163
171
|
});
|
|
164
172
|
|
|
165
173
|
test("flag off removes a managed os-beta and applies fallbacks", () => {
|
|
@@ -415,7 +415,7 @@
|
|
|
415
415
|
"scope": "assistant",
|
|
416
416
|
"key": "os-beta",
|
|
417
417
|
"label": "OS Beta",
|
|
418
|
-
"description": "Enable the OS Beta model profile (
|
|
418
|
+
"description": "Enable the OS Beta model profile (MiniMax M3 / Together) in the assistant's model profile selection.",
|
|
419
419
|
"defaultEnabled": false
|
|
420
420
|
}
|
|
421
421
|
]
|
|
@@ -164,19 +164,20 @@ export const OS_BETA_FEATURE_FLAG_KEY = "os-beta";
|
|
|
164
164
|
* Flag-gated managed profile. NOT in MANAGED_PROFILE_TEMPLATES, so the
|
|
165
165
|
* unconditional boot seed never creates it. Reconciled in/out by
|
|
166
166
|
* the flag-gated profile reconcile based on the `os-beta` feature flag.
|
|
167
|
-
* Balanced
|
|
167
|
+
* Balanced defaults, with lower reasoning effort while the profile is in beta.
|
|
168
168
|
*/
|
|
169
169
|
export const OS_BETA_PROFILE_TEMPLATE: ManagedProfileTemplate = {
|
|
170
|
-
|
|
171
|
-
provider: "
|
|
172
|
-
connectionName: "
|
|
170
|
+
intent: "balanced",
|
|
171
|
+
provider: "together",
|
|
172
|
+
connectionName: "together-managed",
|
|
173
173
|
source: "managed",
|
|
174
174
|
label: "OS Beta",
|
|
175
|
-
description: "
|
|
175
|
+
description: "Good balance of quality, cost, and speed, in beta",
|
|
176
176
|
maxTokens: 32000,
|
|
177
|
-
effort: "
|
|
177
|
+
effort: "low",
|
|
178
178
|
thinking: { enabled: true, streamThinking: true },
|
|
179
179
|
contextWindow: { maxInputTokens: DEFAULT_CONTEXT_WINDOW_MAX_INPUT_TOKENS },
|
|
180
|
+
topP: 0.95,
|
|
180
181
|
};
|
|
181
182
|
|
|
182
183
|
// Membership here marks a name as managed. The route layer applies managed
|
|
@@ -23,12 +23,12 @@ const log = getLogger("sync-gated-profiles");
|
|
|
23
23
|
* Reconcile flag-gated managed profiles against the current feature-flag state.
|
|
24
24
|
*
|
|
25
25
|
* `seedInferenceProfiles()` runs synchronously at boot before feature flags are
|
|
26
|
-
* available, so the OS Beta profile (
|
|
27
|
-
* here once flags have loaded. When the `os-beta` flag is on, the
|
|
28
|
-
* is created (ordered right after `balanced`); when it is off, a
|
|
29
|
-
* managed entry is removed with `profileOrder` / `activeProfile` /
|
|
30
|
-
* fallbacks. The reconcile is idempotent and never touches a
|
|
31
|
-
* the same name.
|
|
26
|
+
* available, so the OS Beta profile (MiniMax M3 / together-managed) is
|
|
27
|
+
* materialized here once flags have loaded. When the `os-beta` flag is on, the
|
|
28
|
+
* managed profile is created (ordered right after `balanced`); when it is off, a
|
|
29
|
+
* previously managed entry is removed with `profileOrder` / `activeProfile` /
|
|
30
|
+
* `advisorProfile` fallbacks. The reconcile is idempotent and never touches a
|
|
31
|
+
* user-owned profile of the same name.
|
|
32
32
|
*
|
|
33
33
|
* Returns whether the on-disk config changed.
|
|
34
34
|
*/
|
|
@@ -105,23 +105,22 @@ function enableProfile(
|
|
|
105
105
|
OS_BETA_PROFILE_TEMPLATE.connectionName,
|
|
106
106
|
) as Record<string, unknown>;
|
|
107
107
|
|
|
108
|
-
// BYOK installs seed managed profiles disabled: the
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
// later reconciles.
|
|
108
|
+
// BYOK installs seed managed profiles disabled: the managed inference
|
|
109
|
+
// connection backing this profile isn't usable until the user enables it, so a
|
|
110
|
+
// fresh OS Beta entry starts disabled to avoid offering an unusable route. A
|
|
111
|
+
// user's own status override (preserved below) wins on later reconciles.
|
|
113
112
|
if (isByokMode && !previous) {
|
|
114
113
|
next.status = "disabled";
|
|
115
114
|
}
|
|
116
115
|
|
|
117
116
|
if (previous) {
|
|
118
|
-
//
|
|
119
|
-
// by key-presence so an explicit null (user cleared it) survives too.
|
|
117
|
+
// Preserve user-owned overrides across reconciles.
|
|
120
118
|
if ("label" in previous) next.label = previous.label;
|
|
121
119
|
if ("status" in previous) next.status = previous.status;
|
|
122
120
|
if ("advisorEnabled" in previous) {
|
|
123
121
|
next.advisorEnabled = previous.advisorEnabled;
|
|
124
122
|
}
|
|
123
|
+
if ("topP" in previous) next.topP = previous.topP;
|
|
125
124
|
}
|
|
126
125
|
|
|
127
126
|
let changed = false;
|
|
@@ -486,6 +486,74 @@ function readPlainObject(value: unknown): Record<string, unknown> | undefined {
|
|
|
486
486
|
return value as Record<string, unknown>;
|
|
487
487
|
}
|
|
488
488
|
|
|
489
|
+
function stripTransportHeadersRecursively(value: unknown): void {
|
|
490
|
+
if (Array.isArray(value)) {
|
|
491
|
+
for (const item of value) {
|
|
492
|
+
stripTransportHeadersRecursively(item);
|
|
493
|
+
}
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const object = readPlainObject(value);
|
|
498
|
+
if (!object) return;
|
|
499
|
+
const transport = readPlainObject(object.transport);
|
|
500
|
+
if (transport) delete transport.headers;
|
|
501
|
+
for (const child of Object.values(object)) {
|
|
502
|
+
stripTransportHeadersRecursively(child);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function containsTransportHeadersRecursively(value: unknown): boolean {
|
|
507
|
+
if (Array.isArray(value)) {
|
|
508
|
+
return value.some((item) => containsTransportHeadersRecursively(item));
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const object = readPlainObject(value);
|
|
512
|
+
if (!object) return false;
|
|
513
|
+
const transport = readPlainObject(object.transport);
|
|
514
|
+
if (transport && Object.hasOwn(transport, "headers")) return true;
|
|
515
|
+
return Object.values(object).some((child) =>
|
|
516
|
+
containsTransportHeadersRecursively(child),
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
function sanitizeMcpTransportHeadersForSettingsRead(config: unknown): void {
|
|
521
|
+
const root = readPlainObject(config);
|
|
522
|
+
if (!root) return;
|
|
523
|
+
const mcp = readPlainObject(root.mcp);
|
|
524
|
+
if (!mcp || !Object.hasOwn(mcp, "servers")) return;
|
|
525
|
+
if (Array.isArray(mcp.servers)) {
|
|
526
|
+
stripTransportHeadersRecursively(mcp.servers);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const servers = readPlainObject(mcp.servers);
|
|
530
|
+
if (!servers) return;
|
|
531
|
+
for (const server of Object.values(servers)) {
|
|
532
|
+
stripTransportHeadersRecursively(server);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function patchContainsMcpTransportHeaders(patch: unknown): boolean {
|
|
537
|
+
const root = readPlainObject(patch);
|
|
538
|
+
const mcp = readPlainObject(root?.mcp);
|
|
539
|
+
if (!mcp || !Object.hasOwn(mcp, "servers")) return false;
|
|
540
|
+
if (Array.isArray(mcp.servers)) {
|
|
541
|
+
return containsTransportHeadersRecursively(mcp.servers);
|
|
542
|
+
}
|
|
543
|
+
const servers = readPlainObject(mcp.servers);
|
|
544
|
+
if (!servers) return false;
|
|
545
|
+
return Object.values(servers).some((server) =>
|
|
546
|
+
containsTransportHeadersRecursively(server),
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function rejectMcpTransportHeaderWrite(patch: unknown): void {
|
|
551
|
+
if (!patchContainsMcpTransportHeaders(patch)) return;
|
|
552
|
+
throw new BadRequestError(
|
|
553
|
+
"MCP authentication headers must be managed through MCP server add/update APIs, not generic config writes.",
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
489
557
|
const WireProfileEntry = ProfileEntry.extend({
|
|
490
558
|
supportsVision: z.boolean().optional(),
|
|
491
559
|
})
|
|
@@ -688,6 +756,7 @@ const ConfigPatchRequestSchema = z
|
|
|
688
756
|
function handleGetConfig() {
|
|
689
757
|
try {
|
|
690
758
|
const config = applyContextDefaultsToRawConfig(loadRawConfig());
|
|
759
|
+
sanitizeMcpTransportHeadersForSettingsRead(config);
|
|
691
760
|
enrichProfilesWithVisionFlag(config);
|
|
692
761
|
return config;
|
|
693
762
|
} catch (err) {
|
|
@@ -840,6 +909,7 @@ async function handlePatchConfig({ body }: RouteHandlerArgs) {
|
|
|
840
909
|
throw new BadRequestError("Body must be a non-empty JSON object");
|
|
841
910
|
}
|
|
842
911
|
rejectManagedProfileDeletion(body as Record<string, unknown>);
|
|
912
|
+
rejectMcpTransportHeaderWrite(body);
|
|
843
913
|
|
|
844
914
|
const raw = loadRawConfig();
|
|
845
915
|
const patch = body as Record<string, unknown>;
|
|
@@ -848,6 +918,7 @@ async function handlePatchConfig({ body }: RouteHandlerArgs) {
|
|
|
848
918
|
await commitConfigWrite(raw, "patch");
|
|
849
919
|
|
|
850
920
|
const merged = applyContextDefaultsToRawConfig(loadRawConfig());
|
|
921
|
+
sanitizeMcpTransportHeadersForSettingsRead(merged);
|
|
851
922
|
enrichProfilesWithVisionFlag(merged);
|
|
852
923
|
return merged;
|
|
853
924
|
}
|
|
@@ -892,6 +963,7 @@ async function handleSetConfig({ body }: RouteHandlerArgs) {
|
|
|
892
963
|
const patchShape: Record<string, unknown> = {};
|
|
893
964
|
setNestedValue(patchShape, path, value);
|
|
894
965
|
rejectManagedProfileDeletion(patchShape);
|
|
966
|
+
rejectMcpTransportHeaderWrite(patchShape);
|
|
895
967
|
|
|
896
968
|
const raw = loadRawConfig();
|
|
897
969
|
setNestedValue(raw, path, value);
|