diff --git a/.licenserc.json b/.licenserc.json index 0e3240b..306a4a0 100644 --- a/.licenserc.json +++ b/.licenserc.json @@ -1,6 +1,8 @@ { "**/*.go": "// Copyright 2021 ecodeclub", - "**/*.{yml,yaml,sql}": "# Copyright 2021 ecodeclub", + "**/*.{yml,yaml}": "# Copyright 2021 ecodeclub", + "**/*.{sql}": "-- Copyright 2021 ecodeclub", + "**/*.sh": "# Copyright 2021 ecodeclub", "ignore": [ diff --git a/Makefile b/Makefile index 6751fc1..1b80c38 100644 --- a/Makefile +++ b/Makefile @@ -38,4 +38,11 @@ tidy: check: @$(MAKE) fmt @$(MAKE) tidy - #@$(MAKE) lint \ No newline at end of file + #@$(MAKE) lint + +# 生成gRPC相关文件 +.PHONY: grpc +grpc: + @buf format -w ./api/gen + @buf lint + @buf generate diff --git a/api/gen/ai/v1/ai.pb.go b/api/gen/ai/v1/ai.pb.go index 269d99e..d45fca7 100644 --- a/api/gen/ai/v1/ai.pb.go +++ b/api/gen/ai/v1/ai.pb.go @@ -1,3 +1,17 @@ +// Copyright 2021 ecodeclub +// +// 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. + // Code generated by protoc-gen-go. DO NOT EDIT. // versions: // protoc-gen-go v1.36.6 @@ -25,6 +39,7 @@ type LLMRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` Text string `protobuf:"bytes,2,opt,name=text,proto3" json:"text,omitempty"` + ContentType string `protobuf:"bytes,3,opt,name=contentType,proto3" json:"contentType,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -73,6 +88,13 @@ func (x *LLMRequest) GetText() string { return "" } +func (x *LLMRequest) GetContentType() string { + if x != nil { + return x.ContentType + } + return "" +} + type StreamEvent struct { state protoimpl.MessageState `protogen:"open.v1"` Final bool `protobuf:"varint,1,opt,name=final,proto3" json:"final,omitempty"` @@ -197,11 +219,12 @@ var File_ai_v1_ai_proto protoreflect.FileDescriptor const file_ai_v1_ai_proto_rawDesc = "" + "\n" + - "\x0eai/v1/ai.proto\x12\x05ai.v1\"0\n" + + "\x0eai/v1/ai.proto\x12\x05ai.v1\"R\n" + "\n" + "LLMRequest\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" + - "\x04text\x18\x02 \x01(\tR\x04text\"{\n" + + "\x04text\x18\x02 \x01(\tR\x04text\x12 \n" + + "\vcontentType\x18\x03 \x01(\tR\vcontentType\"{\n" + "\vStreamEvent\x12\x14\n" + "\x05final\x18\x01 \x01(\bR\x05final\x12*\n" + "\x10reasoningContent\x18\x02 \x01(\tR\x10reasoningContent\x12\x18\n" + @@ -212,8 +235,8 @@ const file_ai_v1_ai_proto_rawDesc = "" + "\acontent\x18\x02 \x01(\tR\acontent2o\n" + "\tAIService\x12/\n" + "\x06Invoke\x12\x11.ai.v1.LLMRequest\x1a\x12.ai.v1.LLMResponse\x121\n" + - "\x06Stream\x12\x11.ai.v1.LLMRequest\x1a\x12.ai.v1.StreamEvent0\x01B\x80\x01\n" + - "\tcom.ai.v1B\aAiProtoP\x01Z5github.com/ecodeclub/ai-gateway-go/api/gen/ai/v1;aiv1\xa2\x02\x03AXX\xaa\x02\x05Ai.V1\xca\x02\x05Ai\\V1\xe2\x02\x11Ai\\V1\\GPBMetadata\xea\x02\x06Ai::V1b\x06proto3" + "\x06Stream\x12\x11.ai.v1.LLMRequest\x1a\x12.ai.v1.StreamEvent0\x01B\x86\x01\n" + + "\tcom.ai.v1B\aAiProtoP\x01Z;github.com/ecodeclub/ai-gateway-go/api/proto/gen/ai/v1;aiv1\xa2\x02\x03AXX\xaa\x02\x05Ai.V1\xca\x02\x05Ai\\V1\xe2\x02\x11Ai\\V1\\GPBMetadata\xea\x02\x06Ai::V1b\x06proto3" var ( file_ai_v1_ai_proto_rawDescOnce sync.Once diff --git a/api/gen/ai/v1/ai_grpc.pb.go b/api/gen/ai/v1/ai_grpc.pb.go index afa30b6..210e5dc 100644 --- a/api/gen/ai/v1/ai_grpc.pb.go +++ b/api/gen/ai/v1/ai_grpc.pb.go @@ -1,3 +1,17 @@ +// Copyright 2021 ecodeclub +// +// 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. + // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: // - protoc-gen-go-grpc v1.5.1 diff --git a/api/proto/ai/v1/ai.proto b/api/proto/ai/v1/ai.proto index 07d7528..6a54b65 100644 --- a/api/proto/ai/v1/ai.proto +++ b/api/proto/ai/v1/ai.proto @@ -24,6 +24,7 @@ service AIService { message LLMRequest { string id = 1; string text = 2; + string contentType=3; } message StreamEvent { diff --git a/buf.gen.yaml b/buf.gen.yaml index dd15de3..13b19d2 100644 --- a/buf.gen.yaml +++ b/buf.gen.yaml @@ -1,3 +1,17 @@ +# Copyright 2021 ecodeclub +# +# 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. + version: v2 managed: enabled: true @@ -6,8 +20,8 @@ managed: value: github.com/ecodeclub/ai-gateway-go/api/proto/gen plugins: - remote: buf.build/protocolbuffers/go - out: ./api/proto/gen + out: ./api/gen opt: paths=source_relative - remote: buf.build/grpc/go - out: ./api/proto/gen + out: ./api/gen opt: paths=source_relative \ No newline at end of file diff --git a/buf.yaml b/buf.yaml index 95458e9..82bc92f 100644 --- a/buf.yaml +++ b/buf.yaml @@ -1,3 +1,17 @@ +# Copyright 2021 ecodeclub +# +# 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. + version: v2 modules: - path: api/proto diff --git a/errs/errs.go b/errs/errs.go index f4b0fea..4336a58 100644 --- a/errs/errs.go +++ b/errs/errs.go @@ -19,3 +19,4 @@ import ( ) var ErrBizConfigNotFound = errors.New("biz config not found") +var ErrApiNotSupport = errors.New("api not support") diff --git a/internal/domain/stream.go b/internal/domain/stream.go index 1cfa8c2..f359256 100644 --- a/internal/domain/stream.go +++ b/internal/domain/stream.go @@ -14,9 +14,19 @@ package domain +type ContentType string + +const ( + ContentTypeText ContentType = "text" + ContentTypeVideo ContentType = "video_url" + ContentTypeImage ContentType = "image_url" + ContentTypeAudio ContentType = "audio_url" +) + type LLMRequest struct { - Id string - Text string + Id string + Text string + ContentType ContentType } type StreamEvent struct { ReasoningContent string diff --git a/internal/grpc/server.go b/internal/grpc/server.go index 0e81a1f..d2b951e 100644 --- a/internal/grpc/server.go +++ b/internal/grpc/server.go @@ -34,7 +34,7 @@ func NewServer(svc *service.AIService) *Server { func (server *Server) Invoke(ctx context.Context, r *ai.LLMRequest) (*ai.LLMResponse, error) { resp, err := server.svc.Invoke( ctx, - domain.LLMRequest{Id: r.GetId(), Text: r.GetText()}) + domain.LLMRequest{Id: r.GetId(), Text: r.GetText(), ContentType: domain.ContentType(r.GetContentType())}) if err != nil { return &ai.LLMResponse{}, err @@ -48,7 +48,7 @@ func (server *Server) Stream(r *ai.LLMRequest, resp ai.AIService_StreamServer) e ch, err := server.svc.Stream( ctx, - domain.LLMRequest{Id: r.GetId(), Text: r.GetText()}) + domain.LLMRequest{Id: r.GetId(), Text: r.GetText(), ContentType: domain.ContentType(r.GetContentType())}) if err != nil { return err diff --git a/internal/service/llm/platform/qwen_omni_turbo/omni.go b/internal/service/llm/platform/qwen_omni_turbo/omni.go new file mode 100644 index 0000000..0650bd6 --- /dev/null +++ b/internal/service/llm/platform/qwen_omni_turbo/omni.go @@ -0,0 +1,205 @@ +// Copyright 2021 ecodeclub +// +// 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 qwen_omni_turbo + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" + + "github.com/ecodeclub/ai-gateway-go/errs" + "github.com/ecodeclub/ai-gateway-go/internal/domain" +) + +// Config 配置选项 +type Config struct { + APIKey string + BaseURL string + Timeout time.Duration + MaxRetries int +} + +// DefaultConfig 返回默认配置 +func DefaultConfig(apiKey string) Config { + return Config{ + APIKey: apiKey, + BaseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions", + Timeout: 30 * time.Second, + MaxRetries: 3, + } +} + +type Handler struct { + config Config + client *http.Client +} + +func NewHandler(config Config) *Handler { + return &Handler{ + config: config, + client: &http.Client{ + Timeout: config.Timeout, + }, + } +} + +// StreamHandle 处理流式请求 api:https://help.aliyun.com/zh/model-studio/qwen-omni +func (h *Handler) StreamHandle(ctx context.Context, llmRequest domain.LLMRequest) (chan domain.StreamEvent, error) { + events := make(chan domain.StreamEvent, 10) + + go func() { + defer close(events) + // 创建可取消的上下文(支持超时控制) + ctx1, cancel := context.WithTimeout(ctx, h.config.Timeout) + defer cancel() + + cs := h.buildContent(llmRequest) + messages := []Messages{{Role: "user", Content: cs}} + reqBody := NewSendRequestBuilder(messages).Build(WithModalities([]string{"text", "audio"})) + + marshal, err := json.Marshal(reqBody) + if err != nil { + events <- domain.StreamEvent{Error: fmt.Errorf("序列化请求失败: %v", err)} + return + } + + req, err := h.buildRequest(ctx1, marshal) + if err != nil { + events <- domain.StreamEvent{Error: fmt.Errorf("创建请求失败: %v", err)} + return + } + + resp, err := h.doRequest(req) + if err != nil { + events <- domain.StreamEvent{Error: err} + return + } + defer func() { _ = resp.Body.Close() }() + + h.recv(ctx, events, resp.Body) + }() + + return events, nil +} + +func (h *Handler) buildContent(llmRequest domain.LLMRequest) []Content { + switch llmRequest.ContentType { + case domain.ContentTypeImage: + return []Content{NewImageContent(llmRequest.Text)} + case domain.ContentTypeAudio: + return []Content{NewInputAudioContent(llmRequest.Text, "wav")} + case domain.ContentTypeText: + return []Content{NewTextContent(llmRequest.Text)} + case domain.ContentTypeVideo: + return []Content{NewVideoContent([]string{llmRequest.Text}), NewTextContent("请参考视频内容")} + default: + return []Content{NewTextContent(llmRequest.Text)} + } +} + +func (h *Handler) buildRequest(ctx context.Context, body []byte) (*http.Request, error) { + req, err := http.NewRequestWithContext(ctx, "POST", h.config.BaseURL, bytes.NewReader(body)) + if err != nil { + return nil, err + } + + req.Header.Set("Accept", "application/json") + req.Header.Set("Cache-Control", "no-cache") + req.Header.Set("Connection", "keep-alive") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", h.config.APIKey)) + + return req, nil +} + +func (h *Handler) doRequest(req *http.Request) (*http.Response, error) { + var resp *http.Response + var err error + + for i := 0; i < h.config.MaxRetries; i++ { + resp, err = h.client.Do(req) + if err == nil { + if resp.StatusCode == http.StatusOK { + return resp, nil + } + _ = resp.Body.Close() + } + time.Sleep(time.Second * time.Duration(i+1)) + } + + if err != nil { + return nil, fmt.Errorf("请求失败: %v", err) + } + if resp == nil { + return nil, fmt.Errorf("请求失败: %v", err) + } + return nil, fmt.Errorf("非200响应: %s", resp.Status) +} + +//nolint:cyclop +func (h *Handler) recv(ctx context.Context, eventCh chan domain.StreamEvent, stream io.ReadCloser) { + reader := bufio.NewReader(stream) + for { + if ctx.Err() != nil { + eventCh <- domain.StreamEvent{Error: fmt.Errorf("上下文取消: %v", ctx.Err())} + return + } + + line, err := reader.ReadBytes('\n') + if err != nil { + if err == io.EOF { + eventCh <- domain.StreamEvent{Done: true} + return + } + eventCh <- domain.StreamEvent{Error: fmt.Errorf("读取错误: %v", err)} + return + } + + lineStr := string(line) + if len(lineStr) <= 6 || !strings.HasPrefix(lineStr, "data: ") { + eventCh <- domain.StreamEvent{Error: fmt.Errorf("解析数据异常 %s ", lineStr)} + return + } + + if strings.Contains(lineStr, "data: [DONE]") { + eventCh <- domain.StreamEvent{Done: true} + return + } + + jsonData := strings.TrimSpace(lineStr[6:]) + var resp StreamResponse + if err := json.Unmarshal([]byte(jsonData), &resp); err != nil { + eventCh <- domain.StreamEvent{Error: fmt.Errorf("解析JSON错误: %v ,原始数据为 %s", err, lineStr)} + continue + } + + if len(resp.Choices) > 0 { + if resp.Choices[0].Delta.Content != "" { + eventCh <- domain.StreamEvent{Content: resp.Choices[0].Delta.Content} + } else if resp.Choices[0].Delta.Audio.Transcript != "" { + eventCh <- domain.StreamEvent{Content: resp.Choices[0].Delta.Audio.Transcript} + } + } + } +} + +func (h *Handler) Handle(_ context.Context, _ domain.LLMRequest) (domain.LLMResponse, error) { + return domain.LLMResponse{}, errs.ErrApiNotSupport +} diff --git a/internal/service/llm/platform/qwen_omni_turbo/omni_test.go b/internal/service/llm/platform/qwen_omni_turbo/omni_test.go new file mode 100644 index 0000000..d1e0001 --- /dev/null +++ b/internal/service/llm/platform/qwen_omni_turbo/omni_test.go @@ -0,0 +1,159 @@ +// Copyright 2021 ecodeclub +// +// 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 qwen_omni_turbo + +import ( + "context" + "os" + "testing" + "time" + + "github.com/ecodeclub/ai-gateway-go/internal/domain" +) + +func TestStreamHandle(t *testing.T) { + apiKey := os.Getenv("QWEN_API_KEY") + if apiKey == "" { + t.Skip("QWEN_API_KEY environment variable not set") + } + + config := DefaultConfig(apiKey) + handler := NewHandler(config) + + timeout, cancelFunc := context.WithTimeout(context.Background(), time.Second*10) + defer cancelFunc() + + ch, err := handler.StreamHandle(timeout, domain.LLMRequest{ + Text: "你是谁", + ContentType: ContentTypeText, + }) + if err != nil { + t.Fatalf("StreamHandle failed: %v", err) + } + + for { + select { + case <-timeout.Done(): + t.Log("Test timeout reached") + return + case event := <-ch: + if event.Error != nil { + t.Fatalf("Received error: %v", event.Error) + } + if event.Done { + t.Log("Stream completed successfully") + return + } + t.Logf("Received content: %s", event.Content) + } + } +} + +func TestStreamHandleWithInvalidAPIKey(t *testing.T) { + config := DefaultConfig("invalid-api-key") + handler := NewHandler(config) + + timeout, cancelFunc := context.WithTimeout(context.Background(), time.Second*10) + defer cancelFunc() + + ch, err := handler.StreamHandle(timeout, domain.LLMRequest{ + Text: "你是谁", + ContentType: ContentTypeText, + }) + if err != nil { + t.Fatalf("StreamHandle failed: %v", err) + } + + for { + select { + case <-timeout.Done(): + t.Log("Test timeout reached") + return + case event := <-ch: + if event.Error != nil { + t.Logf("Expected error received: %v", event.Error) + return + } + if event.Done { + t.Fatal("Stream completed unexpectedly with invalid API key") + } + } + } +} + +func TestStreamHandleWithDifferentContentTypes(t *testing.T) { + apiKey := os.Getenv("QWEN_API_KEY") + if apiKey == "" { + t.Skip("QWEN_API_KEY environment variable not set") + } + + config := DefaultConfig(apiKey) + handler := NewHandler(config) + + testCases := []struct { + name string + contentType domain.ContentType + text string + }{ + { + name: "Image Content", + contentType: domain.ContentTypeImage, + text: "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20241022/emyrja/dog_and_girl.jpeg", + }, + { + name: "Audio Content", + contentType: domain.ContentTypeAudio, + text: "https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250211/tixcef/cherry.wav", + }, + { + name: "Video Content", + contentType: domain.ContentTypeVideo, + text: "https://www.bilibili.com/video/BV1RH4y1a7Sg?t=8.3", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + timeout, cancelFunc := context.WithTimeout(context.Background(), time.Second*10) + defer cancelFunc() + + ch, err := handler.StreamHandle(timeout, domain.LLMRequest{ + Text: tc.text, + ContentType: tc.contentType, + }) + if err != nil { + t.Fatalf("StreamHandle failed: %v", err) + } + + for { + select { + case <-timeout.Done(): + t.Logf("Test timeout reached for %s", tc.name) + return + case event := <-ch: + if event.Error != nil { + t.Logf("Received error for %s: %v", tc.name, event.Error) + return + } + if event.Done { + t.Logf("Stream completed successfully for %s", tc.name) + return + } + t.Logf("Received content for %s: %s", tc.name, event.Content) + } + } + }) + } +} diff --git a/internal/service/llm/platform/qwen_omni_turbo/request.go b/internal/service/llm/platform/qwen_omni_turbo/request.go new file mode 100644 index 0000000..5b7428e --- /dev/null +++ b/internal/service/llm/platform/qwen_omni_turbo/request.go @@ -0,0 +1,180 @@ +// Copyright 2021 ecodeclub +// +// 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 qwen_omni_turbo + +type SendRequest struct { + Model string `json:"model"` // defaults to "qwen-omni-turbo" + Messages []Messages `json:"messages,omitempty"` // required, at least one message + Stream bool `json:"stream"` // defaults to true + StreamOptions StreamOptions `json:"stream_options,omitempty"` // optional, defaults to { "include_usage": true } + Modalities []string `json:"modalities,omitempty"` // optional, defaults to ["text"] + Audio Audio `json:"audio,omitempty"` +} + +// SendRequestBuilder 是构建 SendRequest 的 builder +type SendRequestBuilder struct { + request SendRequest +} + +// NewSendRequestBuilder 创建一个新的构建器,设置默认值 +func NewSendRequestBuilder(ms []Messages) *SendRequestBuilder { + return &SendRequestBuilder{ + request: SendRequest{ + Model: "qwen-omni-turbo", + Stream: true, + StreamOptions: StreamOptions{ + IncludeUsage: true, + }, + Messages: ms, + Modalities: []string{"text"}, + Audio: Audio{Voice: "Cherry", Format: "wav"}, + }, + } +} + +// SendRequestOption 选项函数类型 +type SendRequestOption func(*SendRequest) + +// WithModel 设置模型名称选项 +func WithModel(model string) SendRequestOption { + return func(r *SendRequest) { + r.Model = model + } +} + +// WithModalities 设置模态选项 +func WithModalities(modalities []string) SendRequestOption { + return func(r *SendRequest) { + r.Modalities = modalities + } +} + +// WithAudio 设置音频选项 +func WithAudio(audio Audio) SendRequestOption { + return func(r *SendRequest) { + r.Audio = audio + } +} + +// Build 应用所有选项并构建最终的 SendRequest +func (b *SendRequestBuilder) Build(opts ...SendRequestOption) SendRequest { + // 创建副本,避免修改原始结构 + result := b.request + + // 应用所有选项 + for _, opt := range opts { + opt(&result) + } + + return result +} + +type StreamOptions struct { + IncludeUsage bool `json:"include_usage"` // defaults to true +} + +type Audio struct { + Voice string `json:"voice"` + Format string `json:"format"` +} + +type Messages struct { + Role string `json:"role"` // required, e.g., "user", "assistant", "system" + Content []Content `json:"content"` +} + +type Content interface { + GetType() string +} + +const ( + ContentTypeText = "text" + ContentTypeVideo = "video" + ContentTypeImage = "image_url" + ContentTypeInputAudio = "input_audio" +) + +type TextContent struct { + Type string `json:"type"` + Text string `json:"text"` +} + +func (t TextContent) GetType() string { + return t.Type +} + +func NewTextContent(text string) TextContent { + return TextContent{ + Type: ContentTypeText, + Text: text, + } +} + +type VideoContent struct { + Type string `json:"type"` + Video []string `json:"video"` +} + +func (v VideoContent) GetType() string { + return v.Type +} + +func NewVideoContent(video []string) VideoContent { + return VideoContent{ + Type: ContentTypeVideo, + Video: video, + } +} + +type ImageContent struct { + Type string `json:"type"` + ImageUrl ImageUrl `json:"image_url"` +} + +func (i ImageContent) GetType() string { + return i.Type +} + +type ImageUrl struct { + Url string `json:"url"` +} + +func NewImageContent(url string) ImageContent { + return ImageContent{ + Type: ContentTypeImage, + ImageUrl: ImageUrl{Url: url}, + } +} + +type InputAudioContent struct { + Type string `json:"type"` + InputAudio InputAudio `json:"input_audio"` +} + +func (i InputAudioContent) GetType() string { + return i.Type +} + +type InputAudio struct { + Data string `json:"data"` + Format string `json:"format"` +} + +func NewInputAudioContent(data string, fmt string) InputAudioContent { + return InputAudioContent{ + Type: ContentTypeInputAudio, + InputAudio: InputAudio{Data: data, Format: fmt}, + } +} diff --git a/internal/service/llm/platform/qwen_omni_turbo/response.go b/internal/service/llm/platform/qwen_omni_turbo/response.go new file mode 100644 index 0000000..dba3c39 --- /dev/null +++ b/internal/service/llm/platform/qwen_omni_turbo/response.go @@ -0,0 +1,32 @@ +// Copyright 2021 ecodeclub +// +// 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 qwen_omni_turbo + +type StreamResponse struct { + Choices []struct { + Delta struct { + Content string `json:"content"` + Audio struct { + Transcript string `json:"transcript"` + } `json:"audio"` + } `json:"delta"` + FinishReason *string `json:"finish_reason"` + Index int `json:"index"` + } `json:"choices"` + Object string `json:"object"` + Created int64 `json:"created"` + Model string `json:"model"` + ID string `json:"id"` +}