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, ¶m, ) 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), ¶m, ) 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, ¶m, ) 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, ¶m, ) 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. Load more
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, ¶m, ) 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), ¶m, ) 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, ¶m, ) 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, ¶m, ) 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)