Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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.baseUrl, options.apiKey)
break
case "glama":
models = await getGlamaModels()
Expand Down
9 changes: 6 additions & 3 deletions src/api/providers/fetchers/requesty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import axios from "axios"
import type { ModelInfo } from "@roo-code/types"

import { parseApiPrice } from "../../../shared/cost"
import { toRequestyServiceUrl } from "../../../shared/utils/requesty"

export async function getRequestyModels(apiKey?: string): Promise<Record<string, ModelInfo>> {
export async function getRequestyModels(baseUrl?: string, apiKey?: string): Promise<Record<string, ModelInfo>> {
const models: Record<string, ModelInfo> = {}

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

const url = "https://router.requesty.ai/v1/models"
const response = await axios.get(url, { headers })
const resolvedBaseUrl = toRequestyServiceUrl(baseUrl)
const modelsUrl = new URL("models", resolvedBaseUrl)

const response = await axios.get(modelsUrl.toString(), { headers })
const rawModels = response.data.data

for (const rawModel of rawModels) {
Expand Down
7 changes: 5 additions & 2 deletions src/api/providers/requesty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { DEFAULT_HEADERS } from "./constants"
import { getModels } from "./fetchers/modelCache"
import { BaseProvider } from "./base-provider"
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
import { toRequestyServiceUrl } from "../../shared/utils/requesty"

// Requesty usage includes an extra field for Anthropic use cases.
// Safely cast the prompt token details section to the appropriate structure.
Expand All @@ -40,21 +41,23 @@ export class RequestyHandler extends BaseProvider implements SingleCompletionHan
protected options: ApiHandlerOptions
protected models: ModelRecord = {}
private client: OpenAI
private baseURL: string

constructor(options: ApiHandlerOptions) {
super()

this.options = options
this.baseURL = toRequestyServiceUrl(options.requestyBaseUrl)

this.client = new OpenAI({
baseURL: options.requestyBaseUrl || "https://router.requesty.ai/v1",
baseURL: this.baseURL,
apiKey: this.options.requestyApiKey ?? "not-provided",
defaultHeaders: DEFAULT_HEADERS,
})
}

public async fetchModel() {
this.models = await getModels({ provider: "requesty" })
this.models = await getModels({ provider: "requesty", baseUrl: this.baseURL })
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 @@ -135,7 +135,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
23 changes: 23 additions & 0 deletions src/shared/utils/requesty.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
const REQUESTY_BASE_URL = "https://router.requesty.ai/v1"

type URLType = "router" | "app" | "api"

const replaceCname = (baseUrl: string, type: URLType): string => {
if (type === "router") {
return baseUrl
} else {
return baseUrl.replace("router", type).replace("v1", "")
}
}

export const toRequestyServiceUrl = (baseUrl?: string, service: URLType = "router"): string => {
let url = replaceCname(baseUrl ?? REQUESTY_BASE_URL, service)

try {
return new URL(url).toString()
} catch (error) {
// If the provided baseUrl is invalid, fall back to the default
console.warn(`Invalid base URL "${baseUrl}", falling back to default`)
return new URL(replaceCname(REQUESTY_BASE_URL, service)).toString()
}
}
1 change: 1 addition & 0 deletions webview-ui/src/components/settings/ApiOptions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ const ApiOptions = ({

{selectedProvider === "requesty" && (
<Requesty
uriScheme={uriScheme}
apiConfiguration={apiConfiguration}
setApiConfigurationField={setApiConfigurationField}
routerModels={routerModels}
Expand Down
36 changes: 29 additions & 7 deletions webview-ui/src/components/settings/providers/Requesty.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,13 @@ import type { RouterModels } from "@roo/api"

import { vscode } from "@src/utils/vscode"
import { useAppTranslation } from "@src/i18n/TranslationContext"
import { VSCodeButtonLink } from "@src/components/common/VSCodeButtonLink"
import { Button } from "@src/components/ui"

import { inputEventTransform } from "../transforms"
import { ModelPicker } from "../ModelPicker"
import { RequestyBalanceDisplay } from "./RequestyBalanceDisplay"
import { getCallbackUrl } from "@/oauth/urls"
import { toRequestyServiceUrl } from "@roo/utils/requesty"

type RequestyProps = {
apiConfiguration: ProviderSettings
Expand All @@ -22,6 +23,7 @@ type RequestyProps = {
refetchRouterModels: () => void
organizationAllowList: OrganizationAllowList
modelValidationError?: string
uriScheme?: string
}

export const Requesty = ({
Expand All @@ -31,6 +33,7 @@ export const Requesty = ({
refetchRouterModels,
organizationAllowList,
modelValidationError,
uriScheme,
}: RequestyProps) => {
const { t } = useAppTranslation()

Expand All @@ -54,6 +57,15 @@ export const Requesty = ({
[setApiConfigurationField],
)

const getApiKeyUrl = () => {
const callbackUrl = getCallbackUrl("requesty", uriScheme)
const baseUrl = toRequestyServiceUrl(apiConfiguration.requestyBaseUrl, "app")

const authUrl = new URL(`oauth/authorize?callback_url=${callbackUrl}`, baseUrl)

return authUrl.toString()
}

return (
<>
<VSCodeTextField
Expand All @@ -65,20 +77,30 @@ 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
baseUrl={apiConfiguration.requestyBaseUrl}
apiKey={apiConfiguration.requestyApiKey}
/>
)}
</div>
</VSCodeTextField>
<div className="text-sm text-vscode-descriptionForeground -mt-2">
{t("settings:providers.apiKeyStorageNotice")}
</div>
{!apiConfiguration?.requestyApiKey && (
<VSCodeButtonLink
href="https://app.requesty.ai/api-keys"
style={{ width: "100%" }}
appearance="primary">
<a
href={getApiKeyUrl()}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center whitespace-nowrap text-sm font-medium focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground shadow hover:bg-primary/90 h-9 rounded-md px-3 w-full"
style={{
width: "100%",
textDecoration: "none",
color: "var(--vscode-button-foreground)",
backgroundColor: "var(--vscode-button-background)",
}}>
{t("settings:providers.getRequestyApiKey")}
</VSCodeButtonLink>
</a>
)}

<VSCodeCheckbox
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"

import { useRequestyKeyInfo } from "@/components/ui/hooks/useRequestyKeyInfo"
import { toRequestyServiceUrl } from "@roo/utils/requesty"

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

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

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

const resolvedBaseUrl = toRequestyServiceUrl(baseUrl, "app")
const settingsUrl = new URL("settings", resolvedBaseUrl)

return (
<VSCodeLink href="https://app.requesty.ai/settings" className="text-vscode-foreground hover:underline">
<VSCodeLink href={settingsUrl.toString()} className="text-vscode-foreground hover:underline">
${formattedBalance}
</VSCodeLink>
)
Expand Down
12 changes: 8 additions & 4 deletions webview-ui/src/components/ui/hooks/useRequestyKeyInfo.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import axios from "axios"
import { z } from "zod"
import { useQuery, UseQueryOptions } from "@tanstack/react-query"
import { toRequestyServiceUrl } from "@roo/utils/requesty"

const requestyKeyInfoSchema = z.object({
name: z.string(),
Expand All @@ -14,11 +15,14 @@ const requestyKeyInfoSchema = z.object({

export type RequestyKeyInfo = z.infer<typeof requestyKeyInfoSchema>

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

const url = toRequestyServiceUrl(baseUrl, "api")
const apiKeyUrl = new URL("x/apikey", url)

try {
const response = await axios.get("https://api.requesty.ai/x/apikey", {
const response = await axios.get(apiKeyUrl.toString(), {
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
Expand All @@ -39,10 +43,10 @@ async function getRequestyKeyInfo(apiKey?: string) {
}

type UseRequestyKeyInfoOptions = Omit<UseQueryOptions<RequestyKeyInfo | null>, "queryKey" | "queryFn">
export const useRequestyKeyInfo = (apiKey?: string, options?: UseRequestyKeyInfoOptions) => {
export const useRequestyKeyInfo = (baseUrl?: string, apiKey?: string, options?: UseRequestyKeyInfoOptions) => {
return useQuery<RequestyKeyInfo | null>({
queryKey: ["requesty-key-info", apiKey],
queryFn: () => getRequestyKeyInfo(apiKey),
queryFn: () => getRequestyKeyInfo(baseUrl, apiKey),
staleTime: 30 * 1000, // 30 seconds
enabled: !!apiKey,
...options,
Expand Down