luispater commited on
Commit
a2140ee
·
unverified ·
0 Parent(s):

first commit

Browse files
.gitattributes ADDED
@@ -0,0 +1 @@
 
 
1
+ *.png filter=lfs diff=lfs merge=lfs -text
.github/workflows/release.yaml ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: goreleaser
2
+
3
+ on:
4
+ push:
5
+ # run only against tags
6
+ tags:
7
+ - '*'
8
+
9
+ permissions:
10
+ contents: write
11
+
12
+ jobs:
13
+ goreleaser:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v3
17
+ with:
18
+ fetch-depth: 0
19
+ - run: git fetch --force --tags
20
+ - uses: actions/setup-go@v3
21
+ with:
22
+ go-version: '>=1.24.0'
23
+ cache: true
24
+ - uses: goreleaser/goreleaser-action@v3
25
+ with:
26
+ distribution: goreleaser
27
+ version: latest
28
+ args: release --clean
29
+ env:
30
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
.goreleaser.yml ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ builds:
2
+ - id: "cli-proxy-api"
3
+ goos:
4
+ - linux
5
+ - windows
6
+ - darwin
7
+ goarch:
8
+ - amd64
9
+ - arm64
10
+ main: ./cmd/server/
11
+ binary: cli-proxy-api
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Luis Pater
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md ADDED
@@ -0,0 +1,198 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # CLI Proxy API
2
+
3
+ A proxy server that provides an OpenAI-compatible API interface for CLI. This allows you to use CLI models with tools and libraries designed for the OpenAI API.
4
+
5
+ ## Features
6
+
7
+ - OpenAI-compatible API endpoints for CLI models
8
+ - Support for both streaming and non-streaming responses
9
+ - Function calling/tools support
10
+ - Multimodal input support (text and images)
11
+ - Multiple account support with load balancing
12
+ - Simple CLI authentication flow
13
+
14
+ ## Installation
15
+
16
+ ### Prerequisites
17
+
18
+ - Go 1.24 or higher
19
+ - A Google account with access to CLI models
20
+
21
+ ### Building from Source
22
+
23
+ 1. Clone the repository:
24
+ ```bash
25
+ git clone https://github.com/luispater/CLIProxyAPI.git
26
+ cd CLIProxyAPI
27
+ ```
28
+
29
+ 2. Build the application:
30
+ ```bash
31
+ go build -o cli-proxy-api ./cmd/server
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ### Authentication
37
+
38
+ Before using the API, you need to authenticate with your Google account:
39
+
40
+ ```bash
41
+ ./cli-proxy-api --login
42
+ ```
43
+
44
+ If you are an old gemini code user, you may need to specify a project ID:
45
+
46
+ ```bash
47
+ ./cli-proxy-api --login --project_id <your_project_id>
48
+ ```
49
+
50
+ ### Starting the Server
51
+
52
+ Once authenticated, start the server:
53
+
54
+ ```bash
55
+ ./cli-proxy-api
56
+ ```
57
+
58
+ By default, the server runs on port 8317.
59
+
60
+ ### API Endpoints
61
+
62
+ #### List Models
63
+
64
+ ```
65
+ GET http://localhost:8317/v1/models
66
+ ```
67
+
68
+ #### Chat Completions
69
+
70
+ ```
71
+ POST http://localhost:8317/v1/chat/completions
72
+ ```
73
+
74
+ Request body example:
75
+
76
+ ```json
77
+ {
78
+ "model": "gemini-2.5-pro",
79
+ "messages": [
80
+ {
81
+ "role": "user",
82
+ "content": "Hello, how are you?"
83
+ }
84
+ ],
85
+ "stream": true
86
+ }
87
+ ```
88
+
89
+ ### Using with OpenAI Libraries
90
+
91
+ You can use this proxy with any OpenAI-compatible library by setting the base URL to your local server:
92
+
93
+ #### Python (with OpenAI library)
94
+
95
+ ```python
96
+ from openai import OpenAI
97
+
98
+ client = OpenAI(
99
+ api_key="dummy", # Not used but required
100
+ base_url="http://localhost:8317/v1"
101
+ )
102
+
103
+ response = client.chat.completions.create(
104
+ model="gemini-2.5-pro",
105
+ messages=[
106
+ {"role": "user", "content": "Hello, how are you?"}
107
+ ]
108
+ )
109
+
110
+ print(response.choices[0].message.content)
111
+ ```
112
+
113
+ #### JavaScript/TypeScript
114
+
115
+ ```javascript
116
+ import OpenAI from 'openai';
117
+
118
+ const openai = new OpenAI({
119
+ apiKey: 'dummy', // Not used but required
120
+ baseURL: 'http://localhost:8317/v1',
121
+ });
122
+
123
+ const response = await openai.chat.completions.create({
124
+ model: 'gemini-2.5-pro',
125
+ messages: [
126
+ { role: 'user', content: 'Hello, how are you?' }
127
+ ],
128
+ });
129
+
130
+ console.log(response.choices[0].message.content);
131
+ ```
132
+
133
+ ## Supported Models
134
+
135
+ - gemini-2.5-pro
136
+ - gemini-2.5-flash
137
+ - And various preview versions
138
+
139
+ ## Configuration
140
+
141
+ The server uses a YAML configuration file (`config.yaml`) located in the project root directory by default. You can specify a different configuration file path using the `--config` flag:
142
+
143
+ ```bash
144
+ ./cli-proxy --config /path/to/your/config.yaml
145
+ ```
146
+
147
+ ### Configuration Options
148
+
149
+ | Parameter | Type | Default | Description |
150
+ |-----------|------|--------------------|-------------|
151
+ | `port` | integer | 8317 | The port number on which the server will listen |
152
+ | `auth_dir` | string | "~/.cli-proxy-api" | Directory where authentication tokens are stored. Supports using `~` for home directory |
153
+ | `debug` | boolean | false | Enable debug mode for verbose logging |
154
+ | `api_keys` | string[] | [] | List of API keys that can be used to authenticate requests |
155
+
156
+ ### Example Configuration File
157
+
158
+ ```yaml
159
+ # Server port
160
+ port: 8317
161
+
162
+ # Authentication directory (supports ~ for home directory)
163
+ auth_dir: "~/.cli-proxy-api"
164
+
165
+ # Enable debug logging
166
+ debug: false
167
+
168
+ # API keys for authentication
169
+ api_keys:
170
+ - "your-api-key-1"
171
+ - "your-api-key-2"
172
+ ```
173
+
174
+ ### Authentication Directory
175
+
176
+ The `auth_dir` parameter specifies where authentication tokens are stored. When you run the login command, the application will create JSON files in this directory containing the authentication tokens for your Google accounts. Multiple accounts can be used for load balancing.
177
+
178
+ ### API Keys
179
+
180
+ The `api_keys` parameter allows you to define a list of API keys that can be used to authenticate requests to your proxy server. When making requests to the API, you can include one of these keys in the `Authorization` header:
181
+
182
+ ```
183
+ Authorization: Bearer your-api-key-1
184
+ ```
185
+
186
+ ## Contributing
187
+
188
+ Contributions are welcome! Please feel free to submit a Pull Request.
189
+
190
+ 1. Fork the repository
191
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
192
+ 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
193
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
194
+ 5. Open a Pull Request
195
+
196
+ ## License
197
+
198
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
cmd/server/main.go ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package main
2
+
3
+ import (
4
+ "bytes"
5
+ "context"
6
+ "encoding/json"
7
+ "flag"
8
+ "fmt"
9
+ "github.com/luispater/CLIProxyAPI/internal/api"
10
+ "github.com/luispater/CLIProxyAPI/internal/auth"
11
+ "github.com/luispater/CLIProxyAPI/internal/client"
12
+ "github.com/luispater/CLIProxyAPI/internal/config"
13
+ log "github.com/sirupsen/logrus"
14
+ "io/fs"
15
+ "os"
16
+ "os/signal"
17
+ "path"
18
+ "path/filepath"
19
+ "strings"
20
+ "syscall"
21
+ "time"
22
+ )
23
+
24
+ type LogFormatter struct {
25
+ }
26
+
27
+ func (m *LogFormatter) Format(entry *log.Entry) ([]byte, error) {
28
+ var b *bytes.Buffer
29
+ if entry.Buffer != nil {
30
+ b = entry.Buffer
31
+ } else {
32
+ b = &bytes.Buffer{}
33
+ }
34
+
35
+ timestamp := entry.Time.Format("2006-01-02 15:04:05")
36
+ var newLog string
37
+ newLog = fmt.Sprintf("[%s] [%s] [%s:%d] %s\n", timestamp, entry.Level, path.Base(entry.Caller.File), entry.Caller.Line, entry.Message)
38
+
39
+ b.WriteString(newLog)
40
+ return b.Bytes(), nil
41
+ }
42
+
43
+ func init() {
44
+ log.SetOutput(os.Stdout)
45
+ log.SetReportCaller(true)
46
+ log.SetFormatter(&LogFormatter{})
47
+ }
48
+
49
+ func main() {
50
+ var login bool
51
+ var projectID string
52
+ var configPath string
53
+
54
+ flag.BoolVar(&login, "login", false, "Login Google Account")
55
+ flag.StringVar(&projectID, "project_id", "", "Project ID")
56
+ flag.StringVar(&configPath, "config", "", "Configure File Path")
57
+
58
+ flag.Parse()
59
+
60
+ var err error
61
+ var cfg *config.Config
62
+ var wd string
63
+
64
+ if configPath != "" {
65
+ cfg, err = config.LoadConfig(configPath)
66
+ } else {
67
+ wd, err = os.Getwd()
68
+ if err != nil {
69
+ log.Fatalf("failed to get working directory: %v", err)
70
+ }
71
+ cfg, err = config.LoadConfig(path.Join(wd, "config.yaml"))
72
+ }
73
+ if err != nil {
74
+ log.Fatalf("failed to load config: %v", err)
75
+ }
76
+
77
+ if cfg.Debug {
78
+ log.SetLevel(log.DebugLevel)
79
+ } else {
80
+ log.SetLevel(log.InfoLevel)
81
+ }
82
+
83
+ if strings.HasPrefix(cfg.AuthDir, "~") {
84
+ home, errUserHomeDir := os.UserHomeDir()
85
+ if errUserHomeDir != nil {
86
+ log.Fatalf("failed to get home directory: %v", errUserHomeDir)
87
+ }
88
+ parts := strings.Split(cfg.AuthDir, string(os.PathSeparator))
89
+ if len(parts) > 1 {
90
+ parts[0] = home
91
+ cfg.AuthDir = path.Join(parts...)
92
+ } else {
93
+ cfg.AuthDir = home
94
+ }
95
+ }
96
+
97
+ if login {
98
+ var ts auth.TokenStorage
99
+ if projectID != "" {
100
+ ts.ProjectID = projectID
101
+ }
102
+
103
+ // 2. Initialize authenticated HTTP Client
104
+ clientCtx := context.Background()
105
+
106
+ log.Info("Initializing authentication...")
107
+ httpClient, errGetClient := auth.GetAuthenticatedClient(clientCtx, &ts, cfg.AuthDir)
108
+ if errGetClient != nil {
109
+ log.Fatalf("failed to get authenticated client: %v", errGetClient)
110
+ return
111
+ }
112
+ log.Info("Authentication successful.")
113
+
114
+ // 3. Initialize CLI Client
115
+ cliClient := client.NewClient(httpClient)
116
+ if err = cliClient.SetupUser(clientCtx, ts.Email, projectID); err != nil {
117
+ if err.Error() == "failed to start user onboarding, need define a project id" {
118
+ log.Error("failed to start user onboarding")
119
+ project, errGetProjectList := cliClient.GetProjectList(clientCtx)
120
+ if errGetProjectList != nil {
121
+ log.Fatalf("failed to complete user setup: %v", err)
122
+ } else {
123
+ log.Infof("Your account %s needs specify a project id.", ts.Email)
124
+ log.Info("========================================================================")
125
+ for i := 0; i < len(project.Projects); i++ {
126
+ log.Infof("Project ID: %s", project.Projects[i].ProjectID)
127
+ log.Infof("Project Name: %s", project.Projects[i].Name)
128
+ log.Info("========================================================================")
129
+ }
130
+ log.Infof("Please run this command to login again:\n\n%s --login --project_id <project_id>\n", os.Args[0])
131
+ }
132
+ } else {
133
+ // Log as a warning because in some cases, the CLI might still be usable
134
+ // or the user might want to retry setup later.
135
+ log.Fatalf("failed to complete user setup: %v", err)
136
+ }
137
+ }
138
+ } else {
139
+ // Create API server configuration
140
+ apiConfig := &api.ServerConfig{
141
+ Port: fmt.Sprintf("%d", cfg.Port),
142
+ Debug: cfg.Debug,
143
+ ApiKeys: cfg.ApiKeys,
144
+ }
145
+
146
+ cliClients := make([]*client.Client, 0)
147
+ err = filepath.Walk(cfg.AuthDir, func(path string, info fs.FileInfo, err error) error {
148
+ if err != nil {
149
+ return err
150
+ }
151
+
152
+ if !info.IsDir() && strings.HasSuffix(info.Name(), ".json") {
153
+ log.Debugf(path)
154
+ f, errOpen := os.Open(path)
155
+ if errOpen != nil {
156
+ return errOpen
157
+ }
158
+ defer func() {
159
+ _ = f.Close()
160
+ }()
161
+
162
+ var ts auth.TokenStorage
163
+ if err = json.NewDecoder(f).Decode(&ts); err == nil {
164
+ // 2. Initialize authenticated HTTP Client
165
+ clientCtx := context.Background()
166
+
167
+ log.Info("Initializing authentication...")
168
+ httpClient, errGetClient := auth.GetAuthenticatedClient(clientCtx, &ts, cfg.AuthDir)
169
+ if errGetClient != nil {
170
+ log.Fatalf("failed to get authenticated client: %v", errGetClient)
171
+ return errGetClient
172
+ }
173
+ log.Info("Authentication successful.")
174
+
175
+ // 3. Initialize CLI Client
176
+ cliClient := client.NewClient(httpClient)
177
+ if err = cliClient.SetupUser(clientCtx, ts.Email, ts.ProjectID); err != nil {
178
+ if err.Error() == "failed to start user onboarding, need define a project id" {
179
+ log.Error("failed to start user onboarding")
180
+ project, errGetProjectList := cliClient.GetProjectList(clientCtx)
181
+ if errGetProjectList != nil {
182
+ log.Fatalf("failed to complete user setup: %v", err)
183
+ } else {
184
+ log.Infof("Your account %s needs specify a project id.", ts.Email)
185
+ log.Info("========================================================================")
186
+ for i := 0; i < len(project.Projects); i++ {
187
+ log.Infof("Project ID: %s", project.Projects[i].ProjectID)
188
+ log.Infof("Project Name: %s", project.Projects[i].Name)
189
+ log.Info("========================================================================")
190
+ }
191
+ log.Infof("Please run this command to login again:\n\n%s --login --project_id <project_id>\n", os.Args[0])
192
+ }
193
+ } else {
194
+ // Log as a warning because in some cases, the CLI might still be usable
195
+ // or the user might want to retry setup later.
196
+ log.Fatalf("failed to complete user setup: %v", err)
197
+ }
198
+ } else {
199
+ cliClients = append(cliClients, cliClient)
200
+ }
201
+ }
202
+ }
203
+ return nil
204
+ })
205
+
206
+ // Create API server
207
+ apiServer := api.NewServer(apiConfig, cliClients)
208
+ log.Infof("Starting API server on port %s", apiConfig.Port)
209
+ if err = apiServer.Start(); err != nil {
210
+ log.Fatalf("API server failed to start: %v", err)
211
+ return
212
+ }
213
+
214
+ // Set up graceful shutdown
215
+ sigChan := make(chan os.Signal, 1)
216
+ signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
217
+
218
+ for {
219
+ select {
220
+ case <-sigChan:
221
+ log.Debugf("Received shutdown signal. Cleaning up...")
222
+
223
+ // Create shutdown context
224
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
225
+ _ = ctx // Mark ctx as used to avoid error, as apiServer.Stop(ctx) is commented out
226
+
227
+ // Stop API server
228
+ if err = apiServer.Stop(ctx); err != nil {
229
+ log.Debugf("Error stopping API server: %v", err)
230
+ }
231
+ cancel()
232
+
233
+ log.Debugf("Cleanup completed. Exiting...")
234
+ os.Exit(0)
235
+ case <-time.After(5 * time.Second):
236
+
237
+ }
238
+ }
239
+ }
240
+
241
+ }
config.yaml ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ port: 8317
2
+ auth_dir: "~/.cli-proxy-api"
3
+ debug: false
4
+ api_keys:
5
+ - "12345"
6
+ - "23456"
go.mod ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ module github.com/luispater/CLIProxyAPI
2
+
3
+ go 1.24
4
+
5
+ require (
6
+ github.com/gin-gonic/gin v1.10.1
7
+ github.com/sirupsen/logrus v1.9.3
8
+ github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
9
+ github.com/tidwall/gjson v1.18.0
10
+ github.com/tidwall/sjson v1.2.5
11
+ golang.org/x/oauth2 v0.30.0
12
+ gopkg.in/yaml.v3 v3.0.1
13
+ )
14
+
15
+ require (
16
+ cloud.google.com/go/compute/metadata v0.3.0 // indirect
17
+ github.com/bytedance/sonic v1.11.6 // indirect
18
+ github.com/bytedance/sonic/loader v0.1.1 // indirect
19
+ github.com/cloudwego/base64x v0.1.4 // indirect
20
+ github.com/cloudwego/iasm v0.2.0 // indirect
21
+ github.com/gabriel-vasile/mimetype v1.4.3 // indirect
22
+ github.com/gin-contrib/sse v0.1.0 // indirect
23
+ github.com/go-playground/locales v0.14.1 // indirect
24
+ github.com/go-playground/universal-translator v0.18.1 // indirect
25
+ github.com/go-playground/validator/v10 v10.20.0 // indirect
26
+ github.com/goccy/go-json v0.10.2 // indirect
27
+ github.com/json-iterator/go v1.1.12 // indirect
28
+ github.com/klauspost/cpuid/v2 v2.2.7 // indirect
29
+ github.com/leodido/go-urn v1.4.0 // indirect
30
+ github.com/mattn/go-isatty v0.0.20 // indirect
31
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
32
+ github.com/modern-go/reflect2 v1.0.2 // indirect
33
+ github.com/pelletier/go-toml/v2 v2.2.2 // indirect
34
+ github.com/tidwall/match v1.1.1 // indirect
35
+ github.com/tidwall/pretty v1.2.0 // indirect
36
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
37
+ github.com/ugorji/go/codec v1.2.12 // indirect
38
+ golang.org/x/arch v0.8.0 // indirect
39
+ golang.org/x/crypto v0.36.0 // indirect
40
+ golang.org/x/net v0.37.1-0.20250305215238-2914f4677317 // indirect
41
+ golang.org/x/sys v0.31.0 // indirect
42
+ golang.org/x/text v0.23.0 // indirect
43
+ google.golang.org/protobuf v1.34.1 // indirect
44
+ )
go.sum ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
2
+ cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
3
+ github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
4
+ github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
5
+ github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
6
+ github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
7
+ github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
8
+ github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
9
+ github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
10
+ github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
11
+ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
12
+ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
13
+ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
14
+ github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
15
+ github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
16
+ github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
17
+ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
18
+ github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
19
+ github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
20
+ github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
21
+ github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
22
+ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
23
+ github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
24
+ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
25
+ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
26
+ github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
27
+ github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
28
+ github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
29
+ github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
30
+ github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
31
+ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
32
+ github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
33
+ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
34
+ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
35
+ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
36
+ github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
37
+ github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
38
+ github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
39
+ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
40
+ github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
41
+ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
42
+ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
43
+ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
44
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
45
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
46
+ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
47
+ github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
48
+ github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
49
+ github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
50
+ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
51
+ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
52
+ github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
53
+ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
54
+ github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
55
+ github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
56
+ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
57
+ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
58
+ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
59
+ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
60
+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
61
+ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
62
+ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
63
+ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
64
+ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
65
+ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
66
+ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
67
+ github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
68
+ github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
69
+ github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
70
+ github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
71
+ github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
72
+ github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
73
+ github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
74
+ github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
75
+ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
76
+ github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
77
+ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
78
+ github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
79
+ github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
80
+ github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
81
+ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
82
+ golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
83
+ golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
84
+ golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
85
+ golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
86
+ golang.org/x/net v0.37.1-0.20250305215238-2914f4677317 h1:wneCP+2d9NUmndnyTmY7VwUNYiP26xiN/AtdcojQ1lI=
87
+ golang.org/x/net v0.37.1-0.20250305215238-2914f4677317/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
88
+ golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
89
+ golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
90
+ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
91
+ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
92
+ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
93
+ golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
94
+ golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
95
+ golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
96
+ golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
97
+ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
98
+ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
99
+ google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
100
+ google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
101
+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
102
+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
103
+ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
104
+ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
105
+ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
106
+ nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
107
+ rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
internal/api/handlers.go ADDED
@@ -0,0 +1,724 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package api
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "fmt"
7
+ "github.com/luispater/CLIProxyAPI/internal/client"
8
+ log "github.com/sirupsen/logrus"
9
+ "github.com/tidwall/gjson"
10
+ "github.com/tidwall/sjson"
11
+ "net/http"
12
+ "strings"
13
+ "sync"
14
+ "time"
15
+
16
+ "github.com/gin-gonic/gin"
17
+ )
18
+
19
+ var (
20
+ mutex = &sync.Mutex{}
21
+ lastUsedClientIndex = 0
22
+ )
23
+
24
+ // APIHandlers contains the handlers for API endpoints
25
+ type APIHandlers struct {
26
+ cliClients []*client.Client
27
+ debug bool
28
+ }
29
+
30
+ // NewAPIHandlers creates a new API handlers instance
31
+ func NewAPIHandlers(cliClients []*client.Client, debug bool) *APIHandlers {
32
+ return &APIHandlers{
33
+ cliClients: cliClients,
34
+ debug: debug,
35
+ }
36
+ }
37
+
38
+ func (h *APIHandlers) Models(c *gin.Context) {
39
+ c.JSON(http.StatusOK, gin.H{
40
+ "data": []map[string]any{
41
+ {
42
+ "id": "gemini-2.5-pro-preview-05-06",
43
+ "object": "model",
44
+ "version": "2.5-preview-05-06",
45
+ "name": "Gemini 2.5 Pro Preview 05-06",
46
+ "description": "Preview release (May 6th, 2025) of Gemini 2.5 Pro",
47
+ "context_length": 1048576,
48
+ "max_completion_tokens": 65536,
49
+ "supported_parameters": []string{
50
+ "tools",
51
+ "temperature",
52
+ "top_p",
53
+ "top_k",
54
+ },
55
+ "temperature": 1,
56
+ "topP": 0.95,
57
+ "topK": 64,
58
+ "maxTemperature": 2,
59
+ "thinking": true,
60
+ },
61
+ {
62
+ "id": "gemini-2.5-pro-preview-06-05",
63
+ "object": "model",
64
+ "version": "2.5-preview-06-05",
65
+ "name": "Gemini 2.5 Pro Preview",
66
+ "description": "Preview release (June 5th, 2025) of Gemini 2.5 Pro",
67
+ "context_length": 1048576,
68
+ "max_completion_tokens": 65536,
69
+ "supported_parameters": []string{
70
+ "tools",
71
+ "temperature",
72
+ "top_p",
73
+ "top_k",
74
+ },
75
+ "temperature": 1,
76
+ "topP": 0.95,
77
+ "topK": 64,
78
+ "maxTemperature": 2,
79
+ "thinking": true,
80
+ },
81
+ {
82
+ "id": "gemini-2.5-pro",
83
+ "object": "model",
84
+ "version": "2.5",
85
+ "name": "Gemini 2.5 Pro",
86
+ "description": "Stable release (June 17th, 2025) of Gemini 2.5 Pro",
87
+ "context_length": 1048576,
88
+ "max_completion_tokens": 65536,
89
+ "supported_parameters": []string{
90
+ "tools",
91
+ "temperature",
92
+ "top_p",
93
+ "top_k",
94
+ },
95
+ "temperature": 1,
96
+ "topP": 0.95,
97
+ "topK": 64,
98
+ "maxTemperature": 2,
99
+ "thinking": true,
100
+ },
101
+ {
102
+ "id": "gemini-2.5-flash-preview-04-17",
103
+ "object": "model",
104
+ "version": "2.5-preview-04-17",
105
+ "name": "Gemini 2.5 Flash Preview 04-17",
106
+ "description": "Preview release (April 17th, 2025) of Gemini 2.5 Flash",
107
+ "context_length": 1048576,
108
+ "max_completion_tokens": 65536,
109
+ "supported_parameters": []string{
110
+ "tools",
111
+ "temperature",
112
+ "top_p",
113
+ "top_k",
114
+ },
115
+ "temperature": 1,
116
+ "topP": 0.95,
117
+ "topK": 64,
118
+ "maxTemperature": 2,
119
+ "thinking": true,
120
+ },
121
+ {
122
+ "id": "gemini-2.5-flash-preview-05-20",
123
+ "object": "model",
124
+ "version": "2.5-preview-05-20",
125
+ "name": "Gemini 2.5 Flash Preview 05-20",
126
+ "description": "Preview release (April 17th, 2025) of Gemini 2.5 Flash",
127
+ "context_length": 1048576,
128
+ "max_completion_tokens": 65536,
129
+ "supported_parameters": []string{
130
+ "tools",
131
+ "temperature",
132
+ "top_p",
133
+ "top_k",
134
+ },
135
+ "temperature": 1,
136
+ "topP": 0.95,
137
+ "topK": 64,
138
+ "maxTemperature": 2,
139
+ "thinking": true,
140
+ },
141
+ {
142
+ "id": "gemini-2.5-flash",
143
+ "object": "model",
144
+ "version": "001",
145
+ "name": "Gemini 2.5 Flash",
146
+ "description": "Stable version of Gemini 2.5 Flash, our mid-size multimodal model that supports up to 1 million tokens, released in June of 2025.",
147
+ "context_length": 1048576,
148
+ "max_completion_tokens": 65536,
149
+ "supported_parameters": []string{
150
+ "tools",
151
+ "temperature",
152
+ "top_p",
153
+ "top_k",
154
+ },
155
+ "temperature": 1,
156
+ "topP": 0.95,
157
+ "topK": 64,
158
+ "maxTemperature": 2,
159
+ "thinking": true,
160
+ },
161
+ },
162
+ })
163
+ }
164
+
165
+ // ChatCompletions handles the /v1/chat/completions endpoint
166
+ func (h *APIHandlers) ChatCompletions(c *gin.Context) {
167
+ rawJson, err := c.GetRawData()
168
+ // If data retrieval fails, return 400 error
169
+ if err != nil {
170
+ c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid request: %v", err), "code": 400})
171
+ return
172
+ }
173
+
174
+ streamResult := gjson.GetBytes(rawJson, "stream")
175
+ if streamResult.Type == gjson.True {
176
+ h.handleStreamingResponse(c, rawJson)
177
+ } else {
178
+ h.handleNonStreamingResponse(c, rawJson)
179
+ }
180
+ }
181
+
182
+ func (h *APIHandlers) prepareRequest(rawJson []byte) (string, []client.Content, []client.ToolDeclaration) {
183
+ // log.Debug(string(rawJson))
184
+ modelName := "gemini-2.5-pro"
185
+ modelResult := gjson.GetBytes(rawJson, "model")
186
+ if modelResult.Type == gjson.String {
187
+ modelName = modelResult.String()
188
+ }
189
+
190
+ contents := make([]client.Content, 0)
191
+ messagesResult := gjson.GetBytes(rawJson, "messages")
192
+ if messagesResult.IsArray() {
193
+ messagesResults := messagesResult.Array()
194
+ for i := 0; i < len(messagesResults); i++ {
195
+ messageResult := messagesResults[i]
196
+ roleResult := messageResult.Get("role")
197
+ contentResult := messageResult.Get("content")
198
+ if roleResult.Type == gjson.String {
199
+ if roleResult.String() == "system" {
200
+ if contentResult.Type == gjson.String {
201
+ contents = append(contents, client.Content{Role: "user", Parts: []client.Part{{Text: contentResult.String()}}})
202
+ } else if contentResult.IsObject() {
203
+ contentTypeResult := contentResult.Get("type")
204
+ if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "text" {
205
+ contentTextResult := contentResult.Get("text")
206
+ if contentTextResult.Type == gjson.String {
207
+ contents = append(contents, client.Content{Role: "user", Parts: []client.Part{{Text: contentTextResult.String()}}})
208
+ contents = append(contents, client.Content{Role: "model", Parts: []client.Part{{Text: "Understood. I will follow these instructions and use my tools to assist you."}}})
209
+ }
210
+ }
211
+ }
212
+ } else if roleResult.String() == "user" {
213
+ if contentResult.Type == gjson.String {
214
+ contents = append(contents, client.Content{Role: "user", Parts: []client.Part{{Text: contentResult.String()}}})
215
+ } else if contentResult.IsObject() {
216
+ contentTypeResult := contentResult.Get("type")
217
+ if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "text" {
218
+ contentTextResult := contentResult.Get("text")
219
+ if contentTextResult.Type == gjson.String {
220
+ contents = append(contents, client.Content{Role: "user", Parts: []client.Part{{Text: contentTextResult.String()}}})
221
+ }
222
+ }
223
+ } else if contentResult.IsArray() {
224
+ contentItemResults := contentResult.Array()
225
+ parts := make([]client.Part, 0)
226
+ for j := 0; j < len(contentItemResults); j++ {
227
+ contentItemResult := contentItemResults[j]
228
+ contentTypeResult := contentItemResult.Get("type")
229
+ if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "text" {
230
+ contentTextResult := contentItemResult.Get("text")
231
+ if contentTextResult.Type == gjson.String {
232
+ parts = append(parts, client.Part{Text: contentTextResult.String()})
233
+ }
234
+ } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "image_url" {
235
+ imageURLResult := contentItemResult.Get("image_url.url")
236
+ if imageURLResult.Type == gjson.String {
237
+ imageURL := imageURLResult.String()
238
+ if len(imageURL) > 5 {
239
+ imageURLs := strings.SplitN(imageURL[5:], ";", 2)
240
+ if len(imageURLs) == 2 {
241
+ if len(imageURLs[1]) > 7 {
242
+ parts = append(parts, client.Part{InlineData: &client.InlineData{
243
+ MimeType: imageURLs[0],
244
+ Data: imageURLs[1][7:],
245
+ }})
246
+ }
247
+ }
248
+ }
249
+ }
250
+ } else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "file" {
251
+ filenameResult := contentItemResult.Get("file.filename")
252
+ fileDataResult := contentItemResult.Get("file.file_data")
253
+ if filenameResult.Type == gjson.String && fileDataResult.Type == gjson.String {
254
+ filename := filenameResult.String()
255
+ splitFilename := strings.Split(filename, ".")
256
+ ext := splitFilename[len(splitFilename)-1]
257
+
258
+ mimeType, ok := MimeTypes[ext]
259
+ if !ok {
260
+ log.Warnf("Unknown file name extension '%s' at index %d, skipping file", ext, j)
261
+ continue
262
+ }
263
+
264
+ parts = append(parts, client.Part{InlineData: &client.InlineData{
265
+ MimeType: mimeType,
266
+ Data: fileDataResult.String(),
267
+ }})
268
+ }
269
+ }
270
+ }
271
+ contents = append(contents, client.Content{Role: "user", Parts: parts})
272
+ }
273
+ } else if roleResult.String() == "assistant" {
274
+ if contentResult.Type == gjson.String {
275
+ contents = append(contents, client.Content{Role: "model", Parts: []client.Part{{Text: contentResult.String()}}})
276
+ } else if contentResult.IsObject() {
277
+ contentTypeResult := contentResult.Get("type")
278
+ if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "text" {
279
+ contentTextResult := contentResult.Get("text")
280
+ if contentTextResult.Type == gjson.String {
281
+ contents = append(contents, client.Content{Role: "user", Parts: []client.Part{{Text: contentTextResult.String()}}})
282
+ }
283
+ }
284
+ } else if !contentResult.Exists() || contentResult.Type == gjson.Null {
285
+ toolCallsResult := messageResult.Get("tool_calls")
286
+ if toolCallsResult.IsArray() {
287
+ tcsResult := toolCallsResult.Array()
288
+ for j := 0; j < len(tcsResult); j++ {
289
+ tcResult := tcsResult[j]
290
+ functionNameResult := tcResult.Get("function.name")
291
+ functionArguments := tcResult.Get("function.arguments")
292
+ if functionNameResult.Exists() && functionNameResult.Type == gjson.String && functionArguments.Exists() && functionArguments.Type == gjson.String {
293
+ var args map[string]any
294
+ err := json.Unmarshal([]byte(functionArguments.String()), &args)
295
+ if err == nil {
296
+ contents = append(contents, client.Content{
297
+ Role: "model", Parts: []client.Part{
298
+ {
299
+ FunctionCall: &client.FunctionCall{
300
+ Name: functionNameResult.String(),
301
+ Args: args,
302
+ },
303
+ },
304
+ },
305
+ })
306
+ }
307
+ }
308
+ }
309
+ }
310
+ }
311
+ } else if roleResult.String() == "tool" {
312
+ toolCallIDResult := messageResult.Get("tool_call_id")
313
+ if toolCallIDResult.Exists() && toolCallIDResult.Type == gjson.String {
314
+ if contentResult.Type == gjson.String {
315
+ functionResponse := client.FunctionResponse{Name: toolCallIDResult.String(), Response: map[string]interface{}{"result": contentResult.String()}}
316
+ contents = append(contents, client.Content{Role: "tool", Parts: []client.Part{{FunctionResponse: &functionResponse}}})
317
+ } else if contentResult.IsObject() {
318
+ contentTypeResult := contentResult.Get("type")
319
+ if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "text" {
320
+ contentTextResult := contentResult.Get("text")
321
+ if contentTextResult.Type == gjson.String {
322
+ functionResponse := client.FunctionResponse{Name: toolCallIDResult.String(), Response: map[string]interface{}{"result": contentResult.String()}}
323
+ contents = append(contents, client.Content{Role: "tool", Parts: []client.Part{{FunctionResponse: &functionResponse}}})
324
+ }
325
+ }
326
+ }
327
+ }
328
+ }
329
+ }
330
+ }
331
+ }
332
+
333
+ var tools []client.ToolDeclaration
334
+ toolsResult := gjson.GetBytes(rawJson, "tools")
335
+ if toolsResult.IsArray() {
336
+ tools = make([]client.ToolDeclaration, 1)
337
+ tools[0].FunctionDeclarations = make([]any, 0)
338
+ toolsResults := toolsResult.Array()
339
+ for i := 0; i < len(toolsResults); i++ {
340
+ toolTypeResult := toolsResults[i].Get("type")
341
+ if toolTypeResult.Type != gjson.String || toolTypeResult.String() != "function" {
342
+ continue
343
+ }
344
+ functionTypeResult := toolsResults[i].Get("function")
345
+ if functionTypeResult.Exists() && functionTypeResult.IsObject() {
346
+ var functionDeclaration any
347
+ err := json.Unmarshal([]byte(functionTypeResult.Raw), &functionDeclaration)
348
+ if err == nil {
349
+ tools[0].FunctionDeclarations = append(tools[0].FunctionDeclarations, functionDeclaration)
350
+ }
351
+ }
352
+ }
353
+ } else {
354
+ tools = make([]client.ToolDeclaration, 0)
355
+ }
356
+ return modelName, contents, tools
357
+ }
358
+
359
+ // handleNonStreamingResponse handles non-streaming responses
360
+ func (h *APIHandlers) handleNonStreamingResponse(c *gin.Context, rawJson []byte) {
361
+ c.Header("Content-Type", "application/json")
362
+
363
+ // Handle streaming manually
364
+ flusher, ok := c.Writer.(http.Flusher)
365
+ if !ok {
366
+ c.JSON(http.StatusInternalServerError, ErrorResponse{
367
+ Error: ErrorDetail{
368
+ Message: "Streaming not supported",
369
+ Type: "server_error",
370
+ },
371
+ })
372
+ return
373
+ }
374
+
375
+ modelName, contents, tools := h.prepareRequest(rawJson)
376
+ cliCtx, cliCancel := context.WithCancel(context.Background())
377
+ var cliClient *client.Client
378
+ defer func() {
379
+ if cliClient != nil {
380
+ cliClient.RequestMutex.Unlock()
381
+ }
382
+ }()
383
+
384
+ // Lock the mutex to update the last used page index
385
+ mutex.Lock()
386
+ startIndex := lastUsedClientIndex
387
+ currentIndex := (startIndex + 1) % len(h.cliClients)
388
+ lastUsedClientIndex = currentIndex
389
+ mutex.Unlock()
390
+
391
+ // Reorder the pages to start from the last used index
392
+ reorderedPages := make([]*client.Client, len(h.cliClients))
393
+ for i := 0; i < len(h.cliClients); i++ {
394
+ reorderedPages[i] = h.cliClients[(startIndex+1+i)%len(h.cliClients)]
395
+ }
396
+
397
+ locked := false
398
+ for i := 0; i < len(reorderedPages); i++ {
399
+ cliClient = reorderedPages[i]
400
+ if cliClient.RequestMutex.TryLock() {
401
+ locked = true
402
+ break
403
+ }
404
+ }
405
+ if !locked {
406
+ cliClient = h.cliClients[0]
407
+ cliClient.RequestMutex.Lock()
408
+ }
409
+
410
+ log.Debugf("Request use account: %s", cliClient.Email)
411
+ jsonTemplate := `{"id":"","object":"chat.completion","created":123456,"model":"model","choices":[{"index":0,"message":{"role":"assistant","content":null,"reasoning_content":null,"tool_calls":null},"finish_reason":null,"native_finish_reason":null}]}`
412
+ respChan, errChan := cliClient.SendMessageStream(cliCtx, rawJson, modelName, contents, tools)
413
+ for {
414
+ select {
415
+ case <-c.Request.Context().Done():
416
+ if c.Request.Context().Err().Error() == "context canceled" {
417
+ log.Debugf("Client disconnected: %v", c.Request.Context().Err())
418
+ cliCancel()
419
+ return
420
+ }
421
+ case chunk, okStream := <-respChan:
422
+ if !okStream {
423
+ _, _ = fmt.Fprint(c.Writer, jsonTemplate)
424
+ flusher.Flush()
425
+ cliCancel()
426
+ return
427
+ } else {
428
+ jsonTemplate = h.convertCliToOpenAINonStream(jsonTemplate, chunk)
429
+ }
430
+ case err, okError := <-errChan:
431
+ if okError {
432
+ c.JSON(http.StatusInternalServerError, ErrorResponse{
433
+ Error: ErrorDetail{
434
+ Message: err.Error(),
435
+ Type: "server_error",
436
+ },
437
+ })
438
+ cliCancel()
439
+ return
440
+ }
441
+ case <-time.After(500 * time.Millisecond):
442
+ _, _ = c.Writer.Write([]byte("\n"))
443
+ flusher.Flush()
444
+ }
445
+ }
446
+ }
447
+
448
+ // handleStreamingResponse handles streaming responses
449
+ func (h *APIHandlers) handleStreamingResponse(c *gin.Context, rawJson []byte) {
450
+ c.Header("Content-Type", "text/event-stream")
451
+ c.Header("Cache-Control", "no-cache")
452
+ c.Header("Connection", "keep-alive")
453
+ c.Header("Access-Control-Allow-Origin", "*")
454
+
455
+ // Handle streaming manually
456
+ flusher, ok := c.Writer.(http.Flusher)
457
+ if !ok {
458
+ c.JSON(http.StatusInternalServerError, ErrorResponse{
459
+ Error: ErrorDetail{
460
+ Message: "Streaming not supported",
461
+ Type: "server_error",
462
+ },
463
+ })
464
+ return
465
+ }
466
+ modelName, contents, tools := h.prepareRequest(rawJson)
467
+ cliCtx, cliCancel := context.WithCancel(context.Background())
468
+ var cliClient *client.Client
469
+ defer func() {
470
+ if cliClient != nil {
471
+ cliClient.RequestMutex.Unlock()
472
+ }
473
+ }()
474
+
475
+ // Lock the mutex to update the last used page index
476
+ mutex.Lock()
477
+ startIndex := lastUsedClientIndex
478
+ currentIndex := (startIndex + 1) % len(h.cliClients)
479
+ lastUsedClientIndex = currentIndex
480
+ mutex.Unlock()
481
+
482
+ // Reorder the pages to start from the last used index
483
+ reorderedPages := make([]*client.Client, len(h.cliClients))
484
+ for i := 0; i < len(h.cliClients); i++ {
485
+ reorderedPages[i] = h.cliClients[(startIndex+1+i)%len(h.cliClients)]
486
+ }
487
+
488
+ locked := false
489
+ for i := 0; i < len(reorderedPages); i++ {
490
+ cliClient = reorderedPages[i]
491
+ if cliClient.RequestMutex.TryLock() {
492
+ locked = true
493
+ break
494
+ }
495
+ }
496
+ if !locked {
497
+ cliClient = h.cliClients[0]
498
+ cliClient.RequestMutex.Lock()
499
+ }
500
+
501
+ log.Debugf("Request use account: %s", cliClient.Email)
502
+ respChan, errChan := cliClient.SendMessageStream(cliCtx, rawJson, modelName, contents, tools)
503
+ for {
504
+ select {
505
+ case <-c.Request.Context().Done():
506
+ if c.Request.Context().Err().Error() == "context canceled" {
507
+ log.Debugf("Client disconnected: %v", c.Request.Context().Err())
508
+ cliCancel()
509
+ return
510
+ }
511
+ case chunk, okStream := <-respChan:
512
+ if !okStream {
513
+ _, _ = fmt.Fprintf(c.Writer, "data: [DONE]\n\n")
514
+ flusher.Flush()
515
+ cliCancel()
516
+ return
517
+ } else {
518
+ openAIFormat := h.convertCliToOpenAI(chunk)
519
+ if openAIFormat != "" {
520
+ _, _ = fmt.Fprintf(c.Writer, "data: %s\n\n", openAIFormat)
521
+ flusher.Flush()
522
+ }
523
+ }
524
+ case err, okError := <-errChan:
525
+ if okError {
526
+ c.JSON(http.StatusInternalServerError, ErrorResponse{
527
+ Error: ErrorDetail{
528
+ Message: err.Error(),
529
+ Type: "server_error",
530
+ },
531
+ })
532
+ cliCancel()
533
+ return
534
+ }
535
+ case <-time.After(500 * time.Millisecond):
536
+ _, _ = c.Writer.Write([]byte(": CLI-PROXY-API PROCESSING\n\n"))
537
+ flusher.Flush()
538
+ }
539
+ }
540
+ }
541
+
542
+ func (h *APIHandlers) convertCliToOpenAI(rawJson []byte) string {
543
+ // log.Debugf(string(rawJson))
544
+ template := `{"id":"","object":"chat.completion.chunk","created":12345,"model":"model","choices":[{"index":0,"delta":{"role":null,"content":null,"reasoning_content":null,"tool_calls":null},"finish_reason":null,"native_finish_reason":null}]}`
545
+
546
+ modelVersionResult := gjson.GetBytes(rawJson, "response.modelVersion")
547
+ if modelVersionResult.Exists() && modelVersionResult.Type == gjson.String {
548
+ template, _ = sjson.Set(template, "model", modelVersionResult.String())
549
+ }
550
+
551
+ createTimeResult := gjson.GetBytes(rawJson, "response.createTime")
552
+ if createTimeResult.Exists() && createTimeResult.Type == gjson.String {
553
+ t, err := time.Parse(time.RFC3339Nano, createTimeResult.String())
554
+ var unixTimestamp int64
555
+ if err == nil {
556
+ unixTimestamp = t.Unix()
557
+ } else {
558
+ unixTimestamp = time.Now().Unix()
559
+ }
560
+ template, _ = sjson.Set(template, "created", unixTimestamp)
561
+ }
562
+
563
+ responseIdResult := gjson.GetBytes(rawJson, "response.responseId")
564
+ if responseIdResult.Exists() && responseIdResult.Type == gjson.String {
565
+ template, _ = sjson.Set(template, "id", responseIdResult.String())
566
+ }
567
+
568
+ finishReasonResult := gjson.GetBytes(rawJson, "response.candidates.0.finishReason")
569
+ if finishReasonResult.Exists() && finishReasonResult.Type == gjson.String {
570
+ template, _ = sjson.Set(template, "choices.0.finish_reason", finishReasonResult.String())
571
+ template, _ = sjson.Set(template, "choices.0.native_finish_reason", finishReasonResult.String())
572
+ }
573
+
574
+ usageResult := gjson.GetBytes(rawJson, "response.usageMetadata")
575
+ candidatesTokenCountResult := usageResult.Get("candidatesTokenCount")
576
+ if candidatesTokenCountResult.Exists() && candidatesTokenCountResult.Type == gjson.Number {
577
+ template, _ = sjson.Set(template, "usage.completion_tokens", candidatesTokenCountResult.Int())
578
+ }
579
+ totalTokenCountResult := usageResult.Get("totalTokenCount")
580
+ if totalTokenCountResult.Exists() && totalTokenCountResult.Type == gjson.Number {
581
+ template, _ = sjson.Set(template, "usage.total_tokens", totalTokenCountResult.Int())
582
+ }
583
+ thoughtsTokenCountResult := usageResult.Get("thoughtsTokenCount")
584
+ promptTokenCountResult := usageResult.Get("promptTokenCount")
585
+ if promptTokenCountResult.Exists() && promptTokenCountResult.Type == gjson.Number {
586
+ if thoughtsTokenCountResult.Exists() && thoughtsTokenCountResult.Type == gjson.Number {
587
+ template, _ = sjson.Set(template, "usage.prompt_tokens", promptTokenCountResult.Int()+thoughtsTokenCountResult.Int())
588
+ } else {
589
+ template, _ = sjson.Set(template, "usage.prompt_tokens", promptTokenCountResult.Int())
590
+ }
591
+ }
592
+ if thoughtsTokenCountResult.Exists() && thoughtsTokenCountResult.Type == gjson.Number {
593
+ template, _ = sjson.Set(template, "usage.completion_tokens_details.reasoning_tokens", thoughtsTokenCountResult.Int())
594
+ }
595
+
596
+ partResult := gjson.GetBytes(rawJson, "response.candidates.0.content.parts.0")
597
+ partTextResult := partResult.Get("text")
598
+ functionCallResult := partResult.Get("functionCall")
599
+
600
+ if partTextResult.Exists() && partTextResult.Type == gjson.String {
601
+ partThoughtResult := partResult.Get("thought")
602
+ if partThoughtResult.Exists() && partThoughtResult.Type == gjson.True {
603
+ template, _ = sjson.Set(template, "choices.0.delta.reasoning_content", partTextResult.String())
604
+ } else {
605
+ template, _ = sjson.Set(template, "choices.0.delta.content", partTextResult.String())
606
+ }
607
+ template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
608
+ } else if functionCallResult.Exists() {
609
+ functionCallTemplate := `[{"id": "","type": "function","function": {"name": "","arguments": ""}}]`
610
+ fcNameResult := functionCallResult.Get("name")
611
+ if fcNameResult.Exists() && fcNameResult.Type == gjson.String {
612
+ functionCallTemplate, _ = sjson.Set(functionCallTemplate, "0.id", fcNameResult.String())
613
+ functionCallTemplate, _ = sjson.Set(functionCallTemplate, "0.function.name", fcNameResult.String())
614
+ }
615
+ fcArgsResult := functionCallResult.Get("args")
616
+ if fcArgsResult.Exists() && fcArgsResult.IsObject() {
617
+ functionCallTemplate, _ = sjson.Set(functionCallTemplate, "0.function.arguments", fcArgsResult.Raw)
618
+ }
619
+ template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
620
+ template, _ = sjson.SetRaw(template, "choices.0.delta.tool_calls", functionCallTemplate)
621
+ } else {
622
+ return ""
623
+ }
624
+
625
+ return template
626
+ }
627
+
628
+ func (h *APIHandlers) convertCliToOpenAINonStream(template string, rawJson []byte) string {
629
+ modelVersionResult := gjson.GetBytes(rawJson, "response.modelVersion")
630
+ if modelVersionResult.Exists() && modelVersionResult.Type == gjson.String {
631
+ template, _ = sjson.Set(template, "model", modelVersionResult.String())
632
+ }
633
+
634
+ createTimeResult := gjson.GetBytes(rawJson, "response.createTime")
635
+ if createTimeResult.Exists() && createTimeResult.Type == gjson.String {
636
+ t, err := time.Parse(time.RFC3339Nano, createTimeResult.String())
637
+ var unixTimestamp int64
638
+ if err == nil {
639
+ unixTimestamp = t.Unix()
640
+ } else {
641
+ unixTimestamp = time.Now().Unix()
642
+ }
643
+ template, _ = sjson.Set(template, "created", unixTimestamp)
644
+ }
645
+
646
+ responseIdResult := gjson.GetBytes(rawJson, "response.responseId")
647
+ if responseIdResult.Exists() && responseIdResult.Type == gjson.String {
648
+ template, _ = sjson.Set(template, "id", responseIdResult.String())
649
+ }
650
+
651
+ finishReasonResult := gjson.GetBytes(rawJson, "response.candidates.0.finishReason")
652
+ if finishReasonResult.Exists() && finishReasonResult.Type == gjson.String {
653
+ template, _ = sjson.Set(template, "choices.0.finish_reason", finishReasonResult.String())
654
+ template, _ = sjson.Set(template, "choices.0.native_finish_reason", finishReasonResult.String())
655
+ }
656
+
657
+ usageResult := gjson.GetBytes(rawJson, "response.usageMetadata")
658
+ candidatesTokenCountResult := usageResult.Get("candidatesTokenCount")
659
+ if candidatesTokenCountResult.Exists() && candidatesTokenCountResult.Type == gjson.Number {
660
+ template, _ = sjson.Set(template, "usage.completion_tokens", candidatesTokenCountResult.Int())
661
+ }
662
+ totalTokenCountResult := usageResult.Get("totalTokenCount")
663
+ if totalTokenCountResult.Exists() && totalTokenCountResult.Type == gjson.Number {
664
+ template, _ = sjson.Set(template, "usage.total_tokens", totalTokenCountResult.Int())
665
+ }
666
+ thoughtsTokenCountResult := usageResult.Get("thoughtsTokenCount")
667
+ promptTokenCountResult := usageResult.Get("promptTokenCount")
668
+ if promptTokenCountResult.Exists() && promptTokenCountResult.Type == gjson.Number {
669
+ if thoughtsTokenCountResult.Exists() && thoughtsTokenCountResult.Type == gjson.Number {
670
+ template, _ = sjson.Set(template, "usage.prompt_tokens", promptTokenCountResult.Int()+thoughtsTokenCountResult.Int())
671
+ } else {
672
+ template, _ = sjson.Set(template, "usage.prompt_tokens", promptTokenCountResult.Int())
673
+ }
674
+ }
675
+ if thoughtsTokenCountResult.Exists() && thoughtsTokenCountResult.Type == gjson.Number {
676
+ template, _ = sjson.Set(template, "usage.completion_tokens_details.reasoning_tokens", thoughtsTokenCountResult.Int())
677
+ }
678
+
679
+ partResult := gjson.GetBytes(rawJson, "response.candidates.0.content.parts.0")
680
+ partTextResult := partResult.Get("text")
681
+ functionCallResult := partResult.Get("functionCall")
682
+
683
+ if partTextResult.Exists() && partTextResult.Type == gjson.String {
684
+ partThoughtResult := partResult.Get("thought")
685
+ if partThoughtResult.Exists() && partThoughtResult.Type == gjson.True {
686
+ reasoningContentResult := gjson.Get(template, "choices.0.message.reasoning_content")
687
+ if reasoningContentResult.Type == gjson.String {
688
+ template, _ = sjson.Set(template, "choices.0.message.reasoning_content", reasoningContentResult.String()+partTextResult.String())
689
+ } else {
690
+ template, _ = sjson.Set(template, "choices.0.message.reasoning_content", partTextResult.String())
691
+ }
692
+ } else {
693
+ reasoningContentResult := gjson.Get(template, "choices.0.message.content")
694
+ if reasoningContentResult.Type == gjson.String {
695
+ template, _ = sjson.Set(template, "choices.0.message.content", reasoningContentResult.String()+partTextResult.String())
696
+ } else {
697
+ template, _ = sjson.Set(template, "choices.0.message.content", partTextResult.String())
698
+ }
699
+ }
700
+ template, _ = sjson.Set(template, "choices.0.message.role", "assistant")
701
+ } else if functionCallResult.Exists() {
702
+ toolCallsResult := gjson.Get(template, "choices.0.message.tool_calls")
703
+ if !toolCallsResult.Exists() || toolCallsResult.Type == gjson.Null {
704
+ template, _ = sjson.SetRaw(template, "choices.0.message.tool_calls", `[]`)
705
+ }
706
+
707
+ functionCallItemTemplate := `{"id": "","type": "function","function": {"name": "","arguments": ""}}`
708
+ fcNameResult := functionCallResult.Get("name")
709
+ if fcNameResult.Exists() && fcNameResult.Type == gjson.String {
710
+ functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "id", fcNameResult.String())
711
+ functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "function.name", fcNameResult.String())
712
+ }
713
+ fcArgsResult := functionCallResult.Get("args")
714
+ if fcArgsResult.Exists() && fcArgsResult.IsObject() {
715
+ functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "function.arguments", fcArgsResult.Raw)
716
+ }
717
+ template, _ = sjson.Set(template, "choices.0.message.role", "assistant")
718
+ template, _ = sjson.SetRaw(template, "choices.0.message.tool_calls.-1", functionCallItemTemplate)
719
+ } else {
720
+ return ""
721
+ }
722
+
723
+ return template
724
+ }
internal/api/mine-type.go ADDED
@@ -0,0 +1,736 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package api
2
+
3
+ var MimeTypes = map[string]string{
4
+ "ez": "application/andrew-inset",
5
+ "aw": "application/applixware",
6
+ "atom": "application/atom+xml",
7
+ "atomcat": "application/atomcat+xml",
8
+ "atomsvc": "application/atomsvc+xml",
9
+ "ccxml": "application/ccxml+xml",
10
+ "cdmia": "application/cdmi-capability",
11
+ "cdmic": "application/cdmi-container",
12
+ "cdmid": "application/cdmi-domain",
13
+ "cdmio": "application/cdmi-object",
14
+ "cdmiq": "application/cdmi-queue",
15
+ "cu": "application/cu-seeme",
16
+ "davmount": "application/davmount+xml",
17
+ "dbk": "application/docbook+xml",
18
+ "dssc": "application/dssc+der",
19
+ "xdssc": "application/dssc+xml",
20
+ "ecma": "application/ecmascript",
21
+ "emma": "application/emma+xml",
22
+ "epub": "application/epub+zip",
23
+ "exi": "application/exi",
24
+ "pfr": "application/font-tdpfr",
25
+ "gml": "application/gml+xml",
26
+ "gpx": "application/gpx+xml",
27
+ "gxf": "application/gxf",
28
+ "stk": "application/hyperstudio",
29
+ "ink": "application/inkml+xml",
30
+ "ipfix": "application/ipfix",
31
+ "jar": "application/java-archive",
32
+ "ser": "application/java-serialized-object",
33
+ "class": "application/java-vm",
34
+ "js": "application/javascript",
35
+ "json": "application/json",
36
+ "jsonml": "application/jsonml+json",
37
+ "lostxml": "application/lost+xml",
38
+ "hqx": "application/mac-binhex40",
39
+ "cpt": "application/mac-compactpro",
40
+ "mads": "application/mads+xml",
41
+ "mrc": "application/marc",
42
+ "mrcx": "application/marcxml+xml",
43
+ "ma": "application/mathematica",
44
+ "mathml": "application/mathml+xml",
45
+ "mbox": "application/mbox",
46
+ "mscml": "application/mediaservercontrol+xml",
47
+ "metalink": "application/metalink+xml",
48
+ "meta4": "application/metalink4+xml",
49
+ "mets": "application/mets+xml",
50
+ "mods": "application/mods+xml",
51
+ "m21": "application/mp21",
52
+ "mp4s": "application/mp4",
53
+ "doc": "application/msword",
54
+ "mxf": "application/mxf",
55
+ "bin": "application/octet-stream",
56
+ "oda": "application/oda",
57
+ "opf": "application/oebps-package+xml",
58
+ "ogx": "application/ogg",
59
+ "omdoc": "application/omdoc+xml",
60
+ "onepkg": "application/onenote",
61
+ "oxps": "application/oxps",
62
+ "xer": "application/patch-ops-error+xml",
63
+ "pdf": "application/pdf",
64
+ "pgp": "application/pgp-encrypted",
65
+ "asc": "application/pgp-signature",
66
+ "prf": "application/pics-rules",
67
+ "p10": "application/pkcs10",
68
+ "p7c": "application/pkcs7-mime",
69
+ "p7s": "application/pkcs7-signature",
70
+ "p8": "application/pkcs8",
71
+ "ac": "application/pkix-attr-cert",
72
+ "cer": "application/pkix-cert",
73
+ "crl": "application/pkix-crl",
74
+ "pkipath": "application/pkix-pkipath",
75
+ "pki": "application/pkixcmp",
76
+ "pls": "application/pls+xml",
77
+ "ai": "application/postscript",
78
+ "cww": "application/prs.cww",
79
+ "pskcxml": "application/pskc+xml",
80
+ "rdf": "application/rdf+xml",
81
+ "rif": "application/reginfo+xml",
82
+ "rnc": "application/relax-ng-compact-syntax",
83
+ "rld": "application/resource-lists-diff+xml",
84
+ "rl": "application/resource-lists+xml",
85
+ "rs": "application/rls-services+xml",
86
+ "gbr": "application/rpki-ghostbusters",
87
+ "mft": "application/rpki-manifest",
88
+ "roa": "application/rpki-roa",
89
+ "rsd": "application/rsd+xml",
90
+ "rss": "application/rss+xml",
91
+ "rtf": "application/rtf",
92
+ "sbml": "application/sbml+xml",
93
+ "scq": "application/scvp-cv-request",
94
+ "scs": "application/scvp-cv-response",
95
+ "spq": "application/scvp-vp-request",
96
+ "spp": "application/scvp-vp-response",
97
+ "sdp": "application/sdp",
98
+ "setpay": "application/set-payment-initiation",
99
+ "setreg": "application/set-registration-initiation",
100
+ "shf": "application/shf+xml",
101
+ "smi": "application/smil+xml",
102
+ "rq": "application/sparql-query",
103
+ "srx": "application/sparql-results+xml",
104
+ "gram": "application/srgs",
105
+ "grxml": "application/srgs+xml",
106
+ "sru": "application/sru+xml",
107
+ "ssdl": "application/ssdl+xml",
108
+ "ssml": "application/ssml+xml",
109
+ "tei": "application/tei+xml",
110
+ "tfi": "application/thraud+xml",
111
+ "tsd": "application/timestamped-data",
112
+ "plb": "application/vnd.3gpp.pic-bw-large",
113
+ "psb": "application/vnd.3gpp.pic-bw-small",
114
+ "pvb": "application/vnd.3gpp.pic-bw-var",
115
+ "tcap": "application/vnd.3gpp2.tcap",
116
+ "pwn": "application/vnd.3m.post-it-notes",
117
+ "aso": "application/vnd.accpac.simply.aso",
118
+ "imp": "application/vnd.accpac.simply.imp",
119
+ "acu": "application/vnd.acucobol",
120
+ "acutc": "application/vnd.acucorp",
121
+ "air": "application/vnd.adobe.air-application-installer-package+zip",
122
+ "fcdt": "application/vnd.adobe.formscentral.fcdt",
123
+ "fxp": "application/vnd.adobe.fxp",
124
+ "xdp": "application/vnd.adobe.xdp+xml",
125
+ "xfdf": "application/vnd.adobe.xfdf",
126
+ "ahead": "application/vnd.ahead.space",
127
+ "azf": "application/vnd.airzip.filesecure.azf",
128
+ "azs": "application/vnd.airzip.filesecure.azs",
129
+ "azw": "application/vnd.amazon.ebook",
130
+ "acc": "application/vnd.americandynamics.acc",
131
+ "ami": "application/vnd.amiga.ami",
132
+ "apk": "application/vnd.android.package-archive",
133
+ "cii": "application/vnd.anser-web-certificate-issue-initiation",
134
+ "fti": "application/vnd.anser-web-funds-transfer-initiation",
135
+ "atx": "application/vnd.antix.game-component",
136
+ "mpkg": "application/vnd.apple.installer+xml",
137
+ "m3u8": "application/vnd.apple.mpegurl",
138
+ "swi": "application/vnd.aristanetworks.swi",
139
+ "iota": "application/vnd.astraea-software.iota",
140
+ "aep": "application/vnd.audiograph",
141
+ "mpm": "application/vnd.blueice.multipass",
142
+ "bmi": "application/vnd.bmi",
143
+ "rep": "application/vnd.businessobjects",
144
+ "cdxml": "application/vnd.chemdraw+xml",
145
+ "mmd": "application/vnd.chipnuts.karaoke-mmd",
146
+ "cdy": "application/vnd.cinderella",
147
+ "cla": "application/vnd.claymore",
148
+ "rp9": "application/vnd.cloanto.rp9",
149
+ "c4d": "application/vnd.clonk.c4group",
150
+ "c11amc": "application/vnd.cluetrust.cartomobile-config",
151
+ "c11amz": "application/vnd.cluetrust.cartomobile-config-pkg",
152
+ "csp": "application/vnd.commonspace",
153
+ "cdbcmsg": "application/vnd.contact.cmsg",
154
+ "cmc": "application/vnd.cosmocaller",
155
+ "clkx": "application/vnd.crick.clicker",
156
+ "clkk": "application/vnd.crick.clicker.keyboard",
157
+ "clkp": "application/vnd.crick.clicker.palette",
158
+ "clkt": "application/vnd.crick.clicker.template",
159
+ "clkw": "application/vnd.crick.clicker.wordbank",
160
+ "wbs": "application/vnd.criticaltools.wbs+xml",
161
+ "pml": "application/vnd.ctc-posml",
162
+ "ppd": "application/vnd.cups-ppd",
163
+ "car": "application/vnd.curl.car",
164
+ "pcurl": "application/vnd.curl.pcurl",
165
+ "dart": "application/vnd.dart",
166
+ "rdz": "application/vnd.data-vision.rdz",
167
+ "uvd": "application/vnd.dece.data",
168
+ "fe_launch": "application/vnd.denovo.fcselayout-link",
169
+ "dna": "application/vnd.dna",
170
+ "mlp": "application/vnd.dolby.mlp",
171
+ "dpg": "application/vnd.dpgraph",
172
+ "dfac": "application/vnd.dreamfactory",
173
+ "kpxx": "application/vnd.ds-keypoint",
174
+ "ait": "application/vnd.dvb.ait",
175
+ "svc": "application/vnd.dvb.service",
176
+ "geo": "application/vnd.dynageo",
177
+ "mag": "application/vnd.ecowin.chart",
178
+ "nml": "application/vnd.enliven",
179
+ "esf": "application/vnd.epson.esf",
180
+ "msf": "application/vnd.epson.msf",
181
+ "qam": "application/vnd.epson.quickanime",
182
+ "slt": "application/vnd.epson.salt",
183
+ "ssf": "application/vnd.epson.ssf",
184
+ "es3": "application/vnd.eszigno3+xml",
185
+ "ez2": "application/vnd.ezpix-album",
186
+ "ez3": "application/vnd.ezpix-package",
187
+ "fdf": "application/vnd.fdf",
188
+ "mseed": "application/vnd.fdsn.mseed",
189
+ "dataless": "application/vnd.fdsn.seed",
190
+ "gph": "application/vnd.flographit",
191
+ "ftc": "application/vnd.fluxtime.clip",
192
+ "book": "application/vnd.framemaker",
193
+ "fnc": "application/vnd.frogans.fnc",
194
+ "ltf": "application/vnd.frogans.ltf",
195
+ "fsc": "application/vnd.fsc.weblaunch",
196
+ "oas": "application/vnd.fujitsu.oasys",
197
+ "oa2": "application/vnd.fujitsu.oasys2",
198
+ "oa3": "application/vnd.fujitsu.oasys3",
199
+ "fg5": "application/vnd.fujitsu.oasysgp",
200
+ "bh2": "application/vnd.fujitsu.oasysprs",
201
+ "ddd": "application/vnd.fujixerox.ddd",
202
+ "xdw": "application/vnd.fujixerox.docuworks",
203
+ "xbd": "application/vnd.fujixerox.docuworks.binder",
204
+ "fzs": "application/vnd.fuzzysheet",
205
+ "txd": "application/vnd.genomatix.tuxedo",
206
+ "ggb": "application/vnd.geogebra.file",
207
+ "ggt": "application/vnd.geogebra.tool",
208
+ "gex": "application/vnd.geometry-explorer",
209
+ "gxt": "application/vnd.geonext",
210
+ "g2w": "application/vnd.geoplan",
211
+ "g3w": "application/vnd.geospace",
212
+ "gmx": "application/vnd.gmx",
213
+ "kml": "application/vnd.google-earth.kml+xml",
214
+ "kmz": "application/vnd.google-earth.kmz",
215
+ "gqf": "application/vnd.grafeq",
216
+ "gac": "application/vnd.groove-account",
217
+ "ghf": "application/vnd.groove-help",
218
+ "gim": "application/vnd.groove-identity-message",
219
+ "grv": "application/vnd.groove-injector",
220
+ "gtm": "application/vnd.groove-tool-message",
221
+ "tpl": "application/vnd.groove-tool-template",
222
+ "vcg": "application/vnd.groove-vcard",
223
+ "hal": "application/vnd.hal+xml",
224
+ "zmm": "application/vnd.handheld-entertainment+xml",
225
+ "hbci": "application/vnd.hbci",
226
+ "les": "application/vnd.hhe.lesson-player",
227
+ "hpgl": "application/vnd.hp-hpgl",
228
+ "hpid": "application/vnd.hp-hpid",
229
+ "hps": "application/vnd.hp-hps",
230
+ "jlt": "application/vnd.hp-jlyt",
231
+ "pcl": "application/vnd.hp-pcl",
232
+ "pclxl": "application/vnd.hp-pclxl",
233
+ "sfd-hdstx": "application/vnd.hydrostatix.sof-data",
234
+ "mpy": "application/vnd.ibm.minipay",
235
+ "afp": "application/vnd.ibm.modcap",
236
+ "irm": "application/vnd.ibm.rights-management",
237
+ "sc": "application/vnd.ibm.secure-container",
238
+ "icc": "application/vnd.iccprofile",
239
+ "igl": "application/vnd.igloader",
240
+ "ivp": "application/vnd.immervision-ivp",
241
+ "ivu": "application/vnd.immervision-ivu",
242
+ "igm": "application/vnd.insors.igm",
243
+ "xpw": "application/vnd.intercon.formnet",
244
+ "i2g": "application/vnd.intergeo",
245
+ "qbo": "application/vnd.intu.qbo",
246
+ "qfx": "application/vnd.intu.qfx",
247
+ "rcprofile": "application/vnd.ipunplugged.rcprofile",
248
+ "irp": "application/vnd.irepository.package+xml",
249
+ "xpr": "application/vnd.is-xpr",
250
+ "fcs": "application/vnd.isac.fcs",
251
+ "jam": "application/vnd.jam",
252
+ "rms": "application/vnd.jcp.javame.midlet-rms",
253
+ "jisp": "application/vnd.jisp",
254
+ "joda": "application/vnd.joost.joda-archive",
255
+ "ktr": "application/vnd.kahootz",
256
+ "karbon": "application/vnd.kde.karbon",
257
+ "chrt": "application/vnd.kde.kchart",
258
+ "kfo": "application/vnd.kde.kformula",
259
+ "flw": "application/vnd.kde.kivio",
260
+ "kon": "application/vnd.kde.kontour",
261
+ "kpr": "application/vnd.kde.kpresenter",
262
+ "ksp": "application/vnd.kde.kspread",
263
+ "kwd": "application/vnd.kde.kword",
264
+ "htke": "application/vnd.kenameaapp",
265
+ "kia": "application/vnd.kidspiration",
266
+ "kne": "application/vnd.kinar",
267
+ "skd": "application/vnd.koan",
268
+ "sse": "application/vnd.kodak-descriptor",
269
+ "lasxml": "application/vnd.las.las+xml",
270
+ "lbd": "application/vnd.llamagraphics.life-balance.desktop",
271
+ "lbe": "application/vnd.llamagraphics.life-balance.exchange+xml",
272
+ "123": "application/vnd.lotus-1-2-3",
273
+ "apr": "application/vnd.lotus-approach",
274
+ "pre": "application/vnd.lotus-freelance",
275
+ "nsf": "application/vnd.lotus-notes",
276
+ "org": "application/vnd.lotus-organizer",
277
+ "scm": "application/vnd.lotus-screencam",
278
+ "lwp": "application/vnd.lotus-wordpro",
279
+ "portpkg": "application/vnd.macports.portpkg",
280
+ "mcd": "application/vnd.mcd",
281
+ "mc1": "application/vnd.medcalcdata",
282
+ "cdkey": "application/vnd.mediastation.cdkey",
283
+ "mwf": "application/vnd.mfer",
284
+ "mfm": "application/vnd.mfmp",
285
+ "flo": "application/vnd.micrografx.flo",
286
+ "igx": "application/vnd.micrografx.igx",
287
+ "mif": "application/vnd.mif",
288
+ "daf": "application/vnd.mobius.daf",
289
+ "dis": "application/vnd.mobius.dis",
290
+ "mbk": "application/vnd.mobius.mbk",
291
+ "mqy": "application/vnd.mobius.mqy",
292
+ "msl": "application/vnd.mobius.msl",
293
+ "plc": "application/vnd.mobius.plc",
294
+ "txf": "application/vnd.mobius.txf",
295
+ "mpn": "application/vnd.mophun.application",
296
+ "mpc": "application/vnd.mophun.certificate",
297
+ "xul": "application/vnd.mozilla.xul+xml",
298
+ "cil": "application/vnd.ms-artgalry",
299
+ "cab": "application/vnd.ms-cab-compressed",
300
+ "xls": "application/vnd.ms-excel",
301
+ "xlam": "application/vnd.ms-excel.addin.macroenabled.12",
302
+ "xlsb": "application/vnd.ms-excel.sheet.binary.macroenabled.12",
303
+ "xlsm": "application/vnd.ms-excel.sheet.macroenabled.12",
304
+ "xltm": "application/vnd.ms-excel.template.macroenabled.12",
305
+ "eot": "application/vnd.ms-fontobject",
306
+ "chm": "application/vnd.ms-htmlhelp",
307
+ "ims": "application/vnd.ms-ims",
308
+ "lrm": "application/vnd.ms-lrm",
309
+ "thmx": "application/vnd.ms-officetheme",
310
+ "cat": "application/vnd.ms-pki.seccat",
311
+ "stl": "application/vnd.ms-pki.stl",
312
+ "ppt": "application/vnd.ms-powerpoint",
313
+ "ppam": "application/vnd.ms-powerpoint.addin.macroenabled.12",
314
+ "pptm": "application/vnd.ms-powerpoint.presentation.macroenabled.12",
315
+ "sldm": "application/vnd.ms-powerpoint.slide.macroenabled.12",
316
+ "ppsm": "application/vnd.ms-powerpoint.slideshow.macroenabled.12",
317
+ "potm": "application/vnd.ms-powerpoint.template.macroenabled.12",
318
+ "mpp": "application/vnd.ms-project",
319
+ "docm": "application/vnd.ms-word.document.macroenabled.12",
320
+ "dotm": "application/vnd.ms-word.template.macroenabled.12",
321
+ "wps": "application/vnd.ms-works",
322
+ "wpl": "application/vnd.ms-wpl",
323
+ "xps": "application/vnd.ms-xpsdocument",
324
+ "mseq": "application/vnd.mseq",
325
+ "mus": "application/vnd.musician",
326
+ "msty": "application/vnd.muvee.style",
327
+ "taglet": "application/vnd.mynfc",
328
+ "nlu": "application/vnd.neurolanguage.nlu",
329
+ "nitf": "application/vnd.nitf",
330
+ "nnd": "application/vnd.noblenet-directory",
331
+ "nns": "application/vnd.noblenet-sealer",
332
+ "nnw": "application/vnd.noblenet-web",
333
+ "ngdat": "application/vnd.nokia.n-gage.data",
334
+ "n-gage": "application/vnd.nokia.n-gage.symbian.install",
335
+ "rpst": "application/vnd.nokia.radio-preset",
336
+ "rpss": "application/vnd.nokia.radio-presets",
337
+ "edm": "application/vnd.novadigm.edm",
338
+ "edx": "application/vnd.novadigm.edx",
339
+ "ext": "application/vnd.novadigm.ext",
340
+ "odc": "application/vnd.oasis.opendocument.chart",
341
+ "otc": "application/vnd.oasis.opendocument.chart-template",
342
+ "odb": "application/vnd.oasis.opendocument.database",
343
+ "odf": "application/vnd.oasis.opendocument.formula",
344
+ "odft": "application/vnd.oasis.opendocument.formula-template",
345
+ "odg": "application/vnd.oasis.opendocument.graphics",
346
+ "otg": "application/vnd.oasis.opendocument.graphics-template",
347
+ "odi": "application/vnd.oasis.opendocument.image",
348
+ "oti": "application/vnd.oasis.opendocument.image-template",
349
+ "odp": "application/vnd.oasis.opendocument.presentation",
350
+ "otp": "application/vnd.oasis.opendocument.presentation-template",
351
+ "ods": "application/vnd.oasis.opendocument.spreadsheet",
352
+ "ots": "application/vnd.oasis.opendocument.spreadsheet-template",
353
+ "odt": "application/vnd.oasis.opendocument.text",
354
+ "odm": "application/vnd.oasis.opendocument.text-master",
355
+ "ott": "application/vnd.oasis.opendocument.text-template",
356
+ "oth": "application/vnd.oasis.opendocument.text-web",
357
+ "xo": "application/vnd.olpc-sugar",
358
+ "dd2": "application/vnd.oma.dd2+xml",
359
+ "oxt": "application/vnd.openofficeorg.extension",
360
+ "pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
361
+ "sldx": "application/vnd.openxmlformats-officedocument.presentationml.slide",
362
+ "ppsx": "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
363
+ "potx": "application/vnd.openxmlformats-officedocument.presentationml.template",
364
+ "xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
365
+ "xltx": "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
366
+ "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
367
+ "dotx": "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
368
+ "mgp": "application/vnd.osgeo.mapguide.package",
369
+ "dp": "application/vnd.osgi.dp",
370
+ "esa": "application/vnd.osgi.subsystem",
371
+ "oprc": "application/vnd.palm",
372
+ "paw": "application/vnd.pawaafile",
373
+ "str": "application/vnd.pg.format",
374
+ "ei6": "application/vnd.pg.osasli",
375
+ "efif": "application/vnd.picsel",
376
+ "wg": "application/vnd.pmi.widget",
377
+ "plf": "application/vnd.pocketlearn",
378
+ "pbd": "application/vnd.powerbuilder6",
379
+ "box": "application/vnd.previewsystems.box",
380
+ "mgz": "application/vnd.proteus.magazine",
381
+ "qps": "application/vnd.publishare-delta-tree",
382
+ "ptid": "application/vnd.pvi.ptid1",
383
+ "qwd": "application/vnd.quark.quarkxpress",
384
+ "bed": "application/vnd.realvnc.bed",
385
+ "mxl": "application/vnd.recordare.musicxml",
386
+ "musicxml": "application/vnd.recordare.musicxml+xml",
387
+ "cryptonote": "application/vnd.rig.cryptonote",
388
+ "cod": "application/vnd.rim.cod",
389
+ "rm": "application/vnd.rn-realmedia",
390
+ "rmvb": "application/vnd.rn-realmedia-vbr",
391
+ "link66": "application/vnd.route66.link66+xml",
392
+ "st": "application/vnd.sailingtracker.track",
393
+ "see": "application/vnd.seemail",
394
+ "sema": "application/vnd.sema",
395
+ "semd": "application/vnd.semd",
396
+ "semf": "application/vnd.semf",
397
+ "ifm": "application/vnd.shana.informed.formdata",
398
+ "itp": "application/vnd.shana.informed.formtemplate",
399
+ "iif": "application/vnd.shana.informed.interchange",
400
+ "ipk": "application/vnd.shana.informed.package",
401
+ "twd": "application/vnd.simtech-mindmapper",
402
+ "mmf": "application/vnd.smaf",
403
+ "teacher": "application/vnd.smart.teacher",
404
+ "sdkd": "application/vnd.solent.sdkm+xml",
405
+ "dxp": "application/vnd.spotfire.dxp",
406
+ "sfs": "application/vnd.spotfire.sfs",
407
+ "sdc": "application/vnd.stardivision.calc",
408
+ "sda": "application/vnd.stardivision.draw",
409
+ "sdd": "application/vnd.stardivision.impress",
410
+ "smf": "application/vnd.stardivision.math",
411
+ "sdw": "application/vnd.stardivision.writer",
412
+ "sgl": "application/vnd.stardivision.writer-global",
413
+ "smzip": "application/vnd.stepmania.package",
414
+ "sm": "application/vnd.stepmania.stepchart",
415
+ "sxc": "application/vnd.sun.xml.calc",
416
+ "stc": "application/vnd.sun.xml.calc.template",
417
+ "sxd": "application/vnd.sun.xml.draw",
418
+ "std": "application/vnd.sun.xml.draw.template",
419
+ "sxi": "application/vnd.sun.xml.impress",
420
+ "sti": "application/vnd.sun.xml.impress.template",
421
+ "sxm": "application/vnd.sun.xml.math",
422
+ "sxw": "application/vnd.sun.xml.writer",
423
+ "sxg": "application/vnd.sun.xml.writer.global",
424
+ "stw": "application/vnd.sun.xml.writer.template",
425
+ "sus": "application/vnd.sus-calendar",
426
+ "svd": "application/vnd.svd",
427
+ "sis": "application/vnd.symbian.install",
428
+ "bdm": "application/vnd.syncml.dm+wbxml",
429
+ "xdm": "application/vnd.syncml.dm+xml",
430
+ "xsm": "application/vnd.syncml+xml",
431
+ "tao": "application/vnd.tao.intent-module-archive",
432
+ "cap": "application/vnd.tcpdump.pcap",
433
+ "tmo": "application/vnd.tmobile-livetv",
434
+ "tpt": "application/vnd.trid.tpt",
435
+ "mxs": "application/vnd.triscape.mxs",
436
+ "tra": "application/vnd.trueapp",
437
+ "ufd": "application/vnd.ufdl",
438
+ "utz": "application/vnd.uiq.theme",
439
+ "umj": "application/vnd.umajin",
440
+ "unityweb": "application/vnd.unity",
441
+ "uoml": "application/vnd.uoml+xml",
442
+ "vcx": "application/vnd.vcx",
443
+ "vss": "application/vnd.visio",
444
+ "vis": "application/vnd.visionary",
445
+ "vsf": "application/vnd.vsf",
446
+ "wbxml": "application/vnd.wap.wbxml",
447
+ "wmlc": "application/vnd.wap.wmlc",
448
+ "wmlsc": "application/vnd.wap.wmlscriptc",
449
+ "wtb": "application/vnd.webturbo",
450
+ "nbp": "application/vnd.wolfram.player",
451
+ "wpd": "application/vnd.wordperfect",
452
+ "wqd": "application/vnd.wqd",
453
+ "stf": "application/vnd.wt.stf",
454
+ "xar": "application/vnd.xara",
455
+ "xfdl": "application/vnd.xfdl",
456
+ "hvd": "application/vnd.yamaha.hv-dic",
457
+ "hvs": "application/vnd.yamaha.hv-script",
458
+ "hvp": "application/vnd.yamaha.hv-voice",
459
+ "osf": "application/vnd.yamaha.openscoreformat",
460
+ "osfpvg": "application/vnd.yamaha.openscoreformat.osfpvg+xml",
461
+ "saf": "application/vnd.yamaha.smaf-audio",
462
+ "spf": "application/vnd.yamaha.smaf-phrase",
463
+ "cmp": "application/vnd.yellowriver-custom-menu",
464
+ "zir": "application/vnd.zul",
465
+ "zaz": "application/vnd.zzazz.deck+xml",
466
+ "vxml": "application/voicexml+xml",
467
+ "wgt": "application/widget",
468
+ "hlp": "application/winhlp",
469
+ "wsdl": "application/wsdl+xml",
470
+ "wspolicy": "application/wspolicy+xml",
471
+ "7z": "application/x-7z-compressed",
472
+ "abw": "application/x-abiword",
473
+ "ace": "application/x-ace-compressed",
474
+ "dmg": "application/x-apple-diskimage",
475
+ "aab": "application/x-authorware-bin",
476
+ "aam": "application/x-authorware-map",
477
+ "aas": "application/x-authorware-seg",
478
+ "bcpio": "application/x-bcpio",
479
+ "torrent": "application/x-bittorrent",
480
+ "blb": "application/x-blorb",
481
+ "bz": "application/x-bzip",
482
+ "bz2": "application/x-bzip2",
483
+ "cbr": "application/x-cbr",
484
+ "vcd": "application/x-cdlink",
485
+ "cfs": "application/x-cfs-compressed",
486
+ "chat": "application/x-chat",
487
+ "pgn": "application/x-chess-pgn",
488
+ "nsc": "application/x-conference",
489
+ "cpio": "application/x-cpio",
490
+ "csh": "application/x-csh",
491
+ "deb": "application/x-debian-package",
492
+ "dgc": "application/x-dgc-compressed",
493
+ "cct": "application/x-director",
494
+ "wad": "application/x-doom",
495
+ "ncx": "application/x-dtbncx+xml",
496
+ "dtb": "application/x-dtbook+xml",
497
+ "res": "application/x-dtbresource+xml",
498
+ "dvi": "application/x-dvi",
499
+ "evy": "application/x-envoy",
500
+ "eva": "application/x-eva",
501
+ "bdf": "application/x-font-bdf",
502
+ "gsf": "application/x-font-ghostscript",
503
+ "psf": "application/x-font-linux-psf",
504
+ "pcf": "application/x-font-pcf",
505
+ "snf": "application/x-font-snf",
506
+ "afm": "application/x-font-type1",
507
+ "arc": "application/x-freearc",
508
+ "spl": "application/x-futuresplash",
509
+ "gca": "application/x-gca-compressed",
510
+ "ulx": "application/x-glulx",
511
+ "gnumeric": "application/x-gnumeric",
512
+ "gramps": "application/x-gramps-xml",
513
+ "gtar": "application/x-gtar",
514
+ "hdf": "application/x-hdf",
515
+ "install": "application/x-install-instructions",
516
+ "iso": "application/x-iso9660-image",
517
+ "jnlp": "application/x-java-jnlp-file",
518
+ "latex": "application/x-latex",
519
+ "lzh": "application/x-lzh-compressed",
520
+ "mie": "application/x-mie",
521
+ "mobi": "application/x-mobipocket-ebook",
522
+ "application": "application/x-ms-application",
523
+ "lnk": "application/x-ms-shortcut",
524
+ "wmd": "application/x-ms-wmd",
525
+ "wmz": "application/x-ms-wmz",
526
+ "xbap": "application/x-ms-xbap",
527
+ "mdb": "application/x-msaccess",
528
+ "obd": "application/x-msbinder",
529
+ "crd": "application/x-mscardfile",
530
+ "clp": "application/x-msclip",
531
+ "mny": "application/x-msmoney",
532
+ "pub": "application/x-mspublisher",
533
+ "scd": "application/x-msschedule",
534
+ "trm": "application/x-msterminal",
535
+ "wri": "application/x-mswrite",
536
+ "nzb": "application/x-nzb",
537
+ "p12": "application/x-pkcs12",
538
+ "p7b": "application/x-pkcs7-certificates",
539
+ "p7r": "application/x-pkcs7-certreqresp",
540
+ "rar": "application/x-rar-compressed",
541
+ "ris": "application/x-research-info-systems",
542
+ "sh": "application/x-sh",
543
+ "shar": "application/x-shar",
544
+ "swf": "application/x-shockwave-flash",
545
+ "xap": "application/x-silverlight-app",
546
+ "sql": "application/x-sql",
547
+ "sit": "application/x-stuffit",
548
+ "sitx": "application/x-stuffitx",
549
+ "srt": "application/x-subrip",
550
+ "sv4cpio": "application/x-sv4cpio",
551
+ "sv4crc": "application/x-sv4crc",
552
+ "t3": "application/x-t3vm-image",
553
+ "gam": "application/x-tads",
554
+ "tar": "application/x-tar",
555
+ "tcl": "application/x-tcl",
556
+ "tex": "application/x-tex",
557
+ "tfm": "application/x-tex-tfm",
558
+ "texi": "application/x-texinfo",
559
+ "obj": "application/x-tgif",
560
+ "ustar": "application/x-ustar",
561
+ "src": "application/x-wais-source",
562
+ "crt": "application/x-x509-ca-cert",
563
+ "fig": "application/x-xfig",
564
+ "xlf": "application/x-xliff+xml",
565
+ "xpi": "application/x-xpinstall",
566
+ "xz": "application/x-xz",
567
+ "xaml": "application/xaml+xml",
568
+ "xdf": "application/xcap-diff+xml",
569
+ "xenc": "application/xenc+xml",
570
+ "xhtml": "application/xhtml+xml",
571
+ "xml": "application/xml",
572
+ "dtd": "application/xml-dtd",
573
+ "xop": "application/xop+xml",
574
+ "xpl": "application/xproc+xml",
575
+ "xslt": "application/xslt+xml",
576
+ "xspf": "application/xspf+xml",
577
+ "mxml": "application/xv+xml",
578
+ "yang": "application/yang",
579
+ "yin": "application/yin+xml",
580
+ "zip": "application/zip",
581
+ "adp": "audio/adpcm",
582
+ "au": "audio/basic",
583
+ "mid": "audio/midi",
584
+ "m4a": "audio/mp4",
585
+ "mp3": "audio/mpeg",
586
+ "ogg": "audio/ogg",
587
+ "s3m": "audio/s3m",
588
+ "sil": "audio/silk",
589
+ "uva": "audio/vnd.dece.audio",
590
+ "eol": "audio/vnd.digital-winds",
591
+ "dra": "audio/vnd.dra",
592
+ "dts": "audio/vnd.dts",
593
+ "dtshd": "audio/vnd.dts.hd",
594
+ "lvp": "audio/vnd.lucent.voice",
595
+ "pya": "audio/vnd.ms-playready.media.pya",
596
+ "ecelp4800": "audio/vnd.nuera.ecelp4800",
597
+ "ecelp7470": "audio/vnd.nuera.ecelp7470",
598
+ "ecelp9600": "audio/vnd.nuera.ecelp9600",
599
+ "rip": "audio/vnd.rip",
600
+ "weba": "audio/webm",
601
+ "aac": "audio/x-aac",
602
+ "aiff": "audio/x-aiff",
603
+ "caf": "audio/x-caf",
604
+ "flac": "audio/x-flac",
605
+ "mka": "audio/x-matroska",
606
+ "m3u": "audio/x-mpegurl",
607
+ "wax": "audio/x-ms-wax",
608
+ "wma": "audio/x-ms-wma",
609
+ "rmp": "audio/x-pn-realaudio-plugin",
610
+ "wav": "audio/x-wav",
611
+ "xm": "audio/xm",
612
+ "cdx": "chemical/x-cdx",
613
+ "cif": "chemical/x-cif",
614
+ "cmdf": "chemical/x-cmdf",
615
+ "cml": "chemical/x-cml",
616
+ "csml": "chemical/x-csml",
617
+ "xyz": "chemical/x-xyz",
618
+ "ttc": "font/collection",
619
+ "otf": "font/otf",
620
+ "ttf": "font/ttf",
621
+ "woff": "font/woff",
622
+ "woff2": "font/woff2",
623
+ "bmp": "image/bmp",
624
+ "cgm": "image/cgm",
625
+ "g3": "image/g3fax",
626
+ "gif": "image/gif",
627
+ "ief": "image/ief",
628
+ "jpg": "image/jpeg",
629
+ "ktx": "image/ktx",
630
+ "png": "image/png",
631
+ "btif": "image/prs.btif",
632
+ "sgi": "image/sgi",
633
+ "svg": "image/svg+xml",
634
+ "tiff": "image/tiff",
635
+ "psd": "image/vnd.adobe.photoshop",
636
+ "dwg": "image/vnd.dwg",
637
+ "dxf": "image/vnd.dxf",
638
+ "fbs": "image/vnd.fastbidsheet",
639
+ "fpx": "image/vnd.fpx",
640
+ "fst": "image/vnd.fst",
641
+ "mmr": "image/vnd.fujixerox.edmics-mmr",
642
+ "rlc": "image/vnd.fujixerox.edmics-rlc",
643
+ "mdi": "image/vnd.ms-modi",
644
+ "wdp": "image/vnd.ms-photo",
645
+ "npx": "image/vnd.net-fpx",
646
+ "wbmp": "image/vnd.wap.wbmp",
647
+ "xif": "image/vnd.xiff",
648
+ "webp": "image/webp",
649
+ "3ds": "image/x-3ds",
650
+ "ras": "image/x-cmu-raster",
651
+ "cmx": "image/x-cmx",
652
+ "ico": "image/x-icon",
653
+ "sid": "image/x-mrsid-image",
654
+ "pcx": "image/x-pcx",
655
+ "pnm": "image/x-portable-anymap",
656
+ "pbm": "image/x-portable-bitmap",
657
+ "pgm": "image/x-portable-graymap",
658
+ "ppm": "image/x-portable-pixmap",
659
+ "rgb": "image/x-rgb",
660
+ "tga": "image/x-tga",
661
+ "xbm": "image/x-xbitmap",
662
+ "xpm": "image/x-xpixmap",
663
+ "xwd": "image/x-xwindowdump",
664
+ "dae": "model/vnd.collada+xml",
665
+ "dwf": "model/vnd.dwf",
666
+ "gdl": "model/vnd.gdl",
667
+ "gtw": "model/vnd.gtw",
668
+ "mts": "model/vnd.mts",
669
+ "vtu": "model/vnd.vtu",
670
+ "appcache": "text/cache-manifest",
671
+ "ics": "text/calendar",
672
+ "css": "text/css",
673
+ "csv": "text/csv",
674
+ "html": "text/html",
675
+ "n3": "text/n3",
676
+ "txt": "text/plain",
677
+ "dsc": "text/prs.lines.tag",
678
+ "rtx": "text/richtext",
679
+ "tsv": "text/tab-separated-values",
680
+ "ttl": "text/turtle",
681
+ "vcard": "text/vcard",
682
+ "curl": "text/vnd.curl",
683
+ "dcurl": "text/vnd.curl.dcurl",
684
+ "mcurl": "text/vnd.curl.mcurl",
685
+ "scurl": "text/vnd.curl.scurl",
686
+ "sub": "text/vnd.dvb.subtitle",
687
+ "fly": "text/vnd.fly",
688
+ "flx": "text/vnd.fmi.flexstor",
689
+ "gv": "text/vnd.graphviz",
690
+ "3dml": "text/vnd.in3d.3dml",
691
+ "spot": "text/vnd.in3d.spot",
692
+ "jad": "text/vnd.sun.j2me.app-descriptor",
693
+ "wml": "text/vnd.wap.wml",
694
+ "wmls": "text/vnd.wap.wmlscript",
695
+ "asm": "text/x-asm",
696
+ "c": "text/x-c",
697
+ "java": "text/x-java-source",
698
+ "nfo": "text/x-nfo",
699
+ "opml": "text/x-opml",
700
+ "pas": "text/x-pascal",
701
+ "etx": "text/x-setext",
702
+ "sfv": "text/x-sfv",
703
+ "uu": "text/x-uuencode",
704
+ "vcs": "text/x-vcalendar",
705
+ "vcf": "text/x-vcard",
706
+ "3gp": "video/3gpp",
707
+ "3g2": "video/3gpp2",
708
+ "h261": "video/h261",
709
+ "h263": "video/h263",
710
+ "h264": "video/h264",
711
+ "jpgv": "video/jpeg",
712
+ "mp4": "video/mp4",
713
+ "mpeg": "video/mpeg",
714
+ "ogv": "video/ogg",
715
+ "dvb": "video/vnd.dvb.file",
716
+ "fvt": "video/vnd.fvt",
717
+ "pyv": "video/vnd.ms-playready.media.pyv",
718
+ "viv": "video/vnd.vivo",
719
+ "webm": "video/webm",
720
+ "f4v": "video/x-f4v",
721
+ "fli": "video/x-fli",
722
+ "flv": "video/x-flv",
723
+ "m4v": "video/x-m4v",
724
+ "mkv": "video/x-matroska",
725
+ "mng": "video/x-mng",
726
+ "asf": "video/x-ms-asf",
727
+ "vob": "video/x-ms-vob",
728
+ "wm": "video/x-ms-wm",
729
+ "wmv": "video/x-ms-wmv",
730
+ "wmx": "video/x-ms-wmx",
731
+ "wvx": "video/x-ms-wvx",
732
+ "avi": "video/x-msvideo",
733
+ "movie": "video/x-sgi-movie",
734
+ "smv": "video/x-smv",
735
+ "ice": "x-conference/x-cooltalk",
736
+ }
internal/api/models.go ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package api
2
+
3
+ // ErrorResponse represents an error response
4
+ type ErrorResponse struct {
5
+ Error ErrorDetail `json:"error"`
6
+ }
7
+
8
+ // ErrorDetail represents error details
9
+ type ErrorDetail struct {
10
+ Message string `json:"message"`
11
+ Type string `json:"type"`
12
+ Code string `json:"code,omitempty"`
13
+ }
internal/api/server.go ADDED
@@ -0,0 +1,176 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package api
2
+
3
+ import (
4
+ "context"
5
+ "errors"
6
+ "fmt"
7
+ "github.com/gin-gonic/gin"
8
+ "github.com/luispater/CLIProxyAPI/internal/client"
9
+ log "github.com/sirupsen/logrus"
10
+ "net/http"
11
+ "strings"
12
+ )
13
+
14
+ // Server represents the API server
15
+ type Server struct {
16
+ engine *gin.Engine
17
+ server *http.Server
18
+ handlers *APIHandlers
19
+ cfg *ServerConfig
20
+ }
21
+
22
+ // ServerConfig contains configuration for the API server
23
+ type ServerConfig struct {
24
+ Port string
25
+ Debug bool
26
+ ApiKeys []string
27
+ }
28
+
29
+ // NewServer creates a new API server instance
30
+ func NewServer(config *ServerConfig, cliClients []*client.Client) *Server {
31
+ // Set gin mode
32
+ if !config.Debug {
33
+ gin.SetMode(gin.ReleaseMode)
34
+ }
35
+
36
+ // Create handlers
37
+ handlers := NewAPIHandlers(cliClients, config.Debug)
38
+
39
+ // Create gin engine
40
+ engine := gin.New()
41
+
42
+ // Add middleware
43
+ engine.Use(gin.Logger())
44
+ engine.Use(gin.Recovery())
45
+ engine.Use(corsMiddleware())
46
+
47
+ // Create server instance
48
+ s := &Server{
49
+ engine: engine,
50
+ handlers: handlers,
51
+ cfg: config,
52
+ }
53
+
54
+ // Setup routes
55
+ s.setupRoutes()
56
+
57
+ // Create HTTP server
58
+ s.server = &http.Server{
59
+ Addr: ":" + config.Port,
60
+ Handler: engine,
61
+ }
62
+
63
+ return s
64
+ }
65
+
66
+ // setupRoutes configures the API routes
67
+ func (s *Server) setupRoutes() {
68
+ // OpenAI compatible API routes
69
+ v1 := s.engine.Group("/v1")
70
+ v1.Use(AuthMiddleware(s.cfg))
71
+ {
72
+ v1.GET("/models", s.handlers.Models)
73
+ v1.POST("/chat/completions", s.handlers.ChatCompletions)
74
+ }
75
+
76
+ // Root endpoint
77
+ s.engine.GET("/", func(c *gin.Context) {
78
+ c.JSON(http.StatusOK, gin.H{
79
+ "message": "CLI Proxy API Server",
80
+ "version": "1.0.0",
81
+ "endpoints": []string{
82
+ "POST /v1/chat/completions",
83
+ "GET /v1/models",
84
+ },
85
+ })
86
+ })
87
+ }
88
+
89
+ // Start starts the API server
90
+ func (s *Server) Start() error {
91
+ log.Debugf("Starting API server on %s", s.server.Addr)
92
+
93
+ // Start the HTTP server
94
+ if err := s.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
95
+ return fmt.Errorf("failed to start HTTP server: %v", err)
96
+ }
97
+
98
+ return nil
99
+ }
100
+
101
+ // Stop gracefully stops the API server
102
+ func (s *Server) Stop(ctx context.Context) error {
103
+ log.Debug("Stopping API server...")
104
+
105
+ // Shutdown the HTTP server
106
+ if err := s.server.Shutdown(ctx); err != nil {
107
+ return fmt.Errorf("failed to shutdown HTTP server: %v", err)
108
+ }
109
+
110
+ log.Debug("API server stopped")
111
+ return nil
112
+ }
113
+
114
+ // corsMiddleware adds CORS headers
115
+ func corsMiddleware() gin.HandlerFunc {
116
+ return func(c *gin.Context) {
117
+ c.Header("Access-Control-Allow-Origin", "*")
118
+ c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
119
+ c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
120
+
121
+ if c.Request.Method == "OPTIONS" {
122
+ c.AbortWithStatus(http.StatusNoContent)
123
+ return
124
+ }
125
+
126
+ c.Next()
127
+ }
128
+ }
129
+
130
+ // AuthMiddleware authenticates requests using API keys
131
+ func AuthMiddleware(cfg *ServerConfig) gin.HandlerFunc {
132
+ return func(c *gin.Context) {
133
+ if len(cfg.ApiKeys) == 0 {
134
+ c.Next()
135
+ return
136
+ }
137
+
138
+ // Get the Authorization header
139
+ authHeader := c.GetHeader("Authorization")
140
+ if authHeader == "" {
141
+ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
142
+ "error": "Missing API key",
143
+ })
144
+ return
145
+ }
146
+
147
+ // Extract the API key
148
+ parts := strings.Split(authHeader, " ")
149
+ var apiKey string
150
+ if len(parts) == 2 && strings.ToLower(parts[0]) == "bearer" {
151
+ apiKey = parts[1]
152
+ } else {
153
+ apiKey = authHeader
154
+ }
155
+
156
+ // Find the API key in the in-memory list
157
+ var foundKey string
158
+ for i := range cfg.ApiKeys {
159
+ if cfg.ApiKeys[i] == apiKey {
160
+ foundKey = cfg.ApiKeys[i]
161
+ break
162
+ }
163
+ }
164
+ if foundKey == "" {
165
+ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
166
+ "error": "Invalid API key",
167
+ })
168
+ return
169
+ }
170
+
171
+ // Store the API key and user in the context
172
+ c.Set("apiKey", foundKey)
173
+
174
+ c.Next()
175
+ }
176
+ }
internal/auth/auth.go ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package auth
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "errors"
7
+ "fmt"
8
+ log "github.com/sirupsen/logrus"
9
+ "github.com/tidwall/gjson"
10
+ "io"
11
+ "net/http"
12
+ "os"
13
+ "path/filepath"
14
+ "time"
15
+
16
+ "github.com/skratchdot/open-golang/open"
17
+ "golang.org/x/oauth2"
18
+ "golang.org/x/oauth2/google"
19
+ )
20
+
21
+ const (
22
+ oauthClientID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com"
23
+ oauthClientSecret = "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"
24
+ )
25
+
26
+ var (
27
+ oauthScopes = []string{
28
+ "https://www.googleapis.com/auth/cloud-platform",
29
+ "https://www.googleapis.com/auth/userinfo.email",
30
+ "https://www.googleapis.com/auth/userinfo.profile",
31
+ }
32
+ )
33
+
34
+ type TokenStorage struct {
35
+ Token any `json:"token"`
36
+ ProjectID string `json:"project_id"`
37
+ Email string `json:"email"`
38
+ }
39
+
40
+ // GetAuthenticatedClient configures and returns an HTTP client with OAuth2 tokens.
41
+ // It handles the entire flow: loading, refreshing, and fetching new tokens.
42
+ func GetAuthenticatedClient(ctx context.Context, ts *TokenStorage, authDir string) (*http.Client, error) {
43
+ conf := &oauth2.Config{
44
+ ClientID: oauthClientID,
45
+ ClientSecret: oauthClientSecret,
46
+ RedirectURL: "http://localhost:8085/oauth2callback", // Placeholder, will be updated
47
+ Scopes: oauthScopes,
48
+ Endpoint: google.Endpoint,
49
+ }
50
+
51
+ var token *oauth2.Token
52
+ var err error
53
+
54
+ if ts.Token == nil {
55
+ log.Info("Could not load token from file, starting OAuth flow.")
56
+ token, err = getTokenFromWeb(ctx, conf)
57
+ if err != nil {
58
+ return nil, fmt.Errorf("failed to get token from web: %w", err)
59
+ }
60
+ ts, err = saveTokenToFile(ctx, conf, token, ts.ProjectID, authDir)
61
+ if err != nil {
62
+ // Log the error but proceed, as we have a valid token for the session.
63
+ log.Errorf("Warning: failed to save token to file: %v", err)
64
+ }
65
+ }
66
+ tsToken, _ := json.Marshal(ts.Token)
67
+ if err = json.Unmarshal(tsToken, &token); err != nil {
68
+ return nil, err
69
+ }
70
+
71
+ return conf.Client(ctx, token), nil
72
+ }
73
+
74
+ // saveTokenToFile saves a token to the local credentials file.
75
+ func saveTokenToFile(ctx context.Context, config *oauth2.Config, token *oauth2.Token, projectID, authDir string) (*TokenStorage, error) {
76
+ httpClient := config.Client(ctx, token)
77
+ req, err := http.NewRequestWithContext(ctx, "GET", "https://www.googleapis.com/oauth2/v1/userinfo?alt=json", nil)
78
+ if err != nil {
79
+ return nil, fmt.Errorf("could not get user info: %v", err)
80
+ }
81
+ req.Header.Set("Content-Type", "application/json")
82
+ req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
83
+
84
+ resp, err := httpClient.Do(req)
85
+ if err != nil {
86
+ return nil, fmt.Errorf("failed to execute request: %w", err)
87
+ }
88
+ defer func() {
89
+ _ = resp.Body.Close()
90
+ }()
91
+
92
+ bodyBytes, _ := io.ReadAll(resp.Body)
93
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
94
+ return nil, fmt.Errorf("get user info request failed with status %d: %s", resp.StatusCode, string(bodyBytes))
95
+ }
96
+
97
+ emailResult := gjson.GetBytes(bodyBytes, "email")
98
+ if emailResult.Exists() && emailResult.Type == gjson.String {
99
+ log.Infof("Authenticated user email: %s", emailResult.String())
100
+ } else {
101
+ log.Info("Failed to get user email from token")
102
+ }
103
+
104
+ log.Infof("Saving credentials to %s", filepath.Join(authDir, fmt.Sprintf("%s.json", emailResult.String())))
105
+ if err = os.MkdirAll(authDir, 0700); err != nil {
106
+ return nil, fmt.Errorf("failed to create directory: %w", err)
107
+ }
108
+
109
+ f, err := os.Create(filepath.Join(authDir, fmt.Sprintf("%s.json", emailResult.String())))
110
+ if err != nil {
111
+ return nil, fmt.Errorf("failed to create token file: %w", err)
112
+ }
113
+ defer func() {
114
+ _ = f.Close()
115
+ }()
116
+
117
+ var ifToken map[string]any
118
+ jsonData, _ := json.Marshal(token)
119
+ err = json.Unmarshal(jsonData, &ifToken)
120
+ if err != nil {
121
+ return nil, fmt.Errorf("failed to unmarshal token: %w", err)
122
+ }
123
+
124
+ ifToken["token_uri"] = "https://oauth2.googleapis.com/token"
125
+ ifToken["client_id"] = oauthClientID
126
+ ifToken["client_secret"] = oauthClientSecret
127
+ ifToken["scopes"] = oauthScopes
128
+ ifToken["universe_domain"] = "googleapis.com"
129
+
130
+ ts := TokenStorage{
131
+ Token: ifToken,
132
+ ProjectID: projectID,
133
+ Email: emailResult.String(),
134
+ }
135
+
136
+ if err = json.NewEncoder(f).Encode(ts); err != nil {
137
+ return nil, fmt.Errorf("failed to write token to file: %w", err)
138
+ }
139
+ return &ts, nil
140
+ }
141
+
142
+ // getTokenFromWeb starts a local server to handle the OAuth2 flow.
143
+ func getTokenFromWeb(ctx context.Context, config *oauth2.Config) (*oauth2.Token, error) {
144
+ // Use a channel to pass the authorization code from the HTTP handler to the main function.
145
+ codeChan := make(chan string)
146
+ errChan := make(chan error)
147
+
148
+ // Create a new HTTP server.
149
+ server := &http.Server{Addr: "localhost:8085"}
150
+ config.RedirectURL = "http://localhost:8085/oauth2callback"
151
+
152
+ http.HandleFunc("/oauth2callback", func(w http.ResponseWriter, r *http.Request) {
153
+ if err := r.URL.Query().Get("error"); err != "" {
154
+ _, _ = fmt.Fprintf(w, "Authentication failed: %s", err)
155
+ errChan <- fmt.Errorf("authentication failed via callback: %s", err)
156
+ return
157
+ }
158
+ code := r.URL.Query().Get("code")
159
+ if code == "" {
160
+ _, _ = fmt.Fprint(w, "Authentication failed: code not found.")
161
+ errChan <- fmt.Errorf("code not found in callback")
162
+ return
163
+ }
164
+ _, _ = fmt.Fprint(w, "<html><body><h1>Authentication successful!</h1><p>You can close this window.</p></body></html>")
165
+ codeChan <- code
166
+ })
167
+
168
+ // Start the server in a goroutine.
169
+ go func() {
170
+ if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
171
+ log.Fatalf("ListenAndServe(): %v", err)
172
+ }
173
+ }()
174
+
175
+ // Open the authorization URL in the user's browser.
176
+ authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline, oauth2.SetAuthURLParam("prompt", "consent"))
177
+ log.Debugf("CLI login required.\nAttempting to open authentication page in your browser.\nIf it does not open, please navigate to this URL:\n\n%s\n\n", authURL)
178
+
179
+ err := open.Run(authURL)
180
+ if err != nil {
181
+ log.Errorf("Failed to open browser: %v. Please open the URL manually.", err)
182
+ }
183
+
184
+ // Wait for the authorization code or an error.
185
+ var authCode string
186
+ select {
187
+ case code := <-codeChan:
188
+ authCode = code
189
+ case err = <-errChan:
190
+ return nil, err
191
+ case <-time.After(5 * time.Minute): // Timeout
192
+ return nil, fmt.Errorf("oauth flow timed out")
193
+ }
194
+
195
+ // Shutdown the server.
196
+ if err = server.Shutdown(ctx); err != nil {
197
+ log.Errorf("Failed to shut down server: %v", err)
198
+ }
199
+
200
+ // Exchange the authorization code for a token.
201
+ token, err := config.Exchange(ctx, authCode)
202
+ if err != nil {
203
+ return nil, fmt.Errorf("failed to exchange token: %w", err)
204
+ }
205
+
206
+ log.Info("Authentication successful.")
207
+ return token, nil
208
+ }
internal/client/client.go ADDED
@@ -0,0 +1,452 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package client
2
+
3
+ import (
4
+ "bufio"
5
+ "bytes"
6
+ "context"
7
+ "encoding/json"
8
+ "fmt"
9
+ log "github.com/sirupsen/logrus"
10
+ "github.com/tidwall/gjson"
11
+ "github.com/tidwall/sjson"
12
+ "golang.org/x/oauth2"
13
+ "io"
14
+ "net/http"
15
+ "runtime"
16
+ "strings"
17
+ "sync"
18
+ "time"
19
+ )
20
+
21
+ // --- Constants ---
22
+ const (
23
+ codeAssistEndpoint = "https://cloudcode-pa.googleapis.com"
24
+ apiVersion = "v1internal"
25
+ pluginVersion = "1.0.0"
26
+ )
27
+
28
+ type GCPProject struct {
29
+ Projects []GCPProjectProjects `json:"projects"`
30
+ }
31
+ type GCPProjectLabels struct {
32
+ GenerativeLanguage string `json:"generative-language"`
33
+ }
34
+ type GCPProjectProjects struct {
35
+ ProjectNumber string `json:"projectNumber"`
36
+ ProjectID string `json:"projectId"`
37
+ LifecycleState string `json:"lifecycleState"`
38
+ Name string `json:"name"`
39
+ Labels GCPProjectLabels `json:"labels"`
40
+ CreateTime time.Time `json:"createTime"`
41
+ }
42
+
43
+ type Content struct {
44
+ Role string `json:"role"`
45
+ Parts []Part `json:"parts"`
46
+ }
47
+
48
+ // Part represents a single part of a message's content.
49
+ type Part struct {
50
+ Text string `json:"text,omitempty"`
51
+ InlineData *InlineData `json:"inlineData,omitempty"`
52
+ FunctionCall *FunctionCall `json:"functionCall,omitempty"`
53
+ FunctionResponse *FunctionResponse `json:"functionResponse,omitempty"`
54
+ }
55
+
56
+ type InlineData struct {
57
+ MimeType string `json:"mime_type,omitempty"`
58
+ Data string `json:"data,omitempty"`
59
+ }
60
+
61
+ // FunctionCall represents a tool call requested by the model.
62
+ type FunctionCall struct {
63
+ Name string `json:"name"`
64
+ Args map[string]interface{} `json:"args"`
65
+ }
66
+
67
+ // FunctionResponse represents the result of a tool execution.
68
+ type FunctionResponse struct {
69
+ Name string `json:"name"`
70
+ Response map[string]interface{} `json:"response"`
71
+ }
72
+
73
+ // GenerateContentRequest is the request payload for the streamGenerateContent endpoint.
74
+ type GenerateContentRequest struct {
75
+ Contents []Content `json:"contents"`
76
+ Tools []ToolDeclaration `json:"tools,omitempty"`
77
+ GenerationConfig `json:"generationConfig"`
78
+ }
79
+
80
+ // GenerationConfig defines model generation parameters.
81
+ type GenerationConfig struct {
82
+ ThinkingConfig GenerationConfigThinkingConfig `json:"thinkingConfig,omitempty"`
83
+ Temperature float64 `json:"temperature,omitempty"`
84
+ TopP float64 `json:"topP,omitempty"`
85
+ TopK float64 `json:"topK,omitempty"`
86
+ // Temperature, TopP, TopK, etc. can be added here.
87
+ }
88
+
89
+ type GenerationConfigThinkingConfig struct {
90
+ IncludeThoughts bool `json:"include_thoughts,omitempty"`
91
+ }
92
+
93
+ // ToolDeclaration is the structure for declaring tools to the API.
94
+ // For now, we'll assume a simple structure. A more complete implementation
95
+ // would mirror the OpenAPI schema definition.
96
+ type ToolDeclaration struct {
97
+ FunctionDeclarations []interface{} `json:"functionDeclarations"`
98
+ }
99
+
100
+ // Client is the main client for interacting with the CLI API.
101
+ type Client struct {
102
+ httpClient *http.Client
103
+ projectID string
104
+ RequestMutex sync.Mutex
105
+ Email string
106
+ }
107
+
108
+ // NewClient creates a new CLI API client.
109
+ func NewClient(httpClient *http.Client) *Client {
110
+ return &Client{
111
+ httpClient: httpClient,
112
+ }
113
+ }
114
+
115
+ // SetupUser performs the initial user onboarding and setup.
116
+ func (c *Client) SetupUser(ctx context.Context, email, projectID string) error {
117
+ c.Email = email
118
+ log.Info("Performing user onboarding...")
119
+
120
+ // 1. LoadCodeAssist
121
+ loadAssistReqBody := map[string]interface{}{
122
+ "metadata": getClientMetadata(),
123
+ }
124
+ if projectID != "" {
125
+ loadAssistReqBody["cloudaicompanionProject"] = projectID
126
+ }
127
+
128
+ var loadAssistResp map[string]interface{}
129
+ err := c.makeAPIRequest(ctx, "loadCodeAssist", "POST", loadAssistReqBody, &loadAssistResp)
130
+ if err != nil {
131
+ return fmt.Errorf("failed to load code assist: %w", err)
132
+ }
133
+
134
+ // a, _ := json.Marshal(&loadAssistResp)
135
+ // log.Debug(string(a))
136
+
137
+ // 2. OnboardUser
138
+ var onboardTierID = "legacy-tier"
139
+ if tiers, ok := loadAssistResp["allowedTiers"].([]interface{}); ok {
140
+ for _, t := range tiers {
141
+ if tier, tierOk := t.(map[string]interface{}); tierOk {
142
+ if isDefault, isDefaultOk := tier["isDefault"].(bool); isDefaultOk && isDefault {
143
+ if id, idOk := tier["id"].(string); idOk {
144
+ onboardTierID = id
145
+ break
146
+ }
147
+ }
148
+ }
149
+ }
150
+ }
151
+
152
+ onboardProjectID := projectID
153
+ if p, ok := loadAssistResp["cloudaicompanionProject"].(string); ok && p != "" {
154
+ onboardProjectID = p
155
+ }
156
+
157
+ onboardReqBody := map[string]interface{}{
158
+ "tierId": onboardTierID,
159
+ "metadata": getClientMetadata(),
160
+ }
161
+ if onboardProjectID != "" {
162
+ onboardReqBody["cloudaicompanionProject"] = onboardProjectID
163
+ } else {
164
+ return fmt.Errorf("failed to start user onboarding, need define a project id")
165
+ }
166
+
167
+ var lroResp map[string]interface{}
168
+ err = c.makeAPIRequest(ctx, "onboardUser", "POST", onboardReqBody, &lroResp)
169
+ if err != nil {
170
+ return fmt.Errorf("failed to start user onboarding: %w", err)
171
+ }
172
+
173
+ // a, _ = json.Marshal(&lroResp)
174
+ // log.Debug(string(a))
175
+
176
+ // 3. Poll Long-Running Operation (LRO)
177
+ if done, doneOk := lroResp["done"].(bool); doneOk && done {
178
+ if project, projectOk := lroResp["response"].(map[string]interface{})["cloudaicompanionProject"].(map[string]interface{}); projectOk {
179
+ c.projectID = project["id"].(string)
180
+ log.Infof("Onboarding complete. Using Project ID: %s", c.projectID)
181
+ return nil
182
+ }
183
+ }
184
+ return fmt.Errorf("failed to get operation name from onboarding response: %v", lroResp)
185
+ }
186
+
187
+ // makeAPIRequest handles making requests to the CLI API endpoints.
188
+ func (c *Client) makeAPIRequest(ctx context.Context, endpoint, method string, body interface{}, result interface{}) error {
189
+ var reqBody io.Reader
190
+ if body != nil {
191
+ jsonBody, err := json.Marshal(body)
192
+ if err != nil {
193
+ return fmt.Errorf("failed to marshal request body: %w", err)
194
+ }
195
+ reqBody = bytes.NewBuffer(jsonBody)
196
+ }
197
+
198
+ url := fmt.Sprintf("%s/%s:%s", codeAssistEndpoint, apiVersion, endpoint)
199
+ if strings.HasPrefix(endpoint, "operations/") {
200
+ url = fmt.Sprintf("%s/%s", codeAssistEndpoint, endpoint)
201
+ }
202
+
203
+ req, err := http.NewRequestWithContext(ctx, method, url, reqBody)
204
+ if err != nil {
205
+ return fmt.Errorf("failed to create request: %w", err)
206
+ }
207
+
208
+ token, err := c.httpClient.Transport.(*oauth2.Transport).Source.Token()
209
+ if err != nil {
210
+ return fmt.Errorf("failed to get token: %w", err)
211
+ }
212
+
213
+ // Set headers
214
+ metadataStr := getClientMetadataString()
215
+ req.Header.Set("Content-Type", "application/json")
216
+ req.Header.Set("User-Agent", getUserAgent())
217
+ req.Header.Set("Client-Metadata", metadataStr)
218
+ req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
219
+
220
+ resp, err := c.httpClient.Do(req)
221
+ if err != nil {
222
+ return fmt.Errorf("failed to execute request: %w", err)
223
+ }
224
+ defer func() {
225
+ _ = resp.Body.Close()
226
+ }()
227
+
228
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
229
+ bodyBytes, _ := io.ReadAll(resp.Body)
230
+ return fmt.Errorf("api request failed with status %d: %s", resp.StatusCode, string(bodyBytes))
231
+ }
232
+
233
+ if result != nil {
234
+ if err = json.NewDecoder(resp.Body).Decode(result); err != nil {
235
+ return fmt.Errorf("failed to decode response body: %w", err)
236
+ }
237
+ }
238
+
239
+ return nil
240
+ }
241
+
242
+ // StreamAPIRequest handles making streaming requests to the CLI API endpoints.
243
+ func (c *Client) StreamAPIRequest(ctx context.Context, endpoint string, body interface{}) (io.ReadCloser, error) {
244
+ var jsonBody []byte
245
+ var err error
246
+ if byteBody, ok := body.([]byte); ok {
247
+ jsonBody = byteBody
248
+ } else {
249
+ jsonBody, err = json.Marshal(body)
250
+ if err != nil {
251
+ return nil, fmt.Errorf("failed to marshal request body: %w", err)
252
+ }
253
+ }
254
+ // log.Debug(string(jsonBody))
255
+ reqBody := bytes.NewBuffer(jsonBody)
256
+
257
+ // Add alt=sse for streaming
258
+ url := fmt.Sprintf("%s/%s:%s?alt=sse", codeAssistEndpoint, apiVersion, endpoint)
259
+
260
+ req, err := http.NewRequestWithContext(ctx, "POST", url, reqBody)
261
+ if err != nil {
262
+ return nil, fmt.Errorf("failed to create request: %w", err)
263
+ }
264
+
265
+ token, err := c.httpClient.Transport.(*oauth2.Transport).Source.Token()
266
+ if err != nil {
267
+ return nil, fmt.Errorf("failed to get token: %w", err)
268
+ }
269
+
270
+ // Set headers
271
+ metadataStr := getClientMetadataString()
272
+ req.Header.Set("Content-Type", "application/json")
273
+ req.Header.Set("User-Agent", getUserAgent())
274
+ req.Header.Set("Client-Metadata", metadataStr)
275
+ req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
276
+
277
+ resp, err := c.httpClient.Do(req)
278
+ if err != nil {
279
+ return nil, fmt.Errorf("failed to execute request: %w", err)
280
+ }
281
+
282
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
283
+ defer func() {
284
+ _ = resp.Body.Close()
285
+ }()
286
+ bodyBytes, _ := io.ReadAll(resp.Body)
287
+ return nil, fmt.Errorf("api streaming request failed with status %d: %s", resp.StatusCode, string(bodyBytes))
288
+ }
289
+
290
+ return resp.Body, nil
291
+ }
292
+
293
+ // SendMessageStream handles a single conversational turn, including tool calls.
294
+ func (c *Client) SendMessageStream(ctx context.Context, rawJson []byte, model string, contents []Content, tools []ToolDeclaration) (<-chan []byte, <-chan error) {
295
+ dataTag := []byte("data: ")
296
+ errChan := make(chan error)
297
+ dataChan := make(chan []byte)
298
+ go func() {
299
+ defer close(errChan)
300
+ defer close(dataChan)
301
+
302
+ request := GenerateContentRequest{
303
+ Contents: contents,
304
+ GenerationConfig: GenerationConfig{
305
+ ThinkingConfig: GenerationConfigThinkingConfig{
306
+ IncludeThoughts: true,
307
+ },
308
+ },
309
+ }
310
+ request.Tools = tools
311
+
312
+ requestBody := map[string]interface{}{
313
+ "project": c.projectID, // Assuming ProjectID is available
314
+ "request": request,
315
+ "model": model,
316
+ }
317
+
318
+ byteRequestBody, _ := json.Marshal(requestBody)
319
+
320
+ // log.Debug(string(rawJson))
321
+
322
+ reasoningEffortResult := gjson.GetBytes(rawJson, "reasoning_effort")
323
+ if reasoningEffortResult.String() == "none" {
324
+ byteRequestBody, _ = sjson.DeleteBytes(byteRequestBody, "request.generationConfig.thinkingConfig.include_thoughts")
325
+ byteRequestBody, _ = sjson.SetBytes(byteRequestBody, "request.generationConfig.thinkingConfig.thinkingBudget", 0)
326
+ } else if reasoningEffortResult.String() == "auto" {
327
+ byteRequestBody, _ = sjson.SetBytes(byteRequestBody, "request.generationConfig.thinkingConfig.thinkingBudget", -1)
328
+ } else if reasoningEffortResult.String() == "low" {
329
+ byteRequestBody, _ = sjson.SetBytes(byteRequestBody, "request.generationConfig.thinkingConfig.thinkingBudget", 1024)
330
+ } else if reasoningEffortResult.String() == "medium" {
331
+ byteRequestBody, _ = sjson.SetBytes(byteRequestBody, "request.generationConfig.thinkingConfig.thinkingBudget", 8192)
332
+ } else if reasoningEffortResult.String() == "high" {
333
+ byteRequestBody, _ = sjson.SetBytes(byteRequestBody, "request.generationConfig.thinkingConfig.thinkingBudget", 24576)
334
+ } else {
335
+ byteRequestBody, _ = sjson.SetBytes(byteRequestBody, "request.generationConfig.thinkingConfig.thinkingBudget", -1)
336
+ }
337
+
338
+ temperatureResult := gjson.GetBytes(rawJson, "temperature")
339
+ if temperatureResult.Exists() && temperatureResult.Type == gjson.Number {
340
+ byteRequestBody, _ = sjson.SetBytes(byteRequestBody, "request.generationConfig.temperature", temperatureResult.Num)
341
+ }
342
+
343
+ topPResult := gjson.GetBytes(rawJson, "top_p")
344
+ if topPResult.Exists() && topPResult.Type == gjson.Number {
345
+ byteRequestBody, _ = sjson.SetBytes(byteRequestBody, "request.generationConfig.topP", topPResult.Num)
346
+ }
347
+
348
+ topKResult := gjson.GetBytes(rawJson, "top_k")
349
+ if topKResult.Exists() && topKResult.Type == gjson.Number {
350
+ byteRequestBody, _ = sjson.SetBytes(byteRequestBody, "request.generationConfig.topK", topKResult.Num)
351
+ }
352
+
353
+ // log.Debug(string(byteRequestBody))
354
+
355
+ stream, err := c.StreamAPIRequest(ctx, "streamGenerateContent", byteRequestBody)
356
+ if err != nil {
357
+ // log.Println(err)
358
+ errChan <- err
359
+ return
360
+ }
361
+
362
+ scanner := bufio.NewScanner(stream)
363
+ for scanner.Scan() {
364
+ line := scanner.Bytes()
365
+ // log.Printf("Received stream chunk: %s", line)
366
+ if bytes.HasPrefix(line, dataTag) {
367
+ dataChan <- line[6:]
368
+ }
369
+ }
370
+
371
+ if err = scanner.Err(); err != nil {
372
+ // log.Println(err)
373
+ errChan <- err
374
+ _ = stream.Close()
375
+ return
376
+ }
377
+
378
+ _ = stream.Close()
379
+ }()
380
+
381
+ return dataChan, errChan
382
+ }
383
+
384
+ func (c *Client) GetProjectList(ctx context.Context) (*GCPProject, error) {
385
+ token, err := c.httpClient.Transport.(*oauth2.Transport).Source.Token()
386
+ req, err := http.NewRequestWithContext(ctx, "GET", "https://cloudresourcemanager.googleapis.com/v1/projects", nil)
387
+ if err != nil {
388
+ return nil, fmt.Errorf("could not get project list: %v", err)
389
+ }
390
+ req.Header.Set("Content-Type", "application/json")
391
+ req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
392
+
393
+ resp, err := c.httpClient.Do(req)
394
+ if err != nil {
395
+ return nil, fmt.Errorf("failed to execute request: %w", err)
396
+ }
397
+ defer func() {
398
+ _ = resp.Body.Close()
399
+ }()
400
+
401
+ bodyBytes, _ := io.ReadAll(resp.Body)
402
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
403
+ return nil, fmt.Errorf("get user info request failed with status %d: %s", resp.StatusCode, string(bodyBytes))
404
+ }
405
+
406
+ var project GCPProject
407
+ err = json.Unmarshal(bodyBytes, &project)
408
+ if err != nil {
409
+ return nil, fmt.Errorf("failed to unmarshal project list: %w", err)
410
+ }
411
+ return &project, nil
412
+ }
413
+
414
+ // getClientMetadata returns metadata about the client environment.
415
+ func getClientMetadata() map[string]string {
416
+ return map[string]string{
417
+ "ideType": "IDE_UNSPECIFIED",
418
+ "platform": getPlatform(),
419
+ "pluginType": "GEMINI",
420
+ "pluginVersion": pluginVersion,
421
+ }
422
+ }
423
+
424
+ // getClientMetadataString returns the metadata as a comma-separated string.
425
+ func getClientMetadataString() string {
426
+ md := getClientMetadata()
427
+ parts := make([]string, 0, len(md))
428
+ for k, v := range md {
429
+ parts = append(parts, fmt.Sprintf("%s=%s", k, v))
430
+ }
431
+ return strings.Join(parts, ",")
432
+ }
433
+
434
+ func getUserAgent() string {
435
+ return fmt.Sprintf(fmt.Sprintf("GeminiCLI/%s (%s; %s)", pluginVersion, runtime.GOOS, runtime.GOARCH))
436
+ }
437
+
438
+ // getPlatform returns the OS and architecture in the format expected by the API.
439
+ func getPlatform() string {
440
+ os := runtime.GOOS
441
+ arch := runtime.GOARCH
442
+ switch os {
443
+ case "darwin":
444
+ return fmt.Sprintf("DARWIN_%s", strings.ToUpper(arch))
445
+ case "linux":
446
+ return fmt.Sprintf("LINUX_%s", strings.ToUpper(arch))
447
+ case "windows":
448
+ return fmt.Sprintf("WINDOWS_%s", strings.ToUpper(arch))
449
+ default:
450
+ return "PLATFORM_UNSPECIFIED"
451
+ }
452
+ }
internal/config/config.go ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package config
2
+
3
+ import (
4
+ "fmt"
5
+ "gopkg.in/yaml.v3"
6
+ "os"
7
+ )
8
+
9
+ // Config represents the application's configuration
10
+ type Config struct {
11
+ Port int `yaml:"port"`
12
+ AuthDir string `yaml:"auth_dir"`
13
+ Debug bool `yaml:"debug"`
14
+ ApiKeys []string `yaml:"api_keys"`
15
+ }
16
+
17
+ // / LoadConfig loads the configuration from the specified file
18
+ func LoadConfig(configFile string) (*Config, error) {
19
+ // Read the configuration file
20
+ data, err := os.ReadFile(configFile)
21
+ // If reading the file fails
22
+ if err != nil {
23
+ // Return an error
24
+ return nil, fmt.Errorf("failed to read config file: %w", err)
25
+ }
26
+
27
+ // Parse the YAML data
28
+ var config Config
29
+ // If parsing the YAML data fails
30
+ if err = yaml.Unmarshal(data, &config); err != nil {
31
+ // Return an error
32
+ return nil, fmt.Errorf("failed to parse config file: %w", err)
33
+ }
34
+
35
+ // Return the configuration
36
+ return &config, nil
37
+ }