218 lines
5.4 KiB
TypeScript
218 lines
5.4 KiB
TypeScript
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<string, string>) =>
|
|
param.in !== 'header' ||
|
|
(
|
|
param.name !== 'user_id' &&
|
|
param.name !== 'User-Agent' &&
|
|
param.name !== 'X-Project-Id' &&
|
|
!param.name.startsWith('X-Experimental')
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
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();
|