@lenne.tech/nest-server 11.32.2 → 11.32.3
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/.claude/rules/configurable-features.md +1 -1
- package/FRAMEWORK-API.md +3 -1
- package/dist/core/common/interfaces/server-options.interface.d.ts +2 -0
- package/dist/core/modules/ai/inputs/core-ai-connection.input.js +2 -0
- package/dist/core/modules/ai/inputs/core-ai-connection.input.js.map +1 -1
- package/dist/core/modules/ai/services/core-ai-connection.service.d.ts +1 -0
- package/dist/core/modules/ai/services/core-ai-connection.service.js +68 -0
- package/dist/core/modules/ai/services/core-ai-connection.service.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/migration-guides/11.32.2-to-11.32.3.md +129 -0
- package/package.json +1 -1
- package/src/core/common/interfaces/server-options.interface.ts +23 -0
- package/src/core/modules/ai/README.md +6 -0
- package/src/core/modules/ai/inputs/core-ai-connection.input.ts +2 -0
- package/src/core/modules/ai/interfaces/ai-tool.interface.ts +18 -3
- package/src/core/modules/ai/services/core-ai-connection.service.ts +135 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# Migration Guide: 11.32.2 → 11.32.3
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
| Category | Details |
|
|
6
|
+
|----------|---------|
|
|
7
|
+
| **Breaking Changes** | None |
|
|
8
|
+
| **New Features** | `ai.capabilityDriftCheck` — opt-in boot warning when an explicit connection capability contradicts the endpoint; `ai.defaultConnection.contextWindow` — seed a connection's context window from config |
|
|
9
|
+
| **Bugfixes** | The AI connection `contextWindow` admin input now rejects non-positive / non-integer values |
|
|
10
|
+
| **Migration Effort** | ~2 minutes — everything is opt-in; read §3 only if your admin tooling writes `contextWindow` |
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Quick Migration
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pnpm update @lenne.tech/nest-server@11.32.3
|
|
18
|
+
pnpm run build
|
|
19
|
+
pnpm test
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
No configuration change is required. Everything below is opt-in or a
|
|
23
|
+
tightening you inherit automatically.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## 1. New: `ai.capabilityDriftCheck` (opt-in, default `false`)
|
|
28
|
+
|
|
29
|
+
A connection's `supportsNativeTools` / `supportsJsonResponse` flags are
|
|
30
|
+
auto-detected while they are left **undefined**, but an **explicit** value is
|
|
31
|
+
authoritative and is never re-probed. A wrong explicit flag therefore degrades the
|
|
32
|
+
assistant silently — e.g. `supportsNativeTools: false` on a backend that *does*
|
|
33
|
+
support native function calling forces fragile emulated tool-calling.
|
|
34
|
+
|
|
35
|
+
Enable this opt-in boot self-check to surface such a mismatch:
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
ai: {
|
|
39
|
+
capabilityDriftCheck: true, // default false
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
When enabled, at startup the module probes each enabled connection that declares an
|
|
44
|
+
explicit flag and logs a **warning** on mismatch. It never changes the stored value
|
|
45
|
+
(your explicit choice stays authoritative — clear the flag in the admin UI to
|
|
46
|
+
re-enable auto-detection).
|
|
47
|
+
|
|
48
|
+
**It is off by default because it makes outbound calls to the LLM endpoints on
|
|
49
|
+
every boot** (and is additionally skipped in the `ci`/`e2e` runners). Note this is a
|
|
50
|
+
diagnostic log only: if you deliberately override a flag against what the endpoint
|
|
51
|
+
reports (e.g. native tools are advertised but unreliable on your model), the warning
|
|
52
|
+
is expected and can be ignored.
|
|
53
|
+
|
|
54
|
+
**Action: none** unless you want the diagnostic — then set the flag.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 2. New: `ai.defaultConnection.contextWindow`
|
|
59
|
+
|
|
60
|
+
The one-time `ai.defaultConnection` seed now accepts `contextWindow`, so a
|
|
61
|
+
config-seeded connection can carry its context window from the start instead of
|
|
62
|
+
relying on auto-detection:
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
ai: {
|
|
66
|
+
defaultConnection: {
|
|
67
|
+
name: 'default',
|
|
68
|
+
baseUrl: '...',
|
|
69
|
+
model: '...',
|
|
70
|
+
contextWindow: 32768, // optional — omit to auto-detect
|
|
71
|
+
},
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Omit it to keep the existing behaviour (auto-detect by probing the endpoint /
|
|
76
|
+
`knownContextWindow()`, falling back to the global `ai.contextWindow` default of
|
|
77
|
+
8192).
|
|
78
|
+
|
|
79
|
+
**Action: none** — additive and optional.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 3. Tightened: `contextWindow` admin input validation
|
|
84
|
+
|
|
85
|
+
The `contextWindow` field on the AI connection create/update input is now validated
|
|
86
|
+
as a **positive integer** (`@IsInt` + `@Min(1)`). Previously any number (including
|
|
87
|
+
`0`, a negative, or a float) was accepted verbatim, and an explicit `0` is not
|
|
88
|
+
"unset" — it would have fed the orchestrator's context budget as a real (broken)
|
|
89
|
+
value instead of falling back to the safe default.
|
|
90
|
+
|
|
91
|
+
**Action required if** your admin tooling or tests send a non-integer or
|
|
92
|
+
`< 1` `contextWindow` to `createAiConnection` / `updateAiConnection` — those calls
|
|
93
|
+
now return a validation error. Send a positive integer, or omit the field to
|
|
94
|
+
auto-detect.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## 4. Documentation: the MCP confirmation-gate boundary
|
|
99
|
+
|
|
100
|
+
No code change — a clarification of the `IAiTool.destructive` / `.mutating` JSDoc
|
|
101
|
+
(reinforcing §5 of the 11.32.2 guide). Over `/ai/mcp` there is **no confirmation
|
|
102
|
+
gate**: `mcpCallTool` consults neither flag, so a destructive tool executes on the
|
|
103
|
+
first call. The barriers that *do* hold on every path are the registry **role
|
|
104
|
+
filter** (`forUser()`, applied before `execute()`) and the authorization inside
|
|
105
|
+
`execute()` itself — so a destructive tool restricted to a real role stays
|
|
106
|
+
unreachable by lesser-privileged MCP clients; MCP only skips the extra confirmation
|
|
107
|
+
step for clients that may already see the tool.
|
|
108
|
+
|
|
109
|
+
**Action:** expose `/ai/mcp` only to clients you trust to obtain user consent
|
|
110
|
+
themselves, and keep data-level authorization inside `execute()` (not only in the
|
|
111
|
+
plan-mode `authorize()`).
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## Module Documentation
|
|
116
|
+
|
|
117
|
+
- [AI module README](../src/core/modules/ai/README.md) — see "Capability auto-detection"
|
|
118
|
+
- [AI INTEGRATION-CHECKLIST](../src/core/modules/ai/INTEGRATION-CHECKLIST.md) — advanced configuration
|
|
119
|
+
- [Configurable features](../.claude/rules/configurable-features.md) — the AI Assistant row
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Troubleshooting
|
|
124
|
+
|
|
125
|
+
| Symptom | Cause | Fix |
|
|
126
|
+
|---------|-------|-----|
|
|
127
|
+
| Boot logs "capability drift" for a connection | §1 — an explicit flag disagrees with the endpoint | Correct the flag in the admin UI, or clear it to auto-detect; ignore if the override is deliberate |
|
|
128
|
+
| `createAiConnection` rejects `contextWindow` | §3 — it must be a positive integer now | Send an integer `>= 1`, or omit to auto-detect |
|
|
129
|
+
| A destructive tool ran over `/ai/mcp` without confirmation | §4 — MCP has no confirmation gate by design | Only expose MCP to trusted clients; enforce data-level checks in `execute()` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lenne.tech/nest-server",
|
|
3
|
-
"version": "11.32.
|
|
3
|
+
"version": "11.32.3",
|
|
4
4
|
"description": "Modern, fast, powerful Node.js web framework in TypeScript based on Nest with a GraphQL API and a connection to MongoDB (or other databases).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"node",
|
|
@@ -1110,6 +1110,16 @@ export interface IAiDefaultConnection {
|
|
|
1110
1110
|
/** Capability tags (free-form, e.g. 'analysis', 'vision'). */
|
|
1111
1111
|
capabilities?: string[];
|
|
1112
1112
|
|
|
1113
|
+
/**
|
|
1114
|
+
* Total context window (input + output tokens) the model supports. Drives the
|
|
1115
|
+
* orchestrator's context budget (system prompt + history + tool results). Omit to
|
|
1116
|
+
* auto-detect by probing the endpoint / `knownContextWindow()`; set it explicitly
|
|
1117
|
+
* when the endpoint exposes no limit and the model id is unknown to the heuristic
|
|
1118
|
+
* (otherwise the orchestrator assumes the conservative `ai.contextWindow` default
|
|
1119
|
+
* of 8192 and trims the prompt + tool results on every turn).
|
|
1120
|
+
*/
|
|
1121
|
+
contextWindow?: number;
|
|
1122
|
+
|
|
1113
1123
|
/** Default maximum number of tokens for completions. */
|
|
1114
1124
|
defaultMaxTokens?: number;
|
|
1115
1125
|
|
|
@@ -1212,6 +1222,19 @@ export interface IAi {
|
|
|
1212
1222
|
user?: { maxPrompts?: number; maxTokens?: number };
|
|
1213
1223
|
};
|
|
1214
1224
|
|
|
1225
|
+
/**
|
|
1226
|
+
* Opt-in boot self-check: after startup, probe each enabled connection that declares
|
|
1227
|
+
* an EXPLICIT `supportsNativeTools` / `supportsJsonResponse` and warn (log only) when
|
|
1228
|
+
* the declared value contradicts what the endpoint actually reports — a wrong explicit
|
|
1229
|
+
* flag otherwise silently degrades the assistant (e.g. forcing fragile emulated
|
|
1230
|
+
* tool-calling on a backend that supports native function calling). OFF by default
|
|
1231
|
+
* because it makes outbound calls to the LLM endpoints on every boot; the declared
|
|
1232
|
+
* value is never changed (clear it in the admin UI to re-enable auto-detection). Also
|
|
1233
|
+
* skipped in the ci/e2e runners.
|
|
1234
|
+
* @default false
|
|
1235
|
+
*/
|
|
1236
|
+
capabilityDriftCheck?: boolean;
|
|
1237
|
+
|
|
1215
1238
|
/**
|
|
1216
1239
|
* Confirmation policy for mutating tool actions (create/update/delete).
|
|
1217
1240
|
* `destructive` tools always require confirmation regardless of this policy.
|
|
@@ -111,6 +111,12 @@ never probed). Detection runs in two complementary ways:
|
|
|
111
111
|
once, persists, and uses the result. Until then the safe emulated baseline applies.
|
|
112
112
|
- **On demand:** admins can re-probe via `detectAiConnectionCapabilities` /
|
|
113
113
|
`POST /ai/connections/:id/detect-capabilities` (e.g. after changing `baseUrl`/`model`).
|
|
114
|
+
- **Boot drift check (opt-in):** set `ai.capabilityDriftCheck: true` to probe every enabled
|
|
115
|
+
connection that has an EXPLICIT flag once at startup and log a warning when the declared
|
|
116
|
+
value contradicts what the endpoint reports (a wrong explicit flag otherwise degrades the
|
|
117
|
+
assistant silently, e.g. forcing emulated tool-calling on a native-capable backend). It only
|
|
118
|
+
warns — the stored value is never changed. OFF by default because it makes outbound calls to
|
|
119
|
+
the LLM endpoints on every boot; also skipped in the ci/e2e runners.
|
|
114
120
|
|
|
115
121
|
The probe is provider-agnostic best effort: `response_format: json_object` is sent
|
|
116
122
|
(2xx → JSON supported); a trivial tool with `tool_choice: 'required'` is sent (2xx
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { InputType } from '@nestjs/graphql';
|
|
2
|
+
import { IsInt, Min } from 'class-validator';
|
|
2
3
|
|
|
3
4
|
import { Restricted } from '../../../common/decorators/restricted.decorator';
|
|
4
5
|
import { UnifiedField } from '../../../common/decorators/unified-field.decorator';
|
|
@@ -72,6 +73,7 @@ export class CoreAiConnectionInput {
|
|
|
72
73
|
isOptional: true,
|
|
73
74
|
roles: RoleEnum.ADMIN,
|
|
74
75
|
type: () => Number,
|
|
76
|
+
validator: (options) => [IsInt(options), Min(1, options)],
|
|
75
77
|
})
|
|
76
78
|
contextWindow?: number = undefined;
|
|
77
79
|
|
|
@@ -69,9 +69,21 @@ export interface IAiTool {
|
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
71
|
* Whether the tool performs a destructive/irreversible action (delete, bulk
|
|
72
|
-
* update, payment, …).
|
|
73
|
-
* NOT executed until the prompt is re-sent with
|
|
74
|
-
* response lists them as `pendingActions` with
|
|
72
|
+
* update, payment, …). In the CHAT orchestrator destructive tools always require
|
|
73
|
+
* confirmation: they are NOT executed until the prompt is re-sent with
|
|
74
|
+
* `confirm: true`; the first response lists them as `pendingActions` with
|
|
75
|
+
* `requiresConfirmation: true`.
|
|
76
|
+
*
|
|
77
|
+
* **No confirmation gate over MCP.** `CoreAiMcpService.mcpCallTool` consults
|
|
78
|
+
* neither this flag nor {@link IAiTool.mutating}, so a destructive tool invoked
|
|
79
|
+
* through `/ai/mcp` executes IMMEDIATELY, on the first call. This flag is
|
|
80
|
+
* therefore a chat-orchestrator contract, not a global execution barrier. The
|
|
81
|
+
* barriers that DO hold on every path are the registry role filter ({@link
|
|
82
|
+
* IAiTool.roles}, applied by `forUser()` before `execute()`) and the authorization
|
|
83
|
+
* inside `execute()` itself — so a destructive tool restricted to a real role stays
|
|
84
|
+
* unreachable by lesser-privileged MCP clients; MCP only skips the extra confirmation
|
|
85
|
+
* step for clients that may already see the tool. Expose MCP only to clients you trust
|
|
86
|
+
* to obtain user consent themselves.
|
|
75
87
|
*/
|
|
76
88
|
readonly destructive?: boolean;
|
|
77
89
|
|
|
@@ -80,6 +92,9 @@ export interface IAiTool {
|
|
|
80
92
|
* mutating tools is governed by the `ai.confirmation` policy (admin default,
|
|
81
93
|
* optionally client-overridable, optionally enforced). `destructive` is the
|
|
82
94
|
* stronger flag and always requires confirmation regardless of policy.
|
|
95
|
+
*
|
|
96
|
+
* Same MCP caveat as {@link IAiTool.destructive}: the confirmation policy is not
|
|
97
|
+
* evaluated on the `/ai/mcp` path at all.
|
|
83
98
|
*/
|
|
84
99
|
readonly mutating?: boolean;
|
|
85
100
|
|
|
@@ -32,6 +32,29 @@ import { AI_CONNECTION_CLASS, AI_CONNECTION_MODEL } from '../core-ai.constants';
|
|
|
32
32
|
*/
|
|
33
33
|
export { AI_CONNECTION_CLASS, AI_CONNECTION_MODEL } from '../core-ai.constants';
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Minimal shape of a persisted (lean) connection document that the boot-time drift
|
|
37
|
+
* check needs to build a provider for a probe. It mirrors the fields
|
|
38
|
+
* {@link CoreAiConnectionService.resolve} reads, declared locally so the drift check can
|
|
39
|
+
* use the bulk `find()` result directly (no per-connection re-read / N+1).
|
|
40
|
+
*/
|
|
41
|
+
type ResolvableConnectionDoc = {
|
|
42
|
+
_id: unknown;
|
|
43
|
+
apiKeyEncrypted?: string;
|
|
44
|
+
apiKeyEnv?: string;
|
|
45
|
+
baseUrl: string;
|
|
46
|
+
contextWindow?: number;
|
|
47
|
+
defaultMaxTokens?: number;
|
|
48
|
+
defaultTemperature?: number;
|
|
49
|
+
defaultUserMaxPeriod?: string;
|
|
50
|
+
defaultUserMaxTokens?: number;
|
|
51
|
+
model: string;
|
|
52
|
+
name: string;
|
|
53
|
+
providerType?: string;
|
|
54
|
+
supportsJsonResponse?: boolean;
|
|
55
|
+
supportsNativeTools?: boolean;
|
|
56
|
+
};
|
|
57
|
+
|
|
35
58
|
/**
|
|
36
59
|
* CRUD service for {@link CoreAiConnection} — the database-backed LLM
|
|
37
60
|
* configuration. Admin-only (enforced by the model's `@Restricted(ADMIN)` plus
|
|
@@ -75,6 +98,9 @@ export class CoreAiConnectionService
|
|
|
75
98
|
async onModuleInit(): Promise<void> {
|
|
76
99
|
await this.seedDefaultConnection();
|
|
77
100
|
await this.assertStoredKeysDecryptable();
|
|
101
|
+
// Best-effort, non-blocking: probe endpoints and warn on capability drift. Never
|
|
102
|
+
// awaited so a slow/unreachable endpoint cannot delay boot.
|
|
103
|
+
void this.warnOnCapabilityDrift();
|
|
78
104
|
}
|
|
79
105
|
|
|
80
106
|
/**
|
|
@@ -142,6 +168,115 @@ export class CoreAiConnectionService
|
|
|
142
168
|
}
|
|
143
169
|
}
|
|
144
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Opt-in boot self-check (`ai.capabilityDriftCheck`, default OFF): warn when a
|
|
173
|
+
* connection DECLARES a capability that contradicts what its endpoint actually
|
|
174
|
+
* reports. Capabilities are auto-detected only for flags left UNDEFINED (create +
|
|
175
|
+
* lazy runtime path); an EXPLICIT `supportsNativeTools` / `supportsJsonResponse` is
|
|
176
|
+
* authoritative and is never re-probed by the normal path — so a wrong explicit flag
|
|
177
|
+
* silently degrades the assistant forever (e.g. `supportsNativeTools: false` on an
|
|
178
|
+
* endpoint that DOES support native function calling forces fragile emulated
|
|
179
|
+
* tool-calling, which weaker models do not sustain once the prompt grows).
|
|
180
|
+
*
|
|
181
|
+
* To observe the endpoint's REAL capability for a DECLARED flag, it builds the provider
|
|
182
|
+
* with the flags cleared to `undefined` — otherwise the provider's `detectCapabilities()`,
|
|
183
|
+
* which probes ONLY undefined flags, would return nothing to compare against (the whole
|
|
184
|
+
* point of the check) — then diffs the probed booleans against the stored declaration.
|
|
185
|
+
*
|
|
186
|
+
* It NEVER changes the stored value (the admin's explicit choice stays authoritative),
|
|
187
|
+
* NEVER blocks boot (fire-and-forget, all errors swallowed), and issues outbound calls
|
|
188
|
+
* to the LLM endpoints — hence it is OFF by default and additionally skipped in the
|
|
189
|
+
* ci/e2e runners. It reads every enabled connection in a single query (no per-connection
|
|
190
|
+
* re-read). Connections that leave BOTH flags undefined are handled by
|
|
191
|
+
* {@link detectAndPersistCapabilities} and are skipped here (nothing declared to check).
|
|
192
|
+
*/
|
|
193
|
+
protected async warnOnCapabilityDrift(): Promise<void> {
|
|
194
|
+
// Opt-in: a framework boot must not contact third-party endpoints unless asked.
|
|
195
|
+
if (!ConfigService.get<boolean>('ai.capabilityDriftCheck')) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
// Defense in depth: never probe from the integration test runner (real module boot).
|
|
199
|
+
// The unit runner (NODE_ENV=test) is intentionally NOT excluded so the method stays
|
|
200
|
+
// unit-testable with a mocked providerFactory — the opt-in flag above already prevents
|
|
201
|
+
// accidental probing there.
|
|
202
|
+
if (!this.providerFactory || ['ci', 'e2e'].includes(process.env.NODE_ENV ?? '')) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
// Single read (no per-connection re-resolve): the full docs carry everything the
|
|
207
|
+
// provider factory needs, so there is no N+1 findById per connection.
|
|
208
|
+
const docs = (await this.mainDbModel
|
|
209
|
+
.find({ enabled: { $ne: false } })
|
|
210
|
+
.lean()
|
|
211
|
+
.exec()) as unknown as ResolvableConnectionDoc[];
|
|
212
|
+
for (const doc of docs) {
|
|
213
|
+
// Only a connection that DECLARES a capability can drift; undefined flags are
|
|
214
|
+
// auto-detected on first use, so there is nothing to reconcile here.
|
|
215
|
+
if (typeof doc.supportsNativeTools !== 'boolean' && typeof doc.supportsJsonResponse !== 'boolean') {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
let provider: { detectCapabilities?: () => Promise<{ jsonResponse?: boolean; nativeTools?: boolean }> };
|
|
219
|
+
try {
|
|
220
|
+
// Clear the declared flags so detectCapabilities() actually probes them (it
|
|
221
|
+
// skips any flag that is already a boolean on the connection).
|
|
222
|
+
const probeConnection: ResolvedAiConnection = {
|
|
223
|
+
apiKey: this.resolveApiKeyFromDoc(doc) ?? '',
|
|
224
|
+
baseUrl: doc.baseUrl,
|
|
225
|
+
contextWindow: doc.contextWindow,
|
|
226
|
+
defaultMaxTokens: doc.defaultMaxTokens,
|
|
227
|
+
defaultTemperature: doc.defaultTemperature,
|
|
228
|
+
defaultUserMaxPeriod: doc.defaultUserMaxPeriod,
|
|
229
|
+
defaultUserMaxTokens: doc.defaultUserMaxTokens,
|
|
230
|
+
id: String(doc._id),
|
|
231
|
+
model: doc.model,
|
|
232
|
+
name: doc.name,
|
|
233
|
+
providerType: doc.providerType || 'openai-compatible',
|
|
234
|
+
supportsJsonResponse: undefined,
|
|
235
|
+
supportsNativeTools: undefined,
|
|
236
|
+
};
|
|
237
|
+
provider = this.providerFactory.create(probeConnection);
|
|
238
|
+
} catch {
|
|
239
|
+
continue; // unresolvable / unbuildable — nothing to compare against
|
|
240
|
+
}
|
|
241
|
+
if (typeof provider.detectCapabilities !== 'function') {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const detected = await provider.detectCapabilities().catch(() => undefined);
|
|
245
|
+
if (!detected) {
|
|
246
|
+
continue; // probe failed (endpoint down / transport error) — not a drift signal
|
|
247
|
+
}
|
|
248
|
+
const drift: string[] = [];
|
|
249
|
+
if (
|
|
250
|
+
typeof doc.supportsNativeTools === 'boolean' &&
|
|
251
|
+
typeof detected.nativeTools === 'boolean' &&
|
|
252
|
+
doc.supportsNativeTools !== detected.nativeTools
|
|
253
|
+
) {
|
|
254
|
+
drift.push(
|
|
255
|
+
`supportsNativeTools declared ${doc.supportsNativeTools} but the endpoint reports ${detected.nativeTools}`,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
if (
|
|
259
|
+
typeof doc.supportsJsonResponse === 'boolean' &&
|
|
260
|
+
typeof detected.jsonResponse === 'boolean' &&
|
|
261
|
+
doc.supportsJsonResponse !== detected.jsonResponse
|
|
262
|
+
) {
|
|
263
|
+
drift.push(
|
|
264
|
+
`supportsJsonResponse declared ${doc.supportsJsonResponse} but the endpoint reports ${detected.jsonResponse}`,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
if (drift.length) {
|
|
268
|
+
this.logger.warn(
|
|
269
|
+
`AI connection "${doc.name || String(doc._id)}" capability drift: ${drift.join('; ')}. ` +
|
|
270
|
+
`The declared value is authoritative and was NOT changed — correct it in the admin UI, or clear it to ` +
|
|
271
|
+
`re-enable auto-detection, so the assistant uses the endpoint's real capabilities.`,
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
} catch (err) {
|
|
276
|
+
this.logger.warn(`AI capability drift check skipped: ${(err as Error).message}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
145
280
|
/**
|
|
146
281
|
* Create a connection. Encrypts the optional plaintext `apiKey` and keeps the
|
|
147
282
|
* default connection unique.
|