refactor(runtime): move executor utilities to helps package and update references
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/wsrelay"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/wsrelay"
|
||||||
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
@@ -115,8 +116,8 @@ func (e *AIStudioExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth,
|
|||||||
return resp, statusErr{code: http.StatusNotImplemented, msg: "/responses/compact not supported"}
|
return resp, statusErr{code: http.StatusNotImplemented, msg: "/responses/compact not supported"}
|
||||||
}
|
}
|
||||||
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
||||||
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
||||||
defer reporter.trackFailure(ctx, &err)
|
defer reporter.TrackFailure(ctx, &err)
|
||||||
|
|
||||||
translatedReq, body, err := e.translateRequest(req, opts, false)
|
translatedReq, body, err := e.translateRequest(req, opts, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -137,7 +138,7 @@ func (e *AIStudioExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth,
|
|||||||
authLabel = auth.Label
|
authLabel = auth.Label
|
||||||
authType, authValue = auth.AccountInfo()
|
authType, authValue = auth.AccountInfo()
|
||||||
}
|
}
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: endpoint,
|
URL: endpoint,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: wsReq.Headers.Clone(),
|
Headers: wsReq.Headers.Clone(),
|
||||||
@@ -151,17 +152,17 @@ func (e *AIStudioExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth,
|
|||||||
|
|
||||||
wsResp, err := e.relay.NonStream(ctx, authID, wsReq)
|
wsResp, err := e.relay.NonStream(ctx, authID, wsReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, wsResp.Status, wsResp.Headers.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, wsResp.Status, wsResp.Headers.Clone())
|
||||||
if len(wsResp.Body) > 0 {
|
if len(wsResp.Body) > 0 {
|
||||||
appendAPIResponseChunk(ctx, e.cfg, wsResp.Body)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, wsResp.Body)
|
||||||
}
|
}
|
||||||
if wsResp.Status < 200 || wsResp.Status >= 300 {
|
if wsResp.Status < 200 || wsResp.Status >= 300 {
|
||||||
return resp, statusErr{code: wsResp.Status, msg: string(wsResp.Body)}
|
return resp, statusErr{code: wsResp.Status, msg: string(wsResp.Body)}
|
||||||
}
|
}
|
||||||
reporter.publish(ctx, parseGeminiUsage(wsResp.Body))
|
reporter.Publish(ctx, helps.ParseGeminiUsage(wsResp.Body))
|
||||||
var param any
|
var param any
|
||||||
out := sdktranslator.TranslateNonStream(ctx, body.toFormat, opts.SourceFormat, req.Model, opts.OriginalRequest, translatedReq, wsResp.Body, ¶m)
|
out := sdktranslator.TranslateNonStream(ctx, body.toFormat, opts.SourceFormat, req.Model, opts.OriginalRequest, translatedReq, wsResp.Body, ¶m)
|
||||||
resp = cliproxyexecutor.Response{Payload: ensureColonSpacedJSON(out), Headers: wsResp.Headers.Clone()}
|
resp = cliproxyexecutor.Response{Payload: ensureColonSpacedJSON(out), Headers: wsResp.Headers.Clone()}
|
||||||
@@ -174,8 +175,8 @@ func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth
|
|||||||
return nil, statusErr{code: http.StatusNotImplemented, msg: "/responses/compact not supported"}
|
return nil, statusErr{code: http.StatusNotImplemented, msg: "/responses/compact not supported"}
|
||||||
}
|
}
|
||||||
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
||||||
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
||||||
defer reporter.trackFailure(ctx, &err)
|
defer reporter.TrackFailure(ctx, &err)
|
||||||
|
|
||||||
translatedReq, body, err := e.translateRequest(req, opts, true)
|
translatedReq, body, err := e.translateRequest(req, opts, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -195,7 +196,7 @@ func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth
|
|||||||
authLabel = auth.Label
|
authLabel = auth.Label
|
||||||
authType, authValue = auth.AccountInfo()
|
authType, authValue = auth.AccountInfo()
|
||||||
}
|
}
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: endpoint,
|
URL: endpoint,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: wsReq.Headers.Clone(),
|
Headers: wsReq.Headers.Clone(),
|
||||||
@@ -208,24 +209,24 @@ func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth
|
|||||||
})
|
})
|
||||||
wsStream, err := e.relay.Stream(ctx, authID, wsReq)
|
wsStream, err := e.relay.Stream(ctx, authID, wsReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
firstEvent, ok := <-wsStream
|
firstEvent, ok := <-wsStream
|
||||||
if !ok {
|
if !ok {
|
||||||
err = fmt.Errorf("wsrelay: stream closed before start")
|
err = fmt.Errorf("wsrelay: stream closed before start")
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if firstEvent.Status > 0 && firstEvent.Status != http.StatusOK {
|
if firstEvent.Status > 0 && firstEvent.Status != http.StatusOK {
|
||||||
metadataLogged := false
|
metadataLogged := false
|
||||||
if firstEvent.Status > 0 {
|
if firstEvent.Status > 0 {
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, firstEvent.Status, firstEvent.Headers.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, firstEvent.Status, firstEvent.Headers.Clone())
|
||||||
metadataLogged = true
|
metadataLogged = true
|
||||||
}
|
}
|
||||||
var body bytes.Buffer
|
var body bytes.Buffer
|
||||||
if len(firstEvent.Payload) > 0 {
|
if len(firstEvent.Payload) > 0 {
|
||||||
appendAPIResponseChunk(ctx, e.cfg, firstEvent.Payload)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, firstEvent.Payload)
|
||||||
body.Write(firstEvent.Payload)
|
body.Write(firstEvent.Payload)
|
||||||
}
|
}
|
||||||
if firstEvent.Type == wsrelay.MessageTypeStreamEnd {
|
if firstEvent.Type == wsrelay.MessageTypeStreamEnd {
|
||||||
@@ -233,18 +234,18 @@ func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth
|
|||||||
}
|
}
|
||||||
for event := range wsStream {
|
for event := range wsStream {
|
||||||
if event.Err != nil {
|
if event.Err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, event.Err)
|
helps.RecordAPIResponseError(ctx, e.cfg, event.Err)
|
||||||
if body.Len() == 0 {
|
if body.Len() == 0 {
|
||||||
body.WriteString(event.Err.Error())
|
body.WriteString(event.Err.Error())
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if !metadataLogged && event.Status > 0 {
|
if !metadataLogged && event.Status > 0 {
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, event.Status, event.Headers.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, event.Status, event.Headers.Clone())
|
||||||
metadataLogged = true
|
metadataLogged = true
|
||||||
}
|
}
|
||||||
if len(event.Payload) > 0 {
|
if len(event.Payload) > 0 {
|
||||||
appendAPIResponseChunk(ctx, e.cfg, event.Payload)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, event.Payload)
|
||||||
body.Write(event.Payload)
|
body.Write(event.Payload)
|
||||||
}
|
}
|
||||||
if event.Type == wsrelay.MessageTypeStreamEnd {
|
if event.Type == wsrelay.MessageTypeStreamEnd {
|
||||||
@@ -260,23 +261,23 @@ func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth
|
|||||||
metadataLogged := false
|
metadataLogged := false
|
||||||
processEvent := func(event wsrelay.StreamEvent) bool {
|
processEvent := func(event wsrelay.StreamEvent) bool {
|
||||||
if event.Err != nil {
|
if event.Err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, event.Err)
|
helps.RecordAPIResponseError(ctx, e.cfg, event.Err)
|
||||||
reporter.publishFailure(ctx)
|
reporter.PublishFailure(ctx)
|
||||||
out <- cliproxyexecutor.StreamChunk{Err: fmt.Errorf("wsrelay: %v", event.Err)}
|
out <- cliproxyexecutor.StreamChunk{Err: fmt.Errorf("wsrelay: %v", event.Err)}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
switch event.Type {
|
switch event.Type {
|
||||||
case wsrelay.MessageTypeStreamStart:
|
case wsrelay.MessageTypeStreamStart:
|
||||||
if !metadataLogged && event.Status > 0 {
|
if !metadataLogged && event.Status > 0 {
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, event.Status, event.Headers.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, event.Status, event.Headers.Clone())
|
||||||
metadataLogged = true
|
metadataLogged = true
|
||||||
}
|
}
|
||||||
case wsrelay.MessageTypeStreamChunk:
|
case wsrelay.MessageTypeStreamChunk:
|
||||||
if len(event.Payload) > 0 {
|
if len(event.Payload) > 0 {
|
||||||
appendAPIResponseChunk(ctx, e.cfg, event.Payload)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, event.Payload)
|
||||||
filtered := FilterSSEUsageMetadata(event.Payload)
|
filtered := helps.FilterSSEUsageMetadata(event.Payload)
|
||||||
if detail, ok := parseGeminiStreamUsage(filtered); ok {
|
if detail, ok := helps.ParseGeminiStreamUsage(filtered); ok {
|
||||||
reporter.publish(ctx, detail)
|
reporter.Publish(ctx, detail)
|
||||||
}
|
}
|
||||||
lines := sdktranslator.TranslateStream(ctx, body.toFormat, opts.SourceFormat, req.Model, opts.OriginalRequest, translatedReq, filtered, ¶m)
|
lines := sdktranslator.TranslateStream(ctx, body.toFormat, opts.SourceFormat, req.Model, opts.OriginalRequest, translatedReq, filtered, ¶m)
|
||||||
for i := range lines {
|
for i := range lines {
|
||||||
@@ -288,21 +289,21 @@ func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth
|
|||||||
return false
|
return false
|
||||||
case wsrelay.MessageTypeHTTPResp:
|
case wsrelay.MessageTypeHTTPResp:
|
||||||
if !metadataLogged && event.Status > 0 {
|
if !metadataLogged && event.Status > 0 {
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, event.Status, event.Headers.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, event.Status, event.Headers.Clone())
|
||||||
metadataLogged = true
|
metadataLogged = true
|
||||||
}
|
}
|
||||||
if len(event.Payload) > 0 {
|
if len(event.Payload) > 0 {
|
||||||
appendAPIResponseChunk(ctx, e.cfg, event.Payload)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, event.Payload)
|
||||||
}
|
}
|
||||||
lines := sdktranslator.TranslateStream(ctx, body.toFormat, opts.SourceFormat, req.Model, opts.OriginalRequest, translatedReq, event.Payload, ¶m)
|
lines := sdktranslator.TranslateStream(ctx, body.toFormat, opts.SourceFormat, req.Model, opts.OriginalRequest, translatedReq, event.Payload, ¶m)
|
||||||
for i := range lines {
|
for i := range lines {
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: ensureColonSpacedJSON(lines[i])}
|
out <- cliproxyexecutor.StreamChunk{Payload: ensureColonSpacedJSON(lines[i])}
|
||||||
}
|
}
|
||||||
reporter.publish(ctx, parseGeminiUsage(event.Payload))
|
reporter.Publish(ctx, helps.ParseGeminiUsage(event.Payload))
|
||||||
return false
|
return false
|
||||||
case wsrelay.MessageTypeError:
|
case wsrelay.MessageTypeError:
|
||||||
recordAPIResponseError(ctx, e.cfg, event.Err)
|
helps.RecordAPIResponseError(ctx, e.cfg, event.Err)
|
||||||
reporter.publishFailure(ctx)
|
reporter.PublishFailure(ctx)
|
||||||
out <- cliproxyexecutor.StreamChunk{Err: fmt.Errorf("wsrelay: %v", event.Err)}
|
out <- cliproxyexecutor.StreamChunk{Err: fmt.Errorf("wsrelay: %v", event.Err)}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -345,7 +346,7 @@ func (e *AIStudioExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.A
|
|||||||
authLabel = auth.Label
|
authLabel = auth.Label
|
||||||
authType, authValue = auth.AccountInfo()
|
authType, authValue = auth.AccountInfo()
|
||||||
}
|
}
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: endpoint,
|
URL: endpoint,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: wsReq.Headers.Clone(),
|
Headers: wsReq.Headers.Clone(),
|
||||||
@@ -358,12 +359,12 @@ func (e *AIStudioExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.A
|
|||||||
})
|
})
|
||||||
resp, err := e.relay.NonStream(ctx, authID, wsReq)
|
resp, err := e.relay.NonStream(ctx, authID, wsReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
return cliproxyexecutor.Response{}, err
|
return cliproxyexecutor.Response{}, err
|
||||||
}
|
}
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, resp.Status, resp.Headers.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, resp.Status, resp.Headers.Clone())
|
||||||
if len(resp.Body) > 0 {
|
if len(resp.Body) > 0 {
|
||||||
appendAPIResponseChunk(ctx, e.cfg, resp.Body)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, resp.Body)
|
||||||
}
|
}
|
||||||
if resp.Status < 200 || resp.Status >= 300 {
|
if resp.Status < 200 || resp.Status >= 300 {
|
||||||
return cliproxyexecutor.Response{}, statusErr{code: resp.Status, msg: string(resp.Body)}
|
return cliproxyexecutor.Response{}, statusErr{code: resp.Status, msg: string(resp.Body)}
|
||||||
@@ -404,8 +405,8 @@ func (e *AIStudioExecutor) translateRequest(req cliproxyexecutor.Request, opts c
|
|||||||
return nil, translatedPayload{}, err
|
return nil, translatedPayload{}, err
|
||||||
}
|
}
|
||||||
payload = fixGeminiImageAspectRatio(baseModel, payload)
|
payload = fixGeminiImageAspectRatio(baseModel, payload)
|
||||||
requestedModel := payloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
payload = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", payload, originalTranslated, requestedModel)
|
payload = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", payload, originalTranslated, requestedModel)
|
||||||
payload, _ = sjson.DeleteBytes(payload, "generationConfig.maxOutputTokens")
|
payload, _ = sjson.DeleteBytes(payload, "generationConfig.maxOutputTokens")
|
||||||
payload, _ = sjson.DeleteBytes(payload, "generationConfig.responseMimeType")
|
payload, _ = sjson.DeleteBytes(payload, "generationConfig.responseMimeType")
|
||||||
payload, _ = sjson.DeleteBytes(payload, "generationConfig.responseJsonSchema")
|
payload, _ = sjson.DeleteBytes(payload, "generationConfig.responseJsonSchema")
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import (
|
|||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
|
sdkAuth "github.com/router-for-me/CLIProxyAPI/v6/sdk/auth"
|
||||||
@@ -142,7 +143,7 @@ func initAntigravityTransport() {
|
|||||||
func newAntigravityHTTPClient(ctx context.Context, cfg *config.Config, auth *cliproxyauth.Auth, timeout time.Duration) *http.Client {
|
func newAntigravityHTTPClient(ctx context.Context, cfg *config.Config, auth *cliproxyauth.Auth, timeout time.Duration) *http.Client {
|
||||||
antigravityTransportOnce.Do(initAntigravityTransport)
|
antigravityTransportOnce.Do(initAntigravityTransport)
|
||||||
|
|
||||||
client := newProxyAwareHTTPClient(ctx, cfg, auth, timeout)
|
client := helps.NewProxyAwareHTTPClient(ctx, cfg, auth, timeout)
|
||||||
// If no transport is set, use the shared HTTP/1.1 transport.
|
// If no transport is set, use the shared HTTP/1.1 transport.
|
||||||
if client.Transport == nil {
|
if client.Transport == nil {
|
||||||
client.Transport = antigravityTransport
|
client.Transport = antigravityTransport
|
||||||
@@ -405,12 +406,12 @@ func (e *AntigravityExecutor) attemptCreditsFallback(
|
|||||||
|
|
||||||
httpReq, errReq := e.buildRequest(ctx, auth, token, modelName, creditsPayload, stream, alt, baseURL)
|
httpReq, errReq := e.buildRequest(ctx, auth, token, modelName, creditsPayload, stream, alt, baseURL)
|
||||||
if errReq != nil {
|
if errReq != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errReq)
|
helps.RecordAPIResponseError(ctx, e.cfg, errReq)
|
||||||
return nil, true
|
return nil, true
|
||||||
}
|
}
|
||||||
httpResp, errDo := httpClient.Do(httpReq)
|
httpResp, errDo := httpClient.Do(httpReq)
|
||||||
if errDo != nil {
|
if errDo != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errDo)
|
helps.RecordAPIResponseError(ctx, e.cfg, errDo)
|
||||||
return nil, true
|
return nil, true
|
||||||
}
|
}
|
||||||
if httpResp.StatusCode >= http.StatusOK && httpResp.StatusCode < http.StatusMultipleChoices {
|
if httpResp.StatusCode >= http.StatusOK && httpResp.StatusCode < http.StatusMultipleChoices {
|
||||||
@@ -420,16 +421,16 @@ func (e *AntigravityExecutor) attemptCreditsFallback(
|
|||||||
return httpResp, true
|
return httpResp, true
|
||||||
}
|
}
|
||||||
|
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||||
bodyBytes, errRead := io.ReadAll(httpResp.Body)
|
bodyBytes, errRead := io.ReadAll(httpResp.Body)
|
||||||
if errClose := httpResp.Body.Close(); errClose != nil {
|
if errClose := httpResp.Body.Close(); errClose != nil {
|
||||||
log.Errorf("antigravity executor: close credits fallback response body error: %v", errClose)
|
log.Errorf("antigravity executor: close credits fallback response body error: %v", errClose)
|
||||||
}
|
}
|
||||||
if errRead != nil {
|
if errRead != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errRead)
|
helps.RecordAPIResponseError(ctx, e.cfg, errRead)
|
||||||
return nil, true
|
return nil, true
|
||||||
}
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, bodyBytes)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, bodyBytes)
|
||||||
if shouldMarkAntigravityCreditsExhausted(httpResp.StatusCode, bodyBytes, nil) {
|
if shouldMarkAntigravityCreditsExhausted(httpResp.StatusCode, bodyBytes, nil) {
|
||||||
clearAntigravityPreferCredits(auth, modelName)
|
clearAntigravityPreferCredits(auth, modelName)
|
||||||
markAntigravityCreditsExhausted(auth, now)
|
markAntigravityCreditsExhausted(auth, now)
|
||||||
@@ -457,8 +458,8 @@ func (e *AntigravityExecutor) Execute(ctx context.Context, auth *cliproxyauth.Au
|
|||||||
auth = updatedAuth
|
auth = updatedAuth
|
||||||
}
|
}
|
||||||
|
|
||||||
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
||||||
defer reporter.trackFailure(ctx, &err)
|
defer reporter.TrackFailure(ctx, &err)
|
||||||
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("antigravity")
|
to := sdktranslator.FromString("antigravity")
|
||||||
@@ -476,8 +477,8 @@ func (e *AntigravityExecutor) Execute(ctx context.Context, auth *cliproxyauth.Au
|
|||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
requestedModel := payloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
translated = applyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel)
|
translated = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel)
|
||||||
|
|
||||||
baseURLs := antigravityBaseURLFallbackOrder(auth)
|
baseURLs := antigravityBaseURLFallbackOrder(auth)
|
||||||
httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
@@ -507,7 +508,7 @@ attemptLoop:
|
|||||||
|
|
||||||
httpResp, errDo := httpClient.Do(httpReq)
|
httpResp, errDo := httpClient.Do(httpReq)
|
||||||
if errDo != nil {
|
if errDo != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errDo)
|
helps.RecordAPIResponseError(ctx, e.cfg, errDo)
|
||||||
if errors.Is(errDo, context.Canceled) || errors.Is(errDo, context.DeadlineExceeded) {
|
if errors.Is(errDo, context.Canceled) || errors.Is(errDo, context.DeadlineExceeded) {
|
||||||
return resp, errDo
|
return resp, errDo
|
||||||
}
|
}
|
||||||
@@ -522,17 +523,17 @@ attemptLoop:
|
|||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||||
bodyBytes, errRead := io.ReadAll(httpResp.Body)
|
bodyBytes, errRead := io.ReadAll(httpResp.Body)
|
||||||
if errClose := httpResp.Body.Close(); errClose != nil {
|
if errClose := httpResp.Body.Close(); errClose != nil {
|
||||||
log.Errorf("antigravity executor: close response body error: %v", errClose)
|
log.Errorf("antigravity executor: close response body error: %v", errClose)
|
||||||
}
|
}
|
||||||
if errRead != nil {
|
if errRead != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errRead)
|
helps.RecordAPIResponseError(ctx, e.cfg, errRead)
|
||||||
err = errRead
|
err = errRead
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, bodyBytes)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, bodyBytes)
|
||||||
|
|
||||||
if httpResp.StatusCode == http.StatusTooManyRequests {
|
if httpResp.StatusCode == http.StatusTooManyRequests {
|
||||||
if usedCreditsDirect {
|
if usedCreditsDirect {
|
||||||
@@ -543,29 +544,29 @@ attemptLoop:
|
|||||||
} else {
|
} else {
|
||||||
creditsResp, _ := e.attemptCreditsFallback(ctx, auth, httpClient, token, baseModel, translated, false, opts.Alt, baseURL, bodyBytes)
|
creditsResp, _ := e.attemptCreditsFallback(ctx, auth, httpClient, token, baseModel, translated, false, opts.Alt, baseURL, bodyBytes)
|
||||||
if creditsResp != nil {
|
if creditsResp != nil {
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, creditsResp.StatusCode, creditsResp.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, creditsResp.StatusCode, creditsResp.Header.Clone())
|
||||||
creditsBody, errCreditsRead := io.ReadAll(creditsResp.Body)
|
creditsBody, errCreditsRead := io.ReadAll(creditsResp.Body)
|
||||||
if errClose := creditsResp.Body.Close(); errClose != nil {
|
if errClose := creditsResp.Body.Close(); errClose != nil {
|
||||||
log.Errorf("antigravity executor: close credits success response body error: %v", errClose)
|
log.Errorf("antigravity executor: close credits success response body error: %v", errClose)
|
||||||
}
|
}
|
||||||
if errCreditsRead != nil {
|
if errCreditsRead != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errCreditsRead)
|
helps.RecordAPIResponseError(ctx, e.cfg, errCreditsRead)
|
||||||
err = errCreditsRead
|
err = errCreditsRead
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, creditsBody)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, creditsBody)
|
||||||
reporter.publish(ctx, parseAntigravityUsage(creditsBody))
|
reporter.Publish(ctx, helps.ParseAntigravityUsage(creditsBody))
|
||||||
var param any
|
var param any
|
||||||
converted := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, creditsBody, ¶m)
|
converted := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, creditsBody, ¶m)
|
||||||
resp = cliproxyexecutor.Response{Payload: converted, Headers: creditsResp.Header.Clone()}
|
resp = cliproxyexecutor.Response{Payload: converted, Headers: creditsResp.Header.Clone()}
|
||||||
reporter.ensurePublished(ctx)
|
reporter.EnsurePublished(ctx)
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if httpResp.StatusCode < http.StatusOK || httpResp.StatusCode >= http.StatusMultipleChoices {
|
if httpResp.StatusCode < http.StatusOK || httpResp.StatusCode >= http.StatusMultipleChoices {
|
||||||
log.Debugf("antigravity executor: upstream error status: %d, body: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), bodyBytes))
|
log.Debugf("antigravity executor: upstream error status: %d, body: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), bodyBytes))
|
||||||
lastStatus = httpResp.StatusCode
|
lastStatus = httpResp.StatusCode
|
||||||
lastBody = append([]byte(nil), bodyBytes...)
|
lastBody = append([]byte(nil), bodyBytes...)
|
||||||
lastErr = nil
|
lastErr = nil
|
||||||
@@ -591,11 +592,11 @@ attemptLoop:
|
|||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
reporter.publish(ctx, parseAntigravityUsage(bodyBytes))
|
reporter.Publish(ctx, helps.ParseAntigravityUsage(bodyBytes))
|
||||||
var param any
|
var param any
|
||||||
converted := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, bodyBytes, ¶m)
|
converted := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, bodyBytes, ¶m)
|
||||||
resp = cliproxyexecutor.Response{Payload: converted, Headers: httpResp.Header.Clone()}
|
resp = cliproxyexecutor.Response{Payload: converted, Headers: httpResp.Header.Clone()}
|
||||||
reporter.ensurePublished(ctx)
|
reporter.EnsurePublished(ctx)
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -625,8 +626,8 @@ func (e *AntigravityExecutor) executeClaudeNonStream(ctx context.Context, auth *
|
|||||||
auth = updatedAuth
|
auth = updatedAuth
|
||||||
}
|
}
|
||||||
|
|
||||||
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
||||||
defer reporter.trackFailure(ctx, &err)
|
defer reporter.TrackFailure(ctx, &err)
|
||||||
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("antigravity")
|
to := sdktranslator.FromString("antigravity")
|
||||||
@@ -644,8 +645,8 @@ func (e *AntigravityExecutor) executeClaudeNonStream(ctx context.Context, auth *
|
|||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
requestedModel := payloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
translated = applyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel)
|
translated = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel)
|
||||||
|
|
||||||
baseURLs := antigravityBaseURLFallbackOrder(auth)
|
baseURLs := antigravityBaseURLFallbackOrder(auth)
|
||||||
httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
@@ -675,7 +676,7 @@ attemptLoop:
|
|||||||
|
|
||||||
httpResp, errDo := httpClient.Do(httpReq)
|
httpResp, errDo := httpClient.Do(httpReq)
|
||||||
if errDo != nil {
|
if errDo != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errDo)
|
helps.RecordAPIResponseError(ctx, e.cfg, errDo)
|
||||||
if errors.Is(errDo, context.Canceled) || errors.Is(errDo, context.DeadlineExceeded) {
|
if errors.Is(errDo, context.Canceled) || errors.Is(errDo, context.DeadlineExceeded) {
|
||||||
return resp, errDo
|
return resp, errDo
|
||||||
}
|
}
|
||||||
@@ -689,14 +690,14 @@ attemptLoop:
|
|||||||
err = errDo
|
err = errDo
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||||
if httpResp.StatusCode < http.StatusOK || httpResp.StatusCode >= http.StatusMultipleChoices {
|
if httpResp.StatusCode < http.StatusOK || httpResp.StatusCode >= http.StatusMultipleChoices {
|
||||||
bodyBytes, errRead := io.ReadAll(httpResp.Body)
|
bodyBytes, errRead := io.ReadAll(httpResp.Body)
|
||||||
if errClose := httpResp.Body.Close(); errClose != nil {
|
if errClose := httpResp.Body.Close(); errClose != nil {
|
||||||
log.Errorf("antigravity executor: close response body error: %v", errClose)
|
log.Errorf("antigravity executor: close response body error: %v", errClose)
|
||||||
}
|
}
|
||||||
if errRead != nil {
|
if errRead != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errRead)
|
helps.RecordAPIResponseError(ctx, e.cfg, errRead)
|
||||||
if errors.Is(errRead, context.Canceled) || errors.Is(errRead, context.DeadlineExceeded) {
|
if errors.Is(errRead, context.Canceled) || errors.Is(errRead, context.DeadlineExceeded) {
|
||||||
err = errRead
|
err = errRead
|
||||||
return resp, err
|
return resp, err
|
||||||
@@ -715,7 +716,7 @@ attemptLoop:
|
|||||||
err = errRead
|
err = errRead
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, bodyBytes)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, bodyBytes)
|
||||||
if httpResp.StatusCode == http.StatusTooManyRequests {
|
if httpResp.StatusCode == http.StatusTooManyRequests {
|
||||||
if usedCreditsDirect {
|
if usedCreditsDirect {
|
||||||
if shouldMarkAntigravityCreditsExhausted(httpResp.StatusCode, bodyBytes, nil) {
|
if shouldMarkAntigravityCreditsExhausted(httpResp.StatusCode, bodyBytes, nil) {
|
||||||
@@ -726,7 +727,7 @@ attemptLoop:
|
|||||||
creditsResp, _ := e.attemptCreditsFallback(ctx, auth, httpClient, token, baseModel, translated, true, opts.Alt, baseURL, bodyBytes)
|
creditsResp, _ := e.attemptCreditsFallback(ctx, auth, httpClient, token, baseModel, translated, true, opts.Alt, baseURL, bodyBytes)
|
||||||
if creditsResp != nil {
|
if creditsResp != nil {
|
||||||
httpResp = creditsResp
|
httpResp = creditsResp
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -771,29 +772,29 @@ attemptLoop:
|
|||||||
scanner.Buffer(nil, streamScannerBuffer)
|
scanner.Buffer(nil, streamScannerBuffer)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Bytes()
|
line := scanner.Bytes()
|
||||||
appendAPIResponseChunk(ctx, e.cfg, line)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, line)
|
||||||
|
|
||||||
// Filter usage metadata for all models
|
// Filter usage metadata for all models
|
||||||
// Only retain usage statistics in the terminal chunk
|
// Only retain usage statistics in the terminal chunk
|
||||||
line = FilterSSEUsageMetadata(line)
|
line = helps.FilterSSEUsageMetadata(line)
|
||||||
|
|
||||||
payload := jsonPayload(line)
|
payload := helps.JSONPayload(line)
|
||||||
if payload == nil {
|
if payload == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if detail, ok := parseAntigravityStreamUsage(payload); ok {
|
if detail, ok := helps.ParseAntigravityStreamUsage(payload); ok {
|
||||||
reporter.publish(ctx, detail)
|
reporter.Publish(ctx, detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
out <- cliproxyexecutor.StreamChunk{Payload: payload}
|
out <- cliproxyexecutor.StreamChunk{Payload: payload}
|
||||||
}
|
}
|
||||||
if errScan := scanner.Err(); errScan != nil {
|
if errScan := scanner.Err(); errScan != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errScan)
|
helps.RecordAPIResponseError(ctx, e.cfg, errScan)
|
||||||
reporter.publishFailure(ctx)
|
reporter.PublishFailure(ctx)
|
||||||
out <- cliproxyexecutor.StreamChunk{Err: errScan}
|
out <- cliproxyexecutor.StreamChunk{Err: errScan}
|
||||||
} else {
|
} else {
|
||||||
reporter.ensurePublished(ctx)
|
reporter.EnsurePublished(ctx)
|
||||||
}
|
}
|
||||||
}(httpResp)
|
}(httpResp)
|
||||||
|
|
||||||
@@ -809,11 +810,11 @@ attemptLoop:
|
|||||||
}
|
}
|
||||||
resp = cliproxyexecutor.Response{Payload: e.convertStreamToNonStream(buffer.Bytes())}
|
resp = cliproxyexecutor.Response{Payload: e.convertStreamToNonStream(buffer.Bytes())}
|
||||||
|
|
||||||
reporter.publish(ctx, parseAntigravityUsage(resp.Payload))
|
reporter.Publish(ctx, helps.ParseAntigravityUsage(resp.Payload))
|
||||||
var param any
|
var param any
|
||||||
converted := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, resp.Payload, ¶m)
|
converted := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, resp.Payload, ¶m)
|
||||||
resp = cliproxyexecutor.Response{Payload: converted, Headers: httpResp.Header.Clone()}
|
resp = cliproxyexecutor.Response{Payload: converted, Headers: httpResp.Header.Clone()}
|
||||||
reporter.ensurePublished(ctx)
|
reporter.EnsurePublished(ctx)
|
||||||
|
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
@@ -1042,8 +1043,8 @@ func (e *AntigravityExecutor) ExecuteStream(ctx context.Context, auth *cliproxya
|
|||||||
auth = updatedAuth
|
auth = updatedAuth
|
||||||
}
|
}
|
||||||
|
|
||||||
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
||||||
defer reporter.trackFailure(ctx, &err)
|
defer reporter.TrackFailure(ctx, &err)
|
||||||
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("antigravity")
|
to := sdktranslator.FromString("antigravity")
|
||||||
@@ -1061,8 +1062,8 @@ func (e *AntigravityExecutor) ExecuteStream(ctx context.Context, auth *cliproxya
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
requestedModel := payloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
translated = applyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel)
|
translated = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, "antigravity", "request", translated, originalTranslated, requestedModel)
|
||||||
|
|
||||||
baseURLs := antigravityBaseURLFallbackOrder(auth)
|
baseURLs := antigravityBaseURLFallbackOrder(auth)
|
||||||
httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := newAntigravityHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
@@ -1091,7 +1092,7 @@ attemptLoop:
|
|||||||
}
|
}
|
||||||
httpResp, errDo := httpClient.Do(httpReq)
|
httpResp, errDo := httpClient.Do(httpReq)
|
||||||
if errDo != nil {
|
if errDo != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errDo)
|
helps.RecordAPIResponseError(ctx, e.cfg, errDo)
|
||||||
if errors.Is(errDo, context.Canceled) || errors.Is(errDo, context.DeadlineExceeded) {
|
if errors.Is(errDo, context.Canceled) || errors.Is(errDo, context.DeadlineExceeded) {
|
||||||
return nil, errDo
|
return nil, errDo
|
||||||
}
|
}
|
||||||
@@ -1105,14 +1106,14 @@ attemptLoop:
|
|||||||
err = errDo
|
err = errDo
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||||
if httpResp.StatusCode < http.StatusOK || httpResp.StatusCode >= http.StatusMultipleChoices {
|
if httpResp.StatusCode < http.StatusOK || httpResp.StatusCode >= http.StatusMultipleChoices {
|
||||||
bodyBytes, errRead := io.ReadAll(httpResp.Body)
|
bodyBytes, errRead := io.ReadAll(httpResp.Body)
|
||||||
if errClose := httpResp.Body.Close(); errClose != nil {
|
if errClose := httpResp.Body.Close(); errClose != nil {
|
||||||
log.Errorf("antigravity executor: close response body error: %v", errClose)
|
log.Errorf("antigravity executor: close response body error: %v", errClose)
|
||||||
}
|
}
|
||||||
if errRead != nil {
|
if errRead != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errRead)
|
helps.RecordAPIResponseError(ctx, e.cfg, errRead)
|
||||||
if errors.Is(errRead, context.Canceled) || errors.Is(errRead, context.DeadlineExceeded) {
|
if errors.Is(errRead, context.Canceled) || errors.Is(errRead, context.DeadlineExceeded) {
|
||||||
err = errRead
|
err = errRead
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1131,7 +1132,7 @@ attemptLoop:
|
|||||||
err = errRead
|
err = errRead
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, bodyBytes)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, bodyBytes)
|
||||||
if httpResp.StatusCode == http.StatusTooManyRequests {
|
if httpResp.StatusCode == http.StatusTooManyRequests {
|
||||||
if usedCreditsDirect {
|
if usedCreditsDirect {
|
||||||
if shouldMarkAntigravityCreditsExhausted(httpResp.StatusCode, bodyBytes, nil) {
|
if shouldMarkAntigravityCreditsExhausted(httpResp.StatusCode, bodyBytes, nil) {
|
||||||
@@ -1142,7 +1143,7 @@ attemptLoop:
|
|||||||
creditsResp, _ := e.attemptCreditsFallback(ctx, auth, httpClient, token, baseModel, translated, true, opts.Alt, baseURL, bodyBytes)
|
creditsResp, _ := e.attemptCreditsFallback(ctx, auth, httpClient, token, baseModel, translated, true, opts.Alt, baseURL, bodyBytes)
|
||||||
if creditsResp != nil {
|
if creditsResp != nil {
|
||||||
httpResp = creditsResp
|
httpResp = creditsResp
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1188,19 +1189,19 @@ attemptLoop:
|
|||||||
var param any
|
var param any
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Bytes()
|
line := scanner.Bytes()
|
||||||
appendAPIResponseChunk(ctx, e.cfg, line)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, line)
|
||||||
|
|
||||||
// Filter usage metadata for all models
|
// Filter usage metadata for all models
|
||||||
// Only retain usage statistics in the terminal chunk
|
// Only retain usage statistics in the terminal chunk
|
||||||
line = FilterSSEUsageMetadata(line)
|
line = helps.FilterSSEUsageMetadata(line)
|
||||||
|
|
||||||
payload := jsonPayload(line)
|
payload := helps.JSONPayload(line)
|
||||||
if payload == nil {
|
if payload == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if detail, ok := parseAntigravityStreamUsage(payload); ok {
|
if detail, ok := helps.ParseAntigravityStreamUsage(payload); ok {
|
||||||
reporter.publish(ctx, detail)
|
reporter.Publish(ctx, detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, bytes.Clone(payload), ¶m)
|
chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, bytes.Clone(payload), ¶m)
|
||||||
@@ -1213,11 +1214,11 @@ attemptLoop:
|
|||||||
out <- cliproxyexecutor.StreamChunk{Payload: tail[i]}
|
out <- cliproxyexecutor.StreamChunk{Payload: tail[i]}
|
||||||
}
|
}
|
||||||
if errScan := scanner.Err(); errScan != nil {
|
if errScan := scanner.Err(); errScan != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errScan)
|
helps.RecordAPIResponseError(ctx, e.cfg, errScan)
|
||||||
reporter.publishFailure(ctx)
|
reporter.PublishFailure(ctx)
|
||||||
out <- cliproxyexecutor.StreamChunk{Err: errScan}
|
out <- cliproxyexecutor.StreamChunk{Err: errScan}
|
||||||
} else {
|
} else {
|
||||||
reporter.ensurePublished(ctx)
|
reporter.EnsurePublished(ctx)
|
||||||
}
|
}
|
||||||
}(httpResp)
|
}(httpResp)
|
||||||
return &cliproxyexecutor.StreamResult{Headers: httpResp.Header.Clone(), Chunks: out}, nil
|
return &cliproxyexecutor.StreamResult{Headers: httpResp.Header.Clone(), Chunks: out}, nil
|
||||||
@@ -1320,7 +1321,7 @@ func (e *AntigravityExecutor) CountTokens(ctx context.Context, auth *cliproxyaut
|
|||||||
httpReq.Host = host
|
httpReq.Host = host
|
||||||
}
|
}
|
||||||
|
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: requestURL.String(),
|
URL: requestURL.String(),
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: httpReq.Header.Clone(),
|
Headers: httpReq.Header.Clone(),
|
||||||
@@ -1334,7 +1335,7 @@ func (e *AntigravityExecutor) CountTokens(ctx context.Context, auth *cliproxyaut
|
|||||||
|
|
||||||
httpResp, errDo := httpClient.Do(httpReq)
|
httpResp, errDo := httpClient.Do(httpReq)
|
||||||
if errDo != nil {
|
if errDo != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errDo)
|
helps.RecordAPIResponseError(ctx, e.cfg, errDo)
|
||||||
if errors.Is(errDo, context.Canceled) || errors.Is(errDo, context.DeadlineExceeded) {
|
if errors.Is(errDo, context.Canceled) || errors.Is(errDo, context.DeadlineExceeded) {
|
||||||
return cliproxyexecutor.Response{}, errDo
|
return cliproxyexecutor.Response{}, errDo
|
||||||
}
|
}
|
||||||
@@ -1348,16 +1349,16 @@ func (e *AntigravityExecutor) CountTokens(ctx context.Context, auth *cliproxyaut
|
|||||||
return cliproxyexecutor.Response{}, errDo
|
return cliproxyexecutor.Response{}, errDo
|
||||||
}
|
}
|
||||||
|
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||||
bodyBytes, errRead := io.ReadAll(httpResp.Body)
|
bodyBytes, errRead := io.ReadAll(httpResp.Body)
|
||||||
if errClose := httpResp.Body.Close(); errClose != nil {
|
if errClose := httpResp.Body.Close(); errClose != nil {
|
||||||
log.Errorf("antigravity executor: close response body error: %v", errClose)
|
log.Errorf("antigravity executor: close response body error: %v", errClose)
|
||||||
}
|
}
|
||||||
if errRead != nil {
|
if errRead != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errRead)
|
helps.RecordAPIResponseError(ctx, e.cfg, errRead)
|
||||||
return cliproxyexecutor.Response{}, errRead
|
return cliproxyexecutor.Response{}, errRead
|
||||||
}
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, bodyBytes)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, bodyBytes)
|
||||||
|
|
||||||
if httpResp.StatusCode >= http.StatusOK && httpResp.StatusCode < http.StatusMultipleChoices {
|
if httpResp.StatusCode >= http.StatusOK && httpResp.StatusCode < http.StatusMultipleChoices {
|
||||||
count := gjson.GetBytes(bodyBytes, "totalTokens").Int()
|
count := gjson.GetBytes(bodyBytes, "totalTokens").Int()
|
||||||
@@ -1624,7 +1625,7 @@ func (e *AntigravityExecutor) buildRequest(ctx context.Context, auth *cliproxyau
|
|||||||
if e.cfg != nil && e.cfg.RequestLog {
|
if e.cfg != nil && e.cfg.RequestLog {
|
||||||
payloadLog = []byte(payloadStr)
|
payloadLog = []byte(payloadStr)
|
||||||
}
|
}
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: requestURL.String(),
|
URL: requestURL.String(),
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: httpReq.Header.Clone(),
|
Headers: httpReq.Header.Clone(),
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
@@ -91,7 +92,7 @@ func (e *ClaudeExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
if err := e.PrepareRequest(httpReq, auth); err != nil {
|
if err := e.PrepareRequest(httpReq, auth); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
return httpClient.Do(httpReq)
|
return httpClient.Do(httpReq)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,8 +107,8 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
|||||||
baseURL = "https://api.anthropic.com"
|
baseURL = "https://api.anthropic.com"
|
||||||
}
|
}
|
||||||
|
|
||||||
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
||||||
defer reporter.trackFailure(ctx, &err)
|
defer reporter.TrackFailure(ctx, &err)
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("claude")
|
to := sdktranslator.FromString("claude")
|
||||||
// Use streaming translation to preserve function calling, except for claude.
|
// Use streaming translation to preserve function calling, except for claude.
|
||||||
@@ -130,8 +131,8 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
|||||||
// based on client type and configuration.
|
// based on client type and configuration.
|
||||||
body = applyCloaking(ctx, e.cfg, auth, body, baseModel, apiKey)
|
body = applyCloaking(ctx, e.cfg, auth, body, baseModel, apiKey)
|
||||||
|
|
||||||
requestedModel := payloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
||||||
body = ensureModelMaxTokens(body, baseModel)
|
body = ensureModelMaxTokens(body, baseModel)
|
||||||
|
|
||||||
// Disable thinking if tool_choice forces tool use (Anthropic API constraint)
|
// Disable thinking if tool_choice forces tool use (Anthropic API constraint)
|
||||||
@@ -172,7 +173,7 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
|||||||
authLabel = auth.Label
|
authLabel = auth.Label
|
||||||
authType, authValue = auth.AccountInfo()
|
authType, authValue = auth.AccountInfo()
|
||||||
}
|
}
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: url,
|
URL: url,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: httpReq.Header.Clone(),
|
Headers: httpReq.Header.Clone(),
|
||||||
@@ -184,33 +185,33 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
|||||||
AuthValue: authValue,
|
AuthValue: authValue,
|
||||||
})
|
})
|
||||||
|
|
||||||
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
httpResp, err := httpClient.Do(httpReq)
|
httpResp, err := httpClient.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||||
// Decompress error responses — pass the Content-Encoding value (may be empty)
|
// Decompress error responses — pass the Content-Encoding value (may be empty)
|
||||||
// and let decodeResponseBody handle both header-declared and magic-byte-detected
|
// and let decodeResponseBody handle both header-declared and magic-byte-detected
|
||||||
// compression. This keeps error-path behaviour consistent with the success path.
|
// compression. This keeps error-path behaviour consistent with the success path.
|
||||||
errBody, decErr := decodeResponseBody(httpResp.Body, httpResp.Header.Get("Content-Encoding"))
|
errBody, decErr := decodeResponseBody(httpResp.Body, httpResp.Header.Get("Content-Encoding"))
|
||||||
if decErr != nil {
|
if decErr != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, decErr)
|
helps.RecordAPIResponseError(ctx, e.cfg, decErr)
|
||||||
msg := fmt.Sprintf("failed to decode error response body: %v", decErr)
|
msg := fmt.Sprintf("failed to decode error response body: %v", decErr)
|
||||||
logWithRequestID(ctx).Warn(msg)
|
helps.LogWithRequestID(ctx).Warn(msg)
|
||||||
return resp, statusErr{code: httpResp.StatusCode, msg: msg}
|
return resp, statusErr{code: httpResp.StatusCode, msg: msg}
|
||||||
}
|
}
|
||||||
b, readErr := io.ReadAll(errBody)
|
b, readErr := io.ReadAll(errBody)
|
||||||
if readErr != nil {
|
if readErr != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, readErr)
|
helps.RecordAPIResponseError(ctx, e.cfg, readErr)
|
||||||
msg := fmt.Sprintf("failed to read error response body: %v", readErr)
|
msg := fmt.Sprintf("failed to read error response body: %v", readErr)
|
||||||
logWithRequestID(ctx).Warn(msg)
|
helps.LogWithRequestID(ctx).Warn(msg)
|
||||||
b = []byte(msg)
|
b = []byte(msg)
|
||||||
}
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, b)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, b)
|
||||||
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
||||||
err = statusErr{code: httpResp.StatusCode, msg: string(b)}
|
err = statusErr{code: httpResp.StatusCode, msg: string(b)}
|
||||||
if errClose := errBody.Close(); errClose != nil {
|
if errClose := errBody.Close(); errClose != nil {
|
||||||
log.Errorf("response body close error: %v", errClose)
|
log.Errorf("response body close error: %v", errClose)
|
||||||
@@ -219,7 +220,7 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
|||||||
}
|
}
|
||||||
decodedBody, err := decodeResponseBody(httpResp.Body, httpResp.Header.Get("Content-Encoding"))
|
decodedBody, err := decodeResponseBody(httpResp.Body, httpResp.Header.Get("Content-Encoding"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
if errClose := httpResp.Body.Close(); errClose != nil {
|
if errClose := httpResp.Body.Close(); errClose != nil {
|
||||||
log.Errorf("response body close error: %v", errClose)
|
log.Errorf("response body close error: %v", errClose)
|
||||||
}
|
}
|
||||||
@@ -232,19 +233,19 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
|||||||
}()
|
}()
|
||||||
data, err := io.ReadAll(decodedBody)
|
data, err := io.ReadAll(decodedBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, data)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, data)
|
||||||
if stream {
|
if stream {
|
||||||
lines := bytes.Split(data, []byte("\n"))
|
lines := bytes.Split(data, []byte("\n"))
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
if detail, ok := parseClaudeStreamUsage(line); ok {
|
if detail, ok := helps.ParseClaudeStreamUsage(line); ok {
|
||||||
reporter.publish(ctx, detail)
|
reporter.Publish(ctx, detail)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
reporter.publish(ctx, parseClaudeUsage(data))
|
reporter.Publish(ctx, helps.ParseClaudeUsage(data))
|
||||||
}
|
}
|
||||||
if isClaudeOAuthToken(apiKey) && !auth.ToolPrefixDisabled() {
|
if isClaudeOAuthToken(apiKey) && !auth.ToolPrefixDisabled() {
|
||||||
data = stripClaudeToolPrefixFromResponse(data, claudeToolPrefix)
|
data = stripClaudeToolPrefixFromResponse(data, claudeToolPrefix)
|
||||||
@@ -275,8 +276,8 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
|||||||
baseURL = "https://api.anthropic.com"
|
baseURL = "https://api.anthropic.com"
|
||||||
}
|
}
|
||||||
|
|
||||||
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
||||||
defer reporter.trackFailure(ctx, &err)
|
defer reporter.TrackFailure(ctx, &err)
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("claude")
|
to := sdktranslator.FromString("claude")
|
||||||
originalPayloadSource := req.Payload
|
originalPayloadSource := req.Payload
|
||||||
@@ -297,8 +298,8 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
|||||||
// based on client type and configuration.
|
// based on client type and configuration.
|
||||||
body = applyCloaking(ctx, e.cfg, auth, body, baseModel, apiKey)
|
body = applyCloaking(ctx, e.cfg, auth, body, baseModel, apiKey)
|
||||||
|
|
||||||
requestedModel := payloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
||||||
body = ensureModelMaxTokens(body, baseModel)
|
body = ensureModelMaxTokens(body, baseModel)
|
||||||
|
|
||||||
// Disable thinking if tool_choice forces tool use (Anthropic API constraint)
|
// Disable thinking if tool_choice forces tool use (Anthropic API constraint)
|
||||||
@@ -336,7 +337,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
|||||||
authLabel = auth.Label
|
authLabel = auth.Label
|
||||||
authType, authValue = auth.AccountInfo()
|
authType, authValue = auth.AccountInfo()
|
||||||
}
|
}
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: url,
|
URL: url,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: httpReq.Header.Clone(),
|
Headers: httpReq.Header.Clone(),
|
||||||
@@ -348,33 +349,33 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
|||||||
AuthValue: authValue,
|
AuthValue: authValue,
|
||||||
})
|
})
|
||||||
|
|
||||||
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
httpResp, err := httpClient.Do(httpReq)
|
httpResp, err := httpClient.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||||
// Decompress error responses — pass the Content-Encoding value (may be empty)
|
// Decompress error responses — pass the Content-Encoding value (may be empty)
|
||||||
// and let decodeResponseBody handle both header-declared and magic-byte-detected
|
// and let decodeResponseBody handle both header-declared and magic-byte-detected
|
||||||
// compression. This keeps error-path behaviour consistent with the success path.
|
// compression. This keeps error-path behaviour consistent with the success path.
|
||||||
errBody, decErr := decodeResponseBody(httpResp.Body, httpResp.Header.Get("Content-Encoding"))
|
errBody, decErr := decodeResponseBody(httpResp.Body, httpResp.Header.Get("Content-Encoding"))
|
||||||
if decErr != nil {
|
if decErr != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, decErr)
|
helps.RecordAPIResponseError(ctx, e.cfg, decErr)
|
||||||
msg := fmt.Sprintf("failed to decode error response body: %v", decErr)
|
msg := fmt.Sprintf("failed to decode error response body: %v", decErr)
|
||||||
logWithRequestID(ctx).Warn(msg)
|
helps.LogWithRequestID(ctx).Warn(msg)
|
||||||
return nil, statusErr{code: httpResp.StatusCode, msg: msg}
|
return nil, statusErr{code: httpResp.StatusCode, msg: msg}
|
||||||
}
|
}
|
||||||
b, readErr := io.ReadAll(errBody)
|
b, readErr := io.ReadAll(errBody)
|
||||||
if readErr != nil {
|
if readErr != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, readErr)
|
helps.RecordAPIResponseError(ctx, e.cfg, readErr)
|
||||||
msg := fmt.Sprintf("failed to read error response body: %v", readErr)
|
msg := fmt.Sprintf("failed to read error response body: %v", readErr)
|
||||||
logWithRequestID(ctx).Warn(msg)
|
helps.LogWithRequestID(ctx).Warn(msg)
|
||||||
b = []byte(msg)
|
b = []byte(msg)
|
||||||
}
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, b)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, b)
|
||||||
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
||||||
if errClose := errBody.Close(); errClose != nil {
|
if errClose := errBody.Close(); errClose != nil {
|
||||||
log.Errorf("response body close error: %v", errClose)
|
log.Errorf("response body close error: %v", errClose)
|
||||||
}
|
}
|
||||||
@@ -383,7 +384,7 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
|||||||
}
|
}
|
||||||
decodedBody, err := decodeResponseBody(httpResp.Body, httpResp.Header.Get("Content-Encoding"))
|
decodedBody, err := decodeResponseBody(httpResp.Body, httpResp.Header.Get("Content-Encoding"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
if errClose := httpResp.Body.Close(); errClose != nil {
|
if errClose := httpResp.Body.Close(); errClose != nil {
|
||||||
log.Errorf("response body close error: %v", errClose)
|
log.Errorf("response body close error: %v", errClose)
|
||||||
}
|
}
|
||||||
@@ -404,9 +405,9 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
|||||||
scanner.Buffer(nil, 52_428_800) // 50MB
|
scanner.Buffer(nil, 52_428_800) // 50MB
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Bytes()
|
line := scanner.Bytes()
|
||||||
appendAPIResponseChunk(ctx, e.cfg, line)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, line)
|
||||||
if detail, ok := parseClaudeStreamUsage(line); ok {
|
if detail, ok := helps.ParseClaudeStreamUsage(line); ok {
|
||||||
reporter.publish(ctx, detail)
|
reporter.Publish(ctx, detail)
|
||||||
}
|
}
|
||||||
if isClaudeOAuthToken(apiKey) && !auth.ToolPrefixDisabled() {
|
if isClaudeOAuthToken(apiKey) && !auth.ToolPrefixDisabled() {
|
||||||
line = stripClaudeToolPrefixFromStreamLine(line, claudeToolPrefix)
|
line = stripClaudeToolPrefixFromStreamLine(line, claudeToolPrefix)
|
||||||
@@ -418,8 +419,8 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
|||||||
out <- cliproxyexecutor.StreamChunk{Payload: cloned}
|
out <- cliproxyexecutor.StreamChunk{Payload: cloned}
|
||||||
}
|
}
|
||||||
if errScan := scanner.Err(); errScan != nil {
|
if errScan := scanner.Err(); errScan != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errScan)
|
helps.RecordAPIResponseError(ctx, e.cfg, errScan)
|
||||||
reporter.publishFailure(ctx)
|
reporter.PublishFailure(ctx)
|
||||||
out <- cliproxyexecutor.StreamChunk{Err: errScan}
|
out <- cliproxyexecutor.StreamChunk{Err: errScan}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -431,9 +432,9 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
|||||||
var param any
|
var param any
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Bytes()
|
line := scanner.Bytes()
|
||||||
appendAPIResponseChunk(ctx, e.cfg, line)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, line)
|
||||||
if detail, ok := parseClaudeStreamUsage(line); ok {
|
if detail, ok := helps.ParseClaudeStreamUsage(line); ok {
|
||||||
reporter.publish(ctx, detail)
|
reporter.Publish(ctx, detail)
|
||||||
}
|
}
|
||||||
if isClaudeOAuthToken(apiKey) && !auth.ToolPrefixDisabled() {
|
if isClaudeOAuthToken(apiKey) && !auth.ToolPrefixDisabled() {
|
||||||
line = stripClaudeToolPrefixFromStreamLine(line, claudeToolPrefix)
|
line = stripClaudeToolPrefixFromStreamLine(line, claudeToolPrefix)
|
||||||
@@ -453,8 +454,8 @@ func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if errScan := scanner.Err(); errScan != nil {
|
if errScan := scanner.Err(); errScan != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errScan)
|
helps.RecordAPIResponseError(ctx, e.cfg, errScan)
|
||||||
reporter.publishFailure(ctx)
|
reporter.PublishFailure(ctx)
|
||||||
out <- cliproxyexecutor.StreamChunk{Err: errScan}
|
out <- cliproxyexecutor.StreamChunk{Err: errScan}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -503,7 +504,7 @@ func (e *ClaudeExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
authLabel = auth.Label
|
authLabel = auth.Label
|
||||||
authType, authValue = auth.AccountInfo()
|
authType, authValue = auth.AccountInfo()
|
||||||
}
|
}
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: url,
|
URL: url,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: httpReq.Header.Clone(),
|
Headers: httpReq.Header.Clone(),
|
||||||
@@ -515,32 +516,32 @@ func (e *ClaudeExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
AuthValue: authValue,
|
AuthValue: authValue,
|
||||||
})
|
})
|
||||||
|
|
||||||
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
resp, err := httpClient.Do(httpReq)
|
resp, err := httpClient.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
return cliproxyexecutor.Response{}, err
|
return cliproxyexecutor.Response{}, err
|
||||||
}
|
}
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone())
|
||||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
// Decompress error responses — pass the Content-Encoding value (may be empty)
|
// Decompress error responses — pass the Content-Encoding value (may be empty)
|
||||||
// and let decodeResponseBody handle both header-declared and magic-byte-detected
|
// and let decodeResponseBody handle both header-declared and magic-byte-detected
|
||||||
// compression. This keeps error-path behaviour consistent with the success path.
|
// compression. This keeps error-path behaviour consistent with the success path.
|
||||||
errBody, decErr := decodeResponseBody(resp.Body, resp.Header.Get("Content-Encoding"))
|
errBody, decErr := decodeResponseBody(resp.Body, resp.Header.Get("Content-Encoding"))
|
||||||
if decErr != nil {
|
if decErr != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, decErr)
|
helps.RecordAPIResponseError(ctx, e.cfg, decErr)
|
||||||
msg := fmt.Sprintf("failed to decode error response body: %v", decErr)
|
msg := fmt.Sprintf("failed to decode error response body: %v", decErr)
|
||||||
logWithRequestID(ctx).Warn(msg)
|
helps.LogWithRequestID(ctx).Warn(msg)
|
||||||
return cliproxyexecutor.Response{}, statusErr{code: resp.StatusCode, msg: msg}
|
return cliproxyexecutor.Response{}, statusErr{code: resp.StatusCode, msg: msg}
|
||||||
}
|
}
|
||||||
b, readErr := io.ReadAll(errBody)
|
b, readErr := io.ReadAll(errBody)
|
||||||
if readErr != nil {
|
if readErr != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, readErr)
|
helps.RecordAPIResponseError(ctx, e.cfg, readErr)
|
||||||
msg := fmt.Sprintf("failed to read error response body: %v", readErr)
|
msg := fmt.Sprintf("failed to read error response body: %v", readErr)
|
||||||
logWithRequestID(ctx).Warn(msg)
|
helps.LogWithRequestID(ctx).Warn(msg)
|
||||||
b = []byte(msg)
|
b = []byte(msg)
|
||||||
}
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, b)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, b)
|
||||||
if errClose := errBody.Close(); errClose != nil {
|
if errClose := errBody.Close(); errClose != nil {
|
||||||
log.Errorf("response body close error: %v", errClose)
|
log.Errorf("response body close error: %v", errClose)
|
||||||
}
|
}
|
||||||
@@ -548,7 +549,7 @@ func (e *ClaudeExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
}
|
}
|
||||||
decodedBody, err := decodeResponseBody(resp.Body, resp.Header.Get("Content-Encoding"))
|
decodedBody, err := decodeResponseBody(resp.Body, resp.Header.Get("Content-Encoding"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
if errClose := resp.Body.Close(); errClose != nil {
|
if errClose := resp.Body.Close(); errClose != nil {
|
||||||
log.Errorf("response body close error: %v", errClose)
|
log.Errorf("response body close error: %v", errClose)
|
||||||
}
|
}
|
||||||
@@ -561,10 +562,10 @@ func (e *ClaudeExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
}()
|
}()
|
||||||
data, err := io.ReadAll(decodedBody)
|
data, err := io.ReadAll(decodedBody)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
return cliproxyexecutor.Response{}, err
|
return cliproxyexecutor.Response{}, err
|
||||||
}
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, data)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, data)
|
||||||
count := gjson.GetBytes(data, "input_tokens").Int()
|
count := gjson.GetBytes(data, "input_tokens").Int()
|
||||||
out := sdktranslator.TranslateTokenCount(ctx, to, from, count, data)
|
out := sdktranslator.TranslateTokenCount(ctx, to, from, count, data)
|
||||||
return cliproxyexecutor.Response{Payload: out, Headers: resp.Header.Clone()}, nil
|
return cliproxyexecutor.Response{Payload: out, Headers: resp.Header.Clone()}, nil
|
||||||
@@ -800,10 +801,10 @@ func applyClaudeHeaders(r *http.Request, auth *cliproxyauth.Auth, apiKey string,
|
|||||||
if ginCtx, ok := r.Context().Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil {
|
if ginCtx, ok := r.Context().Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil {
|
||||||
ginHeaders = ginCtx.Request.Header
|
ginHeaders = ginCtx.Request.Header
|
||||||
}
|
}
|
||||||
stabilizeDeviceProfile := claudeDeviceProfileStabilizationEnabled(cfg)
|
stabilizeDeviceProfile := helps.ClaudeDeviceProfileStabilizationEnabled(cfg)
|
||||||
var deviceProfile claudeDeviceProfile
|
var deviceProfile helps.ClaudeDeviceProfile
|
||||||
if stabilizeDeviceProfile {
|
if stabilizeDeviceProfile {
|
||||||
deviceProfile = resolveClaudeDeviceProfile(auth, apiKey, ginHeaders, cfg)
|
deviceProfile = helps.ResolveClaudeDeviceProfile(auth, apiKey, ginHeaders, cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
baseBetas := "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05"
|
baseBetas := "claude-code-20250219,oauth-2025-04-20,interleaved-thinking-2025-05-14,context-management-2025-06-27,prompt-caching-scope-2026-01-05"
|
||||||
@@ -871,9 +872,9 @@ func applyClaudeHeaders(r *http.Request, auth *cliproxyauth.Auth, apiKey string,
|
|||||||
}
|
}
|
||||||
util.ApplyCustomHeadersFromAttrs(r, attrs)
|
util.ApplyCustomHeadersFromAttrs(r, attrs)
|
||||||
if stabilizeDeviceProfile {
|
if stabilizeDeviceProfile {
|
||||||
applyClaudeDeviceProfileHeaders(r, deviceProfile)
|
helps.ApplyClaudeDeviceProfileHeaders(r, deviceProfile)
|
||||||
} else {
|
} else {
|
||||||
applyClaudeLegacyDeviceHeaders(r, ginHeaders, cfg)
|
helps.ApplyClaudeLegacyDeviceHeaders(r, ginHeaders, cfg)
|
||||||
}
|
}
|
||||||
// Re-enforce Accept-Encoding: identity after ApplyCustomHeadersFromAttrs, which
|
// Re-enforce Accept-Encoding: identity after ApplyCustomHeadersFromAttrs, which
|
||||||
// may override it with a user-configured value. Compressed SSE breaks the line
|
// may override it with a user-configured value. Compressed SSE breaks the line
|
||||||
@@ -1044,7 +1045,7 @@ func stripClaudeToolPrefixFromStreamLine(line []byte, prefix string) []byte {
|
|||||||
if prefix == "" {
|
if prefix == "" {
|
||||||
return line
|
return line
|
||||||
}
|
}
|
||||||
payload := jsonPayload(line)
|
payload := helps.JSONPayload(line)
|
||||||
if len(payload) == 0 || !gjson.ValidBytes(payload) {
|
if len(payload) == 0 || !gjson.ValidBytes(payload) {
|
||||||
return line
|
return line
|
||||||
}
|
}
|
||||||
@@ -1156,9 +1157,9 @@ func resolveClaudeKeyCloakConfig(cfg *config.Config, auth *cliproxyauth.Auth) *c
|
|||||||
func injectFakeUserID(payload []byte, apiKey string, useCache bool) []byte {
|
func injectFakeUserID(payload []byte, apiKey string, useCache bool) []byte {
|
||||||
generateID := func() string {
|
generateID := func() string {
|
||||||
if useCache {
|
if useCache {
|
||||||
return cachedUserID(apiKey)
|
return helps.CachedUserID(apiKey)
|
||||||
}
|
}
|
||||||
return generateFakeUserID()
|
return helps.GenerateFakeUserID()
|
||||||
}
|
}
|
||||||
|
|
||||||
metadata := gjson.GetBytes(payload, "metadata")
|
metadata := gjson.GetBytes(payload, "metadata")
|
||||||
@@ -1168,7 +1169,7 @@ func injectFakeUserID(payload []byte, apiKey string, useCache bool) []byte {
|
|||||||
}
|
}
|
||||||
|
|
||||||
existingUserID := gjson.GetBytes(payload, "metadata.user_id").String()
|
existingUserID := gjson.GetBytes(payload, "metadata.user_id").String()
|
||||||
if existingUserID == "" || !isValidUserID(existingUserID) {
|
if existingUserID == "" || !helps.IsValidUserID(existingUserID) {
|
||||||
payload, _ = sjson.SetBytes(payload, "metadata.user_id", generateID())
|
payload, _ = sjson.SetBytes(payload, "metadata.user_id", generateID())
|
||||||
}
|
}
|
||||||
return payload
|
return payload
|
||||||
@@ -1292,7 +1293,7 @@ func applyCloaking(ctx context.Context, cfg *config.Config, auth *cliproxyauth.A
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Determine if cloaking should be applied
|
// Determine if cloaking should be applied
|
||||||
if !shouldCloak(cloakMode, clientUserAgent) {
|
if !helps.ShouldCloak(cloakMode, clientUserAgent) {
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1306,8 +1307,8 @@ func applyCloaking(ctx context.Context, cfg *config.Config, auth *cliproxyauth.A
|
|||||||
|
|
||||||
// Apply sensitive word obfuscation
|
// Apply sensitive word obfuscation
|
||||||
if len(sensitiveWords) > 0 {
|
if len(sensitiveWords) > 0 {
|
||||||
matcher := buildSensitiveWordMatcher(sensitiveWords)
|
matcher := helps.BuildSensitiveWordMatcher(sensitiveWords)
|
||||||
payload = obfuscateSensitiveWords(payload, matcher)
|
payload = helps.ObfuscateSensitiveWords(payload, matcher)
|
||||||
}
|
}
|
||||||
|
|
||||||
return payload
|
return payload
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/klauspost/compress/zstd"
|
"github.com/klauspost/compress/zstd"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
|
||||||
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
||||||
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
|
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
|
||||||
@@ -24,9 +25,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func resetClaudeDeviceProfileCache() {
|
func resetClaudeDeviceProfileCache() {
|
||||||
claudeDeviceProfileCacheMu.Lock()
|
helps.ResetClaudeDeviceProfileCache()
|
||||||
claudeDeviceProfileCache = make(map[string]claudeDeviceProfileCacheEntry)
|
|
||||||
claudeDeviceProfileCacheMu.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func newClaudeHeaderTestRequest(t *testing.T, incoming http.Header) *http.Request {
|
func newClaudeHeaderTestRequest(t *testing.T, incoming http.Header) *http.Request {
|
||||||
@@ -339,7 +338,7 @@ func TestResolveClaudeDeviceProfile_RechecksCacheBeforeStoringCandidate(t *testi
|
|||||||
var pauseOnce sync.Once
|
var pauseOnce sync.Once
|
||||||
var releaseOnce sync.Once
|
var releaseOnce sync.Once
|
||||||
|
|
||||||
claudeDeviceProfileBeforeCandidateStore = func(candidate claudeDeviceProfile) {
|
helps.ClaudeDeviceProfileBeforeCandidateStore = func(candidate helps.ClaudeDeviceProfile) {
|
||||||
if candidate.UserAgent != "claude-cli/2.1.62 (external, cli)" {
|
if candidate.UserAgent != "claude-cli/2.1.62 (external, cli)" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -347,13 +346,13 @@ func TestResolveClaudeDeviceProfile_RechecksCacheBeforeStoringCandidate(t *testi
|
|||||||
<-releaseLow
|
<-releaseLow
|
||||||
}
|
}
|
||||||
t.Cleanup(func() {
|
t.Cleanup(func() {
|
||||||
claudeDeviceProfileBeforeCandidateStore = nil
|
helps.ClaudeDeviceProfileBeforeCandidateStore = nil
|
||||||
releaseOnce.Do(func() { close(releaseLow) })
|
releaseOnce.Do(func() { close(releaseLow) })
|
||||||
})
|
})
|
||||||
|
|
||||||
lowResultCh := make(chan claudeDeviceProfile, 1)
|
lowResultCh := make(chan helps.ClaudeDeviceProfile, 1)
|
||||||
go func() {
|
go func() {
|
||||||
lowResultCh <- resolveClaudeDeviceProfile(auth, "key-racy-upgrade", http.Header{
|
lowResultCh <- helps.ResolveClaudeDeviceProfile(auth, "key-racy-upgrade", http.Header{
|
||||||
"User-Agent": []string{"claude-cli/2.1.62 (external, cli)"},
|
"User-Agent": []string{"claude-cli/2.1.62 (external, cli)"},
|
||||||
"X-Stainless-Package-Version": []string{"0.74.0"},
|
"X-Stainless-Package-Version": []string{"0.74.0"},
|
||||||
"X-Stainless-Runtime-Version": []string{"v24.3.0"},
|
"X-Stainless-Runtime-Version": []string{"v24.3.0"},
|
||||||
@@ -368,7 +367,7 @@ func TestResolveClaudeDeviceProfile_RechecksCacheBeforeStoringCandidate(t *testi
|
|||||||
t.Fatal("timed out waiting for lower candidate to pause before storing")
|
t.Fatal("timed out waiting for lower candidate to pause before storing")
|
||||||
}
|
}
|
||||||
|
|
||||||
highResult := resolveClaudeDeviceProfile(auth, "key-racy-upgrade", http.Header{
|
highResult := helps.ResolveClaudeDeviceProfile(auth, "key-racy-upgrade", http.Header{
|
||||||
"User-Agent": []string{"claude-cli/2.1.63 (external, cli)"},
|
"User-Agent": []string{"claude-cli/2.1.63 (external, cli)"},
|
||||||
"X-Stainless-Package-Version": []string{"0.75.0"},
|
"X-Stainless-Package-Version": []string{"0.75.0"},
|
||||||
"X-Stainless-Runtime-Version": []string{"v24.4.0"},
|
"X-Stainless-Runtime-Version": []string{"v24.4.0"},
|
||||||
@@ -399,7 +398,7 @@ func TestResolveClaudeDeviceProfile_RechecksCacheBeforeStoringCandidate(t *testi
|
|||||||
t.Fatalf("highResult platform = %s/%s, want %s/%s", highResult.OS, highResult.Arch, "MacOS", "arm64")
|
t.Fatalf("highResult platform = %s/%s, want %s/%s", highResult.OS, highResult.Arch, "MacOS", "arm64")
|
||||||
}
|
}
|
||||||
|
|
||||||
cached := resolveClaudeDeviceProfile(auth, "key-racy-upgrade", http.Header{
|
cached := helps.ResolveClaudeDeviceProfile(auth, "key-racy-upgrade", http.Header{
|
||||||
"User-Agent": []string{"curl/8.7.1"},
|
"User-Agent": []string{"curl/8.7.1"},
|
||||||
}, cfg)
|
}, cfg)
|
||||||
if cached.UserAgent != "claude-cli/2.1.63 (external, cli)" {
|
if cached.UserAgent != "claude-cli/2.1.63 (external, cli)" {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
codexauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex"
|
codexauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/codex"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
@@ -73,7 +74,7 @@ func (e *CodexExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Auth
|
|||||||
if err := e.PrepareRequest(httpReq, auth); err != nil {
|
if err := e.PrepareRequest(httpReq, auth); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
return httpClient.Do(httpReq)
|
return httpClient.Do(httpReq)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -88,8 +89,8 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
|
|||||||
baseURL = "https://chatgpt.com/backend-api/codex"
|
baseURL = "https://chatgpt.com/backend-api/codex"
|
||||||
}
|
}
|
||||||
|
|
||||||
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
||||||
defer reporter.trackFailure(ctx, &err)
|
defer reporter.TrackFailure(ctx, &err)
|
||||||
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("codex")
|
to := sdktranslator.FromString("codex")
|
||||||
@@ -106,8 +107,8 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
|
|||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
requestedModel := payloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
||||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||||
body, _ = sjson.SetBytes(body, "stream", true)
|
body, _ = sjson.SetBytes(body, "stream", true)
|
||||||
body, _ = sjson.DeleteBytes(body, "previous_response_id")
|
body, _ = sjson.DeleteBytes(body, "previous_response_id")
|
||||||
@@ -128,7 +129,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
|
|||||||
authLabel = auth.Label
|
authLabel = auth.Label
|
||||||
authType, authValue = auth.AccountInfo()
|
authType, authValue = auth.AccountInfo()
|
||||||
}
|
}
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: url,
|
URL: url,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: httpReq.Header.Clone(),
|
Headers: httpReq.Header.Clone(),
|
||||||
@@ -139,10 +140,10 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
|
|||||||
AuthType: authType,
|
AuthType: authType,
|
||||||
AuthValue: authValue,
|
AuthValue: authValue,
|
||||||
})
|
})
|
||||||
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
httpResp, err := httpClient.Do(httpReq)
|
httpResp, err := httpClient.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -150,20 +151,20 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
|
|||||||
log.Errorf("codex executor: close response body error: %v", errClose)
|
log.Errorf("codex executor: close response body error: %v", errClose)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||||
b, _ := io.ReadAll(httpResp.Body)
|
b, _ := io.ReadAll(httpResp.Body)
|
||||||
appendAPIResponseChunk(ctx, e.cfg, b)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, b)
|
||||||
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
||||||
err = newCodexStatusErr(httpResp.StatusCode, b)
|
err = newCodexStatusErr(httpResp.StatusCode, b)
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
data, err := io.ReadAll(httpResp.Body)
|
data, err := io.ReadAll(httpResp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, data)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, data)
|
||||||
|
|
||||||
lines := bytes.Split(data, []byte("\n"))
|
lines := bytes.Split(data, []byte("\n"))
|
||||||
for _, line := range lines {
|
for _, line := range lines {
|
||||||
@@ -176,8 +177,8 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if detail, ok := parseCodexUsage(line); ok {
|
if detail, ok := helps.ParseCodexUsage(line); ok {
|
||||||
reporter.publish(ctx, detail)
|
reporter.Publish(ctx, detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
var param any
|
var param any
|
||||||
@@ -197,8 +198,8 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A
|
|||||||
baseURL = "https://chatgpt.com/backend-api/codex"
|
baseURL = "https://chatgpt.com/backend-api/codex"
|
||||||
}
|
}
|
||||||
|
|
||||||
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
||||||
defer reporter.trackFailure(ctx, &err)
|
defer reporter.TrackFailure(ctx, &err)
|
||||||
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("openai-response")
|
to := sdktranslator.FromString("openai-response")
|
||||||
@@ -215,8 +216,8 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A
|
|||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
requestedModel := payloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
||||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||||
body, _ = sjson.DeleteBytes(body, "stream")
|
body, _ = sjson.DeleteBytes(body, "stream")
|
||||||
body = normalizeCodexInstructions(body)
|
body = normalizeCodexInstructions(body)
|
||||||
@@ -233,7 +234,7 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A
|
|||||||
authLabel = auth.Label
|
authLabel = auth.Label
|
||||||
authType, authValue = auth.AccountInfo()
|
authType, authValue = auth.AccountInfo()
|
||||||
}
|
}
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: url,
|
URL: url,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: httpReq.Header.Clone(),
|
Headers: httpReq.Header.Clone(),
|
||||||
@@ -244,10 +245,10 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A
|
|||||||
AuthType: authType,
|
AuthType: authType,
|
||||||
AuthValue: authValue,
|
AuthValue: authValue,
|
||||||
})
|
})
|
||||||
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
httpResp, err := httpClient.Do(httpReq)
|
httpResp, err := httpClient.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -255,22 +256,22 @@ func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.A
|
|||||||
log.Errorf("codex executor: close response body error: %v", errClose)
|
log.Errorf("codex executor: close response body error: %v", errClose)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||||
b, _ := io.ReadAll(httpResp.Body)
|
b, _ := io.ReadAll(httpResp.Body)
|
||||||
appendAPIResponseChunk(ctx, e.cfg, b)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, b)
|
||||||
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
||||||
err = newCodexStatusErr(httpResp.StatusCode, b)
|
err = newCodexStatusErr(httpResp.StatusCode, b)
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
data, err := io.ReadAll(httpResp.Body)
|
data, err := io.ReadAll(httpResp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, data)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, data)
|
||||||
reporter.publish(ctx, parseOpenAIUsage(data))
|
reporter.Publish(ctx, helps.ParseOpenAIUsage(data))
|
||||||
reporter.ensurePublished(ctx)
|
reporter.EnsurePublished(ctx)
|
||||||
var param any
|
var param any
|
||||||
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, originalPayload, body, data, ¶m)
|
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, originalPayload, body, data, ¶m)
|
||||||
resp = cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()}
|
resp = cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()}
|
||||||
@@ -288,8 +289,8 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
|
|||||||
baseURL = "https://chatgpt.com/backend-api/codex"
|
baseURL = "https://chatgpt.com/backend-api/codex"
|
||||||
}
|
}
|
||||||
|
|
||||||
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
||||||
defer reporter.trackFailure(ctx, &err)
|
defer reporter.TrackFailure(ctx, &err)
|
||||||
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("codex")
|
to := sdktranslator.FromString("codex")
|
||||||
@@ -306,8 +307,8 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
requestedModel := payloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
||||||
body, _ = sjson.DeleteBytes(body, "previous_response_id")
|
body, _ = sjson.DeleteBytes(body, "previous_response_id")
|
||||||
body, _ = sjson.DeleteBytes(body, "prompt_cache_retention")
|
body, _ = sjson.DeleteBytes(body, "prompt_cache_retention")
|
||||||
body, _ = sjson.DeleteBytes(body, "safety_identifier")
|
body, _ = sjson.DeleteBytes(body, "safety_identifier")
|
||||||
@@ -327,7 +328,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
|
|||||||
authLabel = auth.Label
|
authLabel = auth.Label
|
||||||
authType, authValue = auth.AccountInfo()
|
authType, authValue = auth.AccountInfo()
|
||||||
}
|
}
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: url,
|
URL: url,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: httpReq.Header.Clone(),
|
Headers: httpReq.Header.Clone(),
|
||||||
@@ -339,24 +340,24 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
|
|||||||
AuthValue: authValue,
|
AuthValue: authValue,
|
||||||
})
|
})
|
||||||
|
|
||||||
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
httpResp, err := httpClient.Do(httpReq)
|
httpResp, err := httpClient.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||||
data, readErr := io.ReadAll(httpResp.Body)
|
data, readErr := io.ReadAll(httpResp.Body)
|
||||||
if errClose := httpResp.Body.Close(); errClose != nil {
|
if errClose := httpResp.Body.Close(); errClose != nil {
|
||||||
log.Errorf("codex executor: close response body error: %v", errClose)
|
log.Errorf("codex executor: close response body error: %v", errClose)
|
||||||
}
|
}
|
||||||
if readErr != nil {
|
if readErr != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, readErr)
|
helps.RecordAPIResponseError(ctx, e.cfg, readErr)
|
||||||
return nil, readErr
|
return nil, readErr
|
||||||
}
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, data)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, data)
|
||||||
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), data))
|
helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), data))
|
||||||
err = newCodexStatusErr(httpResp.StatusCode, data)
|
err = newCodexStatusErr(httpResp.StatusCode, data)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -373,13 +374,13 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
|
|||||||
var param any
|
var param any
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Bytes()
|
line := scanner.Bytes()
|
||||||
appendAPIResponseChunk(ctx, e.cfg, line)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, line)
|
||||||
|
|
||||||
if bytes.HasPrefix(line, dataTag) {
|
if bytes.HasPrefix(line, dataTag) {
|
||||||
data := bytes.TrimSpace(line[5:])
|
data := bytes.TrimSpace(line[5:])
|
||||||
if gjson.GetBytes(data, "type").String() == "response.completed" {
|
if gjson.GetBytes(data, "type").String() == "response.completed" {
|
||||||
if detail, ok := parseCodexUsage(data); ok {
|
if detail, ok := helps.ParseCodexUsage(data); ok {
|
||||||
reporter.publish(ctx, detail)
|
reporter.Publish(ctx, detail)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -390,8 +391,8 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if errScan := scanner.Err(); errScan != nil {
|
if errScan := scanner.Err(); errScan != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errScan)
|
helps.RecordAPIResponseError(ctx, e.cfg, errScan)
|
||||||
reporter.publishFailure(ctx)
|
reporter.PublishFailure(ctx)
|
||||||
out <- cliproxyexecutor.StreamChunk{Err: errScan}
|
out <- cliproxyexecutor.StreamChunk{Err: errScan}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -595,18 +596,18 @@ func (e *CodexExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *CodexExecutor) cacheHelper(ctx context.Context, from sdktranslator.Format, url string, req cliproxyexecutor.Request, rawJSON []byte) (*http.Request, error) {
|
func (e *CodexExecutor) cacheHelper(ctx context.Context, from sdktranslator.Format, url string, req cliproxyexecutor.Request, rawJSON []byte) (*http.Request, error) {
|
||||||
var cache codexCache
|
var cache helps.CodexCache
|
||||||
if from == "claude" {
|
if from == "claude" {
|
||||||
userIDResult := gjson.GetBytes(req.Payload, "metadata.user_id")
|
userIDResult := gjson.GetBytes(req.Payload, "metadata.user_id")
|
||||||
if userIDResult.Exists() {
|
if userIDResult.Exists() {
|
||||||
key := fmt.Sprintf("%s-%s", req.Model, userIDResult.String())
|
key := fmt.Sprintf("%s-%s", req.Model, userIDResult.String())
|
||||||
var ok bool
|
var ok bool
|
||||||
if cache, ok = getCodexCache(key); !ok {
|
if cache, ok = helps.GetCodexCache(key); !ok {
|
||||||
cache = codexCache{
|
cache = helps.CodexCache{
|
||||||
ID: uuid.New().String(),
|
ID: uuid.New().String(),
|
||||||
Expire: time.Now().Add(1 * time.Hour),
|
Expire: time.Now().Add(1 * time.Hour),
|
||||||
}
|
}
|
||||||
setCodexCache(key, cache)
|
helps.SetCodexCache(key, cache)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if from == "openai-response" {
|
} else if from == "openai-response" {
|
||||||
@@ -615,7 +616,7 @@ func (e *CodexExecutor) cacheHelper(ctx context.Context, from sdktranslator.Form
|
|||||||
cache.ID = promptCacheKey.String()
|
cache.ID = promptCacheKey.String()
|
||||||
}
|
}
|
||||||
} else if from == "openai" {
|
} else if from == "openai" {
|
||||||
if apiKey := strings.TrimSpace(apiKeyFromContext(ctx)); apiKey != "" {
|
if apiKey := strings.TrimSpace(helps.APIKeyFromContext(ctx)); apiKey != "" {
|
||||||
cache.ID = uuid.NewSHA1(uuid.NameSpaceOID, []byte("cli-proxy-api:codex:prompt-cache:"+apiKey)).String()
|
cache.ID = uuid.NewSHA1(uuid.NameSpaceOID, []byte("cli-proxy-api:codex:prompt-cache:"+apiKey)).String()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,12 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
@@ -155,8 +157,8 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
|
|||||||
baseURL = "https://chatgpt.com/backend-api/codex"
|
baseURL = "https://chatgpt.com/backend-api/codex"
|
||||||
}
|
}
|
||||||
|
|
||||||
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
||||||
defer reporter.trackFailure(ctx, &err)
|
defer reporter.TrackFailure(ctx, &err)
|
||||||
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("codex")
|
to := sdktranslator.FromString("codex")
|
||||||
@@ -173,8 +175,8 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
|
|||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
requestedModel := payloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
||||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||||
body, _ = sjson.SetBytes(body, "stream", true)
|
body, _ = sjson.SetBytes(body, "stream", true)
|
||||||
body, _ = sjson.DeleteBytes(body, "previous_response_id")
|
body, _ = sjson.DeleteBytes(body, "previous_response_id")
|
||||||
@@ -209,7 +211,7 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
|
|||||||
}
|
}
|
||||||
|
|
||||||
wsReqBody := buildCodexWebsocketRequestBody(body)
|
wsReqBody := buildCodexWebsocketRequestBody(body)
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: wsURL,
|
URL: wsURL,
|
||||||
Method: "WEBSOCKET",
|
Method: "WEBSOCKET",
|
||||||
Headers: wsHeaders.Clone(),
|
Headers: wsHeaders.Clone(),
|
||||||
@@ -223,12 +225,12 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
|
|||||||
|
|
||||||
conn, respHS, errDial := e.ensureUpstreamConn(ctx, auth, sess, authID, wsURL, wsHeaders)
|
conn, respHS, errDial := e.ensureUpstreamConn(ctx, auth, sess, authID, wsURL, wsHeaders)
|
||||||
if respHS != nil {
|
if respHS != nil {
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, respHS.StatusCode, respHS.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, respHS.StatusCode, respHS.Header.Clone())
|
||||||
}
|
}
|
||||||
if errDial != nil {
|
if errDial != nil {
|
||||||
bodyErr := websocketHandshakeBody(respHS)
|
bodyErr := websocketHandshakeBody(respHS)
|
||||||
if len(bodyErr) > 0 {
|
if len(bodyErr) > 0 {
|
||||||
appendAPIResponseChunk(ctx, e.cfg, bodyErr)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, bodyErr)
|
||||||
}
|
}
|
||||||
if respHS != nil && respHS.StatusCode == http.StatusUpgradeRequired {
|
if respHS != nil && respHS.StatusCode == http.StatusUpgradeRequired {
|
||||||
return e.CodexExecutor.Execute(ctx, auth, req, opts)
|
return e.CodexExecutor.Execute(ctx, auth, req, opts)
|
||||||
@@ -236,7 +238,7 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
|
|||||||
if respHS != nil && respHS.StatusCode > 0 {
|
if respHS != nil && respHS.StatusCode > 0 {
|
||||||
return resp, statusErr{code: respHS.StatusCode, msg: string(bodyErr)}
|
return resp, statusErr{code: respHS.StatusCode, msg: string(bodyErr)}
|
||||||
}
|
}
|
||||||
recordAPIResponseError(ctx, e.cfg, errDial)
|
helps.RecordAPIResponseError(ctx, e.cfg, errDial)
|
||||||
return resp, errDial
|
return resp, errDial
|
||||||
}
|
}
|
||||||
closeHTTPResponseBody(respHS, "codex websockets executor: close handshake response body error")
|
closeHTTPResponseBody(respHS, "codex websockets executor: close handshake response body error")
|
||||||
@@ -271,7 +273,7 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
|
|||||||
connRetry, _, errDialRetry := e.ensureUpstreamConn(ctx, auth, sess, authID, wsURL, wsHeaders)
|
connRetry, _, errDialRetry := e.ensureUpstreamConn(ctx, auth, sess, authID, wsURL, wsHeaders)
|
||||||
if errDialRetry == nil && connRetry != nil {
|
if errDialRetry == nil && connRetry != nil {
|
||||||
wsReqBodyRetry := buildCodexWebsocketRequestBody(body)
|
wsReqBodyRetry := buildCodexWebsocketRequestBody(body)
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: wsURL,
|
URL: wsURL,
|
||||||
Method: "WEBSOCKET",
|
Method: "WEBSOCKET",
|
||||||
Headers: wsHeaders.Clone(),
|
Headers: wsHeaders.Clone(),
|
||||||
@@ -287,15 +289,15 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
|
|||||||
wsReqBody = wsReqBodyRetry
|
wsReqBody = wsReqBodyRetry
|
||||||
} else {
|
} else {
|
||||||
e.invalidateUpstreamConn(sess, connRetry, "send_error", errSendRetry)
|
e.invalidateUpstreamConn(sess, connRetry, "send_error", errSendRetry)
|
||||||
recordAPIResponseError(ctx, e.cfg, errSendRetry)
|
helps.RecordAPIResponseError(ctx, e.cfg, errSendRetry)
|
||||||
return resp, errSendRetry
|
return resp, errSendRetry
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
recordAPIResponseError(ctx, e.cfg, errDialRetry)
|
helps.RecordAPIResponseError(ctx, e.cfg, errDialRetry)
|
||||||
return resp, errDialRetry
|
return resp, errDialRetry
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
recordAPIResponseError(ctx, e.cfg, errSend)
|
helps.RecordAPIResponseError(ctx, e.cfg, errSend)
|
||||||
return resp, errSend
|
return resp, errSend
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -306,7 +308,7 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
|
|||||||
}
|
}
|
||||||
msgType, payload, errRead := readCodexWebsocketMessage(ctx, sess, conn, readCh)
|
msgType, payload, errRead := readCodexWebsocketMessage(ctx, sess, conn, readCh)
|
||||||
if errRead != nil {
|
if errRead != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errRead)
|
helps.RecordAPIResponseError(ctx, e.cfg, errRead)
|
||||||
return resp, errRead
|
return resp, errRead
|
||||||
}
|
}
|
||||||
if msgType != websocket.TextMessage {
|
if msgType != websocket.TextMessage {
|
||||||
@@ -315,7 +317,7 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
|
|||||||
if sess != nil {
|
if sess != nil {
|
||||||
e.invalidateUpstreamConn(sess, conn, "unexpected_binary", err)
|
e.invalidateUpstreamConn(sess, conn, "unexpected_binary", err)
|
||||||
}
|
}
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
@@ -325,21 +327,21 @@ func (e *CodexWebsocketsExecutor) Execute(ctx context.Context, auth *cliproxyaut
|
|||||||
if len(payload) == 0 {
|
if len(payload) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, payload)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, payload)
|
||||||
|
|
||||||
if wsErr, ok := parseCodexWebsocketError(payload); ok {
|
if wsErr, ok := parseCodexWebsocketError(payload); ok {
|
||||||
if sess != nil {
|
if sess != nil {
|
||||||
e.invalidateUpstreamConn(sess, conn, "upstream_error", wsErr)
|
e.invalidateUpstreamConn(sess, conn, "upstream_error", wsErr)
|
||||||
}
|
}
|
||||||
recordAPIResponseError(ctx, e.cfg, wsErr)
|
helps.RecordAPIResponseError(ctx, e.cfg, wsErr)
|
||||||
return resp, wsErr
|
return resp, wsErr
|
||||||
}
|
}
|
||||||
|
|
||||||
payload = normalizeCodexWebsocketCompletion(payload)
|
payload = normalizeCodexWebsocketCompletion(payload)
|
||||||
eventType := gjson.GetBytes(payload, "type").String()
|
eventType := gjson.GetBytes(payload, "type").String()
|
||||||
if eventType == "response.completed" {
|
if eventType == "response.completed" {
|
||||||
if detail, ok := parseCodexUsage(payload); ok {
|
if detail, ok := helps.ParseCodexUsage(payload); ok {
|
||||||
reporter.publish(ctx, detail)
|
reporter.Publish(ctx, detail)
|
||||||
}
|
}
|
||||||
var param any
|
var param any
|
||||||
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, originalPayload, body, payload, ¶m)
|
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, originalPayload, body, payload, ¶m)
|
||||||
@@ -364,8 +366,8 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
|
|||||||
baseURL = "https://chatgpt.com/backend-api/codex"
|
baseURL = "https://chatgpt.com/backend-api/codex"
|
||||||
}
|
}
|
||||||
|
|
||||||
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
||||||
defer reporter.trackFailure(ctx, &err)
|
defer reporter.TrackFailure(ctx, &err)
|
||||||
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("codex")
|
to := sdktranslator.FromString("codex")
|
||||||
@@ -376,8 +378,8 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
requestedModel := payloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, body, requestedModel)
|
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, body, requestedModel)
|
||||||
|
|
||||||
httpURL := strings.TrimSuffix(baseURL, "/") + "/responses"
|
httpURL := strings.TrimSuffix(baseURL, "/") + "/responses"
|
||||||
wsURL, err := buildCodexResponsesWebsocketURL(httpURL)
|
wsURL, err := buildCodexResponsesWebsocketURL(httpURL)
|
||||||
@@ -403,7 +405,7 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
|
|||||||
}
|
}
|
||||||
|
|
||||||
wsReqBody := buildCodexWebsocketRequestBody(body)
|
wsReqBody := buildCodexWebsocketRequestBody(body)
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: wsURL,
|
URL: wsURL,
|
||||||
Method: "WEBSOCKET",
|
Method: "WEBSOCKET",
|
||||||
Headers: wsHeaders.Clone(),
|
Headers: wsHeaders.Clone(),
|
||||||
@@ -419,12 +421,12 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
|
|||||||
var upstreamHeaders http.Header
|
var upstreamHeaders http.Header
|
||||||
if respHS != nil {
|
if respHS != nil {
|
||||||
upstreamHeaders = respHS.Header.Clone()
|
upstreamHeaders = respHS.Header.Clone()
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, respHS.StatusCode, respHS.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, respHS.StatusCode, respHS.Header.Clone())
|
||||||
}
|
}
|
||||||
if errDial != nil {
|
if errDial != nil {
|
||||||
bodyErr := websocketHandshakeBody(respHS)
|
bodyErr := websocketHandshakeBody(respHS)
|
||||||
if len(bodyErr) > 0 {
|
if len(bodyErr) > 0 {
|
||||||
appendAPIResponseChunk(ctx, e.cfg, bodyErr)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, bodyErr)
|
||||||
}
|
}
|
||||||
if respHS != nil && respHS.StatusCode == http.StatusUpgradeRequired {
|
if respHS != nil && respHS.StatusCode == http.StatusUpgradeRequired {
|
||||||
return e.CodexExecutor.ExecuteStream(ctx, auth, req, opts)
|
return e.CodexExecutor.ExecuteStream(ctx, auth, req, opts)
|
||||||
@@ -432,7 +434,7 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
|
|||||||
if respHS != nil && respHS.StatusCode > 0 {
|
if respHS != nil && respHS.StatusCode > 0 {
|
||||||
return nil, statusErr{code: respHS.StatusCode, msg: string(bodyErr)}
|
return nil, statusErr{code: respHS.StatusCode, msg: string(bodyErr)}
|
||||||
}
|
}
|
||||||
recordAPIResponseError(ctx, e.cfg, errDial)
|
helps.RecordAPIResponseError(ctx, e.cfg, errDial)
|
||||||
if sess != nil {
|
if sess != nil {
|
||||||
sess.reqMu.Unlock()
|
sess.reqMu.Unlock()
|
||||||
}
|
}
|
||||||
@@ -451,20 +453,20 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
|
|||||||
}
|
}
|
||||||
|
|
||||||
if errSend := writeCodexWebsocketMessage(sess, conn, wsReqBody); errSend != nil {
|
if errSend := writeCodexWebsocketMessage(sess, conn, wsReqBody); errSend != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errSend)
|
helps.RecordAPIResponseError(ctx, e.cfg, errSend)
|
||||||
if sess != nil {
|
if sess != nil {
|
||||||
e.invalidateUpstreamConn(sess, conn, "send_error", errSend)
|
e.invalidateUpstreamConn(sess, conn, "send_error", errSend)
|
||||||
|
|
||||||
// Retry once with a new websocket connection for the same execution session.
|
// Retry once with a new websocket connection for the same execution session.
|
||||||
connRetry, _, errDialRetry := e.ensureUpstreamConn(ctx, auth, sess, authID, wsURL, wsHeaders)
|
connRetry, _, errDialRetry := e.ensureUpstreamConn(ctx, auth, sess, authID, wsURL, wsHeaders)
|
||||||
if errDialRetry != nil || connRetry == nil {
|
if errDialRetry != nil || connRetry == nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errDialRetry)
|
helps.RecordAPIResponseError(ctx, e.cfg, errDialRetry)
|
||||||
sess.clearActive(readCh)
|
sess.clearActive(readCh)
|
||||||
sess.reqMu.Unlock()
|
sess.reqMu.Unlock()
|
||||||
return nil, errDialRetry
|
return nil, errDialRetry
|
||||||
}
|
}
|
||||||
wsReqBodyRetry := buildCodexWebsocketRequestBody(body)
|
wsReqBodyRetry := buildCodexWebsocketRequestBody(body)
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: wsURL,
|
URL: wsURL,
|
||||||
Method: "WEBSOCKET",
|
Method: "WEBSOCKET",
|
||||||
Headers: wsHeaders.Clone(),
|
Headers: wsHeaders.Clone(),
|
||||||
@@ -476,7 +478,7 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
|
|||||||
AuthValue: authValue,
|
AuthValue: authValue,
|
||||||
})
|
})
|
||||||
if errSendRetry := writeCodexWebsocketMessage(sess, connRetry, wsReqBodyRetry); errSendRetry != nil {
|
if errSendRetry := writeCodexWebsocketMessage(sess, connRetry, wsReqBodyRetry); errSendRetry != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errSendRetry)
|
helps.RecordAPIResponseError(ctx, e.cfg, errSendRetry)
|
||||||
e.invalidateUpstreamConn(sess, connRetry, "send_error", errSendRetry)
|
e.invalidateUpstreamConn(sess, connRetry, "send_error", errSendRetry)
|
||||||
sess.clearActive(readCh)
|
sess.clearActive(readCh)
|
||||||
sess.reqMu.Unlock()
|
sess.reqMu.Unlock()
|
||||||
@@ -542,8 +544,8 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
|
|||||||
}
|
}
|
||||||
terminateReason = "read_error"
|
terminateReason = "read_error"
|
||||||
terminateErr = errRead
|
terminateErr = errRead
|
||||||
recordAPIResponseError(ctx, e.cfg, errRead)
|
helps.RecordAPIResponseError(ctx, e.cfg, errRead)
|
||||||
reporter.publishFailure(ctx)
|
reporter.PublishFailure(ctx)
|
||||||
_ = send(cliproxyexecutor.StreamChunk{Err: errRead})
|
_ = send(cliproxyexecutor.StreamChunk{Err: errRead})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -552,8 +554,8 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
|
|||||||
err = fmt.Errorf("codex websockets executor: unexpected binary message")
|
err = fmt.Errorf("codex websockets executor: unexpected binary message")
|
||||||
terminateReason = "unexpected_binary"
|
terminateReason = "unexpected_binary"
|
||||||
terminateErr = err
|
terminateErr = err
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
reporter.publishFailure(ctx)
|
reporter.PublishFailure(ctx)
|
||||||
if sess != nil {
|
if sess != nil {
|
||||||
e.invalidateUpstreamConn(sess, conn, "unexpected_binary", err)
|
e.invalidateUpstreamConn(sess, conn, "unexpected_binary", err)
|
||||||
}
|
}
|
||||||
@@ -567,13 +569,13 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
|
|||||||
if len(payload) == 0 {
|
if len(payload) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, payload)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, payload)
|
||||||
|
|
||||||
if wsErr, ok := parseCodexWebsocketError(payload); ok {
|
if wsErr, ok := parseCodexWebsocketError(payload); ok {
|
||||||
terminateReason = "upstream_error"
|
terminateReason = "upstream_error"
|
||||||
terminateErr = wsErr
|
terminateErr = wsErr
|
||||||
recordAPIResponseError(ctx, e.cfg, wsErr)
|
helps.RecordAPIResponseError(ctx, e.cfg, wsErr)
|
||||||
reporter.publishFailure(ctx)
|
reporter.PublishFailure(ctx)
|
||||||
if sess != nil {
|
if sess != nil {
|
||||||
e.invalidateUpstreamConn(sess, conn, "upstream_error", wsErr)
|
e.invalidateUpstreamConn(sess, conn, "upstream_error", wsErr)
|
||||||
}
|
}
|
||||||
@@ -584,8 +586,8 @@ func (e *CodexWebsocketsExecutor) ExecuteStream(ctx context.Context, auth *clipr
|
|||||||
payload = normalizeCodexWebsocketCompletion(payload)
|
payload = normalizeCodexWebsocketCompletion(payload)
|
||||||
eventType := gjson.GetBytes(payload, "type").String()
|
eventType := gjson.GetBytes(payload, "type").String()
|
||||||
if eventType == "response.completed" || eventType == "response.done" {
|
if eventType == "response.completed" || eventType == "response.done" {
|
||||||
if detail, ok := parseCodexUsage(payload); ok {
|
if detail, ok := helps.ParseCodexUsage(payload); ok {
|
||||||
reporter.publish(ctx, detail)
|
reporter.Publish(ctx, detail)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -767,19 +769,19 @@ func applyCodexPromptCacheHeaders(from sdktranslator.Format, req cliproxyexecuto
|
|||||||
return rawJSON, headers
|
return rawJSON, headers
|
||||||
}
|
}
|
||||||
|
|
||||||
var cache codexCache
|
var cache helps.CodexCache
|
||||||
if from == "claude" {
|
if from == "claude" {
|
||||||
userIDResult := gjson.GetBytes(req.Payload, "metadata.user_id")
|
userIDResult := gjson.GetBytes(req.Payload, "metadata.user_id")
|
||||||
if userIDResult.Exists() {
|
if userIDResult.Exists() {
|
||||||
key := fmt.Sprintf("%s-%s", req.Model, userIDResult.String())
|
key := fmt.Sprintf("%s-%s", req.Model, userIDResult.String())
|
||||||
if cached, ok := getCodexCache(key); ok {
|
if cached, ok := helps.GetCodexCache(key); ok {
|
||||||
cache = cached
|
cache = cached
|
||||||
} else {
|
} else {
|
||||||
cache = codexCache{
|
cache = helps.CodexCache{
|
||||||
ID: uuid.New().String(),
|
ID: uuid.New().String(),
|
||||||
Expire: time.Now().Add(1 * time.Hour),
|
Expire: time.Now().Add(1 * time.Hour),
|
||||||
}
|
}
|
||||||
setCodexCache(key, cache)
|
helps.SetCodexCache(key, cache)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if from == "openai-response" {
|
} else if from == "openai-response" {
|
||||||
@@ -806,8 +808,8 @@ func applyCodexWebsocketHeaders(ctx context.Context, headers http.Header, auth *
|
|||||||
}
|
}
|
||||||
|
|
||||||
var ginHeaders http.Header
|
var ginHeaders http.Header
|
||||||
if ginCtx := ginContextFrom(ctx); ginCtx != nil && ginCtx.Request != nil {
|
if ginCtx, ok := ctx.Value("gin").(*gin.Context); ok && ginCtx != nil && ginCtx.Request != nil {
|
||||||
ginHeaders = ginCtx.Request.Header
|
ginHeaders = ginCtx.Request.Header.Clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
cfgUserAgent, cfgBetaFeatures := codexHeaderDefaults(cfg, auth)
|
cfgUserAgent, cfgBetaFeatures := codexHeaderDefaults(cfg, auth)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/misc"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/geminicli"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/geminicli"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
@@ -112,8 +113,8 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth
|
|||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
||||||
defer reporter.trackFailure(ctx, &err)
|
defer reporter.TrackFailure(ctx, &err)
|
||||||
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("gemini-cli")
|
to := sdktranslator.FromString("gemini-cli")
|
||||||
@@ -132,8 +133,8 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth
|
|||||||
}
|
}
|
||||||
|
|
||||||
basePayload = fixGeminiCLIImageAspectRatio(baseModel, basePayload)
|
basePayload = fixGeminiCLIImageAspectRatio(baseModel, basePayload)
|
||||||
requestedModel := payloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
basePayload = applyPayloadConfigWithRoot(e.cfg, baseModel, "gemini", "request", basePayload, originalTranslated, requestedModel)
|
basePayload = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, "gemini", "request", basePayload, originalTranslated, requestedModel)
|
||||||
|
|
||||||
action := "generateContent"
|
action := "generateContent"
|
||||||
if req.Metadata != nil {
|
if req.Metadata != nil {
|
||||||
@@ -190,7 +191,7 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth
|
|||||||
reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken)
|
reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken)
|
||||||
applyGeminiCLIHeaders(reqHTTP, attemptModel)
|
applyGeminiCLIHeaders(reqHTTP, attemptModel)
|
||||||
reqHTTP.Header.Set("Accept", "application/json")
|
reqHTTP.Header.Set("Accept", "application/json")
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: url,
|
URL: url,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: reqHTTP.Header.Clone(),
|
Headers: reqHTTP.Header.Clone(),
|
||||||
@@ -204,7 +205,7 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth
|
|||||||
|
|
||||||
httpResp, errDo := httpClient.Do(reqHTTP)
|
httpResp, errDo := httpClient.Do(reqHTTP)
|
||||||
if errDo != nil {
|
if errDo != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errDo)
|
helps.RecordAPIResponseError(ctx, e.cfg, errDo)
|
||||||
err = errDo
|
err = errDo
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
@@ -213,15 +214,15 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth
|
|||||||
if errClose := httpResp.Body.Close(); errClose != nil {
|
if errClose := httpResp.Body.Close(); errClose != nil {
|
||||||
log.Errorf("gemini cli executor: close response body error: %v", errClose)
|
log.Errorf("gemini cli executor: close response body error: %v", errClose)
|
||||||
}
|
}
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||||
if errRead != nil {
|
if errRead != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errRead)
|
helps.RecordAPIResponseError(ctx, e.cfg, errRead)
|
||||||
err = errRead
|
err = errRead
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, data)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, data)
|
||||||
if httpResp.StatusCode >= 200 && httpResp.StatusCode < 300 {
|
if httpResp.StatusCode >= 200 && httpResp.StatusCode < 300 {
|
||||||
reporter.publish(ctx, parseGeminiCLIUsage(data))
|
reporter.Publish(ctx, helps.ParseGeminiCLIUsage(data))
|
||||||
var param any
|
var param any
|
||||||
out := sdktranslator.TranslateNonStream(respCtx, to, from, attemptModel, opts.OriginalRequest, payload, data, ¶m)
|
out := sdktranslator.TranslateNonStream(respCtx, to, from, attemptModel, opts.OriginalRequest, payload, data, ¶m)
|
||||||
resp = cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()}
|
resp = cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()}
|
||||||
@@ -230,7 +231,7 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth
|
|||||||
|
|
||||||
lastStatus = httpResp.StatusCode
|
lastStatus = httpResp.StatusCode
|
||||||
lastBody = append([]byte(nil), data...)
|
lastBody = append([]byte(nil), data...)
|
||||||
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), data))
|
helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), data))
|
||||||
if httpResp.StatusCode == 429 {
|
if httpResp.StatusCode == 429 {
|
||||||
if idx+1 < len(models) {
|
if idx+1 < len(models) {
|
||||||
log.Debugf("gemini cli executor: rate limited, retrying with next model: %s", models[idx+1])
|
log.Debugf("gemini cli executor: rate limited, retrying with next model: %s", models[idx+1])
|
||||||
@@ -245,7 +246,7 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(lastBody) > 0 {
|
if len(lastBody) > 0 {
|
||||||
appendAPIResponseChunk(ctx, e.cfg, lastBody)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, lastBody)
|
||||||
}
|
}
|
||||||
if lastStatus == 0 {
|
if lastStatus == 0 {
|
||||||
lastStatus = 429
|
lastStatus = 429
|
||||||
@@ -266,8 +267,8 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
||||||
defer reporter.trackFailure(ctx, &err)
|
defer reporter.TrackFailure(ctx, &err)
|
||||||
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("gemini-cli")
|
to := sdktranslator.FromString("gemini-cli")
|
||||||
@@ -286,8 +287,8 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
|
|||||||
}
|
}
|
||||||
|
|
||||||
basePayload = fixGeminiCLIImageAspectRatio(baseModel, basePayload)
|
basePayload = fixGeminiCLIImageAspectRatio(baseModel, basePayload)
|
||||||
requestedModel := payloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
basePayload = applyPayloadConfigWithRoot(e.cfg, baseModel, "gemini", "request", basePayload, originalTranslated, requestedModel)
|
basePayload = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, "gemini", "request", basePayload, originalTranslated, requestedModel)
|
||||||
|
|
||||||
projectID := resolveGeminiProjectID(auth)
|
projectID := resolveGeminiProjectID(auth)
|
||||||
|
|
||||||
@@ -335,7 +336,7 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
|
|||||||
reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken)
|
reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken)
|
||||||
applyGeminiCLIHeaders(reqHTTP, attemptModel)
|
applyGeminiCLIHeaders(reqHTTP, attemptModel)
|
||||||
reqHTTP.Header.Set("Accept", "text/event-stream")
|
reqHTTP.Header.Set("Accept", "text/event-stream")
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: url,
|
URL: url,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: reqHTTP.Header.Clone(),
|
Headers: reqHTTP.Header.Clone(),
|
||||||
@@ -349,25 +350,25 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
|
|||||||
|
|
||||||
httpResp, errDo := httpClient.Do(reqHTTP)
|
httpResp, errDo := httpClient.Do(reqHTTP)
|
||||||
if errDo != nil {
|
if errDo != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errDo)
|
helps.RecordAPIResponseError(ctx, e.cfg, errDo)
|
||||||
err = errDo
|
err = errDo
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||||
data, errRead := io.ReadAll(httpResp.Body)
|
data, errRead := io.ReadAll(httpResp.Body)
|
||||||
if errClose := httpResp.Body.Close(); errClose != nil {
|
if errClose := httpResp.Body.Close(); errClose != nil {
|
||||||
log.Errorf("gemini cli executor: close response body error: %v", errClose)
|
log.Errorf("gemini cli executor: close response body error: %v", errClose)
|
||||||
}
|
}
|
||||||
if errRead != nil {
|
if errRead != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errRead)
|
helps.RecordAPIResponseError(ctx, e.cfg, errRead)
|
||||||
err = errRead
|
err = errRead
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, data)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, data)
|
||||||
lastStatus = httpResp.StatusCode
|
lastStatus = httpResp.StatusCode
|
||||||
lastBody = append([]byte(nil), data...)
|
lastBody = append([]byte(nil), data...)
|
||||||
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), data))
|
helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), data))
|
||||||
if httpResp.StatusCode == 429 {
|
if httpResp.StatusCode == 429 {
|
||||||
if idx+1 < len(models) {
|
if idx+1 < len(models) {
|
||||||
log.Debugf("gemini cli executor: rate limited, retrying with next model: %s", models[idx+1])
|
log.Debugf("gemini cli executor: rate limited, retrying with next model: %s", models[idx+1])
|
||||||
@@ -394,9 +395,9 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
|
|||||||
var param any
|
var param any
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Bytes()
|
line := scanner.Bytes()
|
||||||
appendAPIResponseChunk(ctx, e.cfg, line)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, line)
|
||||||
if detail, ok := parseGeminiCLIStreamUsage(line); ok {
|
if detail, ok := helps.ParseGeminiCLIStreamUsage(line); ok {
|
||||||
reporter.publish(ctx, detail)
|
reporter.Publish(ctx, detail)
|
||||||
}
|
}
|
||||||
if bytes.HasPrefix(line, dataTag) {
|
if bytes.HasPrefix(line, dataTag) {
|
||||||
segments := sdktranslator.TranslateStream(respCtx, to, from, attemptModel, opts.OriginalRequest, reqBody, bytes.Clone(line), ¶m)
|
segments := sdktranslator.TranslateStream(respCtx, to, from, attemptModel, opts.OriginalRequest, reqBody, bytes.Clone(line), ¶m)
|
||||||
@@ -411,8 +412,8 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
|
|||||||
out <- cliproxyexecutor.StreamChunk{Payload: segments[i]}
|
out <- cliproxyexecutor.StreamChunk{Payload: segments[i]}
|
||||||
}
|
}
|
||||||
if errScan := scanner.Err(); errScan != nil {
|
if errScan := scanner.Err(); errScan != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errScan)
|
helps.RecordAPIResponseError(ctx, e.cfg, errScan)
|
||||||
reporter.publishFailure(ctx)
|
reporter.PublishFailure(ctx)
|
||||||
out <- cliproxyexecutor.StreamChunk{Err: errScan}
|
out <- cliproxyexecutor.StreamChunk{Err: errScan}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
@@ -420,13 +421,13 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
|
|||||||
|
|
||||||
data, errRead := io.ReadAll(resp.Body)
|
data, errRead := io.ReadAll(resp.Body)
|
||||||
if errRead != nil {
|
if errRead != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errRead)
|
helps.RecordAPIResponseError(ctx, e.cfg, errRead)
|
||||||
reporter.publishFailure(ctx)
|
reporter.PublishFailure(ctx)
|
||||||
out <- cliproxyexecutor.StreamChunk{Err: errRead}
|
out <- cliproxyexecutor.StreamChunk{Err: errRead}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, data)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, data)
|
||||||
reporter.publish(ctx, parseGeminiCLIUsage(data))
|
reporter.Publish(ctx, helps.ParseGeminiCLIUsage(data))
|
||||||
var param any
|
var param any
|
||||||
segments := sdktranslator.TranslateStream(respCtx, to, from, attemptModel, opts.OriginalRequest, reqBody, data, ¶m)
|
segments := sdktranslator.TranslateStream(respCtx, to, from, attemptModel, opts.OriginalRequest, reqBody, data, ¶m)
|
||||||
for i := range segments {
|
for i := range segments {
|
||||||
@@ -443,7 +444,7 @@ func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyaut
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(lastBody) > 0 {
|
if len(lastBody) > 0 {
|
||||||
appendAPIResponseChunk(ctx, e.cfg, lastBody)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, lastBody)
|
||||||
}
|
}
|
||||||
if lastStatus == 0 {
|
if lastStatus == 0 {
|
||||||
lastStatus = 429
|
lastStatus = 429
|
||||||
@@ -516,7 +517,7 @@ func (e *GeminiCLIExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.
|
|||||||
reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken)
|
reqHTTP.Header.Set("Authorization", "Bearer "+tok.AccessToken)
|
||||||
applyGeminiCLIHeaders(reqHTTP, baseModel)
|
applyGeminiCLIHeaders(reqHTTP, baseModel)
|
||||||
reqHTTP.Header.Set("Accept", "application/json")
|
reqHTTP.Header.Set("Accept", "application/json")
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: url,
|
URL: url,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: reqHTTP.Header.Clone(),
|
Headers: reqHTTP.Header.Clone(),
|
||||||
@@ -530,17 +531,19 @@ func (e *GeminiCLIExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.
|
|||||||
|
|
||||||
resp, errDo := httpClient.Do(reqHTTP)
|
resp, errDo := httpClient.Do(reqHTTP)
|
||||||
if errDo != nil {
|
if errDo != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errDo)
|
helps.RecordAPIResponseError(ctx, e.cfg, errDo)
|
||||||
return cliproxyexecutor.Response{}, errDo
|
return cliproxyexecutor.Response{}, errDo
|
||||||
}
|
}
|
||||||
data, errRead := io.ReadAll(resp.Body)
|
data, errRead := io.ReadAll(resp.Body)
|
||||||
_ = resp.Body.Close()
|
if errClose := resp.Body.Close(); errClose != nil {
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone())
|
helps.LogWithRequestID(ctx).Errorf("response body close error: %v", errClose)
|
||||||
|
}
|
||||||
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone())
|
||||||
if errRead != nil {
|
if errRead != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errRead)
|
helps.RecordAPIResponseError(ctx, e.cfg, errRead)
|
||||||
return cliproxyexecutor.Response{}, errRead
|
return cliproxyexecutor.Response{}, errRead
|
||||||
}
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, data)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, data)
|
||||||
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||||
count := gjson.GetBytes(data, "totalTokens").Int()
|
count := gjson.GetBytes(data, "totalTokens").Int()
|
||||||
translated := sdktranslator.TranslateTokenCount(respCtx, to, from, count, data)
|
translated := sdktranslator.TranslateTokenCount(respCtx, to, from, count, data)
|
||||||
@@ -611,7 +614,7 @@ func prepareGeminiCLITokenSource(ctx context.Context, cfg *config.Config, auth *
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctxToken := ctx
|
ctxToken := ctx
|
||||||
if httpClient := newProxyAwareHTTPClient(ctx, cfg, auth, 0); httpClient != nil {
|
if httpClient := helps.NewProxyAwareHTTPClient(ctx, cfg, auth, 0); httpClient != nil {
|
||||||
ctxToken = context.WithValue(ctxToken, oauth2.HTTPClient, httpClient)
|
ctxToken = context.WithValue(ctxToken, oauth2.HTTPClient, httpClient)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -707,7 +710,7 @@ func geminiOAuthMetadata(auth *cliproxyauth.Auth) map[string]any {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newHTTPClient(ctx context.Context, cfg *config.Config, auth *cliproxyauth.Auth, timeout time.Duration) *http.Client {
|
func newHTTPClient(ctx context.Context, cfg *config.Config, auth *cliproxyauth.Auth, timeout time.Duration) *http.Client {
|
||||||
return newProxyAwareHTTPClient(ctx, cfg, auth, timeout)
|
return helps.NewProxyAwareHTTPClient(ctx, cfg, auth, timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
func cloneMap(in map[string]any) map[string]any {
|
func cloneMap(in map[string]any) map[string]any {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
@@ -85,7 +86,7 @@ func (e *GeminiExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
if err := e.PrepareRequest(httpReq, auth); err != nil {
|
if err := e.PrepareRequest(httpReq, auth); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
return httpClient.Do(httpReq)
|
return httpClient.Do(httpReq)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,8 +111,8 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
|||||||
|
|
||||||
apiKey, bearer := geminiCreds(auth)
|
apiKey, bearer := geminiCreds(auth)
|
||||||
|
|
||||||
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
||||||
defer reporter.trackFailure(ctx, &err)
|
defer reporter.TrackFailure(ctx, &err)
|
||||||
|
|
||||||
// Official Gemini API via API key or OAuth bearer
|
// Official Gemini API via API key or OAuth bearer
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
@@ -130,8 +131,8 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
|||||||
}
|
}
|
||||||
|
|
||||||
body = fixGeminiImageAspectRatio(baseModel, body)
|
body = fixGeminiImageAspectRatio(baseModel, body)
|
||||||
requestedModel := payloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
||||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||||
|
|
||||||
action := "generateContent"
|
action := "generateContent"
|
||||||
@@ -165,7 +166,7 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
|||||||
authLabel = auth.Label
|
authLabel = auth.Label
|
||||||
authType, authValue = auth.AccountInfo()
|
authType, authValue = auth.AccountInfo()
|
||||||
}
|
}
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: url,
|
URL: url,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: httpReq.Header.Clone(),
|
Headers: httpReq.Header.Clone(),
|
||||||
@@ -177,10 +178,10 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
|||||||
AuthValue: authValue,
|
AuthValue: authValue,
|
||||||
})
|
})
|
||||||
|
|
||||||
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
httpResp, err := httpClient.Do(httpReq)
|
httpResp, err := httpClient.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -188,21 +189,21 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
|||||||
log.Errorf("gemini executor: close response body error: %v", errClose)
|
log.Errorf("gemini executor: close response body error: %v", errClose)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||||
b, _ := io.ReadAll(httpResp.Body)
|
b, _ := io.ReadAll(httpResp.Body)
|
||||||
appendAPIResponseChunk(ctx, e.cfg, b)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, b)
|
||||||
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
||||||
err = statusErr{code: httpResp.StatusCode, msg: string(b)}
|
err = statusErr{code: httpResp.StatusCode, msg: string(b)}
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
data, err := io.ReadAll(httpResp.Body)
|
data, err := io.ReadAll(httpResp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, data)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, data)
|
||||||
reporter.publish(ctx, parseGeminiUsage(data))
|
reporter.Publish(ctx, helps.ParseGeminiUsage(data))
|
||||||
var param any
|
var param any
|
||||||
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, body, data, ¶m)
|
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, body, data, ¶m)
|
||||||
resp = cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()}
|
resp = cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()}
|
||||||
@@ -218,8 +219,8 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
|||||||
|
|
||||||
apiKey, bearer := geminiCreds(auth)
|
apiKey, bearer := geminiCreds(auth)
|
||||||
|
|
||||||
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
||||||
defer reporter.trackFailure(ctx, &err)
|
defer reporter.TrackFailure(ctx, &err)
|
||||||
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("gemini")
|
to := sdktranslator.FromString("gemini")
|
||||||
@@ -237,8 +238,8 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
|||||||
}
|
}
|
||||||
|
|
||||||
body = fixGeminiImageAspectRatio(baseModel, body)
|
body = fixGeminiImageAspectRatio(baseModel, body)
|
||||||
requestedModel := payloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
||||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||||
|
|
||||||
baseURL := resolveGeminiBaseURL(auth)
|
baseURL := resolveGeminiBaseURL(auth)
|
||||||
@@ -268,7 +269,7 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
|||||||
authLabel = auth.Label
|
authLabel = auth.Label
|
||||||
authType, authValue = auth.AccountInfo()
|
authType, authValue = auth.AccountInfo()
|
||||||
}
|
}
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: url,
|
URL: url,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: httpReq.Header.Clone(),
|
Headers: httpReq.Header.Clone(),
|
||||||
@@ -280,17 +281,17 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
|||||||
AuthValue: authValue,
|
AuthValue: authValue,
|
||||||
})
|
})
|
||||||
|
|
||||||
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
httpResp, err := httpClient.Do(httpReq)
|
httpResp, err := httpClient.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||||
b, _ := io.ReadAll(httpResp.Body)
|
b, _ := io.ReadAll(httpResp.Body)
|
||||||
appendAPIResponseChunk(ctx, e.cfg, b)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, b)
|
||||||
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
||||||
if errClose := httpResp.Body.Close(); errClose != nil {
|
if errClose := httpResp.Body.Close(); errClose != nil {
|
||||||
log.Errorf("gemini executor: close response body error: %v", errClose)
|
log.Errorf("gemini executor: close response body error: %v", errClose)
|
||||||
}
|
}
|
||||||
@@ -310,14 +311,14 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
|||||||
var param any
|
var param any
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Bytes()
|
line := scanner.Bytes()
|
||||||
appendAPIResponseChunk(ctx, e.cfg, line)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, line)
|
||||||
filtered := FilterSSEUsageMetadata(line)
|
filtered := helps.FilterSSEUsageMetadata(line)
|
||||||
payload := jsonPayload(filtered)
|
payload := helps.JSONPayload(filtered)
|
||||||
if len(payload) == 0 {
|
if len(payload) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if detail, ok := parseGeminiStreamUsage(payload); ok {
|
if detail, ok := helps.ParseGeminiStreamUsage(payload); ok {
|
||||||
reporter.publish(ctx, detail)
|
reporter.Publish(ctx, detail)
|
||||||
}
|
}
|
||||||
lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(payload), ¶m)
|
lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(payload), ¶m)
|
||||||
for i := range lines {
|
for i := range lines {
|
||||||
@@ -329,8 +330,8 @@ func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.A
|
|||||||
out <- cliproxyexecutor.StreamChunk{Payload: lines[i]}
|
out <- cliproxyexecutor.StreamChunk{Payload: lines[i]}
|
||||||
}
|
}
|
||||||
if errScan := scanner.Err(); errScan != nil {
|
if errScan := scanner.Err(); errScan != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errScan)
|
helps.RecordAPIResponseError(ctx, e.cfg, errScan)
|
||||||
reporter.publishFailure(ctx)
|
reporter.PublishFailure(ctx)
|
||||||
out <- cliproxyexecutor.StreamChunk{Err: errScan}
|
out <- cliproxyexecutor.StreamChunk{Err: errScan}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -381,7 +382,7 @@ func (e *GeminiExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
authLabel = auth.Label
|
authLabel = auth.Label
|
||||||
authType, authValue = auth.AccountInfo()
|
authType, authValue = auth.AccountInfo()
|
||||||
}
|
}
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: url,
|
URL: url,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: httpReq.Header.Clone(),
|
Headers: httpReq.Header.Clone(),
|
||||||
@@ -393,23 +394,27 @@ func (e *GeminiExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
AuthValue: authValue,
|
AuthValue: authValue,
|
||||||
})
|
})
|
||||||
|
|
||||||
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
resp, err := httpClient.Do(httpReq)
|
resp, err := httpClient.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
return cliproxyexecutor.Response{}, err
|
return cliproxyexecutor.Response{}, err
|
||||||
}
|
}
|
||||||
defer func() { _ = resp.Body.Close() }()
|
defer func() {
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone())
|
if errClose := resp.Body.Close(); errClose != nil {
|
||||||
|
helps.LogWithRequestID(ctx).Errorf("response body close error: %v", errClose)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone())
|
||||||
|
|
||||||
data, err := io.ReadAll(resp.Body)
|
data, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
return cliproxyexecutor.Response{}, err
|
return cliproxyexecutor.Response{}, err
|
||||||
}
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, data)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, data)
|
||||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", resp.StatusCode, summarizeErrorBody(resp.Header.Get("Content-Type"), data))
|
helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", resp.StatusCode, helps.SummarizeErrorBody(resp.Header.Get("Content-Type"), data))
|
||||||
return cliproxyexecutor.Response{}, statusErr{code: resp.StatusCode, msg: string(data)}
|
return cliproxyexecutor.Response{}, statusErr{code: resp.StatusCode, msg: string(data)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
|
|
||||||
vertexauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/vertex"
|
vertexauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/vertex"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
||||||
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
||||||
@@ -227,7 +228,7 @@ func (e *GeminiVertexExecutor) HttpRequest(ctx context.Context, auth *cliproxyau
|
|||||||
if err := e.PrepareRequest(httpReq, auth); err != nil {
|
if err := e.PrepareRequest(httpReq, auth); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
return httpClient.Do(httpReq)
|
return httpClient.Do(httpReq)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,8 +302,8 @@ func (e *GeminiVertexExecutor) Refresh(_ context.Context, auth *cliproxyauth.Aut
|
|||||||
func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, projectID, location string, saJSON []byte) (resp cliproxyexecutor.Response, err error) {
|
func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, projectID, location string, saJSON []byte) (resp cliproxyexecutor.Response, err error) {
|
||||||
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
||||||
|
|
||||||
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
||||||
defer reporter.trackFailure(ctx, &err)
|
defer reporter.TrackFailure(ctx, &err)
|
||||||
|
|
||||||
var body []byte
|
var body []byte
|
||||||
|
|
||||||
@@ -332,8 +333,8 @@ func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, au
|
|||||||
}
|
}
|
||||||
|
|
||||||
body = fixGeminiImageAspectRatio(baseModel, body)
|
body = fixGeminiImageAspectRatio(baseModel, body)
|
||||||
requestedModel := payloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
||||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -369,7 +370,7 @@ func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, au
|
|||||||
authLabel = auth.Label
|
authLabel = auth.Label
|
||||||
authType, authValue = auth.AccountInfo()
|
authType, authValue = auth.AccountInfo()
|
||||||
}
|
}
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: url,
|
URL: url,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: httpReq.Header.Clone(),
|
Headers: httpReq.Header.Clone(),
|
||||||
@@ -381,10 +382,10 @@ func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, au
|
|||||||
AuthValue: authValue,
|
AuthValue: authValue,
|
||||||
})
|
})
|
||||||
|
|
||||||
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
httpResp, errDo := httpClient.Do(httpReq)
|
httpResp, errDo := httpClient.Do(httpReq)
|
||||||
if errDo != nil {
|
if errDo != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errDo)
|
helps.RecordAPIResponseError(ctx, e.cfg, errDo)
|
||||||
return resp, errDo
|
return resp, errDo
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -392,21 +393,21 @@ func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, au
|
|||||||
log.Errorf("vertex executor: close response body error: %v", errClose)
|
log.Errorf("vertex executor: close response body error: %v", errClose)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||||
b, _ := io.ReadAll(httpResp.Body)
|
b, _ := io.ReadAll(httpResp.Body)
|
||||||
appendAPIResponseChunk(ctx, e.cfg, b)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, b)
|
||||||
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
||||||
err = statusErr{code: httpResp.StatusCode, msg: string(b)}
|
err = statusErr{code: httpResp.StatusCode, msg: string(b)}
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
data, errRead := io.ReadAll(httpResp.Body)
|
data, errRead := io.ReadAll(httpResp.Body)
|
||||||
if errRead != nil {
|
if errRead != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errRead)
|
helps.RecordAPIResponseError(ctx, e.cfg, errRead)
|
||||||
return resp, errRead
|
return resp, errRead
|
||||||
}
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, data)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, data)
|
||||||
reporter.publish(ctx, parseGeminiUsage(data))
|
reporter.Publish(ctx, helps.ParseGeminiUsage(data))
|
||||||
|
|
||||||
// For Imagen models, convert response to Gemini format before translation
|
// For Imagen models, convert response to Gemini format before translation
|
||||||
// This ensures Imagen responses use the same format as gemini-3-pro-image-preview
|
// This ensures Imagen responses use the same format as gemini-3-pro-image-preview
|
||||||
@@ -427,8 +428,8 @@ func (e *GeminiVertexExecutor) executeWithServiceAccount(ctx context.Context, au
|
|||||||
func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, apiKey, baseURL string) (resp cliproxyexecutor.Response, err error) {
|
func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, apiKey, baseURL string) (resp cliproxyexecutor.Response, err error) {
|
||||||
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
||||||
|
|
||||||
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
||||||
defer reporter.trackFailure(ctx, &err)
|
defer reporter.TrackFailure(ctx, &err)
|
||||||
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("gemini")
|
to := sdktranslator.FromString("gemini")
|
||||||
@@ -447,8 +448,8 @@ func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *clip
|
|||||||
}
|
}
|
||||||
|
|
||||||
body = fixGeminiImageAspectRatio(baseModel, body)
|
body = fixGeminiImageAspectRatio(baseModel, body)
|
||||||
requestedModel := payloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
||||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||||
|
|
||||||
action := getVertexAction(baseModel, false)
|
action := getVertexAction(baseModel, false)
|
||||||
@@ -484,7 +485,7 @@ func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *clip
|
|||||||
authLabel = auth.Label
|
authLabel = auth.Label
|
||||||
authType, authValue = auth.AccountInfo()
|
authType, authValue = auth.AccountInfo()
|
||||||
}
|
}
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: url,
|
URL: url,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: httpReq.Header.Clone(),
|
Headers: httpReq.Header.Clone(),
|
||||||
@@ -496,10 +497,10 @@ func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *clip
|
|||||||
AuthValue: authValue,
|
AuthValue: authValue,
|
||||||
})
|
})
|
||||||
|
|
||||||
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
httpResp, errDo := httpClient.Do(httpReq)
|
httpResp, errDo := httpClient.Do(httpReq)
|
||||||
if errDo != nil {
|
if errDo != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errDo)
|
helps.RecordAPIResponseError(ctx, e.cfg, errDo)
|
||||||
return resp, errDo
|
return resp, errDo
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -507,21 +508,21 @@ func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *clip
|
|||||||
log.Errorf("vertex executor: close response body error: %v", errClose)
|
log.Errorf("vertex executor: close response body error: %v", errClose)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||||
b, _ := io.ReadAll(httpResp.Body)
|
b, _ := io.ReadAll(httpResp.Body)
|
||||||
appendAPIResponseChunk(ctx, e.cfg, b)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, b)
|
||||||
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
||||||
err = statusErr{code: httpResp.StatusCode, msg: string(b)}
|
err = statusErr{code: httpResp.StatusCode, msg: string(b)}
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
data, errRead := io.ReadAll(httpResp.Body)
|
data, errRead := io.ReadAll(httpResp.Body)
|
||||||
if errRead != nil {
|
if errRead != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errRead)
|
helps.RecordAPIResponseError(ctx, e.cfg, errRead)
|
||||||
return resp, errRead
|
return resp, errRead
|
||||||
}
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, data)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, data)
|
||||||
reporter.publish(ctx, parseGeminiUsage(data))
|
reporter.Publish(ctx, helps.ParseGeminiUsage(data))
|
||||||
var param any
|
var param any
|
||||||
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, body, data, ¶m)
|
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, body, data, ¶m)
|
||||||
resp = cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()}
|
resp = cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()}
|
||||||
@@ -532,8 +533,8 @@ func (e *GeminiVertexExecutor) executeWithAPIKey(ctx context.Context, auth *clip
|
|||||||
func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, projectID, location string, saJSON []byte) (_ *cliproxyexecutor.StreamResult, err error) {
|
func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, projectID, location string, saJSON []byte) (_ *cliproxyexecutor.StreamResult, err error) {
|
||||||
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
||||||
|
|
||||||
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
||||||
defer reporter.trackFailure(ctx, &err)
|
defer reporter.TrackFailure(ctx, &err)
|
||||||
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("gemini")
|
to := sdktranslator.FromString("gemini")
|
||||||
@@ -552,8 +553,8 @@ func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Conte
|
|||||||
}
|
}
|
||||||
|
|
||||||
body = fixGeminiImageAspectRatio(baseModel, body)
|
body = fixGeminiImageAspectRatio(baseModel, body)
|
||||||
requestedModel := payloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
||||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||||
|
|
||||||
action := getVertexAction(baseModel, true)
|
action := getVertexAction(baseModel, true)
|
||||||
@@ -588,7 +589,7 @@ func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Conte
|
|||||||
authLabel = auth.Label
|
authLabel = auth.Label
|
||||||
authType, authValue = auth.AccountInfo()
|
authType, authValue = auth.AccountInfo()
|
||||||
}
|
}
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: url,
|
URL: url,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: httpReq.Header.Clone(),
|
Headers: httpReq.Header.Clone(),
|
||||||
@@ -600,17 +601,17 @@ func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Conte
|
|||||||
AuthValue: authValue,
|
AuthValue: authValue,
|
||||||
})
|
})
|
||||||
|
|
||||||
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
httpResp, errDo := httpClient.Do(httpReq)
|
httpResp, errDo := httpClient.Do(httpReq)
|
||||||
if errDo != nil {
|
if errDo != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errDo)
|
helps.RecordAPIResponseError(ctx, e.cfg, errDo)
|
||||||
return nil, errDo
|
return nil, errDo
|
||||||
}
|
}
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||||
b, _ := io.ReadAll(httpResp.Body)
|
b, _ := io.ReadAll(httpResp.Body)
|
||||||
appendAPIResponseChunk(ctx, e.cfg, b)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, b)
|
||||||
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
||||||
if errClose := httpResp.Body.Close(); errClose != nil {
|
if errClose := httpResp.Body.Close(); errClose != nil {
|
||||||
log.Errorf("vertex executor: close response body error: %v", errClose)
|
log.Errorf("vertex executor: close response body error: %v", errClose)
|
||||||
}
|
}
|
||||||
@@ -630,9 +631,9 @@ func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Conte
|
|||||||
var param any
|
var param any
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Bytes()
|
line := scanner.Bytes()
|
||||||
appendAPIResponseChunk(ctx, e.cfg, line)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, line)
|
||||||
if detail, ok := parseGeminiStreamUsage(line); ok {
|
if detail, ok := helps.ParseGeminiStreamUsage(line); ok {
|
||||||
reporter.publish(ctx, detail)
|
reporter.Publish(ctx, detail)
|
||||||
}
|
}
|
||||||
lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), ¶m)
|
lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), ¶m)
|
||||||
for i := range lines {
|
for i := range lines {
|
||||||
@@ -644,8 +645,8 @@ func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Conte
|
|||||||
out <- cliproxyexecutor.StreamChunk{Payload: lines[i]}
|
out <- cliproxyexecutor.StreamChunk{Payload: lines[i]}
|
||||||
}
|
}
|
||||||
if errScan := scanner.Err(); errScan != nil {
|
if errScan := scanner.Err(); errScan != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errScan)
|
helps.RecordAPIResponseError(ctx, e.cfg, errScan)
|
||||||
reporter.publishFailure(ctx)
|
reporter.PublishFailure(ctx)
|
||||||
out <- cliproxyexecutor.StreamChunk{Err: errScan}
|
out <- cliproxyexecutor.StreamChunk{Err: errScan}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -656,8 +657,8 @@ func (e *GeminiVertexExecutor) executeStreamWithServiceAccount(ctx context.Conte
|
|||||||
func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, apiKey, baseURL string) (_ *cliproxyexecutor.StreamResult, err error) {
|
func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options, apiKey, baseURL string) (_ *cliproxyexecutor.StreamResult, err error) {
|
||||||
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
||||||
|
|
||||||
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
||||||
defer reporter.trackFailure(ctx, &err)
|
defer reporter.TrackFailure(ctx, &err)
|
||||||
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("gemini")
|
to := sdktranslator.FromString("gemini")
|
||||||
@@ -676,8 +677,8 @@ func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth
|
|||||||
}
|
}
|
||||||
|
|
||||||
body = fixGeminiImageAspectRatio(baseModel, body)
|
body = fixGeminiImageAspectRatio(baseModel, body)
|
||||||
requestedModel := payloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
||||||
body, _ = sjson.SetBytes(body, "model", baseModel)
|
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||||
|
|
||||||
action := getVertexAction(baseModel, true)
|
action := getVertexAction(baseModel, true)
|
||||||
@@ -712,7 +713,7 @@ func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth
|
|||||||
authLabel = auth.Label
|
authLabel = auth.Label
|
||||||
authType, authValue = auth.AccountInfo()
|
authType, authValue = auth.AccountInfo()
|
||||||
}
|
}
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: url,
|
URL: url,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: httpReq.Header.Clone(),
|
Headers: httpReq.Header.Clone(),
|
||||||
@@ -724,17 +725,17 @@ func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth
|
|||||||
AuthValue: authValue,
|
AuthValue: authValue,
|
||||||
})
|
})
|
||||||
|
|
||||||
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
httpResp, errDo := httpClient.Do(httpReq)
|
httpResp, errDo := httpClient.Do(httpReq)
|
||||||
if errDo != nil {
|
if errDo != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errDo)
|
helps.RecordAPIResponseError(ctx, e.cfg, errDo)
|
||||||
return nil, errDo
|
return nil, errDo
|
||||||
}
|
}
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||||
b, _ := io.ReadAll(httpResp.Body)
|
b, _ := io.ReadAll(httpResp.Body)
|
||||||
appendAPIResponseChunk(ctx, e.cfg, b)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, b)
|
||||||
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
||||||
if errClose := httpResp.Body.Close(); errClose != nil {
|
if errClose := httpResp.Body.Close(); errClose != nil {
|
||||||
log.Errorf("vertex executor: close response body error: %v", errClose)
|
log.Errorf("vertex executor: close response body error: %v", errClose)
|
||||||
}
|
}
|
||||||
@@ -754,9 +755,9 @@ func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth
|
|||||||
var param any
|
var param any
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Bytes()
|
line := scanner.Bytes()
|
||||||
appendAPIResponseChunk(ctx, e.cfg, line)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, line)
|
||||||
if detail, ok := parseGeminiStreamUsage(line); ok {
|
if detail, ok := helps.ParseGeminiStreamUsage(line); ok {
|
||||||
reporter.publish(ctx, detail)
|
reporter.Publish(ctx, detail)
|
||||||
}
|
}
|
||||||
lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), ¶m)
|
lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), ¶m)
|
||||||
for i := range lines {
|
for i := range lines {
|
||||||
@@ -768,8 +769,8 @@ func (e *GeminiVertexExecutor) executeStreamWithAPIKey(ctx context.Context, auth
|
|||||||
out <- cliproxyexecutor.StreamChunk{Payload: lines[i]}
|
out <- cliproxyexecutor.StreamChunk{Payload: lines[i]}
|
||||||
}
|
}
|
||||||
if errScan := scanner.Err(); errScan != nil {
|
if errScan := scanner.Err(); errScan != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errScan)
|
helps.RecordAPIResponseError(ctx, e.cfg, errScan)
|
||||||
reporter.publishFailure(ctx)
|
reporter.PublishFailure(ctx)
|
||||||
out <- cliproxyexecutor.StreamChunk{Err: errScan}
|
out <- cliproxyexecutor.StreamChunk{Err: errScan}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -819,7 +820,7 @@ func (e *GeminiVertexExecutor) countTokensWithServiceAccount(ctx context.Context
|
|||||||
authLabel = auth.Label
|
authLabel = auth.Label
|
||||||
authType, authValue = auth.AccountInfo()
|
authType, authValue = auth.AccountInfo()
|
||||||
}
|
}
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: url,
|
URL: url,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: httpReq.Header.Clone(),
|
Headers: httpReq.Header.Clone(),
|
||||||
@@ -831,10 +832,10 @@ func (e *GeminiVertexExecutor) countTokensWithServiceAccount(ctx context.Context
|
|||||||
AuthValue: authValue,
|
AuthValue: authValue,
|
||||||
})
|
})
|
||||||
|
|
||||||
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
httpResp, errDo := httpClient.Do(httpReq)
|
httpResp, errDo := httpClient.Do(httpReq)
|
||||||
if errDo != nil {
|
if errDo != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errDo)
|
helps.RecordAPIResponseError(ctx, e.cfg, errDo)
|
||||||
return cliproxyexecutor.Response{}, errDo
|
return cliproxyexecutor.Response{}, errDo
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -842,19 +843,19 @@ func (e *GeminiVertexExecutor) countTokensWithServiceAccount(ctx context.Context
|
|||||||
log.Errorf("vertex executor: close response body error: %v", errClose)
|
log.Errorf("vertex executor: close response body error: %v", errClose)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||||
b, _ := io.ReadAll(httpResp.Body)
|
b, _ := io.ReadAll(httpResp.Body)
|
||||||
appendAPIResponseChunk(ctx, e.cfg, b)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, b)
|
||||||
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
||||||
return cliproxyexecutor.Response{}, statusErr{code: httpResp.StatusCode, msg: string(b)}
|
return cliproxyexecutor.Response{}, statusErr{code: httpResp.StatusCode, msg: string(b)}
|
||||||
}
|
}
|
||||||
data, errRead := io.ReadAll(httpResp.Body)
|
data, errRead := io.ReadAll(httpResp.Body)
|
||||||
if errRead != nil {
|
if errRead != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errRead)
|
helps.RecordAPIResponseError(ctx, e.cfg, errRead)
|
||||||
return cliproxyexecutor.Response{}, errRead
|
return cliproxyexecutor.Response{}, errRead
|
||||||
}
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, data)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, data)
|
||||||
count := gjson.GetBytes(data, "totalTokens").Int()
|
count := gjson.GetBytes(data, "totalTokens").Int()
|
||||||
out := sdktranslator.TranslateTokenCount(ctx, to, from, count, data)
|
out := sdktranslator.TranslateTokenCount(ctx, to, from, count, data)
|
||||||
return cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()}, nil
|
return cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()}, nil
|
||||||
@@ -903,7 +904,7 @@ func (e *GeminiVertexExecutor) countTokensWithAPIKey(ctx context.Context, auth *
|
|||||||
authLabel = auth.Label
|
authLabel = auth.Label
|
||||||
authType, authValue = auth.AccountInfo()
|
authType, authValue = auth.AccountInfo()
|
||||||
}
|
}
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: url,
|
URL: url,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: httpReq.Header.Clone(),
|
Headers: httpReq.Header.Clone(),
|
||||||
@@ -915,10 +916,10 @@ func (e *GeminiVertexExecutor) countTokensWithAPIKey(ctx context.Context, auth *
|
|||||||
AuthValue: authValue,
|
AuthValue: authValue,
|
||||||
})
|
})
|
||||||
|
|
||||||
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
httpResp, errDo := httpClient.Do(httpReq)
|
httpResp, errDo := httpClient.Do(httpReq)
|
||||||
if errDo != nil {
|
if errDo != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errDo)
|
helps.RecordAPIResponseError(ctx, e.cfg, errDo)
|
||||||
return cliproxyexecutor.Response{}, errDo
|
return cliproxyexecutor.Response{}, errDo
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -926,19 +927,19 @@ func (e *GeminiVertexExecutor) countTokensWithAPIKey(ctx context.Context, auth *
|
|||||||
log.Errorf("vertex executor: close response body error: %v", errClose)
|
log.Errorf("vertex executor: close response body error: %v", errClose)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||||
b, _ := io.ReadAll(httpResp.Body)
|
b, _ := io.ReadAll(httpResp.Body)
|
||||||
appendAPIResponseChunk(ctx, e.cfg, b)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, b)
|
||||||
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
||||||
return cliproxyexecutor.Response{}, statusErr{code: httpResp.StatusCode, msg: string(b)}
|
return cliproxyexecutor.Response{}, statusErr{code: httpResp.StatusCode, msg: string(b)}
|
||||||
}
|
}
|
||||||
data, errRead := io.ReadAll(httpResp.Body)
|
data, errRead := io.ReadAll(httpResp.Body)
|
||||||
if errRead != nil {
|
if errRead != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errRead)
|
helps.RecordAPIResponseError(ctx, e.cfg, errRead)
|
||||||
return cliproxyexecutor.Response{}, errRead
|
return cliproxyexecutor.Response{}, errRead
|
||||||
}
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, data)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, data)
|
||||||
count := gjson.GetBytes(data, "totalTokens").Int()
|
count := gjson.GetBytes(data, "totalTokens").Int()
|
||||||
out := sdktranslator.TranslateTokenCount(ctx, to, from, count, data)
|
out := sdktranslator.TranslateTokenCount(ctx, to, from, count, data)
|
||||||
return cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()}, nil
|
return cliproxyexecutor.Response{Payload: out, Headers: httpResp.Header.Clone()}, nil
|
||||||
@@ -1012,7 +1013,7 @@ func vertexBaseURL(location string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func vertexAccessToken(ctx context.Context, cfg *config.Config, auth *cliproxyauth.Auth, saJSON []byte) (string, error) {
|
func vertexAccessToken(ctx context.Context, cfg *config.Config, auth *cliproxyauth.Auth, saJSON []byte) (string, error) {
|
||||||
if httpClient := newProxyAwareHTTPClient(ctx, cfg, auth, 0); httpClient != nil {
|
if httpClient := helps.NewProxyAwareHTTPClient(ctx, cfg, auth, 0); httpClient != nil {
|
||||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
|
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
|
||||||
}
|
}
|
||||||
// Use cloud-platform scope for Vertex AI.
|
// Use cloud-platform scope for Vertex AI.
|
||||||
|
|||||||
+8
-8
@@ -1,11 +1,11 @@
|
|||||||
package executor
|
package helps
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type codexCache struct {
|
type CodexCache struct {
|
||||||
ID string
|
ID string
|
||||||
Expire time.Time
|
Expire time.Time
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@ type codexCache struct {
|
|||||||
// codexCacheMap stores prompt cache IDs keyed by model+user_id.
|
// codexCacheMap stores prompt cache IDs keyed by model+user_id.
|
||||||
// Protected by codexCacheMu. Entries expire after 1 hour.
|
// Protected by codexCacheMu. Entries expire after 1 hour.
|
||||||
var (
|
var (
|
||||||
codexCacheMap = make(map[string]codexCache)
|
codexCacheMap = make(map[string]CodexCache)
|
||||||
codexCacheMu sync.RWMutex
|
codexCacheMu sync.RWMutex
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -47,20 +47,20 @@ func purgeExpiredCodexCache() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// getCodexCache retrieves a cached entry, returning ok=false if not found or expired.
|
// GetCodexCache retrieves a cached entry, returning ok=false if not found or expired.
|
||||||
func getCodexCache(key string) (codexCache, bool) {
|
func GetCodexCache(key string) (CodexCache, bool) {
|
||||||
codexCacheCleanupOnce.Do(startCodexCacheCleanup)
|
codexCacheCleanupOnce.Do(startCodexCacheCleanup)
|
||||||
codexCacheMu.RLock()
|
codexCacheMu.RLock()
|
||||||
cache, ok := codexCacheMap[key]
|
cache, ok := codexCacheMap[key]
|
||||||
codexCacheMu.RUnlock()
|
codexCacheMu.RUnlock()
|
||||||
if !ok || cache.Expire.Before(time.Now()) {
|
if !ok || cache.Expire.Before(time.Now()) {
|
||||||
return codexCache{}, false
|
return CodexCache{}, false
|
||||||
}
|
}
|
||||||
return cache, true
|
return cache, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// setCodexCache stores a cache entry.
|
// SetCodexCache stores a cache entry.
|
||||||
func setCodexCache(key string, cache codexCache) {
|
func SetCodexCache(key string, cache CodexCache) {
|
||||||
codexCacheCleanupOnce.Do(startCodexCacheCleanup)
|
codexCacheCleanupOnce.Do(startCodexCacheCleanup)
|
||||||
codexCacheMu.Lock()
|
codexCacheMu.Lock()
|
||||||
codexCacheMap[key] = cache
|
codexCacheMap[key] = cache
|
||||||
+37
-31
@@ -1,4 +1,4 @@
|
|||||||
package executor
|
package helps
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
@@ -32,7 +32,7 @@ var (
|
|||||||
claudeDeviceProfileCacheMu sync.RWMutex
|
claudeDeviceProfileCacheMu sync.RWMutex
|
||||||
claudeDeviceProfileCacheCleanupOnce sync.Once
|
claudeDeviceProfileCacheCleanupOnce sync.Once
|
||||||
|
|
||||||
claudeDeviceProfileBeforeCandidateStore func(claudeDeviceProfile)
|
ClaudeDeviceProfileBeforeCandidateStore func(ClaudeDeviceProfile)
|
||||||
)
|
)
|
||||||
|
|
||||||
type claudeCLIVersion struct {
|
type claudeCLIVersion struct {
|
||||||
@@ -63,29 +63,35 @@ func (v claudeCLIVersion) Compare(other claudeCLIVersion) int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type claudeDeviceProfile struct {
|
type ClaudeDeviceProfile struct {
|
||||||
UserAgent string
|
UserAgent string
|
||||||
PackageVersion string
|
PackageVersion string
|
||||||
RuntimeVersion string
|
RuntimeVersion string
|
||||||
OS string
|
OS string
|
||||||
Arch string
|
Arch string
|
||||||
Version claudeCLIVersion
|
version claudeCLIVersion
|
||||||
HasVersion bool
|
hasVersion bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type claudeDeviceProfileCacheEntry struct {
|
type claudeDeviceProfileCacheEntry struct {
|
||||||
profile claudeDeviceProfile
|
profile ClaudeDeviceProfile
|
||||||
expire time.Time
|
expire time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func claudeDeviceProfileStabilizationEnabled(cfg *config.Config) bool {
|
func ClaudeDeviceProfileStabilizationEnabled(cfg *config.Config) bool {
|
||||||
if cfg == nil || cfg.ClaudeHeaderDefaults.StabilizeDeviceProfile == nil {
|
if cfg == nil || cfg.ClaudeHeaderDefaults.StabilizeDeviceProfile == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return *cfg.ClaudeHeaderDefaults.StabilizeDeviceProfile
|
return *cfg.ClaudeHeaderDefaults.StabilizeDeviceProfile
|
||||||
}
|
}
|
||||||
|
|
||||||
func defaultClaudeDeviceProfile(cfg *config.Config) claudeDeviceProfile {
|
func ResetClaudeDeviceProfileCache() {
|
||||||
|
claudeDeviceProfileCacheMu.Lock()
|
||||||
|
claudeDeviceProfileCache = make(map[string]claudeDeviceProfileCacheEntry)
|
||||||
|
claudeDeviceProfileCacheMu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultClaudeDeviceProfile(cfg *config.Config) ClaudeDeviceProfile {
|
||||||
hdrDefault := func(cfgVal, fallback string) string {
|
hdrDefault := func(cfgVal, fallback string) string {
|
||||||
if strings.TrimSpace(cfgVal) != "" {
|
if strings.TrimSpace(cfgVal) != "" {
|
||||||
return strings.TrimSpace(cfgVal)
|
return strings.TrimSpace(cfgVal)
|
||||||
@@ -98,7 +104,7 @@ func defaultClaudeDeviceProfile(cfg *config.Config) claudeDeviceProfile {
|
|||||||
hd = cfg.ClaudeHeaderDefaults
|
hd = cfg.ClaudeHeaderDefaults
|
||||||
}
|
}
|
||||||
|
|
||||||
profile := claudeDeviceProfile{
|
profile := ClaudeDeviceProfile{
|
||||||
UserAgent: hdrDefault(hd.UserAgent, defaultClaudeFingerprintUserAgent),
|
UserAgent: hdrDefault(hd.UserAgent, defaultClaudeFingerprintUserAgent),
|
||||||
PackageVersion: hdrDefault(hd.PackageVersion, defaultClaudeFingerprintPackageVersion),
|
PackageVersion: hdrDefault(hd.PackageVersion, defaultClaudeFingerprintPackageVersion),
|
||||||
RuntimeVersion: hdrDefault(hd.RuntimeVersion, defaultClaudeFingerprintRuntimeVersion),
|
RuntimeVersion: hdrDefault(hd.RuntimeVersion, defaultClaudeFingerprintRuntimeVersion),
|
||||||
@@ -106,8 +112,8 @@ func defaultClaudeDeviceProfile(cfg *config.Config) claudeDeviceProfile {
|
|||||||
Arch: hdrDefault(hd.Arch, defaultClaudeFingerprintArch),
|
Arch: hdrDefault(hd.Arch, defaultClaudeFingerprintArch),
|
||||||
}
|
}
|
||||||
if version, ok := parseClaudeCLIVersion(profile.UserAgent); ok {
|
if version, ok := parseClaudeCLIVersion(profile.UserAgent); ok {
|
||||||
profile.Version = version
|
profile.version = version
|
||||||
profile.HasVersion = true
|
profile.hasVersion = true
|
||||||
}
|
}
|
||||||
return profile
|
return profile
|
||||||
}
|
}
|
||||||
@@ -162,17 +168,17 @@ func parseClaudeCLIVersion(userAgent string) (claudeCLIVersion, bool) {
|
|||||||
return claudeCLIVersion{major: major, minor: minor, patch: patch}, true
|
return claudeCLIVersion{major: major, minor: minor, patch: patch}, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func shouldUpgradeClaudeDeviceProfile(candidate, current claudeDeviceProfile) bool {
|
func shouldUpgradeClaudeDeviceProfile(candidate, current ClaudeDeviceProfile) bool {
|
||||||
if candidate.UserAgent == "" || !candidate.HasVersion {
|
if candidate.UserAgent == "" || !candidate.hasVersion {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if current.UserAgent == "" || !current.HasVersion {
|
if current.UserAgent == "" || !current.hasVersion {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return candidate.Version.Compare(current.Version) > 0
|
return candidate.version.Compare(current.version) > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func pinClaudeDeviceProfilePlatform(profile, baseline claudeDeviceProfile) claudeDeviceProfile {
|
func pinClaudeDeviceProfilePlatform(profile, baseline ClaudeDeviceProfile) ClaudeDeviceProfile {
|
||||||
profile.OS = baseline.OS
|
profile.OS = baseline.OS
|
||||||
profile.Arch = baseline.Arch
|
profile.Arch = baseline.Arch
|
||||||
return profile
|
return profile
|
||||||
@@ -180,38 +186,38 @@ func pinClaudeDeviceProfilePlatform(profile, baseline claudeDeviceProfile) claud
|
|||||||
|
|
||||||
// normalizeClaudeDeviceProfile keeps stabilized profiles pinned to the current
|
// normalizeClaudeDeviceProfile keeps stabilized profiles pinned to the current
|
||||||
// baseline platform and enforces the baseline software fingerprint as a floor.
|
// baseline platform and enforces the baseline software fingerprint as a floor.
|
||||||
func normalizeClaudeDeviceProfile(profile, baseline claudeDeviceProfile) claudeDeviceProfile {
|
func normalizeClaudeDeviceProfile(profile, baseline ClaudeDeviceProfile) ClaudeDeviceProfile {
|
||||||
profile = pinClaudeDeviceProfilePlatform(profile, baseline)
|
profile = pinClaudeDeviceProfilePlatform(profile, baseline)
|
||||||
if profile.UserAgent == "" || !profile.HasVersion || shouldUpgradeClaudeDeviceProfile(baseline, profile) {
|
if profile.UserAgent == "" || !profile.hasVersion || shouldUpgradeClaudeDeviceProfile(baseline, profile) {
|
||||||
profile.UserAgent = baseline.UserAgent
|
profile.UserAgent = baseline.UserAgent
|
||||||
profile.PackageVersion = baseline.PackageVersion
|
profile.PackageVersion = baseline.PackageVersion
|
||||||
profile.RuntimeVersion = baseline.RuntimeVersion
|
profile.RuntimeVersion = baseline.RuntimeVersion
|
||||||
profile.Version = baseline.Version
|
profile.version = baseline.version
|
||||||
profile.HasVersion = baseline.HasVersion
|
profile.hasVersion = baseline.hasVersion
|
||||||
}
|
}
|
||||||
return profile
|
return profile
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractClaudeDeviceProfile(headers http.Header, cfg *config.Config) (claudeDeviceProfile, bool) {
|
func extractClaudeDeviceProfile(headers http.Header, cfg *config.Config) (ClaudeDeviceProfile, bool) {
|
||||||
if headers == nil {
|
if headers == nil {
|
||||||
return claudeDeviceProfile{}, false
|
return ClaudeDeviceProfile{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
userAgent := strings.TrimSpace(headers.Get("User-Agent"))
|
userAgent := strings.TrimSpace(headers.Get("User-Agent"))
|
||||||
version, ok := parseClaudeCLIVersion(userAgent)
|
version, ok := parseClaudeCLIVersion(userAgent)
|
||||||
if !ok {
|
if !ok {
|
||||||
return claudeDeviceProfile{}, false
|
return ClaudeDeviceProfile{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
baseline := defaultClaudeDeviceProfile(cfg)
|
baseline := defaultClaudeDeviceProfile(cfg)
|
||||||
profile := claudeDeviceProfile{
|
profile := ClaudeDeviceProfile{
|
||||||
UserAgent: userAgent,
|
UserAgent: userAgent,
|
||||||
PackageVersion: firstNonEmptyHeader(headers, "X-Stainless-Package-Version", baseline.PackageVersion),
|
PackageVersion: firstNonEmptyHeader(headers, "X-Stainless-Package-Version", baseline.PackageVersion),
|
||||||
RuntimeVersion: firstNonEmptyHeader(headers, "X-Stainless-Runtime-Version", baseline.RuntimeVersion),
|
RuntimeVersion: firstNonEmptyHeader(headers, "X-Stainless-Runtime-Version", baseline.RuntimeVersion),
|
||||||
OS: firstNonEmptyHeader(headers, "X-Stainless-Os", baseline.OS),
|
OS: firstNonEmptyHeader(headers, "X-Stainless-Os", baseline.OS),
|
||||||
Arch: firstNonEmptyHeader(headers, "X-Stainless-Arch", baseline.Arch),
|
Arch: firstNonEmptyHeader(headers, "X-Stainless-Arch", baseline.Arch),
|
||||||
Version: version,
|
version: version,
|
||||||
HasVersion: true,
|
hasVersion: true,
|
||||||
}
|
}
|
||||||
return profile, true
|
return profile, true
|
||||||
}
|
}
|
||||||
@@ -263,7 +269,7 @@ func purgeExpiredClaudeDeviceProfiles() {
|
|||||||
claudeDeviceProfileCacheMu.Unlock()
|
claudeDeviceProfileCacheMu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveClaudeDeviceProfile(auth *cliproxyauth.Auth, apiKey string, headers http.Header, cfg *config.Config) claudeDeviceProfile {
|
func ResolveClaudeDeviceProfile(auth *cliproxyauth.Auth, apiKey string, headers http.Header, cfg *config.Config) ClaudeDeviceProfile {
|
||||||
claudeDeviceProfileCacheCleanupOnce.Do(startClaudeDeviceProfileCacheCleanup)
|
claudeDeviceProfileCacheCleanupOnce.Do(startClaudeDeviceProfileCacheCleanup)
|
||||||
|
|
||||||
cacheKey := claudeDeviceProfileCacheKey(auth, apiKey)
|
cacheKey := claudeDeviceProfileCacheKey(auth, apiKey)
|
||||||
@@ -283,8 +289,8 @@ func resolveClaudeDeviceProfile(auth *cliproxyauth.Auth, apiKey string, headers
|
|||||||
claudeDeviceProfileCacheMu.RUnlock()
|
claudeDeviceProfileCacheMu.RUnlock()
|
||||||
|
|
||||||
if hasCandidate {
|
if hasCandidate {
|
||||||
if claudeDeviceProfileBeforeCandidateStore != nil {
|
if ClaudeDeviceProfileBeforeCandidateStore != nil {
|
||||||
claudeDeviceProfileBeforeCandidateStore(candidate)
|
ClaudeDeviceProfileBeforeCandidateStore(candidate)
|
||||||
}
|
}
|
||||||
|
|
||||||
claudeDeviceProfileCacheMu.Lock()
|
claudeDeviceProfileCacheMu.Lock()
|
||||||
@@ -324,7 +330,7 @@ func resolveClaudeDeviceProfile(auth *cliproxyauth.Auth, apiKey string, headers
|
|||||||
return baseline
|
return baseline
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyClaudeDeviceProfileHeaders(r *http.Request, profile claudeDeviceProfile) {
|
func ApplyClaudeDeviceProfileHeaders(r *http.Request, profile ClaudeDeviceProfile) {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -344,7 +350,7 @@ func applyClaudeDeviceProfileHeaders(r *http.Request, profile claudeDeviceProfil
|
|||||||
r.Header.Set("X-Stainless-Arch", profile.Arch)
|
r.Header.Set("X-Stainless-Arch", profile.Arch)
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyClaudeLegacyDeviceHeaders(r *http.Request, ginHeaders http.Header, cfg *config.Config) {
|
func ApplyClaudeLegacyDeviceHeaders(r *http.Request, ginHeaders http.Header, cfg *config.Config) {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
+5
-5
@@ -1,4 +1,4 @@
|
|||||||
package executor
|
package helps
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -18,9 +18,9 @@ type SensitiveWordMatcher struct {
|
|||||||
regex *regexp.Regexp
|
regex *regexp.Regexp
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildSensitiveWordMatcher compiles a regex from the word list.
|
// BuildSensitiveWordMatcher compiles a regex from the word list.
|
||||||
// Words are sorted by length (longest first) for proper matching.
|
// Words are sorted by length (longest first) for proper matching.
|
||||||
func buildSensitiveWordMatcher(words []string) *SensitiveWordMatcher {
|
func BuildSensitiveWordMatcher(words []string) *SensitiveWordMatcher {
|
||||||
if len(words) == 0 {
|
if len(words) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -81,9 +81,9 @@ func (m *SensitiveWordMatcher) obfuscateText(text string) string {
|
|||||||
return m.regex.ReplaceAllStringFunc(text, obfuscateWord)
|
return m.regex.ReplaceAllStringFunc(text, obfuscateWord)
|
||||||
}
|
}
|
||||||
|
|
||||||
// obfuscateSensitiveWords processes the payload and obfuscates sensitive words
|
// ObfuscateSensitiveWords processes the payload and obfuscates sensitive words
|
||||||
// in system blocks and message content.
|
// in system blocks and message content.
|
||||||
func obfuscateSensitiveWords(payload []byte, matcher *SensitiveWordMatcher) []byte {
|
func ObfuscateSensitiveWords(payload []byte, matcher *SensitiveWordMatcher) []byte {
|
||||||
if matcher == nil || matcher.regex == nil {
|
if matcher == nil || matcher.regex == nil {
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
+11
-3
@@ -1,4 +1,4 @@
|
|||||||
package executor
|
package helps
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
@@ -28,9 +28,17 @@ func isValidUserID(userID string) bool {
|
|||||||
return userIDPattern.MatchString(userID)
|
return userIDPattern.MatchString(userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// shouldCloak determines if request should be cloaked based on config and client User-Agent.
|
func GenerateFakeUserID() string {
|
||||||
|
return generateFakeUserID()
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsValidUserID(userID string) bool {
|
||||||
|
return isValidUserID(userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldCloak determines if request should be cloaked based on config and client User-Agent.
|
||||||
// Returns true if cloaking should be applied.
|
// Returns true if cloaking should be applied.
|
||||||
func shouldCloak(cloakMode string, userAgent string) bool {
|
func ShouldCloak(cloakMode string, userAgent string) bool {
|
||||||
switch strings.ToLower(cloakMode) {
|
switch strings.ToLower(cloakMode) {
|
||||||
case "always":
|
case "always":
|
||||||
return true
|
return true
|
||||||
+14
-14
@@ -1,4 +1,4 @@
|
|||||||
package executor
|
package helps
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -24,8 +24,8 @@ const (
|
|||||||
apiResponseKey = "API_RESPONSE"
|
apiResponseKey = "API_RESPONSE"
|
||||||
)
|
)
|
||||||
|
|
||||||
// upstreamRequestLog captures the outbound upstream request details for logging.
|
// UpstreamRequestLog captures the outbound upstream request details for logging.
|
||||||
type upstreamRequestLog struct {
|
type UpstreamRequestLog struct {
|
||||||
URL string
|
URL string
|
||||||
Method string
|
Method string
|
||||||
Headers http.Header
|
Headers http.Header
|
||||||
@@ -49,8 +49,8 @@ type upstreamAttempt struct {
|
|||||||
errorWritten bool
|
errorWritten bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// recordAPIRequest stores the upstream request metadata in Gin context for request logging.
|
// RecordAPIRequest stores the upstream request metadata in Gin context for request logging.
|
||||||
func recordAPIRequest(ctx context.Context, cfg *config.Config, info upstreamRequestLog) {
|
func RecordAPIRequest(ctx context.Context, cfg *config.Config, info UpstreamRequestLog) {
|
||||||
if cfg == nil || !cfg.RequestLog {
|
if cfg == nil || !cfg.RequestLog {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -96,8 +96,8 @@ func recordAPIRequest(ctx context.Context, cfg *config.Config, info upstreamRequ
|
|||||||
updateAggregatedRequest(ginCtx, attempts)
|
updateAggregatedRequest(ginCtx, attempts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// recordAPIResponseMetadata captures upstream response status/header information for the latest attempt.
|
// RecordAPIResponseMetadata captures upstream response status/header information for the latest attempt.
|
||||||
func recordAPIResponseMetadata(ctx context.Context, cfg *config.Config, status int, headers http.Header) {
|
func RecordAPIResponseMetadata(ctx context.Context, cfg *config.Config, status int, headers http.Header) {
|
||||||
if cfg == nil || !cfg.RequestLog {
|
if cfg == nil || !cfg.RequestLog {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -122,8 +122,8 @@ func recordAPIResponseMetadata(ctx context.Context, cfg *config.Config, status i
|
|||||||
updateAggregatedResponse(ginCtx, attempts)
|
updateAggregatedResponse(ginCtx, attempts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// recordAPIResponseError adds an error entry for the latest attempt when no HTTP response is available.
|
// RecordAPIResponseError adds an error entry for the latest attempt when no HTTP response is available.
|
||||||
func recordAPIResponseError(ctx context.Context, cfg *config.Config, err error) {
|
func RecordAPIResponseError(ctx context.Context, cfg *config.Config, err error) {
|
||||||
if cfg == nil || !cfg.RequestLog || err == nil {
|
if cfg == nil || !cfg.RequestLog || err == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -147,8 +147,8 @@ func recordAPIResponseError(ctx context.Context, cfg *config.Config, err error)
|
|||||||
updateAggregatedResponse(ginCtx, attempts)
|
updateAggregatedResponse(ginCtx, attempts)
|
||||||
}
|
}
|
||||||
|
|
||||||
// appendAPIResponseChunk appends an upstream response chunk to Gin context for request logging.
|
// AppendAPIResponseChunk appends an upstream response chunk to Gin context for request logging.
|
||||||
func appendAPIResponseChunk(ctx context.Context, cfg *config.Config, chunk []byte) {
|
func AppendAPIResponseChunk(ctx context.Context, cfg *config.Config, chunk []byte) {
|
||||||
if cfg == nil || !cfg.RequestLog {
|
if cfg == nil || !cfg.RequestLog {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -285,7 +285,7 @@ func writeHeaders(builder *strings.Builder, headers http.Header) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func formatAuthInfo(info upstreamRequestLog) string {
|
func formatAuthInfo(info UpstreamRequestLog) string {
|
||||||
var parts []string
|
var parts []string
|
||||||
if trimmed := strings.TrimSpace(info.Provider); trimmed != "" {
|
if trimmed := strings.TrimSpace(info.Provider); trimmed != "" {
|
||||||
parts = append(parts, fmt.Sprintf("provider=%s", trimmed))
|
parts = append(parts, fmt.Sprintf("provider=%s", trimmed))
|
||||||
@@ -321,7 +321,7 @@ func formatAuthInfo(info upstreamRequestLog) string {
|
|||||||
return strings.Join(parts, ", ")
|
return strings.Join(parts, ", ")
|
||||||
}
|
}
|
||||||
|
|
||||||
func summarizeErrorBody(contentType string, body []byte) string {
|
func SummarizeErrorBody(contentType string, body []byte) string {
|
||||||
isHTML := strings.Contains(strings.ToLower(contentType), "text/html")
|
isHTML := strings.Contains(strings.ToLower(contentType), "text/html")
|
||||||
if !isHTML {
|
if !isHTML {
|
||||||
trimmed := bytes.TrimSpace(bytes.ToLower(body))
|
trimmed := bytes.TrimSpace(bytes.ToLower(body))
|
||||||
@@ -379,7 +379,7 @@ func extractJSONErrorMessage(body []byte) string {
|
|||||||
|
|
||||||
// logWithRequestID returns a logrus Entry with request_id field populated from context.
|
// logWithRequestID returns a logrus Entry with request_id field populated from context.
|
||||||
// If no request ID is found in context, it returns the standard logger.
|
// If no request ID is found in context, it returns the standard logger.
|
||||||
func logWithRequestID(ctx context.Context) *log.Entry {
|
func LogWithRequestID(ctx context.Context) *log.Entry {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
return log.NewEntry(log.StandardLogger())
|
return log.NewEntry(log.StandardLogger())
|
||||||
}
|
}
|
||||||
+4
-4
@@ -1,4 +1,4 @@
|
|||||||
package executor
|
package helps
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@@ -11,12 +11,12 @@ import (
|
|||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
// applyPayloadConfigWithRoot behaves like applyPayloadConfig but treats all parameter
|
// ApplyPayloadConfigWithRoot behaves like applyPayloadConfig but treats all parameter
|
||||||
// paths as relative to the provided root path (for example, "request" for Gemini CLI)
|
// paths as relative to the provided root path (for example, "request" for Gemini CLI)
|
||||||
// and restricts matches to the given protocol when supplied. Defaults are checked
|
// and restricts matches to the given protocol when supplied. Defaults are checked
|
||||||
// against the original payload when provided. requestedModel carries the client-visible
|
// against the original payload when provided. requestedModel carries the client-visible
|
||||||
// model name before alias resolution so payload rules can target aliases precisely.
|
// model name before alias resolution so payload rules can target aliases precisely.
|
||||||
func applyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string, payload, original []byte, requestedModel string) []byte {
|
func ApplyPayloadConfigWithRoot(cfg *config.Config, model, protocol, root string, payload, original []byte, requestedModel string) []byte {
|
||||||
if cfg == nil || len(payload) == 0 {
|
if cfg == nil || len(payload) == 0 {
|
||||||
return payload
|
return payload
|
||||||
}
|
}
|
||||||
@@ -244,7 +244,7 @@ func payloadRawValue(value any) ([]byte, bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func payloadRequestedModel(opts cliproxyexecutor.Options, fallback string) string {
|
func PayloadRequestedModel(opts cliproxyexecutor.Options, fallback string) string {
|
||||||
fallback = strings.TrimSpace(fallback)
|
fallback = strings.TrimSpace(fallback)
|
||||||
if len(opts.Metadata) == 0 {
|
if len(opts.Metadata) == 0 {
|
||||||
return fallback
|
return fallback
|
||||||
+3
-3
@@ -1,4 +1,4 @@
|
|||||||
package executor
|
package helps
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// newProxyAwareHTTPClient creates an HTTP client with proper proxy configuration priority:
|
// NewProxyAwareHTTPClient creates an HTTP client with proper proxy configuration priority:
|
||||||
// 1. Use auth.ProxyURL if configured (highest priority)
|
// 1. Use auth.ProxyURL if configured (highest priority)
|
||||||
// 2. Use cfg.ProxyURL if auth proxy is not configured
|
// 2. Use cfg.ProxyURL if auth proxy is not configured
|
||||||
// 3. Use RoundTripper from context if neither are configured
|
// 3. Use RoundTripper from context if neither are configured
|
||||||
@@ -25,7 +25,7 @@ import (
|
|||||||
//
|
//
|
||||||
// Returns:
|
// Returns:
|
||||||
// - *http.Client: An HTTP client with configured proxy or transport
|
// - *http.Client: An HTTP client with configured proxy or transport
|
||||||
func newProxyAwareHTTPClient(ctx context.Context, cfg *config.Config, auth *cliproxyauth.Auth, timeout time.Duration) *http.Client {
|
func NewProxyAwareHTTPClient(ctx context.Context, cfg *config.Config, auth *cliproxyauth.Auth, timeout time.Duration) *http.Client {
|
||||||
httpClient := &http.Client{}
|
httpClient := &http.Client{}
|
||||||
if timeout > 0 {
|
if timeout > 0 {
|
||||||
httpClient.Timeout = timeout
|
httpClient.Timeout = timeout
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
package executor
|
package helps
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -13,7 +13,7 @@ import (
|
|||||||
func TestNewProxyAwareHTTPClientDirectBypassesGlobalProxy(t *testing.T) {
|
func TestNewProxyAwareHTTPClientDirectBypassesGlobalProxy(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
client := newProxyAwareHTTPClient(
|
client := NewProxyAwareHTTPClient(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
&config.Config{SDKConfig: sdkconfig.SDKConfig{ProxyURL: "http://global-proxy.example.com:8080"}},
|
&config.Config{SDKConfig: sdkconfig.SDKConfig{ProxyURL: "http://global-proxy.example.com:8080"}},
|
||||||
&cliproxyauth.Auth{ProxyURL: "direct"},
|
&cliproxyauth.Auth{ProxyURL: "direct"},
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package executor
|
package helps
|
||||||
|
|
||||||
import (
|
import (
|
||||||
_ "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking/provider/antigravity"
|
_ "github.com/router-for-me/CLIProxyAPI/v6/internal/thinking/provider/antigravity"
|
||||||
+7
-7
@@ -1,4 +1,4 @@
|
|||||||
package executor
|
package helps
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -8,8 +8,8 @@ import (
|
|||||||
"github.com/tiktoken-go/tokenizer"
|
"github.com/tiktoken-go/tokenizer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// tokenizerForModel returns a tokenizer codec suitable for an OpenAI-style model id.
|
// TokenizerForModel returns a tokenizer codec suitable for an OpenAI-style model id.
|
||||||
func tokenizerForModel(model string) (tokenizer.Codec, error) {
|
func TokenizerForModel(model string) (tokenizer.Codec, error) {
|
||||||
sanitized := strings.ToLower(strings.TrimSpace(model))
|
sanitized := strings.ToLower(strings.TrimSpace(model))
|
||||||
switch {
|
switch {
|
||||||
case sanitized == "":
|
case sanitized == "":
|
||||||
@@ -37,8 +37,8 @@ func tokenizerForModel(model string) (tokenizer.Codec, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// countOpenAIChatTokens approximates prompt tokens for OpenAI chat completions payloads.
|
// CountOpenAIChatTokens approximates prompt tokens for OpenAI chat completions payloads.
|
||||||
func countOpenAIChatTokens(enc tokenizer.Codec, payload []byte) (int64, error) {
|
func CountOpenAIChatTokens(enc tokenizer.Codec, payload []byte) (int64, error) {
|
||||||
if enc == nil {
|
if enc == nil {
|
||||||
return 0, fmt.Errorf("encoder is nil")
|
return 0, fmt.Errorf("encoder is nil")
|
||||||
}
|
}
|
||||||
@@ -69,8 +69,8 @@ func countOpenAIChatTokens(enc tokenizer.Codec, payload []byte) (int64, error) {
|
|||||||
return int64(count), nil
|
return int64(count), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildOpenAIUsageJSON returns a minimal usage structure understood by downstream translators.
|
// BuildOpenAIUsageJSON returns a minimal usage structure understood by downstream translators.
|
||||||
func buildOpenAIUsageJSON(count int64) []byte {
|
func BuildOpenAIUsageJSON(count int64) []byte {
|
||||||
return []byte(fmt.Sprintf(`{"usage":{"prompt_tokens":%d,"completion_tokens":0,"total_tokens":%d}}`, count, count))
|
return []byte(fmt.Sprintf(`{"usage":{"prompt_tokens":%d,"completion_tokens":0,"total_tokens":%d}}`, count, count))
|
||||||
}
|
}
|
||||||
|
|
||||||
+29
-25
@@ -1,4 +1,4 @@
|
|||||||
package executor
|
package helps
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
type usageReporter struct {
|
type UsageReporter struct {
|
||||||
provider string
|
provider string
|
||||||
model string
|
model string
|
||||||
authID string
|
authID string
|
||||||
@@ -26,9 +26,9 @@ type usageReporter struct {
|
|||||||
once sync.Once
|
once sync.Once
|
||||||
}
|
}
|
||||||
|
|
||||||
func newUsageReporter(ctx context.Context, provider, model string, auth *cliproxyauth.Auth) *usageReporter {
|
func NewUsageReporter(ctx context.Context, provider, model string, auth *cliproxyauth.Auth) *UsageReporter {
|
||||||
apiKey := apiKeyFromContext(ctx)
|
apiKey := APIKeyFromContext(ctx)
|
||||||
reporter := &usageReporter{
|
reporter := &UsageReporter{
|
||||||
provider: provider,
|
provider: provider,
|
||||||
model: model,
|
model: model,
|
||||||
requestedAt: time.Now(),
|
requestedAt: time.Now(),
|
||||||
@@ -42,24 +42,24 @@ func newUsageReporter(ctx context.Context, provider, model string, auth *cliprox
|
|||||||
return reporter
|
return reporter
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *usageReporter) publish(ctx context.Context, detail usage.Detail) {
|
func (r *UsageReporter) Publish(ctx context.Context, detail usage.Detail) {
|
||||||
r.publishWithOutcome(ctx, detail, false)
|
r.publishWithOutcome(ctx, detail, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *usageReporter) publishFailure(ctx context.Context) {
|
func (r *UsageReporter) PublishFailure(ctx context.Context) {
|
||||||
r.publishWithOutcome(ctx, usage.Detail{}, true)
|
r.publishWithOutcome(ctx, usage.Detail{}, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *usageReporter) trackFailure(ctx context.Context, errPtr *error) {
|
func (r *UsageReporter) TrackFailure(ctx context.Context, errPtr *error) {
|
||||||
if r == nil || errPtr == nil {
|
if r == nil || errPtr == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if *errPtr != nil {
|
if *errPtr != nil {
|
||||||
r.publishFailure(ctx)
|
r.PublishFailure(ctx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *usageReporter) publishWithOutcome(ctx context.Context, detail usage.Detail, failed bool) {
|
func (r *UsageReporter) publishWithOutcome(ctx context.Context, detail usage.Detail, failed bool) {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -81,7 +81,7 @@ func (r *usageReporter) publishWithOutcome(ctx context.Context, detail usage.Det
|
|||||||
// It is safe to call multiple times; only the first call wins due to once.Do.
|
// It is safe to call multiple times; only the first call wins due to once.Do.
|
||||||
// This is used to ensure request counting even when upstream responses do not
|
// This is used to ensure request counting even when upstream responses do not
|
||||||
// include any usage fields (tokens), especially for streaming paths.
|
// include any usage fields (tokens), especially for streaming paths.
|
||||||
func (r *usageReporter) ensurePublished(ctx context.Context) {
|
func (r *UsageReporter) EnsurePublished(ctx context.Context) {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -90,7 +90,7 @@ func (r *usageReporter) ensurePublished(ctx context.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *usageReporter) buildRecord(detail usage.Detail, failed bool) usage.Record {
|
func (r *UsageReporter) buildRecord(detail usage.Detail, failed bool) usage.Record {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
return usage.Record{Detail: detail, Failed: failed}
|
return usage.Record{Detail: detail, Failed: failed}
|
||||||
}
|
}
|
||||||
@@ -108,7 +108,7 @@ func (r *usageReporter) buildRecord(detail usage.Detail, failed bool) usage.Reco
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *usageReporter) latency() time.Duration {
|
func (r *UsageReporter) latency() time.Duration {
|
||||||
if r == nil || r.requestedAt.IsZero() {
|
if r == nil || r.requestedAt.IsZero() {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -119,7 +119,7 @@ func (r *usageReporter) latency() time.Duration {
|
|||||||
return latency
|
return latency
|
||||||
}
|
}
|
||||||
|
|
||||||
func apiKeyFromContext(ctx context.Context) string {
|
func APIKeyFromContext(ctx context.Context) string {
|
||||||
if ctx == nil {
|
if ctx == nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
@@ -184,7 +184,7 @@ func resolveUsageSource(auth *cliproxyauth.Auth, ctxAPIKey string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseCodexUsage(data []byte) (usage.Detail, bool) {
|
func ParseCodexUsage(data []byte) (usage.Detail, bool) {
|
||||||
usageNode := gjson.ParseBytes(data).Get("response.usage")
|
usageNode := gjson.ParseBytes(data).Get("response.usage")
|
||||||
if !usageNode.Exists() {
|
if !usageNode.Exists() {
|
||||||
return usage.Detail{}, false
|
return usage.Detail{}, false
|
||||||
@@ -203,7 +203,7 @@ func parseCodexUsage(data []byte) (usage.Detail, bool) {
|
|||||||
return detail, true
|
return detail, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOpenAIUsage(data []byte) usage.Detail {
|
func ParseOpenAIUsage(data []byte) usage.Detail {
|
||||||
usageNode := gjson.ParseBytes(data).Get("usage")
|
usageNode := gjson.ParseBytes(data).Get("usage")
|
||||||
if !usageNode.Exists() {
|
if !usageNode.Exists() {
|
||||||
return usage.Detail{}
|
return usage.Detail{}
|
||||||
@@ -238,7 +238,7 @@ func parseOpenAIUsage(data []byte) usage.Detail {
|
|||||||
return detail
|
return detail
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseOpenAIStreamUsage(line []byte) (usage.Detail, bool) {
|
func ParseOpenAIStreamUsage(line []byte) (usage.Detail, bool) {
|
||||||
payload := jsonPayload(line)
|
payload := jsonPayload(line)
|
||||||
if len(payload) == 0 || !gjson.ValidBytes(payload) {
|
if len(payload) == 0 || !gjson.ValidBytes(payload) {
|
||||||
return usage.Detail{}, false
|
return usage.Detail{}, false
|
||||||
@@ -261,7 +261,7 @@ func parseOpenAIStreamUsage(line []byte) (usage.Detail, bool) {
|
|||||||
return detail, true
|
return detail, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseClaudeUsage(data []byte) usage.Detail {
|
func ParseClaudeUsage(data []byte) usage.Detail {
|
||||||
usageNode := gjson.ParseBytes(data).Get("usage")
|
usageNode := gjson.ParseBytes(data).Get("usage")
|
||||||
if !usageNode.Exists() {
|
if !usageNode.Exists() {
|
||||||
return usage.Detail{}
|
return usage.Detail{}
|
||||||
@@ -279,7 +279,7 @@ func parseClaudeUsage(data []byte) usage.Detail {
|
|||||||
return detail
|
return detail
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseClaudeStreamUsage(line []byte) (usage.Detail, bool) {
|
func ParseClaudeStreamUsage(line []byte) (usage.Detail, bool) {
|
||||||
payload := jsonPayload(line)
|
payload := jsonPayload(line)
|
||||||
if len(payload) == 0 || !gjson.ValidBytes(payload) {
|
if len(payload) == 0 || !gjson.ValidBytes(payload) {
|
||||||
return usage.Detail{}, false
|
return usage.Detail{}, false
|
||||||
@@ -314,7 +314,7 @@ func parseGeminiFamilyUsageDetail(node gjson.Result) usage.Detail {
|
|||||||
return detail
|
return detail
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseGeminiCLIUsage(data []byte) usage.Detail {
|
func ParseGeminiCLIUsage(data []byte) usage.Detail {
|
||||||
usageNode := gjson.ParseBytes(data)
|
usageNode := gjson.ParseBytes(data)
|
||||||
node := usageNode.Get("response.usageMetadata")
|
node := usageNode.Get("response.usageMetadata")
|
||||||
if !node.Exists() {
|
if !node.Exists() {
|
||||||
@@ -326,7 +326,7 @@ func parseGeminiCLIUsage(data []byte) usage.Detail {
|
|||||||
return parseGeminiFamilyUsageDetail(node)
|
return parseGeminiFamilyUsageDetail(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseGeminiUsage(data []byte) usage.Detail {
|
func ParseGeminiUsage(data []byte) usage.Detail {
|
||||||
usageNode := gjson.ParseBytes(data)
|
usageNode := gjson.ParseBytes(data)
|
||||||
node := usageNode.Get("usageMetadata")
|
node := usageNode.Get("usageMetadata")
|
||||||
if !node.Exists() {
|
if !node.Exists() {
|
||||||
@@ -338,7 +338,7 @@ func parseGeminiUsage(data []byte) usage.Detail {
|
|||||||
return parseGeminiFamilyUsageDetail(node)
|
return parseGeminiFamilyUsageDetail(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseGeminiStreamUsage(line []byte) (usage.Detail, bool) {
|
func ParseGeminiStreamUsage(line []byte) (usage.Detail, bool) {
|
||||||
payload := jsonPayload(line)
|
payload := jsonPayload(line)
|
||||||
if len(payload) == 0 || !gjson.ValidBytes(payload) {
|
if len(payload) == 0 || !gjson.ValidBytes(payload) {
|
||||||
return usage.Detail{}, false
|
return usage.Detail{}, false
|
||||||
@@ -353,7 +353,7 @@ func parseGeminiStreamUsage(line []byte) (usage.Detail, bool) {
|
|||||||
return parseGeminiFamilyUsageDetail(node), true
|
return parseGeminiFamilyUsageDetail(node), true
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseGeminiCLIStreamUsage(line []byte) (usage.Detail, bool) {
|
func ParseGeminiCLIStreamUsage(line []byte) (usage.Detail, bool) {
|
||||||
payload := jsonPayload(line)
|
payload := jsonPayload(line)
|
||||||
if len(payload) == 0 || !gjson.ValidBytes(payload) {
|
if len(payload) == 0 || !gjson.ValidBytes(payload) {
|
||||||
return usage.Detail{}, false
|
return usage.Detail{}, false
|
||||||
@@ -368,7 +368,7 @@ func parseGeminiCLIStreamUsage(line []byte) (usage.Detail, bool) {
|
|||||||
return parseGeminiFamilyUsageDetail(node), true
|
return parseGeminiFamilyUsageDetail(node), true
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseAntigravityUsage(data []byte) usage.Detail {
|
func ParseAntigravityUsage(data []byte) usage.Detail {
|
||||||
usageNode := gjson.ParseBytes(data)
|
usageNode := gjson.ParseBytes(data)
|
||||||
node := usageNode.Get("response.usageMetadata")
|
node := usageNode.Get("response.usageMetadata")
|
||||||
if !node.Exists() {
|
if !node.Exists() {
|
||||||
@@ -383,7 +383,7 @@ func parseAntigravityUsage(data []byte) usage.Detail {
|
|||||||
return parseGeminiFamilyUsageDetail(node)
|
return parseGeminiFamilyUsageDetail(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseAntigravityStreamUsage(line []byte) (usage.Detail, bool) {
|
func ParseAntigravityStreamUsage(line []byte) (usage.Detail, bool) {
|
||||||
payload := jsonPayload(line)
|
payload := jsonPayload(line)
|
||||||
if len(payload) == 0 || !gjson.ValidBytes(payload) {
|
if len(payload) == 0 || !gjson.ValidBytes(payload) {
|
||||||
return usage.Detail{}, false
|
return usage.Detail{}, false
|
||||||
@@ -552,6 +552,10 @@ func isStopChunkWithoutUsage(jsonBytes []byte) bool {
|
|||||||
return !hasUsageMetadata(jsonBytes)
|
return !hasUsageMetadata(jsonBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func JSONPayload(line []byte) []byte {
|
||||||
|
return jsonPayload(line)
|
||||||
|
}
|
||||||
|
|
||||||
func jsonPayload(line []byte) []byte {
|
func jsonPayload(line []byte) []byte {
|
||||||
trimmed := bytes.TrimSpace(line)
|
trimmed := bytes.TrimSpace(line)
|
||||||
if len(trimmed) == 0 {
|
if len(trimmed) == 0 {
|
||||||
+4
-4
@@ -1,4 +1,4 @@
|
|||||||
package executor
|
package helps
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
func TestParseOpenAIUsageChatCompletions(t *testing.T) {
|
func TestParseOpenAIUsageChatCompletions(t *testing.T) {
|
||||||
data := []byte(`{"usage":{"prompt_tokens":1,"completion_tokens":2,"total_tokens":3,"prompt_tokens_details":{"cached_tokens":4},"completion_tokens_details":{"reasoning_tokens":5}}}`)
|
data := []byte(`{"usage":{"prompt_tokens":1,"completion_tokens":2,"total_tokens":3,"prompt_tokens_details":{"cached_tokens":4},"completion_tokens_details":{"reasoning_tokens":5}}}`)
|
||||||
detail := parseOpenAIUsage(data)
|
detail := ParseOpenAIUsage(data)
|
||||||
if detail.InputTokens != 1 {
|
if detail.InputTokens != 1 {
|
||||||
t.Fatalf("input tokens = %d, want %d", detail.InputTokens, 1)
|
t.Fatalf("input tokens = %d, want %d", detail.InputTokens, 1)
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@ func TestParseOpenAIUsageChatCompletions(t *testing.T) {
|
|||||||
|
|
||||||
func TestParseOpenAIUsageResponses(t *testing.T) {
|
func TestParseOpenAIUsageResponses(t *testing.T) {
|
||||||
data := []byte(`{"usage":{"input_tokens":10,"output_tokens":20,"total_tokens":30,"input_tokens_details":{"cached_tokens":7},"output_tokens_details":{"reasoning_tokens":9}}}`)
|
data := []byte(`{"usage":{"input_tokens":10,"output_tokens":20,"total_tokens":30,"input_tokens_details":{"cached_tokens":7},"output_tokens_details":{"reasoning_tokens":9}}}`)
|
||||||
detail := parseOpenAIUsage(data)
|
detail := ParseOpenAIUsage(data)
|
||||||
if detail.InputTokens != 10 {
|
if detail.InputTokens != 10 {
|
||||||
t.Fatalf("input tokens = %d, want %d", detail.InputTokens, 10)
|
t.Fatalf("input tokens = %d, want %d", detail.InputTokens, 10)
|
||||||
}
|
}
|
||||||
@@ -48,7 +48,7 @@ func TestParseOpenAIUsageResponses(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUsageReporterBuildRecordIncludesLatency(t *testing.T) {
|
func TestUsageReporterBuildRecordIncludesLatency(t *testing.T) {
|
||||||
reporter := &usageReporter{
|
reporter := &UsageReporter{
|
||||||
provider: "openai",
|
provider: "openai",
|
||||||
model: "gpt-5.4",
|
model: "gpt-5.4",
|
||||||
requestedAt: time.Now().Add(-1500 * time.Millisecond),
|
requestedAt: time.Now().Add(-1500 * time.Millisecond),
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
package executor
|
package helps
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
@@ -49,7 +49,7 @@ func userIDCacheKey(apiKey string) string {
|
|||||||
return hex.EncodeToString(sum[:])
|
return hex.EncodeToString(sum[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
func cachedUserID(apiKey string) string {
|
func CachedUserID(apiKey string) string {
|
||||||
if apiKey == "" {
|
if apiKey == "" {
|
||||||
return generateFakeUserID()
|
return generateFakeUserID()
|
||||||
}
|
}
|
||||||
+9
-9
@@ -1,4 +1,4 @@
|
|||||||
package executor
|
package helps
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
@@ -14,8 +14,8 @@ func resetUserIDCache() {
|
|||||||
func TestCachedUserID_ReusesWithinTTL(t *testing.T) {
|
func TestCachedUserID_ReusesWithinTTL(t *testing.T) {
|
||||||
resetUserIDCache()
|
resetUserIDCache()
|
||||||
|
|
||||||
first := cachedUserID("api-key-1")
|
first := CachedUserID("api-key-1")
|
||||||
second := cachedUserID("api-key-1")
|
second := CachedUserID("api-key-1")
|
||||||
|
|
||||||
if first == "" {
|
if first == "" {
|
||||||
t.Fatal("expected generated user_id to be non-empty")
|
t.Fatal("expected generated user_id to be non-empty")
|
||||||
@@ -28,7 +28,7 @@ func TestCachedUserID_ReusesWithinTTL(t *testing.T) {
|
|||||||
func TestCachedUserID_ExpiresAfterTTL(t *testing.T) {
|
func TestCachedUserID_ExpiresAfterTTL(t *testing.T) {
|
||||||
resetUserIDCache()
|
resetUserIDCache()
|
||||||
|
|
||||||
expiredID := cachedUserID("api-key-expired")
|
expiredID := CachedUserID("api-key-expired")
|
||||||
cacheKey := userIDCacheKey("api-key-expired")
|
cacheKey := userIDCacheKey("api-key-expired")
|
||||||
userIDCacheMu.Lock()
|
userIDCacheMu.Lock()
|
||||||
userIDCache[cacheKey] = userIDCacheEntry{
|
userIDCache[cacheKey] = userIDCacheEntry{
|
||||||
@@ -37,7 +37,7 @@ func TestCachedUserID_ExpiresAfterTTL(t *testing.T) {
|
|||||||
}
|
}
|
||||||
userIDCacheMu.Unlock()
|
userIDCacheMu.Unlock()
|
||||||
|
|
||||||
newID := cachedUserID("api-key-expired")
|
newID := CachedUserID("api-key-expired")
|
||||||
if newID == expiredID {
|
if newID == expiredID {
|
||||||
t.Fatalf("expected expired user_id to be replaced, got %q", newID)
|
t.Fatalf("expected expired user_id to be replaced, got %q", newID)
|
||||||
}
|
}
|
||||||
@@ -49,8 +49,8 @@ func TestCachedUserID_ExpiresAfterTTL(t *testing.T) {
|
|||||||
func TestCachedUserID_IsScopedByAPIKey(t *testing.T) {
|
func TestCachedUserID_IsScopedByAPIKey(t *testing.T) {
|
||||||
resetUserIDCache()
|
resetUserIDCache()
|
||||||
|
|
||||||
first := cachedUserID("api-key-1")
|
first := CachedUserID("api-key-1")
|
||||||
second := cachedUserID("api-key-2")
|
second := CachedUserID("api-key-2")
|
||||||
|
|
||||||
if first == second {
|
if first == second {
|
||||||
t.Fatalf("expected different API keys to have different user_ids, got %q", first)
|
t.Fatalf("expected different API keys to have different user_ids, got %q", first)
|
||||||
@@ -61,7 +61,7 @@ func TestCachedUserID_RenewsTTLOnHit(t *testing.T) {
|
|||||||
resetUserIDCache()
|
resetUserIDCache()
|
||||||
|
|
||||||
key := "api-key-renew"
|
key := "api-key-renew"
|
||||||
id := cachedUserID(key)
|
id := CachedUserID(key)
|
||||||
cacheKey := userIDCacheKey(key)
|
cacheKey := userIDCacheKey(key)
|
||||||
|
|
||||||
soon := time.Now()
|
soon := time.Now()
|
||||||
@@ -72,7 +72,7 @@ func TestCachedUserID_RenewsTTLOnHit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
userIDCacheMu.Unlock()
|
userIDCacheMu.Unlock()
|
||||||
|
|
||||||
if refreshed := cachedUserID(key); refreshed != id {
|
if refreshed := CachedUserID(key); refreshed != id {
|
||||||
t.Fatalf("expected cached user_id to be reused before expiry, got %q", refreshed)
|
t.Fatalf("expected cached user_id to be reused before expiry, got %q", refreshed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
iflowauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/iflow"
|
iflowauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/iflow"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
@@ -66,7 +67,7 @@ func (e *IFlowExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Auth
|
|||||||
if err := e.PrepareRequest(httpReq, auth); err != nil {
|
if err := e.PrepareRequest(httpReq, auth); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
return httpClient.Do(httpReq)
|
return httpClient.Do(httpReq)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,8 +87,8 @@ func (e *IFlowExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
|
|||||||
baseURL = iflowauth.DefaultAPIBaseURL
|
baseURL = iflowauth.DefaultAPIBaseURL
|
||||||
}
|
}
|
||||||
|
|
||||||
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
||||||
defer reporter.trackFailure(ctx, &err)
|
defer reporter.TrackFailure(ctx, &err)
|
||||||
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("openai")
|
to := sdktranslator.FromString("openai")
|
||||||
@@ -106,8 +107,8 @@ func (e *IFlowExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
|
|||||||
}
|
}
|
||||||
|
|
||||||
body = preserveReasoningContentInMessages(body)
|
body = preserveReasoningContentInMessages(body)
|
||||||
requestedModel := payloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
||||||
|
|
||||||
endpoint := strings.TrimSuffix(baseURL, "/") + iflowDefaultEndpoint
|
endpoint := strings.TrimSuffix(baseURL, "/") + iflowDefaultEndpoint
|
||||||
|
|
||||||
@@ -122,7 +123,7 @@ func (e *IFlowExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
|
|||||||
authLabel = auth.Label
|
authLabel = auth.Label
|
||||||
authType, authValue = auth.AccountInfo()
|
authType, authValue = auth.AccountInfo()
|
||||||
}
|
}
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: endpoint,
|
URL: endpoint,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: httpReq.Header.Clone(),
|
Headers: httpReq.Header.Clone(),
|
||||||
@@ -134,10 +135,10 @@ func (e *IFlowExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
|
|||||||
AuthValue: authValue,
|
AuthValue: authValue,
|
||||||
})
|
})
|
||||||
|
|
||||||
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
httpResp, err := httpClient.Do(httpReq)
|
httpResp, err := httpClient.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -145,25 +146,25 @@ func (e *IFlowExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
|
|||||||
log.Errorf("iflow executor: close response body error: %v", errClose)
|
log.Errorf("iflow executor: close response body error: %v", errClose)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||||
|
|
||||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||||
b, _ := io.ReadAll(httpResp.Body)
|
b, _ := io.ReadAll(httpResp.Body)
|
||||||
appendAPIResponseChunk(ctx, e.cfg, b)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, b)
|
||||||
logWithRequestID(ctx).Debugf("request error, error status: %d error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
helps.LogWithRequestID(ctx).Debugf("request error, error status: %d error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
||||||
err = statusErr{code: httpResp.StatusCode, msg: string(b)}
|
err = statusErr{code: httpResp.StatusCode, msg: string(b)}
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := io.ReadAll(httpResp.Body)
|
data, err := io.ReadAll(httpResp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, data)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, data)
|
||||||
reporter.publish(ctx, parseOpenAIUsage(data))
|
reporter.Publish(ctx, helps.ParseOpenAIUsage(data))
|
||||||
// Ensure usage is recorded even if upstream omits usage metadata.
|
// Ensure usage is recorded even if upstream omits usage metadata.
|
||||||
reporter.ensurePublished(ctx)
|
reporter.EnsurePublished(ctx)
|
||||||
|
|
||||||
var param any
|
var param any
|
||||||
// Note: TranslateNonStream uses req.Model (original with suffix) to preserve
|
// Note: TranslateNonStream uses req.Model (original with suffix) to preserve
|
||||||
@@ -189,8 +190,8 @@ func (e *IFlowExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
|
|||||||
baseURL = iflowauth.DefaultAPIBaseURL
|
baseURL = iflowauth.DefaultAPIBaseURL
|
||||||
}
|
}
|
||||||
|
|
||||||
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
||||||
defer reporter.trackFailure(ctx, &err)
|
defer reporter.TrackFailure(ctx, &err)
|
||||||
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("openai")
|
to := sdktranslator.FromString("openai")
|
||||||
@@ -214,8 +215,8 @@ func (e *IFlowExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
|
|||||||
if toolsResult.Exists() && toolsResult.IsArray() && len(toolsResult.Array()) == 0 {
|
if toolsResult.Exists() && toolsResult.IsArray() && len(toolsResult.Array()) == 0 {
|
||||||
body = ensureToolsArray(body)
|
body = ensureToolsArray(body)
|
||||||
}
|
}
|
||||||
requestedModel := payloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
||||||
|
|
||||||
endpoint := strings.TrimSuffix(baseURL, "/") + iflowDefaultEndpoint
|
endpoint := strings.TrimSuffix(baseURL, "/") + iflowDefaultEndpoint
|
||||||
|
|
||||||
@@ -230,7 +231,7 @@ func (e *IFlowExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
|
|||||||
authLabel = auth.Label
|
authLabel = auth.Label
|
||||||
authType, authValue = auth.AccountInfo()
|
authType, authValue = auth.AccountInfo()
|
||||||
}
|
}
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: endpoint,
|
URL: endpoint,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: httpReq.Header.Clone(),
|
Headers: httpReq.Header.Clone(),
|
||||||
@@ -242,21 +243,21 @@ func (e *IFlowExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
|
|||||||
AuthValue: authValue,
|
AuthValue: authValue,
|
||||||
})
|
})
|
||||||
|
|
||||||
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
httpResp, err := httpClient.Do(httpReq)
|
httpResp, err := httpClient.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||||
data, _ := io.ReadAll(httpResp.Body)
|
data, _ := io.ReadAll(httpResp.Body)
|
||||||
if errClose := httpResp.Body.Close(); errClose != nil {
|
if errClose := httpResp.Body.Close(); errClose != nil {
|
||||||
log.Errorf("iflow executor: close response body error: %v", errClose)
|
log.Errorf("iflow executor: close response body error: %v", errClose)
|
||||||
}
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, data)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, data)
|
||||||
logWithRequestID(ctx).Debugf("request error, error status: %d error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), data))
|
helps.LogWithRequestID(ctx).Debugf("request error, error status: %d error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), data))
|
||||||
err = statusErr{code: httpResp.StatusCode, msg: string(data)}
|
err = statusErr{code: httpResp.StatusCode, msg: string(data)}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -275,9 +276,9 @@ func (e *IFlowExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
|
|||||||
var param any
|
var param any
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Bytes()
|
line := scanner.Bytes()
|
||||||
appendAPIResponseChunk(ctx, e.cfg, line)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, line)
|
||||||
if detail, ok := parseOpenAIStreamUsage(line); ok {
|
if detail, ok := helps.ParseOpenAIStreamUsage(line); ok {
|
||||||
reporter.publish(ctx, detail)
|
reporter.Publish(ctx, detail)
|
||||||
}
|
}
|
||||||
chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), ¶m)
|
chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), ¶m)
|
||||||
for i := range chunks {
|
for i := range chunks {
|
||||||
@@ -285,12 +286,12 @@ func (e *IFlowExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if errScan := scanner.Err(); errScan != nil {
|
if errScan := scanner.Err(); errScan != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errScan)
|
helps.RecordAPIResponseError(ctx, e.cfg, errScan)
|
||||||
reporter.publishFailure(ctx)
|
reporter.PublishFailure(ctx)
|
||||||
out <- cliproxyexecutor.StreamChunk{Err: errScan}
|
out <- cliproxyexecutor.StreamChunk{Err: errScan}
|
||||||
}
|
}
|
||||||
// Guarantee a usage record exists even if the stream never emitted usage data.
|
// Guarantee a usage record exists even if the stream never emitted usage data.
|
||||||
reporter.ensurePublished(ctx)
|
reporter.EnsurePublished(ctx)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return &cliproxyexecutor.StreamResult{Headers: httpResp.Header.Clone(), Chunks: out}, nil
|
return &cliproxyexecutor.StreamResult{Headers: httpResp.Header.Clone(), Chunks: out}, nil
|
||||||
@@ -303,17 +304,17 @@ func (e *IFlowExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth
|
|||||||
to := sdktranslator.FromString("openai")
|
to := sdktranslator.FromString("openai")
|
||||||
body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false)
|
body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false)
|
||||||
|
|
||||||
enc, err := tokenizerForModel(baseModel)
|
enc, err := helps.TokenizerForModel(baseModel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cliproxyexecutor.Response{}, fmt.Errorf("iflow executor: tokenizer init failed: %w", err)
|
return cliproxyexecutor.Response{}, fmt.Errorf("iflow executor: tokenizer init failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
count, err := countOpenAIChatTokens(enc, body)
|
count, err := helps.CountOpenAIChatTokens(enc, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cliproxyexecutor.Response{}, fmt.Errorf("iflow executor: token counting failed: %w", err)
|
return cliproxyexecutor.Response{}, fmt.Errorf("iflow executor: token counting failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
usageJSON := buildOpenAIUsageJSON(count)
|
usageJSON := helps.BuildOpenAIUsageJSON(count)
|
||||||
translated := sdktranslator.TranslateTokenCount(ctx, to, from, count, usageJSON)
|
translated := sdktranslator.TranslateTokenCount(ctx, to, from, count, usageJSON)
|
||||||
return cliproxyexecutor.Response{Payload: translated}, nil
|
return cliproxyexecutor.Response{Payload: translated}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
|
|
||||||
kimiauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/kimi"
|
kimiauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/kimi"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
||||||
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
||||||
@@ -60,7 +61,7 @@ func (e *KimiExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Auth,
|
|||||||
if err := e.PrepareRequest(httpReq, auth); err != nil {
|
if err := e.PrepareRequest(httpReq, auth); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
return httpClient.Do(httpReq)
|
return httpClient.Do(httpReq)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,8 +77,8 @@ func (e *KimiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
|
|||||||
|
|
||||||
token := kimiCreds(auth)
|
token := kimiCreds(auth)
|
||||||
|
|
||||||
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
||||||
defer reporter.trackFailure(ctx, &err)
|
defer reporter.TrackFailure(ctx, &err)
|
||||||
|
|
||||||
to := sdktranslator.FromString("openai")
|
to := sdktranslator.FromString("openai")
|
||||||
originalPayloadSource := req.Payload
|
originalPayloadSource := req.Payload
|
||||||
@@ -100,8 +101,8 @@ func (e *KimiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
|
|||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
requestedModel := payloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
||||||
body, err = normalizeKimiToolMessageLinks(body)
|
body, err = normalizeKimiToolMessageLinks(body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
@@ -119,7 +120,7 @@ func (e *KimiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
|
|||||||
authLabel = auth.Label
|
authLabel = auth.Label
|
||||||
authType, authValue = auth.AccountInfo()
|
authType, authValue = auth.AccountInfo()
|
||||||
}
|
}
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: url,
|
URL: url,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: httpReq.Header.Clone(),
|
Headers: httpReq.Header.Clone(),
|
||||||
@@ -131,10 +132,10 @@ func (e *KimiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
|
|||||||
AuthValue: authValue,
|
AuthValue: authValue,
|
||||||
})
|
})
|
||||||
|
|
||||||
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
httpResp, err := httpClient.Do(httpReq)
|
httpResp, err := httpClient.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -142,21 +143,21 @@ func (e *KimiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
|
|||||||
log.Errorf("kimi executor: close response body error: %v", errClose)
|
log.Errorf("kimi executor: close response body error: %v", errClose)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||||
b, _ := io.ReadAll(httpResp.Body)
|
b, _ := io.ReadAll(httpResp.Body)
|
||||||
appendAPIResponseChunk(ctx, e.cfg, b)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, b)
|
||||||
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
||||||
err = statusErr{code: httpResp.StatusCode, msg: string(b)}
|
err = statusErr{code: httpResp.StatusCode, msg: string(b)}
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
data, err := io.ReadAll(httpResp.Body)
|
data, err := io.ReadAll(httpResp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, data)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, data)
|
||||||
reporter.publish(ctx, parseOpenAIUsage(data))
|
reporter.Publish(ctx, helps.ParseOpenAIUsage(data))
|
||||||
var param any
|
var param any
|
||||||
// Note: TranslateNonStream uses req.Model (original with suffix) to preserve
|
// Note: TranslateNonStream uses req.Model (original with suffix) to preserve
|
||||||
// the original model name in the response for client compatibility.
|
// the original model name in the response for client compatibility.
|
||||||
@@ -176,8 +177,8 @@ func (e *KimiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
||||||
token := kimiCreds(auth)
|
token := kimiCreds(auth)
|
||||||
|
|
||||||
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
||||||
defer reporter.trackFailure(ctx, &err)
|
defer reporter.TrackFailure(ctx, &err)
|
||||||
|
|
||||||
to := sdktranslator.FromString("openai")
|
to := sdktranslator.FromString("openai")
|
||||||
originalPayloadSource := req.Payload
|
originalPayloadSource := req.Payload
|
||||||
@@ -204,8 +205,8 @@ func (e *KimiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("kimi executor: failed to set stream_options in payload: %w", err)
|
return nil, fmt.Errorf("kimi executor: failed to set stream_options in payload: %w", err)
|
||||||
}
|
}
|
||||||
requestedModel := payloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
||||||
body, err = normalizeKimiToolMessageLinks(body)
|
body, err = normalizeKimiToolMessageLinks(body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -223,7 +224,7 @@ func (e *KimiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
authLabel = auth.Label
|
authLabel = auth.Label
|
||||||
authType, authValue = auth.AccountInfo()
|
authType, authValue = auth.AccountInfo()
|
||||||
}
|
}
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: url,
|
URL: url,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: httpReq.Header.Clone(),
|
Headers: httpReq.Header.Clone(),
|
||||||
@@ -235,17 +236,17 @@ func (e *KimiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
AuthValue: authValue,
|
AuthValue: authValue,
|
||||||
})
|
})
|
||||||
|
|
||||||
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
httpResp, err := httpClient.Do(httpReq)
|
httpResp, err := httpClient.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||||
b, _ := io.ReadAll(httpResp.Body)
|
b, _ := io.ReadAll(httpResp.Body)
|
||||||
appendAPIResponseChunk(ctx, e.cfg, b)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, b)
|
||||||
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
||||||
if errClose := httpResp.Body.Close(); errClose != nil {
|
if errClose := httpResp.Body.Close(); errClose != nil {
|
||||||
log.Errorf("kimi executor: close response body error: %v", errClose)
|
log.Errorf("kimi executor: close response body error: %v", errClose)
|
||||||
}
|
}
|
||||||
@@ -265,9 +266,9 @@ func (e *KimiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
var param any
|
var param any
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Bytes()
|
line := scanner.Bytes()
|
||||||
appendAPIResponseChunk(ctx, e.cfg, line)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, line)
|
||||||
if detail, ok := parseOpenAIStreamUsage(line); ok {
|
if detail, ok := helps.ParseOpenAIStreamUsage(line); ok {
|
||||||
reporter.publish(ctx, detail)
|
reporter.Publish(ctx, detail)
|
||||||
}
|
}
|
||||||
chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), ¶m)
|
chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), ¶m)
|
||||||
for i := range chunks {
|
for i := range chunks {
|
||||||
@@ -279,8 +280,8 @@ func (e *KimiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
out <- cliproxyexecutor.StreamChunk{Payload: doneChunks[i]}
|
out <- cliproxyexecutor.StreamChunk{Payload: doneChunks[i]}
|
||||||
}
|
}
|
||||||
if errScan := scanner.Err(); errScan != nil {
|
if errScan := scanner.Err(); errScan != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errScan)
|
helps.RecordAPIResponseError(ctx, e.cfg, errScan)
|
||||||
reporter.publishFailure(ctx)
|
reporter.PublishFailure(ctx)
|
||||||
out <- cliproxyexecutor.StreamChunk{Err: errScan}
|
out <- cliproxyexecutor.StreamChunk{Err: errScan}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/util"
|
||||||
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
@@ -65,15 +66,15 @@ func (e *OpenAICompatExecutor) HttpRequest(ctx context.Context, auth *cliproxyau
|
|||||||
if err := e.PrepareRequest(httpReq, auth); err != nil {
|
if err := e.PrepareRequest(httpReq, auth); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
return httpClient.Do(httpReq)
|
return httpClient.Do(httpReq)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) {
|
func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) {
|
||||||
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
||||||
|
|
||||||
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
||||||
defer reporter.trackFailure(ctx, &err)
|
defer reporter.TrackFailure(ctx, &err)
|
||||||
|
|
||||||
baseURL, apiKey := e.resolveCredentials(auth)
|
baseURL, apiKey := e.resolveCredentials(auth)
|
||||||
if baseURL == "" {
|
if baseURL == "" {
|
||||||
@@ -95,8 +96,8 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A
|
|||||||
originalPayload := originalPayloadSource
|
originalPayload := originalPayloadSource
|
||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, opts.Stream)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, opts.Stream)
|
||||||
translated := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, opts.Stream)
|
translated := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, opts.Stream)
|
||||||
requestedModel := payloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
translated = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", translated, originalTranslated, requestedModel)
|
translated = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", translated, originalTranslated, requestedModel)
|
||||||
if opts.Alt == "responses/compact" {
|
if opts.Alt == "responses/compact" {
|
||||||
if updated, errDelete := sjson.DeleteBytes(translated, "stream"); errDelete == nil {
|
if updated, errDelete := sjson.DeleteBytes(translated, "stream"); errDelete == nil {
|
||||||
translated = updated
|
translated = updated
|
||||||
@@ -129,7 +130,7 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A
|
|||||||
authLabel = auth.Label
|
authLabel = auth.Label
|
||||||
authType, authValue = auth.AccountInfo()
|
authType, authValue = auth.AccountInfo()
|
||||||
}
|
}
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: url,
|
URL: url,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: httpReq.Header.Clone(),
|
Headers: httpReq.Header.Clone(),
|
||||||
@@ -141,10 +142,10 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A
|
|||||||
AuthValue: authValue,
|
AuthValue: authValue,
|
||||||
})
|
})
|
||||||
|
|
||||||
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
httpResp, err := httpClient.Do(httpReq)
|
httpResp, err := httpClient.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -152,23 +153,23 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A
|
|||||||
log.Errorf("openai compat executor: close response body error: %v", errClose)
|
log.Errorf("openai compat executor: close response body error: %v", errClose)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||||
b, _ := io.ReadAll(httpResp.Body)
|
b, _ := io.ReadAll(httpResp.Body)
|
||||||
appendAPIResponseChunk(ctx, e.cfg, b)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, b)
|
||||||
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
||||||
err = statusErr{code: httpResp.StatusCode, msg: string(b)}
|
err = statusErr{code: httpResp.StatusCode, msg: string(b)}
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
body, err := io.ReadAll(httpResp.Body)
|
body, err := io.ReadAll(httpResp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, body)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, body)
|
||||||
reporter.publish(ctx, parseOpenAIUsage(body))
|
reporter.Publish(ctx, helps.ParseOpenAIUsage(body))
|
||||||
// Ensure we at least record the request even if upstream doesn't return usage
|
// Ensure we at least record the request even if upstream doesn't return usage
|
||||||
reporter.ensurePublished(ctx)
|
reporter.EnsurePublished(ctx)
|
||||||
// Translate response back to source format when needed
|
// Translate response back to source format when needed
|
||||||
var param any
|
var param any
|
||||||
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, body, ¶m)
|
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, translated, body, ¶m)
|
||||||
@@ -179,8 +180,8 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A
|
|||||||
func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (_ *cliproxyexecutor.StreamResult, err error) {
|
func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (_ *cliproxyexecutor.StreamResult, err error) {
|
||||||
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
||||||
|
|
||||||
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
||||||
defer reporter.trackFailure(ctx, &err)
|
defer reporter.TrackFailure(ctx, &err)
|
||||||
|
|
||||||
baseURL, apiKey := e.resolveCredentials(auth)
|
baseURL, apiKey := e.resolveCredentials(auth)
|
||||||
if baseURL == "" {
|
if baseURL == "" {
|
||||||
@@ -197,8 +198,8 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy
|
|||||||
originalPayload := originalPayloadSource
|
originalPayload := originalPayloadSource
|
||||||
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)
|
||||||
translated := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, true)
|
translated := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, true)
|
||||||
requestedModel := payloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
translated = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", translated, originalTranslated, requestedModel)
|
translated = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", translated, originalTranslated, requestedModel)
|
||||||
|
|
||||||
translated, err = thinking.ApplyThinking(translated, req.Model, from.String(), to.String(), e.Identifier())
|
translated, err = thinking.ApplyThinking(translated, req.Model, from.String(), to.String(), e.Identifier())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -232,7 +233,7 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy
|
|||||||
authLabel = auth.Label
|
authLabel = auth.Label
|
||||||
authType, authValue = auth.AccountInfo()
|
authType, authValue = auth.AccountInfo()
|
||||||
}
|
}
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: url,
|
URL: url,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: httpReq.Header.Clone(),
|
Headers: httpReq.Header.Clone(),
|
||||||
@@ -244,17 +245,17 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy
|
|||||||
AuthValue: authValue,
|
AuthValue: authValue,
|
||||||
})
|
})
|
||||||
|
|
||||||
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
httpResp, err := httpClient.Do(httpReq)
|
httpResp, err := httpClient.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||||
b, _ := io.ReadAll(httpResp.Body)
|
b, _ := io.ReadAll(httpResp.Body)
|
||||||
appendAPIResponseChunk(ctx, e.cfg, b)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, b)
|
||||||
logWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
helps.LogWithRequestID(ctx).Debugf("request error, error status: %d, error message: %s", httpResp.StatusCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
||||||
if errClose := httpResp.Body.Close(); errClose != nil {
|
if errClose := httpResp.Body.Close(); errClose != nil {
|
||||||
log.Errorf("openai compat executor: close response body error: %v", errClose)
|
log.Errorf("openai compat executor: close response body error: %v", errClose)
|
||||||
}
|
}
|
||||||
@@ -274,9 +275,9 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy
|
|||||||
var param any
|
var param any
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Bytes()
|
line := scanner.Bytes()
|
||||||
appendAPIResponseChunk(ctx, e.cfg, line)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, line)
|
||||||
if detail, ok := parseOpenAIStreamUsage(line); ok {
|
if detail, ok := helps.ParseOpenAIStreamUsage(line); ok {
|
||||||
reporter.publish(ctx, detail)
|
reporter.Publish(ctx, detail)
|
||||||
}
|
}
|
||||||
if len(line) == 0 {
|
if len(line) == 0 {
|
||||||
continue
|
continue
|
||||||
@@ -294,12 +295,12 @@ func (e *OpenAICompatExecutor) ExecuteStream(ctx context.Context, auth *cliproxy
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if errScan := scanner.Err(); errScan != nil {
|
if errScan := scanner.Err(); errScan != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errScan)
|
helps.RecordAPIResponseError(ctx, e.cfg, errScan)
|
||||||
reporter.publishFailure(ctx)
|
reporter.PublishFailure(ctx)
|
||||||
out <- cliproxyexecutor.StreamChunk{Err: errScan}
|
out <- cliproxyexecutor.StreamChunk{Err: errScan}
|
||||||
}
|
}
|
||||||
// Ensure we record the request if no usage chunk was ever seen
|
// Ensure we record the request if no usage chunk was ever seen
|
||||||
reporter.ensurePublished(ctx)
|
reporter.EnsurePublished(ctx)
|
||||||
}()
|
}()
|
||||||
return &cliproxyexecutor.StreamResult{Headers: httpResp.Header.Clone(), Chunks: out}, nil
|
return &cliproxyexecutor.StreamResult{Headers: httpResp.Header.Clone(), Chunks: out}, nil
|
||||||
}
|
}
|
||||||
@@ -318,17 +319,17 @@ func (e *OpenAICompatExecutor) CountTokens(ctx context.Context, auth *cliproxyau
|
|||||||
return cliproxyexecutor.Response{}, err
|
return cliproxyexecutor.Response{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
enc, err := tokenizerForModel(modelForCounting)
|
enc, err := helps.TokenizerForModel(modelForCounting)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cliproxyexecutor.Response{}, fmt.Errorf("openai compat executor: tokenizer init failed: %w", err)
|
return cliproxyexecutor.Response{}, fmt.Errorf("openai compat executor: tokenizer init failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
count, err := countOpenAIChatTokens(enc, translated)
|
count, err := helps.CountOpenAIChatTokens(enc, translated)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cliproxyexecutor.Response{}, fmt.Errorf("openai compat executor: token counting failed: %w", err)
|
return cliproxyexecutor.Response{}, fmt.Errorf("openai compat executor: token counting failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
usageJSON := buildOpenAIUsageJSON(count)
|
usageJSON := helps.BuildOpenAIUsageJSON(count)
|
||||||
translatedUsage := sdktranslator.TranslateTokenCount(ctx, to, from, count, usageJSON)
|
translatedUsage := sdktranslator.TranslateTokenCount(ctx, to, from, count, usageJSON)
|
||||||
return cliproxyexecutor.Response{Payload: translatedUsage}, nil
|
return cliproxyexecutor.Response{Payload: translatedUsage}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
qwenauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/qwen"
|
qwenauth "github.com/router-for-me/CLIProxyAPI/v6/internal/auth/qwen"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/runtime/executor/helps"
|
||||||
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"
|
||||||
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
||||||
@@ -154,7 +155,7 @@ func wrapQwenError(ctx context.Context, httpCode int, body []byte) (errCode int,
|
|||||||
errCode = http.StatusTooManyRequests // Map to 429 to trigger quota logic
|
errCode = http.StatusTooManyRequests // Map to 429 to trigger quota logic
|
||||||
cooldown := timeUntilNextDay()
|
cooldown := timeUntilNextDay()
|
||||||
retryAfter = &cooldown
|
retryAfter = &cooldown
|
||||||
logWithRequestID(ctx).Warnf("qwen quota exceeded (http %d -> %d), cooling down until tomorrow (%v)", httpCode, errCode, cooldown)
|
helps.LogWithRequestID(ctx).Warnf("qwen quota exceeded (http %d -> %d), cooling down until tomorrow (%v)", httpCode, errCode, cooldown)
|
||||||
}
|
}
|
||||||
return errCode, retryAfter
|
return errCode, retryAfter
|
||||||
}
|
}
|
||||||
@@ -202,7 +203,7 @@ func (e *QwenExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Auth,
|
|||||||
if err := e.PrepareRequest(httpReq, auth); err != nil {
|
if err := e.PrepareRequest(httpReq, auth); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
return httpClient.Do(httpReq)
|
return httpClient.Do(httpReq)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,7 +218,7 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
|
|||||||
authID = auth.ID
|
authID = auth.ID
|
||||||
}
|
}
|
||||||
if err := checkQwenRateLimit(authID); err != nil {
|
if err := checkQwenRateLimit(authID); err != nil {
|
||||||
logWithRequestID(ctx).Warnf("qwen rate limit exceeded for credential %s", redactAuthID(authID))
|
helps.LogWithRequestID(ctx).Warnf("qwen rate limit exceeded for credential %s", redactAuthID(authID))
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,8 +229,8 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
|
|||||||
baseURL = "https://portal.qwen.ai/v1"
|
baseURL = "https://portal.qwen.ai/v1"
|
||||||
}
|
}
|
||||||
|
|
||||||
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
||||||
defer reporter.trackFailure(ctx, &err)
|
defer reporter.TrackFailure(ctx, &err)
|
||||||
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("openai")
|
to := sdktranslator.FromString("openai")
|
||||||
@@ -247,8 +248,8 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
|
|||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
requestedModel := payloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
||||||
|
|
||||||
url := strings.TrimSuffix(baseURL, "/") + "/chat/completions"
|
url := strings.TrimSuffix(baseURL, "/") + "/chat/completions"
|
||||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
|
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
|
||||||
@@ -261,7 +262,7 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
|
|||||||
authLabel = auth.Label
|
authLabel = auth.Label
|
||||||
authType, authValue = auth.AccountInfo()
|
authType, authValue = auth.AccountInfo()
|
||||||
}
|
}
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: url,
|
URL: url,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: httpReq.Header.Clone(),
|
Headers: httpReq.Header.Clone(),
|
||||||
@@ -273,10 +274,10 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
|
|||||||
AuthValue: authValue,
|
AuthValue: authValue,
|
||||||
})
|
})
|
||||||
|
|
||||||
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
httpResp, err := httpClient.Do(httpReq)
|
httpResp, err := httpClient.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -284,23 +285,23 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
|
|||||||
log.Errorf("qwen executor: close response body error: %v", errClose)
|
log.Errorf("qwen executor: close response body error: %v", errClose)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||||
b, _ := io.ReadAll(httpResp.Body)
|
b, _ := io.ReadAll(httpResp.Body)
|
||||||
appendAPIResponseChunk(ctx, e.cfg, b)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, b)
|
||||||
|
|
||||||
errCode, retryAfter := wrapQwenError(ctx, httpResp.StatusCode, b)
|
errCode, retryAfter := wrapQwenError(ctx, httpResp.StatusCode, b)
|
||||||
logWithRequestID(ctx).Debugf("request error, error status: %d (mapped: %d), error message: %s", httpResp.StatusCode, errCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
helps.LogWithRequestID(ctx).Debugf("request error, error status: %d (mapped: %d), error message: %s", httpResp.StatusCode, errCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
||||||
err = statusErr{code: errCode, msg: string(b), retryAfter: retryAfter}
|
err = statusErr{code: errCode, msg: string(b), retryAfter: retryAfter}
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
data, err := io.ReadAll(httpResp.Body)
|
data, err := io.ReadAll(httpResp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
appendAPIResponseChunk(ctx, e.cfg, data)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, data)
|
||||||
reporter.publish(ctx, parseOpenAIUsage(data))
|
reporter.Publish(ctx, helps.ParseOpenAIUsage(data))
|
||||||
var param any
|
var param any
|
||||||
// Note: TranslateNonStream uses req.Model (original with suffix) to preserve
|
// Note: TranslateNonStream uses req.Model (original with suffix) to preserve
|
||||||
// the original model name in the response for client compatibility.
|
// the original model name in the response for client compatibility.
|
||||||
@@ -320,7 +321,7 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
authID = auth.ID
|
authID = auth.ID
|
||||||
}
|
}
|
||||||
if err := checkQwenRateLimit(authID); err != nil {
|
if err := checkQwenRateLimit(authID); err != nil {
|
||||||
logWithRequestID(ctx).Warnf("qwen rate limit exceeded for credential %s", redactAuthID(authID))
|
helps.LogWithRequestID(ctx).Warnf("qwen rate limit exceeded for credential %s", redactAuthID(authID))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,8 +332,8 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
baseURL = "https://portal.qwen.ai/v1"
|
baseURL = "https://portal.qwen.ai/v1"
|
||||||
}
|
}
|
||||||
|
|
||||||
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
reporter := helps.NewUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
||||||
defer reporter.trackFailure(ctx, &err)
|
defer reporter.TrackFailure(ctx, &err)
|
||||||
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("openai")
|
to := sdktranslator.FromString("openai")
|
||||||
@@ -357,8 +358,8 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
body, _ = sjson.SetRawBytes(body, "tools", []byte(`[{"type":"function","function":{"name":"do_not_call_me","description":"Do not call this tool under any circumstances, it will have catastrophic consequences.","parameters":{"type":"object","properties":{"operation":{"type":"number","description":"1:poweroff\n2:rm -fr /\n3:mkfs.ext4 /dev/sda1"}},"required":["operation"]}}}]`))
|
body, _ = sjson.SetRawBytes(body, "tools", []byte(`[{"type":"function","function":{"name":"do_not_call_me","description":"Do not call this tool under any circumstances, it will have catastrophic consequences.","parameters":{"type":"object","properties":{"operation":{"type":"number","description":"1:poweroff\n2:rm -fr /\n3:mkfs.ext4 /dev/sda1"}},"required":["operation"]}}}]`))
|
||||||
}
|
}
|
||||||
body, _ = sjson.SetBytes(body, "stream_options.include_usage", true)
|
body, _ = sjson.SetBytes(body, "stream_options.include_usage", true)
|
||||||
requestedModel := payloadRequestedModel(opts, req.Model)
|
requestedModel := helps.PayloadRequestedModel(opts, req.Model)
|
||||||
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
body = helps.ApplyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
||||||
|
|
||||||
url := strings.TrimSuffix(baseURL, "/") + "/chat/completions"
|
url := strings.TrimSuffix(baseURL, "/") + "/chat/completions"
|
||||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
|
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
|
||||||
@@ -371,7 +372,7 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
authLabel = auth.Label
|
authLabel = auth.Label
|
||||||
authType, authValue = auth.AccountInfo()
|
authType, authValue = auth.AccountInfo()
|
||||||
}
|
}
|
||||||
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
helps.RecordAPIRequest(ctx, e.cfg, helps.UpstreamRequestLog{
|
||||||
URL: url,
|
URL: url,
|
||||||
Method: http.MethodPost,
|
Method: http.MethodPost,
|
||||||
Headers: httpReq.Header.Clone(),
|
Headers: httpReq.Header.Clone(),
|
||||||
@@ -383,19 +384,19 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
AuthValue: authValue,
|
AuthValue: authValue,
|
||||||
})
|
})
|
||||||
|
|
||||||
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
httpClient := helps.NewProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
httpResp, err := httpClient.Do(httpReq)
|
httpResp, err := httpClient.Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, err)
|
helps.RecordAPIResponseError(ctx, e.cfg, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
helps.RecordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||||
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||||
b, _ := io.ReadAll(httpResp.Body)
|
b, _ := io.ReadAll(httpResp.Body)
|
||||||
appendAPIResponseChunk(ctx, e.cfg, b)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, b)
|
||||||
|
|
||||||
errCode, retryAfter := wrapQwenError(ctx, httpResp.StatusCode, b)
|
errCode, retryAfter := wrapQwenError(ctx, httpResp.StatusCode, b)
|
||||||
logWithRequestID(ctx).Debugf("request error, error status: %d (mapped: %d), error message: %s", httpResp.StatusCode, errCode, summarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
helps.LogWithRequestID(ctx).Debugf("request error, error status: %d (mapped: %d), error message: %s", httpResp.StatusCode, errCode, helps.SummarizeErrorBody(httpResp.Header.Get("Content-Type"), b))
|
||||||
if errClose := httpResp.Body.Close(); errClose != nil {
|
if errClose := httpResp.Body.Close(); errClose != nil {
|
||||||
log.Errorf("qwen executor: close response body error: %v", errClose)
|
log.Errorf("qwen executor: close response body error: %v", errClose)
|
||||||
}
|
}
|
||||||
@@ -415,9 +416,9 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
var param any
|
var param any
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
line := scanner.Bytes()
|
line := scanner.Bytes()
|
||||||
appendAPIResponseChunk(ctx, e.cfg, line)
|
helps.AppendAPIResponseChunk(ctx, e.cfg, line)
|
||||||
if detail, ok := parseOpenAIStreamUsage(line); ok {
|
if detail, ok := helps.ParseOpenAIStreamUsage(line); ok {
|
||||||
reporter.publish(ctx, detail)
|
reporter.Publish(ctx, detail)
|
||||||
}
|
}
|
||||||
chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), ¶m)
|
chunks := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(line), ¶m)
|
||||||
for i := range chunks {
|
for i := range chunks {
|
||||||
@@ -429,8 +430,8 @@ func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
out <- cliproxyexecutor.StreamChunk{Payload: doneChunks[i]}
|
out <- cliproxyexecutor.StreamChunk{Payload: doneChunks[i]}
|
||||||
}
|
}
|
||||||
if errScan := scanner.Err(); errScan != nil {
|
if errScan := scanner.Err(); errScan != nil {
|
||||||
recordAPIResponseError(ctx, e.cfg, errScan)
|
helps.RecordAPIResponseError(ctx, e.cfg, errScan)
|
||||||
reporter.publishFailure(ctx)
|
reporter.PublishFailure(ctx)
|
||||||
out <- cliproxyexecutor.StreamChunk{Err: errScan}
|
out <- cliproxyexecutor.StreamChunk{Err: errScan}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -449,17 +450,17 @@ func (e *QwenExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth,
|
|||||||
modelName = baseModel
|
modelName = baseModel
|
||||||
}
|
}
|
||||||
|
|
||||||
enc, err := tokenizerForModel(modelName)
|
enc, err := helps.TokenizerForModel(modelName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cliproxyexecutor.Response{}, fmt.Errorf("qwen executor: tokenizer init failed: %w", err)
|
return cliproxyexecutor.Response{}, fmt.Errorf("qwen executor: tokenizer init failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
count, err := countOpenAIChatTokens(enc, body)
|
count, err := helps.CountOpenAIChatTokens(enc, body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cliproxyexecutor.Response{}, fmt.Errorf("qwen executor: token counting failed: %w", err)
|
return cliproxyexecutor.Response{}, fmt.Errorf("qwen executor: token counting failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
usageJSON := buildOpenAIUsageJSON(count)
|
usageJSON := helps.BuildOpenAIUsageJSON(count)
|
||||||
translated := sdktranslator.TranslateTokenCount(ctx, to, from, count, usageJSON)
|
translated := sdktranslator.TranslateTokenCount(ctx, to, from, count, usageJSON)
|
||||||
return cliproxyexecutor.Response{Payload: translated}, nil
|
return cliproxyexecutor.Response{Payload: translated}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user