mirror of
https://github.com/router-for-me/CLIProxyAPIPlus.git
synced 2026-04-21 07:02:05 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8b89f34f4 | ||
|
|
5da0decef6 |
@@ -2138,9 +2138,6 @@ func (h *Handler) RequestGitLabToken(c *gin.Context) {
|
|||||||
metadata := buildGitLabAuthMetadata(baseURL, gitLabLoginModeOAuth, tokenResp, direct)
|
metadata := buildGitLabAuthMetadata(baseURL, gitLabLoginModeOAuth, tokenResp, direct)
|
||||||
metadata["auth_kind"] = "oauth"
|
metadata["auth_kind"] = "oauth"
|
||||||
metadata["oauth_client_id"] = clientID
|
metadata["oauth_client_id"] = clientID
|
||||||
if clientSecret != "" {
|
|
||||||
metadata["oauth_client_secret"] = clientSecret
|
|
||||||
}
|
|
||||||
metadata["username"] = strings.TrimSpace(user.Username)
|
metadata["username"] = strings.TrimSpace(user.Username)
|
||||||
if email := primaryGitLabEmail(user); email != "" {
|
if email := primaryGitLabEmail(user); email != "" {
|
||||||
metadata["email"] = email
|
metadata["email"] = email
|
||||||
|
|||||||
@@ -30,12 +30,20 @@ const (
|
|||||||
gitLabChatEndpoint = "/api/v4/chat/completions"
|
gitLabChatEndpoint = "/api/v4/chat/completions"
|
||||||
gitLabCodeSuggestionsEndpoint = "/api/v4/code_suggestions/completions"
|
gitLabCodeSuggestionsEndpoint = "/api/v4/code_suggestions/completions"
|
||||||
gitLabSSEStreamingHeader = "X-Supports-Sse-Streaming"
|
gitLabSSEStreamingHeader = "X-Supports-Sse-Streaming"
|
||||||
|
gitLabContext1MBeta = "context-1m-2025-08-07"
|
||||||
|
gitLabNativeUserAgent = "CLIProxyAPIPlus/GitLab-Duo"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GitLabExecutor struct {
|
type GitLabExecutor struct {
|
||||||
cfg *config.Config
|
cfg *config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type gitLabCatalogModel struct {
|
||||||
|
ID string
|
||||||
|
DisplayName string
|
||||||
|
Provider string
|
||||||
|
}
|
||||||
|
|
||||||
type gitLabPrompt struct {
|
type gitLabPrompt struct {
|
||||||
Instruction string
|
Instruction string
|
||||||
FileName string
|
FileName string
|
||||||
@@ -53,6 +61,23 @@ type gitLabOpenAIStreamState struct {
|
|||||||
Finished 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 {
|
func NewGitLabExecutor(cfg *config.Config) *GitLabExecutor {
|
||||||
return &GitLabExecutor{cfg: cfg}
|
return &GitLabExecutor{cfg: cfg}
|
||||||
}
|
}
|
||||||
@@ -249,12 +274,12 @@ func (e *GitLabExecutor) nativeGateway(
|
|||||||
auth *cliproxyauth.Auth,
|
auth *cliproxyauth.Auth,
|
||||||
req cliproxyexecutor.Request,
|
req cliproxyexecutor.Request,
|
||||||
) (cliproxyauth.ProviderExecutor, *cliproxyauth.Auth, cliproxyexecutor.Request, bool) {
|
) (cliproxyauth.ProviderExecutor, *cliproxyauth.Auth, cliproxyexecutor.Request, bool) {
|
||||||
if nativeAuth, ok := buildGitLabAnthropicGatewayAuth(auth); ok {
|
if nativeAuth, ok := buildGitLabAnthropicGatewayAuth(auth, req.Model); ok {
|
||||||
nativeReq := req
|
nativeReq := req
|
||||||
nativeReq.Model = gitLabResolvedModel(auth, req.Model)
|
nativeReq.Model = gitLabResolvedModel(auth, req.Model)
|
||||||
return NewClaudeExecutor(e.cfg), nativeAuth, nativeReq, true
|
return NewClaudeExecutor(e.cfg), nativeAuth, nativeReq, true
|
||||||
}
|
}
|
||||||
if nativeAuth, ok := buildGitLabOpenAIGatewayAuth(auth); ok {
|
if nativeAuth, ok := buildGitLabOpenAIGatewayAuth(auth, req.Model); ok {
|
||||||
nativeReq := req
|
nativeReq := req
|
||||||
nativeReq.Model = gitLabResolvedModel(auth, req.Model)
|
nativeReq.Model = gitLabResolvedModel(auth, req.Model)
|
||||||
return NewCodexExecutor(e.cfg), nativeAuth, nativeReq, true
|
return NewCodexExecutor(e.cfg), nativeAuth, nativeReq, true
|
||||||
@@ -263,10 +288,10 @@ func (e *GitLabExecutor) nativeGateway(
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *GitLabExecutor) nativeGatewayHTTP(auth *cliproxyauth.Auth) (cliproxyauth.ProviderExecutor, *cliproxyauth.Auth) {
|
func (e *GitLabExecutor) nativeGatewayHTTP(auth *cliproxyauth.Auth) (cliproxyauth.ProviderExecutor, *cliproxyauth.Auth) {
|
||||||
if nativeAuth, ok := buildGitLabAnthropicGatewayAuth(auth); ok {
|
if nativeAuth, ok := buildGitLabAnthropicGatewayAuth(auth, ""); ok {
|
||||||
return NewClaudeExecutor(e.cfg), nativeAuth
|
return NewClaudeExecutor(e.cfg), nativeAuth
|
||||||
}
|
}
|
||||||
if nativeAuth, ok := buildGitLabOpenAIGatewayAuth(auth); ok {
|
if nativeAuth, ok := buildGitLabOpenAIGatewayAuth(auth, ""); ok {
|
||||||
return NewCodexExecutor(e.cfg), nativeAuth
|
return NewCodexExecutor(e.cfg), nativeAuth
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -664,7 +689,7 @@ func applyGitLabRequestHeaders(req *http.Request, auth *cliproxyauth.Auth) {
|
|||||||
if auth != nil {
|
if auth != nil {
|
||||||
util.ApplyCustomHeadersFromAttrs(req, auth.Attributes)
|
util.ApplyCustomHeadersFromAttrs(req, auth.Attributes)
|
||||||
}
|
}
|
||||||
for key, value := range gitLabGatewayHeaders(auth) {
|
for key, value := range gitLabGatewayHeaders(auth, "") {
|
||||||
if key == "" || value == "" {
|
if key == "" || value == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -672,34 +697,40 @@ func applyGitLabRequestHeaders(req *http.Request, auth *cliproxyauth.Auth) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func gitLabGatewayHeaders(auth *cliproxyauth.Auth) map[string]string {
|
func gitLabGatewayHeaders(auth *cliproxyauth.Auth, targetProvider string) map[string]string {
|
||||||
if auth == nil || auth.Metadata == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
raw, ok := auth.Metadata["duo_gateway_headers"]
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
out := make(map[string]string)
|
out := make(map[string]string)
|
||||||
switch typed := raw.(type) {
|
if auth != nil && auth.Metadata != nil {
|
||||||
case map[string]string:
|
raw, ok := auth.Metadata["duo_gateway_headers"]
|
||||||
for key, value := range typed {
|
if ok {
|
||||||
key = strings.TrimSpace(key)
|
switch typed := raw.(type) {
|
||||||
value = strings.TrimSpace(value)
|
case map[string]string:
|
||||||
if key != "" && value != "" {
|
for key, value := range typed {
|
||||||
out[key] = value
|
key = strings.TrimSpace(key)
|
||||||
|
value = strings.TrimSpace(value)
|
||||||
|
if key != "" && value != "" {
|
||||||
|
out[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case map[string]any:
|
||||||
|
for key, value := range typed {
|
||||||
|
key = strings.TrimSpace(key)
|
||||||
|
if key == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
strValue := strings.TrimSpace(fmt.Sprint(value))
|
||||||
|
if strValue != "" {
|
||||||
|
out[key] = strValue
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case map[string]any:
|
}
|
||||||
for key, value := range typed {
|
if _, ok := out["User-Agent"]; !ok {
|
||||||
key = strings.TrimSpace(key)
|
out["User-Agent"] = gitLabNativeUserAgent
|
||||||
if key == "" {
|
}
|
||||||
continue
|
if strings.EqualFold(strings.TrimSpace(targetProvider), "openai") {
|
||||||
}
|
if _, ok := out["anthropic-beta"]; !ok {
|
||||||
strValue := strings.TrimSpace(fmt.Sprint(value))
|
out["anthropic-beta"] = gitLabContext1MBeta
|
||||||
if strValue != "" {
|
|
||||||
out[key] = strValue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(out) == 0 {
|
if len(out) == 0 {
|
||||||
@@ -989,8 +1020,8 @@ func gitLabUsage(model string, translatedReq []byte, text string) (int64, int64)
|
|||||||
return promptTokens, int64(completionCount)
|
return promptTokens, int64(completionCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildGitLabAnthropicGatewayAuth(auth *cliproxyauth.Auth) (*cliproxyauth.Auth, bool) {
|
func buildGitLabAnthropicGatewayAuth(auth *cliproxyauth.Auth, requestedModel string) (*cliproxyauth.Auth, bool) {
|
||||||
if !gitLabUsesAnthropicGateway(auth) {
|
if !gitLabUsesAnthropicGateway(auth, requestedModel) {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
baseURL := gitLabAnthropicGatewayBaseURL(auth)
|
baseURL := gitLabAnthropicGatewayBaseURL(auth)
|
||||||
@@ -1006,7 +1037,8 @@ func buildGitLabAnthropicGatewayAuth(auth *cliproxyauth.Auth) (*cliproxyauth.Aut
|
|||||||
}
|
}
|
||||||
nativeAuth.Attributes["api_key"] = token
|
nativeAuth.Attributes["api_key"] = token
|
||||||
nativeAuth.Attributes["base_url"] = baseURL
|
nativeAuth.Attributes["base_url"] = baseURL
|
||||||
for key, value := range gitLabGatewayHeaders(auth) {
|
nativeAuth.Attributes["gitlab_duo_force_context_1m"] = "true"
|
||||||
|
for key, value := range gitLabGatewayHeaders(auth, "anthropic") {
|
||||||
if key == "" || value == "" {
|
if key == "" || value == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1015,8 +1047,8 @@ func buildGitLabAnthropicGatewayAuth(auth *cliproxyauth.Auth) (*cliproxyauth.Aut
|
|||||||
return nativeAuth, true
|
return nativeAuth, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildGitLabOpenAIGatewayAuth(auth *cliproxyauth.Auth) (*cliproxyauth.Auth, bool) {
|
func buildGitLabOpenAIGatewayAuth(auth *cliproxyauth.Auth, requestedModel string) (*cliproxyauth.Auth, bool) {
|
||||||
if !gitLabUsesOpenAIGateway(auth) {
|
if !gitLabUsesOpenAIGateway(auth, requestedModel) {
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
baseURL := gitLabOpenAIGatewayBaseURL(auth)
|
baseURL := gitLabOpenAIGatewayBaseURL(auth)
|
||||||
@@ -1032,7 +1064,7 @@ func buildGitLabOpenAIGatewayAuth(auth *cliproxyauth.Auth) (*cliproxyauth.Auth,
|
|||||||
}
|
}
|
||||||
nativeAuth.Attributes["api_key"] = token
|
nativeAuth.Attributes["api_key"] = token
|
||||||
nativeAuth.Attributes["base_url"] = baseURL
|
nativeAuth.Attributes["base_url"] = baseURL
|
||||||
for key, value := range gitLabGatewayHeaders(auth) {
|
for key, value := range gitLabGatewayHeaders(auth, "openai") {
|
||||||
if key == "" || value == "" {
|
if key == "" || value == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -1041,34 +1073,41 @@ func buildGitLabOpenAIGatewayAuth(auth *cliproxyauth.Auth) (*cliproxyauth.Auth,
|
|||||||
return nativeAuth, true
|
return nativeAuth, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func gitLabUsesAnthropicGateway(auth *cliproxyauth.Auth) bool {
|
func gitLabUsesAnthropicGateway(auth *cliproxyauth.Auth, requestedModel string) bool {
|
||||||
if auth == nil || auth.Metadata == nil {
|
if auth == nil || auth.Metadata == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
provider := strings.ToLower(gitLabMetadataString(auth.Metadata, "model_provider"))
|
provider := gitLabGatewayProvider(auth, requestedModel)
|
||||||
if provider == "" {
|
|
||||||
modelName := strings.ToLower(gitLabMetadataString(auth.Metadata, "model_name"))
|
|
||||||
provider = inferGitLabProviderFromModel(modelName)
|
|
||||||
}
|
|
||||||
return provider == "anthropic" &&
|
return provider == "anthropic" &&
|
||||||
gitLabMetadataString(auth.Metadata, "duo_gateway_base_url") != "" &&
|
gitLabMetadataString(auth.Metadata, "duo_gateway_base_url") != "" &&
|
||||||
gitLabMetadataString(auth.Metadata, "duo_gateway_token") != ""
|
gitLabMetadataString(auth.Metadata, "duo_gateway_token") != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func gitLabUsesOpenAIGateway(auth *cliproxyauth.Auth) bool {
|
func gitLabUsesOpenAIGateway(auth *cliproxyauth.Auth, requestedModel string) bool {
|
||||||
if auth == nil || auth.Metadata == nil {
|
if auth == nil || auth.Metadata == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
provider := strings.ToLower(gitLabMetadataString(auth.Metadata, "model_provider"))
|
provider := gitLabGatewayProvider(auth, requestedModel)
|
||||||
if provider == "" {
|
|
||||||
modelName := strings.ToLower(gitLabMetadataString(auth.Metadata, "model_name"))
|
|
||||||
provider = inferGitLabProviderFromModel(modelName)
|
|
||||||
}
|
|
||||||
return provider == "openai" &&
|
return provider == "openai" &&
|
||||||
gitLabMetadataString(auth.Metadata, "duo_gateway_base_url") != "" &&
|
gitLabMetadataString(auth.Metadata, "duo_gateway_base_url") != "" &&
|
||||||
gitLabMetadataString(auth.Metadata, "duo_gateway_token") != ""
|
gitLabMetadataString(auth.Metadata, "duo_gateway_token") != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func gitLabGatewayProvider(auth *cliproxyauth.Auth, requestedModel string) string {
|
||||||
|
modelName := strings.TrimSpace(gitLabResolvedModel(auth, requestedModel))
|
||||||
|
if provider := inferGitLabProviderFromModel(modelName); provider != "" {
|
||||||
|
return provider
|
||||||
|
}
|
||||||
|
if auth == nil || auth.Metadata == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
provider := strings.ToLower(gitLabMetadataString(auth.Metadata, "model_provider"))
|
||||||
|
if provider == "" {
|
||||||
|
provider = inferGitLabProviderFromModel(gitLabMetadataString(auth.Metadata, "model_name"))
|
||||||
|
}
|
||||||
|
return provider
|
||||||
|
}
|
||||||
|
|
||||||
func inferGitLabProviderFromModel(model string) string {
|
func inferGitLabProviderFromModel(model string) string {
|
||||||
model = strings.ToLower(strings.TrimSpace(model))
|
model = strings.ToLower(strings.TrimSpace(model))
|
||||||
switch {
|
switch {
|
||||||
@@ -1151,6 +1190,9 @@ func gitLabBaseURL(auth *cliproxyauth.Auth) string {
|
|||||||
func gitLabResolvedModel(auth *cliproxyauth.Auth, requested string) string {
|
func gitLabResolvedModel(auth *cliproxyauth.Auth, requested string) string {
|
||||||
requested = strings.TrimSpace(thinking.ParseSuffix(requested).ModelName)
|
requested = strings.TrimSpace(thinking.ParseSuffix(requested).ModelName)
|
||||||
if requested != "" && !strings.EqualFold(requested, "gitlab-duo") {
|
if requested != "" && !strings.EqualFold(requested, "gitlab-duo") {
|
||||||
|
if mapped, ok := gitLabModelAliases[strings.ToLower(requested)]; ok && strings.TrimSpace(mapped) != "" {
|
||||||
|
return mapped
|
||||||
|
}
|
||||||
return requested
|
return requested
|
||||||
}
|
}
|
||||||
if auth != nil && auth.Metadata != nil {
|
if auth != nil && auth.Metadata != nil {
|
||||||
@@ -1277,8 +1319,8 @@ func gitLabAuthKind(method string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GitLabModelsFromAuth(auth *cliproxyauth.Auth) []*registry.ModelInfo {
|
func GitLabModelsFromAuth(auth *cliproxyauth.Auth) []*registry.ModelInfo {
|
||||||
models := make([]*registry.ModelInfo, 0, 4)
|
models := make([]*registry.ModelInfo, 0, len(gitLabAgenticCatalog)+4)
|
||||||
seen := make(map[string]struct{}, 4)
|
seen := make(map[string]struct{}, len(gitLabAgenticCatalog)+4)
|
||||||
addModel := func(id, displayName, provider string) {
|
addModel := func(id, displayName, provider string) {
|
||||||
id = strings.TrimSpace(id)
|
id = strings.TrimSpace(id)
|
||||||
if id == "" {
|
if id == "" {
|
||||||
@@ -1302,6 +1344,18 @@ func GitLabModelsFromAuth(auth *cliproxyauth.Auth) []*registry.ModelInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addModel("gitlab-duo", "GitLab Duo", "gitlab")
|
addModel("gitlab-duo", "GitLab Duo", "gitlab")
|
||||||
|
for _, model := range gitLabAgenticCatalog {
|
||||||
|
addModel(model.ID, model.DisplayName, model.Provider)
|
||||||
|
}
|
||||||
|
for alias, upstream := range gitLabModelAliases {
|
||||||
|
target := strings.TrimSpace(upstream)
|
||||||
|
displayName := "GitLab Duo Alias"
|
||||||
|
provider := strings.TrimSpace(inferGitLabProviderFromModel(target))
|
||||||
|
if provider != "" {
|
||||||
|
displayName = fmt.Sprintf("GitLab Duo Alias (%s)", provider)
|
||||||
|
}
|
||||||
|
addModel(alias, displayName, provider)
|
||||||
|
}
|
||||||
if auth == nil {
|
if auth == nil {
|
||||||
return models
|
return models
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -217,6 +217,69 @@ func TestGitLabExecutorExecuteUsesOpenAIGateway(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGitLabExecutorExecuteUsesRequestedModelToSelectOpenAIGateway(t *testing.T) {
|
||||||
|
var gotAuthHeader, gotRealmHeader, gotBetaHeader, gotUserAgent string
|
||||||
|
var gotPath string
|
||||||
|
var gotModel string
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
gotPath = r.URL.Path
|
||||||
|
gotAuthHeader = r.Header.Get("Authorization")
|
||||||
|
gotRealmHeader = r.Header.Get("X-Gitlab-Realm")
|
||||||
|
gotBetaHeader = r.Header.Get("anthropic-beta")
|
||||||
|
gotUserAgent = r.Header.Get("User-Agent")
|
||||||
|
gotModel = gjson.GetBytes(readBody(t, r), "model").String()
|
||||||
|
w.Header().Set("Content-Type", "text/event-stream")
|
||||||
|
_, _ = w.Write([]byte("data: {\"type\":\"response.created\",\"response\":{\"id\":\"resp_1\",\"created_at\":1710000000,\"model\":\"duo-chat-gpt-5-codex\"}}\n\n"))
|
||||||
|
_, _ = w.Write([]byte("data: {\"type\":\"response.output_text.delta\",\"delta\":\"hello from explicit openai model\"}\n\n"))
|
||||||
|
_, _ = w.Write([]byte("data: {\"type\":\"response.completed\",\"response\":{\"id\":\"resp_1\",\"created_at\":1710000000,\"model\":\"duo-chat-gpt-5-codex\",\"output\":[{\"type\":\"message\",\"id\":\"msg_1\",\"role\":\"assistant\",\"content\":[{\"type\":\"output_text\",\"text\":\"hello from explicit openai model\"}]}],\"usage\":{\"input_tokens\":11,\"output_tokens\":4,\"total_tokens\":15}}}\n\n"))
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
exec := NewGitLabExecutor(&config.Config{})
|
||||||
|
auth := &cliproxyauth.Auth{
|
||||||
|
Provider: "gitlab",
|
||||||
|
Metadata: map[string]any{
|
||||||
|
"duo_gateway_base_url": srv.URL,
|
||||||
|
"duo_gateway_token": "gateway-token",
|
||||||
|
"duo_gateway_headers": map[string]string{"X-Gitlab-Realm": "saas"},
|
||||||
|
"model_provider": "anthropic",
|
||||||
|
"model_name": "claude-sonnet-4-5",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
req := cliproxyexecutor.Request{
|
||||||
|
Model: "duo-chat-gpt-5-codex",
|
||||||
|
Payload: []byte(`{"model":"duo-chat-gpt-5-codex","messages":[{"role":"user","content":"hello"}]}`),
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := exec.Execute(context.Background(), auth, req, cliproxyexecutor.Options{
|
||||||
|
SourceFormat: sdktranslator.FromString("openai"),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Execute() error = %v", err)
|
||||||
|
}
|
||||||
|
if gotPath != "/v1/proxy/openai/v1/responses" {
|
||||||
|
t.Fatalf("Path = %q, want %q", gotPath, "/v1/proxy/openai/v1/responses")
|
||||||
|
}
|
||||||
|
if gotAuthHeader != "Bearer gateway-token" {
|
||||||
|
t.Fatalf("Authorization = %q, want Bearer gateway-token", gotAuthHeader)
|
||||||
|
}
|
||||||
|
if gotRealmHeader != "saas" {
|
||||||
|
t.Fatalf("X-Gitlab-Realm = %q, want saas", gotRealmHeader)
|
||||||
|
}
|
||||||
|
if gotBetaHeader != gitLabContext1MBeta {
|
||||||
|
t.Fatalf("anthropic-beta = %q, want %q", gotBetaHeader, gitLabContext1MBeta)
|
||||||
|
}
|
||||||
|
if gotUserAgent != gitLabNativeUserAgent {
|
||||||
|
t.Fatalf("User-Agent = %q, want %q", gotUserAgent, gitLabNativeUserAgent)
|
||||||
|
}
|
||||||
|
if gotModel != "duo-chat-gpt-5-codex" {
|
||||||
|
t.Fatalf("model = %q, want duo-chat-gpt-5-codex", gotModel)
|
||||||
|
}
|
||||||
|
if got := gjson.GetBytes(resp.Payload, "choices.0.message.content").String(); got != "hello from explicit openai model" {
|
||||||
|
t.Fatalf("expected explicit openai model response, got %q payload=%s", got, string(resp.Payload))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGitLabExecutorRefreshUpdatesMetadata(t *testing.T) {
|
func TestGitLabExecutorRefreshUpdatesMetadata(t *testing.T) {
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
switch r.URL.Path {
|
switch r.URL.Path {
|
||||||
@@ -251,13 +314,12 @@ func TestGitLabExecutorRefreshUpdatesMetadata(t *testing.T) {
|
|||||||
ID: "gitlab-auth.json",
|
ID: "gitlab-auth.json",
|
||||||
Provider: "gitlab",
|
Provider: "gitlab",
|
||||||
Metadata: map[string]any{
|
Metadata: map[string]any{
|
||||||
"base_url": srv.URL,
|
"base_url": srv.URL,
|
||||||
"access_token": "oauth-access",
|
"access_token": "oauth-access",
|
||||||
"refresh_token": "oauth-refresh",
|
"refresh_token": "oauth-refresh",
|
||||||
"oauth_client_id": "client-id",
|
"oauth_client_id": "client-id",
|
||||||
"oauth_client_secret": "client-secret",
|
"auth_method": "oauth",
|
||||||
"auth_method": "oauth",
|
"oauth_expires_at": "2000-01-01T00:00:00Z",
|
||||||
"oauth_expires_at": "2000-01-01T00:00:00Z",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,9 +459,11 @@ func TestGitLabExecutorExecuteStreamFallsBackToSyntheticChat(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestGitLabExecutorExecuteStreamUsesAnthropicGateway(t *testing.T) {
|
func TestGitLabExecutorExecuteStreamUsesAnthropicGateway(t *testing.T) {
|
||||||
var gotPath string
|
var gotPath, gotBetaHeader, gotUserAgent string
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
gotPath = r.URL.Path
|
gotPath = r.URL.Path
|
||||||
|
gotBetaHeader = r.Header.Get("Anthropic-Beta")
|
||||||
|
gotUserAgent = r.Header.Get("User-Agent")
|
||||||
w.Header().Set("Content-Type", "text/event-stream")
|
w.Header().Set("Content-Type", "text/event-stream")
|
||||||
_, _ = w.Write([]byte("event: message_start\n"))
|
_, _ = w.Write([]byte("event: message_start\n"))
|
||||||
_, _ = w.Write([]byte("data: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-5\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":0,\"output_tokens\":0}}}\n\n"))
|
_, _ = w.Write([]byte("data: {\"type\":\"message_start\",\"message\":{\"id\":\"msg_1\",\"type\":\"message\",\"role\":\"assistant\",\"model\":\"claude-sonnet-4-5\",\"content\":[],\"stop_reason\":null,\"stop_sequence\":null,\"usage\":{\"input_tokens\":0,\"output_tokens\":0}}}\n\n"))
|
||||||
@@ -441,6 +505,12 @@ func TestGitLabExecutorExecuteStreamUsesAnthropicGateway(t *testing.T) {
|
|||||||
if gotPath != "/v1/proxy/anthropic/v1/messages" {
|
if gotPath != "/v1/proxy/anthropic/v1/messages" {
|
||||||
t.Fatalf("Path = %q, want %q", gotPath, "/v1/proxy/anthropic/v1/messages")
|
t.Fatalf("Path = %q, want %q", gotPath, "/v1/proxy/anthropic/v1/messages")
|
||||||
}
|
}
|
||||||
|
if !strings.Contains(gotBetaHeader, gitLabContext1MBeta) {
|
||||||
|
t.Fatalf("Anthropic-Beta = %q, want to contain %q", gotBetaHeader, gitLabContext1MBeta)
|
||||||
|
}
|
||||||
|
if gotUserAgent != gitLabNativeUserAgent {
|
||||||
|
t.Fatalf("User-Agent = %q, want %q", gotUserAgent, gitLabNativeUserAgent)
|
||||||
|
}
|
||||||
if !strings.Contains(strings.Join(lines, "\n"), "hello from gateway") {
|
if !strings.Contains(strings.Join(lines, "\n"), "hello from gateway") {
|
||||||
t.Fatalf("expected anthropic gateway stream, got %q", strings.Join(lines, "\n"))
|
t.Fatalf("expected anthropic gateway stream, got %q", strings.Join(lines, "\n"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -209,9 +209,6 @@ waitForCallback:
|
|||||||
metadata := buildGitLabAuthMetadata(baseURL, gitLabLoginModeOAuth, tokenResp, direct)
|
metadata := buildGitLabAuthMetadata(baseURL, gitLabLoginModeOAuth, tokenResp, direct)
|
||||||
metadata["auth_kind"] = "oauth"
|
metadata["auth_kind"] = "oauth"
|
||||||
metadata[gitLabOAuthClientIDMetadataKey] = clientID
|
metadata[gitLabOAuthClientIDMetadataKey] = clientID
|
||||||
if strings.TrimSpace(clientSecret) != "" {
|
|
||||||
metadata[gitLabOAuthClientSecretMetadataKey] = clientSecret
|
|
||||||
}
|
|
||||||
metadata["username"] = strings.TrimSpace(user.Username)
|
metadata["username"] = strings.TrimSpace(user.Username)
|
||||||
if email := strings.TrimSpace(primaryGitLabEmail(user)); email != "" {
|
if email := strings.TrimSpace(primaryGitLabEmail(user)); email != "" {
|
||||||
metadata["email"] = email
|
metadata["email"] = email
|
||||||
|
|||||||
@@ -46,3 +46,41 @@ func TestRegisterModelsForAuth_GitLabUsesDiscoveredModels(t *testing.T) {
|
|||||||
t.Fatalf("expected gitlab-duo and discovered model, got %+v", models)
|
t.Fatalf("expected gitlab-duo and discovered model, got %+v", models)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRegisterModelsForAuth_GitLabIncludesAgenticCatalog(t *testing.T) {
|
||||||
|
service := &Service{cfg: &config.Config{}}
|
||||||
|
auth := &coreauth.Auth{
|
||||||
|
ID: "gitlab-agentic-auth.json",
|
||||||
|
Provider: "gitlab",
|
||||||
|
Status: coreauth.StatusActive,
|
||||||
|
}
|
||||||
|
|
||||||
|
reg := registry.GetGlobalRegistry()
|
||||||
|
reg.UnregisterClient(auth.ID)
|
||||||
|
t.Cleanup(func() { reg.UnregisterClient(auth.ID) })
|
||||||
|
|
||||||
|
service.registerModelsForAuth(auth)
|
||||||
|
models := reg.GetModelsForClient(auth.ID)
|
||||||
|
if len(models) < 5 {
|
||||||
|
t.Fatalf("expected stable alias plus built-in agentic catalog, got %d entries", len(models))
|
||||||
|
}
|
||||||
|
|
||||||
|
required := map[string]bool{
|
||||||
|
"gitlab-duo": false,
|
||||||
|
"duo-chat-opus-4-6": false,
|
||||||
|
"duo-chat-haiku-4-5": false,
|
||||||
|
"duo-chat-sonnet-4-5": false,
|
||||||
|
"duo-chat-opus-4-5": false,
|
||||||
|
"duo-chat-gpt-5-codex": false,
|
||||||
|
}
|
||||||
|
for _, model := range models {
|
||||||
|
if _, ok := required[model.ID]; ok {
|
||||||
|
required[model.ID] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for id, seen := range required {
|
||||||
|
if !seen {
|
||||||
|
t.Fatalf("expected built-in GitLab Duo model %q, got %+v", id, models)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user