// OpenRouter executes web search itself; Spotify calls are handled by the device. const spotifyTypes = ['track', 'artist', 'album', 'playlist']; const actions = { pause: {}, resume: {}, next: {}, previous: {}, status: {}, queue: {}, volume: { volume: { type: 'integer', minimum: 0, maximum: 100 } }, seek: { positionMs: { type: 'integer', minimum: 0, maximum: 86400000 } }, shuffle: { enabled: { type: 'boolean' } }, repeat: { mode: { enum: ['off', 'track', 'context'] } }, enqueue: { query: { type: 'string', maxLength: 500 } }, }; export const tools = [ { type: 'openrouter:web_search' }, { type: 'function', function: { name: 'spotify_play', description: 'Play a track, artist, album or playlist on Spotify. Pass type when the user names a kind of content. Normalize known transliterations.', parameters: { type: 'object', properties: { query: { type: 'string', maxLength: 500 }, uri: { type: 'string', maxLength: 500 }, type: { type: 'string', enum: spotifyTypes } }, additionalProperties: false } } }, { type: 'function', function: { name: 'spotify_control', description: 'Control Spotify on the station. Use status for what is playing, queue to read upcoming tracks, enqueue to add a track by name, seek with an absolute position in milliseconds, and repeat with track, context (album or playlist), or off.', parameters: { type: 'object', properties: { action: { type: 'string', enum: Object.keys(actions) }, ...Object.assign({}, ...Object.values(actions)) }, required: ['action'], additionalProperties: false } } }, { type: 'function', function: { name: 'spotify_playlist', description: 'List own playlists, read tracks, create a private playlist, or add/remove a track. Use playlistUri for an existing playlist, trackUri or trackQuery for a track, and name when creating.', parameters: { type: 'object', properties: { action: { enum: ['list', 'read', 'create', 'add', 'remove'] }, name: { type: 'string', maxLength: 100 }, playlistUri: { type: 'string' }, trackUri: { type: 'string' }, trackQuery: { type: 'string', maxLength: 500 }, }, required: ['action'], additionalProperties: false } } }, { type: 'function', function: { name: 'station_volume', description: 'Set the overall speaker volume for both speech and music. Use this for a general volume request.', parameters: { type: 'object', properties: { level: { type: 'integer', minimum: 0, maximum: 100 }, delta: { type: 'integer', enum: [-10, 10] } }, additionalProperties: false } } }, ]; function validValue(value, schema) { if (schema.enum) return schema.enum.includes(value); if (schema.type === 'integer') return Number.isInteger(value) && value >= schema.minimum && value <= schema.maximum; if (schema.type === 'boolean') return typeof value === 'boolean'; return typeof value === 'string' && !!value.trim() && value.length <= schema.maxLength; } export function validateTool(name, args) { if (!args || typeof args !== 'object' || Array.isArray(args)) throw new Error('invalid_arguments'); const keys = Object.keys(args); if (name === 'spotify_play') { if (keys.some(k => !['query', 'uri', 'type'].includes(k)) || (!args.query && !args.uri) || keys.some(k => k === 'type' ? !spotifyTypes.includes(args[k]) : !validValue(args[k], { maxLength: 500 }))) throw new Error('invalid_arguments'); } else if (name === 'spotify_control') { if (!Object.hasOwn(actions, args.action)) throw new Error('invalid_arguments'); const fields = Object.entries(actions[args.action]); if (keys.length !== fields.length + 1 || fields.some(([key, schema]) => !validValue(args[key], schema))) throw new Error('invalid_arguments'); } else if (name === 'spotify_playlist') { const required = { list: [], read: ['playlistUri'], create: ['name'], add: ['playlistUri'], remove: ['playlistUri'] }[args.action]; if (!required || keys.some(k => !['action', 'name', 'playlistUri', 'trackUri', 'trackQuery'].includes(k)) || required.some(k => !args[k]) || keys.some(k => typeof args[k] !== 'string' || args[k].length > 500)) throw new Error('invalid_arguments'); if (args.playlistUri && !/^spotify:playlist:[A-Za-z0-9]{22}$/.test(args.playlistUri)) throw new Error('invalid_arguments'); if (args.trackUri && !/^spotify:track:[A-Za-z0-9]{22}$/.test(args.trackUri)) throw new Error('invalid_arguments'); if (['add', 'remove'].includes(args.action) && !args.trackUri && !args.trackQuery) throw new Error('invalid_arguments'); } else if (name === 'station_volume') { if (keys.length !== 1 || ('delta' in args ? ![-10, 10].includes(args.delta) : !Number.isInteger(args.level) || args.level < 0 || args.level > 100)) throw new Error('invalid_arguments'); } else throw new Error('unknown_tool'); return args; }