mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: add custom base URL support for Requesty provider (#7337)
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> Co-authored-by: John Costa <john@requesty.ai> Co-authored-by: Thibault Jaigu <thibault.jaigu@gmail.com>
This commit is contained in:
parent
262033ddd0
commit
d175272c76
13 changed files with 267 additions and 26 deletions
|
|
@ -66,11 +66,11 @@ describe("RequestyHandler", () => {
|
|||
})
|
||||
|
||||
it("can use a base URL instead of the default", () => {
|
||||
const handler = new RequestyHandler({ ...mockOptions, requestyBaseUrl: "some-base-url" })
|
||||
const handler = new RequestyHandler({ ...mockOptions, requestyBaseUrl: "https://custom.requesty.ai/v1" })
|
||||
expect(handler).toBeInstanceOf(RequestyHandler)
|
||||
|
||||
expect(OpenAI).toHaveBeenCalledWith({
|
||||
baseURL: "some-base-url",
|
||||
baseURL: "https://custom.requesty.ai/v1",
|
||||
apiKey: mockOptions.requestyApiKey,
|
||||
defaultHeaders: {
|
||||
"HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline",
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ describe("getModels with new GetModelsOptions", () => {
|
|||
|
||||
const result = await getModels({ provider: "requesty", apiKey: DUMMY_REQUESTY_KEY })
|
||||
|
||||
expect(mockGetRequestyModels).toHaveBeenCalledWith(DUMMY_REQUESTY_KEY)
|
||||
expect(mockGetRequestyModels).toHaveBeenCalledWith(undefined, DUMMY_REQUESTY_KEY)
|
||||
expect(result).toEqual(mockModels)
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -565,7 +565,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 } },
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
136
src/shared/utils/__tests__/requesty.spec.ts
Normal file
136
src/shared/utils/__tests__/requesty.spec.ts
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import { describe, it, expect, vi, beforeEach } from "vitest"
|
||||
import { toRequestyServiceUrl } from "../requesty"
|
||||
|
||||
describe("toRequestyServiceUrl", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
// Mock console.warn to avoid noise in test output
|
||||
vi.spyOn(console, "warn").mockImplementation(() => {})
|
||||
})
|
||||
|
||||
describe("with default parameters", () => {
|
||||
it("should return default router URL when no baseUrl provided", () => {
|
||||
const result = toRequestyServiceUrl()
|
||||
expect(result).toBe("https://router.requesty.ai/v1")
|
||||
})
|
||||
|
||||
it("should return default router URL when baseUrl is undefined", () => {
|
||||
const result = toRequestyServiceUrl(undefined)
|
||||
expect(result).toBe("https://router.requesty.ai/v1")
|
||||
})
|
||||
|
||||
it("should return default router URL when baseUrl is empty string", () => {
|
||||
const result = toRequestyServiceUrl("")
|
||||
expect(result).toBe("https://router.requesty.ai/v1")
|
||||
})
|
||||
})
|
||||
|
||||
describe("with custom baseUrl", () => {
|
||||
it("should use custom baseUrl for router service", () => {
|
||||
const result = toRequestyServiceUrl("https://custom.requesty.ai/v1")
|
||||
expect(result).toBe("https://custom.requesty.ai/v1")
|
||||
})
|
||||
|
||||
it("should handle baseUrl with trailing slash", () => {
|
||||
const result = toRequestyServiceUrl("https://custom.requesty.ai/v1/")
|
||||
expect(result).toBe("https://custom.requesty.ai/v1/")
|
||||
})
|
||||
|
||||
it("should handle baseUrl without path", () => {
|
||||
const result = toRequestyServiceUrl("https://custom.requesty.ai")
|
||||
expect(result).toBe("https://custom.requesty.ai/")
|
||||
})
|
||||
|
||||
it("should handle localhost URLs", () => {
|
||||
const result = toRequestyServiceUrl("http://localhost:8080/v1")
|
||||
expect(result).toBe("http://localhost:8080/v1")
|
||||
})
|
||||
|
||||
it("should handle URLs with ports", () => {
|
||||
const result = toRequestyServiceUrl("https://custom.requesty.ai:3000/v1")
|
||||
expect(result).toBe("https://custom.requesty.ai:3000/v1")
|
||||
})
|
||||
})
|
||||
|
||||
describe("with different service types", () => {
|
||||
it("should return router URL for router service", () => {
|
||||
const result = toRequestyServiceUrl("https://router.requesty.ai/v1", "router")
|
||||
expect(result).toBe("https://router.requesty.ai/v1")
|
||||
})
|
||||
|
||||
it("should replace router with app and remove v1 for app service", () => {
|
||||
const result = toRequestyServiceUrl("https://router.requesty.ai/v1", "app")
|
||||
expect(result).toBe("https://app.requesty.ai/")
|
||||
})
|
||||
|
||||
it("should replace router with api and remove v1 for api service", () => {
|
||||
const result = toRequestyServiceUrl("https://router.requesty.ai/v1", "api")
|
||||
expect(result).toBe("https://api.requesty.ai/")
|
||||
})
|
||||
|
||||
it("should handle custom baseUrl with app service", () => {
|
||||
const result = toRequestyServiceUrl("https://router.custom.ai/v1", "app")
|
||||
expect(result).toBe("https://app.custom.ai/")
|
||||
})
|
||||
|
||||
it("should handle URLs where router appears multiple times", () => {
|
||||
const result = toRequestyServiceUrl("https://router.router-requesty.ai/v1", "app")
|
||||
// This will replace the first occurrence only
|
||||
expect(result).toBe("https://app.router-requesty.ai/")
|
||||
})
|
||||
})
|
||||
|
||||
describe("error handling", () => {
|
||||
it("should fall back to default URL for invalid baseUrl", () => {
|
||||
const result = toRequestyServiceUrl("not-a-valid-url")
|
||||
expect(result).toBe("https://router.requesty.ai/v1")
|
||||
expect(console.warn).toHaveBeenCalledWith('Invalid base URL "not-a-valid-url", falling back to default')
|
||||
})
|
||||
|
||||
it("should fall back to default URL for malformed URL", () => {
|
||||
const result = toRequestyServiceUrl("ht!tp://[invalid")
|
||||
expect(result).toBe("https://router.requesty.ai/v1")
|
||||
expect(console.warn).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should fall back to default app URL for invalid baseUrl with app service", () => {
|
||||
const result = toRequestyServiceUrl("invalid-url", "app")
|
||||
expect(result).toBe("https://app.requesty.ai/")
|
||||
expect(console.warn).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should handle null baseUrl gracefully", () => {
|
||||
const result = toRequestyServiceUrl(null as any)
|
||||
expect(result).toBe("https://router.requesty.ai/v1")
|
||||
})
|
||||
|
||||
it("should handle non-string baseUrl gracefully", () => {
|
||||
const result = toRequestyServiceUrl(123 as any)
|
||||
expect(result).toBe("https://router.requesty.ai/v1")
|
||||
})
|
||||
})
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("should handle protocol-relative URLs by falling back to default", () => {
|
||||
const result = toRequestyServiceUrl("//custom.requesty.ai/v1")
|
||||
// Protocol-relative URLs are not valid for URL constructor, will fall back
|
||||
expect(result).toBe("https://router.requesty.ai/v1")
|
||||
expect(console.warn).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should preserve query parameters", () => {
|
||||
const result = toRequestyServiceUrl("https://custom.requesty.ai/v1?key=value")
|
||||
expect(result).toBe("https://custom.requesty.ai/v1?key=value")
|
||||
})
|
||||
|
||||
it("should preserve URL fragments", () => {
|
||||
const result = toRequestyServiceUrl("https://custom.requesty.ai/v1#section")
|
||||
expect(result).toBe("https://custom.requesty.ai/v1#section")
|
||||
})
|
||||
|
||||
it("should handle URLs with authentication", () => {
|
||||
const result = toRequestyServiceUrl("https://user:pass@custom.requesty.ai/v1")
|
||||
expect(result).toBe("https://user:pass@custom.requesty.ai/v1")
|
||||
})
|
||||
})
|
||||
})
|
||||
56
src/shared/utils/requesty.ts
Normal file
56
src/shared/utils/requesty.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
const REQUESTY_BASE_URL = "https://router.requesty.ai/v1"
|
||||
|
||||
type URLType = "router" | "app" | "api"
|
||||
|
||||
/**
|
||||
* Replaces the service type in the URL (router -> app/api) and removes version suffix for non-router services
|
||||
* @param baseUrl The base URL to transform
|
||||
* @param type The service type to use
|
||||
* @returns The transformed URL
|
||||
*/
|
||||
const replaceCname = (baseUrl: string, type: URLType): string => {
|
||||
if (type === "router") {
|
||||
return baseUrl
|
||||
}
|
||||
|
||||
// Parse the URL to safely replace the subdomain
|
||||
try {
|
||||
const url = new URL(baseUrl)
|
||||
// Replace 'router' in the hostname with the service type
|
||||
if (url.hostname.includes("router")) {
|
||||
url.hostname = url.hostname.replace("router", type)
|
||||
}
|
||||
// Remove '/v1' from the pathname for non-router services
|
||||
if (url.pathname.endsWith("/v1")) {
|
||||
url.pathname = url.pathname.slice(0, -3)
|
||||
}
|
||||
return url.toString()
|
||||
} catch {
|
||||
// Fallback to simple string replacement if URL parsing fails
|
||||
return baseUrl.replace("router", type).replace("/v1", "")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a base URL to a Requesty service URL with proper validation and fallback
|
||||
* @param baseUrl Optional custom base URL. Falls back to default if invalid or not provided
|
||||
* @param service The service type (router, app, or api). Defaults to 'router'
|
||||
* @returns A valid Requesty service URL
|
||||
*/
|
||||
export const toRequestyServiceUrl = (baseUrl?: string | null, service: URLType = "router"): string => {
|
||||
// Handle null, undefined, empty string, or non-string values
|
||||
const urlToUse = baseUrl && typeof baseUrl === "string" && baseUrl.trim() ? baseUrl.trim() : REQUESTY_BASE_URL
|
||||
|
||||
try {
|
||||
// Validate the URL first
|
||||
const validatedUrl = new URL(urlToUse).toString()
|
||||
// Apply service type transformation
|
||||
return replaceCname(validatedUrl, service)
|
||||
} catch (error) {
|
||||
// If the provided baseUrl is invalid, fall back to the default
|
||||
if (baseUrl && baseUrl !== REQUESTY_BASE_URL) {
|
||||
console.warn(`Invalid base URL "${baseUrl}", falling back to default`)
|
||||
}
|
||||
return replaceCname(REQUESTY_BASE_URL, service)
|
||||
}
|
||||
}
|
||||
|
|
@ -424,6 +424,7 @@ const ApiOptions = ({
|
|||
|
||||
{selectedProvider === "requesty" && (
|
||||
<Requesty
|
||||
uriScheme={uriScheme}
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
routerModels={routerModels}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -22,6 +23,7 @@ type RequestyProps = {
|
|||
refetchRouterModels: () => void
|
||||
organizationAllowList: OrganizationAllowList
|
||||
modelValidationError?: string
|
||||
uriScheme?: string
|
||||
}
|
||||
|
||||
export const Requesty = ({
|
||||
|
|
@ -31,6 +33,7 @@ export const Requesty = ({
|
|||
refetchRouterModels,
|
||||
organizationAllowList,
|
||||
modelValidationError,
|
||||
uriScheme,
|
||||
}: RequestyProps) => {
|
||||
const { t } = useAppTranslation()
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -65,7 +77,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
|
||||
baseUrl={apiConfiguration.requestyBaseUrl}
|
||||
apiKey={apiConfiguration.requestyApiKey}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</VSCodeTextField>
|
||||
|
|
@ -73,12 +88,19 @@ export const Requesty = ({
|
|||
{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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
@ -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",
|
||||
|
|
@ -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),
|
||||
queryKey: ["requesty-key-info", baseUrl, apiKey],
|
||||
queryFn: () => getRequestyKeyInfo(baseUrl, apiKey),
|
||||
staleTime: 30 * 1000, // 30 seconds
|
||||
enabled: !!apiKey,
|
||||
...options,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue