Files
ez-api/cmd/seeder/client.go
zenfun 5431e24923 fix(seeder): correct log generation fields
- Parse provider group models from API response string and expose as slice
- Send `model` field (not `model_name`) when creating logs
- Use API key ID as `provider_id` instead of provider group ID
- Restrict reset behavior to resources matching seeder tag/prefix
- Refactor usage sample generation to accept a context struct
2026-01-10 00:46:03 +08:00

508 lines
14 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
func (p *ProviderGroupResponse) GetModelsSlice() []string {
if p.Models == "" {
return nil
}
return strings.Split(p.Models, ",")
}
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"`
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
}