mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
feat(openrouter): add open router to model block (#1172)
* feat(openrouter): add open router to model block * improvement(openrouter): streaming fix, temperature fix * pr comments --------- Co-authored-by: waleedlatif1 <walif6@gmail.com>
This commit is contained in:
co-authored by
waleedlatif1
parent
df646256b3
commit
7d62c200fa
@@ -0,0 +1,46 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
|
||||
const logger = createLogger('OpenRouterModelsAPI')
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export async function GET(_request: NextRequest) {
|
||||
try {
|
||||
const response = await fetch('https://openrouter.ai/api/v1/models', {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
cache: 'no-store',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
logger.warn('Failed to fetch OpenRouter models', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
})
|
||||
return NextResponse.json({ models: [] })
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
const models = Array.isArray(data?.data)
|
||||
? Array.from(
|
||||
new Set(
|
||||
data.data
|
||||
.map((m: any) => m?.id)
|
||||
.filter((id: unknown): id is string => typeof id === 'string' && id.length > 0)
|
||||
.map((id: string) => `openrouter/${id}`)
|
||||
)
|
||||
)
|
||||
: []
|
||||
|
||||
logger.info('Successfully fetched OpenRouter models', {
|
||||
count: models.length,
|
||||
})
|
||||
|
||||
return NextResponse.json({ models })
|
||||
} catch (error) {
|
||||
logger.error('Error fetching OpenRouter models', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
})
|
||||
return NextResponse.json({ models: [] })
|
||||
}
|
||||
}
|
||||
+8
-1
@@ -28,6 +28,7 @@ interface ComboBoxProps {
|
||||
placeholder?: string
|
||||
isConnecting: boolean
|
||||
config: SubBlockConfig
|
||||
isWide?: boolean
|
||||
}
|
||||
|
||||
export function ComboBox({
|
||||
@@ -42,6 +43,7 @@ export function ComboBox({
|
||||
placeholder = 'Type or select an option...',
|
||||
isConnecting,
|
||||
config,
|
||||
isWide = false,
|
||||
}: ComboBoxProps) {
|
||||
const [storeValue, setStoreValue] = useSubBlockValue<string>(blockId, subBlockId)
|
||||
const [storeInitialized, setStoreInitialized] = useState(false)
|
||||
@@ -446,7 +448,12 @@ export function ComboBox({
|
||||
|
||||
{/* Dropdown */}
|
||||
{open && (
|
||||
<div className='absolute top-full left-0 z-[100] mt-1 w-full min-w-[286px]'>
|
||||
<div
|
||||
className={cn(
|
||||
'absolute top-full left-0 z-[100] mt-1 w-full overflow-visible',
|
||||
isWide ? 'min-w-[350px]' : 'min-w-[286px]'
|
||||
)}
|
||||
>
|
||||
<div className='allow-scroll fade-in-0 zoom-in-95 animate-in rounded-md border bg-popover text-popover-foreground shadow-lg'>
|
||||
<div
|
||||
ref={dropdownRef}
|
||||
|
||||
+3
-3
@@ -1457,19 +1457,19 @@ export function ToolInput({
|
||||
{tool.usageControl === 'auto' && (
|
||||
<span>
|
||||
{' '}
|
||||
<span className='font-medium'>Auto:</span> Let the model decide
|
||||
<span className='font-medium'> Auto:</span> Let the model decide
|
||||
when to use the tool
|
||||
</span>
|
||||
)}
|
||||
{tool.usageControl === 'force' && (
|
||||
<span>
|
||||
<span className='font-medium'>Force:</span> Always use this tool
|
||||
<span className='font-medium'> Force:</span> Always use this tool
|
||||
in the response
|
||||
</span>
|
||||
)}
|
||||
{tool.usageControl === 'none' && (
|
||||
<span>
|
||||
<span className='font-medium'>Deny:</span> Never use this tool
|
||||
<span className='font-medium'> Deny:</span> Never use this tool
|
||||
</span>
|
||||
)}
|
||||
</p>
|
||||
|
||||
+3
@@ -45,6 +45,7 @@ interface SubBlockProps {
|
||||
disabled?: boolean
|
||||
fieldDiffStatus?: FieldDiffStatus
|
||||
allowExpandInPreview?: boolean
|
||||
isWide?: boolean
|
||||
}
|
||||
|
||||
export function SubBlock({
|
||||
@@ -56,6 +57,7 @@ export function SubBlock({
|
||||
disabled = false,
|
||||
fieldDiffStatus,
|
||||
allowExpandInPreview,
|
||||
isWide = false,
|
||||
}: SubBlockProps) {
|
||||
const [isValidJson, setIsValidJson] = useState(true)
|
||||
|
||||
@@ -148,6 +150,7 @@ export function SubBlock({
|
||||
disabled={isDisabled}
|
||||
isConnecting={isConnecting}
|
||||
config={config}
|
||||
isWide={isWide}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
+1
@@ -995,6 +995,7 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
: undefined
|
||||
}
|
||||
allowExpandInPreview={currentWorkflow.isDiffMode}
|
||||
isWide={displayIsWide}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -6,13 +6,12 @@ import {
|
||||
getAllModelProviders,
|
||||
getBaseModelProviders,
|
||||
getHostedModels,
|
||||
getMaxTemperature,
|
||||
getProviderIcon,
|
||||
MODELS_TEMP_RANGE_0_1,
|
||||
MODELS_TEMP_RANGE_0_2,
|
||||
MODELS_WITH_REASONING_EFFORT,
|
||||
MODELS_WITH_TEMPERATURE_SUPPORT,
|
||||
MODELS_WITH_VERBOSITY,
|
||||
providers,
|
||||
supportsTemperature,
|
||||
} from '@/providers/utils'
|
||||
|
||||
// Get current Ollama models dynamically
|
||||
@@ -21,6 +20,7 @@ const getCurrentOllamaModels = () => {
|
||||
}
|
||||
|
||||
import { useOllamaStore } from '@/stores/ollama/store'
|
||||
import { useOpenRouterStore } from '@/stores/openrouter/store'
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
const logger = createLogger('AgentBlock')
|
||||
@@ -159,8 +159,9 @@ Create a system prompt appropriately detailed for the request, using clear langu
|
||||
required: true,
|
||||
options: () => {
|
||||
const ollamaModels = useOllamaStore.getState().models
|
||||
const openrouterModels = useOpenRouterStore.getState().models
|
||||
const baseModels = Object.keys(getBaseModelProviders())
|
||||
const allModels = [...baseModels, ...ollamaModels]
|
||||
const allModels = Array.from(new Set([...baseModels, ...ollamaModels, ...openrouterModels]))
|
||||
|
||||
return allModels.map((model) => {
|
||||
const icon = getProviderIcon(model)
|
||||
@@ -175,10 +176,15 @@ Create a system prompt appropriately detailed for the request, using clear langu
|
||||
layout: 'half',
|
||||
min: 0,
|
||||
max: 1,
|
||||
condition: {
|
||||
condition: () => ({
|
||||
field: 'model',
|
||||
value: MODELS_TEMP_RANGE_0_1,
|
||||
},
|
||||
value: (() => {
|
||||
const allModels = Object.keys(getAllModelProviders())
|
||||
return allModels.filter(
|
||||
(model) => supportsTemperature(model) && getMaxTemperature(model) === 1
|
||||
)
|
||||
})(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'temperature',
|
||||
@@ -187,30 +193,15 @@ Create a system prompt appropriately detailed for the request, using clear langu
|
||||
layout: 'half',
|
||||
min: 0,
|
||||
max: 2,
|
||||
condition: {
|
||||
condition: () => ({
|
||||
field: 'model',
|
||||
value: MODELS_TEMP_RANGE_0_2,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'temperature',
|
||||
title: 'Temperature',
|
||||
type: 'slider',
|
||||
layout: 'full',
|
||||
min: 0,
|
||||
max: 2,
|
||||
condition: {
|
||||
field: 'model',
|
||||
value: [...MODELS_TEMP_RANGE_0_1, ...MODELS_TEMP_RANGE_0_2],
|
||||
not: true,
|
||||
and: {
|
||||
field: 'model',
|
||||
value: Object.keys(getBaseModelProviders()).filter(
|
||||
(model) => !MODELS_WITH_TEMPERATURE_SUPPORT.includes(model)
|
||||
),
|
||||
not: true,
|
||||
},
|
||||
},
|
||||
value: (() => {
|
||||
const allModels = Object.keys(getAllModelProviders())
|
||||
return allModels.filter(
|
||||
(model) => supportsTemperature(model) && getMaxTemperature(model) === 2
|
||||
)
|
||||
})(),
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: 'reasoningEffort',
|
||||
|
||||
@@ -3436,3 +3436,19 @@ export function MySQLIcon(props: SVGProps<SVGSVGElement>) {
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function OpenRouterIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
{...props}
|
||||
fill='currentColor'
|
||||
fillRule='evenodd'
|
||||
height='1em'
|
||||
viewBox='0 0 24 24'
|
||||
width='1em'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<path d='M16.804 1.957l7.22 4.105v.087L16.73 10.21l.017-2.117-.821-.03c-1.059-.028-1.611.002-2.268.11-1.064.175-2.038.577-3.147 1.352L8.345 11.03c-.284.195-.495.336-.68.455l-.515.322-.397.234.385.23.53.338c.476.314 1.17.796 2.701 1.866 1.11.775 2.083 1.177 3.147 1.352l.3.045c.694.091 1.375.094 2.825.033l.022-2.159 7.22 4.105v.087L16.589 22l.014-1.862-.635.022c-1.386.042-2.137.002-3.138-.162-1.694-.28-3.26-.926-4.881-2.059l-2.158-1.5a21.997 21.997 0 00-.755-.498l-.467-.28a55.927 55.927 0 00-.76-.43C2.908 14.73.563 14.116 0 14.116V9.888l.14.004c.564-.007 2.91-.622 3.809-1.124l1.016-.58.438-.274c.428-.28 1.072-.726 2.686-1.853 1.621-1.133 3.186-1.78 4.881-2.059 1.152-.19 1.974-.213 3.814-.138l.02-1.907z' />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
calculateCost,
|
||||
generateStructuredOutputInstructions,
|
||||
getProvider,
|
||||
shouldBillModelUsage,
|
||||
supportsTemperature,
|
||||
} from '@/providers/utils'
|
||||
|
||||
@@ -85,7 +86,23 @@ export async function executeProviderRequest(
|
||||
const { prompt: promptTokens = 0, completion: completionTokens = 0 } = response.tokens
|
||||
const useCachedInput = !!request.context && request.context.length > 0
|
||||
|
||||
response.cost = calculateCost(response.model, promptTokens, completionTokens, useCachedInput)
|
||||
if (shouldBillModelUsage(response.model, request.apiKey)) {
|
||||
response.cost = calculateCost(response.model, promptTokens, completionTokens, useCachedInput)
|
||||
} else {
|
||||
response.cost = {
|
||||
input: 0,
|
||||
output: 0,
|
||||
total: 0,
|
||||
pricing: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
updatedAt: new Date().toISOString(),
|
||||
},
|
||||
}
|
||||
logger.debug(
|
||||
`Not billing model usage for ${response.model} - user provided API key or not hosted model`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return response
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
GroqIcon,
|
||||
OllamaIcon,
|
||||
OpenAIIcon,
|
||||
OpenRouterIcon,
|
||||
xAIIcon,
|
||||
} from '@/components/icons'
|
||||
|
||||
@@ -56,12 +57,26 @@ export interface ProviderDefinition {
|
||||
defaultModel: string
|
||||
modelPatterns?: RegExp[]
|
||||
icon?: React.ComponentType<{ className?: string }>
|
||||
capabilities?: ModelCapabilities
|
||||
}
|
||||
|
||||
/**
|
||||
* Comprehensive provider definitions, single source of truth
|
||||
*/
|
||||
export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
openrouter: {
|
||||
id: 'openrouter',
|
||||
name: 'OpenRouter',
|
||||
description: 'Unified access to many models via OpenRouter',
|
||||
defaultModel: '',
|
||||
modelPatterns: [/^openrouter\//],
|
||||
icon: OpenRouterIcon,
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 2 },
|
||||
toolUsageControl: true,
|
||||
},
|
||||
models: [],
|
||||
},
|
||||
openai: {
|
||||
id: 'openai',
|
||||
name: 'OpenAI',
|
||||
@@ -69,6 +84,9 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
defaultModel: 'gpt-4o',
|
||||
modelPatterns: [/^gpt/, /^o1/],
|
||||
icon: OpenAIIcon,
|
||||
capabilities: {
|
||||
toolUsageControl: true,
|
||||
},
|
||||
models: [
|
||||
{
|
||||
id: 'gpt-4o',
|
||||
@@ -80,7 +98,6 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 2 },
|
||||
toolUsageControl: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -92,7 +109,6 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
updatedAt: '2025-08-07',
|
||||
},
|
||||
capabilities: {
|
||||
toolUsageControl: true,
|
||||
reasoningEffort: {
|
||||
values: ['minimal', 'low', 'medium', 'high'],
|
||||
},
|
||||
@@ -110,7 +126,6 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
updatedAt: '2025-08-07',
|
||||
},
|
||||
capabilities: {
|
||||
toolUsageControl: true,
|
||||
reasoningEffort: {
|
||||
values: ['minimal', 'low', 'medium', 'high'],
|
||||
},
|
||||
@@ -128,7 +143,6 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
updatedAt: '2025-08-07',
|
||||
},
|
||||
capabilities: {
|
||||
toolUsageControl: true,
|
||||
reasoningEffort: {
|
||||
values: ['minimal', 'low', 'medium', 'high'],
|
||||
},
|
||||
@@ -145,9 +159,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
output: 10.0,
|
||||
updatedAt: '2025-08-07',
|
||||
},
|
||||
capabilities: {
|
||||
toolUsageControl: true,
|
||||
},
|
||||
capabilities: {},
|
||||
},
|
||||
{
|
||||
id: 'o1',
|
||||
@@ -157,9 +169,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
output: 60,
|
||||
updatedAt: '2025-06-17',
|
||||
},
|
||||
capabilities: {
|
||||
toolUsageControl: true,
|
||||
},
|
||||
capabilities: {},
|
||||
},
|
||||
{
|
||||
id: 'o3',
|
||||
@@ -169,9 +179,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
output: 8,
|
||||
updatedAt: '2025-06-17',
|
||||
},
|
||||
capabilities: {
|
||||
toolUsageControl: true,
|
||||
},
|
||||
capabilities: {},
|
||||
},
|
||||
{
|
||||
id: 'o4-mini',
|
||||
@@ -181,9 +189,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
output: 4.4,
|
||||
updatedAt: '2025-06-17',
|
||||
},
|
||||
capabilities: {
|
||||
toolUsageControl: true,
|
||||
},
|
||||
capabilities: {},
|
||||
},
|
||||
{
|
||||
id: 'gpt-4.1',
|
||||
@@ -195,7 +201,6 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 2 },
|
||||
toolUsageControl: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -208,7 +213,6 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 2 },
|
||||
toolUsageControl: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -221,7 +225,6 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 2 },
|
||||
toolUsageControl: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -232,6 +235,9 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
description: 'Microsoft Azure OpenAI Service models',
|
||||
defaultModel: 'azure/gpt-4o',
|
||||
modelPatterns: [/^azure\//],
|
||||
capabilities: {
|
||||
toolUsageControl: true,
|
||||
},
|
||||
icon: AzureIcon,
|
||||
models: [
|
||||
{
|
||||
@@ -244,7 +250,6 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 2 },
|
||||
toolUsageControl: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -256,7 +261,6 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
updatedAt: '2025-08-07',
|
||||
},
|
||||
capabilities: {
|
||||
toolUsageControl: true,
|
||||
reasoningEffort: {
|
||||
values: ['minimal', 'low', 'medium', 'high'],
|
||||
},
|
||||
@@ -274,7 +278,6 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
updatedAt: '2025-08-07',
|
||||
},
|
||||
capabilities: {
|
||||
toolUsageControl: true,
|
||||
reasoningEffort: {
|
||||
values: ['minimal', 'low', 'medium', 'high'],
|
||||
},
|
||||
@@ -292,7 +295,6 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
updatedAt: '2025-08-07',
|
||||
},
|
||||
capabilities: {
|
||||
toolUsageControl: true,
|
||||
reasoningEffort: {
|
||||
values: ['minimal', 'low', 'medium', 'high'],
|
||||
},
|
||||
@@ -309,9 +311,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
output: 10.0,
|
||||
updatedAt: '2025-08-07',
|
||||
},
|
||||
capabilities: {
|
||||
toolUsageControl: true,
|
||||
},
|
||||
capabilities: {},
|
||||
},
|
||||
{
|
||||
id: 'azure/o3',
|
||||
@@ -321,9 +321,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
output: 40,
|
||||
updatedAt: '2025-06-15',
|
||||
},
|
||||
capabilities: {
|
||||
toolUsageControl: true,
|
||||
},
|
||||
capabilities: {},
|
||||
},
|
||||
{
|
||||
id: 'azure/o4-mini',
|
||||
@@ -333,9 +331,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
output: 4.4,
|
||||
updatedAt: '2025-06-15',
|
||||
},
|
||||
capabilities: {
|
||||
toolUsageControl: true,
|
||||
},
|
||||
capabilities: {},
|
||||
},
|
||||
{
|
||||
id: 'azure/gpt-4.1',
|
||||
@@ -345,9 +341,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
output: 8.0,
|
||||
updatedAt: '2025-06-15',
|
||||
},
|
||||
capabilities: {
|
||||
toolUsageControl: true,
|
||||
},
|
||||
capabilities: {},
|
||||
},
|
||||
{
|
||||
id: 'azure/model-router',
|
||||
@@ -357,9 +351,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
output: 8.0,
|
||||
updatedAt: '2025-06-15',
|
||||
},
|
||||
capabilities: {
|
||||
toolUsageControl: true,
|
||||
},
|
||||
capabilities: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -370,6 +362,9 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
defaultModel: 'claude-sonnet-4-0',
|
||||
modelPatterns: [/^claude/],
|
||||
icon: AnthropicIcon,
|
||||
capabilities: {
|
||||
toolUsageControl: true,
|
||||
},
|
||||
models: [
|
||||
{
|
||||
id: 'claude-sonnet-4-0',
|
||||
@@ -381,7 +376,6 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 1 },
|
||||
toolUsageControl: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -394,7 +388,6 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 1 },
|
||||
toolUsageControl: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -407,7 +400,6 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 1 },
|
||||
toolUsageControl: true,
|
||||
computerUse: true,
|
||||
},
|
||||
},
|
||||
@@ -421,7 +413,6 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 1 },
|
||||
toolUsageControl: true,
|
||||
computerUse: true,
|
||||
},
|
||||
},
|
||||
@@ -433,6 +424,9 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
description: "Google's Gemini models",
|
||||
defaultModel: 'gemini-2.5-pro',
|
||||
modelPatterns: [/^gemini/],
|
||||
capabilities: {
|
||||
toolUsageControl: true,
|
||||
},
|
||||
icon: GeminiIcon,
|
||||
models: [
|
||||
{
|
||||
@@ -445,7 +439,6 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 2 },
|
||||
toolUsageControl: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -458,7 +451,6 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 2 },
|
||||
toolUsageControl: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -470,6 +462,9 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
defaultModel: 'deepseek-chat',
|
||||
modelPatterns: [],
|
||||
icon: DeepseekIcon,
|
||||
capabilities: {
|
||||
toolUsageControl: true,
|
||||
},
|
||||
models: [
|
||||
{
|
||||
id: 'deepseek-chat',
|
||||
@@ -479,9 +474,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
output: 1.0,
|
||||
updatedAt: '2025-03-21',
|
||||
},
|
||||
capabilities: {
|
||||
toolUsageControl: true,
|
||||
},
|
||||
capabilities: {},
|
||||
},
|
||||
{
|
||||
id: 'deepseek-v3',
|
||||
@@ -493,7 +486,6 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 2 },
|
||||
toolUsageControl: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -504,9 +496,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
output: 1.5,
|
||||
updatedAt: '2025-03-21',
|
||||
},
|
||||
capabilities: {
|
||||
toolUsageControl: true,
|
||||
},
|
||||
capabilities: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -517,6 +507,9 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
defaultModel: 'grok-4-latest',
|
||||
modelPatterns: [/^grok/],
|
||||
icon: xAIIcon,
|
||||
capabilities: {
|
||||
toolUsageControl: true,
|
||||
},
|
||||
models: [
|
||||
{
|
||||
id: 'grok-4-latest',
|
||||
@@ -528,7 +521,6 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 1 },
|
||||
toolUsageControl: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -541,7 +533,6 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 1 },
|
||||
toolUsageControl: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -554,7 +545,6 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
capabilities: {
|
||||
temperature: { min: 0, max: 1 },
|
||||
toolUsageControl: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -566,6 +556,9 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
defaultModel: 'cerebras/llama-3.3-70b',
|
||||
modelPatterns: [/^cerebras/],
|
||||
icon: CerebrasIcon,
|
||||
capabilities: {
|
||||
toolUsageControl: false,
|
||||
},
|
||||
models: [
|
||||
{
|
||||
id: 'cerebras/llama-3.3-70b',
|
||||
@@ -575,9 +568,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
output: 0.94,
|
||||
updatedAt: '2025-03-21',
|
||||
},
|
||||
capabilities: {
|
||||
toolUsageControl: false,
|
||||
},
|
||||
capabilities: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -588,6 +579,9 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
defaultModel: 'groq/openai/gpt-oss-120b',
|
||||
modelPatterns: [/^groq/],
|
||||
icon: GroqIcon,
|
||||
capabilities: {
|
||||
toolUsageControl: false,
|
||||
},
|
||||
models: [
|
||||
{
|
||||
id: 'groq/openai/gpt-oss-120b',
|
||||
@@ -597,9 +591,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
output: 0.75,
|
||||
updatedAt: '2025-08-05',
|
||||
},
|
||||
capabilities: {
|
||||
toolUsageControl: false,
|
||||
},
|
||||
capabilities: {},
|
||||
},
|
||||
{
|
||||
id: 'groq/openai/gpt-oss-20b',
|
||||
@@ -609,9 +601,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
output: 0.25,
|
||||
updatedAt: '2025-08-05',
|
||||
},
|
||||
capabilities: {
|
||||
toolUsageControl: false,
|
||||
},
|
||||
capabilities: {},
|
||||
},
|
||||
{
|
||||
id: 'groq/gemma2-9b-it',
|
||||
@@ -621,9 +611,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
output: 0.04,
|
||||
updatedAt: '2025-08-05',
|
||||
},
|
||||
capabilities: {
|
||||
toolUsageControl: false,
|
||||
},
|
||||
capabilities: {},
|
||||
},
|
||||
{
|
||||
id: 'groq/llama-3.1-8b-instant',
|
||||
@@ -633,9 +621,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
output: 0.08,
|
||||
updatedAt: '2025-08-05',
|
||||
},
|
||||
capabilities: {
|
||||
toolUsageControl: false,
|
||||
},
|
||||
capabilities: {},
|
||||
},
|
||||
{
|
||||
id: 'groq/llama-3.3-70b-versatile',
|
||||
@@ -645,9 +631,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
output: 0.61,
|
||||
updatedAt: '2025-08-05',
|
||||
},
|
||||
capabilities: {
|
||||
toolUsageControl: false,
|
||||
},
|
||||
capabilities: {},
|
||||
},
|
||||
{
|
||||
id: 'groq/meta-llama/llama-guard-4-12b',
|
||||
@@ -657,9 +641,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
output: 0.2,
|
||||
updatedAt: '2025-08-05',
|
||||
},
|
||||
capabilities: {
|
||||
toolUsageControl: false,
|
||||
},
|
||||
capabilities: {},
|
||||
},
|
||||
{
|
||||
id: 'groq/deepseek-r1-distill-llama-70b',
|
||||
@@ -669,9 +651,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
output: 0.99,
|
||||
updatedAt: '2025-08-05',
|
||||
},
|
||||
capabilities: {
|
||||
toolUsageControl: false,
|
||||
},
|
||||
capabilities: {},
|
||||
},
|
||||
{
|
||||
id: 'groq/meta-llama/llama-4-maverick-17b-128e-instruct',
|
||||
@@ -681,9 +661,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
output: 0.6,
|
||||
updatedAt: '2025-08-05',
|
||||
},
|
||||
capabilities: {
|
||||
toolUsageControl: false,
|
||||
},
|
||||
capabilities: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -698,8 +676,6 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
||||
},
|
||||
}
|
||||
|
||||
// Helper functions to extract information from the comprehensive definitions
|
||||
|
||||
/**
|
||||
* Get all models for a specific provider
|
||||
*/
|
||||
@@ -734,9 +710,23 @@ export function getModelCapabilities(modelId: string): ModelCapabilities | null
|
||||
for (const provider of Object.values(PROVIDER_DEFINITIONS)) {
|
||||
const model = provider.models.find((m) => m.id.toLowerCase() === modelId.toLowerCase())
|
||||
if (model) {
|
||||
return model.capabilities
|
||||
// Merge provider capabilities with model capabilities, model takes precedence
|
||||
const capabilities: ModelCapabilities = { ...provider.capabilities, ...model.capabilities }
|
||||
return capabilities
|
||||
}
|
||||
}
|
||||
|
||||
// If no model found, check for provider-level capabilities for dynamically fetched models
|
||||
for (const provider of Object.values(PROVIDER_DEFINITIONS)) {
|
||||
if (provider.modelPatterns) {
|
||||
for (const pattern of provider.modelPatterns) {
|
||||
if (pattern.test(modelId.toLowerCase())) {
|
||||
return provider.capabilities || null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -791,7 +781,7 @@ export function getModelsWithTempRange02(): string[] {
|
||||
export function getProvidersWithToolUsageControl(): string[] {
|
||||
const providers: string[] = []
|
||||
for (const [providerId, provider] of Object.entries(PROVIDER_DEFINITIONS)) {
|
||||
if (provider.models.some((model) => model.capabilities.toolUsageControl)) {
|
||||
if (provider.capabilities?.toolUsageControl) {
|
||||
providers.push(providerId)
|
||||
}
|
||||
}
|
||||
@@ -859,6 +849,21 @@ export function updateOllamaModels(models: string[]): void {
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Update OpenRouter models dynamically
|
||||
*/
|
||||
export function updateOpenRouterModels(models: string[]): void {
|
||||
PROVIDER_DEFINITIONS.openrouter.models = models.map((modelId) => ({
|
||||
id: modelId,
|
||||
pricing: {
|
||||
input: 0,
|
||||
output: 0,
|
||||
updatedAt: new Date().toISOString().split('T')[0],
|
||||
},
|
||||
capabilities: {},
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Embedding model pricing - separate from chat models
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,481 @@
|
||||
import OpenAI from 'openai'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type { StreamingExecution } from '@/executor/types'
|
||||
import { getProviderDefaultModel, getProviderModels } from '@/providers/models'
|
||||
import type {
|
||||
ProviderConfig,
|
||||
ProviderRequest,
|
||||
ProviderResponse,
|
||||
TimeSegment,
|
||||
} from '@/providers/types'
|
||||
import {
|
||||
prepareToolExecution,
|
||||
prepareToolsWithUsageControl,
|
||||
trackForcedToolUsage,
|
||||
} from '@/providers/utils'
|
||||
import { executeTool } from '@/tools'
|
||||
|
||||
const logger = createLogger('OpenRouterProvider')
|
||||
|
||||
function createReadableStreamFromOpenAIStream(
|
||||
openaiStream: any,
|
||||
onComplete?: (content: string, usage?: any) => void
|
||||
): ReadableStream {
|
||||
let fullContent = ''
|
||||
let usageData: any = null
|
||||
|
||||
return new ReadableStream({
|
||||
async start(controller) {
|
||||
try {
|
||||
for await (const chunk of openaiStream) {
|
||||
if (chunk.usage) {
|
||||
usageData = chunk.usage
|
||||
}
|
||||
|
||||
const content = chunk.choices[0]?.delta?.content || ''
|
||||
if (content) {
|
||||
fullContent += content
|
||||
controller.enqueue(new TextEncoder().encode(content))
|
||||
}
|
||||
}
|
||||
|
||||
if (onComplete) {
|
||||
onComplete(fullContent, usageData)
|
||||
}
|
||||
|
||||
controller.close()
|
||||
} catch (error) {
|
||||
controller.error(error)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const openRouterProvider: ProviderConfig = {
|
||||
id: 'openrouter',
|
||||
name: 'OpenRouter',
|
||||
description: 'Unified access to many models via OpenRouter',
|
||||
version: '1.0.0',
|
||||
models: getProviderModels('openrouter'),
|
||||
defaultModel: getProviderDefaultModel('openrouter'),
|
||||
|
||||
executeRequest: async (
|
||||
request: ProviderRequest
|
||||
): Promise<ProviderResponse | StreamingExecution> => {
|
||||
if (!request.apiKey) {
|
||||
throw new Error('API key is required for OpenRouter')
|
||||
}
|
||||
|
||||
const client = new OpenAI({
|
||||
apiKey: request.apiKey,
|
||||
baseURL: 'https://openrouter.ai/api/v1',
|
||||
})
|
||||
|
||||
const requestedModel = (request.model || '').replace(/^openrouter\//, '')
|
||||
|
||||
logger.info('Preparing OpenRouter request', {
|
||||
model: requestedModel,
|
||||
hasSystemPrompt: !!request.systemPrompt,
|
||||
hasMessages: !!request.messages?.length,
|
||||
hasTools: !!request.tools?.length,
|
||||
toolCount: request.tools?.length || 0,
|
||||
hasResponseFormat: !!request.responseFormat,
|
||||
stream: !!request.stream,
|
||||
})
|
||||
|
||||
const allMessages = [] as any[]
|
||||
|
||||
if (request.systemPrompt) {
|
||||
allMessages.push({ role: 'system', content: request.systemPrompt })
|
||||
}
|
||||
|
||||
if (request.context) {
|
||||
allMessages.push({ role: 'user', content: request.context })
|
||||
}
|
||||
|
||||
if (request.messages) {
|
||||
allMessages.push(...request.messages)
|
||||
}
|
||||
|
||||
const tools = request.tools?.length
|
||||
? request.tools.map((tool) => ({
|
||||
type: 'function',
|
||||
function: {
|
||||
name: tool.id,
|
||||
description: tool.description,
|
||||
parameters: tool.parameters,
|
||||
},
|
||||
}))
|
||||
: undefined
|
||||
|
||||
const payload: any = {
|
||||
model: requestedModel,
|
||||
messages: allMessages,
|
||||
}
|
||||
|
||||
if (request.temperature !== undefined) payload.temperature = request.temperature
|
||||
if (request.maxTokens !== undefined) payload.max_tokens = request.maxTokens
|
||||
|
||||
if (request.responseFormat) {
|
||||
payload.response_format = {
|
||||
type: 'json_schema',
|
||||
json_schema: {
|
||||
name: request.responseFormat.name || 'response_schema',
|
||||
schema: request.responseFormat.schema || request.responseFormat,
|
||||
strict: request.responseFormat.strict !== false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
let preparedTools: ReturnType<typeof prepareToolsWithUsageControl> | null = null
|
||||
let hasActiveTools = false
|
||||
if (tools?.length) {
|
||||
preparedTools = prepareToolsWithUsageControl(tools, request.tools, logger, 'openrouter')
|
||||
const { tools: filteredTools, toolChoice } = preparedTools
|
||||
if (filteredTools?.length && toolChoice) {
|
||||
payload.tools = filteredTools
|
||||
payload.tool_choice = toolChoice
|
||||
hasActiveTools = true
|
||||
}
|
||||
}
|
||||
|
||||
const providerStartTime = Date.now()
|
||||
const providerStartTimeISO = new Date(providerStartTime).toISOString()
|
||||
|
||||
try {
|
||||
if (request.stream && (!tools || tools.length === 0 || !hasActiveTools)) {
|
||||
const streamResponse = await client.chat.completions.create({
|
||||
...payload,
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
})
|
||||
|
||||
const tokenUsage = { prompt: 0, completion: 0, total: 0 }
|
||||
|
||||
const streamingResult = {
|
||||
stream: createReadableStreamFromOpenAIStream(streamResponse, (content, usage) => {
|
||||
if (usage) {
|
||||
const newTokens = {
|
||||
prompt: usage.prompt_tokens || tokenUsage.prompt,
|
||||
completion: usage.completion_tokens || tokenUsage.completion,
|
||||
total: usage.total_tokens || tokenUsage.total,
|
||||
}
|
||||
streamingResult.execution.output.tokens = newTokens
|
||||
}
|
||||
streamingResult.execution.output.content = content
|
||||
const end = Date.now()
|
||||
const endISO = new Date(end).toISOString()
|
||||
if (streamingResult.execution.output.providerTiming) {
|
||||
streamingResult.execution.output.providerTiming.endTime = endISO
|
||||
streamingResult.execution.output.providerTiming.duration = end - providerStartTime
|
||||
if (streamingResult.execution.output.providerTiming.timeSegments?.[0]) {
|
||||
streamingResult.execution.output.providerTiming.timeSegments[0].endTime = end
|
||||
streamingResult.execution.output.providerTiming.timeSegments[0].duration =
|
||||
end - providerStartTime
|
||||
}
|
||||
}
|
||||
}),
|
||||
execution: {
|
||||
success: true,
|
||||
output: {
|
||||
content: '',
|
||||
model: requestedModel,
|
||||
tokens: tokenUsage,
|
||||
toolCalls: undefined,
|
||||
providerTiming: {
|
||||
startTime: providerStartTimeISO,
|
||||
endTime: new Date().toISOString(),
|
||||
duration: Date.now() - providerStartTime,
|
||||
timeSegments: [
|
||||
{
|
||||
type: 'model',
|
||||
name: 'Streaming response',
|
||||
startTime: providerStartTime,
|
||||
endTime: Date.now(),
|
||||
duration: Date.now() - providerStartTime,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
logs: [],
|
||||
metadata: {
|
||||
startTime: providerStartTimeISO,
|
||||
endTime: new Date().toISOString(),
|
||||
duration: Date.now() - providerStartTime,
|
||||
},
|
||||
},
|
||||
} as StreamingExecution
|
||||
|
||||
return streamingResult as StreamingExecution
|
||||
}
|
||||
|
||||
const initialCallTime = Date.now()
|
||||
const originalToolChoice = payload.tool_choice
|
||||
const forcedTools = preparedTools?.forcedTools || []
|
||||
let usedForcedTools: string[] = []
|
||||
|
||||
let currentResponse = await client.chat.completions.create(payload)
|
||||
const firstResponseTime = Date.now() - initialCallTime
|
||||
|
||||
let content = currentResponse.choices[0]?.message?.content || ''
|
||||
const tokens = {
|
||||
prompt: currentResponse.usage?.prompt_tokens || 0,
|
||||
completion: currentResponse.usage?.completion_tokens || 0,
|
||||
total: currentResponse.usage?.total_tokens || 0,
|
||||
}
|
||||
const toolCalls = [] as any[]
|
||||
const toolResults = [] as any[]
|
||||
const currentMessages = [...allMessages]
|
||||
let iterationCount = 0
|
||||
const MAX_ITERATIONS = 10
|
||||
let modelTime = firstResponseTime
|
||||
let toolsTime = 0
|
||||
let hasUsedForcedTool = false
|
||||
const timeSegments: TimeSegment[] = [
|
||||
{
|
||||
type: 'model',
|
||||
name: 'Initial response',
|
||||
startTime: initialCallTime,
|
||||
endTime: initialCallTime + firstResponseTime,
|
||||
duration: firstResponseTime,
|
||||
},
|
||||
]
|
||||
|
||||
const checkForForcedToolUsage = (
|
||||
response: any,
|
||||
toolChoice: string | { type: string; function?: { name: string }; name?: string; any?: any }
|
||||
) => {
|
||||
if (typeof toolChoice === 'object' && response.choices[0]?.message?.tool_calls) {
|
||||
const toolCallsResponse = response.choices[0].message.tool_calls
|
||||
const result = trackForcedToolUsage(
|
||||
toolCallsResponse,
|
||||
toolChoice,
|
||||
logger,
|
||||
'openrouter',
|
||||
forcedTools,
|
||||
usedForcedTools
|
||||
)
|
||||
hasUsedForcedTool = result.hasUsedForcedTool
|
||||
usedForcedTools = result.usedForcedTools
|
||||
}
|
||||
}
|
||||
|
||||
checkForForcedToolUsage(currentResponse, originalToolChoice)
|
||||
|
||||
while (iterationCount < MAX_ITERATIONS) {
|
||||
const toolCallsInResponse = currentResponse.choices[0]?.message?.tool_calls
|
||||
if (!toolCallsInResponse || toolCallsInResponse.length === 0) {
|
||||
break
|
||||
}
|
||||
|
||||
const toolsStartTime = Date.now()
|
||||
for (const toolCall of toolCallsInResponse) {
|
||||
try {
|
||||
const toolName = toolCall.function.name
|
||||
const toolArgs = JSON.parse(toolCall.function.arguments)
|
||||
const tool = request.tools?.find((t) => t.id === toolName)
|
||||
if (!tool) continue
|
||||
|
||||
const toolCallStartTime = Date.now()
|
||||
const { toolParams, executionParams } = prepareToolExecution(tool, toolArgs, request)
|
||||
const result = await executeTool(toolName, executionParams, true)
|
||||
const toolCallEndTime = Date.now()
|
||||
const toolCallDuration = toolCallEndTime - toolCallStartTime
|
||||
|
||||
timeSegments.push({
|
||||
type: 'tool',
|
||||
name: toolName,
|
||||
startTime: toolCallStartTime,
|
||||
endTime: toolCallEndTime,
|
||||
duration: toolCallDuration,
|
||||
})
|
||||
|
||||
let resultContent: any
|
||||
if (result.success) {
|
||||
toolResults.push(result.output)
|
||||
resultContent = result.output
|
||||
} else {
|
||||
resultContent = {
|
||||
error: true,
|
||||
message: result.error || 'Tool execution failed',
|
||||
tool: toolName,
|
||||
}
|
||||
}
|
||||
|
||||
toolCalls.push({
|
||||
name: toolName,
|
||||
arguments: toolParams,
|
||||
startTime: new Date(toolCallStartTime).toISOString(),
|
||||
endTime: new Date(toolCallEndTime).toISOString(),
|
||||
duration: toolCallDuration,
|
||||
result: resultContent,
|
||||
success: result.success,
|
||||
})
|
||||
|
||||
currentMessages.push({
|
||||
role: 'assistant',
|
||||
content: null,
|
||||
tool_calls: [
|
||||
{
|
||||
id: toolCall.id,
|
||||
type: 'function',
|
||||
function: {
|
||||
name: toolName,
|
||||
arguments: toolCall.function.arguments,
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
currentMessages.push({
|
||||
role: 'tool',
|
||||
tool_call_id: toolCall.id,
|
||||
content: JSON.stringify(resultContent),
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('Error processing tool call (OpenRouter):', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
toolName: toolCall?.function?.name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const thisToolsTime = Date.now() - toolsStartTime
|
||||
toolsTime += thisToolsTime
|
||||
|
||||
const nextPayload: any = {
|
||||
...payload,
|
||||
messages: currentMessages,
|
||||
}
|
||||
|
||||
if (typeof originalToolChoice === 'object' && hasUsedForcedTool && forcedTools.length > 0) {
|
||||
const remainingTools = forcedTools.filter((tool) => !usedForcedTools.includes(tool))
|
||||
if (remainingTools.length > 0) {
|
||||
nextPayload.tool_choice = { type: 'function', function: { name: remainingTools[0] } }
|
||||
} else {
|
||||
nextPayload.tool_choice = 'auto'
|
||||
}
|
||||
}
|
||||
|
||||
const nextModelStartTime = Date.now()
|
||||
currentResponse = await client.chat.completions.create(nextPayload)
|
||||
checkForForcedToolUsage(currentResponse, nextPayload.tool_choice)
|
||||
const nextModelEndTime = Date.now()
|
||||
const thisModelTime = nextModelEndTime - nextModelStartTime
|
||||
timeSegments.push({
|
||||
type: 'model',
|
||||
name: `Model response (iteration ${iterationCount + 1})`,
|
||||
startTime: nextModelStartTime,
|
||||
endTime: nextModelEndTime,
|
||||
duration: thisModelTime,
|
||||
})
|
||||
modelTime += thisModelTime
|
||||
if (currentResponse.choices[0]?.message?.content) {
|
||||
content = currentResponse.choices[0].message.content
|
||||
}
|
||||
if (currentResponse.usage) {
|
||||
tokens.prompt += currentResponse.usage.prompt_tokens || 0
|
||||
tokens.completion += currentResponse.usage.completion_tokens || 0
|
||||
tokens.total += currentResponse.usage.total_tokens || 0
|
||||
}
|
||||
iterationCount++
|
||||
}
|
||||
|
||||
if (request.stream && iterationCount > 0) {
|
||||
const streamingPayload = {
|
||||
...payload,
|
||||
messages: currentMessages,
|
||||
tool_choice: 'auto',
|
||||
stream: true,
|
||||
stream_options: { include_usage: true },
|
||||
}
|
||||
|
||||
const streamResponse = await client.chat.completions.create(streamingPayload)
|
||||
const streamingResult = {
|
||||
stream: createReadableStreamFromOpenAIStream(streamResponse, (content, usage) => {
|
||||
if (usage) {
|
||||
const newTokens = {
|
||||
prompt: usage.prompt_tokens || tokens.prompt,
|
||||
completion: usage.completion_tokens || tokens.completion,
|
||||
total: usage.total_tokens || tokens.total,
|
||||
}
|
||||
streamingResult.execution.output.tokens = newTokens
|
||||
}
|
||||
streamingResult.execution.output.content = content
|
||||
}),
|
||||
execution: {
|
||||
success: true,
|
||||
output: {
|
||||
content: '',
|
||||
model: requestedModel,
|
||||
tokens: { prompt: tokens.prompt, completion: tokens.completion, total: tokens.total },
|
||||
toolCalls:
|
||||
toolCalls.length > 0
|
||||
? {
|
||||
list: toolCalls,
|
||||
count: toolCalls.length,
|
||||
}
|
||||
: undefined,
|
||||
providerTiming: {
|
||||
startTime: providerStartTimeISO,
|
||||
endTime: new Date().toISOString(),
|
||||
duration: Date.now() - providerStartTime,
|
||||
modelTime: modelTime,
|
||||
toolsTime: toolsTime,
|
||||
firstResponseTime: firstResponseTime,
|
||||
iterations: iterationCount + 1,
|
||||
timeSegments: timeSegments,
|
||||
},
|
||||
},
|
||||
logs: [],
|
||||
metadata: {
|
||||
startTime: providerStartTimeISO,
|
||||
endTime: new Date().toISOString(),
|
||||
duration: Date.now() - providerStartTime,
|
||||
},
|
||||
},
|
||||
} as StreamingExecution
|
||||
|
||||
return streamingResult as StreamingExecution
|
||||
}
|
||||
|
||||
const providerEndTime = Date.now()
|
||||
const providerEndTimeISO = new Date(providerEndTime).toISOString()
|
||||
const totalDuration = providerEndTime - providerStartTime
|
||||
|
||||
return {
|
||||
content,
|
||||
model: requestedModel,
|
||||
tokens,
|
||||
toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
|
||||
toolResults: toolResults.length > 0 ? toolResults : undefined,
|
||||
timing: {
|
||||
startTime: providerStartTimeISO,
|
||||
endTime: providerEndTimeISO,
|
||||
duration: totalDuration,
|
||||
modelTime: modelTime,
|
||||
toolsTime: toolsTime,
|
||||
firstResponseTime: firstResponseTime,
|
||||
iterations: iterationCount + 1,
|
||||
timeSegments: timeSegments,
|
||||
},
|
||||
}
|
||||
} catch (error) {
|
||||
const providerEndTime = Date.now()
|
||||
const providerEndTimeISO = new Date(providerEndTime).toISOString()
|
||||
const totalDuration = providerEndTime - providerStartTime
|
||||
logger.error('Error in OpenRouter request:', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
duration: totalDuration,
|
||||
})
|
||||
const enhancedError = new Error(error instanceof Error ? error.message : String(error))
|
||||
// @ts-ignore
|
||||
enhancedError.timing = {
|
||||
startTime: providerStartTimeISO,
|
||||
endTime: providerEndTimeISO,
|
||||
duration: totalDuration,
|
||||
}
|
||||
throw enhancedError
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export type ProviderId =
|
||||
| 'cerebras'
|
||||
| 'groq'
|
||||
| 'ollama'
|
||||
| 'openrouter'
|
||||
|
||||
/**
|
||||
* Model pricing information per million tokens
|
||||
|
||||
@@ -166,6 +166,15 @@ describe('Model Capabilities', () => {
|
||||
expect(supportsTemperature('GPT-4O')).toBe(true)
|
||||
expect(supportsTemperature('claude-sonnet-4-0')).toBe(true)
|
||||
})
|
||||
|
||||
it.concurrent(
|
||||
'should inherit temperature support from provider for dynamically fetched models',
|
||||
() => {
|
||||
// OpenRouter models should inherit temperature support from provider capabilities
|
||||
expect(supportsTemperature('openrouter/anthropic/claude-3.5-sonnet')).toBe(true)
|
||||
expect(supportsTemperature('openrouter/openai/gpt-4')).toBe(true)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe('getMaxTemperature', () => {
|
||||
@@ -209,7 +218,7 @@ describe('Model Capabilities', () => {
|
||||
expect(getMaxTemperature('azure/o3')).toBeUndefined()
|
||||
expect(getMaxTemperature('azure/o4-mini')).toBeUndefined()
|
||||
expect(getMaxTemperature('deepseek-r1')).toBeUndefined()
|
||||
// GPT-5 models don't support temperature (removed in our implementation)
|
||||
// GPT-5 models don't support temperature
|
||||
expect(getMaxTemperature('gpt-5')).toBeUndefined()
|
||||
expect(getMaxTemperature('gpt-5-mini')).toBeUndefined()
|
||||
expect(getMaxTemperature('gpt-5-nano')).toBeUndefined()
|
||||
@@ -224,6 +233,14 @@ describe('Model Capabilities', () => {
|
||||
expect(getMaxTemperature('GPT-4O')).toBe(2)
|
||||
expect(getMaxTemperature('CLAUDE-SONNET-4-0')).toBe(1)
|
||||
})
|
||||
|
||||
it.concurrent(
|
||||
'should inherit max temperature from provider for dynamically fetched models',
|
||||
() => {
|
||||
expect(getMaxTemperature('openrouter/anthropic/claude-3.5-sonnet')).toBe(2)
|
||||
expect(getMaxTemperature('openrouter/openai/gpt-4')).toBe(2)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
describe('supportsToolUsageControl', () => {
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
} from '@/providers/models'
|
||||
import { ollamaProvider } from '@/providers/ollama'
|
||||
import { openaiProvider } from '@/providers/openai'
|
||||
import { openRouterProvider } from '@/providers/openrouter'
|
||||
import type { ProviderConfig, ProviderId, ProviderToolConfig } from '@/providers/types'
|
||||
import { xAIProvider } from '@/providers/xai'
|
||||
import { useCustomToolsStore } from '@/stores/custom-tools/store'
|
||||
@@ -88,6 +89,11 @@ export const providers: Record<
|
||||
models: getProviderModelsFromDefinitions('azure-openai'),
|
||||
modelPatterns: PROVIDER_DEFINITIONS['azure-openai'].modelPatterns,
|
||||
},
|
||||
openrouter: {
|
||||
...openRouterProvider,
|
||||
models: getProviderModelsFromDefinitions('openrouter'),
|
||||
modelPatterns: PROVIDER_DEFINITIONS.openrouter.modelPatterns,
|
||||
},
|
||||
ollama: {
|
||||
...ollamaProvider,
|
||||
models: getProviderModelsFromDefinitions('ollama'),
|
||||
@@ -95,7 +101,6 @@ export const providers: Record<
|
||||
},
|
||||
}
|
||||
|
||||
// Initialize all providers that have initialize method
|
||||
Object.entries(providers).forEach(([id, provider]) => {
|
||||
if (provider.initialize) {
|
||||
provider.initialize().catch((error) => {
|
||||
@@ -106,12 +111,17 @@ Object.entries(providers).forEach(([id, provider]) => {
|
||||
}
|
||||
})
|
||||
|
||||
// Function to update Ollama provider models
|
||||
export function updateOllamaProviderModels(models: string[]): void {
|
||||
updateOllamaModelsInDefinitions(models)
|
||||
providers.ollama.models = getProviderModelsFromDefinitions('ollama')
|
||||
}
|
||||
|
||||
export async function updateOpenRouterProviderModels(models: string[]): Promise<void> {
|
||||
const { updateOpenRouterModels } = await import('@/providers/models')
|
||||
updateOpenRouterModels(models)
|
||||
providers.openrouter.models = getProviderModelsFromDefinitions('openrouter')
|
||||
}
|
||||
|
||||
export function getBaseModelProviders(): Record<string, ProviderId> {
|
||||
return Object.entries(providers)
|
||||
.filter(([providerId]) => providerId !== 'ollama')
|
||||
@@ -537,6 +547,26 @@ export function getHostedModels(): string[] {
|
||||
return getHostedModelsFromDefinitions()
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if model usage should be billed to the user
|
||||
*
|
||||
* @param model The model name
|
||||
* @param userProvidedApiKey Whether the user provided their own API key
|
||||
* @returns true if the usage should be billed to the user
|
||||
*/
|
||||
export function shouldBillModelUsage(model: string, userProvidedApiKey?: string): boolean {
|
||||
const hostedModels = getHostedModels()
|
||||
if (!hostedModels.includes(model)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (userProvidedApiKey && userProvidedApiKey.trim() !== '') {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an API key for a specific provider, handling rotation and fallbacks
|
||||
* For use server-side only
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { create } from 'zustand'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { updateOpenRouterProviderModels } from '@/providers/utils'
|
||||
import type { OpenRouterStore } from '@/stores/openrouter/types'
|
||||
|
||||
const logger = createLogger('OpenRouterStore')
|
||||
|
||||
const fetchOpenRouterModels = async (): Promise<string[]> => {
|
||||
try {
|
||||
const response = await fetch('/api/providers/openrouter/models')
|
||||
if (!response.ok) {
|
||||
logger.warn('Failed to fetch OpenRouter models from API', {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
})
|
||||
return []
|
||||
}
|
||||
const data = await response.json()
|
||||
return data.models || []
|
||||
} catch (error) {
|
||||
logger.error('Error fetching OpenRouter models', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
})
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export const useOpenRouterStore = create<OpenRouterStore>((set, get) => ({
|
||||
models: [],
|
||||
isLoading: false,
|
||||
setModels: (models) => {
|
||||
const unique = Array.from(new Set(models))
|
||||
set({ models: unique })
|
||||
updateOpenRouterProviderModels(models)
|
||||
},
|
||||
|
||||
fetchModels: async () => {
|
||||
if (typeof window === 'undefined') {
|
||||
logger.info('Skipping client-side model fetch on server')
|
||||
return
|
||||
}
|
||||
if (get().isLoading) {
|
||||
logger.info('Model fetch already in progress')
|
||||
return
|
||||
}
|
||||
logger.info('Fetching OpenRouter models from API')
|
||||
set({ isLoading: true })
|
||||
try {
|
||||
const models = await fetchOpenRouterModels()
|
||||
logger.info('Successfully fetched OpenRouter models', {
|
||||
count: models.length,
|
||||
})
|
||||
get().setModels(models)
|
||||
} catch (error) {
|
||||
logger.error('Failed to fetch OpenRouter models', {
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
})
|
||||
} finally {
|
||||
set({ isLoading: false })
|
||||
}
|
||||
},
|
||||
}))
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
setTimeout(() => {
|
||||
useOpenRouterStore.getState().fetchModels()
|
||||
}, 1000)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export interface OpenRouterStore {
|
||||
models: string[]
|
||||
isLoading: boolean
|
||||
setModels: (models: string[]) => void
|
||||
fetchModels: () => Promise<void>
|
||||
}
|
||||
Reference in New Issue
Block a user