fix fresh-host acceptance and document real-host debugging learnings
This commit is contained in:
@@ -34,9 +34,13 @@ func (c *Client) BatchCreateAccounts(ctx context.Context, req BatchCreateAccount
|
||||
return models, nil
|
||||
}
|
||||
|
||||
func (c *Client) TestAccount(ctx context.Context, accountID string) (ProbeResult, error) {
|
||||
func (c *Client) TestAccount(ctx context.Context, accountID, modelID string) (ProbeResult, error) {
|
||||
path := "/api/v1/admin/accounts/" + accountID + "/test"
|
||||
statusCode, _, body, err := c.perform(ctx, http.MethodPost, path, map[string]any{})
|
||||
req := map[string]any{}
|
||||
if strings.TrimSpace(modelID) != "" {
|
||||
req["model_id"] = strings.TrimSpace(modelID)
|
||||
}
|
||||
statusCode, _, body, err := c.perform(ctx, http.MethodPost, path, req)
|
||||
if err != nil {
|
||||
return ProbeResult{}, err
|
||||
}
|
||||
@@ -158,35 +162,77 @@ func parseProbeResult(body []byte) (ProbeResult, error) {
|
||||
return ProbeResult{}, fmt.Errorf("missing data event")
|
||||
}
|
||||
|
||||
var event struct {
|
||||
type probeEvent struct {
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Error string `json:"error"`
|
||||
OK *bool `json:"ok"`
|
||||
Success *bool `json:"success"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(payloads[len(payloads)-1]), &event); err != nil {
|
||||
return ProbeResult{}, err
|
||||
}
|
||||
|
||||
ok := false
|
||||
switch {
|
||||
case event.OK != nil:
|
||||
ok = *event.OK
|
||||
case event.Success != nil:
|
||||
ok = *event.Success
|
||||
default:
|
||||
switch strings.ToLower(strings.TrimSpace(event.Status)) {
|
||||
case "ok", "pass", "passed", "success", "succeeded":
|
||||
ok = true
|
||||
var latest ProbeResult
|
||||
sawProbeState := false
|
||||
|
||||
for _, payload := range payloads {
|
||||
var event probeEvent
|
||||
if err := json.Unmarshal([]byte(payload), &event); err != nil {
|
||||
return ProbeResult{}, err
|
||||
}
|
||||
|
||||
message := strings.TrimSpace(event.Message)
|
||||
if message == "" {
|
||||
message = strings.TrimSpace(event.Error)
|
||||
}
|
||||
|
||||
eventType := strings.ToLower(strings.TrimSpace(event.Type))
|
||||
if eventType == "error" || strings.TrimSpace(event.Error) != "" {
|
||||
if message == "" {
|
||||
message = "account probe returned an error event"
|
||||
}
|
||||
return ProbeResult{
|
||||
OK: false,
|
||||
Status: "failed",
|
||||
Message: message,
|
||||
}, nil
|
||||
}
|
||||
|
||||
ok := false
|
||||
switch {
|
||||
case event.OK != nil:
|
||||
ok = *event.OK
|
||||
case event.Success != nil:
|
||||
ok = *event.Success
|
||||
default:
|
||||
switch strings.ToLower(strings.TrimSpace(event.Status)) {
|
||||
case "ok", "pass", "passed", "success", "succeeded":
|
||||
ok = true
|
||||
}
|
||||
}
|
||||
|
||||
if eventType == "test_complete" {
|
||||
return ProbeResult{
|
||||
OK: ok,
|
||||
Status: normalizeProbeStatus(event.Status, ok),
|
||||
Message: message,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if event.Status != "" || event.OK != nil || event.Success != nil {
|
||||
latest = ProbeResult{
|
||||
OK: ok,
|
||||
Status: normalizeProbeStatus(event.Status, ok),
|
||||
Message: message,
|
||||
}
|
||||
sawProbeState = true
|
||||
}
|
||||
}
|
||||
|
||||
status := normalizeProbeStatus(event.Status, ok)
|
||||
return ProbeResult{
|
||||
OK: ok,
|
||||
Status: status,
|
||||
Message: strings.TrimSpace(event.Message),
|
||||
}, nil
|
||||
if sawProbeState {
|
||||
return latest, nil
|
||||
}
|
||||
|
||||
return ProbeResult{}, fmt.Errorf("missing probe status event")
|
||||
}
|
||||
|
||||
func normalizeProbeStatus(status string, ok bool) string {
|
||||
|
||||
@@ -25,11 +25,12 @@ type HostAdapter interface {
|
||||
CreateAccount(ctx context.Context, req CreateAccountRequest) (AccountRef, error)
|
||||
BatchCreateAccounts(ctx context.Context, req BatchCreateAccountsRequest) ([]AccountRef, error)
|
||||
DeleteAccount(ctx context.Context, accountID string) error
|
||||
TestAccount(ctx context.Context, accountID string) (ProbeResult, error)
|
||||
TestAccount(ctx context.Context, accountID, modelID string) (ProbeResult, error)
|
||||
GetAccountModels(ctx context.Context, accountID string) ([]AccountModel, error)
|
||||
EnsureSubscriptionAccess(ctx context.Context, req EnsureSubscriptionAccessRequest) (SubscriptionAccessRef, error)
|
||||
AssignSubscription(ctx context.Context, req AssignSubscriptionRequest) (SubscriptionRef, error)
|
||||
CheckGatewayAccess(ctx context.Context, req GatewayAccessCheckRequest) (GatewayAccessResult, error)
|
||||
CheckGatewayCompletion(ctx context.Context, req GatewayCompletionCheckRequest) (GatewayCompletionResult, error)
|
||||
ListManagedResources(ctx context.Context, req ListManagedResourcesRequest) (ManagedResourceSnapshot, error)
|
||||
}
|
||||
|
||||
@@ -159,6 +160,20 @@ type SubscriptionRef struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type GatewayCompletionCheckRequest struct {
|
||||
APIKey string
|
||||
Model string
|
||||
Prompt string
|
||||
MaxTokens int
|
||||
}
|
||||
|
||||
type GatewayCompletionResult struct {
|
||||
OK bool `json:"ok"`
|
||||
StatusCode int `json:"status_code"`
|
||||
ContentType string `json:"content_type,omitempty"`
|
||||
BodyPreview string `json:"body_preview,omitempty"`
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
baseURL *url.URL
|
||||
httpClient *http.Client
|
||||
|
||||
@@ -17,12 +17,16 @@ type GatewayAccessResult struct {
|
||||
StatusCode int `json:"status_code"`
|
||||
Models []string `json:"models"`
|
||||
HasExpectedModel bool `json:"has_expected_model"`
|
||||
CompletionOK bool `json:"completion_ok"`
|
||||
CompletionStatus int `json:"completion_status"`
|
||||
CompletionType string `json:"completion_content_type,omitempty"`
|
||||
CompletionBody string `json:"completion_body_preview,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Client) CheckGatewayAccess(ctx context.Context, req GatewayAccessCheckRequest) (GatewayAccessResult, error) {
|
||||
gatewayClient := *c
|
||||
gatewayClient.apiKey = strings.TrimSpace(req.APIKey)
|
||||
gatewayClient.bearerToken = ""
|
||||
gatewayClient.apiKey = ""
|
||||
gatewayClient.bearerToken = strings.TrimSpace(req.APIKey)
|
||||
|
||||
statusCode, _, body, err := gatewayClient.perform(ctx, http.MethodGet, "/v1/models", nil)
|
||||
if err != nil {
|
||||
@@ -43,6 +47,44 @@ func (c *Client) CheckGatewayAccess(ctx context.Context, req GatewayAccessCheckR
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *Client) CheckGatewayCompletion(ctx context.Context, req GatewayCompletionCheckRequest) (GatewayCompletionResult, error) {
|
||||
gatewayClient := *c
|
||||
gatewayClient.apiKey = ""
|
||||
gatewayClient.bearerToken = strings.TrimSpace(req.APIKey)
|
||||
|
||||
model := strings.TrimSpace(req.Model)
|
||||
if model == "" {
|
||||
return GatewayCompletionResult{}, nil
|
||||
}
|
||||
prompt := strings.TrimSpace(req.Prompt)
|
||||
if prompt == "" {
|
||||
prompt = "ping"
|
||||
}
|
||||
maxTokens := req.MaxTokens
|
||||
if maxTokens <= 0 {
|
||||
maxTokens = 8
|
||||
}
|
||||
payload := map[string]any{
|
||||
"model": model,
|
||||
"messages": []map[string]string{
|
||||
{"role": "user", "content": prompt},
|
||||
},
|
||||
"max_tokens": maxTokens,
|
||||
"temperature": 0,
|
||||
}
|
||||
|
||||
statusCode, headers, body, err := gatewayClient.perform(ctx, http.MethodPost, "/v1/chat/completions", payload)
|
||||
if err != nil {
|
||||
return GatewayCompletionResult{}, err
|
||||
}
|
||||
return GatewayCompletionResult{
|
||||
OK: statusCode >= http.StatusOK && statusCode < http.StatusMultipleChoices,
|
||||
StatusCode: statusCode,
|
||||
ContentType: strings.TrimSpace(headers.Get("Content-Type")),
|
||||
BodyPreview: previewGatewayBody(body, 400),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func decodeGatewayModelIDs(body []byte) []string {
|
||||
var payload struct {
|
||||
Data []struct {
|
||||
@@ -60,3 +102,11 @@ func decodeGatewayModelIDs(body []byte) []string {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func previewGatewayBody(body []byte, limit int) string {
|
||||
trimmed := strings.TrimSpace(string(body))
|
||||
if limit <= 0 || len(trimmed) <= limit {
|
||||
return trimmed
|
||||
}
|
||||
return trimmed[:limit]
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -20,7 +22,7 @@ func (c *Client) ListManagedResources(ctx context.Context, req ListManagedResour
|
||||
if err != nil {
|
||||
return ManagedResourceSnapshot{}, fmt.Errorf("list plans: %w", err)
|
||||
}
|
||||
accounts, err := c.listNamedResources(ctx, "/api/v1/admin/accounts", "")
|
||||
accounts, err := c.listNamedResourcesPaged(ctx, "/api/v1/admin/accounts", 100)
|
||||
if err != nil {
|
||||
return ManagedResourceSnapshot{}, fmt.Errorf("list accounts: %w", err)
|
||||
}
|
||||
@@ -34,36 +36,71 @@ func (c *Client) ListManagedResources(ctx context.Context, req ListManagedResour
|
||||
}
|
||||
|
||||
func (c *Client) listNamedResources(ctx context.Context, path, expectedName string) ([]NamedResource, error) {
|
||||
statusCode, _, body, err := c.perform(ctx, "GET", path, nil)
|
||||
resources, _, err := c.listNamedResourcesPage(ctx, path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if statusCode < 200 || statusCode >= 300 {
|
||||
return nil, newHTTPError("GET", path, statusCode, body)
|
||||
}
|
||||
|
||||
resources, err := decodeNamedResources(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decode %s response: %w", path, err)
|
||||
}
|
||||
return filterNamedResourcesByName(resources, expectedName), nil
|
||||
}
|
||||
|
||||
func decodeNamedResources(body []byte) ([]NamedResource, error) {
|
||||
func (c *Client) listNamedResourcesPaged(ctx context.Context, path string, pageSize int) ([]NamedResource, error) {
|
||||
if pageSize <= 0 {
|
||||
pageSize = 100
|
||||
}
|
||||
page := 1
|
||||
all := make([]NamedResource, 0)
|
||||
for {
|
||||
query := url.Values{}
|
||||
query.Set("page", strconv.Itoa(page))
|
||||
query.Set("page_size", strconv.Itoa(pageSize))
|
||||
resources, pages, err := c.listNamedResourcesPage(ctx, path+"?"+query.Encode())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
all = append(all, resources...)
|
||||
if pages <= page || pages == 0 {
|
||||
return all, nil
|
||||
}
|
||||
page++
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) listNamedResourcesPage(ctx context.Context, path string) ([]NamedResource, int, error) {
|
||||
statusCode, _, body, err := c.perform(ctx, "GET", path, nil)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if statusCode < 200 || statusCode >= 300 {
|
||||
return nil, 0, newHTTPError("GET", path, statusCode, body)
|
||||
}
|
||||
|
||||
resources, pages, err := decodeNamedResources(body)
|
||||
if err != nil {
|
||||
return nil, 0, fmt.Errorf("decode %s response: %w", path, err)
|
||||
}
|
||||
return resources, pages, nil
|
||||
}
|
||||
|
||||
func decodeNamedResources(body []byte) ([]NamedResource, int, error) {
|
||||
var resources []NamedResource
|
||||
if err := decodeEnvelopeObject(body, &resources); err == nil {
|
||||
return resources, nil
|
||||
return resources, 1, nil
|
||||
}
|
||||
|
||||
var wrapper struct {
|
||||
Data struct {
|
||||
Items []NamedResource `json:"items"`
|
||||
Pages int `json:"pages"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &wrapper); err != nil {
|
||||
return nil, err
|
||||
return nil, 0, err
|
||||
}
|
||||
return wrapper.Data.Items, nil
|
||||
pages := wrapper.Data.Pages
|
||||
if pages <= 0 {
|
||||
pages = 1
|
||||
}
|
||||
return wrapper.Data.Items, pages, nil
|
||||
}
|
||||
|
||||
func filterNamedResourcesByName(resources []NamedResource, expectedName string) []NamedResource {
|
||||
|
||||
@@ -228,34 +228,43 @@ func TestFilterNamedResourcesByPrefix(t *testing.T) {
|
||||
|
||||
func TestDecodeNamedResources(t *testing.T) {
|
||||
t.Run("envelope", func(t *testing.T) {
|
||||
resources, err := decodeNamedResources([]byte(`{"data":[{"id":"r1","name":"n1"}]}`))
|
||||
resources, pages, err := decodeNamedResources([]byte(`{"data":[{"id":"r1","name":"n1"}]}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pages != 1 {
|
||||
t.Fatalf("pages = %d, want 1", pages)
|
||||
}
|
||||
if len(resources) != 1 || resources[0].ID != "r1" {
|
||||
t.Fatalf("got %+v", resources)
|
||||
}
|
||||
})
|
||||
t.Run("numeric id", func(t *testing.T) {
|
||||
resources, err := decodeNamedResources([]byte(`{"data":{"items":[{"id":1,"name":"default"}]}}`))
|
||||
resources, pages, err := decodeNamedResources([]byte(`{"data":{"items":[{"id":1,"name":"default"}],"pages":2}}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pages != 2 {
|
||||
t.Fatalf("pages = %d, want 2", pages)
|
||||
}
|
||||
if len(resources) != 1 || resources[0].ID != "1" {
|
||||
t.Fatalf("got %+v", resources)
|
||||
}
|
||||
})
|
||||
t.Run("wrapper with items", func(t *testing.T) {
|
||||
resources, err := decodeNamedResources([]byte(`{"data":{"items":[{"id":"r2","name":"n2"}]}}`))
|
||||
resources, pages, err := decodeNamedResources([]byte(`{"data":{"items":[{"id":"r2","name":"n2"}]}}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if pages != 1 {
|
||||
t.Fatalf("pages = %d, want 1", pages)
|
||||
}
|
||||
if len(resources) != 1 || resources[0].ID != "r2" {
|
||||
t.Fatalf("got %+v", resources)
|
||||
}
|
||||
})
|
||||
t.Run("invalid json", func(t *testing.T) {
|
||||
_, err := decodeNamedResources([]byte(`not json`))
|
||||
_, _, err := decodeNamedResources([]byte(`not json`))
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
@@ -904,6 +913,12 @@ func TestEnsureSubscriptionAccessWithMock(t *testing.T) {
|
||||
|
||||
func TestCheckGatewayAccessWithMock(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got := r.Header.Get("Authorization"); got != "Bearer gk" {
|
||||
t.Fatalf("Authorization = %q, want %q", got, "Bearer gk")
|
||||
}
|
||||
if got := r.Header.Get("x-api-key"); got != "" {
|
||||
t.Fatalf("x-api-key = %q, want empty", got)
|
||||
}
|
||||
w.Write([]byte(`{"data":[{"id":"gpt-4"},{"id":"claude-3"}]}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
@@ -920,6 +935,50 @@ func TestCheckGatewayAccessWithMock(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckGatewayCompletionWithMock(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/v1/chat/completions" {
|
||||
t.Fatalf("path = %q, want /v1/chat/completions", r.URL.Path)
|
||||
}
|
||||
if got := r.Header.Get("Authorization"); got != "Bearer gk" {
|
||||
t.Fatalf("Authorization = %q, want %q", got, "Bearer gk")
|
||||
}
|
||||
if got := r.Header.Get("x-api-key"); got != "" {
|
||||
t.Fatalf("x-api-key = %q, want empty", got)
|
||||
}
|
||||
var payload struct {
|
||||
Model string `json:"model"`
|
||||
MaxTokens int `json:"max_tokens"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
t.Fatalf("decode request: %v", err)
|
||||
}
|
||||
if payload.Model != "gpt-4" {
|
||||
t.Fatalf("model = %q, want gpt-4", payload.Model)
|
||||
}
|
||||
if payload.MaxTokens != 8 {
|
||||
t.Fatalf("max_tokens = %d, want 8", payload.MaxTokens)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"choices":[{"message":{"content":"pong"}}]}`))
|
||||
}))
|
||||
defer srv.Close()
|
||||
client, _ := NewClient(srv.URL, WithAPIKey("k"))
|
||||
result, err := client.CheckGatewayCompletion(context.Background(), GatewayCompletionCheckRequest{APIKey: "gk", Model: "gpt-4"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !result.OK {
|
||||
t.Fatal("expected completion OK=true")
|
||||
}
|
||||
if result.StatusCode != 200 {
|
||||
t.Fatalf("status = %d, want 200", result.StatusCode)
|
||||
}
|
||||
if result.ContentType != "application/json" {
|
||||
t.Fatalf("content type = %q, want application/json", result.ContentType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchCreateAccountsWithMock(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
@@ -1037,19 +1096,93 @@ func TestListManagedResourcesWithMock(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTestAccountWithMock(t *testing.T) {
|
||||
func TestListManagedResourcesLoadsAllAccountPages(t *testing.T) {
|
||||
accountPages := 0
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/api/v1/admin/groups", "/api/v1/admin/channels":
|
||||
_, _ = w.Write([]byte(`{"data":{"items":[{"id":"r1","name":"resource-1"}],"total":1,"page":1,"page_size":20,"pages":1}}`))
|
||||
case "/api/v1/admin/payment/plans":
|
||||
_, _ = w.Write([]byte(`{"data":[{"id":"plan_1","name":"plan-1"}]}`))
|
||||
case "/api/v1/admin/accounts":
|
||||
accountPages++
|
||||
page := r.URL.Query().Get("page")
|
||||
if page == "" {
|
||||
page = "1"
|
||||
}
|
||||
if got := r.URL.Query().Get("page_size"); got != "100" {
|
||||
t.Fatalf("page_size = %q, want 100", got)
|
||||
}
|
||||
switch page {
|
||||
case "1":
|
||||
_, _ = w.Write([]byte(`{"data":{"items":[{"id":"account_1","name":"deepseek-01"}],"total":2,"page":1,"page_size":100,"pages":2}}`))
|
||||
case "2":
|
||||
_, _ = w.Write([]byte(`{"data":{"items":[{"id":"account_2","name":"deepseek-02"}],"total":2,"page":2,"page_size":100,"pages":2}}`))
|
||||
default:
|
||||
t.Fatalf("unexpected accounts page %q", page)
|
||||
}
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
client, _ := NewClient(srv.URL, WithAPIKey("k"))
|
||||
snapshot, err := client.ListManagedResources(context.Background(), ListManagedResourcesRequest{AccountNamePrefix: "deepseek-"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if accountPages != 2 {
|
||||
t.Fatalf("account pages fetched = %d, want 2", accountPages)
|
||||
}
|
||||
if len(snapshot.Accounts) != 2 || snapshot.Accounts[0].ID != "account_1" || snapshot.Accounts[1].ID != "account_2" {
|
||||
t.Fatalf("Accounts = %+v, want both paged accounts", snapshot.Accounts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTestAccountWithMock(t *testing.T) {
|
||||
var requestBody map[string]any
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := json.NewDecoder(r.Body).Decode(&requestBody); err != nil {
|
||||
t.Fatalf("decode request body: %v", err)
|
||||
}
|
||||
w.Write([]byte("data: {\"status\":\"passed\",\"ok\":true}\n"))
|
||||
}))
|
||||
defer srv.Close()
|
||||
client, _ := NewClient(srv.URL, WithAPIKey("k"))
|
||||
result, err := client.TestAccount(context.Background(), "a1")
|
||||
result, err := client.TestAccount(context.Background(), "a1", "MiniMax-M2.7-highspeed")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !result.OK {
|
||||
t.Fatal("expected OK=true")
|
||||
}
|
||||
if got := requestBody["model_id"]; got != "MiniMax-M2.7-highspeed" {
|
||||
t.Fatalf("model_id = %#v, want MiniMax-M2.7-highspeed", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTestAccountWithMockSSEError(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Write([]byte("data: {\"type\":\"test_start\",\"model\":\"MiniMax-M2.7-highspeed\"}\n\n"))
|
||||
w.Write([]byte("data: {\"type\":\"error\",\"error\":\"账号本身可正常使用,但当前测试接口仅支持 Responses API 路径。请直接通过实际 API 调用验证。\"}\n\n"))
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
client, _ := NewClient(srv.URL, WithAPIKey("k"))
|
||||
result, err := client.TestAccount(context.Background(), "a1", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.OK {
|
||||
t.Fatal("expected OK=false for SSE error event")
|
||||
}
|
||||
if result.Status != "failed" {
|
||||
t.Fatalf("Status = %q, want failed", result.Status)
|
||||
}
|
||||
if !strings.Contains(result.Message, "测试接口仅支持 Responses API 路径") {
|
||||
t.Fatalf("Message = %q, want propagated SSE error message", result.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAccountModelsWithMock(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user