Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/desktop/build/generated-icon.icon/Assets/border.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 33 additions & 0 deletions apps/desktop/build/generated-icon.icon/icon.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"fill": {
"solid": "srgb:1.00000,1.00000,1.00000,1.00000"
},
"groups": [
{
"layers": [
{
"image-name": "logo.png",
"is-glass": false,
"name": "Sim"
},
{
"image-name": "border.svg",
"is-glass": false,
"name": "Dev Border"
}
],
"shadow": {
"kind": "neutral",
"opacity": 0
},
"specular": false,
"translucency": {
"enabled": false,
"value": 0
}
}
],
"supported-platforms": {
"squares": ["macOS"]
}
}
6 changes: 5 additions & 1 deletion apps/sim/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,11 @@ API_ENCRYPTION_KEY=your_api_encryption_key # Use `openssl rand -hex 32` to gener
# VLLM_API_KEY= # Optional bearer token if your vLLM instance requires auth
# LITELLM_BASE_URL=http://localhost:4000 # Base URL for your LiteLLM proxy (OpenAI-compatible)
# LITELLM_API_KEY= # Optional bearer token if your LiteLLM proxy requires auth
# FIREWORKS_API_KEY= # Optional Fireworks AI API key for model listing
# NEXT_PUBLIC_FORCE_HOSTED=true # Dev only: treat this instance as hosted Sim (sim-auto pool, platform keys); ignored in production builds
# FIREWORKS_API_KEY= # Optional Fireworks AI API key for model listing and inference
# FIREWORKS_API_KEY_1= # Optional Fireworks API key for rotation (hosted deployments)
# FIREWORKS_API_KEY_2= # Additional Fireworks API key for load balancing
# FIREWORKS_API_KEY_3= # Additional Fireworks API key for load balancing
# NEXT_PUBLIC_BEDROCK_DEFAULT_CREDENTIALS=true # Set when using AWS default credential chain (IAM roles, ECS task roles, IRSA). Hides credential fields in Agent block UI.
# AZURE_OPENAI_ENDPOINT= # Azure OpenAI endpoint (hides field in UI when set alongside NEXT_PUBLIC_AZURE_CONFIGURED)
# AZURE_OPENAI_API_KEY= # Azure OpenAI API key
Expand Down
9 changes: 8 additions & 1 deletion apps/sim/app/api/providers/fireworks/models/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { validationErrorResponse } from '@/lib/api/server'
import { getBYOKKey } from '@/lib/api-key/byok'
import { getSession } from '@/lib/auth'
import { env } from '@/lib/core/config/env'
import { isHosted } from '@/lib/core/config/env-flags'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
import { filterBlacklistedModels, isProviderBlacklisted } from '@/providers/utils'
Expand Down Expand Up @@ -54,7 +55,13 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
}
}

if (!apiKey) {
/**
* On hosted Sim the platform key is the sim-auto pool's inference key, not a
* workspace credential: enumerating the whole Fireworks catalog from it would
* offer every serverless model as selectable when no key can actually run it
* (`getApiKeyWithBYOK` serves the platform key to catalog models only).
*/
if (!apiKey && !isHosted) {
apiKey = env.FIREWORKS_API_KEY
}

Expand Down
9 changes: 8 additions & 1 deletion apps/sim/blocks/blocks/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
getReasoningEffortValuesForModel,
getThinkingLevelsForModel,
getVerbosityValuesForModel,
isAutoModel,
supportsTemperature,
} from '@/providers/models'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
Expand Down Expand Up @@ -522,7 +523,13 @@ Return ONLY the JSON array.`,
if (!model) {
throw new Error('No model selected')
}
const tool = getBaseModelProviders()[model]
// sim-auto resolves to a concrete pool model at execution time, where
// the agent handler derives the provider from the resolved model and
// never reads this serialized value. Serialization still needs the
// same provider-id shape every other model stores, so look up the
// runtime fallback model's provider.
const lookupModel = isAutoModel(model) ? 'claude-sonnet-5' : model
const tool = getBaseModelProviders()[lookupModel]
if (!tool) {
throw new Error(`Invalid model selected: ${model}`)
}
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/blocks/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ vi.mock('@/providers/models', () => ({
getProviderModels: mockGetProviderModels,
getProviderIcon: mockGetProviderIcon,
getBaseModelProviders: mockGetBaseModelProviders,
SIM_AUTO_MODEL_ID: 'sim-auto',
isAutoModel: (model: string) => model.trim().toLowerCase() === 'sim-auto',
}))

vi.mock('@/providers/utils', () => ({
Expand Down
19 changes: 18 additions & 1 deletion apps/sim/blocks/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { toError } from '@sim/utils/errors'
import { SimAutoIcon } from '@/components/icons'
import {
isAzureConfigured,
isCohereConfigured,
Expand All @@ -14,7 +15,9 @@ import {
getModelSunsetStatus,
getProviderIcon,
getProviderModels,
isAutoModel,
orderModelIdsByReleaseDate,
SIM_AUTO_MODEL_ID,
} from '@/providers/models'
import { isPiSupportedModel } from '@/providers/pi-providers'
import { getProviderFromModel } from '@/providers/utils'
Expand Down Expand Up @@ -75,12 +78,21 @@ export function getModelOptions() {
])
)

return allModels
const options = allModels
.filter((model) => getModelSunsetStatus(model) !== 'deprecated')
.map((model) => {
const icon = getProviderIcon(model)
return { label: model, id: model, ...(icon && { icon }) }
})

// Hosted-only automatic model. Deliberately LAST in the list (limited
// visibility for the initial release): available to anyone who scrolls or
// searches for it, but never the first thing the dropdown offers.
if (isHosted) {
options.push({ label: 'Auto', id: SIM_AUTO_MODEL_ID, icon: SimAutoIcon })
}

return options
}

/**
Expand Down Expand Up @@ -183,6 +195,11 @@ function shouldRequireApiKeyForModel(model: string): boolean {
const normalizedModel = model.trim().toLowerCase()
if (!normalizedModel) return false

// On hosted Sim the auto pseudo-model resolves server-side to a hosted pool
// model. On self-hosted it exists only via imported workflows and always
// falls back to the default Anthropic model, so the key field must show.
if (isAutoModel(normalizedModel)) return !isHosted

if (isHosted) {
const hostedModels = getHostedModels()
if (hostedModels.some((m) => m.toLowerCase() === normalizedModel)) return false
Expand Down
25 changes: 25 additions & 0 deletions apps/sim/components/icons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7558,6 +7558,31 @@ export function SixtyfourIcon(props: SVGProps<SVGSVGElement>) {
)
}

/**
* The "sim" brand wordmark (v1.0 brand guide simLogotype paths — the same mark
* the navbar/login header renders), inked with the theme-adaptive
* `--text-body`. Used as the icon for the Auto model option; the wide viewBox
* letterboxes itself inside square icon slots.
*/
export function SimAutoIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
viewBox='0 0 441 212'
fill='none'
xmlns='http://www.w3.org/2000/svg'
aria-hidden='true'
>
<g fill='var(--text-body)'>
<path d='M0 160.9H29.51C29.51 169.08 32.46 175.61 38.37 180.48C44.27 185.12 52.25 187.44 62.31 187.44C73.24 187.44 81.65 185.34 87.56 181.14C93.46 176.71 96.41 170.85 96.41 163.55C96.41 158.24 94.77 153.82 91.49 150.28C88.43 146.74 82.75 143.86 74.44 141.65L46.24 135.01C32.03 131.47 21.42 126.05 14.43 118.75C7.65 111.45 4.26 101.83 4.26 89.88C4.26 79.93 6.78 71.3 11.81 64C17.05 56.7 24.16 51.06 33.12 47.08C42.3 43.09 52.8 41.1 64.6 41.1C76.41 41.1 86.57 43.2 95.1 47.41C103.84 51.61 110.62 57.47 115.43 64.99C120.46 72.52 123.08 81.48 123.3 91.87H93.79C93.57 83.47 90.84 76.94 85.59 72.3C80.34 67.65 73.02 65.33 63.62 65.33C54 65.33 46.57 67.43 41.32 71.63C36.07 75.83 33.45 81.59 33.45 88.89C33.45 99.73 41.32 107.14 57.06 111.12L85.26 118.09C98.81 121.19 108.98 126.28 115.76 133.35C122.53 140.21 125.92 149.61 125.92 161.56C125.92 171.74 123.19 180.7 117.73 188.44C112.26 195.96 104.72 201.82 95.1 206.03C85.7 210.01 74.55 212 61.65 212C42.85 212 27.87 207.35 16.72 198.06C5.57 188.77 0 176.38 0 160.9Z' />
<path d='M232.8 212H202.13L202.13 49.76H229.54V77.39C232.8 68.34 239.11 60.66 247.81 54.7C256.73 48.52 267.5 45.43 280.12 45.43C294.26 45.43 306.01 49.29 315.36 57.02C324.72 64.75 330.81 75.01 333.64 87.82H328.09C330.27 75.01 336.25 64.75 346.04 57.02C355.83 49.29 367.9 45.43 382.26 45.43C400.54 45.43 414.89 50.84 425.34 61.66C435.78 72.47 441 87.26 441 106.03V212H410.98V113.65C410.98 100.84 407.71 91.02 401.19 84.17C394.88 77.11 386.29 73.58 375.41 73.58C367.79 73.58 361.05 75.34 355.17 78.88C349.52 82.19 345.06 87.04 341.8 93.45C338.53 99.85 336.9 107.36 336.9 115.97V212H306.55V113.32C306.55 100.51 303.4 90.8 297.09 84.17C290.78 77.33 282.19 73.91 271.31 73.91C263.69 73.91 256.95 75.67 251.08 79.21C245.42 82.52 240.96 87.38 237.7 93.78C234.43 99.96 232.8 107.36 232.8 115.97V212Z' />
<path d='M184.83 20.55C184.83 31.9 175.64 41.1 164.29 41.1C152.95 41.1 143.76 31.9 143.76 20.55C143.76 9.2 152.95 0 164.29 0C175.64 0 184.83 9.2 184.83 20.55Z' />
<path d='M179.43 212H149.16V49.76C153.76 51.91 158.88 53.12 164.29 53.12C169.7 53.12 174.83 51.91 179.43 49.76V212Z' />
</g>
</svg>
)
}

export function SimTriggerIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
Expand Down
125 changes: 124 additions & 1 deletion apps/sim/executor/handlers/agent/agent-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,12 @@ import {
vi,
} from 'vitest'
import { getAllBlocks } from '@/blocks'
import { BlockType, isMcpTool } from '@/executor/constants'
import { AGENT, BlockType, isMcpTool } from '@/executor/constants'
import { AgentBlockHandler } from '@/executor/handlers/agent/agent-handler'
import type { ExecutionContext, StreamingExecution } from '@/executor/types'
import { executeProviderRequest } from '@/providers'
import { installStreamingCostPolicy } from '@/providers/cost-policy'
import { SIM_AUTO_MODEL_ID } from '@/providers/models'
import { getProviderFromModel, transformBlockTool } from '@/providers/utils'
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
import { executeTool } from '@/tools'
Expand Down Expand Up @@ -275,6 +277,127 @@ describe('AgentBlockHandler', () => {
expect(result).toEqual(expectedOutput)
})

it('reports a sim-auto run under the sim-auto identity, not the model that served it', async () => {
mockExecuteProviderRequest.mockResolvedValue({
content: 'Mocked response content',
model: AGENT.DEFAULT_MODEL,
tokens: { input: 10, output: 20, total: 30 },
toolCalls: [],
cost: { input: 0.001, output: 0.002, total: 0.003 },
timing: {
total: 100,
timeSegments: [
{ type: 'model', name: AGENT.DEFAULT_MODEL, provider: 'anthropic', duration: 100 },
],
},
})

const result = (await handler.execute(mockContext, mockBlock, {
model: SIM_AUTO_MODEL_ID,
userPrompt: 'Hello!',
})) as {
model: string
cost: unknown
tokens: unknown
providerTiming: { timeSegments: Array<{ name?: string; provider?: string }> }
}

expect(result.model).toBe(SIM_AUTO_MODEL_ID)
expect(result.providerTiming.timeSegments[0].name).toBe(SIM_AUTO_MODEL_ID)
expect(result.providerTiming.timeSegments[0].provider).toBeUndefined()
// Only the label changes: tokens and the already-priced cost are untouched.
expect(result.tokens).toEqual({ input: 10, output: 20, total: 30 })
expect(result.cost).toEqual({ input: 0.001, output: 0.002, total: 0.003 })
})

/** Reaches the private signal builder; routing depends on nothing else. */
const buildAutoRoutingSignalsFor = (inputs: Record<string, unknown>) =>
(
handler as unknown as {
buildAutoRoutingSignals: (i: unknown, rf: unknown) => { mediaKind: string }
}
).buildAutoRoutingSignals(inputs, undefined)

const png = { id: 'f1', type: 'image/png' }
const pdf = { id: 'f2', type: 'application/pdf' }

it('reports no media when neither the files input nor any message carries one', async () => {
const signals = buildAutoRoutingSignalsFor({
messages: [{ role: 'user' as const, content: 'Summarize this text' }],
})

expect(signals.mediaKind).toBe('none')
})

it('detects media carried on inbound messages, not just the files input', async () => {
const signals = buildAutoRoutingSignalsFor({
messages: [{ role: 'user' as const, content: 'What is in this image?', files: [png] }],
})

expect(signals.mediaKind).toBe('image')
})

it('classifies an all-image attachment set as image', async () => {
expect(buildAutoRoutingSignalsFor({ files: [png, png] }).mediaKind).toBe('image')
})

it('classifies a mixed image + document set as file', async () => {
expect(buildAutoRoutingSignalsFor({ files: [png, pdf] }).mediaKind).toBe('file')
})

it('treats an unknown MIME type as file rather than assuming it is an image', async () => {
expect(buildAutoRoutingSignalsFor({ files: [{ id: 'f3' }] }).mediaKind).toBe('file')
})

it('overlays the routing charge on a streaming cost written after the fact', async () => {
// Mirrors the real streaming shape: the policy accessor is installed at
// provider-return time, the drain writes the final cost long after the
// handler returned, and consumers read it at log time.
const output: Record<string, unknown> = { cost: { input: 0, output: 0, total: 0 } }
installStreamingCostPolicy(output as never, { billable: true, multiplier: 1 })
const streaming = { stream: new ReadableStream(), execution: { output } }

;(
handler as unknown as { applyRoutingCost: (r: unknown, c: number) => void }
).applyRoutingCost(streaming, 0.002)

// The drain settles the model cost afterwards.

;(output as { cost: unknown }).cost = { input: 0.01, output: 0.02, total: 0.03 }

expect(output.cost).toEqual({
input: 0.01,
output: 0.02,
total: expect.closeTo(0.032, 10),
routing: 0.002,
})
})

it('adds the routing charge to a settled non-streaming cost', async () => {
const result: Record<string, unknown> = { cost: { input: 0.01, output: 0.02, total: 0.03 } }

;(
handler as unknown as { applyRoutingCost: (r: unknown, c: number) => void }
).applyRoutingCost(result, 0.002)

expect(result.cost).toEqual({
input: 0.01,
output: 0.02,
total: expect.closeTo(0.032, 10),
routing: 0.002,
})
})

it('leaves the reported model alone for an explicitly selected model', async () => {
const result = (await handler.execute(mockContext, mockBlock, {
model: 'gpt-4o',
userPrompt: 'Hello!',
apiKey: 'test-api-key',
})) as { model: string }

expect(result.model).toBe('mock-model')
})

it('should attach files to the last user message only', async () => {
const inputs = {
model: 'gpt-4o',
Expand Down
Loading
Loading