Browse Source

Add oauth button to openrouter

Saoud Rizwan 1 year ago
parent
commit
ce71ed7cba

+ 22 - 10
src/extension.ts

@@ -109,16 +109,28 @@ export function activate(context: vscode.ExtensionContext) {
 		vscode.workspace.registerTextDocumentContentProvider("claude-dev-diff", diffContentProvider)
 	)
 
-	// // URI Handler
-	// const handleUri = async (uri: vscode.Uri) => {
-	// 	const query = new URLSearchParams(uri.query.replace(/\+/g, "%2B"))
-	// 	const token = query.get("token")
-	// 	const email = query.get("email")
-	// 	if (token) {
-	// 		await sidebarProvider.saveKoduApiKey(token, email || undefined)
-	// 	}
-	// }
-	// context.subscriptions.push(vscode.window.registerUriHandler({ handleUri }))
+	// URI Handler
+	const handleUri = async (uri: vscode.Uri) => {
+		console.log("handleUri", uri)
+		const path = uri.path
+		const query = new URLSearchParams(uri.query.replace(/\+/g, "%2B"))
+		const visibleProvider = ClaudeDevProvider.getVisibleInstance()
+		if (!visibleProvider) {
+			return
+		}
+		switch (path) {
+			case "/openrouter": {
+				const code = query.get("code")
+				if (code) {
+					await visibleProvider.handleOpenRouterCallback(code)
+				}
+				break
+			}
+			default:
+				break
+		}
+	}
+	context.subscriptions.push(vscode.window.registerUriHandler({ handleUri }))
 }
 
 // This method is called when your extension is deactivated

+ 37 - 2
src/providers/ClaudeDevProvider.ts

@@ -4,10 +4,11 @@ import { ClaudeDev } from "../ClaudeDev"
 import { ApiModelId, ApiProvider } from "../shared/api"
 import { ExtensionMessage } from "../shared/ExtensionMessage"
 import { WebviewMessage } from "../shared/WebviewMessage"
-import { downloadTask, getNonce, getUri, selectImages } from "../utils"
+import { downloadTask, findLast, getNonce, getUri, selectImages } from "../utils"
 import * as path from "path"
 import fs from "fs/promises"
 import { HistoryItem } from "../shared/HistoryItem"
+import axios from "axios"
 
 /*
 https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts
@@ -30,6 +31,7 @@ type GlobalStateKey =
 export class ClaudeDevProvider implements vscode.WebviewViewProvider {
 	public static readonly sideBarId = "claude-dev.SidebarProvider" // used in package.json as the view's id. This value cannot be changed due to how vscode caches views based on their id, and updating the id would break existing instances of the extension.
 	public static readonly tabPanelId = "claude-dev.TabPanelProvider"
+	private static activeInstances: Set<ClaudeDevProvider> = new Set()
 	private disposables: vscode.Disposable[] = []
 	private view?: vscode.WebviewView | vscode.WebviewPanel
 	private claudeDev?: ClaudeDev
@@ -37,6 +39,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
 
 	constructor(readonly context: vscode.ExtensionContext, private readonly outputChannel: vscode.OutputChannel) {
 		this.outputChannel.appendLine("ClaudeDevProvider instantiated")
+		ClaudeDevProvider.activeInstances.add(this)
 		this.revertKodu()
 	}
 
@@ -81,6 +84,11 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
 			}
 		}
 		this.outputChannel.appendLine("Disposed all disposables")
+		ClaudeDevProvider.activeInstances.delete(this)
+	}
+
+	public static getVisibleInstance(): ClaudeDevProvider | undefined {
+		return findLast(Array.from(this.activeInstances), (instance) => instance.view?.visible === true)
 	}
 
 	resolveWebviewView(
@@ -373,6 +381,33 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
 		)
 	}
 
+	// OpenRouter
+
+	async handleOpenRouterCallback(code: string) {
+		console.log("handleOpenRouterCallback", code)
+		let apiKey: string
+		try {
+			const response = await axios.post("https://openrouter.ai/api/v1/auth/keys", { code })
+			console.log("OpenRouter API response:", response.data)
+
+			if (response.data && response.data.key) {
+				apiKey = response.data.key
+			} else {
+				throw new Error("Invalid response from OpenRouter API")
+			}
+		} catch (error) {
+			console.error("Error exchanging code for API key:", error)
+			throw error
+		}
+
+		const openrouter: ApiProvider = "openrouter"
+		await this.updateGlobalState("apiProvider", openrouter)
+		await this.storeSecret("openRouterApiKey", apiKey)
+		await this.postStateToWebview()
+		this.claudeDev?.updateApi({ apiProvider: openrouter, openRouterApiKey: apiKey })
+		await this.postMessageToWebview({ type: "action", action: "settingsButtonTapped" })
+	}
+
 	// Task history
 
 	async getTaskWithId(id: string): Promise<{
@@ -606,7 +641,7 @@ export class ClaudeDevProvider implements vscode.WebviewViewProvider {
 			if (apiKey) {
 				apiProvider = "anthropic"
 			} else {
-				// New users should default to anthropic
+				// New users should default to anthropic for now, but will change to openrouter after fast edit mode
 				apiProvider = "anthropic"
 			}
 		}

+ 44 - 9
webview-ui/src/components/Announcement.tsx

@@ -1,5 +1,8 @@
 import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
 import { ApiConfiguration } from "../../../src/shared/api"
+// import VSCodeButtonLink from "./VSCodeButtonLink"
+// import { getOpenRouterAuthUrl } from "./ApiOptions"
+// import { vscode } from "../utils/vscode"
 
 interface AnnouncementProps {
 	version: string
@@ -30,22 +33,54 @@ const Announcement = ({ version, hideAnnouncement, apiConfiguration, vscodeUriSc
 				🎉{"  "}New in v{version}
 			</h3>
 			<ul style={{ margin: "0 0 8px", paddingLeft: "12px" }}>
+				{/* <li>
+					OpenRouter now supports prompt caching! They also have much higher rate limits than other providers,
+					so I recommend trying them out.
+					<br />
+					{!apiConfiguration?.openRouterApiKey && (
+						<VSCodeButtonLink
+							href={getOpenRouterAuthUrl(vscodeUriScheme)}
+							style={{
+								transform: "scale(0.85)",
+								transformOrigin: "left center",
+								margin: "4px -30px 2px 0",
+							}}>
+							Get OpenRouter API Key
+						</VSCodeButtonLink>
+					)}
+					{apiConfiguration?.openRouterApiKey && apiConfiguration?.apiProvider !== "openrouter" && (
+						<VSCodeButton
+							onClick={() => {
+								vscode.postMessage({
+									type: "apiConfiguration",
+									apiConfiguration: { ...apiConfiguration, apiProvider: "openrouter" },
+								})
+							}}
+							style={{
+								transform: "scale(0.85)",
+								transformOrigin: "left center",
+								margin: "4px -30px 2px 0",
+							}}>
+							Switch to OpenRouter
+						</VSCodeButton>
+					)}
+				</li> */}
 				<li>
-					New terminal emulator! When Claude runs commands, you can now type directly in the terminal (+
-					support for Python environments)
+					<b>Edit Claude's changes before accepting!</b> When he creates or edits a file, you can modify his
+					changes directly in the right side of the diff view (+ hover over the 'Revert Block' arrow button in
+					the center to undo "<code>{"// rest of code here"}</code>" shenanigans)
 				</li>
 				<li>
-					<b>You can now edit Claude's changes before accepting!</b> When he edits or creates a file, you can
-					modify his changes directly in the right side of the diff view (+ hover over the 'Revert Block'
-					arrow button in the center to undo "<code>{"// rest of code here"}</code>" shenanigans)
+					Adds new <code>search_files</code> tool that lets Claude perform regex searches in your project,
+					making it easy for him to refactor code, address TODOs and FIXMEs, remove dead code, and more!
 				</li>
 				<li>
-					Adds support for reading .pdf and .docx files (try "turn my business_plan.docx into a company
-					website")
+					New terminal emulator! When Claude runs commands, you can now type directly in the terminal (+
+					support for Python environments)
 				</li>
 				<li>
-					Adds new <code>search_files</code> tool that lets Claude perform regex searches in your project,
-					making it easy for him to refactor code, address TODOs and FIXMEs, remove dead code, and more!
+					Adds support for reading .pdf and .docx files (try "turn my business_plan.docx into a company
+					website")
 				</li>
 			</ul>
 			<p style={{ margin: "0" }}>

+ 31 - 18
webview-ui/src/components/ApiOptions.tsx

@@ -14,6 +14,7 @@ import {
 	vertexModels,
 } from "../../../src/shared/api"
 import { useExtensionState } from "../context/ExtensionStateContext"
+import VSCodeButtonLink from "./VSCodeButtonLink"
 
 interface ApiOptionsProps {
 	showModelOptions: boolean
@@ -21,7 +22,7 @@ interface ApiOptionsProps {
 }
 
 const ApiOptions: React.FC<ApiOptionsProps> = ({ showModelOptions, apiErrorMessage }) => {
-	const { apiConfiguration, setApiConfiguration } = useExtensionState()
+	const { apiConfiguration, setApiConfiguration, uriScheme } = useExtensionState()
 	const handleInputChange = (field: keyof ApiConfiguration) => (event: any) => {
 		setApiConfiguration({ ...apiConfiguration, [field]: event.target.value })
 	}
@@ -70,8 +71,8 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({ showModelOptions, apiErrorMessa
 				</label>
 				<VSCodeDropdown id="api-provider" value={selectedProvider} onChange={handleInputChange("apiProvider")}>
 					<VSCodeOption value="anthropic">Anthropic</VSCodeOption>
-					<VSCodeOption value="bedrock">AWS Bedrock</VSCodeOption>
 					<VSCodeOption value="openrouter">OpenRouter</VSCodeOption>
+					<VSCodeOption value="bedrock">AWS Bedrock</VSCodeOption>
 					<VSCodeOption value="vertex">GCP Vertex AI</VSCodeOption>
 				</VSCodeDropdown>
 			</div>
@@ -93,9 +94,11 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({ showModelOptions, apiErrorMessa
 							color: "var(--vscode-descriptionForeground)",
 						}}>
 						This key is stored locally and only used to make API requests from this extension.
-						<VSCodeLink href="https://console.anthropic.com/" style={{ display: "inline" }}>
-							You can get an Anthropic API key by signing up here.
-						</VSCodeLink>
+						{!apiConfiguration?.apiKey && (
+							<VSCodeLink href="https://console.anthropic.com/" style={{ display: "inline" }}>
+								You can get an Anthropic API key by signing up here.
+							</VSCodeLink>
+						)}
 					</p>
 				</div>
 			)}
@@ -110,20 +113,24 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({ showModelOptions, apiErrorMessa
 						placeholder="Enter API Key...">
 						<span style={{ fontWeight: 500 }}>OpenRouter API Key</span>
 					</VSCodeTextField>
+					{!apiConfiguration?.openRouterApiKey && (
+						<VSCodeButtonLink href={getOpenRouterAuthUrl(uriScheme)} style={{ margin: "5px 0 0 0" }}>
+							Get OpenRouter API Key
+						</VSCodeButtonLink>
+					)}
 					<p
 						style={{
 							fontSize: "12px",
 							marginTop: "5px",
 							color: "var(--vscode-descriptionForeground)",
 						}}>
-						This key is stored locally and only used to make API requests from this extension.
-						<VSCodeLink href="https://openrouter.ai/" style={{ display: "inline" }}>
-							You can get an OpenRouter API key by signing up here.
-						</VSCodeLink>{" "}
-						<span style={{ color: "var(--vscode-errorForeground)" }}>
-							(<span style={{ fontWeight: 500 }}>Note:</span> OpenRouter support is experimental and may
-							not work well with large files.)
-						</span>
+						This key is stored locally and only used to make API requests from this extension.{" "}
+						{/* {!apiConfiguration?.openRouterApiKey && (
+							<span style={{ color: "var(--vscode-charts-green)" }}>
+								(<span style={{ fontWeight: 500 }}>Note:</span> OpenRouter is recommended for high rate
+								limits, prompt caching, and wider selection of models.)
+							</span>
+						)} */}
 					</p>
 				</div>
 			)}
@@ -186,11 +193,13 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({ showModelOptions, apiErrorMessa
 							color: "var(--vscode-descriptionForeground)",
 						}}>
 						These credentials are stored locally and only used to make API requests from this extension.
-						<VSCodeLink
-							href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html"
-							style={{ display: "inline" }}>
-							You can find your AWS access key and secret key here.
-						</VSCodeLink>
+						{!(apiConfiguration?.awsAccessKey && apiConfiguration?.awsSecretKey) && (
+							<VSCodeLink
+								href="https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html"
+								style={{ display: "inline" }}>
+								You can find your AWS access key and secret key here.
+							</VSCodeLink>
+						)}
 					</p>
 				</div>
 			)}
@@ -274,6 +283,10 @@ const ApiOptions: React.FC<ApiOptionsProps> = ({ showModelOptions, apiErrorMessa
 	)
 }
 
+export function getOpenRouterAuthUrl(uriScheme?: string) {
+	return `https://openrouter.ai/auth?callback_url=${uriScheme || "vscode"}://saoudrizwan.claude-dev/openrouter`
+}
+
 export const formatPrice = (price: number) => {
 	return new Intl.NumberFormat("en-US", {
 		style: "currency",