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
+1 -1
View File
@@ -1 +1 @@
0.1.136
0.1.137
+2 -1
View File
@@ -153,6 +153,7 @@ const (
ProviderOpenai Provider = "openai"
ProviderAnthropic Provider = "anthropic"
ProviderGemini Provider = "gemini"
ProviderKiro Provider = "kiro"
)
func (pr Provider) String() string {
@@ -162,7 +163,7 @@ func (pr Provider) String() string {
// ProviderValidator is a validator for the "provider" field enum values. It is called by the builders before save.
func ProviderValidator(pr Provider) error {
switch pr {
case ProviderOpenai, ProviderAnthropic, ProviderGemini:
case ProviderOpenai, ProviderAnthropic, ProviderGemini, ProviderKiro:
return nil
default:
return fmt.Errorf("channelmonitor: invalid enum value for provider field: %q", pr)
@@ -96,6 +96,7 @@ const (
ProviderOpenai Provider = "openai"
ProviderAnthropic Provider = "anthropic"
ProviderGemini Provider = "gemini"
ProviderKiro Provider = "kiro"
)
func (pr Provider) String() string {
@@ -105,7 +106,7 @@ func (pr Provider) String() string {
// ProviderValidator is a validator for the "provider" field enum values. It is called by the builders before save.
func ProviderValidator(pr Provider) error {
switch pr {
case ProviderOpenai, ProviderAnthropic, ProviderGemini:
case ProviderOpenai, ProviderAnthropic, ProviderGemini, ProviderKiro:
return nil
default:
return fmt.Errorf("channelmonitorrequesttemplate: invalid enum value for provider field: %q", pr)
+2 -2
View File
@@ -427,7 +427,7 @@ var (
{Name: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
{Name: "updated_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
{Name: "name", Type: field.TypeString, Size: 100},
{Name: "provider", Type: field.TypeEnum, Enums: []string{"openai", "anthropic", "gemini"}},
{Name: "provider", Type: field.TypeEnum, Enums: []string{"openai", "anthropic", "gemini", "kiro"}},
{Name: "endpoint", Type: field.TypeString, Size: 500},
{Name: "api_key_encrypted", Type: field.TypeString},
{Name: "primary_model", Type: field.TypeString, Size: 200},
@@ -565,7 +565,7 @@ var (
{Name: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
{Name: "updated_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
{Name: "name", Type: field.TypeString, Size: 100},
{Name: "provider", Type: field.TypeEnum, Enums: []string{"openai", "anthropic", "gemini"}},
{Name: "provider", Type: field.TypeEnum, Enums: []string{"openai", "anthropic", "gemini", "kiro"}},
{Name: "description", Type: field.TypeString, Nullable: true, Size: 500, Default: ""},
{Name: "extra_headers", Type: field.TypeJSON},
{Name: "body_override_mode", Type: field.TypeString, Size: 10, Default: "off"},
+1 -1
View File
@@ -35,7 +35,7 @@ func (ChannelMonitor) Fields() []ent.Field {
NotEmpty().
MaxLen(100),
field.Enum("provider").
Values("openai", "anthropic", "gemini"),
Values("openai", "anthropic", "gemini", "kiro"),
field.String("endpoint").
NotEmpty().
MaxLen(500).
@@ -39,7 +39,7 @@ func (ChannelMonitorRequestTemplate) Fields() []ent.Field {
NotEmpty().
MaxLen(100),
field.Enum("provider").
Values("openai", "anthropic", "gemini"),
Values("openai", "anthropic", "gemini", "kiro"),
field.String("description").
Optional().
Default("").
@@ -37,7 +37,7 @@ func NewChannelMonitorHandler(monitorService *service.ChannelMonitorService) *Ch
type channelMonitorCreateRequest struct {
Name string `json:"name" binding:"required,max=100"`
Provider string `json:"provider" binding:"required,oneof=openai anthropic gemini"`
Provider string `json:"provider" binding:"required,oneof=openai anthropic gemini kiro"`
Endpoint string `json:"endpoint" binding:"required,max=500"`
APIKey string `json:"api_key" binding:"required,max=2000"`
PrimaryModel string `json:"primary_model" binding:"required,max=200"`
@@ -53,7 +53,7 @@ type channelMonitorCreateRequest struct {
type channelMonitorUpdateRequest struct {
Name *string `json:"name" binding:"omitempty,max=100"`
Provider *string `json:"provider" binding:"omitempty,oneof=openai anthropic gemini"`
Provider *string `json:"provider" binding:"omitempty,oneof=openai anthropic gemini kiro"`
Endpoint *string `json:"endpoint" binding:"omitempty,max=500"`
APIKey *string `json:"api_key" binding:"omitempty,max=2000"`
PrimaryModel *string `json:"primary_model" binding:"omitempty,max=200"`
@@ -26,7 +26,7 @@ func NewChannelMonitorRequestTemplateHandler(templateService *service.ChannelMon
type channelMonitorTemplateCreateRequest struct {
Name string `json:"name" binding:"required,max=100"`
Provider string `json:"provider" binding:"required,oneof=openai anthropic gemini"`
Provider string `json:"provider" binding:"required,oneof=openai anthropic gemini kiro"`
Description string `json:"description" binding:"max=500"`
ExtraHeaders map[string]string `json:"extra_headers"`
BodyOverrideMode string `json:"body_override_mode" binding:"omitempty,oneof=off merge replace"`
+78 -8
View File
@@ -51,10 +51,11 @@ var (
)
type Usage struct {
InputTokens int
OutputTokens int
TotalTokens int
CacheReadInputTokens int
InputTokens int
OutputTokens int
TotalTokens int
CacheReadInputTokens int
CacheCreationInputTokens int
}
type StreamResult struct {
@@ -368,6 +369,8 @@ func StreamEventStreamAsAnthropicWithContext(ctx context.Context, body io.Reader
inThinkingBlock := false
stripThinkingLeadingNewline := false
sawNonThinkingBlock := false
estimatedOutputTokens := 0
contextInputTokens := 0
writeEvent := func(event string, data any) error {
payload, err := json.Marshal(data)
@@ -488,6 +491,7 @@ func StreamEventStreamAsAnthropicWithContext(ctx context.Context, body io.Reader
if toolUseID == "" || !streamingToolStarted[toolUseID] || streamingToolStopped[toolUseID] {
return nil
}
estimatedOutputTokens += countKiroTextTokens(fragment)
return writeEvent("content_block_delta", map[string]any{
"type": "content_block_delta",
"index": streamingToolBlockIndices[toolUseID],
@@ -571,6 +575,7 @@ func StreamEventStreamAsAnthropicWithContext(ctx context.Context, body io.Reader
return err
}
}
estimatedOutputTokens += countKiroTextTokens(text)
return writeEvent("content_block_delta", map[string]any{
"type": "content_block_delta",
"index": contentBlockIndex,
@@ -611,6 +616,7 @@ func StreamEventStreamAsAnthropicWithContext(ctx context.Context, body io.Reader
return err
}
inputJSON, _ := json.Marshal(tool.Input)
estimatedOutputTokens += estimateKiroOutputTokens("", []KiroToolUse{tool})
if err := writeEvent("content_block_delta", map[string]any{
"type": "content_block_delta",
"index": contentBlockIndex,
@@ -678,6 +684,7 @@ func StreamEventStreamAsAnthropicWithContext(ctx context.Context, body io.Reader
return err
}
}
estimatedOutputTokens += countKiroTextTokens(text)
return writeEvent("content_block_delta", map[string]any{
"type": "content_block_delta",
"index": thinkingBlockIndex,
@@ -883,6 +890,14 @@ func StreamEventStreamAsAnthropicWithContext(ctx context.Context, body io.Reader
if err := processStreamingToolUseEvent(event); err != nil {
return nil, err
}
case "contextUsageEvent":
contextUsage := nestedEvent(event, "contextUsageEvent")
if contextUsagePercent, ok := firstFloat(contextUsage, "contextUsagePercentage", "context_usage_percentage"); ok {
contextInputTokens = int(contextUsagePercent * float64(kiroContextWindowSize(model)) / 100)
if contextUsagePercent >= 100 && stopReason == "" {
stopReason = "model_context_window_exceeded"
}
}
case "messageMetadataEvent", "metadataEvent", "supplementaryWebLinksEvent", "usageEvent", "messageStopEvent", "message_stop":
updateUsageFromEvent(&usage, msg.EventType, event)
default:
@@ -912,6 +927,12 @@ func StreamEventStreamAsAnthropicWithContext(ctx context.Context, body io.Reader
if err := closeThinking(); err != nil {
return nil, err
}
if usage.OutputTokens == 0 && estimatedOutputTokens > 0 {
usage.OutputTokens = estimatedOutputTokens
}
if contextInputTokens > 0 {
usage.InputTokens = contextInputTokens
}
if usage.TotalTokens == 0 {
usage.TotalTokens = usage.InputTokens + usage.OutputTokens
}
@@ -935,7 +956,7 @@ func StreamEventStreamAsAnthropicWithContext(ctx context.Context, body io.Reader
"input_tokens": usage.InputTokens,
"output_tokens": usage.OutputTokens,
"cache_read_input_tokens": usage.CacheReadInputTokens,
"cache_creation_input_tokens": 0,
"cache_creation_input_tokens": usage.CacheCreationInputTokens,
},
}); err != nil {
return nil, err
@@ -1883,9 +1904,10 @@ func buildClaudeResponse(content string, toolUses []KiroToolUse, model string, u
"content": blocks,
"stop_reason": stopReason,
"usage": map[string]interface{}{
"input_tokens": usage.InputTokens,
"output_tokens": usage.OutputTokens,
"cache_read_input_tokens": usage.CacheReadInputTokens,
"input_tokens": usage.InputTokens,
"output_tokens": usage.OutputTokens,
"cache_read_input_tokens": usage.CacheReadInputTokens,
"cache_creation_input_tokens": usage.CacheCreationInputTokens,
},
}
result, _ := json.Marshal(response)
@@ -2649,6 +2671,9 @@ func updateUsageFromEvent(usage *Usage, eventType string, event map[string]inter
usage.InputTokens += value
}
}
if value, ok := firstInt(tokenUsage, "cacheCreationInputTokens", "cacheWriteInputTokens", "cacheCreateInputTokens", "cacheCreationTokens", "cacheWriteTokens", "uploadedInputTokens", "uploadInputTokens", "uploadedTokens", "uploadTokens"); ok {
usage.CacheCreationInputTokens = value
}
}
if value, ok := firstInt(event, "inputTokens", "inputTokenCount", "promptTokens", "prompt_tokens"); ok && value > 0 {
usage.InputTokens = value
@@ -2668,6 +2693,18 @@ func updateUsageFromEvent(usage *Usage, eventType string, event map[string]inter
if value, ok := firstInt(meta, "totalTokens", "totalTokenCount"); ok && value > 0 {
usage.TotalTokens = value
}
if value, ok := firstInt(event, "cacheReadInputTokens", "cachedInputTokens", "cacheReadTokens", "cachedTokens", "cached_tokens"); ok && value > 0 {
usage.CacheReadInputTokens = value
}
if value, ok := firstInt(meta, "cacheReadInputTokens", "cachedInputTokens", "cacheReadTokens", "cachedTokens", "cached_tokens"); ok && value > 0 {
usage.CacheReadInputTokens = value
}
if value, ok := firstInt(event, "cacheCreationInputTokens", "cacheWriteInputTokens", "cacheCreateInputTokens", "cacheCreationTokens", "cacheWriteTokens", "uploadedInputTokens", "uploadInputTokens", "uploadedTokens", "uploadTokens"); ok && value > 0 {
usage.CacheCreationInputTokens = value
}
if value, ok := firstInt(meta, "cacheCreationInputTokens", "cacheWriteInputTokens", "cacheCreateInputTokens", "cacheCreationTokens", "cacheWriteTokens", "uploadedInputTokens", "uploadInputTokens", "uploadedTokens", "uploadTokens"); ok && value > 0 {
usage.CacheCreationInputTokens = value
}
}
func firstInt(m map[string]interface{}, keys ...string) (int, bool) {
@@ -2679,6 +2716,39 @@ func firstInt(m map[string]interface{}, keys ...string) (int, bool) {
return 0, false
}
func firstFloat(m map[string]interface{}, keys ...string) (float64, bool) {
for _, key := range keys {
switch v := m[key].(type) {
case float64:
return v, true
case float32:
return float64(v), true
case int:
return float64(v), true
case int64:
return float64(v), true
case json.Number:
if parsed, err := v.Float64(); err == nil {
return parsed, true
}
case string:
if parsed, err := strconv.ParseFloat(strings.TrimSpace(v), 64); err == nil {
return parsed, true
}
}
}
return 0, false
}
func kiroContextWindowSize(model string) int {
switch MapModel(model) {
case "claude-opus-4.6", "claude-sonnet-4.6":
return 1_000_000
default:
return 200_000
}
}
func estimateKiroOutputTokens(content string, toolUses []KiroToolUse) int {
total := countKiroTextTokens(content)
for _, tool := range toolUses {
+105 -1
View File
@@ -292,6 +292,7 @@ func TestParseNonStreamingEventStreamUsageAliases(t *testing.T) {
"inputTokenCount": 12,
"completionTokens": 7,
"cachedTokens": 3,
"uploadedTokens": 5,
"totalTokenCount": 22,
},
},
@@ -302,8 +303,10 @@ func TestParseNonStreamingEventStreamUsageAliases(t *testing.T) {
require.Equal(t, 15, result.Usage.InputTokens)
require.Equal(t, 7, result.Usage.OutputTokens)
require.Equal(t, 3, result.Usage.CacheReadInputTokens)
require.Equal(t, 5, result.Usage.CacheCreationInputTokens)
require.Equal(t, 22, result.Usage.TotalTokens)
require.Equal(t, float64(3), gjson.GetBytes(result.ResponseBody, "usage.cache_read_input_tokens").Float())
require.Equal(t, float64(5), gjson.GetBytes(result.ResponseBody, "usage.cache_creation_input_tokens").Float())
}
func TestParseNonStreamingEventStreamEstimatesMissingOutputTokens(t *testing.T) {
@@ -316,7 +319,7 @@ func TestParseNonStreamingEventStreamEstimatesMissingOutputTokens(t *testing.T)
_, _ = stream.Write(buildEventStreamFrame(t, "messageMetadataEvent", map[string]any{
"messageMetadataEvent": map[string]any{
"tokenUsage": map[string]any{
"uncachedInputTokens": 12,
"uncachedInputTokens": 12,
"cacheReadInputTokens": 3,
},
},
@@ -581,6 +584,89 @@ func TestStreamEventStreamAsAnthropicSkipsLeadingWhitespaceOnlyChunk(t *testing.
require.NotContains(t, output, `"delta":{"text":"","type":"text_delta"}`)
}
func TestStreamEventStreamAsAnthropicUsageAliases(t *testing.T) {
stream := bytes.NewBuffer(nil)
_, _ = stream.Write(buildEventStreamFrame(t, "assistantResponseEvent", map[string]any{
"assistantResponseEvent": map[string]any{
"content": "hello",
},
}))
_, _ = stream.Write(buildEventStreamFrame(t, "metadataEvent", map[string]any{
"metadataEvent": map[string]any{
"tokenUsage": map[string]any{
"inputTokenCount": 12,
"completionTokens": 7,
"cachedTokens": 3,
"uploadedTokens": 5,
"totalTokenCount": 27,
},
},
}))
var out bytes.Buffer
result, err := StreamEventStreamAsAnthropic(context.Background(), stream, &out, "claude-sonnet-4-5", 9)
require.NoError(t, err)
require.Equal(t, 15, result.Usage.InputTokens)
require.Equal(t, 7, result.Usage.OutputTokens)
require.Equal(t, 3, result.Usage.CacheReadInputTokens)
require.Equal(t, 5, result.Usage.CacheCreationInputTokens)
require.Equal(t, 27, result.Usage.TotalTokens)
finalUsage := lastStreamUsage(t, out.String())
require.Equal(t, int64(15), finalUsage.Get("input_tokens").Int())
require.Equal(t, int64(7), finalUsage.Get("output_tokens").Int())
require.Equal(t, int64(3), finalUsage.Get("cache_read_input_tokens").Int())
require.Equal(t, int64(5), finalUsage.Get("cache_creation_input_tokens").Int())
}
func TestStreamEventStreamAsAnthropicEstimatesMissingOutputTokens(t *testing.T) {
stream := bytes.NewBuffer(nil)
_, _ = stream.Write(buildEventStreamFrame(t, "assistantResponseEvent", map[string]any{
"assistantResponseEvent": map[string]any{
"content": "Hello from Kiro",
},
}))
_, _ = stream.Write(buildEventStreamFrame(t, "messageMetadataEvent", map[string]any{
"messageMetadataEvent": map[string]any{
"tokenUsage": map[string]any{
"uncachedInputTokens": 9,
},
},
}))
var out bytes.Buffer
result, err := StreamEventStreamAsAnthropic(context.Background(), stream, &out, "claude-sonnet-4-5", 9)
require.NoError(t, err)
require.Equal(t, 9, result.Usage.InputTokens)
require.Equal(t, 4, result.Usage.OutputTokens)
require.Equal(t, 13, result.Usage.TotalTokens)
finalUsage := lastStreamUsage(t, out.String())
require.Equal(t, int64(4), finalUsage.Get("output_tokens").Int())
}
func TestStreamEventStreamAsAnthropicUsesContextUsageInputTokens(t *testing.T) {
stream := bytes.NewBuffer(nil)
_, _ = stream.Write(buildEventStreamFrame(t, "contextUsageEvent", map[string]any{
"contextUsageEvent": map[string]any{
"contextUsagePercentage": 2.5,
},
}))
_, _ = stream.Write(buildEventStreamFrame(t, "assistantResponseEvent", map[string]any{
"assistantResponseEvent": map[string]any{
"content": "ok",
},
}))
var out bytes.Buffer
result, err := StreamEventStreamAsAnthropic(context.Background(), stream, &out, "claude-opus-4.6", 9)
require.NoError(t, err)
require.Equal(t, 25000, result.Usage.InputTokens)
finalUsage := lastStreamUsage(t, out.String())
require.Equal(t, int64(25000), finalUsage.Get("input_tokens").Int())
}
func TestStreamEventStreamAsAnthropicSkipsTrailingWhitespaceOnlyChunk(t *testing.T) {
stream := bytes.NewBuffer(nil)
_, _ = stream.Write(buildEventStreamFrame(t, "assistantResponseEvent", map[string]any{
@@ -1271,3 +1357,21 @@ func buildEventStreamFrame(t *testing.T, eventType string, payload any) []byte {
require.NoError(t, binary.Write(frame, binary.BigEndian, uint32(0)))
return frame.Bytes()
}
func lastStreamUsage(t *testing.T, output string) gjson.Result {
t.Helper()
var usage gjson.Result
for _, frame := range strings.Split(output, "\n\n") {
if !strings.Contains(frame, "event: message_delta") {
continue
}
for _, line := range strings.Split(frame, "\n") {
if !strings.HasPrefix(line, "data: ") {
continue
}
usage = gjson.Get(strings.TrimPrefix(line, "data: "), "usage")
}
}
require.True(t, usage.Exists(), "message_delta usage not found in stream: %s", output)
return usage
}
@@ -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,
}
}
@@ -0,0 +1,35 @@
-- Migration: 137_allow_kiro_channel_monitor_provider
-- 渠道监控新增 Kiro 平台。Kiro 网关使用 Anthropic Messages 兼容协议,
-- 但监控配置和模板需要独立 provider 归属,便于筛选和复用。
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE table_name = 'channel_monitors'
AND constraint_name = 'channel_monitors_provider_check'
) THEN
ALTER TABLE channel_monitors
DROP CONSTRAINT channel_monitors_provider_check;
END IF;
ALTER TABLE channel_monitors
ADD CONSTRAINT channel_monitors_provider_check
CHECK (provider IN ('openai', 'anthropic', 'gemini', 'kiro'));
END $$;
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.table_constraints
WHERE table_name = 'channel_monitor_request_templates'
AND constraint_name = 'channel_monitor_request_templates_provider_check'
) THEN
ALTER TABLE channel_monitor_request_templates
DROP CONSTRAINT channel_monitor_request_templates_provider_check;
END IF;
ALTER TABLE channel_monitor_request_templates
ADD CONSTRAINT channel_monitor_request_templates_provider_check
CHECK (provider IN ('openai', 'anthropic', 'gemini', 'kiro'));
END $$;