From 02489fd32c4d465b4095babc633ca6800f9c7d2e Mon Sep 17 00:00:00 2001 From: XiaoHuo888-hue Date: Sat, 22 Aug 2026 00:23:40 +0000 Subject: [PATCH] feat(agent): add OrcaRouter as a named AI agent provider Add a named-provider registry for the AI agent chat-completions endpoint and register OrcaRouter (https://api.orcarouter.ai/v1) alongside OpenAI. The agent handler now accepts provider: "orcarouter" and routes requests through the OrcaRouter endpoint. Co-Authored-By: Claude Signed-off-by: XiaoHuo888-hue --- Parse-Dashboard/app.js | 43 ++++++++++++++++++++++----------- README.md | 43 +++++++++++++++++++++++++++++++-- src/lib/tests/AgentAuth.test.js | 21 ++++++++++++++++ 3 files changed, 91 insertions(+), 16 deletions(-) diff --git a/Parse-Dashboard/app.js b/Parse-Dashboard/app.js index 31ea47637c..36842f17de 100644 --- a/Parse-Dashboard/app.js +++ b/Parse-Dashboard/app.js @@ -63,6 +63,19 @@ module.exports = function(config, options) { options = options || {}; const app = express(); + // Named AI agent providers and their chat-completions endpoints. + // Each provider is a first-class integration with its own base URL. + const AGENT_PROVIDERS = { + openai: { + url: 'https://api.openai.com/v1/chat/completions', + label: 'OpenAI', + }, + orcarouter: { + url: 'https://api.orcarouter.ai/v1/chat/completions', + label: 'OrcaRouter', + }, + }; + // Parse JSON and URL-encoded request bodies app.use(express.json()); app.use(express.urlencoded({ extended: true })); @@ -337,8 +350,8 @@ module.exports = function(config, options) { return res.status(400).json({ error: 'Please replace the placeholder API key with your actual API key' }); } - // Only support OpenAI for now - if (provider.toLowerCase() !== 'openai') { + // Only support the named providers registered in AGENT_PROVIDERS + if (!AGENT_PROVIDERS[provider.toLowerCase()]) { return res.status(400).json({ error: `Provider "${provider}" is not supported yet` }); } @@ -357,8 +370,8 @@ module.exports = function(config, options) { // preventing privilege escalation via self-authorized permissions in the request body const effectivePermissions = isReadOnly ? {} : (permissions || {}); - // Make request to OpenAI API with app context and conversation history - const response = await makeOpenAIRequest(message, model, apiKey, appContext, conversationHistory, operationLog, effectivePermissions); + // Make request to the configured provider with app context and conversation history + const response = await makeProviderRequest(message, model, apiKey, provider, appContext, conversationHistory, operationLog, effectivePermissions); // Update conversation history with user message and AI response conversationHistory.push( @@ -912,12 +925,13 @@ module.exports = function(config, options) { } /** - * Make a request to OpenAI API + * Make a request to the configured AI provider's chat-completions API */ - async function makeOpenAIRequest(userMessage, model, apiKey, appContext = null, conversationHistory = [], operationLog = [], permissions = {}) { + async function makeProviderRequest(userMessage, model, apiKey, provider, appContext = null, conversationHistory = [], operationLog = [], permissions = {}) { const fetch = (await import('node-fetch')).default; - const url = 'https://api.openai.com/v1/chat/completions'; + const providerConfig = AGENT_PROVIDERS[provider.toLowerCase()]; + const url = providerConfig ? providerConfig.url : 'https://api.openai.com/v1/chat/completions'; const appInfo = appContext ? `\n\nContext: You are currently helping with the Parse Server app "${appContext.appName}" (ID: ${appContext.appId}) at ${appContext.serverURL}.` : @@ -1044,27 +1058,28 @@ You have direct access to the Parse database through function calls, so you can }); if (!response.ok) { + const providerLabel = providerConfig ? providerConfig.label : 'AI provider'; if (response.status === 401) { - throw new Error('Invalid API key. Please check your OpenAI API key configuration.'); + throw new Error(`Invalid API key. Please check your ${providerLabel} API key configuration.`); } else if (response.status === 429) { throw new Error('Rate limit exceeded. Please try again in a moment.'); } else if (response.status === 403) { throw new Error('Access forbidden. Please check your API key permissions.'); } else if (response.status >= 500) { - throw new Error('OpenAI service is temporarily unavailable. Please try again later.'); + throw new Error(`${providerLabel} service is temporarily unavailable. Please try again later.`); } const errorData = await response.json().catch(() => ({})); const errorMessage = (errorData && typeof errorData === 'object' && 'error' in errorData && errorData.error && typeof errorData.error === 'object' && 'message' in errorData.error) ? errorData.error.message : `HTTP ${response.status}: ${response.statusText}`; - throw new Error(`OpenAI API error: ${errorMessage}`); + throw new Error(`${providerLabel} API error: ${errorMessage}`); } const data = await response.json(); if (!data || typeof data !== 'object' || !('choices' in data) || !Array.isArray(data.choices) || data.choices.length === 0) { - throw new Error('No response received from OpenAI API'); + throw new Error('No response received from the AI provider'); } const choice = data.choices[0]; @@ -1140,19 +1155,19 @@ You have direct access to the Parse database through function calls, so you can const followUpData = await followUpResponse.json(); if (!followUpData || typeof followUpData !== 'object' || !('choices' in followUpData) || !Array.isArray(followUpData.choices) || followUpData.choices.length === 0) { - throw new Error('No follow-up response received from OpenAI API'); + throw new Error('No follow-up response received from the AI provider'); } const followUpContent = followUpData.choices[0].message.content; if (!followUpContent) { - console.warn('OpenAI returned null content in follow-up response, using fallback message'); + console.warn('AI provider returned null content in follow-up response, using fallback message'); } return followUpContent || 'Done.'; } const content = responseMessage.content; if (!content) { - console.warn('OpenAI returned null content in initial response, using fallback message'); + console.warn('AI provider returned null content in initial response, using fallback message'); } return content || 'Done.'; } diff --git a/README.md b/README.md index 7748387290..901d56beed 100644 --- a/README.md +++ b/README.md @@ -1789,6 +1789,12 @@ To configure the AI agent for your dashboard, you need to add the `agent` config "model": "gpt-4.1", "apiKey": "YOUR_OPENAI_API_KEY" }, + { + "name": "OrcaRouter (auto)", + "provider": "orcarouter", + "model": "orcarouter/auto", + "apiKey": "YOUR_ORCAROUTER_API_KEY" + }, ] } } @@ -1799,7 +1805,7 @@ To configure the AI agent for your dashboard, you need to add the `agent` config | `agent` | Object | Yes | The AI agent configuration object. When using the environment variable, provide the complete agent configuration as a JSON string. | | `agent.models` | Array | Yes | Array of AI model configurations available to the agent. | | `agent.models[*].name` | String | Yes | The display name for the model (e.g., `ChatGPT 4.1`). | -| `agent.models[*].provider` | String | Yes | The AI provider identifier (e.g., "openai"). | +| `agent.models[*].provider` | String | Yes | The AI provider identifier (e.g., "openai" or "orcarouter"). | | `agent.models[*].model` | String | Yes | The specific model name from the provider (e.g., `gpt-4.1`). | | `agent.models[*].apiKey` | String | Yes | The API key for authenticating with the AI provider. | @@ -1808,7 +1814,7 @@ The agent will use the configured models to process natural language commands an ### Providers > [!Note] -> Currently, only OpenAI models are supported. Support for additional providers may be added in future releases. +> The following AI providers are supported. Additional providers may be added in future releases. #### OpenAI @@ -1842,6 +1848,39 @@ To get an OpenAI API key for use with the AI agent: > [!Important] > Keep your API key secure and never commit it to version control. Consider using environment variables or secure configuration management for production deployments. +#### OrcaRouter + +[OrcaRouter](https://www.orcarouter.ai) is a gateway that provides access to a wide range of frontier models through a single OpenAI-compatible API. It also runs gateway-level, zero-trust security for AI agents on the same endpoint — screening every prompt/response and governing every tool call on a default-deny basis, with no application code changes. + +To use OrcaRouter with the AI agent: + +1. **Create an account**: Sign up at [orcarouter.ai](https://www.orcarouter.ai) and add credits. + +2. **Generate an API key**: Create an API key in the OrcaRouter dashboard. Keys are prefixed with `sk-orca-`. + +3. **Configure the dashboard**: Add an `orcarouter` model to your Parse Dashboard configuration: + +```json +{ + "agent": { + "models": [ + { + "name": "OrcaRouter (auto)", + "provider": "orcarouter", + "model": "orcarouter/auto", + "apiKey": "YOUR_ORCAROUTER_API_KEY" + } + ] + } +} +``` + + - `provider` must be `orcarouter`. + - `model` can be any model routed by OrcaRouter, e.g. `orcarouter/auto` (automatic routing), `deepseek/deepseek-v4-pro`, or `anthropic/claude-haiku-4.5`. + +> [!Important] +> Keep your API key secure and never commit it to version control. Consider using environment variables or secure configuration management for production deployments. + ## Views ▶️ *Core > Views* diff --git a/src/lib/tests/AgentAuth.test.js b/src/lib/tests/AgentAuth.test.js index 92a322c918..7636a66fd2 100644 --- a/src/lib/tests/AgentAuth.test.js +++ b/src/lib/tests/AgentAuth.test.js @@ -184,6 +184,12 @@ describe('Agent endpoint security', () => { model: 'gpt-4', apiKey: 'fake-api-key-for-testing', }, + { + name: 'orcarouter-test-model', + provider: 'orcarouter', + model: 'orcarouter/auto', + apiKey: 'fake-api-key-for-testing', + }, ], }, }; @@ -280,6 +286,21 @@ describe('Agent endpoint security', () => { expect(res.status).not.toBe(403); }); + it('accepts an orcarouter provider model as a supported provider', async () => { + const res = await makeRequest(port, { + method: 'POST', + path: '/apps/TestApp/agent', + body: agentBody({ modelName: 'orcarouter-test-model' }), + cookie: adminCookie, + headers: { 'X-CSRF-Token': CSRF_TOKEN }, + }); + // 400 would mean the provider was rejected as unsupported. A 500 means auth passed + // and the request was routed to the OrcaRouter endpoint (failing on the fake API key). + expect(res.status).not.toBe(400); + expect(res.status).not.toBe(401); + expect(res.status).not.toBe(403); + }); + it('returns 403 when authenticated admin sends request without CSRF token', async () => { const res = await makeRequest(port, { method: 'POST',