mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-10-07 08:09:48 +08:00
Implement the full Devin/Cognition Connect-RPC provider support across all CPA endpoints (/v1/chat/completions, /v1/messages, /v1/responses), complete with binary protobuf wire framing, streaming tools/arguments delta handling, thinking/reasoning replay, and CLI OAuth authentication. Key highlights: - Wire Protocol & Streaming: * Implemented Connect-RPC uncompressed 5-byte framing (0x00 + 4-byte length + protobuf) for ApiServerService/GetChatMessage. * Implemented Devin protobuf encoder/decoder in internal/runtime/executor/helps/devin_wire.go, including ClientMetadata, prompts, tools, completion_config, and multimodal image handling (Prompt Field #10). * Stream frame consumption via interactions protocol, correctly mapping arguments_delta and tracking multiple sequential tool calls (currentToolCallActive). * Streaming thought summary and sealed.v1 signature deltas targeting the thinking step. - Model Registration & Thinking Clamping: * Registered static fallback models in model_definitions.go (swe-2, claude-fable-5-1, gpt-6-astra, swe-1-7-lightning, glm-5-2, glm-5-3). * Configured ThinkingSupport with discrete levels per model family. * Implemented CPA-standard nearest-neighbor clamping for thinking levels (minimal/low -> medium, xhigh -> max for swe-2). * Mapped thinking effort to Devin upstream model UID (e.g. swe-2-medium, swe-2-high, swe-2-max). - Sensitive Words & System Prompt Sanitization: * Added devin.sensitive-words configuration in internal/config/config_types.go and config.go, matching Antigravity conventions. * Supported zero-width space (\u200b) obfuscation in prompts, tools, and system instructions via SensitiveWordMatcher. * Stripped Claude Code billing headers (x-anthropic-billing-header:) and CLI identity signatures from system instructions and tool descriptions to avoid upstream content filter rejections. - Signature Compatibility: * Added SignatureProviderSWE = "swe" recognizing sealed.v1.* reasoning signatures in internal/signature/provider_compatibility.go. * Propagated reasoning.encrypted_content on Responses API and thinking.signature on Messages API. - Authentication: * Implemented Devin PKCE OAuth flow with loopback callback server and headless manual token/code paste (--no-browser). * Registered Devin authenticator in SDK and CLI (-devin-login flag). * Integrated with management OAuth session endpoints and credentials manager.
264 lines
7.2 KiB
Go
264 lines
7.2 KiB
Go
package helps
|
|
|
|
import (
|
|
"bytes"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestConnectEnvelopeFraming(t *testing.T) {
|
|
payload := []byte("hello devin connect-rpc")
|
|
envelope := WrapConnectEnvelope(payload)
|
|
|
|
if len(envelope) != 5+len(payload) {
|
|
t.Fatalf("expected envelope len %d, got %d", 5+len(payload), len(envelope))
|
|
}
|
|
if envelope[0] != ConnectFlagData {
|
|
t.Fatalf("expected flag 0x00, got 0x%02x", envelope[0])
|
|
}
|
|
|
|
r := bytes.NewReader(envelope)
|
|
flag, readPayload, err := ReadConnectFrame(r)
|
|
if err != nil {
|
|
t.Fatalf("ReadConnectFrame failed: %v", err)
|
|
}
|
|
if flag != ConnectFlagData {
|
|
t.Errorf("flag = 0x%02x, want 0x00", flag)
|
|
}
|
|
if !bytes.Equal(readPayload, payload) {
|
|
t.Errorf("payload = %q, want %q", string(readPayload), string(payload))
|
|
}
|
|
}
|
|
|
|
func TestGenerateDevinDeviceFingerprint(t *testing.T) {
|
|
fp1 := GenerateDevinDeviceFingerprint("seed-1")
|
|
if len(fp1) != DevinFingerprintHexLen {
|
|
t.Fatalf("fp1 len = %d, want %d", len(fp1), DevinFingerprintHexLen)
|
|
}
|
|
// Check hex characters
|
|
for _, c := range fp1 {
|
|
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f')) {
|
|
t.Fatalf("invalid hex char in fingerprint: %c", c)
|
|
}
|
|
}
|
|
|
|
// Deterministic seed produces deterministic fingerprint
|
|
fp2 := GenerateDevinDeviceFingerprint("seed-1")
|
|
if fp1 != fp2 {
|
|
t.Fatalf("fingerprints for same seed do not match: %s != %s", fp1, fp2)
|
|
}
|
|
}
|
|
|
|
func TestBuildDevinGetChatMessageRequest(t *testing.T) {
|
|
prompts := []DevinPrompt{
|
|
{
|
|
MessageID: "msg-1",
|
|
Source: 1,
|
|
Content: "hello",
|
|
},
|
|
{
|
|
MessageID: "msg-2",
|
|
Source: 2,
|
|
Content: "hi there",
|
|
Thinking: "thinking step",
|
|
Signature: []byte("sealed.v1.test"),
|
|
},
|
|
{
|
|
MessageID: "msg-3",
|
|
Source: 4,
|
|
Content: `{"result":"ok"}`,
|
|
ToolCallID: "call-1",
|
|
},
|
|
}
|
|
tools := []DevinTool{
|
|
{
|
|
Name: "get_weather",
|
|
Description: "lookup weather",
|
|
Parameters: []byte(`{"type":"object"}`),
|
|
},
|
|
}
|
|
|
|
temp := 0.7
|
|
req := BuildDevinGetChatMessageRequest(
|
|
"token-123",
|
|
"device-seed-1",
|
|
"swe-2-high",
|
|
"you are a helpful assistant",
|
|
prompts,
|
|
tools,
|
|
&temp,
|
|
4000,
|
|
"session-1",
|
|
"cascade-1",
|
|
nil,
|
|
)
|
|
|
|
if len(req) == 0 {
|
|
t.Fatal("encoded request is empty")
|
|
}
|
|
|
|
// Envelope check
|
|
framed := WrapConnectEnvelope(req)
|
|
flag, readPayload, err := ReadConnectFrame(bytes.NewReader(framed))
|
|
if err != nil {
|
|
t.Fatalf("ReadConnectFrame failed: %v", err)
|
|
}
|
|
if flag != ConnectFlagData || len(readPayload) != len(req) {
|
|
t.Fatalf("framed payload length mismatch")
|
|
}
|
|
}
|
|
|
|
func TestSanitizeDevinSystemPrompt_AndSensitiveWords(t *testing.T) {
|
|
matcher := BuildSensitiveWordMatcher([]string{"API", "proxy"})
|
|
rawPrompt := "x-anthropic-billing-header: cc_version=2.1.260;\nYou are Claude Code, Anthropic's official CLI for Claude.\nHelp the project with API and proxy."
|
|
sanitized := SanitizeDevinSystemPrompt(rawPrompt, matcher)
|
|
|
|
if strings.Contains(sanitized, "x-anthropic-billing-header") {
|
|
t.Errorf("sanitized prompt still contains billing header: %s", sanitized)
|
|
}
|
|
if strings.Contains(sanitized, "You are Claude Code") {
|
|
t.Errorf("sanitized prompt still contains Claude Code identity: %s", sanitized)
|
|
}
|
|
if strings.Contains(sanitized, "API") && !strings.Contains(sanitized, zeroWidthSpace) {
|
|
t.Errorf("API was not obfuscated with zero-width space")
|
|
}
|
|
}
|
|
|
|
func TestBuildDevinGetChatMessageRequest_WithImages(t *testing.T) {
|
|
prompts := []DevinPrompt{
|
|
{
|
|
MessageID: "msg-img-1",
|
|
Source: 1,
|
|
Content: "[Image 1: pasted_image_1.png]\n\nocr this image",
|
|
Images: []DevinImage{
|
|
{
|
|
Base64Data: "iVBORw0KGgoAAAANSUhEUgAA...",
|
|
MimeType: "image/png",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
temp := 0.0
|
|
req := BuildDevinGetChatMessageRequest(
|
|
"token-123",
|
|
"device-seed-1",
|
|
"swe-2-max",
|
|
"assistant instructions",
|
|
prompts,
|
|
nil,
|
|
&temp,
|
|
4000,
|
|
"session-1",
|
|
"cascade-1",
|
|
nil,
|
|
)
|
|
|
|
if len(req) == 0 {
|
|
t.Fatal("encoded request with images is empty")
|
|
}
|
|
|
|
// Verify that the payload contains the image base64 data and mime type
|
|
if !bytes.Contains(req, []byte("iVBORw0KGgoAAAANSUhEUgAA...")) {
|
|
t.Error("encoded payload missing image base64 data")
|
|
}
|
|
if !bytes.Contains(req, []byte("image/png")) {
|
|
t.Error("encoded payload missing image mime type")
|
|
}
|
|
}
|
|
|
|
func TestParseDevinFrame(t *testing.T) {
|
|
// Synthesize a response frame containing:
|
|
// #1 output_id, #3 delta_text, #9 delta_thinking, #10 delta_signature, #21 delta_signature_type
|
|
var payload []byte
|
|
payload = appendFieldBytes(payload, 1, []byte("bot-uuid-123"))
|
|
payload = appendFieldBytes(payload, 3, []byte("Hello world"))
|
|
payload = appendFieldBytes(payload, 9, []byte("Let me think..."))
|
|
payload = appendFieldBytes(payload, 10, []byte("CAQS-signature-bytes"))
|
|
payload = appendFieldBytes(payload, 21, []byte("anthropic"))
|
|
|
|
res, err := ParseDevinFrame(payload)
|
|
if err != nil {
|
|
t.Fatalf("ParseDevinFrame failed: %v", err)
|
|
}
|
|
|
|
if res.OutputID != "bot-uuid-123" {
|
|
t.Errorf("OutputID = %q, want bot-uuid-123", res.OutputID)
|
|
}
|
|
if res.ContentText != "Hello world" {
|
|
t.Errorf("ContentText = %q, want 'Hello world'", res.ContentText)
|
|
}
|
|
if res.ThinkingText != "Let me think..." {
|
|
t.Errorf("ThinkingText = %q, want 'Let me think...'", res.ThinkingText)
|
|
}
|
|
if string(res.DeltaSignature) != "CAQS-signature-bytes" {
|
|
t.Errorf("DeltaSignature = %q, want 'CAQS-signature-bytes'", string(res.DeltaSignature))
|
|
}
|
|
if res.DeltaSignatureType != "anthropic" {
|
|
t.Errorf("DeltaSignatureType = %q, want 'anthropic'", res.DeltaSignatureType)
|
|
}
|
|
}
|
|
|
|
func TestParseDevinTrailerError(t *testing.T) {
|
|
quotaJSON := []byte(`{"error":{"code":"failed_precondition","message":"User monthly ACU quota exhausted (trace ID: 12345)"}}`)
|
|
code, err := ParseDevinTrailerError(quotaJSON)
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
if code != 429 {
|
|
t.Errorf("status code = %d, want 429 for quota error", code)
|
|
}
|
|
|
|
configJSON := []byte(`{"error":{"code":"failed_precondition","message":"Client version 3000.1.0 is no longer supported"}}`)
|
|
code, err = ParseDevinTrailerError(configJSON)
|
|
if code != 400 {
|
|
t.Errorf("status code = %d, want 400 for config error", code)
|
|
}
|
|
|
|
unauthJSON := []byte(`{"error":{"code":"unauthenticated","message":"Invalid or expired session token"}}`)
|
|
code, err = ParseDevinTrailerError(unauthJSON)
|
|
if code != 401 {
|
|
t.Errorf("status code = %d, want 401", code)
|
|
}
|
|
}
|
|
|
|
func TestUTF8SplitBuffer(t *testing.T) {
|
|
buf := &UTF8SplitBuffer{}
|
|
// "你好" in UTF-8: \xe4\xbd\xa0 \xe5\xa5\xbd (3 bytes each)
|
|
chunk1 := []byte{0xe4, 0xbd} // first 2 bytes of 你
|
|
chunk2 := []byte{0xa0, 0xe5, 0xa5} // last 1 byte of 你, first 2 bytes of 好
|
|
chunk3 := []byte{0xbd} // last 1 byte of 好
|
|
|
|
s1 := buf.Feed(chunk1)
|
|
if s1 != "" {
|
|
t.Errorf("expected empty string from split chunk1, got %q", s1)
|
|
}
|
|
|
|
s2 := buf.Feed(chunk2)
|
|
if s2 != "你" {
|
|
t.Errorf("expected '你' from chunk2, got %q", s2)
|
|
}
|
|
|
|
s3 := buf.Feed(chunk3)
|
|
if s3 != "好" {
|
|
t.Errorf("expected '好' from chunk3, got %q", s3)
|
|
}
|
|
}
|
|
|
|
func appendFieldBytes(dst []byte, fieldNum int, val []byte) []byte {
|
|
tag := uint64(fieldNum<<3 | 2)
|
|
dst = appendVarint(dst, tag)
|
|
dst = appendVarint(dst, uint64(len(val)))
|
|
dst = append(dst, val...)
|
|
return dst
|
|
}
|
|
|
|
func appendVarint(dst []byte, v uint64) []byte {
|
|
for v >= 0x80 {
|
|
dst = append(dst, byte(v)|0x80)
|
|
v >>= 7
|
|
}
|
|
dst = append(dst, byte(v))
|
|
return dst
|
|
}
|