@firstlovecenter/ai-chat 0.6.1 → 0.8.0

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.
@@ -1,5 +1,5 @@
1
- import { S as SystemBlock, T as ToolSchema, a as ToolContext, b as ToolDefinition, P as PresentPayload, c as PersistencePort, A as AuthPort, d as ScopePort, e as ToolsPort, V as VertexPort, L as LoggerPort } from '../types-CQntnyDJ.cjs';
2
- export { f as AiSettings, g as AiSettingsPatch, h as AppendMessageInput, i as AuthFail, j as AuthOk, k as AuthResult, B as Block, C as ChartSpec, l as ChatMessage, m as ChatMessageRole, n as ChatSession, o as CreateSessionInput, p as ListSessionsOpts, q as TERMINAL_TOOL_NAME, r as ToolResult, s as err, t as ok } from '../types-CQntnyDJ.cjs';
1
+ import { S as SystemBlock, T as ToolSchema, a as ToolContext, b as ToolDefinition, P as PresentPayload, c as PersistencePort, A as AuthPort, d as ScopePort, e as ToolsPort, V as VertexPort, L as LoggerPort } from '../types-BnwUkqKb.cjs';
2
+ export { f as AiSettings, g as AiSettingsPatch, h as AppendMessageInput, i as AuthFail, j as AuthOk, k as AuthResult, B as Block, C as ChartSpec, l as ChatMessage, m as ChatMessageRole, n as ChatSession, o as CreateSessionInput, p as ListSessionsOpts, q as TERMINAL_TOOL_NAME, r as ToolResult, s as err, t as ok } from '../types-BnwUkqKb.cjs';
3
3
  import { GoogleAuth } from 'google-auth-library';
4
4
  export { GoogleAuth } from 'google-auth-library';
5
5
  import 'zod';
@@ -55,6 +55,14 @@ type NormalizedToolResult = {
55
55
  type NormalizedMessage = {
56
56
  role: 'user';
57
57
  text: string;
58
+ /**
59
+ * Cache hint: when true, the producing route is asking the provider
60
+ * to mark this message's content with a cache breakpoint so the
61
+ * full prefix becomes cacheable on a subsequent request. Anthropic
62
+ * applies `cache_control: ephemeral`; Vertex Gemini ignores the
63
+ * flag (its prefix cache works automatically).
64
+ */
65
+ cached?: boolean;
58
66
  } | {
59
67
  role: 'assistant';
60
68
  /** Free text the model emitted (zero-or-more text blocks joined as-is). */
@@ -69,6 +77,8 @@ type NormalizedMessage = {
69
77
  * thought_signature`. Other adapters can ignore.
70
78
  */
71
79
  providerData?: unknown;
80
+ /** See `user.cached`. */
81
+ cached?: boolean;
72
82
  } | {
73
83
  role: 'tool';
74
84
  results: NormalizedToolResult[];
@@ -155,6 +165,16 @@ type AgentInput<S = unknown> = {
155
165
  systemBlocks: SystemBlock[];
156
166
  /** Constructed ToolProvider — caller resolves the right one via toolProviders[id].createProvider({...}). */
157
167
  provider: ToolProvider;
168
+ /**
169
+ * Conversation history to seed the prompt with, in chronological order.
170
+ * Hosts pass this to give the model memory across turns in a chat session
171
+ * (so a follow-up like "summarize that" resolves the antecedent). The
172
+ * route handler is responsible for fetching prior `chat_messages` and
173
+ * normalising them; see `historyToNormalizedMessages` in `./history`.
174
+ * Tool-call provenance is intentionally not replayed — assistant turns
175
+ * here should be plain text only.
176
+ */
177
+ priorMessages?: NormalizedMessage[];
158
178
  /** Optional caps. Default both. */
159
179
  maxToolTurns?: number;
160
180
  maxOutputTokens?: number;
@@ -398,7 +418,7 @@ declare function createChatSessionsRoutes<S>(ctx: ChatSessionsRouteCtx<S>): {
398
418
  /**
399
419
  * `/api/admin/ai-settings` route factory — global AI configuration (super_admin only).
400
420
  *
401
- * Five patchable fields on the singleton settings row:
421
+ * Six patchable fields on the singleton settings row:
402
422
  * - `tool_provider` — vendor that drives the agent tool loop. Validated
403
423
  * against the registered `toolProviders` registry passed in via ctx.
404
424
  * - `gcp_location` — the Vertex region every provider call hits. Stays
@@ -417,6 +437,10 @@ declare function createChatSessionsRoutes<S>(ctx: ChatSessionsRouteCtx<S>): {
417
437
  * `configureAiChat({ rolePrompt })` fallback; we canonicalise an empty/
418
438
  * whitespace-only string to `null` on write so the "no override" state has
419
439
  * a single representation in storage.
440
+ * - `gcp_project_id` — admin-editable GCP project override. Empty string and
441
+ * the explicit JSON `null` both clear the override back to the host's
442
+ * static `VertexPort.projectId`. Validated against GCP's project-id
443
+ * format (`[a-z][-a-z0-9]{4,28}[a-z0-9]`) when non-null.
420
444
  *
421
445
  * Wire format is snake_case to preserve byte-for-byte parity with the
422
446
  * host route the package replaces — existing host UIs keep working
@@ -1,5 +1,5 @@
1
- import { S as SystemBlock, T as ToolSchema, a as ToolContext, b as ToolDefinition, P as PresentPayload, c as PersistencePort, A as AuthPort, d as ScopePort, e as ToolsPort, V as VertexPort, L as LoggerPort } from '../types-CQntnyDJ.js';
2
- export { f as AiSettings, g as AiSettingsPatch, h as AppendMessageInput, i as AuthFail, j as AuthOk, k as AuthResult, B as Block, C as ChartSpec, l as ChatMessage, m as ChatMessageRole, n as ChatSession, o as CreateSessionInput, p as ListSessionsOpts, q as TERMINAL_TOOL_NAME, r as ToolResult, s as err, t as ok } from '../types-CQntnyDJ.js';
1
+ import { S as SystemBlock, T as ToolSchema, a as ToolContext, b as ToolDefinition, P as PresentPayload, c as PersistencePort, A as AuthPort, d as ScopePort, e as ToolsPort, V as VertexPort, L as LoggerPort } from '../types-BnwUkqKb.js';
2
+ export { f as AiSettings, g as AiSettingsPatch, h as AppendMessageInput, i as AuthFail, j as AuthOk, k as AuthResult, B as Block, C as ChartSpec, l as ChatMessage, m as ChatMessageRole, n as ChatSession, o as CreateSessionInput, p as ListSessionsOpts, q as TERMINAL_TOOL_NAME, r as ToolResult, s as err, t as ok } from '../types-BnwUkqKb.js';
3
3
  import { GoogleAuth } from 'google-auth-library';
4
4
  export { GoogleAuth } from 'google-auth-library';
5
5
  import 'zod';
@@ -55,6 +55,14 @@ type NormalizedToolResult = {
55
55
  type NormalizedMessage = {
56
56
  role: 'user';
57
57
  text: string;
58
+ /**
59
+ * Cache hint: when true, the producing route is asking the provider
60
+ * to mark this message's content with a cache breakpoint so the
61
+ * full prefix becomes cacheable on a subsequent request. Anthropic
62
+ * applies `cache_control: ephemeral`; Vertex Gemini ignores the
63
+ * flag (its prefix cache works automatically).
64
+ */
65
+ cached?: boolean;
58
66
  } | {
59
67
  role: 'assistant';
60
68
  /** Free text the model emitted (zero-or-more text blocks joined as-is). */
@@ -69,6 +77,8 @@ type NormalizedMessage = {
69
77
  * thought_signature`. Other adapters can ignore.
70
78
  */
71
79
  providerData?: unknown;
80
+ /** See `user.cached`. */
81
+ cached?: boolean;
72
82
  } | {
73
83
  role: 'tool';
74
84
  results: NormalizedToolResult[];
@@ -155,6 +165,16 @@ type AgentInput<S = unknown> = {
155
165
  systemBlocks: SystemBlock[];
156
166
  /** Constructed ToolProvider — caller resolves the right one via toolProviders[id].createProvider({...}). */
157
167
  provider: ToolProvider;
168
+ /**
169
+ * Conversation history to seed the prompt with, in chronological order.
170
+ * Hosts pass this to give the model memory across turns in a chat session
171
+ * (so a follow-up like "summarize that" resolves the antecedent). The
172
+ * route handler is responsible for fetching prior `chat_messages` and
173
+ * normalising them; see `historyToNormalizedMessages` in `./history`.
174
+ * Tool-call provenance is intentionally not replayed — assistant turns
175
+ * here should be plain text only.
176
+ */
177
+ priorMessages?: NormalizedMessage[];
158
178
  /** Optional caps. Default both. */
159
179
  maxToolTurns?: number;
160
180
  maxOutputTokens?: number;
@@ -398,7 +418,7 @@ declare function createChatSessionsRoutes<S>(ctx: ChatSessionsRouteCtx<S>): {
398
418
  /**
399
419
  * `/api/admin/ai-settings` route factory — global AI configuration (super_admin only).
400
420
  *
401
- * Five patchable fields on the singleton settings row:
421
+ * Six patchable fields on the singleton settings row:
402
422
  * - `tool_provider` — vendor that drives the agent tool loop. Validated
403
423
  * against the registered `toolProviders` registry passed in via ctx.
404
424
  * - `gcp_location` — the Vertex region every provider call hits. Stays
@@ -417,6 +437,10 @@ declare function createChatSessionsRoutes<S>(ctx: ChatSessionsRouteCtx<S>): {
417
437
  * `configureAiChat({ rolePrompt })` fallback; we canonicalise an empty/
418
438
  * whitespace-only string to `null` on write so the "no override" state has
419
439
  * a single representation in storage.
440
+ * - `gcp_project_id` — admin-editable GCP project override. Empty string and
441
+ * the explicit JSON `null` both clear the override back to the host's
442
+ * static `VertexPort.projectId`. Validated against GCP's project-id
443
+ * format (`[a-z][-a-z0-9]{4,28}[a-z0-9]`) when non-null.
420
444
  *
421
445
  * Wire format is snake_case to preserve byte-for-byte parity with the
422
446
  * host route the package replaces — existing host UIs keep working
@@ -23,7 +23,10 @@ async function runAgent(input) {
23
23
  const maxOutputTokens = input.maxOutputTokens ?? DEFAULT_MAX_OUTPUT_TOKENS;
24
24
  const transcript = [];
25
25
  transcript.push({ kind: "user", text: input.question });
26
- const messages = [{ role: "user", text: input.question }];
26
+ const messages = [
27
+ ...input.priorMessages ?? [],
28
+ { role: "user", text: input.question }
29
+ ];
27
30
  const system = input.systemBlocks;
28
31
  const toolSchemas = Object.values(input.tools).map((t) => t.schema);
29
32
  let toolCallCount = 0;
@@ -216,11 +219,28 @@ function toAnthropicMessages(messages) {
216
219
  const out = [];
217
220
  for (const msg of messages) {
218
221
  if (msg.role === "user") {
219
- out.push({ role: "user", content: msg.text });
222
+ if (msg.cached) {
223
+ out.push({
224
+ role: "user",
225
+ content: [
226
+ {
227
+ type: "text",
228
+ text: msg.text,
229
+ cache_control: { type: "ephemeral" }
230
+ }
231
+ ]
232
+ });
233
+ } else {
234
+ out.push({ role: "user", content: msg.text });
235
+ }
220
236
  } else if (msg.role === "assistant") {
221
237
  const blocks = [];
222
238
  if (msg.text) {
223
- blocks.push({ type: "text", text: msg.text });
239
+ const textBlock = { type: "text", text: msg.text };
240
+ if (msg.cached && msg.toolCalls.length === 0) {
241
+ textBlock.cache_control = { type: "ephemeral" };
242
+ }
243
+ blocks.push(textBlock);
224
244
  }
225
245
  for (const tc of msg.toolCalls) {
226
246
  blocks.push({
@@ -572,6 +592,87 @@ var toolProviders = [
572
592
  function getToolProvider(id) {
573
593
  return toolProviders.find((p) => p.id === id);
574
594
  }
595
+
596
+ // src/server/history.ts
597
+ var DEFAULT_MAX_HISTORY_PAIRS = 20;
598
+ var DEFAULT_MAX_TEXT_CHARS = 4e3;
599
+ function historyToNormalizedMessages(rows, opts = {}) {
600
+ const maxPairs = opts.maxPairs ?? DEFAULT_MAX_HISTORY_PAIRS;
601
+ const maxTextChars = opts.maxTextChars ?? DEFAULT_MAX_TEXT_CHARS;
602
+ const pairs = [];
603
+ let i = 0;
604
+ while (i < rows.length) {
605
+ const row = rows[i];
606
+ if (row.role !== "user" || !row.question) {
607
+ i += 1;
608
+ continue;
609
+ }
610
+ const next = rows[i + 1];
611
+ if (next?.role !== "assistant") {
612
+ i += 1;
613
+ continue;
614
+ }
615
+ const assistantText = truncate(
616
+ assistantMessageToText(next),
617
+ maxTextChars
618
+ );
619
+ if (assistantText) {
620
+ pairs.push([
621
+ { role: "user", text: truncate(row.question, maxTextChars) },
622
+ { role: "assistant", text: assistantText, toolCalls: [] }
623
+ ]);
624
+ }
625
+ i += 2;
626
+ }
627
+ const kept = maxPairs > 0 ? pairs.slice(-maxPairs) : pairs;
628
+ return kept.flat();
629
+ }
630
+ function truncate(text, max) {
631
+ if (max <= 0 || text.length <= max) return text;
632
+ return text.slice(0, max);
633
+ }
634
+ function assistantMessageToText(row) {
635
+ if (row.errorJson) return "";
636
+ const proseText = proseToText(row.prose);
637
+ if (proseText) return proseText;
638
+ const blockText = blocksToText(row.blocks);
639
+ return blockText;
640
+ }
641
+ function proseToText(prose) {
642
+ if (!prose || typeof prose !== "object") return "";
643
+ const entries = Object.entries(prose).map(([k, v]) => [Number(k), typeof v === "string" ? v : ""]).filter(([k, v]) => Number.isFinite(k) && v.length > 0).sort(([a], [b]) => a - b);
644
+ return entries.map(([, v]) => v).join("\n\n").trim();
645
+ }
646
+ function blocksToText(blocks) {
647
+ if (!Array.isArray(blocks)) return "";
648
+ const parts = [];
649
+ for (const raw of blocks) {
650
+ if (!raw || typeof raw !== "object") continue;
651
+ const b = raw;
652
+ switch (b.kind) {
653
+ case "paragraph_brief": {
654
+ const facts = (b.key_facts ?? []).filter((f) => f && f.trim());
655
+ if (b.topic) parts.push(b.topic);
656
+ if (facts.length) parts.push(facts.join("\n"));
657
+ break;
658
+ }
659
+ case "list": {
660
+ const items = (b.items ?? []).filter((s) => s && s.trim());
661
+ if (b.title) parts.push(b.title);
662
+ if (items.length) parts.push(items.map((s) => `- ${s}`).join("\n"));
663
+ break;
664
+ }
665
+ case "chart":
666
+ case "table":
667
+ if (b.title) parts.push(`[${b.title}]`);
668
+ break;
669
+ case "callout":
670
+ if (b.text) parts.push(b.text);
671
+ break;
672
+ }
673
+ }
674
+ return parts.join("\n\n").trim();
675
+ }
575
676
  function vertexHost2(location) {
576
677
  return location === "global" ? "aiplatform.googleapis.com" : `${location}-aiplatform.googleapis.com`;
577
678
  }
@@ -912,13 +1013,25 @@ function createAgentCustomRoutes(ctx) {
912
1013
  const rawChatSessionId = body?.chatSessionId;
913
1014
  const incomingChatSessionId = typeof rawChatSessionId === "number" && Number.isInteger(rawChatSessionId) ? rawChatSessionId : null;
914
1015
  const aiSettings = await persistence.getAiSettings();
1016
+ const effectiveProjectId = aiSettings.gcpProjectId ?? vertex.projectId;
915
1017
  let chatSessionId;
1018
+ let priorMessages = [];
916
1019
  if (incomingChatSessionId !== null) {
917
1020
  const owned = await persistence.getSession(incomingChatSessionId, userId);
918
1021
  if (!owned) {
919
1022
  return jsonError(404, "NOT_FOUND", "Chat session not found.");
920
1023
  }
921
1024
  chatSessionId = owned.id;
1025
+ const stored = await persistence.listMessagesForSession(chatSessionId, userId);
1026
+ priorMessages = historyToNormalizedMessages(stored);
1027
+ if (priorMessages.length > 0) {
1028
+ const last = priorMessages[priorMessages.length - 1];
1029
+ if (last.role === "assistant" && last.toolCalls.length === 0) {
1030
+ priorMessages[priorMessages.length - 1] = { ...last, cached: true };
1031
+ } else if (last.role === "user") {
1032
+ priorMessages[priorMessages.length - 1] = { ...last, cached: true };
1033
+ }
1034
+ }
922
1035
  } else {
923
1036
  const created = await persistence.createSession({
924
1037
  userId,
@@ -955,7 +1068,7 @@ function createAgentCustomRoutes(ctx) {
955
1068
  }
956
1069
  const provider = def.createProvider({
957
1070
  auth: vertex.auth,
958
- projectId: vertex.projectId,
1071
+ projectId: effectiveProjectId,
959
1072
  defaultLocation: vertex.defaultLocation,
960
1073
  modelIds: vertex.modelIds,
961
1074
  location: aiSettings.gcpLocation
@@ -1006,6 +1119,7 @@ data: ${JSON.stringify(data)}
1006
1119
  send("meta", { chatSessionId, scopeLabel });
1007
1120
  const agentResult = await runAgent({
1008
1121
  question,
1122
+ priorMessages,
1009
1123
  ctx: toolContext,
1010
1124
  tools: tools.tools,
1011
1125
  systemBlocks,
@@ -1033,7 +1147,7 @@ data: ${JSON.stringify(data)}
1033
1147
  }
1034
1148
  for await (const token of narratorFn({
1035
1149
  auth: vertex.auth,
1036
- projectId: vertex.projectId,
1150
+ projectId: effectiveProjectId,
1037
1151
  location: aiSettings.gcpLocation,
1038
1152
  modelId: narratorModelId,
1039
1153
  maxTokens: aiSettings.maxOutputTokens,
@@ -1224,7 +1338,17 @@ function createAgentVercelRoutes(ctx) {
1224
1338
  if (short) return short;
1225
1339
  }
1226
1340
  const body = await req.json().catch(() => null);
1227
- const question = typeof body?.question === "string" ? body.question.trim() : "";
1341
+ let question = typeof body?.question === "string" ? body.question.trim() : "";
1342
+ if (!question && Array.isArray(body?.messages)) {
1343
+ const msgs = body.messages;
1344
+ for (let i = msgs.length - 1; i >= 0; i -= 1) {
1345
+ const m = msgs[i];
1346
+ if (m && m.role === "user" && typeof m.content === "string") {
1347
+ question = m.content.trim();
1348
+ break;
1349
+ }
1350
+ }
1351
+ }
1228
1352
  if (!question) {
1229
1353
  return jsonError2(
1230
1354
  400,
@@ -1237,7 +1361,9 @@ function createAgentVercelRoutes(ctx) {
1237
1361
  const rawModel = body?.model;
1238
1362
  const requestedModel = typeof rawModel === "string" && VALID_MODELS.has(rawModel) ? rawModel : null;
1239
1363
  const aiSettings = await persistence.getAiSettings();
1364
+ const effectiveProjectId = aiSettings.gcpProjectId ?? vertex.projectId;
1240
1365
  let chatSessionId;
1366
+ let priorMessages = [];
1241
1367
  if (incomingChatSessionId !== null) {
1242
1368
  const owned = await persistence.getSession(
1243
1369
  incomingChatSessionId,
@@ -1247,6 +1373,11 @@ function createAgentVercelRoutes(ctx) {
1247
1373
  return jsonError2(404, "NOT_FOUND", "Chat session not found.");
1248
1374
  }
1249
1375
  chatSessionId = owned.id;
1376
+ const stored = await persistence.listMessagesForSession(
1377
+ chatSessionId,
1378
+ userId
1379
+ );
1380
+ priorMessages = historyToNormalizedMessages(stored);
1250
1381
  } else {
1251
1382
  const created = await persistence.createSession({
1252
1383
  userId,
@@ -1308,18 +1439,24 @@ function createAgentVercelRoutes(ctx) {
1308
1439
  });
1309
1440
  const system = systemBlocks.map((b) => b.text).join("\n\n");
1310
1441
  const model = provider === "claude" ? createVertexAnthropic({
1311
- project: vertex.projectId,
1442
+ project: effectiveProjectId,
1312
1443
  location: vertex.defaultLocation,
1313
1444
  googleAuthOptions: {}
1314
1445
  })(vertex.modelIds.claude) : createVertex({
1315
- project: vertex.projectId,
1446
+ project: effectiveProjectId,
1316
1447
  location: aiSettings.gcpLocation,
1317
1448
  googleAuthOptions: {}
1318
1449
  })(vertex.modelIds.gemini);
1450
+ const priorCoreMessages = priorMessages.filter(
1451
+ (m) => m.role === "user" || m.role === "assistant"
1452
+ ).map((m) => ({ role: m.role, content: m.text }));
1319
1453
  const result = streamText({
1320
1454
  model,
1321
1455
  system,
1322
- messages: [{ role: "user", content: question }],
1456
+ messages: [
1457
+ ...priorCoreMessages,
1458
+ { role: "user", content: question }
1459
+ ],
1323
1460
  tools: vercelTools,
1324
1461
  maxSteps: 12,
1325
1462
  maxTokens: aiSettings.maxOutputTokens,
@@ -1613,6 +1750,7 @@ function createChatSessionsRoutes(ctx) {
1613
1750
  var VALID_LOCATIONS = ["us-east5", "global"];
1614
1751
  var MIN_MAX_OUTPUT_TOKENS = 256;
1615
1752
  var MAX_MAX_OUTPUT_TOKENS = 64e3;
1753
+ var GCP_PROJECT_ID_REGEX = /^[a-z][-a-z0-9]{4,28}[a-z0-9]$/;
1616
1754
  function isStringRecord(v) {
1617
1755
  return typeof v === "object" && v !== null && !Array.isArray(v);
1618
1756
  }
@@ -1629,6 +1767,7 @@ function toWire(settings) {
1629
1767
  chat_interface: settings.chatInterface,
1630
1768
  max_output_tokens: settings.maxOutputTokens,
1631
1769
  role_prompt: settings.rolePrompt,
1770
+ gcp_project_id: settings.gcpProjectId,
1632
1771
  updated_at: settings.updatedAt ? settings.updatedAt.toISOString() : null,
1633
1772
  updated_by_user_id: settings.updatedByUserId
1634
1773
  };
@@ -1726,11 +1865,28 @@ function createAdminSettingsRoutes(ctx) {
1726
1865
  return jsonResponse({ error: "invalid_role_prompt" }, 400);
1727
1866
  }
1728
1867
  }
1729
- if (patch.toolProvider === void 0 && patch.gcpLocation === void 0 && patch.chatInterface === void 0 && patch.maxOutputTokens === void 0 && !("rolePrompt" in patch)) {
1868
+ if ("gcp_project_id" in body) {
1869
+ const v = body.gcp_project_id;
1870
+ if (v === null) {
1871
+ patch.gcpProjectId = null;
1872
+ } else if (typeof v === "string") {
1873
+ const trimmed = v.trim();
1874
+ if (trimmed === "") {
1875
+ patch.gcpProjectId = null;
1876
+ } else if (trimmed.length > 64 || !GCP_PROJECT_ID_REGEX.test(trimmed)) {
1877
+ return jsonResponse({ error: "invalid_gcp_project_id" }, 400);
1878
+ } else {
1879
+ patch.gcpProjectId = trimmed;
1880
+ }
1881
+ } else {
1882
+ return jsonResponse({ error: "invalid_gcp_project_id" }, 400);
1883
+ }
1884
+ }
1885
+ if (patch.toolProvider === void 0 && patch.gcpLocation === void 0 && patch.chatInterface === void 0 && patch.maxOutputTokens === void 0 && !("rolePrompt" in patch) && !("gcpProjectId" in patch)) {
1730
1886
  return jsonResponse(
1731
1887
  {
1732
1888
  error: "empty_patch",
1733
- message: "Body must set at least one of tool_provider, gcp_location, chat_interface, max_output_tokens, role_prompt."
1889
+ message: "Body must set at least one of tool_provider, gcp_location, chat_interface, max_output_tokens, role_prompt, gcp_project_id."
1734
1890
  },
1735
1891
  400
1736
1892
  );
@@ -1791,9 +1947,10 @@ function configureAiChat(opts) {
1791
1947
  `Unknown tool provider '${providerId ?? settings.toolProvider}'. Registered: ${toolProviders2.map((p) => p.id).join(", ")}.`
1792
1948
  );
1793
1949
  }
1950
+ const effectiveProjectId = settings.gcpProjectId ?? opts.vertex.projectId;
1794
1951
  const provider = def.createProvider({
1795
1952
  auth: opts.vertex.auth,
1796
- projectId: opts.vertex.projectId,
1953
+ projectId: effectiveProjectId,
1797
1954
  defaultLocation: opts.vertex.defaultLocation,
1798
1955
  modelIds: opts.vertex.modelIds,
1799
1956
  location: location ?? settings.gcpLocation