Add z.ai/GLM provider via per-process Claude CLI env injection
Route glm:-prefixed models through the CLI/Anthropic path: ResolveModelId strips the glm: prefix, and CreateCliStartInfo activates the z.ai Anthropic-compatible endpoint (ANTHROPIC_BASE_URL/AUTH_TOKEN) only for that process when IsZaiModel matches, leaving every other request's OAuth login untouched. GLM model list is loaded through the existing model cache and exposed via /api/models/glm with a "GLM (z.ai)" optgroup in the agent form. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
e6770bb5f8
commit
0f3d658fd3
8 files changed
+83
-9
No files matched your search
@@ -16,6 +16,7 @@ public static class ModelsEndpoint
|
||||
app.MapGet("/api/models/gemini", HandleGeminiModels);
|
||||
app.MapGet("/api/models/openai", HandleOpenAiModels);
|
||||
app.MapGet("/api/models/minimax", HandleMinimaxModels);
|
||||
app.MapGet("/api/models/glm", HandleGlmModels);
|
||||
app.MapPost("/api/models/refresh", HandleRefresh);
|
||||
}
|
||||
|
||||
@@ -59,6 +60,14 @@ public static class ModelsEndpoint
|
||||
return Results.Json(new { models });
|
||||
}
|
||||
|
||||
private static IResult HandleGlmModels(OpenAiService openAiService)
|
||||
{
|
||||
var models = openAiService.GetGlmModels()
|
||||
.Select(m => new { id = m.Id, displayName = m.DisplayName })
|
||||
.ToList();
|
||||
return Results.Json(new { models });
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleRefresh(OpenAiService openAiService)
|
||||
{
|
||||
await openAiService.RefreshModelsFromApis();
|
||||
|
||||
+3
-2
@@ -125,8 +125,9 @@ builder.Services.AddHostedService<KeyScannerService>(sp => sp.GetRequiredService
|
||||
builder.Services.AddSingleton<GitHubEventScannerService>();
|
||||
builder.Services.AddHostedService<GitHubEventScannerService>(sp => sp.GetRequiredService<GitHubEventScannerService>());
|
||||
|
||||
// Claude CLI Service (für OAuth-basierte Chats via CLI)
|
||||
builder.Services.AddSingleton<ClaudeCliService>();
|
||||
// Claude CLI Service (für OAuth-basierte Chats via CLI; serverSettings für z.ai/GLM-Env)
|
||||
builder.Services.AddSingleton<ClaudeCliService>(sp =>
|
||||
new ClaudeCliService(sp.GetRequiredService<Microsoft.Extensions.Logging.ILogger<ClaudeCliService>>(), serverSettings));
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
|
||||
@@ -52,6 +52,12 @@ Nur deine finale Text-Antwort wird weiterverwendet. Formuliere sie vollständig
|
||||
// OpenAI-kompatible Provider werden unverändert durchgereicht (Format: "provider:model")
|
||||
if (OpenAiService.IsOpenAiProvider(model)) return model;
|
||||
|
||||
// z.ai / GLM laufen über den Anthropic-kompatiblen Claude-CLI-Pfad (NICHT über den
|
||||
// OpenAI-Provider). Prefix strippen → die rohe Modell-ID (z.B. "glm-4.6") geht an --model,
|
||||
// und das Routing erkennt Provider="" → CLI. Die z.ai-Env-Vars setzt ClaudeCliService.
|
||||
if (!string.IsNullOrEmpty(model) && model.StartsWith("glm:", System.StringComparison.OrdinalIgnoreCase))
|
||||
return model.Substring(4);
|
||||
|
||||
// Defensiv-Strip: alte "[1m]"-Suffixe aus DB transparent normalisieren.
|
||||
// 1M-Context ist seit März 2026 Default bei Opus 4.6/4.7/4.8 + Sonnet 4.6 – kein Suffix mehr nötig.
|
||||
if (!string.IsNullOrEmpty(model) && model.Contains("[1m]"))
|
||||
|
||||
@@ -20,10 +20,12 @@ public class ClaudeCliService
|
||||
private readonly ILogger<ClaudeCliService> _logger;
|
||||
private readonly string _cliPath;
|
||||
private readonly bool _isAvailable;
|
||||
private readonly ServerSettingsService _settings;
|
||||
|
||||
public ClaudeCliService(ILogger<ClaudeCliService> logger)
|
||||
public ClaudeCliService(ILogger<ClaudeCliService> logger, ServerSettingsService settings)
|
||||
{
|
||||
_logger = logger;
|
||||
_settings = settings;
|
||||
_cliPath = FindCliPath();
|
||||
_isAvailable = !string.IsNullOrEmpty(_cliPath) && File.Exists(_cliPath);
|
||||
|
||||
@@ -56,7 +58,14 @@ public class ClaudeCliService
|
||||
return dir;
|
||||
}
|
||||
|
||||
private ProcessStartInfo CreateCliStartInfo(List<string> args, bool redirectInput = false, string effort = null, string workingDirectory = null)
|
||||
/// <summary>
|
||||
/// Erkennt z.ai/GLM-Modelle. Diese laufen über den Anthropic-kompatiblen z.ai-Endpoint,
|
||||
/// nicht über das normale Claude-OAuth-Login.
|
||||
/// </summary>
|
||||
public static bool IsZaiModel(string modelId)
|
||||
=> !string.IsNullOrEmpty(modelId) && modelId.StartsWith("glm-", System.StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private ProcessStartInfo CreateCliStartInfo(List<string> args, bool redirectInput = false, string effort = null, string workingDirectory = null, string modelId = null)
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
@@ -88,6 +97,32 @@ public class ClaudeCliService
|
||||
if (!string.IsNullOrEmpty(effort) && effort != "high")
|
||||
psi.Environment["CLAUDE_CODE_EFFORT_LEVEL"] = effort;
|
||||
|
||||
// z.ai / GLM: das Anthropic-kompatible z.ai-Backend NUR für diesen Prozess aktivieren.
|
||||
// Da gott pro Request einen eigenen CLI-Prozess startet, gilt das ausschließlich für
|
||||
// diesen Lauf – das Claude-OAuth-Login aller anderen Requests bleibt unberührt.
|
||||
if (IsZaiModel(modelId))
|
||||
{
|
||||
var zaiKey = _settings?.Get("ZaiApiKey");
|
||||
if (!string.IsNullOrEmpty(zaiKey))
|
||||
{
|
||||
psi.Environment["ANTHROPIC_BASE_URL"] = "https://api.z.ai/api/anthropic";
|
||||
psi.Environment["ANTHROPIC_AUTH_TOKEN"] = zaiKey;
|
||||
// Interne CLI-Hilfsaufrufe (z.B. Haiku-Klasse für Titel/Utility) auf GLM mappen,
|
||||
// damit sie nicht gegen unbekannte Claude-Modellnamen bei z.ai laufen.
|
||||
psi.Environment["ANTHROPIC_DEFAULT_SONNET_MODEL"] = modelId;
|
||||
psi.Environment["ANTHROPIC_DEFAULT_OPUS_MODEL"] = modelId;
|
||||
psi.Environment["ANTHROPIC_DEFAULT_HAIKU_MODEL"] = "glm-4.5-air";
|
||||
// Effort/Reasoning-Budget ist Claude-spezifisch und für z.ai nicht definiert → nicht senden.
|
||||
psi.Environment.Remove("CLAUDE_CODE_EFFORT_LEVEL");
|
||||
// Ein evtl. global gesetzter Anthropic-API-Key darf den z.ai-Token nicht überschreiben.
|
||||
psi.Environment.Remove("ANTHROPIC_API_KEY");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("GLM-Modell '{Model}' gewählt, aber ZaiApiKey nicht konfiguriert – Request läuft NICHT über z.ai.", modelId);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var arg in args)
|
||||
psi.ArgumentList.Add(arg);
|
||||
|
||||
@@ -225,6 +260,11 @@ public class ClaudeCliService
|
||||
args.Add(tempImageDir);
|
||||
}
|
||||
|
||||
// Defensiv: falls eine unaufgelöste z.ai-ID ("glm:glm-4.6") durchrutscht, Prefix strippen,
|
||||
// damit --model die rohe ID (z.B. "glm-4.6") erhält, die z.ai erwartet.
|
||||
if (!string.IsNullOrEmpty(modelId) && modelId.StartsWith("glm:", System.StringComparison.OrdinalIgnoreCase))
|
||||
modelId = modelId.Substring(4);
|
||||
|
||||
if (!string.IsNullOrEmpty(modelId))
|
||||
{
|
||||
args.Add("--model");
|
||||
@@ -243,7 +283,7 @@ public class ClaudeCliService
|
||||
args.Add(mcpConfigPath);
|
||||
}
|
||||
|
||||
return await RunCliStreamingAsync(args, prompt, onEvent, cancellationToken, effort, workspace);
|
||||
return await RunCliStreamingAsync(args, prompt, onEvent, cancellationToken, effort, workspace, modelId);
|
||||
}
|
||||
|
||||
private async Task<CliChatResult> RunCliStreamingAsync(
|
||||
@@ -252,7 +292,8 @@ public class ClaudeCliService
|
||||
Func<CliStreamEvent, Task> onEvent,
|
||||
CancellationToken cancellationToken,
|
||||
string effort = null,
|
||||
string workspace = null)
|
||||
string workspace = null,
|
||||
string modelId = null)
|
||||
{
|
||||
var result = new CliChatResult();
|
||||
var fullOutput = new StringBuilder();
|
||||
@@ -272,7 +313,7 @@ public class ClaudeCliService
|
||||
|
||||
try
|
||||
{
|
||||
var startInfo = CreateCliStartInfo(args, redirectInput: true, effort: effort, workingDirectory: workspace);
|
||||
var startInfo = CreateCliStartInfo(args, redirectInput: true, effort: effort, workingDirectory: workspace, modelId: modelId);
|
||||
|
||||
using var process = new Process { StartInfo = startInfo };
|
||||
|
||||
|
||||
@@ -71,6 +71,7 @@ public class OpenAiService
|
||||
private List<ProviderModel> _geminiModels = new List<ProviderModel>();
|
||||
private List<ProviderModel> _openaiModels = new List<ProviderModel>();
|
||||
private List<ProviderModel> _minimaxModels = new List<ProviderModel>();
|
||||
private List<ProviderModel> _glmModels = new List<ProviderModel>();
|
||||
|
||||
// Fallback-Modell-Liste für MiniMax (falls /v1/models nichts liefert).
|
||||
// Quelle: https://platform.minimax.io/docs/api-reference/api-overview
|
||||
@@ -126,6 +127,7 @@ public class OpenAiService
|
||||
_geminiModels = cache.ContainsKey("gemini") ? cache["gemini"] : new List<ProviderModel>();
|
||||
_openaiModels = cache.ContainsKey("openai") ? cache["openai"] : new List<ProviderModel>();
|
||||
_minimaxModels = cache.ContainsKey("minimax") ? cache["minimax"] : new List<ProviderModel>();
|
||||
_glmModels = cache.ContainsKey("glm") ? cache["glm"] : new List<ProviderModel>();
|
||||
// Falls der Cache älter als die MiniMax-Integration ist oder leer ist:
|
||||
// statische Fallback-Liste verwenden, damit das Dropdown nicht leer bleibt.
|
||||
if (_minimaxModels.Count == 0)
|
||||
@@ -195,6 +197,13 @@ public class OpenAiService
|
||||
Console.WriteLine($"[OpenAiService] MinimaxApiKey nicht konfiguriert – nutze Fallback-Liste ({_minimaxModels.Count} Modelle).");
|
||||
}
|
||||
|
||||
// z.ai / GLM: Key aus Settings (ZaiApiKey). OpenAI-kompatibler /models-Endpoint von z.ai.
|
||||
var zaiKey = _settings.Get("ZaiApiKey");
|
||||
if (!string.IsNullOrEmpty(zaiKey))
|
||||
_glmModels = await FetchModels("https://api.z.ai/api/paas/v4/models", "GLM (z.ai)", zaiKey);
|
||||
else
|
||||
Console.WriteLine("[OpenAiService] ZaiApiKey nicht konfiguriert – GLM-Modelle nicht geladen.");
|
||||
|
||||
// Cache speichern
|
||||
try
|
||||
{
|
||||
@@ -204,7 +213,8 @@ public class OpenAiService
|
||||
["pollinations"] = _pollinationsModels,
|
||||
["gemini"] = _geminiModels,
|
||||
["openai"] = _openaiModels,
|
||||
["minimax"] = _minimaxModels
|
||||
["minimax"] = _minimaxModels,
|
||||
["glm"] = _glmModels
|
||||
};
|
||||
var json = JsonSerializer.Serialize(cache, new JsonSerializerOptions { WriteIndented = true });
|
||||
await File.WriteAllTextAsync(_cacheFile, json);
|
||||
@@ -264,6 +274,7 @@ public class OpenAiService
|
||||
public List<ProviderModel> GetGeminiModels() => _geminiModels;
|
||||
public List<ProviderModel> GetOpenAiModels() => _openaiModels;
|
||||
public List<ProviderModel> GetMinimaxModels() => _minimaxModels;
|
||||
public List<ProviderModel> GetGlmModels() => _glmModels;
|
||||
|
||||
/// <summary>
|
||||
/// Gibt den Provider und das eigentliche Modell-ID aus einem kombinierten Model-String zurück.
|
||||
|
||||
@@ -38,6 +38,7 @@ public class ServerSettingsService
|
||||
new SettingDefinition( "HuggingFaceToken", "string", "", "HuggingFace Token (optional, erhöht Rate-Limits)", "password" ),
|
||||
new SettingDefinition( "NvidiaApiKey", "string", "", "Nvidia API-Key (direkt statt g4f.space Proxy)", "password" ),
|
||||
new SettingDefinition( "MinimaxApiKey", "string", "", "MiniMax API-Key (für minimax-Provider im Chat)", "password" ),
|
||||
new SettingDefinition( "ZaiApiKey", "string", "", "z.ai API-Key (für GLM-Modelle über die Claude CLI)", "password" ),
|
||||
new SettingDefinition( "ScannerKeyTypes", "string_array", "anthropic_oauth,krea_ai", "Key-Scanner: Aktive Key-Typen", "checkboxes" ),
|
||||
};
|
||||
|
||||
|
||||
@@ -249,6 +249,9 @@
|
||||
<optgroup label="MiniMax" id="minimaxOptgroup">
|
||||
<option value="" disabled>Wird geladen...</option>
|
||||
</optgroup>
|
||||
<optgroup label="GLM (z.ai)" id="glmOptgroup">
|
||||
<option value="" disabled>Wird geladen...</option>
|
||||
</optgroup>
|
||||
</select>
|
||||
<label>Reasoning-Effort <span class="checkbox-hint">(nur Claude: Opus 4.5+, Sonnet 4.6+)</span></label>
|
||||
<select id="agentEffort">
|
||||
|
||||
@@ -28,6 +28,7 @@ registerPage('agent', {
|
||||
const geminiOptgroup = document.getElementById('geminiOptgroup');
|
||||
const openaiOptgroup = document.getElementById('openaiOptgroup');
|
||||
const minimaxOptgroup = document.getElementById('minimaxOptgroup');
|
||||
const glmOptgroup = document.getElementById('glmOptgroup');
|
||||
|
||||
// Modelle dynamisch laden
|
||||
const loadProviderModels = async (url, prefix, optgroup) => {
|
||||
@@ -55,7 +56,8 @@ registerPage('agent', {
|
||||
loadProviderModels('/api/models/nvidia', 'nvidia', nvidiaOptgroup),
|
||||
loadProviderModels('/api/models/pollinations', 'pollinations', pollinationsOptgroup),
|
||||
loadProviderModels('/api/models/gemini', 'gemini', geminiOptgroup),
|
||||
loadProviderModels('/api/models/minimax', 'minimax', minimaxOptgroup)
|
||||
loadProviderModels('/api/models/minimax', 'minimax', minimaxOptgroup),
|
||||
loadProviderModels('/api/models/glm', 'glm', glmOptgroup)
|
||||
]);
|
||||
const allowUserChoiceEl = document.getElementById('agentAllowUserChoice');
|
||||
const autoCommitEl = document.getElementById('agentAutoCommit');
|
||||
|
||||
Reference in new issue
Block a user