diff --git a/src/data/providers.json b/src/data/providers.json index fd801fdc5..62a20efb6 100644 --- a/src/data/providers.json +++ b/src/data/providers.json @@ -212,6 +212,13 @@ "description": "OpenRouter is an innovative platform that provides unified access to multiple large language models through a single interface. It enables businesses and developers to integrate diverse AI capabilities efficiently while focusing on scalability and cost-effectiveness in their applications.", "base_url": "https://openrouter.ai/api" }, + { + "id": "requesty", + "name": "Requesty", + "object": "provider", + "description": "Requesty is an OpenAI-compatible LLM router that provides unified access to models from many providers through a single endpoint, using provider/model naming. It supports chat completions with streaming, tool calling, and reasoning outputs.", + "base_url": "https://router.requesty.ai/v1" + }, { "id": "ovhcloud", "name": "OVHcloud AI Endpoints", diff --git a/src/globals.ts b/src/globals.ts index 4d6e327e4..a752b73ac 100644 --- a/src/globals.ts +++ b/src/globals.ts @@ -68,6 +68,7 @@ export const FIREWORKS_AI: string = 'fireworks-ai'; export const WORKERS_AI: string = 'workers-ai'; export const MOONSHOT: string = 'moonshot'; export const OPENROUTER: string = 'openrouter'; +export const REQUESTY: string = 'requesty'; export const LINGYI: string = 'lingyi'; export const ZHIPU: string = 'zhipu'; export const NOVITA_AI: string = 'novita-ai'; @@ -141,6 +142,7 @@ export const VALID_PROVIDERS = [ WORKERS_AI, MOONSHOT, OPENROUTER, + REQUESTY, LINGYI, ZHIPU, NOVITA_AI, diff --git a/src/providers/index.ts b/src/providers/index.ts index 2cd5355f8..f332b06bb 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -26,6 +26,7 @@ import WorkersAiConfig from './workers-ai'; import RekaAIConfig from './reka-ai'; import MoonshotConfig from './moonshot'; import OpenrouterConfig from './openrouter'; +import RequestyConfig from './requesty'; import LingYiConfig from './lingyi'; import ZhipuConfig from './zhipu'; import NovitaAIConfig from './novita-ai'; @@ -103,6 +104,7 @@ const Providers: { [key: string]: ProviderConfigs } = { 'reka-ai': RekaAIConfig, moonshot: MoonshotConfig, openrouter: OpenrouterConfig, + requesty: RequestyConfig, lingyi: LingYiConfig, zhipu: ZhipuConfig, 'novita-ai': NovitaAIConfig, diff --git a/src/providers/requesty/api.ts b/src/providers/requesty/api.ts new file mode 100644 index 000000000..745d321a2 --- /dev/null +++ b/src/providers/requesty/api.ts @@ -0,0 +1,23 @@ +import { POWERED_BY } from '../../globals'; +import { ProviderAPIConfig } from '../types'; + +const RequestyAPIConfig: ProviderAPIConfig = { + getBaseURL: () => 'https://router.requesty.ai', + headers: ({ providerOptions }) => { + return { + Authorization: `Bearer ${providerOptions.apiKey}`, // https://app.requesty.ai/api-keys + 'HTTP-Referer': 'https://portkey.ai/', + 'X-Title': POWERED_BY, + }; + }, + getEndpoint: ({ fn }) => { + switch (fn) { + case 'chatComplete': + return '/v1/chat/completions'; + default: + return ''; + } + }, +}; + +export default RequestyAPIConfig; diff --git a/src/providers/requesty/chatComplete.ts b/src/providers/requesty/chatComplete.ts new file mode 100644 index 000000000..8aa9ebfb9 --- /dev/null +++ b/src/providers/requesty/chatComplete.ts @@ -0,0 +1,289 @@ +import { REQUESTY } from '../../globals'; +import { Message, Params } from '../../types/requestBody'; +import { + ChatChoice, + ChatCompletionResponse, + ErrorResponse, + ProviderConfig, +} from '../types'; +import { + generateErrorResponse, + generateInvalidProviderResponseError, +} from '../utils'; +import { transformReasoningParams, transformUsageOptions } from './utils'; + +export const RequestyChatCompleteConfig: ProviderConfig = { + model: { + param: 'model', + required: true, + default: 'openai/gpt-4o-mini', + }, + messages: { + param: 'messages', + default: '', + transform: (params: Params) => { + return params.messages?.map((message) => { + if (message.role === 'developer') return { ...message, role: 'system' }; + return message; + }); + }, + }, + max_tokens: { + param: 'max_tokens', + default: 100, + min: 0, + }, + max_completion_tokens: { + param: 'max_tokens', + default: 100, + min: 0, + }, + temperature: { + param: 'temperature', + default: 1, + min: 0, + max: 2, + }, + modalities: { + param: 'modalities', + }, + reasoning: { + param: 'reasoning', + transform: (params: Params) => { + return transformReasoningParams(params); + }, + }, + reasoning_effort: { + param: 'reasoning', + transform: (params: Params) => { + return transformReasoningParams(params); + }, + }, + top_p: { + param: 'top_p', + default: 1, + min: 0, + max: 1, + }, + tools: { + param: 'tools', + }, + tool_choice: { + param: 'tool_choice', + }, + usage: { + param: 'usage', + transform: (params: Params) => { + return transformUsageOptions(params); + }, + }, + stream: { + param: 'stream', + default: false, + }, + stream_options: { + param: 'usage', + transform: (params: Params) => { + return transformUsageOptions(params); + }, + }, + response_format: { + param: 'response_format', + }, +}; + +interface RequestyUsageDetails { + prompt_tokens: number; + completion_tokens: number; + total_tokens: number; + prompt_tokens_details?: { + cached_tokens: number; + audio_tokens: number; + }; + completion_tokens_details?: { + reasoning_tokens: number; + audio_tokens: number; + accepted_prediction_tokens: number; + rejected_prediction_tokens: number; + }; + cost?: number; +} + +interface RequestyChatCompleteResponse extends ChatCompletionResponse { + id: string; + object: string; + created: number; + model: string; + choices: (ChatChoice & { + message: Message & { + reasoning: string; + reasoning_details?: any[]; + }; + })[]; + usage: RequestyUsageDetails; +} + +export interface RequestyErrorResponse { + object: string; + message: string; + type: string; + param: string | null; + code: string; +} + +interface RequestyStreamChunk { + id: string; + object: string; + created: number; + model: string; + usage?: RequestyUsageDetails; + choices: { + delta: { + role?: string | null; + content?: string; + reasoning?: string; + }; + index: number; + finish_reason: string | null; + }[]; +} + +export const RequestyChatCompleteResponseTransform: ( + response: RequestyChatCompleteResponse | RequestyErrorResponse, + responseStatus: number, + _responseHeaders: Headers, + strictOpenAiCompliance: boolean, + _gatewayRequestUrl: string, + _gatewayRequest: Params +) => ChatCompletionResponse | ErrorResponse = ( + response, + responseStatus, + _responseHeaders, + strictOpenAiCompliance, + _gatewayRequestUrl, + _gatewayRequest +) => { + if ('message' in response && responseStatus !== 200) { + return generateErrorResponse( + { + message: response.message, + type: response.type, + param: response.param, + code: response.code, + }, + REQUESTY + ); + } + + if ('choices' in response) { + return { + id: response.id, + object: response.object, + created: response.created, + model: response.model, + provider: REQUESTY, + choices: response.choices.map((c) => { + const content_blocks = []; + + if (!strictOpenAiCompliance) { + if (c.message.reasoning) { + content_blocks.push({ + type: 'thinking', + thinking: c.message.reasoning, + ...(c.message.reasoning_details && { + reasoning_details: c.message.reasoning_details, + }), + }); + } + + content_blocks.push({ + type: 'text', + text: c.message.content, + }); + } + + return { + index: c.index, + message: { + role: c.message.role, + content: c.message.content, + ...(content_blocks.length && { content_blocks }), + ...(c.message.tool_calls && { tool_calls: c.message.tool_calls }), + ...(c.message.reasoning_details && { + reasoning_details: c.message.reasoning_details, + }), + }, + finish_reason: c.finish_reason, + }; + }), + usage: response.usage, + }; + } + + return generateInvalidProviderResponseError(response, REQUESTY); +}; + +export const RequestyChatCompleteStreamChunkTransform: ( + response: string, + fallbackId: string, + _streamState: Record, + _strictOpenAiCompliance: boolean, + gatewayRequest: Params +) => string = ( + responseChunk, + fallbackId, + _streamState, + strictOpenAiCompliance, + gatewayRequest +) => { + let chunk = responseChunk.trim(); + chunk = chunk.replace(/^data: /, ''); + chunk = chunk.trim(); + if (chunk === '[DONE]') { + return `data: ${chunk}\n\n`; + } + const parsedChunk: RequestyStreamChunk = JSON.parse(chunk); + + const content_blocks = []; + if (!strictOpenAiCompliance) { + // add the reasoning first + if (parsedChunk.choices?.[0]?.delta?.reasoning) { + content_blocks.push({ + index: parsedChunk.choices?.[0]?.index, + delta: { + thinking: parsedChunk.choices?.[0]?.delta?.reasoning, + }, + }); + } + // then add the content + if (parsedChunk.choices?.[0]?.delta?.content) { + content_blocks.push({ + index: parsedChunk.choices?.[0]?.index, + delta: { + text: parsedChunk.choices?.[0]?.delta?.content, + }, + }); + } + } + + return ( + `data: ${JSON.stringify({ + id: parsedChunk.id, + object: parsedChunk.object, + created: parsedChunk.created, + model: parsedChunk.model, + provider: REQUESTY, + choices: [ + { + index: parsedChunk.choices?.[0]?.index, + delta: { + ...parsedChunk.choices?.[0]?.delta, + ...(content_blocks.length && { content_blocks }), + }, + finish_reason: parsedChunk.choices?.[0]?.finish_reason, + }, + ], + ...(parsedChunk.usage && { usage: parsedChunk.usage }), + })}` + '\n\n' + ); +}; diff --git a/src/providers/requesty/index.ts b/src/providers/requesty/index.ts new file mode 100644 index 000000000..cfa21405c --- /dev/null +++ b/src/providers/requesty/index.ts @@ -0,0 +1,18 @@ +import { ProviderConfigs } from '../types'; +import RequestyAPIConfig from './api'; +import { + RequestyChatCompleteConfig, + RequestyChatCompleteResponseTransform, + RequestyChatCompleteStreamChunkTransform, +} from './chatComplete'; + +const RequestyConfig: ProviderConfigs = { + chatComplete: RequestyChatCompleteConfig, + api: RequestyAPIConfig, + responseTransforms: { + chatComplete: RequestyChatCompleteResponseTransform, + 'stream-chatComplete': RequestyChatCompleteStreamChunkTransform, + }, +}; + +export default RequestyConfig; diff --git a/src/providers/requesty/utils.ts b/src/providers/requesty/utils.ts new file mode 100644 index 000000000..c2077bd7a --- /dev/null +++ b/src/providers/requesty/utils.ts @@ -0,0 +1,35 @@ +import { Params } from '../../types/requestBody'; + +interface RequestyUsageParam { + include?: boolean; +} + +interface RequestyParams extends Params { + reasoning?: RequestyReasoningParam; + usage?: RequestyUsageParam; + stream_options?: { + include_usage?: boolean; + }; +} + +type RequestyReasoningParam = { + effort?: 'low' | 'medium' | 'high' | string; + max_tokens?: number; + exclude?: boolean; +}; + +export const transformReasoningParams = (params: RequestyParams) => { + let reasoning: RequestyReasoningParam = { ...params.reasoning }; + if (params.reasoning_effort) { + reasoning.effort = params.reasoning_effort; + } + return Object.keys(reasoning).length > 0 ? reasoning : null; +}; + +export const transformUsageOptions = (params: RequestyParams) => { + let usage: RequestyUsageParam = { ...params.usage }; + if (params.stream_options?.include_usage) { + usage.include = params.stream_options?.include_usage; + } + return Object.keys(usage).length > 0 ? usage : null; +};