344 lines
11 KiB
Go
344 lines
11 KiB
Go
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) {
|
|
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()
|
|
switch profile {
|
|
case "retryable_failure":
|
|
return Response{}, &ClientError{
|
|
Code: "server_error",
|
|
Message: "simulated retryable failure",
|
|
RequestID: "simulated-request",
|
|
ResponseStartedAt: responseStartedAt,
|
|
ResponseFinishedAt: responseFinishedAt,
|
|
ResponseDurationMS: responseDurationMS(responseStartedAt, responseFinishedAt),
|
|
Retryable: true,
|
|
}
|
|
case "rate_limit", "overloaded":
|
|
return Response{}, &ClientError{
|
|
Code: profile,
|
|
Message: "simulated " + profile,
|
|
RequestID: "simulated-request",
|
|
ResponseStartedAt: responseStartedAt,
|
|
ResponseFinishedAt: responseFinishedAt,
|
|
ResponseDurationMS: responseDurationMS(responseStartedAt, responseFinishedAt),
|
|
Retryable: true,
|
|
}
|
|
case "invalid_api_key":
|
|
return Response{}, &ClientError{
|
|
Code: "invalid_api_key",
|
|
Message: "simulated invalid_api_key",
|
|
RequestID: "simulated-request",
|
|
ResponseStartedAt: responseStartedAt,
|
|
ResponseFinishedAt: responseFinishedAt,
|
|
ResponseDurationMS: responseDurationMS(responseStartedAt, responseFinishedAt),
|
|
Retryable: false,
|
|
}
|
|
case "fatal_failure", "non_retryable_failure":
|
|
return Response{}, &ClientError{
|
|
Code: "bad_request",
|
|
Message: "simulated non-retryable failure",
|
|
RequestID: "simulated-request",
|
|
ResponseStartedAt: responseStartedAt,
|
|
ResponseFinishedAt: responseFinishedAt,
|
|
ResponseDurationMS: responseDurationMS(responseStartedAt, responseFinishedAt),
|
|
Retryable: false,
|
|
}
|
|
}
|
|
result := simulatedResult(request)
|
|
return Response{
|
|
Result: result,
|
|
RequestID: requestIDFromResult(result),
|
|
Usage: simulatedUsage(request),
|
|
Progress: simulatedProgress(request),
|
|
ResponseStartedAt: responseStartedAt,
|
|
ResponseFinishedAt: responseFinishedAt,
|
|
ResponseDurationMS: responseDurationMS(responseStartedAt, responseFinishedAt),
|
|
}, nil
|
|
}
|
|
|
|
func simulationProfile(request Request) string {
|
|
if value := stringValue(request.Candidate.Credentials, "simulationFailure"); value != "" {
|
|
return value
|
|
}
|
|
if value := stringValue(request.Candidate.PlatformConfig, "simulationFailure"); value != "" {
|
|
return value
|
|
}
|
|
if value := stringValue(request.Body, "simulationProfile"); value != "" {
|
|
return value
|
|
}
|
|
if value := stringValue(request.Body, "testProfile"); value != "" {
|
|
return value
|
|
}
|
|
return "success"
|
|
}
|
|
|
|
func simulatedResult(request Request) map[string]any {
|
|
switch request.Kind {
|
|
case "chat.completions":
|
|
return map[string]any{
|
|
"id": "chatcmpl-simulated",
|
|
"object": "chat.completion",
|
|
"created": nowUnix(),
|
|
"model": request.Model,
|
|
"choices": []any{map[string]any{
|
|
"index": 0,
|
|
"finish_reason": "stop",
|
|
"message": map[string]any{
|
|
"role": "assistant",
|
|
"content": fmt.Sprintf("simulation response from %s", request.Candidate.Provider),
|
|
},
|
|
}},
|
|
"usage": map[string]any{"prompt_tokens": 12, "completion_tokens": 8, "total_tokens": 20},
|
|
}
|
|
case "responses":
|
|
return map[string]any{
|
|
"id": "resp-simulated",
|
|
"object": "response",
|
|
"created_at": nowUnix(),
|
|
"model": request.Model,
|
|
"output_text": fmt.Sprintf("simulation response from %s", request.Candidate.Provider),
|
|
"usage": map[string]any{"input_tokens": 12, "output_tokens": 8, "total_tokens": 20},
|
|
}
|
|
case "images.edits":
|
|
return map[string]any{
|
|
"id": "img-edit-simulated",
|
|
"created": nowUnix(),
|
|
"model": request.Model,
|
|
"data": simulatedImageData(request, "/static/simulation/image-edit.svg", "simulation image edit"),
|
|
}
|
|
case "images.generations":
|
|
return map[string]any{
|
|
"id": "img-simulated",
|
|
"created": nowUnix(),
|
|
"model": request.Model,
|
|
"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.ModelType == "text_generate" || request.Kind == "responses" {
|
|
return Usage{InputTokens: 12, OutputTokens: 8, TotalTokens: 20}
|
|
}
|
|
return Usage{}
|
|
}
|
|
|
|
func simulatedProgress(request Request) []Progress {
|
|
provider := request.Candidate.Provider
|
|
if provider == "" {
|
|
provider = "simulation"
|
|
}
|
|
return []Progress{
|
|
{Phase: "normalizing", Progress: 0.2, Message: "request normalized", Payload: map[string]any{"provider": provider}},
|
|
{Phase: "submitting", Progress: 0.55, Message: "simulation client submitted", Payload: map[string]any{"clientId": request.Candidate.ClientID}},
|
|
{Phase: "fetching_result", Progress: 0.85, Message: "simulation result ready", Payload: map[string]any{"kind": request.Kind}},
|
|
}
|
|
}
|
|
|
|
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 != "" {
|
|
return value
|
|
}
|
|
}
|
|
return fallback
|
|
}
|