mirror of
https://github.com/mark3labs/kit.git
synced 2026-06-13 19:20:06 +00:00
feat(kit): isolate viper config per Kit instance + add NewAgent (#42)
* feat(kit): isolate viper config per Kit instance + add NewAgent (#40) - Give each kit.New()/NewAgent() call an isolated *viper.Viper store so multiple Kit instances in one process no longer clobber each other's config; runtime mutators (SetModel, SetThinkingLevel) touch only the owning instance, making subagent spawning and multi-Kit embedding race-free - Thread the per-instance store through internal/config, internal/models (ProviderConfig.ConfigStore), internal/kitsetup, and the extension runner, with a nil -> process-global fallback so the CLI is unaffected - Share the global store when Options.CLI != nil to preserve cobra flag bindings (also opted in for internal/acpserver) - Remove viperInitMu; preserve the tri-state IsSet precedence contract and sdkDefaultMaxTokens floor - Add ergonomic NewAgent + functional options (WithModel, WithStreaming, Ephemeral, etc.); NewAgent defaults streaming on, opt out via WithStreaming(false). New(ctx, *Options) behavior is unchanged - Add config-isolation regression test and NewAgent/option coverage; document NewAgent and per-instance isolation in README Fixes #40 * docs(sdk): document NewAgent options and per-instance config isolation - Add "Functional options (NewAgent)" and "Per-instance config isolation" sections to the docs site SDK overview, with an options table and a "when to use which" constructor comparison - Cross-reference NewAgent from the SDK options page and correct the now per-instance ProviderAPIKey precedence wording - Document NewAgent + With* helpers and config isolation in pkg/kit/README and list NewAgent/Option in the API reference - Show the NewAgent constructor in the SDK examples getting-started snippet * fix(kit): correct config loading and isolate ACP sessions - Isolate each ACP session's config store instead of sharing the global viper, preventing per-session SetModel/SetThinkingLevel races; seed the root-command flag values (model, thinking-level, provider URL/key) so `kit acp -m <model>` is still honored - Run initConfig for isolated SDK stores by gating on opts.CLI instead of v.GetString("model"), which setSDKDefaults always populates and thus skipped .kit.yml / KIT_* loading for SDK callers - Configure KIT_* env overrides unconditionally in initConfig so passing an explicit config file no longer disables environment variable support - Wrap config unmarshal/validate errors with %w to preserve the error chain * fix(kit): make Options.Streaming a *bool to honor unset - Change Options.Streaming from bool to *bool so a zero-valued Options no longer forces stream=false; New only sets the key when non-nil, letting streaming resolve through the precedence chain (env -> config -> default true). This also fixes the CLI path, which never set the field - Mirror the existing sampling-parameter pointer pattern instead of adding a separate StreamingSet sentinel, keeping Options internally consistent - Update WithStreaming/NewAgent, subagent, and ACP callers to the pointer form; add regression tests for the nil-default and explicit opt-out paths - Update SDK docs (README, pkg/kit/README, options page) with the ptrBool helper and *bool semantics * fix(kit): inherit parent provider config in subagents - Copy the parent's effective provider/runtime config (API key, URL, TLS, thinking level, max-tokens, samplers) onto child Options in Kit.Subagent. After the per-instance viper isolation, the child's isolated store only re-loaded .kit.yml / KIT_*, silently dropping config the parent set via programmatic Options or runtime setters like SetThinkingLevel - Preserve the IsSet tri-state for max-tokens and samplers so per-model defaults still apply on the child when the parent left them unset - Add TestInheritProviderConfig covering propagation, unset keys, and nil-safety
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/mark3labs/kit/internal/extbridge"
|
||||
"github.com/mark3labs/kit/internal/extensions"
|
||||
@@ -38,10 +39,21 @@ func newSessionRegistry() *sessionRegistry {
|
||||
// given working directory. The Kit-generated session ID is used as the ACP
|
||||
// session ID so the mapping is 1:1.
|
||||
func (r *sessionRegistry) create(ctx context.Context, cwd string) (*acpSession, error) {
|
||||
// Each ACP session gets its own isolated config store (CLI is left nil) so
|
||||
// per-session SetModel / SetThinkingLevel calls cannot race or bleed across
|
||||
// the sessionRegistry. We seed the relevant root-command flag values from
|
||||
// the process-global store (which cobra populated from flags) so launching
|
||||
// `kit acp -m <model> [--thinking-level ...] [--provider-url ...]` is still
|
||||
// honored; .kit.yml and KIT_* env vars are loaded per session by kit.New.
|
||||
streamOn := true
|
||||
kitInstance, err := kit.New(ctx, &kit.Options{
|
||||
SessionDir: cwd,
|
||||
Quiet: true,
|
||||
Streaming: true,
|
||||
SessionDir: cwd,
|
||||
Quiet: true,
|
||||
Streaming: &streamOn,
|
||||
Model: viper.GetString("model"),
|
||||
ThinkingLevel: viper.GetString("thinking-level"),
|
||||
ProviderURL: viper.GetString("provider-url"),
|
||||
ProviderAPIKey: viper.GetString("provider-api-key"),
|
||||
})
|
||||
if err != nil {
|
||||
// Provide actionable guidance for provider auth errors, which are
|
||||
|
||||
@@ -7,32 +7,48 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// LoadAndValidateConfig loads configuration from viper, fixes environment variable
|
||||
// casing issues, and validates the configuration. Returns an error if loading or
|
||||
// validation fails.
|
||||
// LoadAndValidateConfig loads configuration from the process-global viper
|
||||
// store, fixes environment variable casing issues, and validates the
|
||||
// configuration. Returns an error if loading or validation fails.
|
||||
//
|
||||
// This is a convenience wrapper around [LoadAndValidateConfigFrom] using the
|
||||
// shared global store; it is retained for the CLI and other callers that rely
|
||||
// on viper's process-global state.
|
||||
func LoadAndValidateConfig() (*Config, error) {
|
||||
return LoadAndValidateConfigFrom(viper.GetViper())
|
||||
}
|
||||
|
||||
// LoadAndValidateConfigFrom loads configuration from the supplied per-instance
|
||||
// store, fixes environment variable casing issues, and validates the
|
||||
// configuration. When v is nil, the process-global store is used. Threading an
|
||||
// explicit store lets each Kit instance own an isolated configuration without
|
||||
// clobbering other instances in the same process.
|
||||
func LoadAndValidateConfigFrom(v *viper.Viper) (*Config, error) {
|
||||
if v == nil {
|
||||
v = viper.GetViper()
|
||||
}
|
||||
config := &Config{
|
||||
MCPServers: make(map[string]MCPServerConfig),
|
||||
}
|
||||
if err := viper.Unmarshal(config); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal config: %v", err)
|
||||
if err := v.Unmarshal(config); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
}
|
||||
|
||||
// Fix environment variable case sensitivity issue
|
||||
// Viper lowercases all keys, but we need to preserve the original case for environment variables
|
||||
fixEnvironmentCase(config)
|
||||
fixEnvironmentCase(v, config)
|
||||
|
||||
if err := config.Validate(); err != nil {
|
||||
return nil, fmt.Errorf("invalid config: %v", err)
|
||||
return nil, fmt.Errorf("invalid config: %w", err)
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// fixEnvironmentCase fixes the case of environment variable keys that were lowercased by Viper
|
||||
func fixEnvironmentCase(config *Config) {
|
||||
func fixEnvironmentCase(v *viper.Viper, config *Config) {
|
||||
// Get the raw config data from viper
|
||||
rawConfig := viper.AllSettings()
|
||||
rawConfig := v.AllSettings()
|
||||
|
||||
// Check if we have mcpServers in the raw config
|
||||
if mcpServersRaw, ok := rawConfig["mcpservers"]; ok {
|
||||
|
||||
@@ -98,9 +98,20 @@ type Runner struct {
|
||||
disabledTools map[string]bool // nil = all tools enabled
|
||||
customEventSubs map[string][]func(string) // inter-extension event bus
|
||||
optionOverrides map[string]string // runtime option overrides
|
||||
configStore *viper.Viper // per-instance config store (nil = global)
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// SetConfigStore sets the per-instance configuration store used by GetOption
|
||||
// to resolve "options.<name>" config values. When unset (nil), GetOption falls
|
||||
// back to the process-global viper store. Threading a per-Kit store keeps
|
||||
// extension option resolution isolated between Kit instances.
|
||||
func (r *Runner) SetConfigStore(v *viper.Viper) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
r.configStore = v
|
||||
}
|
||||
|
||||
// ShortcutEntry pairs a shortcut definition with its handler.
|
||||
type ShortcutEntry struct {
|
||||
Def ShortcutDef
|
||||
@@ -872,7 +883,13 @@ func (r *Runner) GetOption(name string) string {
|
||||
|
||||
// 3. Viper config: options.<name>
|
||||
configKey := "options." + name
|
||||
if v := viper.GetString(configKey); v != "" {
|
||||
r.mu.RLock()
|
||||
store := r.configStore
|
||||
r.mu.RUnlock()
|
||||
if store == nil {
|
||||
store = viper.GetViper()
|
||||
}
|
||||
if v := store.GetString(configKey); v != "" {
|
||||
return v
|
||||
}
|
||||
|
||||
|
||||
+65
-42
@@ -46,9 +46,9 @@ type AgentSetupOptions struct {
|
||||
ToolWrapper func([]fantasy.AgentTool) []fantasy.AgentTool
|
||||
|
||||
// ProviderConfig, when non-nil, is used directly instead of calling
|
||||
// BuildProviderConfig(). Callers that already hold viperInitMu can
|
||||
// pre-build this and release the lock before calling SetupAgent, so the
|
||||
// slow agent/MCP initialisation runs concurrently with other New() calls.
|
||||
// BuildProviderConfig(). Callers (e.g. Kit.New) pre-build this from their
|
||||
// per-instance config store and pass it here, so the slow agent/MCP
|
||||
// initialisation can run without further config reads.
|
||||
ProviderConfig *models.ProviderConfig
|
||||
// Debug enables debug logging. When zero-value, viper is consulted.
|
||||
// Only meaningful when ProviderConfig is also set.
|
||||
@@ -75,6 +75,11 @@ type AgentSetupOptions struct {
|
||||
// MCPTaskConfig configures task-augmented tools/call execution. The
|
||||
// zero value preserves historical synchronous-only behaviour.
|
||||
MCPTaskConfig tools.MCPTaskConfig
|
||||
// Viper is the per-instance configuration store. When set, it is used for
|
||||
// any fallback config reads (debug, no-extensions, max-steps, stream,
|
||||
// extension paths) and is attached to the extension runner. When nil, the
|
||||
// process-global viper store is used.
|
||||
Viper *viper.Viper
|
||||
}
|
||||
|
||||
// AgentSetupResult bundles the created agent and any debug logger so the caller
|
||||
@@ -87,57 +92,62 @@ type AgentSetupResult struct {
|
||||
ExtRunner *extensions.Runner
|
||||
}
|
||||
|
||||
// BuildProviderConfig creates a *models.ProviderConfig from the current viper
|
||||
// state. All entry points (root, script, SDK) converge through this function.
|
||||
// BuildProviderConfig creates a *models.ProviderConfig from the supplied viper
|
||||
// store (or the process-global store when v is nil). All entry points (root,
|
||||
// script, SDK) converge through this function.
|
||||
//
|
||||
// Generation parameter pointers (Temperature, TopP, etc.) are only set when
|
||||
// the user has explicitly configured them via CLI flag, environment variable,
|
||||
// or global config file. This allows per-model defaults from modelSettings
|
||||
// and customModels to fill in unset parameters downstream.
|
||||
func BuildProviderConfig() (*models.ProviderConfig, string, error) {
|
||||
systemPrompt, err := config.LoadSystemPrompt(viper.GetString("system-prompt"))
|
||||
func BuildProviderConfig(v *viper.Viper) (*models.ProviderConfig, string, error) {
|
||||
if v == nil {
|
||||
v = viper.GetViper()
|
||||
}
|
||||
systemPrompt, err := config.LoadSystemPrompt(v.GetString("system-prompt"))
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("failed to load system prompt: %w", err)
|
||||
}
|
||||
|
||||
numGPU := int32(viper.GetInt("num-gpu-layers"))
|
||||
mainGPU := int32(viper.GetInt("main-gpu"))
|
||||
numGPU := int32(v.GetInt("num-gpu-layers"))
|
||||
mainGPU := int32(v.GetInt("main-gpu"))
|
||||
|
||||
cfg := &models.ProviderConfig{
|
||||
ModelString: viper.GetString("model"),
|
||||
ModelString: v.GetString("model"),
|
||||
SystemPrompt: systemPrompt,
|
||||
ProviderAPIKey: viper.GetString("provider-api-key"),
|
||||
ProviderURL: viper.GetString("provider-url"),
|
||||
MaxTokens: viper.GetInt("max-tokens"),
|
||||
StopSequences: viper.GetStringSlice("stop-sequences"),
|
||||
ProviderAPIKey: v.GetString("provider-api-key"),
|
||||
ProviderURL: v.GetString("provider-url"),
|
||||
MaxTokens: v.GetInt("max-tokens"),
|
||||
StopSequences: v.GetStringSlice("stop-sequences"),
|
||||
NumGPU: &numGPU,
|
||||
MainGPU: &mainGPU,
|
||||
TLSSkipVerify: viper.GetBool("tls-skip-verify"),
|
||||
ThinkingLevel: models.ParseThinkingLevel(viper.GetString("thinking-level")),
|
||||
TLSSkipVerify: v.GetBool("tls-skip-verify"),
|
||||
ThinkingLevel: models.ParseThinkingLevel(v.GetString("thinking-level")),
|
||||
ConfigStore: v,
|
||||
}
|
||||
|
||||
// Only set generation parameter pointers when the user has explicitly
|
||||
// provided a value. This leaves nil pointers for unset params, allowing
|
||||
// per-model defaults (modelSettings / customModels params) to apply.
|
||||
if viper.IsSet("temperature") {
|
||||
v := float32(viper.GetFloat64("temperature"))
|
||||
cfg.Temperature = &v
|
||||
if v.IsSet("temperature") {
|
||||
val := float32(v.GetFloat64("temperature"))
|
||||
cfg.Temperature = &val
|
||||
}
|
||||
if viper.IsSet("top-p") {
|
||||
v := float32(viper.GetFloat64("top-p"))
|
||||
cfg.TopP = &v
|
||||
if v.IsSet("top-p") {
|
||||
val := float32(v.GetFloat64("top-p"))
|
||||
cfg.TopP = &val
|
||||
}
|
||||
if viper.IsSet("top-k") {
|
||||
v := int32(viper.GetInt("top-k"))
|
||||
cfg.TopK = &v
|
||||
if v.IsSet("top-k") {
|
||||
val := int32(v.GetInt("top-k"))
|
||||
cfg.TopK = &val
|
||||
}
|
||||
if viper.IsSet("frequency-penalty") {
|
||||
v := float32(viper.GetFloat64("frequency-penalty"))
|
||||
cfg.FrequencyPenalty = &v
|
||||
if v.IsSet("frequency-penalty") {
|
||||
val := float32(v.GetFloat64("frequency-penalty"))
|
||||
cfg.FrequencyPenalty = &val
|
||||
}
|
||||
if viper.IsSet("presence-penalty") {
|
||||
v := float32(viper.GetFloat64("presence-penalty"))
|
||||
cfg.PresencePenalty = &v
|
||||
if v.IsSet("presence-penalty") {
|
||||
val := float32(v.GetFloat64("presence-penalty"))
|
||||
cfg.PresencePenalty = &val
|
||||
}
|
||||
|
||||
return cfg, systemPrompt, nil
|
||||
@@ -149,14 +159,21 @@ func SetupAgent(ctx context.Context, opts AgentSetupOptions) (*AgentSetupResult,
|
||||
var modelConfig *models.ProviderConfig
|
||||
var systemPrompt string
|
||||
|
||||
// Resolve the config store: prefer the per-instance store, falling back to
|
||||
// the process-global store.
|
||||
v := opts.Viper
|
||||
if v == nil {
|
||||
v = viper.GetViper()
|
||||
}
|
||||
|
||||
if opts.ProviderConfig != nil {
|
||||
// Pre-built config supplied by caller (e.g. Kit.New after releasing
|
||||
// viperInitMu). Use it directly — no viper reads needed here.
|
||||
// Pre-built config supplied by caller (e.g. Kit.New after building the
|
||||
// per-instance store). Use it directly — no viper reads needed here.
|
||||
modelConfig = opts.ProviderConfig
|
||||
systemPrompt = modelConfig.SystemPrompt
|
||||
} else {
|
||||
var err error
|
||||
modelConfig, systemPrompt, err = BuildProviderConfig()
|
||||
modelConfig, systemPrompt, err = BuildProviderConfig(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -164,13 +181,13 @@ func SetupAgent(ctx context.Context, opts AgentSetupOptions) (*AgentSetupResult,
|
||||
|
||||
// Resolve debug / no-extensions / max-steps / streaming: prefer explicit
|
||||
// fields (set when ProviderConfig was pre-built) over viper fallback.
|
||||
debugEnabled := opts.Debug || viper.GetBool("debug")
|
||||
noExtensions := opts.NoExtensions || viper.GetBool("no-extensions")
|
||||
debugEnabled := opts.Debug || v.GetBool("debug")
|
||||
noExtensions := opts.NoExtensions || v.GetBool("no-extensions")
|
||||
maxSteps := opts.MaxSteps
|
||||
if maxSteps == 0 {
|
||||
maxSteps = viper.GetInt("max-steps")
|
||||
maxSteps = v.GetInt("max-steps")
|
||||
}
|
||||
streamingEnabled := opts.StreamingEnabled || viper.GetBool("stream")
|
||||
streamingEnabled := opts.StreamingEnabled || v.GetBool("stream")
|
||||
|
||||
// Create the appropriate debug logger.
|
||||
var debugLogger tools.DebugLogger
|
||||
@@ -189,7 +206,7 @@ func SetupAgent(ctx context.Context, opts AgentSetupOptions) (*AgentSetupResult,
|
||||
var extCreationOpts extensionCreationOpts
|
||||
if !noExtensions {
|
||||
var extErr error
|
||||
extRunner, extCreationOpts, extErr = loadExtensions()
|
||||
extRunner, extCreationOpts, extErr = loadExtensions(v)
|
||||
if extErr != nil {
|
||||
fmt.Printf("Warning: Failed to load extensions: %v\n", extErr)
|
||||
}
|
||||
@@ -253,9 +270,14 @@ type extensionCreationOpts struct {
|
||||
}
|
||||
|
||||
// loadExtensions discovers and loads Yaegi extensions, builds the runner,
|
||||
// and returns the tool wrapper/extra tools.
|
||||
func loadExtensions() (*extensions.Runner, extensionCreationOpts, error) {
|
||||
extraPaths := viper.GetStringSlice("extension")
|
||||
// and returns the tool wrapper/extra tools. The supplied store is used to
|
||||
// resolve the "extension" config key and is attached to the runner so
|
||||
// extension option lookups stay isolated to this Kit instance.
|
||||
func loadExtensions(v *viper.Viper) (*extensions.Runner, extensionCreationOpts, error) {
|
||||
if v == nil {
|
||||
v = viper.GetViper()
|
||||
}
|
||||
extraPaths := v.GetStringSlice("extension")
|
||||
loaded, err := extensions.LoadExtensions(extraPaths)
|
||||
if err != nil {
|
||||
return nil, extensionCreationOpts{}, err
|
||||
@@ -266,6 +288,7 @@ func loadExtensions() (*extensions.Runner, extensionCreationOpts, error) {
|
||||
}
|
||||
|
||||
runner := extensions.NewRunner(loaded)
|
||||
runner.SetConfigStore(v)
|
||||
|
||||
wrapper := func(tools []fantasy.AgentTool) []fantasy.AgentTool {
|
||||
return extensions.WrapToolsWithExtensions(tools, runner)
|
||||
|
||||
+51
-18
@@ -10,14 +10,24 @@ import (
|
||||
|
||||
// loadCustomModelsFromConfig loads custom model definitions from the config file
|
||||
// and returns them as a map of model ID -> ModelInfo. Returns nil if no custom
|
||||
// models are configured.
|
||||
// models are configured. Reads from the process-global viper store (the model
|
||||
// registry is a process-global singleton).
|
||||
func loadCustomModelsFromConfig() map[string]ModelInfo {
|
||||
if !viper.IsSet("customModels") {
|
||||
return loadCustomModelsFrom(viper.GetViper())
|
||||
}
|
||||
|
||||
// loadCustomModelsFrom loads custom model definitions from the supplied store.
|
||||
// When v is nil the process-global store is used.
|
||||
func loadCustomModelsFrom(v *viper.Viper) map[string]ModelInfo {
|
||||
if v == nil {
|
||||
v = viper.GetViper()
|
||||
}
|
||||
if !v.IsSet("customModels") {
|
||||
return nil
|
||||
}
|
||||
|
||||
var customModels map[string]CustomModelConfig
|
||||
if err := viper.UnmarshalKey("customModels", &customModels); err != nil {
|
||||
if err := v.UnmarshalKey("customModels", &customModels); err != nil {
|
||||
log.Printf("Warning: Failed to parse customModels: %v", err)
|
||||
return nil
|
||||
}
|
||||
@@ -60,15 +70,26 @@ func modelConfigToModelInfo(modelID string, cfg CustomModelConfig) ModelInfo {
|
||||
}
|
||||
|
||||
// LoadModelSettingsFromConfig loads per-model generation parameter overrides
|
||||
// from the config file. Keys are "provider/model" strings. Returns nil if
|
||||
// no model settings are configured.
|
||||
// from the process-global viper store. Keys are "provider/model" strings.
|
||||
// Returns nil if no model settings are configured.
|
||||
func LoadModelSettingsFromConfig() map[string]*GenerationParams {
|
||||
if !viper.IsSet("modelSettings") {
|
||||
return LoadModelSettingsFrom(viper.GetViper())
|
||||
}
|
||||
|
||||
// LoadModelSettingsFrom loads per-model generation parameter overrides from the
|
||||
// supplied per-instance store. When v is nil the process-global store is used.
|
||||
// Keys are "provider/model" strings. Returns nil if no model settings are
|
||||
// configured.
|
||||
func LoadModelSettingsFrom(v *viper.Viper) map[string]*GenerationParams {
|
||||
if v == nil {
|
||||
v = viper.GetViper()
|
||||
}
|
||||
if !v.IsSet("modelSettings") {
|
||||
return nil
|
||||
}
|
||||
|
||||
var settings map[string]GenerationParamsConfig
|
||||
if err := viper.UnmarshalKey("modelSettings", &settings); err != nil {
|
||||
if err := v.UnmarshalKey("modelSettings", &settings); err != nil {
|
||||
log.Printf("Warning: Failed to parse modelSettings: %v", err)
|
||||
return nil
|
||||
}
|
||||
@@ -148,12 +169,17 @@ func ApplyModelSettings(config *ProviderConfig, modelInfo *ModelInfo) {
|
||||
return
|
||||
}
|
||||
|
||||
// Resolve the config store: prefer the per-instance store carried on the
|
||||
// ProviderConfig (set by BuildProviderConfig / Kit.New), falling back to
|
||||
// the process-global store for callers that don't thread one through.
|
||||
store := config.ConfigStore
|
||||
|
||||
// Collect model-level params: modelSettings override > custom model params.
|
||||
// modelSettings takes priority because it's the more specific/intentional config.
|
||||
var params *GenerationParams
|
||||
|
||||
// First check modelSettings from config.
|
||||
if settings := LoadModelSettingsFromConfig(); settings != nil {
|
||||
if settings := LoadModelSettingsFrom(store); settings != nil {
|
||||
modelKey := provider + "/" + modelName
|
||||
if p, ok := settings[modelKey]; ok {
|
||||
params = p
|
||||
@@ -173,28 +199,28 @@ func ApplyModelSettings(config *ProviderConfig, modelInfo *ModelInfo) {
|
||||
// We check viper.IsSet() which returns true only when the key was
|
||||
// set via CLI flag, environment variable, or config file global section.
|
||||
|
||||
if params.MaxTokens != nil && !isExplicitlySet("max-tokens") {
|
||||
if params.MaxTokens != nil && !isExplicitlySet(store, "max-tokens") {
|
||||
config.MaxTokens = *params.MaxTokens
|
||||
}
|
||||
if params.Temperature != nil && !isExplicitlySet("temperature") {
|
||||
if params.Temperature != nil && !isExplicitlySet(store, "temperature") {
|
||||
config.Temperature = params.Temperature
|
||||
}
|
||||
if params.TopP != nil && !isExplicitlySet("top-p") {
|
||||
if params.TopP != nil && !isExplicitlySet(store, "top-p") {
|
||||
config.TopP = params.TopP
|
||||
}
|
||||
if params.TopK != nil && !isExplicitlySet("top-k") {
|
||||
if params.TopK != nil && !isExplicitlySet(store, "top-k") {
|
||||
config.TopK = params.TopK
|
||||
}
|
||||
if params.FrequencyPenalty != nil && !isExplicitlySet("frequency-penalty") {
|
||||
if params.FrequencyPenalty != nil && !isExplicitlySet(store, "frequency-penalty") {
|
||||
config.FrequencyPenalty = params.FrequencyPenalty
|
||||
}
|
||||
if params.PresencePenalty != nil && !isExplicitlySet("presence-penalty") {
|
||||
if params.PresencePenalty != nil && !isExplicitlySet(store, "presence-penalty") {
|
||||
config.PresencePenalty = params.PresencePenalty
|
||||
}
|
||||
if len(params.StopSequences) > 0 && !isExplicitlySet("stop-sequences") {
|
||||
if len(params.StopSequences) > 0 && !isExplicitlySet(store, "stop-sequences") {
|
||||
config.StopSequences = params.StopSequences
|
||||
}
|
||||
if params.ThinkingLevel != "" && !isExplicitlySet("thinking-level") {
|
||||
if params.ThinkingLevel != "" && !isExplicitlySet(store, "thinking-level") {
|
||||
config.ThinkingLevel = params.ThinkingLevel
|
||||
}
|
||||
if params.SystemPrompt != "" && config.SystemPrompt == "" {
|
||||
@@ -228,7 +254,14 @@ func LoadSystemPromptValue(input string) string {
|
||||
// isExplicitlySet returns true when the user has explicitly set a config key
|
||||
// via CLI flag, environment variable, or the global section of the config file.
|
||||
// Model-level defaults should not override explicitly set values.
|
||||
func isExplicitlySet(key string) bool {
|
||||
//
|
||||
// The check runs against the supplied per-instance store when non-nil,
|
||||
// otherwise the process-global store. This keeps the "explicit vs unset"
|
||||
// precedence contract per-Kit-instance once a store is threaded through.
|
||||
func isExplicitlySet(v *viper.Viper, key string) bool {
|
||||
if v == nil {
|
||||
v = viper.GetViper()
|
||||
}
|
||||
// viper.IsSet returns true if the key has been set in any of the
|
||||
// data stores (flag, env, config file, default). We need to check
|
||||
// whether the value was set at the global config level (not just
|
||||
@@ -239,7 +272,7 @@ func isExplicitlySet(key string) bool {
|
||||
// file values. This means global config file values (e.g.
|
||||
// temperature: 0.7 at the top level) will correctly take precedence
|
||||
// over model-level defaults, which is the desired behavior.
|
||||
return viper.IsSet(key)
|
||||
return v.IsSet(key)
|
||||
}
|
||||
|
||||
// GenerationParams holds per-model generation parameter defaults.
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
openaisdk "github.com/charmbracelet/openai-go"
|
||||
|
||||
"github.com/mark3labs/kit/internal/auth"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -164,6 +165,13 @@ type ProviderConfig struct {
|
||||
ThinkingLevel ThinkingLevel
|
||||
DisableCaching bool // Opt-out: set to true to disable automatic prompt caching
|
||||
|
||||
// ConfigStore is the per-instance configuration store used to resolve
|
||||
// "explicitly set" precedence checks (isExplicitlySet), per-model
|
||||
// settings, and right-sizing. When nil, the process-global viper store is
|
||||
// used. Threading a per-Kit store here keeps generation-parameter
|
||||
// precedence isolated between Kit instances in the same process.
|
||||
ConfigStore *viper.Viper
|
||||
|
||||
// ProgressReaderFunc, when set, wraps an io.Reader with progress display
|
||||
// for long operations like Ollama model pulls. The returned io.ReadCloser
|
||||
// must be closed when done. When nil, the raw reader is consumed directly
|
||||
@@ -530,7 +538,7 @@ func rightSizeMaxTokens(config *ProviderConfig, modelInfo *ModelInfo) {
|
||||
if modelInfo == nil || modelInfo.Limit.Output <= 0 {
|
||||
return
|
||||
}
|
||||
if isExplicitlySet("max-tokens") {
|
||||
if isExplicitlySet(config.ConfigStore, "max-tokens") {
|
||||
return
|
||||
}
|
||||
target := min(modelInfo.Limit.Output, defaultRightSizeCap)
|
||||
|
||||
Reference in New Issue
Block a user