feat: bootstrap supply intelligence baseline
This commit is contained in:
242
internal/integration/platform.go
Normal file
242
internal/integration/platform.go
Normal file
@@ -0,0 +1,242 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user