mirror of
https://github.com/cline/cline.git
synced 2026-09-21 13:21:23 +08:00
fix claude-code setting loading/persistence
This commit is contained in:
committed by
Dominic Cooney
parent
5363b2a6d2
commit
e4ab360c3c
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user