mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
Trim whitespace in provider model lists, format provider names as `group#keyID` to match DP logs, and skip existing API keys during seeding (deleting on reset) to keep runs idempotent and summaries accurate
528 lines
15 KiB
Go
528 lines
15 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Client wraps HTTP calls to the Control Plane API
|
|
type Client struct {
|
|
baseURL string
|
|
adminToken string
|
|
dryRun bool
|
|
verbose bool
|
|
httpClient *http.Client
|
|
}
|
|
|
|
// NewClient creates a new API client
|
|
func NewClient(baseURL, adminToken string, dryRun, verbose bool) *Client {
|
|
return &Client{
|
|
baseURL: baseURL,
|
|
adminToken: adminToken,
|
|
dryRun: dryRun,
|
|
verbose: verbose,
|
|
httpClient: &http.Client{Timeout: 30 * time.Second},
|
|
}
|
|
}
|
|
|
|
// APIError represents an error response from the API
|
|
type APIError struct {
|
|
StatusCode int
|
|
Body string
|
|
}
|
|
|
|
func (e *APIError) Error() string {
|
|
return fmt.Sprintf("API error %d: %s", e.StatusCode, e.Body)
|
|
}
|
|
|
|
// IsNotFound returns true if the error is a 404
|
|
func (e *APIError) IsNotFound() bool {
|
|
return e.StatusCode == http.StatusNotFound
|
|
}
|
|
|
|
// IsConflict returns true if the error is a 409
|
|
func (e *APIError) IsConflict() bool {
|
|
return e.StatusCode == http.StatusConflict
|
|
}
|
|
|
|
// doRequest executes an HTTP request
|
|
func (c *Client) doRequest(method, path string, body any) ([]byte, error) {
|
|
var reqBody io.Reader
|
|
if body != nil {
|
|
data, err := json.Marshal(body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal request: %w", err)
|
|
}
|
|
reqBody = bytes.NewReader(data)
|
|
if c.verbose {
|
|
fmt.Printf(" Request: %s %s\n", method, path)
|
|
fmt.Printf(" Body: %s\n", string(data))
|
|
}
|
|
}
|
|
|
|
req, err := http.NewRequest(method, c.baseURL+path, reqBody)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("create request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Authorization", "Bearer "+c.adminToken)
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("execute request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
respBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read response: %w", err)
|
|
}
|
|
|
|
if c.verbose {
|
|
fmt.Printf(" Response: %d %s\n", resp.StatusCode, string(respBody))
|
|
}
|
|
|
|
if resp.StatusCode >= 400 {
|
|
return nil, &APIError{StatusCode: resp.StatusCode, Body: string(respBody)}
|
|
}
|
|
|
|
return respBody, nil
|
|
}
|
|
|
|
// get performs a GET request
|
|
func (c *Client) get(path string) ([]byte, error) {
|
|
return c.doRequest(http.MethodGet, path, nil)
|
|
}
|
|
|
|
// post performs a POST request (respects dry-run)
|
|
func (c *Client) post(path string, body any) ([]byte, error) {
|
|
if c.dryRun {
|
|
data, _ := json.Marshal(body)
|
|
fmt.Printf(" [DRY-RUN] POST %s: %s\n", path, string(data))
|
|
return []byte("{}"), nil
|
|
}
|
|
return c.doRequest(http.MethodPost, path, body)
|
|
}
|
|
|
|
// put performs a PUT request (respects dry-run)
|
|
func (c *Client) put(path string, body any) ([]byte, error) {
|
|
if c.dryRun {
|
|
data, _ := json.Marshal(body)
|
|
fmt.Printf(" [DRY-RUN] PUT %s: %s\n", path, string(data))
|
|
return []byte("{}"), nil
|
|
}
|
|
return c.doRequest(http.MethodPut, path, body)
|
|
}
|
|
|
|
// delete performs a DELETE request (respects dry-run)
|
|
func (c *Client) delete(path string) error {
|
|
if c.dryRun {
|
|
fmt.Printf(" [DRY-RUN] DELETE %s\n", path)
|
|
return nil
|
|
}
|
|
_, err := c.doRequest(http.MethodDelete, path, nil)
|
|
return err
|
|
}
|
|
|
|
// --- Namespace API ---
|
|
|
|
type NamespaceRequest struct {
|
|
Name string `json:"name"`
|
|
Status string `json:"status,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
}
|
|
|
|
type NamespaceResponse struct {
|
|
ID uint `json:"id"`
|
|
Name string `json:"name"`
|
|
Status string `json:"status"`
|
|
Description string `json:"description"`
|
|
}
|
|
|
|
func (c *Client) ListNamespaces() ([]NamespaceResponse, error) {
|
|
data, err := c.get("/admin/namespaces")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var result []NamespaceResponse
|
|
if err := json.Unmarshal(data, &result); err != nil {
|
|
return nil, fmt.Errorf("unmarshal namespaces: %w", err)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (c *Client) CreateNamespace(req NamespaceRequest) (*NamespaceResponse, error) {
|
|
data, err := c.post("/admin/namespaces", req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var result NamespaceResponse
|
|
if err := json.Unmarshal(data, &result); err != nil {
|
|
return nil, fmt.Errorf("unmarshal namespace: %w", err)
|
|
}
|
|
return &result, nil
|
|
}
|
|
|
|
func (c *Client) DeleteNamespace(id uint) error {
|
|
return c.delete(fmt.Sprintf("/admin/namespaces/%d", id))
|
|
}
|
|
|
|
// --- ProviderGroup API ---
|
|
|
|
type ProviderGroupRequest struct {
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
BaseURL string `json:"base_url,omitempty"`
|
|
GoogleProject string `json:"google_project,omitempty"`
|
|
GoogleLocation string `json:"google_location,omitempty"`
|
|
StaticHeaders string `json:"static_headers,omitempty"`
|
|
HeadersProfile string `json:"headers_profile,omitempty"`
|
|
Models []string `json:"models,omitempty"` // Request uses []string
|
|
Status string `json:"status,omitempty"`
|
|
}
|
|
|
|
type ProviderGroupResponse struct {
|
|
ID uint `json:"id"`
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
BaseURL string `json:"base_url"`
|
|
GoogleProject string `json:"google_project"`
|
|
GoogleLocation string `json:"google_location"`
|
|
Models string `json:"models"` // Response is comma-separated string
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
// GetModelsSlice returns the Models field as a slice with trimmed whitespace
|
|
func (p *ProviderGroupResponse) GetModelsSlice() []string {
|
|
if p.Models == "" {
|
|
return nil
|
|
}
|
|
parts := strings.Split(p.Models, ",")
|
|
for i, part := range parts {
|
|
parts[i] = strings.TrimSpace(part)
|
|
}
|
|
return parts
|
|
}
|
|
|
|
func (c *Client) ListProviderGroups() ([]ProviderGroupResponse, error) {
|
|
data, err := c.get("/admin/provider-groups")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var result []ProviderGroupResponse
|
|
if err := json.Unmarshal(data, &result); err != nil {
|
|
return nil, fmt.Errorf("unmarshal provider groups: %w", err)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (c *Client) CreateProviderGroup(req ProviderGroupRequest) (*ProviderGroupResponse, error) {
|
|
data, err := c.post("/admin/provider-groups", req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var result ProviderGroupResponse
|
|
if err := json.Unmarshal(data, &result); err != nil {
|
|
return nil, fmt.Errorf("unmarshal provider group: %w", err)
|
|
}
|
|
return &result, nil
|
|
}
|
|
|
|
func (c *Client) DeleteProviderGroup(id uint) error {
|
|
return c.delete(fmt.Sprintf("/admin/provider-groups/%d", id))
|
|
}
|
|
|
|
// --- APIKey (Provider) API ---
|
|
|
|
type APIKeyRequest struct {
|
|
GroupID uint `json:"group_id"`
|
|
APIKey string `json:"api_key"`
|
|
AccessToken string `json:"access_token,omitempty"`
|
|
RefreshToken string `json:"refresh_token,omitempty"`
|
|
ExpiresAt *string `json:"expires_at,omitempty"`
|
|
AccountID string `json:"account_id,omitempty"`
|
|
ProjectID string `json:"project_id,omitempty"`
|
|
Weight int `json:"weight,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
AutoBan *bool `json:"auto_ban,omitempty"`
|
|
}
|
|
|
|
type APIKeyResponse struct {
|
|
ID uint `json:"id"`
|
|
GroupID uint `json:"group_id"`
|
|
APIKey string `json:"api_key"`
|
|
Weight int `json:"weight"`
|
|
Status string `json:"status"`
|
|
AutoBan bool `json:"auto_ban"`
|
|
}
|
|
|
|
func (c *Client) ListAPIKeys() ([]APIKeyResponse, error) {
|
|
data, err := c.get("/admin/api-keys")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var result []APIKeyResponse
|
|
if err := json.Unmarshal(data, &result); err != nil {
|
|
return nil, fmt.Errorf("unmarshal api keys: %w", err)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (c *Client) CreateAPIKey(req APIKeyRequest) (*APIKeyResponse, error) {
|
|
data, err := c.post("/admin/api-keys", req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var result APIKeyResponse
|
|
if err := json.Unmarshal(data, &result); err != nil {
|
|
return nil, fmt.Errorf("unmarshal api key: %w", err)
|
|
}
|
|
return &result, nil
|
|
}
|
|
|
|
func (c *Client) DeleteAPIKey(id uint) error {
|
|
return c.delete(fmt.Sprintf("/admin/api-keys/%d", id))
|
|
}
|
|
|
|
// --- Model API ---
|
|
|
|
type ModelRequest struct {
|
|
Name string `json:"name"`
|
|
Kind string `json:"kind,omitempty"`
|
|
ContextWindow int `json:"context_window,omitempty"`
|
|
CostPerToken float64 `json:"cost_per_token,omitempty"`
|
|
SupportsVision bool `json:"supports_vision,omitempty"`
|
|
SupportsFunctions bool `json:"supports_functions,omitempty"`
|
|
SupportsToolChoice bool `json:"supports_tool_choice,omitempty"`
|
|
SupportsFIM bool `json:"supports_fim,omitempty"`
|
|
MaxOutputTokens int `json:"max_output_tokens,omitempty"`
|
|
}
|
|
|
|
type ModelResponse struct {
|
|
ID uint `json:"id"`
|
|
Name string `json:"name"`
|
|
Kind string `json:"kind"`
|
|
ContextWindow int `json:"context_window"`
|
|
CostPerToken float64 `json:"cost_per_token"`
|
|
SupportsVision bool `json:"supports_vision"`
|
|
SupportsFunctions bool `json:"supports_functions"`
|
|
SupportsToolChoice bool `json:"supports_tool_choice"`
|
|
SupportsFIM bool `json:"supports_fim"`
|
|
MaxOutputTokens int `json:"max_output_tokens"`
|
|
}
|
|
|
|
func (c *Client) ListModels() ([]ModelResponse, error) {
|
|
data, err := c.get("/admin/models")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var result []ModelResponse
|
|
if err := json.Unmarshal(data, &result); err != nil {
|
|
return nil, fmt.Errorf("unmarshal models: %w", err)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (c *Client) CreateModel(req ModelRequest) (*ModelResponse, error) {
|
|
data, err := c.post("/admin/models", req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var result ModelResponse
|
|
if err := json.Unmarshal(data, &result); err != nil {
|
|
return nil, fmt.Errorf("unmarshal model: %w", err)
|
|
}
|
|
return &result, nil
|
|
}
|
|
|
|
func (c *Client) DeleteModel(id uint) error {
|
|
return c.delete(fmt.Sprintf("/admin/models/%d", id))
|
|
}
|
|
|
|
// --- Binding API ---
|
|
|
|
type BindingRequest struct {
|
|
Namespace string `json:"namespace"`
|
|
PublicModel string `json:"public_model"`
|
|
GroupID uint `json:"group_id"`
|
|
Weight int `json:"weight,omitempty"`
|
|
SelectorType string `json:"selector_type,omitempty"`
|
|
SelectorValue string `json:"selector_value,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
}
|
|
|
|
type BindingResponse struct {
|
|
ID uint `json:"id"`
|
|
Namespace string `json:"namespace"`
|
|
PublicModel string `json:"public_model"`
|
|
GroupID uint `json:"group_id"`
|
|
Weight int `json:"weight"`
|
|
SelectorType string `json:"selector_type"`
|
|
SelectorValue string `json:"selector_value"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
func (c *Client) ListBindings() ([]BindingResponse, error) {
|
|
data, err := c.get("/admin/bindings")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var result []BindingResponse
|
|
if err := json.Unmarshal(data, &result); err != nil {
|
|
return nil, fmt.Errorf("unmarshal bindings: %w", err)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (c *Client) CreateBinding(req BindingRequest) (*BindingResponse, error) {
|
|
data, err := c.post("/admin/bindings", req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var result BindingResponse
|
|
if err := json.Unmarshal(data, &result); err != nil {
|
|
return nil, fmt.Errorf("unmarshal binding: %w", err)
|
|
}
|
|
return &result, nil
|
|
}
|
|
|
|
func (c *Client) DeleteBinding(id uint) error {
|
|
return c.delete(fmt.Sprintf("/admin/bindings/%d", id))
|
|
}
|
|
|
|
// --- Master API ---
|
|
|
|
type MasterRequest struct {
|
|
Name string `json:"name"`
|
|
Group string `json:"group"`
|
|
MaxChildKeys int `json:"max_child_keys,omitempty"`
|
|
GlobalQPS int `json:"global_qps,omitempty"`
|
|
}
|
|
|
|
type MasterResponse struct {
|
|
ID uint `json:"id"`
|
|
Name string `json:"name"`
|
|
Group string `json:"group"`
|
|
MasterKey string `json:"master_key,omitempty"` // Only returned on create
|
|
MaxChildKeys int `json:"max_child_keys"`
|
|
GlobalQPS int `json:"global_qps"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
func (c *Client) ListMasters() ([]MasterResponse, error) {
|
|
data, err := c.get("/admin/masters")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var result []MasterResponse
|
|
if err := json.Unmarshal(data, &result); err != nil {
|
|
return nil, fmt.Errorf("unmarshal masters: %w", err)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (c *Client) CreateMaster(req MasterRequest) (*MasterResponse, error) {
|
|
data, err := c.post("/admin/masters", req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var result MasterResponse
|
|
if err := json.Unmarshal(data, &result); err != nil {
|
|
return nil, fmt.Errorf("unmarshal master: %w", err)
|
|
}
|
|
return &result, nil
|
|
}
|
|
|
|
func (c *Client) DeleteMaster(id uint) error {
|
|
return c.delete(fmt.Sprintf("/admin/masters/%d", id))
|
|
}
|
|
|
|
// --- Key API ---
|
|
|
|
type KeyRequest struct {
|
|
Group string `json:"group,omitempty"`
|
|
Scopes string `json:"scopes,omitempty"`
|
|
ModelLimits string `json:"model_limits,omitempty"`
|
|
ModelLimitsEnabled *bool `json:"model_limits_enabled,omitempty"`
|
|
ExpiresAt *string `json:"expires_at,omitempty"`
|
|
AllowIPs string `json:"allow_ips,omitempty"`
|
|
DenyIPs string `json:"deny_ips,omitempty"`
|
|
}
|
|
|
|
type KeyResponse struct {
|
|
ID uint `json:"id"`
|
|
KeySecret string `json:"key_secret,omitempty"` // Only returned on create
|
|
Group string `json:"group"`
|
|
Scopes string `json:"scopes"`
|
|
IssuedBy string `json:"issued_by"`
|
|
Status string `json:"status"`
|
|
}
|
|
|
|
func (c *Client) CreateKey(masterID uint, req KeyRequest) (*KeyResponse, error) {
|
|
data, err := c.post(fmt.Sprintf("/admin/masters/%d/keys", masterID), req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var result KeyResponse
|
|
if err := json.Unmarshal(data, &result); err != nil {
|
|
return nil, fmt.Errorf("unmarshal key: %w", err)
|
|
}
|
|
return &result, nil
|
|
}
|
|
|
|
func (c *Client) ListKeys(masterID uint) ([]KeyResponse, error) {
|
|
data, err := c.get(fmt.Sprintf("/admin/masters/%d/keys", masterID))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var result []KeyResponse
|
|
if err := json.Unmarshal(data, &result); err != nil {
|
|
return nil, fmt.Errorf("unmarshal keys: %w", err)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (c *Client) DeleteKey(masterID, keyID uint) error {
|
|
return c.delete(fmt.Sprintf("/admin/masters/%d/keys/%d", masterID, keyID))
|
|
}
|
|
|
|
// --- Log API ---
|
|
|
|
type LogRequest struct {
|
|
Group string `json:"group,omitempty"`
|
|
MasterID uint `json:"master_id,omitempty"`
|
|
KeyID uint `json:"key_id,omitempty"`
|
|
Model string `json:"model,omitempty"` // Field name is "model" not "model_name"
|
|
ProviderID uint `json:"provider_id,omitempty"`
|
|
ProviderType string `json:"provider_type,omitempty"`
|
|
ProviderName string `json:"provider_name,omitempty"`
|
|
StatusCode int `json:"status_code,omitempty"`
|
|
LatencyMs int64 `json:"latency_ms,omitempty"`
|
|
TokensIn int64 `json:"tokens_in,omitempty"`
|
|
TokensOut int64 `json:"tokens_out,omitempty"`
|
|
ErrorMessage string `json:"error_message,omitempty"`
|
|
ClientIP string `json:"client_ip,omitempty"`
|
|
RequestSize int64 `json:"request_size,omitempty"`
|
|
ResponseSize int64 `json:"response_size,omitempty"`
|
|
}
|
|
|
|
func (c *Client) CreateLog(req LogRequest) error {
|
|
_, err := c.post("/logs", req)
|
|
return err
|
|
}
|
|
|
|
func (c *Client) CreateLogsBatch(logs []LogRequest) error {
|
|
for _, log := range logs {
|
|
if err := c.CreateLog(log); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|