UNPKG

symmetry-core

Version:

Use this repository to become an inference provider on the Symmetry network programmatically.

275 lines (274 loc) 12.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SymmetryClient = void 0; const chalk_1 = __importDefault(require("chalk")); const hyperswarm_1 = __importDefault(require("hyperswarm")); const hypercore_crypto_1 = __importDefault(require("hypercore-crypto")); const node_fs_1 = __importDefault(require("node:fs")); const js_yaml_1 = __importDefault(require("js-yaml")); const crypto_1 = __importDefault(require("crypto")); const package_json_1 = require("../package.json"); const fluency_js_1 = require("fluency.js"); const config_1 = require("./config"); const utils_1 = require("./utils"); const logger_1 = require("./logger"); const constants_1 = require("./constants"); const connection_manager_1 = require("./connection-manager"); class SymmetryClient { constructor(configPath) { this._connectionManager = null; this._conversationIndex = 0; this._discoveryKey = null; this._providerConnections = 0; this._providerSwarm = null; this._serverPeer = null; this._serverSwarm = null; this.handleConnection = (peer) => { var _a; this._serverPeer = peer; peer.write((0, utils_1.createMessage)(constants_1.serverMessageKeys.join, { ...this._config.getAll(), symmetryCoreVersion: package_json_1.version, discoveryKey: (_a = this._discoveryKey) === null || _a === void 0 ? void 0 : _a.toString("hex"), apiKey: "", })); this.testProviderCall(); peer.on("data", async (buffer) => { var _a; if (!buffer) return; const data = (0, utils_1.safeParseJson)(buffer.toString()); if (data && data.key) { switch (data.key) { case constants_1.serverMessageKeys.versionMismatch: { const message = data.data; logger_1.logger.info(` ❌ Version mismatch minimum required symmetry client is v${message.minVersion}; Destroying connection, please update. `.trim()); (_a = this._connectionManager) === null || _a === void 0 ? void 0 : _a.destroy(); break; } case constants_1.serverMessageKeys.inference: logger_1.logger.info(chalk_1.default.white(`🔗 Received inference request from server.`)); this.handleInferenceRequest(data, peer); break; case constants_1.serverMessageKeys.healthCheck: this.handleHealthCheckRequest(peer); break; case constants_1.serverMessageKeys.healthCheckAck: this.handleHealthCheckAck(); break; } } }); }; this.getIsOpenAICompatible = (provider) => { const providers = Object.values(constants_1.apiProviders); return providers.includes(provider); }; this.getProviderBaseUrl = () => { return `${this._config.get("apiProtocol")}://${this._config.get("apiHostname")}${this._config.get("apiPort") ? `:${this._config.get("apiPort")}` : ""}${this._config.get("apiBasePath") ? this._config.get("apiBasePath") : ""}`; }; logger_1.logger.info(`🔗 Initializing client using config file: ${configPath}`); this._config = new config_1.ConfigManager(configPath); } async init() { const userSecret = await this.getOrCreateUserSecret(); const keyPair = hypercore_crypto_1.default.keyPair(crypto_1.default.createHash("sha256").update(userSecret).digest()); logger_1.logger.info(`📁 Symmetry client initialized.`); logger_1.logger.info(chalk_1.default.white(`🔑 Server key: ${this._config.get("serverKey")}`)); logger_1.logger.info(chalk_1.default.white("🔗 Joining server, please wait.")); this._providerSwarm = new hyperswarm_1.default(); this._discoveryKey = hypercore_crypto_1.default.discoveryKey(keyPair.publicKey); const discovery = this._providerSwarm.join(this._discoveryKey, { server: true, client: true, }); await discovery.flushed(); this._providerSwarm.on("error", (err) => { logger_1.logger.error(chalk_1.default.red("🚨 Swarm Error:"), err); }); this._providerSwarm.on("connection", (peer) => { logger_1.logger.info(`⚡️ New connection from peer: ${peer.rawStream.remoteHost}`); this.listeners(peer); }); this.joinServer({ keyPair }); process.on("SIGINT", async () => { var _a; await ((_a = this._providerSwarm) === null || _a === void 0 ? void 0 : _a.destroy()); process.exit(0); }); process.on("uncaughtException", (err) => { if (err.message === "connection reset by peer") { this._providerConnections = Math.max(0, this._providerConnections - 1); } }); } async getOrCreateUserSecret() { const userSecret = this._config.get("userSecret"); if (userSecret) return userSecret; const newSecret = hypercore_crypto_1.default.randomBytes(32).toString("hex"); logger_1.logger.info(chalk_1.default.white(`🔒 Secret not created, writing new secret to config file...`)); await node_fs_1.default.promises.writeFile(this._config.getConfigPath(), js_yaml_1.default.dump({ ...this._config.getAll(), userSecret: newSecret, }), "utf8"); return newSecret; } async testProviderCall() { const testCall = async () => { var _a; logger_1.logger.info(chalk_1.default.white(`👋 Saying hello to your provider...`)); const url = this.getProviderBaseUrl(); this._fluencyJs = new fluency_js_1.TokenJS({ baseURL: url, apiKey: this._config.get("apiKey"), }); logger_1.logger.info(chalk_1.default.white(`🚀 Sending test request to ${url}`)); try { await ((_a = this._fluencyJs) === null || _a === void 0 ? void 0 : _a.chat.completions.create({ model: this._config.get("modelName"), messages: [ { role: "user", content: "Hello, this is a test message." }, ], stream: true, provider: "openai-compatible", })); } catch (error) { let errorMessage = "Health check failed"; if (error instanceof Error) errorMessage = error.message; logger_1.logger.error(`🚨 Health check error: ${errorMessage}`); this.destroySwarms(); throw new Error(errorMessage); } logger_1.logger.info(chalk_1.default.green(`✅ Test inference call successful!`)); }; setTimeout(() => testCall(), constants_1.PROVIDER_HELLO_TIMEOUT); } async joinServer(opts) { const serverKey = Buffer.from(this._config.get("serverKey")); this._connectionManager = new connection_manager_1.ConnectionManager({ onConnection: this.handleConnection, serverKey, swarmOptions: opts, onDisconnection: () => (this._serverPeer = null), }); await this._connectionManager.connect(); } async destroySwarms() { var _a, _b; await ((_a = this._providerSwarm) === null || _a === void 0 ? void 0 : _a.destroy()); await ((_b = this._serverSwarm) === null || _b === void 0 ? void 0 : _b.destroy()); } getServerPublicKey(serverKeyHex) { const publicKey = Buffer.from(serverKeyHex, "hex"); if (publicKey.length !== 32) { throw new Error(`Expected a 32-byte public key, but got ${publicKey.length} bytes`); } return publicKey; } listeners(peer) { peer.on("data", async (buffer) => { if (!buffer) return; const data = (0, utils_1.safeParseJson)(buffer.toString()); if (data && data.key) { switch (data.key) { case constants_1.serverMessageKeys.newConversation: this._conversationIndex = this._conversationIndex + 1; break; case constants_1.serverMessageKeys.inference: logger_1.logger.info(`📦 Inference message received from ${peer.rawStream.remoteHost}`); await this.handleInferenceRequest(data, peer); break; } } }); } getMessagesWithSystem(messages) { const systemMessage = this._config.get("systemMessage"); const hasSystem = messages.some((m) => m.role === "system"); if (systemMessage && !hasSystem) { return [ { role: "system", content: systemMessage, }, ...messages, ]; } return messages; } async handleHealthCheckAck() { logger_1.logger.info(`🤖 Health check ack received from server.`); } async handleHealthCheckRequest(peer) { var _a; logger_1.logger.info("🤖 Health check request received."); this._fluencyJs = new fluency_js_1.TokenJS({ baseURL: this.getProviderBaseUrl(), apiKey: this._config.get("apiKey"), }); const body = { model: this._config.get("modelName"), messages: [ { role: "user", content: `Hello, reply with one word only if you are alive. e.g "alive".`, }, ], stream: true, provider: "openai-compatible", }; try { await ((_a = this._fluencyJs) === null || _a === void 0 ? void 0 : _a.chat.completions.create(body)); peer.write((0, utils_1.createMessage)(constants_1.serverMessageKeys.healthCheck)); } catch (error) { let errorMessage = "Health check failed"; if (error instanceof Error) errorMessage = error.message; logger_1.logger.error(`🚨 Health check error: ${errorMessage}`); } } async handleInferenceRequest(data, peer) { var _a, _b; (_a = this._serverPeer) === null || _a === void 0 ? void 0 : _a.write((0, utils_1.createMessage)(constants_1.serverMessageKeys.inference)); const messages = this.getMessagesWithSystem(data === null || data === void 0 ? void 0 : data.data.messages); this._fluencyJs = new fluency_js_1.TokenJS({ baseURL: this.getProviderBaseUrl(), apiKey: this._config.get("apiKey"), }); try { const result = await this._fluencyJs.chat.completions.create({ model: this._config.get("modelName"), messages: messages || undefined, stream: true, provider: "openai-compatible" }); for await (const part of result) { peer.write(JSON.stringify(part)); } peer.write((0, utils_1.createMessage)(constants_1.serverMessageKeys.inferenceEnded, data === null || data === void 0 ? void 0 : data.data.key)); await new Promise((resolve) => peer.once("drain", resolve)); } catch (error) { let errorMessage = "An error occurred during inference"; if (error instanceof Error) errorMessage = error.message; logger_1.logger.error(`🚨 ${errorMessage}`); (_b = this._serverPeer) === null || _b === void 0 ? void 0 : _b.write((0, utils_1.createMessage)(constants_1.serverMessageKeys.inferenceError, { requestId: data.data.key, error: errorMessage, })); } } } exports.SymmetryClient = SymmetryClient; exports.default = SymmetryClient;