AI 网关切换 OpenAI 兼容面并移除 chat 端点,新增模型黑白名单
This commit is contained in:
+28
-121
@@ -2,13 +2,11 @@ package oci
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/oracle/oci-go-sdk/v65/common"
|
||||
"github.com/oracle/oci-go-sdk/v65/generativeai"
|
||||
"github.com/oracle/oci-go-sdk/v65/generativeaiinference"
|
||||
|
||||
@@ -30,12 +28,6 @@ type GenAiModel struct {
|
||||
Retired *time.Time `json:"retired"`
|
||||
}
|
||||
|
||||
// GenAiStream 逐事件读取流式聊天;Next 在流结束时返回 io.EOF。
|
||||
type GenAiStream interface {
|
||||
Next() (aiwire.ChatChunk, error)
|
||||
Close() error
|
||||
}
|
||||
|
||||
func (c *RealClient) genAiClient(cred Credentials, region string) (generativeai.GenerativeAiClient, error) {
|
||||
gc, err := generativeai.NewGenerativeAiClientWithConfigurationProvider(provider(cred))
|
||||
if err != nil {
|
||||
@@ -152,121 +144,15 @@ func capStrings(caps []generativeai.ModelCapabilityEnum) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
// GenAiChat 实现 Client:非流式聊天;modelOcid 走 on-demand serving,
|
||||
// cohere.* 模型走 COHERE 请求格式,其余走 GENERIC。
|
||||
func (c *RealClient) GenAiChat(ctx context.Context, cred Credentials, region, modelOcid string, ir aiwire.ChatRequest) (*aiwire.ChatResponse, error) {
|
||||
ic, err := c.genAiInferenceClient(cred, region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := buildChatRequest(ir, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := ic.Chat(ctx, chatRequest(cred, modelOcid, req))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("genai chat: %w", err)
|
||||
}
|
||||
return chatResponseToIR(resp.ChatResult.ChatResponse, ir.Model)
|
||||
}
|
||||
|
||||
// buildChatRequest 按模型 vendor 组装底层请求体。
|
||||
func buildChatRequest(ir aiwire.ChatRequest, stream bool) (generativeaiinference.BaseChatRequest, error) {
|
||||
if isCohereModel(ir.Model) {
|
||||
return irToCohereSDK(ir, stream)
|
||||
}
|
||||
return irToSDK(ir, stream), nil
|
||||
}
|
||||
|
||||
// chatResponseToIR 按响应实际形态(GENERIC / COHERE)转回 IR。
|
||||
func chatResponseToIR(resp generativeaiinference.BaseChatResponse, model string) (*aiwire.ChatResponse, error) {
|
||||
switch r := resp.(type) {
|
||||
case generativeaiinference.GenericChatResponse:
|
||||
return sdkToIR(r, model), nil
|
||||
case generativeaiinference.CohereChatResponse:
|
||||
return cohereSDKToIR(r, model), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("genai chat: unexpected response format %T", resp)
|
||||
}
|
||||
}
|
||||
|
||||
func chatRequest(cred Credentials, modelOcid string, req generativeaiinference.BaseChatRequest) generativeaiinference.ChatRequest {
|
||||
return generativeaiinference.ChatRequest{
|
||||
ChatDetails: generativeaiinference.ChatDetails{
|
||||
CompartmentId: &cred.TenancyOCID,
|
||||
ServingMode: generativeaiinference.OnDemandServingMode{ModelId: &modelOcid},
|
||||
ChatRequest: req,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GenAiChatStream 实现 Client:流式聊天,返回逐事件读取器。
|
||||
// SDK 对 text/event-stream 跳过 unmarshal,原始流经 RawResponse 交由 SSEReader 消费。
|
||||
func (c *RealClient) GenAiChatStream(ctx context.Context, cred Credentials, region, modelOcid string, ir aiwire.ChatRequest) (GenAiStream, error) {
|
||||
ic, err := c.genAiInferenceClient(cred, region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := buildChatRequest(ir, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := ic.Chat(ctx, chatRequest(cred, modelOcid, req))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("genai chat stream: %w", err)
|
||||
}
|
||||
reader, err := common.NewSSEReader(resp.RawResponse)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("genai chat stream: sse reader: %w", err)
|
||||
}
|
||||
return &genAiSSEStream{reader: reader, body: resp.RawResponse.Body, model: ir.Model}, nil
|
||||
}
|
||||
|
||||
// genAiSSEStream 把 OCI SSE 事件流适配为 IR chunk 流。
|
||||
type genAiSSEStream struct {
|
||||
reader *common.SseReader
|
||||
body io.ReadCloser
|
||||
model string
|
||||
}
|
||||
|
||||
// Next 读取下一条有效事件;空事件与 [DONE] 哨兵跳过,流尽返回 io.EOF。
|
||||
func (s *genAiSSEStream) Next() (aiwire.ChatChunk, error) {
|
||||
for {
|
||||
data, err := s.reader.ReadNextEvent()
|
||||
if err != nil {
|
||||
return aiwire.ChatChunk{}, err
|
||||
}
|
||||
text := strings.TrimSpace(string(data))
|
||||
if text == "" {
|
||||
continue
|
||||
}
|
||||
if text == "[DONE]" {
|
||||
return aiwire.ChatChunk{}, io.EOF
|
||||
}
|
||||
if chunk, ok := parseGenAiEvent([]byte(text), s.model); ok {
|
||||
return chunk, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *genAiSSEStream) Close() error {
|
||||
if s.body != nil {
|
||||
return s.body.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenAiProbeChat 实现 Client:配额探测专用的最小聊天(maxTokens=1),返回 HTTP 状态码;
|
||||
// GenAiProbeChat 实现 Client:经 OpenAI 兼容面(直通同链路)发一次极小请求探测渠道;配额探测专用的最小聊天(maxTokens=1),返回 HTTP 状态码;
|
||||
// modelName 决定请求格式(cohere.* 走 COHERE)。
|
||||
func (c *RealClient) GenAiProbeChat(ctx context.Context, cred Credentials, region, modelOcid, modelName string) (int, error) {
|
||||
one := 1
|
||||
ir := aiwire.ChatRequest{
|
||||
Model: modelName,
|
||||
Messages: []aiwire.ChatMessage{{Role: "user", Content: aiwire.NewTextContent("hi")}},
|
||||
MaxTokens: &one,
|
||||
body, err := json.Marshal(map[string]any{"model": modelName, "input": "hi",
|
||||
"max_output_tokens": 1, "store": false})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
_, err := c.GenAiChat(ctx, cred, region, modelOcid, ir)
|
||||
if err == nil {
|
||||
if _, err = c.GenAiCompatResponses(ctx, cred, region, body); err == nil {
|
||||
return http.StatusOK, nil
|
||||
}
|
||||
if status, ok := ServiceStatus(err); ok {
|
||||
@@ -275,6 +161,27 @@ func (c *RealClient) GenAiProbeChat(ctx context.Context, cred Credentials, regio
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// sdkUsageToIR 把 SDK 用量转为内部记账结构(缓存命中挂 details,仅命中时出现)。
|
||||
func sdkUsageToIR(u *generativeaiinference.Usage) *aiwire.Usage {
|
||||
if u == nil {
|
||||
return nil
|
||||
}
|
||||
out := &aiwire.Usage{}
|
||||
if u.PromptTokens != nil {
|
||||
out.PromptTokens = *u.PromptTokens
|
||||
}
|
||||
if u.CompletionTokens != nil {
|
||||
out.CompletionTokens = *u.CompletionTokens
|
||||
}
|
||||
if u.TotalTokens != nil {
|
||||
out.TotalTokens = *u.TotalTokens
|
||||
}
|
||||
if u.PromptTokensDetails != nil && u.PromptTokensDetails.CachedTokens != nil && *u.PromptTokensDetails.CachedTokens > 0 {
|
||||
out.PromptTokensDetails = &aiwire.PromptTokensDetails{CachedTokens: *u.PromptTokensDetails.CachedTokens}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// GenAiEmbed 实现 Client:文本向量化(on-demand serving);dimensions 透传 OutputDimensions。
|
||||
func (c *RealClient) GenAiEmbed(ctx context.Context, cred Credentials, region, modelOcid string, inputs []string, dimensions *int) ([][]float32, *aiwire.Usage, error) {
|
||||
ic, err := c.genAiInferenceClient(cred, region)
|
||||
|
||||
Reference in New Issue
Block a user