Files
2026-05-12 18:49:52 +08:00

259 lines
7.5 KiB
Go

package integration
import (
"context"
"encoding/json"
"errors"
"net/http"
"os"
)
// SupplierAdapter defines the interface for interacting with a supplier platform
type SupplierAdapter interface {
// Platform returns the platform name (e.g., "openai", "anthropic")
Platform() string
// ProbeAccount sends a health check request to the supplier API
// Returns the HTTP response details needed for probe classification
ProbeAccount(ctx context.Context, account SupplierAccount) ProbeResult
// GetModels fetches the list of available models from the supplier
GetModels(ctx context.Context, account SupplierAccount) ([]ModelInfo, error)
// HealthCheck verifies connectivity to the supplier API
HealthCheck(ctx context.Context, account SupplierAccount) error
}
func getEnvOr(key, defaultVal string) string {
if v := os.Getenv(key); v != "" {
return v
}
return defaultVal
}
// SupplierAccount holds credentials and configuration for a supplier account
type SupplierAccount struct {
AccountID int64
Platform string
APIKey string
BaseURL string // defaults to supplier's public endpoint if empty
Endpoint string // custom endpoint override
}
// ProbeResult holds the raw result of a probe request
type ProbeResult struct {
StatusCode int
TransportError error
ResponseBody string
}
// ModelInfo describes a model available from a supplier
type ModelInfo struct {
ModelID string // supplier's model identifier
ModelName string // display name
ContextLength int // max context length in tokens
IsActive bool // whether the model is currently available
}
// NewOpenAIAdapter creates an adapter for OpenAI-compatible APIs
func NewOpenAIAdapter(httpClient HTTPClient) SupplierAdapter {
return &OpenAIAdapter{httpClient: httpClient}
}
// OpenAIAdapter implements SupplierAdapter for OpenAI and OpenAI-compatible APIs
type OpenAIAdapter struct {
httpClient HTTPClient
}
func (a *OpenAIAdapter) Platform() string { return "openai" }
func (a *OpenAIAdapter) ProbeAccount(ctx context.Context, account SupplierAccount) ProbeResult {
baseURL := account.BaseURL
if baseURL == "" {
baseURL = "https://api.openai.com"
}
endpoint := account.Endpoint
if endpoint == "" {
endpoint = baseURL + "/v1/models"
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return ProbeResult{TransportError: err}
}
req.Header.Set("Authorization", "Bearer "+account.APIKey)
req.Header.Set("User-Agent", "supply-intelligence-probe/1.0")
resp, err := a.httpClient.Do(req)
if err != nil {
return ProbeResult{TransportError: err}
}
defer resp.Body.Close()
body := make([]byte, 1024)
n, _ := resp.Body.Read(body)
return ProbeResult{
StatusCode: resp.StatusCode,
ResponseBody: string(body[:n]),
}
}
func (a *OpenAIAdapter) GetModels(ctx context.Context, account SupplierAccount) ([]ModelInfo, error) {
baseURL := account.BaseURL
if baseURL == "" {
baseURL = "https://api.openai.com"
}
apiKey := account.APIKey
if apiKey == "" {
apiKey = getEnvOr("OPENAI_API_KEY", "")
if apiKey == "" {
return nil, errors.New("OPENAI_API_KEY not set and no account API key provided")
}
}
endpoint := baseURL + "/v1/models"
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := a.httpClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// Parse the OpenAI models list response
// {"object": "list", "data": [{"id": "gpt-4", "object": "model", ...}, ...]}
var raw struct {
Data []struct {
ID string `json:"id"`
Object string `json:"object"`
Context int `json:"context_window,omitempty"`
} `json:"data"`
}
if err := decodeJSON(resp, &raw); err != nil {
return nil, err
}
models := make([]ModelInfo, 0, len(raw.Data))
for _, m := range raw.Data {
if m.Object == "model" {
models = append(models, ModelInfo{
ModelID: m.ID,
ModelName: m.ID,
ContextLength: m.Context,
IsActive: true,
})
}
}
return models, nil
}
func (a *OpenAIAdapter) HealthCheck(ctx context.Context, account SupplierAccount) error {
result := a.ProbeAccount(ctx, account)
if result.TransportError != nil {
return result.TransportError
}
if result.StatusCode == http.StatusOK || result.StatusCode == http.StatusUnauthorized {
return nil
}
return ErrHealthCheckFailed
}
// NewAnthropicAdapter creates an adapter for Anthropic APIs
func NewAnthropicAdapter(httpClient HTTPClient) SupplierAdapter {
return &AnthropicAdapter{httpClient: httpClient}
}
// AnthropicAdapter implements SupplierAdapter for Anthropic Claude API
type AnthropicAdapter struct {
httpClient HTTPClient
}
func (a *AnthropicAdapter) Platform() string { return "anthropic" }
func (a *AnthropicAdapter) ProbeAccount(ctx context.Context, account SupplierAccount) ProbeResult {
baseURL := account.BaseURL
if baseURL == "" {
baseURL = "https://api.anthropic.com"
}
endpoint := baseURL + "/v1/models"
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
if err != nil {
return ProbeResult{TransportError: err}
}
req.Header.Set("x-api-key", account.APIKey)
req.Header.Set("User-Agent", "supply-intelligence-probe/1.0")
req.Header.Set("anthropic-version", "2023-06-01")
resp, err := a.httpClient.Do(req)
if err != nil {
return ProbeResult{TransportError: err}
}
defer resp.Body.Close()
body := make([]byte, 1024)
n, _ := resp.Body.Read(body)
return ProbeResult{
StatusCode: resp.StatusCode,
ResponseBody: string(body[:n]),
}
}
func (a *AnthropicAdapter) GetModels(ctx context.Context, account SupplierAccount) ([]ModelInfo, error) {
// Anthropic doesn't have a public models list endpoint in the same way OpenAI does.
// We return a known static list for Claude models.
// In production this would be fetched from configuration or a dynamic source.
return []ModelInfo{
{ModelID: "claude-3-5-sonnet-20241022", ModelName: "Claude 3.5 Sonnet", ContextLength: 200000, IsActive: true},
{ModelID: "claude-3-5-haiku-20241022", ModelName: "Claude 3.5 Haiku", ContextLength: 200000, IsActive: true},
{ModelID: "claude-3-opus-20240229", ModelName: "Claude 3 Opus", ContextLength: 200000, IsActive: true},
{ModelID: "claude-3-sonnet-20240229", ModelName: "Claude 3 Sonnet", ContextLength: 200000, IsActive: true},
{ModelID: "claude-3-haiku-20240307", ModelName: "Claude 3 Haiku", ContextLength: 200000, IsActive: true},
}, nil
}
func (a *AnthropicAdapter) HealthCheck(ctx context.Context, account SupplierAccount) error {
result := a.ProbeAccount(ctx, account)
if result.TransportError != nil {
return result.TransportError
}
// Anthropic returns 200 on success, 401 on auth failure
if result.StatusCode == http.StatusOK || result.StatusCode == http.StatusUnauthorized {
return nil
}
return ErrHealthCheckFailed
}
// HTTPClient interface for testability
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
// DefaultHTTPClient is the standard HTTP client used for platform adapters
type DefaultHTTPClient struct{}
func (c *DefaultHTTPClient) Do(req *http.Request) (*http.Response, error) {
return http.DefaultClient.Do(req)
}
// NewDefaultHTTPClient creates a new default HTTP client
func NewDefaultHTTPClient() HTTPClient {
return &DefaultHTTPClient{}
}
var ErrHealthCheckFailed = &HealthCheckError{}
type HealthCheckError struct{}
func (e *HealthCheckError) Error() string { return "health check failed" }
func decodeJSON(resp *http.Response, v interface{}) error {
return json.NewDecoder(resp.Body).Decode(v)
}