@vellumai/assistant 0.8.2 → 0.8.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.
Files changed (231) hide show
  1. package/ARCHITECTURE.md +11 -12
  2. package/docker-entrypoint.sh +13 -1
  3. package/docker-init-apt-root.sh +79 -6
  4. package/openapi.yaml +336 -21
  5. package/package.json +1 -1
  6. package/src/__tests__/agent-loop-exit-reason.test.ts +272 -0
  7. package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
  8. package/src/__tests__/compactor-tail-resolution.test.ts +107 -1
  9. package/src/__tests__/config-get-vision-flag.test.ts +136 -0
  10. package/src/__tests__/config-loader-backfill.test.ts +115 -18
  11. package/src/__tests__/context-token-estimator.test.ts +30 -65
  12. package/src/__tests__/conversation-agent-loop.test.ts +57 -1
  13. package/src/__tests__/conversation-media-retry.test.ts +19 -8
  14. package/src/__tests__/conversation-runtime-assembly.test.ts +26 -4
  15. package/src/__tests__/date-context.test.ts +45 -0
  16. package/src/__tests__/external-plugin-loader.test.ts +91 -19
  17. package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
  18. package/src/__tests__/guardian-dispatch.test.ts +1 -0
  19. package/src/__tests__/heartbeat-service.test.ts +24 -164
  20. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  21. package/src/__tests__/host-app-control-proxy.test.ts +241 -0
  22. package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
  23. package/src/__tests__/injector-background-turn.test.ts +153 -0
  24. package/src/__tests__/injector-chain.test.ts +5 -0
  25. package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -2
  26. package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
  27. package/src/__tests__/llm-catalog-parity.test.ts +3 -0
  28. package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
  29. package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
  30. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -0
  31. package/src/__tests__/llm-resolver.test.ts +255 -2
  32. package/src/__tests__/managed-profile-guard.test.ts +10 -0
  33. package/src/__tests__/notification-decision-fallback.test.ts +0 -91
  34. package/src/__tests__/notification-decision-strategy.test.ts +14 -31
  35. package/src/__tests__/notification-deep-link.test.ts +15 -0
  36. package/src/__tests__/notification-guardian-path.test.ts +1 -2
  37. package/src/__tests__/notification-platform-adapter.test.ts +5 -4
  38. package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
  39. package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
  40. package/src/__tests__/openai-provider.test.ts +218 -3
  41. package/src/__tests__/openai-responses-cutover-guard.test.ts +3 -3
  42. package/src/__tests__/openrouter-provider-only.test.ts +51 -3
  43. package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
  44. package/src/__tests__/platform-proxy-context.test.ts +6 -1
  45. package/src/__tests__/plugin-tool-contribution.test.ts +3 -3
  46. package/src/__tests__/plugin-types.test.ts +2 -2
  47. package/src/__tests__/provider-catalog-visibility.test.ts +16 -0
  48. package/src/__tests__/provider-platform-proxy-integration.test.ts +27 -25
  49. package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
  50. package/src/__tests__/system-prompt.test.ts +6 -73
  51. package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
  52. package/src/a2a/__tests__/agent-card.test.ts +98 -0
  53. package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
  54. package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
  55. package/src/a2a/__tests__/task-store.test.ts +246 -0
  56. package/src/a2a/agent-card.ts +58 -0
  57. package/src/a2a/feature-gate.ts +8 -0
  58. package/src/a2a/protocol-constants.ts +21 -0
  59. package/src/a2a/protocol-errors.ts +50 -0
  60. package/src/a2a/protocol-types.ts +162 -0
  61. package/src/a2a/task-store.ts +168 -0
  62. package/src/agent/loop.ts +167 -18
  63. package/src/channels/config.ts +9 -0
  64. package/src/channels/types.ts +14 -0
  65. package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
  66. package/src/cli/commands/__tests__/schedules.test.ts +469 -0
  67. package/src/cli/commands/notifications.ts +65 -35
  68. package/src/cli/commands/plugins.ts +67 -0
  69. package/src/cli/commands/schedules.ts +297 -5
  70. package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
  71. package/src/cli/lib/install-from-github.ts +8 -9
  72. package/src/cli/lib/search-plugins.ts +163 -0
  73. package/src/cli/program.ts +14 -0
  74. package/src/config/assistant-feature-flags.ts +24 -54
  75. package/src/config/bundled-skills/app-builder/SKILL.md +117 -1
  76. package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
  77. package/src/config/call-site-defaults.ts +105 -0
  78. package/src/config/feature-flag-registry.json +21 -29
  79. package/src/config/llm-resolver.ts +52 -1
  80. package/src/config/schema.ts +2 -0
  81. package/src/config/schemas/__tests__/memory-v2.test.ts +3 -3
  82. package/src/config/schemas/channels.ts +9 -0
  83. package/src/config/schemas/conversations.ts +10 -0
  84. package/src/config/schemas/heartbeat.ts +14 -0
  85. package/src/config/schemas/llm.ts +1 -3
  86. package/src/config/schemas/memory-retrospective.ts +1 -1
  87. package/src/config/schemas/memory-v2.ts +4 -4
  88. package/src/config/schemas/memory.ts +3 -1
  89. package/src/config/seed-inference-profiles.ts +99 -29
  90. package/src/context/compactor.ts +72 -12
  91. package/src/context/token-estimator.ts +32 -34
  92. package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -22
  93. package/src/daemon/conversation-agent-loop-handlers.ts +78 -0
  94. package/src/daemon/conversation-agent-loop.ts +29 -2
  95. package/src/daemon/conversation-runtime-assembly.ts +9 -0
  96. package/src/daemon/conversation.ts +0 -7
  97. package/src/daemon/date-context.ts +40 -0
  98. package/src/daemon/guardian-action-generators.ts +1 -125
  99. package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
  100. package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
  101. package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
  102. package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
  103. package/src/daemon/handlers/config-a2a.ts +289 -0
  104. package/src/daemon/handlers/conversations.ts +1 -0
  105. package/src/daemon/host-app-control-proxy.ts +69 -18
  106. package/src/daemon/host-proxy-preactivation.ts +85 -18
  107. package/src/daemon/lifecycle.ts +49 -61
  108. package/src/daemon/memory-v2-startup.ts +49 -13
  109. package/src/daemon/message-types/notifications.ts +21 -0
  110. package/src/daemon/pkb-reminder-builder.test.ts +10 -53
  111. package/src/daemon/pkb-reminder-builder.ts +4 -19
  112. package/src/daemon/process-message.ts +3 -0
  113. package/src/daemon/skill-memory-refresh.ts +5 -1
  114. package/src/daemon/wake-target-adapter.ts +2 -0
  115. package/src/export/__tests__/transcript-formatter.test.ts +121 -0
  116. package/src/export/transcript-formatter.ts +54 -20
  117. package/src/heartbeat/__tests__/heartbeat-service.test.ts +44 -0
  118. package/src/heartbeat/heartbeat-service.ts +34 -191
  119. package/src/home/__tests__/feed-types.test.ts +40 -0
  120. package/src/home/feed-types.ts +14 -2
  121. package/src/ipc/cli-client.ts +147 -45
  122. package/src/memory/__tests__/conversation-queries.test.ts +220 -0
  123. package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
  124. package/src/memory/__tests__/memory-retrospective-job.test.ts +87 -4
  125. package/src/memory/conversation-queries.ts +87 -1
  126. package/src/memory/conversation-title-service.ts +26 -4
  127. package/src/memory/db-init.ts +6 -0
  128. package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +84 -3
  129. package/src/memory/graph/conversation-graph-memory.ts +18 -6
  130. package/src/memory/graph/tools.ts +6 -37
  131. package/src/memory/invite-store.ts +53 -0
  132. package/src/memory/llm-request-log-source-clickhouse.ts +7 -2
  133. package/src/memory/llm-request-log-store.ts +92 -1
  134. package/src/memory/memory-retrospective-enqueue.ts +1 -20
  135. package/src/memory/memory-retrospective-job.ts +33 -6
  136. package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
  137. package/src/memory/migrations/251-a2a-tasks.ts +49 -0
  138. package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
  139. package/src/memory/migrations/index.ts +3 -0
  140. package/src/memory/migrations/registry.ts +8 -0
  141. package/src/memory/schema/a2a.ts +15 -0
  142. package/src/memory/schema/index.ts +1 -0
  143. package/src/memory/schema/inference.ts +2 -0
  144. package/src/memory/schema/infrastructure.ts +1 -0
  145. package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
  146. package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
  147. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
  148. package/src/memory/v2/__tests__/injection.test.ts +190 -3
  149. package/src/memory/v2/__tests__/static-context.test.ts +12 -1
  150. package/src/memory/v2/activation-store.ts +14 -16
  151. package/src/memory/v2/cli-command-content.ts +19 -0
  152. package/src/memory/v2/cli-command-store.ts +304 -0
  153. package/src/memory/v2/frontmatter-sweep.ts +7 -1
  154. package/src/memory/v2/injection.ts +49 -20
  155. package/src/memory/v2/page-index.ts +38 -13
  156. package/src/memory/v2/static-context.ts +4 -4
  157. package/src/memory/v2/types.ts +23 -0
  158. package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
  159. package/src/messaging/providers/a2a/deliver.ts +156 -0
  160. package/src/messaging/providers/gmail/client.ts +9 -2
  161. package/src/messaging/providers/index.ts +11 -2
  162. package/src/notifications/__tests__/broadcaster.test.ts +203 -0
  163. package/src/notifications/__tests__/decision-engine.test.ts +283 -0
  164. package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
  165. package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
  166. package/src/notifications/__tests__/home-feed-side-effect.test.ts +430 -7
  167. package/src/notifications/adapters/macos.ts +12 -2
  168. package/src/notifications/broadcaster.ts +29 -4
  169. package/src/notifications/copy-composer.ts +17 -64
  170. package/src/notifications/decision-engine.ts +111 -44
  171. package/src/notifications/deterministic-checks.ts +96 -0
  172. package/src/notifications/emit-signal.ts +1 -0
  173. package/src/notifications/home-feed-side-effect.ts +85 -6
  174. package/src/notifications/signal.ts +0 -4
  175. package/src/notifications/types.ts +8 -0
  176. package/src/oauth/platform-connection.test.ts +43 -3
  177. package/src/oauth/platform-connection.ts +13 -4
  178. package/src/plugins/defaults/injectors.ts +38 -19
  179. package/src/plugins/external-plugin-loader.ts +82 -10
  180. package/src/plugins/types.ts +16 -7
  181. package/src/prompts/__tests__/system-prompt.test.ts +6 -51
  182. package/src/prompts/__tests__/task-progress-hint-section.test.ts +4 -8
  183. package/src/prompts/system-prompt.ts +0 -8
  184. package/src/prompts/templates/BOOTSTRAP.md +5 -5
  185. package/src/prompts/templates/system-sections.ts +0 -9
  186. package/src/providers/__tests__/inference.test.ts +2 -0
  187. package/src/providers/call-site-routing.ts +24 -6
  188. package/src/providers/connection-resolution.ts +63 -13
  189. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
  190. package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
  191. package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
  192. package/src/providers/inference/adapter-factory.ts +9 -20
  193. package/src/providers/inference/auth.ts +12 -0
  194. package/src/providers/inference/backfill.ts +14 -1
  195. package/src/providers/inference/connections.ts +85 -5
  196. package/src/providers/inference/resolve-auth.ts +2 -0
  197. package/src/providers/model-catalog.ts +199 -244
  198. package/src/providers/model-intents.ts +3 -3
  199. package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
  200. package/src/providers/openai/chat-completions-provider.ts +159 -6
  201. package/src/providers/openrouter/client.ts +42 -4
  202. package/src/providers/platform-proxy/constants.ts +3 -4
  203. package/src/providers/provider-catalog-visibility.ts +3 -1
  204. package/src/providers/provider-send-message.ts +27 -12
  205. package/src/providers/registry.ts +30 -1
  206. package/src/runtime/agent-wake.ts +61 -1
  207. package/src/runtime/auth/route-policy.ts +13 -0
  208. package/src/runtime/http-server.ts +7 -16
  209. package/src/runtime/http-types.ts +0 -47
  210. package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
  211. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +66 -4
  212. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
  213. package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
  214. package/src/runtime/routes/channel-availability-routes.ts +5 -0
  215. package/src/runtime/routes/consolidation-routes.ts +100 -0
  216. package/src/runtime/routes/conversation-query-routes.ts +70 -11
  217. package/src/runtime/routes/conversation-routes.ts +7 -0
  218. package/src/runtime/routes/index.ts +2 -0
  219. package/src/runtime/routes/inference-provider-connection-routes.ts +134 -1
  220. package/src/runtime/routes/integrations/a2a.ts +235 -0
  221. package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
  222. package/src/runtime/routes/subagents-routes.ts +41 -0
  223. package/src/subagent/manager.ts +2 -0
  224. package/src/tools/memory/register.ts +1 -9
  225. package/src/tools/registry.ts +2 -2
  226. package/src/tools/types.ts +37 -2
  227. package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
  228. package/src/workspace/migrations/registry.ts +2 -0
  229. package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
  230. package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
  231. package/src/runtime/guardian-action-conversation-turn.ts +0 -99
@@ -21,6 +21,13 @@ interface FakeChunk {
21
21
  choices: Array<{
22
22
  delta: {
23
23
  content?: string | null;
24
+ reasoning_content?: string | null;
25
+ reasoning?: string | null;
26
+ reasoning_details?: Array<{
27
+ type?: string;
28
+ summary?: string | null;
29
+ text?: string | null;
30
+ }> | null;
24
31
  tool_calls?: Array<{
25
32
  index: number;
26
33
  id?: string;
@@ -209,6 +216,44 @@ function cachedUsageChunk(
209
216
  };
210
217
  }
211
218
 
219
+ function reasoningChunk(
220
+ reasoning: string,
221
+ finish: string | null = null,
222
+ ): FakeChunk {
223
+ return {
224
+ choices: [
225
+ { delta: { reasoning_content: reasoning }, finish_reason: finish },
226
+ ],
227
+ usage: null,
228
+ model: "gpt-5.2",
229
+ };
230
+ }
231
+
232
+ // OpenRouter spec uses `delta.reasoning` rather than `delta.reasoning_content`.
233
+ function openRouterReasoningChunk(
234
+ reasoning: string,
235
+ finish: string | null = null,
236
+ ): FakeChunk {
237
+ return {
238
+ choices: [{ delta: { reasoning }, finish_reason: finish }],
239
+ usage: null,
240
+ model: "gpt-5.2",
241
+ };
242
+ }
243
+
244
+ // OpenRouter's documented reasoning-summary shape: a `reasoning_details` array
245
+ // with entries tagged `reasoning.summary` / `reasoning.text` / `reasoning.encrypted`.
246
+ function reasoningDetailsChunk(
247
+ details: Array<{ type?: string; summary?: string; text?: string }>,
248
+ finish: string | null = null,
249
+ ): FakeChunk {
250
+ return {
251
+ choices: [{ delta: { reasoning_details: details }, finish_reason: finish }],
252
+ usage: null,
253
+ model: "gpt-5.2",
254
+ };
255
+ }
256
+
212
257
  // ---------------------------------------------------------------------------
213
258
  // Class extraction sanity checks
214
259
  // ---------------------------------------------------------------------------
@@ -339,6 +384,153 @@ describe("OpenAIProvider", () => {
339
384
  expect(events[1]).toEqual({ type: "text_delta", text: ", world!" });
340
385
  });
341
386
 
387
+ // -----------------------------------------------------------------------
388
+ // Reasoning content (MiniMax / DeepSeek extension)
389
+ // -----------------------------------------------------------------------
390
+ test("parses reasoning_content into thinking block", async () => {
391
+ fakeChunks = [
392
+ reasoningChunk("Let me think..."),
393
+ textChunk("The answer is 42."),
394
+ usageChunk(10, 20),
395
+ ];
396
+
397
+ const result = await provider.sendMessage([userMsg("Hi")]);
398
+
399
+ expect(result.content).toHaveLength(2);
400
+ expect(result.content[0]).toEqual({
401
+ type: "thinking",
402
+ thinking: "Let me think...",
403
+ signature: "",
404
+ });
405
+ expect(result.content[1]).toEqual({
406
+ type: "text",
407
+ text: "The answer is 42.",
408
+ });
409
+ expect(result.usage).toEqual({ inputTokens: 10, outputTokens: 20 });
410
+ });
411
+
412
+ test("fires thinking_delta events during streaming", async () => {
413
+ fakeChunks = [
414
+ reasoningChunk("Let me think..."),
415
+ textChunk("The answer is 42."),
416
+ usageChunk(10, 20),
417
+ ];
418
+
419
+ const events: ProviderEvent[] = [];
420
+ await provider.sendMessage([userMsg("Hi")], undefined, undefined, {
421
+ onEvent: (e) => events.push(e),
422
+ });
423
+
424
+ expect(events).toHaveLength(2);
425
+ expect(events[0]).toEqual({
426
+ type: "thinking_delta",
427
+ thinking: "Let me think...",
428
+ });
429
+ expect(events[1]).toEqual({
430
+ type: "text_delta",
431
+ text: "The answer is 42.",
432
+ });
433
+ });
434
+
435
+ test("reasoning + tool calls orders correctly (thinking → tool_use)", async () => {
436
+ fakeChunks = [
437
+ reasoningChunk("Planning..."),
438
+ ...toolCallChunks([
439
+ { id: "call_1", name: "file_read", args: '{"path":"/a"}' },
440
+ ]),
441
+ usageChunk(10, 30),
442
+ ];
443
+
444
+ const result = await provider.sendMessage([userMsg("Read /a")]);
445
+
446
+ expect(result.content).toHaveLength(2);
447
+ expect(result.content[0].type).toBe("thinking");
448
+ expect(result.content[1].type).toBe("tool_use");
449
+ });
450
+
451
+ test("no thinking block when reasoning_content is absent", async () => {
452
+ fakeChunks = [textChunk("Just text"), usageChunk(10, 5)];
453
+
454
+ const result = await provider.sendMessage([userMsg("Hi")]);
455
+
456
+ expect(result.content).toHaveLength(1);
457
+ expect(result.content[0].type).toBe("text");
458
+ });
459
+
460
+ test("parses OpenRouter's `delta.reasoning` field into thinking block", async () => {
461
+ fakeChunks = [
462
+ openRouterReasoningChunk("Hmm, let me think..."),
463
+ textChunk("Final answer."),
464
+ usageChunk(10, 8),
465
+ ];
466
+
467
+ const events: ProviderEvent[] = [];
468
+ const result = await provider.sendMessage(
469
+ [userMsg("Hi")],
470
+ undefined,
471
+ undefined,
472
+ {
473
+ onEvent: (e) => events.push(e),
474
+ },
475
+ );
476
+
477
+ expect(result.content).toHaveLength(2);
478
+ expect(result.content[0]).toEqual({
479
+ type: "thinking",
480
+ thinking: "Hmm, let me think...",
481
+ signature: "",
482
+ });
483
+ expect(result.content[1]).toEqual({ type: "text", text: "Final answer." });
484
+ expect(events).toContainEqual({
485
+ type: "thinking_delta",
486
+ thinking: "Hmm, let me think...",
487
+ });
488
+ });
489
+
490
+ test("parses OpenRouter `delta.reasoning_details` summary/text entries and skips encrypted", async () => {
491
+ fakeChunks = [
492
+ reasoningDetailsChunk([
493
+ { type: "reasoning.summary", summary: "Plan step one. " },
494
+ { type: "reasoning.encrypted", text: "ENCRYPTED_BLOB" },
495
+ { type: "reasoning.text", text: "Detailed thought." },
496
+ ]),
497
+ textChunk("Done."),
498
+ usageChunk(10, 8),
499
+ ];
500
+
501
+ const events: ProviderEvent[] = [];
502
+ const result = await provider.sendMessage(
503
+ [userMsg("Hi")],
504
+ undefined,
505
+ undefined,
506
+ {
507
+ onEvent: (e) => events.push(e),
508
+ },
509
+ );
510
+
511
+ expect(result.content).toHaveLength(2);
512
+ expect(result.content[0]).toEqual({
513
+ type: "thinking",
514
+ thinking: "Plan step one. Detailed thought.",
515
+ signature: "",
516
+ });
517
+ expect(result.content[1]).toEqual({ type: "text", text: "Done." });
518
+ expect(events).toContainEqual({
519
+ type: "thinking_delta",
520
+ thinking: "Plan step one. ",
521
+ });
522
+ expect(events).toContainEqual({
523
+ type: "thinking_delta",
524
+ thinking: "Detailed thought.",
525
+ });
526
+ // Encrypted blob must never surface as visible thinking.
527
+ for (const e of events) {
528
+ if (e.type === "thinking_delta") {
529
+ expect(e.thinking).not.toContain("ENCRYPTED_BLOB");
530
+ }
531
+ }
532
+ });
533
+
342
534
  // -----------------------------------------------------------------------
343
535
  // System prompt
344
536
  // -----------------------------------------------------------------------
@@ -1306,14 +1498,17 @@ describe("OpenRouterProvider reasoning", () => {
1306
1498
  shouldThrow = null;
1307
1499
  });
1308
1500
 
1309
- test("sends reasoning.enabled=true when thinking config is present", async () => {
1501
+ test("sends reasoning.enabled=true with default detailed summary when thinking config is present", async () => {
1310
1502
  const provider = new OpenRouterProvider("or-key", "x-ai/grok-4");
1311
1503
  await provider.sendMessage([userMsg("hi")], undefined, undefined, {
1312
1504
  config: { thinking: { type: "adaptive" } },
1313
1505
  });
1314
1506
 
1315
1507
  expect(lastCreateParams).toBeTruthy();
1316
- expect(lastCreateParams!.reasoning).toEqual({ enabled: true });
1508
+ expect(lastCreateParams!.reasoning).toEqual({
1509
+ enabled: true,
1510
+ summary: "detailed",
1511
+ });
1317
1512
  });
1318
1513
 
1319
1514
  test("sends reasoning.enabled=false when thinking is explicitly disabled", async () => {
@@ -1376,7 +1571,10 @@ describe("OpenRouterProvider reasoning", () => {
1376
1571
  await retry.sendMessage([userMsg("hi")], undefined, undefined, {
1377
1572
  config: { thinking: { type: "adaptive" } },
1378
1573
  });
1379
- expect(lastCreateParams!.reasoning).toEqual({ enabled: true });
1574
+ expect(lastCreateParams!.reasoning).toEqual({
1575
+ enabled: true,
1576
+ summary: "detailed",
1577
+ });
1380
1578
  });
1381
1579
 
1382
1580
  test("RetryProvider + OpenRouterProvider disables thinking end-to-end", async () => {
@@ -1389,6 +1587,23 @@ describe("OpenRouterProvider reasoning", () => {
1389
1587
  });
1390
1588
  expect(lastCreateParams!.reasoning).toEqual({ enabled: false });
1391
1589
  });
1590
+
1591
+ test("nests effort under reasoning and omits top-level reasoning_effort", async () => {
1592
+ const provider = new OpenRouterProvider("or-key", "moonshotai/kimi-k2.6");
1593
+ await provider.sendMessage([userMsg("hi")], undefined, undefined, {
1594
+ config: { thinking: { enabled: true }, effort: "max" },
1595
+ });
1596
+
1597
+ expect(lastCreateParams).toBeTruthy();
1598
+ expect(lastCreateParams!.reasoning).toEqual({
1599
+ enabled: true,
1600
+ effort: "xhigh",
1601
+ summary: "detailed",
1602
+ });
1603
+ // Critical: must NOT also send the OpenAI-native flat field — OpenRouter
1604
+ // rejects requests that carry both forms for reasoning models.
1605
+ expect(lastCreateParams).not.toHaveProperty("reasoning_effort");
1606
+ });
1392
1607
  });
1393
1608
 
1394
1609
  describe("OpenRouterProvider Anthropic-compatible errors", () => {
@@ -92,9 +92,9 @@ describe("OpenAI Responses API cutover guard", () => {
92
92
 
93
93
  // The factory must NOT instantiate OpenAIChatCompletionsProvider or
94
94
  // OpenAIProvider (the backward-compatible alias) inside the `openai:`
95
- // factory entry. Other entries (e.g. `zai:`, `deepseek:`, `minimax:`)
96
- // may legitimately use OpenAIChatCompletionsProvider since that's the
97
- // OpenAI Chat Completions transport for third-party endpoints.
95
+ // factory entry. Other entries (e.g. `openai-compatible:`) may legitimately
96
+ // use OpenAIChatCompletionsProvider since that's the OpenAI Chat Completions
97
+ // transport for third-party endpoints.
98
98
  const openaiEntryRegion =
99
99
  /(?:^|\s)openai\s*:\s*\([^)]*\)\s*=>\s*[\s\S]{0,400}?(?=\}\s*,\s*[a-z-]+\s*:|\}\s*;)/m.exec(
100
100
  source,
@@ -116,7 +116,7 @@ describe("OpenRouter provider.only plumbing", () => {
116
116
  expect(extras.provider).toBe(undefined);
117
117
  });
118
118
 
119
- test("still carries reasoning flag alongside provider.only", () => {
119
+ test("enables thinking with default detailed summary alongside provider.only", () => {
120
120
  const provider = new ProbeOpenRouterProvider(
121
121
  "fake-key",
122
122
  "x-ai/grok-4.20-beta",
@@ -128,12 +128,12 @@ describe("OpenRouter provider.only plumbing", () => {
128
128
  },
129
129
  });
130
130
  expect(extras).toEqual({
131
- reasoning: { enabled: true },
131
+ reasoning: { enabled: true, summary: "detailed" },
132
132
  provider: { only: ["xAI"] },
133
133
  });
134
134
  });
135
135
 
136
- test("disabled thinking keeps reasoning disabled alongside provider.only", () => {
136
+ test("disabled thinking keeps reasoning disabled and omits summary", () => {
137
137
  const provider = new ProbeOpenRouterProvider(
138
138
  "fake-key",
139
139
  "x-ai/grok-4.20-beta",
@@ -149,5 +149,53 @@ describe("OpenRouter provider.only plumbing", () => {
149
149
  provider: { only: ["xAI"] },
150
150
  });
151
151
  });
152
+
153
+ test("nests effort under reasoning and maps `max` to xhigh", () => {
154
+ const provider = new ProbeOpenRouterProvider(
155
+ "fake-key",
156
+ "moonshotai/kimi-k2.6",
157
+ );
158
+ const extras = provider.probeExtras({
159
+ config: {
160
+ thinking: { enabled: true },
161
+ effort: "max",
162
+ },
163
+ });
164
+ expect(extras).toEqual({
165
+ reasoning: { enabled: true, effort: "xhigh", summary: "detailed" },
166
+ });
167
+ });
168
+
169
+ test("honors a per-call summary override", () => {
170
+ const provider = new ProbeOpenRouterProvider(
171
+ "fake-key",
172
+ "moonshotai/kimi-k2.6",
173
+ );
174
+ const extras = provider.probeExtras({
175
+ config: {
176
+ thinking: { enabled: true },
177
+ openrouter: { reasoning: { summary: "concise" } },
178
+ },
179
+ });
180
+ expect(extras).toEqual({
181
+ reasoning: { enabled: true, summary: "concise" },
182
+ });
183
+ });
184
+
185
+ test("ignores an invalid summary override and falls back to detailed", () => {
186
+ const provider = new ProbeOpenRouterProvider(
187
+ "fake-key",
188
+ "moonshotai/kimi-k2.6",
189
+ );
190
+ const extras = provider.probeExtras({
191
+ config: {
192
+ thinking: { enabled: true },
193
+ openrouter: { reasoning: { summary: "verbose" } },
194
+ },
195
+ });
196
+ expect(extras).toEqual({
197
+ reasoning: { enabled: true, summary: "detailed" },
198
+ });
199
+ });
152
200
  });
153
201
  });
@@ -5,7 +5,11 @@ import { OpenRouterProvider } from "../providers/openrouter/client.js";
5
5
  import type { Message } from "../providers/types.js";
6
6
 
7
7
  /** Build a minimal valid PNG header encoding the given dimensions. */
8
- function makePngBase64(width: number, height: number, paddingBytes = 0): string {
8
+ function makePngBase64(
9
+ width: number,
10
+ height: number,
11
+ paddingBytes = 0,
12
+ ): string {
9
13
  const header = Buffer.alloc(24);
10
14
  header[0] = 0x89;
11
15
  header[1] = 0x50;
@@ -41,13 +45,13 @@ describe("OpenRouterProvider token estimation routing", () => {
41
45
  expect(provider.tokenEstimationProvider).toBe("openrouter");
42
46
  });
43
47
 
44
- test("estimatePromptTokens applies Anthropic image scaling when routed via OpenRouter", () => {
48
+ test("estimatePromptTokens applies dimension-based image scaling when routed via OpenRouter to Anthropic", () => {
45
49
  const provider = new OpenRouterProvider(
46
50
  "fake-key",
47
51
  "anthropic/claude-opus-4-6",
48
52
  );
49
53
  // 1920x1080 screenshot with ~200 KB of pixel data → base64/4 would be ~65k
50
- // tokens; dimension-based Anthropic rules land around 1.6k tokens.
54
+ // tokens; dimension-based rules land around 1.6k tokens.
51
55
  const messages: Message[] = [
52
56
  {
53
57
  role: "user",
@@ -68,33 +72,38 @@ describe("OpenRouterProvider token estimation routing", () => {
68
72
  providerName: provider.tokenEstimationProvider,
69
73
  });
70
74
 
71
- // Dimension-based estimate should be well under 5k; base64/4 would exceed 50k.
72
75
  expect(estimated).toBeLessThan(5_000);
73
76
  });
74
77
 
75
- test("estimatePromptTokens falls back to base64/4 for non-Anthropic OpenRouter models", () => {
76
- const provider = new OpenRouterProvider("fake-key", "x-ai/grok-4.20-beta");
77
- const messages: Message[] = [
78
- {
79
- role: "user",
80
- content: [
81
- {
82
- type: "image",
83
- source: {
84
- type: "base64",
85
- media_type: "image/png",
86
- data: makePngBase64(1920, 1080, 200_000),
78
+ test("estimatePromptTokens applies dimension-based image scaling for non-Anthropic OpenRouter models", () => {
79
+ // A naive base64/4 estimate on a 1920x1080 screenshot (~200 KB) lands near
80
+ // 65k tokens and trips spurious compaction long before the real context
81
+ // window fills. Vision models on OpenRouter — both anthropic/* and
82
+ // non-Anthropic (Kimi K2.6, Grok, etc.) — must use the dimension-based
83
+ // formula.
84
+ for (const model of ["moonshotai/kimi-k2.6", "x-ai/grok-4.20-beta"]) {
85
+ const provider = new OpenRouterProvider("fake-key", model);
86
+ const messages: Message[] = [
87
+ {
88
+ role: "user",
89
+ content: [
90
+ {
91
+ type: "image",
92
+ source: {
93
+ type: "base64",
94
+ media_type: "image/png",
95
+ data: makePngBase64(1920, 1080, 200_000),
96
+ },
87
97
  },
88
- },
89
- ],
90
- },
91
- ];
98
+ ],
99
+ },
100
+ ];
92
101
 
93
- const estimated = estimatePromptTokens(messages, "system", {
94
- providerName: provider.tokenEstimationProvider,
95
- });
102
+ const estimated = estimatePromptTokens(messages, "system", {
103
+ providerName: provider.tokenEstimationProvider,
104
+ });
96
105
 
97
- // Base64/4 heuristic on ~200 KB of image data → far more than 10k tokens.
98
- expect(estimated).toBeGreaterThan(50_000);
106
+ expect(estimated).toBeLessThan(5_000);
107
+ }
99
108
  });
100
109
  });
@@ -117,8 +117,13 @@ describe("buildManagedBaseUrl", () => {
117
117
  );
118
118
  });
119
119
 
120
+ test("returns managed URL for fireworks", async () => {
121
+ expect(await buildManagedBaseUrl("fireworks")).toBe(
122
+ "https://platform.example.com/v1/runtime-proxy/fireworks",
123
+ );
124
+ });
125
+
120
126
  test("returns undefined for non-managed providers", async () => {
121
- expect(await buildManagedBaseUrl("fireworks")).toBeUndefined();
122
127
  expect(await buildManagedBaseUrl("openrouter")).toBeUndefined();
123
128
  expect(await buildManagedBaseUrl("ollama")).toBeUndefined();
124
129
  });
@@ -59,7 +59,7 @@ import {
59
59
  unregisterPluginTools,
60
60
  } from "../tools/registry.js";
61
61
  import type {
62
- PluginTool,
62
+ LoadedPluginTool,
63
63
  ToolContext,
64
64
  ToolExecutionResult,
65
65
  } from "../tools/types.js";
@@ -81,8 +81,8 @@ const fakeCtx: DaemonContext = {
81
81
 
82
82
  function makeFakeTool(
83
83
  name: string,
84
- extras: Partial<PluginTool> = {},
85
- ): PluginTool {
84
+ extras: Partial<LoadedPluginTool> = {},
85
+ ): LoadedPluginTool {
86
86
  return {
87
87
  name,
88
88
  description: `Fake ${name}`,
@@ -46,7 +46,7 @@ import {
46
46
  type ToolResultTruncateResult,
47
47
  type TurnContext,
48
48
  } from "../plugins/types.js";
49
- import type { PluginTool } from "../tools/types.js";
49
+ import type { LoadedPluginTool } from "../tools/types.js";
50
50
 
51
51
  const sampleTrust: TrustContext = {
52
52
  sourceChannel: "vellum",
@@ -207,7 +207,7 @@ describe("plugin core types", () => {
207
207
  },
208
208
  };
209
209
 
210
- const sampleTool: PluginTool = {
210
+ const sampleTool: LoadedPluginTool = {
211
211
  name: "sample-tool",
212
212
  description: "Sample plugin tool",
213
213
  defaultRiskLevel: RiskLevel.Low,
@@ -19,6 +19,22 @@ function makeConfig(): AssistantConfig {
19
19
  }
20
20
 
21
21
  describe("getVisibleProviderCatalog", () => {
22
+ test("hides openai-compatible endpoints by default", () => {
23
+ _setOverridesForTesting({});
24
+
25
+ const visible = getVisibleProviderCatalog(makeConfig());
26
+
27
+ expect(visible.find((p) => p.id === "openai-compatible")).toBeUndefined();
28
+ });
29
+
30
+ test("shows openai-compatible endpoints when its flag is enabled", () => {
31
+ _setOverridesForTesting({ "openai-compatible-endpoints": true });
32
+
33
+ const visible = getVisibleProviderCatalog(makeConfig());
34
+
35
+ expect(visible.find((p) => p.id === "openai-compatible")).toBeDefined();
36
+ });
37
+
22
38
  test("returns the full catalog when all feature flags are enabled", () => {
23
39
  const allFlags: Record<string, boolean> = {};
24
40
  for (const entry of PROVIDER_CATALOG) {
@@ -113,7 +113,7 @@ const DIRECT_OR_MANAGED_PROVIDER_KEYS: string[] = [
113
113
  "fireworks",
114
114
  "openrouter",
115
115
  ];
116
- const MANAGED_FALLBACK_PROVIDERS: string[] = ["anthropic", "gemini", "openai"];
116
+ const MANAGED_FALLBACK_PROVIDERS: string[] = ["anthropic", "gemini", "openai", "fireworks"];
117
117
 
118
118
  function enableManagedProxy() {
119
119
  mockPlatformBaseUrl = PLATFORM_BASE;
@@ -209,20 +209,19 @@ describe("managed proxy integration — credential precedence", () => {
209
209
  },
210
210
  );
211
211
 
212
- test("managed bootstrap registers anthropic, openai, and gemini", async () => {
212
+ test("managed bootstrap registers anthropic, openai, gemini, and fireworks", async () => {
213
213
  enableManagedProxy();
214
214
  mockProviderKeys = {};
215
215
  await initializeProviders(makeProvidersConfig("anthropic", "test-model"));
216
216
  expect(listProviders()).toEqual(
217
- expect.arrayContaining(["anthropic", "openai", "gemini"]),
217
+ expect.arrayContaining(["anthropic", "openai", "gemini", "fireworks"]),
218
218
  );
219
- expect(listProviders()).toHaveLength(3);
219
+ expect(listProviders()).toHaveLength(4);
220
220
  expect(getProviderRoutingSource("anthropic")).toBe("managed-proxy");
221
221
  expect(getProviderRoutingSource("openai")).toBe("managed-proxy");
222
222
  expect(getProviderRoutingSource("gemini")).toBe("managed-proxy");
223
- for (const p of ["fireworks", "openrouter"]) {
224
- expect(getProviderRoutingSource(p)).toBeUndefined();
225
- }
223
+ expect(getProviderRoutingSource("fireworks")).toBe("managed-proxy");
224
+ expect(getProviderRoutingSource("openrouter")).toBeUndefined();
226
225
  });
227
226
 
228
227
  test("managed anthropic uses anthropic proxy path", async () => {
@@ -376,7 +375,7 @@ describe("managed proxy integration — credential precedence", () => {
376
375
  });
377
376
 
378
377
  describe("mixed: some user keys + managed fallback fills gaps", () => {
379
- test("user key for anthropic routes direct and managed fallback fills openai and gemini", async () => {
378
+ test("user key for anthropic routes direct and managed fallback fills openai, gemini, and fireworks", async () => {
380
379
  enableManagedProxy();
381
380
  setUserKeysFor("anthropic");
382
381
  await initializeProviders(makeProvidersConfig("anthropic", "test-model"));
@@ -387,13 +386,13 @@ describe("managed proxy integration — credential precedence", () => {
387
386
  expect(getProviderRoutingSource("openai")).toBe("managed-proxy");
388
387
  expect(registered).toContain("gemini");
389
388
  expect(getProviderRoutingSource("gemini")).toBe("managed-proxy");
390
- for (const p of ["fireworks", "openrouter"]) {
391
- expect(registered).not.toContain(p);
392
- expect(getProviderRoutingSource(p)).toBeUndefined();
393
- }
389
+ expect(registered).toContain("fireworks");
390
+ expect(getProviderRoutingSource("fireworks")).toBe("managed-proxy");
391
+ expect(registered).not.toContain("openrouter");
392
+ expect(getProviderRoutingSource("openrouter")).toBeUndefined();
394
393
  });
395
394
 
396
- test("user key for openai routes direct while anthropic and gemini still bootstrap via managed proxy", async () => {
395
+ test("user key for openai routes direct while anthropic, gemini, and fireworks still bootstrap via managed proxy", async () => {
397
396
  enableManagedProxy();
398
397
  setUserKeysFor("openai");
399
398
  await initializeProviders(makeProvidersConfig("openai", "test-model"));
@@ -404,11 +403,10 @@ describe("managed proxy integration — credential precedence", () => {
404
403
  expect(getProviderRoutingSource("anthropic")).toBe("managed-proxy");
405
404
  expect(registered).toContain("gemini");
406
405
  expect(getProviderRoutingSource("gemini")).toBe("managed-proxy");
407
- // OpenAI has a user key so it's user-key, not managed-proxy
408
- for (const p of ["fireworks", "openrouter"]) {
409
- expect(registered).not.toContain(p);
410
- expect(getProviderRoutingSource(p)).toBeUndefined();
411
- }
406
+ expect(registered).toContain("fireworks");
407
+ expect(getProviderRoutingSource("fireworks")).toBe("managed-proxy");
408
+ expect(registered).not.toContain("openrouter");
409
+ expect(getProviderRoutingSource("openrouter")).toBeUndefined();
412
410
  });
413
411
  });
414
412
  });
@@ -476,8 +474,8 @@ describe("config mode flip → provider reinit", () => {
476
474
  });
477
475
 
478
476
  describe("managed proxy integration — constants integrity", () => {
479
- test("anthropic, openai, and gemini have metadata with managed=true and a proxyPath", () => {
480
- for (const provider of ["anthropic", "openai", "gemini"]) {
477
+ test("anthropic, openai, gemini, and fireworks have metadata with managed=true and a proxyPath", () => {
478
+ for (const provider of ["anthropic", "openai", "gemini", "fireworks"]) {
481
479
  const meta = PLATFORM_PROVIDER_META[provider];
482
480
  expect(meta).toBeDefined();
483
481
  expect(meta.managed).toBe(true);
@@ -504,10 +502,14 @@ describe("managed proxy integration — constants integrity", () => {
504
502
  );
505
503
  });
506
504
 
507
- test("fireworks and openrouter are not managed proxy capable", () => {
508
- for (const provider of ["fireworks", "openrouter"]) {
509
- expect(PLATFORM_PROVIDER_META[provider].managed).toBe(false);
510
- expect(PLATFORM_PROVIDER_META[provider].proxyPath).toBeUndefined();
511
- }
505
+ test("fireworks routes through fireworks proxy path", () => {
506
+ expect(PLATFORM_PROVIDER_META.fireworks.proxyPath).toBe(
507
+ "/v1/runtime-proxy/fireworks",
508
+ );
509
+ });
510
+
511
+ test("openrouter is not managed proxy capable", () => {
512
+ expect(PLATFORM_PROVIDER_META.openrouter.managed).toBe(false);
513
+ expect(PLATFORM_PROVIDER_META.openrouter.proxyPath).toBeUndefined();
512
514
  });
513
515
  });
@@ -12,7 +12,7 @@ let providerRefreshCalls = 0;
12
12
  const PLATFORM_BASE_URL = "https://platform.example.com";
13
13
  const ASSISTANT_API_KEY_PATH = credentialKey("vellum", "assistant_api_key");
14
14
  const PLATFORM_BASE_URL_PATH = credentialKey("vellum", "platform_base_url");
15
- const MANAGED_PROVIDERS = ["anthropic", "openai", "gemini"] as const;
15
+ const MANAGED_PROVIDERS = ["anthropic", "openai", "gemini", "fireworks"] as const;
16
16
 
17
17
  let platformBaseUrlOverride: string | undefined;
18
18