@@ -8,12 +8,17 @@ import { and, eq, inArray, isNull } from 'drizzle-orm'
88import { normalizeStringRecord , normalizeWorkflowVariables } from '@/lib/core/utils/records'
99import { createMcpToolId } from '@/lib/mcp/utils'
1010import {
11+ type AutoMediaKind ,
1112 type AutoRoutingResult ,
1213 type AutoRoutingSignals ,
1314 resolveAutoModel ,
1415 SIM_AUTO_SYSTEM_PREAMBLE ,
1516} from '@/lib/model-router/resolve'
16- import { processFilesToUserFiles , type RawFileInput } from '@/lib/uploads/utils/file-utils'
17+ import {
18+ MODEL_SUPPORTED_IMAGE_MIME_TYPES ,
19+ processFilesToUserFiles ,
20+ type RawFileInput ,
21+ } from '@/lib/uploads/utils/file-utils'
1722import { hydrateUserFilesWithBase64 } from '@/lib/uploads/utils/user-file-base64.server'
1823import { resolveCustomBlockToolBinding } from '@/lib/workflows/custom-blocks/operations'
1924import { getCustomToolById } from '@/lib/workflows/custom-tools/operations'
@@ -52,7 +57,7 @@ import {
5257 shouldUseLargeFilePath ,
5358 supportsFileAttachments ,
5459} from '@/providers/attachments'
55- import { isAutoModel } from '@/providers/models'
60+ import { isAutoModel , SIM_AUTO_MODEL_ID } from '@/providers/models'
5661import { getProviderFromModel , transformBlockTool } from '@/providers/utils'
5762import type { SerializedBlock } from '@/serializer/types'
5863import { filterSchemaForLLM , type ToolSchema } from '@/tools/params'
@@ -166,6 +171,10 @@ export class AgentBlockHandler implements BlockHandler {
166171 this . applyRoutingCost ( result as BlockOutput , autoRouting . billableRoutingCost )
167172 }
168173
174+ if ( autoRouting ) {
175+ this . applyAutoModelLabel ( result , model )
176+ }
177+
169178 if ( this . isStreamingExecution ( result ) ) {
170179 if ( filteredInputs . memoryType && filteredInputs . memoryType !== 'none' ) {
171180 return this . wrapStreamForMemoryPersistence (
@@ -208,14 +217,77 @@ export class AgentBlockHandler implements BlockHandler {
208217 lastMessage,
209218 messageCount : ( inputs . messages ?. length ?? 0 ) + ( inputs . userPrompt ? 1 : 0 ) ,
210219 toolNames : ( inputs . tools ?? [ ] ) . map ( ( t ) => t . title || t . type || 'tool' ) ,
211- hasAttachments : Array . isArray ( normalizedFiles )
212- ? normalizedFiles . length > 0
213- : Boolean ( normalizedFiles ) ,
220+ mediaKind : this . resolveMediaKind ( inputs , normalizedFiles ) ,
214221 hasResponseFormat : Boolean ( responseFormat ) ,
215222 approxInputTokens : Math . ceil ( approxChars / 4 ) ,
216223 }
217224 }
218225
226+ /**
227+ * Classifies what the block attaches, which decides the sim-auto pool column.
228+ *
229+ * Media reaches the provider by two routes — the block's `files` input and
230+ * files already carried on inbound messages (chat deployments, memory, an
231+ * upstream block feeding `messages`) — and both count. A file whose MIME type
232+ * is missing or unrecognized counts as `file`, the column served by the
233+ * providers that accept the most input types, because the alternative is
234+ * handing a document to a model whose API models no such content part.
235+ */
236+ private resolveMediaKind ( inputs : AgentInputs , normalizedFiles : unknown ) : AutoMediaKind {
237+ const attached : Array < { type ?: string } > = [
238+ ...( Array . isArray ( normalizedFiles ) ? ( normalizedFiles as Array < { type ?: string } > ) : [ ] ) ,
239+ ...( inputs . messages ?? [ ] ) . flatMap ( ( message ) => message . files ?? [ ] ) ,
240+ ]
241+
242+ if ( attached . length === 0 ) return 'none'
243+
244+ return attached . every ( ( file ) =>
245+ MODEL_SUPPORTED_IMAGE_MIME_TYPES . has ( ( file . type ?? '' ) . toLowerCase ( ) )
246+ )
247+ ? 'image'
248+ : 'file'
249+ }
250+
251+ /**
252+ * Reports a completed auto run under the `sim-auto` identity everywhere the
253+ * run is observed — the block output, the trace span, and the usage-ledger
254+ * row keyed on the model name — so the pool model that served the request
255+ * stays an implementation detail (matching the block's configured model and
256+ * the hidden identity preamble the models themselves run under).
257+ *
258+ * Runs after `executeProviderRequest`, whose billability gate and pricing
259+ * key on the concrete pool model: tokens and cost are already settled here,
260+ * only the label changes.
261+ */
262+ private applyAutoModelLabel (
263+ result : BlockOutput | StreamingExecution ,
264+ resolvedModel : string
265+ ) : void {
266+ const output = this . isStreamingExecution ( result )
267+ ? ( result as StreamingExecution ) . execution ?. output
268+ : ( result as BlockOutput )
269+ if ( ! output || typeof output !== 'object' ) return
270+
271+ const target = output as {
272+ model ?: string
273+ providerTiming ?: {
274+ timeSegments ?: Array < { type ?: string ; name ?: string ; provider ?: string } >
275+ }
276+ }
277+ target . model = SIM_AUTO_MODEL_ID
278+
279+ // Model segments name themselves after the model (every provider does) and
280+ // carry the serving provider, which the log detail renders as that
281+ // provider's icon — the same leak by two other routes.
282+ for ( const segment of target . providerTiming ?. timeSegments ?? [ ] ) {
283+ if ( segment . type !== 'model' ) continue
284+ if ( segment . name ?. toLowerCase ( ) === resolvedModel . toLowerCase ( ) ) {
285+ segment . name = SIM_AUTO_MODEL_ID
286+ }
287+ segment . provider = undefined
288+ }
289+ }
290+
219291 /**
220292 * Adds the billable sim-auto routing charge to a non-streaming output's
221293 * cost breakdown as a distinct `routing` component.
0 commit comments