diff --git a/README.md b/README.md index d041b24..1813a77 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ Mind that the same tool/function can be used across multiple APIs, as long as yo ## Resources shared across APIs -- `openai/models` package contains data of all available models across all APIs. You can still just write any model as a literal string if it's not there. When you don't specify a model in a request, a default model appropriate for the API will be chosen. There's pricing and limits data there that can be used in logging. +- `openai/models` package contains data for available models across all APIs. You can still use a model ID literal if it is not listed. When a request omits its model, the API-specific default is used. `models.Data` contains token prices and limits; `models.Data[model].Cost(usage)` calculates Responses API cost, including cached input, cache writes, and long-context rates. - `openai/roles` package contains constants for roles that can be used in messages. Some models may be sensitive to the choice between the older "system" and the newer "developer" roles. - `openai/tools` package contains types for tools/functions that can be used in requests in multiple APIs. You declare a tool/function, add it to the client, and then list its name in the `Functions`/`Tools` field of a request. - `openai/content/input` and `openai/content/output` packages contain all types that can be sent to the API or received from it. Some types can be used for both input and output, such are placed in the output package. Note that there are types that are present in both packages and have the same name, but their implementations differ slightly. @@ -146,6 +146,7 @@ Other exposed types/functions in the `responses` package: - A few more types for request fields. - `Response` wraps the API response and exposes the following: - `Response.ID` field contains the response ID that can be used to chain requests. + - `Response.Usage` contains usage token counts from the API response. When automatic tool handling sends follow-up requests, this usage belongs to the final response. - `Response.()` methods return a slice of outputs of a specific type extracted from the response. For example, `Texts()` returns string of all text outputs, usually just one. - `Response.Outputs` contains all received outputs as `[]output.Any`. `Any` contains parsed `type` field and raw data. - `Response.ParsedOutputs` contains all received outputs fully parsed in an `[]any` slice. The `.Parse()` method for populating it is called automatically before the response is returned so you don't need to call it. @@ -353,6 +354,8 @@ In normal flow, you'll get a sequence of events with types from the `responses/s Some event types have fields than may contain multiple different types of data. Such fields are left as `json.RawMessage` and mostly can be parsed further using types from the `output` package, but this is not done automatically. +Completed SSE and WebSocket response events carry usage when provided. The client logs token counts and calculated cost at `Debug` level for non-streaming responses and completed streams with usage. For manual streaming cost calculation, convert `streaming.ResponseUsage` to `responses.Usage` and pass it to `models.Data[model].Cost(usage)`. + ### WebSocket According to OpenAI, responses with 20+ tool calls can be up to 40% faster over WebSocket. diff --git a/internal/cmd/getmodels/api.go b/internal/cmd/getmodels/api.go deleted file mode 100644 index 3bfb9e6..0000000 --- a/internal/cmd/getmodels/api.go +++ /dev/null @@ -1,83 +0,0 @@ -// Package main / api.go contains types and functions for interacting with models.dev API. -package main - -import ( - "encoding/json" - "fmt" - "io" - "net/http" -) - -const modelsAPI = "https://models.dev/api.json" - -type payload map[providerName]providerData - -type ( - providerName string - modelID string -) - -type providerData struct { - ID providerName `json:"id"` - Env []string `json:"env"` - NPM string `json:"npm"` - Doc string `json:"doc"` - Models map[modelID]modelData `json:"models"` -} - -type modelData struct { - ID modelID `json:"id"` - Name string `json:"name"` - Attachment bool `json:"attachment"` - Reasoning bool `json:"reasoning"` - Temperature bool `json:"temperature"` - ToolCalls bool `json:"tool_calls"` - Knowledge string `json:"knowledge"` // YYYY-MM - ReleaseDate string `json:"release_date"` // YYYY-MM-DD - LastUpdated string `json:"last_updated"` // YYYY-MM-DD - OpenWeights bool `json:"open_weights"` - Modalities struct { - Input []string `json:"input"` - Output []string `json:"output"` - } `json:"modalities"` - Cost struct { - Input float64 `json:"input"` - Output float64 `json:"output"` - CacheRead float64 `json:"cache_read"` - } `json:"cost"` - Limit struct { - Context int `json:"context"` - Output int `json:"output"` - } `json:"limit"` -} - -// fetchOpenAIData retrieves the model metadata for the `openai` provider. -func fetchOpenAIData() (*providerData, error) { - logger.Info("Getting models from models.dev") - resp, err := http.Get(modelsAPI) - if err != nil { - return nil, fmt.Errorf("failed to get models.dev API data: %w", err) - } - defer resp.Body.Close() - logger.Info("Response received", "status", resp.Status) - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read models.dev response: %w", err) - } - - var apiData payload - err = json.Unmarshal(body, &apiData) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal models.dev response: %w", err) - } - logger.Info(fmt.Sprintf("Got providers: %d", len(apiData))) - - openAIData := apiData[providerOpenAI] - logger.Info(fmt.Sprintf("Got %d models for %s", len(openAIData.Models), providerOpenAI)) - if len(openAIData.Models) == 0 { - panic("no models returned for openai") - } - - return &openAIData, nil -} diff --git a/internal/cmd/getmodels/main.go b/internal/cmd/getmodels/main.go deleted file mode 100644 index 7574964..0000000 --- a/internal/cmd/getmodels/main.go +++ /dev/null @@ -1,262 +0,0 @@ -// Command getmodels fetches OpenAI model pricing/limit data from models.dev -// and rewrites models/text.go with the refreshed information. Only the code section -// below the "GENERATED" marker is modified; all hand-written code above it remains -// untouched. -package main - -import ( - "bytes" - "fmt" - "log/slog" - "os" - "os/exec" - "regexp" - "slices" - "strconv" - "strings" - "text/template" -) - -const ( - providerOpenAI = "openai" - modelsFile = "text.go" -) - -const dataBlockFormat = `// CODE BELOW THIS LINE IS GENERATED. ONLY EDIT IF YOU KNOW HOW. - -// Data contains price per 1 token for each model, separately for input and output, and token limits. -// Note that pricing page https://openai.com/pricing lists price per 1k tokens and here it's per 1 token. -// The "" denotes default values. -var Data = map[string]struct { - PriceIn float64 - PriceCachedIn float64 - PriceOut float64 - LimitContext int - LimitOutput int -}{ - // Zeroes in the end of prices are added to align it and make it easier to read. - // Can be read as "0.00000450 = 4.5 micro dollars per token = $4.50 per 1M tokens". - "": {0.00000000, 0.00000000, 0.00000000, 4096, 4096}, - {{- define "modelList"}}{{range .}}{{with .ConstantName}}{{.}}{{else}}"{{.ID}}"{{end}}: { {{- .PriceInStr}}, {{.PriceCachedInStr}}, {{.PriceOutStr}}, {{.LimitContext}}, {{.LimitOutput -}} }, - {{end}}{{end}} - {{template "modelList" .Constants -}} - {{template "modelList" .Literals}} - - // Deprecated or unused models - {{template "modelList" .Deprecated}} -} -` - -var ( - logger = slog.Default() - - // reConstantDefinition matches a line declaring a model constant, e.g. - // GPT3Turbo = "gpt-3.5-turbo" - reConstantDefinition = regexp.MustCompile(`^\s+(\w+)\s+=\s"([-\.\w]+)"$`) - - // reConstantData matches a line assigning pricing data to a previously - // declared constant. - reConstantData = regexp.MustCompile(`^\s+(\w+):\s+\{([\.\d]+),\s*([\.\d]+),\s*([\.\d]+),\s*(\d+),\s*(\d+)\},`) - - // reLiteralData matches a pricing line that uses a model ID literal (no - // constant). - reLiteralData = regexp.MustCompile(`^\s+"([-\.\w]+)":\s+\{([\.\d]+),\s*([\.\d]+),\s*([\.\d]+),\s*(\d+),\s*(\d+)\},`) - - // outputBuilder accumulates the contents of the rewritten models/text.go file. - outputBuilder = strings.Builder{} -) - -// outputData contains model data for the output template. -type outputData struct { - ID modelID - ConstantName string - - IsDeprecated bool - - PriceInStr string - PriceCachedInStr string - PriceOutStr string - LimitContext int - LimitOutput int -} - -// parseModelsFile parses the models/text.go file and returns a list of outputData -// structs, one for each found model. -func parseModelsFile() ([]outputData, error) { - content, err := os.ReadFile(modelsFile) - if err != nil { - return nil, fmt.Errorf("failed to read models file: %w", err) - } - lines := strings.Split(string(content), "\n") - logger.Info(fmt.Sprintf("Got %d lines from models file", len(lines))) - - var data []outputData - endOfConstants := false - isDeprecated := false - generated := false - for _, line := range lines { - if !generated && strings.Contains(line, "GENERATED") { - generated = true - } - if !generated { - outputBuilder.WriteString(line + "\n") - } - - if matches := reConstantDefinition.FindStringSubmatch(line); len(matches) > 0 && !endOfConstants { - // Found a constant declaration (e.g. GPT3Turbo = "gpt-3.5-turbo"). - logger.Info(fmt.Sprintf("Found constant: %s = %s", matches[1], matches[2])) - data = append(data, outputData{ - ConstantName: matches[1], - ID: modelID(matches[2]), - }) - } else if matches := reConstantData.FindStringSubmatch(line); len(matches) > 0 { - // Found pricing data that references a constant. - logger.Info(fmt.Sprintf("Found data for constant %s: %v", matches[1], matches[2:])) - foundIndex := slices.IndexFunc(data, func(data outputData) bool { - return data.ConstantName == matches[1] - }) - if foundIndex == -1 { - logger.Error(fmt.Sprintf("Unknown constant %s", matches[1])) - continue - } - data[foundIndex].PriceInStr = matches[2] - data[foundIndex].PriceCachedInStr = matches[3] - data[foundIndex].PriceOutStr = matches[4] - data[foundIndex].LimitContext, _ = strconv.Atoi(matches[5]) - data[foundIndex].LimitOutput, _ = strconv.Atoi(matches[6]) - } else if matches := reLiteralData.FindStringSubmatch(line); len(matches) > 0 { - // Found pricing data that references a model literal. - logger.Info(fmt.Sprintf("Found data for literal %s: %v", matches[1], matches[2:])) - foundIndex := slices.IndexFunc(data, func(data outputData) bool { - return data.ID == modelID(matches[1]) - }) - if foundIndex == -1 { - data = append(data, outputData{ - ID: modelID(matches[1]), - PriceInStr: matches[2], - PriceCachedInStr: matches[3], - PriceOutStr: matches[4], - IsDeprecated: isDeprecated, - }) - foundIndex = len(data) - 1 - data[foundIndex].LimitContext, _ = strconv.Atoi(matches[5]) - data[foundIndex].LimitOutput, _ = strconv.Atoi(matches[6]) - continue - } - data[foundIndex].PriceInStr = matches[2] - data[foundIndex].PriceCachedInStr = matches[3] - data[foundIndex].PriceOutStr = matches[4] - data[foundIndex].LimitContext, _ = strconv.Atoi(matches[5]) - data[foundIndex].LimitOutput, _ = strconv.Atoi(matches[6]) - } else if strings.Contains(line, "Completion models") { - endOfConstants = true - } else if strings.Contains(line, "// Deprecated or unused models") { - isDeprecated = true - } - } - - return data, nil -} - -// formatWithGoFmt pipes the given Go source through `gofmt` so that the generated -// section follows standard formatting. -func formatWithGoFmt(data string) (string, error) { - cmd := exec.Command("bash", "-c", "cat - | gofmt") - cmd.Stdin = strings.NewReader(data) - var buf bytes.Buffer - cmd.Stdout = &buf - cmd.Stderr = os.Stderr - err := cmd.Run() - if err != nil { - return "", err - } - return buf.String(), nil -} - -func main() { - dataTemplate, err := template.New("data").Parse(dataBlockFormat) - if err != nil { - panic(err) - } - - formattedData, err := parseModelsFile() - if err != nil { - panic(err) - } - - apiData, err := fetchOpenAIData() - if err != nil { - panic(err) - } - - for _, model := range apiData.Models { - foundIndex := slices.IndexFunc(formattedData, func(data outputData) bool { - return data.ID == model.ID - }) - if foundIndex != -1 { - formattedData[foundIndex] = outputData{ - ID: model.ID, - ConstantName: formattedData[foundIndex].ConstantName, - - // prices are formatted as USD/token float64 with 8 decimal places - PriceInStr: fmt.Sprintf("%.8f", model.Cost.Input*1e-6), - PriceCachedInStr: fmt.Sprintf("%.8f", model.Cost.CacheRead*1e-6), - PriceOutStr: fmt.Sprintf("%.8f", model.Cost.Output*1e-6), - - LimitContext: model.Limit.Context, - LimitOutput: model.Limit.Output, - } - continue - } - - formattedData = append(formattedData, outputData{ - ID: model.ID, - ConstantName: "", - - PriceInStr: fmt.Sprintf("%.8f", model.Cost.Input*1e-6), - PriceCachedInStr: fmt.Sprintf("%.8f", model.Cost.CacheRead*1e-6), - PriceOutStr: fmt.Sprintf("%.8f", model.Cost.Output*1e-6), - - LimitContext: model.Limit.Context, - LimitOutput: model.Limit.Output, - }) - } - - groupedData := struct { - Constants []outputData - Literals []outputData - Deprecated []outputData - }{} - for _, data := range formattedData { - switch { - case data.ConstantName != "": - groupedData.Constants = append(groupedData.Constants, data) - case data.IsDeprecated: - groupedData.Deprecated = append(groupedData.Deprecated, data) - default: - groupedData.Literals = append(groupedData.Literals, data) - } - } - - var dataBlock bytes.Buffer - err = dataTemplate.Execute(&dataBlock, groupedData) - if err != nil { - panic(err) - } - logger.Info("Template executed") - - output, err := formatWithGoFmt(dataBlock.String()) - if err != nil { - fmt.Println(dataBlock.String()) - panic(err) - } - logger.Info("Gofmt executed") - - outputBuilder.WriteString(output) - - err = os.WriteFile(modelsFile, []byte(outputBuilder.String()), 0644) - if err != nil { - panic(err) - } - logger.Info("File written, done") -} diff --git a/internal/inchat/api.go b/internal/inchat/api.go index 596b9eb..6f3834e 100644 --- a/internal/inchat/api.go +++ b/internal/inchat/api.go @@ -61,17 +61,24 @@ func (rfs ResponseFormatStr) MarshalJSON() ([]byte, error) { return openai.Marshal(rf) } +// responseUsage contains token usage returned by Chat Completions. +type responseUsage struct { + Prompt int `json:"prompt_tokens"` + Completion int `json:"completion_tokens"` + Total int `json:"total_tokens"` + PromptTokensDetails struct { + CachedTokens int `json:"cached_tokens"` + CacheWriteTokens int `json:"cache_write_tokens"` + } `json:"prompt_tokens_details"` +} + // response is the response body for the Chat Completion API. type response struct { - ID string `json:"id"` - Object string `json:"object"` - Created int `json:"created"` // Unix timestamp - Model string `json:"model"` - Usage struct { - Prompt int `json:"prompt_tokens"` - Completion int `json:"completion_tokens"` - Total int `json:"total_tokens"` - } `json:"usage"` + ID string `json:"id"` + Object string `json:"object"` + Created int `json:"created"` // Unix timestamp + Model string `json:"model"` + Usage responseUsage `json:"usage"` Choices []struct { Message chat.Message `json:"message"` FinishReason string `json:"finish_reason"` // stop/length/content_filter/null @@ -85,6 +92,32 @@ type response struct { } `json:"error"` } +// calculateCost returns the Chat Completions cost and whether its model price is known. +func calculateCost(model string, usage responseUsage) (float64, bool) { + pricing, ok := models.Data[model] + if !ok { + return 0, false + } + + priceIn := pricing.PriceIn + priceCachedIn := pricing.PriceCachedIn + priceCacheWrite := pricing.PriceCacheWrite + priceOut := pricing.PriceOut + if pricing.LongContextThreshold != 0 && usage.Prompt > pricing.LongContextThreshold { + priceIn = pricing.LongContextPriceIn + priceCachedIn = pricing.LongContextPriceCachedIn + priceCacheWrite = pricing.LongContextPriceCacheWrite + priceOut = pricing.LongContextPriceOut + } + + details := usage.PromptTokensDetails + uncachedInput := usage.Prompt - details.CachedTokens - details.CacheWriteTokens + return float64(uncachedInput)*priceIn + + float64(details.CachedTokens)*priceCachedIn + + float64(details.CacheWriteTokens)*priceCacheWrite + + float64(usage.Completion)*priceOut, true +} + // countTokens returns the number of tokens in the request. func countTokens(data chat.Request) int { dup := data @@ -327,12 +360,12 @@ func (c *Client) checkFirst(resp *response) (string, error) { // cost returns the resulting cost of the completed request in USD. // Returns zero if pricing for the model is not known. func (c *Client) cost(resp *response) float64 { - pricing, ok := models.Data[resp.Model] + total, ok := calculateCost(resp.Model, resp.Usage) if !ok { c.Config.Log.Warn(fmt.Sprintf("No pricing for found model '%s'", resp.Model)) return 0 } - return float64(resp.Usage.Prompt)*pricing.PriceIn + float64(resp.Usage.Completion)*pricing.PriceOut + return total } // marshalRequest builds request body including function calls based on registered tools diff --git a/internal/inresponses/client.go b/internal/inresponses/client.go index cafd473..3c734e0 100644 --- a/internal/inresponses/client.go +++ b/internal/inresponses/client.go @@ -180,7 +180,7 @@ func (c *Client) executeRequest(data *responses.Request) (*response, error) { fmt.Sprintf( "Consumed OpenAI Responses tokens: %d + %d = %d ($%f)", res.Usage.InputTokens, res.Usage.OutputTokens, - res.Usage.TotalTokens, c.cost(&res), + res.Usage.TotalTokens, c.cost(res.Model, res.Usage), ), slog.Any("responseID", res.ID), slog.Any("model", res.Model), @@ -228,17 +228,7 @@ type response struct { Truncation string `json:"truncation"` // Usage Information - Usage struct { - InputTokens int `json:"input_tokens"` - InputTokensDetails struct { - CachedTokens int `json:"cached_tokens"` - } `json:"input_tokens_details"` - OutputTokens int `json:"output_tokens"` - OutputTokensDetails struct { - ReasoningTokens int `json:"reasoning_tokens"` - } `json:"output_tokens_details"` - TotalTokens int `json:"total_tokens"` - } `json:"usage"` + Usage responses.Usage `json:"usage"` // Other Properties User string `json:"user"` @@ -263,6 +253,7 @@ func (data *response) checkResponseData() (*responses.Response, error) { resp := &responses.Response{ ID: data.ID, Outputs: data.Output, + Usage: data.Usage, } err := resp.Parse() if err != nil { @@ -299,17 +290,34 @@ func (data *response) checkResponseData() (*responses.Response, error) { // cost returns the resulting cost of the completed request in USD. // Returns zero if pricing for the model is not known. -func (c *Client) cost(resp *response) float64 { - pricing, ok := models.Data[resp.Model] +func (c *Client) cost(model string, usage responses.Usage) float64 { + pricing, ok := models.Data[model] if !ok { - c.Log.Warn(fmt.Sprintf("No pricing for found model '%s'", resp.Model)) + c.Log.Warn(fmt.Sprintf("No pricing for found model '%s'", model)) return 0 } - total := 0.0 - total += float64(resp.Usage.InputTokens-resp.Usage.InputTokensDetails.CachedTokens) * pricing.PriceIn - total += float64(resp.Usage.InputTokensDetails.CachedTokens) * pricing.PriceCachedIn - total += float64(resp.Usage.OutputTokens) * pricing.PriceOut - return total + return pricing.Cost(usage) +} + +// logStreamingCost logs token usage and cost from a completed streaming response. +func (c *Client) logStreamingCost(event any) { + completed, ok := event.(streaming.ResponseCompleted) + if !ok || completed.Response.Usage == nil { + return + } + + resp := completed.Response + usage := responses.Usage(*resp.Usage) + c.Log.Debug( + fmt.Sprintf( + "Consumed OpenAI Responses tokens: %d + %d = %d ($%f)", + usage.InputTokens, usage.OutputTokens, + usage.TotalTokens, c.cost(resp.Model, usage), + ), + slog.Any("responseID", resp.ID), + slog.Any("model", resp.Model), + slog.Any("metadata", resp.Metadata), + ) } // executableFunctionCall is an intermediate representation of a function call that can be executed. @@ -348,6 +356,7 @@ func newSendContext() *sendContext { func (c *Client) Send(req *responses.Request) (*responses.Response, error) { return c.send(req, newSendContext()) } + func (c *Client) send(req *responses.Request, sc *sendContext) (*responses.Response, error) { respData, err := c.executeRequest(req) if err != nil { @@ -582,6 +591,7 @@ func (c *Client) send(req *responses.Request, sc *sendContext) (*responses.Respo resp.Outputs = combinedOutputs resp.ParsedOutputs = combinedParsedOutputs resp.ID = followupResp.ID + resp.Usage = followupResp.Usage return resp, nil @@ -825,6 +835,7 @@ func (c *Client) streamEvents(ctx context.Context, data *responses.Request) (str src.finish(fmt.Errorf("failed to unmarshal event data: %w", err)) return } + c.logStreamingCost(event) select { case src.events <- event: diff --git a/internal/inresponses/ws.go b/internal/inresponses/ws.go index b4539f0..85308ed 100644 --- a/internal/inresponses/ws.go +++ b/internal/inresponses/ws.go @@ -182,6 +182,7 @@ func (w *wsClient) readLoop() { return } + w.client.logStreamingCost(event) w.pushEvent(event) } } diff --git a/models/text.go b/models/text.go index 8c72408..1e1ad1e 100644 --- a/models/text.go +++ b/models/text.go @@ -1,6 +1,8 @@ // Package models contains constants and pricing data for all OpenAI models. package models +import "github.com/unkn0wncode/openai/responses" + // Constant names are derived from the model ID: // // GPT41 = "gpt-4.1" // collapse dotted versions @@ -155,159 +157,219 @@ const ( TextModerationStable = "text-moderation-stable" ) -//go:generate go run ../internal/cmd/getmodels +// pricing contains token prices and limits for one model. +type pricing struct { + PriceIn float64 + PriceCachedIn float64 + PriceCacheWrite float64 + PriceOut float64 + LongContextThreshold int + LongContextPriceIn float64 + LongContextPriceCachedIn float64 + LongContextPriceCacheWrite float64 + LongContextPriceOut float64 + LimitContext int + LimitOutput int +} + +// newModelData returns pricing data whose cache-write price equals its input price. +func newModelData(priceIn, priceCachedIn, priceOut float64, limitContext, limitOutput int) pricing { + return pricing{ + PriceIn: priceIn, + PriceCachedIn: priceCachedIn, + PriceCacheWrite: priceIn, + PriceOut: priceOut, + LimitContext: limitContext, + LimitOutput: limitOutput, + } +} + +// newCacheWriteModelData returns pricing data with a distinct cache-write price. +func newCacheWriteModelData( + priceIn, priceCachedIn, priceCacheWrite, priceOut float64, + limitContext, limitOutput int, +) pricing { + data := newModelData(priceIn, priceCachedIn, priceOut, limitContext, limitOutput) + data.PriceCacheWrite = priceCacheWrite + return data +} + +// newTieredModelData returns pricing data with separate long-context prices. +func newTieredModelData( + priceIn, priceCachedIn, priceCacheWrite, priceOut float64, + longContextThreshold int, + longContextPriceIn, longContextPriceCachedIn, longContextPriceCacheWrite, longContextPriceOut float64, + limitContext, limitOutput int, +) pricing { + data := newCacheWriteModelData(priceIn, priceCachedIn, priceCacheWrite, priceOut, limitContext, limitOutput) + data.LongContextThreshold = longContextThreshold + data.LongContextPriceIn = longContextPriceIn + data.LongContextPriceCachedIn = longContextPriceCachedIn + data.LongContextPriceCacheWrite = longContextPriceCacheWrite + data.LongContextPriceOut = longContextPriceOut + return data +} -// CODE BELOW THIS LINE IS GENERATED. ONLY EDIT IF YOU KNOW HOW. +// Cost returns the Responses API request cost in USD. +func (data pricing) Cost(usage responses.Usage) float64 { + priceIn := data.PriceIn + priceCachedIn := data.PriceCachedIn + priceCacheWrite := data.PriceCacheWrite + priceOut := data.PriceOut + if data.LongContextThreshold != 0 && usage.InputTokens > data.LongContextThreshold { + priceIn = data.LongContextPriceIn + priceCachedIn = data.LongContextPriceCachedIn + priceCacheWrite = data.LongContextPriceCacheWrite + priceOut = data.LongContextPriceOut + } + + details := usage.InputTokensDetails + uncachedInput := usage.InputTokens - details.CachedTokens - details.CacheWriteTokens + return float64(uncachedInput)*priceIn + + float64(details.CachedTokens)*priceCachedIn + + float64(details.CacheWriteTokens)*priceCacheWrite + + float64(usage.OutputTokens)*priceOut +} -// Data contains price per 1 token for each model, separately for input and output, and token limits. +// Data contains token prices and limits for each model. // Note that pricing page https://openai.com/pricing lists price per 1M tokens and here it's per 1 token. // The "" denotes default values. -var Data = map[string]struct { - PriceIn float64 - PriceCachedIn float64 - PriceOut float64 - LimitContext int - LimitOutput int -}{ +var Data = map[string]pricing{ // Zeroes in the end of prices are added to align it and make it easier to read. // Can be read as "0.00000450 = 4.5 micro dollars per token = $4.50 per 1M tokens". - "": {0.00000000, 0.00000000, 0.00000000, 4096, 4096}, + "": newModelData(0.00000000, 0.00000000, 0.00000000, 4096, 4096), // Chat aliases - ChatLatest: {0.00000500, 0.00000050, 0.00003000, 400000, 128000}, + ChatLatest: newModelData(0.00000500, 0.00000050, 0.00003000, 400000, 128000), // GPT-3.5 family - GPT35Turbo: {0.00000050, 0.00000000, 0.00000150, 16385, 4096}, - GPT35Turbo0125: {0.00000050, 0.00000050, 0.00000150, 16348, 4096}, - GPT35Turbo1106: {0.00000100, 0.00000100, 0.00000200, 16348, 4096}, - GPT35TurboInstruct: {0.00000150, 0.00000000, 0.00000200, 16348, 4096}, - GPT35TurboInstruct0914: {0.00000150, 0.00000000, 0.00000200, 16348, 4096}, + GPT35Turbo: newModelData(0.00000050, 0.00000000, 0.00000150, 16385, 4096), + GPT35Turbo0125: newModelData(0.00000050, 0.00000050, 0.00000150, 16348, 4096), + GPT35Turbo1106: newModelData(0.00000100, 0.00000100, 0.00000200, 16348, 4096), + GPT35TurboInstruct: newModelData(0.00000150, 0.00000000, 0.00000200, 16348, 4096), + GPT35TurboInstruct0914: newModelData(0.00000150, 0.00000000, 0.00000200, 16348, 4096), // GPT-4 family - "gpt-4": {0.00003000, 0.00000000, 0.00006000, 8192, 8192}, - GPT4Turbo: {0.00001000, 0.00000000, 0.00003000, 128000, 4096}, - GPT4Turbo20240409: {0.00001000, 0.00001000, 0.00003000, 128000, 4096}, - "gpt-4-0613": {0.00003000, 0.00003000, 0.00006000, 8192, 8192}, + "gpt-4": newModelData(0.00003000, 0.00000000, 0.00006000, 8192, 8192), + GPT4Turbo: newModelData(0.00001000, 0.00000000, 0.00003000, 128000, 4096), + GPT4Turbo20240409: newModelData(0.00001000, 0.00001000, 0.00003000, 128000, 4096), + "gpt-4-0613": newModelData(0.00003000, 0.00003000, 0.00006000, 8192, 8192), // GPT-4.1 family - GPT41: {0.00000200, 0.00000050, 0.00000800, 1047576, 32768}, - GPT4120250414: {0.00000200, 0.00000050, 0.00000800, 1000000, 32768}, - GPT41Mini: {0.00000040, 0.00000010, 0.00000160, 1047576, 32768}, - GPT41Mini20250414: {0.00000040, 0.00000010, 0.00000160, 1000000, 32768}, - GPT41Nano: {0.00000010, 0.00000003, 0.00000040, 1047576, 32768}, - GPT41Nano20250414: {0.00000010, 0.00000003, 0.00000040, 1000000, 32768}, + GPT41: newModelData(0.00000200, 0.00000050, 0.00000800, 1047576, 32768), + GPT4120250414: newModelData(0.00000200, 0.00000050, 0.00000800, 1000000, 32768), + GPT41Mini: newModelData(0.00000040, 0.00000010, 0.00000160, 1047576, 32768), + GPT41Mini20250414: newModelData(0.00000040, 0.00000010, 0.00000160, 1000000, 32768), + GPT41Nano: newModelData(0.00000010, 0.00000003, 0.00000040, 1047576, 32768), + GPT41Nano20250414: newModelData(0.00000010, 0.00000003, 0.00000040, 1000000, 32768), // GPT-4o family - GPT4o: {0.00000250, 0.00000125, 0.00001000, 128000, 16384}, - GPT4o20240513: {0.00000500, 0.00000000, 0.00001500, 128000, 4096}, - GPT4o20240806: {0.00000250, 0.00000125, 0.00001000, 128000, 16384}, - GPT4o20241120: {0.00000250, 0.00000125, 0.00001000, 128000, 16384}, - GPT4oMini: {0.00000015, 0.00000008, 0.00000060, 128000, 16384}, - GPT4oMini20240718: {0.00000015, 0.00000008, 0.00000060, 128000, 16348}, - GPT4oSearchPreview: {0.00000250, 0.00000000, 0.00001000, 128000, 16384}, - GPT4oSearchPreview20250311: {0.00000250, 0.00000000, 0.00001000, 128000, 16384}, - GPT4oMiniSearchPreview: {0.00000015, 0.00000000, 0.00000060, 128000, 16384}, - GPT4oMiniSearchPreview20250311: {0.00000015, 0.00000000, 0.00000060, 128000, 16384}, - GPT4oTranscribe: {0.00000250, 0.00000000, 0.00001000, 128000, 16384}, - GPT4oTranscribeDiarize: {0.00000250, 0.00000000, 0.00001000, 128000, 16384}, - GPT4oMiniTranscribe: {0.00000125, 0.00000000, 0.00000500, 128000, 16384}, - GPT4oMiniTranscribe20250320: {0.00000125, 0.00000000, 0.00000500, 128000, 16384}, - GPT4oMiniTranscribe20251215: {0.00000125, 0.00000000, 0.00000500, 128000, 16384}, - GPT4oMiniTTS: {0.00000060, 0.00000000, 0.00001200, 128000, 16384}, + GPT4o: newModelData(0.00000250, 0.00000125, 0.00001000, 128000, 16384), + GPT4o20240513: newModelData(0.00000500, 0.00000000, 0.00001500, 128000, 4096), + GPT4o20240806: newModelData(0.00000250, 0.00000125, 0.00001000, 128000, 16384), + GPT4o20241120: newModelData(0.00000250, 0.00000125, 0.00001000, 128000, 16384), + GPT4oMini: newModelData(0.00000015, 0.00000008, 0.00000060, 128000, 16384), + GPT4oMini20240718: newModelData(0.00000015, 0.00000008, 0.00000060, 128000, 16348), + GPT4oSearchPreview: newModelData(0.00000250, 0.00000000, 0.00001000, 128000, 16384), + GPT4oSearchPreview20250311: newModelData(0.00000250, 0.00000000, 0.00001000, 128000, 16384), + GPT4oMiniSearchPreview: newModelData(0.00000015, 0.00000000, 0.00000060, 128000, 16384), + GPT4oMiniSearchPreview20250311: newModelData(0.00000015, 0.00000000, 0.00000060, 128000, 16384), + GPT4oTranscribe: newModelData(0.00000250, 0.00000000, 0.00001000, 128000, 16384), + GPT4oTranscribeDiarize: newModelData(0.00000250, 0.00000000, 0.00001000, 128000, 16384), + GPT4oMiniTranscribe: newModelData(0.00000125, 0.00000000, 0.00000500, 128000, 16384), + GPT4oMiniTranscribe20250320: newModelData(0.00000125, 0.00000000, 0.00000500, 128000, 16384), + GPT4oMiniTranscribe20251215: newModelData(0.00000125, 0.00000000, 0.00000500, 128000, 16384), + GPT4oMiniTTS: newModelData(0.00000060, 0.00000000, 0.00001200, 128000, 16384), // GPT-5 family - GPT5: {0.00000125, 0.00000013, 0.00001000, 400000, 128000}, - GPT520250807: {0.00000125, 0.00000013, 0.00001000, 400000, 128000}, - GPT5Mini: {0.00000025, 0.00000003, 0.00000200, 400000, 128000}, - GPT5Mini20250807: {0.00000025, 0.00000003, 0.00000200, 400000, 128000}, - GPT5Nano: {0.00000005, 0.00000001, 0.00000040, 400000, 128000}, - GPT5Nano20250807: {0.00000005, 0.00000001, 0.00000040, 400000, 128000}, - GPT5ChatLatest: {0.00000125, 0.00000013, 0.00001000, 400000, 128000}, - GPT5Codex: {0.00000125, 0.00000013, 0.00001000, 400000, 128000}, - GPT5Pro: {0.00001500, 0.00000000, 0.00012000, 400000, 272000}, - GPT5Pro20251006: {0.00001500, 0.00000000, 0.00012000, 400000, 272000}, - GPT5SearchAPI: {0.00000125, 0.00000013, 0.00001000, 400000, 128000}, - GPT5SearchAPI20251014: {0.00000125, 0.00000013, 0.00001000, 400000, 128000}, - GPT51: {0.00000125, 0.00000013, 0.00001000, 400000, 128000}, - GPT5120251113: {0.00000125, 0.00000013, 0.00001000, 400000, 128000}, - GPT51ChatLatest: {0.00000125, 0.00000013, 0.00001000, 400000, 128000}, - GPT51Codex: {0.00000125, 0.00000013, 0.00001000, 400000, 128000}, - GPT51CodexMax: {0.00000125, 0.00000013, 0.00001000, 400000, 128000}, - GPT51CodexMini: {0.00000025, 0.00000003, 0.00000200, 400000, 128000}, - GPT52: {0.00000175, 0.00000018, 0.00001400, 400000, 128000}, - GPT5220251211: {0.00000175, 0.00000018, 0.00001400, 400000, 128000}, - GPT52ChatLatest: {0.00000175, 0.00000018, 0.00001400, 128000, 16384}, - GPT52Pro: {0.00002100, 0.00000000, 0.00016800, 400000, 128000}, - GPT52Pro20251211: {0.00002100, 0.00000000, 0.00016800, 400000, 128000}, - GPT52Codex: {0.00000175, 0.00000018, 0.00001400, 400000, 128000}, - GPT53Codex: {0.00000175, 0.00000018, 0.00001400, 400000, 128000}, - GPT53ChatLatest: {0.00000175, 0.00000018, 0.00001400, 128000, 16384}, - GPT54: {0.00000250, 0.00000025, 0.00001500, 1050000, 128000}, - GPT5420260305: {0.00000250, 0.00000025, 0.00001500, 1050000, 128000}, - GPT54Mini: {0.00000075, 0.00000008, 0.00000450, 400000, 128000}, - GPT54Mini20260317: {0.00000075, 0.00000008, 0.00000450, 400000, 128000}, - GPT54Nano: {0.00000020, 0.00000002, 0.00000125, 400000, 128000}, - GPT54Nano20260317: {0.00000020, 0.00000002, 0.00000125, 400000, 128000}, - GPT54Pro: {0.00003000, 0.00000000, 0.00018000, 1050000, 128000}, - GPT54Pro20260305: {0.00003000, 0.00000000, 0.00018000, 1050000, 128000}, - // GPT-5.5 prices are the standard short-context rates; official pricing - // applies higher rates to sessions with more than 272K input tokens. - GPT55: {0.00000500, 0.00000050, 0.00003000, 1050000, 128000}, - GPT5520260423: {0.00000500, 0.00000050, 0.00003000, 1050000, 128000}, - GPT55Pro: {0.00003000, 0.00000000, 0.00018000, 1050000, 128000}, - GPT55Pro20260423: {0.00003000, 0.00000000, 0.00018000, 1050000, 128000}, - // GPT-5.6 prices are the standard short-context rates; official pricing - // applies higher rates to sessions with more than 272K input tokens. - GPT56Sol: {0.00000500, 0.00000050, 0.00003000, 1050000, 128000}, - GPT56Terra: {0.00000250, 0.00000025, 0.00001500, 1050000, 128000}, - GPT56Luna: {0.00000100, 0.00000010, 0.00000600, 1050000, 128000}, + GPT5: newModelData(0.00000125, 0.00000013, 0.00001000, 400000, 128000), + GPT520250807: newModelData(0.00000125, 0.00000013, 0.00001000, 400000, 128000), + GPT5Mini: newModelData(0.00000025, 0.00000003, 0.00000200, 400000, 128000), + GPT5Mini20250807: newModelData(0.00000025, 0.00000003, 0.00000200, 400000, 128000), + GPT5Nano: newModelData(0.00000005, 0.00000001, 0.00000040, 400000, 128000), + GPT5Nano20250807: newModelData(0.00000005, 0.00000001, 0.00000040, 400000, 128000), + GPT5ChatLatest: newModelData(0.00000125, 0.00000013, 0.00001000, 400000, 128000), + GPT5Codex: newModelData(0.00000125, 0.00000013, 0.00001000, 400000, 128000), + GPT5Pro: newModelData(0.00001500, 0.00000000, 0.00012000, 400000, 272000), + GPT5Pro20251006: newModelData(0.00001500, 0.00000000, 0.00012000, 400000, 272000), + GPT5SearchAPI: newModelData(0.00000125, 0.00000013, 0.00001000, 400000, 128000), + GPT5SearchAPI20251014: newModelData(0.00000125, 0.00000013, 0.00001000, 400000, 128000), + GPT51: newModelData(0.00000125, 0.00000013, 0.00001000, 400000, 128000), + GPT5120251113: newModelData(0.00000125, 0.00000013, 0.00001000, 400000, 128000), + GPT51ChatLatest: newModelData(0.00000125, 0.00000013, 0.00001000, 400000, 128000), + GPT51Codex: newModelData(0.00000125, 0.00000013, 0.00001000, 400000, 128000), + GPT51CodexMax: newModelData(0.00000125, 0.00000013, 0.00001000, 400000, 128000), + GPT51CodexMini: newModelData(0.00000025, 0.00000003, 0.00000200, 400000, 128000), + GPT52: newModelData(0.00000175, 0.00000018, 0.00001400, 400000, 128000), + GPT5220251211: newModelData(0.00000175, 0.00000018, 0.00001400, 400000, 128000), + GPT52ChatLatest: newModelData(0.00000175, 0.00000018, 0.00001400, 128000, 16384), + GPT52Pro: newModelData(0.00002100, 0.00000000, 0.00016800, 400000, 128000), + GPT52Pro20251211: newModelData(0.00002100, 0.00000000, 0.00016800, 400000, 128000), + GPT52Codex: newModelData(0.00000175, 0.00000018, 0.00001400, 400000, 128000), + GPT53Codex: newModelData(0.00000175, 0.00000018, 0.00001400, 400000, 128000), + GPT53ChatLatest: newModelData(0.00000175, 0.00000018, 0.00001400, 128000, 16384), + GPT54: newTieredModelData(0.00000250, 0.00000025, 0.00000250, 0.00001500, 272000, 0.00000500, 0.00000050, 0.00000500, 0.00002250, 1050000, 128000), + GPT5420260305: newTieredModelData(0.00000250, 0.00000025, 0.00000250, 0.00001500, 272000, 0.00000500, 0.00000050, 0.00000500, 0.00002250, 1050000, 128000), + GPT54Mini: newModelData(0.00000075, 0.00000008, 0.00000450, 400000, 128000), + GPT54Mini20260317: newModelData(0.00000075, 0.00000008, 0.00000450, 400000, 128000), + GPT54Nano: newModelData(0.00000020, 0.00000002, 0.00000125, 400000, 128000), + GPT54Nano20260317: newModelData(0.00000020, 0.00000002, 0.00000125, 400000, 128000), + GPT54Pro: newTieredModelData(0.00003000, 0.00000000, 0.00003000, 0.00018000, 272000, 0.00006000, 0.00000000, 0.00006000, 0.00027000, 1050000, 128000), + GPT54Pro20260305: newTieredModelData(0.00003000, 0.00000000, 0.00003000, 0.00018000, 272000, 0.00006000, 0.00000000, 0.00006000, 0.00027000, 1050000, 128000), + GPT55: newTieredModelData(0.00000500, 0.00000050, 0.00000500, 0.00003000, 272000, 0.00001000, 0.00000100, 0.00001000, 0.00004500, 1050000, 128000), + GPT5520260423: newTieredModelData(0.00000500, 0.00000050, 0.00000500, 0.00003000, 272000, 0.00001000, 0.00000100, 0.00001000, 0.00004500, 1050000, 128000), + GPT55Pro: newTieredModelData(0.00003000, 0.00000000, 0.00003000, 0.00018000, 272000, 0.00006000, 0.00000000, 0.00006000, 0.00027000, 1050000, 128000), + GPT55Pro20260423: newTieredModelData(0.00003000, 0.00000000, 0.00003000, 0.00018000, 272000, 0.00006000, 0.00000000, 0.00006000, 0.00027000, 1050000, 128000), + GPT56Sol: newTieredModelData(0.00000500, 0.00000050, 0.00000625, 0.00003000, 272000, 0.00001000, 0.00000100, 0.00001250, 0.00004500, 1050000, 128000), + GPT56Terra: newTieredModelData(0.00000250, 0.00000025, 0.000003125, 0.00001500, 272000, 0.00000500, 0.00000050, 0.00000625, 0.00002250, 1050000, 128000), + GPT56Luna: newTieredModelData(0.00000100, 0.00000010, 0.00000125, 0.00000600, 272000, 0.00000200, 0.00000020, 0.00000250, 0.00000900, 1050000, 128000), // Multimodal realtime & audio - GPTRealtime: {0.00000400, 0.00000040, 0.00001600, 128000, 16384}, - GPTRealtime15: {0.00000400, 0.00000040, 0.00001600, 128000, 16384}, - GPTRealtime2: {0.00000400, 0.00000040, 0.00002400, 128000, 32000}, - GPTRealtime21: {0.00000400, 0.00000040, 0.00002400, 128000, 32000}, - GPTRealtime21Mini: {0.00000060, 0.00000006, 0.00000240, 0, 0}, // official docs do not provide context/output limits - GPTRealtime20250828: {0.00000400, 0.00000040, 0.00001600, 128000, 16384}, - GPTRealtimeMini: {0.00000060, 0.00000006, 0.00000240, 128000, 16384}, - GPTRealtimeMini20251006: {0.00000060, 0.00000006, 0.00000240, 128000, 16384}, - GPTRealtimeMini20251215: {0.00000060, 0.00000006, 0.00000240, 128000, 16384}, - GPTAudio: {0.00000250, 0.00000000, 0.00001000, 128000, 16384}, - GPTAudio15: {0.00000250, 0.00000000, 0.00001000, 128000, 16384}, - GPTAudio20250828: {0.00000250, 0.00000000, 0.00001000, 128000, 16384}, - GPTAudioMini: {0.00000060, 0.00000000, 0.00000240, 128000, 16384}, - GPTAudioMini20251006: {0.00000060, 0.00000000, 0.00000240, 128000, 16384}, - GPTAudioMini20251215: {0.00000060, 0.00000000, 0.00000240, 128000, 16384}, + GPTRealtime: newModelData(0.00000400, 0.00000040, 0.00001600, 128000, 16384), + GPTRealtime15: newModelData(0.00000400, 0.00000040, 0.00001600, 128000, 16384), + GPTRealtime2: newModelData(0.00000400, 0.00000040, 0.00002400, 128000, 32000), + GPTRealtime21: newModelData(0.00000400, 0.00000040, 0.00002400, 128000, 32000), + GPTRealtime21Mini: newModelData(0.00000060, 0.00000006, 0.00000240, 0, 0), // official docs do not provide context/output limits + GPTRealtime20250828: newModelData(0.00000400, 0.00000040, 0.00001600, 128000, 16384), + GPTRealtimeMini: newModelData(0.00000060, 0.00000006, 0.00000240, 128000, 16384), + GPTRealtimeMini20251006: newModelData(0.00000060, 0.00000006, 0.00000240, 128000, 16384), + GPTRealtimeMini20251215: newModelData(0.00000060, 0.00000006, 0.00000240, 128000, 16384), + GPTAudio: newModelData(0.00000250, 0.00000000, 0.00001000, 128000, 16384), + GPTAudio15: newModelData(0.00000250, 0.00000000, 0.00001000, 128000, 16384), + GPTAudio20250828: newModelData(0.00000250, 0.00000000, 0.00001000, 128000, 16384), + GPTAudioMini: newModelData(0.00000060, 0.00000000, 0.00000240, 128000, 16384), + GPTAudioMini20251006: newModelData(0.00000060, 0.00000000, 0.00000240, 128000, 16384), + GPTAudioMini20251215: newModelData(0.00000060, 0.00000000, 0.00000240, 128000, 16384), // O-series - O1: {0.00001500, 0.00000750, 0.00006000, 200000, 100000}, - O120241217: {0.00001500, 0.00000750, 0.00006000, 200000, 100000}, - O1Pro: {0.00015000, 0.00000000, 0.00060000, 200000, 100000}, - O1Pro20250319: {0.00015000, 0.00000000, 0.00060000, 200000, 100000}, - O3: {0.00000200, 0.00000050, 0.00000800, 200000, 100000}, - O320250416: {0.00000200, 0.00000050, 0.00000800, 200000, 100000}, - O3Mini: {0.00000110, 0.00000055, 0.00000440, 200000, 100000}, - O3Mini20250131: {0.00000110, 0.00000055, 0.00000440, 200000, 100000}, - O3Pro: {0.00002000, 0.00000000, 0.00008000, 200000, 100000}, - O3Pro20250610: {0.00002000, 0.00000000, 0.00008000, 200000, 100000}, - O3DeepResearch: {0.00001000, 0.00000250, 0.00004000, 200000, 100000}, - O3DeepResearch20250626: {0.00001000, 0.00000250, 0.00004000, 200000, 100000}, - O4Mini: {0.00000110, 0.00000028, 0.00000440, 200000, 100000}, - O4Mini20250416: {0.00000110, 0.00000028, 0.00000440, 200000, 100000}, - O4MiniDeepResearch: {0.00000200, 0.00000050, 0.00000800, 200000, 100000}, - O4MiniDeepResearch20250626: {0.00000200, 0.00000050, 0.00000800, 200000, 100000}, + O1: newModelData(0.00001500, 0.00000750, 0.00006000, 200000, 100000), + O120241217: newModelData(0.00001500, 0.00000750, 0.00006000, 200000, 100000), + O1Pro: newModelData(0.00015000, 0.00000000, 0.00060000, 200000, 100000), + O1Pro20250319: newModelData(0.00015000, 0.00000000, 0.00060000, 200000, 100000), + O3: newModelData(0.00000200, 0.00000050, 0.00000800, 200000, 100000), + O320250416: newModelData(0.00000200, 0.00000050, 0.00000800, 200000, 100000), + O3Mini: newModelData(0.00000110, 0.00000055, 0.00000440, 200000, 100000), + O3Mini20250131: newModelData(0.00000110, 0.00000055, 0.00000440, 200000, 100000), + O3Pro: newModelData(0.00002000, 0.00000000, 0.00008000, 200000, 100000), + O3Pro20250610: newModelData(0.00002000, 0.00000000, 0.00008000, 200000, 100000), + O3DeepResearch: newModelData(0.00001000, 0.00000250, 0.00004000, 200000, 100000), + O3DeepResearch20250626: newModelData(0.00001000, 0.00000250, 0.00004000, 200000, 100000), + O4Mini: newModelData(0.00000110, 0.00000028, 0.00000440, 200000, 100000), + O4Mini20250416: newModelData(0.00000110, 0.00000028, 0.00000440, 200000, 100000), + O4MiniDeepResearch: newModelData(0.00000200, 0.00000050, 0.00000800, 200000, 100000), + O4MiniDeepResearch20250626: newModelData(0.00000200, 0.00000050, 0.00000800, 200000, 100000), // Tooling & moderation - ComputerUsePreview: {0.00000300, 0.00000000, 0.00001200, 128000, 16384}, - ComputerUsePreview20250311: {0.00000300, 0.00000000, 0.00001200, 128000, 16384}, - OmniModeration: {0.00000000, 0.00000000, 0.00000000, 8192, 4096}, - OmniModeration20240926: {0.00000000, 0.00000000, 0.00000000, 8192, 4096}, + ComputerUsePreview: newModelData(0.00000300, 0.00000000, 0.00001200, 128000, 16384), + ComputerUsePreview20250311: newModelData(0.00000300, 0.00000000, 0.00001200, 128000, 16384), + OmniModeration: newModelData(0.00000000, 0.00000000, 0.00000000, 8192, 4096), + OmniModeration20240926: newModelData(0.00000000, 0.00000000, 0.00000000, 8192, 4096), // Completion models - Davinci002: {0.00000200, 0.00000000, 0.00000200, 16384, 4096}, - Babbage002: {0.00000040, 0.00000000, 0.00000040, 16384, 4096}, + Davinci002: newModelData(0.00000200, 0.00000000, 0.00000200, 16384, 4096), + Babbage002: newModelData(0.00000040, 0.00000000, 0.00000040, 16384, 4096), // Embedding models - TextEmbedding3Large: {0.00000013, 0.00000000, 0.00000000, 8191, 3072}, - TextEmbedding3Small: {0.00000002, 0.00000000, 0.00000000, 8191, 1536}, + TextEmbedding3Large: newModelData(0.00000013, 0.00000000, 0.00000000, 8191, 3072), + TextEmbedding3Small: newModelData(0.00000002, 0.00000000, 0.00000000, 8191, 1536), } diff --git a/responses/service.go b/responses/service.go index 1a8a52a..b557a58 100644 --- a/responses/service.go +++ b/responses/service.go @@ -180,6 +180,21 @@ type Response struct { ID string Outputs []output.Any ParsedOutputs []any + Usage Usage +} + +// Usage contains token usage for a response. +type Usage struct { + InputTokens int `json:"input_tokens"` + InputTokensDetails struct { + CachedTokens int `json:"cached_tokens"` + CacheWriteTokens int `json:"cache_write_tokens"` + } `json:"input_tokens_details"` + OutputTokens int `json:"output_tokens"` + OutputTokensDetails struct { + ReasoningTokens int `json:"reasoning_tokens"` + } `json:"output_tokens_details"` + TotalTokens int `json:"total_tokens"` } // Parse parses the []output.Any and places the parsed objects in ParsedOutputs. diff --git a/responses/streaming/types.go b/responses/streaming/types.go index 87d7102..e62a19f 100644 --- a/responses/streaming/types.go +++ b/responses/streaming/types.go @@ -199,6 +199,20 @@ type ( Response Response `json:"response"` } + // ResponseUsage contains token usage for a response. + ResponseUsage struct { + InputTokens int `json:"input_tokens"` + InputTokensDetails struct { + CachedTokens int `json:"cached_tokens"` + CacheWriteTokens int `json:"cache_write_tokens"` + } `json:"input_tokens_details"` + OutputTokens int `json:"output_tokens"` + OutputTokensDetails struct { + ReasoningTokens int `json:"reasoning_tokens"` + } `json:"output_tokens_details"` + TotalTokens int `json:"total_tokens"` + } + // Response represents a response object payload in streaming events. Response struct { ID string `json:"id"` @@ -237,22 +251,11 @@ type ( Strict *bool `json:"strict"` } `json:"format"` } `json:"text"` - ToolChoice json.RawMessage `json:"tool_choice"` // string, ToolChoiceMode, HostedTool, FunctionTool, or MCPTool - Tools json.RawMessage `json:"tools"` // array of Tool - TopP *float64 `json:"top_p"` - Truncation *string `json:"truncation"` // "auto", "disabled" (default) - Usage *struct { - InputTokens int `json:"input_tokens"` - InputTokensDetails struct { - CachedTokens int `json:"cached_tokens"` - PromptTokens int `json:"prompt_tokens"` - } `json:"input_tokens_details"` - OutputTokens int `json:"output_tokens"` - OutputTokensDetails struct { - ReasoningTokens int `json:"reasoning_tokens"` - } `json:"output_tokens_details"` - TotalTokens int `json:"total_tokens"` - } `json:"usage"` + ToolChoice json.RawMessage `json:"tool_choice"` // string, ToolChoiceMode, HostedTool, FunctionTool, or MCPTool + Tools json.RawMessage `json:"tools"` // array of Tool + TopP *float64 `json:"top_p"` + Truncation *string `json:"truncation"` // "auto", "disabled" (default) + Usage *ResponseUsage `json:"usage"` User string `json:"user"` Metadata map[string]string `json:"metadata"` Background *bool `json:"background"`