|
8 | 8 | "strings" |
9 | 9 | "testing" |
10 | 10 |
|
| 11 | + "github.com/bytedance/sonic" |
11 | 12 | "github.com/maximhq/bifrost/core/internal/llmtests" |
12 | 13 | "github.com/maximhq/bifrost/core/providers/gemini" |
13 | 14 | "github.com/stretchr/testify/assert" |
@@ -432,6 +433,180 @@ func TestThoughtSignatureBypassSentinelRoundTripsThroughJSON(t *testing.T) { |
432 | 433 | assert.Equal(t, []byte("skip_thought_signature_validator"), decoded.ThoughtSignature) |
433 | 434 | } |
434 | 435 |
|
| 436 | +func TestGeminiGenerationRequestUnmarshalAcceptsSchemaIntegerConstraints(t *testing.T) { |
| 437 | + tests := []struct { |
| 438 | + name string |
| 439 | + body string |
| 440 | + }{ |
| 441 | + { |
| 442 | + name: "numeric constraints", |
| 443 | + body: `{ |
| 444 | + "contents": [{"role": "user", "parts": [{"text": "Search docs"}]}], |
| 445 | + "tools": [{ |
| 446 | + "functionDeclarations": [{ |
| 447 | + "name": "exa_web_search_exa", |
| 448 | + "description": "Search project docs", |
| 449 | + "parameters": { |
| 450 | + "type": "object", |
| 451 | + "minProperties": 1, |
| 452 | + "maxProperties": 3, |
| 453 | + "properties": { |
| 454 | + "query": {"type": "string", "description": "Search query", "minLength": 1, "maxLength": 100}, |
| 455 | + "tags": {"type": "array", "items": {"type": "string"}, "minItems": 1, "maxItems": 5} |
| 456 | + }, |
| 457 | + "required": ["query"] |
| 458 | + } |
| 459 | + }] |
| 460 | + }] |
| 461 | + }`, |
| 462 | + }, |
| 463 | + { |
| 464 | + name: "quoted constraints", |
| 465 | + body: `{ |
| 466 | + "contents": [{"role": "user", "parts": [{"text": "Search docs"}]}], |
| 467 | + "tools": [{ |
| 468 | + "functionDeclarations": [{ |
| 469 | + "name": "exa_web_search_exa", |
| 470 | + "description": "Search project docs", |
| 471 | + "parameters": { |
| 472 | + "type": "object", |
| 473 | + "minProperties": "1", |
| 474 | + "maxProperties": "3", |
| 475 | + "properties": { |
| 476 | + "query": {"type": "string", "description": "Search query", "minLength": "1", "maxLength": "100"}, |
| 477 | + "tags": {"type": "array", "items": {"type": "string"}, "minItems": "1", "maxItems": "5"} |
| 478 | + }, |
| 479 | + "required": ["query"] |
| 480 | + } |
| 481 | + }] |
| 482 | + }] |
| 483 | + }`, |
| 484 | + }, |
| 485 | + } |
| 486 | + |
| 487 | + for _, tt := range tests { |
| 488 | + t.Run(tt.name, func(t *testing.T) { |
| 489 | + var req gemini.GeminiGenerationRequest |
| 490 | + require.NoError(t, sonic.Unmarshal([]byte(tt.body), &req)) |
| 491 | + require.Len(t, req.Tools, 1) |
| 492 | + require.Len(t, req.Tools[0].FunctionDeclarations, 1) |
| 493 | + |
| 494 | + params := req.Tools[0].FunctionDeclarations[0].Parameters |
| 495 | + require.NotNil(t, params) |
| 496 | + require.NotNil(t, params.MinProperties) |
| 497 | + require.NotNil(t, params.MaxProperties) |
| 498 | + assert.Equal(t, int64(1), *params.MinProperties) |
| 499 | + assert.Equal(t, int64(3), *params.MaxProperties) |
| 500 | + |
| 501 | + query := params.Properties["query"] |
| 502 | + require.NotNil(t, query) |
| 503 | + require.NotNil(t, query.MinLength) |
| 504 | + require.NotNil(t, query.MaxLength) |
| 505 | + assert.Equal(t, int64(1), *query.MinLength) |
| 506 | + assert.Equal(t, int64(100), *query.MaxLength) |
| 507 | + |
| 508 | + tags := params.Properties["tags"] |
| 509 | + require.NotNil(t, tags) |
| 510 | + require.NotNil(t, tags.MinItems) |
| 511 | + require.NotNil(t, tags.MaxItems) |
| 512 | + assert.Equal(t, int64(1), *tags.MinItems) |
| 513 | + assert.Equal(t, int64(5), *tags.MaxItems) |
| 514 | + }) |
| 515 | + } |
| 516 | + |
| 517 | + t.Run("invalid string constraint", func(t *testing.T) { |
| 518 | + var req gemini.GeminiGenerationRequest |
| 519 | + err := sonic.Unmarshal([]byte(`{ |
| 520 | + "tools": [{ |
| 521 | + "functionDeclarations": [{ |
| 522 | + "name": "exa_web_search_exa", |
| 523 | + "parameters": { |
| 524 | + "type": "object", |
| 525 | + "properties": { |
| 526 | + "query": {"type": "string", "minLength": "many"} |
| 527 | + } |
| 528 | + } |
| 529 | + }] |
| 530 | + }] |
| 531 | + }`), &req) |
| 532 | + require.Error(t, err) |
| 533 | + assert.Contains(t, err.Error(), "invalid schema integer constraint") |
| 534 | + }) |
| 535 | + |
| 536 | + t.Run("max int64 numeric constraint", func(t *testing.T) { |
| 537 | + var req gemini.GeminiGenerationRequest |
| 538 | + require.NoError(t, sonic.Unmarshal([]byte(`{ |
| 539 | + "tools": [{ |
| 540 | + "functionDeclarations": [{ |
| 541 | + "name": "exa_web_search_exa", |
| 542 | + "parameters": { |
| 543 | + "type": "object", |
| 544 | + "properties": { |
| 545 | + "query": {"type": "string", "maxLength": 9223372036854775807} |
| 546 | + } |
| 547 | + } |
| 548 | + }] |
| 549 | + }] |
| 550 | + }`), &req)) |
| 551 | + |
| 552 | + query := req.Tools[0].FunctionDeclarations[0].Parameters.Properties["query"] |
| 553 | + require.NotNil(t, query.MaxLength) |
| 554 | + assert.Equal(t, int64(9223372036854775807), *query.MaxLength) |
| 555 | + }) |
| 556 | + |
| 557 | + t.Run("null constraint remains unset", func(t *testing.T) { |
| 558 | + var req gemini.GeminiGenerationRequest |
| 559 | + require.NoError(t, sonic.Unmarshal([]byte(`{ |
| 560 | + "tools": [{ |
| 561 | + "functionDeclarations": [{ |
| 562 | + "name": "exa_web_search_exa", |
| 563 | + "parameters": { |
| 564 | + "type": "object", |
| 565 | + "properties": { |
| 566 | + "query": {"type": "string", "minLength": null} |
| 567 | + } |
| 568 | + } |
| 569 | + }] |
| 570 | + }] |
| 571 | + }`), &req)) |
| 572 | + |
| 573 | + query := req.Tools[0].FunctionDeclarations[0].Parameters.Properties["query"] |
| 574 | + assert.Nil(t, query.MinLength) |
| 575 | + }) |
| 576 | + |
| 577 | + invalidConstraints := []struct { |
| 578 | + name string |
| 579 | + constraint string |
| 580 | + }{ |
| 581 | + {name: "float", constraint: `1.5`}, |
| 582 | + {name: "bool", constraint: `true`}, |
| 583 | + {name: "object", constraint: `{}`}, |
| 584 | + {name: "array", constraint: `[]`}, |
| 585 | + {name: "overflow string", constraint: `"9223372036854775808"`}, |
| 586 | + } |
| 587 | + |
| 588 | + for _, tt := range invalidConstraints { |
| 589 | + t.Run("invalid "+tt.name+" constraint", func(t *testing.T) { |
| 590 | + var req gemini.GeminiGenerationRequest |
| 591 | + err := sonic.Unmarshal([]byte(`{ |
| 592 | + "tools": [{ |
| 593 | + "functionDeclarations": [{ |
| 594 | + "name": "exa_web_search_exa", |
| 595 | + "parameters": { |
| 596 | + "type": "object", |
| 597 | + "properties": { |
| 598 | + "query": {"type": "string", "minLength": `+tt.constraint+`} |
| 599 | + } |
| 600 | + } |
| 601 | + }] |
| 602 | + }] |
| 603 | + }`), &req) |
| 604 | + require.Error(t, err) |
| 605 | + assert.Contains(t, err.Error(), "invalid schema integer constraint") |
| 606 | + }) |
| 607 | + } |
| 608 | +} |
| 609 | + |
435 | 610 | // parseToolParams parses fd.ParametersJSONSchema (raw JSON Schema passthrough) into a |
436 | 611 | // map for assertions. All tool conversion paths now use ParametersJSONSchema; fd.Parameters |
437 | 612 | // is always nil. |
|
0 commit comments