mirror of
https://github.com/simstudioai/sim.git
synced 2026-02-07 21:25:38 -05:00
Compare commits
55 Commits
fix/logs-f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
479cd347ad | ||
|
|
0cb6714496 | ||
|
|
7b36f9257e | ||
|
|
99ae5435e3 | ||
|
|
925f06add7 | ||
|
|
193b95cfec | ||
|
|
a3a99eda19 | ||
|
|
1a66d48add | ||
|
|
46822e91f3 | ||
|
|
2bb68335ee | ||
|
|
8528fbe2d2 | ||
|
|
31fdd2be13 | ||
|
|
028bc652c2 | ||
|
|
c6bf5cd58c | ||
|
|
11dc18a80d | ||
|
|
ab4e9dc72f | ||
|
|
1c58c35bd8 | ||
|
|
d63a5cb504 | ||
|
|
8bd5d41723 | ||
|
|
c12931bc50 | ||
|
|
e9c4251c1c | ||
|
|
cc2be33d6b | ||
|
|
45371e521e | ||
|
|
0ce0f98aa5 | ||
|
|
dff1c9d083 | ||
|
|
b09f683072 | ||
|
|
a8bb0db660 | ||
|
|
af82820a28 | ||
|
|
4372841797 | ||
|
|
5e8c843241 | ||
|
|
7bf3d73ee6 | ||
|
|
7ffc11a738 | ||
|
|
be578e2ed7 | ||
|
|
f415e5edc4 | ||
|
|
13a6e6c3fa | ||
|
|
f5ab7f21ae | ||
|
|
bfb6fffe38 | ||
|
|
4fbec0a43f | ||
|
|
585f5e365b | ||
|
|
3792bdd252 | ||
|
|
eb5d1f3e5b | ||
|
|
54ab82c8dd | ||
|
|
f895bf469b | ||
|
|
dd3209af06 | ||
|
|
b6ba3b50a7 | ||
|
|
b304233062 | ||
|
|
57e4b49bd6 | ||
|
|
e12dd204ed | ||
|
|
3d9d9cbc54 | ||
|
|
0f4ec962ad | ||
|
|
4827866f9a | ||
|
|
3e697d9ed9 | ||
|
|
4431a1a484 | ||
|
|
4d1a9a3f22 | ||
|
|
eb07a080fb |
@@ -5,7 +5,7 @@ import { eq } from 'drizzle-orm'
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { generateAgentCard, generateSkillsFromWorkflow } from '@/lib/a2a/agent-card'
|
import { generateAgentCard, generateSkillsFromWorkflow } from '@/lib/a2a/agent-card'
|
||||||
import type { AgentCapabilities, AgentSkill } from '@/lib/a2a/types'
|
import type { AgentCapabilities, AgentSkill } from '@/lib/a2a/types'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { getRedisClient } from '@/lib/core/config/redis'
|
import { getRedisClient } from '@/lib/core/config/redis'
|
||||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||||
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
||||||
@@ -40,7 +40,7 @@ export async function GET(request: NextRequest, { params }: { params: Promise<Ro
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!agent.agent.isPublished) {
|
if (!agent.agent.isPublished) {
|
||||||
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
|
const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
if (!auth.success) {
|
if (!auth.success) {
|
||||||
return NextResponse.json({ error: 'Agent not published' }, { status: 404 })
|
return NextResponse.json({ error: 'Agent not published' }, { status: 404 })
|
||||||
}
|
}
|
||||||
@@ -81,7 +81,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<Ro
|
|||||||
const { agentId } = await params
|
const { agentId } = await params
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
|
const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
if (!auth.success || !auth.userId) {
|
if (!auth.success || !auth.userId) {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
@@ -151,7 +151,7 @@ export async function DELETE(request: NextRequest, { params }: { params: Promise
|
|||||||
const { agentId } = await params
|
const { agentId } = await params
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
|
const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
if (!auth.success || !auth.userId) {
|
if (!auth.success || !auth.userId) {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
@@ -189,7 +189,7 @@ export async function POST(request: NextRequest, { params }: { params: Promise<R
|
|||||||
const { agentId } = await params
|
const { agentId } = await params
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
|
const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
if (!auth.success || !auth.userId) {
|
if (!auth.success || !auth.userId) {
|
||||||
logger.warn('A2A agent publish auth failed:', { error: auth.error, hasUserId: !!auth.userId })
|
logger.warn('A2A agent publish auth failed:', { error: auth.error, hasUserId: !!auth.userId })
|
||||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { v4 as uuidv4 } from 'uuid'
|
|||||||
import { generateSkillsFromWorkflow } from '@/lib/a2a/agent-card'
|
import { generateSkillsFromWorkflow } from '@/lib/a2a/agent-card'
|
||||||
import { A2A_DEFAULT_CAPABILITIES } from '@/lib/a2a/constants'
|
import { A2A_DEFAULT_CAPABILITIES } from '@/lib/a2a/constants'
|
||||||
import { sanitizeAgentName } from '@/lib/a2a/utils'
|
import { sanitizeAgentName } from '@/lib/a2a/utils'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||||
import { hasValidStartBlockInState } from '@/lib/workflows/triggers/trigger-utils'
|
import { hasValidStartBlockInState } from '@/lib/workflows/triggers/trigger-utils'
|
||||||
import { getWorkspaceById } from '@/lib/workspaces/permissions/utils'
|
import { getWorkspaceById } from '@/lib/workspaces/permissions/utils'
|
||||||
@@ -27,7 +27,7 @@ export const dynamic = 'force-dynamic'
|
|||||||
*/
|
*/
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
|
const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
if (!auth.success || !auth.userId) {
|
if (!auth.success || !auth.userId) {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
@@ -87,7 +87,7 @@ export async function GET(request: NextRequest) {
|
|||||||
*/
|
*/
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
|
const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
if (!auth.success || !auth.userId) {
|
if (!auth.success || !auth.userId) {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { and, eq } from 'drizzle-orm'
|
|||||||
import { jwtDecode } from 'jwt-decode'
|
import { jwtDecode } from 'jwt-decode'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { evaluateScopeCoverage, type OAuthProvider, parseProvider } from '@/lib/oauth'
|
import { evaluateScopeCoverage, type OAuthProvider, parseProvider } from '@/lib/oauth'
|
||||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||||
@@ -81,7 +81,7 @@ export async function GET(request: NextRequest) {
|
|||||||
const { provider: providerParam, workflowId, credentialId } = parseResult.data
|
const { provider: providerParam, workflowId, credentialId } = parseResult.data
|
||||||
|
|
||||||
// Authenticate requester (supports session, API key, internal JWT)
|
// Authenticate requester (supports session, API key, internal JWT)
|
||||||
const authResult = await checkHybridAuth(request)
|
const authResult = await checkSessionOrInternalAuth(request)
|
||||||
if (!authResult.success || !authResult.userId) {
|
if (!authResult.success || !authResult.userId) {
|
||||||
logger.warn(`[${requestId}] Unauthenticated credentials request rejected`)
|
logger.warn(`[${requestId}] Unauthenticated credentials request rejected`)
|
||||||
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
|
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ describe('OAuth Token API Routes', () => {
|
|||||||
const mockRefreshTokenIfNeeded = vi.fn()
|
const mockRefreshTokenIfNeeded = vi.fn()
|
||||||
const mockGetOAuthToken = vi.fn()
|
const mockGetOAuthToken = vi.fn()
|
||||||
const mockAuthorizeCredentialUse = vi.fn()
|
const mockAuthorizeCredentialUse = vi.fn()
|
||||||
const mockCheckHybridAuth = vi.fn()
|
const mockCheckSessionOrInternalAuth = vi.fn()
|
||||||
|
|
||||||
const mockLogger = createMockLogger()
|
const mockLogger = createMockLogger()
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ describe('OAuth Token API Routes', () => {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||||
checkHybridAuth: mockCheckHybridAuth,
|
checkSessionOrInternalAuth: mockCheckSessionOrInternalAuth,
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -235,7 +235,7 @@ describe('OAuth Token API Routes', () => {
|
|||||||
|
|
||||||
describe('credentialAccountUserId + providerId path', () => {
|
describe('credentialAccountUserId + providerId path', () => {
|
||||||
it('should reject unauthenticated requests', async () => {
|
it('should reject unauthenticated requests', async () => {
|
||||||
mockCheckHybridAuth.mockResolvedValueOnce({
|
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Authentication required',
|
error: 'Authentication required',
|
||||||
})
|
})
|
||||||
@@ -255,30 +255,8 @@ describe('OAuth Token API Routes', () => {
|
|||||||
expect(mockGetOAuthToken).not.toHaveBeenCalled()
|
expect(mockGetOAuthToken).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should reject API key authentication', async () => {
|
|
||||||
mockCheckHybridAuth.mockResolvedValueOnce({
|
|
||||||
success: true,
|
|
||||||
authType: 'api_key',
|
|
||||||
userId: 'test-user-id',
|
|
||||||
})
|
|
||||||
|
|
||||||
const req = createMockRequest('POST', {
|
|
||||||
credentialAccountUserId: 'test-user-id',
|
|
||||||
providerId: 'google',
|
|
||||||
})
|
|
||||||
|
|
||||||
const { POST } = await import('@/app/api/auth/oauth/token/route')
|
|
||||||
|
|
||||||
const response = await POST(req)
|
|
||||||
const data = await response.json()
|
|
||||||
|
|
||||||
expect(response.status).toBe(401)
|
|
||||||
expect(data).toHaveProperty('error', 'User not authenticated')
|
|
||||||
expect(mockGetOAuthToken).not.toHaveBeenCalled()
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should reject internal JWT authentication', async () => {
|
it('should reject internal JWT authentication', async () => {
|
||||||
mockCheckHybridAuth.mockResolvedValueOnce({
|
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
|
||||||
success: true,
|
success: true,
|
||||||
authType: 'internal_jwt',
|
authType: 'internal_jwt',
|
||||||
userId: 'test-user-id',
|
userId: 'test-user-id',
|
||||||
@@ -300,7 +278,7 @@ describe('OAuth Token API Routes', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should reject requests for other users credentials', async () => {
|
it('should reject requests for other users credentials', async () => {
|
||||||
mockCheckHybridAuth.mockResolvedValueOnce({
|
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
|
||||||
success: true,
|
success: true,
|
||||||
authType: 'session',
|
authType: 'session',
|
||||||
userId: 'attacker-user-id',
|
userId: 'attacker-user-id',
|
||||||
@@ -322,7 +300,7 @@ describe('OAuth Token API Routes', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should allow session-authenticated users to access their own credentials', async () => {
|
it('should allow session-authenticated users to access their own credentials', async () => {
|
||||||
mockCheckHybridAuth.mockResolvedValueOnce({
|
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
|
||||||
success: true,
|
success: true,
|
||||||
authType: 'session',
|
authType: 'session',
|
||||||
userId: 'test-user-id',
|
userId: 'test-user-id',
|
||||||
@@ -345,7 +323,7 @@ describe('OAuth Token API Routes', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should return 404 when credential not found for user', async () => {
|
it('should return 404 when credential not found for user', async () => {
|
||||||
mockCheckHybridAuth.mockResolvedValueOnce({
|
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
|
||||||
success: true,
|
success: true,
|
||||||
authType: 'session',
|
authType: 'session',
|
||||||
userId: 'test-user-id',
|
userId: 'test-user-id',
|
||||||
@@ -373,7 +351,7 @@ describe('OAuth Token API Routes', () => {
|
|||||||
*/
|
*/
|
||||||
describe('GET handler', () => {
|
describe('GET handler', () => {
|
||||||
it('should return access token successfully', async () => {
|
it('should return access token successfully', async () => {
|
||||||
mockCheckHybridAuth.mockResolvedValueOnce({
|
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
|
||||||
success: true,
|
success: true,
|
||||||
authType: 'session',
|
authType: 'session',
|
||||||
userId: 'test-user-id',
|
userId: 'test-user-id',
|
||||||
@@ -402,7 +380,7 @@ describe('OAuth Token API Routes', () => {
|
|||||||
expect(response.status).toBe(200)
|
expect(response.status).toBe(200)
|
||||||
expect(data).toHaveProperty('accessToken', 'fresh-token')
|
expect(data).toHaveProperty('accessToken', 'fresh-token')
|
||||||
|
|
||||||
expect(mockCheckHybridAuth).toHaveBeenCalled()
|
expect(mockCheckSessionOrInternalAuth).toHaveBeenCalled()
|
||||||
expect(mockGetCredential).toHaveBeenCalledWith(mockRequestId, 'credential-id', 'test-user-id')
|
expect(mockGetCredential).toHaveBeenCalledWith(mockRequestId, 'credential-id', 'test-user-id')
|
||||||
expect(mockRefreshTokenIfNeeded).toHaveBeenCalled()
|
expect(mockRefreshTokenIfNeeded).toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
@@ -421,7 +399,7 @@ describe('OAuth Token API Routes', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should handle authentication failure', async () => {
|
it('should handle authentication failure', async () => {
|
||||||
mockCheckHybridAuth.mockResolvedValueOnce({
|
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Authentication required',
|
error: 'Authentication required',
|
||||||
})
|
})
|
||||||
@@ -440,7 +418,7 @@ describe('OAuth Token API Routes', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should handle credential not found', async () => {
|
it('should handle credential not found', async () => {
|
||||||
mockCheckHybridAuth.mockResolvedValueOnce({
|
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
|
||||||
success: true,
|
success: true,
|
||||||
authType: 'session',
|
authType: 'session',
|
||||||
userId: 'test-user-id',
|
userId: 'test-user-id',
|
||||||
@@ -461,7 +439,7 @@ describe('OAuth Token API Routes', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should handle missing access token', async () => {
|
it('should handle missing access token', async () => {
|
||||||
mockCheckHybridAuth.mockResolvedValueOnce({
|
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
|
||||||
success: true,
|
success: true,
|
||||||
authType: 'session',
|
authType: 'session',
|
||||||
userId: 'test-user-id',
|
userId: 'test-user-id',
|
||||||
@@ -487,7 +465,7 @@ describe('OAuth Token API Routes', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should handle token refresh failure', async () => {
|
it('should handle token refresh failure', async () => {
|
||||||
mockCheckHybridAuth.mockResolvedValueOnce({
|
mockCheckSessionOrInternalAuth.mockResolvedValueOnce({
|
||||||
success: true,
|
success: true,
|
||||||
authType: 'session',
|
authType: 'session',
|
||||||
userId: 'test-user-id',
|
userId: 'test-user-id',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { getCredential, getOAuthToken, refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
import { getCredential, getOAuthToken, refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ export async function POST(request: NextRequest) {
|
|||||||
providerId,
|
providerId,
|
||||||
})
|
})
|
||||||
|
|
||||||
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
|
const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
if (!auth.success || auth.authType !== 'session' || !auth.userId) {
|
if (!auth.success || auth.authType !== 'session' || !auth.userId) {
|
||||||
logger.warn(`[${requestId}] Unauthorized request for credentialAccountUserId path`, {
|
logger.warn(`[${requestId}] Unauthorized request for credentialAccountUserId path`, {
|
||||||
success: auth.success,
|
success: auth.success,
|
||||||
@@ -187,7 +187,7 @@ export async function GET(request: NextRequest) {
|
|||||||
const { credentialId } = parseResult.data
|
const { credentialId } = parseResult.data
|
||||||
|
|
||||||
// For GET requests, we only support session-based authentication
|
// For GET requests, we only support session-based authentication
|
||||||
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
|
const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
if (!auth.success || auth.authType !== 'session' || !auth.userId) {
|
if (!auth.success || auth.authType !== 'session' || !auth.userId) {
|
||||||
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
|
return NextResponse.json({ error: 'User not authenticated' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ function setupFileApiMocks(
|
|||||||
}
|
}
|
||||||
|
|
||||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||||
checkHybridAuth: vi.fn().mockResolvedValue({
|
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||||
success: authenticated,
|
success: authenticated,
|
||||||
userId: authenticated ? 'test-user-id' : undefined,
|
userId: authenticated ? 'test-user-id' : undefined,
|
||||||
error: authenticated ? undefined : 'Unauthorized',
|
error: authenticated ? undefined : 'Unauthorized',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import type { NextRequest } from 'next/server'
|
import type { NextRequest } from 'next/server'
|
||||||
import { NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import type { StorageContext } from '@/lib/uploads/config'
|
import type { StorageContext } from '@/lib/uploads/config'
|
||||||
import { deleteFile, hasCloudStorage } from '@/lib/uploads/core/storage-service'
|
import { deleteFile, hasCloudStorage } from '@/lib/uploads/core/storage-service'
|
||||||
import { extractStorageKey, inferContextFromKey } from '@/lib/uploads/utils/file-utils'
|
import { extractStorageKey, inferContextFromKey } from '@/lib/uploads/utils/file-utils'
|
||||||
@@ -24,7 +24,7 @@ const logger = createLogger('FilesDeleteAPI')
|
|||||||
*/
|
*/
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success || !authResult.userId) {
|
if (!authResult.success || !authResult.userId) {
|
||||||
logger.warn('Unauthorized file delete request', {
|
logger.warn('Unauthorized file delete request', {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import type { StorageContext } from '@/lib/uploads/config'
|
import type { StorageContext } from '@/lib/uploads/config'
|
||||||
import { hasCloudStorage } from '@/lib/uploads/core/storage-service'
|
import { hasCloudStorage } from '@/lib/uploads/core/storage-service'
|
||||||
import { verifyFileAccess } from '@/app/api/files/authorization'
|
import { verifyFileAccess } from '@/app/api/files/authorization'
|
||||||
@@ -12,7 +12,7 @@ export const dynamic = 'force-dynamic'
|
|||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success || !authResult.userId) {
|
if (!authResult.success || !authResult.userId) {
|
||||||
logger.warn('Unauthorized download URL request', {
|
logger.warn('Unauthorized download URL request', {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ function setupFileApiMocks(
|
|||||||
}
|
}
|
||||||
|
|
||||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||||
checkHybridAuth: vi.fn().mockResolvedValue({
|
checkInternalAuth: vi.fn().mockResolvedValue({
|
||||||
success: authenticated,
|
success: authenticated,
|
||||||
userId: authenticated ? 'test-user-id' : undefined,
|
userId: authenticated ? 'test-user-id' : undefined,
|
||||||
error: authenticated ? undefined : 'Unauthorized',
|
error: authenticated ? undefined : 'Unauthorized',
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import path from 'path'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import binaryExtensionsList from 'binary-extensions'
|
import binaryExtensionsList from 'binary-extensions'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import {
|
import {
|
||||||
secureFetchWithPinnedIP,
|
secureFetchWithPinnedIP,
|
||||||
validateUrlWithDNS,
|
validateUrlWithDNS,
|
||||||
@@ -66,7 +66,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const startTime = Date.now()
|
const startTime = Date.now()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: true })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: true })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn('Unauthorized file parse request', {
|
logger.warn('Unauthorized file parse request', {
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ describe('File Serve API Route', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||||
checkHybridAuth: vi.fn().mockResolvedValue({
|
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||||
success: true,
|
success: true,
|
||||||
userId: 'test-user-id',
|
userId: 'test-user-id',
|
||||||
}),
|
}),
|
||||||
@@ -165,7 +165,7 @@ describe('File Serve API Route', () => {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||||
checkHybridAuth: vi.fn().mockResolvedValue({
|
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||||
success: true,
|
success: true,
|
||||||
userId: 'test-user-id',
|
userId: 'test-user-id',
|
||||||
}),
|
}),
|
||||||
@@ -226,7 +226,7 @@ describe('File Serve API Route', () => {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||||
checkHybridAuth: vi.fn().mockResolvedValue({
|
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||||
success: true,
|
success: true,
|
||||||
userId: 'test-user-id',
|
userId: 'test-user-id',
|
||||||
}),
|
}),
|
||||||
@@ -291,7 +291,7 @@ describe('File Serve API Route', () => {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||||
checkHybridAuth: vi.fn().mockResolvedValue({
|
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||||
success: true,
|
success: true,
|
||||||
userId: 'test-user-id',
|
userId: 'test-user-id',
|
||||||
}),
|
}),
|
||||||
@@ -350,7 +350,7 @@ describe('File Serve API Route', () => {
|
|||||||
for (const test of contentTypeTests) {
|
for (const test of contentTypeTests) {
|
||||||
it(`should serve ${test.ext} file with correct content type`, async () => {
|
it(`should serve ${test.ext} file with correct content type`, async () => {
|
||||||
vi.doMock('@/lib/auth/hybrid', () => ({
|
vi.doMock('@/lib/auth/hybrid', () => ({
|
||||||
checkHybridAuth: vi.fn().mockResolvedValue({
|
checkSessionOrInternalAuth: vi.fn().mockResolvedValue({
|
||||||
success: true,
|
success: true,
|
||||||
userId: 'test-user-id',
|
userId: 'test-user-id',
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { readFile } from 'fs/promises'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import type { NextRequest } from 'next/server'
|
import type { NextRequest } from 'next/server'
|
||||||
import { NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { CopilotFiles, isUsingCloudStorage } from '@/lib/uploads'
|
import { CopilotFiles, isUsingCloudStorage } from '@/lib/uploads'
|
||||||
import type { StorageContext } from '@/lib/uploads/config'
|
import type { StorageContext } from '@/lib/uploads/config'
|
||||||
import { downloadFile } from '@/lib/uploads/core/storage-service'
|
import { downloadFile } from '@/lib/uploads/core/storage-service'
|
||||||
@@ -49,7 +49,7 @@ export async function GET(
|
|||||||
return await handleLocalFilePublic(fullPath)
|
return await handleLocalFilePublic(fullPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success || !authResult.userId) {
|
if (!authResult.success || !authResult.userId) {
|
||||||
logger.warn('Unauthorized file access attempt', {
|
logger.warn('Unauthorized file access attempt', {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { randomUUID } from 'crypto'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { SUPPORTED_FIELD_TYPES } from '@/lib/knowledge/constants'
|
import { SUPPORTED_FIELD_TYPES } from '@/lib/knowledge/constants'
|
||||||
import { createTagDefinition, getTagDefinitions } from '@/lib/knowledge/tags/service'
|
import { createTagDefinition, getTagDefinitions } from '@/lib/knowledge/tags/service'
|
||||||
import { checkKnowledgeBaseAccess } from '@/app/api/knowledge/utils'
|
import { checkKnowledgeBaseAccess } from '@/app/api/knowledge/utils'
|
||||||
@@ -19,19 +19,11 @@ export async function GET(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
try {
|
try {
|
||||||
logger.info(`[${requestId}] Getting tag definitions for knowledge base ${knowledgeBaseId}`)
|
logger.info(`[${requestId}] Getting tag definitions for knowledge base ${knowledgeBaseId}`)
|
||||||
|
|
||||||
const auth = await checkHybridAuth(req, { requireWorkflowId: false })
|
const auth = await checkSessionOrInternalAuth(req, { requireWorkflowId: false })
|
||||||
if (!auth.success) {
|
if (!auth.success) {
|
||||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only allow session and internal JWT auth (not API key)
|
|
||||||
if (auth.authType === 'api_key') {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'API key auth not supported for this endpoint' },
|
|
||||||
{ status: 401 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// For session auth, verify KB access. Internal JWT is trusted.
|
// For session auth, verify KB access. Internal JWT is trusted.
|
||||||
if (auth.authType === 'session' && auth.userId) {
|
if (auth.authType === 'session' && auth.userId) {
|
||||||
const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, auth.userId)
|
const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, auth.userId)
|
||||||
@@ -64,19 +56,11 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id:
|
|||||||
try {
|
try {
|
||||||
logger.info(`[${requestId}] Creating tag definition for knowledge base ${knowledgeBaseId}`)
|
logger.info(`[${requestId}] Creating tag definition for knowledge base ${knowledgeBaseId}`)
|
||||||
|
|
||||||
const auth = await checkHybridAuth(req, { requireWorkflowId: false })
|
const auth = await checkSessionOrInternalAuth(req, { requireWorkflowId: false })
|
||||||
if (!auth.success) {
|
if (!auth.success) {
|
||||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only allow session and internal JWT auth (not API key)
|
|
||||||
if (auth.authType === 'api_key') {
|
|
||||||
return NextResponse.json(
|
|
||||||
{ error: 'API key auth not supported for this endpoint' },
|
|
||||||
{ status: 401 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// For session auth, verify KB access. Internal JWT is trusted.
|
// For session auth, verify KB access. Internal JWT is trusted.
|
||||||
if (auth.authType === 'session' && auth.userId) {
|
if (auth.authType === 'session' && auth.userId) {
|
||||||
const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, auth.userId)
|
const accessCheck = await checkKnowledgeBaseAccess(knowledgeBaseId, auth.userId)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { and, eq, inArray } from 'drizzle-orm'
|
import { and, eq, inArray } from 'drizzle-orm'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import type { TraceSpan, WorkflowExecutionLog } from '@/lib/logs/types'
|
import type { TraceSpan, WorkflowExecutionLog } from '@/lib/logs/types'
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ export async function GET(
|
|||||||
try {
|
try {
|
||||||
const { executionId } = await params
|
const { executionId } = await params
|
||||||
|
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
if (!authResult.success || !authResult.userId) {
|
if (!authResult.success || !authResult.userId) {
|
||||||
logger.warn(`[${requestId}] Unauthorized execution data access attempt for: ${executionId}`)
|
logger.warn(`[${requestId}] Unauthorized execution data access attempt for: ${executionId}`)
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { and, eq } from 'drizzle-orm'
|
import { and, eq } from 'drizzle-orm'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ async function validateMemoryAccess(
|
|||||||
requestId: string,
|
requestId: string,
|
||||||
action: 'read' | 'write'
|
action: 'read' | 'write'
|
||||||
): Promise<{ userId: string } | { error: NextResponse }> {
|
): Promise<{ userId: string } | { error: NextResponse }> {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkInternalAuth(request, { requireWorkflowId: false })
|
||||||
if (!authResult.success || !authResult.userId) {
|
if (!authResult.success || !authResult.userId) {
|
||||||
logger.warn(`[${requestId}] Unauthorized memory ${action} attempt`)
|
logger.warn(`[${requestId}] Unauthorized memory ${action} attempt`)
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { memory } from '@sim/db/schema'
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { and, eq, isNull, like } from 'drizzle-orm'
|
import { and, eq, isNull, like } from 'drizzle-orm'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
import { checkWorkspaceAccess } from '@/lib/workspaces/permissions/utils'
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ export async function GET(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request)
|
const authResult = await checkInternalAuth(request)
|
||||||
if (!authResult.success || !authResult.userId) {
|
if (!authResult.success || !authResult.userId) {
|
||||||
logger.warn(`[${requestId}] Unauthorized memory access attempt`)
|
logger.warn(`[${requestId}] Unauthorized memory access attempt`)
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -89,7 +89,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request)
|
const authResult = await checkInternalAuth(request)
|
||||||
if (!authResult.success || !authResult.userId) {
|
if (!authResult.success || !authResult.userId) {
|
||||||
logger.warn(`[${requestId}] Unauthorized memory creation attempt`)
|
logger.warn(`[${requestId}] Unauthorized memory creation attempt`)
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -228,7 +228,7 @@ export async function DELETE(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request)
|
const authResult = await checkInternalAuth(request)
|
||||||
if (!authResult.success || !authResult.userId) {
|
if (!authResult.success || !authResult.userId) {
|
||||||
logger.warn(`[${requestId}] Unauthorized memory deletion attempt`)
|
logger.warn(`[${requestId}] Unauthorized memory deletion attempt`)
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { createA2AClient } from '@/lib/a2a/utils'
|
import { createA2AClient } from '@/lib/a2a/utils'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
const logger = createLogger('A2ACancelTaskAPI')
|
const logger = createLogger('A2ACancelTaskAPI')
|
||||||
@@ -20,7 +20,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized A2A cancel task attempt`)
|
logger.warn(`[${requestId}] Unauthorized A2A cancel task attempt`)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { createA2AClient } from '@/lib/a2a/utils'
|
import { createA2AClient } from '@/lib/a2a/utils'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -20,7 +20,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { createA2AClient } from '@/lib/a2a/utils'
|
import { createA2AClient } from '@/lib/a2a/utils'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -18,7 +18,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized A2A get agent card attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized A2A get agent card attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { createA2AClient } from '@/lib/a2a/utils'
|
import { createA2AClient } from '@/lib/a2a/utils'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -19,7 +19,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { createA2AClient } from '@/lib/a2a/utils'
|
import { createA2AClient } from '@/lib/a2a/utils'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'
|
export const dynamic = 'force-dynamic'
|
||||||
@@ -21,7 +21,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized A2A get task attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized A2A get task attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { createA2AClient, extractTextContent, isTerminalState } from '@/lib/a2a/utils'
|
import { createA2AClient, extractTextContent, isTerminalState } from '@/lib/a2a/utils'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
const logger = createLogger('A2AResubscribeAPI')
|
const logger = createLogger('A2AResubscribeAPI')
|
||||||
@@ -27,7 +27,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized A2A resubscribe attempt`)
|
logger.warn(`[${requestId}] Unauthorized A2A resubscribe attempt`)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { createA2AClient, extractTextContent, isTerminalState } from '@/lib/a2a/utils'
|
import { createA2AClient, extractTextContent, isTerminalState } from '@/lib/a2a/utils'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server'
|
import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized A2A send message attempt: ${authResult.error}`)
|
logger.warn(`[${requestId}] Unauthorized A2A send message attempt: ${authResult.error}`)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
|
|||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { createA2AClient } from '@/lib/a2a/utils'
|
import { createA2AClient } from '@/lib/a2a/utils'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server'
|
import { validateUrlWithDNS } from '@/lib/core/security/input-validation.server'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const authResult = await checkHybridAuth(request, { requireWorkflowId: false })
|
const authResult = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!authResult.success) {
|
if (!authResult.success) {
|
||||||
logger.warn(`[${requestId}] Unauthorized A2A set push notification attempt`, {
|
logger.warn(`[${requestId}] Unauthorized A2A set push notification attempt`, {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { type NextRequest, NextResponse } from 'next/server'
|
import { type NextRequest, NextResponse } from 'next/server'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { getUserUsageLogs, type UsageLogSource } from '@/lib/billing/core/usage-log'
|
import { getUserUsageLogs, type UsageLogSource } from '@/lib/billing/core/usage-log'
|
||||||
|
|
||||||
const logger = createLogger('UsageLogsAPI')
|
const logger = createLogger('UsageLogsAPI')
|
||||||
@@ -20,7 +20,7 @@ const QuerySchema = z.object({
|
|||||||
*/
|
*/
|
||||||
export async function GET(req: NextRequest) {
|
export async function GET(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const auth = await checkHybridAuth(req, { requireWorkflowId: false })
|
const auth = await checkSessionOrInternalAuth(req, { requireWorkflowId: false })
|
||||||
|
|
||||||
if (!auth.success || !auth.userId) {
|
if (!auth.success || !auth.userId) {
|
||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
||||||
|
|||||||
@@ -74,8 +74,7 @@ function FileCard({ file, isExecutionFile = false, workspaceId }: FileCardProps)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isExecutionFile) {
|
if (isExecutionFile) {
|
||||||
const serveUrl =
|
const serveUrl = `/api/files/serve/${encodeURIComponent(file.key)}?context=execution`
|
||||||
file.url || `/api/files/serve/${encodeURIComponent(file.key)}?context=execution`
|
|
||||||
window.open(serveUrl, '_blank')
|
window.open(serveUrl, '_blank')
|
||||||
logger.info(`Opened execution file serve URL: ${serveUrl}`)
|
logger.info(`Opened execution file serve URL: ${serveUrl}`)
|
||||||
} else {
|
} else {
|
||||||
@@ -88,16 +87,12 @@ function FileCard({ file, isExecutionFile = false, workspaceId }: FileCardProps)
|
|||||||
logger.warn(
|
logger.warn(
|
||||||
`Could not construct viewer URL for file: ${file.name}, falling back to serve URL`
|
`Could not construct viewer URL for file: ${file.name}, falling back to serve URL`
|
||||||
)
|
)
|
||||||
const serveUrl =
|
const serveUrl = `/api/files/serve/${encodeURIComponent(file.key)}?context=workspace`
|
||||||
file.url || `/api/files/serve/${encodeURIComponent(file.key)}?context=workspace`
|
|
||||||
window.open(serveUrl, '_blank')
|
window.open(serveUrl, '_blank')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to download file ${file.name}:`, error)
|
logger.error(`Failed to download file ${file.name}:`, error)
|
||||||
if (file.url) {
|
|
||||||
window.open(file.url, '_blank')
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsDownloading(false)
|
setIsDownloading(false)
|
||||||
}
|
}
|
||||||
@@ -198,8 +193,7 @@ export function FileDownload({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isExecutionFile) {
|
if (isExecutionFile) {
|
||||||
const serveUrl =
|
const serveUrl = `/api/files/serve/${encodeURIComponent(file.key)}?context=execution`
|
||||||
file.url || `/api/files/serve/${encodeURIComponent(file.key)}?context=execution`
|
|
||||||
window.open(serveUrl, '_blank')
|
window.open(serveUrl, '_blank')
|
||||||
logger.info(`Opened execution file serve URL: ${serveUrl}`)
|
logger.info(`Opened execution file serve URL: ${serveUrl}`)
|
||||||
} else {
|
} else {
|
||||||
@@ -212,16 +206,12 @@ export function FileDownload({
|
|||||||
logger.warn(
|
logger.warn(
|
||||||
`Could not construct viewer URL for file: ${file.name}, falling back to serve URL`
|
`Could not construct viewer URL for file: ${file.name}, falling back to serve URL`
|
||||||
)
|
)
|
||||||
const serveUrl =
|
const serveUrl = `/api/files/serve/${encodeURIComponent(file.key)}?context=workspace`
|
||||||
file.url || `/api/files/serve/${encodeURIComponent(file.key)}?context=workspace`
|
|
||||||
window.open(serveUrl, '_blank')
|
window.open(serveUrl, '_blank')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to download file ${file.name}:`, error)
|
logger.error(`Failed to download file ${file.name}:`, error)
|
||||||
if (file.url) {
|
|
||||||
window.open(file.url, '_blank')
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
setIsDownloading(false)
|
setIsDownloading(false)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export function WorkflowSelector({
|
|||||||
onMouseDown={(e) => handleRemove(e, w.id)}
|
onMouseDown={(e) => handleRemove(e, w.id)}
|
||||||
>
|
>
|
||||||
{w.name}
|
{w.name}
|
||||||
<X className='h-3 w-3' />
|
<X className='!text-[var(--text-primary)] h-4 w-4 flex-shrink-0 opacity-50' />
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
{selectedWorkflows.length > 2 && (
|
{selectedWorkflows.length > 2 && (
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ interface CredentialSelectorProps {
|
|||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
isPreview?: boolean
|
isPreview?: boolean
|
||||||
previewValue?: any | null
|
previewValue?: any | null
|
||||||
|
previewContextValues?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CredentialSelector({
|
export function CredentialSelector({
|
||||||
@@ -43,6 +44,7 @@ export function CredentialSelector({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
isPreview = false,
|
isPreview = false,
|
||||||
previewValue,
|
previewValue,
|
||||||
|
previewContextValues,
|
||||||
}: CredentialSelectorProps) {
|
}: CredentialSelectorProps) {
|
||||||
const [showOAuthModal, setShowOAuthModal] = useState(false)
|
const [showOAuthModal, setShowOAuthModal] = useState(false)
|
||||||
const [editingValue, setEditingValue] = useState('')
|
const [editingValue, setEditingValue] = useState('')
|
||||||
@@ -67,7 +69,11 @@ export function CredentialSelector({
|
|||||||
canUseCredentialSets
|
canUseCredentialSets
|
||||||
)
|
)
|
||||||
|
|
||||||
const { depsSatisfied, dependsOn } = useDependsOnGate(blockId, subBlock, { disabled, isPreview })
|
const { depsSatisfied, dependsOn } = useDependsOnGate(blockId, subBlock, {
|
||||||
|
disabled,
|
||||||
|
isPreview,
|
||||||
|
previewContextValues,
|
||||||
|
})
|
||||||
const hasDependencies = dependsOn.length > 0
|
const hasDependencies = dependsOn.length > 0
|
||||||
|
|
||||||
const effectiveDisabled = disabled || (hasDependencies && !depsSatisfied)
|
const effectiveDisabled = disabled || (hasDependencies && !depsSatisfied)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Tooltip } from '@/components/emcn'
|
|||||||
import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox'
|
import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox'
|
||||||
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
||||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||||
|
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||||
import type { SubBlockConfig } from '@/blocks/types'
|
import type { SubBlockConfig } from '@/blocks/types'
|
||||||
import type { SelectorContext } from '@/hooks/selectors/types'
|
import type { SelectorContext } from '@/hooks/selectors/types'
|
||||||
|
|
||||||
@@ -33,7 +34,9 @@ export function DocumentSelector({
|
|||||||
previewContextValues,
|
previewContextValues,
|
||||||
})
|
})
|
||||||
const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId')
|
const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId')
|
||||||
const knowledgeBaseIdValue = previewContextValues?.knowledgeBaseId ?? knowledgeBaseIdFromStore
|
const knowledgeBaseIdValue = previewContextValues
|
||||||
|
? resolvePreviewContextValue(previewContextValues.knowledgeBaseId)
|
||||||
|
: knowledgeBaseIdFromStore
|
||||||
const normalizedKnowledgeBaseId =
|
const normalizedKnowledgeBaseId =
|
||||||
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
|
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
|
||||||
? knowledgeBaseIdValue
|
? knowledgeBaseIdValue
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/
|
|||||||
import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
|
import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
|
||||||
import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input'
|
import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input'
|
||||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||||
|
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||||
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
||||||
import type { SubBlockConfig } from '@/blocks/types'
|
import type { SubBlockConfig } from '@/blocks/types'
|
||||||
import { useKnowledgeBaseTagDefinitions } from '@/hooks/kb/use-knowledge-base-tag-definitions'
|
import { useKnowledgeBaseTagDefinitions } from '@/hooks/kb/use-knowledge-base-tag-definitions'
|
||||||
@@ -77,7 +78,9 @@ export function DocumentTagEntry({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId')
|
const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId')
|
||||||
const knowledgeBaseIdValue = previewContextValues?.knowledgeBaseId ?? knowledgeBaseIdFromStore
|
const knowledgeBaseIdValue = previewContextValues
|
||||||
|
? resolvePreviewContextValue(previewContextValues.knowledgeBaseId)
|
||||||
|
: knowledgeBaseIdFromStore
|
||||||
const knowledgeBaseId =
|
const knowledgeBaseId =
|
||||||
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
|
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
|
||||||
? knowledgeBaseIdValue
|
? knowledgeBaseIdValue
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/c
|
|||||||
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
||||||
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential'
|
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential'
|
||||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||||
|
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||||
import { getBlock } from '@/blocks/registry'
|
import { getBlock } from '@/blocks/registry'
|
||||||
import type { SubBlockConfig } from '@/blocks/types'
|
import type { SubBlockConfig } from '@/blocks/types'
|
||||||
import { isDependency } from '@/blocks/utils'
|
import { isDependency } from '@/blocks/utils'
|
||||||
@@ -62,42 +63,56 @@ export function FileSelectorInput({
|
|||||||
|
|
||||||
const [domainValueFromStore] = useSubBlockValue(blockId, 'domain')
|
const [domainValueFromStore] = useSubBlockValue(blockId, 'domain')
|
||||||
|
|
||||||
const connectedCredential = previewContextValues?.credential ?? blockValues.credential
|
const connectedCredential = previewContextValues
|
||||||
const domainValue = previewContextValues?.domain ?? domainValueFromStore
|
? resolvePreviewContextValue(previewContextValues.credential)
|
||||||
|
: blockValues.credential
|
||||||
|
const domainValue = previewContextValues
|
||||||
|
? resolvePreviewContextValue(previewContextValues.domain)
|
||||||
|
: domainValueFromStore
|
||||||
|
|
||||||
const teamIdValue = useMemo(
|
const teamIdValue = useMemo(
|
||||||
() =>
|
() =>
|
||||||
previewContextValues?.teamId ??
|
previewContextValues
|
||||||
resolveDependencyValue('teamId', blockValues, canonicalIndex, canonicalModeOverrides),
|
? resolvePreviewContextValue(previewContextValues.teamId)
|
||||||
[previewContextValues?.teamId, blockValues, canonicalIndex, canonicalModeOverrides]
|
: resolveDependencyValue('teamId', blockValues, canonicalIndex, canonicalModeOverrides),
|
||||||
|
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
|
||||||
)
|
)
|
||||||
|
|
||||||
const siteIdValue = useMemo(
|
const siteIdValue = useMemo(
|
||||||
() =>
|
() =>
|
||||||
previewContextValues?.siteId ??
|
previewContextValues
|
||||||
resolveDependencyValue('siteId', blockValues, canonicalIndex, canonicalModeOverrides),
|
? resolvePreviewContextValue(previewContextValues.siteId)
|
||||||
[previewContextValues?.siteId, blockValues, canonicalIndex, canonicalModeOverrides]
|
: resolveDependencyValue('siteId', blockValues, canonicalIndex, canonicalModeOverrides),
|
||||||
|
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
|
||||||
)
|
)
|
||||||
|
|
||||||
const collectionIdValue = useMemo(
|
const collectionIdValue = useMemo(
|
||||||
() =>
|
() =>
|
||||||
previewContextValues?.collectionId ??
|
previewContextValues
|
||||||
resolveDependencyValue('collectionId', blockValues, canonicalIndex, canonicalModeOverrides),
|
? resolvePreviewContextValue(previewContextValues.collectionId)
|
||||||
[previewContextValues?.collectionId, blockValues, canonicalIndex, canonicalModeOverrides]
|
: resolveDependencyValue(
|
||||||
|
'collectionId',
|
||||||
|
blockValues,
|
||||||
|
canonicalIndex,
|
||||||
|
canonicalModeOverrides
|
||||||
|
),
|
||||||
|
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
|
||||||
)
|
)
|
||||||
|
|
||||||
const projectIdValue = useMemo(
|
const projectIdValue = useMemo(
|
||||||
() =>
|
() =>
|
||||||
previewContextValues?.projectId ??
|
previewContextValues
|
||||||
resolveDependencyValue('projectId', blockValues, canonicalIndex, canonicalModeOverrides),
|
? resolvePreviewContextValue(previewContextValues.projectId)
|
||||||
[previewContextValues?.projectId, blockValues, canonicalIndex, canonicalModeOverrides]
|
: resolveDependencyValue('projectId', blockValues, canonicalIndex, canonicalModeOverrides),
|
||||||
|
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
|
||||||
)
|
)
|
||||||
|
|
||||||
const planIdValue = useMemo(
|
const planIdValue = useMemo(
|
||||||
() =>
|
() =>
|
||||||
previewContextValues?.planId ??
|
previewContextValues
|
||||||
resolveDependencyValue('planId', blockValues, canonicalIndex, canonicalModeOverrides),
|
? resolvePreviewContextValue(previewContextValues.planId)
|
||||||
[previewContextValues?.planId, blockValues, canonicalIndex, canonicalModeOverrides]
|
: resolveDependencyValue('planId', blockValues, canonicalIndex, canonicalModeOverrides),
|
||||||
|
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
|
||||||
)
|
)
|
||||||
|
|
||||||
const normalizedCredentialId =
|
const normalizedCredentialId =
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/c
|
|||||||
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
||||||
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential'
|
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential'
|
||||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||||
|
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||||
import type { SubBlockConfig } from '@/blocks/types'
|
import type { SubBlockConfig } from '@/blocks/types'
|
||||||
import { resolveSelectorForSubBlock } from '@/hooks/selectors/resolution'
|
import { resolveSelectorForSubBlock } from '@/hooks/selectors/resolution'
|
||||||
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
||||||
@@ -17,6 +18,7 @@ interface FolderSelectorInputProps {
|
|||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
isPreview?: boolean
|
isPreview?: boolean
|
||||||
previewValue?: any | null
|
previewValue?: any | null
|
||||||
|
previewContextValues?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FolderSelectorInput({
|
export function FolderSelectorInput({
|
||||||
@@ -25,9 +27,13 @@ export function FolderSelectorInput({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
isPreview = false,
|
isPreview = false,
|
||||||
previewValue,
|
previewValue,
|
||||||
|
previewContextValues,
|
||||||
}: FolderSelectorInputProps) {
|
}: FolderSelectorInputProps) {
|
||||||
const [storeValue] = useSubBlockValue(blockId, subBlock.id)
|
const [storeValue] = useSubBlockValue(blockId, subBlock.id)
|
||||||
const [connectedCredential] = useSubBlockValue(blockId, 'credential')
|
const [credentialFromStore] = useSubBlockValue(blockId, 'credential')
|
||||||
|
const connectedCredential = previewContextValues
|
||||||
|
? resolvePreviewContextValue(previewContextValues.credential)
|
||||||
|
: credentialFromStore
|
||||||
const { collaborativeSetSubblockValue } = useCollaborativeWorkflow()
|
const { collaborativeSetSubblockValue } = useCollaborativeWorkflow()
|
||||||
const { activeWorkflowId } = useWorkflowRegistry()
|
const { activeWorkflowId } = useWorkflowRegistry()
|
||||||
const [selectedFolderId, setSelectedFolderId] = useState<string>('')
|
const [selectedFolderId, setSelectedFolderId] = useState<string>('')
|
||||||
@@ -47,7 +53,11 @@ export function FolderSelectorInput({
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Central dependsOn gating
|
// Central dependsOn gating
|
||||||
const { finalDisabled } = useDependsOnGate(blockId, subBlock, { disabled, isPreview })
|
const { finalDisabled } = useDependsOnGate(blockId, subBlock, {
|
||||||
|
disabled,
|
||||||
|
isPreview,
|
||||||
|
previewContextValues,
|
||||||
|
})
|
||||||
|
|
||||||
// Get the current value from the store or prop value if in preview mode
|
// Get the current value from the store or prop value if in preview mode
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/
|
|||||||
import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
|
import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
|
||||||
import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input'
|
import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input'
|
||||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||||
|
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||||
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
||||||
import { useWorkflowState } from '@/hooks/queries/workflows'
|
import { useWorkflowState } from '@/hooks/queries/workflows'
|
||||||
|
|
||||||
@@ -37,6 +38,8 @@ interface InputMappingProps {
|
|||||||
isPreview?: boolean
|
isPreview?: boolean
|
||||||
previewValue?: Record<string, unknown>
|
previewValue?: Record<string, unknown>
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
/** Sub-block values from the preview context for resolving sibling sub-block values */
|
||||||
|
previewContextValues?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,9 +53,13 @@ export function InputMapping({
|
|||||||
isPreview = false,
|
isPreview = false,
|
||||||
previewValue,
|
previewValue,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
|
previewContextValues,
|
||||||
}: InputMappingProps) {
|
}: InputMappingProps) {
|
||||||
const [mapping, setMapping] = useSubBlockValue(blockId, subBlockId)
|
const [mapping, setMapping] = useSubBlockValue(blockId, subBlockId)
|
||||||
const [selectedWorkflowId] = useSubBlockValue(blockId, 'workflowId')
|
const [storeWorkflowId] = useSubBlockValue(blockId, 'workflowId')
|
||||||
|
const selectedWorkflowId = previewContextValues
|
||||||
|
? resolvePreviewContextValue(previewContextValues.workflowId)
|
||||||
|
: storeWorkflowId
|
||||||
|
|
||||||
const inputController = useSubBlockInput({
|
const inputController = useSubBlockInput({
|
||||||
blockId,
|
blockId,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { type FilterFieldType, getOperatorsForFieldType } from '@/lib/knowledge/
|
|||||||
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
|
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
|
||||||
import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
|
import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
|
||||||
import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input'
|
import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input'
|
||||||
|
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||||
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
||||||
import type { SubBlockConfig } from '@/blocks/types'
|
import type { SubBlockConfig } from '@/blocks/types'
|
||||||
import { useKnowledgeBaseTagDefinitions } from '@/hooks/kb/use-knowledge-base-tag-definitions'
|
import { useKnowledgeBaseTagDefinitions } from '@/hooks/kb/use-knowledge-base-tag-definitions'
|
||||||
@@ -69,7 +70,9 @@ export function KnowledgeTagFilters({
|
|||||||
const overlayRefs = useRef<Record<string, HTMLDivElement>>({})
|
const overlayRefs = useRef<Record<string, HTMLDivElement>>({})
|
||||||
|
|
||||||
const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId')
|
const [knowledgeBaseIdFromStore] = useSubBlockValue(blockId, 'knowledgeBaseId')
|
||||||
const knowledgeBaseIdValue = previewContextValues?.knowledgeBaseId ?? knowledgeBaseIdFromStore
|
const knowledgeBaseIdValue = previewContextValues
|
||||||
|
? resolvePreviewContextValue(previewContextValues.knowledgeBaseId)
|
||||||
|
: knowledgeBaseIdFromStore
|
||||||
const knowledgeBaseId =
|
const knowledgeBaseId =
|
||||||
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
|
typeof knowledgeBaseIdValue === 'string' && knowledgeBaseIdValue.trim().length > 0
|
||||||
? knowledgeBaseIdValue
|
? knowledgeBaseIdValue
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { cn } from '@/lib/core/utils/cn'
|
|||||||
import { LongInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/long-input/long-input'
|
import { LongInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/long-input/long-input'
|
||||||
import { ShortInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input'
|
import { ShortInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input'
|
||||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||||
|
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||||
import type { SubBlockConfig } from '@/blocks/types'
|
import type { SubBlockConfig } from '@/blocks/types'
|
||||||
import { useMcpTools } from '@/hooks/mcp/use-mcp-tools'
|
import { useMcpTools } from '@/hooks/mcp/use-mcp-tools'
|
||||||
import { formatParameterLabel } from '@/tools/params'
|
import { formatParameterLabel } from '@/tools/params'
|
||||||
@@ -18,6 +19,7 @@ interface McpDynamicArgsProps {
|
|||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
isPreview?: boolean
|
isPreview?: boolean
|
||||||
previewValue?: any
|
previewValue?: any
|
||||||
|
previewContextValues?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,12 +49,19 @@ export function McpDynamicArgs({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
isPreview = false,
|
isPreview = false,
|
||||||
previewValue,
|
previewValue,
|
||||||
|
previewContextValues,
|
||||||
}: McpDynamicArgsProps) {
|
}: McpDynamicArgsProps) {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const workspaceId = params.workspaceId as string
|
const workspaceId = params.workspaceId as string
|
||||||
const { mcpTools, isLoading } = useMcpTools(workspaceId)
|
const { mcpTools, isLoading } = useMcpTools(workspaceId)
|
||||||
const [selectedTool] = useSubBlockValue(blockId, 'tool')
|
const [toolFromStore] = useSubBlockValue(blockId, 'tool')
|
||||||
const [cachedSchema] = useSubBlockValue(blockId, '_toolSchema')
|
const selectedTool = previewContextValues
|
||||||
|
? resolvePreviewContextValue(previewContextValues.tool)
|
||||||
|
: toolFromStore
|
||||||
|
const [schemaFromStore] = useSubBlockValue(blockId, '_toolSchema')
|
||||||
|
const cachedSchema = previewContextValues
|
||||||
|
? resolvePreviewContextValue(previewContextValues._toolSchema)
|
||||||
|
: schemaFromStore
|
||||||
const [toolArgs, setToolArgs] = useSubBlockValue(blockId, subBlockId)
|
const [toolArgs, setToolArgs] = useSubBlockValue(blockId, subBlockId)
|
||||||
|
|
||||||
const selectedToolConfig = mcpTools.find((tool) => tool.id === selectedTool)
|
const selectedToolConfig = mcpTools.find((tool) => tool.id === selectedTool)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useEffect, useMemo, useState } from 'react'
|
|||||||
import { useParams } from 'next/navigation'
|
import { useParams } from 'next/navigation'
|
||||||
import { Combobox } from '@/components/emcn/components'
|
import { Combobox } from '@/components/emcn/components'
|
||||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||||
|
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||||
import type { SubBlockConfig } from '@/blocks/types'
|
import type { SubBlockConfig } from '@/blocks/types'
|
||||||
import { useMcpTools } from '@/hooks/mcp/use-mcp-tools'
|
import { useMcpTools } from '@/hooks/mcp/use-mcp-tools'
|
||||||
|
|
||||||
@@ -13,6 +14,7 @@ interface McpToolSelectorProps {
|
|||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
isPreview?: boolean
|
isPreview?: boolean
|
||||||
previewValue?: string | null
|
previewValue?: string | null
|
||||||
|
previewContextValues?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function McpToolSelector({
|
export function McpToolSelector({
|
||||||
@@ -21,6 +23,7 @@ export function McpToolSelector({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
isPreview = false,
|
isPreview = false,
|
||||||
previewValue,
|
previewValue,
|
||||||
|
previewContextValues,
|
||||||
}: McpToolSelectorProps) {
|
}: McpToolSelectorProps) {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const workspaceId = params.workspaceId as string
|
const workspaceId = params.workspaceId as string
|
||||||
@@ -31,7 +34,10 @@ export function McpToolSelector({
|
|||||||
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id)
|
const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlock.id)
|
||||||
const [, setSchemaCache] = useSubBlockValue(blockId, '_toolSchema')
|
const [, setSchemaCache] = useSubBlockValue(blockId, '_toolSchema')
|
||||||
|
|
||||||
const [serverValue] = useSubBlockValue(blockId, 'server')
|
const [serverFromStore] = useSubBlockValue(blockId, 'server')
|
||||||
|
const serverValue = previewContextValues
|
||||||
|
? resolvePreviewContextValue(previewContextValues.server)
|
||||||
|
: serverFromStore
|
||||||
|
|
||||||
const label = subBlock.placeholder || 'Select tool'
|
const label = subBlock.placeholder || 'Select tool'
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/c
|
|||||||
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
||||||
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential'
|
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential'
|
||||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||||
|
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||||
import { getBlock } from '@/blocks/registry'
|
import { getBlock } from '@/blocks/registry'
|
||||||
import type { SubBlockConfig } from '@/blocks/types'
|
import type { SubBlockConfig } from '@/blocks/types'
|
||||||
import { resolveSelectorForSubBlock } from '@/hooks/selectors/resolution'
|
import { resolveSelectorForSubBlock } from '@/hooks/selectors/resolution'
|
||||||
@@ -55,14 +56,19 @@ export function ProjectSelectorInput({
|
|||||||
return (workflowValues as Record<string, Record<string, unknown>>)[blockId] || {}
|
return (workflowValues as Record<string, Record<string, unknown>>)[blockId] || {}
|
||||||
})
|
})
|
||||||
|
|
||||||
const connectedCredential = previewContextValues?.credential ?? blockValues.credential
|
const connectedCredential = previewContextValues
|
||||||
const jiraDomain = previewContextValues?.domain ?? jiraDomainFromStore
|
? resolvePreviewContextValue(previewContextValues.credential)
|
||||||
|
: blockValues.credential
|
||||||
|
const jiraDomain = previewContextValues
|
||||||
|
? resolvePreviewContextValue(previewContextValues.domain)
|
||||||
|
: jiraDomainFromStore
|
||||||
|
|
||||||
const linearTeamId = useMemo(
|
const linearTeamId = useMemo(
|
||||||
() =>
|
() =>
|
||||||
previewContextValues?.teamId ??
|
previewContextValues
|
||||||
resolveDependencyValue('teamId', blockValues, canonicalIndex, canonicalModeOverrides),
|
? resolvePreviewContextValue(previewContextValues.teamId)
|
||||||
[previewContextValues?.teamId, blockValues, canonicalIndex, canonicalModeOverrides]
|
: resolveDependencyValue('teamId', blockValues, canonicalIndex, canonicalModeOverrides),
|
||||||
|
[previewContextValues, blockValues, canonicalIndex, canonicalModeOverrides]
|
||||||
)
|
)
|
||||||
|
|
||||||
const serviceId = subBlock.serviceId || ''
|
const serviceId = subBlock.serviceId || ''
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { buildCanonicalIndex, resolveDependencyValue } from '@/lib/workflows/sub
|
|||||||
import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox'
|
import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/selector-combobox/selector-combobox'
|
||||||
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
||||||
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential'
|
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential'
|
||||||
|
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||||
import { getBlock } from '@/blocks/registry'
|
import { getBlock } from '@/blocks/registry'
|
||||||
import type { SubBlockConfig } from '@/blocks/types'
|
import type { SubBlockConfig } from '@/blocks/types'
|
||||||
import { resolveSelectorForSubBlock, type SelectorResolution } from '@/hooks/selectors/resolution'
|
import { resolveSelectorForSubBlock, type SelectorResolution } from '@/hooks/selectors/resolution'
|
||||||
@@ -66,9 +67,12 @@ export function SheetSelectorInput({
|
|||||||
[blockValues, canonicalIndex, canonicalModeOverrides]
|
[blockValues, canonicalIndex, canonicalModeOverrides]
|
||||||
)
|
)
|
||||||
|
|
||||||
const connectedCredential = previewContextValues?.credential ?? connectedCredentialFromStore
|
const connectedCredential = previewContextValues
|
||||||
|
? resolvePreviewContextValue(previewContextValues.credential)
|
||||||
|
: connectedCredentialFromStore
|
||||||
const spreadsheetId = previewContextValues
|
const spreadsheetId = previewContextValues
|
||||||
? (previewContextValues.spreadsheetId ?? previewContextValues.manualSpreadsheetId)
|
? (resolvePreviewContextValue(previewContextValues.spreadsheetId) ??
|
||||||
|
resolvePreviewContextValue(previewContextValues.manualSpreadsheetId))
|
||||||
: spreadsheetIdFromStore
|
: spreadsheetIdFromStore
|
||||||
|
|
||||||
const normalizedCredentialId =
|
const normalizedCredentialId =
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { SelectorCombobox } from '@/app/workspace/[workspaceId]/w/[workflowId]/c
|
|||||||
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
|
||||||
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential'
|
import { useForeignCredential } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-foreign-credential'
|
||||||
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
|
||||||
|
import { resolvePreviewContextValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/utils'
|
||||||
import type { SubBlockConfig } from '@/blocks/types'
|
import type { SubBlockConfig } from '@/blocks/types'
|
||||||
import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types'
|
import type { SelectorContext, SelectorKey } from '@/hooks/selectors/types'
|
||||||
|
|
||||||
@@ -58,9 +59,15 @@ export function SlackSelectorInput({
|
|||||||
const [botToken] = useSubBlockValue(blockId, 'botToken')
|
const [botToken] = useSubBlockValue(blockId, 'botToken')
|
||||||
const [connectedCredential] = useSubBlockValue(blockId, 'credential')
|
const [connectedCredential] = useSubBlockValue(blockId, 'credential')
|
||||||
|
|
||||||
const effectiveAuthMethod = previewContextValues?.authMethod ?? authMethod
|
const effectiveAuthMethod = previewContextValues
|
||||||
const effectiveBotToken = previewContextValues?.botToken ?? botToken
|
? resolvePreviewContextValue(previewContextValues.authMethod)
|
||||||
const effectiveCredential = previewContextValues?.credential ?? connectedCredential
|
: authMethod
|
||||||
|
const effectiveBotToken = previewContextValues
|
||||||
|
? resolvePreviewContextValue(previewContextValues.botToken)
|
||||||
|
: botToken
|
||||||
|
const effectiveCredential = previewContextValues
|
||||||
|
? resolvePreviewContextValue(previewContextValues.credential)
|
||||||
|
: connectedCredential
|
||||||
const [_selectedValue, setSelectedValue] = useState<string | null>(null)
|
const [_selectedValue, setSelectedValue] = useState<string | null>(null)
|
||||||
|
|
||||||
const serviceId = subBlock.serviceId || ''
|
const serviceId = subBlock.serviceId || ''
|
||||||
|
|||||||
@@ -332,6 +332,7 @@ function FolderSelectorSyncWrapper({
|
|||||||
dependsOn: uiComponent.dependsOn,
|
dependsOn: uiComponent.dependsOn,
|
||||||
}}
|
}}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
previewContextValues={previewContextValues}
|
||||||
/>
|
/>
|
||||||
</GenericSyncWrapper>
|
</GenericSyncWrapper>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -797,6 +797,7 @@ function SubBlockComponent({
|
|||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
isPreview={isPreview}
|
isPreview={isPreview}
|
||||||
previewValue={previewValue}
|
previewValue={previewValue}
|
||||||
|
previewContextValues={isPreview ? subBlockValues : undefined}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -832,6 +833,7 @@ function SubBlockComponent({
|
|||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
isPreview={isPreview}
|
isPreview={isPreview}
|
||||||
previewValue={previewValue}
|
previewValue={previewValue}
|
||||||
|
previewContextValues={isPreview ? subBlockValues : undefined}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -843,6 +845,7 @@ function SubBlockComponent({
|
|||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
isPreview={isPreview}
|
isPreview={isPreview}
|
||||||
previewValue={previewValue}
|
previewValue={previewValue}
|
||||||
|
previewContextValues={isPreview ? subBlockValues : undefined}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -865,6 +868,7 @@ function SubBlockComponent({
|
|||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
isPreview={isPreview}
|
isPreview={isPreview}
|
||||||
previewValue={previewValue as any}
|
previewValue={previewValue as any}
|
||||||
|
previewContextValues={isPreview ? subBlockValues : undefined}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -876,6 +880,7 @@ function SubBlockComponent({
|
|||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
isPreview={isPreview}
|
isPreview={isPreview}
|
||||||
previewValue={previewValue as any}
|
previewValue={previewValue as any}
|
||||||
|
previewContextValues={isPreview ? subBlockValues : undefined}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -887,6 +892,7 @@ function SubBlockComponent({
|
|||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
isPreview={isPreview}
|
isPreview={isPreview}
|
||||||
previewValue={previewValue as any}
|
previewValue={previewValue as any}
|
||||||
|
previewContextValues={isPreview ? subBlockValues : undefined}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -911,6 +917,7 @@ function SubBlockComponent({
|
|||||||
isPreview={isPreview}
|
isPreview={isPreview}
|
||||||
previewValue={previewValue as any}
|
previewValue={previewValue as any}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
|
previewContextValues={isPreview ? subBlockValues : undefined}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -946,6 +953,7 @@ function SubBlockComponent({
|
|||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
isPreview={isPreview}
|
isPreview={isPreview}
|
||||||
previewValue={previewValue}
|
previewValue={previewValue}
|
||||||
|
previewContextValues={isPreview ? subBlockValues : undefined}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -979,6 +987,7 @@ function SubBlockComponent({
|
|||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
isPreview={isPreview}
|
isPreview={isPreview}
|
||||||
previewValue={previewValue as any}
|
previewValue={previewValue as any}
|
||||||
|
previewContextValues={isPreview ? subBlockValues : undefined}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -990,6 +999,7 @@ function SubBlockComponent({
|
|||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
isPreview={isPreview}
|
isPreview={isPreview}
|
||||||
previewValue={previewValue}
|
previewValue={previewValue}
|
||||||
|
previewContextValues={isPreview ? subBlockValues : undefined}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/**
|
||||||
|
* Extracts the raw value from a preview context entry.
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* In the sub-block preview context, values are wrapped as `{ value: T }` objects
|
||||||
|
* (the full sub-block state). In the tool-input preview context, values are already
|
||||||
|
* raw. This function normalizes both cases to return the underlying value.
|
||||||
|
*
|
||||||
|
* @param raw - The preview context entry, which may be a raw value or a `{ value: T }` wrapper
|
||||||
|
* @returns The unwrapped value, or `null` if the input is nullish
|
||||||
|
*/
|
||||||
|
export function resolvePreviewContextValue(raw: unknown): unknown {
|
||||||
|
if (raw === null || raw === undefined) return null
|
||||||
|
if (typeof raw === 'object' && !Array.isArray(raw) && 'value' in raw) {
|
||||||
|
return (raw as Record<string, unknown>).value ?? null
|
||||||
|
}
|
||||||
|
return raw
|
||||||
|
}
|
||||||
@@ -784,8 +784,12 @@ function PreviewEditorContent({
|
|||||||
? childWorkflowSnapshotState
|
? childWorkflowSnapshotState
|
||||||
: childWorkflowState
|
: childWorkflowState
|
||||||
const resolvedIsLoadingChildWorkflow = isExecutionMode ? false : isLoadingChildWorkflow
|
const resolvedIsLoadingChildWorkflow = isExecutionMode ? false : isLoadingChildWorkflow
|
||||||
|
const isBlockNotExecuted = isExecutionMode && !executionData
|
||||||
const isMissingChildWorkflow =
|
const isMissingChildWorkflow =
|
||||||
Boolean(childWorkflowId) && !resolvedIsLoadingChildWorkflow && !resolvedChildWorkflowState
|
Boolean(childWorkflowId) &&
|
||||||
|
!isBlockNotExecuted &&
|
||||||
|
!resolvedIsLoadingChildWorkflow &&
|
||||||
|
!resolvedChildWorkflowState
|
||||||
|
|
||||||
/** Drills down into the child workflow or opens it in a new tab */
|
/** Drills down into the child workflow or opens it in a new tab */
|
||||||
const handleExpandChildWorkflow = useCallback(() => {
|
const handleExpandChildWorkflow = useCallback(() => {
|
||||||
@@ -1192,7 +1196,7 @@ function PreviewEditorContent({
|
|||||||
<div ref={subBlocksRef} className='subblocks-section flex flex-1 flex-col overflow-hidden'>
|
<div ref={subBlocksRef} className='subblocks-section flex flex-1 flex-col overflow-hidden'>
|
||||||
<div className='flex-1 overflow-y-auto overflow-x-hidden'>
|
<div className='flex-1 overflow-y-auto overflow-x-hidden'>
|
||||||
{/* Not Executed Banner - shown when in execution mode but block wasn't executed */}
|
{/* Not Executed Banner - shown when in execution mode but block wasn't executed */}
|
||||||
{isExecutionMode && !executionData && (
|
{isBlockNotExecuted && (
|
||||||
<div className='flex min-w-0 flex-col gap-[8px] overflow-hidden border-[var(--border)] border-b px-[12px] py-[10px]'>
|
<div className='flex min-w-0 flex-col gap-[8px] overflow-hidden border-[var(--border)] border-b px-[12px] py-[10px]'>
|
||||||
<div className='flex items-center justify-between'>
|
<div className='flex items-center justify-between'>
|
||||||
<Badge variant='gray-secondary' size='sm' dot>
|
<Badge variant='gray-secondary' size='sm' dot>
|
||||||
@@ -1419,7 +1423,9 @@ function PreviewEditorContent({
|
|||||||
) : (
|
) : (
|
||||||
<div className='flex h-full items-center justify-center bg-[var(--surface-3)]'>
|
<div className='flex h-full items-center justify-center bg-[var(--surface-3)]'>
|
||||||
<span className='text-[13px] text-[var(--text-tertiary)]'>
|
<span className='text-[13px] text-[var(--text-tertiary)]'>
|
||||||
{isMissingChildWorkflow
|
{isBlockNotExecuted
|
||||||
|
? 'Not Executed'
|
||||||
|
: isMissingChildWorkflow
|
||||||
? DELETED_WORKFLOW_LABEL
|
? DELETED_WORKFLOW_LABEL
|
||||||
: 'Unable to load preview'}
|
: 'Unable to load preview'}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { AgentIcon } from '@/components/icons'
|
import { AgentIcon } from '@/components/icons'
|
||||||
import { isHosted } from '@/lib/core/config/feature-flags'
|
|
||||||
import type { BlockConfig } from '@/blocks/types'
|
import type { BlockConfig } from '@/blocks/types'
|
||||||
import { AuthMode } from '@/blocks/types'
|
import { AuthMode } from '@/blocks/types'
|
||||||
|
import { getApiKeyCondition } from '@/blocks/utils'
|
||||||
import {
|
import {
|
||||||
getBaseModelProviders,
|
getBaseModelProviders,
|
||||||
getHostedModels,
|
|
||||||
getMaxTemperature,
|
getMaxTemperature,
|
||||||
getProviderIcon,
|
getProviderIcon,
|
||||||
getReasoningEffortValuesForModel,
|
getReasoningEffortValuesForModel,
|
||||||
@@ -17,15 +16,6 @@ import {
|
|||||||
providers,
|
providers,
|
||||||
supportsTemperature,
|
supportsTemperature,
|
||||||
} from '@/providers/utils'
|
} from '@/providers/utils'
|
||||||
|
|
||||||
const getCurrentOllamaModels = () => {
|
|
||||||
return useProvidersStore.getState().providers.ollama.models
|
|
||||||
}
|
|
||||||
|
|
||||||
const getCurrentVLLMModels = () => {
|
|
||||||
return useProvidersStore.getState().providers.vllm.models
|
|
||||||
}
|
|
||||||
|
|
||||||
import { useProvidersStore } from '@/stores/providers'
|
import { useProvidersStore } from '@/stores/providers'
|
||||||
import type { ToolResponse } from '@/tools/types'
|
import type { ToolResponse } from '@/tools/types'
|
||||||
|
|
||||||
@@ -164,6 +154,7 @@ Return ONLY the JSON array.`,
|
|||||||
type: 'dropdown',
|
type: 'dropdown',
|
||||||
placeholder: 'Select reasoning effort...',
|
placeholder: 'Select reasoning effort...',
|
||||||
options: [
|
options: [
|
||||||
|
{ label: 'auto', id: 'auto' },
|
||||||
{ label: 'low', id: 'low' },
|
{ label: 'low', id: 'low' },
|
||||||
{ label: 'medium', id: 'medium' },
|
{ label: 'medium', id: 'medium' },
|
||||||
{ label: 'high', id: 'high' },
|
{ label: 'high', id: 'high' },
|
||||||
@@ -173,9 +164,12 @@ Return ONLY the JSON array.`,
|
|||||||
const { useSubBlockStore } = await import('@/stores/workflows/subblock/store')
|
const { useSubBlockStore } = await import('@/stores/workflows/subblock/store')
|
||||||
const { useWorkflowRegistry } = await import('@/stores/workflows/registry/store')
|
const { useWorkflowRegistry } = await import('@/stores/workflows/registry/store')
|
||||||
|
|
||||||
|
const autoOption = { label: 'auto', id: 'auto' }
|
||||||
|
|
||||||
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
||||||
if (!activeWorkflowId) {
|
if (!activeWorkflowId) {
|
||||||
return [
|
return [
|
||||||
|
autoOption,
|
||||||
{ label: 'low', id: 'low' },
|
{ label: 'low', id: 'low' },
|
||||||
{ label: 'medium', id: 'medium' },
|
{ label: 'medium', id: 'medium' },
|
||||||
{ label: 'high', id: 'high' },
|
{ label: 'high', id: 'high' },
|
||||||
@@ -188,6 +182,7 @@ Return ONLY the JSON array.`,
|
|||||||
|
|
||||||
if (!modelValue) {
|
if (!modelValue) {
|
||||||
return [
|
return [
|
||||||
|
autoOption,
|
||||||
{ label: 'low', id: 'low' },
|
{ label: 'low', id: 'low' },
|
||||||
{ label: 'medium', id: 'medium' },
|
{ label: 'medium', id: 'medium' },
|
||||||
{ label: 'high', id: 'high' },
|
{ label: 'high', id: 'high' },
|
||||||
@@ -197,15 +192,16 @@ Return ONLY the JSON array.`,
|
|||||||
const validOptions = getReasoningEffortValuesForModel(modelValue)
|
const validOptions = getReasoningEffortValuesForModel(modelValue)
|
||||||
if (!validOptions) {
|
if (!validOptions) {
|
||||||
return [
|
return [
|
||||||
|
autoOption,
|
||||||
{ label: 'low', id: 'low' },
|
{ label: 'low', id: 'low' },
|
||||||
{ label: 'medium', id: 'medium' },
|
{ label: 'medium', id: 'medium' },
|
||||||
{ label: 'high', id: 'high' },
|
{ label: 'high', id: 'high' },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
return validOptions.map((opt) => ({ label: opt, id: opt }))
|
return [autoOption, ...validOptions.map((opt) => ({ label: opt, id: opt }))]
|
||||||
},
|
},
|
||||||
value: () => 'medium',
|
mode: 'advanced',
|
||||||
condition: {
|
condition: {
|
||||||
field: 'model',
|
field: 'model',
|
||||||
value: MODELS_WITH_REASONING_EFFORT,
|
value: MODELS_WITH_REASONING_EFFORT,
|
||||||
@@ -217,6 +213,7 @@ Return ONLY the JSON array.`,
|
|||||||
type: 'dropdown',
|
type: 'dropdown',
|
||||||
placeholder: 'Select verbosity...',
|
placeholder: 'Select verbosity...',
|
||||||
options: [
|
options: [
|
||||||
|
{ label: 'auto', id: 'auto' },
|
||||||
{ label: 'low', id: 'low' },
|
{ label: 'low', id: 'low' },
|
||||||
{ label: 'medium', id: 'medium' },
|
{ label: 'medium', id: 'medium' },
|
||||||
{ label: 'high', id: 'high' },
|
{ label: 'high', id: 'high' },
|
||||||
@@ -226,9 +223,12 @@ Return ONLY the JSON array.`,
|
|||||||
const { useSubBlockStore } = await import('@/stores/workflows/subblock/store')
|
const { useSubBlockStore } = await import('@/stores/workflows/subblock/store')
|
||||||
const { useWorkflowRegistry } = await import('@/stores/workflows/registry/store')
|
const { useWorkflowRegistry } = await import('@/stores/workflows/registry/store')
|
||||||
|
|
||||||
|
const autoOption = { label: 'auto', id: 'auto' }
|
||||||
|
|
||||||
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
||||||
if (!activeWorkflowId) {
|
if (!activeWorkflowId) {
|
||||||
return [
|
return [
|
||||||
|
autoOption,
|
||||||
{ label: 'low', id: 'low' },
|
{ label: 'low', id: 'low' },
|
||||||
{ label: 'medium', id: 'medium' },
|
{ label: 'medium', id: 'medium' },
|
||||||
{ label: 'high', id: 'high' },
|
{ label: 'high', id: 'high' },
|
||||||
@@ -241,6 +241,7 @@ Return ONLY the JSON array.`,
|
|||||||
|
|
||||||
if (!modelValue) {
|
if (!modelValue) {
|
||||||
return [
|
return [
|
||||||
|
autoOption,
|
||||||
{ label: 'low', id: 'low' },
|
{ label: 'low', id: 'low' },
|
||||||
{ label: 'medium', id: 'medium' },
|
{ label: 'medium', id: 'medium' },
|
||||||
{ label: 'high', id: 'high' },
|
{ label: 'high', id: 'high' },
|
||||||
@@ -250,15 +251,16 @@ Return ONLY the JSON array.`,
|
|||||||
const validOptions = getVerbosityValuesForModel(modelValue)
|
const validOptions = getVerbosityValuesForModel(modelValue)
|
||||||
if (!validOptions) {
|
if (!validOptions) {
|
||||||
return [
|
return [
|
||||||
|
autoOption,
|
||||||
{ label: 'low', id: 'low' },
|
{ label: 'low', id: 'low' },
|
||||||
{ label: 'medium', id: 'medium' },
|
{ label: 'medium', id: 'medium' },
|
||||||
{ label: 'high', id: 'high' },
|
{ label: 'high', id: 'high' },
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
return validOptions.map((opt) => ({ label: opt, id: opt }))
|
return [autoOption, ...validOptions.map((opt) => ({ label: opt, id: opt }))]
|
||||||
},
|
},
|
||||||
value: () => 'medium',
|
mode: 'advanced',
|
||||||
condition: {
|
condition: {
|
||||||
field: 'model',
|
field: 'model',
|
||||||
value: MODELS_WITH_VERBOSITY,
|
value: MODELS_WITH_VERBOSITY,
|
||||||
@@ -270,6 +272,7 @@ Return ONLY the JSON array.`,
|
|||||||
type: 'dropdown',
|
type: 'dropdown',
|
||||||
placeholder: 'Select thinking level...',
|
placeholder: 'Select thinking level...',
|
||||||
options: [
|
options: [
|
||||||
|
{ label: 'none', id: 'none' },
|
||||||
{ label: 'minimal', id: 'minimal' },
|
{ label: 'minimal', id: 'minimal' },
|
||||||
{ label: 'low', id: 'low' },
|
{ label: 'low', id: 'low' },
|
||||||
{ label: 'medium', id: 'medium' },
|
{ label: 'medium', id: 'medium' },
|
||||||
@@ -281,12 +284,11 @@ Return ONLY the JSON array.`,
|
|||||||
const { useSubBlockStore } = await import('@/stores/workflows/subblock/store')
|
const { useSubBlockStore } = await import('@/stores/workflows/subblock/store')
|
||||||
const { useWorkflowRegistry } = await import('@/stores/workflows/registry/store')
|
const { useWorkflowRegistry } = await import('@/stores/workflows/registry/store')
|
||||||
|
|
||||||
|
const noneOption = { label: 'none', id: 'none' }
|
||||||
|
|
||||||
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
|
||||||
if (!activeWorkflowId) {
|
if (!activeWorkflowId) {
|
||||||
return [
|
return [noneOption, { label: 'low', id: 'low' }, { label: 'high', id: 'high' }]
|
||||||
{ label: 'low', id: 'low' },
|
|
||||||
{ label: 'high', id: 'high' },
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const workflowValues = useSubBlockStore.getState().workflowValues[activeWorkflowId]
|
const workflowValues = useSubBlockStore.getState().workflowValues[activeWorkflowId]
|
||||||
@@ -294,23 +296,17 @@ Return ONLY the JSON array.`,
|
|||||||
const modelValue = blockValues?.model as string
|
const modelValue = blockValues?.model as string
|
||||||
|
|
||||||
if (!modelValue) {
|
if (!modelValue) {
|
||||||
return [
|
return [noneOption, { label: 'low', id: 'low' }, { label: 'high', id: 'high' }]
|
||||||
{ label: 'low', id: 'low' },
|
|
||||||
{ label: 'high', id: 'high' },
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const validOptions = getThinkingLevelsForModel(modelValue)
|
const validOptions = getThinkingLevelsForModel(modelValue)
|
||||||
if (!validOptions) {
|
if (!validOptions) {
|
||||||
return [
|
return [noneOption, { label: 'low', id: 'low' }, { label: 'high', id: 'high' }]
|
||||||
{ label: 'low', id: 'low' },
|
|
||||||
{ label: 'high', id: 'high' },
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return validOptions.map((opt) => ({ label: opt, id: opt }))
|
return [noneOption, ...validOptions.map((opt) => ({ label: opt, id: opt }))]
|
||||||
},
|
},
|
||||||
value: () => 'high',
|
mode: 'advanced',
|
||||||
condition: {
|
condition: {
|
||||||
field: 'model',
|
field: 'model',
|
||||||
value: MODELS_WITH_THINKING,
|
value: MODELS_WITH_THINKING,
|
||||||
@@ -401,6 +397,16 @@ Return ONLY the JSON array.`,
|
|||||||
value: providers.bedrock.models,
|
value: providers.bedrock.models,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'apiKey',
|
||||||
|
title: 'API Key',
|
||||||
|
type: 'short-input',
|
||||||
|
placeholder: 'Enter your API key',
|
||||||
|
password: true,
|
||||||
|
connectionDroppable: false,
|
||||||
|
required: true,
|
||||||
|
condition: getApiKeyCondition(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'tools',
|
id: 'tools',
|
||||||
title: 'Tools',
|
title: 'Tools',
|
||||||
@@ -413,32 +419,6 @@ Return ONLY the JSON array.`,
|
|||||||
type: 'skill-input',
|
type: 'skill-input',
|
||||||
defaultValue: [],
|
defaultValue: [],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'apiKey',
|
|
||||||
title: 'API Key',
|
|
||||||
type: 'short-input',
|
|
||||||
placeholder: 'Enter your API key',
|
|
||||||
password: true,
|
|
||||||
connectionDroppable: false,
|
|
||||||
required: true,
|
|
||||||
// Hide API key for hosted models, Ollama models, vLLM models, Vertex models (uses OAuth), and Bedrock (uses AWS credentials)
|
|
||||||
condition: isHosted
|
|
||||||
? {
|
|
||||||
field: 'model',
|
|
||||||
value: [...getHostedModels(), ...providers.vertex.models, ...providers.bedrock.models],
|
|
||||||
not: true, // Show for all models EXCEPT those listed
|
|
||||||
}
|
|
||||||
: () => ({
|
|
||||||
field: 'model',
|
|
||||||
value: [
|
|
||||||
...getCurrentOllamaModels(),
|
|
||||||
...getCurrentVLLMModels(),
|
|
||||||
...providers.vertex.models,
|
|
||||||
...providers.bedrock.models,
|
|
||||||
],
|
|
||||||
not: true, // Show for all models EXCEPT Ollama, vLLM, Vertex, and Bedrock models
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'memoryType',
|
id: 'memoryType',
|
||||||
title: 'Memory',
|
title: 'Memory',
|
||||||
@@ -493,6 +473,7 @@ Return ONLY the JSON array.`,
|
|||||||
min: 0,
|
min: 0,
|
||||||
max: 1,
|
max: 1,
|
||||||
defaultValue: 0.3,
|
defaultValue: 0.3,
|
||||||
|
mode: 'advanced',
|
||||||
condition: () => ({
|
condition: () => ({
|
||||||
field: 'model',
|
field: 'model',
|
||||||
value: (() => {
|
value: (() => {
|
||||||
@@ -510,6 +491,7 @@ Return ONLY the JSON array.`,
|
|||||||
min: 0,
|
min: 0,
|
||||||
max: 2,
|
max: 2,
|
||||||
defaultValue: 0.3,
|
defaultValue: 0.3,
|
||||||
|
mode: 'advanced',
|
||||||
condition: () => ({
|
condition: () => ({
|
||||||
field: 'model',
|
field: 'model',
|
||||||
value: (() => {
|
value: (() => {
|
||||||
@@ -525,6 +507,7 @@ Return ONLY the JSON array.`,
|
|||||||
title: 'Max Output Tokens',
|
title: 'Max Output Tokens',
|
||||||
type: 'short-input',
|
type: 'short-input',
|
||||||
placeholder: 'Enter max tokens (e.g., 4096)...',
|
placeholder: 'Enter max tokens (e.g., 4096)...',
|
||||||
|
mode: 'advanced',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'responseFormat',
|
id: 'responseFormat',
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ export interface SubBlockConfig {
|
|||||||
not?: boolean
|
not?: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
| (() => {
|
| ((values?: Record<string, unknown>) => {
|
||||||
field: string
|
field: string
|
||||||
value: string | number | boolean | Array<string | number | boolean>
|
value: string | number | boolean | Array<string | number | boolean>
|
||||||
not?: boolean
|
not?: boolean
|
||||||
@@ -261,7 +261,7 @@ export interface SubBlockConfig {
|
|||||||
not?: boolean
|
not?: boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
| (() => {
|
| ((values?: Record<string, unknown>) => {
|
||||||
field: string
|
field: string
|
||||||
value: string | number | boolean | Array<string | number | boolean>
|
value: string | number | boolean | Array<string | number | boolean>
|
||||||
not?: boolean
|
not?: boolean
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { isHosted } from '@/lib/core/config/feature-flags'
|
import { isHosted } from '@/lib/core/config/feature-flags'
|
||||||
import type { BlockOutput, OutputFieldDefinition, SubBlockConfig } from '@/blocks/types'
|
import type { BlockOutput, OutputFieldDefinition, SubBlockConfig } from '@/blocks/types'
|
||||||
import { getHostedModels, providers } from '@/providers/utils'
|
import { getHostedModels, getProviderFromModel, providers } from '@/providers/utils'
|
||||||
import { useProvidersStore } from '@/stores/providers/store'
|
import { useProvidersStore } from '@/stores/providers/store'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -48,11 +48,54 @@ const getCurrentOllamaModels = () => {
|
|||||||
return useProvidersStore.getState().providers.ollama.models
|
return useProvidersStore.getState().providers.ollama.models
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function buildModelVisibilityCondition(model: string, shouldShow: boolean) {
|
||||||
* Helper to get current vLLM models from store
|
if (!model) {
|
||||||
*/
|
return { field: 'model', value: '__no_model_selected__' }
|
||||||
const getCurrentVLLMModels = () => {
|
}
|
||||||
return useProvidersStore.getState().providers.vllm.models
|
|
||||||
|
return shouldShow ? { field: 'model', value: model } : { field: 'model', value: model, not: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldRequireApiKeyForModel(model: string): boolean {
|
||||||
|
const normalizedModel = model.trim().toLowerCase()
|
||||||
|
if (!normalizedModel) return false
|
||||||
|
|
||||||
|
const hostedModels = getHostedModels()
|
||||||
|
const isHostedModel = hostedModels.some(
|
||||||
|
(hostedModel) => hostedModel.toLowerCase() === normalizedModel
|
||||||
|
)
|
||||||
|
if (isHosted && isHostedModel) return false
|
||||||
|
|
||||||
|
if (normalizedModel.startsWith('vertex/') || normalizedModel.startsWith('bedrock/')) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (normalizedModel.startsWith('vllm/')) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentOllamaModels = getCurrentOllamaModels()
|
||||||
|
if (currentOllamaModels.some((ollamaModel) => ollamaModel.toLowerCase() === normalizedModel)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isHosted) {
|
||||||
|
try {
|
||||||
|
const providerId = getProviderFromModel(model)
|
||||||
|
if (
|
||||||
|
providerId === 'ollama' ||
|
||||||
|
providerId === 'vllm' ||
|
||||||
|
providerId === 'vertex' ||
|
||||||
|
providerId === 'bedrock'
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// If model resolution fails, fall through and require an API key.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -60,22 +103,11 @@ const getCurrentVLLMModels = () => {
|
|||||||
* Handles hosted vs self-hosted environments and excludes providers that don't need API key.
|
* Handles hosted vs self-hosted environments and excludes providers that don't need API key.
|
||||||
*/
|
*/
|
||||||
export function getApiKeyCondition() {
|
export function getApiKeyCondition() {
|
||||||
return isHosted
|
return (values?: Record<string, unknown>) => {
|
||||||
? {
|
const model = typeof values?.model === 'string' ? values.model : ''
|
||||||
field: 'model',
|
const shouldShow = shouldRequireApiKeyForModel(model)
|
||||||
value: [...getHostedModels(), ...providers.vertex.models, ...providers.bedrock.models],
|
return buildModelVisibilityCondition(model, shouldShow)
|
||||||
not: true,
|
|
||||||
}
|
}
|
||||||
: () => ({
|
|
||||||
field: 'model',
|
|
||||||
value: [
|
|
||||||
...getCurrentOllamaModels(),
|
|
||||||
...getCurrentVLLMModels(),
|
|
||||||
...providers.vertex.models,
|
|
||||||
...providers.bedrock.models,
|
|
||||||
],
|
|
||||||
not: true,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -378,6 +378,9 @@ export class AgentBlockHandler implements BlockHandler {
|
|||||||
if (ctx.workflowId) {
|
if (ctx.workflowId) {
|
||||||
params.workflowId = ctx.workflowId
|
params.workflowId = ctx.workflowId
|
||||||
}
|
}
|
||||||
|
if (ctx.userId) {
|
||||||
|
params.userId = ctx.userId
|
||||||
|
}
|
||||||
|
|
||||||
const url = buildAPIUrl('/api/tools/custom', params)
|
const url = buildAPIUrl('/api/tools/custom', params)
|
||||||
const response = await fetch(url.toString(), {
|
const response = await fetch(url.toString(), {
|
||||||
@@ -488,7 +491,9 @@ export class AgentBlockHandler implements BlockHandler {
|
|||||||
usageControl: tool.usageControl || 'auto',
|
usageControl: tool.usageControl || 'auto',
|
||||||
executeFunction: async (callParams: Record<string, any>) => {
|
executeFunction: async (callParams: Record<string, any>) => {
|
||||||
const headers = await buildAuthHeaders()
|
const headers = await buildAuthHeaders()
|
||||||
const execUrl = buildAPIUrl('/api/mcp/tools/execute')
|
const execParams: Record<string, string> = {}
|
||||||
|
if (ctx.userId) execParams.userId = ctx.userId
|
||||||
|
const execUrl = buildAPIUrl('/api/mcp/tools/execute', execParams)
|
||||||
|
|
||||||
const execResponse = await fetch(execUrl.toString(), {
|
const execResponse = await fetch(execUrl.toString(), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -597,6 +602,7 @@ export class AgentBlockHandler implements BlockHandler {
|
|||||||
serverId,
|
serverId,
|
||||||
workspaceId: ctx.workspaceId,
|
workspaceId: ctx.workspaceId,
|
||||||
workflowId: ctx.workflowId,
|
workflowId: ctx.workflowId,
|
||||||
|
...(ctx.userId ? { userId: ctx.userId } : {}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const maxAttempts = 2
|
const maxAttempts = 2
|
||||||
@@ -671,7 +677,9 @@ export class AgentBlockHandler implements BlockHandler {
|
|||||||
usageControl: tool.usageControl || 'auto',
|
usageControl: tool.usageControl || 'auto',
|
||||||
executeFunction: async (callParams: Record<string, any>) => {
|
executeFunction: async (callParams: Record<string, any>) => {
|
||||||
const headers = await buildAuthHeaders()
|
const headers = await buildAuthHeaders()
|
||||||
const execUrl = buildAPIUrl('/api/mcp/tools/execute')
|
const discoverExecParams: Record<string, string> = {}
|
||||||
|
if (ctx.userId) discoverExecParams.userId = ctx.userId
|
||||||
|
const execUrl = buildAPIUrl('/api/mcp/tools/execute', discoverExecParams)
|
||||||
|
|
||||||
const execResponse = await fetch(execUrl.toString(), {
|
const execResponse = await fetch(execUrl.toString(), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -907,24 +915,17 @@ export class AgentBlockHandler implements BlockHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find first system message
|
|
||||||
const firstSystemIndex = messages.findIndex((msg) => msg.role === 'system')
|
const firstSystemIndex = messages.findIndex((msg) => msg.role === 'system')
|
||||||
|
|
||||||
if (firstSystemIndex === -1) {
|
if (firstSystemIndex === -1) {
|
||||||
// No system message exists - add at position 0
|
|
||||||
messages.unshift({ role: 'system', content })
|
messages.unshift({ role: 'system', content })
|
||||||
} else if (firstSystemIndex === 0) {
|
} else if (firstSystemIndex === 0) {
|
||||||
// System message already at position 0 - replace it
|
|
||||||
// Explicit systemPrompt parameter takes precedence over memory/messages
|
|
||||||
messages[0] = { role: 'system', content }
|
messages[0] = { role: 'system', content }
|
||||||
} else {
|
} else {
|
||||||
// System message exists but not at position 0 - move it to position 0
|
|
||||||
// and update with new content
|
|
||||||
messages.splice(firstSystemIndex, 1)
|
messages.splice(firstSystemIndex, 1)
|
||||||
messages.unshift({ role: 'system', content })
|
messages.unshift({ role: 'system', content })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove any additional system messages (keep only the first one)
|
|
||||||
for (let i = messages.length - 1; i >= 1; i--) {
|
for (let i = messages.length - 1; i >= 1; i--) {
|
||||||
if (messages[i].role === 'system') {
|
if (messages[i].role === 'system') {
|
||||||
messages.splice(i, 1)
|
messages.splice(i, 1)
|
||||||
@@ -990,13 +991,14 @@ export class AgentBlockHandler implements BlockHandler {
|
|||||||
workflowId: ctx.workflowId,
|
workflowId: ctx.workflowId,
|
||||||
workspaceId: ctx.workspaceId,
|
workspaceId: ctx.workspaceId,
|
||||||
stream: streaming,
|
stream: streaming,
|
||||||
messages,
|
messages: messages?.map(({ executionId, ...msg }) => msg),
|
||||||
environmentVariables: ctx.environmentVariables || {},
|
environmentVariables: ctx.environmentVariables || {},
|
||||||
workflowVariables: ctx.workflowVariables || {},
|
workflowVariables: ctx.workflowVariables || {},
|
||||||
blockData,
|
blockData,
|
||||||
blockNameMapping,
|
blockNameMapping,
|
||||||
reasoningEffort: inputs.reasoningEffort,
|
reasoningEffort: inputs.reasoningEffort,
|
||||||
verbosity: inputs.verbosity,
|
verbosity: inputs.verbosity,
|
||||||
|
thinkingLevel: inputs.thinkingLevel,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1056,6 +1058,7 @@ export class AgentBlockHandler implements BlockHandler {
|
|||||||
responseFormat: providerRequest.responseFormat,
|
responseFormat: providerRequest.responseFormat,
|
||||||
workflowId: providerRequest.workflowId,
|
workflowId: providerRequest.workflowId,
|
||||||
workspaceId: ctx.workspaceId,
|
workspaceId: ctx.workspaceId,
|
||||||
|
userId: ctx.userId,
|
||||||
stream: providerRequest.stream,
|
stream: providerRequest.stream,
|
||||||
messages: 'messages' in providerRequest ? providerRequest.messages : undefined,
|
messages: 'messages' in providerRequest ? providerRequest.messages : undefined,
|
||||||
environmentVariables: ctx.environmentVariables || {},
|
environmentVariables: ctx.environmentVariables || {},
|
||||||
@@ -1065,6 +1068,7 @@ export class AgentBlockHandler implements BlockHandler {
|
|||||||
isDeployedContext: ctx.isDeployedContext,
|
isDeployedContext: ctx.isDeployedContext,
|
||||||
reasoningEffort: providerRequest.reasoningEffort,
|
reasoningEffort: providerRequest.reasoningEffort,
|
||||||
verbosity: providerRequest.verbosity,
|
verbosity: providerRequest.verbosity,
|
||||||
|
thinkingLevel: providerRequest.thinkingLevel,
|
||||||
})
|
})
|
||||||
|
|
||||||
return this.processProviderResponse(response, block, responseFormat)
|
return this.processProviderResponse(response, block, responseFormat)
|
||||||
@@ -1082,8 +1086,6 @@ export class AgentBlockHandler implements BlockHandler {
|
|||||||
|
|
||||||
logger.info(`[${requestId}] Resolving Vertex AI credential: ${credentialId}`)
|
logger.info(`[${requestId}] Resolving Vertex AI credential: ${credentialId}`)
|
||||||
|
|
||||||
// Get the credential - we need to find the owner
|
|
||||||
// Since we're in a workflow context, we can query the credential directly
|
|
||||||
const credential = await db.query.account.findFirst({
|
const credential = await db.query.account.findFirst({
|
||||||
where: eq(account.id, credentialId),
|
where: eq(account.id, credentialId),
|
||||||
})
|
})
|
||||||
@@ -1092,7 +1094,6 @@ export class AgentBlockHandler implements BlockHandler {
|
|||||||
throw new Error(`Vertex AI credential not found: ${credentialId}`)
|
throw new Error(`Vertex AI credential not found: ${credentialId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh the token if needed
|
|
||||||
const { accessToken } = await refreshTokenIfNeeded(requestId, credential, credentialId)
|
const { accessToken } = await refreshTokenIfNeeded(requestId, credential, credentialId)
|
||||||
|
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ export interface AgentInputs {
|
|||||||
bedrockRegion?: string
|
bedrockRegion?: string
|
||||||
reasoningEffort?: string
|
reasoningEffort?: string
|
||||||
verbosity?: string
|
verbosity?: string
|
||||||
|
thinkingLevel?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ToolInput {
|
export interface ToolInput {
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export class EvaluatorBlockHandler implements BlockHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const url = buildAPIUrl('/api/providers')
|
const url = buildAPIUrl('/api/providers', ctx.userId ? { userId: ctx.userId } : {})
|
||||||
|
|
||||||
const providerRequest: Record<string, any> = {
|
const providerRequest: Record<string, any> = {
|
||||||
provider: providerId,
|
provider: providerId,
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ export class RouterBlockHandler implements BlockHandler {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const url = new URL('/api/providers', getBaseUrl())
|
const url = new URL('/api/providers', getBaseUrl())
|
||||||
|
if (ctx.userId) url.searchParams.set('userId', ctx.userId)
|
||||||
|
|
||||||
const messages = [{ role: 'user', content: routerConfig.prompt }]
|
const messages = [{ role: 'user', content: routerConfig.prompt }]
|
||||||
const systemPrompt = generateRouterPrompt(routerConfig.prompt, targetBlocks)
|
const systemPrompt = generateRouterPrompt(routerConfig.prompt, targetBlocks)
|
||||||
@@ -209,6 +210,7 @@ export class RouterBlockHandler implements BlockHandler {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const url = new URL('/api/providers', getBaseUrl())
|
const url = new URL('/api/providers', getBaseUrl())
|
||||||
|
if (ctx.userId) url.searchParams.set('userId', ctx.userId)
|
||||||
|
|
||||||
const messages = [{ role: 'user', content: routerConfig.context }]
|
const messages = [{ role: 'user', content: routerConfig.context }]
|
||||||
const systemPrompt = generateRouterV2Prompt(routerConfig.context, routes)
|
const systemPrompt = generateRouterV2Prompt(routerConfig.context, routes)
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { db } from '@sim/db'
|
|||||||
import { account, workflow as workflowTable } from '@sim/db/schema'
|
import { account, workflow as workflowTable } from '@sim/db/schema'
|
||||||
import { eq } from 'drizzle-orm'
|
import { eq } from 'drizzle-orm'
|
||||||
import type { NextRequest } from 'next/server'
|
import type { NextRequest } from 'next/server'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||||
|
|
||||||
export interface CredentialAccessResult {
|
export interface CredentialAccessResult {
|
||||||
ok: boolean
|
ok: boolean
|
||||||
error?: string
|
error?: string
|
||||||
authType?: 'session' | 'api_key' | 'internal_jwt'
|
authType?: 'session' | 'internal_jwt'
|
||||||
requesterUserId?: string
|
requesterUserId?: string
|
||||||
credentialOwnerUserId?: string
|
credentialOwnerUserId?: string
|
||||||
workspaceId?: string
|
workspaceId?: string
|
||||||
@@ -16,10 +16,10 @@ export interface CredentialAccessResult {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Centralizes auth + collaboration rules for credential use.
|
* Centralizes auth + collaboration rules for credential use.
|
||||||
* - Uses checkHybridAuth to authenticate the caller
|
* - Uses checkSessionOrInternalAuth to authenticate the caller
|
||||||
* - Fetches credential owner
|
* - Fetches credential owner
|
||||||
* - Authorization rules:
|
* - Authorization rules:
|
||||||
* - session/api_key: allow if requester owns the credential; otherwise require workflowId and
|
* - session: allow if requester owns the credential; otherwise require workflowId and
|
||||||
* verify BOTH requester and owner have access to the workflow's workspace
|
* verify BOTH requester and owner have access to the workflow's workspace
|
||||||
* - internal_jwt: require workflowId (by default) and verify credential owner has access to the
|
* - internal_jwt: require workflowId (by default) and verify credential owner has access to the
|
||||||
* workflow's workspace (requester identity is the system/workflow)
|
* workflow's workspace (requester identity is the system/workflow)
|
||||||
@@ -30,7 +30,9 @@ export async function authorizeCredentialUse(
|
|||||||
): Promise<CredentialAccessResult> {
|
): Promise<CredentialAccessResult> {
|
||||||
const { credentialId, workflowId, requireWorkflowIdForInternal = true } = params
|
const { credentialId, workflowId, requireWorkflowIdForInternal = true } = params
|
||||||
|
|
||||||
const auth = await checkHybridAuth(request, { requireWorkflowId: requireWorkflowIdForInternal })
|
const auth = await checkSessionOrInternalAuth(request, {
|
||||||
|
requireWorkflowId: requireWorkflowIdForInternal,
|
||||||
|
})
|
||||||
if (!auth.success || !auth.userId) {
|
if (!auth.success || !auth.userId) {
|
||||||
return { ok: false, error: auth.error || 'Authentication required' }
|
return { ok: false, error: auth.error || 'Authentication required' }
|
||||||
}
|
}
|
||||||
@@ -52,7 +54,7 @@ export async function authorizeCredentialUse(
|
|||||||
if (auth.authType !== 'internal_jwt' && auth.userId === credentialOwnerUserId) {
|
if (auth.authType !== 'internal_jwt' && auth.userId === credentialOwnerUserId) {
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
authType: auth.authType,
|
authType: auth.authType as CredentialAccessResult['authType'],
|
||||||
requesterUserId: auth.userId,
|
requesterUserId: auth.userId,
|
||||||
credentialOwnerUserId,
|
credentialOwnerUserId,
|
||||||
}
|
}
|
||||||
@@ -85,14 +87,14 @@ export async function authorizeCredentialUse(
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
authType: auth.authType,
|
authType: auth.authType as CredentialAccessResult['authType'],
|
||||||
requesterUserId: auth.userId,
|
requesterUserId: auth.userId,
|
||||||
credentialOwnerUserId,
|
credentialOwnerUserId,
|
||||||
workspaceId: wf.workspaceId,
|
workspaceId: wf.workspaceId,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Session/API key: verify BOTH requester and owner belong to the workflow's workspace
|
// Session: verify BOTH requester and owner belong to the workflow's workspace
|
||||||
const requesterPerm = await getUserEntityPermissions(auth.userId, 'workspace', wf.workspaceId)
|
const requesterPerm = await getUserEntityPermissions(auth.userId, 'workspace', wf.workspaceId)
|
||||||
const ownerPerm = await getUserEntityPermissions(
|
const ownerPerm = await getUserEntityPermissions(
|
||||||
credentialOwnerUserId,
|
credentialOwnerUserId,
|
||||||
@@ -105,7 +107,7 @@ export async function authorizeCredentialUse(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
ok: true,
|
ok: true,
|
||||||
authType: auth.authType,
|
authType: auth.authType as CredentialAccessResult['authType'],
|
||||||
requesterUserId: auth.userId,
|
requesterUserId: auth.userId,
|
||||||
credentialOwnerUserId,
|
credentialOwnerUserId,
|
||||||
workspaceId: wf.workspaceId,
|
workspaceId: wf.workspaceId,
|
||||||
|
|||||||
@@ -33,11 +33,25 @@ export class SnapshotService implements ISnapshotService {
|
|||||||
|
|
||||||
const existingSnapshot = await this.getSnapshotByHash(workflowId, stateHash)
|
const existingSnapshot = await this.getSnapshotByHash(workflowId, stateHash)
|
||||||
if (existingSnapshot) {
|
if (existingSnapshot) {
|
||||||
|
let refreshedState: WorkflowState = existingSnapshot.stateData
|
||||||
|
try {
|
||||||
|
await db
|
||||||
|
.update(workflowExecutionSnapshots)
|
||||||
|
.set({ stateData: state })
|
||||||
|
.where(eq(workflowExecutionSnapshots.id, existingSnapshot.id))
|
||||||
|
refreshedState = state
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(
|
||||||
|
`Failed to refresh snapshot stateData for ${existingSnapshot.id}, continuing with existing data`,
|
||||||
|
error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`Reusing existing snapshot for workflow ${workflowId} (hash: ${stateHash.slice(0, 12)}...)`
|
`Reusing existing snapshot for workflow ${workflowId} (hash: ${stateHash.slice(0, 12)}...)`
|
||||||
)
|
)
|
||||||
return {
|
return {
|
||||||
snapshot: existingSnapshot,
|
snapshot: { ...existingSnapshot, stateData: refreshedState },
|
||||||
isNew: false,
|
isNew: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import type { NextRequest, NextResponse } from 'next/server'
|
import type { NextRequest, NextResponse } from 'next/server'
|
||||||
import { checkHybridAuth } from '@/lib/auth/hybrid'
|
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||||
import { generateRequestId } from '@/lib/core/utils/request'
|
import { generateRequestId } from '@/lib/core/utils/request'
|
||||||
import { createMcpErrorResponse } from '@/lib/mcp/utils'
|
import { createMcpErrorResponse } from '@/lib/mcp/utils'
|
||||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||||
@@ -43,7 +43,7 @@ async function validateMcpAuth(
|
|||||||
const requestId = generateRequestId()
|
const requestId = generateRequestId()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const auth = await checkHybridAuth(request, { requireWorkflowId: false })
|
const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
|
||||||
if (!auth.success || !auth.userId) {
|
if (!auth.success || !auth.userId) {
|
||||||
logger.warn(`[${requestId}] Authentication failed: ${auth.error}`)
|
logger.warn(`[${requestId}] Authentication failed: ${auth.error}`)
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
validateTypeformSignature,
|
validateTypeformSignature,
|
||||||
verifyProviderWebhook,
|
verifyProviderWebhook,
|
||||||
} from '@/lib/webhooks/utils.server'
|
} from '@/lib/webhooks/utils.server'
|
||||||
|
import { getWorkspaceBilledAccountUserId } from '@/lib/workspaces/utils'
|
||||||
import { executeWebhookJob } from '@/background/webhook-execution'
|
import { executeWebhookJob } from '@/background/webhook-execution'
|
||||||
import { resolveEnvVarReferences } from '@/executor/utils/reference-validation'
|
import { resolveEnvVarReferences } from '@/executor/utils/reference-validation'
|
||||||
import { isGitHubEventMatch } from '@/triggers/github/utils'
|
import { isGitHubEventMatch } from '@/triggers/github/utils'
|
||||||
@@ -1003,10 +1004,23 @@ export async function queueWebhookExecution(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!foundWorkflow.workspaceId) {
|
||||||
|
logger.error(`[${options.requestId}] Workflow ${foundWorkflow.id} has no workspaceId`)
|
||||||
|
return NextResponse.json({ error: 'Workflow has no associated workspace' }, { status: 500 })
|
||||||
|
}
|
||||||
|
|
||||||
|
const actorUserId = await getWorkspaceBilledAccountUserId(foundWorkflow.workspaceId)
|
||||||
|
if (!actorUserId) {
|
||||||
|
logger.error(
|
||||||
|
`[${options.requestId}] No billing account for workspace ${foundWorkflow.workspaceId}`
|
||||||
|
)
|
||||||
|
return NextResponse.json({ error: 'Unable to resolve billing account' }, { status: 500 })
|
||||||
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
webhookId: foundWebhook.id,
|
webhookId: foundWebhook.id,
|
||||||
workflowId: foundWorkflow.id,
|
workflowId: foundWorkflow.id,
|
||||||
userId: foundWorkflow.userId,
|
userId: actorUserId,
|
||||||
provider: foundWebhook.provider,
|
provider: foundWebhook.provider,
|
||||||
body,
|
body,
|
||||||
headers,
|
headers,
|
||||||
@@ -1017,7 +1031,7 @@ export async function queueWebhookExecution(
|
|||||||
|
|
||||||
const jobQueue = await getJobQueue()
|
const jobQueue = await getJobQueue()
|
||||||
const jobId = await jobQueue.enqueue('webhook-execution', payload, {
|
const jobId = await jobQueue.enqueue('webhook-execution', payload, {
|
||||||
metadata: { workflowId: foundWorkflow.id, userId: foundWorkflow.userId },
|
metadata: { workflowId: foundWorkflow.id, userId: actorUserId },
|
||||||
})
|
})
|
||||||
logger.info(
|
logger.info(
|
||||||
`[${options.requestId}] Queued webhook execution task ${jobId} for ${foundWebhook.provider} webhook`
|
`[${options.requestId}] Queued webhook execution task ${jobId} for ${foundWebhook.provider} webhook`
|
||||||
|
|||||||
@@ -156,6 +156,15 @@ describe('evaluateSubBlockCondition', () => {
|
|||||||
expect(evaluateSubBlockCondition(condition, values)).toBe(true)
|
expect(evaluateSubBlockCondition(condition, values)).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it.concurrent('passes current values into function conditions', () => {
|
||||||
|
const condition = (values?: Record<string, unknown>) => ({
|
||||||
|
field: 'model',
|
||||||
|
value: typeof values?.model === 'string' ? values.model : '__no_model_selected__',
|
||||||
|
})
|
||||||
|
const values = { model: 'ollama/gemma3:4b' }
|
||||||
|
expect(evaluateSubBlockCondition(condition, values)).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
it.concurrent('handles boolean values', () => {
|
it.concurrent('handles boolean values', () => {
|
||||||
const condition = { field: 'enabled', value: true }
|
const condition = { field: 'enabled', value: true }
|
||||||
const values = { enabled: true }
|
const values = { enabled: true }
|
||||||
|
|||||||
@@ -100,11 +100,14 @@ export function resolveCanonicalMode(
|
|||||||
* Evaluate a subblock condition against a map of raw values.
|
* Evaluate a subblock condition against a map of raw values.
|
||||||
*/
|
*/
|
||||||
export function evaluateSubBlockCondition(
|
export function evaluateSubBlockCondition(
|
||||||
condition: SubBlockCondition | (() => SubBlockCondition) | undefined,
|
condition:
|
||||||
|
| SubBlockCondition
|
||||||
|
| ((values?: Record<string, unknown>) => SubBlockCondition)
|
||||||
|
| undefined,
|
||||||
values: Record<string, unknown>
|
values: Record<string, unknown>
|
||||||
): boolean {
|
): boolean {
|
||||||
if (!condition) return true
|
if (!condition) return true
|
||||||
const actual = typeof condition === 'function' ? condition() : condition
|
const actual = typeof condition === 'function' ? condition(values) : condition
|
||||||
const fieldValue = values[actual.field]
|
const fieldValue = values[actual.field]
|
||||||
const valueMatch = Array.isArray(actual.value)
|
const valueMatch = Array.isArray(actual.value)
|
||||||
? fieldValue != null &&
|
? fieldValue != null &&
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type Anthropic from '@anthropic-ai/sdk'
|
import type Anthropic from '@anthropic-ai/sdk'
|
||||||
import { transformJSONSchema } from '@anthropic-ai/sdk/lib/transform-json-schema'
|
import { transformJSONSchema } from '@anthropic-ai/sdk/lib/transform-json-schema'
|
||||||
|
import type { RawMessageStreamEvent } from '@anthropic-ai/sdk/resources/messages/messages'
|
||||||
import type { Logger } from '@sim/logger'
|
import type { Logger } from '@sim/logger'
|
||||||
import type { StreamingExecution } from '@/executor/types'
|
import type { StreamingExecution } from '@/executor/types'
|
||||||
import { MAX_TOOL_ITERATIONS } from '@/providers'
|
import { MAX_TOOL_ITERATIONS } from '@/providers'
|
||||||
@@ -34,11 +35,21 @@ export interface AnthropicProviderConfig {
|
|||||||
logger: Logger
|
logger: Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom payload type extending the SDK's base message creation params.
|
||||||
|
* Adds fields not yet in the SDK: adaptive thinking, output_format, output_config.
|
||||||
|
*/
|
||||||
|
interface AnthropicPayload extends Omit<Anthropic.Messages.MessageStreamParams, 'thinking'> {
|
||||||
|
thinking?: Anthropic.Messages.ThinkingConfigParam | { type: 'adaptive' }
|
||||||
|
output_format?: { type: 'json_schema'; schema: Record<string, unknown> }
|
||||||
|
output_config?: { effort: string }
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates prompt-based schema instructions for older models that don't support native structured outputs.
|
* Generates prompt-based schema instructions for older models that don't support native structured outputs.
|
||||||
* This is a fallback approach that adds schema requirements to the system prompt.
|
* This is a fallback approach that adds schema requirements to the system prompt.
|
||||||
*/
|
*/
|
||||||
function generateSchemaInstructions(schema: any, schemaName?: string): string {
|
function generateSchemaInstructions(schema: Record<string, unknown>, schemaName?: string): string {
|
||||||
const name = schemaName || 'response'
|
const name = schemaName || 'response'
|
||||||
return `IMPORTANT: You must respond with a valid JSON object that conforms to the following schema.
|
return `IMPORTANT: You must respond with a valid JSON object that conforms to the following schema.
|
||||||
Do not include any text before or after the JSON object. Only output the JSON.
|
Do not include any text before or after the JSON object. Only output the JSON.
|
||||||
@@ -113,6 +124,30 @@ function buildThinkingConfig(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Anthropic SDK requires streaming for non-streaming requests when max_tokens exceeds
|
||||||
|
* this threshold, to avoid HTTP timeouts. When thinking is enabled and pushes max_tokens
|
||||||
|
* above this limit, we use streaming internally and collect the final message.
|
||||||
|
*/
|
||||||
|
const ANTHROPIC_SDK_NON_STREAMING_MAX_TOKENS = 21333
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an Anthropic message, automatically using streaming internally when max_tokens
|
||||||
|
* exceeds the SDK's non-streaming threshold. Returns the same Message object either way.
|
||||||
|
*/
|
||||||
|
async function createMessage(
|
||||||
|
anthropic: Anthropic,
|
||||||
|
payload: AnthropicPayload
|
||||||
|
): Promise<Anthropic.Messages.Message> {
|
||||||
|
if (payload.max_tokens > ANTHROPIC_SDK_NON_STREAMING_MAX_TOKENS && !payload.stream) {
|
||||||
|
const stream = anthropic.messages.stream(payload as Anthropic.Messages.MessageStreamParams)
|
||||||
|
return stream.finalMessage()
|
||||||
|
}
|
||||||
|
return anthropic.messages.create(
|
||||||
|
payload as Anthropic.Messages.MessageCreateParamsNonStreaming
|
||||||
|
) as Promise<Anthropic.Messages.Message>
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Executes a request using the Anthropic API with full tool loop support.
|
* Executes a request using the Anthropic API with full tool loop support.
|
||||||
* This is the shared core implementation used by both the standard Anthropic provider
|
* This is the shared core implementation used by both the standard Anthropic provider
|
||||||
@@ -135,7 +170,7 @@ export async function executeAnthropicProviderRequest(
|
|||||||
|
|
||||||
const anthropic = config.createClient(request.apiKey, useNativeStructuredOutputs)
|
const anthropic = config.createClient(request.apiKey, useNativeStructuredOutputs)
|
||||||
|
|
||||||
const messages: any[] = []
|
const messages: Anthropic.Messages.MessageParam[] = []
|
||||||
let systemPrompt = request.systemPrompt || ''
|
let systemPrompt = request.systemPrompt || ''
|
||||||
|
|
||||||
if (request.context) {
|
if (request.context) {
|
||||||
@@ -153,8 +188,8 @@ export async function executeAnthropicProviderRequest(
|
|||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: 'tool_result',
|
type: 'tool_result',
|
||||||
tool_use_id: msg.name,
|
tool_use_id: msg.name || '',
|
||||||
content: msg.content,
|
content: msg.content || undefined,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
@@ -188,12 +223,12 @@ export async function executeAnthropicProviderRequest(
|
|||||||
systemPrompt = ''
|
systemPrompt = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
let anthropicTools = request.tools?.length
|
let anthropicTools: Anthropic.Messages.Tool[] | undefined = request.tools?.length
|
||||||
? request.tools.map((tool) => ({
|
? request.tools.map((tool) => ({
|
||||||
name: tool.id,
|
name: tool.id,
|
||||||
description: tool.description,
|
description: tool.description,
|
||||||
input_schema: {
|
input_schema: {
|
||||||
type: 'object',
|
type: 'object' as const,
|
||||||
properties: tool.parameters.properties,
|
properties: tool.parameters.properties,
|
||||||
required: tool.parameters.required,
|
required: tool.parameters.required,
|
||||||
},
|
},
|
||||||
@@ -238,13 +273,12 @@ export async function executeAnthropicProviderRequest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload: any = {
|
const payload: AnthropicPayload = {
|
||||||
model: request.model,
|
model: request.model,
|
||||||
messages,
|
messages,
|
||||||
system: systemPrompt,
|
system: systemPrompt,
|
||||||
max_tokens:
|
max_tokens:
|
||||||
Number.parseInt(String(request.maxTokens)) ||
|
Number.parseInt(String(request.maxTokens)) || getMaxOutputTokensForModel(request.model),
|
||||||
getMaxOutputTokensForModel(request.model, request.stream ?? false),
|
|
||||||
temperature: Number.parseFloat(String(request.temperature ?? 0.7)),
|
temperature: Number.parseFloat(String(request.temperature ?? 0.7)),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,13 +302,35 @@ export async function executeAnthropicProviderRequest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add extended thinking configuration if supported and requested
|
// Add extended thinking configuration if supported and requested
|
||||||
if (request.thinkingLevel) {
|
// The 'none' sentinel means "disable thinking" — skip configuration entirely.
|
||||||
|
if (request.thinkingLevel && request.thinkingLevel !== 'none') {
|
||||||
const thinkingConfig = buildThinkingConfig(request.model, request.thinkingLevel)
|
const thinkingConfig = buildThinkingConfig(request.model, request.thinkingLevel)
|
||||||
if (thinkingConfig) {
|
if (thinkingConfig) {
|
||||||
payload.thinking = thinkingConfig.thinking
|
payload.thinking = thinkingConfig.thinking
|
||||||
if (thinkingConfig.outputConfig) {
|
if (thinkingConfig.outputConfig) {
|
||||||
payload.output_config = thinkingConfig.outputConfig
|
payload.output_config = thinkingConfig.outputConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Per Anthropic docs: budget_tokens must be less than max_tokens.
|
||||||
|
// Ensure max_tokens leaves room for both thinking and text output.
|
||||||
|
if (
|
||||||
|
thinkingConfig.thinking.type === 'enabled' &&
|
||||||
|
'budget_tokens' in thinkingConfig.thinking
|
||||||
|
) {
|
||||||
|
const budgetTokens = thinkingConfig.thinking.budget_tokens
|
||||||
|
const minMaxTokens = budgetTokens + 4096
|
||||||
|
if (payload.max_tokens < minMaxTokens) {
|
||||||
|
const modelMax = getMaxOutputTokensForModel(request.model)
|
||||||
|
payload.max_tokens = Math.min(minMaxTokens, modelMax)
|
||||||
|
logger.info(
|
||||||
|
`Adjusted max_tokens to ${payload.max_tokens} to satisfy budget_tokens (${budgetTokens}) constraint`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per Anthropic docs: thinking is not compatible with temperature or top_k modifications.
|
||||||
|
payload.temperature = undefined
|
||||||
|
|
||||||
const isAdaptive = thinkingConfig.thinking.type === 'adaptive'
|
const isAdaptive = thinkingConfig.thinking.type === 'adaptive'
|
||||||
logger.info(
|
logger.info(
|
||||||
`Using ${isAdaptive ? 'adaptive' : 'extended'} thinking for model: ${modelId} with ${isAdaptive ? `effort: ${request.thinkingLevel}` : `budget: ${(thinkingConfig.thinking as { budget_tokens: number }).budget_tokens}`}`
|
`Using ${isAdaptive ? 'adaptive' : 'extended'} thinking for model: ${modelId} with ${isAdaptive ? `effort: ${request.thinkingLevel}` : `budget: ${(thinkingConfig.thinking as { budget_tokens: number }).budget_tokens}`}`
|
||||||
@@ -288,7 +344,16 @@ export async function executeAnthropicProviderRequest(
|
|||||||
|
|
||||||
if (anthropicTools?.length) {
|
if (anthropicTools?.length) {
|
||||||
payload.tools = anthropicTools
|
payload.tools = anthropicTools
|
||||||
if (toolChoice !== 'auto') {
|
// Per Anthropic docs: forced tool_choice (type: "tool" or "any") is incompatible with
|
||||||
|
// thinking. Only auto and none are supported when thinking is enabled.
|
||||||
|
if (payload.thinking) {
|
||||||
|
// Per Anthropic docs: only 'auto' (default) and 'none' work with thinking.
|
||||||
|
if (toolChoice === 'none') {
|
||||||
|
payload.tool_choice = { type: 'none' }
|
||||||
|
}
|
||||||
|
} else if (toolChoice === 'none') {
|
||||||
|
payload.tool_choice = { type: 'none' }
|
||||||
|
} else if (toolChoice !== 'auto') {
|
||||||
payload.tool_choice = toolChoice
|
payload.tool_choice = toolChoice
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -301,13 +366,15 @@ export async function executeAnthropicProviderRequest(
|
|||||||
const providerStartTime = Date.now()
|
const providerStartTime = Date.now()
|
||||||
const providerStartTimeISO = new Date(providerStartTime).toISOString()
|
const providerStartTimeISO = new Date(providerStartTime).toISOString()
|
||||||
|
|
||||||
const streamResponse: any = await anthropic.messages.create({
|
const streamResponse = await anthropic.messages.create({
|
||||||
...payload,
|
...payload,
|
||||||
stream: true,
|
stream: true,
|
||||||
})
|
} as Anthropic.Messages.MessageCreateParamsStreaming)
|
||||||
|
|
||||||
const streamingResult = {
|
const streamingResult = {
|
||||||
stream: createReadableStreamFromAnthropicStream(streamResponse, (content, usage) => {
|
stream: createReadableStreamFromAnthropicStream(
|
||||||
|
streamResponse as AsyncIterable<RawMessageStreamEvent>,
|
||||||
|
(content, usage) => {
|
||||||
streamingResult.execution.output.content = content
|
streamingResult.execution.output.content = content
|
||||||
streamingResult.execution.output.tokens = {
|
streamingResult.execution.output.tokens = {
|
||||||
input: usage.input_tokens,
|
input: usage.input_tokens,
|
||||||
@@ -331,12 +398,14 @@ export async function executeAnthropicProviderRequest(
|
|||||||
streamEndTime - providerStartTime
|
streamEndTime - providerStartTime
|
||||||
|
|
||||||
if (streamingResult.execution.output.providerTiming.timeSegments?.[0]) {
|
if (streamingResult.execution.output.providerTiming.timeSegments?.[0]) {
|
||||||
streamingResult.execution.output.providerTiming.timeSegments[0].endTime = streamEndTime
|
streamingResult.execution.output.providerTiming.timeSegments[0].endTime =
|
||||||
|
streamEndTime
|
||||||
streamingResult.execution.output.providerTiming.timeSegments[0].duration =
|
streamingResult.execution.output.providerTiming.timeSegments[0].duration =
|
||||||
streamEndTime - providerStartTime
|
streamEndTime - providerStartTime
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}),
|
}
|
||||||
|
),
|
||||||
execution: {
|
execution: {
|
||||||
success: true,
|
success: true,
|
||||||
output: {
|
output: {
|
||||||
@@ -385,21 +454,13 @@ export async function executeAnthropicProviderRequest(
|
|||||||
const providerStartTime = Date.now()
|
const providerStartTime = Date.now()
|
||||||
const providerStartTimeISO = new Date(providerStartTime).toISOString()
|
const providerStartTimeISO = new Date(providerStartTime).toISOString()
|
||||||
|
|
||||||
// Cap intermediate calls at non-streaming limit to avoid SDK timeout errors,
|
|
||||||
// but allow users to set lower values if desired
|
|
||||||
const nonStreamingLimit = getMaxOutputTokensForModel(request.model, false)
|
|
||||||
const nonStreamingMaxTokens = request.maxTokens
|
|
||||||
? Math.min(Number.parseInt(String(request.maxTokens)), nonStreamingLimit)
|
|
||||||
: nonStreamingLimit
|
|
||||||
const intermediatePayload = { ...payload, max_tokens: nonStreamingMaxTokens }
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const initialCallTime = Date.now()
|
const initialCallTime = Date.now()
|
||||||
const originalToolChoice = intermediatePayload.tool_choice
|
const originalToolChoice = payload.tool_choice
|
||||||
const forcedTools = preparedTools?.forcedTools || []
|
const forcedTools = preparedTools?.forcedTools || []
|
||||||
let usedForcedTools: string[] = []
|
let usedForcedTools: string[] = []
|
||||||
|
|
||||||
let currentResponse = await anthropic.messages.create(intermediatePayload)
|
let currentResponse = await createMessage(anthropic, payload)
|
||||||
const firstResponseTime = Date.now() - initialCallTime
|
const firstResponseTime = Date.now() - initialCallTime
|
||||||
|
|
||||||
let content = ''
|
let content = ''
|
||||||
@@ -468,10 +529,10 @@ export async function executeAnthropicProviderRequest(
|
|||||||
const toolExecutionPromises = toolUses.map(async (toolUse) => {
|
const toolExecutionPromises = toolUses.map(async (toolUse) => {
|
||||||
const toolCallStartTime = Date.now()
|
const toolCallStartTime = Date.now()
|
||||||
const toolName = toolUse.name
|
const toolName = toolUse.name
|
||||||
const toolArgs = toolUse.input as Record<string, any>
|
const toolArgs = toolUse.input as Record<string, unknown>
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tool = request.tools?.find((t: any) => t.id === toolName)
|
const tool = request.tools?.find((t) => t.id === toolName)
|
||||||
if (!tool) return null
|
if (!tool) return null
|
||||||
|
|
||||||
const { toolParams, executionParams } = prepareToolExecution(tool, toolArgs, request)
|
const { toolParams, executionParams } = prepareToolExecution(tool, toolArgs, request)
|
||||||
@@ -512,17 +573,8 @@ export async function executeAnthropicProviderRequest(
|
|||||||
const executionResults = await Promise.allSettled(toolExecutionPromises)
|
const executionResults = await Promise.allSettled(toolExecutionPromises)
|
||||||
|
|
||||||
// Collect all tool_use and tool_result blocks for batching
|
// Collect all tool_use and tool_result blocks for batching
|
||||||
const toolUseBlocks: Array<{
|
const toolUseBlocks: Anthropic.Messages.ToolUseBlockParam[] = []
|
||||||
type: 'tool_use'
|
const toolResultBlocks: Anthropic.Messages.ToolResultBlockParam[] = []
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
input: Record<string, unknown>
|
|
||||||
}> = []
|
|
||||||
const toolResultBlocks: Array<{
|
|
||||||
type: 'tool_result'
|
|
||||||
tool_use_id: string
|
|
||||||
content: string
|
|
||||||
}> = []
|
|
||||||
|
|
||||||
for (const settledResult of executionResults) {
|
for (const settledResult of executionResults) {
|
||||||
if (settledResult.status === 'rejected' || !settledResult.value) continue
|
if (settledResult.status === 'rejected' || !settledResult.value) continue
|
||||||
@@ -583,11 +635,25 @@ export async function executeAnthropicProviderRequest(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add ONE assistant message with ALL tool_use blocks
|
// Per Anthropic docs: thinking blocks must be preserved in assistant messages
|
||||||
|
// during tool use to maintain reasoning continuity.
|
||||||
|
const thinkingBlocks = currentResponse.content.filter(
|
||||||
|
(
|
||||||
|
item
|
||||||
|
): item is
|
||||||
|
| Anthropic.Messages.ThinkingBlock
|
||||||
|
| Anthropic.Messages.RedactedThinkingBlock =>
|
||||||
|
item.type === 'thinking' || item.type === 'redacted_thinking'
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add ONE assistant message with thinking + tool_use blocks
|
||||||
if (toolUseBlocks.length > 0) {
|
if (toolUseBlocks.length > 0) {
|
||||||
currentMessages.push({
|
currentMessages.push({
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: toolUseBlocks as unknown as Anthropic.Messages.ContentBlock[],
|
content: [
|
||||||
|
...thinkingBlocks,
|
||||||
|
...toolUseBlocks,
|
||||||
|
] as Anthropic.Messages.ContentBlockParam[],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -595,19 +661,23 @@ export async function executeAnthropicProviderRequest(
|
|||||||
if (toolResultBlocks.length > 0) {
|
if (toolResultBlocks.length > 0) {
|
||||||
currentMessages.push({
|
currentMessages.push({
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: toolResultBlocks as unknown as Anthropic.Messages.ContentBlockParam[],
|
content: toolResultBlocks as Anthropic.Messages.ContentBlockParam[],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const thisToolsTime = Date.now() - toolsStartTime
|
const thisToolsTime = Date.now() - toolsStartTime
|
||||||
toolsTime += thisToolsTime
|
toolsTime += thisToolsTime
|
||||||
|
|
||||||
const nextPayload = {
|
const nextPayload: AnthropicPayload = {
|
||||||
...intermediatePayload,
|
...payload,
|
||||||
messages: currentMessages,
|
messages: currentMessages,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Per Anthropic docs: forced tool_choice is incompatible with thinking.
|
||||||
|
// Only auto and none are supported when thinking is enabled.
|
||||||
|
const thinkingEnabled = !!payload.thinking
|
||||||
if (
|
if (
|
||||||
|
!thinkingEnabled &&
|
||||||
typeof originalToolChoice === 'object' &&
|
typeof originalToolChoice === 'object' &&
|
||||||
hasUsedForcedTool &&
|
hasUsedForcedTool &&
|
||||||
forcedTools.length > 0
|
forcedTools.length > 0
|
||||||
@@ -624,7 +694,11 @@ export async function executeAnthropicProviderRequest(
|
|||||||
nextPayload.tool_choice = undefined
|
nextPayload.tool_choice = undefined
|
||||||
logger.info('All forced tools have been used, removing tool_choice parameter')
|
logger.info('All forced tools have been used, removing tool_choice parameter')
|
||||||
}
|
}
|
||||||
} else if (hasUsedForcedTool && typeof originalToolChoice === 'object') {
|
} else if (
|
||||||
|
!thinkingEnabled &&
|
||||||
|
hasUsedForcedTool &&
|
||||||
|
typeof originalToolChoice === 'object'
|
||||||
|
) {
|
||||||
nextPayload.tool_choice = undefined
|
nextPayload.tool_choice = undefined
|
||||||
logger.info(
|
logger.info(
|
||||||
'Removing tool_choice parameter for subsequent requests after forced tool was used'
|
'Removing tool_choice parameter for subsequent requests after forced tool was used'
|
||||||
@@ -633,7 +707,7 @@ export async function executeAnthropicProviderRequest(
|
|||||||
|
|
||||||
const nextModelStartTime = Date.now()
|
const nextModelStartTime = Date.now()
|
||||||
|
|
||||||
currentResponse = await anthropic.messages.create(nextPayload)
|
currentResponse = await createMessage(anthropic, nextPayload)
|
||||||
|
|
||||||
const nextCheckResult = checkForForcedToolUsage(
|
const nextCheckResult = checkForForcedToolUsage(
|
||||||
currentResponse,
|
currentResponse,
|
||||||
@@ -682,10 +756,14 @@ export async function executeAnthropicProviderRequest(
|
|||||||
tool_choice: undefined,
|
tool_choice: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
const streamResponse: any = await anthropic.messages.create(streamingPayload)
|
const streamResponse = await anthropic.messages.create(
|
||||||
|
streamingPayload as Anthropic.Messages.MessageCreateParamsStreaming
|
||||||
|
)
|
||||||
|
|
||||||
const streamingResult = {
|
const streamingResult = {
|
||||||
stream: createReadableStreamFromAnthropicStream(streamResponse, (streamContent, usage) => {
|
stream: createReadableStreamFromAnthropicStream(
|
||||||
|
streamResponse as AsyncIterable<RawMessageStreamEvent>,
|
||||||
|
(streamContent, usage) => {
|
||||||
streamingResult.execution.output.content = streamContent
|
streamingResult.execution.output.content = streamContent
|
||||||
streamingResult.execution.output.tokens = {
|
streamingResult.execution.output.tokens = {
|
||||||
input: tokens.input + usage.input_tokens,
|
input: tokens.input + usage.input_tokens,
|
||||||
@@ -708,7 +786,8 @@ export async function executeAnthropicProviderRequest(
|
|||||||
streamingResult.execution.output.providerTiming.duration =
|
streamingResult.execution.output.providerTiming.duration =
|
||||||
streamEndTime - providerStartTime
|
streamEndTime - providerStartTime
|
||||||
}
|
}
|
||||||
}),
|
}
|
||||||
|
),
|
||||||
execution: {
|
execution: {
|
||||||
success: true,
|
success: true,
|
||||||
output: {
|
output: {
|
||||||
@@ -778,21 +857,13 @@ export async function executeAnthropicProviderRequest(
|
|||||||
const providerStartTime = Date.now()
|
const providerStartTime = Date.now()
|
||||||
const providerStartTimeISO = new Date(providerStartTime).toISOString()
|
const providerStartTimeISO = new Date(providerStartTime).toISOString()
|
||||||
|
|
||||||
// Cap intermediate calls at non-streaming limit to avoid SDK timeout errors,
|
|
||||||
// but allow users to set lower values if desired
|
|
||||||
const nonStreamingLimit = getMaxOutputTokensForModel(request.model, false)
|
|
||||||
const toolLoopMaxTokens = request.maxTokens
|
|
||||||
? Math.min(Number.parseInt(String(request.maxTokens)), nonStreamingLimit)
|
|
||||||
: nonStreamingLimit
|
|
||||||
const toolLoopPayload = { ...payload, max_tokens: toolLoopMaxTokens }
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const initialCallTime = Date.now()
|
const initialCallTime = Date.now()
|
||||||
const originalToolChoice = toolLoopPayload.tool_choice
|
const originalToolChoice = payload.tool_choice
|
||||||
const forcedTools = preparedTools?.forcedTools || []
|
const forcedTools = preparedTools?.forcedTools || []
|
||||||
let usedForcedTools: string[] = []
|
let usedForcedTools: string[] = []
|
||||||
|
|
||||||
let currentResponse = await anthropic.messages.create(toolLoopPayload)
|
let currentResponse = await createMessage(anthropic, payload)
|
||||||
const firstResponseTime = Date.now() - initialCallTime
|
const firstResponseTime = Date.now() - initialCallTime
|
||||||
|
|
||||||
let content = ''
|
let content = ''
|
||||||
@@ -872,7 +943,7 @@ export async function executeAnthropicProviderRequest(
|
|||||||
const toolExecutionPromises = toolUses.map(async (toolUse) => {
|
const toolExecutionPromises = toolUses.map(async (toolUse) => {
|
||||||
const toolCallStartTime = Date.now()
|
const toolCallStartTime = Date.now()
|
||||||
const toolName = toolUse.name
|
const toolName = toolUse.name
|
||||||
const toolArgs = toolUse.input as Record<string, any>
|
const toolArgs = toolUse.input as Record<string, unknown>
|
||||||
// Preserve the original tool_use ID from Claude's response
|
// Preserve the original tool_use ID from Claude's response
|
||||||
const toolUseId = toolUse.id
|
const toolUseId = toolUse.id
|
||||||
|
|
||||||
@@ -918,17 +989,8 @@ export async function executeAnthropicProviderRequest(
|
|||||||
const executionResults = await Promise.allSettled(toolExecutionPromises)
|
const executionResults = await Promise.allSettled(toolExecutionPromises)
|
||||||
|
|
||||||
// Collect all tool_use and tool_result blocks for batching
|
// Collect all tool_use and tool_result blocks for batching
|
||||||
const toolUseBlocks: Array<{
|
const toolUseBlocks: Anthropic.Messages.ToolUseBlockParam[] = []
|
||||||
type: 'tool_use'
|
const toolResultBlocks: Anthropic.Messages.ToolResultBlockParam[] = []
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
input: Record<string, unknown>
|
|
||||||
}> = []
|
|
||||||
const toolResultBlocks: Array<{
|
|
||||||
type: 'tool_result'
|
|
||||||
tool_use_id: string
|
|
||||||
content: string
|
|
||||||
}> = []
|
|
||||||
|
|
||||||
for (const settledResult of executionResults) {
|
for (const settledResult of executionResults) {
|
||||||
if (settledResult.status === 'rejected' || !settledResult.value) continue
|
if (settledResult.status === 'rejected' || !settledResult.value) continue
|
||||||
@@ -989,11 +1051,23 @@ export async function executeAnthropicProviderRequest(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add ONE assistant message with ALL tool_use blocks
|
// Per Anthropic docs: thinking blocks must be preserved in assistant messages
|
||||||
|
// during tool use to maintain reasoning continuity.
|
||||||
|
const thinkingBlocks = currentResponse.content.filter(
|
||||||
|
(
|
||||||
|
item
|
||||||
|
): item is Anthropic.Messages.ThinkingBlock | Anthropic.Messages.RedactedThinkingBlock =>
|
||||||
|
item.type === 'thinking' || item.type === 'redacted_thinking'
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add ONE assistant message with thinking + tool_use blocks
|
||||||
if (toolUseBlocks.length > 0) {
|
if (toolUseBlocks.length > 0) {
|
||||||
currentMessages.push({
|
currentMessages.push({
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
content: toolUseBlocks as unknown as Anthropic.Messages.ContentBlock[],
|
content: [
|
||||||
|
...thinkingBlocks,
|
||||||
|
...toolUseBlocks,
|
||||||
|
] as Anthropic.Messages.ContentBlockParam[],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1001,19 +1075,27 @@ export async function executeAnthropicProviderRequest(
|
|||||||
if (toolResultBlocks.length > 0) {
|
if (toolResultBlocks.length > 0) {
|
||||||
currentMessages.push({
|
currentMessages.push({
|
||||||
role: 'user',
|
role: 'user',
|
||||||
content: toolResultBlocks as unknown as Anthropic.Messages.ContentBlockParam[],
|
content: toolResultBlocks as Anthropic.Messages.ContentBlockParam[],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const thisToolsTime = Date.now() - toolsStartTime
|
const thisToolsTime = Date.now() - toolsStartTime
|
||||||
toolsTime += thisToolsTime
|
toolsTime += thisToolsTime
|
||||||
|
|
||||||
const nextPayload = {
|
const nextPayload: AnthropicPayload = {
|
||||||
...toolLoopPayload,
|
...payload,
|
||||||
messages: currentMessages,
|
messages: currentMessages,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof originalToolChoice === 'object' && hasUsedForcedTool && forcedTools.length > 0) {
|
// Per Anthropic docs: forced tool_choice is incompatible with thinking.
|
||||||
|
// Only auto and none are supported when thinking is enabled.
|
||||||
|
const thinkingEnabled = !!payload.thinking
|
||||||
|
if (
|
||||||
|
!thinkingEnabled &&
|
||||||
|
typeof originalToolChoice === 'object' &&
|
||||||
|
hasUsedForcedTool &&
|
||||||
|
forcedTools.length > 0
|
||||||
|
) {
|
||||||
const remainingTools = forcedTools.filter((tool) => !usedForcedTools.includes(tool))
|
const remainingTools = forcedTools.filter((tool) => !usedForcedTools.includes(tool))
|
||||||
|
|
||||||
if (remainingTools.length > 0) {
|
if (remainingTools.length > 0) {
|
||||||
@@ -1026,7 +1108,11 @@ export async function executeAnthropicProviderRequest(
|
|||||||
nextPayload.tool_choice = undefined
|
nextPayload.tool_choice = undefined
|
||||||
logger.info('All forced tools have been used, removing tool_choice parameter')
|
logger.info('All forced tools have been used, removing tool_choice parameter')
|
||||||
}
|
}
|
||||||
} else if (hasUsedForcedTool && typeof originalToolChoice === 'object') {
|
} else if (
|
||||||
|
!thinkingEnabled &&
|
||||||
|
hasUsedForcedTool &&
|
||||||
|
typeof originalToolChoice === 'object'
|
||||||
|
) {
|
||||||
nextPayload.tool_choice = undefined
|
nextPayload.tool_choice = undefined
|
||||||
logger.info(
|
logger.info(
|
||||||
'Removing tool_choice parameter for subsequent requests after forced tool was used'
|
'Removing tool_choice parameter for subsequent requests after forced tool was used'
|
||||||
@@ -1035,7 +1121,7 @@ export async function executeAnthropicProviderRequest(
|
|||||||
|
|
||||||
const nextModelStartTime = Date.now()
|
const nextModelStartTime = Date.now()
|
||||||
|
|
||||||
currentResponse = await anthropic.messages.create(nextPayload)
|
currentResponse = await createMessage(anthropic, nextPayload)
|
||||||
|
|
||||||
const nextCheckResult = checkForForcedToolUsage(
|
const nextCheckResult = checkForForcedToolUsage(
|
||||||
currentResponse,
|
currentResponse,
|
||||||
@@ -1098,10 +1184,14 @@ export async function executeAnthropicProviderRequest(
|
|||||||
tool_choice: undefined,
|
tool_choice: undefined,
|
||||||
}
|
}
|
||||||
|
|
||||||
const streamResponse: any = await anthropic.messages.create(streamingPayload)
|
const streamResponse = await anthropic.messages.create(
|
||||||
|
streamingPayload as Anthropic.Messages.MessageCreateParamsStreaming
|
||||||
|
)
|
||||||
|
|
||||||
const streamingResult = {
|
const streamingResult = {
|
||||||
stream: createReadableStreamFromAnthropicStream(streamResponse, (streamContent, usage) => {
|
stream: createReadableStreamFromAnthropicStream(
|
||||||
|
streamResponse as AsyncIterable<RawMessageStreamEvent>,
|
||||||
|
(streamContent, usage) => {
|
||||||
streamingResult.execution.output.content = streamContent
|
streamingResult.execution.output.content = streamContent
|
||||||
streamingResult.execution.output.tokens = {
|
streamingResult.execution.output.tokens = {
|
||||||
input: tokens.input + usage.input_tokens,
|
input: tokens.input + usage.input_tokens,
|
||||||
@@ -1124,7 +1214,8 @@ export async function executeAnthropicProviderRequest(
|
|||||||
streamingResult.execution.output.providerTiming.duration =
|
streamingResult.execution.output.providerTiming.duration =
|
||||||
streamEndTime - providerStartTime
|
streamEndTime - providerStartTime
|
||||||
}
|
}
|
||||||
}),
|
}
|
||||||
|
),
|
||||||
execution: {
|
execution: {
|
||||||
success: true,
|
success: true,
|
||||||
output: {
|
output: {
|
||||||
@@ -1179,7 +1270,7 @@ export async function executeAnthropicProviderRequest(
|
|||||||
toolCalls.length > 0
|
toolCalls.length > 0
|
||||||
? toolCalls.map((tc) => ({
|
? toolCalls.map((tc) => ({
|
||||||
name: tc.name,
|
name: tc.name,
|
||||||
arguments: tc.arguments as Record<string, any>,
|
arguments: tc.arguments as Record<string, unknown>,
|
||||||
startTime: tc.startTime,
|
startTime: tc.startTime,
|
||||||
endTime: tc.endTime,
|
endTime: tc.endTime,
|
||||||
duration: tc.duration,
|
duration: tc.duration,
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
import { AzureOpenAI } from 'openai'
|
import { AzureOpenAI } from 'openai'
|
||||||
import type { ChatCompletionCreateParamsStreaming } from 'openai/resources/chat/completions'
|
import type {
|
||||||
|
ChatCompletion,
|
||||||
|
ChatCompletionCreateParamsBase,
|
||||||
|
ChatCompletionCreateParamsStreaming,
|
||||||
|
ChatCompletionMessageParam,
|
||||||
|
ChatCompletionTool,
|
||||||
|
ChatCompletionToolChoiceOption,
|
||||||
|
} from 'openai/resources/chat/completions'
|
||||||
|
import type { ReasoningEffort } from 'openai/resources/shared'
|
||||||
import { env } from '@/lib/core/config/env'
|
import { env } from '@/lib/core/config/env'
|
||||||
import type { StreamingExecution } from '@/executor/types'
|
import type { StreamingExecution } from '@/executor/types'
|
||||||
import { MAX_TOOL_ITERATIONS } from '@/providers'
|
import { MAX_TOOL_ITERATIONS } from '@/providers'
|
||||||
@@ -16,6 +24,7 @@ import {
|
|||||||
import { getProviderDefaultModel, getProviderModels } from '@/providers/models'
|
import { getProviderDefaultModel, getProviderModels } from '@/providers/models'
|
||||||
import { executeResponsesProviderRequest } from '@/providers/openai/core'
|
import { executeResponsesProviderRequest } from '@/providers/openai/core'
|
||||||
import type {
|
import type {
|
||||||
|
FunctionCallResponse,
|
||||||
ProviderConfig,
|
ProviderConfig,
|
||||||
ProviderRequest,
|
ProviderRequest,
|
||||||
ProviderResponse,
|
ProviderResponse,
|
||||||
@@ -59,7 +68,7 @@ async function executeChatCompletionsRequest(
|
|||||||
endpoint: azureEndpoint,
|
endpoint: azureEndpoint,
|
||||||
})
|
})
|
||||||
|
|
||||||
const allMessages: any[] = []
|
const allMessages: ChatCompletionMessageParam[] = []
|
||||||
|
|
||||||
if (request.systemPrompt) {
|
if (request.systemPrompt) {
|
||||||
allMessages.push({
|
allMessages.push({
|
||||||
@@ -76,12 +85,12 @@ async function executeChatCompletionsRequest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (request.messages) {
|
if (request.messages) {
|
||||||
allMessages.push(...request.messages)
|
allMessages.push(...(request.messages as ChatCompletionMessageParam[]))
|
||||||
}
|
}
|
||||||
|
|
||||||
const tools = request.tools?.length
|
const tools: ChatCompletionTool[] | undefined = request.tools?.length
|
||||||
? request.tools.map((tool) => ({
|
? request.tools.map((tool) => ({
|
||||||
type: 'function',
|
type: 'function' as const,
|
||||||
function: {
|
function: {
|
||||||
name: tool.id,
|
name: tool.id,
|
||||||
description: tool.description,
|
description: tool.description,
|
||||||
@@ -90,7 +99,7 @@ async function executeChatCompletionsRequest(
|
|||||||
}))
|
}))
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
const payload: any = {
|
const payload: ChatCompletionCreateParamsBase & { verbosity?: string } = {
|
||||||
model: deploymentName,
|
model: deploymentName,
|
||||||
messages: allMessages,
|
messages: allMessages,
|
||||||
}
|
}
|
||||||
@@ -98,8 +107,10 @@ async function executeChatCompletionsRequest(
|
|||||||
if (request.temperature !== undefined) payload.temperature = request.temperature
|
if (request.temperature !== undefined) payload.temperature = request.temperature
|
||||||
if (request.maxTokens != null) payload.max_completion_tokens = request.maxTokens
|
if (request.maxTokens != null) payload.max_completion_tokens = request.maxTokens
|
||||||
|
|
||||||
if (request.reasoningEffort !== undefined) payload.reasoning_effort = request.reasoningEffort
|
if (request.reasoningEffort !== undefined && request.reasoningEffort !== 'auto')
|
||||||
if (request.verbosity !== undefined) payload.verbosity = request.verbosity
|
payload.reasoning_effort = request.reasoningEffort as ReasoningEffort
|
||||||
|
if (request.verbosity !== undefined && request.verbosity !== 'auto')
|
||||||
|
payload.verbosity = request.verbosity
|
||||||
|
|
||||||
if (request.responseFormat) {
|
if (request.responseFormat) {
|
||||||
payload.response_format = {
|
payload.response_format = {
|
||||||
@@ -121,8 +132,8 @@ async function executeChatCompletionsRequest(
|
|||||||
const { tools: filteredTools, toolChoice } = preparedTools
|
const { tools: filteredTools, toolChoice } = preparedTools
|
||||||
|
|
||||||
if (filteredTools?.length && toolChoice) {
|
if (filteredTools?.length && toolChoice) {
|
||||||
payload.tools = filteredTools
|
payload.tools = filteredTools as ChatCompletionTool[]
|
||||||
payload.tool_choice = toolChoice
|
payload.tool_choice = toolChoice as ChatCompletionToolChoiceOption
|
||||||
|
|
||||||
logger.info('Azure OpenAI request configuration:', {
|
logger.info('Azure OpenAI request configuration:', {
|
||||||
toolCount: filteredTools.length,
|
toolCount: filteredTools.length,
|
||||||
@@ -231,7 +242,7 @@ async function executeChatCompletionsRequest(
|
|||||||
const forcedTools = preparedTools?.forcedTools || []
|
const forcedTools = preparedTools?.forcedTools || []
|
||||||
let usedForcedTools: string[] = []
|
let usedForcedTools: string[] = []
|
||||||
|
|
||||||
let currentResponse = await azureOpenAI.chat.completions.create(payload)
|
let currentResponse = (await azureOpenAI.chat.completions.create(payload)) as ChatCompletion
|
||||||
const firstResponseTime = Date.now() - initialCallTime
|
const firstResponseTime = Date.now() - initialCallTime
|
||||||
|
|
||||||
let content = currentResponse.choices[0]?.message?.content || ''
|
let content = currentResponse.choices[0]?.message?.content || ''
|
||||||
@@ -240,8 +251,8 @@ async function executeChatCompletionsRequest(
|
|||||||
output: currentResponse.usage?.completion_tokens || 0,
|
output: currentResponse.usage?.completion_tokens || 0,
|
||||||
total: currentResponse.usage?.total_tokens || 0,
|
total: currentResponse.usage?.total_tokens || 0,
|
||||||
}
|
}
|
||||||
const toolCalls = []
|
const toolCalls: (FunctionCallResponse & { success: boolean })[] = []
|
||||||
const toolResults = []
|
const toolResults: Record<string, unknown>[] = []
|
||||||
const currentMessages = [...allMessages]
|
const currentMessages = [...allMessages]
|
||||||
let iterationCount = 0
|
let iterationCount = 0
|
||||||
let modelTime = firstResponseTime
|
let modelTime = firstResponseTime
|
||||||
@@ -260,7 +271,7 @@ async function executeChatCompletionsRequest(
|
|||||||
|
|
||||||
const firstCheckResult = checkForForcedToolUsage(
|
const firstCheckResult = checkForForcedToolUsage(
|
||||||
currentResponse,
|
currentResponse,
|
||||||
originalToolChoice,
|
originalToolChoice ?? 'auto',
|
||||||
logger,
|
logger,
|
||||||
forcedTools,
|
forcedTools,
|
||||||
usedForcedTools
|
usedForcedTools
|
||||||
@@ -356,10 +367,10 @@ async function executeChatCompletionsRequest(
|
|||||||
duration: duration,
|
duration: duration,
|
||||||
})
|
})
|
||||||
|
|
||||||
let resultContent: any
|
let resultContent: Record<string, unknown>
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
toolResults.push(result.output)
|
toolResults.push(result.output as Record<string, unknown>)
|
||||||
resultContent = result.output
|
resultContent = result.output as Record<string, unknown>
|
||||||
} else {
|
} else {
|
||||||
resultContent = {
|
resultContent = {
|
||||||
error: true,
|
error: true,
|
||||||
@@ -409,11 +420,11 @@ async function executeChatCompletionsRequest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const nextModelStartTime = Date.now()
|
const nextModelStartTime = Date.now()
|
||||||
currentResponse = await azureOpenAI.chat.completions.create(nextPayload)
|
currentResponse = (await azureOpenAI.chat.completions.create(nextPayload)) as ChatCompletion
|
||||||
|
|
||||||
const nextCheckResult = checkForForcedToolUsage(
|
const nextCheckResult = checkForForcedToolUsage(
|
||||||
currentResponse,
|
currentResponse,
|
||||||
nextPayload.tool_choice,
|
nextPayload.tool_choice ?? 'auto',
|
||||||
logger,
|
logger,
|
||||||
forcedTools,
|
forcedTools,
|
||||||
usedForcedTools
|
usedForcedTools
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Logger } from '@sim/logger'
|
import type { Logger } from '@sim/logger'
|
||||||
|
import type OpenAI from 'openai'
|
||||||
import type { ChatCompletionChunk } from 'openai/resources/chat/completions'
|
import type { ChatCompletionChunk } from 'openai/resources/chat/completions'
|
||||||
import type { CompletionUsage } from 'openai/resources/completions'
|
import type { CompletionUsage } from 'openai/resources/completions'
|
||||||
import type { Stream } from 'openai/streaming'
|
import type { Stream } from 'openai/streaming'
|
||||||
@@ -20,8 +21,8 @@ export function createReadableStreamFromAzureOpenAIStream(
|
|||||||
* Uses the shared OpenAI-compatible forced tool usage helper.
|
* Uses the shared OpenAI-compatible forced tool usage helper.
|
||||||
*/
|
*/
|
||||||
export function checkForForcedToolUsage(
|
export function checkForForcedToolUsage(
|
||||||
response: any,
|
response: OpenAI.Chat.Completions.ChatCompletion,
|
||||||
toolChoice: string | { type: string; function?: { name: string }; name?: string; any?: any },
|
toolChoice: string | { type: string; function?: { name: string }; name?: string },
|
||||||
_logger: Logger,
|
_logger: Logger,
|
||||||
forcedTools: string[],
|
forcedTools: string[],
|
||||||
usedForcedTools: string[]
|
usedForcedTools: string[]
|
||||||
|
|||||||
@@ -197,6 +197,9 @@ export const bedrockProvider: ProviderConfig = {
|
|||||||
} else if (tc.type === 'function' && tc.function?.name) {
|
} else if (tc.type === 'function' && tc.function?.name) {
|
||||||
toolChoice = { tool: { name: tc.function.name } }
|
toolChoice = { tool: { name: tc.function.name } }
|
||||||
logger.info(`Using Bedrock tool_choice format: force tool "${tc.function.name}"`)
|
logger.info(`Using Bedrock tool_choice format: force tool "${tc.function.name}"`)
|
||||||
|
} else if (tc.type === 'any') {
|
||||||
|
toolChoice = { any: {} }
|
||||||
|
logger.info('Using Bedrock tool_choice format: any tool')
|
||||||
} else {
|
} else {
|
||||||
toolChoice = { auto: {} }
|
toolChoice = { auto: {} }
|
||||||
}
|
}
|
||||||
@@ -413,6 +416,7 @@ export const bedrockProvider: ProviderConfig = {
|
|||||||
input: initialCost.input,
|
input: initialCost.input,
|
||||||
output: initialCost.output,
|
output: initialCost.output,
|
||||||
total: initialCost.total,
|
total: initialCost.total,
|
||||||
|
pricing: initialCost.pricing,
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolCalls: any[] = []
|
const toolCalls: any[] = []
|
||||||
@@ -860,6 +864,12 @@ export const bedrockProvider: ProviderConfig = {
|
|||||||
content,
|
content,
|
||||||
model: request.model,
|
model: request.model,
|
||||||
tokens,
|
tokens,
|
||||||
|
cost: {
|
||||||
|
input: cost.input,
|
||||||
|
output: cost.output,
|
||||||
|
total: cost.total,
|
||||||
|
pricing: cost.pricing,
|
||||||
|
},
|
||||||
toolCalls:
|
toolCalls:
|
||||||
toolCalls.length > 0
|
toolCalls.length > 0
|
||||||
? toolCalls.map((tc) => ({
|
? toolCalls.map((tc) => ({
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import {
|
|||||||
extractTextContent,
|
extractTextContent,
|
||||||
mapToThinkingLevel,
|
mapToThinkingLevel,
|
||||||
} from '@/providers/google/utils'
|
} from '@/providers/google/utils'
|
||||||
import { getThinkingCapability } from '@/providers/models'
|
|
||||||
import type { FunctionCallResponse, ProviderRequest, ProviderResponse } from '@/providers/types'
|
import type { FunctionCallResponse, ProviderRequest, ProviderResponse } from '@/providers/types'
|
||||||
import {
|
import {
|
||||||
calculateCost,
|
calculateCost,
|
||||||
@@ -432,13 +431,11 @@ export async function executeGeminiRequest(
|
|||||||
logger.warn('Gemini does not support responseFormat with tools. Structured output ignored.')
|
logger.warn('Gemini does not support responseFormat with tools. Structured output ignored.')
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure thinking for models that support it
|
// Configure thinking only when the user explicitly selects a thinking level
|
||||||
const thinkingCapability = getThinkingCapability(model)
|
if (request.thinkingLevel && request.thinkingLevel !== 'none') {
|
||||||
if (thinkingCapability) {
|
|
||||||
const level = request.thinkingLevel ?? thinkingCapability.default ?? 'high'
|
|
||||||
const thinkingConfig: ThinkingConfig = {
|
const thinkingConfig: ThinkingConfig = {
|
||||||
includeThoughts: false,
|
includeThoughts: false,
|
||||||
thinkingLevel: mapToThinkingLevel(level),
|
thinkingLevel: mapToThinkingLevel(request.thinkingLevel),
|
||||||
}
|
}
|
||||||
geminiConfig.thinkingConfig = thinkingConfig
|
geminiConfig.thinkingConfig = thinkingConfig
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -141,7 +141,6 @@ export const mistralProvider: ProviderConfig = {
|
|||||||
const streamingParams: ChatCompletionCreateParamsStreaming = {
|
const streamingParams: ChatCompletionCreateParamsStreaming = {
|
||||||
...payload,
|
...payload,
|
||||||
stream: true,
|
stream: true,
|
||||||
stream_options: { include_usage: true },
|
|
||||||
}
|
}
|
||||||
const streamResponse = await mistral.chat.completions.create(streamingParams)
|
const streamResponse = await mistral.chat.completions.create(streamingParams)
|
||||||
|
|
||||||
@@ -453,7 +452,6 @@ export const mistralProvider: ProviderConfig = {
|
|||||||
messages: currentMessages,
|
messages: currentMessages,
|
||||||
tool_choice: 'auto',
|
tool_choice: 'auto',
|
||||||
stream: true,
|
stream: true,
|
||||||
stream_options: { include_usage: true },
|
|
||||||
}
|
}
|
||||||
const streamResponse = await mistral.chat.completions.create(streamingParams)
|
const streamResponse = await mistral.chat.completions.create(streamingParams)
|
||||||
|
|
||||||
|
|||||||
@@ -34,17 +34,8 @@ export interface ModelCapabilities {
|
|||||||
toolUsageControl?: boolean
|
toolUsageControl?: boolean
|
||||||
computerUse?: boolean
|
computerUse?: boolean
|
||||||
nativeStructuredOutputs?: boolean
|
nativeStructuredOutputs?: boolean
|
||||||
/**
|
/** Maximum supported output tokens for this model */
|
||||||
* Max output tokens configuration for Anthropic SDK's streaming timeout workaround.
|
maxOutputTokens?: number
|
||||||
* The Anthropic SDK throws an error for non-streaming requests that may take >10 minutes.
|
|
||||||
* This only applies to direct Anthropic API calls, not Bedrock (which uses AWS SDK).
|
|
||||||
*/
|
|
||||||
maxOutputTokens?: {
|
|
||||||
/** Maximum tokens for streaming requests */
|
|
||||||
max: number
|
|
||||||
/** Safe default for non-streaming requests (to avoid Anthropic SDK timeout errors) */
|
|
||||||
default: number
|
|
||||||
}
|
|
||||||
reasoningEffort?: {
|
reasoningEffort?: {
|
||||||
values: string[]
|
values: string[]
|
||||||
}
|
}
|
||||||
@@ -109,7 +100,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
|||||||
name: 'OpenAI',
|
name: 'OpenAI',
|
||||||
description: "OpenAI's models",
|
description: "OpenAI's models",
|
||||||
defaultModel: 'gpt-4o',
|
defaultModel: 'gpt-4o',
|
||||||
modelPatterns: [/^gpt/, /^o1/, /^text-embedding/],
|
modelPatterns: [/^gpt/, /^o\d/, /^text-embedding/],
|
||||||
icon: OpenAIIcon,
|
icon: OpenAIIcon,
|
||||||
capabilities: {
|
capabilities: {
|
||||||
toolUsageControl: true,
|
toolUsageControl: true,
|
||||||
@@ -138,7 +129,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
|||||||
},
|
},
|
||||||
capabilities: {
|
capabilities: {
|
||||||
reasoningEffort: {
|
reasoningEffort: {
|
||||||
values: ['none', 'minimal', 'low', 'medium', 'high', 'xhigh'],
|
values: ['none', 'low', 'medium', 'high', 'xhigh'],
|
||||||
},
|
},
|
||||||
verbosity: {
|
verbosity: {
|
||||||
values: ['low', 'medium', 'high'],
|
values: ['low', 'medium', 'high'],
|
||||||
@@ -164,60 +155,6 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
|||||||
},
|
},
|
||||||
contextWindow: 400000,
|
contextWindow: 400000,
|
||||||
},
|
},
|
||||||
// {
|
|
||||||
// id: 'gpt-5.1-mini',
|
|
||||||
// pricing: {
|
|
||||||
// input: 0.25,
|
|
||||||
// cachedInput: 0.025,
|
|
||||||
// output: 2.0,
|
|
||||||
// updatedAt: '2025-11-14',
|
|
||||||
// },
|
|
||||||
// capabilities: {
|
|
||||||
// reasoningEffort: {
|
|
||||||
// values: ['none', 'low', 'medium', 'high'],
|
|
||||||
// },
|
|
||||||
// verbosity: {
|
|
||||||
// values: ['low', 'medium', 'high'],
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// contextWindow: 400000,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: 'gpt-5.1-nano',
|
|
||||||
// pricing: {
|
|
||||||
// input: 0.05,
|
|
||||||
// cachedInput: 0.005,
|
|
||||||
// output: 0.4,
|
|
||||||
// updatedAt: '2025-11-14',
|
|
||||||
// },
|
|
||||||
// capabilities: {
|
|
||||||
// reasoningEffort: {
|
|
||||||
// values: ['none', 'low', 'medium', 'high'],
|
|
||||||
// },
|
|
||||||
// verbosity: {
|
|
||||||
// values: ['low', 'medium', 'high'],
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// contextWindow: 400000,
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// id: 'gpt-5.1-codex',
|
|
||||||
// pricing: {
|
|
||||||
// input: 1.25,
|
|
||||||
// cachedInput: 0.125,
|
|
||||||
// output: 10.0,
|
|
||||||
// updatedAt: '2025-11-14',
|
|
||||||
// },
|
|
||||||
// capabilities: {
|
|
||||||
// reasoningEffort: {
|
|
||||||
// values: ['none', 'medium', 'high'],
|
|
||||||
// },
|
|
||||||
// verbosity: {
|
|
||||||
// values: ['low', 'medium', 'high'],
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// contextWindow: 400000,
|
|
||||||
// },
|
|
||||||
{
|
{
|
||||||
id: 'gpt-5',
|
id: 'gpt-5',
|
||||||
pricing: {
|
pricing: {
|
||||||
@@ -280,8 +217,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
|||||||
output: 10.0,
|
output: 10.0,
|
||||||
updatedAt: '2025-08-07',
|
updatedAt: '2025-08-07',
|
||||||
},
|
},
|
||||||
capabilities: {},
|
capabilities: {
|
||||||
contextWindow: 400000,
|
temperature: { min: 0, max: 2 },
|
||||||
|
},
|
||||||
|
contextWindow: 128000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'o1',
|
id: 'o1',
|
||||||
@@ -311,7 +250,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
|||||||
values: ['low', 'medium', 'high'],
|
values: ['low', 'medium', 'high'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
contextWindow: 128000,
|
contextWindow: 200000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'o4-mini',
|
id: 'o4-mini',
|
||||||
@@ -326,7 +265,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
|||||||
values: ['low', 'medium', 'high'],
|
values: ['low', 'medium', 'high'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
contextWindow: 128000,
|
contextWindow: 200000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'gpt-4.1',
|
id: 'gpt-4.1',
|
||||||
@@ -391,7 +330,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
|||||||
capabilities: {
|
capabilities: {
|
||||||
temperature: { min: 0, max: 1 },
|
temperature: { min: 0, max: 1 },
|
||||||
nativeStructuredOutputs: true,
|
nativeStructuredOutputs: true,
|
||||||
maxOutputTokens: { max: 128000, default: 8192 },
|
maxOutputTokens: 128000,
|
||||||
thinking: {
|
thinking: {
|
||||||
levels: ['low', 'medium', 'high', 'max'],
|
levels: ['low', 'medium', 'high', 'max'],
|
||||||
default: 'high',
|
default: 'high',
|
||||||
@@ -410,10 +349,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
|||||||
capabilities: {
|
capabilities: {
|
||||||
temperature: { min: 0, max: 1 },
|
temperature: { min: 0, max: 1 },
|
||||||
nativeStructuredOutputs: true,
|
nativeStructuredOutputs: true,
|
||||||
maxOutputTokens: { max: 64000, default: 8192 },
|
maxOutputTokens: 64000,
|
||||||
thinking: {
|
thinking: {
|
||||||
levels: ['low', 'medium', 'high'],
|
levels: ['low', 'medium', 'high'],
|
||||||
default: 'medium',
|
default: 'high',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
@@ -429,10 +368,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
|||||||
capabilities: {
|
capabilities: {
|
||||||
temperature: { min: 0, max: 1 },
|
temperature: { min: 0, max: 1 },
|
||||||
nativeStructuredOutputs: true,
|
nativeStructuredOutputs: true,
|
||||||
maxOutputTokens: { max: 64000, default: 8192 },
|
maxOutputTokens: 64000,
|
||||||
thinking: {
|
thinking: {
|
||||||
levels: ['low', 'medium', 'high'],
|
levels: ['low', 'medium', 'high'],
|
||||||
default: 'medium',
|
default: 'high',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
@@ -447,10 +386,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
|||||||
},
|
},
|
||||||
capabilities: {
|
capabilities: {
|
||||||
temperature: { min: 0, max: 1 },
|
temperature: { min: 0, max: 1 },
|
||||||
maxOutputTokens: { max: 64000, default: 8192 },
|
maxOutputTokens: 64000,
|
||||||
thinking: {
|
thinking: {
|
||||||
levels: ['low', 'medium', 'high'],
|
levels: ['low', 'medium', 'high'],
|
||||||
default: 'medium',
|
default: 'high',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
@@ -466,10 +405,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
|||||||
capabilities: {
|
capabilities: {
|
||||||
temperature: { min: 0, max: 1 },
|
temperature: { min: 0, max: 1 },
|
||||||
nativeStructuredOutputs: true,
|
nativeStructuredOutputs: true,
|
||||||
maxOutputTokens: { max: 64000, default: 8192 },
|
maxOutputTokens: 64000,
|
||||||
thinking: {
|
thinking: {
|
||||||
levels: ['low', 'medium', 'high'],
|
levels: ['low', 'medium', 'high'],
|
||||||
default: 'medium',
|
default: 'high',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
@@ -484,10 +423,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
|||||||
},
|
},
|
||||||
capabilities: {
|
capabilities: {
|
||||||
temperature: { min: 0, max: 1 },
|
temperature: { min: 0, max: 1 },
|
||||||
maxOutputTokens: { max: 64000, default: 8192 },
|
maxOutputTokens: 64000,
|
||||||
thinking: {
|
thinking: {
|
||||||
levels: ['low', 'medium', 'high'],
|
levels: ['low', 'medium', 'high'],
|
||||||
default: 'medium',
|
default: 'high',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
@@ -503,10 +442,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
|||||||
capabilities: {
|
capabilities: {
|
||||||
temperature: { min: 0, max: 1 },
|
temperature: { min: 0, max: 1 },
|
||||||
nativeStructuredOutputs: true,
|
nativeStructuredOutputs: true,
|
||||||
maxOutputTokens: { max: 64000, default: 8192 },
|
maxOutputTokens: 64000,
|
||||||
thinking: {
|
thinking: {
|
||||||
levels: ['low', 'medium', 'high'],
|
levels: ['low', 'medium', 'high'],
|
||||||
default: 'medium',
|
default: 'high',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
@@ -515,13 +454,13 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
|||||||
id: 'claude-3-haiku-20240307',
|
id: 'claude-3-haiku-20240307',
|
||||||
pricing: {
|
pricing: {
|
||||||
input: 0.25,
|
input: 0.25,
|
||||||
cachedInput: 0.025,
|
cachedInput: 0.03,
|
||||||
output: 1.25,
|
output: 1.25,
|
||||||
updatedAt: '2026-02-05',
|
updatedAt: '2026-02-05',
|
||||||
},
|
},
|
||||||
capabilities: {
|
capabilities: {
|
||||||
temperature: { min: 0, max: 1 },
|
temperature: { min: 0, max: 1 },
|
||||||
maxOutputTokens: { max: 4096, default: 4096 },
|
maxOutputTokens: 4096,
|
||||||
},
|
},
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
},
|
},
|
||||||
@@ -536,10 +475,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
|||||||
capabilities: {
|
capabilities: {
|
||||||
temperature: { min: 0, max: 1 },
|
temperature: { min: 0, max: 1 },
|
||||||
computerUse: true,
|
computerUse: true,
|
||||||
maxOutputTokens: { max: 8192, default: 8192 },
|
maxOutputTokens: 64000,
|
||||||
thinking: {
|
thinking: {
|
||||||
levels: ['low', 'medium', 'high'],
|
levels: ['low', 'medium', 'high'],
|
||||||
default: 'medium',
|
default: 'high',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
@@ -580,7 +519,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
|||||||
},
|
},
|
||||||
capabilities: {
|
capabilities: {
|
||||||
reasoningEffort: {
|
reasoningEffort: {
|
||||||
values: ['none', 'minimal', 'low', 'medium', 'high', 'xhigh'],
|
values: ['none', 'low', 'medium', 'high', 'xhigh'],
|
||||||
},
|
},
|
||||||
verbosity: {
|
verbosity: {
|
||||||
values: ['low', 'medium', 'high'],
|
values: ['low', 'medium', 'high'],
|
||||||
@@ -606,42 +545,6 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
|||||||
},
|
},
|
||||||
contextWindow: 400000,
|
contextWindow: 400000,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'azure/gpt-5.1-mini',
|
|
||||||
pricing: {
|
|
||||||
input: 0.25,
|
|
||||||
cachedInput: 0.025,
|
|
||||||
output: 2.0,
|
|
||||||
updatedAt: '2025-11-14',
|
|
||||||
},
|
|
||||||
capabilities: {
|
|
||||||
reasoningEffort: {
|
|
||||||
values: ['none', 'low', 'medium', 'high'],
|
|
||||||
},
|
|
||||||
verbosity: {
|
|
||||||
values: ['low', 'medium', 'high'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
contextWindow: 400000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'azure/gpt-5.1-nano',
|
|
||||||
pricing: {
|
|
||||||
input: 0.05,
|
|
||||||
cachedInput: 0.005,
|
|
||||||
output: 0.4,
|
|
||||||
updatedAt: '2025-11-14',
|
|
||||||
},
|
|
||||||
capabilities: {
|
|
||||||
reasoningEffort: {
|
|
||||||
values: ['none', 'low', 'medium', 'high'],
|
|
||||||
},
|
|
||||||
verbosity: {
|
|
||||||
values: ['low', 'medium', 'high'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
contextWindow: 400000,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'azure/gpt-5.1-codex',
|
id: 'azure/gpt-5.1-codex',
|
||||||
pricing: {
|
pricing: {
|
||||||
@@ -652,7 +555,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
|||||||
},
|
},
|
||||||
capabilities: {
|
capabilities: {
|
||||||
reasoningEffort: {
|
reasoningEffort: {
|
||||||
values: ['none', 'medium', 'high'],
|
values: ['none', 'low', 'medium', 'high'],
|
||||||
},
|
},
|
||||||
verbosity: {
|
verbosity: {
|
||||||
values: ['low', 'medium', 'high'],
|
values: ['low', 'medium', 'high'],
|
||||||
@@ -722,23 +625,25 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
|||||||
output: 10.0,
|
output: 10.0,
|
||||||
updatedAt: '2025-08-07',
|
updatedAt: '2025-08-07',
|
||||||
},
|
},
|
||||||
capabilities: {},
|
capabilities: {
|
||||||
contextWindow: 400000,
|
temperature: { min: 0, max: 2 },
|
||||||
|
},
|
||||||
|
contextWindow: 128000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'azure/o3',
|
id: 'azure/o3',
|
||||||
pricing: {
|
pricing: {
|
||||||
input: 10,
|
input: 2,
|
||||||
cachedInput: 2.5,
|
cachedInput: 0.5,
|
||||||
output: 40,
|
output: 8,
|
||||||
updatedAt: '2025-06-15',
|
updatedAt: '2026-02-06',
|
||||||
},
|
},
|
||||||
capabilities: {
|
capabilities: {
|
||||||
reasoningEffort: {
|
reasoningEffort: {
|
||||||
values: ['low', 'medium', 'high'],
|
values: ['low', 'medium', 'high'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
contextWindow: 128000,
|
contextWindow: 200000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'azure/o4-mini',
|
id: 'azure/o4-mini',
|
||||||
@@ -753,7 +658,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
|||||||
values: ['low', 'medium', 'high'],
|
values: ['low', 'medium', 'high'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
contextWindow: 128000,
|
contextWindow: 200000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'azure/gpt-4.1',
|
id: 'azure/gpt-4.1',
|
||||||
@@ -763,7 +668,35 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
|||||||
output: 8.0,
|
output: 8.0,
|
||||||
updatedAt: '2025-06-15',
|
updatedAt: '2025-06-15',
|
||||||
},
|
},
|
||||||
capabilities: {},
|
capabilities: {
|
||||||
|
temperature: { min: 0, max: 2 },
|
||||||
|
},
|
||||||
|
contextWindow: 1000000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'azure/gpt-4.1-mini',
|
||||||
|
pricing: {
|
||||||
|
input: 0.4,
|
||||||
|
cachedInput: 0.1,
|
||||||
|
output: 1.6,
|
||||||
|
updatedAt: '2025-06-15',
|
||||||
|
},
|
||||||
|
capabilities: {
|
||||||
|
temperature: { min: 0, max: 2 },
|
||||||
|
},
|
||||||
|
contextWindow: 1000000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'azure/gpt-4.1-nano',
|
||||||
|
pricing: {
|
||||||
|
input: 0.1,
|
||||||
|
cachedInput: 0.025,
|
||||||
|
output: 0.4,
|
||||||
|
updatedAt: '2025-06-15',
|
||||||
|
},
|
||||||
|
capabilities: {
|
||||||
|
temperature: { min: 0, max: 2 },
|
||||||
|
},
|
||||||
contextWindow: 1000000,
|
contextWindow: 1000000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -775,7 +708,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
|||||||
updatedAt: '2025-06-15',
|
updatedAt: '2025-06-15',
|
||||||
},
|
},
|
||||||
capabilities: {},
|
capabilities: {},
|
||||||
contextWindow: 1000000,
|
contextWindow: 200000,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -801,7 +734,7 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
|||||||
capabilities: {
|
capabilities: {
|
||||||
temperature: { min: 0, max: 1 },
|
temperature: { min: 0, max: 1 },
|
||||||
nativeStructuredOutputs: true,
|
nativeStructuredOutputs: true,
|
||||||
maxOutputTokens: { max: 128000, default: 8192 },
|
maxOutputTokens: 128000,
|
||||||
thinking: {
|
thinking: {
|
||||||
levels: ['low', 'medium', 'high', 'max'],
|
levels: ['low', 'medium', 'high', 'max'],
|
||||||
default: 'high',
|
default: 'high',
|
||||||
@@ -820,10 +753,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
|||||||
capabilities: {
|
capabilities: {
|
||||||
temperature: { min: 0, max: 1 },
|
temperature: { min: 0, max: 1 },
|
||||||
nativeStructuredOutputs: true,
|
nativeStructuredOutputs: true,
|
||||||
maxOutputTokens: { max: 64000, default: 8192 },
|
maxOutputTokens: 64000,
|
||||||
thinking: {
|
thinking: {
|
||||||
levels: ['low', 'medium', 'high'],
|
levels: ['low', 'medium', 'high'],
|
||||||
default: 'medium',
|
default: 'high',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
@@ -839,10 +772,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
|||||||
capabilities: {
|
capabilities: {
|
||||||
temperature: { min: 0, max: 1 },
|
temperature: { min: 0, max: 1 },
|
||||||
nativeStructuredOutputs: true,
|
nativeStructuredOutputs: true,
|
||||||
maxOutputTokens: { max: 64000, default: 8192 },
|
maxOutputTokens: 64000,
|
||||||
thinking: {
|
thinking: {
|
||||||
levels: ['low', 'medium', 'high'],
|
levels: ['low', 'medium', 'high'],
|
||||||
default: 'medium',
|
default: 'high',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
@@ -858,10 +791,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
|||||||
capabilities: {
|
capabilities: {
|
||||||
temperature: { min: 0, max: 1 },
|
temperature: { min: 0, max: 1 },
|
||||||
nativeStructuredOutputs: true,
|
nativeStructuredOutputs: true,
|
||||||
maxOutputTokens: { max: 64000, default: 8192 },
|
maxOutputTokens: 64000,
|
||||||
thinking: {
|
thinking: {
|
||||||
levels: ['low', 'medium', 'high'],
|
levels: ['low', 'medium', 'high'],
|
||||||
default: 'medium',
|
default: 'high',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
@@ -877,10 +810,10 @@ export const PROVIDER_DEFINITIONS: Record<string, ProviderDefinition> = {
|
|||||||
capabilities: {
|
capabilities: {
|
||||||
temperature: { min: 0, max: 1 },
|
temperature: { min: 0, max: 1 },
|
||||||
nativeStructuredOutputs: true,
|
nativeStructuredOutputs: true,
|
||||||
maxOutputTokens: { max: 64000, default: 8192 },
|
maxOutputTokens: 64000,
|
||||||
thinking: {
|
thinking: {
|
||||||
levels: ['low', 'medium', 'high'],
|
levels: ['low', 'medium', 'high'],
|
||||||
default: 'medium',
|
default: 'high',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
@@ -2548,14 +2481,11 @@ export function getThinkingLevelsForModel(modelId: string): string[] | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the max output tokens for a specific model
|
* Get the max output tokens for a specific model.
|
||||||
* Returns the model's max capacity for streaming requests,
|
|
||||||
* or the model's safe default for non-streaming requests to avoid timeout issues.
|
|
||||||
*
|
*
|
||||||
* @param modelId - The model ID
|
* @param modelId - The model ID
|
||||||
* @param streaming - Whether the request is streaming (default: false)
|
|
||||||
*/
|
*/
|
||||||
export function getMaxOutputTokensForModel(modelId: string, streaming = false): number {
|
export function getMaxOutputTokensForModel(modelId: string): number {
|
||||||
const normalizedModelId = modelId.toLowerCase()
|
const normalizedModelId = modelId.toLowerCase()
|
||||||
const STANDARD_MAX_OUTPUT_TOKENS = 4096
|
const STANDARD_MAX_OUTPUT_TOKENS = 4096
|
||||||
|
|
||||||
@@ -2563,11 +2493,7 @@ export function getMaxOutputTokensForModel(modelId: string, streaming = false):
|
|||||||
for (const model of provider.models) {
|
for (const model of provider.models) {
|
||||||
const baseModelId = model.id.toLowerCase()
|
const baseModelId = model.id.toLowerCase()
|
||||||
if (normalizedModelId === baseModelId || normalizedModelId.startsWith(`${baseModelId}-`)) {
|
if (normalizedModelId === baseModelId || normalizedModelId.startsWith(`${baseModelId}-`)) {
|
||||||
const outputTokens = model.capabilities.maxOutputTokens
|
return model.capabilities.maxOutputTokens || STANDARD_MAX_OUTPUT_TOKENS
|
||||||
if (outputTokens) {
|
|
||||||
return streaming ? outputTokens.max : outputTokens.default
|
|
||||||
}
|
|
||||||
return STANDARD_MAX_OUTPUT_TOKENS
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Logger } from '@sim/logger'
|
import type { Logger } from '@sim/logger'
|
||||||
|
import type OpenAI from 'openai'
|
||||||
import type { StreamingExecution } from '@/executor/types'
|
import type { StreamingExecution } from '@/executor/types'
|
||||||
import { MAX_TOOL_ITERATIONS } from '@/providers'
|
import { MAX_TOOL_ITERATIONS } from '@/providers'
|
||||||
import type { Message, ProviderRequest, ProviderResponse, TimeSegment } from '@/providers/types'
|
import type { Message, ProviderRequest, ProviderResponse, TimeSegment } from '@/providers/types'
|
||||||
@@ -30,7 +31,7 @@ type ToolChoice = PreparedTools['toolChoice']
|
|||||||
* - Sets additionalProperties: false on all object types.
|
* - Sets additionalProperties: false on all object types.
|
||||||
* - Ensures required includes ALL property keys.
|
* - Ensures required includes ALL property keys.
|
||||||
*/
|
*/
|
||||||
function enforceStrictSchema(schema: any): any {
|
function enforceStrictSchema(schema: Record<string, unknown>): Record<string, unknown> {
|
||||||
if (!schema || typeof schema !== 'object') return schema
|
if (!schema || typeof schema !== 'object') return schema
|
||||||
|
|
||||||
const result = { ...schema }
|
const result = { ...schema }
|
||||||
@@ -41,23 +42,26 @@ function enforceStrictSchema(schema: any): any {
|
|||||||
|
|
||||||
// Recursively process properties and ensure required includes all keys
|
// Recursively process properties and ensure required includes all keys
|
||||||
if (result.properties && typeof result.properties === 'object') {
|
if (result.properties && typeof result.properties === 'object') {
|
||||||
const propKeys = Object.keys(result.properties)
|
const propKeys = Object.keys(result.properties as Record<string, unknown>)
|
||||||
result.required = propKeys // Strict mode requires ALL properties
|
result.required = propKeys // Strict mode requires ALL properties
|
||||||
result.properties = Object.fromEntries(
|
result.properties = Object.fromEntries(
|
||||||
Object.entries(result.properties).map(([key, value]) => [key, enforceStrictSchema(value)])
|
Object.entries(result.properties as Record<string, unknown>).map(([key, value]) => [
|
||||||
|
key,
|
||||||
|
enforceStrictSchema(value as Record<string, unknown>),
|
||||||
|
])
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle array items
|
// Handle array items
|
||||||
if (result.type === 'array' && result.items) {
|
if (result.type === 'array' && result.items) {
|
||||||
result.items = enforceStrictSchema(result.items)
|
result.items = enforceStrictSchema(result.items as Record<string, unknown>)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle anyOf, oneOf, allOf
|
// Handle anyOf, oneOf, allOf
|
||||||
for (const keyword of ['anyOf', 'oneOf', 'allOf']) {
|
for (const keyword of ['anyOf', 'oneOf', 'allOf']) {
|
||||||
if (Array.isArray(result[keyword])) {
|
if (Array.isArray(result[keyword])) {
|
||||||
result[keyword] = result[keyword].map(enforceStrictSchema)
|
result[keyword] = (result[keyword] as Record<string, unknown>[]).map(enforceStrictSchema)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +69,10 @@ function enforceStrictSchema(schema: any): any {
|
|||||||
for (const defKey of ['$defs', 'definitions']) {
|
for (const defKey of ['$defs', 'definitions']) {
|
||||||
if (result[defKey] && typeof result[defKey] === 'object') {
|
if (result[defKey] && typeof result[defKey] === 'object') {
|
||||||
result[defKey] = Object.fromEntries(
|
result[defKey] = Object.fromEntries(
|
||||||
Object.entries(result[defKey]).map(([key, value]) => [key, enforceStrictSchema(value)])
|
Object.entries(result[defKey] as Record<string, unknown>).map(([key, value]) => [
|
||||||
|
key,
|
||||||
|
enforceStrictSchema(value as Record<string, unknown>),
|
||||||
|
])
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -123,29 +130,29 @@ export async function executeResponsesProviderRequest(
|
|||||||
|
|
||||||
const initialInput = buildResponsesInputFromMessages(allMessages)
|
const initialInput = buildResponsesInputFromMessages(allMessages)
|
||||||
|
|
||||||
const basePayload: Record<string, any> = {
|
const basePayload: Record<string, unknown> = {
|
||||||
model: config.modelName,
|
model: config.modelName,
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.temperature !== undefined) basePayload.temperature = request.temperature
|
if (request.temperature !== undefined) basePayload.temperature = request.temperature
|
||||||
if (request.maxTokens != null) basePayload.max_output_tokens = request.maxTokens
|
if (request.maxTokens != null) basePayload.max_output_tokens = request.maxTokens
|
||||||
|
|
||||||
if (request.reasoningEffort !== undefined) {
|
if (request.reasoningEffort !== undefined && request.reasoningEffort !== 'auto') {
|
||||||
basePayload.reasoning = {
|
basePayload.reasoning = {
|
||||||
effort: request.reasoningEffort,
|
effort: request.reasoningEffort,
|
||||||
summary: 'auto',
|
summary: 'auto',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.verbosity !== undefined) {
|
if (request.verbosity !== undefined && request.verbosity !== 'auto') {
|
||||||
basePayload.text = {
|
basePayload.text = {
|
||||||
...(basePayload.text ?? {}),
|
...((basePayload.text as Record<string, unknown>) ?? {}),
|
||||||
verbosity: request.verbosity,
|
verbosity: request.verbosity,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store response format config - for Azure with tools, we defer applying it until after tool calls complete
|
// Store response format config - for Azure with tools, we defer applying it until after tool calls complete
|
||||||
let deferredTextFormat: { type: string; name: string; schema: any; strict: boolean } | undefined
|
let deferredTextFormat: OpenAI.Responses.ResponseFormatTextJSONSchemaConfig | undefined
|
||||||
const hasTools = !!request.tools?.length
|
const hasTools = !!request.tools?.length
|
||||||
const isAzure = config.providerId === 'azure-openai'
|
const isAzure = config.providerId === 'azure-openai'
|
||||||
|
|
||||||
@@ -171,7 +178,7 @@ export async function executeResponsesProviderRequest(
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
basePayload.text = {
|
basePayload.text = {
|
||||||
...(basePayload.text ?? {}),
|
...((basePayload.text as Record<string, unknown>) ?? {}),
|
||||||
format: textFormat,
|
format: textFormat,
|
||||||
}
|
}
|
||||||
logger.info(`Added JSON schema response format to ${config.providerLabel} request`)
|
logger.info(`Added JSON schema response format to ${config.providerLabel} request`)
|
||||||
@@ -231,7 +238,10 @@ export async function executeResponsesProviderRequest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const createRequestBody = (input: ResponsesInputItem[], overrides: Record<string, any> = {}) => ({
|
const createRequestBody = (
|
||||||
|
input: ResponsesInputItem[],
|
||||||
|
overrides: Record<string, unknown> = {}
|
||||||
|
) => ({
|
||||||
...basePayload,
|
...basePayload,
|
||||||
input,
|
input,
|
||||||
...overrides,
|
...overrides,
|
||||||
@@ -247,7 +257,9 @@ export async function executeResponsesProviderRequest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const postResponses = async (body: Record<string, any>) => {
|
const postResponses = async (
|
||||||
|
body: Record<string, unknown>
|
||||||
|
): Promise<OpenAI.Responses.Response> => {
|
||||||
const response = await fetch(config.endpoint, {
|
const response = await fetch(config.endpoint, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: config.headers,
|
headers: config.headers,
|
||||||
@@ -496,10 +508,10 @@ export async function executeResponsesProviderRequest(
|
|||||||
duration: duration,
|
duration: duration,
|
||||||
})
|
})
|
||||||
|
|
||||||
let resultContent: any
|
let resultContent: Record<string, unknown>
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
toolResults.push(result.output)
|
toolResults.push(result.output)
|
||||||
resultContent = result.output
|
resultContent = result.output as Record<string, unknown>
|
||||||
} else {
|
} else {
|
||||||
resultContent = {
|
resultContent = {
|
||||||
error: true,
|
error: true,
|
||||||
@@ -615,11 +627,11 @@ export async function executeResponsesProviderRequest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Make final call with the response format - build payload without tools
|
// Make final call with the response format - build payload without tools
|
||||||
const finalPayload: Record<string, any> = {
|
const finalPayload: Record<string, unknown> = {
|
||||||
model: config.modelName,
|
model: config.modelName,
|
||||||
input: formattedInput,
|
input: formattedInput,
|
||||||
text: {
|
text: {
|
||||||
...(basePayload.text ?? {}),
|
...((basePayload.text as Record<string, unknown>) ?? {}),
|
||||||
format: deferredTextFormat,
|
format: deferredTextFormat,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -627,15 +639,15 @@ export async function executeResponsesProviderRequest(
|
|||||||
// Copy over non-tool related settings
|
// Copy over non-tool related settings
|
||||||
if (request.temperature !== undefined) finalPayload.temperature = request.temperature
|
if (request.temperature !== undefined) finalPayload.temperature = request.temperature
|
||||||
if (request.maxTokens != null) finalPayload.max_output_tokens = request.maxTokens
|
if (request.maxTokens != null) finalPayload.max_output_tokens = request.maxTokens
|
||||||
if (request.reasoningEffort !== undefined) {
|
if (request.reasoningEffort !== undefined && request.reasoningEffort !== 'auto') {
|
||||||
finalPayload.reasoning = {
|
finalPayload.reasoning = {
|
||||||
effort: request.reasoningEffort,
|
effort: request.reasoningEffort,
|
||||||
summary: 'auto',
|
summary: 'auto',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (request.verbosity !== undefined) {
|
if (request.verbosity !== undefined && request.verbosity !== 'auto') {
|
||||||
finalPayload.text = {
|
finalPayload.text = {
|
||||||
...finalPayload.text,
|
...((finalPayload.text as Record<string, unknown>) ?? {}),
|
||||||
verbosity: request.verbosity,
|
verbosity: request.verbosity,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -679,10 +691,10 @@ export async function executeResponsesProviderRequest(
|
|||||||
const accumulatedCost = calculateCost(request.model, tokens.input, tokens.output)
|
const accumulatedCost = calculateCost(request.model, tokens.input, tokens.output)
|
||||||
|
|
||||||
// For Azure with deferred format in streaming mode, include the format in the streaming call
|
// For Azure with deferred format in streaming mode, include the format in the streaming call
|
||||||
const streamOverrides: Record<string, any> = { stream: true, tool_choice: 'auto' }
|
const streamOverrides: Record<string, unknown> = { stream: true, tool_choice: 'auto' }
|
||||||
if (deferredTextFormat) {
|
if (deferredTextFormat) {
|
||||||
streamOverrides.text = {
|
streamOverrides.text = {
|
||||||
...(basePayload.text ?? {}),
|
...((basePayload.text as Record<string, unknown>) ?? {}),
|
||||||
format: deferredTextFormat,
|
format: deferredTextFormat,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createLogger } from '@sim/logger'
|
import { createLogger } from '@sim/logger'
|
||||||
|
import type OpenAI from 'openai'
|
||||||
import type { Message } from '@/providers/types'
|
import type { Message } from '@/providers/types'
|
||||||
|
|
||||||
const logger = createLogger('ResponsesUtils')
|
const logger = createLogger('ResponsesUtils')
|
||||||
@@ -38,7 +39,7 @@ export interface ResponsesToolDefinition {
|
|||||||
type: 'function'
|
type: 'function'
|
||||||
name: string
|
name: string
|
||||||
description?: string
|
description?: string
|
||||||
parameters?: Record<string, any>
|
parameters?: Record<string, unknown>
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -85,7 +86,15 @@ export function buildResponsesInputFromMessages(messages: Message[]): ResponsesI
|
|||||||
/**
|
/**
|
||||||
* Converts tool definitions to the Responses API format.
|
* Converts tool definitions to the Responses API format.
|
||||||
*/
|
*/
|
||||||
export function convertToolsToResponses(tools: any[]): ResponsesToolDefinition[] {
|
export function convertToolsToResponses(
|
||||||
|
tools: Array<{
|
||||||
|
type?: string
|
||||||
|
name?: string
|
||||||
|
description?: string
|
||||||
|
parameters?: Record<string, unknown>
|
||||||
|
function?: { name: string; description?: string; parameters?: Record<string, unknown> }
|
||||||
|
}>
|
||||||
|
): ResponsesToolDefinition[] {
|
||||||
return tools
|
return tools
|
||||||
.map((tool) => {
|
.map((tool) => {
|
||||||
const name = tool.function?.name ?? tool.name
|
const name = tool.function?.name ?? tool.name
|
||||||
@@ -131,7 +140,7 @@ export function toResponsesToolChoice(
|
|||||||
return 'auto'
|
return 'auto'
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractTextFromMessageItem(item: any): string {
|
function extractTextFromMessageItem(item: Record<string, unknown>): string {
|
||||||
if (!item) {
|
if (!item) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
@@ -170,7 +179,7 @@ function extractTextFromMessageItem(item: any): string {
|
|||||||
/**
|
/**
|
||||||
* Extracts plain text from Responses API output items.
|
* Extracts plain text from Responses API output items.
|
||||||
*/
|
*/
|
||||||
export function extractResponseText(output: unknown): string {
|
export function extractResponseText(output: OpenAI.Responses.ResponseOutputItem[]): string {
|
||||||
if (!Array.isArray(output)) {
|
if (!Array.isArray(output)) {
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
@@ -181,7 +190,7 @@ export function extractResponseText(output: unknown): string {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const text = extractTextFromMessageItem(item)
|
const text = extractTextFromMessageItem(item as unknown as Record<string, unknown>)
|
||||||
if (text) {
|
if (text) {
|
||||||
textParts.push(text)
|
textParts.push(text)
|
||||||
}
|
}
|
||||||
@@ -193,7 +202,9 @@ export function extractResponseText(output: unknown): string {
|
|||||||
/**
|
/**
|
||||||
* Converts Responses API output items into input items for subsequent calls.
|
* Converts Responses API output items into input items for subsequent calls.
|
||||||
*/
|
*/
|
||||||
export function convertResponseOutputToInputItems(output: unknown): ResponsesInputItem[] {
|
export function convertResponseOutputToInputItems(
|
||||||
|
output: OpenAI.Responses.ResponseOutputItem[]
|
||||||
|
): ResponsesInputItem[] {
|
||||||
if (!Array.isArray(output)) {
|
if (!Array.isArray(output)) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@@ -205,7 +216,7 @@ export function convertResponseOutputToInputItems(output: unknown): ResponsesInp
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (item.type === 'message') {
|
if (item.type === 'message') {
|
||||||
const text = extractTextFromMessageItem(item)
|
const text = extractTextFromMessageItem(item as unknown as Record<string, unknown>)
|
||||||
if (text) {
|
if (text) {
|
||||||
items.push({
|
items.push({
|
||||||
role: 'assistant',
|
role: 'assistant',
|
||||||
@@ -213,18 +224,20 @@ export function convertResponseOutputToInputItems(output: unknown): ResponsesInp
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const toolCalls = Array.isArray(item.tool_calls) ? item.tool_calls : []
|
// Handle Chat Completions-style tool_calls nested under message items
|
||||||
|
const msgRecord = item as unknown as Record<string, unknown>
|
||||||
|
const toolCalls = Array.isArray(msgRecord.tool_calls) ? msgRecord.tool_calls : []
|
||||||
for (const toolCall of toolCalls) {
|
for (const toolCall of toolCalls) {
|
||||||
const callId = toolCall?.id
|
const tc = toolCall as Record<string, unknown>
|
||||||
const name = toolCall?.function?.name ?? toolCall?.name
|
const fn = tc.function as Record<string, unknown> | undefined
|
||||||
|
const callId = tc.id as string | undefined
|
||||||
|
const name = (fn?.name ?? tc.name) as string | undefined
|
||||||
if (!callId || !name) {
|
if (!callId || !name) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const argumentsValue =
|
const argumentsValue =
|
||||||
typeof toolCall?.function?.arguments === 'string'
|
typeof fn?.arguments === 'string' ? fn.arguments : JSON.stringify(fn?.arguments ?? {})
|
||||||
? toolCall.function.arguments
|
|
||||||
: JSON.stringify(toolCall?.function?.arguments ?? {})
|
|
||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
type: 'function_call',
|
type: 'function_call',
|
||||||
@@ -238,14 +251,18 @@ export function convertResponseOutputToInputItems(output: unknown): ResponsesInp
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (item.type === 'function_call') {
|
if (item.type === 'function_call') {
|
||||||
const callId = item.call_id ?? item.id
|
const fc = item as OpenAI.Responses.ResponseFunctionToolCall
|
||||||
const name = item.name ?? item.function?.name
|
const fcRecord = item as unknown as Record<string, unknown>
|
||||||
|
const callId = fc.call_id ?? (fcRecord.id as string | undefined)
|
||||||
|
const name =
|
||||||
|
fc.name ??
|
||||||
|
((fcRecord.function as Record<string, unknown> | undefined)?.name as string | undefined)
|
||||||
if (!callId || !name) {
|
if (!callId || !name) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const argumentsValue =
|
const argumentsValue =
|
||||||
typeof item.arguments === 'string' ? item.arguments : JSON.stringify(item.arguments ?? {})
|
typeof fc.arguments === 'string' ? fc.arguments : JSON.stringify(fc.arguments ?? {})
|
||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
type: 'function_call',
|
type: 'function_call',
|
||||||
@@ -262,7 +279,9 @@ export function convertResponseOutputToInputItems(output: unknown): ResponsesInp
|
|||||||
/**
|
/**
|
||||||
* Extracts tool calls from Responses API output items.
|
* Extracts tool calls from Responses API output items.
|
||||||
*/
|
*/
|
||||||
export function extractResponseToolCalls(output: unknown): ResponsesToolCall[] {
|
export function extractResponseToolCalls(
|
||||||
|
output: OpenAI.Responses.ResponseOutputItem[]
|
||||||
|
): ResponsesToolCall[] {
|
||||||
if (!Array.isArray(output)) {
|
if (!Array.isArray(output)) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
@@ -275,14 +294,18 @@ export function extractResponseToolCalls(output: unknown): ResponsesToolCall[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (item.type === 'function_call') {
|
if (item.type === 'function_call') {
|
||||||
const callId = item.call_id ?? item.id
|
const fc = item as OpenAI.Responses.ResponseFunctionToolCall
|
||||||
const name = item.name ?? item.function?.name
|
const fcRecord = item as unknown as Record<string, unknown>
|
||||||
|
const callId = fc.call_id ?? (fcRecord.id as string | undefined)
|
||||||
|
const name =
|
||||||
|
fc.name ??
|
||||||
|
((fcRecord.function as Record<string, unknown> | undefined)?.name as string | undefined)
|
||||||
if (!callId || !name) {
|
if (!callId || !name) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const argumentsValue =
|
const argumentsValue =
|
||||||
typeof item.arguments === 'string' ? item.arguments : JSON.stringify(item.arguments ?? {})
|
typeof fc.arguments === 'string' ? fc.arguments : JSON.stringify(fc.arguments ?? {})
|
||||||
|
|
||||||
toolCalls.push({
|
toolCalls.push({
|
||||||
id: callId,
|
id: callId,
|
||||||
@@ -292,18 +315,20 @@ export function extractResponseToolCalls(output: unknown): ResponsesToolCall[] {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.type === 'message' && Array.isArray(item.tool_calls)) {
|
// Handle Chat Completions-style tool_calls nested under message items
|
||||||
for (const toolCall of item.tool_calls) {
|
const msgRecord = item as unknown as Record<string, unknown>
|
||||||
const callId = toolCall?.id
|
if (item.type === 'message' && Array.isArray(msgRecord.tool_calls)) {
|
||||||
const name = toolCall?.function?.name ?? toolCall?.name
|
for (const toolCall of msgRecord.tool_calls) {
|
||||||
|
const tc = toolCall as Record<string, unknown>
|
||||||
|
const fn = tc.function as Record<string, unknown> | undefined
|
||||||
|
const callId = tc.id as string | undefined
|
||||||
|
const name = (fn?.name ?? tc.name) as string | undefined
|
||||||
if (!callId || !name) {
|
if (!callId || !name) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const argumentsValue =
|
const argumentsValue =
|
||||||
typeof toolCall?.function?.arguments === 'string'
|
typeof fn?.arguments === 'string' ? fn.arguments : JSON.stringify(fn?.arguments ?? {})
|
||||||
? toolCall.function.arguments
|
|
||||||
: JSON.stringify(toolCall?.function?.arguments ?? {})
|
|
||||||
|
|
||||||
toolCalls.push({
|
toolCalls.push({
|
||||||
id: callId,
|
id: callId,
|
||||||
@@ -323,15 +348,17 @@ export function extractResponseToolCalls(output: unknown): ResponsesToolCall[] {
|
|||||||
* Note: output_tokens is expected to include reasoning tokens; fall back to reasoning_tokens
|
* Note: output_tokens is expected to include reasoning tokens; fall back to reasoning_tokens
|
||||||
* when output_tokens is missing or zero.
|
* when output_tokens is missing or zero.
|
||||||
*/
|
*/
|
||||||
export function parseResponsesUsage(usage: any): ResponsesUsageTokens | undefined {
|
export function parseResponsesUsage(
|
||||||
if (!usage || typeof usage !== 'object') {
|
usage: OpenAI.Responses.ResponseUsage | undefined
|
||||||
|
): ResponsesUsageTokens | undefined {
|
||||||
|
if (!usage) {
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
const inputTokens = Number(usage.input_tokens ?? 0)
|
const inputTokens = usage.input_tokens ?? 0
|
||||||
const outputTokens = Number(usage.output_tokens ?? 0)
|
const outputTokens = usage.output_tokens ?? 0
|
||||||
const cachedTokens = Number(usage.input_tokens_details?.cached_tokens ?? 0)
|
const cachedTokens = usage.input_tokens_details?.cached_tokens ?? 0
|
||||||
const reasoningTokens = Number(usage.output_tokens_details?.reasoning_tokens ?? 0)
|
const reasoningTokens = usage.output_tokens_details?.reasoning_tokens ?? 0
|
||||||
const completionTokens = Math.max(outputTokens, reasoningTokens)
|
const completionTokens = Math.max(outputTokens, reasoningTokens)
|
||||||
const totalTokens = inputTokens + completionTokens
|
const totalTokens = inputTokens + completionTokens
|
||||||
|
|
||||||
@@ -398,7 +425,7 @@ export function createReadableStreamFromResponses(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
let event: any
|
let event: Record<string, unknown>
|
||||||
try {
|
try {
|
||||||
event = JSON.parse(data)
|
event = JSON.parse(data)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -416,7 +443,8 @@ export function createReadableStreamFromResponses(
|
|||||||
eventType === 'error' ||
|
eventType === 'error' ||
|
||||||
eventType === 'response.failed'
|
eventType === 'response.failed'
|
||||||
) {
|
) {
|
||||||
const message = event?.error?.message || 'Responses API stream error'
|
const errorObj = event.error as Record<string, unknown> | undefined
|
||||||
|
const message = (errorObj?.message as string) || 'Responses API stream error'
|
||||||
controller.error(new Error(message))
|
controller.error(new Error(message))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -426,12 +454,13 @@ export function createReadableStreamFromResponses(
|
|||||||
eventType === 'response.output_json.delta'
|
eventType === 'response.output_json.delta'
|
||||||
) {
|
) {
|
||||||
let deltaText = ''
|
let deltaText = ''
|
||||||
if (typeof event.delta === 'string') {
|
const delta = event.delta as string | Record<string, unknown> | undefined
|
||||||
deltaText = event.delta
|
if (typeof delta === 'string') {
|
||||||
} else if (event.delta && typeof event.delta.text === 'string') {
|
deltaText = delta
|
||||||
deltaText = event.delta.text
|
} else if (delta && typeof delta.text === 'string') {
|
||||||
} else if (event.delta && event.delta.json !== undefined) {
|
deltaText = delta.text
|
||||||
deltaText = JSON.stringify(event.delta.json)
|
} else if (delta && delta.json !== undefined) {
|
||||||
|
deltaText = JSON.stringify(delta.json)
|
||||||
} else if (event.json !== undefined) {
|
} else if (event.json !== undefined) {
|
||||||
deltaText = JSON.stringify(event.json)
|
deltaText = JSON.stringify(event.json)
|
||||||
} else if (typeof event.text === 'string') {
|
} else if (typeof event.text === 'string') {
|
||||||
@@ -445,7 +474,11 @@ export function createReadableStreamFromResponses(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (eventType === 'response.completed') {
|
if (eventType === 'response.completed') {
|
||||||
finalUsage = parseResponsesUsage(event?.response?.usage ?? event?.usage)
|
const responseObj = event.response as Record<string, unknown> | undefined
|
||||||
|
const usageData = (responseObj?.usage ?? event.usage) as
|
||||||
|
| OpenAI.Responses.ResponseUsage
|
||||||
|
| undefined
|
||||||
|
finalUsage = parseResponsesUsage(usageData)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -431,19 +431,13 @@ export const openRouterProvider: ProviderConfig = {
|
|||||||
const accumulatedCost = calculateCost(requestedModel, tokens.input, tokens.output)
|
const accumulatedCost = calculateCost(requestedModel, tokens.input, tokens.output)
|
||||||
|
|
||||||
const streamingParams: ChatCompletionCreateParamsStreaming & { provider?: any } = {
|
const streamingParams: ChatCompletionCreateParamsStreaming & { provider?: any } = {
|
||||||
model: payload.model,
|
...payload,
|
||||||
messages: [...currentMessages],
|
messages: [...currentMessages],
|
||||||
|
tool_choice: 'auto',
|
||||||
stream: true,
|
stream: true,
|
||||||
stream_options: { include_usage: true },
|
stream_options: { include_usage: true },
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload.temperature !== undefined) {
|
|
||||||
streamingParams.temperature = payload.temperature
|
|
||||||
}
|
|
||||||
if (payload.max_tokens !== undefined) {
|
|
||||||
streamingParams.max_tokens = payload.max_tokens
|
|
||||||
}
|
|
||||||
|
|
||||||
if (request.responseFormat) {
|
if (request.responseFormat) {
|
||||||
;(streamingParams as any).messages = await applyResponseFormat(
|
;(streamingParams as any).messages = await applyResponseFormat(
|
||||||
streamingParams as any,
|
streamingParams as any,
|
||||||
|
|||||||
@@ -12,16 +12,22 @@ import {
|
|||||||
getApiKey,
|
getApiKey,
|
||||||
getBaseModelProviders,
|
getBaseModelProviders,
|
||||||
getHostedModels,
|
getHostedModels,
|
||||||
|
getMaxOutputTokensForModel,
|
||||||
getMaxTemperature,
|
getMaxTemperature,
|
||||||
|
getModelPricing,
|
||||||
getProvider,
|
getProvider,
|
||||||
getProviderConfigFromModel,
|
getProviderConfigFromModel,
|
||||||
getProviderFromModel,
|
getProviderFromModel,
|
||||||
getProviderModels,
|
getProviderModels,
|
||||||
|
getReasoningEffortValuesForModel,
|
||||||
|
getThinkingLevelsForModel,
|
||||||
|
getVerbosityValuesForModel,
|
||||||
isProviderBlacklisted,
|
isProviderBlacklisted,
|
||||||
MODELS_TEMP_RANGE_0_1,
|
MODELS_TEMP_RANGE_0_1,
|
||||||
MODELS_TEMP_RANGE_0_2,
|
MODELS_TEMP_RANGE_0_2,
|
||||||
MODELS_WITH_REASONING_EFFORT,
|
MODELS_WITH_REASONING_EFFORT,
|
||||||
MODELS_WITH_TEMPERATURE_SUPPORT,
|
MODELS_WITH_TEMPERATURE_SUPPORT,
|
||||||
|
MODELS_WITH_THINKING,
|
||||||
MODELS_WITH_VERBOSITY,
|
MODELS_WITH_VERBOSITY,
|
||||||
PROVIDERS_WITH_TOOL_USAGE_CONTROL,
|
PROVIDERS_WITH_TOOL_USAGE_CONTROL,
|
||||||
prepareToolExecution,
|
prepareToolExecution,
|
||||||
@@ -169,6 +175,8 @@ describe('Model Capabilities', () => {
|
|||||||
'gpt-4.1',
|
'gpt-4.1',
|
||||||
'gpt-4.1-mini',
|
'gpt-4.1-mini',
|
||||||
'gpt-4.1-nano',
|
'gpt-4.1-nano',
|
||||||
|
'gpt-5-chat-latest',
|
||||||
|
'azure/gpt-5-chat-latest',
|
||||||
'gemini-2.5-flash',
|
'gemini-2.5-flash',
|
||||||
'claude-sonnet-4-0',
|
'claude-sonnet-4-0',
|
||||||
'claude-opus-4-0',
|
'claude-opus-4-0',
|
||||||
@@ -186,34 +194,27 @@ describe('Model Capabilities', () => {
|
|||||||
it.concurrent('should return false for models that do not support temperature', () => {
|
it.concurrent('should return false for models that do not support temperature', () => {
|
||||||
const unsupportedModels = [
|
const unsupportedModels = [
|
||||||
'unsupported-model',
|
'unsupported-model',
|
||||||
'cerebras/llama-3.3-70b', // Cerebras models don't have temperature defined
|
'cerebras/llama-3.3-70b',
|
||||||
'groq/meta-llama/llama-4-scout-17b-16e-instruct', // Groq models don't have temperature defined
|
'groq/meta-llama/llama-4-scout-17b-16e-instruct',
|
||||||
// Reasoning models that don't support temperature
|
|
||||||
'o1',
|
'o1',
|
||||||
'o3',
|
'o3',
|
||||||
'o4-mini',
|
'o4-mini',
|
||||||
'azure/o3',
|
'azure/o3',
|
||||||
'azure/o4-mini',
|
'azure/o4-mini',
|
||||||
'deepseek-r1',
|
'deepseek-r1',
|
||||||
// Chat models that don't support temperature
|
|
||||||
'deepseek-chat',
|
'deepseek-chat',
|
||||||
'azure/gpt-4.1',
|
|
||||||
'azure/model-router',
|
'azure/model-router',
|
||||||
// GPT-5.1 models don't support temperature (removed in our implementation)
|
|
||||||
'gpt-5.1',
|
'gpt-5.1',
|
||||||
'azure/gpt-5.1',
|
'azure/gpt-5.1',
|
||||||
'azure/gpt-5.1-mini',
|
'azure/gpt-5.1-mini',
|
||||||
'azure/gpt-5.1-nano',
|
'azure/gpt-5.1-nano',
|
||||||
'azure/gpt-5.1-codex',
|
'azure/gpt-5.1-codex',
|
||||||
// GPT-5 models don't support temperature (removed in our implementation)
|
|
||||||
'gpt-5',
|
'gpt-5',
|
||||||
'gpt-5-mini',
|
'gpt-5-mini',
|
||||||
'gpt-5-nano',
|
'gpt-5-nano',
|
||||||
'gpt-5-chat-latest',
|
|
||||||
'azure/gpt-5',
|
'azure/gpt-5',
|
||||||
'azure/gpt-5-mini',
|
'azure/gpt-5-mini',
|
||||||
'azure/gpt-5-nano',
|
'azure/gpt-5-nano',
|
||||||
'azure/gpt-5-chat-latest',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
for (const model of unsupportedModels) {
|
for (const model of unsupportedModels) {
|
||||||
@@ -240,6 +241,8 @@ describe('Model Capabilities', () => {
|
|||||||
const modelsRange02 = [
|
const modelsRange02 = [
|
||||||
'gpt-4o',
|
'gpt-4o',
|
||||||
'azure/gpt-4o',
|
'azure/gpt-4o',
|
||||||
|
'gpt-5-chat-latest',
|
||||||
|
'azure/gpt-5-chat-latest',
|
||||||
'gemini-2.5-pro',
|
'gemini-2.5-pro',
|
||||||
'gemini-2.5-flash',
|
'gemini-2.5-flash',
|
||||||
'deepseek-v3',
|
'deepseek-v3',
|
||||||
@@ -268,28 +271,23 @@ describe('Model Capabilities', () => {
|
|||||||
expect(getMaxTemperature('unsupported-model')).toBeUndefined()
|
expect(getMaxTemperature('unsupported-model')).toBeUndefined()
|
||||||
expect(getMaxTemperature('cerebras/llama-3.3-70b')).toBeUndefined()
|
expect(getMaxTemperature('cerebras/llama-3.3-70b')).toBeUndefined()
|
||||||
expect(getMaxTemperature('groq/meta-llama/llama-4-scout-17b-16e-instruct')).toBeUndefined()
|
expect(getMaxTemperature('groq/meta-llama/llama-4-scout-17b-16e-instruct')).toBeUndefined()
|
||||||
// Reasoning models that don't support temperature
|
|
||||||
expect(getMaxTemperature('o1')).toBeUndefined()
|
expect(getMaxTemperature('o1')).toBeUndefined()
|
||||||
expect(getMaxTemperature('o3')).toBeUndefined()
|
expect(getMaxTemperature('o3')).toBeUndefined()
|
||||||
expect(getMaxTemperature('o4-mini')).toBeUndefined()
|
expect(getMaxTemperature('o4-mini')).toBeUndefined()
|
||||||
expect(getMaxTemperature('azure/o3')).toBeUndefined()
|
expect(getMaxTemperature('azure/o3')).toBeUndefined()
|
||||||
expect(getMaxTemperature('azure/o4-mini')).toBeUndefined()
|
expect(getMaxTemperature('azure/o4-mini')).toBeUndefined()
|
||||||
expect(getMaxTemperature('deepseek-r1')).toBeUndefined()
|
expect(getMaxTemperature('deepseek-r1')).toBeUndefined()
|
||||||
// GPT-5.1 models don't support temperature
|
|
||||||
expect(getMaxTemperature('gpt-5.1')).toBeUndefined()
|
expect(getMaxTemperature('gpt-5.1')).toBeUndefined()
|
||||||
expect(getMaxTemperature('azure/gpt-5.1')).toBeUndefined()
|
expect(getMaxTemperature('azure/gpt-5.1')).toBeUndefined()
|
||||||
expect(getMaxTemperature('azure/gpt-5.1-mini')).toBeUndefined()
|
expect(getMaxTemperature('azure/gpt-5.1-mini')).toBeUndefined()
|
||||||
expect(getMaxTemperature('azure/gpt-5.1-nano')).toBeUndefined()
|
expect(getMaxTemperature('azure/gpt-5.1-nano')).toBeUndefined()
|
||||||
expect(getMaxTemperature('azure/gpt-5.1-codex')).toBeUndefined()
|
expect(getMaxTemperature('azure/gpt-5.1-codex')).toBeUndefined()
|
||||||
// GPT-5 models don't support temperature
|
|
||||||
expect(getMaxTemperature('gpt-5')).toBeUndefined()
|
expect(getMaxTemperature('gpt-5')).toBeUndefined()
|
||||||
expect(getMaxTemperature('gpt-5-mini')).toBeUndefined()
|
expect(getMaxTemperature('gpt-5-mini')).toBeUndefined()
|
||||||
expect(getMaxTemperature('gpt-5-nano')).toBeUndefined()
|
expect(getMaxTemperature('gpt-5-nano')).toBeUndefined()
|
||||||
expect(getMaxTemperature('gpt-5-chat-latest')).toBeUndefined()
|
|
||||||
expect(getMaxTemperature('azure/gpt-5')).toBeUndefined()
|
expect(getMaxTemperature('azure/gpt-5')).toBeUndefined()
|
||||||
expect(getMaxTemperature('azure/gpt-5-mini')).toBeUndefined()
|
expect(getMaxTemperature('azure/gpt-5-mini')).toBeUndefined()
|
||||||
expect(getMaxTemperature('azure/gpt-5-nano')).toBeUndefined()
|
expect(getMaxTemperature('azure/gpt-5-nano')).toBeUndefined()
|
||||||
expect(getMaxTemperature('azure/gpt-5-chat-latest')).toBeUndefined()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
it.concurrent('should be case insensitive', () => {
|
it.concurrent('should be case insensitive', () => {
|
||||||
@@ -340,13 +338,13 @@ describe('Model Capabilities', () => {
|
|||||||
expect(MODELS_TEMP_RANGE_0_2).toContain('gpt-4o')
|
expect(MODELS_TEMP_RANGE_0_2).toContain('gpt-4o')
|
||||||
expect(MODELS_TEMP_RANGE_0_2).toContain('gemini-2.5-flash')
|
expect(MODELS_TEMP_RANGE_0_2).toContain('gemini-2.5-flash')
|
||||||
expect(MODELS_TEMP_RANGE_0_2).toContain('deepseek-v3')
|
expect(MODELS_TEMP_RANGE_0_2).toContain('deepseek-v3')
|
||||||
expect(MODELS_TEMP_RANGE_0_2).not.toContain('claude-sonnet-4-0') // Should be in 0-1 range
|
expect(MODELS_TEMP_RANGE_0_2).not.toContain('claude-sonnet-4-0')
|
||||||
})
|
})
|
||||||
|
|
||||||
it.concurrent('should have correct models in MODELS_TEMP_RANGE_0_1', () => {
|
it.concurrent('should have correct models in MODELS_TEMP_RANGE_0_1', () => {
|
||||||
expect(MODELS_TEMP_RANGE_0_1).toContain('claude-sonnet-4-0')
|
expect(MODELS_TEMP_RANGE_0_1).toContain('claude-sonnet-4-0')
|
||||||
expect(MODELS_TEMP_RANGE_0_1).toContain('grok-3-latest')
|
expect(MODELS_TEMP_RANGE_0_1).toContain('grok-3-latest')
|
||||||
expect(MODELS_TEMP_RANGE_0_1).not.toContain('gpt-4o') // Should be in 0-2 range
|
expect(MODELS_TEMP_RANGE_0_1).not.toContain('gpt-4o')
|
||||||
})
|
})
|
||||||
|
|
||||||
it.concurrent('should have correct providers in PROVIDERS_WITH_TOOL_USAGE_CONTROL', () => {
|
it.concurrent('should have correct providers in PROVIDERS_WITH_TOOL_USAGE_CONTROL', () => {
|
||||||
@@ -363,20 +361,19 @@ describe('Model Capabilities', () => {
|
|||||||
expect(MODELS_WITH_TEMPERATURE_SUPPORT.length).toBe(
|
expect(MODELS_WITH_TEMPERATURE_SUPPORT.length).toBe(
|
||||||
MODELS_TEMP_RANGE_0_2.length + MODELS_TEMP_RANGE_0_1.length
|
MODELS_TEMP_RANGE_0_2.length + MODELS_TEMP_RANGE_0_1.length
|
||||||
)
|
)
|
||||||
expect(MODELS_WITH_TEMPERATURE_SUPPORT).toContain('gpt-4o') // From 0-2 range
|
expect(MODELS_WITH_TEMPERATURE_SUPPORT).toContain('gpt-4o')
|
||||||
expect(MODELS_WITH_TEMPERATURE_SUPPORT).toContain('claude-sonnet-4-0') // From 0-1 range
|
expect(MODELS_WITH_TEMPERATURE_SUPPORT).toContain('claude-sonnet-4-0')
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
it.concurrent('should have correct models in MODELS_WITH_REASONING_EFFORT', () => {
|
it.concurrent('should have correct models in MODELS_WITH_REASONING_EFFORT', () => {
|
||||||
// Should contain GPT-5.1 models that support reasoning effort
|
|
||||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('gpt-5.1')
|
expect(MODELS_WITH_REASONING_EFFORT).toContain('gpt-5.1')
|
||||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/gpt-5.1')
|
expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/gpt-5.1')
|
||||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/gpt-5.1-mini')
|
|
||||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/gpt-5.1-nano')
|
|
||||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/gpt-5.1-codex')
|
expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/gpt-5.1-codex')
|
||||||
|
|
||||||
// Should contain GPT-5 models that support reasoning effort
|
expect(MODELS_WITH_REASONING_EFFORT).not.toContain('azure/gpt-5.1-mini')
|
||||||
|
expect(MODELS_WITH_REASONING_EFFORT).not.toContain('azure/gpt-5.1-nano')
|
||||||
|
|
||||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('gpt-5')
|
expect(MODELS_WITH_REASONING_EFFORT).toContain('gpt-5')
|
||||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('gpt-5-mini')
|
expect(MODELS_WITH_REASONING_EFFORT).toContain('gpt-5-mini')
|
||||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('gpt-5-nano')
|
expect(MODELS_WITH_REASONING_EFFORT).toContain('gpt-5-nano')
|
||||||
@@ -384,35 +381,30 @@ describe('Model Capabilities', () => {
|
|||||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/gpt-5-mini')
|
expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/gpt-5-mini')
|
||||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/gpt-5-nano')
|
expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/gpt-5-nano')
|
||||||
|
|
||||||
// Should contain gpt-5.2 models
|
|
||||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('gpt-5.2')
|
expect(MODELS_WITH_REASONING_EFFORT).toContain('gpt-5.2')
|
||||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/gpt-5.2')
|
expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/gpt-5.2')
|
||||||
|
|
||||||
// Should contain o-series reasoning models (reasoning_effort added Dec 17, 2024)
|
|
||||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('o1')
|
expect(MODELS_WITH_REASONING_EFFORT).toContain('o1')
|
||||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('o3')
|
expect(MODELS_WITH_REASONING_EFFORT).toContain('o3')
|
||||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('o4-mini')
|
expect(MODELS_WITH_REASONING_EFFORT).toContain('o4-mini')
|
||||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/o3')
|
expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/o3')
|
||||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/o4-mini')
|
expect(MODELS_WITH_REASONING_EFFORT).toContain('azure/o4-mini')
|
||||||
|
|
||||||
// Should NOT contain non-reasoning GPT-5 models
|
|
||||||
expect(MODELS_WITH_REASONING_EFFORT).not.toContain('gpt-5-chat-latest')
|
expect(MODELS_WITH_REASONING_EFFORT).not.toContain('gpt-5-chat-latest')
|
||||||
expect(MODELS_WITH_REASONING_EFFORT).not.toContain('azure/gpt-5-chat-latest')
|
expect(MODELS_WITH_REASONING_EFFORT).not.toContain('azure/gpt-5-chat-latest')
|
||||||
|
|
||||||
// Should NOT contain other models
|
|
||||||
expect(MODELS_WITH_REASONING_EFFORT).not.toContain('gpt-4o')
|
expect(MODELS_WITH_REASONING_EFFORT).not.toContain('gpt-4o')
|
||||||
expect(MODELS_WITH_REASONING_EFFORT).not.toContain('claude-sonnet-4-0')
|
expect(MODELS_WITH_REASONING_EFFORT).not.toContain('claude-sonnet-4-0')
|
||||||
})
|
})
|
||||||
|
|
||||||
it.concurrent('should have correct models in MODELS_WITH_VERBOSITY', () => {
|
it.concurrent('should have correct models in MODELS_WITH_VERBOSITY', () => {
|
||||||
// Should contain GPT-5.1 models that support verbosity
|
|
||||||
expect(MODELS_WITH_VERBOSITY).toContain('gpt-5.1')
|
expect(MODELS_WITH_VERBOSITY).toContain('gpt-5.1')
|
||||||
expect(MODELS_WITH_VERBOSITY).toContain('azure/gpt-5.1')
|
expect(MODELS_WITH_VERBOSITY).toContain('azure/gpt-5.1')
|
||||||
expect(MODELS_WITH_VERBOSITY).toContain('azure/gpt-5.1-mini')
|
|
||||||
expect(MODELS_WITH_VERBOSITY).toContain('azure/gpt-5.1-nano')
|
|
||||||
expect(MODELS_WITH_VERBOSITY).toContain('azure/gpt-5.1-codex')
|
expect(MODELS_WITH_VERBOSITY).toContain('azure/gpt-5.1-codex')
|
||||||
|
|
||||||
// Should contain GPT-5 models that support verbosity
|
expect(MODELS_WITH_VERBOSITY).not.toContain('azure/gpt-5.1-mini')
|
||||||
|
expect(MODELS_WITH_VERBOSITY).not.toContain('azure/gpt-5.1-nano')
|
||||||
|
|
||||||
expect(MODELS_WITH_VERBOSITY).toContain('gpt-5')
|
expect(MODELS_WITH_VERBOSITY).toContain('gpt-5')
|
||||||
expect(MODELS_WITH_VERBOSITY).toContain('gpt-5-mini')
|
expect(MODELS_WITH_VERBOSITY).toContain('gpt-5-mini')
|
||||||
expect(MODELS_WITH_VERBOSITY).toContain('gpt-5-nano')
|
expect(MODELS_WITH_VERBOSITY).toContain('gpt-5-nano')
|
||||||
@@ -420,26 +412,39 @@ describe('Model Capabilities', () => {
|
|||||||
expect(MODELS_WITH_VERBOSITY).toContain('azure/gpt-5-mini')
|
expect(MODELS_WITH_VERBOSITY).toContain('azure/gpt-5-mini')
|
||||||
expect(MODELS_WITH_VERBOSITY).toContain('azure/gpt-5-nano')
|
expect(MODELS_WITH_VERBOSITY).toContain('azure/gpt-5-nano')
|
||||||
|
|
||||||
// Should contain gpt-5.2 models
|
|
||||||
expect(MODELS_WITH_VERBOSITY).toContain('gpt-5.2')
|
expect(MODELS_WITH_VERBOSITY).toContain('gpt-5.2')
|
||||||
expect(MODELS_WITH_VERBOSITY).toContain('azure/gpt-5.2')
|
expect(MODELS_WITH_VERBOSITY).toContain('azure/gpt-5.2')
|
||||||
|
|
||||||
// Should NOT contain non-reasoning GPT-5 models
|
|
||||||
expect(MODELS_WITH_VERBOSITY).not.toContain('gpt-5-chat-latest')
|
expect(MODELS_WITH_VERBOSITY).not.toContain('gpt-5-chat-latest')
|
||||||
expect(MODELS_WITH_VERBOSITY).not.toContain('azure/gpt-5-chat-latest')
|
expect(MODELS_WITH_VERBOSITY).not.toContain('azure/gpt-5-chat-latest')
|
||||||
|
|
||||||
// Should NOT contain o-series models (they support reasoning_effort but not verbosity)
|
|
||||||
expect(MODELS_WITH_VERBOSITY).not.toContain('o1')
|
expect(MODELS_WITH_VERBOSITY).not.toContain('o1')
|
||||||
expect(MODELS_WITH_VERBOSITY).not.toContain('o3')
|
expect(MODELS_WITH_VERBOSITY).not.toContain('o3')
|
||||||
expect(MODELS_WITH_VERBOSITY).not.toContain('o4-mini')
|
expect(MODELS_WITH_VERBOSITY).not.toContain('o4-mini')
|
||||||
|
|
||||||
// Should NOT contain other models
|
|
||||||
expect(MODELS_WITH_VERBOSITY).not.toContain('gpt-4o')
|
expect(MODELS_WITH_VERBOSITY).not.toContain('gpt-4o')
|
||||||
expect(MODELS_WITH_VERBOSITY).not.toContain('claude-sonnet-4-0')
|
expect(MODELS_WITH_VERBOSITY).not.toContain('claude-sonnet-4-0')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it.concurrent('should have correct models in MODELS_WITH_THINKING', () => {
|
||||||
|
expect(MODELS_WITH_THINKING).toContain('claude-opus-4-6')
|
||||||
|
expect(MODELS_WITH_THINKING).toContain('claude-opus-4-5')
|
||||||
|
expect(MODELS_WITH_THINKING).toContain('claude-opus-4-1')
|
||||||
|
expect(MODELS_WITH_THINKING).toContain('claude-opus-4-0')
|
||||||
|
expect(MODELS_WITH_THINKING).toContain('claude-sonnet-4-5')
|
||||||
|
expect(MODELS_WITH_THINKING).toContain('claude-sonnet-4-0')
|
||||||
|
|
||||||
|
expect(MODELS_WITH_THINKING).toContain('gemini-3-pro-preview')
|
||||||
|
expect(MODELS_WITH_THINKING).toContain('gemini-3-flash-preview')
|
||||||
|
|
||||||
|
expect(MODELS_WITH_THINKING).toContain('claude-haiku-4-5')
|
||||||
|
|
||||||
|
expect(MODELS_WITH_THINKING).not.toContain('gpt-4o')
|
||||||
|
expect(MODELS_WITH_THINKING).not.toContain('gpt-5')
|
||||||
|
expect(MODELS_WITH_THINKING).not.toContain('o3')
|
||||||
|
})
|
||||||
|
|
||||||
it.concurrent('should have GPT-5 models in both reasoning effort and verbosity arrays', () => {
|
it.concurrent('should have GPT-5 models in both reasoning effort and verbosity arrays', () => {
|
||||||
// GPT-5 series models support both reasoning effort and verbosity
|
|
||||||
const gpt5ModelsWithReasoningEffort = MODELS_WITH_REASONING_EFFORT.filter(
|
const gpt5ModelsWithReasoningEffort = MODELS_WITH_REASONING_EFFORT.filter(
|
||||||
(m) => m.includes('gpt-5') && !m.includes('chat-latest')
|
(m) => m.includes('gpt-5') && !m.includes('chat-latest')
|
||||||
)
|
)
|
||||||
@@ -448,11 +453,201 @@ describe('Model Capabilities', () => {
|
|||||||
)
|
)
|
||||||
expect(gpt5ModelsWithReasoningEffort.sort()).toEqual(gpt5ModelsWithVerbosity.sort())
|
expect(gpt5ModelsWithReasoningEffort.sort()).toEqual(gpt5ModelsWithVerbosity.sort())
|
||||||
|
|
||||||
// o-series models have reasoning effort but NOT verbosity
|
|
||||||
expect(MODELS_WITH_REASONING_EFFORT).toContain('o1')
|
expect(MODELS_WITH_REASONING_EFFORT).toContain('o1')
|
||||||
expect(MODELS_WITH_VERBOSITY).not.toContain('o1')
|
expect(MODELS_WITH_VERBOSITY).not.toContain('o1')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
describe('Reasoning Effort Values Per Model', () => {
|
||||||
|
it.concurrent('should return correct values for GPT-5.2', () => {
|
||||||
|
const values = getReasoningEffortValuesForModel('gpt-5.2')
|
||||||
|
expect(values).toBeDefined()
|
||||||
|
expect(values).toContain('none')
|
||||||
|
expect(values).toContain('low')
|
||||||
|
expect(values).toContain('medium')
|
||||||
|
expect(values).toContain('high')
|
||||||
|
expect(values).toContain('xhigh')
|
||||||
|
expect(values).not.toContain('minimal')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should return correct values for GPT-5', () => {
|
||||||
|
const values = getReasoningEffortValuesForModel('gpt-5')
|
||||||
|
expect(values).toBeDefined()
|
||||||
|
expect(values).toContain('minimal')
|
||||||
|
expect(values).toContain('low')
|
||||||
|
expect(values).toContain('medium')
|
||||||
|
expect(values).toContain('high')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should return correct values for o-series models', () => {
|
||||||
|
for (const model of ['o1', 'o3', 'o4-mini']) {
|
||||||
|
const values = getReasoningEffortValuesForModel(model)
|
||||||
|
expect(values).toBeDefined()
|
||||||
|
expect(values).toContain('low')
|
||||||
|
expect(values).toContain('medium')
|
||||||
|
expect(values).toContain('high')
|
||||||
|
expect(values).not.toContain('none')
|
||||||
|
expect(values).not.toContain('minimal')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should return null for non-reasoning models', () => {
|
||||||
|
expect(getReasoningEffortValuesForModel('gpt-4o')).toBeNull()
|
||||||
|
expect(getReasoningEffortValuesForModel('claude-sonnet-4-5')).toBeNull()
|
||||||
|
expect(getReasoningEffortValuesForModel('gemini-2.5-flash')).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should return correct values for Azure GPT-5.2', () => {
|
||||||
|
const values = getReasoningEffortValuesForModel('azure/gpt-5.2')
|
||||||
|
expect(values).toBeDefined()
|
||||||
|
expect(values).not.toContain('minimal')
|
||||||
|
expect(values).toContain('xhigh')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Verbosity Values Per Model', () => {
|
||||||
|
it.concurrent('should return correct values for GPT-5 family', () => {
|
||||||
|
for (const model of ['gpt-5.2', 'gpt-5.1', 'gpt-5', 'gpt-5-mini', 'gpt-5-nano']) {
|
||||||
|
const values = getVerbosityValuesForModel(model)
|
||||||
|
expect(values).toBeDefined()
|
||||||
|
expect(values).toContain('low')
|
||||||
|
expect(values).toContain('medium')
|
||||||
|
expect(values).toContain('high')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should return null for o-series models', () => {
|
||||||
|
expect(getVerbosityValuesForModel('o1')).toBeNull()
|
||||||
|
expect(getVerbosityValuesForModel('o3')).toBeNull()
|
||||||
|
expect(getVerbosityValuesForModel('o4-mini')).toBeNull()
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should return null for non-reasoning models', () => {
|
||||||
|
expect(getVerbosityValuesForModel('gpt-4o')).toBeNull()
|
||||||
|
expect(getVerbosityValuesForModel('claude-sonnet-4-5')).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Thinking Levels Per Model', () => {
|
||||||
|
it.concurrent('should return correct levels for Claude Opus 4.6 (adaptive)', () => {
|
||||||
|
const levels = getThinkingLevelsForModel('claude-opus-4-6')
|
||||||
|
expect(levels).toBeDefined()
|
||||||
|
expect(levels).toContain('low')
|
||||||
|
expect(levels).toContain('medium')
|
||||||
|
expect(levels).toContain('high')
|
||||||
|
expect(levels).toContain('max')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should return correct levels for other Claude models (budget_tokens)', () => {
|
||||||
|
for (const model of ['claude-opus-4-5', 'claude-sonnet-4-5', 'claude-sonnet-4-0']) {
|
||||||
|
const levels = getThinkingLevelsForModel(model)
|
||||||
|
expect(levels).toBeDefined()
|
||||||
|
expect(levels).toContain('low')
|
||||||
|
expect(levels).toContain('medium')
|
||||||
|
expect(levels).toContain('high')
|
||||||
|
expect(levels).not.toContain('max')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should return correct levels for Gemini 3 models', () => {
|
||||||
|
const proLevels = getThinkingLevelsForModel('gemini-3-pro-preview')
|
||||||
|
expect(proLevels).toBeDefined()
|
||||||
|
expect(proLevels).toContain('low')
|
||||||
|
expect(proLevels).toContain('high')
|
||||||
|
|
||||||
|
const flashLevels = getThinkingLevelsForModel('gemini-3-flash-preview')
|
||||||
|
expect(flashLevels).toBeDefined()
|
||||||
|
expect(flashLevels).toContain('minimal')
|
||||||
|
expect(flashLevels).toContain('low')
|
||||||
|
expect(flashLevels).toContain('medium')
|
||||||
|
expect(flashLevels).toContain('high')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should return correct levels for Claude Haiku 4.5', () => {
|
||||||
|
const levels = getThinkingLevelsForModel('claude-haiku-4-5')
|
||||||
|
expect(levels).toBeDefined()
|
||||||
|
expect(levels).toContain('low')
|
||||||
|
expect(levels).toContain('medium')
|
||||||
|
expect(levels).toContain('high')
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should return null for non-thinking models', () => {
|
||||||
|
expect(getThinkingLevelsForModel('gpt-4o')).toBeNull()
|
||||||
|
expect(getThinkingLevelsForModel('gpt-5')).toBeNull()
|
||||||
|
expect(getThinkingLevelsForModel('o3')).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Max Output Tokens', () => {
|
||||||
|
describe('getMaxOutputTokensForModel', () => {
|
||||||
|
it.concurrent('should return correct max for Claude Opus 4.6', () => {
|
||||||
|
expect(getMaxOutputTokensForModel('claude-opus-4-6')).toBe(128000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should return correct max for Claude Sonnet 4.5', () => {
|
||||||
|
expect(getMaxOutputTokensForModel('claude-sonnet-4-5')).toBe(64000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should return correct max for Claude Opus 4.1', () => {
|
||||||
|
expect(getMaxOutputTokensForModel('claude-opus-4-1')).toBe(64000)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should return standard default for models without maxOutputTokens', () => {
|
||||||
|
expect(getMaxOutputTokensForModel('gpt-4o')).toBe(4096)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should return standard default for unknown models', () => {
|
||||||
|
expect(getMaxOutputTokensForModel('unknown-model')).toBe(4096)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Model Pricing Validation', () => {
|
||||||
|
it.concurrent('should have correct pricing for key Anthropic models', () => {
|
||||||
|
const opus46 = getModelPricing('claude-opus-4-6')
|
||||||
|
expect(opus46).toBeDefined()
|
||||||
|
expect(opus46.input).toBe(5.0)
|
||||||
|
expect(opus46.output).toBe(25.0)
|
||||||
|
|
||||||
|
const sonnet45 = getModelPricing('claude-sonnet-4-5')
|
||||||
|
expect(sonnet45).toBeDefined()
|
||||||
|
expect(sonnet45.input).toBe(3.0)
|
||||||
|
expect(sonnet45.output).toBe(15.0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should have correct pricing for key OpenAI models', () => {
|
||||||
|
const gpt4o = getModelPricing('gpt-4o')
|
||||||
|
expect(gpt4o).toBeDefined()
|
||||||
|
expect(gpt4o.input).toBe(2.5)
|
||||||
|
expect(gpt4o.output).toBe(10.0)
|
||||||
|
|
||||||
|
const o3 = getModelPricing('o3')
|
||||||
|
expect(o3).toBeDefined()
|
||||||
|
expect(o3.input).toBe(2.0)
|
||||||
|
expect(o3.output).toBe(8.0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should have correct pricing for Azure OpenAI o3', () => {
|
||||||
|
const azureO3 = getModelPricing('azure/o3')
|
||||||
|
expect(azureO3).toBeDefined()
|
||||||
|
expect(azureO3.input).toBe(2.0)
|
||||||
|
expect(azureO3.output).toBe(8.0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it.concurrent('should return null for unknown models', () => {
|
||||||
|
expect(getModelPricing('unknown-model')).toBeNull()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('Context Window Validation', () => {
|
||||||
|
it.concurrent('should have correct context windows for key models', () => {
|
||||||
|
const allModels = getAllModels()
|
||||||
|
|
||||||
|
expect(allModels).toContain('gpt-5-chat-latest')
|
||||||
|
|
||||||
|
expect(allModels).toContain('o3')
|
||||||
|
expect(allModels).toContain('o4-mini')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('Cost Calculation', () => {
|
describe('Cost Calculation', () => {
|
||||||
@@ -464,7 +659,7 @@ describe('Cost Calculation', () => {
|
|||||||
expect(result.output).toBeGreaterThan(0)
|
expect(result.output).toBeGreaterThan(0)
|
||||||
expect(result.total).toBeCloseTo(result.input + result.output, 6)
|
expect(result.total).toBeCloseTo(result.input + result.output, 6)
|
||||||
expect(result.pricing).toBeDefined()
|
expect(result.pricing).toBeDefined()
|
||||||
expect(result.pricing.input).toBe(2.5) // GPT-4o pricing
|
expect(result.pricing.input).toBe(2.5)
|
||||||
})
|
})
|
||||||
|
|
||||||
it.concurrent('should handle cached input pricing when enabled', () => {
|
it.concurrent('should handle cached input pricing when enabled', () => {
|
||||||
@@ -472,7 +667,7 @@ describe('Cost Calculation', () => {
|
|||||||
const cachedCost = calculateCost('gpt-4o', 1000, 500, true)
|
const cachedCost = calculateCost('gpt-4o', 1000, 500, true)
|
||||||
|
|
||||||
expect(cachedCost.input).toBeLessThan(regularCost.input)
|
expect(cachedCost.input).toBeLessThan(regularCost.input)
|
||||||
expect(cachedCost.output).toBe(regularCost.output) // Output cost should be same
|
expect(cachedCost.output).toBe(regularCost.output)
|
||||||
})
|
})
|
||||||
|
|
||||||
it.concurrent('should return default pricing for unknown models', () => {
|
it.concurrent('should return default pricing for unknown models', () => {
|
||||||
@@ -481,7 +676,7 @@ describe('Cost Calculation', () => {
|
|||||||
expect(result.input).toBe(0)
|
expect(result.input).toBe(0)
|
||||||
expect(result.output).toBe(0)
|
expect(result.output).toBe(0)
|
||||||
expect(result.total).toBe(0)
|
expect(result.total).toBe(0)
|
||||||
expect(result.pricing.input).toBe(1.0) // Default pricing
|
expect(result.pricing.input).toBe(1.0)
|
||||||
})
|
})
|
||||||
|
|
||||||
it.concurrent('should handle zero tokens', () => {
|
it.concurrent('should handle zero tokens', () => {
|
||||||
@@ -528,19 +723,15 @@ describe('getHostedModels', () => {
|
|||||||
it.concurrent('should return OpenAI, Anthropic, and Google models as hosted', () => {
|
it.concurrent('should return OpenAI, Anthropic, and Google models as hosted', () => {
|
||||||
const hostedModels = getHostedModels()
|
const hostedModels = getHostedModels()
|
||||||
|
|
||||||
// OpenAI models
|
|
||||||
expect(hostedModels).toContain('gpt-4o')
|
expect(hostedModels).toContain('gpt-4o')
|
||||||
expect(hostedModels).toContain('o1')
|
expect(hostedModels).toContain('o1')
|
||||||
|
|
||||||
// Anthropic models
|
|
||||||
expect(hostedModels).toContain('claude-sonnet-4-0')
|
expect(hostedModels).toContain('claude-sonnet-4-0')
|
||||||
expect(hostedModels).toContain('claude-opus-4-0')
|
expect(hostedModels).toContain('claude-opus-4-0')
|
||||||
|
|
||||||
// Google models
|
|
||||||
expect(hostedModels).toContain('gemini-2.5-pro')
|
expect(hostedModels).toContain('gemini-2.5-pro')
|
||||||
expect(hostedModels).toContain('gemini-2.5-flash')
|
expect(hostedModels).toContain('gemini-2.5-flash')
|
||||||
|
|
||||||
// Should not contain models from other providers
|
|
||||||
expect(hostedModels).not.toContain('deepseek-v3')
|
expect(hostedModels).not.toContain('deepseek-v3')
|
||||||
expect(hostedModels).not.toContain('grok-4-latest')
|
expect(hostedModels).not.toContain('grok-4-latest')
|
||||||
})
|
})
|
||||||
@@ -558,31 +749,24 @@ describe('getHostedModels', () => {
|
|||||||
|
|
||||||
describe('shouldBillModelUsage', () => {
|
describe('shouldBillModelUsage', () => {
|
||||||
it.concurrent('should return true for exact matches of hosted models', () => {
|
it.concurrent('should return true for exact matches of hosted models', () => {
|
||||||
// OpenAI models
|
|
||||||
expect(shouldBillModelUsage('gpt-4o')).toBe(true)
|
expect(shouldBillModelUsage('gpt-4o')).toBe(true)
|
||||||
expect(shouldBillModelUsage('o1')).toBe(true)
|
expect(shouldBillModelUsage('o1')).toBe(true)
|
||||||
|
|
||||||
// Anthropic models
|
|
||||||
expect(shouldBillModelUsage('claude-sonnet-4-0')).toBe(true)
|
expect(shouldBillModelUsage('claude-sonnet-4-0')).toBe(true)
|
||||||
expect(shouldBillModelUsage('claude-opus-4-0')).toBe(true)
|
expect(shouldBillModelUsage('claude-opus-4-0')).toBe(true)
|
||||||
|
|
||||||
// Google models
|
|
||||||
expect(shouldBillModelUsage('gemini-2.5-pro')).toBe(true)
|
expect(shouldBillModelUsage('gemini-2.5-pro')).toBe(true)
|
||||||
expect(shouldBillModelUsage('gemini-2.5-flash')).toBe(true)
|
expect(shouldBillModelUsage('gemini-2.5-flash')).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
it.concurrent('should return false for non-hosted models', () => {
|
it.concurrent('should return false for non-hosted models', () => {
|
||||||
// Other providers
|
|
||||||
expect(shouldBillModelUsage('deepseek-v3')).toBe(false)
|
expect(shouldBillModelUsage('deepseek-v3')).toBe(false)
|
||||||
expect(shouldBillModelUsage('grok-4-latest')).toBe(false)
|
expect(shouldBillModelUsage('grok-4-latest')).toBe(false)
|
||||||
|
|
||||||
// Unknown models
|
|
||||||
expect(shouldBillModelUsage('unknown-model')).toBe(false)
|
expect(shouldBillModelUsage('unknown-model')).toBe(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
it.concurrent('should return false for versioned model names not in hosted list', () => {
|
it.concurrent('should return false for versioned model names not in hosted list', () => {
|
||||||
// Versioned model names that are NOT in the hosted list
|
|
||||||
// These should NOT be billed (user provides own API key)
|
|
||||||
expect(shouldBillModelUsage('claude-sonnet-4-20250514')).toBe(false)
|
expect(shouldBillModelUsage('claude-sonnet-4-20250514')).toBe(false)
|
||||||
expect(shouldBillModelUsage('gpt-4o-2024-08-06')).toBe(false)
|
expect(shouldBillModelUsage('gpt-4o-2024-08-06')).toBe(false)
|
||||||
expect(shouldBillModelUsage('claude-3-5-sonnet-20241022')).toBe(false)
|
expect(shouldBillModelUsage('claude-3-5-sonnet-20241022')).toBe(false)
|
||||||
@@ -595,8 +779,7 @@ describe('shouldBillModelUsage', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it.concurrent('should not match partial model names', () => {
|
it.concurrent('should not match partial model names', () => {
|
||||||
// Should not match partial/prefix models
|
expect(shouldBillModelUsage('gpt-4')).toBe(false)
|
||||||
expect(shouldBillModelUsage('gpt-4')).toBe(false) // gpt-4o is hosted, not gpt-4
|
|
||||||
expect(shouldBillModelUsage('claude-sonnet')).toBe(false)
|
expect(shouldBillModelUsage('claude-sonnet')).toBe(false)
|
||||||
expect(shouldBillModelUsage('gemini')).toBe(false)
|
expect(shouldBillModelUsage('gemini')).toBe(false)
|
||||||
})
|
})
|
||||||
@@ -612,8 +795,8 @@ describe('Provider Management', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it.concurrent('should use model patterns for pattern matching', () => {
|
it.concurrent('should use model patterns for pattern matching', () => {
|
||||||
expect(getProviderFromModel('gpt-5-custom')).toBe('openai') // Matches /^gpt/ pattern
|
expect(getProviderFromModel('gpt-5-custom')).toBe('openai')
|
||||||
expect(getProviderFromModel('claude-custom-model')).toBe('anthropic') // Matches /^claude/ pattern
|
expect(getProviderFromModel('claude-custom-model')).toBe('anthropic')
|
||||||
})
|
})
|
||||||
|
|
||||||
it.concurrent('should default to ollama for unknown models', () => {
|
it.concurrent('should default to ollama for unknown models', () => {
|
||||||
@@ -667,7 +850,6 @@ describe('Provider Management', () => {
|
|||||||
expect(Array.isArray(allModels)).toBe(true)
|
expect(Array.isArray(allModels)).toBe(true)
|
||||||
expect(allModels.length).toBeGreaterThan(0)
|
expect(allModels.length).toBeGreaterThan(0)
|
||||||
|
|
||||||
// Should contain models from different providers
|
|
||||||
expect(allModels).toContain('gpt-4o')
|
expect(allModels).toContain('gpt-4o')
|
||||||
expect(allModels).toContain('claude-sonnet-4-0')
|
expect(allModels).toContain('claude-sonnet-4-0')
|
||||||
expect(allModels).toContain('gemini-2.5-pro')
|
expect(allModels).toContain('gemini-2.5-pro')
|
||||||
@@ -712,7 +894,6 @@ describe('Provider Management', () => {
|
|||||||
|
|
||||||
const baseProviders = getBaseModelProviders()
|
const baseProviders = getBaseModelProviders()
|
||||||
expect(typeof baseProviders).toBe('object')
|
expect(typeof baseProviders).toBe('object')
|
||||||
// Should exclude ollama models
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -720,10 +901,8 @@ describe('Provider Management', () => {
|
|||||||
it.concurrent('should update ollama models', () => {
|
it.concurrent('should update ollama models', () => {
|
||||||
const mockModels = ['llama2', 'codellama', 'mistral']
|
const mockModels = ['llama2', 'codellama', 'mistral']
|
||||||
|
|
||||||
// This should not throw
|
|
||||||
expect(() => updateOllamaProviderModels(mockModels)).not.toThrow()
|
expect(() => updateOllamaProviderModels(mockModels)).not.toThrow()
|
||||||
|
|
||||||
// Verify the models were updated
|
|
||||||
const ollamaModels = getProviderModels('ollama')
|
const ollamaModels = getProviderModels('ollama')
|
||||||
expect(ollamaModels).toEqual(mockModels)
|
expect(ollamaModels).toEqual(mockModels)
|
||||||
})
|
})
|
||||||
@@ -754,7 +933,7 @@ describe('JSON and Structured Output', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it.concurrent('should clean up common JSON issues', () => {
|
it.concurrent('should clean up common JSON issues', () => {
|
||||||
const content = '{\n "key": "value",\n "number": 42,\n}' // Trailing comma
|
const content = '{\n "key": "value",\n "number": 42,\n}'
|
||||||
const result = extractAndParseJSON(content)
|
const result = extractAndParseJSON(content)
|
||||||
expect(result).toEqual({ key: 'value', number: 42 })
|
expect(result).toEqual({ key: 'value', number: 42 })
|
||||||
})
|
})
|
||||||
@@ -945,13 +1124,13 @@ describe('prepareToolExecution', () => {
|
|||||||
const { toolParams } = prepareToolExecution(tool, llmArgs, request)
|
const { toolParams } = prepareToolExecution(tool, llmArgs, request)
|
||||||
|
|
||||||
expect(toolParams.apiKey).toBe('user-key')
|
expect(toolParams.apiKey).toBe('user-key')
|
||||||
expect(toolParams.channel).toBe('#general') // User value wins
|
expect(toolParams.channel).toBe('#general')
|
||||||
expect(toolParams.message).toBe('Hello world')
|
expect(toolParams.message).toBe('Hello world')
|
||||||
})
|
})
|
||||||
|
|
||||||
it.concurrent('should filter out empty string user params', () => {
|
it.concurrent('should filter out empty string user params', () => {
|
||||||
const tool = {
|
const tool = {
|
||||||
params: { apiKey: 'user-key', channel: '' }, // Empty channel
|
params: { apiKey: 'user-key', channel: '' },
|
||||||
}
|
}
|
||||||
const llmArgs = { message: 'Hello', channel: '#llm-channel' }
|
const llmArgs = { message: 'Hello', channel: '#llm-channel' }
|
||||||
const request = {}
|
const request = {}
|
||||||
@@ -959,7 +1138,7 @@ describe('prepareToolExecution', () => {
|
|||||||
const { toolParams } = prepareToolExecution(tool, llmArgs, request)
|
const { toolParams } = prepareToolExecution(tool, llmArgs, request)
|
||||||
|
|
||||||
expect(toolParams.apiKey).toBe('user-key')
|
expect(toolParams.apiKey).toBe('user-key')
|
||||||
expect(toolParams.channel).toBe('#llm-channel') // LLM value used since user is empty
|
expect(toolParams.channel).toBe('#llm-channel')
|
||||||
expect(toolParams.message).toBe('Hello')
|
expect(toolParams.message).toBe('Hello')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -969,7 +1148,7 @@ describe('prepareToolExecution', () => {
|
|||||||
const tool = {
|
const tool = {
|
||||||
params: {
|
params: {
|
||||||
workflowId: 'child-workflow-123',
|
workflowId: 'child-workflow-123',
|
||||||
inputMapping: '{}', // Empty JSON string from UI
|
inputMapping: '{}',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const llmArgs = {
|
const llmArgs = {
|
||||||
@@ -979,7 +1158,6 @@ describe('prepareToolExecution', () => {
|
|||||||
|
|
||||||
const { toolParams } = prepareToolExecution(tool, llmArgs, request)
|
const { toolParams } = prepareToolExecution(tool, llmArgs, request)
|
||||||
|
|
||||||
// LLM values should be used since user object is empty
|
|
||||||
expect(toolParams.inputMapping).toEqual({ query: 'search term', limit: 10 })
|
expect(toolParams.inputMapping).toEqual({ query: 'search term', limit: 10 })
|
||||||
expect(toolParams.workflowId).toBe('child-workflow-123')
|
expect(toolParams.workflowId).toBe('child-workflow-123')
|
||||||
})
|
})
|
||||||
@@ -988,7 +1166,7 @@ describe('prepareToolExecution', () => {
|
|||||||
const tool = {
|
const tool = {
|
||||||
params: {
|
params: {
|
||||||
workflowId: 'child-workflow',
|
workflowId: 'child-workflow',
|
||||||
inputMapping: '{"query": "", "customField": "user-value"}', // Partial values
|
inputMapping: '{"query": "", "customField": "user-value"}',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const llmArgs = {
|
const llmArgs = {
|
||||||
@@ -998,7 +1176,6 @@ describe('prepareToolExecution', () => {
|
|||||||
|
|
||||||
const { toolParams } = prepareToolExecution(tool, llmArgs, request)
|
const { toolParams } = prepareToolExecution(tool, llmArgs, request)
|
||||||
|
|
||||||
// LLM fills empty query, user's customField preserved, LLM's limit included
|
|
||||||
expect(toolParams.inputMapping).toEqual({
|
expect(toolParams.inputMapping).toEqual({
|
||||||
query: 'llm-search',
|
query: 'llm-search',
|
||||||
limit: 10,
|
limit: 10,
|
||||||
@@ -1020,7 +1197,6 @@ describe('prepareToolExecution', () => {
|
|||||||
|
|
||||||
const { toolParams } = prepareToolExecution(tool, llmArgs, request)
|
const { toolParams } = prepareToolExecution(tool, llmArgs, request)
|
||||||
|
|
||||||
// User values win, but LLM's extra field is included
|
|
||||||
expect(toolParams.inputMapping).toEqual({
|
expect(toolParams.inputMapping).toEqual({
|
||||||
query: 'user-search',
|
query: 'user-search',
|
||||||
limit: 5,
|
limit: 5,
|
||||||
@@ -1032,7 +1208,7 @@ describe('prepareToolExecution', () => {
|
|||||||
const tool = {
|
const tool = {
|
||||||
params: {
|
params: {
|
||||||
workflowId: 'child-workflow',
|
workflowId: 'child-workflow',
|
||||||
inputMapping: { query: '', customField: 'user-value' }, // Object, not string
|
inputMapping: { query: '', customField: 'user-value' },
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const llmArgs = {
|
const llmArgs = {
|
||||||
@@ -1051,7 +1227,7 @@ describe('prepareToolExecution', () => {
|
|||||||
|
|
||||||
it.concurrent('should use LLM inputMapping when user does not provide it', () => {
|
it.concurrent('should use LLM inputMapping when user does not provide it', () => {
|
||||||
const tool = {
|
const tool = {
|
||||||
params: { workflowId: 'child-workflow' }, // No inputMapping
|
params: { workflowId: 'child-workflow' },
|
||||||
}
|
}
|
||||||
const llmArgs = {
|
const llmArgs = {
|
||||||
inputMapping: { query: 'llm-search', limit: 10 },
|
inputMapping: { query: 'llm-search', limit: 10 },
|
||||||
@@ -1070,7 +1246,7 @@ describe('prepareToolExecution', () => {
|
|||||||
inputMapping: '{"query": "user-search"}',
|
inputMapping: '{"query": "user-search"}',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
const llmArgs = {} // No inputMapping from LLM
|
const llmArgs = {}
|
||||||
const request = {}
|
const request = {}
|
||||||
|
|
||||||
const { toolParams } = prepareToolExecution(tool, llmArgs, request)
|
const { toolParams } = prepareToolExecution(tool, llmArgs, request)
|
||||||
@@ -1092,7 +1268,6 @@ describe('prepareToolExecution', () => {
|
|||||||
|
|
||||||
const { toolParams } = prepareToolExecution(tool, llmArgs, request)
|
const { toolParams } = prepareToolExecution(tool, llmArgs, request)
|
||||||
|
|
||||||
// Should use LLM values since user JSON is invalid
|
|
||||||
expect(toolParams.inputMapping).toEqual({ query: 'llm-search' })
|
expect(toolParams.inputMapping).toEqual({ query: 'llm-search' })
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1105,9 +1280,8 @@ describe('prepareToolExecution', () => {
|
|||||||
|
|
||||||
const { toolParams } = prepareToolExecution(tool, llmArgs, request)
|
const { toolParams } = prepareToolExecution(tool, llmArgs, request)
|
||||||
|
|
||||||
// Normal behavior: user values override LLM values
|
|
||||||
expect(toolParams.apiKey).toBe('user-key')
|
expect(toolParams.apiKey).toBe('user-key')
|
||||||
expect(toolParams.channel).toBe('#general') // User value wins
|
expect(toolParams.channel).toBe('#general')
|
||||||
expect(toolParams.message).toBe('Hello')
|
expect(toolParams.message).toBe('Hello')
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1125,8 +1299,6 @@ describe('prepareToolExecution', () => {
|
|||||||
|
|
||||||
const { toolParams } = prepareToolExecution(tool, llmArgs, request)
|
const { toolParams } = prepareToolExecution(tool, llmArgs, request)
|
||||||
|
|
||||||
// 0 and false should be preserved (they're valid values)
|
|
||||||
// empty string should be filled by LLM
|
|
||||||
expect(toolParams.inputMapping).toEqual({
|
expect(toolParams.inputMapping).toEqual({
|
||||||
limit: 0,
|
limit: 0,
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { createLogger, type Logger } from '@sim/logger'
|
import { createLogger, type Logger } from '@sim/logger'
|
||||||
|
import type OpenAI from 'openai'
|
||||||
import type { ChatCompletionChunk } from 'openai/resources/chat/completions'
|
import type { ChatCompletionChunk } from 'openai/resources/chat/completions'
|
||||||
import type { CompletionUsage } from 'openai/resources/completions'
|
import type { CompletionUsage } from 'openai/resources/completions'
|
||||||
import { env } from '@/lib/core/config/env'
|
import { env } from '@/lib/core/config/env'
|
||||||
@@ -113,6 +114,8 @@ function buildProviderMetadata(providerId: ProviderId): ProviderMetadata {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const providers: Record<ProviderId, ProviderMetadata> = {
|
export const providers: Record<ProviderId, ProviderMetadata> = {
|
||||||
|
ollama: buildProviderMetadata('ollama'),
|
||||||
|
vllm: buildProviderMetadata('vllm'),
|
||||||
openai: {
|
openai: {
|
||||||
...buildProviderMetadata('openai'),
|
...buildProviderMetadata('openai'),
|
||||||
computerUseModels: ['computer-use-preview'],
|
computerUseModels: ['computer-use-preview'],
|
||||||
@@ -123,19 +126,17 @@ export const providers: Record<ProviderId, ProviderMetadata> = {
|
|||||||
getProviderModelsFromDefinitions('anthropic').includes(model)
|
getProviderModelsFromDefinitions('anthropic').includes(model)
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
'azure-anthropic': buildProviderMetadata('azure-anthropic'),
|
|
||||||
google: buildProviderMetadata('google'),
|
google: buildProviderMetadata('google'),
|
||||||
vertex: buildProviderMetadata('vertex'),
|
vertex: buildProviderMetadata('vertex'),
|
||||||
|
'azure-openai': buildProviderMetadata('azure-openai'),
|
||||||
|
'azure-anthropic': buildProviderMetadata('azure-anthropic'),
|
||||||
deepseek: buildProviderMetadata('deepseek'),
|
deepseek: buildProviderMetadata('deepseek'),
|
||||||
xai: buildProviderMetadata('xai'),
|
xai: buildProviderMetadata('xai'),
|
||||||
cerebras: buildProviderMetadata('cerebras'),
|
cerebras: buildProviderMetadata('cerebras'),
|
||||||
groq: buildProviderMetadata('groq'),
|
groq: buildProviderMetadata('groq'),
|
||||||
vllm: buildProviderMetadata('vllm'),
|
|
||||||
mistral: buildProviderMetadata('mistral'),
|
mistral: buildProviderMetadata('mistral'),
|
||||||
'azure-openai': buildProviderMetadata('azure-openai'),
|
|
||||||
openrouter: buildProviderMetadata('openrouter'),
|
|
||||||
ollama: buildProviderMetadata('ollama'),
|
|
||||||
bedrock: buildProviderMetadata('bedrock'),
|
bedrock: buildProviderMetadata('bedrock'),
|
||||||
|
openrouter: buildProviderMetadata('openrouter'),
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateOllamaProviderModels(models: string[]): void {
|
export function updateOllamaProviderModels(models: string[]): void {
|
||||||
@@ -995,15 +996,12 @@ export function getThinkingLevelsForModel(model: string): string[] | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get max output tokens for a specific model
|
* Get max output tokens for a specific model.
|
||||||
* Returns the model's maxOutputTokens capability for streaming requests,
|
|
||||||
* or a conservative default (8192) for non-streaming requests to avoid timeout issues.
|
|
||||||
*
|
*
|
||||||
* @param model - The model ID
|
* @param model - The model ID
|
||||||
* @param streaming - Whether the request is streaming (default: false)
|
|
||||||
*/
|
*/
|
||||||
export function getMaxOutputTokensForModel(model: string, streaming = false): number {
|
export function getMaxOutputTokensForModel(model: string): number {
|
||||||
return getMaxOutputTokensForModelFromDefinitions(model, streaming)
|
return getMaxOutputTokensForModelFromDefinitions(model)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1126,8 +1124,8 @@ export function createOpenAICompatibleStream(
|
|||||||
* @returns Object with hasUsedForcedTool flag and updated usedForcedTools array
|
* @returns Object with hasUsedForcedTool flag and updated usedForcedTools array
|
||||||
*/
|
*/
|
||||||
export function checkForForcedToolUsageOpenAI(
|
export function checkForForcedToolUsageOpenAI(
|
||||||
response: any,
|
response: OpenAI.Chat.Completions.ChatCompletion,
|
||||||
toolChoice: string | { type: string; function?: { name: string }; name?: string; any?: any },
|
toolChoice: string | { type: string; function?: { name: string }; name?: string },
|
||||||
providerName: string,
|
providerName: string,
|
||||||
forcedTools: string[],
|
forcedTools: string[],
|
||||||
usedForcedTools: string[],
|
usedForcedTools: string[],
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ export function setupConnectionHandlers(socket: AuthenticatedSocket, roomManager
|
|||||||
cleanupPendingSubblocksForSocket(socket.id)
|
cleanupPendingSubblocksForSocket(socket.id)
|
||||||
cleanupPendingVariablesForSocket(socket.id)
|
cleanupPendingVariablesForSocket(socket.id)
|
||||||
|
|
||||||
const workflowId = await roomManager.removeUserFromRoom(socket.id)
|
const workflowIdHint = [...socket.rooms].find((roomId) => roomId !== socket.id)
|
||||||
|
const workflowId = await roomManager.removeUserFromRoom(socket.id, workflowIdHint)
|
||||||
|
|
||||||
if (workflowId) {
|
if (workflowId) {
|
||||||
await roomManager.broadcastPresenceUpdate(workflowId)
|
await roomManager.broadcastPresenceUpdate(workflowId)
|
||||||
|
|||||||
@@ -51,26 +51,66 @@ export function setupWorkflowHandlers(socket: AuthenticatedSocket, roomManager:
|
|||||||
const currentWorkflowId = await roomManager.getWorkflowIdForSocket(socket.id)
|
const currentWorkflowId = await roomManager.getWorkflowIdForSocket(socket.id)
|
||||||
if (currentWorkflowId) {
|
if (currentWorkflowId) {
|
||||||
socket.leave(currentWorkflowId)
|
socket.leave(currentWorkflowId)
|
||||||
await roomManager.removeUserFromRoom(socket.id)
|
await roomManager.removeUserFromRoom(socket.id, currentWorkflowId)
|
||||||
await roomManager.broadcastPresenceUpdate(currentWorkflowId)
|
await roomManager.broadcastPresenceUpdate(currentWorkflowId)
|
||||||
}
|
}
|
||||||
|
|
||||||
const STALE_THRESHOLD_MS = 60_000
|
// Keep this above Redis socket key TTL (1h) so a normal idle user is not evicted too aggressively.
|
||||||
|
const STALE_THRESHOLD_MS = 75 * 60 * 1000
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const existingUsers = await roomManager.getWorkflowUsers(workflowId)
|
const existingUsers = await roomManager.getWorkflowUsers(workflowId)
|
||||||
for (const existingUser of existingUsers) {
|
let liveSocketIds = new Set<string>()
|
||||||
if (existingUser.userId === userId && existingUser.socketId !== socket.id) {
|
let canCheckLiveness = false
|
||||||
const isSameTab = tabSessionId && existingUser.tabSessionId === tabSessionId
|
|
||||||
const isStale =
|
|
||||||
now - (existingUser.lastActivity || existingUser.joinedAt || 0) > STALE_THRESHOLD_MS
|
|
||||||
|
|
||||||
if (isSameTab || isStale) {
|
try {
|
||||||
logger.info(
|
const liveSockets = await roomManager.io.in(workflowId).fetchSockets()
|
||||||
`Cleaning up socket ${existingUser.socketId} for user ${userId} (${isSameTab ? 'same tab' : 'stale'})`
|
liveSocketIds = new Set(liveSockets.map((liveSocket) => liveSocket.id))
|
||||||
|
canCheckLiveness = true
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(
|
||||||
|
`Skipping stale cleanup for ${workflowId} due to live socket lookup failure`,
|
||||||
|
error
|
||||||
)
|
)
|
||||||
await roomManager.removeUserFromRoom(existingUser.socketId)
|
|
||||||
roomManager.io.in(existingUser.socketId).socketsLeave(workflowId)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const existingUser of existingUsers) {
|
||||||
|
try {
|
||||||
|
if (existingUser.socketId === socket.id) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const isSameTab = Boolean(
|
||||||
|
existingUser.userId === userId &&
|
||||||
|
tabSessionId &&
|
||||||
|
existingUser.tabSessionId === tabSessionId
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isSameTab) {
|
||||||
|
logger.info(
|
||||||
|
`Cleaning up socket ${existingUser.socketId} for user ${existingUser.userId} (same tab)`
|
||||||
|
)
|
||||||
|
await roomManager.removeUserFromRoom(existingUser.socketId, workflowId)
|
||||||
|
await roomManager.io.in(existingUser.socketId).socketsLeave(workflowId)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!canCheckLiveness || liveSocketIds.has(existingUser.socketId)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
const isStaleByActivity =
|
||||||
|
now - (existingUser.lastActivity || existingUser.joinedAt || 0) > STALE_THRESHOLD_MS
|
||||||
|
if (!isStaleByActivity) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Cleaning up socket ${existingUser.socketId} for user ${existingUser.userId} (stale activity)`
|
||||||
|
)
|
||||||
|
await roomManager.removeUserFromRoom(existingUser.socketId, workflowId)
|
||||||
|
await roomManager.io.in(existingUser.socketId).socketsLeave(workflowId)
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`Best-effort cleanup failed for socket ${existingUser.socketId}`, error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,7 +176,7 @@ export function setupWorkflowHandlers(socket: AuthenticatedSocket, roomManager:
|
|||||||
logger.error('Error joining workflow:', error)
|
logger.error('Error joining workflow:', error)
|
||||||
// Undo socket.join and room manager entry if any operation failed
|
// Undo socket.join and room manager entry if any operation failed
|
||||||
socket.leave(workflowId)
|
socket.leave(workflowId)
|
||||||
await roomManager.removeUserFromRoom(socket.id)
|
await roomManager.removeUserFromRoom(socket.id, workflowId)
|
||||||
const isReady = roomManager.isReady()
|
const isReady = roomManager.isReady()
|
||||||
socket.emit('join-workflow-error', {
|
socket.emit('join-workflow-error', {
|
||||||
error: isReady ? 'Failed to join workflow' : 'Realtime unavailable',
|
error: isReady ? 'Failed to join workflow' : 'Realtime unavailable',
|
||||||
@@ -156,7 +196,7 @@ export function setupWorkflowHandlers(socket: AuthenticatedSocket, roomManager:
|
|||||||
|
|
||||||
if (workflowId && session) {
|
if (workflowId && session) {
|
||||||
socket.leave(workflowId)
|
socket.leave(workflowId)
|
||||||
await roomManager.removeUserFromRoom(socket.id)
|
await roomManager.removeUserFromRoom(socket.id, workflowId)
|
||||||
await roomManager.broadcastPresenceUpdate(workflowId)
|
await roomManager.broadcastPresenceUpdate(workflowId)
|
||||||
|
|
||||||
logger.info(`User ${session.userId} (${session.userName}) left workflow ${workflowId}`)
|
logger.info(`User ${session.userId} (${session.userName}) left workflow ${workflowId}`)
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ export class MemoryRoomManager implements IRoomManager {
|
|||||||
logger.debug(`Added user ${presence.userId} to workflow ${workflowId} (socket: ${socketId})`)
|
logger.debug(`Added user ${presence.userId} to workflow ${workflowId} (socket: ${socketId})`)
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeUserFromRoom(socketId: string): Promise<string | null> {
|
async removeUserFromRoom(socketId: string, _workflowIdHint?: string): Promise<string | null> {
|
||||||
const workflowId = this.socketToWorkflow.get(socketId)
|
const workflowId = this.socketToWorkflow.get(socketId)
|
||||||
|
|
||||||
if (!workflowId) {
|
if (!workflowId) {
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ const KEYS = {
|
|||||||
workflowMeta: (wfId: string) => `workflow:${wfId}:meta`,
|
workflowMeta: (wfId: string) => `workflow:${wfId}:meta`,
|
||||||
socketWorkflow: (socketId: string) => `socket:${socketId}:workflow`,
|
socketWorkflow: (socketId: string) => `socket:${socketId}:workflow`,
|
||||||
socketSession: (socketId: string) => `socket:${socketId}:session`,
|
socketSession: (socketId: string) => `socket:${socketId}:session`,
|
||||||
|
socketPresenceWorkflow: (socketId: string) => `socket:${socketId}:presence-workflow`,
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
const SOCKET_KEY_TTL = 3600
|
const SOCKET_KEY_TTL = 3600
|
||||||
|
const SOCKET_PRESENCE_WORKFLOW_KEY_TTL = 24 * 60 * 60
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lua script for atomic user removal from room.
|
* Lua script for atomic user removal from room.
|
||||||
@@ -22,11 +24,21 @@ const SOCKET_KEY_TTL = 3600
|
|||||||
const REMOVE_USER_SCRIPT = `
|
const REMOVE_USER_SCRIPT = `
|
||||||
local socketWorkflowKey = KEYS[1]
|
local socketWorkflowKey = KEYS[1]
|
||||||
local socketSessionKey = KEYS[2]
|
local socketSessionKey = KEYS[2]
|
||||||
|
local socketPresenceWorkflowKey = KEYS[3]
|
||||||
local workflowUsersPrefix = ARGV[1]
|
local workflowUsersPrefix = ARGV[1]
|
||||||
local workflowMetaPrefix = ARGV[2]
|
local workflowMetaPrefix = ARGV[2]
|
||||||
local socketId = ARGV[3]
|
local socketId = ARGV[3]
|
||||||
|
local workflowIdHint = ARGV[4]
|
||||||
|
|
||||||
local workflowId = redis.call('GET', socketWorkflowKey)
|
local workflowId = redis.call('GET', socketWorkflowKey)
|
||||||
|
if not workflowId then
|
||||||
|
workflowId = redis.call('GET', socketPresenceWorkflowKey)
|
||||||
|
end
|
||||||
|
|
||||||
|
if not workflowId and workflowIdHint ~= '' then
|
||||||
|
workflowId = workflowIdHint
|
||||||
|
end
|
||||||
|
|
||||||
if not workflowId then
|
if not workflowId then
|
||||||
return nil
|
return nil
|
||||||
end
|
end
|
||||||
@@ -35,7 +47,7 @@ local workflowUsersKey = workflowUsersPrefix .. workflowId .. ':users'
|
|||||||
local workflowMetaKey = workflowMetaPrefix .. workflowId .. ':meta'
|
local workflowMetaKey = workflowMetaPrefix .. workflowId .. ':meta'
|
||||||
|
|
||||||
redis.call('HDEL', workflowUsersKey, socketId)
|
redis.call('HDEL', workflowUsersKey, socketId)
|
||||||
redis.call('DEL', socketWorkflowKey, socketSessionKey)
|
redis.call('DEL', socketWorkflowKey, socketSessionKey, socketPresenceWorkflowKey)
|
||||||
|
|
||||||
local remaining = redis.call('HLEN', workflowUsersKey)
|
local remaining = redis.call('HLEN', workflowUsersKey)
|
||||||
if remaining == 0 then
|
if remaining == 0 then
|
||||||
@@ -54,11 +66,13 @@ const UPDATE_ACTIVITY_SCRIPT = `
|
|||||||
local workflowUsersKey = KEYS[1]
|
local workflowUsersKey = KEYS[1]
|
||||||
local socketWorkflowKey = KEYS[2]
|
local socketWorkflowKey = KEYS[2]
|
||||||
local socketSessionKey = KEYS[3]
|
local socketSessionKey = KEYS[3]
|
||||||
|
local socketPresenceWorkflowKey = KEYS[4]
|
||||||
local socketId = ARGV[1]
|
local socketId = ARGV[1]
|
||||||
local cursorJson = ARGV[2]
|
local cursorJson = ARGV[2]
|
||||||
local selectionJson = ARGV[3]
|
local selectionJson = ARGV[3]
|
||||||
local lastActivity = ARGV[4]
|
local lastActivity = ARGV[4]
|
||||||
local ttl = tonumber(ARGV[5])
|
local ttl = tonumber(ARGV[5])
|
||||||
|
local presenceWorkflowTtl = tonumber(ARGV[6])
|
||||||
|
|
||||||
local existingJson = redis.call('HGET', workflowUsersKey, socketId)
|
local existingJson = redis.call('HGET', workflowUsersKey, socketId)
|
||||||
if not existingJson then
|
if not existingJson then
|
||||||
@@ -78,6 +92,7 @@ existing.lastActivity = tonumber(lastActivity)
|
|||||||
redis.call('HSET', workflowUsersKey, socketId, cjson.encode(existing))
|
redis.call('HSET', workflowUsersKey, socketId, cjson.encode(existing))
|
||||||
redis.call('EXPIRE', socketWorkflowKey, ttl)
|
redis.call('EXPIRE', socketWorkflowKey, ttl)
|
||||||
redis.call('EXPIRE', socketSessionKey, ttl)
|
redis.call('EXPIRE', socketSessionKey, ttl)
|
||||||
|
redis.call('EXPIRE', socketPresenceWorkflowKey, presenceWorkflowTtl)
|
||||||
return 1
|
return 1
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -164,6 +179,8 @@ export class RedisRoomManager implements IRoomManager {
|
|||||||
pipeline.hSet(KEYS.workflowMeta(workflowId), 'lastModified', Date.now().toString())
|
pipeline.hSet(KEYS.workflowMeta(workflowId), 'lastModified', Date.now().toString())
|
||||||
pipeline.set(KEYS.socketWorkflow(socketId), workflowId)
|
pipeline.set(KEYS.socketWorkflow(socketId), workflowId)
|
||||||
pipeline.expire(KEYS.socketWorkflow(socketId), SOCKET_KEY_TTL)
|
pipeline.expire(KEYS.socketWorkflow(socketId), SOCKET_KEY_TTL)
|
||||||
|
pipeline.set(KEYS.socketPresenceWorkflow(socketId), workflowId)
|
||||||
|
pipeline.expire(KEYS.socketPresenceWorkflow(socketId), SOCKET_PRESENCE_WORKFLOW_KEY_TTL)
|
||||||
pipeline.hSet(KEYS.socketSession(socketId), {
|
pipeline.hSet(KEYS.socketSession(socketId), {
|
||||||
userId: presence.userId,
|
userId: presence.userId,
|
||||||
userName: presence.userName,
|
userName: presence.userName,
|
||||||
@@ -187,7 +204,11 @@ export class RedisRoomManager implements IRoomManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async removeUserFromRoom(socketId: string, retried = false): Promise<string | null> {
|
async removeUserFromRoom(
|
||||||
|
socketId: string,
|
||||||
|
workflowIdHint?: string,
|
||||||
|
retried = false
|
||||||
|
): Promise<string | null> {
|
||||||
if (!this.removeUserScriptSha) {
|
if (!this.removeUserScriptSha) {
|
||||||
logger.error('removeUserFromRoom called before initialize()')
|
logger.error('removeUserFromRoom called before initialize()')
|
||||||
return null
|
return null
|
||||||
@@ -195,19 +216,25 @@ export class RedisRoomManager implements IRoomManager {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const workflowId = await this.redis.evalSha(this.removeUserScriptSha, {
|
const workflowId = await this.redis.evalSha(this.removeUserScriptSha, {
|
||||||
keys: [KEYS.socketWorkflow(socketId), KEYS.socketSession(socketId)],
|
keys: [
|
||||||
arguments: ['workflow:', 'workflow:', socketId],
|
KEYS.socketWorkflow(socketId),
|
||||||
|
KEYS.socketSession(socketId),
|
||||||
|
KEYS.socketPresenceWorkflow(socketId),
|
||||||
|
],
|
||||||
|
arguments: ['workflow:', 'workflow:', socketId, workflowIdHint ?? ''],
|
||||||
})
|
})
|
||||||
|
|
||||||
if (workflowId) {
|
if (typeof workflowId === 'string' && workflowId.length > 0) {
|
||||||
logger.debug(`Removed socket ${socketId} from workflow ${workflowId}`)
|
logger.debug(`Removed socket ${socketId} from workflow ${workflowId}`)
|
||||||
|
return workflowId
|
||||||
}
|
}
|
||||||
return workflowId as string | null
|
|
||||||
|
return null
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if ((error as Error).message?.includes('NOSCRIPT') && !retried) {
|
if ((error as Error).message?.includes('NOSCRIPT') && !retried) {
|
||||||
logger.warn('Lua script not found, reloading...')
|
logger.warn('Lua script not found, reloading...')
|
||||||
this.removeUserScriptSha = await this.redis.scriptLoad(REMOVE_USER_SCRIPT)
|
this.removeUserScriptSha = await this.redis.scriptLoad(REMOVE_USER_SCRIPT)
|
||||||
return this.removeUserFromRoom(socketId, true)
|
return this.removeUserFromRoom(socketId, workflowIdHint, true)
|
||||||
}
|
}
|
||||||
logger.error(`Failed to remove user from room: ${socketId}`, error)
|
logger.error(`Failed to remove user from room: ${socketId}`, error)
|
||||||
return null
|
return null
|
||||||
@@ -215,7 +242,12 @@ export class RedisRoomManager implements IRoomManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getWorkflowIdForSocket(socketId: string): Promise<string | null> {
|
async getWorkflowIdForSocket(socketId: string): Promise<string | null> {
|
||||||
return this.redis.get(KEYS.socketWorkflow(socketId))
|
const workflowId = await this.redis.get(KEYS.socketWorkflow(socketId))
|
||||||
|
if (workflowId) {
|
||||||
|
return workflowId
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.redis.get(KEYS.socketPresenceWorkflow(socketId))
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserSession(socketId: string): Promise<UserSession | null> {
|
async getUserSession(socketId: string): Promise<UserSession | null> {
|
||||||
@@ -278,6 +310,7 @@ export class RedisRoomManager implements IRoomManager {
|
|||||||
KEYS.workflowUsers(workflowId),
|
KEYS.workflowUsers(workflowId),
|
||||||
KEYS.socketWorkflow(socketId),
|
KEYS.socketWorkflow(socketId),
|
||||||
KEYS.socketSession(socketId),
|
KEYS.socketSession(socketId),
|
||||||
|
KEYS.socketPresenceWorkflow(socketId),
|
||||||
],
|
],
|
||||||
arguments: [
|
arguments: [
|
||||||
socketId,
|
socketId,
|
||||||
@@ -285,6 +318,7 @@ export class RedisRoomManager implements IRoomManager {
|
|||||||
updates.selection !== undefined ? JSON.stringify(updates.selection) : '',
|
updates.selection !== undefined ? JSON.stringify(updates.selection) : '',
|
||||||
(updates.lastActivity ?? Date.now()).toString(),
|
(updates.lastActivity ?? Date.now()).toString(),
|
||||||
SOCKET_KEY_TTL.toString(),
|
SOCKET_KEY_TTL.toString(),
|
||||||
|
SOCKET_PRESENCE_WORKFLOW_KEY_TTL.toString(),
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -348,7 +382,7 @@ export class RedisRoomManager implements IRoomManager {
|
|||||||
|
|
||||||
// Remove all users from Redis state
|
// Remove all users from Redis state
|
||||||
for (const user of users) {
|
for (const user of users) {
|
||||||
await this.removeUserFromRoom(user.socketId)
|
await this.removeUserFromRoom(user.socketId, workflowId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up room data
|
// Clean up room data
|
||||||
|
|||||||
@@ -65,9 +65,10 @@ export interface IRoomManager {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove a user from their current room
|
* Remove a user from their current room
|
||||||
* Returns the workflowId they were in, or null if not in any room
|
* Optional workflowIdHint is used when socket mapping keys are missing/expired.
|
||||||
|
* Returns the workflowId they were in, or null if not in any room.
|
||||||
*/
|
*/
|
||||||
removeUserFromRoom(socketId: string): Promise<string | null>
|
removeUserFromRoom(socketId: string, workflowIdHint?: string): Promise<string | null>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the workflow ID for a socket
|
* Get the workflow ID for a socket
|
||||||
|
|||||||
@@ -961,6 +961,7 @@ async function executeMcpTool(
|
|||||||
|
|
||||||
const workspaceId = params._context?.workspaceId || executionContext?.workspaceId
|
const workspaceId = params._context?.workspaceId || executionContext?.workspaceId
|
||||||
const workflowId = params._context?.workflowId || executionContext?.workflowId
|
const workflowId = params._context?.workflowId || executionContext?.workflowId
|
||||||
|
const userId = params._context?.userId || executionContext?.userId
|
||||||
|
|
||||||
if (!workspaceId) {
|
if (!workspaceId) {
|
||||||
return {
|
return {
|
||||||
@@ -1002,7 +1003,12 @@ async function executeMcpTool(
|
|||||||
hasToolSchema: !!toolSchema,
|
hasToolSchema: !!toolSchema,
|
||||||
})
|
})
|
||||||
|
|
||||||
const response = await fetch(`${baseUrl}/api/mcp/tools/execute`, {
|
const mcpUrl = new URL('/api/mcp/tools/execute', baseUrl)
|
||||||
|
if (userId) {
|
||||||
|
mcpUrl.searchParams.set('userId', userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetch(mcpUrl.toString(), {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers,
|
headers,
|
||||||
body,
|
body,
|
||||||
|
|||||||
Reference in New Issue
Block a user