first commit
This commit is contained in:
306
plugins/semanticcache/plugin_nil_content_test.go
Normal file
306
plugins/semanticcache/plugin_nil_content_test.go
Normal file
@@ -0,0 +1,306 @@
|
||||
package semanticcache
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
bifrost "github.com/maximhq/bifrost/core"
|
||||
"github.com/maximhq/bifrost/core/schemas"
|
||||
)
|
||||
|
||||
// TestExtractTextForEmbedding_NilContent verifies that extractTextForEmbedding
|
||||
// does not panic when chat messages have nil Content (e.g., assistant tool-call messages).
|
||||
func TestExtractTextForEmbedding_NilContent(t *testing.T) {
|
||||
plugin := &Plugin{
|
||||
config: &Config{},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
request *schemas.BifrostRequest
|
||||
}{
|
||||
{
|
||||
name: "ChatRequest with nil Content in assistant tool-call message",
|
||||
request: &schemas.BifrostRequest{
|
||||
RequestType: schemas.ChatCompletionRequest,
|
||||
ChatRequest: &schemas.BifrostChatRequest{
|
||||
Provider: schemas.OpenAI,
|
||||
Model: "gpt-4o-mini",
|
||||
Input: []schemas.ChatMessage{
|
||||
{
|
||||
Role: schemas.ChatMessageRoleUser,
|
||||
Content: &schemas.ChatMessageContent{
|
||||
ContentStr: bifrost.Ptr("Call the get_weather function"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Role: schemas.ChatMessageRoleAssistant,
|
||||
Content: nil, // tool-call message with no content
|
||||
ChatAssistantMessage: &schemas.ChatAssistantMessage{
|
||||
ToolCalls: []schemas.ChatAssistantMessageToolCall{
|
||||
{
|
||||
ID: bifrost.Ptr("call_123"),
|
||||
Type: bifrost.Ptr("function"),
|
||||
Function: schemas.ChatAssistantMessageToolCallFunction{
|
||||
Name: bifrost.Ptr("get_weather"),
|
||||
Arguments: `{"location": "San Francisco"}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Params: &schemas.ChatParameters{
|
||||
Temperature: bifrost.Ptr(0.7),
|
||||
MaxCompletionTokens: bifrost.Ptr(100),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ChatRequest where all messages have nil Content",
|
||||
request: &schemas.BifrostRequest{
|
||||
RequestType: schemas.ChatCompletionRequest,
|
||||
ChatRequest: &schemas.BifrostChatRequest{
|
||||
Provider: schemas.OpenAI,
|
||||
Model: "gpt-4o-mini",
|
||||
Input: []schemas.ChatMessage{
|
||||
{
|
||||
Role: schemas.ChatMessageRoleAssistant,
|
||||
Content: nil,
|
||||
},
|
||||
},
|
||||
Params: &schemas.ChatParameters{
|
||||
Temperature: bifrost.Ptr(0.7),
|
||||
MaxCompletionTokens: bifrost.Ptr(100),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ResponsesRequest with nil Content",
|
||||
request: &schemas.BifrostRequest{
|
||||
RequestType: schemas.ResponsesRequest,
|
||||
ResponsesRequest: createResponsesRequestWithNilContent(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// This should not panic
|
||||
text, hash, err := plugin.extractTextForEmbedding(tt.request)
|
||||
// We don't care about the error — the important thing is no panic
|
||||
t.Logf("text=%q, hash=%q, err=%v", text, hash, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareDirectCacheLookup_ResponsesStreamRequest(t *testing.T) {
|
||||
plugin := &Plugin{
|
||||
config: getDefaultTestConfig(),
|
||||
logger: bifrost.NewDefaultLogger(schemas.LogLevelDebug),
|
||||
}
|
||||
|
||||
req := &schemas.BifrostRequest{
|
||||
RequestType: schemas.ResponsesStreamRequest,
|
||||
ResponsesRequest: CreateStreamingResponsesRequest("Explain cache invalidation", 0.2, 200),
|
||||
}
|
||||
|
||||
ctx := CreateContextWithCacheKey("responses-stream-direct")
|
||||
directID, err := plugin.prepareDirectCacheLookup(ctx, req, "responses-stream-direct")
|
||||
if err != nil {
|
||||
t.Fatalf("prepareDirectCacheLookup failed: %v", err)
|
||||
}
|
||||
if directID == "" {
|
||||
t.Fatal("expected deterministic direct cache id")
|
||||
}
|
||||
if got, _ := ctx.Value(requestHashKey).(string); got == "" {
|
||||
t.Fatal("expected request hash to be stored in context")
|
||||
}
|
||||
if got, _ := ctx.Value(requestParamsHashKey).(string); got == "" {
|
||||
t.Fatal("expected params hash to be stored in context")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareDirectCacheLookup_UnsupportedRequestTypeFailsClosed(t *testing.T) {
|
||||
plugin := &Plugin{
|
||||
config: getDefaultTestConfig(),
|
||||
logger: bifrost.NewDefaultLogger(schemas.LogLevelDebug),
|
||||
}
|
||||
|
||||
req := &schemas.BifrostRequest{
|
||||
RequestType: schemas.PassthroughRequest,
|
||||
PassthroughRequest: &schemas.BifrostPassthroughRequest{
|
||||
Provider: schemas.OpenAI,
|
||||
Model: "gpt-4o-mini",
|
||||
Method: "GET",
|
||||
Path: "/v1/models",
|
||||
},
|
||||
}
|
||||
|
||||
ctx := CreateContextWithCacheKey("unsupported-direct")
|
||||
directID, err := plugin.prepareDirectCacheLookup(ctx, req, "unsupported-direct")
|
||||
if err == nil {
|
||||
t.Fatal("expected prepareDirectCacheLookup to reject unsupported request type")
|
||||
}
|
||||
if directID != "" {
|
||||
t.Fatalf("expected no direct cache id, got %q", directID)
|
||||
}
|
||||
if got, _ := ctx.Value(requestHashKey).(string); got != "" {
|
||||
t.Fatalf("expected request hash to remain unset, got %q", got)
|
||||
}
|
||||
if got, _ := ctx.Value(requestParamsHashKey).(string); got != "" {
|
||||
t.Fatalf("expected params hash to remain unset, got %q", got)
|
||||
}
|
||||
if got, _ := ctx.Value(requestStorageIDKey).(string); got != "" {
|
||||
t.Fatalf("expected storage id to remain unset, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreLLMHookSkipsUnsupportedCountTokensRequest(t *testing.T) {
|
||||
plugin := &Plugin{
|
||||
config: getDefaultTestConfig(),
|
||||
logger: bifrost.NewDefaultLogger(schemas.LogLevelDebug),
|
||||
}
|
||||
|
||||
req := &schemas.BifrostRequest{
|
||||
RequestType: schemas.CountTokensRequest,
|
||||
CountTokensRequest: &schemas.BifrostResponsesRequest{
|
||||
Provider: schemas.Anthropic,
|
||||
Model: "claude-sonnet-4-5",
|
||||
Input: []schemas.ResponsesMessage{
|
||||
{
|
||||
Role: bifrost.Ptr(schemas.ResponsesInputMessageRoleUser),
|
||||
Content: &schemas.ResponsesMessageContent{
|
||||
ContentStr: bifrost.Ptr("How many tokens is this message?"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ctx := CreateContextWithCacheKey("count-tokens-test")
|
||||
ctx.SetValue(requestIDKey, "stale-request-id")
|
||||
ctx.SetValue(requestStorageIDKey, "stale-storage-id")
|
||||
ctx.SetValue(requestHashKey, "stale-request-hash")
|
||||
ctx.SetValue(requestParamsHashKey, "stale-params-hash")
|
||||
ctx.SetValue(requestModelKey, "stale-model")
|
||||
ctx.SetValue(requestProviderKey, schemas.OpenAI)
|
||||
ctx.SetValue(requestEmbeddingKey, []float32{1, 2, 3})
|
||||
ctx.SetValue(requestEmbeddingTokensKey, 99)
|
||||
ctx.SetValue(isCacheHitKey, true)
|
||||
ctx.SetValue(cacheHitTypeKey, CacheTypeDirect)
|
||||
|
||||
modifiedReq, shortCircuit, err := plugin.PreLLMHook(ctx, req)
|
||||
if err != nil {
|
||||
t.Fatalf("PreLLMHook failed: %v", err)
|
||||
}
|
||||
if modifiedReq != req {
|
||||
t.Fatal("expected original request to be returned unchanged")
|
||||
}
|
||||
if shortCircuit != nil {
|
||||
t.Fatal("expected no short-circuit for unsupported count tokens request")
|
||||
}
|
||||
if got, _ := ctx.Value(requestIDKey).(string); got != "" {
|
||||
t.Fatalf("expected requestIDKey to remain unset, got %q", got)
|
||||
}
|
||||
if got, _ := ctx.Value(requestHashKey).(string); got != "" {
|
||||
t.Fatalf("expected requestHashKey to remain unset, got %q", got)
|
||||
}
|
||||
if got, _ := ctx.Value(requestParamsHashKey).(string); got != "" {
|
||||
t.Fatalf("expected requestParamsHashKey to remain unset, got %q", got)
|
||||
}
|
||||
if got, _ := ctx.Value(requestStorageIDKey).(string); got != "" {
|
||||
t.Fatalf("expected requestStorageIDKey to remain unset, got %q", got)
|
||||
}
|
||||
if got, _ := ctx.Value(requestModelKey).(string); got != "" {
|
||||
t.Fatalf("expected requestModelKey to remain unset, got %q", got)
|
||||
}
|
||||
if got, ok := ctx.Value(requestProviderKey).(schemas.ModelProvider); ok && got != "" {
|
||||
t.Fatalf("expected requestProviderKey to remain unset, got %q", got)
|
||||
}
|
||||
if got := ctx.Value(requestEmbeddingKey); got != nil {
|
||||
t.Fatalf("expected requestEmbeddingKey to remain unset, got %#v", got)
|
||||
}
|
||||
if got, ok := ctx.Value(requestEmbeddingTokensKey).(int); ok && got != 0 {
|
||||
t.Fatalf("expected requestEmbeddingTokensKey to remain unset, got %d", got)
|
||||
}
|
||||
if got, ok := ctx.Value(isCacheHitKey).(bool); ok && got {
|
||||
t.Fatal("expected isCacheHitKey to remain unset")
|
||||
}
|
||||
if got, ok := ctx.Value(cacheHitTypeKey).(CacheType); ok && got != "" {
|
||||
t.Fatalf("expected cacheHitTypeKey to remain unset, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetNormalizedInputForCaching_NilContent verifies that getNormalizedInputForCaching
|
||||
// does not panic when chat messages have nil Content.
|
||||
func TestGetNormalizedInputForCaching_NilContent(t *testing.T) {
|
||||
plugin := &Plugin{
|
||||
config: &Config{},
|
||||
}
|
||||
|
||||
request := &schemas.BifrostRequest{
|
||||
RequestType: schemas.ChatCompletionRequest,
|
||||
ChatRequest: &schemas.BifrostChatRequest{
|
||||
Provider: schemas.OpenAI,
|
||||
Model: "gpt-4o-mini",
|
||||
Input: []schemas.ChatMessage{
|
||||
{
|
||||
Role: schemas.ChatMessageRoleUser,
|
||||
Content: &schemas.ChatMessageContent{
|
||||
ContentStr: bifrost.Ptr("Call the get_weather function"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Role: schemas.ChatMessageRoleAssistant,
|
||||
Content: nil,
|
||||
ChatAssistantMessage: &schemas.ChatAssistantMessage{
|
||||
ToolCalls: []schemas.ChatAssistantMessageToolCall{
|
||||
{
|
||||
ID: bifrost.Ptr("call_123"),
|
||||
Type: bifrost.Ptr("function"),
|
||||
Function: schemas.ChatAssistantMessageToolCallFunction{
|
||||
Name: bifrost.Ptr("get_weather"),
|
||||
Arguments: `{"location": "San Francisco"}`,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Params: &schemas.ChatParameters{
|
||||
Temperature: bifrost.Ptr(0.7),
|
||||
MaxCompletionTokens: bifrost.Ptr(100),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// This should not panic
|
||||
result := plugin.getNormalizedInputForCaching(request)
|
||||
t.Logf("result type: %T", result)
|
||||
}
|
||||
|
||||
// createResponsesRequestWithNilContent builds a BifrostResponsesRequest with a nil Content message for testing.
|
||||
func createResponsesRequestWithNilContent() *schemas.BifrostResponsesRequest {
|
||||
return &schemas.BifrostResponsesRequest{
|
||||
Provider: schemas.OpenAI,
|
||||
Model: "gpt-4o-mini",
|
||||
Input: []schemas.ResponsesMessage{
|
||||
{
|
||||
Role: bifrost.Ptr(schemas.ResponsesInputMessageRoleUser),
|
||||
Content: &schemas.ResponsesMessageContent{
|
||||
ContentStr: bifrost.Ptr("Hello"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Role: bifrost.Ptr(schemas.ResponsesInputMessageRoleAssistant),
|
||||
Content: nil,
|
||||
},
|
||||
},
|
||||
Params: &schemas.ResponsesParameters{
|
||||
Temperature: bifrost.Ptr(0.7),
|
||||
MaxOutputTokens: bifrost.Ptr(100),
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user