feat: add custom model input with suggestions and model listing links
This commit is contained in:
parent
dc90f0d214
commit
a02e4f27af
|
|
@ -19,7 +19,7 @@ import { getOauthAccountInfo, validateForceLoginOrg } from '../utils/auth.js';
|
||||||
import { openBrowser } from '../utils/browser.js';
|
import { openBrowser } from '../utils/browser.js';
|
||||||
import { logError } from '../utils/log.js';
|
import { logError } from '../utils/log.js';
|
||||||
import { getSettings_DEPRECATED, updateSettingsForSource } from '../utils/settings/settings.js';
|
import { getSettings_DEPRECATED, updateSettingsForSource } from '../utils/settings/settings.js';
|
||||||
import { CHINA_LLM_PROVIDERS, type ProviderPreset, resolveChinaProviderBaseURL } from '../utils/chinaLlmProviders.js';
|
import { CHINA_LLM_PROVIDERS, type ProviderPreset, resolveChinaProviderBaseURL } from 'src/utils/chinaLlmProviders.js';
|
||||||
import { Select } from './CustomSelect/select.js';
|
import { Select } from './CustomSelect/select.js';
|
||||||
import { Spinner } from './Spinner.js';
|
import { Spinner } from './Spinner.js';
|
||||||
import TextInput from './TextInput.js';
|
import TextInput from './TextInput.js';
|
||||||
|
|
@ -1372,26 +1372,38 @@ function OAuthStatusMessage({
|
||||||
</Text>
|
</Text>
|
||||||
<Box>
|
<Box>
|
||||||
<Select
|
<Select
|
||||||
options={models.map(m => {
|
options={[
|
||||||
const priceLabel =
|
...models.map(m => {
|
||||||
m.inputPricePerMTok === 0 && m.outputPricePerMTok === 0
|
const priceLabel =
|
||||||
? 'Free'
|
m.inputPricePerMTok === 0 && m.outputPricePerMTok === 0
|
||||||
: `¥${m.inputPricePerMTok}/¥${m.outputPricePerMTok}`;
|
? 'Free'
|
||||||
const tagLabel = m.tags?.length ? ` [${m.tags.join(', ')}]` : '';
|
: `¥${m.inputPricePerMTok}/¥${m.outputPricePerMTok}`;
|
||||||
return {
|
const tagLabel = m.tags?.length ? ` [${m.tags.join(', ')}]` : '';
|
||||||
|
return {
|
||||||
|
label: (
|
||||||
|
<Text>
|
||||||
|
{m.label} ·{' '}
|
||||||
|
<Text dimColor>
|
||||||
|
{priceLabel} · {m.contextWindow}
|
||||||
|
{tagLabel}
|
||||||
|
</Text>
|
||||||
|
{'\n'}
|
||||||
|
</Text>
|
||||||
|
),
|
||||||
|
value: m.id,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
{
|
||||||
label: (
|
label: (
|
||||||
<Text>
|
<Text>
|
||||||
{m.label} ·{' '}
|
✏️ Custom model
|
||||||
<Text dimColor>
|
<Text dimColor> · enter model name manually</Text>
|
||||||
{priceLabel} · {m.contextWindow}
|
|
||||||
{tagLabel}
|
|
||||||
</Text>
|
|
||||||
{'\n'}
|
{'\n'}
|
||||||
</Text>
|
</Text>
|
||||||
),
|
),
|
||||||
value: m.id,
|
value: '__custom__',
|
||||||
};
|
},
|
||||||
})}
|
]}
|
||||||
onChange={value => {
|
onChange={value => {
|
||||||
logEvent('tengu_china_model_selected', {});
|
logEvent('tengu_china_model_selected', {});
|
||||||
setOAuthStatus({ state: 'china_apikey', provider, mode: accessMode, modelId: value, apiKey: '' });
|
setOAuthStatus({ state: 'china_apikey', provider, mode: accessMode, modelId: value, apiKey: '' });
|
||||||
|
|
@ -1410,22 +1422,36 @@ function OAuthStatusMessage({
|
||||||
const [chinaKeyError, setChinaKeyError] = useState<string | null>(null);
|
const [chinaKeyError, setChinaKeyError] = useState<string | null>(null);
|
||||||
|
|
||||||
const doChinaSave = useCallback(() => {
|
const doChinaSave = useCallback(() => {
|
||||||
|
const effectiveModelId = modelId === '__custom__' ? chinaKeyValue.trim() : modelId;
|
||||||
|
if (!effectiveModelId) {
|
||||||
|
setChinaKeyError(modelId === '__custom__' ? 'Please enter a model name' : 'Please enter an API key');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (modelId === '__custom__') {
|
||||||
|
logEvent('tengu_china_custom_model_entered', {});
|
||||||
|
setOAuthStatus({ state: 'china_apikey', provider, mode: accessMode, modelId: effectiveModelId, apiKey: '' });
|
||||||
|
setChinaKeyValue('');
|
||||||
|
setChinaKeyError(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!chinaKeyValue.trim()) {
|
if (!chinaKeyValue.trim()) {
|
||||||
setChinaKeyError('Please enter an API key');
|
setChinaKeyError('Please enter an API key');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const baseUrl = resolveChinaProviderBaseURL(provider.id, accessMode);
|
const baseUrl = resolveChinaProviderBaseURL(provider.id, accessMode);
|
||||||
const env: Record<string, string> = {
|
const env: Record<string, string | undefined> = {
|
||||||
|
OPENAI_AUTH_MODE: undefined,
|
||||||
OPENAI_BASE_URL: baseUrl,
|
OPENAI_BASE_URL: baseUrl,
|
||||||
OPENAI_API_KEY: chinaKeyValue.trim(),
|
OPENAI_API_KEY: chinaKeyValue.trim(),
|
||||||
OPENAI_DEFAULT_SONNET_MODEL: modelId,
|
OPENAI_DEFAULT_SONNET_MODEL: modelId,
|
||||||
OPENAI_DEFAULT_HAIKU_MODEL: modelId,
|
OPENAI_DEFAULT_HAIKU_MODEL: modelId,
|
||||||
OPENAI_DEFAULT_OPUS_MODEL: modelId,
|
OPENAI_DEFAULT_OPUS_MODEL: modelId,
|
||||||
};
|
};
|
||||||
const { error } = updateSettingsForSource('userSettings', {
|
const settingsUpdate: Parameters<typeof updateSettingsForSource>[1] = {
|
||||||
modelType: 'openai' as any,
|
modelType: 'openai',
|
||||||
env,
|
env: env as unknown as Record<string, string>,
|
||||||
} as any);
|
};
|
||||||
|
const { error } = updateSettingsForSource('userSettings', settingsUpdate);
|
||||||
if (error) {
|
if (error) {
|
||||||
setOAuthStatus({
|
setOAuthStatus({
|
||||||
state: 'error',
|
state: 'error',
|
||||||
|
|
@ -1433,7 +1459,13 @@ function OAuthStatusMessage({
|
||||||
toRetry: { state: 'china_apikey', provider, mode: accessMode, modelId, apiKey: chinaKeyValue },
|
toRetry: { state: 'china_apikey', provider, mode: accessMode, modelId, apiKey: chinaKeyValue },
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
for (const [k, v] of Object.entries(env)) process.env[k] = v;
|
for (const [k, v] of Object.entries(env)) {
|
||||||
|
if (v === undefined) {
|
||||||
|
delete process.env[k];
|
||||||
|
} else {
|
||||||
|
process.env[k] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
logEvent('tengu_china_login_success', {});
|
logEvent('tengu_china_login_success', {});
|
||||||
setOAuthStatus({ state: 'success' });
|
setOAuthStatus({ state: 'success' });
|
||||||
void onDone();
|
void onDone();
|
||||||
|
|
@ -1448,18 +1480,47 @@ function OAuthStatusMessage({
|
||||||
{ context: 'Confirmation' },
|
{ context: 'Confirmation' },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isCustomModelEntry = modelId === '__custom__';
|
||||||
|
const allModels = CHINA_LLM_PROVIDERS.flatMap(p =>
|
||||||
|
p.models.map(m => ({ id: m.id, label: m.label, provider: p.label })),
|
||||||
|
);
|
||||||
|
const modelSuggestions = isCustomModelEntry
|
||||||
|
? chinaKeyValue.trim()
|
||||||
|
? allModels.filter(m => m.id.toLowerCase().includes(chinaKeyValue.trim().toLowerCase()))
|
||||||
|
: allModels
|
||||||
|
: [];
|
||||||
|
const keyPage = isCustomModelEntry
|
||||||
|
? provider.apiKeyPage
|
||||||
|
: accessMode === 'coding-plan' && provider.codingPlan
|
||||||
|
? provider.codingPlan.purchasePage
|
||||||
|
: provider.apiKeyPage;
|
||||||
|
const keyFormat = isCustomModelEntry
|
||||||
|
? provider.keyFormat
|
||||||
|
: accessMode === 'coding-plan' && provider.codingPlan
|
||||||
|
? provider.codingPlan.keyFormat
|
||||||
|
: provider.keyFormat;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" gap={1} marginTop={1}>
|
<Box flexDirection="column" gap={1} marginTop={1}>
|
||||||
<Text bold>
|
<Text bold>
|
||||||
{provider.icon} {provider.label} API Key
|
{provider.icon} {provider.label} {isCustomModelEntry ? '— Custom Model' : 'API Key'}
|
||||||
</Text>
|
</Text>
|
||||||
<Box flexDirection="column" gap={0}>
|
<Box flexDirection="column" gap={0}>
|
||||||
<Text dimColor> Get your key: {provider.apiKeyPage}</Text>
|
{isCustomModelEntry ? (
|
||||||
<Text dimColor> {provider.freeTier}</Text>
|
<Text dimColor> Enter any model ID supported by this provider. Browse models: {provider.modelsPage}</Text>
|
||||||
<Text dimColor> Key format: {provider.keyFormat}</Text>
|
) : (
|
||||||
|
<>
|
||||||
|
<Text dimColor> Get your key: {keyPage}</Text>
|
||||||
|
<Text dimColor>
|
||||||
|
{' '}
|
||||||
|
{accessMode === 'coding-plan' ? 'Use your Coding Plan credential here' : provider.freeTier}
|
||||||
|
</Text>
|
||||||
|
<Text dimColor> Key format: {keyFormat}</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Text>API Key: </Text>
|
<Text>{isCustomModelEntry ? 'Model name: ' : 'API Key: '}</Text>
|
||||||
<TextInput
|
<TextInput
|
||||||
value={chinaKeyValue}
|
value={chinaKeyValue}
|
||||||
onChange={v => {
|
onChange={v => {
|
||||||
|
|
@ -1470,12 +1531,28 @@ function OAuthStatusMessage({
|
||||||
cursorOffset={chinaKeyCursor}
|
cursorOffset={chinaKeyCursor}
|
||||||
onChangeCursorOffset={setChinaKeyCursor}
|
onChangeCursorOffset={setChinaKeyCursor}
|
||||||
columns={useTerminalSize().columns - 12}
|
columns={useTerminalSize().columns - 12}
|
||||||
mask="*"
|
mask={isCustomModelEntry ? undefined : '*'}
|
||||||
focus={true}
|
focus={true}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
{chinaKeyError ? <Text color="error">{chinaKeyError}</Text> : null}
|
{chinaKeyError ? <Text color="error">{chinaKeyError}</Text> : null}
|
||||||
<Text dimColor>Enter to confirm · Esc to go back</Text>
|
{isCustomModelEntry && modelSuggestions.length > 0 && (
|
||||||
|
<Box flexDirection="column" gap={0}>
|
||||||
|
<Text dimColor>{chinaKeyValue.trim() ? 'Matching models:' : 'Known models:'}</Text>
|
||||||
|
{modelSuggestions.map(m => (
|
||||||
|
<Text key={m.id} dimColor>
|
||||||
|
{' '}
|
||||||
|
{m.id}{' '}
|
||||||
|
<Text>
|
||||||
|
({m.label} — {m.provider})
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Text dimColor>
|
||||||
|
{isCustomModelEntry ? 'Enter to continue · Esc to go back' : 'Enter to confirm · Esc to go back'}
|
||||||
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ export type ProviderPreset = {
|
||||||
icon: string
|
icon: string
|
||||||
baseURL: string
|
baseURL: string
|
||||||
apiKeyPage: string
|
apiKeyPage: string
|
||||||
|
modelsPage: string
|
||||||
freeTier: string
|
freeTier: string
|
||||||
keyFormat: string
|
keyFormat: string
|
||||||
codingPlan?: {
|
codingPlan?: {
|
||||||
|
|
@ -48,6 +49,7 @@ export const CHINA_LLM_PROVIDERS: ProviderPreset[] = [
|
||||||
icon: '\u{1F525}',
|
icon: '\u{1F525}',
|
||||||
baseURL: 'https://api.deepseek.com',
|
baseURL: 'https://api.deepseek.com',
|
||||||
apiKeyPage: 'https://platform.deepseek.com/api_keys',
|
apiKeyPage: 'https://platform.deepseek.com/api_keys',
|
||||||
|
modelsPage: 'https://api-docs.deepseek.com/zh-cn/',
|
||||||
freeTier: '5M tokens on signup (30 days), min top-up ¥10',
|
freeTier: '5M tokens on signup (30 days), min top-up ¥10',
|
||||||
keyFormat: 'sk-...',
|
keyFormat: 'sk-...',
|
||||||
models: [
|
models: [
|
||||||
|
|
@ -76,6 +78,7 @@ export const CHINA_LLM_PROVIDERS: ProviderPreset[] = [
|
||||||
icon: '\u{1F9E0}',
|
icon: '\u{1F9E0}',
|
||||||
baseURL: 'https://open.bigmodel.cn/api/paas/v4',
|
baseURL: 'https://open.bigmodel.cn/api/paas/v4',
|
||||||
apiKeyPage: 'https://open.bigmodel.cn/user/apiKeys',
|
apiKeyPage: 'https://open.bigmodel.cn/user/apiKeys',
|
||||||
|
modelsPage: 'https://docs.bigmodel.cn/cn/guide/start/model-overview',
|
||||||
freeTier: 'GLM-4.7-Flash / GLM-Z1-Flash free forever',
|
freeTier: 'GLM-4.7-Flash / GLM-Z1-Flash free forever',
|
||||||
keyFormat: '{id}.{secret}',
|
keyFormat: '{id}.{secret}',
|
||||||
codingPlan: {
|
codingPlan: {
|
||||||
|
|
@ -141,6 +144,8 @@ export const CHINA_LLM_PROVIDERS: ProviderPreset[] = [
|
||||||
icon: '☁️',
|
icon: '☁️',
|
||||||
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
||||||
apiKeyPage: 'https://bailian.console.aliyun.com',
|
apiKeyPage: 'https://bailian.console.aliyun.com',
|
||||||
|
modelsPage:
|
||||||
|
'https://help.aliyun.com/zh/model-studio/getting-started/models',
|
||||||
freeTier: '90-day free tier for all models after activation',
|
freeTier: '90-day free tier for all models after activation',
|
||||||
keyFormat: 'sk-...',
|
keyFormat: 'sk-...',
|
||||||
codingPlan: {
|
codingPlan: {
|
||||||
|
|
@ -191,6 +196,7 @@ export const CHINA_LLM_PROVIDERS: ProviderPreset[] = [
|
||||||
icon: '\u{1F4F1}',
|
icon: '\u{1F4F1}',
|
||||||
baseURL: 'https://api.xiaomimimo.com/v1',
|
baseURL: 'https://api.xiaomimimo.com/v1',
|
||||||
apiKeyPage: 'https://platform.xiaomimimo.com/api-keys',
|
apiKeyPage: 'https://platform.xiaomimimo.com/api-keys',
|
||||||
|
modelsPage: 'https://platform.xiaomimimo.com/models',
|
||||||
freeTier: 'Credits for new users, mimo-v2-flash low cost',
|
freeTier: 'Credits for new users, mimo-v2-flash low cost',
|
||||||
keyFormat: 'sk-...',
|
keyFormat: 'sk-...',
|
||||||
codingPlan: {
|
codingPlan: {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user