release: prepare v0.1.137
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user