@vellumai/assistant 0.10.7-dev.202607100313.fffdfea → 0.10.7-dev.202607100720.fba456e

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 (99) hide show
  1. package/openapi.yaml +846 -27
  2. package/package.json +1 -1
  3. package/src/__tests__/config-watcher-skill-reseed.test.ts +1 -0
  4. package/src/__tests__/config-watcher.test.ts +22 -4
  5. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  6. package/src/__tests__/plugin-import-boundary-guard.test.ts +6 -9
  7. package/src/__tests__/plugin-import-boundary-reverse-guard.test.ts +0 -2
  8. package/src/cli/commands/__tests__/inference-models.test.ts +86 -0
  9. package/src/cli/commands/__tests__/inference-profiles.test.ts +254 -0
  10. package/src/cli/commands/__tests__/inference-providers.test.ts +171 -0
  11. package/src/cli/commands/__tests__/schedules.test.ts +133 -1
  12. package/src/cli/commands/__tests__/usage.test.ts +131 -0
  13. package/src/cli/commands/inference-callsites.ts +158 -0
  14. package/src/cli/commands/inference-models.ts +84 -0
  15. package/src/cli/commands/inference-profiles.ts +371 -0
  16. package/src/cli/commands/inference-providers-default.ts +89 -0
  17. package/src/cli/commands/inference-providers.ts +87 -16
  18. package/src/cli/commands/inference.ts +6 -0
  19. package/src/cli/commands/memory/nodes.ts +90 -50
  20. package/src/cli/commands/schedules.ts +17 -2
  21. package/src/cli/commands/usage.ts +59 -21
  22. package/src/cli/lib/__tests__/cli-output.test.ts +20 -0
  23. package/src/cli/lib/cli-output.ts +55 -0
  24. package/src/config/__tests__/loader-last-known-good.test.ts +229 -0
  25. package/src/config/__tests__/loader-sparse-array-cleanup.test.ts +166 -0
  26. package/src/config/default-profile-catalog.ts +28 -0
  27. package/src/config/loader.ts +276 -46
  28. package/src/daemon/config-watcher.ts +65 -20
  29. package/src/daemon/message-types/sync.ts +1 -0
  30. package/src/oauth/byo-connection.test.ts +1 -1
  31. package/src/oauth/connection-resolver.test.ts +140 -0
  32. package/src/oauth/connection-resolver.ts +48 -12
  33. package/src/plugins/defaults/memory/anisotropy.ts +1 -1
  34. package/src/plugins/defaults/memory/config.ts +1 -1
  35. package/src/plugins/defaults/memory/context-search/agent-protocol.ts +1 -1
  36. package/src/plugins/defaults/memory/context-search/search.ts +1 -1
  37. package/src/plugins/defaults/memory/graph/bootstrap.ts +1 -1
  38. package/src/plugins/defaults/memory/graph/consolidation.ts +1 -1
  39. package/src/plugins/defaults/memory/graph/conversation-graph-memory.ts +11 -1
  40. package/src/plugins/defaults/memory/graph/extraction.ts +1 -1
  41. package/src/plugins/defaults/memory/graph/narrative.ts +1 -1
  42. package/src/plugins/defaults/memory/graph/pattern-scan.ts +1 -1
  43. package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -1
  44. package/src/plugins/defaults/memory/graph/tools.ts +1 -1
  45. package/src/plugins/defaults/memory/graph-topology/build-memory-graph.ts +43 -16
  46. package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +1 -1
  47. package/src/plugins/defaults/memory/host-utils.ts +80 -0
  48. package/src/plugins/defaults/memory/identity-context.ts +1 -1
  49. package/src/plugins/defaults/memory/injectors.ts +1 -1
  50. package/src/plugins/defaults/memory/jobs/embed-concept-page.ts +2 -2
  51. package/src/plugins/defaults/memory/jobs-worker.ts +2 -9
  52. package/src/plugins/defaults/memory/memory-retrospective-state.ts +1 -1
  53. package/src/plugins/defaults/memory/paths.ts +48 -0
  54. package/src/plugins/defaults/memory/pkb/autoinject.ts +1 -1
  55. package/src/plugins/defaults/memory/pkb/context.ts +2 -2
  56. package/src/plugins/defaults/memory/pkb/types.ts +1 -1
  57. package/src/plugins/defaults/memory/pkb-schedule.ts +2 -2
  58. package/src/plugins/defaults/memory/rerank-local.ts +2 -3
  59. package/src/plugins/defaults/memory/routes/memory-eval-routes.ts +1 -1
  60. package/src/plugins/defaults/memory/routes/memory-item-routes.test.ts +149 -1
  61. package/src/plugins/defaults/memory/routes/memory-item-routes.ts +144 -1
  62. package/src/plugins/defaults/memory/routes/memory-v2-routes.ts +1 -1
  63. package/src/plugins/defaults/memory/startup.ts +1 -1
  64. package/src/plugins/defaults/memory/v2/backfill-jobs.ts +1 -1
  65. package/src/plugins/defaults/memory/v2/consolidation-job.ts +2 -2
  66. package/src/plugins/defaults/memory/v2/injection.ts +1 -1
  67. package/src/plugins/defaults/memory/v2/memory-v2-startup.ts +1 -1
  68. package/src/plugins/defaults/memory/v2/prompts/consolidation.ts +1 -1
  69. package/src/plugins/defaults/memory/v2/qdrant.ts +1 -1
  70. package/src/plugins/defaults/memory/v2/reranker.ts +1 -1
  71. package/src/plugins/defaults/memory/v2/static-context.ts +1 -1
  72. package/src/plugins/defaults/memory/v2/sweep-job.ts +1 -1
  73. package/src/plugins/defaults/memory/v3/candidate-match.ts +1 -1
  74. package/src/plugins/defaults/memory/v3/maintain-job.ts +1 -1
  75. package/src/plugins/defaults/memory/v3/page-content.ts +1 -1
  76. package/src/plugins/defaults/memory/v3/pool-select.ts +1 -2
  77. package/src/plugins/defaults/memory/v3/selection-log-store.ts +1 -1
  78. package/src/plugins/defaults/memory/v3/shadow-plugin.ts +2 -5
  79. package/src/plugins/defaults/memory/worker.ts +1 -1
  80. package/src/plugins/defaults/workspace/injectors.ts +17 -15
  81. package/src/plugins/types.ts +9 -14
  82. package/src/providers/inference/connection-availability.ts +193 -0
  83. package/src/runtime/routes/__tests__/inference-callsites-routes.test.ts +54 -0
  84. package/src/runtime/routes/__tests__/inference-models-routes.test.ts +54 -0
  85. package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +405 -0
  86. package/src/runtime/routes/__tests__/inference-routes-registration.test.ts +42 -0
  87. package/src/runtime/routes/__tests__/theme-routes.test.ts +66 -0
  88. package/src/runtime/routes/conversation-query-routes.ts +5 -3
  89. package/src/runtime/routes/default-provider-routes.ts +5 -152
  90. package/src/runtime/routes/index.ts +8 -0
  91. package/src/runtime/routes/inference-callsites-routes.ts +211 -0
  92. package/src/runtime/routes/inference-models-routes.ts +104 -0
  93. package/src/runtime/routes/inference-profiles-routes.ts +717 -0
  94. package/src/runtime/routes/theme-routes.ts +36 -0
  95. package/src/runtime/sync/resource-sync-events.ts +4 -0
  96. package/src/theme/workspace-theme.test.ts +269 -0
  97. package/src/theme/workspace-theme.ts +304 -0
  98. package/src/watcher/__tests__/engine-auth-notify.test.ts +316 -0
  99. package/src/watcher/engine.ts +87 -2
@@ -0,0 +1,371 @@
1
+ /**
2
+ * `assistant inference profiles` CLI namespace.
3
+ *
4
+ * profiles list — effective profile catalog (managed + user)
5
+ * profiles get <name> — a single effective profile
6
+ * profiles create <name> ... — create a validated custom profile
7
+ * profiles update <name> ... — partial update of a custom profile
8
+ * profiles delete <name> — delete a custom profile (managed protected)
9
+ * profiles active [name] — read or set the active (chat) profile
10
+ *
11
+ * All subcommands delegate to the daemon via IPC. Provider/model/connection
12
+ * validation is enforced by the daemon (`inference_profiles_*` routes); the
13
+ * CLI only shape-parses flags.
14
+ */
15
+
16
+ import type { Command } from "commander";
17
+
18
+ import { cliIpcCall } from "../../ipc/cli-client.js";
19
+ import { renderTable, writeCliError, writeLine } from "../lib/cli-output.js";
20
+
21
+ interface ProfileSummary {
22
+ name: string;
23
+ label: string | null;
24
+ provider: string | null;
25
+ model: string | null;
26
+ status: "active" | "disabled";
27
+ source: "managed" | "user";
28
+ provider_connection?: string;
29
+ availability: { status: string; message?: string } | null;
30
+ }
31
+
32
+ interface ProfileWriteResult {
33
+ ok: true;
34
+ name: string;
35
+ entry: Record<string, unknown>;
36
+ warnings: string[];
37
+ }
38
+
39
+ type WriteFlags = {
40
+ provider?: string;
41
+ model?: string;
42
+ connection?: string;
43
+ label?: string;
44
+ effort?: string;
45
+ maxTokens?: string;
46
+ temperature?: string;
47
+ thinking?: string;
48
+ description?: string;
49
+ allowUnlisted?: boolean;
50
+ json?: boolean;
51
+ };
52
+
53
+ /**
54
+ * Parse the shared write flags into an IPC body. Returns an error string when
55
+ * a numeric/enum flag is malformed. Only keys the user supplied are included.
56
+ */
57
+ function buildWriteBody(
58
+ opts: WriteFlags,
59
+ ): { ok: true; body: Record<string, unknown> } | { ok: false; error: string } {
60
+ const body: Record<string, unknown> = {};
61
+ if (opts.provider !== undefined) {
62
+ body.provider = opts.provider;
63
+ }
64
+ if (opts.model !== undefined) {
65
+ body.model = opts.model;
66
+ }
67
+ if (opts.connection !== undefined) {
68
+ body.connection = opts.connection;
69
+ }
70
+ if (opts.label !== undefined) {
71
+ body.label = opts.label;
72
+ }
73
+ if (opts.effort !== undefined) {
74
+ body.effort = opts.effort;
75
+ }
76
+ if (opts.description !== undefined) {
77
+ body.description = opts.description;
78
+ }
79
+ if (opts.allowUnlisted) {
80
+ body.allowUnlisted = true;
81
+ }
82
+
83
+ if (opts.maxTokens !== undefined) {
84
+ if (!/^\d+$/.test(opts.maxTokens.trim())) {
85
+ return { ok: false, error: "--max-tokens must be a positive integer." };
86
+ }
87
+ body.maxTokens = Number(opts.maxTokens.trim());
88
+ }
89
+ if (opts.temperature !== undefined) {
90
+ const value = Number(opts.temperature.trim());
91
+ if (!Number.isFinite(value)) {
92
+ return { ok: false, error: "--temperature must be a number." };
93
+ }
94
+ body.temperature = value;
95
+ }
96
+ if (opts.thinking !== undefined) {
97
+ const normalized = opts.thinking.trim().toLowerCase();
98
+ if (normalized !== "on" && normalized !== "off") {
99
+ return { ok: false, error: "--thinking must be 'on' or 'off'." };
100
+ }
101
+ body.thinking = normalized === "on";
102
+ }
103
+
104
+ return { ok: true, body };
105
+ }
106
+
107
+ function printWriteResult(
108
+ verb: string,
109
+ result: ProfileWriteResult,
110
+ json?: boolean,
111
+ ): void {
112
+ if (json) {
113
+ process.stdout.write(JSON.stringify(result) + "\n");
114
+ return;
115
+ }
116
+ for (const warning of result.warnings) {
117
+ writeLine(`warning: ${warning}`);
118
+ }
119
+ writeLine(`profile ${result.name} ${verb}`);
120
+ writeLine(
121
+ `Verify it works: assistant inference send --profile ${result.name} "Reply with OK"`,
122
+ );
123
+ }
124
+
125
+ function addWriteFlags(cmd: Command): Command {
126
+ return cmd
127
+ .option("--provider <p>", "LLM provider (e.g. anthropic, openai)")
128
+ .option("--model <id>", "Model id (see 'assistant inference models list')")
129
+ .option("--connection <name>", "Provider connection name to use")
130
+ .option("--label <text>", "Human-readable label")
131
+ .option(
132
+ "--effort <tier>",
133
+ "Reasoning effort (none|low|medium|high|xhigh|max)",
134
+ )
135
+ .option("--max-tokens <n>", "Max response tokens")
136
+ .option("--temperature <x>", "Sampling temperature")
137
+ .option("--thinking <on|off>", "Enable or disable thinking")
138
+ .option("--description <text>", "Profile description")
139
+ .option("--allow-unlisted", "Allow a model not in the catalog (warns)")
140
+ .option("--json", "Output as machine-readable JSON");
141
+ }
142
+
143
+ export function attachProfilesSubcommand(inference: Command): void {
144
+ const profiles = inference
145
+ .command("profiles")
146
+ .description("Manage inference profiles (named model configurations)");
147
+
148
+ profiles.addHelpText(
149
+ "after",
150
+ `
151
+ Profiles are named model configurations. Managed defaults (balanced,
152
+ quality-optimized, cost-optimized) are read-only; create your own to
153
+ customize provider, model, and tuning.
154
+
155
+ Examples:
156
+ $ assistant inference profiles list
157
+ $ assistant inference profiles create my-fast --provider anthropic \\
158
+ --model claude-haiku-4-5 --connection anthropic-personal --effort low
159
+ $ assistant inference profiles update my-fast --effort high
160
+ $ assistant inference profiles active my-fast
161
+ $ assistant inference profiles delete my-fast`,
162
+ );
163
+
164
+ // ── list ────────────────────────────────────────────────────────────
165
+ profiles
166
+ .command("list")
167
+ .description("List the effective profile catalog")
168
+ .option("--json", "Output as machine-readable JSON")
169
+ .action(async (opts: { json?: boolean }) => {
170
+ const ipcResult = await cliIpcCall<{ profiles: ProfileSummary[] }>(
171
+ "inference_profiles_list",
172
+ {},
173
+ );
174
+ if (!ipcResult.ok) {
175
+ writeCliError(ipcResult.error ?? "Unknown error", opts.json);
176
+ return;
177
+ }
178
+ const rows = ipcResult.result!.profiles;
179
+ if (opts.json) {
180
+ process.stdout.write(
181
+ JSON.stringify({ ok: true, profiles: rows }) + "\n",
182
+ );
183
+ return;
184
+ }
185
+ if (rows.length === 0) {
186
+ writeLine("No profiles found.");
187
+ return;
188
+ }
189
+ renderTable(
190
+ ["NAME", "LABEL", "PROVIDER", "MODEL", "STATUS", "SOURCE", "AVAIL"],
191
+ rows.map((p) => [
192
+ p.name,
193
+ p.label ?? "-",
194
+ p.provider ?? "-",
195
+ p.model ?? "-",
196
+ p.status,
197
+ p.source,
198
+ p.availability ? p.availability.status : "-",
199
+ ]),
200
+ );
201
+ });
202
+
203
+ // ── get ─────────────────────────────────────────────────────────────
204
+ profiles
205
+ .command("get <name>")
206
+ .description("Show a single effective profile")
207
+ .option("--json", "Output as machine-readable JSON")
208
+ .action(async (name: string, opts: { json?: boolean }) => {
209
+ const ipcResult = await cliIpcCall<{
210
+ name: string;
211
+ entry: Record<string, unknown>;
212
+ availability: { status: string; message?: string } | null;
213
+ }>("inference_profiles_get", { pathParams: { name } });
214
+ if (!ipcResult.ok) {
215
+ writeCliError(ipcResult.error ?? "Unknown error", opts.json);
216
+ return;
217
+ }
218
+ const result = ipcResult.result!;
219
+ if (opts.json) {
220
+ process.stdout.write(JSON.stringify({ ok: true, ...result }) + "\n");
221
+ return;
222
+ }
223
+ writeLine(`profile: ${result.name}`);
224
+ for (const [key, value] of Object.entries(result.entry)) {
225
+ writeLine(` ${key}: ${JSON.stringify(value)}`);
226
+ }
227
+ if (result.availability) {
228
+ writeLine(` availability: ${result.availability.status}`);
229
+ if (result.availability.message) {
230
+ writeLine(` ${result.availability.message}`);
231
+ }
232
+ }
233
+ });
234
+
235
+ // ── create ──────────────────────────────────────────────────────────
236
+ addWriteFlags(
237
+ profiles
238
+ .command("create <name>")
239
+ .description("Create a validated custom profile"),
240
+ ).action(async (name: string, opts: WriteFlags) => {
241
+ if (!opts.provider) {
242
+ writeCliError("--provider is required.", opts.json);
243
+ return;
244
+ }
245
+ if (!opts.model) {
246
+ writeCliError("--model is required.", opts.json);
247
+ return;
248
+ }
249
+ const built = buildWriteBody(opts);
250
+ if (!built.ok) {
251
+ writeCliError(built.error, opts.json);
252
+ return;
253
+ }
254
+ const ipcResult = await cliIpcCall<ProfileWriteResult>(
255
+ "inference_profiles_create",
256
+ { body: { ...built.body, name } },
257
+ );
258
+ if (!ipcResult.ok) {
259
+ writeCliError(ipcResult.error ?? "Unknown error", opts.json);
260
+ return;
261
+ }
262
+ printWriteResult("created", ipcResult.result!, opts.json);
263
+ });
264
+
265
+ // ── update ──────────────────────────────────────────────────────────
266
+ addWriteFlags(
267
+ profiles
268
+ .command("update <name>")
269
+ .description("Partially update a custom profile"),
270
+ ).action(async (name: string, opts: WriteFlags) => {
271
+ const built = buildWriteBody(opts);
272
+ if (!built.ok) {
273
+ writeCliError(built.error, opts.json);
274
+ return;
275
+ }
276
+ if (Object.keys(built.body).length === 0) {
277
+ writeCliError(
278
+ "Nothing to update — pass at least one field flag.",
279
+ opts.json,
280
+ );
281
+ return;
282
+ }
283
+ const ipcResult = await cliIpcCall<ProfileWriteResult>(
284
+ "inference_profiles_update",
285
+ { pathParams: { name }, body: built.body },
286
+ );
287
+ if (!ipcResult.ok) {
288
+ writeCliError(ipcResult.error ?? "Unknown error", opts.json);
289
+ return;
290
+ }
291
+ printWriteResult("updated", ipcResult.result!, opts.json);
292
+ });
293
+
294
+ // ── delete ──────────────────────────────────────────────────────────
295
+ profiles
296
+ .command("delete <name>")
297
+ .description("Delete a custom profile")
298
+ .option("--json", "Output as machine-readable JSON")
299
+ .action(async (name: string, opts: { json?: boolean }) => {
300
+ const ipcResult = await cliIpcCall<{ ok: true; name: string }>(
301
+ "inference_profiles_delete",
302
+ { pathParams: { name } },
303
+ );
304
+ if (!ipcResult.ok) {
305
+ writeCliError(ipcResult.error ?? "Unknown error", opts.json);
306
+ return;
307
+ }
308
+ if (opts.json) {
309
+ process.stdout.write(
310
+ JSON.stringify({ ok: true, name: ipcResult.result!.name }) + "\n",
311
+ );
312
+ return;
313
+ }
314
+ writeLine(`profile ${name} deleted`);
315
+ });
316
+
317
+ // ── active ──────────────────────────────────────────────────────────
318
+ profiles
319
+ .command("active [name]")
320
+ .description("Read or set the active (chat) profile")
321
+ .option("--json", "Output as machine-readable JSON")
322
+ .addHelpText(
323
+ "after",
324
+ `
325
+ With no argument, prints the active profile. With a name, sets it — the
326
+ same deep-merge write the model picker performs.
327
+
328
+ Examples:
329
+ $ assistant inference profiles active
330
+ $ assistant inference profiles active balanced`,
331
+ )
332
+ .action(async (name: string | undefined, opts: { json?: boolean }) => {
333
+ if (name === undefined) {
334
+ const ipcResult = await cliIpcCall<{
335
+ llm?: { activeProfile?: string };
336
+ }>("config_get");
337
+ if (!ipcResult.ok) {
338
+ writeCliError(ipcResult.error ?? "Unknown error", opts.json);
339
+ return;
340
+ }
341
+ const active = ipcResult.result!.llm?.activeProfile ?? null;
342
+ if (opts.json) {
343
+ process.stdout.write(
344
+ JSON.stringify({ ok: true, activeProfile: active }) + "\n",
345
+ );
346
+ return;
347
+ }
348
+ writeLine(
349
+ active ? `active profile: ${active}` : "no active profile set",
350
+ );
351
+ return;
352
+ }
353
+
354
+ const ipcResult = await cliIpcCall<{ ok: true; activeProfile: string }>(
355
+ "inference_profiles_set_active",
356
+ { body: { name } },
357
+ );
358
+ if (!ipcResult.ok) {
359
+ writeCliError(ipcResult.error ?? "Unknown error", opts.json);
360
+ return;
361
+ }
362
+ const activeProfile = ipcResult.result!.activeProfile;
363
+ if (opts.json) {
364
+ process.stdout.write(
365
+ JSON.stringify({ ok: true, activeProfile }) + "\n",
366
+ );
367
+ return;
368
+ }
369
+ writeLine(`active profile set to ${activeProfile}`);
370
+ });
371
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * `assistant inference providers default [name]` — read or set the workspace
3
+ * default provider (`llm.defaultProvider`), wrapping the daemon's
4
+ * `llm_default_provider_get` / `llm_default_provider_put` routes. The read
5
+ * form prints the resolved connection and its availability status.
6
+ */
7
+
8
+ import type { Command } from "commander";
9
+
10
+ import { cliIpcCall } from "../../ipc/cli-client.js";
11
+ import { writeCliError, writeLine } from "../lib/cli-output.js";
12
+
13
+ interface DefaultProviderStatus {
14
+ provider: string | null;
15
+ connectionName?: string;
16
+ resolvedConnectionName: string | null;
17
+ availability: { status: string; message?: string };
18
+ }
19
+
20
+ function printStatus(status: DefaultProviderStatus, json?: boolean): void {
21
+ if (json) {
22
+ process.stdout.write(JSON.stringify({ ok: true, ...status }) + "\n");
23
+ return;
24
+ }
25
+ if (!status.provider) {
26
+ writeLine("no default provider configured");
27
+ } else {
28
+ writeLine(`default provider: ${status.provider}`);
29
+ if (status.resolvedConnectionName) {
30
+ writeLine(` connection: ${status.resolvedConnectionName}`);
31
+ }
32
+ }
33
+ writeLine(` availability: ${status.availability.status}`);
34
+ if (status.availability.message) {
35
+ writeLine(` ${status.availability.message}`);
36
+ }
37
+ }
38
+
39
+ export function attachDefaultProviderSubcommand(providers: Command): void {
40
+ providers
41
+ .command("default [name]")
42
+ .description("Read or set the default provider (prints availability)")
43
+ .option("--connection <name>", "Pin a specific connection when setting")
44
+ .option("--json", "Output as machine-readable JSON")
45
+ .addHelpText(
46
+ "after",
47
+ `
48
+ With no argument, prints the default provider and whether it is usable.
49
+ With a provider name, sets it (optionally pinning a connection).
50
+
51
+ Examples:
52
+ $ assistant inference providers default
53
+ $ assistant inference providers default anthropic
54
+ $ assistant inference providers default anthropic --connection anthropic-personal`,
55
+ )
56
+ .action(
57
+ async (
58
+ name: string | undefined,
59
+ opts: { connection?: string; json?: boolean },
60
+ ) => {
61
+ if (name === undefined) {
62
+ const ipcResult = await cliIpcCall<DefaultProviderStatus>(
63
+ "llm_default_provider_get",
64
+ {},
65
+ );
66
+ if (!ipcResult.ok) {
67
+ writeCliError(ipcResult.error ?? "Unknown error", opts.json);
68
+ return;
69
+ }
70
+ printStatus(ipcResult.result!, opts.json);
71
+ return;
72
+ }
73
+
74
+ const body: Record<string, unknown> = { provider: name };
75
+ if (opts.connection !== undefined) {
76
+ body.connectionName = opts.connection;
77
+ }
78
+ const ipcResult = await cliIpcCall<DefaultProviderStatus>(
79
+ "llm_default_provider_put",
80
+ { body },
81
+ );
82
+ if (!ipcResult.ok) {
83
+ writeCliError(ipcResult.error ?? "Unknown error", opts.json);
84
+ return;
85
+ }
86
+ printStatus(ipcResult.result!, opts.json);
87
+ },
88
+ );
89
+ }
@@ -18,7 +18,8 @@ import type { Command } from "commander";
18
18
 
19
19
  import { cliIpcCall } from "../../ipc/cli-client.js";
20
20
  import type { OAuth2Config } from "../../security/oauth2.js";
21
- import { log } from "../logger.js";
21
+ import { writeCliError } from "../lib/cli-output.js";
22
+ import { attachDefaultProviderSubcommand } from "./inference-providers-default.js";
22
23
 
23
24
  // ---------------------------------------------------------------------------
24
25
  // Response types
@@ -159,32 +160,55 @@ function buildAuthInput(
159
160
  credential?: string,
160
161
  ): Record<string, unknown> | string {
161
162
  if (authType === "api_key") {
162
- if (!credential) return "--credential is required when --auth api_key";
163
+ if (!credential) {
164
+ return "--credential is required when --auth api_key";
165
+ }
163
166
  return { type: "api_key", credential };
164
167
  }
165
168
  if (authType === "platform") {
166
- if (credential) return "--credential is not accepted with --auth platform";
169
+ if (credential) {
170
+ return "--credential is not accepted with --auth platform";
171
+ }
167
172
  return { type: "platform" };
168
173
  }
169
174
  if (authType === "none") {
170
- if (credential) return "--credential is not accepted with --auth none";
175
+ if (credential) {
176
+ return "--credential is not accepted with --auth none";
177
+ }
171
178
  return { type: "none" };
172
179
  }
173
180
  if (authType === "oauth_subscription") {
174
- if (!credential)
181
+ if (!credential) {
175
182
  return "--credential is required when --auth oauth_subscription";
183
+ }
176
184
  return { type: "oauth_subscription", credential };
177
185
  }
178
186
  return `Unknown auth type "${authType}". Use: api_key, platform, none, oauth_subscription`;
179
187
  }
180
188
 
181
- function writeCliError(msg: string, json?: boolean): void {
182
- if (json) {
183
- process.stdout.write(JSON.stringify({ ok: false, error: msg }) + "\n");
184
- } else {
185
- log.error(msg);
189
+ /** Commander collector for a repeatable option (e.g. `--model` multiple times). */
190
+ function collectRepeatable(value: string, previous: string[]): string[] {
191
+ return [...previous, value];
192
+ }
193
+
194
+ /**
195
+ * Build the openai-compatible custom-provider fields (`base_url`, `models`)
196
+ * from CLI flags, forwarded to the connection route under its exact field
197
+ * names. The daemon stays authoritative on whether they are required/allowed;
198
+ * the CLI only shape-forwards what the user passed.
199
+ */
200
+ function buildCustomProviderFields(opts: {
201
+ baseUrl?: string;
202
+ model?: string[];
203
+ }): Record<string, unknown> {
204
+ const fields: Record<string, unknown> = {};
205
+ if (opts.baseUrl !== undefined) {
206
+ fields.base_url = opts.baseUrl;
207
+ }
208
+ if (opts.model !== undefined && opts.model.length > 0) {
209
+ fields.models = opts.model.map((id) => ({ id }));
186
210
  }
187
- process.exitCode = 1;
211
+ return fields;
188
212
  }
189
213
 
190
214
  // ---------------------------------------------------------------------------
@@ -204,6 +228,16 @@ function attachCreateSubcommand(connections: Command): void {
204
228
  "--credential <vault-key>",
205
229
  "Vault credential name (required for --auth api_key)",
206
230
  )
231
+ .option(
232
+ "--base-url <url>",
233
+ "Endpoint base URL (required for --provider openai-compatible)",
234
+ )
235
+ .option(
236
+ "--model <id>",
237
+ "Model id offered by this connection (repeatable; required for openai-compatible)",
238
+ collectRepeatable,
239
+ [] as string[],
240
+ )
207
241
  .option("--json", "Output as JSON")
208
242
  .action(
209
243
  async (
@@ -212,6 +246,8 @@ function attachCreateSubcommand(connections: Command): void {
212
246
  provider: string;
213
247
  auth: string;
214
248
  credential?: string;
249
+ baseUrl?: string;
250
+ model?: string[];
215
251
  json?: boolean;
216
252
  },
217
253
  ) => {
@@ -221,6 +257,14 @@ function attachCreateSubcommand(connections: Command): void {
221
257
  return;
222
258
  }
223
259
 
260
+ if (opts.provider === "openai-compatible" && !opts.baseUrl) {
261
+ writeCliError(
262
+ "--base-url is required when --provider openai-compatible",
263
+ opts.json,
264
+ );
265
+ return;
266
+ }
267
+
224
268
  const ipcResult = await cliIpcCall<ProviderConnection>(
225
269
  "inference_provider_connections_create",
226
270
  {
@@ -228,6 +272,7 @@ function attachCreateSubcommand(connections: Command): void {
228
272
  name,
229
273
  provider: opts.provider,
230
274
  auth: authInput,
275
+ ...buildCustomProviderFields(opts),
231
276
  },
232
277
  },
233
278
  );
@@ -245,7 +290,8 @@ function attachCreateSubcommand(connections: Command): void {
245
290
  );
246
291
  } else {
247
292
  process.stdout.write(
248
- `Created connection "${conn.name}" (provider=${conn.provider}, auth=${formatAuth(conn.auth)})\n`,
293
+ `Created connection "${conn.name}" (provider=${conn.provider}, auth=${formatAuth(conn.auth)})\n` +
294
+ `Verify it works: point a profile's provider_connection at "${conn.name}", then run: assistant inference send --profile <profile> "Reply with OK"\n`,
249
295
  );
250
296
  }
251
297
  },
@@ -265,11 +311,27 @@ function attachUpdateSubcommand(connections: Command): void {
265
311
  "--credential <vault-key>",
266
312
  "Vault credential name (required for --auth api_key)",
267
313
  )
314
+ .option(
315
+ "--base-url <url>",
316
+ "Endpoint base URL (openai-compatible connections)",
317
+ )
318
+ .option(
319
+ "--model <id>",
320
+ "Model id offered by this connection (repeatable; openai-compatible)",
321
+ collectRepeatable,
322
+ [] as string[],
323
+ )
268
324
  .option("--json", "Output as JSON")
269
325
  .action(
270
326
  async (
271
327
  name: string,
272
- opts: { auth: string; credential?: string; json?: boolean },
328
+ opts: {
329
+ auth: string;
330
+ credential?: string;
331
+ baseUrl?: string;
332
+ model?: string[];
333
+ json?: boolean;
334
+ },
273
335
  ) => {
274
336
  const authInput = buildAuthInput(opts.auth, opts.credential);
275
337
  if (typeof authInput === "string") {
@@ -281,7 +343,7 @@ function attachUpdateSubcommand(connections: Command): void {
281
343
  "inference_provider_connections_update",
282
344
  {
283
345
  pathParams: { name },
284
- body: { auth: authInput },
346
+ body: { auth: authInput, ...buildCustomProviderFields(opts) },
285
347
  },
286
348
  );
287
349
 
@@ -298,7 +360,8 @@ function attachUpdateSubcommand(connections: Command): void {
298
360
  );
299
361
  } else {
300
362
  process.stdout.write(
301
- `Updated connection "${name}" auth to ${formatAuth(conn.auth)}\n`,
363
+ `Updated connection "${name}" auth to ${formatAuth(conn.auth)}\n` +
364
+ `Verify it works: assistant inference send --profile <profile-using-this-connection> "Reply with OK"\n`,
302
365
  );
303
366
  }
304
367
  },
@@ -498,8 +561,15 @@ Examples:
498
561
  $ assistant inference providers connections get anthropic-managed
499
562
  $ assistant inference providers connections create anthropic-personal \\
500
563
  --provider anthropic --auth api_key --credential credential/anthropic/api_key
564
+ $ assistant inference providers connections create local-llm \\
565
+ --provider openai-compatible --auth none \\
566
+ --base-url http://localhost:1234/v1 --model my-model
501
567
  $ assistant inference providers connections update anthropic-personal --auth platform
502
- $ assistant inference providers connections delete anthropic-personal`,
568
+ $ assistant inference providers connections delete anthropic-personal
569
+
570
+ After creating or updating a connection, validate it with a live call through
571
+ a profile that uses it:
572
+ $ assistant inference send --profile <profile> "Reply with OK"`,
503
573
  );
504
574
 
505
575
  attachListSubcommand(connections);
@@ -509,4 +579,5 @@ Examples:
509
579
  attachDeleteSubcommand(connections);
510
580
 
511
581
  attachLoginChatgptSubcommand(providers);
582
+ attachDefaultProviderSubcommand(providers);
512
583
  }
@@ -15,6 +15,9 @@ import { cliIpcCall } from "../../ipc/cli-client.js";
15
15
  import { readStdinSync } from "../../util/read-stdin.js";
16
16
  import { registerCommand } from "../lib/register-command.js";
17
17
  import { log } from "../logger.js";
18
+ import { attachCallsitesSubcommand } from "./inference-callsites.js";
19
+ import { attachModelsSubcommand } from "./inference-models.js";
20
+ import { attachProfilesSubcommand } from "./inference-profiles.js";
18
21
  import { attachProvidersSubcommand } from "./inference-providers.js";
19
22
  import { attachSessionSubcommand } from "./inference-session.js";
20
23
 
@@ -248,6 +251,9 @@ Examples:
248
251
  attachSendSubcommand(inference);
249
252
  attachSessionSubcommand(inference);
250
253
  attachProvidersSubcommand(inference);
254
+ attachModelsSubcommand(inference);
255
+ attachProfilesSubcommand(inference);
256
+ attachCallsitesSubcommand(inference);
251
257
  },
252
258
  });
253
259