549 lines · 18.9 KB · Go
RawDownload
// Package executor provides runtime execution capabilities for various AI service providers.// It includes stateless executors that handle API requests, streaming responses,// token counting, and authentication refresh for different AI service providers.package executor import (	"bufio"	"bytes"	"context"	"fmt"	"io"	"net/http"	"strings" 	"github.com/router-for-me/CLIProxyAPI/v6/internal/config"	"github.com/router-for-me/CLIProxyAPI/v6/internal/thinking"	"github.com/router-for-me/CLIProxyAPI/v6/internal/util"	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"	log "github.com/sirupsen/logrus"	"github.com/tidwall/gjson"	"github.com/tidwall/sjson") const (	// glEndpoint is the base URL for the Google Generative Language API.	glEndpoint = "https://generativelanguage.googleapis.com" 	// glAPIVersion is the API version used for Gemini requests.	glAPIVersion = "v1beta" 	// streamScannerBuffer is the buffer size for SSE stream scanning.	streamScannerBuffer = 52_428_800) // GeminiExecutor is a stateless executor for the official Gemini API using API keys.// It handles both API key and OAuth bearer token authentication, supporting both// regular and streaming requests to the Google Generative Language API.type GeminiExecutor struct {	// cfg holds the application configuration.	cfg *config.Config} // NewGeminiExecutor creates a new Gemini executor instance.//// Parameters://   - cfg: The application configuration//// Returns://   - *GeminiExecutor: A new Gemini executor instancefunc NewGeminiExecutor(cfg *config.Config) *GeminiExecutor {	return &GeminiExecutor{cfg: cfg}} // Identifier returns the executor identifier.func (e *GeminiExecutor) Identifier() string { return "gemini" } // PrepareRequest injects Gemini credentials into the outgoing HTTP request.func (e *GeminiExecutor) PrepareRequest(req *http.Request, auth *cliproxyauth.Auth) error {	if req == nil {		return nil	}	apiKey, bearer := geminiCreds(auth)	if apiKey != "" {		req.Header.Set("x-goog-api-key", apiKey)		req.Header.Del("Authorization")	} else if bearer != "" {		req.Header.Set("Authorization", "Bearer "+bearer)		req.Header.Del("x-goog-api-key")	}	applyGeminiHeaders(req, auth)	return nil} // HttpRequest injects Gemini credentials into the request and executes it.func (e *GeminiExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Auth, req *http.Request) (*http.Response, error) {	if req == nil {		return nil, fmt.Errorf("gemini executor: request is nil")	}	if ctx == nil {		ctx = req.Context()	}	httpReq := req.WithContext(ctx)	if err := e.PrepareRequest(httpReq, auth); err != nil {		return nil, err	}	httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)	return httpClient.Do(httpReq)} // Execute performs a non-streaming request to the Gemini API.// It translates the request to Gemini format, sends it to the API, and translates// the response back to the requested format.//// Parameters://   - ctx: The context for the request//   - auth: The authentication information//   - req: The request to execute//   - opts: Additional execution options//// Returns://   - cliproxyexecutor.Response: The response from the API//   - error: An error if the request failsfunc (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 	apiKey, bearer := geminiCreds(auth) 	reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth)	defer reporter.trackFailure(ctx, &err) 	// Official Gemini API via API key or OAuth bearer	from := opts.SourceFormat	to := sdktranslator.FromString("gemini")	originalPayloadSource := req.Payload	if len(opts.OriginalRequest) > 0 {		originalPayloadSource = opts.OriginalRequest	}	originalPayload := originalPayloadSource	originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, false)	body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false) 	body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())	if err != nil {		return resp, err	} 	body = fixGeminiImageAspectRatio(baseModel, body)	requestedModel := payloadRequestedModel(opts, req.Model)	body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)	body, _ = sjson.SetBytes(body, "model", baseModel) 	action := "generateContent"	if req.Metadata != nil {		if a, _ := req.Metadata["action"].(string); a == "countTokens" {			action = "countTokens"		}	}	baseURL := resolveGeminiBaseURL(auth)	url := fmt.Sprintf("%s/%s/models/%s:%s", baseURL, glAPIVersion, baseModel, action)	if opts.Alt != "" && action != "countTokens" {		url = url + fmt.Sprintf("?$alt=%s", opts.Alt)	} 	body, _ = sjson.DeleteBytes(body, "session_id") 	httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))	if err != nil {		return resp, err	}	httpReq.Header.Set("Content-Type", "application/json")	if apiKey != "" {		httpReq.Header.Set("x-goog-api-key", apiKey)	} else if bearer != "" {		httpReq.Header.Set("Authorization", "Bearer "+bearer)	}	applyGeminiHeaders(httpReq, auth)	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("gemini 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, parseGeminiUsage(data))	var param any	out := sdktranslator.TranslateNonStream(ctx, to, from, req.Model, opts.OriginalRequest, body, data, &param)	resp = cliproxyexecutor.Response{Payload: []byte(out), Headers: httpResp.Header.Clone()}	return resp, nil} // 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) (_ *cliproxyexecutor.StreamResult, err error) {	if opts.Alt == "responses/compact" {		return nil, statusErr{code: http.StatusNotImplemented, msg: "/responses/compact not supported"}	}	baseModel := thinking.ParseSuffix(req.Model).ModelName 	apiKey, bearer := geminiCreds(auth) 	reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth)	defer reporter.trackFailure(ctx, &err) 	from := opts.SourceFormat	to := sdktranslator.FromString("gemini")	originalPayloadSource := req.Payload	if len(opts.OriginalRequest) > 0 {		originalPayloadSource = opts.OriginalRequest	}	originalPayload := originalPayloadSource	originalTranslated := sdktranslator.TranslateRequest(from, to, baseModel, originalPayload, true)	body := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, true) 	body, err = thinking.ApplyThinking(body, req.Model, from.String(), to.String(), e.Identifier())	if err != nil {		return nil, err	} 	body = fixGeminiImageAspectRatio(baseModel, body)	requestedModel := payloadRequestedModel(opts, req.Model)	body = applyPayloadConfigWithRoot(e.cfg, baseModel, to.String(), "", body, originalTranslated, requestedModel)	body, _ = sjson.SetBytes(body, "model", baseModel) 	baseURL := resolveGeminiBaseURL(auth)	url := fmt.Sprintf("%s/%s/models/%s:%s", baseURL, glAPIVersion, baseModel, "streamGenerateContent")	if opts.Alt == "" {		url = url + "?alt=sse"	} else {		url = url + fmt.Sprintf("?$alt=%s", opts.Alt)	} 	body, _ = sjson.DeleteBytes(body, "session_id") 	httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))	if err != nil {		return nil, err	}	httpReq.Header.Set("Content-Type", "application/json")	if apiKey != "" {		httpReq.Header.Set("x-goog-api-key", apiKey)	} else {		httpReq.Header.Set("Authorization", "Bearer "+bearer)	}	applyGeminiHeaders(httpReq, auth)	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 nil, err	}	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))		if errClose := httpResp.Body.Close(); errClose != nil {			log.Errorf("gemini executor: close response body error: %v", errClose)		}		err = statusErr{code: httpResp.StatusCode, msg: string(b)}		return nil, err	}	out := make(chan cliproxyexecutor.StreamChunk)	go func() {		defer close(out)		defer func() {			if errClose := httpResp.Body.Close(); errClose != nil {				log.Errorf("gemini executor: close response body error: %v", errClose)			}		}()		scanner := bufio.NewScanner(httpResp.Body)		scanner.Buffer(nil, streamScannerBuffer)		var param any		for scanner.Scan() {			line := scanner.Bytes()			appendAPIResponseChunk(ctx, e.cfg, line)			filtered := FilterSSEUsageMetadata(line)			payload := jsonPayload(filtered)			if len(payload) == 0 {				continue			}			if detail, ok := parseGeminiStreamUsage(payload); ok {				reporter.publish(ctx, detail)			}			lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, bytes.Clone(payload), &param)			for i := range lines {				out <- cliproxyexecutor.StreamChunk{Payload: []byte(lines[i])}			}		}		lines := sdktranslator.TranslateStream(ctx, to, from, req.Model, opts.OriginalRequest, body, []byte("[DONE]"), &param)		for i := range lines {			out <- cliproxyexecutor.StreamChunk{Payload: []byte(lines[i])}		}		if errScan := scanner.Err(); errScan != nil {			recordAPIResponseError(ctx, e.cfg, errScan)			reporter.publishFailure(ctx)			out <- cliproxyexecutor.StreamChunk{Err: errScan}		}	}()	return &cliproxyexecutor.StreamResult{Headers: httpResp.Header.Clone(), Chunks: out}, nil} // CountTokens counts tokens for the given request using the Gemini API.func (e *GeminiExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (cliproxyexecutor.Response, error) {	baseModel := thinking.ParseSuffix(req.Model).ModelName 	apiKey, bearer := geminiCreds(auth) 	from := opts.SourceFormat	to := sdktranslator.FromString("gemini")	translatedReq := sdktranslator.TranslateRequest(from, to, baseModel, req.Payload, false) 	translatedReq, err := thinking.ApplyThinking(translatedReq, req.Model, from.String(), to.String(), e.Identifier())	if err != nil {		return cliproxyexecutor.Response{}, err	} 	translatedReq = fixGeminiImageAspectRatio(baseModel, translatedReq)	respCtx := context.WithValue(ctx, "alt", opts.Alt)	translatedReq, _ = sjson.DeleteBytes(translatedReq, "tools")	translatedReq, _ = sjson.DeleteBytes(translatedReq, "generationConfig")	translatedReq, _ = sjson.DeleteBytes(translatedReq, "safetySettings")	translatedReq, _ = sjson.SetBytes(translatedReq, "model", baseModel) 	baseURL := resolveGeminiBaseURL(auth)	url := fmt.Sprintf("%s/%s/models/%s:%s", baseURL, glAPIVersion, baseModel, "countTokens") 	requestBody := bytes.NewReader(translatedReq) 	httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, url, requestBody)	if err != nil {		return cliproxyexecutor.Response{}, err	}	httpReq.Header.Set("Content-Type", "application/json")	if apiKey != "" {		httpReq.Header.Set("x-goog-api-key", apiKey)	} else {		httpReq.Header.Set("Authorization", "Bearer "+bearer)	}	applyGeminiHeaders(httpReq, auth)	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:      translatedReq,		Provider:  e.Identifier(),		AuthID:    authID,		AuthLabel: authLabel,		AuthType:  authType,		AuthValue: authValue,	}) 	httpClient := newProxyAwareHTTPClient(ctx, e.cfg, auth, 0)	resp, err := httpClient.Do(httpReq)	if err != nil {		recordAPIResponseError(ctx, e.cfg, err)		return cliproxyexecutor.Response{}, err	}	defer func() { _ = resp.Body.Close() }()	recordAPIResponseMetadata(ctx, e.cfg, resp.StatusCode, resp.Header.Clone()) 	data, err := io.ReadAll(resp.Body)	if err != nil {		recordAPIResponseError(ctx, e.cfg, err)		return cliproxyexecutor.Response{}, err	}	appendAPIResponseChunk(ctx, e.cfg, data)	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))		return cliproxyexecutor.Response{}, statusErr{code: resp.StatusCode, msg: string(data)}	} 	count := gjson.GetBytes(data, "totalTokens").Int()	translated := sdktranslator.TranslateTokenCount(respCtx, to, from, count, data)	return cliproxyexecutor.Response{Payload: []byte(translated), Headers: resp.Header.Clone()}, nil} // Refresh refreshes the authentication credentials (no-op for Gemini API key).func (e *GeminiExecutor) Refresh(_ context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) {	return auth, nil} func geminiCreds(a *cliproxyauth.Auth) (apiKey, bearer string) {	if a == nil {		return "", ""	}	if a.Attributes != nil {		if v := a.Attributes["api_key"]; v != "" {			apiKey = v		}	}	if a.Metadata != nil {		// GeminiTokenStorage.Token is a map that may contain access_token		if v, ok := a.Metadata["access_token"].(string); ok && v != "" {			bearer = v		}		if token, ok := a.Metadata["token"].(map[string]any); ok && token != nil {			if v, ok2 := token["access_token"].(string); ok2 && v != "" {				bearer = v			}		}	}	return} func resolveGeminiBaseURL(auth *cliproxyauth.Auth) string {	base := glEndpoint	if auth != nil && auth.Attributes != nil {		if custom := strings.TrimSpace(auth.Attributes["base_url"]); custom != "" {			base = strings.TrimRight(custom, "/")		}	}	if base == "" {		return glEndpoint	}	return base} func (e *GeminiExecutor) resolveGeminiConfig(auth *cliproxyauth.Auth) *config.GeminiKey {	if auth == nil || e.cfg == nil {		return nil	}	var attrKey, attrBase string	if auth.Attributes != nil {		attrKey = strings.TrimSpace(auth.Attributes["api_key"])		attrBase = strings.TrimSpace(auth.Attributes["base_url"])	}	for i := range e.cfg.GeminiKey {		entry := &e.cfg.GeminiKey[i]		cfgKey := strings.TrimSpace(entry.APIKey)		cfgBase := strings.TrimSpace(entry.BaseURL)		if attrKey != "" && attrBase != "" {			if strings.EqualFold(cfgKey, attrKey) && strings.EqualFold(cfgBase, attrBase) {				return entry			}			continue		}		if attrKey != "" && strings.EqualFold(cfgKey, attrKey) {			if cfgBase == "" || strings.EqualFold(cfgBase, attrBase) {				return entry			}		}		if attrKey == "" && attrBase != "" && strings.EqualFold(cfgBase, attrBase) {			return entry		}	}	if attrKey != "" {		for i := range e.cfg.GeminiKey {			entry := &e.cfg.GeminiKey[i]			if strings.EqualFold(strings.TrimSpace(entry.APIKey), attrKey) {				return entry			}		}	}	return nil} 

Lines 1–500 of 549.