@objectstack/service-ai 6.8.1 → 6.9.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.
package/dist/index.d.cts CHANGED
@@ -308,6 +308,20 @@ declare class AIService implements IAIService {
308
308
  private readonly pendingDispatchers;
309
309
  /** Data engine for `ai_pending_actions` persistence. */
310
310
  private readonly dataEngine?;
311
+ /**
312
+ * Auto-title configuration. When `enabled`, the first `chatWithTools` /
313
+ * `streamChatWithTools` call against a still-untitled conversation
314
+ * triggers a one-shot LLM call (fire-and-forget) that summarises the
315
+ * exchange into a short title and PATCHes it onto the conversation row.
316
+ *
317
+ * Defaults to disabled — `AIServicePlugin` flips this on (with values
318
+ * read from the `ai` settings namespace) once the kernel is ready.
319
+ * Keeping the default off means unit tests don't accidentally make
320
+ * extra adapter calls.
321
+ */
322
+ private titleGeneration;
323
+ /** Tracks conversations we've already attempted to title to avoid duplicate LLM calls. */
324
+ private readonly titledConversations;
311
325
  constructor(config?: AIServiceConfig);
312
326
  /** The name of the active LLM adapter. */
313
327
  get adapterName(): string;
@@ -318,6 +332,36 @@ declare class AIService implements IAIService {
318
332
  * subsequent calls go through the new adapter.
319
333
  */
320
334
  setAdapter(next: LLMAdapter): void;
335
+ /**
336
+ * Configure conversation auto-titling. Called by `AIServicePlugin`
337
+ * when the `ai` settings namespace is bound (so admins can toggle
338
+ * the feature live from the Setup app without a restart).
339
+ *
340
+ * - `enabled=false` is the safe default for unit tests and the
341
+ * memory adapter (which would just echo the prompt back as a title).
342
+ * - `maxLength` is enforced both in the prompt and as a hard server-side
343
+ * `slice()` so a misbehaving model can't write a 4 KB "title".
344
+ */
345
+ setTitleGenerationConfig(config: {
346
+ enabled: boolean;
347
+ maxLength?: number;
348
+ }): void;
349
+ /**
350
+ * Best-effort title generation for a conversation. Idempotent per
351
+ * `AIService` instance — once attempted, the id is recorded in
352
+ * `titledConversations` so subsequent chats don't burn extra tokens
353
+ * re-summarising the same thread.
354
+ *
355
+ * Skips when:
356
+ * - feature disabled
357
+ * - conversation already has a non-empty title
358
+ * - conversation has fewer than 2 messages (no exchange to summarise)
359
+ * - conversation has no user message
360
+ *
361
+ * Failures are logged at debug level and swallowed — title generation
362
+ * is purely cosmetic and must never break chat.
363
+ */
364
+ summarizeConversation(conversationId: string): Promise<void>;
321
365
  /**
322
366
  * Best-effort auto-creation of a conversation when the caller did not
323
367
  * supply one but did supply an actor we can attribute the chat to.
@@ -2258,6 +2302,14 @@ declare const AiConversationObject: Omit<{
2258
2302
  } | undefined;
2259
2303
  recordTypes?: string[] | undefined;
2260
2304
  sharingModel?: "private" | "read" | "full" | "read_write" | undefined;
2305
+ publicSharing?: {
2306
+ enabled: boolean;
2307
+ allowedAudiences?: ("email" | "public" | "link_only" | "signed_in")[] | undefined;
2308
+ allowedPermissions?: ("edit" | "view" | "comment")[] | undefined;
2309
+ maxExpiryDays?: number | undefined;
2310
+ redactFields?: string[] | undefined;
2311
+ eligibility?: string | undefined;
2312
+ } | undefined;
2261
2313
  keyPrefix?: string | undefined;
2262
2314
  detail?: {
2263
2315
  [x: string]: unknown;
@@ -2348,6 +2400,13 @@ declare const AiConversationObject: Omit<{
2348
2400
  readonly icon: "message-square";
2349
2401
  readonly isSystem: true;
2350
2402
  readonly description: "Persistent AI conversation metadata";
2403
+ readonly publicSharing: {
2404
+ readonly enabled: true;
2405
+ readonly allowedAudiences: ["link_only", "signed_in"];
2406
+ readonly allowedPermissions: ["view"];
2407
+ readonly maxExpiryDays: 90;
2408
+ readonly redactFields: ["metadata"];
2409
+ };
2351
2410
  readonly fields: {
2352
2411
  readonly id: {
2353
2412
  readonly readonly?: boolean | undefined;
@@ -4187,6 +4246,14 @@ declare const AiMessageObject: Omit<{
4187
4246
  } | undefined;
4188
4247
  recordTypes?: string[] | undefined;
4189
4248
  sharingModel?: "private" | "read" | "full" | "read_write" | undefined;
4249
+ publicSharing?: {
4250
+ enabled: boolean;
4251
+ allowedAudiences?: ("email" | "public" | "link_only" | "signed_in")[] | undefined;
4252
+ allowedPermissions?: ("edit" | "view" | "comment")[] | undefined;
4253
+ maxExpiryDays?: number | undefined;
4254
+ redactFields?: string[] | undefined;
4255
+ eligibility?: string | undefined;
4256
+ } | undefined;
4190
4257
  keyPrefix?: string | undefined;
4191
4258
  detail?: {
4192
4259
  [x: string]: unknown;
@@ -6118,6 +6185,14 @@ declare const AiTraceObject: Omit<{
6118
6185
  } | undefined;
6119
6186
  recordTypes?: string[] | undefined;
6120
6187
  sharingModel?: "private" | "read" | "full" | "read_write" | undefined;
6188
+ publicSharing?: {
6189
+ enabled: boolean;
6190
+ allowedAudiences?: ("email" | "public" | "link_only" | "signed_in")[] | undefined;
6191
+ allowedPermissions?: ("edit" | "view" | "comment")[] | undefined;
6192
+ maxExpiryDays?: number | undefined;
6193
+ redactFields?: string[] | undefined;
6194
+ eligibility?: string | undefined;
6195
+ } | undefined;
6121
6196
  keyPrefix?: string | undefined;
6122
6197
  detail?: {
6123
6198
  [x: string]: unknown;
package/dist/index.d.ts CHANGED
@@ -308,6 +308,20 @@ declare class AIService implements IAIService {
308
308
  private readonly pendingDispatchers;
309
309
  /** Data engine for `ai_pending_actions` persistence. */
310
310
  private readonly dataEngine?;
311
+ /**
312
+ * Auto-title configuration. When `enabled`, the first `chatWithTools` /
313
+ * `streamChatWithTools` call against a still-untitled conversation
314
+ * triggers a one-shot LLM call (fire-and-forget) that summarises the
315
+ * exchange into a short title and PATCHes it onto the conversation row.
316
+ *
317
+ * Defaults to disabled — `AIServicePlugin` flips this on (with values
318
+ * read from the `ai` settings namespace) once the kernel is ready.
319
+ * Keeping the default off means unit tests don't accidentally make
320
+ * extra adapter calls.
321
+ */
322
+ private titleGeneration;
323
+ /** Tracks conversations we've already attempted to title to avoid duplicate LLM calls. */
324
+ private readonly titledConversations;
311
325
  constructor(config?: AIServiceConfig);
312
326
  /** The name of the active LLM adapter. */
313
327
  get adapterName(): string;
@@ -318,6 +332,36 @@ declare class AIService implements IAIService {
318
332
  * subsequent calls go through the new adapter.
319
333
  */
320
334
  setAdapter(next: LLMAdapter): void;
335
+ /**
336
+ * Configure conversation auto-titling. Called by `AIServicePlugin`
337
+ * when the `ai` settings namespace is bound (so admins can toggle
338
+ * the feature live from the Setup app without a restart).
339
+ *
340
+ * - `enabled=false` is the safe default for unit tests and the
341
+ * memory adapter (which would just echo the prompt back as a title).
342
+ * - `maxLength` is enforced both in the prompt and as a hard server-side
343
+ * `slice()` so a misbehaving model can't write a 4 KB "title".
344
+ */
345
+ setTitleGenerationConfig(config: {
346
+ enabled: boolean;
347
+ maxLength?: number;
348
+ }): void;
349
+ /**
350
+ * Best-effort title generation for a conversation. Idempotent per
351
+ * `AIService` instance — once attempted, the id is recorded in
352
+ * `titledConversations` so subsequent chats don't burn extra tokens
353
+ * re-summarising the same thread.
354
+ *
355
+ * Skips when:
356
+ * - feature disabled
357
+ * - conversation already has a non-empty title
358
+ * - conversation has fewer than 2 messages (no exchange to summarise)
359
+ * - conversation has no user message
360
+ *
361
+ * Failures are logged at debug level and swallowed — title generation
362
+ * is purely cosmetic and must never break chat.
363
+ */
364
+ summarizeConversation(conversationId: string): Promise<void>;
321
365
  /**
322
366
  * Best-effort auto-creation of a conversation when the caller did not
323
367
  * supply one but did supply an actor we can attribute the chat to.
@@ -2258,6 +2302,14 @@ declare const AiConversationObject: Omit<{
2258
2302
  } | undefined;
2259
2303
  recordTypes?: string[] | undefined;
2260
2304
  sharingModel?: "private" | "read" | "full" | "read_write" | undefined;
2305
+ publicSharing?: {
2306
+ enabled: boolean;
2307
+ allowedAudiences?: ("email" | "public" | "link_only" | "signed_in")[] | undefined;
2308
+ allowedPermissions?: ("edit" | "view" | "comment")[] | undefined;
2309
+ maxExpiryDays?: number | undefined;
2310
+ redactFields?: string[] | undefined;
2311
+ eligibility?: string | undefined;
2312
+ } | undefined;
2261
2313
  keyPrefix?: string | undefined;
2262
2314
  detail?: {
2263
2315
  [x: string]: unknown;
@@ -2348,6 +2400,13 @@ declare const AiConversationObject: Omit<{
2348
2400
  readonly icon: "message-square";
2349
2401
  readonly isSystem: true;
2350
2402
  readonly description: "Persistent AI conversation metadata";
2403
+ readonly publicSharing: {
2404
+ readonly enabled: true;
2405
+ readonly allowedAudiences: ["link_only", "signed_in"];
2406
+ readonly allowedPermissions: ["view"];
2407
+ readonly maxExpiryDays: 90;
2408
+ readonly redactFields: ["metadata"];
2409
+ };
2351
2410
  readonly fields: {
2352
2411
  readonly id: {
2353
2412
  readonly readonly?: boolean | undefined;
@@ -4187,6 +4246,14 @@ declare const AiMessageObject: Omit<{
4187
4246
  } | undefined;
4188
4247
  recordTypes?: string[] | undefined;
4189
4248
  sharingModel?: "private" | "read" | "full" | "read_write" | undefined;
4249
+ publicSharing?: {
4250
+ enabled: boolean;
4251
+ allowedAudiences?: ("email" | "public" | "link_only" | "signed_in")[] | undefined;
4252
+ allowedPermissions?: ("edit" | "view" | "comment")[] | undefined;
4253
+ maxExpiryDays?: number | undefined;
4254
+ redactFields?: string[] | undefined;
4255
+ eligibility?: string | undefined;
4256
+ } | undefined;
4190
4257
  keyPrefix?: string | undefined;
4191
4258
  detail?: {
4192
4259
  [x: string]: unknown;
@@ -6118,6 +6185,14 @@ declare const AiTraceObject: Omit<{
6118
6185
  } | undefined;
6119
6186
  recordTypes?: string[] | undefined;
6120
6187
  sharingModel?: "private" | "read" | "full" | "read_write" | undefined;
6188
+ publicSharing?: {
6189
+ enabled: boolean;
6190
+ allowedAudiences?: ("email" | "public" | "link_only" | "signed_in")[] | undefined;
6191
+ allowedPermissions?: ("edit" | "view" | "comment")[] | undefined;
6192
+ maxExpiryDays?: number | undefined;
6193
+ redactFields?: string[] | undefined;
6194
+ eligibility?: string | undefined;
6195
+ } | undefined;
6121
6196
  keyPrefix?: string | undefined;
6122
6197
  detail?: {
6123
6198
  [x: string]: unknown;
package/dist/index.js CHANGED
@@ -1450,6 +1450,36 @@ function finishPart(result) {
1450
1450
  rawFinishReason: "stop"
1451
1451
  };
1452
1452
  }
1453
+ function extractMessageText(message) {
1454
+ const c = message.content;
1455
+ if (typeof c === "string") return c;
1456
+ if (!Array.isArray(c)) return "";
1457
+ const parts = [];
1458
+ for (const part of c) {
1459
+ if (typeof part === "string") {
1460
+ parts.push(part);
1461
+ } else if (part && typeof part === "object") {
1462
+ const p = part;
1463
+ if (p.type === "text" && typeof p.text === "string") parts.push(p.text);
1464
+ }
1465
+ }
1466
+ return parts.join(" ").trim();
1467
+ }
1468
+ function cleanTitle(raw, maxLen) {
1469
+ let s = raw.replace(/\s+/g, " ").trim();
1470
+ s = s.replace(/^[\s"'“”‘’`「『((\[【]+/, "").replace(/[\s"'“”‘’`」』))\]】]+$/, "");
1471
+ s = s.replace(/^(title|标题|主题)\s*[::]\s*/i, "");
1472
+ s = s.replace(/^[\s"'“”‘’`「『((\[【]+/, "").replace(/[\s"'“”‘’`」』))\]】]+$/, "");
1473
+ s = s.replace(/[.。!!??,,;;::]+$/, "").trim();
1474
+ if (!s) return "";
1475
+ if (s.length <= maxLen) return s;
1476
+ if (/^[\x00-\x7F]+$/.test(s)) {
1477
+ const cut = s.slice(0, maxLen);
1478
+ const lastSpace = cut.lastIndexOf(" ");
1479
+ return lastSpace > maxLen / 2 ? cut.slice(0, lastSpace) : cut;
1480
+ }
1481
+ return s.slice(0, maxLen);
1482
+ }
1453
1483
  var _AIService = class _AIService {
1454
1484
  constructor(config = {}) {
1455
1485
  /**
@@ -1459,6 +1489,23 @@ var _AIService = class _AIService {
1459
1489
  * through `approvePendingAction()`.
1460
1490
  */
1461
1491
  this.pendingDispatchers = /* @__PURE__ */ new Map();
1492
+ /**
1493
+ * Auto-title configuration. When `enabled`, the first `chatWithTools` /
1494
+ * `streamChatWithTools` call against a still-untitled conversation
1495
+ * triggers a one-shot LLM call (fire-and-forget) that summarises the
1496
+ * exchange into a short title and PATCHes it onto the conversation row.
1497
+ *
1498
+ * Defaults to disabled — `AIServicePlugin` flips this on (with values
1499
+ * read from the `ai` settings namespace) once the kernel is ready.
1500
+ * Keeping the default off means unit tests don't accidentally make
1501
+ * extra adapter calls.
1502
+ */
1503
+ this.titleGeneration = {
1504
+ enabled: false,
1505
+ maxLength: 16
1506
+ };
1507
+ /** Tracks conversations we've already attempted to title to avoid duplicate LLM calls. */
1508
+ this.titledConversations = /* @__PURE__ */ new Set();
1462
1509
  this.adapter = config.adapter ?? new MemoryLLMAdapter();
1463
1510
  this.logger = config.logger ?? createLogger({ level: "info", format: "pretty" });
1464
1511
  this.toolRegistry = config.toolRegistry ?? new ToolRegistry();
@@ -1487,6 +1534,89 @@ var _AIService = class _AIService {
1487
1534
  this.logger.info(`[AI] LLM adapter swapped: ${prev} \u2192 ${next.name}`);
1488
1535
  }
1489
1536
  }
1537
+ /**
1538
+ * Configure conversation auto-titling. Called by `AIServicePlugin`
1539
+ * when the `ai` settings namespace is bound (so admins can toggle
1540
+ * the feature live from the Setup app without a restart).
1541
+ *
1542
+ * - `enabled=false` is the safe default for unit tests and the
1543
+ * memory adapter (which would just echo the prompt back as a title).
1544
+ * - `maxLength` is enforced both in the prompt and as a hard server-side
1545
+ * `slice()` so a misbehaving model can't write a 4 KB "title".
1546
+ */
1547
+ setTitleGenerationConfig(config) {
1548
+ this.titleGeneration = {
1549
+ enabled: config.enabled,
1550
+ maxLength: Math.max(8, Math.min(80, config.maxLength ?? 16))
1551
+ };
1552
+ this.logger.debug("[AI] title generation config", this.titleGeneration);
1553
+ }
1554
+ /**
1555
+ * Best-effort title generation for a conversation. Idempotent per
1556
+ * `AIService` instance — once attempted, the id is recorded in
1557
+ * `titledConversations` so subsequent chats don't burn extra tokens
1558
+ * re-summarising the same thread.
1559
+ *
1560
+ * Skips when:
1561
+ * - feature disabled
1562
+ * - conversation already has a non-empty title
1563
+ * - conversation has fewer than 2 messages (no exchange to summarise)
1564
+ * - conversation has no user message
1565
+ *
1566
+ * Failures are logged at debug level and swallowed — title generation
1567
+ * is purely cosmetic and must never break chat.
1568
+ */
1569
+ async summarizeConversation(conversationId) {
1570
+ if (!this.titleGeneration.enabled) return;
1571
+ if (this.titledConversations.has(conversationId)) return;
1572
+ this.titledConversations.add(conversationId);
1573
+ try {
1574
+ const conv = await this.conversationService.get(conversationId);
1575
+ if (!conv) return;
1576
+ if (conv.title && conv.title.trim().length > 0) return;
1577
+ if (!conv.messages || conv.messages.length < 2) return;
1578
+ const userMsg = conv.messages.find((m) => m.role === "user");
1579
+ const assistantMsg = conv.messages.find((m) => m.role === "assistant");
1580
+ if (!userMsg) return;
1581
+ const userText = extractMessageText(userMsg);
1582
+ const assistantText = assistantMsg ? extractMessageText(assistantMsg) : "";
1583
+ if (!userText) return;
1584
+ const maxLen = this.titleGeneration.maxLength;
1585
+ const prompt = [
1586
+ {
1587
+ role: "system",
1588
+ content: `You are a title generator. Produce a SHORT (<=${maxLen} characters), noun-phrase title that captures the topic of the conversation below. Reply with the title ONLY \u2014 no quotes, no punctuation, no preamble, no trailing period. Match the language of the user's message.`
1589
+ },
1590
+ {
1591
+ role: "user",
1592
+ content: `User said:
1593
+ ${userText.slice(0, 800)}` + (assistantText ? `
1594
+
1595
+ Assistant replied:
1596
+ ${assistantText.slice(0, 800)}` : "")
1597
+ }
1598
+ ];
1599
+ const result = await this.adapter.chat(prompt, {
1600
+ temperature: 0.3,
1601
+ maxTokens: 32
1602
+ });
1603
+ const raw = (result.content ?? "").trim();
1604
+ if (!raw) return;
1605
+ const cleaned = cleanTitle(raw, maxLen);
1606
+ if (!cleaned) return;
1607
+ await this.conversationService.update(conversationId, { title: cleaned });
1608
+ this.logger.debug("[AI] auto-titled conversation", {
1609
+ conversationId,
1610
+ title: cleaned
1611
+ });
1612
+ } catch (err) {
1613
+ this.titledConversations.delete(conversationId);
1614
+ this.logger.debug("[AI] summarizeConversation failed", {
1615
+ conversationId,
1616
+ error: err instanceof Error ? err.message : String(err)
1617
+ });
1618
+ }
1619
+ }
1490
1620
  /**
1491
1621
  * Best-effort auto-creation of a conversation when the caller did not
1492
1622
  * supply one but did supply an actor we can attribute the chat to.
@@ -1687,6 +1817,7 @@ var _AIService = class _AIService {
1687
1817
  role: "assistant",
1688
1818
  content: result.content
1689
1819
  });
1820
+ void this.summarizeConversation(conversationId);
1690
1821
  }
1691
1822
  return autoCreatedConversationId ? { ...result, conversationId: autoCreatedConversationId } : result;
1692
1823
  }
@@ -1754,6 +1885,7 @@ var _AIService = class _AIService {
1754
1885
  role: "assistant",
1755
1886
  content: finalResult.content
1756
1887
  });
1888
+ void this.summarizeConversation(conversationId);
1757
1889
  }
1758
1890
  return autoCreatedConversationId ? { ...finalResult, conversationId: autoCreatedConversationId } : finalResult;
1759
1891
  }
@@ -1812,6 +1944,7 @@ var _AIService = class _AIService {
1812
1944
  role: "assistant",
1813
1945
  content: result2.content
1814
1946
  });
1947
+ void this.summarizeConversation(conversationId);
1815
1948
  }
1816
1949
  yield textDeltaPart("stream", result2.content);
1817
1950
  yield finishPart(result2);
@@ -1877,6 +2010,7 @@ var _AIService = class _AIService {
1877
2010
  role: "assistant",
1878
2011
  content: result.content
1879
2012
  });
2013
+ void this.summarizeConversation(conversationId);
1880
2014
  }
1881
2015
  yield textDeltaPart("stream", result.content);
1882
2016
  yield finishPart(result);
@@ -3363,6 +3497,19 @@ var AiConversationObject = ObjectSchema.create({
3363
3497
  icon: "message-square",
3364
3498
  isSystem: true,
3365
3499
  description: "Persistent AI conversation metadata",
3500
+ // Enable Notion / Figma-style "anyone with the link" sharing.
3501
+ // The platform's plugin-sharing service exposes the share-link UI
3502
+ // and REST surface as soon as this flag is set; no further wiring
3503
+ // is needed in service-ai. `metadata` is redacted so internal
3504
+ // tracking payloads (model token counts, source app context) do not
3505
+ // leak into public shares.
3506
+ publicSharing: {
3507
+ enabled: true,
3508
+ allowedAudiences: ["link_only", "signed_in"],
3509
+ allowedPermissions: ["view"],
3510
+ maxExpiryDays: 90,
3511
+ redactFields: ["metadata"]
3512
+ },
3366
3513
  fields: {
3367
3514
  id: Field.text({
3368
3515
  label: "Conversation ID",
@@ -5552,9 +5699,9 @@ var AIServicePlugin = class {
5552
5699
  }
5553
5700
  }
5554
5701
  const providerSpecs = {
5555
- openai: { pkg: "@ai-sdk/openai", factory: "openai", defaultModel: "gpt-4o", displayName: "OpenAI" },
5556
- anthropic: { pkg: "@ai-sdk/anthropic", factory: "anthropic", defaultModel: "claude-sonnet-4-20250514", displayName: "Anthropic" },
5557
- google: { pkg: "@ai-sdk/google", factory: "google", defaultModel: "gemini-2.0-flash", displayName: "Google" }
5702
+ openai: { pkg: "@ai-sdk/openai", factory: "openai", createFactory: "createOpenAI", defaultModel: "gpt-4o", displayName: "OpenAI" },
5703
+ anthropic: { pkg: "@ai-sdk/anthropic", factory: "anthropic", createFactory: "createAnthropic", defaultModel: "claude-sonnet-4-20250514", displayName: "Anthropic" },
5704
+ google: { pkg: "@ai-sdk/google", factory: "google", createFactory: "createGoogleGenerativeAI", defaultModel: "gemini-2.0-flash", displayName: "Google" }
5558
5705
  };
5559
5706
  const spec = providerSpecs[provider];
5560
5707
  if (!spec) return null;
@@ -5567,20 +5714,30 @@ var AIServicePlugin = class {
5567
5714
  if (!apiKey) return null;
5568
5715
  const envKey = provider === "openai" ? "OPENAI_API_KEY" : provider === "anthropic" ? "ANTHROPIC_API_KEY" : "GOOGLE_GENERATIVE_AI_API_KEY";
5569
5716
  process.env[envKey] = apiKey;
5717
+ const baseUrl = String(values[`${provider}_base_url`] ?? "").trim() || void 0;
5570
5718
  try {
5571
5719
  const mod = await import(
5572
5720
  /* webpackIgnore: true */
5573
5721
  spec.pkg
5574
5722
  );
5575
- const factory = mod[spec.factory] ?? mod.default;
5723
+ let factory = mod[spec.factory] ?? mod.default;
5724
+ if (baseUrl) {
5725
+ const createFn = mod[spec.createFactory];
5726
+ if (typeof createFn === "function") {
5727
+ factory = createFn({ apiKey, baseURL: baseUrl });
5728
+ } else {
5729
+ ctx.logger.warn(`[AI] ${spec.pkg} has no ${spec.createFactory}; baseURL override ignored.`);
5730
+ }
5731
+ }
5576
5732
  if (typeof factory !== "function") return null;
5577
5733
  const modelId = String(values[`${provider}_model`] ?? "").trim() || spec.defaultModel;
5578
5734
  const useChatApi = provider === "openai" && typeof factory.chat === "function";
5579
5735
  const model = useChatApi ? factory.chat(modelId) : factory(modelId);
5580
5736
  const apiSuffix = useChatApi ? " [chat-completions]" : "";
5737
+ const baseSuffix = baseUrl ? ` @ ${baseUrl}` : "";
5581
5738
  return {
5582
5739
  adapter: new VercelLLMAdapter({ model }),
5583
- description: `${spec.displayName} (model: ${modelId})${apiSuffix}`
5740
+ description: `${spec.displayName} (model: ${modelId})${apiSuffix}${baseSuffix}`
5584
5741
  };
5585
5742
  } catch (err) {
5586
5743
  ctx.logger.warn(
@@ -6109,7 +6266,14 @@ var AIServicePlugin = class {
6109
6266
  for (const [k, v] of Object.entries(payload.values)) {
6110
6267
  values[k] = v?.value;
6111
6268
  }
6112
- const provider = String(values.provider ?? "memory");
6269
+ const providerForTitles = String(values.provider ?? "memory");
6270
+ const titleEnabled = providerForTitles !== "memory" && values.title_generation_enabled !== false;
6271
+ const titleMaxLen = typeof values.title_max_length === "number" ? values.title_max_length : 16;
6272
+ this.service.setTitleGenerationConfig({
6273
+ enabled: titleEnabled,
6274
+ maxLength: titleMaxLen
6275
+ });
6276
+ const provider = providerForTitles;
6113
6277
  if (provider === "memory") return;
6114
6278
  const built = await this.buildAdapterFromValues(ctx, values);
6115
6279
  if (!built) {