Skip to content

fix(core): deep-merge user model config over defaults#28364

Open
AriaZhao-coder wants to merge 1 commit into
google-gemini:mainfrom
AriaZhao-coder:fix/deep-merge-model-configs
Open

fix(core): deep-merge user model config over defaults#28364
AriaZhao-coder wants to merge 1 commit into
google-gemini:mainfrom
AriaZhao-coder:fix/deep-merge-model-configs

Conversation

@AriaZhao-coder

Copy link
Copy Markdown

Problem

The Config constructor merges the user's modelConfigServiceConfig into DEFAULT_MODEL_CONFIGS using shallow spreads plus a ?? DEFAULT fallback for aliases/overrides. Because DEFAULT_MODEL_CONFIGS is deeply nested (aliasesmodelConfiggenerateContentConfig → …), any partial user override obliterates the default model aliases.

This was a self-admitted // HACK in config.ts, guarded by TODO(12593) — whose tracking issue is now closed as stale.

Fixes #28264.

Fix

Replace the hack with a proper recursive merge by reusing the module's existing genericDeepMerge, exposed via a new typed ModelConfigService.mergeConfigs(base, override):

  • Object maps (aliases, modelDefinitions, modelIdResolutions, classifierIdResolutions, modelChains) are merged recursively, so partial user overrides augment the defaults instead of dropping them.
  • Arrays (overrides, customOverrides) are replaced wholesale, so a user can still fully override them (matching the documented genericDeepMerge behavior).
  • A missing user config returns the defaults unchanged.

The config.ts hydration block shrinks from ~40 lines of manual re-stitching to a single mergeConfigs call, and the stale TODO(12593) is removed.

Testing

  • New unit tests for ModelConfigService.mergeConfigs (partial merge preserves defaults, nested deep-merge, key-add, array replace/keep, undefined override, no input mutation).
  • Updated the existing config-hydration tests to assert the fixed (merge, not obliterate) behavior.
  • Lint and tsc clean for the changed files.

…28264)

The Config constructor merged the user's `modelConfigServiceConfig` into
DEFAULT_MODEL_CONFIGS with shallow spreads plus a `?? DEFAULT` fallback
for `aliases`/`overrides`. Because DEFAULT_MODEL_CONFIGS is deeply nested
(aliases -> modelConfig -> generateContentConfig -> ...), any partial
user override obliterated the default aliases — a self-admitted HACK
guarded by TODO(12593), whose tracking issue is now closed as stale.

Replace the hack with a proper recursive merge by reusing the existing
`genericDeepMerge` via a new typed `ModelConfigService.mergeConfigs`.
Object maps (aliases, modelDefinitions, ...) merge recursively so partial
user overrides augment the defaults; arrays (overrides) are replaced
wholesale so users can still fully override them.

Adds unit tests for mergeConfigs and updates the config hydration tests
to assert the fixed (merge, not obliterate) behavior.
@AriaZhao-coder AriaZhao-coder requested a review from a team as a code owner July 12, 2026 10:19
@github-actions github-actions Bot added the size/m A medium sized PR label Jul 12, 2026
@github-actions

Copy link
Copy Markdown

📊 PR Size: size/M

  • Lines changed: 218
  • Additions: +164
  • Deletions: -54
  • Files changed: 4

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a long-standing issue where partial user model configurations would inadvertently overwrite default model aliases and definitions. By implementing a proper recursive merge strategy, the system now correctly augments default settings with user-provided overrides, improving the flexibility and reliability of the configuration hydration process. This change simplifies the core configuration logic and removes technical debt associated with previous workarounds.

Highlights

  • Deep Merge Implementation: Replaced the manual shallow-merge hack in the Config constructor with a robust recursive merge using a new ModelConfigService.mergeConfigs method.
  • Preservation of Defaults: Ensured that partial user configurations now augment default model aliases and definitions instead of obliterating them.
  • Array Handling: Maintained the existing behavior where array-based configurations (like overrides) are replaced wholesale, allowing users to fully override default lists.
  • Testing: Added comprehensive unit tests for mergeConfigs to verify deep-merging, key preservation, and immutability of the base configuration.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request replaces a manual configuration merging hack with a robust ModelConfigService.mergeConfigs method that deep-merges user-provided model configurations over built-in defaults, preserving default aliases and nested structures. Comprehensive unit tests have been added to verify this behavior. The reviewer identified a critical issue where nested properties of the global DEFAULT_MODEL_CONFIGS could be mutated by reference because genericDeepMerge does not deep-clone the base object. It is recommended to use structuredClone(base) before merging to prevent accidental mutation of global defaults.

Note: Security Review did not run due to the size of the PR.

Comment on lines +627 to +637
static mergeConfigs(
base: ModelConfigServiceConfig,
override: ModelConfigServiceConfig | undefined,
): ModelConfigServiceConfig {
return ModelConfigService.genericDeepMerge(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
base as Record<string, unknown>,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
override as Record<string, unknown> | undefined,
) as ModelConfigServiceConfig;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

Issue: Potential Shared Reference Mutation of Global Defaults

Because genericDeepMerge only recursively merges keys that are present in both objects, any nested properties in base (such as default aliases) that are not overridden by override are copied directly by reference.

Since base is passed as DEFAULT_MODEL_CONFIGS (which is a module-level global constant), any subsequent mutation of the merged configuration (e.g., by the Gemini SDK or runtime overrides) will propagate back and mutate the global DEFAULT_MODEL_CONFIGS object. This can lead to extremely subtle bugs, memory leaks, or flaky tests in concurrent environments.

Solution

Deep-clone the base configuration using structuredClone before merging to completely decouple the merged result from the global defaults.

Suggested change
static mergeConfigs(
base: ModelConfigServiceConfig,
override: ModelConfigServiceConfig | undefined,
): ModelConfigServiceConfig {
return ModelConfigService.genericDeepMerge(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
base as Record<string, unknown>,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
override as Record<string, unknown> | undefined,
) as ModelConfigServiceConfig;
}
static mergeConfigs(
base: ModelConfigServiceConfig,
override: ModelConfigServiceConfig | undefined,
): ModelConfigServiceConfig {
return ModelConfigService.genericDeepMerge(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
structuredClone(base) as Record<string, unknown>,
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
override as Record<string, unknown> | undefined,
) as ModelConfigServiceConfig;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core Issues related to User Interface, OS Support, Core Functionality priority/p2 Important but can be addressed in a future release. size/m A medium sized PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Configuration Deep-Merge Failure Causes Loss of Default Model Aliases

1 participant