Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions internal/multiagent/eino_summarize.go
Original file line number Diff line number Diff line change
Expand Up @@ -300,9 +300,13 @@ func newEinoSummarizationModelOptions(outputReserve int, modelName, kind string,
if strings.TrimSpace(kind) != "" && kind != "classic" {
label = "eino " + kind + " summarization generate request"
}
tokenLimit := einoopenai.WithMaxCompletionTokens(outputReserve)
if oa != nil && isEinoAgenticClaudeProvider(oa.Provider) {
// Native Claude consumes the common option; OpenAI rejects both token fields together.
tokenLimit = model.WithMaxTokens(outputReserve)
}
return []model.Option{
model.WithMaxTokens(outputReserve),
einoopenai.WithMaxCompletionTokens(outputReserve),
tokenLimit,
einoopenai.WithExtraHeader(map[string]string{
copenai.SummarizationRequestHeader: "1",
}),
Expand Down
4 changes: 2 additions & 2 deletions internal/multiagent/eino_summarize_payload_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,9 @@ func TestStripReasoningFromSummarizationPayloadHonorsOpenAICompatProfileForNonDe
}
}

func TestEinoSummarizationModelOptionsSetCommonMaxTokens(t *testing.T) {
func TestEinoSummarizationModelOptionsSetClaudeCommonMaxTokens(t *testing.T) {
const outputReserve = 4096
opts := newEinoSummarizationModelOptions(outputReserve, "minimax-m3", "agentic", nil, nil)
opts := newEinoSummarizationModelOptions(outputReserve, "claude-sonnet", "agentic", &config.OpenAIConfig{Provider: "claude"}, nil)
common := model.GetCommonOptions(nil, opts...)
if common == nil || common.MaxTokens == nil {
t.Fatal("expected summarization options to set common max_tokens")
Expand Down
113 changes: 113 additions & 0 deletions internal/multiagent/eino_summarize_tokens_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package multiagent

import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"cyberstrike-ai/internal/config"

"github.com/cloudwego/eino-ext/components/model/agenticopenai"
einoopenai "github.com/cloudwego/eino-ext/components/model/openai"
"github.com/cloudwego/eino/schema"
)

func TestEinoSummarizationSendsOneTokenLimit(t *testing.T) {
for _, kind := range []string{"classic", "agentic"} {
t.Run(kind, func(t *testing.T) {
const outputReserve = 4096
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var body map[string]json.RawMessage
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Error(err)
w.WriteHeader(http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
if _, exists := body["max_tokens"]; exists {
t.Errorf("summary request includes max_tokens alongside max_completion_tokens: %s", body["max_tokens"])
w.WriteHeader(http.StatusBadRequest)
_, _ = w.Write([]byte(`{"error":{"message":"max_tokens and max_completion_tokens cannot be set at the same time","type":"invalid_request_error"}}`))
return
}
if string(body["max_completion_tokens"]) != "4096" {
t.Errorf("summary output budget = %s, want 4096", body["max_completion_tokens"])
}
_, _ = w.Write([]byte(`{"id":"summary","object":"chat.completion","model":"test-model","choices":[{"index":0,"message":{"role":"assistant","content":"summary"},"finish_reason":"stop"}]}`))
}))
defer server.Close()

ctx := context.Background()
oa := &config.OpenAIConfig{Provider: "openai_compatible", Model: "test-model"}
opts := newEinoSummarizationModelOptions(outputReserve, oa.Model, kind, oa, nil)
defaultLimit := 8192
if kind == "classic" {
chat, err := einoopenai.NewChatModel(ctx, &einoopenai.ChatModelConfig{
APIKey: "test", BaseURL: server.URL, Model: oa.Model, HTTPClient: server.Client(), MaxCompletionTokens: &defaultLimit,
})
if err != nil {
t.Fatal(err)
}
if _, err := chat.Generate(ctx, []*schema.Message{schema.UserMessage("summarize")}, opts...); err != nil {
t.Fatal(err)
}
} else {
chat, err := agenticopenai.NewChatModel(ctx, &agenticopenai.ChatConfig{
APIKey: "test", BaseURL: server.URL, Model: oa.Model, HTTPClient: server.Client(), MaxCompletionTokens: &defaultLimit,
})
if err != nil {
t.Fatal(err)
}
if _, err := chat.Generate(ctx, []*schema.AgenticMessage{schema.UserAgenticMessage("summarize")}, opts...); err != nil {
t.Fatal(err)
}
}
})
}
}

func TestEinoSummarizationPreservesClaudeTokenLimit(t *testing.T) {
for _, kind := range []string{"classic", "agentic"} {
t.Run(kind, func(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var body map[string]json.RawMessage
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Error(err)
w.WriteHeader(http.StatusBadRequest)
return
}
if string(body["max_tokens"]) != "4096" {
t.Errorf("Claude summary budget = %s, want 4096", body["max_tokens"])
}
if _, exists := body["max_completion_tokens"]; exists {
t.Error("Claude request includes OpenAI token limit")
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"id":"summary","type":"message","role":"assistant","model":"claude-sonnet","content":[{"type":"text","text":"summary"}],"stop_reason":"end_turn","usage":{"input_tokens":1,"output_tokens":1}}`))
}))
defer server.Close()
ctx := context.Background()
oa := config.OpenAIConfig{Provider: "claude", APIKey: "test", BaseURL: server.URL, Model: "claude-sonnet", MaxCompletionTokens: 8192}
opts := newEinoSummarizationModelOptions(4096, oa.Model, kind, &oa, nil)
if kind == "classic" {
chat, err := newEinoToolCallingChatModelFactory(server.Client(), nil, nil)(ctx, oa, einoModelModeNormal)
if err != nil {
t.Fatal(err)
}
if _, err := chat.Generate(ctx, []*schema.Message{schema.UserMessage("summarize")}, opts...); err != nil {
t.Fatal(err)
}
} else {
chat, err := newEinoAgenticChatModelFactory(server.Client(), nil, nil)(ctx, oa, einoModelModeNormal)
if err != nil {
t.Fatal(err)
}
if _, err := chat.Generate(ctx, []*schema.AgenticMessage{schema.UserAgenticMessage("summarize")}, opts...); err != nil {
t.Fatal(err)
}
}
})
}
}