symmetry-core
Version:
Use this repository to become an inference provider on the Symmetry network programmatically.
275 lines (274 loc) • 12.6 kB
JavaScript
"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;