Skip to content

Commit bbdd7fd

Browse files
videos
1 parent ecbe3e2 commit bbdd7fd

5 files changed

Lines changed: 199 additions & 0 deletions

File tree

internal/provider/openai/cost.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,14 @@ var OpenAiPerThousandTokenCost = map[string]map[string]float64{
152152
"tts-1": 0.015,
153153
"tts-1-hd": 0.03,
154154
},
155+
"video": { // $ per sec
156+
"sora-2": 0.1,
157+
"sora-2-pro": 0.30,
158+
"sora-2-720": 0.1,
159+
"sora-2-pro-720": 0.30,
160+
"sora-2-pro-1024": 0.5,
161+
"sora-2-pro-1080": 0.7,
162+
},
155163
"completion": {
156164
"gpt-image-1.5": 0.010,
157165
"chatgpt-image-latest": 0.010,
@@ -769,6 +777,40 @@ func (ce *CostEstimator) EstimateResponseApiToolCreateContainerCost(req *Respons
769777
return totalCost, nil
770778
}
771779

780+
func (ce *CostEstimator) EstimateVideoCost(metadata *VideoResponseMetadata) (float64, error) {
781+
if metadata == nil {
782+
return 0, errors.New("metadata is nil")
783+
}
784+
costMap, ok := ce.tokenCostMap["video"]
785+
if !ok {
786+
return 0, errors.New("video cost map is not provided")
787+
}
788+
model := metadata.Model
789+
size, err := normalizedVideoSize(metadata.Size)
790+
if err != nil {
791+
return 0, err
792+
}
793+
costKey := fmt.Sprintf("%s-%s", model, size)
794+
cost, ok := costMap[costKey]
795+
if !ok {
796+
return 0, errors.New("model with provided size is not present in the video cost map")
797+
}
798+
return cost * metadata.GetSecondsAsFloat(), nil
799+
}
800+
801+
func normalizedVideoSize(size string) (string, error) {
802+
switch size {
803+
case "720x1280", "1280x720":
804+
return "720", nil
805+
case "1024x1792", "1792x1024":
806+
return "1024", nil
807+
case "1080x1920", "1920x1080":
808+
return "1080", nil
809+
default:
810+
return "", errors.New("size is not valid")
811+
}
812+
}
813+
772814
var reasoningModelPrefix = []string{"gpt-5", "o1", "o2", "o3"}
773815

774816
func extendedToolType(toolType, model string) string {

internal/provider/openai/types.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package openai
22

3+
import "strconv"
4+
35
type ResponseRequest struct {
46
Background *bool `json:"background,omitzero"`
57
Conversation *any `json:"conversation,omitzero"`
@@ -89,3 +91,16 @@ type ImageResponseMetadata struct {
8991
Size string `json:"size,omitempty"`
9092
Usage ImageResponseUsage `json:"usage,omitempty"`
9193
}
94+
95+
type VideoResponseMetadata struct {
96+
Model string `json:"model,omitempty"`
97+
Size string `json:"size,omitempty"`
98+
Seconds string `json:"seconds,omitempty"`
99+
}
100+
101+
func (v *VideoResponseMetadata) GetSecondsAsFloat() float64 {
102+
if secondsFloat, err := strconv.ParseFloat(v.Seconds, 64); err == nil {
103+
return secondsFloat
104+
}
105+
return 0
106+
}

internal/server/web/proxy/middleware.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ type estimator interface {
6161
EstimateResponseApiTotalCost(model string, usage responsesOpenai.ResponseUsage) (float64, error)
6262
EstimateResponseApiToolCallsCost(tools []responsesOpenai.ToolUnion, model string) (float64, error)
6363
EstimateResponseApiToolCreateContainerCost(req *openai.ResponseRequest) (float64, error)
64+
EstimateVideoCost(metadata *openai.VideoResponseMetadata) (float64, error)
6465
}
6566

6667
type azureEstimator interface {

internal/server/web/proxy/proxy.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,15 @@ func NewProxyServer(log *zap.Logger, mode, privacyMode string, c cache, m KeyMan
104104
router.POST("/api/providers/openai/v1/audio/transcriptions", getTranscriptionsHandler(prod, client, e))
105105
router.POST("/api/providers/openai/v1/audio/translations", getTranslationsHandler(prod, client, e))
106106

107+
// videos
108+
router.POST("/api/providers/openai/v1/videos", getVideoHandler(prod, client, e))
109+
router.POST("/api/providers/openai/v1/videos/edits", getVideoHandler(prod, client, e))
110+
router.POST("/api/providers/openai/v1/videos/extensions", getVideoHandler(prod, client, e))
111+
router.GET("/api/providers/openai/v1/videos/:video_id", getVideoHandler(prod, client, e))
112+
router.DELETE("/api/providers/openai/v1/videos/:video_id", getVideoHandler(prod, client, e))
113+
router.POST("/api/providers/openai/v1/videos/:video_id/remix", getVideoHandler(prod, client, e))
114+
router.GET("/api/providers/openai/v1/videos/:video_id/content", getVideoHandler(prod, client, e))
115+
107116
// completions
108117
router.POST("/api/providers/openai/v1/chat/completions", getChatCompletionHandler(prod, private, client, e))
109118

internal/server/web/proxy/video.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package proxy
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"errors"
7+
"io"
8+
"net/http"
9+
"strings"
10+
"time"
11+
12+
"github.com/bricks-cloud/bricksllm/internal/provider/openai"
13+
"github.com/bricks-cloud/bricksllm/internal/telemetry"
14+
"github.com/bricks-cloud/bricksllm/internal/util"
15+
"github.com/gin-gonic/gin"
16+
goopenai "github.com/sashabaranov/go-openai"
17+
)
18+
19+
func getVideoHandler(prod bool, client http.Client, e estimator) gin.HandlerFunc {
20+
return func(ginCtx *gin.Context) {
21+
log := util.GetLogFromCtx(ginCtx)
22+
telemetry.Incr("bricksllm.proxy.get_responses_handler.requests", nil, 1)
23+
24+
if ginCtx == nil || ginCtx.Request == nil {
25+
JSON(ginCtx, http.StatusInternalServerError, "[BricksLLM] context is empty")
26+
return
27+
}
28+
29+
ctx, cancel := context.WithTimeout(ginCtx.Request.Context(), ginCtx.GetDuration("requestTimeout"))
30+
defer cancel()
31+
32+
videoURL, err := constructVideoURL(ginCtx.Request.URL.Path)
33+
if err != nil {
34+
logError(log, "failed to construct video URL", prod, err)
35+
JSON(ginCtx, http.StatusBadRequest, "[BricksLLM] invalid video request")
36+
return
37+
}
38+
39+
req, err := http.NewRequestWithContext(ctx, ginCtx.Request.Method, videoURL, ginCtx.Request.Body)
40+
if err != nil {
41+
logError(log, "error when creating openai http request", prod, err)
42+
JSON(ginCtx, http.StatusInternalServerError, "[BricksLLM] failed to create openai http request")
43+
return
44+
}
45+
46+
copyHttpHeaders(ginCtx.Request, req, ginCtx.GetBool("removeUserAgent"))
47+
48+
start := time.Now()
49+
res, err := client.Do(req)
50+
if err != nil {
51+
telemetry.Incr("bricksllm.proxy.get_video_handler.http_client_error", nil, 1)
52+
53+
logError(log, "error when sending http request to openai", prod, err)
54+
JSON(ginCtx, http.StatusInternalServerError, "[BricksLLM] failed to send http request to openai")
55+
return
56+
}
57+
defer res.Body.Close()
58+
59+
for name, values := range res.Header {
60+
for _, value := range values {
61+
ginCtx.Header(name, value)
62+
}
63+
}
64+
65+
if res.StatusCode != http.StatusOK {
66+
dur := time.Since(start)
67+
telemetry.Timing("bricksllm.proxy.get_video_handler.error_latency", dur, nil, 1)
68+
telemetry.Incr("bricksllm.proxy.get_video_handler.error_response", nil, 1)
69+
70+
bytes, err2 := io.ReadAll(res.Body)
71+
if err2 != nil {
72+
logError(log, "error when reading openai http video response body", prod, err2)
73+
JSON(ginCtx, http.StatusInternalServerError, "[BricksLLM] failed to read openai response body")
74+
return
75+
}
76+
77+
errorRes := &goopenai.ErrorResponse{}
78+
err2 = json.Unmarshal(bytes, errorRes)
79+
if err2 != nil {
80+
logError(log, "error when unmarshalling openai video error response body", prod, err2)
81+
}
82+
83+
logOpenAiError(log, prod, errorRes)
84+
85+
ginCtx.Data(res.StatusCode, "application/json", bytes)
86+
return
87+
}
88+
89+
dur := time.Since(start)
90+
telemetry.Timing("bricksllm.proxy.get_video_handler.latency", dur, nil, 1)
91+
92+
bytes, err := io.ReadAll(res.Body)
93+
if err != nil {
94+
logError(log, "error when reading openai http video response body", prod, err)
95+
JSON(ginCtx, http.StatusInternalServerError, "[BricksLLM] failed to read openai response body")
96+
return
97+
}
98+
99+
var cost float64 = 0
100+
respMetadata := &openai.VideoResponseMetadata{}
101+
telemetry.Incr("bricksllm.proxy.get_video_handler.success", nil, 1)
102+
telemetry.Timing("bricksllm.proxy.get_video_handler.success_latency", dur, nil, 1)
103+
104+
err = json.Unmarshal(bytes, respMetadata)
105+
if err != nil {
106+
logError(log, "error when unmarshalling openai http video response body", prod, err)
107+
}
108+
109+
isPaidRequest := ginCtx.Request.Method == http.MethodPost
110+
if err == nil && isPaidRequest {
111+
cost, err = e.EstimateVideoCost(respMetadata)
112+
if err != nil {
113+
telemetry.Incr("bricksllm.proxy.get_video_handler.estimate_cost_error", nil, 1)
114+
logError(log, "error when estimating video cost", prod, err)
115+
}
116+
}
117+
ginCtx.Set("costInUsd", cost)
118+
ginCtx.Data(res.StatusCode, "application/json", bytes)
119+
return
120+
}
121+
}
122+
123+
func constructVideoURL(fullPath string) (string, error) {
124+
if fullPath == "" {
125+
return "", errors.New("empty full path")
126+
}
127+
if !strings.HasPrefix(fullPath, "/api/providers/openai") {
128+
return "", errors.New("invalid path prefix")
129+
}
130+
path := strings.TrimPrefix(fullPath, "/api/providers/openai")
131+
return "https://api.openai.com" + path, nil
132+
}

0 commit comments

Comments
 (0)