Skip to content

Commit 26ef936

Browse files
committed
feat: add LiteLLM OAuth2 SSO authentication button
Implement OAuth2 SSO integration for LiteLLM proxy authentication: - Add OAuth URL generator for LiteLLM SSO flow with proper redirect_uri - Implement URI callback handler for LiteLLM OAuth2 responses - Add ClineProvider method to handle LiteLLM OAuth callback and store tokens - Create LiteLLM SSO authentication button in settings UI - Add internationalization support for LiteLLM OAuth button text This enables users to authenticate with their LiteLLM proxy via SSO by: 1. Entering their LiteLLM base URL in settings 2. Clicking "Get LiteLLM API Key via SSO" button 3. Completing OAuth flow in browser 4. Automatically receiving and storing the API token The implementation follows OAuth2 RFC 6749 standards and supports both JSON response format and VSCode extension redirect flows as implemented in LiteLLM PR #13227.
1 parent 8513263 commit 26ef936

File tree

6 files changed

+82
-0
lines changed

6 files changed

+82
-0
lines changed

src/activate/handleUri.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,35 @@ export const handleUri = async (uri: vscode.Uri) => {
3535
}
3636
break
3737
}
38+
case "/litellm": {
39+
const accessToken = query.get("access_token")
40+
const tokenType = query.get("token_type")
41+
const expiresIn = query.get("expires_in")
42+
const scope = query.get("scope")
43+
const error = query.get("error")
44+
const errorDescription = query.get("error_description")
45+
46+
if (error) {
47+
// Handle OAuth error
48+
console.error(`LiteLLM OAuth error: ${error}`, errorDescription)
49+
vscode.window.showErrorMessage(
50+
`LiteLLM authentication failed: ${error}${errorDescription ? ` - ${errorDescription}` : ""}`,
51+
)
52+
return
53+
}
54+
55+
if (accessToken) {
56+
await visibleProvider.handleLiteLLMCallback({
57+
accessToken,
58+
tokenType: tokenType || "Bearer",
59+
expiresIn: expiresIn ? parseInt(expiresIn, 10) : 86400,
60+
scope: scope || undefined,
61+
})
62+
} else {
63+
vscode.window.showErrorMessage("LiteLLM authentication failed: No access token received")
64+
}
65+
break
66+
}
3867
case "/auth/clerk/callback": {
3968
const code = query.get("code")
4069
const state = query.get("state")

src/core/webview/ClineProvider.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
requestyDefaultModelId,
2828
openRouterDefaultModelId,
2929
glamaDefaultModelId,
30+
litellmDefaultModelId,
3031
ORGANIZATION_ALLOW_ALL,
3132
DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT,
3233
DEFAULT_WRITE_DELAY_MS,
@@ -1236,6 +1237,38 @@ export class ClineProvider
12361237
await this.upsertProviderProfile(currentApiConfigName, newConfiguration)
12371238
}
12381239

1240+
// LiteLLM
1241+
async handleLiteLLMCallback(oauthResponse: {
1242+
accessToken: string
1243+
tokenType: string
1244+
expiresIn: number
1245+
scope?: string
1246+
}) {
1247+
let { apiConfiguration, currentApiConfigName } = await this.getState()
1248+
1249+
// Store the OAuth response metadata
1250+
const { accessToken, tokenType, expiresIn, scope } = oauthResponse
1251+
const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString()
1252+
1253+
this.log(
1254+
`LiteLLM OAuth success: ${tokenType} token expires in ${expiresIn}s (${expiresAt})${scope ? ` with scope: ${scope}` : ""}`,
1255+
)
1256+
1257+
const newConfiguration: ProviderSettings = {
1258+
...apiConfiguration,
1259+
apiProvider: "litellm",
1260+
litellmApiKey: accessToken,
1261+
litellmModelId: apiConfiguration?.litellmModelId || litellmDefaultModelId,
1262+
}
1263+
await this.upsertProviderProfile(currentApiConfigName, newConfiguration)
1264+
1265+
// Notify webview of the configuration update
1266+
await this.postStateToWebview()
1267+
1268+
// Show success message to user
1269+
vscode.window.showInformationMessage("Successfully authenticated with LiteLLM via SSO!")
1270+
}
1271+
12391272
// Glama
12401273

12411274
async handleGlamaCallback(code: string) {

webview-ui/src/components/settings/ApiOptions.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,7 @@ const ApiOptions = ({
523523
setApiConfigurationField={setApiConfigurationField}
524524
organizationAllowList={organizationAllowList}
525525
modelValidationError={modelValidationError}
526+
uriScheme={uriScheme}
526527
/>
527528
)}
528529

webview-ui/src/components/settings/providers/LiteLLM.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { vscode } from "@src/utils/vscode"
1010
import { useExtensionState } from "@src/context/ExtensionStateContext"
1111
import { useAppTranslation } from "@src/i18n/TranslationContext"
1212
import { Button } from "@src/components/ui"
13+
import { getLiteLLMAuthUrl } from "@src/oauth/urls"
14+
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
1315

1416
import { inputEventTransform } from "../transforms"
1517
import { ModelPicker } from "../ModelPicker"
@@ -19,13 +21,15 @@ type LiteLLMProps = {
1921
setApiConfigurationField: (field: keyof ProviderSettings, value: ProviderSettings[keyof ProviderSettings]) => void
2022
organizationAllowList: OrganizationAllowList
2123
modelValidationError?: string
24+
uriScheme?: string
2225
}
2326

2427
export const LiteLLM = ({
2528
apiConfiguration,
2629
setApiConfigurationField,
2730
organizationAllowList,
2831
modelValidationError,
32+
uriScheme,
2933
}: LiteLLMProps) => {
3034
const { t } = useAppTranslation()
3135
const { routerModels } = useExtensionState()
@@ -111,6 +115,15 @@ export const LiteLLM = ({
111115
{t("settings:providers.apiKeyStorageNotice")}
112116
</div>
113117

118+
{apiConfiguration?.litellmBaseUrl && (
119+
<VSCodeButtonLink
120+
href={getLiteLLMAuthUrl(apiConfiguration.litellmBaseUrl, uriScheme)}
121+
style={{ width: "100%" }}
122+
appearance="primary">
123+
{t("settings:providers.getLiteLLMApiKey")}
124+
</VSCodeButtonLink>
125+
)}
126+
114127
<Button
115128
variant="outline"
116129
onClick={handleRefreshModels}

webview-ui/src/i18n/locales/en/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,7 @@
235235
"apiKeyStorageNotice": "API keys are stored securely in VSCode's Secret Storage",
236236
"glamaApiKey": "Glama API Key",
237237
"getGlamaApiKey": "Get Glama API Key",
238+
"getLiteLLMApiKey": "Get LiteLLM API Key via SSO",
238239
"useCustomBaseUrl": "Use custom base URL",
239240
"useReasoning": "Enable reasoning",
240241
"useHostHeader": "Use custom Host header",

webview-ui/src/oauth/urls.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,8 @@ export function getOpenRouterAuthUrl(uriScheme?: string) {
1515
export function getRequestyAuthUrl(uriScheme?: string) {
1616
return `https://app.requesty.ai/oauth/authorize?callback_url=${getCallbackUrl("requesty", uriScheme)}`
1717
}
18+
19+
export function getLiteLLMAuthUrl(baseUrl: string, uriScheme?: string) {
20+
const cleanBaseUrl = baseUrl.replace(/\/+$/, "")
21+
return `${cleanBaseUrl}/sso/key/generate?response_type=oauth_token&redirect_uri=${getCallbackUrl("litellm", uriScheme)}`
22+
}

0 commit comments

Comments
 (0)