Skip to content

feat: 增加 GCP Vertex AI 企业级接口支持与向导页 UI/UX 优化#156

Open
professorczh wants to merge 26 commits into
shenminglinyi:masterfrom
professorczh:master
Open

feat: 增加 GCP Vertex AI 企业级接口支持与向导页 UI/UX 优化#156
professorczh wants to merge 26 commits into
shenminglinyi:masterfrom
professorczh:master

Conversation

@professorczh
Copy link
Copy Markdown

@professorczh professorczh commented May 2, 2026

变更类型

  • feat 新功能
  • fix Bug 修复
  • chore 构建/工具链

变更说明

  1. Vertex AI 支持:新增 GCP Vertex AI 官方接口适配,支持企业级 ADC 认证与计费头注入,解决了 404 及鉴权报错问题。
  2. 向导页增强:将 NovelSetupGuide 中的原生 confirm 替换为 Naive UI dialog;修复了向导重置时 locationsError 未定义导致的崩溃 Bug。
  3. 样式与依赖:将向导页硬编码颜色改为 CSS 变量以适配暗色模式;剥离 Vertex AI 依赖至 requirements-vertex.txt 以保持核心轻量。

架构影响

  • 涉及层级:application / infrastructure / interfaces / frontend
  • 是否新增数据库表/字段:否
  • 是否修改现有 API 契约:否

测试

  • 后端测试:已通过自定义脚本验证 VertexAIProvider 在 Enterprise 模式下的流式生成逻辑,成功连接 global 端点。

  • 前端测试:已在本地通过 npm run dev 验证向导页在亮色/暗色模式下的视觉表现,弹窗交互功能正常。

  • 启动验证:本地后端与前端服务均可正常启动并完成小说创建流程。

  • 本地后端启动正常

  • 本地前端启动正常


风险说明

  • 潜在风险:Vertex AI 需要用户本地配置正确的 ADC 凭据,否则会报错(已在 .env.example 增加说明)。
  • 回滚方式:删除 infrastructure/ai/providers/vertex_ai_provider.py 并还原 provider_factory.py 即可。

Summary by CodeRabbit

  • New Features

    • Vertex AI / GCP LLM support with new env vars and optional install requirements
    • Novel management store: create, list, delete novels
    • Quick Actions: Settings button to open LLM settings
  • Improvements

    • Onboarding wizard adds an extra step, improved navigation and confirmations
    • UI: new Google font, theme-driven styling, added icon dependency
    • Tweaked generation defaults (temperature)
  • Bug Fixes

    • Validation guards for temperature/max tokens
    • Voice fingerprint endpoint may return null
    • Windows terminal encoding handling improved
  • Chores

    • Updated ignore rules and DB migration/auto-fix for LLM protocol constraint

…ntegrate LLM provider infrastructure with context budgeting services.
…implement continuous planning API routes and theme management.
…ex AI integration, and frontend configuration management
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 2, 2026

Note

Reviews paused

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

Use the following commands to manage reviews:

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

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds Vertex AI as a new LLM protocol/provider (google-genai), wires it through presets, factory, client, and DB schema/migration; exposes Vertex-related env vars and optional requirements file; updates frontend UI to select Vertex AI and onboarding/workbench UX; includes supporting tooling, CLI, and gitignore adjustments.

Changes

Vertex AI + Integration Flow

Layer / File(s) Summary
Env & Dependencies
.env.example, requirements-vertex.txt, requirements.txt
Adds GCP/Vertex env vars (GOOGLE_CLOUD_PROJECT, GOOGLE_CLOUD_LOCATION, GOOGLE_CLOUD_API_KEY, GOOGLE_APPLICATION_CREDENTIALS, GOOGLE_GENAI_USE_VERTEXAI) and an optional requirements-vertex.txt with usage guidance in requirements.txt.
DB Schema
infrastructure/persistence/database/schema.sql
Expands llm_profiles.protocol CHECK to include vertex-ai.
DB Migration / Repair
infrastructure/persistence/database/connection.py
Adds _fix_llm_profiles_protocol_check(conn) to detect and recreate llm_profiles with updated CHECK constraint and invoked during DB ensure.
Provider Implementation
infrastructure/ai/providers/vertex_ai_provider.py, infrastructure/ai/providers/__init__.py
New VertexAIProvider using google-genai (guarded by conditional import); __all__ updated to export when available.
Provider Refinements
infrastructure/ai/providers/gemini_provider.py
Refactors Gemini provider: optional HTTP transport (proxy), defensive URL building, safety settings injection, and optional JSON response-format validation.
Factory Wiring
infrastructure/ai/provider_factory.py
LLMProviderFactory.create_from_profile and _profile_to_settings extended to instantiate/configure VertexAIProvider for vertex-ai profiles (preserves base_url handling).
Service Presets & Auto-detect
application/ai/llm_control_service.py
Adds vertex-ai to LLMProtocol, adds vertex-ai-official preset and default Vertex profile; auto-populates profile model/project/region from GCP env vars when present.
Client Integration
infrastructure/ai/llm_client.py
Adds module logger and stronger typing, changes default temperature fallback to 0.7, and allows overriding the system prompt via kwargs for generate/stream_generate.
API Surface
interfaces/api/v1/workbench/llm_control.py, interfaces/api/v1/analyst/voice.py
list_models enhanced to accept Gemini model shape, uses trust_env=True, improves upstream error messages and API-key redaction; get_voice_fingerprint return/response_model changed to Optional[VoiceFingerprintResponse].
Frontend Wiring
frontend/src/components/workbench/LLMControlPanel.vue, frontend/src/api/llmControl.ts
Adds vertex-ai protocol option in UI and extends shared LLMProtocol type to include 'vertex-ai'.
Frontend UX / State
frontend/src/components/onboarding/NovelSetupGuide.vue, frontend/src/stores/novelStore.ts, frontend/src/components/stats/StatsSidebar.vue, frontend/src/views/Home.vue
Onboarding adds a “Plot Arc” step, polling timers, Naive UI dialogs, step guards; new useNovelStore Pinia store with async actions; StatsSidebar emits open-settings and Home.vue listens to open LLM settings.
Assets & Styling
frontend/index.html, frontend/package.json, frontend/src/views/Cast.vue, frontend/src/views/Home.vue
Adds Ma Shan Zheng font link, adds lucide-vue-next dependency, and replaces some hardcoded rgba colors with theme variables.
Misc Tooling & Runtime
cli.py, load_env.py, scripts/utils/download_model_via_modelscope.py, .gitignore
uvicorn.run now uses module path "interfaces.main:app" instead of importing app; exports PROJECT_ROOT; Windows stdout/stderr UTF-8 wrapping added for script; .gitignore updated to include models/ and scratch/ and reorganized PyInstaller ignores.

Sequence Diagram(s)

sequenceDiagram
    participant User as Frontend/User
    participant UI as LLMControlPanel
    participant Service as LLMControlService
    participant Factory as LLMProviderFactory
    participant Provider as VertexAIProvider
    participant GCP as Vertex AI API

    User->>UI: Select "Vertex AI / GCP"
    UI->>Service: Request presets/profiles
    Service-->>UI: Return presets (includes vertex-ai)
    User->>Service: Trigger generation (with credentials present)
    Service->>Factory: create_from_profile(vertex-ai profile)
    Factory->>Provider: Instantiate VertexAIProvider(settings)
    Provider->>GCP: Initialize genai.Client (vertexai=True, billing header)
    Service->>Provider: generate(prompt, config)
    Provider->>GCP: models.generate_content (async)
    GCP-->>Provider: response + usage
    Provider-->>Service: GenerationResult
    Service-->>UI: Rendered response to User
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • shenminglinyi

Poem

🐰 I nibble code beneath the moonlit screen,

Vertex hops in, a new provider seen.
Env set, DB patched, and clients hum,
Frontend steps align — the new day’s begun.
A rabbit cheers — small changes, big fun!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 51.52% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title clearly summarizes the main changes: adding GCP Vertex AI enterprise support and optimizing the NovelSetupGuide wizard UI/UX, directly reflecting the substantial changes across multiple layers.
Description check ✅ Passed The PR description comprehensively covers all required sections: change type (feat, fix, chore), clear explanation of three main improvements (Vertex AI support, wizard UI enhancement, style/dependency refactoring), architecture impact statement, testing evidence, and risk/rollback guidance.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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
Contributor

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

Caution

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

⚠️ Outside diff range comments (4)
frontend/src/components/onboarding/NovelSetupGuide.vue (1)

260-313: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Step 6 is unreachable, and the progress header is now out of sync.

This adds a sixth panel, but the wizard still behaves like a 5-step flow: the header at Line 11 still renders 5 n-steps, handleNext() only advances while currentStep < 5, and the Step 5 CTA closes the modal instead of advancing. detectWizardProgress() also still resumes fully completed novels at Step 5, so they reopen on the “设计情节弧线” screen instead of the completion screen.

Suggested fix
-      <n-step title="故事线" description="主线支线" />
-      <n-step title="开始" description="进入工作台" />
+      <n-step title="故事线" description="主线支线" />
+      <n-step title="情节弧线" description="起承转合" />
+      <n-step title="开始" description="进入工作台" />
-          <n-button v-if="currentStep === 5" type="primary" `@click`="handleComplete">
-            进入工作台
-          </n-button>
+          <n-button v-if="currentStep === 5" type="primary" `@click`="handleNext">
+            下一步
+          </n-button>
+          <n-button v-if="currentStep === 6" type="primary" `@click`="handleComplete">
+            进入工作台
+          </n-button>

And outside this hunk, align the state machine as well:

// handleNext
} else if (currentStep.value < 6) {
  currentStep.value++
}

// detectWizardProgress
resumedFromStep.value = 6
return 6
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/components/onboarding/NovelSetupGuide.vue` around lines 260 -
313, The wizard was extended to six panels but state/controls still assume five
steps; update the step count and transitions so currentStep can reach 6: adjust
the progress header to render 6 n-steps (instead of 5), change handleNext to
increment currentStep while currentStep < 6 (so Next advances from 5→6), change
the Step 5 CTA behavior (it should call handleNext instead of closing the modal)
or ensure handleComplete only runs when on step 6, and update
detectWizardProgress to return/resume at step 6 (set resumedFromStep.value = 6).
Locate and modify the symbols currentStep, handleNext, handleComplete (or the
Step 5 CTA), the progress header n-steps rendering, and detectWizardProgress to
keep the state machine consistent.
interfaces/api/v1/analyst/voice.py (1)

83-114: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Type declaration allows null but implementation never returns it.

The response_model=Optional[VoiceFingerprintResponse] and return type -> Optional[VoiceFingerprintResponse] declare that null is a valid response. However, the implementation always returns a VoiceFingerprintResponse object—either the found fingerprint (lines 116-122) or a default empty fingerprint (lines 108-114).

Either:

  1. Remove Optional since None is never returned, or
  2. If null is the intended behavior for missing fingerprints, change lines 106-114 to return None.
🔧 Option 1: Remove Optional (matches current behavior)
 `@router.get`(
     "/novels/{novel_id}/voice/fingerprint",
-    response_model=Optional[VoiceFingerprintResponse],
+    response_model=VoiceFingerprintResponse,
     status_code=200,
     summary="获取文风指纹",
     description="获取小说的文风指纹统计数据"
 )
 def get_voice_fingerprint(
     novel_id: str = Path(..., description="小说 ID"),
     pov_character_id: Optional[str] = Query(None, description="POV 角色 ID"),
     service=Depends(get_voice_fingerprint_service)
-) -> Optional[VoiceFingerprintResponse]:
+) -> VoiceFingerprintResponse:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interfaces/api/v1/analyst/voice.py` around lines 83 - 114, The route declares
Optional but always returns a VoiceFingerprintResponse; update
get_voice_fingerprint to remove Optional: change
response_model=Optional[VoiceFingerprintResponse] to
response_model=VoiceFingerprintResponse and change the function return
annotation from -> Optional[VoiceFingerprintResponse] to ->
VoiceFingerprintResponse (ensure any imports/type hints are adjusted and keep
the existing default-empty VoiceFingerprintResponse return path).
application/ai/llm_control_service.py (1)

390-399: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Vertex AI ADC profiles show incorrect mock-provider warnings and are blocked from connection testing.

get_runtime_summary (line 390) and test_profile_model (line 418) both gate on not profile.api_key.strip(). For ADC-based Vertex AI profiles this is always empty, so:

  • The runtime summary incorrectly reports using_mock=True with the "缺少 API Key" reason, misleading users.
  • The test endpoint returns ok=False with "请先填写 API Key 与模型名后再测试" before ever attempting a connection.

Both checks need the same protocol-aware fix applied in provider_factory.py:

🐛 Proposed fix (same pattern for both occurrences)
-        if not profile.api_key.strip() or not profile.model.strip():
+        uses_api_key = profile.protocol != 'vertex-ai'
+        if (uses_api_key and not profile.api_key.strip()) or not profile.model.strip():

Also applies to: 418-424

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

In `@application/ai/llm_control_service.py` around lines 390 - 399, The
runtime-summary and profile-test checks wrongly treat ADC-based Vertex AI
profiles as missing API keys because they only test profile.api_key.strip();
update both get_runtime_summary and test_profile_model to be protocol-aware
(same pattern used in provider_factory.py) so that when profile.protocol
indicates Vertex/ADC (e.g., protocol == 'vertex' or provider_factory helper like
supports_adc(profile.protocol)), you skip the api_key-empty gate and allow
model/base_url validation and connection testing; apply this exact change to
both get_runtime_summary and test_profile_model so Vertex ADC profiles do not
get using_mock=True or premature "fill API Key" test failures.
infrastructure/ai/provider_factory.py (1)

32-33: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Vertex AI (ADC) profiles always silently fall back to MockProvider — the feature is effectively broken.

The api_key guard at line 32 returns MockProvider whenever api_key is empty. For Vertex AI with ADC authentication, api_key is intentionally blank — the profile is populated by the GCP_PROJECT_ID/GOOGLE_APPLICATION_CREDENTIALS detection branch in _build_initial_config, which never sets api_key. Meanwhile, VertexAIProvider.__init__ constructs genai.Client(vertexai=True, ...) using ADC and never reads settings.api_key. So every ADC-configured Vertex AI profile is permanently routed to MockProvider and no generation request ever reaches VertexAIProvider.

The fix is to make the api_key check protocol-aware:

🐛 Proposed fix
-        if not resolved.api_key.strip() or not resolved.model.strip():
-            return MockProvider()
+        uses_api_key = resolved.protocol != 'vertex-ai'
+        if (uses_api_key and not resolved.api_key.strip()) or not resolved.model.strip():
+            return MockProvider()

The same pattern must also be fixed in llm_control_service.py's get_runtime_summary (line 390) and test_profile_model (line 418), which will otherwise report "will use MockProvider" and refuse to run connection tests for ADC users.

🧹 Nitpick comments (9)
scripts/utils/download_model_via_modelscope.py (1)

12-15: Use reconfigure() as the idiomatic alternative.

Python 3.7+ (this project uses Python 3.11) provides sys.stdout.reconfigure(encoding='utf-8'), which mutates the existing wrapper in-place and is more concise than manually re-wrapping with io.TextIOWrapper.

♻️ Proposed refactor
 if sys.platform == 'win32':
-    import io
-    sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
-    sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
+    sys.stdout.reconfigure(encoding='utf-8')
+    sys.stderr.reconfigure(encoding='utf-8')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/utils/download_model_via_modelscope.py` around lines 12 - 15, Replace
the manual re-wrapping of stdout/stderr in the Windows branch with the idiomatic
reconfigure() call: locate the sys.platform == 'win32' block that currently
imports io and reassigns sys.stdout/sys.stderr using io.TextIOWrapper, and
instead call sys.stdout.reconfigure(encoding='utf-8') and
sys.stderr.reconfigure(encoding='utf-8') to mutate the existing streams in-place
(remove the io import and the TextIOWrapper reassignment).
frontend/src/stores/novelStore.ts (2)

47-54: ⚡ Quick win

genre is always ''; consider mapping locked_genre or dropping the field.

NovelDTO.locked_genre (see frontend/src/api/novel.ts line 86) carries the parsed genre, but Line 52 hardcodes genre: ''. The Book interface exports this field as non-optional string, so any consumer that renders book.genre will silently receive an empty string.

♻️ Proposed fix
-       genre: '',
+       genre: novel.locked_genre ?? '',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/src/stores/novelStore.ts` around lines 47 - 54, The mapping for
books.value currently hardcodes genre: '' — update the books.value mapping in
novelStore.ts to populate Book.genre from NovelDTO.locked_genre (e.g., set
genre: novel.locked_genre || '') so consumers receive the parsed genre;
alternatively, if you prefer to drop the field, remove Book.genre from the Book
interface and omit it from the books.value mapping, but do not leave the
non-optional Book.genre always empty.

76-76: ⚡ Quick win

Replace payload: any with the typed parameter from novelApi.createNovel.

novelApi.createNovel already declares a precise parameter shape (see frontend/src/api/novel.ts lines 115-128). Using any here silently drops that safety at the store boundary.

♻️ Proposed refactor
-  const createBook = async (payload: any) => {
+  const createBook = async (payload: Parameters<typeof novelApi.createNovel>[0]) => {

Or import and re-export the type explicitly if it's needed elsewhere.

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

In `@frontend/src/stores/novelStore.ts` at line 76, The createBook function
currently accepts an untyped payload (payload: any); replace that with the exact
parameter type from novelApi.createNovel by changing the signature to use
Parameters<typeof novelApi.createNovel>[0] (or import the concrete request type
from frontend/src/api/novel.ts and use it) so the store preserves the API's type
safety; update any imports if necessary and adjust any call sites to satisfy the
new typed parameter.
frontend/package.json (1)

24-24: Migrate to @lucide/vue instead of the deprecated lucide-vue-next.

lucide-vue-next v1.0.0 is the final published version under this package name. As of Lucide v1, the package was renamed to @lucide/vue, and all future updates (bug fixes, new icons, security patches) will only be released under the new name. The current ^1.0.0 range is effectively pinned to that exact version and will never receive updates.

Note: The package does not appear to be imported anywhere in the codebase, so consider whether it should be removed entirely or if it's intended for future use.

♻️ Proposed fix: migrate to `@lucide/vue`
-    "lucide-vue-next": "^1.0.0",
+    "@lucide/vue": "^1.0.0",

If using lucide icons in component files, update imports:

-import { SomeIcon } from 'lucide-vue-next';
+import { SomeIcon } from '@lucide/vue';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@frontend/package.json` at line 24, Update the dependency entry for lucide in
package.json by replacing the deprecated package name "lucide-vue-next" with the
maintained scope package "@lucide/vue" (preserve or update the version
specifier, e.g. to a caret version like "^1.x" or to the latest appropriate
release), and then run your package manager to install; if the package is not
imported anywhere (search for imports of "lucide-vue-next" or icon usages),
remove the dependency entirely instead of migrating.
infrastructure/ai/config/settings.py (1)

27-33: 💤 Low value

Consider aligning type annotations with the None checks.

The fields default_temperature: float and default_max_tokens: int are not typed as Optional, yet the validation now guards against None. While this defensive check is harmless, the type annotations could be updated to Optional[float] and Optional[int] if None is a valid state, or the is not None checks could be removed if the default values guarantee non-null.

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

In `@infrastructure/ai/config/settings.py` around lines 27 - 33, The type
annotations for the fields used in __post_init__ (default_temperature and
default_max_tokens) are inconsistent with the None checks; either update their
annotations to Optional[float] and Optional[int] (and add from typing import
Optional) if None is a valid state, or remove the "is not None" guards in
__post_init__ and ensure the dataclass provides non-null defaults so the fields
remain plain float/int; pick one approach and make the corresponding change to
the field declarations and imports, referencing default_temperature,
default_max_tokens, and __post_init__.
infrastructure/ai/providers/gemini_provider.py (1)

74-82: 💤 Low value

Consider catching a more specific exception type.

The broad except Exception could mask programming errors beyond JSON parsing issues (e.g., AttributeError if target_schema is misconfigured). Consider catching pydantic.ValidationError specifically.

♻️ Proposed refinement
+from pydantic import BaseModel, ValidationError
 ...
         if target_schema and isinstance(target_schema, type) and issubclass(target_schema, BaseModel):
             try:
                 validated_obj = target_schema.model_validate_json(content)
                 content = validated_obj.model_dump_json()
-            except Exception as e:
+            except ValidationError as e:
                 logger.warning(f"Gemini output Pydantic check failed (returning raw): {e}")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@infrastructure/ai/providers/gemini_provider.py` around lines 74 - 82, The
current broad except Exception around the Pydantic validation (for
target_schema.model_validate_json and model_dump_json) can hide unrelated
errors; change it to catch pydantic.ValidationError specifically (import
ValidationError from pydantic) and use "except ValidationError as e:" to log the
validation failure via logger.warning; if you still want to surface unexpected
programming errors, add a separate generic except that re-raises or logs with
logger.exception so AttributeError/ImportError/etc. aren't silently swallowed.
application/ai/llm_control_service.py (1)

502-508: ⚡ Quick win

_normalize_base_url missing vertex-ai branch — falls through to OpenAI normalization.

Vertex AI profiles have an empty base_url. If normalize_openai_base_url('') ever returns a non-empty default (e.g., https://api.openai.com/v1), resolve_profile would silently embed an incorrect URL in the resolved profile. Adding an explicit passthrough prevents this ambiguity.

♻️ Proposed fix
     if protocol == 'anthropic':
         return normalize_anthropic_base_url(base_url) or ''
     if protocol == 'gemini':
         return normalize_gemini_base_url(base_url) or ''
+    if protocol == 'vertex-ai':
+        return base_url  # Vertex AI uses ADC; base_url may be empty or a custom endpoint
     return normalize_openai_base_url(base_url) or ''
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@application/ai/llm_control_service.py` around lines 502 - 508,
_normalize_base_url incorrectly falls through to OpenAI normalization for
vertex-ai profiles; add an explicit vertex-ai branch to passthrough the provided
base_url. Update _normalize_base_url (the static method) to check if protocol ==
'vertex-ai' and return base_url or '' directly, keeping the existing branches
for 'anthropic', 'gemini' (normalize_anthropic_base_url,
normalize_gemini_base_url) and the default OpenAI normalization
(normalize_openai_base_url) unchanged.
requirements-vertex.txt (1)

5-7: Remove google-cloud-aiplatform from requirements — it is completely unused.

vertex_ai_provider.py only imports from google.genai (provided by google-genai). No references to google.cloud.aiplatform exist in the codebase. Since google-genai is Google's recommended replacement for generative AI features, including the older google-cloud-aiplatform package adds unnecessary weight to the optional install without any benefit.

♻️ Proposed cleanup
 google-genai>=1.0.0
 google-auth>=2.26.0
-google-cloud-aiplatform>=1.70.0
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@requirements-vertex.txt` around lines 5 - 7, Remove the unnecessary
dependency "google-cloud-aiplatform" from requirements-vertex.txt (leave
google-genai and google-auth intact); confirm there are no imports referencing
google.cloud.aiplatform (e.g., check vertex_ai_provider.py which uses
google.genai only), then update any lock/CI dependency manifests if present and
run the test/installation step to ensure no breakage.
infrastructure/ai/providers/vertex_ai_provider.py (1)

10-10: Use types.HttpOptions() instead of plain dict for better type safety and IDE support.

The HttpOptions import at line 10 is unused; the code passes http_options as a plain dict. While the SDK internally converts plain dicts to HttpOptions and the headers will work correctly, using the typed types.HttpOptions() is the recommended approach per SDK documentation, providing better validation, IDE autocompletion, and consistency with SDK examples.

Suggested refactor
-    from google.genai.types import HttpOptions
+    from google.genai import types
             self.client = genai.Client(
                 vertexai=True,
                 project=self.project_id,
                 location=self.region if self.region != "us-central1" else "global",
-                http_options={
-                    'headers': {'x-goog-user-project': self.project_id}
-                }
+                http_options=types.HttpOptions(
+                    headers={'x-goog-user-project': self.project_id}
+                )
             )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@infrastructure/ai/providers/vertex_ai_provider.py` at line 10, The code
currently constructs http_options as a plain dict and doesn't use the imported
HttpOptions type; update the construction to instantiate and pass a typed
HttpOptions object (from google.genai.types.HttpOptions) instead of a raw dict
so you get IDE/type support and validation—locate where http_options is built
and passed (the variable named http_options used when calling the Vertex AI
client or request) and replace the dict with types.HttpOptions(...) setting the
headers field (e.g., headers={"x-goog-user-project": ...}) accordingly, remove
any redundant dict usage and ensure the existing HttpOptions import is used.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/components/onboarding/NovelSetupGuide.vue`:
- Around line 1061-1065: handlePrev currently only decrements currentStep and
can re-trigger generation when stepping back into Step 2/3; update handlePrev to
first prevent/back out when a generation is in-flight or to cancel/invalidate
that generation: check the reactive flags generatingCharacters and
generatingLocations and if either is true either (a) return early (do not
decrement) or (b) call an existing cancelGeneration or set a new invalidation
flag (e.g., invalidateStep(stepKey) or set
generatingCharacters=false/generatingLocations=false plus a requestId/token to
ignore results) before decrementing currentStep; also disable the footer back
button when generatingCharacters or generatingLocations is true so users cannot
navigate back during generation.

In `@frontend/src/components/stats/StatsSidebar.vue`:
- Around line 139-148: The settings button (.action-settings) is orphaned in a
2-column grid; update the scoped CSS in StatsSidebar.vue to make that button
span both columns (e.g., target .actions-grid .action-settings or
.action-settings inside the component and set grid-column: 1 / -1 or
grid-column: span 2) so the fifth button becomes full-width and removes the
empty right cell.

In `@frontend/src/stores/novelStore.ts`:
- Around line 64-73: The catch is mis-attributing a stats refresh failure as a
deletion failure in deleteBook: after calling novelApi.deleteNovel and updating
books, move the await statsStore.loadGlobalStats(true) into its own try/catch
(or fire-and-forget) so any network error from statsStore.loadGlobalStats(true)
is logged or handled but not re-thrown; keep the original deleteBook rejection
path only for novelApi.deleteNovel failures and reference the functions/vars
novelApi.deleteNovel, books, deleteBook, and statsStore.loadGlobalStats when
making the change.

In `@infrastructure/ai/llm_client.py`:
- Line 4: The module uses Union[Prompt, str] (e.g., in the annotation on line
using Union at symbol: Union[Prompt, str]) but Union is not imported, causing a
NameError at module import; fix by adding Union to the typing imports on the top
import line (update "from typing import AsyncIterator, Optional, Any, Dict" to
include Union) so the annotation resolves eagerly, then run the linter/tests to
confirm the F821 is resolved.

In `@infrastructure/ai/providers/vertex_ai_provider.py`:
- Around line 88-92: The code dereferences response.usage_metadata without a
guard in the VertexAI provider (the block that builds token_usage using
TokenUsage(input_tokens=usage.prompt_token_count...,
output_tokens=usage.candidates_token_count...)), which will raise AttributeError
if usage_metadata is None; update the logic to safely handle a missing
usage_metadata by checking if response.usage_metadata is truthy (or use getattr
with defaults) and default input_tokens/output_tokens to 0 when absent before
constructing TokenUsage so TokenUsage always receives integers.
- Around line 43-53: The constructor currently calls
settings.extra_body.get('...') without guarding extra_body and ends up calling
.strip() on a None for project_id; update the init to safely read extra_body
(use self.settings.extra_body or {} as done in _build_genai_config) and provide
a default fallback for project_id (e.g., fallback to DEFAULT_PROJECT or empty
string) before calling .strip(); specifically, change the expressions that set
self.region and self.project_id so they first read from
(self.settings.extra_body or {}).get('region') / .get('project_id') and ensure
the project_id or-region chain ends with a non-None default (like DEFAULT_REGION
for region and a safe default for project_id) prior to calling .strip().
- Around line 140-144: The inline comment and handling for thinking_level are
incorrect: change the comment to say thinking_level is Gemini 3-only and
document that the SDK expects types.ThinkingLevel enum values (e.g.,
types.ThinkingLevel.HIGH) or lowercase strings like "low"/"medium"/"high"; do
not claim "Gemini 2.0+". Update the code that sets params["thinking_config"]
(the block using types.ThinkingConfig and eb["thinking_level"]) to first guard
by the active model version/name (only set thinking_config when the selected
model is a Gemini 3 series) and, for Gemini 2.5 models, instead prefer using
thinking_budget (an integer token count) or skip setting thinking_config to
avoid API errors. Ensure you reference types.ThinkingConfig,
eb["thinking_level"], and the active model variable used in this module when
adding the guard.
- Around line 56-70: The try/except around genai.Client in VertexAIProvider
currently swallows initialization errors so self.client is never set, causing
AttributeError later; either rethrow the error after logging or set a safe
sentinel and make _ensure_sdk validate the actual client. Update the init block
around genai.Client (reference: self.client) to on failure log the error and
raise a RuntimeError (or assign self.client = None), and modify _ensure_sdk to
check both HAS_GENAI and that self.client is truthy (and raise a clear
RuntimeError mentioning VertexAIProvider and ADC/credentials issues) so
generate() and stream_generate() fail with a helpful message instead of
AttributeError.

In `@infrastructure/persistence/database/schema.sql`:
- Line 615: The llm_profiles table's protocol CHECK constraint was expanded to
include 'vertex-ai' but existing databases keep the old constraint, causing
SQLITE_CONSTRAINT on INSERT; add a one-time migration that renames llm_profiles
(e.g., llm_profiles_v1), creates a new llm_profiles table with the updated
protocol CHECK including 'vertex-ai' (all other columns identical), copies data
from llm_profiles_v1 into the new table, then drops llm_profiles_v1; implement
this migration as a separate upgrade script (not in schema.sql) and ensure it
runs once during upgrade to preserve existing rows and enable saving Vertex AI
profiles.

In `@interfaces/api/v1/workbench/llm_control.py`:
- Around line 116-125: The environment variable assignments inside the
api_format == 'gemini' branch (setting HTTPX_HTTP2, HTTPS_PROXY, HTTP_PROXY) are
ineffective because the httpx.AsyncClient is created with trust_env=False (and
mirror code in GeminiProvider uses an explicit transport); remove those dead
environment-setting lines or replace them by wiring an explicit proxy transport
when creating the client. Specifically, edit the gemini branch around the
api_format == 'gemini' block to either delete the
os.environ["HTTPX_HTTP2"/"HTTPS_PROXY"/"HTTP_PROXY"] assignments, or
construct/pass an httpx proxy/transport (as done in GeminiProvider) when
instantiating httpx.AsyncClient so proxy settings are actually used rather than
ignored.

In `@scripts/utils/download_model_via_modelscope.py`:
- Around line 14-15: The re-wrapping of sys.stdout and sys.stderr with
io.TextIOWrapper disables line buffering and can delay incremental progress
prints; update the two reassignments (sys.stdout =
io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8') and sys.stderr =
io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')) to pass
line_buffering=True so output is flushed per line (keep encoding='utf-8' as
before).

---

Outside diff comments:
In `@application/ai/llm_control_service.py`:
- Around line 390-399: The runtime-summary and profile-test checks wrongly treat
ADC-based Vertex AI profiles as missing API keys because they only test
profile.api_key.strip(); update both get_runtime_summary and test_profile_model
to be protocol-aware (same pattern used in provider_factory.py) so that when
profile.protocol indicates Vertex/ADC (e.g., protocol == 'vertex' or
provider_factory helper like supports_adc(profile.protocol)), you skip the
api_key-empty gate and allow model/base_url validation and connection testing;
apply this exact change to both get_runtime_summary and test_profile_model so
Vertex ADC profiles do not get using_mock=True or premature "fill API Key" test
failures.

In `@frontend/src/components/onboarding/NovelSetupGuide.vue`:
- Around line 260-313: The wizard was extended to six panels but state/controls
still assume five steps; update the step count and transitions so currentStep
can reach 6: adjust the progress header to render 6 n-steps (instead of 5),
change handleNext to increment currentStep while currentStep < 6 (so Next
advances from 5→6), change the Step 5 CTA behavior (it should call handleNext
instead of closing the modal) or ensure handleComplete only runs when on step 6,
and update detectWizardProgress to return/resume at step 6 (set
resumedFromStep.value = 6). Locate and modify the symbols currentStep,
handleNext, handleComplete (or the Step 5 CTA), the progress header n-steps
rendering, and detectWizardProgress to keep the state machine consistent.

In `@interfaces/api/v1/analyst/voice.py`:
- Around line 83-114: The route declares Optional but always returns a
VoiceFingerprintResponse; update get_voice_fingerprint to remove Optional:
change response_model=Optional[VoiceFingerprintResponse] to
response_model=VoiceFingerprintResponse and change the function return
annotation from -> Optional[VoiceFingerprintResponse] to ->
VoiceFingerprintResponse (ensure any imports/type hints are adjusted and keep
the existing default-empty VoiceFingerprintResponse return path).

---

Nitpick comments:
In `@application/ai/llm_control_service.py`:
- Around line 502-508: _normalize_base_url incorrectly falls through to OpenAI
normalization for vertex-ai profiles; add an explicit vertex-ai branch to
passthrough the provided base_url. Update _normalize_base_url (the static
method) to check if protocol == 'vertex-ai' and return base_url or '' directly,
keeping the existing branches for 'anthropic', 'gemini'
(normalize_anthropic_base_url, normalize_gemini_base_url) and the default OpenAI
normalization (normalize_openai_base_url) unchanged.

In `@frontend/package.json`:
- Line 24: Update the dependency entry for lucide in package.json by replacing
the deprecated package name "lucide-vue-next" with the maintained scope package
"@lucide/vue" (preserve or update the version specifier, e.g. to a caret version
like "^1.x" or to the latest appropriate release), and then run your package
manager to install; if the package is not imported anywhere (search for imports
of "lucide-vue-next" or icon usages), remove the dependency entirely instead of
migrating.

In `@frontend/src/stores/novelStore.ts`:
- Around line 47-54: The mapping for books.value currently hardcodes genre: '' —
update the books.value mapping in novelStore.ts to populate Book.genre from
NovelDTO.locked_genre (e.g., set genre: novel.locked_genre || '') so consumers
receive the parsed genre; alternatively, if you prefer to drop the field, remove
Book.genre from the Book interface and omit it from the books.value mapping, but
do not leave the non-optional Book.genre always empty.
- Line 76: The createBook function currently accepts an untyped payload
(payload: any); replace that with the exact parameter type from
novelApi.createNovel by changing the signature to use Parameters<typeof
novelApi.createNovel>[0] (or import the concrete request type from
frontend/src/api/novel.ts and use it) so the store preserves the API's type
safety; update any imports if necessary and adjust any call sites to satisfy the
new typed parameter.

In `@infrastructure/ai/config/settings.py`:
- Around line 27-33: The type annotations for the fields used in __post_init__
(default_temperature and default_max_tokens) are inconsistent with the None
checks; either update their annotations to Optional[float] and Optional[int]
(and add from typing import Optional) if None is a valid state, or remove the
"is not None" guards in __post_init__ and ensure the dataclass provides non-null
defaults so the fields remain plain float/int; pick one approach and make the
corresponding change to the field declarations and imports, referencing
default_temperature, default_max_tokens, and __post_init__.

In `@infrastructure/ai/providers/gemini_provider.py`:
- Around line 74-82: The current broad except Exception around the Pydantic
validation (for target_schema.model_validate_json and model_dump_json) can hide
unrelated errors; change it to catch pydantic.ValidationError specifically
(import ValidationError from pydantic) and use "except ValidationError as e:" to
log the validation failure via logger.warning; if you still want to surface
unexpected programming errors, add a separate generic except that re-raises or
logs with logger.exception so AttributeError/ImportError/etc. aren't silently
swallowed.

In `@infrastructure/ai/providers/vertex_ai_provider.py`:
- Line 10: The code currently constructs http_options as a plain dict and
doesn't use the imported HttpOptions type; update the construction to
instantiate and pass a typed HttpOptions object (from
google.genai.types.HttpOptions) instead of a raw dict so you get IDE/type
support and validation—locate where http_options is built and passed (the
variable named http_options used when calling the Vertex AI client or request)
and replace the dict with types.HttpOptions(...) setting the headers field
(e.g., headers={"x-goog-user-project": ...}) accordingly, remove any redundant
dict usage and ensure the existing HttpOptions import is used.

In `@requirements-vertex.txt`:
- Around line 5-7: Remove the unnecessary dependency "google-cloud-aiplatform"
from requirements-vertex.txt (leave google-genai and google-auth intact);
confirm there are no imports referencing google.cloud.aiplatform (e.g., check
vertex_ai_provider.py which uses google.genai only), then update any lock/CI
dependency manifests if present and run the test/installation step to ensure no
breakage.

In `@scripts/utils/download_model_via_modelscope.py`:
- Around line 12-15: Replace the manual re-wrapping of stdout/stderr in the
Windows branch with the idiomatic reconfigure() call: locate the sys.platform ==
'win32' block that currently imports io and reassigns sys.stdout/sys.stderr
using io.TextIOWrapper, and instead call
sys.stdout.reconfigure(encoding='utf-8') and
sys.stderr.reconfigure(encoding='utf-8') to mutate the existing streams in-place
(remove the io import and the TextIOWrapper reassignment).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: abbf22e1-13aa-4693-a92d-4bab2153dafb

📥 Commits

Reviewing files that changed from the base of the PR and between 6dab93a and 326c74b.

⛔ Files ignored due to path filters (1)
  • frontend/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (26)
  • .env.example
  • .gitignore
  • application/ai/llm_control_service.py
  • cli.py
  • frontend/index.html
  • frontend/package.json
  • frontend/src/api/llmControl.ts
  • frontend/src/components/onboarding/NovelSetupGuide.vue
  • frontend/src/components/stats/StatsSidebar.vue
  • frontend/src/components/workbench/LLMControlPanel.vue
  • frontend/src/stores/novelStore.ts
  • frontend/src/views/Cast.vue
  • frontend/src/views/Home.vue
  • infrastructure/ai/config/settings.py
  • infrastructure/ai/llm_client.py
  • infrastructure/ai/provider_factory.py
  • infrastructure/ai/providers/__init__.py
  • infrastructure/ai/providers/gemini_provider.py
  • infrastructure/ai/providers/vertex_ai_provider.py
  • infrastructure/persistence/database/schema.sql
  • interfaces/api/v1/analyst/voice.py
  • interfaces/api/v1/workbench/llm_control.py
  • load_env.py
  • requirements-vertex.txt
  • requirements.txt
  • scripts/utils/download_model_via_modelscope.py

Comment thread frontend/src/components/onboarding/NovelSetupGuide.vue
Comment thread frontend/src/components/stats/StatsSidebar.vue
Comment thread frontend/src/stores/novelStore.ts
Comment thread infrastructure/ai/llm_client.py Outdated
Comment thread infrastructure/ai/providers/vertex_ai_provider.py Outdated
Comment thread infrastructure/ai/providers/vertex_ai_provider.py
Comment thread infrastructure/ai/providers/vertex_ai_provider.py Outdated
Comment on lines +140 to +144
# 针对 Gemini 2.0+ 的思维配置 (Thinking)
if "thinking_level" in eb:
params["thinking_config"] = types.ThinkingConfig(
thinking_level=eb["thinking_level"] # e.g., "LOW", "MEDIUM", "HIGH"
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Misleading comment: thinking_level is Gemini 3-only, not "Gemini 2.0+".

"No models (apart from Gemini 2.5/3 series) support thinking or thinking budgets APIs." The inline comment on Line 140 says "Gemini 2.0+" but thinking_level in ThinkingConfig is specific to Gemini 3 models; Gemini 2.5 models use thinking_budget (an integer token count) instead. If a user sets thinking_level in extra_body while the active model is the default gemini-1.5-flash (or any 2.x model), the API will return an error.

Additionally, the official Python SDK uses types.ThinkingLevel.HIGH enum values or lowercase string literals such as "low" — document this expectation in the comment alongside the uppercase examples.

📝 Suggested comment/guard update
-        # 针对 Gemini 2.0+ 的思维配置 (Thinking)
+        # Thinking config: only valid for Gemini 3+ models (thinking_level)
+        # and Gemini 2.5 models (use thinking_budget instead).
+        # Accepted string values: "low" | "medium" | "high" (or types.ThinkingLevel enum).
         if "thinking_level" in eb:
             params["thinking_config"] = types.ThinkingConfig(
-                thinking_level=eb["thinking_level"] # e.g., "LOW", "MEDIUM", "HIGH"
+                thinking_level=eb["thinking_level"]  # e.g., "low", "medium", "high"
             )
📝 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
# 针对 Gemini 2.0+ 的思维配置 (Thinking)
if "thinking_level" in eb:
params["thinking_config"] = types.ThinkingConfig(
thinking_level=eb["thinking_level"] # e.g., "LOW", "MEDIUM", "HIGH"
)
# Thinking config: only valid for Gemini 3+ models (thinking_level)
# and Gemini 2.5 models (use thinking_budget instead).
# Accepted string values: "low" | "medium" | "high" (or types.ThinkingLevel enum).
if "thinking_level" in eb:
params["thinking_config"] = types.ThinkingConfig(
thinking_level=eb["thinking_level"] # e.g., "low", "medium", "high"
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@infrastructure/ai/providers/vertex_ai_provider.py` around lines 140 - 144,
The inline comment and handling for thinking_level are incorrect: change the
comment to say thinking_level is Gemini 3-only and document that the SDK expects
types.ThinkingLevel enum values (e.g., types.ThinkingLevel.HIGH) or lowercase
strings like "low"/"medium"/"high"; do not claim "Gemini 2.0+". Update the code
that sets params["thinking_config"] (the block using types.ThinkingConfig and
eb["thinking_level"]) to first guard by the active model version/name (only set
thinking_config when the selected model is a Gemini 3 series) and, for Gemini
2.5 models, instead prefer using thinking_budget (an integer token count) or
skip setting thinking_config to avoid API errors. Ensure you reference
types.ThinkingConfig, eb["thinking_level"], and the active model variable used
in this module when adding the guard.

name TEXT NOT NULL DEFAULT '',
preset_key TEXT NOT NULL DEFAULT 'custom-openai-compatible',
protocol TEXT NOT NULL DEFAULT 'openai' CHECK(protocol IN ('openai', 'anthropic', 'gemini')),
protocol TEXT NOT NULL DEFAULT 'openai' CHECK(protocol IN ('openai', 'anthropic', 'gemini', 'vertex-ai')),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Schema migration required — CREATE TABLE IF NOT EXISTS silently ignores the updated CHECK constraint on existing databases.

Any deployment whose llm_profiles table was created before this change retains the old constraint CHECK(protocol IN ('openai', 'anthropic', 'gemini')). Trying to INSERT a row with protocol='vertex-ai' will raise SQLITE_CONSTRAINT, so Vertex AI profiles can never be saved on upgraded deployments.

SQLite does not support ALTER TABLE … MODIFY COLUMN, so updating the constraint requires a table-rename migration, e.g.:

-- Migration: extend llm_profiles.protocol CHECK to include 'vertex-ai'
ALTER TABLE llm_profiles RENAME TO llm_profiles_v1;

CREATE TABLE IF NOT EXISTS llm_profiles (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL DEFAULT '',
    preset_key TEXT NOT NULL DEFAULT 'custom-openai-compatible',
    protocol TEXT NOT NULL DEFAULT 'openai' CHECK(protocol IN ('openai', 'anthropic', 'gemini', 'vertex-ai')),
    -- ... (all remaining columns identical) ...
    sort_order INTEGER NOT NULL DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

INSERT INTO llm_profiles SELECT * FROM llm_profiles_v1;
DROP TABLE llm_profiles_v1;

This migration script should be run once on upgrade and is separate from schema.sql.

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

In `@infrastructure/persistence/database/schema.sql` at line 615, The llm_profiles
table's protocol CHECK constraint was expanded to include 'vertex-ai' but
existing databases keep the old constraint, causing SQLITE_CONSTRAINT on INSERT;
add a one-time migration that renames llm_profiles (e.g., llm_profiles_v1),
creates a new llm_profiles table with the updated protocol CHECK including
'vertex-ai' (all other columns identical), copies data from llm_profiles_v1 into
the new table, then drops llm_profiles_v1; implement this migration as a
separate upgrade script (not in schema.sql) and ensure it runs once during
upgrade to preserve existing rows and enable saving Vertex AI profiles.

Comment thread scripts/utils/download_model_via_modelscope.py Outdated
Copy link
Copy Markdown
Contributor

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

♻️ Duplicate comments (2)
infrastructure/ai/providers/vertex_ai_provider.py (2)

60-74: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Swallowed init exception leaves self.client unset; _ensure_sdk doesn't guard it — downstream calls crash with AttributeError.

If genai.Client(...) raises (bad ADC credentials, network error, etc.), the except Exception at line 72 logs and continues without setting self.client. Because self.client = None is never initialized before the try, the attribute does not exist on the object. _ensure_sdk() only checks HAS_GENAI (line 143), so the first call to generate() or stream_generate() proceeds past the guard and fails with AttributeError: 'VertexAIProvider' object has no attribute 'client' — not a meaningful error for the caller.

🐛 Proposed fix (option A — fail fast, preferred)
+        self.client = None
         try:
             self.client = genai.Client(
                 vertexai=True,
                 project=self.project_id,
                 location=self.region if self.region != "us-central1" else "global",
                 http_options={
                     'headers': {'x-goog-user-project': self.project_id}
                 }
             )
             logger.info(f"Vertex AI (GenAI SDK) initialized: project={self.project_id}")
-        except Exception as e:
+        except Exception as e:  # noqa: BLE001
             logger.error(f"Failed to initialize GenAI Client: {e}")
+            raise RuntimeError(f"Vertex AI client initialization failed: {e}") from e
🐛 Proposed fix (option B — soft failure with clear guard)
+        self.client = None
         try:
             self.client = genai.Client(...)
             ...
-        except Exception as e:
+        except Exception as e:  # noqa: BLE001
             logger.error(f"Failed to initialize GenAI Client: {e}")

     def _ensure_sdk(self):
         if not HAS_GENAI:
             raise ImportError("Please install 'google-genai' to use VertexAIProvider.")
+        if not getattr(self, 'client', None):
+            raise RuntimeError(
+                "Vertex AI client failed to initialize. Check logs for the root cause."
+            )

Also applies to: 142-144

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

In `@infrastructure/ai/providers/vertex_ai_provider.py` around lines 60 - 74, The
constructor may leave self.client undefined if genai.Client(...) raises, causing
AttributeError later; initialize self.client = None before the try in class
VertexAIProvider and on except either re-raise the exception to fail fast or
explicitly set self.client = None and log the error; then update _ensure_sdk()
to check both HAS_GENAI and that self.client is not None (and raise a clear
RuntimeError mentioning VertexAIProvider and misconfigured/failed GenAI init) so
generate() and stream_generate() fail with a meaningful message instead of
AttributeError.

43-57: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

settings.extra_body guard missing in __init__; empty project_id still unvalidated.

Two residual issues from the prior review:

  1. extra_body guard: Lines 44 and 52 call settings.extra_body.get(...) directly. _build_genai_config already applies the safe eb = self.settings.extra_body or {} pattern, but __init__ does not. If profile.extra_body is None (e.g., nullable DB column not set), construction crashes with AttributeError.

  2. Empty project_id: The or "" at line 55 prevents the None.strip() crash, but there is no early ValueError when project_id resolves to an empty string. genai.Client(project="", ...) will then either fail with an opaque SDK error or silently misconfigure the billing target.

🐛 Proposed fix
-        raw_region = (
-            settings.extra_body.get('region') 
+        eb = settings.extra_body or {}
+        raw_region = (
+            eb.get('region')
             or os.getenv("GOOGLE_CLOUD_LOCATION") 
             or os.getenv("GCP_REGION") 
             or DEFAULT_REGION
         )
         self.region = raw_region.strip() if raw_region else DEFAULT_REGION

         raw_project_id = (
-            settings.extra_body.get('project_id') 
+            eb.get('project_id')
             or os.getenv("GOOGLE_CLOUD_PROJECT") 
             or os.getenv("GCP_PROJECT_ID")
             or ""
         )
         self.project_id = raw_project_id.strip()
+        if not self.project_id:
+            raise ValueError(
+                "Vertex AI: project_id is required. Set 'project_id' in the profile "
+                "extra_body, or set GOOGLE_CLOUD_PROJECT / GCP_PROJECT_ID env var."
+            )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@infrastructure/ai/providers/vertex_ai_provider.py` around lines 43 - 57, The
constructor (__init__) accesses settings.extra_body without a null guard and
allows an empty project_id; update __init__ to first normalize eb =
self.settings.extra_body or {} and use eb.get('region') / eb.get('project_id')
instead of directly calling settings.extra_body.get(...), trim the results into
self.region and self.project_id as before, and add a validation after trimming
that raises a ValueError (with a clear message) if self.project_id is empty so
callers don't construct genai.Client with an empty project string; reference the
__init__ method, settings.extra_body, self.region, self.project_id, and ensure
behavior matches the _build_genai_config safe eb pattern.
🧹 Nitpick comments (4)
infrastructure/persistence/database/connection.py (1)

293-293: ⚡ Quick win

避免在迁移里用 SELECT * 复制表数据。

INSERT INTO llm_profiles SELECT * FROM llm_profiles_old 依赖两张表的列数量和顺序完全一致,但这个 helper 本身就是给“旧 schema”准备的,后续只要 llm_profiles 再增一列,这里就会复制失败或把数据错位。建议改成显式列清单,或先用 PRAGMA table_info 取交集列再复制。

一种更稳妥的实现思路
+        target_cols = [
+            "id", "name", "preset_key", "protocol", "base_url", "api_key", "model",
+            "temperature", "max_tokens", "timeout_seconds", "extra_headers",
+            "extra_query", "extra_body", "notes",
+            "use_legacy_chat_completions", "sort_order", "created_at", "updated_at",
+        ]
+        source_cols = {
+            row[1] for row in conn.execute("PRAGMA table_info(llm_profiles_old)").fetchall()
+        }
+        shared_cols = [col for col in target_cols if col in source_cols]
+        cols_sql = ", ".join(shared_cols)
+        conn.execute(
+            f"INSERT INTO llm_profiles ({cols_sql}) "
+            f"SELECT {cols_sql} FROM llm_profiles_old"
+        )
-        conn.execute("INSERT INTO llm_profiles SELECT * FROM llm_profiles_old")
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@infrastructure/persistence/database/connection.py` at line 293, The migration
currently uses conn.execute("INSERT INTO llm_profiles SELECT * FROM
llm_profiles_old"), which breaks if column order/counts diverge; change it to
explicitly copy only the matching columns between llm_profiles and
llm_profiles_old by querying PRAGMA table_info for both tables, computing the
intersection of column names, then executing an INSERT INTO llm_profiles
(<col_list>) SELECT <col_list> FROM llm_profiles_old using that explicit column
list (or hardcode the safe column list) so you never rely on SELECT *; reference
the conn.execute call and the table names llm_profiles and llm_profiles_old when
making this change.
frontend/src/components/onboarding/NovelSetupGuide.vue (3)

30-32: 💤 Low value

Error message appears in both title and body.

The alert now shows bibleError in both the :title attribute and the inner <div> (line 31). This creates duplicate display of the same error text. Consider either removing the title binding or the inner div content.

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

In `@frontend/src/components/onboarding/NovelSetupGuide.vue` around lines 30 - 32,
The n-alert currently renders the same message twice by binding bibleError to
both :title and the inner div; remove one of them to avoid duplication — either
delete the :title="bibleError" attribute on the <n-alert> component or remove
the inner <div class="wizard-error-text">{{ bibleError }}</div>, keeping the
other so the error is shown only once; update any tests/styles that relied on
the removed element if needed.

278-288: 💤 Low value

Step 6 panel appears unreachable.

The Step 6 panel exists but there's no navigation path to it:

  • handleNext only increments currentStep when < 5, stopping at step 5
  • At step 5, the "进入工作台" button calls handleComplete() which closes the modal

Either this is intentional placeholder code for future use, or the step indicator and navigation logic need adjustment.

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

In `@frontend/src/components/onboarding/NovelSetupGuide.vue` around lines 278 -
288, The Step 6 panel is unreachable because currentStep is only incremented
while < 5 and the step-5 button calls handleComplete which closes the modal;
update the navigation logic so the UI can actually reach step 6: modify the step
advancement condition in handleNext (referencing currentStep and handleNext) to
allow increment up to 6 (e.g., change the guard from currentStep < 5 to
currentStep < 6 or adjust the comparison accordingly) or change the step-5
button’s handler (where handleComplete is invoked) to call handleNext instead so
the flow proceeds to the panel for step 6; ensure any step-indicator/limits that
reference max steps are consistent with the new max.

531-569: syncGenerationState() is defined but never called anywhere in the component.

Remove this unused function or integrate it into the component's initialization logic if it was intended for a future feature.

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

In `@frontend/src/components/onboarding/NovelSetupGuide.vue` around lines 531 -
569, The function syncGenerationState is defined but never invoked; either
remove it or call it during component initialization and when inputs change —
specifically, invoke syncGenerationState() from the component's setup lifecycle
(e.g., onMounted) and add watchers that call it when props.show or props.novelId
change (and also call it after any generation actions that update bibleData);
reference the syncGenerationState function name to locate it and ensure
bibleData.value, bibleGenerated, charactersGenerated, locationsGenerated,
generatingCharacters, generatingLocations and related worldbuildingApi/bibleApi
logic remain consistent when wiring the calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@frontend/src/components/onboarding/NovelSetupGuide.vue`:
- Around line 260-276: The template references an undefined IconChart component
(used in the step panel rendering), so add a render-function definition named
IconChart alongside the other icon renderers (the same area where IconBook,
IconPeople, IconMap, IconTimeline, IconCheck are defined); implement IconChart
to return an SVG element (matching the pattern used by the other icons) with an
appropriate path for a chart/plot arc, or simply alias IconChart = IconTimeline
if that icon is acceptable—this will eliminate the runtime warning and render
the icon in the step panel.

In `@infrastructure/persistence/database/connection.py`:
- Around line 297-299: The migration except block that currently logs the error
and calls conn.rollback() is silently swallowing failures for the llm_profiles
schema; update the handler in connection.py so that after logger.error(...) and
conn.rollback() you either re-raise the caught exception (raise) or perform a
post-rollback validation that the CHECK constraint on llm_profiles.protocol was
actually updated (and raise if not), ensuring failures surface at startup
instead of later when application/ai/llm_control_service.py writes
protocol='vertex-ai'. Reference the existing identifiers logger.error,
conn.rollback, the llm_profiles schema/check constraint and the
protocol='vertex-ai' write path when implementing the change.

In `@interfaces/api/v1/workbench/llm_control.py`:
- Around line 128-129: The proxy troubleshooting text is incorrect for the
client configured with trust_env=True; update the RequestError handling message
that currently says environment proxies are disabled to instead state that the
HTTP client inherits system/environment proxy settings and advise checking
environment proxy variables and system-wide proxy configuration (e.g.,
HTTP_PROXY/HTTPS_PROXY) and unwinding/clearing them if needed; locate the async
client instantiation "async with httpx.AsyncClient(timeout=timeout,
trust_env=True) as client" and the corresponding RequestError except block and
replace the misleading guidance with this corrected troubleshooting text.
- Around line 117-118: The current construction of url =
f"{actual_base.rstrip('/')}/models?key={api_key}" can leak the Gemini API key in
HTTPException.detail strings; keep using url for the request but create a
safe_url variant (e.g., remove the ?key=... query or replace the value with
?key=[REDACTED]) and replace uses of url in all error
messages/HTTPException.detail outputs (the error paths around where url is
embedded) with safe_url so the real api_key from actual_base/api_key is never
echoed back to callers.
- Around line 116-119: The Gemini branch currently builds a correct request
(api_format == 'gemini' -> actual_base, url, headers) but the response is later
normalized by _normalize_model_items which expects data["data"]; update the
Gemini handling to detect responses from models.list and transform
data["models"] into the shape _normalize_model_items expects (map each item
using name -> id (strip "models/" prefix if needed) and displayName -> friendly
name, or default to name if displayName missing) before calling
_normalize_model_items so successful Gemini responses no longer return count=0;
locate the api_format == 'gemini' branch and the call sites of
_normalize_model_items to insert this conversion logic (referencing actual_base,
url, _normalize_model_items, and the Gemini response fields "models", "name",
"displayName").

---

Duplicate comments:
In `@infrastructure/ai/providers/vertex_ai_provider.py`:
- Around line 60-74: The constructor may leave self.client undefined if
genai.Client(...) raises, causing AttributeError later; initialize self.client =
None before the try in class VertexAIProvider and on except either re-raise the
exception to fail fast or explicitly set self.client = None and log the error;
then update _ensure_sdk() to check both HAS_GENAI and that self.client is not
None (and raise a clear RuntimeError mentioning VertexAIProvider and
misconfigured/failed GenAI init) so generate() and stream_generate() fail with a
meaningful message instead of AttributeError.
- Around line 43-57: The constructor (__init__) accesses settings.extra_body
without a null guard and allows an empty project_id; update __init__ to first
normalize eb = self.settings.extra_body or {} and use eb.get('region') /
eb.get('project_id') instead of directly calling settings.extra_body.get(...),
trim the results into self.region and self.project_id as before, and add a
validation after trimming that raises a ValueError (with a clear message) if
self.project_id is empty so callers don't construct genai.Client with an empty
project string; reference the __init__ method, settings.extra_body, self.region,
self.project_id, and ensure behavior matches the _build_genai_config safe eb
pattern.

---

Nitpick comments:
In `@frontend/src/components/onboarding/NovelSetupGuide.vue`:
- Around line 30-32: The n-alert currently renders the same message twice by
binding bibleError to both :title and the inner div; remove one of them to avoid
duplication — either delete the :title="bibleError" attribute on the <n-alert>
component or remove the inner <div class="wizard-error-text">{{ bibleError
}}</div>, keeping the other so the error is shown only once; update any
tests/styles that relied on the removed element if needed.
- Around line 278-288: The Step 6 panel is unreachable because currentStep is
only incremented while < 5 and the step-5 button calls handleComplete which
closes the modal; update the navigation logic so the UI can actually reach step
6: modify the step advancement condition in handleNext (referencing currentStep
and handleNext) to allow increment up to 6 (e.g., change the guard from
currentStep < 5 to currentStep < 6 or adjust the comparison accordingly) or
change the step-5 button’s handler (where handleComplete is invoked) to call
handleNext instead so the flow proceeds to the panel for step 6; ensure any
step-indicator/limits that reference max steps are consistent with the new max.
- Around line 531-569: The function syncGenerationState is defined but never
invoked; either remove it or call it during component initialization and when
inputs change — specifically, invoke syncGenerationState() from the component's
setup lifecycle (e.g., onMounted) and add watchers that call it when props.show
or props.novelId change (and also call it after any generation actions that
update bibleData); reference the syncGenerationState function name to locate it
and ensure bibleData.value, bibleGenerated, charactersGenerated,
locationsGenerated, generatingCharacters, generatingLocations and related
worldbuildingApi/bibleApi logic remain consistent when wiring the calls.

In `@infrastructure/persistence/database/connection.py`:
- Line 293: The migration currently uses conn.execute("INSERT INTO llm_profiles
SELECT * FROM llm_profiles_old"), which breaks if column order/counts diverge;
change it to explicitly copy only the matching columns between llm_profiles and
llm_profiles_old by querying PRAGMA table_info for both tables, computing the
intersection of column names, then executing an INSERT INTO llm_profiles
(<col_list>) SELECT <col_list> FROM llm_profiles_old using that explicit column
list (or hardcode the safe column list) so you never rely on SELECT *; reference
the conn.execute call and the table names llm_profiles and llm_profiles_old when
making this change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 092ce778-92bc-4fad-9180-c987da25c614

📥 Commits

Reviewing files that changed from the base of the PR and between 326c74b and bdbc1d3.

📒 Files selected for processing (5)
  • frontend/src/components/onboarding/NovelSetupGuide.vue
  • infrastructure/ai/llm_client.py
  • infrastructure/ai/providers/vertex_ai_provider.py
  • infrastructure/persistence/database/connection.py
  • interfaces/api/v1/workbench/llm_control.py

Comment thread frontend/src/components/onboarding/NovelSetupGuide.vue
Comment thread infrastructure/persistence/database/connection.py
Comment thread interfaces/api/v1/workbench/llm_control.py
Comment thread interfaces/api/v1/workbench/llm_control.py
Comment thread interfaces/api/v1/workbench/llm_control.py
Copy link
Copy Markdown
Contributor

@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

Caution

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

⚠️ Outside diff range comments (1)
interfaces/api/v1/workbench/llm_control.py (1)

103-126: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

vertex-ai protocol not handled — misleading 400 error for ADC users.

vertex-ai is a valid LLMProtocol (defined alongside openai, anthropic, gemini), and provider_factory.py already has an explicit branch for it. However, list_models has no vertex-ai branch. For users following the standard ADC path (no API key), the request is rejected at lines 104–106 with "API key is required to fetch model list" — which is misleading because Vertex AI doesn't use API keys.

Add an early return specific to vertex-ai so users get a clear, actionable message instead of a confusing 400:

🐛 Proposed fix
     api_format = (candidate.get('protocol') or '').strip().lower()
     api_key = (candidate.get('api_key') or '').strip()
+    if api_format == 'vertex-ai':
+        # Vertex AI authenticates via ADC and does not support model listing
+        # through this endpoint. Users should enter the model name manually.
+        raise HTTPException(
+            status_code=400,
+            detail='Vertex AI 使用 ADC 认证,不支持通过此接口列举模型,请直接填写模型名称(如 gemini-2.0-flash-001)。',
+        )
     if not api_key:
         raise HTTPException(status_code=400, detail='API key is required to fetch model list')
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interfaces/api/v1/workbench/llm_control.py` around lines 103 - 126, The code
currently treats any candidate without api_key as an error before handling
vertex-ai; update the logic in the list_models flow that reads api_format
(variable api_format) so that if api_format == 'vertex-ai' you short-circuit
before the api_key check and return a clear, actionable response (or raise an
HTTPException with a message explaining Vertex AI uses ADC/credentials rather
than an API key). Keep the existing branches for 'anthropic', 'gemini', and the
OpenAI-compatible path intact (referencing api_format, api_key, base_url, and
_openai_compatible_models_base) and ensure the vertex-ai branch does not require
api_key so ADC users are not shown the misleading "API key is required to fetch
model list" error.
♻️ Duplicate comments (1)
interfaces/api/v1/workbench/llm_control.py (1)

78-89: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Gemini entry field mapping still broken after partial fix — id is always empty.

The change at line 79 correctly extracts data.get('models', []) for Gemini responses, but the per-entry mapping at lines 85–88 still uses entry.get('id', '') for both ModelItem.id and ModelItem.name. Gemini's models.list response identifies each model by its name field (e.g., "models/gemini-1.5-flash-001"), not id — so every normalized ModelItem will have id='' and name='', still yielding an effectively unusable model list despite the correct count.

To fix this, the per-entry mapping must account for the Gemini schema:

🐛 Proposed fix
     for entry in raw_list:
         if not isinstance(entry, dict):
             continue
+        raw_id = str(entry.get('id', ''))
+        raw_name = str(entry.get('name', ''))          # Gemini: "models/gemini-1.5-flash-001"
+        raw_display = str(entry.get('display_name') or entry.get('displayName') or '')
+        effective_id = raw_id or raw_name              # prefer OpenAI `id`, fall back to Gemini `name`
+        effective_name = raw_display or effective_id   # prefer human-readable displayName
         items.append(ModelItem(
-            id=str(entry.get('id', '')),
-            name=str(entry.get('id', '')),  # 多数网关不返回 name,回退到 id
-            owned_by=str(entry.get('owned_by', '')),
+            id=effective_id,
+            name=effective_name,
+            owned_by=str(entry.get('owned_by', '')),
         ))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@interfaces/api/v1/workbench/llm_control.py` around lines 78 - 89, The
per-entry mapping currently always uses entry.get('id') so Gemini responses
(which use "name") produce empty ids; update the loop that builds ModelItem to
set id = str(entry.get('name') or entry.get('id', '')) and name =
str(entry.get('name') or entry.get('id', '')) (keep the existing owned_by
mapping as-is), so ModelItem.id and ModelItem.name prefer the Gemini "name"
field but fall back to "id".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@infrastructure/ai/providers/vertex_ai_provider.py`:
- Around line 99-111: After reading response.text into content, detect if
content is None or empty and handle the safety-blocked case explicitly: do not
pass an empty string into GenerationResult; instead raise or return a clear,
attributed error describing that Vertex AI safety filters suppressed all output
(include identifying info from response such as any id or usage_metadata when
available). Update the code around content/response usage (the variables
response, content, and usage_metadata and the GenerationResult construction) so
generate() returns a descriptive ValueError (or a GenerationResult with a clear
"safety_blocked" sentinel) rather than the opaque ValueError from
GenerationResult.__post_init__; also ensure TokenUsage is still constructed from
response.usage_metadata if present.

---

Outside diff comments:
In `@interfaces/api/v1/workbench/llm_control.py`:
- Around line 103-126: The code currently treats any candidate without api_key
as an error before handling vertex-ai; update the logic in the list_models flow
that reads api_format (variable api_format) so that if api_format == 'vertex-ai'
you short-circuit before the api_key check and return a clear, actionable
response (or raise an HTTPException with a message explaining Vertex AI uses
ADC/credentials rather than an API key). Keep the existing branches for
'anthropic', 'gemini', and the OpenAI-compatible path intact (referencing
api_format, api_key, base_url, and _openai_compatible_models_base) and ensure
the vertex-ai branch does not require api_key so ADC users are not shown the
misleading "API key is required to fetch model list" error.

---

Duplicate comments:
In `@interfaces/api/v1/workbench/llm_control.py`:
- Around line 78-89: The per-entry mapping currently always uses entry.get('id')
so Gemini responses (which use "name") produce empty ids; update the loop that
builds ModelItem to set id = str(entry.get('name') or entry.get('id', '')) and
name = str(entry.get('name') or entry.get('id', '')) (keep the existing owned_by
mapping as-is), so ModelItem.id and ModelItem.name prefer the Gemini "name"
field but fall back to "id".
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a820078c-f33f-4d4b-843c-77e17b5e3fa0

📥 Commits

Reviewing files that changed from the base of the PR and between bdbc1d3 and 0608942.

📒 Files selected for processing (3)
  • frontend/src/components/onboarding/NovelSetupGuide.vue
  • infrastructure/ai/providers/vertex_ai_provider.py
  • interfaces/api/v1/workbench/llm_control.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • frontend/src/components/onboarding/NovelSetupGuide.vue

Comment thread infrastructure/ai/providers/vertex_ai_provider.py Outdated
Copy link
Copy Markdown
Contributor

@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.

♻️ Duplicate comments (3)
infrastructure/ai/providers/vertex_ai_provider.py (3)

98-115: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle empty model responses before constructing GenerationResult.

response.text or "" still falls through to GenerationResult.__post_init__, which raises an opaque empty-content error if Vertex returns a blank / no-candidate response that is not flagged SAFETY. Check for blank content here and raise a provider-specific error before building the result.

🛠️ Proposed fix
         content = response.text or ""
+        if not content.strip():
+            finish_reason = candidate.finish_reason if candidate else "UNKNOWN"
+            raise ValueError(
+                f"Vertex AI returned empty content (finish_reason={finish_reason})."
+            )
         
         # 提取 Token 使用量
         usage = getattr(response, 'usage_metadata', None)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@infrastructure/ai/providers/vertex_ai_provider.py` around lines 98 - 115, The
code currently sets content = response.text or "" but may still pass an empty
string into GenerationResult which triggers an opaque __post_init__ error;
before constructing GenerationResult (after reading response.candidates and
response.text and before creating TokenUsage/GenerationResult), detect blank
responses (e.g., candidate is None and not response.text, or
response.text.strip() == ""), and raise a provider-specific error (ValueError or
a custom ProviderError) with a clear message like "Vertex AI returned an empty
response — no content or candidates" so callers see a meaningful error; keep
using response.candidates, response.text, TokenUsage and GenerationResult names
to locate the fix.

43-75: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Guard Vertex AI init configuration and fail fast on client setup errors.

settings.extra_body is read directly here, and project_id can still end up blank. That leaves the provider half-configured and pushes the failure to a later call site. Please use a safe extra_body fallback, reject empty project_id up front, and re-raise genai.Client(...) init failures instead of only logging them.

🛠️ Proposed fix
-        raw_region = (
-            settings.extra_body.get('region') 
+        eb = settings.extra_body or {}
+        raw_region = (
+            eb.get('region')
             or os.getenv("GOOGLE_CLOUD_LOCATION") 
             or os.getenv("GCP_REGION") 
             or DEFAULT_REGION
         )
         self.region = raw_region.strip() if raw_region else DEFAULT_REGION
 
         raw_project_id = (
-            settings.extra_body.get('project_id') 
+            eb.get('project_id')
             or os.getenv("GOOGLE_CLOUD_PROJECT") 
             or os.getenv("GCP_PROJECT_ID")
             or ""
         )
         self.project_id = raw_project_id.strip()
+        if not self.project_id:
+            raise ValueError("Vertex AI requires project_id.")
@@
         except Exception as e:
             logger.error(f"Failed to initialize GenAI Client: {e}")
+            raise RuntimeError("Vertex AI client initialization failed") from e
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@infrastructure/ai/providers/vertex_ai_provider.py` around lines 43 - 75, Read
settings.extra_body safely (e.g. settings.extra_body or {}) when extracting
'project_id' and ensure you validate the resulting self.project_id after
.strip() is non-empty; if it's empty, raise a clear ValueError (or custom
exception) immediately instead of allowing a blank project to be used. When
initializing self.client via genai.Client (vertexai=True, project=...,
location=..., http_options=...), do not swallow exceptions — log the error with
context using logger.error and then re-raise the exception so the caller fails
fast; keep references to settings.extra_body, self.project_id, genai.Client,
self.client, and logger to locate and update the code.

86-90: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Only send thinking_config for supported Gemini models.

This block injects ThinkingConfig whenever thinking_level is present, even though the resolved model can still be an unsupported default like gemini-1.5-flash. That will turn an otherwise valid request into an API error. Thread the resolved model into _build_genai_config and gate this field by model family; for Gemini 2.5, prefer thinking_budget or omit the config.

🛠️ Proposed fix
-        gen_config = self._build_genai_config(config, prompt.system)
+        gen_config = self._build_genai_config(config, prompt.system, model_id)
@@
-        gen_config = self._build_genai_config(config, prompt.system)
+        gen_config = self._build_genai_config(config, prompt.system, model_id)
@@
-    def _build_genai_config(self, config: GenerationConfig, system_instruction: Optional[str]) -> types.GenerateContentConfig:
+    def _build_genai_config(self, config: GenerationConfig, system_instruction: Optional[str], model_id: str) -> types.GenerateContentConfig:
@@
-        if "thinking_level" in eb:
+        if "thinking_level" in eb and model_id.startswith("gemini-3"):

Also applies to: 127-130, 163-183

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

In `@infrastructure/ai/providers/vertex_ai_provider.py` around lines 86 - 90,
Thread the resolved model into _build_genai_config and only attach a
ThinkingConfig when the resolved model is a Gemini variant that actually
supports it; change _build_genai_config to accept the resolved model (from
_get_resolved_model) and conditionally construct ThinkingConfig (based on model
family/version), using thinking_budget or omitting thinking_config for
gemini-2.5/default unsupported models instead of always sending ThinkingConfig
when thinking_level is present. Apply this same conditional logic wherever
_build_genai_config is called (including the other occurrences noted) so
ThinkingConfig is only included for supported Gemini models and requests to
unsupported defaults (e.g., gemini-1.5-flash) are not mutated into API errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@infrastructure/ai/providers/vertex_ai_provider.py`:
- Around line 98-115: The code currently sets content = response.text or "" but
may still pass an empty string into GenerationResult which triggers an opaque
__post_init__ error; before constructing GenerationResult (after reading
response.candidates and response.text and before creating
TokenUsage/GenerationResult), detect blank responses (e.g., candidate is None
and not response.text, or response.text.strip() == ""), and raise a
provider-specific error (ValueError or a custom ProviderError) with a clear
message like "Vertex AI returned an empty response — no content or candidates"
so callers see a meaningful error; keep using response.candidates,
response.text, TokenUsage and GenerationResult names to locate the fix.
- Around line 43-75: Read settings.extra_body safely (e.g. settings.extra_body
or {}) when extracting 'project_id' and ensure you validate the resulting
self.project_id after .strip() is non-empty; if it's empty, raise a clear
ValueError (or custom exception) immediately instead of allowing a blank project
to be used. When initializing self.client via genai.Client (vertexai=True,
project=..., location=..., http_options=...), do not swallow exceptions — log
the error with context using logger.error and then re-raise the exception so the
caller fails fast; keep references to settings.extra_body, self.project_id,
genai.Client, self.client, and logger to locate and update the code.
- Around line 86-90: Thread the resolved model into _build_genai_config and only
attach a ThinkingConfig when the resolved model is a Gemini variant that
actually supports it; change _build_genai_config to accept the resolved model
(from _get_resolved_model) and conditionally construct ThinkingConfig (based on
model family/version), using thinking_budget or omitting thinking_config for
gemini-2.5/default unsupported models instead of always sending ThinkingConfig
when thinking_level is present. Apply this same conditional logic wherever
_build_genai_config is called (including the other occurrences noted) so
ThinkingConfig is only included for supported Gemini models and requests to
unsupported defaults (e.g., gemini-1.5-flash) are not mutated into API errors.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d8851b98-6903-443c-9f10-0de0a0059bc1

📥 Commits

Reviewing files that changed from the base of the PR and between 0608942 and e4ebe46.

📒 Files selected for processing (5)
  • frontend/src/components/onboarding/NovelSetupGuide.vue
  • frontend/src/components/stats/StatsSidebar.vue
  • frontend/src/stores/novelStore.ts
  • infrastructure/ai/providers/vertex_ai_provider.py
  • infrastructure/persistence/database/connection.py
✅ Files skipped from review due to trivial changes (1)
  • frontend/src/stores/novelStore.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • frontend/src/components/stats/StatsSidebar.vue
  • frontend/src/components/onboarding/NovelSetupGuide.vue

Copy link
Copy Markdown
Contributor

@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

♻️ Duplicate comments (1)
infrastructure/ai/providers/vertex_ai_provider.py (1)

43-58: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate project_id early (empty string should fail fast).

self.project_id is allowed to become "" (fallback chain ends with or "", then .strip()), and then genai.Client(... project=self.project_id ...) is attempted. That can shift failures to request-time with confusing auth/404 errors.

Add an explicit check in __init__ after computing self.project_id so the user gets a clear “project_id required” message.

Suggested fix
@@
         raw_project_id = (
             settings.extra_body.get('project_id') 
             or os.getenv("GOOGLE_CLOUD_PROJECT") 
             or os.getenv("GCP_PROJECT_ID")
             or ""
         )
         self.project_id = raw_project_id.strip()
+
+        if not self.project_id:
+            raise ValueError(
+                "Vertex AI: project_id is required. Set it in profile extra_body['project_id'], "
+                "or via GOOGLE_CLOUD_PROJECT / GCP_PROJECT_ID."
+            )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@infrastructure/ai/providers/vertex_ai_provider.py` around lines 43 - 58,
After computing raw_project_id and assigning self.project_id (from
raw_project_id.strip()), add a fast-fail validation that raises a clear error
when project_id is empty; e.g., after self.project_id = raw_project_id.strip()
check if not self.project_id and raise ValueError("project_id is required for
Vertex AI provider") (or a custom/configured exception) so code that later
constructs genai.Client(...) fails early with a clear message; reference
raw_project_id and self.project_id in the check inside the class __init__ that
builds the provider from settings.extra_body.
🧹 Nitpick comments (5)
infrastructure/ai/providers/vertex_ai_provider.py (5)

24-34: ⚡ Quick win

Ruff FULLWIDTH punctuation warnings: normalize to ASCII punctuation.

Static analysis hints show RUF002/RUF003 “ambiguous FULLWIDTH punctuation” warnings in docstrings/comments (e.g., ). If CI treats Ruff warnings as failures, this will block merges.

Please replace FULLWIDTH punctuation with ASCII equivalents in affected docstrings/comments.

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

In `@infrastructure/ai/providers/vertex_ai_provider.py` around lines 24 - 34,
Replace FULLWIDTH punctuation in the module/class docstrings and comments (e.g.,
the docstring on VertexAIProvider) with ASCII equivalents to silence Ruff
RUF002/RUF003 warnings: update characters like `:` `,` `(` `)` to `:` `,` `(`
`)` in the docstring and any nearby comments referencing DEFAULT_MODEL or
DEFAULT_REGION so the strings and comments use standard ASCII punctuation.

61-75: ⚡ Quick win

Preserve the underlying init failure cause (don’t lose actionable context).

You swallow exceptions in genai.Client(...) and just logger.error(...) then return, relying on _ensure_sdk() later. While _ensure_sdk() will prevent AttributeError, the later error message (“client not ready…”) won’t include the original exception details.

At minimum, store self._init_error = e and include it in _ensure_sdk()’s RuntimeError message (or re-raise during init).

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

In `@infrastructure/ai/providers/vertex_ai_provider.py` around lines 61 - 75, The
except block swallowing the genai.Client init loses the original error context;
capture and preserve it by assigning self._init_error = e inside the except
before logging (and keep the logger.error), and then update _ensure_sdk() to
include self._init_error in the raised RuntimeError (or re-raise the original
exception from __init__); reference genai.Client initialization, self.client,
self._init_error, and _ensure_sdk() so reviewers can locate and surface the
underlying init failure in later errors.

197-209: ⚖️ Poor tradeoff

Verify types.SafetySetting.threshold="OFF" is the correct value for this SDK.

The code sets threshold="OFF" with a comment implying it maps to “BLOCK_NONE”. If the google-genai SDK expects a different enum/string (e.g., BLOCK_NONE), this could either:

  • raise a client-side/validation error, or
  • silently not apply the intended safety configuration.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@infrastructure/ai/providers/vertex_ai_provider.py` around lines 197 - 209,
The _build_safety_settings function currently sets
types.SafetySetting(threshold="OFF"), which may be the wrong token for the
google-genai SDK; check the SDK docs or the types.SafetySetting enum to
determine the correct value (for example BLOCK_NONE or
types.SafetySetting.Threshold.BLOCK_NONE) and replace the literal "OFF" with the
proper enum/constant or string expected by the SDK so the safety settings are
applied/validated correctly; update the threshold in the list comprehension
inside _build_safety_settings accordingly.

7-14: 💤 Low value

Blind exception catch (except Exception) may be flagged by linting (BLE001).

Ruff static hints indicate BLE001 for “Do not catch blind exception: Exception”. Since this try/except is specifically for SDK initialization, consider either:

  • catching a narrower exception type, or
  • explicitly documenting why Exception is acceptable (Ruff noqa comment on that line).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@infrastructure/ai/providers/vertex_ai_provider.py` around lines 7 - 14, The
import block currently uses a broad exception handler; narrow it to catch
ImportError (e.g., replace "except Exception" with "except ImportError") when
importing google.genai (symbols: genai, types, HttpOptions) and set HAS_GENAI
accordingly, or if you intentionally need to catch all exceptions, add an
explicit Ruff noqa with a brief justification (e.g., "# noqa: BLE001  # allow
broad catch during optional SDK init") next to the except so HAS_GENAI and the
import logic remain correct and linters stop flagging the blind exception.

177-189: ⚖️ Poor tradeoff

Verify accepted type/value for thinking_level in types.ThinkingConfig.

You convert thinking_level to str(...).lower() and pass it into types.ThinkingConfig(thinking_level=level, ...). Depending on google-genai’s schema, it may require:

  • a specific enum value (e.g., types.ThinkingLevel.HIGH), or
  • exact lowercase literals (e.g., "high"|"medium"|"low").

This is worth verifying to avoid runtime API errors when users set extra_body.thinking_level.

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

In `@infrastructure/ai/providers/vertex_ai_provider.py` around lines 177 - 189,
The code coerces eb["thinking_level"] to a lowercase string and passes it into
types.ThinkingConfig as thinking_level which may expect a specific enum member
rather than a free-form string; update the block that builds
params["thinking_config"] (referencing eb, level, budget, and
types.ThinkingConfig) to validate/normalize the input: if types exposes an enum
(e.g., types.ThinkingLevel), map the lowercased string to the corresponding enum
member (e.g., {"low": types.ThinkingLevel.LOW, "medium":
types.ThinkingLevel.MEDIUM, "high": types.ThinkingLevel.HIGH}); otherwise,
ensure the exact literal format required by the API is used (or raise a clear
ValueError for invalid values) and only pass thinking_level when budget is not
provided.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@infrastructure/ai/providers/vertex_ai_provider.py`:
- Around line 76-116: In generate (async def generate) you must guard against a
missing/empty response.text and missing candidate before creating the
GenerationResult; currently you only raise when candidate.finish_reason ==
"SAFETY" but then do content = response.text or "" which can later fail
validation. Change the logic after retrieving response and candidate to: if
response.text is None or (not candidate and not response.text): raise a
descriptive ValueError (e.g., "Vertex AI returned no content") so we fail fast
and clearly; still compute token_usage from response.usage_metadata (TokenUsage)
and only construct and return GenerationResult when content is present.

---

Duplicate comments:
In `@infrastructure/ai/providers/vertex_ai_provider.py`:
- Around line 43-58: After computing raw_project_id and assigning
self.project_id (from raw_project_id.strip()), add a fast-fail validation that
raises a clear error when project_id is empty; e.g., after self.project_id =
raw_project_id.strip() check if not self.project_id and raise
ValueError("project_id is required for Vertex AI provider") (or a
custom/configured exception) so code that later constructs genai.Client(...)
fails early with a clear message; reference raw_project_id and self.project_id
in the check inside the class __init__ that builds the provider from
settings.extra_body.

---

Nitpick comments:
In `@infrastructure/ai/providers/vertex_ai_provider.py`:
- Around line 24-34: Replace FULLWIDTH punctuation in the module/class
docstrings and comments (e.g., the docstring on VertexAIProvider) with ASCII
equivalents to silence Ruff RUF002/RUF003 warnings: update characters like `:`
`,` `(` `)` to `:` `,` `(` `)` in the docstring and any nearby comments
referencing DEFAULT_MODEL or DEFAULT_REGION so the strings and comments use
standard ASCII punctuation.
- Around line 61-75: The except block swallowing the genai.Client init loses the
original error context; capture and preserve it by assigning self._init_error =
e inside the except before logging (and keep the logger.error), and then update
_ensure_sdk() to include self._init_error in the raised RuntimeError (or
re-raise the original exception from __init__); reference genai.Client
initialization, self.client, self._init_error, and _ensure_sdk() so reviewers
can locate and surface the underlying init failure in later errors.
- Around line 197-209: The _build_safety_settings function currently sets
types.SafetySetting(threshold="OFF"), which may be the wrong token for the
google-genai SDK; check the SDK docs or the types.SafetySetting enum to
determine the correct value (for example BLOCK_NONE or
types.SafetySetting.Threshold.BLOCK_NONE) and replace the literal "OFF" with the
proper enum/constant or string expected by the SDK so the safety settings are
applied/validated correctly; update the threshold in the list comprehension
inside _build_safety_settings accordingly.
- Around line 7-14: The import block currently uses a broad exception handler;
narrow it to catch ImportError (e.g., replace "except Exception" with "except
ImportError") when importing google.genai (symbols: genai, types, HttpOptions)
and set HAS_GENAI accordingly, or if you intentionally need to catch all
exceptions, add an explicit Ruff noqa with a brief justification (e.g., "# noqa:
BLE001  # allow broad catch during optional SDK init") next to the except so
HAS_GENAI and the import logic remain correct and linters stop flagging the
blind exception.
- Around line 177-189: The code coerces eb["thinking_level"] to a lowercase
string and passes it into types.ThinkingConfig as thinking_level which may
expect a specific enum member rather than a free-form string; update the block
that builds params["thinking_config"] (referencing eb, level, budget, and
types.ThinkingConfig) to validate/normalize the input: if types exposes an enum
(e.g., types.ThinkingLevel), map the lowercased string to the corresponding enum
member (e.g., {"low": types.ThinkingLevel.LOW, "medium":
types.ThinkingLevel.MEDIUM, "high": types.ThinkingLevel.HIGH}); otherwise,
ensure the exact literal format required by the API is used (or raise a clear
ValueError for invalid values) and only pass thinking_level when budget is not
provided.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e10f7a7c-45b7-490c-bf43-c02e9933ec9d

📥 Commits

Reviewing files that changed from the base of the PR and between e4ebe46 and f8ed1b0.

📒 Files selected for processing (2)
  • infrastructure/ai/providers/vertex_ai_provider.py
  • scripts/utils/download_model_via_modelscope.py

Comment on lines +76 to +116
async def generate(self, prompt: Prompt, config: GenerationConfig) -> GenerationResult:
"""执行单次文本生成任务。

Args:
prompt: 包含系统指令和用户输入的 Prompt 对象。
config: 包含模型 ID、温度等参数的生成配置。

Returns:
GenerationResult: 包含生成文本及 Token 使用量元数据的结果。
"""
self._ensure_sdk()
model_id = self._get_resolved_model(config)

gen_config = self._build_genai_config(config, prompt.system)

# 使用 aio 异步客户端
response = await self.client.aio.models.generate_content(
model=model_id,
contents=prompt.user,
config=gen_config
)

# 提取内容 (CodeRabbit: 处理安全拦截,防止 response.text 为空时崩溃)
candidate = response.candidates[0] if response.candidates else None
if candidate and candidate.finish_reason == "SAFETY":
raise ValueError("内容生成被 Vertex AI 安全过滤器拦截。请尝试修改输入或降低敏感度。")

content = response.text or ""

# 提取 Token 使用量
usage = getattr(response, 'usage_metadata', None)
if usage:
token_usage = TokenUsage(
input_tokens=usage.prompt_token_count or 0,
output_tokens=usage.candidates_token_count or 0
)
else:
token_usage = TokenUsage(input_tokens=0, output_tokens=0)

return GenerationResult(content=content, token_usage=token_usage)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Guard against empty response.text before GenerationResult (safety-blocked case).

You already handle finish_reason == "SAFETY" when response.candidates[0] exists, but you still do content = response.text or "" and then always construct GenerationResult(content=content, ...). If Vertex AI returns response.text=None (or empty) without finish_reason=="SAFETY" (e.g., candidates missing/partial response), this will likely trip GenerationResult’s validation and produce an opaque error.

Suggested fix
@@
-        content = response.text or ""
+        content = response.text or ""
+        if not content.strip():
+            finish_reason = (
+                candidate.finish_reason
+                if candidate and getattr(candidate, "finish_reason", None)
+                else "UNKNOWN"
+            )
+            raise ValueError(
+                "Vertex AI returned empty content. "
+                f"(finish_reason={finish_reason}) "
+                "This may be caused by safety filters or blocked output."
+            )
@@
         return GenerationResult(content=content, token_usage=token_usage)
🧰 Tools
🪛 Ruff (0.15.12)

[warning] 98-98: Comment contains ambiguous (FULLWIDTH COMMA). Did you mean , (COMMA)?

(RUF003)

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

In `@infrastructure/ai/providers/vertex_ai_provider.py` around lines 76 - 116, In
generate (async def generate) you must guard against a missing/empty
response.text and missing candidate before creating the GenerationResult;
currently you only raise when candidate.finish_reason == "SAFETY" but then do
content = response.text or "" which can later fail validation. Change the logic
after retrieving response and candidate to: if response.text is None or (not
candidate and not response.text): raise a descriptive ValueError (e.g., "Vertex
AI returned no content") so we fail fast and clearly; still compute token_usage
from response.usage_metadata (TokenUsage) and only construct and return
GenerationResult when content is present.

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