mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-08-17 19:47:10 +08:00
v0.3.8
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
legacyruntime "cursor/internal/runtime"
|
||||
)
|
||||
|
||||
func (store *Store) LegacyRuntimeSnapshot(ctx context.Context) (legacyruntime.RuntimeConfigSnapshot, error) {
|
||||
cfg, err := store.Load(ctx)
|
||||
if err != nil {
|
||||
return legacyruntime.RuntimeConfigSnapshot{}, err
|
||||
}
|
||||
|
||||
adapters := make([]legacyruntime.ModelAdapterConfig, 0, len(cfg.ModelAdapters))
|
||||
for _, item := range cfg.ModelAdapters {
|
||||
adapters = append(adapters, legacyruntime.ModelAdapterConfig{
|
||||
ID: item.ID,
|
||||
DisplayName: item.DisplayName,
|
||||
Type: item.Type,
|
||||
BaseURL: item.BaseURL,
|
||||
APIKey: item.APIKey,
|
||||
TooltipData: item.TooltipData,
|
||||
ModelID: item.ModelID,
|
||||
ReasoningEffort: item.ReasoningEffort,
|
||||
OpenAIEndpoint: item.OpenAIEndpoint,
|
||||
OpenAIExtraParamsEnabled: item.OpenAIExtraParamsEnabled,
|
||||
OpenAIExtraParamsJSON: item.OpenAIExtraParamsJSON,
|
||||
CustomHeadersEnabled: item.CustomHeadersEnabled,
|
||||
CustomHeadersJSON: item.CustomHeadersJSON,
|
||||
AnthropicExtraParamsEnabled: item.AnthropicExtraParamsEnabled,
|
||||
AnthropicExtraParamsJSON: item.AnthropicExtraParamsJSON,
|
||||
ContextWindowTokens: item.ContextWindowTokens,
|
||||
MaxCompletionTokens: item.MaxCompletionTokens,
|
||||
AnthropicMaxTokens: item.AnthropicMaxTokens,
|
||||
AnthropicThinkingEffort: item.AnthropicThinkingEffort,
|
||||
ThinkingBudgetTokens: item.ThinkingBudgetTokens,
|
||||
})
|
||||
}
|
||||
|
||||
return legacyruntime.RuntimeConfigSnapshot{
|
||||
ObservabilityLogEnabled: cfg.Log,
|
||||
ProviderStreamIdleTimeout: cfg.ProviderStreamIdleTimeout,
|
||||
ModelAdapters: adapters,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
legacyruntime "cursor/internal/runtime"
|
||||
)
|
||||
|
||||
const configHotReloadMinInterval = 500 * time.Millisecond
|
||||
|
||||
type Manager struct {
|
||||
store *Store
|
||||
current atomic.Pointer[Config]
|
||||
listenersMu sync.RWMutex
|
||||
listeners []func(Config)
|
||||
reloadMu sync.Mutex
|
||||
snapshot fileSnapshot
|
||||
lastReload time.Time
|
||||
reloadError string
|
||||
}
|
||||
|
||||
func NewManager(ctx context.Context, store *Store) (*Manager, error) {
|
||||
if store == nil {
|
||||
return nil, fmt.Errorf("config store is required")
|
||||
}
|
||||
cfg, err := store.Load(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
manager := &Manager{
|
||||
store: store,
|
||||
snapshot: store.snapshot(),
|
||||
}
|
||||
manager.setCurrent(cfg)
|
||||
return manager, nil
|
||||
}
|
||||
|
||||
func (manager *Manager) Current() Config {
|
||||
if manager == nil {
|
||||
return DefaultConfig()
|
||||
}
|
||||
manager.reloadIfChanged(context.Background())
|
||||
return manager.currentConfig()
|
||||
}
|
||||
|
||||
func (manager *Manager) currentConfig() Config {
|
||||
if manager == nil {
|
||||
return DefaultConfig()
|
||||
}
|
||||
if current := manager.current.Load(); current != nil {
|
||||
return *current
|
||||
}
|
||||
return DefaultConfig()
|
||||
}
|
||||
|
||||
func (manager *Manager) Load(ctx context.Context) (Config, error) {
|
||||
if manager == nil {
|
||||
return DefaultConfig(), nil
|
||||
}
|
||||
manager.reloadIfChanged(ctx)
|
||||
return manager.currentConfig(), nil
|
||||
}
|
||||
|
||||
func (manager *Manager) Save(ctx context.Context, cfg Config) (Config, error) {
|
||||
if manager == nil || manager.store == nil {
|
||||
return Config{}, fmt.Errorf("config manager is not initialized")
|
||||
}
|
||||
normalized, err := manager.store.Save(ctx, cfg)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
manager.setCurrent(normalized)
|
||||
manager.reloadMu.Lock()
|
||||
manager.snapshot = manager.store.snapshot()
|
||||
manager.lastReload = time.Now()
|
||||
manager.reloadError = ""
|
||||
manager.reloadMu.Unlock()
|
||||
manager.notify(normalized)
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
func (manager *Manager) LastAgentModelHash() string {
|
||||
if manager == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(manager.Current().LastAgentModelHash)
|
||||
}
|
||||
|
||||
func (manager *Manager) SaveLastAgentModelHash(ctx context.Context, value string) error {
|
||||
if manager == nil {
|
||||
return fmt.Errorf("config manager is not initialized")
|
||||
}
|
||||
normalizedValue := strings.TrimSpace(value)
|
||||
current := manager.Current()
|
||||
if strings.TrimSpace(current.LastAgentModelHash) == normalizedValue {
|
||||
return nil
|
||||
}
|
||||
current.LastAgentModelHash = normalizedValue
|
||||
_, err := manager.Save(ctx, current)
|
||||
return err
|
||||
}
|
||||
|
||||
func (manager *Manager) ProviderStreamIdleTimeout(ctx context.Context) time.Duration {
|
||||
if manager == nil {
|
||||
return time.Duration(DefaultProviderStreamIdleTimeoutSeconds) * time.Second
|
||||
}
|
||||
manager.reloadIfChanged(ctx)
|
||||
seconds := normalizeProviderStreamIdleTimeout(manager.currentConfig().ProviderStreamIdleTimeout)
|
||||
return time.Duration(seconds) * time.Second
|
||||
}
|
||||
|
||||
func (manager *Manager) IsObservabilityLogEnabled(ctx context.Context) bool {
|
||||
if manager == nil {
|
||||
return false
|
||||
}
|
||||
manager.reloadIfChanged(ctx)
|
||||
return manager.currentConfig().Log
|
||||
}
|
||||
|
||||
func (manager *Manager) Subscribe(listener func(Config)) func() {
|
||||
if manager == nil || listener == nil {
|
||||
return func() {}
|
||||
}
|
||||
manager.listenersMu.Lock()
|
||||
manager.listeners = append(manager.listeners, listener)
|
||||
index := len(manager.listeners) - 1
|
||||
manager.listenersMu.Unlock()
|
||||
return func() {
|
||||
manager.listenersMu.Lock()
|
||||
defer manager.listenersMu.Unlock()
|
||||
if index < 0 || index >= len(manager.listeners) {
|
||||
return
|
||||
}
|
||||
manager.listeners[index] = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *Manager) LegacyRuntimeSnapshot(_ context.Context) (legacyruntime.RuntimeConfigSnapshot, error) {
|
||||
cfg := manager.Current()
|
||||
adapters := make([]legacyruntime.ModelAdapterConfig, 0, len(cfg.ModelAdapters))
|
||||
for _, item := range cfg.ModelAdapters {
|
||||
adapters = append(adapters, legacyruntime.ModelAdapterConfig{
|
||||
ID: item.ID,
|
||||
DisplayName: item.DisplayName,
|
||||
Type: item.Type,
|
||||
BaseURL: item.BaseURL,
|
||||
APIKey: item.APIKey,
|
||||
TooltipData: item.TooltipData,
|
||||
ModelID: item.ModelID,
|
||||
ReasoningEffort: item.ReasoningEffort,
|
||||
OpenAIEndpoint: item.OpenAIEndpoint,
|
||||
OpenAIExtraParamsEnabled: item.OpenAIExtraParamsEnabled,
|
||||
OpenAIExtraParamsJSON: item.OpenAIExtraParamsJSON,
|
||||
ContextWindowTokens: item.ContextWindowTokens,
|
||||
MaxCompletionTokens: item.MaxCompletionTokens,
|
||||
AnthropicMaxTokens: item.AnthropicMaxTokens,
|
||||
AnthropicThinkingEffort: item.AnthropicThinkingEffort,
|
||||
ThinkingBudgetTokens: item.ThinkingBudgetTokens,
|
||||
})
|
||||
}
|
||||
return legacyruntime.RuntimeConfigSnapshot{
|
||||
ObservabilityLogEnabled: cfg.Log,
|
||||
ProviderStreamIdleTimeout: cfg.ProviderStreamIdleTimeout,
|
||||
ModelAdapters: adapters,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (manager *Manager) RouteMode(hasUpstreamURL bool) string {
|
||||
if !hasUpstreamURL {
|
||||
return DefaultRoutingMode
|
||||
}
|
||||
if manager == nil {
|
||||
return DefaultRoutingMode
|
||||
}
|
||||
mode := normalizeRoutingMode(manager.Current().Routing.Mode)
|
||||
if mode == "" {
|
||||
return DefaultRoutingMode
|
||||
}
|
||||
return mode
|
||||
}
|
||||
|
||||
func (manager *Manager) setCurrent(cfg Config) {
|
||||
next := cfg
|
||||
manager.current.Store(&next)
|
||||
}
|
||||
|
||||
func (manager *Manager) reloadIfChanged(ctx context.Context) {
|
||||
if manager == nil || manager.store == nil {
|
||||
return
|
||||
}
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
now := time.Now()
|
||||
manager.reloadMu.Lock()
|
||||
if !manager.lastReload.IsZero() && now.Sub(manager.lastReload) < configHotReloadMinInterval {
|
||||
manager.reloadMu.Unlock()
|
||||
return
|
||||
}
|
||||
manager.lastReload = now
|
||||
nextSnapshot := manager.store.snapshot()
|
||||
if nextSnapshot == manager.snapshot {
|
||||
manager.reloadMu.Unlock()
|
||||
return
|
||||
}
|
||||
cfg, err := manager.store.Load(ctx)
|
||||
if err != nil {
|
||||
errText := err.Error()
|
||||
if errText != manager.reloadError {
|
||||
log.Printf("config hot reload skipped path=%s error=%v", manager.store.Path(), err)
|
||||
manager.reloadError = errText
|
||||
}
|
||||
manager.reloadMu.Unlock()
|
||||
return
|
||||
}
|
||||
manager.snapshot = nextSnapshot
|
||||
manager.reloadError = ""
|
||||
manager.setCurrent(cfg)
|
||||
manager.reloadMu.Unlock()
|
||||
manager.notify(cfg)
|
||||
}
|
||||
|
||||
func (manager *Manager) notify(cfg Config) {
|
||||
manager.listenersMu.RLock()
|
||||
listeners := append([]func(Config){}, manager.listeners...)
|
||||
manager.listenersMu.RUnlock()
|
||||
for _, listener := range listeners {
|
||||
if listener != nil {
|
||||
listener(cfg)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"cursor/internal/modelchannel"
|
||||
legacyruntime "cursor/internal/runtime"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultChannelTimeoutMS = int((2 * 60 * 60) * 1000)
|
||||
defaultChannelContextWindowTokens = 200_000
|
||||
defaultChannelMaxTokens = 65_536
|
||||
defaultChannelThinkingBudget = 4_096
|
||||
defaultChannelAnthropicEffort = "xhigh"
|
||||
)
|
||||
|
||||
func (manager *Manager) SelectChannelForModel(_ context.Context, modelID string) (*legacyruntime.ResolvedChannel, error) {
|
||||
if manager == nil {
|
||||
return nil, legacyruntime.ErrChannelNotAvailable
|
||||
}
|
||||
adapters, err := NormalizeModelAdapterConfigs(manager.Current().ModelAdapters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resolveModelAdapterChannel(adapters, modelID)
|
||||
}
|
||||
|
||||
func resolveModelAdapterChannel(adapters []ModelAdapterConfig, requestedModel string) (*legacyruntime.ResolvedChannel, error) {
|
||||
matchIndex, ok := modelchannel.ResolveAdapterIndex(
|
||||
adapters,
|
||||
requestedModel,
|
||||
func(adapter ModelAdapterConfig) string { return adapter.ID },
|
||||
func(adapter ModelAdapterConfig) string { return adapter.ModelID },
|
||||
func(adapter ModelAdapterConfig) string {
|
||||
return modelchannel.BuildLegacyChannelID(adapter.BaseURL, adapter.ModelID, adapter.APIKey, adapter.DisplayName)
|
||||
},
|
||||
)
|
||||
if !ok {
|
||||
return nil, legacyruntime.ErrChannelNotAvailable
|
||||
}
|
||||
matched := adapters[matchIndex]
|
||||
|
||||
resolved := &legacyruntime.ResolvedChannel{
|
||||
ID: strings.TrimSpace(matched.ID),
|
||||
Name: strings.TrimSpace(matched.DisplayName),
|
||||
GroupName: "local",
|
||||
Code: strings.TrimSpace(matched.ID),
|
||||
Provider: strings.TrimSpace(matched.Type),
|
||||
BaseURL: strings.TrimSpace(matched.BaseURL),
|
||||
APIKey: strings.TrimSpace(matched.APIKey),
|
||||
Model: strings.TrimSpace(matched.ModelID),
|
||||
OpenAIEndpoint: strings.TrimSpace(matched.OpenAIEndpoint),
|
||||
OpenAIExtraParamsEnabled: matched.OpenAIExtraParamsEnabled,
|
||||
OpenAIExtraParamsJSON: strings.TrimSpace(matched.OpenAIExtraParamsJSON),
|
||||
CustomHeadersEnabled: matched.CustomHeadersEnabled,
|
||||
CustomHeadersJSON: strings.TrimSpace(matched.CustomHeadersJSON),
|
||||
AnthropicExtraParamsEnabled: matched.AnthropicExtraParamsEnabled,
|
||||
AnthropicExtraParamsJSON: strings.TrimSpace(matched.AnthropicExtraParamsJSON),
|
||||
TimeoutMS: defaultChannelTimeoutMS,
|
||||
ContextWindowTokens: defaultChannelContextWindowTokens,
|
||||
MaxTokens: defaultChannelMaxTokens,
|
||||
ReasoningEffort: strings.TrimSpace(matched.ReasoningEffort),
|
||||
AnthropicMaxTokens: defaultChannelMaxTokens,
|
||||
AnthropicThinkingEffort: defaultChannelAnthropicEffort,
|
||||
ThinkingEnabled: true,
|
||||
ThinkingBudgetTokens: defaultChannelThinkingBudget,
|
||||
}
|
||||
if matched.ContextWindowTokens > 0 {
|
||||
resolved.ContextWindowTokens = matched.ContextWindowTokens
|
||||
}
|
||||
if matched.MaxCompletionTokens > 0 {
|
||||
resolved.MaxTokens = matched.MaxCompletionTokens
|
||||
}
|
||||
if matched.AnthropicMaxTokens > 0 {
|
||||
resolved.AnthropicMaxTokens = matched.AnthropicMaxTokens
|
||||
}
|
||||
if matched.ThinkingBudgetTokens > 0 {
|
||||
resolved.ThinkingBudgetTokens = matched.ThinkingBudgetTokens
|
||||
}
|
||||
if strings.TrimSpace(matched.AnthropicThinkingEffort) != "" {
|
||||
resolved.AnthropicThinkingEffort = strings.TrimSpace(matched.AnthropicThinkingEffort)
|
||||
}
|
||||
return resolved, nil
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type Store struct {
|
||||
path string
|
||||
logsRoot string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type fileSnapshot struct {
|
||||
exists bool
|
||||
modTime int64
|
||||
size int64
|
||||
}
|
||||
|
||||
func NewStore(path string, logsRoot string) *Store {
|
||||
return &Store{
|
||||
path: strings.TrimSpace(path),
|
||||
logsRoot: strings.TrimSpace(logsRoot),
|
||||
}
|
||||
}
|
||||
|
||||
func (store *Store) Path() string {
|
||||
if store == nil {
|
||||
return ""
|
||||
}
|
||||
return store.path
|
||||
}
|
||||
|
||||
func (store *Store) LogsRoot() string {
|
||||
if store == nil {
|
||||
return ""
|
||||
}
|
||||
return store.logsRoot
|
||||
}
|
||||
|
||||
func (store *Store) snapshot() fileSnapshot {
|
||||
if store == nil || strings.TrimSpace(store.path) == "" {
|
||||
return fileSnapshot{}
|
||||
}
|
||||
info, err := os.Stat(store.path)
|
||||
if err != nil {
|
||||
return fileSnapshot{}
|
||||
}
|
||||
return fileSnapshot{
|
||||
exists: true,
|
||||
modTime: info.ModTime().UnixNano(),
|
||||
size: info.Size(),
|
||||
}
|
||||
}
|
||||
|
||||
func (store *Store) Load(_ context.Context) (Config, error) {
|
||||
if store == nil || strings.TrimSpace(store.path) == "" {
|
||||
return DefaultConfig(), nil
|
||||
}
|
||||
|
||||
store.mu.Lock()
|
||||
defer store.mu.Unlock()
|
||||
|
||||
data, err := os.ReadFile(store.path)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
defaultConfig := DefaultConfig()
|
||||
if err := store.saveLocked(defaultConfig); err != nil {
|
||||
return DefaultConfig(), err
|
||||
}
|
||||
return defaultConfig, nil
|
||||
}
|
||||
return DefaultConfig(), fmt.Errorf("读取用户配置失败: %w", err)
|
||||
}
|
||||
|
||||
var current Config
|
||||
if err := yaml.Unmarshal(data, ¤t); err != nil {
|
||||
return DefaultConfig(), fmt.Errorf("解析用户配置失败: %w", err)
|
||||
}
|
||||
normalized, err := NormalizeConfig(current)
|
||||
if err != nil {
|
||||
return DefaultConfig(), err
|
||||
}
|
||||
if shouldPersistNormalizedConfig(data, current, normalized) {
|
||||
if err := store.saveLocked(normalized); err != nil {
|
||||
return DefaultConfig(), err
|
||||
}
|
||||
}
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
func (store *Store) Save(_ context.Context, cfg Config) (Config, error) {
|
||||
if store == nil || strings.TrimSpace(store.path) == "" {
|
||||
return Config{}, errors.New("配置存储未初始化")
|
||||
}
|
||||
|
||||
normalized, err := NormalizeConfig(cfg)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
store.mu.Lock()
|
||||
defer store.mu.Unlock()
|
||||
|
||||
if err := store.saveLocked(normalized); err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
func (store *Store) saveLocked(normalized Config) error {
|
||||
if err := os.MkdirAll(filepath.Dir(store.path), 0o755); err != nil {
|
||||
return fmt.Errorf("创建用户配置目录失败: %w", err)
|
||||
}
|
||||
|
||||
data, err := yaml.Marshal(normalized)
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化用户配置失败: %w", err)
|
||||
}
|
||||
|
||||
tempPath := store.path + ".tmp"
|
||||
if err := os.WriteFile(tempPath, data, 0o644); err != nil {
|
||||
return fmt.Errorf("写入临时配置失败: %w", err)
|
||||
}
|
||||
if err := os.Rename(tempPath, store.path); err != nil {
|
||||
return fmt.Errorf("保存用户配置失败: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func shouldPersistNormalizedConfig(raw []byte, current Config, normalized Config) bool {
|
||||
if !yamlHasKey(raw, "backendListenAddr") || !yamlHasKey(raw, "proxyListenAddr") {
|
||||
return true
|
||||
}
|
||||
if current.BackendListenAddr != normalized.BackendListenAddr || current.ProxyListenAddr != normalized.ProxyListenAddr {
|
||||
return true
|
||||
}
|
||||
if current.ProviderStreamIdleTimeout == normalized.ProviderStreamIdleTimeout {
|
||||
return false
|
||||
}
|
||||
return yamlHasKey(raw, "providerStreamIdleTimeout")
|
||||
}
|
||||
|
||||
func yamlHasKey(raw []byte, key string) bool {
|
||||
var root yaml.Node
|
||||
if err := yaml.Unmarshal(raw, &root); err != nil {
|
||||
return false
|
||||
}
|
||||
if len(root.Content) == 0 || root.Content[0].Kind != yaml.MappingNode {
|
||||
return false
|
||||
}
|
||||
mapping := root.Content[0]
|
||||
for index := 0; index+1 < len(mapping.Content); index += 2 {
|
||||
if mapping.Content[index].Value == key {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,293 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"cursor/internal/modelchannel"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultBackendListenAddr = "127.0.0.1:18090"
|
||||
DefaultProxyListenAddr = "127.0.0.1:18080"
|
||||
DefaultFrontendBaseURL = "http://127.0.0.1"
|
||||
DefaultRoutingMode = "local"
|
||||
DefaultProviderStreamIdleTimeoutSeconds = 240
|
||||
MinProviderStreamIdleTimeoutSeconds = 30
|
||||
)
|
||||
|
||||
type ModelAdapterConfig struct {
|
||||
ID string `json:"id,omitempty" yaml:"-"`
|
||||
DisplayName string `json:"displayName" yaml:"displayName"`
|
||||
Type string `json:"type" yaml:"type"`
|
||||
BaseURL string `json:"baseURL" yaml:"baseURL"`
|
||||
APIKey string `json:"apiKey" yaml:"apiKey"`
|
||||
TooltipData string `json:"tooltipData" yaml:"tooltipData"`
|
||||
ModelID string `json:"modelID" yaml:"modelID"`
|
||||
ReasoningEffort string `json:"reasoningEffort" yaml:"reasoningEffort"`
|
||||
OpenAIEndpoint string `json:"openAIEndpoint" yaml:"openAIEndpoint"`
|
||||
OpenAIExtraParamsEnabled bool `json:"openAIExtraParamsEnabled" yaml:"openAIExtraParamsEnabled"`
|
||||
OpenAIExtraParamsJSON string `json:"openAIExtraParamsJSON" yaml:"openAIExtraParamsJSON"`
|
||||
CustomHeadersEnabled bool `json:"customHeadersEnabled" yaml:"customHeadersEnabled"`
|
||||
CustomHeadersJSON string `json:"customHeadersJSON" yaml:"customHeadersJSON"`
|
||||
AnthropicExtraParamsEnabled bool `json:"anthropicExtraParamsEnabled" yaml:"anthropicExtraParamsEnabled"`
|
||||
AnthropicExtraParamsJSON string `json:"anthropicExtraParamsJSON" yaml:"anthropicExtraParamsJSON"`
|
||||
ContextWindowTokens int `json:"contextWindowTokens" yaml:"contextWindowTokens"`
|
||||
MaxCompletionTokens int `json:"maxCompletionTokens" yaml:"maxCompletionTokens"`
|
||||
AnthropicMaxTokens int `json:"anthropicMaxTokens" yaml:"anthropicMaxTokens"`
|
||||
AnthropicThinkingEffort string `json:"anthropicThinkingEffort,omitempty" yaml:"anthropicThinkingEffort,omitempty"`
|
||||
ThinkingBudgetTokens int `json:"thinkingBudgetTokens" yaml:"thinkingBudgetTokens"`
|
||||
}
|
||||
|
||||
type RoutingConfig struct {
|
||||
Mode string `json:"mode" yaml:"mode"`
|
||||
}
|
||||
|
||||
type HomeMetricsConfig struct {
|
||||
IncludeCacheWriteInHitRate bool `json:"includeCacheWriteInHitRate" yaml:"includeCacheWriteInHitRate"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
Log bool `json:"log" yaml:"log"`
|
||||
ProviderStreamIdleTimeout int `json:"providerStreamIdleTimeout" yaml:"providerStreamIdleTimeout"`
|
||||
BackendListenAddr string `json:"backendListenAddr" yaml:"backendListenAddr"`
|
||||
ProxyListenAddr string `json:"proxyListenAddr" yaml:"proxyListenAddr"`
|
||||
ModelAdapters []ModelAdapterConfig `json:"modelAdapters" yaml:"modelAdapters"`
|
||||
Routing RoutingConfig `json:"routing" yaml:"routing"`
|
||||
HomeMetrics HomeMetricsConfig `json:"homeMetrics" yaml:"homeMetrics"`
|
||||
LastAgentModelHash string `json:"lastAgentModelHash" yaml:"lastAgentModelHash"`
|
||||
}
|
||||
|
||||
func DefaultConfig() Config {
|
||||
return Config{
|
||||
Log: false,
|
||||
ProviderStreamIdleTimeout: DefaultProviderStreamIdleTimeoutSeconds,
|
||||
BackendListenAddr: DefaultBackendListenAddr,
|
||||
ProxyListenAddr: DefaultProxyListenAddr,
|
||||
ModelAdapters: []ModelAdapterConfig{},
|
||||
Routing: RoutingConfig{
|
||||
Mode: DefaultRoutingMode,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func NormalizeConfig(input Config) (Config, error) {
|
||||
output := DefaultConfig()
|
||||
output.Log = input.Log
|
||||
output.ProviderStreamIdleTimeout = normalizeProviderStreamIdleTimeout(input.ProviderStreamIdleTimeout)
|
||||
backendListenAddr, err := normalizeListenAddr(input.BackendListenAddr, DefaultBackendListenAddr, "backendListenAddr")
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
proxyListenAddr, err := normalizeListenAddr(input.ProxyListenAddr, DefaultProxyListenAddr, "proxyListenAddr")
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
output.BackendListenAddr = backendListenAddr
|
||||
output.ProxyListenAddr = proxyListenAddr
|
||||
output.HomeMetrics.IncludeCacheWriteInHitRate = input.HomeMetrics.IncludeCacheWriteInHitRate
|
||||
output.LastAgentModelHash = strings.TrimSpace(input.LastAgentModelHash)
|
||||
output.Routing.Mode = normalizeRoutingMode(input.Routing.Mode)
|
||||
if output.Routing.Mode == "" {
|
||||
output.Routing.Mode = DefaultRoutingMode
|
||||
}
|
||||
adapters, err := NormalizeModelAdapterConfigs(input.ModelAdapters)
|
||||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
output.ModelAdapters = adapters
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func NormalizeModelAdapterConfigs(input []ModelAdapterConfig) ([]ModelAdapterConfig, error) {
|
||||
if len(input) == 0 {
|
||||
return []ModelAdapterConfig{}, nil
|
||||
}
|
||||
|
||||
normalized := make([]ModelAdapterConfig, 0, len(input))
|
||||
seenChannelIDs := make(map[string]struct{}, len(input))
|
||||
for _, item := range input {
|
||||
baseURL, err := modelchannel.NormalizeBaseURL(item.BaseURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nextType := normalizeModelAdapterType(item.Type)
|
||||
next := ModelAdapterConfig{
|
||||
DisplayName: strings.TrimSpace(item.DisplayName),
|
||||
Type: nextType,
|
||||
BaseURL: baseURL,
|
||||
APIKey: strings.TrimSpace(item.APIKey),
|
||||
TooltipData: strings.TrimSpace(item.TooltipData),
|
||||
ModelID: strings.TrimSpace(item.ModelID),
|
||||
ReasoningEffort: normalizeReasoningEffort(item.ReasoningEffort),
|
||||
OpenAIEndpoint: modelchannel.NormalizeOpenAIEndpoint(item.Type, item.OpenAIEndpoint),
|
||||
ContextWindowTokens: normalizeMaxCompletionTokens(item.ContextWindowTokens),
|
||||
MaxCompletionTokens: normalizeMaxCompletionTokens(item.MaxCompletionTokens),
|
||||
AnthropicMaxTokens: normalizeMaxCompletionTokens(item.AnthropicMaxTokens),
|
||||
ThinkingBudgetTokens: normalizeMaxCompletionTokens(item.ThinkingBudgetTokens),
|
||||
}
|
||||
if next.Type == "openai" {
|
||||
next.OpenAIExtraParamsEnabled = item.OpenAIExtraParamsEnabled
|
||||
next.OpenAIExtraParamsJSON = strings.TrimSpace(item.OpenAIExtraParamsJSON)
|
||||
} else if next.Type == "anthropic" {
|
||||
next.AnthropicThinkingEffort = normalizeAnthropicThinkingEffort(item.AnthropicThinkingEffort)
|
||||
next.AnthropicExtraParamsEnabled = item.AnthropicExtraParamsEnabled
|
||||
next.AnthropicExtraParamsJSON = strings.TrimSpace(item.AnthropicExtraParamsJSON)
|
||||
}
|
||||
next.CustomHeadersEnabled = item.CustomHeadersEnabled
|
||||
next.CustomHeadersJSON = strings.TrimSpace(item.CustomHeadersJSON)
|
||||
switch {
|
||||
case next.DisplayName == "":
|
||||
return nil, errors.New("模型适配器 displayName 不能为空")
|
||||
case next.Type == "":
|
||||
return nil, errors.New("模型适配器 type 仅支持 openai 或 anthropic")
|
||||
case next.APIKey == "":
|
||||
return nil, errors.New("模型适配器 apiKey 不能为空")
|
||||
case next.TooltipData == "":
|
||||
return nil, errors.New("模型适配器 tooltipData 不能为空")
|
||||
case next.ModelID == "":
|
||||
return nil, errors.New("模型适配器 modelID 不能为空")
|
||||
case next.Type == "openai" && next.ReasoningEffort == "":
|
||||
return nil, errors.New("模型适配器 reasoningEffort 仅支持 low、medium、high、xhigh")
|
||||
case next.Type == "openai" && next.OpenAIEndpoint == "":
|
||||
return nil, errors.New("模型适配器 openAIEndpoint 仅支持 /v1/responses 或 /v1/chat/completions")
|
||||
case next.Type == "openai" && next.OpenAIExtraParamsEnabled:
|
||||
if err := validateJSONMap(next.OpenAIExtraParamsJSON, "openAIExtraParamsJSON"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case next.CustomHeadersEnabled:
|
||||
if err := validateHeadersJSON(next.CustomHeadersJSON); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case next.Type == "anthropic" && next.AnthropicExtraParamsEnabled:
|
||||
if err := validateJSONMap(next.AnthropicExtraParamsJSON, "anthropicExtraParamsJSON"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case next.Type == "anthropic" && next.AnthropicThinkingEffort == "":
|
||||
return nil, errors.New("模型适配器 anthropicThinkingEffort 仅支持 low、medium、high、xhigh、max")
|
||||
}
|
||||
next.ID = modelchannel.BuildChannelID(next.BaseURL, next.ModelID, next.APIKey, next.DisplayName, next.OpenAIEndpoint)
|
||||
if _, exists := seenChannelIDs[next.ID]; exists {
|
||||
return nil, errors.New("模型适配器渠道不能重复,请检查 url、modelID、apiKey、displayName、endpoint 组合")
|
||||
}
|
||||
seenChannelIDs[next.ID] = struct{}{}
|
||||
normalized = append(normalized, next)
|
||||
}
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
func validateJSONMap(value string, fieldName string) error {
|
||||
text := strings.TrimSpace(value)
|
||||
if text == "" {
|
||||
return fmt.Errorf("模型适配器 %s 不能为空", fieldName)
|
||||
}
|
||||
var parsed map[string]any
|
||||
if err := json.Unmarshal([]byte(text), &parsed); err != nil {
|
||||
return fmt.Errorf("模型适配器 %s 必须是合法 JSON 对象", fieldName)
|
||||
}
|
||||
if parsed == nil {
|
||||
return fmt.Errorf("模型适配器 %s 必须是 JSON 对象", fieldName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateHeadersJSON(value string) error {
|
||||
text := strings.TrimSpace(value)
|
||||
if err := validateJSONMap(text, "customHeadersJSON"); err != nil {
|
||||
return err
|
||||
}
|
||||
var parsed map[string]string
|
||||
if err := json.Unmarshal([]byte(text), &parsed); err != nil {
|
||||
return errors.New("模型适配器 customHeadersJSON 的值必须是字符串")
|
||||
}
|
||||
for key := range parsed {
|
||||
if strings.TrimSpace(key) == "" {
|
||||
return errors.New("模型适配器 customHeadersJSON 的请求头名称不能为空")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeReasoningEffort(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "", "medium":
|
||||
return "medium"
|
||||
case "low", "high", "xhigh":
|
||||
return strings.ToLower(strings.TrimSpace(value))
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeAnthropicThinkingEffort(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "", "xhigh":
|
||||
return "xhigh"
|
||||
case "low", "medium", "high", "max":
|
||||
return strings.ToLower(strings.TrimSpace(value))
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeListenAddr(value string, defaultValue string, fieldName string) (string, error) {
|
||||
addr := strings.TrimSpace(value)
|
||||
if addr == "" {
|
||||
addr = defaultValue
|
||||
}
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s 必须是 host:port 格式", fieldName)
|
||||
}
|
||||
if strings.TrimSpace(host) == "" {
|
||||
return "", fmt.Errorf("%s host 不能为空", fieldName)
|
||||
}
|
||||
parsedPort, err := strconv.Atoi(port)
|
||||
if err != nil || parsedPort < 1 || parsedPort > 65535 {
|
||||
return "", fmt.Errorf("%s port 必须在 1-65535 之间", fieldName)
|
||||
}
|
||||
return net.JoinHostPort(host, strconv.Itoa(parsedPort)), nil
|
||||
}
|
||||
|
||||
func normalizeProviderStreamIdleTimeout(value int) int {
|
||||
if value <= 0 {
|
||||
return DefaultProviderStreamIdleTimeoutSeconds
|
||||
}
|
||||
if value < MinProviderStreamIdleTimeoutSeconds {
|
||||
return MinProviderStreamIdleTimeoutSeconds
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func normalizeMaxCompletionTokens(value int) int {
|
||||
if value <= 0 {
|
||||
return 0
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func normalizeModelAdapterType(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "openai":
|
||||
return "openai"
|
||||
case "anthropic":
|
||||
return "anthropic"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeRoutingMode(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "", "local":
|
||||
return "local"
|
||||
case "upstream":
|
||||
return "upstream"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const HeaderServerUpstreamURL = "X-Server-Upstream-URL"
|
||||
|
||||
type SourceKind string
|
||||
|
||||
const (
|
||||
SourceNative SourceKind = "native"
|
||||
SourceMITM SourceKind = "mitm"
|
||||
)
|
||||
|
||||
type ProtocolClass string
|
||||
|
||||
const (
|
||||
ProtocolHTTP ProtocolClass = "http"
|
||||
ProtocolConnectUnary ProtocolClass = "connect_unary"
|
||||
ProtocolConnectStream ProtocolClass = "connect_stream"
|
||||
)
|
||||
|
||||
type Context struct {
|
||||
Writer http.ResponseWriter
|
||||
Request *http.Request
|
||||
RouteName string
|
||||
Source SourceKind
|
||||
Protocol ProtocolClass
|
||||
StartedAt time.Time
|
||||
|
||||
UpstreamURL *url.URL
|
||||
Mode ExecutionMode
|
||||
LastError error
|
||||
|
||||
Logger *slog.Logger
|
||||
}
|
||||
|
||||
func newContext(writer http.ResponseWriter, request *http.Request, route Route) *Context {
|
||||
return &Context{
|
||||
Writer: writer,
|
||||
Request: request,
|
||||
RouteName: route.Name,
|
||||
Protocol: route.Protocol,
|
||||
StartedAt: time.Now(),
|
||||
Logger: slog.Default(),
|
||||
Mode: ModeLocal,
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *Context) ParseUpstreamURL() error {
|
||||
if ctx == nil || ctx.Request == nil {
|
||||
return nil
|
||||
}
|
||||
rawURL := strings.TrimSpace(ctx.Request.Header.Get(HeaderServerUpstreamURL))
|
||||
if rawURL == "" {
|
||||
ctx.Source = SourceNative
|
||||
ctx.UpstreamURL = nil
|
||||
return nil
|
||||
}
|
||||
parsed, err := ParseAndValidateRawURL(rawURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx.Source = SourceMITM
|
||||
ctx.UpstreamURL = parsed
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Package server 提供 ConnectRPC 服务端入口,并将协议请求转发到运行时协调层。
|
||||
package server
|
||||
@@ -0,0 +1,5 @@
|
||||
package server
|
||||
|
||||
import "errors"
|
||||
|
||||
var ErrInvalidBidiAppendPayload = errors.New("invalid bidi append payload")
|
||||
@@ -0,0 +1,38 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"cursor/internal/logger"
|
||||
)
|
||||
|
||||
func Health() HandlerFunc {
|
||||
return func(ctx *Context) error {
|
||||
if ctx == nil || ctx.Writer == nil {
|
||||
return nil
|
||||
}
|
||||
if ctx.Request != nil && ctx.Request.Method != http.MethodGet {
|
||||
http.Error(ctx.Writer, "method not allowed", http.StatusMethodNotAllowed)
|
||||
return nil
|
||||
}
|
||||
if ctx.Request != nil {
|
||||
logger.Infof("内置后端 healthz 命中 remote_addr=%s user_agent=%s", ctx.Request.RemoteAddr, ctx.Request.UserAgent())
|
||||
}
|
||||
ctx.Writer.WriteHeader(http.StatusOK)
|
||||
_, _ = ctx.Writer.Write([]byte("ok"))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func HTTPHandlerAction(handler http.Handler) HandlerFunc {
|
||||
return func(ctx *Context) error {
|
||||
if ctx == nil {
|
||||
return nil
|
||||
}
|
||||
if handler == nil {
|
||||
return nil
|
||||
}
|
||||
handler.ServeHTTP(ctx.Writer, ctx.Request)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"cursor/internal/logger"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
serverconfig "cursor/internal/backend/server/config"
|
||||
legacyruntime "cursor/internal/runtime"
|
||||
)
|
||||
|
||||
func Recover() Middleware {
|
||||
return func(next HandlerFunc) HandlerFunc {
|
||||
return func(ctx *Context) (err error) {
|
||||
defer func() {
|
||||
if recovered := recover(); recovered != nil {
|
||||
err = fmt.Errorf("panic: %v\n%s", recovered, string(debug.Stack()))
|
||||
}
|
||||
}()
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ServerContext() Middleware {
|
||||
return func(next HandlerFunc) HandlerFunc {
|
||||
return func(ctx *Context) error {
|
||||
if ctx == nil {
|
||||
return fmt.Errorf("server context is nil")
|
||||
}
|
||||
if err := ctx.ParseUpstreamURL(); err != nil {
|
||||
return err
|
||||
}
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func PolicyMiddleware(configs *serverconfig.Manager) Middleware {
|
||||
return func(next HandlerFunc) HandlerFunc {
|
||||
return func(ctx *Context) error {
|
||||
ctx.Mode = parseExecutionMode(configs.RouteMode(ctx.UpstreamURL != nil))
|
||||
logger.Infof("ctx.Mode=%s upstream=%t", ctx.Mode, ctx.UpstreamURL != nil)
|
||||
return next(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ErrorEncoder() Middleware {
|
||||
return func(next HandlerFunc) HandlerFunc {
|
||||
return func(ctx *Context) error {
|
||||
if ctx != nil {
|
||||
ctx.LastError = nil
|
||||
}
|
||||
if err := next(ctx); err != nil {
|
||||
if ctx != nil {
|
||||
ctx.LastError = err
|
||||
}
|
||||
if ctx == nil || ctx.Writer == nil {
|
||||
return err
|
||||
}
|
||||
writeServerError(ctx.Writer, err)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func writeServerError(writer http.ResponseWriter, err error) {
|
||||
if responseWriterHasWrittenHeader(writer) {
|
||||
return
|
||||
}
|
||||
status := http.StatusBadGateway
|
||||
message := "bad gateway"
|
||||
switch {
|
||||
case err == nil:
|
||||
status = http.StatusOK
|
||||
message = ""
|
||||
case strings.TrimSpace(err.Error()) == "empty raw url":
|
||||
status = http.StatusBadRequest
|
||||
message = "invalid raw url"
|
||||
case errors.Is(err, ErrInvalidBidiAppendPayload):
|
||||
status = http.StatusBadRequest
|
||||
message = "invalid bidi append payload"
|
||||
case errors.Is(err, legacyruntime.ErrInvalidSystemSetting):
|
||||
status = http.StatusInternalServerError
|
||||
message = "invalid system setting"
|
||||
case errors.Is(err, legacyruntime.ErrChannelNotAvailable):
|
||||
status = http.StatusServiceUnavailable
|
||||
message = "no available channel"
|
||||
}
|
||||
http.Error(writer, message, status)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package server
|
||||
|
||||
type ExecutionMode string
|
||||
|
||||
const (
|
||||
// ModeLocal 表示本地模式,适用于直接处理请求的情况。
|
||||
ModeLocal ExecutionMode = "local"
|
||||
// ModeUpstream 表示直连上游模式,适用于将请求转发到原始地址。
|
||||
ModeUpstream ExecutionMode = "upstream"
|
||||
)
|
||||
|
||||
func parseExecutionMode(value string) ExecutionMode {
|
||||
switch value {
|
||||
case string(ModeUpstream):
|
||||
return ModeUpstream
|
||||
default:
|
||||
return ModeLocal
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type responseWrittenTracker interface {
|
||||
ResponseWritten() bool
|
||||
}
|
||||
|
||||
type trackedResponseWriter struct {
|
||||
http.ResponseWriter
|
||||
wroteHeader bool
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func newTrackedResponseWriter(writer http.ResponseWriter) *trackedResponseWriter {
|
||||
return &trackedResponseWriter{ResponseWriter: writer}
|
||||
}
|
||||
|
||||
func (writer *trackedResponseWriter) WriteHeader(statusCode int) {
|
||||
if writer == nil || writer.ResponseWriter == nil {
|
||||
return
|
||||
}
|
||||
if writer.wroteHeader {
|
||||
return
|
||||
}
|
||||
writer.wroteHeader = true
|
||||
writer.statusCode = statusCode
|
||||
writer.ResponseWriter.WriteHeader(statusCode)
|
||||
}
|
||||
|
||||
func (writer *trackedResponseWriter) Write(payload []byte) (int, error) {
|
||||
if writer == nil || writer.ResponseWriter == nil {
|
||||
return 0, http.ErrAbortHandler
|
||||
}
|
||||
if !writer.wroteHeader {
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
}
|
||||
return writer.ResponseWriter.Write(payload)
|
||||
}
|
||||
|
||||
func (writer *trackedResponseWriter) Flush() {
|
||||
if writer == nil || writer.ResponseWriter == nil {
|
||||
return
|
||||
}
|
||||
if !writer.wroteHeader {
|
||||
writer.WriteHeader(http.StatusOK)
|
||||
}
|
||||
if flusher, ok := writer.ResponseWriter.(http.Flusher); ok {
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func (writer *trackedResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
if writer == nil || writer.ResponseWriter == nil {
|
||||
return nil, nil, fmt.Errorf("response writer is nil")
|
||||
}
|
||||
hijacker, ok := writer.ResponseWriter.(http.Hijacker)
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("response writer does not support hijacking")
|
||||
}
|
||||
writer.wroteHeader = true
|
||||
return hijacker.Hijack()
|
||||
}
|
||||
|
||||
func (writer *trackedResponseWriter) Unwrap() http.ResponseWriter {
|
||||
if writer == nil {
|
||||
return nil
|
||||
}
|
||||
return writer.ResponseWriter
|
||||
}
|
||||
|
||||
func (writer *trackedResponseWriter) ResponseWritten() bool {
|
||||
return writer != nil && writer.wroteHeader
|
||||
}
|
||||
|
||||
func responseWriterHasWrittenHeader(writer http.ResponseWriter) bool {
|
||||
for writer != nil {
|
||||
if tracker, ok := writer.(responseWrittenTracker); ok {
|
||||
if tracker.ResponseWritten() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
unwrapper, ok := writer.(interface{ Unwrap() http.ResponseWriter })
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
next := unwrapper.Unwrap()
|
||||
if next == nil || next == writer {
|
||||
return false
|
||||
}
|
||||
writer = next
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type HandlerFunc func(*Context) error
|
||||
type Middleware func(HandlerFunc) HandlerFunc
|
||||
|
||||
type Route struct {
|
||||
Method string
|
||||
Pattern string
|
||||
Name string
|
||||
Protocol ProtocolClass
|
||||
Middleware []Middleware
|
||||
Local HandlerFunc
|
||||
Upstream HandlerFunc
|
||||
}
|
||||
|
||||
type App struct {
|
||||
router chi.Router
|
||||
globalMiddlewares []Middleware
|
||||
routes []Route
|
||||
mounts []mountSpec
|
||||
}
|
||||
|
||||
type mountSpec struct {
|
||||
pattern string
|
||||
handler http.Handler
|
||||
}
|
||||
|
||||
type Option func(*App)
|
||||
type RouteOption func(*Route)
|
||||
|
||||
func New(options ...Option) http.Handler {
|
||||
app := &App{router: chi.NewRouter()}
|
||||
for _, option := range options {
|
||||
if option != nil {
|
||||
option(app)
|
||||
}
|
||||
}
|
||||
for _, route := range app.routes {
|
||||
app.registerRoute(route)
|
||||
}
|
||||
for _, mount := range app.mounts {
|
||||
app.router.Mount(mount.pattern, mount.handler)
|
||||
}
|
||||
return app.router
|
||||
}
|
||||
|
||||
func Use(middlewares ...Middleware) Option {
|
||||
return func(app *App) {
|
||||
app.globalMiddlewares = append(app.globalMiddlewares, middlewares...)
|
||||
}
|
||||
}
|
||||
|
||||
func Mount(pattern string, handler http.Handler) Option {
|
||||
return func(app *App) {
|
||||
if handler == nil {
|
||||
return
|
||||
}
|
||||
app.mounts = append(app.mounts, mountSpec{pattern: pattern, handler: handler})
|
||||
}
|
||||
}
|
||||
|
||||
func GET(pattern string, options ...RouteOption) Option {
|
||||
return routeOption(http.MethodGet, pattern, options...)
|
||||
}
|
||||
func POST(pattern string, options ...RouteOption) Option {
|
||||
return routeOption(http.MethodPost, pattern, options...)
|
||||
}
|
||||
func Any(pattern string, options ...RouteOption) Option { return routeOption("", pattern, options...) }
|
||||
|
||||
func routeOption(method string, pattern string, options ...RouteOption) Option {
|
||||
return func(app *App) {
|
||||
route := Route{
|
||||
Method: method,
|
||||
Pattern: pattern,
|
||||
Protocol: ProtocolHTTP,
|
||||
Local: func(ctx *Context) error {
|
||||
return fmt.Errorf("route %s has no local action", pattern)
|
||||
},
|
||||
}
|
||||
for _, option := range options {
|
||||
if option != nil {
|
||||
option(&route)
|
||||
}
|
||||
}
|
||||
app.routes = append(app.routes, route)
|
||||
}
|
||||
}
|
||||
|
||||
func Name(name string) RouteOption {
|
||||
return func(route *Route) {
|
||||
route.Name = name
|
||||
}
|
||||
}
|
||||
|
||||
func HTTP() RouteOption {
|
||||
return func(route *Route) {
|
||||
route.Protocol = ProtocolHTTP
|
||||
}
|
||||
}
|
||||
|
||||
func ConnectUnary() RouteOption {
|
||||
return func(route *Route) {
|
||||
route.Protocol = ProtocolConnectUnary
|
||||
}
|
||||
}
|
||||
|
||||
func ConnectStream() RouteOption {
|
||||
return func(route *Route) {
|
||||
route.Protocol = ProtocolConnectStream
|
||||
}
|
||||
}
|
||||
|
||||
func With(middlewares ...Middleware) RouteOption {
|
||||
return func(route *Route) {
|
||||
route.Middleware = append(route.Middleware, middlewares...)
|
||||
}
|
||||
}
|
||||
|
||||
func Local(action HandlerFunc) RouteOption {
|
||||
return func(route *Route) {
|
||||
route.Local = action
|
||||
}
|
||||
}
|
||||
|
||||
func Upstream(action HandlerFunc) RouteOption {
|
||||
return func(route *Route) {
|
||||
route.Upstream = action
|
||||
}
|
||||
}
|
||||
|
||||
func (app *App) registerRoute(route Route) {
|
||||
handler := app.buildRouteHandler(route)
|
||||
if route.Method == "" {
|
||||
app.router.HandleFunc(route.Pattern, handler)
|
||||
return
|
||||
}
|
||||
app.router.MethodFunc(route.Method, route.Pattern, handler)
|
||||
}
|
||||
|
||||
func (app *App) buildRouteHandler(route Route) http.HandlerFunc {
|
||||
chain := append([]Middleware{}, app.globalMiddlewares...)
|
||||
chain = append(chain, route.Middleware...)
|
||||
final := Chain(chain...)(func(ctx *Context) error {
|
||||
if shouldUseUpstreamAction(ctx, route) && route.Upstream != nil {
|
||||
return route.Upstream(ctx)
|
||||
}
|
||||
if shouldUseUpstreamAction(ctx, route) && ctx.UpstreamURL != nil {
|
||||
return fmt.Errorf("route %s is missing upstream action while request targets upstream %s", route.Name, ctx.UpstreamURL.String())
|
||||
}
|
||||
if route.Local != nil {
|
||||
return route.Local(ctx)
|
||||
}
|
||||
return fmt.Errorf("route %s has no executable action", route.Name)
|
||||
})
|
||||
return func(writer http.ResponseWriter, request *http.Request) {
|
||||
trackedWriter := newTrackedResponseWriter(writer)
|
||||
ctx := newContext(trackedWriter, request, route)
|
||||
if err := final(ctx); err != nil {
|
||||
writeServerError(trackedWriter, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func shouldUseUpstreamAction(ctx *Context, route Route) bool {
|
||||
_ = route
|
||||
if ctx == nil {
|
||||
return false
|
||||
}
|
||||
return ctx.Mode == ModeUpstream
|
||||
}
|
||||
|
||||
func Chain(middlewares ...Middleware) Middleware {
|
||||
return func(final HandlerFunc) HandlerFunc {
|
||||
wrapped := final
|
||||
for index := len(middlewares) - 1; index >= 0; index-- {
|
||||
current := middlewares[index]
|
||||
if current == nil {
|
||||
continue
|
||||
}
|
||||
wrapped = current(wrapped)
|
||||
}
|
||||
return wrapped
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
package upstream
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cursor/internal/backend/server"
|
||||
)
|
||||
|
||||
type CompatRouteConfig struct {
|
||||
Name string
|
||||
StatusCode int
|
||||
JSONBody map[string]any
|
||||
MockProtoType string
|
||||
MockBuilder func(*RequestContext) (map[string]any, error)
|
||||
ConsoleLog bool
|
||||
}
|
||||
|
||||
func DirectAction(deps Dependencies, cfg CompatRouteConfig) server.HandlerFunc {
|
||||
return func(ctx *server.Context) error {
|
||||
reqCtx, route, err := newCompatRouteObjects(ctx, deps, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return handleDirect(reqCtx, route)
|
||||
}
|
||||
}
|
||||
|
||||
func FixedStatusAction(deps Dependencies, cfg CompatRouteConfig) server.HandlerFunc {
|
||||
return func(ctx *server.Context) error {
|
||||
reqCtx, route, err := newCompatRouteObjects(ctx, deps, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return handleFixedStatus(reqCtx, route)
|
||||
}
|
||||
}
|
||||
|
||||
func MockJSONAction(deps Dependencies, cfg CompatRouteConfig) server.HandlerFunc {
|
||||
return func(ctx *server.Context) error {
|
||||
reqCtx, route, err := newCompatRouteObjects(ctx, deps, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return handleMockJSON(reqCtx, route)
|
||||
}
|
||||
}
|
||||
|
||||
func MockOAuthAction(deps Dependencies, cfg CompatRouteConfig) server.HandlerFunc {
|
||||
return func(ctx *server.Context) error {
|
||||
reqCtx, route, err := newCompatRouteObjects(ctx, deps, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return handleMockOAuth(reqCtx, route)
|
||||
}
|
||||
}
|
||||
|
||||
func MockAuthFullStripeProfileAction(deps Dependencies, cfg CompatRouteConfig) server.HandlerFunc {
|
||||
return func(ctx *server.Context) error {
|
||||
reqCtx, route, err := newCompatRouteObjects(ctx, deps, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return handleMockAuthFullStripeProfile(reqCtx, route)
|
||||
}
|
||||
}
|
||||
|
||||
func MockAuthStripeProfileAction(deps Dependencies, cfg CompatRouteConfig) server.HandlerFunc {
|
||||
return func(ctx *server.Context) error {
|
||||
reqCtx, route, err := newCompatRouteObjects(ctx, deps, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return handleMockAuthStripeProfile(reqCtx, route)
|
||||
}
|
||||
}
|
||||
|
||||
func MockAuthPollAction(deps Dependencies, cfg CompatRouteConfig) server.HandlerFunc {
|
||||
return func(ctx *server.Context) error {
|
||||
reqCtx, route, err := newCompatRouteObjects(ctx, deps, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return handleMockAuthPoll(reqCtx, route)
|
||||
}
|
||||
}
|
||||
|
||||
func MockAuthEmailAction(deps Dependencies, cfg CompatRouteConfig) server.HandlerFunc {
|
||||
return func(ctx *server.Context) error {
|
||||
reqCtx, route, err := newCompatRouteObjects(ctx, deps, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return handleMockAuthEmail(reqCtx, route)
|
||||
}
|
||||
}
|
||||
|
||||
func MockProtoAction(deps Dependencies, cfg CompatRouteConfig) server.HandlerFunc {
|
||||
return func(ctx *server.Context) error {
|
||||
reqCtx, route, err := newCompatRouteObjects(ctx, deps, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return handleMockProto(reqCtx, route)
|
||||
}
|
||||
}
|
||||
|
||||
func newCompatRouteObjects(ctx *server.Context, deps Dependencies, cfg CompatRouteConfig) (*RequestContext, *Route, error) {
|
||||
if ctx == nil || ctx.Request == nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
body, err := io.ReadAll(ctx.Request.Body)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
ctx.Request.Body = io.NopCloser(bytes.NewReader(body))
|
||||
targetURL := ctx.UpstreamURL
|
||||
if targetURL == nil && ctx.Request.URL != nil {
|
||||
copyURL := *ctx.Request.URL
|
||||
targetURL = ©URL
|
||||
}
|
||||
reqCtx := &RequestContext{
|
||||
ResponseWriter: ctx.Writer,
|
||||
Request: ctx.Request,
|
||||
StartedAt: ctx.StartedAt,
|
||||
RawURL: strings.TrimSpace(ctx.Request.Header.Get(server.HeaderServerUpstreamURL)),
|
||||
TargetURL: targetURL,
|
||||
Method: strings.ToUpper(strings.TrimSpace(ctx.Request.Method)),
|
||||
Headers: ctx.Request.Header.Clone(),
|
||||
ContentType: strings.TrimSpace(ctx.Request.Header.Get("content-type")),
|
||||
RequestBody: body,
|
||||
Mode: ctx.Mode,
|
||||
Deps: &deps,
|
||||
HTTPRequestID: resolveHTTPRequestID(ctx.Request),
|
||||
}
|
||||
route := &Route{
|
||||
Name: cfg.Name,
|
||||
Pattern: ctx.Request.URL.Path,
|
||||
StatusCode: cfg.StatusCode,
|
||||
JSONBody: cfg.JSONBody,
|
||||
MockProtoType: cfg.MockProtoType,
|
||||
MockPayloadBuilder: cfg.MockBuilder,
|
||||
ConsoleLog: cfg.ConsoleLog,
|
||||
}
|
||||
return reqCtx, route, nil
|
||||
}
|
||||
|
||||
func ServerTimeMockBuilder(reqCtx *RequestContext) (map[string]any, error) {
|
||||
return buildServerTimePayload(reqCtx)
|
||||
}
|
||||
|
||||
func ServerConfigMockBuilder(reqCtx *RequestContext) (map[string]any, error) {
|
||||
return buildServerConfigPayload(reqCtx)
|
||||
}
|
||||
|
||||
func AvailableModelsMockBuilder(reqCtx *RequestContext) (map[string]any, error) {
|
||||
return buildAvailableModelsPayload(reqCtx)
|
||||
}
|
||||
|
||||
func DefaultModelNudgeMockBuilder(reqCtx *RequestContext) (map[string]any, error) {
|
||||
return buildDefaultModelNudgeDataPayload(reqCtx)
|
||||
}
|
||||
|
||||
func BootstrapStatsigMockBuilder(reqCtx *RequestContext) (map[string]any, error) {
|
||||
return buildBootstrapStatsigPayload(reqCtx)
|
||||
}
|
||||
|
||||
func FirstWindowStatsigDecisionMockBuilder(reqCtx *RequestContext) (map[string]any, error) {
|
||||
return buildFirstWindowStatsigDecisionPayload(reqCtx)
|
||||
}
|
||||
|
||||
func DashboardCurrentPeriodUsageMockBuilder(reqCtx *RequestContext) (map[string]any, error) {
|
||||
return buildDashboardCurrentPeriodUsagePayload(reqCtx)
|
||||
}
|
||||
|
||||
func DashboardTeamsMockBuilder(reqCtx *RequestContext) (map[string]any, error) {
|
||||
return buildDashboardTeamsPayload(reqCtx)
|
||||
}
|
||||
|
||||
func DashboardManagedSkillsMockBuilder(reqCtx *RequestContext) (map[string]any, error) {
|
||||
return buildDashboardManagedSkillsPayload(reqCtx)
|
||||
}
|
||||
|
||||
func DashboardGetMeMockBuilder(reqCtx *RequestContext) (map[string]any, error) {
|
||||
return buildDashboardGetMePayload(reqCtx)
|
||||
}
|
||||
|
||||
func DashboardUserPrivacyModeMockBuilder(reqCtx *RequestContext) (map[string]any, error) {
|
||||
return buildDashboardUserPrivacyModePayload(reqCtx)
|
||||
}
|
||||
|
||||
func DashboardPlanInfoMockBuilder(reqCtx *RequestContext) (map[string]any, error) {
|
||||
return buildDashboardPlanInfoPayload(reqCtx)
|
||||
}
|
||||
|
||||
func DashboardUsageLimitStatusAndActiveGrantsMockBuilder(reqCtx *RequestContext) (map[string]any, error) {
|
||||
return buildDashboardUsageLimitStatusAndActiveGrantsPayload(reqCtx)
|
||||
}
|
||||
|
||||
func DashboardIsOnNewPricingMockBuilder(reqCtx *RequestContext) (map[string]any, error) {
|
||||
return buildDashboardIsOnNewPricingPayload(reqCtx)
|
||||
}
|
||||
|
||||
func resolveHTTPRequestID(request *http.Request) string {
|
||||
requestID := strings.TrimSpace(request.Header.Get("x-request-id"))
|
||||
if requestID != "" {
|
||||
return requestID
|
||||
}
|
||||
return strings.ReplaceAll(time.Now().UTC().Format(time.RFC3339Nano), ":", "-")
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
package upstream
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cursor/gen/aiserverv1"
|
||||
"cursor/internal/backend/server"
|
||||
"cursor/internal/logger"
|
||||
"cursor/internal/netproxy"
|
||||
legacyruntime "cursor/internal/runtime"
|
||||
|
||||
"google.golang.org/protobuf/encoding/protojson"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
var hopByHopHeaders = map[string]struct{}{
|
||||
"connection": {},
|
||||
"proxy-connection": {},
|
||||
"keep-alive": {},
|
||||
"proxy-authenticate": {},
|
||||
"proxy-authorization": {},
|
||||
"te": {},
|
||||
"trailer": {},
|
||||
"transfer-encoding": {},
|
||||
"upgrade": {},
|
||||
}
|
||||
|
||||
func ForwardToUpstream(reqCtx *RequestContext, options ForwardOptions) (*ForwardMeta, error) {
|
||||
requestBody := reqCtx.RequestBody
|
||||
if options.BodyOverride != nil {
|
||||
requestBody = options.BodyOverride
|
||||
}
|
||||
if !shouldRequestCarryBody(reqCtx.Method) {
|
||||
requestBody = []byte{}
|
||||
}
|
||||
|
||||
upstreamRequest, upstreamClient, err := buildUpstreamRequest(reqCtx, requestBody, options)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
upstreamResponse, err := upstreamClient.Do(upstreamRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer upstreamResponse.Body.Close()
|
||||
|
||||
copyResponseHeadersToClient(reqCtx.ResponseWriter.Header(), upstreamResponse.Header)
|
||||
reqCtx.ResponseWriter.WriteHeader(upstreamResponse.StatusCode)
|
||||
|
||||
written, copyErr := copyResponse(reqCtx.ResponseWriter, upstreamResponse.Body)
|
||||
meta := &ForwardMeta{
|
||||
StatusCode: upstreamResponse.StatusCode,
|
||||
Status: upstreamResponse.Status,
|
||||
ContentType: upstreamResponse.Header.Get("content-type"),
|
||||
ResponseSize: written,
|
||||
}
|
||||
if copyErr != nil {
|
||||
return meta, copyErr
|
||||
}
|
||||
return meta, nil
|
||||
}
|
||||
|
||||
func buildUpstreamRequest(reqCtx *RequestContext, body []byte, options ForwardOptions) (*http.Request, HTTPClient, error) {
|
||||
upstreamRequest, err := http.NewRequestWithContext(reqCtx.Request.Context(), reqCtx.Method, reqCtx.TargetURL.String(), bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("create upstream request failed: %w", err)
|
||||
}
|
||||
|
||||
copyRequestHeadersForUpstream(upstreamRequest.Header, reqCtx.Headers)
|
||||
upstreamRequest.Header.Del(HeaderRawServerURL)
|
||||
if !shouldRequestCarryBody(reqCtx.Method) {
|
||||
upstreamRequest.Header.Del("content-length")
|
||||
} else {
|
||||
upstreamRequest.Header.Set("content-length", strconv.Itoa(len(body)))
|
||||
}
|
||||
upstreamRequest.Host = reqCtx.TargetURL.Host
|
||||
|
||||
if reqCtx.Mode == server.ModeLocal && shouldRewriteHost(reqCtx.TargetURL.Hostname()) {
|
||||
auth := formatBearerAuthorization(legacyruntime.LocalRelayToken)
|
||||
if auth == "" {
|
||||
return nil, nil, legacyruntime.ErrInvalidSystemSetting
|
||||
}
|
||||
upstreamRequest.Header.Set("Authorization", auth)
|
||||
upstreamRequest.Header.Set("x-cursor-checksum", BuildCursorChecksum(auth))
|
||||
}
|
||||
if options.PatchHeaders != nil {
|
||||
options.PatchHeaders(upstreamRequest.Header)
|
||||
}
|
||||
|
||||
upstreamClient := reqCtx.Deps.HTTPClient
|
||||
if upstreamClient == nil {
|
||||
upstreamClient = netproxy.NewHTTPClient(0)
|
||||
}
|
||||
|
||||
return upstreamRequest, upstreamClient, nil
|
||||
}
|
||||
|
||||
func copyResponse(writer io.Writer, reader io.Reader) (int64, error) {
|
||||
buffer := make([]byte, 32*1024)
|
||||
var total int64
|
||||
|
||||
for {
|
||||
readCount, readErr := reader.Read(buffer)
|
||||
if readCount > 0 {
|
||||
chunk := buffer[:readCount]
|
||||
written, writeErr := writer.Write(chunk)
|
||||
total += int64(written)
|
||||
if writeErr != nil {
|
||||
return total, writeErr
|
||||
}
|
||||
if written < len(chunk) {
|
||||
return total, io.ErrShortWrite
|
||||
}
|
||||
if flusher, ok := writer.(http.Flusher); ok {
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
if readErr != nil {
|
||||
if readErr == io.EOF {
|
||||
return total, nil
|
||||
}
|
||||
return total, readErr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ParseAndValidateRawURL(raw string) (*url.URL, error) {
|
||||
value := strings.TrimSpace(raw)
|
||||
if value == "" {
|
||||
return nil, fmt.Errorf("empty raw url")
|
||||
}
|
||||
parsed, err := url.Parse(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if parsed.Scheme != "http" && parsed.Scheme != "https" {
|
||||
return nil, fmt.Errorf("unsupported scheme %q", parsed.Scheme)
|
||||
}
|
||||
if strings.TrimSpace(parsed.Host) == "" {
|
||||
return nil, fmt.Errorf("empty host")
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func copyRequestHeadersForUpstream(target http.Header, source http.Header) {
|
||||
for key, values := range source {
|
||||
lowerKey := strings.ToLower(key)
|
||||
if _, exists := hopByHopHeaders[lowerKey]; exists {
|
||||
continue
|
||||
}
|
||||
for _, value := range values {
|
||||
target.Add(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func copyResponseHeadersToClient(target http.Header, source http.Header) {
|
||||
for key, values := range source {
|
||||
lowerKey := strings.ToLower(key)
|
||||
if _, exists := hopByHopHeaders[lowerKey]; exists {
|
||||
continue
|
||||
}
|
||||
for _, value := range values {
|
||||
target.Add(key, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func shouldRewriteHost(host string) bool {
|
||||
normalized := strings.TrimSuffix(strings.ToLower(strings.TrimSpace(host)), ".")
|
||||
if normalized == "" {
|
||||
return false
|
||||
}
|
||||
return normalized == "cursor.sh" || strings.HasSuffix(normalized, ".cursor.sh")
|
||||
}
|
||||
|
||||
func BuildCursorChecksum(authorization string) string {
|
||||
const (
|
||||
checksumTimestampDivisor = 1_000_000
|
||||
checksumInitialSeed = 165
|
||||
)
|
||||
timestamp := time.Now().UnixMilli() / checksumTimestampDivisor
|
||||
timestampBytes := make([]byte, 6)
|
||||
timestampBigInt := big.NewInt(timestamp)
|
||||
for index := 0; index < len(timestampBytes); index++ {
|
||||
shift := uint((len(timestampBytes) - 1 - index) * 8)
|
||||
timestampBytes[index] = byte(new(big.Int).Rsh(timestampBigInt, shift).Uint64() & 0xff)
|
||||
}
|
||||
seed := checksumInitialSeed
|
||||
for index := 0; index < len(timestampBytes); index++ {
|
||||
current := int(timestampBytes[index]^byte(seed)) + (index % 256)
|
||||
current &= 0xff
|
||||
timestampBytes[index] = byte(current)
|
||||
seed = current
|
||||
}
|
||||
prefix := strings.TrimRight(base64.StdEncoding.EncodeToString(timestampBytes), "=")
|
||||
hashBytes := sha256.Sum256([]byte(strings.TrimSpace(authorization)))
|
||||
hash := fmt.Sprintf("%x", hashBytes)
|
||||
return prefix + hash[:32]
|
||||
}
|
||||
|
||||
func formatBearerAuthorization(raw string) string {
|
||||
value := strings.TrimSpace(raw)
|
||||
if value == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(strings.ToLower(value), "bearer ") {
|
||||
return value
|
||||
}
|
||||
return "Bearer " + value
|
||||
}
|
||||
|
||||
func shouldRequestCarryBody(method string) bool {
|
||||
switch strings.ToUpper(strings.TrimSpace(method)) {
|
||||
case http.MethodGet, http.MethodHead, http.MethodDelete:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func marshalJSONBody(payload map[string]any) ([]byte, error) {
|
||||
if payload == nil {
|
||||
return []byte("{}"), nil
|
||||
}
|
||||
return json.Marshal(payload)
|
||||
}
|
||||
|
||||
func handleMockJSON(reqCtx *RequestContext, route *Route) error {
|
||||
responseBody, err := marshalJSONBody(route.JSONBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reqCtx.ResponseWriter.Header().Set("content-type", "application/json")
|
||||
reqCtx.ResponseWriter.WriteHeader(route.StatusCode)
|
||||
_, _ = reqCtx.ResponseWriter.Write(responseBody)
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleMockProto(reqCtx *RequestContext, route *Route) error {
|
||||
payload := map[string]any{}
|
||||
if route.MockPayloadBuilder != nil {
|
||||
built, err := route.MockPayloadBuilder(reqCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload = built
|
||||
}
|
||||
responseBody, err := encodeMockProto(route.MockProtoType, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reqCtx.ResponseWriter.Header().Set("content-type", "application/proto")
|
||||
reqCtx.ResponseWriter.Header().Del("content-encoding")
|
||||
reqCtx.ResponseWriter.Header().Set("content-length", strconv.Itoa(len(responseBody)))
|
||||
reqCtx.ResponseWriter.WriteHeader(route.StatusCode)
|
||||
_, _ = reqCtx.ResponseWriter.Write(responseBody)
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleMockOAuth(reqCtx *RequestContext, route *Route) error {
|
||||
payload := struct {
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}{}
|
||||
_ = json.Unmarshal(reqCtx.RequestBody, &payload)
|
||||
responseBody, err := marshalJSONBody(map[string]any{
|
||||
"access_token": payload.RefreshToken,
|
||||
"id_token": payload.RefreshToken,
|
||||
"shouldLogout": false,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reqCtx.ResponseWriter.Header().Set("content-type", "application/json")
|
||||
reqCtx.ResponseWriter.WriteHeader(http.StatusOK)
|
||||
_, _ = reqCtx.ResponseWriter.Write(responseBody)
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleMockAuthFullStripeProfile(reqCtx *RequestContext, route *Route) error {
|
||||
_ = route
|
||||
responseBody, err := marshalJSONBody(map[string]any{
|
||||
"membershipType": localUltraMembershipType,
|
||||
"subscriptionStatus": localUltraSubscriptionStatus,
|
||||
"lastPaymentFailed": false,
|
||||
"pendingCancellationDate": "",
|
||||
"daysRemainingOnTrial": 0,
|
||||
"paymentId": localUltraPaymentID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reqCtx.ResponseWriter.Header().Set("content-type", "application/json")
|
||||
reqCtx.ResponseWriter.WriteHeader(http.StatusOK)
|
||||
_, _ = reqCtx.ResponseWriter.Write(responseBody)
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleMockAuthStripeProfile(reqCtx *RequestContext, route *Route) error {
|
||||
_ = route
|
||||
responseBody, err := json.Marshal(localUltraPaymentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reqCtx.ResponseWriter.Header().Set("content-type", "application/json")
|
||||
reqCtx.ResponseWriter.WriteHeader(http.StatusOK)
|
||||
_, _ = reqCtx.ResponseWriter.Write(responseBody)
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleMockAuthPoll(reqCtx *RequestContext, route *Route) error {
|
||||
_ = route
|
||||
responseBody, err := marshalJSONBody(map[string]any{
|
||||
"accessToken": legacyruntime.InjectAuthToken,
|
||||
"refreshToken": legacyruntime.InjectAuthToken,
|
||||
"authId": "local_auth",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reqCtx.ResponseWriter.Header().Set("content-type", "application/json")
|
||||
reqCtx.ResponseWriter.WriteHeader(http.StatusOK)
|
||||
_, _ = reqCtx.ResponseWriter.Write(responseBody)
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleMockAuthEmail(reqCtx *RequestContext, route *Route) error {
|
||||
_ = route
|
||||
responseBody := encodeAuthGetEmailResponse(legacyruntime.InjectAccountEmail)
|
||||
reqCtx.ResponseWriter.Header().Set("content-type", "application/proto")
|
||||
reqCtx.ResponseWriter.Header().Set("content-length", strconv.Itoa(len(responseBody)))
|
||||
reqCtx.ResponseWriter.WriteHeader(http.StatusOK)
|
||||
_, _ = reqCtx.ResponseWriter.Write(responseBody)
|
||||
return nil
|
||||
}
|
||||
|
||||
func encodeAuthGetEmailResponse(email string) []byte {
|
||||
output := make([]byte, 0, len(email)+8)
|
||||
output = append(output, 0x0a)
|
||||
output = appendProtoVarint(output, uint64(len(email)))
|
||||
output = append(output, []byte(email)...)
|
||||
output = append(output, 0x10, 0x03) // GetEmailResponse.SignUpType.SIGN_UP_TYPE_GOOGLE
|
||||
return output
|
||||
}
|
||||
|
||||
func appendProtoVarint(output []byte, value uint64) []byte {
|
||||
for value >= 0x80 {
|
||||
output = append(output, byte(value)|0x80)
|
||||
value >>= 7
|
||||
}
|
||||
return append(output, byte(value))
|
||||
}
|
||||
|
||||
func handleFixedStatus(reqCtx *RequestContext, route *Route) error {
|
||||
if route != nil && route.ConsoleLog {
|
||||
logger.Infof("backend server fixed-status route hit name=%s method=%s path=%s raw_url=%s status=%d", route.Name, reqCtx.Method, reqCtx.TargetURL.Path, reqCtx.RawURL, route.StatusCode)
|
||||
}
|
||||
writeFixedStatus(reqCtx, route.StatusCode)
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeFixedStatus(reqCtx *RequestContext, statusCode int) {
|
||||
if reqCtx == nil || reqCtx.ResponseWriter == nil {
|
||||
return
|
||||
}
|
||||
reqCtx.ResponseWriter.WriteHeader(statusCode)
|
||||
}
|
||||
|
||||
func handleDirect(reqCtx *RequestContext, route *Route) error {
|
||||
_ = route
|
||||
_, err := ForwardToUpstream(reqCtx, ForwardOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
func encodeMockProto(typeName string, payload map[string]any) ([]byte, error) {
|
||||
message, err := newProtoMessage(typeName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
data, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := (protojson.UnmarshalOptions{DiscardUnknown: false}).Unmarshal(data, message); err != nil {
|
||||
return nil, fmt.Errorf("mock proto json decode failed: %w", err)
|
||||
}
|
||||
return proto.Marshal(message)
|
||||
}
|
||||
|
||||
func newProtoMessage(typeName string) (proto.Message, error) {
|
||||
switch strings.TrimSpace(typeName) {
|
||||
case "aiserver.v1.ServerTimeResponse":
|
||||
return &aiserverv1.ServerTimeResponse{}, nil
|
||||
case "aiserver.v1.GetServerConfigResponse":
|
||||
return &aiserverv1.GetServerConfigResponse{}, nil
|
||||
case "aiserver.v1.AvailableModelsResponse":
|
||||
return &aiserverv1.AvailableModelsResponse{}, nil
|
||||
case "aiserver.v1.GetDefaultModelNudgeDataResponse":
|
||||
return &aiserverv1.GetDefaultModelNudgeDataResponse{}, nil
|
||||
case "aiserver.v1.BootstrapStatsigResponse":
|
||||
return &aiserverv1.BootstrapStatsigResponse{}, nil
|
||||
case "aiserver.v1.GetFirstWindowStatsigDecisionResponse":
|
||||
return &aiserverv1.GetFirstWindowStatsigDecisionResponse{}, nil
|
||||
case "aiserver.v1.GetCurrentPeriodUsageResponse":
|
||||
return &aiserverv1.GetCurrentPeriodUsageResponse{}, nil
|
||||
case "aiserver.v1.GetTeamsResponse":
|
||||
return &aiserverv1.GetTeamsResponse{}, nil
|
||||
case "aiserver.v1.GetMeResponse":
|
||||
return &aiserverv1.GetMeResponse{}, nil
|
||||
case "aiserver.v1.GetUserPrivacyModeResponse":
|
||||
return &aiserverv1.GetUserPrivacyModeResponse{}, nil
|
||||
case "aiserver.v1.GetPlanInfoResponse":
|
||||
return &aiserverv1.GetPlanInfoResponse{}, nil
|
||||
case "aiserver.v1.GetUsageLimitStatusAndActiveGrantsResponse":
|
||||
return &aiserverv1.GetUsageLimitStatusAndActiveGrantsResponse{}, nil
|
||||
case "aiserver.v1.IsOnNewPricingResponse":
|
||||
return &aiserverv1.IsOnNewPricingResponse{}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported proto message type %q", typeName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,886 @@
|
||||
package upstream
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"html"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
legacyruntime "cursor/internal/runtime"
|
||||
)
|
||||
|
||||
const (
|
||||
availableModelsDisableUnusedHours = 2400000
|
||||
availableModelsUpgradeHours = 2
|
||||
|
||||
modelRuntimeThinkingEffortParameterID = "thinking_effort"
|
||||
|
||||
localUltraMembershipType = "ultra"
|
||||
localUltraPaymentID = "local_ultra"
|
||||
localUltraSubscriptionStatus = "active"
|
||||
localUltraPlanIncludedCents = 20000
|
||||
localUltraDashboardUserID = 1
|
||||
localUltraBillingCycleDuration = 30 * 24 * time.Hour
|
||||
|
||||
bootstrapStatsigGlassModeAvailableGate = "glass_mode_available"
|
||||
bootstrapStatsigGlassOpenAgentInWindowGate = "glass.enable_open_agent_in_window"
|
||||
bootstrapStatsigOpenAgentsTitlebarGate = "glass_open_agents_titlebar_button"
|
||||
bootstrapStatsigOpenAgentWindowTopGate = "open_agent_window_top"
|
||||
bootstrapStatsigOpenAgentWindowBottomGate = "open_agent_window_bottom_convo"
|
||||
bootstrapStatsigNALAgentRetriesGate = "nal_agent_retries"
|
||||
bootstrapStatsigNALFreshRetryIDsGate = "nal_fresh_retry_ids"
|
||||
bootstrapStatsigUseModelParametersGate = "use_model_parameters"
|
||||
bootstrapStatsigUseReactModelPickerGate = "use_react_model_picker"
|
||||
bootstrapStatsigIDECmdEnterSubmitGate = "ide_cmd_enter_submit"
|
||||
bootstrapStatsigContextVisualizerGate = "context_visualizer"
|
||||
bootstrapStatsigWysiwygMarkdownGate = "wysiwyg_markdown"
|
||||
bootstrapStatsigWysiwygMarkdownDefaultGate = "wysiwyg_markdown_default"
|
||||
bootstrapStatsigSubagentSupportInterrupt = "subagent_support_interrupt"
|
||||
bootstrapStatsigExplicitSubagentModels = "explicit_subagent_models"
|
||||
bootstrapStatsigMcpDirectClientToolFetch = "mcp_direct_client_tool_fetch"
|
||||
bootstrapStatsigGlassCustomThemeSupport = "glass_custom_theme_support"
|
||||
bootstrapStatsigGlassAutomationsUI = "glass_automations_ui"
|
||||
bootstrapStatsigTerminalUI2 = "terminal_ui_2"
|
||||
bootstrapStatsigDisableTerminalOutputUIStreaming = "disable_terminal_output_ui_streaming"
|
||||
bootstrapStatsigBrowserCanvas = "browser_canvas"
|
||||
bootstrapStatsigEnableMultitaskMode = "enable_multitask_mode"
|
||||
bootstrapStatsigCursorAgentWorkerExtension = "enable_cursor_agent_worker_extension"
|
||||
bootstrapStatsigExperimentName = "free_user_model_picker"
|
||||
bootstrapStatsigVariantParam = "variant"
|
||||
bootstrapStatsigVariantControl = "control"
|
||||
bootstrapStatsigVariantLockedPicker = "locked_picker"
|
||||
bootstrapStatsigVariantGrayedModels = "grayed_models"
|
||||
bootstrapStatsigProductTipsConfigName = "product_tips_config"
|
||||
bootstrapStatsigIdleExtensionHostKiller = "idle_extension_host_killer_config"
|
||||
bootstrapStatsigIdleMinutesToKill = "idleMinutesToKillExtensionHost"
|
||||
bootstrapStatsigFreeMemoryPercentageToKill = "freeMemoryPercentageToKillExtensionHost"
|
||||
bootstrapStatsigHTTP2PingConfig = "http2_ping_config"
|
||||
bootstrapStatsigHTTP1KeepaliveConfig = "http1_keepalive_config"
|
||||
bootstrapStatsigHTTP2AgentPoolConfig = "http2_agent_connection_pool_config"
|
||||
bootstrapStatsigCanvasPromptTextConfig = "canvas_prompt_text_config"
|
||||
bootstrapStatsigEditorBugbotConfig = "editor_bugbot_config"
|
||||
bootstrapStatsigExtensionMonitorControl = "extension_monitor_control"
|
||||
bootstrapStatsigExtensionSignatureBypass = "extension_signature_verification_bypass_list"
|
||||
bootstrapStatsigGCTraceControl = "gc_trace_control"
|
||||
bootstrapStatsigInlineDiffPerformance = "inline_diff_performance_config"
|
||||
bootstrapStatsigLeakedDisposablesTracker = "leaked_disposables_tracker"
|
||||
bootstrapStatsigMcpIPCTimeouts = "mcp_ipc_timeouts"
|
||||
bootstrapStatsigMcpWakeProbeConfig = "mcp_wake_probe_config"
|
||||
bootstrapStatsigNALStallDetectorTimeout = "nal_stall_detector_timeout_config"
|
||||
bootstrapStatsigSimulatedThinkingErrorTimeout = "simulated_thinking_error_timeout"
|
||||
bootstrapStatsigPlaywrightLogConfigs = "playwright_log_configs"
|
||||
bootstrapStatsigRetryInterceptorParams = "retry_interceptor_params_config"
|
||||
bootstrapStatsigSandboxNetworkAllowlist = "sandbox_default_network_allowlist"
|
||||
bootstrapStatsigUpdatePromptConfig = "update_prompt_config"
|
||||
bootstrapStatsigLocalDefaultRule = "local_default"
|
||||
)
|
||||
|
||||
type statsigSecondaryExposure struct {
|
||||
Gate string `json:"gate,omitempty"`
|
||||
GateValue string `json:"gateValue,omitempty"`
|
||||
GateValueSnake string `json:"gate_value,omitempty"`
|
||||
RuleID string `json:"ruleID,omitempty"`
|
||||
RuleIDSnake string `json:"rule_id,omitempty"`
|
||||
}
|
||||
|
||||
type statsigDynamicConfigTemplate struct {
|
||||
Name string `json:"name"`
|
||||
Value map[string]any `json:"value"`
|
||||
RuleID string `json:"rule_id"`
|
||||
RuleIDCamel string `json:"ruleID"`
|
||||
GroupName string `json:"group_name"`
|
||||
GroupNameCamel string `json:"groupName"`
|
||||
SecondaryExposures []statsigSecondaryExposure `json:"secondary_exposures"`
|
||||
SecondaryExposuresCamel []statsigSecondaryExposure `json:"secondaryExposures"`
|
||||
UndelegatedSecondaryExposures []statsigSecondaryExposure `json:"undelegated_secondary_exposures"`
|
||||
UndelegatedSecondaryExposuresCamel []statsigSecondaryExposure `json:"undelegatedSecondaryExposures"`
|
||||
IsDeviceBased bool `json:"is_device_based"`
|
||||
IsDeviceBasedCamel bool `json:"isDeviceBased"`
|
||||
IsExperimentActive bool `json:"is_experiment_active"`
|
||||
IsExperimentActiveCamel bool `json:"isExperimentActive"`
|
||||
IsUserInExperiment bool `json:"is_user_in_experiment"`
|
||||
IsUserInExperimentCamel bool `json:"isUserInExperiment"`
|
||||
}
|
||||
|
||||
type statsigBootstrapTemplate struct {
|
||||
FeatureGates map[string]map[string]any `json:"feature_gates"`
|
||||
DynamicConfigs map[string]statsigDynamicConfigTemplate `json:"dynamic_configs"`
|
||||
LayerConfigs map[string]map[string]any `json:"layer_configs"`
|
||||
User map[string]any `json:"user"`
|
||||
HasUpdates bool `json:"has_updates"`
|
||||
HashUsed string `json:"hash_used"`
|
||||
SDKParams map[string]any `json:"sdkParams"`
|
||||
Time int64 `json:"time"`
|
||||
}
|
||||
|
||||
var bootstrapStatsigTemplate = statsigBootstrapTemplate{
|
||||
FeatureGates: map[string]map[string]any{
|
||||
bootstrapStatsigGlassModeAvailableGate: buildEnabledStatsigGate(bootstrapStatsigGlassModeAvailableGate),
|
||||
bootstrapStatsigGlassOpenAgentInWindowGate: buildEnabledStatsigGate(bootstrapStatsigGlassOpenAgentInWindowGate),
|
||||
bootstrapStatsigOpenAgentsTitlebarGate: buildEnabledStatsigGate(bootstrapStatsigOpenAgentsTitlebarGate),
|
||||
bootstrapStatsigOpenAgentWindowTopGate: buildEnabledStatsigGate(bootstrapStatsigOpenAgentWindowTopGate),
|
||||
bootstrapStatsigOpenAgentWindowBottomGate: buildEnabledStatsigGate(bootstrapStatsigOpenAgentWindowBottomGate),
|
||||
bootstrapStatsigNALAgentRetriesGate: buildEnabledStatsigGate(bootstrapStatsigNALAgentRetriesGate),
|
||||
bootstrapStatsigNALFreshRetryIDsGate: buildEnabledStatsigGate(bootstrapStatsigNALFreshRetryIDsGate),
|
||||
bootstrapStatsigUseModelParametersGate: buildEnabledStatsigGate(bootstrapStatsigUseModelParametersGate),
|
||||
bootstrapStatsigUseReactModelPickerGate: buildEnabledStatsigGate(bootstrapStatsigUseReactModelPickerGate),
|
||||
bootstrapStatsigIDECmdEnterSubmitGate: buildEnabledStatsigGate(bootstrapStatsigIDECmdEnterSubmitGate),
|
||||
bootstrapStatsigContextVisualizerGate: buildEnabledStatsigGate(bootstrapStatsigContextVisualizerGate),
|
||||
bootstrapStatsigWysiwygMarkdownGate: buildEnabledStatsigGate(bootstrapStatsigWysiwygMarkdownGate),
|
||||
bootstrapStatsigWysiwygMarkdownDefaultGate: buildEnabledStatsigGate(bootstrapStatsigWysiwygMarkdownDefaultGate),
|
||||
bootstrapStatsigSubagentSupportInterrupt: buildEnabledStatsigGate(bootstrapStatsigSubagentSupportInterrupt),
|
||||
bootstrapStatsigExplicitSubagentModels: buildEnabledStatsigGate(bootstrapStatsigExplicitSubagentModels),
|
||||
bootstrapStatsigMcpDirectClientToolFetch: buildEnabledStatsigGate(bootstrapStatsigMcpDirectClientToolFetch),
|
||||
bootstrapStatsigGlassCustomThemeSupport: buildEnabledStatsigGate(bootstrapStatsigGlassCustomThemeSupport),
|
||||
bootstrapStatsigGlassAutomationsUI: buildEnabledStatsigGate(bootstrapStatsigGlassAutomationsUI),
|
||||
bootstrapStatsigTerminalUI2: buildEnabledStatsigGate(bootstrapStatsigTerminalUI2),
|
||||
bootstrapStatsigDisableTerminalOutputUIStreaming: buildEnabledStatsigGate(bootstrapStatsigDisableTerminalOutputUIStreaming),
|
||||
bootstrapStatsigBrowserCanvas: buildEnabledStatsigGate(bootstrapStatsigBrowserCanvas),
|
||||
bootstrapStatsigEnableMultitaskMode: buildEnabledStatsigGate(bootstrapStatsigEnableMultitaskMode),
|
||||
bootstrapStatsigCursorAgentWorkerExtension: buildDisabledStatsigGate(bootstrapStatsigCursorAgentWorkerExtension),
|
||||
},
|
||||
DynamicConfigs: map[string]statsigDynamicConfigTemplate{
|
||||
bootstrapStatsigExperimentName: buildStatsigDynamicConfig(
|
||||
bootstrapStatsigExperimentName,
|
||||
map[string]any{bootstrapStatsigVariantParam: bootstrapStatsigVariantControl},
|
||||
bootstrapStatsigVariantControl,
|
||||
),
|
||||
bootstrapStatsigProductTipsConfigName: buildStatsigDynamicConfig(
|
||||
bootstrapStatsigProductTipsConfigName,
|
||||
map[string]any{
|
||||
"tips": []map[string]any{},
|
||||
"config": map[string]any{
|
||||
"intervalMs": 8000,
|
||||
"minClientVersion": "",
|
||||
},
|
||||
},
|
||||
bootstrapStatsigLocalDefaultRule,
|
||||
),
|
||||
bootstrapStatsigIdleExtensionHostKiller: buildStatsigDynamicConfig(
|
||||
bootstrapStatsigIdleExtensionHostKiller,
|
||||
map[string]any{
|
||||
bootstrapStatsigIdleMinutesToKill: 0,
|
||||
bootstrapStatsigFreeMemoryPercentageToKill: 0,
|
||||
},
|
||||
bootstrapStatsigLocalDefaultRule,
|
||||
),
|
||||
bootstrapStatsigCanvasPromptTextConfig: buildStatsigDynamicConfig(
|
||||
bootstrapStatsigCanvasPromptTextConfig,
|
||||
buildCanvasPromptTextConfigValue(),
|
||||
bootstrapStatsigLocalDefaultRule,
|
||||
),
|
||||
bootstrapStatsigEditorBugbotConfig: buildStatsigDynamicConfig(
|
||||
bootstrapStatsigEditorBugbotConfig,
|
||||
map[string]any{
|
||||
"model": "claude-4-5-sonnet-20250929",
|
||||
"iterations": 0,
|
||||
"agentic_iterations": 1,
|
||||
"agentic_model": "claude-4.5-haiku",
|
||||
"context_lines": 10,
|
||||
},
|
||||
bootstrapStatsigLocalDefaultRule,
|
||||
),
|
||||
bootstrapStatsigExtensionMonitorControl: buildStatsigDynamicConfig(
|
||||
bootstrapStatsigExtensionMonitorControl,
|
||||
map[string]any{
|
||||
"local_enabled": false,
|
||||
"backend_reporting_enabled": false,
|
||||
"subsample_polling_rate_sec": 0,
|
||||
"sample_polling_rate_min": 0,
|
||||
},
|
||||
bootstrapStatsigLocalDefaultRule,
|
||||
),
|
||||
bootstrapStatsigExtensionSignatureBypass: buildStatsigDynamicConfig(
|
||||
bootstrapStatsigExtensionSignatureBypass,
|
||||
map[string]any{
|
||||
"extensionIds": []string{
|
||||
"nromanov.dotrush",
|
||||
"ms-python.python",
|
||||
"typescriptteam.native-preview",
|
||||
"typespec.typespec-vscode",
|
||||
"ms-toolsai.jupyter",
|
||||
"k3ndr1ckfu.tcl-language-support-for-vscode",
|
||||
"amiq.dvt",
|
||||
},
|
||||
"remoteVerificationMinVersion": "2.25.0",
|
||||
},
|
||||
bootstrapStatsigLocalDefaultRule,
|
||||
),
|
||||
bootstrapStatsigGCTraceControl: buildStatsigDynamicConfig(
|
||||
bootstrapStatsigGCTraceControl,
|
||||
map[string]any{
|
||||
"enabled": false,
|
||||
"drain_interval_sec": 120,
|
||||
},
|
||||
bootstrapStatsigLocalDefaultRule,
|
||||
),
|
||||
bootstrapStatsigInlineDiffPerformance: buildStatsigDynamicConfig(
|
||||
bootstrapStatsigInlineDiffPerformance,
|
||||
map[string]any{
|
||||
"maxDecorations": 100,
|
||||
},
|
||||
bootstrapStatsigLocalDefaultRule,
|
||||
),
|
||||
bootstrapStatsigLeakedDisposablesTracker: buildStatsigDynamicConfig(
|
||||
bootstrapStatsigLeakedDisposablesTracker,
|
||||
map[string]any{
|
||||
"enabled": false,
|
||||
"reportIntervalMs": 60000,
|
||||
},
|
||||
bootstrapStatsigLocalDefaultRule,
|
||||
),
|
||||
bootstrapStatsigMcpIPCTimeouts: buildStatsigDynamicConfig(
|
||||
bootstrapStatsigMcpIPCTimeouts,
|
||||
map[string]any{
|
||||
"metadata_timeout_ms": 10000,
|
||||
"lifecycle_timeout_ms": 10000,
|
||||
"dashboard_timeout_ms": 10000,
|
||||
"recovery_per_retry_timeout_ms": 10000,
|
||||
},
|
||||
bootstrapStatsigLocalDefaultRule,
|
||||
),
|
||||
bootstrapStatsigMcpWakeProbeConfig: buildStatsigDynamicConfig(
|
||||
bootstrapStatsigMcpWakeProbeConfig,
|
||||
map[string]any{
|
||||
"probeOnFocus": true,
|
||||
"probeOnBrowserOnline": true,
|
||||
"probeOnElapsedTimeGap": true,
|
||||
"elapsedTimeGapThresholdMs": 300000,
|
||||
"focusProbeDebounceMs": 60000,
|
||||
"onlineProbeDebounceMs": 5000,
|
||||
"resumeProbeDebounceMs": 5000,
|
||||
"startupGraceMs": 15000,
|
||||
"minProbeIntervalMs": 30000,
|
||||
},
|
||||
bootstrapStatsigLocalDefaultRule,
|
||||
),
|
||||
bootstrapStatsigNALStallDetectorTimeout: buildStatsigDynamicConfig(
|
||||
bootstrapStatsigNALStallDetectorTimeout,
|
||||
map[string]any{
|
||||
"advisoryTimeoutMs": 20000,
|
||||
"failTimeoutMs": 30000,
|
||||
},
|
||||
bootstrapStatsigLocalDefaultRule,
|
||||
),
|
||||
bootstrapStatsigSimulatedThinkingErrorTimeout: buildStatsigDynamicConfig(
|
||||
bootstrapStatsigSimulatedThinkingErrorTimeout,
|
||||
map[string]any{
|
||||
"timeout_ms": 120000,
|
||||
},
|
||||
bootstrapStatsigLocalDefaultRule,
|
||||
),
|
||||
bootstrapStatsigPlaywrightLogConfigs: buildStatsigDynamicConfig(
|
||||
bootstrapStatsigPlaywrightLogConfigs,
|
||||
map[string]any{
|
||||
"logSizeThreshold": 25000,
|
||||
"logPreviewLines": 25,
|
||||
"logPreviewChars": 25000,
|
||||
},
|
||||
bootstrapStatsigLocalDefaultRule,
|
||||
),
|
||||
bootstrapStatsigRetryInterceptorParams: buildStatsigDynamicConfig(
|
||||
bootstrapStatsigRetryInterceptorParams,
|
||||
map[string]any{},
|
||||
bootstrapStatsigLocalDefaultRule,
|
||||
),
|
||||
bootstrapStatsigUpdatePromptConfig: buildStatsigDynamicConfig(
|
||||
bootstrapStatsigUpdatePromptConfig,
|
||||
map[string]any{
|
||||
"min_hours_between_prompts": 48,
|
||||
"max_prompts_per_version": 3,
|
||||
"max_prompts_per_day": 1,
|
||||
"snooze_duration_hours": 72,
|
||||
},
|
||||
bootstrapStatsigLocalDefaultRule,
|
||||
),
|
||||
bootstrapStatsigHTTP2PingConfig: buildStatsigDynamicConfig(
|
||||
bootstrapStatsigHTTP2PingConfig,
|
||||
map[string]any{
|
||||
"enabled": []string{},
|
||||
"pingIdleConnection": nil,
|
||||
"pingIntervalMs": nil,
|
||||
"pingTimeoutMs": nil,
|
||||
"idleConnectionTimeoutMs": nil,
|
||||
},
|
||||
bootstrapStatsigLocalDefaultRule,
|
||||
),
|
||||
bootstrapStatsigHTTP1KeepaliveConfig: buildStatsigDynamicConfig(
|
||||
bootstrapStatsigHTTP1KeepaliveConfig,
|
||||
map[string]any{
|
||||
"keepAliveInitialDelayMs": nil,
|
||||
},
|
||||
bootstrapStatsigLocalDefaultRule,
|
||||
),
|
||||
bootstrapStatsigHTTP2AgentPoolConfig: buildStatsigDynamicConfig(
|
||||
bootstrapStatsigHTTP2AgentPoolConfig,
|
||||
map[string]any{
|
||||
"poolSize": 4,
|
||||
},
|
||||
bootstrapStatsigLocalDefaultRule,
|
||||
),
|
||||
},
|
||||
LayerConfigs: map[string]map[string]any{},
|
||||
User: map[string]any{
|
||||
"userID": localUltraPaymentID,
|
||||
"email": legacyruntime.InjectAccountEmail,
|
||||
"customIDs": map[string]string{
|
||||
"localUserID": localUltraPaymentID,
|
||||
},
|
||||
},
|
||||
HasUpdates: true,
|
||||
HashUsed: "none",
|
||||
SDKParams: map[string]any{
|
||||
"stableID": localUltraPaymentID,
|
||||
"disableDiagnosticsLogging": true,
|
||||
},
|
||||
}
|
||||
|
||||
func buildStatsigDynamicConfig(name string, value map[string]any, ruleID string) statsigDynamicConfigTemplate {
|
||||
name = strings.TrimSpace(name)
|
||||
ruleID = strings.TrimSpace(ruleID)
|
||||
if ruleID == "" {
|
||||
ruleID = bootstrapStatsigLocalDefaultRule
|
||||
}
|
||||
exposures := []statsigSecondaryExposure{}
|
||||
return statsigDynamicConfigTemplate{
|
||||
Name: name,
|
||||
Value: value,
|
||||
RuleID: ruleID,
|
||||
RuleIDCamel: ruleID,
|
||||
GroupName: ruleID,
|
||||
GroupNameCamel: ruleID,
|
||||
SecondaryExposures: exposures,
|
||||
SecondaryExposuresCamel: exposures,
|
||||
UndelegatedSecondaryExposures: exposures,
|
||||
UndelegatedSecondaryExposuresCamel: exposures,
|
||||
IsDeviceBased: false,
|
||||
IsDeviceBasedCamel: false,
|
||||
IsExperimentActive: false,
|
||||
IsExperimentActiveCamel: false,
|
||||
IsUserInExperiment: false,
|
||||
IsUserInExperimentCamel: false,
|
||||
}
|
||||
}
|
||||
|
||||
func buildCanvasPromptTextConfigValue() map[string]any {
|
||||
return map[string]any{
|
||||
"skillDescription": "A Cursor Canvas is a live React app that the user can open beside the chat. You MUST use a canvas when the agent produces a standalone analytical artifact \u2014 quantitative analyses, billing investigations, security audits, architecture reviews, data-heavy content, timelines, charts, tables, interactive explorations, repeatable tools, or any response that benefits from visual layout. Especially prefer a canvas when presenting results from MCP tools (Datadog, Databricks, Linear, Sentry, Slack, etc.) where the data is the deliverable \u2014 render it in a rich canvas rather than dumping it into a markdown table or code block. If you catch yourself about to write a markdown table, stop and use a canvas instead. You MUST also read this skill whenever you create, edit, or debug any .canvas.tsx file.",
|
||||
"errorFixPromptTemplate": strings.Join([]string{
|
||||
"The canvas at `{canvasPath}` has the following error:",
|
||||
"",
|
||||
`"""`,
|
||||
"{errorMessage}",
|
||||
`"""`,
|
||||
"",
|
||||
"Check if the canvas SDK has changed since this canvas was created.",
|
||||
"Update the canvas to use the latest SDK components according to the supplied documentation in the canvas skill.",
|
||||
}, "\n"),
|
||||
"welcomePageEnabled": true,
|
||||
"marketplaceCategoryKey": "canvas-featured",
|
||||
"marketplaceMaxCards": 4,
|
||||
}
|
||||
}
|
||||
|
||||
func buildEnabledStatsigGate(name string) map[string]any {
|
||||
return buildStatsigGate(name, true, "local_enabled")
|
||||
}
|
||||
|
||||
func buildDisabledStatsigGate(name string) map[string]any {
|
||||
return buildStatsigGate(name, false, "local_disabled")
|
||||
}
|
||||
|
||||
func buildStatsigGate(name string, value bool, ruleID string) map[string]any {
|
||||
return map[string]any{
|
||||
"name": name,
|
||||
"value": value,
|
||||
"rule_id": ruleID,
|
||||
"ruleID": ruleID,
|
||||
"group_name": ruleID,
|
||||
"groupName": ruleID,
|
||||
"secondary_exposures": []statsigSecondaryExposure{},
|
||||
"secondaryExposures": []statsigSecondaryExposure{},
|
||||
"undelegated_secondary_exposures": []statsigSecondaryExposure{},
|
||||
"undelegatedSecondaryExposures": []statsigSecondaryExposure{},
|
||||
"is_device_based": false,
|
||||
"isDeviceBased": false,
|
||||
"id_type": "userID",
|
||||
"idType": "userID",
|
||||
}
|
||||
}
|
||||
|
||||
func buildServerTimePayload(*RequestContext) (map[string]any, error) {
|
||||
now := float64(time.Now().UnixMilli())
|
||||
return map[string]any{
|
||||
"receiveTimestamp": now,
|
||||
"transmitTimestamp": now,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildServerConfigPayload(*RequestContext) (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"configVersion": "local_cli_sandbox_defaults_disabled_v2",
|
||||
// "http2Config": "HTTP2_CONFIG_FORCE_ALL_DISABLED",
|
||||
"cliSandboxDefaultEnabled": true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildAvailableModelsPayload(reqCtx *RequestContext) (map[string]any, error) {
|
||||
adapters, err := loadConfiguredModelAdapters(reqCtx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
modelRefs := collectModelAdapterRefs(adapters)
|
||||
defaultModel := ""
|
||||
if len(modelRefs) > 0 {
|
||||
defaultModel = modelRefs[0]
|
||||
}
|
||||
return map[string]any{
|
||||
"backgroundComposerModelConfig": map[string]any{
|
||||
"bestOfNDefaultModels": append([]string(nil), modelRefs...),
|
||||
"defaultModel": defaultModel,
|
||||
"fallbackModels": append([]string(nil), modelRefs...),
|
||||
},
|
||||
"cmdKModelConfig": map[string]any{
|
||||
"defaultModel": defaultModel,
|
||||
"fallbackModels": append([]string(nil), modelRefs...),
|
||||
},
|
||||
"composerModelConfig": map[string]any{
|
||||
"bestOfNDefaultModels": append([]string(nil), modelRefs...),
|
||||
"defaultModel": defaultModel,
|
||||
"fallbackModels": append([]string(nil), modelRefs...),
|
||||
},
|
||||
"deepSearchModelConfig": map[string]any{
|
||||
"defaultModel": defaultModel,
|
||||
},
|
||||
"disableUnusedModelsAfterNHours": availableModelsDisableUnusedHours,
|
||||
"models": buildAvailableModelEntries(adapters),
|
||||
"planExecutionModelConfig": map[string]any{
|
||||
"defaultModel": defaultModel,
|
||||
"fallbackModels": append([]string(nil), modelRefs...),
|
||||
},
|
||||
"quickAgentModelConfig": map[string]any{
|
||||
"defaultModel": defaultModel,
|
||||
},
|
||||
"specModelConfig": map[string]any{
|
||||
"defaultModel": defaultModel,
|
||||
},
|
||||
"useModelParameters": true,
|
||||
"upgradeUnchangedModelsAfterNHours": availableModelsUpgradeHours,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildDefaultModelNudgeDataPayload(reqCtx *RequestContext) (map[string]any, error) {
|
||||
adapters, err := loadConfiguredModelAdapters(reqCtx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]any{
|
||||
"modelsWithNoDefaultSwitch": collectModelAdapterRefs(adapters),
|
||||
"nudgeDate": "0",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildBootstrapStatsigPayload(reqCtx *RequestContext) (map[string]any, error) {
|
||||
generatedAtMs := uint64(time.Now().UnixMilli())
|
||||
authID := resolveBootstrapStatsigAuthID(reqCtx)
|
||||
configJSON, err := buildBootstrapStatsigConfigJSON(int64(generatedAtMs), authID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]any{
|
||||
"config": string(configJSON),
|
||||
"generatedAtMs": generatedAtMs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildFirstWindowStatsigDecisionPayload(*RequestContext) (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"variant": bootstrapStatsigVariantControl,
|
||||
"reason": bootstrapStatsigLocalDefaultRule,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildDashboardCurrentPeriodUsagePayload(*RequestContext) (map[string]any, error) {
|
||||
billingCycleStart := time.Now().Add(-localUltraBillingCycleDuration).UnixMilli()
|
||||
billingCycleEnd := time.Now().Add(10 * 365 * 24 * time.Hour).UnixMilli()
|
||||
return map[string]any{
|
||||
"autoModelSelectedDisplayMessage": "Ultra plan active",
|
||||
"billingCycleEnd": billingCycleEnd,
|
||||
"billingCycleStart": billingCycleStart,
|
||||
"displayMessage": "Ultra plan active",
|
||||
"displayThreshold": 99999999,
|
||||
"enabled": true,
|
||||
"namedModelSelectedDisplayMessage": "Ultra plan active",
|
||||
"planUsage": map[string]any{
|
||||
"apiPercentUsed": 0,
|
||||
"apiSpend": 0,
|
||||
"autoPercentUsed": 0,
|
||||
"autoSpend": 0,
|
||||
"bonusTooltip": "Ultra local account mock is active.",
|
||||
"includedSpend": localUltraPlanIncludedCents,
|
||||
"limit": localUltraPlanIncludedCents,
|
||||
"remaining": localUltraPlanIncludedCents,
|
||||
"remainingBonus": false,
|
||||
"totalPercentUsed": 0,
|
||||
"totalSpend": 0,
|
||||
},
|
||||
"spendLimitUsage": map[string]any{
|
||||
"limitType": "user",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildDashboardTeamsPayload(*RequestContext) (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"teams": []map[string]any{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildDashboardManagedSkillsPayload(*RequestContext) (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"skills": []map[string]any{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildDashboardGetMePayload(reqCtx *RequestContext) (map[string]any, error) {
|
||||
authID := ""
|
||||
if reqCtx != nil {
|
||||
authID = authIDFromBearer(reqCtx.Headers.Get("authorization"))
|
||||
}
|
||||
if authID == "" {
|
||||
authID = authIDFromJWT(legacyruntime.InjectAuthToken)
|
||||
}
|
||||
if authID == "" {
|
||||
authID = localUltraPaymentID
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"authId": authID,
|
||||
"userId": localUltraDashboardUserID,
|
||||
"email": legacyruntime.InjectAccountEmail,
|
||||
"firstName": "Cursor",
|
||||
"lastName": "Local",
|
||||
"createdAt": time.Now().UTC().Format(time.RFC3339),
|
||||
"isEnterpriseUser": false,
|
||||
"teamName": "",
|
||||
"emailDomainType": "personal",
|
||||
"country": "US",
|
||||
"profilePictureUrl": "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildDashboardUserPrivacyModePayload(*RequestContext) (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"privacyMode": "PRIVACY_MODE_NO_STORAGE",
|
||||
"hoursRemainingInGracePeriod": 0,
|
||||
"isEnforcedByTeam": false,
|
||||
"isNotMigratedToServerSourceOfTruth": false,
|
||||
"partnerDataShare": false,
|
||||
"hasAcknowledgedGracePeriodDisclaimer": true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildDashboardPlanInfoPayload(*RequestContext) (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"planInfo": map[string]any{
|
||||
"planName": "Ultra Plan",
|
||||
"includedAmountCents": localUltraPlanIncludedCents,
|
||||
"price": "$200/mo",
|
||||
"billingCycleEnd": time.Now().Add(10 * 365 * 24 * time.Hour).UnixMilli(),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildDashboardUsageLimitStatusAndActiveGrantsPayload(*RequestContext) (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"usageLimitPolicyStatus": map[string]any{
|
||||
"isInSlowPool": false,
|
||||
"features": map[string]string{},
|
||||
"canConfigureSpendLimit": true,
|
||||
"hasPendingRequest": false,
|
||||
"allowedModelIds": []string{},
|
||||
"allowedModelTags": []string{},
|
||||
},
|
||||
"activeGrants": []map[string]any{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildDashboardIsOnNewPricingPayload(*RequestContext) (map[string]any, error) {
|
||||
return map[string]any{
|
||||
"isOnNewPricing": true,
|
||||
"isOptedOut": false,
|
||||
"hasAutoSpillover": true,
|
||||
"dashboardUserId": localUltraDashboardUserID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func loadConfiguredModelAdapters(reqCtx *RequestContext) ([]legacyruntime.ModelAdapterConfig, error) {
|
||||
if reqCtx == nil || reqCtx.Deps == nil || reqCtx.Deps.SystemSettingService == nil {
|
||||
return []legacyruntime.ModelAdapterConfig{}, nil
|
||||
}
|
||||
ctx := context.Background()
|
||||
if reqCtx.Request != nil {
|
||||
ctx = reqCtx.Request.Context()
|
||||
}
|
||||
return reqCtx.Deps.SystemSettingService.ResolveModelAdapters(ctx)
|
||||
}
|
||||
|
||||
func buildAvailableModelEntries(adapters []legacyruntime.ModelAdapterConfig) []map[string]any {
|
||||
if len(adapters) == 0 {
|
||||
return []map[string]any{}
|
||||
}
|
||||
output := make([]map[string]any, 0, len(adapters))
|
||||
for _, adapter := range adapters {
|
||||
channelID := strings.TrimSpace(adapter.ID)
|
||||
displayName := strings.TrimSpace(adapter.DisplayName)
|
||||
modelID := strings.TrimSpace(adapter.ModelID)
|
||||
tooltipData := strings.TrimSpace(adapter.TooltipData)
|
||||
if channelID == "" || modelID == "" {
|
||||
continue
|
||||
}
|
||||
modelDisplayName := displayName
|
||||
if modelDisplayName == "" {
|
||||
modelDisplayName = modelID
|
||||
}
|
||||
defaultThinkingEffort := defaultThinkingEffortForAdapter(adapter)
|
||||
output = append(output, map[string]any{
|
||||
"clientDisplayName": displayName,
|
||||
"defaultOn": true,
|
||||
"degradationStatus": "DEGRADATION_STATUS_UNSPECIFIED",
|
||||
"inputboxShortModelName": displayName,
|
||||
"isRecommendedForBackgroundComposer": false,
|
||||
"name": channelID,
|
||||
"namedModelSectionIndex": 1,
|
||||
"parameterDefinitions": buildThinkingEffortParameterDefinitions(adapter.Type),
|
||||
"serverModelName": channelID,
|
||||
"supportsAgent": true,
|
||||
"supportsImages": true,
|
||||
"supportsMaxMode": false,
|
||||
"supportsNonMaxMode": true,
|
||||
"supportsPlanMode": true,
|
||||
"supportsSandboxing": true,
|
||||
"supportsThinking": true,
|
||||
"tagline": thinkingEffortDisplayName(defaultThinkingEffort),
|
||||
"tooltipData": map[string]any{
|
||||
"markdownContent": tooltipData,
|
||||
},
|
||||
"tooltipDataForMaxMode": map[string]any{
|
||||
"markdownContent": tooltipData,
|
||||
},
|
||||
"variants": buildThinkingEffortVariants(adapter.Type, channelID, modelDisplayName, tooltipData, defaultThinkingEffort),
|
||||
})
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func buildThinkingEffortParameterDefinitions(adapterType string) []map[string]any {
|
||||
values := thinkingEffortValuesForAdapter(adapterType)
|
||||
options := make([]map[string]any, 0, len(values))
|
||||
for _, value := range values {
|
||||
options = append(options, map[string]any{
|
||||
"displayName": thinkingEffortDisplayName(value),
|
||||
"increasesModelCost": value == "xhigh" || value == "max",
|
||||
"value": value,
|
||||
})
|
||||
}
|
||||
return []map[string]any{{
|
||||
"id": modelRuntimeThinkingEffortParameterID,
|
||||
"isCycleableByHotkey": true,
|
||||
"markdownTooltip": "Controls the model thinking intensity for this run.",
|
||||
"name": "Thinking intensity",
|
||||
"parameterType": map[string]any{
|
||||
"enumParameter": map[string]any{
|
||||
"values": options,
|
||||
},
|
||||
},
|
||||
}}
|
||||
}
|
||||
|
||||
func buildThinkingEffortVariants(adapterType string, channelID string, modelDisplayName string, tooltipData string, defaultThinkingEffort string) []map[string]any {
|
||||
values := orderThinkingEffortValues(thinkingEffortValuesForAdapter(adapterType), defaultThinkingEffort)
|
||||
channelID = strings.TrimSpace(channelID)
|
||||
modelDisplayName = strings.TrimSpace(modelDisplayName)
|
||||
variants := make([]map[string]any, 0, len(values))
|
||||
for _, value := range values {
|
||||
effortDisplayName := thinkingEffortDisplayName(value)
|
||||
variantDisplayName := buildThinkingEffortVariantDisplayName(modelDisplayName, value)
|
||||
variant := map[string]any{
|
||||
"displayName": variantDisplayName,
|
||||
"displayNameOutsidePicker": variantDisplayName,
|
||||
"isDefaultNonMaxConfig": value == defaultThinkingEffort,
|
||||
"isMaxMode": false,
|
||||
"parameterValues": []map[string]any{{"id": modelRuntimeThinkingEffortParameterID, "value": value}},
|
||||
}
|
||||
if normalizeAvailableModelThinkingEffort(value, true, "") != "disabled" {
|
||||
variant["tagline"] = effortDisplayName
|
||||
}
|
||||
if channelID != "" {
|
||||
variant["variantStringRepresentation"] = channelID + ":" + value
|
||||
}
|
||||
if strings.TrimSpace(tooltipData) != "" {
|
||||
variant["tooltipData"] = map[string]any{"markdownContent": tooltipData}
|
||||
}
|
||||
variants = append(variants, variant)
|
||||
}
|
||||
return variants
|
||||
}
|
||||
|
||||
func buildThinkingEffortVariantDisplayName(modelDisplayName string, effortValue string) string {
|
||||
modelDisplayName = html.EscapeString(strings.TrimSpace(modelDisplayName))
|
||||
if normalizeAvailableModelThinkingEffort(effortValue, true, "") == "disabled" {
|
||||
return modelDisplayName
|
||||
}
|
||||
effortDisplayName := thinkingEffortDisplayName(effortValue)
|
||||
effortDisplayName = html.EscapeString(strings.TrimSpace(effortDisplayName))
|
||||
if modelDisplayName == "" {
|
||||
return `<span class="ui-model-picker__item-tagline" style="color: var(--cursor-text-secondary); white-space: nowrap;">:icon-brain: ` + effortDisplayName + `</span>`
|
||||
}
|
||||
return modelDisplayName + ` <span class="ui-model-picker__item-tagline" style="color: var(--cursor-text-secondary); white-space: nowrap;">:icon-brain: ` + effortDisplayName + `</span>`
|
||||
}
|
||||
|
||||
func thinkingEffortValuesForAdapter(adapterType string) []string {
|
||||
values := []string{"disabled", "low", "medium", "high", "xhigh"}
|
||||
if strings.EqualFold(strings.TrimSpace(adapterType), "anthropic") {
|
||||
values = append(values, "max")
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func orderThinkingEffortValues(values []string, defaultValue string) []string {
|
||||
defaultValue = strings.ToLower(strings.TrimSpace(defaultValue))
|
||||
output := make([]string, 0, len(values))
|
||||
for _, value := range values {
|
||||
if strings.EqualFold(value, defaultValue) {
|
||||
output = append(output, value)
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, value := range values {
|
||||
if !strings.EqualFold(value, defaultValue) {
|
||||
output = append(output, value)
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func defaultThinkingEffortForAdapter(adapter legacyruntime.ModelAdapterConfig) string {
|
||||
if strings.EqualFold(strings.TrimSpace(adapter.Type), "anthropic") {
|
||||
return normalizeAvailableModelThinkingEffort(adapter.AnthropicThinkingEffort, true, "xhigh")
|
||||
}
|
||||
return normalizeAvailableModelThinkingEffort(adapter.ReasoningEffort, false, "medium")
|
||||
}
|
||||
|
||||
func normalizeAvailableModelThinkingEffort(raw string, allowMax bool, fallback string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(raw)) {
|
||||
case "disabled", "low", "medium", "high", "xhigh":
|
||||
return strings.ToLower(strings.TrimSpace(raw))
|
||||
case "disable", "off", "none", "false", "no", "0":
|
||||
return "disabled"
|
||||
case "max":
|
||||
if allowMax {
|
||||
return "max"
|
||||
}
|
||||
return fallback
|
||||
default:
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
func thinkingEffortDisplayName(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "disabled":
|
||||
return "Disabled"
|
||||
case "low":
|
||||
return "Low"
|
||||
case "medium":
|
||||
return "Medium"
|
||||
case "high":
|
||||
return "High"
|
||||
case "xhigh":
|
||||
return "XHigh"
|
||||
case "max":
|
||||
return "Max"
|
||||
default:
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
|
||||
func collectModelAdapterRefs(adapters []legacyruntime.ModelAdapterConfig) []string {
|
||||
output := make([]string, 0, len(adapters))
|
||||
for _, adapter := range adapters {
|
||||
channelID := strings.TrimSpace(adapter.ID)
|
||||
if channelID == "" {
|
||||
continue
|
||||
}
|
||||
output = append(output, channelID)
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
func resolveBootstrapStatsigAuthID(reqCtx *RequestContext) string {
|
||||
if reqCtx != nil {
|
||||
if authID := authIDFromBearer(reqCtx.Headers.Get("authorization")); authID != "" {
|
||||
return authID
|
||||
}
|
||||
}
|
||||
if authID := authIDFromJWT(legacyruntime.InjectAuthToken); authID != "" {
|
||||
return authID
|
||||
}
|
||||
return localUltraPaymentID
|
||||
}
|
||||
|
||||
func authIDFromBearer(authorization string) string {
|
||||
authorization = strings.TrimSpace(authorization)
|
||||
if len(authorization) >= len("Bearer ") && strings.EqualFold(authorization[:len("Bearer ")], "Bearer ") {
|
||||
authorization = strings.TrimSpace(authorization[len("Bearer "):])
|
||||
}
|
||||
return authIDFromJWT(authorization)
|
||||
}
|
||||
|
||||
func authIDFromJWT(token string) string {
|
||||
parts := strings.Split(strings.TrimSpace(token), ".")
|
||||
if len(parts) < 2 {
|
||||
return ""
|
||||
}
|
||||
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
payload, err = base64.URLEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
var claims struct {
|
||||
Sub string `json:"sub"`
|
||||
}
|
||||
if err := json.Unmarshal(payload, &claims); err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(claims.Sub)
|
||||
}
|
||||
|
||||
func buildBootstrapStatsigConfigJSON(nowMs int64, authID string) ([]byte, error) {
|
||||
authID = strings.TrimSpace(authID)
|
||||
if authID == "" {
|
||||
authID = localUltraPaymentID
|
||||
}
|
||||
template := bootstrapStatsigTemplate
|
||||
template.Time = nowMs
|
||||
template.User = map[string]any{
|
||||
"userID": authID,
|
||||
"email": legacyruntime.InjectAccountEmail,
|
||||
"customIDs": map[string]string{
|
||||
"localUserID": authID,
|
||||
},
|
||||
}
|
||||
|
||||
// This template mirrors the Statsig initialize/bootstrap response shape that
|
||||
// the bundled client reads for experiments. hash_used stays "none" so the
|
||||
// experiment can be looked up by its plain name without spec hashing.
|
||||
//
|
||||
// Cursor currently branches on free_user_model_picker.variant. Known values
|
||||
// are "control", "locked_picker", and "grayed_models". Keep this template
|
||||
// centralized and update it first if the bundled Statsig bootstrap shape changes.
|
||||
return json.Marshal(template)
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package upstream
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"cursor/internal/backend/server"
|
||||
legacyruntime "cursor/internal/runtime"
|
||||
)
|
||||
|
||||
const (
|
||||
HeaderRawServerURL = server.HeaderServerUpstreamURL
|
||||
)
|
||||
|
||||
type SystemSettingService interface {
|
||||
ResolveModelAdapters(context.Context) ([]legacyruntime.ModelAdapterConfig, error)
|
||||
}
|
||||
|
||||
type HTTPClient interface {
|
||||
Do(req *http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
type Dependencies struct {
|
||||
SystemSettingService SystemSettingService
|
||||
HTTPClient HTTPClient
|
||||
LogRoot string
|
||||
Routes []Route
|
||||
}
|
||||
|
||||
type RequestContext struct {
|
||||
ResponseWriter http.ResponseWriter
|
||||
Request *http.Request
|
||||
StartedAt time.Time
|
||||
RawURL string
|
||||
TargetURL *url.URL
|
||||
Method string
|
||||
Headers http.Header
|
||||
ContentType string
|
||||
RequestBody []byte
|
||||
Mode server.ExecutionMode
|
||||
Deps *Dependencies
|
||||
HTTPRequestID string
|
||||
}
|
||||
|
||||
type ForwardOptions struct {
|
||||
BodyOverride []byte
|
||||
PatchHeaders func(headers http.Header)
|
||||
}
|
||||
|
||||
type ForwardMeta struct {
|
||||
StatusCode int
|
||||
Status string
|
||||
ContentType string
|
||||
ResponseSize int64
|
||||
}
|
||||
|
||||
type Matcher interface {
|
||||
Match(path string) bool
|
||||
}
|
||||
|
||||
type Exact string
|
||||
|
||||
func (m Exact) Match(path string) bool { return path == string(m) }
|
||||
|
||||
type Prefix string
|
||||
|
||||
func (m Prefix) Match(path string) bool {
|
||||
value := string(m)
|
||||
return value != "" && strings.HasPrefix(path, value)
|
||||
}
|
||||
|
||||
type Wildcard struct{}
|
||||
|
||||
func (Wildcard) Match(string) bool { return true }
|
||||
|
||||
type RouteHandler func(reqCtx *RequestContext, route *Route) error
|
||||
|
||||
type Route struct {
|
||||
Name string
|
||||
Pattern string
|
||||
Matcher Matcher
|
||||
ConsoleLog bool
|
||||
StatusCode int
|
||||
JSONBody map[string]any
|
||||
MockProtoType string
|
||||
MockPayloadBuilder func(*RequestContext) (map[string]any, error)
|
||||
Handler RouteHandler
|
||||
}
|
||||
|
||||
func BuildChannelCallError(statusCode int, forwardErr error) (string, string) {
|
||||
if forwardErr != nil {
|
||||
return "UPSTREAM_REQUEST_FAILED", strings.TrimSpace(forwardErr.Error())
|
||||
}
|
||||
if statusCode >= 200 && statusCode < 300 {
|
||||
return "", ""
|
||||
}
|
||||
if statusCode <= 0 {
|
||||
return "UPSTREAM_STATUS_UNKNOWN", ""
|
||||
}
|
||||
return "UPSTREAM_STATUS_" + strconv.Itoa(statusCode), ""
|
||||
}
|
||||
|
||||
func ReadStringAny(data map[string]any, keys ...string) string {
|
||||
if data == nil {
|
||||
return ""
|
||||
}
|
||||
for _, key := range keys {
|
||||
value, ok := data[key]
|
||||
if !ok || value == nil {
|
||||
continue
|
||||
}
|
||||
if text, ok := value.(string); ok {
|
||||
return text
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func ReadMapAny(data map[string]any, keys ...string) map[string]any {
|
||||
if data == nil {
|
||||
return nil
|
||||
}
|
||||
for _, key := range keys {
|
||||
value, ok := data[key]
|
||||
if !ok || value == nil {
|
||||
continue
|
||||
}
|
||||
if mapped, ok := value.(map[string]any); ok {
|
||||
return mapped
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func CloneAnyMap(input map[string]any) map[string]any {
|
||||
if input == nil {
|
||||
return nil
|
||||
}
|
||||
output := make(map[string]any, len(input))
|
||||
for key, value := range input {
|
||||
output[key] = value
|
||||
}
|
||||
return output
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ParseAndValidateRawURL(raw string) (*url.URL, error) {
|
||||
value := strings.TrimSpace(raw)
|
||||
if value == "" {
|
||||
return nil, fmt.Errorf("empty raw url")
|
||||
}
|
||||
parsed, err := url.Parse(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if parsed.Scheme != "http" && parsed.Scheme != "https" {
|
||||
return nil, fmt.Errorf("unsupported scheme %q", parsed.Scheme)
|
||||
}
|
||||
if strings.TrimSpace(parsed.Host) == "" {
|
||||
return nil, fmt.Errorf("empty host")
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
Reference in New Issue
Block a user