REPOSITORY / ScuroNeko/go-deepseek

Compare commits

DIFF REPOSITORY
2 Commits
Author SHA1 Message Date
ScuroNeko 1bb05f0938 (new): v0.2.0 release 2026-07-31 15:21:56 +03:00
ScuroNeko fb95804b4b initial commit 2026-07-31 14:27:20 +03:00
10 changed files with 424 additions and 311 deletions
+25
View File
@@ -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.
+4 -3
View File
@@ -13,8 +13,9 @@ package main
import ( import (
"context" "context"
"deepseek"
"log" "log"
"git.scuroneko.dev/scuroneko/go-deepseek"
) )
func main() { func main() {
@@ -40,10 +41,10 @@ func main() {
panic(err) panic(err)
} }
str, err := ds.ReadStreamAsString(ctx, res) str, err := deepseek.ReadStreamAsString(ctx, res)
if err != nil { if err != nil {
panic(err) panic(err)
} }
log.Println(str) log.Println(str)
} }
``` ```
+159
View File
@@ -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
}
+170
View File
@@ -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")
}
}
+1 -1
View File
@@ -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. // decoding its server-sent event streams.
package deepseek package deepseek
+12 -143
View File
@@ -2,10 +2,6 @@ package deepseek
import ( import (
"context" "context"
"errors"
"io"
"net/http"
"strings"
) )
// ChatSession describes a DeepSeek chat session. // 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 return data.Data.Data.ChatSession, nil
} }
// CompletionReq configures a chat completion request. type deleteChatReq struct {
type CompletionReq struct { ChatSessionID string `json:"chat_session_id"`
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 // DeleteChat deletes a chat session using a background context.
// model type. func (api *Client) DeleteChat(chatID string) error {
func NewCompletionReq(chatID string) *CompletionReq { return api.DeleteChatWithContext(context.Background(), chatID)
return &CompletionReq{
ChatSessionID: chatID, ModelType: ModelTypeDefault,
}
} }
// SetParentMessageID sets the parent message for a continued conversation. // DeleteChatWithContext deletes a chat session and honors ctx cancellation.
func (req *CompletionReq) SetParentMessageID(id uint64) *CompletionReq { func (api *Client) DeleteChatWithContext(ctx context.Context, chatID string) error {
req.ParentMessageID = new(id) data := deleteChatReq{ChatSessionID: chatID}
return req req := NewRequest[any]("POST", "chat_session/delete", data)
}
// 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) resp, err := req.DoWithContext(ctx, api)
if err != nil { 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() defer resp.Body.Close()
reader := NewSSEReader(resp.Body) _, err = req.unmarshallBizResponse(resp)
state := &StreamState{} return err
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
} }
+48 -152
View File
@@ -2,42 +2,13 @@ package deepseek
import ( import (
"context" "context"
"encoding/base64"
"encoding/json" "encoding/json"
"io" "io"
"net/http" "net/http"
"reflect"
"strings" "strings"
"testing" "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) { func TestCreateChatWithContext(t *testing.T) {
client := testAPI(func(r *http.Request) (*http.Response, error) { client := testAPI(func(r *http.Request) (*http.Response, error) {
if r.URL.Path != "/chat_session/create" { if r.URL.Path != "/chat_session/create" {
@@ -62,133 +33,58 @@ func TestCreateChatWithContext(t *testing.T) {
} }
} }
func TestCompletionWithContextPreservesRequestOptions(t *testing.T) { func TestDeleteChatWithContext(t *testing.T) {
const challenge = "2f90572ad390d758b5e55b3bb74f14722166388023b3b28876d056a358591197" tests := []struct {
const salt = "2eeb8f3a703002bfca70" name string
const expectedAnswer = uint64(61830) body string
wantErr string
client := testAPI(func(r *http.Request) (*http.Response, error) { }{
switch r.URL.Path { {
case "/chat/create_pow_challenge": name: "success",
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 body: `{"code":0,"data":{"biz_code":0,"biz_data":{}}}`,
case "/chat/completion": },
encoded := r.Header.Get("x-ds-pow-response") {
raw, err := base64.URLEncoding.DecodeString(encoded) name: "business error",
if err != nil { body: `{"code":0,"data":{"biz_code":42,"biz_msg":"delete denied"}}`,
t.Errorf("decode PoW header: %v", err) wantErr: "delete denied (42)",
} },
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 { for _, tt := range tests {
io.Reader t.Run(tt.name, func(t *testing.T) {
closed bool 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 { var body deleteChatReq
r.closed = true if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
return 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) { resp := testHTTPResponse("")
stream := strings.Join([]string{ resp.Body = responseBody
`event: ready`, return resp, nil
`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 := NewClient().ReadStream(context.Background(), resp, func(s string) { err := client.DeleteChatWithContext(context.Background(), "chat-id")
chunks = append(chunks, s) if tt.wantErr != "" {
}) if err == nil || !strings.Contains(err.Error(), tt.wantErr) {
if err != nil { t.Fatalf("DeleteChatWithContext() error = %v, want containing %q", err, tt.wantErr)
t.Fatalf("ReadStream() error = %v", err) }
} } else if err != nil {
if !body.closed { t.Fatalf("DeleteChatWithContext() error = %v", err)
t.Fatal("response body was not closed") }
} if !responseBody.closed {
if got := state.Content.String(); got != "hello" { t.Fatal("response body was not closed")
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")
} }
} }
+2 -1
View File
@@ -2,10 +2,11 @@ package deepseek
import ( import (
"context" "context"
"deepseek/pow"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"log" "log"
"git.scuroneko.dev/scuroneko/go-deepseek/pow"
) )
// CreatePowChallengeReq is the request body used to obtain a PoW challenge. // CreatePowChallengeReq is the request body used to obtain a PoW challenge.
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"errors" "errors"
"testing" "testing"
deeppow "dsp/pow" deeppow "git.scuroneko.dev/scuroneko/go-deepseek/pow"
) )
func TestPowChallengeToBase64PropagatesSolverError(t *testing.T) { func TestPowChallengeToBase64PropagatesSolverError(t *testing.T) {
+2 -10
View File
@@ -137,12 +137,6 @@ type TitleEvent struct {
Content string `json:"content"` 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. // PatchEvent describes a compact patch in the DeepSeek stream protocol.
// //
// Pointer fields distinguish: // Pointer fields distinguish:
@@ -475,9 +469,8 @@ func (s *StreamState) applyPatch(
case "response": case "response":
if operation != "BATCH" { if operation != "BATCH" {
log.Printf( log.Printf(
"unknown response operation: op=%q value=%s", "unknown response operation: op=%q",
operation, operation,
value,
) )
return nil return nil
@@ -489,10 +482,9 @@ func (s *StreamState) applyPatch(
default: default:
log.Printf( log.Printf(
"unknown patch: path=%q op=%q value=%s", "unknown patch: path=%q op=%q",
path, path,
operation, operation,
value,
) )
} }