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
@@ -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 };
|
||||
|
||||
|
||||
Reference in new issue
Block a user