feat(db): Llm.kind discriminator + virtual-provider lifecycle (v1 Stage 1)

First step of the virtual-LLM feature. A virtual Llm row is one that
gets *registered by an mcplocal client* rather than created via
\`mcpctl create llm\`. Its inference is relayed back through an SSE
control channel to the publishing session (mcpd routes added in
Stage 3). The lifecycle fields below let mcpd reap stale rows when
the publisher goes away.

Schema additions:
- enum LlmKind (public | virtual). Default public.
- enum LlmStatus (active | inactive | hibernating). Default active.
  hibernating is reserved for v2 wake-on-demand.
- Llm.kind, providerSessionId, lastHeartbeatAt, status, inactiveSince.
- @@index([kind, status]) for the GC sweep.
- @@index([providerSessionId]) for the reconnect lookup.

All existing rows backfill with kind=public/status=active so v1 is
purely additive — public LLMs ignore the lifecycle columns entirely.

7 new prisma-level assertions in tests/llm-virtual-schema.test.ts
cover: defaults, persisting kind=virtual + lifecycle together, the
active→inactive flip, hibernating value, enum rejection, the
(kind,status) GC index, the providerSessionId reconnect index.

mcpd suite still 801/801 (regenerated client) and typecheck clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michal
2026-04-27 13:59:44 +01:00
parent e65a396d3e
commit 1acd8b58bc
3 changed files with 190 additions and 13 deletions

View File

@@ -182,21 +182,44 @@ model Secret {
// provider API key server-side so credentials never leave the cluster.
// Credentials are stored by reference: `apiKeySecret` points at a Secret, and
// `apiKeySecretKey` names the key within that secret's data.
//
// `kind=virtual` rows are *registered by an mcplocal client* (rather than a
// human via `mcpctl create llm`). Their inference is relayed back through
// the SSE control channel to the publishing mcplocal session. The lifecycle
// fields (lastHeartbeatAt, status, inactiveSince) belong to virtual rows;
// public rows ignore them.
enum LlmKind {
public // upstream-URL row, mcpd calls directly
virtual // mcplocal-registered, inference relayed via SSE control channel
}
enum LlmStatus {
active // healthy, accepting requests
inactive // publisher went away; row pending 4-h GC
hibernating // publisher present but backend asleep — wakes on demand (v2)
}
model Llm {
id String @id @default(cuid())
name String @unique
type String // anthropic | openai | deepseek | vllm | ollama | gemini-cli
model String // e.g. claude-3-5-sonnet-20241022
url String @default("") // endpoint (empty for provider default)
tier String @default("fast") // fast | heavy
description String @default("")
apiKeySecretId String? // FK to Secret
apiKeySecretKey String? // key inside the Secret's data
extraConfig Json @default("{}") // per-type extras
version Int @default(1)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id String @id @default(cuid())
name String @unique
type String // anthropic | openai | deepseek | vllm | ollama | gemini-cli
model String // e.g. claude-3-5-sonnet-20241022
url String @default("") // endpoint (empty for provider default)
tier String @default("fast") // fast | heavy
description String @default("")
apiKeySecretId String? // FK to Secret
apiKeySecretKey String? // key inside the Secret's data
extraConfig Json @default("{}") // per-type extras
// ── Virtual-provider lifecycle (NULL/default for kind=public) ──
kind LlmKind @default(public)
providerSessionId String? // mcplocal session that owns this row when virtual
lastHeartbeatAt DateTime? // bumped on every publisher heartbeat
status LlmStatus @default(active)
inactiveSince DateTime? // when status flipped from active; used for 4-h GC
version Int @default(1)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
apiKeySecret Secret? @relation(fields: [apiKeySecretId], references: [id], onDelete: SetNull)
agents Agent[]
@@ -204,6 +227,8 @@ model Llm {
@@index([name])
@@index([tier])
@@index([apiKeySecretId])
@@index([kind, status])
@@index([providerSessionId])
}
// ── Groups ──