Files
wehub-resource-sync d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:20:55 +08:00

399 lines
14 KiB
TypeScript

/**
* @vitest-environment node
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'
const {
mockCreate,
openAIArgs,
mockOpenAI,
mockExecuteTool,
mockPrepareTools,
mockCheckForced,
mockCreateStream,
mockValidateUrlWithDNS,
mockCreatePinnedFetch,
pinnedFetchFn,
envState,
} = vi.hoisted(() => {
const openAIArgs: Array<Record<string, unknown>> = []
const mockCreate = vi.fn()
const pinnedFetchFn = vi.fn()
class MockOpenAI {
chat = { completions: { create: mockCreate } }
constructor(opts: Record<string, unknown>) {
openAIArgs.push(opts)
}
}
return {
mockCreate,
openAIArgs,
mockOpenAI: MockOpenAI,
mockExecuteTool: vi.fn(),
mockPrepareTools: vi.fn(),
mockCheckForced: vi.fn(),
mockCreateStream: vi.fn(),
mockValidateUrlWithDNS: vi.fn(),
mockCreatePinnedFetch: vi.fn(() => pinnedFetchFn),
pinnedFetchFn,
envState: {
VLLM_BASE_URL: 'http://localhost:8000',
VLLM_API_KEY: undefined as string | undefined,
},
}
})
vi.mock('openai', () => ({ default: mockOpenAI }))
vi.mock('@/lib/core/config/env', () => ({ env: envState }))
vi.mock('@/lib/core/security/input-validation.server', () => ({
validateUrlWithDNS: mockValidateUrlWithDNS,
createPinnedFetch: mockCreatePinnedFetch,
}))
vi.mock('@/providers', () => ({ MAX_TOOL_ITERATIONS: 20 }))
vi.mock('@/providers/models', () => ({
getProviderFileAttachment: vi
.fn()
.mockReturnValue({ maxBytes: 10 * 1024 * 1024, strategy: 'inline' }),
INLINE_ATTACHMENT_MAX_BYTES: 10 * 1024 * 1024,
getProviderModels: vi.fn(() => []),
getProviderDefaultModel: vi.fn(() => 'vllm/generic'),
}))
vi.mock('@/providers/attachments', () => ({
formatMessagesForProvider: vi.fn((messages) => messages),
}))
vi.mock('@/providers/trace-enrichment', () => ({
enrichLastModelSegmentFromChatCompletions: vi.fn(),
}))
vi.mock('@/providers/utils', () => ({
calculateCost: vi.fn(() => ({ input: 0, output: 0, total: 0 })),
prepareToolExecution: vi.fn((_tool, args) => ({ toolParams: args, executionParams: args })),
prepareToolsWithUsageControl: mockPrepareTools,
sumToolCosts: vi.fn(() => 0),
}))
vi.mock('@/providers/vllm/utils', () => ({
checkForForcedToolUsage: mockCheckForced,
createReadableStreamFromVLLMStream: mockCreateStream,
}))
vi.mock('@/tools', () => ({ executeTool: mockExecuteTool }))
vi.mock('@/stores/providers', () => ({
useProvidersStore: { getState: () => ({ setProviderModels: vi.fn() }) },
}))
import { clearProviderClientCacheForTests } from '@/providers/client-cache'
import type { ProviderToolConfig } from '@/providers/types'
import { vllmProvider } from '@/providers/vllm/index'
interface ToolCall {
id: string
type: 'function'
function: { name: string; arguments: string }
}
function chatResponse(content: string | null, toolCalls?: ToolCall[]) {
return {
choices: [{ message: { content, tool_calls: toolCalls } }],
usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
}
}
function makeTool(id: string): ProviderToolConfig {
return {
id,
name: id,
description: '',
params: {},
parameters: { type: 'object', properties: {}, required: [] },
}
}
const toolCall = (id: string, name: string, args = '{}'): ToolCall => ({
id,
type: 'function',
function: { name, arguments: args },
})
/** Payload passed to the Nth `chat.completions.create` call. */
const createPayload = (callIndex: number) => mockCreate.mock.calls[callIndex][0]
describe('vllmProvider', () => {
beforeEach(() => {
vi.clearAllMocks()
clearProviderClientCacheForTests()
openAIArgs.length = 0
envState.VLLM_BASE_URL = 'http://localhost:8000'
envState.VLLM_API_KEY = undefined
mockPrepareTools.mockReturnValue({
tools: [{ type: 'function', function: { name: 'myTool' } }],
toolChoice: 'auto',
forcedTools: [],
hasFilteredTools: false,
})
mockCheckForced.mockReturnValue({ hasUsedForcedTool: false, usedForcedTools: [] })
mockCreateStream.mockReturnValue(new ReadableStream({ start: (c) => c.close() }))
mockExecuteTool.mockResolvedValue({ success: true, output: { result: 'ok' } })
mockValidateUrlWithDNS.mockResolvedValue({ isValid: true, resolvedIP: '203.0.113.10' })
mockCreatePinnedFetch.mockReturnValue(pinnedFetchFn)
})
describe('endpoint SSRF protection', () => {
it('does not validate or pin when no endpoint is supplied (uses env base URL)', async () => {
mockCreate.mockResolvedValueOnce(chatResponse('hi'))
await vllmProvider.executeRequest({
model: 'vllm/llama-3',
messages: [{ role: 'user', content: 'hi' }],
})
expect(mockValidateUrlWithDNS).not.toHaveBeenCalled()
expect(mockCreatePinnedFetch).not.toHaveBeenCalled()
expect(openAIArgs[0].baseURL).toBe('http://localhost:8000/v1')
expect(openAIArgs[0].fetch).toBeUndefined()
})
it('validates a user-supplied endpoint and pins the connection to the resolved IP', async () => {
mockCreate.mockResolvedValueOnce(chatResponse('hi'))
await vllmProvider.executeRequest({
model: 'vllm/llama-3',
messages: [{ role: 'user', content: 'hi' }],
azureEndpoint: 'https://my-vllm.example.com',
})
expect(mockValidateUrlWithDNS).toHaveBeenCalledWith(
'https://my-vllm.example.com',
'vLLM endpoint',
{ allowHttp: true }
)
expect(mockCreatePinnedFetch).toHaveBeenCalledWith('203.0.113.10')
expect(openAIArgs[0].baseURL).toBe('https://my-vllm.example.com/v1')
expect(openAIArgs[0].fetch).toBe(pinnedFetchFn)
})
it('rejects a user-supplied endpoint that fails SSRF validation without issuing a request', async () => {
mockValidateUrlWithDNS.mockResolvedValueOnce({
isValid: false,
error: 'vLLM endpoint resolves to a blocked IP address',
})
await expect(
vllmProvider.executeRequest({
model: 'vllm/llama-3',
messages: [{ role: 'user', content: 'hi' }],
azureEndpoint: 'http://169.254.169.254',
})
).rejects.toThrow('Invalid vLLM endpoint')
expect(mockCreatePinnedFetch).not.toHaveBeenCalled()
expect(openAIArgs).toHaveLength(0)
expect(mockCreate).not.toHaveBeenCalled()
})
it('rejects a validated endpoint that did not resolve to a pinnable IP', async () => {
mockValidateUrlWithDNS.mockResolvedValueOnce({ isValid: true })
await expect(
vllmProvider.executeRequest({
model: 'vllm/llama-3',
messages: [{ role: 'user', content: 'hi' }],
azureEndpoint: 'https://my-vllm.example.com',
})
).rejects.toThrow('could not resolve a pinnable IP address')
expect(mockCreatePinnedFetch).not.toHaveBeenCalled()
expect(openAIArgs).toHaveLength(0)
expect(mockCreate).not.toHaveBeenCalled()
})
})
it('builds a chat payload with the vllm/ prefix stripped and messages assembled in order', async () => {
mockCreate.mockResolvedValueOnce(chatResponse('hello'))
const result = await vllmProvider.executeRequest({
model: 'vllm/llama-3',
systemPrompt: 'be helpful',
context: 'prior context',
messages: [{ role: 'user', content: 'hi' }],
temperature: 0.7,
maxTokens: 256,
})
const payload = createPayload(0)
expect(payload.model).toBe('llama-3')
expect(payload.temperature).toBe(0.7)
expect(payload.max_completion_tokens).toBe(256)
expect(payload.messages.map((m: { role: string }) => m.role)).toEqual([
'system',
'user',
'user',
])
expect(result.content).toBe('hello')
expect(result.tokens).toEqual({ input: 10, output: 5, total: 15 })
})
it('sends response_format as json_schema with strict when a responseFormat is provided', async () => {
mockCreate.mockResolvedValueOnce(chatResponse('{}'))
await vllmProvider.executeRequest({
model: 'vllm/llama-3',
messages: [{ role: 'user', content: 'hi' }],
responseFormat: { name: 'out', schema: { type: 'object' }, strict: true },
})
expect(createPayload(0).response_format).toEqual({
type: 'json_schema',
json_schema: { name: 'out', schema: { type: 'object' }, strict: true },
})
})
it('strips markdown code fences from structured-output content', async () => {
mockCreate.mockResolvedValueOnce(chatResponse('```json\n{"a":1}\n```'))
const result = await vllmProvider.executeRequest({
model: 'vllm/llama-3',
messages: [{ role: 'user', content: 'hi' }],
responseFormat: { name: 'out', schema: { type: 'object' }, strict: true },
})
expect(result.content).toBe('{"a":1}')
})
it('runs the tool loop: executes tools, appends assistant + tool messages, returns results', async () => {
mockCreate
.mockResolvedValueOnce(chatResponse(null, [toolCall('call_1', 'myTool', '{"x":1}')]))
.mockResolvedValueOnce(chatResponse('final answer'))
const result = await vllmProvider.executeRequest({
model: 'vllm/llama-3',
messages: [{ role: 'user', content: 'use a tool' }],
tools: [makeTool('myTool')],
})
expect(mockExecuteTool).toHaveBeenCalledWith('myTool', { x: 1 }, expect.anything())
const [assistantMessage, toolMessage] = createPayload(1).messages.slice(-2)
expect(assistantMessage).toMatchObject({
role: 'assistant',
content: null,
tool_calls: [{ id: 'call_1', type: 'function', function: { name: 'myTool' } }],
})
expect(toolMessage).toMatchObject({ role: 'tool', tool_call_id: 'call_1' })
expect(toolMessage).not.toHaveProperty('name')
expect(result.content).toBe('final answer')
expect(result.toolCalls).toHaveLength(1)
expect(result.toolCalls?.[0]).toMatchObject({ name: 'myTool', success: true })
expect(result.toolResults).toHaveLength(1)
})
it('records a failed tool result without throwing', async () => {
mockExecuteTool.mockResolvedValueOnce({ success: false, error: 'tool blew up' })
mockCreate
.mockResolvedValueOnce(chatResponse(null, [toolCall('call_1', 'myTool')]))
.mockResolvedValueOnce(chatResponse('done'))
const result = await vllmProvider.executeRequest({
model: 'vllm/llama-3',
messages: [{ role: 'user', content: 'go' }],
tools: [makeTool('myTool')],
})
expect(result.toolCalls?.[0]).toMatchObject({ name: 'myTool', success: false })
const toolMessage = createPayload(1).messages.at(-1)
expect(JSON.parse(toolMessage.content)).toMatchObject({ error: true, tool: 'myTool' })
})
it('surfaces a ProviderError when a follow-up model call fails mid-loop', async () => {
mockCreate
.mockResolvedValueOnce(chatResponse(null, [toolCall('call_1', 'myTool')]))
.mockRejectedValueOnce(new Error('connection reset'))
await expect(
vllmProvider.executeRequest({
model: 'vllm/llama-3',
messages: [{ role: 'user', content: 'go' }],
tools: [makeTool('myTool')],
})
).rejects.toThrow('connection reset')
expect(mockExecuteTool).toHaveBeenCalledTimes(1)
})
it('cycles forced tools: forces the next forced tool after the first is used', async () => {
mockPrepareTools.mockReturnValue({
tools: [{ type: 'function', function: { name: 'toolA' } }],
toolChoice: { type: 'function', function: { name: 'toolA' } },
forcedTools: ['toolA', 'toolB'],
hasFilteredTools: false,
})
mockCheckForced
.mockReturnValueOnce({ hasUsedForcedTool: true, usedForcedTools: ['toolA'] })
.mockReturnValueOnce({ hasUsedForcedTool: true, usedForcedTools: ['toolA', 'toolB'] })
mockCreate
.mockResolvedValueOnce(chatResponse(null, [toolCall('c1', 'toolA')]))
.mockResolvedValueOnce(chatResponse('done'))
await vllmProvider.executeRequest({
model: 'vllm/llama-3',
messages: [{ role: 'user', content: 'go' }],
tools: [makeTool('toolA'), makeTool('toolB')],
})
expect(createPayload(1).tool_choice).toEqual({ type: 'function', function: { name: 'toolB' } })
})
it('streams directly when there are no tools, requesting usage in the stream', async () => {
mockCreate.mockResolvedValueOnce({})
const result = await vllmProvider.executeRequest({
model: 'vllm/llama-3',
messages: [{ role: 'user', content: 'hi' }],
stream: true,
})
expect(mockCreate).toHaveBeenCalledTimes(1)
const payload = createPayload(0)
expect(payload.stream).toBe(true)
expect(payload.stream_options).toEqual({ include_usage: true })
expect('stream' in result && 'execution' in result).toBe(true)
})
it('uses tool_choice "none" on the final streaming call after tool processing', async () => {
mockCreate.mockResolvedValueOnce(chatResponse('answer')).mockResolvedValueOnce({})
await vllmProvider.executeRequest({
model: 'vllm/llama-3',
messages: [{ role: 'user', content: 'hi' }],
stream: true,
tools: [makeTool('myTool')],
})
const streamingPayload = createPayload(1)
expect(streamingPayload.stream).toBe(true)
expect(streamingPayload.tool_choice).toBe('none')
})
it('throws a ProviderError carrying the vLLM error message on API failure', async () => {
mockCreate.mockRejectedValueOnce({
error: { message: 'bad request', type: 'invalid', code: 400 },
})
await expect(
vllmProvider.executeRequest({
model: 'vllm/llama-3',
messages: [{ role: 'user', content: 'hi' }],
})
).rejects.toThrow('bad request')
})
it('throws when no base URL is configured', async () => {
envState.VLLM_BASE_URL = ''
await expect(
vllmProvider.executeRequest({
model: 'vllm/llama-3',
messages: [{ role: 'user', content: 'hi' }],
})
).rejects.toThrow('VLLM_BASE_URL is required')
})
})