import * as fs from 'fs'; import * as path from 'path'; import { omit } from 'lodash'; import { execSync } from 'child_process'; import { merge, isErrorResult } from 'openapi-merge'; import type { Swagger } from 'atlassian-openapi'; import { RESTRICTED_ROUTE_BASE_PATHS } from '@letta-cloud/sdk-core'; const lettaWebOpenAPIPath = path.join( __dirname, '..', '..', '..', 'web', 'autogenerated', 'letta-web-openapi.json', ); const lettaAgentsAPIPath = path.join( __dirname, '..', '..', 'letta', 'server', 'openapi_letta.json', ); const lettaWebOpenAPI = JSON.parse( fs.readFileSync(lettaWebOpenAPIPath, 'utf8'), ) as Swagger.SwaggerV3; const lettaAgentsAPI = JSON.parse( fs.readFileSync(lettaAgentsAPIPath, 'utf8'), ) as Swagger.SwaggerV3; // removes any routes that are restricted lettaAgentsAPI.paths = Object.fromEntries( Object.entries(lettaAgentsAPI.paths).filter(([path]) => RESTRICTED_ROUTE_BASE_PATHS.every( (restrictedPath) => !path.startsWith(restrictedPath), ), ), ); const lettaAgentsAPIWithNoEndslash = Object.keys(lettaAgentsAPI.paths).reduce( (acc, path) => { const pathWithoutSlash = path.endsWith('/') ? path.slice(0, path.length - 1) : path; acc[pathWithoutSlash] = lettaAgentsAPI.paths[path]; return acc; }, {} as Swagger.SwaggerV3['paths'], ); // remove duplicate paths, delete from letta-web-openapi if it exists in sdk-core // some paths will have an extra / at the end, so we need to remove that as well lettaWebOpenAPI.paths = Object.fromEntries( Object.entries(lettaWebOpenAPI.paths).filter(([path]) => { const pathWithoutSlash = path.endsWith('/') ? path.slice(0, path.length - 1) : path; return !lettaAgentsAPIWithNoEndslash[pathWithoutSlash]; }), ); const agentStatePathsToOverride: Array<[string, string]> = [ ['/v1/templates/{project}/{template_version}/agents', '201'], ['/v1/agents/search', '200'], ]; for (const [path, responseCode] of agentStatePathsToOverride) { if (lettaWebOpenAPI.paths[path]?.post?.responses?.[responseCode]) { // Get direct reference to the schema object const responseSchema = lettaWebOpenAPI.paths[path].post.responses[responseCode]; const contentSchema = responseSchema.content['application/json'].schema; // Replace the entire agents array schema with the reference if (contentSchema.properties?.agents) { contentSchema.properties.agents = { type: 'array', items: { $ref: '#/components/schemas/AgentState', }, }; } } } // go through the paths and remove "user_id"/"actor_id" from the headers for (const path of Object.keys(lettaAgentsAPI.paths)) { for (const method of Object.keys(lettaAgentsAPI.paths[path])) { // @ts-expect-error - a if (lettaAgentsAPI.paths[path][method]?.parameters) { // @ts-expect-error - a lettaAgentsAPI.paths[path][method].parameters = lettaAgentsAPI.paths[ path ][method].parameters.filter( (param: Record) => param.in !== 'header' || ( param.name !== 'user_id' && param.name !== 'User-Agent' && param.name !== 'X-Project-Id' && param.name !== 'X-Letta-Source' && param.name !== 'X-Stainless-Package-Version' && !param.name.startsWith('X-Experimental') && !param.name.startsWith('X-Billing') ), ); } } } const result = merge([ { oas: lettaAgentsAPI, }, { oas: lettaWebOpenAPI, }, ]); if (isErrorResult(result)) { console.error(`${result.message} (${result.type})`); process.exit(1); } result.output.openapi = '3.1.0'; result.output.info = { title: 'Letta API', version: '1.0.0', }; result.output.servers = [ { url: 'https://app.letta.com', description: 'Letta Cloud', }, { url: 'http://localhost:8283', description: 'Self-hosted', }, ]; result.output.components = { ...result.output.components, securitySchemes: { bearerAuth: { type: 'http', scheme: 'bearer', }, }, }; result.output.security = [ ...(result.output.security || []), { bearerAuth: [], }, ]; // omit all instances of "user_id" from the openapi.json file function deepOmitPreserveArrays(obj: unknown, key: string): unknown { if (Array.isArray(obj)) { return obj.map((item) => deepOmitPreserveArrays(item, key)); } if (typeof obj !== 'object' || obj === null) { return obj; } if (key in obj) { return omit(obj, key); } return Object.fromEntries( Object.entries(obj).map(([k, v]) => [k, deepOmitPreserveArrays(v, key)]), ); } // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore result.output.components = deepOmitPreserveArrays( result.output.components, 'user_id', ); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore result.output.components = deepOmitPreserveArrays( result.output.components, 'actor_id', ); // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore result.output.components = deepOmitPreserveArrays( result.output.components, 'organization_id', ); fs.writeFileSync( path.join(__dirname, '..', 'openapi.json'), JSON.stringify(result.output, null, 2), ); function formatOpenAPIJson() { const openApiPath = path.join(__dirname, '..', 'openapi.json'); try { execSync(`npx prettier --write "${openApiPath}"`, { stdio: 'inherit' }); console.log('Successfully formatted openapi.json with Prettier'); } catch (error) { console.error('Error formatting openapi.json:', error); process.exit(1); } } formatOpenAPIJson();