-
Notifications
You must be signed in to change notification settings - Fork 3.8k
feat(providers): add OrcaRouter as a named provider #6978
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: staging
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| import { createLogger } from '@sim/logger' | ||
| import { getErrorMessage } from '@sim/utils/errors' | ||
| import { type NextRequest, NextResponse } from 'next/server' | ||
| import { providerModelsResponseSchema } from '@/lib/api/contracts/providers' | ||
| import { withRouteHandler } from '@/lib/core/utils/with-route-handler' | ||
| import { filterBlacklistedModels, isProviderBlacklisted } from '@/providers/utils' | ||
|
|
||
| const logger = createLogger('OrcaRouterModelsAPI') | ||
|
|
||
| interface OrcaRouterModel { | ||
| id: string | ||
| supported_endpoint_types?: string[] | ||
| } | ||
|
|
||
| interface OrcaRouterResponse { | ||
| data: OrcaRouterModel[] | ||
| } | ||
|
|
||
| /** | ||
| * Enumerates the public OrcaRouter model catalog. OrcaRouter is an | ||
| * OpenAI-compatible gateway that routes across many upstream providers, so its | ||
| * `/v1/models` endpoint (like OpenRouter's) is public — no key needed to list. | ||
| * Chat models are filtered to those exposing chat completions. | ||
| */ | ||
| export const GET = withRouteHandler(async (_request: NextRequest) => { | ||
| if (isProviderBlacklisted('orcarouter')) { | ||
| logger.info('OrcaRouter provider is blacklisted, returning empty models') | ||
| return NextResponse.json({ models: [], modelInfo: {} }) | ||
| } | ||
|
|
||
| try { | ||
| const response = await fetch('https://api.orcarouter.ai/v1/models', { | ||
| headers: { 'Content-Type': 'application/json' }, | ||
| next: { revalidate: 300 }, | ||
| }) | ||
|
|
||
| if (!response.ok) { | ||
| logger.warn('Failed to fetch OrcaRouter models', { | ||
| status: response.status, | ||
| statusText: response.statusText, | ||
| }) | ||
| return NextResponse.json({ models: [], modelInfo: {} }) | ||
| } | ||
|
|
||
| const data: OrcaRouterResponse = await response.json() | ||
|
|
||
| const allModels: string[] = [] | ||
| for (const model of data.data ?? []) { | ||
| const endpoints = model.supported_endpoint_types ?? [] | ||
| // OrcaRouter exposes chat completions through the OpenAI-compatible | ||
| // endpoint (`openai`); models without an endpoint list default to included. | ||
| if (endpoints.length > 0 && !endpoints.includes('openai')) continue | ||
| allModels.push(`orcarouter/${model.id}`) | ||
| } | ||
|
|
||
| const uniqueModels = Array.from(new Set(allModels)) | ||
| const models = filterBlacklistedModels(uniqueModels) | ||
|
|
||
| logger.info('Successfully fetched OrcaRouter models', { | ||
| count: models.length, | ||
| filtered: uniqueModels.length - models.length, | ||
| }) | ||
|
|
||
| return NextResponse.json(providerModelsResponseSchema.parse({ models, modelInfo: {} })) | ||
| } catch (error) { | ||
| logger.error('Error fetching OrcaRouter models', { | ||
| error: getErrorMessage(error, 'Unknown error'), | ||
| }) | ||
| return NextResponse.json({ models: [], modelInfo: {} }) | ||
| } | ||
| }) |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -322,6 +322,27 @@ export async function getApiKeyWithBYOK( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { apiKey: PROVIDER_PLACEHOLDER_KEY, isBYOK: false } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const isOrcaRouterModel = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| provider === 'orcarouter' || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useProvidersStore.getState().providers.orcarouter.models.includes(model) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isOrcaRouterModel) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (workspaceId) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const byokResult = await getBYOKKey(workspaceId, 'orcarouter') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (byokResult) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| logger.info('Using BYOK key for OrcaRouter', { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| model, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| workspaceId, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| scope: byokResult.scope, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return byokResult | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (userProvidedKey) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { apiKey: userProvidedKey, isBYOK: false } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error(`API key is required for OrcaRouter ${model}`) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+325
to
+344
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a self-hosted deployment configures
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Env API key never usedMedium Severity
Additional Locations (2)Reviewed by Cursor Bugbot for commit 3cafb60. Configure here. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (provider === 'azure-openai') { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { apiKey: userProvidedKey || env.AZURE_OPENAI_API_KEY || '', isBYOK: false } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,6 +26,7 @@ export type AttachmentProvider = | |
| | 'google' | ||
| | 'bedrock' | ||
| | 'openrouter' | ||
| | 'orcarouter' | ||
| | 'mistral' | ||
| | 'groq' | ||
| | 'fireworks' | ||
|
|
@@ -164,6 +165,7 @@ const PROVIDER_SUPPORTED_LABELS: Record<AttachmentProvider, string> = { | |
| google: 'images, audio, video, PDFs, and text documents through Gemini inlineData', | ||
| bedrock: 'Bedrock Converse image, document, and video content blocks', | ||
| openrouter: 'images and PDFs through OpenRouter multimodal message parts', | ||
| orcarouter: 'images and PDFs through OrcaRouter multimodal message parts', | ||
| mistral: 'images through image_url message parts', | ||
| groq: 'images through image_url message parts on multimodal models', | ||
| fireworks: 'images through image_url message parts on vision models', | ||
|
|
@@ -188,6 +190,7 @@ export function getAttachmentProvider(providerId: ProviderId | string): Attachme | |
| if (providerId === 'google' || providerId === 'vertex') return 'google' | ||
| if (providerId === 'bedrock') return 'bedrock' | ||
| if (providerId === 'openrouter') return 'openrouter' | ||
| if (providerId === 'orcarouter') return 'orcarouter' | ||
| if (providerId === 'mistral') return 'mistral' | ||
| if (providerId === 'groq') return 'groq' | ||
| if (providerId === 'fireworks') return 'fireworks' | ||
|
|
@@ -306,7 +309,8 @@ export function isProviderAttachmentFilenameModelBound( | |
| providerId === 'openai' || | ||
| provider === 'anthropic' || | ||
| provider === 'bedrock' || | ||
| provider === 'openrouter' | ||
| provider === 'openrouter' || | ||
| provider === 'orcarouter' | ||
| ) | ||
| } | ||
|
|
||
|
|
@@ -401,6 +405,7 @@ function isMimeTypeSupportedByProvider( | |
| (contentType === 'video' && BEDROCK_VIDEO_FORMATS.has(extension)) | ||
| ) | ||
| case 'openrouter': | ||
| case 'orcarouter': | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. PDF attachments formatted incorrectlyHigh Severity
Additional Locations (2)Reviewed by Cursor Bugbot for commit 3cafb60. Configure here. |
||
| return isImageMimeType(mimeType) || mimeType === PDF_MIME_TYPE | ||
| case 'mistral': | ||
| case 'groq': | ||
|
|
||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot mock missing provider
Medium Severity
getModelOptionsnow readsproviders.orcarouter.models, but the copilot metadata fallback mock still omitsorcarouter. When that mock replaces the store, option resolution throws, is caught, and Agent model options come back empty for copilot.Reviewed by Cursor Bugbot for commit 3cafb60. Configure here.