Implement support for Anthropic connectors in the LLM and sandbox components
- Add handling for Anthropic connector types in the sandbox, allowing direct connections without a proxy. - Enhance capability retrieval to support both OpenAI and Anthropic formats, ensuring a unified interface. - Update the executor and command logic to differentiate between OpenAI and Anthropic configurations, streamlining environment setup. - Modify the provider selection logic to accommodate Anthropic capabilities, improving flexibility in LLM provider management. - Refactor API detection to include Anthropic, ensuring accurate identification of connector types.
This commit is contained in:
parent
ad311cad2a
commit
650a002a5a
10 changed files with 1424 additions and 18 deletions
|
|
@ -9,6 +9,7 @@ import (
|
|||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/yaoapp/gou/connector"
|
||||
gouMCP "github.com/yaoapp/gou/mcp"
|
||||
mcpProcess "github.com/yaoapp/gou/mcp/process"
|
||||
"github.com/yaoapp/yao/agent/context"
|
||||
|
|
@ -257,6 +258,14 @@ func (ast *Assistant) buildSandboxOptions(ctx *context.Context, opts *context.Op
|
|||
return nil, fmt.Errorf("failed to get connector: %w", err)
|
||||
}
|
||||
|
||||
// Determine connector type for sandbox proxy behavior
|
||||
// Anthropic connectors bypass the proxy (Claude CLI connects directly)
|
||||
if conn.Is(connector.ANTHROPIC) {
|
||||
execOpts.ConnectorType = "anthropic"
|
||||
} else {
|
||||
execOpts.ConnectorType = "openai"
|
||||
}
|
||||
|
||||
setting := conn.Setting()
|
||||
if host, ok := setting["host"].(string); ok {
|
||||
execOpts.ConnectorHost = host
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package llm
|
|||
|
||||
import (
|
||||
"github.com/yaoapp/gou/connector"
|
||||
"github.com/yaoapp/gou/connector/anthropic"
|
||||
"github.com/yaoapp/gou/connector/openai"
|
||||
)
|
||||
|
||||
|
|
@ -68,6 +69,14 @@ func GetCapabilitiesFromConn(conn connector.Connector, modelCapabilities map[str
|
|||
if capabilities, ok := caps.(openai.Capabilities); ok {
|
||||
return &capabilities
|
||||
}
|
||||
// Try to convert from *anthropic.Capabilities
|
||||
if capabilities, ok := caps.(*anthropic.Capabilities); ok {
|
||||
return convertAnthropicCaps(capabilities)
|
||||
}
|
||||
// Try to convert from anthropic.Capabilities (value type)
|
||||
if capabilities, ok := caps.(anthropic.Capabilities); ok {
|
||||
return convertAnthropicCaps(&capabilities)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -125,3 +134,21 @@ func ToMap(caps *openai.Capabilities) map[string]interface{} {
|
|||
|
||||
return result
|
||||
}
|
||||
|
||||
// convertAnthropicCaps converts anthropic.Capabilities to openai.Capabilities
|
||||
// This provides a unified capabilities interface across connector types
|
||||
func convertAnthropicCaps(caps *anthropic.Capabilities) *openai.Capabilities {
|
||||
if caps == nil {
|
||||
return getDefaultCapabilities()
|
||||
}
|
||||
return &openai.Capabilities{
|
||||
Vision: caps.Vision,
|
||||
Audio: caps.Audio,
|
||||
ToolCalls: caps.ToolCalls,
|
||||
Reasoning: caps.Reasoning,
|
||||
Streaming: caps.Streaming,
|
||||
JSON: caps.JSON,
|
||||
Multimodal: caps.Multimodal,
|
||||
TemperatureAdjustable: caps.TemperatureAdjustable,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1173
agent/llm/providers/anthropic/anthropic.go
Normal file
1173
agent/llm/providers/anthropic/anthropic.go
Normal file
File diff suppressed because it is too large
Load diff
154
agent/llm/providers/anthropic/types.go
Normal file
154
agent/llm/providers/anthropic/types.go
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
package anthropic
|
||||
|
||||
import (
|
||||
"github.com/yaoapp/yao/agent/output/message"
|
||||
)
|
||||
|
||||
// ============================================================
|
||||
// Anthropic Messages API types
|
||||
// Reference: https://docs.anthropic.com/en/api/messages
|
||||
// ============================================================
|
||||
|
||||
// StreamEvent represents an SSE event from Anthropic streaming API
|
||||
type StreamEvent struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
// MessageStartEvent represents the message_start SSE event
|
||||
type MessageStartEvent struct {
|
||||
Type string `json:"type"`
|
||||
Message MessageStart `json:"message"`
|
||||
}
|
||||
|
||||
// MessageStart represents the message object in message_start event
|
||||
type MessageStart struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Role string `json:"role"`
|
||||
Content []ContentBlock `json:"content"`
|
||||
Model string `json:"model"`
|
||||
StopReason *string `json:"stop_reason"`
|
||||
StopSequence *string `json:"stop_sequence"`
|
||||
Usage *UsageInfo `json:"usage,omitempty"`
|
||||
}
|
||||
|
||||
// ContentBlockStartEvent represents the content_block_start SSE event
|
||||
type ContentBlockStartEvent struct {
|
||||
Type string `json:"type"`
|
||||
Index int `json:"index"`
|
||||
ContentBlock ContentBlock `json:"content_block"`
|
||||
}
|
||||
|
||||
// ContentBlockDeltaEvent represents the content_block_delta SSE event
|
||||
type ContentBlockDeltaEvent struct {
|
||||
Type string `json:"type"`
|
||||
Index int `json:"index"`
|
||||
Delta DeltaBlock `json:"delta"`
|
||||
}
|
||||
|
||||
// ContentBlockStopEvent represents the content_block_stop SSE event
|
||||
type ContentBlockStopEvent struct {
|
||||
Type string `json:"type"`
|
||||
Index int `json:"index"`
|
||||
}
|
||||
|
||||
// MessageDeltaEvent represents the message_delta SSE event
|
||||
type MessageDeltaEvent struct {
|
||||
Type string `json:"type"`
|
||||
Delta MessageDelta `json:"delta"`
|
||||
Usage *DeltaUsage `json:"usage,omitempty"`
|
||||
}
|
||||
|
||||
// MessageDelta represents the delta in message_delta event
|
||||
type MessageDelta struct {
|
||||
StopReason string `json:"stop_reason,omitempty"`
|
||||
StopSequence *string `json:"stop_sequence,omitempty"`
|
||||
}
|
||||
|
||||
// DeltaUsage represents usage in message_delta event
|
||||
type DeltaUsage struct {
|
||||
OutputTokens int `json:"output_tokens"`
|
||||
}
|
||||
|
||||
// ContentBlock represents a content block in the response
|
||||
type ContentBlock struct {
|
||||
Type string `json:"type"` // "text", "thinking", "tool_use"
|
||||
Text string `json:"text,omitempty"` // for type "text"
|
||||
Thinking string `json:"thinking,omitempty"` // for type "thinking"
|
||||
Signature string `json:"signature,omitempty"` // for type "thinking"
|
||||
ID string `json:"id,omitempty"` // for type "tool_use"
|
||||
Name string `json:"name,omitempty"` // for type "tool_use"
|
||||
Input interface{} `json:"input,omitempty"` // for type "tool_use"
|
||||
}
|
||||
|
||||
// DeltaBlock represents a delta block in streaming
|
||||
type DeltaBlock struct {
|
||||
Type string `json:"type"` // "text_delta", "thinking_delta", "input_json_delta"
|
||||
Text string `json:"text,omitempty"` // for type "text_delta"
|
||||
Thinking string `json:"thinking,omitempty"` // for type "thinking_delta"
|
||||
PartialJSON string `json:"partial_json,omitempty"` // for type "input_json_delta"
|
||||
}
|
||||
|
||||
// UsageInfo represents token usage information
|
||||
type UsageInfo struct {
|
||||
InputTokens int `json:"input_tokens"`
|
||||
OutputTokens int `json:"output_tokens"`
|
||||
CacheCreationInputTokens int `json:"cache_creation_input_tokens,omitempty"`
|
||||
CacheReadInputTokens int `json:"cache_read_input_tokens,omitempty"`
|
||||
}
|
||||
|
||||
// NonStreamResponse represents the full non-streaming response from Anthropic API
|
||||
type NonStreamResponse struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Role string `json:"role"`
|
||||
Content []ContentBlock `json:"content"`
|
||||
Model string `json:"model"`
|
||||
StopReason string `json:"stop_reason"`
|
||||
StopSequence *string `json:"stop_sequence"`
|
||||
Usage *UsageInfo `json:"usage,omitempty"`
|
||||
}
|
||||
|
||||
// APIError represents an error response from Anthropic API
|
||||
type APIError struct {
|
||||
Type string `json:"type"`
|
||||
Error struct {
|
||||
Type string `json:"type"`
|
||||
Message string `json:"message"`
|
||||
} `json:"error"`
|
||||
}
|
||||
|
||||
// streamAccumulator accumulates streaming response data
|
||||
type streamAccumulator struct {
|
||||
id string
|
||||
model string
|
||||
role string
|
||||
content string
|
||||
thinkingContent string
|
||||
thinkingSignature string
|
||||
toolCalls map[int]*accumulatedToolCall
|
||||
stopReason string
|
||||
usage *message.UsageInfo
|
||||
|
||||
// Current content block tracking
|
||||
currentBlockIndex int
|
||||
currentBlockType string
|
||||
}
|
||||
|
||||
// accumulatedToolCall accumulates a single tool call from streaming
|
||||
type accumulatedToolCall struct {
|
||||
id string
|
||||
name string
|
||||
inputJSON string
|
||||
}
|
||||
|
||||
// messageTracker tracks message lifecycle for stream events
|
||||
type messageTracker struct {
|
||||
active bool
|
||||
messageID string
|
||||
messageType message.StreamChunkType
|
||||
startTime int64
|
||||
chunkCount int
|
||||
toolCallInfo *message.EventToolCallInfo
|
||||
idGenerator *message.IDGenerator
|
||||
}
|
||||
|
|
@ -4,7 +4,9 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/yaoapp/gou/connector"
|
||||
gouAnthropicConn "github.com/yaoapp/gou/connector/anthropic"
|
||||
"github.com/yaoapp/yao/agent/context"
|
||||
"github.com/yaoapp/yao/agent/llm/providers/anthropic"
|
||||
"github.com/yaoapp/yao/agent/llm/providers/openai"
|
||||
"github.com/yaoapp/yao/agent/output/message"
|
||||
)
|
||||
|
|
@ -40,10 +42,15 @@ func SelectProvider(conn connector.Connector, options *context.CompletionOptions
|
|||
// - Reasoning (o1, GPT-4o thinking, etc.)
|
||||
return openai.New(conn, options.Capabilities), nil
|
||||
|
||||
case "claude":
|
||||
// TODO: Implement Claude provider
|
||||
// For now, use OpenAI provider (may have compatibility issues)
|
||||
return openai.New(conn, options.Capabilities), nil
|
||||
case "anthropic":
|
||||
// Anthropic Messages API (Claude, Kimi Code, etc.)
|
||||
// Check if connector has native Anthropic capabilities
|
||||
settings := conn.Setting()
|
||||
if caps, ok := settings["capabilities"].(*gouAnthropicConn.Capabilities); ok {
|
||||
return anthropic.NewFromAnthropicCaps(conn, caps), nil
|
||||
}
|
||||
// Fallback: use OpenAI capabilities (converted from connector settings)
|
||||
return anthropic.New(conn, options.Capabilities), nil
|
||||
|
||||
default:
|
||||
// Default to OpenAI-compatible provider
|
||||
|
|
@ -53,21 +60,24 @@ func SelectProvider(conn connector.Connector, options *context.CompletionOptions
|
|||
|
||||
// DetectAPIFormat detects the API format from connector
|
||||
func DetectAPIFormat(conn connector.Connector) string {
|
||||
// Check connector type
|
||||
// Check connector type directly
|
||||
if conn.Is(connector.ANTHROPIC) {
|
||||
return "anthropic"
|
||||
}
|
||||
|
||||
if conn.Is(connector.OPENAI) {
|
||||
return "openai"
|
||||
}
|
||||
|
||||
// Check connector settings for host URL
|
||||
// Check connector settings for host URL patterns as fallback
|
||||
settings := conn.Setting()
|
||||
if settings != nil {
|
||||
if host, ok := settings["host"].(string); ok {
|
||||
// Detect by host URL patterns
|
||||
if contains(host, "anthropic.com") || contains(host, "claude") {
|
||||
return "claude"
|
||||
if contains(host, "anthropic.com") || contains(host, "api.kimi.com/coding") {
|
||||
return "anthropic"
|
||||
}
|
||||
if contains(host, "deepseek.com") {
|
||||
return "openai" // DeepSeek uses OpenAI-compatible API
|
||||
return "openai"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -338,9 +338,27 @@ func buildEnvironment(opts *Options, systemPrompt string) map[string]string {
|
|||
// Explicitly set XAUTHORITY to the correct path.
|
||||
env["XAUTHORITY"] = "/home/sandbox/.Xauthority"
|
||||
|
||||
// claude-proxy runs on localhost:3456, Claude CLI connects to it
|
||||
env["ANTHROPIC_BASE_URL"] = "http://127.0.0.1:3456"
|
||||
env["ANTHROPIC_API_KEY"] = "dummy" // Proxy doesn't verify this
|
||||
if opts.ConnectorType == "anthropic" {
|
||||
// Anthropic mode: Claude CLI connects directly to the Anthropic-compatible backend
|
||||
// No proxy needed — the backend already speaks Anthropic Messages API
|
||||
env["ANTHROPIC_BASE_URL"] = opts.ConnectorHost
|
||||
env["ANTHROPIC_API_KEY"] = opts.ConnectorKey
|
||||
} else {
|
||||
// OpenAI mode (default): Claude CLI connects to claude-proxy on localhost:3456
|
||||
// The proxy translates Anthropic Messages API → OpenAI Chat Completions API
|
||||
env["ANTHROPIC_BASE_URL"] = "http://127.0.0.1:3456"
|
||||
env["ANTHROPIC_API_KEY"] = "dummy" // Proxy doesn't verify this
|
||||
}
|
||||
|
||||
// Set model environment variables from connector
|
||||
// Claude CLI uses these to select the model for all roles
|
||||
if opts.Model != "" {
|
||||
env["ANTHROPIC_MODEL"] = opts.Model
|
||||
env["ANTHROPIC_DEFAULT_OPUS_MODEL"] = opts.Model
|
||||
env["ANTHROPIC_DEFAULT_SONNET_MODEL"] = opts.Model
|
||||
env["ANTHROPIC_DEFAULT_HAIKU_MODEL"] = opts.Model
|
||||
env["CLAUDE_CODE_SUBAGENT_MODEL"] = opts.Model
|
||||
}
|
||||
|
||||
// Pass secrets as environment variables for Claude CLI to use
|
||||
// These are configured in package.yao sandbox.secrets (e.g., LLM_API_KEY, GITHUB_TOKEN)
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ type Options struct {
|
|||
ConnectorHost string
|
||||
ConnectorKey string
|
||||
Model string
|
||||
ConnectorType string // Connector API type: "openai" or "anthropic"
|
||||
ConnectorOptions map[string]interface{} // Extra connector options (e.g., thinking, max_tokens)
|
||||
Secrets map[string]string // Secrets to pass to container (e.g., GITHUB_TOKEN)
|
||||
}
|
||||
|
|
@ -330,6 +331,12 @@ func (e *Executor) startClaudeProxy(ctx context.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Skip proxy for Anthropic connectors — Claude CLI connects directly
|
||||
// The backend already speaks Anthropic Messages API, no conversion needed
|
||||
if e.opts.ConnectorType == "anthropic" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Build proxy config
|
||||
configJSON, err := BuildProxyConfig(e.opts)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ func New(manager *infraSandbox.Manager, opts *Options) (Executor, error) {
|
|||
ConnectorHost: opts.ConnectorHost,
|
||||
ConnectorKey: opts.ConnectorKey,
|
||||
Model: opts.Model,
|
||||
ConnectorType: opts.ConnectorType, // "openai" or "anthropic"
|
||||
ConnectorOptions: opts.ConnectorOptions, // Extra options like thinking, max_tokens
|
||||
Secrets: opts.Secrets, // Secrets for container env vars
|
||||
}
|
||||
|
|
|
|||
|
|
@ -94,6 +94,10 @@ type Options struct {
|
|||
ConnectorKey string `json:"-"`
|
||||
Model string `json:"-"`
|
||||
|
||||
// ConnectorType - connector API type: "openai" or "anthropic"
|
||||
// Determines whether to use claude-proxy (openai) or direct connection (anthropic)
|
||||
ConnectorType string `json:"-"`
|
||||
|
||||
// ConnectorOptions - extra options from connector config (e.g., thinking, max_tokens, temperature)
|
||||
// These are backend-specific parameters passed to the proxy
|
||||
ConnectorOptions map[string]interface{} `json:"-"`
|
||||
|
|
|
|||
|
|
@ -59,13 +59,13 @@ func listProviders(c *gin.Context) {
|
|||
// Get user-defined model capabilities once at the start of request
|
||||
modelCapabilities := getModelCapabilities()
|
||||
|
||||
// Get all OpenAI-compatible LLM connectors from AIConnectors
|
||||
// Note: All openai type connectors are automatically added to AIConnectors during loading
|
||||
// Get all LLM connectors from AIConnectors
|
||||
// Note: All AI type connectors (openai, anthropic, fastembed) are automatically added to AIConnectors during loading
|
||||
// See gou/connector/connector.go LoadSource() for details
|
||||
for _, opt := range connector.AIConnectors {
|
||||
connType := getConnectorType(opt.Value)
|
||||
// Only include OpenAI-compatible LLM connectors
|
||||
if connType == "openai" {
|
||||
// Include OpenAI-compatible and Anthropic LLM connectors
|
||||
if connType == "openai" || connType == "anthropic" {
|
||||
conn, ok := connector.Connectors[opt.Value]
|
||||
if !ok {
|
||||
continue
|
||||
|
|
@ -99,11 +99,14 @@ func getConnectorType(id string) string {
|
|||
return "unknown"
|
||||
}
|
||||
|
||||
// Only return openai type (OpenAI-compatible format)
|
||||
if conn.Is(connector.OPENAI) {
|
||||
return "openai"
|
||||
}
|
||||
|
||||
if conn.Is(connector.ANTHROPIC) {
|
||||
return "anthropic"
|
||||
}
|
||||
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue