import 'dart:convert'; import 'package:crypto/crypto.dart'; import 'package:http/http.dart' as http; import '../http_client.dart'; import 'package:uuid/uuid.dart'; import '../http_util.dart'; import '../models.dart'; import '../sse.dart'; import 'base_provider.dart'; /// z.ai GLM web chat with a guest JWT. Captcha-protected replies fail fast. class GlmZai extends BaseProvider { @override String get name => 'GLM'; @override bool get working => true; @override List get models => ['x-preview-l', 'GLM-5-Turbo', 'glm-4.7']; @override String get defaultModel => 'x-preview-l'; static const _base = 'https://chat.z.ai'; static const _uuid = Uuid(); @override Stream createAsyncGenerator( String model, List messages, { bool stream = false, }) async* { final authRes = await g4fHttp.get( Uri.parse('$_base/api/v1/auths/'), headers: browserHeaders(origin: _base, referer: '$_base/', desktop: true), ).timeout(const Duration(seconds: 12)); if (authRes.statusCode != 200) { throw Exception('GLM auth ${authRes.statusCode}'); } final auth = jsonDecode(authRes.body) as Map; final token = auth['token']?.toString(); final userId = auth['id']?.toString() ?? ''; if (token == null || token.isEmpty) { throw Exception('GLM: no guest token'); } final headers = browserHeaders( origin: _base, referer: '$_base/', contentType: 'application/json', desktop: true, extra: { 'Authorization': 'Bearer $token', 'x-fe-version': 'prod-fe-1.1.69', 'x-region': 'overseas', }, ); String useModel = defaultModel; try { final modelsRes = await g4fHttp.get( Uri.parse('$_base/api/models'), headers: headers, ).timeout(const Duration(seconds: 10)); if (modelsRes.statusCode == 200) { final data = jsonDecode(modelsRes.body); final list = (data is Map ? data['data'] : data) as List?; if (list != null && list.isNotEmpty) { final ids = [ for (final m in list) if (m is Map && m['id'] != null) m['id'].toString() ]; if (ids.contains(model)) { useModel = model; } else if (ids.isNotEmpty) { useModel = ids.first; } } } } catch (_) {} final chatRes = await g4fHttp.post( Uri.parse('$_base/api/v1/chats/new'), headers: headers, body: jsonEncode({ 'chat': { 'id': _uuid.v4(), 'title': 'New Chat', 'models': [useModel], 'params': {}, 'history': {'messages': {}, 'currentId': null}, 'tags': [], 'flags': [], 'features': [], } }), ).timeout(const Duration(seconds: 12)); if (chatRes.statusCode != 200) { throw Exception('GLM new chat ${chatRes.statusCode}'); } final chatJson = jsonDecode(chatRes.body) as Map; final chatId = (chatJson['id'] ?? (chatJson['chat'] as Map?)?['id'])?.toString(); if (chatId == null) throw Exception('GLM: no chat id'); final ts = DateTime.now().millisecondsSinceEpoch.toString(); final requestId = _uuid.v4(); final sigSrc = 'requestId=$requestId,timestamp=$ts,user_id=$userId'; final signature = Hmac(sha256, utf8.encode('')).convert(utf8.encode(sigSrc)).toString(); headers['x-signature'] = signature; final lastUser = getLastUserMessage(messages); final body = jsonEncode({ 'stream': true, 'model': useModel, 'messages': [ {'role': 'user', 'content': lastUser} ], 'signature_prompt': lastUser.substring(0, lastUser.length.clamp(0, 500)), 'params': {}, 'extra': {}, 'features': { 'image_generation': false, 'web_search': false, 'auto_web_search': false, 'preview_mode': true, 'enable_thinking': false, }, 'chat_id': chatId, 'id': _uuid.v4(), }); final url = Uri.parse( '$_base/api/v2/chat/completions?timestamp=$ts&requestId=$requestId&user_id=$userId&version=0.0.1&platform=web&token=$token', ); final req = http.Request('POST', url); req.headers.addAll(headers); req.body = body; final res = await g4fHttp.send(req).timeout(const Duration(seconds: 30)); if (res.statusCode != 200) { throw Exception('GLM completions ${res.statusCode}'); } final collected = StringBuffer(); await for (final line in splitSseLines(res.stream.transform(utf8.decoder))) { final trimmed = line.trim(); if (trimmed.isEmpty || !trimmed.startsWith('data:')) continue; final data = trimmed.substring(5).trim(); if (data.contains('CAPTCHA') || data.contains('captcha')) { throw Exception('GLM captcha required'); } final content = extractSseContent(data); if (content != null && content.isNotEmpty) { collected.write(content); if (stream) yield content; } } final text = collected.toString().trim(); if (!looksLikeModelOutput(text)) { throw Exception('GLM empty response'); } if (!stream) yield text; } }