M4a:神识全量网关能力——OpenAI 兼容适配器(元炁共用)、候选路由+凭证/绑定级冷却+首字超时切道+粘性会话、用量计量与 /me/usage,故障注入测试四场景
This commit is contained in:
1 parent
df5caa69e7
commit
7279904043
8 files changed
+717
-35
No files matched your search
@@ -0,0 +1,138 @@
|
||||
// OpenAI 兼容出站适配器:IR → OpenAI chat/completions(元炁亦走此协议,type=local 复用)。
|
||||
package transform
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"hunjing/shenshi/internal/ir"
|
||||
)
|
||||
|
||||
// UpstreamError 上游错误:携带 HTTP 状态,供路由层分类冷却(凭证级/绑定级)。
|
||||
type UpstreamError struct {
|
||||
StatusCode int
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *UpstreamError) Error() string {
|
||||
return fmt.Sprintf("上游 %d: %s", e.StatusCode, e.Message)
|
||||
}
|
||||
|
||||
// OpenAICompatAdaptor OpenAI 兼容端点适配器(云厂商兼容面 + 元炁本地端点)。
|
||||
type OpenAICompatAdaptor struct {
|
||||
EndpointName string // 端点标识(usage 日志与冷却键)
|
||||
BaseURL string
|
||||
APIKey string
|
||||
// UpstreamModel 该端点承载的上游模型名(逻辑模型 → 端点绑定的组成部分)
|
||||
UpstreamModel string
|
||||
Client *http.Client
|
||||
}
|
||||
|
||||
// Name 实现 Adaptor。
|
||||
func (a *OpenAICompatAdaptor) Name() string { return a.EndpointName }
|
||||
|
||||
type oaMessage struct {
|
||||
Role string `json:"role"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type oaRequest struct {
|
||||
Model string `json:"model"`
|
||||
Messages []oaMessage `json:"messages"`
|
||||
Stream bool `json:"stream"`
|
||||
Temperature *float64 `json:"temperature,omitempty"`
|
||||
MaxTokens *int `json:"max_tokens,omitempty"`
|
||||
}
|
||||
|
||||
type oaChunk struct {
|
||||
Choices []struct {
|
||||
Delta struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"delta"`
|
||||
FinishReason *string `json:"finish_reason"`
|
||||
} `json:"choices"`
|
||||
Usage *struct {
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
} `json:"usage"`
|
||||
}
|
||||
|
||||
// Stream 实现 Adaptor:SSE 逐帧转 IR Delta。首字节超时由调用方 context 控制。
|
||||
func (a *OpenAICompatAdaptor) Stream(ctx context.Context, req *ir.ChatRequest, out chan<- ir.Delta) error {
|
||||
body := oaRequest{
|
||||
Model: a.UpstreamModel,
|
||||
Messages: make([]oaMessage, 0, len(req.Messages)),
|
||||
Stream: true,
|
||||
}
|
||||
for _, m := range req.Messages {
|
||||
body.Messages = append(body.Messages, oaMessage{Role: m.Role, Content: m.Content})
|
||||
}
|
||||
body.Temperature = req.Temperature
|
||||
body.MaxTokens = req.MaxTokens
|
||||
|
||||
payload, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(a.BaseURL, "/")+"/v1/chat/completions", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
httpReq.Header.Set("content-type", "application/json")
|
||||
if a.APIKey != "" {
|
||||
httpReq.Header.Set("authorization", "Bearer "+a.APIKey)
|
||||
}
|
||||
|
||||
client := a.Client
|
||||
if client == nil {
|
||||
client = &http.Client{Timeout: 120 * time.Second}
|
||||
}
|
||||
resp, err := client.Do(httpReq)
|
||||
if err != nil {
|
||||
return &UpstreamError{StatusCode: 0, Message: err.Error()} // 0 = 网络/超时(绑定级冷却)
|
||||
}
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
if resp.StatusCode >= 400 {
|
||||
buf := new(bytes.Buffer)
|
||||
_, _ = buf.ReadFrom(resp.Body)
|
||||
return &UpstreamError{StatusCode: resp.StatusCode, Message: buf.String()}
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if !strings.HasPrefix(line, "data:") {
|
||||
continue
|
||||
}
|
||||
data := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
|
||||
if data == "" || data == "[DONE]" {
|
||||
continue
|
||||
}
|
||||
var chunk oaChunk
|
||||
if err := json.Unmarshal([]byte(data), &chunk); err != nil {
|
||||
continue
|
||||
}
|
||||
if len(chunk.Choices) > 0 {
|
||||
c := chunk.Choices[0]
|
||||
if c.Delta.Content != "" {
|
||||
out <- ir.Delta{Delta: c.Delta.Content}
|
||||
}
|
||||
if c.FinishReason != nil {
|
||||
finish := *c.FinishReason
|
||||
usagePrompt, usageCompletion := 0, 0
|
||||
if chunk.Usage != nil {
|
||||
usagePrompt, usageCompletion = chunk.Usage.PromptTokens, chunk.Usage.CompletionTokens
|
||||
}
|
||||
out <- ir.Delta{FinishReason: finish, UsagePromptTokens: usagePrompt, UsageCompletionTokens: usageCompletion}
|
||||
}
|
||||
}
|
||||
}
|
||||
return scanner.Err()
|
||||
}
|
||||
Reference in new issue
Block a user