mirror of
https://github.com/cline/cline.git
synced 2026-10-29 17:42:34 +08:00
persist openai-codex provider model settings
- also don't show a sign in button if openai-codex is the only provider
This commit is contained in:
committed by
Dominic Cooney
parent
0e9a5c0fc1
commit
3757a8fd0d
@@ -1,13 +1,16 @@
|
||||
import type { ApiConfiguration } from "@shared/api"
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
||||
import { hasUsableProvider, hasUsableProviderForActiveMode } from "./provider-usability"
|
||||
import type { ApiConfiguration } from "@shared/api";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
hasUsableProvider,
|
||||
hasUsableProviderForActiveMode,
|
||||
} from "./provider-usability";
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
const providerSettingsManager = {
|
||||
getProviderSettings: vi.fn(() => undefined as unknown),
|
||||
getLastUsedProviderSettings: vi.fn(() => undefined as unknown),
|
||||
saveProviderSettings: vi.fn(),
|
||||
}
|
||||
};
|
||||
return {
|
||||
providerSettingsManager,
|
||||
getProviderSettingsManager: vi.fn(() => providerSettingsManager),
|
||||
@@ -15,18 +18,18 @@ const mocks = vi.hoisted(() => {
|
||||
getApiConfiguration: vi.fn((): ApiConfiguration => ({})),
|
||||
getGlobalSettingsKey: vi.fn((_key: string): unknown => undefined),
|
||||
},
|
||||
}
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@/core/storage/StateManager", () => ({
|
||||
StateManager: {
|
||||
get: () => mocks.stateManager,
|
||||
},
|
||||
}))
|
||||
}));
|
||||
|
||||
vi.mock("./provider-migration", () => ({
|
||||
getProviderSettingsManager: mocks.getProviderSettingsManager,
|
||||
}))
|
||||
}));
|
||||
|
||||
vi.mock("@shared/services/Logger", () => ({
|
||||
Logger: {
|
||||
@@ -35,44 +38,44 @@ vi.mock("@shared/services/Logger", () => ({
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
},
|
||||
}))
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.providerSettingsManager.getProviderSettings.mockReturnValue(undefined)
|
||||
})
|
||||
mocks.providerSettingsManager.getProviderSettings.mockReturnValue(undefined);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("hasUsableProvider", () => {
|
||||
it("returns false when no provider is set for the active mode", () => {
|
||||
expect(hasUsableProvider({}, "act")).toBe(false)
|
||||
expect(hasUsableProvider({}, "plan")).toBe(false)
|
||||
})
|
||||
expect(hasUsableProvider({}, "act")).toBe(false);
|
||||
expect(hasUsableProvider({}, "plan")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns true for a BYOK provider with an API key (act mode)", () => {
|
||||
const config: ApiConfiguration = {
|
||||
actModeApiProvider: "openrouter",
|
||||
openRouterApiKey: "sk-or-123",
|
||||
}
|
||||
expect(hasUsableProvider(config, "act")).toBe(true)
|
||||
})
|
||||
};
|
||||
expect(hasUsableProvider(config, "act")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false for a BYOK provider with no API key", () => {
|
||||
const config: ApiConfiguration = {
|
||||
actModeApiProvider: "openrouter",
|
||||
}
|
||||
expect(hasUsableProvider(config, "act")).toBe(false)
|
||||
})
|
||||
};
|
||||
expect(hasUsableProvider(config, "act")).toBe(false);
|
||||
});
|
||||
|
||||
it("returns false for a BYOK provider whose key is only whitespace", () => {
|
||||
const config: ApiConfiguration = {
|
||||
actModeApiProvider: "anthropic",
|
||||
apiKey: " ",
|
||||
}
|
||||
expect(hasUsableProvider(config, "act")).toBe(false)
|
||||
})
|
||||
};
|
||||
expect(hasUsableProvider(config, "act")).toBe(false);
|
||||
});
|
||||
|
||||
it("resolves the provider for the active mode independently (plan vs act)", () => {
|
||||
const config: ApiConfiguration = {
|
||||
@@ -80,92 +83,123 @@ describe("hasUsableProvider", () => {
|
||||
apiKey: "sk-ant-123",
|
||||
planModeApiProvider: "openrouter",
|
||||
// no openRouterApiKey -> plan is not usable
|
||||
}
|
||||
expect(hasUsableProvider(config, "act")).toBe(true)
|
||||
expect(hasUsableProvider(config, "plan")).toBe(false)
|
||||
})
|
||||
};
|
||||
expect(hasUsableProvider(config, "act")).toBe(true);
|
||||
expect(hasUsableProvider(config, "plan")).toBe(false);
|
||||
});
|
||||
|
||||
it("treats the cline provider as usable when a token is present in providers.json", () => {
|
||||
mocks.providerSettingsManager.getProviderSettings.mockReturnValue({
|
||||
provider: "cline",
|
||||
auth: { accessToken: "workos:abc123" },
|
||||
})
|
||||
const config: ApiConfiguration = { actModeApiProvider: "cline" }
|
||||
expect(hasUsableProvider(config, "act")).toBe(true)
|
||||
})
|
||||
});
|
||||
const config: ApiConfiguration = { actModeApiProvider: "cline" };
|
||||
expect(hasUsableProvider(config, "act")).toBe(true);
|
||||
});
|
||||
|
||||
it("treats the cline provider as NOT usable when no token and no model is present", () => {
|
||||
mocks.providerSettingsManager.getProviderSettings.mockReturnValue(undefined)
|
||||
const config: ApiConfiguration = { actModeApiProvider: "cline" }
|
||||
mocks.providerSettingsManager.getProviderSettings.mockReturnValue(
|
||||
undefined,
|
||||
);
|
||||
const config: ApiConfiguration = { actModeApiProvider: "cline" };
|
||||
// cline is keyless (oauth), but with no token AND no model -> not usable
|
||||
expect(hasUsableProvider(config, "act")).toBe(false)
|
||||
})
|
||||
expect(hasUsableProvider(config, "act")).toBe(false);
|
||||
});
|
||||
|
||||
it("treats the cline provider as NOT usable when a model is configured but there is no token", () => {
|
||||
mocks.providerSettingsManager.getProviderSettings.mockReturnValue(undefined)
|
||||
mocks.providerSettingsManager.getProviderSettings.mockReturnValue(
|
||||
undefined,
|
||||
);
|
||||
const config: ApiConfiguration = {
|
||||
actModeApiProvider: "cline",
|
||||
actModeClineModelId: "anthropic/claude-sonnet-4.6",
|
||||
}
|
||||
};
|
||||
// This is the core case of scenario #3: an unauthenticated Cline user
|
||||
// (the default provider, which always has a model preselected) must be
|
||||
// gated. A configured model alone must NOT make an OAuth provider usable.
|
||||
expect(hasUsableProvider(config, "act")).toBe(false)
|
||||
})
|
||||
expect(hasUsableProvider(config, "act")).toBe(false);
|
||||
});
|
||||
|
||||
it("treats OpenAI Codex as usable when ChatGPT subscription OAuth credentials are present", () => {
|
||||
mocks.providerSettingsManager.getProviderSettings.mockReturnValue({
|
||||
provider: "openai-codex",
|
||||
auth: { accessToken: "codex-access", refreshToken: "codex-refresh" },
|
||||
});
|
||||
const config: ApiConfiguration = {
|
||||
actModeApiProvider: "openai-codex",
|
||||
actModeApiModelId: "gpt-5.4",
|
||||
};
|
||||
expect(hasUsableProvider(config, "act")).toBe(true);
|
||||
});
|
||||
|
||||
it("treats OpenAI Codex as NOT usable when a model is configured but no OAuth credentials are present", () => {
|
||||
mocks.providerSettingsManager.getProviderSettings.mockReturnValue(
|
||||
undefined,
|
||||
);
|
||||
const config: ApiConfiguration = {
|
||||
actModeApiProvider: "openai-codex",
|
||||
actModeApiModelId: "gpt-5.4",
|
||||
};
|
||||
expect(hasUsableProvider(config, "act")).toBe(false);
|
||||
});
|
||||
|
||||
it("treats ollama (keyless local) as usable when a model is configured, without a key", () => {
|
||||
const config: ApiConfiguration = {
|
||||
actModeApiProvider: "ollama",
|
||||
actModeOllamaModelId: "llama3",
|
||||
}
|
||||
expect(hasUsableProvider(config, "act")).toBe(true)
|
||||
})
|
||||
};
|
||||
expect(hasUsableProvider(config, "act")).toBe(true);
|
||||
});
|
||||
|
||||
it("treats ollama (keyless local) as NOT usable when no model is configured", () => {
|
||||
const config: ApiConfiguration = {
|
||||
actModeApiProvider: "ollama",
|
||||
}
|
||||
expect(hasUsableProvider(config, "act")).toBe(false)
|
||||
})
|
||||
};
|
||||
expect(hasUsableProvider(config, "act")).toBe(false);
|
||||
});
|
||||
|
||||
it("treats lmstudio (keyless local) as usable when a model is configured", () => {
|
||||
const config: ApiConfiguration = {
|
||||
actModeApiProvider: "lmstudio",
|
||||
actModeLmStudioModelId: "local-model",
|
||||
}
|
||||
expect(hasUsableProvider(config, "act")).toBe(true)
|
||||
})
|
||||
})
|
||||
};
|
||||
expect(hasUsableProvider(config, "act")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasUsableProviderForActiveMode", () => {
|
||||
it("reads ApiConfiguration + mode from StateManager and returns true for a usable BYOK config", () => {
|
||||
mocks.stateManager.getApiConfiguration.mockReturnValue({
|
||||
actModeApiProvider: "openrouter",
|
||||
openRouterApiKey: "sk-or-xyz",
|
||||
})
|
||||
mocks.stateManager.getGlobalSettingsKey.mockImplementation((key: string) => (key === "mode" ? "act" : undefined))
|
||||
expect(hasUsableProviderForActiveMode()).toBe(true)
|
||||
})
|
||||
});
|
||||
mocks.stateManager.getGlobalSettingsKey.mockImplementation((key: string) =>
|
||||
key === "mode" ? "act" : undefined,
|
||||
);
|
||||
expect(hasUsableProviderForActiveMode()).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false when StateManager has no provider configured", () => {
|
||||
mocks.stateManager.getApiConfiguration.mockReturnValue({})
|
||||
mocks.stateManager.getGlobalSettingsKey.mockImplementation((key: string) => (key === "mode" ? "act" : undefined))
|
||||
expect(hasUsableProviderForActiveMode()).toBe(false)
|
||||
})
|
||||
mocks.stateManager.getApiConfiguration.mockReturnValue({});
|
||||
mocks.stateManager.getGlobalSettingsKey.mockImplementation((key: string) =>
|
||||
key === "mode" ? "act" : undefined,
|
||||
);
|
||||
expect(hasUsableProviderForActiveMode()).toBe(false);
|
||||
});
|
||||
|
||||
it("defaults mode to act when StateManager returns no mode", () => {
|
||||
mocks.stateManager.getApiConfiguration.mockReturnValue({
|
||||
actModeApiProvider: "anthropic",
|
||||
apiKey: "sk-ant",
|
||||
})
|
||||
mocks.stateManager.getGlobalSettingsKey.mockReturnValue(undefined)
|
||||
expect(hasUsableProviderForActiveMode()).toBe(true)
|
||||
})
|
||||
});
|
||||
mocks.stateManager.getGlobalSettingsKey.mockReturnValue(undefined);
|
||||
expect(hasUsableProviderForActiveMode()).toBe(true);
|
||||
});
|
||||
|
||||
it("fails open (returns true) when StateManager throws", () => {
|
||||
mocks.stateManager.getApiConfiguration.mockImplementation(() => {
|
||||
throw new Error("boom")
|
||||
})
|
||||
expect(hasUsableProviderForActiveMode()).toBe(true)
|
||||
})
|
||||
})
|
||||
throw new Error("boom");
|
||||
});
|
||||
expect(hasUsableProviderForActiveMode()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,19 +3,20 @@
|
||||
// No legacy state-key heuristics.
|
||||
//
|
||||
// "Usable" mirrors what buildSessionConfig can actually turn into a working
|
||||
// session: if we can resolve a credential (BYOK key or Cline OAuth token) for
|
||||
// the active-mode provider, the user can chat. Keyless providers (Cline/OAuth
|
||||
// providers, SDK "local-auth" providers, and the local-inference providers
|
||||
// session: if we can resolve a credential (BYOK key or OAuth auth token) for
|
||||
// the active-mode provider, the user can chat. Keyless providers (SDK
|
||||
// "local-auth" providers and the local-inference providers
|
||||
// ollama/lmstudio/vscode-lm) are usable once a model is configured, even
|
||||
// without an API key.
|
||||
|
||||
import { getProviderConfigFields } from "@cline/core"
|
||||
import type { ApiConfiguration } from "@shared/api"
|
||||
import { Logger } from "@shared/services/Logger"
|
||||
import type { Mode } from "@shared/storage/types"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import { resolveApiKey, resolveModelId } from "./cline-session-factory"
|
||||
import { toSdkProviderId } from "./model-catalog/sdk-provider-id"
|
||||
import { getProviderConfigFields } from "@cline/core";
|
||||
import type { ApiConfiguration } from "@shared/api";
|
||||
import { Logger } from "@shared/services/Logger";
|
||||
import type { Mode } from "@shared/storage/types";
|
||||
import { StateManager } from "@/core/storage/StateManager";
|
||||
import { resolveApiKey, resolveModelId } from "./cline-session-factory";
|
||||
import { toSdkProviderId } from "./model-catalog/sdk-provider-id";
|
||||
import { getProviderSettingsManager } from "./provider-migration";
|
||||
|
||||
/**
|
||||
* Local-inference providers that the legacy ApiConfiguration path treats as
|
||||
@@ -26,7 +27,11 @@ import { toSdkProviderId } from "./model-catalog/sdk-provider-id"
|
||||
* listed here explicitly. `vscode-lm` is a VSCode-host provider that is not in
|
||||
* the SDK builtin catalog at all.
|
||||
*/
|
||||
const KEYLESS_LOCAL_PROVIDERS = new Set<string>(["ollama", "lmstudio", "vscode-lm"])
|
||||
const KEYLESS_LOCAL_PROVIDERS = new Set<string>([
|
||||
"ollama",
|
||||
"lmstudio",
|
||||
"vscode-lm",
|
||||
]);
|
||||
|
||||
/**
|
||||
* Whether a provider can be usable WITHOUT an API key, on the strength of a
|
||||
@@ -49,15 +54,38 @@ const KEYLESS_LOCAL_PROVIDERS = new Set<string>(["ollama", "lmstudio", "vscode-l
|
||||
* Keeps the keyless set catalog-driven instead of a hardcoded list of every
|
||||
* provider, matching how the rest of the SDK adapter classifies providers.
|
||||
*/
|
||||
function getProviderAuthMethod(providerId: string): string | undefined {
|
||||
try {
|
||||
return getProviderConfigFields(toSdkProviderId(providerId)).authMethod;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function isKeylessViaModelProvider(providerId: string): boolean {
|
||||
if (KEYLESS_LOCAL_PROVIDERS.has(providerId)) {
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
return getProviderAuthMethod(providerId) === "local";
|
||||
}
|
||||
|
||||
function hasProviderSettingsAuthCredential(providerId: string): boolean {
|
||||
try {
|
||||
const fields = getProviderConfigFields(toSdkProviderId(providerId))
|
||||
return fields.authMethod === "local"
|
||||
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
|
||||
);
|
||||
} catch {
|
||||
return false
|
||||
Logger.warn(
|
||||
`[ProviderUsability] Failed to read provider settings auth credentials for provider ${providerId}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,29 +95,43 @@ function isKeylessViaModelProvider(providerId: string): boolean {
|
||||
* Decoupled from the StateManager singleton so it can be unit-tested with a
|
||||
* plain ApiConfiguration + mode.
|
||||
*/
|
||||
export function hasUsableProvider(apiConfig: ApiConfiguration, mode: Mode): boolean {
|
||||
const providerId = mode === "plan" ? apiConfig.planModeApiProvider : apiConfig.actModeApiProvider
|
||||
export function hasUsableProvider(
|
||||
apiConfig: ApiConfiguration,
|
||||
mode: Mode,
|
||||
): boolean {
|
||||
const providerId =
|
||||
mode === "plan"
|
||||
? apiConfig.planModeApiProvider
|
||||
: apiConfig.actModeApiProvider;
|
||||
|
||||
// No provider selected for the active mode -> cannot chat.
|
||||
if (!providerId) {
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
// A resolvable credential (BYOK key, Bedrock key, or Cline OAuth token)
|
||||
// A resolvable credential (BYOK key, Bedrock key, or OAuth token)
|
||||
// means we can build a session and chat.
|
||||
const apiKey = resolveApiKey(providerId, apiConfig)
|
||||
const apiKey = resolveApiKey(providerId, apiConfig);
|
||||
if (typeof apiKey === "string" && apiKey.trim().length > 0) {
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
|
||||
// OAuth-backed providers such as OpenAI Codex store subscription credentials
|
||||
// in providers.json as `settings.auth.accessToken`; they do not necessarily
|
||||
// project that token into ApiConfiguration, so resolve the auth envelope
|
||||
// directly through the shared provider settings store.
|
||||
if (hasProviderSettingsAuthCredential(providerId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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)
|
||||
return typeof modelId === "string" && modelId.trim().length > 0
|
||||
const modelId = resolveModelId(providerId, mode, apiConfig);
|
||||
return typeof modelId === "string" && modelId.trim().length > 0;
|
||||
}
|
||||
|
||||
return false
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,13 +141,16 @@ export function hasUsableProvider(apiConfig: ApiConfiguration, mode: Mode): bool
|
||||
*/
|
||||
export function hasUsableProviderForActiveMode(): boolean {
|
||||
try {
|
||||
const stateManager = StateManager.get()
|
||||
const apiConfig = stateManager.getApiConfiguration()
|
||||
const mode: Mode = stateManager.getGlobalSettingsKey("mode") ?? "act"
|
||||
return hasUsableProvider(apiConfig, mode)
|
||||
const stateManager = StateManager.get();
|
||||
const apiConfig = stateManager.getApiConfiguration();
|
||||
const mode: Mode = stateManager.getGlobalSettingsKey("mode") ?? "act";
|
||||
return hasUsableProvider(apiConfig, mode);
|
||||
} catch (error) {
|
||||
Logger.warn("[ProviderUsability] Failed to compute usable provider:", error)
|
||||
Logger.warn(
|
||||
"[ProviderUsability] Failed to compute usable provider:",
|
||||
error,
|
||||
);
|
||||
// Fail open: do not gate the UI on an internal read error.
|
||||
return true
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import { Mode } from "@shared/storage/types"
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
import { useStaticProviderSelection } from "@/hooks/useStaticProviderSelection"
|
||||
import { AccountServiceClient } from "@/services/grpc-client"
|
||||
import { ModelInfoView } from "../common/ModelInfoView"
|
||||
import { ModelSelector } from "../common/ModelSelector"
|
||||
import ReasoningEffortSelector from "../ReasoningEffortSelector"
|
||||
import { supportsReasoningEffortForModelId } from "../utils/providerUtils"
|
||||
import { useApiConfigurationHandlers } from "../utils/useApiConfigurationHandlers"
|
||||
import { openAiModelInfoSafeDefaults } from "@shared/api";
|
||||
import { fromProtobufModelInfo } from "@shared/proto-conversions/models/typeConversion";
|
||||
import type { Mode } from "@shared/storage/types";
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react";
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext";
|
||||
import { useProviderConfig } from "@/hooks/useProviderConfig";
|
||||
import { useStaticProviderSelection } from "@/hooks/useStaticProviderSelection";
|
||||
import { AccountServiceClient } from "@/services/grpc-client";
|
||||
import { ModelInfoView } from "../common/ModelInfoView";
|
||||
import { ModelSelector } from "../common/ModelSelector";
|
||||
import ReasoningEffortSelector from "../ReasoningEffortSelector";
|
||||
import { supportsReasoningEffortForModelId } from "../utils/providerUtils";
|
||||
|
||||
const OPENAI_CODEX_PROVIDER_ID = "openai-codex"
|
||||
const OPENAI_CODEX_PROVIDER_ID = "openai-codex";
|
||||
|
||||
interface OpenAiCodexProviderProps {
|
||||
showModelOptions: boolean
|
||||
isPopup?: boolean
|
||||
currentMode: Mode
|
||||
showModelOptions: boolean;
|
||||
isPopup?: boolean;
|
||||
currentMode: Mode;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,39 +30,89 @@ interface OpenAiCodexProviderProps {
|
||||
* provider, and the `hideUsageCost` flag is not hard-coded — it is
|
||||
* derived from the SDK's `ProviderInfo.metadata.usageCostDisplay`.
|
||||
*/
|
||||
export const OpenAiCodexProvider = ({ showModelOptions, isPopup, currentMode }: OpenAiCodexProviderProps) => {
|
||||
const { apiConfiguration, openAiCodexIsAuthenticated } = useExtensionState()
|
||||
const { handleModeFieldChange } = useApiConfigurationHandlers()
|
||||
const { models, selectedModelId, selectedModelInfo, hideUsageCost } = useStaticProviderSelection(
|
||||
export const OpenAiCodexProvider = ({
|
||||
showModelOptions,
|
||||
isPopup,
|
||||
currentMode,
|
||||
}: OpenAiCodexProviderProps) => {
|
||||
const { apiConfiguration, openAiCodexIsAuthenticated } = useExtensionState();
|
||||
const { config, commitSelection } = useProviderConfig(
|
||||
OPENAI_CODEX_PROVIDER_ID,
|
||||
);
|
||||
const {
|
||||
models,
|
||||
defaultModelId,
|
||||
selectedModelId: legacySelectedModelId,
|
||||
selectedModelInfo: legacySelectedModelInfo,
|
||||
hideUsageCost,
|
||||
} = useStaticProviderSelection(
|
||||
OPENAI_CODEX_PROVIDER_ID,
|
||||
apiConfiguration,
|
||||
currentMode,
|
||||
)
|
||||
);
|
||||
const committedSelection =
|
||||
currentMode === "plan" ? config?.planSelection : config?.actSelection;
|
||||
const selectedModelId = committedSelection?.modelId ?? legacySelectedModelId;
|
||||
const selectedModelInfo = committedSelection?.modelInfo
|
||||
? fromProtobufModelInfo(committedSelection.modelInfo)
|
||||
: legacySelectedModelInfo;
|
||||
|
||||
const showReasoningEffort = supportsReasoningEffortForModelId(selectedModelId, true)
|
||||
const showReasoningEffort = supportsReasoningEffortForModelId(
|
||||
selectedModelId,
|
||||
true,
|
||||
);
|
||||
|
||||
const handleModelChange = (modelId: string) => {
|
||||
if (!modelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fallbackModelId = defaultModelId || Object.keys(models)[0] || modelId;
|
||||
const modelInfo =
|
||||
models[modelId] ??
|
||||
models[fallbackModelId] ??
|
||||
selectedModelInfo ??
|
||||
openAiModelInfoSafeDefaults;
|
||||
|
||||
void commitSelection(currentMode, {
|
||||
providerId: OPENAI_CODEX_PROVIDER_ID,
|
||||
modelId,
|
||||
modelInfo,
|
||||
}).catch((err) =>
|
||||
console.error("Failed to commit OpenAI Codex model selection:", err),
|
||||
);
|
||||
};
|
||||
|
||||
const handleSignIn = async () => {
|
||||
try {
|
||||
await AccountServiceClient.openAiCodexSignIn({})
|
||||
await AccountServiceClient.openAiCodexSignIn({});
|
||||
} catch (error) {
|
||||
console.error("Failed to sign in to OpenAI Codex:", error)
|
||||
console.error("Failed to sign in to OpenAI Codex:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleSignOut = async () => {
|
||||
try {
|
||||
await AccountServiceClient.openAiCodexSignOut({})
|
||||
await AccountServiceClient.openAiCodexSignOut({});
|
||||
} catch (error) {
|
||||
console.error("Failed to sign out of OpenAI Codex:", error)
|
||||
console.error("Failed to sign out of OpenAI Codex:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: "15px" }}>
|
||||
{openAiCodexIsAuthenticated ? (
|
||||
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
||||
<span style={{ color: "var(--vscode-descriptionForeground)" }}>Signed in to OpenAI Codex</span>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<span style={{ color: "var(--vscode-descriptionForeground)" }}>
|
||||
Signed in to OpenAI Codex
|
||||
</span>
|
||||
<VSCodeButton appearance="secondary" onClick={handleSignOut}>
|
||||
Sign Out
|
||||
</VSCodeButton>
|
||||
@@ -72,10 +124,14 @@ export const OpenAiCodexProvider = ({ showModelOptions, isPopup, currentMode }:
|
||||
fontSize: "12px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
marginBottom: "10px",
|
||||
}}>
|
||||
Sign in with your ChatGPT Plus or Pro subscription to use GPT-5 models without an API key.
|
||||
}}
|
||||
>
|
||||
Sign in with your ChatGPT Plus or Pro subscription to use GPT-5
|
||||
models without an API key.
|
||||
</p>
|
||||
<VSCodeButton onClick={handleSignIn}>Sign in to OpenAI Codex</VSCodeButton>
|
||||
<VSCodeButton onClick={handleSignIn}>
|
||||
Sign in to OpenAI Codex
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -85,16 +141,16 @@ export const OpenAiCodexProvider = ({ showModelOptions, isPopup, currentMode }:
|
||||
<ModelSelector
|
||||
label="Model"
|
||||
models={models}
|
||||
onChange={(e: any) =>
|
||||
handleModeFieldChange(
|
||||
{ plan: "planModeApiModelId", act: "actModeApiModelId" },
|
||||
e.target.value,
|
||||
currentMode,
|
||||
onChange={(event: Event) =>
|
||||
handleModelChange(
|
||||
(event.target as HTMLSelectElement | null)?.value ?? "",
|
||||
)
|
||||
}
|
||||
selectedModelId={selectedModelId}
|
||||
/>
|
||||
{showReasoningEffort && <ReasoningEffortSelector currentMode={currentMode} />}
|
||||
{showReasoningEffort && (
|
||||
<ReasoningEffortSelector currentMode={currentMode} />
|
||||
)}
|
||||
|
||||
<ModelInfoView
|
||||
hideUsageCost={hideUsageCost}
|
||||
@@ -105,5 +161,5 @@ export const OpenAiCodexProvider = ({ showModelOptions, isPopup, currentMode }:
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user