Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
178
backend/internal/runtime/executor/xai_executor_stream.go
Normal file
178
backend/internal/runtime/executor/xai_executor_stream.go
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
package executor
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/router-for-me/CLIProxyAPI/v7/internal/runtime/executor/helps"
|
||||
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
|
||||
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/tidwall/gjson"
|
||||
)
|
||||
|
||||
func (e *XAIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (_ *cliproxyexecutor.StreamResult, err error) {
|
||||
if opts.Alt == "responses/compact" {
|
||||
return nil, statusErr{code: http.StatusBadRequest, msg: "streaming not supported for /responses/compact"}
|
||||
}
|
||||
if xaiInputHasItemType(req.Payload, "compaction_trigger") {
|
||||
return e.executeCompactionTriggerStream(ctx, auth, req, opts)
|
||||
}
|
||||
|
||||
token, _ := xaiCreds(auth)
|
||||
baseURL := xaiChatBaseURL(auth)
|
||||
logXAIResolvedBaseURL(ctx, baseURL)
|
||||
|
||||
prepared, err := e.prepareResponsesRequest(ctx, req, opts, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
reporter := helps.NewExecutorUsageReporter(ctx, e, prepared.baseModel, auth)
|
||||
defer reporter.TrackFailure(ctx, &err)
|
||||
reporter.SetTranslatedReasoningEffort(prepared.body, e.Identifier())
|
||||
|
||||
url := strings.TrimSuffix(baseURL, "/") + "/responses"
|
||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(prepared.body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
applyXAIChatHeaders(httpReq, auth, token, true, prepared.sessionID, opts.Headers)
|
||||
e.recordXAIRequest(ctx, auth, url, httpReq.Header.Clone(), prepared.body)
|
||||
|
||||
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||
httpClient = reporter.TrackHTTPClient(httpClient)
|
||||
httpResp, err := httpClient.Do(httpReq)
|
||||
if err != nil {
|
||||
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||
return nil, err
|
||||
}
|
||||
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||
data, errRead := io.ReadAll(httpResp.Body)
|
||||
if errClose := httpResp.Body.Close(); errClose != nil {
|
||||
log.Errorf("xai executor: close response body error: %v", errClose)
|
||||
}
|
||||
if errRead != nil {
|
||||
helps.RecordAPIResponseError(ctx, e.cfg, errRead)
|
||||
return nil, errRead
|
||||
}
|
||||
helps.AppendAPIResponseChunk(ctx, e.cfg, data)
|
||||
helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), data))
|
||||
return nil, xaiStatusErr(httpResp.StatusCode, data)
|
||||
}
|
||||
|
||||
out := make(chan cliproxyexecutor.StreamChunk)
|
||||
go func() {
|
||||
defer close(out)
|
||||
defer func() {
|
||||
if errClose := httpResp.Body.Close(); errClose != nil {
|
||||
log.Errorf("xai executor: close response body error: %v", errClose)
|
||||
}
|
||||
}()
|
||||
scanner := bufio.NewScanner(httpResp.Body)
|
||||
scanner.Buffer(nil, 52_428_800)
|
||||
claudeInputTokens := helps.NewClaudeInputTokenState(prepared.from, prepared.to, prepared.responseFormat, prepared.originalPayload)
|
||||
var param any
|
||||
outputItemsByIndex := make(map[int64][]byte)
|
||||
var outputItemsFallback [][]byte
|
||||
responseFilter := newXAIInternalXSearchResponseFilter(prepared.filterInternalXSearch, prepared.clientDeclaredTools)
|
||||
var pendingEventLine []byte
|
||||
emitTranslatedLine := func(translatedLine []byte) bool {
|
||||
chunks := helps.TranslateStreamWithClaudeInputTokens(ctx, prepared.to, prepared.responseFormat, req.Model, prepared.originalPayload, prepared.body, translatedLine, ¶m, claudeInputTokens)
|
||||
for i := range chunks {
|
||||
select {
|
||||
case out <- cliproxyexecutor.StreamChunk{Payload: chunks[i]}:
|
||||
case <-ctx.Done():
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
for scanner.Scan() {
|
||||
line := scanner.Bytes()
|
||||
helps.AppendAPIResponseChunk(ctx, e.cfg, line)
|
||||
|
||||
if bytes.HasPrefix(line, xaiEventTag) {
|
||||
if pendingEventLine != nil && !emitTranslatedLine(xaiNormalizeReasoningSummaryEventLine(pendingEventLine, "")) {
|
||||
return
|
||||
}
|
||||
pendingEventLine = bytes.Clone(line)
|
||||
continue
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(line, xaiDataTag) {
|
||||
eventDataList := xaiNormalizeReasoningSummaryDataEvents(bytes.TrimSpace(line[len(xaiDataTag):]))
|
||||
hasPendingEventLine := pendingEventLine != nil
|
||||
for i, eventData := range eventDataList {
|
||||
eventData = restoreXAINamespaceToolCalls(eventData, prepared.namespaceTools)
|
||||
eventData = responseFilter.apply(eventData)
|
||||
if len(eventData) == 0 {
|
||||
if hasPendingEventLine && i == 0 {
|
||||
pendingEventLine = nil
|
||||
}
|
||||
continue
|
||||
}
|
||||
normalizedEventName := gjson.GetBytes(eventData, "type").String()
|
||||
switch normalizedEventName {
|
||||
case "response.output_item.done":
|
||||
xaiCollectOutputItemDone(eventData, outputItemsByIndex, &outputItemsFallback)
|
||||
case "response.completed", "response.incomplete":
|
||||
if detail, ok := helps.ParseCodexUsage(eventData); ok {
|
||||
reporter.Publish(ctx, detail)
|
||||
}
|
||||
eventData = xaiPatchCompletedOutput(eventData, outputItemsByIndex, outputItemsFallback)
|
||||
eventData = xaiNormalizeReasoningSummaryData(eventData)
|
||||
if normalizedEventName == "response.completed" {
|
||||
// A truncated turn carries no replayable terminal state, so only a
|
||||
// completed response may refresh the reasoning replay cache.
|
||||
cacheXAIReasoningReplayFromCompleted(ctx, prepared.replayScope, eventData)
|
||||
}
|
||||
normalizedEventName = gjson.GetBytes(eventData, "type").String()
|
||||
}
|
||||
|
||||
if hasPendingEventLine {
|
||||
eventLine := []byte("event: " + normalizedEventName)
|
||||
if i == 0 {
|
||||
eventLine = xaiNormalizeReasoningSummaryEventLine(pendingEventLine, normalizedEventName)
|
||||
pendingEventLine = nil
|
||||
}
|
||||
if !emitTranslatedLine(eventLine) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if !emitTranslatedLine(append([]byte("data: "), eventData...)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if pendingEventLine != nil {
|
||||
if !emitTranslatedLine(xaiNormalizeReasoningSummaryEventLine(pendingEventLine, "")) {
|
||||
return
|
||||
}
|
||||
pendingEventLine = nil
|
||||
}
|
||||
if !emitTranslatedLine(bytes.Clone(line)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if pendingEventLine != nil {
|
||||
emitTranslatedLine(xaiNormalizeReasoningSummaryEventLine(pendingEventLine, ""))
|
||||
}
|
||||
if errScan := scanner.Err(); errScan != nil {
|
||||
helps.RecordAPIResponseError(ctx, e.cfg, errScan)
|
||||
reporter.PublishFailure(ctx, errScan)
|
||||
select {
|
||||
case out <- cliproxyexecutor.StreamChunk{Err: errScan}:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}
|
||||
}()
|
||||
return &cliproxyexecutor.StreamResult{Headers: httpResp.Header.Clone(), Chunks: out}, nil
|
||||
}
|
||||
Loading…
Reference in a new issue