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:
@@ -1,7 +1,9 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm install
|
||||
RUN --mount=type=cache,target=/root/.npm \
|
||||
npm install
|
||||
COPY tsconfig.json ./
|
||||
COPY src ./src
|
||||
RUN npm run build
|
||||
@@ -9,7 +11,8 @@ RUN npm run build
|
||||
FROM node:20-alpine
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm install --omit=dev
|
||||
RUN --mount=type=cache,target=/root/.npm \
|
||||
npm install --omit=dev
|
||||
COPY --from=builder /app/dist ./dist
|
||||
EXPOSE 3020
|
||||
ENV DATA_DIR=/app/data
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user