feat(groups): 增加模型详情与别名校验
This commit is contained in:
11 files changed
+334
-51
No files matched your search
@@ -41,7 +41,6 @@ func TestCreateGroupNormalizesAndPublishesOnce(t *testing.T) {
|
||||
Set: true,
|
||||
Values: []GroupModel{
|
||||
{ID: " gpt-4o ", Alias: " primary "},
|
||||
{ID: "gpt-4o", Alias: "primary"},
|
||||
{ID: "claude-3-5", Alias: ""},
|
||||
},
|
||||
},
|
||||
@@ -124,8 +123,9 @@ func TestCreateGroupRejectsDuplicateExternalModelWithoutMutation(t *testing.T) {
|
||||
},
|
||||
Keys: "sk-duplicate-external-model",
|
||||
})
|
||||
if !errors.Is(err, app_errors.ErrValidation) {
|
||||
t.Fatalf("CreateGroup() error = %v, want ErrValidation", err)
|
||||
var apiErr *app_errors.APIError
|
||||
if !errors.As(err, &apiErr) || apiErr.Code != app_errors.ErrModelNameConflict.Code {
|
||||
t.Fatalf("CreateGroup() error = %#v, want MODEL_NAME_CONFLICT", err)
|
||||
}
|
||||
assertGroupCount(t, fixture.db, 0)
|
||||
var keyCount int64
|
||||
|
||||
@@ -15,6 +15,69 @@ type GroupModelsUpdateRequest struct {
|
||||
Models optionalGroupModels `json:"models"`
|
||||
}
|
||||
|
||||
type GroupModelResponse struct {
|
||||
ID string `json:"id"`
|
||||
Alias string `json:"alias"`
|
||||
AliasEnabled bool `json:"alias_enabled"`
|
||||
ClientModel string `json:"client_model"`
|
||||
PricingStatus string `json:"pricing_status"`
|
||||
}
|
||||
|
||||
type GroupModelsResponse struct {
|
||||
Items []GroupModelResponse `json:"items"`
|
||||
Total int `json:"total"`
|
||||
Unpriced int `json:"unpriced"`
|
||||
}
|
||||
|
||||
type ModelNameConflict struct {
|
||||
ClientModel string `json:"client_model"`
|
||||
Indexes []int `json:"indexes"`
|
||||
}
|
||||
|
||||
type ModelNameConflictData struct {
|
||||
Conflicts []ModelNameConflict `json:"conflicts"`
|
||||
}
|
||||
|
||||
func (s *Service) GetGroupModels(ctx context.Context, groupID uint) (GroupModelsResponse, error) {
|
||||
if groupID == 0 {
|
||||
return GroupModelsResponse{}, app_errors.ErrBadRequest
|
||||
}
|
||||
|
||||
s.writeMu.RLock()
|
||||
defer s.writeMu.RUnlock()
|
||||
|
||||
detail, _, err := loadGroupDetail(s.db.WithContext(ctx), groupID)
|
||||
if err != nil {
|
||||
return GroupModelsResponse{}, err
|
||||
}
|
||||
table := s.priceRuntime.Load()
|
||||
if table == nil {
|
||||
return GroupModelsResponse{}, fmt.Errorf("pricing runtime unavailable: %w", app_errors.ErrInternalServer)
|
||||
}
|
||||
|
||||
result := GroupModelsResponse{Items: make([]GroupModelResponse, 0, len(detail.Models))}
|
||||
for _, model := range detail.Models {
|
||||
item := GroupModelResponse{
|
||||
ID: model.ID,
|
||||
Alias: model.Alias,
|
||||
AliasEnabled: model.Alias != "",
|
||||
ClientModel: model.ID,
|
||||
PricingStatus: "unpriced",
|
||||
}
|
||||
if item.AliasEnabled {
|
||||
item.ClientModel = model.Alias
|
||||
}
|
||||
if _, priced := table.Match(model.ID); priced {
|
||||
item.PricingStatus = "priced"
|
||||
} else {
|
||||
result.Unpriced++
|
||||
}
|
||||
result.Items = append(result.Items, item)
|
||||
}
|
||||
result.Total = len(result.Items)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *Service) UpdateGroupModels(
|
||||
ctx context.Context,
|
||||
groupID uint,
|
||||
|
||||
@@ -15,27 +15,145 @@ import (
|
||||
"gpt-load/internal/storage/models"
|
||||
)
|
||||
|
||||
func TestGetGroupModelsReturnsClientNamesAndPricingStatus(t *testing.T) {
|
||||
fixture := newServiceFixture(t)
|
||||
mustEnsureInitialPrices(t, fixture)
|
||||
created, err := fixture.service.CreateGroup(t.Context(), GroupCreateRequest{
|
||||
UpstreamURL: "https://model-read.example.com/v1",
|
||||
Protocols: []protocol.Protocol{protocol.OpenAICompletions},
|
||||
Models: optionalGroupModels{Set: true, Values: []GroupModel{
|
||||
{ID: "gpt-4o", Alias: "default", AliasEnabled: true},
|
||||
{ID: "missing-price", Alias: ""},
|
||||
}},
|
||||
Keys: "sk-model-read",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := fixture.service.GetGroupModels(t.Context(), created.GroupID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetGroupModels() error = %v", err)
|
||||
}
|
||||
want := GroupModelsResponse{
|
||||
Items: []GroupModelResponse{
|
||||
{ID: "gpt-4o", Alias: "default", AliasEnabled: true, ClientModel: "default", PricingStatus: "priced"},
|
||||
{ID: "missing-price", Alias: "", AliasEnabled: false, ClientModel: "missing-price", PricingStatus: "unpriced"},
|
||||
},
|
||||
Total: 2,
|
||||
Unpriced: 1,
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("GetGroupModels() = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeGroupModelsAppliesAliasSwitchAndReportsStableConflicts(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
values []GroupModel
|
||||
want []GroupModel
|
||||
wantConflicts []ModelNameConflict
|
||||
wantError error
|
||||
}{
|
||||
{
|
||||
name: "disabled alias uses upstream ID and conflicts with enabled alias",
|
||||
values: []GroupModel{
|
||||
{ID: "a", Alias: ""},
|
||||
{ID: "b", Alias: "a", AliasEnabled: true},
|
||||
},
|
||||
wantConflicts: []ModelNameConflict{{ClientModel: "a", Indexes: []int{0, 1}}},
|
||||
wantError: app_errors.ErrModelNameConflict,
|
||||
},
|
||||
{
|
||||
name: "enabled aliases conflict",
|
||||
values: []GroupModel{
|
||||
{ID: "a", Alias: "x", AliasEnabled: true},
|
||||
{ID: "b", Alias: "x", AliasEnabled: true},
|
||||
},
|
||||
wantConflicts: []ModelNameConflict{{ClientModel: "x", Indexes: []int{0, 1}}},
|
||||
wantError: app_errors.ErrModelNameConflict,
|
||||
},
|
||||
{
|
||||
name: "model names remain case sensitive",
|
||||
values: []GroupModel{
|
||||
{ID: "a", Alias: "X", AliasEnabled: true},
|
||||
{ID: "b", Alias: "x", AliasEnabled: true},
|
||||
},
|
||||
want: []GroupModel{
|
||||
{ID: "a", Alias: "X"},
|
||||
{ID: "b", Alias: "x"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "trimmed IDs and aliases conflict",
|
||||
values: []GroupModel{
|
||||
{ID: " a ", Alias: ""},
|
||||
{ID: "b", Alias: " a ", AliasEnabled: true},
|
||||
},
|
||||
wantConflicts: []ModelNameConflict{{ClientModel: "a", Indexes: []int{0, 1}}},
|
||||
wantError: app_errors.ErrModelNameConflict,
|
||||
},
|
||||
{
|
||||
name: "enabled alias cannot be blank after trimming",
|
||||
values: []GroupModel{{ID: "a", Alias: " ", AliasEnabled: true}},
|
||||
wantError: app_errors.ErrValidation,
|
||||
},
|
||||
{
|
||||
name: "multiple conflicts use first occurrence order",
|
||||
values: []GroupModel{
|
||||
{ID: "a"},
|
||||
{ID: "b", Alias: "a", AliasEnabled: true},
|
||||
{ID: "c"},
|
||||
{ID: "d", Alias: "c", AliasEnabled: true},
|
||||
},
|
||||
wantConflicts: []ModelNameConflict{
|
||||
{ClientModel: "a", Indexes: []int{0, 1}},
|
||||
{ClientModel: "c", Indexes: []int{2, 3}},
|
||||
},
|
||||
wantError: app_errors.ErrModelNameConflict,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got, err := normalizeGroupModels(test.values)
|
||||
if test.wantError == nil {
|
||||
if err != nil {
|
||||
t.Fatalf("normalizeGroupModels() error = %v", err)
|
||||
}
|
||||
if !reflect.DeepEqual(got, test.want) {
|
||||
t.Fatalf("normalizeGroupModels() = %#v, want %#v", got, test.want)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var apiErr *app_errors.APIError
|
||||
if !errors.As(err, &apiErr) || apiErr.Code != test.wantError.(*app_errors.APIError).Code {
|
||||
t.Fatalf("normalizeGroupModels() error = %#v, want %q", err, test.wantError)
|
||||
}
|
||||
if test.wantConflicts == nil {
|
||||
return
|
||||
}
|
||||
data, ok := apiErr.Data.(ModelNameConflictData)
|
||||
if !ok || !reflect.DeepEqual(data.Conflicts, test.wantConflicts) {
|
||||
t.Fatalf("conflict data = %#v, want %#v", apiErr.Data, test.wantConflicts)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeGroupModelsRejectsDuplicateExternalNames(t *testing.T) {
|
||||
for _, values := range [][]GroupModel{
|
||||
{{ID: "provider-a", Alias: "public"}, {ID: "provider-b", Alias: "public"}},
|
||||
{{ID: "public"}, {ID: "provider-b", Alias: "public"}},
|
||||
} {
|
||||
if _, err := normalizeGroupModels(values); !errors.Is(err, app_errors.ErrValidation) {
|
||||
t.Fatalf("normalizeGroupModels(%#v) error = %v", values, err)
|
||||
var apiErr *app_errors.APIError
|
||||
if _, err := normalizeGroupModels(values); !errors.As(err, &apiErr) ||
|
||||
apiErr.Code != app_errors.ErrModelNameConflict.Code {
|
||||
t.Fatalf("normalizeGroupModels(%#v) error = %#v", values, err)
|
||||
}
|
||||
}
|
||||
got, err := normalizeGroupModels([]GroupModel{
|
||||
{ID: "provider", Alias: "a"},
|
||||
{ID: "provider", Alias: "b"},
|
||||
{ID: " provider ", Alias: " a "},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := []GroupModel{{ID: "provider", Alias: "a"}, {ID: "provider", Alias: "b"}}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("normalized = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateGroupModelsRequiresNonNullModelsField(t *testing.T) {
|
||||
@@ -104,7 +222,6 @@ func TestUpdateGroupModelsReplacesAuthoritativeListAndPublishesOnce(t *testing.T
|
||||
Values: []GroupModel{
|
||||
{ID: "provider-b", Alias: "public-b"},
|
||||
{ID: "provider-a", Alias: "public-a"},
|
||||
{ID: " provider-a ", Alias: " public-a "},
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -280,8 +397,9 @@ func TestUpdateGroupModelsFailuresDoNotPublish(t *testing.T) {
|
||||
},
|
||||
},
|
||||
})
|
||||
if !errors.Is(err, app_errors.ErrValidation) {
|
||||
t.Fatalf("UpdateGroupModels() error = %v, want ErrValidation", err)
|
||||
var apiErr *app_errors.APIError
|
||||
if !errors.As(err, &apiErr) || apiErr.Code != app_errors.ErrModelNameConflict.Code {
|
||||
t.Fatalf("UpdateGroupModels() error = %#v, want MODEL_NAME_CONFLICT", err)
|
||||
}
|
||||
assertModelsUpdateStateUnchanged(t, fixture, groupID, beforeRevision, beforeRegistry, beforeModels)
|
||||
})
|
||||
|
||||
@@ -21,8 +21,36 @@ import (
|
||||
const maxUpstreamKeyLines = 1000
|
||||
|
||||
type GroupModel struct {
|
||||
ID string `json:"id"`
|
||||
Alias string `json:"alias"`
|
||||
ID string `json:"id"`
|
||||
Alias string `json:"alias"`
|
||||
AliasEnabled bool `json:"-"`
|
||||
|
||||
aliasEnabledSet bool
|
||||
}
|
||||
|
||||
func (model *GroupModel) UnmarshalJSON(data []byte) error {
|
||||
var wire struct {
|
||||
ID string `json:"id"`
|
||||
Alias string `json:"alias"`
|
||||
AliasEnabled *bool `json:"alias_enabled"`
|
||||
}
|
||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||
decoder.DisallowUnknownFields()
|
||||
if err := decoder.Decode(&wire); err != nil {
|
||||
return fmt.Errorf("decode group model: %w", err)
|
||||
}
|
||||
var trailing json.RawMessage
|
||||
if err := decoder.Decode(&trailing); err != io.EOF {
|
||||
if err == nil {
|
||||
return fmt.Errorf("decode group model: trailing JSON value")
|
||||
}
|
||||
return fmt.Errorf("decode group model trailing value: %w", err)
|
||||
}
|
||||
model.ID = wire.ID
|
||||
model.Alias = wire.Alias
|
||||
model.AliasEnabled = wire.AliasEnabled != nil && *wire.AliasEnabled
|
||||
model.aliasEnabledSet = wire.AliasEnabled != nil
|
||||
return nil
|
||||
}
|
||||
|
||||
type upstreamKeyCandidate struct {
|
||||
@@ -152,36 +180,53 @@ func normalizeGroupProtocols(values []protocol.Protocol) ([]protocol.Protocol, e
|
||||
}
|
||||
|
||||
func normalizeGroupModels(values []GroupModel) ([]GroupModel, error) {
|
||||
type pair struct {
|
||||
id string
|
||||
alias string
|
||||
}
|
||||
|
||||
result := make([]GroupModel, 0, len(values))
|
||||
seenPairs := make(map[pair]struct{}, len(values))
|
||||
seenExternal := make(map[string]struct{}, len(values))
|
||||
for _, value := range values {
|
||||
indexesByClientModel := make(map[string][]int, len(values))
|
||||
clientModelOrder := make([]string, 0, len(values))
|
||||
for index, value := range values {
|
||||
normalized := GroupModel{
|
||||
ID: strings.TrimSpace(value.ID),
|
||||
Alias: strings.TrimSpace(value.Alias),
|
||||
ID: strings.TrimSpace(value.ID),
|
||||
}
|
||||
if normalized.ID == "" {
|
||||
return nil, app_errors.ErrValidation
|
||||
}
|
||||
identity := pair{id: normalized.ID, alias: normalized.Alias}
|
||||
if _, duplicate := seenPairs[identity]; duplicate {
|
||||
alias := strings.TrimSpace(value.Alias)
|
||||
aliasEnabled := value.AliasEnabled
|
||||
if !value.aliasEnabledSet && alias != "" {
|
||||
aliasEnabled = true
|
||||
}
|
||||
if aliasEnabled {
|
||||
if alias == "" {
|
||||
return nil, app_errors.ErrValidation
|
||||
}
|
||||
normalized.Alias = alias
|
||||
}
|
||||
clientModel := normalized.ID
|
||||
if normalized.Alias != "" {
|
||||
clientModel = normalized.Alias
|
||||
}
|
||||
if _, exists := indexesByClientModel[clientModel]; !exists {
|
||||
clientModelOrder = append(clientModelOrder, clientModel)
|
||||
}
|
||||
indexesByClientModel[clientModel] = append(indexesByClientModel[clientModel], index)
|
||||
result = append(result, normalized)
|
||||
}
|
||||
conflicts := make([]ModelNameConflict, 0)
|
||||
for _, clientModel := range clientModelOrder {
|
||||
indexes := indexesByClientModel[clientModel]
|
||||
if len(indexes) < 2 {
|
||||
continue
|
||||
}
|
||||
external := normalized.Alias
|
||||
if external == "" {
|
||||
external = normalized.ID
|
||||
}
|
||||
if _, duplicate := seenExternal[external]; duplicate {
|
||||
return nil, app_errors.ErrValidation
|
||||
}
|
||||
seenPairs[identity] = struct{}{}
|
||||
seenExternal[external] = struct{}{}
|
||||
result = append(result, normalized)
|
||||
conflicts = append(conflicts, ModelNameConflict{
|
||||
ClientModel: clientModel,
|
||||
Indexes: append([]int(nil), indexes...),
|
||||
})
|
||||
}
|
||||
if len(conflicts) > 0 {
|
||||
return nil, app_errors.NewAPIErrorWithData(
|
||||
app_errors.ErrModelNameConflict,
|
||||
ModelNameConflictData{Conflicts: conflicts},
|
||||
)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -136,6 +136,12 @@ func (s *Server) HTTPModule() httproute.Module {
|
||||
)),
|
||||
s.handleUpdateGroupSettings,
|
||||
),
|
||||
controlRoute(
|
||||
"control.groups.models.get",
|
||||
http.MethodGet,
|
||||
"/groups/:group_id/models",
|
||||
s.handleGetGroupModels,
|
||||
),
|
||||
controlRoute(
|
||||
"control.groups.models.update",
|
||||
http.MethodPut,
|
||||
|
||||
@@ -181,6 +181,19 @@ func (s *Server) handleUpdateGroupModels(c *gin.Context) {
|
||||
response.SuccessI18n(c, "common.success", result)
|
||||
}
|
||||
|
||||
func (s *Server) handleGetGroupModels(c *gin.Context) {
|
||||
id, ok := groupID(c, "get_group_models")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
result, err := s.service.GetGroupModels(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
writeServiceError(c, "get_group_models", err)
|
||||
return
|
||||
}
|
||||
response.SuccessI18n(c, "common.success", result)
|
||||
}
|
||||
|
||||
func (s *Server) handleDeleteGroup(c *gin.Context) {
|
||||
id, ok := groupID(c, "delete_group")
|
||||
if !ok {
|
||||
@@ -599,7 +612,7 @@ func serviceErrorMessageID(
|
||||
return "key.not_found"
|
||||
}
|
||||
switch operation {
|
||||
case "list_groups", "get_group", "get_group_settings", "update_group",
|
||||
case "list_groups", "get_group", "get_group_settings", "get_group_models", "update_group",
|
||||
"update_group_settings", "delete_group",
|
||||
"update_group_models", "import_group_keys",
|
||||
"discover_group_models", "list_group_keys":
|
||||
@@ -622,6 +635,8 @@ func serviceErrorMessageID(
|
||||
return "group.in_use"
|
||||
case app_errors.ErrUpstreamURLConflict.Code:
|
||||
return "group.upstream_url_conflict"
|
||||
case app_errors.ErrModelNameConflict.Code:
|
||||
return "group.model_name_conflict"
|
||||
case app_errors.ErrBadGateway.Code:
|
||||
return "bad_gateway"
|
||||
default:
|
||||
|
||||
@@ -92,6 +92,7 @@ func TestGroupCollectionHTTPRoutesDeclareStaticOptionsBeforeDynamicDetail(t *tes
|
||||
{name: "control.groups.options", path: "/groups/options"},
|
||||
{name: "control.groups.get", path: "/groups/:group_id"},
|
||||
{name: "control.groups.settings.get", path: "/groups/:group_id/settings"},
|
||||
{name: "control.groups.models.get", path: "/groups/:group_id/models"},
|
||||
{name: "control.group-keys.list", path: "/groups/:group_id/keys"},
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
@@ -1310,12 +1311,6 @@ func TestUpdateGroupModelsEndpointRejectsStrictInvalidBodiesWithoutMutation(t *t
|
||||
{name: "unknown top-level field", body: `{"models":[],"unknown":true}`, code: app_errors.ErrInvalidJSON.Code},
|
||||
{name: "unknown nested field", body: `{"models":[{"id":"provider","unknown":true}]}`, code: app_errors.ErrInvalidJSON.Code},
|
||||
{name: "blank upstream model ID", body: `{"models":[{"id":" "}]}`, code: app_errors.ErrValidation.Code},
|
||||
{
|
||||
name: "duplicate external model",
|
||||
body: `{"models":[{"id":"provider-a","alias":"public"},` +
|
||||
`{"id":"provider-b","alias":"public"}]}`,
|
||||
code: app_errors.ErrValidation.Code,
|
||||
},
|
||||
{name: "multiple JSON values", body: `{"models":[]} {"models":[]}`, code: app_errors.ErrInvalidJSON.Code},
|
||||
{name: "top-level null", body: `null`, code: app_errors.ErrInvalidJSON.Code},
|
||||
} {
|
||||
@@ -1349,6 +1344,43 @@ func TestUpdateGroupModelsEndpointRejectsStrictInvalidBodiesWithoutMutation(t *t
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupModelsHTTPReturnsStructuredConflictWithoutMutation(t *testing.T) {
|
||||
initControlI18n(t)
|
||||
fixture := newServiceFixture(t)
|
||||
groupID := createGroupForKeyImport(t, fixture, "sk-model-conflict-http")
|
||||
engine := gin.New()
|
||||
NewServer(&config.Config{AuthKey: "test-auth-key"}, fixture.service).RegisterRoutes(engine)
|
||||
beforeRevision := fixture.manager.Current().Revision
|
||||
beforeModels := loadCreatedGroupModels(t, fixture, groupID)
|
||||
|
||||
recorder := serveRawGroupModelsUpdateRequest(
|
||||
t,
|
||||
engine,
|
||||
"test-auth-key",
|
||||
"en-US",
|
||||
strconv.FormatUint(uint64(groupID), 10),
|
||||
`{"models":[{"id":"a","alias":"discarded","alias_enabled":false},{"id":"b","alias":"a","alias_enabled":true}]}`,
|
||||
)
|
||||
var envelope struct {
|
||||
Code string `json:"code"`
|
||||
Data ModelNameConflictData `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(recorder.Body.Bytes(), &envelope); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantConflicts := []ModelNameConflict{{ClientModel: "a", Indexes: []int{0, 1}}}
|
||||
if recorder.Code != http.StatusConflict || envelope.Code != app_errors.ErrModelNameConflict.Code ||
|
||||
!reflect.DeepEqual(envelope.Data.Conflicts, wantConflicts) {
|
||||
t.Fatalf("response = %d %#v, want 409 %#v", recorder.Code, envelope, wantConflicts)
|
||||
}
|
||||
if fixture.manager.Current().Revision != beforeRevision {
|
||||
t.Fatal("model conflict published a Snapshot")
|
||||
}
|
||||
if got := loadCreatedGroupModels(t, fixture, groupID); !reflect.DeepEqual(got, beforeModels) {
|
||||
t.Fatalf("model conflict changed persistence: got=%#v want=%#v", got, beforeModels)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateGroupModelsEndpointIDsAuthNotFoundAndSuccessDTO(t *testing.T) {
|
||||
initControlI18n(t)
|
||||
fixture := newServiceFixture(t)
|
||||
|
||||
@@ -35,6 +35,7 @@ var (
|
||||
ErrUnauthorized = &APIError{HTTPStatus: http.StatusUnauthorized, Code: "UNAUTHORIZED", Message: "Authentication failed"}
|
||||
ErrAuthLocked = &APIError{HTTPStatus: http.StatusTooManyRequests, Code: "AUTH_LOCKED", Message: "Authentication is temporarily locked"}
|
||||
ErrUpstreamURLConflict = &APIError{HTTPStatus: http.StatusConflict, Code: "UPSTREAM_URL_CONFLICT", Message: "Upstream URL conflicts with an existing group"}
|
||||
ErrModelNameConflict = &APIError{HTTPStatus: http.StatusConflict, Code: "MODEL_NAME_CONFLICT", Message: "Client model names conflict within the group"}
|
||||
ErrUpstreamURLChangeConfirmationRequired = &APIError{HTTPStatus: http.StatusConflict, Code: "UPSTREAM_URL_CHANGE_CONFIRMATION_REQUIRED", Message: "Upstream URL change requires explicit confirmation"}
|
||||
ErrNoActiveUpstreamKey = &APIError{HTTPStatus: http.StatusConflict, Code: "NO_ACTIVE_UPSTREAM_KEY", Message: "No active upstream key available for this group"}
|
||||
ErrBadGateway = &APIError{HTTPStatus: http.StatusBadGateway, Code: "BAD_GATEWAY", Message: "Upstream service error"}
|
||||
|
||||
@@ -23,6 +23,7 @@ var MessagesEnUS = map[string]string{
|
||||
"group.name_exists": "Group name already exists",
|
||||
"group.in_use": "The group is still referenced by access keys",
|
||||
"group.upstream_url_conflict": "An existing group already uses this upstream URL",
|
||||
"group.model_name_conflict": "Client model names must be unique within the group",
|
||||
"group.upstream_url_change_confirmation_required": "Changing the upstream URL requires explicit confirmation",
|
||||
"group.no_active_upstream_key": "No active upstream key is available for this group",
|
||||
"key.not_found": "Key not found",
|
||||
|
||||
@@ -23,6 +23,7 @@ var MessagesJaJP = map[string]string{
|
||||
"group.name_exists": "グループ名が既に存在します",
|
||||
"group.in_use": "グループはアクセスキーから参照されています",
|
||||
"group.upstream_url_conflict": "このアップストリームURLは既存のグループで使用されています",
|
||||
"group.model_name_conflict": "グループ内のクライアントモデル名は一意である必要があります",
|
||||
"group.upstream_url_change_confirmation_required": "アップストリームURLの変更には明示的な確認が必要です",
|
||||
"group.no_active_upstream_key": "このグループには利用可能な有効なアップストリームキーがありません",
|
||||
"key.not_found": "キーが存在しません",
|
||||
|
||||
@@ -23,6 +23,7 @@ var MessagesZhCN = map[string]string{
|
||||
"group.name_exists": "分组名称已存在",
|
||||
"group.in_use": "分组仍被访问密钥引用",
|
||||
"group.upstream_url_conflict": "已有分组使用该上游地址",
|
||||
"group.model_name_conflict": "分组内客户端模型名称必须唯一",
|
||||
"group.upstream_url_change_confirmation_required": "修改上游地址需要明确确认",
|
||||
"group.no_active_upstream_key": "该分组没有可用的活跃上游密钥",
|
||||
"key.not_found": "密钥不存在",
|
||||
|
||||
Reference in new issue
Block a user