Skip to content

Commit a02e4f2

Browse files
committed
feat: add custom model input with suggestions and model listing links
1 parent dc90f0d commit a02e4f2

2 files changed

Lines changed: 112 additions & 29 deletions

File tree

src/components/ConsoleOAuthFlow.tsx

Lines changed: 106 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { getOauthAccountInfo, validateForceLoginOrg } from '../utils/auth.js';
1919
import { openBrowser } from '../utils/browser.js';
2020
import { logError } from '../utils/log.js';
2121
import { getSettings_DEPRECATED, updateSettingsForSource } from '../utils/settings/settings.js';
22-
import { CHINA_LLM_PROVIDERS, type ProviderPreset, resolveChinaProviderBaseURL } from '../utils/chinaLlmProviders.js';
22+
import { CHINA_LLM_PROVIDERS, type ProviderPreset, resolveChinaProviderBaseURL } from 'src/utils/chinaLlmProviders.js';
2323
import { Select } from './CustomSelect/select.js';
2424
import { Spinner } from './Spinner.js';
2525
import TextInput from './TextInput.js';
@@ -1372,26 +1372,38 @@ function OAuthStatusMessage({
13721372
</Text>
13731373
<Box>
13741374
<Select
1375-
options={models.map(m => {
1376-
const priceLabel =
1377-
m.inputPricePerMTok === 0 && m.outputPricePerMTok === 0
1378-
? 'Free'
1379-
: ${m.inputPricePerMTok}${m.outputPricePerMTok}`;
1380-
const tagLabel = m.tags?.length ? ` [${m.tags.join(', ')}]` : '';
1381-
return {
1375+
options={[
1376+
...models.map(m => {
1377+
const priceLabel =
1378+
m.inputPricePerMTok === 0 && m.outputPricePerMTok === 0
1379+
? 'Free'
1380+
: ${m.inputPricePerMTok}${m.outputPricePerMTok}`;
1381+
const tagLabel = m.tags?.length ? ` [${m.tags.join(', ')}]` : '';
1382+
return {
1383+
label: (
1384+
<Text>
1385+
{m.label} ·{' '}
1386+
<Text dimColor>
1387+
{priceLabel} · {m.contextWindow}
1388+
{tagLabel}
1389+
</Text>
1390+
{'\n'}
1391+
</Text>
1392+
),
1393+
value: m.id,
1394+
};
1395+
}),
1396+
{
13821397
label: (
13831398
<Text>
1384-
{m.label} ·{' '}
1385-
<Text dimColor>
1386-
{priceLabel} · {m.contextWindow}
1387-
{tagLabel}
1388-
</Text>
1399+
✏️ Custom model
1400+
<Text dimColor> · enter model name manually</Text>
13891401
{'\n'}
13901402
</Text>
13911403
),
1392-
value: m.id,
1393-
};
1394-
})}
1404+
value: '__custom__',
1405+
},
1406+
]}
13951407
onChange={value => {
13961408
logEvent('tengu_china_model_selected', {});
13971409
setOAuthStatus({ state: 'china_apikey', provider, mode: accessMode, modelId: value, apiKey: '' });
@@ -1410,30 +1422,50 @@ function OAuthStatusMessage({
14101422
const [chinaKeyError, setChinaKeyError] = useState<string | null>(null);
14111423

14121424
const doChinaSave = useCallback(() => {
1425+
const effectiveModelId = modelId === '__custom__' ? chinaKeyValue.trim() : modelId;
1426+
if (!effectiveModelId) {
1427+
setChinaKeyError(modelId === '__custom__' ? 'Please enter a model name' : 'Please enter an API key');
1428+
return;
1429+
}
1430+
if (modelId === '__custom__') {
1431+
logEvent('tengu_china_custom_model_entered', {});
1432+
setOAuthStatus({ state: 'china_apikey', provider, mode: accessMode, modelId: effectiveModelId, apiKey: '' });
1433+
setChinaKeyValue('');
1434+
setChinaKeyError(null);
1435+
return;
1436+
}
14131437
if (!chinaKeyValue.trim()) {
14141438
setChinaKeyError('Please enter an API key');
14151439
return;
14161440
}
14171441
const baseUrl = resolveChinaProviderBaseURL(provider.id, accessMode);
1418-
const env: Record<string, string> = {
1442+
const env: Record<string, string | undefined> = {
1443+
OPENAI_AUTH_MODE: undefined,
14191444
OPENAI_BASE_URL: baseUrl,
14201445
OPENAI_API_KEY: chinaKeyValue.trim(),
14211446
OPENAI_DEFAULT_SONNET_MODEL: modelId,
14221447
OPENAI_DEFAULT_HAIKU_MODEL: modelId,
14231448
OPENAI_DEFAULT_OPUS_MODEL: modelId,
14241449
};
1425-
const { error } = updateSettingsForSource('userSettings', {
1426-
modelType: 'openai' as any,
1427-
env,
1428-
} as any);
1450+
const settingsUpdate: Parameters<typeof updateSettingsForSource>[1] = {
1451+
modelType: 'openai',
1452+
env: env as unknown as Record<string, string>,
1453+
};
1454+
const { error } = updateSettingsForSource('userSettings', settingsUpdate);
14291455
if (error) {
14301456
setOAuthStatus({
14311457
state: 'error',
14321458
message: 'Failed to save settings. Please try again.',
14331459
toRetry: { state: 'china_apikey', provider, mode: accessMode, modelId, apiKey: chinaKeyValue },
14341460
});
14351461
} else {
1436-
for (const [k, v] of Object.entries(env)) process.env[k] = v;
1462+
for (const [k, v] of Object.entries(env)) {
1463+
if (v === undefined) {
1464+
delete process.env[k];
1465+
} else {
1466+
process.env[k] = v;
1467+
}
1468+
}
14371469
logEvent('tengu_china_login_success', {});
14381470
setOAuthStatus({ state: 'success' });
14391471
void onDone();
@@ -1448,18 +1480,47 @@ function OAuthStatusMessage({
14481480
{ context: 'Confirmation' },
14491481
);
14501482

1483+
const isCustomModelEntry = modelId === '__custom__';
1484+
const allModels = CHINA_LLM_PROVIDERS.flatMap(p =>
1485+
p.models.map(m => ({ id: m.id, label: m.label, provider: p.label })),
1486+
);
1487+
const modelSuggestions = isCustomModelEntry
1488+
? chinaKeyValue.trim()
1489+
? allModels.filter(m => m.id.toLowerCase().includes(chinaKeyValue.trim().toLowerCase()))
1490+
: allModels
1491+
: [];
1492+
const keyPage = isCustomModelEntry
1493+
? provider.apiKeyPage
1494+
: accessMode === 'coding-plan' && provider.codingPlan
1495+
? provider.codingPlan.purchasePage
1496+
: provider.apiKeyPage;
1497+
const keyFormat = isCustomModelEntry
1498+
? provider.keyFormat
1499+
: accessMode === 'coding-plan' && provider.codingPlan
1500+
? provider.codingPlan.keyFormat
1501+
: provider.keyFormat;
1502+
14511503
return (
14521504
<Box flexDirection="column" gap={1} marginTop={1}>
14531505
<Text bold>
1454-
{provider.icon} {provider.label} API Key
1506+
{provider.icon} {provider.label} {isCustomModelEntry ? '— Custom Model' : 'API Key'}
14551507
</Text>
14561508
<Box flexDirection="column" gap={0}>
1457-
<Text dimColor> Get your key: {provider.apiKeyPage}</Text>
1458-
<Text dimColor> {provider.freeTier}</Text>
1459-
<Text dimColor> Key format: {provider.keyFormat}</Text>
1509+
{isCustomModelEntry ? (
1510+
<Text dimColor> Enter any model ID supported by this provider. Browse models: {provider.modelsPage}</Text>
1511+
) : (
1512+
<>
1513+
<Text dimColor> Get your key: {keyPage}</Text>
1514+
<Text dimColor>
1515+
{' '}
1516+
{accessMode === 'coding-plan' ? 'Use your Coding Plan credential here' : provider.freeTier}
1517+
</Text>
1518+
<Text dimColor> Key format: {keyFormat}</Text>
1519+
</>
1520+
)}
14601521
</Box>
14611522
<Box>
1462-
<Text>API Key: </Text>
1523+
<Text>{isCustomModelEntry ? 'Model name: ' : 'API Key: '}</Text>
14631524
<TextInput
14641525
value={chinaKeyValue}
14651526
onChange={v => {
@@ -1470,12 +1531,28 @@ function OAuthStatusMessage({
14701531
cursorOffset={chinaKeyCursor}
14711532
onChangeCursorOffset={setChinaKeyCursor}
14721533
columns={useTerminalSize().columns - 12}
1473-
mask="*"
1534+
mask={isCustomModelEntry ? undefined : '*'}
14741535
focus={true}
14751536
/>
14761537
</Box>
14771538
{chinaKeyError ? <Text color="error">{chinaKeyError}</Text> : null}
1478-
<Text dimColor>Enter to confirm · Esc to go back</Text>
1539+
{isCustomModelEntry && modelSuggestions.length > 0 && (
1540+
<Box flexDirection="column" gap={0}>
1541+
<Text dimColor>{chinaKeyValue.trim() ? 'Matching models:' : 'Known models:'}</Text>
1542+
{modelSuggestions.map(m => (
1543+
<Text key={m.id} dimColor>
1544+
{' '}
1545+
{m.id}{' '}
1546+
<Text>
1547+
({m.label}{m.provider})
1548+
</Text>
1549+
</Text>
1550+
))}
1551+
</Box>
1552+
)}
1553+
<Text dimColor>
1554+
{isCustomModelEntry ? 'Enter to continue · Esc to go back' : 'Enter to confirm · Esc to go back'}
1555+
</Text>
14791556
</Box>
14801557
);
14811558
}

src/utils/chinaLlmProviders.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ export type ProviderPreset = {
2929
icon: string
3030
baseURL: string
3131
apiKeyPage: string
32+
modelsPage: string
3233
freeTier: string
3334
keyFormat: string
3435
codingPlan?: {
@@ -48,6 +49,7 @@ export const CHINA_LLM_PROVIDERS: ProviderPreset[] = [
4849
icon: '\u{1F525}',
4950
baseURL: 'https://api.deepseek.com',
5051
apiKeyPage: 'https://platform.deepseek.com/api_keys',
52+
modelsPage: 'https://api-docs.deepseek.com/zh-cn/',
5153
freeTier: '5M tokens on signup (30 days), min top-up ¥10',
5254
keyFormat: 'sk-...',
5355
models: [
@@ -76,6 +78,7 @@ export const CHINA_LLM_PROVIDERS: ProviderPreset[] = [
7678
icon: '\u{1F9E0}',
7779
baseURL: 'https://open.bigmodel.cn/api/paas/v4',
7880
apiKeyPage: 'https://open.bigmodel.cn/user/apiKeys',
81+
modelsPage: 'https://docs.bigmodel.cn/cn/guide/start/model-overview',
7982
freeTier: 'GLM-4.7-Flash / GLM-Z1-Flash free forever',
8083
keyFormat: '{id}.{secret}',
8184
codingPlan: {
@@ -141,6 +144,8 @@ export const CHINA_LLM_PROVIDERS: ProviderPreset[] = [
141144
icon: '☁️',
142145
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
143146
apiKeyPage: 'https://bailian.console.aliyun.com',
147+
modelsPage:
148+
'https://help.aliyun.com/zh/model-studio/getting-started/models',
144149
freeTier: '90-day free tier for all models after activation',
145150
keyFormat: 'sk-...',
146151
codingPlan: {
@@ -191,6 +196,7 @@ export const CHINA_LLM_PROVIDERS: ProviderPreset[] = [
191196
icon: '\u{1F4F1}',
192197
baseURL: 'https://api.xiaomimimo.com/v1',
193198
apiKeyPage: 'https://platform.xiaomimimo.com/api-keys',
199+
modelsPage: 'https://platform.xiaomimimo.com/models',
194200
freeTier: 'Credits for new users, mimo-v2-flash low cost',
195201
keyFormat: 'sk-...',
196202
codingPlan: {

0 commit comments

Comments
 (0)