Skip to content

Commit 98b62c5

Browse files
feat: support JSON schema dereferencing in tool-param for GCP Anthropic req (#1508)
**Description** This commit adds support for dereferencing JSON schema `$ref` references in tool parameters for the OpenAI to GCP Anthropic translator. When OpenAI tool definitions contain JSON schemas with `$ref` pointers, these references are now automatically resolved before the schema is converted to Anthropic's format. Signed-off-by: Sukumar Gaonkar <[email protected]>
1 parent a427796 commit 98b62c5

File tree

2 files changed

+277
-0
lines changed

2 files changed

+277
-0
lines changed

internal/extproc/translator/openai_gcpanthropic.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,16 @@ func translateOpenAItoAnthropicTools(openAITools []openai.Tool, openAIToolChoice
163163

164164
inputSchema := anthropic.ToolInputSchemaParam{}
165165

166+
// Dereference json schema
167+
// If the paramsMap contains $refs we need to dereference them
168+
var dereferencedParamsMap any
169+
if dereferencedParamsMap, err = jsonSchemaDereference(paramsMap); err != nil {
170+
return nil, anthropic.ToolChoiceUnionParam{}, fmt.Errorf("failed to dereference tool parameters: %w", err)
171+
}
172+
if paramsMap, ok = dereferencedParamsMap.(map[string]any); !ok {
173+
return nil, anthropic.ToolChoiceUnionParam{}, fmt.Errorf("failed to cast dereferenced tool parameters to map[string]interface{}")
174+
}
175+
166176
var typeVal string
167177
if typeVal, ok = paramsMap["type"].(string); ok {
168178
inputSchema.Type = constant.Object(typeVal)

internal/extproc/translator/openai_gcpanthropic_test.go

Lines changed: 267 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1318,6 +1318,273 @@ func TestFinishReasonTranslation(t *testing.T) {
13181318
}
13191319
}
13201320

1321+
// TestToolParameterDereferencing tests the JSON schema dereferencing functionality
1322+
// for tool parameters when translating from OpenAI to GCP Anthropic.
1323+
func TestToolParameterDereferencing(t *testing.T) {
1324+
tests := []struct {
1325+
name string
1326+
openAIReq *openai.ChatCompletionRequest
1327+
expectedTools []anthropic.ToolUnionParam
1328+
expectedToolChoice anthropic.ToolChoiceUnionParam
1329+
expectErr bool
1330+
expectedErrMsg string
1331+
}{
1332+
{
1333+
name: "tool with complex nested $ref - successful dereferencing",
1334+
openAIReq: &openai.ChatCompletionRequest{
1335+
Tools: []openai.Tool{
1336+
{
1337+
Type: "function",
1338+
Function: &openai.FunctionDefinition{
1339+
Name: "complex_tool",
1340+
Description: "Tool with complex nested references",
1341+
Parameters: map[string]any{
1342+
"type": "object",
1343+
"$defs": map[string]any{
1344+
"BaseType": map[string]any{
1345+
"type": "object",
1346+
"properties": map[string]any{
1347+
"id": map[string]any{
1348+
"type": "string",
1349+
},
1350+
"required": []any{"id"},
1351+
},
1352+
},
1353+
"NestedType": map[string]any{
1354+
"allOf": []any{
1355+
map[string]any{"$ref": "#/$defs/BaseType"},
1356+
map[string]any{
1357+
"properties": map[string]any{
1358+
"name": map[string]any{
1359+
"type": "string",
1360+
},
1361+
},
1362+
},
1363+
},
1364+
},
1365+
},
1366+
"properties": map[string]any{
1367+
"nested": map[string]any{
1368+
"$ref": "#/$defs/NestedType",
1369+
},
1370+
},
1371+
},
1372+
},
1373+
},
1374+
},
1375+
},
1376+
expectedTools: []anthropic.ToolUnionParam{
1377+
{
1378+
OfTool: &anthropic.ToolParam{
1379+
Name: "complex_tool",
1380+
Description: anthropic.String("Tool with complex nested references"),
1381+
InputSchema: anthropic.ToolInputSchemaParam{
1382+
Type: "object",
1383+
Properties: map[string]any{
1384+
"nested": map[string]any{
1385+
"allOf": []any{
1386+
map[string]any{
1387+
"type": "object",
1388+
"properties": map[string]any{
1389+
"id": map[string]any{
1390+
"type": "string",
1391+
},
1392+
"required": []any{"id"},
1393+
},
1394+
},
1395+
map[string]any{
1396+
"properties": map[string]any{
1397+
"name": map[string]any{
1398+
"type": "string",
1399+
},
1400+
},
1401+
},
1402+
},
1403+
},
1404+
},
1405+
},
1406+
},
1407+
},
1408+
},
1409+
},
1410+
{
1411+
name: "tool with invalid $ref - dereferencing error",
1412+
openAIReq: &openai.ChatCompletionRequest{
1413+
Tools: []openai.Tool{
1414+
{
1415+
Type: "function",
1416+
Function: &openai.FunctionDefinition{
1417+
Name: "invalid_ref_tool",
1418+
Description: "Tool with invalid reference",
1419+
Parameters: map[string]any{
1420+
"type": "object",
1421+
"properties": map[string]any{
1422+
"location": map[string]any{
1423+
"$ref": "#/$defs/NonExistent",
1424+
},
1425+
},
1426+
},
1427+
},
1428+
},
1429+
},
1430+
},
1431+
expectErr: true,
1432+
expectedErrMsg: "failed to dereference tool parameters",
1433+
},
1434+
{
1435+
name: "tool with circular $ref - dereferencing error",
1436+
openAIReq: &openai.ChatCompletionRequest{
1437+
Tools: []openai.Tool{
1438+
{
1439+
Type: "function",
1440+
Function: &openai.FunctionDefinition{
1441+
Name: "circular_ref_tool",
1442+
Description: "Tool with circular reference",
1443+
Parameters: map[string]any{
1444+
"type": "object",
1445+
"$defs": map[string]any{
1446+
"A": map[string]any{
1447+
"type": "object",
1448+
"properties": map[string]any{
1449+
"b": map[string]any{
1450+
"$ref": "#/$defs/B",
1451+
},
1452+
},
1453+
},
1454+
"B": map[string]any{
1455+
"type": "object",
1456+
"properties": map[string]any{
1457+
"a": map[string]any{
1458+
"$ref": "#/$defs/A",
1459+
},
1460+
},
1461+
},
1462+
},
1463+
"properties": map[string]any{
1464+
"circular": map[string]any{
1465+
"$ref": "#/$defs/A",
1466+
},
1467+
},
1468+
},
1469+
},
1470+
},
1471+
},
1472+
},
1473+
expectErr: true,
1474+
expectedErrMsg: "failed to dereference tool parameters",
1475+
},
1476+
{
1477+
name: "tool without $ref - no dereferencing needed",
1478+
openAIReq: &openai.ChatCompletionRequest{
1479+
Tools: []openai.Tool{
1480+
{
1481+
Type: "function",
1482+
Function: &openai.FunctionDefinition{
1483+
Name: "simple_tool",
1484+
Description: "Simple tool without references",
1485+
Parameters: map[string]any{
1486+
"type": "object",
1487+
"properties": map[string]any{
1488+
"location": map[string]any{
1489+
"type": "string",
1490+
},
1491+
},
1492+
"required": []any{"location"},
1493+
},
1494+
},
1495+
},
1496+
},
1497+
},
1498+
expectedTools: []anthropic.ToolUnionParam{
1499+
{
1500+
OfTool: &anthropic.ToolParam{
1501+
Name: "simple_tool",
1502+
Description: anthropic.String("Simple tool without references"),
1503+
InputSchema: anthropic.ToolInputSchemaParam{
1504+
Type: "object",
1505+
Properties: map[string]any{
1506+
"location": map[string]any{
1507+
"type": "string",
1508+
},
1509+
},
1510+
Required: []string{"location"},
1511+
},
1512+
},
1513+
},
1514+
},
1515+
},
1516+
{
1517+
name: "tool parameter dereferencing returns non-map type - casting error",
1518+
openAIReq: &openai.ChatCompletionRequest{
1519+
Tools: []openai.Tool{
1520+
{
1521+
Type: "function",
1522+
Function: &openai.FunctionDefinition{
1523+
Name: "problematic_tool",
1524+
Description: "Tool with parameters that can't be properly dereferenced to map",
1525+
// This creates a scenario where jsonSchemaDereference might return a non-map type
1526+
// though this is a contrived example since normally the function should return map[string]any
1527+
Parameters: map[string]any{
1528+
"$ref": "#/$defs/StringType", // This would resolve to a string, not a map
1529+
"$defs": map[string]any{
1530+
"StringType": "not-a-map", // This would cause the casting to fail
1531+
},
1532+
},
1533+
},
1534+
},
1535+
},
1536+
},
1537+
expectErr: true,
1538+
expectedErrMsg: "failed to cast dereferenced tool parameters",
1539+
},
1540+
}
1541+
1542+
for _, tt := range tests {
1543+
t.Run(tt.name, func(t *testing.T) {
1544+
tools, toolChoice, err := translateOpenAItoAnthropicTools(tt.openAIReq.Tools, tt.openAIReq.ToolChoice, tt.openAIReq.ParallelToolCalls)
1545+
1546+
if tt.expectErr {
1547+
require.Error(t, err)
1548+
if tt.expectedErrMsg != "" {
1549+
require.Contains(t, err.Error(), tt.expectedErrMsg)
1550+
}
1551+
return
1552+
}
1553+
1554+
require.NoError(t, err)
1555+
1556+
if tt.openAIReq.Tools != nil {
1557+
require.NotNil(t, tools)
1558+
require.Len(t, tools, len(tt.expectedTools))
1559+
1560+
for i, expectedTool := range tt.expectedTools {
1561+
actualTool := tools[i]
1562+
require.Equal(t, expectedTool.GetName(), actualTool.GetName())
1563+
require.Equal(t, expectedTool.GetType(), actualTool.GetType())
1564+
require.Equal(t, expectedTool.GetDescription(), actualTool.GetDescription())
1565+
1566+
expectedSchema := expectedTool.GetInputSchema()
1567+
actualSchema := actualTool.GetInputSchema()
1568+
1569+
require.Equal(t, expectedSchema.Type, actualSchema.Type)
1570+
require.Equal(t, expectedSchema.Required, actualSchema.Required)
1571+
1572+
// For properties, we'll do a deep comparison to verify dereferencing worked
1573+
if expectedSchema.Properties != nil {
1574+
require.NotNil(t, actualSchema.Properties)
1575+
require.Equal(t, expectedSchema.Properties, actualSchema.Properties)
1576+
}
1577+
}
1578+
}
1579+
1580+
if tt.openAIReq.ToolChoice != nil {
1581+
require.NotNil(t, toolChoice)
1582+
require.Equal(t, *tt.expectedToolChoice.GetType(), *toolChoice.GetType())
1583+
}
1584+
})
1585+
}
1586+
}
1587+
13211588
// TestContentTranslationCoverage adds specific coverage for the openAIToAnthropicContent helper.
13221589
func TestContentTranslationCoverage(t *testing.T) {
13231590
tests := []struct {

0 commit comments

Comments
 (0)