forked from sashabaranov/go-openai
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for querying billing details
This commit adds support for querying daily costs usage information (as shown on https://platform.openai.com/usage). The implementation uses the public endpoing available at https://api.openai.com/dashboard/billing/usage However, note that this API is not documented in https://platform.openai.com/docs/api-reference and may thus be controversial to include. The API requires a browser session key and will explicitly reject requests made using an API key. (cherry picked from commit 2f53565) (cherry picked from commit 33269e5) (cherry picked from commit 26947c0) (cherry picked from commit 7cf5c6b)
- Loading branch information
Showing
6 changed files
with
211 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
package openai | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"net/http" | ||
"strings" | ||
"time" | ||
) | ||
|
||
const billingUsageSuffix = "/billing/usage" | ||
|
||
type CostLineItemResponse struct { | ||
Name string `json:"name"` | ||
Cost float64 `json:"cost"` // in cents | ||
} | ||
|
||
type DailyCostResponse struct { | ||
TimestampRaw float64 `json:"timestamp"` | ||
LineItems []CostLineItemResponse `json:"line_items"` | ||
|
||
Time time.Time `json:"-"` | ||
} | ||
|
||
type BillingUsageResponse struct { | ||
Object string `json:"object"` | ||
DailyCosts []DailyCostResponse `json:"daily_costs"` | ||
TotalUsage float64 `json:"total_usage"` // in cents | ||
|
||
httpHeader | ||
} | ||
|
||
// currently the OpenAI usage API is not publicly documented and will explictly | ||
// reject requests using an API key authorization. however, it can be utilized | ||
// logging into https://platform.openai.com/usage and retrieving your session | ||
// key from the browser console. session keys have the form 'sess-<keytext>'. | ||
var ( | ||
BillingAPIKeyNotAllowedErrMsg = "Your request to GET /dashboard/billing/usage must be made with a session key (that is, it can only be made from the browser)." //nolint:lll | ||
ErrSessKeyRequired = errors.New("an OpenAI API key cannot be used for this request; a session key is required instead") //nolint:lll | ||
) | ||
|
||
// GetBillingUsage — API call to Get billing usage details. | ||
func (c *Client) GetBillingUsage(ctx context.Context, startDate time.Time, | ||
endDate time.Time) (response BillingUsageResponse, err error) { | ||
startDateArg := fmt.Sprintf("start_date=%v", startDate.Format(time.DateOnly)) | ||
endDateArg := fmt.Sprintf("end_date=%v", endDate.Format(time.DateOnly)) | ||
queryParams := fmt.Sprintf("%v&%v", startDateArg, endDateArg) | ||
urlSuffix := fmt.Sprintf("%v?%v", billingUsageSuffix, queryParams) | ||
|
||
req, err := c.newRequest(ctx, http.MethodGet, c.fullDashboardURL(urlSuffix)) | ||
if err != nil { | ||
return | ||
} | ||
|
||
err = c.sendRequest(req, &response) | ||
if err != nil { | ||
if strings.Contains(err.Error(), BillingAPIKeyNotAllowedErrMsg) { | ||
err = ErrSessKeyRequired | ||
} | ||
return | ||
} | ||
|
||
for idx, d := range response.DailyCosts { | ||
dTime := time.Unix(int64(d.TimestampRaw), 0) | ||
response.DailyCosts[idx].Time = dTime | ||
} | ||
|
||
return | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
package openai_test | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"fmt" | ||
"net/http" | ||
"strings" | ||
"testing" | ||
"time" | ||
|
||
"github.com/sashabaranov/go-openai" | ||
"github.com/sashabaranov/go-openai/internal/test/checks" | ||
) | ||
|
||
const ( | ||
TestTotCost = float64(126.234) | ||
TestEndDate = "2023-11-30" | ||
TestStartDate = "2023-11-01" | ||
TestSessionKey = "sess-whatever" | ||
TestAPIKey = "sk-whatever" | ||
) | ||
|
||
func TestBillingUsageAPIKey(t *testing.T) { | ||
client, server, teardown := setupOpenAITestServerWithAuth(TestAPIKey) | ||
defer teardown() | ||
server.RegisterHandler("/dashboard/billing/usage", handleBillingEndpoint) | ||
|
||
ctx := context.Background() | ||
|
||
endDate, err := time.Parse(time.DateOnly, TestEndDate) | ||
checks.NoError(t, err) | ||
startDate, err := time.Parse(time.DateOnly, TestStartDate) | ||
checks.NoError(t, err) | ||
|
||
_, err = client.GetBillingUsage(ctx, startDate, endDate) | ||
checks.HasError(t, err) | ||
} | ||
|
||
func TestBillingUsageSessKey(t *testing.T) { | ||
client, server, teardown := setupOpenAITestServerWithAuth(TestSessionKey) | ||
defer teardown() | ||
server.RegisterHandler("/dashboard/billing/usage", handleBillingEndpoint) | ||
|
||
ctx := context.Background() | ||
endDate, err := time.Parse(time.DateOnly, TestEndDate) | ||
checks.NoError(t, err) | ||
startDate, err := time.Parse(time.DateOnly, TestStartDate) | ||
checks.NoError(t, err) | ||
|
||
resp, err := client.GetBillingUsage(ctx, startDate, endDate) | ||
checks.NoError(t, err) | ||
|
||
if resp.TotalUsage != TestTotCost { | ||
t.Errorf("expected total cost %v but got %v", TestTotCost, | ||
resp.TotalUsage) | ||
} | ||
for idx, dc := range resp.DailyCosts { | ||
if dc.Time.Compare(startDate) < 0 { | ||
t.Errorf("expected daily cost%v date(%v) before start date %v", idx, | ||
dc.Time, TestStartDate) | ||
} | ||
if dc.Time.Compare(endDate) > 0 { | ||
t.Errorf("expected daily cost%v date(%v) after end date %v", idx, | ||
dc.Time, TestEndDate) | ||
} | ||
} | ||
} | ||
|
||
// handleBillingEndpoint Handles the billing usage endpoint by the test server. | ||
func handleBillingEndpoint(w http.ResponseWriter, r *http.Request) { | ||
if r.Method != "GET" { | ||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) | ||
return | ||
} | ||
if strings.Contains(r.Header.Get("Authorization"), TestAPIKey) { | ||
http.Error(w, openai.BillingAPIKeyNotAllowedErrMsg, http.StatusUnauthorized) | ||
return | ||
} | ||
|
||
var resBytes []byte | ||
|
||
dailyCosts := make([]openai.DailyCostResponse, 0) | ||
|
||
d, _ := time.Parse(time.DateOnly, TestStartDate) | ||
d = d.Add(24 * time.Hour) | ||
dailyCosts = append(dailyCosts, openai.DailyCostResponse{ | ||
TimestampRaw: float64(d.Unix()), | ||
LineItems: []openai.CostLineItemResponse{ | ||
{Name: "GPT-4 Turbo", Cost: 0.12}, | ||
{Name: "Audio models", Cost: 0.24}, | ||
}, | ||
Time: time.Time{}, | ||
}) | ||
d = d.Add(24 * time.Hour) | ||
dailyCosts = append(dailyCosts, openai.DailyCostResponse{ | ||
TimestampRaw: float64(d.Unix()), | ||
LineItems: []openai.CostLineItemResponse{ | ||
{Name: "image models", Cost: 0.56}, | ||
}, | ||
Time: time.Time{}, | ||
}) | ||
res := &openai.BillingUsageResponse{ | ||
Object: "list", | ||
DailyCosts: dailyCosts, | ||
TotalUsage: TestTotCost, | ||
} | ||
|
||
resBytes, err := json.Marshal(res) | ||
if err != nil { | ||
http.Error(w, err.Error(), http.StatusInternalServerError) | ||
return | ||
} | ||
|
||
fmt.Fprintln(w, string(resBytes)) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters