import 'dart:convert'; import 'package:http/http.dart' as http; /// OpenRouter API client for Dart class OpenRouter { final String apiKey; final String baseUrl; final http.Client _client; OpenRouter({ required this.apiKey, this.baseUrl = 'https://openrouter.ai/api/v1', http.Client? client, }) : _client = client ?? http.Client(); ChatCompletions get chat => ChatCompletions(this); Embeddings get embeddings => Embeddings(this); void dispose() { _client.close(); } } class ChatCompletions { final OpenRouter _openRouter; ChatCompletions(this._openRouter); Completions get completions => Completions(_openRouter); } class Completions { final OpenRouter _openRouter; Completions(this._openRouter); Future create({ required String model, required List messages, bool stream = false, StreamOptions? streamOptions, double? temperature, double? topP, int? n, List? stop, int? maxTokens, double? presencePenalty, double? frequencyPenalty, Map? logitBias, String? user, }) async { final body = { 'model': model, 'messages': messages, 'stream': stream, if (streamOptions != null) 'stream_options': streamOptions.toJson(), if (temperature != null) 'temperature': temperature, if (topP != null) 'top_p': topP, if (n != null) 'n': n, if (stop != null) 'stop': stop, if (maxTokens != null) 'max_tokens': maxTokens, if (presencePenalty != null) 'presence_penalty': presencePenalty, if (frequencyPenalty != null) 'frequency_penalty': frequencyPenalty, if (logitBias != null) 'logit_bias': logitBias, if (user != null) 'user': user, }; if (stream) { return _streamCompletion(body); } else { return _createCompletion(body); } } Future _createCompletion(Map body) async { final response = await _openRouter._client.post( Uri.parse('${_openRouter.baseUrl}/chat/completions'), headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ${_openRouter.apiKey}', }, body: jsonEncode(body), ); if (response.statusCode != 200) { throw OpenRouterException( statusCode: response.statusCode, message: response.body, ); } return ChatCompletion.fromJson(jsonDecode(response.body)); } Stream _streamCompletion( Map body) async* { final request = http.Request( 'POST', Uri.parse('${_openRouter.baseUrl}/chat/completions'), ); request.headers.addAll({ 'Content-Type': 'application/json', 'Authorization': 'Bearer ${_openRouter.apiKey}', }); request.body = jsonEncode(body); final streamedResponse = await _openRouter._client.send(request); if (streamedResponse.statusCode != 200) { final body = await streamedResponse.stream.bytesToString(); throw OpenRouterException( statusCode: streamedResponse.statusCode, message: body, ); } final stream = streamedResponse.stream .transform(utf8.decoder) .transform(const LineSplitter()); await for (final line in stream) { if (line.isEmpty) continue; if (line.startsWith(':')) continue; if (!line.startsWith('data: ')) continue; final data = line.substring(6); if (data == '[DONE]') { break; } try { final json = jsonDecode(data); yield ChatCompletionChunk.fromJson(json); } catch (e) { continue; } } } } class ChatMessage { final String role; final String content; ChatMessage({ required this.role, required this.content, }); Map toJson() => { 'role': role, 'content': content, }; factory ChatMessage.fromJson(Map json) => ChatMessage( role: json['role'], content: json['content'], ); factory ChatMessage.system(String content) => ChatMessage(role: 'system', content: content); factory ChatMessage.user(String content) => ChatMessage(role: 'user', content: content); factory ChatMessage.assistant(String content) => ChatMessage(role: 'assistant', content: content); } class StreamOptions { final bool includeUsage; StreamOptions({this.includeUsage = false}); Map toJson() => { 'include_usage': includeUsage, }; } class ChatCompletion { final String id; final String object; final int created; final String model; final List choices; final Usage? usage; ChatCompletion({ required this.id, required this.object, required this.created, required this.model, required this.choices, this.usage, }); factory ChatCompletion.fromJson(Map json) => ChatCompletion( id: json['id'], object: json['object'], created: json['created'], model: json['model'], choices: (json['choices'] as List) .map((c) => Choice.fromJson(c)) .toList(), usage: json['usage'] != null ? Usage.fromJson(json['usage']) : null, ); } class ChatCompletionChunk { final String id; final String object; final int created; final String model; final List choices; final Usage? usage; ChatCompletionChunk({ required this.id, required this.object, required this.created, required this.model, required this.choices, this.usage, }); factory ChatCompletionChunk.fromJson(Map json) => ChatCompletionChunk( id: json['id'], object: json['object'], created: json['created'], model: json['model'], choices: (json['choices'] as List) .map((c) => ChunkChoice.fromJson(c)) .toList(), usage: json['usage'] != null ? Usage.fromJson(json['usage']) : null, ); } class Choice { final int index; final ChatMessage message; final String? finishReason; Choice({ required this.index, required this.message, this.finishReason, }); factory Choice.fromJson(Map json) => Choice( index: json['index'], message: ChatMessage.fromJson(json['message']), finishReason: json['finish_reason'], ); } class ChunkChoice { final int index; final Delta? delta; final String? finishReason; ChunkChoice({ required this.index, this.delta, this.finishReason, }); factory ChunkChoice.fromJson(Map json) => ChunkChoice( index: json['index'], delta: json['delta'] != null ? Delta.fromJson(json['delta']) : null, finishReason: json['finish_reason'], ); } class Delta { final String? role; final String? content; Delta({ this.role, this.content, }); factory Delta.fromJson(Map json) => Delta( role: json['role'], content: json['content'], ); } class Usage { final int? promptTokens; final int? completionTokens; final int? totalTokens; Usage({ this.promptTokens, this.completionTokens, this.totalTokens, }); factory Usage.fromJson(Map json) => Usage( promptTokens: json['prompt_tokens'], completionTokens: json['completion_tokens'], totalTokens: json['total_tokens'], ); Map toJson() => { if (promptTokens != null) 'prompt_tokens': promptTokens, if (completionTokens != null) 'completion_tokens': completionTokens, if (totalTokens != null) 'total_tokens': totalTokens, }; } class OpenRouterException implements Exception { final int statusCode; final String message; OpenRouterException({ required this.statusCode, required this.message, }); @override String toString() => 'OpenRouterException($statusCode): $message'; } class Embeddings { final OpenRouter _openRouter; Embeddings(this._openRouter); Future create({ required String model, required dynamic input, String? user, String? encodingFormat, int? dimensions, }) async { final body = { 'model': model, 'input': input, if (user != null) 'user': user, if (encodingFormat != null) 'encoding_format': encodingFormat, if (dimensions != null) 'dimensions': dimensions, }; final response = await _openRouter._client.post( Uri.parse('${_openRouter.baseUrl}/embeddings'), headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ${_openRouter.apiKey}', }, body: jsonEncode(body), ); if (response.statusCode != 200) { throw OpenRouterException( statusCode: response.statusCode, message: response.body, ); } return EmbeddingResponse.fromJson(jsonDecode(response.body)); } } class EmbeddingResponse { final String object; final List data; final String model; final Usage? usage; EmbeddingResponse({ required this.object, required this.data, required this.model, this.usage, }); factory EmbeddingResponse.fromJson(Map json) => EmbeddingResponse( object: (json['object'] ?? '').toString(), data: (json['data'] as List? ?? []).map((e) => Embedding.fromJson(e)).toList(), model: (json['model'] ?? '').toString(), usage: json['usage'] != null ? Usage.fromJson(json['usage']) : null, ); } class Embedding { final String object; final int index; final List embedding; Embedding({ required this.object, required this.index, required this.embedding, }); factory Embedding.fromJson(Map json) => Embedding( object: (json['object'] ?? '').toString(), index: (json['index'] ?? 0) as int, embedding: (json['embedding'] as List? ?? []) .map((e) => (e as num).toDouble()) .toList(), ); }