Skip to content

Commit 9e55270

Browse files
committed
updates
1 parent 21cb3c3 commit 9e55270

13 files changed

Lines changed: 486 additions & 19 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1193,6 +1193,7 @@ export class AgentBlockHandler implements BlockHandler {
11931193
: ('catalog' as const),
11941194
credentialMode: customModelConfig?.credentials.mode,
11951195
providerOptions: customModelConfig?.providerOptions,
1196+
providerModel: customModelConfig?.deployment,
11961197
previousInteractionId: inputs.previousInteractionId,
11971198
/** Agent-events remains the opt-in for exposing thinking and tool lifecycle events. */
11981199
agentEvents: streaming && ctx.metadata?.agentEvents === true,

apps/sim/lib/copilot/tools/server/workflow/edit-workflow/operations.test.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ vi.mock('@/blocks/registry', () => ({
1717
subBlocks: [
1818
{ id: 'systemPrompt', type: 'long-input' },
1919
{ id: 'model', type: 'combobox' },
20+
{ id: 'customModelConfig', type: 'code' },
2021
],
2122
},
2223
{
@@ -41,6 +42,7 @@ vi.mock('@/blocks/registry', () => ({
4142
subBlocks: [
4243
{ id: 'systemPrompt', type: 'long-input' },
4344
{ id: 'model', type: 'combobox' },
45+
{ id: 'customModelConfig', type: 'code' },
4446
],
4547
},
4648
function: {
@@ -557,3 +559,126 @@ describe('forward-reference connections (pending resolution)', () => {
557559
expect(state.blocks[BLOCK_A].data?.pendingConnections).toBeUndefined()
558560
})
559561
})
562+
563+
describe('custom model credential round trips', () => {
564+
const existingSecret = 'sk-existing-custom-secret'
565+
566+
function makeCustomAgentWorkflow(includeConfig = true) {
567+
const customModelConfig = JSON.stringify({
568+
provider: 'openai',
569+
model: 'gpt-5.6-terra',
570+
parameters: { reasoningEffort: 'medium', temperature: 0.2 },
571+
credentials: { mode: 'explicit', apiKey: existingSecret },
572+
providerOptions: {},
573+
})
574+
575+
return {
576+
blocks: {
577+
'agent-1': {
578+
id: 'agent-1',
579+
type: 'agent',
580+
name: 'Agent 1',
581+
position: { x: 0, y: 0 },
582+
enabled: true,
583+
subBlocks: {
584+
model: { id: 'model', type: 'combobox', value: 'sim-custom' },
585+
...(includeConfig
586+
? {
587+
customModelConfig: {
588+
id: 'customModelConfig',
589+
type: 'code',
590+
value: customModelConfig,
591+
},
592+
}
593+
: {}),
594+
},
595+
outputs: {},
596+
data: {},
597+
},
598+
},
599+
edges: [] as any[],
600+
loops: {},
601+
parallels: {},
602+
}
603+
}
604+
605+
it('restores the existing literal key when an edit round-trips the VFS placeholder', () => {
606+
const result = applyOperationsToWorkflowState(makeCustomAgentWorkflow(), [
607+
{
608+
operation_type: 'edit',
609+
block_id: 'agent-1',
610+
params: {
611+
inputs: {
612+
customModelConfig: {
613+
provider: 'openai',
614+
model: 'gpt-5.6-sol',
615+
parameters: { reasoningEffort: 'high', temperature: 0.1 },
616+
credentials: { mode: 'explicit', apiKey: '<redacted>' },
617+
providerOptions: {},
618+
},
619+
},
620+
},
621+
},
622+
])
623+
624+
expect(result.validationErrors).toHaveLength(0)
625+
const stored = JSON.parse(
626+
result.state.blocks['agent-1'].subBlocks.customModelConfig.value as string
627+
)
628+
expect(stored).toMatchObject({
629+
provider: 'openai',
630+
model: 'gpt-5.6-sol',
631+
parameters: { reasoningEffort: 'high', temperature: 0.1 },
632+
credentials: { mode: 'explicit', apiKey: existingSecret },
633+
})
634+
})
635+
636+
it('rejects the placeholder on a provider change without overwriting the stored config', () => {
637+
const workflow = makeCustomAgentWorkflow()
638+
const original = workflow.blocks['agent-1'].subBlocks.customModelConfig.value
639+
const result = applyOperationsToWorkflowState(workflow, [
640+
{
641+
operation_type: 'edit',
642+
block_id: 'agent-1',
643+
params: {
644+
inputs: {
645+
customModelConfig: {
646+
provider: 'xai',
647+
model: 'grok-4.5',
648+
credentials: { mode: 'explicit', apiKey: '<redacted>' },
649+
providerOptions: {},
650+
},
651+
},
652+
},
653+
},
654+
])
655+
656+
expect(result.validationErrors).toHaveLength(1)
657+
expect(result.validationErrors[0]?.error).toContain('when changing providers')
658+
expect(result.state.blocks['agent-1'].subBlocks.customModelConfig.value).toBe(original)
659+
expect(JSON.stringify(result.validationErrors)).not.toContain(existingSecret)
660+
})
661+
662+
it('rejects the placeholder when the existing block has no key to preserve', () => {
663+
const result = applyOperationsToWorkflowState(makeCustomAgentWorkflow(false), [
664+
{
665+
operation_type: 'edit',
666+
block_id: 'agent-1',
667+
params: {
668+
inputs: {
669+
customModelConfig: {
670+
provider: 'openai',
671+
model: 'gpt-5.6-terra',
672+
credentials: { mode: 'explicit', apiKey: '<redacted>' },
673+
providerOptions: {},
674+
},
675+
},
676+
},
677+
},
678+
])
679+
680+
expect(result.validationErrors).toHaveLength(1)
681+
expect(result.validationErrors[0]?.error).toContain('without an existing stored key')
682+
expect(result.state.blocks['agent-1'].subBlocks).not.toHaveProperty('customModelConfig')
683+
})
684+
})

apps/sim/lib/copilot/tools/server/workflow/edit-workflow/operations.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,8 @@ function mergeNestedNodesForParent(
201201
const childValidation = validateInputsForBlock(
202202
existingBlock.type,
203203
childBlock.inputs,
204-
existingId
204+
existingId,
205+
{ existingSubBlocks: existingBlock.subBlocks }
205206
)
206207
validationErrors.push(...childValidation.errors)
207208

@@ -426,7 +427,9 @@ export function handleEditOperation(op: EditWorkflowOperation, ctx: OperationCon
426427
if (!block.subBlocks) block.subBlocks = {}
427428

428429
// Validate inputs against block configuration
429-
const validationResult = validateInputsForBlock(block.type, params.inputs, block_id)
430+
const validationResult = validateInputsForBlock(block.type, params.inputs, block_id, {
431+
existingSubBlocks: block.subBlocks,
432+
})
430433
validationErrors.push(...validationResult.errors)
431434

432435
Object.entries(validationResult.validInputs).forEach(([inputKey, value]) => {
@@ -898,7 +901,9 @@ export function handleInsertIntoSubflowOperation(
898901
// Update inputs if provided (with validation)
899902
if (params.inputs) {
900903
// Validate inputs against block configuration
901-
const validationResult = validateInputsForBlock(existingBlock.type, params.inputs, block_id)
904+
const validationResult = validateInputsForBlock(existingBlock.type, params.inputs, block_id, {
905+
existingSubBlocks: existingBlock.subBlocks,
906+
})
902907
validationErrors.push(...validationResult.errors)
903908

904909
Object.entries(validationResult.validInputs).forEach(([key, value]) => {

apps/sim/lib/copilot/tools/server/workflow/edit-workflow/validation.ts

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,11 @@ import { SELECTOR_TYPES } from './types'
3838

3939
const validationLogger = createLogger('EditWorkflowValidation')
4040
const agentToolLintLogger = createLogger('EditWorkflowAgentToolLint')
41+
const REDACTED_CUSTOM_MODEL_API_KEY = '<redacted>'
42+
43+
interface ValidateInputsForBlockOptions {
44+
existingSubBlocks?: Record<string, { value?: unknown } | undefined>
45+
}
4146

4247
/**
4348
* Detect privileged Agent custom-model writes anywhere in an edit operation,
@@ -78,7 +83,8 @@ export function findBlockWithDuplicateNormalizedName(
7883
export function validateInputsForBlock(
7984
blockType: string,
8085
inputs: Record<string, any>,
81-
blockId: string
86+
blockId: string,
87+
options?: ValidateInputsForBlockOptions
8288
): ValidationResult {
8389
const errors: ValidationError[] = []
8490

@@ -173,7 +179,8 @@ export function validateInputsForBlock(
173179
value,
174180
key,
175181
blockType,
176-
blockId
182+
blockId,
183+
options?.existingSubBlocks?.[key]?.value
177184
)
178185
if (validationResult.valid) {
179186
validatedInputs[key] = validationResult.value
@@ -302,7 +309,8 @@ export function validateValueForSubBlockType(
302309
value: any,
303310
fieldName: string,
304311
blockType: string,
305-
blockId: string
312+
blockId: string,
313+
existingValue?: unknown
306314
): ValueValidationResult {
307315
const { type } = subBlockConfig
308316

@@ -533,6 +541,38 @@ export function validateValueForSubBlockType(
533541
// subblock persists text. Accept the object Mothership naturally
534542
// produces, validate it now, and store one canonical JSON string.
535543
const parsed = parseCustomModelConfig(value)
544+
545+
if (parsed.credentials.apiKey === REDACTED_CUSTOM_MODEL_API_KEY) {
546+
if (existingValue === undefined || existingValue === null) {
547+
throw new Error(
548+
'credentials.apiKey cannot be "<redacted>" without an existing stored key; provide an environment-variable reference or a new key'
549+
)
550+
}
551+
552+
const existing = parseCustomModelConfig(existingValue)
553+
if (existing.provider !== parsed.provider) {
554+
throw new Error(
555+
'credentials.apiKey cannot remain "<redacted>" when changing providers; provide the new provider key or an environment-variable reference'
556+
)
557+
}
558+
559+
const existingApiKey = existing.credentials.apiKey
560+
if (
561+
existing.credentials.mode !== 'explicit' ||
562+
!existingApiKey ||
563+
existingApiKey === REDACTED_CUSTOM_MODEL_API_KEY
564+
) {
565+
throw new Error(
566+
'credentials.apiKey cannot be "<redacted>" because no usable stored key exists; provide an environment-variable reference or a new key'
567+
)
568+
}
569+
570+
// `<redacted>` is a read-view sentinel, never a credential. Restore
571+
// the existing value only after the provider identity is proven to
572+
// match, so a model/parameter edit cannot destroy or misroute it.
573+
parsed.credentials.apiKey = existingApiKey
574+
}
575+
536576
return { valid: true, value: JSON.stringify(parsed, null, 2) }
537577
} catch (error) {
538578
return {

apps/sim/providers/cost-policy.ts

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,10 @@ export interface ModelUsage {
185185
cacheRead?: number
186186
/** Tokens written to the cache, each bucket at its own premium. */
187187
cacheWrites?: CacheWriteUsage[]
188+
/** Whole provider prompt for long-context selection when input is split into cache buckets. */
189+
contextInputTokens?: number
190+
/** Effective processing tier reported by the provider, or requested when no response field exists. */
191+
serviceTier?: 'default' | 'priority'
188192
}
189193

190194
/**
@@ -210,10 +214,30 @@ export function priceModelUsage(
210214
}
211215

212216
const multiplier = policy.multiplier
213-
const base = calculateCost(model, usage.input, usage.output, false, multiplier, multiplier)
214-
215217
const cacheRead = usage.cacheRead ?? 0
216-
const read = cacheRead > 0 ? calculateCost(model, cacheRead, 0, true, multiplier, 0) : undefined
218+
const cacheWriteTokens = (usage.cacheWrites ?? []).reduce(
219+
(total, write) => total + Math.max(0, write.tokens),
220+
0
221+
)
222+
const contextInputTokens = usage.contextInputTokens ?? usage.input + cacheRead + cacheWriteTokens
223+
const pricingOptions = {
224+
contextInputTokens,
225+
serviceTier: usage.serviceTier,
226+
}
227+
const base = calculateCost(
228+
model,
229+
usage.input,
230+
usage.output,
231+
false,
232+
multiplier,
233+
multiplier,
234+
pricingOptions
235+
)
236+
237+
const read =
238+
cacheRead > 0
239+
? calculateCost(model, cacheRead, 0, true, multiplier, 0, pricingOptions)
240+
: undefined
217241

218242
let writeInputCost = 0
219243
for (const write of usage.cacheWrites ?? []) {
@@ -224,7 +248,8 @@ export function priceModelUsage(
224248
0,
225249
false,
226250
multiplier * write.inputRateMultiplier,
227-
0
251+
0,
252+
pricingOptions
228253
).input
229254
}
230255

0 commit comments

Comments
 (0)