Files
CLIProxyAPI/internal/runtime/executor/helps/devin_models.go
sususu f94752762b feat(devin): add Devin/Cognition provider integration and CLI OAuth
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.
2026-09-13 23:34:43 +08:00

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
}