Skip to content

Commit 4e4c488

Browse files
committed
Fix auto model
1 parent 4ef7bba commit 4e4c488

6 files changed

Lines changed: 481 additions & 62 deletions

File tree

apps/sim/executor/handlers/evaluator/evaluator-handler.test.ts

Lines changed: 139 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import '@sim/testing/mocks/executor'
33
import { authOAuthUtilsMock, authOAuthUtilsMockFns } from '@sim/testing'
44
import { beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
55

6+
const { mockResolveAutoModel } = vi.hoisted(() => ({
7+
mockResolveAutoModel: vi.fn(),
8+
}))
9+
610
vi.mock('@/app/api/auth/oauth/utils', () => authOAuthUtilsMock)
711

812
vi.mock('@/lib/credentials/access', () => ({
@@ -20,6 +24,13 @@ vi.mock('@/lib/credentials/access', () => ({
2024
}),
2125
}))
2226

27+
vi.mock('@/lib/model-router/resolve', () => ({
28+
addAutoRoutingCost: (cost: Record<string, number>, routingCost: number) =>
29+
routingCost > 0 ? { ...cost, routing: routingCost, total: cost.total + routingCost } : cost,
30+
resolveAutoModel: mockResolveAutoModel,
31+
SIM_AUTO_SYSTEM_PREAMBLE: 'Sim auto system preamble',
32+
}))
33+
2334
import { BlockType } from '@/executor/constants'
2435
import { EvaluatorBlockHandler } from '@/executor/handlers/evaluator/evaluator-handler'
2536
import type { ExecutionContext } from '@/executor/types'
@@ -82,6 +93,12 @@ describe('EvaluatorBlockHandler', () => {
8293
refreshed: false,
8394
})
8495
mockGetProviderFromModel.mockReturnValue('openai')
96+
mockResolveAutoModel.mockResolvedValue({
97+
model: 'fireworks/glm-5.2',
98+
tier: '2',
99+
decidedBy: 'llm',
100+
billableRoutingCost: 0.002,
101+
})
85102

86103
// Set up fetch mock to return a successful response
87104
mockFetch.mockImplementation(() => {
@@ -101,16 +118,27 @@ describe('EvaluatorBlockHandler', () => {
101118

102119
it('should handle evaluator blocks', () => {
103120
expect(handler.canHandle(mockBlock)).toBe(true)
104-
const nonEvalBlock: SerializedBlock = { ...mockBlock, metadata: { id: 'other' } }
121+
const nonEvalBlock: SerializedBlock = {
122+
...mockBlock,
123+
metadata: { id: 'other' },
124+
}
105125
expect(handler.canHandle(nonEvalBlock)).toBe(false)
106126
})
107127

108128
it('should execute evaluator block correctly with basic inputs', async () => {
109129
const inputs = {
110130
content: 'This is the content to evaluate.',
111131
metrics: [
112-
{ name: 'score1', description: 'First score', range: { min: 0, max: 10 } },
113-
{ name: 'score2', description: 'Second score', range: { min: 0, max: 10 } },
132+
{
133+
name: 'score1',
134+
description: 'First score',
135+
range: { min: 0, max: 10 },
136+
},
137+
{
138+
name: 'score2',
139+
description: 'Second score',
140+
range: { min: 0, max: 10 },
141+
},
114142
],
115143
model: 'gpt-4o',
116144
apiKey: 'test-api-key',
@@ -163,6 +191,64 @@ describe('EvaluatorBlockHandler', () => {
163191
})
164192
})
165193

194+
it('resolves sim-auto before executing evaluator and preserves its public identity', async () => {
195+
const inputs = {
196+
content: 'A clear and accurate answer.',
197+
metrics: [
198+
{
199+
name: 'quality',
200+
description: 'Overall answer quality',
201+
range: { min: 1, max: 5 },
202+
},
203+
],
204+
model: 'sim-auto',
205+
}
206+
207+
mockFetch.mockResolvedValueOnce({
208+
ok: true,
209+
json: () =>
210+
Promise.resolve({
211+
content: JSON.stringify({ quality: 5 }),
212+
model: 'fireworks/glm-5.2',
213+
tokens: { input: 80, output: 10, total: 90 },
214+
cost: { input: 0.001, output: 0.0005, total: 0.0015 },
215+
}),
216+
})
217+
218+
const result = await handler.execute(mockContext, mockBlock, inputs)
219+
220+
expect(mockResolveAutoModel).toHaveBeenCalledWith({
221+
ctx: mockContext,
222+
blockId: mockBlock.id,
223+
signals: expect.objectContaining({
224+
lastMessage: inputs.content,
225+
messageCount: 1,
226+
toolNames: [],
227+
mediaKind: 'none',
228+
hasResponseFormat: true,
229+
}),
230+
fallbackModel: 'claude-sonnet-5',
231+
})
232+
expect(mockGetProviderFromModel).toHaveBeenCalledWith('fireworks/glm-5.2')
233+
234+
const requestBody = JSON.parse(mockFetch.mock.calls[0][1].body)
235+
expect(requestBody).toMatchObject({
236+
provider: 'openai',
237+
model: 'fireworks/glm-5.2',
238+
systemPrompt: expect.stringMatching(/^Sim auto system preamble\n\n/),
239+
})
240+
expect(result).toMatchObject({
241+
model: 'sim-auto',
242+
quality: 5,
243+
cost: {
244+
input: 0.001,
245+
output: 0.0005,
246+
routing: 0.002,
247+
total: 0.0035,
248+
},
249+
})
250+
})
251+
166252
it('bills the cost the provider proxy decided rather than recomputing it', async () => {
167253
// The proxy already resolved key provenance and the margin; recomputing
168254
// here would re-charge a BYOK caller the proxy correctly zeroed.
@@ -195,7 +281,13 @@ describe('EvaluatorBlockHandler', () => {
195281
const contentObj = { text: 'Evaluate this JSON.', value: 42 }
196282
const inputs = {
197283
content: JSON.stringify(contentObj),
198-
metrics: [{ name: 'clarity', description: 'Clarity score', range: { min: 1, max: 5 } }],
284+
metrics: [
285+
{
286+
name: 'clarity',
287+
description: 'Clarity score',
288+
range: { min: 1, max: 5 },
289+
},
290+
],
199291
apiKey: 'test-api-key',
200292
}
201293

@@ -227,7 +319,11 @@ describe('EvaluatorBlockHandler', () => {
227319
const inputs = {
228320
content: contentObj,
229321
metrics: [
230-
{ name: 'completeness', description: 'Data completeness', range: { min: 0, max: 1 } },
322+
{
323+
name: 'completeness',
324+
description: 'Data completeness',
325+
range: { min: 0, max: 1 },
326+
},
231327
],
232328
apiKey: 'test-api-key',
233329
}
@@ -258,7 +354,13 @@ describe('EvaluatorBlockHandler', () => {
258354
it('should parse valid JSON response correctly', async () => {
259355
const inputs = {
260356
content: 'Test content',
261-
metrics: [{ name: 'quality', description: 'Quality score', range: { min: 1, max: 10 } }],
357+
metrics: [
358+
{
359+
name: 'quality',
360+
description: 'Quality score',
361+
range: { min: 1, max: 10 },
362+
},
363+
],
262364
apiKey: 'test-api-key',
263365
}
264366

@@ -339,7 +441,13 @@ describe('EvaluatorBlockHandler', () => {
339441
it('should extract metric scores ignoring case', async () => {
340442
const inputs = {
341443
content: 'Test',
342-
metrics: [{ name: 'CamelCaseScore', description: 'Desc', range: { min: 0, max: 10 } }],
444+
metrics: [
445+
{
446+
name: 'CamelCaseScore',
447+
description: 'Desc',
448+
range: { min: 0, max: 10 },
449+
},
450+
],
343451
apiKey: 'test-api-key',
344452
}
345453

@@ -366,8 +474,16 @@ describe('EvaluatorBlockHandler', () => {
366474
const inputs = {
367475
content: 'Test',
368476
metrics: [
369-
{ name: 'presentScore', description: 'Desc1', range: { min: 0, max: 5 } },
370-
{ name: 'missingScore', description: 'Desc2', range: { min: 0, max: 5 } },
477+
{
478+
name: 'presentScore',
479+
description: 'Desc1',
480+
range: { min: 0, max: 5 },
481+
},
482+
{
483+
name: 'missingScore',
484+
description: 'Desc2',
485+
range: { min: 0, max: 5 },
486+
},
371487
],
372488
apiKey: 'test-api-key',
373489
}
@@ -410,7 +526,13 @@ describe('EvaluatorBlockHandler', () => {
410526
it('should handle Azure OpenAI models with endpoint and API version', async () => {
411527
const inputs = {
412528
content: 'Test content to evaluate',
413-
metrics: [{ name: 'quality', description: 'Quality score', range: { min: 1, max: 10 } }],
529+
metrics: [
530+
{
531+
name: 'quality',
532+
description: 'Quality score',
533+
range: { min: 1, max: 10 },
534+
},
535+
],
414536
model: 'gpt-4o',
415537
apiKey: 'test-azure-key',
416538
azureEndpoint: 'https://test.openai.azure.com',
@@ -450,7 +572,13 @@ describe('EvaluatorBlockHandler', () => {
450572
it('should handle Vertex AI models with OAuth credential', async () => {
451573
const inputs = {
452574
content: 'Test content to evaluate',
453-
metrics: [{ name: 'quality', description: 'Quality score', range: { min: 1, max: 10 } }],
575+
metrics: [
576+
{
577+
name: 'quality',
578+
description: 'Quality score',
579+
range: { min: 1, max: 10 },
580+
},
581+
],
454582
model: 'gemini-2.0-flash-exp',
455583
vertexCredential: 'test-vertex-credential-id',
456584
vertexProject: 'test-gcp-project',

apps/sim/executor/handlers/evaluator/evaluator-handler.ts

Lines changed: 57 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11
import { createLogger } from '@sim/logger'
2+
import {
3+
type AutoRoutingResult,
4+
addAutoRoutingCost,
5+
resolveAutoModel,
6+
SIM_AUTO_SYSTEM_PREAMBLE,
7+
} from '@/lib/model-router/resolve'
28
import type { BlockOutput } from '@/blocks/types'
39
import { validateModelProvider } from '@/ee/access-control/utils/permission-check'
410
import { BlockType, DEFAULTS, EVALUATOR } from '@/executor/constants'
@@ -7,6 +13,7 @@ import { buildAPIUrl, buildAuthHeaders, extractAPIErrorMessage } from '@/executo
713
import { isJSONString, parseJSON, stringifyJSON } from '@/executor/utils/json'
814
import { resolveVertexCredential } from '@/executor/utils/vertex-credential'
915
import { resolveProxiedModelCost } from '@/providers/cost-policy'
16+
import { isAutoModel, SIM_AUTO_MODEL_ID } from '@/providers/models'
1017
import { getProviderFromModel } from '@/providers/utils'
1118
import type { SerializedBlock } from '@/serializer/types'
1219

@@ -36,19 +43,6 @@ export class EvaluatorBlockHandler implements BlockHandler {
3643
bedrockRegion: inputs.bedrockRegion,
3744
}
3845

39-
await validateModelProvider(ctx.userId, ctx.workspaceId, evaluatorConfig.model, ctx)
40-
41-
const providerId = getProviderFromModel(evaluatorConfig.model)
42-
43-
let finalApiKey: string | undefined = evaluatorConfig.apiKey
44-
if (providerId === 'vertex' && evaluatorConfig.vertexCredential) {
45-
finalApiKey = await resolveVertexCredential(
46-
evaluatorConfig.vertexCredential,
47-
ctx.userId,
48-
'vertex-evaluator'
49-
)
50-
}
51-
5246
const processedContent = this.processContent(inputs.content)
5347

5448
let systemPromptObj: { systemPrompt: string; responseFormat: any } = {
@@ -105,12 +99,55 @@ export class EvaluatorBlockHandler implements BlockHandler {
10599
'Evaluate the content and provide scores for each metric as JSON.'
106100
}
107101

102+
let model = evaluatorConfig.model
103+
let autoRouting: AutoRoutingResult | null = null
104+
if (isAutoModel(model)) {
105+
autoRouting = await resolveAutoModel({
106+
ctx,
107+
blockId: block.id,
108+
signals: {
109+
systemPrompt: systemPromptObj.systemPrompt,
110+
lastMessage: processedContent,
111+
messageCount: 1,
112+
toolNames: [],
113+
mediaKind: 'none',
114+
hasResponseFormat: true,
115+
approxInputTokens: Math.ceil(
116+
(systemPromptObj.systemPrompt.length + processedContent.length) / 4
117+
),
118+
},
119+
fallbackModel: EVALUATOR.DEFAULT_MODEL,
120+
})
121+
model = autoRouting.model
122+
systemPromptObj.systemPrompt = [SIM_AUTO_SYSTEM_PREAMBLE, systemPromptObj.systemPrompt]
123+
.filter(Boolean)
124+
.join('\n\n')
125+
logger.info('Resolved sim-auto model for evaluator', {
126+
blockId: block.id,
127+
model,
128+
tier: autoRouting.tier,
129+
decidedBy: autoRouting.decidedBy,
130+
})
131+
}
132+
133+
await validateModelProvider(ctx.userId, ctx.workspaceId, model, ctx)
134+
const providerId = getProviderFromModel(model)
135+
136+
let finalApiKey: string | undefined = evaluatorConfig.apiKey
137+
if (providerId === 'vertex' && evaluatorConfig.vertexCredential) {
138+
finalApiKey = await resolveVertexCredential(
139+
evaluatorConfig.vertexCredential,
140+
ctx.userId,
141+
'vertex-evaluator'
142+
)
143+
}
144+
108145
try {
109146
const url = buildAPIUrl('/api/providers', ctx.userId ? { userId: ctx.userId } : {})
110147

111148
const providerRequest: Record<string, any> = {
112149
provider: providerId,
113-
model: evaluatorConfig.model,
150+
model,
114151
systemPrompt: systemPromptObj.systemPrompt,
115152
responseFormat: systemPromptObj.responseFormat,
116153
context: stringifyJSON([
@@ -155,11 +192,14 @@ export class EvaluatorBlockHandler implements BlockHandler {
155192
const outputTokens =
156193
result.tokens?.output || result.tokens?.completion || DEFAULTS.TOKENS.COMPLETION
157194

158-
const cost = resolveProxiedModelCost(result.cost)
195+
const cost = addAutoRoutingCost(
196+
resolveProxiedModelCost(result.cost),
197+
autoRouting?.billableRoutingCost ?? 0
198+
)
159199

160200
return {
161201
content: inputs.content,
162-
model: result.model,
202+
model: autoRouting ? SIM_AUTO_MODEL_ID : result.model,
163203
tokens: {
164204
input: inputTokens,
165205
output: outputTokens,
@@ -169,6 +209,7 @@ export class EvaluatorBlockHandler implements BlockHandler {
169209
input: cost.input,
170210
output: cost.output,
171211
total: cost.total,
212+
...(cost.routing === undefined ? {} : { routing: cost.routing }),
172213
},
173214
...metricScores,
174215
}

0 commit comments

Comments
 (0)