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

+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 };