Power Shell 使用 Groq Kimi K2 的代码实现一种

了解到 Use Kimi K2 on Claude Code through Groq 这个项目,想着 PowerShell 也能部分办到。借助 acli rovodev,于是有

# Anthropic to Groq Proxy - PowerShell Implementation (Robust Multi-Key Version)
param(
    [int]$Port = 7187,
    [string[]]$GroqApiKeys = @()
)

# 设置编码
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::InputEncoding = [System.Text.Encoding]::UTF8
$OutputEncoding = [System.Text.Encoding]::UTF8

# 多API密钥配置
if ($GroqApiKeys.Count -eq 0) {
    $GroqApiKeys = @(
        $env:GROQ_API_KEY,
        $env:GROQ_API_KEY_2,
        $env:GROQ_API_KEY_3,
        $env:GROQ_API_KEY_4,
        $env:GROQ_API_KEY_5
    ) | Where-Object { $_ -and $_.Trim() -ne "" }
}

if ($GroqApiKeys.Count -eq 0) {
    Write-Error "至少需要一个GROQ_API_KEY环境变量"
    exit 1
}

# 常量定义
$GROQ_MODEL = "moonshotai/kimi-k2-instruct"
$GROQ_BASE_URL = "https://api.groq.com/openai/v1"
$GROQ_MAX_OUTPUT_TOKENS = 16384

# API密钥状态管理
$script:ApiKeyStatus = @()
for ($i = 0; $i -lt $GroqApiKeys.Count; $i++) {
    $script:ApiKeyStatus += @{
        Index = $i
        Key = $GroqApiKeys[$i]
        IsAvailable = $true
        RateLimitResetTime = $null
        DailyLimitResetTime = $null
        LastError = $null
        RequestCount = 0
        SuccessCount = 0
        ErrorCount = 0
    }
}

$script:CurrentKeyIndex = 0

# 安全输出函数
function Safe-Output {
    param([string]$Message, [string]$Color = "White")
    
    $safeMessage = $Message -replace '🛑', '[STOP]'
    $safeMessage = $safeMessage -replace '🚀', '[ROCKET]'
    $safeMessage = $safeMessage -replace '📤', '[SEND]'
    $safeMessage = $safeMessage -replace '✅', '[OK]'
    $safeMessage = $safeMessage -replace '❌', '[ERROR]'
    $safeMessage = $safeMessage -replace '⚠️', '[WARN]'
    $safeMessage = $safeMessage -replace '🔄', '[REFRESH]'
    $safeMessage = $safeMessage -replace '📊', '[CHART]'
    $safeMessage = $safeMessage -replace '🔑', '[KEY]'
    $safeMessage = $safeMessage -replace '⏳', '[WAIT]'
    
    Write-Host $safeMessage -ForegroundColor $Color
}

# 解析速率限制错误
function Parse-RateLimitError {
    param([string]$ErrorMessage)
    
    $waitSeconds = 60  # 默认等待时间
    $limitType = "TPM"
    
    # 匹配分钟和秒的格式: "13m39.872999999s"
    if ($ErrorMessage -match "Please try again in (\d+)m(\d+\.?\d*)s") {
        $minutes = [int]$matches[1]
        $seconds = [double]$matches[2]
        $waitSeconds = $minutes * 60 + $seconds
    }
    # 匹配只有秒的格式: "39.872999999s"
    elseif ($ErrorMessage -match "Please try again in (\d+\.?\d*)s") {
        $waitSeconds = [double]$matches[1]
    }
    
    # 判断限制类型
    if ($ErrorMessage -match "TPD|tokens per day") {
        $limitType = "TPD"
    }
    
    return @{
        WaitSeconds = $waitSeconds
        LimitType = $limitType
    }
}

# 标记API密钥为限制状态
function Mark-KeyAsLimited {
    param([int]$KeyIndex, [double]$WaitSeconds, [string]$LimitType)
    
    $keyStatus = $script:ApiKeyStatus[$KeyIndex]
    $keyStatus.IsAvailable = $false
    
    $resetTime = (Get-Date).AddSeconds($WaitSeconds)
    
    if ($LimitType -eq "TPD") {
        $keyStatus.DailyLimitResetTime = $resetTime
        Safe-Output "[ERROR] API Key $($KeyIndex + 1) 达到每日限制,重置时间: $($resetTime.ToString('HH:mm:ss'))" "Red"
    } else {
        $keyStatus.RateLimitResetTime = $resetTime
        Safe-Output "[WARN] API Key $($KeyIndex + 1) 达到速率限制,重置时间: $($resetTime.ToString('HH:mm:ss'))" "Yellow"
    }
}

# 检查API密钥是否可用
function Test-KeyAvailability {
    param([int]$KeyIndex)
    
    $keyStatus = $script:ApiKeyStatus[$KeyIndex]
    $now = Get-Date
    
    # 检查每日限制
    if ($keyStatus.DailyLimitResetTime -and $now -lt $keyStatus.DailyLimitResetTime) {
        return $false
    }
    
    # 检查速率限制
    if ($keyStatus.RateLimitResetTime -and $now -lt $keyStatus.RateLimitResetTime) {
        return $false
    }
    
    # 如果时间已过,重置状态
    if (-not $keyStatus.IsAvailable) {
        $keyStatus.IsAvailable = $true
        $keyStatus.RateLimitResetTime = $null
        Safe-Output "[OK] API Key $($KeyIndex + 1) 已恢复可用" "Green"
    }
    
    return $true
}

# 获取下一个可用的API密钥
function Get-NextAvailableKey {
    # 首先检查当前密钥
    if (Test-KeyAvailability -KeyIndex $script:CurrentKeyIndex) {
        return $script:CurrentKeyIndex
    }
    
    # 寻找下一个可用的密钥
    for ($i = 0; $i -lt $script:ApiKeyStatus.Count; $i++) {
        $nextIndex = ($script:CurrentKeyIndex + $i + 1) % $script:ApiKeyStatus.Count
        if (Test-KeyAvailability -KeyIndex $nextIndex) {
            $script:CurrentKeyIndex = $nextIndex
            Safe-Output "[REFRESH] 切换到 API Key $($nextIndex + 1)" "Cyan"
            return $nextIndex
        }
    }
    
    return -1  # 没有可用的密钥
}

# 获取等待时间最短的密钥
function Get-ShortestWaitKey {
    $minWaitTime = [double]::MaxValue
    $bestKeyIndex = -1
    
    for ($i = 0; $i -lt $script:ApiKeyStatus.Count; $i++) {
        $keyStatus = $script:ApiKeyStatus[$i]
        $now = Get-Date
        
        $waitTime = 0
        
        # 检查每日限制等待时间
        if ($keyStatus.DailyLimitResetTime -and $now -lt $keyStatus.DailyLimitResetTime) {
            $waitTime = ($keyStatus.DailyLimitResetTime - $now).TotalSeconds
        }
        # 检查速率限制等待时间
        elseif ($keyStatus.RateLimitResetTime -and $now -lt $keyStatus.RateLimitResetTime) {
            $waitTime = ($keyStatus.RateLimitResetTime - $now).TotalSeconds
        }
        
        if ($waitTime -gt 0 -and $waitTime -lt $minWaitTime) {
            $minWaitTime = $waitTime
            $bestKeyIndex = $i
        }
    }
    
    return @{
        KeyIndex = $bestKeyIndex
        WaitTime = $minWaitTime
    }
}

# HTTP Listener
$listener = New-Object System.Net.HttpListener
$listener.Prefixes.Add("http://localhost:$Port/")
$listener.Prefixes.Add("http://127.0.0.1:$Port/")

try {
    $listener.Start()
    Safe-Output "[ROCKET] 多密钥 Anthropic-Groq 代理运行在端口 $Port" "Green"
    Safe-Output "配置了 $($GroqApiKeys.Count) 个API密钥" "Cyan"
    Safe-Output "模型: $GROQ_MODEL" "Cyan"
    Safe-Output "最大令牌: $GROQ_MAX_OUTPUT_TOKENS" "Cyan"
    Safe-Output "按 Ctrl+C 停止" "Yellow"
} catch {
    Write-Error "Failed to start HTTP listener: $_"
    exit 1
}

function Convert-AnthropicToOpenAI {
    param([PSObject]$AnthropicRequest)
    
    Safe-Output "转换 Anthropic 请求为 OpenAI 格式" "Cyan"
    
    $openaiMessages = @()
    foreach ($msg in $AnthropicRequest.messages) {
        if ($msg.content -is [string]) {
            $content = $msg.content
        } else {
            $parts = @()
            foreach ($block in $msg.content) {
                switch ($block.type) {
                    "text" { $parts += $block.text }
                    "tool_use" { 
                        $toolInfo = "[工具使用: $($block.name)] $($block.input | ConvertTo-Json -Compress)"
                        $parts += $toolInfo
                        Safe-Output "工具调用: $($block.name)" "Green"
                    }
                    "tool_result" {
                        $result = $block.content | ConvertTo-Json -Compress
                        $parts += "<tool_result>$result</tool_result>"
                        Safe-Output "工具结果: $($block.tool_use_id)" "Yellow"
                    }
                }
            }
            $content = $parts -join "`n"
        }
        
        $openaiMessages += @{
            role = $msg.role
            content = $content
        }
    }
    
    $openaiTools = @()
    if ($AnthropicRequest.tools) {
        foreach ($tool in $AnthropicRequest.tools) {
            $openaiTools += @{
                type = "function"
                function = @{
                    name = $tool.name
                    description = $tool.description
                    parameters = $tool.input_schema
                }
            }
        }
        Safe-Output "工具已转换: $($openaiTools.Count)" "Magenta"
    }
    
    $maxTokens = if ($AnthropicRequest.max_tokens) { 
        [Math]::Min($AnthropicRequest.max_tokens, $GROQ_MAX_OUTPUT_TOKENS) 
    } else { 
        1024 
    }
    
    if ($AnthropicRequest.max_tokens -and $AnthropicRequest.max_tokens -gt $GROQ_MAX_OUTPUT_TOKENS) {
        Safe-Output "[WARN] 限制 max_tokens 从 $($AnthropicRequest.max_tokens) 到 $GROQ_MAX_OUTPUT_TOKENS" "Yellow"
    }
    
    $requestObj = @{
        model = $GROQ_MODEL
        messages = $openaiMessages
        max_tokens = $maxTokens
        temperature = if ($AnthropicRequest.temperature) { $AnthropicRequest.temperature } else { 0.7 }
    }
    
    if ($openaiTools.Count -gt 0) {
        $requestObj.tools = $openaiTools
        $requestObj.tool_choice = "auto"
    }
    
    Safe-Output "请求已准备 - 消息: $($openaiMessages.Count), 最大令牌: $maxTokens" "Gray"
    return $requestObj
}

function Invoke-GroqAPIWithMultiKey {
    param([PSObject]$OpenAIRequest)
    
    $maxRetries = $script:ApiKeyStatus.Count * 2  # 每个密钥最多重试2次
    
    for ($attempt = 0; $attempt -lt $maxRetries; $attempt++) {
        # 获取可用的API密钥
        $keyIndex = Get-NextAvailableKey
        
        if ($keyIndex -eq -1) {
            # 所有密钥都不可用,找等待时间最短的
            $waitInfo = Get-ShortestWaitKey
            
            if ($waitInfo.KeyIndex -eq -1 -or $waitInfo.WaitTime -eq [double]::MaxValue) {
                Safe-Output "[ERROR] 所有API密钥都不可用" "Red"
                return $null
            }
            
            if ($waitInfo.WaitTime -gt 300) {  # 如果等待时间超过5分钟
                Safe-Output "[ERROR] 最短等待时间超过5分钟: $([Math]::Round($waitInfo.WaitTime/60, 1)) 分钟" "Red"
                return $null
            }
            
            Safe-Output "[WAIT] 所有密钥都不可用,等待 $([Math]::Round($waitInfo.WaitTime, 1)) 秒..." "Yellow"
            Start-Sleep -Seconds ([Math]::Ceiling($waitInfo.WaitTime))
            continue
        }
        
        $keyStatus = $script:ApiKeyStatus[$keyIndex]
        $headers = @{
            "Authorization" = "Bearer $($keyStatus.Key)"
            "Content-Type" = "application/json; charset=utf-8"
            "User-Agent" = "PowerShell-Anthropic-Proxy/4.0-MultiKey"
        }
        
        $body = $OpenAIRequest | ConvertTo-Json -Depth 10 -Compress
        
        try {
            Safe-Output "[SEND] 使用 API Key $($keyIndex + 1) (尝试 $($attempt + 1)/$maxRetries)" "Green"
            $keyStatus.RequestCount++
            
            $response = Invoke-RestMethod -Uri "$GROQ_BASE_URL/chat/completions" -Method POST -Headers $headers -Body $body -TimeoutSec 30
            
            $keyStatus.SuccessCount++
            Safe-Output "[OK] API 调用成功" "Green"
            
            return $response
            
        } catch {
            $keyStatus.ErrorCount++
            $keyStatus.LastError = $_.Exception.Message
            
            Safe-Output "[ERROR] API Key $($keyIndex + 1) 错误: $($_.Exception.Message)" "Red"
            
            # 检查是否是速率限制错误
            $errorDetails = $null
            if ($_.ErrorDetails) {
                try {
                    $errorDetails = $_.ErrorDetails.Message | ConvertFrom-Json
                } catch {
                    Safe-Output "[ERROR] 无法解析错误详情" "Red"
                }
            }
            
            if ($errorDetails -and $errorDetails.error.code -eq "rate_limit_exceeded") {
                $rateLimitInfo = Parse-RateLimitError -ErrorMessage $errorDetails.error.message
                Mark-KeyAsLimited -KeyIndex $keyIndex -WaitSeconds $rateLimitInfo.WaitSeconds -LimitType $rateLimitInfo.LimitType
                
                Safe-Output "[CHART] 速率限制详情: $($errorDetails.error.message)" "Gray"
                continue  # 尝试下一个密钥
            }
            
            # 如果不是速率限制错误,短暂等待后重试
            if ($attempt -lt $maxRetries - 1) {
                Safe-Output "[WAIT] 2秒后重试..." "Yellow"
                Start-Sleep -Seconds 2
            }
        }
    }
    
    Safe-Output "[ERROR] 超出最大重试次数" "Red"
    return $null
}

function Convert-OpenAIToAnthropic {
    param([PSObject]$OpenAIResponse)
    
    Safe-Output "转换 OpenAI 响应为 Anthropic 格式" "Cyan"
    
    $choice = $OpenAIResponse.choices[0]
    $message = $choice.message
    
    $content = @()
    $stopReason = "end_turn"
    
    if ($message.tool_calls) {
        foreach ($toolCall in $message.tool_calls) {
            $arguments = $toolCall.function.arguments | ConvertFrom-Json
            $content += @{
                type = "tool_use"
                id = $toolCall.id
                name = $toolCall.function.name
                input = $arguments
            }
            Safe-Output "工具调用: $($toolCall.function.name)" "Green"
        }
        $stopReason = "tool_use"
    } else {
        $content += @{
            type = "text"
            text = $message.content
        }
    }
    
    $response = @{
        id = "msg_$(New-Guid | ForEach-Object { $_.ToString().Replace('-','').Substring(0,12) })"
        model = "groq/$GROQ_MODEL"
        role = "assistant"
        type = "message"
        content = $content
        stop_reason = $stopReason
        stop_sequence = $null
        usage = @{
            input_tokens = $OpenAIResponse.usage.prompt_tokens
            output_tokens = $OpenAIResponse.usage.completion_tokens
        }
    }
    
    Safe-Output "[OK] 响应转换完成" "Green"
    return $response
}

# Main server loop
try {
    while ($listener.IsListening) {
        $context = $listener.GetContext()
        $request = $context.Request
        $response = $context.Response
        
        $response.Headers.Add("Access-Control-Allow-Origin", "*")
        $response.Headers.Add("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
        $response.Headers.Add("Access-Control-Allow-Headers", "Content-Type, Authorization")
        
        if ($request.HttpMethod -eq "OPTIONS") {
            $response.StatusCode = 200
            $response.Close()
            continue
        }
        
        try {
            if ($request.Url.AbsolutePath -eq "/v1/messages" -and $request.HttpMethod -eq "POST") {
                Safe-Output "`n[ROCKET] Anthropic -> Groq | 处理请求" "Magenta"
                
                $reader = New-Object System.IO.StreamReader($request.InputStream, [System.Text.Encoding]::UTF8)
                $requestBody = $reader.ReadToEnd()
                $reader.Close()
                
                try {
                    $anthropicRequest = $requestBody | ConvertFrom-Json
                    Safe-Output "请求模型: $($anthropicRequest.model)" "Cyan"
                } catch {
                    Safe-Output "[ERROR] 解析请求 JSON 失败: $_" "Red"
                    $response.StatusCode = 400
                    $response.Close()
                    continue
                }
                
                $openaiRequest = Convert-AnthropicToOpenAI -AnthropicRequest $anthropicRequest
                $groqResponse = Invoke-GroqAPIWithMultiKey -OpenAIRequest $openaiRequest
                
                if ($groqResponse) {
                    $anthropicResponse = Convert-OpenAIToAnthropic -OpenAIResponse $groqResponse
                    $responseJson = $anthropicResponse | ConvertTo-Json -Depth 10
                    
                    $response.ContentType = "application/json; charset=utf-8"
                    $response.StatusCode = 200
                    
                    $utf8NoBom = New-Object System.Text.UTF8Encoding($false)
                    $buffer = $utf8NoBom.GetBytes($responseJson)
                    $response.OutputStream.Write($buffer, 0, $buffer.Length)
                    
                    Safe-Output "[OK] 响应发送成功`n" "Green"
                } else {
                    Safe-Output "[ERROR] 所有API密钥都失败" "Red"
                    $response.StatusCode = 503
                    $response.ContentType = "application/json; charset=utf-8"
                    $errorResponse = @{ 
                        error = @{ 
                            message = "所有API密钥都不可用,请稍后重试"
                            type = "service_unavailable"
                        } 
                    } | ConvertTo-Json
                    $utf8NoBom = New-Object System.Text.UTF8Encoding($false)
                    $buffer = $utf8NoBom.GetBytes($errorResponse)
                    $response.OutputStream.Write($buffer, 0, $buffer.Length)
                }
            } elseif ($request.Url.AbsolutePath -eq "/status" -and $request.HttpMethod -eq "GET") {
                # 状态检查端点
                $statusInfo = @{
                    total_keys = $script:ApiKeyStatus.Count
                    current_key = $script:CurrentKeyIndex + 1
                    keys = @()
                }
                
                foreach ($keyStatus in $script:ApiKeyStatus) {
                    $now = Get-Date
                    $waitTime = 0
                    
                    if ($keyStatus.DailyLimitResetTime -and $now -lt $keyStatus.DailyLimitResetTime) {
                        $waitTime = ($keyStatus.DailyLimitResetTime - $now).TotalSeconds
                    } elseif ($keyStatus.RateLimitResetTime -and $now -lt $keyStatus.RateLimitResetTime) {
                        $waitTime = ($keyStatus.RateLimitResetTime - $now).TotalSeconds
                    }
                    
                    $statusInfo.keys += @{
                        index = $keyStatus.Index + 1
                        is_available = (Test-KeyAvailability -KeyIndex $keyStatus.Index)
                        request_count = $keyStatus.RequestCount
                        success_count = $keyStatus.SuccessCount
                        error_count = $keyStatus.ErrorCount
                        wait_time_seconds = [Math]::Round($waitTime, 1)
                        last_error = $keyStatus.LastError
                    }
                }
                
                $response.ContentType = "application/json; charset=utf-8"
                $statusJson = $statusInfo | ConvertTo-Json -Depth 10
                $utf8NoBom = New-Object System.Text.UTF8Encoding($false)
                $buffer = $utf8NoBom.GetBytes($statusJson)
                $response.OutputStream.Write($buffer, 0, $buffer.Length)
            } else {
                # Health check
                $response.ContentType = "application/json; charset=utf-8"
                $healthResponse = @{ 
                    message = "Groq Anthropic 多密钥代理运行正常"
                    model = $GROQ_MODEL
                    max_tokens = $GROQ_MAX_OUTPUT_TOKENS
                    port = $Port
                    total_keys = $script:ApiKeyStatus.Count
                    unicode_safe = $true
                } | ConvertTo-Json
                $utf8NoBom = New-Object System.Text.UTF8Encoding($false)
                $buffer = $utf8NoBom.GetBytes($healthResponse)
                $response.OutputStream.Write($buffer, 0, $buffer.Length)
            }
        } catch {
            Safe-Output "[ERROR] 请求处理错误: $_" "Red"
            $response.StatusCode = 500
        } finally {
            $response.Close()
        }
    }
} catch {
    Safe-Output "[WARN] 服务器中断: $_" "Yellow"
} finally {
    if ($listener.IsListening) {
        $listener.Stop()
    }
    $listener.Close()
    Safe-Output "[STOP] 服务器已停止" "Yellow"
}

文件保存为 ANSI/GBK 编码的 rovodev_robust_proxy.ps1 文件,设置 $env:GROQ_API_KEY 等环境变量, 然后. .\rovodev_robust_proxy.ps1 执行(需要提前允许脚本执行,Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

posted @ 2025-07-18 21:54  geyee  阅读(120)  评论(0)    收藏  举报