Skip to content

Add Google engine safety settings and default thinking budget config#559

Open
ermanhavuc wants to merge 5 commits intoKochava-Studios:mainfrom
ermanhavuc:clean/google-engine-settings
Open

Add Google engine safety settings and default thinking budget config#559
ermanhavuc wants to merge 5 commits intoKochava-Studios:mainfrom
ermanhavuc:clean/google-engine-settings

Conversation

@ermanhavuc
Copy link
Copy Markdown

@ermanhavuc ermanhavuc commented Mar 22, 2026

Summary

  • add Google engine safety settings and default thinking budget configuration
  • surface the new Google settings in the renderer with clearer thinking budget help text
  • add unit coverage for Google engine config generation and settings persistence

Verification

  • npm run lint
  • npm run test:ai -- tests/unit/renderer/services/llms/google.test.ts tests/unit/renderer/screens/settings_models.test.ts

Summary by CodeRabbit

Release Notes

  • New Features

    • Added thinking budget configuration for Google engine to set automatic, disabled, or custom token limits for AI reasoning
    • Introduced safety filter settings for Google engine, allowing customization of content moderation across multiple harmful content categories
  • Tests

    • Added comprehensive test coverage for new Google engine configuration features

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 22, 2026

📝 Walkthrough

Walkthrough

This change adds configuration support for Google LLM engine features including thinking budget defaults and safety filter settings. It introduces localized UI strings, a new config type definition, UI controls in the settings component, service-layer logic to apply these settings, and comprehensive test coverage.

Changes

Cohort / File(s) Summary
Localization & Type Definitions
locales/en.json, src/types/config.ts
Added user-facing strings for Google thinking budget and safety settings configuration. Added GoogleEngineConfig type extending EngineConfig with optional safetySettings and defaultThinkingBudget fields.
Service Layer
src/renderer/services/llms/google.ts
Updated getGenerationConfig to apply defaultThinkingBudget when no explicit override is provided and to conditionally apply safety settings across four harm categories (harassment, hate speech, sexual content, dangerous content) when configured.
Settings UI
src/renderer/settings/SettingsGoogle.vue
Added form controls for safetySettings (dropdown with BLOCK options) and defaultThinkingBudget (numeric input). Integrated typed loading/saving via GoogleEngineConfig into store.
Test Coverage
tests/unit/renderer/screens/settings_models.test.ts, tests/unit/renderer/services/llms/google.test.ts
Updated settings model test for new form fields. Added new test suite for GoogleEngine verifying thinking budget defaults, safety settings application, and proper fallback behavior.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Poem

🐰 A thinking budget finds its home,
Safety settings learn to roam,
Google's mind grows ever wise,
With defaults that harmonize—
Configurations bloom and grow! 🌿✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Description check ❓ Inconclusive The pull request description provides a clear summary and verification steps, but lacks structured alignment with the template's required sections including explicit related issues, detailed testing methodology with checkboxes, and contributor checklist completion. Complete the PR description by filling all template sections: add related issue reference (Fixes #), mark testing checkboxes (Manual/Unit/E2E), and complete the contributor checklist with CLA and branch/lint/test confirmations.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: adding Google engine safety settings and default thinking budget configuration, which aligns with the primary modifications across localization, service logic, UI components, and types.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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
Copy Markdown

@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: 2

🧹 Nitpick comments (2)
tests/unit/renderer/services/llms/google.test.ts (1)

17-24: Use LlmMock instead of patching Google.prototype here.

This suite reaches into a protected method via as any and replaces Google.prototype.getGenerationConfig directly, which makes the test depend on multi-llm-ts internals instead of the repo's standard provider harness. Please switch this to LlmMock so the test follows the unit-test convention used elsewhere.

As per coding guidelines: mock LLM providers via 'LlmMock' class.

Also applies to: 31-33

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

In `@tests/unit/renderer/services/llms/google.test.ts` around lines 17 - 24, The
test currently reaches into internals by calling (engine as
any).getGenerationConfig and by patching Google.prototype; replace that approach
with the project's LlmMock harness: construct a GoogleEngine (via createEngine)
but register or inject an LlmMock instance that implements the Google provider
behavior, then call the engine's public method that delegates to the provider so
you can assert the generation config; specifically stop mutating
Google.prototype.getGenerationConfig and instead use LlmMock to simulate
responses for GoogleEngine.getGenerationConfig (and the similar spots at lines
31-33), ensuring the tests exercise the engine through the standard LlmMock
provider interface.
src/types/config.ts (1)

53-56: Expose the google key through the central engines config.

GoogleEngineConfig is defined here, but Configuration.engines still only exposes the generic engine union. That leaves store.config.engines.google untyped for these new fields, which is why the renderer, service, and tests all have to drop to casts. Wiring google into the central config contract would make these settings type-safe end to end.

Based on learnings: Centralize configuration in src/types/config.ts with backwards compatibility, organizing engines, plugins, and other settings as Record types.

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

In `@src/types/config.ts` around lines 53 - 56, Update the central
Configuration.engines type to explicitly include a google?: GoogleEngineConfig
field while preserving the existing general engine shape (e.g., keep the
EngineConfig union/index-signature for other engines) so
store.config.engines.google is fully typed; locate the Configuration
interface/type and add the google key typed to GoogleEngineConfig and ensure
backward compatibility by retaining the broad Record<string, EngineConfig> or
union for non-google engines.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/renderer/services/llms/google.ts`:
- Around line 25-33: The code currently casts googleConfig.safetySettings to
HarmBlockThreshold without validation; update the block that reads
googleConfig.safetySettings (in the file where googleConfig is cast) to
whitelist only the valid HarmBlockThreshold values
(HARM_BLOCK_THRESHOLD_UNSPECIFIED, BLOCK_LOW_AND_ABOVE, BLOCK_MEDIUM_AND_ABOVE,
BLOCK_ONLY_HIGH, BLOCK_NONE, OFF), and if the persisted value is not one of
those, skip setting config.safetySettings so Google defaults apply (optionally
emit a debug/warn via the same logger). Ensure you reference the
googleConfig.safetySettings value, validate against the HarmBlockThreshold set,
and only populate config.safetySettings when the value is valid.

In `@src/renderer/settings/SettingsGoogle.vue`:
- Around line 105-106: The defaultThinkingBudget ref should accept the empty
string Vue emits from a cleared number input and be normalized to undefined
before persisting: change the type of defaultThinkingBudget (currently
ref<number>(null)) to allow number|string (and/or null) so it can hold '' from
the input, and in the save path (the settings save handler that builds
googleConfig and where you currently use the nullish coalescing on
defaultThinkingBudget) convert an empty string ('') to undefined so
googleConfig.defaultThinkingBudget is omitted rather than set to ''. Ensure any
downstream check in google.ts that uses typeof
googleConfig.defaultThinkingBudget !== 'undefined' will therefore treat cleared
values as not configured.

---

Nitpick comments:
In `@src/types/config.ts`:
- Around line 53-56: Update the central Configuration.engines type to explicitly
include a google?: GoogleEngineConfig field while preserving the existing
general engine shape (e.g., keep the EngineConfig union/index-signature for
other engines) so store.config.engines.google is fully typed; locate the
Configuration interface/type and add the google key typed to GoogleEngineConfig
and ensure backward compatibility by retaining the broad Record<string,
EngineConfig> or union for non-google engines.

In `@tests/unit/renderer/services/llms/google.test.ts`:
- Around line 17-24: The test currently reaches into internals by calling
(engine as any).getGenerationConfig and by patching Google.prototype; replace
that approach with the project's LlmMock harness: construct a GoogleEngine (via
createEngine) but register or inject an LlmMock instance that implements the
Google provider behavior, then call the engine's public method that delegates to
the provider so you can assert the generation config; specifically stop mutating
Google.prototype.getGenerationConfig and instead use LlmMock to simulate
responses for GoogleEngine.getGenerationConfig (and the similar spots at lines
31-33), ensuring the tests exercise the engine through the standard LlmMock
provider interface.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b1643ad7-3f0a-4a58-ae6d-a959fb89cff6

📥 Commits

Reviewing files that changed from the base of the PR and between 0fa4302 and 2f3dd79.

📒 Files selected for processing (6)
  • locales/en.json
  • src/renderer/services/llms/google.ts
  • src/renderer/settings/SettingsGoogle.vue
  • src/types/config.ts
  • tests/unit/renderer/screens/settings_models.test.ts
  • tests/unit/renderer/services/llms/google.test.ts

Comment on lines +25 to +33
const googleConfig = this.config as unknown as GoogleEngineConfig
if (googleConfig.safetySettings) {
const threshold = googleConfig.safetySettings as HarmBlockThreshold
config.safetySettings = [
{ category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold },
{ category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold },
{ category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, threshold },
{ category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold },
]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In @google/genai, what are the valid HarmBlockThresholdvalues accepted byGenerateContentConfig.safetySettings?

💡 Result:

The valid HarmBlockThreshold values accepted by GenerateContentConfig.safetySettings in @google/genai are: HARM_BLOCK_THRESHOLD_UNSPECIFIED, BLOCK_LOW_AND_ABOVE, BLOCK_MEDIUM_AND_ABOVE, BLOCK_ONLY_HIGH, BLOCK_NONE, and OFF. These are string values corresponding to the enum defined in the Gemini API protobufs, used across Google GenAI SDKs including JavaScript (@google/genai). They control the probability threshold at which content is blocked for each harm category in safety settings. - HARM_BLOCK_THRESHOLD_UNSPECIFIED: Uses default threshold (high confidence from multiple official sources). - BLOCK_LOW_AND_ABOVE: Blocks low, medium, high probability harm. - BLOCK_MEDIUM_AND_ABOVE: Blocks medium, high. - BLOCK_ONLY_HIGH: Blocks only high. - BLOCK_NONE: Blocks none (always allow). - OFF: Turns off safety filter. Example usage in @google/genai (inferred from API compatibility and JS examples): const safetySettings = [ { category: "HARM_CATEGORY_HATE_SPEECH", threshold: "BLOCK_LOW_AND_ABOVE" } ]; Official Gemini API docs confirm these values for safetySettings.threshold, and @google/genai wraps the same REST API.

Citations:


🏁 Script executed:

fd -t f "google.ts" src/renderer/services/llms

Repository: nbonamy/witsy

Length of output: 93


🏁 Script executed:

fd -t f "SettingsGoogle.vue" src/renderer

Repository: nbonamy/witsy

Length of output: 97


🏁 Script executed:

head -60 src/renderer/services/llms/google.ts

Repository: nbonamy/witsy

Length of output: 1587


🏁 Script executed:

cat src/renderer/settings/SettingsGoogle.vue | head -100

Repository: nbonamy/witsy

Length of output: 5112


🏁 Script executed:

grep -A 20 -B 5 "safetySettings" src/renderer/settings/SettingsGoogle.vue

Repository: nbonamy/witsy

Length of output: 4567


🏁 Script executed:

grep -r "HARM_BLOCK_THRESHOLD_UNSPECIFIED" src/

Repository: nbonamy/witsy

Length of output: 39


🏁 Script executed:

cat -n src/renderer/services/llms/google.ts | sed -n '20,40p'

Repository: nbonamy/witsy

Length of output: 958


Validate safetySettings before using it with the Google API.

Line 27 blindly casts safetySettings from persisted config to HarmBlockThreshold without validation. A hand-edited config file with an invalid threshold value will cause API errors. The UI restricts options to valid values, but the persisted setting can be modified outside the application. Add validation to whitelist known values and skip safety settings if an invalid value is encountered so the provider defaults are used instead.

Valid HarmBlockThreshold values are: HARM_BLOCK_THRESHOLD_UNSPECIFIED, BLOCK_LOW_AND_ABOVE, BLOCK_MEDIUM_AND_ABOVE, BLOCK_ONLY_HIGH, BLOCK_NONE, and OFF.

Suggested guard
 import { GenerateContentConfig, HarmBlockThreshold, HarmCategory } from '@google/genai'
 import { ChatModel, Google, LlmCompletionOpts } from 'multi-llm-ts'
 import { GoogleEngineConfig } from 'types/config'
 
+const GOOGLE_SAFETY_THRESHOLDS = new Set([
+  'HARM_BLOCK_THRESHOLD_UNSPECIFIED',
+  'BLOCK_LOW_AND_ABOVE',
+  'BLOCK_MEDIUM_AND_ABOVE',
+  'BLOCK_ONLY_HIGH',
+  'BLOCK_NONE',
+  'OFF',
+])
+
 export default class GoogleEngine extends Google {
-      if (googleConfig.safetySettings) {
+      if (googleConfig.safetySettings && GOOGLE_SAFETY_THRESHOLDS.has(googleConfig.safetySettings)) {
         const threshold = googleConfig.safetySettings as HarmBlockThreshold
         config.safetySettings = [
📝 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
const googleConfig = this.config as unknown as GoogleEngineConfig
if (googleConfig.safetySettings) {
const threshold = googleConfig.safetySettings as HarmBlockThreshold
config.safetySettings = [
{ category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold },
{ category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold },
{ category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, threshold },
{ category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold },
]
import { GenerateContentConfig, HarmBlockThreshold, HarmCategory } from '@google/genai'
import { ChatModel, Google, LlmCompletionOpts } from 'multi-llm-ts'
import { GoogleEngineConfig } from 'types/config'
const GOOGLE_SAFETY_THRESHOLDS = new Set([
'HARM_BLOCK_THRESHOLD_UNSPECIFIED',
'BLOCK_LOW_AND_ABOVE',
'BLOCK_MEDIUM_AND_ABOVE',
'BLOCK_ONLY_HIGH',
'BLOCK_NONE',
'OFF',
])
export default class GoogleEngine extends Google {
// ... other code ...
const googleConfig = this.config as unknown as GoogleEngineConfig
if (googleConfig.safetySettings && GOOGLE_SAFETY_THRESHOLDS.has(googleConfig.safetySettings)) {
const threshold = googleConfig.safetySettings as HarmBlockThreshold
config.safetySettings = [
{ category: HarmCategory.HARM_CATEGORY_HARASSMENT, threshold },
{ category: HarmCategory.HARM_CATEGORY_HATE_SPEECH, threshold },
{ category: HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT, threshold },
{ category: HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT, threshold },
]
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/services/llms/google.ts` around lines 25 - 33, The code
currently casts googleConfig.safetySettings to HarmBlockThreshold without
validation; update the block that reads googleConfig.safetySettings (in the file
where googleConfig is cast) to whitelist only the valid HarmBlockThreshold
values (HARM_BLOCK_THRESHOLD_UNSPECIFIED, BLOCK_LOW_AND_ABOVE,
BLOCK_MEDIUM_AND_ABOVE, BLOCK_ONLY_HIGH, BLOCK_NONE, OFF), and if the persisted
value is not one of those, skip setting config.safetySettings so Google defaults
apply (optionally emit a debug/warn via the same logger). Ensure you reference
the googleConfig.safetySettings value, validate against the HarmBlockThreshold
set, and only populate config.safetySettings when the value is valid.

Comment on lines +105 to +106
const safetySettings = ref<string>('')
const defaultThinkingBudget = ref<number>(null)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In Vue 3, what value does v-model.numberproduce for an when the input is cleared?

💡 Result:

In Vue 3, v-model.number on an produces an empty string ("") when the input is cleared.

Citations:


🏁 Script executed:

# Find and read SettingsGoogle.vue to check lines 105-106 and 169-170
find . -name "SettingsGoogle.vue" -type f

Repository: nbonamy/witsy

Length of output: 99


🏁 Script executed:

# Find google.ts service file
find . -name "google.ts" -path "*/renderer/services/llms/*" -type f

Repository: nbonamy/witsy

Length of output: 95


🏁 Script executed:

# Read SettingsGoogle.vue to check lines 105-106 and 169-170
wc -l ./src/renderer/settings/SettingsGoogle.vue

Repository: nbonamy/witsy

Length of output: 103


🏁 Script executed:

# Read the relevant sections of SettingsGoogle.vue
sed -n '100,180p' ./src/renderer/settings/SettingsGoogle.vue

Repository: nbonamy/witsy

Length of output: 2952


🏁 Script executed:

# Read google.ts to understand how defaultThinkingBudget is handled
cat -n ./src/renderer/services/llms/google.ts | head -100

Repository: nbonamy/witsy

Length of output: 1874


🏁 Script executed:

# Check the template section around lines 57-77 to see the input for defaultThinkingBudget
sed -n '1,100p' ./src/renderer/settings/SettingsGoogle.vue

Repository: nbonamy/witsy

Length of output: 5112


Normalize a cleared thinking budget before saving it.

With Vue's .number modifier, clearing the <input type="number"> yields '', not null. Line 170 uses ??, which doesn't catch empty strings, so '' ?? undefined evaluates to '' and persists verbatim. In google.ts, the check typeof googleConfig.defaultThinkingBudget !== 'undefined' treats this empty string as configured and forwards it to Google instead of falling back to automatic mode.

Fix the type annotation to allow the empty string that Vue produces, and normalize it to undefined during save:

Suggested fix
-const defaultThinkingBudget = ref<number>(null)
+const defaultThinkingBudget = ref<number | '' | null>(null)
-  ;(store.config.engines.google as GoogleEngineConfig).defaultThinkingBudget = defaultThinkingBudget.value ?? undefined
+  ;(store.config.engines.google as GoogleEngineConfig).defaultThinkingBudget =
+    defaultThinkingBudget.value === '' || defaultThinkingBudget.value === null
+      ? undefined
+      : defaultThinkingBudget.value
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/settings/SettingsGoogle.vue` around lines 105 - 106, The
defaultThinkingBudget ref should accept the empty string Vue emits from a
cleared number input and be normalized to undefined before persisting: change
the type of defaultThinkingBudget (currently ref<number>(null)) to allow
number|string (and/or null) so it can hold '' from the input, and in the save
path (the settings save handler that builds googleConfig and where you currently
use the nullish coalescing on defaultThinkingBudget) convert an empty string
('') to undefined so googleConfig.defaultThinkingBudget is omitted rather than
set to ''. Ensure any downstream check in google.ts that uses typeof
googleConfig.defaultThinkingBudget !== 'undefined' will therefore treat cleared
values as not configured.

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.

1 participant