mirror of
https://github.com/hanzoai/dashscope-go-sdk.git
synced 2026-09-30 21:39:25 +00:00
197 lines
5.0 KiB
Go
197 lines
5.0 KiB
Go
package qwen
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"strconv"
|
|
"strings"
|
|
|
|
httpclient "github.com/hanzoai/dashscope-go-sdk/httpclient"
|
|
)
|
|
|
|
//nolint:lll
|
|
func SendMessage[T IQwenContent](ctx context.Context, payload *Request[T], cli httpclient.IHttpClient, url, token string) (*OutputResponse[T], error) {
|
|
if payload.Model == "" {
|
|
return nil, ErrModelNotSet
|
|
}
|
|
|
|
resp := OutputResponse[T]{}
|
|
tokenOpt := httpclient.WithTokenHeaderOption(token)
|
|
|
|
header := map[string]string{
|
|
"Content-Type": "application/json",
|
|
}
|
|
if payload.HasUploadOss {
|
|
header["X-DashScope-OssResourceResolve"] = "enable"
|
|
}
|
|
|
|
headerOpt := httpclient.WithHeader(header)
|
|
|
|
err := cli.Post(ctx, url, payload, &resp, tokenOpt, headerOpt)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(resp.Output.Choices) == 0 {
|
|
return nil, ErrEmptyResponse
|
|
}
|
|
return &resp, nil
|
|
}
|
|
|
|
//nolint:lll
|
|
func SendMessageStream[T IQwenContent](ctx context.Context, payload *Request[T], cli httpclient.IHttpClient, url, token string) (*OutputResponse[T], error) {
|
|
if payload.Model == "" {
|
|
return nil, ErrModelNotSet
|
|
}
|
|
|
|
header := map[string]string{
|
|
"Accept": "text/event-stream",
|
|
"Content-Type": "application/json",
|
|
}
|
|
|
|
if payload.HasUploadOss {
|
|
header["X-DashScope-OssResourceResolve"] = "enable"
|
|
}
|
|
|
|
responseChan := asyncChatStreaming(ctx, payload, header, cli, url, token)
|
|
|
|
return iterateStreamChannel(ctx, responseChan, payload.StreamingFn)
|
|
}
|
|
|
|
func iterateStreamChannel[T IQwenContent](ctx context.Context, channel <-chan StreamOutput[T], fn StreamingFunc) (*OutputResponse[T], error) {
|
|
outputMessage := OutputResponse[T]{}
|
|
for rspData := range channel {
|
|
if rspData.Err != nil {
|
|
return nil, &httpclient.HTTPRequestError{Message: "SSE Error: ", Cause: rspData.Err}
|
|
}
|
|
if len(rspData.Output.Output.Choices) == 0 {
|
|
return nil, ErrEmptyResponse
|
|
}
|
|
|
|
chunk := rspData.Output.Output.Choices[0].Message.Content.ToBytes()
|
|
|
|
if err := fn(ctx, chunk); err != nil {
|
|
return nil, &WrapMessageError{Message: "StreamingFunc Error", Cause: err}
|
|
}
|
|
|
|
outputMessage.RequestID = rspData.Output.RequestID
|
|
outputMessage.Usage = rspData.Output.Usage
|
|
if outputMessage.Output.Choices == nil {
|
|
outputMessage.Output.Choices = rspData.Output.Output.Choices
|
|
} else {
|
|
choice := outputMessage.Output.Choices[0]
|
|
choice.Message.Role = rspData.Output.Output.Choices[0].Message.Role
|
|
choice.Message.Content.AppendText(rspData.Output.Output.Choices[0].Message.Content.ToString())
|
|
choice.FinishReason = rspData.Output.Output.Choices[0].FinishReason
|
|
|
|
outputMessage.Output.Choices[0] = choice
|
|
}
|
|
}
|
|
|
|
return &outputMessage, nil
|
|
}
|
|
|
|
//nolint:lll
|
|
func asyncChatStreaming[T IQwenContent](
|
|
ctx context.Context,
|
|
payload *Request[T],
|
|
header map[string]string,
|
|
cli httpclient.IHttpClient,
|
|
url, token string,
|
|
) <-chan StreamOutput[T] {
|
|
chanBuffer := 100
|
|
_respChunkChannel := make(chan StreamOutput[T], chanBuffer)
|
|
|
|
go func() {
|
|
_combineStreamingChunk(ctx, payload, header, _respChunkChannel, cli, url, token)
|
|
}()
|
|
return _respChunkChannel
|
|
}
|
|
|
|
/*
|
|
* combine SSE streaming lines to be a structed response data
|
|
* id: xxxx
|
|
* event: xxxxx
|
|
* ......
|
|
*/
|
|
func _combineStreamingChunk[T IQwenContent](
|
|
ctx context.Context,
|
|
payload *Request[T],
|
|
header map[string]string,
|
|
_respChunkChannel chan StreamOutput[T],
|
|
cli httpclient.IHttpClient,
|
|
url string,
|
|
token string,
|
|
) {
|
|
defer close(_respChunkChannel)
|
|
var _rawStreamOutChannel chan string
|
|
|
|
var err error
|
|
headerOpt := httpclient.WithHeader(header)
|
|
tokenOpt := httpclient.WithTokenHeaderOption(token)
|
|
|
|
_rawStreamOutChannel, err = cli.PostSSE(ctx, url, payload, headerOpt, tokenOpt)
|
|
if err != nil {
|
|
_respChunkChannel <- StreamOutput[T]{Err: err}
|
|
return
|
|
}
|
|
|
|
rsp := StreamOutput[T]{}
|
|
|
|
for v := range _rawStreamOutChannel {
|
|
if strings.TrimSpace(v) == "" {
|
|
// streaming out combined response
|
|
_respChunkChannel <- rsp
|
|
rsp = StreamOutput[T]{}
|
|
continue
|
|
}
|
|
|
|
err = fillInRespData(v, &rsp)
|
|
if err != nil {
|
|
rsp.Err = err
|
|
_respChunkChannel <- rsp
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// filled in response data line by line.
|
|
func fillInRespData[T IQwenContent](line string, output *StreamOutput[T]) error {
|
|
if strings.TrimSpace(line) == "" {
|
|
return nil
|
|
}
|
|
|
|
switch {
|
|
case strings.HasPrefix(line, "id:"):
|
|
output.ID = strings.TrimPrefix(line, "id:")
|
|
case strings.HasPrefix(line, "event:"):
|
|
output.Event = strings.TrimPrefix(line, "event:")
|
|
case strings.HasPrefix(line, ":HTTP_STATUS/"):
|
|
code, err := strconv.Atoi(strings.TrimPrefix(line, ":HTTP_STATUS/"))
|
|
if err != nil {
|
|
output.Err = fmt.Errorf("http_status err: strconv.Atoi %w", err)
|
|
}
|
|
output.HTTPStatus = code
|
|
case strings.HasPrefix(line, "data:"):
|
|
dataJSON := strings.TrimPrefix(line, "data:")
|
|
if output.Event == "error" {
|
|
output.Err = &WrapMessageError{Message: dataJSON}
|
|
return nil
|
|
}
|
|
outputData := OutputResponse[T]{}
|
|
err := json.Unmarshal([]byte(dataJSON), &outputData)
|
|
if err != nil {
|
|
return &WrapMessageError{Message: "unmarshal OutputData Err", Cause: err}
|
|
}
|
|
|
|
output.Output = outputData
|
|
default:
|
|
data := bytes.TrimSpace([]byte(line))
|
|
log.Printf("unknown line: %s", data)
|
|
}
|
|
|
|
return nil
|
|
}
|