uncloseai.com/public/languages/deno/uncloseai.ts

281 lines
7.2 KiB
TypeScript

/**
* UncloseAI Deno/TypeScript Library
* OpenAI-compatible API client with streaming support
* Compatible with vLLM, Ollama, and OpenAI-compatible endpoints
*/
interface ModelInfo {
id: string;
endpoint: string;
maxTokens: number;
}
interface Message {
role: string;
content: string;
}
interface ChatOptions {
modelIdx?: number;
maxTokens?: number;
temperature?: number;
}
/**
* UncloseAI Client class
*/
export class UncloseAI {
public models: ModelInfo[] = [];
public ttsEndpoints: string[] = [];
private timeout: number;
constructor(timeout = 30000) {
this.timeout = timeout;
this.discoverModels();
}
/**
* Discover models from environment variables
*/
private async discoverModels(): Promise<void> {
console.log('Initializing UncloseAI client...');
// Discover chat/code models
for (let i = 1; i <= 9999; i++) {
const endpoint = Deno.env.get(`MODEL_ENDPOINT_${i}`);
if (!endpoint) break;
console.log(`Endpoint ${i}: ${endpoint}`);
await this.discoverModelsFromEndpoint(endpoint);
}
// Discover TTS endpoints
for (let i = 1; i <= 9999; i++) {
const endpoint = Deno.env.get(`TTS_ENDPOINT_${i}`);
if (!endpoint) break;
this.ttsEndpoints.push(endpoint);
}
console.log(`Discovered ${this.models.length} models, ${this.ttsEndpoints.length} TTS endpoints\n`);
}
/**
* Discover models from a specific endpoint
*/
private async discoverModelsFromEndpoint(endpoint: string): Promise<void> {
try {
const response = await fetch(`${endpoint}/models`, {
signal: AbortSignal.timeout(10000)
});
const data = await response.json();
if (data.data) {
for (const model of data.data) {
const modelId = model.id;
// Skip modelperm-* entries
if (modelId.startsWith('modelperm-')) continue;
const maxTokens = model.max_model_len || 8192;
this.models.push({ id: modelId, endpoint, maxTokens });
}
}
} catch (_error) {
// Silently skip failed endpoints
}
}
/**
* Non-streaming chat completion
*/
async chat(messages: Message[], options: ChatOptions = {}): Promise<string> {
const modelIdx = options.modelIdx ?? 0;
const maxTokens = options.maxTokens ?? 100;
const temperature = options.temperature ?? 0.7;
if (modelIdx >= this.models.length) {
throw new Error('Invalid model index');
}
const model = this.models[modelIdx];
const url = `${model.endpoint}/chat/completions`;
const request = {
model: model.id,
messages,
stream: false,
max_tokens: maxTokens,
temperature
};
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request),
signal: AbortSignal.timeout(this.timeout)
});
const data = await response.json();
return data.choices[0].message.content;
}
/**
* Streaming chat completion - returns an async iterator
*/
async *chatStream(messages: Message[], options: ChatOptions = {}): AsyncGenerator<string> {
const modelIdx = options.modelIdx ?? 0;
const maxTokens = options.maxTokens ?? 500;
const temperature = options.temperature ?? 0.7;
if (modelIdx >= this.models.length) {
throw new Error('Invalid model index');
}
const model = this.models[modelIdx];
const url = `${model.endpoint}/chat/completions`;
const request = {
model: model.id,
messages,
stream: true,
max_tokens: maxTokens,
temperature
};
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request),
signal: AbortSignal.timeout(this.timeout)
});
if (!response.body) {
throw new Error('No response body');
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const data = line.slice(6).trim();
if (data === '[DONE]') return;
try {
const json = JSON.parse(data);
const content = json.choices?.[0]?.delta?.content;
if (content) {
yield content;
}
} catch {
// Skip malformed JSON
}
}
}
} finally {
reader.releaseLock();
}
}
/**
* Text-to-speech generation
*/
async tts(text: string, voice = 'alloy', outputFile = '/tmp/speech.mp3'): Promise<boolean> {
if (this.ttsEndpoints.length === 0) return false;
const endpoint = this.ttsEndpoints[0];
const url = `${endpoint}/audio/speech`;
const request = {
model: 'tts-1',
voice,
input: text
};
try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request),
signal: AbortSignal.timeout(this.timeout)
});
const audioData = await response.arrayBuffer();
await Deno.writeFile(outputFile, new Uint8Array(audioData));
return true;
} catch {
return false;
}
}
}
/**
* Demo program showing library usage
*/
if (import.meta.main) {
console.log('=== UncloseAI Deno Client (with Streaming) ===\n');
// Initialize client
const client = new UncloseAI();
// Wait for discovery to complete
await new Promise(resolve => setTimeout(resolve, 1000));
if (client.models.length === 0) {
console.log('ERROR: No models discovered');
Deno.exit(1);
}
// Non-streaming chat example
console.log('=== Non-Streaming Chat ===');
console.log(`Model: ${client.models[0].id}`);
try {
const messages = [{ role: 'user', content: 'Explain quantum computing in one sentence' }];
const response = await client.chat(messages);
console.log(`Response: ${response}\n`);
} catch (error) {
console.log(`Error: ${error.message}\n`);
}
// Streaming chat example
const modelIdx = client.models.length >= 2 ? 1 : 0;
console.log('=== Streaming Chat ===');
console.log(`Model: ${client.models[modelIdx].id}`);
Deno.stdout.writeSync(new TextEncoder().encode('Response: '));
try {
const messages = [{ role: 'user', content: 'Write a hello world program in Deno TypeScript' }];
for await (const content of client.chatStream(messages, { modelIdx })) {
Deno.stdout.writeSync(new TextEncoder().encode(content));
}
console.log('\n');
} catch (error) {
console.log(`\nError: ${error.message}\n`);
}
// TTS example
if (client.ttsEndpoints.length > 0) {
console.log('=== TTS Speech Generation ===');
console.log('Model: tts-1');
if (await client.tts('Hello from UncloseAI Deno client!', 'alloy', '/tmp/speech.mp3')) {
console.log('Audio saved to /tmp/speech.mp3');
} else {
console.log('TTS failed');
}
}
console.log('\n=== Examples Complete ===');
}