Skip to content
Merged
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
236 changes: 0 additions & 236 deletions internal/client/anthropic.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package client

import (
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
Expand Down Expand Up @@ -41,10 +40,6 @@ type AnthropicClientInterface interface {
APIStyle() protocol.APIStyle
SetRecordSink(sink *obs.Sink)
Client() *anthropic.Client

// Prober interface methods
Probe(ctx context.Context, model string) ProbeResult
ProbeStream(ctx context.Context, model, message string, testMode ProbeMode) (*ProbeResult, error)
}

// AnthropicClient wraps the Anthropic SDK client
Expand Down Expand Up @@ -212,234 +207,3 @@ func (c *AnthropicClient) ListModels(ctx context.Context) ([]string, error) {

return result, nil
}

// ProbeChatEndpoint tests the messages endpoint with a minimal request
func (c *AnthropicClient) Probe(ctx context.Context, model string) ProbeResult {
startTime := time.Now()

// Determine system message based on OAuth provider type
systemMessages := []anthropic.TextBlockParam{
{
Text: "work as `echo`",
},
}
if c.provider.AuthType == typ.AuthTypeOAuth && c.provider.OAuthDetail != nil &&
c.provider.OAuthDetail.GetIssuer() == ai.IssuerClaudeCode {
// Prepend Claude Code system message as the first block
systemMessages = append([]anthropic.TextBlockParam{{
Text: ClaudeCodeSystemHeader,
}}, systemMessages...)
}

// Create message request using Anthropic SDK
messageRequest := anthropic.MessageNewParams{
Model: anthropic.Model(model),
MaxTokens: 100,
System: systemMessages,
Messages: []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock("hi")),
},
}

// Make request
resp, err := c.client.Messages.New(ctx, messageRequest)
latencyMs := time.Since(startTime).Milliseconds()

if err != nil {
return ProbeResult{
Success: false,
ErrorMessage: err.Error(),
LatencyMs: latencyMs,
}
}

// Extract response data
responseContent := ""
promptTokens := 0
completionTokens := 0
totalTokens := 0

if resp != nil {
for _, block := range resp.Content {
if block.Type == "text" {
responseContent += string(block.Text)
}
}
if resp.Usage.InputTokens != 0 {
promptTokens = int(resp.Usage.InputTokens)
completionTokens = int(resp.Usage.OutputTokens)
totalTokens = promptTokens + completionTokens
}
}

if responseContent == "" {
responseContent = "<response content is empty, but request success>"
}

return ProbeResult{
Success: true,
Message: "Messages endpoint is accessible",
Content: responseContent,
LatencyMs: latencyMs,
PromptTokens: promptTokens,
CompletionTokens: completionTokens,
TotalTokens: totalTokens,
}
}

// ProbeStream performs a streaming probe with configurable test mode (public interface)
func (c *AnthropicClient) ProbeStream(ctx context.Context, model, message string, testMode ProbeMode) (*ProbeResult, error) {
return c.probeStream(ctx, model, message, testMode)
}

// probeStream performs a streaming probe with configurable test mode
func (c *AnthropicClient) probeStream(ctx context.Context, model, message string, testMode ProbeMode) (*ProbeResult, error) {
startTime := time.Now()

// Determine system message based on OAuth provider type
systemMessages := []anthropic.TextBlockParam{
{
Text: "work as `echo` if possible",
},
}
if c.provider.AuthType == typ.AuthTypeOAuth && c.provider.OAuthDetail != nil &&
c.provider.OAuthDetail.GetIssuer() == ai.IssuerClaudeCode {
// Prepend Claude Code system message as the first block
systemMessages = append([]anthropic.TextBlockParam{{
Text: ClaudeCodeSystemHeader,
}}, systemMessages...)
}

messages := []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock(message)),
}

params := &anthropic.MessageNewParams{
Model: anthropic.Model(model),
MaxTokens: 1024,
System: systemMessages,
Messages: messages,
}

if testMode == ProbeModeTool {
params.Tools = GetProbeToolsAnthropic()
params.ToolChoice = GetProbeToolChoiceAutoAnthropic()
}

// For simple mode, use non-streaming request
if testMode == ProbeModeSimple {
resp, err := c.client.Messages.New(ctx, *params)
if err != nil {
return nil, err
}

respJSON, _ := json.Marshal(resp)
return ToProbeResult(string(respJSON), time.Since(startTime).Milliseconds(), c.provider.APIBase+"/v1/messages", false), nil
}

// For streaming and tool modes, use streaming
stream := c.client.Messages.NewStreaming(ctx, *params)
defer stream.Close()

var chunks []interface{}
for stream.Next() {
event := stream.Current()
chunks = append(chunks, event)
}

if err := stream.Err(); err != nil {
return nil, err
}

chunksJSON, _ := json.Marshal(chunks)
return ToProbeResult(string(chunksJSON), time.Since(startTime).Milliseconds(), c.provider.APIBase+"/v1/messages", true), nil
}

// ProbeModelsEndpoint tests the models list endpoint
func (c *AnthropicClient) ProbeModelsEndpoint(ctx context.Context) ProbeResult {
startTime := time.Now()

// Make request to models endpoint
resp, err := c.client.Models.List(ctx, anthropic.ModelListParams{})
latencyMs := time.Since(startTime).Milliseconds()

if err != nil {
return ProbeResult{
Success: false,
ErrorMessage: err.Error(),
LatencyMs: latencyMs,
}
}

modelsCount := 0
if resp != nil {
modelsCount = len(resp.Data)
}

if modelsCount == 0 {
return ProbeResult{
Success: false,
ErrorMessage: "No models available from provider",
LatencyMs: latencyMs,
}
}

return ProbeResult{
Success: true,
Message: "Models endpoint is accessible",
LatencyMs: latencyMs,
ModelsCount: modelsCount,
}
}

// ProbeOptionsEndpoint tests basic connectivity with an OPTIONS request
func (c *AnthropicClient) ProbeOptionsEndpoint(ctx context.Context) ProbeResult {
startTime := time.Now()

// Build the options URL - ensure it has /v1 suffix for Anthropic
apiBase := strings.TrimSuffix(c.provider.APIBase, "/")
if !strings.Contains(apiBase, "/v1") {
apiBase = apiBase + "/v1"
}
optionsURL := apiBase

req, err := http.NewRequestWithContext(ctx, "OPTIONS", optionsURL, nil)
if err != nil {
return ProbeResult{
Success: false,
ErrorMessage: fmt.Sprintf("Failed to create OPTIONS request: %v", err),
}
}

// Set authentication headers
req.Header.Set("x-api-key", c.provider.GetAccessToken())
req.Header.Set("anthropic-version", "2023-06-01")

client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Do(req)
latencyMs := time.Since(startTime).Milliseconds()

if err != nil {
return ProbeResult{
Success: false,
ErrorMessage: fmt.Sprintf("OPTIONS request failed: %v", err),
LatencyMs: latencyMs,
}
}
defer resp.Body.Close()

// Consider any 2xx status as success for OPTIONS
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return ProbeResult{
Success: true,
Message: "OPTIONS request successful",
LatencyMs: latencyMs,
}
}

return ProbeResult{
Success: false,
ErrorMessage: fmt.Sprintf("OPTIONS request failed with status: %d", resp.StatusCode),
LatencyMs: latencyMs,
}
}
120 changes: 0 additions & 120 deletions internal/client/anthropic_probe_test.go
Original file line number Diff line number Diff line change
@@ -1,131 +1,12 @@
package client

import (
"context"
"testing"

"github.com/tingly-dev/tingly-box/internal/protocol"
"github.com/tingly-dev/tingly-box/internal/typ"
)

// TestAnthropicClient_ProbeChatEndpoint tests the ProbeChatEndpoint method
func TestAnthropicClient_ProbeChatEndpoint(t *testing.T) {
tests := []struct {
name string
provider *typ.Provider
model string
wantErr bool
}{
{
name: "skip live test - requires valid API key",
provider: &typ.Provider{
Name: "test-anthropic",
APIBase: "https://api.anthropic.com",
APIStyle: protocol.APIStyleAnthropic,
Token: "sk-test-key",
},
model: "claude-3-haiku-20240307",
wantErr: true, // Will fail with invalid key
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, err := NewAnthropicClient(tt.provider, tt.model, typ.SessionID{})
if err != nil {
t.Fatalf("NewAnthropicClient() error = %v", err)
}

result := client.Probe(context.Background(), tt.model)

if !tt.wantErr && !result.Success {
t.Errorf("ProbeChatEndpoint() failed = %v", result.ErrorMessage)
}
if tt.wantErr && result.Success {
t.Errorf("ProbeChatEndpoint() expected error but succeeded")
}
})
}
}

// TestAnthropicClient_ProbeModelsEndpoint tests the ProbeModelsEndpoint method
func TestAnthropicClient_ProbeModelsEndpoint(t *testing.T) {
tests := []struct {
name string
provider *typ.Provider
model string
wantErr bool
}{
{
name: "skip live test - requires valid API key",
provider: &typ.Provider{
Name: "test-anthropic",
APIBase: "https://api.anthropic.com",
APIStyle: protocol.APIStyleAnthropic,
Token: "sk-test-key",
},
model: "claude-3-haiku-20240307",
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, err := NewAnthropicClient(tt.provider, tt.model, typ.SessionID{})
if err != nil {
t.Fatalf("NewAnthropicClient() error = %v", err)
}

result := client.ProbeModelsEndpoint(context.Background())

if !tt.wantErr && !result.Success {
t.Errorf("ProbeModelsEndpoint() failed = %v", result.ErrorMessage)
}
if tt.wantErr && result.Success {
t.Errorf("ProbeModelsEndpoint() expected error but succeeded")
}
})
}
}

// TestAnthropicClient_ProbeOptionsEndpoint tests the ProbeOptionsEndpoint method
func TestAnthropicClient_ProbeOptionsEndpoint(t *testing.T) {
tests := []struct {
name string
provider *typ.Provider
model string
wantErr bool
}{
{
name: "skip live test - requires valid API key",
provider: &typ.Provider{
Name: "test-anthropic",
APIBase: "https://api.anthropic.com",
APIStyle: protocol.APIStyleAnthropic,
Token: "sk-test-key",
},
model: "claude-3-haiku-20240307",
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client, err := NewAnthropicClient(tt.provider, tt.model, typ.SessionID{})
if err != nil {
t.Fatalf("NewAnthropicClient() error = %v", err)
}

result := client.ProbeOptionsEndpoint(context.Background())

if !tt.wantErr && !result.Success {
t.Errorf("ProbeOptionsEndpoint() failed = %v", result.ErrorMessage)
}
// OPTIONS might succeed even with invalid key for some providers
})
}
}

// TestAnthropicClient_Timeout tests that timeout is properly configured from provider
func TestAnthropicClient_Timeout(t *testing.T) {
tests := []struct {
Expand Down Expand Up @@ -171,4 +52,3 @@ func TestAnthropicClient_Timeout(t *testing.T) {
})
}
}

Loading