up
This commit is contained in:
+173
@@ -189,3 +189,176 @@ func TestRunAgentToolLoopMaxIterations(t *testing.T) {
|
||||
t.Fatalf("unexpected last message: %#v", last)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildArkMessageImageTextOrder(t *testing.T) {
|
||||
msg, err := buildArkMessage(ChatMessage{Role: "user", Content: "请描述图片", ImageURL: "data:image/png;base64,aGVsbG8="})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if msg.Content == nil || len(msg.Content.ListValue) != 2 {
|
||||
t.Fatalf("unexpected content: %#v", msg.Content)
|
||||
}
|
||||
if msg.Content.ListValue[0].Type != model.ChatCompletionMessageContentPartTypeText || msg.Content.ListValue[0].Text != "请描述图片" {
|
||||
t.Fatalf("first part should be text: %#v", msg.Content.ListValue[0])
|
||||
}
|
||||
if msg.Content.ListValue[1].Type != model.ChatCompletionMessageContentPartTypeImageURL || msg.Content.ListValue[1].ImageURL == nil {
|
||||
t.Fatalf("second part should be image: %#v", msg.Content.ListValue[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildArkMessageImageOnly(t *testing.T) {
|
||||
msg, err := buildArkMessage(ChatMessage{Role: "user", ImageURL: "data:image/png;base64,aGVsbG8="})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if msg.Content == nil || len(msg.Content.ListValue) != 1 || msg.Content.ListValue[0].Type != model.ChatCompletionMessageContentPartTypeImageURL {
|
||||
t.Fatalf("unexpected content: %#v", msg.Content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestThinkTagParserSingleChunk(t *testing.T) {
|
||||
parser := &thinkTagParser{}
|
||||
visible, reasoning := parser.Accept("hello <think>abc</think> world")
|
||||
flushVisible, flushReasoning := parser.Flush()
|
||||
visible += flushVisible
|
||||
reasoning += flushReasoning
|
||||
if visible != "hello world" || reasoning != "abc" {
|
||||
t.Fatalf("visible=%q reasoning=%q", visible, reasoning)
|
||||
}
|
||||
}
|
||||
|
||||
func TestThinkTagParserAcrossChunks(t *testing.T) {
|
||||
parser := &thinkTagParser{}
|
||||
var visible, reasoning string
|
||||
for _, chunk := range []string{"hello <thi", "nk>abc</thi", "nk> world"} {
|
||||
v, r := parser.Accept(chunk)
|
||||
visible += v
|
||||
reasoning += r
|
||||
}
|
||||
v, r := parser.Flush()
|
||||
visible += v
|
||||
reasoning += r
|
||||
if visible != "hello world" || reasoning != "abc" {
|
||||
t.Fatalf("visible=%q reasoning=%q", visible, reasoning)
|
||||
}
|
||||
}
|
||||
|
||||
func TestThinkTagParserUnclosedThink(t *testing.T) {
|
||||
parser := &thinkTagParser{}
|
||||
visible, reasoning := parser.Accept("answer <think>still thinking")
|
||||
v, r := parser.Flush()
|
||||
visible += v
|
||||
reasoning += r
|
||||
if visible != "answer " || reasoning != "still thinking" {
|
||||
t.Fatalf("visible=%q reasoning=%q", visible, reasoning)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldParseThinkTags(t *testing.T) {
|
||||
if !shouldParseThinkTags(&OpenAIProfile{Config: OpenAIConfig{BaseURL: "http://127.0.0.1:11434/v1"}}) {
|
||||
t.Fatal("expected local ollama to parse think tags")
|
||||
}
|
||||
if shouldParseThinkTags(&OpenAIProfile{Config: OpenAIConfig{BaseURL: defaultOpenAIBaseURL}}) {
|
||||
t.Fatal("expected remote profile not to parse think tags by default")
|
||||
}
|
||||
falseValue := false
|
||||
if shouldParseThinkTags(&OpenAIProfile{Config: OpenAIConfig{BaseURL: "http://127.0.0.1:11434/v1", ParseThinkTags: &falseValue}}) {
|
||||
t.Fatal("explicit false should disable think parsing")
|
||||
}
|
||||
trueValue := true
|
||||
if !shouldParseThinkTags(&OpenAIProfile{Config: OpenAIConfig{BaseURL: defaultOpenAIBaseURL, ParseThinkTags: &trueValue}}) {
|
||||
t.Fatal("explicit true should enable think parsing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildToolDecisionMessagesRemovesImages(t *testing.T) {
|
||||
messages, err := buildToolDecisionMessages([]ChatMessage{{Role: "user", Content: "描述这张图", ImageURL: "data:image/png;base64,aGVsbG8="}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(messages) != 1 || messages[0].Content == nil || messages[0].Content.StringValue == nil {
|
||||
t.Fatalf("unexpected messages: %#v", messages)
|
||||
}
|
||||
if !strings.Contains(*messages[0].Content.StringValue, "工具判断阶段不读取图片内容") {
|
||||
t.Fatalf("missing image placeholder: %q", *messages[0].Content.StringValue)
|
||||
}
|
||||
if messages[0].Content.ListValue != nil {
|
||||
t.Fatalf("decision message should be text-only: %#v", messages[0].Content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunAgentToolLoopImageUsesTextOnlyDecisionMessages(t *testing.T) {
|
||||
oldRouter := toolRouterState
|
||||
defer func() { toolRouterState = oldRouter }()
|
||||
|
||||
toolRouterState = &ToolRouterState{cfg: &ToolRouterConfig{
|
||||
Enabled: true,
|
||||
Timeout: 1,
|
||||
MaxTokens: 128,
|
||||
SystemPrompt: "use tools",
|
||||
Tools: []ToolRouteConfig{{Name: "time", Enabled: true}},
|
||||
}}
|
||||
toolRouterState.complete = func(ctx context.Context, profile *OpenAIProfile, req model.CreateChatCompletionRequest, timeout time.Duration) (model.ChatCompletionResponse, error) {
|
||||
for _, msg := range req.Messages {
|
||||
if msg.Content != nil && len(msg.Content.ListValue) > 0 {
|
||||
t.Fatalf("tool decision should not receive multimodal content: %#v", msg.Content)
|
||||
}
|
||||
}
|
||||
joined := ""
|
||||
for _, msg := range req.Messages {
|
||||
if msg.Content != nil && msg.Content.StringValue != nil {
|
||||
joined += *msg.Content.StringValue
|
||||
}
|
||||
}
|
||||
if !strings.Contains(joined, "工具判断阶段不读取图片内容") {
|
||||
t.Fatalf("missing placeholder in decision messages: %q", joined)
|
||||
}
|
||||
return model.ChatCompletionResponse{Choices: []*model.ChatCompletionChoice{{Message: model.ChatCompletionMessage{Content: stringContent("no tool")}}}}, nil
|
||||
}
|
||||
|
||||
messages, err := runAgentToolLoop(context.Background(), &OpenAIProfile{Config: OpenAIConfig{Model: "chat"}}, []ChatMessage{{Role: "user", Content: "描述这张图", ImageURL: "data:image/png;base64,aGVsbG8="}}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
foundImage := false
|
||||
for _, msg := range messages {
|
||||
if msg.Content != nil && len(msg.Content.ListValue) > 0 {
|
||||
foundImage = true
|
||||
}
|
||||
}
|
||||
if !foundImage {
|
||||
t.Fatalf("final messages should retain image: %#v", messages)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunAgentToolLoopUsesConfiguredRouterProfile(t *testing.T) {
|
||||
oldRouter := toolRouterState
|
||||
defer func() { toolRouterState = oldRouter }()
|
||||
|
||||
ai, err := NewOpenAIState([]OpenAIConfig{
|
||||
{Name: "chat", APIKey: "key", BaseURL: defaultOpenAIBaseURL, Model: "chat-model", Timeout: 1, Active: true},
|
||||
{Name: "router", APIKey: "key", BaseURL: defaultOpenAIBaseURL, Model: "router-model", Timeout: 1},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
toolRouterState = &ToolRouterState{ai: ai, cfg: &ToolRouterConfig{
|
||||
Enabled: true,
|
||||
OpenAIName: "router",
|
||||
Timeout: 1,
|
||||
MaxTokens: 128,
|
||||
SystemPrompt: "use tools",
|
||||
Tools: []ToolRouteConfig{{Name: "time", Enabled: true}},
|
||||
}}
|
||||
toolRouterState.complete = func(ctx context.Context, profile *OpenAIProfile, req model.CreateChatCompletionRequest, timeout time.Duration) (model.ChatCompletionResponse, error) {
|
||||
if profile.Config.Name != "router" || req.Model != "router-model" {
|
||||
t.Fatalf("router profile not used: profile=%s model=%s", profile.Config.Name, req.Model)
|
||||
}
|
||||
return model.ChatCompletionResponse{Choices: []*model.ChatCompletionChoice{{Message: model.ChatCompletionMessage{Content: stringContent("no tool")}}}}, nil
|
||||
}
|
||||
|
||||
_, err = runAgentToolLoop(context.Background(), &OpenAIProfile{Config: OpenAIConfig{Name: "chat", Model: "chat-model"}}, []ChatMessage{{Role: "user", Content: "今天"}}, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user