| |
| |
| |
| |
| |
| |
| package claude |
|
|
| import ( |
| "bytes" |
| "compress/gzip" |
| "context" |
| "encoding/json" |
| "fmt" |
| "io" |
| "net/http" |
|
|
| "github.com/gin-gonic/gin" |
| . "github.com/router-for-me/CLIProxyAPI/v6/internal/constant" |
| "github.com/router-for-me/CLIProxyAPI/v6/internal/interfaces" |
| "github.com/router-for-me/CLIProxyAPI/v6/internal/registry" |
| "github.com/router-for-me/CLIProxyAPI/v6/sdk/api/handlers" |
| log "github.com/sirupsen/logrus" |
| "github.com/tidwall/gjson" |
| ) |
|
|
| |
| |
| type ClaudeCodeAPIHandler struct { |
| *handlers.BaseAPIHandler |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| func NewClaudeCodeAPIHandler(apiHandlers *handlers.BaseAPIHandler) *ClaudeCodeAPIHandler { |
| return &ClaudeCodeAPIHandler{ |
| BaseAPIHandler: apiHandlers, |
| } |
| } |
|
|
| |
| func (h *ClaudeCodeAPIHandler) HandlerType() string { |
| return Claude |
| } |
|
|
| |
| func (h *ClaudeCodeAPIHandler) Models() []map[string]any { |
| |
| modelRegistry := registry.GetGlobalRegistry() |
| models := modelRegistry.GetAvailableModels("claude") |
|
|
| |
| kiroModels := registry.GetKiroModels() |
| seen := make(map[string]bool) |
| for _, m := range models { |
| if id, ok := m["id"].(string); ok { |
| seen[id] = true |
| } |
| } |
| for _, km := range kiroModels { |
| if seen[km.ID] { |
| continue |
| } |
| models = append(models, map[string]any{ |
| "id": km.ID, |
| "object": "model", |
| "created": km.Created, |
| "owned_by": "kiro", |
| "type": "model", |
| "display_name": km.DisplayName, |
| }) |
| } |
|
|
| return models |
| } |
|
|
| |
| |
| |
| |
| |
| |
| func (h *ClaudeCodeAPIHandler) ClaudeMessages(c *gin.Context) { |
| |
| rawJSON, err := c.GetRawData() |
| |
| if err != nil { |
| c.JSON(http.StatusBadRequest, handlers.ErrorResponse{ |
| Error: handlers.ErrorDetail{ |
| Message: fmt.Sprintf("Invalid request: %v", err), |
| Type: "invalid_request_error", |
| }, |
| }) |
| return |
| } |
|
|
| |
| streamResult := gjson.GetBytes(rawJSON, "stream") |
| if !streamResult.Exists() || streamResult.Type == gjson.False { |
| h.handleNonStreamingResponse(c, rawJSON) |
| } else { |
| h.handleStreamingResponse(c, rawJSON) |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| func (h *ClaudeCodeAPIHandler) ClaudeCountTokens(c *gin.Context) { |
| |
| rawJSON, err := c.GetRawData() |
| |
| if err != nil { |
| c.JSON(http.StatusBadRequest, handlers.ErrorResponse{ |
| Error: handlers.ErrorDetail{ |
| Message: fmt.Sprintf("Invalid request: %v", err), |
| Type: "invalid_request_error", |
| }, |
| }) |
| return |
| } |
|
|
| c.Header("Content-Type", "application/json") |
|
|
| alt := h.GetAlt(c) |
| cliCtx, cliCancel := h.GetContextWithCancel(h, c, context.Background()) |
|
|
| modelName := gjson.GetBytes(rawJSON, "model").String() |
|
|
| resp, errMsg := h.ExecuteCountWithAuthManager(cliCtx, h.HandlerType(), modelName, rawJSON, alt) |
| if errMsg != nil { |
| h.WriteErrorResponse(c, errMsg) |
| cliCancel(errMsg.Error) |
| return |
| } |
| _, _ = c.Writer.Write(resp) |
| cliCancel() |
| } |
|
|
| |
| |
| |
| |
| |
| func (h *ClaudeCodeAPIHandler) ClaudeModels(c *gin.Context) { |
| models := h.Models() |
| firstID := "" |
| lastID := "" |
| if len(models) > 0 { |
| if id, ok := models[0]["id"].(string); ok { |
| firstID = id |
| } |
| if id, ok := models[len(models)-1]["id"].(string); ok { |
| lastID = id |
| } |
| } |
|
|
| c.JSON(http.StatusOK, gin.H{ |
| "data": models, |
| "has_more": false, |
| "first_id": firstID, |
| "last_id": lastID, |
| }) |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| func (h *ClaudeCodeAPIHandler) handleNonStreamingResponse(c *gin.Context, rawJSON []byte) { |
| c.Header("Content-Type", "application/json") |
| alt := h.GetAlt(c) |
| cliCtx, cliCancel := h.GetContextWithCancel(h, c, context.Background()) |
| stopKeepAlive := h.StartNonStreamingKeepAlive(c, cliCtx) |
|
|
| modelName := gjson.GetBytes(rawJSON, "model").String() |
|
|
| resp, errMsg := h.ExecuteWithAuthManager(cliCtx, h.HandlerType(), modelName, rawJSON, alt) |
| stopKeepAlive() |
| if errMsg != nil { |
| h.WriteErrorResponse(c, errMsg) |
| cliCancel(errMsg.Error) |
| return |
| } |
|
|
| |
| |
| if len(resp) >= 2 && resp[0] == 0x1f && resp[1] == 0x8b { |
| gzReader, errGzip := gzip.NewReader(bytes.NewReader(resp)) |
| if errGzip != nil { |
| log.Warnf("failed to decompress gzipped Claude response: %v", errGzip) |
| } else { |
| defer func() { |
| if errClose := gzReader.Close(); errClose != nil { |
| log.Warnf("failed to close Claude gzip reader: %v", errClose) |
| } |
| }() |
| decompressed, errRead := io.ReadAll(gzReader) |
| if errRead != nil { |
| log.Warnf("failed to read decompressed Claude response: %v", errRead) |
| } else { |
| resp = decompressed |
| } |
| } |
| } |
|
|
| _, _ = c.Writer.Write(resp) |
| cliCancel() |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| func (h *ClaudeCodeAPIHandler) handleStreamingResponse(c *gin.Context, rawJSON []byte) { |
| |
| |
| flusher, ok := c.Writer.(http.Flusher) |
| if !ok { |
| c.JSON(http.StatusInternalServerError, handlers.ErrorResponse{ |
| Error: handlers.ErrorDetail{ |
| Message: "Streaming not supported", |
| Type: "server_error", |
| }, |
| }) |
| return |
| } |
|
|
| modelName := gjson.GetBytes(rawJSON, "model").String() |
|
|
| |
| |
| cliCtx, cliCancel := h.GetContextWithCancel(h, c, context.Background()) |
|
|
| dataChan, errChan := h.ExecuteStreamWithAuthManager(cliCtx, h.HandlerType(), modelName, rawJSON, "") |
| setSSEHeaders := func() { |
| c.Header("Content-Type", "text/event-stream") |
| c.Header("Cache-Control", "no-cache") |
| c.Header("Connection", "keep-alive") |
| c.Header("Access-Control-Allow-Origin", "*") |
| } |
|
|
| |
| for { |
| select { |
| case <-c.Request.Context().Done(): |
| cliCancel(c.Request.Context().Err()) |
| return |
| case errMsg, ok := <-errChan: |
| if !ok { |
| |
| errChan = nil |
| continue |
| } |
| |
| h.WriteErrorResponse(c, errMsg) |
| if errMsg != nil { |
| cliCancel(errMsg.Error) |
| } else { |
| cliCancel(nil) |
| } |
| return |
| case chunk, ok := <-dataChan: |
| if !ok { |
| |
| setSSEHeaders() |
| flusher.Flush() |
| cliCancel(nil) |
| return |
| } |
|
|
| |
| setSSEHeaders() |
|
|
| |
| if len(chunk) > 0 { |
| _, _ = c.Writer.Write(chunk) |
| flusher.Flush() |
| } |
|
|
| |
| h.forwardClaudeStream(c, flusher, func(err error) { cliCancel(err) }, dataChan, errChan) |
| return |
| } |
| } |
| } |
|
|
| func (h *ClaudeCodeAPIHandler) forwardClaudeStream(c *gin.Context, flusher http.Flusher, cancel func(error), data <-chan []byte, errs <-chan *interfaces.ErrorMessage) { |
| h.ForwardStream(c, flusher, cancel, data, errs, handlers.StreamForwardOptions{ |
| WriteChunk: func(chunk []byte) { |
| if len(chunk) == 0 { |
| return |
| } |
| _, _ = c.Writer.Write(chunk) |
| }, |
| WriteTerminalError: func(errMsg *interfaces.ErrorMessage) { |
| if errMsg == nil { |
| return |
| } |
| status := http.StatusInternalServerError |
| if errMsg.StatusCode > 0 { |
| status = errMsg.StatusCode |
| } |
| c.Status(status) |
|
|
| errorBytes, _ := json.Marshal(h.toClaudeError(errMsg)) |
| _, _ = fmt.Fprintf(c.Writer, "event: error\ndata: %s\n\n", errorBytes) |
| }, |
| }) |
| } |
|
|
| type claudeErrorDetail struct { |
| Type string `json:"type"` |
| Message string `json:"message"` |
| } |
|
|
| type claudeErrorResponse struct { |
| Type string `json:"type"` |
| Error claudeErrorDetail `json:"error"` |
| } |
|
|
| func (h *ClaudeCodeAPIHandler) toClaudeError(msg *interfaces.ErrorMessage) claudeErrorResponse { |
| return claudeErrorResponse{ |
| Type: "error", |
| Error: claudeErrorDetail{ |
| Type: "api_error", |
| Message: msg.Error.Error(), |
| }, |
| } |
| } |
|
|