mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-02-02 20:40:52 +08:00
first commit
This commit is contained in:
30
.github/workflows/release.yaml
vendored
Normal file
30
.github/workflows/release.yaml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: goreleaser
|
||||
|
||||
on:
|
||||
push:
|
||||
# run only against tags
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- run: git fetch --force --tags
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '>=1.24.0'
|
||||
cache: true
|
||||
- uses: goreleaser/goreleaser-action@v3
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
11
.goreleaser.yml
Normal file
11
.goreleaser.yml
Normal file
@@ -0,0 +1,11 @@
|
||||
builds:
|
||||
- id: "cli-proxy-api"
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
main: ./cmd/server/
|
||||
binary: cli-proxy-api
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Luis Pater
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
198
README.md
Normal file
198
README.md
Normal file
@@ -0,0 +1,198 @@
|
||||
# CLI Proxy API
|
||||
|
||||
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.
|
||||
|
||||
## Features
|
||||
|
||||
- OpenAI-compatible API endpoints for CLI models
|
||||
- Support for both streaming and non-streaming responses
|
||||
- Function calling/tools support
|
||||
- Multimodal input support (text and images)
|
||||
- Multiple account support with load balancing
|
||||
- Simple CLI authentication flow
|
||||
|
||||
## Installation
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Go 1.24 or higher
|
||||
- A Google account with access to CLI models
|
||||
|
||||
### Building from Source
|
||||
|
||||
1. Clone the repository:
|
||||
```bash
|
||||
git clone https://github.com/luispater/CLIProxyAPI.git
|
||||
cd CLIProxyAPI
|
||||
```
|
||||
|
||||
2. Build the application:
|
||||
```bash
|
||||
go build -o cli-proxy-api ./cmd/server
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Authentication
|
||||
|
||||
Before using the API, you need to authenticate with your Google account:
|
||||
|
||||
```bash
|
||||
./cli-proxy-api --login
|
||||
```
|
||||
|
||||
If you are an old gemini code user, you may need to specify a project ID:
|
||||
|
||||
```bash
|
||||
./cli-proxy-api --login --project_id <your_project_id>
|
||||
```
|
||||
|
||||
### Starting the Server
|
||||
|
||||
Once authenticated, start the server:
|
||||
|
||||
```bash
|
||||
./cli-proxy-api
|
||||
```
|
||||
|
||||
By default, the server runs on port 8317.
|
||||
|
||||
### API Endpoints
|
||||
|
||||
#### List Models
|
||||
|
||||
```
|
||||
GET http://localhost:8317/v1/models
|
||||
```
|
||||
|
||||
#### Chat Completions
|
||||
|
||||
```
|
||||
POST http://localhost:8317/v1/chat/completions
|
||||
```
|
||||
|
||||
Request body example:
|
||||
|
||||
```json
|
||||
{
|
||||
"model": "gemini-2.5-pro",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Hello, how are you?"
|
||||
}
|
||||
],
|
||||
"stream": true
|
||||
}
|
||||
```
|
||||
|
||||
### Using with OpenAI Libraries
|
||||
|
||||
You can use this proxy with any OpenAI-compatible library by setting the base URL to your local server:
|
||||
|
||||
#### Python (with OpenAI library)
|
||||
|
||||
```python
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI(
|
||||
api_key="dummy", # Not used but required
|
||||
base_url="http://localhost:8317/v1"
|
||||
)
|
||||
|
||||
response = client.chat.completions.create(
|
||||
model="gemini-2.5-pro",
|
||||
messages=[
|
||||
{"role": "user", "content": "Hello, how are you?"}
|
||||
]
|
||||
)
|
||||
|
||||
print(response.choices[0].message.content)
|
||||
```
|
||||
|
||||
#### JavaScript/TypeScript
|
||||
|
||||
```javascript
|
||||
import OpenAI from 'openai';
|
||||
|
||||
const openai = new OpenAI({
|
||||
apiKey: 'dummy', // Not used but required
|
||||
baseURL: 'http://localhost:8317/v1',
|
||||
});
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: 'gemini-2.5-pro',
|
||||
messages: [
|
||||
{ role: 'user', content: 'Hello, how are you?' }
|
||||
],
|
||||
});
|
||||
|
||||
console.log(response.choices[0].message.content);
|
||||
```
|
||||
|
||||
## Supported Models
|
||||
|
||||
- gemini-2.5-pro
|
||||
- gemini-2.5-flash
|
||||
- And various preview versions
|
||||
|
||||
## Configuration
|
||||
|
||||
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:
|
||||
|
||||
```bash
|
||||
./cli-proxy --config /path/to/your/config.yaml
|
||||
```
|
||||
|
||||
### Configuration Options
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|--------------------|-------------|
|
||||
| `port` | integer | 8317 | The port number on which the server will listen |
|
||||
| `auth_dir` | string | "~/.cli-proxy-api" | Directory where authentication tokens are stored. Supports using `~` for home directory |
|
||||
| `debug` | boolean | false | Enable debug mode for verbose logging |
|
||||
| `api_keys` | string[] | [] | List of API keys that can be used to authenticate requests |
|
||||
|
||||
### Example Configuration File
|
||||
|
||||
```yaml
|
||||
# Server port
|
||||
port: 8317
|
||||
|
||||
# Authentication directory (supports ~ for home directory)
|
||||
auth_dir: "~/.cli-proxy-api"
|
||||
|
||||
# Enable debug logging
|
||||
debug: false
|
||||
|
||||
# API keys for authentication
|
||||
api_keys:
|
||||
- "your-api-key-1"
|
||||
- "your-api-key-2"
|
||||
```
|
||||
|
||||
### Authentication Directory
|
||||
|
||||
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.
|
||||
|
||||
### API Keys
|
||||
|
||||
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:
|
||||
|
||||
```
|
||||
Authorization: Bearer your-api-key-1
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
|
||||
1. Fork the repository
|
||||
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
||||
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
||||
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||
5. Open a Pull Request
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
241
cmd/server/main.go
Normal file
241
cmd/server/main.go
Normal file
@@ -0,0 +1,241 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/luispater/CLIProxyAPI/internal/api"
|
||||
"github.com/luispater/CLIProxyAPI/internal/auth"
|
||||
"github.com/luispater/CLIProxyAPI/internal/client"
|
||||
"github.com/luispater/CLIProxyAPI/internal/config"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LogFormatter struct {
|
||||
}
|
||||
|
||||
func (m *LogFormatter) Format(entry *log.Entry) ([]byte, error) {
|
||||
var b *bytes.Buffer
|
||||
if entry.Buffer != nil {
|
||||
b = entry.Buffer
|
||||
} else {
|
||||
b = &bytes.Buffer{}
|
||||
}
|
||||
|
||||
timestamp := entry.Time.Format("2006-01-02 15:04:05")
|
||||
var newLog string
|
||||
newLog = fmt.Sprintf("[%s] [%s] [%s:%d] %s\n", timestamp, entry.Level, path.Base(entry.Caller.File), entry.Caller.Line, entry.Message)
|
||||
|
||||
b.WriteString(newLog)
|
||||
return b.Bytes(), nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
log.SetOutput(os.Stdout)
|
||||
log.SetReportCaller(true)
|
||||
log.SetFormatter(&LogFormatter{})
|
||||
}
|
||||
|
||||
func main() {
|
||||
var login bool
|
||||
var projectID string
|
||||
var configPath string
|
||||
|
||||
flag.BoolVar(&login, "login", false, "Login Google Account")
|
||||
flag.StringVar(&projectID, "project_id", "", "Project ID")
|
||||
flag.StringVar(&configPath, "config", "", "Configure File Path")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
var err error
|
||||
var cfg *config.Config
|
||||
var wd string
|
||||
|
||||
if configPath != "" {
|
||||
cfg, err = config.LoadConfig(configPath)
|
||||
} else {
|
||||
wd, err = os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get working directory: %v", err)
|
||||
}
|
||||
cfg, err = config.LoadConfig(path.Join(wd, "config.yaml"))
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("failed to load config: %v", err)
|
||||
}
|
||||
|
||||
if cfg.Debug {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
} else {
|
||||
log.SetLevel(log.InfoLevel)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(cfg.AuthDir, "~") {
|
||||
home, errUserHomeDir := os.UserHomeDir()
|
||||
if errUserHomeDir != nil {
|
||||
log.Fatalf("failed to get home directory: %v", errUserHomeDir)
|
||||
}
|
||||
parts := strings.Split(cfg.AuthDir, string(os.PathSeparator))
|
||||
if len(parts) > 1 {
|
||||
parts[0] = home
|
||||
cfg.AuthDir = path.Join(parts...)
|
||||
} else {
|
||||
cfg.AuthDir = home
|
||||
}
|
||||
}
|
||||
|
||||
if login {
|
||||
var ts auth.TokenStorage
|
||||
if projectID != "" {
|
||||
ts.ProjectID = projectID
|
||||
}
|
||||
|
||||
// 2. Initialize authenticated HTTP Client
|
||||
clientCtx := context.Background()
|
||||
|
||||
log.Info("Initializing authentication...")
|
||||
httpClient, errGetClient := auth.GetAuthenticatedClient(clientCtx, &ts, cfg.AuthDir)
|
||||
if errGetClient != nil {
|
||||
log.Fatalf("failed to get authenticated client: %v", errGetClient)
|
||||
return
|
||||
}
|
||||
log.Info("Authentication successful.")
|
||||
|
||||
// 3. Initialize CLI Client
|
||||
cliClient := client.NewClient(httpClient)
|
||||
if err = cliClient.SetupUser(clientCtx, ts.Email, projectID); err != nil {
|
||||
if err.Error() == "failed to start user onboarding, need define a project id" {
|
||||
log.Error("failed to start user onboarding")
|
||||
project, errGetProjectList := cliClient.GetProjectList(clientCtx)
|
||||
if errGetProjectList != nil {
|
||||
log.Fatalf("failed to complete user setup: %v", err)
|
||||
} else {
|
||||
log.Infof("Your account %s needs specify a project id.", ts.Email)
|
||||
log.Info("========================================================================")
|
||||
for i := 0; i < len(project.Projects); i++ {
|
||||
log.Infof("Project ID: %s", project.Projects[i].ProjectID)
|
||||
log.Infof("Project Name: %s", project.Projects[i].Name)
|
||||
log.Info("========================================================================")
|
||||
}
|
||||
log.Infof("Please run this command to login again:\n\n%s --login --project_id <project_id>\n", os.Args[0])
|
||||
}
|
||||
} else {
|
||||
// Log as a warning because in some cases, the CLI might still be usable
|
||||
// or the user might want to retry setup later.
|
||||
log.Fatalf("failed to complete user setup: %v", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Create API server configuration
|
||||
apiConfig := &api.ServerConfig{
|
||||
Port: fmt.Sprintf("%d", cfg.Port),
|
||||
Debug: cfg.Debug,
|
||||
ApiKeys: cfg.ApiKeys,
|
||||
}
|
||||
|
||||
cliClients := make([]*client.Client, 0)
|
||||
err = filepath.Walk(cfg.AuthDir, func(path string, info fs.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !info.IsDir() && strings.HasSuffix(info.Name(), ".json") {
|
||||
log.Debugf(path)
|
||||
f, errOpen := os.Open(path)
|
||||
if errOpen != nil {
|
||||
return errOpen
|
||||
}
|
||||
defer func() {
|
||||
_ = f.Close()
|
||||
}()
|
||||
|
||||
var ts auth.TokenStorage
|
||||
if err = json.NewDecoder(f).Decode(&ts); err == nil {
|
||||
// 2. Initialize authenticated HTTP Client
|
||||
clientCtx := context.Background()
|
||||
|
||||
log.Info("Initializing authentication...")
|
||||
httpClient, errGetClient := auth.GetAuthenticatedClient(clientCtx, &ts, cfg.AuthDir)
|
||||
if errGetClient != nil {
|
||||
log.Fatalf("failed to get authenticated client: %v", errGetClient)
|
||||
return errGetClient
|
||||
}
|
||||
log.Info("Authentication successful.")
|
||||
|
||||
// 3. Initialize CLI Client
|
||||
cliClient := client.NewClient(httpClient)
|
||||
if err = cliClient.SetupUser(clientCtx, ts.Email, ts.ProjectID); err != nil {
|
||||
if err.Error() == "failed to start user onboarding, need define a project id" {
|
||||
log.Error("failed to start user onboarding")
|
||||
project, errGetProjectList := cliClient.GetProjectList(clientCtx)
|
||||
if errGetProjectList != nil {
|
||||
log.Fatalf("failed to complete user setup: %v", err)
|
||||
} else {
|
||||
log.Infof("Your account %s needs specify a project id.", ts.Email)
|
||||
log.Info("========================================================================")
|
||||
for i := 0; i < len(project.Projects); i++ {
|
||||
log.Infof("Project ID: %s", project.Projects[i].ProjectID)
|
||||
log.Infof("Project Name: %s", project.Projects[i].Name)
|
||||
log.Info("========================================================================")
|
||||
}
|
||||
log.Infof("Please run this command to login again:\n\n%s --login --project_id <project_id>\n", os.Args[0])
|
||||
}
|
||||
} else {
|
||||
// Log as a warning because in some cases, the CLI might still be usable
|
||||
// or the user might want to retry setup later.
|
||||
log.Fatalf("failed to complete user setup: %v", err)
|
||||
}
|
||||
} else {
|
||||
cliClients = append(cliClients, cliClient)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Create API server
|
||||
apiServer := api.NewServer(apiConfig, cliClients)
|
||||
log.Infof("Starting API server on port %s", apiConfig.Port)
|
||||
if err = apiServer.Start(); err != nil {
|
||||
log.Fatalf("API server failed to start: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Set up graceful shutdown
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-sigChan:
|
||||
log.Debugf("Received shutdown signal. Cleaning up...")
|
||||
|
||||
// Create shutdown context
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
_ = ctx // Mark ctx as used to avoid error, as apiServer.Stop(ctx) is commented out
|
||||
|
||||
// Stop API server
|
||||
if err = apiServer.Stop(ctx); err != nil {
|
||||
log.Debugf("Error stopping API server: %v", err)
|
||||
}
|
||||
cancel()
|
||||
|
||||
log.Debugf("Cleanup completed. Exiting...")
|
||||
os.Exit(0)
|
||||
case <-time.After(5 * time.Second):
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
6
config.yaml
Normal file
6
config.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
port: 8317
|
||||
auth_dir: "~/.cli-proxy-api"
|
||||
debug: false
|
||||
api_keys:
|
||||
- "12345"
|
||||
- "23456"
|
||||
44
go.mod
Normal file
44
go.mod
Normal file
@@ -0,0 +1,44 @@
|
||||
module github.com/luispater/CLIProxyAPI
|
||||
|
||||
go 1.24
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.10.1
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
github.com/tidwall/sjson v1.2.5
|
||||
golang.org/x/oauth2 v0.30.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute/metadata v0.3.0 // indirect
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/net v0.37.1-0.20250305215238-2914f4677317 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
)
|
||||
107
go.sum
Normal file
107
go.sum
Normal file
@@ -0,0 +1,107 @@
|
||||
cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc=
|
||||
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
|
||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
|
||||
github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/net v0.37.1-0.20250305215238-2914f4677317 h1:wneCP+2d9NUmndnyTmY7VwUNYiP26xiN/AtdcojQ1lI=
|
||||
golang.org/x/net v0.37.1-0.20250305215238-2914f4677317/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
|
||||
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
724
internal/api/handlers.go
Normal file
724
internal/api/handlers.go
Normal file
@@ -0,0 +1,724 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/luispater/CLIProxyAPI/internal/client"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
var (
|
||||
mutex = &sync.Mutex{}
|
||||
lastUsedClientIndex = 0
|
||||
)
|
||||
|
||||
// APIHandlers contains the handlers for API endpoints
|
||||
type APIHandlers struct {
|
||||
cliClients []*client.Client
|
||||
debug bool
|
||||
}
|
||||
|
||||
// NewAPIHandlers creates a new API handlers instance
|
||||
func NewAPIHandlers(cliClients []*client.Client, debug bool) *APIHandlers {
|
||||
return &APIHandlers{
|
||||
cliClients: cliClients,
|
||||
debug: debug,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *APIHandlers) Models(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"data": []map[string]any{
|
||||
{
|
||||
"id": "gemini-2.5-pro-preview-05-06",
|
||||
"object": "model",
|
||||
"version": "2.5-preview-05-06",
|
||||
"name": "Gemini 2.5 Pro Preview 05-06",
|
||||
"description": "Preview release (May 6th, 2025) of Gemini 2.5 Pro",
|
||||
"context_length": 1048576,
|
||||
"max_completion_tokens": 65536,
|
||||
"supported_parameters": []string{
|
||||
"tools",
|
||||
"temperature",
|
||||
"top_p",
|
||||
"top_k",
|
||||
},
|
||||
"temperature": 1,
|
||||
"topP": 0.95,
|
||||
"topK": 64,
|
||||
"maxTemperature": 2,
|
||||
"thinking": true,
|
||||
},
|
||||
{
|
||||
"id": "gemini-2.5-pro-preview-06-05",
|
||||
"object": "model",
|
||||
"version": "2.5-preview-06-05",
|
||||
"name": "Gemini 2.5 Pro Preview",
|
||||
"description": "Preview release (June 5th, 2025) of Gemini 2.5 Pro",
|
||||
"context_length": 1048576,
|
||||
"max_completion_tokens": 65536,
|
||||
"supported_parameters": []string{
|
||||
"tools",
|
||||
"temperature",
|
||||
"top_p",
|
||||
"top_k",
|
||||
},
|
||||
"temperature": 1,
|
||||
"topP": 0.95,
|
||||
"topK": 64,
|
||||
"maxTemperature": 2,
|
||||
"thinking": true,
|
||||
},
|
||||
{
|
||||
"id": "gemini-2.5-pro",
|
||||
"object": "model",
|
||||
"version": "2.5",
|
||||
"name": "Gemini 2.5 Pro",
|
||||
"description": "Stable release (June 17th, 2025) of Gemini 2.5 Pro",
|
||||
"context_length": 1048576,
|
||||
"max_completion_tokens": 65536,
|
||||
"supported_parameters": []string{
|
||||
"tools",
|
||||
"temperature",
|
||||
"top_p",
|
||||
"top_k",
|
||||
},
|
||||
"temperature": 1,
|
||||
"topP": 0.95,
|
||||
"topK": 64,
|
||||
"maxTemperature": 2,
|
||||
"thinking": true,
|
||||
},
|
||||
{
|
||||
"id": "gemini-2.5-flash-preview-04-17",
|
||||
"object": "model",
|
||||
"version": "2.5-preview-04-17",
|
||||
"name": "Gemini 2.5 Flash Preview 04-17",
|
||||
"description": "Preview release (April 17th, 2025) of Gemini 2.5 Flash",
|
||||
"context_length": 1048576,
|
||||
"max_completion_tokens": 65536,
|
||||
"supported_parameters": []string{
|
||||
"tools",
|
||||
"temperature",
|
||||
"top_p",
|
||||
"top_k",
|
||||
},
|
||||
"temperature": 1,
|
||||
"topP": 0.95,
|
||||
"topK": 64,
|
||||
"maxTemperature": 2,
|
||||
"thinking": true,
|
||||
},
|
||||
{
|
||||
"id": "gemini-2.5-flash-preview-05-20",
|
||||
"object": "model",
|
||||
"version": "2.5-preview-05-20",
|
||||
"name": "Gemini 2.5 Flash Preview 05-20",
|
||||
"description": "Preview release (April 17th, 2025) of Gemini 2.5 Flash",
|
||||
"context_length": 1048576,
|
||||
"max_completion_tokens": 65536,
|
||||
"supported_parameters": []string{
|
||||
"tools",
|
||||
"temperature",
|
||||
"top_p",
|
||||
"top_k",
|
||||
},
|
||||
"temperature": 1,
|
||||
"topP": 0.95,
|
||||
"topK": 64,
|
||||
"maxTemperature": 2,
|
||||
"thinking": true,
|
||||
},
|
||||
{
|
||||
"id": "gemini-2.5-flash",
|
||||
"object": "model",
|
||||
"version": "001",
|
||||
"name": "Gemini 2.5 Flash",
|
||||
"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.",
|
||||
"context_length": 1048576,
|
||||
"max_completion_tokens": 65536,
|
||||
"supported_parameters": []string{
|
||||
"tools",
|
||||
"temperature",
|
||||
"top_p",
|
||||
"top_k",
|
||||
},
|
||||
"temperature": 1,
|
||||
"topP": 0.95,
|
||||
"topK": 64,
|
||||
"maxTemperature": 2,
|
||||
"thinking": true,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ChatCompletions handles the /v1/chat/completions endpoint
|
||||
func (h *APIHandlers) ChatCompletions(c *gin.Context) {
|
||||
rawJson, err := c.GetRawData()
|
||||
// If data retrieval fails, return 400 error
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid request: %v", err), "code": 400})
|
||||
return
|
||||
}
|
||||
|
||||
streamResult := gjson.GetBytes(rawJson, "stream")
|
||||
if streamResult.Type == gjson.True {
|
||||
h.handleStreamingResponse(c, rawJson)
|
||||
} else {
|
||||
h.handleNonStreamingResponse(c, rawJson)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *APIHandlers) prepareRequest(rawJson []byte) (string, []client.Content, []client.ToolDeclaration) {
|
||||
// log.Debug(string(rawJson))
|
||||
modelName := "gemini-2.5-pro"
|
||||
modelResult := gjson.GetBytes(rawJson, "model")
|
||||
if modelResult.Type == gjson.String {
|
||||
modelName = modelResult.String()
|
||||
}
|
||||
|
||||
contents := make([]client.Content, 0)
|
||||
messagesResult := gjson.GetBytes(rawJson, "messages")
|
||||
if messagesResult.IsArray() {
|
||||
messagesResults := messagesResult.Array()
|
||||
for i := 0; i < len(messagesResults); i++ {
|
||||
messageResult := messagesResults[i]
|
||||
roleResult := messageResult.Get("role")
|
||||
contentResult := messageResult.Get("content")
|
||||
if roleResult.Type == gjson.String {
|
||||
if roleResult.String() == "system" {
|
||||
if contentResult.Type == gjson.String {
|
||||
contents = append(contents, client.Content{Role: "user", Parts: []client.Part{{Text: contentResult.String()}}})
|
||||
} else if contentResult.IsObject() {
|
||||
contentTypeResult := contentResult.Get("type")
|
||||
if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "text" {
|
||||
contentTextResult := contentResult.Get("text")
|
||||
if contentTextResult.Type == gjson.String {
|
||||
contents = append(contents, client.Content{Role: "user", Parts: []client.Part{{Text: contentTextResult.String()}}})
|
||||
contents = append(contents, client.Content{Role: "model", Parts: []client.Part{{Text: "Understood. I will follow these instructions and use my tools to assist you."}}})
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if roleResult.String() == "user" {
|
||||
if contentResult.Type == gjson.String {
|
||||
contents = append(contents, client.Content{Role: "user", Parts: []client.Part{{Text: contentResult.String()}}})
|
||||
} else if contentResult.IsObject() {
|
||||
contentTypeResult := contentResult.Get("type")
|
||||
if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "text" {
|
||||
contentTextResult := contentResult.Get("text")
|
||||
if contentTextResult.Type == gjson.String {
|
||||
contents = append(contents, client.Content{Role: "user", Parts: []client.Part{{Text: contentTextResult.String()}}})
|
||||
}
|
||||
}
|
||||
} else if contentResult.IsArray() {
|
||||
contentItemResults := contentResult.Array()
|
||||
parts := make([]client.Part, 0)
|
||||
for j := 0; j < len(contentItemResults); j++ {
|
||||
contentItemResult := contentItemResults[j]
|
||||
contentTypeResult := contentItemResult.Get("type")
|
||||
if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "text" {
|
||||
contentTextResult := contentItemResult.Get("text")
|
||||
if contentTextResult.Type == gjson.String {
|
||||
parts = append(parts, client.Part{Text: contentTextResult.String()})
|
||||
}
|
||||
} else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "image_url" {
|
||||
imageURLResult := contentItemResult.Get("image_url.url")
|
||||
if imageURLResult.Type == gjson.String {
|
||||
imageURL := imageURLResult.String()
|
||||
if len(imageURL) > 5 {
|
||||
imageURLs := strings.SplitN(imageURL[5:], ";", 2)
|
||||
if len(imageURLs) == 2 {
|
||||
if len(imageURLs[1]) > 7 {
|
||||
parts = append(parts, client.Part{InlineData: &client.InlineData{
|
||||
MimeType: imageURLs[0],
|
||||
Data: imageURLs[1][7:],
|
||||
}})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "file" {
|
||||
filenameResult := contentItemResult.Get("file.filename")
|
||||
fileDataResult := contentItemResult.Get("file.file_data")
|
||||
if filenameResult.Type == gjson.String && fileDataResult.Type == gjson.String {
|
||||
filename := filenameResult.String()
|
||||
splitFilename := strings.Split(filename, ".")
|
||||
ext := splitFilename[len(splitFilename)-1]
|
||||
|
||||
mimeType, ok := MimeTypes[ext]
|
||||
if !ok {
|
||||
log.Warnf("Unknown file name extension '%s' at index %d, skipping file", ext, j)
|
||||
continue
|
||||
}
|
||||
|
||||
parts = append(parts, client.Part{InlineData: &client.InlineData{
|
||||
MimeType: mimeType,
|
||||
Data: fileDataResult.String(),
|
||||
}})
|
||||
}
|
||||
}
|
||||
}
|
||||
contents = append(contents, client.Content{Role: "user", Parts: parts})
|
||||
}
|
||||
} else if roleResult.String() == "assistant" {
|
||||
if contentResult.Type == gjson.String {
|
||||
contents = append(contents, client.Content{Role: "model", Parts: []client.Part{{Text: contentResult.String()}}})
|
||||
} else if contentResult.IsObject() {
|
||||
contentTypeResult := contentResult.Get("type")
|
||||
if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "text" {
|
||||
contentTextResult := contentResult.Get("text")
|
||||
if contentTextResult.Type == gjson.String {
|
||||
contents = append(contents, client.Content{Role: "user", Parts: []client.Part{{Text: contentTextResult.String()}}})
|
||||
}
|
||||
}
|
||||
} else if !contentResult.Exists() || contentResult.Type == gjson.Null {
|
||||
toolCallsResult := messageResult.Get("tool_calls")
|
||||
if toolCallsResult.IsArray() {
|
||||
tcsResult := toolCallsResult.Array()
|
||||
for j := 0; j < len(tcsResult); j++ {
|
||||
tcResult := tcsResult[j]
|
||||
functionNameResult := tcResult.Get("function.name")
|
||||
functionArguments := tcResult.Get("function.arguments")
|
||||
if functionNameResult.Exists() && functionNameResult.Type == gjson.String && functionArguments.Exists() && functionArguments.Type == gjson.String {
|
||||
var args map[string]any
|
||||
err := json.Unmarshal([]byte(functionArguments.String()), &args)
|
||||
if err == nil {
|
||||
contents = append(contents, client.Content{
|
||||
Role: "model", Parts: []client.Part{
|
||||
{
|
||||
FunctionCall: &client.FunctionCall{
|
||||
Name: functionNameResult.String(),
|
||||
Args: args,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if roleResult.String() == "tool" {
|
||||
toolCallIDResult := messageResult.Get("tool_call_id")
|
||||
if toolCallIDResult.Exists() && toolCallIDResult.Type == gjson.String {
|
||||
if contentResult.Type == gjson.String {
|
||||
functionResponse := client.FunctionResponse{Name: toolCallIDResult.String(), Response: map[string]interface{}{"result": contentResult.String()}}
|
||||
contents = append(contents, client.Content{Role: "tool", Parts: []client.Part{{FunctionResponse: &functionResponse}}})
|
||||
} else if contentResult.IsObject() {
|
||||
contentTypeResult := contentResult.Get("type")
|
||||
if contentTypeResult.Type == gjson.String && contentTypeResult.String() == "text" {
|
||||
contentTextResult := contentResult.Get("text")
|
||||
if contentTextResult.Type == gjson.String {
|
||||
functionResponse := client.FunctionResponse{Name: toolCallIDResult.String(), Response: map[string]interface{}{"result": contentResult.String()}}
|
||||
contents = append(contents, client.Content{Role: "tool", Parts: []client.Part{{FunctionResponse: &functionResponse}}})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var tools []client.ToolDeclaration
|
||||
toolsResult := gjson.GetBytes(rawJson, "tools")
|
||||
if toolsResult.IsArray() {
|
||||
tools = make([]client.ToolDeclaration, 1)
|
||||
tools[0].FunctionDeclarations = make([]any, 0)
|
||||
toolsResults := toolsResult.Array()
|
||||
for i := 0; i < len(toolsResults); i++ {
|
||||
toolTypeResult := toolsResults[i].Get("type")
|
||||
if toolTypeResult.Type != gjson.String || toolTypeResult.String() != "function" {
|
||||
continue
|
||||
}
|
||||
functionTypeResult := toolsResults[i].Get("function")
|
||||
if functionTypeResult.Exists() && functionTypeResult.IsObject() {
|
||||
var functionDeclaration any
|
||||
err := json.Unmarshal([]byte(functionTypeResult.Raw), &functionDeclaration)
|
||||
if err == nil {
|
||||
tools[0].FunctionDeclarations = append(tools[0].FunctionDeclarations, functionDeclaration)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tools = make([]client.ToolDeclaration, 0)
|
||||
}
|
||||
return modelName, contents, tools
|
||||
}
|
||||
|
||||
// handleNonStreamingResponse handles non-streaming responses
|
||||
func (h *APIHandlers) handleNonStreamingResponse(c *gin.Context, rawJson []byte) {
|
||||
c.Header("Content-Type", "application/json")
|
||||
|
||||
// Handle streaming manually
|
||||
flusher, ok := c.Writer.(http.Flusher)
|
||||
if !ok {
|
||||
c.JSON(http.StatusInternalServerError, ErrorResponse{
|
||||
Error: ErrorDetail{
|
||||
Message: "Streaming not supported",
|
||||
Type: "server_error",
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
modelName, contents, tools := h.prepareRequest(rawJson)
|
||||
cliCtx, cliCancel := context.WithCancel(context.Background())
|
||||
var cliClient *client.Client
|
||||
defer func() {
|
||||
if cliClient != nil {
|
||||
cliClient.RequestMutex.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
// Lock the mutex to update the last used page index
|
||||
mutex.Lock()
|
||||
startIndex := lastUsedClientIndex
|
||||
currentIndex := (startIndex + 1) % len(h.cliClients)
|
||||
lastUsedClientIndex = currentIndex
|
||||
mutex.Unlock()
|
||||
|
||||
// Reorder the pages to start from the last used index
|
||||
reorderedPages := make([]*client.Client, len(h.cliClients))
|
||||
for i := 0; i < len(h.cliClients); i++ {
|
||||
reorderedPages[i] = h.cliClients[(startIndex+1+i)%len(h.cliClients)]
|
||||
}
|
||||
|
||||
locked := false
|
||||
for i := 0; i < len(reorderedPages); i++ {
|
||||
cliClient = reorderedPages[i]
|
||||
if cliClient.RequestMutex.TryLock() {
|
||||
locked = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !locked {
|
||||
cliClient = h.cliClients[0]
|
||||
cliClient.RequestMutex.Lock()
|
||||
}
|
||||
|
||||
log.Debugf("Request use account: %s", cliClient.Email)
|
||||
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}]}`
|
||||
respChan, errChan := cliClient.SendMessageStream(cliCtx, rawJson, modelName, contents, tools)
|
||||
for {
|
||||
select {
|
||||
case <-c.Request.Context().Done():
|
||||
if c.Request.Context().Err().Error() == "context canceled" {
|
||||
log.Debugf("Client disconnected: %v", c.Request.Context().Err())
|
||||
cliCancel()
|
||||
return
|
||||
}
|
||||
case chunk, okStream := <-respChan:
|
||||
if !okStream {
|
||||
_, _ = fmt.Fprint(c.Writer, jsonTemplate)
|
||||
flusher.Flush()
|
||||
cliCancel()
|
||||
return
|
||||
} else {
|
||||
jsonTemplate = h.convertCliToOpenAINonStream(jsonTemplate, chunk)
|
||||
}
|
||||
case err, okError := <-errChan:
|
||||
if okError {
|
||||
c.JSON(http.StatusInternalServerError, ErrorResponse{
|
||||
Error: ErrorDetail{
|
||||
Message: err.Error(),
|
||||
Type: "server_error",
|
||||
},
|
||||
})
|
||||
cliCancel()
|
||||
return
|
||||
}
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
_, _ = c.Writer.Write([]byte("\n"))
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleStreamingResponse handles streaming responses
|
||||
func (h *APIHandlers) handleStreamingResponse(c *gin.Context, rawJson []byte) {
|
||||
c.Header("Content-Type", "text/event-stream")
|
||||
c.Header("Cache-Control", "no-cache")
|
||||
c.Header("Connection", "keep-alive")
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
|
||||
// Handle streaming manually
|
||||
flusher, ok := c.Writer.(http.Flusher)
|
||||
if !ok {
|
||||
c.JSON(http.StatusInternalServerError, ErrorResponse{
|
||||
Error: ErrorDetail{
|
||||
Message: "Streaming not supported",
|
||||
Type: "server_error",
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
modelName, contents, tools := h.prepareRequest(rawJson)
|
||||
cliCtx, cliCancel := context.WithCancel(context.Background())
|
||||
var cliClient *client.Client
|
||||
defer func() {
|
||||
if cliClient != nil {
|
||||
cliClient.RequestMutex.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
// Lock the mutex to update the last used page index
|
||||
mutex.Lock()
|
||||
startIndex := lastUsedClientIndex
|
||||
currentIndex := (startIndex + 1) % len(h.cliClients)
|
||||
lastUsedClientIndex = currentIndex
|
||||
mutex.Unlock()
|
||||
|
||||
// Reorder the pages to start from the last used index
|
||||
reorderedPages := make([]*client.Client, len(h.cliClients))
|
||||
for i := 0; i < len(h.cliClients); i++ {
|
||||
reorderedPages[i] = h.cliClients[(startIndex+1+i)%len(h.cliClients)]
|
||||
}
|
||||
|
||||
locked := false
|
||||
for i := 0; i < len(reorderedPages); i++ {
|
||||
cliClient = reorderedPages[i]
|
||||
if cliClient.RequestMutex.TryLock() {
|
||||
locked = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !locked {
|
||||
cliClient = h.cliClients[0]
|
||||
cliClient.RequestMutex.Lock()
|
||||
}
|
||||
|
||||
log.Debugf("Request use account: %s", cliClient.Email)
|
||||
respChan, errChan := cliClient.SendMessageStream(cliCtx, rawJson, modelName, contents, tools)
|
||||
for {
|
||||
select {
|
||||
case <-c.Request.Context().Done():
|
||||
if c.Request.Context().Err().Error() == "context canceled" {
|
||||
log.Debugf("Client disconnected: %v", c.Request.Context().Err())
|
||||
cliCancel()
|
||||
return
|
||||
}
|
||||
case chunk, okStream := <-respChan:
|
||||
if !okStream {
|
||||
_, _ = fmt.Fprintf(c.Writer, "data: [DONE]\n\n")
|
||||
flusher.Flush()
|
||||
cliCancel()
|
||||
return
|
||||
} else {
|
||||
openAIFormat := h.convertCliToOpenAI(chunk)
|
||||
if openAIFormat != "" {
|
||||
_, _ = fmt.Fprintf(c.Writer, "data: %s\n\n", openAIFormat)
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
case err, okError := <-errChan:
|
||||
if okError {
|
||||
c.JSON(http.StatusInternalServerError, ErrorResponse{
|
||||
Error: ErrorDetail{
|
||||
Message: err.Error(),
|
||||
Type: "server_error",
|
||||
},
|
||||
})
|
||||
cliCancel()
|
||||
return
|
||||
}
|
||||
case <-time.After(500 * time.Millisecond):
|
||||
_, _ = c.Writer.Write([]byte(": CLI-PROXY-API PROCESSING\n\n"))
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *APIHandlers) convertCliToOpenAI(rawJson []byte) string {
|
||||
// log.Debugf(string(rawJson))
|
||||
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}]}`
|
||||
|
||||
modelVersionResult := gjson.GetBytes(rawJson, "response.modelVersion")
|
||||
if modelVersionResult.Exists() && modelVersionResult.Type == gjson.String {
|
||||
template, _ = sjson.Set(template, "model", modelVersionResult.String())
|
||||
}
|
||||
|
||||
createTimeResult := gjson.GetBytes(rawJson, "response.createTime")
|
||||
if createTimeResult.Exists() && createTimeResult.Type == gjson.String {
|
||||
t, err := time.Parse(time.RFC3339Nano, createTimeResult.String())
|
||||
var unixTimestamp int64
|
||||
if err == nil {
|
||||
unixTimestamp = t.Unix()
|
||||
} else {
|
||||
unixTimestamp = time.Now().Unix()
|
||||
}
|
||||
template, _ = sjson.Set(template, "created", unixTimestamp)
|
||||
}
|
||||
|
||||
responseIdResult := gjson.GetBytes(rawJson, "response.responseId")
|
||||
if responseIdResult.Exists() && responseIdResult.Type == gjson.String {
|
||||
template, _ = sjson.Set(template, "id", responseIdResult.String())
|
||||
}
|
||||
|
||||
finishReasonResult := gjson.GetBytes(rawJson, "response.candidates.0.finishReason")
|
||||
if finishReasonResult.Exists() && finishReasonResult.Type == gjson.String {
|
||||
template, _ = sjson.Set(template, "choices.0.finish_reason", finishReasonResult.String())
|
||||
template, _ = sjson.Set(template, "choices.0.native_finish_reason", finishReasonResult.String())
|
||||
}
|
||||
|
||||
usageResult := gjson.GetBytes(rawJson, "response.usageMetadata")
|
||||
candidatesTokenCountResult := usageResult.Get("candidatesTokenCount")
|
||||
if candidatesTokenCountResult.Exists() && candidatesTokenCountResult.Type == gjson.Number {
|
||||
template, _ = sjson.Set(template, "usage.completion_tokens", candidatesTokenCountResult.Int())
|
||||
}
|
||||
totalTokenCountResult := usageResult.Get("totalTokenCount")
|
||||
if totalTokenCountResult.Exists() && totalTokenCountResult.Type == gjson.Number {
|
||||
template, _ = sjson.Set(template, "usage.total_tokens", totalTokenCountResult.Int())
|
||||
}
|
||||
thoughtsTokenCountResult := usageResult.Get("thoughtsTokenCount")
|
||||
promptTokenCountResult := usageResult.Get("promptTokenCount")
|
||||
if promptTokenCountResult.Exists() && promptTokenCountResult.Type == gjson.Number {
|
||||
if thoughtsTokenCountResult.Exists() && thoughtsTokenCountResult.Type == gjson.Number {
|
||||
template, _ = sjson.Set(template, "usage.prompt_tokens", promptTokenCountResult.Int()+thoughtsTokenCountResult.Int())
|
||||
} else {
|
||||
template, _ = sjson.Set(template, "usage.prompt_tokens", promptTokenCountResult.Int())
|
||||
}
|
||||
}
|
||||
if thoughtsTokenCountResult.Exists() && thoughtsTokenCountResult.Type == gjson.Number {
|
||||
template, _ = sjson.Set(template, "usage.completion_tokens_details.reasoning_tokens", thoughtsTokenCountResult.Int())
|
||||
}
|
||||
|
||||
partResult := gjson.GetBytes(rawJson, "response.candidates.0.content.parts.0")
|
||||
partTextResult := partResult.Get("text")
|
||||
functionCallResult := partResult.Get("functionCall")
|
||||
|
||||
if partTextResult.Exists() && partTextResult.Type == gjson.String {
|
||||
partThoughtResult := partResult.Get("thought")
|
||||
if partThoughtResult.Exists() && partThoughtResult.Type == gjson.True {
|
||||
template, _ = sjson.Set(template, "choices.0.delta.reasoning_content", partTextResult.String())
|
||||
} else {
|
||||
template, _ = sjson.Set(template, "choices.0.delta.content", partTextResult.String())
|
||||
}
|
||||
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
|
||||
} else if functionCallResult.Exists() {
|
||||
functionCallTemplate := `[{"id": "","type": "function","function": {"name": "","arguments": ""}}]`
|
||||
fcNameResult := functionCallResult.Get("name")
|
||||
if fcNameResult.Exists() && fcNameResult.Type == gjson.String {
|
||||
functionCallTemplate, _ = sjson.Set(functionCallTemplate, "0.id", fcNameResult.String())
|
||||
functionCallTemplate, _ = sjson.Set(functionCallTemplate, "0.function.name", fcNameResult.String())
|
||||
}
|
||||
fcArgsResult := functionCallResult.Get("args")
|
||||
if fcArgsResult.Exists() && fcArgsResult.IsObject() {
|
||||
functionCallTemplate, _ = sjson.Set(functionCallTemplate, "0.function.arguments", fcArgsResult.Raw)
|
||||
}
|
||||
template, _ = sjson.Set(template, "choices.0.delta.role", "assistant")
|
||||
template, _ = sjson.SetRaw(template, "choices.0.delta.tool_calls", functionCallTemplate)
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
|
||||
return template
|
||||
}
|
||||
|
||||
func (h *APIHandlers) convertCliToOpenAINonStream(template string, rawJson []byte) string {
|
||||
modelVersionResult := gjson.GetBytes(rawJson, "response.modelVersion")
|
||||
if modelVersionResult.Exists() && modelVersionResult.Type == gjson.String {
|
||||
template, _ = sjson.Set(template, "model", modelVersionResult.String())
|
||||
}
|
||||
|
||||
createTimeResult := gjson.GetBytes(rawJson, "response.createTime")
|
||||
if createTimeResult.Exists() && createTimeResult.Type == gjson.String {
|
||||
t, err := time.Parse(time.RFC3339Nano, createTimeResult.String())
|
||||
var unixTimestamp int64
|
||||
if err == nil {
|
||||
unixTimestamp = t.Unix()
|
||||
} else {
|
||||
unixTimestamp = time.Now().Unix()
|
||||
}
|
||||
template, _ = sjson.Set(template, "created", unixTimestamp)
|
||||
}
|
||||
|
||||
responseIdResult := gjson.GetBytes(rawJson, "response.responseId")
|
||||
if responseIdResult.Exists() && responseIdResult.Type == gjson.String {
|
||||
template, _ = sjson.Set(template, "id", responseIdResult.String())
|
||||
}
|
||||
|
||||
finishReasonResult := gjson.GetBytes(rawJson, "response.candidates.0.finishReason")
|
||||
if finishReasonResult.Exists() && finishReasonResult.Type == gjson.String {
|
||||
template, _ = sjson.Set(template, "choices.0.finish_reason", finishReasonResult.String())
|
||||
template, _ = sjson.Set(template, "choices.0.native_finish_reason", finishReasonResult.String())
|
||||
}
|
||||
|
||||
usageResult := gjson.GetBytes(rawJson, "response.usageMetadata")
|
||||
candidatesTokenCountResult := usageResult.Get("candidatesTokenCount")
|
||||
if candidatesTokenCountResult.Exists() && candidatesTokenCountResult.Type == gjson.Number {
|
||||
template, _ = sjson.Set(template, "usage.completion_tokens", candidatesTokenCountResult.Int())
|
||||
}
|
||||
totalTokenCountResult := usageResult.Get("totalTokenCount")
|
||||
if totalTokenCountResult.Exists() && totalTokenCountResult.Type == gjson.Number {
|
||||
template, _ = sjson.Set(template, "usage.total_tokens", totalTokenCountResult.Int())
|
||||
}
|
||||
thoughtsTokenCountResult := usageResult.Get("thoughtsTokenCount")
|
||||
promptTokenCountResult := usageResult.Get("promptTokenCount")
|
||||
if promptTokenCountResult.Exists() && promptTokenCountResult.Type == gjson.Number {
|
||||
if thoughtsTokenCountResult.Exists() && thoughtsTokenCountResult.Type == gjson.Number {
|
||||
template, _ = sjson.Set(template, "usage.prompt_tokens", promptTokenCountResult.Int()+thoughtsTokenCountResult.Int())
|
||||
} else {
|
||||
template, _ = sjson.Set(template, "usage.prompt_tokens", promptTokenCountResult.Int())
|
||||
}
|
||||
}
|
||||
if thoughtsTokenCountResult.Exists() && thoughtsTokenCountResult.Type == gjson.Number {
|
||||
template, _ = sjson.Set(template, "usage.completion_tokens_details.reasoning_tokens", thoughtsTokenCountResult.Int())
|
||||
}
|
||||
|
||||
partResult := gjson.GetBytes(rawJson, "response.candidates.0.content.parts.0")
|
||||
partTextResult := partResult.Get("text")
|
||||
functionCallResult := partResult.Get("functionCall")
|
||||
|
||||
if partTextResult.Exists() && partTextResult.Type == gjson.String {
|
||||
partThoughtResult := partResult.Get("thought")
|
||||
if partThoughtResult.Exists() && partThoughtResult.Type == gjson.True {
|
||||
reasoningContentResult := gjson.Get(template, "choices.0.message.reasoning_content")
|
||||
if reasoningContentResult.Type == gjson.String {
|
||||
template, _ = sjson.Set(template, "choices.0.message.reasoning_content", reasoningContentResult.String()+partTextResult.String())
|
||||
} else {
|
||||
template, _ = sjson.Set(template, "choices.0.message.reasoning_content", partTextResult.String())
|
||||
}
|
||||
} else {
|
||||
reasoningContentResult := gjson.Get(template, "choices.0.message.content")
|
||||
if reasoningContentResult.Type == gjson.String {
|
||||
template, _ = sjson.Set(template, "choices.0.message.content", reasoningContentResult.String()+partTextResult.String())
|
||||
} else {
|
||||
template, _ = sjson.Set(template, "choices.0.message.content", partTextResult.String())
|
||||
}
|
||||
}
|
||||
template, _ = sjson.Set(template, "choices.0.message.role", "assistant")
|
||||
} else if functionCallResult.Exists() {
|
||||
toolCallsResult := gjson.Get(template, "choices.0.message.tool_calls")
|
||||
if !toolCallsResult.Exists() || toolCallsResult.Type == gjson.Null {
|
||||
template, _ = sjson.SetRaw(template, "choices.0.message.tool_calls", `[]`)
|
||||
}
|
||||
|
||||
functionCallItemTemplate := `{"id": "","type": "function","function": {"name": "","arguments": ""}}`
|
||||
fcNameResult := functionCallResult.Get("name")
|
||||
if fcNameResult.Exists() && fcNameResult.Type == gjson.String {
|
||||
functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "id", fcNameResult.String())
|
||||
functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "function.name", fcNameResult.String())
|
||||
}
|
||||
fcArgsResult := functionCallResult.Get("args")
|
||||
if fcArgsResult.Exists() && fcArgsResult.IsObject() {
|
||||
functionCallItemTemplate, _ = sjson.Set(functionCallItemTemplate, "function.arguments", fcArgsResult.Raw)
|
||||
}
|
||||
template, _ = sjson.Set(template, "choices.0.message.role", "assistant")
|
||||
template, _ = sjson.SetRaw(template, "choices.0.message.tool_calls.-1", functionCallItemTemplate)
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
|
||||
return template
|
||||
}
|
||||
736
internal/api/mine-type.go
Normal file
736
internal/api/mine-type.go
Normal file
@@ -0,0 +1,736 @@
|
||||
package api
|
||||
|
||||
var MimeTypes = map[string]string{
|
||||
"ez": "application/andrew-inset",
|
||||
"aw": "application/applixware",
|
||||
"atom": "application/atom+xml",
|
||||
"atomcat": "application/atomcat+xml",
|
||||
"atomsvc": "application/atomsvc+xml",
|
||||
"ccxml": "application/ccxml+xml",
|
||||
"cdmia": "application/cdmi-capability",
|
||||
"cdmic": "application/cdmi-container",
|
||||
"cdmid": "application/cdmi-domain",
|
||||
"cdmio": "application/cdmi-object",
|
||||
"cdmiq": "application/cdmi-queue",
|
||||
"cu": "application/cu-seeme",
|
||||
"davmount": "application/davmount+xml",
|
||||
"dbk": "application/docbook+xml",
|
||||
"dssc": "application/dssc+der",
|
||||
"xdssc": "application/dssc+xml",
|
||||
"ecma": "application/ecmascript",
|
||||
"emma": "application/emma+xml",
|
||||
"epub": "application/epub+zip",
|
||||
"exi": "application/exi",
|
||||
"pfr": "application/font-tdpfr",
|
||||
"gml": "application/gml+xml",
|
||||
"gpx": "application/gpx+xml",
|
||||
"gxf": "application/gxf",
|
||||
"stk": "application/hyperstudio",
|
||||
"ink": "application/inkml+xml",
|
||||
"ipfix": "application/ipfix",
|
||||
"jar": "application/java-archive",
|
||||
"ser": "application/java-serialized-object",
|
||||
"class": "application/java-vm",
|
||||
"js": "application/javascript",
|
||||
"json": "application/json",
|
||||
"jsonml": "application/jsonml+json",
|
||||
"lostxml": "application/lost+xml",
|
||||
"hqx": "application/mac-binhex40",
|
||||
"cpt": "application/mac-compactpro",
|
||||
"mads": "application/mads+xml",
|
||||
"mrc": "application/marc",
|
||||
"mrcx": "application/marcxml+xml",
|
||||
"ma": "application/mathematica",
|
||||
"mathml": "application/mathml+xml",
|
||||
"mbox": "application/mbox",
|
||||
"mscml": "application/mediaservercontrol+xml",
|
||||
"metalink": "application/metalink+xml",
|
||||
"meta4": "application/metalink4+xml",
|
||||
"mets": "application/mets+xml",
|
||||
"mods": "application/mods+xml",
|
||||
"m21": "application/mp21",
|
||||
"mp4s": "application/mp4",
|
||||
"doc": "application/msword",
|
||||
"mxf": "application/mxf",
|
||||
"bin": "application/octet-stream",
|
||||
"oda": "application/oda",
|
||||
"opf": "application/oebps-package+xml",
|
||||
"ogx": "application/ogg",
|
||||
"omdoc": "application/omdoc+xml",
|
||||
"onepkg": "application/onenote",
|
||||
"oxps": "application/oxps",
|
||||
"xer": "application/patch-ops-error+xml",
|
||||
"pdf": "application/pdf",
|
||||
"pgp": "application/pgp-encrypted",
|
||||
"asc": "application/pgp-signature",
|
||||
"prf": "application/pics-rules",
|
||||
"p10": "application/pkcs10",
|
||||
"p7c": "application/pkcs7-mime",
|
||||
"p7s": "application/pkcs7-signature",
|
||||
"p8": "application/pkcs8",
|
||||
"ac": "application/pkix-attr-cert",
|
||||
"cer": "application/pkix-cert",
|
||||
"crl": "application/pkix-crl",
|
||||
"pkipath": "application/pkix-pkipath",
|
||||
"pki": "application/pkixcmp",
|
||||
"pls": "application/pls+xml",
|
||||
"ai": "application/postscript",
|
||||
"cww": "application/prs.cww",
|
||||
"pskcxml": "application/pskc+xml",
|
||||
"rdf": "application/rdf+xml",
|
||||
"rif": "application/reginfo+xml",
|
||||
"rnc": "application/relax-ng-compact-syntax",
|
||||
"rld": "application/resource-lists-diff+xml",
|
||||
"rl": "application/resource-lists+xml",
|
||||
"rs": "application/rls-services+xml",
|
||||
"gbr": "application/rpki-ghostbusters",
|
||||
"mft": "application/rpki-manifest",
|
||||
"roa": "application/rpki-roa",
|
||||
"rsd": "application/rsd+xml",
|
||||
"rss": "application/rss+xml",
|
||||
"rtf": "application/rtf",
|
||||
"sbml": "application/sbml+xml",
|
||||
"scq": "application/scvp-cv-request",
|
||||
"scs": "application/scvp-cv-response",
|
||||
"spq": "application/scvp-vp-request",
|
||||
"spp": "application/scvp-vp-response",
|
||||
"sdp": "application/sdp",
|
||||
"setpay": "application/set-payment-initiation",
|
||||
"setreg": "application/set-registration-initiation",
|
||||
"shf": "application/shf+xml",
|
||||
"smi": "application/smil+xml",
|
||||
"rq": "application/sparql-query",
|
||||
"srx": "application/sparql-results+xml",
|
||||
"gram": "application/srgs",
|
||||
"grxml": "application/srgs+xml",
|
||||
"sru": "application/sru+xml",
|
||||
"ssdl": "application/ssdl+xml",
|
||||
"ssml": "application/ssml+xml",
|
||||
"tei": "application/tei+xml",
|
||||
"tfi": "application/thraud+xml",
|
||||
"tsd": "application/timestamped-data",
|
||||
"plb": "application/vnd.3gpp.pic-bw-large",
|
||||
"psb": "application/vnd.3gpp.pic-bw-small",
|
||||
"pvb": "application/vnd.3gpp.pic-bw-var",
|
||||
"tcap": "application/vnd.3gpp2.tcap",
|
||||
"pwn": "application/vnd.3m.post-it-notes",
|
||||
"aso": "application/vnd.accpac.simply.aso",
|
||||
"imp": "application/vnd.accpac.simply.imp",
|
||||
"acu": "application/vnd.acucobol",
|
||||
"acutc": "application/vnd.acucorp",
|
||||
"air": "application/vnd.adobe.air-application-installer-package+zip",
|
||||
"fcdt": "application/vnd.adobe.formscentral.fcdt",
|
||||
"fxp": "application/vnd.adobe.fxp",
|
||||
"xdp": "application/vnd.adobe.xdp+xml",
|
||||
"xfdf": "application/vnd.adobe.xfdf",
|
||||
"ahead": "application/vnd.ahead.space",
|
||||
"azf": "application/vnd.airzip.filesecure.azf",
|
||||
"azs": "application/vnd.airzip.filesecure.azs",
|
||||
"azw": "application/vnd.amazon.ebook",
|
||||
"acc": "application/vnd.americandynamics.acc",
|
||||
"ami": "application/vnd.amiga.ami",
|
||||
"apk": "application/vnd.android.package-archive",
|
||||
"cii": "application/vnd.anser-web-certificate-issue-initiation",
|
||||
"fti": "application/vnd.anser-web-funds-transfer-initiation",
|
||||
"atx": "application/vnd.antix.game-component",
|
||||
"mpkg": "application/vnd.apple.installer+xml",
|
||||
"m3u8": "application/vnd.apple.mpegurl",
|
||||
"swi": "application/vnd.aristanetworks.swi",
|
||||
"iota": "application/vnd.astraea-software.iota",
|
||||
"aep": "application/vnd.audiograph",
|
||||
"mpm": "application/vnd.blueice.multipass",
|
||||
"bmi": "application/vnd.bmi",
|
||||
"rep": "application/vnd.businessobjects",
|
||||
"cdxml": "application/vnd.chemdraw+xml",
|
||||
"mmd": "application/vnd.chipnuts.karaoke-mmd",
|
||||
"cdy": "application/vnd.cinderella",
|
||||
"cla": "application/vnd.claymore",
|
||||
"rp9": "application/vnd.cloanto.rp9",
|
||||
"c4d": "application/vnd.clonk.c4group",
|
||||
"c11amc": "application/vnd.cluetrust.cartomobile-config",
|
||||
"c11amz": "application/vnd.cluetrust.cartomobile-config-pkg",
|
||||
"csp": "application/vnd.commonspace",
|
||||
"cdbcmsg": "application/vnd.contact.cmsg",
|
||||
"cmc": "application/vnd.cosmocaller",
|
||||
"clkx": "application/vnd.crick.clicker",
|
||||
"clkk": "application/vnd.crick.clicker.keyboard",
|
||||
"clkp": "application/vnd.crick.clicker.palette",
|
||||
"clkt": "application/vnd.crick.clicker.template",
|
||||
"clkw": "application/vnd.crick.clicker.wordbank",
|
||||
"wbs": "application/vnd.criticaltools.wbs+xml",
|
||||
"pml": "application/vnd.ctc-posml",
|
||||
"ppd": "application/vnd.cups-ppd",
|
||||
"car": "application/vnd.curl.car",
|
||||
"pcurl": "application/vnd.curl.pcurl",
|
||||
"dart": "application/vnd.dart",
|
||||
"rdz": "application/vnd.data-vision.rdz",
|
||||
"uvd": "application/vnd.dece.data",
|
||||
"fe_launch": "application/vnd.denovo.fcselayout-link",
|
||||
"dna": "application/vnd.dna",
|
||||
"mlp": "application/vnd.dolby.mlp",
|
||||
"dpg": "application/vnd.dpgraph",
|
||||
"dfac": "application/vnd.dreamfactory",
|
||||
"kpxx": "application/vnd.ds-keypoint",
|
||||
"ait": "application/vnd.dvb.ait",
|
||||
"svc": "application/vnd.dvb.service",
|
||||
"geo": "application/vnd.dynageo",
|
||||
"mag": "application/vnd.ecowin.chart",
|
||||
"nml": "application/vnd.enliven",
|
||||
"esf": "application/vnd.epson.esf",
|
||||
"msf": "application/vnd.epson.msf",
|
||||
"qam": "application/vnd.epson.quickanime",
|
||||
"slt": "application/vnd.epson.salt",
|
||||
"ssf": "application/vnd.epson.ssf",
|
||||
"es3": "application/vnd.eszigno3+xml",
|
||||
"ez2": "application/vnd.ezpix-album",
|
||||
"ez3": "application/vnd.ezpix-package",
|
||||
"fdf": "application/vnd.fdf",
|
||||
"mseed": "application/vnd.fdsn.mseed",
|
||||
"dataless": "application/vnd.fdsn.seed",
|
||||
"gph": "application/vnd.flographit",
|
||||
"ftc": "application/vnd.fluxtime.clip",
|
||||
"book": "application/vnd.framemaker",
|
||||
"fnc": "application/vnd.frogans.fnc",
|
||||
"ltf": "application/vnd.frogans.ltf",
|
||||
"fsc": "application/vnd.fsc.weblaunch",
|
||||
"oas": "application/vnd.fujitsu.oasys",
|
||||
"oa2": "application/vnd.fujitsu.oasys2",
|
||||
"oa3": "application/vnd.fujitsu.oasys3",
|
||||
"fg5": "application/vnd.fujitsu.oasysgp",
|
||||
"bh2": "application/vnd.fujitsu.oasysprs",
|
||||
"ddd": "application/vnd.fujixerox.ddd",
|
||||
"xdw": "application/vnd.fujixerox.docuworks",
|
||||
"xbd": "application/vnd.fujixerox.docuworks.binder",
|
||||
"fzs": "application/vnd.fuzzysheet",
|
||||
"txd": "application/vnd.genomatix.tuxedo",
|
||||
"ggb": "application/vnd.geogebra.file",
|
||||
"ggt": "application/vnd.geogebra.tool",
|
||||
"gex": "application/vnd.geometry-explorer",
|
||||
"gxt": "application/vnd.geonext",
|
||||
"g2w": "application/vnd.geoplan",
|
||||
"g3w": "application/vnd.geospace",
|
||||
"gmx": "application/vnd.gmx",
|
||||
"kml": "application/vnd.google-earth.kml+xml",
|
||||
"kmz": "application/vnd.google-earth.kmz",
|
||||
"gqf": "application/vnd.grafeq",
|
||||
"gac": "application/vnd.groove-account",
|
||||
"ghf": "application/vnd.groove-help",
|
||||
"gim": "application/vnd.groove-identity-message",
|
||||
"grv": "application/vnd.groove-injector",
|
||||
"gtm": "application/vnd.groove-tool-message",
|
||||
"tpl": "application/vnd.groove-tool-template",
|
||||
"vcg": "application/vnd.groove-vcard",
|
||||
"hal": "application/vnd.hal+xml",
|
||||
"zmm": "application/vnd.handheld-entertainment+xml",
|
||||
"hbci": "application/vnd.hbci",
|
||||
"les": "application/vnd.hhe.lesson-player",
|
||||
"hpgl": "application/vnd.hp-hpgl",
|
||||
"hpid": "application/vnd.hp-hpid",
|
||||
"hps": "application/vnd.hp-hps",
|
||||
"jlt": "application/vnd.hp-jlyt",
|
||||
"pcl": "application/vnd.hp-pcl",
|
||||
"pclxl": "application/vnd.hp-pclxl",
|
||||
"sfd-hdstx": "application/vnd.hydrostatix.sof-data",
|
||||
"mpy": "application/vnd.ibm.minipay",
|
||||
"afp": "application/vnd.ibm.modcap",
|
||||
"irm": "application/vnd.ibm.rights-management",
|
||||
"sc": "application/vnd.ibm.secure-container",
|
||||
"icc": "application/vnd.iccprofile",
|
||||
"igl": "application/vnd.igloader",
|
||||
"ivp": "application/vnd.immervision-ivp",
|
||||
"ivu": "application/vnd.immervision-ivu",
|
||||
"igm": "application/vnd.insors.igm",
|
||||
"xpw": "application/vnd.intercon.formnet",
|
||||
"i2g": "application/vnd.intergeo",
|
||||
"qbo": "application/vnd.intu.qbo",
|
||||
"qfx": "application/vnd.intu.qfx",
|
||||
"rcprofile": "application/vnd.ipunplugged.rcprofile",
|
||||
"irp": "application/vnd.irepository.package+xml",
|
||||
"xpr": "application/vnd.is-xpr",
|
||||
"fcs": "application/vnd.isac.fcs",
|
||||
"jam": "application/vnd.jam",
|
||||
"rms": "application/vnd.jcp.javame.midlet-rms",
|
||||
"jisp": "application/vnd.jisp",
|
||||
"joda": "application/vnd.joost.joda-archive",
|
||||
"ktr": "application/vnd.kahootz",
|
||||
"karbon": "application/vnd.kde.karbon",
|
||||
"chrt": "application/vnd.kde.kchart",
|
||||
"kfo": "application/vnd.kde.kformula",
|
||||
"flw": "application/vnd.kde.kivio",
|
||||
"kon": "application/vnd.kde.kontour",
|
||||
"kpr": "application/vnd.kde.kpresenter",
|
||||
"ksp": "application/vnd.kde.kspread",
|
||||
"kwd": "application/vnd.kde.kword",
|
||||
"htke": "application/vnd.kenameaapp",
|
||||
"kia": "application/vnd.kidspiration",
|
||||
"kne": "application/vnd.kinar",
|
||||
"skd": "application/vnd.koan",
|
||||
"sse": "application/vnd.kodak-descriptor",
|
||||
"lasxml": "application/vnd.las.las+xml",
|
||||
"lbd": "application/vnd.llamagraphics.life-balance.desktop",
|
||||
"lbe": "application/vnd.llamagraphics.life-balance.exchange+xml",
|
||||
"123": "application/vnd.lotus-1-2-3",
|
||||
"apr": "application/vnd.lotus-approach",
|
||||
"pre": "application/vnd.lotus-freelance",
|
||||
"nsf": "application/vnd.lotus-notes",
|
||||
"org": "application/vnd.lotus-organizer",
|
||||
"scm": "application/vnd.lotus-screencam",
|
||||
"lwp": "application/vnd.lotus-wordpro",
|
||||
"portpkg": "application/vnd.macports.portpkg",
|
||||
"mcd": "application/vnd.mcd",
|
||||
"mc1": "application/vnd.medcalcdata",
|
||||
"cdkey": "application/vnd.mediastation.cdkey",
|
||||
"mwf": "application/vnd.mfer",
|
||||
"mfm": "application/vnd.mfmp",
|
||||
"flo": "application/vnd.micrografx.flo",
|
||||
"igx": "application/vnd.micrografx.igx",
|
||||
"mif": "application/vnd.mif",
|
||||
"daf": "application/vnd.mobius.daf",
|
||||
"dis": "application/vnd.mobius.dis",
|
||||
"mbk": "application/vnd.mobius.mbk",
|
||||
"mqy": "application/vnd.mobius.mqy",
|
||||
"msl": "application/vnd.mobius.msl",
|
||||
"plc": "application/vnd.mobius.plc",
|
||||
"txf": "application/vnd.mobius.txf",
|
||||
"mpn": "application/vnd.mophun.application",
|
||||
"mpc": "application/vnd.mophun.certificate",
|
||||
"xul": "application/vnd.mozilla.xul+xml",
|
||||
"cil": "application/vnd.ms-artgalry",
|
||||
"cab": "application/vnd.ms-cab-compressed",
|
||||
"xls": "application/vnd.ms-excel",
|
||||
"xlam": "application/vnd.ms-excel.addin.macroenabled.12",
|
||||
"xlsb": "application/vnd.ms-excel.sheet.binary.macroenabled.12",
|
||||
"xlsm": "application/vnd.ms-excel.sheet.macroenabled.12",
|
||||
"xltm": "application/vnd.ms-excel.template.macroenabled.12",
|
||||
"eot": "application/vnd.ms-fontobject",
|
||||
"chm": "application/vnd.ms-htmlhelp",
|
||||
"ims": "application/vnd.ms-ims",
|
||||
"lrm": "application/vnd.ms-lrm",
|
||||
"thmx": "application/vnd.ms-officetheme",
|
||||
"cat": "application/vnd.ms-pki.seccat",
|
||||
"stl": "application/vnd.ms-pki.stl",
|
||||
"ppt": "application/vnd.ms-powerpoint",
|
||||
"ppam": "application/vnd.ms-powerpoint.addin.macroenabled.12",
|
||||
"pptm": "application/vnd.ms-powerpoint.presentation.macroenabled.12",
|
||||
"sldm": "application/vnd.ms-powerpoint.slide.macroenabled.12",
|
||||
"ppsm": "application/vnd.ms-powerpoint.slideshow.macroenabled.12",
|
||||
"potm": "application/vnd.ms-powerpoint.template.macroenabled.12",
|
||||
"mpp": "application/vnd.ms-project",
|
||||
"docm": "application/vnd.ms-word.document.macroenabled.12",
|
||||
"dotm": "application/vnd.ms-word.template.macroenabled.12",
|
||||
"wps": "application/vnd.ms-works",
|
||||
"wpl": "application/vnd.ms-wpl",
|
||||
"xps": "application/vnd.ms-xpsdocument",
|
||||
"mseq": "application/vnd.mseq",
|
||||
"mus": "application/vnd.musician",
|
||||
"msty": "application/vnd.muvee.style",
|
||||
"taglet": "application/vnd.mynfc",
|
||||
"nlu": "application/vnd.neurolanguage.nlu",
|
||||
"nitf": "application/vnd.nitf",
|
||||
"nnd": "application/vnd.noblenet-directory",
|
||||
"nns": "application/vnd.noblenet-sealer",
|
||||
"nnw": "application/vnd.noblenet-web",
|
||||
"ngdat": "application/vnd.nokia.n-gage.data",
|
||||
"n-gage": "application/vnd.nokia.n-gage.symbian.install",
|
||||
"rpst": "application/vnd.nokia.radio-preset",
|
||||
"rpss": "application/vnd.nokia.radio-presets",
|
||||
"edm": "application/vnd.novadigm.edm",
|
||||
"edx": "application/vnd.novadigm.edx",
|
||||
"ext": "application/vnd.novadigm.ext",
|
||||
"odc": "application/vnd.oasis.opendocument.chart",
|
||||
"otc": "application/vnd.oasis.opendocument.chart-template",
|
||||
"odb": "application/vnd.oasis.opendocument.database",
|
||||
"odf": "application/vnd.oasis.opendocument.formula",
|
||||
"odft": "application/vnd.oasis.opendocument.formula-template",
|
||||
"odg": "application/vnd.oasis.opendocument.graphics",
|
||||
"otg": "application/vnd.oasis.opendocument.graphics-template",
|
||||
"odi": "application/vnd.oasis.opendocument.image",
|
||||
"oti": "application/vnd.oasis.opendocument.image-template",
|
||||
"odp": "application/vnd.oasis.opendocument.presentation",
|
||||
"otp": "application/vnd.oasis.opendocument.presentation-template",
|
||||
"ods": "application/vnd.oasis.opendocument.spreadsheet",
|
||||
"ots": "application/vnd.oasis.opendocument.spreadsheet-template",
|
||||
"odt": "application/vnd.oasis.opendocument.text",
|
||||
"odm": "application/vnd.oasis.opendocument.text-master",
|
||||
"ott": "application/vnd.oasis.opendocument.text-template",
|
||||
"oth": "application/vnd.oasis.opendocument.text-web",
|
||||
"xo": "application/vnd.olpc-sugar",
|
||||
"dd2": "application/vnd.oma.dd2+xml",
|
||||
"oxt": "application/vnd.openofficeorg.extension",
|
||||
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
||||
"sldx": "application/vnd.openxmlformats-officedocument.presentationml.slide",
|
||||
"ppsx": "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
|
||||
"potx": "application/vnd.openxmlformats-officedocument.presentationml.template",
|
||||
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"xltx": "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
|
||||
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"dotx": "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
|
||||
"mgp": "application/vnd.osgeo.mapguide.package",
|
||||
"dp": "application/vnd.osgi.dp",
|
||||
"esa": "application/vnd.osgi.subsystem",
|
||||
"oprc": "application/vnd.palm",
|
||||
"paw": "application/vnd.pawaafile",
|
||||
"str": "application/vnd.pg.format",
|
||||
"ei6": "application/vnd.pg.osasli",
|
||||
"efif": "application/vnd.picsel",
|
||||
"wg": "application/vnd.pmi.widget",
|
||||
"plf": "application/vnd.pocketlearn",
|
||||
"pbd": "application/vnd.powerbuilder6",
|
||||
"box": "application/vnd.previewsystems.box",
|
||||
"mgz": "application/vnd.proteus.magazine",
|
||||
"qps": "application/vnd.publishare-delta-tree",
|
||||
"ptid": "application/vnd.pvi.ptid1",
|
||||
"qwd": "application/vnd.quark.quarkxpress",
|
||||
"bed": "application/vnd.realvnc.bed",
|
||||
"mxl": "application/vnd.recordare.musicxml",
|
||||
"musicxml": "application/vnd.recordare.musicxml+xml",
|
||||
"cryptonote": "application/vnd.rig.cryptonote",
|
||||
"cod": "application/vnd.rim.cod",
|
||||
"rm": "application/vnd.rn-realmedia",
|
||||
"rmvb": "application/vnd.rn-realmedia-vbr",
|
||||
"link66": "application/vnd.route66.link66+xml",
|
||||
"st": "application/vnd.sailingtracker.track",
|
||||
"see": "application/vnd.seemail",
|
||||
"sema": "application/vnd.sema",
|
||||
"semd": "application/vnd.semd",
|
||||
"semf": "application/vnd.semf",
|
||||
"ifm": "application/vnd.shana.informed.formdata",
|
||||
"itp": "application/vnd.shana.informed.formtemplate",
|
||||
"iif": "application/vnd.shana.informed.interchange",
|
||||
"ipk": "application/vnd.shana.informed.package",
|
||||
"twd": "application/vnd.simtech-mindmapper",
|
||||
"mmf": "application/vnd.smaf",
|
||||
"teacher": "application/vnd.smart.teacher",
|
||||
"sdkd": "application/vnd.solent.sdkm+xml",
|
||||
"dxp": "application/vnd.spotfire.dxp",
|
||||
"sfs": "application/vnd.spotfire.sfs",
|
||||
"sdc": "application/vnd.stardivision.calc",
|
||||
"sda": "application/vnd.stardivision.draw",
|
||||
"sdd": "application/vnd.stardivision.impress",
|
||||
"smf": "application/vnd.stardivision.math",
|
||||
"sdw": "application/vnd.stardivision.writer",
|
||||
"sgl": "application/vnd.stardivision.writer-global",
|
||||
"smzip": "application/vnd.stepmania.package",
|
||||
"sm": "application/vnd.stepmania.stepchart",
|
||||
"sxc": "application/vnd.sun.xml.calc",
|
||||
"stc": "application/vnd.sun.xml.calc.template",
|
||||
"sxd": "application/vnd.sun.xml.draw",
|
||||
"std": "application/vnd.sun.xml.draw.template",
|
||||
"sxi": "application/vnd.sun.xml.impress",
|
||||
"sti": "application/vnd.sun.xml.impress.template",
|
||||
"sxm": "application/vnd.sun.xml.math",
|
||||
"sxw": "application/vnd.sun.xml.writer",
|
||||
"sxg": "application/vnd.sun.xml.writer.global",
|
||||
"stw": "application/vnd.sun.xml.writer.template",
|
||||
"sus": "application/vnd.sus-calendar",
|
||||
"svd": "application/vnd.svd",
|
||||
"sis": "application/vnd.symbian.install",
|
||||
"bdm": "application/vnd.syncml.dm+wbxml",
|
||||
"xdm": "application/vnd.syncml.dm+xml",
|
||||
"xsm": "application/vnd.syncml+xml",
|
||||
"tao": "application/vnd.tao.intent-module-archive",
|
||||
"cap": "application/vnd.tcpdump.pcap",
|
||||
"tmo": "application/vnd.tmobile-livetv",
|
||||
"tpt": "application/vnd.trid.tpt",
|
||||
"mxs": "application/vnd.triscape.mxs",
|
||||
"tra": "application/vnd.trueapp",
|
||||
"ufd": "application/vnd.ufdl",
|
||||
"utz": "application/vnd.uiq.theme",
|
||||
"umj": "application/vnd.umajin",
|
||||
"unityweb": "application/vnd.unity",
|
||||
"uoml": "application/vnd.uoml+xml",
|
||||
"vcx": "application/vnd.vcx",
|
||||
"vss": "application/vnd.visio",
|
||||
"vis": "application/vnd.visionary",
|
||||
"vsf": "application/vnd.vsf",
|
||||
"wbxml": "application/vnd.wap.wbxml",
|
||||
"wmlc": "application/vnd.wap.wmlc",
|
||||
"wmlsc": "application/vnd.wap.wmlscriptc",
|
||||
"wtb": "application/vnd.webturbo",
|
||||
"nbp": "application/vnd.wolfram.player",
|
||||
"wpd": "application/vnd.wordperfect",
|
||||
"wqd": "application/vnd.wqd",
|
||||
"stf": "application/vnd.wt.stf",
|
||||
"xar": "application/vnd.xara",
|
||||
"xfdl": "application/vnd.xfdl",
|
||||
"hvd": "application/vnd.yamaha.hv-dic",
|
||||
"hvs": "application/vnd.yamaha.hv-script",
|
||||
"hvp": "application/vnd.yamaha.hv-voice",
|
||||
"osf": "application/vnd.yamaha.openscoreformat",
|
||||
"osfpvg": "application/vnd.yamaha.openscoreformat.osfpvg+xml",
|
||||
"saf": "application/vnd.yamaha.smaf-audio",
|
||||
"spf": "application/vnd.yamaha.smaf-phrase",
|
||||
"cmp": "application/vnd.yellowriver-custom-menu",
|
||||
"zir": "application/vnd.zul",
|
||||
"zaz": "application/vnd.zzazz.deck+xml",
|
||||
"vxml": "application/voicexml+xml",
|
||||
"wgt": "application/widget",
|
||||
"hlp": "application/winhlp",
|
||||
"wsdl": "application/wsdl+xml",
|
||||
"wspolicy": "application/wspolicy+xml",
|
||||
"7z": "application/x-7z-compressed",
|
||||
"abw": "application/x-abiword",
|
||||
"ace": "application/x-ace-compressed",
|
||||
"dmg": "application/x-apple-diskimage",
|
||||
"aab": "application/x-authorware-bin",
|
||||
"aam": "application/x-authorware-map",
|
||||
"aas": "application/x-authorware-seg",
|
||||
"bcpio": "application/x-bcpio",
|
||||
"torrent": "application/x-bittorrent",
|
||||
"blb": "application/x-blorb",
|
||||
"bz": "application/x-bzip",
|
||||
"bz2": "application/x-bzip2",
|
||||
"cbr": "application/x-cbr",
|
||||
"vcd": "application/x-cdlink",
|
||||
"cfs": "application/x-cfs-compressed",
|
||||
"chat": "application/x-chat",
|
||||
"pgn": "application/x-chess-pgn",
|
||||
"nsc": "application/x-conference",
|
||||
"cpio": "application/x-cpio",
|
||||
"csh": "application/x-csh",
|
||||
"deb": "application/x-debian-package",
|
||||
"dgc": "application/x-dgc-compressed",
|
||||
"cct": "application/x-director",
|
||||
"wad": "application/x-doom",
|
||||
"ncx": "application/x-dtbncx+xml",
|
||||
"dtb": "application/x-dtbook+xml",
|
||||
"res": "application/x-dtbresource+xml",
|
||||
"dvi": "application/x-dvi",
|
||||
"evy": "application/x-envoy",
|
||||
"eva": "application/x-eva",
|
||||
"bdf": "application/x-font-bdf",
|
||||
"gsf": "application/x-font-ghostscript",
|
||||
"psf": "application/x-font-linux-psf",
|
||||
"pcf": "application/x-font-pcf",
|
||||
"snf": "application/x-font-snf",
|
||||
"afm": "application/x-font-type1",
|
||||
"arc": "application/x-freearc",
|
||||
"spl": "application/x-futuresplash",
|
||||
"gca": "application/x-gca-compressed",
|
||||
"ulx": "application/x-glulx",
|
||||
"gnumeric": "application/x-gnumeric",
|
||||
"gramps": "application/x-gramps-xml",
|
||||
"gtar": "application/x-gtar",
|
||||
"hdf": "application/x-hdf",
|
||||
"install": "application/x-install-instructions",
|
||||
"iso": "application/x-iso9660-image",
|
||||
"jnlp": "application/x-java-jnlp-file",
|
||||
"latex": "application/x-latex",
|
||||
"lzh": "application/x-lzh-compressed",
|
||||
"mie": "application/x-mie",
|
||||
"mobi": "application/x-mobipocket-ebook",
|
||||
"application": "application/x-ms-application",
|
||||
"lnk": "application/x-ms-shortcut",
|
||||
"wmd": "application/x-ms-wmd",
|
||||
"wmz": "application/x-ms-wmz",
|
||||
"xbap": "application/x-ms-xbap",
|
||||
"mdb": "application/x-msaccess",
|
||||
"obd": "application/x-msbinder",
|
||||
"crd": "application/x-mscardfile",
|
||||
"clp": "application/x-msclip",
|
||||
"mny": "application/x-msmoney",
|
||||
"pub": "application/x-mspublisher",
|
||||
"scd": "application/x-msschedule",
|
||||
"trm": "application/x-msterminal",
|
||||
"wri": "application/x-mswrite",
|
||||
"nzb": "application/x-nzb",
|
||||
"p12": "application/x-pkcs12",
|
||||
"p7b": "application/x-pkcs7-certificates",
|
||||
"p7r": "application/x-pkcs7-certreqresp",
|
||||
"rar": "application/x-rar-compressed",
|
||||
"ris": "application/x-research-info-systems",
|
||||
"sh": "application/x-sh",
|
||||
"shar": "application/x-shar",
|
||||
"swf": "application/x-shockwave-flash",
|
||||
"xap": "application/x-silverlight-app",
|
||||
"sql": "application/x-sql",
|
||||
"sit": "application/x-stuffit",
|
||||
"sitx": "application/x-stuffitx",
|
||||
"srt": "application/x-subrip",
|
||||
"sv4cpio": "application/x-sv4cpio",
|
||||
"sv4crc": "application/x-sv4crc",
|
||||
"t3": "application/x-t3vm-image",
|
||||
"gam": "application/x-tads",
|
||||
"tar": "application/x-tar",
|
||||
"tcl": "application/x-tcl",
|
||||
"tex": "application/x-tex",
|
||||
"tfm": "application/x-tex-tfm",
|
||||
"texi": "application/x-texinfo",
|
||||
"obj": "application/x-tgif",
|
||||
"ustar": "application/x-ustar",
|
||||
"src": "application/x-wais-source",
|
||||
"crt": "application/x-x509-ca-cert",
|
||||
"fig": "application/x-xfig",
|
||||
"xlf": "application/x-xliff+xml",
|
||||
"xpi": "application/x-xpinstall",
|
||||
"xz": "application/x-xz",
|
||||
"xaml": "application/xaml+xml",
|
||||
"xdf": "application/xcap-diff+xml",
|
||||
"xenc": "application/xenc+xml",
|
||||
"xhtml": "application/xhtml+xml",
|
||||
"xml": "application/xml",
|
||||
"dtd": "application/xml-dtd",
|
||||
"xop": "application/xop+xml",
|
||||
"xpl": "application/xproc+xml",
|
||||
"xslt": "application/xslt+xml",
|
||||
"xspf": "application/xspf+xml",
|
||||
"mxml": "application/xv+xml",
|
||||
"yang": "application/yang",
|
||||
"yin": "application/yin+xml",
|
||||
"zip": "application/zip",
|
||||
"adp": "audio/adpcm",
|
||||
"au": "audio/basic",
|
||||
"mid": "audio/midi",
|
||||
"m4a": "audio/mp4",
|
||||
"mp3": "audio/mpeg",
|
||||
"ogg": "audio/ogg",
|
||||
"s3m": "audio/s3m",
|
||||
"sil": "audio/silk",
|
||||
"uva": "audio/vnd.dece.audio",
|
||||
"eol": "audio/vnd.digital-winds",
|
||||
"dra": "audio/vnd.dra",
|
||||
"dts": "audio/vnd.dts",
|
||||
"dtshd": "audio/vnd.dts.hd",
|
||||
"lvp": "audio/vnd.lucent.voice",
|
||||
"pya": "audio/vnd.ms-playready.media.pya",
|
||||
"ecelp4800": "audio/vnd.nuera.ecelp4800",
|
||||
"ecelp7470": "audio/vnd.nuera.ecelp7470",
|
||||
"ecelp9600": "audio/vnd.nuera.ecelp9600",
|
||||
"rip": "audio/vnd.rip",
|
||||
"weba": "audio/webm",
|
||||
"aac": "audio/x-aac",
|
||||
"aiff": "audio/x-aiff",
|
||||
"caf": "audio/x-caf",
|
||||
"flac": "audio/x-flac",
|
||||
"mka": "audio/x-matroska",
|
||||
"m3u": "audio/x-mpegurl",
|
||||
"wax": "audio/x-ms-wax",
|
||||
"wma": "audio/x-ms-wma",
|
||||
"rmp": "audio/x-pn-realaudio-plugin",
|
||||
"wav": "audio/x-wav",
|
||||
"xm": "audio/xm",
|
||||
"cdx": "chemical/x-cdx",
|
||||
"cif": "chemical/x-cif",
|
||||
"cmdf": "chemical/x-cmdf",
|
||||
"cml": "chemical/x-cml",
|
||||
"csml": "chemical/x-csml",
|
||||
"xyz": "chemical/x-xyz",
|
||||
"ttc": "font/collection",
|
||||
"otf": "font/otf",
|
||||
"ttf": "font/ttf",
|
||||
"woff": "font/woff",
|
||||
"woff2": "font/woff2",
|
||||
"bmp": "image/bmp",
|
||||
"cgm": "image/cgm",
|
||||
"g3": "image/g3fax",
|
||||
"gif": "image/gif",
|
||||
"ief": "image/ief",
|
||||
"jpg": "image/jpeg",
|
||||
"ktx": "image/ktx",
|
||||
"png": "image/png",
|
||||
"btif": "image/prs.btif",
|
||||
"sgi": "image/sgi",
|
||||
"svg": "image/svg+xml",
|
||||
"tiff": "image/tiff",
|
||||
"psd": "image/vnd.adobe.photoshop",
|
||||
"dwg": "image/vnd.dwg",
|
||||
"dxf": "image/vnd.dxf",
|
||||
"fbs": "image/vnd.fastbidsheet",
|
||||
"fpx": "image/vnd.fpx",
|
||||
"fst": "image/vnd.fst",
|
||||
"mmr": "image/vnd.fujixerox.edmics-mmr",
|
||||
"rlc": "image/vnd.fujixerox.edmics-rlc",
|
||||
"mdi": "image/vnd.ms-modi",
|
||||
"wdp": "image/vnd.ms-photo",
|
||||
"npx": "image/vnd.net-fpx",
|
||||
"wbmp": "image/vnd.wap.wbmp",
|
||||
"xif": "image/vnd.xiff",
|
||||
"webp": "image/webp",
|
||||
"3ds": "image/x-3ds",
|
||||
"ras": "image/x-cmu-raster",
|
||||
"cmx": "image/x-cmx",
|
||||
"ico": "image/x-icon",
|
||||
"sid": "image/x-mrsid-image",
|
||||
"pcx": "image/x-pcx",
|
||||
"pnm": "image/x-portable-anymap",
|
||||
"pbm": "image/x-portable-bitmap",
|
||||
"pgm": "image/x-portable-graymap",
|
||||
"ppm": "image/x-portable-pixmap",
|
||||
"rgb": "image/x-rgb",
|
||||
"tga": "image/x-tga",
|
||||
"xbm": "image/x-xbitmap",
|
||||
"xpm": "image/x-xpixmap",
|
||||
"xwd": "image/x-xwindowdump",
|
||||
"dae": "model/vnd.collada+xml",
|
||||
"dwf": "model/vnd.dwf",
|
||||
"gdl": "model/vnd.gdl",
|
||||
"gtw": "model/vnd.gtw",
|
||||
"mts": "model/vnd.mts",
|
||||
"vtu": "model/vnd.vtu",
|
||||
"appcache": "text/cache-manifest",
|
||||
"ics": "text/calendar",
|
||||
"css": "text/css",
|
||||
"csv": "text/csv",
|
||||
"html": "text/html",
|
||||
"n3": "text/n3",
|
||||
"txt": "text/plain",
|
||||
"dsc": "text/prs.lines.tag",
|
||||
"rtx": "text/richtext",
|
||||
"tsv": "text/tab-separated-values",
|
||||
"ttl": "text/turtle",
|
||||
"vcard": "text/vcard",
|
||||
"curl": "text/vnd.curl",
|
||||
"dcurl": "text/vnd.curl.dcurl",
|
||||
"mcurl": "text/vnd.curl.mcurl",
|
||||
"scurl": "text/vnd.curl.scurl",
|
||||
"sub": "text/vnd.dvb.subtitle",
|
||||
"fly": "text/vnd.fly",
|
||||
"flx": "text/vnd.fmi.flexstor",
|
||||
"gv": "text/vnd.graphviz",
|
||||
"3dml": "text/vnd.in3d.3dml",
|
||||
"spot": "text/vnd.in3d.spot",
|
||||
"jad": "text/vnd.sun.j2me.app-descriptor",
|
||||
"wml": "text/vnd.wap.wml",
|
||||
"wmls": "text/vnd.wap.wmlscript",
|
||||
"asm": "text/x-asm",
|
||||
"c": "text/x-c",
|
||||
"java": "text/x-java-source",
|
||||
"nfo": "text/x-nfo",
|
||||
"opml": "text/x-opml",
|
||||
"pas": "text/x-pascal",
|
||||
"etx": "text/x-setext",
|
||||
"sfv": "text/x-sfv",
|
||||
"uu": "text/x-uuencode",
|
||||
"vcs": "text/x-vcalendar",
|
||||
"vcf": "text/x-vcard",
|
||||
"3gp": "video/3gpp",
|
||||
"3g2": "video/3gpp2",
|
||||
"h261": "video/h261",
|
||||
"h263": "video/h263",
|
||||
"h264": "video/h264",
|
||||
"jpgv": "video/jpeg",
|
||||
"mp4": "video/mp4",
|
||||
"mpeg": "video/mpeg",
|
||||
"ogv": "video/ogg",
|
||||
"dvb": "video/vnd.dvb.file",
|
||||
"fvt": "video/vnd.fvt",
|
||||
"pyv": "video/vnd.ms-playready.media.pyv",
|
||||
"viv": "video/vnd.vivo",
|
||||
"webm": "video/webm",
|
||||
"f4v": "video/x-f4v",
|
||||
"fli": "video/x-fli",
|
||||
"flv": "video/x-flv",
|
||||
"m4v": "video/x-m4v",
|
||||
"mkv": "video/x-matroska",
|
||||
"mng": "video/x-mng",
|
||||
"asf": "video/x-ms-asf",
|
||||
"vob": "video/x-ms-vob",
|
||||
"wm": "video/x-ms-wm",
|
||||
"wmv": "video/x-ms-wmv",
|
||||
"wmx": "video/x-ms-wmx",
|
||||
"wvx": "video/x-ms-wvx",
|
||||
"avi": "video/x-msvideo",
|
||||
"movie": "video/x-sgi-movie",
|
||||
"smv": "video/x-smv",
|
||||
"ice": "x-conference/x-cooltalk",
|
||||
}
|
||||
13
internal/api/models.go
Normal file
13
internal/api/models.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package api
|
||||
|
||||
// ErrorResponse represents an error response
|
||||
type ErrorResponse struct {
|
||||
Error ErrorDetail `json:"error"`
|
||||
}
|
||||
|
||||
// ErrorDetail represents error details
|
||||
type ErrorDetail struct {
|
||||
Message string `json:"message"`
|
||||
Type string `json:"type"`
|
||||
Code string `json:"code,omitempty"`
|
||||
}
|
||||
176
internal/api/server.go
Normal file
176
internal/api/server.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/luispater/CLIProxyAPI/internal/client"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Server represents the API server
|
||||
type Server struct {
|
||||
engine *gin.Engine
|
||||
server *http.Server
|
||||
handlers *APIHandlers
|
||||
cfg *ServerConfig
|
||||
}
|
||||
|
||||
// ServerConfig contains configuration for the API server
|
||||
type ServerConfig struct {
|
||||
Port string
|
||||
Debug bool
|
||||
ApiKeys []string
|
||||
}
|
||||
|
||||
// NewServer creates a new API server instance
|
||||
func NewServer(config *ServerConfig, cliClients []*client.Client) *Server {
|
||||
// Set gin mode
|
||||
if !config.Debug {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
// Create handlers
|
||||
handlers := NewAPIHandlers(cliClients, config.Debug)
|
||||
|
||||
// Create gin engine
|
||||
engine := gin.New()
|
||||
|
||||
// Add middleware
|
||||
engine.Use(gin.Logger())
|
||||
engine.Use(gin.Recovery())
|
||||
engine.Use(corsMiddleware())
|
||||
|
||||
// Create server instance
|
||||
s := &Server{
|
||||
engine: engine,
|
||||
handlers: handlers,
|
||||
cfg: config,
|
||||
}
|
||||
|
||||
// Setup routes
|
||||
s.setupRoutes()
|
||||
|
||||
// Create HTTP server
|
||||
s.server = &http.Server{
|
||||
Addr: ":" + config.Port,
|
||||
Handler: engine,
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// setupRoutes configures the API routes
|
||||
func (s *Server) setupRoutes() {
|
||||
// OpenAI compatible API routes
|
||||
v1 := s.engine.Group("/v1")
|
||||
v1.Use(AuthMiddleware(s.cfg))
|
||||
{
|
||||
v1.GET("/models", s.handlers.Models)
|
||||
v1.POST("/chat/completions", s.handlers.ChatCompletions)
|
||||
}
|
||||
|
||||
// Root endpoint
|
||||
s.engine.GET("/", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "CLI Proxy API Server",
|
||||
"version": "1.0.0",
|
||||
"endpoints": []string{
|
||||
"POST /v1/chat/completions",
|
||||
"GET /v1/models",
|
||||
},
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// Start starts the API server
|
||||
func (s *Server) Start() error {
|
||||
log.Debugf("Starting API server on %s", s.server.Addr)
|
||||
|
||||
// Start the HTTP server
|
||||
if err := s.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
return fmt.Errorf("failed to start HTTP server: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop gracefully stops the API server
|
||||
func (s *Server) Stop(ctx context.Context) error {
|
||||
log.Debug("Stopping API server...")
|
||||
|
||||
// Shutdown the HTTP server
|
||||
if err := s.server.Shutdown(ctx); err != nil {
|
||||
return fmt.Errorf("failed to shutdown HTTP server: %v", err)
|
||||
}
|
||||
|
||||
log.Debug("API server stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
// corsMiddleware adds CORS headers
|
||||
func corsMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Header("Access-Control-Allow-Origin", "*")
|
||||
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
|
||||
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// AuthMiddleware authenticates requests using API keys
|
||||
func AuthMiddleware(cfg *ServerConfig) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if len(cfg.ApiKeys) == 0 {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// Get the Authorization header
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "Missing API key",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Extract the API key
|
||||
parts := strings.Split(authHeader, " ")
|
||||
var apiKey string
|
||||
if len(parts) == 2 && strings.ToLower(parts[0]) == "bearer" {
|
||||
apiKey = parts[1]
|
||||
} else {
|
||||
apiKey = authHeader
|
||||
}
|
||||
|
||||
// Find the API key in the in-memory list
|
||||
var foundKey string
|
||||
for i := range cfg.ApiKeys {
|
||||
if cfg.ApiKeys[i] == apiKey {
|
||||
foundKey = cfg.ApiKeys[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if foundKey == "" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "Invalid API key",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Store the API key and user in the context
|
||||
c.Set("apiKey", foundKey)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
208
internal/auth/auth.go
Normal file
208
internal/auth/auth.go
Normal file
@@ -0,0 +1,208 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/tidwall/gjson"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/skratchdot/open-golang/open"
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/google"
|
||||
)
|
||||
|
||||
const (
|
||||
oauthClientID = "681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com"
|
||||
oauthClientSecret = "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"
|
||||
)
|
||||
|
||||
var (
|
||||
oauthScopes = []string{
|
||||
"https://www.googleapis.com/auth/cloud-platform",
|
||||
"https://www.googleapis.com/auth/userinfo.email",
|
||||
"https://www.googleapis.com/auth/userinfo.profile",
|
||||
}
|
||||
)
|
||||
|
||||
type TokenStorage struct {
|
||||
Token any `json:"token"`
|
||||
ProjectID string `json:"project_id"`
|
||||
Email string `json:"email"`
|
||||
}
|
||||
|
||||
// GetAuthenticatedClient configures and returns an HTTP client with OAuth2 tokens.
|
||||
// It handles the entire flow: loading, refreshing, and fetching new tokens.
|
||||
func GetAuthenticatedClient(ctx context.Context, ts *TokenStorage, authDir string) (*http.Client, error) {
|
||||
conf := &oauth2.Config{
|
||||
ClientID: oauthClientID,
|
||||
ClientSecret: oauthClientSecret,
|
||||
RedirectURL: "http://localhost:8085/oauth2callback", // Placeholder, will be updated
|
||||
Scopes: oauthScopes,
|
||||
Endpoint: google.Endpoint,
|
||||
}
|
||||
|
||||
var token *oauth2.Token
|
||||
var err error
|
||||
|
||||
if ts.Token == nil {
|
||||
log.Info("Could not load token from file, starting OAuth flow.")
|
||||
token, err = getTokenFromWeb(ctx, conf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get token from web: %w", err)
|
||||
}
|
||||
ts, err = saveTokenToFile(ctx, conf, token, ts.ProjectID, authDir)
|
||||
if err != nil {
|
||||
// Log the error but proceed, as we have a valid token for the session.
|
||||
log.Errorf("Warning: failed to save token to file: %v", err)
|
||||
}
|
||||
}
|
||||
tsToken, _ := json.Marshal(ts.Token)
|
||||
if err = json.Unmarshal(tsToken, &token); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conf.Client(ctx, token), nil
|
||||
}
|
||||
|
||||
// saveTokenToFile saves a token to the local credentials file.
|
||||
func saveTokenToFile(ctx context.Context, config *oauth2.Config, token *oauth2.Token, projectID, authDir string) (*TokenStorage, error) {
|
||||
httpClient := config.Client(ctx, token)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "https://www.googleapis.com/oauth2/v1/userinfo?alt=json", nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get user info: %v", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute request: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("get user info request failed with status %d: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
|
||||
emailResult := gjson.GetBytes(bodyBytes, "email")
|
||||
if emailResult.Exists() && emailResult.Type == gjson.String {
|
||||
log.Infof("Authenticated user email: %s", emailResult.String())
|
||||
} else {
|
||||
log.Info("Failed to get user email from token")
|
||||
}
|
||||
|
||||
log.Infof("Saving credentials to %s", filepath.Join(authDir, fmt.Sprintf("%s.json", emailResult.String())))
|
||||
if err = os.MkdirAll(authDir, 0700); err != nil {
|
||||
return nil, fmt.Errorf("failed to create directory: %w", err)
|
||||
}
|
||||
|
||||
f, err := os.Create(filepath.Join(authDir, fmt.Sprintf("%s.json", emailResult.String())))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create token file: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = f.Close()
|
||||
}()
|
||||
|
||||
var ifToken map[string]any
|
||||
jsonData, _ := json.Marshal(token)
|
||||
err = json.Unmarshal(jsonData, &ifToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal token: %w", err)
|
||||
}
|
||||
|
||||
ifToken["token_uri"] = "https://oauth2.googleapis.com/token"
|
||||
ifToken["client_id"] = oauthClientID
|
||||
ifToken["client_secret"] = oauthClientSecret
|
||||
ifToken["scopes"] = oauthScopes
|
||||
ifToken["universe_domain"] = "googleapis.com"
|
||||
|
||||
ts := TokenStorage{
|
||||
Token: ifToken,
|
||||
ProjectID: projectID,
|
||||
Email: emailResult.String(),
|
||||
}
|
||||
|
||||
if err = json.NewEncoder(f).Encode(ts); err != nil {
|
||||
return nil, fmt.Errorf("failed to write token to file: %w", err)
|
||||
}
|
||||
return &ts, nil
|
||||
}
|
||||
|
||||
// getTokenFromWeb starts a local server to handle the OAuth2 flow.
|
||||
func getTokenFromWeb(ctx context.Context, config *oauth2.Config) (*oauth2.Token, error) {
|
||||
// Use a channel to pass the authorization code from the HTTP handler to the main function.
|
||||
codeChan := make(chan string)
|
||||
errChan := make(chan error)
|
||||
|
||||
// Create a new HTTP server.
|
||||
server := &http.Server{Addr: "localhost:8085"}
|
||||
config.RedirectURL = "http://localhost:8085/oauth2callback"
|
||||
|
||||
http.HandleFunc("/oauth2callback", func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := r.URL.Query().Get("error"); err != "" {
|
||||
_, _ = fmt.Fprintf(w, "Authentication failed: %s", err)
|
||||
errChan <- fmt.Errorf("authentication failed via callback: %s", err)
|
||||
return
|
||||
}
|
||||
code := r.URL.Query().Get("code")
|
||||
if code == "" {
|
||||
_, _ = fmt.Fprint(w, "Authentication failed: code not found.")
|
||||
errChan <- fmt.Errorf("code not found in callback")
|
||||
return
|
||||
}
|
||||
_, _ = fmt.Fprint(w, "<html><body><h1>Authentication successful!</h1><p>You can close this window.</p></body></html>")
|
||||
codeChan <- code
|
||||
})
|
||||
|
||||
// Start the server in a goroutine.
|
||||
go func() {
|
||||
if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatalf("ListenAndServe(): %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Open the authorization URL in the user's browser.
|
||||
authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline, oauth2.SetAuthURLParam("prompt", "consent"))
|
||||
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)
|
||||
|
||||
err := open.Run(authURL)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to open browser: %v. Please open the URL manually.", err)
|
||||
}
|
||||
|
||||
// Wait for the authorization code or an error.
|
||||
var authCode string
|
||||
select {
|
||||
case code := <-codeChan:
|
||||
authCode = code
|
||||
case err = <-errChan:
|
||||
return nil, err
|
||||
case <-time.After(5 * time.Minute): // Timeout
|
||||
return nil, fmt.Errorf("oauth flow timed out")
|
||||
}
|
||||
|
||||
// Shutdown the server.
|
||||
if err = server.Shutdown(ctx); err != nil {
|
||||
log.Errorf("Failed to shut down server: %v", err)
|
||||
}
|
||||
|
||||
// Exchange the authorization code for a token.
|
||||
token, err := config.Exchange(ctx, authCode)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to exchange token: %w", err)
|
||||
}
|
||||
|
||||
log.Info("Authentication successful.")
|
||||
return token, nil
|
||||
}
|
||||
452
internal/client/client.go
Normal file
452
internal/client/client.go
Normal file
@@ -0,0 +1,452 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/tidwall/gjson"
|
||||
"github.com/tidwall/sjson"
|
||||
"golang.org/x/oauth2"
|
||||
"io"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// --- Constants ---
|
||||
const (
|
||||
codeAssistEndpoint = "https://cloudcode-pa.googleapis.com"
|
||||
apiVersion = "v1internal"
|
||||
pluginVersion = "1.0.0"
|
||||
)
|
||||
|
||||
type GCPProject struct {
|
||||
Projects []GCPProjectProjects `json:"projects"`
|
||||
}
|
||||
type GCPProjectLabels struct {
|
||||
GenerativeLanguage string `json:"generative-language"`
|
||||
}
|
||||
type GCPProjectProjects struct {
|
||||
ProjectNumber string `json:"projectNumber"`
|
||||
ProjectID string `json:"projectId"`
|
||||
LifecycleState string `json:"lifecycleState"`
|
||||
Name string `json:"name"`
|
||||
Labels GCPProjectLabels `json:"labels"`
|
||||
CreateTime time.Time `json:"createTime"`
|
||||
}
|
||||
|
||||
type Content struct {
|
||||
Role string `json:"role"`
|
||||
Parts []Part `json:"parts"`
|
||||
}
|
||||
|
||||
// Part represents a single part of a message's content.
|
||||
type Part struct {
|
||||
Text string `json:"text,omitempty"`
|
||||
InlineData *InlineData `json:"inlineData,omitempty"`
|
||||
FunctionCall *FunctionCall `json:"functionCall,omitempty"`
|
||||
FunctionResponse *FunctionResponse `json:"functionResponse,omitempty"`
|
||||
}
|
||||
|
||||
type InlineData struct {
|
||||
MimeType string `json:"mime_type,omitempty"`
|
||||
Data string `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// FunctionCall represents a tool call requested by the model.
|
||||
type FunctionCall struct {
|
||||
Name string `json:"name"`
|
||||
Args map[string]interface{} `json:"args"`
|
||||
}
|
||||
|
||||
// FunctionResponse represents the result of a tool execution.
|
||||
type FunctionResponse struct {
|
||||
Name string `json:"name"`
|
||||
Response map[string]interface{} `json:"response"`
|
||||
}
|
||||
|
||||
// GenerateContentRequest is the request payload for the streamGenerateContent endpoint.
|
||||
type GenerateContentRequest struct {
|
||||
Contents []Content `json:"contents"`
|
||||
Tools []ToolDeclaration `json:"tools,omitempty"`
|
||||
GenerationConfig `json:"generationConfig"`
|
||||
}
|
||||
|
||||
// GenerationConfig defines model generation parameters.
|
||||
type GenerationConfig struct {
|
||||
ThinkingConfig GenerationConfigThinkingConfig `json:"thinkingConfig,omitempty"`
|
||||
Temperature float64 `json:"temperature,omitempty"`
|
||||
TopP float64 `json:"topP,omitempty"`
|
||||
TopK float64 `json:"topK,omitempty"`
|
||||
// Temperature, TopP, TopK, etc. can be added here.
|
||||
}
|
||||
|
||||
type GenerationConfigThinkingConfig struct {
|
||||
IncludeThoughts bool `json:"include_thoughts,omitempty"`
|
||||
}
|
||||
|
||||
// ToolDeclaration is the structure for declaring tools to the API.
|
||||
// For now, we'll assume a simple structure. A more complete implementation
|
||||
// would mirror the OpenAPI schema definition.
|
||||
type ToolDeclaration struct {
|
||||
FunctionDeclarations []interface{} `json:"functionDeclarations"`
|
||||
}
|
||||
|
||||
// Client is the main client for interacting with the CLI API.
|
||||
type Client struct {
|
||||
httpClient *http.Client
|
||||
projectID string
|
||||
RequestMutex sync.Mutex
|
||||
Email string
|
||||
}
|
||||
|
||||
// NewClient creates a new CLI API client.
|
||||
func NewClient(httpClient *http.Client) *Client {
|
||||
return &Client{
|
||||
httpClient: httpClient,
|
||||
}
|
||||
}
|
||||
|
||||
// SetupUser performs the initial user onboarding and setup.
|
||||
func (c *Client) SetupUser(ctx context.Context, email, projectID string) error {
|
||||
c.Email = email
|
||||
log.Info("Performing user onboarding...")
|
||||
|
||||
// 1. LoadCodeAssist
|
||||
loadAssistReqBody := map[string]interface{}{
|
||||
"metadata": getClientMetadata(),
|
||||
}
|
||||
if projectID != "" {
|
||||
loadAssistReqBody["cloudaicompanionProject"] = projectID
|
||||
}
|
||||
|
||||
var loadAssistResp map[string]interface{}
|
||||
err := c.makeAPIRequest(ctx, "loadCodeAssist", "POST", loadAssistReqBody, &loadAssistResp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load code assist: %w", err)
|
||||
}
|
||||
|
||||
// a, _ := json.Marshal(&loadAssistResp)
|
||||
// log.Debug(string(a))
|
||||
|
||||
// 2. OnboardUser
|
||||
var onboardTierID = "legacy-tier"
|
||||
if tiers, ok := loadAssistResp["allowedTiers"].([]interface{}); ok {
|
||||
for _, t := range tiers {
|
||||
if tier, tierOk := t.(map[string]interface{}); tierOk {
|
||||
if isDefault, isDefaultOk := tier["isDefault"].(bool); isDefaultOk && isDefault {
|
||||
if id, idOk := tier["id"].(string); idOk {
|
||||
onboardTierID = id
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onboardProjectID := projectID
|
||||
if p, ok := loadAssistResp["cloudaicompanionProject"].(string); ok && p != "" {
|
||||
onboardProjectID = p
|
||||
}
|
||||
|
||||
onboardReqBody := map[string]interface{}{
|
||||
"tierId": onboardTierID,
|
||||
"metadata": getClientMetadata(),
|
||||
}
|
||||
if onboardProjectID != "" {
|
||||
onboardReqBody["cloudaicompanionProject"] = onboardProjectID
|
||||
} else {
|
||||
return fmt.Errorf("failed to start user onboarding, need define a project id")
|
||||
}
|
||||
|
||||
var lroResp map[string]interface{}
|
||||
err = c.makeAPIRequest(ctx, "onboardUser", "POST", onboardReqBody, &lroResp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start user onboarding: %w", err)
|
||||
}
|
||||
|
||||
// a, _ = json.Marshal(&lroResp)
|
||||
// log.Debug(string(a))
|
||||
|
||||
// 3. Poll Long-Running Operation (LRO)
|
||||
if done, doneOk := lroResp["done"].(bool); doneOk && done {
|
||||
if project, projectOk := lroResp["response"].(map[string]interface{})["cloudaicompanionProject"].(map[string]interface{}); projectOk {
|
||||
c.projectID = project["id"].(string)
|
||||
log.Infof("Onboarding complete. Using Project ID: %s", c.projectID)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("failed to get operation name from onboarding response: %v", lroResp)
|
||||
}
|
||||
|
||||
// makeAPIRequest handles making requests to the CLI API endpoints.
|
||||
func (c *Client) makeAPIRequest(ctx context.Context, endpoint, method string, body interface{}, result interface{}) error {
|
||||
var reqBody io.Reader
|
||||
if body != nil {
|
||||
jsonBody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal request body: %w", err)
|
||||
}
|
||||
reqBody = bytes.NewBuffer(jsonBody)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s/%s:%s", codeAssistEndpoint, apiVersion, endpoint)
|
||||
if strings.HasPrefix(endpoint, "operations/") {
|
||||
url = fmt.Sprintf("%s/%s", codeAssistEndpoint, endpoint)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, url, reqBody)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
token, err := c.httpClient.Transport.(*oauth2.Transport).Source.Token()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get token: %w", err)
|
||||
}
|
||||
|
||||
// Set headers
|
||||
metadataStr := getClientMetadataString()
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", getUserAgent())
|
||||
req.Header.Set("Client-Metadata", metadataStr)
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute request: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("api request failed with status %d: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
|
||||
if result != nil {
|
||||
if err = json.NewDecoder(resp.Body).Decode(result); err != nil {
|
||||
return fmt.Errorf("failed to decode response body: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StreamAPIRequest handles making streaming requests to the CLI API endpoints.
|
||||
func (c *Client) StreamAPIRequest(ctx context.Context, endpoint string, body interface{}) (io.ReadCloser, error) {
|
||||
var jsonBody []byte
|
||||
var err error
|
||||
if byteBody, ok := body.([]byte); ok {
|
||||
jsonBody = byteBody
|
||||
} else {
|
||||
jsonBody, err = json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request body: %w", err)
|
||||
}
|
||||
}
|
||||
// log.Debug(string(jsonBody))
|
||||
reqBody := bytes.NewBuffer(jsonBody)
|
||||
|
||||
// Add alt=sse for streaming
|
||||
url := fmt.Sprintf("%s/%s:%s?alt=sse", codeAssistEndpoint, apiVersion, endpoint)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, reqBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
token, err := c.httpClient.Transport.(*oauth2.Transport).Source.Token()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get token: %w", err)
|
||||
}
|
||||
|
||||
// Set headers
|
||||
metadataStr := getClientMetadataString()
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", getUserAgent())
|
||||
req.Header.Set("Client-Metadata", metadataStr)
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute request: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("api streaming request failed with status %d: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
|
||||
return resp.Body, nil
|
||||
}
|
||||
|
||||
// SendMessageStream handles a single conversational turn, including tool calls.
|
||||
func (c *Client) SendMessageStream(ctx context.Context, rawJson []byte, model string, contents []Content, tools []ToolDeclaration) (<-chan []byte, <-chan error) {
|
||||
dataTag := []byte("data: ")
|
||||
errChan := make(chan error)
|
||||
dataChan := make(chan []byte)
|
||||
go func() {
|
||||
defer close(errChan)
|
||||
defer close(dataChan)
|
||||
|
||||
request := GenerateContentRequest{
|
||||
Contents: contents,
|
||||
GenerationConfig: GenerationConfig{
|
||||
ThinkingConfig: GenerationConfigThinkingConfig{
|
||||
IncludeThoughts: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
request.Tools = tools
|
||||
|
||||
requestBody := map[string]interface{}{
|
||||
"project": c.projectID, // Assuming ProjectID is available
|
||||
"request": request,
|
||||
"model": model,
|
||||
}
|
||||
|
||||
byteRequestBody, _ := json.Marshal(requestBody)
|
||||
|
||||
// log.Debug(string(rawJson))
|
||||
|
||||
reasoningEffortResult := gjson.GetBytes(rawJson, "reasoning_effort")
|
||||
if reasoningEffortResult.String() == "none" {
|
||||
byteRequestBody, _ = sjson.DeleteBytes(byteRequestBody, "request.generationConfig.thinkingConfig.include_thoughts")
|
||||
byteRequestBody, _ = sjson.SetBytes(byteRequestBody, "request.generationConfig.thinkingConfig.thinkingBudget", 0)
|
||||
} else if reasoningEffortResult.String() == "auto" {
|
||||
byteRequestBody, _ = sjson.SetBytes(byteRequestBody, "request.generationConfig.thinkingConfig.thinkingBudget", -1)
|
||||
} else if reasoningEffortResult.String() == "low" {
|
||||
byteRequestBody, _ = sjson.SetBytes(byteRequestBody, "request.generationConfig.thinkingConfig.thinkingBudget", 1024)
|
||||
} else if reasoningEffortResult.String() == "medium" {
|
||||
byteRequestBody, _ = sjson.SetBytes(byteRequestBody, "request.generationConfig.thinkingConfig.thinkingBudget", 8192)
|
||||
} else if reasoningEffortResult.String() == "high" {
|
||||
byteRequestBody, _ = sjson.SetBytes(byteRequestBody, "request.generationConfig.thinkingConfig.thinkingBudget", 24576)
|
||||
} else {
|
||||
byteRequestBody, _ = sjson.SetBytes(byteRequestBody, "request.generationConfig.thinkingConfig.thinkingBudget", -1)
|
||||
}
|
||||
|
||||
temperatureResult := gjson.GetBytes(rawJson, "temperature")
|
||||
if temperatureResult.Exists() && temperatureResult.Type == gjson.Number {
|
||||
byteRequestBody, _ = sjson.SetBytes(byteRequestBody, "request.generationConfig.temperature", temperatureResult.Num)
|
||||
}
|
||||
|
||||
topPResult := gjson.GetBytes(rawJson, "top_p")
|
||||
if topPResult.Exists() && topPResult.Type == gjson.Number {
|
||||
byteRequestBody, _ = sjson.SetBytes(byteRequestBody, "request.generationConfig.topP", topPResult.Num)
|
||||
}
|
||||
|
||||
topKResult := gjson.GetBytes(rawJson, "top_k")
|
||||
if topKResult.Exists() && topKResult.Type == gjson.Number {
|
||||
byteRequestBody, _ = sjson.SetBytes(byteRequestBody, "request.generationConfig.topK", topKResult.Num)
|
||||
}
|
||||
|
||||
// log.Debug(string(byteRequestBody))
|
||||
|
||||
stream, err := c.StreamAPIRequest(ctx, "streamGenerateContent", byteRequestBody)
|
||||
if err != nil {
|
||||
// log.Println(err)
|
||||
errChan <- err
|
||||
return
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(stream)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Bytes()
|
||||
// log.Printf("Received stream chunk: %s", line)
|
||||
if bytes.HasPrefix(line, dataTag) {
|
||||
dataChan <- line[6:]
|
||||
}
|
||||
}
|
||||
|
||||
if err = scanner.Err(); err != nil {
|
||||
// log.Println(err)
|
||||
errChan <- err
|
||||
_ = stream.Close()
|
||||
return
|
||||
}
|
||||
|
||||
_ = stream.Close()
|
||||
}()
|
||||
|
||||
return dataChan, errChan
|
||||
}
|
||||
|
||||
func (c *Client) GetProjectList(ctx context.Context) (*GCPProject, error) {
|
||||
token, err := c.httpClient.Transport.(*oauth2.Transport).Source.Token()
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "https://cloudresourcemanager.googleapis.com/v1/projects", nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not get project list: %v", err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken))
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to execute request: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
_ = resp.Body.Close()
|
||||
}()
|
||||
|
||||
bodyBytes, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("get user info request failed with status %d: %s", resp.StatusCode, string(bodyBytes))
|
||||
}
|
||||
|
||||
var project GCPProject
|
||||
err = json.Unmarshal(bodyBytes, &project)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal project list: %w", err)
|
||||
}
|
||||
return &project, nil
|
||||
}
|
||||
|
||||
// getClientMetadata returns metadata about the client environment.
|
||||
func getClientMetadata() map[string]string {
|
||||
return map[string]string{
|
||||
"ideType": "IDE_UNSPECIFIED",
|
||||
"platform": getPlatform(),
|
||||
"pluginType": "GEMINI",
|
||||
"pluginVersion": pluginVersion,
|
||||
}
|
||||
}
|
||||
|
||||
// getClientMetadataString returns the metadata as a comma-separated string.
|
||||
func getClientMetadataString() string {
|
||||
md := getClientMetadata()
|
||||
parts := make([]string, 0, len(md))
|
||||
for k, v := range md {
|
||||
parts = append(parts, fmt.Sprintf("%s=%s", k, v))
|
||||
}
|
||||
return strings.Join(parts, ",")
|
||||
}
|
||||
|
||||
func getUserAgent() string {
|
||||
return fmt.Sprintf(fmt.Sprintf("GeminiCLI/%s (%s; %s)", pluginVersion, runtime.GOOS, runtime.GOARCH))
|
||||
}
|
||||
|
||||
// getPlatform returns the OS and architecture in the format expected by the API.
|
||||
func getPlatform() string {
|
||||
os := runtime.GOOS
|
||||
arch := runtime.GOARCH
|
||||
switch os {
|
||||
case "darwin":
|
||||
return fmt.Sprintf("DARWIN_%s", strings.ToUpper(arch))
|
||||
case "linux":
|
||||
return fmt.Sprintf("LINUX_%s", strings.ToUpper(arch))
|
||||
case "windows":
|
||||
return fmt.Sprintf("WINDOWS_%s", strings.ToUpper(arch))
|
||||
default:
|
||||
return "PLATFORM_UNSPECIFIED"
|
||||
}
|
||||
}
|
||||
37
internal/config/config.go
Normal file
37
internal/config/config.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gopkg.in/yaml.v3"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Config represents the application's configuration
|
||||
type Config struct {
|
||||
Port int `yaml:"port"`
|
||||
AuthDir string `yaml:"auth_dir"`
|
||||
Debug bool `yaml:"debug"`
|
||||
ApiKeys []string `yaml:"api_keys"`
|
||||
}
|
||||
|
||||
// / LoadConfig loads the configuration from the specified file
|
||||
func LoadConfig(configFile string) (*Config, error) {
|
||||
// Read the configuration file
|
||||
data, err := os.ReadFile(configFile)
|
||||
// If reading the file fails
|
||||
if err != nil {
|
||||
// Return an error
|
||||
return nil, fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
|
||||
// Parse the YAML data
|
||||
var config Config
|
||||
// If parsing the YAML data fails
|
||||
if err = yaml.Unmarshal(data, &config); err != nil {
|
||||
// Return an error
|
||||
return nil, fmt.Errorf("failed to parse config file: %w", err)
|
||||
}
|
||||
|
||||
// Return the configuration
|
||||
return &config, nil
|
||||
}
|
||||
Reference in New Issue
Block a user