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
5 changes: 3 additions & 2 deletions edge-server/cmd/agenthub-edge/adapter_registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/agenthub/edge-server/internal/adapters"
"github.com/agenthub/edge-server/internal/adapters/orchestrator"
"github.com/agenthub/edge-server/internal/adapters/sdk"
"github.com/agenthub/edge-server/internal/edgehttp"
)

Expand Down Expand Up @@ -146,7 +147,7 @@ func registerManifestAdapters(reg *adapters.Registry, cfg config) {
func registerSDKAdapters(reg *adapters.Registry, cfg config) {
if cfg.AnthropicSDKPath != "" {
apiKey := resolveSDKAPIKey(cfg.AnthropicSDKPath, "ANTHROPIC_API_KEY")
a := adapters.NewAnthropicSDKAdapter(apiKey, cfg.AgentModel, edgehttp.NewClient(adapters.AnthropicHTTPTimeout))
a := sdk.NewAnthropicSDKAdapter(apiKey, cfg.AgentModel, edgehttp.NewClient(sdk.AnthropicHTTPTimeout))
if err := reg.Register(a); err != nil {
slog.Warn("failed to register anthropic-sdk adapter", "err", err)
} else {
Expand All @@ -155,7 +156,7 @@ func registerSDKAdapters(reg *adapters.Registry, cfg config) {
}
if cfg.OpenAISDKPath != "" {
apiKey := resolveSDKAPIKey(cfg.OpenAISDKPath, "OPENAI_API_KEY")
a := adapters.NewOpenAISDKAdapter(apiKey, cfg.AgentModel, edgehttp.NewClient(adapters.OpenAIHTTPTimeout))
a := sdk.NewOpenAISDKAdapter(apiKey, cfg.AgentModel, edgehttp.NewClient(sdk.OpenAIHTTPTimeout))
if err := reg.Register(a); err != nil {
slog.Warn("failed to register openai-sdk adapter", "err", err)
} else {
Expand Down
83 changes: 1 addition & 82 deletions edge-server/internal/adapters/adapter_latency_test.go
Original file line number Diff line number Diff line change
@@ -1,96 +1,15 @@
package adapters

import (
"net/http"
"strings"
"testing"
"time"

"github.com/agenthub/edge-server/internal/router"
"github.com/agenthub/edge-server/internal/runnerctx"
)

// =============================================================================
// Benchmark: SDK adapter creation + request building latency
// =============================================================================

// BenchmarkSDKAdapterLatency measures the wall-clock time for creating
// an AnthropicSDKAdapter and an OpenAISDKAdapter, calling BuildCommand,
// and building the outgoing messages array (buildMessages). These are the
// hot-path operations that run on every agent invocation before any HTTP
// request is made.
func BenchmarkSDKAdapterLatency(b *testing.B) {
runCtx := runnerctx.RunProcessContext{
SystemPrompt: "You are a helpful assistant.",
Prompt: "What is the capital of France?",
Model: "claude-sonnet-4-6",
}

b.Run("anthropic_sdk", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
adapter := NewAnthropicSDKAdapter("test-key-not-used", "claude-sonnet-4-6", &http.Client{})
adapter.BuildCommand(runCtx)
_ = adapter.buildMessages(runCtx)
}
})

b.Run("openai_sdk", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
adapter := NewOpenAISDKAdapter("test-key-not-used", "gpt-5.5", &http.Client{})
adapter.BuildCommand(runCtx)
_ = adapter.buildMessages(runCtx)
}
})
}

// =============================================================================
// Test: adapter setup latency baseline (<100ms)
// =============================================================================

// TestSDKAdapterLatencyBaseline asserts that adapter creation, BuildCommand,
// and buildMessages complete in under 100ms. This is a generous baseline —
// typical times are well under 1ms. The test acts as a canary for accidental
// regressions (e.g. blocking I/O, expensive init, unbounded allocations).
func TestSDKAdapterLatencyBaseline(t *testing.T) {
runCtx := runnerctx.RunProcessContext{
SystemPrompt: "You are a helpful assistant.",
Prompt: "What is the capital of France?",
Model: "claude-sonnet-4-6",
}

const maxLatency = 100 * time.Millisecond

t.Run("anthropic_sdk", func(t *testing.T) {
start := time.Now()
adapter := NewAnthropicSDKAdapter("test-key-not-used", "claude-sonnet-4-6", &http.Client{})
adapter.BuildCommand(runCtx)
_ = adapter.buildMessages(runCtx)
elapsed := time.Since(start)

if elapsed > maxLatency {
t.Errorf("AnthropicSDK adapter setup took %v, want <= %v", elapsed, maxLatency)
}
t.Logf("AnthropicSDK setup: %v", elapsed)
})

t.Run("openai_sdk", func(t *testing.T) {
start := time.Now()
adapter := NewOpenAISDKAdapter("test-key-not-used", "gpt-5.5", &http.Client{})
adapter.BuildCommand(runCtx)
_ = adapter.buildMessages(runCtx)
elapsed := time.Since(start)

if elapsed > maxLatency {
t.Errorf("OpenAISDK adapter setup took %v, want <= %v", elapsed, maxLatency)
}
t.Logf("OpenAISDK setup: %v", elapsed)
})
}

// =============================================================================
// Benchmark: ClassifyComplexity with various prompt sizes
// (SDK 适配器延迟基准已随 #1760 归组迁入 internal/adapters/sdk)
// =============================================================================

// BenchmarkClassifyComplexity benchmarks the deterministic prompt complexity
Expand Down
89 changes: 89 additions & 0 deletions edge-server/internal/adapters/sdk/adapter_latency_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package sdk

import (
"net/http"
"testing"
"time"

"github.com/agenthub/edge-server/internal/runnerctx"
)

// =============================================================================
// Benchmark: SDK adapter creation + request building latency
// (随 anthropic/openai SDK 适配器归组迁入 package sdk,#1760)
// =============================================================================

// BenchmarkSDKAdapterLatency measures the wall-clock time for creating
// an AnthropicSDKAdapter and an OpenAISDKAdapter, calling BuildCommand,
// and building the outgoing messages array (buildMessages). These are the
// hot-path operations that run on every agent invocation before any HTTP
// request is made.
func BenchmarkSDKAdapterLatency(b *testing.B) {
runCtx := runnerctx.RunProcessContext{
SystemPrompt: "You are a helpful assistant.",
Prompt: "What is the capital of France?",
Model: "claude-sonnet-4-6",
}

b.Run("anthropic_sdk", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
adapter := NewAnthropicSDKAdapter("test-key-not-used", "claude-sonnet-4-6", &http.Client{})
adapter.BuildCommand(runCtx)
_ = adapter.buildMessages(runCtx)
}
})

b.Run("openai_sdk", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
adapter := NewOpenAISDKAdapter("test-key-not-used", "gpt-5.5", &http.Client{})
adapter.BuildCommand(runCtx)
_ = adapter.buildMessages(runCtx)
}
})
}

// =============================================================================
// Test: adapter setup latency baseline (<100ms)
// =============================================================================

// TestSDKAdapterLatencyBaseline asserts that adapter creation, BuildCommand,
// and buildMessages complete in under 100ms. This is a generous baseline —
// typical times are well under 1ms. The test acts as a canary for accidental
// regressions (e.g. blocking I/O, expensive init, unbounded allocations).
func TestSDKAdapterLatencyBaseline(t *testing.T) {
runCtx := runnerctx.RunProcessContext{
SystemPrompt: "You are a helpful assistant.",
Prompt: "What is the capital of France?",
Model: "claude-sonnet-4-6",
}

const maxLatency = 100 * time.Millisecond

t.Run("anthropic_sdk", func(t *testing.T) {
start := time.Now()
adapter := NewAnthropicSDKAdapter("test-key-not-used", "claude-sonnet-4-6", &http.Client{})
adapter.BuildCommand(runCtx)
_ = adapter.buildMessages(runCtx)
elapsed := time.Since(start)

if elapsed > maxLatency {
t.Errorf("AnthropicSDK adapter setup took %v, want <= %v", elapsed, maxLatency)
}
t.Logf("AnthropicSDK setup: %v", elapsed)
})

t.Run("openai_sdk", func(t *testing.T) {
start := time.Now()
adapter := NewOpenAISDKAdapter("test-key-not-used", "gpt-5.5", &http.Client{})
adapter.BuildCommand(runCtx)
_ = adapter.buildMessages(runCtx)
elapsed := time.Since(start)

if elapsed > maxLatency {
t.Errorf("OpenAISDK adapter setup took %v, want <= %v", elapsed, maxLatency)
}
t.Logf("OpenAISDK setup: %v", elapsed)
})
}
29 changes: 29 additions & 0 deletions edge-server/internal/adapters/sdk/aliases.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Contract aliases for package sdk(#1760 首个增量)。
//
// 合同类型与 BusEvent* 常量的唯一权威(SSOT)在 internal/orchestration;
// 本文件与 adapters 根包的 contract_aliases.go、兄弟叶子包
// adapters/orchestrator 的 aliases.go 采用同一模式:派生 alias / 常量,
// 不复制定义,使归组后的实现文件继续免限定引用合同词汇。
package sdk

import "github.com/agenthub/edge-server/internal/orchestration"

// ── Contract type aliases (single SSOT: internal/orchestration) ─────────────

type EventEmitter = orchestration.EventEmitter
type AdapterMetadata = orchestration.AdapterMetadata
type AgentCapabilities = orchestration.AgentCapabilities
type RunProcessContext = orchestration.RunProcessContext

// ── Bus event type strings(与 orchestration 完全一致,防双 SSOT)────────────
const (
BusEventAPIRetry = orchestration.BusEventAPIRetry
BusEventContextUsage = orchestration.BusEventContextUsage
BusEventResult = orchestration.BusEventResult
BusEventSessionInit = orchestration.BusEventSessionInit
BusEventStatusChange = orchestration.BusEventStatusChange
BusEventTextBlock = orchestration.BusEventTextBlock
BusEventTextDelta = orchestration.BusEventTextDelta
BusEventThinking = orchestration.BusEventThinking
BusEventToolCall = orchestration.BusEventToolCall
)
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package adapters
package sdk

import (
"context"
Expand All @@ -10,11 +10,13 @@ import (
"strings"
"time"

"github.com/agenthub/edge-server/internal/adapters"
"github.com/agenthub/edge-server/internal/store"
)

// Residual pure-helper peel #1142: keep AnthropicSDKAdapter surface + ParseStream
// orchestration; request/SSE/types moved to companion files.
// orchestration; request/SSE/types moved to companion files. Grouped into
// package sdk (#1760).

const (
anthropicSDKAdapterID = "anthropic-sdk"
Expand Down Expand Up @@ -176,13 +178,13 @@ func (a *AnthropicSDKAdapter) ParseStream(ctx context.Context, stdout io.Reader,

// Extract RunProcessContext from the context to get the prompt and model.
var runCtx RunProcessContext
if rc, ok := ctx.Value(CtxRunContext).(RunProcessContext); ok {
if rc, ok := ctx.Value(adapters.CtxRunContext).(RunProcessContext); ok {
runCtx = rc
}

model := a.model
if runCtx.Model != "" {
model = ResolveModel(anthropicSDKAdapterID, runCtx.Model)
model = adapters.ResolveModel(anthropicSDKAdapterID, runCtx.Model)
if model == "" {
model = runCtx.Model
}
Expand Down Expand Up @@ -253,7 +255,7 @@ func (a *AnthropicSDKAdapter) ParseStream(ctx context.Context, stdout io.Reader,

bodyBytes, err := json.Marshal(requestBody)
if err != nil {
return NewNonRecoverableParseError(fmt.Errorf("anthropic-sdk: failed to marshal request: %w", err))
return adapters.NewNonRecoverableParseError(fmt.Errorf("anthropic-sdk: failed to marshal request: %w", err))
}

// Emit session init
Expand All @@ -279,7 +281,7 @@ func (a *AnthropicSDKAdapter) ParseStream(ctx context.Context, stdout io.Reader,
"terminalReason": "error",
"provider": "anthropic",
})
return NewNonRecoverableParseError(fmt.Errorf("%s", errMsg))
return adapters.NewNonRecoverableParseError(fmt.Errorf("%s", errMsg))
}

// Parse SSE stream
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package adapters
package sdk

import (
"context"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package adapters
package sdk

import (
"bytes"
Expand All @@ -10,11 +10,12 @@ import (
"net/http"
"time"

"github.com/agenthub/edge-server/internal/adapters"
"github.com/agenthub/edge-server/internal/runnerctx"
)

// Residual pure-helper peel #1142: Anthropic SDK HTTP request + message build helpers.
// Same package adapters; doRequestWithRetry / buildMessages called from ParseStream.
// Grouped into package sdk (#1760); doRequestWithRetry / buildMessages called from ParseStream.

// doRequestWithRetry makes the HTTP request with automatic retry for transient
// failures (429 rate limit, 500/502/503/504 server errors). Auth errors (401/403)
Expand Down Expand Up @@ -50,7 +51,7 @@ func (a *AnthropicSDKAdapter) doRequestWithRetry(ctx context.Context, body []byt

req, err := http.NewRequestWithContext(ctx, http.MethodPost, a.baseURL+"/v1/messages", bytes.NewReader(body))
if err != nil {
return nil, NewNonRecoverableParseError(fmt.Errorf("anthropic-sdk: failed to create request: %w", err))
return nil, adapters.NewNonRecoverableParseError(fmt.Errorf("anthropic-sdk: failed to create request: %w", err))
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("x-api-key", a.apiKey)
Expand Down Expand Up @@ -90,7 +91,7 @@ func (a *AnthropicSDKAdapter) doRequestWithRetry(ctx context.Context, body []byt
"terminalReason": "error",
"provider": "anthropic",
})
return nil, NewNonRecoverableParseError(fmt.Errorf("anthropic-sdk: request failed after %d retries: %w", anthropicMaxRetries, lastErr))
return nil, adapters.NewNonRecoverableParseError(fmt.Errorf("anthropic-sdk: request failed after %d retries: %w", anthropicMaxRetries, lastErr))
}

// buildMessages converts the RunProcessContext into Anthropic message format.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package adapters
package sdk

import (
"bufio"
Expand All @@ -8,10 +8,12 @@ import (
"io"
"log/slog"
"strings"

"github.com/agenthub/edge-server/internal/adapters"
)

// Residual pure-helper peel #1142: Anthropic SSE stream parse + event dispatch.
// Same package adapters; ParseStream continues to call parseSSEStream.
// Grouped into package sdk (#1760); ParseStream continues to call parseSSEStream.

// anthropicSSEState accumulates cross-event state while parsing an SSE stream.
type anthropicSSEState struct {
Expand Down Expand Up @@ -69,7 +71,7 @@ func (a *AnthropicSDKAdapter) parseSSEStream(ctx context.Context, body io.Reader
if ctx.Err() != nil {
return ctx.Err()
}
return NewNonRecoverableParseError(fmt.Errorf("anthropic-sdk: SSE stream read error: %w", err))
return adapters.NewNonRecoverableParseError(fmt.Errorf("anthropic-sdk: SSE stream read error: %w", err))
}

return nil
Expand Down Expand Up @@ -232,5 +234,5 @@ func handleAnthropicStreamError(event anthropicSSEEvent, emitter EventEmitter, s
"terminalReason": "error",
"provider": "anthropic",
})
return NewNonRecoverableParseError(fmt.Errorf("anthropic-sdk: API error: %s", errMsg))
return adapters.NewNonRecoverableParseError(fmt.Errorf("anthropic-sdk: API error: %s", errMsg))
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package adapters
package sdk

// Residual pure-helper peel #1142: Anthropic Messages API request/SSE types.
// Same package adapters; public Adapter surface unchanged.
// Grouped into package sdk (#1760); public Adapter surface unchanged.

// --- Anthropic API types ---

Expand Down
Loading
Loading