mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
feat(seeder): add control plane data seeder
Introduce a `cmd/seeder` CLI to generate deterministic demo datasets and seed them into the Control Plane via admin endpoints, supporting reset, dry-run, profiles, and usage sample generation. Add Cobra/Viper dependencies to support the new command.
This commit is contained in:
498
cmd/seeder/client.go
Normal file
498
cmd/seeder/client.go
Normal file
@@ -0,0 +1,498 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"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"`
|
||||
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"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// --- Log API ---
|
||||
|
||||
type LogRequest struct {
|
||||
Group string `json:"group,omitempty"`
|
||||
MasterID uint `json:"master_id,omitempty"`
|
||||
KeyID uint `json:"key_id,omitempty"`
|
||||
ModelName string `json:"model_name,omitempty"`
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user