From 008f5975b987bda12df10c17aa16d0e0e97e16e6 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Wed, 6 Nov 2024 10:04:18 -0500 Subject: [PATCH 1/7] Added Vector Store File List properties that allow for pagination --- vector_store.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/vector_store.go b/vector_store.go index 5c364362a..682bb1cf9 100644 --- a/vector_store.go +++ b/vector_store.go @@ -83,6 +83,9 @@ type VectorStoreFileRequest struct { type VectorStoreFilesList struct { VectorStoreFiles []VectorStoreFile `json:"data"` + FirstID *string `json:"first_id"` + LastID *string `json:"last_id"` + HasMore bool `json:"has_more"` httpHeader } From 9e37b2704fd21a026c503d812532b270ad8f99dd Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Tue, 11 Mar 2025 13:40:04 -0400 Subject: [PATCH 2/7] Added example of admin api usage --- examples/admin_usage/README.md | 7 ++++ examples/admin_usage/main.go | 62 ++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 examples/admin_usage/README.md create mode 100644 examples/admin_usage/main.go diff --git a/examples/admin_usage/README.md b/examples/admin_usage/README.md new file mode 100644 index 000000000..40d4b3c46 --- /dev/null +++ b/examples/admin_usage/README.md @@ -0,0 +1,7 @@ +### OpenAI Administration Endpoints + +The examples in this directory require the use of an Admin API Key, which differ from a normal API key. You must create at least one Admin API Key using the OpenAI Console before using these functions. + +You can generate the link in the [Admin Keys Console](https://platform.openai.com/organization/admin-keys). + +For additional information, refer to the Administrator section of the [API Reference](https://platform.openai.com/docs/api-reference/administration). \ No newline at end of file diff --git a/examples/admin_usage/main.go b/examples/admin_usage/main.go new file mode 100644 index 000000000..b7720f20a --- /dev/null +++ b/examples/admin_usage/main.go @@ -0,0 +1,62 @@ +package main + +import ( + "context" + "fmt" + "os" + "time" + + "github.com/sashabaranov/go-openai" +) + +func main() { + ctx := context.Background() + + // Admin API Keys are different than regular API keys and require a different + // endpoint to be used. You can find your Admin API key in the OpenAI dashboard. + config := openai.DefaultConfig(os.Getenv("OPENAI_ADMIN_API_KEY")) + client := openai.NewClientWithConfig(config) + + // Specify the date range of the usage data you want to retrieve, the end date is optional, + // but when specified, it should include through the end of the day you want to retrieve. + startTime := convertDateStringToTimestamp("2025-02-01") + endTime := convertDateStringToTimestamp("2025-03-01") + + // In this example each bucket represents a day of usage data. To avoid + // making several requests to get the data for each day, we'll increase + // the limit to 31 to get all the data in one request. + limit := 31 + + // Create the request object, only StartTime is required. + req := openai.AdminUsageCostRequest{ + StartTime: startTime, + EndTime: &endTime, + Limit: &limit, + } + + // Request the usage data. + res, err := client.GetAdminUsageCost(ctx, req) + if err != nil { + fmt.Printf("error getting openai usage data: %v\n", err) + return + } + + // Calculate the total cost of the usage data. + totalCost := 0.0 + for _, bucket := range res.Data { + for _, cost := range bucket.Results { + totalCost += cost.Amount.Value + } + } + + fmt.Printf("Total Cost: %f\n", totalCost) +} + +// Helper function to convert a date string to a Unix timestamp. +func convertDateStringToTimestamp(date string) int64 { + t, err := time.Parse("2006-01-02", date) + if err != nil { + panic(err) + } + return t.Unix() +} From 6c564fe2c4d9c72a323b8c8e090e4aa38cfa0605 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Tue, 11 Mar 2025 13:40:32 -0400 Subject: [PATCH 3/7] Admin Functionality for Invites, Usage, User and Partial Project Management --- admin_invite.go | 151 +++++++++++++++++++++++++++++++++++ admin_invite_test.go | 147 ++++++++++++++++++++++++++++++++++ admin_key.go | 138 ++++++++++++++++++++++++++++++++ admin_key_test.go | 156 ++++++++++++++++++++++++++++++++++++ admin_project.go | 141 +++++++++++++++++++++++++++++++++ admin_project_test.go | 157 +++++++++++++++++++++++++++++++++++++ admin_project_user.go | 152 +++++++++++++++++++++++++++++++++++ admin_project_user_test.go | 154 ++++++++++++++++++++++++++++++++++++ admin_usage.go | 99 +++++++++++++++++++++++ admin_usage_test.go | 109 +++++++++++++++++++++++++ admin_user.go | 129 ++++++++++++++++++++++++++++++ admin_user_test.go | 128 ++++++++++++++++++++++++++++++ 12 files changed, 1661 insertions(+) create mode 100644 admin_invite.go create mode 100644 admin_invite_test.go create mode 100644 admin_key.go create mode 100644 admin_key_test.go create mode 100644 admin_project.go create mode 100644 admin_project_test.go create mode 100644 admin_project_user.go create mode 100644 admin_project_user_test.go create mode 100644 admin_usage.go create mode 100644 admin_usage_test.go create mode 100644 admin_user.go create mode 100644 admin_user_test.go diff --git a/admin_invite.go b/admin_invite.go new file mode 100644 index 000000000..ba2a078ec --- /dev/null +++ b/admin_invite.go @@ -0,0 +1,151 @@ +package openai + +import ( + "context" + "fmt" + "net/http" + "net/url" +) + +const ( + adminInvitesSuffix = "/organization/invites" +) + +var ( + // adminInviteRoles is a list of valid roles for an Admin Invite. + adminInviteRoles = []string{"owner", "member"} +) + +// AdminInvite represents an Admin Invite. +type AdminInvite struct { + Object string `json:"object"` + ID string `json:"id"` + Email string `json:"email"` + Role string `json:"role"` + Status string `json:"status"` + InvitedAt int64 `json:"invited_at"` + ExpiresAt int64 `json:"expires_at"` + AcceptedAt int64 `json:"accepted_at"` + Projects []AdminInviteProject `json:"projects"` + + httpHeader +} + +// AdminInviteProject represents a project associated with an Admin Invite. +type AdminInviteProject struct { + ID string `json:"id"` + Role string `json:"role"` +} + +// AdminInviteList represents a list of Admin Invites. +type AdminInviteList struct { + Object string `json:"object"` + AdminInvites []AdminInvite `json:"data"` + FirstID string `json:"first_id"` + LastID string `json:"last_id"` + HasMore bool `json:"has_more"` + + httpHeader +} + +// AdminInviteDeleteResponse represents the response from deleting an Admin Invite. +type AdminInviteDeleteResponse struct { + ID string `json:"id"` + Object string `json:"object"` + Deleted bool `json:"deleted"` + + httpHeader +} + +// ListAdminInvites lists Admin Invites associated with the organization. +func (c *Client) ListAdminInvites( + ctx context.Context, + limit *int, + after *string, +) (response AdminInviteList, err error) { + urlValues := url.Values{} + if limit != nil { + urlValues.Add("limit", fmt.Sprintf("%d", *limit)) + } + if after != nil { + urlValues.Add("after", *after) + } + + encodedValues := "" + if len(urlValues) > 0 { + encodedValues = "?" + urlValues.Encode() + } + + urlSuffix := adminInvitesSuffix + encodedValues + req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// CreateAdminInvite creates a new Admin Invite. +func (c *Client) CreateAdminInvite( + ctx context.Context, + email string, + role string, + projects *[]AdminInviteProject, +) (response AdminInvite, err error) { + // Validate the role. + if !containsSubstr(adminInviteRoles, role) { + return response, fmt.Errorf("invalid admin role: %s", role) + } + + // Create the request object. + request := struct { + Email string `json:"email"` + Role string `json:"role"` + Projects *[]AdminInviteProject `json:"projects,omitempty"` + }{ + Email: email, + Role: role, + Projects: projects, + } + + urlSuffix := adminInvitesSuffix + req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix), withBody(request)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + + return +} + +// RetrieveAdminInvite retrieves an Admin Invite. +func (c *Client) RetrieveAdminInvite( + ctx context.Context, + inviteID string, +) (response AdminInvite, err error) { + urlSuffix := fmt.Sprintf("%s/%s", adminInvitesSuffix, inviteID) + req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// DeleteAdminInvite deletes an Admin Invite. +func (c *Client) DeleteAdminInvite( + ctx context.Context, + inviteID string, +) (response AdminInviteDeleteResponse, err error) { + urlSuffix := fmt.Sprintf("%s/%s", adminInvitesSuffix, inviteID) + req, err := c.newRequest(ctx, http.MethodDelete, c.fullURL(urlSuffix)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} diff --git a/admin_invite_test.go b/admin_invite_test.go new file mode 100644 index 000000000..0bfb44dbc --- /dev/null +++ b/admin_invite_test.go @@ -0,0 +1,147 @@ +package openai_test + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/sashabaranov/go-openai" + "github.com/sashabaranov/go-openai/internal/test/checks" +) + +func TestAdminInvite(t *testing.T) { + adminInviteObject := "organization.invite" + adminInviteID := "invite-abc-123" + adminInviteEmail := "invite@openai.com" + adminInviteRole := "owner" + adminInviteStatus := "pending" + + adminInviteInvitedAt := int64(1711471533) + adminInviteExpiresAt := int64(1711471533) + adminInviteAcceptedAt := int64(1711471533) + adminInviteProjects := []openai.AdminInviteProject{ + { + ID: "project-id", + Role: "owner", + }, + } + + client, server, teardown := setupOpenAITestServer() + defer teardown() + + server.RegisterHandler( + "/v1/organization/invites", + func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + resBytes, _ := json.Marshal(openai.AdminInviteList{ + Object: "list", + AdminInvites: []openai.AdminInvite{ + { + Object: adminInviteObject, + ID: adminInviteID, + Email: adminInviteEmail, + Role: adminInviteRole, + Status: adminInviteStatus, + InvitedAt: adminInviteInvitedAt, + ExpiresAt: adminInviteExpiresAt, + AcceptedAt: adminInviteAcceptedAt, + Projects: adminInviteProjects, + }, + }, + FirstID: "first_id", + LastID: "last_id", + HasMore: false, + }) + fmt.Fprintln(w, string(resBytes)) + + case http.MethodPost: + resBytes, _ := json.Marshal(openai.AdminInvite{ + Object: adminInviteObject, + ID: adminInviteID, + Email: adminInviteEmail, + Role: adminInviteRole, + Status: adminInviteStatus, + InvitedAt: adminInviteInvitedAt, + ExpiresAt: adminInviteExpiresAt, + AcceptedAt: adminInviteAcceptedAt, + Projects: adminInviteProjects, + }) + fmt.Fprintln(w, string(resBytes)) + } + }, + ) + + server.RegisterHandler( + "/v1/organization/invites/"+adminInviteID, + func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodDelete: + resBytes, _ := json.Marshal(openai.AdminInviteDeleteResponse{ + ID: adminInviteID, + Object: adminInviteObject, + Deleted: true, + }) + fmt.Fprintln(w, string(resBytes)) + + case http.MethodGet: + resBytes, _ := json.Marshal(openai.AdminInvite{ + Object: adminInviteObject, + ID: adminInviteID, + Email: adminInviteEmail, + Role: adminInviteRole, + Status: adminInviteStatus, + InvitedAt: adminInviteInvitedAt, + ExpiresAt: adminInviteExpiresAt, + AcceptedAt: adminInviteAcceptedAt, + Projects: adminInviteProjects, + }) + fmt.Fprintln(w, string(resBytes)) + } + }, + ) + + ctx := context.Background() + + t.Run("ListAdminInvites", func(t *testing.T) { + adminInvites, err := client.ListAdminInvites(ctx, nil, nil) + checks.NoError(t, err, "ListAdminInvites error") + + if len(adminInvites.AdminInvites) != 1 { + t.Fatalf("expected 1 admin invite, got %d", len(adminInvites.AdminInvites)) + } + + if adminInvites.AdminInvites[0].ID != adminInviteID { + t.Errorf("expected admin invite ID %s, got %s", adminInviteID, adminInvites.AdminInvites[0].ID) + } + }) + + t.Run("CreateAdminInvite", func(t *testing.T) { + adminInvite, err := client.CreateAdminInvite(ctx, adminInviteEmail, adminInviteRole, &adminInviteProjects) + checks.NoError(t, err, "CreateAdminInvite error") + + if adminInvite.ID != adminInviteID { + t.Errorf("expected admin invite ID %s, got %s", adminInviteID, adminInvite.ID) + } + }) + + t.Run("RetrieveAdminInvite", func(t *testing.T) { + adminInvite, err := client.RetrieveAdminInvite(ctx, adminInviteID) + checks.NoError(t, err, "RetrieveAdminInvite error") + + if adminInvite.ID != adminInviteID { + t.Errorf("expected admin invite ID %s, got %s", adminInviteID, adminInvite.ID) + } + }) + + t.Run("DeleteAdminInvite", func(t *testing.T) { + adminInviteDeleteResponse, err := client.DeleteAdminInvite(ctx, adminInviteID) + checks.NoError(t, err, "DeleteAdminInvite error") + + if !adminInviteDeleteResponse.Deleted { + t.Errorf("expected admin invite to be deleted, got not deleted") + } + }) +} diff --git a/admin_key.go b/admin_key.go new file mode 100644 index 000000000..e8ee14edc --- /dev/null +++ b/admin_key.go @@ -0,0 +1,138 @@ +package openai + +import ( + "context" + "fmt" + "net/http" + "net/url" +) + +const ( + adminKeysSuffix = "/organization/admin_api_keys" +) + +// AdminKey represents an Admin API Key. +type AdminKey struct { + Object string `json:"object"` + ID string `json:"id"` + Name string `json:"name"` + RedactedValue string `json:"redacted_value"` + CreatedAt int64 `json:"created_at"` + Owner AdminKeyOwner `json:"owner"` + Value *string `json:"value"` + + httpHeader +} + +// AdminKeyOwner represents the owner of an Admin API Key. +type AdminKeyOwner struct { + Type string `json:"type"` + Object string `json:"object"` + ID string `json:"id"` + Name string `json:"name"` + CreatedAt int64 `json:"created_at"` + Role string `json:"role"` +} + +// AdminKeyList represents a list of Admin API Keys. +type AdminKeyList struct { + Object string `json:"object"` + AdminKeys []AdminKey `json:"data"` + FirstID string `json:"first_id"` + LastID string `json:"last_id"` + HasMore bool `json:"has_more"` + httpHeader +} + +// AdminKeyDeleteResponse represents the response from deleting an Admin API Key. +type AdminKeyDeleteResponse struct { + ID string `json:"id"` + Object string `json:"object"` + Deleted bool `json:"deleted"` + + httpHeader +} + +// ListAdminKeys lists Admin API Keys associated with the organization. +func (c *Client) ListAdminKeys( + ctx context.Context, + limit *int, + order *string, + after *string, +) (response AdminKeyList, err error) { + urlValues := url.Values{} + if limit != nil { + urlValues.Add("limit", fmt.Sprintf("%d", *limit)) + } + if order != nil { + urlValues.Add("order", *order) + } + if after != nil { + urlValues.Add("after", *after) + } + + encodedValues := "" + if len(urlValues) > 0 { + encodedValues = "?" + urlValues.Encode() + } + + urlSuffix := adminKeysSuffix + encodedValues + req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// CreateAdminKey creates a new Admin API Key. +func (c *Client) CreateAdminKey( + ctx context.Context, + keyName string, +) (response AdminKey, err error) { + type KeyName struct { + Name string `json:"name"` + } + req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(adminKeysSuffix), + withBody(KeyName{Name: keyName})) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + + return +} + +// RetrieveAdminKey retrieves an Admin API Key. +func (c *Client) RetrieveAdminKey( + ctx context.Context, + keyID string, +) (response AdminKey, err error) { + urlSuffix := fmt.Sprintf("%s/%s", adminKeysSuffix, keyID) + req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// DeleteAdminKey deletes an Admin API Key. +func (c *Client) DeleteAdminKey( + ctx context.Context, + keyID string, +) ( + response AdminKeyDeleteResponse, err error, +) { + urlSuffix := fmt.Sprintf("%s/%s", adminKeysSuffix, keyID) + req, err := c.newRequest(ctx, http.MethodDelete, c.fullURL(urlSuffix)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} diff --git a/admin_key_test.go b/admin_key_test.go new file mode 100644 index 000000000..e1f8a15f8 --- /dev/null +++ b/admin_key_test.go @@ -0,0 +1,156 @@ +package openai_test + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/sashabaranov/go-openai" + "github.com/sashabaranov/go-openai/internal/test/checks" +) + +func TestAdminKey(t *testing.T) { + adminKeyObject := "oranization.admin_api_key" + adminKeyID := "test_key_id" + adminKeyName := "test_key_name" + adminKeyRedactedValue := "test_key_redacted_value" + adminKeyCreatedAt := int64(1711471533) + + adminKeyOwnerType := "service_account" + adminKeyOwnerObject := "organization.service_account" + adminKeyOwnerID := "test_owner_id" + adminKeyOwnerName := "test_owner_name" + adminKeyOwnerRole := "member" + adminKeyOwnerCreatedAt := int64(1711471533) + + client, server, teardown := setupOpenAITestServer() + defer teardown() + + server.RegisterHandler( + "/v1/organization/admin_api_keys", + func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + resBytes, _ := json.Marshal(openai.AdminKeyList{ + Object: "list", + AdminKeys: []openai.AdminKey{ + { + Object: adminKeyObject, + ID: adminKeyID, + Name: adminKeyName, + RedactedValue: adminKeyRedactedValue, + CreatedAt: adminKeyCreatedAt, + Owner: openai.AdminKeyOwner{ + Type: adminKeyOwnerType, + Object: adminKeyOwnerObject, + ID: adminKeyOwnerID, + Name: adminKeyOwnerName, + CreatedAt: adminKeyOwnerCreatedAt, + Role: adminKeyOwnerRole, + }, + }, + }, + FirstID: "first_id", + LastID: "last_id", + HasMore: false, + }) + fmt.Fprintln(w, string(resBytes)) + + case http.MethodPost: + resBytes, _ := json.Marshal(openai.AdminKey{ + Object: adminKeyObject, + ID: adminKeyID, + Name: adminKeyName, + RedactedValue: adminKeyRedactedValue, + CreatedAt: adminKeyCreatedAt, + Owner: openai.AdminKeyOwner{ + Type: adminKeyOwnerType, + Object: adminKeyOwnerObject, + ID: adminKeyOwnerID, + Name: adminKeyOwnerName, + CreatedAt: adminKeyOwnerCreatedAt, + Role: adminKeyOwnerRole, + }, + }) + fmt.Fprintln(w, string(resBytes)) + } + }, + ) + + server.RegisterHandler( + "/v1/organization/admin_api_keys/"+adminKeyID, + func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + resBytes, _ := json.Marshal(openai.AdminKey{ + Object: adminKeyObject, + ID: adminKeyID, + Name: adminKeyName, + RedactedValue: adminKeyRedactedValue, + CreatedAt: adminKeyCreatedAt, + Owner: openai.AdminKeyOwner{ + Type: adminKeyOwnerType, + Object: adminKeyOwnerObject, + ID: adminKeyOwnerID, + Name: adminKeyOwnerName, + CreatedAt: adminKeyOwnerCreatedAt, + Role: adminKeyOwnerRole, + }, + }) + fmt.Fprintln(w, string(resBytes)) + + case http.MethodDelete: + resBytes, _ := json.Marshal(openai.AdminKeyDeleteResponse{ + ID: adminKeyID, + Object: adminKeyObject, + Deleted: true, + }) + fmt.Fprintln(w, string(resBytes)) + } + }, + ) + + ctx := context.Background() + + t.Run("ListAdminKeys", func(t *testing.T) { + adminKeys, err := client.ListAdminKeys(ctx, nil, nil, nil) + checks.NoError(t, err, "ListAdminKeys error") + + if len(adminKeys.AdminKeys) != 1 { + t.Fatalf("ListAdminKeys: expected 1 key, got %d", len(adminKeys.AdminKeys)) + } + + if adminKeys.AdminKeys[0].ID != adminKeyID { + t.Fatalf("ListAdminKeys: expected key ID %s, got %s", adminKeyID, adminKeys.AdminKeys[0].ID) + } + }) + + t.Run("CreateAdminKey", func(t *testing.T) { + adminKey, err := client.CreateAdminKey(ctx, adminKeyName) + checks.NoError(t, err, "CreateAdminKey error") + + if adminKey.ID != adminKeyID { + t.Fatalf("CreateAdminKey: expected key ID %s, got %s", adminKeyID, adminKey.ID) + } + }) + + t.Run("RetrieveAdminKey", func(t *testing.T) { + adminKey, err := client.RetrieveAdminKey(ctx, adminKeyID) + checks.NoError(t, err, "RetrieveAdminKey error") + + if adminKey.ID != adminKeyID { + t.Fatalf("RetrieveAdminKey: expected key ID %s, got %s", adminKeyID, adminKey.ID) + } + }) + + t.Run("DeleteAdminKey", func(t *testing.T) { + adminKeyDeleteResponse, err := client.DeleteAdminKey(ctx, adminKeyID) + checks.NoError(t, err, "DeleteAdminKey error") + + if !adminKeyDeleteResponse.Deleted { + t.Fatalf("DeleteAdminKey: expected key to be deleted, got not deleted") + } + }) +} diff --git a/admin_project.go b/admin_project.go new file mode 100644 index 000000000..32c6afb58 --- /dev/null +++ b/admin_project.go @@ -0,0 +1,141 @@ +package openai + +import ( + "context" + "fmt" + "net/http" + "net/url" +) + +const ( + adminProjectSuffix = "/organization/projects" +) + +// AdminProject represents an Admin Project object. +type AdminProject struct { + ID string `json:"id"` + Object string `json:"object"` + Name string `json:"name"` + CreatedAt int64 `json:"created_at"` + ArchivedAt *int64 `json:"archived_at"` + Status string `json:"status"` + + httpHeader +} + +// AdminProjectList represents a list of Admin Projects. +type AdminProjectList struct { + Object string `json:"object"` + Data []AdminProject `json:"data"` + FirstID string `json:"first_id"` + LastID string `json:"last_id"` + HasMore bool `json:"has_more"` + + httpHeader +} + +// ListAdminProjects lists Admin Projects associated with the organization. +func (c *Client) ListAdminProjects( + ctx context.Context, + limit *int, + after *string, + includeArchived *bool, +) (response AdminProjectList, err error) { + urlValues := url.Values{} + if limit != nil { + urlValues.Set("limit", fmt.Sprintf("%d", *limit)) + } + if after != nil { + urlValues.Set("after", *after) + } + if includeArchived != nil { + urlValues.Set("include_archived", fmt.Sprintf("%t", *includeArchived)) + } + + encodedValues := "" + if len(urlValues) > 0 { + encodedValues = "?" + urlValues.Encode() + } + + urlSuffix := adminProjectSuffix + encodedValues + req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// CreateAdminProject creates an Admin Project associated with the organization. +func (c *Client) CreateAdminProject( + ctx context.Context, + name string, +) (response AdminProject, err error) { + request := struct { + Name string `json:"name"` + }{ + Name: name, + } + + urlSuffix := adminProjectSuffix + req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix), withBody(request)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// RetrieveAdminProject retrieves an Admin Project associated with the organization. +func (c *Client) RetrieveAdminProject( + ctx context.Context, + id string, +) (response AdminProject, err error) { + urlSuffix := adminProjectSuffix + "/" + id + req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// ModifyAdminProject modifies an Admin Project associated with the organization. +func (c *Client) ModifyAdminProject( + ctx context.Context, + id string, + name string, +) (response AdminProject, err error) { + request := struct { + Name string `json:"name"` + }{ + Name: name, + } + + urlSuffix := adminProjectSuffix + "/" + id + req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix), withBody(request)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// UpdateAdminProject updates an Admin Project associated with the organization. +func (c *Client) ArchiveAdminProject( + ctx context.Context, + id string, +) (response AdminProject, err error) { + urlSuffix := adminProjectSuffix + "/" + id + "/archive" + req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} diff --git a/admin_project_test.go b/admin_project_test.go new file mode 100644 index 000000000..6c60dca57 --- /dev/null +++ b/admin_project_test.go @@ -0,0 +1,157 @@ +package openai_test + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/sashabaranov/go-openai" + "github.com/sashabaranov/go-openai/internal/test/checks" +) + +func TestAdminProject(t *testing.T) { + adminProjectObject := "organization.project" + adminProjectID := "project-abc-123" + adminProjectName := "Project Name" + + adminProjectCreatedAt := int64(1711471533) + adminProjectArchivedAt := int64(1711471533) + adminProjectStatus := "active" + + client, server, teardown := setupOpenAITestServer() + defer teardown() + + server.RegisterHandler( + "/v1/organization/projects", + func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + resBytes, _ := json.Marshal(openai.AdminProjectList{ + Object: "list", + Data: []openai.AdminProject{ + { + ID: adminProjectID, + Object: adminProjectObject, + Name: adminProjectName, + CreatedAt: adminProjectCreatedAt, + ArchivedAt: &adminProjectArchivedAt, + Status: adminProjectStatus, + }, + }, + FirstID: "first_id", + LastID: "last_id", + HasMore: false, + }) + fmt.Fprintln(w, string(resBytes)) + + case http.MethodPost: + resBytes, _ := json.Marshal(openai.AdminProject{ + ID: adminProjectID, + Object: adminProjectObject, + Name: adminProjectName, + CreatedAt: adminProjectCreatedAt, + Status: adminProjectStatus, + }) + fmt.Fprintln(w, string(resBytes)) + } + }, + ) + + server.RegisterHandler( + "/v1/organization/projects/"+adminProjectID, + func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + resBytes, _ := json.Marshal(openai.AdminProject{ + ID: adminProjectID, + Object: adminProjectObject, + Name: adminProjectName, + CreatedAt: adminProjectCreatedAt, + ArchivedAt: &adminProjectArchivedAt, + Status: adminProjectStatus, + }) + fmt.Fprintln(w, string(resBytes)) + + case http.MethodPost: + resBytes, _ := json.Marshal(openai.AdminProject{ + ID: adminProjectID, + Object: adminProjectObject, + Name: adminProjectName, + CreatedAt: adminProjectCreatedAt, + Status: adminProjectStatus, + }) + fmt.Fprintln(w, string(resBytes)) + } + }, + ) + + server.RegisterHandler( + "/v1/organization/projects/"+adminProjectID+"/archive", + func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + t.Errorf("expected POST request, got %s", r.Method) + } + resBytes, _ := json.Marshal(openai.AdminProject{ + ID: adminProjectID, + Object: adminProjectObject, + Name: adminProjectName, + CreatedAt: adminProjectCreatedAt, + Status: "archived", + }) + fmt.Fprintln(w, string(resBytes)) + }, + ) + + ctx := context.Background() + + t.Run("ListAdminProjects", func(t *testing.T) { + adminProjects, err := client.ListAdminProjects(ctx, nil, nil, nil) + checks.NoError(t, err, "ListAdminProjects error") + + if len(adminProjects.Data) != 1 { + t.Errorf("expected 1 project, got %d", len(adminProjects.Data)) + } + + if adminProjects.Data[0].ID != adminProjectID { + t.Errorf("expected project ID %s, got %s", adminProjectID, adminProjects.Data[0].ID) + } + }) + + t.Run("CreateAdminProject", func(t *testing.T) { + adminProject, err := client.CreateAdminProject(ctx, adminProjectName) + checks.NoError(t, err, "CreateAdminProject error") + + if adminProject.ID != adminProjectID { + t.Errorf("expected project ID %s, got %s", adminProjectID, adminProject.ID) + } + }) + + t.Run("GetAdminProject", func(t *testing.T) { + adminProject, err := client.RetrieveAdminProject(ctx, adminProjectID) + checks.NoError(t, err, "GetAdminProject error") + + if adminProject.ID != adminProjectID { + t.Errorf("expected project ID %s, got %s", adminProjectID, adminProject.ID) + } + }) + + t.Run("ModifyAdminProject", func(t *testing.T) { + adminProject, err := client.ModifyAdminProject(ctx, adminProjectID, adminProjectName) + checks.NoError(t, err, "ModifyAdminProject error") + + if adminProject.ID != adminProjectID { + t.Errorf("expected project ID %s, got %s", adminProjectID, adminProject.ID) + } + }) + + t.Run("ArchiveAdminProject", func(t *testing.T) { + adminProject, err := client.ArchiveAdminProject(ctx, adminProjectID) + checks.NoError(t, err, "ArchiveAdminProject error") + + if adminProject.Status != "archived" { + t.Errorf("expected project status archived, got %s", adminProject.Status) + } + }) +} diff --git a/admin_project_user.go b/admin_project_user.go new file mode 100644 index 000000000..d6bd74e82 --- /dev/null +++ b/admin_project_user.go @@ -0,0 +1,152 @@ +package openai + +import ( + "context" + "fmt" + "net/http" + "net/url" +) + +// AdminProjectUser represents a user associated with a project. +type AdminProjectUser struct { + Object string `json:"object"` + ID string `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + Role string `json:"role"` + AddedAt int64 `json:"added_at"` + + httpHeader +} + +// AdminProjectUserList represents a list of users associated with a project. +type AdminProjectUserList struct { + Object string `json:"object"` + Data []AdminProjectUser `json:"data"` + FirstID string `json:"first_id"` + LastID string `json:"last_id"` + HasMore bool `json:"has_more"` + + httpHeader +} + +// AdminProjectDeleteResponse represents the response when deleting a project. +type AdminProjectDeleteResponse struct { + Object string `json:"object"` + ID string `json:"id"` + Deleted bool `json:"deleted"` + + httpHeader +} + +// ListAdminProjectUsers lists users associated with a project. +func (c *Client) ListAdminProjectUsers( + ctx context.Context, + projectID string, + limit *int, + after *string, +) (response AdminProjectUserList, err error) { + urlValues := url.Values{} + urlValues.Add("project", projectID) + if limit != nil { + urlValues.Add("limit", fmt.Sprintf("%d", *limit)) + } + if after != nil { + urlValues.Add("after", *after) + } + + encodedValues := "" + if len(urlValues) > 0 { + encodedValues = "?" + urlValues.Encode() + } + + urlSuffix := fmt.Sprintf("/v1/projects/%s/users%s", projectID, encodedValues) + req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// CreateProjectUser creates a user associated with a project. +func (c *Client) CreateAdminProjectUser( + ctx context.Context, + projectID string, + userID string, + role string, +) (response AdminProjectUser, err error) { + request := struct { + UserID string `json:"user_id"` + Role string `json:"role"` + }{ + UserID: userID, + Role: role, + } + + urlSuffix := fmt.Sprintf("/v1/projects/%s/users", projectID) + req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix), withBody(request)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// RetrieveProjectUser retrieves a user associated with a project. +func (c *Client) RetrieveAdminProjectUser( + ctx context.Context, + projectID string, + userID string, +) (response AdminProjectUser, err error) { + urlSuffix := fmt.Sprintf("/v1/projects/%s/users/%s", projectID, userID) + req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// ModifyProjectUser modifies a user associated with a project. +func (c *Client) ModifyAdminProjectUser( + ctx context.Context, + projectID string, + userID string, + role string, +) (response AdminProjectUser, err error) { + request := struct { + Role string `json:"role"` + }{ + Role: role, + } + + urlSuffix := fmt.Sprintf("/v1/projects/%s/users/%s", projectID, userID) + req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix), withBody(request)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// DeleteProjectUser deletes a user associated with a project. +func (c *Client) DeleteAdminProjectUser( + ctx context.Context, + projectID string, + userID string, +) (response AdminProjectDeleteResponse, err error) { + urlSuffix := fmt.Sprintf("/v1/projects/%s/users/%s", projectID, userID) + + req, err := c.newRequest(ctx, http.MethodDelete, c.fullURL(urlSuffix)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} diff --git a/admin_project_user_test.go b/admin_project_user_test.go new file mode 100644 index 000000000..48a07382f --- /dev/null +++ b/admin_project_user_test.go @@ -0,0 +1,154 @@ +package openai_test + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/sashabaranov/go-openai" + "github.com/sashabaranov/go-openai/internal/test/checks" +) + +func TestAdminProjectUser(t *testing.T) { + adminProjectID := "project-abc-123" + adminProjectUserObject := "organization.project.user" + adminProjectUserID := "user-abc-123" + adminProjectUserName := "User Name" + adminProjectUserEmail := "test@here.com" + adminProjectUserRole := "owner" + adminProjectUserAddedAt := int64(1711471533) + + client, server, teardown := setupOpenAITestServer() + defer teardown() + + server.RegisterHandler( + fmt.Sprintf("/v1/organization/projects/%s/users", adminProjectID), + func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + resBytes, _ := json.Marshal(openai.AdminProjectUserList{ + Object: "list", + Data: []openai.AdminProjectUser{ + { + ID: adminProjectUserID, + Object: adminProjectUserObject, + Name: adminProjectUserName, + Email: adminProjectUserEmail, + Role: adminProjectUserRole, + AddedAt: adminProjectUserAddedAt, + }, + }, + FirstID: "first_id", + LastID: "last_id", + HasMore: false, + }) + fmt.Fprintln(w, string(resBytes)) + + case http.MethodPost: + resBytes, _ := json.Marshal(openai.AdminProjectUser{ + ID: adminProjectUserID, + Object: adminProjectUserObject, + Email: adminProjectUserEmail, + Role: adminProjectUserRole, + AddedAt: adminProjectUserAddedAt, + }) + fmt.Fprintln(w, string(resBytes)) + } + }, + ) + + server.RegisterHandler( + fmt.Sprintf("/v1/organization/projects/%s/users/%s", adminProjectID, adminProjectUserID), + func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + resBytes, _ := json.Marshal(openai.AdminProjectUser{ + ID: adminProjectUserID, + Object: adminProjectUserObject, + Name: adminProjectUserName, + Email: adminProjectUserEmail, + Role: adminProjectUserRole, + AddedAt: adminProjectUserAddedAt, + }) + fmt.Fprintln(w, string(resBytes)) + + case http.MethodPost: + resBytes, _ := json.Marshal(openai.AdminProjectUser{ + ID: adminProjectUserID, + Object: adminProjectUserObject, + Email: adminProjectUserEmail, + Role: adminProjectUserRole, + AddedAt: adminProjectUserAddedAt, + }) + fmt.Fprintln(w, string(resBytes)) + + case http.MethodDelete: + resBytes, _ := json.Marshal(openai.AdminProjectDeleteResponse{ + Object: "delete", + ID: adminProjectUserID, + Deleted: true, + }) + fmt.Fprintln(w, string(resBytes)) + } + }, + ) + + ctx := context.Background() + + t.Run("ListAdminProjectUsers", func(t *testing.T) { + adminProjectUsers, err := client.ListAdminProjectUsers(ctx, adminProjectID, nil, nil) + checks.NoError(t, err, "ListAdminProjectUsers error") + + if len(adminProjectUsers.Data) != 1 { + t.Errorf("expected 1 project user, got %d", len(adminProjectUsers.Data)) + } + + adminProjectUser := adminProjectUsers.Data[0] + if adminProjectUser.ID != adminProjectUserID { + t.Errorf("expected user ID %s, got %s", adminProjectUserID, adminProjectUser.ID) + } + }) + + t.Run("CreateAdminProjectUser", func(t *testing.T) { + adminProjectUser, err := client.CreateAdminProjectUser( + ctx, + adminProjectID, + adminProjectUserEmail, + adminProjectUserRole, + ) + checks.NoError(t, err, "CreateAdminProjectUser error") + + if adminProjectUser.ID != adminProjectUserID { + t.Errorf("expected user ID %s, got %s", adminProjectUserID, adminProjectUser.ID) + } + }) + + t.Run("RetrieveAdminProjectUser", func(t *testing.T) { + adminProjectUser, err := client.RetrieveAdminProjectUser(ctx, adminProjectID, adminProjectUserID) + checks.NoError(t, err, "RetrieveAdminProjectUser error") + + if adminProjectUser.ID != adminProjectUserID { + t.Errorf("expected user ID %s, got %s", adminProjectUserID, adminProjectUser.ID) + } + }) + + t.Run("ModifyAdminProjectUser", func(t *testing.T) { + adminProjectUser, err := client.ModifyAdminProjectUser(ctx, adminProjectID, adminProjectUserID, adminProjectUserRole) + checks.NoError(t, err, "ModifyAdminProjectUser error") + + if adminProjectUser.ID != adminProjectUserID { + t.Errorf("expected user ID %s, got %s", adminProjectUserID, adminProjectUser.ID) + } + }) + + t.Run("DeleteAdminProjectUser", func(t *testing.T) { + adminProjectUser, err := client.DeleteAdminProjectUser(ctx, adminProjectID, adminProjectUserID) + checks.NoError(t, err, "DeleteAdminProjectUser error") + + if !adminProjectUser.Deleted { + t.Errorf("expected user to be deleted, got %t", adminProjectUser.Deleted) + } + }) +} diff --git a/admin_usage.go b/admin_usage.go new file mode 100644 index 000000000..1065402f9 --- /dev/null +++ b/admin_usage.go @@ -0,0 +1,99 @@ +package openai + +import ( + "context" + "fmt" + "net/http" + "net/url" +) + +const ( + adminUsageCostSuffix = "/organization/costs" +) + +// AdminUsageCostRequest represents a request to get usage costs. +type AdminUsageCostRequest struct { + StartTime int64 `json:"start_time"` + EndTime *int64 `json:"end_time,omitempty"` + BucketWidth *string `json:"bucket_width,omitempty"` + ProjectIDs []string `json:"project_ids,omitempty"` + GroupBy *string `json:"group_by,omitempty"` + Limit *int `json:"limit,omitempty"` + Page *string `json:"page,omitempty"` +} + +// AdminUsageCost represents a usage cost. +type AdminUsageCost struct { + Object string `json:"object"` + Amount AdminUsageCostAmount `json:"amount"` + LineItem *string `json:"line_item"` + ProjectID *string `json:"project_id"` + OrganizationID *string `json:"organization_id"` +} + +// AdminUsageCostAmount represents the amount of a usage cost. +type AdminUsageCostAmount struct { + Value float64 `json:"value"` + Currency string `json:"currency"` +} + +// AdminUsageCostBucket represents a bucket of usage costs based on a time range. +type AdminUsageCostBucket struct { + Object string `json:"object"` + StartTime int64 `json:"start_time"` + EndTime int64 `json:"end_time"` + Results []AdminUsageCost `json:"results"` +} + +// AdminUsageCostResult represents the response from getting usage costs. +type AdminUsageCostResult struct { + Object string `json:"object"` + Data []AdminUsageCostBucket `json:"data"` + HasMore bool `json:"has_more"` + NextPage *string `json:"next_page"` + + httpHeader +} + +// GetAdminUsageCost gets usage costs for the organization. +func (c *Client) GetAdminUsageCost( + ctx context.Context, + request AdminUsageCostRequest, +) (response AdminUsageCostResult, err error) { + urlValues := url.Values{} + urlValues.Add("start_time", fmt.Sprintf("%d", request.StartTime)) + if request.EndTime != nil { + urlValues.Add("end_time", fmt.Sprintf("%d", *request.EndTime)) + } + if request.BucketWidth != nil { + urlValues.Add("bucket_width", *request.BucketWidth) + } + if len(request.ProjectIDs) > 0 { + for _, projectID := range request.ProjectIDs { + urlValues.Add("project_ids[]", projectID) + } + } + if request.GroupBy != nil { + urlValues.Add("group_by", *request.GroupBy) + } + if request.Limit != nil { + urlValues.Add("limit", fmt.Sprintf("%d", *request.Limit)) + } + if request.Page != nil { + urlValues.Add("page", *request.Page) + } + + encodedValues := "" + if len(urlValues) > 0 { + encodedValues = "?" + urlValues.Encode() + } + + urlSuffix := adminUsageCostSuffix + encodedValues + req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} diff --git a/admin_usage_test.go b/admin_usage_test.go new file mode 100644 index 000000000..3ccce4e15 --- /dev/null +++ b/admin_usage_test.go @@ -0,0 +1,109 @@ +package openai_test + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/sashabaranov/go-openai" + "github.com/sashabaranov/go-openai/internal/test/checks" +) + +func TestAdminUsageCost(t *testing.T) { + costObject := "page" + bucketObject := "bucket" + startTime := int64(1711471533) + endTime := int64(1711471534) + resultObject := "organization.costs.result" + amountValue := 50.23 + amountCurrency := "usd" + lineItem := "Image Models" + projectID := "project_abc" + organizationID := "organization_id" + + client, server, teardown := setupOpenAITestServer() + defer teardown() + + server.RegisterHandler( + "/v1/organization/costs", + func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + resBytes, _ := json.Marshal(openai.AdminUsageCostResult{ + Object: resultObject, + Data: []openai.AdminUsageCostBucket{ + { + Object: bucketObject, + StartTime: startTime, + EndTime: endTime, + Results: []openai.AdminUsageCost{ + { + Object: costObject, + Amount: openai.AdminUsageCostAmount{ + Value: amountValue, + Currency: amountCurrency, + }, + LineItem: &lineItem, + ProjectID: &projectID, + OrganizationID: &organizationID, + }, + }, + }, + }, + HasMore: false, + NextPage: nil, + }) + fmt.Fprintln(w, string(resBytes)) + } + }, + ) + + ctx := context.Background() + + t.Run("GetAdminUsageCost", func(t *testing.T) { + request := openai.AdminUsageCostRequest{ + StartTime: startTime, + } + costResult, err := client.GetAdminUsageCost(ctx, request) + checks.NoError(t, err) + + if costResult.Object != resultObject { + t.Errorf("unexpected object: %v", costResult.Object) + } + + if len(costResult.Data) != 1 { + t.Errorf("unexpected data length: %v", len(costResult.Data)) + } + + bucket := costResult.Data[0] + if bucket.Object != bucketObject { + t.Errorf("unexpected bucket object: %v", bucket.Object) + } + + if bucket.StartTime != startTime { + t.Errorf("unexpected start time: %v", bucket.StartTime) + } + + if bucket.EndTime != endTime { + t.Errorf("unexpected end time: %v", bucket.EndTime) + } + + if len(bucket.Results) != 1 { + t.Errorf("unexpected results length: %v", len(bucket.Results)) + } + + cost := bucket.Results[0] + if cost.Object != costObject { + t.Errorf("unexpected cost object: %v", cost.Object) + } + + if cost.Amount.Value != amountValue { + t.Errorf("unexpected amount value: %v", cost.Amount.Value) + } + + if cost.Amount.Currency != amountCurrency { + t.Errorf("unexpected amount currency: %v", cost.Amount.Currency) + } + }) +} diff --git a/admin_user.go b/admin_user.go new file mode 100644 index 000000000..dd3354f78 --- /dev/null +++ b/admin_user.go @@ -0,0 +1,129 @@ +package openai + +import ( + "context" + "fmt" + "net/http" + "net/url" +) + +const ( + adminUsersSuffix = "/organization/users" +) + +// AdminUser represents a User. +type AdminUser struct { + Object string `json:"object"` + ID string `json:"id"` + Name string `json:"name"` + Email string `json:"email"` + Role string `json:"role"` + AddedAt int64 `json:"added_at"` + + httpHeader +} + +// AdminUserList represents a list of Users. +type AdminUserList struct { + Object string `json:"object"` + User []AdminUser `json:"data"` + FirstID string `json:"first_id"` + LastID string `json:"last_id"` + HasMore bool `json:"has_more"` + + httpHeader +} + +// AdminUserDeleteResponse represents the response from deleting an User. +type AdminUserDeleteResponse struct { + ID string `json:"id"` + Object string `json:"object"` + Deleted bool `json:"deleted"` + + httpHeader +} + +// AdminListUsers lists Users associated with the organization. +func (c *Client) ListAdminUsers( + ctx context.Context, + limit *int, + after *string, + emails *[]string, +) (response AdminUserList, err error) { + urlValues := url.Values{} + if limit != nil { + urlValues.Add("limit", fmt.Sprintf("%d", *limit)) + } + if after != nil { + urlValues.Add("after", *after) + } + if emails != nil { + for _, email := range *emails { + urlValues.Add("emails[]", email) + } + } + + encodedValues := "" + if len(urlValues) > 0 { + encodedValues = "?" + urlValues.Encode() + } + + urlSuffix := adminUsersSuffix + encodedValues + req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// ModifyAdminUser modifies an User. +func (c *Client) ModifyAdminUser( + ctx context.Context, + id string, + role string, +) (response AdminUser, err error) { + type ModifyAdminUserRequest struct { + Role string `json:"role"` + } + urlSuffix := adminUsersSuffix + "/" + id + req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix), + withBody(ModifyAdminUserRequest{Role: role})) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// RetrieveAdminUser retrieves an User. +func (c *Client) RetrieveAdminUser( + ctx context.Context, + id string, +) (response AdminUser, err error) { + urlSuffix := adminUsersSuffix + "/" + id + req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} + +// AdminDeleteUser deletes an User. +func (c *Client) DeleteAdminUser( + ctx context.Context, + id string, +) (response AdminUserDeleteResponse, err error) { + urlSuffix := adminUsersSuffix + "/" + id + req, err := c.newRequest(ctx, http.MethodDelete, c.fullURL(urlSuffix)) + if err != nil { + return + } + + err = c.sendRequest(req, &response) + return +} diff --git a/admin_user_test.go b/admin_user_test.go new file mode 100644 index 000000000..dee0643ee --- /dev/null +++ b/admin_user_test.go @@ -0,0 +1,128 @@ +package openai_test + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "testing" + + "github.com/sashabaranov/go-openai" + "github.com/sashabaranov/go-openai/internal/test/checks" +) + +func TestAdminUser(t *testing.T) { + adminUserObject := "organization.user" + adminUserID := "user-id" + adminUserName := "user-name" + adminUserEmail := "user-email" + adminUserRole := "member" + adminUserAddedAt := int64(1711471533) + + client, server, teardown := setupOpenAITestServer() + defer teardown() + + server.RegisterHandler( + "/v1/organization/users", + func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodGet { + resBytes, _ := json.Marshal(openai.AdminUserList{ + Object: "list", + User: []openai.AdminUser{ + { + Object: adminUserObject, + ID: adminUserID, + Name: adminUserName, + Email: adminUserEmail, + Role: adminUserRole, + AddedAt: adminUserAddedAt, + }, + }, + FirstID: "first_id", + LastID: "last_id", + HasMore: false, + }) + fmt.Fprintln(w, string(resBytes)) + } + }, + ) + + server.RegisterHandler( + "/v1/organization/users/"+adminUserID, + func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + resBytes, _ := json.Marshal(openai.AdminUser{ + Object: adminUserObject, + ID: adminUserID, + Name: adminUserName, + Email: adminUserEmail, + Role: adminUserRole, + AddedAt: adminUserAddedAt, + }) + fmt.Fprintln(w, string(resBytes)) + + case http.MethodPost: + resBytes, _ := json.Marshal(openai.AdminUser{ + Object: adminUserObject, + ID: adminUserID, + Name: adminUserName, + Email: adminUserEmail, + Role: adminUserRole, + AddedAt: adminUserAddedAt, + }) + fmt.Fprintln(w, string(resBytes)) + + case http.MethodDelete: + resBytes, _ := json.Marshal(openai.AdminUserDeleteResponse{ + ID: adminUserID, + Object: adminUserObject, + Deleted: true, + }) + fmt.Fprintln(w, string(resBytes)) + } + }, + ) + + ctx := context.Background() + + t.Run("ListAdminUsers", func(t *testing.T) { + response, err := client.ListAdminUsers(ctx, nil, nil, nil) + checks.NoError(t, err, "AdminListUsers error") + + if len(response.User) != 1 { + t.Errorf("AdminListUsers returned %d users, want 1", len(response.User)) + } + + if response.User[0].ID != adminUserID { + t.Errorf("AdminListUsers returned user ID %s, want %s", response.User[0].ID, adminUserID) + } + }) + + t.Run("ModifyAdminUser", func(t *testing.T) { + response, err := client.ModifyAdminUser(ctx, adminUserID, adminUserRole) + checks.NoError(t, err, "ModifyAdminUser error") + + if response.ID != adminUserID { + t.Errorf("ModifyAdminUser returned user ID %s, want %s", response.ID, adminUserID) + } + }) + + t.Run("RetrieveAdminUser", func(t *testing.T) { + response, err := client.RetrieveAdminUser(ctx, adminUserID) + checks.NoError(t, err, "GetAdminUser error") + + if response.ID != adminUserID { + t.Errorf("GetAdminUser returned user ID %s, want %s", response.ID, adminUserID) + } + }) + + t.Run("DeleteAdminUser", func(t *testing.T) { + response, err := client.DeleteAdminUser(ctx, adminUserID) + checks.NoError(t, err, "DeleteAdminUser error") + + if !response.Deleted { + t.Errorf("DeleteAdminUser returned user not deleted, want deleted") + } + }) +} From 2ec4c2ce0099bb05d0ddc96ac7dc05d445eae702 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Tue, 11 Mar 2025 14:13:57 -0400 Subject: [PATCH 4/7] Fixed endpoints for project user --- admin_project_user.go | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/admin_project_user.go b/admin_project_user.go index d6bd74e82..7258cda21 100644 --- a/admin_project_user.go +++ b/admin_project_user.go @@ -47,7 +47,6 @@ func (c *Client) ListAdminProjectUsers( after *string, ) (response AdminProjectUserList, err error) { urlValues := url.Values{} - urlValues.Add("project", projectID) if limit != nil { urlValues.Add("limit", fmt.Sprintf("%d", *limit)) } @@ -60,7 +59,7 @@ func (c *Client) ListAdminProjectUsers( encodedValues = "?" + urlValues.Encode() } - urlSuffix := fmt.Sprintf("/v1/projects/%s/users%s", projectID, encodedValues) + urlSuffix := fmt.Sprintf("/organization/projects/%s/users%s", projectID, encodedValues) req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix)) if err != nil { return @@ -85,7 +84,7 @@ func (c *Client) CreateAdminProjectUser( Role: role, } - urlSuffix := fmt.Sprintf("/v1/projects/%s/users", projectID) + urlSuffix := fmt.Sprintf("/organization/projects/%s/users", projectID) req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix), withBody(request)) if err != nil { return @@ -101,7 +100,7 @@ func (c *Client) RetrieveAdminProjectUser( projectID string, userID string, ) (response AdminProjectUser, err error) { - urlSuffix := fmt.Sprintf("/v1/projects/%s/users/%s", projectID, userID) + urlSuffix := fmt.Sprintf("/organization/projects/%s/users/%s", projectID, userID) req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix)) if err != nil { return @@ -124,7 +123,7 @@ func (c *Client) ModifyAdminProjectUser( Role: role, } - urlSuffix := fmt.Sprintf("/v1/projects/%s/users/%s", projectID, userID) + urlSuffix := fmt.Sprintf("/organization/projects/%s/users/%s", projectID, userID) req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix), withBody(request)) if err != nil { return @@ -140,7 +139,7 @@ func (c *Client) DeleteAdminProjectUser( projectID string, userID string, ) (response AdminProjectDeleteResponse, err error) { - urlSuffix := fmt.Sprintf("/v1/projects/%s/users/%s", projectID, userID) + urlSuffix := fmt.Sprintf("/organization/projects/%s/users/%s", projectID, userID) req, err := c.newRequest(ctx, http.MethodDelete, c.fullURL(urlSuffix)) if err != nil { From 71a1f93e9003775d79bf11e3982e88bd05c42557 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Wed, 7 May 2025 10:46:30 -0400 Subject: [PATCH 5/7] Added query params to testing for better coverage --- admin_invite_test.go | 6 +++++- admin_usage_test.go | 15 ++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/admin_invite_test.go b/admin_invite_test.go index 0bfb44dbc..ab94eea3d 100644 --- a/admin_invite_test.go +++ b/admin_invite_test.go @@ -106,7 +106,11 @@ func TestAdminInvite(t *testing.T) { ctx := context.Background() t.Run("ListAdminInvites", func(t *testing.T) { - adminInvites, err := client.ListAdminInvites(ctx, nil, nil) + + limit := 10 + after := "after-id" + + adminInvites, err := client.ListAdminInvites(ctx, &limit, &after) checks.NoError(t, err, "ListAdminInvites error") if len(adminInvites.AdminInvites) != 1 { diff --git a/admin_usage_test.go b/admin_usage_test.go index 3ccce4e15..0b34a75a9 100644 --- a/admin_usage_test.go +++ b/admin_usage_test.go @@ -62,8 +62,21 @@ func TestAdminUsageCost(t *testing.T) { ctx := context.Background() t.Run("GetAdminUsageCost", func(t *testing.T) { + + bucketWidth := "1h" + groupBy := "project_id" + projectIDs := []string{"project_1", "project_2"} + limit := 10 + page := "page_1" + request := openai.AdminUsageCostRequest{ - StartTime: startTime, + StartTime: startTime, + EndTime: &endTime, + BucketWidth: &bucketWidth, + GroupBy: &groupBy, + ProjectIDs: projectIDs, + Limit: &limit, + Page: &page, } costResult, err := client.GetAdminUsageCost(ctx, request) checks.NoError(t, err) From f69ba173ea291d4545757fe0a6d5f00396623e8a Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Wed, 7 May 2025 10:48:11 -0400 Subject: [PATCH 6/7] Fixed lint checks --- admin_invite_test.go | 1 - admin_usage_test.go | 1 - 2 files changed, 2 deletions(-) diff --git a/admin_invite_test.go b/admin_invite_test.go index ab94eea3d..3d3b98ca1 100644 --- a/admin_invite_test.go +++ b/admin_invite_test.go @@ -106,7 +106,6 @@ func TestAdminInvite(t *testing.T) { ctx := context.Background() t.Run("ListAdminInvites", func(t *testing.T) { - limit := 10 after := "after-id" diff --git a/admin_usage_test.go b/admin_usage_test.go index 0b34a75a9..d625623de 100644 --- a/admin_usage_test.go +++ b/admin_usage_test.go @@ -62,7 +62,6 @@ func TestAdminUsageCost(t *testing.T) { ctx := context.Background() t.Run("GetAdminUsageCost", func(t *testing.T) { - bucketWidth := "1h" groupBy := "project_id" projectIDs := []string{"project_1", "project_2"} From b295cf78e81c9a54645fbd037ec5a75e30d31a2b Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Wed, 7 May 2025 13:19:19 -0400 Subject: [PATCH 7/7] Added additional test cases to address coverage of tests against filtering 'List' functionality --- admin_invite_test.go | 13 +++++++++++++ admin_key_test.go | 17 +++++++++++++++++ admin_project_test.go | 6 +++++- admin_project_user_test.go | 17 +++++++++++++++++ admin_user_test.go | 17 +++++++++++++++++ 5 files changed, 69 insertions(+), 1 deletion(-) diff --git a/admin_invite_test.go b/admin_invite_test.go index 3d3b98ca1..47dbccb53 100644 --- a/admin_invite_test.go +++ b/admin_invite_test.go @@ -106,6 +106,19 @@ func TestAdminInvite(t *testing.T) { ctx := context.Background() t.Run("ListAdminInvites", func(t *testing.T) { + adminInvites, err := client.ListAdminInvites(ctx, nil, nil) + checks.NoError(t, err, "ListAdminInvites error") + + if len(adminInvites.AdminInvites) != 1 { + t.Fatalf("expected 1 admin invite, got %d", len(adminInvites.AdminInvites)) + } + + if adminInvites.AdminInvites[0].ID != adminInviteID { + t.Errorf("expected admin invite ID %s, got %s", adminInviteID, adminInvites.AdminInvites[0].ID) + } + }) + + t.Run("ListAdminInvitesFilter", func(t *testing.T) { limit := 10 after := "after-id" diff --git a/admin_key_test.go b/admin_key_test.go index e1f8a15f8..7124e1844 100644 --- a/admin_key_test.go +++ b/admin_key_test.go @@ -127,6 +127,23 @@ func TestAdminKey(t *testing.T) { } }) + t.Run("ListAdminKeysFilter", func(t *testing.T) { + limit := 5 + order := "asc" + after := "after_id" + + adminKeys, err := client.ListAdminKeys(ctx, &limit, &order, &after) + checks.NoError(t, err, "ListAdminKeys error") + + if len(adminKeys.AdminKeys) != 1 { + t.Fatalf("ListAdminKeys: expected 1 key, got %d", len(adminKeys.AdminKeys)) + } + + if adminKeys.AdminKeys[0].ID != adminKeyID { + t.Fatalf("ListAdminKeys: expected key ID %s, got %s", adminKeyID, adminKeys.AdminKeys[0].ID) + } + }) + t.Run("CreateAdminKey", func(t *testing.T) { adminKey, err := client.CreateAdminKey(ctx, adminKeyName) checks.NoError(t, err, "CreateAdminKey error") diff --git a/admin_project_test.go b/admin_project_test.go index 6c60dca57..a4c1813de 100644 --- a/admin_project_test.go +++ b/admin_project_test.go @@ -107,7 +107,11 @@ func TestAdminProject(t *testing.T) { ctx := context.Background() t.Run("ListAdminProjects", func(t *testing.T) { - adminProjects, err := client.ListAdminProjects(ctx, nil, nil, nil) + limit := 5 + after := "after_id" + includeArchived := true + + adminProjects, err := client.ListAdminProjects(ctx, &limit, &after, &includeArchived) checks.NoError(t, err, "ListAdminProjects error") if len(adminProjects.Data) != 1 { diff --git a/admin_project_user_test.go b/admin_project_user_test.go index 48a07382f..c243bb47a 100644 --- a/admin_project_user_test.go +++ b/admin_project_user_test.go @@ -111,6 +111,23 @@ func TestAdminProjectUser(t *testing.T) { } }) + t.Run("ListAdminProjectUsersFilter", func(t *testing.T) { + limit := 5 + after := "after_id" + + adminProjectUsers, err := client.ListAdminProjectUsers(ctx, adminProjectID, &limit, &after) + checks.NoError(t, err, "ListAdminProjectUsers error") + + if len(adminProjectUsers.Data) != 1 { + t.Errorf("expected 1 project user, got %d", len(adminProjectUsers.Data)) + } + + adminProjectUser := adminProjectUsers.Data[0] + if adminProjectUser.ID != adminProjectUserID { + t.Errorf("expected user ID %s, got %s", adminProjectUserID, adminProjectUser.ID) + } + }) + t.Run("CreateAdminProjectUser", func(t *testing.T) { adminProjectUser, err := client.CreateAdminProjectUser( ctx, diff --git a/admin_user_test.go b/admin_user_test.go index dee0643ee..ba809ebc9 100644 --- a/admin_user_test.go +++ b/admin_user_test.go @@ -99,6 +99,23 @@ func TestAdminUser(t *testing.T) { } }) + t.Run("ListAdminUsersFilter", func(t *testing.T) { + limit := 10 + after := "after-id" + emails := []string{"test@here.com"} + + response, err := client.ListAdminUsers(ctx, &limit, &after, &emails) + checks.NoError(t, err, "AdminListUsers error") + + if len(response.User) != 1 { + t.Errorf("AdminListUsers returned %d users, want 1", len(response.User)) + } + + if response.User[0].ID != adminUserID { + t.Errorf("AdminListUsers returned user ID %s, want %s", response.User[0].ID, adminUserID) + } + }) + t.Run("ModifyAdminUser", func(t *testing.T) { response, err := client.ModifyAdminUser(ctx, adminUserID, adminUserRole) checks.NoError(t, err, "ModifyAdminUser error")