Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -191,11 +191,8 @@ export function ModelSelectorView(
handleApiKeyChange={props.handleApiKeyChange}
handleApiKeySubmit={props.handleApiKeySubmit}
handleCursorOffsetChange={props.handleCursorOffsetChange}
apiKeyCleanedNotification={props.apiKeyCleanedNotification}
isLoadingModels={props.isLoadingModels}
providerBaseUrl={props.providerBaseUrl}
modelLoadError={props.modelLoadError}
formatApiKeyDisplay={props.formatApiKeyDisplay}
getProviderLabel={props.getProviderLabel}
/>
)
Expand Down Expand Up @@ -369,12 +366,7 @@ export function ModelSelectorView(
ollamaBaseUrl={props.ollamaBaseUrl}
customBaseUrl={props.customBaseUrl}
apiKey={props.apiKey}
maxTokens={props.maxTokens}
contextLength={props.contextLength}
supportsReasoningEffort={props.supportsReasoningEffort}
reasoningEffort={props.reasoningEffort}
validationError={props.validationError}
formatApiKeyDisplay={props.formatApiKeyDisplay}
getProviderLabel={props.getProviderLabel}
/>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ type Params = {
providerBaseUrl: string
resourceName: string
customBaseUrl: string
apiKey: string
apiKeyEnv?: string
maxTokens: string
contextLength: number
reasoningEffort: any
Expand All @@ -26,7 +26,7 @@ export async function saveModelConfiguration({
providerBaseUrl,
resourceName,
customBaseUrl,
apiKey,
apiKeyEnv,
maxTokens,
contextLength,
reasoningEffort,
Expand Down Expand Up @@ -68,7 +68,8 @@ export async function saveModelConfiguration({
provider: actualProvider,
modelName: model || actualProvider, // Use provider name if no specific model
baseURL: baseURL,
apiKey: apiKey || '',
apiKey: '',
...(apiKeyEnv ? { apiKeyEnv } : {}),
maxTokens: parseInt(maxTokens) || DEFAULT_MAX_TOKENS,
contextLength: contextLength || DEFAULT_CONTEXT_LENGTH,
reasoningEffort,
Expand Down
279 changes: 36 additions & 243 deletions apps/cli/src/ui/components/ModelSelector/flow/screens/ApiKeyScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react'
import { Box, Newline, Text } from 'ink'
import { Box, Text } from 'ink'

import TextInput from '#ui-ink/components/TextInput'
import {
Expand All @@ -21,11 +21,8 @@ type Props = {
handleApiKeyChange: (value: string) => void
handleApiKeySubmit: (value: string) => void
handleCursorOffsetChange: (offset: number) => void
apiKeyCleanedNotification: boolean
isLoadingModels: boolean
providerBaseUrl: string
modelLoadError: string | null
formatApiKeyDisplay: (key: string) => string
getProviderLabel: (provider: string, modelCount: number) => string
}

Expand All @@ -43,271 +40,67 @@ export function ApiKeyScreen({
handleApiKeyChange,
handleApiKeySubmit,
handleCursorOffsetChange,
apiKeyCleanedNotification,
isLoadingModels,
providerBaseUrl,
modelLoadError,
formatApiKeyDisplay,
getProviderLabel,
}: Props) {
const modelTypeText = 'this model profile'
const apiKeyInputColumns = Math.max(1, Math.min(80, terminalColumns - 10))
const descriptionWidth = Math.max(1, Math.min(70, terminalColumns - 10))
const inputColumns = Math.max(1, Math.min(80, terminalColumns - 10))
const providerDisplayName = getProviderLabel(selectedProvider, 0).split(
' (',
)[0]
const skipsValidation =
selectedProvider === 'minimax' || selectedProvider === 'minimax-coding'

const providerHint = (() => {
if (tightLayout) return null

if (selectedProvider === 'kimi') {
return (
<Text color={theme.secondaryText}>
Tip: Get your API key from:{' '}
<Text color={theme.suggestion}>
https://platform.moonshot.cn/console/api-keys
</Text>
</Text>
)
}
if (selectedProvider === 'deepseek') {
return (
<Text color={theme.secondaryText}>
Tip: Get your API key from:{' '}
<Text color={theme.suggestion}>
https://platform.deepseek.com/api_keys
</Text>
</Text>
)
}
if (selectedProvider === 'siliconflow') {
return (
<Text color={theme.secondaryText}>
Tip: Get your API key from:{' '}
<Text color={theme.suggestion}>
https://cloud.siliconflow.cn/i/oJWsm6io
</Text>
</Text>
)
}
if (selectedProvider === 'qwen') {
return (
<Text color={theme.secondaryText}>
Tip: Get your API key from:{' '}
<Text color={theme.suggestion}>
https://bailian.console.aliyun.com/?tab=model#/api-key
</Text>
</Text>
)
}
if (selectedProvider === 'glm') {
return (
<Text color={theme.secondaryText}>
Tip: Get your API key from:{' '}
<Text color={theme.suggestion}>
https://open.bigmodel.cn (API Keys section)
</Text>
</Text>
)
}
if (selectedProvider === 'glm-coding') {
return (
<Text color={theme.secondaryText}>
Tip: This is for GLM Coding Plan API.{' '}
<Text color={theme.suggestion}>
Use the same API key as regular GLM
</Text>
<Newline />
<Text dimColor>
Note: This uses a special endpoint for coding tasks.
</Text>
</Text>
)
}
if (selectedProvider === 'minimax') {
return (
<Text color={theme.secondaryText}>
Tip: Get your API key from:{' '}
<Text color={theme.suggestion}>
https://www.minimax.io/platform/user-center/basic-information
</Text>
</Text>
)
}
if (selectedProvider === 'minimax-coding') {
return (
<Text color={theme.secondaryText}>
Tip: Get your Coding Plan API key from:{' '}
<Text color={theme.suggestion}>
https://platform.minimaxi.com/user-center/payment/coding-plan
</Text>
<Newline />
<Text dimColor>
Note: This requires a MiniMax Coding Plan subscription.
</Text>
</Text>
)
}
if (selectedProvider === 'baidu-qianfan') {
return (
<Text color={theme.secondaryText}>
Tip: Get your API key from:{' '}
<Text color={theme.suggestion}>
https://console.bce.baidu.com/iam/#/iam/accesslist
</Text>
</Text>
)
}
if (selectedProvider === 'openai') {
return (
<Text color={theme.secondaryText}>
Tip: Get your API key from:{' '}
<Text color={theme.suggestion}>
https://platform.openai.com/api-keys
</Text>
</Text>
)
}
if (selectedProvider === 'anthropic') {
return (
<Text color={theme.secondaryText}>
Tip: Get your API key from your provider dashboard.
</Text>
)
}

return null
})()

return (
<ScreenFrame
title="API Key Setup"
title="Credential Source / 凭据来源"
exitState={exitState}
paddingX={tightLayout || compactLayout ? 1 : 2}
paddingY={containerPaddingY}
gap={containerGap}
>
<Box flexDirection="column" gap={containerGap}>
<Text bold>
Enter your {providerDisplayName} API key for {modelTypeText}:
<Text bold wrap="truncate-end">
Environment variable for {providerDisplayName}:
</Text>
<Text color={theme.secondaryText} wrap="truncate-end">
Enter the variable name, not its secret value. Kode saves only this
reference and never shows the key here.
</Text>

<Box
flexDirection="column"
width={tightLayout ? undefined : descriptionWidth}
>
{tightLayout ? (
<Text color={theme.secondaryText}>
Stored locally. Never sent to our servers.
</Text>
) : (
<>
<Text color={theme.secondaryText}>
{compactLayout
? `Stored locally and used to access ${selectedProvider}. Never sent to our servers.`
: `This key will be stored locally and used to access the ${selectedProvider} API. Your key is never sent to our servers.`}
</Text>
{providerHint ? <Box marginTop={1}>{providerHint}</Box> : null}
</>
)}
</Box>

<Box flexDirection="column">
<TextInput
placeholder="Paste your API key here..."
value={apiKey}
displayValue={formatApiKeyDisplay(apiKey)}
onChange={handleApiKeyChange}
onSubmit={handleApiKeySubmit}
mask="*"
columns={apiKeyInputColumns}
maxHeight={1}
cursorOffset={cursorOffset}
onChangeCursorOffset={handleCursorOffsetChange}
showCursor={!isLoadingModels}
focus={!isLoadingModels}
/>
<TextInput
placeholder="OPENAI_API_KEY"
value={apiKey}
onChange={handleApiKeyChange}
onSubmit={handleApiKeySubmit}
columns={inputColumns}
maxHeight={1}
cursorOffset={cursorOffset}
onChangeCursorOffset={handleCursorOffsetChange}
showCursor={!isLoadingModels}
focus={!isLoadingModels}
/>

{apiKey && !tightLayout && (
<Box marginTop={1}>
<Text color={theme.secondaryText}>
Key: {formatApiKeyDisplay(apiKey)} ({apiKey.length} chars)
</Text>
</Box>
)}
</Box>

{apiKeyCleanedNotification && !tightLayout && (
<Box marginTop={1}>
<Text color={theme.success}>
✓ API key cleaned: trimmed whitespace
</Text>
{isLoadingModels ? (
<Box flexDirection="column">
<Text color={theme.suggestion}>Discovering available models…</Text>
</Box>
)}
) : null}

{!tightLayout && (
<Box marginTop={1}>
<Text>
<Text color={theme.suggestion} dimColor={!apiKey}>
[Submit API Key]
</Text>
<Text>
{' '}
- Press Enter to{' '}
{skipsValidation ? 'continue' : 'validate and continue'}
</Text>
{modelLoadError ? (
<Box flexDirection="column">
<Text color={theme.error} wrap="truncate-end">
{modelLoadError}
</Text>
</Box>
)}

{isLoadingModels && (
<Box marginTop={tightLayout ? 0 : 1} flexDirection="column">
<Text color={theme.suggestion}>Validating API key…</Text>
{!tightLayout && modelLoadError ? (
<Text dimColor>{modelLoadError}</Text>
) : null}
{!tightLayout && providerBaseUrl ? (
<Text dimColor>Endpoint: {providerBaseUrl}</Text>
) : null}
</Box>
)}

{modelLoadError && !isLoadingModels && (
<Box marginTop={tightLayout ? 0 : 1} flexDirection="column">
<Text color={theme.error}>
Validation failed{tightLayout ? `: ${modelLoadError}` : ''}
<Text color={theme.secondaryText} wrap="truncate-end">
Correct the reference, or press Enter to configure the model ID
without discovery.
</Text>
{!tightLayout ? (
<>
<Text color={theme.error}>{modelLoadError}</Text>
<Text color={theme.warning}>
Please check your API key and try again.
</Text>
</>
) : null}
</Box>
)}
) : null}

<Box marginTop={tightLayout ? 0 : 1}>
<Text dimColor>
Press <Text color={theme.suggestion}>Enter</Text> to continue,{' '}
<Text color={theme.suggestion}>Tab</Text> to{' '}
{selectedProvider === 'anthropic' ||
selectedProvider === 'kimi' ||
selectedProvider === 'deepseek' ||
selectedProvider === 'qwen' ||
selectedProvider === 'glm' ||
selectedProvider === 'glm-coding' ||
selectedProvider === 'minimax' ||
selectedProvider === 'minimax-coding' ||
selectedProvider === 'baidu-qianfan' ||
selectedProvider === 'siliconflow' ||
selectedProvider === 'custom-openai'
? 'skip to manual model input'
: 'skip using a key'}
, or <Text color={theme.suggestion}>Esc</Text> to go back
<Text color={theme.secondaryText} wrap="truncate-end">
Enter use reference & enter model ID · Tab discover models · Esc
back
</Text>
</Box>
</Box>
Expand Down
Loading
Loading