Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/api/providers/fetchers/modelCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const getModels = async (options: GetModelsOptions): Promise<ModelRecord>
break
case "requesty":
// Requesty models endpoint requires an API key for per-user custom policies
models = await getRequestyModels(options.apiKey)
models = await getRequestyModels(options.apiKey, options.baseUrl)
break
case "glama":
models = await getGlamaModels()
Expand Down
6 changes: 4 additions & 2 deletions src/api/providers/fetchers/requesty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { ModelInfo } from "@roo-code/types"

import { parseApiPrice } from "../../../shared/cost"

export async function getRequestyModels(apiKey?: string): Promise<Record<string, ModelInfo>> {
export async function getRequestyModels(apiKey?: string, baseUrl?: string): Promise<Record<string, ModelInfo>> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new baseUrl parameter works, but there's no test coverage for this functionality. Could we add unit tests to verify the URL transformations work correctly with different subdomain patterns (router/api/app)?

const models: Record<string, ModelInfo> = {}

try {
Expand All @@ -14,7 +14,9 @@ export async function getRequestyModels(apiKey?: string): Promise<Record<string,
headers["Authorization"] = `Bearer ${apiKey}`
}

const url = "https://router.requesty.ai/v1/models"
// Use the base URL if provided, otherwise default to the router endpoint
const apiUrl = baseUrl ? baseUrl.replace(/\/$/, "") : "https://router.requesty.ai"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding JSDoc comments to document the expected format of the baseUrl parameter and what transformations are applied. This would help future maintainers understand the URL handling logic.

const url = `${apiUrl}/v1/models`
const response = await axios.get(url, { headers })
const rawModels = response.data.data

Expand Down
6 changes: 5 additions & 1 deletion src/api/providers/requesty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ export class RequestyHandler extends BaseProvider implements SingleCompletionHan
}

public async fetchModel() {
this.models = await getModels({ provider: "requesty" })
this.models = await getModels({
provider: "requesty",
apiKey: this.options.requestyApiKey,
baseUrl: this.options.requestyBaseUrl,
})
return this.getModel()
}

Expand Down
9 changes: 8 additions & 1 deletion src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -548,7 +548,14 @@ export const webviewMessageHandler = async (

const modelFetchPromises: Array<{ key: RouterName; options: GetModelsOptions }> = [
{ key: "openrouter", options: { provider: "openrouter" } },
{ key: "requesty", options: { provider: "requesty", apiKey: apiConfiguration.requestyApiKey } },
{
key: "requesty",
options: {
provider: "requesty",
apiKey: apiConfiguration.requestyApiKey,
baseUrl: apiConfiguration.requestyBaseUrl,
},
},
{ key: "glama", options: { provider: "glama" } },
{ key: "unbound", options: { provider: "unbound", apiKey: apiConfiguration.unboundApiKey } },
]
Expand Down
2 changes: 1 addition & 1 deletion src/shared/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ export const getModelMaxOutputTokens = ({
export type GetModelsOptions =
| { provider: "openrouter" }
| { provider: "glama" }
| { provider: "requesty"; apiKey?: string }
| { provider: "requesty"; apiKey?: string; baseUrl?: string }
| { provider: "unbound"; apiKey?: string }
| { provider: "litellm"; apiKey: string; baseUrl: string }
| { provider: "ollama"; baseUrl?: string }
Expand Down
16 changes: 14 additions & 2 deletions webview-ui/src/components/settings/providers/Requesty.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,10 @@ export const Requesty = ({
<div className="flex justify-between items-center mb-1">
<label className="block font-medium">{t("settings:providers.requestyApiKey")}</label>
{apiConfiguration?.requestyApiKey && (
<RequestyBalanceDisplay apiKey={apiConfiguration.requestyApiKey} />
<RequestyBalanceDisplay
apiKey={apiConfiguration.requestyApiKey}
baseUrl={apiConfiguration?.requestyBaseUrl}
/>
)}
</div>
</VSCodeTextField>
Expand All @@ -74,7 +77,16 @@ export const Requesty = ({
</div>
{!apiConfiguration?.requestyApiKey && (
<VSCodeButtonLink
href="https://app.requesty.ai/api-keys"
href={(() => {
const baseUrl = apiConfiguration?.requestyBaseUrl || "https://app.requesty.ai"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no validation that the custom base URL is a valid URL format. Invalid URLs could cause runtime errors. Consider adding basic URL validation before using the value.

// Remove trailing slash if present and ensure we're using the app subdomain
const cleanBaseUrl = baseUrl.replace(/\/$/, "")
// If the base URL contains 'router' or 'api', replace with 'app' for web interface
const appUrl = cleanBaseUrl
.replace(/router\.requesty/, "app.requesty")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This URL transformation logic is duplicated in RequestyBalanceDisplay.tsx and urls.ts. Would it make sense to extract this into a shared utility function like transformRequestyUrlToApp(baseUrl: string) to maintain consistency?

.replace(/api\.requesty/, "app.requesty")
return `${appUrl}/api-keys`
})()}
style={{ width: "100%" }}
appearance="primary">
{t("settings:providers.getRequestyApiKey")}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"

import { useRequestyKeyInfo } from "@/components/ui/hooks/useRequestyKeyInfo"

export const RequestyBalanceDisplay = ({ apiKey }: { apiKey: string }) => {
const { data: keyInfo } = useRequestyKeyInfo(apiKey)
export const RequestyBalanceDisplay = ({ apiKey, baseUrl }: { apiKey: string; baseUrl?: string }) => {
const { data: keyInfo } = useRequestyKeyInfo(apiKey, baseUrl)

if (!keyInfo) {
return null
Expand All @@ -13,8 +13,18 @@ export const RequestyBalanceDisplay = ({ apiKey }: { apiKey: string }) => {
const balance = parseFloat(keyInfo.org_balance)
const formattedBalance = balance.toFixed(2)

// Use the base URL if provided, otherwise default to the standard app URL
const settingsUrl = (() => {
const appUrl = baseUrl || "https://app.requesty.ai"
// Remove trailing slash if present and ensure we're using the app subdomain
const cleanUrl = appUrl.replace(/\/$/, "")
// If the base URL contains 'router' or 'api', replace with 'app' for web interface
const webUrl = cleanUrl.replace(/router\.requesty/, "app.requesty").replace(/api\.requesty/, "app.requesty")
return `${webUrl}/settings`
})()

return (
<VSCodeLink href="https://app.requesty.ai/settings" className="text-vscode-foreground hover:underline">
<VSCodeLink href={settingsUrl} className="text-vscode-foreground hover:underline">
${formattedBalance}
</VSCodeLink>
)
Expand Down
15 changes: 10 additions & 5 deletions webview-ui/src/components/ui/hooks/useRequestyKeyInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,16 @@ const requestyKeyInfoSchema = z.object({

export type RequestyKeyInfo = z.infer<typeof requestyKeyInfoSchema>

async function getRequestyKeyInfo(apiKey?: string) {
async function getRequestyKeyInfo(apiKey?: string, baseUrl?: string) {
if (!apiKey) return null

try {
const response = await axios.get("https://api.requesty.ai/x/apikey", {
// Use the base URL if provided, otherwise default to the standard API endpoint
const apiUrl = baseUrl ? baseUrl.replace(/\/$/, "") : "https://api.requesty.ai"
// Convert router.requesty.ai to api.requesty.ai for API calls
const cleanApiUrl = apiUrl.replace(/router\.requesty/, "api.requesty").replace(/app\.requesty/, "api.requesty")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if someone uses a completely custom domain that doesn't follow the .requesty. pattern (e.g., https://my-requesty-instance.com)? The current regex replacements might not handle this case correctly.


const response = await axios.get(`${cleanApiUrl}/x/apikey`, {
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
Expand All @@ -39,10 +44,10 @@ async function getRequestyKeyInfo(apiKey?: string) {
}

type UseRequestyKeyInfoOptions = Omit<UseQueryOptions<RequestyKeyInfo | null>, "queryKey" | "queryFn">
export const useRequestyKeyInfo = (apiKey?: string, options?: UseRequestyKeyInfoOptions) => {
export const useRequestyKeyInfo = (apiKey?: string, baseUrl?: string, options?: UseRequestyKeyInfoOptions) => {
return useQuery<RequestyKeyInfo | null>({
queryKey: ["requesty-key-info", apiKey],
queryFn: () => getRequestyKeyInfo(apiKey),
queryKey: ["requesty-key-info", apiKey, baseUrl],
queryFn: () => getRequestyKeyInfo(apiKey, baseUrl),
staleTime: 30 * 1000, // 30 seconds
enabled: !!apiKey,
...options,
Expand Down
2 changes: 1 addition & 1 deletion webview-ui/src/components/welcome/WelcomeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ const WelcomeView = () => {
name: "Requesty",
description: t("welcome:routers.requesty.description"),
incentive: t("welcome:routers.requesty.incentive"),
authUrl: getRequestyAuthUrl(uriScheme),
authUrl: getRequestyAuthUrl(uriScheme, apiConfiguration?.requestyBaseUrl),
},
{
slug: "openrouter",
Expand Down
9 changes: 7 additions & 2 deletions webview-ui/src/oauth/urls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ export function getOpenRouterAuthUrl(uriScheme?: string) {
return `https://openrouter.ai/auth?callback_url=${getCallbackUrl("openrouter", uriScheme)}`
}

export function getRequestyAuthUrl(uriScheme?: string) {
return `https://app.requesty.ai/oauth/authorize?callback_url=${getCallbackUrl("requesty", uriScheme)}`
export function getRequestyAuthUrl(uriScheme?: string, baseUrl?: string) {
const requestyBaseUrl = baseUrl || "https://app.requesty.ai"
// Remove trailing slash if present and ensure we're using the app subdomain for OAuth
const cleanBaseUrl = requestyBaseUrl.replace(/\/$/, "")
// If the base URL contains 'router' or 'api', replace with 'app' for OAuth
const oauthUrl = cleanBaseUrl.replace(/router\.requesty/, "app.requesty").replace(/api\.requesty/, "app.requesty")
return `${oauthUrl}/oauth/authorize?callback_url=${getCallbackUrl("requesty", uriScheme)}`
}
Loading