first commit
This commit is contained in:
238
core/providers/vertex/vertex_caching_test.go
Normal file
238
core/providers/vertex/vertex_caching_test.go
Normal file
@@ -0,0 +1,238 @@
|
||||
package vertex_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/maximhq/bifrost/core/providers/anthropic"
|
||||
"github.com/maximhq/bifrost/core/schemas"
|
||||
)
|
||||
|
||||
// TestVertex_AnthropicModel_CachingDeterminism verifies that Vertex's delegation
|
||||
// to anthropic.ToAnthropicChatRequest() produces deterministic JSON for prompt caching.
|
||||
// Two schemas with the same properties but different structural key order within
|
||||
// property definitions must produce byte-identical JSON after normalization.
|
||||
func TestVertex_AnthropicModel_CachingDeterminism(t *testing.T) {
|
||||
makeReq := func(props *schemas.OrderedMap) *schemas.BifrostChatRequest {
|
||||
return &schemas.BifrostChatRequest{
|
||||
Provider: schemas.Vertex,
|
||||
Model: "claude-sonnet-4-20250514",
|
||||
Input: []schemas.ChatMessage{{
|
||||
Role: schemas.ChatMessageRoleUser,
|
||||
Content: &schemas.ChatMessageContent{ContentStr: schemas.Ptr("test")},
|
||||
}},
|
||||
Params: &schemas.ChatParameters{
|
||||
Tools: []schemas.ChatTool{{
|
||||
Type: schemas.ChatToolTypeFunction,
|
||||
Function: &schemas.ChatToolFunction{
|
||||
Name: "test",
|
||||
Parameters: &schemas.ToolFunctionParameters{
|
||||
Type: "object",
|
||||
Properties: props,
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Version A: type before description
|
||||
propsA := schemas.NewOrderedMapFromPairs(
|
||||
schemas.KV("chain_of_thought", schemas.NewOrderedMapFromPairs(
|
||||
schemas.KV("type", "string"),
|
||||
schemas.KV("description", "Reasoning"),
|
||||
)),
|
||||
schemas.KV("answer", schemas.NewOrderedMapFromPairs(
|
||||
schemas.KV("type", "string"),
|
||||
schemas.KV("description", "The answer"),
|
||||
)),
|
||||
)
|
||||
|
||||
// Version B: description before type (different structural order)
|
||||
propsB := schemas.NewOrderedMapFromPairs(
|
||||
schemas.KV("chain_of_thought", schemas.NewOrderedMapFromPairs(
|
||||
schemas.KV("description", "Reasoning"),
|
||||
schemas.KV("type", "string"),
|
||||
)),
|
||||
schemas.KV("answer", schemas.NewOrderedMapFromPairs(
|
||||
schemas.KV("description", "The answer"),
|
||||
schemas.KV("type", "string"),
|
||||
)),
|
||||
)
|
||||
|
||||
// Vertex delegates Anthropic models to anthropic.ToAnthropicChatRequest()
|
||||
ctx, cancel := schemas.NewBifrostContextWithCancel(nil)
|
||||
defer cancel()
|
||||
resultA, err := anthropic.ToAnthropicChatRequest(ctx, makeReq(propsA))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
resultB, err := anthropic.ToAnthropicChatRequest(ctx, makeReq(propsB))
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
jsonA, err := schemas.Marshal(resultA.Tools[0].InputSchema)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal params A: %v", err)
|
||||
}
|
||||
jsonB, err := schemas.Marshal(resultB.Tools[0].InputSchema)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal params B: %v", err)
|
||||
}
|
||||
|
||||
// Caching: byte-identical JSON
|
||||
if string(jsonA) != string(jsonB) {
|
||||
t.Errorf("caching broken via Vertex→Anthropic path: same schema produced different JSON\nA: %s\nB: %s", jsonA, jsonB)
|
||||
}
|
||||
|
||||
// CoT: property order preserved
|
||||
keys := resultA.Tools[0].InputSchema.Properties.Keys()
|
||||
if len(keys) != 2 || keys[0] != "chain_of_thought" || keys[1] != "answer" {
|
||||
t.Errorf("expected property order [chain_of_thought, answer], got %v", keys)
|
||||
}
|
||||
}
|
||||
|
||||
// TestVertex_AnthropicModel_PreservesPropertyOrder verifies that the
|
||||
// Vertex→Anthropic delegation path preserves user-defined property ordering.
|
||||
func TestVertex_AnthropicModel_PreservesPropertyOrder(t *testing.T) {
|
||||
bifrostReq := &schemas.BifrostChatRequest{
|
||||
Provider: schemas.Vertex,
|
||||
Model: "claude-sonnet-4-20250514",
|
||||
Input: []schemas.ChatMessage{{
|
||||
Role: schemas.ChatMessageRoleUser,
|
||||
Content: &schemas.ChatMessageContent{ContentStr: schemas.Ptr("test")},
|
||||
}},
|
||||
Params: &schemas.ChatParameters{
|
||||
Tools: []schemas.ChatTool{{
|
||||
Type: schemas.ChatToolTypeFunction,
|
||||
Function: &schemas.ChatToolFunction{
|
||||
Name: "AnswerResponseModel",
|
||||
Description: schemas.Ptr("Extract answer"),
|
||||
Parameters: &schemas.ToolFunctionParameters{
|
||||
Type: "object",
|
||||
Properties: schemas.NewOrderedMapFromPairs(
|
||||
schemas.KV("chain_of_thought", schemas.NewOrderedMapFromPairs(schemas.KV("type", "string"))),
|
||||
schemas.KV("answer", schemas.NewOrderedMapFromPairs(schemas.KV("type", "string"))),
|
||||
schemas.KV("citations", schemas.NewOrderedMapFromPairs(schemas.KV("type", "array"))),
|
||||
schemas.KV("is_unanswered", schemas.NewOrderedMapFromPairs(schemas.KV("type", "boolean"))),
|
||||
),
|
||||
Required: []string{"answer", "is_unanswered"},
|
||||
},
|
||||
},
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
ctx, cancel := schemas.NewBifrostContextWithCancel(nil)
|
||||
defer cancel()
|
||||
result, err := anthropic.ToAnthropicChatRequest(ctx, bifrostReq)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
keys := result.Tools[0].InputSchema.Properties.Keys()
|
||||
expected := []string{"chain_of_thought", "answer", "citations", "is_unanswered"}
|
||||
if len(keys) != len(expected) {
|
||||
t.Fatalf("expected %d properties, got %d: %v", len(expected), len(keys), keys)
|
||||
}
|
||||
for i, k := range expected {
|
||||
if keys[i] != k {
|
||||
t.Errorf("property %d: expected %q, got %q (full order: %v)", i, k, keys[i], keys)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestVertex_ToolInputKeyOrderPreservation verifies that tool call arguments
|
||||
// preserve their original key ordering through the Vertex→Anthropic delegation path.
|
||||
// TestVertex_ToolInputKeyOrderPreservation verifies that Vertex→Anthropic delegation
|
||||
// preserves the original key ordering of tool call arguments for prompt caching.
|
||||
// Tests multiple parallel tool calls with different key orderings per block.
|
||||
func TestVertex_ToolInputKeyOrderPreservation(t *testing.T) {
|
||||
bifrostReq := &schemas.BifrostChatRequest{
|
||||
Provider: schemas.Vertex,
|
||||
Model: "claude-sonnet-4-20250514",
|
||||
Input: []schemas.ChatMessage{
|
||||
{
|
||||
Role: schemas.ChatMessageRoleUser,
|
||||
Content: &schemas.ChatMessageContent{ContentStr: schemas.Ptr("test")},
|
||||
},
|
||||
{
|
||||
Role: schemas.ChatMessageRoleAssistant,
|
||||
ChatAssistantMessage: &schemas.ChatAssistantMessage{
|
||||
ToolCalls: []schemas.ChatAssistantMessageToolCall{
|
||||
{
|
||||
Index: 0,
|
||||
Type: schemas.Ptr("function"),
|
||||
ID: schemas.Ptr("toolu_vrtx_001"),
|
||||
Function: schemas.ChatAssistantMessageToolCallFunction{
|
||||
Name: schemas.Ptr("bash"),
|
||||
Arguments: `{"description":"Find references quickly","timeout":30000,"command":"grep -r auth_injector ."}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Index: 1,
|
||||
Type: schemas.Ptr("function"),
|
||||
ID: schemas.Ptr("toolu_vrtx_002"),
|
||||
Function: schemas.ChatAssistantMessageToolCallFunction{
|
||||
Name: schemas.Ptr("bash"),
|
||||
Arguments: `{"command":"git diff main...HEAD --stat","description":"Show diff"}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
Index: 2,
|
||||
Type: schemas.Ptr("function"),
|
||||
ID: schemas.Ptr("toolu_vrtx_003"),
|
||||
Function: schemas.ChatAssistantMessageToolCallFunction{
|
||||
Name: schemas.Ptr("bash"),
|
||||
Arguments: `{"command":"git log main..HEAD","description":"Show commits"}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx, cancel := schemas.NewBifrostContextWithCancel(nil)
|
||||
defer cancel()
|
||||
result, err := anthropic.ToAnthropicChatRequest(ctx, bifrostReq)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
// Collect all tool_use content blocks
|
||||
var toolUseBlocks []struct{ jsonStr string }
|
||||
for _, msg := range result.Messages {
|
||||
for _, block := range msg.Content.ContentBlocks {
|
||||
if block.Type == "tool_use" {
|
||||
jsonBytes, _ := json.Marshal(block.Input)
|
||||
toolUseBlocks = append(toolUseBlocks, struct{ jsonStr string }{string(jsonBytes)})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(toolUseBlocks) != 3 {
|
||||
t.Fatalf("expected 3 tool_use blocks, got %d", len(toolUseBlocks))
|
||||
}
|
||||
|
||||
// Block 0: keys should be description, timeout, command (NOT alphabetical)
|
||||
s0 := toolUseBlocks[0].jsonStr
|
||||
if !(strings.Index(s0, "description") < strings.Index(s0, "timeout") &&
|
||||
strings.Index(s0, "timeout") < strings.Index(s0, "command")) {
|
||||
t.Errorf("block 0: key order not preserved, expected description < timeout < command in: %s", s0)
|
||||
}
|
||||
|
||||
// Block 1: keys should be command, description (NOT alphabetical)
|
||||
s1 := toolUseBlocks[1].jsonStr
|
||||
if !(strings.Index(s1, "command") < strings.Index(s1, "description")) {
|
||||
t.Errorf("block 1: key order not preserved, expected command < description in: %s", s1)
|
||||
}
|
||||
|
||||
// Block 2: keys should be command, description
|
||||
s2 := toolUseBlocks[2].jsonStr
|
||||
if !(strings.Index(s2, "command") < strings.Index(s2, "description")) {
|
||||
t.Errorf("block 2: key order not preserved, expected command < description in: %s", s2)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user