mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-10-09 09:12:33 +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.
191 lines
5.0 KiB
Go
191 lines
5.0 KiB
Go
package helps
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"
|
|
)
|
|
|
|
// Family effort configurations extracted from GetCliModelConfigs (215 models).
|
|
var (
|
|
swe2Efforts = []string{"medium", "high", "max"}
|
|
fable51Efforts = []string{"low", "medium", "high", "xhigh", "max"}
|
|
astraEfforts = []string{"low", "medium", "high", "xhigh", "max"}
|
|
glm53Efforts = []string{"low", "high", "max"}
|
|
)
|
|
|
|
// knownDevinSuffixes lists recognized model uid suffixes.
|
|
var knownDevinSuffixes = []string{
|
|
"-low",
|
|
"-medium",
|
|
"-high",
|
|
"-xhigh",
|
|
"-max",
|
|
"-fast",
|
|
"-lightning",
|
|
"-priority",
|
|
"-low-priority",
|
|
"-medium-priority",
|
|
"-high-priority",
|
|
"-xhigh-priority",
|
|
"-max-priority",
|
|
}
|
|
|
|
// HasDevinEffortSuffix reports whether the model name already ends with a known Devin effort suffix.
|
|
func HasDevinEffortSuffix(model string) bool {
|
|
lower := strings.ToLower(strings.TrimSpace(model))
|
|
for _, s := range knownDevinSuffixes {
|
|
if strings.HasSuffix(lower, s) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// NormalizeThinkingLevel converts numeric budgets or loose effort strings to canonical Devin efforts.
|
|
func NormalizeThinkingLevel(level string, budgetTokens int) string {
|
|
normalized := strings.ToLower(strings.TrimSpace(level))
|
|
switch normalized {
|
|
case "minimal", "low", "medium", "high", "xhigh", "max":
|
|
return normalized
|
|
case "none", "off", "disabled":
|
|
return "none"
|
|
case "auto", "adaptive":
|
|
return "high"
|
|
}
|
|
|
|
if budgetTokens > 0 {
|
|
switch {
|
|
case budgetTokens <= 4096:
|
|
return "low"
|
|
case budgetTokens <= 16384:
|
|
return "medium"
|
|
case budgetTokens <= 32768:
|
|
return "high"
|
|
default:
|
|
return "max"
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
// ResolveDevinChatModelUID resolves a model identifier into a valid upstream Devin chat_model_uid.
|
|
// It prioritizes already-formed UIDs, strips CPA suffixes, resolves body effort/budgets,
|
|
// and clamps efforts to the specific family whitelist.
|
|
func ResolveDevinChatModelUID(rawModel string, thinkingLevel string, budgetTokens int) string {
|
|
model := strings.TrimSpace(rawModel)
|
|
if model == "" {
|
|
return "swe-2-high"
|
|
}
|
|
|
|
// 1. If already ends with an exact Devin effort suffix, use directly
|
|
if HasDevinEffortSuffix(model) {
|
|
return model
|
|
}
|
|
|
|
// 2. Strip CPA colon or parenthesis suffix (suffix overrides body per CPA convention)
|
|
parsedSuffix := thinking.ParseSuffix(model)
|
|
baseModel := strings.TrimSpace(parsedSuffix.ModelName)
|
|
if parsedSuffix.HasSuffix {
|
|
thinkingLevel = parsedSuffix.RawSuffix
|
|
}
|
|
|
|
// 3. Normalize requested effort
|
|
effort := NormalizeThinkingLevel(thinkingLevel, budgetTokens)
|
|
|
|
// 4. Family-specific mapping and clamping
|
|
lowerBase := strings.ToLower(baseModel)
|
|
switch {
|
|
case strings.Contains(lowerBase, "swe-2"):
|
|
clamped := clampEffort(effort, swe2Efforts, "high")
|
|
return "swe-2-" + clamped
|
|
|
|
case strings.Contains(lowerBase, "fable-5-1") || strings.Contains(lowerBase, "fable-5.1"):
|
|
clamped := clampEffort(effort, fable51Efforts, "medium")
|
|
return "claude-fable-5-1-" + clamped
|
|
|
|
case strings.Contains(lowerBase, "fable-5") || strings.Contains(lowerBase, "5-fable"):
|
|
clamped := clampEffort(effort, fable51Efforts, "medium")
|
|
return "claude-5-fable-" + clamped
|
|
|
|
case strings.Contains(lowerBase, "astra"):
|
|
clamped := clampEffort(effort, astraEfforts, "medium")
|
|
return "gpt-6-astra-" + clamped
|
|
|
|
case strings.Contains(lowerBase, "glm-5-2") || strings.Contains(lowerBase, "glm-5.2"):
|
|
if effort == "max" {
|
|
return "glm-5-2-max"
|
|
}
|
|
if effort == "none" {
|
|
return "glm-5-2-none"
|
|
}
|
|
return "glm-5-2"
|
|
|
|
case strings.Contains(lowerBase, "glm-5-3") || strings.Contains(lowerBase, "glm-5.3"):
|
|
clamped := clampEffort(effort, glm53Efforts, "high")
|
|
return "glm-5-3-" + clamped
|
|
|
|
default:
|
|
// Default generic fallback: append effort if present, else return base
|
|
if effort != "" && effort != "none" {
|
|
return baseModel + "-" + effort
|
|
}
|
|
return baseModel
|
|
}
|
|
}
|
|
|
|
var devinStandardLevelOrder = []string{"minimal", "low", "medium", "high", "xhigh", "max"}
|
|
|
|
func devinLevelIndex(level string) int {
|
|
lower := strings.ToLower(strings.TrimSpace(level))
|
|
for i, l := range devinStandardLevelOrder {
|
|
if l == lower {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func clampEffort(requested string, allowed []string, defaultEffort string) string {
|
|
if requested == "" || requested == "none" {
|
|
return defaultEffort
|
|
}
|
|
reqLower := strings.ToLower(strings.TrimSpace(requested))
|
|
for _, a := range allowed {
|
|
if reqLower == strings.ToLower(strings.TrimSpace(a)) {
|
|
return a
|
|
}
|
|
}
|
|
|
|
reqIdx := devinLevelIndex(reqLower)
|
|
if reqIdx == -1 {
|
|
return defaultEffort
|
|
}
|
|
|
|
bestMatch := defaultEffort
|
|
bestDist := 999
|
|
bestIdx := -1
|
|
for _, a := range allowed {
|
|
aIdx := devinLevelIndex(a)
|
|
if aIdx == -1 {
|
|
continue
|
|
}
|
|
dist := reqIdx - aIdx
|
|
if dist < 0 {
|
|
dist = -dist
|
|
}
|
|
if dist < bestDist {
|
|
bestDist = dist
|
|
bestMatch = a
|
|
bestIdx = aIdx
|
|
} else if dist == bestDist && aIdx > bestIdx {
|
|
// On tie, prefer the higher effort (e.g. medium -> high for glm-5-3; xhigh -> max for swe-2)
|
|
bestMatch = a
|
|
bestIdx = aIdx
|
|
}
|
|
}
|
|
|
|
return bestMatch
|
|
}
|