fbf04d90db
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.
91 lines
2.6 KiB
Dart
91 lines
2.6 KiB
Dart
import 'dart:convert';
|
|
import 'package:http/http.dart' as http;
|
|
import '../http_client.dart';
|
|
import '../http_util.dart';
|
|
import '../models.dart';
|
|
import '../sse.dart';
|
|
import 'base_provider.dart';
|
|
|
|
/// OpenAI-compatible `/v1/chat/completions` client used by several free proxies.
|
|
class OpenAiCompatProvider extends BaseProvider {
|
|
OpenAiCompatProvider({
|
|
required this.providerName,
|
|
required this.apiBase,
|
|
required this.fallbackModel,
|
|
List<String>? supportedModels,
|
|
this.apiKey,
|
|
this.extraHeaders = const {},
|
|
}) : supportedModels = supportedModels ?? [fallbackModel];
|
|
|
|
final String providerName;
|
|
final String apiBase;
|
|
final String fallbackModel;
|
|
final List<String> supportedModels;
|
|
final String? apiKey;
|
|
final Map<String, String> extraHeaders;
|
|
|
|
@override
|
|
String get name => providerName;
|
|
|
|
@override
|
|
bool get working => true;
|
|
|
|
@override
|
|
List<String> get models => supportedModels;
|
|
|
|
@override
|
|
String get defaultModel => fallbackModel;
|
|
|
|
String get chatUrl {
|
|
final base = apiBase.endsWith('/') ? apiBase.substring(0, apiBase.length - 1) : apiBase;
|
|
if (base.endsWith('/chat/completions')) return base;
|
|
return '$base/chat/completions';
|
|
}
|
|
|
|
@override
|
|
Stream<String> createAsyncGenerator(
|
|
String model,
|
|
List<Message> messages, {
|
|
bool stream = false,
|
|
}) async* {
|
|
final payload = <String, dynamic>{
|
|
'model': models.contains(model) ? model : defaultModel,
|
|
'messages': toOpenAiMessages(messages),
|
|
'stream': false,
|
|
'temperature': 0.8,
|
|
};
|
|
|
|
final headers = browserHeaders(
|
|
contentType: 'application/json',
|
|
accept: 'application/json',
|
|
extra: {
|
|
if (apiKey != null && apiKey!.isNotEmpty) 'Authorization': 'Bearer $apiKey',
|
|
...extraHeaders,
|
|
},
|
|
);
|
|
|
|
final req = http.Request('POST', Uri.parse(chatUrl));
|
|
req.headers.addAll(headers);
|
|
req.body = jsonEncode(payload);
|
|
|
|
final response = await g4fHttp.send(req).timeout(requestTimeout);
|
|
if (response.statusCode < 200 || response.statusCode >= 300) {
|
|
final err = await response.stream.bytesToString();
|
|
throw Exception(
|
|
'$name returned ${response.statusCode}: ${err.substring(0, err.length.clamp(0, 240))}',
|
|
);
|
|
}
|
|
|
|
final body = utf8.decode(await response.stream.toBytes());
|
|
final fromJson = extractOpenAiMessage(body);
|
|
final fromSse = extractAllSseContent(body).join();
|
|
final text = (fromJson != null && looksLikeModelOutput(fromJson))
|
|
? fromJson
|
|
: fromSse;
|
|
if (!looksLikeModelOutput(text)) {
|
|
throw Exception('$name returned unusable body');
|
|
}
|
|
yield text;
|
|
}
|
|
}
|