helper.ts 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. /**
  2. * Type definition for a JSON Schema object
  3. */
  4. export interface JsonSchemaObject {
  5. $ref?: string;
  6. $defs?: Record<string, JsonSchemaObject>;
  7. type?: string;
  8. properties?: Record<string, JsonSchemaObject>;
  9. items?: JsonSchemaObject;
  10. anyOf?: JsonSchemaObject[];
  11. title?: string;
  12. description?: string;
  13. required?: string[];
  14. default?: unknown;
  15. additionalProperties?: boolean;
  16. [key: string]: unknown;
  17. }
  18. /**
  19. * Dereferences all $ref fields in a JSON schema by replacing them with the actual referenced schema
  20. *
  21. * @param schema The JSON schema to dereference
  22. * @returns A new JSON schema with all references resolved
  23. */
  24. export function dereferenceJsonSchema(schema: JsonSchemaObject): JsonSchemaObject {
  25. // Create a deep copy of the schema to avoid modifying the original
  26. const clonedSchema = JSON.parse(JSON.stringify(schema));
  27. // Extract definitions to use for resolving references
  28. const definitions = clonedSchema.$defs || {};
  29. // Process the schema
  30. const result = processSchemaNode(clonedSchema, definitions);
  31. // Create a new object without $defs
  32. const resultWithoutDefs: JsonSchemaObject = {};
  33. // Copy all properties except $defs
  34. for (const [key, value] of Object.entries(result)) {
  35. if (key !== '$defs') {
  36. resultWithoutDefs[key] = value;
  37. }
  38. }
  39. return resultWithoutDefs;
  40. }
  41. /**
  42. * Process a schema node, resolving all references
  43. */
  44. function processSchemaNode(node: JsonSchemaObject, definitions: Record<string, JsonSchemaObject>): JsonSchemaObject {
  45. // If it's not an object or is null, return as is
  46. if (typeof node !== 'object' || node === null) {
  47. return node;
  48. }
  49. // If it's a reference, resolve it
  50. if (node.$ref) {
  51. const refPath = node.$ref.replace('#/$defs/', '');
  52. const definition = definitions[refPath];
  53. if (definition) {
  54. // Process the definition to resolve any nested references
  55. return processSchemaNode(definition, definitions);
  56. }
  57. }
  58. // Handle anyOf for references
  59. if (node.anyOf) {
  60. // Process each item in anyOf
  61. const processedAnyOf = node.anyOf.map(item => processSchemaNode(item, definitions));
  62. // If anyOf contains a reference and a null type, merge them
  63. const nonNullTypes = processedAnyOf.filter(item => item.type !== 'null');
  64. const hasNullType = processedAnyOf.some(item => item.type === 'null');
  65. if (nonNullTypes.length === 1 && hasNullType) {
  66. const result = { ...nonNullTypes[0] };
  67. result.nullable = true;
  68. return result;
  69. }
  70. // Otherwise, keep the anyOf structure but with processed items
  71. return {
  72. ...node,
  73. anyOf: processedAnyOf,
  74. };
  75. }
  76. // Create a new node with processed properties
  77. const result: JsonSchemaObject = {};
  78. // Copy all properties except $ref
  79. for (const [key, value] of Object.entries(node)) {
  80. if (key !== '$ref') {
  81. if (key === 'properties' && typeof value === 'object' && value !== null) {
  82. // Process properties
  83. result.properties = {};
  84. for (const [propKey, propValue] of Object.entries(value)) {
  85. result.properties[propKey] = processSchemaNode(propValue as JsonSchemaObject, definitions);
  86. }
  87. } else if (key === 'items' && typeof value === 'object' && value !== null) {
  88. // Process items for arrays
  89. result.items = processSchemaNode(value as JsonSchemaObject, definitions);
  90. } else {
  91. // Copy other properties as is
  92. result[key] = value;
  93. }
  94. }
  95. }
  96. return result;
  97. }
  98. /**
  99. * Converts an OpenAI format JSON schema to a Google Gemini compatible schema
  100. *
  101. * Key differences handled:
  102. * 1. OpenAI uses $defs and $ref for references, Gemini uses inline definitions
  103. * 2. Different structure for nullable properties
  104. * 3. Gemini has a flatter structure for defining properties
  105. *
  106. * @param openaiSchema The OpenAI format JSON schema to convert
  107. * @returns A Google Gemini compatible JSON schema
  108. */
  109. export function convertOpenAISchemaToGemini(openaiSchema: JsonSchemaObject): JsonSchemaObject {
  110. // Create a new schema object
  111. const geminiSchema: JsonSchemaObject = {
  112. type: openaiSchema.type,
  113. properties: {},
  114. required: openaiSchema.required || [],
  115. };
  116. // Process definitions to use for resolving references
  117. const definitions = openaiSchema.$defs || {};
  118. // Process properties
  119. if (openaiSchema.properties) {
  120. geminiSchema.properties = processProperties(openaiSchema.properties, definitions);
  121. }
  122. return geminiSchema;
  123. }
  124. /**
  125. * Process properties recursively, resolving references and converting to Gemini format
  126. */
  127. function processProperties(
  128. properties: Record<string, JsonSchemaObject>,
  129. definitions: Record<string, JsonSchemaObject>,
  130. ): Record<string, JsonSchemaObject> {
  131. const result: Record<string, JsonSchemaObject> = {};
  132. for (const [key, value] of Object.entries(properties)) {
  133. if (typeof value !== 'object' || value === null) continue;
  134. result[key] = processProperty(value, definitions);
  135. }
  136. return result;
  137. }
  138. /**
  139. * Process a single property, resolving references and converting to Gemini format
  140. */
  141. function processProperty(property: JsonSchemaObject, definitions: Record<string, JsonSchemaObject>): JsonSchemaObject {
  142. // If it's a reference, resolve it
  143. if (property.$ref) {
  144. const refPath = property.$ref.replace('#/$defs/', '');
  145. const definition = definitions[refPath];
  146. if (definition) {
  147. return processProperty(definition, definitions);
  148. }
  149. }
  150. // Handle anyOf for nullable properties
  151. if (property.anyOf) {
  152. const nonNullType = property.anyOf.find(item => item.type !== 'null' && !item.$ref);
  153. const refType = property.anyOf.find(item => item.$ref);
  154. const isNullable = property.anyOf.some(item => item.type === 'null');
  155. if (refType?.$ref) {
  156. const refPath = refType.$ref.replace('#/$defs/', '');
  157. const definition = definitions[refPath];
  158. if (definition) {
  159. const processed = processProperty(definition, definitions);
  160. if (isNullable) {
  161. processed.nullable = true;
  162. }
  163. return processed;
  164. }
  165. }
  166. if (nonNullType) {
  167. const processed = processProperty(nonNullType, definitions);
  168. if (isNullable) {
  169. processed.nullable = true;
  170. }
  171. return processed;
  172. }
  173. }
  174. // Create a new property object
  175. const result: JsonSchemaObject = {
  176. type: property.type,
  177. };
  178. // Copy description if it exists
  179. if (property.description) {
  180. result.description = property.description;
  181. }
  182. // Process nested properties
  183. if (property.properties) {
  184. result.properties = processProperties(property.properties, definitions);
  185. // Copy required fields
  186. if (property.required) {
  187. result.required = property.required;
  188. } else {
  189. result.required = [];
  190. }
  191. }
  192. // Handle arrays
  193. if (property.items) {
  194. result.items = processProperty(property.items, definitions);
  195. }
  196. // Handle special case for NoParamsAction which is an object in OpenAI but a string in Gemini
  197. if (property.additionalProperties === true && property.title === 'NoParamsAction' && property.description) {
  198. return {
  199. type: 'string',
  200. nullable: true,
  201. description: property.description,
  202. };
  203. }
  204. return result;
  205. }
  206. export type JSONSchemaType = JsonSchemaObject | JSONSchemaType[];
  207. // Custom stringify function
  208. export function stringifyCustom(value: JSONSchemaType, indent = '', baseIndent = ' '): string {
  209. const currentIndent = indent + baseIndent;
  210. if (value === null) {
  211. return 'null';
  212. }
  213. switch (typeof value) {
  214. case 'string':
  215. // Escape single quotes within the string if necessary
  216. return `'${value.replace(/'/g, "\\\\'")}'`;
  217. case 'number':
  218. case 'boolean':
  219. return String(value);
  220. case 'object': {
  221. if (Array.isArray(value)) {
  222. if (value.length === 0) {
  223. return '[]';
  224. }
  225. const items = value.map(item => `${currentIndent}${stringifyCustom(item, currentIndent, baseIndent)}`);
  226. return `[\n${items.join(',\n')}\n${indent}]`;
  227. }
  228. const keys = Object.keys(value);
  229. if (keys.length === 0) {
  230. return '{}';
  231. }
  232. const properties = keys.map(key => {
  233. // Assume keys are valid JS identifiers and don't need quotes
  234. const formattedKey = key;
  235. const formattedValue = stringifyCustom(value[key], currentIndent, baseIndent);
  236. return `${currentIndent}${formattedKey}: ${formattedValue}`;
  237. });
  238. return `{\n${properties.join(',\n')}\n${indent}}`;
  239. }
  240. default:
  241. // Handle undefined, etc.
  242. return 'undefined';
  243. }
  244. }