Deploy: migrate k3s → Docker; search logic → master-agents-svc

- deploy/k3s удалён, deploy/docker добавлен (Caddyfile, docker-compose, searxng)
- chat-svc: agents/models/prompts удалены, использует llm-svc (LLMClient, EmbeddingClient)
- master-agents-svc: SearchOrchestrator, classifier, researcher, actions, widgets
- web-svc: ChatModelSelector, Optimization, Sources удалены; InputBarPlus; UnregisterSW
- geo-device-svc, localization-svc: Dockerfiles
- docs: 02-k3s-services-spec.md, RUNBOOK/TELEMETRY/WORKING удалены

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
home
2026-02-23 22:14:00 +03:00
parent cd6b7857ba
commit 328d968f3f
180 changed files with 3022 additions and 9798 deletions

View File

@@ -1,5 +1,5 @@
/**
* llm-svc — LLM provider management microservice
* llm-svc — LLM provider management service (СОА)
* API: GET/POST/PATCH/DELETE /api/v1/providers, GET/POST /api/v1/providers/:id/models
* Config: data/llm-providers.json, envOnlyMode via LLM_PROVIDER=ollama|timeweb
*/
@@ -59,6 +59,22 @@ app.get('/metrics', async (_req, reply) => {
);
});
/* --- Provider UI config (for chat-svc config) --- */
app.get('/api/v1/providers/ui-config', async (_req, reply) => {
try {
const { providers } = await import('./lib/models/providers/index.js');
const sections = Object.entries(providers).map(([key, Provider]) => {
const configFields = Provider.getProviderConfigFields();
const metadata = Provider.getProviderMetadata();
return { key, name: metadata.name, fields: configFields };
});
return reply.send({ sections });
} catch (err) {
app.log.error(err);
return reply.status(500).send({ message: 'An error has occurred.' });
}
});
/* --- Providers --- */
app.get<{ Querystring: { internal?: string } }>('/api/v1/providers', async (req, reply) => {
try {
@@ -257,6 +273,156 @@ app.post<{ Params: { id: string }; Body: unknown }>(
},
);
const modelSchema = z.object({
providerId: z.string().min(1),
key: z.string().min(1),
});
const messageSchema = z.object({
role: z.enum(['user', 'assistant', 'system', 'tool']),
content: z.string(),
id: z.string().optional(),
name: z.string().optional(),
tool_calls: z.array(z.object({
id: z.string(),
name: z.string(),
arguments: z.record(z.string(), z.unknown()),
})).optional(),
});
const toolSchema = z.object({
name: z.string(),
description: z.string(),
schema: z.record(z.string(), z.unknown()),
});
const generateOptionsSchema = z.object({
temperature: z.number().optional(),
maxTokens: z.number().optional(),
topP: z.number().optional(),
stopSequences: z.array(z.string()).optional(),
frequencyPenalty: z.number().optional(),
presencePenalty: z.number().optional(),
}).optional();
const generateSchema = z.object({
model: modelSchema,
messages: z.array(messageSchema),
tools: z.array(toolSchema).optional(),
options: generateOptionsSchema,
});
const generateObjectSchema = z.object({
model: modelSchema,
messages: z.array(messageSchema),
schema: z.record(z.string(), z.unknown()),
options: generateOptionsSchema,
});
const embeddingsSchema = z.object({
model: modelSchema,
texts: z.array(z.string()),
});
/* --- Generation API --- */
app.post<{ Body: unknown }>('/api/v1/generate', async (req, reply) => {
const parsed = generateSchema.safeParse(req.body);
if (!parsed.success) {
return reply.status(400).send({ message: 'Invalid request', error: parsed.error.issues });
}
try {
const registry = new ModelRegistry();
const llm = await registry.loadChatModel(parsed.data.model.providerId, parsed.data.model.key);
const tools = parsed.data.tools?.map((t) => ({
name: t.name,
description: t.description,
schema: z.fromJSONSchema(t.schema as Parameters<typeof z.fromJSONSchema>[0]),
}));
const result = await llm.generateText({
messages: parsed.data.messages as unknown as import('./lib/types.js').Message[],
tools,
options: parsed.data.options,
});
return reply.send(result);
} catch (err) {
app.log.error(err);
return reply.status(500).send({ message: err instanceof Error ? err.message : 'Generation failed' });
}
});
app.post<{ Body: unknown }>('/api/v1/generate/stream', async (req, reply) => {
const parsed = generateSchema.safeParse(req.body);
if (!parsed.success) {
return reply.status(400).send({ message: 'Invalid request', error: parsed.error.issues });
}
try {
const registry = new ModelRegistry();
const llm = await registry.loadChatModel(parsed.data.model.providerId, parsed.data.model.key);
const tools = parsed.data.tools?.map((t) => ({
name: t.name,
description: t.description,
schema: z.fromJSONSchema(t.schema as Parameters<typeof z.fromJSONSchema>[0]),
}));
const stream = llm.streamText({
messages: parsed.data.messages as unknown as import('./lib/types.js').Message[],
tools,
options: parsed.data.options,
});
const encoder = new TextEncoder();
const readable = new ReadableStream({
async start(controller) {
try {
for await (const chunk of stream) {
controller.enqueue(encoder.encode(JSON.stringify(chunk) + '\n'));
}
} catch (e) {
app.log.error(e);
controller.enqueue(encoder.encode(JSON.stringify({ error: e instanceof Error ? e.message : String(e) }) + '\n'));
}
controller.close();
},
});
return reply
.header('Content-Type', 'application/x-ndjson')
.header('Cache-Control', 'no-cache')
.send(readable);
} catch (err) {
app.log.error(err);
return reply.status(500).send({ message: err instanceof Error ? err.message : 'Stream generation failed' });
}
});
app.post<{ Body: unknown }>('/api/v1/generate/object', async (req, reply) => {
const parsed = generateObjectSchema.safeParse(req.body);
if (!parsed.success) {
return reply.status(400).send({ message: 'Invalid request', error: parsed.error.issues });
}
try {
const registry = new ModelRegistry();
const llm = await registry.loadChatModel(parsed.data.model.providerId, parsed.data.model.key);
const zodSchema = z.fromJSONSchema(parsed.data.schema as Parameters<typeof z.fromJSONSchema>[0]);
const result = await llm.generateObject({
messages: parsed.data.messages as unknown as import('./lib/types.js').Message[],
schema: zodSchema,
options: parsed.data.options,
});
return reply.send({ object: result });
} catch (err) {
app.log.error(err);
return reply.status(500).send({ message: err instanceof Error ? err.message : 'Object generation failed' });
}
});
app.post<{ Body: unknown }>('/api/v1/embeddings', async (req, reply) => {
const parsed = embeddingsSchema.safeParse(req.body);
if (!parsed.success) {
return reply.status(400).send({ message: 'Invalid request', error: parsed.error.issues });
}
try {
const registry = new ModelRegistry();
const embedding = await registry.loadEmbeddingModel(parsed.data.model.providerId, parsed.data.model.key);
const embeddings = await embedding.embedText(parsed.data.texts);
return reply.send({ embeddings });
} catch (err) {
app.log.error(err);
return reply.status(500).send({ message: err instanceof Error ? err.message : 'Embeddings failed' });
}
});
const modelDeleteSchema = z.object({
type: z.enum(['chat', 'embedding']),
key: z.string().min(1),

View File

@@ -2,7 +2,7 @@ import type { ConfigModelProvider } from '../config/types.js';
import BaseModelProvider, {
createProviderInstance,
} from './base/provider.js';
import { getConfiguredModelProviders } from '../config/serverRegistry.js';
import { getConfiguredModelProviders, isEnvOnlyMode } from '../config/serverRegistry.js';
import { providers } from './providers/index.js';
import type { MinimalProvider, ModelList } from './types.js';
import { providersConfig } from '../config/ProvidersConfig.js';
@@ -74,13 +74,25 @@ class ModelRegistry {
}
async loadChatModel(providerId: string, modelName: string) {
const provider = this.activeProviders.find((p) => p.id === providerId);
let provider = this.activeProviders.find((p) => p.id === providerId);
if (!provider && isEnvOnlyMode() && (providerId === 'env' || !providerId)) {
provider = this.activeProviders.find((p) => p.id.startsWith('env-'));
if (provider && modelName === 'default') {
modelName = provider.chatModels[0]?.key ?? modelName;
}
}
if (!provider) throw new Error('Invalid provider id');
return provider.provider.loadChatModel(modelName);
}
async loadEmbeddingModel(providerId: string, modelName: string) {
const provider = this.activeProviders.find((p) => p.id === providerId);
let provider = this.activeProviders.find((p) => p.id === providerId);
if (!provider && isEnvOnlyMode() && (providerId === 'env' || !providerId)) {
provider = this.activeProviders.find((p) => p.id.startsWith('env-'));
if (provider && modelName === 'default') {
modelName = provider.embeddingModels[0]?.key ?? modelName;
}
}
if (!provider) throw new Error('Invalid provider id');
return provider.provider.loadEmbeddingModel(modelName);
}