diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 18bd7d1..3f080bd 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -863,6 +863,8 @@ Ein **Agent** definiert die Persönlichkeit und Fähigkeiten der AI: | `AllowUserChoice` | `ask_user_choice`-Tool verfügbar (Default: true) | | `TaskInstruction` | Aufgabe/Anweisung – optionaler Text, wird als `## Anweisungen`-Block in System-Prompt eingefügt | | `Effort` | Reasoning-Effort-Level für Claude. `""` (=high, Default), `low`, `medium`, `high`, `xhigh` (nur Opus 4.7+), `max` (nur Opus 4.6+/Sonnet 4.6). API: als `output_config.effort` gesendet. CLI: via Env-Var `CLAUDE_CODE_EFFORT_LEVEL`. Bei OpenAI-Providern ignoriert; bei z.ai/GLM nur ab **GLM 5.2** durchgereicht (sonst entfernt). Ultracode hebt es auf `xhigh` an. | +| `MaxOutputTokens` | Hartes Limit der Ausgabelänge in Tokens. `0` = aus (Modell-Default bzw. API-Default 16384). CLI: via Env-Var `CLAUDE_CODE_MAX_OUTPUT_TOKENS`. API: als `max_tokens` im Payload. Wirkt auch über z.ai/GLM. | +| `DisableThinking` | Reasoning/Thinking komplett aus. CLI: `CLAUDE_CODE_DISABLE_THINKING=1` + `MAX_THINKING_TOKENS=0`, und `CLAUDE_CODE_EFFORT_LEVEL` wird entfernt (Explizit-Aus schlägt Effort). API-Pfad: kein Äquivalent → still ignoriert. | | `IsPublic` | Andere User können den Agent in ihren Sessions verwenden (aber nicht bearbeiten) | ### Model-Auflösung diff --git a/Endpoints/AgentEndpoint.cs b/Endpoints/AgentEndpoint.cs index f028653..9d77323 100644 --- a/Endpoints/AgentEndpoint.cs +++ b/Endpoints/AgentEndpoint.cs @@ -72,6 +72,8 @@ public static class AgentEndpoint a.AllowUserChoice, a.TaskInstruction, a.Effort, + a.MaxOutputTokens, + a.DisableThinking, a.AvatarScale, a.AvatarOffsetX, a.AvatarOffsetY, @@ -87,6 +89,18 @@ public static class AgentEndpoint return results; } + /// + /// Max-Output-Tokens robust parsen: Zahl, Zahl-als-String oder leer/null (= 0 = aus). + /// Negative Werte werden auf 0 geklemmt; das Frontend schickt bei leerem Feld null. + /// + private static int ParseMaxOutputTokens(JsonElement el) + { + int val = 0; + if (el.ValueKind == JsonValueKind.Number) el.TryGetInt32(out val); + else if (el.ValueKind == JsonValueKind.String) int.TryParse(el.GetString(), out val); + return val > 0 ? val : 0; + } + private static async Task HandleGet(HttpContext ctx, DatabaseService db) { var agentId = ctx.Request.RouteValues["agentId"]?.ToString(); @@ -139,6 +153,8 @@ public static class AgentEndpoint AllowUserChoice = !body.TryGetProperty("allowUserChoice", out var auc) || auc.GetBoolean(), TaskInstruction = body.TryGetProperty("taskInstruction", out var ti) ? ti.GetString() : null, Effort = body.TryGetProperty("effort", out var ef) ? ef.GetString() ?? "" : "", + MaxOutputTokens = body.TryGetProperty("maxOutputTokens", out var mot) ? ParseMaxOutputTokens(mot) : 0, + DisableThinking = body.TryGetProperty("disableThinking", out var dt) && dt.ValueKind == JsonValueKind.True, AvatarScale = body.TryGetProperty("avatarScale", out var asc) ? asc.GetDouble() : 1.0, AvatarOffsetX = body.TryGetProperty("avatarOffsetX", out var aox) ? aox.GetDouble() : 0, AvatarOffsetY = body.TryGetProperty("avatarOffsetY", out var aoy) ? aoy.GetDouble() : 0, @@ -187,6 +203,8 @@ public static class AgentEndpoint if (body.TryGetProperty("allowUserChoice", out var auc)) agent.AllowUserChoice = auc.GetBoolean(); if (body.TryGetProperty("taskInstruction", out var ti)) agent.TaskInstruction = ti.GetString(); if (body.TryGetProperty("effort", out var ef)) agent.Effort = ef.GetString() ?? ""; + if (body.TryGetProperty("maxOutputTokens", out var mot)) agent.MaxOutputTokens = ParseMaxOutputTokens(mot); + if (body.TryGetProperty("disableThinking", out var dt)) agent.DisableThinking = dt.ValueKind == JsonValueKind.True; if (body.TryGetProperty("avatarScale", out var asc)) agent.AvatarScale = asc.GetDouble(); if (body.TryGetProperty("avatarOffsetX", out var aox)) agent.AvatarOffsetX = aox.GetDouble(); if (body.TryGetProperty("avatarOffsetY", out var aoy)) agent.AvatarOffsetY = aoy.GetDouble(); diff --git a/Endpoints/ChatEndpoint.cs b/Endpoints/ChatEndpoint.cs index 3fb65f3..cd251f1 100644 --- a/Endpoints/ChatEndpoint.cs +++ b/Endpoints/ChatEndpoint.cs @@ -294,7 +294,7 @@ public static class ChatEndpoint if (anthropic.IsCliAvailable) { - await anthropic.StreamChatViaCli(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, workspace: ctx.Workspace, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: body.PlanMode, ultracode: body.Ultracode, webSearch: !string.IsNullOrEmpty(ctx.WebSearchVersion), sessionId: sessionId, agentId: ctx.AgentId, remoteMgr: remoteMgr, mcpServers: ctx.Session?.McpServers, useAutoContext: ctx.Agent?.UseAutoContext ?? true, effort: ctx.Effort, seedUsage: panelUsage, workCt: workCt, sseCt: sseCt); + await anthropic.StreamChatViaCli(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, workspace: ctx.Workspace, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: body.PlanMode, ultracode: body.Ultracode, webSearch: !string.IsNullOrEmpty(ctx.WebSearchVersion), sessionId: sessionId, agentId: ctx.AgentId, remoteMgr: remoteMgr, mcpServers: ctx.Session?.McpServers, useAutoContext: ctx.Agent?.UseAutoContext ?? true, effort: ctx.Effort, maxOutputTokens: ctx.MaxOutputTokens, disableThinking: ctx.DisableThinking, seedUsage: panelUsage, workCt: workCt, sseCt: sseCt); } else { @@ -304,7 +304,7 @@ public static class ChatEndpoint return; } - await anthropic.StreamChat(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, ctx.Workspace, apiToken: anthropicToken, sessionId: sessionId, agentId: ctx.AgentId, userId: userId, useMemory: ctx.UseMemory, webSearchVersion: ctx.WebSearchVersion, maxIterations: ctx.MaxIterations, toolChoice: effectiveToolChoice, review: ctx.Review, finalizer: ctx.Finalizer, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: ctx.PlanMode, useAutoContext: ctx.Agent?.UseAutoContext ?? true, useGitHubSearch: ctx.Agent?.UseGitHubSearch ?? false, useSerperSearch: ctx.Agent?.UseSerperSearch ?? false, effort: ctx.Effort, seedUsage: panelUsage, workCt: workCt, hardCt: hardCt, sseCt: sseCt, remoteMgr: remoteMgr); + await anthropic.StreamChat(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, ctx.Workspace, apiToken: anthropicToken, sessionId: sessionId, agentId: ctx.AgentId, userId: userId, useMemory: ctx.UseMemory, webSearchVersion: ctx.WebSearchVersion, maxIterations: ctx.MaxIterations, toolChoice: effectiveToolChoice, review: ctx.Review, finalizer: ctx.Finalizer, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: ctx.PlanMode, useAutoContext: ctx.Agent?.UseAutoContext ?? true, useGitHubSearch: ctx.Agent?.UseGitHubSearch ?? false, useSerperSearch: ctx.Agent?.UseSerperSearch ?? false, effort: ctx.Effort, maxOutputTokens: ctx.MaxOutputTokens, seedUsage: panelUsage, workCt: workCt, hardCt: hardCt, sseCt: sseCt, remoteMgr: remoteMgr); } } finally diff --git a/Models/AgentRecord.cs b/Models/AgentRecord.cs index 36d3eb0..37972f3 100644 --- a/Models/AgentRecord.cs +++ b/Models/AgentRecord.cs @@ -30,6 +30,18 @@ public class AgentRecord /// Bei OpenAI-Providern (MiniMax/Nvidia/etc.) wird das Feld stillschweigend ignoriert. /// public string Effort { get; set; } = ""; + /// + /// Hartes Limit für die Ausgabelänge (Tokens). 0 = aus (Modell-Default). + /// CLI-Pfad: wird als CLAUDE_CODE_MAX_OUTPUT_TOKENS gesetzt. API-Pfad: als max_tokens gesendet. + /// Wirkt auch bei z.ai/GLM (dort einer der wenigen Parameter, die tatsächlich honoriert werden). + /// + public int MaxOutputTokens { get; set; } + /// + /// Reasoning/Thinking komplett abschalten (Modell spielt/antwortet statt zu analysieren). + /// CLI-Pfad: CLAUDE_CODE_DISABLE_THINKING=1 + MAX_THINKING_TOKENS=0. + /// API-Pfad: kein Äquivalent (dort steuert Effort) → wird still ignoriert. + /// + public bool DisableThinking { get; set; } public double AvatarScale { get; set; } = 1.0; public double AvatarOffsetX { get; set; } public double AvatarOffsetY { get; set; } diff --git a/Models/ChatContext.cs b/Models/ChatContext.cs index aefb8e1..c340a43 100644 --- a/Models/ChatContext.cs +++ b/Models/ChatContext.cs @@ -18,6 +18,8 @@ public class ChatContext public bool UseMemory { get; set; } public string WebSearchVersion { get; set; } public string Effort { get; set; } + public int MaxOutputTokens { get; set; } // 0 = aus (Modell-Default) + public bool DisableThinking { get; set; } // nur CLI-Pfad wirksam public int MaxIterations { get; set; } public AgentRecord Agent { get; set; } public SessionRecord Session { get; set; } diff --git a/Services/AnthropicService.Cli.cs b/Services/AnthropicService.Cli.cs index d5f4669..59d16a7 100644 --- a/Services/AnthropicService.Cli.cs +++ b/Services/AnthropicService.Cli.cs @@ -38,6 +38,8 @@ public partial class AnthropicService string mcpServers = null, bool useAutoContext = true, string effort = null, + int maxOutputTokens = 0, + bool disableThinking = false, TokenUsage seedUsage = null, CancellationToken workCt = default, CancellationToken sseCt = default) @@ -377,6 +379,8 @@ public partial class AnthropicService mcpConfigPath: mcpConfigPath, mcpServerNames: mcpServerList.Select(s => s.Name).ToList(), effort: effort, + maxOutputTokens: maxOutputTokens, + disableThinking: disableThinking, sessionId: sessionId, chainId: chainId, cancellationToken: segCts.Token diff --git a/Services/AnthropicService.Streaming.cs b/Services/AnthropicService.Streaming.cs index ae40311..270f4b6 100644 --- a/Services/AnthropicService.Streaming.cs +++ b/Services/AnthropicService.Streaming.cs @@ -76,6 +76,7 @@ public partial class AnthropicService bool useGitHubSearch = false, bool useSerperSearch = false, string effort = null, + int maxOutputTokens = 0, CancellationToken workCt = default, CancellationToken hardCt = default, CancellationToken sseCt = default, @@ -136,7 +137,8 @@ public partial class AnthropicService var payload = new Dictionary { ["model"] = modelId, - ["max_tokens"] = 16384, + // Agent-Einstellung "Max Output Tokens" (0 = aus → bisheriger Default 16384). + ["max_tokens"] = maxOutputTokens > 0 ? maxOutputTokens : 16384, ["messages"] = messages, ["stream"] = true }; diff --git a/Services/ChatService.cs b/Services/ChatService.cs index 9d9963b..d11fee7 100644 --- a/Services/ChatService.cs +++ b/Services/ChatService.cs @@ -252,6 +252,8 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop? UseMemory = agent.UseMemory, WebSearchVersion = agent.WebSearchVersion, Effort = agent.Effort, + MaxOutputTokens = agent.MaxOutputTokens, + DisableThinking = agent.DisableThinking, MaxIterations = maxIterations, Agent = agent, Session = session, diff --git a/Services/ClaudeCliService.cs b/Services/ClaudeCliService.cs index 0a68156..f09a9d1 100644 --- a/Services/ClaudeCliService.cs +++ b/Services/ClaudeCliService.cs @@ -136,7 +136,7 @@ public class ClaudeCliService catch { /* Telemetrie darf den Stream nie stören */ } } - private ProcessStartInfo CreateCliStartInfo(List args, bool redirectInput = false, string effort = null, string workingDirectory = null, string modelId = null) + private ProcessStartInfo CreateCliStartInfo(List args, bool redirectInput = false, string effort = null, string workingDirectory = null, string modelId = null, int maxOutputTokens = 0, bool disableThinking = false) { var psi = new ProcessStartInfo { @@ -168,6 +168,23 @@ public class ClaudeCliService if (!string.IsNullOrEmpty(effort) && effort != "high") psi.Environment["CLAUDE_CODE_EFFORT_LEVEL"] = effort; + // Hartes Ausgabelimit (Agent-Einstellung, 0 = aus). Bremst "Wall of Text". + // Wirkt auch über die z.ai/Anthropic-Route – dort einer der wenigen Parameter, + // die tatsächlich honoriert werden (Sampling-Parameter wirft z.ai still weg). + if (maxOutputTokens > 0) + psi.Environment["CLAUDE_CODE_MAX_OUTPUT_TOKENS"] = maxOutputTokens.ToString(); + + // Thinking/Reasoning komplett abschalten (Agent-Einstellung). MAX_THINKING_TOKENS=0 + // zusätzlich, damit auch Backends ohne den Disable-Schalter kein Budget bekommen. + if (disableThinking) + { + psi.Environment["CLAUDE_CODE_DISABLE_THINKING"] = "1"; + psi.Environment["MAX_THINKING_TOKENS"] = "0"; + // Effort steuert genau das Reasoning-Budget, das hier abgeschaltet wird – + // beides gleichzeitig zu setzen wäre widersprüchlich. Explizit-Aus gewinnt. + psi.Environment.Remove("CLAUDE_CODE_EFFORT_LEVEL"); + } + // 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. @@ -324,6 +341,8 @@ public class ClaudeCliService string mcpConfigPath = null, List mcpServerNames = null, string effort = null, + int maxOutputTokens = 0, + bool disableThinking = false, string sessionId = null, string chainId = null, CancellationToken cancellationToken = default) @@ -489,7 +508,7 @@ public class ClaudeCliService try { - return await RunCliStreamingAsync(args, prompt, onEvent, cancellationToken, effortToUse, workspace, modelId, sessionId, chainId); + return await RunCliStreamingAsync(args, prompt, onEvent, cancellationToken, effortToUse, workspace, modelId, sessionId, chainId, maxOutputTokens, disableThinking); } finally { @@ -510,7 +529,9 @@ public class ClaudeCliService string workspace = null, string modelId = null, string sessionId = null, - string chainId = null) + string chainId = null, + int maxOutputTokens = 0, + bool disableThinking = false) { var result = new CliChatResult(); var fullOutput = new StringBuilder(); @@ -530,7 +551,7 @@ public class ClaudeCliService try { - var startInfo = CreateCliStartInfo(args, redirectInput: true, effort: effort, workingDirectory: workspace, modelId: modelId); + var startInfo = CreateCliStartInfo(args, redirectInput: true, effort: effort, workingDirectory: workspace, modelId: modelId, maxOutputTokens: maxOutputTokens, disableThinking: disableThinking); using var process = new Process { StartInfo = startInfo }; diff --git a/Services/DatabaseService.Agents.cs b/Services/DatabaseService.Agents.cs index 6cabd97..b3bdb89 100644 --- a/Services/DatabaseService.Agents.cs +++ b/Services/DatabaseService.Agents.cs @@ -19,8 +19,8 @@ public partial class DatabaseService agent.CreatedAt = now; agent.UpdatedAt = now; await conn.ExecuteAsync(@" - INSERT INTO agents (agent_id, user_id, name, avatar, avatar_small, description, model, tool_choice, custom_commands, auto_commit, use_soul, use_identity, use_user_info, use_memory, use_auto_context, use_github_search, use_serper_search, use_web_search, web_search_version, allow_user_choice, task_instruction, effort, avatar_scale, avatar_offset_x, avatar_offset_y, is_public, is_deleted, created_at, updated_at) - VALUES (@AgentId, @UserId, @Name, @Avatar, @AvatarSmall, @Description, @Model, @ToolChoice, @CustomCommands, @AutoCommit, @UseSoul, @UseIdentity, @UseUserInfo, @UseMemory, @UseAutoContext, @UseGitHubSearch, @UseSerperSearch, @UseWebSearch, @WebSearchVersion, @AllowUserChoice, @TaskInstruction, @Effort, @AvatarScale, @AvatarOffsetX, @AvatarOffsetY, @IsPublic, 0, @CreatedAt, @UpdatedAt) + INSERT INTO agents (agent_id, user_id, name, avatar, avatar_small, description, model, tool_choice, custom_commands, auto_commit, use_soul, use_identity, use_user_info, use_memory, use_auto_context, use_github_search, use_serper_search, use_web_search, web_search_version, allow_user_choice, task_instruction, effort, max_output_tokens, disable_thinking, avatar_scale, avatar_offset_x, avatar_offset_y, is_public, is_deleted, created_at, updated_at) + VALUES (@AgentId, @UserId, @Name, @Avatar, @AvatarSmall, @Description, @Model, @ToolChoice, @CustomCommands, @AutoCommit, @UseSoul, @UseIdentity, @UseUserInfo, @UseMemory, @UseAutoContext, @UseGitHubSearch, @UseSerperSearch, @UseWebSearch, @WebSearchVersion, @AllowUserChoice, @TaskInstruction, @Effort, @MaxOutputTokens, @DisableThinking, @AvatarScale, @AvatarOffsetX, @AvatarOffsetY, @IsPublic, 0, @CreatedAt, @UpdatedAt) ", agent); return agent; } @@ -56,6 +56,7 @@ public partial class DatabaseService custom_commands = @CustomCommands, auto_commit = @AutoCommit, use_soul = @UseSoul, use_identity = @UseIdentity, use_user_info = @UseUserInfo, use_memory = @UseMemory, use_auto_context = @UseAutoContext, use_github_search = @UseGitHubSearch, use_serper_search = @UseSerperSearch, use_web_search = @UseWebSearch, web_search_version = @WebSearchVersion, allow_user_choice = @AllowUserChoice, task_instruction = @TaskInstruction, effort = @Effort, + max_output_tokens = @MaxOutputTokens, disable_thinking = @DisableThinking, avatar_scale = @AvatarScale, avatar_offset_x = @AvatarOffsetX, avatar_offset_y = @AvatarOffsetY, is_public = @IsPublic, updated_at = @UpdatedAt WHERE agent_id = @AgentId AND user_id = @UserId diff --git a/Services/DatabaseService.cs b/Services/DatabaseService.cs index 2fb1cbc..b65c126 100644 --- a/Services/DatabaseService.cs +++ b/Services/DatabaseService.cs @@ -201,6 +201,8 @@ public partial class DatabaseService TryAddColumn(conn, "messages", "tag", "TEXT"); TryAddColumn(conn, "agents", "effort", "TEXT NOT NULL DEFAULT ''"); TryAddColumn(conn, "chains", "effort", "TEXT NOT NULL DEFAULT ''"); + TryAddColumn(conn, "agents", "max_output_tokens", "INTEGER NOT NULL DEFAULT 0"); + TryAddColumn(conn, "agents", "disable_thinking", "INTEGER NOT NULL DEFAULT 0"); TryAddColumn(conn, "sessions", "multi_model_mode", "TEXT"); TryAddColumn(conn, "sessions", "panel_agent_ids", "TEXT"); TryAddColumn(conn, "sessions", "moa_layers", "INTEGER NOT NULL DEFAULT 2"); diff --git a/wwwroot/index.html b/wwwroot/index.html index 560a281..5755d3e 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -262,6 +262,12 @@ + + +
Weitere Einstellungen