Skip to content

Commit a2d580d

Browse files
bincooobincooo
bincooo
authored and
bincooo
committed
feat: cursor的基础实现
1 parent da9e135 commit a2d580d

File tree

16 files changed

+1449
-16
lines changed

16 files changed

+1449
-16
lines changed

go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,22 @@ go 1.23.3
55
require (
66
github.com/antonfisher/nested-logrus-formatter v1.3.1
77
github.com/bincooo/coze-api v1.0.2-0.20241204052445-8100a9ce45d0
8-
github.com/bincooo/emit.io v1.0.1-0.20241204040549-a9d53d3b6cfe
8+
github.com/bincooo/emit.io v1.0.1-0.20241206102606-d234e60afcc9
99
github.com/bincooo/you.com v0.0.0-20241111060258-85f9deb66109
1010
github.com/bogdanfinn/tls-client v1.7.7
1111
github.com/dlclark/regexp2 v1.11.4
1212
github.com/dop251/goja v0.0.0-20241024094426-79f3a7efcdbd
1313
github.com/eko/gocache/lib/v4 v4.1.6
1414
github.com/eko/gocache/store/go_cache/v4 v4.2.2
1515
github.com/gin-gonic/gin v1.10.0
16+
github.com/golang/protobuf v1.5.3
1617
github.com/google/uuid v1.6.0
1718
github.com/iocgo/sdk v0.0.0-20241203133330-43dcedf3291e
1819
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible
1920
github.com/patrickmn/go-cache v2.1.0+incompatible
2021
github.com/samber/go-gpt-3-encoder v0.3.1
2122
github.com/sirupsen/logrus v1.9.3
23+
google.golang.org/protobuf v1.34.1
2224
)
2325

2426
//github.com/iocgo/sdk v0.0.0-20241129021727-ca323c08f298 => ../sdk
@@ -47,7 +49,6 @@ require (
4749
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
4850
github.com/goccy/go-json v0.10.2 // indirect
4951
github.com/golang/mock v1.6.0 // indirect
50-
github.com/golang/protobuf v1.5.3 // indirect
5152
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
5253
github.com/hashicorp/errwrap v1.1.0 // indirect
5354
github.com/hashicorp/go-multierror v1.1.1 // indirect
@@ -101,7 +102,6 @@ require (
101102
golang.org/x/sys v0.25.0 // indirect
102103
golang.org/x/text v0.18.0 // indirect
103104
golang.org/x/tools v0.25.0 // indirect
104-
google.golang.org/protobuf v1.34.1 // indirect
105105
gopkg.in/ini.v1 v1.67.0 // indirect
106106
gopkg.in/yaml.v3 v3.0.1 // indirect
107107
)

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
5252
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
5353
github.com/bincooo/coze-api v1.0.2-0.20241204052445-8100a9ce45d0 h1:LHJy+OS1JyHNznFu3Rz1LtUsbOI0qxMVH2mtsvATllA=
5454
github.com/bincooo/coze-api v1.0.2-0.20241204052445-8100a9ce45d0/go.mod h1:qhaSqKdvR4T594sBMzp+TKZ+PcaeelAiBMvIE254b6g=
55-
github.com/bincooo/emit.io v1.0.1-0.20241204040549-a9d53d3b6cfe h1:gTf9Rs/DbqacE4naMkVxeAVSkGhLw971DChMoclp8RA=
56-
github.com/bincooo/emit.io v1.0.1-0.20241204040549-a9d53d3b6cfe/go.mod h1:OJbKJoZ6x6vSpCC+JNtfcaXo3ilpvQscWrcGEmtmrZI=
55+
github.com/bincooo/emit.io v1.0.1-0.20241206102606-d234e60afcc9 h1:xAmw70FCu96WcJpL+m2bZHel+fBGyPFzUTn8tBex7oM=
56+
github.com/bincooo/emit.io v1.0.1-0.20241206102606-d234e60afcc9/go.mod h1:OJbKJoZ6x6vSpCC+JNtfcaXo3ilpvQscWrcGEmtmrZI=
5757
github.com/bincooo/go-annotation v0.0.0-20241129022159-ea84d341fcda h1:bhkDbl8cz++1rXqroCNLYxs2Eu5ac/D3yDkxlUfHL1Y=
5858
github.com/bincooo/go-annotation v0.0.0-20241129022159-ea84d341fcda/go.mod h1:3ZV3/eOQJ/28O5jINzNi98+K+8WY/pOvDYidT7WWaFs=
5959
github.com/bincooo/you.com v0.0.0-20241111060258-85f9deb66109 h1:/3BaO9f/cvon9N/CzNbI/hlWuLCc1epAiHTsnXEd2aI=

relay/llm/blackbox/message.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ func waitMessage(r *http.Response, cancel func(str string) bool) (content string
3535
}
3636

3737
raw := string(char)
38+
logger.Debug("----- raw -----")
39+
logger.Debug(raw)
3840
content += raw
3941
}
4042
return content, nil

relay/llm/coze/message.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ func waitMessage(chatResponse chan string, cancel func(str string) bool) (conten
3333
}
3434

3535
message = strings.TrimPrefix(message, "text: ")
36+
logger.Debug("----- raw -----")
37+
logger.Debug(message)
3638
if len(message) > 0 {
3739
content += message
3840
if cancel != nil && cancel(content) {

relay/llm/cursor/adapter.go

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package cursor
2+
3+
import (
4+
"chatgpt-adapter/core/common"
5+
"chatgpt-adapter/core/common/toolcall"
6+
"chatgpt-adapter/core/common/vars"
7+
"chatgpt-adapter/core/gin/inter"
8+
"chatgpt-adapter/core/gin/model"
9+
"chatgpt-adapter/core/gin/response"
10+
"chatgpt-adapter/core/logger"
11+
"github.com/gin-gonic/gin"
12+
"github.com/iocgo/sdk/env"
13+
"strings"
14+
)
15+
16+
var (
17+
Model = "cursor"
18+
)
19+
20+
type api struct {
21+
inter.BaseAdapter
22+
23+
env *env.Environment
24+
holder *response.ContentHolder
25+
}
26+
27+
func (api *api) Match(ctx *gin.Context, model string) (ok bool, err error) {
28+
if Model+"/" != model[:7] {
29+
return
30+
}
31+
for _, mod := range []string{
32+
"claude-3-5-sonnet-20241022",
33+
"claude-3-opus",
34+
"claude-3.5-haiku",
35+
"claude-3.5-sonnet",
36+
"cursor-small",
37+
"gpt-3.5-turbo",
38+
"gpt-4",
39+
"gpt-4-turbo-2024-04-09",
40+
"gpt-4o",
41+
"gpt-4o-mini",
42+
"o1-mini",
43+
"o1-prevew",
44+
} {
45+
if model[7:] == mod {
46+
ok = true
47+
return
48+
}
49+
}
50+
return
51+
}
52+
53+
func (*api) Models() (slice []model.Model) {
54+
for _, mod := range []string{
55+
"claude-3-5-sonnet-20241022",
56+
"claude-3-opus",
57+
"claude-3.5-haiku",
58+
"claude-3.5-sonnet",
59+
"cursor-small",
60+
"gpt-3.5-turbo",
61+
"gpt-4",
62+
"gpt-4-turbo-2024-04-09",
63+
"gpt-4o",
64+
"gpt-4o-mini",
65+
"o1-mini",
66+
"o1-prevew",
67+
} {
68+
slice = append(slice, model.Model{
69+
Id: Model + "/" + mod,
70+
Object: "model",
71+
Created: 1686935002,
72+
By: Model + "-adapter",
73+
})
74+
}
75+
return
76+
}
77+
78+
func (api *api) HandleMessages(ctx *gin.Context, completion model.Completion) (messages []model.Keyv[interface{}], err error) {
79+
var (
80+
toolMessages = toolcall.ExtractToolMessages(&completion)
81+
)
82+
83+
if messages, err = api.holder.Handle(ctx, completion); err != nil {
84+
return
85+
}
86+
messages = append(messages, toolMessages...)
87+
return
88+
}
89+
90+
func (api *api) ToolChoice(ctx *gin.Context) (ok bool, err error) {
91+
var (
92+
cookie = ctx.GetString("token")
93+
proxied = api.env.GetString("server.proxied")
94+
completion = common.GetGinCompletion(ctx)
95+
echo = ctx.GetBool(vars.GinEcho)
96+
)
97+
98+
if echo {
99+
echoMessages(ctx, completion)
100+
return
101+
}
102+
103+
if toolChoice(ctx, proxied, cookie, completion) {
104+
ok = true
105+
}
106+
return
107+
}
108+
109+
func (api *api) Completion(ctx *gin.Context) (err error) {
110+
var (
111+
cookie = ctx.GetString("token")
112+
proxied = api.env.GetString("server.proxied")
113+
completion = common.GetGinCompletion(ctx)
114+
echo = ctx.GetBool(vars.GinEcho)
115+
)
116+
117+
if echo {
118+
echoMessages(ctx, completion)
119+
return
120+
}
121+
122+
if strings.Contains(cookie, "%3A%3A") {
123+
cookie = strings.Split(cookie, "%3A%3A")[1]
124+
}
125+
126+
buffer, err := convertRequest(completion)
127+
if err != nil {
128+
return
129+
}
130+
131+
r, err := fetch(ctx.Request.Context(), proxied, cookie, buffer)
132+
if err != nil {
133+
logger.Error(err)
134+
return
135+
}
136+
137+
content := waitResponse(ctx, r, completion.Stream)
138+
if content == "" && response.NotResponse(ctx) {
139+
response.Error(ctx, -1, "EMPTY RESPONSE")
140+
}
141+
return
142+
}

relay/llm/cursor/ctor.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package cursor
2+
3+
import (
4+
"chatgpt-adapter/core/gin/inter"
5+
"chatgpt-adapter/core/gin/response"
6+
"github.com/iocgo/sdk/env"
7+
8+
_ "github.com/iocgo/sdk"
9+
)
10+
11+
// @Inject(name = "cursor-adapter")
12+
func New(env *env.Environment, holder *response.ContentHolder) inter.Adapter {
13+
return &api{env: env, holder: holder}
14+
}

relay/llm/cursor/fetch.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package cursor
2+
3+
import (
4+
"context"
5+
"encoding/hex"
6+
"fmt"
7+
"github.com/iocgo/sdk/stream"
8+
"math/rand"
9+
"net/http"
10+
11+
"chatgpt-adapter/core/common"
12+
"chatgpt-adapter/core/gin/model"
13+
"github.com/bincooo/emit.io"
14+
"github.com/golang/protobuf/proto"
15+
"github.com/google/uuid"
16+
17+
. "chatgpt-adapter/relay/llm/cursor/proto"
18+
)
19+
20+
func fetch(ctx context.Context, proxied string, cookie string, buffer []byte) (response *http.Response, err error) {
21+
checksum := fmt.Sprintf("zo%s%s/%s", getRandId(6, "max"), getRandId(64, "max"), getRandId(64, "max"))
22+
response, err = emit.ClientBuilder(common.HTTPClient).
23+
Context(ctx).
24+
Proxies(proxied).
25+
POST("https://api2.cursor.sh/aiserver.v1.AiService/StreamChat").
26+
Header("authorization", "Bearer "+cookie).
27+
Header("content-type", "application/connect+proto").
28+
Header("connect-accept-encoding", "gzip,br").
29+
Header("connect-protocol-version", "1").
30+
Header("user-agent", "connect-es/1.4.0").
31+
Header("x-amzn-trace-id", "Root="+uuid.NewString()).
32+
Header("x-cursor-checksum", checksum).
33+
Header("x-cursor-client-version", "0.42.3").
34+
Header("x-cursor-timezone", "Asia/Shanghai").
35+
Header("x-ghost-mode", "false").
36+
Header("x-request-id", uuid.New().String()).
37+
Header("host", "api2.cursor.sh").
38+
Bytes(buffer).
39+
DoC(emit.Status(http.StatusOK), emit.IsPROTO)
40+
return
41+
}
42+
43+
func convertRequest(completion model.Completion) (buffer []byte, err error) {
44+
messages := stream.Map(stream.OfSlice(completion.Messages), func(message model.Keyv[interface{}]) *ChatMessage_UserMessage {
45+
return &ChatMessage_UserMessage{
46+
MessageId: uuid.NewString(),
47+
Role: elseOf[int32](message.Is("role", "user"), 1, 2),
48+
Content: message.GetString("content"),
49+
}
50+
}).ToSlice()
51+
message := &ChatMessage{
52+
Messages: messages,
53+
Instructions: &ChatMessage_Instructions{
54+
Instruction: "",
55+
},
56+
ProjectPath: "/path/to/project",
57+
Model: &ChatMessage_Model{
58+
Name: completion.Model[7:],
59+
Empty: "",
60+
},
61+
Summary: "",
62+
RequestId: uuid.NewString(),
63+
ConversationId: uuid.NewString(),
64+
}
65+
66+
protoBytes, err := proto.Marshal(message)
67+
if err != nil {
68+
return
69+
}
70+
71+
bufLen := fmt.Sprintf("%010x", len(protoBytes))
72+
buffer, err = hex.DecodeString(bufLen + hex.EncodeToString(protoBytes))
73+
return
74+
}
75+
76+
func getRandId(size int, dictType string) string {
77+
customDict := ""
78+
switch dictType {
79+
case "alphabet":
80+
customDict = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
81+
case "max":
82+
customDict = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-"
83+
default:
84+
customDict = "0123456789"
85+
}
86+
87+
buf := make([]byte, 0)
88+
for range size {
89+
buf = append(buf, customDict[rand.Intn(len(customDict))])
90+
}
91+
return string(buf)
92+
}
93+
94+
func elseOf[T any](condition bool, a1, a2 T) T {
95+
if condition {
96+
return a1
97+
}
98+
return a2
99+
}

0 commit comments

Comments
 (0)