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:
MelekiandClaude Opus 4.8 committed 2026-06-20 14:01:43 +02:00
1 parent e6770bb5f8
commit 0f3d658fd3
8 files changed
+83 -9

No files matched your search

+9
View File
@@ -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
View File
@@ -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();
+6
View File
@@ -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]"))
+46 -5
View File
@@ -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 };
+12 -1
View File
@@ -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.
+1
View File
@@ -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" ),
};
+3
View File
@@ -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">
+3 -1
View File
@@ -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');