Files
new-api/controller/plugin_protocol_test.go
T
RedwindA 19d435e7fd fix(relay): mask only the custom upstream address in error messages (#7822)
* fix(relay): mask only the upstream address in error messages

MaskSensitiveInfo masked every URL, domain and IPv4 in upstream error
messages, which garbled documentation links, email domains and dotted
identifiers that clients need to read.

It now takes the upstream addresses of the request: the channel base URL,
the origin the gateway actually dialed (recorded in doRequest, DoWssRequest,
the AWS SDK client and the Xunfei and Volcengine TTS dials, which covers
absolute Advanced Custom routes), the channel proxy and the environment
proxy. URLs and bare hostnames on those hosts and all IPv4 addresses are
masked; other URLs and domains stay readable. With no upstream address no
address is masked. Credentials are masked on every URL regardless of host:
api_key values, query values and userinfo.

The URL matcher also keeps quotes, colons and semicolons that follow a URL,
keeps parentheses and apostrophes inside it, masks an IPv4 URL host whole and
keeps a country-code TLD when the host has a port.

* fix(relay): keep preset official endpoints readable in masked errors

A channel on the base URL new-api presets for its type reaches a public
official endpoint, so its address is not masked. Only addresses the admin
configured are: a custom base URL, the origin of an Advanced Custom route,
and the channel and environment proxies. Preset hosts are compared with
their port so a configured localhost upstream stays apart from the Ollama
preset.

The origins recorded for the AWS SDK, Xunfei and Volcengine TTS dials and the
plugin allowedHosts masking are dropped: they are all official endpoints.

* fix(relay): mask IPv4 addresses and plugin allowed hosts in every error

IPv4 addresses in network errors include the gateway's own address, so they
are masked even when the channel uses a preset base URL. Task plugin build
failures mask the plugin's allowedHosts again: an installed plugin may point
them at private hosts.

* fix(relay): mask bracketed IPv6 addresses in errors

Go network errors write IPv6 addresses in brackets, as in
"dial tcp [2001:db8::1]:443". Like IPv4, they are the resolved upstream or
the gateway's own address, so they are masked on every error once the
bracketed content parses as an IP.
2026-10-15 15:49:45 +08:00

1870 lines
72 KiB
Go

package controller
import (
"context"
"errors"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"time"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/constant"
"github.com/QuantumNous/new-api/dto"
"github.com/QuantumNous/new-api/model"
pluginruntime "github.com/QuantumNous/new-api/pkg/jsplugin"
"github.com/QuantumNous/new-api/relay"
relaycommon "github.com/QuantumNous/new-api/relay/common"
"github.com/QuantumNous/new-api/service"
"github.com/gin-gonic/gin"
"github.com/glebarez/sqlite"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
)
func TestServeTaskPluginProtocolWaitsForDurableSubmissionBeforeWriting(t *testing.T) {
pinned := compilePluginProtocolTestEndpoint(t, "durable-barrier", `
export const protocols = {openai_responses: {
renderEvents: function() { return {events: [], done: false}; },
renderFinal: function() { return {}; }
}};
`)
c, recorder := newPluginProtocolTestContext(true, false)
submitStarted := make(chan struct{})
releaseSubmit := make(chan struct{})
done := make(chan struct{})
deps := pluginProtocolTestDeps()
deps.submit = func(_ *gin.Context, info *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
close(submitStarted)
<-releaseSubmit
return pluginProtocolTestOutcome(info, pinned.Plugin.Meta.Key, "task_durable", map[string]any{
"must_not": "be_written",
}), nil
}
deps.loadTask = func(context.Context, int, constant.TaskPlatform, string) (*model.Task, bool, error) {
return nil, false, errors.New("observation failed after durable barrier")
}
go func() {
defer close(done)
serveTaskPluginProtocol(c, pinned, deps)
}()
select {
case <-submitStarted:
case <-time.After(2 * time.Second):
require.FailNow(t, "submission did not start")
}
assert.Empty(t, recorder.Header().Get("Content-Type"))
assert.Empty(t, recorder.Body.String())
assert.False(t, recorder.Flushed)
close(releaseSubmit)
select {
case <-done:
case <-time.After(2 * time.Second):
require.FailNow(t, "protocol handler did not finish")
}
assert.Equal(t, "text/event-stream", recorder.Header().Get("Content-Type"))
assert.Contains(t, recorder.Body.String(), "event: response.created\n")
assert.NotContains(t, recorder.Body.String(), "must_not")
}
func TestServeTaskPluginProtocolDisconnectDuringSubmissionFinishesDurableWithoutWriting(t *testing.T) {
pinned := compilePluginProtocolTestEndpoint(t, "disconnect-during-submit", `
export const protocols = {openai_responses: {
renderEvents: function() { return {events: [], done: false}; },
renderFinal: function() { return {}; }
}};
`)
c, recorder := newPluginProtocolTestContext(true, true)
requestContext, cancel := context.WithCancel(c.Request.Context())
c.Request = c.Request.WithContext(requestContext)
submitStarted := make(chan struct{})
checkSubmissionContext := make(chan struct{})
submissionContextActive := make(chan struct{})
releaseSubmit := make(chan struct{})
observationStarted := make(chan struct{}, 1)
done := make(chan struct{})
deps := pluginProtocolTestDeps()
deps.submit = func(c *gin.Context, info *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
close(submitStarted)
<-checkSubmissionContext
select {
case <-c.Request.Context().Done():
return nil, service.TaskErrorWrapperLocal(c.Request.Context().Err(), "request_cancelled", http.StatusRequestTimeout)
default:
close(submissionContextActive)
}
<-releaseSubmit
return pluginProtocolTestOutcome(info, pinned.Plugin.Meta.Key, "task_disconnect_durable", nil), nil
}
deps.loadTask = func(context.Context, int, constant.TaskPlatform, string) (*model.Task, bool, error) {
observationStarted <- struct{}{}
return nil, false, errors.New("observation must not start after disconnect")
}
go func() {
defer close(done)
serveTaskPluginProtocol(c, pinned, deps)
}()
select {
case <-submitStarted:
case <-time.After(2 * time.Second):
require.FailNow(t, "submission did not start")
}
cancel()
close(checkSubmissionContext)
select {
case <-submissionContextActive:
case <-time.After(2 * time.Second):
require.FailNow(t, "submission context was canceled with the client")
}
select {
case <-done:
require.FailNow(t, "protocol handler stopped before submission became durable")
default:
}
close(releaseSubmit)
select {
case <-done:
case <-time.After(2 * time.Second):
require.FailNow(t, "protocol handler did not finish after durable submission")
}
assert.Empty(t, recorder.Header().Get("Content-Type"))
assert.Empty(t, recorder.Body.String())
assert.False(t, recorder.Flushed)
select {
case <-observationStarted:
require.FailNow(t, "protocol observation started after client disconnect")
default:
}
}
func TestServeTaskPluginProtocolDisconnectBeforeDurableBarrierPersistsAndSettlesWithoutRefund(t *testing.T) {
events := make([]string, 0, 3)
database := setupTaskSubmissionDatabase(t, true, &events)
previousLogConsumeEnabled := common.LogConsumeEnabled
common.LogConsumeEnabled = false
t.Cleanup(func() { common.LogConsumeEnabled = previousLogConsumeEnabled })
pinned := compilePluginProtocolTestEndpoint(t, "disconnect-before-durable", `
export const protocols = {openai_responses: {
renderEvents: function() { return {events: [], done: false}; },
renderFinal: function() { return {}; }
}};
`)
c, recorder := newPluginProtocolTestContext(true, true)
requestContext, cancel := context.WithCancel(c.Request.Context())
c.Request = c.Request.WithContext(requestContext)
billing := &taskSubmissionTestBilling{events: &events}
submitStarted := make(chan struct{})
releaseSubmit := make(chan struct{})
observationStarted := make(chan struct{}, 1)
done := make(chan struct{})
deps := pluginProtocolTestDeps()
deps.submit = func(c *gin.Context, info *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
info.Billing = billing
info.TaskRelayInfo.PublicTaskID = "task_disconnect_persisted"
info.TaskRelayInfo.LockedChannel = &model.Channel{
Id: 1,
Type: constant.ChannelTypeTaskPlugin,
Name: "disconnect-before-durable",
}
info.ChannelMeta = &relaycommon.ChannelMeta{
ChannelId: 1,
ChannelType: constant.ChannelTypeTaskPlugin,
}
return executeTaskSubmissionWith(c, info, func(*gin.Context, *relaycommon.RelayInfo) (*relay.TaskSubmitResult, *dto.TaskError) {
close(submitStarted)
<-releaseSubmit
return &relay.TaskSubmitResult{
UpstreamTaskID: "upstream_disconnect_persisted",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
Quota: 7,
}, nil
})
}
deps.loadTask = func(context.Context, int, constant.TaskPlatform, string) (*model.Task, bool, error) {
observationStarted <- struct{}{}
return nil, false, errors.New("observation must not start after disconnect")
}
go func() {
defer close(done)
serveTaskPluginProtocol(c, pinned, deps)
}()
select {
case <-submitStarted:
case <-time.After(2 * time.Second):
require.FailNow(t, "submission did not start")
}
cancel()
close(releaseSubmit)
select {
case <-done:
case <-time.After(2 * time.Second):
require.FailNow(t, "detached submission did not finish")
}
assert.Equal(t, []string{"reserve", "insert", "settle"}, events)
assert.Zero(t, billing.refunds)
var persisted model.Task
require.NoError(t, database.Where("task_id = ?", "task_disconnect_persisted").First(&persisted).Error)
assert.Equal(t, model.TaskStatus(model.TaskStatusNotStart), persisted.Status)
assert.Equal(t, 7, persisted.Quota)
assert.Equal(t, "upstream_disconnect_persisted", persisted.PrivateData.UpstreamTaskID)
assert.Empty(t, recorder.Header().Get("Content-Type"))
assert.Empty(t, recorder.Body.String())
assert.False(t, recorder.Flushed)
select {
case <-observationStarted:
require.FailNow(t, "protocol observation started after client disconnect")
default:
}
}
func TestServeTaskPluginProtocolDisconnectDuringTerminalSettlementStopsOnlyObservation(t *testing.T) {
pinned := compilePluginProtocolTestEndpoint(t, "disconnect-terminal-settlement", `
export const protocols = {openai_responses: {
renderEvents: function() { return {events: [], done: false}; },
renderFinal: function() { return {}; }
}};
`)
previousDB := model.DB
previousMemoryCache := common.MemoryCacheEnabled
database, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)
require.NoError(t, database.AutoMigrate(&model.Channel{}, &model.Task{}))
model.DB = database
common.MemoryCacheEnabled = false
t.Cleanup(func() {
model.DB = previousDB
common.MemoryCacheEnabled = previousMemoryCache
})
baseURL := "https://example.com"
channel := model.Channel{
Type: constant.ChannelTypeTaskPlugin,
Name: "terminal-settlement",
Key: "test-key",
BaseURL: &baseURL,
Status: common.ChannelStatusEnabled,
}
require.NoError(t, database.Create(&channel).Error)
task := model.Task{
TaskID: "task_terminal_disconnect",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
ChannelId: channel.Id,
Quota: 10,
Status: model.TaskStatusSubmitted,
PrivateData: model.TaskPrivateData{
UpstreamTaskID: "upstream-terminal",
},
}
require.NoError(t, database.Create(&task).Error)
c, recorder := newPluginProtocolTestContext(true, true)
requestContext, cancel := context.WithCancel(c.Request.Context())
c.Request = c.Request.WithContext(requestContext)
billingEvents := make([]string, 0)
billing := &taskSubmissionTestBilling{events: &billingEvents}
observationStarted := make(chan struct{})
settlementStarted := make(chan struct{})
releaseSettlement := make(chan struct{})
t.Cleanup(func() {
select {
case <-releaseSettlement:
default:
close(releaseSettlement)
}
})
pollingDone := make(chan struct{})
adaptor := &terminalSettlementPollingAdaptor{
started: settlementStarted,
release: releaseSettlement,
}
previousAdaptorFactory := service.GetTaskAdaptorFunc
service.GetTaskAdaptorFunc = func(constant.TaskPlatform) service.TaskPollingAdaptor {
return adaptor
}
t.Cleanup(func() { service.GetTaskAdaptorFunc = previousAdaptorFactory })
deps := pluginProtocolTestDeps()
deps.submit = func(_ *gin.Context, info *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
info.Billing = billing
return &taskSubmissionOutcome{
Result: &relay.TaskSubmitResult{},
Task: &task,
RelayInfo: info,
}, nil
}
deps.loadTask = func(ctx context.Context, _ int, _ constant.TaskPlatform, _ string) (*model.Task, bool, error) {
close(observationStarted)
<-ctx.Done()
return nil, false, ctx.Err()
}
done := make(chan struct{})
go func() {
<-observationStarted
defer close(pollingDone)
service.DispatchPlatformUpdate(
context.Background(),
task.Platform,
map[int][]string{channel.Id: {"upstream-terminal"}},
map[string]*model.Task{"upstream-terminal": &task},
)
}()
go func() {
defer close(done)
serveTaskPluginProtocol(c, pinned, deps)
}()
select {
case <-settlementStarted:
case <-time.After(2 * time.Second):
require.FailNow(t, "terminal settlement did not start")
}
cancel()
select {
case <-done:
case <-time.After(2 * time.Second):
require.FailNow(t, "protocol observation did not stop after terminal disconnect")
}
assert.Equal(t, []string{"response.created"}, pluginProtocolTestSSEEventTypes(recorder.Body.String()))
assert.Zero(t, billing.refunds)
close(releaseSettlement)
select {
case <-pollingDone:
case <-time.After(2 * time.Second):
require.FailNow(t, "terminal settlement was canceled with the client observation")
}
var persisted model.Task
require.NoError(t, database.Where("task_id = ?", task.TaskID).First(&persisted).Error)
assert.Equal(t, model.TaskStatus(model.TaskStatusSuccess), persisted.Status)
assert.Equal(t, "100%", persisted.Progress)
assert.Equal(t, 10, persisted.Quota)
assert.True(t, adaptor.completed)
assert.Empty(t, billingEvents)
}
type terminalSettlementPollingAdaptor struct {
started chan struct{}
release chan struct{}
completed bool
}
func (a *terminalSettlementPollingAdaptor) Init(*relaycommon.RelayInfo) {}
func (a *terminalSettlementPollingAdaptor) FetchTask(string, string, *model.Task, string) (*http.Response, error) {
return &http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(`{}`)),
}, nil
}
func (a *terminalSettlementPollingAdaptor) ParseTaskResult(*model.Task, *http.Response, []byte) (*relaycommon.TaskInfo, error) {
return &relaycommon.TaskInfo{
Status: model.TaskStatusSuccess,
Progress: "100%",
}, nil
}
func (a *terminalSettlementPollingAdaptor) AdjustBillingOnComplete(task *model.Task, _ *relaycommon.TaskInfo) int {
close(a.started)
<-a.release
a.completed = true
return task.Quota
}
func TestPluginProtocolBridgeBoundsDatabaseReadBelowHeartbeat(t *testing.T) {
deps := pluginProtocolBridgeDeps{
observationTimeout: time.Minute,
loadTimeout: 10 * time.Second,
tickInterval: time.Second,
heartbeatInterval: 4 * time.Second,
admissionTimeout: time.Second,
}.withDefaults()
assert.Equal(t, 2*time.Second, deps.loadTimeout)
}
func TestServeTaskPluginProtocolPostDurableObservationFailureUsesCanonicalResponse(t *testing.T) {
pinned := compilePluginProtocolTestEndpoint(t, "observation-failure", `
export const protocols = {openai_responses: {
renderEvents: function() { return {events: [], done: false}; },
renderFinal: function() { return {}; }
}};
`)
c, recorder := newPluginProtocolTestContext(false, false)
deps := pluginProtocolTestDeps()
deps.submit = func(_ *gin.Context, info *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
return pluginProtocolTestOutcome(info, pinned.Plugin.Meta.Key, "task_observation_failure", nil), nil
}
deps.loadTask = func(context.Context, int, constant.TaskPlatform, string) (*model.Task, bool, error) {
return nil, false, errors.New("database-secret https://database.invalid/?token=hidden")
}
serveTaskPluginProtocol(c, pinned, deps)
assert.Equal(t, http.StatusOK, recorder.Code)
var response dto.PluginResponsesResponse
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
assert.Equal(t, "failed", response.Status)
require.NotNil(t, response.Error)
assert.Equal(t, "server_error", response.Error.Code)
assert.Equal(t, "The task could not be observed.", response.Error.Message)
assert.Equal(t, "queued", response.Metadata["task_status"])
assert.Equal(t, "/v1/responses/resp_observation_failure", response.Metadata["retrieval_path"])
assert.NotContains(t, recorder.Body.String(), "secret")
assert.NotContains(t, recorder.Body.String(), "database.invalid")
}
func TestServeTaskPluginProtocolStreamsPinnedGenerationWithHostFraming(t *testing.T) {
oldPinned := compilePluginProtocolTestEndpoint(t, "generation-pinned", `
export const protocols = {openai_responses: {
renderEvents: function(ctx, task, previousState) {
if (ctx.stream !== true || ctx.body.value.stream !== true) {
throw new Error("host did not preserve parsed stream mode");
}
if (arguments.length === 2) {
return {events: [], state: null, done: false};
}
if (arguments.length !== 3 || previousState !== null) {
throw new Error("explicit null state was not supplied on the next tick");
}
return {events: [{type: "output", data: "old-generation"}], done: true};
},
renderFinal: function() { throw new Error("stream called renderFinal"); }
}};
`)
newPinned := compilePluginProtocolTestEndpoint(t, "generation-pinned", `
export const protocols = {openai_responses: {
renderEvents: function() {
return {events: [{type: "output", data: "new-generation"}], done: true};
},
renderFinal: function() { return "new-generation"; }
}};
`)
require.NotSame(t, oldPinned.Plugin.Engine, newPinned.Plugin.Engine)
c, recorder := newPluginProtocolTestContext(true, true)
c.Set(pluginruntime.ContextKeyRouteRequest, pluginruntime.RouteRequestContext{
Path: "/v1/responses",
Method: http.MethodPost,
RequestBody: map[string]any{"model": "video-model", "stream": false},
})
loadCount := 0
deps := pluginProtocolTestDeps()
deps.submit = func(_ *gin.Context, info *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
return pluginProtocolTestOutcome(info, oldPinned.Plugin.Meta.Key, "task_generation", map[string]any{
"client_response": "ignored",
}), nil
}
deps.loadTask = func(_ context.Context, userID int, platform constant.TaskPlatform, taskID string) (*model.Task, bool, error) {
loadCount++
assert.Equal(t, 71, userID)
assert.Equal(t, constant.TaskPlatform(oldPinned.Plugin.Meta.Key), platform)
assert.Equal(t, "task_generation", taskID)
status := model.TaskStatus(model.TaskStatusInProgress)
if loadCount == 2 {
status = model.TaskStatus(model.TaskStatusSuccess)
}
return &model.Task{
TaskID: taskID,
UserId: userID,
Platform: platform,
Status: status,
}, true, nil
}
serveTaskPluginProtocol(c, oldPinned, deps)
assert.Equal(t, 2, loadCount)
assert.True(t, recorder.Flushed)
assert.Equal(t, "text/event-stream", recorder.Header().Get("Content-Type"))
assert.Equal(t, []string{
"response.created",
"response.output_item.added",
"response.content_part.added",
"response.output_text.delta",
"response.output_text.done",
"response.content_part.done",
"response.output_item.done",
"response.completed",
}, pluginProtocolTestSSEEventTypes(recorder.Body.String()))
assert.True(t, strings.HasPrefix(recorder.Body.String(), "event: response.created\ndata: {"))
assert.Contains(t, recorder.Body.String(), `"sequence_number":0`)
assert.Contains(t, recorder.Body.String(), `"sequence_number":7`)
assert.Contains(t, recorder.Body.String(), "old-generation")
assert.NotContains(t, recorder.Body.String(), "new-generation")
}
func TestServeTaskPluginProtocolStreamMissingRenderEventsUsesFailureEnvelope(t *testing.T) {
tests := []struct {
name string
status model.TaskStatus
}{
{name: "success", status: model.TaskStatusSuccess},
{name: "failure", status: model.TaskStatusFailure},
}
for _, testCase := range tests {
t.Run(testCase.name, func(t *testing.T) {
pinned := compilePluginProtocolTestEndpoint(t, "default-events-"+testCase.name, `
export const protocols = {openai_responses: {
renderFinal: function() { throw new Error("stream must not call renderFinal"); }
}};
`)
c, recorder := newPluginProtocolTestContext(true, true)
deps := pluginProtocolTestDeps()
deps.submit = func(_ *gin.Context, info *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
return pluginProtocolTestOutcome(info, pinned.Plugin.Meta.Key, "task_default_events"), nil
}
deps.loadTask = func(context.Context, int, constant.TaskPlatform, string) (*model.Task, bool, error) {
return &model.Task{TaskID: "task_default_events", Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key), UserId: 71, Status: testCase.status}, true, nil
}
serveTaskPluginProtocol(c, pinned, deps)
assert.Equal(t, []string{"response.created", "response.failed"}, pluginProtocolTestSSEEventTypes(recorder.Body.String()))
assert.NotContains(t, recorder.Body.String(), "stream must not call")
})
}
}
func TestServeTaskPluginProtocolStreamInjectsHostArtifactCapabilities(t *testing.T) {
pinned := compilePluginProtocolTestEndpoint(t, "stream-artifacts", `
export function listArtifacts(task) {
if (task.data.output.video_url !== "https://upstream.invalid/video.mp4?secret=hidden") {
throw new Error("listArtifacts did not receive raw Task.Data");
}
return [{key: "video", type: "video", mimeType: "video/mp4"}];
}
export function buildContentRequest() {
throw new Error("rendering must not resolve provider content");
}
export const protocols = {openai_responses: {
renderEvents: function(ctx, task) {
const artifact = ctx.artifacts && ctx.artifacts.video;
if (!artifact || artifact.key !== "video" || artifact.type !== "video" ||
artifact.mimeType !== "video/mp4") {
throw new Error("host artifact context is invalid");
}
return {events: [{type: "output", data: artifact.url}], done: true};
},
renderFinal: function() { throw new Error("stream called renderFinal"); }
}};
`)
c, recorder := newPluginProtocolTestContext(true, true)
deps := pluginProtocolTestDeps()
deps.submit = func(_ *gin.Context, info *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
return pluginProtocolTestOutcome(info, pinned.Plugin.Meta.Key, "task_stream_artifact", nil), nil
}
task := &model.Task{
TaskID: "task_stream_artifact",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
Status: model.TaskStatusSuccess,
}
task.SetData(map[string]any{
"output": map[string]any{
"video_url": "https://upstream.invalid/video.mp4?secret=hidden",
},
})
deps.loadTask = func(context.Context, int, constant.TaskPlatform, string) (*model.Task, bool, error) {
return task, true, nil
}
deps.artifactContentURL = func(taskID, artifactKey string) (string, error) {
assert.Equal(t, "task_stream_artifact", taskID)
assert.Equal(t, "video", artifactKey)
return "https://gateway.example/v1/tasks/task_stream_artifact/artifacts/video/content?access=host-capability", nil
}
serveTaskPluginProtocol(c, pinned, deps)
assert.Equal(t, model.TaskStatus(model.TaskStatusSuccess), task.Status)
assert.Equal(t, []string{
"response.created",
"response.output_item.added",
"response.content_part.added",
"response.output_text.delta",
"response.output_text.done",
"response.content_part.done",
"response.output_item.done",
"response.completed",
}, pluginProtocolTestSSEEventTypes(recorder.Body.String()))
assert.Contains(t, recorder.Body.String(), "host-capability")
assert.NotContains(t, recorder.Body.String(), "upstream.invalid")
assert.NotContains(t, recorder.Body.String(), "secret")
}
func TestTaskPluginProtocolHeartbeatDoesNotDispatchEmptySDKEvent(t *testing.T) {
recorder := httptest.NewRecorder()
c, _ := gin.CreateTestContext(recorder)
c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", nil)
require.NoError(t, writeTaskPluginProtocolHeartbeat(c))
assert.Equal(t, ": PING\n", recorder.Body.String())
assert.True(t, recorder.Flushed)
}
func TestServeTaskPluginProtocolNonStreamUsesFinalHookAndHostEnvelope(t *testing.T) {
pinned := compilePluginProtocolTestEndpoint(t, "final-response", `
export const protocols = {openai_responses: {
renderEvents: function() { throw new Error("non-stream called renderEvents"); },
renderFinal: function(ctx, task) {
if (ctx.stream !== false || ctx.body.value.stream !== false) {
throw new Error("host did not preserve parsed non-stream mode");
}
return {
id: "plugin-controlled-id",
status: "plugin-controlled-status",
metadata: {plugin_field: "kept", task_id: "plugin-controlled-task"},
output: [{
id: "plugin-controlled-item",
type: "message",
status: "plugin-controlled-item-status",
role: "assistant",
content: [{
id: "plugin-controlled-content",
type: "output_text",
text: task.data.value,
annotations: [],
logprobs: []
}]
}],
custom_field: "kept"
};
}
}};
`)
c, recorder := newPluginProtocolTestContext(false, false)
c.Set(pluginruntime.ContextKeyRouteRequest, pluginruntime.RouteRequestContext{
Path: "/v1/responses",
Method: http.MethodPost,
RequestBody: map[string]any{"model": "video-model", "stream": true},
})
deps := pluginProtocolTestDeps()
deps.submit = func(_ *gin.Context, info *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
return pluginProtocolTestOutcome(info, pinned.Plugin.Meta.Key, "task_final", map[string]any{
"client_response_secret": "must-be-ignored",
}), nil
}
deps.loadTask = func(context.Context, int, constant.TaskPlatform, string) (*model.Task, bool, error) {
task := &model.Task{
TaskID: "task_final",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
Status: model.TaskStatusSuccess,
}
task.SetData(map[string]any{"value": "plugin-semantic-result"})
return task, true, nil
}
serveTaskPluginProtocol(c, pinned, deps)
assert.Equal(t, http.StatusOK, recorder.Code)
assert.NotEqual(t, "text/event-stream", recorder.Header().Get("Content-Type"))
var response dto.PluginResponsesResponse
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
assert.Equal(t, "resp_final", response.ID)
assert.Equal(t, "response", response.Object)
assert.Equal(t, "completed", response.Status)
assert.Equal(t, "video-model", response.Model)
assert.Equal(t, "task_final", response.Metadata["task_id"])
assert.Equal(t, "kept", response.Metadata["plugin_field"])
require.Len(t, response.Output, 1)
assert.Equal(t, "item_task_final_0", response.Output[0].ID)
assert.Equal(t, "completed", response.Output[0].Status)
require.Len(t, response.Output[0].Content, 1)
assert.Equal(t, "content_task_final_0_0", response.Output[0].Content[0].ID)
assert.Equal(t, "plugin-semantic-result", response.Output[0].Content[0].Text)
assert.Contains(t, recorder.Body.String(), `"custom_field":"kept"`)
assert.NotContains(t, recorder.Body.String(), "plugin-controlled-id")
assert.NotContains(t, recorder.Body.String(), "client_response_secret")
}
func TestServeTaskPluginProtocolNonStreamInjectsHostArtifactCapabilities(t *testing.T) {
pinned := compilePluginProtocolTestEndpoint(t, "final-artifacts", `
export function listArtifacts() {
return [{key: "video", type: "video"}];
}
export function buildContentRequest() {
throw new Error("rendering must not resolve provider content");
}
export const protocols = {openai_responses: {
renderEvents: function() { throw new Error("non-stream called renderEvents"); },
renderFinal: function(ctx) {
const artifact = ctx.artifacts && ctx.artifacts.video;
if (!artifact || artifact.key !== "video" || artifact.type !== "video" ||
Object.prototype.hasOwnProperty.call(artifact, "mimeType")) {
throw new Error("host artifact context is invalid");
}
return {
output: [{
type: "message",
status: "completed",
role: "assistant",
content: [{
type: "output_text",
text: artifact.url,
annotations: [],
logprobs: []
}]
}]
};
}
}};
`)
c, recorder := newPluginProtocolTestContext(false, false)
deps := pluginProtocolTestDeps()
deps.submit = func(_ *gin.Context, info *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
return pluginProtocolTestOutcome(info, pinned.Plugin.Meta.Key, "task_final_artifact", nil), nil
}
task := &model.Task{
TaskID: "task_final_artifact",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
Status: model.TaskStatusSuccess,
}
deps.loadTask = func(context.Context, int, constant.TaskPlatform, string) (*model.Task, bool, error) {
return task, true, nil
}
deps.artifactContentURL = func(taskID, artifactKey string) (string, error) {
assert.Equal(t, "task_final_artifact", taskID)
assert.Equal(t, "video", artifactKey)
return "https://gateway.example/v1/tasks/task_final_artifact/artifacts/video/content?access=host-capability", nil
}
serveTaskPluginProtocol(c, pinned, deps)
assert.Equal(t, model.TaskStatus(model.TaskStatusSuccess), task.Status)
var response dto.PluginResponsesResponse
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
assert.Equal(t, "completed", response.Status)
require.Len(t, response.Output, 1)
require.Len(t, response.Output[0].Content, 1)
assert.Contains(t, response.Output[0].Content[0].Text, "host-capability")
}
func TestServeTaskPluginProtocolArtifactURLFailureOnlyFailsCurrentRendering(t *testing.T) {
for _, stream := range []bool{false, true} {
t.Run(strconv.FormatBool(stream), func(t *testing.T) {
pinned := compilePluginProtocolTestEndpoint(t, "artifact-url-failure-"+strconv.FormatBool(stream), `
export function listArtifacts() {
return [{key: "video", type: "video"}];
}
export function buildContentRequest() {
throw new Error("unused");
}
export const protocols = {openai_responses: {
renderEvents: function() {
return {events: [{type: "output", data: "must-not-render"}], done: true};
},
renderFinal: function() {
return {output: []};
}
}};
`)
c, recorder := newPluginProtocolTestContext(stream, stream)
deps := pluginProtocolTestDeps()
deps.submit = func(_ *gin.Context, info *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
return pluginProtocolTestOutcome(info, pinned.Plugin.Meta.Key, "task_capability_failure", nil), nil
}
task := &model.Task{
TaskID: "task_capability_failure",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
Status: model.TaskStatusSuccess,
}
deps.loadTask = func(context.Context, int, constant.TaskPlatform, string) (*model.Task, bool, error) {
return task, true, nil
}
deps.artifactContentURL = func(string, string) (string, error) {
return "", errors.New("public address is unavailable")
}
serveTaskPluginProtocol(c, pinned, deps)
assert.Equal(t, model.TaskStatus(model.TaskStatusSuccess), task.Status)
assert.NotContains(t, recorder.Body.String(), "must-not-render")
assert.NotContains(t, recorder.Body.String(), "public address")
if stream {
assert.Equal(t, []string{"response.created", "response.failed"}, pluginProtocolTestSSEEventTypes(recorder.Body.String()))
} else {
var response dto.PluginResponsesResponse
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
assert.Equal(t, "failed", response.Status)
require.NotNil(t, response.Error)
assert.Equal(t, "server_error", response.Error.Code)
assert.Equal(t, "completed", response.Metadata["task_status"])
}
})
}
}
func TestServeTaskPluginProtocolNonStreamTaskFailureSkipsFinalHook(t *testing.T) {
logs := make([]string, 0, 1)
pinned := compilePluginProtocolTestEndpointWithOptions(t, "failed-final", `
export const protocols = {openai_responses: {
renderEvents: function() { return {events: [], done: false}; },
renderFinal: function() {
console.log("renderFinal called");
return {
output: [{id: "secret-id", content: [{text: "plugin-secret"}]}],
secret: "https://secret.invalid/"
};
}
}};
`, pluginruntime.Options{
Log: func(message string) { logs = append(logs, message) },
})
c, recorder := newPluginProtocolTestContext(false, false)
deps := pluginProtocolTestDeps()
deps.artifactContentURL = func(string, string) (string, error) {
require.FailNow(t, "failed tasks must not project artifact URLs")
return "", nil
}
deps.submit = func(_ *gin.Context, info *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
return pluginProtocolTestOutcome(info, pinned.Plugin.Meta.Key, "task_failed", map[string]any{
"credential": "client-response-secret",
}), nil
}
deps.loadTask = func(context.Context, int, constant.TaskPlatform, string) (*model.Task, bool, error) {
task := &model.Task{
TaskID: "task_failed",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
Status: model.TaskStatusFailure,
FailReason: "upstream credential at https://secret.invalid/",
}
task.SetData(map[string]any{"secret": "database-secret"})
return task, true, nil
}
serveTaskPluginProtocol(c, pinned, deps)
assert.Equal(t, http.StatusOK, recorder.Code)
var response dto.PluginResponsesResponse
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
assert.Equal(t, "failed", response.Status)
require.NotNil(t, response.Error)
assert.Equal(t, "server_error", response.Error.Code)
assert.Equal(t, "The task failed.", response.Error.Message)
assert.Empty(t, response.Output)
assert.NotContains(t, recorder.Body.String(), "secret")
assert.NotContains(t, recorder.Body.String(), "credential")
assert.Empty(t, logs)
}
func TestServeTaskPluginProtocolStreamTaskFailureSuppressesPluginAndDatabaseDetails(t *testing.T) {
pinned := compilePluginProtocolTestEndpoint(t, "failed-stream", `
export const protocols = {openai_responses: {
renderEvents: function() {
return {
events: [{type: "output", data: "plugin-secret https://plugin.invalid/?key=hidden"}],
done: true
};
},
renderFinal: function() { return "unused-secret"; }
}};
`)
c, recorder := newPluginProtocolTestContext(true, false)
deps := pluginProtocolTestDeps()
deps.artifactContentURL = func(string, string) (string, error) {
require.FailNow(t, "failed tasks must not project artifact URLs")
return "", nil
}
deps.submit = func(_ *gin.Context, info *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
return pluginProtocolTestOutcome(info, pinned.Plugin.Meta.Key, "task_stream_failed", nil), nil
}
deps.loadTask = func(context.Context, int, constant.TaskPlatform, string) (*model.Task, bool, error) {
task := &model.Task{
TaskID: "task_stream_failed",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
Status: model.TaskStatusFailure,
FailReason: "database-secret https://database.invalid/?token=hidden",
}
task.SetData(map[string]any{"secret": "private-result"})
return task, true, nil
}
serveTaskPluginProtocol(c, pinned, deps)
assert.Equal(t, []string{"response.created", "response.failed"}, pluginProtocolTestSSEEventTypes(recorder.Body.String()))
assert.Contains(t, recorder.Body.String(), `"code":"server_error"`)
assert.Contains(t, recorder.Body.String(), `"message":"The task failed."`)
assert.Contains(t, recorder.Body.String(), `"task_status":"failed"`)
assert.NotContains(t, recorder.Body.String(), "secret")
assert.NotContains(t, recorder.Body.String(), "invalid")
assert.NotContains(t, recorder.Body.String(), "hidden")
}
func TestServeTaskPluginProtocolRejectsUnsupportedProtocolBeforeSubmission(t *testing.T) {
pinned := compilePluginProtocolTestEndpoint(t, "unsupported-protocol", `
export const protocols = {openai_responses: {
renderEvents: function() { return {events: [], done: false}; },
renderFinal: function() { return {}; }
}};
`)
pinned.Protocol = "unsupported"
c, recorder := newPluginProtocolTestContext(false, false)
c.Set(pluginruntime.ContextKeyProtocolRequest, pluginruntime.ProtocolRequestContext{
RouteRequestContext: pluginruntime.RouteRequestContext{
Path: "/v1/videos",
Method: http.MethodPost,
RequestBody: map[string]any{"model": "video-model"},
},
Protocol: pinned.Protocol,
})
submitted := false
deps := pluginProtocolTestDeps()
deps.submit = func(*gin.Context, *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
submitted = true
return nil, nil
}
serveTaskPluginProtocol(c, pinned, deps)
assert.False(t, submitted)
assert.Equal(t, http.StatusNotImplemented, recorder.Code)
assert.Contains(t, recorder.Body.String(), `"code":"task_protocol_not_available"`)
}
func TestServeTaskPluginProtocolRejectsObservationAdmissionBeforeSubmission(t *testing.T) {
pinned := compilePluginProtocolTestEndpoint(t, "admission-limit", `
export const protocols = {openai_responses: {
renderEvents: function() { return {events: [], done: false}; },
renderFinal: function() { return {}; }
}};
`)
for _, stream := range []bool{false, true} {
t.Run(map[bool]string{false: "non-stream", true: "stream"}[stream], func(t *testing.T) {
c, recorder := newPluginProtocolTestContext(stream, stream)
submitted := false
deps := pluginProtocolTestDeps()
deps.admissions = newPluginProtocolObservationLimiter(pluginProtocolObservationLimits{
global: 0,
perPlugin: 1,
perUser: 1,
perToken: 1,
})
deps.submit = func(*gin.Context, *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
submitted = true
return nil, nil
}
serveTaskPluginProtocol(c, pinned, deps)
assert.False(t, submitted)
assert.Equal(t, http.StatusTooManyRequests, recorder.Code)
assert.Contains(t, recorder.Body.String(), `"code":"rate_limit_exceeded"`)
assert.NotEqual(t, "text/event-stream", recorder.Header().Get("Content-Type"))
})
}
}
func TestServeTaskPluginProtocolBackgroundNonStreamReturnsPendingWithoutObservation(t *testing.T) {
pinned := compilePluginProtocolTestEndpoint(t, "background-create", `
export const protocols = {openai_responses: {
renderEvents: function() { throw new Error("background create called renderEvents"); },
renderFinal: function() { throw new Error("background create called renderFinal"); }
}};
`)
c, recorder := newPluginProtocolTestContext(false, false)
setProtocolRequestBackground(c, true)
loadCalls := 0
deps := pluginProtocolTestDeps()
deps.submit = func(_ *gin.Context, info *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
return pluginProtocolTestOutcome(info, pinned.Plugin.Meta.Key, "task_background", nil), nil
}
deps.loadTask = func(context.Context, int, constant.TaskPlatform, string) (*model.Task, bool, error) {
loadCalls++
return nil, false, errors.New("observation must not start for background create")
}
serveTaskPluginProtocol(c, pinned, deps)
assert.Equal(t, 0, loadCalls)
assert.Equal(t, http.StatusOK, recorder.Code)
var response map[string]any
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
assert.Equal(t, "resp_background", response["id"])
assert.Equal(t, "response", response["object"])
assert.Equal(t, "queued", response["status"])
assert.Equal(t, true, response["background"])
assert.Nil(t, response["completed_at"])
assert.Nil(t, response["error"])
assert.Nil(t, response["usage"])
assert.Empty(t, response["output"])
metadata, ok := response["metadata"].(map[string]any)
require.True(t, ok)
assert.Equal(t, "task_background", metadata["task_id"])
assert.Equal(t, "queued", metadata["task_status"])
assert.Equal(t, "/v1/responses/resp_background", metadata["retrieval_path"])
assert.NotEqual(t, "text/event-stream", recorder.Header().Get("Content-Type"))
}
func TestServeTaskPluginProtocolBackgroundStreamEntersObservation(t *testing.T) {
pinned := compilePluginProtocolTestEndpoint(t, "background-stream", `
export const protocols = {openai_responses: {
renderEvents: function() { return {events: [{type: "output", data: "streamed"}], done: true}; },
renderFinal: function() { throw new Error("stream called renderFinal"); }
}};
`)
c, recorder := newPluginProtocolTestContext(true, true)
setProtocolRequestBackground(c, true)
loadCalls := 0
deps := pluginProtocolTestDeps()
deps.submit = func(_ *gin.Context, info *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
return pluginProtocolTestOutcome(info, pinned.Plugin.Meta.Key, "task_background_stream", nil), nil
}
deps.loadTask = func(context.Context, int, constant.TaskPlatform, string) (*model.Task, bool, error) {
loadCalls++
return &model.Task{
TaskID: "task_background_stream",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
Status: model.TaskStatusSuccess,
}, true, nil
}
serveTaskPluginProtocol(c, pinned, deps)
assert.Greater(t, loadCalls, 0)
assert.Equal(t, []string{
"response.created",
"response.output_item.added",
"response.content_part.added",
"response.output_text.delta",
"response.output_text.done",
"response.content_part.done",
"response.output_item.done",
"response.completed",
}, pluginProtocolTestSSEEventTypes(recorder.Body.String()))
}
func TestRetrieveTaskPluginResponsePendingSkipsRenderFinal(t *testing.T) {
logs := make([]string, 0, 1)
pinned := compilePluginProtocolRetrieveEndpoint(t, "retrieve-pending", `
export const protocols = {openai_responses: {
renderEvents: function() { throw new Error("retrieve pending called renderEvents"); },
renderFinal: function() {
console.log("renderFinal called");
return {};
}
}};
`, logsAppender(&logs))
c, recorder := newPluginProtocolRetrieveContext("resp_retrieve_pending")
deps := pluginProtocolRetrieveDeps(pinned, &model.Task{
TaskID: "task_retrieve_pending",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
Status: model.TaskStatusInProgress,
PrivateData: model.TaskPrivateData{
ResponsesBackground: true,
},
Properties: model.Properties{OriginModelName: "video-model"},
CreatedAt: 1_710_000_000,
}, true, nil)
retrieveTaskPluginResponse(c, deps)
assert.Empty(t, logs)
assert.Equal(t, http.StatusOK, recorder.Code)
var response map[string]any
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
assert.Equal(t, "resp_retrieve_pending", response["id"])
assert.Equal(t, "in_progress", response["status"])
assert.Equal(t, "video-model", response["model"])
assert.Equal(t, true, response["background"])
assert.Nil(t, response["completed_at"])
assert.Empty(t, response["output"])
metadata, ok := response["metadata"].(map[string]any)
require.True(t, ok)
assert.Equal(t, "/v1/responses/resp_retrieve_pending", metadata["retrieval_path"])
}
func TestRetrieveTaskPluginResponseEchoesOriginModelName(t *testing.T) {
pinned := compilePluginProtocolRetrieveEndpoint(t, "retrieve-alias-echo", `
export const protocols = {openai_responses: {
renderEvents: function() { throw new Error("pending retrieve called renderEvents"); },
renderFinal: function() { throw new Error("pending retrieve called renderFinal"); }
}};
`, pluginruntime.Options{})
c, recorder := newPluginProtocolRetrieveContext("resp_retrieve_alias")
deps := pluginProtocolRetrieveDeps(pinned, &model.Task{
TaskID: "task_retrieve_alias",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
Status: model.TaskStatusInProgress,
Properties: model.Properties{OriginModelName: "alias-model"},
CreatedAt: 1_710_000_000,
}, true, nil)
retrieveTaskPluginResponse(c, deps)
assert.Equal(t, http.StatusOK, recorder.Code)
var response map[string]any
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
assert.Equal(t, "alias-model", response["model"])
}
func TestRetrieveTaskPluginResponseHidesDiscardedResult(t *testing.T) {
pinned := compilePluginProtocolRetrieveEndpoint(t, "retrieve-discarded", `
export const protocols = {openai_responses: {
renderEvents: function() { throw new Error("discarded retrieve called renderEvents"); },
renderFinal: function() { throw new Error("discarded retrieve called renderFinal"); }
}};
`, pluginruntime.Options{})
c, recorder := newPluginProtocolRetrieveContext("resp_retrieve_discarded")
deps := pluginProtocolRetrieveDeps(pinned, &model.Task{
TaskID: "task_retrieve_discarded",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
Status: model.TaskStatusSuccess,
PrivateData: model.TaskPrivateData{ResultDiscarded: true},
CreatedAt: 1_710_000_000,
}, true, nil)
retrieveTaskPluginResponse(c, deps)
assert.Equal(t, http.StatusNotFound, recorder.Code)
assert.Contains(t, recorder.Body.String(), `"not_found"`)
}
func TestRetrieveTaskPluginResponseSuccessRendersFinal(t *testing.T) {
logs := make([]string, 0, 1)
pinned := compilePluginProtocolRetrieveEndpoint(t, "retrieve-success", `
export const protocols = {openai_responses: {
renderEvents: function() { throw new Error("retrieve success called renderEvents"); },
renderFinal: function() {
console.log("renderFinal called");
return {
output: [{
type: "message",
status: "completed",
role: "assistant",
content: [{type: "output_text", text: "retrieved-final", annotations: [], logprobs: []}]
}]
};
}
}};
`, logsAppender(&logs))
c, recorder := newPluginProtocolRetrieveContext("resp_retrieve_success")
deps := pluginProtocolRetrieveDeps(pinned, &model.Task{
TaskID: "task_retrieve_success",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
Status: model.TaskStatusSuccess,
Properties: model.Properties{OriginModelName: "video-model"},
CreatedAt: 1_710_000_000,
}, true, nil)
retrieveTaskPluginResponse(c, deps)
require.NotEmpty(t, logs)
assert.Contains(t, logs[0], "renderFinal called")
assert.Equal(t, http.StatusOK, recorder.Code)
var response dto.PluginResponsesResponse
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
assert.Equal(t, "completed", response.Status)
assert.Equal(t, "resp_retrieve_success", response.ID)
require.Len(t, response.Output, 1)
require.Len(t, response.Output[0].Content, 1)
assert.Equal(t, "retrieved-final", response.Output[0].Content[0].Text)
}
func TestRetrieveTaskPluginResponseStreamOnlySuccessSynthesizesFromEvents(t *testing.T) {
logs := make([]string, 0, 1)
pinned := compilePluginProtocolRetrieveEndpoint(t, "retrieve-stream-only", `
export const protocols = {openai_responses: {
renderEvents: function() {
console.log("renderEvents called");
return {events: [{type: "output", data: "synthesized-retrieve"}], done: true};
},
renderFinal: function() { throw new Error("stream-only retrieve called renderFinal"); }
}};
`, logsAppender(&logs))
pinned.Plugin.Meta.Protocols = []pluginruntime.ProtocolClaim{{Name: "openai_responses", Supports: []string{"stream"}}}
c, recorder := newPluginProtocolRetrieveContext("resp_retrieve_stream")
deps := pluginProtocolRetrieveDeps(pinned, &model.Task{
TaskID: "task_retrieve_stream",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
Status: model.TaskStatusSuccess,
Properties: model.Properties{OriginModelName: "video-model"},
CreatedAt: 1_710_000_000,
}, true, nil)
retrieveTaskPluginResponse(c, deps)
require.NotEmpty(t, logs)
assert.Contains(t, logs[0], "renderEvents called")
assert.Equal(t, http.StatusOK, recorder.Code)
var response dto.PluginResponsesResponse
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
assert.Equal(t, "completed", response.Status)
require.NotEmpty(t, response.Output)
}
func TestRetrieveTaskPluginResponseStreamOnlyPendingAndFailureStayHostEnvelopes(t *testing.T) {
logs := make([]string, 0, 1)
pinned := compilePluginProtocolRetrieveEndpoint(t, "retrieve-stream-envelope", `
export const protocols = {openai_responses: {
renderEvents: function() { throw new Error("envelope retrieve called renderEvents"); },
renderFinal: function() { throw new Error("envelope retrieve called renderFinal"); }
}};
`, logsAppender(&logs))
pinned.Plugin.Meta.Protocols = []pluginruntime.ProtocolClaim{{Name: "openai_responses", Supports: []string{"stream"}}}
t.Run("pending", func(t *testing.T) {
logs = logs[:0]
c, recorder := newPluginProtocolRetrieveContext("resp_retrieve_stream_pending")
deps := pluginProtocolRetrieveDeps(pinned, &model.Task{
TaskID: "task_retrieve_stream_pending",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
Status: model.TaskStatusInProgress,
Properties: model.Properties{OriginModelName: "video-model"},
CreatedAt: 1_710_000_000,
}, true, nil)
retrieveTaskPluginResponse(c, deps)
assert.Empty(t, logs)
assert.Equal(t, http.StatusOK, recorder.Code)
var response map[string]any
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
assert.Equal(t, "in_progress", response["status"])
assert.Empty(t, response["output"])
})
t.Run("failure", func(t *testing.T) {
logs = logs[:0]
c, recorder := newPluginProtocolRetrieveContext("resp_retrieve_stream_failure")
deps := pluginProtocolRetrieveDeps(pinned, &model.Task{
TaskID: "task_retrieve_stream_failure",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
Status: model.TaskStatusFailure,
Properties: model.Properties{OriginModelName: "video-model"},
}, true, nil)
retrieveTaskPluginResponse(c, deps)
assert.Empty(t, logs)
assert.Equal(t, http.StatusOK, recorder.Code)
var response dto.PluginResponsesResponse
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
assert.Equal(t, "failed", response.Status)
require.NotNil(t, response.Error)
assert.Equal(t, "The task failed.", response.Error.Message)
})
}
func TestRetrieveTaskPluginResponseStreamOnlyRenderErrorUsesFailureEnvelope(t *testing.T) {
pinned := compilePluginProtocolRetrieveEndpoint(t, "retrieve-stream-throw", `
export const protocols = {openai_responses: {
renderEvents: function() { throw new Error("retrieve boom"); },
renderFinal: function() { throw new Error("stream-only retrieve called renderFinal"); }
}};
`, pluginruntime.Options{})
pinned.Plugin.Meta.Protocols = []pluginruntime.ProtocolClaim{{Name: "openai_responses", Supports: []string{"stream"}}}
c, recorder := newPluginProtocolRetrieveContext("resp_retrieve_stream_throw")
deps := pluginProtocolRetrieveDeps(pinned, &model.Task{
TaskID: "task_retrieve_stream_throw",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
Status: model.TaskStatusSuccess,
Properties: model.Properties{OriginModelName: "video-model"},
CreatedAt: 1_710_000_000,
}, true, nil)
retrieveTaskPluginResponse(c, deps)
assert.Equal(t, http.StatusOK, recorder.Code)
assert.NotContains(t, recorder.Body.String(), "retrieve boom")
var response dto.PluginResponsesResponse
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
assert.Equal(t, "failed", response.Status)
require.NotNil(t, response.Error)
assert.Equal(t, "server_error", response.Error.Code)
assert.Equal(t, "The task could not be observed.", response.Error.Message)
}
func TestRetrieveTaskPluginResponseFailureUsesFailedEnvelope(t *testing.T) {
logs := make([]string, 0, 1)
pinned := compilePluginProtocolRetrieveEndpoint(t, "retrieve-failure", `
export const protocols = {openai_responses: {
renderEvents: function() { throw new Error("retrieve failure called renderEvents"); },
renderFinal: function() {
console.log("renderFinal called");
return {};
}
}};
`, logsAppender(&logs))
c, recorder := newPluginProtocolRetrieveContext("resp_retrieve_failure")
deps := pluginProtocolRetrieveDeps(pinned, &model.Task{
TaskID: "task_retrieve_failure",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
Status: model.TaskStatusFailure,
Properties: model.Properties{OriginModelName: "video-model"},
}, true, nil)
retrieveTaskPluginResponse(c, deps)
assert.Empty(t, logs)
assert.Equal(t, http.StatusOK, recorder.Code)
var response dto.PluginResponsesResponse
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
assert.Equal(t, "failed", response.Status)
require.NotNil(t, response.Error)
assert.Equal(t, "server_error", response.Error.Code)
assert.Equal(t, "The task failed.", response.Error.Message)
}
func TestRetrieveTaskPluginResponseNotFound(t *testing.T) {
pinned := compilePluginProtocolRetrieveEndpoint(t, "retrieve-404", `
export const protocols = {openai_responses: {
renderFinal: function() { return {}; }
}};
`, pluginruntime.Options{})
owned := &model.Task{
TaskID: "task_owned",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
UserId: 71,
Status: model.TaskStatusInProgress,
Properties: model.Properties{OriginModelName: "video-model"},
}
tests := []struct {
name string
responseID string
userID int
task *model.Task
exists bool
plugin *pluginruntime.LoadedPlugin
claims []pluginruntime.ProtocolClaim
}{
{name: "bad prefix", responseID: "task_owned", userID: 71, task: owned, exists: true, plugin: pinned.Plugin},
{name: "missing", responseID: "resp_missing", userID: 71, exists: false, plugin: pinned.Plugin},
{name: "other user", responseID: "resp_owned", userID: 99, task: owned, exists: false, plugin: pinned.Plugin},
{name: "no plugin", responseID: "resp_owned", userID: 71, task: owned, exists: true},
{name: "plugin does not claim protocol", responseID: "resp_owned", userID: 71, task: owned, exists: true, plugin: pinned.Plugin, claims: []pluginruntime.ProtocolClaim{{Name: "openai_video"}}},
}
for _, testCase := range tests {
t.Run(testCase.name, func(t *testing.T) {
if testCase.plugin != nil {
if testCase.claims != nil {
testCase.plugin.Meta.Protocols = testCase.claims
} else {
testCase.plugin.Meta.Protocols = []pluginruntime.ProtocolClaim{{Name: "openai_responses", Supports: []string{"stream", "sync", "background"}}}
}
}
c, recorder := newPluginProtocolRetrieveContext(testCase.responseID)
common.SetContextKey(c, constant.ContextKeyUserId, testCase.userID)
deps := pluginProtocolRetrieveDeps(pinned, testCase.task, testCase.exists, nil)
if testCase.plugin == nil {
deps.resolvePlugin = func(constant.TaskPlatform) (*pluginruntime.LoadedPlugin, *pluginruntime.RoutingGeneration, bool) {
return nil, nil, false
}
}
retrieveTaskPluginResponse(c, deps)
assert.Equal(t, http.StatusNotFound, recorder.Code)
assert.Contains(t, recorder.Body.String(), `"code":"not_found"`)
assert.Contains(t, recorder.Body.String(), "No response found with id '"+testCase.responseID+"'.")
})
}
}
func TestRespondPluginProtocolSubmissionErrorPassesValidationMessage(t *testing.T) {
pinned := compilePluginProtocolTestEndpoint(t, "protocol-validation-detail", `
export const protocols = {openai_responses: {
renderEvents: function() { return {events: [], done: false}; },
renderFinal: function() { return {}; }
}};
`)
c, recorder := newPluginProtocolTestContext(false, false)
deps := pluginProtocolTestDeps()
deps.submit = func(*gin.Context, *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
return nil, &dto.TaskError{
Code: "invalid_request",
Message: "model is required",
StatusCode: http.StatusBadRequest,
LocalError: true,
}
}
serveTaskPluginProtocol(c, pinned, deps)
assert.Equal(t, http.StatusBadRequest, recorder.Code)
assert.Contains(t, recorder.Body.String(), `"message":"model is required"`)
assert.Contains(t, recorder.Body.String(), `"code":"invalid_request_error"`)
assert.NotContains(t, recorder.Body.String(), "Invalid task protocol request")
}
func TestRespondPluginProtocolSubmissionErrorKeepsGenericNonValidation400(t *testing.T) {
pinned := compilePluginProtocolTestEndpoint(t, "protocol-generic-400", `
export const protocols = {openai_responses: {
renderEvents: function() { return {events: [], done: false}; },
renderFinal: function() { return {}; }
}};
`)
c, recorder := newPluginProtocolTestContext(false, false)
deps := pluginProtocolTestDeps()
deps.submit = func(*gin.Context, *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
return nil, &dto.TaskError{
Code: "task_not_exist",
Message: "task_origin_not_exist",
StatusCode: http.StatusBadRequest,
LocalError: true,
}
}
serveTaskPluginProtocol(c, pinned, deps)
assert.Equal(t, http.StatusBadRequest, recorder.Code)
assert.Contains(t, recorder.Body.String(), "Invalid task protocol request")
assert.NotContains(t, recorder.Body.String(), "task_origin_not_exist")
}
func compilePluginProtocolTestEndpoint(t *testing.T, key, source string) pluginruntime.PinnedEndpoint {
t.Helper()
return compilePluginProtocolTestEndpointWithOptions(t, key, source, pluginruntime.Options{})
}
func compilePluginProtocolTestEndpointWithOptions(
t *testing.T,
key string,
source string,
options pluginruntime.Options,
) pluginruntime.PinnedEndpoint {
t.Helper()
options.Key = key
options.Version = "1.0.0"
options.Concurrency = 1
engine, err := pluginruntime.Compile(source, options)
require.NoError(t, err)
return pluginruntime.PinnedEndpoint{
Generation: &pluginruntime.RoutingGeneration{Number: 41},
Plugin: &pluginruntime.LoadedPlugin{
Meta: pluginruntime.Meta{
Key: key,
Version: "1.0.0",
Protocols: []pluginruntime.ProtocolClaim{{
Name: "openai_responses",
Supports: []string{"stream", "sync", "background"},
}},
},
Engine: engine,
},
Protocol: "openai_responses",
Operation: pluginruntime.HostProtocolOperation{Name: "create", Methods: []string{http.MethodPost}, Path: "/v1/responses", BodyKinds: []pluginruntime.BodyKind{pluginruntime.BodyJSON}, ModelField: "model"},
Model: "video-model",
}
}
func newPluginProtocolTestContext(stream, requestBodyStream bool) (*gin.Context, *httptest.ResponseRecorder) {
recorder := httptest.NewRecorder()
c, _ := gin.CreateTestContext(recorder)
c.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", strings.NewReader(`{}`))
common.SetContextKey(c, constant.ContextKeyUserId, 71)
common.SetContextKey(c, constant.ContextKeyTokenId, 81)
common.SetContextKey(c, constant.ContextKeyUsingGroup, "default")
c.Set("resolved_task_model", "video-model")
c.Set(pluginruntime.ContextKeyProtocolRequest, pluginruntime.ProtocolRequestContext{
RouteRequestContext: pluginruntime.RouteRequestContext{
Path: "/v1/responses",
Method: http.MethodPost,
Params: map[string]string{},
Query: map[string][]string{},
Body: map[string]any{
"kind": "json",
"value": map[string]any{"model": "video-model", "stream": requestBodyStream},
},
RequestBody: map[string]any{
"model": "video-model",
"stream": requestBodyStream,
},
},
Protocol: "openai_responses",
Stream: stream,
})
return c, recorder
}
func pluginProtocolTestDeps() pluginProtocolBridgeDeps {
return pluginProtocolBridgeDeps{
now: func() time.Time { return time.Unix(1_710_000_000, 0) },
admissions: newPluginProtocolObservationLimiter(defaultPluginProtocolObservationLimits),
protocolLimits: relay.DefaultPluginProtocolLimits(),
observationTimeout: time.Hour,
tickInterval: time.Nanosecond,
tickJitter: 0,
heartbeatInterval: time.Hour,
admissionTimeout: time.Second,
}
}
func pluginProtocolTestOutcome(
info *relaycommon.RelayInfo,
pluginKey string,
taskID string,
_ ...any,
) *taskSubmissionOutcome {
return &taskSubmissionOutcome{
Result: &relay.TaskSubmitResult{},
Task: &model.Task{
TaskID: taskID,
Platform: constant.TaskPlatform(pluginKey),
UserId: info.UserId,
Status: model.TaskStatusSubmitted,
CreatedAt: 1_710_000_000,
},
RelayInfo: info,
}
}
func pluginProtocolTestSSEEventTypes(body string) []string {
lines := strings.Split(body, "\n")
events := make([]string, 0)
for _, line := range lines {
if after, ok := strings.CutPrefix(line, "event: "); ok {
events = append(events, after)
}
}
return events
}
func setProtocolRequestBackground(c *gin.Context, background bool) {
request := c.MustGet(pluginruntime.ContextKeyProtocolRequest).(pluginruntime.ProtocolRequestContext)
if body, ok := request.Body.(map[string]any); ok {
if value, ok := body["value"].(map[string]any); ok {
value["background"] = background
}
}
}
func compilePluginProtocolRetrieveEndpoint(t *testing.T, key, source string, options pluginruntime.Options) pluginruntime.PinnedEndpoint {
t.Helper()
pinned := compilePluginProtocolTestEndpointWithOptions(t, key, source, options)
pinned.Plugin.Meta.Protocols = []pluginruntime.ProtocolClaim{{Name: "openai_responses", Supports: []string{"stream", "sync", "background"}}}
return pinned
}
func logsAppender(logs *[]string) pluginruntime.Options {
if logs == nil {
return pluginruntime.Options{}
}
return pluginruntime.Options{
Log: func(message string) { *logs = append(*logs, message) },
}
}
func newPluginProtocolRetrieveContext(responseID string) (*gin.Context, *httptest.ResponseRecorder) {
recorder := httptest.NewRecorder()
c, _ := gin.CreateTestContext(recorder)
c.Request = httptest.NewRequest(http.MethodGet, "/v1/responses/"+responseID, nil)
c.Params = gin.Params{{Key: "response_id", Value: responseID}}
common.SetContextKey(c, constant.ContextKeyUserId, 71)
common.SetContextKey(c, constant.ContextKeyTokenId, 81)
return c, recorder
}
func pluginProtocolRetrieveDeps(pinned pluginruntime.PinnedEndpoint, task *model.Task, exists bool, err error) pluginProtocolBridgeDeps {
deps := pluginProtocolTestDeps()
deps.getByTaskId = func(userId int, taskId string) (*model.Task, bool, error) {
if !exists {
return nil, false, err
}
if task != nil && (userId != task.UserId || taskId != task.TaskID) {
return nil, false, err
}
return task, task != nil, err
}
deps.resolvePlugin = func(constant.TaskPlatform) (*pluginruntime.LoadedPlugin, *pluginruntime.RoutingGeneration, bool) {
if pinned.Plugin == nil {
return nil, nil, false
}
return pinned.Plugin, pinned.Generation, true
}
return deps
}
const imageProtocolTestPlugin = `
export const meta = {apiVersion:1,key:"image-bridge",name:"Image Bridge",version:"1.0.0",author:{name:"Test"},models:["image-model"],fetchMode:"per_task",protocols:["openai_image"]};
export function buildSubmitRequest(ctx){return {url:ctx.baseUrl+"/images"};}
export function parseSubmitResponse(ctx,resp){return {taskId:"vendor"};}
export function buildQueryRequest(){return {url:"https://provider.example"};}
export function parseTaskResult(){return {status:"SUCCESS"};}
export const protocols = {openai_image: {
decodeRequest: function(ctx) { return {kind:"submit", model: ctx.model, requestBody: ctx.body.value}; },
render: function(ctx, task) {
const data = (task.data && task.data.images || []).map(function(url) { return {url: url}; });
return {data: data, vendor: "test"};
},
}};
`
func imageProtocolTestEndpoint(t *testing.T) pluginruntime.PinnedEndpoint {
t.Helper()
engine, err := pluginruntime.Compile(imageProtocolTestPlugin, pluginruntime.Options{Key: "image-bridge", Version: "1.0.0", Concurrency: 1})
require.NoError(t, err)
return pluginruntime.PinnedEndpoint{
Generation: &pluginruntime.RoutingGeneration{Number: 7},
Plugin: &pluginruntime.LoadedPlugin{Meta: pluginruntime.Meta{Key: "image-bridge", Version: "1.0.0", Protocols: []pluginruntime.ProtocolClaim{{Name: pluginruntime.ProtocolOpenAIImage}}}, Engine: engine},
Protocol: pluginruntime.ProtocolOpenAIImage,
Operation: pluginruntime.HostProtocolOperation{Name: "generate", Methods: []string{http.MethodPost}, Path: "/v1/images/generations", BodyKinds: []pluginruntime.BodyKind{pluginruntime.BodyJSON}, ModelField: "model"},
Model: "image-model",
}
}
func newImageProtocolTestContext(responseFormat string) (*gin.Context, *httptest.ResponseRecorder) {
recorder := httptest.NewRecorder()
c, _ := gin.CreateTestContext(recorder)
c.Request = httptest.NewRequest(http.MethodPost, "/v1/images/generations", strings.NewReader(`{}`))
common.SetContextKey(c, constant.ContextKeyUserId, 71)
common.SetContextKey(c, constant.ContextKeyTokenId, 81)
common.SetContextKey(c, constant.ContextKeyUsingGroup, "default")
c.Set("resolved_task_model", "image-model")
value := map[string]any{"model": "image-model", "prompt": "a cat", "n": 2}
if responseFormat != "" {
value["response_format"] = responseFormat
}
c.Set(pluginruntime.ContextKeyProtocolRequest, pluginruntime.ProtocolRequestContext{
RouteRequestContext: pluginruntime.RouteRequestContext{Path: "/v1/images/generations", Method: http.MethodPost, Params: map[string]string{}, Query: map[string][]string{}, Body: map[string]any{"kind": "json", "value": value}, RequestBody: value},
Protocol: pluginruntime.ProtocolOpenAIImage,
Operation: "generate",
Model: "image-model",
})
return c, recorder
}
func imageProtocolTestOutcome(info *relaycommon.RelayInfo, status model.TaskStatus, images ...string) *taskSubmissionOutcome {
task := &model.Task{TaskID: "task_image", Platform: constant.TaskPlatform("image-bridge"), UserId: info.UserId, ChannelId: 3, Status: status, CreatedAt: 1_710_000_000}
task.PrivateData.UpstreamTaskID = "vendor"
if len(images) > 0 {
task.SetData(map[string]any{"images": images})
}
return &taskSubmissionOutcome{Result: &relay.TaskSubmitResult{}, Task: task, RelayInfo: info}
}
// The OpenAI Images protocol answers synchronously: an immediate result is
// rendered by the plugin, `created` is host-filled, and response_format
// b64_json inlines each URL while keeping the URL.
func TestServeTaskPluginImageProtocolRendersImmediateResultAndInlinesBase64(t *testing.T) {
for _, tc := range []struct {
name, responseFormat string
downloadErr error
wantB64 []any
}{
{"url", "", nil, []any{nil, nil}},
{"b64_json", "b64_json", nil, []any{"QUFB", "QUFB"}},
{"b64_json download failure keeps the URL", "b64_json", errors.New("blocked"), []any{nil, nil}},
} {
t.Run(tc.name, func(t *testing.T) {
pinned := imageProtocolTestEndpoint(t)
c, recorder := newImageProtocolTestContext(tc.responseFormat)
deps := pluginProtocolTestDeps()
downloads := 0
deps.downloadImage = func(url string) (string, string, error) {
downloads++
if tc.downloadErr != nil {
return "", "", tc.downloadErr
}
return "image/png", "QUFB", nil
}
deps.submit = func(_ *gin.Context, info *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
return imageProtocolTestOutcome(info, model.TaskStatusSuccess, "https://cdn.example/1.png", "https://cdn.example/2.png"), nil
}
serveTaskPluginImageProtocol(c, pinned, deps)
require.Equal(t, http.StatusOK, recorder.Code, recorder.Body.String())
var response map[string]any
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
assert.Equal(t, float64(1_710_000_000), response["created"])
assert.Equal(t, "test", response["vendor"], "plugin fields are preserved")
data, ok := response["data"].([]any)
require.True(t, ok)
require.Len(t, data, 2)
for index, entry := range data {
item := entry.(map[string]any)
assert.Equal(t, fmt.Sprintf("https://cdn.example/%d.png", index+1), item["url"])
assert.Equal(t, tc.wantB64[index], item["b64_json"])
}
if tc.responseFormat == "b64_json" {
assert.Equal(t, 2, downloads)
} else {
assert.Zero(t, downloads)
}
})
}
}
// An asynchronous vendor task is polled inside the request until it is
// terminal; a failure and the protocol timeout become OpenAI error envelopes
// while the durable task stays with the background poller.
func TestServeTaskPluginImageProtocolWaitsForAsynchronousTask(t *testing.T) {
for _, tc := range []struct {
name string
terminal model.TaskStatus
wantStatus int
wantCode string
wantMessage string
}{
{"success", model.TaskStatusSuccess, http.StatusOK, "", ""},
{"failure", model.TaskStatusFailure, http.StatusBadRequest, "image_generation_failed", "DataInspectionFailed"},
{"timeout", "", http.StatusGatewayTimeout, "task_timeout", "still running"},
} {
t.Run(tc.name, func(t *testing.T) {
pinned := imageProtocolTestEndpoint(t)
c, recorder := newImageProtocolTestContext("")
common.SetContextKey(c, constant.ContextKeyChannelBaseUrl, "https://private.example.com")
deps := pluginProtocolTestDeps()
deps.imagePollInterval = time.Millisecond
deps.submissionTimeout = 50 * time.Millisecond
polls := 0
deps.pollTask = func(_ context.Context, task *model.Task) error {
polls++
if polls == 1 {
return errors.New("transient poll failure")
}
if polls < 3 || tc.terminal == "" {
return nil
}
task.Status = tc.terminal
task.FailReason = "DataInspectionFailed: see https://private.example.com/jobs/1"
task.SetData(map[string]any{"images": []string{"https://cdn.example/polled.png"}})
return nil
}
deps.submit = func(_ *gin.Context, info *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
return imageProtocolTestOutcome(info, model.TaskStatusSubmitted), nil
}
serveTaskPluginImageProtocol(c, pinned, deps)
require.Equal(t, tc.wantStatus, recorder.Code, recorder.Body.String())
var response map[string]any
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
if tc.wantStatus == http.StatusOK {
assert.GreaterOrEqual(t, polls, 3)
assert.Equal(t, []any{map[string]any{"url": "https://cdn.example/polled.png"}}, response["data"])
return
}
errorBody := response["error"].(map[string]any)
assert.Equal(t, tc.wantCode, errorBody["code"])
assert.Contains(t, errorBody["message"], tc.wantMessage)
assert.NotContains(t, errorBody["message"], "private.example.com")
})
}
}
// Submission failures use the OpenAI error envelope and surface a DashScope
// {code, message} failure body as a readable vendor message.
func TestServeTaskPluginImageProtocolSubmissionErrorUsesOpenAIEnvelope(t *testing.T) {
pinned := imageProtocolTestEndpoint(t)
c, recorder := newImageProtocolTestContext("")
deps := pluginProtocolTestDeps()
deps.submit = func(*gin.Context, *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
return nil, service.TaskErrorWrapper(errors.New(`{"code":"InvalidParameter","message":"n must be 1","request_id":"r"}`), "fail_to_fetch_task", http.StatusBadRequest)
}
serveTaskPluginImageProtocol(c, pinned, deps)
require.Equal(t, http.StatusBadRequest, recorder.Code)
var response map[string]any
require.NoError(t, common.Unmarshal(recorder.Body.Bytes(), &response))
errorBody := response["error"].(map[string]any)
assert.Equal(t, "InvalidParameter", errorBody["code"])
assert.Equal(t, "invalid_request_error", errorBody["type"])
assert.Contains(t, errorBody["message"], "n must be 1")
assert.NotContains(t, errorBody["message"], "request_id")
}
// A synchronous image submission is billable the moment DashScope accepts it.
// A client that disconnects while the upstream call is in flight must not
// cancel that call or refund a durable result: the task row, its settlement
// and its consume log stay, and nothing is written to the closed connection.
func TestServeTaskPluginImageProtocolDisconnectDuringSubmissionKeepsDurableSettlement(t *testing.T) {
events := make([]string, 0, 3)
database := setupTaskSubmissionDatabase(t, true, &events)
previousLogConsumeEnabled := common.LogConsumeEnabled
common.LogConsumeEnabled = false
t.Cleanup(func() { common.LogConsumeEnabled = previousLogConsumeEnabled })
pinned := imageProtocolTestEndpoint(t)
c, recorder := newImageProtocolTestContext("")
c.Set(pluginruntime.ContextKeyPinnedEndpoint, pinned)
requestContext, cancel := context.WithCancel(c.Request.Context())
c.Request = c.Request.WithContext(requestContext)
billing := &taskSubmissionTestBilling{events: &events}
submitStarted := make(chan struct{})
releaseSubmit := make(chan struct{})
done := make(chan struct{})
polls := 0
deps := pluginProtocolTestDeps()
deps.pollTask = func(context.Context, *model.Task) error {
polls++
return nil
}
deps.submit = func(c *gin.Context, info *relaycommon.RelayInfo) (*taskSubmissionOutcome, *dto.TaskError) {
info.Billing = billing
info.TaskRelayInfo.PublicTaskID = "task_image_disconnect"
info.TaskRelayInfo.LockedChannel = &model.Channel{Id: 1, Type: constant.ChannelTypeTaskPlugin, Name: "image-bridge"}
info.ChannelMeta = &relaycommon.ChannelMeta{ChannelId: 1, ChannelType: constant.ChannelTypeTaskPlugin}
return executeTaskSubmissionWith(c, info, func(c *gin.Context, _ *relaycommon.RelayInfo) (*relay.TaskSubmitResult, *dto.TaskError) {
close(submitStarted)
<-releaseSubmit
// The upstream call runs on the request context: a context that
// followed the client would abort here and refund.
if requestErr := c.Request.Context().Err(); requestErr != nil {
return nil, service.TaskErrorWrapperLocal(requestErr, "request_cancelled", http.StatusRequestTimeout)
}
return &relay.TaskSubmitResult{
UpstreamTaskID: "task_image_disconnect",
Platform: constant.TaskPlatform(pinned.Plugin.Meta.Key),
Quota: 7,
TaskData: []byte(`{"images":["https://cdn.example/1.png"]}`),
Immediate: &relaycommon.TaskInfo{Status: model.TaskStatusSuccess, Progress: "100%"},
}, nil
})
}
go func() {
defer close(done)
serveTaskPluginImageProtocol(c, pinned, deps)
}()
select {
case <-submitStarted:
case <-time.After(2 * time.Second):
require.FailNow(t, "submission did not start")
}
cancel()
close(releaseSubmit)
select {
case <-done:
case <-time.After(2 * time.Second):
require.FailNow(t, "detached submission did not finish")
}
assert.Equal(t, []string{"reserve", "insert", "settle"}, events)
assert.Zero(t, billing.refunds)
var persisted model.Task
require.NoError(t, database.Where("task_id = ?", "task_image_disconnect").First(&persisted).Error)
assert.Equal(t, model.TaskStatus(model.TaskStatusSuccess), persisted.Status)
assert.Equal(t, 7, persisted.Quota)
assert.True(t, persisted.PrivateData.ResultDiscarded, "the inline image result is not retained after the barrier")
assert.Zero(t, polls, "an immediate result is never polled")
assert.False(t, c.Writer.Written())
assert.Empty(t, recorder.Body.String())
}