Skip to content

Commit cb0d8b5

Browse files
committed
fix(chat): hosted key execution via call-integration-tool
1 parent 772b710 commit cb0d8b5

4 files changed

Lines changed: 114 additions & 3 deletions

File tree

apps/sim/lib/copilot/chat/payload.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,11 +185,11 @@ describe('buildIntegrationToolSchemas', () => {
185185

186186
expect(mockCreateUserToolSchema).toHaveBeenCalledWith(
187187
expect.objectContaining({ id: 'gmail_send' }),
188-
{ surface: 'copilot' }
188+
{ surface: 'copilot', hostedKeySupport: expect.any(Boolean) }
189189
)
190190
expect(mockCreateUserToolSchema).toHaveBeenCalledWith(
191191
expect.objectContaining({ id: 'brandfetch_search' }),
192-
{ surface: 'copilot' }
192+
{ surface: 'copilot', hostedKeySupport: expect.any(Boolean) }
193193
)
194194
})
195195

apps/sim/lib/copilot/chat/payload.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,10 @@ async function buildIntegrationToolSchemasUncached(
226226
}
227227
const userSchema = createUserToolSchema(toolConfig, {
228228
surface: options.schemaSurface,
229+
// On hosted deployments the executor injects hosted keys server-side,
230+
// so the gateway schema must not force the model to supply one (the
231+
// model never sees the key either way).
232+
hostedKeySupport: isHosted,
229233
})
230234
const catalogEntry = getToolEntry(toolId)
231235
integrationTools.push({

apps/sim/tools/params.test.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,95 @@ describe('Tool Parameters Utils', () => {
142142
expect(schema.properties).toHaveProperty('message')
143143
})
144144

145+
it.concurrent('keeps the hosted key param optional when hosted keys are supported', () => {
146+
const hostedTool = {
147+
...mockToolConfig,
148+
id: 'hosted_key_tool',
149+
params: {
150+
query: {
151+
type: 'string',
152+
required: true,
153+
visibility: 'user-or-llm' as ParameterVisibility,
154+
description: 'Search query',
155+
},
156+
apiKey: {
157+
type: 'string',
158+
required: true,
159+
visibility: 'user-only' as ParameterVisibility,
160+
description: 'Exa AI API Key',
161+
},
162+
},
163+
hosting: {
164+
envKeyPrefix: 'EXA_API_KEY',
165+
apiKeyParam: 'apiKey',
166+
byokProviderId: 'exa',
167+
pricing: { type: 'per_request' as const, cost: 0.005 },
168+
rateLimit: { mode: 'per_request' as const, requestsPerMinute: 100 },
169+
},
170+
}
171+
172+
const hostedSchema = createUserToolSchema(hostedTool, {
173+
surface: 'copilot',
174+
hostedKeySupport: true,
175+
})
176+
177+
// The key stays available as a bring-your-own-key override but is never
178+
// a required argument — the executor injects the hosted key server-side.
179+
expect(hostedSchema.properties).toHaveProperty('apiKey')
180+
expect(hostedSchema.required).not.toContain('apiKey')
181+
expect(hostedSchema.properties.apiKey.description).toContain('hosted key')
182+
expect(hostedSchema.required).toContain('query')
183+
})
184+
185+
it.concurrent('keeps the hosted key param required without hosted key support', () => {
186+
const hostedTool = {
187+
...mockToolConfig,
188+
id: 'hosted_key_tool_self_hosted',
189+
params: {
190+
apiKey: {
191+
type: 'string',
192+
required: true,
193+
visibility: 'user-only' as ParameterVisibility,
194+
description: 'Exa AI API Key',
195+
},
196+
},
197+
hosting: {
198+
envKeyPrefix: 'EXA_API_KEY',
199+
apiKeyParam: 'apiKey',
200+
byokProviderId: 'exa',
201+
pricing: { type: 'per_request' as const, cost: 0.005 },
202+
rateLimit: { mode: 'per_request' as const, requestsPerMinute: 100 },
203+
},
204+
}
205+
206+
const selfHostedSchema = createUserToolSchema(hostedTool, { surface: 'copilot' })
207+
208+
expect(selfHostedSchema.required).toContain('apiKey')
209+
expect(selfHostedSchema.properties.apiKey.description).not.toContain('hosted key')
210+
})
211+
212+
it.concurrent('does not relax required keys on tools without hosting', () => {
213+
const plainTool = {
214+
...mockToolConfig,
215+
id: 'plain_key_tool',
216+
params: {
217+
apiKey: {
218+
type: 'string',
219+
required: true,
220+
visibility: 'user-only' as ParameterVisibility,
221+
description: 'Service API Key',
222+
},
223+
},
224+
}
225+
226+
const schema = createUserToolSchema(plainTool, {
227+
surface: 'copilot',
228+
hostedKeySupport: true,
229+
})
230+
231+
expect(schema.required).toContain('apiKey')
232+
})
233+
145234
it.concurrent('adds credentialId only for copilot-facing oauth schemas', () => {
146235
const oauthTool = {
147236
...mockToolConfig,

apps/sim/tools/params.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,14 @@ export interface ToolSchema {
129129

130130
export interface UserToolSchemaOptions {
131131
surface?: 'default' | 'copilot'
132+
/**
133+
* Set when the deployment provides hosted API keys for tools with a
134+
* `hosting` config. The hosted key param then stays in the schema only as an
135+
* optional bring-your-own-key override instead of a required argument — the
136+
* executor injects the hosted key server-side after validation, and the key
137+
* value itself is never exposed to the model or the mothership.
138+
*/
139+
hostedKeySupport?: boolean
132140
}
133141

134142
export interface LLMToolSchemaResult {
@@ -505,6 +513,8 @@ export function createUserToolSchema(
505513
options: UserToolSchemaOptions = {}
506514
): ToolSchema {
507515
const surface = options.surface ?? 'default'
516+
const hostedApiKeyParam =
517+
options.hostedKeySupport && toolConfig.hosting ? toolConfig.hosting.apiKeyParam : undefined
508518
const schema: ToolSchema = {
509519
type: 'object',
510520
properties: {},
@@ -519,9 +529,17 @@ export function createUserToolSchema(
519529
}
520530

521531
const propertySchema = buildParameterSchema(toolConfig.id, paramId, param, options)
532+
if (paramId === hostedApiKeyParam) {
533+
propertySchema.description = [
534+
propertySchema.description,
535+
'Optional: Sim provides a hosted key for this tool. Omit this parameter unless intentionally overriding with your own key.',
536+
]
537+
.filter(Boolean)
538+
.join(' ')
539+
}
522540
schema.properties[paramId] = propertySchema
523541

524-
if (param.required) {
542+
if (param.required && paramId !== hostedApiKeyParam) {
525543
schema.required.push(paramId)
526544
}
527545
}

0 commit comments

Comments
 (0)