diff --git a/docs/models.md b/docs/models.md index 2827beda..c482cda2 100644 --- a/docs/models.md +++ b/docs/models.md @@ -108,12 +108,19 @@ For any model not matching the table, specify `Provider`, `Endpoint`, and `ApiKe { "modelId": "anthropic.claude-sonnet-4-6-20250929-v1:0", "endpoint": "http://localhost:3000/api/openai/v1", - "apiKeyEnvVar": "OPENWEBUI_API_KEY" + "apiKeyEnvVar": "OPENWEBUI_API_KEY", + "replContextBudget": 400000 } ``` Set this file via `fuseraft repl` or `fuseraft models` (the setup wizard runs automatically on first use) or edit it directly. Run `fuseraft models` to see all models available from the configured provider, or use `/models` inside a REPL session for the same list. +### `replContextBudget` — REPL working-context override + +The REPL trims conversation history against a working-context-token budget (`ctx.ContextTokenBudget`, shown in `/context`), separate from `MaxContextTokens` above. By default this budget comes from a per-model-family heuristic (150K for 1M/128K+-class frontier models like `claude-*`/`gemini-*`/`grok-*`/`gpt-5*`, 100K for ~128K-class models like `gpt-4*`/`mistral-*`/`deepseek-*`, 80K otherwise) — deliberately conservative, since the REPL's char-based token estimate doesn't account for tool-schema tokens. + +Set `replContextBudget` in `~/.fuseraft/config` (a positive integer, in tokens) to override that heuristic for every model used in the REPL session, regardless of family. Leave it unset (or `0`) to keep the built-in heuristic. This is REPL-only and does not affect `MaxContextTokens` above (a separate per-agent hard ceiling enforced before each API call in non-REPL agent/orchestration contexts), nor the unrelated `ContextBudget` YAML block used in `orchestration.yaml` (warn/cutover/tool-result trimming for multi-agent orchestration runs) — the similarly-named `replContextBudget` field intentionally carries the `Repl` prefix to keep the two apart. + ### OS keychain fallback If an agent model has neither `ApiKey` nor `ApiKeyEnvVar` set after global defaults are applied, fuseraft retrieves the key stored in the OS keychain (set via `fuseraft key set` or the REPL wizard) and injects it as a literal `ApiKey`. This means the full auth resolution order for any agent model is: diff --git a/src/Cli/Commands/Repl/ModelContextWindow.cs b/src/Cli/Commands/Repl/ModelContextWindow.cs index 2c95dff7..0ecce616 100644 --- a/src/Cli/Commands/Repl/ModelContextWindow.cs +++ b/src/Cli/Commands/Repl/ModelContextWindow.cs @@ -35,8 +35,15 @@ internal static class ModelContextWindow /// so both bare model IDs (e.g. claude-sonnet-4-6) and provider-prefixed deployment /// IDs (e.g. Bedrock's anthropic.claude-sonnet-4-6-20250929-v1:0) resolve correctly. /// - internal static int GetBudget(string? modelId) + /// The model ID whose family determines the heuristic budget. + /// + /// User-configured override (). + /// When positive, takes precedence over the per-family heuristic below. + /// + internal static int GetBudget(string? modelId, int? overrideBudget = null) { + if (overrideBudget is > 0) return overrideBudget.Value; + if (string.IsNullOrWhiteSpace(modelId)) return DefaultBudget; if (LargeFamilyMarkers.Any(m => modelId.Contains(m, StringComparison.OrdinalIgnoreCase))) diff --git a/src/Cli/Commands/Repl/ReplSessionContext.cs b/src/Cli/Commands/Repl/ReplSessionContext.cs index 682ed187..2aee84bc 100644 --- a/src/Cli/Commands/Repl/ReplSessionContext.cs +++ b/src/Cli/Commands/Repl/ReplSessionContext.cs @@ -49,15 +49,17 @@ public string ModelId set { _modelId = value; - ContextTokenBudget = ModelContextWindow.GetBudget(value); + ContextTokenBudget = ModelContextWindow.GetBudget(value, UserCfg?.ReplContextBudget); } } // Working token budget for history trimming (TrimHistory) and the /context, /compact, - // and context-warning displays — derived from ModelId so a large-context model isn't - // held to the same ceiling as a small-context local model. Recomputed automatically - // whenever ModelId is (re)assigned, including on /provider setup, /model switch, and - // session resume. + // and context-warning displays — derived from ModelId (and UserCfg.ReplContextBudget, if set) + // so a large-context model isn't held to the same ceiling as a small-context local model. + // Recomputed automatically whenever ModelId is (re)assigned, including on /provider setup, + // /model switch, and session resume. Relies on UserCfg already being current at that point + // — the constructor below sets UserCfg before ModelId for this reason, and every later + // reassignment site that changes both (e.g. /provider setup) must preserve that order. public int ContextTokenBudget { get; private set; } = ModelContextWindow.DefaultBudget; public ModelConfig ModelConfig { get; set; } @@ -164,9 +166,9 @@ public ReplSessionContext( Cwd = cwd; SessionId = sessionId; StartedAt = startedAt; + UserCfg = userCfg; ModelId = modelId; ModelConfig = modelConfig; - UserCfg = userCfg; Client = client; Factory = factory; KeyStore = keyStore; diff --git a/src/Core/Models/Config/UserConfig.cs b/src/Core/Models/Config/UserConfig.cs index 663f590f..76ff4f9d 100644 --- a/src/Core/Models/Config/UserConfig.cs +++ b/src/Core/Models/Config/UserConfig.cs @@ -19,6 +19,18 @@ public sealed class UserConfig [JsonPropertyName("skillCuration")] public SkillCurationConfig? SkillCuration { get; set; } + /// + /// Overrides the REPL's heuristic working-context-token budget () + /// used for history trimming and the /context, /compact, and context-warning displays. + /// REPL-only — unrelated to the orchestration-level ContextBudgetConfig + /// (warn/cutover/tool-result trimming for agent orchestration runs); the similar name is + /// coincidental, hence the Repl prefix here to keep the two unambiguous. + /// Applies to every model used in the REPL session, regardless of model family. Null or + /// <= 0 falls back to the built-in per-family heuristic. + /// + [JsonPropertyName("replContextBudget")] + public int? ReplContextBudget { get; set; } + // Never written to disk — populated at runtime from the OS keychain. [JsonIgnore] public string ApiKey { get; set; } = string.Empty; diff --git a/src/Infrastructure/Storage/UserConfigStore.cs b/src/Infrastructure/Storage/UserConfigStore.cs index 8770a4c0..be698825 100644 --- a/src/Infrastructure/Storage/UserConfigStore.cs +++ b/src/Infrastructure/Storage/UserConfigStore.cs @@ -34,10 +34,11 @@ public static (UserConfig? Config, string? LegacyKey) Load() var config = new UserConfig { - ModelId = onDisk.ModelId ?? string.Empty, - Endpoint = onDisk.Endpoint ?? string.Empty, - Provider = onDisk.Provider ?? string.Empty, - ApiKeyEnvVar = onDisk.ApiKeyEnvVar ?? string.Empty, + ModelId = onDisk.ModelId ?? string.Empty, + Endpoint = onDisk.Endpoint ?? string.Empty, + Provider = onDisk.Provider ?? string.Empty, + ApiKeyEnvVar = onDisk.ApiKeyEnvVar ?? string.Empty, + ReplContextBudget = onDisk.ReplContextBudget, }; return (config, onDisk.ApiKey ?? legacyKeyFile); } @@ -67,10 +68,11 @@ public static void Save(UserConfig config) Directory.CreateDirectory(ConfigDir); var onDisk = new OnDiskConfig { - ModelId = config.ModelId, - Endpoint = config.Endpoint, - Provider = config.Provider, - ApiKeyEnvVar = config.ApiKeyEnvVar, + ModelId = config.ModelId, + Endpoint = config.Endpoint, + Provider = config.Provider, + ApiKeyEnvVar = config.ApiKeyEnvVar, + ReplContextBudget = config.ReplContextBudget, }; File.WriteAllText(ConfigPath, JsonSerializer.Serialize(onDisk, JsonOptions)); } @@ -91,6 +93,9 @@ private sealed class OnDiskConfig [JsonPropertyName("apiKeyEnvVar")] public string? ApiKeyEnvVar { get; set; } + [JsonPropertyName("replContextBudget")] + public int? ReplContextBudget { get; set; } + // Present only in configs created before keychain support was added. [JsonPropertyName("apiKey")] public string? ApiKey { get; set; }