1374 lines · 40.6 KB · Go
RawDownload
package executor import (	"bufio"	"bytes"	"context"	"encoding/json"	"fmt"	"io"	"net/http"	"net/url"	"strings"	"time" 	"github.com/router-for-me/CLIProxyAPI/v6/internal/auth/gitlab"	"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/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"	"github.com/tidwall/gjson") const (	gitLabProviderKey             = "gitlab"	gitLabAuthMethodOAuth         = "oauth"	gitLabAuthMethodPAT           = "pat"	gitLabChatEndpoint            = "/api/v4/chat/completions"	gitLabCodeSuggestionsEndpoint = "/api/v4/code_suggestions/completions"	gitLabSSEStreamingHeader      = "X-Supports-Sse-Streaming"	gitLabContext1MBeta           = "context-1m-2025-08-07"	gitLabNativeUserAgent         = "CLIProxyAPIPlus/GitLab-Duo") type GitLabExecutor struct {	cfg *config.Config} type gitLabCatalogModel struct {	ID          string	DisplayName string	Provider    string} type gitLabPrompt struct {	Instruction           string	FileName              string	ContentAboveCursor    string	ChatContext           []map[string]any	CodeSuggestionContext []map[string]any} type gitLabOpenAIStreamState struct {	ID           string	Model        string	Created      int64	LastFullText string	Started      bool	Finished     bool} var gitLabAgenticCatalog = []gitLabCatalogModel{	{ID: "duo-chat-gpt-5-1", DisplayName: "GitLab Duo (GPT-5.1)", Provider: "openai"},	{ID: "duo-chat-opus-4-6", DisplayName: "GitLab Duo (Claude Opus 4.6)", Provider: "anthropic"},	{ID: "duo-chat-opus-4-5", DisplayName: "GitLab Duo (Claude Opus 4.5)", Provider: "anthropic"},	{ID: "duo-chat-sonnet-4-6", DisplayName: "GitLab Duo (Claude Sonnet 4.6)", Provider: "anthropic"},	{ID: "duo-chat-sonnet-4-5", DisplayName: "GitLab Duo (Claude Sonnet 4.5)", Provider: "anthropic"},	{ID: "duo-chat-gpt-5-mini", DisplayName: "GitLab Duo (GPT-5 Mini)", Provider: "openai"},	{ID: "duo-chat-gpt-5-2", DisplayName: "GitLab Duo (GPT-5.2)", Provider: "openai"},	{ID: "duo-chat-gpt-5-2-codex", DisplayName: "GitLab Duo (GPT-5.2 Codex)", Provider: "openai"},	{ID: "duo-chat-gpt-5-codex", DisplayName: "GitLab Duo (GPT-5 Codex)", Provider: "openai"},	{ID: "duo-chat-haiku-4-5", DisplayName: "GitLab Duo (Claude Haiku 4.5)", Provider: "anthropic"},} var gitLabModelAliases = map[string]string{	"duo-chat-haiku-4-6":  "duo-chat-haiku-4-5",} func NewGitLabExecutor(cfg *config.Config) *GitLabExecutor {	return &GitLabExecutor{cfg: cfg}} func (e *GitLabExecutor) Identifier() string { return gitLabProviderKey } func (e *GitLabExecutor) Execute(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (resp cliproxyexecutor.Response, err error) {	if nativeExec, nativeAuth, nativeReq, ok := e.nativeGateway(auth, req); ok {		return nativeExec.Execute(ctx, nativeAuth, nativeReq, opts)	}	baseModel := thinking.ParseSuffix(req.Model).ModelName 	reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth)	defer reporter.trackFailure(ctx, &err) 	translated, err := e.translateToOpenAI(req, opts)	if err != nil {		return resp, err	}	prompt := buildGitLabPrompt(translated)	if strings.TrimSpace(prompt.Instruction) == "" && strings.TrimSpace(prompt.ContentAboveCursor) == "" {		err = statusErr{code: http.StatusBadRequest, msg: "gitlab duo executor: request has no usable text content"}		return resp, err	} 	text, err := e.invokeText(ctx, auth, prompt)	if err != nil {		return resp, err	} 	responseModel := gitLabResolvedModel(auth, req.Model)	openAIResponse := buildGitLabOpenAIResponse(responseModel, text, translated)	reporter.publish(ctx, parseOpenAIUsage(openAIResponse))	reporter.ensurePublished(ctx) 	var param any	out := sdktranslator.TranslateNonStream(		ctx,		sdktranslator.FromString("openai"),		opts.SourceFormat,		req.Model,		opts.OriginalRequest,		translated,		openAIResponse,		&param,	)	return cliproxyexecutor.Response{Payload: []byte(out), Headers: make(http.Header)}, nil} func (e *GitLabExecutor) ExecuteStream(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (_ *cliproxyexecutor.StreamResult, err error) {	if nativeExec, nativeAuth, nativeReq, ok := e.nativeGateway(auth, req); ok {		return nativeExec.ExecuteStream(ctx, nativeAuth, nativeReq, opts)	}	baseModel := thinking.ParseSuffix(req.Model).ModelName 	reporter := newUsageReporter(ctx, e.Identifier(), baseModel, auth)	defer reporter.trackFailure(ctx, &err) 	translated, err := e.translateToOpenAI(req, opts)	if err != nil {		return nil, err	}	prompt := buildGitLabPrompt(translated)	if strings.TrimSpace(prompt.Instruction) == "" && strings.TrimSpace(prompt.ContentAboveCursor) == "" {		return nil, statusErr{code: http.StatusBadRequest, msg: "gitlab duo executor: request has no usable text content"}	} 	if result, streamErr := e.requestCodeSuggestionsStream(ctx, auth, prompt, translated, req, opts, reporter); streamErr == nil {		return result, nil	} else if !shouldFallbackToCodeSuggestions(streamErr) {		return nil, streamErr	} 	text, err := e.invokeText(ctx, auth, prompt)	if err != nil {		return nil, err	}	responseModel := gitLabResolvedModel(auth, req.Model)	openAIResponse := buildGitLabOpenAIResponse(responseModel, text, translated)	reporter.publish(ctx, parseOpenAIUsage(openAIResponse))	reporter.ensurePublished(ctx) 	out := make(chan cliproxyexecutor.StreamChunk, 8)	go func() {		defer close(out)		var param any		lines := buildGitLabOpenAIStream(responseModel, text)		for _, line := range lines {			chunks := sdktranslator.TranslateStream(				ctx,				sdktranslator.FromString("openai"),				opts.SourceFormat,				req.Model,				opts.OriginalRequest,				translated,				[]byte(line),				&param,			)			for i := range chunks {				out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])}			}		}	}()	return &cliproxyexecutor.StreamResult{Headers: make(http.Header), Chunks: out}, nil} func (e *GitLabExecutor) Refresh(ctx context.Context, auth *cliproxyauth.Auth) (*cliproxyauth.Auth, error) {	if auth == nil {		return nil, fmt.Errorf("gitlab duo executor: auth is nil")	}	baseURL := gitLabBaseURL(auth)	token := gitLabPrimaryToken(auth)	if baseURL == "" || token == "" {		return nil, fmt.Errorf("gitlab duo executor: missing base URL or token")	} 	client := gitlab.NewAuthClient(e.cfg)	method := strings.ToLower(strings.TrimSpace(gitLabMetadataString(auth.Metadata, "auth_method", "auth_kind")))	if method == "" {		method = gitLabAuthMethodOAuth	} 	if method == gitLabAuthMethodOAuth {		if refreshed, refreshErr := e.refreshOAuthToken(ctx, client, auth, baseURL); refreshErr == nil && refreshed != nil {			token = refreshed.AccessToken			applyGitLabTokenMetadata(auth.Metadata, refreshed)		}	} 	direct, err := client.FetchDirectAccess(ctx, baseURL, token)	if err != nil && method == gitLabAuthMethodOAuth {		if refreshed, refreshErr := e.refreshOAuthToken(ctx, client, auth, baseURL); refreshErr == nil && refreshed != nil {			token = refreshed.AccessToken			applyGitLabTokenMetadata(auth.Metadata, refreshed)			direct, err = client.FetchDirectAccess(ctx, baseURL, token)		}	}	if err != nil {		return nil, err	} 	if auth.Metadata == nil {		auth.Metadata = make(map[string]any)	}	auth.Metadata["type"] = gitLabProviderKey	auth.Metadata["auth_method"] = method	auth.Metadata["auth_kind"] = gitLabAuthKind(method)	auth.Metadata["base_url"] = gitlab.NormalizeBaseURL(baseURL)	auth.Metadata["last_refresh"] = time.Now().UTC().Format(time.RFC3339)	mergeGitLabDirectAccessMetadata(auth.Metadata, direct)	return auth, nil} func (e *GitLabExecutor) CountTokens(ctx context.Context, auth *cliproxyauth.Auth, req cliproxyexecutor.Request, opts cliproxyexecutor.Options) (cliproxyexecutor.Response, error) {	if nativeExec, nativeAuth, nativeReq, ok := e.nativeGateway(auth, req); ok {		return nativeExec.CountTokens(ctx, nativeAuth, nativeReq, opts)	}	baseModel := thinking.ParseSuffix(req.Model).ModelName	translated := sdktranslator.TranslateRequest(opts.SourceFormat, sdktranslator.FromString("openai"), baseModel, req.Payload, false)	enc, err := tokenizerForModel(baseModel)	if err != nil {		return cliproxyexecutor.Response{}, fmt.Errorf("gitlab duo executor: tokenizer init failed: %w", err)	}	count, err := countOpenAIChatTokens(enc, translated)	if err != nil {		return cliproxyexecutor.Response{}, err	}	return cliproxyexecutor.Response{Payload: buildOpenAIUsageJSON(count), Headers: make(http.Header)}, nil} func (e *GitLabExecutor) HttpRequest(ctx context.Context, auth *cliproxyauth.Auth, req *http.Request) (*http.Response, error) {	if req == nil {		return nil, fmt.Errorf("gitlab duo executor: request is nil")	}	if nativeExec, nativeAuth := e.nativeGatewayHTTP(auth); nativeExec != nil {		return nativeExec.HttpRequest(ctx, nativeAuth, req)	}	if ctx == nil {		ctx = req.Context()	}	httpReq := req.WithContext(ctx)	if token := gitLabPrimaryToken(auth); token != "" {		httpReq.Header.Set("Authorization", "Bearer "+token)	}	return newProxyAwareHTTPClient(ctx, e.cfg, auth, 0).Do(httpReq)} func (e *GitLabExecutor) translateToOpenAI(req cliproxyexecutor.Request, opts cliproxyexecutor.Options) ([]byte, error) {	baseModel := thinking.ParseSuffix(req.Model).ModelName	return sdktranslator.TranslateRequest(opts.SourceFormat, sdktranslator.FromString("openai"), baseModel, req.Payload, opts.Stream), nil} func (e *GitLabExecutor) nativeGateway(	auth *cliproxyauth.Auth,	req cliproxyexecutor.Request,) (cliproxyauth.ProviderExecutor, *cliproxyauth.Auth, cliproxyexecutor.Request, bool) {	if nativeAuth, ok := buildGitLabAnthropicGatewayAuth(auth, req.Model); ok {		nativeReq := req		nativeReq.Model = gitLabResolvedModel(auth, req.Model)		return NewClaudeExecutor(e.cfg), nativeAuth, nativeReq, true	}	if nativeAuth, ok := buildGitLabOpenAIGatewayAuth(auth, req.Model); ok {		nativeReq := req		nativeReq.Model = gitLabResolvedModel(auth, req.Model)		return NewCodexExecutor(e.cfg), nativeAuth, nativeReq, true	}	return nil, nil, req, false} func (e *GitLabExecutor) nativeGatewayHTTP(auth *cliproxyauth.Auth) (cliproxyauth.ProviderExecutor, *cliproxyauth.Auth) {	if nativeAuth, ok := buildGitLabAnthropicGatewayAuth(auth, ""); ok {		return NewClaudeExecutor(e.cfg), nativeAuth	}	if nativeAuth, ok := buildGitLabOpenAIGatewayAuth(auth, ""); ok {		return NewCodexExecutor(e.cfg), nativeAuth	}	return nil, nil} func (e *GitLabExecutor) invokeText(ctx context.Context, auth *cliproxyauth.Auth, prompt gitLabPrompt) (string, error) {	if text, err := e.requestChat(ctx, auth, prompt); err == nil {		return text, nil	} else if !shouldFallbackToCodeSuggestions(err) {		return "", err	}	return e.requestCodeSuggestions(ctx, auth, prompt)} func (e *GitLabExecutor) requestChat(ctx context.Context, auth *cliproxyauth.Auth, prompt gitLabPrompt) (string, error) {	body := map[string]any{		"content":            prompt.Instruction,		"with_clean_history": true,	}	if len(prompt.ChatContext) > 0 {		body["additional_context"] = prompt.ChatContext	}	return e.doJSONTextRequest(ctx, auth, gitLabChatEndpoint, body)} func (e *GitLabExecutor) requestCodeSuggestions(ctx context.Context, auth *cliproxyauth.Auth, prompt gitLabPrompt) (string, error) {	contentAbove := strings.TrimSpace(prompt.ContentAboveCursor)	if contentAbove == "" {		contentAbove = prompt.Instruction	}	body := map[string]any{		"current_file": map[string]any{			"file_name":            prompt.FileName,			"content_above_cursor": contentAbove,			"content_below_cursor": "",		},		"intent":           "generation",		"generation_type":  "small_file",		"user_instruction": prompt.Instruction,		"stream":           false,	}	if len(prompt.CodeSuggestionContext) > 0 {		body["context"] = prompt.CodeSuggestionContext	}	return e.doJSONTextRequest(ctx, auth, gitLabCodeSuggestionsEndpoint, body)} func (e *GitLabExecutor) requestCodeSuggestionsStream(	ctx context.Context,	auth *cliproxyauth.Auth,	prompt gitLabPrompt,	translated []byte,	req cliproxyexecutor.Request,	opts cliproxyexecutor.Options,	reporter *usageReporter,) (*cliproxyexecutor.StreamResult, error) {	contentAbove := strings.TrimSpace(prompt.ContentAboveCursor)	if contentAbove == "" {		contentAbove = prompt.Instruction	}	body := map[string]any{		"current_file": map[string]any{			"file_name":            prompt.FileName,			"content_above_cursor": contentAbove,			"content_below_cursor": "",		},		"intent":           "generation",		"generation_type":  "small_file",		"user_instruction": prompt.Instruction,		"stream":           true,	}	if len(prompt.CodeSuggestionContext) > 0 {		body["context"] = prompt.CodeSuggestionContext	} 	httpResp, bodyRaw, err := e.doJSONRequest(ctx, auth, gitLabCodeSuggestionsEndpoint, body, "text/event-stream")	if err != nil {		return nil, err	}	if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {		defer func() { _ = httpResp.Body.Close() }()		respBody, readErr := io.ReadAll(httpResp.Body)		if readErr != nil {			recordAPIResponseError(ctx, e.cfg, readErr)			return nil, readErr		}		appendAPIResponseChunk(ctx, e.cfg, respBody)		return nil, statusErr{code: httpResp.StatusCode, msg: strings.TrimSpace(string(respBody))}	} 	responseModel := gitLabResolvedModel(auth, req.Model)	out := make(chan cliproxyexecutor.StreamChunk, 16)	go func() {		defer close(out)		defer func() { _ = httpResp.Body.Close() }() 		scanner := bufio.NewScanner(httpResp.Body)		scanner.Buffer(nil, 52_428_800) 		var (			param     any			eventName string			state     gitLabOpenAIStreamState		)		for scanner.Scan() {			line := bytes.Clone(scanner.Bytes())			appendAPIResponseChunk(ctx, e.cfg, line)			trimmed := bytes.TrimSpace(line)			if len(trimmed) == 0 {				continue			}			if bytes.HasPrefix(trimmed, []byte("event:")) {				eventName = strings.TrimSpace(string(trimmed[len("event:"):]))				continue			}			if !bytes.HasPrefix(trimmed, []byte("data:")) {				continue			}			payload := bytes.TrimSpace(trimmed[len("data:"):])			normalized := normalizeGitLabStreamChunk(eventName, payload, responseModel, &state)			eventName = ""			for _, item := range normalized {				if detail, ok := parseOpenAIStreamUsage(item); ok {					reporter.publish(ctx, detail)				}				chunks := sdktranslator.TranslateStream(					ctx,					sdktranslator.FromString("openai"),					opts.SourceFormat,					req.Model,					opts.OriginalRequest,					translated,					item,					&param,				)				for i := range chunks {					out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])}				}			}		}		if errScan := scanner.Err(); errScan != nil {			recordAPIResponseError(ctx, e.cfg, errScan)			reporter.publishFailure(ctx)			out <- cliproxyexecutor.StreamChunk{Err: errScan}			return		}		if !state.Finished {			for _, item := range finalizeGitLabStream(responseModel, &state) {				chunks := sdktranslator.TranslateStream(					ctx,					sdktranslator.FromString("openai"),					opts.SourceFormat,					req.Model,					opts.OriginalRequest,					translated,					item,					&param,				)				for i := range chunks {					out <- cliproxyexecutor.StreamChunk{Payload: []byte(chunks[i])}				}			}		}		reporter.ensurePublished(ctx)	}() 	return &cliproxyexecutor.StreamResult{		Headers: cloneGitLabStreamHeaders(httpResp.Header, bodyRaw),		Chunks:  out,	}, nil} func (e *GitLabExecutor) doJSONTextRequest(ctx context.Context, auth *cliproxyauth.Auth, endpoint string, payload map[string]any) (string, error) {	resp, _, err := e.doJSONRequest(ctx, auth, endpoint, payload, "application/json")	if err != nil {		return "", err	}	defer func() { _ = resp.Body.Close() }() 	respBody, err := io.ReadAll(resp.Body)	if err != nil {		recordAPIResponseError(ctx, e.cfg, err)		return "", err	}	appendAPIResponseChunk(ctx, e.cfg, respBody) 	if resp.StatusCode < 200 || resp.StatusCode >= 300 {		return "", statusErr{code: resp.StatusCode, msg: strings.TrimSpace(string(respBody))}	} 	text, err := parseGitLabTextResponse(endpoint, respBody)	if err != nil {		return "", err	}	return strings.TrimSpace(text), nil} func (e *GitLabExecutor) doJSONRequest(	ctx context.Context,	auth *cliproxyauth.Auth,	endpoint string,	payload map[string]any,	accept string,) (*http.Response, []byte, error) {	token := gitLabPrimaryToken(auth)	baseURL := gitLabBaseURL(auth)

Lines 1–500 of 1374.