Compare commits

...
2 changed files with 107 additions and 2 deletions
@@ -337,6 +337,87 @@ describe("ProviderSettingsManager", () => {
expect(manager.read().providers["openai-codex"]?.tokenSource).toBe("oauth");
});
it("preserves OAuth auth when updating only Cline model settings", () => {
const tempDir = mkdtempSync(
path.join(os.tmpdir(), "core-provider-settings-"),
);
tempDirs.push(tempDir);
const filePath = path.join(tempDir, "provider-settings.json");
const manager = new ProviderSettingsManager({ filePath });
manager.saveProviderSettings(
{
provider: "cline",
model: "anthropic/claude-sonnet-4.6",
auth: {
accessToken: "workos:access-old",
refreshToken: "refresh-old",
expiresAt: 4_000_000_000_000,
accountId: "acct-old",
},
},
{ tokenSource: "oauth" },
);
manager.saveProviderSettings({
provider: "cline",
model: "anthropic/claude-haiku-4.5",
reasoning: { enabled: false },
});
expect(manager.getProviderSettings("cline")).toEqual({
provider: "cline",
model: "anthropic/claude-haiku-4.5",
reasoning: { enabled: false },
auth: {
accessToken: "workos:access-old",
refreshToken: "refresh-old",
expiresAt: 4_000_000_000_000,
accountId: "acct-old",
},
});
expect(manager.read().providers.cline?.tokenSource).toBe("oauth");
});
it("merges partial OAuth auth updates with existing refresh metadata", () => {
const tempDir = mkdtempSync(
path.join(os.tmpdir(), "core-provider-settings-"),
);
tempDirs.push(tempDir);
const filePath = path.join(tempDir, "provider-settings.json");
const manager = new ProviderSettingsManager({ filePath });
manager.saveProviderSettings(
{
provider: "openai-codex",
auth: {
accessToken: "access-old",
refreshToken: "refresh-old",
expiresAt: 4_000_000_000_000,
accountId: "acct-old",
},
},
{ tokenSource: "oauth" },
);
manager.saveProviderSettings(
{
provider: "openai-codex",
auth: {
accessToken: "access-new",
},
},
{ tokenSource: "oauth" },
);
expect(manager.getProviderSettings("openai-codex")?.auth).toEqual({
accessToken: "access-new",
refreshToken: "refresh-old",
expiresAt: 4_000_000_000_000,
accountId: "acct-old",
});
});
it("ignores invalid persisted JSON and falls back to empty state", () => {
const tempDir = mkdtempSync(
path.join(os.tmpdir(), "core-provider-settings-"),
@@ -6,6 +6,7 @@ import {
writeFileSync,
} from "node:fs";
import { basename, dirname } from "node:path";
import { isOAuthProviderId } from "@cline/shared";
import { resolveProviderSettingsPath } from "@cline/shared/storage";
import { getLiveModelsCatalog } from "../..";
import {
@@ -50,6 +51,25 @@ function inferLegacyDataDir(filePath: string): string | undefined {
return dirname(settingsDir);
}
function preserveOAuthAuthFields(
next: ProviderSettings,
previous: ProviderSettings | undefined,
): ProviderSettings {
if (!isOAuthProviderId(next.provider) || !previous?.auth) {
return next;
}
if (!next.auth) {
return { ...next, auth: previous.auth };
}
return {
...next,
auth: {
...previous.auth,
...next.auth,
},
};
}
export class ProviderSettingsManager {
private readonly filePath: string;
private readonly dataDir?: string;
@@ -124,11 +144,15 @@ export class ProviderSettingsManager {
settings: unknown,
options: SaveProviderSettingsOptions = {},
): StoredProviderSettings {
const validatedSettings = ProviderSettingsSchema.parse(settings);
const parsedSettings = ProviderSettingsSchema.parse(settings);
const previous = this.read();
const providerId = validatedSettings.provider;
const providerId = parsedSettings.provider;
const shouldSetLastUsed = options.setLastUsed !== false;
const previousEntry = previous.providers[providerId];
const validatedSettings = preserveOAuthAuthFields(
parsedSettings,
previousEntry?.settings,
);
const tokenSource =
options.tokenSource ?? previousEntry?.tokenSource ?? "manual";
const next: StoredProviderSettings = {