Add files via upload

This commit is contained in:
公明
2026-08-18 20:34:53 +08:00
committed by GitHub
parent 98c380ed5f
commit 5aa2c382b0
94 changed files with 27142 additions and 0 deletions
+125
View File
@@ -0,0 +1,125 @@
package llm
import (
"context"
"fmt"
"github.com/cloudwego/eino/components/model"
"github.com/cloudwego/eino/schema"
)
// AgenticChatModelAdapter exposes a text-oriented AgenticModel as a classic
// BaseChatModel for Eino components that have not adopted AgenticMessage yet.
// It adapts only Eino's in-memory message shape; no HTTP protocol is translated.
type AgenticChatModelAdapter struct {
model model.AgenticModel
tools []*schema.ToolInfo
}
func NewAgenticChatModelAdapter(agenticModel model.AgenticModel) model.ChatModel {
return &AgenticChatModelAdapter{model: agenticModel}
}
func (a *AgenticChatModelAdapter) BindTools(tools []*schema.ToolInfo) error {
if a == nil || a.model == nil {
return fmt.Errorf("agentic chat adapter: model is nil")
}
a.tools = append([]*schema.ToolInfo(nil), tools...)
return nil
}
func (a *AgenticChatModelAdapter) Generate(
ctx context.Context,
input []*schema.Message,
opts ...model.Option,
) (*schema.Message, error) {
if a == nil || a.model == nil {
return nil, fmt.Errorf("agentic chat adapter: model is nil")
}
out, err := a.model.Generate(ctx, classicMessagesToAgentic(input), commonAgenticOptions(a.tools, opts...)...)
if err != nil {
return nil, err
}
return agenticMessageToClassic(out), nil
}
func (a *AgenticChatModelAdapter) Stream(
ctx context.Context,
input []*schema.Message,
opts ...model.Option,
) (*schema.StreamReader[*schema.Message], error) {
if a == nil || a.model == nil {
return nil, fmt.Errorf("agentic chat adapter: model is nil")
}
stream, err := a.model.Stream(ctx, classicMessagesToAgentic(input), commonAgenticOptions(a.tools, opts...)...)
if err != nil {
return nil, err
}
return schema.StreamReaderWithConvert(stream, func(msg *schema.AgenticMessage) (*schema.Message, error) {
return agenticMessageToClassic(msg), nil
}), nil
}
func classicMessagesToAgentic(input []*schema.Message) []*schema.AgenticMessage {
out := make([]*schema.AgenticMessage, 0, len(input))
for _, msg := range input {
if msg == nil {
continue
}
role := schema.AgenticRoleTypeUser
switch msg.Role {
case schema.System:
role = schema.AgenticRoleTypeSystem
case schema.Assistant:
role = schema.AgenticRoleTypeAssistant
}
agentic := &schema.AgenticMessage{Role: role}
if msg.Role == schema.Assistant {
if msg.Content != "" {
agentic.ContentBlocks = append(agentic.ContentBlocks, schema.NewContentBlock(&schema.AssistantGenText{Text: msg.Content}))
}
} else if msg.Content != "" {
agentic.ContentBlocks = append(agentic.ContentBlocks, schema.NewContentBlock(&schema.UserInputText{Text: msg.Content}))
}
out = append(out, agentic)
}
return out
}
func agenticMessageToClassic(msg *schema.AgenticMessage) *schema.Message {
if msg == nil {
return nil
}
content, reasoning := AgenticText(msg)
return &schema.Message{
Role: schema.Assistant,
Content: content,
ReasoningContent: reasoning,
}
}
func commonAgenticOptions(boundTools []*schema.ToolInfo, opts ...model.Option) []model.Option {
common := model.GetCommonOptions(&model.Options{
Tools: append([]*schema.ToolInfo(nil), boundTools...),
}, opts...)
out := make([]model.Option, 0, 6)
if common.Temperature != nil {
out = append(out, model.WithTemperature(*common.Temperature))
}
if common.Model != nil {
out = append(out, model.WithModel(*common.Model))
}
if common.TopP != nil {
out = append(out, model.WithTopP(*common.TopP))
}
if common.MaxTokens != nil {
out = append(out, model.WithMaxTokens(*common.MaxTokens))
}
if len(common.Stop) > 0 {
out = append(out, model.WithStop(common.Stop))
}
if common.Tools != nil {
out = append(out, model.WithTools(common.Tools))
}
return out
}
+60
View File
@@ -0,0 +1,60 @@
package llm
import (
"context"
"net/http"
"strings"
"cyberstrike-ai/internal/config"
agenticclaude "github.com/cloudwego/eino-ext/components/model/agenticclaude"
"github.com/cloudwego/eino/components/model"
"github.com/cloudwego/eino/schema"
)
func IsClaudeProvider(provider string) bool {
provider = strings.ToLower(strings.TrimSpace(provider))
return provider == "claude" || provider == "anthropic"
}
func NewClaudeAgenticModel(
ctx context.Context,
cfg config.OpenAIConfig,
httpClient *http.Client,
maxTokens int,
extraFields map[string]any,
) (model.AgenticModel, error) {
if maxTokens <= 0 {
maxTokens = cfg.MaxCompletionTokensEffective()
}
if cfg.IsDeepSeekEndpointOrModel() {
httpClient = newDeepSeekAnthropicCompatibleClient(httpClient)
}
return agenticclaude.New(ctx, &agenticclaude.Config{
APIKey: strings.TrimSpace(cfg.APIKey),
BaseURL: strings.TrimSuffix(strings.TrimSpace(cfg.BaseURL), "/"),
Model: strings.TrimSpace(cfg.Model),
MaxTokens: maxTokens,
HTTPClient: httpClient,
ExtraFields: extraFields,
})
}
func AgenticText(msg *schema.AgenticMessage) (content, reasoning string) {
if msg == nil {
return "", ""
}
var contentParts, reasoningParts []string
for _, block := range msg.ContentBlocks {
if block == nil {
continue
}
switch {
case block.AssistantGenText != nil:
contentParts = append(contentParts, block.AssistantGenText.Text)
case block.Reasoning != nil:
reasoningParts = append(reasoningParts, block.Reasoning.Text)
}
}
return strings.Join(contentParts, ""), strings.Join(reasoningParts, "")
}
+76
View File
@@ -0,0 +1,76 @@
package llm
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
// newDeepSeekAnthropicCompatibleClient compensates for DeepSeek's Anthropic
// endpoint lagging behind the current Anthropic SDK. The SDK emits
// {"type":"custom"} for function tools, while DeepSeek expects the older
// name/input_schema/description shape without that discriminator.
//
// This is a field-level compatibility fix; requests still originate from
// Eino's native agenticclaude model and remain Anthropic Messages API requests.
func newDeepSeekAnthropicCompatibleClient(base *http.Client) *http.Client {
if base == nil {
base = http.DefaultClient
}
cloned := *base
transport := base.Transport
if transport == nil {
transport = http.DefaultTransport
}
cloned.Transport = &deepSeekAnthropicCompatRoundTripper{base: transport}
return &cloned
}
type deepSeekAnthropicCompatRoundTripper struct {
base http.RoundTripper
}
func (rt *deepSeekAnthropicCompatRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
if req == nil || req.Body == nil || req.Method != http.MethodPost {
return rt.base.RoundTrip(req)
}
body, err := io.ReadAll(req.Body)
if err != nil {
return nil, fmt.Errorf("read DeepSeek Anthropic request: %w", err)
}
_ = req.Body.Close()
var payload map[string]any
if err := json.Unmarshal(body, &payload); err != nil {
req.Body = io.NopCloser(bytes.NewReader(body))
return rt.base.RoundTrip(req)
}
tools, ok := payload["tools"].([]any)
if !ok {
req.Body = io.NopCloser(bytes.NewReader(body))
return rt.base.RoundTrip(req)
}
changed := false
for _, rawTool := range tools {
tool, ok := rawTool.(map[string]any)
if !ok || tool["type"] != "custom" {
continue
}
delete(tool, "type")
changed = true
}
if changed {
body, err = json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("marshal DeepSeek Anthropic request: %w", err)
}
}
req.Body = io.NopCloser(bytes.NewReader(body))
req.ContentLength = int64(len(body))
req.GetBody = func() (io.ReadCloser, error) {
return io.NopCloser(bytes.NewReader(body)), nil
}
return rt.base.RoundTrip(req)
}
@@ -0,0 +1,55 @@
package llm
import (
"bytes"
"io"
"net/http"
"strings"
"testing"
)
type captureRoundTripper struct {
body string
}
func (rt *captureRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
body, err := io.ReadAll(req.Body)
if err != nil {
return nil, err
}
rt.body = string(body)
return &http.Response{
StatusCode: http.StatusOK,
Header: make(http.Header),
Body: io.NopCloser(bytes.NewReader(nil)),
Request: req,
}, nil
}
func TestDeepSeekAnthropicCompatStripsOnlyCustomToolType(t *testing.T) {
t.Parallel()
capture := &captureRoundTripper{}
client := newDeepSeekAnthropicCompatibleClient(&http.Client{Transport: capture})
req, err := http.NewRequest(
http.MethodPost,
"https://api.deepseek.com/anthropic/v1/messages",
strings.NewReader(`{"tools":[{"type":"custom","name":"mcp_tool","input_schema":{"type":"object"}},{"type":"web_search_20260209","name":"web_search"}]}`),
)
if err != nil {
t.Fatalf("NewRequest: %v", err)
}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("Do: %v", err)
}
_ = resp.Body.Close()
if strings.Contains(capture.body, `"type":"custom"`) {
t.Fatalf("custom discriminator was not removed: %s", capture.body)
}
if !strings.Contains(capture.body, `"type":"web_search_20260209"`) {
t.Fatalf("server tool discriminator was removed: %s", capture.body)
}
if !strings.Contains(capture.body, `"name":"mcp_tool"`) {
t.Fatalf("custom tool definition was removed: %s", capture.body)
}
}