release: prepare v0.1.137
This commit is contained in:
@@ -1 +1 @@
|
|||||||
0.1.136
|
0.1.137
|
||||||
|
|||||||
@@ -153,6 +153,7 @@ const (
|
|||||||
ProviderOpenai Provider = "openai"
|
ProviderOpenai Provider = "openai"
|
||||||
ProviderAnthropic Provider = "anthropic"
|
ProviderAnthropic Provider = "anthropic"
|
||||||
ProviderGemini Provider = "gemini"
|
ProviderGemini Provider = "gemini"
|
||||||
|
ProviderKiro Provider = "kiro"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (pr Provider) String() string {
|
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.
|
// ProviderValidator is a validator for the "provider" field enum values. It is called by the builders before save.
|
||||||
func ProviderValidator(pr Provider) error {
|
func ProviderValidator(pr Provider) error {
|
||||||
switch pr {
|
switch pr {
|
||||||
case ProviderOpenai, ProviderAnthropic, ProviderGemini:
|
case ProviderOpenai, ProviderAnthropic, ProviderGemini, ProviderKiro:
|
||||||
return nil
|
return nil
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("channelmonitor: invalid enum value for provider field: %q", pr)
|
return fmt.Errorf("channelmonitor: invalid enum value for provider field: %q", pr)
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ const (
|
|||||||
ProviderOpenai Provider = "openai"
|
ProviderOpenai Provider = "openai"
|
||||||
ProviderAnthropic Provider = "anthropic"
|
ProviderAnthropic Provider = "anthropic"
|
||||||
ProviderGemini Provider = "gemini"
|
ProviderGemini Provider = "gemini"
|
||||||
|
ProviderKiro Provider = "kiro"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (pr Provider) String() string {
|
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.
|
// ProviderValidator is a validator for the "provider" field enum values. It is called by the builders before save.
|
||||||
func ProviderValidator(pr Provider) error {
|
func ProviderValidator(pr Provider) error {
|
||||||
switch pr {
|
switch pr {
|
||||||
case ProviderOpenai, ProviderAnthropic, ProviderGemini:
|
case ProviderOpenai, ProviderAnthropic, ProviderGemini, ProviderKiro:
|
||||||
return nil
|
return nil
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("channelmonitorrequesttemplate: invalid enum value for provider field: %q", pr)
|
return fmt.Errorf("channelmonitorrequesttemplate: invalid enum value for provider field: %q", pr)
|
||||||
|
|||||||
@@ -427,7 +427,7 @@ var (
|
|||||||
{Name: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
{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: "updated_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
||||||
{Name: "name", Type: field.TypeString, Size: 100},
|
{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: "endpoint", Type: field.TypeString, Size: 500},
|
||||||
{Name: "api_key_encrypted", Type: field.TypeString},
|
{Name: "api_key_encrypted", Type: field.TypeString},
|
||||||
{Name: "primary_model", Type: field.TypeString, Size: 200},
|
{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: "created_at", Type: field.TypeTime, SchemaType: map[string]string{"postgres": "timestamptz"}},
|
||||||
{Name: "updated_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: "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: "description", Type: field.TypeString, Nullable: true, Size: 500, Default: ""},
|
||||||
{Name: "extra_headers", Type: field.TypeJSON},
|
{Name: "extra_headers", Type: field.TypeJSON},
|
||||||
{Name: "body_override_mode", Type: field.TypeString, Size: 10, Default: "off"},
|
{Name: "body_override_mode", Type: field.TypeString, Size: 10, Default: "off"},
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ func (ChannelMonitor) Fields() []ent.Field {
|
|||||||
NotEmpty().
|
NotEmpty().
|
||||||
MaxLen(100),
|
MaxLen(100),
|
||||||
field.Enum("provider").
|
field.Enum("provider").
|
||||||
Values("openai", "anthropic", "gemini"),
|
Values("openai", "anthropic", "gemini", "kiro"),
|
||||||
field.String("endpoint").
|
field.String("endpoint").
|
||||||
NotEmpty().
|
NotEmpty().
|
||||||
MaxLen(500).
|
MaxLen(500).
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ func (ChannelMonitorRequestTemplate) Fields() []ent.Field {
|
|||||||
NotEmpty().
|
NotEmpty().
|
||||||
MaxLen(100),
|
MaxLen(100),
|
||||||
field.Enum("provider").
|
field.Enum("provider").
|
||||||
Values("openai", "anthropic", "gemini"),
|
Values("openai", "anthropic", "gemini", "kiro"),
|
||||||
field.String("description").
|
field.String("description").
|
||||||
Optional().
|
Optional().
|
||||||
Default("").
|
Default("").
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ func NewChannelMonitorHandler(monitorService *service.ChannelMonitorService) *Ch
|
|||||||
|
|
||||||
type channelMonitorCreateRequest struct {
|
type channelMonitorCreateRequest struct {
|
||||||
Name string `json:"name" binding:"required,max=100"`
|
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"`
|
Endpoint string `json:"endpoint" binding:"required,max=500"`
|
||||||
APIKey string `json:"api_key" binding:"required,max=2000"`
|
APIKey string `json:"api_key" binding:"required,max=2000"`
|
||||||
PrimaryModel string `json:"primary_model" binding:"required,max=200"`
|
PrimaryModel string `json:"primary_model" binding:"required,max=200"`
|
||||||
@@ -53,7 +53,7 @@ type channelMonitorCreateRequest struct {
|
|||||||
|
|
||||||
type channelMonitorUpdateRequest struct {
|
type channelMonitorUpdateRequest struct {
|
||||||
Name *string `json:"name" binding:"omitempty,max=100"`
|
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"`
|
Endpoint *string `json:"endpoint" binding:"omitempty,max=500"`
|
||||||
APIKey *string `json:"api_key" binding:"omitempty,max=2000"`
|
APIKey *string `json:"api_key" binding:"omitempty,max=2000"`
|
||||||
PrimaryModel *string `json:"primary_model" binding:"omitempty,max=200"`
|
PrimaryModel *string `json:"primary_model" binding:"omitempty,max=200"`
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ func NewChannelMonitorRequestTemplateHandler(templateService *service.ChannelMon
|
|||||||
|
|
||||||
type channelMonitorTemplateCreateRequest struct {
|
type channelMonitorTemplateCreateRequest struct {
|
||||||
Name string `json:"name" binding:"required,max=100"`
|
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"`
|
Description string `json:"description" binding:"max=500"`
|
||||||
ExtraHeaders map[string]string `json:"extra_headers"`
|
ExtraHeaders map[string]string `json:"extra_headers"`
|
||||||
BodyOverrideMode string `json:"body_override_mode" binding:"omitempty,oneof=off merge replace"`
|
BodyOverrideMode string `json:"body_override_mode" binding:"omitempty,oneof=off merge replace"`
|
||||||
|
|||||||
@@ -51,10 +51,11 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Usage struct {
|
type Usage struct {
|
||||||
InputTokens int
|
InputTokens int
|
||||||
OutputTokens int
|
OutputTokens int
|
||||||
TotalTokens int
|
TotalTokens int
|
||||||
CacheReadInputTokens int
|
CacheReadInputTokens int
|
||||||
|
CacheCreationInputTokens int
|
||||||
}
|
}
|
||||||
|
|
||||||
type StreamResult struct {
|
type StreamResult struct {
|
||||||
@@ -368,6 +369,8 @@ func StreamEventStreamAsAnthropicWithContext(ctx context.Context, body io.Reader
|
|||||||
inThinkingBlock := false
|
inThinkingBlock := false
|
||||||
stripThinkingLeadingNewline := false
|
stripThinkingLeadingNewline := false
|
||||||
sawNonThinkingBlock := false
|
sawNonThinkingBlock := false
|
||||||
|
estimatedOutputTokens := 0
|
||||||
|
contextInputTokens := 0
|
||||||
|
|
||||||
writeEvent := func(event string, data any) error {
|
writeEvent := func(event string, data any) error {
|
||||||
payload, err := json.Marshal(data)
|
payload, err := json.Marshal(data)
|
||||||
@@ -488,6 +491,7 @@ func StreamEventStreamAsAnthropicWithContext(ctx context.Context, body io.Reader
|
|||||||
if toolUseID == "" || !streamingToolStarted[toolUseID] || streamingToolStopped[toolUseID] {
|
if toolUseID == "" || !streamingToolStarted[toolUseID] || streamingToolStopped[toolUseID] {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
estimatedOutputTokens += countKiroTextTokens(fragment)
|
||||||
return writeEvent("content_block_delta", map[string]any{
|
return writeEvent("content_block_delta", map[string]any{
|
||||||
"type": "content_block_delta",
|
"type": "content_block_delta",
|
||||||
"index": streamingToolBlockIndices[toolUseID],
|
"index": streamingToolBlockIndices[toolUseID],
|
||||||
@@ -571,6 +575,7 @@ func StreamEventStreamAsAnthropicWithContext(ctx context.Context, body io.Reader
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
estimatedOutputTokens += countKiroTextTokens(text)
|
||||||
return writeEvent("content_block_delta", map[string]any{
|
return writeEvent("content_block_delta", map[string]any{
|
||||||
"type": "content_block_delta",
|
"type": "content_block_delta",
|
||||||
"index": contentBlockIndex,
|
"index": contentBlockIndex,
|
||||||
@@ -611,6 +616,7 @@ func StreamEventStreamAsAnthropicWithContext(ctx context.Context, body io.Reader
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
inputJSON, _ := json.Marshal(tool.Input)
|
inputJSON, _ := json.Marshal(tool.Input)
|
||||||
|
estimatedOutputTokens += estimateKiroOutputTokens("", []KiroToolUse{tool})
|
||||||
if err := writeEvent("content_block_delta", map[string]any{
|
if err := writeEvent("content_block_delta", map[string]any{
|
||||||
"type": "content_block_delta",
|
"type": "content_block_delta",
|
||||||
"index": contentBlockIndex,
|
"index": contentBlockIndex,
|
||||||
@@ -678,6 +684,7 @@ func StreamEventStreamAsAnthropicWithContext(ctx context.Context, body io.Reader
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
estimatedOutputTokens += countKiroTextTokens(text)
|
||||||
return writeEvent("content_block_delta", map[string]any{
|
return writeEvent("content_block_delta", map[string]any{
|
||||||
"type": "content_block_delta",
|
"type": "content_block_delta",
|
||||||
"index": thinkingBlockIndex,
|
"index": thinkingBlockIndex,
|
||||||
@@ -883,6 +890,14 @@ func StreamEventStreamAsAnthropicWithContext(ctx context.Context, body io.Reader
|
|||||||
if err := processStreamingToolUseEvent(event); err != nil {
|
if err := processStreamingToolUseEvent(event); err != nil {
|
||||||
return nil, err
|
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":
|
case "messageMetadataEvent", "metadataEvent", "supplementaryWebLinksEvent", "usageEvent", "messageStopEvent", "message_stop":
|
||||||
updateUsageFromEvent(&usage, msg.EventType, event)
|
updateUsageFromEvent(&usage, msg.EventType, event)
|
||||||
default:
|
default:
|
||||||
@@ -912,6 +927,12 @@ func StreamEventStreamAsAnthropicWithContext(ctx context.Context, body io.Reader
|
|||||||
if err := closeThinking(); err != nil {
|
if err := closeThinking(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if usage.OutputTokens == 0 && estimatedOutputTokens > 0 {
|
||||||
|
usage.OutputTokens = estimatedOutputTokens
|
||||||
|
}
|
||||||
|
if contextInputTokens > 0 {
|
||||||
|
usage.InputTokens = contextInputTokens
|
||||||
|
}
|
||||||
if usage.TotalTokens == 0 {
|
if usage.TotalTokens == 0 {
|
||||||
usage.TotalTokens = usage.InputTokens + usage.OutputTokens
|
usage.TotalTokens = usage.InputTokens + usage.OutputTokens
|
||||||
}
|
}
|
||||||
@@ -935,7 +956,7 @@ func StreamEventStreamAsAnthropicWithContext(ctx context.Context, body io.Reader
|
|||||||
"input_tokens": usage.InputTokens,
|
"input_tokens": usage.InputTokens,
|
||||||
"output_tokens": usage.OutputTokens,
|
"output_tokens": usage.OutputTokens,
|
||||||
"cache_read_input_tokens": usage.CacheReadInputTokens,
|
"cache_read_input_tokens": usage.CacheReadInputTokens,
|
||||||
"cache_creation_input_tokens": 0,
|
"cache_creation_input_tokens": usage.CacheCreationInputTokens,
|
||||||
},
|
},
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -1883,9 +1904,10 @@ func buildClaudeResponse(content string, toolUses []KiroToolUse, model string, u
|
|||||||
"content": blocks,
|
"content": blocks,
|
||||||
"stop_reason": stopReason,
|
"stop_reason": stopReason,
|
||||||
"usage": map[string]interface{}{
|
"usage": map[string]interface{}{
|
||||||
"input_tokens": usage.InputTokens,
|
"input_tokens": usage.InputTokens,
|
||||||
"output_tokens": usage.OutputTokens,
|
"output_tokens": usage.OutputTokens,
|
||||||
"cache_read_input_tokens": usage.CacheReadInputTokens,
|
"cache_read_input_tokens": usage.CacheReadInputTokens,
|
||||||
|
"cache_creation_input_tokens": usage.CacheCreationInputTokens,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
result, _ := json.Marshal(response)
|
result, _ := json.Marshal(response)
|
||||||
@@ -2649,6 +2671,9 @@ func updateUsageFromEvent(usage *Usage, eventType string, event map[string]inter
|
|||||||
usage.InputTokens += value
|
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 {
|
if value, ok := firstInt(event, "inputTokens", "inputTokenCount", "promptTokens", "prompt_tokens"); ok && value > 0 {
|
||||||
usage.InputTokens = value
|
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 {
|
if value, ok := firstInt(meta, "totalTokens", "totalTokenCount"); ok && value > 0 {
|
||||||
usage.TotalTokens = value
|
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) {
|
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
|
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 {
|
func estimateKiroOutputTokens(content string, toolUses []KiroToolUse) int {
|
||||||
total := countKiroTextTokens(content)
|
total := countKiroTextTokens(content)
|
||||||
for _, tool := range toolUses {
|
for _, tool := range toolUses {
|
||||||
|
|||||||
@@ -292,6 +292,7 @@ func TestParseNonStreamingEventStreamUsageAliases(t *testing.T) {
|
|||||||
"inputTokenCount": 12,
|
"inputTokenCount": 12,
|
||||||
"completionTokens": 7,
|
"completionTokens": 7,
|
||||||
"cachedTokens": 3,
|
"cachedTokens": 3,
|
||||||
|
"uploadedTokens": 5,
|
||||||
"totalTokenCount": 22,
|
"totalTokenCount": 22,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -302,8 +303,10 @@ func TestParseNonStreamingEventStreamUsageAliases(t *testing.T) {
|
|||||||
require.Equal(t, 15, result.Usage.InputTokens)
|
require.Equal(t, 15, result.Usage.InputTokens)
|
||||||
require.Equal(t, 7, result.Usage.OutputTokens)
|
require.Equal(t, 7, result.Usage.OutputTokens)
|
||||||
require.Equal(t, 3, result.Usage.CacheReadInputTokens)
|
require.Equal(t, 3, result.Usage.CacheReadInputTokens)
|
||||||
|
require.Equal(t, 5, result.Usage.CacheCreationInputTokens)
|
||||||
require.Equal(t, 22, result.Usage.TotalTokens)
|
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(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) {
|
func TestParseNonStreamingEventStreamEstimatesMissingOutputTokens(t *testing.T) {
|
||||||
@@ -316,7 +319,7 @@ func TestParseNonStreamingEventStreamEstimatesMissingOutputTokens(t *testing.T)
|
|||||||
_, _ = stream.Write(buildEventStreamFrame(t, "messageMetadataEvent", map[string]any{
|
_, _ = stream.Write(buildEventStreamFrame(t, "messageMetadataEvent", map[string]any{
|
||||||
"messageMetadataEvent": map[string]any{
|
"messageMetadataEvent": map[string]any{
|
||||||
"tokenUsage": map[string]any{
|
"tokenUsage": map[string]any{
|
||||||
"uncachedInputTokens": 12,
|
"uncachedInputTokens": 12,
|
||||||
"cacheReadInputTokens": 3,
|
"cacheReadInputTokens": 3,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -581,6 +584,89 @@ func TestStreamEventStreamAsAnthropicSkipsLeadingWhitespaceOnlyChunk(t *testing.
|
|||||||
require.NotContains(t, output, `"delta":{"text":"","type":"text_delta"}`)
|
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) {
|
func TestStreamEventStreamAsAnthropicSkipsTrailingWhitespaceOnlyChunk(t *testing.T) {
|
||||||
stream := bytes.NewBuffer(nil)
|
stream := bytes.NewBuffer(nil)
|
||||||
_, _ = stream.Write(buildEventStreamFrame(t, "assistantResponseEvent", map[string]any{
|
_, _ = 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)))
|
require.NoError(t, binary.Write(frame, binary.BigEndian, uint32(0)))
|
||||||
return frame.Bytes()
|
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",
|
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: {
|
MonitorProviderGemini: {
|
||||||
// Gemini 把 model 名写在 URL path 上:/v1beta/models/{model}:generateContent
|
// Gemini 把 model 名写在 URL path 上:/v1beta/models/{model}:generateContent
|
||||||
buildPath: func(model string) string { return fmt.Sprintf(providerGeminiPathTemplate, model) },
|
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{
|
var bodyMergeKeyDenyList = map[string]map[string]bool{
|
||||||
MonitorProviderOpenAI: {"model": true, "messages": true, "stream": true},
|
MonitorProviderOpenAI: {"model": true, "messages": true, "stream": true},
|
||||||
MonitorProviderAnthropic: {"model": true, "messages": true},
|
MonitorProviderAnthropic: {"model": true, "messages": true},
|
||||||
|
MonitorProviderKiro: {"model": true, "messages": true},
|
||||||
MonitorProviderGemini: {"contents": true},
|
MonitorProviderGemini: {"contents": true},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -36,6 +38,11 @@ func (h *captureHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
_ = json.NewDecoder(r.Body).Decode(&parsed)
|
_ = json.NewDecoder(r.Body).Decode(&parsed)
|
||||||
h.lastBody = parsed
|
h.lastBody = parsed
|
||||||
|
|
||||||
|
respondText := h.respondText
|
||||||
|
if respondText == autoChallengeResponse {
|
||||||
|
respondText = solveMonitorChallengeFromBody(parsed)
|
||||||
|
}
|
||||||
|
|
||||||
if h.status == 0 {
|
if h.status == 0 {
|
||||||
h.status = 200
|
h.status = 200
|
||||||
}
|
}
|
||||||
@@ -44,11 +51,35 @@ func (h *captureHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
// 构造 Anthropic 格式的响应:content[0].text = h.respondText
|
// 构造 Anthropic 格式的响应:content[0].text = h.respondText
|
||||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
"content": []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 {
|
func setupFakeAnthropic(t *testing.T, handler *captureHandler) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
swapMonitorHTTPClient(t)
|
swapMonitorHTTPClient(t)
|
||||||
@@ -58,7 +89,7 @@ func setupFakeAnthropic(t *testing.T, handler *captureHandler) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRunCheckForModel_OffMode_PreservesDefaultBody(t *testing.T) {
|
func TestRunCheckForModel_OffMode_PreservesDefaultBody(t *testing.T) {
|
||||||
h := &captureHandler{respondText: "the answer is 42"}
|
h := &captureHandler{respondText: autoChallengeResponse}
|
||||||
endpoint := setupFakeAnthropic(t, h)
|
endpoint := setupFakeAnthropic(t, h)
|
||||||
|
|
||||||
// 跑一次 off 模式(opts=nil),确认默认 body 行为未变
|
// 跑一次 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) {
|
func TestRunCheckForModel_MergeMode_UserFieldsWinButDenyListProtects(t *testing.T) {
|
||||||
h := &captureHandler{respondText: "the answer is 42"}
|
h := &captureHandler{respondText: "the answer is 42"}
|
||||||
endpoint := setupFakeAnthropic(t, h)
|
endpoint := setupFakeAnthropic(t, h)
|
||||||
|
|||||||
@@ -52,10 +52,11 @@ const (
|
|||||||
// providerGeminiPathTemplate Gemini generateContent 路径模板(含 model 占位)。
|
// providerGeminiPathTemplate Gemini generateContent 路径模板(含 model 占位)。
|
||||||
providerGeminiPathTemplate = "/v1beta/models/%s:generateContent"
|
providerGeminiPathTemplate = "/v1beta/models/%s:generateContent"
|
||||||
|
|
||||||
// MonitorProviderOpenAI / Anthropic / Gemini provider 字符串常量(也是 ent enum 的实际值)。
|
// MonitorProviderOpenAI / Anthropic / Gemini / Kiro provider 字符串常量(也是 ent enum 的实际值)。
|
||||||
MonitorProviderOpenAI = "openai"
|
MonitorProviderOpenAI = "openai"
|
||||||
MonitorProviderAnthropic = "anthropic"
|
MonitorProviderAnthropic = "anthropic"
|
||||||
MonitorProviderGemini = "gemini"
|
MonitorProviderGemini = "gemini"
|
||||||
|
MonitorProviderKiro = "kiro"
|
||||||
|
|
||||||
// MonitorStatusOperational 等监控状态字符串常量(与 ent enum 一致)。
|
// MonitorStatusOperational 等监控状态字符串常量(与 ent enum 一致)。
|
||||||
MonitorStatusOperational = "operational"
|
MonitorStatusOperational = "operational"
|
||||||
@@ -110,7 +111,7 @@ var (
|
|||||||
"CHANNEL_MONITOR_NOT_FOUND", "channel monitor not found",
|
"CHANNEL_MONITOR_NOT_FOUND", "channel monitor not found",
|
||||||
)
|
)
|
||||||
ErrChannelMonitorInvalidProvider = infraerrors.BadRequest(
|
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(
|
ErrChannelMonitorInvalidInterval = infraerrors.BadRequest(
|
||||||
"CHANNEL_MONITOR_INVALID_INTERVAL", "interval_seconds must be in [15, 3600]",
|
"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",
|
"CHANNEL_MONITOR_TEMPLATE_NOT_FOUND", "channel monitor request template not found",
|
||||||
)
|
)
|
||||||
ErrChannelMonitorTemplateInvalidProvider = infraerrors.BadRequest(
|
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(
|
ErrChannelMonitorTemplateMissingName = infraerrors.BadRequest(
|
||||||
"CHANNEL_MONITOR_TEMPLATE_MISSING_NAME", "template name is required",
|
"CHANNEL_MONITOR_TEMPLATE_MISSING_NAME", "template name is required",
|
||||||
|
|||||||
@@ -587,9 +587,10 @@ func kiroUsageToClaude(usage kiropkg.Usage, fallbackInput int) ClaudeUsage {
|
|||||||
inputTokens = fallbackInput
|
inputTokens = fallbackInput
|
||||||
}
|
}
|
||||||
return ClaudeUsage{
|
return ClaudeUsage{
|
||||||
InputTokens: inputTokens,
|
InputTokens: inputTokens,
|
||||||
OutputTokens: usage.OutputTokens,
|
OutputTokens: usage.OutputTokens,
|
||||||
CacheReadInputTokens: usage.CacheReadInputTokens,
|
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 $$;
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import { apiClient } from '../client'
|
import { apiClient } from '../client'
|
||||||
|
|
||||||
export type Provider = 'openai' | 'anthropic' | 'gemini'
|
export type Provider = 'openai' | 'anthropic' | 'gemini' | 'kiro'
|
||||||
export type MonitorStatus = 'operational' | 'degraded' | 'failed' | 'error'
|
export type MonitorStatus = 'operational' | 'degraded' | 'failed' | 'error'
|
||||||
export type BodyOverrideMode = 'off' | 'merge' | 'replace'
|
export type BodyOverrideMode = 'off' | 'merge' | 'replace'
|
||||||
|
|
||||||
|
|||||||
@@ -184,6 +184,7 @@ export function getPlatformTagClass(platform: string): string {
|
|||||||
case 'openai': return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'
|
case 'openai': return 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-400'
|
||||||
case 'gemini': return 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
|
case 'gemini': return 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
|
||||||
case 'antigravity': return 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400'
|
case 'antigravity': return 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400'
|
||||||
|
case 'kiro': return 'bg-violet-100 text-violet-700 dark:bg-violet-900/30 dark:text-violet-400'
|
||||||
default: return 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400'
|
default: return 'bg-gray-100 text-gray-700 dark:bg-gray-900/30 dark:text-gray-400'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -195,6 +196,7 @@ export function getPlatformTextClass(platform: string): string {
|
|||||||
case 'openai': return 'text-emerald-700 dark:text-emerald-400'
|
case 'openai': return 'text-emerald-700 dark:text-emerald-400'
|
||||||
case 'gemini': return 'text-blue-700 dark:text-blue-400'
|
case 'gemini': return 'text-blue-700 dark:text-blue-400'
|
||||||
case 'antigravity': return 'text-purple-700 dark:text-purple-400'
|
case 'antigravity': return 'text-purple-700 dark:text-purple-400'
|
||||||
|
case 'kiro': return 'text-violet-700 dark:text-violet-400'
|
||||||
default: return ''
|
default: return ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ import {
|
|||||||
PROVIDER_OPENAI,
|
PROVIDER_OPENAI,
|
||||||
PROVIDER_ANTHROPIC,
|
PROVIDER_ANTHROPIC,
|
||||||
PROVIDER_GEMINI,
|
PROVIDER_GEMINI,
|
||||||
|
PROVIDER_KIRO,
|
||||||
} from '@/constants/channelMonitor'
|
} from '@/constants/channelMonitor'
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -94,6 +95,7 @@ const providerFilterOptions = computed(() => [
|
|||||||
{ value: PROVIDER_OPENAI, label: t('monitorCommon.providers.openai') },
|
{ value: PROVIDER_OPENAI, label: t('monitorCommon.providers.openai') },
|
||||||
{ value: PROVIDER_ANTHROPIC, label: t('monitorCommon.providers.anthropic') },
|
{ value: PROVIDER_ANTHROPIC, label: t('monitorCommon.providers.anthropic') },
|
||||||
{ value: PROVIDER_GEMINI, label: t('monitorCommon.providers.gemini') },
|
{ value: PROVIDER_GEMINI, label: t('monitorCommon.providers.gemini') },
|
||||||
|
{ value: PROVIDER_KIRO, label: t('monitorCommon.providers.kiro') },
|
||||||
])
|
])
|
||||||
|
|
||||||
const enabledFilterOptions = computed(() => [
|
const enabledFilterOptions = computed(() => [
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="input-label">{{ t('admin.channelMonitor.form.provider') }} <span class="text-red-500">*</span></label>
|
<label class="input-label">{{ t('admin.channelMonitor.form.provider') }} <span class="text-red-500">*</span></label>
|
||||||
<div class="grid grid-cols-3 gap-3">
|
<div class="grid grid-cols-2 gap-3 sm:grid-cols-4">
|
||||||
<button
|
<button
|
||||||
v-for="opt in providerOptions"
|
v-for="opt in providerOptions"
|
||||||
:key="opt.value"
|
:key="opt.value"
|
||||||
@@ -186,6 +186,7 @@ import {
|
|||||||
PROVIDER_OPENAI,
|
PROVIDER_OPENAI,
|
||||||
PROVIDER_ANTHROPIC,
|
PROVIDER_ANTHROPIC,
|
||||||
PROVIDER_GEMINI,
|
PROVIDER_GEMINI,
|
||||||
|
PROVIDER_KIRO,
|
||||||
DEFAULT_INTERVAL_SECONDS,
|
DEFAULT_INTERVAL_SECONDS,
|
||||||
} from '@/constants/channelMonitor'
|
} from '@/constants/channelMonitor'
|
||||||
|
|
||||||
@@ -310,6 +311,7 @@ const providerOptions = computed<ProviderOption[]>(() => [
|
|||||||
{ value: PROVIDER_ANTHROPIC, label: t('monitorCommon.providers.anthropic') },
|
{ value: PROVIDER_ANTHROPIC, label: t('monitorCommon.providers.anthropic') },
|
||||||
{ value: PROVIDER_OPENAI, label: t('monitorCommon.providers.openai') },
|
{ value: PROVIDER_OPENAI, label: t('monitorCommon.providers.openai') },
|
||||||
{ value: PROVIDER_GEMINI, label: t('monitorCommon.providers.gemini') },
|
{ value: PROVIDER_GEMINI, label: t('monitorCommon.providers.gemini') },
|
||||||
|
{ value: PROVIDER_KIRO, label: t('monitorCommon.providers.kiro') },
|
||||||
])
|
])
|
||||||
|
|
||||||
// Clear api_key whenever provider changes to avoid cross-provider key mismatch.
|
// Clear api_key whenever provider changes to avoid cross-provider key mismatch.
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ import {
|
|||||||
PROVIDER_ANTHROPIC,
|
PROVIDER_ANTHROPIC,
|
||||||
PROVIDER_OPENAI,
|
PROVIDER_OPENAI,
|
||||||
PROVIDER_GEMINI,
|
PROVIDER_GEMINI,
|
||||||
|
PROVIDER_KIRO,
|
||||||
} from '@/constants/channelMonitor'
|
} from '@/constants/channelMonitor'
|
||||||
|
|
||||||
const props = defineProps<{ show: boolean }>()
|
const props = defineProps<{ show: boolean }>()
|
||||||
@@ -238,6 +239,7 @@ const providerTabs = computed<{ value: Provider; label: string }[]>(() => [
|
|||||||
{ value: PROVIDER_ANTHROPIC, label: t('monitorCommon.providers.anthropic') },
|
{ value: PROVIDER_ANTHROPIC, label: t('monitorCommon.providers.anthropic') },
|
||||||
{ value: PROVIDER_OPENAI, label: t('monitorCommon.providers.openai') },
|
{ value: PROVIDER_OPENAI, label: t('monitorCommon.providers.openai') },
|
||||||
{ value: PROVIDER_GEMINI, label: t('monitorCommon.providers.gemini') },
|
{ value: PROVIDER_GEMINI, label: t('monitorCommon.providers.gemini') },
|
||||||
|
{ value: PROVIDER_KIRO, label: t('monitorCommon.providers.kiro') },
|
||||||
])
|
])
|
||||||
|
|
||||||
const activeProvider = ref<Provider>(PROVIDER_ANTHROPIC)
|
const activeProvider = ref<Provider>(PROVIDER_ANTHROPIC)
|
||||||
@@ -253,6 +255,7 @@ const countByProvider = computed<Record<Provider, number>>(() => {
|
|||||||
anthropic: 0,
|
anthropic: 0,
|
||||||
openai: 0,
|
openai: 0,
|
||||||
gemini: 0,
|
gemini: 0,
|
||||||
|
kiro: 0,
|
||||||
}
|
}
|
||||||
for (const t of templates.value) out[t.provider]++
|
for (const t of templates.value) out[t.provider]++
|
||||||
return out
|
return out
|
||||||
|
|||||||
@@ -51,6 +51,11 @@ const PROVIDER_ICONS: Record<Provider, IconData> = {
|
|||||||
'M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z',
|
'M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
kiro: {
|
||||||
|
paths: [
|
||||||
|
'M12 2.25a.75.75 0 01.673.418l2.247 4.555 5.027.73a.75.75 0 01.416 1.279l-3.637 3.546.859 5.006a.75.75 0 01-1.088.79L12 16.21l-4.497 2.364a.75.75 0 01-1.088-.79l.859-5.006-3.637-3.546a.75.75 0 01.416-1.28l5.027-.729 2.247-4.555A.75.75 0 0112 2.25zm0 2.445l-1.75 3.547a.75.75 0 01-.565.41l-3.915.568 2.833 2.761a.75.75 0 01.216.664l-.669 3.899 3.502-1.841a.75.75 0 01.696 0l3.502 1.841-.669-3.899a.75.75 0 01.216-.664L18.23 9.22l-3.915-.568a.75.75 0 01-.565-.41L12 4.695z',
|
||||||
|
],
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<{
|
const props = withDefaults(defineProps<{
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
PROVIDER_OPENAI,
|
PROVIDER_OPENAI,
|
||||||
PROVIDER_ANTHROPIC,
|
PROVIDER_ANTHROPIC,
|
||||||
PROVIDER_GEMINI,
|
PROVIDER_GEMINI,
|
||||||
|
PROVIDER_KIRO,
|
||||||
STATUS_OPERATIONAL,
|
STATUS_OPERATIONAL,
|
||||||
STATUS_DEGRADED,
|
STATUS_DEGRADED,
|
||||||
STATUS_FAILED,
|
STATUS_FAILED,
|
||||||
@@ -57,7 +58,7 @@ export function useChannelMonitorFormat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function providerLabel(p: Provider | string): string {
|
function providerLabel(p: Provider | string): string {
|
||||||
if (p === PROVIDER_OPENAI || p === PROVIDER_ANTHROPIC || p === PROVIDER_GEMINI) {
|
if (p === PROVIDER_OPENAI || p === PROVIDER_ANTHROPIC || p === PROVIDER_GEMINI || p === PROVIDER_KIRO) {
|
||||||
return t(`monitorCommon.providers.${p}`)
|
return t(`monitorCommon.providers.${p}`)
|
||||||
}
|
}
|
||||||
return p || '-'
|
return p || '-'
|
||||||
@@ -71,6 +72,8 @@ export function useChannelMonitorFormat() {
|
|||||||
return 'bg-orange-100 text-orange-700 dark:bg-orange-500/15 dark:text-orange-300'
|
return 'bg-orange-100 text-orange-700 dark:bg-orange-500/15 dark:text-orange-300'
|
||||||
case PROVIDER_GEMINI:
|
case PROVIDER_GEMINI:
|
||||||
return 'bg-sky-100 text-sky-700 dark:bg-sky-500/15 dark:text-sky-300'
|
return 'bg-sky-100 text-sky-700 dark:bg-sky-500/15 dark:text-sky-300'
|
||||||
|
case PROVIDER_KIRO:
|
||||||
|
return 'bg-violet-100 text-violet-700 dark:bg-violet-500/15 dark:text-violet-300'
|
||||||
default:
|
default:
|
||||||
return NEUTRAL_BADGE
|
return NEUTRAL_BADGE
|
||||||
}
|
}
|
||||||
@@ -95,6 +98,10 @@ export function useChannelMonitorFormat() {
|
|||||||
return active
|
return active
|
||||||
? 'border-sky-500 bg-sky-50 text-sky-700 dark:bg-sky-500/15 dark:text-sky-300 dark:border-sky-400'
|
? 'border-sky-500 bg-sky-50 text-sky-700 dark:bg-sky-500/15 dark:text-sky-300 dark:border-sky-400'
|
||||||
: 'border-gray-200 bg-white text-gray-600 hover:border-sky-300 hover:text-sky-700 dark:border-dark-700 dark:bg-dark-800 dark:text-gray-400 dark:hover:border-sky-500/50'
|
: 'border-gray-200 bg-white text-gray-600 hover:border-sky-300 hover:text-sky-700 dark:border-dark-700 dark:bg-dark-800 dark:text-gray-400 dark:hover:border-sky-500/50'
|
||||||
|
case PROVIDER_KIRO:
|
||||||
|
return active
|
||||||
|
? 'border-violet-500 bg-violet-50 text-violet-700 dark:bg-violet-500/15 dark:text-violet-300 dark:border-violet-400'
|
||||||
|
: 'border-gray-200 bg-white text-gray-600 hover:border-violet-300 hover:text-violet-700 dark:border-dark-700 dark:bg-dark-800 dark:text-gray-400 dark:hover:border-violet-500/50'
|
||||||
default:
|
default:
|
||||||
return active
|
return active
|
||||||
? 'border-gray-400 bg-gray-50 text-gray-700 dark:border-dark-500 dark:bg-dark-700 dark:text-gray-200'
|
? 'border-gray-400 bg-gray-50 text-gray-700 dark:border-dark-500 dark:bg-dark-700 dark:text-gray-200'
|
||||||
@@ -166,6 +173,8 @@ export function providerGradient(provider: string): string {
|
|||||||
return 'bg-gradient-to-br from-orange-50 to-amber-100 dark:from-orange-500/10 dark:to-amber-500/20'
|
return 'bg-gradient-to-br from-orange-50 to-amber-100 dark:from-orange-500/10 dark:to-amber-500/20'
|
||||||
case PROVIDER_GEMINI:
|
case PROVIDER_GEMINI:
|
||||||
return 'bg-gradient-to-br from-sky-50 to-indigo-100 dark:from-sky-500/10 dark:to-indigo-500/20'
|
return 'bg-gradient-to-br from-sky-50 to-indigo-100 dark:from-sky-500/10 dark:to-indigo-500/20'
|
||||||
|
case PROVIDER_KIRO:
|
||||||
|
return 'bg-gradient-to-br from-violet-50 to-fuchsia-100 dark:from-violet-500/10 dark:to-fuchsia-500/20'
|
||||||
default:
|
default:
|
||||||
return 'bg-gradient-to-br from-gray-100 to-gray-200 dark:from-dark-700 dark:to-dark-600'
|
return 'bg-gradient-to-br from-gray-100 to-gray-200 dark:from-dark-700 dark:to-dark-600'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,11 +12,13 @@ import type { Provider, MonitorStatus } from '@/api/admin/channelMonitor'
|
|||||||
export const PROVIDER_OPENAI: Provider = 'openai'
|
export const PROVIDER_OPENAI: Provider = 'openai'
|
||||||
export const PROVIDER_ANTHROPIC: Provider = 'anthropic'
|
export const PROVIDER_ANTHROPIC: Provider = 'anthropic'
|
||||||
export const PROVIDER_GEMINI: Provider = 'gemini'
|
export const PROVIDER_GEMINI: Provider = 'gemini'
|
||||||
|
export const PROVIDER_KIRO: Provider = 'kiro'
|
||||||
|
|
||||||
export const PROVIDERS: readonly Provider[] = [
|
export const PROVIDERS: readonly Provider[] = [
|
||||||
PROVIDER_OPENAI,
|
PROVIDER_OPENAI,
|
||||||
PROVIDER_ANTHROPIC,
|
PROVIDER_ANTHROPIC,
|
||||||
PROVIDER_GEMINI,
|
PROVIDER_GEMINI,
|
||||||
|
PROVIDER_KIRO,
|
||||||
]
|
]
|
||||||
|
|
||||||
export const STATUS_OPERATIONAL: MonitorStatus = 'operational'
|
export const STATUS_OPERATIONAL: MonitorStatus = 'operational'
|
||||||
|
|||||||
@@ -891,7 +891,8 @@ export default {
|
|||||||
providers: {
|
providers: {
|
||||||
openai: 'OpenAI',
|
openai: 'OpenAI',
|
||||||
anthropic: 'Anthropic',
|
anthropic: 'Anthropic',
|
||||||
gemini: 'Gemini'
|
gemini: 'Gemini',
|
||||||
|
kiro: 'Kiro'
|
||||||
},
|
},
|
||||||
extraModelsHeader: 'Extra Models',
|
extraModelsHeader: 'Extra Models',
|
||||||
extraModelsEmpty: 'No extra models',
|
extraModelsEmpty: 'No extra models',
|
||||||
|
|||||||
@@ -895,7 +895,8 @@ export default {
|
|||||||
providers: {
|
providers: {
|
||||||
openai: 'OpenAI',
|
openai: 'OpenAI',
|
||||||
anthropic: 'Anthropic',
|
anthropic: 'Anthropic',
|
||||||
gemini: 'Gemini'
|
gemini: 'Gemini',
|
||||||
|
kiro: 'Kiro'
|
||||||
},
|
},
|
||||||
extraModelsHeader: '附加模型',
|
extraModelsHeader: '附加模型',
|
||||||
extraModelsEmpty: '无附加模型',
|
extraModelsEmpty: '无附加模型',
|
||||||
|
|||||||
Reference in New Issue
Block a user