Skip to content

Add template_fill config type to generate model variants from one template#550

Closed
kamjin3086 wants to merge 5 commits intomostlygeek:mainfrom
kamjin3086:main
Closed

Add template_fill config type to generate model variants from one template#550
kamjin3086 wants to merge 5 commits intomostlygeek:mainfrom
kamjin3086:main

Conversation

@kamjin3086
Copy link

Implements #549.

Intent:
One base config holds shared settings; fill_enum holds per-variant overrides. The server expands this into multiple models (e.g. Qwen3.5-35B-A3B-thinking-normal, -thinking-coding, etc.), so users can switch variants in the UI or by model ID without duplicating config. This is especially useful when controlling parameters on the client is inconvenient or not possible.

Compatibility:
New config type only; existing configs are unchanged. It also works with setParamById without conflict—both can be used in the same setup; they do not interfere with each other.

Example:

# Example: template_fill / variants — one base config, multiple model variants
healthCheckTimeout: 30

models:
  # Qwen3.5 with variants: one template expands to several models
  Qwen3.5-35B-A3B:
    proxy: "http://127.0.0.1:${PORT}"
    cmdStop: "podman stop llama-rocm-7.2"
    name: "Qwen3.5 35B A3B"
    description: "Qwen3.5 thinking model with multiple parameter variants"
    cmd: |
      toolbox run --container llama-rocm-7.2 --
      llama-server
      --model /home/kamjin/models/Qwen3.5/Qwen3.5-35B-A3B.Q6_K.gguf
      --mmproj /home/kamjin/models/Qwen3.5/mmproj-F16.gguf
      --fit on
      --seed 3407
      --temp 1.0
      --top-p 0.95
      --min-p 0.0
      --top-k 20
      --repeat-penalty 1.0
      --presence-penalty 1.5
      --port ${PORT}
      --jinja
      -c 262144
      -ngl 999
      -fa 1
      --no-mmap

    # Variants: expand this template into multiple model configs
    variants:
      # Becomes: Qwen3.5-35B-A3B-thinking_normal
      thinking_normal:
        cmdAdd: --temp 1.0 --presence-penalty 1.5
        name: "Qwen3.5 35B (Thinking, Normal)"
        description: "For general tasks with thinking enabled, temp=1.0"
        aliases:
          - "qwen35-thinking"

      # Becomes: Qwen3.5-35B-A3B-thinking_coding
      thinking_coding:
        cmdAdd: --temp 0.6 --presence-penalty 0.0
        name: "Qwen3.5 35B (Thinking, Coding)"
        description: "For coding tasks with thinking enabled, temp=0.6"
        aliases:
          - "qwen35-code"

      # Becomes: Qwen3.5-35B-A3B-nothinking_normal
      nothinking_normal:
        cmdAdd: "--temp 0.7 --top-p 0.8 --chat-template-kwargs '{\"enable_thinking\": false}'"
        name: "Qwen3.5 35B (Instruct, Normal)"
        description: "For general tasks without thinking, temp=0.7"
        aliases:
          - "qwen35-instruct"

      # Becomes: Qwen3.5-35B-A3B-nothinking_coding
      nothinking_coding:
        cmdAdd: "--temp 0.6 --presence-penalty 0.0 --chat-template-kwargs '{\"enable_thinking\": false}'"
        name: "Qwen3.5 35B (Instruct, Coding)"
        description: "For coding without thinking, temp=0.6"

  # Regular model (no variants) for comparison
  Gemma-3-4B-IT:
    proxy: "http://127.0.0.1:${PORT}"
    cmdStop: "podman stop llama-rocm-7.2"
    aliases:
      - "gemma3-4b"
    cmd: |
      toolbox run --container llama-rocm-7.2 --
      llama-server
      --model /home/kamjin/.lmstudio/models/lmstudio-community/gemma-3-4b-it-GGUF/gemma-3-4b-it-Q4_K_M.gguf
      --alias "lmstudio-community/gemma-3-4b-it"
      --fit on
      --seed 3407
      --temp 1.0
      --top-p 0.95
      --min-p 0.01
      --top-k 64
      --port ${PORT}
      --jinja
      -c 8192
      -ngl 999
      -fa 1
      --no-mmap

GET /v1/models response (after expanding variants)
With the config above, the template Qwen3.5-35B-A3B is expanded into four variant model IDs. A request to GET /v1/models returns something like:

{
  "object": "list",
  "data": [
    {
      "id": "Gemma-3-4B-IT",
      "object": "model",
      "created": 1709251200,
      "owned_by": "llama-swap"
    },
    {
      "id": "Qwen3.5-35B-A3B-nothinking_coding",
      "object": "model",
      "created": 1709251200,
      "owned_by": "llama-swap",
      "name": "Qwen3.5 35B (Instruct, Coding)",
      "description": "For coding without thinking, temp=0.6"
    },
    {
      "id": "Qwen3.5-35B-A3B-nothinking_normal",
      "object": "model",
      "created": 1709251200,
      "owned_by": "llama-swap",
      "name": "Qwen3.5 35B (Instruct, Normal)",
      "description": "For general tasks without thinking, temp=0.7"
    },
    {
      "id": "Qwen3.5-35B-A3B-thinking_coding",
      "object": "model",
      "created": 1709251200,
      "owned_by": "llama-swap",
      "name": "Qwen3.5 35B (Thinking, Coding)",
      "description": "For coding tasks with thinking enabled, temp=0.6"
    },
    {
      "id": "Qwen3.5-35B-A3B-thinking_normal",
      "object": "model",
      "created": 1709251200,
      "owned_by": "llama-swap",
      "name": "Qwen3.5 35B (Thinking, Normal)",
      "description": "For general tasks with thinking enabled, temp=1.0"
    }
  ]
}

Entries are sorted by id. Clients can use these model IDs (e.g. in model for chat completions) or the configured aliases (e.g. qwen35-thinking, qwen35-code) where supported.

@coderabbitai
Copy link

coderabbitai bot commented Mar 1, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds template-based model variants: new variants schema and example, VariantConfig and Variants field on ModelConfig, an ExpandVariants implementation that materializes per-variant models, integration into config loading (substituting template refs), and comprehensive unit tests for expansion and command merging.

Changes

Cohort / File(s) Summary
Schema & Example
config-schema.json, config.example.yaml
Adds variants JSON-schema and example YAML demonstrating per-variant overrides (cmdAdd, name, description, env, aliases, unlisted) and generated model suffix naming.
Model types
proxy/config/model_config.go
Adds Variants map[string]VariantConfig to ModelConfig and a new exported VariantConfig struct for per-variant overrides.
Variant expansion logic
proxy/config/template_fill.go
New file implementing ExpandVariants(models) (ExpandVariantsResult, error), helpers to merge/parse commands (mergeCommands, tokenizeCommand, argument normalization), env/aliases/unlisted merging, collision/duplicate detection, and shallow-copy utilities.
Config loading integration
proxy/config/config.go
Calls ExpandVariants inside LoadConfigFromReader, replaces config.Models with expanded models, and substitutes template IDs in groups and preload hooks via new substituteTemplateRefsInConfig helper.
Tests
proxy/config/template_fill_test.go
Adds extensive unit tests covering expansion behavior, command merging/tokenization, macro propagation, alias uniqueness, group interactions, and loading variants from config file.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

enhancement, configuration

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 44.83% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: adding template variant generation functionality to allow multiple model variants from a single template configuration.
Description check ✅ Passed The description provides clear context linking to issue #549, explains the intent (reusing base config with per-variant overrides), confirms backward compatibility, and includes comprehensive YAML and JSON examples demonstrating the feature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
proxy/config/template_fill_test.go (1)

238-301: Add a regression case for mixed flag override formats.

Please add a table case where base uses --temp 0.8 and add uses --temp=1.0 so this bug class is covered going forward.

➕ Suggested test case
@@
 		{
 			name:     "equals format override",
 			base:     "server --temp=0.8 --ctx=4096",
 			add:      "--temp=1.0",
 			expected: "server --temp=1.0 --ctx=4096",
 		},
+		{
+			name:     "mixed format override",
+			base:     "server --temp 0.8 --ctx 4096",
+			add:      "--temp=1.0",
+			expected: "server --temp=1.0 --ctx 4096",
+		},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@proxy/config/template_fill_test.go` around lines 238 - 301, Add a regression
test in TestMergeCommands_Basic that covers mixed flag formats: add a test case
entry where name is like "mixed flag override", base is "server --temp 0.8" and
add is "--temp=1.0" with expected "server --temp=1.0"; place it alongside the
other cases in the tests slice so mergeCommands is exercised for overriding a
space-separated flag with an equals-form flag.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@proxy/config/config.go`:
- Around line 211-215: ExpandVariants currently replaces template entries in
config.Models which can break references in groups.members and
hooks.on_startup.preload; change the flow in the code that calls ExpandVariants
to either (a) return a mapping from a template ID to the generated variant IDs
and then replace any occurrences of that template ID in groups.members and
hooks.on_startup.preload with the appropriate variant IDs, or (b) detect when a
template ID is still referenced after expansion and return a clear validation
error. Concretely: modify ExpandVariants (or wrap it) to produce both the new
[]Model and a map[string][]string of templateID->variantIDs, then scan
config.Groups[*].Members and config.Hooks.OnStartup.Preload to substitute or
expand template IDs using that map (or collect missing references and return an
error), so no references remain dangling after config.Models is replaced.

In `@proxy/config/template_fill.go`:
- Around line 126-141: When replacing a separate-value form in template_fill.go
(variables: baseTokens, addTokens, baseIdx, i, flag, token, embeddedValue, and
helper isArgument), if you convert to "--flag=value" you must also remove the
stale following positional token from baseTokens (delete baseTokens[baseIdx+1]
or splice it out) so you don't leave the old value behind; adjust any indexes
accordingly and ensure i is advanced consistently. Apply the same fix to the
similar branch around the other occurrence (lines handling baseIdx+1 at the
other block) so both the "--flag=value overwrites --flag value" conversion
splices out the old value token rather than leaving it.
- Around line 13-26: ExpandVariants currently silently overwrites entries in
result when a generated variant ID collides with an existing key; change
ExpandVariants to return (map[string]ModelConfig, error), and before writing
into result (both the no-variant branch and when adding variantModelID created
by expandVariant), check if that key already exists in result and if so return a
descriptive error (include modelID and variantSuffix for collisions); ensure
callers are updated to handle the error.

---

Nitpick comments:
In `@proxy/config/template_fill_test.go`:
- Around line 238-301: Add a regression test in TestMergeCommands_Basic that
covers mixed flag formats: add a test case entry where name is like "mixed flag
override", base is "server --temp 0.8" and add is "--temp=1.0" with expected
"server --temp=1.0"; place it alongside the other cases in the tests slice so
mergeCommands is exercised for overriding a space-separated flag with an
equals-form flag.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 49546e2 and d2bb742.

📒 Files selected for processing (6)
  • config-schema.json
  • config.example.yaml
  • proxy/config/config.go
  • proxy/config/model_config.go
  • proxy/config/template_fill.go
  • proxy/config/template_fill_test.go

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
proxy/config/template_fill_test.go (1)

303-342: Test name should include a descriptive suffix.

Per the coding guidelines, test names should follow the pattern Test<Component>_<TestCase>. Consider renaming to something like TestTokenizeCommand_Parsing or TestTokenizeCommand_BasicCases.

Suggested rename
-func TestTokenizeCommand(t *testing.T) {
+func TestTokenizeCommand_Parsing(t *testing.T) {

As per coding guidelines: "proxy/**/*_test.go: Follow test naming conventions like TestProxyManager_<test name>, TestProcessGroup_<test name>, etc."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@proxy/config/template_fill_test.go` around lines 303 - 342, Rename the test
function TestTokenizeCommand to follow the project's test naming convention
(e.g., TestTokenizeCommand_Parsing or TestTokenizeCommand_BasicCases) so it
matches the required pattern; update the test declaration for
TestTokenizeCommand and any references to it while keeping the same test body
and calls to tokenizeCommand and assert.Equal.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@proxy/config/template_fill_test.go`:
- Around line 303-342: Rename the test function TestTokenizeCommand to follow
the project's test naming convention (e.g., TestTokenizeCommand_Parsing or
TestTokenizeCommand_BasicCases) so it matches the required pattern; update the
test declaration for TestTokenizeCommand and any references to it while keeping
the same test body and calls to tokenizeCommand and assert.Equal.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d2bb742 and edcf548.

📒 Files selected for processing (1)
  • proxy/config/template_fill_test.go

Apply fixes from code review: collision detection, stale command
tokens, and template refs in groups/preload.

- ExpandVariants now returns (ExpandVariantsResult, error); detect
  duplicate model IDs and variant collisions and fail fast with
  clear errors. Expose TemplateToVariants for config substitution.
- mergeCommands: when add uses --flag=value and base had --flag value,
  clear the base's value slot and compact baseTokens so the final
  command has no leftover positional value (e.g. no "--temp=1.0 0.8").
- substituteTemplateRefsInConfig: replace template model IDs in
  groups.members and hooks.on_startup.preload with the expanded
  variant IDs so refs stay valid after expansion.
- Tests: add mixed-format override case to TestMergeCommands_Basic;
  rename TestTokenizeCommand to TestTokenizeCommand_Parsing.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
proxy/config/template_fill.go (2)

201-233: Tokenizer doesn't handle escape sequences within quotes.

The tokenizer handles quoted strings but doesn't process escape sequences like \" or \\ inside quotes. For example, --arg "value with \"escaped\" quotes" would not be parsed correctly. This may be acceptable if such edge cases aren't expected in practice, but consider documenting this limitation.

Consider adding a comment to document this limitation:

 // tokenizeCommand splits a command string into tokens, handling quoted strings
+// Note: Does not handle escape sequences within quoted strings (e.g., \")
 func tokenizeCommand(cmd string) []string {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@proxy/config/template_fill.go` around lines 201 - 233, The tokenizer function
tokenizeCommand does not handle escape sequences inside quoted strings (e.g., \"
or \\), so update tokenizeCommand to recognize backslash as an escape character
when inQuote: on seeing '\' consume the next rune and append it literally
(handling end-of-string safely), ensuring sequences like \" and \\ are preserved
inside the token; alternatively (if you prefer not to implement escapes), add a
clear comment above tokenizeCommand documenting the limitation and examples of
unsupported inputs so callers know quoted-escape sequences are not handled.

68-70: Consider copying Macros to avoid shared slice mutation.

base.Macros is assigned directly to expanded.Macros. Since MacroList is a slice type ([]MacroEntry), this shares the underlying array. If code elsewhere mutates the expanded model's macros, it would affect the base model's macros too. While this may not be an issue in the current flow (since templates are discarded), it's safer to copy.

♻️ Suggested fix
 		Filters:          base.Filters,
-		Macros:           base.Macros,
+		Macros:           copyMacroList(base.Macros),
 		Metadata:         copyMetadata(base.Metadata),

Add the helper function:

// copyMacroList creates a copy of a MacroList
func copyMacroList(ml MacroList) MacroList {
	if ml == nil {
		return nil
	}
	result := make(MacroList, len(ml))
	copy(result, ml)
	return result
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@proxy/config/template_fill.go` around lines 68 - 70, The expanded model
currently assigns base.Macros directly to expanded.Macros which shares the
underlying slice and risks unintended mutation; add a helper like
copyMacroList(ml MacroList) that returns a newly allocated MacroList copy
(preserving nil semantics) and use it when populating expanded.Macros (replace
assignment of base.Macros with copyMacroList(base.Macros)) to avoid shared-slice
aliasing.
proxy/config/template_fill_test.go (1)

366-389: Consider adding a test for template ID substitution in groups.

This test verifies that explicitly named variant IDs work in groups. However, it doesn't test the substituteTemplateRefsInConfig functionality where a template ID (e.g., template-model) in groups.members is automatically expanded to its variant IDs.

🧪 Suggested additional test
func TestExpandVariants_TemplateRefSubstitutionInGroups(t *testing.T) {
	content := `
models:
  template-model:
    cmd: server --port ${PORT}
    variants:
      v1:
        cmdAdd: --v1
      v2:
        cmdAdd: --v2

groups:
  mygroup:
    members:
      - template-model
`
	config, err := LoadConfigFromReader(strings.NewReader(content))
	assert.NoError(t, err)

	// Template ID should be substituted with variant IDs
	group := config.Groups["mygroup"]
	assert.Contains(t, group.Members, "template-model-v1")
	assert.Contains(t, group.Members, "template-model-v2")
	assert.NotContains(t, group.Members, "template-model")
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@proxy/config/template_fill_test.go` around lines 366 - 389, Add a new unit
test named TestExpandVariants_TemplateRefSubstitutionInGroups that loads a
config where a group member is the template ID "template-model" (not explicit
variant IDs), calls LoadConfigFromReader (so substituteTemplateRefsInConfig runs
during config load), and asserts that group := config.Groups["mygroup"] contains
"template-model-v1" and "template-model-v2" and does NOT contain
"template-model"; this verifies substituteTemplateRefsInConfig correctly expands
template IDs into their variant IDs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@proxy/config/template_fill_test.go`:
- Around line 366-389: Add a new unit test named
TestExpandVariants_TemplateRefSubstitutionInGroups that loads a config where a
group member is the template ID "template-model" (not explicit variant IDs),
calls LoadConfigFromReader (so substituteTemplateRefsInConfig runs during config
load), and asserts that group := config.Groups["mygroup"] contains
"template-model-v1" and "template-model-v2" and does NOT contain
"template-model"; this verifies substituteTemplateRefsInConfig correctly expands
template IDs into their variant IDs.

In `@proxy/config/template_fill.go`:
- Around line 201-233: The tokenizer function tokenizeCommand does not handle
escape sequences inside quoted strings (e.g., \" or \\), so update
tokenizeCommand to recognize backslash as an escape character when inQuote: on
seeing '\' consume the next rune and append it literally (handling end-of-string
safely), ensuring sequences like \" and \\ are preserved inside the token;
alternatively (if you prefer not to implement escapes), add a clear comment
above tokenizeCommand documenting the limitation and examples of unsupported
inputs so callers know quoted-escape sequences are not handled.
- Around line 68-70: The expanded model currently assigns base.Macros directly
to expanded.Macros which shares the underlying slice and risks unintended
mutation; add a helper like copyMacroList(ml MacroList) that returns a newly
allocated MacroList copy (preserving nil semantics) and use it when populating
expanded.Macros (replace assignment of base.Macros with
copyMacroList(base.Macros)) to avoid shared-slice aliasing.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between edcf548 and 8fb9cd7.

📒 Files selected for processing (3)
  • proxy/config/config.go
  • proxy/config/template_fill.go
  • proxy/config/template_fill_test.go

- tokenizeCommand: handle backslash escapes inside quoted strings (\ " \' \n \t)
  so --chat-template-kwargs "{\"enable_thinking\": false}" parses correctly.
- copyMacroList: avoid expanded variants sharing base.Macros slice.
- Tests: tokenizer double/single-quoted JSON, mergeCommands chat-template-kwargs,
  TestExpandVariants_TemplateRefSubstitutionInGroups, TestLoadTestVariantsConfig_FromFile.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@proxy/config/template_fill.go`:
- Line 68: The Filters field is being assigned directly from base.Filters which
shares underlying map references; add a deep-copy helper (e.g., copyFilters(f
ModelFilters) ModelFilters) that clones SetParams and SetParamsByID (copying
inner maps) and use it where Filters is set (replace direct assignment of
base.Filters with copyFilters(base.Filters)) so variants get their own
independent maps; reference ModelFilters, SetParams, and SetParamsByID when
implementing the helper and wiring it into the code that constructs the variant.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8fb9cd7 and 966c861.

📒 Files selected for processing (2)
  • proxy/config/template_fill.go
  • proxy/config/template_fill_test.go

- copyFilters: deep-copy ModelFilters (SetParams, SetParamsByID maps) so
  variants do not share map references with base.
- gofmt template_fill_test.go (slice literal spacing).
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@proxy/config/template_fill.go`:
- Around line 289-299: The copy operation for SetParamsByID currently omits keys
whose value is nil, changing semantics; update the loop in the function that
populates result.SetParamsByID so that when f.SetParamsByID[k] (v) is nil you
still assign result.SetParamsByID[k] = nil instead of skipping it, and when v !=
nil continue to deep-copy the inner map as done now (references: SetParamsByID,
result.SetParamsByID).

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 966c861 and a4be509.

📒 Files selected for processing (2)
  • proxy/config/template_fill.go
  • proxy/config/template_fill_test.go

Comment on lines +289 to +299
if f.SetParamsByID != nil {
result.SetParamsByID = make(map[string]map[string]any, len(f.SetParamsByID))
for k, v := range f.SetParamsByID {
if v != nil {
copied := make(map[string]any, len(v))
for kk, vv := range v {
copied[kk] = vv
}
result.SetParamsByID[k] = copied
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Preserve SetParamsByID keys when inner map is nil.

On Line 292, nil entries are skipped, which drops keys from the copied map. That changes semantics (key exists with nil vs key absent). copyFilters should retain those keys to stay lossless.

🛠️ Suggested fix
 	if f.SetParamsByID != nil {
 		result.SetParamsByID = make(map[string]map[string]any, len(f.SetParamsByID))
 		for k, v := range f.SetParamsByID {
-			if v != nil {
-				copied := make(map[string]any, len(v))
-				for kk, vv := range v {
-					copied[kk] = vv
-				}
-				result.SetParamsByID[k] = copied
-			}
+			if v == nil {
+				result.SetParamsByID[k] = nil
+				continue
+			}
+			copied := make(map[string]any, len(v))
+			for kk, vv := range v {
+				copied[kk] = vv
+			}
+			result.SetParamsByID[k] = copied
 		}
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if f.SetParamsByID != nil {
result.SetParamsByID = make(map[string]map[string]any, len(f.SetParamsByID))
for k, v := range f.SetParamsByID {
if v != nil {
copied := make(map[string]any, len(v))
for kk, vv := range v {
copied[kk] = vv
}
result.SetParamsByID[k] = copied
}
}
if f.SetParamsByID != nil {
result.SetParamsByID = make(map[string]map[string]any, len(f.SetParamsByID))
for k, v := range f.SetParamsByID {
if v == nil {
result.SetParamsByID[k] = nil
continue
}
copied := make(map[string]any, len(v))
for kk, vv := range v {
copied[kk] = vv
}
result.SetParamsByID[k] = copied
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@proxy/config/template_fill.go` around lines 289 - 299, The copy operation for
SetParamsByID currently omits keys whose value is nil, changing semantics;
update the loop in the function that populates result.SetParamsByID so that when
f.SetParamsByID[k] (v) is nil you still assign result.SetParamsByID[k] = nil
instead of skipping it, and when v != nil continue to deep-copy the inner map as
done now (references: SetParamsByID, result.SetParamsByID).

@mostlygeek
Copy link
Owner

Thanks for the work on this. I won't be accepting it into llama-swap. Please feel free to maintain it in your own fork.

As mentioned in my comment in #549, I recommend using the new SetParamsByID filter for model variants. It's not quite as powerful as this design but it is balance of simple and functional that I want.

@kamjin3086
Copy link
Author

Thanks for the work on this. I won't be accepting it into llama-swap. Please feel free to maintain it in your own fork.

As mentioned in my comment in #549, I recommend using the new SetParamsByID filter for model variants. It's not quite as powerful as this design but it is balance of simple and functional that I want.

No problem. My situation is indeed quite specific, and I understand your decision.

@kamjin3086 kamjin3086 closed this Mar 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants