@vellumai/assistant 0.8.9-staging.2 → 0.8.9-staging.4

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 (196) hide show
  1. package/docs/activation-funnel-telemetry.md +310 -0
  2. package/openapi.yaml +16 -115
  3. package/package.json +1 -1
  4. package/src/__tests__/activation-early-marking.test.ts +120 -0
  5. package/src/__tests__/agent-loop-output-hooks.test.ts +13 -13
  6. package/src/__tests__/anthropic-provider.test.ts +23 -8
  7. package/src/__tests__/approval-cascade.test.ts +1 -1
  8. package/src/__tests__/compaction-direct.test.ts +32 -18
  9. package/src/__tests__/compaction-events.test.ts +2 -2
  10. package/src/__tests__/compaction.benchmark.test.ts +1 -1
  11. package/src/__tests__/context-overflow-reducer.test.ts +5 -5
  12. package/src/__tests__/context-window-manager-compact-retry.test.ts +121 -15
  13. package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
  14. package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
  15. package/src/__tests__/conversation-error.test.ts +15 -1
  16. package/src/__tests__/conversation-history-web-search.test.ts +5 -0
  17. package/src/__tests__/conversation-media-retry.test.ts +1 -1
  18. package/src/__tests__/conversation-process-app-control-preactivation.test.ts +40 -0
  19. package/src/__tests__/conversation-process-callsite.test.ts +1 -1
  20. package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
  21. package/src/__tests__/conversation-queue.test.ts +1 -1
  22. package/src/__tests__/conversation-runtime-assembly.test.ts +71 -0
  23. package/src/__tests__/conversation-slash-queue.test.ts +1 -1
  24. package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
  25. package/src/__tests__/conversation-speed-override.test.ts +1 -1
  26. package/src/__tests__/conversation-surfaces-activation-emit.test.ts +395 -0
  27. package/src/__tests__/conversation-surfaces-app-control.test.ts +44 -0
  28. package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +73 -4
  29. package/src/__tests__/conversation-undo.test.ts +2 -2
  30. package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
  31. package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
  32. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
  33. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  34. package/src/__tests__/cu-unified-flow.test.ts +36 -0
  35. package/src/__tests__/history-repair-hook.test.ts +2 -0
  36. package/src/__tests__/llm-resolver.test.ts +73 -0
  37. package/src/__tests__/memory-retrieval-hook.test.ts +1 -2
  38. package/src/__tests__/persist-unsendable-image-downscale.test.ts +145 -0
  39. package/src/__tests__/persist-unsendable-image.test.ts +97 -1
  40. package/src/__tests__/plugin-external-api.test.ts +68 -0
  41. package/src/__tests__/post-turn-tool-result-truncation.test.ts +69 -0
  42. package/src/__tests__/published-app-updater.test.ts +138 -0
  43. package/src/__tests__/skill-feature-flags-integration.test.ts +5 -7
  44. package/src/__tests__/title-generate-hook.test.ts +2 -0
  45. package/src/__tests__/web-fetch.test.ts +45 -0
  46. package/src/acp/__tests__/helpers/acp-config-stub.ts +0 -2
  47. package/src/acp/resolve-agent.test.ts +0 -56
  48. package/src/acp/resolve-agent.ts +10 -38
  49. package/src/agent/loop.ts +13 -27
  50. package/src/api/responses/memory-v3-selection-log.ts +19 -10
  51. package/src/cli/commands/__tests__/memory-v3.test.ts +191 -210
  52. package/src/cli/commands/memory-v3.ts +57 -199
  53. package/src/cli/lib/__tests__/install-from-github.test.ts +232 -29
  54. package/src/cli/lib/__tests__/plugin-details.test.ts +28 -19
  55. package/src/cli/lib/__tests__/plugin-marketplace.test.ts +57 -7
  56. package/src/cli/lib/__tests__/search-plugins.test.ts +17 -10
  57. package/src/cli/lib/install-from-github.ts +258 -41
  58. package/src/cli/lib/plugin-details.ts +20 -13
  59. package/src/cli/lib/plugin-marketplace.ts +23 -5
  60. package/src/cli/lib/search-plugins.ts +14 -8
  61. package/src/config/acp-defaults.ts +3 -3
  62. package/src/config/acp-schema.ts +1 -7
  63. package/src/config/bundled-skills/acp/SKILL.md +4 -17
  64. package/src/config/bundled-skills/acp/TOOLS.json +2 -2
  65. package/src/config/call-site-defaults.ts +0 -1
  66. package/src/config/feature-flag-registry.json +3 -18
  67. package/src/config/llm-resolver.ts +39 -7
  68. package/src/config/schemas/__tests__/memory-v3.test.ts +25 -9
  69. package/src/config/schemas/call-site-catalog.ts +0 -7
  70. package/src/config/schemas/llm.ts +17 -1
  71. package/src/config/schemas/memory-v3.ts +58 -8
  72. package/src/config/seed-inference-profiles.ts +18 -0
  73. package/src/context/post-turn-tool-result-truncation.ts +39 -1
  74. package/src/daemon/conversation-agent-loop-handlers.ts +8 -1
  75. package/src/daemon/conversation-agent-loop.ts +87 -95
  76. package/src/daemon/conversation-error.ts +31 -4
  77. package/src/daemon/conversation-history.ts +1 -1
  78. package/src/daemon/conversation-media-retry.ts +19 -6
  79. package/src/daemon/conversation-messaging.ts +17 -0
  80. package/src/daemon/conversation-process.ts +14 -5
  81. package/src/daemon/conversation-queue-manager.ts +8 -0
  82. package/src/daemon/conversation-runtime-assembly.ts +37 -1
  83. package/src/daemon/conversation-surfaces.ts +141 -3
  84. package/src/daemon/conversation.ts +48 -13
  85. package/src/daemon/external-plugins-bootstrap.ts +8 -3
  86. package/src/daemon/persist-unsendable-image.ts +62 -25
  87. package/src/daemon/process-message.ts +1 -1
  88. package/src/daemon/tool-side-effects.ts +15 -0
  89. package/src/memory/__tests__/activation-session-store.test.ts +41 -0
  90. package/src/memory/__tests__/onboarding-events-store.test.ts +80 -0
  91. package/src/memory/activation-session-store.ts +43 -0
  92. package/src/memory/db-init.ts +4 -0
  93. package/src/memory/migrations/273-onboarding-events-funnel-columns.ts +46 -0
  94. package/src/memory/migrations/274-create-activation-sessions.ts +15 -0
  95. package/src/memory/migrations/index.ts +2 -0
  96. package/src/memory/onboarding-events-store.ts +66 -18
  97. package/src/memory/schema/infrastructure.ts +13 -0
  98. package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -97
  99. package/src/memory/v2/__tests__/page-store.test.ts +22 -0
  100. package/src/memory/v2/consolidation-job.ts +2 -72
  101. package/src/memory/v2/types.ts +5 -0
  102. package/src/messaging/providers/telegram-bot/api.ts +14 -5
  103. package/src/notifications/adapters/telegram.ts +7 -1
  104. package/src/plugin-api/constants.ts +2 -2
  105. package/src/plugin-api/index.ts +2 -2
  106. package/src/plugin-api/types.ts +19 -5
  107. package/src/plugins/defaults/compaction/compact.ts +24 -15
  108. package/src/plugins/defaults/compaction/context-overflow-reducer.ts +4 -4
  109. package/src/plugins/defaults/compaction/manager-store.ts +1 -1
  110. package/src/{context → plugins/defaults/compaction}/window-manager.ts +68 -12
  111. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +12 -18
  112. package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +19 -66
  113. package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +181 -0
  114. package/src/plugins/defaults/memory-v3-shadow/__tests__/edge.test.ts +247 -0
  115. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +139 -129
  116. package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +332 -164
  117. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +516 -293
  118. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +306 -0
  119. package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +51 -21
  120. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +402 -0
  121. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-needle.test.ts +135 -0
  122. package/src/plugins/defaults/memory-v3-shadow/__tests__/sections.test.ts +125 -0
  123. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +66 -11
  124. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +446 -0
  125. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +271 -110
  126. package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +0 -22
  127. package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +26 -62
  128. package/src/plugins/defaults/memory-v3-shadow/dense.ts +97 -0
  129. package/src/plugins/defaults/memory-v3-shadow/edge.ts +252 -0
  130. package/src/plugins/defaults/memory-v3-shadow/injector.ts +3 -2
  131. package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +415 -181
  132. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +196 -56
  133. package/src/plugins/defaults/memory-v3-shadow/page-content.ts +39 -2
  134. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +204 -0
  135. package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +15 -7
  136. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +236 -0
  137. package/src/plugins/defaults/memory-v3-shadow/section-needle.ts +200 -0
  138. package/src/plugins/defaults/memory-v3-shadow/sections.ts +115 -0
  139. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +75 -3
  140. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +111 -78
  141. package/src/plugins/defaults/memory-v3-shadow/types.ts +52 -19
  142. package/src/plugins/defaults/memory-v3-shadow/working-set.ts +4 -1
  143. package/src/plugins/external-api.ts +114 -0
  144. package/src/prompts/system-prompt.ts +61 -10
  145. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +37 -2
  146. package/src/providers/anthropic/client.ts +9 -10
  147. package/src/providers/inference/kimi-cjk-token-ids.ts +493 -0
  148. package/src/providers/inference/logit-bias.ts +55 -0
  149. package/src/providers/openai/__tests__/vision-not-supported.test.ts +75 -0
  150. package/src/providers/openai/chat-completions-provider.ts +44 -1
  151. package/src/providers/retry.ts +22 -0
  152. package/src/providers/types.ts +6 -0
  153. package/src/runtime/routes/__tests__/stt-routes.test.ts +112 -0
  154. package/src/runtime/routes/acp-routes.test.ts +3 -20
  155. package/src/runtime/routes/app-management-routes.ts +3 -0
  156. package/src/runtime/routes/conversation-routes.ts +2 -0
  157. package/src/runtime/routes/memory-v3-routes.ts +64 -314
  158. package/src/runtime/routes/playground/__tests__/force-compact.test.ts +1 -1
  159. package/src/runtime/routes/stt-routes.ts +45 -12
  160. package/src/runtime/routes/workspace-routes.ts +50 -15
  161. package/src/services/published-app-updater.ts +30 -8
  162. package/src/telemetry/__tests__/activation-funnel.test.ts +95 -0
  163. package/src/telemetry/activation-funnel.ts +167 -0
  164. package/src/telemetry/types.ts +13 -0
  165. package/src/telemetry/usage-telemetry-reporter.test.ts +154 -0
  166. package/src/telemetry/usage-telemetry-reporter.ts +26 -1
  167. package/src/tools/acp/list-agents.test.ts +2 -18
  168. package/src/tools/acp/list-agents.ts +3 -15
  169. package/src/tools/acp/spawn.test.ts +0 -10
  170. package/src/tools/browser/browser-execution.ts +12 -2
  171. package/src/tools/network/web-fetch.ts +65 -24
  172. package/src/tools/skills/load.ts +1 -1
  173. package/src/tools/ui-surface/definitions.ts +7 -0
  174. package/src/acp/feature-gate.test.ts +0 -48
  175. package/src/acp/feature-gate.ts +0 -34
  176. package/src/plugins/defaults/memory-v3-shadow/__tests__/assign.test.ts +0 -242
  177. package/src/plugins/defaults/memory-v3-shadow/__tests__/core.test.ts +0 -39
  178. package/src/plugins/defaults/memory-v3-shadow/__tests__/health.test.ts +0 -219
  179. package/src/plugins/defaults/memory-v3-shadow/__tests__/needle.test.ts +0 -107
  180. package/src/plugins/defaults/memory-v3-shadow/__tests__/provider-blocks.test.ts +0 -13
  181. package/src/plugins/defaults/memory-v3-shadow/__tests__/reconcile.test.ts +0 -274
  182. package/src/plugins/defaults/memory-v3-shadow/__tests__/router.test.ts +0 -337
  183. package/src/plugins/defaults/memory-v3-shadow/__tests__/selector.test.ts +0 -470
  184. package/src/plugins/defaults/memory-v3-shadow/__tests__/snapshot.test.ts +0 -168
  185. package/src/plugins/defaults/memory-v3-shadow/__tests__/tree.test.ts +0 -192
  186. package/src/plugins/defaults/memory-v3-shadow/assign.ts +0 -272
  187. package/src/plugins/defaults/memory-v3-shadow/core.ts +0 -26
  188. package/src/plugins/defaults/memory-v3-shadow/health.ts +0 -0
  189. package/src/plugins/defaults/memory-v3-shadow/needle.ts +0 -115
  190. package/src/plugins/defaults/memory-v3-shadow/provider-blocks.ts +0 -26
  191. package/src/plugins/defaults/memory-v3-shadow/reconcile.ts +0 -527
  192. package/src/plugins/defaults/memory-v3-shadow/router.ts +0 -190
  193. package/src/plugins/defaults/memory-v3-shadow/selector.ts +0 -226
  194. package/src/plugins/defaults/memory-v3-shadow/snapshot.ts +0 -209
  195. package/src/plugins/defaults/memory-v3-shadow/tree.ts +0 -174
  196. package/src/util/map-limit.ts +0 -27
@@ -0,0 +1,55 @@
1
+ import { KIMI_CJK_SINGLE_CHAR_TOKEN_IDS } from "./kimi-cjk-token-ids.js";
2
+
3
+ /**
4
+ * Named logit-bias presets that a profile may opt into via the `logitBias`
5
+ * field on `ProfileEntry` (see `config/schemas/llm.ts`). A preset resolves to a
6
+ * `{ "<tokenId>": bias }` map that OpenAI-compatible providers forward as the
7
+ * `logit_bias` request field.
8
+ *
9
+ * Presets are profile-scoped: `RetryProvider.normalizeSendMessageOptions`
10
+ * resolves the active profile's `logitBias` and only attaches the map on the
11
+ * Fireworks (OpenAI-compatible) path, so it never reaches providers/models it
12
+ * wasn't built for. Token IDs are tokenizer-specific — `suppress-cjk` only makes
13
+ * sense for the Kimi-tokenizer models Fireworks serves.
14
+ *
15
+ * The valid preset names must match the `logitBias` enum in
16
+ * `config/schemas/llm.ts` (kept as a plain literal there to avoid a schema →
17
+ * provider import cycle).
18
+ */
19
+
20
+ /**
21
+ * Soft negative bias applied to each CJK token. -4 discourages *spontaneous*
22
+ * Chinese (a marginal-probability slip) while leaving explicitly-requested
23
+ * Chinese (dominant-probability tokens) intact. Not a hard ban (-100 would
24
+ * clip legitimate CJK); tunable if slips persist in real usage.
25
+ */
26
+ export const SUPPRESS_CJK_BIAS = -4;
27
+
28
+ let suppressCjkMap: Readonly<Record<string, number>> | undefined;
29
+
30
+ function buildSuppressCjkMap(): Readonly<Record<string, number>> {
31
+ const map: Record<string, number> = {};
32
+ for (const id of KIMI_CJK_SINGLE_CHAR_TOKEN_IDS) {
33
+ map[id] = SUPPRESS_CJK_BIAS;
34
+ }
35
+ return map;
36
+ }
37
+
38
+ /**
39
+ * Resolve a preset to its `logit_bias` map for `model`, or `undefined` when the
40
+ * preset is unknown or doesn't apply to the model. `suppress-cjk`'s token IDs
41
+ * come from the Kimi tokenizer, so it only resolves for Kimi models — this keeps
42
+ * the preset from being misapplied to a different-tokenizer model that happened
43
+ * to inherit it. The map is built once and cached (a constant ~5.3k-entry object
44
+ * reused across every request on the owning profile).
45
+ */
46
+ export function resolveLogitBiasPreset(
47
+ preset: string,
48
+ model: string,
49
+ ): Readonly<Record<string, number>> | undefined {
50
+ if (preset === "suppress-cjk" && /kimi/i.test(model)) {
51
+ suppressCjkMap ??= buildSuppressCjkMap();
52
+ return suppressCjkMap;
53
+ }
54
+ return undefined;
55
+ }
@@ -0,0 +1,75 @@
1
+ import { describe, expect, test } from "bun:test";
2
+
3
+ import OpenAI from "openai";
4
+
5
+ import { detectVisionNotSupported } from "../chat-completions-provider.js";
6
+
7
+ function buildApiError(
8
+ status: number,
9
+ body: unknown,
10
+ ): InstanceType<typeof OpenAI.APIError> {
11
+ return new OpenAI.APIError(
12
+ status,
13
+ body as Record<string, unknown>,
14
+ undefined,
15
+ new Headers(),
16
+ );
17
+ }
18
+
19
+ describe("detectVisionNotSupported", () => {
20
+ test("detects OpenRouter 'No endpoints found that support image input'", () => {
21
+ const err = buildApiError(404, {
22
+ error: {
23
+ message: "No endpoints found that support image input",
24
+ code: 404,
25
+ },
26
+ });
27
+ expect(detectVisionNotSupported(err)).toBe(true);
28
+ });
29
+
30
+ test("detects 'does not support image' phrasing", () => {
31
+ const err = buildApiError(400, {
32
+ error: {
33
+ message: "This model does not support image input",
34
+ },
35
+ });
36
+ expect(detectVisionNotSupported(err)).toBe(true);
37
+ });
38
+
39
+ test("detects 'image input is not supported'", () => {
40
+ const err = buildApiError(400, {
41
+ error: {
42
+ message: "image input is not supported for this model",
43
+ },
44
+ });
45
+ expect(detectVisionNotSupported(err)).toBe(true);
46
+ });
47
+
48
+ test("detects 'vision is not supported'", () => {
49
+ const err = buildApiError(400, {
50
+ error: {
51
+ message: "Vision is not supported by this model",
52
+ },
53
+ });
54
+ expect(detectVisionNotSupported(err)).toBe(true);
55
+ });
56
+
57
+ test("returns false for unrelated errors", () => {
58
+ const err = buildApiError(429, {
59
+ error: {
60
+ message: "Rate limit exceeded",
61
+ },
62
+ });
63
+ expect(detectVisionNotSupported(err)).toBe(false);
64
+ });
65
+
66
+ test("returns false for context overflow", () => {
67
+ const err = buildApiError(400, {
68
+ error: {
69
+ code: "context_length_exceeded",
70
+ message: "This model's maximum context length is 128000 tokens",
71
+ },
72
+ });
73
+ expect(detectVisionNotSupported(err)).toBe(false);
74
+ });
75
+ });
@@ -101,6 +101,22 @@ export function extractApiErrorDetail(
101
101
  * OpenRouter's `error.metadata.raw` strings, which are typically <1KB. */
102
102
  const MAX_API_ERROR_DETAIL_CHARS = 2000;
103
103
 
104
+ const VISION_NOT_SUPPORTED_PATTERNS = [
105
+ /no endpoints found that support image input/i,
106
+ /does not support image/i,
107
+ /image input is not supported/i,
108
+ /this model does not support vision/i,
109
+ /vision is not supported/i,
110
+ /multi-?modal.*not.*support/i,
111
+ ];
112
+
113
+ export function detectVisionNotSupported(
114
+ error: InstanceType<typeof OpenAI.APIError>,
115
+ ): boolean {
116
+ const haystack = `${error.message} ${JSON.stringify((error as { error?: unknown }).error ?? "")}`;
117
+ return VISION_NOT_SUPPORTED_PATTERNS.some((re) => re.test(haystack));
118
+ }
119
+
104
120
  /**
105
121
  * Fallback `content` for an assistant turn that has neither visible text nor
106
122
  * tool calls (e.g. a reasoning-only turn truncated at the output-token limit).
@@ -293,6 +309,9 @@ export class OpenAIChatCompletionsProvider implements Provider {
293
309
  const maxTokens = configObj?.max_tokens as number | undefined;
294
310
  const modelOverride = configObj?.model as string | undefined;
295
311
  const effort = configObj?.effort as string | undefined;
312
+ const logitBias = configObj?.logit_bias as
313
+ | Record<string, number>
314
+ | undefined;
296
315
  const usageAttributionHeaders = configObj?.usageAttributionHeaders as
297
316
  | Record<string, string>
298
317
  | undefined;
@@ -313,6 +332,12 @@ export class OpenAIChatCompletionsProvider implements Provider {
313
332
  params.max_completion_tokens = maxTokens;
314
333
  }
315
334
 
335
+ // Profile-scoped token biasing (e.g. the `suppress-cjk` preset). Resolved
336
+ // and gated to this provider family upstream in `RetryProvider`.
337
+ if (logitBias) {
338
+ params.logit_bias = logitBias;
339
+ }
340
+
316
341
  // Subclasses (OpenRouter) may already have nested effort under
317
342
  // `reasoning.effort` via `buildExtraCreateParams`. Skip the flat
318
343
  // `reasoning_effort` assignment in that case to avoid sending both forms,
@@ -625,7 +650,16 @@ export class OpenAIChatCompletionsProvider implements Provider {
625
650
  : {}),
626
651
  },
627
652
  stopReason: finishReason,
628
- rawRequest: params,
653
+ // `rawRequest` is persisted to the request-log DB and inspector on every
654
+ // call. A `logit_bias` preset (e.g. `suppress-cjk`) is ~5.3k deterministic
655
+ // entries (~68KB); summarize it here so logs don't balloon. The full map
656
+ // still went out on the wire above.
657
+ rawRequest: params.logit_bias
658
+ ? {
659
+ ...params,
660
+ logit_bias: `<${Object.keys(params.logit_bias).length} token biases omitted>`,
661
+ }
662
+ : params,
629
663
  rawResponse,
630
664
  };
631
665
  } catch (error) {
@@ -649,6 +683,15 @@ export class OpenAIChatCompletionsProvider implements Provider {
649
683
  cause: error,
650
684
  });
651
685
  }
686
+ if (detectVisionNotSupported(error)) {
687
+ const model = modelOverride ?? this.model;
688
+ throw new ProviderError(
689
+ `This model (${model}) doesn't support image input. Remove the image or switch to a vision-capable model.`,
690
+ this.name,
691
+ error.status,
692
+ abortReason ? { abortReason } : undefined,
693
+ );
694
+ }
652
695
  const retryAfterMs = extractRetryAfterMs(error.headers);
653
696
  const errorOptions: {
654
697
  retryAfterMs?: number;
@@ -13,6 +13,7 @@ import {
13
13
  isRetryableNetworkError,
14
14
  sleep,
15
15
  } from "../util/retry.js";
16
+ import { resolveLogitBiasPreset } from "./inference/logit-bias.js";
16
17
  import {
17
18
  isThinkingConfigDisabled,
18
19
  normalizeThinkingConfigForWire,
@@ -277,6 +278,27 @@ function normalizeSendMessageOptions(
277
278
  ) {
278
279
  nextConfig.openrouter = { only: resolved.openrouter.only };
279
280
  }
281
+ // Forward a profile's opted-in `logit_bias` preset only on the Fireworks
282
+ // (OpenAI-compatible) path. `resolved.logitBias` is set by the resolver from
283
+ // the single winning profile (not the deep-merge), so it can't leak from a
284
+ // lower-precedence profile into one that didn't opt in.
285
+ // `resolveLogitBiasPreset` additionally gates on the resolved model's
286
+ // tokenizer. Strict-schema clients (Anthropic) reject unknown body fields,
287
+ // hence the provider gate.
288
+ if (
289
+ providerName === "fireworks" &&
290
+ nextConfig.logit_bias === undefined &&
291
+ resolved.logitBias !== undefined &&
292
+ typeof nextConfig.model === "string"
293
+ ) {
294
+ const biasMap = resolveLogitBiasPreset(
295
+ resolved.logitBias,
296
+ nextConfig.model,
297
+ );
298
+ if (biasMap !== undefined) {
299
+ nextConfig.logit_bias = biasMap;
300
+ }
301
+ }
280
302
  // `contextWindow` and `provider` are server-side concerns, not provider
281
303
  // request parameters: effective context is resolved per call site/profile
282
304
  // by the agent/conversation path, while `provider` selection is handled by
@@ -207,6 +207,12 @@ export interface SendMessageConfig {
207
207
  effort?: "none" | "low" | "medium" | "high" | "xhigh" | "max";
208
208
  speed?: "standard" | "fast";
209
209
  verbosity?: "low" | "medium" | "high";
210
+ /**
211
+ * Wire-format `logit_bias` map (`{ "<tokenId>": bias }`). Set by
212
+ * `RetryProvider` from a profile's `logitBias` preset and forwarded only on
213
+ * the OpenAI-compatible (Fireworks) path; other providers ignore it.
214
+ */
215
+ logit_bias?: Record<string, number>;
210
216
  /**
211
217
  * When true, the most recent user message's content varies across
212
218
  * otherwise-identical turns (e.g. a per-turn memory block was injected into
@@ -1,3 +1,8 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { writeFileSync } from "node:fs";
3
+ import { unlink, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
1
6
  import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
2
7
 
3
8
  // ---------------------------------------------------------------------------
@@ -26,6 +31,35 @@ mock.module("../../../providers/speech-to-text/resolve.js", () => ({
26
31
  },
27
32
  }));
28
33
 
34
+ // -- Spawn mock (ffmpeg/ffprobe) for the file-transcription path ------------
35
+
36
+ type SpawnResult = { exitCode: number; stdout: string; stderr: string };
37
+ let spawnOverride: ((args: string[]) => SpawnResult) | null = null;
38
+
39
+ /**
40
+ * Default spawn behavior: ffmpeg writes the requested output file so the
41
+ * downstream readFile succeeds, ffprobe reports a short duration.
42
+ */
43
+ function defaultSpawn(args: string[]): SpawnResult {
44
+ if (args[0] === "ffmpeg") {
45
+ const outputPath = args[args.length - 1];
46
+ writeFileSync(outputPath, Buffer.from("fake-wav-bytes"));
47
+ return { exitCode: 0, stdout: "", stderr: "" };
48
+ }
49
+ if (args[0] === "ffprobe") {
50
+ return { exitCode: 0, stdout: "1.0", stderr: "" };
51
+ }
52
+ return { exitCode: 0, stdout: "", stderr: "" };
53
+ }
54
+
55
+ mock.module("../../../util/spawn.js", () => ({
56
+ FFMPEG_TRANSCODE_TIMEOUT_MS: 60_000,
57
+ FFPROBE_TIMEOUT_MS: 10_000,
58
+ STT_REQUEST_TIMEOUT_MS: 300_000,
59
+ spawnWithTimeout: async (args: string[]) =>
60
+ (spawnOverride ?? defaultSpawn)(args),
61
+ }));
62
+
29
63
  // ---------------------------------------------------------------------------
30
64
  // Import under test — after mocks
31
65
  // ---------------------------------------------------------------------------
@@ -86,6 +120,7 @@ const fakeTranscriber: BatchTranscriber = {
86
120
  beforeEach(() => {
87
121
  mockTranscriber = fakeTranscriber;
88
122
  mockResolveError = null;
123
+ spawnOverride = null;
89
124
  });
90
125
 
91
126
  afterEach(() => {
@@ -366,4 +401,81 @@ describe("stt-routes", () => {
366
401
  "BAD_GATEWAY",
367
402
  );
368
403
  });
404
+
405
+ // -- File transcription: provider error categorization --------------------
406
+
407
+ async function withTempAudioFile(
408
+ run: (filePath: string) => Promise<void>,
409
+ ): Promise<void> {
410
+ const filePath = join(tmpdir(), `vellum-stt-test-${randomUUID()}.wav`);
411
+ await writeFile(filePath, Buffer.from("fake-audio"));
412
+ try {
413
+ await run(filePath);
414
+ } finally {
415
+ await unlink(filePath).catch(() => {});
416
+ }
417
+ }
418
+
419
+ test("transcribe-file maps provider auth failures (403) to 401", async () => {
420
+ // Reproduces JARVIS-994: a raw Gemini 403 denial must surface as an
421
+ // actionable 401, not a generic 502, and must not be treated as a
422
+ // daemon-level error.
423
+ mockTranscriber = {
424
+ providerId: "google-gemini",
425
+ boundaryId: "daemon-batch",
426
+ transcribe: async () => {
427
+ throw new Error(
428
+ 'Google Gemini API error (403): {"error":{"code":403,"message":"Your project has been denied access. Please contact support.","status":"PERMISSION_DENIED"}}',
429
+ );
430
+ },
431
+ };
432
+
433
+ await withTempAudioFile(async (filePath) => {
434
+ const { handler } = getRoute("stt/transcribe-file");
435
+ const err = await expectRouteError(
436
+ () => handler(makeArgs({ filePath })),
437
+ 401,
438
+ "UNAUTHORIZED",
439
+ );
440
+ expect(err.message).toContain("credentials");
441
+ });
442
+ });
443
+
444
+ test("transcribe-file maps generic provider failures to 502", async () => {
445
+ mockTranscriber = {
446
+ providerId: "openai-whisper",
447
+ boundaryId: "daemon-batch",
448
+ transcribe: async () => {
449
+ throw new Error("upstream kaboom");
450
+ },
451
+ };
452
+
453
+ await withTempAudioFile(async (filePath) => {
454
+ const { handler } = getRoute("stt/transcribe-file");
455
+ await expectRouteError(
456
+ () => handler(makeArgs({ filePath })),
457
+ 502,
458
+ "BAD_GATEWAY",
459
+ );
460
+ });
461
+ });
462
+
463
+ test("transcribe-file maps ffmpeg failures to 502 even when stderr contains a status-like number", async () => {
464
+ // Guards against ffmpeg stderr (or a user-supplied path) deciding the HTTP
465
+ // category: a conversion failure whose message contains "403" must stay a
466
+ // 502 conversion error, not become a misleading 401 auth error.
467
+ spawnOverride = (args) =>
468
+ args[0] === "ffmpeg"
469
+ ? { exitCode: 1, stdout: "", stderr: "Error opening /tmp/403/clip.wav" }
470
+ : { exitCode: 0, stdout: "1.0", stderr: "" };
471
+
472
+ await withTempAudioFile(async (filePath) => {
473
+ const { handler } = getRoute("stt/transcribe-file");
474
+ await expectRouteError(
475
+ () => handler(makeArgs({ filePath })),
476
+ 502,
477
+ "BAD_GATEWAY",
478
+ );
479
+ });
480
+ });
369
481
  });
@@ -2,9 +2,9 @@
2
2
  * Tests for the ACP route handlers.
3
3
  *
4
4
  * Suites:
5
- * - POST /v1/acp/spawn: resolver failure paths (acp_disabled,
6
- * unknown_agent) and the body-shape guard. The binary_not_found /
7
- * auto-install surface is covered in `__tests__/acp-routes.test.ts`.
5
+ * - POST /v1/acp/spawn: resolver failure paths (unknown_agent) and the
6
+ * body-shape guard. The binary_not_found / auto-install surface is
7
+ * covered in `__tests__/acp-routes.test.ts`.
8
8
  * - POST /v1/acp/spawn (env injection) — CLAUDE_CODE_OAUTH_TOKEN is read
9
9
  * from the credential broker (policy-gated + audited) and merged into
10
10
  * `agentConfig.env` ONLY for the `claude` agent.
@@ -200,21 +200,6 @@ beforeEach(() => {
200
200
  // ---------------------------------------------------------------------------
201
201
 
202
202
  describe("POST /v1/acp/spawn", () => {
203
- test("throws BadRequestError when ACP is disabled", async () => {
204
- config.setConfig({ enabled: false });
205
-
206
- const handler = getSpawnHandler();
207
- await expect(
208
- handler({
209
- body: {
210
- agent: "claude",
211
- task: "do a thing",
212
- conversationId: "conv-1",
213
- },
214
- }),
215
- ).rejects.toThrow("acp.enabled");
216
- });
217
-
218
203
  test("throws BadRequestError with merged available list when agent id is unknown", async () => {
219
204
  config.setConfig({
220
205
  agents: {
@@ -235,8 +220,6 @@ describe("POST /v1/acp/spawn", () => {
235
220
  });
236
221
 
237
222
  test("body-shape guard short-circuits before the resolver runs", async () => {
238
- config.setConfig({ enabled: false });
239
-
240
223
  const handler = getSpawnHandler();
241
224
  await expect(handler({ body: { agent: "claude" } })).rejects.toThrow(
242
225
  "agent, task, and conversationId are required",
@@ -89,6 +89,7 @@ function listAppsFiltered(apps?: AppDefinition[]): Array<{
89
89
  description?: string;
90
90
  icon?: string;
91
91
  createdAt: number;
92
+ updatedAt: number;
92
93
  version: string;
93
94
  contentId: string;
94
95
  }> {
@@ -101,6 +102,7 @@ function listAppsFiltered(apps?: AppDefinition[]): Array<{
101
102
  description: a.description,
102
103
  icon: a.icon,
103
104
  createdAt: a.createdAt,
105
+ updatedAt: a.updatedAt,
104
106
  version,
105
107
  contentId,
106
108
  };
@@ -751,6 +753,7 @@ export const ROUTES: RouteDefinition[] = [
751
753
  description: z.string().optional(),
752
754
  icon: z.string().optional(),
753
755
  createdAt: z.number(),
756
+ updatedAt: z.number(),
754
757
  version: z.string(),
755
758
  contentId: z.string(),
756
759
  }),
@@ -1741,6 +1741,7 @@ export async function handleSendMessage(
1741
1741
  ...(body.automated === true ? { automated: true } : {}),
1742
1742
  },
1743
1743
  isInteractive,
1744
+ sourceActorPrincipalId,
1744
1745
  transport,
1745
1746
  clientMessageId,
1746
1747
  });
@@ -1842,6 +1843,7 @@ export async function handleSendMessage(
1842
1843
  userMessageInterface: sourceInterface,
1843
1844
  assistantMessageInterface: sourceInterface,
1844
1845
  });
1846
+ conversation.currentTurnSourceActorPrincipalId = sourceActorPrincipalId;
1845
1847
 
1846
1848
  await conversation.ensureActorScopedHistory();
1847
1849