From ae8e45b2a898787724ade763b0210fb9284f38fd Mon Sep 17 00:00:00 2001 From: YuanyuanMa03 <118096301+YuanyuanMa03@users.noreply.github.com> Date: Mon, 25 May 2026 04:07:19 +0000 Subject: [PATCH 1/4] docs: update contributors --- contributors.svg | 52 +++++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/contributors.svg b/contributors.svg index c8d49ab998..0d6ecea8f0 100644 --- a/contributors.svg +++ b/contributors.svg @@ -11,49 +11,49 @@ - + - - - - - - - - - - - + + + + + + + + + + + + + - + - + - + - + - + - + - + - + - + - + - - - + @@ -74,5 +74,7 @@ + + \ No newline at end of file From e7b664e70f7aae0d1e814c6864909cb785e69204 Mon Sep 17 00:00:00 2001 From: YuanyuanMa03 <118096301+YuanyuanMa03@users.noreply.github.com> Date: Mon, 1 Jun 2026 04:22:26 +0000 Subject: [PATCH 2/4] docs: update contributors --- contributors.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contributors.svg b/contributors.svg index 0d6ecea8f0..bae442e85f 100644 --- a/contributors.svg +++ b/contributors.svg @@ -23,9 +23,9 @@ - + - + From dc90f0d214c459bcc3d44c398dfbd3e652e2ae7a Mon Sep 17 00:00:00 2001 From: YuanyuanMa03 <2942204237@qq.com> Date: Thu, 4 Jun 2026 18:35:48 +0800 Subject: [PATCH 3/4] feat: add China LLM providers guided login flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a guided login experience for 4 domestic (China) LLM providers in the /login command: DeepSeek, Zhipu GLM, Tongyi Qianwen, and MiMo Xiaomi. Each provider includes model presets with pricing, context windows, and optional Coding Plan integration. - New file: src/utils/chinaLlmProviders.ts — provider preset configs - Modified: src/components/ConsoleOAuthFlow.tsx — 4-step guided flow (select provider → select mode → select model → enter API key) All providers are OpenAI-compatible; credentials saved as OPENAI_BASE_URL + OPENAI_API_KEY under modelType: 'openai'. Co-Authored-By: Claude Opus 4.7 --- src/components/ConsoleOAuthFlow.tsx | 208 +++++++++++++++++++++ src/utils/chinaLlmProviders.ts | 274 ++++++++++++++++++++++++++++ 2 files changed, 482 insertions(+) create mode 100644 src/utils/chinaLlmProviders.ts diff --git a/src/components/ConsoleOAuthFlow.tsx b/src/components/ConsoleOAuthFlow.tsx index 084cdf2d05..29d8f8c55d 100644 --- a/src/components/ConsoleOAuthFlow.tsx +++ b/src/components/ConsoleOAuthFlow.tsx @@ -19,6 +19,7 @@ import { getOauthAccountInfo, validateForceLoginOrg } from '../utils/auth.js'; import { openBrowser } from '../utils/browser.js'; import { logError } from '../utils/log.js'; import { getSettings_DEPRECATED, updateSettingsForSource } from '../utils/settings/settings.js'; +import { CHINA_LLM_PROVIDERS, type ProviderPreset, resolveChinaProviderBaseURL } from '../utils/chinaLlmProviders.js'; import { Select } from './CustomSelect/select.js'; import { Spinner } from './Spinner.js'; import TextInput from './TextInput.js'; @@ -65,6 +66,10 @@ type OAuthStatus = opusModel: string; activeField: 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model'; } // Gemini Generate Content API platform + | { state: 'china_provider_select'; activeIndex: number } // China LLM: pick provider + | { state: 'china_mode_select'; provider: ProviderPreset; activeIndex: number } // China LLM: pick access mode + | { state: 'china_model_select'; provider: ProviderPreset; mode: 'api' | 'coding-plan'; activeIndex: number } // China LLM: pick model + | { state: 'china_apikey'; provider: ProviderPreset; mode: 'api' | 'coding-plan'; modelId: string; apiKey: string } // China LLM: enter API key | { state: 'ready_to_start' } // Flow started, waiting for browser to open | { state: 'waiting_for_login'; url: string } // Browser opened, waiting for user to login | { state: 'creating_api_key' } // Got access token, creating API key @@ -455,6 +460,15 @@ function OAuthStatusMessage({ ), value: 'openai_chat_api', }, + { + label: ( + + China LLM Providers · DeepSeek, Zhipu GLM, Qwen, MiMo + {'\n'} + + ), + value: 'china_providers', + }, { label: ( @@ -534,6 +548,9 @@ function OAuthStatusMessage({ opusModel: process.env.OPENAI_DEFAULT_OPUS_MODEL ?? '', activeField: 'base_url', }); + } else if (value === 'china_providers') { + logEvent('tengu_china_providers_selected', {}); + setOAuthStatus({ state: 'china_provider_select', activeIndex: 0 }); } else if (value === 'chatgpt_subscription') { logEvent('tengu_chatgpt_subscription_selected', {}); setOAuthStatus({ @@ -1272,6 +1289,197 @@ function OAuthStatusMessage({ ); } + case 'china_provider_select': { + return ( + + Select China LLM Provider + Direct connection, no proxy needed. All providers are OpenAI-compatible. + + ({ + label: ( + + {m.label} · {m.desc} + {'\n'} + + ), + value: m.id, + }))} + onChange={value => { + logEvent('tengu_china_mode_selected', {}); + setOAuthStatus({ + state: 'china_model_select', + provider, + mode: value as 'api' | 'coding-plan', + activeIndex: 0, + }); + }} + /> + + + No plan? Select "Pay-as-you-go" + {provider.id === 'zhipu' ? ' · GLM-4.7-Flash is free forever' : ''} + + + ); + } + + case 'china_model_select': { + const { provider, mode: accessMode } = oauthStatus; + const models = provider.models; + return ( + + + {provider.icon} {provider.label} — Select Model + + + { - const priceLabel = - m.inputPricePerMTok === 0 && m.outputPricePerMTok === 0 - ? 'Free' - : `¥${m.inputPricePerMTok}/¥${m.outputPricePerMTok}`; - const tagLabel = m.tags?.length ? ` [${m.tags.join(', ')}]` : ''; - return { + options={[ + ...models.map(m => { + const priceLabel = + m.inputPricePerMTok === 0 && m.outputPricePerMTok === 0 + ? 'Free' + : `¥${m.inputPricePerMTok}/¥${m.outputPricePerMTok}`; + const tagLabel = m.tags?.length ? ` [${m.tags.join(', ')}]` : ''; + return { + label: ( + + {m.label} ·{' '} + + {priceLabel} · {m.contextWindow} + {tagLabel} + + {'\n'} + + ), + value: m.id, + }; + }), + { label: ( - {m.label} ·{' '} - - {priceLabel} · {m.contextWindow} - {tagLabel} - + ✏️ Custom model + · enter model name manually {'\n'} ), - value: m.id, - }; - })} + value: '__custom__', + }, + ]} onChange={value => { logEvent('tengu_china_model_selected', {}); setOAuthStatus({ state: 'china_apikey', provider, mode: accessMode, modelId: value, apiKey: '' }); @@ -1410,22 +1422,36 @@ function OAuthStatusMessage({ const [chinaKeyError, setChinaKeyError] = useState(null); 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()) { setChinaKeyError('Please enter an API key'); return; } const baseUrl = resolveChinaProviderBaseURL(provider.id, accessMode); - const env: Record = { + const env: Record = { + OPENAI_AUTH_MODE: undefined, OPENAI_BASE_URL: baseUrl, OPENAI_API_KEY: chinaKeyValue.trim(), OPENAI_DEFAULT_SONNET_MODEL: modelId, OPENAI_DEFAULT_HAIKU_MODEL: modelId, OPENAI_DEFAULT_OPUS_MODEL: modelId, }; - const { error } = updateSettingsForSource('userSettings', { - modelType: 'openai' as any, - env, - } as any); + const settingsUpdate: Parameters[1] = { + modelType: 'openai', + env: env as unknown as Record, + }; + const { error } = updateSettingsForSource('userSettings', settingsUpdate); if (error) { setOAuthStatus({ state: 'error', @@ -1433,7 +1459,13 @@ function OAuthStatusMessage({ toRetry: { state: 'china_apikey', provider, mode: accessMode, modelId, apiKey: chinaKeyValue }, }); } 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', {}); setOAuthStatus({ state: 'success' }); void onDone(); @@ -1448,18 +1480,47 @@ function OAuthStatusMessage({ { 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 ( - {provider.icon} {provider.label} API Key + {provider.icon} {provider.label} {isCustomModelEntry ? '— Custom Model' : 'API Key'} - Get your key: {provider.apiKeyPage} - {provider.freeTier} - Key format: {provider.keyFormat} + {isCustomModelEntry ? ( + Enter any model ID supported by this provider. Browse models: {provider.modelsPage} + ) : ( + <> + Get your key: {keyPage} + + {' '} + {accessMode === 'coding-plan' ? 'Use your Coding Plan credential here' : provider.freeTier} + + Key format: {keyFormat} + + )} - API Key: + {isCustomModelEntry ? 'Model name: ' : 'API Key: '} { @@ -1470,12 +1531,28 @@ function OAuthStatusMessage({ cursorOffset={chinaKeyCursor} onChangeCursorOffset={setChinaKeyCursor} columns={useTerminalSize().columns - 12} - mask="*" + mask={isCustomModelEntry ? undefined : '*'} focus={true} /> {chinaKeyError ? {chinaKeyError} : null} - Enter to confirm · Esc to go back + {isCustomModelEntry && modelSuggestions.length > 0 && ( + + {chinaKeyValue.trim() ? 'Matching models:' : 'Known models:'} + {modelSuggestions.map(m => ( + + {' '} + {m.id}{' '} + + ({m.label} — {m.provider}) + + + ))} + + )} + + {isCustomModelEntry ? 'Enter to continue · Esc to go back' : 'Enter to confirm · Esc to go back'} + ); } diff --git a/src/utils/chinaLlmProviders.ts b/src/utils/chinaLlmProviders.ts index c51ffdc1c6..a5f3bad723 100644 --- a/src/utils/chinaLlmProviders.ts +++ b/src/utils/chinaLlmProviders.ts @@ -29,6 +29,7 @@ export type ProviderPreset = { icon: string baseURL: string apiKeyPage: string + modelsPage: string freeTier: string keyFormat: string codingPlan?: { @@ -48,6 +49,7 @@ export const CHINA_LLM_PROVIDERS: ProviderPreset[] = [ icon: '\u{1F525}', baseURL: 'https://api.deepseek.com', 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', keyFormat: 'sk-...', models: [ @@ -76,6 +78,7 @@ export const CHINA_LLM_PROVIDERS: ProviderPreset[] = [ icon: '\u{1F9E0}', baseURL: 'https://open.bigmodel.cn/api/paas/v4', 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', keyFormat: '{id}.{secret}', codingPlan: { @@ -141,6 +144,8 @@ export const CHINA_LLM_PROVIDERS: ProviderPreset[] = [ icon: '☁️', baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1', 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', keyFormat: 'sk-...', codingPlan: { @@ -191,6 +196,7 @@ export const CHINA_LLM_PROVIDERS: ProviderPreset[] = [ icon: '\u{1F4F1}', baseURL: 'https://api.xiaomimimo.com/v1', apiKeyPage: 'https://platform.xiaomimimo.com/api-keys', + modelsPage: 'https://platform.xiaomimimo.com/models', freeTier: 'Credits for new users, mimo-v2-flash low cost', keyFormat: 'sk-...', codingPlan: {