Skip to content

Commit 47f030d

Browse files
committed
handle derefer logic in case of union of defs and other edge cases in null type
1 parent 215913a commit 47f030d

File tree

1 file changed

+47
-47
lines changed

1 file changed

+47
-47
lines changed

src/providers/google-vertex-ai/utils.ts

Lines changed: 47 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -191,44 +191,48 @@ export const GoogleErrorResponseTransform: (
191191
return undefined;
192192
};
193193

194-
const getDefFromRef = (ref: string) => {
195-
const refParts = ref.split('/');
196-
return refParts.at(-1);
194+
// Extract definition key from a JSON Schema $ref string
195+
const getDefFromRef = (ref: string): string | null => {
196+
const match = ref.match(/^#\/\$defs\/(.+)$/);
197+
return match ? match[1] : null;
197198
};
198199

199-
const getRefParts = (spec: Record<string, any>, ref: string) => {
200-
return spec?.[ref];
201-
};
202-
203-
export const derefer = (spec: Record<string, any>, defs = null) => {
204-
const original = { ...spec };
205-
206-
const finalDefs = defs ?? original?.['$defs'];
207-
const entries = Object.entries(original);
208-
209-
for (let [key, object] of entries) {
210-
if (key === '$defs') {
211-
continue;
212-
}
213-
if (
214-
object === null ||
215-
typeof object !== 'object' ||
216-
Array.isArray(object)
217-
) {
218-
continue;
219-
}
220-
const ref = object?.['$ref'];
221-
if (ref) {
222-
const def = getDefFromRef(ref);
223-
const defData = getRefParts(finalDefs, def ?? '');
224-
const newValue = derefer(defData, finalDefs);
225-
original[key] = newValue;
226-
} else {
227-
const newValue = derefer(object, finalDefs);
228-
original[key] = newValue;
200+
const getDefObject = (
201+
defs: Record<string, any> | undefined | null,
202+
key: string | null
203+
): any => (key && defs ? defs[key] : undefined);
204+
205+
// Recursively expands $ref nodes in a JSON Schema object tree
206+
export const derefer = (
207+
schema: any,
208+
defs: Record<string, any> | null = null,
209+
stack: Set<string> = new Set()
210+
): any => {
211+
if (schema === null || typeof schema !== 'object') return schema;
212+
if (Array.isArray(schema))
213+
return schema.map((item) => derefer(item, defs, stack));
214+
const node = { ...schema };
215+
const activeDefs =
216+
defs ?? (node.$defs as Record<string, any> | undefined) ?? null;
217+
if ('$ref' in node && typeof node.$ref === 'string') {
218+
const defKey = getDefFromRef(node.$ref);
219+
const target = getDefObject(activeDefs, defKey);
220+
if (defKey && target) {
221+
if (stack.has(defKey)) return node;
222+
stack.add(defKey);
223+
const resolved = derefer(target, activeDefs, stack);
224+
stack.delete(defKey);
225+
const keys = Object.keys(node);
226+
if (keys.length === 1) return resolved;
227+
const { $ref: _, ...siblings } = node;
228+
return derefer({ ...resolved, ...siblings }, activeDefs, stack);
229229
}
230230
}
231-
return original;
231+
for (const [k, v] of Object.entries(node)) {
232+
if (k === '$defs') continue;
233+
node[k] = derefer(v, activeDefs, stack);
234+
}
235+
return node;
232236
};
233237

234238
export const transformGeminiToolParameters = (
@@ -248,6 +252,9 @@ export const transformGeminiToolParameters = (
248252
delete schema.$defs;
249253
}
250254

255+
const isNullTypeNode = (node: any): boolean =>
256+
node && typeof node === 'object' && node.type === 'null';
257+
251258
const transformNode = (node: JsonSchema): JsonSchema => {
252259
if (Array.isArray(node)) {
253260
return node.map(transformNode);
@@ -260,21 +267,14 @@ export const transformGeminiToolParameters = (
260267
if (key === 'enum' && Array.isArray(value)) {
261268
transformed.enum = value;
262269
transformed.format = 'enum';
263-
} else if (
264-
key === 'anyOf' &&
265-
Array.isArray(value) &&
266-
value.length === 2
267-
) {
268-
// Convert anyOf with null type to nullable which is a supported param
269-
const nonNullItems = value.filter(
270-
(item) => !(typeof item === 'object' && item?.type === 'null')
271-
);
272-
if (nonNullItems.length === 1) {
273-
Object.assign(transformed, transformNode(nonNullItems[0]));
270+
} else if ((key === 'anyOf' || key === 'oneOf') && Array.isArray(value)) {
271+
const nonNullItems = value.filter((item) => !isNullTypeNode(item));
272+
if (nonNullItems.length < value.length) {
273+
// remove `null` type in schema and set nullable: true
274+
transformed[key] = transformNode(nonNullItems);
274275
transformed.nullable = true;
275276
} else {
276-
// leave true unions as-is which is not supported by Google, let Google raise an error
277-
transformed.anyOf = transformNode(value);
277+
transformed[key] = transformNode(value);
278278
}
279279
} else {
280280
transformed[key] = transformNode(value);

0 commit comments

Comments
 (0)