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)
浙公网安备 33010602011771号