fix claude-code setting loading/persistence

This commit is contained in:
Max Paulus 🥪
2026-06-24 14:11:31 +09:00
committed by Dominic Cooney
parent 5363b2a6d2
commit e4ab360c3c
4 changed files with 98 additions and 42 deletions
@@ -238,6 +238,25 @@ describe("createProviderConfigStore", () => {
})
})
it("persists Claude Code model selections to providers.json", async () => {
const { createProviderConfigStore } = await import("./store")
const store = createProviderConfigStore()
const providerId = parseProviderId("claude-code")
const selection = { providerId, modelId: "haiku", modelInfo: modelInfoA }
store.commitSelection(providerId, "act", selection)
expect(mocks.getSavedProviderSettings("claude-code")).toMatchObject({
provider: "claude-code",
model: "haiku",
contextWindow: 128_000,
maxTokens: 8_192,
})
expect(mocks.getSaveProviderSettingsMock()).toHaveBeenCalledWith(expect.objectContaining({ model: "haiku" }), {
setLastUsed: false,
})
})
it("subscribers fire synchronously and multiple writes emit events in order", async () => {
const { createProviderConfigStore } = await import("./store")
const store = createProviderConfigStore()
@@ -134,6 +134,26 @@ describe("hasUsableProvider", () => {
expect(hasUsableProvider(config, "act")).toBe(false)
})
it("treats Claude Code as usable through local Claude CLI auth when a model is configured", () => {
mocks.providerSettingsManager.getProviderSettings.mockReturnValue(undefined)
const config: ApiConfiguration = {
actModeApiProvider: "claude-code",
actModeApiModelId: "sonnet",
}
expect(hasUsableProvider(config, "act")).toBe(true)
})
it("treats Claude Code as usable when its model is stored in providers.json", () => {
mocks.providerSettingsManager.getProviderSettings.mockReturnValue({
provider: "claude-code",
model: "haiku",
})
const config: ApiConfiguration = {
actModeApiProvider: "claude-code",
}
expect(hasUsableProvider(config, "act")).toBe(true)
})
it("treats ollama (keyless local) as usable when a model is configured, without a key", () => {
const config: ApiConfiguration = {
actModeApiProvider: "ollama",
+29 -16
View File
@@ -20,15 +20,15 @@ import { toSdkProviderId } from "./model-catalog/sdk-provider-id"
import { getProviderSettingsManager } from "./provider-migration"
/**
* Local-inference providers that the legacy ApiConfiguration path treats as
* keyless: they run against a local/self-hosted endpoint and need no API key,
* only a selected model (and optionally a base URL). The SDK catalog models
* ollama/lmstudio as "api-key" providers (they accept an optional key), so the
* catalog's authMethod alone does not classify them as keyless — they are
* listed here explicitly. `vscode-lm` is a VSCode-host provider that is not in
* the SDK builtin catalog at all.
* Local/external-auth providers that the legacy ApiConfiguration path treats as
* keyless: they need no API key in Cline, only a selected model (and optionally
* a base URL). The SDK catalog models ollama/lmstudio as "api-key" providers
* because they accept an optional key; Claude Code authenticates through the
* local Claude CLI; and `vscode-lm` is a VSCode-host provider that is not in
* the SDK builtin catalog at all. List them here when the catalog auth method
* cannot classify them as keyless.
*/
const KEYLESS_LOCAL_PROVIDERS = new Set<string>(["ollama", "lmstudio", "vscode-lm"])
const KEYLESS_LOCAL_PROVIDERS = new Set<string>(["ollama", "lmstudio", "vscode-lm", "claude-code"])
/**
* Whether a provider can be usable WITHOUT an API key, on the strength of a
@@ -66,18 +66,31 @@ function isKeylessViaModelProvider(providerId: string): boolean {
return getProviderAuthMethod(providerId) === "local"
}
function hasProviderSettingsAuthCredential(providerId: string): boolean {
function readProviderSettings(providerId: string): unknown {
try {
const settings = getProviderSettingsManager().getProviderSettings(providerId)
const auth =
settings && typeof settings === "object" ? (settings as { auth?: { accessToken?: unknown } }).auth : undefined
return typeof auth?.accessToken === "string" && auth.accessToken.trim().length > 0
return getProviderSettingsManager().getProviderSettings(providerId)
} catch {
Logger.warn(`[ProviderUsability] Failed to read provider settings auth credentials for provider ${providerId}`)
return false
Logger.warn(`[ProviderUsability] Failed to read provider settings for provider ${providerId}`)
return undefined
}
}
function hasProviderSettingsAuthCredential(providerId: string): boolean {
const settings = readProviderSettings(providerId)
const auth = settings && typeof settings === "object" ? (settings as { auth?: { accessToken?: unknown } }).auth : undefined
return typeof auth?.accessToken === "string" && auth.accessToken.trim().length > 0
}
function readProviderSettingsModelId(providerId: string): string | undefined {
const settings = readProviderSettings(providerId)
const model = settings && typeof settings === "object" ? (settings as { model?: unknown }).model : undefined
return typeof model === "string" && model.trim().length > 0 ? model.trim() : undefined
}
function resolveConfiguredModelId(providerId: string, mode: Mode, apiConfig: ApiConfiguration): string | undefined {
return resolveModelId(providerId, mode, apiConfig) ?? readProviderSettingsModelId(providerId)
}
/**
* Whether the Bedrock provider has usable auth. resolveApiKey() only sees the
* Bedrock API key; profile / SigV4 / default-chain auth also build a working
@@ -140,7 +153,7 @@ export function hasUsableProvider(apiConfig: ApiConfiguration, mode: Mode): bool
// Keyless local/local-auth providers are usable once a model is
// configured for the mode (no API key required).
if (isKeylessViaModelProvider(providerId)) {
const modelId = resolveModelId(providerId, mode, apiConfig)
const modelId = resolveConfiguredModelId(providerId, mode, apiConfig)
return typeof modelId === "string" && modelId.trim().length > 0
}
@@ -1,6 +1,9 @@
import { openAiModelInfoSafeDefaults } from "@shared/api"
import { Mode } from "@shared/storage/types"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useStaticProviderSelection } from "@/hooks/useStaticProviderSelection"
import { useProviderConfig } from "@/hooks/useProviderConfig"
import { useProviderModelSelection } from "@/hooks/useProviderModelSelection"
import { useProviderModels } from "@/hooks/useProviderModels"
import { DebouncedTextField } from "../common/DebouncedTextField"
import { ModelInfoView } from "../common/ModelInfoView"
import { ModelSelector } from "../common/ModelSelector"
@@ -36,14 +39,31 @@ interface ClaudeCodeProviderProps {
*/
export const ClaudeCodeProvider = ({ showModelOptions, isPopup, currentMode }: ClaudeCodeProviderProps) => {
const { apiConfiguration } = useExtensionState()
const { handleFieldChange, handleModeFieldChange } = useApiConfigurationHandlers()
const { handleFieldChange } = useApiConfigurationHandlers()
const providerId = "claude-code"
const { models, defaultModelId } = useProviderModels(providerId)
const { config, commitSelection } = useProviderConfig(providerId)
const { selectedModelId, selectedModelInfo, commitModelSelection } = useProviderModelSelection(providerId, currentMode, {
models,
defaultModelId,
config,
commitSelection,
})
// Get the normalized configuration
const { models, selectedModelId, selectedModelInfo, hideUsageCost } = useStaticProviderSelection(
"claude-code",
apiConfiguration,
currentMode,
)
const handleModelSelect = (event: {
target?: { value?: unknown }
currentTarget?: { value?: unknown }
detail?: { value?: unknown }
}) => {
const modelId = event.target?.value ?? event.currentTarget?.value ?? event.detail?.value
if (typeof modelId !== "string" || modelId.length === 0) {
return
}
void commitModelSelection({
modelId,
modelInfo: models[modelId] ?? selectedModelInfo ?? openAiModelInfoSafeDefaults,
}).catch((err) => console.error("Failed to commit Claude Code model selection:", err))
}
return (
<div>
@@ -67,18 +87,7 @@ export const ClaudeCodeProvider = ({ showModelOptions, isPopup, currentMode }: C
{showModelOptions && (
<>
<ModelSelector
label="Model"
models={models}
onChange={(e: any) =>
handleModeFieldChange(
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
e.target.value,
currentMode,
)
}
selectedModelId={selectedModelId}
/>
<ModelSelector label="Model" models={models} onChange={handleModelSelect} selectedModelId={selectedModelId} />
{(selectedModelId === "sonnet" || selectedModelId === "opus") && (
<p
@@ -96,12 +105,7 @@ export const ClaudeCodeProvider = ({ showModelOptions, isPopup, currentMode }: C
<ThinkingBudgetSlider currentMode={currentMode} maxBudget={selectedModelInfo.thinkingConfig?.maxBudget} />
)}
<ModelInfoView
hideUsageCost={hideUsageCost}
isPopup={isPopup}
modelInfo={selectedModelInfo}
selectedModelId={selectedModelId}
/>
<ModelInfoView isPopup={isPopup} modelInfo={selectedModelInfo} selectedModelId={selectedModelId} />
</>
)}
</div>