release: prepare v0.1.137

This commit is contained in:
kone
2026-05-17 06:19:56 +08:00
parent f4055c773c
commit dd2b08d875
26 changed files with 344 additions and 32 deletions
@@ -196,6 +196,23 @@ var providerAdapters = map[string]providerAdapter{
},
textPath: "content.0.text",
},
MonitorProviderKiro: {
buildPath: func(string) string { return providerAnthropicPath },
buildBody: func(model, prompt string) ([]byte, error) {
return json.Marshal(map[string]any{
"model": model,
"messages": []map[string]string{{"role": "user", "content": prompt}},
"max_tokens": monitorChallengeMaxTokens,
})
},
buildHeaders: func(apiKey string) map[string]string {
return map[string]string{
"x-api-key": apiKey,
"anthropic-version": monitorAnthropicAPIVersion,
}
},
textPath: "content.0.text",
},
MonitorProviderGemini: {
// Gemini 把 model 名写在 URL path 上:/v1beta/models/{model}:generateContent
buildPath: func(model string) string { return fmt.Sprintf(providerGeminiPathTemplate, model) },
@@ -323,6 +340,7 @@ func buildRequestBody(adapter providerAdapter, provider, model, prompt string, o
var bodyMergeKeyDenyList = map[string]map[string]bool{
MonitorProviderOpenAI: {"model": true, "messages": true, "stream": true},
MonitorProviderAnthropic: {"model": true, "messages": true},
MonitorProviderKiro: {"model": true, "messages": true},
MonitorProviderGemini: {"contents": true},
}
@@ -7,6 +7,8 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
"regexp"
"strconv"
"strings"
"testing"
"time"
@@ -36,6 +38,11 @@ func (h *captureHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
_ = json.NewDecoder(r.Body).Decode(&parsed)
h.lastBody = parsed
respondText := h.respondText
if respondText == autoChallengeResponse {
respondText = solveMonitorChallengeFromBody(parsed)
}
if h.status == 0 {
h.status = 200
}
@@ -44,11 +51,35 @@ func (h *captureHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 构造 Anthropic 格式的响应:content[0].text = h.respondText
_ = json.NewEncoder(w).Encode(map[string]any{
"content": []map[string]any{
{"type": "text", "text": h.respondText},
{"type": "text", "text": respondText},
},
})
}
const autoChallengeResponse = "__auto_challenge__"
var testChallengeLineRegex = regexp.MustCompile(`Q:\s*(\d+)\s*([+-])\s*(\d+)\s*=\s*\?`)
func solveMonitorChallengeFromBody(body map[string]any) string {
msgs, _ := body["messages"].([]any)
if len(msgs) == 0 {
return ""
}
first, _ := msgs[0].(map[string]any)
prompt, _ := first["content"].(string)
matches := testChallengeLineRegex.FindAllStringSubmatch(prompt, -1)
if len(matches) == 0 {
return ""
}
last := matches[len(matches)-1]
a, _ := strconv.Atoi(last[1])
b, _ := strconv.Atoi(last[3])
if last[2] == "-" {
return strconv.Itoa(a - b)
}
return strconv.Itoa(a + b)
}
func setupFakeAnthropic(t *testing.T, handler *captureHandler) string {
t.Helper()
swapMonitorHTTPClient(t)
@@ -58,7 +89,7 @@ func setupFakeAnthropic(t *testing.T, handler *captureHandler) string {
}
func TestRunCheckForModel_OffMode_PreservesDefaultBody(t *testing.T) {
h := &captureHandler{respondText: "the answer is 42"}
h := &captureHandler{respondText: autoChallengeResponse}
endpoint := setupFakeAnthropic(t, h)
// 跑一次 off 模式(opts=nil),确认默认 body 行为未变
@@ -75,6 +106,29 @@ func TestRunCheckForModel_OffMode_PreservesDefaultBody(t *testing.T) {
}
}
func TestRunCheckForModel_KiroUsesAnthropicCompatibleMessages(t *testing.T) {
h := &captureHandler{respondText: autoChallengeResponse}
endpoint := setupFakeAnthropic(t, h)
res := runCheckForModel(context.Background(), MonitorProviderKiro, endpoint, "sk-kiro", "kiro-model", nil)
if res.Status != MonitorStatusOperational {
t.Fatalf("expected operational, got status=%s message=%q", res.Status, res.Message)
}
if h.lastBody["model"] != "kiro-model" {
t.Errorf("kiro body should contain model=kiro-model, got %v", h.lastBody["model"])
}
if _, ok := h.lastBody["messages"]; !ok {
t.Error("kiro body should contain Anthropic-compatible messages")
}
if h.lastHeaders.Get("x-api-key") != "sk-kiro" {
t.Errorf("expected x-api-key header, got %q", h.lastHeaders.Get("x-api-key"))
}
if h.lastHeaders.Get("anthropic-version") != monitorAnthropicAPIVersion {
t.Errorf("expected anthropic-version header, got %q", h.lastHeaders.Get("anthropic-version"))
}
}
func TestRunCheckForModel_MergeMode_UserFieldsWinButDenyListProtects(t *testing.T) {
h := &captureHandler{respondText: "the answer is 42"}
endpoint := setupFakeAnthropic(t, h)
@@ -52,10 +52,11 @@ const (
// providerGeminiPathTemplate Gemini generateContent 路径模板(含 model 占位)。
providerGeminiPathTemplate = "/v1beta/models/%s:generateContent"
// MonitorProviderOpenAI / Anthropic / Gemini provider 字符串常量(也是 ent enum 的实际值)。
// MonitorProviderOpenAI / Anthropic / Gemini / Kiro provider 字符串常量(也是 ent enum 的实际值)。
MonitorProviderOpenAI = "openai"
MonitorProviderAnthropic = "anthropic"
MonitorProviderGemini = "gemini"
MonitorProviderKiro = "kiro"
// MonitorStatusOperational 等监控状态字符串常量(与 ent enum 一致)。
MonitorStatusOperational = "operational"
@@ -110,7 +111,7 @@ var (
"CHANNEL_MONITOR_NOT_FOUND", "channel monitor not found",
)
ErrChannelMonitorInvalidProvider = infraerrors.BadRequest(
"CHANNEL_MONITOR_INVALID_PROVIDER", "provider must be one of openai/anthropic/gemini",
"CHANNEL_MONITOR_INVALID_PROVIDER", "provider must be one of openai/anthropic/gemini/kiro",
)
ErrChannelMonitorInvalidInterval = infraerrors.BadRequest(
"CHANNEL_MONITOR_INVALID_INTERVAL", "interval_seconds must be in [15, 3600]",
@@ -51,7 +51,7 @@ var (
"CHANNEL_MONITOR_TEMPLATE_NOT_FOUND", "channel monitor request template not found",
)
ErrChannelMonitorTemplateInvalidProvider = infraerrors.BadRequest(
"CHANNEL_MONITOR_TEMPLATE_INVALID_PROVIDER", "template provider must be one of openai/anthropic/gemini",
"CHANNEL_MONITOR_TEMPLATE_INVALID_PROVIDER", "template provider must be one of openai/anthropic/gemini/kiro",
)
ErrChannelMonitorTemplateMissingName = infraerrors.BadRequest(
"CHANNEL_MONITOR_TEMPLATE_MISSING_NAME", "template name is required",
+4 -3
View File
@@ -587,9 +587,10 @@ func kiroUsageToClaude(usage kiropkg.Usage, fallbackInput int) ClaudeUsage {
inputTokens = fallbackInput
}
return ClaudeUsage{
InputTokens: inputTokens,
OutputTokens: usage.OutputTokens,
CacheReadInputTokens: usage.CacheReadInputTokens,
InputTokens: inputTokens,
OutputTokens: usage.OutputTokens,
CacheReadInputTokens: usage.CacheReadInputTokens,
CacheCreationInputTokens: usage.CacheCreationInputTokens,
}
}