diff --git a/README.md b/README.md
index 5133adec..096ef06c 100644
--- a/README.md
+++ b/README.md
@@ -17,7 +17,7 @@ Alice is more than a chatbot; she’s built to feel present, responsive, emotion
### 💻 Local and Cloud use
-Alice is designed to work with Cloud(OpenAI / Codex subscription, OpenRouter, Z.ai, Minimax, Deepseek) and Local LLMs (Ollama/LM Studio).
+Alice is designed to work with Cloud(OpenAI / Codex subscription, OpenRouter, Requesty, Z.ai, Minimax, Deepseek) and Local LLMs (Ollama/LM Studio).
Has built-in speech-to-text, text-to-speech, and embedding services.
While the OpenAI cloud API is preferred and provides the best user experience, Alice can also operate **fully locally** (experimental).
@@ -83,7 +83,7 @@ With the local STT model, you can set a **wake-up word** (like "Hey, Siri").
Fully customizable settings interface:
-* LLM provider selection between OpenAI, OpenRouter, API Route, DeepSeek, Z.ai(coding plan), Minimax(token plan), Ollama, LM Studio
+* LLM provider selection between OpenAI, OpenRouter, API Route, Requesty, DeepSeek, Z.ai(coding plan), Minimax(token plan), Ollama, LM Studio
* Cloud or local TTS, STT, Embeddings
* Model choice & parameters (temperature, top\_p, history, etc)
* Prompt and summarization tuning
@@ -130,7 +130,7 @@ Follow the [Setup Instructions](https://github.com/pmbstyle/Alice/blob/main/docs
* **Frontend:** [Vue.js](https://vuejs.org/), [TailwindCSS](https://tailwindcss.com/)
* **Desktop Shell:** [Electron](https://www.electronjs.org/)
* **State Management:** [Pinia](https://pinia.vuejs.org/)
-* **AI APIs:** [OpenAI](https://platform.openai.com/), [OpenRouter](https://openrouter.ai/), [API Route](https://www.api-route.com/), [DeepSeek](https://platform.deepseek.com/), [Groq](https://console.groq.com/)
+* **AI APIs:** [OpenAI](https://platform.openai.com/), [OpenRouter](https://openrouter.ai/), [API Route](https://www.api-route.com/), [Requesty](https://www.requesty.ai/), [DeepSeek](https://platform.deepseek.com/), [Groq](https://console.groq.com/)
* **Backend:** [Go](https://go.dev/)
* **Vector search engine**: [hnswlib-node](https://github.com/nmslib/hnswlib)
* **Local storage**: [better-sqlite3](https://github.com/WiseLibs/better-sqlite3)
diff --git a/docs/setupInstructions.md b/docs/setupInstructions.md
index d31a4925..07d499e7 100644
--- a/docs/setupInstructions.md
+++ b/docs/setupInstructions.md
@@ -14,7 +14,7 @@ Run in your terminal `xattr -cr "/Applications/Alice AI App.app"`
# AI Provider Setup
-Alice supports OpenAI, OpenRouter, API Route, DeepSeek, MiniMax, Z.ai, and local LLM inference.
+Alice supports OpenAI, OpenRouter, API Route, Requesty, DeepSeek, MiniMax, Z.ai, and local LLM inference.
## OpenAI (Default)
@@ -46,6 +46,15 @@ Alice supports OpenAI, OpenRouter, API Route, DeepSeek, MiniMax, Z.ai, and local
- API Route powers chat inference only; cloud TTS/STT/embeddings still require OpenAI or local voice and memory mode
- No image generation support (use OpenAI provider for image-gen)
+## Requesty (Alternative)
+
+- Go to [Requesty](https://app.requesty.ai/api-keys) to get your Requesty API key
+- Select "AI Provider" in Core Settings as Requesty
+- Alice loads Requesty managed models (for example `claude-sonnet-4-5`) and the full model catalog (for example `openai/gpt-4o-mini`); the default model is `openai/gpt-4o-mini`
+- Requesty powers chat inference only; cloud TTS/STT/embeddings still require OpenAI or local voice and memory mode
+- No image generation support (use OpenAI provider for image-gen)
+- See the [Requesty docs](https://docs.requesty.ai) for more details
+
## Local Ollama / LM studio
- Run Ollama or LM studio
diff --git a/electron/main/securityBoundaries.ts b/electron/main/securityBoundaries.ts
index 53b82952..b4562395 100644
--- a/electron/main/securityBoundaries.ts
+++ b/electron/main/securityBoundaries.ts
@@ -7,6 +7,7 @@ const BUILT_IN_HTTP_BASE_URLS = [
'https://api.minimax.io',
'https://api.deepseek.com',
'https://global.api-route.com',
+ 'https://router.requesty.ai',
'http://localhost:11434',
'http://localhost:1234',
]
diff --git a/electron/main/settingsManager.ts b/electron/main/settingsManager.ts
index fd472bdb..e36b4812 100644
--- a/electron/main/settingsManager.ts
+++ b/electron/main/settingsManager.ts
@@ -26,6 +26,7 @@ export interface AppSettings {
VITE_MINIMAX_API_KEY?: string
VITE_DEEPSEEK_API_KEY?: string
VITE_API_ROUTE_API_KEY?: string
+ VITE_REQUESTY_API_KEY?: string
VITE_GROQ_API_KEY?: string
sttProvider?: 'openai' | 'groq' | 'transformers'
aiProvider?:
@@ -37,6 +38,7 @@ export interface AppSettings {
| 'minimax'
| 'deepseek'
| 'api-route'
+ | 'requesty'
| 'codex'
// Transformers STT settings
diff --git a/electron/main/settingsSecurity.ts b/electron/main/settingsSecurity.ts
index 722f064c..1c8623ad 100644
--- a/electron/main/settingsSecurity.ts
+++ b/electron/main/settingsSecurity.ts
@@ -5,6 +5,7 @@ export const SECRET_SETTING_KEYS = [
'VITE_MINIMAX_API_KEY',
'VITE_DEEPSEEK_API_KEY',
'VITE_API_ROUTE_API_KEY',
+ 'VITE_REQUESTY_API_KEY',
'VITE_GROQ_API_KEY',
'VITE_GOOGLE_API_KEY',
'VITE_JACKETT_API_KEY',
diff --git a/src/__tests__/securityBoundaries.test.ts b/src/__tests__/securityBoundaries.test.ts
index a6d47da0..ae2ad54e 100644
--- a/src/__tests__/securityBoundaries.test.ts
+++ b/src/__tests__/securityBoundaries.test.ts
@@ -56,6 +56,7 @@ describe('security boundaries', () => {
'https://openrouter.ai/api/v1/models',
'https://api.minimax.io/v1/models',
'https://global.api-route.com/v1/models',
+ 'https://router.requesty.ai/v1/models',
]) {
expect(validateHttpBridgeUrl(url, origins)).toBe(url)
}
diff --git a/src/components/settings/AssistantSettingsTab.vue b/src/components/settings/AssistantSettingsTab.vue
index 9ce7dfc0..f412db29 100644
--- a/src/components/settings/AssistantSettingsTab.vue
+++ b/src/components/settings/AssistantSettingsTab.vue
@@ -73,6 +73,8 @@
currentSettings.deepseekBaseUrl) ||
(currentSettings.aiProvider === 'api-route' &&
currentSettings.VITE_API_ROUTE_API_KEY) ||
+ (currentSettings.aiProvider === 'requesty' &&
+ currentSettings.VITE_REQUESTY_API_KEY) ||
(currentSettings.aiProvider === 'ollama' &&
currentSettings.ollamaBaseUrl) ||
(currentSettings.aiProvider === 'lm-studio' &&
@@ -316,6 +318,8 @@
currentSettings.deepseekBaseUrl) ||
(currentSettings.aiProvider === 'api-route' &&
currentSettings.VITE_API_ROUTE_API_KEY) ||
+ (currentSettings.aiProvider === 'requesty' &&
+ currentSettings.VITE_REQUESTY_API_KEY) ||
(currentSettings.aiProvider === 'ollama' &&
currentSettings.ollamaBaseUrl) ||
(currentSettings.aiProvider === 'lm-studio' &&
@@ -531,6 +535,7 @@ const getProviderDisplayName = (provider: string): string => {
minimax: 'MiniMax',
deepseek: 'DeepSeek',
'api-route': 'API Route',
+ requesty: 'Requesty',
}
return providerNames[provider] || provider
}
diff --git a/src/components/settings/CoreSettingsTab.vue b/src/components/settings/CoreSettingsTab.vue
index e8960a36..fed2d48e 100644
--- a/src/components/settings/CoreSettingsTab.vue
+++ b/src/components/settings/CoreSettingsTab.vue
@@ -23,6 +23,7 @@
+
@@ -238,6 +239,28 @@
>.
+
+
+
+
+ Required for chat models when using Requesty.
+ Get an API key.
+
+
{
formData.aiProvider === 'minimax' ||
formData.aiProvider === 'deepseek' ||
formData.aiProvider === 'api-route' ||
+ formData.aiProvider === 'requesty' ||
formData.aiProvider === 'codex') &&
!formData.VITE_OPENAI_API_KEY.trim()
) {
@@ -546,6 +552,33 @@ const testAPIRouteKey = async () => {
}
}
+const testRequestyKey = async () => {
+ if (!formData.VITE_REQUESTY_API_KEY.trim()) {
+ testResult.requesty.error = 'API Key cannot be empty.'
+ testResult.requesty.success = false
+ return
+ }
+
+ isTesting.requesty = true
+ testResult.requesty.error = ''
+ testResult.requesty.success = false
+
+ try {
+ await listRequestyModelsForConfig(formData.VITE_REQUESTY_API_KEY)
+ testResult.requesty.success = true
+ } catch (e: any) {
+ testResult.requesty.error = 'API Key is invalid or has no permissions.'
+ if (e.message?.includes('401')) {
+ testResult.requesty.error = 'Invalid API key - please check your key.'
+ } else if (e.message?.includes('429')) {
+ testResult.requesty.error =
+ 'Rate limit exceeded - please try again later.'
+ }
+ } finally {
+ isTesting.requesty = false
+ }
+}
+
const syncCodexStatus = async () => {
const status = await window.aliceIPC.invoke('codex-auth:status')
const connected = Boolean(status?.connected)
@@ -706,6 +739,8 @@ const resetTestResults = () => {
testResult.deepseek.error = ''
testResult.apiRoute.success = false
testResult.apiRoute.error = ''
+ testResult.requesty.success = false
+ testResult.requesty.error = ''
testResult.codex.success = false
testResult.codex.error = ''
testResult.ollama.success = false
@@ -719,6 +754,8 @@ const isCurrentProviderTested = () => {
return testResult.openai.success
} else if (formData.aiProvider === 'openrouter') {
return testResult.openrouter.success
+ } else if (formData.aiProvider === 'requesty') {
+ return testResult.requesty.success
} else if (formData.aiProvider === 'zai') {
return (
testResult.zai.success &&
diff --git a/src/components/wizard/steps/AIProviderStep.vue b/src/components/wizard/steps/AIProviderStep.vue
index f89f248b..a606529d 100644
--- a/src/components/wizard/steps/AIProviderStep.vue
+++ b/src/components/wizard/steps/AIProviderStep.vue
@@ -25,6 +25,7 @@
+
@@ -163,6 +164,69 @@
+
+
+
+
+
Requesty routes to hundreds of models through one API key. You can
+ choose local models for voice features in the next step.
+
+
+
+
+
+
+
+
+
+
@@ -918,6 +982,7 @@ defineEmits<{
'test-minimax': []
'test-deepseek': []
'test-api-route': []
+ 'test-requesty': []
'test-codex': []
'test-ollama': []
'test-lmstudio': []
diff --git a/src/components/wizard/steps/FinalSetupStep.vue b/src/components/wizard/steps/FinalSetupStep.vue
index fc46dfd3..2c117f8e 100644
--- a/src/components/wizard/steps/FinalSetupStep.vue
+++ b/src/components/wizard/steps/FinalSetupStep.vue
@@ -160,6 +160,7 @@ const providerLabel = (provider: string) => {
minimax: 'MiniMax',
deepseek: 'DeepSeek',
'api-route': 'API Route',
+ requesty: 'Requesty',
ollama: 'Ollama',
openai: 'OpenAI',
openrouter: 'OpenRouter',
diff --git a/src/components/wizard/steps/VoiceModelsStep.vue b/src/components/wizard/steps/VoiceModelsStep.vue
index a2950a7f..d6f1414b 100644
--- a/src/components/wizard/steps/VoiceModelsStep.vue
+++ b/src/components/wizard/steps/VoiceModelsStep.vue
@@ -157,7 +157,8 @@
formData.aiProvider === 'zai' ||
formData.aiProvider === 'minimax' ||
formData.aiProvider === 'deepseek' ||
- formData.aiProvider === 'api-route') &&
+ formData.aiProvider === 'api-route' ||
+ formData.aiProvider === 'requesty') &&
!formData.VITE_OPENAI_API_KEY?.trim()
"
class="alert alert-warning text-sm"
@@ -210,7 +211,8 @@
formData.aiProvider === 'zai' ||
formData.aiProvider === 'minimax' ||
formData.aiProvider === 'deepseek' ||
- formData.aiProvider === 'api-route'
+ formData.aiProvider === 'api-route' ||
+ formData.aiProvider === 'requesty'
"
class="form-control"
>
diff --git a/src/modules/conversation/backendService.ts b/src/modules/conversation/backendService.ts
index 5bd2d511..b51d919c 100644
--- a/src/modules/conversation/backendService.ts
+++ b/src/modules/conversation/backendService.ts
@@ -11,6 +11,7 @@ export interface BackendServiceDependencies {
VITE_MINIMAX_API_KEY?: string
VITE_DEEPSEEK_API_KEY?: string
VITE_API_ROUTE_API_KEY?: string
+ VITE_REQUESTY_API_KEY?: string
codexAuthConnected?: boolean
ollamaBaseUrl?: string
lmStudioBaseUrl?: string
@@ -108,6 +109,10 @@ export function createBackendService(
deps.logInfo('Cannot fetch models: API Route API Key is missing.')
return
}
+ if (provider === 'requesty' && !config.VITE_REQUESTY_API_KEY) {
+ deps.logInfo('Cannot fetch models: Requesty API Key is missing.')
+ return
+ }
if (provider === 'codex' && !config.codexAuthConnected) {
deps.logInfo('Cannot fetch models: ChatGPT Codex is not connected.')
return
@@ -134,6 +139,7 @@ export function createBackendService(
minimax: 'MiniMax',
deepseek: 'DeepSeek',
'api-route': 'API Route',
+ requesty: 'Requesty',
codex: 'ChatGPT Codex',
}
const providerName = providerNameMap[provider] || provider
diff --git a/src/services/apiClients.ts b/src/services/apiClients.ts
index 986f9381..82b63faf 100644
--- a/src/services/apiClients.ts
+++ b/src/services/apiClients.ts
@@ -6,6 +6,7 @@ import {
API_ROUTE_OPENAI_BASE_URL,
DEEPSEEK_OPENAI_BASE_URL,
MINIMAX_OPENAI_BASE_URL,
+ REQUESTY_OPENAI_BASE_URL,
ZAI_CODING_BASE_URL,
} from './llmProviders/providerCatalog'
@@ -18,6 +19,7 @@ let zaiClient: OpenAI | null = null
let minimaxClient: OpenAI | null = null
let deepseekClient: OpenAI | null = null
let apiRouteClient: OpenAI | null = null
+let requestyClient: OpenAI | null = null
export function getOpenAIClient(): OpenAI {
if (!openaiClient) {
@@ -109,6 +111,16 @@ export function getAPIRouteClient(): OpenAI {
return apiRouteClient
}
+export function getRequestyClient(): OpenAI {
+ if (!requestyClient) {
+ initializeRequestyClient()
+ }
+ if (!requestyClient) {
+ throw new Error('Requesty client could not be initialized')
+ }
+ return requestyClient
+}
+
function initializeOpenAIClient(): void {
const settings = useSettingsStore().config
if (!settings.VITE_OPENAI_API_KEY) {
@@ -253,6 +265,21 @@ function initializeAPIRouteClient(): void {
})
}
+function initializeRequestyClient(): void {
+ const settings = useSettingsStore().config
+ if (!settings.VITE_REQUESTY_API_KEY) {
+ throw new Error('Requesty API Key is not configured.')
+ }
+
+ requestyClient = new OpenAI({
+ apiKey: settings.VITE_REQUESTY_API_KEY,
+ baseURL: REQUESTY_OPENAI_BASE_URL,
+ dangerouslyAllowBrowser: true,
+ timeout: 20 * 1000,
+ maxRetries: 1,
+ })
+}
+
export function reinitializeClients(): void {
console.log('Reinitializing API clients with updated settings...')
@@ -327,6 +354,14 @@ export function reinitializeClients(): void {
console.error('Failed to reinitialize API Route client:', error)
apiRouteClient = null
}
+
+ try {
+ initializeRequestyClient()
+ console.log('Requesty client reinitialized successfully')
+ } catch (error) {
+ console.error('Failed to reinitialize Requesty client:', error)
+ requestyClient = null
+ }
}
export async function initializeClients(): Promise
{
diff --git a/src/services/apiService.ts b/src/services/apiService.ts
index 4b1743e4..a5451b69 100644
--- a/src/services/apiService.ts
+++ b/src/services/apiService.ts
@@ -11,6 +11,7 @@ import {
getMiniMaxClient,
getDeepSeekClient,
getAPIRouteClient,
+ getRequestyClient,
} from './apiClients'
import {
createOpenAIResponse as createOpenAIResponseWithOpenAI,
@@ -38,6 +39,10 @@ import {
createAPIRouteResponse,
listAPIRouteModels,
} from './llmProviders/apiRoute'
+import {
+ createRequestyResponse,
+ listRequestyModels,
+} from './llmProviders/requesty'
import {
createCodexResponse,
createCodexTextResponse,
@@ -128,6 +133,8 @@ function getAIClient(): OpenAI {
return getDeepSeekClient()
case 'api-route':
return getAPIRouteClient()
+ case 'requesty':
+ return getRequestyClient()
default:
return getOpenAIClient()
}
@@ -156,6 +163,9 @@ export const fetchOpenAIModels = async (): Promise => {
if (settings.aiProvider === 'api-route') {
return listAPIRouteModels()
}
+ if (settings.aiProvider === 'requesty') {
+ return listRequestyModels()
+ }
if (settings.aiProvider === 'codex') {
return listCodexModels()
}
@@ -234,6 +244,15 @@ export const createOpenAIResponse = async (
signal
)
}
+ if (settings.aiProvider === 'requesty') {
+ return createRequestyResponse(
+ input,
+ previousResponseId,
+ stream,
+ customInstructions,
+ signal
+ )
+ }
if (settings.aiProvider === 'codex') {
return createCodexResponse(
input,
diff --git a/src/services/llmProviders/__tests__/providerCatalog.test.ts b/src/services/llmProviders/__tests__/providerCatalog.test.ts
index 77a32ef0..a18baae2 100644
--- a/src/services/llmProviders/__tests__/providerCatalog.test.ts
+++ b/src/services/llmProviders/__tests__/providerCatalog.test.ts
@@ -84,4 +84,12 @@ describe('providerCatalog', () => {
'claude-sonnet-4-5'
)
})
+
+ it('accepts dynamically discovered Requesty models', () => {
+ expect(getStaticModelsForProvider('requesty')).toEqual([])
+ expect(getSafeProviderModel('requesty', '')).toBe('openai/gpt-4o-mini')
+ expect(getSafeProviderModel('requesty', 'claude-sonnet-4-5')).toBe(
+ 'claude-sonnet-4-5'
+ )
+ })
})
diff --git a/src/services/llmProviders/__tests__/requesty.test.ts b/src/services/llmProviders/__tests__/requesty.test.ts
new file mode 100644
index 00000000..c4d4cba4
--- /dev/null
+++ b/src/services/llmProviders/__tests__/requesty.test.ts
@@ -0,0 +1,148 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+import { createPinia, setActivePinia } from 'pinia'
+import { useSettingsStore } from '../../../stores/settingsStore'
+import { getRequestyClient } from '../../apiClients'
+import {
+ createRequestyResponse,
+ listRequestyModelsForConfig,
+} from '../requesty'
+
+vi.mock('../../apiClients', () => ({
+ getRequestyClient: vi.fn(),
+}))
+
+describe('Requesty model listing', () => {
+ afterEach(() => {
+ vi.restoreAllMocks()
+ delete (globalThis as any).window
+ })
+
+ it('lists managed models first, then the model catalog', async () => {
+ const request = vi.fn().mockImplementation(({ url }: { url: string }) =>
+ Promise.resolve({
+ success: true,
+ status: 200,
+ data: {
+ data: url.endsWith('/models/managed')
+ ? [
+ { id: 'gpt-5.4-mini', api: 'chat' },
+ { id: 'claude-sonnet-4-5', api: 'chat' },
+ ]
+ : [
+ { id: 'openai/gpt-4o-mini', api: 'chat' },
+ { id: 'openai/text-embedding-3-small', api: 'embedding' },
+ { id: 'anthropic/claude-sonnet-4-5', api: 'chat' },
+ ],
+ },
+ })
+ )
+ ;(globalThis as any).window = { httpAPI: { request } }
+
+ const models = await listRequestyModelsForConfig('rqsty-test')
+
+ expect(models.map(model => model.id)).toEqual([
+ 'claude-sonnet-4-5',
+ 'gpt-5.4-mini',
+ 'anthropic/claude-sonnet-4-5',
+ 'openai/gpt-4o-mini',
+ ])
+ expect(request).toHaveBeenCalledWith(
+ expect.objectContaining({
+ url: 'https://router.requesty.ai/v1/models',
+ method: 'GET',
+ headers: { Authorization: 'Bearer rqsty-test' },
+ })
+ )
+ expect(request).toHaveBeenCalledWith(
+ expect.objectContaining({
+ url: 'https://router.requesty.ai/v1/models/managed',
+ })
+ )
+ })
+
+ it('keeps the catalog when the managed list is unavailable', async () => {
+ const request = vi.fn().mockImplementation(({ url }: { url: string }) =>
+ Promise.resolve(
+ url.endsWith('/models/managed')
+ ? { success: false, error: 'network error' }
+ : {
+ success: true,
+ status: 200,
+ data: { data: [{ id: 'openai/gpt-4o-mini', api: 'chat' }] },
+ }
+ )
+ )
+ ;(globalThis as any).window = { httpAPI: { request } }
+
+ const models = await listRequestyModelsForConfig('rqsty-test')
+
+ expect(models.map(model => model.id)).toEqual(['openai/gpt-4o-mini'])
+ })
+
+ it('rejects an invalid key', async () => {
+ const request = vi.fn().mockResolvedValue({
+ success: true,
+ status: 403,
+ data: { error: { message: 'Invalid authorization token' } },
+ })
+ ;(globalThis as any).window = { httpAPI: { request } }
+
+ await expect(listRequestyModelsForConfig('bad-key')).rejects.toThrow(
+ 'Invalid authorization token'
+ )
+ })
+})
+
+describe('createRequestyResponse', () => {
+ beforeEach(() => {
+ setActivePinia(createPinia())
+ ;(globalThis as any).window = {
+ customToolsAPI: {
+ list: vi.fn().mockResolvedValue({
+ success: true,
+ data: {
+ tools: [],
+ diagnostics: [],
+ filePath: '',
+ lastModified: Date.now(),
+ },
+ }),
+ },
+ }
+ })
+
+ afterEach(() => {
+ vi.restoreAllMocks()
+ delete (globalThis as any).window
+ })
+
+ it('sends chat completions with the configured Requesty model', async () => {
+ const settingsStore = useSettingsStore()
+ settingsStore.updateSetting('aiProvider', 'requesty')
+ settingsStore.updateSetting('assistantModel', 'anthropic/claude-sonnet-4-5')
+
+ const create = vi.fn().mockResolvedValue({ choices: [] })
+ vi.mocked(getRequestyClient).mockReturnValue({
+ chat: { completions: { create } },
+ } as any)
+
+ await createRequestyResponse(
+ [
+ {
+ role: 'user',
+ content: [{ type: 'input_text', text: 'Hello there' }],
+ } as any,
+ ],
+ null,
+ false
+ )
+
+ expect(create).toHaveBeenCalledWith(
+ expect.objectContaining({
+ model: 'anthropic/claude-sonnet-4-5',
+ stream: false,
+ }),
+ expect.any(Object)
+ )
+ })
+})
diff --git a/src/services/llmProviders/openAICompatible.ts b/src/services/llmProviders/openAICompatible.ts
index 334fd59d..93f6c519 100644
--- a/src/services/llmProviders/openAICompatible.ts
+++ b/src/services/llmProviders/openAICompatible.ts
@@ -10,7 +10,8 @@ import {
} from './providerCatalog'
type OpenAIClientGetter = () => OpenAI
-type OpenAICompatibleProviderKey = 'zai' | 'minimax' | 'deepseek' | 'api-route'
+type OpenAICompatibleProviderKey =
+ 'zai' | 'minimax' | 'deepseek' | 'api-route' | 'requesty'
function convertResponsesInputToChatMessages(
input: OpenAI.Responses.Request.InputItemLike[]
diff --git a/src/services/llmProviders/providerCatalog.ts b/src/services/llmProviders/providerCatalog.ts
index 9c1bdfad..0189f150 100644
--- a/src/services/llmProviders/providerCatalog.ts
+++ b/src/services/llmProviders/providerCatalog.ts
@@ -7,6 +7,7 @@ export type AIProviderKey =
| 'minimax'
| 'deepseek'
| 'api-route'
+ | 'requesty'
| 'codex'
export type ChatCompletionsProviderKey = Exclude<
@@ -98,6 +99,11 @@ export const PROVIDER_CONFIGS: Record = {
defaultModel: 'gpt-5.4-mini',
nativeWebSearch: false,
},
+ requesty: {
+ displayName: 'Requesty',
+ defaultModel: 'openai/gpt-4o-mini',
+ nativeWebSearch: false,
+ },
codex: {
displayName: 'ChatGPT Codex',
defaultModel: 'gpt-5.4',
@@ -109,6 +115,7 @@ export const ZAI_CODING_BASE_URL = 'https://api.z.ai/api/coding/paas/v4'
export const MINIMAX_OPENAI_BASE_URL = 'https://api.minimax.io/v1'
export const DEEPSEEK_OPENAI_BASE_URL = 'https://api.deepseek.com'
export const API_ROUTE_OPENAI_BASE_URL = 'https://global.api-route.com/v1'
+export const REQUESTY_OPENAI_BASE_URL = 'https://router.requesty.ai/v1'
export const CHAT_COMPLETIONS_PROVIDERS: ChatCompletionsProviderKey[] = [
'openrouter',
@@ -118,6 +125,7 @@ export const CHAT_COMPLETIONS_PROVIDERS: ChatCompletionsProviderKey[] = [
'minimax',
'deepseek',
'api-route',
+ 'requesty',
]
export function getProviderDisplayName(provider: string): string {
diff --git a/src/services/llmProviders/requesty.ts b/src/services/llmProviders/requesty.ts
new file mode 100644
index 00000000..7ede3a3f
--- /dev/null
+++ b/src/services/llmProviders/requesty.ts
@@ -0,0 +1,81 @@
+import type OpenAI from 'openai'
+import { getRequestyClient } from '../apiClients'
+import { useSettingsStore } from '../../stores/settingsStore'
+import { listModelsViaMainProcess } from './modelDiscovery'
+import { createOpenAICompatibleResponse } from './openAICompatible'
+import { REQUESTY_OPENAI_BASE_URL } from './providerCatalog'
+
+function isChatModel(model: any): boolean {
+ return !model.api || model.api === 'chat'
+}
+
+async function listRequestyManagedModels(
+ apiKey: string
+): Promise {
+ try {
+ const response = await window.httpAPI.request({
+ url: `${REQUESTY_OPENAI_BASE_URL}/models/managed`,
+ method: 'GET',
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ },
+ timeout: 10 * 1000,
+ })
+ if (!response.success || (response.status && response.status >= 400)) {
+ return []
+ }
+ const models = Array.isArray(response.data?.data) ? response.data.data : []
+ return models
+ .filter((model: any) => typeof model?.id === 'string')
+ .map((model: any) => ({ ...model, object: model.object || 'model' }))
+ .sort((a: OpenAI.Models.Model, b: OpenAI.Models.Model) =>
+ a.id.localeCompare(b.id)
+ )
+ } catch {
+ return []
+ }
+}
+
+export async function listRequestyModelsForConfig(
+ apiKey: string
+): Promise {
+ // The authenticated catalog request also validates the key.
+ const catalogModels = await listModelsViaMainProcess({
+ apiKey,
+ baseURL: REQUESTY_OPENAI_BASE_URL,
+ providerName: 'Requesty',
+ })
+ const managedModels = await listRequestyManagedModels(apiKey)
+
+ // Managed policies come first, followed by the full model catalog.
+ const seen = new Set()
+ return [...managedModels, ...catalogModels].filter(model => {
+ if (!isChatModel(model) || seen.has(model.id)) {
+ return false
+ }
+ seen.add(model.id)
+ return true
+ })
+}
+
+export function listRequestyModels(): Promise {
+ const settings = useSettingsStore().config
+ return listRequestyModelsForConfig(settings.VITE_REQUESTY_API_KEY || '')
+}
+
+export function createRequestyResponse(
+ input: OpenAI.Responses.Request.InputItemLike[],
+ _previousResponseId: string | null,
+ stream: boolean = false,
+ customInstructions?: string,
+ signal?: AbortSignal
+): Promise {
+ return createOpenAICompatibleResponse(
+ 'requesty',
+ getRequestyClient,
+ input,
+ stream,
+ customInstructions,
+ signal
+ )
+}
diff --git a/src/services/llmProviders/streamAdapters.ts b/src/services/llmProviders/streamAdapters.ts
index bac13042..b9b441ef 100644
--- a/src/services/llmProviders/streamAdapters.ts
+++ b/src/services/llmProviders/streamAdapters.ts
@@ -3,7 +3,13 @@ import { isExpectedAbortError } from '../../utils/isAbortError'
export async function* convertLocalLLMStreamToResponsesFormat(
stream: any,
provider:
- 'ollama' | 'lm-studio' | 'zai' | 'minimax' | 'deepseek' | 'api-route'
+ | 'ollama'
+ | 'lm-studio'
+ | 'zai'
+ | 'minimax'
+ | 'deepseek'
+ | 'api-route'
+ | 'requesty'
) {
let responseId = `${provider}-${Date.now()}`
let messageItemId = `message-${Date.now()}`
@@ -207,7 +213,7 @@ export async function* convertLocalLLMStreamToResponsesFormat(
export async function* convertChatCompletionToResponsesFormat(
completion: any,
- provider: 'zai' | 'minimax' | 'deepseek' | 'api-route'
+ provider: 'zai' | 'minimax' | 'deepseek' | 'api-route' | 'requesty'
) {
const responseId = completion?.id || `${provider}-${Date.now()}`
const messageItemId = `message-${Date.now()}`
diff --git a/src/stores/settingsStore.ts b/src/stores/settingsStore.ts
index 2eb8c4c1..135a4717 100644
--- a/src/stores/settingsStore.ts
+++ b/src/stores/settingsStore.ts
@@ -35,6 +35,7 @@ export interface AliceSettings {
VITE_MINIMAX_API_KEY: string
VITE_DEEPSEEK_API_KEY: string
VITE_API_ROUTE_API_KEY: string
+ VITE_REQUESTY_API_KEY: string
VITE_GROQ_API_KEY: string
VITE_GOOGLE_API_KEY: string
sttProvider: 'openai' | 'groq' | 'google' | 'local'
@@ -130,6 +131,9 @@ function hasMinimumConfigForOnboarding(config: AliceSettings): boolean {
if (config.VITE_API_ROUTE_API_KEY?.trim()) {
return true
}
+ if (config.VITE_REQUESTY_API_KEY?.trim()) {
+ return true
+ }
if (config.codexAuthConnected) {
return true
}
@@ -151,6 +155,7 @@ const defaultSettings: AliceSettings = {
VITE_MINIMAX_API_KEY: '',
VITE_DEEPSEEK_API_KEY: '',
VITE_API_ROUTE_API_KEY: '',
+ VITE_REQUESTY_API_KEY: '',
VITE_GROQ_API_KEY: '',
VITE_GOOGLE_API_KEY: '',
sttProvider: 'openai',
@@ -226,6 +231,7 @@ const settingKeyToLabelMap: Record = {
VITE_MINIMAX_API_KEY: 'MiniMax API Key',
VITE_DEEPSEEK_API_KEY: 'DeepSeek API Key',
VITE_API_ROUTE_API_KEY: 'API Route API Key',
+ VITE_REQUESTY_API_KEY: 'Requesty API Key',
VITE_GROQ_API_KEY: 'Groq API Key (STT)',
VITE_GOOGLE_API_KEY: 'Google API Key',
sttProvider: 'Speech-to-Text Provider',
@@ -294,6 +300,7 @@ const ESSENTIAL_CORE_API_KEYS: (keyof AliceSettings)[] = [
'VITE_MINIMAX_API_KEY',
'VITE_DEEPSEEK_API_KEY',
'VITE_API_ROUTE_API_KEY',
+ 'VITE_REQUESTY_API_KEY',
]
function requiresOpenAIKey(config: AliceSettings): boolean {
@@ -369,6 +376,7 @@ export const useSettingsStore = defineStore('settings', () => {
'minimax',
'deepseek',
'api-route',
+ 'requesty',
'codex',
] as const
if (!validAIProviders.includes(validated.aiProvider as any)) {
@@ -458,6 +466,8 @@ export const useSettingsStore = defineStore('settings', () => {
essentialKeys.push('VITE_DEEPSEEK_API_KEY', 'deepseekBaseUrl')
} else if (settings.value.aiProvider === 'api-route') {
essentialKeys.push('VITE_API_ROUTE_API_KEY')
+ } else if (settings.value.aiProvider === 'requesty') {
+ essentialKeys.push('VITE_REQUESTY_API_KEY')
} else if (settings.value.aiProvider === 'codex') {
essentialKeys.push('codexAuthConnected')
} else if (settings.value.aiProvider === 'ollama') {
@@ -532,6 +542,10 @@ export const useSettingsStore = defineStore('settings', () => {
return !!settings.value.VITE_API_ROUTE_API_KEY?.trim()
}
+ if (settings.value.aiProvider === 'requesty') {
+ return !!settings.value.VITE_REQUESTY_API_KEY?.trim()
+ }
+
if (settings.value.aiProvider === 'ollama') {
return !!settings.value.ollamaBaseUrl?.trim()
}
@@ -790,6 +804,10 @@ export const useSettingsStore = defineStore('settings', () => {
PROVIDER_CONFIGS['api-route'].defaultModel
settings.value.SUMMARIZATION_MODEL =
PROVIDER_CONFIGS['api-route'].defaultModel
+ } else if (settings.value.aiProvider === 'requesty') {
+ settings.value.assistantModel = PROVIDER_CONFIGS.requesty.defaultModel
+ settings.value.SUMMARIZATION_MODEL =
+ PROVIDER_CONFIGS.requesty.defaultModel
} else if (settings.value.aiProvider === 'codex') {
settings.value.assistantModel = PROVIDER_CONFIGS.codex.defaultModel
settings.value.SUMMARIZATION_MODEL = PROVIDER_CONFIGS.codex.defaultModel
@@ -835,6 +853,7 @@ export const useSettingsStore = defineStore('settings', () => {
key === 'VITE_MINIMAX_API_KEY' ||
key === 'VITE_DEEPSEEK_API_KEY' ||
key === 'VITE_API_ROUTE_API_KEY' ||
+ key === 'VITE_REQUESTY_API_KEY' ||
key === 'ollamaBaseUrl' ||
key === 'lmStudioBaseUrl' ||
key === 'zaiBaseUrl' ||
@@ -881,6 +900,7 @@ export const useSettingsStore = defineStore('settings', () => {
VITE_MINIMAX_API_KEY: settings.value.VITE_MINIMAX_API_KEY,
VITE_DEEPSEEK_API_KEY: settings.value.VITE_DEEPSEEK_API_KEY,
VITE_API_ROUTE_API_KEY: settings.value.VITE_API_ROUTE_API_KEY,
+ VITE_REQUESTY_API_KEY: settings.value.VITE_REQUESTY_API_KEY,
VITE_GROQ_API_KEY: settings.value.VITE_GROQ_API_KEY,
VITE_GOOGLE_API_KEY: settings.value.VITE_GOOGLE_API_KEY,
sttProvider: settings.value.sttProvider,
@@ -1034,6 +1054,13 @@ export const useSettingsStore = defineStore('settings', () => {
isSaving.value = false
return
}
+ } else if (currentConfigForTest.aiProvider === 'requesty') {
+ if (!currentConfigForTest.VITE_REQUESTY_API_KEY?.trim()) {
+ error.value = `Essential setting '${settingKeyToLabelMap.VITE_REQUESTY_API_KEY}' is missing.`
+ generalStore.statusMessage = 'Requesty API Key is required.'
+ isSaving.value = false
+ return
+ }
} else if (currentConfigForTest.aiProvider === 'codex') {
if (!currentConfigForTest.codexAuthConnected) {
error.value = `Essential setting '${settingKeyToLabelMap.codexAuthConnected}' is missing.`
@@ -1161,6 +1188,7 @@ export const useSettingsStore = defineStore('settings', () => {
VITE_MINIMAX_API_KEY?: string
VITE_DEEPSEEK_API_KEY?: string
VITE_API_ROUTE_API_KEY?: string
+ VITE_REQUESTY_API_KEY?: string
sttProvider: 'openai' | 'groq' | 'google' | 'local'
ttsProvider?: 'openai' | 'google' | 'local'
embeddingProvider?: 'openai' | 'local'
@@ -1187,6 +1215,8 @@ export const useSettingsStore = defineStore('settings', () => {
onboardingData.VITE_DEEPSEEK_API_KEY || ''
settings.value.VITE_API_ROUTE_API_KEY =
onboardingData.VITE_API_ROUTE_API_KEY || ''
+ settings.value.VITE_REQUESTY_API_KEY =
+ onboardingData.VITE_REQUESTY_API_KEY || ''
settings.value.sttProvider = onboardingData.sttProvider
settings.value.aiProvider = onboardingData.aiProvider
settings.value.VITE_GROQ_API_KEY = onboardingData.VITE_GROQ_API_KEY