package deepseek import ( "bytes" "context" "encoding/json" "fmt" "io" "log" "net/http" "net/http/cookiejar" ) const baseURL = "https://chat.deepseek.com/api/v0" // NoParams is an empty JSON object used by endpoints without request fields. var NoParams struct{} // Response is the outer response envelope returned by the DeepSeek API. type Response[T any] struct { Code int `json:"code"` Message string `json:"msg"` Data T `json:"data"` } // BizResponse is the business-level response envelope nested inside Response. type BizResponse[T any] struct { Code int `json:"biz_code"` Message string `json:"biz_msg"` Data T `json:"biz_data"` } // Client is a configurable client for the DeepSeek chat API. // // A client may be reused for multiple requests. Callers must not mutate its // configuration concurrently with active requests. type Client struct { httpClient *http.Client token string baseURL string deviceIDProvider DeviceIDProvider } // NewClient creates a client with the default endpoint and a cookie jar. func NewClient() *Client { jar, err := cookiejar.New(nil) if err != nil { log.Println(err) } return &Client{httpClient: &http.Client{Jar: jar}, baseURL: baseURL} } // SetToken sets the bearer token used by subsequent requests. func (api *Client) SetToken(token string) *Client { api.token = token return api } // SetDeviceIDProvider sets the source used to obtain login device IDs. // Passing nil restores the random provider behavior used by Login. func (api *Client) SetDeviceIDProvider(p DeviceIDProvider) *Client { api.deviceIDProvider = p return api } // SetHTTPClient replaces the underlying HTTP client when client is non-nil. func (api *Client) SetHTTPClient(client *http.Client) *Client { if client != nil { api.httpClient = client } return api } // SetBaseURL replaces the API base URL when url is non-empty. func (api *Client) SetBaseURL(url string) *Client { if url != "" { api.baseURL = url } return api } // Request describes an HTTP request with parameter type P and response data // type R. type Request[P, R any] struct { params P method string path string powAnswer string } // NewRequest creates a typed API request. func NewRequest[R, P any](method, path string, params P) *Request[P, R] { return &Request[P, R]{params: params, method: method, path: path} } func (r *Request[P, R]) marshallRequest() ([]byte, error) { data, err := json.Marshal(r.params) if err != nil { return nil, err } return data, err } func (r *Request[P, R]) unmarshallResponse(resp *http.Response) (Response[R], error) { var zero Response[R] data, err := io.ReadAll(resp.Body) if err != nil { return zero, err } err = json.Unmarshal(data, &zero) if err != nil { return zero, err } if zero.Code != 0 { return zero, fmt.Errorf("unknown error: %s (%d)", zero.Message, zero.Code) } return zero, err } func (r *Request[P, R]) unmarshallBizResponse(resp *http.Response) (Response[BizResponse[R]], error) { var zero Response[BizResponse[R]] data, err := io.ReadAll(resp.Body) if err != nil { return zero, err } err = json.Unmarshal(data, &zero) if err != nil { return zero, err } if zero.Code != 0 { return zero, fmt.Errorf("unknown error: %s (%d)", zero.Message, zero.Code) } if zero.Data.Code != 0 { return zero, fmt.Errorf("unknown error: %s (%d)", zero.Data.Message, zero.Data.Code) } return zero, err } // SolvePow requests and solves a proof-of-work challenge for r. func (r *Request[P, R]) SolvePow(ctx context.Context, api *Client) error { res, err := api.CreatePowChallengeWithContext(ctx) if err != nil { return err } data, err := res.Challenge.ToBase64() if err != nil { return err } r.powAnswer = string(data) return nil } // DoWithContext sends r using api and returns the raw HTTP response. // The caller owns and must close the response body. func (r *Request[P, R]) DoWithContext(ctx context.Context, api *Client) (*http.Response, error) { data, err := r.marshallRequest() if err != nil { return nil, err } req, err := http.NewRequestWithContext(ctx, r.method, api.baseURL+"/"+r.path, bytes.NewBuffer(data)) if err != nil { return nil, err } log.Printf( "request: method=%s scheme=%s host=%s path=%s rawQuery=%s", req.Method, req.URL.Scheme, req.URL.Host, req.URL.Path, req.URL.RawQuery, ) headers := getHeaders() if r.powAnswer != "" { headers["x-ds-pow-response"] = r.powAnswer } if api.token != "" { headers["authorization"] = "Bearer " + api.token } for k, v := range headers { req.Header.Add(k, v) } resp, err := api.httpClient.Do(req) if err != nil { return nil, err } log.Printf("response: method=%s status=%s code=%d", req.Method, resp.Status, resp.StatusCode, ) return resp, err } // Do sends r using a background context. // The caller owns and must close the response body. func (r *Request[P, R]) Do(api *Client) (*http.Response, error) { return r.DoWithContext(context.Background(), api) } func getHeaders() map[string]string { return map[string]string{ "Content-Type": "application/json", "Accept": "application/json", "User-Agent": "DeepSeek/2 CFNetwork/1568.100.1 Darwin/24.0.0", "x-client-platform": "ios", "x-client-version": "2.0.4", "x-client-bundle-id": "com.deepseek.chat", "x-client-locale": "en_US", "x-client-timezone-offset": "3600", } }