From 1bb05f093837f713eac4ad5f8addc08afad20755 Mon Sep 17 00:00:00 2001 From: ScuroNeko Date: Fri, 31 Jul 2026 15:21:56 +0300 Subject: [PATCH] (new): v0.2.0 release --- CHANGELOG.md | 25 ++++++ README.md | 7 +- completion.go | 159 +++++++++++++++++++++++++++++++++++ completion_test.go | 170 ++++++++++++++++++++++++++++++++++++++ doc.go | 2 +- methods.go | 155 +++-------------------------------- methods_test.go | 200 +++++++++++---------------------------------- pow_test.go | 2 +- sse.go | 12 +-- 9 files changed, 422 insertions(+), 310 deletions(-) create mode 100644 completion.go create mode 100644 completion_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index e69de29..96a1a1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -0,0 +1,25 @@ +## v0.2.0 + +### Breaking changes + +- Changed the module path from `deepseek` to `git.scuroneko.dev/scuroneko/go-deepseek`. +- Replaced the `Client.ReadStream` and `Client.ReadStreamAsString` methods with package-level `ReadStream` and `ReadStreamAsString` functions. +- Removed the unused `CloseEvent` protocol type. + +### Added + +- Added `Client.DeleteChat` and `Client.DeleteChatWithContext` for deleting chat sessions. + +### Fixed + +- Updated the PoW package import to use the canonical module path. +- Propagated API business errors from chat deletion and ensured response bodies are closed. +- Stopped logging raw values from unsupported SSE patches and operations. + +### Documentation + +- Updated the package documentation and quick-start example for the `deepseek` package and the canonical import path. + +## v0.1.0 + +- Initial release. diff --git a/README.md b/README.md index ce022d6..e98d04c 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,9 @@ package main import ( "context" - "deepseek" "log" + + "git.scuroneko.dev/scuroneko/go-deepseek" ) func main() { @@ -40,10 +41,10 @@ func main() { panic(err) } - str, err := ds.ReadStreamAsString(ctx, res) + str, err := deepseek.ReadStreamAsString(ctx, res) if err != nil { panic(err) } log.Println(str) } -``` \ No newline at end of file +``` diff --git a/completion.go b/completion.go new file mode 100644 index 0000000..a0d5958 --- /dev/null +++ b/completion.go @@ -0,0 +1,159 @@ +package deepseek + +import ( + "context" + "errors" + "io" + "net/http" + "strings" +) + +// CompletionReq configures a chat completion request. +type CompletionReq struct { + ChatSessionID string `json:"chat_session_id"` + ParentMessageID *uint64 `json:"parent_message_id"` + ModelType ModelType `json:"model_type"` + promptBuilder strings.Builder + Prompt string `json:"prompt"` + RefFileIDs []string `json:"ref_file_ids"` + ThinkingEnabled bool `json:"thinking_enabled"` + SearchEnabled bool `json:"search_enabled"` +} + +// NewCompletionReq creates a completion request for chatID using the default +// model type. +func NewCompletionReq(chatID string) *CompletionReq { + return &CompletionReq{ + ChatSessionID: chatID, ModelType: ModelTypeDefault, + } +} + +// SetParentMessageID sets the parent message for a continued conversation. +func (req *CompletionReq) SetParentMessageID(id uint64) *CompletionReq { + req.ParentMessageID = new(id) + return req +} + +// SetModelType selects the model used for the completion. +func (req *CompletionReq) SetModelType(t ModelType) *CompletionReq { + req.ModelType = t + return req +} + +// AddPrompt appends s to the prompt under construction. +func (req *CompletionReq) AddPrompt(s string) *CompletionReq { + _, _ = req.promptBuilder.WriteString(s) + return req +} + +// BuildPrompt stores the accumulated prompt and resets the internal builder. +func (req *CompletionReq) BuildPrompt() *CompletionReq { + req.Prompt = req.promptBuilder.String() + req.promptBuilder = strings.Builder{} + return req +} + +// SetPrompt replaces the request prompt. +func (req *CompletionReq) SetPrompt(prompt string) *CompletionReq { + req.Prompt = prompt + return req +} + +// SetThinkingEnabled enables or disables model thinking output. +func (req *CompletionReq) SetThinkingEnabled(b bool) *CompletionReq { + req.ThinkingEnabled = b + return req +} + +// SetSearchEnabled enables or disables web search for the completion. +func (req *CompletionReq) SetSearchEnabled(b bool) *CompletionReq { + req.SearchEnabled = b + return req +} + +// ModelType identifies a DeepSeek chat model mode. +type ModelType string + +const ( + // ModelTypeDefault selects the default model mode. + ModelTypeDefault ModelType = "default" + // ModelTypeExpert selects the expert model mode. + ModelTypeExpert ModelType = "expert" +) + +// Completion starts a streaming completion using a background context. +// The caller owns and must either close the response body or pass the response +// to ReadStream or ReadStreamAsString. +func (api *Client) Completion(body CompletionReq) (*http.Response, error) { + return api.CompletionWithContext(context.Background(), body) +} + +// CompletionWithContext starts a streaming completion and honors ctx +// cancellation. The caller owns and must either close the response body or +// pass the response to ReadStream or ReadStreamAsString. +func (api *Client) CompletionWithContext(ctx context.Context, body CompletionReq) (*http.Response, error) { + req := NewRequest[CreatePowChallengeRes]("POST", "chat/completion", body) + err := req.SolvePow(ctx, api) + if err != nil { + return nil, err + } + + resp, err := req.DoWithContext(ctx, api) + if err != nil { + return nil, err + } + return resp, nil +} + +// ReadStream consumes and closes resp.Body, updates stream state, and calls +// onText for newly appended response text. A nil callback is allowed. +func ReadStream(ctx context.Context, resp *http.Response, onText func(string)) (*StreamState, error) { + defer resp.Body.Close() + + reader := NewSSEReader(resp.Body) + state := &StreamState{} + + var previousLength int + + for { + event, err := reader.Next(ctx) + if errors.Is(err, io.EOF) { + break + } + if err != nil { + return nil, err + } + + if err := state.Apply(event); err != nil { + return nil, err + } + + current := state.Content.String() + + if len(current) > previousLength { + chunk := current[previousLength:] + + if onText != nil { + onText(chunk) + } + + previousLength = len(current) + } + + if event.Event == "close" { + break + } + } + + return state, nil +} + +// ReadStreamAsString consumes and closes resp.Body and returns the final +// response content. +func ReadStreamAsString(ctx context.Context, resp *http.Response) (string, error) { + state, err := ReadStream(ctx, resp, nil) + if err != nil { + return "", err + } + return state.Content.String(), nil +} diff --git a/completion_test.go b/completion_test.go new file mode 100644 index 0000000..af19701 --- /dev/null +++ b/completion_test.go @@ -0,0 +1,170 @@ +package deepseek + +import ( + "context" + "encoding/base64" + "encoding/json" + "io" + "net/http" + "reflect" + "strings" + "testing" +) + +func TestCompletionReqBuilder(t *testing.T) { + req := NewCompletionReq("chat-id"). + SetParentMessageID(42). + SetModelType(ModelTypeExpert). + AddPrompt("hello"). + AddPrompt(" world"). + BuildPrompt(). + SetThinkingEnabled(true). + SetSearchEnabled(true) + req.RefFileIDs = []string{"file-id"} + + if req.ChatSessionID != "chat-id" || req.ModelType != ModelTypeExpert || req.Prompt != "hello world" { + t.Fatalf("request = %#v", req) + } + if req.ParentMessageID == nil || *req.ParentMessageID != 42 { + t.Fatalf("parent message ID = %v", req.ParentMessageID) + } + if !req.ThinkingEnabled || !req.SearchEnabled || !reflect.DeepEqual(req.RefFileIDs, []string{"file-id"}) { + t.Fatalf("request options = %#v", req) + } + + req.AddPrompt("new").BuildPrompt() + if req.Prompt != "new" { + t.Fatalf("rebuilt prompt = %q, want new", req.Prompt) + } +} + +func TestCompletionWithContextPreservesRequestOptions(t *testing.T) { + const challenge = "2f90572ad390d758b5e55b3bb74f14722166388023b3b28876d056a358591197" + const salt = "2eeb8f3a703002bfca70" + const expectedAnswer = uint64(61830) + + client := testAPI(func(r *http.Request) (*http.Response, error) { + switch r.URL.Path { + case "/chat/create_pow_challenge": + return testHTTPResponse(`{"code":0,"data":{"biz_code":0,"biz_data":{"challenge":{"algorithm":"DeepSeekHashV1","challenge":"` + challenge + `","salt":"` + salt + `","signature":"signature","difficulty":144000,"expire_at":1785483643587,"target_path":"/api/v0/chat/completion"}}}}`), nil + case "/chat/completion": + encoded := r.Header.Get("x-ds-pow-response") + raw, err := base64.URLEncoding.DecodeString(encoded) + if err != nil { + t.Errorf("decode PoW header: %v", err) + } + var proof struct { + Answer uint64 `json:"answer"` + Signature string `json:"signature"` + TargetPath string `json:"target_path"` + } + if err := json.Unmarshal(raw, &proof); err != nil { + t.Errorf("decode PoW JSON: %v", err) + } + if proof.Answer != expectedAnswer || proof.Signature != "signature" || proof.TargetPath != "/api/v0/chat/completion" { + t.Errorf("proof = %#v", proof) + } + + var body CompletionReq + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Errorf("decode completion: %v", err) + } + if body.ChatSessionID != "chat-id" || body.ParentMessageID == nil || *body.ParentMessageID != 9 || body.ModelType != ModelTypeExpert || body.Prompt != "prompt" || !body.ThinkingEnabled || !body.SearchEnabled || !reflect.DeepEqual(body.RefFileIDs, []string{"file-id"}) { + t.Errorf("completion body = %#v", body) + } + resp := testHTTPResponse("event: close\ndata: {}\n\n") + resp.Header.Set("Content-Type", "text/event-stream") + return resp, nil + default: + t.Fatalf("unexpected path %q", r.URL.Path) + return nil, nil + } + }) + + parentID := uint64(9) + body := CompletionReq{ + ChatSessionID: "chat-id", + ParentMessageID: &parentID, + ModelType: ModelTypeExpert, + Prompt: "prompt", + RefFileIDs: []string{"file-id"}, + ThinkingEnabled: true, + SearchEnabled: true, + } + resp, err := client.CompletionWithContext(context.Background(), body) + if err != nil { + t.Fatalf("CompletionWithContext() error = %v", err) + } + defer resp.Body.Close() +} + +type trackingReadCloser struct { + io.Reader + closed bool +} + +func (r *trackingReadCloser) Close() error { + r.closed = true + return nil +} + +func TestReadStream(t *testing.T) { + stream := strings.Join([]string{ + `event: ready`, + `data: {"request_message_id":1,"response_message_id":2,"model_type":"default"}`, + ``, + `data: {"p":"response/content","o":"APPEND","v":"hel"}`, + ``, + `data: {"v":"lo"}`, + ``, + `event: close`, + `data: {}`, + ``, + }, "\n") + body := &trackingReadCloser{Reader: strings.NewReader(stream)} + resp := &http.Response{Body: body} + var chunks []string + + state, err := ReadStream(context.Background(), resp, func(s string) { + chunks = append(chunks, s) + }) + if err != nil { + t.Fatalf("ReadStream() error = %v", err) + } + if !body.closed { + t.Fatal("response body was not closed") + } + if got := state.Content.String(); got != "hello" { + t.Fatalf("content = %q, want hello", got) + } + if !reflect.DeepEqual(chunks, []string{"hel", "lo"}) { + t.Fatalf("chunks = %#v", chunks) + } + if state.RequestMessageID != 1 || state.ResponseMessageID != 2 || !state.Closed { + t.Fatalf("state = %#v", state) + } +} + +func TestReadStreamAsStringReturnsFinalSetValue(t *testing.T) { + stream := strings.Join([]string{ + `data: {"p":"response/content","o":"APPEND","v":"obsolete value"}`, + ``, + `data: {"p":"response/content","o":"SET","v":"final"}`, + ``, + `event: close`, + `data: {}`, + ``, + }, "\n") + body := &trackingReadCloser{Reader: strings.NewReader(stream)} + + got, err := ReadStreamAsString(context.Background(), &http.Response{Body: body}) + if err != nil { + t.Fatalf("ReadStreamAsString() error = %v", err) + } + if got != "final" { + t.Fatalf("ReadStreamAsString() = %q, want final", got) + } + if !body.closed { + t.Fatal("response body was not closed") + } +} diff --git a/doc.go b/doc.go index b183ceb..5d89d35 100644 --- a/doc.go +++ b/doc.go @@ -1,3 +1,3 @@ -// Package api provides a client for the DeepSeek chat HTTP API and helpers for +// Package deepseek provides a client for the DeepSeek chat HTTP API and helpers for // decoding its server-sent event streams. package deepseek diff --git a/methods.go b/methods.go index d4f1fcd..f97ccd1 100644 --- a/methods.go +++ b/methods.go @@ -2,10 +2,6 @@ package deepseek import ( "context" - "errors" - "io" - "net/http" - "strings" ) // ChatSession describes a DeepSeek chat session. @@ -47,152 +43,25 @@ func (api *Client) CreateChatWithContext(ctx context.Context) (ChatSession, erro return data.Data.Data.ChatSession, nil } -// CompletionReq configures a chat completion request. -type CompletionReq struct { - ChatSessionID string `json:"chat_session_id"` - ParentMessageID *uint64 `json:"parent_message_id"` - ModelType ModelType `json:"model_type"` - promptBuilder strings.Builder - Prompt string `json:"prompt"` - RefFileIDs []string `json:"ref_file_ids"` - ThinkingEnabled bool `json:"thinking_enabled"` - SearchEnabled bool `json:"search_enabled"` +type deleteChatReq struct { + ChatSessionID string `json:"chat_session_id"` } -// NewCompletionReq creates a completion request for chatID using the default -// model type. -func NewCompletionReq(chatID string) *CompletionReq { - return &CompletionReq{ - ChatSessionID: chatID, ModelType: ModelTypeDefault, - } +// DeleteChat deletes a chat session using a background context. +func (api *Client) DeleteChat(chatID string) error { + return api.DeleteChatWithContext(context.Background(), chatID) } -// SetParentMessageID sets the parent message for a continued conversation. -func (req *CompletionReq) SetParentMessageID(id uint64) *CompletionReq { - req.ParentMessageID = new(id) - return req -} - -// SetModelType selects the model used for the completion. -func (req *CompletionReq) SetModelType(t ModelType) *CompletionReq { - req.ModelType = t - return req -} - -// AddPrompt appends s to the prompt under construction. -func (req *CompletionReq) AddPrompt(s string) *CompletionReq { - _, _ = req.promptBuilder.WriteString(s) - return req -} - -// BuildPrompt stores the accumulated prompt and resets the internal builder. -func (req *CompletionReq) BuildPrompt() *CompletionReq { - req.Prompt = req.promptBuilder.String() - req.promptBuilder = strings.Builder{} - return req -} - -// SetPrompt replaces the request prompt. -func (req *CompletionReq) SetPrompt(prompt string) *CompletionReq { - req.Prompt = prompt - return req -} - -// SetThinkingEnabled enables or disables model thinking output. -func (req *CompletionReq) SetThinkingEnabled(b bool) *CompletionReq { - req.ThinkingEnabled = b - return req -} - -// SetSearchEnabled enables or disables web search for the completion. -func (req *CompletionReq) SetSearchEnabled(b bool) *CompletionReq { - req.SearchEnabled = b - return req -} - -// ModelType identifies a DeepSeek chat model mode. -type ModelType string - -const ( - // ModelTypeDefault selects the default model mode. - ModelTypeDefault ModelType = "default" - // ModelTypeExpert selects the expert model mode. - ModelTypeExpert ModelType = "expert" -) - -// Completion starts a streaming completion using a background context. -// The caller owns and must either close the response body or pass the response -// to ReadStream or ReadStreamAsString. -func (api *Client) Completion(body CompletionReq) (*http.Response, error) { - return api.CompletionWithContext(context.Background(), body) -} - -// CompletionWithContext starts a streaming completion and honors ctx -// cancellation. The caller owns and must either close the response body or -// pass the response to ReadStream or ReadStreamAsString. -func (api *Client) CompletionWithContext(ctx context.Context, body CompletionReq) (*http.Response, error) { - req := NewRequest[CreatePowChallengeRes]("POST", "chat/completion", body) - err := req.SolvePow(ctx, api) - if err != nil { - return nil, err - } - +// DeleteChatWithContext deletes a chat session and honors ctx cancellation. +func (api *Client) DeleteChatWithContext(ctx context.Context, chatID string) error { + data := deleteChatReq{ChatSessionID: chatID} + req := NewRequest[any]("POST", "chat_session/delete", data) resp, err := req.DoWithContext(ctx, api) if err != nil { - return nil, err + return err } - return resp, nil -} - -// ReadStream consumes and closes resp.Body, updates stream state, and calls -// onText for newly appended response text. A nil callback is allowed. -func (api *Client) ReadStream(ctx context.Context, resp *http.Response, onText func(string)) (*StreamState, error) { defer resp.Body.Close() - reader := NewSSEReader(resp.Body) - state := &StreamState{} - - var previousLength int - - for { - event, err := reader.Next(ctx) - if errors.Is(err, io.EOF) { - break - } - if err != nil { - return nil, err - } - - if err := state.Apply(event); err != nil { - return nil, err - } - - current := state.Content.String() - - if len(current) > previousLength { - chunk := current[previousLength:] - - if onText != nil { - onText(chunk) - } - - previousLength = len(current) - } - - if event.Event == "close" { - break - } - } - - return state, nil -} - -// ReadStreamAsString consumes and closes resp.Body and returns the final -// response content. -func (api *Client) ReadStreamAsString(ctx context.Context, resp *http.Response) (string, error) { - state, err := api.ReadStream(ctx, resp, nil) - if err != nil { - return "", err - } - return state.Content.String(), nil + _, err = req.unmarshallBizResponse(resp) + return err } diff --git a/methods_test.go b/methods_test.go index 429a678..f805c6f 100644 --- a/methods_test.go +++ b/methods_test.go @@ -2,42 +2,13 @@ package deepseek import ( "context" - "encoding/base64" "encoding/json" "io" "net/http" - "reflect" "strings" "testing" ) -func TestCompletionReqBuilder(t *testing.T) { - req := NewCompletionReq("chat-id"). - SetParentMessageID(42). - SetModelType(ModelTypeExpert). - AddPrompt("hello"). - AddPrompt(" world"). - BuildPrompt(). - SetThinkingEnabled(true). - SetSearchEnabled(true) - req.RefFileIDs = []string{"file-id"} - - if req.ChatSessionID != "chat-id" || req.ModelType != ModelTypeExpert || req.Prompt != "hello world" { - t.Fatalf("request = %#v", req) - } - if req.ParentMessageID == nil || *req.ParentMessageID != 42 { - t.Fatalf("parent message ID = %v", req.ParentMessageID) - } - if !req.ThinkingEnabled || !req.SearchEnabled || !reflect.DeepEqual(req.RefFileIDs, []string{"file-id"}) { - t.Fatalf("request options = %#v", req) - } - - req.AddPrompt("new").BuildPrompt() - if req.Prompt != "new" { - t.Fatalf("rebuilt prompt = %q, want new", req.Prompt) - } -} - func TestCreateChatWithContext(t *testing.T) { client := testAPI(func(r *http.Request) (*http.Response, error) { if r.URL.Path != "/chat_session/create" { @@ -62,133 +33,58 @@ func TestCreateChatWithContext(t *testing.T) { } } -func TestCompletionWithContextPreservesRequestOptions(t *testing.T) { - const challenge = "2f90572ad390d758b5e55b3bb74f14722166388023b3b28876d056a358591197" - const salt = "2eeb8f3a703002bfca70" - const expectedAnswer = uint64(61830) - - client := testAPI(func(r *http.Request) (*http.Response, error) { - switch r.URL.Path { - case "/chat/create_pow_challenge": - return testHTTPResponse(`{"code":0,"data":{"biz_code":0,"biz_data":{"challenge":{"algorithm":"DeepSeekHashV1","challenge":"` + challenge + `","salt":"` + salt + `","signature":"signature","difficulty":144000,"expire_at":1785483643587,"target_path":"/api/v0/chat/completion"}}}}`), nil - case "/chat/completion": - encoded := r.Header.Get("x-ds-pow-response") - raw, err := base64.URLEncoding.DecodeString(encoded) - if err != nil { - t.Errorf("decode PoW header: %v", err) - } - var proof struct { - Answer uint64 `json:"answer"` - Signature string `json:"signature"` - TargetPath string `json:"target_path"` - } - if err := json.Unmarshal(raw, &proof); err != nil { - t.Errorf("decode PoW JSON: %v", err) - } - if proof.Answer != expectedAnswer || proof.Signature != "signature" || proof.TargetPath != "/api/v0/chat/completion" { - t.Errorf("proof = %#v", proof) - } - - var body CompletionReq - if err := json.NewDecoder(r.Body).Decode(&body); err != nil { - t.Errorf("decode completion: %v", err) - } - if body.ChatSessionID != "chat-id" || body.ParentMessageID == nil || *body.ParentMessageID != 9 || body.ModelType != ModelTypeExpert || body.Prompt != "prompt" || !body.ThinkingEnabled || !body.SearchEnabled || !reflect.DeepEqual(body.RefFileIDs, []string{"file-id"}) { - t.Errorf("completion body = %#v", body) - } - resp := testHTTPResponse("event: close\ndata: {}\n\n") - resp.Header.Set("Content-Type", "text/event-stream") - return resp, nil - default: - t.Fatalf("unexpected path %q", r.URL.Path) - return nil, nil - } - }) - - parentID := uint64(9) - body := CompletionReq{ - ChatSessionID: "chat-id", - ParentMessageID: &parentID, - ModelType: ModelTypeExpert, - Prompt: "prompt", - RefFileIDs: []string{"file-id"}, - ThinkingEnabled: true, - SearchEnabled: true, +func TestDeleteChatWithContext(t *testing.T) { + tests := []struct { + name string + body string + wantErr string + }{ + { + name: "success", + body: `{"code":0,"data":{"biz_code":0,"biz_data":{}}}`, + }, + { + name: "business error", + body: `{"code":0,"data":{"biz_code":42,"biz_msg":"delete denied"}}`, + wantErr: "delete denied (42)", + }, } - resp, err := client.CompletionWithContext(context.Background(), body) - if err != nil { - t.Fatalf("CompletionWithContext() error = %v", err) - } - defer resp.Body.Close() -} -type trackingReadCloser struct { - io.Reader - closed bool -} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + responseBody := &trackingReadCloser{Reader: strings.NewReader(tt.body)} + client := testAPI(func(r *http.Request) (*http.Response, error) { + if r.Method != http.MethodPost { + t.Errorf("method = %q, want POST", r.Method) + } + if r.URL.Path != "/chat_session/delete" { + t.Errorf("path = %q, want /chat_session/delete", r.URL.Path) + } -func (r *trackingReadCloser) Close() error { - r.closed = true - return nil -} + var body deleteChatReq + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Errorf("decode request: %v", err) + } + if body.ChatSessionID != "chat-id" { + t.Errorf("chat_session_id = %q, want chat-id", body.ChatSessionID) + } -func TestReadStream(t *testing.T) { - stream := strings.Join([]string{ - `event: ready`, - `data: {"request_message_id":1,"response_message_id":2,"model_type":"default"}`, - ``, - `data: {"p":"response/content","o":"APPEND","v":"hel"}`, - ``, - `data: {"v":"lo"}`, - ``, - `event: close`, - `data: {}`, - ``, - }, "\n") - body := &trackingReadCloser{Reader: strings.NewReader(stream)} - resp := &http.Response{Body: body} - var chunks []string + resp := testHTTPResponse("") + resp.Body = responseBody + return resp, nil + }) - state, err := NewClient().ReadStream(context.Background(), resp, func(s string) { - chunks = append(chunks, s) - }) - if err != nil { - t.Fatalf("ReadStream() error = %v", err) - } - if !body.closed { - t.Fatal("response body was not closed") - } - if got := state.Content.String(); got != "hello" { - t.Fatalf("content = %q, want hello", got) - } - if !reflect.DeepEqual(chunks, []string{"hel", "lo"}) { - t.Fatalf("chunks = %#v", chunks) - } - if state.RequestMessageID != 1 || state.ResponseMessageID != 2 || !state.Closed { - t.Fatalf("state = %#v", state) - } -} - -func TestReadStreamAsStringReturnsFinalSetValue(t *testing.T) { - stream := strings.Join([]string{ - `data: {"p":"response/content","o":"APPEND","v":"obsolete value"}`, - ``, - `data: {"p":"response/content","o":"SET","v":"final"}`, - ``, - `event: close`, - `data: {}`, - ``, - }, "\n") - body := &trackingReadCloser{Reader: strings.NewReader(stream)} - - got, err := NewClient().ReadStreamAsString(context.Background(), &http.Response{Body: body}) - if err != nil { - t.Fatalf("ReadStreamAsString() error = %v", err) - } - if got != "final" { - t.Fatalf("ReadStreamAsString() = %q, want final", got) - } - if !body.closed { - t.Fatal("response body was not closed") + err := client.DeleteChatWithContext(context.Background(), "chat-id") + if tt.wantErr != "" { + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Fatalf("DeleteChatWithContext() error = %v, want containing %q", err, tt.wantErr) + } + } else if err != nil { + t.Fatalf("DeleteChatWithContext() error = %v", err) + } + if !responseBody.closed { + t.Fatal("response body was not closed") + } + }) } } diff --git a/pow_test.go b/pow_test.go index 28c54f4..5ade4ee 100644 --- a/pow_test.go +++ b/pow_test.go @@ -4,7 +4,7 @@ import ( "errors" "testing" - deeppow "dsp/pow" + deeppow "git.scuroneko.dev/scuroneko/go-deepseek/pow" ) func TestPowChallengeToBase64PropagatesSolverError(t *testing.T) { diff --git a/sse.go b/sse.go index 3032501..ece9e65 100644 --- a/sse.go +++ b/sse.go @@ -137,12 +137,6 @@ type TitleEvent struct { Content string `json:"content"` } -// CloseEvent describes how a completed stream should be closed or resumed. -type CloseEvent struct { - ClickBehavior string `json:"click_behavior"` - AutoResume bool `json:"auto_resume"` -} - // PatchEvent describes a compact patch in the DeepSeek stream protocol. // // Pointer fields distinguish: @@ -475,9 +469,8 @@ func (s *StreamState) applyPatch( case "response": if operation != "BATCH" { log.Printf( - "unknown response operation: op=%q value=%s", + "unknown response operation: op=%q", operation, - value, ) return nil @@ -489,10 +482,9 @@ func (s *StreamState) applyPatch( default: log.Printf( - "unknown patch: path=%q op=%q value=%s", + "unknown patch: path=%q op=%q", path, operation, - value, ) }