@objectstack/service-ai 6.6.0 → 6.7.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.cjs CHANGED
@@ -1517,6 +1517,19 @@ var _AIService = class _AIService {
1517
1517
  get adapterName() {
1518
1518
  return this.adapter.name;
1519
1519
  }
1520
+ /**
1521
+ * Hot-swap the LLM adapter. Used by AIServicePlugin when the `ai`
1522
+ * settings namespace changes (provider/key/model edited via Setup UI).
1523
+ * In-flight requests bound to the previous adapter complete normally;
1524
+ * subsequent calls go through the new adapter.
1525
+ */
1526
+ setAdapter(next) {
1527
+ const prev = this.adapter.name;
1528
+ this.adapter = next;
1529
+ if (prev !== next.name) {
1530
+ this.logger.info(`[AI] LLM adapter swapped: ${prev} \u2192 ${next.name}`);
1531
+ }
1532
+ }
1520
1533
  /**
1521
1534
  * Best-effort persistence of a single chat message to the conversation
1522
1535
  * store. Failures are logged at warn level and swallowed — chat requests
@@ -2008,6 +2021,9 @@ function cryptoRandomId() {
2008
2021
  return `${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
2009
2022
  }
2010
2023
 
2024
+ // src/plugin.ts
2025
+ var import_contracts = require("@objectstack/spec/contracts");
2026
+
2011
2027
  // src/stream/vercel-stream-encoder.ts
2012
2028
  function sse(data) {
2013
2029
  return `data: ${JSON.stringify(data)}
@@ -5423,6 +5439,136 @@ var AIServicePlugin = class {
5423
5439
  this.dependencies = ["com.objectstack.engine.objectql"];
5424
5440
  this.options = options;
5425
5441
  }
5442
+ /**
5443
+ * Build an LLM adapter from a provider/key/model triple. Used both
5444
+ * by the boot-time auto-detect path and by the live `settings:changed`
5445
+ * rebuild path. Returns `null` if the requested provider cannot be
5446
+ * loaded or required credentials are missing.
5447
+ */
5448
+ async buildAdapterFromValues(ctx, values) {
5449
+ const provider = String(values.provider ?? "memory");
5450
+ if (provider === "memory") {
5451
+ return { adapter: new MemoryLLMAdapter(), description: "MemoryLLMAdapter (echo mode)" };
5452
+ }
5453
+ if (provider === "gateway") {
5454
+ const gatewayModel = String(values.gateway_model ?? "").trim();
5455
+ if (!gatewayModel) return null;
5456
+ try {
5457
+ const gatewayPkg = "@ai-sdk/gateway";
5458
+ const { gateway } = await import(
5459
+ /* webpackIgnore: true */
5460
+ gatewayPkg
5461
+ );
5462
+ return {
5463
+ adapter: new VercelLLMAdapter({ model: gateway(gatewayModel) }),
5464
+ description: `Vercel AI Gateway (model: ${gatewayModel})`
5465
+ };
5466
+ } catch (err) {
5467
+ ctx.logger.warn(
5468
+ `[AI] Failed to load @ai-sdk/gateway for provider=gateway`,
5469
+ err instanceof Error ? { error: err.message } : void 0
5470
+ );
5471
+ return null;
5472
+ }
5473
+ }
5474
+ const providerSpecs = {
5475
+ openai: { pkg: "@ai-sdk/openai", factory: "openai", defaultModel: "gpt-4o", displayName: "OpenAI" },
5476
+ anthropic: { pkg: "@ai-sdk/anthropic", factory: "anthropic", defaultModel: "claude-sonnet-4-20250514", displayName: "Anthropic" },
5477
+ google: { pkg: "@ai-sdk/google", factory: "google", defaultModel: "gemini-2.0-flash", displayName: "Google" }
5478
+ };
5479
+ const spec = providerSpecs[provider];
5480
+ if (!spec) return null;
5481
+ const apiKey = String(values[`${provider}_api_key`] ?? "").trim();
5482
+ if (!apiKey) return null;
5483
+ const envKey = provider === "openai" ? "OPENAI_API_KEY" : provider === "anthropic" ? "ANTHROPIC_API_KEY" : "GOOGLE_GENERATIVE_AI_API_KEY";
5484
+ process.env[envKey] = apiKey;
5485
+ try {
5486
+ const mod = await import(
5487
+ /* webpackIgnore: true */
5488
+ spec.pkg
5489
+ );
5490
+ const factory = mod[spec.factory] ?? mod.default;
5491
+ if (typeof factory !== "function") return null;
5492
+ const modelId = String(values[`${provider}_model`] ?? "").trim() || spec.defaultModel;
5493
+ const useChatApi = provider === "openai" && typeof factory.chat === "function";
5494
+ const model = useChatApi ? factory.chat(modelId) : factory(modelId);
5495
+ const apiSuffix = useChatApi ? " [chat-completions]" : "";
5496
+ return {
5497
+ adapter: new VercelLLMAdapter({ model }),
5498
+ description: `${spec.displayName} (model: ${modelId})${apiSuffix}`
5499
+ };
5500
+ } catch (err) {
5501
+ ctx.logger.warn(
5502
+ `[AI] Failed to load ${spec.pkg} for provider=${provider}`,
5503
+ err instanceof Error ? { error: err.message } : void 0
5504
+ );
5505
+ return null;
5506
+ }
5507
+ }
5508
+ /**
5509
+ * Build an `IEmbedder` instance from embedder settings values
5510
+ * (`embedder_provider`, `embedder_api_key`, …) by dynamically
5511
+ * importing `@objectstack/embedder-openai`. Returns `null` for
5512
+ * `none` (embedder disabled) or when required credentials are
5513
+ * missing / the package isn't installed.
5514
+ *
5515
+ * The OpenAI-compatible plugin covers OpenAI, Azure, 阿里通义,
5516
+ * 智谱, 硅基流动, 火山 Doubao, MiniMax, Ollama, and any custom
5517
+ * OpenAI-shape endpoint via `embedder_base_url`.
5518
+ */
5519
+ async buildEmbedderFromValues(ctx, values) {
5520
+ const provider = String(values.embedder_provider ?? "none").trim();
5521
+ if (!provider || provider === "none") return null;
5522
+ const apiKey = String(values.embedder_api_key ?? "").trim();
5523
+ const model = String(values.embedder_model ?? "").trim() || void 0;
5524
+ const baseUrlOverride = String(values.embedder_base_url ?? "").trim() || void 0;
5525
+ const dimensions = values.embedder_dimensions != null && values.embedder_dimensions !== "" ? Number(values.embedder_dimensions) : void 0;
5526
+ if (!apiKey && provider !== "ollama") {
5527
+ ctx.logger.warn(
5528
+ `[AI] Embedder provider=${provider} requires embedder_api_key \u2014 embedder unchanged.`
5529
+ );
5530
+ return null;
5531
+ }
5532
+ if ((provider === "custom" || provider === "azure") && !baseUrlOverride) {
5533
+ ctx.logger.warn(
5534
+ `[AI] Embedder provider=${provider} requires embedder_base_url \u2014 embedder unchanged.`
5535
+ );
5536
+ return null;
5537
+ }
5538
+ try {
5539
+ const pkg = "@objectstack/embedder-openai";
5540
+ const mod = await import(
5541
+ /* webpackIgnore: true */
5542
+ pkg
5543
+ );
5544
+ const create = mod.createOpenAIEmbedder ?? mod.default?.createOpenAIEmbedder;
5545
+ if (typeof create !== "function") {
5546
+ ctx.logger.warn(
5547
+ `[AI] ${pkg} did not export createOpenAIEmbedder \u2014 embedder unchanged.`
5548
+ );
5549
+ return null;
5550
+ }
5551
+ const embedder = create({
5552
+ preset: provider === "custom" ? void 0 : provider,
5553
+ baseUrl: baseUrlOverride,
5554
+ apiKey: apiKey || "ollama",
5555
+ model,
5556
+ dimensions: Number.isFinite(dimensions) ? dimensions : void 0,
5557
+ id: provider
5558
+ });
5559
+ const dimsLabel = embedder.dimensions ? `dims=${embedder.dimensions}` : "dims=?";
5560
+ return {
5561
+ embedder,
5562
+ description: `OpenAI-compatible embedder (provider=${provider}${model ? `, model=${model}` : ""}, ${dimsLabel})`
5563
+ };
5564
+ } catch (err) {
5565
+ ctx.logger.warn(
5566
+ `[AI] Failed to load @objectstack/embedder-openai for embedder provider=${provider}`,
5567
+ err instanceof Error ? { error: err.message } : void 0
5568
+ );
5569
+ return null;
5570
+ }
5571
+ }
5426
5572
  /**
5427
5573
  * Auto-detect LLM provider from environment variables.
5428
5574
  *
@@ -5842,6 +5988,179 @@ var AIServicePlugin = class {
5842
5988
  ctx.logger.info(
5843
5989
  `[AI] Service started \u2014 adapter="${this.service.adapterName}", tools=${this.service.toolRegistry.size}, routes=${routes.length}`
5844
5990
  );
5991
+ if (this.options.bindToSettings !== false) {
5992
+ ctx.hook("kernel:ready", async () => {
5993
+ await this.bindSettings(ctx);
5994
+ });
5995
+ }
5996
+ }
5997
+ /**
5998
+ * Resolve the `settings` service, apply any persisted `ai` values,
5999
+ * subscribe to changes, and register the live `ai/test` action
6000
+ * (overrides the manifest's fallback stub).
6001
+ */
6002
+ async bindSettings(ctx) {
6003
+ if (!this.service) return;
6004
+ let settings;
6005
+ try {
6006
+ settings = ctx.getService("settings");
6007
+ } catch {
6008
+ return;
6009
+ }
6010
+ if (!settings || typeof settings.getNamespace !== "function") return;
6011
+ const applySettings = async () => {
6012
+ if (!this.service) return;
6013
+ try {
6014
+ const payload = await settings.getNamespace("ai");
6015
+ const values = {};
6016
+ for (const [k, v] of Object.entries(payload.values)) {
6017
+ values[k] = v?.value;
6018
+ }
6019
+ const provider = String(values.provider ?? "memory");
6020
+ if (provider === "memory") return;
6021
+ const built = await this.buildAdapterFromValues(ctx, values);
6022
+ if (!built) {
6023
+ ctx.logger.warn(
6024
+ `[AI] Settings provider=${provider} could not be applied (missing credentials or package). Adapter unchanged (current="${this.service.adapterName}").`
6025
+ );
6026
+ return;
6027
+ }
6028
+ this.service.setAdapter(built.adapter);
6029
+ ctx.logger.info(`[AI] Adapter rebuilt from settings: ${built.description}`);
6030
+ } catch (err) {
6031
+ ctx.logger.warn("[AI] Failed to apply ai settings: " + (err?.message ?? err));
6032
+ }
6033
+ };
6034
+ await applySettings();
6035
+ if (typeof settings.subscribe === "function") {
6036
+ settings.subscribe("ai", () => {
6037
+ void applySettings();
6038
+ });
6039
+ ctx.logger.info("[AI] Bound to settings:changed for namespace=ai");
6040
+ }
6041
+ let currentEmbedderId = null;
6042
+ const applyEmbedder = async () => {
6043
+ try {
6044
+ const payload = await settings.getNamespace("ai");
6045
+ const values = {};
6046
+ for (const [k, v] of Object.entries(payload.values)) {
6047
+ values[k] = v?.value;
6048
+ }
6049
+ const built = await this.buildEmbedderFromValues(ctx, values);
6050
+ if (!built) {
6051
+ if (currentEmbedderId !== null) {
6052
+ ctx.logger.info("[AI] Embedder disabled by settings; kernel embedder service unset.");
6053
+ currentEmbedderId = null;
6054
+ }
6055
+ return;
6056
+ }
6057
+ const replace = ctx.replaceService ?? ctx.registerService;
6058
+ replace.call(ctx, import_contracts.EMBEDDER_SERVICE, built.embedder);
6059
+ currentEmbedderId = built.embedder.id;
6060
+ ctx.logger.info(`[AI] Embedder registered from settings: ${built.description}`);
6061
+ } catch (err) {
6062
+ ctx.logger.warn("[AI] Failed to apply embedder settings: " + (err?.message ?? err));
6063
+ }
6064
+ };
6065
+ await applyEmbedder();
6066
+ if (typeof settings.subscribe === "function") {
6067
+ settings.subscribe("ai", () => {
6068
+ void applyEmbedder();
6069
+ });
6070
+ }
6071
+ if (typeof settings.registerAction === "function") {
6072
+ settings.registerAction("ai", "test_embedder", async ({ values, payload }) => {
6073
+ const overrides = payload && typeof payload === "object" && payload !== null && "values" in payload ? payload.values ?? {} : {};
6074
+ const merged = { ...values ?? {}, ...overrides };
6075
+ const provider = String(merged.embedder_provider ?? "none");
6076
+ if (provider === "none") {
6077
+ return {
6078
+ ok: false,
6079
+ severity: "warning",
6080
+ message: "Embedder disabled (provider=none). Select a provider to enable knowledge search."
6081
+ };
6082
+ }
6083
+ let built;
6084
+ try {
6085
+ built = await this.buildEmbedderFromValues(ctx, merged);
6086
+ } catch (err) {
6087
+ return { ok: false, severity: "error", message: err?.message ?? String(err) };
6088
+ }
6089
+ if (!built) {
6090
+ return {
6091
+ ok: false,
6092
+ severity: "error",
6093
+ message: `Could not build embedder for provider=${provider}. Check api key, base URL, and that @objectstack/embedder-openai is installed.`
6094
+ };
6095
+ }
6096
+ const started = Date.now();
6097
+ try {
6098
+ const vectors = await built.embedder.embed(["ping"]);
6099
+ const latency = Date.now() - started;
6100
+ const dim = vectors[0]?.length ?? 0;
6101
+ return {
6102
+ ok: true,
6103
+ severity: "info",
6104
+ message: `${built.description} responded in ${latency}ms (vector dims=${dim}).`
6105
+ };
6106
+ } catch (err) {
6107
+ return {
6108
+ ok: false,
6109
+ severity: "error",
6110
+ message: `${built.description} request failed: ${err?.message ?? String(err)}`
6111
+ };
6112
+ }
6113
+ });
6114
+ ctx.logger.info("[AI] Registered live settings action ai/test_embedder");
6115
+ }
6116
+ if (typeof settings.registerAction === "function") {
6117
+ settings.registerAction("ai", "test", async ({ values, payload }) => {
6118
+ const overrides = payload && typeof payload === "object" && payload !== null && "values" in payload ? payload.values ?? {} : {};
6119
+ const merged = { ...values ?? {}, ...overrides };
6120
+ const provider = String(merged.provider ?? "memory");
6121
+ if (provider === "memory") {
6122
+ return {
6123
+ ok: true,
6124
+ severity: "warning",
6125
+ message: "Memory provider is an echo stub \u2014 no external call to validate. Switch to a real provider for production."
6126
+ };
6127
+ }
6128
+ let built;
6129
+ try {
6130
+ built = await this.buildAdapterFromValues(ctx, merged);
6131
+ } catch (err) {
6132
+ return { ok: false, severity: "error", message: err?.message ?? String(err) };
6133
+ }
6134
+ if (!built) {
6135
+ return {
6136
+ ok: false,
6137
+ severity: "error",
6138
+ message: `Could not build adapter for provider=${provider}. Check API key and that the provider SDK package is installed.`
6139
+ };
6140
+ }
6141
+ const started = Date.now();
6142
+ try {
6143
+ const result = await built.adapter.chat(
6144
+ [{ role: "user", content: "ping" }],
6145
+ { maxTokens: 8 }
6146
+ );
6147
+ const latency = Date.now() - started;
6148
+ const preview = String(result?.text ?? "").slice(0, 60);
6149
+ return {
6150
+ ok: true,
6151
+ severity: "info",
6152
+ message: `${built.description} responded in ${latency}ms${preview ? ` \u2014 "${preview}"` : ""}.`
6153
+ };
6154
+ } catch (err) {
6155
+ return {
6156
+ ok: false,
6157
+ severity: "error",
6158
+ message: `${built.description} request failed: ${err?.message ?? String(err)}`
6159
+ };
6160
+ }
6161
+ });
6162
+ ctx.logger.info("[AI] Registered live settings action ai/test");
6163
+ }
5845
6164
  }
5846
6165
  async destroy() {
5847
6166
  this.service = void 0;