From a2c55174a6a1078e49eaec29d8f9d73680561cff Mon Sep 17 00:00:00 2001 From: Thibault Jaigu Date: Sun, 12 Jul 2026 09:58:13 +0100 Subject: [PATCH] feat(agent): add Requesty as an LLM provider Requesty is an OpenAI-compatible LLM gateway that uses the same provider/model naming convention as OpenRouter, so this mirrors the existing OpenRouter provider on the generic OpenAI-compatible path (createOpenAICompatible + a dedicated fetch), not the openrouter SDK. - apps/server/src/lib/requesty-fetch.ts: createRequestyCompatibleFetch mirroring openrouter-fetch.ts (APICallError extraction, SDK-default retryable handling for Requesty's transient 502s) - provider-factory.ts + clients/llm/provider.ts: register requesty (baseURL https://router.requesty.ai/v1, apiKey, HTTP-Referer/X-Title headers) - packages/shared: REQUESTY_API url constant + REQUESTY in the LLM_PROVIDERS schema - apps/app: provider type, template (base URL, api-keys/docs links), form-schema enum, icon fallback - server + eval READMEs: Requesty added to the provider lists Verified: @browseros/server typecheck passes; live chat/completions to router.requesty.ai succeeds. The models.dev catalog is generated from an upstream that does not list Requesty, so it is intentionally not edited here. Signed-off-by: Thibault Jaigu --- .../app/lib/llm-providers/providerIcons.tsx | 1 + .../lib/llm-providers/providerTemplates.ts | 12 +++++ .../apps/app/lib/llm-providers/types.ts | 1 + .../ai-settings/provider-form-schema.ts | 1 + packages/browseros-agent/apps/eval/README.md | 1 + .../browseros-agent/apps/server/README.md | 2 +- .../apps/server/src/agent/provider-factory.ts | 18 +++++++ .../server/src/lib/clients/llm/provider.ts | 16 ++++++ .../apps/server/src/lib/requesty-fetch.ts | 53 +++++++++++++++++++ .../packages/shared/src/constants/urls.ts | 1 + .../packages/shared/src/schemas/llm.ts | 3 ++ 11 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 packages/browseros-agent/apps/server/src/lib/requesty-fetch.ts diff --git a/packages/browseros-agent/apps/app/lib/llm-providers/providerIcons.tsx b/packages/browseros-agent/apps/app/lib/llm-providers/providerIcons.tsx index 4c1feb3eb5..15a1416a85 100644 --- a/packages/browseros-agent/apps/app/lib/llm-providers/providerIcons.tsx +++ b/packages/browseros-agent/apps/app/lib/llm-providers/providerIcons.tsx @@ -27,6 +27,7 @@ const providerIconMap: Record = { 'openai-compatible': OpenAI, google: Gemini, openrouter: OpenRouter, + requesty: null, azure: Azure, ollama: Ollama, lmstudio: LmStudio, diff --git a/packages/browseros-agent/apps/app/lib/llm-providers/providerTemplates.ts b/packages/browseros-agent/apps/app/lib/llm-providers/providerTemplates.ts index 7d14defafb..ccb7b180f9 100644 --- a/packages/browseros-agent/apps/app/lib/llm-providers/providerTemplates.ts +++ b/packages/browseros-agent/apps/app/lib/llm-providers/providerTemplates.ts @@ -133,6 +133,16 @@ export const providerTemplates: ProviderTemplate[] = [ setupGuideUrl: 'https://docs.browseros.com/features/bring-your-own-llm#openrouter', }), + { + id: 'requesty', + name: 'Requesty', + defaultBaseUrl: 'https://router.requesty.ai/v1', + defaultModelId: 'anthropic/claude-sonnet-4-5', + supportsImages: true, + contextWindow: 200000, + apiKeyUrl: 'https://app.requesty.ai/api-keys', + setupGuideUrl: 'https://docs.requesty.ai', + }, enrichTemplate('lmstudio', { defaultModelId: 'openai/gpt-oss-20b', defaultBaseUrl: 'http://localhost:1234/v1', @@ -168,6 +178,7 @@ export const providerTypeOptions: { value: ProviderType; label: string }[] = [ { value: 'openai-compatible', label: 'OpenAI Compatible' }, { value: 'google', label: 'Gemini' }, { value: 'openrouter', label: 'OpenRouter' }, + { value: 'requesty', label: 'Requesty' }, { value: 'azure', label: 'Azure' }, { value: 'ollama', label: 'Ollama' }, { value: 'lmstudio', label: 'LM Studio' }, @@ -203,6 +214,7 @@ const DEFAULT_BASE_URLS: Record = { 'openai-compatible': '', google: 'https://generativelanguage.googleapis.com/v1beta', openrouter: 'https://openrouter.ai/api/v1', + requesty: 'https://router.requesty.ai/v1', azure: '', ollama: 'http://localhost:11434/v1', lmstudio: 'http://localhost:1234/v1', diff --git a/packages/browseros-agent/apps/app/lib/llm-providers/types.ts b/packages/browseros-agent/apps/app/lib/llm-providers/types.ts index d764c17ced..46f24708c9 100644 --- a/packages/browseros-agent/apps/app/lib/llm-providers/types.ts +++ b/packages/browseros-agent/apps/app/lib/llm-providers/types.ts @@ -5,6 +5,7 @@ export type ProviderType = | 'openai-compatible' | 'google' | 'openrouter' + | 'requesty' | 'azure' | 'ollama' | 'lmstudio' diff --git a/packages/browseros-agent/apps/app/screens/ai-settings/provider-form-schema.ts b/packages/browseros-agent/apps/app/screens/ai-settings/provider-form-schema.ts index 60d0b8adf9..37e5e6b8e5 100644 --- a/packages/browseros-agent/apps/app/screens/ai-settings/provider-form-schema.ts +++ b/packages/browseros-agent/apps/app/screens/ai-settings/provider-form-schema.ts @@ -8,6 +8,7 @@ const providerTypeEnum = z.enum([ 'openai-compatible', 'google', 'openrouter', + 'requesty', 'azure', 'ollama', 'lmstudio', diff --git a/packages/browseros-agent/apps/eval/README.md b/packages/browseros-agent/apps/eval/README.md index fcc764fd06..c60d01991e 100644 --- a/packages/browseros-agent/apps/eval/README.md +++ b/packages/browseros-agent/apps/eval/README.md @@ -188,6 +188,7 @@ Set eval environment variables in the monorepo root `.env.development` file. | Azure OpenAI | `azure` | Yes | | AWS Bedrock | `bedrock` | No | | OpenRouter | `openrouter` | No | +| Requesty | `requesty` | No | | Fireworks, Together, etc. | `openai-compatible` | Yes | | Ollama | `ollama` | No | | Clado Action (executor only) | `clado-action` | Yes | diff --git a/packages/browseros-agent/apps/server/README.md b/packages/browseros-agent/apps/server/README.md index e4fdda8c18..c47f86d632 100644 --- a/packages/browseros-agent/apps/server/README.md +++ b/packages/browseros-agent/apps/server/README.md @@ -72,7 +72,7 @@ Tools organized by category: The agent loop uses the [Vercel AI SDK](https://sdk.vercel.ai) to orchestrate multi-step browser automation: -- **Multi-provider support** — OpenAI, Anthropic, Google, Azure, Bedrock, OpenRouter, Ollama, LM Studio, and any OpenAI-compatible endpoint +- **Multi-provider support** — OpenAI, Anthropic, Google, Azure, Bedrock, OpenRouter, Requesty, Ollama, LM Studio, and any OpenAI-compatible endpoint - **Session management** — conversations persist in a local SQLite database - **Context overflow handling** — automatic message compaction when context windows fill up - **MCP client** — connects to external MCP servers for additional tool access (40+ app integrations) diff --git a/packages/browseros-agent/apps/server/src/agent/provider-factory.ts b/packages/browseros-agent/apps/server/src/agent/provider-factory.ts index e2cd2ffe99..f7093d1ab4 100644 --- a/packages/browseros-agent/apps/server/src/agent/provider-factory.ts +++ b/packages/browseros-agent/apps/server/src/agent/provider-factory.ts @@ -28,6 +28,7 @@ import { createCodexFetch } from '../lib/clients/oauth/codex-fetch' import { createCopilotFetch } from '../lib/clients/oauth/copilot-fetch' import { logger } from '../lib/logger' import { createOpenRouterCompatibleFetch } from '../lib/openrouter-fetch' +import { createRequestyCompatibleFetch } from '../lib/requesty-fetch' import { ensureWorkspaceInstructionFile } from './acp-instructions/ensureInstructionFile' import { ACP_PROVIDER_TYPES, isAcpProvider } from './acp-providers' import type { BuildSystemPromptOptions } from './prompt' @@ -300,6 +301,22 @@ function createOpenRouterFactory( }) } +function createRequestyFactory( + config: ResolvedAgentConfig, +): (modelId: string) => unknown { + if (!config.apiKey) throw new Error('Requesty provider requires apiKey') + return createOpenAICompatible({ + name: 'requesty', + baseURL: EXTERNAL_URLS.REQUESTY_API, + apiKey: config.apiKey, + headers: { + 'HTTP-Referer': 'https://browseros.com', + 'X-Title': 'BrowserOS', + }, + fetch: createRequestyCompatibleFetch(), + }) +} + function createAzureFactory( config: ResolvedAgentConfig, ): (modelId: string) => unknown { @@ -452,6 +469,7 @@ const PROVIDER_FACTORIES: Record = { [LLM_PROVIDERS.OPENAI]: createOpenAIFactory, [LLM_PROVIDERS.GOOGLE]: createGoogleFactory, [LLM_PROVIDERS.OPENROUTER]: createOpenRouterFactory, + [LLM_PROVIDERS.REQUESTY]: createRequestyFactory, [LLM_PROVIDERS.AZURE]: createAzureFactory, [LLM_PROVIDERS.LMSTUDIO]: createLMStudioFactory, [LLM_PROVIDERS.OLLAMA]: createOllamaFactory, diff --git a/packages/browseros-agent/apps/server/src/lib/clients/llm/provider.ts b/packages/browseros-agent/apps/server/src/lib/clients/llm/provider.ts index a3d8a8af3a..4619946b96 100644 --- a/packages/browseros-agent/apps/server/src/lib/clients/llm/provider.ts +++ b/packages/browseros-agent/apps/server/src/lib/clients/llm/provider.ts @@ -19,6 +19,7 @@ import type { LanguageModel } from 'ai' import { createBrowserOSFetch } from '../../browseros-fetch' import { logger } from '../../logger' import { createOpenRouterCompatibleFetch } from '../../openrouter-fetch' +import { createRequestyCompatibleFetch } from '../../requesty-fetch' import { createCodexFetch } from '../oauth/codex-fetch' import { createCopilotFetch } from '../oauth/copilot-fetch' import { @@ -53,6 +54,20 @@ function createOpenRouterModel(config: ResolvedLLMConfig): LanguageModel { })(config.model) } +function createRequestyModel(config: ResolvedLLMConfig): LanguageModel { + if (!config.apiKey) throw new Error('Requesty provider requires apiKey') + return createOpenAICompatible({ + name: 'requesty', + baseURL: EXTERNAL_URLS.REQUESTY_API, + apiKey: config.apiKey, + headers: { + 'HTTP-Referer': 'https://browseros.com', + 'X-Title': 'BrowserOS', + }, + fetch: createRequestyCompatibleFetch(), + })(config.model) +} + function createAzureModel(config: ResolvedLLMConfig): LanguageModel { if (!config.apiKey || !config.resourceName) { throw new Error('Azure provider requires apiKey and resourceName') @@ -185,6 +200,7 @@ const PROVIDER_FACTORIES: Record = { [LLM_PROVIDERS.OPENAI]: createOpenAIModel, [LLM_PROVIDERS.GOOGLE]: createGoogleModel, [LLM_PROVIDERS.OPENROUTER]: createOpenRouterModel, + [LLM_PROVIDERS.REQUESTY]: createRequestyModel, [LLM_PROVIDERS.AZURE]: createAzureModel, [LLM_PROVIDERS.OLLAMA]: createOllamaModel, [LLM_PROVIDERS.LMSTUDIO]: createLMStudioModel, diff --git a/packages/browseros-agent/apps/server/src/lib/requesty-fetch.ts b/packages/browseros-agent/apps/server/src/lib/requesty-fetch.ts new file mode 100644 index 0000000000..ff3535c8df --- /dev/null +++ b/packages/browseros-agent/apps/server/src/lib/requesty-fetch.ts @@ -0,0 +1,53 @@ +import { APICallError } from '@ai-sdk/provider' + +/** + * Creates a fetch function that extracts detailed error messages from Requesty. + * + * Requesty is an OpenAI-compatible router (like OpenRouter). It can wrap + * upstream provider errors in a generic message, with the actual details + * nested under metadata.raw. This fetch intercepts HTTP errors and extracts + * the real error message so callers see the upstream failure. + * + * IMPORTANT: Throws APICallError (not plain Error) so the Vercel AI SDK's retry + * mechanism works correctly. The SDK's APICallError automatically calculates + * `isRetryable` from the statusCode (408, 409, 429, 500+ are retryable) - we + * don't override this default. Requesty's router occasionally returns transient + * 502s, so retryable handling matters here. + */ +export function createRequestyCompatibleFetch(): typeof fetch { + return (async (url: RequestInfo | URL, options?: RequestInit) => { + const response = await globalThis.fetch(url, options) + + if (!response.ok) { + const statusCode = response.status + let errorMessage = `HTTP ${statusCode}: ${response.statusText}` + let responseBody: string | undefined + + try { + responseBody = await response.clone().text() + const parsed = JSON.parse(responseBody) + if (parsed.error?.message) { + errorMessage = parsed.error.message + if (parsed.error.code) { + errorMessage = `[${parsed.error.code}] ${errorMessage}` + } + if (parsed.error.metadata?.raw) { + errorMessage += ` (${JSON.stringify(parsed.error.metadata.raw)})` + } + } + } catch { + // Keep default error message if parsing fails + } + + throw new APICallError({ + message: errorMessage, + url: typeof url === 'string' ? url : url.toString(), + requestBodyValues: {}, + statusCode, + responseBody, + }) + } + + return response + }) as typeof fetch +} diff --git a/packages/browseros-agent/packages/shared/src/constants/urls.ts b/packages/browseros-agent/packages/shared/src/constants/urls.ts index dc625a6bfa..fd9db0aa13 100644 --- a/packages/browseros-agent/packages/shared/src/constants/urls.ts +++ b/packages/browseros-agent/packages/shared/src/constants/urls.ts @@ -18,5 +18,6 @@ export const EXTERNAL_URLS = { QWEN_DEVICE_CODE: 'https://chat.qwen.ai/api/v1/oauth2/device/code', QWEN_OAUTH_TOKEN: 'https://chat.qwen.ai/api/v1/oauth2/token', QWEN_CODE_API: 'https://portal.qwen.ai/v1', + REQUESTY_API: 'https://router.requesty.ai/v1', AGENT_CONTROL_WORKER: 'https://agent-control-worker.eng-6b4.workers.dev', } as const diff --git a/packages/browseros-agent/packages/shared/src/schemas/llm.ts b/packages/browseros-agent/packages/shared/src/schemas/llm.ts index 3de65f6972..7a8bcc18c6 100644 --- a/packages/browseros-agent/packages/shared/src/schemas/llm.ts +++ b/packages/browseros-agent/packages/shared/src/schemas/llm.ts @@ -17,6 +17,7 @@ export const LLM_PROVIDERS = { OPENAI: 'openai', GOOGLE: 'google', OPENROUTER: 'openrouter', + REQUESTY: 'requesty', AZURE: 'azure', OLLAMA: 'ollama', LMSTUDIO: 'lmstudio', @@ -42,6 +43,7 @@ export const LLMProviderSchema: z.ZodEnum< 'openai', 'google', 'openrouter', + 'requesty', 'azure', 'ollama', 'lmstudio', @@ -62,6 +64,7 @@ export const LLMProviderSchema: z.ZodEnum< LLM_PROVIDERS.OPENAI, LLM_PROVIDERS.GOOGLE, LLM_PROVIDERS.OPENROUTER, + LLM_PROVIDERS.REQUESTY, LLM_PROVIDERS.AZURE, LLM_PROVIDERS.OLLAMA, LLM_PROVIDERS.LMSTUDIO,