Files
999/lib/g4f/providers/glm.dart
T
Alma Alexyz fbf04d90db feat: embed a Flutter G4F client with Cronet, cookie jar, and origin WebView
Port the Oracle off the Python g4f container onto on-device providers: Chromium HTTP via Cronet, a shared cookie session, IterList retry, hosted OpenAI-compat aggregators, and same-origin WebView fallbacks instead of CORS fetch.
2026-09-01 22:05:38 +05:00

165 lines
5.4 KiB
Dart

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<String> 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<String> createAsyncGenerator(
String model,
List<Message> 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<String, dynamic>;
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<String, dynamic>;
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;
}
}