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
4 changes: 2 additions & 2 deletions apps/sim/lib/copilot/chat/payload.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,11 +185,11 @@ describe('buildIntegrationToolSchemas', () => {

expect(mockCreateUserToolSchema).toHaveBeenCalledWith(
expect.objectContaining({ id: 'gmail_send' }),
{ surface: 'copilot' }
{ surface: 'copilot', hostedKeySupport: expect.any(Boolean) }
)
expect(mockCreateUserToolSchema).toHaveBeenCalledWith(
expect.objectContaining({ id: 'brandfetch_search' }),
{ surface: 'copilot' }
{ surface: 'copilot', hostedKeySupport: expect.any(Boolean) }
)
})

Expand Down
4 changes: 4 additions & 0 deletions apps/sim/lib/copilot/chat/payload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,10 @@ async function buildIntegrationToolSchemasUncached(
}
const userSchema = createUserToolSchema(toolConfig, {
surface: options.schemaSurface,
// On hosted deployments the executor injects hosted keys server-side,
// so the gateway schema must not force the model to supply one (the
// model never sees the key either way).
hostedKeySupport: isHosted,
})
const catalogEntry = getToolEntry(toolId)
integrationTools.push({
Expand Down
128 changes: 128 additions & 0 deletions apps/sim/tools/params.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,134 @@ describe('Tool Parameters Utils', () => {
expect(schema.properties).toHaveProperty('message')
})

it.concurrent('keeps the hosted key param optional when hosted keys are supported', () => {
const hostedTool = {
...mockToolConfig,
id: 'hosted_key_tool',
params: {
query: {
type: 'string',
required: true,
visibility: 'user-or-llm' as ParameterVisibility,
description: 'Search query',
},
apiKey: {
type: 'string',
required: true,
visibility: 'user-only' as ParameterVisibility,
description: 'Exa AI API Key',
},
},
hosting: {
envKeyPrefix: 'EXA_API_KEY',
apiKeyParam: 'apiKey',
byokProviderId: 'exa',
pricing: { type: 'per_request' as const, cost: 0.005 },
rateLimit: { mode: 'per_request' as const, requestsPerMinute: 100 },
},
}

const hostedSchema = createUserToolSchema(hostedTool, {
surface: 'copilot',
hostedKeySupport: true,
})

// The key stays available as a bring-your-own-key override but is never
// a required argument — the executor injects the hosted key server-side.
expect(hostedSchema.properties).toHaveProperty('apiKey')
expect(hostedSchema.required).not.toContain('apiKey')
expect(hostedSchema.properties.apiKey.description).toContain('hosted key')
expect(hostedSchema.required).toContain('query')
})

it.concurrent('keeps the hosted key param required without hosted key support', () => {
const hostedTool = {
...mockToolConfig,
id: 'hosted_key_tool_self_hosted',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only' as ParameterVisibility,
description: 'Exa AI API Key',
},
},
hosting: {
envKeyPrefix: 'EXA_API_KEY',
apiKeyParam: 'apiKey',
byokProviderId: 'exa',
pricing: { type: 'per_request' as const, cost: 0.005 },
rateLimit: { mode: 'per_request' as const, requestsPerMinute: 100 },
},
}

const selfHostedSchema = createUserToolSchema(hostedTool, { surface: 'copilot' })

expect(selfHostedSchema.required).toContain('apiKey')
expect(selfHostedSchema.properties.apiKey.description).not.toContain('hosted key')
})

it.concurrent('keeps the key required for conditionally hosted tools', () => {
const enabled = Object.assign(
(params: Record<string, unknown>) => params.provider === 'falai',
{
condition: { field: 'provider', operator: 'equals' as const, value: 'falai' },
}
)
const conditionalTool = {
...mockToolConfig,
id: 'conditional_hosted_tool',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only' as ParameterVisibility,
description: 'Provider API Key',
},
},
hosting: {
enabled,
envKeyPrefix: 'FALAI_API_KEY',
apiKeyParam: 'apiKey',
byokProviderId: 'falai',
pricing: { type: 'per_request' as const, cost: 0.01 },
rateLimit: { mode: 'per_request' as const, requestsPerMinute: 100 },
},
}

const schema = createUserToolSchema(conditionalTool, {
surface: 'copilot',
hostedKeySupport: true,
})

// Injection only happens when the predicate passes at runtime, so the
// schema must not promise a hosted key for every configuration.
expect(schema.required).toContain('apiKey')
expect(schema.properties.apiKey.description).not.toContain('hosted key')
})

it.concurrent('does not relax required keys on tools without hosting', () => {
const plainTool = {
...mockToolConfig,
id: 'plain_key_tool',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only' as ParameterVisibility,
description: 'Service API Key',
},
},
}

const schema = createUserToolSchema(plainTool, {
surface: 'copilot',
hostedKeySupport: true,
})

expect(schema.required).toContain('apiKey')
})

it.concurrent('adds credentialId only for copilot-facing oauth schemas', () => {
const oauthTool = {
...mockToolConfig,
Expand Down
26 changes: 25 additions & 1 deletion apps/sim/tools/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,18 @@ export interface ToolSchema {

export interface UserToolSchemaOptions {
surface?: 'default' | 'copilot'
/**
* Set when the deployment provides hosted API keys for tools with a
* `hosting` config. For unconditionally hosted tools the key param then
* stays in the schema only as an optional bring-your-own-key override
* instead of a required argument — the executor injects the hosted key
* server-side after validation, and the key value itself is never exposed
* to the model or the mothership. Tools with a conditional
* `hosting.enabled` predicate keep the key required, since injection only
* happens for configurations that satisfy the predicate (mirrors the VFS
* `conditional_hosted_or_byok` auth mode).
*/
hostedKeySupport?: boolean
}

export interface LLMToolSchemaResult {
Expand Down Expand Up @@ -505,6 +517,10 @@ export function createUserToolSchema(
options: UserToolSchemaOptions = {}
): ToolSchema {
const surface = options.surface ?? 'default'
const hostedApiKeyParam =
options.hostedKeySupport && toolConfig.hosting && !toolConfig.hosting.enabled
? toolConfig.hosting.apiKeyParam
: undefined
const schema: ToolSchema = {
type: 'object',
properties: {},
Expand All @@ -519,9 +535,17 @@ export function createUserToolSchema(
}

const propertySchema = buildParameterSchema(toolConfig.id, paramId, param, options)
if (paramId === hostedApiKeyParam) {
propertySchema.description = [
propertySchema.description,
'Optional: Sim provides a hosted key for this tool. Omit this parameter unless intentionally overriding with your own key.',
]
.filter(Boolean)
.join(' ')
}
schema.properties[paramId] = propertySchema

if (param.required) {
if (param.required && paramId !== hostedApiKeyParam) {
Comment thread
icecrasher321 marked this conversation as resolved.
schema.required.push(paramId)
}
}
Expand Down
Loading