Files
cloudpods/pkg/aiproxy/extensions/visual/responses.go
Zexi Li cdc8ab78b5 feat(aiproxy): move visual provider to model columns and support streaming responses (#25144)
Promote visual_provider_id/visual_model_key to dedicated ai_model columns,
force non-stream upstream orchestration for visual paths, and synthesize SSE
chunks for streaming Responses requests that include images.
2026-07-14 18:39:30 +08:00

104 lines
3.1 KiB
Go

// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package visual
import (
"context"
"fmt"
"yunion.io/x/jsonutils"
"yunion.io/x/onecloud/pkg/mcclient/auth"
"yunion.io/x/onecloud/pkg/aiproxy/models"
"yunion.io/x/onecloud/pkg/aiproxy/providers/openai"
)
// ShouldHandle reports whether the Responses visual orchestration path should run.
// Streaming requests with images use non-streaming orchestration and synthetic SSE (moon-bridge pattern).
func ShouldHandle(dict *jsonutils.JSONDict, up *models.ChatUpstream, isStream bool) bool {
_ = isStream
if dict == nil || up == nil || !Enabled(up) {
return false
}
return openai.ResponsesInputHasImage(dict)
}
// HandleResponsesCreateChat runs visual orchestration and returns the upstream chat completion body.
func HandleResponsesCreateChat(
ctx context.Context,
dict *jsonutils.JSONDict,
textUp *models.ChatUpstream,
) ([]byte, *openai.ResponsesConvertState, error) {
runtime, visCfg := RuntimeConfigFromModel(textUp.ModelConfig)
if visCfg == nil || !visCfg.Enabled {
return nil, nil, fmt.Errorf("visual config is missing")
}
userCred := auth.AdminCredential()
vk, err := models.LoadEnabledVirtualKeyById(textUp.VirtualKeyId)
if err != nil {
return nil, nil, err
}
visUp, err := models.ResolveVisualUpstream(ctx, userCred, vk, textUp.VisualProviderId, textUp.VisualModelKey)
if err != nil {
return nil, nil, err
}
chatBody, state, err := openai.ResponsesToChatCompletions(dict, textUp.UpstreamModel)
if err != nil {
return nil, nil, err
}
chatClone := cloneDict(chatBody)
forceNonStreamChatBody(chatClone)
textUpChat := *textUp
textUpChat.BaseURL = ChatBaseURL(textUp.BaseURL)
visUpChat := *visUp
visUpChat.BaseURL = ChatBaseURL(visUp.BaseURL)
respBody, err := RunChatOrchestrator(ctx, &textUpChat, &visUpChat, chatClone, runtime)
if err != nil {
return nil, nil, err
}
return respBody, state, nil
}
// HandleResponsesCreate runs the visual orchestration loop for a non-streaming Responses request.
func HandleResponsesCreate(
ctx context.Context,
dict *jsonutils.JSONDict,
textUp *models.ChatUpstream,
) ([]byte, *openai.ResponsesConvertState, error) {
respBody, state, err := HandleResponsesCreateChat(ctx, dict, textUp)
if err != nil {
return nil, nil, err
}
out, err := openai.ChatCompletionToResponses(respBody, state)
if err != nil {
return nil, nil, err
}
return out, state, nil
}
func cloneDict(src *jsonutils.JSONDict) *jsonutils.JSONDict {
if src == nil {
return nil
}
parsed, err := jsonutils.Parse([]byte(src.String()))
if err != nil {
return src
}
if d, ok := parsed.(*jsonutils.JSONDict); ok {
return d
}
return src
}