243 lines
7.2 KiB
Go
243 lines
7.2 KiB
Go
package integration
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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"
|
|
}
|
|
endpoint := baseURL + "/v1/models"
|
|
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+account.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)
|
|
}
|