| 
									
										
										
										
											2024-10-07 11:26:25 +00:00
										 |  |  | using System.Net.Http.Headers; | 
					
						
							| 
									
										
										
										
											2024-07-03 18:31:04 +00:00
										 |  |  | using System.Runtime.CompilerServices; | 
					
						
							|  |  |  | using System.Text; | 
					
						
							|  |  |  | using System.Text.Json; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | using AIStudio.Chat; | 
					
						
							|  |  |  | using AIStudio.Provider.OpenAI; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | namespace AIStudio.Provider.SelfHosted; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-03 14:24:40 +00:00
										 |  |  | public sealed class ProviderSelfHosted(ILogger logger, Host host, string hostname) : BaseProvider($"{hostname}{host.BaseURL()}", logger) | 
					
						
							| 
									
										
										
										
											2024-07-03 18:31:04 +00:00
										 |  |  | { | 
					
						
							|  |  |  |     private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new() | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     #region Implementation of IProvider | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-03 14:24:40 +00:00
										 |  |  |     public override string Id => LLMProviders.SELF_HOSTED.ToName(); | 
					
						
							| 
									
										
										
										
											2024-07-03 18:31:04 +00:00
										 |  |  |      | 
					
						
							| 
									
										
										
										
											2024-12-03 14:24:40 +00:00
										 |  |  |     public override string InstanceName { get; set; } = "Self-hosted"; | 
					
						
							| 
									
										
										
										
											2024-07-03 18:31:04 +00:00
										 |  |  |      | 
					
						
							| 
									
										
										
										
											2024-09-01 18:10:03 +00:00
										 |  |  |     /// <inheritdoc /> | 
					
						
							| 
									
										
										
										
											2024-12-03 14:24:40 +00:00
										 |  |  |     public override async IAsyncEnumerable<string> StreamChatCompletion(Provider.Model chatModel, ChatThread chatThread, [EnumeratorCancellation] CancellationToken token = default) | 
					
						
							| 
									
										
										
										
											2024-07-03 18:31:04 +00:00
										 |  |  |     { | 
					
						
							| 
									
										
										
										
											2024-10-07 11:26:25 +00:00
										 |  |  |         // Get the API key: | 
					
						
							|  |  |  |         var requestedSecret = await RUST_SERVICE.GetAPIKey(this, isTrying: true); | 
					
						
							|  |  |  |          | 
					
						
							| 
									
										
										
										
											2024-07-03 18:31:04 +00:00
										 |  |  |         // Prepare the system prompt: | 
					
						
							|  |  |  |         var systemPrompt = new Message | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |             Role = "system", | 
					
						
							|  |  |  |             Content = chatThread.SystemPrompt, | 
					
						
							|  |  |  |         }; | 
					
						
							|  |  |  |          | 
					
						
							|  |  |  |         // Prepare the OpenAI HTTP chat request: | 
					
						
							|  |  |  |         var providerChatRequest = JsonSerializer.Serialize(new ChatRequest | 
					
						
							|  |  |  |         { | 
					
						
							| 
									
										
										
										
											2024-07-16 08:28:13 +00:00
										 |  |  |             Model = chatModel.Id, | 
					
						
							| 
									
										
										
										
											2024-07-03 18:31:04 +00:00
										 |  |  |              | 
					
						
							|  |  |  |             // Build the messages: | 
					
						
							|  |  |  |             // - First of all the system prompt | 
					
						
							|  |  |  |             // - Then none-empty user and AI messages | 
					
						
							|  |  |  |             Messages = [systemPrompt, ..chatThread.Blocks.Where(n => n.ContentType is ContentType.TEXT && !string.IsNullOrWhiteSpace((n.Content as ContentText)?.Text)).Select(n => new Message | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 Role = n.Role switch | 
					
						
							|  |  |  |                 { | 
					
						
							|  |  |  |                     ChatRole.USER => "user", | 
					
						
							|  |  |  |                     ChatRole.AI => "assistant", | 
					
						
							| 
									
										
										
										
											2024-08-01 19:53:28 +00:00
										 |  |  |                     ChatRole.AGENT => "assistant", | 
					
						
							| 
									
										
										
										
											2024-07-03 18:31:04 +00:00
										 |  |  |                     ChatRole.SYSTEM => "system", | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                     _ => "user", | 
					
						
							|  |  |  |                 }, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 Content = n.Content switch | 
					
						
							|  |  |  |                 { | 
					
						
							|  |  |  |                     ContentText text => text.Text, | 
					
						
							|  |  |  |                     _ => string.Empty, | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |             }).ToList()], | 
					
						
							|  |  |  |              | 
					
						
							|  |  |  |             // Right now, we only support streaming completions: | 
					
						
							|  |  |  |             Stream = true, | 
					
						
							|  |  |  |             MaxTokens = -1, | 
					
						
							|  |  |  |         }, JSON_SERIALIZER_OPTIONS); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-07 11:26:25 +00:00
										 |  |  |         StreamReader? streamReader = default; | 
					
						
							|  |  |  |         try | 
					
						
							| 
									
										
										
										
											2024-07-03 18:31:04 +00:00
										 |  |  |         { | 
					
						
							| 
									
										
										
										
											2024-10-07 11:26:25 +00:00
										 |  |  |             // Build the HTTP post request: | 
					
						
							| 
									
										
										
										
											2024-12-03 14:24:40 +00:00
										 |  |  |             var request = new HttpRequestMessage(HttpMethod.Post, host.ChatURL()); | 
					
						
							| 
									
										
										
										
											2024-10-07 11:26:25 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |             // Set the authorization header: | 
					
						
							|  |  |  |             if (requestedSecret.Success) | 
					
						
							|  |  |  |                 request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await requestedSecret.Secret.Decrypt(ENCRYPTION)); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             // Set the content: | 
					
						
							|  |  |  |             request.Content = new StringContent(providerChatRequest, Encoding.UTF8, "application/json"); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             // Send the request with the ResponseHeadersRead option. | 
					
						
							|  |  |  |             // This allows us to read the stream as soon as the headers are received. | 
					
						
							|  |  |  |             // This is important because we want to stream the responses. | 
					
						
							|  |  |  |             var response = await this.httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, token); | 
					
						
							| 
									
										
										
										
											2024-07-03 18:31:04 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-07 11:26:25 +00:00
										 |  |  |             // Open the response stream: | 
					
						
							|  |  |  |             var providerStream = await response.Content.ReadAsStreamAsync(token); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             // Add a stream reader to read the stream, line by line: | 
					
						
							|  |  |  |             streamReader = new StreamReader(providerStream); | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         catch(Exception e) | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |             this.logger.LogError($"Failed to stream chat completion from self-hosted provider '{this.InstanceName}': {e.Message}"); | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2024-07-03 18:31:04 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-10-07 11:26:25 +00:00
										 |  |  |         if (streamReader is not null) | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |             // Read the stream, line by line: | 
					
						
							|  |  |  |             while (!streamReader.EndOfStream) | 
					
						
							| 
									
										
										
										
											2024-07-03 18:31:04 +00:00
										 |  |  |             { | 
					
						
							| 
									
										
										
										
											2024-10-07 11:26:25 +00:00
										 |  |  |                 // Check if the token is canceled: | 
					
						
							|  |  |  |                 if (token.IsCancellationRequested) | 
					
						
							|  |  |  |                     yield break; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 // Read the next line: | 
					
						
							|  |  |  |                 var line = await streamReader.ReadLineAsync(token); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 // Skip empty lines: | 
					
						
							|  |  |  |                 if (string.IsNullOrWhiteSpace(line)) | 
					
						
							|  |  |  |                     continue; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 // Skip lines that do not start with "data: ". Regard | 
					
						
							|  |  |  |                 // to the specification, we only want to read the data lines: | 
					
						
							|  |  |  |                 if (!line.StartsWith("data: ", StringComparison.InvariantCulture)) | 
					
						
							|  |  |  |                     continue; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 // Check if the line is the end of the stream: | 
					
						
							|  |  |  |                 if (line.StartsWith("data: [DONE]", StringComparison.InvariantCulture)) | 
					
						
							|  |  |  |                     yield break; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 ResponseStreamLine providerResponse; | 
					
						
							|  |  |  |                 try | 
					
						
							|  |  |  |                 { | 
					
						
							|  |  |  |                     // We know that the line starts with "data: ". Hence, we can | 
					
						
							|  |  |  |                     // skip the first 6 characters to get the JSON data after that. | 
					
						
							|  |  |  |                     var jsonData = line[6..]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                     // Deserialize the JSON data: | 
					
						
							|  |  |  |                     providerResponse = JsonSerializer.Deserialize<ResponseStreamLine>(jsonData, JSON_SERIALIZER_OPTIONS); | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  |                 catch | 
					
						
							|  |  |  |                 { | 
					
						
							|  |  |  |                     // Skip invalid JSON data: | 
					
						
							|  |  |  |                     continue; | 
					
						
							|  |  |  |                 } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 // Skip empty responses: | 
					
						
							|  |  |  |                 if (providerResponse == default || providerResponse.Choices.Count == 0) | 
					
						
							|  |  |  |                     continue; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |                 // Yield the response: | 
					
						
							|  |  |  |                 yield return providerResponse.Choices[0].Delta.Content; | 
					
						
							| 
									
										
										
										
											2024-07-03 18:31:04 +00:00
										 |  |  |             } | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously | 
					
						
							|  |  |  |     /// <inheritdoc /> | 
					
						
							| 
									
										
										
										
											2024-12-03 14:24:40 +00:00
										 |  |  |     public override async IAsyncEnumerable<ImageURL> StreamImageCompletion(Provider.Model imageModel, string promptPositive, string promptNegative = FilterOperator.String.Empty, ImageURL referenceImageURL = default, [EnumeratorCancellation] CancellationToken token = default) | 
					
						
							| 
									
										
										
										
											2024-07-03 18:31:04 +00:00
										 |  |  |     { | 
					
						
							|  |  |  |         yield break; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  |     #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-03 14:24:40 +00:00
										 |  |  |     public override async Task<IEnumerable<Provider.Model>> GetTextModels(string? apiKeyProvisional = null, CancellationToken token = default) | 
					
						
							| 
									
										
										
										
											2024-07-03 18:31:04 +00:00
										 |  |  |     { | 
					
						
							| 
									
										
										
										
											2024-07-16 08:28:13 +00:00
										 |  |  |         try | 
					
						
							|  |  |  |         { | 
					
						
							| 
									
										
										
										
											2024-12-03 14:24:40 +00:00
										 |  |  |             switch (host) | 
					
						
							| 
									
										
										
										
											2024-07-16 08:28:13 +00:00
										 |  |  |             { | 
					
						
							|  |  |  |                 case Host.LLAMACPP: | 
					
						
							|  |  |  |                     // Right now, llama.cpp only supports one model. | 
					
						
							|  |  |  |                     // There is no API to list the model(s). | 
					
						
							| 
									
										
										
										
											2024-11-09 21:04:00 +00:00
										 |  |  |                     return [ new Provider.Model("as configured by llama.cpp", null) ]; | 
					
						
							| 
									
										
										
										
											2024-07-16 08:28:13 +00:00
										 |  |  |              | 
					
						
							|  |  |  |                 case Host.LM_STUDIO: | 
					
						
							|  |  |  |                 case Host.OLLAMA: | 
					
						
							| 
									
										
										
										
											2024-12-03 14:24:40 +00:00
										 |  |  |                     return await this.LoadModels(["embed"], [], token, apiKeyProvisional); | 
					
						
							| 
									
										
										
										
											2024-07-16 08:28:13 +00:00
										 |  |  |             } | 
					
						
							| 
									
										
										
										
											2024-07-03 18:31:04 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-07-16 08:28:13 +00:00
										 |  |  |             return []; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         catch(Exception e) | 
					
						
							|  |  |  |         { | 
					
						
							| 
									
										
										
										
											2024-09-01 18:10:03 +00:00
										 |  |  |             this.logger.LogError($"Failed to load text models from self-hosted provider: {e.Message}"); | 
					
						
							| 
									
										
										
										
											2024-07-16 08:28:13 +00:00
										 |  |  |             return []; | 
					
						
							|  |  |  |         } | 
					
						
							| 
									
										
										
										
											2024-07-03 18:31:04 +00:00
										 |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     /// <inheritdoc /> | 
					
						
							| 
									
										
										
										
											2024-12-03 14:24:40 +00:00
										 |  |  |     public override Task<IEnumerable<Provider.Model>> GetImageModels(string? apiKeyProvisional = null, CancellationToken token = default) | 
					
						
							| 
									
										
										
										
											2024-07-03 18:31:04 +00:00
										 |  |  |     { | 
					
						
							|  |  |  |         return Task.FromResult(Enumerable.Empty<Provider.Model>()); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2024-12-03 14:24:40 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |     public override async Task<IEnumerable<Provider.Model>> GetEmbeddingModels(string? apiKeyProvisional = null, CancellationToken token = default) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         try | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |             switch (host) | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 case Host.LM_STUDIO: | 
					
						
							|  |  |  |                 case Host.OLLAMA: | 
					
						
							|  |  |  |                     return await this.LoadModels([], ["embed"], token, apiKeyProvisional); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             return []; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |         catch(Exception e) | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |             this.logger.LogError($"Failed to load text models from self-hosted provider: {e.Message}"); | 
					
						
							|  |  |  |             return []; | 
					
						
							|  |  |  |         } | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2024-07-03 18:31:04 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |     #endregion | 
					
						
							| 
									
										
										
										
											2024-12-03 14:24:40 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |     private async Task<IEnumerable<Provider.Model>> LoadModels(string[] ignorePhrases, string[] filterPhrases, CancellationToken token, string? apiKeyProvisional = null) | 
					
						
							|  |  |  |     { | 
					
						
							|  |  |  |         var secretKey = apiKeyProvisional switch | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |             not null => apiKeyProvisional, | 
					
						
							|  |  |  |             _ => await RUST_SERVICE.GetAPIKey(this, isTrying: true) switch | 
					
						
							|  |  |  |             { | 
					
						
							|  |  |  |                 { Success: true } result => await result.Secret.Decrypt(ENCRYPTION), | 
					
						
							|  |  |  |                 _ => null, | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |         }; | 
					
						
							|  |  |  |                      | 
					
						
							|  |  |  |         var lmStudioRequest = new HttpRequestMessage(HttpMethod.Get, "models"); | 
					
						
							|  |  |  |         if(secretKey is not null) | 
					
						
							|  |  |  |             lmStudioRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKeyProvisional); | 
					
						
							|  |  |  |                      | 
					
						
							|  |  |  |         var lmStudioResponse = await this.httpClient.SendAsync(lmStudioRequest, token); | 
					
						
							|  |  |  |         if(!lmStudioResponse.IsSuccessStatusCode) | 
					
						
							|  |  |  |             return []; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         var lmStudioModelResponse = await lmStudioResponse.Content.ReadFromJsonAsync<ModelsResponse>(token); | 
					
						
							|  |  |  |         return lmStudioModelResponse.Data. | 
					
						
							|  |  |  |             Where(model => !ignorePhrases.Any(ignorePhrase => model.Id.Contains(ignorePhrase, StringComparison.InvariantCulture)) && | 
					
						
							|  |  |  |                            filterPhrases.All( filter => model.Id.Contains(filter, StringComparison.InvariantCulture))) | 
					
						
							|  |  |  |             .Select(n => new Provider.Model(n.Id, null)); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2024-07-03 18:31:04 +00:00
										 |  |  | } |