Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/cmd/asterrouter/VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.14.0
0.15.0
2 changes: 1 addition & 1 deletion backend/internal/controlplane/alert_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ FROM alert_events`
return nil, err
}
defer rows.Close()
var out []AlertEvent
out := make([]AlertEvent, 0)
for rows.Next() {
event, err := scanAlertEvent(rows)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion backend/internal/controlplane/department_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ ORDER BY status ASC, code ASC
return nil, err
}
defer rows.Close()
var out []Department
out := make([]Department, 0)
for rows.Next() {
var department Department
if err := rows.Scan(&department.ID, &department.Name, &department.Code, &department.ParentID, &department.CostCenter, &department.MonthlyBudgetCents, &department.Status, &department.CreatedAt, &department.UpdatedAt); err != nil {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ ORDER BY status ASC, name ASC
return nil, err
}
defer rows.Close()
var out []GovernancePolicy
out := make([]GovernancePolicy, 0)
for rows.Next() {
var policy GovernancePolicy
var allowlist, denylist string
Expand Down
8 changes: 4 additions & 4 deletions backend/internal/controlplane/identity_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func (r *MemoryRepository) SaveWorkspaceUser(_ context.Context, user WorkspaceUs
func (r *MemoryRepository) ListAuthIdentities(_ context.Context, userID string) ([]AuthIdentity, error) {
r.mu.RLock()
defer r.mu.RUnlock()
var out []AuthIdentity
out := make([]AuthIdentity, 0)
for _, identity := range r.authIdentities {
if identity.UserID == userID {
out = append(out, identity)
Expand Down Expand Up @@ -121,7 +121,7 @@ ORDER BY u.status ASC, u.email ASC
return nil, err
}
defer rows.Close()
var out []WorkspaceUser
out := make([]WorkspaceUser, 0)
for rows.Next() {
var user WorkspaceUser
var recovery string
Expand Down Expand Up @@ -171,7 +171,7 @@ func (r *PostgresRepository) ListAuthIdentities(ctx context.Context, userID stri
return nil, err
}
defer rows.Close()
var out []AuthIdentity
out := make([]AuthIdentity, 0)
for rows.Next() {
var identity AuthIdentity
if err := rows.Scan(&identity.ID, &identity.UserID, &identity.Issuer, &identity.Subject, &identity.Email, &identity.CreatedAt, &identity.UpdatedAt); err != nil {
Expand Down Expand Up @@ -223,7 +223,7 @@ ORDER BY user_id ASC, scope_type ASC, scope_id ASC
return nil, err
}
defer rows.Close()
var out []RoleBinding
out := make([]RoleBinding, 0)
for rows.Next() {
var binding RoleBinding
if err := rows.Scan(&binding.ID, &binding.UserID, &binding.Role, &binding.ScopeType, &binding.ScopeID, &binding.CreatedAt, &binding.UpdatedAt); err != nil {
Expand Down
51 changes: 51 additions & 0 deletions backend/internal/controlplane/postgres_repository_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,57 @@ import (
"github.com/astercloud/asterrouter/backend/internal/testutil"
)

func assertEmptyList[T any](t *testing.T, name string, load func() ([]T, error)) {
t.Helper()
items, err := load()
if err != nil {
t.Fatalf("%s: %v", name, err)
}
if items == nil {
t.Fatalf("%s returned a nil slice", name)
}
if len(items) != 0 {
t.Fatalf("%s returned %d items, want 0", name, len(items))
}
}

func TestPostgresRepositoryEmptyListContracts(t *testing.T) {
schema := testutil.NewPostgresSchema(t)
ctx := context.Background()
repo, err := NewPostgresRepository(ctx, schema.URL)
if err != nil {
t.Fatalf("NewPostgresRepository(): %v", err)
}
defer repo.Close()

assertEmptyList(t, "ListDepartments", func() ([]Department, error) { return repo.ListDepartments(ctx) })
assertEmptyList(t, "ListOrganizationGroups", func() ([]OrganizationGroup, error) { return repo.ListOrganizationGroups(ctx) })
assertEmptyList(t, "ListGovernancePolicies", func() ([]GovernancePolicy, error) { return repo.ListGovernancePolicies(ctx) })
assertEmptyList(t, "ListWorkspaceUsers", func() ([]WorkspaceUser, error) { return repo.ListWorkspaceUsers(ctx) })
assertEmptyList(t, "ListRoleBindings", func() ([]RoleBinding, error) { return repo.ListRoleBindings(ctx) })
assertEmptyList(t, "ListProviders", func() ([]ProviderConnection, error) { return repo.ListProviders(ctx) })
assertEmptyList(t, "ListLatestProviderHealthChecks", func() ([]ProviderHealthCheck, error) { return repo.ListLatestProviderHealthChecks(ctx) })
assertEmptyList(t, "ListRoutingGroups", func() ([]RoutingGroup, error) { return repo.ListRoutingGroups(ctx) })
assertEmptyList(t, "ListProviderAccounts", func() ([]ProviderAccount, error) { return repo.ListProviderAccounts(ctx) })
assertEmptyList(t, "ListLatestProviderAccountHealthChecks", func() ([]ProviderAccountHealthCheck, error) { return repo.ListLatestProviderAccountHealthChecks(ctx) })
assertEmptyList(t, "ListGatewayModels", func() ([]GatewayModel, error) { return repo.ListGatewayModels(ctx) })
assertEmptyList(t, "ListModelRoutes", func() ([]ModelRoute, error) { return repo.ListModelRoutes(ctx) })
assertEmptyList(t, "ListModelPricings", func() ([]ModelPricing, error) { return repo.ListModelPricings(ctx) })
assertEmptyList(t, "ListAPIKeys", func() ([]APIKeyRecord, error) { return repo.ListAPIKeys(ctx) })
assertEmptyList(t, "ListUsageRecords", func() ([]UsageRecord, error) { return repo.ListUsageRecords(ctx, 100) })
assertEmptyList(t, "ListGatewayTraces", func() ([]GatewayTrace, error) { return repo.ListGatewayTraces(ctx, 100) })
assertEmptyList(t, "ListAuditLogs", func() ([]AuditLog, error) { return repo.ListAuditLogs(ctx, 100) })
}

func TestListParsersNormalizeJSONNull(t *testing.T) {
if values := parseStringList("null"); values == nil || len(values) != 0 {
t.Fatalf("parseStringList(null) = %#v, want []", values)
}
if rules := parseTempUnschedulableRules("null"); rules == nil || len(rules) != 0 {
t.Fatalf("parseTempUnschedulableRules(null) = %#v, want []", rules)
}
}

func TestPostgresRepositoryPersistsCoreRecordsAcrossRestart(t *testing.T) {
schema := testutil.NewPostgresSchema(t)
ctx := context.Background()
Expand Down
28 changes: 17 additions & 11 deletions backend/internal/controlplane/repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -2839,7 +2839,7 @@ ORDER BY priority ASC, name ASC
return nil, err
}
defer rows.Close()
var out []ProviderConnection
out := make([]ProviderConnection, 0)
for rows.Next() {
var provider ProviderConnection
var models string
Expand Down Expand Up @@ -2882,7 +2882,7 @@ ORDER BY provider_id, checked_at DESC
return nil, err
}
defer rows.Close()
var out []ProviderHealthCheck
out := make([]ProviderHealthCheck, 0)
for rows.Next() {
var check ProviderHealthCheck
var models string
Expand Down Expand Up @@ -2922,7 +2922,7 @@ ORDER BY sort_order ASC, name ASC
}
defer rows.Close()

var out []RoutingGroup
out := make([]RoutingGroup, 0)
for rows.Next() {
var group RoutingGroup
if err := rows.Scan(
Expand Down Expand Up @@ -3066,7 +3066,7 @@ ORDER BY priority ASC, name ASC
}
defer rows.Close()

var out []ProviderAccount
out := make([]ProviderAccount, 0)
for rows.Next() {
var account ProviderAccount
var models, groupIDs, tempUnschedulableRules string
Expand Down Expand Up @@ -3169,7 +3169,7 @@ func (r *PostgresRepository) ListProviderAccountModels(ctx context.Context, acco
return nil, err
}
defer rows.Close()
var out []ProviderAccountModel
out := make([]ProviderAccountModel, 0)
for rows.Next() {
var model ProviderAccountModel
var lastSeenAt sql.NullTime
Expand Down Expand Up @@ -3217,7 +3217,7 @@ ORDER BY account_id, checked_at DESC
return nil, err
}
defer rows.Close()
var out []ProviderAccountHealthCheck
out := make([]ProviderAccountHealthCheck, 0)
for rows.Next() {
var check ProviderAccountHealthCheck
var models string
Expand Down Expand Up @@ -3250,7 +3250,7 @@ ORDER BY model ASC
return nil, err
}
defer rows.Close()
var out []ModelPricing
out := make([]ModelPricing, 0)
for rows.Next() {
var pricing ModelPricing
if err := rows.Scan(&pricing.ID, &pricing.Model, &pricing.Currency, &pricing.InputPriceCentsPer1MTokens, &pricing.OutputPriceCentsPer1MTokens, &pricing.Status, &pricing.CreatedAt, &pricing.UpdatedAt); err != nil {
Expand Down Expand Up @@ -3359,7 +3359,7 @@ ORDER BY created_at DESC`)
return nil, err
}
defer rows.Close()
var out []APIKeyRecord
out := make([]APIKeyRecord, 0)
for rows.Next() {
key, err := scanAPIKey(rows)
if err != nil {
Expand Down Expand Up @@ -3566,7 +3566,7 @@ FROM usage_records`
return nil, err
}
defer rows.Close()
var out []UsageRecord
out := make([]UsageRecord, 0)
for rows.Next() {
var record UsageRecord
var usageDimensionsJSON []byte
Expand Down Expand Up @@ -3717,7 +3717,7 @@ FROM gateway_traces`
return nil, err
}
defer rows.Close()
var out []GatewayTrace
out := make([]GatewayTrace, 0)
for rows.Next() {
var trace GatewayTrace
if err := rows.Scan(&trace.ID, &trace.OperationID, &trace.AttemptID, &trace.RequestFingerprint, &trace.APIKeyID, &trace.APIFingerprint, &trace.ProfileScope, &trace.PlatformTenantID, &trace.PlatformTenantName, &trace.GatewayPrincipalID, &trace.GatewayPrincipalName, &trace.ExternalAuthIntegrationID, &trace.ExternalSubjectReference, &trace.Model, &trace.Stream, &trace.MessageCount, &trace.ProviderID, &trace.ProviderAccountID, &trace.GatewayModelID, &trace.RouteID, &trace.RouteGroup, &trace.UpstreamModel, &trace.RouteSource, &trace.RouteReason, &trace.PolicyID, &trace.PolicyName, &trace.PolicySource, &trace.PolicyVersion, &trace.PolicySnapshot, &trace.Status, &trace.HTTPStatus, &trace.ErrorType, &trace.LatencyMS, &trace.InputTokens, &trace.OutputTokens, &trace.RequestSummary, &trace.ResponseSummary, &trace.RouteAttempts, &trace.CreatedAt); err != nil {
Expand Down Expand Up @@ -3779,7 +3779,7 @@ FROM audit_logs`
return nil, err
}
defer rows.Close()
var out []AuditLog
out := make([]AuditLog, 0)
for rows.Next() {
var event AuditLog
if err := rows.Scan(&event.ID, &event.Actor, &event.Action, &event.ResourceType, &event.ResourceID, &event.Summary, &event.ProfileScope, &event.PlatformTenantID, &event.PlatformTenantName, &event.GatewayPrincipalID, &event.GatewayPrincipalName, &event.ExternalAuthIntegrationID, &event.ExternalSubjectReference, &event.CreatedAt); err != nil {
Expand Down Expand Up @@ -3846,6 +3846,9 @@ func parseStringList(value string) []string {
if err := json.Unmarshal([]byte(value), &out); err != nil {
return []string{}
}
if out == nil {
return []string{}
}
return out
}

Expand All @@ -3862,5 +3865,8 @@ func parseTempUnschedulableRules(value string) []ProviderAccountTempUnschedulabl
if err := json.Unmarshal([]byte(value), &out); err != nil {
return []ProviderAccountTempUnschedulableRule{}
}
if out == nil {
return []ProviderAccountTempUnschedulableRule{}
}
return out
}
2 changes: 1 addition & 1 deletion backend/internal/controlplane/risk_block_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ func (r *PostgresRepository) ListActiveGatewayRiskBlocks(ctx context.Context, no
return nil, err
}
defer rows.Close()
var out []GatewayRiskBlock
out := make([]GatewayRiskBlock, 0)
for rows.Next() {
var block GatewayRiskBlock
if err := rows.Scan(&block.APIKeyID, &block.RuleID, &block.Reason, &block.ExpiresAt, &block.CreatedAt); err != nil {
Expand Down
3 changes: 3 additions & 0 deletions backend/internal/plugins/artifact_sink_s3.go
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,9 @@ func artifactSinkDestinationRecords(config configRecord) ([]artifactSinkDestinat
if err := json.Unmarshal([]byte(raw), &destinations); err != nil {
return nil, fmt.Errorf("%w: artifact sink destinations are invalid", ErrPluginConfigInvalid)
}
if destinations == nil {
destinations = []artifactSinkDestinationRecord{}
}
seen := map[string]struct{}{}
for _, destination := range destinations {
if err := validateArtifactSinkDestination(destination); err != nil {
Expand Down
2 changes: 1 addition & 1 deletion backend/internal/plugins/catalog_mapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ func mapRemoteCatalogPlugins(index remoteCatalogIndex, now time.Time) []Plugin {
}

func mapRemoteCatalogPackages(index remoteCatalogIndex, now time.Time) []packageRecord {
var out []packageRecord
out := make([]packageRecord, 0)
for _, item := range index.Plugins {
slug := sanitizeCatalogSlug(item.Slug)
if slug == "" {
Expand Down
2 changes: 1 addition & 1 deletion backend/internal/plugins/license_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -430,7 +430,7 @@ func licenseAllowsResource(record licenseRecord, entitlementType string, resourc
}

func entitlementsFromRecord(record licenseRecord) []Entitlement {
var out []Entitlement
out := make([]Entitlement, 0)
if err := json.Unmarshal([]byte(defaultString(record.EntitlementsJSON, "[]")), &out); err != nil {
return []Entitlement{}
}
Expand Down
2 changes: 1 addition & 1 deletion backend/internal/plugins/package_compatibility.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func (s *Service) packageRevocation(ctx context.Context, record packageRecord) (
func (s *Service) revokedAffectedVersions(ctx context.Context, record packageRecord) ([]affectedVersionRecord, error) {
keys := []string{record.PluginSlug, record.PluginPublicID, record.PluginID}
seen := map[string]struct{}{}
var out []affectedVersionRecord
out := make([]affectedVersionRecord, 0)
for _, key := range keys {
key = strings.TrimSpace(key)
if key == "" {
Expand Down
8 changes: 4 additions & 4 deletions backend/internal/plugins/postgres_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ ORDER BY category ASC, tier ASC, name ASC
return nil, err
}
defer rows.Close()
var out []Plugin
out := make([]Plugin, 0)
for rows.Next() {
plugin, err := scanPlugin(rows)
if err != nil {
Expand Down Expand Up @@ -398,7 +398,7 @@ FROM notification_deliveries`
return nil, err
}
defer rows.Close()
var out []DeliveryAttempt
out := make([]DeliveryAttempt, 0)
for rows.Next() {
attempt, err := scanDeliveryAttempt(rows)
if err != nil {
Expand Down Expand Up @@ -486,7 +486,7 @@ ORDER BY version DESC, os ASC, arch ASC
return nil, err
}
defer rows.Close()
var out []packageRecord
out := make([]packageRecord, 0)
for rows.Next() {
record, err := scanPackageRecord(rows)
if err != nil {
Expand Down Expand Up @@ -576,7 +576,7 @@ ORDER BY created_at ASC
return nil, err
}
defer rows.Close()
var out []affectedVersionRecord
out := make([]affectedVersionRecord, 0)
for rows.Next() {
record, err := scanAffectedVersionRecord(rows)
if err != nil {
Expand Down
3 changes: 3 additions & 0 deletions backend/internal/plugins/repository_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,9 @@ func parseStringList(value string) []string {
if err := json.Unmarshal([]byte(value), &out); err != nil {
return []string{}
}
if out == nil {
return []string{}
}
return out
}

Expand Down
2 changes: 1 addition & 1 deletion backend/internal/system/s3_backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ func (s *S3BackupStore) List(ctx context.Context) ([]S3BackupObject, error) {
if prefix != "" {
prefix += "/"
}
var out []S3BackupObject
out := make([]S3BackupObject, 0)
paginator := s3.NewListObjectsV2Paginator(s.client, &s3.ListObjectsV2Input{Bucket: aws.String(s.config.Bucket), Prefix: aws.String(prefix)})
for paginator.HasMorePages() {
page, err := paginator.NextPage(ctx)
Expand Down
6 changes: 3 additions & 3 deletions docs/product-positioning.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ AsterRouter 是面向企业团队、AI API 平台和客户产品的 AI Access Ga
- **更省钱:** 在策略、健康和容量约束内比较合格线路,逐步实现价格插件、成本感知调度和可验证节省。
- **两种对外接入:** 可以直接签发 AsterRouter Gateway API Key,也可以保留 SaaS/OEM 的用户、Session 和权益体系,只委托 AI 访问上下文。
- **企业治理:** 按 Tenant、用户、部门、Group、Key Principal、模型和外部 Subject 管理权限、预算、用量与审计。
- **灵活交付:** 支持自主部署、私有化交付和托管运维,从单机低成本运行平滑扩展到 Kubernetes。
- **灵活交付:** 支持自主部署、私有化交付和托管运维;当前以低成本单机运行,Kubernetes 弹性部署属于后续路线图
- **统一 Core:** 协议、Credential Source 和 Provider 可以扩展,但 Policy、候选规划、账号调度、Usage、Billing 和 Audit 只有一套事实源。

## 商业场景
Expand Down Expand Up @@ -88,7 +88,7 @@ AsterRouter 不实现以下能力:
- 飞书(中国大陆)/ Lark(国际版)、钉钉企业登录与自动入职,OIDC 作为通用企业 SSO 兼容入口。
- GitHub、Google 作为已验证邮箱的辅助登录源;本地安全登录保留为企业救援入口。
- 企业账户支持多身份记录、安全解绑和持久化会话撤销;身份变更、改密与人员禁用必须即时收回旧令牌权限。
- Operator 仅用于企业内部使用方、额度策略、成本规则和额度流水治理;不接受月费、充值、退款或订阅售卖业务
- Relay Operator 管理 Customer、Plan、余额分配、兑换、账单视图和风险运营;真实支付渠道、订单、退款、税务与订阅合同仍由外部业务系统或独立插件负责,不能进入 Gateway Core。Enterprise 不复用这些中转商业对象
- 用户、部门、Group 和资源级 RBAC。
- Workspace Key 生命周期、模型白名单、限流、预算和过期策略。
- User/Customer/Service Key 基础类型、Bearer 鉴权、Hash/Fingerprint、禁用、立即轮换和用量归属。
Expand All @@ -98,7 +98,7 @@ AsterRouter 不实现以下能力:

员工 Portal 只展示本人可见的 Key、模型、额度和用量,不暴露上游账号、渠道或运营信息。

后续路线图会在同一 Core 上增加 API Key Principal 的细粒度 Scope 与分布式并发、平台委托鉴权、Responses/Anthropic/Gemini、多模态 Job/Artifact、价格插件、成本感知调度、PostgreSQL + Redis 多模态基础设施,以及可选 Kubernetes 弹性部署。未交付能力不能作为当前产品承诺。
后续路线图会在同一 Core 上增加 API Key Principal 的细粒度 Scope 与分布式并发、OIDC Introspection/撤销事件与其他平台鉴权适配、Responses/Anthropic/Gemini、多模态 Job/Artifact、价格插件、成本感知调度、PostgreSQL + Redis 多模态基础设施,以及可选 Kubernetes 弹性部署。HMAC 与 RS256 JWT/JWKS 平台委托鉴权已经交付;未交付能力不能作为当前产品承诺。

## 产品原则

Expand Down
Loading
Loading