Skip to content

Conversation

@stefandevo
Copy link
Collaborator

@stefandevo stefandevo commented Jan 20, 2026

Summary

This PR introduces the Claude Compatible Providers system, a major enhancement that allows users to configure alternative API endpoints that expose Claude-compatible models throughout the application.

Key Features

  • Provider Models in All Dropdowns: Models from enabled providers (GLM, MiniMax, OpenRouter) now appear in all phase model selectors alongside native Claude models
  • Per-Phase Model Selection: Select different providers for different phases (e.g., GLM 4.7 for enhancements, Claude Opus for spec generation)
  • Project-Level Overrides: Override global phase model settings on a per-project basis via phaseModelOverrides
  • Bulk Replace Feature: Quickly switch all phase models to a provider at once, with tier mapping (haiku→haiku, sonnet→sonnet, opus→opus)
  • Provider-Specific Icons: GLM, MiniMax, and OpenRouter models display their respective provider icons

Technical Changes

Types (libs/types/src/settings.ts):

  • Renamed ClaudeApiProfile to ClaudeCompatibleProvider
  • Added ProviderModel type with mapsToClaudeModel field
  • Updated PhaseModelEntry with optional providerId field

Server (apps/server/src/):

  • New helper functions: getProviderByModelId(), getPhaseModelWithOverrides()
  • All routes that call Claude SDK now support provider configuration
  • Settings migration from v5 to v6 format (automatic)

UI (apps/ui/src/):

  • Phase model selectors show provider models with "Maps to X" labels
  • Bulk replace dialogs for global and project-level settings
  • Provider icons based on providerType field
  • Project settings loader handles phaseModelOverrides

Bug Fixes in This PR

  1. AtomicWriter Race Condition: Fixed concurrent write failures by adding random suffix to temp file names
  2. Bulk Replace "Anthropic Direct": Now properly resets to DEFAULT_PHASE_MODELS including thinking levels
  3. Provider Deletion Persistence: Empty provider arrays now persist correctly
  4. Terminology Updates: Changed "Claude API Profiles" to "Claude Compatible Providers" throughout UI

Test Plan

  • Configure a new GLM or MiniMax provider with models
  • Verify models appear in all phase model dropdowns
  • Select a provider model for a phase, verify it persists after refresh
  • Test project-level overrides, verify they persist after refresh
  • Use bulk replace to switch all models to a provider
  • Use bulk replace with "Anthropic Direct" to reset to defaults
  • Delete all providers, verify empty state persists
  • Test concurrent settings updates (bulk replace should no longer fail)

Screenshots

CleanShot 2026-01-20 at 19 58 18@2x CleanShot 2026-01-20 at 19 58 58@2x CleanShot 2026-01-20 at 19 59 15@2x CleanShot 2026-01-20 at 19 59 35@2x

Summary by CodeRabbit

  • New Features

    • Introduced a multi-provider system supporting Claude-compatible providers (GLM, MiniMax, OpenRouter, etc.) alongside native Anthropic
    • Added per-phase model overrides at the project level
    • Added bulk provider switching for all phase models
    • Provider-aware model resolution with credential management
  • UI Updates

    • Renamed "Claude" settings section to "Models"
    • Updated provider configuration UI from profiles to provider management
    • Added provider type badges and model mapping display
  • Chores

    • Migrated settings schema from v5 to v6 with provider system support

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

- Rename ClaudeApiProfile to ClaudeCompatibleProvider with models[] array
- Each ProviderModel has mapsToClaudeModel field for Claude tier mapping
- Add providerType field for provider-specific icons (glm, minimax, openrouter)
- Add thinking level support for provider models in phase selectors
- Show all mapped Claude models per provider model (e.g., "Maps to Haiku, Sonnet, Opus")
- Add Bulk Replace feature to switch all phases to a provider at once
- Hide Bulk Replace button when no providers are enabled
- Fix project-level phaseModelOverrides not persisting after refresh
- Fix deleting last provider not persisting (remove empty array guard)
- Add getProviderByModelId() helper for all SDK routes
- Update all routes to pass provider config for provider models
- Update terminology from "profiles" to "providers" throughout UI
- Update documentation to reflect new provider system
Resolved conflict in use-project-settings-loader.ts:
- Keep React Query approach from upstream
- Add phaseModelOverrides loading for provider model persistence
- Update both currentProject and projects array to keep in sync
1. AtomicWriter Race Condition Fix (libs/utils/src/atomic-writer.ts):
   - Changed temp file naming from Date.now() to Date.now() + random hex
   - Uses crypto.randomBytes(4).toString('hex') for uniqueness
   - Prevents ENOENT errors when multiple concurrent writes happen
     within the same millisecond

2. Bulk Replace "Anthropic Direct" Reset (both dialogs):
   - When selecting "Anthropic Direct", now uses DEFAULT_PHASE_MODELS
   - Properly resets thinking levels and other settings to defaults
   - Added thinkingLevel to the change detection comparison
   - Affects both global and project-level bulk replace dialogs
@coderabbitai
Copy link

coderabbitai bot commented Jan 20, 2026

📝 Walkthrough

Walkthrough

This PR introduces a unified Claude-compatible provider system replacing legacy API profiles. It adds server-side provider resolution helpers, per-project phase model overrides, provider-aware credential handling, and comprehensive UI for managing multiple providers (Anthropic, GLM, MiniMax, OpenRouter) with model mappings and bulk configuration tools.

Changes

Cohort / File(s) Summary
Core Provider Types & Exports
libs/types/src/settings.ts, libs/types/src/index.ts
Introduces ClaudeCompatibleProvider, ProviderModel, ClaudeCompatibleProviderType, ClaudeModelAlias, PhaseModelEntry types and CLAUDE_PROVIDER_TEMPLATES constant; increases SETTINGS_VERSION to 6; adds phaseModelOverrides and claudeCompatibleProviders to GlobalSettings and ProjectSettings.
Provider Configuration & Resolution
apps/server/src/lib/settings-helpers.ts, apps/server/src/types/settings.ts
Exports 4 new public helper functions (getProviderById, getPhaseModelWithOverrides, getProviderByModelId, getAllProviderModels) and 3 new result interfaces; imports ClaudeCompatibleProvider, PhaseModelKey, PhaseModelEntry, Credentials for provider resolution.
Provider SDK Integration
apps/server/src/providers/claude-provider.ts, libs/types/src/provider.ts
Extends ExecuteOptions with claudeCompatibleProvider field; updates buildEnv to handle both ClaudeApiProfile and ClaudeCompatibleProvider configs; adds runtime type guard isClaudeCompatibleProvider.
Settings Service Migration & Storage
apps/server/src/services/settings-service.ts
Adds v5→v6 migration: converts legacy claudeApiProfiles to claudeCompatibleProviders via migrateProfilesToProviders; adds phaseModelOverrides support with __CLEAR__ marker; introduces inferProviderType and inferModelDisplayName helpers.
Query Service Updates
apps/server/src/providers/simple-query-service.ts
Adds optional claudeCompatibleProvider to SimpleQueryOptions and StreamingQueryOptions with deprecation note on claudeApiProfile; propagates provider through query execution.
Route Handler Updates
apps/server/src/routes/app-spec/generate-*.ts, apps/server/src/routes/backlog-plan/generate-plan.ts, apps/server/src/routes/context/routes/describe-*.ts, apps/server/src/routes/enhance-prompt/routes/enhance.ts, apps/server/src/routes/github/routes/validate-issue.ts, apps/server/src/routes/suggestions/generate-suggestions.ts, apps/server/src/routes/worktree/routes/generate-commit-message.ts, apps/server/src/routes/features/routes/generate-title.ts
Replace getActiveClaudeApiProfile with getPhaseModelWithOverrides or getProviderByModelId; pass claudeCompatibleProvider instead of claudeApiProfile to provider queries; add provider-aware logging.
Service Layer Updates
apps/server/src/services/agent-service.ts, apps/server/src/services/auto-mode-service.ts, apps/server/src/services/ideation-service.ts
Integrate provider resolution via getProviderByModelId or direct credential retrieval; update ExecuteOptions to use claudeCompatibleProvider; resolve model mappings when provider is available.
Model Resolution
libs/model-resolver/src/resolver.ts
Adds providerId?: string to ResolvedPhaseModel; passes through unknown/provider models without fallback; logs provider context in phase model resolution.
UI Provider Management
apps/ui/src/components/views/settings-view/providers/claude-settings-tab/api-profiles-section.tsx
Replaces Claude API profile form with provider form supporting multiple models per provider; adds PROVIDER_TYPE_LABELS, PROVIDER_TYPE_COLORS, CLAUDE_MODEL_LABELS; introduces FIXED_SETTINGS_PROVIDERS concept; implements model badge UI and provider-type handling.
UI Model Selection & Bulk Replace
apps/ui/src/components/views/settings-view/model-defaults/phase-model-selector.tsx, apps/ui/src/components/views/settings-view/model-defaults/bulk-replace-dialog.tsx, apps/ui/src/components/views/settings-view/model-defaults/model-defaults-section.tsx
Extends model selector to render provider-specific models with popover/mobile support; adds BulkReplaceDialog for switching all phases to a selected provider's models; integrates provider availability checks and model mapping preview.
Project-level Settings UI
apps/ui/src/components/views/project-settings-view/project-models-section.tsx, apps/ui/src/components/views/project-settings-view/project-bulk-replace-dialog.tsx, apps/ui/src/components/views/project-settings-view/project-settings-view.tsx, apps/ui/src/components/views/project-settings-view/project-claude-section.tsx
Introduces ProjectModelsSection and ProjectBulkReplaceDialog for per-project phase model overrides; groups phases into task categories; supports per-phase override UI with reset and bulk-replace actions.
UI Settings & Navigation
apps/ui/src/components/views/settings-view/api-keys/api-keys-section.tsx, apps/ui/src/components/views/project-settings-view/config/navigation.ts, apps/ui/src/components/views/settings-view/providers/claude-settings-tab.tsx, apps/ui/src/components/ui/provider-icon.tsx
Updates documentation text from "profiles" to "providers"; changes navigation label from "Claude" to "Models" with Workflow icon; adds provider icon detection for GLM/MiniMax models.
State Management & Hooks
apps/ui/src/store/app-store.ts, apps/ui/src/hooks/use-settings-migration.ts, apps/ui/src/hooks/use-project-settings-loader.ts, apps/ui/src/lib/electron.ts
Adds claudeCompatibleProviders to AppState; implements 5 new provider actions (add, update, delete, set, toggle); adds setProjectPhaseModelOverride and clearAllProjectPhaseModelOverrides actions; extends settings migration and project loader to handle new overrides.
Utilities & Atomicity
libs/utils/src/atomic-writer.ts
Improves temp file naming for atomic writes by using timestamp + random bytes (.tmp.{timestamp}.{randomHex}) instead of timestamp alone.
Documentation
docs/UNIFIED_API_KEY_PROFILES.md
Documents replacement of ClaudeApiProfile with ClaudeCompatibleProvider, new ProviderModel and PhaseModelEntry types, provider-based model resolution, per-project overrides, and migration path.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant UI as UI (Route Handler)
    participant SettingsService
    participant Helpers as Provider Helpers
    participant ProviderSDK as Claude Provider SDK
    participant ClaudeAPI as Claude API

    User->>UI: Request with model selection
    UI->>SettingsService: getPhaseModelWithOverrides(phase)
    SettingsService->>Helpers: Resolve phase model + overrides
    Helpers->>SettingsService: Return { phaseModel, provider, credentials }
    SettingsService-->>UI: { phaseModel, provider, credentials }
    
    alt Provider Available
        UI->>Helpers: getProviderByModelId(modelId)
        Helpers->>SettingsService: Lookup provider + mappings
        Helpers-->>UI: { provider, resolvedModel, credentials }
        UI->>ProviderSDK: execute({ claudeCompatibleProvider, credentials })
        ProviderSDK->>ClaudeAPI: POST /messages (provider endpoint)
        ClaudeAPI-->>ProviderSDK: Response
    else Direct API (No Provider)
        UI->>ProviderSDK: execute({ credentials })
        ProviderSDK->>ClaudeAPI: POST /messages (Anthropic endpoint)
        ClaudeAPI-->>ProviderSDK: Response
    end
    
    ProviderSDK-->>UI: Result
    UI-->>User: Response
Loading
sequenceDiagram
    actor Admin as Admin / User
    participant UI as UI (Settings)
    participant AppStore as App Store
    participant SettingsService as Backend Settings Service
    participant Storage as Storage / DB

    Admin->>UI: Add / Edit Claude-compatible Provider
    UI->>AppStore: addClaudeCompatibleProvider(provider)
    AppStore->>SettingsService: addClaudeCompatibleProvider(provider)
    SettingsService->>Storage: Save updated claudeCompatibleProviders[]
    Storage-->>SettingsService: Persisted
    SettingsService-->>AppStore: Acknowledged
    AppStore->>UI: Update state (refresh provider list)
    UI-->>Admin: Provider added/saved

    Admin->>UI: Select provider in model dropdown
    UI->>AppStore: getClaudeCompatibleProviders()
    AppStore-->>UI: providers[]
    UI->>UI: Render provider models + mappings
    Admin->>UI: Select provider model for phase
    UI->>AppStore: setProjectPhaseModelOverride(projectId, phase, entry)
    AppStore->>SettingsService: updateProject({phaseModelOverrides})
    SettingsService->>Storage: Save project overrides
    Storage-->>SettingsService: Persisted
    SettingsService-->>AppStore: Acknowledged
    AppStore->>UI: Update state
    UI-->>Admin: Override applied
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

The changes introduce a substantial new provider system spanning 20+ files across types, server services, routes, and UI components. While individual patterns are relatively straightforward (provider resolution, credentials propagation, state management), the heterogeneity of changes—spanning settings migration, complex UI component logic, and multiple service integrations—combined with the breadth of affected modules requires careful verification of consistency across all integration points and end-to-end provider resolution flows.

Possibly related PRs

Suggested labels

Enhancement

Poem

🐰 Hops with glee through provider lands,
No longer bound by profile strands,
GLM, MiniMax, Anthropic too—
Each model mapped, each phase brand new!
Per-project overrides, bulk replace delight,
The unified provider system shines so bright!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.82% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Claude Compatible Providers System' accurately and concisely describes the main change: introducing a new system for configuring Claude-compatible providers as alternative API endpoints. It highlights the primary feature from the developer's perspective.

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

✨ Finishing touches
  • 📝 Generate docstrings

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

❤️ Share

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

@gemini-code-assist
Copy link
Contributor

Summary of Changes

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

This pull request significantly enhances the application's flexibility by introducing a robust system for integrating third-party AI model providers that adhere to the Claude API protocol. This allows for greater choice in model selection, enabling users to optimize for cost, performance, or specific model capabilities across various tasks and projects. The changes span across core logic, API routes, and the user interface to seamlessly incorporate these new provider options and management capabilities.

Highlights

  • Claude Compatible Providers System: Introduces a new system allowing users to configure and use alternative API endpoints that expose Claude-compatible models (e.g., GLM, MiniMax, OpenRouter) throughout the application.
  • Per-Phase Model Selection & Project Overrides: Users can now select different providers and models for different phases (e.g., GLM 4.7 for enhancements, Claude Opus for spec generation) and override these settings on a per-project basis.
  • Bulk Replace Feature: A new utility allows quickly switching all phase models to a specific provider, intelligently mapping Claude tiers (haiku→haiku, sonnet→sonnet, opus→opus) to the provider's equivalent models.
  • Settings Migration: An automatic migration from v5 to v6 settings format converts legacy ClaudeApiProfile configurations into the new ClaudeCompatibleProvider system, ensuring backward compatibility.
  • AtomicWriter Race Condition Fix: Resolved a bug causing concurrent write failures in atomicWriteJson by adding a random suffix to temporary file names, improving settings persistence reliability.

🧠 New Feature in Public Preview: You can now enable Memory to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

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

Invoking Gemini

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

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

Customization

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

Limitations & Feedback

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

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

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

Copy link
Contributor

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

Choose a reason for hiding this comment

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

Code Review

This is an impressive and extensive pull request that introduces the Claude Compatible Providers system. The refactoring is well-executed across both the server and UI, allowing for flexible configuration of alternative API endpoints. The changes are consistent, and backward compatibility is thoughtfully handled with a settings migration from v5 to v6. The new server-side helpers for provider and model resolution are well-designed, and the UI enhancements, including the updated model selector and new bulk replace dialogs, are comprehensive. Additionally, the bug fixes for the AtomicWriter race condition and provider deletion persistence are solid improvements. I have one minor suggestion to improve code clarity by importing a type at the top level, but overall, this is an excellent and well-thought-out contribution.

Comment on lines 10 to 17
McpServerConfig,
PromptCustomization,
ClaudeApiProfile,
ClaudeCompatibleProvider,
PhaseModelKey,
PhaseModelEntry,
Credentials,
} from '@automaker/types';
Copy link
Contributor

Choose a reason for hiding this comment

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

medium

For better code clarity and maintainability, it's good practice to import all necessary types at the top of the file. The ProviderModel type is used in getProviderByModelId and getAllProviderModels but is not imported here. Adding it to the import statement would make the code cleaner by avoiding inline import(...) statements later in the file.

  McpServerConfig,
  PromptCustomization,
  ClaudeApiProfile,
  ClaudeCompatibleProvider,
  PhaseModelKey,
  PhaseModelEntry,
  Credentials,
  ProviderModel,
} from '@automaker/types';

@stefandevo
Copy link
Collaborator Author

Closing to recreate as cross-repository PR from fork

@stefandevo stefandevo closed this Jan 20, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

Caution

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

⚠️ Outside diff range comments (3)
apps/server/src/services/agent-service.ts (1)

278-337: Check provider model vision support using capabilities from configuration, not ProviderFactory.

Line 190 validates vision support using ProviderFactory.modelSupportsVision() against the requested model ID, but user-configured provider models (from claudeCompatibleProviders in settings) are not registered in the built-in provider registry. Provider model capabilities should be checked from the modelConfig.capabilities.supportsVision returned by getProviderByModelId() at line 287, which is called after the vision check. Move the vision validation to after provider resolution completes (line 294), or add a helper to check the provider model's capabilities before falling back to ProviderFactory.modelSupportsVision() for built-in models.

apps/server/src/services/auto-mode-service.ts (2)

3750-3760: Use the mapped model for revision/task/continuation calls.

The initial call uses effectiveBareModel, but the follow-up executeQuery calls still use bareModel, which bypasses provider mappings and can hit the wrong model. Use effectiveBareModel consistently.

🐛 Suggested fix
-  model: bareModel,
+  model: effectiveBareModel,
-  model: bareModel,
+  model: effectiveBareModel,
-  model: bareModel,
+  model: effectiveBareModel,

Also applies to: 3899-3909, 3999-4009


2316-2372: Analysis path does not apply provider model mapping (mapsToClaudeModel) when provider overrides are used.

The analysis code uses getPhaseModelWithOverrides + resolvePhaseModel but unlike runAgent (agent-service.ts), it never calls getProviderByModelId to resolve the provider model to its mapped Claude model. When a provider model like "GLM-4.5-Air" is selected, resolvePhaseModel returns it unchanged without applying the mapsToClaudeModel mapping (which specifies it should resolve to 'haiku'). This means provider-specific model IDs are sent directly to the executor instead of being mapped to their corresponding Claude model tier. The analysis path should call getProviderByModelId to properly resolve the mapped Claude model, as agent-service does.

🤖 Fix all issues with AI agents
In `@apps/server/src/lib/settings-helpers.ts`:
- Around line 510-579: In getPhaseModelWithOverrides, when resolving provider
(function getPhaseModelWithOverrides) ensure that if the provider is not found
or provider.enabled === false you strip phaseModel.providerId and reset
phaseModel.model to a safe direct-API fallback (e.g., 'sonnet') so
provider-specific model strings or stale provider IDs are not passed to the
direct Anthropic SDK; implement this right after the provider resolution
branches (where provider is set to undefined or missing) so the returned
phaseModel contains no providerId and uses a safe default model.

In `@apps/server/src/routes/github/routes/validate-issue.ts`:
- Around line 170-191: The validation gate currently runs
isClaudeModel/isCursorModel/isCodexModel/isOpencodeModel before provider
resolution, causing provider model IDs (e.g., "GLM-4.5-Air") to be rejected;
change the flow so you call getProviderByModelId(model, settingsService, ...)
first (as in the block that sets claudeCompatibleProvider,
providerResolvedModel, credentials and effectiveModel) and then run the
validation against the resolved model (providerResolvedModel || model), and
ensure useStructuredOutput uses the Claude-compatible provider check
(claudeCompatibleProvider or isClaudeModel(resolvedModel)) rather than the
original raw model string. This ensures provider models are recognized and
structured output is correctly applied.

In
`@apps/ui/src/components/views/project-settings-view/project-models-section.tsx`:
- Around line 91-192: The getModelDisplayName function in PhaseOverrideItem
currently maps only 'haiku', 'sonnet', 'opus' and thus shows raw IDs like
'claude-sonnet'; update the modelMap inside getModelDisplayName to also include
the 'claude-haiku', 'claude-sonnet', and 'claude-opus' keys (mapping to 'Claude
Haiku', 'Claude Sonnet', 'Claude Opus' respectively) while keeping the existing
fallback to entry.model so any unknown IDs still render as-is.

In
`@apps/ui/src/components/views/settings-view/model-defaults/bulk-replace-dialog.tsx`:
- Around line 58-110: getClaudeModelAlias fails to recognize extended Claude
model IDs (like "claude-opus-4-5-20251101") because it only checks exact
strings; update getClaudeModelAlias to match prefixes instead of exact equality
(e.g., use startsWith or a regex) for the three checks (haiku, sonnet, opus) so
values like "claude-opus-..." or "opus-..." map to 'opus'; keep the existing
provider lookup for custom provider models unchanged.

In
`@apps/ui/src/components/views/settings-view/providers/claude-settings-tab/api-profiles-section.tsx`:
- Around line 209-243: The save routine always sets providerData.enabled = true
which re-enables a provider when editing; instead, preserve the existing enabled
value for edits by using the original provider's enabled flag when
editingProviderId is present. Update handleSave to set providerData.enabled to
the existing provider's enabled state (e.g., editingProvider.enabled) if
editingProviderId/editingProvider is available, otherwise default to true for
new providers; locate the providerData construction in handleSave and replace
the hardcoded true with editingProvider?.enabled ?? true (or the equivalent
lookup of the original provider by editingProviderId).

In `@apps/ui/src/hooks/use-project-settings-loader.ts`:
- Around line 98-137: The code only updates project fields when
activeClaudeApiProfileId/phaseModelOverrides are defined, so removals leave
stale values; change the logic in use-project-settings-loader.ts to detect
presence of the keys on settingsWithExtras (e.g., "'activeClaudeApiProfileId' in
settingsWithExtras" and "'phaseModelOverrides' in settingsWithExtras") instead
of checking !== undefined, update needsUpdate to consider presence + difference
(including presence meaning "should be cleared"), and build updatedProjectData
to explicitly set activeClaudeApiProfileId to null (or undefined per your model)
when the key is present but value is null, or to remove/clear
phaseModelOverrides when the key is present and value is undefined/null; then
call setCurrentProject(updatedProjectData) and useAppStore.setState({ projects:
updatedProjects }) as before so both currentProject and the projects array
reflect clears as well.

In `@docs/UNIFIED_API_KEY_PROFILES.md`:
- Around line 149-164: The docs list getPhaseModelWithOverrides as returning {
model, thinkingLevel, providerId, providerConfig, credentials } but call sites
(e.g., commit-message route) destructure values as phaseModel and provider;
reconcile by verifying the actual function signature of
getPhaseModelWithOverrides and updating the docs or the function so they match:
either change the docs to document the real return shape (e.g., { phaseModel,
provider, ... } if that is what the function returns) or modify
getPhaseModelWithOverrides to return the documented keys; search for usages of
getPhaseModelWithOverrides, confirm the de-structuring patterns, and update all
affected call sites or the helper return object (reference the
getPhaseModelWithOverrides function and the commit-message route usage) so
documentation and implementation are consistent.
🧹 Nitpick comments (6)
apps/ui/src/components/views/settings-view/api-keys/api-keys-section.tsx (1)

116-128: Terminology updated correctly. Minor wording suggestion.

The informational text has been successfully updated to reflect the new "Claude Compatible Providers" terminology. The guidance accurately describes the provider configuration workflow.

The sentence structure in the first paragraph (lines 118-123) is functional but slightly convoluted. Consider rephrasing for improved clarity:

✨ Optional wording improvement
                    <p>
                      <span className="font-medium text-foreground/80">
                        Using Claude Compatible Providers?
                      </span>{' '}
-                      Add a provider in <span className="text-blue-500">AI Providers → Claude</span>{' '}
-                      with{' '}
+                      To use this key, add a provider in <span className="text-blue-500">AI Providers → Claude</span>{' '}
+                      and select{' '}
                      <span className="font-mono text-[10px] bg-muted/50 px-1 rounded">
                        credentials
                      </span>{' '}
                      as the API key source to use this key.
+                      as the API key source.
                    </p>
libs/types/src/settings.ts (1)

340-356: Duplicate templateId may cause lookup issues.

Both MiniMax templates (international and China) share templateId: 'minimax'. If template lookup uses templateId as a key, only one will be found. Consider using distinct IDs like 'minimax' and 'minimax-china'.

♻️ Suggested fix
   {
-    templateId: 'minimax',
+    templateId: 'minimax-china',
     name: 'MiniMax (China)',
     providerType: 'minimax',
     baseUrl: 'https://api.minimaxi.com/anthropic',
apps/server/src/routes/context/routes/describe-image.ts (1)

296-299: Logger arguments may not concatenate as expected.

The logger.info call uses comma-separated arguments instead of template literals. Depending on the logger implementation, this may log as separate items rather than a single concatenated message.

Suggested fix for consistent logging
-      logger.info(
-        `[${requestId}] Using model: ${model}`,
-        provider ? `via provider: ${provider.name}` : 'direct API'
-      );
+      logger.info(
+        `[${requestId}] Using model: ${model} ${provider ? `via provider: ${provider.name}` : 'direct API'}`
+      );
apps/ui/src/components/views/project-settings-view/project-bulk-replace-dialog.tsx (3)

100-119: Helper function defined inside component may cause unnecessary re-renders.

getClaudeModelAlias is defined inside the component and used in the preview useMemo. Since it's recreated on every render, it could cause the useMemo to recompute unnecessarily, though React's dependency checking may not catch this since it's not listed in dependencies.

Consider either:

  1. Moving this function outside the component (it only depends on enabledProviders)
  2. Wrapping it in useCallback with [enabledProviders] dependency
  3. Including it in the useMemo dependencies (though this would still cause recomputation)
Option: Wrap in useCallback
-  // Get the Claude model alias from a PhaseModelEntry
-  const getClaudeModelAlias = (entry: PhaseModelEntry): ClaudeModelAlias => {
+  // Get the Claude model alias from a PhaseModelEntry
+  const getClaudeModelAlias = useCallback((entry: PhaseModelEntry): ClaudeModelAlias => {
     // Check if model string directly matches a Claude alias
     if (entry.model === 'haiku' || entry.model === 'claude-haiku') return 'haiku';
     if (entry.model === 'sonnet' || entry.model === 'claude-sonnet') return 'sonnet';
     if (entry.model === 'opus' || entry.model === 'claude-opus') return 'opus';

     // If it's a provider model, look up the mapping
     if (entry.providerId) {
       const provider = enabledProviders.find((p) => p.id === entry.providerId);
       if (provider) {
         const model = provider.models?.find((m) => m.id === entry.model);
         if (model?.mapsToClaudeModel) {
           return model.mapsToClaudeModel;
         }
       }
     }

     // Default to sonnet
     return 'sonnet';
-  };
+  }, [enabledProviders]);

140-147: Fallback behavior may be unexpected for users.

When a provider doesn't have a model mapping for a specific Claude tier (e.g., no Opus equivalent), the code falls back to the first available model (lines 141-143). This could be confusing—if a user expects Opus-tier performance but gets mapped to a random first model.

The warning UI at lines 273-290 does alert users about missing mappings, which helps. Consider whether the fallback should instead keep the current model unchanged or display a more specific warning per-phase in the preview.


199-206: Consider adding error handling for override persistence.

The handleApply function applies multiple overrides but doesn't handle potential errors from setProjectPhaseModelOverride. If one override fails to persist, the user won't be notified and the dialog will still close.

Suggested improvement with error handling
   const handleApply = () => {
-    preview.forEach(({ phase, newEntry, isChanged }) => {
-      if (isChanged) {
-        setProjectPhaseModelOverride(project.id, phase, newEntry);
-      }
-    });
-    onOpenChange(false);
+    try {
+      preview.forEach(({ phase, newEntry, isChanged }) => {
+        if (isChanged) {
+          setProjectPhaseModelOverride(project.id, phase, newEntry);
+        }
+      });
+      onOpenChange(false);
+    } catch (error) {
+      console.error('Failed to apply overrides:', error);
+      // Optionally show toast/notification to user
+    }
   };

Comment on lines +510 to +579
export async function getPhaseModelWithOverrides(
phase: PhaseModelKey,
settingsService: SettingsService,
projectPath?: string,
logPrefix = '[SettingsHelper]'
): Promise<PhaseModelWithOverridesResult> {
try {
const globalSettings = await settingsService.getGlobalSettings();
const credentials = await settingsService.getCredentials();
const globalPhaseModels = globalSettings.phaseModels || {};

// Start with global phase model
let phaseModel = globalPhaseModels[phase];
let isProjectOverride = false;

// Check for project override
if (projectPath) {
const projectSettings = await settingsService.getProjectSettings(projectPath);
const projectOverrides = projectSettings.phaseModelOverrides || {};

if (projectOverrides[phase]) {
phaseModel = projectOverrides[phase];
isProjectOverride = true;
logger.debug(`${logPrefix} Using project override for ${phase}`);
}
}

// If no phase model found, use a default
if (!phaseModel) {
phaseModel = { model: 'sonnet' };
logger.debug(`${logPrefix} No ${phase} configured, using default: sonnet`);
}

// Resolve provider if providerId is set
let provider: ClaudeCompatibleProvider | undefined;
if (phaseModel.providerId) {
const providers = globalSettings.claudeCompatibleProviders || [];
provider = providers.find((p) => p.id === phaseModel.providerId);

if (provider) {
if (provider.enabled === false) {
logger.warn(
`${logPrefix} Provider "${provider.name}" for ${phase} is disabled, falling back to direct API`
);
provider = undefined;
} else {
logger.debug(`${logPrefix} Using provider "${provider.name}" for ${phase}`);
}
} else {
logger.warn(
`${logPrefix} Provider ${phaseModel.providerId} not found for ${phase}, falling back to direct API`
);
}
}

return {
phaseModel,
isProjectOverride,
provider,
credentials,
};
} catch (error) {
logger.error(`${logPrefix} Failed to get phase model with overrides:`, error);
// Return a safe default
return {
phaseModel: { model: 'sonnet' },
isProjectOverride: false,
provider: undefined,
credentials: undefined,
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Strip providerId (or fallback) when provider is missing/disabled to avoid invalid direct calls.

resolvePhaseModel now bypasses Claude mapping when providerId is set. If the provider is missing/disabled, phaseModel still carries the provider ID and provider-specific model string, which can end up sent to the direct Anthropic SDK and fail. Consider clearing providerId and falling back to a safe default when provider resolution fails.

🐛 Proposed fix
-    if (phaseModel.providerId) {
+    if (phaseModel.providerId) {
+      const fallbackPhaseModel: PhaseModelEntry = {
+        model: 'sonnet',
+        thinkingLevel: phaseModel.thinkingLevel,
+        reasoningEffort: phaseModel.reasoningEffort,
+      };
       const providers = globalSettings.claudeCompatibleProviders || [];
       provider = providers.find((p) => p.id === phaseModel.providerId);

       if (provider) {
         if (provider.enabled === false) {
           logger.warn(
             `${logPrefix} Provider "${provider.name}" for ${phase} is disabled, falling back to direct API`
           );
-          provider = undefined;
+          provider = undefined;
+          phaseModel = fallbackPhaseModel;
         } else {
           logger.debug(`${logPrefix} Using provider "${provider.name}" for ${phase}`);
         }
       } else {
         logger.warn(
           `${logPrefix} Provider ${phaseModel.providerId} not found for ${phase}, falling back to direct API`
         );
+        phaseModel = fallbackPhaseModel;
       }
     }
📝 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
export async function getPhaseModelWithOverrides(
phase: PhaseModelKey,
settingsService: SettingsService,
projectPath?: string,
logPrefix = '[SettingsHelper]'
): Promise<PhaseModelWithOverridesResult> {
try {
const globalSettings = await settingsService.getGlobalSettings();
const credentials = await settingsService.getCredentials();
const globalPhaseModels = globalSettings.phaseModels || {};
// Start with global phase model
let phaseModel = globalPhaseModels[phase];
let isProjectOverride = false;
// Check for project override
if (projectPath) {
const projectSettings = await settingsService.getProjectSettings(projectPath);
const projectOverrides = projectSettings.phaseModelOverrides || {};
if (projectOverrides[phase]) {
phaseModel = projectOverrides[phase];
isProjectOverride = true;
logger.debug(`${logPrefix} Using project override for ${phase}`);
}
}
// If no phase model found, use a default
if (!phaseModel) {
phaseModel = { model: 'sonnet' };
logger.debug(`${logPrefix} No ${phase} configured, using default: sonnet`);
}
// Resolve provider if providerId is set
let provider: ClaudeCompatibleProvider | undefined;
if (phaseModel.providerId) {
const providers = globalSettings.claudeCompatibleProviders || [];
provider = providers.find((p) => p.id === phaseModel.providerId);
if (provider) {
if (provider.enabled === false) {
logger.warn(
`${logPrefix} Provider "${provider.name}" for ${phase} is disabled, falling back to direct API`
);
provider = undefined;
} else {
logger.debug(`${logPrefix} Using provider "${provider.name}" for ${phase}`);
}
} else {
logger.warn(
`${logPrefix} Provider ${phaseModel.providerId} not found for ${phase}, falling back to direct API`
);
}
}
return {
phaseModel,
isProjectOverride,
provider,
credentials,
};
} catch (error) {
logger.error(`${logPrefix} Failed to get phase model with overrides:`, error);
// Return a safe default
return {
phaseModel: { model: 'sonnet' },
isProjectOverride: false,
provider: undefined,
credentials: undefined,
};
export async function getPhaseModelWithOverrides(
phase: PhaseModelKey,
settingsService: SettingsService,
projectPath?: string,
logPrefix = '[SettingsHelper]'
): Promise<PhaseModelWithOverridesResult> {
try {
const globalSettings = await settingsService.getGlobalSettings();
const credentials = await settingsService.getCredentials();
const globalPhaseModels = globalSettings.phaseModels || {};
// Start with global phase model
let phaseModel = globalPhaseModels[phase];
let isProjectOverride = false;
// Check for project override
if (projectPath) {
const projectSettings = await settingsService.getProjectSettings(projectPath);
const projectOverrides = projectSettings.phaseModelOverrides || {};
if (projectOverrides[phase]) {
phaseModel = projectOverrides[phase];
isProjectOverride = true;
logger.debug(`${logPrefix} Using project override for ${phase}`);
}
}
// If no phase model found, use a default
if (!phaseModel) {
phaseModel = { model: 'sonnet' };
logger.debug(`${logPrefix} No ${phase} configured, using default: sonnet`);
}
// Resolve provider if providerId is set
let provider: ClaudeCompatibleProvider | undefined;
if (phaseModel.providerId) {
const fallbackPhaseModel: PhaseModelEntry = {
model: 'sonnet',
thinkingLevel: phaseModel.thinkingLevel,
reasoningEffort: phaseModel.reasoningEffort,
};
const providers = globalSettings.claudeCompatibleProviders || [];
provider = providers.find((p) => p.id === phaseModel.providerId);
if (provider) {
if (provider.enabled === false) {
logger.warn(
`${logPrefix} Provider "${provider.name}" for ${phase} is disabled, falling back to direct API`
);
provider = undefined;
phaseModel = fallbackPhaseModel;
} else {
logger.debug(`${logPrefix} Using provider "${provider.name}" for ${phase}`);
}
} else {
logger.warn(
`${logPrefix} Provider ${phaseModel.providerId} not found for ${phase}, falling back to direct API`
);
phaseModel = fallbackPhaseModel;
}
}
return {
phaseModel,
isProjectOverride,
provider,
credentials,
};
} catch (error) {
logger.error(`${logPrefix} Failed to get phase model with overrides:`, error);
// Return a safe default
return {
phaseModel: { model: 'sonnet' },
isProjectOverride: false,
provider: undefined,
credentials: undefined,
};
}
}
🤖 Prompt for AI Agents
In `@apps/server/src/lib/settings-helpers.ts` around lines 510 - 579, In
getPhaseModelWithOverrides, when resolving provider (function
getPhaseModelWithOverrides) ensure that if the provider is not found or
provider.enabled === false you strip phaseModel.providerId and reset
phaseModel.model to a safe direct-API fallback (e.g., 'sonnet') so
provider-specific model strings or stale provider IDs are not passed to the
direct Anthropic SDK; implement this right after the provider resolution
branches (where provider is set to undefined or missing) so the returned
phaseModel contains no providerId and uses a safe default model.

Comment on lines +170 to +191
// Check if the model is a provider model (like "GLM-4.5-Air")
// If so, get the provider config and resolved Claude model
let claudeCompatibleProvider: import('@automaker/types').ClaudeCompatibleProvider | undefined;
let providerResolvedModel: string | undefined;
let credentials = await settingsService?.getCredentials();

if (settingsService) {
const providerResult = await getProviderByModelId(model, settingsService, '[ValidateIssue]');
if (providerResult.provider) {
claudeCompatibleProvider = providerResult.provider;
providerResolvedModel = providerResult.resolvedModel;
credentials = providerResult.credentials;
logger.info(
`Using provider "${providerResult.provider.name}" for model "${model}"` +
(providerResolvedModel ? ` -> resolved to "${providerResolvedModel}"` : '')
);
}
}

// Get active Claude API profile for alternative endpoint configuration
const { profile: claudeApiProfile, credentials } = await getActiveClaudeApiProfile(
settingsService,
'[IssueValidation]',
projectPath
);
// Use provider resolved model if available, otherwise use original model
const effectiveModel = providerResolvedModel || (model as string);
logger.info(`Using model: ${effectiveModel}`);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect model validation helpers and ModelId typing for provider model support
rg -n "isClaudeModel|isCodexModel|isCursorModel|isOpencodeModel" -g '*.{ts,tsx,js,jsx}'
rg -n "type ModelId|export type ModelId" -g '*.{ts,tsx,js,jsx}'

Repository: AutoMaker-Org/automaker

Length of output: 5492


🏁 Script executed:

#!/bin/bash
# Check isClaudeModel implementation
head -80 libs/types/src/provider-utils.ts | tail -40

# Find useStructuredOutput in validate-issue.ts and check ordering
sed -n '120,200p' apps/server/src/routes/github/routes/validate-issue.ts

Repository: AutoMaker-Org/automaker

Length of output: 4636


🏁 Script executed:

#!/bin/bash
# See full streamingQuery call with useStructuredOutput
sed -n '200,240p' apps/server/src/routes/github/routes/validate-issue.ts

# Check if useStructuredOutput is used elsewhere in the file
grep -n "useStructuredOutput" apps/server/src/routes/github/routes/validate-issue.ts

Repository: AutoMaker-Org/automaker

Length of output: 1872


🏁 Script executed:

#!/bin/bash
# Check the validation at lines 358-361 and surrounding context
sed -n '350,370p' apps/server/src/routes/github/routes/validate-issue.ts

# Check if there's validation that rejects the original model before provider resolution
sed -n '1,130p' apps/server/src/routes/github/routes/validate-issue.ts | tail -40

Repository: AutoMaker-Org/automaker

Length of output: 1980


🏁 Script executed:

#!/bin/bash
# Find getProviderByModelId to understand its contract
rg -n "export.*getProviderByModelId|function getProviderByModelId" -g '*.{ts,tsx,js,jsx}'

# Check if provider models should be recognized differently
rg -n "getProviderByModelId" -B 2 -A 2 apps/server/src/routes/github/routes/validate-issue.ts | head -30

Repository: AutoMaker-Org/automaker

Length of output: 549


🏁 Script executed:

#!/bin/bash
# Check getProviderByModelId implementation
sed -n '608,680p' apps/lib/settings-helpers.ts

Repository: AutoMaker-Org/automaker

Length of output: 138


🏁 Script executed:

#!/bin/bash
# Find the correct path for settings-helpers
find . -name "settings-helpers.ts" -type f

# Get the getProviderByModelId implementation
rg -n "export async function getProviderByModelId" -A 50 -g '*.{ts,tsx,js,jsx}'

Repository: AutoMaker-Org/automaker

Length of output: 4244


🏁 Script executed:

#!/bin/bash
# Check the exact context - is line 358-361 in route handler or function?
sed -n '340,375p' apps/server/src/routes/github/routes/validate-issue.ts

Repository: AutoMaker-Org/automaker

Length of output: 1266


🏁 Script executed:

#!/bin/bash
# Confirm: are provider models supposed to be valid?
# Check if provider models should pass validation or if they need special handling
rg -n "claudeCompatibleProviders|ClaudeCompatibleProvider" -A 3 -B 1 libs/types/src/index.ts | head -30

# Check settings type to understand provider model structure
rg -n "type.*ClaudeCompatibleProvider|interface.*ClaudeCompatibleProvider" -A 10 -g '*.{ts,tsx}'

Repository: AutoMaker-Org/automaker

Length of output: 10840


🏁 Script executed:

#!/bin/bash
# Check how other endpoints handle provider model validation
# Look at enhance.ts to see if it has the same validation pattern
sed -n '70,130p' apps/server/src/routes/enhance-prompt/routes/enhance.ts

Repository: AutoMaker-Org/automaker

Length of output: 2572


🏁 Script executed:

#!/bin/bash
# Check if enhance.ts has model validation like validate-issue.ts does
rg -n "isClaudeModel|isCursorModel|isCodexModel|isOpencodeModel" apps/server/src/routes/enhance-prompt/routes/enhance.ts | head -20

Repository: AutoMaker-Org/automaker

Length of output: 49


🏁 Script executed:

#!/bin/bash
# Check the complete picture - confirm the validation order
# Show the full route handler structure to see validation vs handler function call
sed -n '330,410p' apps/server/src/routes/github/routes/validate-issue.ts

Repository: AutoMaker-Org/automaker

Length of output: 2539


Fix validation gate to recognize provider models before rejecting them.

Provider model IDs (like "GLM-4.5-Air") fail validation at lines 358–369 and return 400 immediately, never reaching the provider resolution code at lines 170–191. The validation checks isClaudeModel(model) || isCursorModel(model) || isCodexModel(model) || isOpencodeModel(model), but provider models don't match these checks since they contain no "claude-" string and aren't recognized aliases.

Resolve the provider model before validation, or validate provider models via getProviderByModelId first, then validate the resolved model. This also prevents the secondary issue where useStructuredOutput (line 128) would skip JSON schema output even for Claude-compatible providers.

🤖 Prompt for AI Agents
In `@apps/server/src/routes/github/routes/validate-issue.ts` around lines 170 -
191, The validation gate currently runs
isClaudeModel/isCursorModel/isCodexModel/isOpencodeModel before provider
resolution, causing provider model IDs (e.g., "GLM-4.5-Air") to be rejected;
change the flow so you call getProviderByModelId(model, settingsService, ...)
first (as in the block that sets claudeCompatibleProvider,
providerResolvedModel, credentials and effectiveModel) and then run the
validation against the resolved model (providerResolvedModel || model), and
ensure useStructuredOutput uses the Claude-compatible provider check
(claudeCompatibleProvider or isClaudeModel(resolvedModel)) rather than the
original raw model string. This ensures provider models are recognized and
structured output is correctly applied.

Comment on lines +91 to +192
function PhaseOverrideItem({
phase,
project,
globalValue,
projectOverride,
}: {
phase: PhaseConfig;
project: Project;
globalValue: PhaseModelEntry;
projectOverride?: PhaseModelEntry;
}) {
const { setProjectPhaseModelOverride, claudeCompatibleProviders } = useAppStore();

const hasOverride = !!projectOverride;
const effectiveValue = projectOverride || globalValue;

// Get display name for a model
const getModelDisplayName = (entry: PhaseModelEntry): string => {
if (entry.providerId) {
const provider = (claudeCompatibleProviders || []).find((p) => p.id === entry.providerId);
if (provider) {
const model = provider.models?.find((m) => m.id === entry.model);
if (model) {
return `${model.displayName} (${provider.name})`;
}
}
}
// Default to model ID for built-in models
const modelMap: Record<string, string> = {
haiku: 'Claude Haiku',
sonnet: 'Claude Sonnet',
opus: 'Claude Opus',
};
return modelMap[entry.model] || entry.model;
};

const handleClearOverride = () => {
setProjectPhaseModelOverride(project.id, phase.key, null);
};

const handleSetOverride = (entry: PhaseModelEntry) => {
setProjectPhaseModelOverride(project.id, phase.key, entry);
};

return (
<div
className={cn(
'flex items-center justify-between p-4 rounded-xl',
'bg-accent/20 border',
hasOverride ? 'border-brand-500/30 bg-brand-500/5' : 'border-border/30',
'hover:bg-accent/30 transition-colors'
)}
>
<div className="flex-1 pr-4">
<div className="flex items-center gap-2">
<h4 className="text-sm font-medium text-foreground">{phase.label}</h4>
{hasOverride ? (
<span className="px-1.5 py-0.5 text-[10px] font-medium rounded bg-brand-500/20 text-brand-500">
Override
</span>
) : (
<span className="flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium rounded bg-muted text-muted-foreground">
<Globe className="w-3 h-3" />
Global
</span>
)}
</div>
<p className="text-xs text-muted-foreground">{phase.description}</p>
{hasOverride && (
<p className="text-xs text-brand-500 mt-1">
Using: {getModelDisplayName(effectiveValue)}
</p>
)}
{!hasOverride && (
<p className="text-xs text-muted-foreground/70 mt-1">
Using global: {getModelDisplayName(globalValue)}
</p>
)}
</div>

<div className="flex items-center gap-2">
{hasOverride && (
<Button
variant="ghost"
size="sm"
onClick={handleClearOverride}
className="h-8 px-2 text-xs text-muted-foreground hover:text-foreground"
>
<RotateCcw className="w-3.5 h-3.5 mr-1" />
Reset
</Button>
)}
<PhaseModelSelector
compact
value={effectiveValue}
onChange={handleSetOverride}
align="end"
/>
</div>
</div>
);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix Claude display names for default IDs.

getModelDisplayName only maps haiku/sonnet/opus, so defaults like claude-sonnet render as raw IDs. Add claude-* keys (and keep fallback).

🩹 Proposed fix
-    const modelMap: Record<string, string> = {
-      haiku: 'Claude Haiku',
-      sonnet: 'Claude Sonnet',
-      opus: 'Claude Opus',
-    };
+    const modelMap: Record<string, string> = {
+      haiku: 'Claude Haiku',
+      sonnet: 'Claude Sonnet',
+      opus: 'Claude Opus',
+      'claude-haiku': 'Claude Haiku',
+      'claude-sonnet': 'Claude Sonnet',
+      'claude-opus': 'Claude Opus',
+    };
🤖 Prompt for AI Agents
In
`@apps/ui/src/components/views/project-settings-view/project-models-section.tsx`
around lines 91 - 192, The getModelDisplayName function in PhaseOverrideItem
currently maps only 'haiku', 'sonnet', 'opus' and thus shows raw IDs like
'claude-sonnet'; update the modelMap inside getModelDisplayName to also include
the 'claude-haiku', 'claude-sonnet', and 'claude-opus' keys (mapping to 'Claude
Haiku', 'Claude Sonnet', 'Claude Opus' respectively) while keeping the existing
fallback to entry.model so any unknown IDs still render as-is.

Comment on lines +58 to +110
export function BulkReplaceDialog({ open, onOpenChange }: BulkReplaceDialogProps) {
const { phaseModels, setPhaseModel, claudeCompatibleProviders } = useAppStore();
const [selectedProvider, setSelectedProvider] = useState<string>('anthropic');

// Get enabled providers
const enabledProviders = useMemo(() => {
return (claudeCompatibleProviders || []).filter((p) => p.enabled !== false);
}, [claudeCompatibleProviders]);

// Build provider options for the dropdown
const providerOptions = useMemo(() => {
const options: Array<{ id: string; name: string; isNative: boolean }> = [
{ id: 'anthropic', name: 'Anthropic Direct', isNative: true },
];

enabledProviders.forEach((provider) => {
options.push({
id: provider.id,
name: provider.name,
isNative: false,
});
});

return options;
}, [enabledProviders]);

// Get the selected provider config (if custom)
const selectedProviderConfig = useMemo(() => {
if (selectedProvider === 'anthropic') return null;
return enabledProviders.find((p) => p.id === selectedProvider);
}, [selectedProvider, enabledProviders]);

// Get the Claude model alias from a PhaseModelEntry
const getClaudeModelAlias = (entry: PhaseModelEntry): ClaudeModelAlias => {
// Check if model string directly matches a Claude alias
if (entry.model === 'haiku' || entry.model === 'claude-haiku') return 'haiku';
if (entry.model === 'sonnet' || entry.model === 'claude-sonnet') return 'sonnet';
if (entry.model === 'opus' || entry.model === 'claude-opus') return 'opus';

// If it's a provider model, look up the mapping
if (entry.providerId) {
const provider = enabledProviders.find((p) => p.id === entry.providerId);
if (provider) {
const model = provider.models?.find((m) => m.id === entry.model);
if (model?.mapsToClaudeModel) {
return model.mapsToClaudeModel;
}
}
}

// Default to sonnet
return 'sonnet';
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix Claude tier detection for full model IDs.

getClaudeModelAlias only matches exact claude-haiku/sonnet/opus. If a phase is configured with full model IDs (e.g., claude-opus-4-5-20251101), the code defaults to sonnet, causing incorrect bulk mapping.

🩹 Proposed fix
-  const getClaudeModelAlias = (entry: PhaseModelEntry): ClaudeModelAlias => {
-    // Check if model string directly matches a Claude alias
-    if (entry.model === 'haiku' || entry.model === 'claude-haiku') return 'haiku';
-    if (entry.model === 'sonnet' || entry.model === 'claude-sonnet') return 'sonnet';
-    if (entry.model === 'opus' || entry.model === 'claude-opus') return 'opus';
+  const getClaudeModelAlias = (entry: PhaseModelEntry): ClaudeModelAlias => {
+    const modelId = entry.model.toLowerCase();
+    // Check if model string directly matches a Claude alias or full model id
+    if (modelId === 'haiku' || modelId.startsWith('claude-haiku')) return 'haiku';
+    if (modelId === 'sonnet' || modelId.startsWith('claude-sonnet')) return 'sonnet';
+    if (modelId === 'opus' || modelId.startsWith('claude-opus')) return 'opus';
🤖 Prompt for AI Agents
In
`@apps/ui/src/components/views/settings-view/model-defaults/bulk-replace-dialog.tsx`
around lines 58 - 110, getClaudeModelAlias fails to recognize extended Claude
model IDs (like "claude-opus-4-5-20251101") because it only checks exact
strings; update getClaudeModelAlias to match prefixes instead of exact equality
(e.g., use startsWith or a regex) for the three checks (haiku, sonnet, opus) so
values like "claude-opus-..." or "opus-..." map to 'opus'; keep the existing
provider lookup for custom provider models unchanged.

Comment on lines 209 to 243
const handleSave = () => {
const profileData: ClaudeApiProfile = {
id: editingProfileId ?? generateProfileId(),
// For GLM/MiniMax, enforce fixed settings
const isFixedProvider = hasFixedSettings(formData.providerType);

// Convert form models to ProviderModel format
const models: ProviderModel[] = formData.models
.filter((m) => m.id.trim()) // Only include models with IDs
.map((m) => ({
id: m.id.trim(),
displayName: m.displayName.trim() || m.id.trim(),
mapsToClaudeModel: m.mapsToClaudeModel,
}));

const providerData: ClaudeCompatibleProvider = {
id: editingProviderId ?? generateProviderId(),
name: formData.name.trim(),
providerType: formData.providerType,
enabled: true,
baseUrl: formData.baseUrl.trim(),
apiKeySource: formData.apiKeySource,
// For fixed providers, always use inline
apiKeySource: isFixedProvider ? 'inline' : formData.apiKeySource,
// Only include apiKey when source is 'inline'
apiKey: formData.apiKeySource === 'inline' ? formData.apiKey : undefined,
useAuthToken: formData.useAuthToken,
apiKey: isFixedProvider || formData.apiKeySource === 'inline' ? formData.apiKey : undefined,
// For fixed providers, always use auth token
useAuthToken: isFixedProvider ? true : formData.useAuthToken,
timeoutMs: (() => {
const parsed = Number(formData.timeoutMs);
return Number.isFinite(parsed) ? parsed : undefined;
})(),
modelMappings:
formData.modelMappings.haiku || formData.modelMappings.sonnet || formData.modelMappings.opus
? {
...(formData.modelMappings.haiku && { haiku: formData.modelMappings.haiku }),
...(formData.modelMappings.sonnet && { sonnet: formData.modelMappings.sonnet }),
...(formData.modelMappings.opus && { opus: formData.modelMappings.opus }),
}
: undefined,
disableNonessentialTraffic: formData.disableNonessentialTraffic || undefined,
models,
// For fixed providers, always disable non-essential
disableNonessentialTraffic: isFixedProvider
? true
: formData.disableNonessentialTraffic || undefined,
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Preserve provider enabled state on edit.

enabled: true is always set on save, so editing a disabled provider re-enables it unintentionally. Preserve the prior enabled state when editing.

🐛 Suggested fix
-    const providerData: ClaudeCompatibleProvider = {
-      id: editingProviderId ?? generateProviderId(),
-      name: formData.name.trim(),
-      providerType: formData.providerType,
-      enabled: true,
+    const existingProvider = editingProviderId
+      ? claudeCompatibleProviders.find((p) => p.id === editingProviderId)
+      : undefined;
+    const providerData: ClaudeCompatibleProvider = {
+      id: editingProviderId ?? generateProviderId(),
+      name: formData.name.trim(),
+      providerType: formData.providerType,
+      enabled: existingProvider?.enabled ?? true,
📝 Committable suggestion

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

Suggested change
const handleSave = () => {
const profileData: ClaudeApiProfile = {
id: editingProfileId ?? generateProfileId(),
// For GLM/MiniMax, enforce fixed settings
const isFixedProvider = hasFixedSettings(formData.providerType);
// Convert form models to ProviderModel format
const models: ProviderModel[] = formData.models
.filter((m) => m.id.trim()) // Only include models with IDs
.map((m) => ({
id: m.id.trim(),
displayName: m.displayName.trim() || m.id.trim(),
mapsToClaudeModel: m.mapsToClaudeModel,
}));
const providerData: ClaudeCompatibleProvider = {
id: editingProviderId ?? generateProviderId(),
name: formData.name.trim(),
providerType: formData.providerType,
enabled: true,
baseUrl: formData.baseUrl.trim(),
apiKeySource: formData.apiKeySource,
// For fixed providers, always use inline
apiKeySource: isFixedProvider ? 'inline' : formData.apiKeySource,
// Only include apiKey when source is 'inline'
apiKey: formData.apiKeySource === 'inline' ? formData.apiKey : undefined,
useAuthToken: formData.useAuthToken,
apiKey: isFixedProvider || formData.apiKeySource === 'inline' ? formData.apiKey : undefined,
// For fixed providers, always use auth token
useAuthToken: isFixedProvider ? true : formData.useAuthToken,
timeoutMs: (() => {
const parsed = Number(formData.timeoutMs);
return Number.isFinite(parsed) ? parsed : undefined;
})(),
modelMappings:
formData.modelMappings.haiku || formData.modelMappings.sonnet || formData.modelMappings.opus
? {
...(formData.modelMappings.haiku && { haiku: formData.modelMappings.haiku }),
...(formData.modelMappings.sonnet && { sonnet: formData.modelMappings.sonnet }),
...(formData.modelMappings.opus && { opus: formData.modelMappings.opus }),
}
: undefined,
disableNonessentialTraffic: formData.disableNonessentialTraffic || undefined,
models,
// For fixed providers, always disable non-essential
disableNonessentialTraffic: isFixedProvider
? true
: formData.disableNonessentialTraffic || undefined,
};
const handleSave = () => {
// For GLM/MiniMax, enforce fixed settings
const isFixedProvider = hasFixedSettings(formData.providerType);
// Convert form models to ProviderModel format
const models: ProviderModel[] = formData.models
.filter((m) => m.id.trim()) // Only include models with IDs
.map((m) => ({
id: m.id.trim(),
displayName: m.displayName.trim() || m.id.trim(),
mapsToClaudeModel: m.mapsToClaudeModel,
}));
const existingProvider = editingProviderId
? claudeCompatibleProviders.find((p) => p.id === editingProviderId)
: undefined;
const providerData: ClaudeCompatibleProvider = {
id: editingProviderId ?? generateProviderId(),
name: formData.name.trim(),
providerType: formData.providerType,
enabled: existingProvider?.enabled ?? true,
baseUrl: formData.baseUrl.trim(),
// For fixed providers, always use inline
apiKeySource: isFixedProvider ? 'inline' : formData.apiKeySource,
// Only include apiKey when source is 'inline'
apiKey: isFixedProvider || formData.apiKeySource === 'inline' ? formData.apiKey : undefined,
// For fixed providers, always use auth token
useAuthToken: isFixedProvider ? true : formData.useAuthToken,
timeoutMs: (() => {
const parsed = Number(formData.timeoutMs);
return Number.isFinite(parsed) ? parsed : undefined;
})(),
models,
// For fixed providers, always disable non-essential
disableNonessentialTraffic: isFixedProvider
? true
: formData.disableNonessentialTraffic || undefined,
};
🤖 Prompt for AI Agents
In
`@apps/ui/src/components/views/settings-view/providers/claude-settings-tab/api-profiles-section.tsx`
around lines 209 - 243, The save routine always sets providerData.enabled = true
which re-enables a provider when editing; instead, preserve the existing enabled
value for edits by using the original provider's enabled flag when
editingProviderId is present. Update handleSave to set providerData.enabled to
the existing provider's enabled state (e.g., editingProvider.enabled) if
editingProviderId/editingProvider is available, otherwise default to true for
new providers; locate the providerData construction in handleSave and replace
the hardcoded true with editingProvider?.enabled ?? true (or the equivalent
lookup of the original provider by editingProviderId).

Comment on lines +98 to 137
// Apply activeClaudeApiProfileId and phaseModelOverrides if present
// These are stored directly on the project, so we need to update both
// currentProject AND the projects array to keep them in sync
// Type assertion needed because API returns Record<string, unknown>
const settingsWithExtras = settings as Record<string, unknown>;
const activeClaudeApiProfileId = settingsWithExtras.activeClaudeApiProfileId as
| string
| null
| undefined;
const phaseModelOverrides = settingsWithExtras.phaseModelOverrides as
| import('@automaker/types').PhaseModelConfig
| undefined;

// Check if we need to update the project
const storeState = useAppStore.getState();
const updatedProject = storeState.currentProject;
if (updatedProject && updatedProject.path === projectPath) {
const needsUpdate =
(activeClaudeApiProfileId !== undefined &&
updatedProject.activeClaudeApiProfileId !== activeClaudeApiProfileId) ||
(phaseModelOverrides !== undefined &&
JSON.stringify(updatedProject.phaseModelOverrides) !==
JSON.stringify(phaseModelOverrides));

if (needsUpdate) {
const updatedProjectData = {
...updatedProject,
activeClaudeApiProfileId: settings.activeClaudeApiProfileId,
});
...(activeClaudeApiProfileId !== undefined && { activeClaudeApiProfileId }),
...(phaseModelOverrides !== undefined && { phaseModelOverrides }),
};

// Update currentProject
setCurrentProject(updatedProjectData);

// Also update the project in the projects array to keep them in sync
const updatedProjects = storeState.projects.map((p) =>
p.id === updatedProject.id ? updatedProjectData : p
);
useAppStore.setState({ projects: updatedProjects });
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Clear overrides when settings omit the fields to avoid stale project state.

Right now updates only occur when activeClaudeApiProfileId / phaseModelOverrides are defined. If a project removes these fields, the store retains stale overrides. Consider treating “field missing” as an explicit clear.

🛠️ Proposed fix
-    const settingsWithExtras = settings as Record<string, unknown>;
-    const activeClaudeApiProfileId = settingsWithExtras.activeClaudeApiProfileId as
-      | string
-      | null
-      | undefined;
-    const phaseModelOverrides = settingsWithExtras.phaseModelOverrides as
-      | import('@automaker/types').PhaseModelConfig
-      | undefined;
+    const settingsWithExtras = settings as Record<string, unknown>;
+    const hasActiveClaudeApiProfileId = Object.prototype.hasOwnProperty.call(
+      settingsWithExtras,
+      'activeClaudeApiProfileId'
+    );
+    const activeClaudeApiProfileId = hasActiveClaudeApiProfileId
+      ? (settingsWithExtras.activeClaudeApiProfileId as string | null)
+      : undefined;
+    const hasPhaseModelOverrides = Object.prototype.hasOwnProperty.call(
+      settingsWithExtras,
+      'phaseModelOverrides'
+    );
+    const phaseModelOverrides = hasPhaseModelOverrides
+      ? (settingsWithExtras.phaseModelOverrides as
+          | Partial<import('@automaker/types').PhaseModelConfig>
+          | undefined)
+      : undefined;
@@
-    const needsUpdate =
-      (activeClaudeApiProfileId !== undefined &&
-        updatedProject.activeClaudeApiProfileId !== activeClaudeApiProfileId) ||
-      (phaseModelOverrides !== undefined &&
-        JSON.stringify(updatedProject.phaseModelOverrides) !==
-          JSON.stringify(phaseModelOverrides));
+    const needsUpdate =
+      (hasActiveClaudeApiProfileId &&
+        updatedProject.activeClaudeApiProfileId !== activeClaudeApiProfileId) ||
+      (!hasActiveClaudeApiProfileId && updatedProject.activeClaudeApiProfileId !== undefined) ||
+      (hasPhaseModelOverrides &&
+        JSON.stringify(updatedProject.phaseModelOverrides) !==
+          JSON.stringify(phaseModelOverrides)) ||
+      (!hasPhaseModelOverrides && updatedProject.phaseModelOverrides !== undefined);
@@
-        const updatedProjectData = {
+        const updatedProjectData = {
           ...updatedProject,
-          ...(activeClaudeApiProfileId !== undefined && { activeClaudeApiProfileId }),
-          ...(phaseModelOverrides !== undefined && { phaseModelOverrides }),
+          ...(hasActiveClaudeApiProfileId
+            ? { activeClaudeApiProfileId }
+            : { activeClaudeApiProfileId: undefined }),
+          ...(hasPhaseModelOverrides
+            ? { phaseModelOverrides }
+            : { phaseModelOverrides: undefined }),
         };
🤖 Prompt for AI Agents
In `@apps/ui/src/hooks/use-project-settings-loader.ts` around lines 98 - 137, The
code only updates project fields when
activeClaudeApiProfileId/phaseModelOverrides are defined, so removals leave
stale values; change the logic in use-project-settings-loader.ts to detect
presence of the keys on settingsWithExtras (e.g., "'activeClaudeApiProfileId' in
settingsWithExtras" and "'phaseModelOverrides' in settingsWithExtras") instead
of checking !== undefined, update needsUpdate to consider presence + difference
(including presence meaning "should be cleared"), and build updatedProjectData
to explicitly set activeClaudeApiProfileId to null (or undefined per your model)
when the key is present but value is null, or to remove/clear
phaseModelOverrides when the key is present and value is undefined/null; then
call setCurrentProject(updatedProjectData) and useAppStore.setState({ projects:
updatedProjects }) as before so both currentProject and the projects array
reflect clears as well.

Comment on lines +149 to +164
The `getPhaseModelWithOverrides()` helper gets effective phase model config:

Added an informational note:
```typescript
export async function getPhaseModelWithOverrides(
phaseKey: PhaseModelKey,
settingsService: SettingsService,
projectPath?: string,
logPrefix?: string
): Promise<{
model: string;
thinkingLevel?: ThinkingLevel;
providerId?: string;
providerConfig?: ClaudeCompatibleProvider;
credentials?: Credentials;
}>;
```
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Validate getPhaseModelWithOverrides return shape in docs.
This section documents { model, thinkingLevel, providerId, providerConfig, credentials }, but call sites (e.g., commit-message route) destructure phaseModel and provider. Please confirm the helper’s actual signature and update this section to avoid drift.

#!/bin/bash
# Verify getPhaseModelWithOverrides signature and usage.
rg -n -C2 "getPhaseModelWithOverrides"
🤖 Prompt for AI Agents
In `@docs/UNIFIED_API_KEY_PROFILES.md` around lines 149 - 164, The docs list
getPhaseModelWithOverrides as returning { model, thinkingLevel, providerId,
providerConfig, credentials } but call sites (e.g., commit-message route)
destructure values as phaseModel and provider; reconcile by verifying the actual
function signature of getPhaseModelWithOverrides and updating the docs or the
function so they match: either change the docs to document the real return shape
(e.g., { phaseModel, provider, ... } if that is what the function returns) or
modify getPhaseModelWithOverrides to return the documented keys; search for
usages of getPhaseModelWithOverrides, confirm the de-structuring patterns, and
update all affected call sites or the helper return object (reference the
getPhaseModelWithOverrides function and the commit-message route usage) so
documentation and implementation are consistent.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants