Skip to content

Commit e810a88

Browse files
mrubensdtrugman
andauthored
Welcome page OAuth (#1913)
* Add Requesty OAuth flow * New 1-click onboarding flow * Requesty: Use correct default model info * When called from the onboard flow, created the default profile Glama OAuth handler changed for consistency. * Add router images * Shuffle the routers * Translate * Appease knip --------- Co-authored-by: Daniel Trugman <[email protected]>
1 parent 003388a commit e810a88

File tree

31 files changed

+502
-141
lines changed

31 files changed

+502
-141
lines changed

.changeset/late-chefs-remember.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"roo-cline": patch
3+
---
4+
5+
Update the welcome page to provide 1-click OAuth flows with LLM routers or more advanced configuration with custom providers.

.vscodeignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@ webview-ui/node_modules/**
4848
# Include default themes JSON files used in getTheme
4949
!src/integrations/theme/default-themes/**
5050

51-
# Include icons
51+
# Include icons and images
5252
!assets/icons/**
53+
!assets/images/**
5354

5455
# Include .env file for telemetry
5556
!.env

assets/images/openrouter.png

6.56 KB
Loading

assets/images/requesty.png

12.9 KB
Loading

src/activate/handleUri.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ export const handleUri = async (uri: vscode.Uri) => {
66
const path = uri.path
77
const query = new URLSearchParams(uri.query.replace(/\+/g, "%2B"))
88
const visibleProvider = ClineProvider.getVisibleInstance()
9-
109
if (!visibleProvider) {
1110
return
1211
}
@@ -26,6 +25,13 @@ export const handleUri = async (uri: vscode.Uri) => {
2625
}
2726
break
2827
}
28+
case "/requesty": {
29+
const code = query.get("code")
30+
if (code) {
31+
await visibleProvider.handleRequestyCallback(code)
32+
}
33+
break
34+
}
2935
default:
3036
break
3137
}

src/api/providers/__tests__/requesty.test.ts

Lines changed: 50 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Anthropic } from "@anthropic-ai/sdk"
22
import OpenAI from "openai"
3-
import { ApiHandlerOptions, ModelInfo, requestyModelInfoSaneDefaults } from "../../../shared/api"
3+
import { ApiHandlerOptions, ModelInfo, requestyDefaultModelInfo } from "../../../shared/api"
44
import { RequestyHandler } from "../requesty"
55
import { convertToOpenAiMessages } from "../../transform/openai-format"
66
import { convertToR1Format } from "../../transform/r1-format"
@@ -18,14 +18,17 @@ describe("RequestyHandler", () => {
1818
requestyApiKey: "test-key",
1919
requestyModelId: "test-model",
2020
requestyModelInfo: {
21-
maxTokens: 1000,
22-
contextWindow: 4000,
23-
supportsPromptCache: false,
21+
maxTokens: 8192,
22+
contextWindow: 200_000,
2423
supportsImages: true,
25-
inputPrice: 1,
26-
outputPrice: 10,
27-
cacheReadsPrice: 0.1,
28-
cacheWritesPrice: 1.5,
24+
supportsComputerUse: true,
25+
supportsPromptCache: true,
26+
inputPrice: 3.0,
27+
outputPrice: 15.0,
28+
cacheWritesPrice: 3.75,
29+
cacheReadsPrice: 0.3,
30+
description:
31+
"Claude 3.7 Sonnet is an advanced large language model with improved reasoning, coding, and problem-solving capabilities. It introduces a hybrid reasoning approach, allowing users to choose between rapid responses and extended, step-by-step processing for complex tasks. The model demonstrates notable improvements in coding, particularly in front-end development and full-stack updates, and excels in agentic workflows, where it can autonomously navigate multi-step processes. Claude 3.7 Sonnet maintains performance parity with its predecessor in standard mode while offering an extended reasoning mode for enhanced accuracy in math, coding, and instruction-following tasks. Read more at the [blog post here](https://www.anthropic.com/news/claude-3-7-sonnet)",
2932
},
3033
openAiStreamingEnabled: true,
3134
includeMaxTokens: true, // Add this to match the implementation
@@ -115,16 +118,38 @@ describe("RequestyHandler", () => {
115118
outputTokens: 10,
116119
cacheWriteTokens: 5,
117120
cacheReadTokens: 15,
118-
totalCost: 0.000119, // (10 * 1 / 1,000,000) + (5 * 1.5 / 1,000,000) + (15 * 0.1 / 1,000,000) + (10 * 10 / 1,000,000)
121+
totalCost: 0.00020325000000000003, // (10 * 3 / 1,000,000) + (5 * 3.75 / 1,000,000) + (15 * 0.3 / 1,000,000) + (10 * 15 / 1,000,000) (the ...0 is a fp skew)
119122
},
120123
])
121124

122125
expect(mockCreate).toHaveBeenCalledWith({
123126
model: defaultOptions.requestyModelId,
124127
temperature: 0,
125128
messages: [
126-
{ role: "system", content: systemPrompt },
127-
{ role: "user", content: "Hello" },
129+
{
130+
role: "system",
131+
content: [
132+
{
133+
cache_control: {
134+
type: "ephemeral",
135+
},
136+
text: systemPrompt,
137+
type: "text",
138+
},
139+
],
140+
},
141+
{
142+
role: "user",
143+
content: [
144+
{
145+
cache_control: {
146+
type: "ephemeral",
147+
},
148+
text: "Hello",
149+
type: "text",
150+
},
151+
],
152+
},
128153
],
129154
stream: true,
130155
stream_options: { include_usage: true },
@@ -191,15 +216,26 @@ describe("RequestyHandler", () => {
191216
outputTokens: 5,
192217
cacheWriteTokens: 0,
193218
cacheReadTokens: 0,
194-
totalCost: 0.00006, // (10 * 1 / 1,000,000) + (5 * 10 / 1,000,000)
219+
totalCost: 0.000105, // (10 * 3 / 1,000,000) + (5 * 15 / 1,000,000)
195220
},
196221
])
197222

198223
expect(mockCreate).toHaveBeenCalledWith({
199224
model: defaultOptions.requestyModelId,
200225
messages: [
201226
{ role: "user", content: systemPrompt },
202-
{ role: "user", content: "Hello" },
227+
{
228+
role: "user",
229+
content: [
230+
{
231+
cache_control: {
232+
type: "ephemeral",
233+
},
234+
text: "Hello",
235+
type: "text",
236+
},
237+
],
238+
},
203239
],
204240
})
205241
})
@@ -224,7 +260,7 @@ describe("RequestyHandler", () => {
224260
const result = handler.getModel()
225261
expect(result).toEqual({
226262
id: defaultOptions.requestyModelId,
227-
info: requestyModelInfoSaneDefaults,
263+
info: defaultOptions.requestyModelInfo,
228264
})
229265
})
230266
})

src/api/providers/requesty.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import axios from "axios"
22

3-
import { ModelInfo, requestyModelInfoSaneDefaults, requestyDefaultModelId } from "../../shared/api"
3+
import { ModelInfo, requestyDefaultModelInfo, requestyDefaultModelId } from "../../shared/api"
44
import { calculateApiCostOpenAI, parseApiPrice } from "../../utils/cost"
55
import { ApiStreamUsageChunk } from "../transform/stream"
66
import { OpenAiHandler, OpenAiHandlerOptions } from "./openai"
@@ -26,15 +26,15 @@ export class RequestyHandler extends OpenAiHandler {
2626
openAiApiKey: options.requestyApiKey,
2727
openAiModelId: options.requestyModelId ?? requestyDefaultModelId,
2828
openAiBaseUrl: "https://router.requesty.ai/v1",
29-
openAiCustomModelInfo: options.requestyModelInfo ?? requestyModelInfoSaneDefaults,
29+
openAiCustomModelInfo: options.requestyModelInfo ?? requestyDefaultModelInfo,
3030
})
3131
}
3232

3333
override getModel(): { id: string; info: ModelInfo } {
3434
const modelId = this.options.requestyModelId ?? requestyDefaultModelId
3535
return {
3636
id: modelId,
37-
info: this.options.requestyModelInfo ?? requestyModelInfoSaneDefaults,
37+
info: this.options.requestyModelInfo ?? requestyDefaultModelInfo,
3838
}
3939
}
4040

src/core/webview/ClineProvider.ts

Lines changed: 81 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,18 @@ import * as vscode from "vscode"
1010

1111
import { changeLanguage, t } from "../../i18n"
1212
import { setPanel } from "../../activate/registerCommands"
13-
import { ApiConfiguration, ApiProvider, ModelInfo, API_CONFIG_KEYS } from "../../shared/api"
13+
import {
14+
ApiConfiguration,
15+
ApiProvider,
16+
ModelInfo,
17+
API_CONFIG_KEYS,
18+
requestyDefaultModelId,
19+
requestyDefaultModelInfo,
20+
openRouterDefaultModelId,
21+
openRouterDefaultModelInfo,
22+
glamaDefaultModelId,
23+
glamaDefaultModelInfo,
24+
} from "../../shared/api"
1425
import { findLast } from "../../shared/array"
1526
import { supportPrompt } from "../../shared/support-prompt"
1627
import { GlobalFileNames } from "../../shared/globalFileNames"
@@ -593,6 +604,8 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
593604
"codicon.css",
594605
])
595606

607+
const imagesUri = getUri(webview, this.contextProxy.extensionUri, ["assets", "images"])
608+
596609
const file = "src/index.tsx"
597610
const scriptUri = `http://${localServerUrl}/${file}`
598611

@@ -611,7 +624,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
611624
`font-src ${webview.cspSource}`,
612625
`style-src ${webview.cspSource} 'unsafe-inline' https://* http://${localServerUrl} http://0.0.0.0:${localPort}`,
613626
`img-src ${webview.cspSource} data:`,
614-
`script-src 'unsafe-eval' https://* https://*.posthog.com http://${localServerUrl} http://0.0.0.0:${localPort} 'nonce-${nonce}'`,
627+
`script-src 'unsafe-eval' ${webview.cspSource} https://* https://*.posthog.com http://${localServerUrl} http://0.0.0.0:${localPort} 'nonce-${nonce}'`,
615628
`connect-src https://* https://*.posthog.com ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`,
616629
]
617630

@@ -624,6 +637,9 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
624637
<meta http-equiv="Content-Security-Policy" content="${csp.join("; ")}">
625638
<link rel="stylesheet" type="text/css" href="${stylesUri}">
626639
<link href="${codiconsUri}" rel="stylesheet" />
640+
<script nonce="${nonce}">
641+
window.IMAGES_BASE_URI = "${imagesUri}"
642+
</script>
627643
<title>Roo Code</title>
628644
</head>
629645
<body>
@@ -672,6 +688,8 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
672688
"codicon.css",
673689
])
674690

691+
const imagesUri = getUri(webview, this.contextProxy.extensionUri, ["assets", "images"])
692+
675693
// const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "main.js"))
676694

677695
// const styleResetUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, "assets", "reset.css"))
@@ -704,6 +722,9 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
704722
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webview.cspSource}; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} data:; script-src 'nonce-${nonce}' https://us-assets.i.posthog.com; connect-src https://openrouter.ai https://us.i.posthog.com https://us-assets.i.posthog.com;">
705723
<link rel="stylesheet" type="text/css" href="${stylesUri}">
706724
<link href="${codiconsUri}" rel="stylesheet" />
725+
<script nonce="${nonce}">
726+
window.IMAGES_BASE_URI = "${imagesUri}"
727+
</script>
707728
<title>Roo Code</title>
708729
</head>
709730
<body>
@@ -1811,23 +1832,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
18111832
break
18121833
case "upsertApiConfiguration":
18131834
if (message.text && message.apiConfiguration) {
1814-
try {
1815-
await this.configManager.saveConfig(message.text, message.apiConfiguration)
1816-
const listApiConfig = await this.configManager.listConfig()
1817-
1818-
await Promise.all([
1819-
this.updateGlobalState("listApiConfigMeta", listApiConfig),
1820-
this.updateApiConfiguration(message.apiConfiguration),
1821-
this.updateGlobalState("currentApiConfigName", message.text),
1822-
])
1823-
1824-
await this.postStateToWebview()
1825-
} catch (error) {
1826-
this.outputChannel.appendLine(
1827-
`Error create new api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
1828-
)
1829-
vscode.window.showErrorMessage(t("common:errors.create_api_config"))
1830-
}
1835+
await this.upsertApiConfiguration(message.text, message.apiConfiguration)
18311836
}
18321837
break
18331838
case "renameApiConfiguration":
@@ -2251,9 +2256,10 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
22512256
// OpenRouter
22522257

22532258
async handleOpenRouterCallback(code: string) {
2259+
let { apiConfiguration, currentApiConfigName } = await this.getState()
2260+
22542261
let apiKey: string
22552262
try {
2256-
const { apiConfiguration } = await this.getState()
22572263
const baseUrl = apiConfiguration.openRouterBaseUrl || "https://openrouter.ai/api/v1"
22582264
// Extract the base domain for the auth endpoint
22592265
const baseUrlDomain = baseUrl.match(/^(https?:\/\/[^\/]+)/)?.[1] || "https://openrouter.ai"
@@ -2270,17 +2276,15 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
22702276
throw error
22712277
}
22722278

2273-
const openrouter: ApiProvider = "openrouter"
2274-
await this.contextProxy.setValues({
2275-
apiProvider: openrouter,
2279+
const newConfiguration: ApiConfiguration = {
2280+
...apiConfiguration,
2281+
apiProvider: "openrouter",
22762282
openRouterApiKey: apiKey,
2277-
})
2278-
2279-
await this.postStateToWebview()
2280-
if (this.getCurrentCline()) {
2281-
this.getCurrentCline()!.api = buildApiHandler({ apiProvider: openrouter, openRouterApiKey: apiKey })
2283+
openRouterModelId: apiConfiguration?.openRouterModelId || openRouterDefaultModelId,
2284+
openRouterModelInfo: apiConfiguration?.openRouterModelInfo || openRouterDefaultModelInfo,
22822285
}
2283-
// await this.postMessageToWebview({ type: "action", action: "settingsButtonClicked" }) // bad ux if user is on welcome
2286+
2287+
await this.upsertApiConfiguration(currentApiConfigName, newConfiguration)
22842288
}
22852289

22862290
// Glama
@@ -2301,19 +2305,55 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
23012305
throw error
23022306
}
23032307

2304-
const glama: ApiProvider = "glama"
2305-
await this.contextProxy.setValues({
2306-
apiProvider: glama,
2308+
const { apiConfiguration, currentApiConfigName } = await this.getState()
2309+
2310+
const newConfiguration: ApiConfiguration = {
2311+
...apiConfiguration,
2312+
apiProvider: "glama",
23072313
glamaApiKey: apiKey,
2308-
})
2309-
await this.postStateToWebview()
2310-
if (this.getCurrentCline()) {
2311-
this.getCurrentCline()!.api = buildApiHandler({
2312-
apiProvider: glama,
2313-
glamaApiKey: apiKey,
2314-
})
2314+
glamaModelId: apiConfiguration?.glamaModelId || glamaDefaultModelId,
2315+
glamaModelInfo: apiConfiguration?.glamaModelInfo || glamaDefaultModelInfo,
2316+
}
2317+
2318+
await this.upsertApiConfiguration(currentApiConfigName, newConfiguration)
2319+
}
2320+
2321+
// Requesty
2322+
2323+
async handleRequestyCallback(code: string) {
2324+
let { apiConfiguration, currentApiConfigName } = await this.getState()
2325+
2326+
const newConfiguration: ApiConfiguration = {
2327+
...apiConfiguration,
2328+
apiProvider: "requesty",
2329+
requestyApiKey: code,
2330+
requestyModelId: apiConfiguration?.requestyModelId || requestyDefaultModelId,
2331+
requestyModelInfo: apiConfiguration?.requestyModelInfo || requestyDefaultModelInfo,
2332+
}
2333+
2334+
await this.upsertApiConfiguration(currentApiConfigName, newConfiguration)
2335+
}
2336+
2337+
// Save configuration
2338+
2339+
async upsertApiConfiguration(configName: string, apiConfiguration: ApiConfiguration) {
2340+
try {
2341+
await this.configManager.saveConfig(configName, apiConfiguration)
2342+
const listApiConfig = await this.configManager.listConfig()
2343+
2344+
await Promise.all([
2345+
this.updateGlobalState("listApiConfigMeta", listApiConfig),
2346+
this.updateApiConfiguration(apiConfiguration),
2347+
this.updateGlobalState("currentApiConfigName", configName),
2348+
])
2349+
2350+
await this.postStateToWebview()
2351+
} catch (error) {
2352+
this.outputChannel.appendLine(
2353+
`Error create new api configuration: ${JSON.stringify(error, Object.getOwnPropertyNames(error), 2)}`,
2354+
)
2355+
vscode.window.showErrorMessage(t("common:errors.create_api_config"))
23152356
}
2316-
// await this.postMessageToWebview({ type: "action", action: "settingsButtonClicked" }) // bad ux if user is on welcome
23172357
}
23182358

23192359
// Task history
@@ -2627,14 +2667,7 @@ export class ClineProvider extends EventEmitter<ClineProviderEvents> implements
26272667
if (stateValues.apiProvider) {
26282668
apiProvider = stateValues.apiProvider
26292669
} else {
2630-
// Either new user or legacy user that doesn't have the apiProvider stored in state
2631-
// (If they're using OpenRouter or Bedrock, then apiProvider state will exist)
2632-
if (secretValues.apiKey) {
2633-
apiProvider = "anthropic"
2634-
} else {
2635-
// New users should default to openrouter
2636-
apiProvider = "openrouter"
2637-
}
2670+
apiProvider = "anthropic"
26382671
}
26392672

26402673
// Build the apiConfiguration object combining state values and secrets

0 commit comments

Comments
 (0)