rynn-k / gists
z-ai.js javascript
const crypto = require('crypto');
const axios = require('axios');

class GLM {
    constructor() {
        this.url = 'https://chat.z.ai';
        this.apiEndpoint = 'https://chat.z.ai/api/v2/chat/completions';
        this.apiKey = null;
        this.authUserId = null;
        this.authCache = null;
        this.authCacheTime = null;
        this.models = {
            'glm-4.6': 'GLM-4-6-API-V1',
            'glm-4.6v': 'glm-4.6v',
            'glm-4.5': '0727-360B-API',
            'glm-4.5-air': '0727-106B-API',
            'glm-4.5v': 'glm-4.5v',
            'glm-4.1v-9b-thinking': 'GLM-4.1V-Thinking-FlashX',
            'z1-rumination': 'deep-research',
            'z1-32b': 'zero',
            'chatglm': 'glm-4-flash',
            '0808-360b-dr': '0808-360B-DR',
            'glm-4-32b': 'glm-4-air-250414'
        };
    }
    
    createSignature(sortedPayload, userPrompt) {
        const currentTime = Date.now();
        const dataString = `${sortedPayload}|${Buffer.from(userPrompt).toString('base64')}|${currentTime}`;
        const timeWindow = Math.floor(currentTime / (5 * 60 * 1000));
        const baseSignature = crypto.createHmac('sha256', 'key-@@@@)))()((9))-xxxx&&&%%%%%').update(String(timeWindow)).digest('hex');
        const signature = crypto.createHmac('sha256', baseSignature).update(dataString).digest('hex');
        return { signature, timestamp: currentTime };
    }
    
    buildEndpoint(token, userId, userPrompt) {
        const currentTime = String(Date.now());
        const requestId = crypto.randomUUID();
        const timezoneOffset = -new Date().getTimezoneOffset();
        const now = new Date();
        
        const basicParams = {
            timestamp: currentTime,
            requestId: requestId,
            user_id: userId,
        };
        
        const additionalParams = {
            version: '0.0.1',
            platform: 'web',
            token: token,
            user_agent: 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Mobile Safari/537.36',
            language: 'en-US',
            languages: 'en-US',
            timezone: 'Asia/Makassar',
            cookie_enabled: 'true',
            screen_width: '360',
            screen_height: '806',
            screen_resolution: '360x806',
            viewport_height: '714',
            viewport_width: '360',
            viewport_size: '360x714',
            color_depth: '24',
            pixel_ratio: '2',
            current_url: 'https://chat.z.ai/c/25455c46-9de3-4689-9e0a-0f9f70c5b67e',
            pathname: '/c/25455c46-9de3-4689-9e0a-0f9f70c5b67e',
            search: '',
            hash: '',
            host: 'chat.z.ai',
            hostname: 'chat.z.ai',
            protocol: 'https:',
            referrer: '',
            title: 'Z.ai Chat - Free AI powered by GLM-4.6 & GLM-4.5',
            timezone_offset: String(timezoneOffset),
            local_time: now.toISOString(),
            utc_time: now.toUTCString(),
            is_mobile: 'true',
            is_touch: 'true',
            max_touch_points: '2',
            browser_name: 'Chrome',
            os_name: 'Android',
        };
        
        const allParams = { ...basicParams, ...additionalParams };
        const urlParams = new URLSearchParams(allParams).toString();
        const sortedPayload = Object.keys(basicParams).sort().map(k => `${k},${basicParams[k]}`).join(',');
        const { signature, timestamp } = this.createSignature(sortedPayload, userPrompt.trim());
        
        return {
            endpoint: `${this.apiEndpoint}?${urlParams}&signature_timestamp=${timestamp}`,
            signature
        };
    }
    
    async authenticate() {
        if (this.authCache && this.authCacheTime && (Date.now() - this.authCacheTime) / 1000 < 300) {
            return this.authCache;
        }
        const response = (await axios.get(`${this.url}/api/v1/auths/`)).data;
        this.authCache = response;
        this.authCacheTime = Date.now();
        this.apiKey = response.token;
        this.authUserId = response.id;
        return response;
    }
    
    getDateTime() {
        const now = new Date();
        const pad = n => String(n).padStart(2, '0');
        const year = now.getFullYear();
        const month = pad(now.getMonth() + 1);
        const day = pad(now.getDate());
        const hours = pad(now.getHours());
        const minutes = pad(now.getMinutes());
        const seconds = pad(now.getSeconds());
        const days = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
        
        return {
            datetime: `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`,
            date: `${year}-${month}-${day}`,
            time: `${hours}:${minutes}:${seconds}`,
            weekday: days[now.getDay()]
        };
    }
    
    async chat(messages, options = {}) {
        try {
            const {
                model = 'glm-4.6',
                systemMessage,
                search = false,
                reasoning = false,
                userName
            } = options;
            
            await this.authenticate();
            
            if (!this.models[model]) throw new Error(`Available models: ${Object.keys(this.models).join(', ')}.`);
            if (!this.apiKey) throw new Error('Failed to obtain API key.');
            
            const msgs = [];
            if (systemMessage) msgs.push({ role: 'system', content: systemMessage });
            
            if (typeof messages === 'string') {
                msgs.push({ role: 'user', content: messages });
            } else {
                msgs.push(...messages);
            }
            
            const userPrompt = [...msgs].reverse().find(m => m.role === 'user')?.content || '';
            const { endpoint, signature } = this.buildEndpoint(this.apiKey, this.authUserId, userPrompt);
            const dateTime = this.getDateTime();
            const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'Asia/Makassar';
            
            const response = await axios.post(endpoint, {
                stream: true,
                model: this.models[model],
                messages: msgs,
                signature_prompt: userPrompt,
                params: {},
                features: {
                    image_generation: false,
                    web_search: search,
                    auto_web_search: search,
                    preview_mode: true,
                    flags: [],
                    enable_thinking: reasoning
                },
                variables: {
                    '{{USER_NAME}}': userName || `Guest-${Date.now()}`,
                    '{{USER_LOCATION}}': 'Unknown',
                    '{{CURRENT_DATETIME}}': dateTime.datetime,
                    '{{CURRENT_DATE}}': dateTime.date,
                    '{{CURRENT_TIME}}': dateTime.time,
                    '{{CURRENT_WEEKDAY}}': dateTime.weekday,
                    '{{CURRENT_TIMEZONE}}': timezone,
                    '{{USER_LANGUAGE}}': 'en-US'
                },
                chat_id: options.chatId || crypto.randomUUID(),
                id: crypto.randomUUID(),
                current_user_message_id: crypto.randomUUID(),
                current_user_message_parent_id: null,
                background_tasks: {
                    title_generation: true,
                    tags_generation: true
                }
            }, {
                headers: {
                    'Authorization': `Bearer ${this.apiKey}`,
                    'X-FE-Version': 'prod-fe-1.0.150',
                    'X-Signature': signature,
                    'Content-Type': 'application/json',
                    'Accept': 'text/event-stream',
                    'User-Agent': 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/127.0.0.0 Mobile Safari/537.36',
                    'Origin': 'https://chat.z.ai',
                    'Referer': 'https://chat.z.ai/'
                },
                responseType: 'stream'
            });
            
            let fullContent = '';
            let reasoningContent = '';
            let searchResults = [];
            let lineBuffer = '';
            let mainBuffer = [];
            let lastYieldedAnswerLength = 0;
            let answerStartIndex = -1;
            let inAnswerPhase = false;
            
            for await (const chunk of response.data) {
                lineBuffer += chunk.toString();
                const lines = lineBuffer.split('\n');
                lineBuffer = lines.pop() || '';
                
                for (const line of lines) {
                    if (!line.startsWith('data: ')) continue;
                    
                    try {
                        const jsonData = JSON.parse(line.slice(6));
                        if (jsonData.type !== 'chat:completion') continue;
                        
                        const eventData = jsonData.data;
                        if (!eventData) continue;
                        
                        const phase = eventData.phase;
                        
                        if (typeof eventData.edit_index === 'number') {
                            const index = eventData.edit_index;
                            const contentChunk = (eventData.edit_content || '').split('');
                            mainBuffer.splice(index, contentChunk.length, ...contentChunk);
                            
                            if (inAnswerPhase && answerStartIndex >= 0 && index >= answerStartIndex) {
                                const currentAnswer = mainBuffer.slice(answerStartIndex).join('');
                                if (currentAnswer.length > lastYieldedAnswerLength) {
                                    fullContent += currentAnswer.slice(lastYieldedAnswerLength);
                                    lastYieldedAnswerLength = currentAnswer.length;
                                }
                            }
                        } else if (eventData.delta_content) {
                            const contentChunk = eventData.delta_content.split('');
                            mainBuffer.splice(mainBuffer.length, 0, ...contentChunk);
                            
                            if (phase === 'thinking') {
                                let cleaned = eventData.delta_content
                                    .replace(/<details[^>]*>/g, '')
                                    .replace(/<\/details>/g, '')
                                    .replace(/<summary>.*?<\/summary>/gs, '')
                                    .replace(/^>\s?/gm, '');
                                
                                if (cleaned.trim()) reasoningContent += cleaned;
                            } else if (phase === 'answer') {
                                if (!inAnswerPhase) {
                                    inAnswerPhase = true;
                                    const fullText = mainBuffer.join('');
                                    const detailsEnd = fullText.lastIndexOf('</details>');
                                    answerStartIndex = detailsEnd >= 0 ? detailsEnd + '</details>'.length : mainBuffer.length - contentChunk.length;
                                }
                                fullContent += eventData.delta_content;
                                lastYieldedAnswerLength += eventData.delta_content.length;
                            }
                        }
                        
                        if (phase === 'done' && eventData.done) {
                            const fullOutput = mainBuffer.join('');
                            const toolCallMatch = fullOutput.match(/<glm_block[^>]*>([\s\S]*?)<\/glm_block>/);
                            if (toolCallMatch) {
                                try {
                                    const dt = JSON.parse(toolCallMatch[1]);
                                    const results = dt?.data?.browser?.search_result;
                                    if (results && results.length > 0) searchResults = results;
                                } catch (e) {}
                            }
                        }
                    } catch (e) {}
                }
            }
            
            return {
                reasoning: reasoningContent.trim(),
                content: fullContent.trim(),
                search: searchResults
            };
        } catch (error) {
            throw new Error(error.message);
        }
    }
}

// Usage:
const glm = new GLM();
glm.chat('hi!', { model: 'glm-4.5' }).then(console.log);
12758 bytes ยท Updated Feb 27, 2026