mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 13:55:50 +00:00
feat: 添加gemini协议适配 (#125)
* feat: 添加gemini协议适配 * Remove unrelated local files from Gemini PR
This commit is contained in:
@@ -48,6 +48,15 @@ type OAuthStatus =
|
||||
opusModel: string
|
||||
activeField: 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model'
|
||||
} // OpenAI Chat Completions API platform
|
||||
| {
|
||||
state: 'gemini_api'
|
||||
baseUrl: string
|
||||
apiKey: string
|
||||
haikuModel: string
|
||||
sonnetModel: string
|
||||
opusModel: string
|
||||
activeField: 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model'
|
||||
} // Gemini Generate Content API platform
|
||||
| { 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
|
||||
@@ -60,7 +69,6 @@ type OAuthStatus =
|
||||
}
|
||||
|
||||
const PASTE_HERE_MSG = 'Paste code here if prompted > '
|
||||
|
||||
export function ConsoleOAuthFlow({
|
||||
onDone,
|
||||
startingMessage,
|
||||
@@ -476,6 +484,16 @@ function OAuthStatusMessage({
|
||||
),
|
||||
value: 'openai_chat_api',
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Text>
|
||||
Gemini API ·{' '}
|
||||
<Text dimColor>Google Gemini native REST/SSE</Text>
|
||||
{'\n'}
|
||||
</Text>
|
||||
),
|
||||
value: 'gemini_api',
|
||||
},
|
||||
{
|
||||
label: (
|
||||
<Text>
|
||||
@@ -543,6 +561,17 @@ function OAuthStatusMessage({
|
||||
opusModel: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL ?? '',
|
||||
activeField: 'base_url',
|
||||
})
|
||||
} else if (value === 'gemini_api') {
|
||||
logEvent('tengu_gemini_api_selected', {})
|
||||
setOAuthStatus({
|
||||
state: 'gemini_api',
|
||||
baseUrl: process.env.GEMINI_BASE_URL ?? '',
|
||||
apiKey: process.env.GEMINI_API_KEY ?? '',
|
||||
haikuModel: process.env.ANTHROPIC_DEFAULT_HAIKU_MODEL ?? '',
|
||||
sonnetModel: process.env.ANTHROPIC_DEFAULT_SONNET_MODEL ?? '',
|
||||
opusModel: process.env.ANTHROPIC_DEFAULT_OPUS_MODEL ?? '',
|
||||
activeField: 'base_url',
|
||||
})
|
||||
} else if (value === 'platform') {
|
||||
logEvent('tengu_oauth_platform_selected', {})
|
||||
setOAuthStatus({ state: 'platform_setup' })
|
||||
@@ -974,6 +1003,238 @@ function OAuthStatusMessage({
|
||||
)
|
||||
}
|
||||
|
||||
case 'gemini_api':
|
||||
{
|
||||
type GeminiField = 'base_url' | 'api_key' | 'haiku_model' | 'sonnet_model' | 'opus_model'
|
||||
const GEMINI_FIELDS: GeminiField[] = [
|
||||
'base_url',
|
||||
'api_key',
|
||||
'haiku_model',
|
||||
'sonnet_model',
|
||||
'opus_model',
|
||||
]
|
||||
const gp = oauthStatus as {
|
||||
state: 'gemini_api'
|
||||
activeField: GeminiField
|
||||
baseUrl: string
|
||||
apiKey: string
|
||||
haikuModel: string
|
||||
sonnetModel: string
|
||||
opusModel: string
|
||||
}
|
||||
const { activeField, baseUrl, apiKey, haikuModel, sonnetModel, opusModel } = gp
|
||||
const geminiDisplayValues: Record<GeminiField, string> = {
|
||||
base_url: baseUrl,
|
||||
api_key: apiKey,
|
||||
haiku_model: haikuModel,
|
||||
sonnet_model: sonnetModel,
|
||||
opus_model: opusModel,
|
||||
}
|
||||
|
||||
const [geminiInputValue, setGeminiInputValue] = useState(
|
||||
() => geminiDisplayValues[activeField],
|
||||
)
|
||||
const [geminiInputCursorOffset, setGeminiInputCursorOffset] = useState(
|
||||
() => geminiDisplayValues[activeField].length,
|
||||
)
|
||||
|
||||
const buildGeminiState = useCallback(
|
||||
(field: GeminiField, value: string, newActive?: GeminiField) => {
|
||||
const s = {
|
||||
state: 'gemini_api' as const,
|
||||
activeField: newActive ?? activeField,
|
||||
baseUrl,
|
||||
apiKey,
|
||||
haikuModel,
|
||||
sonnetModel,
|
||||
opusModel,
|
||||
}
|
||||
switch (field) {
|
||||
case 'base_url':
|
||||
return { ...s, baseUrl: value }
|
||||
case 'api_key':
|
||||
return { ...s, apiKey: value }
|
||||
case 'haiku_model':
|
||||
return { ...s, haikuModel: value }
|
||||
case 'sonnet_model':
|
||||
return { ...s, sonnetModel: value }
|
||||
case 'opus_model':
|
||||
return { ...s, opusModel: value }
|
||||
}
|
||||
},
|
||||
[activeField, baseUrl, apiKey, haikuModel, sonnetModel, opusModel],
|
||||
)
|
||||
|
||||
const doGeminiSave = useCallback(() => {
|
||||
const finalVals = { ...geminiDisplayValues, [activeField]: geminiInputValue }
|
||||
if (!finalVals.haiku_model || !finalVals.sonnet_model || !finalVals.opus_model) {
|
||||
setOAuthStatus({
|
||||
state: 'error',
|
||||
message: 'Gemini setup requires Haiku, Sonnet, and Opus model names.',
|
||||
toRetry: {
|
||||
state: 'gemini_api',
|
||||
baseUrl: finalVals.base_url,
|
||||
apiKey: finalVals.api_key,
|
||||
haikuModel: finalVals.haiku_model,
|
||||
sonnetModel: finalVals.sonnet_model,
|
||||
opusModel: finalVals.opus_model,
|
||||
activeField,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const env: Record<string, string> = {}
|
||||
if (finalVals.base_url) env.GEMINI_BASE_URL = finalVals.base_url
|
||||
if (finalVals.api_key) env.GEMINI_API_KEY = finalVals.api_key
|
||||
if (finalVals.haiku_model) env.ANTHROPIC_DEFAULT_HAIKU_MODEL = finalVals.haiku_model
|
||||
if (finalVals.sonnet_model) env.ANTHROPIC_DEFAULT_SONNET_MODEL = finalVals.sonnet_model
|
||||
if (finalVals.opus_model) env.ANTHROPIC_DEFAULT_OPUS_MODEL = finalVals.opus_model
|
||||
const { error } = updateSettingsForSource('userSettings', {
|
||||
modelType: 'gemini' as any,
|
||||
env,
|
||||
} as any)
|
||||
if (error) {
|
||||
setOAuthStatus({
|
||||
state: 'error',
|
||||
message: `Failed to save: ${error.message}`,
|
||||
toRetry: {
|
||||
state: 'gemini_api',
|
||||
baseUrl: '',
|
||||
apiKey: '',
|
||||
haikuModel: '',
|
||||
sonnetModel: '',
|
||||
opusModel: '',
|
||||
activeField: 'base_url',
|
||||
},
|
||||
})
|
||||
} else {
|
||||
for (const [k, v] of Object.entries(env)) process.env[k] = v
|
||||
setOAuthStatus({ state: 'success' })
|
||||
void onDone()
|
||||
}
|
||||
}, [activeField, geminiInputValue, geminiDisplayValues, onDone, setOAuthStatus])
|
||||
|
||||
const handleGeminiEnter = useCallback(() => {
|
||||
const idx = GEMINI_FIELDS.indexOf(activeField)
|
||||
setOAuthStatus(buildGeminiState(activeField, geminiInputValue))
|
||||
if (idx === GEMINI_FIELDS.length - 1) {
|
||||
doGeminiSave()
|
||||
} else {
|
||||
const next = GEMINI_FIELDS[idx + 1]!
|
||||
setGeminiInputValue(geminiDisplayValues[next] ?? '')
|
||||
setGeminiInputCursorOffset((geminiDisplayValues[next] ?? '').length)
|
||||
}
|
||||
}, [
|
||||
activeField,
|
||||
buildGeminiState,
|
||||
doGeminiSave,
|
||||
geminiDisplayValues,
|
||||
geminiInputValue,
|
||||
setOAuthStatus,
|
||||
])
|
||||
|
||||
useKeybinding(
|
||||
'tabs:next',
|
||||
() => {
|
||||
const idx = GEMINI_FIELDS.indexOf(activeField)
|
||||
if (idx < GEMINI_FIELDS.length - 1) {
|
||||
setOAuthStatus(
|
||||
buildGeminiState(activeField, geminiInputValue, GEMINI_FIELDS[idx + 1]),
|
||||
)
|
||||
setGeminiInputValue(geminiDisplayValues[GEMINI_FIELDS[idx + 1]!] ?? '')
|
||||
setGeminiInputCursorOffset(
|
||||
(geminiDisplayValues[GEMINI_FIELDS[idx + 1]!] ?? '').length,
|
||||
)
|
||||
}
|
||||
},
|
||||
{ context: 'Tabs' },
|
||||
)
|
||||
useKeybinding(
|
||||
'tabs:previous',
|
||||
() => {
|
||||
const idx = GEMINI_FIELDS.indexOf(activeField)
|
||||
if (idx > 0) {
|
||||
setOAuthStatus(
|
||||
buildGeminiState(activeField, geminiInputValue, GEMINI_FIELDS[idx - 1]),
|
||||
)
|
||||
setGeminiInputValue(geminiDisplayValues[GEMINI_FIELDS[idx - 1]!] ?? '')
|
||||
setGeminiInputCursorOffset(
|
||||
(geminiDisplayValues[GEMINI_FIELDS[idx - 1]!] ?? '').length,
|
||||
)
|
||||
}
|
||||
},
|
||||
{ context: 'Tabs' },
|
||||
)
|
||||
useKeybinding(
|
||||
'confirm:no',
|
||||
() => {
|
||||
setOAuthStatus({ state: 'idle' })
|
||||
},
|
||||
{ context: 'Confirmation' },
|
||||
)
|
||||
|
||||
const geminiColumns = useTerminalSize().columns - 20
|
||||
|
||||
const renderGeminiRow = (
|
||||
field: GeminiField,
|
||||
label: string,
|
||||
opts?: { mask?: boolean },
|
||||
) => {
|
||||
const active = activeField === field
|
||||
const val = geminiDisplayValues[field]
|
||||
return (
|
||||
<Box>
|
||||
<Text
|
||||
backgroundColor={active ? 'suggestion' : undefined}
|
||||
color={active ? 'inverseText' : undefined}
|
||||
>
|
||||
{` ${label} `}
|
||||
</Text>
|
||||
<Text> </Text>
|
||||
{active ? (
|
||||
<TextInput
|
||||
value={geminiInputValue}
|
||||
onChange={setGeminiInputValue}
|
||||
onSubmit={handleGeminiEnter}
|
||||
cursorOffset={geminiInputCursorOffset}
|
||||
onChangeCursorOffset={setGeminiInputCursorOffset}
|
||||
columns={geminiColumns}
|
||||
mask={opts?.mask ? '*' : undefined}
|
||||
focus={true}
|
||||
/>
|
||||
) : val ? (
|
||||
<Text color="success">
|
||||
{opts?.mask
|
||||
? val.slice(0, 8) + '\u00b7'.repeat(Math.max(0, val.length - 8))
|
||||
: val}
|
||||
</Text>
|
||||
) : null}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
<Text bold>Gemini API Setup</Text>
|
||||
<Text dimColor>
|
||||
Configure a Gemini Generate Content compatible endpoint. Base URL is
|
||||
optional and defaults to Google's v1beta API.
|
||||
</Text>
|
||||
<Box flexDirection="column" gap={1}>
|
||||
{renderGeminiRow('base_url', 'Base URL ')}
|
||||
{renderGeminiRow('api_key', 'API Key ', { mask: true })}
|
||||
{renderGeminiRow('haiku_model', 'Haiku ')}
|
||||
{renderGeminiRow('sonnet_model', 'Sonnet ')}
|
||||
{renderGeminiRow('opus_model', 'Opus ')}
|
||||
</Box>
|
||||
<Text dimColor>
|
||||
Tab to switch · Enter on last field to save · Esc to go back
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
case 'platform_setup':
|
||||
return (
|
||||
<Box flexDirection="column" gap={1} marginTop={1}>
|
||||
|
||||
Reference in New Issue
Block a user