@chat-js/cli 0.1.4 → 0.2.1

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 (160) hide show
  1. package/dist/index.js +394 -247
  2. package/package.json +48 -48
  3. package/templates/chat-app/.claude/skiller.toml +18 -0
  4. package/templates/chat-app/.claude/skills/chat-context/SKILL.md +6 -0
  5. package/templates/chat-app/.claude/skills/chat-context/chat-context.mdc +36 -0
  6. package/templates/chat-app/.claude/skills/lazy-prefetch-pattern/lazy-prefetch-pattern.mdc +27 -0
  7. package/templates/chat-app/.claude/skills/react/react.mdc +29 -0
  8. package/templates/chat-app/.claude/skills/trpc-patterns/trpc-patterns.mdc +77 -0
  9. package/templates/chat-app/.claude/skills/typescript/typescript.mdc +53 -0
  10. package/templates/chat-app/.claude/skills/ultracite/ultracite.mdc +129 -0
  11. package/templates/chat-app/.cursor/skills/chat-context/SKILL.md +37 -0
  12. package/templates/chat-app/.cursor/skills/lazy-prefetch-pattern/SKILL.md +26 -0
  13. package/templates/chat-app/.cursor/skills/react/SKILL.md +28 -0
  14. package/templates/chat-app/.cursor/skills/trpc-patterns/SKILL.md +76 -0
  15. package/templates/chat-app/.cursor/skills/typescript/SKILL.md +52 -0
  16. package/templates/chat-app/.cursor/skills/ultracite/SKILL.md +128 -0
  17. package/templates/chat-app/app/(chat)/actions.ts +17 -13
  18. package/templates/chat-app/app/(chat)/api/chat/[id]/stream/route.ts +6 -5
  19. package/templates/chat-app/app/(chat)/api/chat/route.ts +14 -15
  20. package/templates/chat-app/app/(chat)/chat-providers.tsx +2 -2
  21. package/templates/chat-app/app/(chat)/layout.tsx +7 -6
  22. package/templates/chat-app/app/api/cron/cleanup/route.ts +4 -3
  23. package/templates/chat-app/app/globals.css +23 -23
  24. package/templates/chat-app/app/layout.tsx +1 -1
  25. package/templates/chat-app/biome.jsonc +3 -3
  26. package/templates/chat-app/chat.config.ts +48 -21
  27. package/templates/chat-app/components/anonymous-session-init.tsx +4 -12
  28. package/templates/chat-app/components/artifact-actions.tsx +5 -5
  29. package/templates/chat-app/components/artifact-panel.tsx +6 -6
  30. package/templates/chat-app/components/assistant-message.tsx +1 -1
  31. package/templates/chat-app/components/chat/chat-layout.tsx +2 -2
  32. package/templates/chat-app/components/chat/chat-welcome.tsx +1 -0
  33. package/templates/chat-app/components/chat-features-definitions.ts +11 -8
  34. package/templates/chat-app/components/chat-menu-items.tsx +4 -4
  35. package/templates/chat-app/components/chat-sync.tsx +1 -1
  36. package/templates/chat-app/components/clone-chat-button.tsx +2 -2
  37. package/templates/chat-app/components/code-editor.tsx +5 -5
  38. package/templates/chat-app/components/connectors-dropdown.tsx +2 -2
  39. package/templates/chat-app/components/console.tsx +5 -5
  40. package/templates/chat-app/components/create-artifact.tsx +28 -28
  41. package/templates/chat-app/components/data-stream-provider.tsx +2 -2
  42. package/templates/chat-app/components/deep-research-progress.tsx +2 -2
  43. package/templates/chat-app/components/delete-chat-dialog.tsx +3 -3
  44. package/templates/chat-app/components/delete-project-dialog.tsx +3 -3
  45. package/templates/chat-app/components/diffview.tsx +3 -3
  46. package/templates/chat-app/components/favicon-group.tsx +7 -7
  47. package/templates/chat-app/components/header-breadcrumb.tsx +11 -11
  48. package/templates/chat-app/components/image-editor.tsx +5 -5
  49. package/templates/chat-app/components/image-modal.tsx +4 -4
  50. package/templates/chat-app/components/interactive-chart-impl.tsx +269 -0
  51. package/templates/chat-app/components/interactive-charts.tsx +18 -246
  52. package/templates/chat-app/components/lexical-chat-input.tsx +10 -10
  53. package/templates/chat-app/components/message-editor.tsx +3 -3
  54. package/templates/chat-app/components/message-parts.tsx +8 -3
  55. package/templates/chat-app/components/messages-pane.tsx +4 -4
  56. package/templates/chat-app/components/messages.tsx +5 -5
  57. package/templates/chat-app/components/model-selector.tsx +4 -1
  58. package/templates/chat-app/components/multimodal-input.tsx +14 -5
  59. package/templates/chat-app/components/part/code-execution.tsx +4 -1
  60. package/templates/chat-app/components/part/document-common.tsx +8 -8
  61. package/templates/chat-app/components/part/document-preview.tsx +34 -16
  62. package/templates/chat-app/components/part/document-tool.tsx +3 -3
  63. package/templates/chat-app/components/part/dynamic-tool.tsx +3 -3
  64. package/templates/chat-app/components/part/generate-video.tsx +54 -0
  65. package/templates/chat-app/components/part/message-reasoning.tsx +3 -3
  66. package/templates/chat-app/components/project-details-dialog.tsx +4 -4
  67. package/templates/chat-app/components/project-home.tsx +1 -0
  68. package/templates/chat-app/components/project-icon-picker.tsx +5 -5
  69. package/templates/chat-app/components/project-icon.tsx +4 -4
  70. package/templates/chat-app/components/project-menu-items.tsx +3 -3
  71. package/templates/chat-app/components/research-tasks.tsx +3 -3
  72. package/templates/chat-app/components/sandbox.tsx +4 -4
  73. package/templates/chat-app/components/search-chats-dialog.tsx +11 -11
  74. package/templates/chat-app/components/settings/connectors-settings.tsx +1 -1
  75. package/templates/chat-app/components/settings/settings-nav.tsx +1 -1
  76. package/templates/chat-app/components/sheet-editor.tsx +5 -5
  77. package/templates/chat-app/components/sidebar-chats-list.tsx +5 -5
  78. package/templates/chat-app/components/suggested-actions.tsx +3 -3
  79. package/templates/chat-app/components/text-editor.tsx +5 -5
  80. package/templates/chat-app/components/toolbar.tsx +6 -6
  81. package/templates/chat-app/components/upgrade-cta/login-cta-banner.tsx +5 -5
  82. package/templates/chat-app/components/upgrade-cta/login-prompt.tsx +4 -4
  83. package/templates/chat-app/components/upgrade-cta/share-menu-item.tsx +3 -3
  84. package/templates/chat-app/components/user-message.tsx +3 -3
  85. package/templates/chat-app/components/version-footer.tsx +4 -4
  86. package/templates/chat-app/hooks/chat-sync-hooks.ts +0 -55
  87. package/templates/chat-app/hooks/use-artifact.tsx +3 -3
  88. package/templates/chat-app/hooks/use-auto-focus.ts +37 -7
  89. package/templates/chat-app/hooks/use-media-query.tsx +2 -4
  90. package/templates/chat-app/lib/ai/active-gateway.ts +1 -1
  91. package/templates/chat-app/lib/ai/ai-gateway-models-schemas.ts +30 -6
  92. package/templates/chat-app/lib/ai/app-model-id.ts +1 -1
  93. package/templates/chat-app/lib/ai/app-models.ts +4 -4
  94. package/templates/chat-app/lib/ai/eval-agent.ts +5 -5
  95. package/templates/chat-app/lib/ai/followup-suggestions.ts +5 -2
  96. package/templates/chat-app/lib/ai/gateway-model-defaults.ts +131 -41
  97. package/templates/chat-app/lib/ai/gateways/gateway-provider.ts +10 -6
  98. package/templates/chat-app/lib/ai/gateways/openai-compatible-gateway.ts +9 -4
  99. package/templates/chat-app/lib/ai/gateways/openai-gateway.ts +9 -4
  100. package/templates/chat-app/lib/ai/gateways/openrouter-gateway.ts +17 -12
  101. package/templates/chat-app/lib/ai/gateways/registry.ts +9 -0
  102. package/templates/chat-app/lib/ai/gateways/vercel-gateway.ts +36 -4
  103. package/templates/chat-app/lib/ai/mcp/cache.ts +13 -13
  104. package/templates/chat-app/lib/ai/model-data.ts +21 -20
  105. package/templates/chat-app/lib/ai/models.generated.ts +4397 -3592
  106. package/templates/chat-app/lib/ai/models.ts +1 -1
  107. package/templates/chat-app/lib/ai/providers.ts +10 -0
  108. package/templates/chat-app/lib/ai/text-splitter.ts +3 -4
  109. package/templates/chat-app/lib/ai/to-model-data.ts +1 -0
  110. package/templates/chat-app/lib/ai/tools/code-execution.ts +122 -53
  111. package/templates/chat-app/lib/ai/tools/deep-research/configuration.ts +35 -32
  112. package/templates/chat-app/lib/ai/tools/deep-research/pipeline.ts +2 -2
  113. package/templates/chat-app/lib/ai/tools/deep-research/types.ts +9 -9
  114. package/templates/chat-app/lib/ai/tools/documents/types.ts +4 -4
  115. package/templates/chat-app/lib/ai/tools/generate-image.ts +42 -20
  116. package/templates/chat-app/lib/ai/tools/generate-video.ts +166 -0
  117. package/templates/chat-app/lib/ai/tools/get-weather.ts +20 -20
  118. package/templates/chat-app/lib/ai/tools/read-document.ts +3 -3
  119. package/templates/chat-app/lib/ai/tools/steps/multi-query-web-search.ts +11 -11
  120. package/templates/chat-app/lib/ai/tools/steps/web-search.ts +6 -6
  121. package/templates/chat-app/lib/ai/tools/tools-definitions.ts +10 -5
  122. package/templates/chat-app/lib/ai/tools/tools.ts +15 -6
  123. package/templates/chat-app/lib/ai/tools/types.ts +2 -2
  124. package/templates/chat-app/lib/ai/types.ts +22 -13
  125. package/templates/chat-app/lib/artifacts/code/client.tsx +5 -5
  126. package/templates/chat-app/lib/artifacts/sheet/client.tsx +2 -2
  127. package/templates/chat-app/lib/artifacts/text/client.tsx +18 -3
  128. package/templates/chat-app/lib/clone-messages.test.ts +6 -1
  129. package/templates/chat-app/lib/config-requirements.ts +19 -10
  130. package/templates/chat-app/lib/config-schema.ts +189 -103
  131. package/templates/chat-app/lib/config.ts +4 -4
  132. package/templates/chat-app/lib/credits/cost-accumulator.ts +11 -8
  133. package/templates/chat-app/lib/env-schema.ts +1 -1
  134. package/templates/chat-app/lib/features-config.ts +6 -6
  135. package/templates/chat-app/lib/stores/with-threads.ts +3 -3
  136. package/templates/chat-app/lib/thread-utils.ts +2 -2
  137. package/templates/chat-app/lib/types/anonymous.ts +4 -4
  138. package/templates/chat-app/lib/types/ui-chat.ts +7 -7
  139. package/templates/chat-app/lib/utils/download-assets.ts +3 -3
  140. package/templates/chat-app/lib/utils/rate-limit.ts +8 -8
  141. package/templates/chat-app/next.config.ts +0 -25
  142. package/templates/chat-app/package.json +16 -16
  143. package/templates/chat-app/playwright.config.ts +5 -5
  144. package/templates/chat-app/providers/chat-id-provider.tsx +5 -5
  145. package/templates/chat-app/providers/chat-input-provider.tsx +15 -15
  146. package/templates/chat-app/providers/chat-models-provider.tsx +3 -3
  147. package/templates/chat-app/providers/default-model-provider.tsx +5 -5
  148. package/templates/chat-app/providers/parse-chat-id-from-pathname.test.ts +16 -0
  149. package/templates/chat-app/providers/session-provider.tsx +2 -2
  150. package/templates/chat-app/scripts/check-env.ts +36 -4
  151. package/templates/chat-app/tests/artifacts.e2e.ts +7 -0
  152. package/templates/chat-app/tests/auth.setup.e2e.ts +10 -0
  153. package/templates/chat-app/tests/chat.e2e.ts +7 -0
  154. package/templates/chat-app/tests/reasoning.e2e.ts +7 -0
  155. package/templates/chat-app/tests/reasoning.setup.e2e.ts +10 -0
  156. package/templates/chat-app/trpc/routers/chat.router.ts +1 -1
  157. package/templates/chat-app/trpc/routers/mcp.router.ts +3 -3
  158. package/templates/chat-app/vitest.config.ts +7 -0
  159. package/templates/chat-app/next-env.d.ts +0 -6
  160. package/templates/chat-app/tsconfig.tsbuildinfo +0 -1
@@ -34,7 +34,7 @@ export const fetchModels = unstable_cache(
34
34
  const models = await fetchModelsRaw();
35
35
  return models.map(toModelData);
36
36
  },
37
- [`ai-gateway-models-${config.models.gateway}`],
37
+ [`ai-gateway-models-${config.ai.gateway}`],
38
38
  {
39
39
  revalidate: 3600,
40
40
  tags: ["ai-gateway-models"],
@@ -51,6 +51,16 @@ export const getImageModel = (modelId: string) => {
51
51
  return imageModel;
52
52
  };
53
53
 
54
+ export const getVideoModel = (modelId: string) => {
55
+ const videoModel = getActiveGateway().createVideoModel(modelId);
56
+ if (!videoModel) {
57
+ throw new Error(
58
+ `Gateway '${getActiveGateway().type}' does not support video models.`
59
+ );
60
+ }
61
+ return videoModel;
62
+ };
63
+
54
64
  // Get a multimodal language model that can generate images via generateText
55
65
  export const getMultimodalImageModel = (modelId: string) =>
56
66
  getActiveGateway().createLanguageModel(modelId);
@@ -1,8 +1,7 @@
1
- type TextSplitterParams = {
2
- chunkSize: number;
3
-
1
+ interface TextSplitterParams {
4
2
  chunkOverlap: number;
5
- };
3
+ chunkSize: number;
4
+ }
6
5
 
7
6
  abstract class TextSplitter implements TextSplitterParams {
8
7
  chunkSize = 1000;
@@ -28,6 +28,7 @@ export function toModelData(model: AiGatewayModel): ModelData {
28
28
  image: tags.includes("image-generation") || model.type === "image",
29
29
  text: model.type === "language",
30
30
  audio: false,
31
+ video: model.type === "video",
31
32
  },
32
33
  };
33
34
  }
@@ -7,6 +7,7 @@ import { createModuleLogger } from "@/lib/logger";
7
7
  import { toolsDefinitions } from "./tools-definitions";
8
8
 
9
9
  const WHITESPACE_REGEX = /\s+/;
10
+ const PACKAGE_SPEC_SPLIT_RE = /[=<>![\s]/;
10
11
 
11
12
  async function installBasePackages(
12
13
  sandbox: Sandbox,
@@ -39,8 +40,13 @@ async function installBasePackages(
39
40
  return { success: true };
40
41
  }
41
42
 
43
+ function packageName(spec: string): string {
44
+ return spec.split(PACKAGE_SPEC_SPLIT_RE)[0].toLowerCase();
45
+ }
46
+
42
47
  async function processExtraPackages(
43
48
  code: string,
49
+ basePackages: readonly string[],
44
50
  sandbox: Sandbox,
45
51
  log: ReturnType<typeof createModuleLogger>,
46
52
  requestId: string
@@ -51,43 +57,51 @@ async function processExtraPackages(
51
57
  result?: { message: string; chart: string };
52
58
  };
53
59
  }> {
60
+ const basePackageNames = new Set(basePackages.map((p) => p.toLowerCase()));
54
61
  const lines = code.split("\n");
55
62
  const pipLines = lines.filter((l) => l.trim().startsWith("!pip install "));
56
- const extraPackages = pipLines.flatMap((l) =>
57
- l
58
- .trim()
59
- .slice("!pip install ".length)
60
- .split(WHITESPACE_REGEX)
61
- .filter(Boolean)
62
- );
63
+ const extraPackages = pipLines
64
+ .flatMap((l) =>
65
+ l
66
+ .trim()
67
+ .slice("!pip install ".length)
68
+ .split(WHITESPACE_REGEX)
69
+ .filter(Boolean)
70
+ )
71
+ .filter((spec) => !basePackageNames.has(packageName(spec)));
72
+
73
+ const codeWithoutPipLines = lines
74
+ .filter((l) => !l.trim().startsWith("!pip install "))
75
+ .join("\n");
76
+
77
+ if (extraPackages.length === 0) {
78
+ return { codeToRun: codeWithoutPipLines, installResult: { success: true } };
79
+ }
63
80
 
64
- let codeToRun = code;
65
- if (extraPackages.length > 0) {
66
- log.info({ requestId, extraPackages }, "installing extra packages");
67
- const dynamicInstall = await sandbox.runCommand({
68
- cmd: "pip",
69
- args: ["install", ...extraPackages],
70
- });
71
- if (dynamicInstall.exitCode !== 0) {
72
- const stderr = await dynamicInstall.stderr();
73
- log.error({ requestId, stderr }, "dynamic package installation failed");
74
- return {
75
- codeToRun: code,
76
- installResult: {
77
- success: false,
78
- result: {
79
- message: `Failed to install packages: ${stderr}`,
80
- chart: "",
81
- },
81
+ log.info({ requestId, extraPackages }, "installing extra packages");
82
+ const dynamicInstall = await sandbox.runCommand({
83
+ cmd: "pip",
84
+ args: ["install", ...extraPackages],
85
+ });
86
+ if (dynamicInstall.exitCode !== 0) {
87
+ const stderr = await dynamicInstall.stderr();
88
+ log.error({ requestId, stderr }, "dynamic package installation failed");
89
+ return {
90
+ codeToRun: code,
91
+ installResult: {
92
+ success: false,
93
+ result: {
94
+ message: `Failed to install packages: ${stderr}`,
95
+ chart: "",
82
96
  },
83
- };
84
- }
85
- codeToRun = lines
86
- .filter((l) => !l.trim().startsWith("!pip install "))
87
- .join("\n");
97
+ },
98
+ };
88
99
  }
89
100
 
90
- return { codeToRun, installResult: { success: true } };
101
+ return {
102
+ codeToRun: codeWithoutPipLines,
103
+ installResult: { success: true },
104
+ };
91
105
  }
92
106
 
93
107
  function createWrappedCode(codeToRun: string, chartPath: string): string {
@@ -96,19 +110,36 @@ import sys
96
110
  import json
97
111
  import traceback
98
112
 
113
+ try:
114
+ import matplotlib.pyplot as _plt_module
115
+ _orig_savefig = _plt_module.savefig
116
+ def _intercepted_savefig(*args, **kwargs):
117
+ _orig_savefig('${chartPath}', format='png', bbox_inches='tight', dpi=100)
118
+ if args or kwargs.get('fname') not in (None, '${chartPath}'):
119
+ return _orig_savefig(*args, **kwargs)
120
+ _plt_module.savefig = _intercepted_savefig
121
+ except ImportError:
122
+ pass
123
+
99
124
  try:
100
125
  exec(${JSON.stringify(codeToRun)})
101
126
  try:
102
127
  _locals = locals()
103
128
  _globals = globals()
104
- if "result" in _locals:
105
- print(_locals["result"])
106
- elif "result" in _globals:
107
- print(_globals["result"])
108
- elif "results" in _locals:
109
- print(_locals["results"])
110
- elif "results" in _globals:
111
- print(_globals["results"])
129
+ _chart_var = _locals.get("chart") or _globals.get("chart")
130
+ if (isinstance(_chart_var, dict)
131
+ and isinstance(_chart_var.get("type"), str)
132
+ and isinstance(_chart_var.get("elements"), list)):
133
+ print("__CHART_JSON__:" + json.dumps(_chart_var))
134
+ else:
135
+ if "result" in _locals:
136
+ print(_locals["result"])
137
+ elif "result" in _globals:
138
+ print(_globals["result"])
139
+ elif "results" in _locals:
140
+ print(_locals["results"])
141
+ elif "results" in _globals:
142
+ print(_globals["results"])
112
143
  except Exception:
113
144
  pass
114
145
  try:
@@ -126,11 +157,14 @@ except Exception as e:
126
157
  `;
127
158
  }
128
159
 
160
+ const CHART_JSON_PREFIX = "__CHART_JSON__:";
161
+
129
162
  async function parseExecutionOutput(execResult: {
130
163
  stdout: () => Promise<string>;
131
164
  exitCode: number;
132
165
  }): Promise<{
133
166
  outputText: string;
167
+ chartData: Record<string, unknown> | null;
134
168
  execInfo: {
135
169
  success: boolean;
136
170
  error?: { name: string; value: string; traceback: string };
@@ -142,18 +176,33 @@ async function parseExecutionOutput(execResult: {
142
176
  error?: { name: string; value: string; traceback: string };
143
177
  } = { success: true };
144
178
  let outputText = "";
179
+ let chartData: Record<string, unknown> | null = null;
145
180
 
146
181
  try {
147
182
  const outLines = (stdout ?? "").trim().split("\n");
148
183
  const lastLine = outLines.at(-1);
149
184
  execInfo = JSON.parse(lastLine ?? "{}");
150
185
  outLines.pop();
186
+
187
+ const chartLineIdx = outLines.findIndex((l) =>
188
+ l.startsWith(CHART_JSON_PREFIX)
189
+ );
190
+ if (chartLineIdx !== -1) {
191
+ const raw = outLines[chartLineIdx].slice(CHART_JSON_PREFIX.length);
192
+ try {
193
+ chartData = JSON.parse(raw) as Record<string, unknown>;
194
+ } catch {
195
+ // ignore malformed chart JSON
196
+ }
197
+ outLines.splice(chartLineIdx, 1);
198
+ }
199
+
151
200
  outputText = outLines.join("\n");
152
201
  } catch {
153
202
  outputText = stdout ?? "";
154
203
  }
155
204
 
156
- return { outputText, execInfo };
205
+ return { outputText, chartData, execInfo };
157
206
  }
158
207
 
159
208
  async function checkForChart(
@@ -248,7 +297,7 @@ async function executeInSandbox({
248
297
  requestId: string;
249
298
  }): Promise<{
250
299
  message: string;
251
- chart: string | { base64: string; format: string };
300
+ chart: string | { base64: string; format: string } | Record<string, unknown>;
252
301
  }> {
253
302
  const baseInstallResult = await installBasePackages(
254
303
  sandbox,
@@ -262,6 +311,7 @@ async function executeInSandbox({
262
311
 
263
312
  const { codeToRun, installResult } = await processExtraPackages(
264
313
  code,
314
+ basePackages,
265
315
  sandbox,
266
316
  log,
267
317
  requestId
@@ -276,8 +326,8 @@ async function executeInSandbox({
276
326
  args: ["-c", wrappedCode],
277
327
  });
278
328
 
279
- const { outputText, execInfo } = await parseExecutionOutput(execResult);
280
- const chartOut = await checkForChart(sandbox, chartPath, log, requestId);
329
+ const { outputText, chartData, execInfo } =
330
+ await parseExecutionOutput(execResult);
281
331
 
282
332
  const message = buildResponseMessage({
283
333
  outputText,
@@ -287,6 +337,12 @@ async function executeInSandbox({
287
337
  requestId,
288
338
  });
289
339
 
340
+ if (chartData) {
341
+ log.info({ requestId }, "interactive chart data returned");
342
+ return { message: message.trim(), chart: chartData };
343
+ }
344
+
345
+ const chartOut = await checkForChart(sandbox, chartPath, log, requestId);
290
346
  return {
291
347
  message: message.trim(),
292
348
  chart: chartOut ?? "",
@@ -319,23 +375,36 @@ export const codeExecution = ({
319
375
  costAccumulator?: CostAccumulator;
320
376
  }) =>
321
377
  tool({
322
- description: `Python-only sandbox for calculations, data analysis & simple visualisations.
378
+ description: `Python-only sandbox for calculations, data analysis & visualisations.
323
379
 
324
380
  Use for:
325
- - Execute Python (matplotlib, pandas, numpy, sympy, yfinance pre-installed)
326
- - Produce line / scatter / bar charts (no need to call 'plt.show()')
327
- - Install extra libs by adding lines like: '!pip install <pkg> [<pkg2> ...]' (we auto-install and strip these lines)
381
+ - Execute Python (matplotlib, pandas, numpy, sympy, yfinance pre-installed — do NOT reinstall them)
382
+ - Produce interactive line / scatter / bar charts OR matplotlib PNG charts
383
+ - Install extra libs by adding lines like: '!pip install <pkg> [<pkg2> ...]' (we auto-install and strip these lines; pre-installed packages are ignored)
384
+
385
+ Chart output — choose ONE:
386
+ 1. Interactive chart (preferred for line/scatter/bar): assign a 'chart' variable matching this schema:
387
+ chart = {
388
+ "type": "line" | "scatter" | "bar",
389
+ "title": "My Chart",
390
+ "x_label": "X", # optional
391
+ "y_label": "Y", # optional
392
+ "x_scale": "datetime" | None, # optional, for time-series x axes
393
+ "elements": [
394
+ # for line/scatter: {"label": "Series A", "points": [[x1,y1],[x2,y2],...]}
395
+ # for bar: {"label": "Category", "group": "Group A", "value": 42}
396
+ ]
397
+ }
398
+ 2. Matplotlib PNG: use plt.plot()/plt.savefig() normally (no need to call plt.show())
328
399
 
329
400
  Restrictions:
330
401
  - No images in the assistant response; don't embed them
331
-
332
- Avoid:
333
- - Any non-Python language
334
- - Chart types other than line / scatter / bar
402
+ - Interactive chart: only line / scatter / bar types
335
403
 
336
404
  Output rules:
337
- - Print the values you want returned (e.g. 'print(df.head())' or 'print(answer)')
338
- - Or assign to a variable named 'result' or 'results' and we'll print it automatically
405
+ - Assign 'chart' dict for interactive charts (takes priority over matplotlib PNG)
406
+ - Assign 'result' or 'results' for other computed values (auto-printed)
407
+ - Or print explicitly: print(answer)
339
408
  - Don't rely on implicit REPL last-expression output`,
340
409
  inputSchema: z.object({
341
410
  title: z.string().describe("The title of the code snippet."),
@@ -3,28 +3,16 @@ import { env } from "@/lib/env";
3
3
 
4
4
  export type SearchAPI = "firecrawl" | "tavily" | "none";
5
5
 
6
- export type DeepResearchRuntimeConfig = {
7
- // General Configuration
8
- max_structured_output_retries: number;
6
+ export interface DeepResearchRuntimeConfig {
9
7
  allow_clarification: boolean;
10
- max_concurrent_research_units: number;
11
-
12
- // Research Configuration
13
- search_api: SearchAPI;
14
- search_api_max_queries: number;
15
- max_researcher_iterations: number;
16
-
17
- // Model Configuration
18
- summarization_model: string;
19
- summarization_model_max_tokens: number;
20
- research_model: string;
21
- research_model_max_tokens: number;
22
8
  compression_model: string;
23
9
  compression_model_max_tokens: number;
24
10
  final_report_model: string;
25
11
  final_report_model_max_tokens: number;
26
- status_update_model: string;
27
- status_update_model_max_tokens: number;
12
+ max_concurrent_research_units: number;
13
+ max_researcher_iterations: number;
14
+ // General Configuration
15
+ max_structured_output_retries: number;
28
16
 
29
17
  // MCP server configuration (not yet implemented)
30
18
  mcp_config?: {
@@ -33,7 +21,19 @@ export type DeepResearchRuntimeConfig = {
33
21
  headers?: Record<string, string>;
34
22
  };
35
23
  mcp_prompt?: string;
36
- };
24
+ research_model: string;
25
+ research_model_max_tokens: number;
26
+
27
+ // Research Configuration
28
+ search_api: SearchAPI;
29
+ search_api_max_queries: number;
30
+ status_update_model: string;
31
+ status_update_model_max_tokens: number;
32
+
33
+ // Model Configuration
34
+ summarization_model: string;
35
+ summarization_model_max_tokens: number;
36
+ }
37
37
 
38
38
  function getSearchApi(): SearchAPI {
39
39
  if (env.TAVILY_API_KEY) {
@@ -46,33 +46,36 @@ function getSearchApi(): SearchAPI {
46
46
  }
47
47
 
48
48
  export function getDeepResearchConfig(): DeepResearchRuntimeConfig {
49
- const deepResearchModel = config.models.defaults.deepResearch;
50
- const deepResearchFinalReportModel =
51
- config.models.defaults.deepResearchFinalReport;
52
- const deepResearchSettings = config.deepResearch;
49
+ const {
50
+ defaultModel,
51
+ finalReportModel,
52
+ allowClarification,
53
+ maxConcurrentResearchUnits,
54
+ maxSearchQueries,
55
+ maxResearcherIterations,
56
+ } = config.ai.tools.deepResearch;
53
57
 
54
58
  return {
55
59
  // General Configuration
56
60
  max_structured_output_retries: 3,
57
- allow_clarification: deepResearchSettings.allowClarification,
58
- max_concurrent_research_units:
59
- deepResearchSettings.maxConcurrentResearchUnits,
61
+ allow_clarification: allowClarification,
62
+ max_concurrent_research_units: maxConcurrentResearchUnits,
60
63
 
61
64
  // Research Configuration
62
65
  search_api: getSearchApi(),
63
- search_api_max_queries: deepResearchSettings.maxSearchQueries,
64
- max_researcher_iterations: deepResearchSettings.maxResearcherIterations,
66
+ search_api_max_queries: maxSearchQueries,
67
+ max_researcher_iterations: maxResearcherIterations,
65
68
 
66
69
  // Model Configuration - use same model for research/compression/summarization
67
- summarization_model: deepResearchModel,
70
+ summarization_model: defaultModel,
68
71
  summarization_model_max_tokens: 4000,
69
- research_model: deepResearchModel,
72
+ research_model: defaultModel,
70
73
  research_model_max_tokens: 4000,
71
- compression_model: deepResearchModel,
74
+ compression_model: defaultModel,
72
75
  compression_model_max_tokens: 4000,
73
- final_report_model: deepResearchFinalReportModel,
76
+ final_report_model: finalReportModel,
74
77
  final_report_model_max_tokens: 6000,
75
- status_update_model: deepResearchModel,
78
+ status_update_model: defaultModel,
76
79
  status_update_model_max_tokens: 4000,
77
80
  };
78
81
  }
@@ -163,10 +163,10 @@ async function clarifyWithUser(
163
163
 
164
164
  // Step 2: Research Brief
165
165
 
166
- type ResearchBrief = {
166
+ interface ResearchBrief {
167
167
  research_brief: string;
168
168
  title: string;
169
- };
169
+ }
170
170
 
171
171
  async function writeResearchBrief(
172
172
  messages: ModelMessage[],
@@ -9,15 +9,15 @@ import type { DeepResearchRuntimeConfig } from "./configuration";
9
9
  // Shared Agent Options
10
10
  //##################
11
11
 
12
- export type AgentOptions = {
12
+ export interface AgentOptions {
13
+ abortSignal?: AbortSignal;
13
14
  config: DeepResearchRuntimeConfig;
15
+ costAccumulator?: CostAccumulator;
14
16
  dataStream: StreamWriter;
15
- toolCallId: string;
16
17
  messageId: string;
17
18
  requestId: string;
18
- costAccumulator?: CostAccumulator;
19
- abortSignal?: AbortSignal;
20
- };
19
+ toolCallId: string;
20
+ }
21
21
 
22
22
  //##################
23
23
  // Telemetry Helper
@@ -67,12 +67,12 @@ export const ResearchQuestionSchema = z.object({
67
67
  // Pipeline IO
68
68
  //##################
69
69
 
70
- export type DeepResearchInput = {
71
- requestId: string;
70
+ export interface DeepResearchInput {
72
71
  messageId: string;
73
- toolCallId: string;
74
72
  messages: ModelMessage[];
75
- };
73
+ requestId: string;
74
+ toolCallId: string;
75
+ }
76
76
 
77
77
  export type DeepResearchResult =
78
78
  | {
@@ -15,13 +15,13 @@ export type DocumentToolResult =
15
15
  error: string;
16
16
  };
17
17
 
18
- export type DocumentToolContext = {
19
- session: ToolSession;
18
+ export interface DocumentToolContext {
19
+ costAccumulator?: CostAccumulator;
20
20
  // dataStream: StreamWriter;
21
21
  messageId: string;
22
22
  selectedModel: ModelId;
23
- costAccumulator?: CostAccumulator;
24
- };
23
+ session: ToolSession;
24
+ }
25
25
 
26
26
  // Document tool type names as they appear in ChatMessage parts
27
27
  export const createDocumentToolTypes = [
@@ -6,14 +6,13 @@ import { uploadFile } from "@/lib/blob";
6
6
  import { config } from "@/lib/config";
7
7
  import type { CostAccumulator } from "@/lib/credits/cost-accumulator";
8
8
  import { createModuleLogger } from "@/lib/logger";
9
- import { toolsDefinitions } from "./tools-definitions";
10
9
 
11
- type GenerateImageProps = {
10
+ interface GenerateImageProps {
12
11
  attachments?: FileUIPart[];
12
+ costAccumulator?: CostAccumulator;
13
13
  lastGeneratedImage?: { imageUrl: string; name: string } | null;
14
14
  selectedModel?: string;
15
- costAccumulator?: CostAccumulator;
16
- };
15
+ }
17
16
 
18
17
  const log = createModuleLogger("ai.tools.generate-image");
19
18
 
@@ -42,7 +41,7 @@ async function resolveImageModel(selectedModel?: string): Promise<{
42
41
  }
43
42
 
44
43
  // Fall back to the configured default image model
45
- const defaultId = config.models.defaults.image;
44
+ const defaultId = config.ai.tools.image.default;
46
45
  try {
47
46
  const model = await getAppModelDefinition(defaultId as AppModelId);
48
47
  // Default could be a multimodal language model (e.g. gemini-3-pro-image)
@@ -126,12 +125,14 @@ async function runGenerateImageTraditional({
126
125
  imageParts,
127
126
  lastGeneratedImage,
128
127
  startMs,
128
+ costAccumulator,
129
129
  }: {
130
130
  mode: ImageMode;
131
131
  prompt: string;
132
132
  imageParts: FileUIPart[];
133
133
  lastGeneratedImage: { imageUrl: string; name: string } | null;
134
134
  startMs: number;
135
+ costAccumulator?: CostAccumulator;
135
136
  }): Promise<{ imageUrl: string; prompt: string }> {
136
137
  let promptInput:
137
138
  | string
@@ -160,7 +161,7 @@ async function runGenerateImageTraditional({
160
161
  }
161
162
 
162
163
  const res = await generateImage({
163
- model: getImageModel(config.models.defaults.image),
164
+ model: getImageModel(config.ai.tools.image.default),
164
165
  prompt: promptInput,
165
166
  n: 1,
166
167
  providerOptions: {
@@ -181,6 +182,17 @@ async function runGenerateImageTraditional({
181
182
  const filename = `generated-image-${timestamp}.png`;
182
183
  const result = await uploadFile(filename, buffer);
183
184
 
185
+ if (res.usage) {
186
+ costAccumulator?.addLLMCost(
187
+ config.ai.tools.image.default as AppModelId,
188
+ {
189
+ inputTokens: res.usage.inputTokens,
190
+ outputTokens: res.usage.outputTokens,
191
+ },
192
+ "generateImage-traditional"
193
+ );
194
+ }
195
+
184
196
  log.info(
185
197
  {
186
198
  mode,
@@ -212,8 +224,14 @@ async function runGenerateImageMultimodal({
212
224
  costAccumulator?: CostAccumulator;
213
225
  }): Promise<{ imageUrl: string; prompt: string }> {
214
226
  // Build messages with image context if in edit mode
215
- type ImageContent = { type: "image"; image: URL };
216
- type TextContent = { type: "text"; text: string };
227
+ interface ImageContent {
228
+ image: URL;
229
+ type: "image";
230
+ }
231
+ interface TextContent {
232
+ text: string;
233
+ type: "text";
234
+ }
217
235
  const userContent: Array<TextContent | ImageContent> = [];
218
236
 
219
237
  // Add reference images if in edit mode
@@ -247,13 +265,24 @@ async function runGenerateImageMultimodal({
247
265
  "generateImage: using multimodal model"
248
266
  );
249
267
 
268
+ const isGoogleModel =
269
+ modelId.startsWith("google/") || modelId.includes("gemini");
270
+ const isOpenAIModel = modelId.startsWith("openai/");
271
+
250
272
  const res = await generateText({
251
273
  model: getMultimodalImageModel(modelId),
252
274
  messages: [{ role: "user", content: userContent }],
253
275
  providerOptions: {
254
- google: {
255
- responseModalities: ["TEXT", "IMAGE"],
256
- },
276
+ ...(isGoogleModel && {
277
+ google: {
278
+ responseModalities: ["TEXT", "IMAGE"],
279
+ },
280
+ }),
281
+ ...(isOpenAIModel && {
282
+ openai: {
283
+ modalities: ["text", "image"],
284
+ },
285
+ }),
257
286
  },
258
287
  });
259
288
 
@@ -362,21 +391,14 @@ The assistant must not add new subjects, claims, branding, or alter the tone or
362
391
  }
363
392
 
364
393
  // Traditional image generation for dedicated image models
365
- const result = await runGenerateImageTraditional({
394
+ return await runGenerateImageTraditional({
366
395
  mode,
367
396
  prompt,
368
397
  imageParts,
369
398
  lastGeneratedImage,
370
399
  startMs,
400
+ costAccumulator,
371
401
  });
372
-
373
- // Report API cost for traditional image generation
374
- costAccumulator?.addAPICost(
375
- "generateImage",
376
- toolsDefinitions.generateImage.cost
377
- );
378
-
379
- return result;
380
402
  } catch (error) {
381
403
  const resolvedError = await resolveError(error);
382
404
  log.error(