Skip to content

Comments

feat: Add xAI provider, upgrade Gemini SDK#494

Merged
kevin-on merged 17 commits intomainfrom
feat/add-gpt-5.1-model
Jan 14, 2026
Merged

feat: Add xAI provider, upgrade Gemini SDK#494
kevin-on merged 17 commits intomainfrom
feat/add-gpt-5.1-model

Conversation

@kevin-on
Copy link
Collaborator

@kevin-on kevin-on commented Nov 19, 2025

Summary

This PR introduces several provider-related improvements: adds xAI (Grok) as a first-class provider, upgrades the Gemini SDK from @google/generative-ai to @google/genai, and adds providerMetadata to preserve thinking/reasoning context across multi-turn conversations with tool calls.

Changes

New Provider: xAI

  • Added XaiProvider with support for Grok models (grok-4-1-fast, grok-4-1-fast-non-reasoning)
  • Added XAI_PRICES to the price calculator

Deprecated Providers: Groq & Morph

  • Removed GroqProvider and MorphProvider as dedicated providers
  • Migration automatically converts existing groq/morph configurations to openai-compatible type with appropriate base URLs

Gemini SDK Upgrade

  • Migrated from @google/generative-ai (v0.24.0) to @google/genai (v1.35.0)
  • Added thinking configuration support for Gemini models:
    • thinking_level (minimal/low/medium/high) for Gemini 3 models
    • thinking_budget for Gemini 2.5 models
    • include_thoughts to return thought summaries

Provider Metadata for Thinking Context

  • Added providerMetadata field to chat messages and LLM request/response types
  • Gemini: Stores thoughtSignature to maintain thinking continuity across tool call iterations
  • DeepSeek: Stores reasoningContent to pass back reasoning in assistant messages during tool call loops

Model Updates

  • Added: gpt-5.2, gpt-4.1-mini, claude-opus-4.5, gemini-3-pro-preview, gemini-3-flash-preview, grok-4-1-fast, grok-4-1-fast-non-reasoning
  • Removed from defaults: Various older models (gpt-4o, gemini-2.x series, morph-v0, perplexity sonar variants)

Other Improvements

  • Made OpenAI predicted outputs conditional (only for supported gpt-4o/gpt-4.1 models)
  • Fixed type safety in migration files by using local frozen types instead of importing current types

Checklist before requesting a review

  • I have reviewed the guidelines for contributing to this repository.
  • I have performed a self-review of my code
  • I have performed a code linting check and type check (by running npm run lint:check and npm run type:check)
  • I have run the test suite (by running npm run test)
  • I have tested the functionality manually

Summary by CodeRabbit

  • New Features

    • Defaults updated: GPT‑5.2 and Claude Opus 4.5 recommended; Gemini preview names updated; per-model "thinking" controls for Gemini and reasoning content propagation added.
  • Migrations

    • Settings schema advanced to v14 with migrations that preserve custom models, merge new defaults, and convert legacy providers (v12→v13→v14).
  • Improvements

    • Prediction payloads sent only for compatible models; assistant messages and streaming chunks now include provider metadata.
  • Pricing

    • Public pricing added/expanded for GPT‑5.2/5.1, Claude Opus 4.5, Grok (XAI) and Deepseek.
  • Tests

    • New migration tests covering v12→v13 and v13→v14.

✏️ Tip: You can customize this high-level summary in your review settings.

- Added GPT-5.1 to default chat models with pricing ($1.25 input, $10 output per million tokens)
- Updated recommended models to include GPT-5.1 instead of GPT-4.1
- Introduced migration logic to transition settings from version 12 to 13
- Migration adds GPT-5.1 to user settings while preserving existing GPT-5 as custom model
- Removed GPT-5 from default models (users keep it as custom model)
- Added comprehensive tests to verify migration functionality
@coderabbitai
Copy link

coderabbitai bot commented Nov 19, 2025

📝 Walkthrough

Walkthrough

Updates default models, provider/type mappings, and pricing; adds migrations (12→13, 13→14) and tests; propagates providerMetadata and DeepSeek reasoning through request/response flows; ports Gemini to @google/genai; introduces conditional predicted-outputs support; adjusts UI and type schemas accordingly.

Changes

Cohort / File(s) Summary
Constants & Pricing
src/constants.ts
Updated DEFAULT_APPLY_MODEL_ID, RECOMMENDED_MODELS_FOR_CHAT, DEFAULT_CHAT_MODELS, provider-type remaps (e.g., groqxai), DEFAULT_PROVIDERS, and added XAI_PRICES/DEEPSEEK_PRICES; expanded OPENAI/ANTHROPIC pricing.
Settings migrations & tests
src/settings/schema/migrations/*
src/settings/schema/migrations/12_to_13.ts, .../13_to_14.ts, .../index.ts, .../12_to_13.test.ts, .../13_to_14.test.ts
Added migrateFrom12To13 and migrateFrom13To14, exported DEFAULT_CHAT_MODELS_V13/V14, bumped SETTINGS_SCHEMA_VERSION to 14, added comprehensive migration tests covering provider conversions, default merges, and preservation rules.
Chat apply / prompt / response plumbing
src/utils/chat/apply.ts, src/utils/chat/promptGenerator.ts, src/utils/chat/responseGenerator.ts, src/hooks/useChatHistory.ts
Added PREDICTED_OUTPUTS_SUPPORTED_MODELS and supportsPredictedOutputs; include predicted outputs conditionally. Propagate providerMetadata through parse/serialize/deserialize and streaming response assembly.
Provider implementations & LLM core
src/core/llm/gemini.ts, src/core/llm/xaiProvider.ts, src/core/llm/deepseekMessageAdapter.ts, src/core/llm/manager.ts, src/core/llm/morphProvider.ts
Ported Gemini to @google/genai API shapes and thinking config handling; Groq→Xai class rename and baseURL updates; DeepSeek adapter surfaces reasoning_content into providerMetadata and request messages; removed MorphProvider; manager updated to instantiate XaiProvider.
Types & schemas
src/types/*
src/types/chat-model.types.ts, src/types/chat.ts, src/types/llm/request.ts, src/types/llm/response.ts, src/types/provider.types.ts, src/types/embedding-model.types.ts
Added Request/Response provider metadata types and extended assistant messages with providerMetadata; added Gemini thinking config; remapped many providerType literal values across discriminated unions.
UI & settings components
src/components/settings/modals/AddChatModelModal.tsx, src/components/settings/sections/models/ChatModelSettings.tsx
Cast form state to ChatModel when changing provider discriminant; added Gemini thinking settings UI entry (duplicate registry entry present).
Utilities & pricing calc
src/utils/llm/price-calculator.ts
Imported XAI prices and added xai case in cost calculation using XAI_PRICES.
Package
package.json
Replaced dependency @google/generative-ai with @google/genai.

Sequence Diagram(s)

sequenceDiagram
    participant App as App / CLI
    participant Migrator as SettingsMigrator
    participant Defaults as DEFAULT_CHAT_MODELS_V13/V14
    participant Merger as getMigratedChatModels
    participant Store as SettingsStorage

    App->>Migrator: detect settings version (12 or 13)
    Migrator->>Defaults: load DEFAULT_CHAT_MODELS_V13/V14 & DEFAULT_PROVIDERS_V14
    Migrator->>Merger: merge existing chatModels/providers with defaults
    Merger-->>Migrator: merged chatModels/providers (preserve customs)
    Migrator->>Store: set version = 13/14 and persist migrated settings
    Store-->>App: return persisted migrated settings
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I hopped through defaults, models, and maps,
Carrots of metadata tucked in my laps,
I stitched migrations with nimble paws,
Kept thinking bits and streaming laws,
Tests and prices neat—I've earned my naps. 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% 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 'feat: Add xAI provider, upgrade Gemini SDK' accurately describes the main changes: addition of the xAI provider and the Gemini SDK upgrade (from @google/generative-ai to @google/genai).
Description check ✅ Passed The PR description comprehensively covers all major changes including new providers (xAI), SDK upgrades (Gemini), provider metadata additions, model updates, and improvements. Structure follows the provided template with Summary, Changes, and Checklist sections clearly documented.

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

✨ Finishing touches
  • 📝 Generate docstrings

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.

- Removed gpt-4.1, gpt-4.1-mini, and gpt-4.1-nano from default chat models
- Updated migration v12 to v13 to handle removal of GPT-4.1 models
- Added test to verify GPT-4.1 models are preserved as custom models during migration
- Users with existing GPT-4.1 models will keep them as custom models
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: 0

🧹 Nitpick comments (1)
src/settings/schema/migrations/12_to_13.ts (1)

24-205: DEFAULT_CHAT_MODELS_V13 snapshot is consistent but duplicates constants

DEFAULT_CHAT_MODELS_V13 mirrors the DEFAULT_CHAT_MODELS list in src/constants.ts (same ids/models/providerType/providerId and nested options), which is good for this migration but introduces duplication that can drift if one list is changed without the other.

If you want to reduce future maintenance risk, consider deriving one from the other (or sharing a common source) while still keeping the “v13 snapshot” semantics clear, e.g., by exporting a v13-specific array from the constants module and reusing it here.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6046d48 and bd2ec3b.

📒 Files selected for processing (3)
  • src/constants.ts (3 hunks)
  • src/settings/schema/migrations/12_to_13.test.ts (1 hunks)
  • src/settings/schema/migrations/12_to_13.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/settings/schema/migrations/12_to_13.test.ts
🧰 Additional context used
🧬 Code graph analysis (1)
src/settings/schema/migrations/12_to_13.ts (2)
src/settings/schema/setting.types.ts (1)
  • SettingMigration (93-97)
src/settings/schema/migrations/migrationUtils.ts (1)
  • getMigratedChatModels (56-90)
🔇 Additional comments (4)
src/constants.ts (3)

15-15: Chat recommendations now include gpt-5.1 – consistent with defaults

RECOMMENDED_MODELS_FOR_CHAT now references gpt-5.1, which is present in DEFAULT_CHAT_MODELS and priced in OPENAI_PRICES, so the recommendation list stays in sync with available/priceable models.


246-250: OpenAI default chat model switched to gpt-5.1 – aligns with migration intent

The OpenAI default chat model entry now uses id/model of gpt-5.1, and gpt-5 is no longer in the default list, matching the stated behavior that gpt-5 becomes a custom model while gpt-5.1 is the new default.


446-461: Pricing entry for gpt-5.1 added and matches described rates

The OPENAI_PRICES map now includes gpt-5.1 with { input: 1.25, output: 10 }, consistent with the PR description and with the existing gpt-5 entry for users who keep that as a custom model.

src/settings/schema/migrations/12_to_13.ts (1)

5-22: Migration behavior matches the described v12→v13 transition

The migration:

  • Bumps version to 13.
  • Rebuilds chatModels via getMigratedChatModels using DEFAULT_CHAT_MODELS_V13.

Given getMigratedChatModels keeps any models not in the new defaults as “custom” entries, this correctly:

  • Adds gpt-5.1 as a default.
  • Removes gpt-5 and the gpt-4.1* family from the default set while preserving any existing instances in user settings as custom models.

This lines up well with the PR description and the inline comment.

- Introduced a new utility function to check if a model supports predicted outputs.
- Updated the applyChangesToFile function to conditionally include prediction logic for supported models: gpt-4o, gpt-4o-mini, gpt-4.1, gpt-4.1-mini, and gpt-4.1-nano.
… comment

- Changed the default apply model ID to 'gpt-4.1-mini' and added a comment explaining its preference over gpt-5-mini due to performance considerations regarding predicted outputs.
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

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bd2ec3b and 83ff1a4.

📒 Files selected for processing (2)
  • src/constants.ts
  • src/utils/chat/apply.ts
🔇 Additional comments (4)
src/utils/chat/apply.ts (2)

18-30: LGTM! Predicted outputs support check is well-implemented.

The startsWith approach correctly handles versioned model IDs (e.g., gpt-4o-2024-01-01). The exclusion of GPT-5 models from this list aligns with the comment in constants.ts explaining that GPT-5 models don't support predicted outputs.


159-173: Incorrect API structure — prediction.content should be a string, not an array of objects.

The OpenAI Predicted Outputs API specifies that prediction.content must be a string (the full predicted text), but the code currently passes an array of content block objects [{ type: 'text', text: ... }]. This structure appears to conflate the message content format with the predicted outputs format and will not work with the OpenAI API. Refactor to pass currentFileContent and blockToApply concatenated as a single string value:

prediction: {
  type: 'content',
  content: currentFileContent + blockToApply,
},
⛔ Skipped due to learnings
Learnt from: kevin-on
Repo: glowingjade/obsidian-smart-composer PR: 372
File: src/core/llm/anthropic.ts:352-359
Timestamp: 2025-04-27T20:59:14.376Z
Learning: In AnthropicProvider's parseRequestMessage method, the content property for assistant messages is already guaranteed to be a string by the type system, so additional type checking with `typeof content === 'string'` is unnecessary.
src/constants.ts (2)

12-17: LGTM! Clear documentation of model selection rationale.

The comment clearly explains why gpt-4.1-mini is preferred over gpt-5-mini for apply tasks (predicted outputs support). The update to RECOMMENDED_MODELS_FOR_CHAT to include gpt-5.1 aligns with the PR objectives.


449-449: LGTM! Pricing entry added correctly.

The pricing for gpt-5.1 matches the PR description ($1.25 per million input tokens, $10 per million output tokens).

- Changed the recommended chat model from 'gpt-5.1' to 'gpt-5.2'.
- Updated the pricing for 'gpt-5.2' in the OPENAI_PRICES record.
- Incremented the SETTINGS_SCHEMA_VERSION to 14 and added migration logic for version 13 to 14.
- Removed outdated models 'gpt-5-nano', 'gpt-4o', and 'gpt-4o-mini' from default chat models.
- Added 'gpt-4.1-mini' to the default chat models and migration logic.
- Updated the test cases to reflect changes in model IDs and ensure proper migration from version 13 to 14.
- Adjusted pricing for 'gpt-5.2' in the OPENAI_PRICES record.
…ion tests

- Updated the default chat model from 'claude-opus-4.1' to 'claude-opus-4.5' in constants and migration logic.
- Added tests to ensure 'claude-opus-4.5' is correctly added as a new default model during migration from version 13 to 14.
- Preserved 'claude-opus-4.1' as a custom model in migration tests.
- Added 'gemini-3-pro-preview' and 'gemini-3-flash-preview' as new default chat models in the migration logic.
- Updated the constants to reflect the new model IDs and removed outdated models.
- Enhanced migration tests to ensure the new models are correctly added and existing models are preserved during migration.
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

🤖 Fix all issues with AI agents
In @src/constants.ts:
- Around line 247-252: DEFAULT_CHAT_MODELS is missing an entry for gpt-5.1; add
an object to DEFAULT_CHAT_MODELS with providerType: 'openai', providerId:
PROVIDER_TYPES_INFO.openai.defaultProviderId, id: 'gpt-5.1', and model:
'gpt-5.1' so the default models list matches the pricing entry for gpt-5.1;
ensure the new entry follows the same shape and ordering as the existing gpt-5.2
entry and run tests/linters.
- Around line 229-234: The Anthropic entry uses an invalid model identifier: the
object with providerType 'anthropic' and id 'claude-opus-4.5' should have its
model field replaced with a valid Claude Opus identifier; update the model
property (currently 'claude-opus-4-5') to one of the supported values such as
'claude-opus-4-20250514', 'claude-opus-4-1-20250805', or an alias like
'claude-opus-4-0'/'claude-opus-4-1' so the entry
(providerType/providerId/id/model) matches Anthropic’s documented model names.

In @src/settings/schema/migrations/13_to_14.ts:
- Around line 134-169: Update the incorrect Perplexity model identifiers so the
`model` field matches each provider entry's `id`: set the entry with id
'sonar-pro' to model 'sonar-pro', set 'sonar-reasoning-pro' to model
'sonar-reasoning-pro', and set 'sonar-reasoning' to model 'sonar-reasoning'
(leave 'sonar-deep-research' as-is). Locate these objects in the migration list
where providerId is 'perplexity' and replace the wrong 'sonar' values with the
matching model IDs to ensure API requests use the correct model identifiers.
🧹 Nitpick comments (1)
src/settings/schema/migrations/13_to_14.test.ts (1)

1-291: Good test coverage for core migration scenarios.

The tests comprehensively cover version increment, model merging, and custom model preservation.

Consider adding tests for models with nested configuration objects (e.g., o4-mini with reasoning, Perplexity models with web_search_options) to verify the shallow merge behavior documented in getMigratedChatModels. Per the FIXME in migrationUtils.ts (lines 73-76), nested objects are overwritten rather than merged, which could affect users who customized these settings.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 83ff1a4 and 11e5512.

📒 Files selected for processing (4)
  • src/constants.ts
  • src/settings/schema/migrations/13_to_14.test.ts
  • src/settings/schema/migrations/13_to_14.ts
  • src/settings/schema/migrations/index.ts
🧰 Additional context used
🧬 Code graph analysis (3)
src/settings/schema/migrations/13_to_14.test.ts (1)
src/settings/schema/migrations/13_to_14.ts (2)
  • migrateFrom13To14 (26-33)
  • DEFAULT_CHAT_MODELS_V14 (54-176)
src/settings/schema/migrations/13_to_14.ts (2)
src/settings/schema/setting.types.ts (1)
  • SettingMigration (93-97)
src/settings/schema/migrations/migrationUtils.ts (1)
  • getMigratedChatModels (56-90)
src/settings/schema/migrations/index.ts (2)
src/settings/schema/migrations/12_to_13.ts (1)
  • migrateFrom12To13 (15-22)
src/settings/schema/migrations/13_to_14.ts (1)
  • migrateFrom13To14 (26-33)
🔇 Additional comments (7)
src/settings/schema/migrations/index.ts (1)

6-7: LGTM!

The migration registry is correctly updated:

  • Imports follow existing alphabetical ordering convention
  • Version bump to 14 is consistent with the two new migrations
  • Migration chain (12→13→14) is properly linked with no gaps

Also applies to: 18-18, 81-90

src/settings/schema/migrations/13_to_14.ts (2)

26-33: LGTM!

The migration function follows the established pattern correctly, using getMigratedChatModels for consistent model merging behavior.


5-25: Clear migration documentation.

The comment effectively documents the intent of this migration, making it easy for future maintainers to understand what changed between schema versions.

src/constants.ts (4)

12-17: Clarify: PR description says GPT-5.1, but code adds GPT-5.2.

The PR objectives state "Adds GPT-5.1 to default chat models" and "Updates recommended models to include GPT-5.1 instead of GPT-4.1", but the code adds gpt-5.2 to RECOMMENDED_MODELS_FOR_CHAT. Please confirm whether this is intentional or if the PR description needs updating.

The rationale comment for DEFAULT_APPLY_MODEL_ID is helpful and clear.


408-411: LGTM!

Pricing entries for the new models are added correctly. Note that gpt-5.1 and gpt-5 share identical pricing ($1.25/$10), which appears intentional based on the PR description.


426-428: Claude Opus 4.5 pricing confirmed accurate.

The pricing for claude-opus-4-5 ($5/$25 per million tokens) matches Anthropic's current published rates as of January 2026. The substantial difference from claude-opus-4-1 ($15/$75) is correct.


276-286: Verify gemini-3-flash-preview model identifier.

gemini-3-pro-preview appears to be a valid official Gemini API identifier, but gemini-3-flash-preview is not listed in the current official Gemini model catalog. Confirm this identifier matches an actual available Gemini API model name before deployment.

- Replaced the deprecated '@google/generative-ai' package with '@google/genai' to support the latest models and features.
- Updated the GeminiProvider class to utilize the new API structure and methods from '@google/genai'.
- Enhanced chat message serialization and deserialization to include provider metadata.
- Added support for thought signatures in function calls to improve response handling.
- Updated related types and utility functions to accommodate changes in the new API.
- Added comprehensive settings for the Gemini model, including thinking level, budget, and thought summaries.
- Implemented a new ThinkingConfig structure to manage thinking settings based on model type.
- Updated the GeminiProvider to extract and include reasoning in responses.
- Enhanced the AddChatModelModal to support the new provider settings for Gemini.
- Introduced the xAI provider with necessary configurations and API integration.
- Added new chat models 'grok-4-1-fast' and 'grok-4-1-fast-non-reasoning' to the default settings.
- Updated migration logic to include the xAI provider and new models.
- Enhanced pricing calculations to support xAI model costs.
- Updated schemas to accommodate the new xAI provider type.
- Updated DeepSeekMessageAdapter to include reasoning content in both non-streaming and streaming responses.
- Introduced providerMetadata to store reasoning content for assistant messages during tool call iterations.
- Refactored request and response types to accommodate new RequestProviderMetadata structure for DeepSeek integration.
- Introduced the xAI provider and updated associated configurations.
- Removed the Groq and Morph providers, converting their models to openai-compatible.
- Updated migration logic to reflect these changes and ensure proper handling of existing models.
- Enhanced schemas to accommodate the new provider types and updated chat model definitions.
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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/core/llm/deepseekMessageAdapter.ts (1)

54-82: Potential data loss for incrementally streamed reasoning content.

In streaming mode, reasoningContent is extracted per chunk and stored in providerMetadata. However, responseGenerator.ts uses a "keep first" strategy for providerMetadata, meaning only the first chunk's reasoningContent is preserved.

If DeepSeek streams reasoning_content across multiple chunks, the providerMetadata.deepseek.reasoningContent field will be incomplete when sent back during tool call iterations, even though the accumulated reasoning field (for display) would be complete.

Consider one of these approaches:

  1. Accumulate reasoningContent in providerMetadata similar to how reasoning is accumulated
  2. Reconstruct providerMetadata.deepseek.reasoningContent from the final accumulated reasoning before making tool call requests
  3. Confirm that DeepSeek's API sends reasoning_content in a single chunk
src/utils/chat/responseGenerator.ts (1)

282-302: Fix providerMetadata accumulation for DeepSeek's incrementally streamed reasoning_content.

DeepSeek sends reasoning_content incrementally across multiple streaming chunks, not once. The current implementation keeps only the first providerMetadata received (line 301-302), which discards subsequent reasoning_content chunks. While the reasoning field properly accumulates across chunks (lines 290-292), providerMetadata.deepseek.reasoningContent is lost.

Update the logic to accumulate providerMetadata similarly to how reasoning is concatenated, or ensure providerMetadata.deepseek.reasoningContent is merged across chunks rather than discarded.

src/settings/schema/migrations/1_to_2.ts (1)

691-702: Fix ESLint pipeline failure: wrap ollamaApplyProviderId with String().

The pipeline is failing because ollamaApplyProviderId has type any (derived from the V2LLMProvider type alias), which violates @typescript-eslint/restrict-template-expressions.

🔧 Proposed fix
     let ollamaApplyModelId
     if (existingSameChatModelForApplyModel) {
       ollamaApplyModelId = existingSameChatModelForApplyModel.id
     } else {
-      ollamaApplyModelId = `${ollamaApplyProviderId}/${data.ollamaApplyModel.model}`
+      ollamaApplyModelId = `${String(ollamaApplyProviderId)}/${data.ollamaApplyModel.model}`
       chatModels.push({
🤖 Fix all issues with AI agents
In `@package.json`:
- Line 50: Update the invalid dependency version for "@google/genai" in
package.json from "^1.35.0" to the published "^1.34.0"; after updating the
version string, run your package manager (npm/yarn/pnpm) to reinstall and
regenerate the lockfile so the dependency graph is consistent and CI installs
succeed.

In `@src/constants.ts`:
- Around line 288-298: Update the incorrect Grok model identifier in the
constants list: change the object with id 'grok-4-1-fast' (where providerType is
'xai' and providerId uses PROVIDER_TYPES_INFO.xai.defaultProviderId) so its
model and id use 'grok-4-1-fast-reasoning' instead of 'grok-4-1-fast' to match
xAI's API; ensure the other entry 'grok-4-1-fast-non-reasoning' remains
unchanged.
♻️ Duplicate comments (2)
src/constants.ts (2)

220-221: Verify claude-opus-4-5 model identifier with Anthropic API.

This was flagged in a previous review. The model identifier claude-opus-4-5 should be verified against Anthropic's current API documentation to ensure it's a valid model name.

Anthropic claude-opus-4-5 model identifier API 2025

238-239: PR description inconsistency: GPT-5.1 not in default models.

The PR description states "Adds GPT-5.1 to default chat models" but only gpt-5.2 is present here. The pricing for gpt-5.1 exists at line 359, but there's no corresponding entry in DEFAULT_CHAT_MODELS. Either add the gpt-5.1 model entry or update the PR description.

🧹 Nitpick comments (7)
src/settings/schema/migrations/2_to_3.ts (1)

48-91: Shallow copy causes mutation of original data.

The spread { ...data } on line 49 creates a shallow copy, so newData.providers and newData.chatModels still reference the same arrays (and objects) as data. The in-place mutations on lines 63 and 82 modify the original input, which could cause unexpected side effects if callers rely on data being unchanged.

♻️ Suggested fix: deep-copy the arrays before mutation
 export const migrateFrom2To3: SettingMigration['migrate'] = (data) => {
   const newData = { ...data }
   newData.version = 3
 
   // Handle providers migration
   if ('providers' in newData && Array.isArray(newData.providers)) {
+    // Deep copy to avoid mutating original data
+    newData.providers = newData.providers.map((p) => ({ ...p }))
     const existingProvidersMap = new Map(
       newData.providers.map((provider) => [provider.id, provider]),
     )
 
     // Add new providers, overriding provider type if ID exists
     for (const newProvider of NEW_DEFAULT_PROVIDERS) {
       const existingProvider = existingProvidersMap.get(newProvider.id)
       if (existingProvider) {
         // Override the provider type while keeping other settings
         existingProvider.type = newProvider.type
       } else {
         // Add new provider
         newData.providers.push({ ...newProvider })
       }
     }
   }
 
   // Handle chat models migration
   if ('chatModels' in newData && Array.isArray(newData.chatModels)) {
+    // Deep copy to avoid mutating original data
+    newData.chatModels = newData.chatModels.map((m) => ({ ...m }))
     const existingModelsMap = new Map(
       newData.chatModels.map((model) => [model.id, model]),
     )
src/types/llm/request.ts (1)

68-75: Consider unifying with ResponseProviderMetadata.

This type has an identical structure to ResponseProviderMetadata in src/types/llm/response.ts. You could define a single ProviderMetadata type and re-export or alias it in both modules to reduce duplication.

src/settings/schema/migrations/1_to_2.ts (2)

5-11: Migration-local any types are pragmatic but cause ESLint issues downstream.

Using any to freeze v2 state is a reasonable approach for migration stability. However, this causes ESLint restrict-template-expressions errors when these values are used in template literals (see pipeline failure at line 695).

Consider adding explicit String() coercion at template literal usage sites, or define minimal structural types instead of any:

// Minimal structural types that capture only what the migration needs
type V2LLMProvider = { type: string; id: string; baseUrl?: string; apiKey?: string }
type V2ChatModel = { providerType: string; providerId: string; id: string; model: string; streamingDisabled?: boolean }
type V2EmbeddingModel = { providerType: string; providerId: string; id: string; model: string; dimension: number }

649-660: Consider adding String() coercion here for consistency.

Similar to line 695, this template literal uses a provider ID that may be typed as any. For consistency and to prevent future ESLint issues:

♻️ Suggested change
   if (data.ollamaChatModel.model) {
-    const ollamaChatModelId = `${ollamaChatProviderId}/${data.ollamaChatModel.model}`
+    const ollamaChatModelId = `${String(ollamaChatProviderId)}/${data.ollamaChatModel.model}`
     chatModels.push({
src/components/settings/sections/models/ChatModelSettings.tsx (1)

257-263: Consider adding budget range validation.

The budget validation only checks for NaN. Per the UI description (line 347), valid values include -1 for dynamic and 0 to disable. Consider adding validation to ensure the budget is either -1 or a non-negative number to prevent invalid configurations.

🔧 Suggested improvement
       if (controlMode === 'budget') {
         parsedBudget = parseInt(thinkingBudget, 10)
         if (isNaN(parsedBudget)) {
           new Notice('Please enter a valid number for thinking budget')
           return
         }
+        if (parsedBudget < -1) {
+          new Notice('Thinking budget must be -1 (dynamic), 0 (disabled), or a positive number')
+          return
+        }
       }
src/core/llm/gemini.ts (2)

228-272: Potential edge case: thoughtSignature lost when content is empty.

If an assistant message has an empty content string (''), no tool calls, but has a thoughtSignature, the signature will be lost since no parts are created (line 235 skips empty content, line 270 returns null).

This is likely a rare edge case, but consider whether the signature should be preserved in such scenarios or if this case is impossible in practice.


517-542: Consider enforcing mutual exclusivity between thinkingLevel and thinkingBudget.

The buildThinkingConfig method can set both thinkingLevel and thinkingBudget if the model settings have both defined. Per the UI (ChatModelSettings.tsx), these are meant to be mutually exclusive based on control_mode. However, if user settings somehow have both values, both would be sent to the API.

Consider adding logic to only set one based on the model's intended use (Gemini 3 uses level, Gemini 2.5 uses budget).

🔧 Suggested improvement
 private static buildThinkingConfig(
   model: ChatModel & { providerType: 'gemini' },
 ): ThinkingConfig | undefined {
   if (!model.thinking?.enabled) {
     return undefined
   }

   const config: ThinkingConfig = {}

-  if (model.thinking.thinking_level) {
+  // Use control_mode to determine which setting to apply
+  // Level for Gemini 3, Budget for Gemini 2.5
+  if (model.thinking.control_mode === 'level' && model.thinking.thinking_level) {
     const level = this.THINKING_LEVEL_MAP[model.thinking.thinking_level]
     if (level) {
       config.thinkingLevel = level
     }
-  }
-
-  if (model.thinking.thinking_budget !== undefined) {
+  } else if (model.thinking.control_mode === 'budget' && model.thinking.thinking_budget !== undefined) {
     config.thinkingBudget = model.thinking.thinking_budget
   }

   if (model.thinking.include_thoughts) {
     config.includeThoughts = model.thinking.include_thoughts
   }

   return config
 }
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 11e5512 and 5237d82.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (23)
  • package.json
  • src/components/settings/modals/AddChatModelModal.tsx
  • src/components/settings/sections/models/ChatModelSettings.tsx
  • src/constants.ts
  • src/core/llm/deepseekMessageAdapter.ts
  • src/core/llm/gemini.ts
  • src/core/llm/manager.ts
  • src/core/llm/morphProvider.ts
  • src/core/llm/xaiProvider.ts
  • src/hooks/useChatHistory.ts
  • src/settings/schema/migrations/13_to_14.test.ts
  • src/settings/schema/migrations/13_to_14.ts
  • src/settings/schema/migrations/1_to_2.ts
  • src/settings/schema/migrations/2_to_3.ts
  • src/types/chat-model.types.ts
  • src/types/chat.ts
  • src/types/embedding-model.types.ts
  • src/types/llm/request.ts
  • src/types/llm/response.ts
  • src/types/provider.types.ts
  • src/utils/chat/promptGenerator.ts
  • src/utils/chat/responseGenerator.ts
  • src/utils/llm/price-calculator.ts
💤 Files with no reviewable changes (1)
  • src/core/llm/morphProvider.ts
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-04-27T19:00:27.177Z
Learnt from: kevin-on
Repo: glowingjade/obsidian-smart-composer PR: 372
File: src/types/chat.ts:101-110
Timestamp: 2025-04-27T19:00:27.177Z
Learning: Tool-call-related types were extracted into a dedicated module (src/types/tool-call.types.ts) to avoid circular dependencies between chat.ts and llm/request.ts, creating a single source of truth.

Applied to files:

  • src/types/provider.types.ts
  • src/core/llm/deepseekMessageAdapter.ts
  • src/utils/llm/price-calculator.ts
  • src/types/chat.ts
  • src/settings/schema/migrations/2_to_3.ts
  • src/core/llm/gemini.ts
  • src/types/llm/request.ts
  • src/settings/schema/migrations/1_to_2.ts
  • src/types/chat-model.types.ts
📚 Learning: 2025-04-27T20:59:14.376Z
Learnt from: kevin-on
Repo: glowingjade/obsidian-smart-composer PR: 372
File: src/core/llm/anthropic.ts:352-359
Timestamp: 2025-04-27T20:59:14.376Z
Learning: In AnthropicProvider's parseRequestMessage method, the content property for assistant messages is already guaranteed to be a string by the type system, so additional type checking with `typeof content === 'string'` is unnecessary.

Applied to files:

  • src/core/llm/deepseekMessageAdapter.ts
  • src/utils/chat/promptGenerator.ts
  • src/utils/chat/responseGenerator.ts
  • src/types/chat.ts
  • src/types/llm/request.ts
  • src/hooks/useChatHistory.ts
📚 Learning: 2025-04-27T18:49:51.611Z
Learnt from: kevin-on
Repo: glowingjade/obsidian-smart-composer PR: 372
File: src/core/llm/anthropic.ts:521-527
Timestamp: 2025-04-27T18:49:51.611Z
Learning: When handling Anthropic streaming responses, the `input_json_delta` chunks only contain argument updates without the function name field. The complete metadata (ID, type, name) is provided in the initial `content_block_start` event, and these two types of events should be merged properly during streaming response processing.

Applied to files:

  • src/utils/chat/responseGenerator.ts
🧬 Code graph analysis (11)
src/core/llm/deepseekMessageAdapter.ts (3)
src/core/llm/openaiMessageAdapter.ts (1)
  • OpenAIMessageAdapter (23-202)
src/types/llm/response.ts (1)
  • LLMResponseNonStreaming (12-15)
src/types/llm/request.ts (1)
  • RequestMessage (88-92)
src/utils/llm/price-calculator.ts (1)
src/constants.ts (1)
  • XAI_PRICES (390-393)
src/types/chat.ts (1)
src/types/llm/request.ts (1)
  • RequestProviderMetadata (68-75)
src/settings/schema/migrations/13_to_14.ts (2)
src/settings/schema/setting.types.ts (1)
  • SettingMigration (93-97)
src/settings/schema/migrations/migrationUtils.ts (2)
  • getMigratedProviders (9-39)
  • getMigratedChatModels (56-90)
src/core/llm/gemini.ts (3)
src/types/llm/response.ts (2)
  • LLMResponseStreaming (17-20)
  • LLMResponseNonStreaming (12-15)
src/types/llm/request.ts (1)
  • RequestTool (98-101)
src/types/chat-model.types.ts (1)
  • ChatModel (109-109)
src/types/llm/request.ts (1)
src/types/tool-call.types.ts (1)
  • ToolCallRequest (1-5)
src/core/llm/xaiProvider.ts (1)
src/types/provider.types.ts (1)
  • LLMProvider (90-90)
src/settings/schema/migrations/13_to_14.test.ts (1)
src/settings/schema/migrations/13_to_14.ts (2)
  • migrateFrom13To14 (76-86)
  • DEFAULT_CHAT_MODELS_V14 (117-200)
src/components/settings/modals/AddChatModelModal.tsx (1)
src/types/chat-model.types.ts (1)
  • ChatModel (109-109)
src/components/settings/sections/models/ChatModelSettings.tsx (1)
src/types/chat-model.types.ts (2)
  • ChatModel (109-109)
  • chatModelSchema (28-107)
src/core/llm/manager.ts (1)
src/core/llm/xaiProvider.ts (1)
  • XaiProvider (18-65)
🪛 GitHub Actions: CI
src/settings/schema/migrations/1_to_2.ts

[error] 695-695: ESLint: Invalid type 'any' of template literal expression. @typescript-eslint/restrict-template-expressions

🪛 Gitleaks (8.30.0)
src/settings/schema/migrations/13_to_14.test.ts

[high] 151-151: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🔇 Additional comments (38)
src/settings/schema/migrations/2_to_3.ts (1)

3-8: Good practice: hardcoded constants for migration stability.

Decoupling migrations from current constants ensures this migration remains stable even if PROVIDER_TYPES_INFO changes in future versions.

src/components/settings/modals/AddChatModelModal.tsx (1)

106-114: LGTM!

The type cast is necessary when updating the discriminant field (providerType) of a discriminated union. The runtime validation via chatModelSchema.safeParse on submit provides safety against invalid state.

src/types/llm/request.ts (1)

77-82: LGTM!

Adding providerMetadata to RequestAssistantMessage enables round-tripping provider-specific context (like Gemini's thought signatures and DeepSeek's reasoning content) through multi-turn conversations.

src/types/chat.ts (3)

6-6: LGTM!

Import correctly added to bring in RequestProviderMetadata alongside other LLM types.


21-33: LGTM!

Adding providerMetadata to ChatAssistantMessage enables preserving provider-specific context (Gemini thought signatures, DeepSeek reasoning) across conversation turns.


63-75: LGTM!

SerializedChatAssistantMessage is correctly updated in parallel with ChatAssistantMessage, ensuring providerMetadata persists through serialization.

src/types/llm/response.ts (3)

30-37: LGTM!

ResponseProviderMetadata cleanly defines provider-specific metadata for responses. As noted in request.ts, this type is identical to RequestProviderMetadata and could potentially be unified.


39-50: LGTM!

Non-streaming choice correctly extended with providerMetadata to surface provider-specific data from the LLM response.


52-63: LGTM!

Streaming choice delta correctly mirrors the non-streaming structure, ensuring consistent metadata propagation for both response modes.

src/hooks/useChatHistory.ts (2)

141-151: LGTM!

The serialization of providerMetadata for assistant messages is consistent with the existing pattern for other optional fields like metadata, reasoning, and annotations.


178-188: LGTM!

The deserialization correctly restores providerMetadata from the serialized form, maintaining symmetry with the serialization logic.

src/utils/chat/promptGenerator.ts (1)

202-213: LGTM!

Including providerMetadata in the assistant RequestMessage enables provider-specific data (like DeepSeek's reasoning content) to be passed back during tool call iterations, which is essential for maintaining conversation context with providers that require it.

src/core/llm/deepseekMessageAdapter.ts (2)

23-52: LGTM on non-streaming response parsing.

The extraction of reasoning_content and its storage in both reasoning (for display) and providerMetadata (for API round-trip) is well-structured. The type cast is acceptable for accessing vendor-specific extensions not present in OpenAI's types.


84-100: LGTM on the request message parsing.

The method correctly:

  • Delegates to the parent class for base message construction
  • Conditionally injects reasoning_content only for assistant messages with the relevant metadata
  • Uses the type cast appropriately since reasoning_content is a DeepSeek-specific extension
src/types/provider.types.ts (1)

17-41: LGTM! Provider implementations verified.

The xAI, deepseek, and perplexity provider additions are properly implemented across all required files:

  • src/core/llm/manager.ts: XaiProvider imported and case handler present
  • src/constants.ts: xAI configuration with defaultProviderId defined
  • src/types/chat-model.types.ts: xAI literal included in schema
  • src/types/embedding-model.types.ts: xAI literal included in schema
src/core/llm/xaiProvider.ts (2)

18-34: LGTM!

The XaiProvider implementation correctly:

  • Extracts the 'xai' provider type from the union
  • Normalizes the baseURL by stripping trailing slashes (line 30)
  • Defaults to the correct xAI API endpoint
  • Uses the OpenAI SDK with the message adapter pattern consistent with other providers

60-64: LGTM!

Good use of String(this.provider.id) to safely coerce the provider ID in the template literal, avoiding potential ESLint restrict-template-expressions issues.

src/types/chat-model.types.ts (2)

52-65: LGTM!

The Gemini thinking configuration is well-structured:

  • control_mode discriminates between level-based (Gemini 3) and budget-based (Gemini 2.5) approaches
  • Comments clearly document the purpose of each field
  • All fields are appropriately optional to support various model capabilities

66-107: LGTM!

Provider type schema changes are consistent with the manager.ts implementation. The discriminated union covers all provider types handled in getProviderClient.

src/utils/llm/price-calculator.ts (2)

1-6: LGTM!

Clean addition of XAI_PRICES import alongside existing provider price constants.


46-54: LGTM!

The xAI pricing calculation follows the established pattern used by other providers. The null-return for unsupported models ensures graceful degradation.

src/core/llm/manager.ts (3)

18-18: LGTM!

XaiProvider import added correctly.


65-67: LGTM!

The 'xai' case correctly instantiates XaiProvider with the provider configuration. This aligns with the provider type definition in chat-model.types.ts.


37-74: Switch statement covers all provider types.

The switch statement handles all 12 provider types defined in the chatModelSchema discriminated union, ensuring exhaustive coverage. TypeScript will enforce this at compile time.

src/components/settings/sections/models/ChatModelSettings.tsx (1)

228-378: Well-structured Gemini thinking settings implementation.

The settings component follows the established patterns from other providers and correctly implements conditional UI for level vs. budget control modes. The schema validation ensures type safety before persisting settings.

src/types/embedding-model.types.ts (1)

35-62: Provider types align with migration changes.

The embedding model schema correctly reflects the updated provider types, including the new xai provider and removal of legacy providers (groq, morph). This is consistent with the provider changes in the v13→v14 migration.

src/settings/schema/migrations/13_to_14.ts (2)

56-66: Verify baseUrl preservation logic.

Line 63 uses p.baseUrl || LEGACY_PROVIDERS[p.type] which treats empty string '' as falsy, falling back to the default URL. This is likely the intended behavior, but verify that an explicitly set empty baseUrl should fall back to the default rather than preserving the empty value.


117-200: Default models list is well-structured.

The new default models cover major providers with appropriate configurations. The o4-mini model correctly includes reasoning settings with medium effort as default.

src/settings/schema/migrations/13_to_14.test.ts (3)

428-458: Good documentation of shallow merge behavior.

The test correctly documents that Object.assign does shallow merging, so user's custom reasoning_effort: 'high' gets overwritten by the default 'medium'. The comment at line 453 explicitly notes this behavior, which is helpful for future maintainers.

Note: The FIXME comment in migrationUtils.ts (lines 17-20, 70-73) already tracks this as a known limitation. Consider addressing the deep merge TODO if preserving nested user settings becomes a priority.


679-717: Test data reflects historical model identifier issues.

The Perplexity models in the test input (e.g., sonar-pro with model: 'sonar' at line 683) intentionally reflect incorrect historical configurations that users may have. This tests that the migration preserves existing user settings as custom models even when they contain incorrect identifiers, which is the correct behavior.


143-191: Static analysis false positive - test API keys.

The Gitleaks warning about a "generic API key" at line 151 is a false positive. All API keys in this test file are obviously fake placeholder values (e.g., 'mistral-key-789', 'sk-openai-secret-key', 'test-*-api-key'). No remediation needed.

src/core/llm/gemini.ts (2)

552-556: Embedding API migration looks correct.

The embedding retrieval correctly uses the new client.models.embedContent API and safely handles the response with optional chaining and a fallback to an empty array.


1-12: API migration to @google/genai is correctly implemented.

The code properly implements the @google/genai SDK API:

  • Client initialization at line 56: new GoogleGenAI({ apiKey: ... }) matches SDK requirements
  • Method calls at lines 82+ use client.models.generateContent() per SDK specification

No API compatibility issues found.

src/constants.ts (5)

12-14: Helpful documentation on model selection rationale.

The comment clearly explains the technical reasoning for preferring gpt-4.1-mini over gpt-5-mini for apply tasks. The model is correctly present in DEFAULT_CHAT_MODELS (lines 247-252).


17-17: LGTM!

The recommended models list is updated consistently with the new gpt-5.2 entry in DEFAULT_CHAT_MODELS.


48-103: Provider type additions look correct.

The new provider types follow the established pattern. Local inference providers (ollama, lm-studio) appropriately have requireApiKey: false, and the embedding support flags align with provider capabilities.


358-399: Pricing tables properly extended.

The new pricing entries follow the established pattern. The version comment for DeepSeek (DeepSeek-V3.2) is a good documentation practice that aids future maintenance.


266-274: The model identifiers gemini-3-pro-preview and gemini-3-flash-preview are correct and match Google's official Gemini 3 API documentation. These preview models are expected to evolve (similar to gemini-2.5-pro's progression from preview to official release), and the codebase already has migration infrastructure to handle future changes.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/core/llm/gemini.ts (3)

227-278: Bug risk: assistant message.content may be null/undefined (still pushed as text)
if (message.content !== '') will treat null/undefined as “not empty string” and attempt to push { text: message.content }, which can cause bad requests or runtime issues.

Proposed fix (null-safe + avoids sending empty text parts)
-        // Add text content part
-        if (message.content !== '') {
+        const text =
+          typeof message.content === 'string' ? message.content : ''
+
+        // Add text content part
+        if (text.trim() !== '') {
           // If no tool calls and we have a signature, attach it to the text part
           if (!hasToolCalls && thoughtSignature) {
-            contentParts.push({ text: message.content, thoughtSignature })
+            contentParts.push({ text, thoughtSignature })
           } else {
-            contentParts.push({ text: message.content })
+            contentParts.push({ text })
           }
         }

335-372: Streaming: tool-call IDs may not be stable across chunks
If the SDK re-emits functionCalls across multiple chunks without an id, uuidv4() will generate a new id each chunk, which can break downstream “merge deltas” logic. Consider deriving a deterministic id when f.id is absent (e.g., ${messageId}:${index}:${name}), or persist a map in the generator.

Example deterministic id (stateless)
-  private static parseFunctionCallsForStreaming(
+  private static parseFunctionCallsForStreaming(
     functionCalls: FunctionCall[] | undefined,
+    messageId?: string,
   ) {
     return functionCalls?.map((f, index) => ({
       index,
-      id: f.id ?? uuidv4(),
+      id: f.id ?? `${messageId ?? 'msg'}:${index}:${f.name ?? 'function'}`,
       type: 'function' as const,
       function: {
         name: f.name ?? '',
         arguments: JSON.stringify(f.args ?? {}),
       },
     }))
   }

And pass messageId from parseStreamingResponseChunk.


468-486: Tool schema: missing required (and properties may be undefined)
If tool.function.parameters has required, dropping it can change tool-call behavior. Also cleanedParameters.properties may be missing, and the cast will explode later.

Proposed fix (preserve required + safe default)
     return {
       functionDeclarations: [
         {
           name: tool.function.name,
           description: tool.function.description,
           parameters: {
             type: Type.OBJECT,
-            properties: cleanedParameters.properties as Record<string, Schema>,
+            properties:
+              (cleanedParameters.properties as Record<string, Schema> | undefined) ??
+              {},
+            required: cleanedParameters.required as string[] | undefined,
           },
         },
       ],
     }
🤖 Fix all issues with AI agents
In `@src/core/llm/gemini.ts`:
- Around line 505-542: In buildThinkingConfig (class-level THINKING_LEVEL_MAP
and method buildThinkingConfig), the check "if
(model.thinking.include_thoughts)" ignores explicit false; change the gating to
test for undefined (e.g., "if (model.thinking.include_thoughts !== undefined)")
so includeThoughts is set when the caller explicitly passes false or true; keep
the rest of the mapping for thinkingLevel/thinkingBudget unchanged.
- Around line 295-333: In parseNonStreamingResponse, stop mixing response.text
and response.functionCalls with candidate parts; instead treat
response.candidates?.[0]?.content?.parts as the canonical source: iterate the
parts array, concatenate text-type parts into the final content string and
collect functionCall-type parts into the functionCalls structure passed to
GeminiProvider.parseFunctionCalls (or adapt parseFunctionCalls to accept parts),
then remove usage of response.text and response.functionCalls and use the
extracted content and functionCalls when building the returned choice (preserve
existing use of extractThoughtSignature and extractThoughtSummaries on parts).
- Around line 81-101: The abortSignal (options?.signal) is incorrectly passed
inside the config object to this.client.models.generateContent and thus ignored;
move abortSignal out of config to the top-level request parameter when calling
this.client.models.generateContent (keep thinkingConfig inside config), e.g.,
remove abortSignal from the config and add abortSignal: options?.signal
alongside model, contents, and config; apply the same change to the other
generateContent call that uses GeminiProvider.parseRequestTool /
GeminiProvider.buildThinkingConfig so cancellations work correctly.
🧹 Nitpick comments (1)
src/core/llm/gemini.ts (1)

184-192: Streaming generator typing may be too narrow
If the SDK yields a different chunk type than GenerateContentResponse (or wraps it), this signature will fight TypeScript and/or break at runtime. Consider typing to the SDK’s exported stream chunk type (or infer from the SDK call).

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 327a17e and 3cc9b86.

📒 Files selected for processing (1)
  • src/core/llm/gemini.ts
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: namyeop
Repo: glowingjade/obsidian-smart-composer PR: 433
File: src/settings/schema/migrations/10_to_11.test.ts:20-20
Timestamp: 2025-07-11T08:35:40.553Z
Learning: The Gemini 2.5 Pro model naming evolution: preview-03-25 → exp-03-25 (migration 8→9) → gemini-2.5-pro (migration 10→11). Migration 10→11 should migrate from exp-03-25, not preview-03-25.
📚 Learning: 2025-04-27T19:00:27.177Z
Learnt from: kevin-on
Repo: glowingjade/obsidian-smart-composer PR: 372
File: src/types/chat.ts:101-110
Timestamp: 2025-04-27T19:00:27.177Z
Learning: Tool-call-related types were extracted into a dedicated module (src/types/tool-call.types.ts) to avoid circular dependencies between chat.ts and llm/request.ts, creating a single source of truth.

Applied to files:

  • src/core/llm/gemini.ts
🔇 Additional comments (4)
src/core/llm/gemini.ts (4)

399-443: Thought extraction looks reasonable; confirm thought/thoughtSignature fields exist on Part
Logic matches your comment (signature on first functionCall part else last part). Just verify the SDK actually populates part.thought and part.thoughtSignature in v1.35.0 for the models you’re targeting.


551-557: The code is correct as written. The @google/genai SDK (v1.35.0 per package.json) expects the contents parameter to accept a raw string, exactly as shown in the official documentation example. The SDK handles the conversion to the required structured format internally.

Likely an incorrect or invalid review comment.


124-165: No action needed—implementation is correct for SDK v1.35.0

The return type from generateContentStream() in @google/genai v1.35.0 is already Promise<AsyncGenerator<GenerateContentResponse>>. After await, stream is the async generator itself (not a wrapper object), which matches the streamResponseGenerator parameter type. Additionally, abortSignal and thinkingConfig are correctly placed in the config object, matching the non-streaming generateContent() method exactly.

Likely an incorrect or invalid review comment.


1-12: No changes needed — @google/genai v1.35.0 usage is correct

The constructor new GoogleGenAI({ apiKey }) is correct for v1.35.0. The models.generateContent and models.generateContentStream API shapes match the types imported (GenerateContentResponse, FunctionCall, etc.), and models.embedContent correctly accepts string/Content inputs. Obsidian's current Electron runtime (v39.x with Node 22.21.1) includes crypto.randomUUID() (available since Node v14.17), so no fallback is needed.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines 81 to 101
try {
const model = this.client.getGenerativeModel({
const result = await this.client.models.generateContent({
model: request.model,
generationConfig: {
contents: request.messages
.map((message) => GeminiProvider.parseRequestMessage(message))
.filter((m): m is Content => m !== null),
config: {
maxOutputTokens: request.max_tokens,
temperature: request.temperature,
topP: request.top_p,
presencePenalty: request.presence_penalty,
frequencyPenalty: request.frequency_penalty,
},
systemInstruction: systemInstruction,
})

const result = await model.generateContent(
{
systemInstruction: systemInstruction,
contents: request.messages
.map((message) => GeminiProvider.parseRequestMessage(message))
.filter((m): m is Content => m !== null),
abortSignal: options?.signal,
tools: request.tools?.map((tool) =>
GeminiProvider.parseRequestTool(tool),
),
thinkingConfig: GeminiProvider.buildThinkingConfig(model),
},
{
signal: options?.signal,
},
)
})

Copy link

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

🏁 Script executed:

# First, let's locate and examine the file
find . -name "gemini.ts" -type f

Repository: glowingjade/obsidian-smart-composer

Length of output: 103


🏁 Script executed:

# Let's also look at the package.json to confirm the SDK version
cat package.json | grep -A 5 -B 5 "@google"

Repository: glowingjade/obsidian-smart-composer

Length of output: 436


🏁 Script executed:

# Now let's examine the specific file around the mentioned lines
head -150 src/core/llm/gemini.ts | tail -80

Repository: glowingjade/obsidian-smart-composer

Length of output: 2778


🌐 Web query:

Google Genai SDK v1.35.0 generateContent API signature thinkingConfig abortSignal

💡 Result:

Short answer — in the @google/genai SDK (v1.35.0 era) generateContent accepts a config that can include thinking configuration, and you may pass an AbortSignal to cancel the request.

Example shape (JS/TS, simplified):

  • generateContent(options)
    • options.model: string
    • options.contents / parts: string | Part[]
    • options.config?: GenerateContentConfig (includes thinking_config / thinkingConfig / ThinkingConfig fields such as thinking_budget, thinking_level)
    • options.signal / abortSignal?: AbortSignal (used to abort streaming or long requests)

Concrete references:

  • thinking config usage in examples (GenerateContentConfig / ThinkingConfig). [1]
  • @google/genai SDK README / API surface showing generateContent / streaming methods. [2]
  • SDK/type bundles using AbortSignal on request/streaming-related methods (unpkg / d.ts references). [3]

If you want, I can paste an exact typed signature and a minimal example showing how to pass ThinkingConfig and an AbortSignal for v1.35.0.


🌐 Web query:

@google/genai generateContent request shape config object fields

💡 Result:

Below are the common fields you can pass in the Node.js @google/genai generateContent "config" object (camelCased in the SDK). Not exhaustive for every backend variant, but covers the typical/most-used options:

  • model (passed separately as top-level arg, not inside config) — string.
  • candidateCount — number (how many response candidates to return). [1][2]
  • maxOutputTokens — number (max tokens in model output). [1][2]
  • temperature — number (sampling temperature). [1][2]
  • topP — number (nucleus sampling). [1][2]
  • topK — number (top-k sampling). [1][2]
  • stopSequences — string[] (sequences where generation should stop). [1][2]
  • responseMimeType — string (e.g., "text/plain" or "application/json" for structured responses). [1][2]
  • responseSchema — JSON Schema object (when using responseMimeType: application/json to validate/structure output). [1][2]
  • safetySettings / safetySetting — object[] / object (per-request safety controls; backend-specific names/shape vary). [2]
  • tools — array (tool descriptors to enable tool/function calling). [2]
  • toolConfig — object (shared tool configuration). [2]
  • generationConfig / config alias — object (backend GenerationConfig for advanced model controls; SDK may expose top-level fields as above). [1][2]
  • labels — map<string,string> (optional request labels for billing/telemetry on Vertex AI). [2]

References:

  • Google GenAI / Gemini generateContent docs (request body and GenerationConfig examples). [1]
  • @google/genai (JS SDK) usage & examples (camelCase config examples, tools, responseSchema). [2]

If you want, I can paste an exact example config object for Node.js (which SDK/version you're using?)


🏁 Script executed:

# Check imports and type definitions in the file
head -50 src/core/llm/gemini.ts

Repository: glowingjade/obsidian-smart-composer

Length of output: 1435


Move abortSignal to the top level of the request—it should not be in config.

The thinkingConfig field is correctly placed inside config and should remain there. However, abortSignal is a top-level request parameter in the @google/genai v1.35.0 API, not a config field. Placing it inside config will cause the abort signal to be silently ignored, breaking request cancellation.

Fix required
       const result = await this.client.models.generateContent({
         model: request.model,
         contents: request.messages
           .map((message) => GeminiProvider.parseRequestMessage(message))
           .filter((m): m is Content => m !== null),
         config: {
           maxOutputTokens: request.max_tokens,
           temperature: request.temperature,
           topP: request.top_p,
           presencePenalty: request.presence_penalty,
           frequencyPenalty: request.frequency_penalty,
           systemInstruction: systemInstruction,
-          abortSignal: options?.signal,
           tools: request.tools?.map((tool) =>
             GeminiProvider.parseRequestTool(tool),
           ),
           thinkingConfig: GeminiProvider.buildThinkingConfig(model),
         },
+        signal: options?.signal,
       })

Also applies to: 108-121

🤖 Prompt for AI Agents
In `@src/core/llm/gemini.ts` around lines 81 - 101, The abortSignal
(options?.signal) is incorrectly passed inside the config object to
this.client.models.generateContent and thus ignored; move abortSignal out of
config to the top-level request parameter when calling
this.client.models.generateContent (keep thinkingConfig inside config), e.g.,
remove abortSignal from the config and add abortSignal: options?.signal
alongside model, contents, and config; apply the same change to the other
generateContent call that uses GeminiProvider.parseRequestTool /
GeminiProvider.buildThinkingConfig so cancellations work correctly.

Comment on lines 295 to 333
static parseNonStreamingResponse(
response: GenerateContentResult,
response: GenerateContentResponse,
model: string,
messageId: string,
): LLMResponseNonStreaming {
const parts = response.candidates?.[0]?.content?.parts
const thoughtSignature = GeminiProvider.extractThoughtSignature(parts)
const reasoning = GeminiProvider.extractThoughtSummaries(parts)

return {
id: messageId,
choices: [
{
finish_reason:
response.response.candidates?.[0]?.finishReason ?? null,
finish_reason: response.candidates?.[0]?.finishReason ?? null,
message: {
content: response.response.text(),
content: response.text ?? '',
reasoning: reasoning,
role: 'assistant',
tool_calls: response.response.functionCalls()?.map((f) => ({
id: uuidv4(),
type: 'function',
function: {
name: f.name,
arguments: JSON.stringify(f.args),
},
})),
tool_calls: GeminiProvider.parseFunctionCalls(
response.functionCalls,
),
providerMetadata: thoughtSignature
? { gemini: { thoughtSignature } }
: undefined,
},
},
],
created: Date.now(),
model: model,
object: 'chat.completion',
usage: response.response.usageMetadata
usage: response.usageMetadata
? {
prompt_tokens: response.response.usageMetadata.promptTokenCount,
completion_tokens:
response.response.usageMetadata.candidatesTokenCount,
total_tokens: response.response.usageMetadata.totalTokenCount,
prompt_tokens: response.usageMetadata.promptTokenCount ?? 0,
completion_tokens: response.usageMetadata.candidatesTokenCount ?? 0,
total_tokens: response.usageMetadata.totalTokenCount ?? 0,
}
: undefined,
}
}
Copy link

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

🏁 Script executed:

# First, let's examine the full gemini.ts file to understand the context
wc -l src/core/llm/gemini.ts

Repository: glowingjade/obsidian-smart-composer

Length of output: 105


🏁 Script executed:

# Let's look at the complete parseNonStreamingResponse method and surrounding code
cat -n src/core/llm/gemini.ts | sed -n '280,350p'

Repository: glowingjade/obsidian-smart-composer

Length of output: 2708


🏁 Script executed:

# Find the related helper methods to understand what they're doing
rg -A 10 "extractThoughtSignature|extractThoughtSummaries|parseFunctionCalls" src/core/llm/gemini.ts

Repository: glowingjade/obsidian-smart-composer

Length of output: 2748


🏁 Script executed:

# Look for the GenerateContentResponse type definition
rg -B 5 "GenerateContentResponse" src/core/llm/gemini.ts | head -50

Repository: glowingjade/obsidian-smart-composer

Length of output: 474


🏁 Script executed:

# Search for how the method is used to understand expected behavior
rg "parseNonStreamingResponse" --type ts -A 3 -B 3

Repository: glowingjade/obsidian-smart-composer

Length of output: 3261


🏁 Script executed:

# Check the imports to understand the SDK source and version
head -50 src/core/llm/gemini.ts | grep -E "^import|@google"

Repository: glowingjade/obsidian-smart-composer

Length of output: 386


🏁 Script executed:

# Look for package.json to see which version of Gemini SDK is used
cat package.json | grep -A 5 -B 5 "google\|gemini"

Repository: glowingjade/obsidian-smart-composer

Length of output: 436


🏁 Script executed:

# Search for any tests or documentation files mentioning Gemini response parsing
find . -type f \( -name "*.test.ts" -o -name "*.spec.ts" -o -name "README*" -o -name "*.md" \) -exec grep -l "parseNonStreamingResponse\|GenerateContentResponse" {} \;

Repository: glowingjade/obsidian-smart-composer

Length of output: 61


🏁 Script executed:

# Let's look at the complete extractThoughtSummaries method to understand what it's extracting
sed -n '500,566p' src/core/llm/gemini.ts | grep -A 20 "extractThoughtSummaries"

Repository: glowingjade/obsidian-smart-composer

Length of output: 61


🏁 Script executed:

# Let's see all helper methods in detail
sed -n '380,500p' src/core/llm/gemini.ts

Repository: glowingjade/obsidian-smart-composer

Length of output: 3509


🌐 Web query:

Google Gemini SDK GenerateContentResponse structure response.text vs response.functionCalls consistency

💡 Result:

Short answer: the SDK can legitimately return a GenerateContentResponse with no text field when the model emits a functionCall/functionResponse part. response.text in some SDKs is a convenience accessor that assumes a text part exists and will fail or be empty in those cases — you must instead inspect candidates[].content.parts for functionCall / functionResponse and handle those branches explicitly. See the official docs and SDK issue discussion for examples and guidance. [1][2][3]

Recommended handling

  • Don’t rely on response.text as the single source of truth.
  • Inspect response.candidates[].content.parts in order; each Part’s union field may be text, functionCall, functionResponse, or inlineData. If you see functionCall, call your tool and append a corresponding functionResponse part back into the conversation before asking for final text. [2][3]
  • Defensive code: if response.text is missing or throws, fall back to parsing parts for text or functionCall. The community-reported SDK bug shows response.text can raise when only a function call is returned. [1]

Sources

  1. GitHub issue reporting response.text exception when only a function call is returned. [1]
  2. Google AI / Gemini function-calling docs (explains parts: text, functionCall, functionResponse and recommended flow). [2]
  3. GenerateContentResponse schema (Vertex AI / Firebase docs showing Part union field types). [3]

🌐 Web query:

Google Gemini API thought parts response.text exclusion documentation

💡 Result:

Short answer: Gemini's docs show "thoughts" are delivered as separate content parts (part.thought boolean and thought_signature) when you enable thinking (thinkingConfig.includeThoughts). You must iterate response.candidates[0].content.parts to see thought vs non-thought text; response.text (the single flat text) may not include thought fragments. Also, Google has recently changed behavior for some models — several forum posts and docs note that thoughts may not be returned for certain model versions (or are only returned as summaries/signatures) so you may see thought content excluded from streamed response.text. [1][2][3]

Sources:

  • Thought signatures / thought parts (Gemini API docs). [1]
  • Thinking & multi‑step reasoning (how to enable includeThoughts and parts iteration). [2]
  • Community reports / Google staff notes about thoughts being missing for some models/versions. [3]

Use response.candidates[0].content.parts as the canonical source instead of mixing response.text with response.functionCalls

Per the Gemini SDK, response.text is a convenience accessor that excludes thought parts and can be empty or unreliable when function calls are present. The current code mixes root-level response.text and response.functionCalls with candidate-level parts extraction, risking inconsistency.

Recommended fix: Extract both content text and function calls directly from response.candidates[0].content.parts by iterating through parts and classifying each by type (text, functionCall, etc.), rather than relying on response.text as a primary source.

🤖 Prompt for AI Agents
In `@src/core/llm/gemini.ts` around lines 295 - 333, In parseNonStreamingResponse,
stop mixing response.text and response.functionCalls with candidate parts;
instead treat response.candidates?.[0]?.content?.parts as the canonical source:
iterate the parts array, concatenate text-type parts into the final content
string and collect functionCall-type parts into the functionCalls structure
passed to GeminiProvider.parseFunctionCalls (or adapt parseFunctionCalls to
accept parts), then remove usage of response.text and response.functionCalls and
use the extracted content and functionCalls when building the returned choice
(preserve existing use of extractThoughtSignature and extractThoughtSummaries on
parts).

Comment on lines +505 to +542
private static readonly THINKING_LEVEL_MAP: Record<string, ThinkingLevel> = {
minimal: ThinkingLevel.MINIMAL,
low: ThinkingLevel.LOW,
medium: ThinkingLevel.MEDIUM,
high: ThinkingLevel.HIGH,
}

/**
* Builds the thinking config for Gemini API based on model settings.
* - Gemini 3 models use thinkingLevel
* - Gemini 2.5 models use thinkingBudget
*/
private static buildThinkingConfig(
model: ChatModel & { providerType: 'gemini' },
): ThinkingConfig | undefined {
if (!model.thinking?.enabled) {
return undefined
}

const config: ThinkingConfig = {}

if (model.thinking.thinking_level) {
const level = this.THINKING_LEVEL_MAP[model.thinking.thinking_level]
if (level) {
config.thinkingLevel = level
}
}

if (model.thinking.thinking_budget !== undefined) {
config.thinkingBudget = model.thinking.thinking_budget
}

if (model.thinking.include_thoughts) {
config.includeThoughts = model.thinking.include_thoughts
}

return config
}
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

include_thoughts should be applied when explicitly false too
Current if (model.thinking.include_thoughts) skips false. If the SDK supports explicitly disabling thought inclusion, this prevents users from turning it off once enabled by defaults.

Proposed fix
-    if (model.thinking.include_thoughts) {
-      config.includeThoughts = model.thinking.include_thoughts
-    }
+    if (model.thinking.include_thoughts !== undefined) {
+      config.includeThoughts = model.thinking.include_thoughts
+    }
📝 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
private static readonly THINKING_LEVEL_MAP: Record<string, ThinkingLevel> = {
minimal: ThinkingLevel.MINIMAL,
low: ThinkingLevel.LOW,
medium: ThinkingLevel.MEDIUM,
high: ThinkingLevel.HIGH,
}
/**
* Builds the thinking config for Gemini API based on model settings.
* - Gemini 3 models use thinkingLevel
* - Gemini 2.5 models use thinkingBudget
*/
private static buildThinkingConfig(
model: ChatModel & { providerType: 'gemini' },
): ThinkingConfig | undefined {
if (!model.thinking?.enabled) {
return undefined
}
const config: ThinkingConfig = {}
if (model.thinking.thinking_level) {
const level = this.THINKING_LEVEL_MAP[model.thinking.thinking_level]
if (level) {
config.thinkingLevel = level
}
}
if (model.thinking.thinking_budget !== undefined) {
config.thinkingBudget = model.thinking.thinking_budget
}
if (model.thinking.include_thoughts) {
config.includeThoughts = model.thinking.include_thoughts
}
return config
}
private static readonly THINKING_LEVEL_MAP: Record<string, ThinkingLevel> = {
minimal: ThinkingLevel.MINIMAL,
low: ThinkingLevel.LOW,
medium: ThinkingLevel.MEDIUM,
high: ThinkingLevel.HIGH,
}
/**
* Builds the thinking config for Gemini API based on model settings.
* - Gemini 3 models use thinkingLevel
* - Gemini 2.5 models use thinkingBudget
*/
private static buildThinkingConfig(
model: ChatModel & { providerType: 'gemini' },
): ThinkingConfig | undefined {
if (!model.thinking?.enabled) {
return undefined
}
const config: ThinkingConfig = {}
if (model.thinking.thinking_level) {
const level = this.THINKING_LEVEL_MAP[model.thinking.thinking_level]
if (level) {
config.thinkingLevel = level
}
}
if (model.thinking.thinking_budget !== undefined) {
config.thinkingBudget = model.thinking.thinking_budget
}
if (model.thinking.include_thoughts !== undefined) {
config.includeThoughts = model.thinking.include_thoughts
}
return config
}
🤖 Prompt for AI Agents
In `@src/core/llm/gemini.ts` around lines 505 - 542, In buildThinkingConfig
(class-level THINKING_LEVEL_MAP and method buildThinkingConfig), the check "if
(model.thinking.include_thoughts)" ignores explicit false; change the gating to
test for undefined (e.g., "if (model.thinking.include_thoughts !== undefined)")
so includeThoughts is set when the caller explicitly passes false or true; keep
the rest of the mapping for thinkingLevel/thinkingBudget unchanged.

@kevin-on kevin-on changed the title Add gpt 5.1 model feat: Add xAI provider, upgrade Gemini SDK Jan 14, 2026
@kevin-on kevin-on merged commit fb9b838 into main Jan 14, 2026
2 checks passed
@kevin-on kevin-on deleted the feat/add-gpt-5.1-model branch January 14, 2026 10:18
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