mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-02 20:40:52 +08:00
feat(openai): add responses/compact support
This commit is contained in:
@@ -325,6 +325,7 @@ func (s *Server) setupRoutes() {
|
|||||||
v1.POST("/messages", claudeCodeHandlers.ClaudeMessages)
|
v1.POST("/messages", claudeCodeHandlers.ClaudeMessages)
|
||||||
v1.POST("/messages/count_tokens", claudeCodeHandlers.ClaudeCountTokens)
|
v1.POST("/messages/count_tokens", claudeCodeHandlers.ClaudeCountTokens)
|
||||||
v1.POST("/responses", openaiResponsesHandlers.Responses)
|
v1.POST("/responses", openaiResponsesHandlers.Responses)
|
||||||
|
v1.POST("/responses/compact", openaiResponsesHandlers.Compact)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Gemini compatible API routes
|
// Gemini compatible API routes
|
||||||
|
|||||||
@@ -111,6 +111,9 @@ func (e *AIStudioExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.A
|
|||||||
|
|
||||||
// Execute performs a non-streaming request to the AI Studio API.
|
// Execute performs a non-streaming request to the AI Studio API.
|
||||||
func (e *AIStudioExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) {
|
func (e *AIStudioExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) {
|
||||||
|
if opts.Alt == "responses/compact" {
|
||||||
|
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 := newUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
||||||
defer reporter.trackFailure(ctx, &err)
|
defer reporter.trackFailure(ctx, &err)
|
||||||
@@ -167,6 +170,9 @@ func (e *AIStudioExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth,
|
|||||||
|
|
||||||
// ExecuteStream performs a streaming request to the AI Studio API.
|
// ExecuteStream performs a streaming request to the AI Studio API.
|
||||||
func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (stream <-chan cliproxyexecutor.StreamChunk, err error) {
|
func (e *AIStudioExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (stream <-chan cliproxyexecutor.StreamChunk, err error) {
|
||||||
|
if opts.Alt == "responses/compact" {
|
||||||
|
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 := newUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
||||||
defer reporter.trackFailure(ctx, &err)
|
defer reporter.trackFailure(ctx, &err)
|
||||||
|
|||||||
@@ -109,6 +109,9 @@ func (e *AntigravityExecutor) HttpRequest(ctx context.Context, auth *cliproxyaut
|
|||||||
|
|
||||||
// Execute performs a non-streaming request to the Antigravity API.
|
// Execute performs a non-streaming request to the Antigravity API.
|
||||||
func (e *AntigravityExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) {
|
func (e *AntigravityExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) {
|
||||||
|
if opts.Alt == "responses/compact" {
|
||||||
|
return resp, statusErr{code: http.StatusNotImplemented, msg: "/responses/compact not supported"}
|
||||||
|
}
|
||||||
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
||||||
isClaude := strings.Contains(strings.ToLower(baseModel), "claude")
|
isClaude := strings.Contains(strings.ToLower(baseModel), "claude")
|
||||||
|
|
||||||
@@ -641,6 +644,9 @@ func (e *AntigravityExecutor) convertStreamToNonStream(stream []byte) []byte {
|
|||||||
|
|
||||||
// ExecuteStream performs a streaming request to the Antigravity API.
|
// ExecuteStream performs a streaming request to the Antigravity API.
|
||||||
func (e *AntigravityExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (stream <-chan cliproxyexecutor.StreamChunk, err error) {
|
func (e *AntigravityExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (stream <-chan cliproxyexecutor.StreamChunk, err error) {
|
||||||
|
if opts.Alt == "responses/compact" {
|
||||||
|
return nil, statusErr{code: http.StatusNotImplemented, msg: "/responses/compact not supported"}
|
||||||
|
}
|
||||||
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
||||||
|
|
||||||
ctx = context.WithValue(ctx, "alt", "")
|
ctx = context.WithValue(ctx, "alt", "")
|
||||||
|
|||||||
@@ -84,6 +84,9 @@ func (e *ClaudeExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) {
|
func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) {
|
||||||
|
if opts.Alt == "responses/compact" {
|
||||||
|
return resp, statusErr{code: http.StatusNotImplemented, msg: "/responses/compact not supported"}
|
||||||
|
}
|
||||||
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
||||||
|
|
||||||
apiKey, baseURL := claudeCreds(auth)
|
apiKey, baseURL := claudeCreds(auth)
|
||||||
@@ -218,6 +221,9 @@ func (e *ClaudeExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (stream <-chan cliproxyexecutor.StreamChunk, err error) {
|
func (e *ClaudeExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (stream <-chan cliproxyexecutor.StreamChunk, err error) {
|
||||||
|
if opts.Alt == "responses/compact" {
|
||||||
|
return nil, statusErr{code: http.StatusNotImplemented, msg: "/responses/compact not supported"}
|
||||||
|
}
|
||||||
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
||||||
|
|
||||||
apiKey, baseURL := claudeCreds(auth)
|
apiKey, baseURL := claudeCreds(auth)
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ func (e *CodexExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Auth
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) {
|
func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) {
|
||||||
|
if opts.Alt == "responses/compact" {
|
||||||
|
return e.executeCompact(ctx, auth, req, opts)
|
||||||
|
}
|
||||||
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
||||||
|
|
||||||
apiKey, baseURL := codexCreds(auth)
|
apiKey, baseURL := codexCreds(auth)
|
||||||
@@ -117,7 +120,7 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
applyCodexHeaders(httpReq, auth, apiKey)
|
applyCodexHeaders(httpReq, auth, apiKey, true)
|
||||||
var authID, authLabel, authType, authValue string
|
var authID, authLabel, authType, authValue string
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
authID = auth.ID
|
authID = auth.ID
|
||||||
@@ -185,7 +188,96 @@ func (e *CodexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
|
|||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *CodexExecutor) executeCompact(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) {
|
||||||
|
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
||||||
|
|
||||||
|
apiKey, baseURL := codexCreds(auth)
|
||||||
|
if baseURL == "" {
|
||||||
|
baseURL = "https://chatgpt.com/backend-api/codex"
|
||||||
|
}
|
||||||
|
|
||||||
|
reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth)
|
||||||
|
defer reporter.trackFailure(ctx, &err)
|
||||||
|
|
||||||
|
from := opts.SourceFormat
|
||||||
|
to := sdktranslator.FromString("openai-response")
|
||||||
|
originalPayload := bytes.Clone(req.Payload)
|
||||||
|
if len(opts.OriginalRequest) > 0 {
|
||||||
|
originalPayload = bytes.Clone(opts.OriginalRequest)
|
||||||
|
}
|
||||||
|
originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)
|
||||||
|
body := sdktranslator.TranslateRequest(from, to, baseModel, bytes.Clone(req.Payload), false)
|
||||||
|
|
||||||
|
body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
requestedModel := payloadRequestedModel(opts, req.Model)
|
||||||
|
body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)
|
||||||
|
body, _ = sjson.SetBytes(body, "model", baseModel)
|
||||||
|
body, _ = sjson.SetBytes(body, "stream", false)
|
||||||
|
|
||||||
|
url := strings.TrimSuffix(baseURL, "/") + "/responses/compact"
|
||||||
|
httpReq, err := e.cacheHelper(ctx, from, url, req, body)
|
||||||
|
if err != nil {
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
applyCodexHeaders(httpReq, auth, apiKey, false)
|
||||||
|
var authID, authLabel, authType, authValue string
|
||||||
|
if auth != nil {
|
||||||
|
authID = auth.ID
|
||||||
|
authLabel = auth.Label
|
||||||
|
authType, authValue = auth.AccountInfo()
|
||||||
|
}
|
||||||
|
recordAPIRequest(ctx, e.cfg, upstreamRequestLog{
|
||||||
|
URL: url,
|
||||||
|
Method: http.MethodPost,
|
||||||
|
Headers: httpReq.Header.Clone(),
|
||||||
|
Body: body,
|
||||||
|
Provider: e.Identifier(),
|
||||||
|
AuthID: authID,
|
||||||
|
AuthLabel: authLabel,
|
||||||
|
AuthType: authType,
|
||||||
|
AuthValue: authValue,
|
||||||
|
})
|
||||||
|
httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)
|
||||||
|
httpResp, err := httpClient.Do(httpReq)
|
||||||
|
if err != nil {
|
||||||
|
recordAPIResponseError(ctx, e.cfg, err)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if errClose := httpResp.Body.Close(); errClose != nil {
|
||||||
|
log.Errorf("codex executor: close response body error: %v", errClose)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
recordAPIResponseMetadata(ctx, e.cfg, httpResp.StatusCode, httpResp.Header.Clone())
|
||||||
|
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||||
|
b, _ := io.ReadAll(httpResp.Body)
|
||||||
|
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))
|
||||||
|
err = statusErr{code: httpResp.StatusCode, msg: string(b)}
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
data, err := io.ReadAll(httpResp.Body)
|
||||||
|
if err != nil {
|
||||||
|
recordAPIResponseError(ctx, e.cfg, err)
|
||||||
|
return resp, err
|
||||||
|
}
|
||||||
|
appendAPIResponseChunk(ctx, e.cfg, data)
|
||||||
|
reporter.publish(ctx, parseOpenAIUsage(data))
|
||||||
|
reporter.ensurePublished(ctx)
|
||||||
|
var param any
|
||||||
|
out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, bytes.Clone(originalPayload), body, data, ¶m)
|
||||||
|
resp = cliproxyexecutor.Response{Payload: []byte(out)}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (stream <-chan cliproxyexecutor.StreamChunk, err error) {
|
func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (stream <-chan cliproxyexecutor.StreamChunk, err error) {
|
||||||
|
if opts.Alt == "responses/compact" {
|
||||||
|
return nil, statusErr{code: http.StatusBadRequest, msg: "streaming not supported for /responses/compact"}
|
||||||
|
}
|
||||||
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
||||||
|
|
||||||
apiKey, baseURL := codexCreds(auth)
|
apiKey, baseURL := codexCreds(auth)
|
||||||
@@ -229,7 +321,7 @@ func (e *CodexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Au
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
applyCodexHeaders(httpReq, auth, apiKey)
|
applyCodexHeaders(httpReq, auth, apiKey, true)
|
||||||
var authID, authLabel, authType, authValue string
|
var authID, authLabel, authType, authValue string
|
||||||
if auth != nil {
|
if auth != nil {
|
||||||
authID = auth.ID
|
authID = auth.ID
|
||||||
@@ -540,7 +632,7 @@ func (e *CodexExecutor) cacheHelper(ctx context.Context, from sdktranslator.Form
|
|||||||
return httpReq, nil
|
return httpReq, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string) {
|
func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, stream bool) {
|
||||||
r.Header.Set("Content-Type", "application/json")
|
r.Header.Set("Content-Type", "application/json")
|
||||||
r.Header.Set("Authorization", "Bearer "+token)
|
r.Header.Set("Authorization", "Bearer "+token)
|
||||||
|
|
||||||
@@ -554,7 +646,11 @@ func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string) {
|
|||||||
misc.EnsureHeader(r.Header, ginHeaders, "Session_id", uuid.NewString())
|
misc.EnsureHeader(r.Header, ginHeaders, "Session_id", uuid.NewString())
|
||||||
misc.EnsureHeader(r.Header, ginHeaders, "User-Agent", "codex_cli_rs/0.50.0 (Mac OS 26.0.1; arm64) Apple_Terminal/464")
|
misc.EnsureHeader(r.Header, ginHeaders, "User-Agent", "codex_cli_rs/0.50.0 (Mac OS 26.0.1; arm64) Apple_Terminal/464")
|
||||||
|
|
||||||
r.Header.Set("Accept", "text/event-stream")
|
if stream {
|
||||||
|
r.Header.Set("Accept", "text/event-stream")
|
||||||
|
} else {
|
||||||
|
r.Header.Set("Accept", "application/json")
|
||||||
|
}
|
||||||
r.Header.Set("Connection", "Keep-Alive")
|
r.Header.Set("Connection", "Keep-Alive")
|
||||||
|
|
||||||
isAPIKey := false
|
isAPIKey := false
|
||||||
|
|||||||
@@ -103,6 +103,9 @@ func (e *GeminiCLIExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.
|
|||||||
|
|
||||||
// Execute performs a non-streaming request to the Gemini CLI API.
|
// Execute performs a non-streaming request to the Gemini CLI API.
|
||||||
func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) {
|
func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) {
|
||||||
|
if opts.Alt == "responses/compact" {
|
||||||
|
return resp, statusErr{code: http.StatusNotImplemented, msg: "/responses/compact not supported"}
|
||||||
|
}
|
||||||
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
||||||
|
|
||||||
tokenSource, baseTokenData, err := prepareGeminiCLITokenSource(ctx, e.cfg, auth)
|
tokenSource, baseTokenData, err := prepareGeminiCLITokenSource(ctx, e.cfg, auth)
|
||||||
@@ -253,6 +256,9 @@ func (e *GeminiCLIExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth
|
|||||||
|
|
||||||
// ExecuteStream performs a streaming request to the Gemini CLI API.
|
// ExecuteStream performs a streaming request to the Gemini CLI API.
|
||||||
func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (stream <-chan cliproxyexecutor.StreamChunk, err error) {
|
func (e *GeminiCLIExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (stream <-chan cliproxyexecutor.StreamChunk, err error) {
|
||||||
|
if opts.Alt == "responses/compact" {
|
||||||
|
return nil, statusErr{code: http.StatusNotImplemented, msg: "/responses/compact not supported"}
|
||||||
|
}
|
||||||
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
||||||
|
|
||||||
tokenSource, baseTokenData, err := prepareGeminiCLITokenSource(ctx, e.cfg, auth)
|
tokenSource, baseTokenData, err := prepareGeminiCLITokenSource(ctx, e.cfg, auth)
|
||||||
|
|||||||
@@ -103,6 +103,9 @@ func (e *GeminiExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Aut
|
|||||||
// - cliproxyexecutor.Response: The response from the API
|
// - cliproxyexecutor.Response: The response from the API
|
||||||
// - error: An error if the request fails
|
// - error: An error if the request fails
|
||||||
func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) {
|
func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) {
|
||||||
|
if opts.Alt == "responses/compact" {
|
||||||
|
return resp, statusErr{code: http.StatusNotImplemented, msg: "/responses/compact not supported"}
|
||||||
|
}
|
||||||
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
||||||
|
|
||||||
apiKey, bearer := geminiCreds(auth)
|
apiKey, bearer := geminiCreds(auth)
|
||||||
@@ -207,6 +210,9 @@ func (e *GeminiExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, r
|
|||||||
|
|
||||||
// ExecuteStream performs a streaming request to the Gemini API.
|
// ExecuteStream performs a streaming request to the Gemini API.
|
||||||
func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (stream <-chan cliproxyexecutor.StreamChunk, err error) {
|
func (e *GeminiExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (stream <-chan cliproxyexecutor.StreamChunk, err error) {
|
||||||
|
if opts.Alt == "responses/compact" {
|
||||||
|
return nil, statusErr{code: http.StatusNotImplemented, msg: "/responses/compact not supported"}
|
||||||
|
}
|
||||||
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
||||||
|
|
||||||
apiKey, bearer := geminiCreds(auth)
|
apiKey, bearer := geminiCreds(auth)
|
||||||
|
|||||||
@@ -233,6 +233,9 @@ func (e *GeminiVertexExecutor) HttpRequest(ctx context.Context, auth *cliproxyau
|
|||||||
|
|
||||||
// Execute performs a non-streaming request to the Vertex AI API.
|
// Execute performs a non-streaming request to the Vertex AI API.
|
||||||
func (e *GeminiVertexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) {
|
func (e *GeminiVertexExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) {
|
||||||
|
if opts.Alt == "responses/compact" {
|
||||||
|
return resp, statusErr{code: http.StatusNotImplemented, msg: "/responses/compact not supported"}
|
||||||
|
}
|
||||||
// Try API key authentication first
|
// Try API key authentication first
|
||||||
apiKey, baseURL := vertexAPICreds(auth)
|
apiKey, baseURL := vertexAPICreds(auth)
|
||||||
|
|
||||||
@@ -251,6 +254,9 @@ func (e *GeminiVertexExecutor) Execute(ctx context.Context, auth *cliproxyauth.A
|
|||||||
|
|
||||||
// ExecuteStream performs a streaming request to the Vertex AI API.
|
// ExecuteStream performs a streaming request to the Vertex AI API.
|
||||||
func (e *GeminiVertexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (stream <-chan cliproxyexecutor.StreamChunk, err error) {
|
func (e *GeminiVertexExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (stream <-chan cliproxyexecutor.StreamChunk, err error) {
|
||||||
|
if opts.Alt == "responses/compact" {
|
||||||
|
return nil, statusErr{code: http.StatusNotImplemented, msg: "/responses/compact not supported"}
|
||||||
|
}
|
||||||
// Try API key authentication first
|
// Try API key authentication first
|
||||||
apiKey, baseURL := vertexAPICreds(auth)
|
apiKey, baseURL := vertexAPICreds(auth)
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ func (e *IFlowExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Auth
|
|||||||
|
|
||||||
// Execute performs a non-streaming chat completion request.
|
// Execute performs a non-streaming chat completion request.
|
||||||
func (e *IFlowExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) {
|
func (e *IFlowExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) {
|
||||||
|
if opts.Alt == "responses/compact" {
|
||||||
|
return resp, statusErr{code: http.StatusNotImplemented, msg: "/responses/compact not supported"}
|
||||||
|
}
|
||||||
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
||||||
|
|
||||||
apiKey, baseURL := iflowCreds(auth)
|
apiKey, baseURL := iflowCreds(auth)
|
||||||
@@ -167,6 +170,9 @@ func (e *IFlowExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, re
|
|||||||
|
|
||||||
// ExecuteStream performs a streaming chat completion request.
|
// ExecuteStream performs a streaming chat completion request.
|
||||||
func (e *IFlowExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (stream <-chan cliproxyexecutor.StreamChunk, err error) {
|
func (e *IFlowExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (stream <-chan cliproxyexecutor.StreamChunk, err error) {
|
||||||
|
if opts.Alt == "responses/compact" {
|
||||||
|
return nil, statusErr{code: http.StatusNotImplemented, msg: "/responses/compact not supported"}
|
||||||
|
}
|
||||||
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
||||||
|
|
||||||
apiKey, baseURL := iflowCreds(auth)
|
apiKey, baseURL := iflowCreds(auth)
|
||||||
|
|||||||
@@ -81,9 +81,13 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Translate inbound request to OpenAI format
|
|
||||||
from := opts.SourceFormat
|
from := opts.SourceFormat
|
||||||
to := sdktranslator.FromString("openai")
|
to := sdktranslator.FromString("openai")
|
||||||
|
endpoint := "/chat/completions"
|
||||||
|
if opts.Alt == "responses/compact" {
|
||||||
|
to = sdktranslator.FromString("openai-response")
|
||||||
|
endpoint = "/responses/compact"
|
||||||
|
}
|
||||||
originalPayload := bytes.Clone(req.Payload)
|
originalPayload := bytes.Clone(req.Payload)
|
||||||
if len(opts.OriginalRequest) > 0 {
|
if len(opts.OriginalRequest) > 0 {
|
||||||
originalPayload = bytes.Clone(opts.OriginalRequest)
|
originalPayload = bytes.Clone(opts.OriginalRequest)
|
||||||
@@ -98,7 +102,7 @@ func (e *OpenAICompatExecutor) Execute(ctx context.Context, auth *cliproxyauth.A
|
|||||||
return resp, err
|
return resp, err
|
||||||
}
|
}
|
||||||
|
|
||||||
url := strings.TrimSuffix(baseURL, "/") + "/chat/completions"
|
url := strings.TrimSuffix(baseURL, "/") + endpoint
|
||||||
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(translated))
|
httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(translated))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return resp, err
|
return resp, err
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package executor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/config"
|
||||||
|
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
|
cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
||||||
|
sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOpenAICompatExecutorCompactPassthrough(t *testing.T) {
|
||||||
|
var gotPath string
|
||||||
|
var gotBody []byte
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
gotPath = r.URL.Path
|
||||||
|
body, _ := io.ReadAll(r.Body)
|
||||||
|
gotBody = body
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_, _ = w.Write([]byte(`{"id":"resp_1","object":"response.compaction","usage":{"input_tokens":1,"output_tokens":2,"total_tokens":3}}`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
executor := NewOpenAICompatExecutor("openai-compatibility", &config.Config{})
|
||||||
|
auth := &cliproxyauth.Auth{Attributes: map[string]string{
|
||||||
|
"base_url": server.URL + "/v1",
|
||||||
|
"api_key": "test",
|
||||||
|
}}
|
||||||
|
payload := []byte(`{"model":"gpt-5.1-codex-max","input":[{"role":"user","content":"hi"}]}`)
|
||||||
|
resp, err := executor.Execute(context.Background(), auth, cliproxyexecutor.Request{
|
||||||
|
Model: "gpt-5.1-codex-max",
|
||||||
|
Payload: payload,
|
||||||
|
}, cliproxyexecutor.Options{
|
||||||
|
SourceFormat: sdktranslator.FromString("openai-response"),
|
||||||
|
Alt: "responses/compact",
|
||||||
|
Stream: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Execute error: %v", err)
|
||||||
|
}
|
||||||
|
if gotPath != "/v1/responses/compact" {
|
||||||
|
t.Fatalf("path = %q, want %q", gotPath, "/v1/responses/compact")
|
||||||
|
}
|
||||||
|
if !gjson.GetBytes(gotBody, "input").Exists() {
|
||||||
|
t.Fatalf("expected input in body")
|
||||||
|
}
|
||||||
|
if gjson.GetBytes(gotBody, "messages").Exists() {
|
||||||
|
t.Fatalf("unexpected messages in body")
|
||||||
|
}
|
||||||
|
if string(resp.Payload) != `{"id":"resp_1","object":"response.compaction","usage":{"input_tokens":1,"output_tokens":2,"total_tokens":3}}` {
|
||||||
|
t.Fatalf("payload = %s", string(resp.Payload))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -66,6 +66,9 @@ func (e *QwenExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Auth,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) {
|
func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) {
|
||||||
|
if opts.Alt == "responses/compact" {
|
||||||
|
return resp, statusErr{code: http.StatusNotImplemented, msg: "/responses/compact not supported"}
|
||||||
|
}
|
||||||
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
||||||
|
|
||||||
token, baseURL := qwenCreds(auth)
|
token, baseURL := qwenCreds(auth)
|
||||||
@@ -153,6 +156,9 @@ func (e *QwenExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (stream <-chan cliproxyexecutor.StreamChunk, err error) {
|
func (e *QwenExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (stream <-chan cliproxyexecutor.StreamChunk, err error) {
|
||||||
|
if opts.Alt == "responses/compact" {
|
||||||
|
return nil, statusErr{code: http.StatusNotImplemented, msg: "/responses/compact not supported"}
|
||||||
|
}
|
||||||
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
baseModel := thinking.ParseSuffix(req.Model).ModelName
|
||||||
|
|
||||||
token, baseURL := qwenCreds(auth)
|
token, baseURL := qwenCreds(auth)
|
||||||
|
|||||||
@@ -199,15 +199,31 @@ func parseOpenAIUsage(data []byte) usage.Detail {
|
|||||||
if !usageNode.Exists() {
|
if !usageNode.Exists() {
|
||||||
return usage.Detail{}
|
return usage.Detail{}
|
||||||
}
|
}
|
||||||
|
inputNode := usageNode.Get("prompt_tokens")
|
||||||
|
if !inputNode.Exists() {
|
||||||
|
inputNode = usageNode.Get("input_tokens")
|
||||||
|
}
|
||||||
|
outputNode := usageNode.Get("completion_tokens")
|
||||||
|
if !outputNode.Exists() {
|
||||||
|
outputNode = usageNode.Get("output_tokens")
|
||||||
|
}
|
||||||
detail := usage.Detail{
|
detail := usage.Detail{
|
||||||
InputTokens: usageNode.Get("prompt_tokens").Int(),
|
InputTokens: inputNode.Int(),
|
||||||
OutputTokens: usageNode.Get("completion_tokens").Int(),
|
OutputTokens: outputNode.Int(),
|
||||||
TotalTokens: usageNode.Get("total_tokens").Int(),
|
TotalTokens: usageNode.Get("total_tokens").Int(),
|
||||||
}
|
}
|
||||||
if cached := usageNode.Get("prompt_tokens_details.cached_tokens"); cached.Exists() {
|
cached := usageNode.Get("prompt_tokens_details.cached_tokens")
|
||||||
|
if !cached.Exists() {
|
||||||
|
cached = usageNode.Get("input_tokens_details.cached_tokens")
|
||||||
|
}
|
||||||
|
if cached.Exists() {
|
||||||
detail.CachedTokens = cached.Int()
|
detail.CachedTokens = cached.Int()
|
||||||
}
|
}
|
||||||
if reasoning := usageNode.Get("completion_tokens_details.reasoning_tokens"); reasoning.Exists() {
|
reasoning := usageNode.Get("completion_tokens_details.reasoning_tokens")
|
||||||
|
if !reasoning.Exists() {
|
||||||
|
reasoning = usageNode.Get("output_tokens_details.reasoning_tokens")
|
||||||
|
}
|
||||||
|
if reasoning.Exists() {
|
||||||
detail.ReasoningTokens = reasoning.Int()
|
detail.ReasoningTokens = reasoning.Int()
|
||||||
}
|
}
|
||||||
return detail
|
return detail
|
||||||
|
|||||||
43
internal/runtime/executor/usage_helpers_test.go
Normal file
43
internal/runtime/executor/usage_helpers_test.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package executor
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
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}}}`)
|
||||||
|
detail := parseOpenAIUsage(data)
|
||||||
|
if detail.InputTokens != 1 {
|
||||||
|
t.Fatalf("input tokens = %d, want %d", detail.InputTokens, 1)
|
||||||
|
}
|
||||||
|
if detail.OutputTokens != 2 {
|
||||||
|
t.Fatalf("output tokens = %d, want %d", detail.OutputTokens, 2)
|
||||||
|
}
|
||||||
|
if detail.TotalTokens != 3 {
|
||||||
|
t.Fatalf("total tokens = %d, want %d", detail.TotalTokens, 3)
|
||||||
|
}
|
||||||
|
if detail.CachedTokens != 4 {
|
||||||
|
t.Fatalf("cached tokens = %d, want %d", detail.CachedTokens, 4)
|
||||||
|
}
|
||||||
|
if detail.ReasoningTokens != 5 {
|
||||||
|
t.Fatalf("reasoning tokens = %d, want %d", detail.ReasoningTokens, 5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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}}}`)
|
||||||
|
detail := parseOpenAIUsage(data)
|
||||||
|
if detail.InputTokens != 10 {
|
||||||
|
t.Fatalf("input tokens = %d, want %d", detail.InputTokens, 10)
|
||||||
|
}
|
||||||
|
if detail.OutputTokens != 20 {
|
||||||
|
t.Fatalf("output tokens = %d, want %d", detail.OutputTokens, 20)
|
||||||
|
}
|
||||||
|
if detail.TotalTokens != 30 {
|
||||||
|
t.Fatalf("total tokens = %d, want %d", detail.TotalTokens, 30)
|
||||||
|
}
|
||||||
|
if detail.CachedTokens != 7 {
|
||||||
|
t.Fatalf("cached tokens = %d, want %d", detail.CachedTokens, 7)
|
||||||
|
}
|
||||||
|
if detail.ReasoningTokens != 9 {
|
||||||
|
t.Fatalf("reasoning tokens = %d, want %d", detail.ReasoningTokens, 9)
|
||||||
|
}
|
||||||
|
}
|
||||||
120
sdk/api/handlers/openai/openai_responses_compact_test.go
Normal file
120
sdk/api/handlers/openai/openai_responses_compact_test.go
Normal file
@@ -0,0 +1,120 @@
|
|||||||
|
package openai
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/internal/registry"
|
||||||
|
"github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers"
|
||||||
|
coreauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth"
|
||||||
|
coreexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor"
|
||||||
|
sdkconfig "github.com/router-for-me/CLIProxyAPI/v6/sdk/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type compactCaptureExecutor struct {
|
||||||
|
alt string
|
||||||
|
sourceFormat string
|
||||||
|
calls int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *compactCaptureExecutor) Identifier() string { return "test-provider" }
|
||||||
|
|
||||||
|
func (e *compactCaptureExecutor) Execute(ctx context.Context, auth *coreauth.Auth, req coreexecutor.Request, opts coreexecutor.Options) (coreexecutor.Response, error) {
|
||||||
|
e.calls++
|
||||||
|
e.alt = opts.Alt
|
||||||
|
e.sourceFormat = opts.SourceFormat.String()
|
||||||
|
return coreexecutor.Response{Payload: []byte(`{"ok":true}`)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *compactCaptureExecutor) ExecuteStream(context.Context, *coreauth.Auth, coreexecutor.Request, coreexecutor.Options) (<-chan coreexecutor.StreamChunk, error) {
|
||||||
|
return nil, errors.New("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *compactCaptureExecutor) Refresh(ctx context.Context, auth *coreauth.Auth) (*coreauth.Auth, error) {
|
||||||
|
return auth, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *compactCaptureExecutor) CountTokens(context.Context, *coreauth.Auth, coreexecutor.Request, coreexecutor.Options) (coreexecutor.Response, error) {
|
||||||
|
return coreexecutor.Response{}, errors.New("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *compactCaptureExecutor) HttpRequest(context.Context, *coreauth.Auth, *http.Request) (*http.Response, error) {
|
||||||
|
return nil, errors.New("not implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpenAIResponsesCompactRejectsStream(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
executor := &compactCaptureExecutor{}
|
||||||
|
manager := coreauth.NewManager(nil, nil, nil)
|
||||||
|
manager.RegisterExecutor(executor)
|
||||||
|
|
||||||
|
auth := &coreauth.Auth{ID: "auth1", Provider: executor.Identifier(), Status: coreauth.StatusActive}
|
||||||
|
if _, err := manager.Register(context.Background(), auth); err != nil {
|
||||||
|
t.Fatalf("Register auth: %v", err)
|
||||||
|
}
|
||||||
|
registry.GetGlobalRegistry().RegisterClient(auth.ID, auth.Provider, []*registry.ModelInfo{{ID: "test-model"}})
|
||||||
|
t.Cleanup(func() {
|
||||||
|
registry.GetGlobalRegistry().UnregisterClient(auth.ID)
|
||||||
|
})
|
||||||
|
|
||||||
|
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
|
||||||
|
h := NewOpenAIResponsesAPIHandler(base)
|
||||||
|
router := gin.New()
|
||||||
|
router.POST("/v1/responses/compact", h.Compact)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/v1/responses/compact", strings.NewReader(`{"model":"test-model","stream":true}`))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
resp := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(resp, req)
|
||||||
|
|
||||||
|
if resp.Code != http.StatusBadRequest {
|
||||||
|
t.Fatalf("status = %d, want %d", resp.Code, http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
if executor.calls != 0 {
|
||||||
|
t.Fatalf("executor calls = %d, want 0", executor.calls)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOpenAIResponsesCompactExecute(t *testing.T) {
|
||||||
|
gin.SetMode(gin.TestMode)
|
||||||
|
executor := &compactCaptureExecutor{}
|
||||||
|
manager := coreauth.NewManager(nil, nil, nil)
|
||||||
|
manager.RegisterExecutor(executor)
|
||||||
|
|
||||||
|
auth := &coreauth.Auth{ID: "auth2", Provider: executor.Identifier(), Status: coreauth.StatusActive}
|
||||||
|
if _, err := manager.Register(context.Background(), auth); err != nil {
|
||||||
|
t.Fatalf("Register auth: %v", err)
|
||||||
|
}
|
||||||
|
registry.GetGlobalRegistry().RegisterClient(auth.ID, auth.Provider, []*registry.ModelInfo{{ID: "test-model"}})
|
||||||
|
t.Cleanup(func() {
|
||||||
|
registry.GetGlobalRegistry().UnregisterClient(auth.ID)
|
||||||
|
})
|
||||||
|
|
||||||
|
base := handlers.NewBaseAPIHandlers(&sdkconfig.SDKConfig{}, manager)
|
||||||
|
h := NewOpenAIResponsesAPIHandler(base)
|
||||||
|
router := gin.New()
|
||||||
|
router.POST("/v1/responses/compact", h.Compact)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/v1/responses/compact", strings.NewReader(`{"model":"test-model","input":"hello"}`))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
resp := httptest.NewRecorder()
|
||||||
|
router.ServeHTTP(resp, req)
|
||||||
|
|
||||||
|
if resp.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status = %d, want %d", resp.Code, http.StatusOK)
|
||||||
|
}
|
||||||
|
if executor.alt != "responses/compact" {
|
||||||
|
t.Fatalf("alt = %q, want %q", executor.alt, "responses/compact")
|
||||||
|
}
|
||||||
|
if executor.sourceFormat != "openai-response" {
|
||||||
|
t.Fatalf("source format = %q, want %q", executor.sourceFormat, "openai-response")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(resp.Body.String()) != `{"ok":true}` {
|
||||||
|
t.Fatalf("body = %s", resp.Body.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -91,6 +91,44 @@ func (h *OpenAIResponsesAPIHandler) Responses(c *gin.Context) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *OpenAIResponsesAPIHandler) Compact(c *gin.Context) {
|
||||||
|
rawJSON, err := c.GetRawData()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, handlers.ErrorResponse{
|
||||||
|
Error: handlers.ErrorDetail{
|
||||||
|
Message: fmt.Sprintf("Invalid request: %v", err),
|
||||||
|
Type: "invalid_request_error",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
streamResult := gjson.GetBytes(rawJSON, "stream")
|
||||||
|
if streamResult.Type == gjson.True {
|
||||||
|
c.JSON(http.StatusBadRequest, handlers.ErrorResponse{
|
||||||
|
Error: handlers.ErrorDetail{
|
||||||
|
Message: "Streaming not supported for compact responses",
|
||||||
|
Type: "invalid_request_error",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Header("Content-Type", "application/json")
|
||||||
|
modelName := gjson.GetBytes(rawJSON, "model").String()
|
||||||
|
cliCtx, cliCancel := h.GetContextWithCancel(h, c, context.Background())
|
||||||
|
stopKeepAlive := h.StartNonStreamingKeepAlive(c, cliCtx)
|
||||||
|
resp, errMsg := h.ExecuteWithAuthManager(cliCtx, h.HandlerType(), modelName, rawJSON, "responses/compact")
|
||||||
|
stopKeepAlive()
|
||||||
|
if errMsg != nil {
|
||||||
|
h.WriteErrorResponse(c, errMsg)
|
||||||
|
cliCancel(errMsg.Error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = c.Writer.Write(resp)
|
||||||
|
cliCancel()
|
||||||
|
}
|
||||||
|
|
||||||
// handleNonStreamingResponse handles non-streaming chat completion responses
|
// handleNonStreamingResponse handles non-streaming chat completion responses
|
||||||
// for Gemini models. It selects a client from the pool, sends the request, and
|
// for Gemini models. It selects a client from the pool, sends the request, and
|
||||||
// aggregates the response before sending it back to the client in OpenAIResponses format.
|
// aggregates the response before sending it back to the client in OpenAIResponses format.
|
||||||
|
|||||||
Reference in New Issue
Block a user