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:
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