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) }