@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.
- package/ARCHITECTURE.md +11 -12
- package/docker-entrypoint.sh +13 -1
- package/docker-init-apt-root.sh +79 -6
- package/openapi.yaml +336 -21
- package/package.json +1 -1
- package/src/__tests__/agent-loop-exit-reason.test.ts +272 -0
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
- package/src/__tests__/compactor-tail-resolution.test.ts +107 -1
- package/src/__tests__/config-get-vision-flag.test.ts +136 -0
- package/src/__tests__/config-loader-backfill.test.ts +115 -18
- package/src/__tests__/context-token-estimator.test.ts +30 -65
- package/src/__tests__/conversation-agent-loop.test.ts +57 -1
- package/src/__tests__/conversation-media-retry.test.ts +19 -8
- package/src/__tests__/conversation-runtime-assembly.test.ts +26 -4
- package/src/__tests__/date-context.test.ts +45 -0
- package/src/__tests__/external-plugin-loader.test.ts +91 -19
- package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
- package/src/__tests__/guardian-dispatch.test.ts +1 -0
- package/src/__tests__/heartbeat-service.test.ts +24 -164
- package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
- package/src/__tests__/host-app-control-proxy.test.ts +241 -0
- package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
- package/src/__tests__/injector-background-turn.test.ts +153 -0
- package/src/__tests__/injector-chain.test.ts +5 -0
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -2
- package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
- package/src/__tests__/llm-catalog-parity.test.ts +3 -0
- package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
- package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
- package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -0
- package/src/__tests__/llm-resolver.test.ts +255 -2
- package/src/__tests__/managed-profile-guard.test.ts +10 -0
- package/src/__tests__/notification-decision-fallback.test.ts +0 -91
- package/src/__tests__/notification-decision-strategy.test.ts +14 -31
- package/src/__tests__/notification-deep-link.test.ts +15 -0
- package/src/__tests__/notification-guardian-path.test.ts +1 -2
- package/src/__tests__/notification-platform-adapter.test.ts +5 -4
- package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
- package/src/__tests__/openai-provider.test.ts +218 -3
- package/src/__tests__/openai-responses-cutover-guard.test.ts +3 -3
- package/src/__tests__/openrouter-provider-only.test.ts +51 -3
- package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
- package/src/__tests__/platform-proxy-context.test.ts +6 -1
- package/src/__tests__/plugin-tool-contribution.test.ts +3 -3
- package/src/__tests__/plugin-types.test.ts +2 -2
- package/src/__tests__/provider-catalog-visibility.test.ts +16 -0
- package/src/__tests__/provider-platform-proxy-integration.test.ts +27 -25
- package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
- package/src/__tests__/system-prompt.test.ts +6 -73
- package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
- package/src/a2a/__tests__/agent-card.test.ts +98 -0
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
- package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
- package/src/a2a/__tests__/task-store.test.ts +246 -0
- package/src/a2a/agent-card.ts +58 -0
- package/src/a2a/feature-gate.ts +8 -0
- package/src/a2a/protocol-constants.ts +21 -0
- package/src/a2a/protocol-errors.ts +50 -0
- package/src/a2a/protocol-types.ts +162 -0
- package/src/a2a/task-store.ts +168 -0
- package/src/agent/loop.ts +167 -18
- package/src/channels/config.ts +9 -0
- package/src/channels/types.ts +14 -0
- package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
- package/src/cli/commands/__tests__/schedules.test.ts +469 -0
- package/src/cli/commands/notifications.ts +65 -35
- package/src/cli/commands/plugins.ts +67 -0
- package/src/cli/commands/schedules.ts +297 -5
- package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
- package/src/cli/lib/install-from-github.ts +8 -9
- package/src/cli/lib/search-plugins.ts +163 -0
- package/src/cli/program.ts +14 -0
- package/src/config/assistant-feature-flags.ts +24 -54
- package/src/config/bundled-skills/app-builder/SKILL.md +117 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
- package/src/config/call-site-defaults.ts +105 -0
- package/src/config/feature-flag-registry.json +21 -29
- package/src/config/llm-resolver.ts +52 -1
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/memory-v2.test.ts +3 -3
- package/src/config/schemas/channels.ts +9 -0
- package/src/config/schemas/conversations.ts +10 -0
- package/src/config/schemas/heartbeat.ts +14 -0
- package/src/config/schemas/llm.ts +1 -3
- package/src/config/schemas/memory-retrospective.ts +1 -1
- package/src/config/schemas/memory-v2.ts +4 -4
- package/src/config/schemas/memory.ts +3 -1
- package/src/config/seed-inference-profiles.ts +99 -29
- package/src/context/compactor.ts +72 -12
- package/src/context/token-estimator.ts +32 -34
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -22
- package/src/daemon/conversation-agent-loop-handlers.ts +78 -0
- package/src/daemon/conversation-agent-loop.ts +29 -2
- package/src/daemon/conversation-runtime-assembly.ts +9 -0
- package/src/daemon/conversation.ts +0 -7
- package/src/daemon/date-context.ts +40 -0
- package/src/daemon/guardian-action-generators.ts +1 -125
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
- package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
- package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
- package/src/daemon/handlers/config-a2a.ts +289 -0
- package/src/daemon/handlers/conversations.ts +1 -0
- package/src/daemon/host-app-control-proxy.ts +69 -18
- package/src/daemon/host-proxy-preactivation.ts +85 -18
- package/src/daemon/lifecycle.ts +49 -61
- package/src/daemon/memory-v2-startup.ts +49 -13
- package/src/daemon/message-types/notifications.ts +21 -0
- package/src/daemon/pkb-reminder-builder.test.ts +10 -53
- package/src/daemon/pkb-reminder-builder.ts +4 -19
- package/src/daemon/process-message.ts +3 -0
- package/src/daemon/skill-memory-refresh.ts +5 -1
- package/src/daemon/wake-target-adapter.ts +2 -0
- package/src/export/__tests__/transcript-formatter.test.ts +121 -0
- package/src/export/transcript-formatter.ts +54 -20
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +44 -0
- package/src/heartbeat/heartbeat-service.ts +34 -191
- package/src/home/__tests__/feed-types.test.ts +40 -0
- package/src/home/feed-types.ts +14 -2
- package/src/ipc/cli-client.ts +147 -45
- package/src/memory/__tests__/conversation-queries.test.ts +220 -0
- package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
- package/src/memory/__tests__/memory-retrospective-job.test.ts +87 -4
- package/src/memory/conversation-queries.ts +87 -1
- package/src/memory/conversation-title-service.ts +26 -4
- package/src/memory/db-init.ts +6 -0
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +84 -3
- package/src/memory/graph/conversation-graph-memory.ts +18 -6
- package/src/memory/graph/tools.ts +6 -37
- package/src/memory/invite-store.ts +53 -0
- package/src/memory/llm-request-log-source-clickhouse.ts +7 -2
- package/src/memory/llm-request-log-store.ts +92 -1
- package/src/memory/memory-retrospective-enqueue.ts +1 -20
- package/src/memory/memory-retrospective-job.ts +33 -6
- package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
- package/src/memory/migrations/251-a2a-tasks.ts +49 -0
- package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/a2a.ts +15 -0
- package/src/memory/schema/index.ts +1 -0
- package/src/memory/schema/inference.ts +2 -0
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
- package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
- package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
- package/src/memory/v2/__tests__/injection.test.ts +190 -3
- package/src/memory/v2/__tests__/static-context.test.ts +12 -1
- package/src/memory/v2/activation-store.ts +14 -16
- package/src/memory/v2/cli-command-content.ts +19 -0
- package/src/memory/v2/cli-command-store.ts +304 -0
- package/src/memory/v2/frontmatter-sweep.ts +7 -1
- package/src/memory/v2/injection.ts +49 -20
- package/src/memory/v2/page-index.ts +38 -13
- package/src/memory/v2/static-context.ts +4 -4
- package/src/memory/v2/types.ts +23 -0
- package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
- package/src/messaging/providers/a2a/deliver.ts +156 -0
- package/src/messaging/providers/gmail/client.ts +9 -2
- package/src/messaging/providers/index.ts +11 -2
- package/src/notifications/__tests__/broadcaster.test.ts +203 -0
- package/src/notifications/__tests__/decision-engine.test.ts +283 -0
- package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +430 -7
- package/src/notifications/adapters/macos.ts +12 -2
- package/src/notifications/broadcaster.ts +29 -4
- package/src/notifications/copy-composer.ts +17 -64
- package/src/notifications/decision-engine.ts +111 -44
- package/src/notifications/deterministic-checks.ts +96 -0
- package/src/notifications/emit-signal.ts +1 -0
- package/src/notifications/home-feed-side-effect.ts +85 -6
- package/src/notifications/signal.ts +0 -4
- package/src/notifications/types.ts +8 -0
- package/src/oauth/platform-connection.test.ts +43 -3
- package/src/oauth/platform-connection.ts +13 -4
- package/src/plugins/defaults/injectors.ts +38 -19
- package/src/plugins/external-plugin-loader.ts +82 -10
- package/src/plugins/types.ts +16 -7
- package/src/prompts/__tests__/system-prompt.test.ts +6 -51
- package/src/prompts/__tests__/task-progress-hint-section.test.ts +4 -8
- package/src/prompts/system-prompt.ts +0 -8
- package/src/prompts/templates/BOOTSTRAP.md +5 -5
- package/src/prompts/templates/system-sections.ts +0 -9
- package/src/providers/__tests__/inference.test.ts +2 -0
- package/src/providers/call-site-routing.ts +24 -6
- package/src/providers/connection-resolution.ts +63 -13
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
- package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
- package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
- package/src/providers/inference/adapter-factory.ts +9 -20
- package/src/providers/inference/auth.ts +12 -0
- package/src/providers/inference/backfill.ts +14 -1
- package/src/providers/inference/connections.ts +85 -5
- package/src/providers/inference/resolve-auth.ts +2 -0
- package/src/providers/model-catalog.ts +199 -244
- package/src/providers/model-intents.ts +3 -3
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
- package/src/providers/openai/chat-completions-provider.ts +159 -6
- package/src/providers/openrouter/client.ts +42 -4
- package/src/providers/platform-proxy/constants.ts +3 -4
- package/src/providers/provider-catalog-visibility.ts +3 -1
- package/src/providers/provider-send-message.ts +27 -12
- package/src/providers/registry.ts +30 -1
- package/src/runtime/agent-wake.ts +61 -1
- package/src/runtime/auth/route-policy.ts +13 -0
- package/src/runtime/http-server.ts +7 -16
- package/src/runtime/http-types.ts +0 -47
- package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +66 -4
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
- package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
- package/src/runtime/routes/channel-availability-routes.ts +5 -0
- package/src/runtime/routes/consolidation-routes.ts +100 -0
- package/src/runtime/routes/conversation-query-routes.ts +70 -11
- package/src/runtime/routes/conversation-routes.ts +7 -0
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +134 -1
- package/src/runtime/routes/integrations/a2a.ts +235 -0
- package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
- package/src/runtime/routes/subagents-routes.ts +41 -0
- package/src/subagent/manager.ts +2 -0
- package/src/tools/memory/register.ts +1 -9
- package/src/tools/registry.ts +2 -2
- package/src/tools/types.ts +37 -2
- package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
- package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
- package/src/runtime/guardian-action-conversation-turn.ts +0 -99
|
@@ -95,6 +95,7 @@ describe("injector chain", () => {
|
|
|
95
95
|
expect(names).toEqual([
|
|
96
96
|
"disk-pressure-warning",
|
|
97
97
|
"workspace-context",
|
|
98
|
+
"background-turn",
|
|
98
99
|
"unified-turn-context",
|
|
99
100
|
"pkb-context",
|
|
100
101
|
"pkb-reminder",
|
|
@@ -117,6 +118,9 @@ describe("injector chain", () => {
|
|
|
117
118
|
expect(byName.get("workspace-context")).toBe(
|
|
118
119
|
DEFAULT_INJECTOR_ORDER.workspaceContext,
|
|
119
120
|
);
|
|
121
|
+
expect(byName.get("background-turn")).toBe(
|
|
122
|
+
DEFAULT_INJECTOR_ORDER.backgroundTurn,
|
|
123
|
+
);
|
|
120
124
|
expect(byName.get("unified-turn-context")).toBe(
|
|
121
125
|
DEFAULT_INJECTOR_ORDER.unifiedTurnContext,
|
|
122
126
|
);
|
|
@@ -154,6 +158,7 @@ describe("injector chain", () => {
|
|
|
154
158
|
expect(names).toEqual([
|
|
155
159
|
"disk-pressure-warning", // 5
|
|
156
160
|
"workspace-context", // 10
|
|
161
|
+
"background-turn", // 15
|
|
157
162
|
"unified-turn-context", // 20
|
|
158
163
|
"plugin-25", // 25 — slots in
|
|
159
164
|
"pkb-context", // 30
|
|
@@ -71,6 +71,13 @@ mock.module("../memory/v2/skill-store.js", () => ({
|
|
|
71
71
|
},
|
|
72
72
|
}));
|
|
73
73
|
|
|
74
|
+
// Mock the sibling CLI-command seeder so `rebuildBm25CorpusStatsAndReseedSkills`
|
|
75
|
+
// (which runs both reseeds in parallel) does not invoke the real Qdrant-backed
|
|
76
|
+
// implementation and emit warnings that break the no-warnings assertions below.
|
|
77
|
+
mock.module("../memory/v2/cli-command-store.js", () => ({
|
|
78
|
+
seedV2CliCommandEntries: async (): Promise<void> => {},
|
|
79
|
+
}));
|
|
80
|
+
|
|
74
81
|
mock.module("../memory/v2/qdrant.js", () => ({
|
|
75
82
|
ensureConceptPageCollection: async (): Promise<{ migrated: boolean }> => {
|
|
76
83
|
state.ensureCollectionCallCount += 1;
|
|
@@ -312,10 +319,10 @@ describe("rebuildBm25CorpusStatsAndReseedSkills", () => {
|
|
|
312
319
|
expect(state.warnCalls).toHaveLength(0);
|
|
313
320
|
});
|
|
314
321
|
|
|
315
|
-
test("
|
|
322
|
+
test("skips both corpus stats and skill reseed when v2 is disabled", async () => {
|
|
316
323
|
await rebuildBm25CorpusStatsAndReseedSkills(makeConfig(false));
|
|
317
324
|
|
|
318
|
-
expect(state.corpusStatsBuildCount).toBe(
|
|
325
|
+
expect(state.corpusStatsBuildCount).toBe(0);
|
|
319
326
|
expect(state.seedCallCount).toBe(0);
|
|
320
327
|
expect(state.warnCalls).toHaveLength(0);
|
|
321
328
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
|
+
import { CALL_SITE_DEFAULTS } from "../config/call-site-defaults.js";
|
|
3
4
|
import { getLLMCallSiteLabel } from "../config/llm-callsite-catalog.js";
|
|
4
5
|
import { CALL_SITE_CATALOG } from "../config/schemas/call-site-catalog.js";
|
|
5
6
|
import { LLMCallSiteEnum, LLMSchema } from "../config/schemas/llm.js";
|
|
@@ -50,4 +51,28 @@ describe("LLM call-site catalog", () => {
|
|
|
50
51
|
});
|
|
51
52
|
expect(parsed.callSites.memoryRouter?.model).toBe("claude-sonnet-4-6");
|
|
52
53
|
});
|
|
54
|
+
|
|
55
|
+
test("CALL_SITE_DEFAULTS covers every LLMCallSite enum value", () => {
|
|
56
|
+
const defaultIds = new Set(Object.keys(CALL_SITE_DEFAULTS));
|
|
57
|
+
const missing = LLMCallSiteEnum.options.filter(
|
|
58
|
+
(id) => !defaultIds.has(id),
|
|
59
|
+
);
|
|
60
|
+
expect(missing).toEqual([]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("CALL_SITE_DEFAULTS contains no unknown call-site keys", () => {
|
|
64
|
+
const enumIds = new Set<string>(LLMCallSiteEnum.options);
|
|
65
|
+
const extra = Object.keys(CALL_SITE_DEFAULTS).filter(
|
|
66
|
+
(id) => !enumIds.has(id),
|
|
67
|
+
);
|
|
68
|
+
expect(extra).toEqual([]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("every CALL_SITE_DEFAULTS entry has a profile field", () => {
|
|
72
|
+
for (const [, config] of Object.entries(CALL_SITE_DEFAULTS)) {
|
|
73
|
+
expect(config.profile).toBeDefined();
|
|
74
|
+
expect(typeof config.profile).toBe("string");
|
|
75
|
+
expect(config.profile!.length).toBeGreaterThan(0);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
53
78
|
});
|
|
@@ -208,6 +208,9 @@ describe("LLM catalog parity: daemon vs client", () => {
|
|
|
208
208
|
|
|
209
209
|
test("every provider's defaultModel exists in its models list", () => {
|
|
210
210
|
for (const entry of PROVIDER_CATALOG) {
|
|
211
|
+
// Providers with an empty models list (e.g. openai-compatible) use
|
|
212
|
+
// per-connection model identifiers instead of a static catalog.
|
|
213
|
+
if (entry.models.length === 0) continue;
|
|
211
214
|
const found = entry.models.some((m) => m.id === entry.defaultModel);
|
|
212
215
|
expect(
|
|
213
216
|
found,
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for `setAgentLoopExitReasonOnLatestLog` and the
|
|
3
|
+
* `agentLoopExitReason` field on the LogRow type. The helper stamps the
|
|
4
|
+
* reason onto the most-recent `llm_request_logs` row for a conversation,
|
|
5
|
+
* which is how downstream tooling distinguishes "loop kept going" (null)
|
|
6
|
+
* from "loop exited because X" (specific reason) on a per-row basis.
|
|
7
|
+
*/
|
|
8
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
9
|
+
|
|
10
|
+
mock.module("../util/logger.js", () => ({
|
|
11
|
+
getLogger: () =>
|
|
12
|
+
new Proxy({} as Record<string, unknown>, {
|
|
13
|
+
get: () => () => {},
|
|
14
|
+
}),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
mock.module("../config/loader.js", () => ({
|
|
18
|
+
getConfig: () => ({
|
|
19
|
+
ui: {},
|
|
20
|
+
model: "test",
|
|
21
|
+
provider: "test",
|
|
22
|
+
memory: { enabled: false },
|
|
23
|
+
rateLimit: { maxRequestsPerMinute: 0 },
|
|
24
|
+
secretDetection: { enabled: false },
|
|
25
|
+
}),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
import { getDb } from "../memory/db-connection.js";
|
|
29
|
+
import { initializeDb } from "../memory/db-init.js";
|
|
30
|
+
import {
|
|
31
|
+
getRequestLogById,
|
|
32
|
+
recordRequestLog,
|
|
33
|
+
setAgentLoopExitReasonOnLatestLog,
|
|
34
|
+
} from "../memory/llm-request-log-store.js";
|
|
35
|
+
import { llmRequestLogs } from "../memory/schema.js";
|
|
36
|
+
|
|
37
|
+
initializeDb();
|
|
38
|
+
|
|
39
|
+
function resetLogs(): void {
|
|
40
|
+
const db = getDb();
|
|
41
|
+
db.delete(llmRequestLogs).run();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe("setAgentLoopExitReasonOnLatestLog", () => {
|
|
45
|
+
beforeEach(resetLogs);
|
|
46
|
+
|
|
47
|
+
test("recordRequestLog leaves agentLoopExitReason NULL", () => {
|
|
48
|
+
const id = recordRequestLog("conv-1", '{"req":1}', '{"res":1}');
|
|
49
|
+
const row = getRequestLogById(id);
|
|
50
|
+
expect(row).not.toBeNull();
|
|
51
|
+
expect(row!.agentLoopExitReason).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("stamps the reason onto the most-recent log for the conversation", () => {
|
|
55
|
+
const first = recordRequestLog("conv-1", '{"req":1}', '{"res":1}');
|
|
56
|
+
// Ensure createdAt strict ordering — `recordRequestLog` uses
|
|
57
|
+
// `Date.now()` and bun-sqlite is fast enough that two consecutive
|
|
58
|
+
// inserts can share a millisecond. Sleep a tick to disambiguate.
|
|
59
|
+
Bun.sleepSync(2);
|
|
60
|
+
const second = recordRequestLog("conv-1", '{"req":2}', '{"res":2}');
|
|
61
|
+
|
|
62
|
+
setAgentLoopExitReasonOnLatestLog("conv-1", "no_tool_calls");
|
|
63
|
+
|
|
64
|
+
expect(getRequestLogById(first)?.agentLoopExitReason).toBeNull();
|
|
65
|
+
expect(getRequestLogById(second)?.agentLoopExitReason).toBe(
|
|
66
|
+
"no_tool_calls",
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("scopes the stamp to the given conversation only", () => {
|
|
71
|
+
const a = recordRequestLog("conv-a", '{"req":1}', '{"res":1}');
|
|
72
|
+
Bun.sleepSync(2);
|
|
73
|
+
const b = recordRequestLog("conv-b", '{"req":1}', '{"res":1}');
|
|
74
|
+
|
|
75
|
+
setAgentLoopExitReasonOnLatestLog("conv-a", "yield_to_user");
|
|
76
|
+
|
|
77
|
+
expect(getRequestLogById(a)?.agentLoopExitReason).toBe("yield_to_user");
|
|
78
|
+
// conv-b is later overall but belongs to a different conversation —
|
|
79
|
+
// must stay NULL.
|
|
80
|
+
expect(getRequestLogById(b)?.agentLoopExitReason).toBeNull();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("no-op when conversation has no logs", () => {
|
|
84
|
+
expect(() =>
|
|
85
|
+
setAgentLoopExitReasonOnLatestLog("conv-missing", "error"),
|
|
86
|
+
).not.toThrow();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("does not clobber a previous run's reason when the current run never landed a row", () => {
|
|
90
|
+
// Previous run: completes, lands a log, gets stamped.
|
|
91
|
+
const prev = recordRequestLog("conv-1", '{"prev_req":1}', '{"prev_res":1}');
|
|
92
|
+
setAgentLoopExitReasonOnLatestLog("conv-1", "no_tool_calls");
|
|
93
|
+
expect(getRequestLogById(prev)?.agentLoopExitReason).toBe("no_tool_calls");
|
|
94
|
+
|
|
95
|
+
// Current run aborts pre-call (or similar) before any LLM call lands.
|
|
96
|
+
// The helper must NOT overwrite the previous run's row.
|
|
97
|
+
setAgentLoopExitReasonOnLatestLog("conv-1", "aborted_pre_call");
|
|
98
|
+
expect(getRequestLogById(prev)?.agentLoopExitReason).toBe("no_tool_calls");
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("stamps the current run's newest row even when a prior row is already stamped", () => {
|
|
102
|
+
// Prior run already stamped.
|
|
103
|
+
const prev = recordRequestLog("conv-1", '{"prev_req":1}', '{"prev_res":1}');
|
|
104
|
+
setAgentLoopExitReasonOnLatestLog("conv-1", "no_tool_calls");
|
|
105
|
+
|
|
106
|
+
// Current run lands a new log, then exits.
|
|
107
|
+
Bun.sleepSync(2);
|
|
108
|
+
const current = recordRequestLog("conv-1", '{"cur_req":1}', '{"cur_res":1}');
|
|
109
|
+
setAgentLoopExitReasonOnLatestLog("conv-1", "yield_to_user");
|
|
110
|
+
|
|
111
|
+
expect(getRequestLogById(prev)?.agentLoopExitReason).toBe("no_tool_calls");
|
|
112
|
+
expect(getRequestLogById(current)?.agentLoopExitReason).toBe(
|
|
113
|
+
"yield_to_user",
|
|
114
|
+
);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for `buildProviderErrorResponsePayload` — the shared serializer
|
|
3
|
+
* used by `handleProviderError` (daemon) and the wake-path `onEvent` to
|
|
4
|
+
* record provider-rejected LLM calls in `llm_request_logs`.
|
|
5
|
+
*
|
|
6
|
+
* The serializer's job: take an arbitrary thrown `Error`, return a
|
|
7
|
+
* structured `{ error: {...} }` object whose fields are queryable in the
|
|
8
|
+
* LLM inspector and that round-trips cleanly through `JSON.stringify`.
|
|
9
|
+
* The `error` key wrap is load-bearing — it mirrors a successful row's
|
|
10
|
+
* `usage.rawResponse` shape so an inspector consumer can branch on
|
|
11
|
+
* `responsePayload.error` vs the success shape without re-parsing.
|
|
12
|
+
*
|
|
13
|
+
* Coverage:
|
|
14
|
+
* - `ProviderError` with full metadata (provider, statusCode, retryAfterMs).
|
|
15
|
+
* - `ProviderError` without optional metadata.
|
|
16
|
+
* - Non-provider `AssistantError` (carries `code` but not provider fields).
|
|
17
|
+
* - Plain `Error` (degrades to `{name, message}`).
|
|
18
|
+
* - Custom `Error` subclass with overridden `name` is preserved.
|
|
19
|
+
*
|
|
20
|
+
* Each test stringifies and re-parses the payload so the on-disk shape
|
|
21
|
+
* (what eventually lands in the `responsePayload` column) is what we
|
|
22
|
+
* assert on, not the JS object identity.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { describe, expect, test } from "bun:test";
|
|
26
|
+
|
|
27
|
+
import { buildProviderErrorResponsePayload } from "../memory/llm-request-log-store.js";
|
|
28
|
+
import {
|
|
29
|
+
AssistantError,
|
|
30
|
+
ErrorCode,
|
|
31
|
+
ProviderError,
|
|
32
|
+
} from "../util/errors.js";
|
|
33
|
+
|
|
34
|
+
function persisted(err: Error): { error: Record<string, unknown> } {
|
|
35
|
+
// Round-trip through JSON to assert on the actual stored shape, not the
|
|
36
|
+
// in-memory object reference.
|
|
37
|
+
return JSON.parse(
|
|
38
|
+
JSON.stringify(buildProviderErrorResponsePayload(err)),
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe("buildProviderErrorResponsePayload", () => {
|
|
43
|
+
test("ProviderError with statusCode + retryAfterMs serializes every queryable field", () => {
|
|
44
|
+
const err = new ProviderError(
|
|
45
|
+
"Anthropic API error (429): rate limited",
|
|
46
|
+
"anthropic",
|
|
47
|
+
429,
|
|
48
|
+
{ retryAfterMs: 1500 },
|
|
49
|
+
);
|
|
50
|
+
const got = persisted(err);
|
|
51
|
+
expect(got).toEqual({
|
|
52
|
+
error: {
|
|
53
|
+
name: "ProviderError",
|
|
54
|
+
message: "Anthropic API error (429): rate limited",
|
|
55
|
+
code: ErrorCode.PROVIDER_ERROR,
|
|
56
|
+
provider: "anthropic",
|
|
57
|
+
statusCode: 429,
|
|
58
|
+
retryAfterMs: 1500,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("ProviderError without optional metadata omits statusCode + retryAfterMs", () => {
|
|
64
|
+
const err = new ProviderError(
|
|
65
|
+
"Gemini API error: surprise internal state",
|
|
66
|
+
"gemini",
|
|
67
|
+
);
|
|
68
|
+
const got = persisted(err);
|
|
69
|
+
expect(got).toEqual({
|
|
70
|
+
error: {
|
|
71
|
+
name: "ProviderError",
|
|
72
|
+
message: "Gemini API error: surprise internal state",
|
|
73
|
+
code: ErrorCode.PROVIDER_ERROR,
|
|
74
|
+
provider: "gemini",
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
// Explicit assertion: omitted fields aren't present as `null` either —
|
|
78
|
+
// the inspector should be able to test `'statusCode' in error` reliably.
|
|
79
|
+
expect("statusCode" in got.error).toBe(false);
|
|
80
|
+
expect("retryAfterMs" in got.error).toBe(false);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("non-provider AssistantError carries the ErrorCode but no provider fields", () => {
|
|
84
|
+
// Tool errors / permission denials are technically also AssistantErrors;
|
|
85
|
+
// we just want to confirm the generic AssistantError branch produces a
|
|
86
|
+
// sensible row rather than silently degrading to a plain Error shape.
|
|
87
|
+
const err = new AssistantError(
|
|
88
|
+
"internal state corrupted",
|
|
89
|
+
ErrorCode.INTERNAL_ERROR,
|
|
90
|
+
);
|
|
91
|
+
const got = persisted(err);
|
|
92
|
+
expect(got).toEqual({
|
|
93
|
+
error: {
|
|
94
|
+
name: "AssistantError",
|
|
95
|
+
message: "internal state corrupted",
|
|
96
|
+
code: ErrorCode.INTERNAL_ERROR,
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
expect("provider" in got.error).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("plain Error degrades to {name, message} with no code/provider noise", () => {
|
|
103
|
+
const err = new Error("connection reset");
|
|
104
|
+
const got = persisted(err);
|
|
105
|
+
expect(got).toEqual({
|
|
106
|
+
error: {
|
|
107
|
+
name: "Error",
|
|
108
|
+
message: "connection reset",
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
expect("code" in got.error).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("custom Error subclass with overridden name is preserved", () => {
|
|
115
|
+
class TimeoutError extends Error {
|
|
116
|
+
constructor(message: string) {
|
|
117
|
+
super(message);
|
|
118
|
+
this.name = "TimeoutError";
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const got = persisted(new TimeoutError("provider timed out after 60s"));
|
|
122
|
+
expect(got).toEqual({
|
|
123
|
+
error: {
|
|
124
|
+
name: "TimeoutError",
|
|
125
|
+
message: "provider timed out after 60s",
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("ProviderError with statusCode 0 is still recorded (not coerced to undefined)", () => {
|
|
131
|
+
// Defensive: `if (err.statusCode !== undefined)` correctly admits 0.
|
|
132
|
+
// A raw `if (err.statusCode)` would drop it, so the test guards against
|
|
133
|
+
// a regression to truthy-checking.
|
|
134
|
+
const err = new ProviderError("weird provider", "fake", 0);
|
|
135
|
+
const got = persisted(err);
|
|
136
|
+
expect(got.error.statusCode).toBe(0);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
@@ -83,6 +83,7 @@ const SAMPLE_ROW = {
|
|
|
83
83
|
response_payload: '{"bar":2}',
|
|
84
84
|
// ClickHouse emits Int64 as a quoted string under JSONEachRow by default.
|
|
85
85
|
created_at: "1778465138786",
|
|
86
|
+
agent_loop_exit_reason: "no_tool_calls",
|
|
86
87
|
};
|
|
87
88
|
|
|
88
89
|
describe("ClickHouseLlmRequestLogSource", () => {
|
|
@@ -102,6 +103,7 @@ describe("ClickHouseLlmRequestLogSource", () => {
|
|
|
102
103
|
requestPayload: '{"foo":1}',
|
|
103
104
|
responsePayload: '{"bar":2}',
|
|
104
105
|
createdAt: 1778465138786,
|
|
106
|
+
agentLoopExitReason: "no_tool_calls",
|
|
105
107
|
});
|
|
106
108
|
});
|
|
107
109
|
|
|
@@ -3,7 +3,7 @@ import { describe, expect, test } from "bun:test";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
|
|
5
5
|
import { resolveCallSiteConfig } from "../config/llm-resolver.js";
|
|
6
|
-
import { LLMSchema } from "../config/schemas/llm.js";
|
|
6
|
+
import { type LLMCallSite, LLMSchema } from "../config/schemas/llm.js";
|
|
7
7
|
|
|
8
8
|
const fullDefault = {
|
|
9
9
|
provider: "anthropic" as const,
|
|
@@ -369,7 +369,8 @@ describe("resolveCallSiteConfig", () => {
|
|
|
369
369
|
const resolved = resolveCallSiteConfig("mainAgent", llm, {
|
|
370
370
|
overrideProfile: "nonexistent",
|
|
371
371
|
});
|
|
372
|
-
//
|
|
372
|
+
// overrideProfile is set so the shipped default's profile is stripped.
|
|
373
|
+
// The nonexistent overrideProfile also adds nothing. Falls through to default.
|
|
373
374
|
expect(resolved.effort).toBe("max");
|
|
374
375
|
expect(resolved.model).toBe("claude-opus-4-7");
|
|
375
376
|
});
|
|
@@ -536,4 +537,256 @@ describe("resolveCallSiteConfig", () => {
|
|
|
536
537
|
expect(resolved.maxTokens).toBe(65536);
|
|
537
538
|
expect(resolved.contextWindow.maxInputTokens).toBe(1048576);
|
|
538
539
|
});
|
|
540
|
+
|
|
541
|
+
test("call site with no explicit config falls back to CALL_SITE_DEFAULTS", () => {
|
|
542
|
+
const llm = LLMSchema.parse({
|
|
543
|
+
default: fullDefault,
|
|
544
|
+
profiles: {
|
|
545
|
+
"cost-optimized": {
|
|
546
|
+
model: "claude-haiku-4-5-20251001",
|
|
547
|
+
effort: "low",
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
});
|
|
551
|
+
const resolved = resolveCallSiteConfig("memoryExtraction", llm);
|
|
552
|
+
expect(resolved.model).toBe("claude-haiku-4-5-20251001");
|
|
553
|
+
expect(resolved.effort).toBe("low");
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
test("explicit callSites config overrides CALL_SITE_DEFAULTS", () => {
|
|
557
|
+
const llm = LLMSchema.parse({
|
|
558
|
+
default: fullDefault,
|
|
559
|
+
profiles: {
|
|
560
|
+
"cost-optimized": {
|
|
561
|
+
model: "claude-haiku-4-5-20251001",
|
|
562
|
+
effort: "low",
|
|
563
|
+
},
|
|
564
|
+
"quality-optimized": { model: "claude-opus-4-7", effort: "max" },
|
|
565
|
+
},
|
|
566
|
+
callSites: {
|
|
567
|
+
memoryExtraction: { profile: "quality-optimized" },
|
|
568
|
+
},
|
|
569
|
+
});
|
|
570
|
+
const resolved = resolveCallSiteConfig("memoryExtraction", llm);
|
|
571
|
+
expect(resolved.model).toBe("claude-opus-4-7");
|
|
572
|
+
expect(resolved.effort).toBe("max");
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
test("BYOK: disabled managed profile falls back to custom-* user profile", () => {
|
|
576
|
+
const llm = LLMSchema.parse({
|
|
577
|
+
default: {
|
|
578
|
+
...fullDefault,
|
|
579
|
+
provider: "openai",
|
|
580
|
+
model: "gpt-5.5",
|
|
581
|
+
provider_connection: "openai-personal",
|
|
582
|
+
},
|
|
583
|
+
profiles: {
|
|
584
|
+
"cost-optimized": {
|
|
585
|
+
status: "disabled",
|
|
586
|
+
model: "claude-haiku-4-5-20251001",
|
|
587
|
+
provider: "anthropic",
|
|
588
|
+
provider_connection: "anthropic-managed",
|
|
589
|
+
},
|
|
590
|
+
"custom-cost-optimized": {
|
|
591
|
+
source: "user",
|
|
592
|
+
model: "gpt-5.4-nano",
|
|
593
|
+
provider: "openai",
|
|
594
|
+
provider_connection: "openai-personal",
|
|
595
|
+
},
|
|
596
|
+
"custom-balanced": {
|
|
597
|
+
source: "user",
|
|
598
|
+
model: "gpt-5.5",
|
|
599
|
+
provider: "openai",
|
|
600
|
+
provider_connection: "openai-personal",
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
activeProfile: "custom-balanced",
|
|
604
|
+
});
|
|
605
|
+
const resolved = resolveCallSiteConfig("memoryExtraction", llm);
|
|
606
|
+
expect(resolved.provider).toBe("openai");
|
|
607
|
+
expect(resolved.model).toBe("gpt-5.4-nano");
|
|
608
|
+
expect(resolved.provider_connection).toBe("openai-personal");
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
test("BYOK: strips profile when neither managed nor custom-* is available", () => {
|
|
612
|
+
const llm = LLMSchema.parse({
|
|
613
|
+
default: {
|
|
614
|
+
...fullDefault,
|
|
615
|
+
provider: "openai",
|
|
616
|
+
model: "gpt-5.5",
|
|
617
|
+
provider_connection: "openai-personal",
|
|
618
|
+
},
|
|
619
|
+
profiles: {
|
|
620
|
+
"cost-optimized": {
|
|
621
|
+
status: "disabled",
|
|
622
|
+
model: "claude-haiku-4-5-20251001",
|
|
623
|
+
provider: "anthropic",
|
|
624
|
+
provider_connection: "anthropic-managed",
|
|
625
|
+
},
|
|
626
|
+
"custom-balanced": {
|
|
627
|
+
model: "gpt-5.5",
|
|
628
|
+
provider: "openai",
|
|
629
|
+
provider_connection: "openai-personal",
|
|
630
|
+
},
|
|
631
|
+
},
|
|
632
|
+
activeProfile: "custom-balanced",
|
|
633
|
+
});
|
|
634
|
+
const resolved = resolveCallSiteConfig("memoryExtraction", llm);
|
|
635
|
+
expect(resolved.provider).toBe("openai");
|
|
636
|
+
expect(resolved.model).toBe("gpt-5.5");
|
|
637
|
+
expect(resolved.provider_connection).toBe("openai-personal");
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
test("BYOK full-workspace: cost-optimized call sites use custom-cost-optimized, balanced use custom-balanced", () => {
|
|
641
|
+
const byokConfig = LLMSchema.parse({
|
|
642
|
+
default: {
|
|
643
|
+
...fullDefault,
|
|
644
|
+
provider: "openai",
|
|
645
|
+
model: "gpt-5.5",
|
|
646
|
+
provider_connection: "openai-personal",
|
|
647
|
+
},
|
|
648
|
+
profiles: {
|
|
649
|
+
balanced: {
|
|
650
|
+
status: "disabled",
|
|
651
|
+
source: "managed",
|
|
652
|
+
provider: "anthropic",
|
|
653
|
+
model: "claude-sonnet-4-6",
|
|
654
|
+
provider_connection: "anthropic-managed",
|
|
655
|
+
},
|
|
656
|
+
"cost-optimized": {
|
|
657
|
+
status: "disabled",
|
|
658
|
+
source: "managed",
|
|
659
|
+
provider: "anthropic",
|
|
660
|
+
model: "claude-haiku-4-5-20251001",
|
|
661
|
+
provider_connection: "anthropic-managed",
|
|
662
|
+
},
|
|
663
|
+
"quality-optimized": {
|
|
664
|
+
status: "disabled",
|
|
665
|
+
source: "managed",
|
|
666
|
+
provider: "anthropic",
|
|
667
|
+
model: "claude-opus-4-7",
|
|
668
|
+
provider_connection: "anthropic-managed",
|
|
669
|
+
},
|
|
670
|
+
"custom-balanced": {
|
|
671
|
+
source: "user",
|
|
672
|
+
provider: "openai",
|
|
673
|
+
model: "gpt-5.5",
|
|
674
|
+
provider_connection: "openai-personal",
|
|
675
|
+
},
|
|
676
|
+
"custom-cost-optimized": {
|
|
677
|
+
source: "user",
|
|
678
|
+
provider: "openai",
|
|
679
|
+
model: "gpt-5.4-nano",
|
|
680
|
+
provider_connection: "openai-personal",
|
|
681
|
+
},
|
|
682
|
+
"custom-quality-optimized": {
|
|
683
|
+
source: "user",
|
|
684
|
+
provider: "openai",
|
|
685
|
+
model: "gpt-5.5-pro",
|
|
686
|
+
provider_connection: "openai-personal",
|
|
687
|
+
},
|
|
688
|
+
},
|
|
689
|
+
activeProfile: "custom-balanced",
|
|
690
|
+
});
|
|
691
|
+
|
|
692
|
+
const callSites: LLMCallSite[] = [
|
|
693
|
+
"mainAgent", "subagentSpawn", "heartbeatAgent", "filingAgent",
|
|
694
|
+
"compactionAgent", "analyzeConversation", "callAgent",
|
|
695
|
+
"memoryExtraction", "memoryConsolidation", "memoryRetrieval",
|
|
696
|
+
"memoryRouter", "recall", "conversationSummarization",
|
|
697
|
+
"commitMessage", "conversationStarters", "replySuggestion",
|
|
698
|
+
"conversationTitle", "identityIntro", "emptyStateGreeting",
|
|
699
|
+
"notificationDecision", "interactionClassifier", "inference",
|
|
700
|
+
];
|
|
701
|
+
|
|
702
|
+
for (const cs of callSites) {
|
|
703
|
+
const resolved = resolveCallSiteConfig(cs, byokConfig);
|
|
704
|
+
expect(resolved.provider_connection).not.toBe("anthropic-managed");
|
|
705
|
+
expect(resolved.provider).toBe("openai");
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// Cost-optimized call sites should use the user's nano model
|
|
709
|
+
const costSite = resolveCallSiteConfig("heartbeatAgent", byokConfig);
|
|
710
|
+
expect(costSite.model).toBe("gpt-5.4-nano");
|
|
711
|
+
|
|
712
|
+
// Balanced call sites should use the user's balanced model
|
|
713
|
+
const balancedSite = resolveCallSiteConfig("mainAgent", byokConfig);
|
|
714
|
+
expect(balancedSite.model).toBe("gpt-5.5");
|
|
715
|
+
});
|
|
716
|
+
|
|
717
|
+
test("BYOK: tuning overrides from defaults apply on top of custom-* fallback profile", () => {
|
|
718
|
+
const byokConfig = LLMSchema.parse({
|
|
719
|
+
default: {
|
|
720
|
+
...fullDefault,
|
|
721
|
+
provider: "openai",
|
|
722
|
+
model: "gpt-5.5",
|
|
723
|
+
provider_connection: "openai-personal",
|
|
724
|
+
},
|
|
725
|
+
profiles: {
|
|
726
|
+
"cost-optimized": {
|
|
727
|
+
status: "disabled",
|
|
728
|
+
provider: "anthropic",
|
|
729
|
+
model: "claude-haiku-4-5-20251001",
|
|
730
|
+
provider_connection: "anthropic-managed",
|
|
731
|
+
},
|
|
732
|
+
"custom-cost-optimized": {
|
|
733
|
+
source: "user",
|
|
734
|
+
provider: "openai",
|
|
735
|
+
model: "gpt-5.4-nano",
|
|
736
|
+
provider_connection: "openai-personal",
|
|
737
|
+
},
|
|
738
|
+
"custom-balanced": {
|
|
739
|
+
provider: "openai",
|
|
740
|
+
model: "gpt-5.5",
|
|
741
|
+
provider_connection: "openai-personal",
|
|
742
|
+
},
|
|
743
|
+
},
|
|
744
|
+
activeProfile: "custom-balanced",
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
const resolved = resolveCallSiteConfig("commitMessage", byokConfig);
|
|
748
|
+
expect(resolved.provider).toBe("openai");
|
|
749
|
+
expect(resolved.model).toBe("gpt-5.4-nano");
|
|
750
|
+
expect(resolved.maxTokens).toBe(120);
|
|
751
|
+
expect(resolved.effort).toBe("low");
|
|
752
|
+
expect(resolved.thinking.enabled).toBe(false);
|
|
753
|
+
});
|
|
754
|
+
|
|
755
|
+
test("overrideProfile wins over CALL_SITE_DEFAULTS profile for non-main call sites", () => {
|
|
756
|
+
const llm = LLMSchema.parse({
|
|
757
|
+
default: fullDefault,
|
|
758
|
+
profiles: {
|
|
759
|
+
"cost-optimized": { model: "claude-haiku-4-5-20251001", effort: "low" },
|
|
760
|
+
"quality-optimized": { model: "claude-opus-4-7", effort: "max" },
|
|
761
|
+
},
|
|
762
|
+
});
|
|
763
|
+
const resolved = resolveCallSiteConfig("inference", llm, {
|
|
764
|
+
overrideProfile: "quality-optimized",
|
|
765
|
+
});
|
|
766
|
+
expect(resolved.model).toBe("claude-opus-4-7");
|
|
767
|
+
expect(resolved.effort).toBe("max");
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
test("profile with provider but no provider_connection inherits stale default connection (JARVIS-861)", () => {
|
|
771
|
+
// This test documents the merge behavior that causes JARVIS-861: a profile
|
|
772
|
+
// overrides `provider` but not `provider_connection`, so the deep merge
|
|
773
|
+
// inherits a stale connection from the default layer. The fix is in the
|
|
774
|
+
// dispatch layer (connection-resolution auto-resolves the mismatch).
|
|
775
|
+
const llm = LLMSchema.parse({
|
|
776
|
+
default: {
|
|
777
|
+
...fullDefault,
|
|
778
|
+
provider_connection: "anthropic-managed",
|
|
779
|
+
},
|
|
780
|
+
profiles: {
|
|
781
|
+
fireworks: { provider: "fireworks", model: "accounts/fireworks/models/kimi-k2p5" },
|
|
782
|
+
},
|
|
783
|
+
activeProfile: "fireworks",
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
const resolved = resolveCallSiteConfig("mainAgent", llm);
|
|
787
|
+
|
|
788
|
+
expect(resolved.provider).toBe("fireworks");
|
|
789
|
+
// The merge inherits the stale connection — the dispatch layer handles this.
|
|
790
|
+
expect(resolved.provider_connection).toBe("anthropic-managed");
|
|
791
|
+
});
|
|
539
792
|
});
|
|
@@ -79,6 +79,16 @@ mock.module("../memory/embedding-backend.js", () => ({
|
|
|
79
79
|
clearEmbeddingBackendCache: () => {},
|
|
80
80
|
}));
|
|
81
81
|
|
|
82
|
+
// The replace-profile handler auto-derives `provider_connection` from the
|
|
83
|
+
// first active connection matching the requested provider when the body
|
|
84
|
+
// omits it. That path queries the `provider_connections` table, which the
|
|
85
|
+
// test doesn't migrate — stub it out so the guard logic stays the focus.
|
|
86
|
+
mock.module("../providers/inference/connections.js", () => ({
|
|
87
|
+
listConnections: () => [],
|
|
88
|
+
createConnection: () => ({ ok: false, error: { code: "already_exists" } }),
|
|
89
|
+
PROVIDERS_REQUIRING_BASE_URL_AND_MODELS: new Set(["openai-compatible"]),
|
|
90
|
+
}));
|
|
91
|
+
|
|
82
92
|
import { ROUTES } from "../runtime/routes/conversation-query-routes.js";
|
|
83
93
|
import { BadRequestError } from "../runtime/routes/errors.js";
|
|
84
94
|
|