@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.
- package/docs/activation-funnel-telemetry.md +310 -0
- package/openapi.yaml +16 -115
- package/package.json +1 -1
- package/src/__tests__/activation-early-marking.test.ts +120 -0
- package/src/__tests__/agent-loop-output-hooks.test.ts +13 -13
- package/src/__tests__/anthropic-provider.test.ts +23 -8
- package/src/__tests__/approval-cascade.test.ts +1 -1
- package/src/__tests__/compaction-direct.test.ts +32 -18
- package/src/__tests__/compaction-events.test.ts +2 -2
- package/src/__tests__/compaction.benchmark.test.ts +1 -1
- package/src/__tests__/context-overflow-reducer.test.ts +5 -5
- package/src/__tests__/context-window-manager-compact-retry.test.ts +121 -15
- package/src/__tests__/conversation-abort-tool-results.test.ts +1 -1
- package/src/__tests__/conversation-confirmation-signals.test.ts +1 -1
- package/src/__tests__/conversation-error.test.ts +15 -1
- package/src/__tests__/conversation-history-web-search.test.ts +5 -0
- package/src/__tests__/conversation-media-retry.test.ts +1 -1
- package/src/__tests__/conversation-process-app-control-preactivation.test.ts +40 -0
- package/src/__tests__/conversation-process-callsite.test.ts +1 -1
- package/src/__tests__/conversation-provider-retry-repair.test.ts +1 -1
- package/src/__tests__/conversation-queue.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +71 -0
- package/src/__tests__/conversation-slash-queue.test.ts +1 -1
- package/src/__tests__/conversation-slash-unknown.test.ts +1 -1
- package/src/__tests__/conversation-speed-override.test.ts +1 -1
- package/src/__tests__/conversation-surfaces-activation-emit.test.ts +395 -0
- package/src/__tests__/conversation-surfaces-app-control.test.ts +44 -0
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +73 -4
- package/src/__tests__/conversation-undo.test.ts +2 -2
- package/src/__tests__/conversation-workspace-cache-state.test.ts +1 -1
- package/src/__tests__/conversation-workspace-injection.test.ts +1 -1
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/cu-unified-flow.test.ts +36 -0
- package/src/__tests__/history-repair-hook.test.ts +2 -0
- package/src/__tests__/llm-resolver.test.ts +73 -0
- package/src/__tests__/memory-retrieval-hook.test.ts +1 -2
- package/src/__tests__/persist-unsendable-image-downscale.test.ts +145 -0
- package/src/__tests__/persist-unsendable-image.test.ts +97 -1
- package/src/__tests__/plugin-external-api.test.ts +68 -0
- package/src/__tests__/post-turn-tool-result-truncation.test.ts +69 -0
- package/src/__tests__/published-app-updater.test.ts +138 -0
- package/src/__tests__/skill-feature-flags-integration.test.ts +5 -7
- package/src/__tests__/title-generate-hook.test.ts +2 -0
- package/src/__tests__/web-fetch.test.ts +45 -0
- package/src/acp/__tests__/helpers/acp-config-stub.ts +0 -2
- package/src/acp/resolve-agent.test.ts +0 -56
- package/src/acp/resolve-agent.ts +10 -38
- package/src/agent/loop.ts +13 -27
- package/src/api/responses/memory-v3-selection-log.ts +19 -10
- package/src/cli/commands/__tests__/memory-v3.test.ts +191 -210
- package/src/cli/commands/memory-v3.ts +57 -199
- package/src/cli/lib/__tests__/install-from-github.test.ts +232 -29
- package/src/cli/lib/__tests__/plugin-details.test.ts +28 -19
- package/src/cli/lib/__tests__/plugin-marketplace.test.ts +57 -7
- package/src/cli/lib/__tests__/search-plugins.test.ts +17 -10
- package/src/cli/lib/install-from-github.ts +258 -41
- package/src/cli/lib/plugin-details.ts +20 -13
- package/src/cli/lib/plugin-marketplace.ts +23 -5
- package/src/cli/lib/search-plugins.ts +14 -8
- package/src/config/acp-defaults.ts +3 -3
- package/src/config/acp-schema.ts +1 -7
- package/src/config/bundled-skills/acp/SKILL.md +4 -17
- package/src/config/bundled-skills/acp/TOOLS.json +2 -2
- package/src/config/call-site-defaults.ts +0 -1
- package/src/config/feature-flag-registry.json +3 -18
- package/src/config/llm-resolver.ts +39 -7
- package/src/config/schemas/__tests__/memory-v3.test.ts +25 -9
- package/src/config/schemas/call-site-catalog.ts +0 -7
- package/src/config/schemas/llm.ts +17 -1
- package/src/config/schemas/memory-v3.ts +58 -8
- package/src/config/seed-inference-profiles.ts +18 -0
- package/src/context/post-turn-tool-result-truncation.ts +39 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +8 -1
- package/src/daemon/conversation-agent-loop.ts +87 -95
- package/src/daemon/conversation-error.ts +31 -4
- package/src/daemon/conversation-history.ts +1 -1
- package/src/daemon/conversation-media-retry.ts +19 -6
- package/src/daemon/conversation-messaging.ts +17 -0
- package/src/daemon/conversation-process.ts +14 -5
- package/src/daemon/conversation-queue-manager.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +37 -1
- package/src/daemon/conversation-surfaces.ts +141 -3
- package/src/daemon/conversation.ts +48 -13
- package/src/daemon/external-plugins-bootstrap.ts +8 -3
- package/src/daemon/persist-unsendable-image.ts +62 -25
- package/src/daemon/process-message.ts +1 -1
- package/src/daemon/tool-side-effects.ts +15 -0
- package/src/memory/__tests__/activation-session-store.test.ts +41 -0
- package/src/memory/__tests__/onboarding-events-store.test.ts +80 -0
- package/src/memory/activation-session-store.ts +43 -0
- package/src/memory/db-init.ts +4 -0
- package/src/memory/migrations/273-onboarding-events-funnel-columns.ts +46 -0
- package/src/memory/migrations/274-create-activation-sessions.ts +15 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/onboarding-events-store.ts +66 -18
- package/src/memory/schema/infrastructure.ts +13 -0
- package/src/memory/v2/__tests__/consolidation-job.test.ts +4 -97
- package/src/memory/v2/__tests__/page-store.test.ts +22 -0
- package/src/memory/v2/consolidation-job.ts +2 -72
- package/src/memory/v2/types.ts +5 -0
- package/src/messaging/providers/telegram-bot/api.ts +14 -5
- package/src/notifications/adapters/telegram.ts +7 -1
- package/src/plugin-api/constants.ts +2 -2
- package/src/plugin-api/index.ts +2 -2
- package/src/plugin-api/types.ts +19 -5
- package/src/plugins/defaults/compaction/compact.ts +24 -15
- package/src/plugins/defaults/compaction/context-overflow-reducer.ts +4 -4
- package/src/plugins/defaults/compaction/manager-store.ts +1 -1
- package/src/{context → plugins/defaults/compaction}/window-manager.ts +68 -12
- package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit-temp.ts +12 -18
- package/src/plugins/defaults/memory-v3-shadow/__tests__/capabilities.test.ts +19 -66
- package/src/plugins/defaults/memory-v3-shadow/__tests__/dense.test.ts +181 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/edge.test.ts +247 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +139 -129
- package/src/plugins/defaults/memory-v3-shadow/__tests__/maintain-job.test.ts +332 -164
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +516 -293
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +306 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/render-injection.test.ts +51 -21
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +402 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/section-needle.test.ts +135 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/sections.test.ts +125 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +66 -11
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +446 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +271 -110
- package/src/plugins/defaults/memory-v3-shadow/__tests__/types.test.ts +0 -22
- package/src/plugins/defaults/memory-v3-shadow/capabilities.ts +26 -62
- package/src/plugins/defaults/memory-v3-shadow/dense.ts +97 -0
- package/src/plugins/defaults/memory-v3-shadow/edge.ts +252 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +3 -2
- package/src/plugins/defaults/memory-v3-shadow/maintain-job.ts +415 -181
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +196 -56
- package/src/plugins/defaults/memory-v3-shadow/page-content.ts +39 -2
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +204 -0
- package/src/plugins/defaults/memory-v3-shadow/render-injection.ts +15 -7
- package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +236 -0
- package/src/plugins/defaults/memory-v3-shadow/section-needle.ts +200 -0
- package/src/plugins/defaults/memory-v3-shadow/sections.ts +115 -0
- package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +75 -3
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +111 -78
- package/src/plugins/defaults/memory-v3-shadow/types.ts +52 -19
- package/src/plugins/defaults/memory-v3-shadow/working-set.ts +4 -1
- package/src/plugins/external-api.ts +114 -0
- package/src/prompts/system-prompt.ts +61 -10
- package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +37 -2
- package/src/providers/anthropic/client.ts +9 -10
- package/src/providers/inference/kimi-cjk-token-ids.ts +493 -0
- package/src/providers/inference/logit-bias.ts +55 -0
- package/src/providers/openai/__tests__/vision-not-supported.test.ts +75 -0
- package/src/providers/openai/chat-completions-provider.ts +44 -1
- package/src/providers/retry.ts +22 -0
- package/src/providers/types.ts +6 -0
- package/src/runtime/routes/__tests__/stt-routes.test.ts +112 -0
- package/src/runtime/routes/acp-routes.test.ts +3 -20
- package/src/runtime/routes/app-management-routes.ts +3 -0
- package/src/runtime/routes/conversation-routes.ts +2 -0
- package/src/runtime/routes/memory-v3-routes.ts +64 -314
- package/src/runtime/routes/playground/__tests__/force-compact.test.ts +1 -1
- package/src/runtime/routes/stt-routes.ts +45 -12
- package/src/runtime/routes/workspace-routes.ts +50 -15
- package/src/services/published-app-updater.ts +30 -8
- package/src/telemetry/__tests__/activation-funnel.test.ts +95 -0
- package/src/telemetry/activation-funnel.ts +167 -0
- package/src/telemetry/types.ts +13 -0
- package/src/telemetry/usage-telemetry-reporter.test.ts +154 -0
- package/src/telemetry/usage-telemetry-reporter.ts +26 -1
- package/src/tools/acp/list-agents.test.ts +2 -18
- package/src/tools/acp/list-agents.ts +3 -15
- package/src/tools/acp/spawn.test.ts +0 -10
- package/src/tools/browser/browser-execution.ts +12 -2
- package/src/tools/network/web-fetch.ts +65 -24
- package/src/tools/skills/load.ts +1 -1
- package/src/tools/ui-surface/definitions.ts +7 -0
- package/src/acp/feature-gate.test.ts +0 -48
- package/src/acp/feature-gate.ts +0 -34
- package/src/plugins/defaults/memory-v3-shadow/__tests__/assign.test.ts +0 -242
- package/src/plugins/defaults/memory-v3-shadow/__tests__/core.test.ts +0 -39
- package/src/plugins/defaults/memory-v3-shadow/__tests__/health.test.ts +0 -219
- package/src/plugins/defaults/memory-v3-shadow/__tests__/needle.test.ts +0 -107
- package/src/plugins/defaults/memory-v3-shadow/__tests__/provider-blocks.test.ts +0 -13
- package/src/plugins/defaults/memory-v3-shadow/__tests__/reconcile.test.ts +0 -274
- package/src/plugins/defaults/memory-v3-shadow/__tests__/router.test.ts +0 -337
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selector.test.ts +0 -470
- package/src/plugins/defaults/memory-v3-shadow/__tests__/snapshot.test.ts +0 -168
- package/src/plugins/defaults/memory-v3-shadow/__tests__/tree.test.ts +0 -192
- package/src/plugins/defaults/memory-v3-shadow/assign.ts +0 -272
- package/src/plugins/defaults/memory-v3-shadow/core.ts +0 -26
- package/src/plugins/defaults/memory-v3-shadow/health.ts +0 -0
- package/src/plugins/defaults/memory-v3-shadow/needle.ts +0 -115
- package/src/plugins/defaults/memory-v3-shadow/provider-blocks.ts +0 -26
- package/src/plugins/defaults/memory-v3-shadow/reconcile.ts +0 -527
- package/src/plugins/defaults/memory-v3-shadow/router.ts +0 -190
- package/src/plugins/defaults/memory-v3-shadow/selector.ts +0 -226
- package/src/plugins/defaults/memory-v3-shadow/snapshot.ts +0 -209
- package/src/plugins/defaults/memory-v3-shadow/tree.ts +0 -174
- 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
|
|
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;
|
package/src/providers/retry.ts
CHANGED
|
@@ -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
|
package/src/providers/types.ts
CHANGED
|
@@ -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 (
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|