diff --git a/.gitignore b/.gitignore index e7c93c57..196761a9 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ .env.* !.env.example /gophergate +/gophergate_* /llm-proxy /llm-proxy-go *.log diff --git a/README.md b/README.md index 24081c22..195eb9ec 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ A unified, high-performance LLM proxy gateway built in Go. It provides OpenAI-co ## Features - **Unified API:** OpenAI-compatible `/v1/chat/completions`, `/v1/images/generations`, `/v1/responses`, and `/v1/models` endpoints. - - The `/v1/responses` endpoint (OpenAI Responses API) is currently supported for OpenAI models only. Non-OpenAI providers (Gemini, DeepSeek, Moonshot, Grok, Ollama, Xiaomi) return a "not supported" response. + - The `/v1/responses` endpoint (OpenAI Responses API) is supported for OpenAI and DeepSeek models. Non-supported providers (Gemini, Moonshot, Grok, Ollama, Xiaomi) return a "not supported" response. - **Multi-Provider Support:** - **OpenAI:** GPT-4o, GPT-4o Mini, GPT-5, GPT-5.4, o1/o3/o4 reasoning models, DALL-E 2/3 image generation. - **Google Gemini:** Gemini 2.5 Flash/Pro, Gemini 3 Flash/Pro previews, Imagen 3 image generation. diff --git a/internal/db/db.go b/internal/db/db.go index 1dd29702..7c9b6b6b 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -26,7 +26,7 @@ func Init(path string) (*DB, error) { } // Connect to SQLite - dsn := fmt.Sprintf("file:%s?_pragma=foreign_keys(1)", path) + dsn := fmt.Sprintf("file:%s?_pragma=foreign_keys(1)&_busy_timeout=5000", path) db, err := sqlx.Connect("sqlite", dsn) if err != nil { return nil, fmt.Errorf("failed to connect to database: %w", err) diff --git a/internal/models/models.go b/internal/models/models.go index 29286989..9487181d 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -32,6 +32,7 @@ type ChatMessage struct { ToolCalls []ToolCall `json:"tool_calls,omitempty"` Name *string `json:"name,omitempty"` ToolCallID *string `json:"tool_call_id,omitempty"` + Prefix *bool `json:"prefix,omitempty"` } type ContentPart struct { @@ -168,6 +169,7 @@ type UnifiedMessage struct { ToolCalls []ToolCall Name *string ToolCallID *string + Prefix *bool } type UnifiedContentPart struct { diff --git a/internal/providers/deepseek.go b/internal/providers/deepseek.go index b7743d8d..d74c860e 100644 --- a/internal/providers/deepseek.go +++ b/internal/providers/deepseek.go @@ -257,9 +257,70 @@ func (p *DeepSeekProvider) ImageGeneration(ctx context.Context, req *models.Imag } func (p *DeepSeekProvider) Responses(ctx context.Context, req *models.ResponsesRequest) (*models.ResponsesResponse, error) { - return nil, fmt.Errorf("responses API not supported by deepseek") + stream := req.Stream != nil && *req.Stream + body := BuildOpenAIResponsesBody(req, stream) + + resp, err := p.client.R(). + SetContext(ctx). + SetHeader("Authorization", "Bearer "+p.apiKey). + SetBody(body). + Post(fmt.Sprintf("%s/responses", p.config.BaseURL)) + + if err != nil { + return nil, fmt.Errorf("responses request failed: %w", err) + } + + if !resp.IsSuccess() { + msg := resp.String() + if msg == "" && resp.RawBody() != nil { + if bodyBytes, err := io.ReadAll(resp.RawBody()); err == nil { + msg = string(bodyBytes) + } + } + return nil, fmt.Errorf("DeepSeek Responses API error (%d): %s", resp.StatusCode(), msg) + } + + var respJSON map[string]interface{} + if err := json.Unmarshal(resp.Body(), &respJSON); err != nil { + return nil, fmt.Errorf("failed to parse responses response: %w", err) + } + + return ParseOpenAIResponsesResponse(respJSON, req.Model) } func (p *DeepSeekProvider) ResponsesStream(ctx context.Context, req *models.ResponsesRequest) (<-chan *models.ResponsesStreamChunk, error) { - return nil, fmt.Errorf("responses API not supported by deepseek") + body := BuildOpenAIResponsesBody(req, true) + + resp, err := p.client.R(). + SetContext(ctx). + SetHeader("Authorization", "Bearer "+p.apiKey). + SetBody(body). + SetDoNotParseResponse(true). + Post(fmt.Sprintf("%s/responses", p.config.BaseURL)) + + if err != nil { + return nil, fmt.Errorf("responses stream request failed: %w", err) + } + + if !resp.IsSuccess() { + msg := resp.String() + if msg == "" && resp.RawBody() != nil { + if bodyBytes, err := io.ReadAll(resp.RawBody()); err == nil { + msg = string(bodyBytes) + } + } + return nil, fmt.Errorf("DeepSeek Responses API error (%d): %s", resp.StatusCode(), msg) + } + + ch := make(chan *models.ResponsesStreamChunk) + + go func() { + defer close(ch) + err := StreamOpenAIResponses(resp.RawBody(), ch) + if err != nil { + fmt.Printf("DeepSeek Responses stream error: %v\n", err) + } + }() + + return ch, nil } diff --git a/internal/providers/helpers.go b/internal/providers/helpers.go index 6b0d35d3..9fe6b624 100644 --- a/internal/providers/helpers.go +++ b/internal/providers/helpers.go @@ -158,6 +158,9 @@ func MessagesToOpenAIJSON(messages []models.UnifiedMessage) ([]interface{}, erro if m.Name != nil { msg["name"] = *m.Name } + if m.Prefix != nil { + msg["prefix"] = *m.Prefix + } result = append(result, msg) } return result, nil diff --git a/internal/server/server.go b/internal/server/server.go index f1215f58..0b63c077 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -700,6 +700,7 @@ func (s *Server) handleChatCompletions(c *gin.Context) { ToolCalls: msg.ToolCalls, Name: msg.Name, ToolCallID: msg.ToolCallID, + Prefix: msg.Prefix, } // Handle multimodal content