feat(api, xai): add xAI Grok video model support with API integration
- Introduced new xAI `grok-imagine-video` model for video generation with configurable options (e.g., duration, size, resolution). - Implemented video-specific API endpoints (`/v1/videos`, `/v1/videos/generations`, `/v1/videos/edits`, `/v1/videos/extensions`), including request validation and model handling. - Enhanced model registry with `xaiBuiltinVideoModelID` and metadata for video capabilities. - Added unit tests to validate video model support, request structures, and API response handling. - Extended `XAIExecutor` to integrate video generation and retrieval via runtime requests.
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -29,9 +30,15 @@ var xaiDataTag = []byte("data:")
|
||||
|
||||
const (
|
||||
xaiImageHandlerType = "openai-image"
|
||||
xaiVideoHandlerType = "openai-video"
|
||||
xaiImagesGenerationsPath = "/images/generations"
|
||||
xaiImagesEditsPath = "/images/edits"
|
||||
xaiDefaultImageEndpointPath = xaiImagesGenerationsPath
|
||||
xaiVideosGenerationsPath = "/videos/generations"
|
||||
xaiVideosEditsPath = "/videos/edits"
|
||||
xaiVideosExtensionsPath = "/videos/extensions"
|
||||
xaiVideosPath = "/videos"
|
||||
xaiIdempotencyKeyMetaKey = "idempotency_key"
|
||||
)
|
||||
|
||||
// XAIExecutor is a stateless executor for xAI Grok's Responses API.
|
||||
@@ -86,6 +93,9 @@ func (e *XAIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
|
||||
if endpointPath := xaiImageEndpointPath(opts); endpointPath != "" {
|
||||
return e.executeImages(ctx, auth, req, endpointPath)
|
||||
}
|
||||
if xaiIsVideoRequest(opts) {
|
||||
return e.executeVideos(ctx, auth, req, opts)
|
||||
}
|
||||
|
||||
token, baseURL := xaiCreds(auth)
|
||||
if baseURL == "" {
|
||||
@@ -207,6 +217,71 @@ func (e *XAIExecutor) executeImages(ctx context.Context, auth *cliproxyauth.Auth
|
||||
return cliproxyexecutor.Response{Payload: data, Headers: httpResp.Header.Clone()}, nil
|
||||
}
|
||||
|
||||
func (e *XAIExecutor) executeVideos(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) {
|
||||
token, baseURL := xaiCreds(auth)
|
||||
if baseURL == "" {
|
||||
baseURL = xaiauth.DefaultAPIBaseURL
|
||||
}
|
||||
|
||||
method := http.MethodPost
|
||||
endpointPath := xaiVideosGenerationsPath
|
||||
var body io.Reader = bytes.NewReader(req.Payload)
|
||||
|
||||
switch path := xaiVideoEndpointPath(opts); path {
|
||||
case xaiVideosGenerationsPath, xaiVideosEditsPath, xaiVideosExtensionsPath:
|
||||
endpointPath = path
|
||||
default:
|
||||
if requestID := strings.TrimSpace(gjson.GetBytes(req.Payload, "request_id").String()); requestID != "" {
|
||||
method = http.MethodGet
|
||||
endpointPath = xaiVideosPath + "/" + url.PathEscape(requestID)
|
||||
body = nil
|
||||
}
|
||||
}
|
||||
requestURL := strings.TrimSuffix(baseURL, "/") + endpointPath
|
||||
httpReq, err := http.NewRequestWithContext(ctx, method, requestURL, body)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
applyXAIHeaders(httpReq, auth, token, false, "")
|
||||
if method == http.MethodPost {
|
||||
key := xaiMetadataString(opts.Metadata, xaiIdempotencyKeyMetaKey)
|
||||
if key == "" && opts.Headers != nil {
|
||||
key = strings.TrimSpace(opts.Headers.Get("x-idempotency-key"))
|
||||
}
|
||||
if key != "" {
|
||||
httpReq.Header.Set("x-idempotency-key", key)
|
||||
}
|
||||
}
|
||||
e.recordXAIRequest(ctx, auth, requestURL, httpReq.Header.Clone(), req.Payload)
|
||||
|
||||
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||
httpResp, err := httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||
return resp, err
|
||||
}
|
||||
defer func() {
|
||||
if errClose := httpResp.Body.Close(); errClose != nil {
|
||||
log.Errorf("xai executor: close response body error: %v", errClose)
|
||||
}
|
||||
}()
|
||||
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||
|
||||
data, err := io.ReadAll(httpResp.Body)
|
||||
if err != nil {
|
||||
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||
return resp, err
|
||||
}
|
||||
helps.AppendAPIResponseChunk(ctx, e.cfg, data)
|
||||
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), data))
|
||||
return resp, statusErr{code: httpResp.StatusCode, msg: string(data)}
|
||||
}
|
||||
|
||||
return cliproxyexecutor.Response{Payload: data, Headers: httpResp.Header.Clone()}, nil
|
||||
}
|
||||
|
||||
func (e *XAIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (_ *cliproxyexecutor.StreamResult, err error) {
|
||||
token, baseURL := xaiCreds(auth)
|
||||
if baseURL == "" {
|
||||
@@ -525,6 +600,27 @@ func xaiImageEndpointPath(opts cliproxyexecutor.Options) string {
|
||||
return xaiDefaultImageEndpointPath
|
||||
}
|
||||
|
||||
func xaiIsVideoRequest(opts cliproxyexecutor.Options) bool {
|
||||
return opts.SourceFormat.String() == xaiVideoHandlerType
|
||||
}
|
||||
|
||||
func xaiVideoEndpointPath(opts cliproxyexecutor.Options) string {
|
||||
if !xaiIsVideoRequest(opts) {
|
||||
return ""
|
||||
}
|
||||
path := xaiMetadataString(opts.Metadata, cliproxyexecutor.RequestPathMetadataKey)
|
||||
if strings.HasSuffix(path, "/videos/edits") {
|
||||
return xaiVideosEditsPath
|
||||
}
|
||||
if strings.HasSuffix(path, "/videos/extensions") {
|
||||
return xaiVideosExtensionsPath
|
||||
}
|
||||
if strings.HasSuffix(path, "/videos/generations") {
|
||||
return xaiVideosGenerationsPath
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func xaiMetadataString(meta map[string]any, key string) string {
|
||||
if len(meta) == 0 || key == "" {
|
||||
return ""
|
||||
|
||||
Reference in New Issue
Block a user