feat: improve simulation media tasks
This commit is contained in:
@@ -7,10 +7,96 @@ import (
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
||||
)
|
||||
|
||||
func TestSimulationClientReturnsImageDemoAssets(t *testing.T) {
|
||||
response, err := (SimulationClient{}).Run(context.Background(), Request{
|
||||
Kind: "images.generations",
|
||||
Model: "gpt-image-1",
|
||||
Body: map[string]any{
|
||||
"prompt": "demo image",
|
||||
"n": 2,
|
||||
"simulationDurationMs": 5,
|
||||
},
|
||||
Candidate: store.RuntimeModelCandidate{Provider: "simulation"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("run simulation image client: %v", err)
|
||||
}
|
||||
data, _ := response.Result["data"].([]any)
|
||||
if len(data) != 2 || response.ResponseDurationMS <= 0 {
|
||||
t.Fatalf("unexpected simulated image response: %+v duration=%d", response.Result, response.ResponseDurationMS)
|
||||
}
|
||||
item, _ := data[0].(map[string]any)
|
||||
if item["url"] != "/static/simulation/image.svg" || item["assetSource"] != "simulation" {
|
||||
t.Fatalf("unexpected simulated image item: %+v", item)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimulationClientReturnsVideoDemoAssets(t *testing.T) {
|
||||
response, err := (SimulationClient{}).Run(context.Background(), Request{
|
||||
Kind: "videos.generations",
|
||||
ModelType: "video_generate",
|
||||
Model: "demo-video-model",
|
||||
Body: map[string]any{
|
||||
"prompt": "demo video",
|
||||
"count": 2,
|
||||
"duration": 6,
|
||||
"simulationDurationMs": 5,
|
||||
},
|
||||
Candidate: store.RuntimeModelCandidate{Provider: "simulation"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("run simulation video client: %v", err)
|
||||
}
|
||||
data, _ := response.Result["data"].([]any)
|
||||
if len(data) != 2 || response.ResponseDurationMS <= 0 {
|
||||
t.Fatalf("unexpected simulated video response: %+v duration=%d", response.Result, response.ResponseDurationMS)
|
||||
}
|
||||
item, _ := data[0].(map[string]any)
|
||||
if item["video_url"] != "/static/simulation/video.mp4" || item["url"] != "/static/simulation/video.mp4" || item["poster"] != "/static/simulation/video-poster.svg" {
|
||||
t.Fatalf("unexpected simulated video item: %+v", item)
|
||||
}
|
||||
if item["duration"] != 6 || item["assetSource"] != "simulation" {
|
||||
t.Fatalf("unexpected simulated video metadata: %+v", item)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimulationDurationDefaultsByMediaType(t *testing.T) {
|
||||
imageDuration := simulationDuration(Request{Kind: "images.generations"})
|
||||
if imageDuration < 10*time.Second || imageDuration > 30*time.Second {
|
||||
t.Fatalf("image simulation duration should default to 10-30s, got %s", imageDuration)
|
||||
}
|
||||
videoDuration := simulationDuration(Request{Kind: "videos.generations"})
|
||||
if videoDuration < 2*time.Minute || videoDuration > 3*time.Minute {
|
||||
t.Fatalf("video simulation duration should default to 2-3m, got %s", videoDuration)
|
||||
}
|
||||
textDuration := simulationDuration(Request{Kind: "chat.completions"})
|
||||
if textDuration < 800*time.Millisecond || textDuration > 2400*time.Millisecond {
|
||||
t.Fatalf("text simulation duration should keep short defaults, got %s", textDuration)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSimulationDurationCanBeControlledByParams(t *testing.T) {
|
||||
fixedDuration := simulationDuration(Request{Body: map[string]any{"simulationDurationSeconds": 7}})
|
||||
if fixedDuration != 7*time.Second {
|
||||
t.Fatalf("simulationDurationSeconds should set fixed duration, got %s", fixedDuration)
|
||||
}
|
||||
rangeDuration := simulationDuration(Request{
|
||||
Kind: "videos.generations",
|
||||
Body: map[string]any{
|
||||
"simulationMinDurationSeconds": 1,
|
||||
"simulationMaxDurationSeconds": 1,
|
||||
},
|
||||
})
|
||||
if rangeDuration != time.Second {
|
||||
t.Fatalf("simulation duration range params should override video defaults, got %s", rangeDuration)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAIClientChatContract(t *testing.T) {
|
||||
var gotPath string
|
||||
var gotAuth string
|
||||
|
||||
@@ -3,24 +3,60 @@ package clients
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SimulationClient struct{}
|
||||
|
||||
const (
|
||||
defaultSimulationTextMinDuration = 800 * time.Millisecond
|
||||
defaultSimulationTextMaxDuration = 2400 * time.Millisecond
|
||||
defaultSimulationImageMinDuration = 10 * time.Second
|
||||
defaultSimulationImageMaxDuration = 30 * time.Second
|
||||
defaultSimulationVideoMinDuration = 2 * time.Minute
|
||||
defaultSimulationVideoMaxDuration = 3 * time.Minute
|
||||
maxSimulationDuration = 10 * time.Minute
|
||||
)
|
||||
|
||||
func (c SimulationClient) Run(ctx context.Context, request Request) (Response, error) {
|
||||
_ = ctx
|
||||
profile := simulationProfile(request)
|
||||
responseStartedAt := time.Now()
|
||||
duration := simulationDuration(request)
|
||||
if duration > 0 {
|
||||
timer := time.NewTimer(duration)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
timer.Stop()
|
||||
return Response{}, ctx.Err()
|
||||
case <-timer.C:
|
||||
}
|
||||
}
|
||||
responseFinishedAt := time.Now()
|
||||
if profile == "retryable_failure" {
|
||||
return Response{}, &ClientError{Code: "server_error", Message: "simulated retryable failure", Retryable: true}
|
||||
return Response{}, &ClientError{
|
||||
Code: "server_error",
|
||||
Message: "simulated retryable failure",
|
||||
RequestID: "simulated-request",
|
||||
ResponseStartedAt: responseStartedAt,
|
||||
ResponseFinishedAt: responseFinishedAt,
|
||||
ResponseDurationMS: responseDurationMS(responseStartedAt, responseFinishedAt),
|
||||
Retryable: true,
|
||||
}
|
||||
}
|
||||
if profile == "fatal_failure" || profile == "non_retryable_failure" {
|
||||
return Response{}, &ClientError{Code: "bad_request", Message: "simulated non-retryable failure", Retryable: false}
|
||||
return Response{}, &ClientError{
|
||||
Code: "bad_request",
|
||||
Message: "simulated non-retryable failure",
|
||||
RequestID: "simulated-request",
|
||||
ResponseStartedAt: responseStartedAt,
|
||||
ResponseFinishedAt: responseFinishedAt,
|
||||
ResponseDurationMS: responseDurationMS(responseStartedAt, responseFinishedAt),
|
||||
Retryable: false,
|
||||
}
|
||||
}
|
||||
responseStartedAt := time.Now()
|
||||
result := simulatedResult(request)
|
||||
responseFinishedAt := time.Now()
|
||||
return Response{
|
||||
Result: result,
|
||||
RequestID: requestIDFromResult(result),
|
||||
@@ -80,24 +116,73 @@ func simulatedResult(request Request) map[string]any {
|
||||
"id": "img-edit-simulated",
|
||||
"created": nowUnix(),
|
||||
"model": request.Model,
|
||||
"data": []any{map[string]any{
|
||||
"url": "/static/simulation/image-edit.png",
|
||||
"revised_prompt": firstNonEmptyPrompt(request.Body, "simulation image edit"),
|
||||
}},
|
||||
"data": simulatedImageData(request, "/static/simulation/image-edit.svg", "simulation image edit"),
|
||||
}
|
||||
default:
|
||||
case "images.generations":
|
||||
return map[string]any{
|
||||
"id": "img-simulated",
|
||||
"created": nowUnix(),
|
||||
"model": request.Model,
|
||||
"data": []any{map[string]any{
|
||||
"url": "/static/simulation/image.png",
|
||||
"revised_prompt": firstNonEmptyPrompt(request.Body, "simulation image"),
|
||||
}},
|
||||
"data": simulatedImageData(request, "/static/simulation/image.svg", "simulation image"),
|
||||
}
|
||||
case "videos.generations":
|
||||
return map[string]any{
|
||||
"id": "video-simulated",
|
||||
"created": nowUnix(),
|
||||
"model": request.Model,
|
||||
"data": simulatedVideoData(request),
|
||||
}
|
||||
default:
|
||||
modelType := strings.ToLower(request.ModelType)
|
||||
kind := strings.ToLower(request.Kind)
|
||||
if strings.Contains(modelType, "video") || strings.Contains(kind, "video") {
|
||||
return map[string]any{
|
||||
"id": "video-simulated",
|
||||
"created": nowUnix(),
|
||||
"model": request.Model,
|
||||
"data": simulatedVideoData(request),
|
||||
}
|
||||
}
|
||||
return map[string]any{
|
||||
"id": "img-simulated",
|
||||
"created": nowUnix(),
|
||||
"model": request.Model,
|
||||
"data": simulatedImageData(request, "/static/simulation/image.svg", "simulation image"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func simulatedImageData(request Request, url string, fallbackPrompt string) []any {
|
||||
count := simulatedOutputCount(request.Body)
|
||||
items := make([]any, 0, count)
|
||||
for index := 0; index < count; index += 1 {
|
||||
items = append(items, map[string]any{
|
||||
"url": url,
|
||||
"assetSource": "simulation",
|
||||
"index": index,
|
||||
"revised_prompt": firstNonEmptyPrompt(request.Body, fallbackPrompt),
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func simulatedVideoData(request Request) []any {
|
||||
count := simulatedOutputCount(request.Body)
|
||||
items := make([]any, 0, count)
|
||||
for index := 0; index < count; index += 1 {
|
||||
items = append(items, map[string]any{
|
||||
"url": "/static/simulation/video.mp4",
|
||||
"video_url": "/static/simulation/video.mp4",
|
||||
"poster": "/static/simulation/video-poster.svg",
|
||||
"duration": simulatedVideoDurationSeconds(request),
|
||||
"assetSource": "simulation",
|
||||
"index": index,
|
||||
"revised_prompt": firstNonEmptyPrompt(request.Body, "simulation video"),
|
||||
})
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func simulatedUsage(request Request) Usage {
|
||||
if request.ModelType == "chat" || request.Kind == "responses" {
|
||||
return Usage{InputTokens: 12, OutputTokens: 8, TotalTokens: 20}
|
||||
@@ -117,6 +202,117 @@ func simulatedProgress(request Request) []Progress {
|
||||
}
|
||||
}
|
||||
|
||||
func simulationDuration(request Request) time.Duration {
|
||||
if fixedMS := simulationDurationMS(request, "simulationDurationMs", "testDurationMs"); fixedMS >= 0 {
|
||||
return clampSimulationDuration(time.Duration(fixedMS) * time.Millisecond)
|
||||
}
|
||||
if fixedSeconds := simulationDurationSeconds(request, "simulationDurationSeconds", "testDurationSeconds"); fixedSeconds >= 0 {
|
||||
return clampSimulationDuration(time.Duration(fixedSeconds) * time.Second)
|
||||
}
|
||||
minDuration, maxDuration := defaultSimulationDurationRange(request)
|
||||
if minMS := simulationDurationMS(request, "simulationMinDurationMs", "simulationDurationMinMs", "testMinDurationMs", "testDurationMinMs"); minMS >= 0 {
|
||||
minDuration = time.Duration(minMS) * time.Millisecond
|
||||
}
|
||||
if maxMS := simulationDurationMS(request, "simulationMaxDurationMs", "simulationDurationMaxMs", "testMaxDurationMs", "testDurationMaxMs"); maxMS >= 0 {
|
||||
maxDuration = time.Duration(maxMS) * time.Millisecond
|
||||
}
|
||||
if minSeconds := simulationDurationSeconds(request, "simulationMinDurationSeconds", "simulationDurationMinSeconds", "testMinDurationSeconds", "testDurationMinSeconds"); minSeconds >= 0 {
|
||||
minDuration = time.Duration(minSeconds) * time.Second
|
||||
}
|
||||
if maxSeconds := simulationDurationSeconds(request, "simulationMaxDurationSeconds", "simulationDurationMaxSeconds", "testMaxDurationSeconds", "testDurationMaxSeconds"); maxSeconds >= 0 {
|
||||
maxDuration = time.Duration(maxSeconds) * time.Second
|
||||
}
|
||||
minDuration = clampSimulationDuration(minDuration)
|
||||
maxDuration = clampSimulationDuration(maxDuration)
|
||||
if maxDuration < minDuration {
|
||||
maxDuration = minDuration
|
||||
}
|
||||
spread := maxDuration - minDuration
|
||||
if spread <= 0 {
|
||||
return minDuration
|
||||
}
|
||||
return minDuration + time.Duration(rand.Int63n(int64(spread)+1))
|
||||
}
|
||||
|
||||
func defaultSimulationDurationRange(request Request) (time.Duration, time.Duration) {
|
||||
if simulationVideoRequest(request) {
|
||||
return defaultSimulationVideoMinDuration, defaultSimulationVideoMaxDuration
|
||||
}
|
||||
if simulationImageRequest(request) {
|
||||
return defaultSimulationImageMinDuration, defaultSimulationImageMaxDuration
|
||||
}
|
||||
return defaultSimulationTextMinDuration, defaultSimulationTextMaxDuration
|
||||
}
|
||||
|
||||
func simulationVideoRequest(request Request) bool {
|
||||
kind := strings.ToLower(request.Kind)
|
||||
modelType := strings.ToLower(request.ModelType)
|
||||
return strings.Contains(kind, "video") || strings.Contains(modelType, "video")
|
||||
}
|
||||
|
||||
func simulationImageRequest(request Request) bool {
|
||||
kind := strings.ToLower(request.Kind)
|
||||
modelType := strings.ToLower(request.ModelType)
|
||||
return strings.Contains(kind, "image") || strings.Contains(modelType, "image")
|
||||
}
|
||||
|
||||
func simulationDurationSeconds(request Request, keys ...string) int {
|
||||
for _, source := range []map[string]any{request.Body, request.Candidate.PlatformConfig, request.Candidate.Credentials} {
|
||||
for _, key := range keys {
|
||||
value := intValue(source, key, -1)
|
||||
if value >= 0 {
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func simulationDurationMS(request Request, keys ...string) int {
|
||||
for _, source := range []map[string]any{request.Body, request.Candidate.PlatformConfig, request.Candidate.Credentials} {
|
||||
for _, key := range keys {
|
||||
value := intValue(source, key, -1)
|
||||
if value >= 0 {
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func clampSimulationDuration(duration time.Duration) time.Duration {
|
||||
if duration < 0 {
|
||||
return 0
|
||||
}
|
||||
if duration > maxSimulationDuration {
|
||||
return maxSimulationDuration
|
||||
}
|
||||
return duration
|
||||
}
|
||||
|
||||
func simulatedOutputCount(body map[string]any) int {
|
||||
count := intValue(body, "n", 0)
|
||||
if count <= 0 {
|
||||
count = intValue(body, "count", 0)
|
||||
}
|
||||
if count <= 0 {
|
||||
return 1
|
||||
}
|
||||
if count > 20 {
|
||||
return 20
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func simulatedVideoDurationSeconds(request Request) int {
|
||||
for _, key := range []string{"duration", "duration_seconds", "durationSeconds"} {
|
||||
if value := intValue(request.Body, key, 0); value > 0 {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return 5
|
||||
}
|
||||
|
||||
func firstNonEmptyPrompt(body map[string]any, fallback string) string {
|
||||
for _, key := range []string{"prompt", "input"} {
|
||||
if value := strings.TrimSpace(stringValue(body, key)); value != "" {
|
||||
|
||||
Reference in New Issue
Block a user