Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
SERVICE_TYPE.GEMINI,
SERVICE_TYPE.LOCALAI,
SERVICE_TYPE.MISTRAL,
SERVICE_TYPE.OPENAI
SERVICE_TYPE.OPENAI,
SERVICE_TYPE.XAI
].includes(
serviceType as
| 'openai'
Expand All @@ -23,6 +24,7 @@
| 'gemini'
| 'deepseek'
| 'mistral'
| 'xai'
)
">
<div class="flex flex-col space-x-2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ const templateMap: Record<string, keyof ISettings> = {
[SERVICE_TYPE.LOCALAI]: SETTINGS.LOCAL_AI_CHAT_REQUEST_TEMPLATE,
[SERVICE_TYPE.GEMINI]: SETTINGS.GEMINI_REQUEST_TEMPLATE,
[SERVICE_TYPE.DEEPSEEK]: SETTINGS.DEEPSEEK_REQUEST_TEMPLATE,
[SERVICE_TYPE.MISTRAL]: SETTINGS.MISTRAL_REQUEST_TEMPLATE
[SERVICE_TYPE.MISTRAL]: SETTINGS.MISTRAL_REQUEST_TEMPLATE,
[SERVICE_TYPE.XAI]: SETTINGS.XAI_REQUEST_TEMPLATE
}

const localAiEndpoint = computed(
Expand Down Expand Up @@ -207,8 +208,7 @@ const presetLabelMap: Record<string, string> = {
gemini_request_template: 'Gemini Content',
local_ai_generate_request_template: 'Ollama Generate',
local_ai_chat_request_template: 'LocalAI Chat',
deepseek_request_template: 'DeepSeek Chat',
mistral_request_template: 'Mistral Chat'
deepseek_request_template: 'DeepSeek Chat'
}

onMounted(async () => {
Expand Down
7 changes: 1 addition & 6 deletions Lingarr.Client/src/ts/setting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const SETTINGS = {
GEMINI_MODEL: 'gemini_model',
DEEPSEEK_MODEL: 'deepseek_model',
MISTRAL_MODEL: 'mistral_model',
XAI_MODEL: 'xai_model',
AI_PROMPT: 'ai_prompt',
AI_USER_PROMPT: 'ai_user_prompt',
PROOFREAD_PROMPT: 'proofread_prompt',
Expand Down Expand Up @@ -63,7 +64,6 @@ export const SETTINGS = {
LOCAL_AI_GENERATE_REQUEST_TEMPLATE: 'local_ai_generate_request_template',
DEEPSEEK_REQUEST_TEMPLATE: 'deepseek_request_template',
GEMINI_REQUEST_TEMPLATE: 'gemini_request_template',
MISTRAL_REQUEST_TEMPLATE: 'mistral_request_template',
LANGUAGE_CODE_FORMAT: 'language_code_format',
RADARR_DEFAULT_INCLUDE: 'radarr_default_include',
SONARR_DEFAULT_INCLUDE: 'sonarr_default_include'
Expand All @@ -90,7 +90,6 @@ export interface ISettings {
local_ai_model: string
gemini_model: string
deepseek_model: string
mistral_model: string
ai_prompt: string
ai_user_prompt: string
proofread_prompt: string
Expand Down Expand Up @@ -131,7 +130,6 @@ export interface ISettings {
local_ai_generate_request_template: string
deepseek_request_template: string
gemini_request_template: string
mistral_request_template: string
language_code_format: string
radarr_default_include: string
sonarr_default_include: string
Expand All @@ -146,7 +144,6 @@ export const ENCRYPTED_SETTINGS = {
ANTHROPIC_API_KEY: 'anthropic_api_key',
GEMINI_API_KEY: 'gemini_api_key',
DEEPSEEK_API_KEY: 'deepseek_api_key',
MISTRAL_API_KEY: 'mistral_api_key',
DEEPL_API_KEY: 'deepl_api_key',
LIBRETRANSLATE_API_KEY: 'libretranslate_api_key',
LOCAL_AI_API_KEY: 'local_ai_api_key',
Expand All @@ -160,7 +157,6 @@ export interface IEncryptedSettings {
anthropic_api_key: string
gemini_api_key: string
deepseek_api_key: string
mistral_api_key: string
deepl_api_key: string
libretranslate_api_key: string
local_ai_api_key: string
Expand All @@ -174,7 +170,6 @@ export const SERVICE_TYPE = {
DEEPL: 'deepl',
GEMINI: 'gemini',
DEEPSEEK: 'deepseek',
MISTRAL: 'mistral',
GOOGLE: 'google',
BING: 'bing',
MICROSOFT: 'microsoft',
Expand Down
7 changes: 7 additions & 0 deletions Lingarr.Core/Configuration/SettingKeys.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ public static class Mistral
public const string RequestTemplate = "mistral_request_template";
}

public static class XAi
{
public const string Model = "xai_model";
public const string ApiKey = "xai_api_key";
public const string RequestTemplate = "xai_request_template";
}

public static class LibreTranslate
{
public const string Url = "libretranslate_url";
Expand Down
2 changes: 1 addition & 1 deletion Lingarr.Docs/developers/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Lingarr scans a folder specified by the `PLUGINS_PATH` environment variable at s

Do not use these plugin identifiers (they are used by built-in providers):

`anthropic`, `openai`, `gemini`, `deepseek`, `mistral`, `localai`, `deepl`, `libretranslate`, `google`, `bing`, `microsoft`, `yandex`
`anthropic`, `openai`, `gemini`, `deepseek`, `mistral`, `xai`, `localai`, `deepl`, `libretranslate`, `google`, `bing`, `microsoft`, `yandex`

## Settings

Expand Down
2 changes: 1 addition & 1 deletion Lingarr.Docs/getting-started/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ The `SOURCE_LANGUAGES` and `TARGET_LANGUAGES` variables should be provided as a

The supported values are:

- **[AI services](/translation-services/ai-services)**: [`openai`](/translation-services/ai-services#openai), [`anthropic`](/translation-services/ai-services#anthropic), [`gemini`](/translation-services/ai-services#gemini), [`deepseek`](/translation-services/ai-services#deepseek), [`mistral`](/translation-services/ai-services#mistral) and [`localai`](/translation-services/ai-services#localai)
- **[AI services](/translation-services/ai-services)**: [`openai`](/translation-services/ai-services#openai), [`anthropic`](/translation-services/ai-services#anthropic), [`gemini`](/translation-services/ai-services#gemini), [`deepseek`](/translation-services/ai-services#deepseek), [`mistral`](/translation-services/ai-services#mistral), [`xai`](/translation-services/ai-services#xai) and [`localai`](/translation-services/ai-services#localai)
- **[Machine translation](/translation-services/machine-translation)**: [`libretranslate`](/translation-services/machine-translation#libretranslate), [`deepl`](/translation-services/machine-translation#deepl), [`google`](/translation-services/machine-translation#google-bing-microsoft-and-yandex), [`bing`](/translation-services/machine-translation#google-bing-microsoft-and-yandex), [`microsoft`](/translation-services/machine-translation#google-bing-microsoft-and-yandex) and [`yandex`](/translation-services/machine-translation#google-bing-microsoft-and-yandex)

Each service has its own configuration variables, such as API keys and model selection, documented on its settings page.
11 changes: 1 addition & 10 deletions Lingarr.Docs/translation-services/ai-services.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Batch translation does not use the user prompt. The batch is sent as the user me

Proofreading re-examines a completed translation. For each subtitle line it sends the source line and the existing translation together to the AI service and asks for a corrected translation, without translating from scratch.

Only services that implement proofreading offer it: OpenAI, Anthropic, Gemini, DeepSeek, Mistral and LocalAI. LibreTranslate, DeepL, Google, Bing, Microsoft and Yandex do not support proofreading.
Only services that implement proofreading offer it: OpenAI, Anthropic, Gemini, DeepSeek, Mistral, xAI and LocalAI. LibreTranslate, DeepL, Google, Bing, Microsoft and Yandex do not support proofreading.

You can run it two ways:

Expand Down Expand Up @@ -112,15 +112,6 @@ Both accept the placeholders already listed above, plus two more:
| `AI_PROMPT` | The system prompt template. |
| `AI_USER_PROMPT` | The user message template. |

### Mistral

| **Environment Variable** | **Description** |
|--------------------------|-----------------------------------------------------------------------------|
| `MISTRAL_MODEL` | The model to use for Mistral translations. Example: `mistral-large-latest`. |
| `MISTRAL_API_KEY` | The API key for authenticating with Mistral. |
| `AI_PROMPT` | The system prompt template. |
| `AI_USER_PROMPT` | The user message template. |

### LocalAI

LocalAI works with Ollama or any other OpenAI-compatible model or router.
Expand Down
42 changes: 42 additions & 0 deletions Lingarr.Migrations/Migrations/M0018_SeedXAiSettings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using FluentMigrator;

namespace Lingarr.Migrations.Migrations;

[Migration(18)]
public class M0018_SeedXAiSettings : Migration
{
public override void Up()
{
Insert.IntoTable("settings").Row(new
{
key = "xai_model",
value = ""
});
Insert.IntoTable("settings").Row(new
{
key = "xai_api_key",
value = ""
});
Insert.IntoTable("settings").Row(new
{
key = "xai_request_template",
value = ""
});
}

public override void Down()
{
Delete.FromTable("settings").Row(new
{
key = "xai_model"
});
Delete.FromTable("settings").Row(new
{
key = "xai_api_key"
});
Delete.FromTable("settings").Row(new
{
key = "xai_request_template"
});
}
}
2 changes: 2 additions & 0 deletions Lingarr.Server.Tests/Services/StartupServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ private static readonly (string EnvVar, string SettingKey)[] EnvMap =
("DEEPSEEK_API_KEY", SettingKeys.Translation.DeepSeek.ApiKey),
("MISTRAL_MODEL", SettingKeys.Translation.Mistral.Model),
("MISTRAL_API_KEY", SettingKeys.Translation.Mistral.ApiKey),
("XAI_MODEL", SettingKeys.Translation.XAi.Model),
("XAI_API_KEY", SettingKeys.Translation.XAi.ApiKey),
("DEEPL_API_KEY", SettingKeys.Translation.DeepL.DeeplApiKey),
("AUTH_ENABLED", SettingKeys.Authentication.AuthEnabled),
("TELEMETRY_ENABLED", SettingKeys.Telemetry.TelemetryEnabled)
Expand Down
1 change: 1 addition & 0 deletions Lingarr.Server/Extensions/ApplicationBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ private static async Task MigrateApiKeyEncryption(this WebApplication app)
SettingKeys.Translation.Gemini.ApiKey,
SettingKeys.Translation.DeepSeek.ApiKey,
SettingKeys.Translation.Mistral.ApiKey,
SettingKeys.Translation.XAi.ApiKey,
SettingKeys.Translation.DeepL.DeeplApiKey,
SettingKeys.Translation.LibreTranslate.ApiKey,
SettingKeys.Translation.LocalAi.ApiKey,
Expand Down
1 change: 1 addition & 0 deletions Lingarr.Server/Extensions/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ private static void ConfigureServices(this WebApplicationBuilder builder)
builder.Services.AddSingleton<IPluginManifest, GeminiPluginManifest>();
builder.Services.AddSingleton<IPluginManifest, DeepSeekPluginManifest>();
builder.Services.AddSingleton<IPluginManifest, MistralPluginManifest>();
builder.Services.AddSingleton<IPluginManifest, XAiPluginManifest>();
builder.Services.AddSingleton<IPluginManifest, LocalAiPluginManifest>();
builder.Services.AddSingleton<IPluginManifest, DeepLPluginManifest>();
builder.Services.AddSingleton<IPluginManifest, LibreTranslatePluginManifest>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public interface ITranslationServiceFactory
/// <summary>
/// Creates and returns an instance of a translation service based on the specified service type.
/// </summary>
/// <param name="serviceType">The provider name (case-insensitive), e.g. "libretranslate", "openai", "anthropic", "deepl", "gemini", "deepseek", "mistral", "localai", "google", "bing", "microsoft", "yandex".</param>
/// <param name="serviceType">The provider name (case-insensitive), e.g. "libretranslate", "openai", "anthropic", "deepl", "gemini", "deepseek", "mistral", "xai", "localai", "google", "bing", "microsoft", "yandex".</param>
/// <returns>An instance of <see cref="ITranslationService"/> corresponding to the specified service type.</returns>
/// <exception cref="ArgumentException">Thrown when an unsupported service type is specified.</exception>
ITranslationService CreateTranslationService(string serviceType);
Expand Down
2 changes: 1 addition & 1 deletion Lingarr.Server/Listener/SettingChangedListener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public class SettingChangedListener
private readonly ILogger<SettingChangedListener> _logger;
private static readonly HashSet<string> BatchServiceTypes = new(StringComparer.OrdinalIgnoreCase)
{
"openai", "anthropic", "localai", "gemini", "mistral"
"openai", "anthropic", "localai", "gemini", "mistral", "xai"
};

public SettingChangedListener(IServiceProvider serviceProvider,
Expand Down
27 changes: 27 additions & 0 deletions Lingarr.Server/Models/RequestTemplates/XAiTemplate.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Text.Json.Serialization;

namespace Lingarr.Server.Models.RequestTemplates;

public class XAiTemplate
{
[JsonPropertyName("model")]
public string Model { get; set; } = "{model}";

[JsonPropertyName("messages")]
public List<ChatMessage> Messages { get; set; } =
[
new()
{
Role = "system",
Content = "{systemPrompt}"
},
new()
{
Role = "user",
Content = "{userMessage}"
}
];

[JsonPropertyName("stream")]
public bool Stream { get; set; } = false;
}
40 changes: 40 additions & 0 deletions Lingarr.Server/Services/Plugins/Manifests/XAiPluginManifest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
using Lingarr.Contracts.Interfaces.Plugins;
using Lingarr.Contracts.Plugins;
using Lingarr.Core.Configuration;

namespace Lingarr.Server.Services.Plugins.Manifests;

public sealed class XAiPluginManifest : IPluginManifest
{
public string Provider => "xai";

public string DisplayName => "xAI";

public string? Description =>
"xAI's OpenAI-compatible chat completion models. AI translation can be costly, only use it when you know what you are doing and keep automation disabled.";

public bool HasRequestTemplate => true;

public IReadOnlyList<PluginSettingField> Settings { get; } =
[
new()
{
Key = SettingKeys.Translation.XAi.ApiKey,
Label = "API key",
Type = PluginSettingType.Secret,
Required = true,
Description = "xAI API key. Stored encrypted.",
MinLength = 1,
ValidationErrorMessage = "Value must not be empty"
},
new()
{
Key = SettingKeys.Translation.XAi.Model,
Label = "AI Model",
Type = PluginSettingType.RemoteDropdown,
Required = true,
OptionsEndpoint = "/api/plugin/xai/models",
Description = "Select a model from your xAI catalogue."
}
];
}
3 changes: 3 additions & 0 deletions Lingarr.Server/Services/StartupService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,9 @@ private async Task ApplySettingsFromEnvironment(LingarrDbContext dbContext)
{ "MISTRAL_MODEL", SettingKeys.Translation.Mistral.Model },
{ "MISTRAL_API_KEY", SettingKeys.Translation.Mistral.ApiKey },

{ "XAI_MODEL", SettingKeys.Translation.XAi.Model },
{ "XAI_API_KEY", SettingKeys.Translation.XAi.ApiKey },

{ "DEEPL_API_KEY", SettingKeys.Translation.DeepL.DeeplApiKey },

{ "AUTH_ENABLED", SettingKeys.Authentication.AuthEnabled },
Expand Down
8 changes: 5 additions & 3 deletions Lingarr.Server/Services/Translation/AnthropicService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -504,16 +504,18 @@ public override async Task<ModelsResponse> GetModels()

var response = await _httpClient.SendAsync(request);

var responseContent = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
_logger.LogError("Failed to fetch models. Status: {StatusCode}", response.StatusCode);
_logger.LogError(
"Failed to fetch models. Status: {StatusCode}, response: {ResponseContent}",
response.StatusCode, responseContent);
return new ModelsResponse
{
Message = $"Failed to fetch models. Status: {response.StatusCode}"
Message = $"Failed to fetch models. Status: {response.StatusCode}, response: {responseContent}"
};
}

var responseContent = await response.Content.ReadAsStringAsync();
var jsonResponse = JsonSerializer.Deserialize<JsonElement>(responseContent);

if (!jsonResponse.TryGetProperty("data", out var dataElement))
Expand Down
8 changes: 5 additions & 3 deletions Lingarr.Server/Services/Translation/DeepSeekService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,16 +182,18 @@ public override async Task<ModelsResponse> GetModels()

var response = await _httpClient.SendAsync(request);

var responseContent = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
_logger.LogError("Failed to fetch models. Status: {StatusCode}", response.StatusCode);
_logger.LogError(
"Failed to fetch models. Status: {StatusCode}, response: {ResponseContent}",
response.StatusCode, responseContent);
return new ModelsResponse
{
Message = $"Failed to fetch models. Status: {response.StatusCode}"
Message = $"Failed to fetch models. Status: {response.StatusCode}, response: {responseContent}"
};
}

var responseContent = await response.Content.ReadAsStringAsync();
var jsonResponse = JsonSerializer.Deserialize<JsonElement>(responseContent);

if (!jsonResponse.TryGetProperty("data", out var dataElement))
Expand Down
8 changes: 5 additions & 3 deletions Lingarr.Server/Services/Translation/GoogleGeminiService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -279,16 +279,18 @@ public override async Task<ModelsResponse> GetModels()
var request = new HttpRequestMessage(HttpMethod.Get, $"{_endpoint}/models?key={apiKey}");
var response = await _httpClient.SendAsync(request);

var responseContent = await response.Content.ReadAsStringAsync();
if (!response.IsSuccessStatusCode)
{
_logger.LogError("Failed to fetch models. Status: {StatusCode}", response.StatusCode);
_logger.LogError(
"Failed to fetch models. Status: {StatusCode}, response: {ResponseContent}",
response.StatusCode, responseContent);
return new ModelsResponse
{
Message = $"Failed to fetch models. Status: {response.StatusCode}"
Message = $"Failed to fetch models. Status: {response.StatusCode}, response: {responseContent}"
};
}

var responseContent = await response.Content.ReadAsStringAsync();
var jsonResponse = JsonSerializer.Deserialize<JsonElement>(responseContent);

if (!jsonResponse.TryGetProperty("models", out var modelsElement))
Expand Down
7 changes: 5 additions & 2 deletions Lingarr.Server/Services/Translation/MistralService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -448,10 +448,13 @@ public override async Task<ModelsResponse> GetModels()

if (!response.IsSuccessStatusCode)
{
_logger.LogError("Failed to fetch models. Status: {StatusCode}", response.StatusCode);
var responseContent = await response.Content.ReadAsStringAsync();
_logger.LogError(
"Failed to fetch models. Status: {StatusCode}, response: {ResponseContent}",
response.StatusCode, responseContent);
return new ModelsResponse
{
Message = $"Failed to fetch models. Status: {response.StatusCode}"
Message = $"Failed to fetch models. Status: {response.StatusCode}, response: {responseContent}"
};
}

Expand Down
Loading
Loading