Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion docs/models.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
9 changes: 8 additions & 1 deletion src/Cli/Commands/Repl/ModelContextWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,15 @@ internal static class ModelContextWindow
/// so both bare model IDs (e.g. <c>claude-sonnet-4-6</c>) and provider-prefixed deployment
/// IDs (e.g. Bedrock's <c>anthropic.claude-sonnet-4-6-20250929-v1:0</c>) resolve correctly.
/// </summary>
internal static int GetBudget(string? modelId)
/// <param name="modelId">The model ID whose family determines the heuristic budget.</param>
/// <param name="overrideBudget">
/// User-configured override (<see cref="fuseraft.Core.Models.Config.UserConfig.ReplContextBudget"/>).
/// When positive, takes precedence over the per-family heuristic below.
/// </param>
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)))
Expand Down
14 changes: 8 additions & 6 deletions src/Cli/Commands/Repl/ReplSessionContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -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;
Expand Down
12 changes: 12 additions & 0 deletions src/Core/Models/Config/UserConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,18 @@ public sealed class UserConfig
[JsonPropertyName("skillCuration")]
public SkillCurationConfig? SkillCuration { get; set; }

/// <summary>
/// Overrides the REPL's heuristic working-context-token budget (<see cref="fuseraft.Cli.Commands.Repl.ModelContextWindow"/>)
/// used for history trimming and the /context, /compact, and context-warning displays.
/// REPL-only — unrelated to the orchestration-level <c>ContextBudgetConfig</c>
/// (warn/cutover/tool-result trimming for agent orchestration runs); the similar name is
/// coincidental, hence the <c>Repl</c> prefix here to keep the two unambiguous.
/// Applies to every model used in the REPL session, regardless of model family. Null or
/// &lt;= 0 falls back to the built-in per-family heuristic.
/// </summary>
[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;
Expand Down
21 changes: 13 additions & 8 deletions src/Infrastructure/Storage/UserConfigStore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -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));
}
Expand All @@ -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; }
Expand Down